@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.
- package/LICENSE +21 -0
- package/README.md +446 -0
- package/dist/adapters/client.d.ts +117 -0
- package/dist/adapters/client.js +241 -0
- package/dist/adapters/cloudflare-do.d.ts +72 -0
- package/dist/adapters/cloudflare-do.js +192 -0
- package/dist/adapters/index.d.ts +13 -0
- package/dist/adapters/index.js +16 -0
- package/dist/adapters/server.d.ts +10 -0
- package/dist/adapters/server.js +122 -0
- package/dist/adapters/types.d.ts +125 -0
- package/dist/adapters/types.js +3 -0
- package/dist/codecs/cbor.d.ts +16 -0
- package/dist/codecs/cbor.js +36 -0
- package/dist/codecs/factory.d.ts +3 -0
- package/dist/codecs/factory.js +3 -0
- package/dist/codecs/index.d.ts +5 -0
- package/dist/codecs/index.js +5 -0
- package/dist/codecs/json.d.ts +4 -0
- package/dist/codecs/json.js +4 -0
- package/dist/codecs/msgpack.d.ts +16 -0
- package/dist/codecs/msgpack.js +34 -0
- package/dist/codecs-BmYG2d_U.js +0 -0
- package/dist/default-BkrMd28n.js +253 -0
- package/dist/default-xDNNMrg0.d.ts +129 -0
- package/dist/durable-MZjkvyS6.js +165 -0
- package/dist/errors-5BfreE63.js +96 -0
- package/dist/errors.d.ts +69 -0
- package/dist/errors.js +7 -0
- package/dist/factory-3ziwTuZe.js +132 -0
- package/dist/factory-C1v0AEHY.d.ts +101 -0
- package/dist/index-Be7jjS77.d.ts +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +14 -0
- package/dist/interface-C4S-WCqW.d.ts +120 -0
- package/dist/json-54Z2bIIs.d.ts +22 -0
- package/dist/json-Bshec-bZ.js +41 -0
- package/dist/memory-Bqb3KEVr.js +48 -0
- package/dist/memory-D1nGjzzH.d.ts +41 -0
- package/dist/multi-peer-BAi9yVzp.js +242 -0
- package/dist/peers/default.d.ts +8 -0
- package/dist/peers/default.js +8 -0
- package/dist/peers/durable.d.ts +136 -0
- package/dist/peers/durable.js +9 -0
- package/dist/peers/index.d.ts +10 -0
- package/dist/peers/index.js +9 -0
- package/dist/protocol-DA84zrc2.d.ts +211 -0
- package/dist/protocol-_mpoOPp6.js +192 -0
- package/dist/protocol.d.ts +6 -0
- package/dist/protocol.js +6 -0
- package/dist/reconnect-CGAA_1Gf.js +26 -0
- package/dist/reconnect-DbcN0R_1.d.ts +35 -0
- package/dist/schema-CN5HHHku.d.ts +108 -0
- package/dist/schema.d.ts +2 -0
- package/dist/schema.js +43 -0
- package/dist/server-zTjpJpoX.d.ts +209 -0
- package/dist/sql-CCjc6Bid.js +142 -0
- package/dist/sql-DPmHOeZy.d.ts +131 -0
- package/dist/storage/index.d.ts +8 -0
- package/dist/storage/index.js +7 -0
- package/dist/storage/interface.d.ts +3 -0
- package/dist/storage/interface.js +0 -0
- package/dist/storage/memory.d.ts +7 -0
- package/dist/storage/memory.js +6 -0
- package/dist/storage/sql.d.ts +7 -0
- package/dist/storage/sql.js +6 -0
- package/dist/types-Be-qmQu0.d.ts +111 -0
- package/dist/types-D_psiH09.js +13 -0
- package/dist/types.d.ts +7 -0
- package/dist/types.js +3 -0
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/index.js +3 -0
- package/dist/utils/reconnect.d.ts +2 -0
- package/dist/utils/reconnect.js +3 -0
- package/package.json +156 -0
- package/src/adapters/client.ts +396 -0
- package/src/adapters/cloudflare-do.ts +346 -0
- package/src/adapters/index.ts +16 -0
- package/src/adapters/multi-peer.ts +404 -0
- package/src/adapters/server.ts +192 -0
- package/src/adapters/types.ts +202 -0
- package/src/codecs/cbor.ts +42 -0
- package/src/codecs/factory.ts +210 -0
- package/src/codecs/index.ts +30 -0
- package/src/codecs/json.ts +42 -0
- package/src/codecs/msgpack.ts +36 -0
- package/src/errors.ts +105 -0
- package/src/index.ts +102 -0
- package/src/peers/default.ts +433 -0
- package/src/peers/durable.ts +280 -0
- package/src/peers/index.ts +13 -0
- package/src/protocol.ts +306 -0
- package/src/schema.ts +167 -0
- package/src/storage/index.ts +20 -0
- package/src/storage/interface.ts +146 -0
- package/src/storage/memory.ts +84 -0
- package/src/storage/sql.ts +266 -0
- package/src/types.ts +158 -0
- package/src/utils/index.ts +9 -0
- package/src/utils/reconnect.ts +51 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Ian Goforth
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
# @igoforth/ws-rpc
|
|
2
|
+
|
|
3
|
+
[](https://github.com/igoforth/ws-rpc/actions/workflows/ci.yml)
|
|
4
|
+
[](https://www.typescriptlang.org/)
|
|
5
|
+
[](https://nodejs.org/)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
|
|
8
|
+
Bidirectional RPC over WebSocket with Zod schema validation and full TypeScript inference.
|
|
9
|
+
|
|
10
|
+
## Features
|
|
11
|
+
|
|
12
|
+
- **Bidirectional RPC** - Both client and server can call methods on each other
|
|
13
|
+
- **Schema-first** - Define your API with Zod schemas, get full TypeScript inference
|
|
14
|
+
- **Multiple codecs** - JSON (built-in), MessagePack, and CBOR support
|
|
15
|
+
- **Cloudflare Durable Objects** - First-class support with hibernation-safe persistence
|
|
16
|
+
- **Auto-reconnect** - Client automatically reconnects with exponential backoff
|
|
17
|
+
- **Fire-and-forget events** - Decoupled from request/response pattern
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install @igoforth/ws-rpc zod
|
|
23
|
+
# or
|
|
24
|
+
pnpm add @igoforth/ws-rpc zod
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Optional codecs
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
# MessagePack (faster, smaller)
|
|
31
|
+
pnpm add @msgpack/msgpack
|
|
32
|
+
|
|
33
|
+
# CBOR (binary, compact)
|
|
34
|
+
pnpm add cbor-x
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Cloudflare Durable Objects
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pnpm add @cloudflare/actors
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Quick Start
|
|
44
|
+
|
|
45
|
+
### 1. Define your schema
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
import { z } from "zod";
|
|
49
|
+
import { method, event, type RpcSchema } from "@igoforth/ws-rpc/schema";
|
|
50
|
+
|
|
51
|
+
// Server schema - methods the server implements
|
|
52
|
+
export const ServerSchema = {
|
|
53
|
+
methods: {
|
|
54
|
+
getUser: method({
|
|
55
|
+
input: z.object({ id: z.string() }),
|
|
56
|
+
output: z.object({ name: z.string(), email: z.string() }),
|
|
57
|
+
}),
|
|
58
|
+
createOrder: method({
|
|
59
|
+
input: z.object({ product: z.string(), quantity: z.number() }),
|
|
60
|
+
output: z.object({ orderId: z.string() }),
|
|
61
|
+
}),
|
|
62
|
+
},
|
|
63
|
+
events: {
|
|
64
|
+
orderUpdated: event({
|
|
65
|
+
data: z.object({ orderId: z.string(), status: z.string() }),
|
|
66
|
+
}),
|
|
67
|
+
},
|
|
68
|
+
} satisfies RpcSchema;
|
|
69
|
+
|
|
70
|
+
// Client schema - methods the client implements (for bidirectional RPC)
|
|
71
|
+
export const ClientSchema = {
|
|
72
|
+
methods: {
|
|
73
|
+
ping: method({
|
|
74
|
+
input: z.object({}),
|
|
75
|
+
output: z.object({ pong: z.boolean() }),
|
|
76
|
+
}),
|
|
77
|
+
},
|
|
78
|
+
events: {},
|
|
79
|
+
} satisfies RpcSchema;
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### 2. Create a client
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
import { RpcClient } from "@igoforth/ws-rpc/adapters/client";
|
|
86
|
+
import { ServerSchema, ClientSchema } from "./schemas";
|
|
87
|
+
|
|
88
|
+
const client = new RpcClient({
|
|
89
|
+
url: "wss://your-server.com/ws",
|
|
90
|
+
localSchema: ClientSchema,
|
|
91
|
+
remoteSchema: ServerSchema,
|
|
92
|
+
provider: {
|
|
93
|
+
// Implement methods the server can call on us
|
|
94
|
+
ping: async () => ({ pong: true }),
|
|
95
|
+
},
|
|
96
|
+
reconnect: {
|
|
97
|
+
initialDelay: 1000,
|
|
98
|
+
maxDelay: 30000,
|
|
99
|
+
maxAttempts: 10,
|
|
100
|
+
},
|
|
101
|
+
autoConnect: true, // Connect immediately (or call client.connect() manually)
|
|
102
|
+
onConnect: () => console.log("Connected"),
|
|
103
|
+
onDisconnect: (code, reason) => console.log("Disconnected:", code, reason),
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// Call server methods with full type safety
|
|
107
|
+
const user = await client.driver.getUser({ id: "123" });
|
|
108
|
+
console.log(user.name, user.email);
|
|
109
|
+
|
|
110
|
+
const order = await client.driver.createOrder({ product: "widget", quantity: 5 });
|
|
111
|
+
console.log(order.orderId);
|
|
112
|
+
|
|
113
|
+
// Emit events to the server
|
|
114
|
+
client.emit("someEvent", { data: "value" });
|
|
115
|
+
|
|
116
|
+
// Disconnect when done
|
|
117
|
+
client.disconnect();
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### 3. Create a server (Node.js)
|
|
121
|
+
|
|
122
|
+
```typescript
|
|
123
|
+
import { WebSocketServer } from "ws";
|
|
124
|
+
import { RpcServer } from "@igoforth/ws-rpc/adapters/server";
|
|
125
|
+
import { ServerSchema, ClientSchema } from "./schemas";
|
|
126
|
+
|
|
127
|
+
const server = new RpcServer({
|
|
128
|
+
wss: { port: 8080 },
|
|
129
|
+
WebSocketServer,
|
|
130
|
+
localSchema: ServerSchema,
|
|
131
|
+
remoteSchema: ClientSchema,
|
|
132
|
+
provider: {
|
|
133
|
+
getUser: async ({ id }) => {
|
|
134
|
+
return { name: "John Doe", email: "john@example.com" };
|
|
135
|
+
},
|
|
136
|
+
createOrder: async ({ product, quantity }) => {
|
|
137
|
+
return { orderId: crypto.randomUUID() };
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
hooks: {
|
|
141
|
+
onConnect: (peer) => {
|
|
142
|
+
console.log(`Client ${peer.id} connected`);
|
|
143
|
+
|
|
144
|
+
// Call methods on this specific client
|
|
145
|
+
peer.driver.ping({}).then((result) => {
|
|
146
|
+
console.log("Client responded:", result.pong);
|
|
147
|
+
});
|
|
148
|
+
},
|
|
149
|
+
onDisconnect: (peer) => {
|
|
150
|
+
console.log(`Client ${peer.id} disconnected`);
|
|
151
|
+
},
|
|
152
|
+
onError: (peer, error) => {
|
|
153
|
+
console.error(`Error from ${peer?.id}:`, error);
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// Emit to all connected clients
|
|
159
|
+
server.emit("orderUpdated", { orderId: "123", status: "shipped" });
|
|
160
|
+
|
|
161
|
+
// Emit to specific clients by ID
|
|
162
|
+
server.emit("orderUpdated", { orderId: "456", status: "delivered" }, ["peer-id-1", "peer-id-2"]);
|
|
163
|
+
|
|
164
|
+
// Call methods on all clients
|
|
165
|
+
const results = await server.driver.ping({});
|
|
166
|
+
for (const { id, result } of results) {
|
|
167
|
+
if (result.success) {
|
|
168
|
+
console.log(`Peer ${id}:`, result.value);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Get connection info
|
|
173
|
+
console.log("Connected clients:", server.getConnectionCount());
|
|
174
|
+
console.log("Client IDs:", server.getConnectionIds());
|
|
175
|
+
|
|
176
|
+
// Close a specific client
|
|
177
|
+
server.closePeer("peer-id", 1000, "Goodbye");
|
|
178
|
+
|
|
179
|
+
// Graceful shutdown
|
|
180
|
+
process.on("SIGTERM", () => server.close());
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### 4. Cloudflare Durable Object
|
|
184
|
+
|
|
185
|
+
```typescript
|
|
186
|
+
import { Actor } from "@cloudflare/actors";
|
|
187
|
+
import { withRpc } from "@igoforth/ws-rpc/adapters/cloudflare-do";
|
|
188
|
+
import { ServerSchema, ClientSchema } from "./schemas";
|
|
189
|
+
|
|
190
|
+
// The mixin adds RPC capabilities to your Actor
|
|
191
|
+
// Your class must implement the methods defined in localSchema
|
|
192
|
+
export class GameRoom extends withRpc(Actor, {
|
|
193
|
+
localSchema: ServerSchema,
|
|
194
|
+
remoteSchema: ClientSchema,
|
|
195
|
+
}) {
|
|
196
|
+
private gameState = { players: [] as string[] };
|
|
197
|
+
|
|
198
|
+
// Implement methods from ServerSchema
|
|
199
|
+
async getUser({ id }: { id: string }) {
|
|
200
|
+
return { name: `Player ${id}`, email: `${id}@game.com` };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async createOrder({ product, quantity }: { product: string; quantity: number }) {
|
|
204
|
+
return { orderId: crypto.randomUUID() };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Use this.driver to call methods on connected clients
|
|
208
|
+
async notifyAllPlayers() {
|
|
209
|
+
// Call ping on all connected clients
|
|
210
|
+
const results = await this.driver.ping({});
|
|
211
|
+
console.log("Ping results:", results);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Use this.emit to send events to clients
|
|
215
|
+
broadcastUpdate() {
|
|
216
|
+
this.emit("orderUpdated", { orderId: "123", status: "updated" });
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Check connection status
|
|
220
|
+
getPlayerCount() {
|
|
221
|
+
return this.getConnectionCount();
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
## API Reference
|
|
227
|
+
|
|
228
|
+
### Schema Definition
|
|
229
|
+
|
|
230
|
+
```typescript
|
|
231
|
+
import { method, event } from "@igoforth/ws-rpc/schema";
|
|
232
|
+
import { z } from "zod";
|
|
233
|
+
|
|
234
|
+
// Define a method with input/output validation
|
|
235
|
+
const myMethod = method({
|
|
236
|
+
input: z.object({ /* ... */ }),
|
|
237
|
+
output: z.object({ /* ... */ }),
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
// Define an event (fire-and-forget)
|
|
241
|
+
const myEvent = event({
|
|
242
|
+
data: z.object({ /* ... */ }),
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
// Combine into a schema
|
|
246
|
+
const MySchema = {
|
|
247
|
+
methods: { myMethod },
|
|
248
|
+
events: { myEvent },
|
|
249
|
+
} satisfies RpcSchema;
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
### Type Inference
|
|
253
|
+
|
|
254
|
+
```typescript
|
|
255
|
+
import type { Provider, Driver, InferInput, InferOutput } from "@igoforth/ws-rpc/schema";
|
|
256
|
+
|
|
257
|
+
// Get the provider type (what you implement)
|
|
258
|
+
type MyProvider = Provider<typeof MySchema>;
|
|
259
|
+
|
|
260
|
+
// Get the driver type (what you call)
|
|
261
|
+
type MyDriver = Driver<typeof MySchema>;
|
|
262
|
+
|
|
263
|
+
// Get input/output types for a specific method
|
|
264
|
+
type MyMethodInput = InferInput<typeof MySchema["methods"]["myMethod"]>;
|
|
265
|
+
type MyMethodOutput = InferOutput<typeof MySchema["methods"]["myMethod"]>;
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
### Codecs
|
|
269
|
+
|
|
270
|
+
```typescript
|
|
271
|
+
import { createMsgpackCodec } from "@igoforth/ws-rpc/codecs/msgpack";
|
|
272
|
+
import { createCborCodec } from "@igoforth/ws-rpc/codecs/cbor";
|
|
273
|
+
import { createJsonCodec } from "@igoforth/ws-rpc/codecs/json";
|
|
274
|
+
|
|
275
|
+
// JSON (default)
|
|
276
|
+
const jsonCodec = createJsonCodec(z.unknown());
|
|
277
|
+
|
|
278
|
+
// MessagePack (requires @msgpack/msgpack)
|
|
279
|
+
const msgpackCodec = createMsgpackCodec(z.unknown());
|
|
280
|
+
|
|
281
|
+
// CBOR (requires cbor-x)
|
|
282
|
+
const cborCodec = createCborCodec(z.unknown());
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
### Error Handling
|
|
286
|
+
|
|
287
|
+
```typescript
|
|
288
|
+
import {
|
|
289
|
+
RpcError,
|
|
290
|
+
RpcTimeoutError,
|
|
291
|
+
RpcRemoteError,
|
|
292
|
+
RpcConnectionClosed,
|
|
293
|
+
RpcValidationError,
|
|
294
|
+
RpcMethodNotFoundError,
|
|
295
|
+
} from "@igoforth/ws-rpc/errors";
|
|
296
|
+
|
|
297
|
+
try {
|
|
298
|
+
await client.driver.someMethod({ /* ... */ });
|
|
299
|
+
} catch (error) {
|
|
300
|
+
if (error instanceof RpcTimeoutError) {
|
|
301
|
+
console.log(`Request '${error.method}' timed out after ${error.timeoutMs}ms`);
|
|
302
|
+
} else if (error instanceof RpcValidationError) {
|
|
303
|
+
console.log("Invalid input/output:", error.message);
|
|
304
|
+
} else if (error instanceof RpcRemoteError) {
|
|
305
|
+
console.log("Server error:", error.message, error.code);
|
|
306
|
+
} else if (error instanceof RpcMethodNotFoundError) {
|
|
307
|
+
console.log(`Method '${error.method}' not found`);
|
|
308
|
+
} else if (error instanceof RpcConnectionClosed) {
|
|
309
|
+
console.log("Connection closed");
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
### Client Options
|
|
315
|
+
|
|
316
|
+
```typescript
|
|
317
|
+
const client = new RpcClient({
|
|
318
|
+
url: "wss://...",
|
|
319
|
+
localSchema: MyLocalSchema,
|
|
320
|
+
remoteSchema: MyRemoteSchema,
|
|
321
|
+
provider: { /* method implementations */ },
|
|
322
|
+
|
|
323
|
+
// Reconnection options (set to false to disable)
|
|
324
|
+
reconnect: {
|
|
325
|
+
initialDelay: 1000, // First retry delay (ms)
|
|
326
|
+
maxDelay: 30000, // Maximum retry delay (ms)
|
|
327
|
+
backoffMultiplier: 2, // Exponential backoff multiplier
|
|
328
|
+
maxAttempts: 0, // Max attempts (0 = unlimited)
|
|
329
|
+
jitter: 0.1, // Random jitter factor (0-1)
|
|
330
|
+
},
|
|
331
|
+
|
|
332
|
+
// Request timeout (ms)
|
|
333
|
+
timeout: 30000,
|
|
334
|
+
|
|
335
|
+
// Auto-connect on creation (default: false)
|
|
336
|
+
autoConnect: true,
|
|
337
|
+
|
|
338
|
+
// WebSocket options
|
|
339
|
+
protocols: ["v1"], // Subprotocols
|
|
340
|
+
headers: { Authorization: "Bearer ..." }, // Headers (Node.js/Bun only)
|
|
341
|
+
|
|
342
|
+
// Event handlers
|
|
343
|
+
onConnect: () => console.log("Connected"),
|
|
344
|
+
onDisconnect: (code, reason) => console.log("Disconnected"),
|
|
345
|
+
onReconnect: (attempt, delay) => console.log(`Reconnecting in ${delay}ms`),
|
|
346
|
+
onReconnectFailed: () => console.log("Reconnection failed"),
|
|
347
|
+
onEvent: (event, data) => console.log("Event:", event, data),
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
// Connection state
|
|
351
|
+
client.state; // "disconnected" | "connecting" | "connected" | "reconnecting"
|
|
352
|
+
client.isConnected; // boolean
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
## Hibernation-Safe Durable Objects
|
|
356
|
+
|
|
357
|
+
For Cloudflare Durable Objects that need hibernation-safe outgoing calls, use `DurableRpcPeer` with continuation-passing style:
|
|
358
|
+
|
|
359
|
+
```typescript
|
|
360
|
+
import { DurableRpcPeer } from "@igoforth/ws-rpc/peer";
|
|
361
|
+
import { SqlPendingCallStorage } from "@igoforth/ws-rpc/storage";
|
|
362
|
+
|
|
363
|
+
class MyDO extends Actor<Env> {
|
|
364
|
+
private peer!: DurableRpcPeer<LocalSchema, RemoteSchema, this>;
|
|
365
|
+
|
|
366
|
+
onWebSocketConnect(ws: WebSocket) {
|
|
367
|
+
this.peer = new DurableRpcPeer({
|
|
368
|
+
ws,
|
|
369
|
+
localSchema: LocalSchema,
|
|
370
|
+
remoteSchema: RemoteSchema,
|
|
371
|
+
provider: this,
|
|
372
|
+
storage: new SqlPendingCallStorage(this.ctx.storage.sql),
|
|
373
|
+
actor: this,
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
onWebSocketMessage(ws: WebSocket, message: string | ArrayBuffer) {
|
|
378
|
+
this.peer.handleMessage(message);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
async doSomething() {
|
|
382
|
+
// Promise-based (NOT hibernation-safe - pending if DO hibernates)
|
|
383
|
+
const result = await this.peer.driver.someMethod({ data: "value" });
|
|
384
|
+
|
|
385
|
+
// Continuation-based (hibernation-safe)
|
|
386
|
+
this.peer.callWithCallback("someMethod", { data: "value" }, "onResult");
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Callback receives result even after hibernation
|
|
390
|
+
onResult(result: SomeResult, context: CallContext) {
|
|
391
|
+
console.log("Result:", result);
|
|
392
|
+
console.log("Latency:", context.latencyMs, "ms");
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
## Performance
|
|
398
|
+
|
|
399
|
+
Real WebSocket RPC round-trip benchmarks (localhost, Node.js):
|
|
400
|
+
|
|
401
|
+
**Wire sizes:**
|
|
402
|
+
| Payload | JSON | MessagePack | CBOR |
|
|
403
|
+
|---------|------|-------------|------|
|
|
404
|
+
| Small | 93 B | 71 B | 112 B |
|
|
405
|
+
| Medium | 3.5 KB | 2.1 KB | 1.4 KB |
|
|
406
|
+
| Large | 24.5 KB | 19.6 KB | 14.1 KB |
|
|
407
|
+
|
|
408
|
+
**Throughput (ops/sec):**
|
|
409
|
+
| Payload | JSON | MessagePack | CBOR |
|
|
410
|
+
|---------|------|-------------|------|
|
|
411
|
+
| Small | 1,371 | 2,208 | **2,423** |
|
|
412
|
+
| Medium | 2,218 | 2,221 | **2,249** |
|
|
413
|
+
| Large | 1,334 | 1,245 | **1,562** |
|
|
414
|
+
|
|
415
|
+
CBOR provides the best balance of speed and wire size for most payloads. MessagePack excels with smaller payloads. JSON is the most portable but largest.
|
|
416
|
+
|
|
417
|
+
Run benchmarks yourself: `pnpm bench`
|
|
418
|
+
|
|
419
|
+
## Multi-Peer Driver Results
|
|
420
|
+
|
|
421
|
+
When calling methods via `server.driver` or `this.driver` in a Durable Object, results are returned as an array:
|
|
422
|
+
|
|
423
|
+
```typescript
|
|
424
|
+
// Call all connected peers
|
|
425
|
+
const results = await server.driver.getData({});
|
|
426
|
+
|
|
427
|
+
// Each result contains the peer ID and success/error
|
|
428
|
+
for (const { id, result } of results) {
|
|
429
|
+
if (result.success) {
|
|
430
|
+
console.log(`Peer ${id} returned:`, result.value);
|
|
431
|
+
} else {
|
|
432
|
+
console.error(`Peer ${id} failed:`, result.error.message);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// Call specific peers
|
|
437
|
+
const singleResult = await server.driver.getData({}, { ids: "peer-123" });
|
|
438
|
+
const multiResult = await server.driver.getData({}, {
|
|
439
|
+
ids: ["peer-1", "peer-2"],
|
|
440
|
+
timeout: 5000,
|
|
441
|
+
});
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
## License
|
|
445
|
+
|
|
446
|
+
MIT
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { a as InferEventData, h as StringKeys, m as RpcSchema, n as EventDef, p as Provider, t as Driver } from "../schema-CN5HHHku.js";
|
|
2
|
+
import "../factory-C1v0AEHY.js";
|
|
3
|
+
import "../json-54Z2bIIs.js";
|
|
4
|
+
import "../index-Be7jjS77.js";
|
|
5
|
+
import "../protocol-DA84zrc2.js";
|
|
6
|
+
import { a as IRpcOptions, c as WebSocketOptions, o as IWebSocket } from "../types-Be-qmQu0.js";
|
|
7
|
+
import "../default-xDNNMrg0.js";
|
|
8
|
+
import { t as ReconnectOptions } from "../reconnect-DbcN0R_1.js";
|
|
9
|
+
import { IAdapterHooks, IConnectionAdapter } from "./types.js";
|
|
10
|
+
|
|
11
|
+
//#region src/adapters/client.d.ts
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Options for creating an RpcClient
|
|
15
|
+
*/
|
|
16
|
+
interface RpcClientOptions<TLocalSchema extends RpcSchema, TRemoteSchema extends RpcSchema> extends IAdapterHooks<TRemoteSchema>, IRpcOptions<TLocalSchema, TRemoteSchema> {
|
|
17
|
+
/** WebSocket URL to connect to */
|
|
18
|
+
url: string;
|
|
19
|
+
/** Implementation of local methods */
|
|
20
|
+
provider: Provider<TLocalSchema>;
|
|
21
|
+
/** Auto-reconnect options (set to false to disable) */
|
|
22
|
+
reconnect?: ReconnectOptions | false;
|
|
23
|
+
/** Automatically connect when client is created (default: false) */
|
|
24
|
+
autoConnect?: boolean;
|
|
25
|
+
/** WebSocket subprotocols */
|
|
26
|
+
protocols?: string | string[];
|
|
27
|
+
/** HTTP headers for WebSocket upgrade request (Bun/Node.js only) */
|
|
28
|
+
headers?: Record<string, string>;
|
|
29
|
+
/** Custom WebSocket constructor (defaults to global WebSocket) */
|
|
30
|
+
WebSocket?: new (url: string, options?: string | string[] | WebSocketOptions) => IWebSocket;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Connection state
|
|
34
|
+
*/
|
|
35
|
+
type ConnectionState = "disconnected" | "connecting" | "connected" | "reconnecting";
|
|
36
|
+
/**
|
|
37
|
+
* RPC Client with auto-reconnect
|
|
38
|
+
*
|
|
39
|
+
* Manages WebSocket connection lifecycle and provides RPC capabilities.
|
|
40
|
+
*/
|
|
41
|
+
declare class RpcClient<TLocalSchema extends RpcSchema, TRemoteSchema extends RpcSchema> implements IConnectionAdapter<TLocalSchema, TRemoteSchema> {
|
|
42
|
+
readonly localSchema: TLocalSchema;
|
|
43
|
+
readonly remoteSchema: TRemoteSchema;
|
|
44
|
+
readonly provider: Provider<TLocalSchema>;
|
|
45
|
+
readonly hooks: IAdapterHooks<TRemoteSchema>;
|
|
46
|
+
private readonly url;
|
|
47
|
+
private readonly reconnectOptions;
|
|
48
|
+
private readonly defaultTimeout;
|
|
49
|
+
private readonly protocols;
|
|
50
|
+
private readonly headers;
|
|
51
|
+
private readonly WebSocketImpl;
|
|
52
|
+
private ws;
|
|
53
|
+
private peer;
|
|
54
|
+
private _state;
|
|
55
|
+
private reconnectAttempt;
|
|
56
|
+
private reconnectTimeout;
|
|
57
|
+
private intentionalClose;
|
|
58
|
+
constructor(options: RpcClientOptions<TLocalSchema, TRemoteSchema>);
|
|
59
|
+
/**
|
|
60
|
+
* Current connection state
|
|
61
|
+
*/
|
|
62
|
+
get state(): ConnectionState;
|
|
63
|
+
/**
|
|
64
|
+
* Whether the client is currently connected
|
|
65
|
+
*/
|
|
66
|
+
get isConnected(): boolean;
|
|
67
|
+
/**
|
|
68
|
+
* Get the driver for calling remote methods
|
|
69
|
+
*
|
|
70
|
+
* @returns Driver proxy for calling remote methods
|
|
71
|
+
* @throws Error if not connected
|
|
72
|
+
*/
|
|
73
|
+
get driver(): Driver<TRemoteSchema>;
|
|
74
|
+
/**
|
|
75
|
+
* Emit an event to the server (fire-and-forget)
|
|
76
|
+
*
|
|
77
|
+
* @param event - Event name from local schema
|
|
78
|
+
* @param data - Event data matching the schema
|
|
79
|
+
*/
|
|
80
|
+
emit<K extends StringKeys<TLocalSchema["events"]>>(event: K, data: TLocalSchema["events"] extends Record<string, EventDef> ? InferEventData<TLocalSchema["events"][K]> : never): void;
|
|
81
|
+
/**
|
|
82
|
+
* Connect to the WebSocket server
|
|
83
|
+
*
|
|
84
|
+
* @returns Promise that resolves when connected
|
|
85
|
+
* @throws Error if connection fails
|
|
86
|
+
*/
|
|
87
|
+
connect(): Promise<void>;
|
|
88
|
+
/**
|
|
89
|
+
* Disconnect from the server
|
|
90
|
+
*
|
|
91
|
+
* @param code - WebSocket close code (default: 1000)
|
|
92
|
+
* @param reason - Close reason message (default: "Client disconnect")
|
|
93
|
+
*/
|
|
94
|
+
disconnect(code?: number, reason?: string): void;
|
|
95
|
+
/**
|
|
96
|
+
* Handle WebSocket open event
|
|
97
|
+
*/
|
|
98
|
+
private handleOpen;
|
|
99
|
+
/**
|
|
100
|
+
* Handle WebSocket close event
|
|
101
|
+
*/
|
|
102
|
+
private handleClose;
|
|
103
|
+
/**
|
|
104
|
+
* Schedule a reconnection attempt
|
|
105
|
+
*/
|
|
106
|
+
private scheduleReconnect;
|
|
107
|
+
/**
|
|
108
|
+
* Attempt to reconnect
|
|
109
|
+
*/
|
|
110
|
+
private attemptReconnect;
|
|
111
|
+
/**
|
|
112
|
+
* Cancel any pending reconnection
|
|
113
|
+
*/
|
|
114
|
+
private cancelReconnect;
|
|
115
|
+
}
|
|
116
|
+
//#endregion
|
|
117
|
+
export { ConnectionState, RpcClient, RpcClientOptions };
|