@ioka-technologies/asyncapi-ts-client-template 0.0.36 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ioka-technologies/asyncapi-ts-client-template",
3
- "version": "0.0.36",
3
+ "version": "0.0.41",
4
4
  "description": "TypeScript AsyncAPI client generator template compatible with rust-asyncapi patterns",
5
5
  "main": "template/index.js",
6
6
  "scripts": {
@@ -24,6 +24,10 @@
24
24
  "rust-asyncapi"
25
25
  ],
26
26
  "author": "AsyncAPI TypeScript Generator",
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "https://github.com/Ioka-Technologies/asyncapi-template"
30
+ },
27
31
  "license": "Apache-2.0",
28
32
  "dependencies": {
29
33
  "@asyncapi/generator-react-sdk": "^1.0.20"
@@ -142,14 +142,23 @@ import * as Models from './models';
142
142
  export class ${clientName} {
143
143
  private transport: Transport;
144
144
  private config: TransportConfig;
145
+ private connectPromise: Promise<void> | null = null;
145
146
 
146
147
  constructor(config: TransportConfig) {
147
148
  this.config = config;
148
149
  this.transport = TransportFactory.create(config);
149
150
  }
150
151
 
151
- async connect(): Promise<void> {
152
- await this.transport.connect();
152
+ connect(): Promise<void> {
153
+ if (this.transport.isConnected()) {
154
+ return Promise.resolve();
155
+ }
156
+ if (!this.connectPromise) {
157
+ this.connectPromise = this.transport.connect().finally(() => {
158
+ this.connectPromise = null;
159
+ });
160
+ }
161
+ return this.connectPromise;
153
162
  }
154
163
 
155
164
  async disconnect(): Promise<void> {
@@ -60,6 +60,10 @@ export class WebSocketTransport implements Transport {
60
60
  private _isConnected = false;
61
61
  private reconnectAttempts = 0;
62
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;
63
67
  private reconnectConfig: Required<ReconnectConfig>;
64
68
 
65
69
  // Event listener registries (for application-layer listeners beyond config callbacks)
@@ -104,37 +108,73 @@ export class WebSocketTransport implements Transport {
104
108
  return () => { this.disconnectedListeners.delete(callback); };
105
109
  }
106
110
 
107
- async connect(): Promise<void> {
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> {
108
123
  if (!this.WebSocketImpl) {
109
124
  this.WebSocketImpl = await getWebSocketImpl();
110
125
  }
111
126
 
112
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
+ };
113
135
  try {
114
- this.ws = new this.WebSocketImpl(this.config.url) as WebSocketLike;
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;
115
139
 
116
140
  // Handle both browser and Node.js WebSocket APIs
117
- if (typeof window !== 'undefined' && this.ws.addEventListener) {
141
+ if (typeof window !== 'undefined' && socket.addEventListener) {
118
142
  // Browser WebSocket API
119
- this.ws.addEventListener('open', () => {
143
+ socket.addEventListener('open', () => {
144
+ if (!isCurrent()) return;
145
+ settled = true;
146
+ this.pendingConnectionReject = null;
147
+ const wasReconnect = this.hasConnectedSuccessfully;
148
+ this.hasConnectedSuccessfully = true;
120
149
  this.reconnectAttempts = 0;
121
150
  this._isConnected = true;
122
- this.config.connectionCallbacks?.onConnected?.();
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
+ }
123
158
  resolve();
124
159
  });
125
160
 
126
- this.ws.addEventListener('message', (event: MessageEvent) => {
127
- this.handleMessage(event.data);
161
+ socket.addEventListener('message', (event: MessageEvent) => {
162
+ if (isCurrent()) this.handleMessage(event.data);
128
163
  });
129
164
 
130
- this.ws.addEventListener('close', () => {
165
+ socket.addEventListener('close', () => {
166
+ if (!isCurrent()) return;
167
+ this._isConnected = false;
168
+ this.ws = null;
169
+ fail(new ConnectionError('WebSocket connection closed'));
131
170
  this.handleDisconnect();
132
171
  });
133
172
 
134
- this.ws.addEventListener('error', (event: Event) => {
173
+ socket.addEventListener('error', (event: Event) => {
174
+ if (!isCurrent()) return;
135
175
  if (!this._isConnected) {
136
176
  // Error during initial connection
137
- reject(new ConnectionError(\`WebSocket connection failed: \${event.type}\`));
177
+ fail(new ConnectionError(\`WebSocket connection failed: \${event.type}\`));
138
178
  } else {
139
179
  // Error on established connection — handleDisconnect will be called by 'close'
140
180
  console.error('WebSocket error on established connection:', event);
@@ -143,27 +183,43 @@ export class WebSocketTransport implements Transport {
143
183
  );
144
184
  }
145
185
  });
146
- } else if (this.ws.on) {
186
+ } else if (socket.on) {
147
187
  // Node.js ws library API
148
- this.ws.on('open', () => {
188
+ socket.on('open', () => {
189
+ if (!isCurrent()) return;
190
+ settled = true;
191
+ this.pendingConnectionReject = null;
192
+ const wasReconnect = this.hasConnectedSuccessfully;
193
+ this.hasConnectedSuccessfully = true;
149
194
  this.reconnectAttempts = 0;
150
195
  this._isConnected = true;
151
- this.config.connectionCallbacks?.onConnected?.();
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
+ }
152
203
  resolve();
153
204
  });
154
205
 
155
- this.ws.on('message', (data: any) => {
156
- this.handleMessage(data.toString());
206
+ socket.on('message', (data: any) => {
207
+ if (isCurrent()) this.handleMessage(data.toString());
157
208
  });
158
209
 
159
- this.ws.on('close', () => {
210
+ socket.on('close', () => {
211
+ if (!isCurrent()) return;
212
+ this._isConnected = false;
213
+ this.ws = null;
214
+ fail(new ConnectionError('WebSocket connection closed'));
160
215
  this.handleDisconnect();
161
216
  });
162
217
 
163
- this.ws.on('error', (error: any) => {
218
+ socket.on('error', (error: any) => {
219
+ if (!isCurrent()) return;
164
220
  if (!this._isConnected) {
165
221
  // Error during initial connection
166
- reject(new ConnectionError(\`WebSocket connection failed: \${error.message}\`));
222
+ fail(new ConnectionError(\`WebSocket connection failed: \${error.message}\`));
167
223
  } else {
168
224
  // Error on established connection — handleDisconnect will be called by 'close'
169
225
  console.error('WebSocket error on established connection:', error);
@@ -173,10 +229,10 @@ export class WebSocketTransport implements Transport {
173
229
  }
174
230
  });
175
231
  } else {
176
- reject(new ConnectionError('WebSocket implementation does not support required event handling'));
232
+ fail(new ConnectionError('WebSocket implementation does not support required event handling'));
177
233
  }
178
234
  } catch (error: any) {
179
- reject(new ConnectionError(\`Failed to create WebSocket: \${error.message}\`));
235
+ fail(new ConnectionError(\`Failed to create WebSocket: \${error.message}\`));
180
236
  }
181
237
  });
182
238
  }
@@ -191,12 +247,19 @@ export class WebSocketTransport implements Transport {
191
247
  this.reconnectTimer = null;
192
248
  }
193
249
 
250
+ this.connectionGeneration++;
251
+ if (this.pendingConnectionReject) {
252
+ this.pendingConnectionReject(new ConnectionError('Connection closed intentionally'));
253
+ this.pendingConnectionReject = null;
254
+ }
255
+
194
256
  // Reject all pending requests
195
257
  this.rejectPendingRequests('Connection closed intentionally');
196
258
 
197
- if (this.ws) {
198
- this.ws.close();
199
- this.ws = null;
259
+ const socket = this.ws;
260
+ this.ws = null;
261
+ if (socket) {
262
+ socket.close();
200
263
  }
201
264
  }
202
265
 
@@ -443,10 +506,7 @@ export class WebSocketTransport implements Transport {
443
506
  this.rejectPendingRequests('WebSocket connection lost');
444
507
 
445
508
  // Don't reconnect if this was intentional
446
- if (this.intentionalDisconnect) {
447
- this.intentionalDisconnect = false;
448
- return;
449
- }
509
+ if (this.intentionalDisconnect) return;
450
510
 
451
511
  // Notify listeners of unexpected disconnect
452
512
  const reason = 'WebSocket connection lost unexpectedly';
@@ -464,6 +524,9 @@ export class WebSocketTransport implements Transport {
464
524
  }
465
525
 
466
526
  private attemptReconnect(): void {
527
+ if (this.intentionalDisconnect || this.reconnectTimer || this.connectPromise) {
528
+ return;
529
+ }
467
530
  // Check if we've exceeded max attempts (0 = unlimited)
468
531
  if (this.reconnectConfig.maxAttempts > 0 &&
469
532
  this.reconnectAttempts >= this.reconnectConfig.maxAttempts) {
@@ -479,22 +542,13 @@ export class WebSocketTransport implements Transport {
479
542
  this.config.connectionCallbacks?.onReconnecting?.(this.reconnectAttempts, delay);
480
543
 
481
544
  this.reconnectTimer = setTimeout(async () => {
545
+ this.reconnectTimer = null;
482
546
  try {
483
547
  // Reset the intentional disconnect flag before reconnecting
484
548
  this.intentionalDisconnect = false;
485
549
  await this.connect();
486
550
 
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); }
492
- });
493
-
494
- // Re-subscribe to all channels
495
- this.resubscribeAll();
496
551
  } catch (error) {
497
- console.error('WebSocket reconnection attempt failed:', error);
498
552
  // Try again
499
553
  this.attemptReconnect();
500
554
  }