@igoforth/ws-rpc 1.0.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.
Files changed (100) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +446 -0
  3. package/dist/adapters/client.d.ts +117 -0
  4. package/dist/adapters/client.js +241 -0
  5. package/dist/adapters/cloudflare-do.d.ts +72 -0
  6. package/dist/adapters/cloudflare-do.js +192 -0
  7. package/dist/adapters/index.d.ts +13 -0
  8. package/dist/adapters/index.js +16 -0
  9. package/dist/adapters/server.d.ts +10 -0
  10. package/dist/adapters/server.js +122 -0
  11. package/dist/adapters/types.d.ts +125 -0
  12. package/dist/adapters/types.js +3 -0
  13. package/dist/codecs/cbor.d.ts +16 -0
  14. package/dist/codecs/cbor.js +36 -0
  15. package/dist/codecs/factory.d.ts +3 -0
  16. package/dist/codecs/factory.js +3 -0
  17. package/dist/codecs/index.d.ts +5 -0
  18. package/dist/codecs/index.js +5 -0
  19. package/dist/codecs/json.d.ts +4 -0
  20. package/dist/codecs/json.js +4 -0
  21. package/dist/codecs/msgpack.d.ts +16 -0
  22. package/dist/codecs/msgpack.js +34 -0
  23. package/dist/codecs-BmYG2d_U.js +0 -0
  24. package/dist/default-BkrMd28n.js +253 -0
  25. package/dist/default-xDNNMrg0.d.ts +129 -0
  26. package/dist/durable-MZjkvyS6.js +165 -0
  27. package/dist/errors-5BfreE63.js +96 -0
  28. package/dist/errors.d.ts +69 -0
  29. package/dist/errors.js +7 -0
  30. package/dist/factory-3ziwTuZe.js +132 -0
  31. package/dist/factory-C1v0AEHY.d.ts +101 -0
  32. package/dist/index-Be7jjS77.d.ts +1 -0
  33. package/dist/index.d.ts +14 -0
  34. package/dist/index.js +14 -0
  35. package/dist/interface-C4S-WCqW.d.ts +120 -0
  36. package/dist/json-54Z2bIIs.d.ts +22 -0
  37. package/dist/json-Bshec-bZ.js +41 -0
  38. package/dist/memory-Bqb3KEVr.js +48 -0
  39. package/dist/memory-D1nGjzzH.d.ts +41 -0
  40. package/dist/multi-peer-BAi9yVzp.js +242 -0
  41. package/dist/peers/default.d.ts +8 -0
  42. package/dist/peers/default.js +8 -0
  43. package/dist/peers/durable.d.ts +136 -0
  44. package/dist/peers/durable.js +9 -0
  45. package/dist/peers/index.d.ts +10 -0
  46. package/dist/peers/index.js +9 -0
  47. package/dist/protocol-DA84zrc2.d.ts +211 -0
  48. package/dist/protocol-_mpoOPp6.js +192 -0
  49. package/dist/protocol.d.ts +6 -0
  50. package/dist/protocol.js +6 -0
  51. package/dist/reconnect-CGAA_1Gf.js +26 -0
  52. package/dist/reconnect-DbcN0R_1.d.ts +35 -0
  53. package/dist/schema-CN5HHHku.d.ts +108 -0
  54. package/dist/schema.d.ts +2 -0
  55. package/dist/schema.js +43 -0
  56. package/dist/server-zTjpJpoX.d.ts +209 -0
  57. package/dist/sql-CCjc6Bid.js +142 -0
  58. package/dist/sql-DPmHOeZy.d.ts +131 -0
  59. package/dist/storage/index.d.ts +8 -0
  60. package/dist/storage/index.js +7 -0
  61. package/dist/storage/interface.d.ts +3 -0
  62. package/dist/storage/interface.js +0 -0
  63. package/dist/storage/memory.d.ts +7 -0
  64. package/dist/storage/memory.js +6 -0
  65. package/dist/storage/sql.d.ts +7 -0
  66. package/dist/storage/sql.js +6 -0
  67. package/dist/types-Be-qmQu0.d.ts +111 -0
  68. package/dist/types-D_psiH09.js +13 -0
  69. package/dist/types.d.ts +7 -0
  70. package/dist/types.js +3 -0
  71. package/dist/utils/index.d.ts +2 -0
  72. package/dist/utils/index.js +3 -0
  73. package/dist/utils/reconnect.d.ts +2 -0
  74. package/dist/utils/reconnect.js +3 -0
  75. package/package.json +156 -0
  76. package/src/adapters/client.ts +396 -0
  77. package/src/adapters/cloudflare-do.ts +346 -0
  78. package/src/adapters/index.ts +16 -0
  79. package/src/adapters/multi-peer.ts +404 -0
  80. package/src/adapters/server.ts +192 -0
  81. package/src/adapters/types.ts +202 -0
  82. package/src/codecs/cbor.ts +42 -0
  83. package/src/codecs/factory.ts +210 -0
  84. package/src/codecs/index.ts +30 -0
  85. package/src/codecs/json.ts +42 -0
  86. package/src/codecs/msgpack.ts +36 -0
  87. package/src/errors.ts +105 -0
  88. package/src/index.ts +102 -0
  89. package/src/peers/default.ts +433 -0
  90. package/src/peers/durable.ts +280 -0
  91. package/src/peers/index.ts +13 -0
  92. package/src/protocol.ts +306 -0
  93. package/src/schema.ts +167 -0
  94. package/src/storage/index.ts +20 -0
  95. package/src/storage/interface.ts +146 -0
  96. package/src/storage/memory.ts +84 -0
  97. package/src/storage/sql.ts +266 -0
  98. package/src/types.ts +158 -0
  99. package/src/utils/index.ts +9 -0
  100. package/src/utils/reconnect.ts +51 -0
