@igoforth/ws-rpc 1.0.2 → 1.1.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.
package/README.md CHANGED
@@ -185,6 +185,7 @@ process.on("SIGTERM", () => server.close());
185
185
  ```typescript
186
186
  import { Actor } from "@cloudflare/actors";
187
187
  import { withRpc } from "@igoforth/ws-rpc/adapters/cloudflare-do";
188
+ import { RpcPeer } from "@igoforth/ws-rpc/peers";
188
189
  import { ServerSchema, ClientSchema } from "./schemas";
189
190
 
190
191
  // First, create an Actor with the RPC method implementations
@@ -222,138 +223,32 @@ export class GameRoom extends withRpc(GameRoomActor, {
222
223
  getPlayerCount() {
223
224
  return this.getConnectionCount();
224
225
  }
225
- }
226
- ```
227
-
228
- ## API Reference
229
-
230
- ### Schema Definition
231
-
232
- ```typescript
233
- import { method, event } from "@igoforth/ws-rpc/schema";
234
- import { z } from "zod";
235
-
236
- // Define a method with input/output validation
237
- const myMethod = method({
238
- input: z.object({ /* ... */ }),
239
- output: z.object({ /* ... */ }),
240
- });
241
226
 
242
- // Define an event (fire-and-forget)
243
- const myEvent = event({
244
- data: z.object({ /* ... */ }),
245
- });
246
-
247
- // Combine into a schema
248
- const MySchema = {
249
- methods: { myMethod },
250
- events: { myEvent },
251
- } satisfies RpcSchema;
252
- ```
253
-
254
- ### Type Inference
255
-
256
- ```typescript
257
- import type { Provider, Driver, InferInput, InferOutput } from "@igoforth/ws-rpc/schema";
258
-
259
- // Get the provider type (what you implement)
260
- type MyProvider = Provider<typeof MySchema>;
261
-
262
- // Get the driver type (what you call)
263
- type MyDriver = Driver<typeof MySchema>;
264
-
265
- // Get input/output types for a specific method
266
- type MyMethodInput = InferInput<typeof MySchema["methods"]["myMethod"]>;
267
- type MyMethodOutput = InferOutput<typeof MySchema["methods"]["myMethod"]>;
268
- ```
269
-
270
- ### Codecs
271
-
272
- ```typescript
273
- import { createMsgpackCodec } from "@igoforth/ws-rpc/codecs/msgpack";
274
- import { createCborCodec } from "@igoforth/ws-rpc/codecs/cbor";
275
- import { createJsonCodec } from "@igoforth/ws-rpc/codecs/json";
276
-
277
- // JSON (default)
278
- const jsonCodec = createJsonCodec(z.unknown());
279
-
280
- // MessagePack (requires @msgpack/msgpack)
281
- const msgpackCodec = createMsgpackCodec(z.unknown());
227
+ // Override RPC lifecycle hooks
228
+ protected override onRpcConnect(peer: RpcPeer<typeof ServerSchema, typeof ClientSchema>) {
229
+ console.log(`Player ${peer.id} joined`);
230
+ }
282
231
 
283
- // CBOR (requires cbor-x)
284
- const cborCodec = createCborCodec(z.unknown());
285
- ```
232
+ protected override onRpcDisconnect(peer: RpcPeer<typeof ServerSchema, typeof ClientSchema>) {
233
+ console.log(`Player ${peer.id} left`);
234
+ }
286
235
 
287
- ### Error Handling
236
+ protected override onRpcError(
237
+ peer: RpcPeer<typeof ServerSchema, typeof ClientSchema> | null,
238
+ error: Error,
239
+ ) {
240
+ console.error(`Error from ${peer?.id}:`, error);
241
+ }
288
242
 
289
- ```typescript
290
- import {
291
- RpcError,
292
- RpcTimeoutError,
293
- RpcRemoteError,
294
- RpcConnectionClosed,
295
- RpcValidationError,
296
- RpcMethodNotFoundError,
297
- } from "@igoforth/ws-rpc/errors";
298
-
299
- try {
300
- await client.driver.someMethod({ /* ... */ });
301
- } catch (error) {
302
- if (error instanceof RpcTimeoutError) {
303
- console.log(`Request '${error.method}' timed out after ${error.timeoutMs}ms`);
304
- } else if (error instanceof RpcValidationError) {
305
- console.log("Invalid input/output:", error.message);
306
- } else if (error instanceof RpcRemoteError) {
307
- console.log("Server error:", error.message, error.code);
308
- } else if (error instanceof RpcMethodNotFoundError) {
309
- console.log(`Method '${error.method}' not found`);
310
- } else if (error instanceof RpcConnectionClosed) {
311
- console.log("Connection closed");
243
+ protected override onRpcPeerRecreated(
244
+ peer: RpcPeer<typeof ServerSchema, typeof ClientSchema>,
245
+ ws: WebSocket,
246
+ ) {
247
+ console.log(`Player ${peer.id} recovered from hibernation`);
312
248
  }
313
249
  }
314
250
  ```
315
251
 
