@iam4x/reconnecting-websocket 1.2.0 → 1.4.0
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/README.md +38 -18
- package/dist/index.d.ts +5 -0
- package/dist/index.js +52 -0
- package/package.json +8 -8
package/README.md
CHANGED
|
@@ -5,7 +5,9 @@ A robust, TypeScript-first WebSocket client with automatic reconnection, exponen
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
7
|
- ✅ **Automatic Reconnection** - Automatically reconnects on connection loss with exponential backoff
|
|
8
|
+
- ✅ **Message Queueing** - Messages sent while disconnected are queued and delivered on reconnection
|
|
8
9
|
- ✅ **Connection Timeout** - Configurable timeout to detect stalled connections
|
|
10
|
+
- ✅ **Inactivity Detection** - Optionally reconnect when no messages are received within a timeout
|
|
9
11
|
- ✅ **Event-Driven API** - Familiar event listener pattern matching WebSocket API
|
|
10
12
|
- ✅ **TypeScript Support** - Full TypeScript definitions included
|
|
11
13
|
- ✅ **Customizable** - Configurable retry delays, backoff factors, and WebSocket implementations
|
|
@@ -76,6 +78,8 @@ interface ReconnectOptions {
|
|
|
76
78
|
maxRetryDelay?: number; // Maximum retry delay in ms (default: 30000)
|
|
77
79
|
connectionTimeout?: number; // Connection timeout in ms (default: 10000)
|
|
78
80
|
backoffFactor?: number; // Exponential backoff multiplier (default: 2)
|
|
81
|
+
healthCheckInterval?: number; // Health check interval in ms (default: 30000)
|
|
82
|
+
watchingInactivityTimeout?: number; // Inactivity timeout in ms (default: 0, disabled)
|
|
79
83
|
WebSocketConstructor?: typeof WebSocket; // Custom WebSocket implementation
|
|
80
84
|
}
|
|
81
85
|
```
|
|
@@ -86,6 +90,8 @@ interface ReconnectOptions {
|
|
|
86
90
|
- **maxRetryDelay**: The maximum delay between reconnection attempts. The delay will grow exponentially but won't exceed this value
|
|
87
91
|
- **connectionTimeout**: If a connection doesn't establish within this time, it will be aborted and retried
|
|
88
92
|
- **backoffFactor**: The multiplier for exponential backoff. Each retry delay is multiplied by this factor
|
|
93
|
+
- **healthCheckInterval**: Interval for checking if the socket is still healthy. Set to `0` to disable (default: 30000ms)
|
|
94
|
+
- **watchingInactivityTimeout**: If no message is received within this timeout, the connection will be closed and a reconnection attempt will be made. Useful for detecting silent connection failures or keeping connections alive on servers that expect regular activity. Set to `0` to disable (default: 0, disabled). A common value is `300000` (5 minutes)
|
|
89
95
|
- **WebSocketConstructor**: Allows you to provide a custom WebSocket implementation (useful for Node.js environments using libraries like `ws`)
|
|
90
96
|
|
|
91
97
|
### Methods
|
|
@@ -119,14 +125,14 @@ ws.removeEventListener("open", handler);
|
|
|
119
125
|
|
|
120
126
|
#### `send(data)`
|
|
121
127
|
|
|
122
|
-
Sends data through the WebSocket connection.
|
|
128
|
+
Sends data through the WebSocket connection. If the socket is not open, messages are automatically queued and sent once the connection is established.
|
|
123
129
|
|
|
124
130
|
```typescript
|
|
125
131
|
ws.send("Hello, Server!");
|
|
126
132
|
ws.send(JSON.stringify({ type: "ping" }));
|
|
127
133
|
```
|
|
128
134
|
|
|
129
|
-
**Note:**
|
|
135
|
+
**Note:** Messages sent while disconnected are queued and delivered in order when the socket opens. The queue is cleared if `close()` is called.
|
|
130
136
|
|
|
131
137
|
#### `close(code?, reason?)`
|
|
132
138
|
|
|
@@ -181,6 +187,26 @@ const ws = new ReconnectingWebSocket("wss://api.example.com", {
|
|
|
181
187
|
});
|
|
182
188
|
```
|
|
183
189
|
|
|
190
|
+
### Inactivity Timeout
|
|
191
|
+
|
|
192
|
+
Use `watchingInactivityTimeout` to automatically reconnect when no messages are received for a period of time. This is useful for detecting silent connection failures or when the server expects regular activity:
|
|
193
|
+
|
|
194
|
+
```typescript
|
|
195
|
+
const ws = new ReconnectingWebSocket("wss://api.example.com", {
|
|
196
|
+
watchingInactivityTimeout: 300000, // Reconnect if no message received for 5 minutes
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
ws.addEventListener("message", (event: MessageEvent) => {
|
|
200
|
+
// Each message received resets the inactivity timer
|
|
201
|
+
console.log("Received:", event.data);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
ws.addEventListener("close", (event) => {
|
|
205
|
+
// This will fire when inactivity timeout triggers a reconnect
|
|
206
|
+
console.log("Connection closed:", event.code, event.reason);
|
|
207
|
+
});
|
|
208
|
+
```
|
|
209
|
+
|
|
184
210
|
### Using with Node.js
|
|
185
211
|
|
|
186
212
|
```typescript
|
|
@@ -194,30 +220,24 @@ const ws = new ReconnectingWebSocket("wss://api.example.com", {
|
|
|
194
220
|
|
|
195
221
|
### Handling Reconnections
|
|
196
222
|
|
|
197
|
-
|
|
198
|
-
let messageQueue: string[] = [];
|
|
223
|
+
Messages sent while disconnected are automatically queued and delivered when the connection is restored:
|
|
199
224
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
while (messageQueue.length > 0) {
|
|
203
|
-
ws.send(messageQueue.shift()!);
|
|
204
|
-
}
|
|
205
|
-
});
|
|
225
|
+
```typescript
|
|
226
|
+
const ws = new ReconnectingWebSocket("wss://api.example.com");
|
|
206
227
|
|
|
207
228
|
ws.addEventListener("reconnect", () => {
|
|
208
229
|
console.log("Reconnected! Resuming operations...");
|
|
209
230
|
});
|
|
210
231
|
|
|
211
|
-
//
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
messageQueue.push(data);
|
|
217
|
-
}
|
|
218
|
-
}
|
|
232
|
+
// Safe to call anytime - messages are queued if disconnected
|
|
233
|
+
ws.send("message1");
|
|
234
|
+
ws.send("message2");
|
|
235
|
+
|
|
236
|
+
// When socket opens/reconnects, queued messages are sent automatically
|
|
219
237
|
```
|
|
220
238
|
|
|
239
|
+
**Note:** Calling `close()` clears the message queue. Messages queued before a forced close are discarded.
|
|
240
|
+
|
|
221
241
|
### Error Handling
|
|
222
242
|
|
|
223
243
|
```typescript
|
package/dist/index.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ interface ReconnectOptions {
|
|
|
7
7
|
backoffFactor?: number;
|
|
8
8
|
WebSocketConstructor?: typeof WebSocket;
|
|
9
9
|
healthCheckInterval?: number;
|
|
10
|
+
watchingInactivityTimeout?: number;
|
|
10
11
|
}
|
|
11
12
|
export declare class ReconnectingWebSocket {
|
|
12
13
|
options: Required<ReconnectOptions & {
|
|
@@ -17,6 +18,7 @@ export declare class ReconnectingWebSocket {
|
|
|
17
18
|
connectTimeout?: ReturnType<typeof setTimeout>;
|
|
18
19
|
reconnectTimeout?: ReturnType<typeof setTimeout>;
|
|
19
20
|
healthCheckInterval?: ReturnType<typeof setInterval>;
|
|
21
|
+
inactivityTimeout?: ReturnType<typeof setTimeout>;
|
|
20
22
|
retryCount: number;
|
|
21
23
|
forcedClose: boolean;
|
|
22
24
|
wasConnected: boolean;
|
|
@@ -35,6 +37,9 @@ export declare class ReconnectingWebSocket {
|
|
|
35
37
|
scheduleReconnect(): void;
|
|
36
38
|
startHealthCheck(): void;
|
|
37
39
|
stopHealthCheck(): void;
|
|
40
|
+
startInactivityTimer(): void;
|
|
41
|
+
stopInactivityTimer(): void;
|
|
42
|
+
resetInactivityTimer(): void;
|
|
38
43
|
clearTimers(): void;
|
|
39
44
|
addEventListener(event: EventType, listener: Listener): void;
|
|
40
45
|
removeEventListener(event: EventType, listener: Listener): void;
|
package/dist/index.js
CHANGED
|
@@ -5,6 +5,7 @@ export class ReconnectingWebSocket {
|
|
|
5
5
|
connectTimeout;
|
|
6
6
|
reconnectTimeout;
|
|
7
7
|
healthCheckInterval;
|
|
8
|
+
inactivityTimeout;
|
|
8
9
|
retryCount = 0;
|
|
9
10
|
forcedClose = false;
|
|
10
11
|
wasConnected = false;
|
|
@@ -38,6 +39,7 @@ export class ReconnectingWebSocket {
|
|
|
38
39
|
backoffFactor: options.backoffFactor ?? 2,
|
|
39
40
|
WebSocketConstructor: options.WebSocketConstructor ?? WebSocket,
|
|
40
41
|
healthCheckInterval: options.healthCheckInterval ?? 30_000,
|
|
42
|
+
watchingInactivityTimeout: options.watchingInactivityTimeout ?? 0, // disabled by default, set to 300_000 for 5 minutes
|
|
41
43
|
};
|
|
42
44
|
this.connect();
|
|
43
45
|
}
|
|
@@ -89,6 +91,7 @@ export class ReconnectingWebSocket {
|
|
|
89
91
|
}
|
|
90
92
|
this.wasConnected = true;
|
|
91
93
|
this.startHealthCheck();
|
|
94
|
+
this.startInactivityTimer();
|
|
92
95
|
// Flush any queued messages now that socket is open
|
|
93
96
|
this.flushMessageQueue();
|
|
94
97
|
}
|
|
@@ -96,6 +99,7 @@ export class ReconnectingWebSocket {
|
|
|
96
99
|
this.msgFn = (event) => {
|
|
97
100
|
// Only process if event is from the current socket
|
|
98
101
|
if (event.target === currentWs && this.ws === currentWs) {
|
|
102
|
+
this.resetInactivityTimer();
|
|
99
103
|
this.emit("message", event);
|
|
100
104
|
}
|
|
101
105
|
};
|
|
@@ -103,6 +107,7 @@ export class ReconnectingWebSocket {
|
|
|
103
107
|
// Only process if event is from the current socket
|
|
104
108
|
if (event.target === currentWs && this.ws === currentWs) {
|
|
105
109
|
this.stopHealthCheck();
|
|
110
|
+
this.stopInactivityTimer();
|
|
106
111
|
this.emit("close", { code: event.code, reason: event.reason });
|
|
107
112
|
if (!this.forcedClose) {
|
|
108
113
|
this.scheduleReconnect();
|
|
@@ -165,6 +170,52 @@ export class ReconnectingWebSocket {
|
|
|
165
170
|
this.healthCheckInterval = undefined;
|
|
166
171
|
}
|
|
167
172
|
}
|
|
173
|
+
startInactivityTimer() {
|
|
174
|
+
this.stopInactivityTimer();
|
|
175
|
+
if (this.options.watchingInactivityTimeout <= 0) {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
this.inactivityTimeout = setTimeout(() => {
|
|
179
|
+
// Only trigger if we're not forcing a close and we expect to be connected
|
|
180
|
+
if (this.forcedClose) {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
// Proactively trigger reconnection due to inactivity
|
|
184
|
+
// 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
|
+
}
|
|
206
|
+
}, this.options.watchingInactivityTimeout);
|
|
207
|
+
}
|
|
208
|
+
stopInactivityTimer() {
|
|
209
|
+
if (this.inactivityTimeout) {
|
|
210
|
+
clearTimeout(this.inactivityTimeout);
|
|
211
|
+
this.inactivityTimeout = undefined;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
resetInactivityTimer() {
|
|
215
|
+
if (this.options.watchingInactivityTimeout > 0) {
|
|
216
|
+
this.startInactivityTimer();
|
|
217
|
+
}
|
|
218
|
+
}
|
|
168
219
|
clearTimers() {
|
|
169
220
|
if (this.connectTimeout) {
|
|
170
221
|
clearTimeout(this.connectTimeout);
|
|
@@ -183,6 +234,7 @@ export class ReconnectingWebSocket {
|
|
|
183
234
|
this.abortController = undefined;
|
|
184
235
|
}
|
|
185
236
|
this.stopHealthCheck();
|
|
237
|
+
this.stopInactivityTimer();
|
|
186
238
|
}
|
|
187
239
|
addEventListener(event, listener) {
|
|
188
240
|
this.listeners[event].push(listener);
|
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.
|
|
6
|
+
"version": "1.4.0",
|
|
7
7
|
"private": false,
|
|
8
8
|
"types": "./dist/index.d.ts",
|
|
9
9
|
"exports": {
|
|
@@ -24,18 +24,18 @@
|
|
|
24
24
|
"prepare": "husky"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
|
-
"@eslint/js": "^9.39.
|
|
28
|
-
"@types/bun": "^1.3.
|
|
29
|
-
"@typescript-eslint/parser": "^8.
|
|
30
|
-
"eslint": "^9.39.
|
|
27
|
+
"@eslint/js": "^9.39.2",
|
|
28
|
+
"@types/bun": "^1.3.6",
|
|
29
|
+
"@typescript-eslint/parser": "^8.54.0",
|
|
30
|
+
"eslint": "^9.39.2",
|
|
31
31
|
"eslint-config-prettier": "10.1.1",
|
|
32
32
|
"eslint-import-resolver-typescript": "4.3.1",
|
|
33
33
|
"eslint-plugin-import": "^2.32.0",
|
|
34
|
-
"eslint-plugin-prettier": "^5.5.
|
|
34
|
+
"eslint-plugin-prettier": "^5.5.5",
|
|
35
35
|
"globals": "^16.5.0",
|
|
36
36
|
"husky": "^9.1.7",
|
|
37
|
-
"prettier": "^3.
|
|
38
|
-
"typescript-eslint": "^8.
|
|
37
|
+
"prettier": "^3.8.1",
|
|
38
|
+
"typescript-eslint": "^8.54.0"
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
|
41
41
|
"typescript": "^5.8.3"
|