@ioka-technologies/asyncapi-ts-client-template 0.0.35 → 0.0.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/common/index.js +4887 -158
- package/package.json +5 -1
- package/template/src/client.ts.js +42 -10
- package/template/src/index.ts.js +2 -1
- package/template/src/runtime/retry/presets.ts.js +52 -0
- package/template/src/runtime/transports/http.ts.js +28 -0
- package/template/src/runtime/transports/websocket.ts.js +286 -52
- package/template/src/runtime/types.ts.js +52 -0
|
@@ -11,7 +11,7 @@ export default function ({ asyncapi, params }) {
|
|
|
11
11
|
return (
|
|
12
12
|
<File name="websocket.ts">
|
|
13
13
|
{`import { v4 as uuidv4 } from 'uuid';
|
|
14
|
-
import { Transport, TransportConfig, RequestOptions, ResponseHandler, MessageEnvelope, EnvelopeCallback } from '../types';
|
|
14
|
+
import { Transport, TransportConfig, RequestOptions, ResponseHandler, MessageEnvelope, EnvelopeCallback, ReconnectConfig } from '../types';
|
|
15
15
|
import { TransportError, ConnectionError, TimeoutError } from '../errors';
|
|
16
16
|
import { generateAuthHeaders, generateAuthQueryParams, hasAuthCredentials, AuthError, UnauthorizedError, AuthCredentials } from '../auth';
|
|
17
17
|
import { createRetryManager, getRetryConfig } from '../retry';
|
|
@@ -54,74 +54,212 @@ export class WebSocketTransport implements Transport {
|
|
|
54
54
|
private subscriptions: Map<string, Set<EnvelopeCallback>> = new Map();
|
|
55
55
|
private operationSubscriptions: Map<string, Set<(payload: any) => void>> = new Map();
|
|
56
56
|
private channelOperations: Map<string, string> = new Map(); // Track operation for each channel
|
|
57
|
+
|
|
58
|
+
// Reconnect state
|
|
59
|
+
private intentionalDisconnect = false;
|
|
60
|
+
private _isConnected = false;
|
|
57
61
|
private reconnectAttempts = 0;
|
|
58
|
-
private
|
|
59
|
-
private
|
|
62
|
+
private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
|
63
|
+
private connectPromise: Promise<void> | null = null;
|
|
64
|
+
private pendingConnectionReject: ((error: Error) => void) | null = null;
|
|
65
|
+
private connectionGeneration = 0;
|
|
66
|
+
private hasConnectedSuccessfully = false;
|
|
67
|
+
private reconnectConfig: Required<ReconnectConfig>;
|
|
68
|
+
|
|
69
|
+
// Event listener registries (for application-layer listeners beyond config callbacks)
|
|
70
|
+
private reconnectedListeners = new Set<() => void>();
|
|
71
|
+
private disconnectedListeners = new Set<(reason: string) => void>();
|
|
60
72
|
|
|
61
73
|
constructor(config: TransportConfig) {
|
|
62
74
|
this.config = config;
|
|
75
|
+
this.reconnectConfig = {
|
|
76
|
+
enabled: config.reconnect?.enabled ?? true,
|
|
77
|
+
maxAttempts: config.reconnect?.maxAttempts ?? 0, // 0 = unlimited
|
|
78
|
+
baseDelay: config.reconnect?.baseDelay ?? 1000,
|
|
79
|
+
maxDelay: config.reconnect?.maxDelay ?? 30000,
|
|
80
|
+
backoffMultiplier: config.reconnect?.backoffMultiplier ?? 2,
|
|
81
|
+
jitter: config.reconnect?.jitter ?? true,
|
|
82
|
+
};
|
|
63
83
|
// WebSocketImpl will be set during connect()
|
|
64
84
|
}
|
|
65
85
|
|
|
66
|
-
|
|
86
|
+
/**
|
|
87
|
+
* Check if the transport is currently connected
|
|
88
|
+
*/
|
|
89
|
+
isConnected(): boolean {
|
|
90
|
+
return this._isConnected && this.ws !== null && this.ws.readyState === this.ws.OPEN;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Register a callback for when the transport reconnects after a disconnect.
|
|
95
|
+
* Returns an unsubscribe function.
|
|
96
|
+
*/
|
|
97
|
+
onReconnected(callback: () => void): () => void {
|
|
98
|
+
this.reconnectedListeners.add(callback);
|
|
99
|
+
return () => { this.reconnectedListeners.delete(callback); };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Register a callback for when the transport disconnects unexpectedly.
|
|
104
|
+
* Returns an unsubscribe function.
|
|
105
|
+
*/
|
|
106
|
+
onDisconnected(callback: (reason: string) => void): () => void {
|
|
107
|
+
this.disconnectedListeners.add(callback);
|
|
108
|
+
return () => { this.disconnectedListeners.delete(callback); };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
connect(): Promise<void> {
|
|
112
|
+
if (this.isConnected()) return Promise.resolve();
|
|
113
|
+
if (this.connectPromise) return this.connectPromise;
|
|
114
|
+
this.intentionalDisconnect = false;
|
|
115
|
+
this.connectPromise = this.createConnection().finally(() => {
|
|
116
|
+
this.connectPromise = null;
|
|
117
|
+
if (!this.isConnected()) this.attemptReconnect();
|
|
118
|
+
});
|
|
119
|
+
return this.connectPromise;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private async createConnection(): Promise<void> {
|
|
67
123
|
if (!this.WebSocketImpl) {
|
|
68
124
|
this.WebSocketImpl = await getWebSocketImpl();
|
|
69
125
|
}
|
|
70
126
|
|
|
71
127
|
return new Promise((resolve, reject) => {
|
|
128
|
+
let settled = false;
|
|
129
|
+
this.pendingConnectionReject = reject;
|
|
130
|
+
const generation = ++this.connectionGeneration;
|
|
131
|
+
const fail = (error: Error): void => {
|
|
132
|
+
if (!settled) { settled = true; reject(error); }
|
|
133
|
+
this.pendingConnectionReject = null;
|
|
134
|
+
};
|
|
72
135
|
try {
|
|
73
|
-
|
|
136
|
+
const socket = new this.WebSocketImpl(this.config.url) as WebSocketLike;
|
|
137
|
+
this.ws = socket;
|
|
138
|
+
const isCurrent = (): boolean => generation === this.connectionGeneration && this.ws === socket;
|
|
74
139
|
|
|
75
140
|
// Handle both browser and Node.js WebSocket APIs
|
|
76
|
-
if (typeof window !== 'undefined' &&
|
|
141
|
+
if (typeof window !== 'undefined' && socket.addEventListener) {
|
|
77
142
|
// Browser WebSocket API
|
|
78
|
-
|
|
143
|
+
socket.addEventListener('open', () => {
|
|
144
|
+
if (!isCurrent()) return;
|
|
145
|
+
settled = true;
|
|
146
|
+
this.pendingConnectionReject = null;
|
|
147
|
+
const wasReconnect = this.hasConnectedSuccessfully;
|
|
148
|
+
this.hasConnectedSuccessfully = true;
|
|
79
149
|
this.reconnectAttempts = 0;
|
|
150
|
+
this._isConnected = true;
|
|
151
|
+
if (wasReconnect) {
|
|
152
|
+
this.config.connectionCallbacks?.onReconnected?.();
|
|
153
|
+
this.reconnectedListeners.forEach(cb => cb());
|
|
154
|
+
this.resubscribeAll();
|
|
155
|
+
} else {
|
|
156
|
+
this.config.connectionCallbacks?.onConnected?.();
|
|
157
|
+
}
|
|
80
158
|
resolve();
|
|
81
159
|
});
|
|
82
160
|
|
|
83
|
-
|
|
84
|
-
this.handleMessage(event.data);
|
|
161
|
+
socket.addEventListener('message', (event: MessageEvent) => {
|
|
162
|
+
if (isCurrent()) this.handleMessage(event.data);
|
|
85
163
|
});
|
|
86
164
|
|
|
87
|
-
|
|
165
|
+
socket.addEventListener('close', () => {
|
|
166
|
+
if (!isCurrent()) return;
|
|
167
|
+
this._isConnected = false;
|
|
168
|
+
this.ws = null;
|
|
169
|
+
fail(new ConnectionError('WebSocket connection closed'));
|
|
88
170
|
this.handleDisconnect();
|
|
89
171
|
});
|
|
90
172
|
|
|
91
|
-
|
|
92
|
-
|
|
173
|
+
socket.addEventListener('error', (event: Event) => {
|
|
174
|
+
if (!isCurrent()) return;
|
|
175
|
+
if (!this._isConnected) {
|
|
176
|
+
// Error during initial connection
|
|
177
|
+
fail(new ConnectionError(\`WebSocket connection failed: \${event.type}\`));
|
|
178
|
+
} else {
|
|
179
|
+
// Error on established connection — handleDisconnect will be called by 'close'
|
|
180
|
+
console.error('WebSocket error on established connection:', event);
|
|
181
|
+
this.config.connectionCallbacks?.onError?.(
|
|
182
|
+
new ConnectionError('WebSocket error')
|
|
183
|
+
);
|
|
184
|
+
}
|
|
93
185
|
});
|
|
94
|
-
} else if (
|
|
186
|
+
} else if (socket.on) {
|
|
95
187
|
// Node.js ws library API
|
|
96
|
-
|
|
188
|
+
socket.on('open', () => {
|
|
189
|
+
if (!isCurrent()) return;
|
|
190
|
+
settled = true;
|
|
191
|
+
this.pendingConnectionReject = null;
|
|
192
|
+
const wasReconnect = this.hasConnectedSuccessfully;
|
|
193
|
+
this.hasConnectedSuccessfully = true;
|
|
97
194
|
this.reconnectAttempts = 0;
|
|
195
|
+
this._isConnected = true;
|
|
196
|
+
if (wasReconnect) {
|
|
197
|
+
this.config.connectionCallbacks?.onReconnected?.();
|
|
198
|
+
this.reconnectedListeners.forEach(cb => cb());
|
|
199
|
+
this.resubscribeAll();
|
|
200
|
+
} else {
|
|
201
|
+
this.config.connectionCallbacks?.onConnected?.();
|
|
202
|
+
}
|
|
98
203
|
resolve();
|
|
99
204
|
});
|
|
100
205
|
|
|
101
|
-
|
|
102
|
-
this.handleMessage(data.toString());
|
|
206
|
+
socket.on('message', (data: any) => {
|
|
207
|
+
if (isCurrent()) this.handleMessage(data.toString());
|
|
103
208
|
});
|
|
104
209
|
|
|
105
|
-
|
|
210
|
+
socket.on('close', () => {
|
|
211
|
+
if (!isCurrent()) return;
|
|
212
|
+
this._isConnected = false;
|
|
213
|
+
this.ws = null;
|
|
214
|
+
fail(new ConnectionError('WebSocket connection closed'));
|
|
106
215
|
this.handleDisconnect();
|
|
107
216
|
});
|
|
108
217
|
|
|
109
|
-
|
|
110
|
-
|
|
218
|
+
socket.on('error', (error: any) => {
|
|
219
|
+
if (!isCurrent()) return;
|
|
220
|
+
if (!this._isConnected) {
|
|
221
|
+
// Error during initial connection
|
|
222
|
+
fail(new ConnectionError(\`WebSocket connection failed: \${error.message}\`));
|
|
223
|
+
} else {
|
|
224
|
+
// Error on established connection — handleDisconnect will be called by 'close'
|
|
225
|
+
console.error('WebSocket error on established connection:', error);
|
|
226
|
+
this.config.connectionCallbacks?.onError?.(
|
|
227
|
+
new ConnectionError(\`WebSocket error: \${error.message}\`)
|
|
228
|
+
);
|
|
229
|
+
}
|
|
111
230
|
});
|
|
112
231
|
} else {
|
|
113
|
-
|
|
232
|
+
fail(new ConnectionError('WebSocket implementation does not support required event handling'));
|
|
114
233
|
}
|
|
115
234
|
} catch (error: any) {
|
|
116
|
-
|
|
235
|
+
fail(new ConnectionError(\`Failed to create WebSocket: \${error.message}\`));
|
|
117
236
|
}
|
|
118
237
|
});
|
|
119
238
|
}
|
|
120
239
|
|
|
121
240
|
async disconnect(): Promise<void> {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
241
|
+
this.intentionalDisconnect = true;
|
|
242
|
+
this._isConnected = false;
|
|
243
|
+
|
|
244
|
+
// Cancel any pending reconnect timer
|
|
245
|
+
if (this.reconnectTimer) {
|
|
246
|
+
clearTimeout(this.reconnectTimer);
|
|
247
|
+
this.reconnectTimer = null;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
this.connectionGeneration++;
|
|
251
|
+
if (this.pendingConnectionReject) {
|
|
252
|
+
this.pendingConnectionReject(new ConnectionError('Connection closed intentionally'));
|
|
253
|
+
this.pendingConnectionReject = null;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Reject all pending requests
|
|
257
|
+
this.rejectPendingRequests('Connection closed intentionally');
|
|
258
|
+
|
|
259
|
+
const socket = this.ws;
|
|
260
|
+
this.ws = null;
|
|
261
|
+
if (socket) {
|
|
262
|
+
socket.close();
|
|
125
263
|
}
|
|
126
264
|
}
|
|
127
265
|
|
|
@@ -203,7 +341,7 @@ export class WebSocketTransport implements Transport {
|
|
|
203
341
|
const authHeaders = this.config.auth ? generateAuthHeaders(this.config.auth) : {};
|
|
204
342
|
|
|
205
343
|
const subscribeEnvelope: MessageEnvelope = {
|
|
206
|
-
operation: operation, //
|
|
344
|
+
operation: operation, // Use the actual operation name
|
|
207
345
|
channel,
|
|
208
346
|
payload: { channel, operation },
|
|
209
347
|
timestamp: new Date().toISOString(),
|
|
@@ -244,7 +382,7 @@ export class WebSocketTransport implements Transport {
|
|
|
244
382
|
* This provides operation-based filtering on the client side
|
|
245
383
|
*/
|
|
246
384
|
subscribeToOperation(channel: string, operation: string, callback: (payload: any) => void): () => void {
|
|
247
|
-
const operationKey = \`\${channel}
|
|
385
|
+
const operationKey = \`\${channel}::\${operation}\`;
|
|
248
386
|
|
|
249
387
|
if (!this.operationSubscriptions.has(operationKey)) {
|
|
250
388
|
this.operationSubscriptions.set(operationKey, new Set());
|
|
@@ -265,7 +403,9 @@ export class WebSocketTransport implements Transport {
|
|
|
265
403
|
// Generate auth headers if credentials are available
|
|
266
404
|
const authHeaders = this.config.auth ? generateAuthHeaders(this.config.auth) : {};
|
|
267
405
|
|
|
406
|
+
const requestId = uuidv4();
|
|
268
407
|
const subscribeEnvelope: MessageEnvelope = {
|
|
408
|
+
id: requestId,
|
|
269
409
|
operation: operation,
|
|
270
410
|
channel,
|
|
271
411
|
payload: { channel, operation },
|
|
@@ -293,7 +433,7 @@ export class WebSocketTransport implements Transport {
|
|
|
293
433
|
return;
|
|
294
434
|
}
|
|
295
435
|
|
|
296
|
-
const operationKey = \`\${envelope.channel}
|
|
436
|
+
const operationKey = \`\${envelope.channel}::\${envelope.operation}\`;
|
|
297
437
|
const operationCallbacks = this.operationSubscriptions.get(operationKey);
|
|
298
438
|
|
|
299
439
|
if (operationCallbacks) {
|
|
@@ -359,36 +499,130 @@ export class WebSocketTransport implements Transport {
|
|
|
359
499
|
}
|
|
360
500
|
|
|
361
501
|
private handleDisconnect(): void {
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
502
|
+
const wasConnected = this._isConnected;
|
|
503
|
+
this._isConnected = false;
|
|
504
|
+
|
|
505
|
+
// Reject all pending request/response handlers immediately
|
|
506
|
+
this.rejectPendingRequests('WebSocket connection lost');
|
|
507
|
+
|
|
508
|
+
// Don't reconnect if this was intentional
|
|
509
|
+
if (this.intentionalDisconnect) return;
|
|
510
|
+
|
|
511
|
+
// Notify listeners of unexpected disconnect
|
|
512
|
+
const reason = 'WebSocket connection lost unexpectedly';
|
|
513
|
+
this.config.connectionCallbacks?.onDisconnected?.(reason);
|
|
514
|
+
this.disconnectedListeners.forEach(cb => {
|
|
515
|
+
try { cb(reason); } catch (e) { console.error('Error in disconnect listener:', e); }
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
// Start reconnection if enabled
|
|
519
|
+
if (!this.reconnectConfig.enabled) {
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
this.attemptReconnect();
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
private attemptReconnect(): void {
|
|
527
|
+
if (this.intentionalDisconnect || this.reconnectTimer || this.connectPromise) {
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
// Check if we've exceeded max attempts (0 = unlimited)
|
|
531
|
+
if (this.reconnectConfig.maxAttempts > 0 &&
|
|
532
|
+
this.reconnectAttempts >= this.reconnectConfig.maxAttempts) {
|
|
533
|
+
console.error(\`WebSocket reconnect failed after \${this.reconnectAttempts} attempts\`);
|
|
534
|
+
this.config.connectionCallbacks?.onReconnectFailed?.(this.reconnectAttempts);
|
|
535
|
+
return;
|
|
372
536
|
}
|
|
537
|
+
|
|
538
|
+
this.reconnectAttempts++;
|
|
539
|
+
const delay = this.calculateReconnectDelay(this.reconnectAttempts);
|
|
540
|
+
|
|
541
|
+
console.log(\`WebSocket reconnecting in \${delay}ms (attempt \${this.reconnectAttempts})\`);
|
|
542
|
+
this.config.connectionCallbacks?.onReconnecting?.(this.reconnectAttempts, delay);
|
|
543
|
+
|
|
544
|
+
this.reconnectTimer = setTimeout(async () => {
|
|
545
|
+
this.reconnectTimer = null;
|
|
546
|
+
try {
|
|
547
|
+
// Reset the intentional disconnect flag before reconnecting
|
|
548
|
+
this.intentionalDisconnect = false;
|
|
549
|
+
await this.connect();
|
|
550
|
+
|
|
551
|
+
} catch (error) {
|
|
552
|
+
// Try again
|
|
553
|
+
this.attemptReconnect();
|
|
554
|
+
}
|
|
555
|
+
}, delay);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
private calculateReconnectDelay(attempt: number): number {
|
|
559
|
+
let delay = this.reconnectConfig.baseDelay *
|
|
560
|
+
Math.pow(this.reconnectConfig.backoffMultiplier, attempt - 1);
|
|
561
|
+
|
|
562
|
+
// Cap at max delay
|
|
563
|
+
delay = Math.min(delay, this.reconnectConfig.maxDelay);
|
|
564
|
+
|
|
565
|
+
// Add jitter
|
|
566
|
+
if (this.reconnectConfig.jitter) {
|
|
567
|
+
delay = delay * (0.5 + Math.random() * 0.5);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
return Math.floor(delay);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
private rejectPendingRequests(reason: string): void {
|
|
574
|
+
for (const [requestId, handler] of this.responseHandlers) {
|
|
575
|
+
handler.reject(new ConnectionError(reason));
|
|
576
|
+
}
|
|
577
|
+
this.responseHandlers.clear();
|
|
373
578
|
}
|
|
374
579
|
|
|
375
580
|
private resubscribeAll(): void {
|
|
376
|
-
if (this.ws
|
|
377
|
-
|
|
378
|
-
|
|
581
|
+
if (!this.ws || this.ws.readyState !== this.ws.OPEN) {
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
379
584
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
585
|
+
const authHeaders = this.config.auth ? generateAuthHeaders(this.config.auth) : {};
|
|
586
|
+
|
|
587
|
+
// Collect all unique channel+operation pairs that need resubscription
|
|
588
|
+
const subscriptionsToRestore = new Set<string>();
|
|
589
|
+
|
|
590
|
+
// From channel-based subscriptions
|
|
591
|
+
for (const channel of this.subscriptions.keys()) {
|
|
592
|
+
const operation = this.channelOperations.get(channel);
|
|
593
|
+
if (operation) {
|
|
594
|
+
subscriptionsToRestore.add(\`\${channel}::\${operation}\`);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// From operation-based subscriptions (these may have additional operations
|
|
599
|
+
// on channels already tracked above)
|
|
600
|
+
for (const operationKey of this.operationSubscriptions.keys()) {
|
|
601
|
+
// operationKey format is "channel::operation"
|
|
602
|
+
const separatorIndex = operationKey.indexOf('::');
|
|
603
|
+
if (separatorIndex !== -1) {
|
|
604
|
+
subscriptionsToRestore.add(operationKey);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// Send subscribe messages for all unique channel+operation pairs
|
|
609
|
+
for (const key of subscriptionsToRestore) {
|
|
610
|
+
const separatorIndex = key.indexOf('::');
|
|
611
|
+
const channel = key.substring(0, separatorIndex);
|
|
612
|
+
const operation = key.substring(separatorIndex + 2);
|
|
613
|
+
try {
|
|
614
|
+
const subscribeEnvelope: MessageEnvelope = {
|
|
615
|
+
id: uuidv4(),
|
|
616
|
+
operation: operation,
|
|
617
|
+
channel: channel,
|
|
618
|
+
payload: { channel, operation },
|
|
619
|
+
timestamp: new Date().toISOString(),
|
|
620
|
+
headers: authHeaders
|
|
621
|
+
};
|
|
622
|
+
this.ws.send(JSON.stringify(subscribeEnvelope));
|
|
623
|
+
console.log(\`Resubscribed to \${channel} (operation: \${operation})\`);
|
|
624
|
+
} catch (error) {
|
|
625
|
+
console.error(\`Failed to resubscribe to \${channel}:\`, error);
|
|
392
626
|
}
|
|
393
627
|
}
|
|
394
628
|
}
|
|
@@ -23,6 +23,47 @@ export interface MessageEnvelope {
|
|
|
23
23
|
};
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Connection lifecycle event callbacks
|
|
28
|
+
*/
|
|
29
|
+
export interface ConnectionEventCallbacks {
|
|
30
|
+
/** Called when the WebSocket connection is established (including reconnections) */
|
|
31
|
+
onConnected?: () => void;
|
|
32
|
+
|
|
33
|
+
/** Called when the WebSocket connection is lost unexpectedly */
|
|
34
|
+
onDisconnected?: (reason: string) => void;
|
|
35
|
+
|
|
36
|
+
/** Called when a reconnection attempt starts */
|
|
37
|
+
onReconnecting?: (attempt: number, delay: number) => void;
|
|
38
|
+
|
|
39
|
+
/** Called when reconnection succeeds after a disconnection */
|
|
40
|
+
onReconnected?: () => void;
|
|
41
|
+
|
|
42
|
+
/** Called when all reconnection attempts are exhausted (if a limit is set) */
|
|
43
|
+
onReconnectFailed?: (attempts: number) => void;
|
|
44
|
+
|
|
45
|
+
/** Called on any WebSocket error */
|
|
46
|
+
onError?: (error: Error) => void;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Configuration for WebSocket reconnection behavior
|
|
51
|
+
*/
|
|
52
|
+
export interface ReconnectConfig {
|
|
53
|
+
/** Whether to automatically reconnect on disconnect. Default: true */
|
|
54
|
+
enabled?: boolean;
|
|
55
|
+
/** Maximum number of reconnect attempts. 0 = unlimited. Default: 0 (unlimited) */
|
|
56
|
+
maxAttempts?: number;
|
|
57
|
+
/** Initial delay in ms before first reconnect attempt. Default: 1000 */
|
|
58
|
+
baseDelay?: number;
|
|
59
|
+
/** Maximum delay in ms between reconnect attempts. Default: 30000 */
|
|
60
|
+
maxDelay?: number;
|
|
61
|
+
/** Backoff multiplier. Default: 2 */
|
|
62
|
+
backoffMultiplier?: number;
|
|
63
|
+
/** Add jitter to prevent thundering herd. Default: true */
|
|
64
|
+
jitter?: boolean;
|
|
65
|
+
}
|
|
66
|
+
|
|
26
67
|
/**
|
|
27
68
|
* Transport interface for sending and receiving messages
|
|
28
69
|
*/
|
|
@@ -32,6 +73,15 @@ export interface Transport {
|
|
|
32
73
|
send(channel: string, envelope: MessageEnvelope, options?: RequestOptions): Promise<any>;
|
|
33
74
|
subscribe(channel: string, operation: string, callback: (envelope: MessageEnvelope) => void): () => void;
|
|
34
75
|
unsubscribe(channel: string, callback?: (envelope: MessageEnvelope) => void): void;
|
|
76
|
+
|
|
77
|
+
/** Check if the transport is currently connected */
|
|
78
|
+
isConnected(): boolean;
|
|
79
|
+
|
|
80
|
+
/** Register a callback for when the transport reconnects after a disconnect */
|
|
81
|
+
onReconnected(callback: () => void): () => void;
|
|
82
|
+
|
|
83
|
+
/** Register a callback for when the transport disconnects unexpectedly */
|
|
84
|
+
onDisconnected(callback: (reason: string) => void): () => void;
|
|
35
85
|
}
|
|
36
86
|
|
|
37
87
|
/**
|
|
@@ -46,6 +96,8 @@ export interface TransportConfig {
|
|
|
46
96
|
retry?: RetryConfig | RetryPreset; // Retry configuration
|
|
47
97
|
authCallbacks?: AuthEventCallbacks; // Auth event callbacks
|
|
48
98
|
retryCallbacks?: RetryEventCallbacks; // Retry event callbacks
|
|
99
|
+
reconnect?: ReconnectConfig; // Reconnection configuration
|
|
100
|
+
connectionCallbacks?: ConnectionEventCallbacks; // Connection lifecycle callbacks
|
|
49
101
|
}
|
|
50
102
|
|
|
51
103
|
/**
|