@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.
Files changed (2) hide show
  1. package/README.md +91 -245
  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,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
- 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
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
- ### Constructor
37
+ `ReconnectingWebSocket` maintains a persistent connection with exponential backoff on disconnect, JSON message parsing, and optional authentication.
48
38
 
49
- ```typescript
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 (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 |
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
- 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.
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
- client.stopReconnecting();
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
- #### `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`
141
-
142
- Fired when a message is received and parsed.
101
+ ### Token Refresh Strategy
143
102
 
144
- ```typescript
145
- client.on("message", (data: T) => {
146
- console.log("Received:", data);
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
- ### Properties
108
+ ## Request Tracker
151
109
 
152
- #### `connected: boolean`
110
+ `RequestTracker` helps manage graceful shutdown by rejecting new requests during drain and notifying when all active requests complete.
153
111
 
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
- ```
115
+ const tracker = new RequestTracker();
161
116
 
162
- ### Exponential Backoff
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
- The client uses exponential backoff for reconnection delays. The delay for attempt `n` is calculated as:
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
- You can access this calculation directly:
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
- ### Methods
134
+ tracker.on("draining", (reason) => console.log("Draining:", reason));
135
+ tracker.on("drained", () => console.log("All requests complete"));
201
136
 
202
- #### `tryAccept(): boolean`
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
- // Process request
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
- Number of currently active requests.
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
- ### Example: Graceful Shutdown
146
+ ## Public API Reference
284
147
 
285
- ```typescript
286
- import { ReconnectingWebSocket, RequestTracker } from "@hardlydifficult/websocket";
148
+ ### Exports
287
149
 
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");
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
- // Wait for all in-flight requests to complete
308
- await new Promise<void>((resolve) => {
309
- tracker.on("drained", () => {
310
- resolve();
311
- });
312
- });
159
+ ### Types
313
160
 
314
- client.disconnect();
315
- }
316
- ```
161
+ - `WebSocketEvents<T>`: Event callback types for `ReconnectingWebSocket`
162
+ - `RequestTrackerEvents`: Event callback types for `RequestTracker`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hardlydifficult/websocket",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "main": "./dist/index.js",
5
5
  "types": "./dist/index.d.ts",
6
6
  "files": [