@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,404 @@
1
+ /**
2
+ * Multi-Peer Base Class
3
+ *
4
+ * Abstract base class for adapters managing multiple RPC peers.
5
+ * Extended by RpcServer and Cloudflare DO adapter.
6
+ */
7
+
8
+ import { RpcPeer } from "../peers/default.js";
9
+ import type {
10
+ EventDef,
11
+ InferEventData,
12
+ Provider,
13
+ RpcSchema,
14
+ StringKeys,
15
+ } from "../schema.js";
16
+ import type { IMinWebSocket } from "../types.js";
17
+ import type {
18
+ IMultiAdapterHooks,
19
+ IMultiConnectionAdapter,
20
+ MultiCallOptions,
21
+ MultiCallResult,
22
+ MultiDriver,
23
+ } from "./types.js";
24
+
25
+ /**
26
+ * Options for creating a MultiPeerBase subclass
27
+ */
28
+ export interface MultiPeerOptions<
29
+ TLocalSchema extends RpcSchema,
30
+ TRemoteSchema extends RpcSchema,
31
+ > {
32
+ /** Schema defining local methods we implement */
33
+ localSchema: TLocalSchema;
34
+ /** Schema defining remote methods we can call */
35
+ remoteSchema: TRemoteSchema;
36
+ /** Implementation of local methods */
37
+ provider: Provider<TLocalSchema>;
38
+ /** Default timeout for RPC calls in ms */
39
+ timeout?: number;
40
+ /** Lifecycle hooks */
41
+ hooks?: IMultiAdapterHooks<TLocalSchema, TRemoteSchema>;
42
+ }
43
+
44
+ /**
45
+ * Abstract base class for multi-peer adapters
46
+ *
47
+ * Provides shared functionality for managing multiple RPC peers:
48
+ * - Driver for calling methods on multiple peers
49
+ * - Broadcast emit to all peers
50
+ * - Peer lookup by ID
51
+ * - Connection count/IDs
52
+ *
53
+ * @typeParam TLocalSchema - Schema for methods/events this side provides
54
+ * @typeParam TRemoteSchema - Schema for methods/events the remote side provides
55
+ * @typeParam TConnection - Connection type (IWebSocket, WebSocket, etc.)
56
+ */
57
+ export abstract class MultiPeerBase<
58
+ TLocalSchema extends RpcSchema,
59
+ TRemoteSchema extends RpcSchema,
60
+ TConnection,
61
+ > implements IMultiConnectionAdapter<TLocalSchema, TRemoteSchema>
62
+ {
63
+ protected readonly peers = new Map<
64
+ TConnection,
65
+ RpcPeer<TLocalSchema, TRemoteSchema>
66
+ >();
67
+
68
+ /** Local schema */
69
+ public readonly localSchema: TLocalSchema;
70
+
71
+ /** Remote schema */
72
+ public readonly remoteSchema: TRemoteSchema;
73
+
74
+ /** Implementation of local methods */
75
+ public readonly provider: Provider<TLocalSchema>;
76
+
77
+ /** Default timeout for RPC calls */
78
+ public readonly timeout: number;
79
+
80
+ /** Lifecycle hooks */
81
+ public readonly hooks: IMultiAdapterHooks<TLocalSchema, TRemoteSchema>;
82
+
83
+ constructor(options: MultiPeerOptions<TLocalSchema, TRemoteSchema>) {
84
+ this.localSchema = options.localSchema;
85
+ this.remoteSchema = options.remoteSchema;
86
+ this.provider = options.provider;
87
+ this.timeout = options.timeout ?? 30000;
88
+ this.hooks = options.hooks ?? {};
89
+ }
90
+
91
+ // =========================================================================
92
+ // Peer Creation
93
+ // =========================================================================
94
+
95
+ /**
96
+ * Create an RPC peer for a WebSocket connection.
97
+ * Subclasses can override to customize peer creation.
98
+ */
99
+ protected createPeer(
100
+ ws: IMinWebSocket,
101
+ ): RpcPeer<TLocalSchema, TRemoteSchema> {
102
+ return new RpcPeer<TLocalSchema, TRemoteSchema>({
103
+ ws,
104
+ localSchema: this.localSchema,
105
+ remoteSchema: this.remoteSchema,
106
+ provider: this.provider,
107
+ onEvent: this.hooks.onEvent
108
+ ? (event, data) => {
109
+ const peer = this.findPeerByWs(ws);
110
+ if (peer) {
111
+ (
112
+ this.hooks.onEvent as (
113
+ peer: RpcPeer<TLocalSchema, TRemoteSchema>,
114
+ event: string,
115
+ data: unknown,
116
+ ) => void
117
+ )(peer, event, data);
118
+ }
119
+ }
120
+ : undefined,
121
+ timeout: this.timeout,
122
+ });
123
+ }
124
+
125
+ /**
126
+ * Find peer by WebSocket (internal helper for event routing)
127
+ */
128
+ private findPeerByWs(
129
+ ws: IMinWebSocket,
130
+ ): RpcPeer<TLocalSchema, TRemoteSchema> | null {
131
+ for (const peer of this.peers.values()) {
132
+ if (peer.getWebSocket() === ws) {
133
+ return peer;
134
+ }
135
+ }
136
+ return null;
137
+ }
138
+
139
+ // =========================================================================
140
+ // Peer Management
141
+ // =========================================================================
142
+
143
+ /**
144
+ * Add a peer (called by subclass when connection established)
145
+ */
146
+ protected addPeer(
147
+ connection: TConnection,
148
+ peer: RpcPeer<TLocalSchema, TRemoteSchema>,
149
+ ): void {
150
+ this.peers.set(connection, peer);
151
+ this.hooks.onConnect?.(peer);
152
+ }
153
+
154
+ /**
155
+ * Remove a peer (called by subclass when connection closes)
156
+ */
157
+ protected removePeer(
158
+ connection: TConnection,
159
+ ): RpcPeer<TLocalSchema, TRemoteSchema> | null {
160
+ const peer = this.peers.get(connection);
161
+ if (peer) {
162
+ peer.close();
163
+ this.peers.delete(connection);
164
+ this.hooks.onDisconnect?.(peer);
165
+ return peer;
166
+ }
167
+ return null;
168
+ }
169
+
170
+ /**
171
+ * Get peer by connection object
172
+ */
173
+ public getPeerFor(
174
+ connection: TConnection,
175
+ ): RpcPeer<TLocalSchema, TRemoteSchema> | null {
176
+ return this.peers.get(connection) ?? null;
177
+ }
178
+
179
+ /**
180
+ * Get peer by ID
181
+ */
182
+ public getPeer(id: string): RpcPeer<TLocalSchema, TRemoteSchema> | null {
183
+ for (const peer of this.peers.values()) {
184
+ if (peer.id === id) {
185
+ return peer;
186
+ }
187
+ }
188
+ return null;
189
+ }
190
+
191
+ /**
192
+ * Find peer entry by ID (internal - includes connection)
193
+ */
194
+ protected findPeerEntry(id: string): {
195
+ peer: RpcPeer<TLocalSchema, TRemoteSchema>;
196
+ connection: TConnection;
197
+ } | null {
198
+ for (const [connection, peer] of this.peers) {
199
+ if (peer.id === id) {
200
+ return { peer, connection };
201
+ }
202
+ }
203
+ return null;
204
+ }
205
+
206
+ /**
207
+ * Get all peers
208
+ */
209
+ public getPeers(): IterableIterator<RpcPeer<TLocalSchema, TRemoteSchema>> {
210
+ return this.peers.values();
211
+ }
212
+
213
+ /**
214
+ * Get all open peer entries (internal)
215
+ */
216
+ protected getOpenEntries(): Array<{
217
+ peer: RpcPeer<TLocalSchema, TRemoteSchema>;
218
+ connection: TConnection;
219
+ }> {
220
+ const result: Array<{
221
+ peer: RpcPeer<TLocalSchema, TRemoteSchema>;
222
+ connection: TConnection;
223
+ }> = [];
224
+ for (const [connection, peer] of this.peers) {
225
+ if (peer.isOpen) result.push({ peer, connection });
226
+ }
227
+ return result;
228
+ }
229
+
230
+ // =========================================================================
231
+ // IMultiConnectionAdapter Implementation
232
+ // =========================================================================
233
+
234
+ /**
235
+ * Get count of open connections
236
+ */
237
+ public getConnectionCount(): number {
238
+ let count = 0;
239
+ for (const peer of this.peers.values()) {
240
+ if (peer.isOpen) count++;
241
+ }
242
+ return count;
243
+ }
244
+
245
+ /**
246
+ * Get all open peer IDs
247
+ */
248
+ public getConnectionIds(): string[] {
249
+ const ids: string[] = [];
250
+ for (const peer of this.peers.values()) {
251
+ if (peer.isOpen) ids.push(peer.id);
252
+ }
253
+ return ids;
254
+ }
255
+
256
+ /**
257
+ * Driver for calling remote methods on connected peers
258
+ *
259
+ * @returns MultiDriver proxy for calling methods on all or specific peers
260
+ */
261
+ public get driver(): MultiDriver<TRemoteSchema> {
262
+ return this.createMultiDriver();
263
+ }
264
+
265
+ /**
266
+ * Emit an event to connected peers
267
+ *
268
+ * @param event - Event name from local schema
269
+ * @param data - Event data matching the schema
270
+ * @param ids - Optional array of peer IDs to emit to (broadcasts to all if omitted)
271
+ */
272
+ public emit<K extends StringKeys<TLocalSchema["events"]>>(
273
+ event: K,
274
+ data: TLocalSchema["events"] extends Record<string, EventDef>
275
+ ? InferEventData<TLocalSchema["events"][K]>
276
+ : never,
277
+ ids?: string[],
278
+ ): void {
279
+ const validPeers = ids
280
+ ? this.peers.values().filter((p) => ids.includes(p.id) && p.isOpen)
281
+ : this.peers.values().filter((p) => p.isOpen);
282
+ for (const peer of validPeers) peer.emit(event, data);
283
+ }
284
+
285
+ /**
286
+ * Close a specific peer by ID
287
+ *
288
+ * @param id - Peer ID to close
289
+ * @returns true if peer was found and closed, false otherwise
290
+ */
291
+ public closePeer(id: string): boolean {
292
+ const entry = this.findPeerEntry(id);
293
+ if (entry) {
294
+ entry.peer.close();
295
+ this.peers.delete(entry.connection);
296
+ this.hooks.onDisconnect?.(entry.peer);
297
+ return true;
298
+ }
299
+ return false;
300
+ }
301
+
302
+ /**
303
+ * Close all peers
304
+ */
305
+ protected closeAll(): void {
306
+ for (const peer of this.peers.values()) {
307
+ peer.close();
308
+ this.hooks.onDisconnect?.(peer);
309
+ }
310
+ this.peers.clear();
311
+ this.hooks.onClose?.();
312
+ }
313
+
314
+ // =========================================================================
315
+ // Driver Implementation
316
+ // =========================================================================
317
+
318
+ /**
319
+ * Create a driver proxy for calling remote methods on multiple peers
320
+ */
321
+ private createMultiDriver(): MultiDriver<TRemoteSchema> {
322
+ const methods = this.remoteSchema.methods ?? {};
323
+ const driver: Record<
324
+ string,
325
+ (input: unknown, options?: MultiCallOptions) => Promise<unknown>
326
+ > = {};
327
+
328
+ for (const methodName of Object.keys(methods)) {
329
+ driver[methodName] = async (
330
+ input: unknown,
331
+ options?: MultiCallOptions,
332
+ ) => {
333
+ return this.callMethod(methodName, input, options);
334
+ };
335
+ }
336
+
337
+ return driver as MultiDriver<TRemoteSchema>;
338
+ }
339
+
340
+ /**
341
+ * Call a method on multiple peers with timeout handling
342
+ *
343
+ * @param method - Method name to call
344
+ * @param input - Method input parameters
345
+ * @param options - Call options including target peer IDs and timeout
346
+ * @returns Array of results from each peer (success or error)
347
+ */
348
+ private async callMethod(
349
+ method: string,
350
+ input: unknown,
351
+ options?: MultiCallOptions,
352
+ ): Promise<Array<MultiCallResult<unknown>>> {
353
+ const ids = options?.ids;
354
+ const timeout = options?.timeout ?? this.timeout;
355
+
356
+ // Determine which peers to call
357
+ let targetPeers: Array<RpcPeer<TLocalSchema, TRemoteSchema>>;
358
+
359
+ if (ids === undefined) {
360
+ targetPeers = Array.from(this.peers.values()).filter((p) => p.isOpen);
361
+ } else if (typeof ids === "string") {
362
+ const peer = this.getPeer(ids);
363
+ targetPeers = peer?.isOpen ? [peer] : [];
364
+ } else {
365
+ targetPeers = ids
366
+ .map((id) => this.getPeer(id))
367
+ .filter(
368
+ (p): p is RpcPeer<TLocalSchema, TRemoteSchema> => p?.isOpen === true,
369
+ );
370
+ }
371
+
372
+ const promises = targetPeers.map(async (peer) => {
373
+ try {
374
+ const peerDriver = peer.driver as Record<
375
+ string,
376
+ (input: unknown) => Promise<unknown>
377
+ >;
378
+ const callPromise = peerDriver[method]!(input);
379
+
380
+ const value = await Promise.race([
381
+ callPromise,
382
+ new Promise<never>((_, reject) =>
383
+ setTimeout(
384
+ () => reject(new Error(`Timeout after ${timeout}ms`)),
385
+ timeout,
386
+ ),
387
+ ),
388
+ ]);
389
+
390
+ return { id: peer.id, result: { success: true as const, value } };
391
+ } catch (error) {
392
+ return {
393
+ id: peer.id,
394
+ result: {
395
+ success: false as const,
396
+ error: error instanceof Error ? error : new Error(String(error)),
397
+ },
398
+ };
399
+ }
400
+ });
401
+
402
+ return Promise.all(promises);
403
+ }
404
+ }
@@ -0,0 +1,192 @@
1
+ /**
2
+ * RPC Server Adapter
3
+ *
4
+ * Manages RpcPeer instances for incoming WebSocket connections.
5
+ * Works with Node.js `ws`, Bun's native WebSocket, or any compatible server.
6
+ */
7
+
8
+ import { RpcPeer } from "../peers/default.js";
9
+ import type { Provider, RpcSchema } from "../schema.js";
10
+ import {
11
+ type IRpcOptions,
12
+ type IWebSocket,
13
+ type IWebSocketServer,
14
+ WebSocketReadyState,
15
+ type WebSocketServerOptions,
16
+ } from "../types.js";
17
+ import { MultiPeerBase } from "./multi-peer.js";
18
+ import type { IMultiAdapterHooks } from "./types.js";
19
+
20
+ /**
21
+ * Options for creating an RpcServer
22
+ */
23
+ export interface RpcServerOptions<
24
+ TLocalSchema extends RpcSchema,
25
+ TRemoteSchema extends RpcSchema,
26
+ > extends IRpcOptions<TLocalSchema, TRemoteSchema> {
27
+ /** Implementation of local methods */
28
+ provider: Provider<TLocalSchema>;
29
+ /** WebSocket server instance or options to create one */
30
+ wss: IWebSocketServer | WebSocketServerOptions;
31
+ /** WebSocket server constructor (defaults to require('ws').WebSocketServer) */
32
+ WebSocketServer?: new (
33
+ options: WebSocketServerOptions,
34
+ ) => IWebSocketServer;
35
+ /** Lifecycle hooks */
36
+ hooks?: IMultiAdapterHooks<TLocalSchema, TRemoteSchema>;
37
+ }
38
+
39
+ /**
40
+ * RPC Server
41
+ *
42
+ * Manages WebSocket server and client connections with RPC capabilities.
43
+ *
44
+ * @example
45
+ * ```typescript
46
+ * import { WebSocketServer } from "ws";
47
+ * import { RpcServer } from "@igoforth/ws-rpc/adapters/server";
48
+ *
49
+ * const server = new RpcServer({
50
+ * wss: { port: 8080 },
51
+ * WebSocketServer,
52
+ * localSchema: ServerSchema,
53
+ * remoteSchema: ClientSchema,
54
+ * provider: {
55
+ * getUser: async ({ id }) => ({ name: "John", email: "john@example.com" }),
56
+ * },
57
+ * hooks: {
58
+ * onConnect: (peer) => {
59
+ * console.log(`Client ${peer.id} connected`);
60
+ * peer.driver.ping({}).then(console.log);
61
+ * },
62
+ * onDisconnect: (peer) => console.log(`Client ${peer.id} disconnected`),
63
+ * },
64
+ * });
65
+ *
66
+ * // Emit to all clients
67
+ * server.emit("orderUpdated", { orderId: "123", status: "shipped" });
68
+ *
69
+ * // Graceful shutdown
70
+ * process.on("SIGTERM", () => server.close());
71
+ * ```
72
+ */
73
+ export class RpcServer<
74
+ TLocalSchema extends RpcSchema,
75
+ TRemoteSchema extends RpcSchema,
76
+ > extends MultiPeerBase<TLocalSchema, TRemoteSchema, IWebSocket> {
77
+ private readonly wss: IWebSocketServer;
78
+
79
+ constructor(options: RpcServerOptions<TLocalSchema, TRemoteSchema>) {
80
+ super({
81
+ localSchema: options.localSchema,
82
+ remoteSchema: options.remoteSchema,
83
+ provider: options.provider,
84
+ ...(options.timeout !== undefined && { timeout: options.timeout }),
85
+ ...(options.hooks !== undefined && { hooks: options.hooks }),
86
+ });
87
+
88
+ // Create or use existing WebSocket server
89
+ if ("on" in options.wss && typeof options.wss.on === "function") {
90
+ this.wss = options.wss as IWebSocketServer;
91
+ } else {
92
+ if (!options.WebSocketServer) {
93
+ throw new Error(
94
+ "WebSocketServer constructor required when passing options",
95
+ );
96
+ }
97
+ this.wss = new options.WebSocketServer(
98
+ options.wss as WebSocketServerOptions,
99
+ );
100
+ }
101
+
102
+ // Set up server event handlers
103
+ this.wss.on("connection", (ws) => this.handleConnection(ws));
104
+ this.wss.on("error", (error) => this.hooks.onError?.(null, error));
105
+ this.wss.on("close", () => this.hooks.onClose?.());
106
+ }
107
+
108
+ // =========================================================================
109
+ // Connection Handling
110
+ // =========================================================================
111
+
112
+ private handleConnection(ws: IWebSocket): void {
113
+ const peer = new RpcPeer({
114
+ ws,
115
+ localSchema: this.localSchema,
116
+ remoteSchema: this.remoteSchema,
117
+ provider: this.provider,
118
+ timeout: this.timeout,
119
+ onEvent: (event, data) => {
120
+ this.hooks.onEvent?.(peer, event, data);
121
+ },
122
+ });
123
+
124
+ this.addPeer(ws, peer);
125
+
126
+ ws.onmessage = (event) => {
127
+ if (
128
+ typeof event === "object" &&
129
+ event != null &&
130
+ "data" in event &&
131
+ (typeof event.data === "string" || event.data instanceof ArrayBuffer)
132
+ ) {
133
+ peer.handleMessage(event.data as string | ArrayBuffer);
134
+ }
135
+ };
136
+
137
+ ws.onclose = () => {
138
+ this.removePeer(ws);
139
+ };
140
+
141
+ ws.onerror = (event) => {
142
+ const error =
143
+ event instanceof Error
144
+ ? event
145
+ : new Error(`WebSocket error for peer ${peer.id}`);
146
+ this.hooks.onError?.(peer, error);
147
+ };
148
+ }
149
+
150
+ // =========================================================================
151
+ // Server-Specific Methods
152
+ // =========================================================================
153
+
154
+ /**
155
+ * Close a peer connection with WebSocket close code/reason
156
+ *
157
+ * @param id - Peer ID to close
158
+ * @param code - WebSocket close code (default: 1000)
159
+ * @param reason - Close reason message (default: "Server disconnect")
160
+ * @returns true if peer was found and closed, false otherwise
161
+ */
162
+ override closePeer(
163
+ id: string,
164
+ code = 1000,
165
+ reason = "Server disconnect",
166
+ ): boolean {
167
+ const entry = this.findPeerEntry(id);
168
+ if (entry) {
169
+ if (entry.connection.readyState !== WebSocketReadyState.CLOSED) {
170
+ entry.connection.close(code, reason);
171
+ }
172
+ return super.closePeer(id);
173
+ }
174
+ return false;
175
+ }
176
+
177
+ /**
178
+ * Close the server and all client connections
179
+ *
180
+ * @param callback - Optional callback invoked when server is closed
181
+ */
182
+ close(callback?: (err?: Error) => void): void {
183
+ for (const entry of this.getOpenEntries()) {
184
+ if (entry.connection.readyState !== WebSocketReadyState.CLOSED) {
185
+ entry.connection.close(1001, "Server shutdown");
186
+ }
187
+ }
188
+
189
+ this.closeAll();
190
+ this.wss.close(callback);
191
+ }
192
+ }