@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 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:** This method will silently fail if the socket is not connected. Check `readyState` before sending if needed.
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
- ```typescript
198
- let messageQueue: string[] = [];
223
+ Messages sent while disconnected are automatically queued and delivered when the connection is restored:
199
224
 
200
- ws.addEventListener("open", () => {
201
- // Flush queued messages when reconnected
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
- // Queue messages when disconnected
212
- function sendMessage(data: string) {
213
- if (ws.readyState === WebSocket.OPEN) {
214
- ws.send(data);
215
- } else {
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.2.0",
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.1",
28
- "@types/bun": "^1.3.3",
29
- "@typescript-eslint/parser": "^8.48.1",
30
- "eslint": "^9.39.1",
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.4",
34
+ "eslint-plugin-prettier": "^5.5.5",
35
35
  "globals": "^16.5.0",
36
36
  "husky": "^9.1.7",
37
- "prettier": "^3.7.3",
38
- "typescript-eslint": "^8.48.1"
37
+ "prettier": "^3.8.1",
38
+ "typescript-eslint": "^8.54.0"
39
39
  },
40
40
  "peerDependencies": {
41
41
  "typescript": "^5.8.3"