@@ -0,0 +1,2 @@
1
+ import { n as calculateReconnectDelay, r as defaultReconnectOptions, t as ReconnectOptions } from "../reconnect-DbcN0R_1.js";
2
+ export { ReconnectOptions, calculateReconnectDelay, defaultReconnectOptions };
@@ -0,0 +1,3 @@
1
+ import { n as defaultReconnectOptions, t as calculateReconnectDelay } from "../reconnect-CGAA_1Gf.js";
2
+
3
+ export { calculateReconnectDelay, defaultReconnectOptions };
package/package.json ADDED
@@ -0,0 +1,156 @@
1
+ {
2
+ "name": "@igoforth/ws-rpc",
3
+ "version": "1.0.0",
4
+ "description": "Bidirectional RPC over WebSocket with Zod schema validation, TypeScript inference, and Cloudflare Durable Object support",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": {
8
+ "name": "Ian Goforth",
9
+ "email": "ian.goforth@gmail.com",
10
+ "url": "https://ian.goforth.systems/"
11
+ },
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/igoforth/ws-rpc.git"
15
+ },
16
+ "bugs": {
17
+ "url": "https://github.com/igoforth/ws-rpc/issues"
18
+ },
19
+ "homepage": "https://github.com/igoforth/ws-rpc#readme",
20
+ "keywords": [
21
+ "rpc",
22
+ "websocket",
23
+ "bidirectional",
24
+ "zod",
25
+ "typescript",
26
+ "cloudflare",
27
+ "durable-objects",
28
+ "workers",
29
+ "json-rpc",
30
+ "msgpack",
31
+ "cbor",
32
+ "realtime"
33
+ ],
34
+ "engines": {
35
+ "node": ">=18"
36
+ },
37
+ "main": "./dist/index.js",
38
+ "types": "./dist/index.d.ts",
39
+ "files": [
40
+ "dist",
41
+ "src",
42
+ "LICENSE",
43
+ "README.md"
44
+ ],
45
+ "exports": {
46
+ ".": {
47
+ "types": "./dist/index.d.ts",
48
+ "import": "./dist/index.js"
49
+ },
50
+ "./protocol": {
51
+ "types": "./dist/protocol.d.ts",
52
+ "import": "./dist/protocol.js"
53
+ },
54
+ "./schema": {
55
+ "types": "./dist/schema.d.ts",
56
+ "import": "./dist/schema.js"
57
+ },
58
+ "./errors": {
59
+ "types": "./dist/errors.d.ts",
60
+ "import": "./dist/errors.js"
61
+ },
62
+ "./peer": {
63
+ "types": "./dist/peers/index.d.ts",
64
+ "import": "./dist/peers/index.js"
65
+ },
66
+ "./adapters": {
67
+ "types": "./dist/adapters/index.d.ts",
68
+ "import": "./dist/adapters/index.js"
69
+ },
70
+ "./adapters/client": {
71
+ "types": "./dist/adapters/client.d.ts",
72
+ "import": "./dist/adapters/client.js"
73
+ },
74
+ "./adapters/server": {
75
+ "types": "./dist/adapters/server.d.ts",
76
+ "import": "./dist/adapters/server.js"
77
+ },
78
+ "./adapters/cloudflare-do": {
79
+ "types": "./dist/adapters/cloudflare-do.d.ts",
80
+ "import": "./dist/adapters/cloudflare-do.js"
81
+ },
82
+ "./codecs": {
83
+ "types": "./dist/codecs/index.d.ts",
84
+ "import": "./dist/codecs/index.js"
85
+ },
86
+ "./codecs/json": {
87
+ "types": "./dist/codecs/json.d.ts",
88
+ "import": "./dist/codecs/json.js"
89
+ },
90
+ "./codecs/msgpack": {
91
+ "types": "./dist/codecs/msgpack.d.ts",
92
+ "import": "./dist/codecs/msgpack.js"
93
+ },
94
+ "./codecs/cbor": {
95
+ "types": "./dist/codecs/cbor.d.ts",
96
+ "import": "./dist/codecs/cbor.js"
97
+ },
98
+ "./storage": {
99
+ "types": "./dist/storage/index.d.ts",
100
+ "import": "./dist/storage/index.js"
101
+ },
102
+ "./types": {
103
+ "types": "./dist/types.d.ts",
104
+ "import": "./dist/types.js"
105
+ },
106
+ "./utils": {
107
+ "types": "./dist/utils/index.d.ts",
108
+ "import": "./dist/utils/index.js"
109
+ }
110
+ },
111
+ "dependencies": {
112
+ "type-fest": "^5.3.1",
113
+ "uuid": "^13.0.0",
114
+ "zod": "^4.2.0"
115
+ },
116
+ "peerDependencies": {
117
+ "@cloudflare/actors": ">=0.0.1-beta.6",
118
+ "@msgpack/msgpack": "^3.1.2",
119
+ "cbor-x": "^1.6.0"
120
+ },
121
+ "peerDependenciesMeta": {
122
+ "@cloudflare/actors": {
123
+ "optional": true
124
+ },
125
+ "@msgpack/msgpack": {
126
+ "optional": true
127
+ },
128
+ "cbor-x": {
129
+ "optional": true
130
+ }
131
+ },
132
+ "devDependencies": {
133
+ "@biomejs/biome": "^2.3.10",
134
+ "@cloudflare/actors": "^0.0.1-beta.6",
135
+ "@cloudflare/vitest-pool-workers": "^0.11.1",
136
+ "@msgpack/msgpack": "^3.1.2",
137
+ "@types/node": "^22.10.2",
138
+ "@types/ws": "^8.18.1",
139
+ "cbor-x": "^1.6.0",
140
+ "tsdown": "^0.18.1",
141
+ "typescript": "^5.9.3",
142
+ "vitest": "^3.2.4",
143
+ "wrangler": "^4.56.0",
144
+ "ws": "^8.18.3"
145
+ },
146
+ "scripts": {
147
+ "build": "tsdown",
148
+ "test": "vitest run",
149
+ "test:watch": "vitest",
150
+ "bench": "vitest bench --config vitest.config.bench.ts",
151
+ "typecheck": "tsc --noEmit",
152
+ "typegen": "wrangler types",
153
+ "lint": "biome check .",
154
+ "format": "biome format --write ."
155
+ }
156
+ }
@@ -0,0 +1,396 @@
1
+ /**
2
+ * RPC Client Adapter
3
+ *
4
+ * WebSocket client with auto-reconnect for Node.js/Bun environments.
5
+ * Wraps RpcPeer with connection management.
6
+ */
7
+
8
+ import type { Constructor } from "type-fest";
9
+ import { RpcPeer } from "../peers/default.js";
10
+ import type { WireInput } from "../protocol.js";
11
+ import type {
12
+ Driver,
13
+ EventDef,
14
+ InferEventData,
15
+ Provider,
16
+ RpcSchema,
17
+ StringKeys,
18
+ } from "../schema.js";
19
+ import {
20
+ type IRpcOptions,
21
+ type IWebSocket,
22
+ type WebSocketOptions,
23
+ WebSocketReadyState,
24
+ } from "../types.js";
25
+ import {
26
+ calculateReconnectDelay,
27
+ defaultReconnectOptions,
28
+ type IAdapterHooks,
29
+ type IConnectionAdapter,
30
+ type ReconnectOptions,
31
+ } from "./types.js";
32
+
33
+ /**
34
+ * Options for creating an RpcClient
35
+ */
36
+ export interface RpcClientOptions<
37
+ TLocalSchema extends RpcSchema,
38
+ TRemoteSchema extends RpcSchema,
39
+ > extends IAdapterHooks<TRemoteSchema>,
40
+ IRpcOptions<TLocalSchema, TRemoteSchema> {
41
+ /** WebSocket URL to connect to */
42
+ url: string;
43
+ /** Implementation of local methods */
44
+ provider: Provider<TLocalSchema>;
45
+ /** Auto-reconnect options (set to false to disable) */
46
+ reconnect?: ReconnectOptions | false;
47
+ /** Automatically connect when client is created (default: false) */
48
+ autoConnect?: boolean;
49
+ /** WebSocket subprotocols */
50
+ protocols?: string | string[];
51
+ /** HTTP headers for WebSocket upgrade request (Bun/Node.js only) */
52
+ headers?: Record<string, string>;
53
+ /** Custom WebSocket constructor (defaults to global WebSocket) */
54
+ WebSocket?: new (
55
+ url: string,
56
+ options?: string | string[] | WebSocketOptions,
57
+ ) => IWebSocket;
58
+ }
59
+
60
+ /**
61
+ * Connection state
62
+ */
63
+ export type ConnectionState =
64
+ | "disconnected"
65
+ | "connecting"
66
+ | "connected"
67
+ | "reconnecting";
68
+
69
+ /**
70
+ * RPC Client with auto-reconnect
71
+ *
72
+ * Manages WebSocket connection lifecycle and provides RPC capabilities.
73
+ */
74
+ export class RpcClient<
75
+ TLocalSchema extends RpcSchema,
76
+ TRemoteSchema extends RpcSchema,
77
+ > implements IConnectionAdapter<TLocalSchema, TRemoteSchema>
78
+ {
79
+ readonly localSchema: TLocalSchema;
80
+ readonly remoteSchema: TRemoteSchema;
81
+ readonly provider: Provider<TLocalSchema>;
82
+ readonly hooks: IAdapterHooks<TRemoteSchema> = {};
83
+
84
+ private readonly url: string;
85
+ private readonly reconnectOptions: Required<ReconnectOptions> | false;
86
+ private readonly defaultTimeout: number;
87
+ private readonly protocols: string | string[] | undefined;
88
+ private readonly headers: Record<string, string> | undefined;
89
+ private readonly WebSocketImpl: new (
90
+ url: string,
91
+ options?: string | string[] | WebSocketOptions,
92
+ ) => IWebSocket;
93
+
94
+ // Connection state
95
+ private ws: IWebSocket | null = null;
96
+ private peer: RpcPeer<TLocalSchema, TRemoteSchema> | null = null;
97
+ private _state: ConnectionState = "disconnected";
98
+ private reconnectAttempt = 0;
99
+ private reconnectTimeout: ReturnType<typeof setTimeout> | null = null;
100
+ private intentionalClose = false;
101
+
102
+ constructor(options: RpcClientOptions<TLocalSchema, TRemoteSchema>) {
103
+ this.url = options.url;
104
+ this.localSchema = options.localSchema;
105
+ this.remoteSchema = options.remoteSchema;
106
+ this.provider = options.provider;
107
+ this.reconnectOptions =
108
+ options.reconnect === false
109
+ ? false
110
+ : { ...defaultReconnectOptions, ...options.reconnect };
111
+ this.defaultTimeout = options.timeout ?? 30000;
112
+ this.protocols = options.protocols;
113
+ this.headers = options.headers;
114
+ this.WebSocketImpl =
115
+ options.WebSocket ?? (globalThis.WebSocket as Constructor<IWebSocket>);
116
+
117
+ if (options.onConnect) this.hooks.onConnect = options.onConnect;
118
+ if (options.onDisconnect) this.hooks.onDisconnect = options.onDisconnect;
119
+ if (options.onReconnect) this.hooks.onReconnect = options.onReconnect;
120
+ if (options.onReconnectFailed)
121
+ this.hooks.onReconnectFailed = options.onReconnectFailed;
122
+ if (options.onEvent) this.hooks.onEvent = options.onEvent;
123
+
124
+ if (options.autoConnect) {
125
+ void this.connect();
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Current connection state
131
+ */
132
+ get state(): ConnectionState {
133
+ return this._state;
134
+ }
135
+
136
+ /**
137
+ * Whether the client is currently connected
138
+ */
139
+ get isConnected(): boolean {
140
+ return this._state === "connected" && this.peer?.isOpen === true;
141
+ }
142
+
143
+ /**
144
+ * Get the driver for calling remote methods
145
+ *
146
+ * @returns Driver proxy for calling remote methods
147
+ * @throws Error if not connected
148
+ */
149
+ get driver(): Driver<TRemoteSchema> {
150
+ if (!this.peer) {
151
+ throw new Error("Not connected - call connect() first");
152
+ }
153
+ return this.peer.driver;
154
+ }
155
+
156
+ /**
157
+ * Emit an event to the server (fire-and-forget)
158
+ *
159
+ * @param event - Event name from local schema
160
+ * @param data - Event data matching the schema
161
+ */
162
+ emit<K extends StringKeys<TLocalSchema["events"]>>(
163
+ event: K,
164
+ data: TLocalSchema["events"] extends Record<string, EventDef>
165
+ ? InferEventData<TLocalSchema["events"][K]>
166
+ : never,
167
+ ): void {
168
+ if (!this.peer) {
169
+ console.warn(`Cannot emit event '${String(event)}': not connected`);
170
+ return;
171
+ }
172
+ this.peer.emit(event, data);
173
+ }
174
+
175
+ /**
176
+ * Connect to the WebSocket server
177
+ *
178
+ * @returns Promise that resolves when connected
179
+ * @throws Error if connection fails
180
+ */
181
+ async connect(): Promise<void> {
182
+ if (this._state === "connected" || this._state === "connecting") {
183
+ return;
184
+ }
185
+
186
+ this.intentionalClose = false;
187
+ this._state = "connecting";
188
+
189
+ return new Promise<void>((resolve, reject) => {
190
+ try {
191
+ // Use options object when headers are present (Bun/Node.js)
192
+ // Fall back to protocols-only for browser compatibility
193
+ let wsOptions: WebSocketOptions | string | string[] | undefined;
194
+ if (this.headers) {
195
+ wsOptions = { headers: this.headers };
196
+ if (this.protocols) {
197
+ wsOptions.protocols = this.protocols;
198
+ }
199
+ } else {
200
+ wsOptions = this.protocols;
201
+ }
202
+ this.ws = new this.WebSocketImpl(this.url, wsOptions);
203
+ } catch (error) {
204
+ this._state = "disconnected";
205
+ reject(error);
206
+ return;
207
+ }
208
+
209
+ const onOpen = () => {
210
+ cleanup();
211
+ this.handleOpen();
212
+ resolve();
213
+ };
214
+
215
+ const onError = (event: unknown) => {
216
+ cleanup();
217
+ this._state = "disconnected";
218
+ reject(new Error(`WebSocket connection failed: ${event}`));
219
+ };
220
+
221
+ const onClose = (event: unknown) => {
222
+ cleanup();
223
+ this._state = "disconnected";
224
+ const code =
225
+ typeof event === "object" && event != null && "code" in event
226
+ ? event.code
227
+ : "Unknown code";
228
+ const reason =
229
+ typeof event === "object" && event != null && "reason" in event
230
+ ? event.reason
231
+ : "Unknown reason";
232
+ reject(new Error(`WebSocket closed: ${code} ${reason}`));
233
+ };
234
+
235
+ const cleanup = () => {
236
+ this.ws?.removeEventListener?.("open", onOpen);
237
+ this.ws?.removeEventListener?.("error", onError);
238
+ this.ws?.removeEventListener?.("close", onClose);
239
+ };
240
+
241
+ this.ws.addEventListener?.("open", onOpen);
242
+ this.ws.addEventListener?.("error", onError);
243
+ this.ws.addEventListener?.("close", onClose);
244
+ });
245
+ }
246
+
247
+ /**
248
+ * Disconnect from the server
249
+ *
250
+ * @param code - WebSocket close code (default: 1000)
251
+ * @param reason - Close reason message (default: "Client disconnect")
252
+ */
253
+ disconnect(code = 1000, reason = "Client disconnect"): void {
254
+ this.intentionalClose = true;
255
+ this.cancelReconnect();
256
+
257
+ if (this.peer) {
258
+ this.peer.close();
259
+ this.peer = null;
260
+ }
261
+
262
+ if (this.ws && this.ws.readyState !== WebSocketReadyState.CLOSED) {
263
+ this.ws.close(code, reason);
264
+ }
265
+ this.ws = null;
266
+
267
+ this._state = "disconnected";
268
+ }
269
+
270
+ /**
271
+ * Handle WebSocket open event
272
+ */
273
+ private handleOpen(): void {
274
+ if (!this.ws) return;
275
+
276
+ this._state = "connected";
277
+ this.reconnectAttempt = 0;
278
+
279
+ // Create RPC peer
280
+ this.peer = new RpcPeer({
281
+ ws: this.ws,
282
+ localSchema: this.localSchema,
283
+ remoteSchema: this.remoteSchema,
284
+ provider: this.provider,
285
+ onEvent: this.hooks.onEvent,
286
+ timeout: this.defaultTimeout,
287
+ });
288
+
289
+ // Set up WebSocket event handlers
290
+ this.ws.onmessage = (event) => {
291
+ if (typeof event === "object" && event != null && "data" in event)
292
+ this.peer?.handleMessage(event.data as WireInput);
293
+ else
294
+ throw new Error(
295
+ `Received invalid event type in RpcClient.ws.onmessage ${JSON.stringify(event)}`,
296
+ );
297
+ };
298
+
299
+ this.ws.onclose = (event) => {
300
+ if (
301
+ typeof event === "object" &&
302
+ event != null &&
303
+ "code" in event &&
304
+ "reason" in event &&
305
+ typeof event.code === "number" &&
306
+ typeof event.reason === "string"
307
+ )
308
+ this.handleClose(event.code, event.reason);
309
+ else
310
+ throw new Error(
311
+ `Received invalid event type in RpcClient.ws.onclose ${JSON.stringify(event)}`,
312
+ );
313
+ };
314
+
315
+ this.ws.onerror = (event) => {
316
+ console.error("WebSocket error:", event);
317
+ };
318
+
319
+ this.hooks.onConnect?.();
320
+ }
321
+
322
+ /**
323
+ * Handle WebSocket close event
324
+ */
325
+ private handleClose(code: number, reason: string): void {
326
+ this.peer?.close();
327
+ this.peer = null;
328
+ this.ws = null;
329
+
330
+ this.hooks.onDisconnect?.(code, reason);
331
+
332
+ if (this.intentionalClose) {
333
+ this._state = "disconnected";
334
+ return;
335
+ }
336
+
337
+ // Attempt reconnection
338
+ if (this.reconnectOptions !== false) {
339
+ this.scheduleReconnect();
340
+ } else {
341
+ this._state = "disconnected";
342
+ }
343
+ }
344
+
345
+ /**
346
+ * Schedule a reconnection attempt
347
+ */
348
+ private scheduleReconnect(): void {
349
+ if (this.reconnectOptions === false) return;
350
+
351
+ const { maxAttempts } = this.reconnectOptions;
352
+ if (maxAttempts > 0 && this.reconnectAttempt >= maxAttempts) {
353
+ this._state = "disconnected";
354
+ this.hooks.onReconnectFailed?.();
355
+ return;
356
+ }
357
+
358
+ this._state = "reconnecting";
359
+ const delay = calculateReconnectDelay(
360
+ this.reconnectAttempt,
361
+ this.reconnectOptions,
362
+ );
363
+ this.reconnectAttempt++;
364
+
365
+ this.hooks.onReconnect?.(this.reconnectAttempt, delay);
366
+
367
+ this.reconnectTimeout = setTimeout(() => {
368
+ this.reconnectTimeout = null;
369
+ void this.attemptReconnect();
370
+ }, delay);
371
+ }
372
+
373
+ /**
374
+ * Attempt to reconnect
375
+ */
376
+ private async attemptReconnect(): Promise<void> {
377
+ try {
378
+ await this.connect();
379
+ } catch {
380
+ // connect() failed during reconnection - schedule another attempt
381
+ if (!this.intentionalClose && this.reconnectOptions !== false) {
382
+ this.scheduleReconnect();
383
+ }
384
+ }
385
+ }
386
+
387
+ /**
388
+ * Cancel any pending reconnection
389
+ */
390
+ private cancelReconnect(): void {
391
+ if (this.reconnectTimeout) {
392
+ clearTimeout(this.reconnectTimeout);
393
+ this.reconnectTimeout = null;
394
+ }
395
+ }
396
+ }