316
- ### Client Options
317
-
318
- ```typescript
319
- const client = new RpcClient({
320
- url: "wss://...",
321
- localSchema: MyLocalSchema,
322
- remoteSchema: MyRemoteSchema,
323
- provider: { /* method implementations */ },
324
-
325
- // Reconnection options (set to false to disable)
326
- reconnect: {
327
- initialDelay: 1000, // First retry delay (ms)
328
- maxDelay: 30000, // Maximum retry delay (ms)
329
- backoffMultiplier: 2, // Exponential backoff multiplier
330
- maxAttempts: 0, // Max attempts (0 = unlimited)
331
- jitter: 0.1, // Random jitter factor (0-1)
332
- },
333
-
334
- // Request timeout (ms)
335
- timeout: 30000,
336
-
337
- // Auto-connect on creation (default: false)
338
- autoConnect: true,
339
-
340
- // WebSocket options
341
- protocols: ["v1"], // Subprotocols
342
- headers: { Authorization: "Bearer ..." }, // Headers (Node.js/Bun only)
343
-
344
- // Event handlers
345
- onConnect: () => console.log("Connected"),
346
- onDisconnect: (code, reason) => console.log("Disconnected"),
347
- onReconnect: (attempt, delay) => console.log(`Reconnecting in ${delay}ms`),
348
- onReconnectFailed: () => console.log("Reconnection failed"),
349
- onEvent: (event, data) => console.log("Event:", event, data),
350
- });
351
-
352
- // Connection state
353
- client.state; // "disconnected" | "connecting" | "connected" | "reconnecting"
354
- client.isConnected; // boolean
355
- ```
356
-
357
252
  ## Hibernation-Safe Durable Objects
358
253
 
359
254
  For Cloudflare Durable Objects that need hibernation-safe outgoing calls, use `DurableRpcPeer` with continuation-passing style:
@@ -410,9 +305,9 @@ Real WebSocket RPC round-trip benchmarks (GitHub Actions runner, Node.js 22):
410
305
  **Throughput (ops/sec):**
411
306
  | Payload | JSON | MessagePack | CBOR | Fastest |
412
307
  |---------|------|-------------|------|---------|
413
- | Small | 0 | 0 | 0 | JSON |
414
- | Medium | 0 | 0 | 0 | JSON |
415
- | Large | 0 | 0 | 0 | JSON |
308
+ | Small | 1443 | 1798 | 1812 | CBOR |
309
+ | Medium | 1514 | 1591 | 5142 | CBOR |
310
+ | Large | 1656 | 1270 | 1999 | CBOR |
416
311
 
417
312
  > Benchmarks run automatically via GitHub Actions. Results may vary based on runner load.
418
313
  > Run locally with `pnpm bench` for your environment.
