@iam4x/reconnecting-websocket 1.4.0 → 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,7 +33,11 @@ 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;
40
+ private runWithFinalizer;
37
41
  scheduleReconnect(): void;
38
42
  startHealthCheck(): void;
39
43
  stopHealthCheck(): 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,52 +48,54 @@ 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) {
84
+ const isReconnect = this.wasConnected;
85
85
  this.clearTimers();
86
86
  this.retryCount = 0;
87
- this.emit("open", event);
88
- // Emit reconnect event if this was a reconnection after a disconnection
89
- if (this.wasConnected) {
90
- this.emit("reconnect", event);
91
- }
92
87
  this.wasConnected = true;
93
88
  this.startHealthCheck();
94
89
  this.startInactivityTimer();
95
- // Flush any queued messages now that socket is open
96
- this.flushMessageQueue();
90
+ this.runWithFinalizer(() => {
91
+ this.emit("open", event);
92
+ if (isReconnect) {
93
+ this.emit("reconnect", event);
94
+ }
95
+ }, () => {
96
+ // Flush queued messages even if a listener throws during open/reconnect.
97
+ this.flushMessageQueue();
98
+ });
97
99
  }
98
100
  };
99
101
  this.msgFn = (event) => {
@@ -106,12 +108,16 @@ export class ReconnectingWebSocket {
106
108
  this.closeFn = (event) => {
107
109
  // Only process if event is from the current socket
108
110
  if (event.target === currentWs && this.ws === currentWs) {
111
+ const shouldReconnect = !this.forcedClose;
109
112
  this.stopHealthCheck();
110
113
  this.stopInactivityTimer();
111
- this.emit("close", { code: event.code, reason: event.reason });
112
- if (!this.forcedClose) {
113
- this.scheduleReconnect();
114
- }
114
+ this.runWithFinalizer(() => {
115
+ this.emit("close", { code: event.code, reason: event.reason });
116
+ }, () => {
117
+ if (shouldReconnect) {
118
+ this.scheduleReconnect();
119
+ }
120
+ });
115
121
  }
116
122
  };
117
123
  this.errorFn = (event) => {
@@ -126,11 +132,75 @@ export class ReconnectingWebSocket {
126
132
  currentWs.addEventListener("close", this.closeFn);
127
133
  currentWs.addEventListener("error", this.errorFn);
128
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
+ }
129
172
  emit(event, payload) {
130
173
  for (const listener of this.listeners[event]) {
131
174
  listener(payload);
132
175
  }
133
176
  }
177
+ runWithFinalizer(action, finalizer) {
178
+ let didThrow = false;
179
+ let thrown;
180
+ try {
181
+ action();
182
+ }
183
+ catch (error) {
184
+ didThrow = true;
185
+ thrown = error;
186
+ }
187
+ finally {
188
+ if (finalizer) {
189
+ try {
190
+ finalizer();
191
+ }
192
+ catch (error) {
193
+ if (!didThrow) {
194
+ didThrow = true;
195
+ thrown = error;
196
+ }
197
+ }
198
+ }
199
+ }
200
+ if (didThrow) {
201
+ throw thrown;
202
+ }
203
+ }
134
204
  scheduleReconnect() {
135
205
  // Clear any existing reconnect timeout first to prevent multiple reconnects
136
206
  if (this.reconnectTimeout) {
@@ -147,15 +217,20 @@ export class ReconnectingWebSocket {
147
217
  if (this.options.healthCheckInterval <= 0) {
148
218
  return;
149
219
  }
220
+ const currentWs = this.ws;
221
+ if (!currentWs) {
222
+ return;
223
+ }
150
224
  this.healthCheckInterval = setInterval(() => {
151
225
  // Only check if we're not forcing a close and we expect to be connected
152
- if (this.forcedClose) {
226
+ if (this.forcedClose || this.ws !== currentWs) {
153
227
  return;
154
228
  }
155
229
  // If we've been connected before and the socket is not OPEN, trigger reconnection
156
- if (this.wasConnected && this.readyState !== WebSocket.OPEN) {
230
+ if (this.wasConnected &&
231
+ currentWs.readyState !== this.getSocketState("OPEN")) {
157
232
  // Clear the existing socket reference since it's in a bad state
158
- if (this.ws) {
233
+ if (this.ws === currentWs) {
159
234
  // Don't emit close event since we didn't receive one - this is a silent failure
160
235
  this.ws = undefined;
161
236
  }
@@ -175,34 +250,21 @@ export class ReconnectingWebSocket {
175
250
  if (this.options.watchingInactivityTimeout <= 0) {
176
251
  return;
177
252
  }
253
+ const currentWs = this.ws;
254
+ if (!currentWs) {
255
+ return;
256
+ }
178
257
  this.inactivityTimeout = setTimeout(() => {
179
258
  // Only trigger if we're not forcing a close and we expect to be connected
180
- if (this.forcedClose) {
259
+ if (this.forcedClose || this.ws !== currentWs) {
181
260
  return;
182
261
  }
183
262
  // Proactively trigger reconnection due to inactivity
184
263
  // Don't rely on the close event as it may never fire on a stalled connection
185
- if (this.ws) {
186
- // Stop health check to prevent it from also triggering reconnection
187
- this.stopHealthCheck();
188
- // Remove event listeners to prevent any late events from interfering
189
- if (this.openFn)
190
- this.ws.removeEventListener("open", this.openFn);
191
- if (this.msgFn)
192
- this.ws.removeEventListener("message", this.msgFn);
193
- if (this.closeFn)
194
- this.ws.removeEventListener("close", this.closeFn);
195
- if (this.errorFn)
196
- this.ws.removeEventListener("error", this.errorFn);
197
- // Try to close the socket (may hang on stalled connections, but we don't wait)
198
- this.ws.close();
199
- // Clear the socket reference
200
- this.ws = undefined;
201
- // Emit close event to listeners with a special code indicating inactivity timeout
202
- this.emit("close", { code: 4000, reason: "Inactivity timeout" });
203
- // Schedule reconnection directly without waiting for close event
204
- this.scheduleReconnect();
205
- }
264
+ this.forceReconnectForSocket(currentWs, {
265
+ code: 4000,
266
+ reason: "Inactivity timeout",
267
+ });
206
268
  }, this.options.watchingInactivityTimeout);
207
269
  }
208
270
  stopInactivityTimer() {
@@ -243,7 +305,10 @@ export class ReconnectingWebSocket {
243
305
  this.listeners[event] = this.listeners[event].filter((l) => l !== listener);
244
306
  }
245
307
  send(...args) {
246
- if (this.ws?.readyState === WebSocket.OPEN) {
308
+ if (this.forcedClose) {
309
+ return;
310
+ }
311
+ if (this.ws?.readyState === this.getSocketState("OPEN")) {
247
312
  this.ws.send(...args);
248
313
  }
249
314
  else {
@@ -252,7 +317,7 @@ export class ReconnectingWebSocket {
252
317
  }
253
318
  flushMessageQueue() {
254
319
  while (this.messageQueue.length > 0 &&
255
- this.ws?.readyState === WebSocket.OPEN) {
320
+ this.ws?.readyState === this.getSocketState("OPEN")) {
256
321
  const args = this.messageQueue.shift();
257
322
  this.ws.send(...args);
258
323
  }
@@ -264,14 +329,7 @@ export class ReconnectingWebSocket {
264
329
  this.messageQueue = [];
265
330
  if (this.ws) {
266
331
  // Remove event listeners before closing to prevent memory leaks
267
- if (this.openFn)
268
- this.ws.removeEventListener("open", this.openFn);
269
- if (this.msgFn)
270
- this.ws.removeEventListener("message", this.msgFn);
271
- if (this.closeFn)
272
- this.ws.removeEventListener("close", this.closeFn);
273
- if (this.errorFn)
274
- this.ws.removeEventListener("error", this.errorFn);
332
+ this.removeSocketListeners(this.ws);
275
333
  this.ws.close(...args);
276
334
  this.ws = undefined;
277
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.0",
6
+ "version": "1.4.2",
7
7
  "private": false,
8
8
  "types": "./dist/index.d.ts",
9
9
  "exports": {