@ioka-technologies/asyncapi-ts-client-template 0.0.35 → 0.0.36
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 +1 -1
- package/template/src/client.ts.js +31 -8
- 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 +212 -32
- package/template/src/runtime/types.ts.js +52 -0
package/package.json
CHANGED
|
@@ -185,6 +185,29 @@ export class ${clientName} {
|
|
|
185
185
|
return this.config.auth;
|
|
186
186
|
}
|
|
187
187
|
|
|
188
|
+
/**
|
|
189
|
+
* Check if the transport is currently connected
|
|
190
|
+
*/
|
|
191
|
+
isConnected(): boolean {
|
|
192
|
+
return this.transport.isConnected();
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Register a callback for when the transport reconnects after a disconnect.
|
|
197
|
+
* Returns an unsubscribe function.
|
|
198
|
+
*/
|
|
199
|
+
onReconnected(callback: () => void): () => void {
|
|
200
|
+
return this.transport.onReconnected(callback);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Register a callback for when the transport disconnects unexpectedly.
|
|
205
|
+
* Returns an unsubscribe function.
|
|
206
|
+
*/
|
|
207
|
+
onDisconnected(callback: (reason: string) => void): () => void {
|
|
208
|
+
return this.transport.onDisconnected(callback);
|
|
209
|
+
}
|
|
210
|
+
|
|
188
211
|
// Generated operation methods
|
|
189
212
|
`;
|
|
190
213
|
|
|
@@ -473,8 +496,8 @@ export class ${clientName} {
|
|
|
473
496
|
|
|
474
497
|
if (hasReply) {
|
|
475
498
|
// Request/Response pattern - send and wait for response
|
|
476
|
-
const requestPayloadType = messageTypes.length > 0 ? `Models.${messageTypes[0]}
|
|
477
|
-
const responseType = replyMessageTypes.length > 0 ? `Models.${replyMessageTypes[0]}
|
|
499
|
+
const requestPayloadType = messageTypes.length > 0 ? `Models.${messageTypes[0]}` : 'any';
|
|
500
|
+
const responseType = replyMessageTypes.length > 0 ? `Models.${replyMessageTypes[0]}` : 'any';
|
|
478
501
|
|
|
479
502
|
content += `
|
|
480
503
|
/**
|
|
@@ -496,7 +519,7 @@ export class ${clientName} {
|
|
|
496
519
|
`;
|
|
497
520
|
} else {
|
|
498
521
|
// Regular send operation (fire and forget)
|
|
499
|
-
const payloadType = messageTypes.length > 0 ? `Models.${messageTypes[0]}
|
|
522
|
+
const payloadType = messageTypes.length > 0 ? `Models.${messageTypes[0]}` : 'any';
|
|
500
523
|
content += `
|
|
501
524
|
/**
|
|
502
525
|
* ${methodName} - Send operation (fire and forget)
|
|
@@ -539,7 +562,7 @@ export class ${clientName} {
|
|
|
539
562
|
const methodName = sanitizeMethodName(operationId);
|
|
540
563
|
|
|
541
564
|
// Generate receive method (event listener setup)
|
|
542
|
-
const payloadType = messageTypes.length > 0 ? `Models.${messageTypes[0]}
|
|
565
|
+
const payloadType = messageTypes.length > 0 ? `Models.${messageTypes[0]}` : 'any';
|
|
543
566
|
content += `
|
|
544
567
|
/**
|
|
545
568
|
* ${methodName} - Receive operation
|
|
@@ -708,8 +731,8 @@ export class ${serviceName} {
|
|
|
708
731
|
}
|
|
709
732
|
|
|
710
733
|
if (hasReply) {
|
|
711
|
-
const requestPayloadType = messageTypes.length > 0 ? `Models.${messageTypes[0]}
|
|
712
|
-
const responseType = replyMessageTypes.length > 0 ? `Models.${replyMessageTypes[0]}
|
|
734
|
+
const requestPayloadType = messageTypes.length > 0 ? `Models.${messageTypes[0]}` : 'any';
|
|
735
|
+
const responseType = replyMessageTypes.length > 0 ? `Models.${replyMessageTypes[0]}` : 'any';
|
|
713
736
|
|
|
714
737
|
content += `
|
|
715
738
|
/**
|
|
@@ -729,7 +752,7 @@ export class ${serviceName} {
|
|
|
729
752
|
}
|
|
730
753
|
`;
|
|
731
754
|
} else {
|
|
732
|
-
const payloadType = messageTypes.length > 0 ? `Models.${messageTypes[0]}
|
|
755
|
+
const payloadType = messageTypes.length > 0 ? `Models.${messageTypes[0]}` : 'any';
|
|
733
756
|
content += `
|
|
734
757
|
/**
|
|
735
758
|
* ${methodName} - Send operation (fire and forget)
|
|
@@ -767,7 +790,7 @@ export class ${serviceName} {
|
|
|
767
790
|
}
|
|
768
791
|
} else if (action === 'receive') {
|
|
769
792
|
// Generate receive method (event listener setup) for dynamic channels
|
|
770
|
-
const payloadType = messageTypes.length > 0 ? `Models.${messageTypes[0]}
|
|
793
|
+
const payloadType = messageTypes.length > 0 ? `Models.${messageTypes[0]}` : 'any';
|
|
771
794
|
content += `
|
|
772
795
|
/**
|
|
773
796
|
* ${methodName} - Receive operation
|
package/template/src/index.ts.js
CHANGED
|
@@ -7,7 +7,8 @@ export default function ({ asyncapi, params }) {
|
|
|
7
7
|
{`export * from './client';
|
|
8
8
|
export * from './models';
|
|
9
9
|
export * from './runtime/types';
|
|
10
|
-
export * from './runtime/errors'
|
|
10
|
+
export * from './runtime/errors';
|
|
11
|
+
export { RECONNECT_PRESETS, ReconnectPreset, getReconnectPreset } from './runtime/retry/presets';`}
|
|
11
12
|
</File>
|
|
12
13
|
);
|
|
13
14
|
}
|
|
@@ -5,6 +5,7 @@ export default function ({ asyncapi, params }) {
|
|
|
5
5
|
return (
|
|
6
6
|
<File name="presets.ts">
|
|
7
7
|
{`import { RetryConfig, RetryPreset } from './types';
|
|
8
|
+
import { ReconnectConfig } from '../types';
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Predefined retry configuration presets
|
|
@@ -75,6 +76,57 @@ export function mergeRetryConfig(preset: RetryPreset, overrides: Partial<RetryCo
|
|
|
75
76
|
...overrides
|
|
76
77
|
};
|
|
77
78
|
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Predefined reconnection configuration presets for WebSocket transport
|
|
82
|
+
*/
|
|
83
|
+
export type ReconnectPreset = 'aggressive' | 'balanced' | 'conservative' | 'none';
|
|
84
|
+
|
|
85
|
+
export const RECONNECT_PRESETS: Record<ReconnectPreset, Required<ReconnectConfig>> = {
|
|
86
|
+
/** Aggressive reconnection - fast retries, unlimited attempts */
|
|
87
|
+
aggressive: {
|
|
88
|
+
enabled: true,
|
|
89
|
+
maxAttempts: 0,
|
|
90
|
+
baseDelay: 500,
|
|
91
|
+
maxDelay: 10000,
|
|
92
|
+
backoffMultiplier: 1.5,
|
|
93
|
+
jitter: true,
|
|
94
|
+
},
|
|
95
|
+
/** Balanced reconnection - moderate retries, unlimited attempts */
|
|
96
|
+
balanced: {
|
|
97
|
+
enabled: true,
|
|
98
|
+
maxAttempts: 0,
|
|
99
|
+
baseDelay: 1000,
|
|
100
|
+
maxDelay: 30000,
|
|
101
|
+
backoffMultiplier: 2,
|
|
102
|
+
jitter: true,
|
|
103
|
+
},
|
|
104
|
+
/** Conservative reconnection - slow retries, limited attempts */
|
|
105
|
+
conservative: {
|
|
106
|
+
enabled: true,
|
|
107
|
+
maxAttempts: 10,
|
|
108
|
+
baseDelay: 2000,
|
|
109
|
+
maxDelay: 60000,
|
|
110
|
+
backoffMultiplier: 2,
|
|
111
|
+
jitter: true,
|
|
112
|
+
},
|
|
113
|
+
/** No reconnection */
|
|
114
|
+
none: {
|
|
115
|
+
enabled: false,
|
|
116
|
+
maxAttempts: 0,
|
|
117
|
+
baseDelay: 0,
|
|
118
|
+
maxDelay: 0,
|
|
119
|
+
backoffMultiplier: 0,
|
|
120
|
+
jitter: false,
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Get reconnect configuration from preset name
|
|
126
|
+
*/
|
|
127
|
+
export function getReconnectPreset(preset: ReconnectPreset): Required<ReconnectConfig> {
|
|
128
|
+
return RECONNECT_PRESETS[preset];
|
|
129
|
+
}
|
|
78
130
|
`}
|
|
79
131
|
</File>
|
|
80
132
|
);
|
|
@@ -195,6 +195,34 @@ export class HttpTransport implements Transport {
|
|
|
195
195
|
}
|
|
196
196
|
}
|
|
197
197
|
|
|
198
|
+
/**
|
|
199
|
+
* Check if the transport is currently connected.
|
|
200
|
+
* HTTP transport is always considered connected.
|
|
201
|
+
*/
|
|
202
|
+
isConnected(): boolean {
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Register a callback for when the transport reconnects after a disconnect.
|
|
208
|
+
* HTTP transport does not have persistent connections, so this is a no-op.
|
|
209
|
+
* Returns an unsubscribe function.
|
|
210
|
+
*/
|
|
211
|
+
onReconnected(callback: () => void): () => void {
|
|
212
|
+
// No-op for HTTP transport
|
|
213
|
+
return () => {};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Register a callback for when the transport disconnects unexpectedly.
|
|
218
|
+
* HTTP transport does not have persistent connections, so this is a no-op.
|
|
219
|
+
* Returns an unsubscribe function.
|
|
220
|
+
*/
|
|
221
|
+
onDisconnected(callback: (reason: string) => void): () => void {
|
|
222
|
+
// No-op for HTTP transport
|
|
223
|
+
return () => {};
|
|
224
|
+
}
|
|
225
|
+
|
|
198
226
|
subscribe(channel: string, operation: string, callback: (envelope: MessageEnvelope) => void): () => void {
|
|
199
227
|
// HTTP transport doesn't support real-time subscriptions
|
|
200
228
|
// This is a placeholder implementation that logs a warning
|
|
@@ -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,15 +54,56 @@ 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 reconnectConfig: Required<ReconnectConfig>;
|
|
64
|
+
|
|
65
|
+
// Event listener registries (for application-layer listeners beyond config callbacks)
|
|
66
|
+
private reconnectedListeners = new Set<() => void>();
|
|
67
|
+
private disconnectedListeners = new Set<(reason: string) => void>();
|
|
60
68
|
|
|
61
69
|
constructor(config: TransportConfig) {
|
|
62
70
|
this.config = config;
|
|
71
|
+
this.reconnectConfig = {
|
|
72
|
+
enabled: config.reconnect?.enabled ?? true,
|
|
73
|
+
maxAttempts: config.reconnect?.maxAttempts ?? 0, // 0 = unlimited
|
|
74
|
+
baseDelay: config.reconnect?.baseDelay ?? 1000,
|
|
75
|
+
maxDelay: config.reconnect?.maxDelay ?? 30000,
|
|
76
|
+
backoffMultiplier: config.reconnect?.backoffMultiplier ?? 2,
|
|
77
|
+
jitter: config.reconnect?.jitter ?? true,
|
|
78
|
+
};
|
|
63
79
|
// WebSocketImpl will be set during connect()
|
|
64
80
|
}
|
|
65
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Check if the transport is currently connected
|
|
84
|
+
*/
|
|
85
|
+
isConnected(): boolean {
|
|
86
|
+
return this._isConnected && this.ws !== null && this.ws.readyState === this.ws.OPEN;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Register a callback for when the transport reconnects after a disconnect.
|
|
91
|
+
* Returns an unsubscribe function.
|
|
92
|
+
*/
|
|
93
|
+
onReconnected(callback: () => void): () => void {
|
|
94
|
+
this.reconnectedListeners.add(callback);
|
|
95
|
+
return () => { this.reconnectedListeners.delete(callback); };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Register a callback for when the transport disconnects unexpectedly.
|
|
100
|
+
* Returns an unsubscribe function.
|
|
101
|
+
*/
|
|
102
|
+
onDisconnected(callback: (reason: string) => void): () => void {
|
|
103
|
+
this.disconnectedListeners.add(callback);
|
|
104
|
+
return () => { this.disconnectedListeners.delete(callback); };
|
|
105
|
+
}
|
|
106
|
+
|
|
66
107
|
async connect(): Promise<void> {
|
|
67
108
|
if (!this.WebSocketImpl) {
|
|
68
109
|
this.WebSocketImpl = await getWebSocketImpl();
|
|
@@ -77,6 +118,8 @@ export class WebSocketTransport implements Transport {
|
|
|
77
118
|
// Browser WebSocket API
|
|
78
119
|
this.ws.addEventListener('open', () => {
|
|
79
120
|
this.reconnectAttempts = 0;
|
|
121
|
+
this._isConnected = true;
|
|
122
|
+
this.config.connectionCallbacks?.onConnected?.();
|
|
80
123
|
resolve();
|
|
81
124
|
});
|
|
82
125
|
|
|
@@ -89,12 +132,23 @@ export class WebSocketTransport implements Transport {
|
|
|
89
132
|
});
|
|
90
133
|
|
|
91
134
|
this.ws.addEventListener('error', (event: Event) => {
|
|
92
|
-
|
|
135
|
+
if (!this._isConnected) {
|
|
136
|
+
// Error during initial connection
|
|
137
|
+
reject(new ConnectionError(\`WebSocket connection failed: \${event.type}\`));
|
|
138
|
+
} else {
|
|
139
|
+
// Error on established connection — handleDisconnect will be called by 'close'
|
|
140
|
+
console.error('WebSocket error on established connection:', event);
|
|
141
|
+
this.config.connectionCallbacks?.onError?.(
|
|
142
|
+
new ConnectionError('WebSocket error')
|
|
143
|
+
);
|
|
144
|
+
}
|
|
93
145
|
});
|
|
94
146
|
} else if (this.ws.on) {
|
|
95
147
|
// Node.js ws library API
|
|
96
148
|
this.ws.on('open', () => {
|
|
97
149
|
this.reconnectAttempts = 0;
|
|
150
|
+
this._isConnected = true;
|
|
151
|
+
this.config.connectionCallbacks?.onConnected?.();
|
|
98
152
|
resolve();
|
|
99
153
|
});
|
|
100
154
|
|
|
@@ -107,7 +161,16 @@ export class WebSocketTransport implements Transport {
|
|
|
107
161
|
});
|
|
108
162
|
|
|
109
163
|
this.ws.on('error', (error: any) => {
|
|
110
|
-
|
|
164
|
+
if (!this._isConnected) {
|
|
165
|
+
// Error during initial connection
|
|
166
|
+
reject(new ConnectionError(\`WebSocket connection failed: \${error.message}\`));
|
|
167
|
+
} else {
|
|
168
|
+
// Error on established connection — handleDisconnect will be called by 'close'
|
|
169
|
+
console.error('WebSocket error on established connection:', error);
|
|
170
|
+
this.config.connectionCallbacks?.onError?.(
|
|
171
|
+
new ConnectionError(\`WebSocket error: \${error.message}\`)
|
|
172
|
+
);
|
|
173
|
+
}
|
|
111
174
|
});
|
|
112
175
|
} else {
|
|
113
176
|
reject(new ConnectionError('WebSocket implementation does not support required event handling'));
|
|
@@ -119,6 +182,18 @@ export class WebSocketTransport implements Transport {
|
|
|
119
182
|
}
|
|
120
183
|
|
|
121
184
|
async disconnect(): Promise<void> {
|
|
185
|
+
this.intentionalDisconnect = true;
|
|
186
|
+
this._isConnected = false;
|
|
187
|
+
|
|
188
|
+
// Cancel any pending reconnect timer
|
|
189
|
+
if (this.reconnectTimer) {
|
|
190
|
+
clearTimeout(this.reconnectTimer);
|
|
191
|
+
this.reconnectTimer = null;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Reject all pending requests
|
|
195
|
+
this.rejectPendingRequests('Connection closed intentionally');
|
|
196
|
+
|
|
122
197
|
if (this.ws) {
|
|
123
198
|
this.ws.close();
|
|
124
199
|
this.ws = null;
|
|
@@ -203,7 +278,7 @@ export class WebSocketTransport implements Transport {
|
|
|
203
278
|
const authHeaders = this.config.auth ? generateAuthHeaders(this.config.auth) : {};
|
|
204
279
|
|
|
205
280
|
const subscribeEnvelope: MessageEnvelope = {
|
|
206
|
-
operation: operation, //
|
|
281
|
+
operation: operation, // Use the actual operation name
|
|
207
282
|
channel,
|
|
208
283
|
payload: { channel, operation },
|
|
209
284
|
timestamp: new Date().toISOString(),
|
|
@@ -244,7 +319,7 @@ export class WebSocketTransport implements Transport {
|
|
|
244
319
|
* This provides operation-based filtering on the client side
|
|
245
320
|
*/
|
|
246
321
|
subscribeToOperation(channel: string, operation: string, callback: (payload: any) => void): () => void {
|
|
247
|
-
const operationKey = \`\${channel}
|
|
322
|
+
const operationKey = \`\${channel}::\${operation}\`;
|
|
248
323
|
|
|
249
324
|
if (!this.operationSubscriptions.has(operationKey)) {
|
|
250
325
|
this.operationSubscriptions.set(operationKey, new Set());
|
|
@@ -265,7 +340,9 @@ export class WebSocketTransport implements Transport {
|
|
|
265
340
|
// Generate auth headers if credentials are available
|
|
266
341
|
const authHeaders = this.config.auth ? generateAuthHeaders(this.config.auth) : {};
|
|
267
342
|
|
|
343
|
+
const requestId = uuidv4();
|
|
268
344
|
const subscribeEnvelope: MessageEnvelope = {
|
|
345
|
+
id: requestId,
|
|
269
346
|
operation: operation,
|
|
270
347
|
channel,
|
|
271
348
|
payload: { channel, operation },
|
|
@@ -293,7 +370,7 @@ export class WebSocketTransport implements Transport {
|
|
|
293
370
|
return;
|
|
294
371
|
}
|
|
295
372
|
|
|
296
|
-
const operationKey = \`\${envelope.channel}
|
|
373
|
+
const operationKey = \`\${envelope.channel}::\${envelope.operation}\`;
|
|
297
374
|
const operationCallbacks = this.operationSubscriptions.get(operationKey);
|
|
298
375
|
|
|
299
376
|
if (operationCallbacks) {
|
|
@@ -359,36 +436,139 @@ export class WebSocketTransport implements Transport {
|
|
|
359
436
|
}
|
|
360
437
|
|
|
361
438
|
private handleDisconnect(): void {
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
439
|
+
const wasConnected = this._isConnected;
|
|
440
|
+
this._isConnected = false;
|
|
441
|
+
|
|
442
|
+
// Reject all pending request/response handlers immediately
|
|
443
|
+
this.rejectPendingRequests('WebSocket connection lost');
|
|
444
|
+
|
|
445
|
+
// Don't reconnect if this was intentional
|
|
446
|
+
if (this.intentionalDisconnect) {
|
|
447
|
+
this.intentionalDisconnect = false;
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Notify listeners of unexpected disconnect
|
|
452
|
+
const reason = 'WebSocket connection lost unexpectedly';
|
|
453
|
+
this.config.connectionCallbacks?.onDisconnected?.(reason);
|
|
454
|
+
this.disconnectedListeners.forEach(cb => {
|
|
455
|
+
try { cb(reason); } catch (e) { console.error('Error in disconnect listener:', e); }
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
// Start reconnection if enabled
|
|
459
|
+
if (!this.reconnectConfig.enabled) {
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
this.attemptReconnect();
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
private attemptReconnect(): void {
|
|
467
|
+
// Check if we've exceeded max attempts (0 = unlimited)
|
|
468
|
+
if (this.reconnectConfig.maxAttempts > 0 &&
|
|
469
|
+
this.reconnectAttempts >= this.reconnectConfig.maxAttempts) {
|
|
470
|
+
console.error(\`WebSocket reconnect failed after \${this.reconnectAttempts} attempts\`);
|
|
471
|
+
this.config.connectionCallbacks?.onReconnectFailed?.(this.reconnectAttempts);
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
this.reconnectAttempts++;
|
|
476
|
+
const delay = this.calculateReconnectDelay(this.reconnectAttempts);
|
|
477
|
+
|
|
478
|
+
console.log(\`WebSocket reconnecting in \${delay}ms (attempt \${this.reconnectAttempts})\`);
|
|
479
|
+
this.config.connectionCallbacks?.onReconnecting?.(this.reconnectAttempts, delay);
|
|
480
|
+
|
|
481
|
+
this.reconnectTimer = setTimeout(async () => {
|
|
482
|
+
try {
|
|
483
|
+
// Reset the intentional disconnect flag before reconnecting
|
|
484
|
+
this.intentionalDisconnect = false;
|
|
485
|
+
await this.connect();
|
|
486
|
+
|
|
487
|
+
// Reconnection succeeded
|
|
488
|
+
console.log('WebSocket reconnected successfully');
|
|
489
|
+
this.config.connectionCallbacks?.onReconnected?.();
|
|
490
|
+
this.reconnectedListeners.forEach(cb => {
|
|
491
|
+
try { cb(); } catch (e) { console.error('Error in reconnect listener:', e); }
|
|
370
492
|
});
|
|
371
|
-
|
|
493
|
+
|
|
494
|
+
// Re-subscribe to all channels
|
|
495
|
+
this.resubscribeAll();
|
|
496
|
+
} catch (error) {
|
|
497
|
+
console.error('WebSocket reconnection attempt failed:', error);
|
|
498
|
+
// Try again
|
|
499
|
+
this.attemptReconnect();
|
|
500
|
+
}
|
|
501
|
+
}, delay);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
private calculateReconnectDelay(attempt: number): number {
|
|
505
|
+
let delay = this.reconnectConfig.baseDelay *
|
|
506
|
+
Math.pow(this.reconnectConfig.backoffMultiplier, attempt - 1);
|
|
507
|
+
|
|
508
|
+
// Cap at max delay
|
|
509
|
+
delay = Math.min(delay, this.reconnectConfig.maxDelay);
|
|
510
|
+
|
|
511
|
+
// Add jitter
|
|
512
|
+
if (this.reconnectConfig.jitter) {
|
|
513
|
+
delay = delay * (0.5 + Math.random() * 0.5);
|
|
372
514
|
}
|
|
515
|
+
|
|
516
|
+
return Math.floor(delay);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
private rejectPendingRequests(reason: string): void {
|
|
520
|
+
for (const [requestId, handler] of this.responseHandlers) {
|
|
521
|
+
handler.reject(new ConnectionError(reason));
|
|
522
|
+
}
|
|
523
|
+
this.responseHandlers.clear();
|
|
373
524
|
}
|
|
374
525
|
|
|
375
526
|
private resubscribeAll(): void {
|
|
376
|
-
if (this.ws
|
|
377
|
-
|
|
378
|
-
|
|
527
|
+
if (!this.ws || this.ws.readyState !== this.ws.OPEN) {
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
379
530
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
531
|
+
const authHeaders = this.config.auth ? generateAuthHeaders(this.config.auth) : {};
|
|
532
|
+
|
|
533
|
+
// Collect all unique channel+operation pairs that need resubscription
|
|
534
|
+
const subscriptionsToRestore = new Set<string>();
|
|
535
|
+
|
|
536
|
+
// From channel-based subscriptions
|
|
537
|
+
for (const channel of this.subscriptions.keys()) {
|
|
538
|
+
const operation = this.channelOperations.get(channel);
|
|
539
|
+
if (operation) {
|
|
540
|
+
subscriptionsToRestore.add(\`\${channel}::\${operation}\`);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// From operation-based subscriptions (these may have additional operations
|
|
545
|
+
// on channels already tracked above)
|
|
546
|
+
for (const operationKey of this.operationSubscriptions.keys()) {
|
|
547
|
+
// operationKey format is "channel::operation"
|
|
548
|
+
const separatorIndex = operationKey.indexOf('::');
|
|
549
|
+
if (separatorIndex !== -1) {
|
|
550
|
+
subscriptionsToRestore.add(operationKey);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// Send subscribe messages for all unique channel+operation pairs
|
|
555
|
+
for (const key of subscriptionsToRestore) {
|
|
556
|
+
const separatorIndex = key.indexOf('::');
|
|
557
|
+
const channel = key.substring(0, separatorIndex);
|
|
558
|
+
const operation = key.substring(separatorIndex + 2);
|
|
559
|
+
try {
|
|
560
|
+
const subscribeEnvelope: MessageEnvelope = {
|
|
561
|
+
id: uuidv4(),
|
|
562
|
+
operation: operation,
|
|
563
|
+
channel: channel,
|
|
564
|
+
payload: { channel, operation },
|
|
565
|
+
timestamp: new Date().toISOString(),
|
|
566
|
+
headers: authHeaders
|
|
567
|
+
};
|
|
568
|
+
this.ws.send(JSON.stringify(subscribeEnvelope));
|
|
569
|
+
console.log(\`Resubscribed to \${channel} (operation: \${operation})\`);
|
|
570
|
+
} catch (error) {
|
|
571
|
+
console.error(\`Failed to resubscribe to \${channel}:\`, error);
|
|
392
572
|
}
|
|
393
573
|
}
|
|
394
574
|
}
|
|
@@ -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
|
/**
|