@iam4x/reconnecting-websocket 1.4.1 → 1.4.2

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/index.d.ts CHANGED
@@ -33,6 +33,9 @@ export declare class ReconnectingWebSocket {
33
33
  get bufferedAmount(): number;
34
34
  constructor(url: string, options?: ReconnectOptions);
35
35
  connect(): void;
36
+ private getSocketState;
37
+ private removeSocketListeners;
38
+ private forceReconnectForSocket;
36
39
  emit(event: EventType, payload: any): void;
37
40
  private runWithFinalizer;
38
41
  scheduleReconnect(): void;
package/dist/index.js CHANGED
@@ -25,7 +25,7 @@ export class ReconnectingWebSocket {
25
25
  // Queue for messages sent when socket is not open
26
26
  messageQueue = [];
27
27
  get readyState() {
28
- return this.ws?.readyState ?? WebSocket.CLOSED;
28
+ return this.ws?.readyState ?? this.getSocketState("CLOSED");
29
29
  }
30
30
  get bufferedAmount() {
31
31
  return this.ws?.bufferedAmount ?? 0;
@@ -48,37 +48,36 @@ export class ReconnectingWebSocket {
48
48
  // This ensures that manual reconnections (via connect()) can auto-reconnect
49
49
  this.forcedClose = false;
50
50
  // Remove event listeners from old socket
51
- if (this.openFn)
52
- this.ws?.removeEventListener("open", this.openFn);
53
- if (this.msgFn)
54
- this.ws?.removeEventListener("message", this.msgFn);
55
- if (this.closeFn)
56
- this.ws?.removeEventListener("close", this.closeFn);
57
- if (this.errorFn)
58
- this.ws?.removeEventListener("error", this.errorFn);
51
+ if (this.ws) {
52
+ this.removeSocketListeners(this.ws);
53
+ }
59
54
  // Close old socket if still connecting or open
60
- if (this.ws?.readyState === WebSocket.CONNECTING ||
61
- this.ws?.readyState === WebSocket.OPEN) {
55
+ if (this.ws?.readyState === this.getSocketState("CONNECTING") ||
56
+ this.ws?.readyState === this.getSocketState("OPEN")) {
62
57
  this.ws?.close();
63
58
  }
64
59
  // Clear any pending timers (this also removes abort listener from old controller)
65
60
  this.clearTimers();
61
+ // Create new socket
62
+ this.ws = new this.options.WebSocketConstructor(this.options.url);
63
+ const currentWs = this.ws;
66
64
  // Create new abort controller
67
65
  this.abortController = new AbortController();
68
66
  this.abortHandler = () => {
69
- if (this.ws?.readyState === WebSocket.CONNECTING) {
70
- this.ws.close();
67
+ if (this.ws === currentWs &&
68
+ currentWs.readyState === this.getSocketState("CONNECTING")) {
69
+ this.forceReconnectForSocket(currentWs, {
70
+ code: 1006,
71
+ reason: "Connection timeout",
72
+ });
71
73
  }
72
74
  };
73
75
  this.abortController.signal.addEventListener("abort", this.abortHandler);
74
- // Create new socket
75
- this.ws = new this.options.WebSocketConstructor(this.options.url);
76
76
  this.connectTimeout = setTimeout(() => {
77
77
  this.abortController?.abort();
78
78
  }, this.options.connectionTimeout);
79
79
  // Create and store new event handlers
80
80
  // Capture the new socket reference to check against in handlers
81
- const currentWs = this.ws;
82
81
  this.openFn = (event) => {
83
82
  // Only process if event is from the current socket
84
83
  if (event.target === currentWs && this.ws === currentWs) {
@@ -133,6 +132,43 @@ export class ReconnectingWebSocket {
133
132
  currentWs.addEventListener("close", this.closeFn);
134
133
  currentWs.addEventListener("error", this.errorFn);
135
134
  }
135
+ getSocketState(state) {
136
+ const fallbackStates = {
137
+ CONNECTING: 0,
138
+ OPEN: 1,
139
+ CLOSED: 3,
140
+ };
141
+ const WebSocketConstructor = this.options
142
+ .WebSocketConstructor;
143
+ return WebSocketConstructor[state] ?? fallbackStates[state];
144
+ }
145
+ removeSocketListeners(socket) {
146
+ if (this.openFn)
147
+ socket.removeEventListener("open", this.openFn);
148
+ if (this.msgFn)
149
+ socket.removeEventListener("message", this.msgFn);
150
+ if (this.closeFn)
151
+ socket.removeEventListener("close", this.closeFn);
152
+ if (this.errorFn)
153
+ socket.removeEventListener("error", this.errorFn);
154
+ }
155
+ forceReconnectForSocket(socket, closePayload) {
156
+ const shouldReconnect = !this.forcedClose;
157
+ this.stopHealthCheck();
158
+ this.stopInactivityTimer();
159
+ this.removeSocketListeners(socket);
160
+ socket.close();
161
+ if (this.ws === socket) {
162
+ this.ws = undefined;
163
+ }
164
+ this.runWithFinalizer(() => {
165
+ this.emit("close", closePayload);
166
+ }, () => {
167
+ if (shouldReconnect) {
168
+ this.scheduleReconnect();
169
+ }
170
+ });
171
+ }
136
172
  emit(event, payload) {
137
173
  for (const listener of this.listeners[event]) {
138
174
  listener(payload);
@@ -181,15 +217,20 @@ export class ReconnectingWebSocket {
181
217
  if (this.options.healthCheckInterval <= 0) {
182
218
  return;
183
219
  }
220
+ const currentWs = this.ws;
221
+ if (!currentWs) {
222
+ return;
223
+ }
184
224
  this.healthCheckInterval = setInterval(() => {
185
225
  // Only check if we're not forcing a close and we expect to be connected
186
- if (this.forcedClose) {
226
+ if (this.forcedClose || this.ws !== currentWs) {
187
227
  return;
188
228
  }
189
229
  // If we've been connected before and the socket is not OPEN, trigger reconnection
190
- if (this.wasConnected && this.readyState !== WebSocket.OPEN) {
230
+ if (this.wasConnected &&
231
+ currentWs.readyState !== this.getSocketState("OPEN")) {
191
232
  // Clear the existing socket reference since it's in a bad state
192
- if (this.ws) {
233
+ if (this.ws === currentWs) {
193
234
  // Don't emit close event since we didn't receive one - this is a silent failure
194
235
  this.ws = undefined;
195
236
  }
@@ -209,40 +250,21 @@ export class ReconnectingWebSocket {
209
250
  if (this.options.watchingInactivityTimeout <= 0) {
210
251
  return;
211
252
  }
253
+ const currentWs = this.ws;
254
+ if (!currentWs) {
255
+ return;
256
+ }
212
257
  this.inactivityTimeout = setTimeout(() => {
213
258
  // Only trigger if we're not forcing a close and we expect to be connected
214
- if (this.forcedClose) {
259
+ if (this.forcedClose || this.ws !== currentWs) {
215
260
  return;
216
261
  }
217
- const shouldReconnect = !this.forcedClose;
218
262
  // Proactively trigger reconnection due to inactivity
219
263
  // Don't rely on the close event as it may never fire on a stalled connection
220
- if (this.ws) {
221
- // Stop health check to prevent it from also triggering reconnection
222
- this.stopHealthCheck();
223
- // Remove event listeners to prevent any late events from interfering
224
- if (this.openFn)
225
- this.ws.removeEventListener("open", this.openFn);
226
- if (this.msgFn)
227
- this.ws.removeEventListener("message", this.msgFn);
228
- if (this.closeFn)
229
- this.ws.removeEventListener("close", this.closeFn);
230
- if (this.errorFn)
231
- this.ws.removeEventListener("error", this.errorFn);
232
- // Try to close the socket (may hang on stalled connections, but we don't wait)
233
- this.ws.close();
234
- // Clear the socket reference
235
- this.ws = undefined;
236
- this.runWithFinalizer(() => {
237
- // Emit close event to listeners with a special code indicating inactivity timeout.
238
- this.emit("close", { code: 4000, reason: "Inactivity timeout" });
239
- }, () => {
240
- if (shouldReconnect) {
241
- // Schedule reconnection directly without waiting for close event.
242
- this.scheduleReconnect();
243
- }
244
- });
245
- }
264
+ this.forceReconnectForSocket(currentWs, {
265
+ code: 4000,
266
+ reason: "Inactivity timeout",
267
+ });
246
268
  }, this.options.watchingInactivityTimeout);
247
269
  }
248
270
  stopInactivityTimer() {
@@ -283,7 +305,10 @@ export class ReconnectingWebSocket {
283
305
  this.listeners[event] = this.listeners[event].filter((l) => l !== listener);
284
306
  }
285
307
  send(...args) {
286
- if (this.ws?.readyState === WebSocket.OPEN) {
308
+ if (this.forcedClose) {
309
+ return;
310
+ }
311
+ if (this.ws?.readyState === this.getSocketState("OPEN")) {
287
312
  this.ws.send(...args);
288
313
  }
289
314
  else {
@@ -292,7 +317,7 @@ export class ReconnectingWebSocket {
292
317
  }
293
318
  flushMessageQueue() {
294
319
  while (this.messageQueue.length > 0 &&
295
- this.ws?.readyState === WebSocket.OPEN) {
320
+ this.ws?.readyState === this.getSocketState("OPEN")) {
296
321
  const args = this.messageQueue.shift();
297
322
  this.ws.send(...args);
298
323
  }
@@ -304,14 +329,7 @@ export class ReconnectingWebSocket {
304
329
  this.messageQueue = [];
305
330
  if (this.ws) {
306
331
  // Remove event listeners before closing to prevent memory leaks
307
- if (this.openFn)
308
- this.ws.removeEventListener("open", this.openFn);
309
- if (this.msgFn)
310
- this.ws.removeEventListener("message", this.msgFn);
311
- if (this.closeFn)
312
- this.ws.removeEventListener("close", this.closeFn);
313
- if (this.errorFn)
314
- this.ws.removeEventListener("error", this.errorFn);
332
+ this.removeSocketListeners(this.ws);
315
333
  this.ws.close(...args);
316
334
  this.ws = undefined;
317
335
  }
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "module": "src/index.ts",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
- "version": "1.4.1",
6
+ "version": "1.4.2",
7
7
  "private": false,
8
8
  "types": "./dist/index.d.ts",
9
9
  "exports": {