@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,346 @@
1
+ /**
2
+ * Cloudflare Durable Object RPC Adapter
3
+ *
4
+ * Mixin for @cloudflare/actors that adds RPC capabilities to Durable Objects.
5
+ * Manages RPC peers for each WebSocket connection with hibernation support.
6
+ *
7
+ * ## Usage
8
+ *
9
+ * The mixin requires that your class implements the methods defined in the
10
+ * local schema. TypeScript will enforce this at compile time.
11
+ *
12
+ * ```ts
13
+ * class MyDO extends withRpc(Actor, {
14
+ * localSchema: SignalSchema,
15
+ * remoteSchema: FilterSchema,
16
+ * }) {
17
+ * // Required: implement methods from SignalSchema
18
+ * async getWallets() {
19
+ * return { wallets: this.wallets };
20
+ * }
21
+ *
22
+ * // Call methods on connected clients via driver
23
+ * async notifyClients() {
24
+ * // Call all connected peers
25
+ * const results = await this.driver.someClientMethod({});
26
+ *
27
+ * // Or call specific peer with timeout
28
+ * const results = await this.driver.someClientMethod({}, {
29
+ * ids: "peer-id",
30
+ * timeout: 5000,
31
+ * });
32
+ * }
33
+ * }
34
+ * ```
35
+ *
36
+ * ## Hibernation Handling
37
+ *
38
+ * When a DO hibernates, all in-memory state is lost but WebSocket connections
39
+ * remain open. This adapter handles hibernation by lazily recreating RpcPeer
40
+ * instances when messages arrive on connections that were established before
41
+ * hibernation.
42
+ *
43
+ * For hibernation-safe outgoing calls, use DurableRpcPeer which persists
44
+ * pending calls to durable storage.
45
+ */
46
+
47
+ import type { Actor } from "@cloudflare/actors";
48
+ import type { Constructor } from "type-fest";
49
+ import { RpcPeer } from "../peers/default.js";
50
+ import {
51
+ createDurableRpcPeerFactory,
52
+ DurableRpcPeer,
53
+ type DurableRpcPeerOptions,
54
+ } from "../peers/durable.js";
55
+ import type {
56
+ EventDef,
57
+ InferEventData,
58
+ Provider,
59
+ RpcSchema,
60
+ StringKeys,
61
+ } from "../schema.js";
62
+ import { SqlPendingCallStorage } from "../storage/sql.js";
63
+ import type { IRpcOptions } from "../types.js";
64
+ import { MultiPeerBase, type MultiPeerOptions } from "./multi-peer.js";
65
+ import type { IMultiAdapterHooks, IMultiConnectionAdapter } from "./types.js";
66
+
67
+ /**
68
+ * Extended hooks for Durable Object adapter
69
+ */
70
+ export interface IDOHooks<
71
+ TLocalSchema extends RpcSchema,
72
+ TRemoteSchema extends RpcSchema,
73
+ > extends IMultiAdapterHooks<TLocalSchema, TRemoteSchema> {
74
+ /** Called when a peer is recreated after hibernation */
75
+ onPeerRecreated?(
76
+ peer: RpcPeer<TLocalSchema, TRemoteSchema>,
77
+ ws: WebSocket,
78
+ ): void;
79
+ }
80
+
81
+ /**
82
+ * Concrete MultiPeerBase for Durable Objects using native WebSocket
83
+ */
84
+ class DOMultiPeer<
85
+ TLocalSchema extends RpcSchema,
86
+ TRemoteSchema extends RpcSchema,
87
+ TActor,
88
+ > extends MultiPeerBase<TLocalSchema, TRemoteSchema, WebSocket> {
89
+ public override readonly hooks: IDOHooks<TLocalSchema, TRemoteSchema>;
90
+ private readonly _createPeer;
91
+
92
+ constructor(
93
+ options: MultiPeerOptions<TLocalSchema, TRemoteSchema> &
94
+ DurableRpcPeerOptions<TActor> & {
95
+ hooks?: IDOHooks<TLocalSchema, TRemoteSchema>;
96
+ },
97
+ ) {
98
+ super(options);
99
+ this.hooks = options.hooks ?? {};
100
+ this._createPeer = createDurableRpcPeerFactory({
101
+ actor: options.actor,
102
+ storage: options.storage,
103
+ ...(options.durableTimeout != null && {
104
+ durableTimeout: options.durableTimeout,
105
+ }),
106
+ });
107
+ }
108
+
109
+ /**
110
+ * Create an RPC peer for a WebSocket connection.
111
+ * Override to use the actor instance as provider via closure.
112
+ */
113
+ public createPeerWithProvider(
114
+ ws: WebSocket,
115
+ provider: Provider<TLocalSchema>,
116
+ ): DurableRpcPeer<TLocalSchema, TRemoteSchema, TActor> {
117
+ const peer = this._createPeer({
118
+ ws,
119
+ localSchema: this.localSchema,
120
+ remoteSchema: this.remoteSchema,
121
+ provider,
122
+ onEvent: (event, data) => {
123
+ this.hooks.onEvent?.(peer, event, data);
124
+ },
125
+ timeout: this.timeout,
126
+ });
127
+ return peer;
128
+ }
129
+
130
+ /**
131
+ * Get or create RPC peer for a WebSocket
132
+ * Handles lazy recreation after hibernation.
133
+ */
134
+ public getOrCreatePeer(
135
+ ws: WebSocket,
136
+ provider: Provider<TLocalSchema>,
137
+ isHibernationRecovery = false,
138
+ ): RpcPeer<TLocalSchema, TRemoteSchema> {
139
+ let peer = this.getPeerFor(ws);
140
+
141
+ if (!peer) {
142
+ peer = this.createPeerWithProvider(ws, provider);
143
+ this.addPeer(ws, peer);
144
+
145
+ if (isHibernationRecovery) {
146
+ this.hooks.onPeerRecreated?.(peer, ws);
147
+ }
148
+ }
149
+
150
+ return peer;
151
+ }
152
+
153
+ /**
154
+ * Connect a new WebSocket and create its peer
155
+ */
156
+ public connectPeer(
157
+ ws: WebSocket,
158
+ provider: Provider<TLocalSchema>,
159
+ ): RpcPeer<TLocalSchema, TRemoteSchema> {
160
+ const peer = this.createPeerWithProvider(ws, provider);
161
+ this.addPeer(ws, peer);
162
+ return peer;
163
+ }
164
+
165
+ /**
166
+ * Disconnect a WebSocket and remove its peer
167
+ */
168
+ public disconnectPeer(ws: WebSocket): void {
169
+ this.removePeer(ws);
170
+ }
171
+
172
+ /**
173
+ * Handle an error on a WebSocket
174
+ */
175
+ public handleError(ws: WebSocket, error: Error): void {
176
+ const peer = this.getPeerFor(ws);
177
+ if (peer) {
178
+ peer.close();
179
+ this.removePeer(ws);
180
+ }
181
+ this.hooks.onError?.(peer ?? null, error);
182
+ }
183
+ }
184
+
185
+ /**
186
+ * Constructor type for the RPC mixin result.
187
+ *
188
+ * Subclasses must implement methods from TLocalSchema on `this`.
189
+ * Runtime enforces this when methods are called via RPC.
190
+ */
191
+ export type RpcActorConstructor<
192
+ TBase extends Constructor<Actor<unknown>>,
193
+ TLocalSchema extends RpcSchema,
194
+ TRemoteSchema extends RpcSchema,
195
+ > = {
196
+ new (
197
+ ...args: ConstructorParameters<TBase>
198
+ ): InstanceType<TBase> & IMultiConnectionAdapter<TLocalSchema, TRemoteSchema>;
199
+ } & Omit<TBase, "new">;
200
+
201
+ /**
202
+ * Create a mixin that adds RPC capabilities to a Durable Object Actor.
203
+ *
204
+ * The resulting class requires implementation of all methods defined in
205
+ * `localSchema`. TypeScript enforces this at compile time.
206
+ *
207
+ * @param Base - The Actor class to extend
208
+ * @param options - RPC configuration including local/remote schemas and timeout
209
+ * @returns A new class with RPC capabilities mixed in
210
+ *
211
+ * @example
212
+ * ```ts
213
+ * const ServerSchema = {
214
+ * methods: {
215
+ * getData: method({
216
+ * input: z.object({}),
217
+ * output: z.object({ data: z.array(z.string()) }),
218
+ * }),
219
+ * },
220
+ * events: {},
221
+ * } as const;
222
+ *
223
+ * class MyDO extends withRpc(Actor, {
224
+ * localSchema: ServerSchema,
225
+ * remoteSchema: ClientSchema,
226
+ * }) {
227
+ * // Required: implement methods from ServerSchema
228
+ * async getData() {
229
+ * return { data: this.dataList };
230
+ * }
231
+ *
232
+ * // Call methods on connected clients
233
+ * async notifyClients() {
234
+ * const results = await this.driver.clientMethod({ info: "update" });
235
+ * }
236
+ * }
237
+ * ```
238
+ */
239
+ export function withRpc<
240
+ TLocalSchema extends RpcSchema,
241
+ TRemoteSchema extends RpcSchema,
242
+ TEnv,
243
+ TBase extends Constructor<Actor<TEnv>> & {
244
+ prototype: Provider<TLocalSchema>;
245
+ },
246
+ >(
247
+ Base: TBase,
248
+ options: IRpcOptions<TLocalSchema, TRemoteSchema>,
249
+ ): RpcActorConstructor<TBase, TLocalSchema, TRemoteSchema> {
250
+ // @ts-expect-error - TypeScript can't verify the anonymous class satisfies RpcActorConstructor
251
+ return class RpcActor extends Base {
252
+ private __rpc: DOMultiPeer<TLocalSchema, TRemoteSchema, this> | null = null;
253
+
254
+ /**
255
+ * Internal RPC manager (lazily initialized)
256
+ *
257
+ * Handles peer management, message routing, and durable storage.
258
+ * Uses SQL storage from the Durable Object for hibernation-safe calls.
259
+ *
260
+ * @throws Error if DurableObjectStorage is not available
261
+ */
262
+ get _rpc() {
263
+ if (!this.storage.raw)
264
+ throw new Error("DurableObjectStorage not present in actor `raw`");
265
+ return (this.__rpc ??= new DOMultiPeer({
266
+ actor: this,
267
+ storage: new SqlPendingCallStorage(this.storage.raw.sql),
268
+ localSchema: options.localSchema,
269
+ remoteSchema: options.remoteSchema,
270
+ provider: this as Provider<TLocalSchema>,
271
+ ...(options.timeout !== undefined && { timeout: options.timeout }),
272
+ }));
273
+ }
274
+
275
+ /**
276
+ * Driver for calling methods on connected clients
277
+ */
278
+ get driver() {
279
+ return this._rpc.driver;
280
+ }
281
+
282
+ /**
283
+ * Emit an event to connected clients
284
+ *
285
+ * @param event - Event name from local schema
286
+ * @param data - Event data matching the schema
287
+ * @param ids - Optional array of peer IDs to emit to (broadcasts to all if omitted)
288
+ */
289
+ public emit<K extends StringKeys<TLocalSchema["events"]>>(
290
+ event: K,
291
+ data: TLocalSchema["events"] extends Record<string, EventDef>
292
+ ? InferEventData<TLocalSchema["events"][K]>
293
+ : never,
294
+ ids?: string[],
295
+ ): void {
296
+ this._rpc.emit(event, data, ids);
297
+ }
298
+
299
+ /**
300
+ * Get the number of connected peers
301
+ */
302
+ public getConnectionCount() {
303
+ return this._rpc.getConnectionCount();
304
+ }
305
+
306
+ /**
307
+ * Get the IDs of all connected peers
308
+ */
309
+ public getConnectionIds() {
310
+ return this._rpc.getConnectionIds();
311
+ }
312
+
313
+ // =========================================================================
314
+ // Actor WebSocket Hooks (DO-specific, required by Actor class)
315
+ // =========================================================================
316
+
317
+ /** Called by Actor when WebSocket connects */
318
+ protected onWebSocketConnect(ws: WebSocket, _request: Request): void {
319
+ this._rpc.connectPeer(ws, this as Provider<TLocalSchema>);
320
+ }
321
+
322
+ /** Called by Actor when WebSocket message received (handles hibernation recovery) */
323
+ protected onWebSocketMessage(
324
+ ws: WebSocket,
325
+ message: ArrayBuffer | string,
326
+ ): void {
327
+ const existingPeer = this._rpc.getPeerFor(ws);
328
+ const peer = this._rpc.getOrCreatePeer(
329
+ ws,
330
+ this as Provider<TLocalSchema>,
331
+ !existingPeer,
332
+ );
333
+ peer.handleMessage(message);
334
+ }
335
+
336
+ /** Called by Actor when WebSocket disconnects */
337
+ protected onWebSocketDisconnect(ws: WebSocket): void {
338
+ this._rpc.disconnectPeer(ws);
339
+ }
340
+
341
+ /** Called by Actor when WebSocket error occurs */
342
+ protected onWebSocketError(ws: WebSocket, error: Error): void {
343
+ this._rpc.handleError(ws, error);
344
+ }
345
+ };
346
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Adapter Exports
3
+ */
4
+
5
+ export {
6
+ type ConnectionState,
7
+ RpcClient,
8
+ type RpcClientOptions,
9
+ } from "./client.js";
10
+ export {
11
+ type RpcActorConstructor,
12
+ withRpc,
13
+ } from "./cloudflare-do.js";
14
+ export { MultiPeerBase } from "./multi-peer.js";
15
+ export { RpcServer, type RpcServerOptions } from "./server.js";
16
+ export * from "./types.js";