@hardlydifficult/websocket 1.0.2 → 1.0.3

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.
Files changed (2) hide show
  1. package/README.md +77 -249
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @hardlydifficult/websocket
2
2
 
3
- A resilient WebSocket client for Node.js with automatic reconnection, heartbeat-based dead connection detection, and graceful request draining.
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,137 @@ 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
- interface Message {
19
- type: string;
20
- data: unknown;
21
- }
22
-
23
- const client = new ReconnectingWebSocket<Message>({
24
- url: "ws://localhost:8080",
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
- client.on("error", (err) => {
37
- console.error("Error:", err);
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
- client.connect();
31
+ ws.connect();
32
+ ws.send({ type: "hello" }); // sends as JSON
41
33
  ```
42
34
 
43
- ## ReconnectingWebSocket
35
+ ## Auto-Reconnecting WebSocket
44
36
 
45
- A generic WebSocket client that automatically reconnects on disconnection, sends protocol-level pings for heartbeats, and parses JSON messages.
37
+ `ReconnectingWebSocket` maintains a persistent connection with exponential backoff on disconnect, JSON message parsing, and optional authentication.
46
38
 
47
- ### Constructor
48
-
49
- ```typescript
50
- const client = new ReconnectingWebSocket<T>(options: WebSocketOptions);
51
- ```
39
+ ### Constructor Options
52
40
 
53
41
  | Option | Type | Default | Description |
54
42
  |--------|------|---------|-------------|
55
- | `url` | `string` | — | WebSocket server URL (required) |
56
- | `backoff.initialDelayMs` | `number` | `1000` | Initial reconnection delay in milliseconds |
57
- | `backoff.maxDelayMs` | `number` | `30000` | Maximum reconnection delay in milliseconds |
58
- | `backoff.multiplier` | `number` | `2` | Multiplier applied per reconnection attempt |
59
- | `heartbeat.intervalMs` | `number` | `30000` | Interval between pings in milliseconds |
60
- | `heartbeat.timeoutMs` | `number` | `10000` | Time to wait for pong before terminating |
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 |
61
49
 
62
- ### Methods
63
-
64
- #### `connect(): void`
65
-
66
- Connect to the WebSocket server. Idempotent — calling multiple times has no additional effect. If a reconnect timer is pending, cancels it and connects immediately, resetting the attempt counter.
50
+ ### Core Methods
67
51
 
68
52
  ```typescript
69
- client.connect();
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
- #### `disconnect(): void`
73
-
74
- Disconnect from the server and stop all reconnection attempts. Closes the socket with code 1000.
62
+ ### Event Types
75
63
 
76
64
  ```typescript
77
- client.disconnect();
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 */ });
78
69
  ```
79
70
 
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.
91
-
92
- ```typescript
93
- client.stopReconnecting();
94
- ```
95
-
96
- #### `on<K extends keyof WebSocketEvents<T>>(event: K, listener: WebSocketEvents<T>[K]): () => void`
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
- const unsubscribe = client.on("message", (msg) => {
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
- Fired when the connection is established.
76
+ const opts = { initialDelayMs: 1000, maxDelayMs: 30000, multiplier: 2 };
113
77
 
114
- ```typescript
115
- client.on("open", () => {
116
- console.log("Connected");
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
- #### `close`
84
+ ## Proactive Token Refresh
121
85
 
122
- Fired when the connection is closed.
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
- client.on("close", (code: number, reason: string) => {
126
- console.log(`Closed with code ${code}: ${reason}`);
127
- });
128
- ```
89
+ import { calculateTokenRefreshTime } from "@hardlydifficult/websocket";
129
90
 
130
- #### `error`
91
+ // 60s token → refresh at 30s (50% wins)
92
+ calculateTokenRefreshTime(Date.now(), Date.now() + 60_000);
131
93
 
132
- Fired on connection or parse errors.
94
+ // 5min token refresh at 3min (2min buffer wins)
95
+ calculateTokenRefreshTime(Date.now(), Date.now() + 5 * 60_000);
133
96
 
134
- ```typescript
135
- client.on("error", (error: Error) => {
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
- #### `message`
101
+ ### Token Refresh Strategy
141
102
 
142
- Fired when a message is received and parsed.
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 |
143
107
 
144
- ```typescript
145
- client.on("message", (data: T) => {
146
- console.log("Received:", data);
147
- });
148
- ```
108
+ ## Request Tracker
149
109
 
150
- ### Properties
110
+ `RequestTracker` helps manage graceful shutdown by rejecting new requests during drain and notifying when all active requests complete.
151
111
 
152
- #### `connected: boolean`
153
-
154
- Whether the socket is currently open.
112
+ ### Methods
155
113
 
156
114
  ```typescript
157
- if (client.connected) {
158
- client.send({ type: "ping" });
159
- }
160
- ```
161
-
162
- ### Exponential Backoff
163
-
164
- The client uses exponential backoff for reconnection delays. The delay for attempt `n` is calculated as:
165
-
166
- ```
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)
115
+ const tracker = new RequestTracker();
176
116
 
177
- You can access this calculation directly:
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
178
120
 
179
- ```typescript
180
- import { getBackoffDelay } from "@hardlydifficult/websocket";
121
+ tracker.draining; // true if draining
122
+ tracker.active; // number of in-flight requests
181
123
 
182
- const delay = getBackoffDelay(2, {
183
- initialDelayMs: 1000,
184
- maxDelayMs: 30000,
185
- multiplier: 2,
186
- });
187
- // delay = 4000
124
+ tracker.on("draining", (reason) => { /* draining started */ });
125
+ tracker.on("drained", () => { /* all requests complete */ });
188
126
  ```
189
127
 
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
- ### Methods
201
-
202
- #### `tryAccept(): boolean`
134
+ tracker.on("draining", (reason) => console.log("Draining:", reason));
135
+ tracker.on("drained", () => console.log("All requests complete"));
203
136
 
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
- // Process request
209
- tracker.complete();
210
- } else {
211
- // Send rejection (service is shutting down)
139
+ processRequest().finally(() => tracker.complete());
212
140
  }
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
141
 
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");
272
- }
273
- ```
274
-
275
- #### `active: number`
276
-
277
- Number of currently active requests.
278
-
279
- ```typescript
280
- console.log(`${tracker.active} requests in flight`);
281
- ```
282
-
283
- ### Example: Graceful Shutdown
284
-
285
- ```typescript
286
- import { ReconnectingWebSocket, RequestTracker } from "@hardlydifficult/websocket";
287
-
288
- const client = new ReconnectingWebSocket({ url: "ws://localhost:8080" });
289
- const tracker = new RequestTracker();
290
-
291
- client.on("message", (msg) => {
292
- if (!tracker.tryAccept()) {
293
- // Reject new requests during shutdown
294
- return;
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");
306
-
307
- // Wait for all in-flight requests to complete
308
- await new Promise<void>((resolve) => {
309
- tracker.on("drained", () => {
310
- resolve();
311
- });
312
- });
313
-
314
- client.disconnect();
315
- }
142
+ // On shutdown signal
143
+ tracker.startDraining("Server shutdown");
316
144
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hardlydifficult/websocket",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "main": "./dist/index.js",
5
5
  "types": "./dist/index.d.ts",
6
6
  "files": [