0
# Transport and Utilities
1
2
Supporting utilities including Timer for reconnection backoff, LongPoll fallback transport, and Ajax helper for HTTP requests when WebSocket is unavailable.
3
4
## Capabilities
5
6
### Timer
7
8
Utility class for managing reconnection timing with configurable backoff strategies.
9
10
```typescript { .api }
11
/**
12
* Creates a Timer for managing reconnection backoff
13
* @param callback - Function to call when timer fires
14
* @param timerCalc - Function to calculate timeout based on attempt count
15
*/
16
constructor(callback: () => void | Promise<void>, timerCalc: (tries: number) => number);
17
18
/**
19
* Reset timer state (clears current timeout and resets attempt counter)
20
*/
21
reset(): void;
22
23
/**
24
* Schedule the next timeout based on current attempt count
25
*/
26
scheduleTimeout(): void;
27
```
28
29
**Usage Example:**
30
31
```typescript
32
import { Timer } from "phoenix";
33
34
// Basic exponential backoff
35
const reconnectTimer = new Timer(
36
() => {
37
console.log("Attempting to reconnect...");
38
socket.connect();
39
},
40
(tries) => {
41
// Exponential backoff: 1s, 2s, 4s, 8s, max 30s
42
return Math.min(1000 * Math.pow(2, tries - 1), 30000);
43
}
44
);
45
46
// Start the timer
47
reconnectTimer.scheduleTimeout();
48
49
// Reset on successful connection
50
socket.onOpen(() => {
51
reconnectTimer.reset();
52
});
53
54
// Advanced backoff with jitter
55
const advancedTimer = new Timer(
56
async () => {
57
try {
58
await attemptReconnection();
59
} catch (error) {
60
console.error("Reconnection failed:", error);
61
}
62
},
63
(tries) => {
64
const baseDelay = [1000, 2000, 5000, 10000, 30000][tries - 1] || 30000;
65
const jitter = Math.random() * 1000; // Add up to 1s jitter
66
return baseDelay + jitter;
67
}
68
);
69
```
70
71
### LongPoll
72
73
Fallback transport mechanism for long polling when WebSocket connections are unavailable.
74
75
```typescript { .api }
76
/**
77
* Creates a LongPoll transport instance
78
* @param endPoint - Long polling endpoint URL
79
*/
80
constructor(endPoint: string);
81
82
/**
83
* Normalize endpoint URL for long polling
84
* @param endPoint - Raw endpoint URL
85
* @returns Normalized endpoint URL
86
*/
87
normalizeEndpoint(endPoint: string): string;
88
89
/**
90
* Get the formatted endpoint URL
91
* @returns Complete long polling URL
92
*/
93
endpointURL(): string;
94
95
/**
96
* Close current connection and retry
97
*/
98
closeAndRetry(): void;
99
100
/**
101
* Handle timeout events
102
*/
103
ontimeout(): void;
104
105
/**
106
* Perform long polling request
107
*/
108
poll(): void;
109
110
/**
111
* Send data via long polling
112
* @param body - Data to send
113
*/
114
send(body: any): void;
115
116
/**
117
* Close the long polling connection
118
* @param code - Close code (optional)
119
* @param reason - Close reason (optional)
120
*/
121
close(code?: any, reason?: any): void;
122
```
123
124
**Usage Example:**
125
126
```typescript
127
import { LongPoll } from "phoenix";
128
129
// Create long poll transport
130
const longPoll = new LongPoll("/longpoll");
131
132
console.log("LongPoll URL:", longPoll.endpointURL());
133
134
// Send data
135
longPoll.send({
136
event: "heartbeat",
137
payload: {},
138
ref: "unique-ref"
139
});
140
141
// Start polling
142
longPoll.poll();
143
144
// Handle timeout
145
longPoll.ontimeout = () => {
146
console.log("Long poll timed out, retrying...");
147
longPoll.closeAndRetry();
148
};
149
150
// Close when done
151
longPoll.close(1000, "Normal closure");
152
153
// Custom transport implementation
154
class CustomLongPoll extends LongPoll {
155
constructor(endpoint: string) {
156
super(endpoint);
157
}
158
159
poll() {
160
console.log("Custom polling logic");
161
super.poll();
162
}
163
164
send(body: any) {
165
console.log("Sending via custom long poll:", body);
166
super.send(body);
167
}
168
}
169
```
170
171
### Ajax
172
173
HTTP request utility class for Phoenix communication with various request methods.
174
175
```typescript { .api }
176
/**
177
* Request state constants
178
*/
179
static states: { [state: string]: number };
180
181
/**
182
* Make HTTP request
183
* @param method - HTTP method (GET, POST, PUT, DELETE, etc.)
184
* @param endPoint - Request endpoint URL
185
* @param accept - Accept header value
186
* @param body - Request body
187
* @param timeout - Request timeout in milliseconds (optional)
188
* @param ontimeout - Timeout callback (optional)
189
* @param callback - Response callback (optional)
190
*/
191
static request(
192
method: string,
193
endPoint: string,
194
accept: string,
195
body: any,
196
timeout?: number,
197
ontimeout?: any,
198
callback?: (response?: any) => void | Promise<void>
199
): void;
200
201
/**
202
* Cross-domain request handler
203
* @param req - Request object
204
* @param method - HTTP method
205
* @param endPoint - Request endpoint
206
* @param body - Request body
207
* @param timeout - Request timeout (optional)
208
* @param ontimeout - Timeout callback (optional)
209
* @param callback - Response callback (optional)
210
*/
211
static xdomainRequest(
212
req: any,
213
method: string,
214
endPoint: string,
215
body: any,
216
timeout?: number,
217
ontimeout?: any,
218
callback?: (response?: any) => void | Promise<void>
219
): void;
220
221
/**
222
* XMLHttpRequest wrapper
223
* @param req - XMLHttpRequest object
224
* @param method - HTTP method
225
* @param endPoint - Request endpoint
226
* @param accept - Accept header value
227
* @param body - Request body
228
* @param timeout - Request timeout (optional)
229
* @param ontimeout - Timeout callback (optional)
230
* @param callback - Response callback (optional)
231
*/
232
static xhrRequest(
233
req: any,
234
method: string,
235
endPoint: string,
236
accept: string,
237
body: any,
238
timeout?: number,
239
ontimeout?: any,
240
callback?: (response?: any) => void | Promise<void>
241
): void;
242
243
/**
244
* Parse JSON response safely
245
* @param resp - Response string
246
* @returns Parsed JSON object
247
*/
248
static parseJSON(resp: string): JSON;
249
250
/**
251
* Serialize object to query string
252
* @param obj - Object to serialize
253
* @param parentKey - Parent key for nested serialization
254
* @returns URL-encoded query string
255
*/
256
static serialize(obj: any, parentKey: string): string;
257
258
/**
259
* Append parameters to URL
260
* @param url - Base URL
261
* @param params - Parameters to append
262
* @returns URL with appended parameters
263
*/
264
static appendParams(url: string, params: any): string;
265
```
266
267
**Usage Example:**
268
269
```typescript
270
import { Ajax } from "phoenix";
271
272
// Check request states
273
console.log("Ajax states:", Ajax.states);
274
275
// Basic GET request
276
Ajax.request(
277
"GET",
278
"/api/users",
279
"application/json",
280
null,
281
5000,
282
() => console.log("Request timed out"),
283
(response) => {
284
console.log("Users loaded:", response);
285
}
286
);
287
288
// POST request with JSON body
289
Ajax.request(
290
"POST",
291
"/api/users",
292
"application/json",
293
{ name: "John Doe", email: "john@example.com" },
294
10000,
295
() => console.log("Create user timed out"),
296
(response) => {
297
if (response.status === 201) {
298
console.log("User created:", response.data);
299
} else {
300
console.error("Failed to create user:", response.errors);
301
}
302
}
303
);
304
305
// Serialize object for query parameters
306
const params = { filter: "active", sort: "name", limit: 10 };
307
const queryString = Ajax.serialize(params, "");
308
console.log("Query string:", queryString); // "filter=active&sort=name&limit=10"
309
310
// Append parameters to URL
311
const baseUrl = "/api/posts";
312
const fullUrl = Ajax.appendParams(baseUrl, params);
313
console.log("Full URL:", fullUrl); // "/api/posts?filter=active&sort=name&limit=10"
314
315
// Parse JSON response
316
try {
317
const data = Ajax.parseJSON('{"success": true, "data": [1,2,3]}');
318
console.log("Parsed data:", data);
319
} catch (error) {
320
console.error("JSON parse error:", error);
321
}
322
```
323
324
## Advanced Usage Patterns
325
326
### Connection Recovery with Timer
327
328
```typescript
329
class ConnectionRecovery {
330
private reconnectTimer: Timer;
331
private maxAttempts: number = 10;
332
private attempts: number = 0;
333
334
constructor(private socket: Socket) {
335
this.reconnectTimer = new Timer(
336
() => this.attemptReconnection(),
337
(tries) => this.calculateBackoff(tries)
338
);
339
340
this.setupSocketHandlers();
341
}
342
343
private setupSocketHandlers() {
344
this.socket.onOpen(() => {
345
console.log("Connected successfully");
346
this.attempts = 0;
347
this.reconnectTimer.reset();
348
});
349
350
this.socket.onClose(() => {
351
console.log("Connection lost, starting recovery");
352
this.startRecovery();
353
});
354
355
this.socket.onError((error) => {
356
console.error("Socket error:", error);
357
this.startRecovery();
358
});
359
}
360
361
private startRecovery() {
362
if (this.attempts < this.maxAttempts) {
363
this.attempts++;
364
console.log(`Recovery attempt ${this.attempts}/${this.maxAttempts}`);
365
this.reconnectTimer.scheduleTimeout();
366
} else {
367
console.error("Max reconnection attempts exceeded");
368
this.onMaxAttemptsExceeded();
369
}
370
}
371
372
private attemptReconnection() {
373
console.log("Attempting to reconnect...");
374
this.socket.connect();
375
}
376
377
private calculateBackoff(tries: number): number {
378
// Progressive backoff: 1s, 2s, 5s, 10s, 30s, then 30s
379
const delays = [1000, 2000, 5000, 10000, 30000];
380
return delays[tries - 1] || 30000;
381
}
382
383
private onMaxAttemptsExceeded() {
384
// Show user notification or redirect
385
console.log("Connection could not be recovered");
386
387
// Optional: Show user a retry button
388
this.showRetryDialog();
389
}
390
391
private showRetryDialog() {
392
const retry = confirm("Connection lost. Would you like to try reconnecting?");
393
if (retry) {
394
this.attempts = 0;
395
this.startRecovery();
396
}
397
}
398
399
forceReconnect() {
400
this.attempts = 0;
401
this.reconnectTimer.reset();
402
this.attemptReconnection();
403
}
404
}
405
406
// Usage
407
const recovery = new ConnectionRecovery(socket);
408
```
409
410
### HTTP Fallback Service
411
412
```typescript
413
class HttpFallbackService {
414
private isUsingFallback = false;
415
416
constructor(private baseUrl: string) {}
417
418
async request(method: string, endpoint: string, data?: any, timeout = 5000): Promise<any> {
419
return new Promise((resolve, reject) => {
420
const fullUrl = `${this.baseUrl}${endpoint}`;
421
422
Ajax.request(
423
method,
424
fullUrl,
425
"application/json",
426
data ? JSON.stringify(data) : null,
427
timeout,
428
() => {
429
reject(new Error(`Request to ${endpoint} timed out`));
430
},
431
(response) => {
432
try {
433
const parsed = typeof response === 'string'
434
? Ajax.parseJSON(response)
435
: response;
436
resolve(parsed);
437
} catch (error) {
438
reject(new Error(`Failed to parse response: ${error}`));
439
}
440
}
441
);
442
});
443
}
444
445
async get(endpoint: string, params?: any, timeout?: number): Promise<any> {
446
const url = params ? Ajax.appendParams(endpoint, params) : endpoint;
447
return this.request("GET", url, null, timeout);
448
}
449
450
async post(endpoint: string, data: any, timeout?: number): Promise<any> {
451
return this.request("POST", endpoint, data, timeout);
452
}
453
454
async put(endpoint: string, data: any, timeout?: number): Promise<any> {
455
return this.request("PUT", endpoint, data, timeout);
456
}
457
458
async delete(endpoint: string, timeout?: number): Promise<any> {
459
return this.request("DELETE", endpoint, null, timeout);
460
}
461
462
enableFallback() {
463
this.isUsingFallback = true;
464
console.log("HTTP fallback enabled");
465
}
466
467
disableFallback() {
468
this.isUsingFallback = false;
469
console.log("HTTP fallback disabled");
470
}
471
}
472
473
// Usage
474
const httpService = new HttpFallbackService("/api");
475
476
// Use when WebSocket is unavailable
477
socket.onError(() => {
478
httpService.enableFallback();
479
});
480
481
socket.onOpen(() => {
482
httpService.disableFallback();
483
});
484
485
// Make HTTP requests
486
httpService.get("/users", { active: true })
487
.then(users => console.log("Users:", users))
488
.catch(error => console.error("Failed to fetch users:", error));
489
490
httpService.post("/messages", {
491
channel: "general",
492
content: "Hello via HTTP"
493
})
494
.then(message => console.log("Message sent:", message))
495
.catch(error => console.error("Failed to send message:", error));
496
```
497
498
### Hybrid Transport Manager
499
500
```typescript
501
class HybridTransportManager {
502
private socket: Socket;
503
private longPoll: LongPoll;
504
private httpService: HttpFallbackService;
505
private currentTransport: 'websocket' | 'longpoll' | 'http' = 'websocket';
506
507
constructor(wsEndpoint: string, httpBaseUrl: string) {
508
this.socket = new Socket(wsEndpoint, {
509
longPollFallbackMs: 2000,
510
transport: (endpoint) => new LongPoll(endpoint)
511
});
512
513
this.longPoll = new LongPoll(wsEndpoint + "/longpoll");
514
this.httpService = new HttpFallbackService(httpBaseUrl);
515
516
this.setupTransportFallback();
517
}
518
519
private setupTransportFallback() {
520
// Try WebSocket first
521
this.socket.onOpen(() => {
522
this.currentTransport = 'websocket';
523
console.log("Using WebSocket transport");
524
});
525
526
// Fallback to long polling
527
this.socket.onError(() => {
528
if (this.currentTransport === 'websocket') {
529
console.log("WebSocket failed, trying long polling");
530
this.currentTransport = 'longpoll';
531
this.startLongPolling();
532
} else if (this.currentTransport === 'longpoll') {
533
console.log("Long polling failed, using HTTP");
534
this.currentTransport = 'http';
535
}
536
});
537
}
538
539
private startLongPolling() {
540
this.longPoll.poll();
541
542
this.longPoll.ontimeout = () => {
543
console.log("Long poll timeout, falling back to HTTP");
544
this.currentTransport = 'http';
545
};
546
}
547
548
async sendMessage(channel: string, event: string, payload: any): Promise<any> {
549
switch (this.currentTransport) {
550
case 'websocket':
551
return this.sendViaWebSocket(channel, event, payload);
552
553
case 'longpoll':
554
return this.sendViaLongPoll(event, payload);
555
556
case 'http':
557
return this.sendViaHttp(channel, event, payload);
558
}
559
}
560
561
private sendViaWebSocket(channel: string, event: string, payload: any): Promise<any> {
562
return new Promise((resolve, reject) => {
563
const ch = this.socket.channel(channel);
564
ch.push(event, payload)
565
.receive("ok", resolve)
566
.receive("error", reject);
567
});
568
}
569
570
private sendViaLongPoll(event: string, payload: any): Promise<any> {
571
return new Promise((resolve, reject) => {
572
this.longPoll.send({
573
event,
574
payload,
575
ref: Date.now().toString()
576
});
577
578
// Simplified - in reality you'd handle the response
579
setTimeout(() => resolve({ status: "sent" }), 100);
580
});
581
}
582
583
private sendViaHttp(channel: string, event: string, payload: any): Promise<any> {
584
return this.httpService.post(`/channels/${channel}/messages`, {
585
event,
586
payload
587
});
588
}
589
590
getCurrentTransport(): string {
591
return this.currentTransport;
592
}
593
594
connect() {
595
this.socket.connect();
596
}
597
598
disconnect() {
599
this.socket.disconnect();
600
this.longPoll.close();
601
}
602
}
603
604
// Usage
605
const transportManager = new HybridTransportManager("/socket", "/api");
606
607
transportManager.connect();
608
609
// Send message - automatically uses best available transport
610
transportManager.sendMessage("room:lobby", "new_message", {
611
body: "Hello from hybrid transport!"
612
})
613
.then(response => console.log("Message sent:", response))
614
.catch(error => console.error("Send failed:", error));
615
616
console.log("Current transport:", transportManager.getCurrentTransport());
617
```