@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 +3 -0
- package/dist/index.js +75 -57
- package/package.json +1 -1
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 ??
|
|
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.
|
|
52
|
-
this.
|
|
53
|
-
|
|
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 ===
|
|
61
|
-
this.ws?.readyState ===
|
|
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
|
|
70
|
-
this.
|
|
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 &&
|
|
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
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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.
|
|
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 ===
|
|
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
|
-
|
|
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
|
}
|