@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ioka-technologies/asyncapi-ts-client-template",
3
- "version": "0.0.35",
3
+ "version": "0.0.36",
4
4
  "description": "TypeScript AsyncAPI client generator template compatible with rust-asyncapi patterns",
5
5
  "main": "template/index.js",
6
6
  "scripts": {
@@ -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]}Payload` : 'any';
477
- const responseType = replyMessageTypes.length > 0 ? `Models.${replyMessageTypes[0]}Payload` : 'any';
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]}Payload` : 'any';
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]}Payload` : 'any';
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]}Payload` : 'any';
712
- const responseType = replyMessageTypes.length > 0 ? `Models.${replyMessageTypes[0]}Payload` : 'any';
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]}Payload` : 'any';
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]}Payload` : 'any';
793
+ const payloadType = messageTypes.length > 0 ? `Models.${messageTypes[0]}` : 'any';
771
794
  content += `
772
795
  /**
773
796
  * ${methodName} - Receive operation
@@ -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 maxReconnectAttempts = 5;
59
- private reconnectDelay = 1000;
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
- reject(new ConnectionError(\`WebSocket connection failed: \${event.type}\`));
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
- reject(new ConnectionError(\`WebSocket connection failed: \${error.message}\`));
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, // Use the actual operation name
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}:\${operation}\`;
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}:\${envelope.operation}\`;
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
- if (this.reconnectAttempts < this.maxReconnectAttempts) {
363
- this.reconnectAttempts++;
364
- setTimeout(() => {
365
- this.connect().then(() => {
366
- // Re-subscribe to all channels after reconnection
367
- this.resubscribeAll();
368
- }).catch(() => {
369
- // Failed to reconnect
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
- }, this.reconnectDelay * this.reconnectAttempts);
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 && this.ws.readyState === this.ws.OPEN) {
377
- // Generate auth headers if credentials are available
378
- const authHeaders = this.config.auth ? generateAuthHeaders(this.config.auth) : {};
527
+ if (!this.ws || this.ws.readyState !== this.ws.OPEN) {
528
+ return;
529
+ }
379
530
 
380
- for (const channel of this.subscriptions.keys()) {
381
- const operation = this.channelOperations.get(channel);
382
- if (operation) {
383
- const subscribeEnvelope: MessageEnvelope = {
384
- operation: operation,
385
- channel,
386
- payload: { channel, operation },
387
- timestamp: new Date().toISOString(),
388
- headers: authHeaders
389
- };
390
- this.ws.send(JSON.stringify(subscribeEnvelope));
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
  /**