@hardlydifficult/websocket 1.0.2 → 1.0.4
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 +91 -245
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @hardlydifficult/websocket
|
|
2
2
|
|
|
3
|
-
A resilient WebSocket client
|
|
3
|
+
A resilient WebSocket client with automatic reconnection (exponential backoff), heartbeat monitoring, proactive token refresh, and request tracking — fully typed in TypeScript.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -8,309 +8,155 @@ A resilient WebSocket client for Node.js with automatic reconnection, heartbeat-
|
|
|
8
8
|
npm install @hardlydifficult/websocket
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
Requires Node.js 18+.
|
|
12
|
-
|
|
13
11
|
## Quick Start
|
|
14
12
|
|
|
15
13
|
```typescript
|
|
16
14
|
import { ReconnectingWebSocket } from "@hardlydifficult/websocket";
|
|
17
15
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
client.on("open", () => {
|
|
28
|
-
console.log("Connected");
|
|
29
|
-
client.send({ type: "hello", data: "world" });
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
client.on("message", (msg) => {
|
|
33
|
-
console.log("Received:", msg);
|
|
16
|
+
const ws = new ReconnectingWebSocket({
|
|
17
|
+
url: "wss://api.example.com/ws",
|
|
18
|
+
auth: {
|
|
19
|
+
getToken: () => "Bearer token",
|
|
20
|
+
},
|
|
21
|
+
heartbeat: {
|
|
22
|
+
intervalMs: 30000, // ping every 30s
|
|
23
|
+
timeoutMs: 10000, // terminate if no pong in 10s
|
|
24
|
+
},
|
|
34
25
|
});
|
|
35
26
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
27
|
+
ws.on("open", () => console.log("Connected"));
|
|
28
|
+
ws.on("message", (data) => console.log("Received:", data));
|
|
29
|
+
ws.on("close", (code, reason) => console.log("Closed:", code, reason));
|
|
39
30
|
|
|
40
|
-
|
|
31
|
+
ws.connect();
|
|
32
|
+
ws.send({ type: "hello" }); // sends as JSON
|
|
41
33
|
```
|
|
42
34
|
|
|
43
|
-
##
|
|
44
|
-
|
|
45
|
-
A generic WebSocket client that automatically reconnects on disconnection, sends protocol-level pings for heartbeats, and parses JSON messages.
|
|
35
|
+
## Auto-Reconnecting WebSocket
|
|
46
36
|
|
|
47
|
-
|
|
37
|
+
`ReconnectingWebSocket` maintains a persistent connection with exponential backoff on disconnect, JSON message parsing, and optional authentication.
|
|
48
38
|
|
|
49
|
-
|
|
50
|
-
const client = new ReconnectingWebSocket<T>(options: WebSocketOptions);
|
|
51
|
-
```
|
|
39
|
+
### Constructor Options
|
|
52
40
|
|
|
53
41
|
| Option | Type | Default | Description |
|
|
54
|
-
|
|
55
|
-
| `url` | `string` | — | WebSocket server URL
|
|
56
|
-
| `backoff
|
|
57
|
-
| `
|
|
58
|
-
| `
|
|
59
|
-
| `
|
|
60
|
-
| `
|
|
61
|
-
|
|
62
|
-
### Methods
|
|
63
|
-
|
|
64
|
-
#### `connect(): void`
|
|
42
|
+
|---|---|---|---|
|
|
43
|
+
| `url` | `string` | — | WebSocket server URL |
|
|
44
|
+
| `backoff` | `BackoffOptions` | `{ initialDelayMs: 1000, maxDelayMs: 30000, multiplier: 2 }` | Reconnection backoff config |
|
|
45
|
+
| `heartbeat` | `HeartbeatOptions` | `{ intervalMs: 30000, timeoutMs: 10000 }` | Ping/pong monitoring |
|
|
46
|
+
| `auth` | `AuthOptions` | — | Authentication (token fetched on each connect) |
|
|
47
|
+
| `protocols` | `string[]` | — | WebSocket subprotocols |
|
|
48
|
+
| `headers` | `Record<string, string>` | — | Additional handshake headers |
|
|
65
49
|
|
|
66
|
-
|
|
50
|
+
### Core Methods
|
|
67
51
|
|
|
68
52
|
```typescript
|
|
69
|
-
|
|
53
|
+
ws.connect(); // Connect or reconnect (idempotent)
|
|
54
|
+
ws.disconnect(); // Stop and prevent future reconnects
|
|
55
|
+
ws.reconnect(); // Force reconnect with fresh auth token
|
|
56
|
+
ws.send(message); // Send JSON-serializable message
|
|
57
|
+
ws.stopReconnecting(); // Allow in-flight work but prevent reconnection
|
|
58
|
+
ws.connected; // Read-only: true if socket is open
|
|
59
|
+
ws.on(event, listener); // Register event listener (returns unsubscribe function)
|
|
70
60
|
```
|
|
71
61
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
Disconnect from the server and stop all reconnection attempts. Closes the socket with code 1000.
|
|
75
|
-
|
|
76
|
-
```typescript
|
|
77
|
-
client.disconnect();
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
#### `send(message: T): void`
|
|
81
|
-
|
|
82
|
-
Send a message as JSON. No-op if not currently connected.
|
|
83
|
-
|
|
84
|
-
```typescript
|
|
85
|
-
client.send({ type: "ping" });
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
#### `stopReconnecting(): void`
|
|
89
|
-
|
|
90
|
-
Prevent reconnection without closing the current connection. Useful for graceful shutdown: deliver in-flight results but do not reconnect if the socket drops.
|
|
62
|
+
### Event Types
|
|
91
63
|
|
|
92
64
|
```typescript
|
|
93
|
-
|
|
65
|
+
ws.on("open", () => { /* connected */ });
|
|
66
|
+
ws.on("close", (code, reason) => { /* disconnected */ });
|
|
67
|
+
ws.on("error", (error) => { /* connection or parse error */ });
|
|
68
|
+
ws.on("message", (data) => { /* message received & parsed */ });
|
|
94
69
|
```
|
|
95
70
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
Subscribe to a WebSocket lifecycle event. Multiple listeners per event are supported. Returns an unsubscribe function.
|
|
71
|
+
### Backoff Behavior
|
|
99
72
|
|
|
100
73
|
```typescript
|
|
101
|
-
|
|
102
|
-
console.log(msg);
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
unsubscribe(); // Stop listening
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
### Events
|
|
109
|
-
|
|
110
|
-
#### `open`
|
|
74
|
+
import { getBackoffDelay } from "@hardlydifficult/websocket";
|
|
111
75
|
|
|
112
|
-
|
|
76
|
+
const opts = { initialDelayMs: 1000, maxDelayMs: 30000, multiplier: 2 };
|
|
113
77
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
78
|
+
getBackoffDelay(0, opts); // 1000 ms
|
|
79
|
+
getBackoffDelay(1, opts); // 2000 ms
|
|
80
|
+
getBackoffDelay(2, opts); // 4000 ms
|
|
81
|
+
getBackoffDelay(10, opts); // capped at 30000 ms
|
|
118
82
|
```
|
|
119
83
|
|
|
120
|
-
|
|
84
|
+
## Proactive Token Refresh
|
|
121
85
|
|
|
122
|
-
|
|
86
|
+
`calculateTokenRefreshTime` schedules token refresh before expiry, using either 50% lifetime (short tokens) or a 2-minute buffer (longer tokens):
|
|
123
87
|
|
|
124
88
|
```typescript
|
|
125
|
-
|
|
126
|
-
console.log(`Closed with code ${code}: ${reason}`);
|
|
127
|
-
});
|
|
128
|
-
```
|
|
89
|
+
import { calculateTokenRefreshTime } from "@hardlydifficult/websocket";
|
|
129
90
|
|
|
130
|
-
|
|
91
|
+
// 60s token → refresh at 30s (50% wins)
|
|
92
|
+
calculateTokenRefreshTime(Date.now(), Date.now() + 60_000);
|
|
131
93
|
|
|
132
|
-
|
|
94
|
+
// 5min token → refresh at 3min (2min buffer wins)
|
|
95
|
+
calculateTokenRefreshTime(Date.now(), Date.now() + 5 * 60_000);
|
|
133
96
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
console.error("Error:", error.message);
|
|
137
|
-
});
|
|
97
|
+
// Reconnect to refresh token
|
|
98
|
+
ws.reconnect(); // fetches fresh token from auth
|
|
138
99
|
```
|
|
139
100
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
Fired when a message is received and parsed.
|
|
101
|
+
### Token Refresh Strategy
|
|
143
102
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
```
|
|
103
|
+
| Token lifetime | Refresh strategy | Example (60s token) | Example (5min token) |
|
|
104
|
+
|---|---|---|---|
|
|
105
|
+
| Short (≤4min) | 50% lifetime | 30s after issue | N/A |
|
|
106
|
+
| Long (>4min) | 2min before expiry | N/A | 3min after issue |
|
|
149
107
|
|
|
150
|
-
|
|
108
|
+
## Request Tracker
|
|
151
109
|
|
|
152
|
-
|
|
110
|
+
`RequestTracker` helps manage graceful shutdown by rejecting new requests during drain and notifying when all active requests complete.
|
|
153
111
|
|
|
154
|
-
|
|
112
|
+
### Methods
|
|
155
113
|
|
|
156
114
|
```typescript
|
|
157
|
-
|
|
158
|
-
client.send({ type: "ping" });
|
|
159
|
-
}
|
|
160
|
-
```
|
|
115
|
+
const tracker = new RequestTracker();
|
|
161
116
|
|
|
162
|
-
|
|
117
|
+
tracker.tryAccept(); // false if draining, otherwise increments active & returns true
|
|
118
|
+
tracker.complete(); // decrements active; emits 'drained' when active reaches 0
|
|
119
|
+
tracker.startDraining("reason"); // enter drain mode; rejects new requests
|
|
163
120
|
|
|
164
|
-
|
|
121
|
+
tracker.draining; // true if draining
|
|
122
|
+
tracker.active; // number of in-flight requests
|
|
165
123
|
|
|
124
|
+
tracker.on("draining", (reason) => { /* draining started */ });
|
|
125
|
+
tracker.on("drained", () => { /* all requests complete */ });
|
|
166
126
|
```
|
|
167
|
-
delay = min(initialDelayMs × multiplier^n, maxDelayMs)
|
|
168
|
-
```
|
|
169
|
-
|
|
170
|
-
For example, with default settings (initial: 1000ms, max: 30000ms, multiplier: 2):
|
|
171
|
-
- Attempt 0: 1000ms
|
|
172
|
-
- Attempt 1: 2000ms
|
|
173
|
-
- Attempt 2: 4000ms
|
|
174
|
-
- Attempt 3: 8000ms
|
|
175
|
-
- Attempt 4+: 30000ms (capped)
|
|
176
127
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
```typescript
|
|
180
|
-
import { getBackoffDelay } from "@hardlydifficult/websocket";
|
|
181
|
-
|
|
182
|
-
const delay = getBackoffDelay(2, {
|
|
183
|
-
initialDelayMs: 1000,
|
|
184
|
-
maxDelayMs: 30000,
|
|
185
|
-
multiplier: 2,
|
|
186
|
-
});
|
|
187
|
-
// delay = 4000
|
|
188
|
-
```
|
|
189
|
-
|
|
190
|
-
## RequestTracker
|
|
191
|
-
|
|
192
|
-
Tracks active requests and manages draining state. Centralizes the pattern of rejecting new work during shutdown and notifying listeners when the last request completes.
|
|
193
|
-
|
|
194
|
-
### Constructor
|
|
128
|
+
### Example Usage
|
|
195
129
|
|
|
196
130
|
```typescript
|
|
197
131
|
const tracker = new RequestTracker();
|
|
198
|
-
|
|
132
|
+
let activeRequests = 0;
|
|
199
133
|
|
|
200
|
-
|
|
134
|
+
tracker.on("draining", (reason) => console.log("Draining:", reason));
|
|
135
|
+
tracker.on("drained", () => console.log("All requests complete"));
|
|
201
136
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
Try to accept a new request. Returns `false` if draining — caller should send a rejection response.
|
|
205
|
-
|
|
206
|
-
```typescript
|
|
137
|
+
// Accept a request
|
|
207
138
|
if (tracker.tryAccept()) {
|
|
208
|
-
|
|
209
|
-
tracker.complete();
|
|
210
|
-
} else {
|
|
211
|
-
// Send rejection (service is shutting down)
|
|
212
|
-
}
|
|
213
|
-
```
|
|
214
|
-
|
|
215
|
-
#### `complete(): void`
|
|
216
|
-
|
|
217
|
-
Mark a request as complete. Decrements the active count and emits `drained` when the last request finishes during a drain.
|
|
218
|
-
|
|
219
|
-
```typescript
|
|
220
|
-
tracker.complete();
|
|
221
|
-
```
|
|
222
|
-
|
|
223
|
-
#### `startDraining(reason: string): void`
|
|
224
|
-
|
|
225
|
-
Enter draining mode — no new requests will be accepted. Idempotent: subsequent calls are ignored. Emits `draining` immediately and `drained` when active reaches zero.
|
|
226
|
-
|
|
227
|
-
```typescript
|
|
228
|
-
tracker.startDraining("server shutting down");
|
|
229
|
-
```
|
|
230
|
-
|
|
231
|
-
#### `on<K extends keyof RequestTrackerEvents>(event: K, listener: RequestTrackerEvents[K]): () => void`
|
|
232
|
-
|
|
233
|
-
Subscribe to a RequestTracker event. Returns an unsubscribe function.
|
|
234
|
-
|
|
235
|
-
```typescript
|
|
236
|
-
const unsubscribe = tracker.on("drained", () => {
|
|
237
|
-
console.log("All requests completed");
|
|
238
|
-
});
|
|
239
|
-
```
|
|
240
|
-
|
|
241
|
-
### Events
|
|
242
|
-
|
|
243
|
-
#### `draining`
|
|
244
|
-
|
|
245
|
-
Fired when draining mode is entered.
|
|
246
|
-
|
|
247
|
-
```typescript
|
|
248
|
-
tracker.on("draining", (reason: string) => {
|
|
249
|
-
console.log(`Draining: ${reason}`);
|
|
250
|
-
});
|
|
251
|
-
```
|
|
252
|
-
|
|
253
|
-
#### `drained`
|
|
254
|
-
|
|
255
|
-
Fired when all active requests complete during drain.
|
|
256
|
-
|
|
257
|
-
```typescript
|
|
258
|
-
tracker.on("drained", () => {
|
|
259
|
-
console.log("Ready to shut down");
|
|
260
|
-
});
|
|
261
|
-
```
|
|
262
|
-
|
|
263
|
-
### Properties
|
|
264
|
-
|
|
265
|
-
#### `draining: boolean`
|
|
266
|
-
|
|
267
|
-
Whether the tracker is in draining mode.
|
|
268
|
-
|
|
269
|
-
```typescript
|
|
270
|
-
if (tracker.draining) {
|
|
271
|
-
console.log("Not accepting new requests");
|
|
139
|
+
processRequest().finally(() => tracker.complete());
|
|
272
140
|
}
|
|
273
|
-
```
|
|
274
|
-
|
|
275
|
-
#### `active: number`
|
|
276
141
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
```typescript
|
|
280
|
-
console.log(`${tracker.active} requests in flight`);
|
|
142
|
+
// On shutdown signal
|
|
143
|
+
tracker.startDraining("Server shutdown");
|
|
281
144
|
```
|
|
282
145
|
|
|
283
|
-
|
|
146
|
+
## Public API Reference
|
|
284
147
|
|
|
285
|
-
|
|
286
|
-
import { ReconnectingWebSocket, RequestTracker } from "@hardlydifficult/websocket";
|
|
148
|
+
### Exports
|
|
287
149
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
// Process message
|
|
298
|
-
processMessage(msg);
|
|
299
|
-
tracker.complete();
|
|
300
|
-
});
|
|
301
|
-
|
|
302
|
-
// Initiate graceful shutdown
|
|
303
|
-
async function shutdown() {
|
|
304
|
-
client.stopReconnecting();
|
|
305
|
-
tracker.startDraining("server shutting down");
|
|
150
|
+
- `ReconnectingWebSocket<T>`: Auto-reconnecting WebSocket client class
|
|
151
|
+
- `RequestTracker`: Request tracking class for graceful shutdown
|
|
152
|
+
- `calculateTokenRefreshTime(issuedAt: number, expiresAt: number): number`: Calculate refresh time
|
|
153
|
+
- `getBackoffDelay(attempt: number, options: Required<BackoffOptions>): number`: Calculate exponential backoff delay
|
|
154
|
+
- `AuthOptions`: Type for auth configuration
|
|
155
|
+
- `BackoffOptions`: Type for backoff configuration
|
|
156
|
+
- `HeartbeatOptions`: Type for heartbeat configuration
|
|
157
|
+
- `WebSocketOptions<T>`: Type for WebSocket client options
|
|
306
158
|
|
|
307
|
-
|
|
308
|
-
await new Promise<void>((resolve) => {
|
|
309
|
-
tracker.on("drained", () => {
|
|
310
|
-
resolve();
|
|
311
|
-
});
|
|
312
|
-
});
|
|
159
|
+
### Types
|
|
313
160
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
```
|
|
161
|
+
- `WebSocketEvents<T>`: Event callback types for `ReconnectingWebSocket`
|
|
162
|
+
- `RequestTrackerEvents`: Event callback types for `RequestTracker`
|