@@ -2,9 +2,9 @@ import { a as InferEventData, h as StringKeys, m as RpcSchema, n as EventDef, p
2
2
  import "../factory-BGezZgAP.js";
3
3
  import "../json-BZzd4eNj.js";
4
4
  import "../index-Be7jjS77.js";
5
- import "../protocol-ipDMUaRc.js";
6
- import { a as IRpcOptions, c as WebSocketOptions, o as IWebSocket } from "../types-BVaXox7F.js";
7
- import "../default-C5z3UDd1.js";
5
+ import { u as RpcProtocol } from "../protocol-ipDMUaRc.js";
6
+ import { a as IRpcOptions, c as WebSocketOptions, o as IWebSocket } from "../types-BGM8eZe5.js";
7
+ import "../default-DOftsqyK.js";
8
8
  import { t as ReconnectOptions } from "../reconnect-DbcN0R_1.js";
9
9
  import { IAdapterHooks, IConnectionAdapter } from "./types.js";
10
10
 
@@ -41,14 +41,12 @@ type ConnectionState = "disconnected" | "connecting" | "connected" | "reconnecti
41
41
  declare class RpcClient<TLocalSchema extends RpcSchema, TRemoteSchema extends RpcSchema> implements IConnectionAdapter<TLocalSchema, TRemoteSchema> {
42
42
  readonly localSchema: TLocalSchema;
43
43
  readonly remoteSchema: TRemoteSchema;
44
+ readonly timeout: number;
45
+ readonly protocol?: RpcProtocol;
44
46
  readonly provider: Provider<TLocalSchema>;
45
47
  readonly hooks: IAdapterHooks<TRemoteSchema>;
46
- private readonly url;
47
48
  private readonly reconnectOptions;
48
- private readonly defaultTimeout;
49
- private readonly protocols;
50
- private readonly headers;
51
- private readonly WebSocketImpl;
49
+ private readonly createWebSocket;
52
50
  private ws;
53
51
  private peer;
54
52
  private _state;
@@ -5,7 +5,7 @@ import "../protocol-_mpoOPp6.js";
5
5
  import "../errors-5BfreE63.js";
6
6
  import { t as WebSocketReadyState } from "../types-D_psiH09.js";
7
7
  import { n as defaultReconnectOptions, t as calculateReconnectDelay } from "../reconnect-CGAA_1Gf.js";
8
- import { t as RpcPeer } from "../default-BkrMd28n.js";
8
+ import { t as RpcPeer } from "../default-B6CVJfTD.js";
9
9
  import "./types.js";
10
10
 
11
11
  //#region src/adapters/client.ts
@@ -17,14 +17,12 @@ import "./types.js";
17
17
  var RpcClient = class {
18
18
  localSchema;
19
19
  remoteSchema;
20
+ timeout;
21
+ protocol;
20
22
  provider;
21
23
  hooks = {};
22
- url;
23
24
  reconnectOptions;
24
- defaultTimeout;
25
- protocols;
26
- headers;
27
- WebSocketImpl;
25
+ createWebSocket;
28
26
  ws = null;
29
27
  peer = null;
30
28
  _state = "disconnected";
@@ -32,18 +30,25 @@ var RpcClient = class {
32
30
  reconnectTimeout = null;
33
31
  intentionalClose = false;
34
32
  constructor(options) {
35
- this.url = options.url;
36
33
  this.localSchema = options.localSchema;
37
34
  this.remoteSchema = options.remoteSchema;
35
+ this.timeout = options.timeout ?? 3e4;
36
+ if (options.protocol) this.protocol = options.protocol;
38
37
  this.provider = options.provider;
39
38
  this.reconnectOptions = options.reconnect === false ? false : {
40
39
  ...defaultReconnectOptions,
41
40
  ...options.reconnect
42
41
  };
43
- this.defaultTimeout = options.timeout ?? 3e4;
44
- this.protocols = options.protocols;
45
- this.headers = options.headers;
46
- this.WebSocketImpl = options.WebSocket ?? globalThis.WebSocket;
42
+ const url = options.url;
43
+ const protocols = options.protocols;
44
+ const headers = options.headers;
45
+ const WebSocketImpl = options.WebSocket ?? globalThis.WebSocket;
46
+ this.createWebSocket = () => {
47
+ return new WebSocketImpl(url, headers ? {
48
+ headers,
49
+ ...protocols && { protocols }
50
+ } : protocols);
51
+ };
47
52
  if (options.onConnect) this.hooks.onConnect = options.onConnect;
48
53
  if (options.onDisconnect) this.hooks.onDisconnect = options.onDisconnect;
49
54
  if (options.onReconnect) this.hooks.onReconnect = options.onReconnect;
@@ -98,12 +103,7 @@ var RpcClient = class {
98
103
  this._state = "connecting";
99
104
  return new Promise((resolve, reject) => {
100
105
  try {
101
- let wsOptions;
102
- if (this.headers) {
103
- wsOptions = { headers: this.headers };
104
- if (this.protocols) wsOptions.protocols = this.protocols;
105
- } else wsOptions = this.protocols;
106
- this.ws = new this.WebSocketImpl(this.url, wsOptions);
106
+ this.ws = this.createWebSocket();
107
107
  } catch (error) {
108
108
  this._state = "disconnected";
109
109
  reject(error);
@@ -165,8 +165,9 @@ var RpcClient = class {
165
165
  localSchema: this.localSchema,
166
166
  remoteSchema: this.remoteSchema,
167
167
  provider: this.provider,
168
+ ...this.protocol !== void 0 && { protocol: this.protocol },
168
169
  onEvent: this.hooks.onEvent,
169
- timeout: this.defaultTimeout
170
+ timeout: this.timeout
170
171
  });
171
172
  this.ws.onmessage = (event) => {
172
173
  if (typeof event === "object" && event != null && "data" in event) this.peer?.handleMessage(event.data);
@@ -1,10 +1,10 @@
1
- import { m as RpcSchema, p as Provider } from "../schema-BPJJ9slm.js";
1
+ import { a as InferEventData, h as StringKeys, m as RpcSchema, n as EventDef, p as Provider } from "../schema-BPJJ9slm.js";
2
2
  import "../factory-BGezZgAP.js";
3
3
  import "../json-BZzd4eNj.js";
4
4
  import "../index-Be7jjS77.js";
5
5
  import "../protocol-ipDMUaRc.js";
6
- import { a as IRpcOptions } from "../types-BVaXox7F.js";
7
- import { t as RpcPeer } from "../default-C5z3UDd1.js";
6
+ import { a as IRpcOptions } from "../types-BGM8eZe5.js";
7
+ import { t as RpcPeer } from "../default-DOftsqyK.js";
8
8
  import { IMultiAdapterHooks, IMultiConnectionAdapter } from "./types.js";
9
9
  import { Constructor } from "type-fest";
10
10
  import { Actor } from "@cloudflare/actors";
@@ -18,6 +18,22 @@ interface IDOHooks<TLocalSchema extends RpcSchema, TRemoteSchema extends RpcSche
18
18
  /** Called when a peer is recreated after hibernation */
19
19
  onPeerRecreated?(peer: RpcPeer<TLocalSchema, TRemoteSchema>, ws: WebSocket): void;
20
20
  }
21
+ /**
22
+ * Interface for overridable RPC hooks exposed by the mixin.
23
+ * Subclasses can override these to handle RPC lifecycle events.
24
+ */
25
+ interface IRpcActorHooks<TLocalSchema extends RpcSchema, TRemoteSchema extends RpcSchema> {
26
+ /** Called when a peer connects. Override to handle connection events. */
27
+ onRpcConnect(peer: RpcPeer<TLocalSchema, TRemoteSchema>): void;
28
+ /** Called when a peer disconnects. Override to handle disconnection events. */
29
+ onRpcDisconnect(peer: RpcPeer<TLocalSchema, TRemoteSchema>): void;
30
+ /** Called when an event is received from a peer. Override to handle events. */
31
+ onRpcEvent<K extends StringKeys<TRemoteSchema["events"]>>(peer: RpcPeer<TLocalSchema, TRemoteSchema>, event: K, data: TRemoteSchema["events"] extends Record<string, EventDef> ? InferEventData<TRemoteSchema["events"][K]> : never): void;
32
+ /** Called when a peer encounters an error. Override to handle errors. */
33
+ onRpcError(peer: RpcPeer<TLocalSchema, TRemoteSchema> | null, error: Error): void;
34
+ /** Called when a peer is recreated after hibernation. Override to handle recovery. */
35
+ onRpcPeerRecreated(peer: RpcPeer<TLocalSchema, TRemoteSchema>, ws: WebSocket): void;
36
+ }
21
37
  /**
22
38
  * Constructor type for the RPC mixin result.
23
39
  *
@@ -25,7 +41,7 @@ interface IDOHooks<TLocalSchema extends RpcSchema, TRemoteSchema extends RpcSche
25
41
  * Runtime enforces this when methods are called via RPC.
26
42
  */
27
43
  type RpcActorConstructor<TBase extends Constructor<Actor<unknown>>, TLocalSchema extends RpcSchema, TRemoteSchema extends RpcSchema> = {
28
- new (...args: ConstructorParameters<TBase>): InstanceType<TBase> & IMultiConnectionAdapter<TLocalSchema, TRemoteSchema>;
44
+ new (...args: ConstructorParameters<TBase>): InstanceType<TBase> & IMultiConnectionAdapter<TLocalSchema, TRemoteSchema> & IRpcActorHooks<TLocalSchema, TRemoteSchema>;
29
45
  } & Omit<TBase, "new">;
30
46
  /**
31
47
  * Create a mixin that adds RPC capabilities to a Durable Object Actor.
@@ -74,4 +90,4 @@ declare function withRpc<TLocalSchema extends RpcSchema, TRemoteSchema extends R
74
90
  prototype: Provider<TLocalSchema>;
75
91
  }>(Base: TBase, options: IRpcOptions<TLocalSchema, TRemoteSchema>): RpcActorConstructor<TBase, TLocalSchema, TRemoteSchema>;
76
92
  //#endregion
77
- export { IDOHooks, RpcActorConstructor, withRpc };
93
+ export { IDOHooks, IRpcActorHooks, RpcActorConstructor, withRpc };
@@ -4,9 +4,9 @@ import "../codecs-BmYG2d_U.js";
4
4
  import "../protocol-_mpoOPp6.js";
5
5
  import "../errors-5BfreE63.js";
6
6
  import { t as SqlPendingCallStorage } from "../sql-CCjc6Bid.js";
7
- import "../default-BkrMd28n.js";
8
- import { n as createDurableRpcPeerFactory } from "../durable-MZjkvyS6.js";
9
- import { t as MultiPeerBase } from "../multi-peer-BAi9yVzp.js";
7
+ import "../default-B6CVJfTD.js";
8
+ import { n as createDurableRpcPeerFactory } from "../durable-DENcbnTp.js";
9
+ import { t as MultiPeerBase } from "../multi-peer-DFUqFBsx.js";
10
10
 
11
11
  //#region src/adapters/cloudflare-do.ts
12
12
  /**
@@ -34,6 +34,7 @@ var DOMultiPeer = class extends MultiPeerBase {
34
34
  localSchema: this.localSchema,
35
35
  remoteSchema: this.remoteSchema,
36
36
  provider,
37
+ ...this.protocol !== void 0 && { protocol: this.protocol },
37
38
  onEvent: (event, data) => {
38
39
  this.hooks.onEvent?.(peer, event, data);
39
40
  },
@@ -142,9 +143,27 @@ function withRpc(Base, options) {
142
143
  localSchema: options.localSchema,
143
144
  remoteSchema: options.remoteSchema,
144
145
  provider: this,
145
- ...options.timeout !== void 0 && { timeout: options.timeout }
146
+ ...options.timeout !== void 0 && { timeout: options.timeout },
147
+ ...options.protocol !== void 0 && { protocol: options.protocol },
148
+ hooks: {
149
+ onConnect: (peer) => this.onRpcConnect(peer),
150
+ onDisconnect: (peer) => this.onRpcDisconnect(peer),
151
+ onEvent: (peer, event, data) => this.onRpcEvent(peer, event, data),
152
+ onError: (peer, error) => this.onRpcError(peer, error),
153
+ onPeerRecreated: (peer, ws) => this.onRpcPeerRecreated(peer, ws)
154
+ }
146
155
  });
147
156
  }
157
+ /** Called when a peer connects. Override to handle connection events. */
158
+ onRpcConnect(_peer) {}
159
+ /** Called when a peer disconnects. Override to handle disconnection events. */
160
+ onRpcDisconnect(_peer) {}
161
+ /** Called when an event is received from a peer. Override to handle events. */
162
+ onRpcEvent(_peer, _event, _data) {}
163
+ /** Called when a peer encounters an error. Override to handle errors. */
164
+ onRpcError(_peer, _error) {}
165
+ /** Called when a peer is recreated after hibernation. Override to handle recovery. */
166
+ onRpcPeerRecreated(_peer, _ws) {}
148
167
  /**
149
168
  * Driver for calling methods on connected clients
150
169
  */
@@ -3,11 +3,11 @@ import "../factory-BGezZgAP.js";
3
3
  import "../json-BZzd4eNj.js";
4
4
  import "../index-Be7jjS77.js";
5
5
  import "../protocol-ipDMUaRc.js";
6
- import "../types-BVaXox7F.js";
7
- import "../default-C5z3UDd1.js";
6
+ import "../types-BGM8eZe5.js";
7
+ import "../default-DOftsqyK.js";
8
8
  import { n as calculateReconnectDelay, r as defaultReconnectOptions, t as ReconnectOptions } from "../reconnect-DbcN0R_1.js";
9
9
  import { IAdapterHooks, IConnectionAdapter, IMultiAdapterHooks, IMultiConnectionAdapter, MultiCallOptions, MultiCallResult, MultiDriver } from "./types.js";
10
10
  import { ConnectionState, RpcClient, RpcClientOptions } from "./client.js";
11
- import { RpcActorConstructor, withRpc } from "./cloudflare-do.js";
12
- import { n as RpcServerOptions, r as MultiPeerBase, t as RpcServer } from "../server-C5EvcG9T.js";
13
- export { type ConnectionState, IAdapterHooks, IConnectionAdapter, IMultiAdapterHooks, IMultiConnectionAdapter, MultiCallOptions, MultiCallResult, MultiDriver, MultiPeerBase, ReconnectOptions, type RpcActorConstructor, RpcClient, type RpcClientOptions, RpcServer, type RpcServerOptions, calculateReconnectDelay, defaultReconnectOptions, withRpc };
11
+ import { IDOHooks, IRpcActorHooks, RpcActorConstructor, withRpc } from "./cloudflare-do.js";
12
+ import { n as RpcServerOptions, r as MultiPeerBase, t as RpcServer } from "../server-BJm2ViQB.js";
13
+ export { type ConnectionState, IAdapterHooks, IConnectionAdapter, type IDOHooks, IMultiAdapterHooks, IMultiConnectionAdapter, type IRpcActorHooks, MultiCallOptions, MultiCallResult, MultiDriver, MultiPeerBase, ReconnectOptions, type RpcActorConstructor, RpcClient, type RpcClientOptions, RpcServer, type RpcServerOptions, calculateReconnectDelay, defaultReconnectOptions, withRpc };
@@ -5,11 +5,11 @@ import "../protocol-_mpoOPp6.js";
5
5
  import "../errors-5BfreE63.js";
6
6
  import "../sql-CCjc6Bid.js";
7
7
  import { n as defaultReconnectOptions, t as calculateReconnectDelay } from "../reconnect-CGAA_1Gf.js";
8
- import "../default-BkrMd28n.js";
9
- import "../durable-MZjkvyS6.js";
8
+ import "../default-B6CVJfTD.js";
9
+ import "../durable-DENcbnTp.js";
10
10
  import "./types.js";
11
11
  import { RpcClient } from "./client.js";
12
- import { t as MultiPeerBase } from "../multi-peer-BAi9yVzp.js";
12
+ import { t as MultiPeerBase } from "../multi-peer-DFUqFBsx.js";
13
13
  import { withRpc } from "./cloudflare-do.js";
14
14
  import { RpcServer } from "./server.js";
15
15
 
@@ -3,8 +3,8 @@ import "../factory-BGezZgAP.js";
3
3
  import "../json-BZzd4eNj.js";
4
4
  import "../index-Be7jjS77.js";
5
5
  import "../protocol-ipDMUaRc.js";
6
- import "../types-BVaXox7F.js";
7
- import "../default-C5z3UDd1.js";
6
+ import "../types-BGM8eZe5.js";
7
+ import "../default-DOftsqyK.js";
8
8
  import "./types.js";
9
- import { n as RpcServerOptions, t as RpcServer } from "../server-C5EvcG9T.js";
9
+ import { n as RpcServerOptions, t as RpcServer } from "../server-BJm2ViQB.js";
10
10
  export { RpcServer, RpcServerOptions };
@@ -4,8 +4,8 @@ import "../codecs-BmYG2d_U.js";
4
4
  import "../protocol-_mpoOPp6.js";
5
5
  import "../errors-5BfreE63.js";
6
6
  import { t as WebSocketReadyState } from "../types-D_psiH09.js";
7
- import { t as RpcPeer } from "../default-BkrMd28n.js";
8
- import { t as MultiPeerBase } from "../multi-peer-BAi9yVzp.js";
7
+ import { t as RpcPeer } from "../default-B6CVJfTD.js";
8
+ import { t as MultiPeerBase } from "../multi-peer-DFUqFBsx.js";
9
9
 
10
10
  //#region src/adapters/server.ts
11
11
  /**
@@ -15,6 +15,14 @@ import { t as MultiPeerBase } from "../multi-peer-BAi9yVzp.js";
15
15
  * Works with Node.js `ws`, Bun's native WebSocket, or any compatible server.
16
16
  */
17
17
  /**
18
+ * Create or return existing WebSocket server
19
+ */
20
+ function createWebSocketServer(wss, WebSocketServer) {
21
+ if ("on" in wss && typeof wss.on === "function") return wss;
22
+ if (!WebSocketServer) throw new Error("WebSocketServer constructor required when passing options");
23
+ return new WebSocketServer(wss);
24
+ }
25
+ /**
18
26
  * RPC Server
19
27
  *
20
28
  * Manages WebSocket server and client connections with RPC capabilities.
@@ -56,13 +64,10 @@ var RpcServer = class extends MultiPeerBase {
56
64
  remoteSchema: options.remoteSchema,
57
65
  provider: options.provider,
58
66
  ...options.timeout !== void 0 && { timeout: options.timeout },
67
+ ...options.protocol !== void 0 && { protocol: options.protocol },
59
68
  ...options.hooks !== void 0 && { hooks: options.hooks }
60
69
  });
61
- if ("on" in options.wss && typeof options.wss.on === "function") this.wss = options.wss;
62
- else {
63
- if (!options.WebSocketServer) throw new Error("WebSocketServer constructor required when passing options");
64
- this.wss = new options.WebSocketServer(options.wss);
65
- }
70
+ this.wss = createWebSocketServer(options.wss, options.WebSocketServer);
66
71
  this.wss.on("connection", (ws) => this.handleConnection(ws));
67
72
  this.wss.on("error", (error) => this.hooks.onError?.(null, error));
68
73
  this.wss.on("close", () => this.hooks.onClose?.());
@@ -73,6 +78,7 @@ var RpcServer = class extends MultiPeerBase {
73
78
  localSchema: this.localSchema,
74
79
  remoteSchema: this.remoteSchema,
75
80
  provider: this.provider,
81
+ ...this.protocol !== void 0 && { protocol: this.protocol },
76
82
  timeout: this.timeout,
77
83
  onEvent: (event, data) => {
78
84
  this.hooks.onEvent?.(peer, event, data);
@@ -3,8 +3,8 @@ import "../factory-BGezZgAP.js";
3
3
  import "../json-BZzd4eNj.js";
4
4
  import "../index-Be7jjS77.js";
5
5
  import "../protocol-ipDMUaRc.js";
6
- import { a as IRpcOptions, n as IMethodController, t as IEventController } from "../types-BVaXox7F.js";
7
- import { t as RpcPeer } from "../default-C5z3UDd1.js";
6
+ import { a as IRpcOptions, n as IMethodController, t as IEventController } from "../types-BGM8eZe5.js";
7
+ import { t as RpcPeer } from "../default-DOftsqyK.js";
8
8
  import { n as calculateReconnectDelay, r as defaultReconnectOptions, t as ReconnectOptions } from "../reconnect-DbcN0R_1.js";
9
9
 
10
10
  //#region src/adapters/types.d.ts
@@ -143,7 +143,7 @@ var RpcPeer = class {
143
143
  this.sendError(id, RpcErrorCodes.METHOD_NOT_FOUND, `Method '${method}' not found`);
144
144
  return;
145
145
  }
146
- const parseResult = methodDef.input.safeParse(params);
146
+ const parseResult = await methodDef.input.safeParseAsync(params);
147
147
  if (!parseResult.success) {
148
148
  this.sendError(id, RpcErrorCodes.INVALID_PARAMS, `Invalid params for '${method}'`, parseResult.error);
149
149
  return;
@@ -155,7 +155,7 @@ var RpcPeer = class {
155
155
  }
156
156
  try {
157
157
  const result = await handler.call(this.provider, parseResult.data);
158
- const outputResult = methodDef.output.safeParse(result);
158
+ const outputResult = await methodDef.output.safeParseAsync(result);
159
159
  if (!outputResult.success) {
160
160
  this.sendError(id, RpcErrorCodes.INTERNAL_ERROR, `Invalid output from '${method}'`, outputResult.error);
161
161
  return;
@@ -1,6 +1,6 @@
1
1
  import { a as InferEventData, h as StringKeys, i as EventHandler, m as RpcSchema, n as EventDef, p as Provider, t as Driver } from "./schema-BPJJ9slm.js";
2
2
  import { g as WireInput, u as RpcProtocol } from "./protocol-ipDMUaRc.js";
3
- import { a as IRpcOptions, r as IMinWebSocket } from "./types-BVaXox7F.js";
3
+ import { a as IRpcOptions, r as IMinWebSocket } from "./types-BGM8eZe5.js";
4
4
 
5
5
  //#region src/peers/default.d.ts
6
6
 
@@ -14,23 +14,6 @@ interface RpcPeerOptions<TLocalSchema extends RpcSchema, TRemoteSchema extends R
14
14
  ws: IMinWebSocket;
15
15
  /** Implementation of local methods */
16
16
  provider: Partial<Provider<TLocalSchema>>;
17
- /**
18
- * Protocol for encoding/decoding messages
19
- *
20
- * Defaults to JSON. Use createProtocol() with a binary codec for better performance.
21
- *
22
- * @example
23
- * ```ts
24
- * import { createProtocol, RpcMessageSchema } from "@igoforth/ws-rpc/protocol";
25
- * import { createMsgpackCodec } from "@igoforth/ws-rpc/codecs/msgpack";
26
- *
27
- * const peer = new RpcPeer({
28
- * protocol: createProtocol(createMsgpackCodec(RpcMessageSchema)),
29
- * // ...
30
- * });
31
- * ```
32
- */
33
- protocol?: RpcProtocol | undefined;
34
17
  /** Handler for incoming events */
35
18
  onEvent?: EventHandler<TRemoteSchema> | undefined;
36
19
  /** Generate unique request IDs */
@@ -1,4 +1,4 @@
1
- import { t as RpcPeer } from "./default-BkrMd28n.js";
1
+ import { t as RpcPeer } from "./default-B6CVJfTD.js";
2
2
 
3
3
  //#region src/peers/durable.ts
4
4
  /**
package/dist/index.d.ts CHANGED
@@ -3,7 +3,7 @@ import "./factory-BGezZgAP.js";
3
3
  import "./json-BZzd4eNj.js";
4
4
  import "./index-Be7jjS77.js";
5
5
  import { _ as createProtocol, a as RpcEvent, c as RpcMessageCodec, d as RpcRequest, f as RpcRequestSchema, h as RpcWireCodec, i as RpcErrorSchema, l as RpcMessageSchema, m as RpcResponseSchema, n as RpcError$1, o as RpcEventSchema, p as RpcResponse, r as RpcErrorCodes, s as RpcMessage, t as JsonProtocol, u as RpcProtocol } from "./protocol-ipDMUaRc.js";
6
- import { a as IRpcOptions, c as WebSocketOptions, i as IRpcConnection, l as WebSocketReadyState, n as IMethodController, o as IWebSocket, r as IMinWebSocket, s as IWebSocketServer, t as IEventController, u as WebSocketServerOptions } from "./types-BVaXox7F.js";
6
+ import { a as IRpcOptions, c as WebSocketOptions, i as IRpcConnection, l as WebSocketReadyState, n as IMethodController, o as IWebSocket, r as IMinWebSocket, s as IWebSocketServer, t as IEventController, u as WebSocketServerOptions } from "./types-BGM8eZe5.js";
7
7
  import { n as calculateReconnectDelay, r as defaultReconnectOptions, t as ReconnectOptions } from "./reconnect-DbcN0R_1.js";
8
8
  import { RpcConnectionClosed, RpcError, RpcMethodNotFoundError, RpcRemoteError, RpcTimeoutError, RpcValidationError } from "./errors.js";
9
9
  import { a as PendingCallStorage, i as PendingCall, o as StorageMode, r as MaybePromise, s as SyncPendingCallStorage, t as AsyncPendingCallStorage } from "./interface-CuTDe_Pq.js";
@@ -1,4 +1,4 @@
1
- import { t as RpcPeer } from "./default-BkrMd28n.js";
1
+ import { t as RpcPeer } from "./default-B6CVJfTD.js";
2
2
 
3
3
  //#region src/adapters/multi-peer.ts
4
4
  /**
@@ -30,6 +30,8 @@ var MultiPeerBase = class {
30
30
  provider;
31
31
  /** Default timeout for RPC calls */
32
32
  timeout;
33
+ /** Protocol for encoding/decoding messages */
34
+ protocol;
33
35
  /** Lifecycle hooks */
34
36
  hooks;
35
37
  constructor(options) {
@@ -37,6 +39,7 @@ var MultiPeerBase = class {
37
39
  this.remoteSchema = options.remoteSchema;
38
40
  this.provider = options.provider;
39
41
  this.timeout = options.timeout ?? 3e4;
42
+ if (options.protocol) this.protocol = options.protocol;
40
43
  this.hooks = options.hooks ?? {};
41
44
  }
42
45
  /**
@@ -49,6 +52,7 @@ var MultiPeerBase = class {
49
52
  localSchema: this.localSchema,
50
53
  remoteSchema: this.remoteSchema,
51
54
  provider: this.provider,
55
+ ...this.protocol !== void 0 && { protocol: this.protocol },
52
56
  onEvent: this.hooks.onEvent ? (event, data) => {
53
57
  const peer = this.findPeerByWs(ws);
54
58
  if (peer) this.hooks.onEvent(peer, event, data);
@@ -3,6 +3,6 @@ import "../factory-BGezZgAP.js";
3
3
  import "../json-BZzd4eNj.js";
4
4
  import "../index-Be7jjS77.js";
5
5
  import "../protocol-ipDMUaRc.js";
6
- import "../types-BVaXox7F.js";
7
- import { n as RpcPeerOptions, t as RpcPeer } from "../default-C5z3UDd1.js";
6
+ import "../types-BGM8eZe5.js";
7
+ import { n as RpcPeerOptions, t as RpcPeer } from "../default-DOftsqyK.js";
8
8
  export { RpcPeer, RpcPeerOptions };
@@ -3,6 +3,6 @@ import "../json-Bshec-bZ.js";
3
3
  import "../codecs-BmYG2d_U.js";
4
4
  import "../protocol-_mpoOPp6.js";
5
5
  import "../errors-5BfreE63.js";
6
- import { t as RpcPeer } from "../default-BkrMd28n.js";
6
+ import { t as RpcPeer } from "../default-B6CVJfTD.js";
7
7
 
8
8
  export { RpcPeer };
@@ -3,8 +3,8 @@ import "../factory-BGezZgAP.js";
3
3
  import "../json-BZzd4eNj.js";
4
4
  import "../index-Be7jjS77.js";
5
5
  import { g as WireInput } from "../protocol-ipDMUaRc.js";
6
- import "../types-BVaXox7F.js";
7
- import { n as RpcPeerOptions, t as RpcPeer } from "../default-C5z3UDd1.js";
6
+ import "../types-BGM8eZe5.js";
7
+ import { n as RpcPeerOptions, t as RpcPeer } from "../default-DOftsqyK.js";
8
8
  import { i as PendingCall, s as SyncPendingCallStorage } from "../interface-CuTDe_Pq.js";
9
9
 
10
10
  //#region src/peers/durable.d.ts
@@ -3,7 +3,7 @@ import "../json-Bshec-bZ.js";
3
3
  import "../codecs-BmYG2d_U.js";
4
4
  import "../protocol-_mpoOPp6.js";
5
5
  import "../errors-5BfreE63.js";
6
- import "../default-BkrMd28n.js";
7
- import { n as createDurableRpcPeerFactory, t as DurableRpcPeer } from "../durable-MZjkvyS6.js";
6
+ import "../default-B6CVJfTD.js";
7
+ import { n as createDurableRpcPeerFactory, t as DurableRpcPeer } from "../durable-DENcbnTp.js";
8
8
 
9
9
  export { DurableRpcPeer, createDurableRpcPeerFactory };
@@ -3,8 +3,8 @@ import "../factory-BGezZgAP.js";
3
3
  import "../json-BZzd4eNj.js";
4
4
  import "../index-Be7jjS77.js";
5
5
  import "../protocol-ipDMUaRc.js";
6
- import "../types-BVaXox7F.js";
7
- import { n as RpcPeerOptions, t as RpcPeer } from "../default-C5z3UDd1.js";
6
+ import "../types-BGM8eZe5.js";
7
+ import { n as RpcPeerOptions, t as RpcPeer } from "../default-DOftsqyK.js";
8
8
  import "../interface-CuTDe_Pq.js";
9
9
  import { CallContext, DurableRpcPeer, DurableRpcPeerOptions } from "./durable.js";
10
10
  export { type CallContext, DurableRpcPeer, type DurableRpcPeerOptions, RpcPeer, type RpcPeerOptions };
@@ -3,7 +3,7 @@ import "../json-Bshec-bZ.js";
3
3
  import "../codecs-BmYG2d_U.js";
4
4
  import "../protocol-_mpoOPp6.js";
5
5
  import "../errors-5BfreE63.js";
6
- import { t as RpcPeer } from "../default-BkrMd28n.js";
7
- import { t as DurableRpcPeer } from "../durable-MZjkvyS6.js";
6
+ import { t as RpcPeer } from "../default-B6CVJfTD.js";
7
+ import { t as DurableRpcPeer } from "../durable-DENcbnTp.js";
8
8
 
9
9
  export { DurableRpcPeer, RpcPeer };
@@ -1,6 +1,7 @@
1
1
  import { a as InferEventData, h as StringKeys, m as RpcSchema, n as EventDef, p as Provider } from "./schema-BPJJ9slm.js";
2
- import { a as IRpcOptions, o as IWebSocket, r as IMinWebSocket, s as IWebSocketServer, u as WebSocketServerOptions } from "./types-BVaXox7F.js";
3
- import { t as RpcPeer } from "./default-C5z3UDd1.js";
2
+ import { u as RpcProtocol } from "./protocol-ipDMUaRc.js";
3
+ import { a as IRpcOptions, o as IWebSocket, r as IMinWebSocket, s as IWebSocketServer, u as WebSocketServerOptions } from "./types-BGM8eZe5.js";
4
+ import { t as RpcPeer } from "./default-DOftsqyK.js";
4
5
  import { IMultiAdapterHooks, IMultiConnectionAdapter, MultiDriver } from "./adapters/types.js";
5
6
 
6
7
  //#region src/adapters/multi-peer.d.ts
@@ -17,6 +18,8 @@ interface MultiPeerOptions<TLocalSchema extends RpcSchema, TRemoteSchema extends
17
18
  provider: Provider<TLocalSchema>;
18
19
  /** Default timeout for RPC calls in ms */
19
20
  timeout?: number;
21
+ /** Protocol for encoding/decoding messages */
22
+ protocol?: RpcProtocol;
20
23
  /** Lifecycle hooks */
21
24
  hooks?: IMultiAdapterHooks<TLocalSchema, TRemoteSchema>;
22
25
  }
@@ -43,6 +46,8 @@ declare abstract class MultiPeerBase<TLocalSchema extends RpcSchema, TRemoteSche
43
46
  readonly provider: Provider<TLocalSchema>;
44
47
  /** Default timeout for RPC calls */
45
48
  readonly timeout: number;
49
+ /** Protocol for encoding/decoding messages */
50
+ readonly protocol?: RpcProtocol;
46
51
  /** Lifecycle hooks */
47
52
  readonly hooks: IMultiAdapterHooks<TLocalSchema, TRemoteSchema>;
48
53
  constructor(options: MultiPeerOptions<TLocalSchema, TRemoteSchema>);
@@ -146,7 +151,7 @@ interface RpcServerOptions<TLocalSchema extends RpcSchema, TRemoteSchema extends
146
151
  provider: Provider<TLocalSchema>;
147
152
  /** WebSocket server instance or options to create one */
148
153
  wss: IWebSocketServer | WebSocketServerOptions;
149
- /** WebSocket server constructor (defaults to require('ws').WebSocketServer) */
154
+ /** WebSocket server constructor (required when passing options instead of server instance) */
150
155
  WebSocketServer?: new (options: WebSocketServerOptions) => IWebSocketServer;
151
156
  /** Lifecycle hooks */
152
157
  hooks?: IMultiAdapterHooks<TLocalSchema, TRemoteSchema>;
@@ -1,5 +1,5 @@
1
1
  import { i as EventHandler, m as RpcSchema, p as Provider, r as EventEmitter } from "./schema-BPJJ9slm.js";
2
- import { g as WireInput } from "./protocol-ipDMUaRc.js";
2
+ import { g as WireInput, u as RpcProtocol } from "./protocol-ipDMUaRc.js";
3
3
 
4
4
  //#region src/types.d.ts
5
5
 
@@ -72,6 +72,11 @@ interface IRpcOptions<TLocalSchema extends RpcSchema, TRemoteSchema extends RpcS
72
72
  readonly remoteSchema: TRemoteSchema;
73
73
  /** Default timeout for RPC calls in ms */
74
74
  readonly timeout?: number;
75
+ /**
76
+ * Protocol for encoding/decoding messages.
77
+ * Defaults to JSON. Use createProtocol() with a binary codec for better performance.
78
+ */
79
+ readonly protocol?: RpcProtocol;
75
80
  }
76
81
  /**
77
82
  * Interface for types that provide RPC method implementations
package/dist/types.d.ts CHANGED
@@ -3,5 +3,5 @@ import "./factory-BGezZgAP.js";
3
3
  import "./json-BZzd4eNj.js";
4
4
  import "./index-Be7jjS77.js";
5
5
  import "./protocol-ipDMUaRc.js";
6
- import { a as IRpcOptions, c as WebSocketOptions, i as IRpcConnection, l as WebSocketReadyState, n as IMethodController, o as IWebSocket, r as IMinWebSocket, s as IWebSocketServer, t as IEventController, u as WebSocketServerOptions } from "./types-BVaXox7F.js";
6
+ import { a as IRpcOptions, c as WebSocketOptions, i as IRpcConnection, l as WebSocketReadyState, n as IMethodController, o as IWebSocket, r as IMinWebSocket, s as IWebSocketServer, t as IEventController, u as WebSocketServerOptions } from "./types-BGM8eZe5.js";
7
7
  export { IEventController, IMethodController, IMinWebSocket, IRpcConnection, IRpcOptions, IWebSocket, IWebSocketServer, WebSocketOptions, WebSocketReadyState, WebSocketServerOptions };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@igoforth/ws-rpc",
3
- "version": "1.0.2",
3
+ "version": "1.1.0",
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",
@@ -135,6 +135,7 @@
135
135
  "@msgpack/msgpack": "^3.1.2",
136
136
  "@types/node": "^22.10.2",
137
137
  "@types/ws": "^8.18.1",
138
+ "@vitest/coverage-istanbul": "^3.2.4",
138
139
  "cbor-x": "^1.6.0",
139
140
  "tsdown": "^0.18.1",
140
141
  "typescript": "^5.9.3",
@@ -148,6 +149,7 @@
148
149
  "test:watch": "vitest",
149
150
  "bench": "vitest bench --config vitest.config.bench.ts",
150
151
  "typecheck": "tsc --noEmit",
152
+ "typecheck:test": "tsc --noEmit -p tsconfig.test.json",
151
153
  "typegen": "wrangler types",
152
154
  "lint": "biome check .",
153
155
  "format": "biome format --write ."