@igoforth/ws-rpc 1.0.0 → 1.0.1
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 +21 -20
- package/dist/adapters/cloudflare-do.d.ts +10 -5
- package/dist/adapters/cloudflare-do.js +10 -5
- package/package.json +1 -2
- package/src/adapters/client.ts +0 -396
- package/src/adapters/cloudflare-do.ts +0 -346
- package/src/adapters/index.ts +0 -16
- package/src/adapters/multi-peer.ts +0 -404
- package/src/adapters/server.ts +0 -192
- package/src/adapters/types.ts +0 -202
- package/src/codecs/cbor.ts +0 -42
- package/src/codecs/factory.ts +0 -210
- package/src/codecs/index.ts +0 -30
- package/src/codecs/json.ts +0 -42
- package/src/codecs/msgpack.ts +0 -36
- package/src/errors.ts +0 -105
- package/src/index.ts +0 -102
- package/src/peers/default.ts +0 -433
- package/src/peers/durable.ts +0 -280
- package/src/peers/index.ts +0 -13
- package/src/protocol.ts +0 -306
- package/src/schema.ts +0 -167
- package/src/storage/index.ts +0 -20
- package/src/storage/interface.ts +0 -146
- package/src/storage/memory.ts +0 -84
- package/src/storage/sql.ts +0 -266
- package/src/types.ts +0 -158
- package/src/utils/index.ts +0 -9
- package/src/utils/reconnect.ts +0 -51
package/README.md
CHANGED
|
@@ -187,13 +187,10 @@ import { Actor } from "@cloudflare/actors";
|
|
|
187
187
|
import { withRpc } from "@igoforth/ws-rpc/adapters/cloudflare-do";
|
|
188
188
|
import { ServerSchema, ClientSchema } from "./schemas";
|
|
189
189
|
|
|
190
|
-
//
|
|
191
|
-
//
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
remoteSchema: ClientSchema,
|
|
195
|
-
}) {
|
|
196
|
-
private gameState = { players: [] as string[] };
|
|
190
|
+
// First, create an Actor with the RPC method implementations
|
|
191
|
+
// Methods from localSchema MUST be defined here for type checking
|
|
192
|
+
class GameRoomActor extends Actor<Env> {
|
|
193
|
+
protected gameState = { players: [] as string[] };
|
|
197
194
|
|
|
198
195
|
// Implement methods from ServerSchema
|
|
199
196
|
async getUser({ id }: { id: string }) {
|
|
@@ -203,10 +200,15 @@ export class GameRoom extends withRpc(Actor, {
|
|
|
203
200
|
async createOrder({ product, quantity }: { product: string; quantity: number }) {
|
|
204
201
|
return { orderId: crypto.randomUUID() };
|
|
205
202
|
}
|
|
203
|
+
}
|
|
206
204
|
|
|
205
|
+
// Then apply the RPC mixin to get driver, emit, etc.
|
|
206
|
+
export class GameRoom extends withRpc(GameRoomActor, {
|
|
207
|
+
localSchema: ServerSchema,
|
|
208
|
+
remoteSchema: ClientSchema,
|
|
209
|
+
}) {
|
|
207
210
|
// Use this.driver to call methods on connected clients
|
|
208
211
|
async notifyAllPlayers() {
|
|
209
|
-
// Call ping on all connected clients
|
|
210
212
|
const results = await this.driver.ping({});
|
|
211
213
|
console.log("Ping results:", results);
|
|
212
214
|
}
|
|
@@ -396,25 +398,24 @@ class MyDO extends Actor<Env> {
|
|
|
396
398
|
|
|
397
399
|
## Performance
|
|
398
400
|
|
|
399
|
-
Real WebSocket RPC round-trip benchmarks (
|
|
401
|
+
Real WebSocket RPC round-trip benchmarks (GitHub Actions runner, Node.js 22):
|
|
400
402
|
|
|
401
403
|
**Wire sizes:**
|
|
402
404
|
| Payload | JSON | MessagePack | CBOR |
|
|
403
405
|
|---------|------|-------------|------|
|
|
404
406
|
| Small | 93 B | 71 B | 112 B |
|
|
405
|
-
| Medium | 3.
|
|
406
|
-
| Large | 24.
|
|
407
|
+
| Medium | 3.4 KB | 2.1 KB | 1.3 KB |
|
|
408
|
+
| Large | 24.4 KB | 19.5 KB | 14.1 KB |
|
|
407
409
|
|
|
408
410
|
**Throughput (ops/sec):**
|
|
409
|
-
| Payload | JSON | MessagePack | CBOR |
|
|
410
|
-
|
|
411
|
-
| Small |
|
|
412
|
-
| Medium |
|
|
413
|
-
| Large |
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
Run benchmarks yourself: `pnpm bench`
|
|
411
|
+
| Payload | JSON | MessagePack | CBOR | Fastest |
|
|
412
|
+
|---------|------|-------------|------|---------|
|
|
413
|
+
| Small | 0 | 0 | 0 | JSON |
|
|
414
|
+
| Medium | 0 | 0 | 0 | JSON |
|
|
415
|
+
| Large | 0 | 0 | 0 | JSON |
|
|
416
|
+
|
|
417
|
+
> Benchmarks run automatically via GitHub Actions. Results may vary based on runner load.
|
|
418
|
+
> Run locally with `pnpm bench` for your environment.
|
|
418
419
|
|
|
419
420
|
## Multi-Peer Driver Results
|
|
420
421
|
|
|
@@ -49,15 +49,20 @@ type RpcActorConstructor<TBase extends Constructor<Actor<unknown>>, TLocalSchema
|
|
|
49
49
|
* events: {},
|
|
50
50
|
* } as const;
|
|
51
51
|
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
* // Required: implement methods from ServerSchema
|
|
52
|
+
* // Define methods on the base Actor class
|
|
53
|
+
* class MyActorBase extends Actor<Env> {
|
|
54
|
+
* protected dataList: string[] = [];
|
|
55
|
+
*
|
|
57
56
|
* async getData() {
|
|
58
57
|
* return { data: this.dataList };
|
|
59
58
|
* }
|
|
59
|
+
* }
|
|
60
60
|
*
|
|
61
|
+
* // Apply the RPC mixin
|
|
62
|
+
* class MyDO extends withRpc(MyActorBase, {
|
|
63
|
+
* localSchema: ServerSchema,
|
|
64
|
+
* remoteSchema: ClientSchema,
|
|
65
|
+
* }) {
|
|
61
66
|
* // Call methods on connected clients
|
|
62
67
|
* async notifyClients() {
|
|
63
68
|
* const results = await this.driver.clientMethod({ info: "update" });
|
|
@@ -102,15 +102,20 @@ var DOMultiPeer = class extends MultiPeerBase {
|
|
|
102
102
|
* events: {},
|
|
103
103
|
* } as const;
|
|
104
104
|
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
* // Required: implement methods from ServerSchema
|
|
105
|
+
* // Define methods on the base Actor class
|
|
106
|
+
* class MyActorBase extends Actor<Env> {
|
|
107
|
+
* protected dataList: string[] = [];
|
|
108
|
+
*
|
|
110
109
|
* async getData() {
|
|
111
110
|
* return { data: this.dataList };
|
|
112
111
|
* }
|
|
112
|
+
* }
|
|
113
113
|
*
|
|
114
|
+
* // Apply the RPC mixin
|
|
115
|
+
* class MyDO extends withRpc(MyActorBase, {
|
|
116
|
+
* localSchema: ServerSchema,
|
|
117
|
+
* remoteSchema: ClientSchema,
|
|
118
|
+
* }) {
|
|
114
119
|
* // Call methods on connected clients
|
|
115
120
|
* async notifyClients() {
|
|
116
121
|
* const results = await this.driver.clientMethod({ info: "update" });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@igoforth/ws-rpc",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Bidirectional RPC over WebSocket with Zod schema validation, TypeScript inference, and Cloudflare Durable Object support",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -38,7 +38,6 @@
|
|
|
38
38
|
"types": "./dist/index.d.ts",
|
|
39
39
|
"files": [
|
|
40
40
|
"dist",
|
|
41
|
-
"src",
|
|
42
41
|
"LICENSE",
|
|
43
42
|
"README.md"
|
|
44
43
|
],
|
package/src/adapters/client.ts
DELETED
|
@@ -1,396 +0,0 @@
|
|
|
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
|
-
}
|