@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 +4 -0
- package/dist/index.js +120 -62
- package/package.json +1 -1
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 ??
|
|
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.
|
|
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) {
|
|
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
|
-
|
|
96
|
-
|
|
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.
|
|
112
|
-
|
|
113
|
-
|
|
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 &&
|
|
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
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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.
|
|
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 ===
|
|
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
|
-
|
|
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
|
}
|