@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
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Durable RPC Peer
|
|
3
|
+
*
|
|
4
|
+
* Extends RpcPeer to add hibernation-safe continuation-based RPC calls.
|
|
5
|
+
* Uses synchronous storage to persist pending calls across DO hibernation.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```ts
|
|
9
|
+
* // In your Durable Object
|
|
10
|
+
* class MyDO extends Actor<Env> {
|
|
11
|
+
* private peer: DurableRpcPeer<LocalSchema, RemoteSchema, this>;
|
|
12
|
+
*
|
|
13
|
+
* onWebSocketConnect(ws: WebSocket) {
|
|
14
|
+
* const storage = new SqlPendingCallStorage(this.ctx.storage.sql);
|
|
15
|
+
* this.peer = new DurableRpcPeer({
|
|
16
|
+
* ws,
|
|
17
|
+
* localSchema,
|
|
18
|
+
* remoteSchema,
|
|
19
|
+
* provider,
|
|
20
|
+
* storage,
|
|
21
|
+
* actor: this,
|
|
22
|
+
* });
|
|
23
|
+
* }
|
|
24
|
+
*
|
|
25
|
+
* async doSomething() {
|
|
26
|
+
* // Promise-based (not hibernation-safe)
|
|
27
|
+
* const result = await this.peer.driver.someMethod({ data });
|
|
28
|
+
*
|
|
29
|
+
* // Continuation-based (hibernation-safe)
|
|
30
|
+
* this.peer.callWithCallback('someMethod', { data }, 'onSomeMethodResult');
|
|
31
|
+
* }
|
|
32
|
+
*
|
|
33
|
+
* // Callback method - called with result even after hibernation
|
|
34
|
+
* onSomeMethodResult(result: SomeResult, context: CallContext) {
|
|
35
|
+
* // Handle result
|
|
36
|
+
* }
|
|
37
|
+
* }
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
import type { WireInput } from "../protocol.js";
|
|
42
|
+
import type { RpcSchema } from "../schema.js";
|
|
43
|
+
import type {
|
|
44
|
+
PendingCall,
|
|
45
|
+
SyncPendingCallStorage,
|
|
46
|
+
} from "../storage/interface.js";
|
|
47
|
+
import { RpcPeer, type RpcPeerOptions } from "./default.js";
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Options for creating a DurableRpcPeer
|
|
51
|
+
*/
|
|
52
|
+
export interface DurableRpcPeerOptions<TActor> {
|
|
53
|
+
/** Synchronous storage for persisting pending calls */
|
|
54
|
+
storage: SyncPendingCallStorage;
|
|
55
|
+
/** The actor instance (for resolving callback methods) */
|
|
56
|
+
actor: TActor;
|
|
57
|
+
/** Default timeout for continuation-based calls (ms) */
|
|
58
|
+
durableTimeout?: number;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Context passed to callback methods along with the result
|
|
63
|
+
*/
|
|
64
|
+
export interface CallContext {
|
|
65
|
+
/** The original pending call */
|
|
66
|
+
call: PendingCall;
|
|
67
|
+
/** Time from send to response (ms) */
|
|
68
|
+
latencyMs: number;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Durable RPC Peer
|
|
73
|
+
*
|
|
74
|
+
* Extends RpcPeer to add:
|
|
75
|
+
* - Hibernation-safe continuation-based calls via `callWithCallback`
|
|
76
|
+
* - Automatic recovery of pending calls after hibernation
|
|
77
|
+
* - Timeout cleanup for stale calls
|
|
78
|
+
*/
|
|
79
|
+
export class DurableRpcPeer<
|
|
80
|
+
TLocalSchema extends RpcSchema,
|
|
81
|
+
TRemoteSchema extends RpcSchema,
|
|
82
|
+
TActor,
|
|
83
|
+
> extends RpcPeer<TLocalSchema, TRemoteSchema> {
|
|
84
|
+
private readonly storage: SyncPendingCallStorage;
|
|
85
|
+
private readonly actor: TActor;
|
|
86
|
+
private readonly durableTimeout: number;
|
|
87
|
+
private durableRequestCounter = 0;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Create a durable RPC peer
|
|
91
|
+
*
|
|
92
|
+
* @param options - Combined RPC peer and durable options
|
|
93
|
+
*/
|
|
94
|
+
constructor(
|
|
95
|
+
options: RpcPeerOptions<TLocalSchema, TRemoteSchema> &
|
|
96
|
+
DurableRpcPeerOptions<TActor>,
|
|
97
|
+
) {
|
|
98
|
+
super(options);
|
|
99
|
+
this.storage = options.storage;
|
|
100
|
+
this.actor = options.actor;
|
|
101
|
+
this.durableTimeout = options.durableTimeout ?? options.timeout ?? 30000;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Make a hibernation-safe RPC call using continuation-passing style
|
|
106
|
+
*
|
|
107
|
+
* Instead of returning a Promise, the result will be passed to the
|
|
108
|
+
* named callback method on the actor. This survives DO hibernation.
|
|
109
|
+
*
|
|
110
|
+
* @param method - Remote method to call
|
|
111
|
+
* @param params - Parameters for the method
|
|
112
|
+
* @param callback - Name of method on actor to call with result
|
|
113
|
+
* @param timeout - Optional timeout override (ms)
|
|
114
|
+
*
|
|
115
|
+
* @example
|
|
116
|
+
* ```ts
|
|
117
|
+
* // Make the call
|
|
118
|
+
* peer.callWithCallback('executeOrder', { market, side }, 'onOrderExecuted');
|
|
119
|
+
*
|
|
120
|
+
* // Define the callback on your actor
|
|
121
|
+
* onOrderExecuted(result: OrderResult, context: CallContext) {
|
|
122
|
+
* console.log('Order executed:', result);
|
|
123
|
+
* console.log('Latency:', context.latencyMs, 'ms');
|
|
124
|
+
* }
|
|
125
|
+
* ```
|
|
126
|
+
*/
|
|
127
|
+
callWithCallback<K extends keyof TRemoteSchema["methods"] & string>(
|
|
128
|
+
method: K,
|
|
129
|
+
params: unknown,
|
|
130
|
+
callback: keyof TActor & string,
|
|
131
|
+
timeout?: number,
|
|
132
|
+
): void {
|
|
133
|
+
// Validate callback exists and is a function
|
|
134
|
+
const callbackFn = this.actor[callback as keyof TActor];
|
|
135
|
+
if (typeof callbackFn !== "function") {
|
|
136
|
+
throw new Error(`Callback '${callback}' is not a function on the actor`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const now = Date.now();
|
|
140
|
+
const timeoutMs = timeout ?? this.durableTimeout;
|
|
141
|
+
|
|
142
|
+
const call: PendingCall = {
|
|
143
|
+
id: `durable-${++this.durableRequestCounter}`,
|
|
144
|
+
method,
|
|
145
|
+
params,
|
|
146
|
+
callback,
|
|
147
|
+
sentAt: now,
|
|
148
|
+
timeoutAt: now + timeoutMs,
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
// Persist to storage BEFORE sending (ensures delivery even if we crash)
|
|
152
|
+
this.storage.save(call);
|
|
153
|
+
|
|
154
|
+
// Send the request
|
|
155
|
+
const ws = this.getWebSocket();
|
|
156
|
+
if (ws.readyState === 1) {
|
|
157
|
+
ws.send(this.protocol.createRequest(call.id, method, params));
|
|
158
|
+
} else {
|
|
159
|
+
console.warn(`Cannot send durable call '${method}': connection not open`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Handle an incoming WebSocket message
|
|
165
|
+
*
|
|
166
|
+
* Checks durable storage for continuation-based calls before
|
|
167
|
+
* delegating to the base class for promise-based calls.
|
|
168
|
+
*
|
|
169
|
+
* @param data - Raw WebSocket message data
|
|
170
|
+
*/
|
|
171
|
+
override handleMessage(data: WireInput): void {
|
|
172
|
+
const message = this.protocol.safeDecodeMessage(data);
|
|
173
|
+
if (!message) {
|
|
174
|
+
// Let base class handle parse errors
|
|
175
|
+
super.handleMessage(data);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Check if this is a response for a durable call
|
|
180
|
+
if (message.type === "rpc:response" || message.type === "rpc:error") {
|
|
181
|
+
const id = message.id;
|
|
182
|
+
const call = this.storage.get(id);
|
|
183
|
+
|
|
184
|
+
if (call) {
|
|
185
|
+
// This is a durable call - handle via callback
|
|
186
|
+
this.storage.delete(id);
|
|
187
|
+
|
|
188
|
+
const context: CallContext = {
|
|
189
|
+
call,
|
|
190
|
+
latencyMs: Date.now() - call.sentAt,
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const callbackFn = this.actor[call.callback as keyof TActor];
|
|
194
|
+
if (typeof callbackFn === "function") {
|
|
195
|
+
if (message.type === "rpc:response") {
|
|
196
|
+
callbackFn.call(this.actor, message.result, context);
|
|
197
|
+
} else {
|
|
198
|
+
callbackFn.call(this.actor, new Error(message.message), context);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Not a durable call - delegate to base class
|
|
206
|
+
super.handleMessage(data);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Get all pending durable calls (for debugging/monitoring)
|
|
211
|
+
*
|
|
212
|
+
* @returns Array of all pending calls
|
|
213
|
+
*/
|
|
214
|
+
getPendingCalls(): PendingCall[] {
|
|
215
|
+
return this.storage.listAll();
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Get expired calls that have exceeded their timeout
|
|
220
|
+
*
|
|
221
|
+
* @returns Array of calls that have exceeded their timeout
|
|
222
|
+
*/
|
|
223
|
+
getExpiredCalls(): PendingCall[] {
|
|
224
|
+
return this.storage.listExpired(Date.now());
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Clean up expired calls
|
|
229
|
+
*
|
|
230
|
+
* Call this periodically (e.g., on alarm) to remove stale calls.
|
|
231
|
+
*
|
|
232
|
+
* @returns The expired calls that were removed (for optional error handling)
|
|
233
|
+
*/
|
|
234
|
+
cleanupExpired(): PendingCall[] {
|
|
235
|
+
const expired = this.getExpiredCalls();
|
|
236
|
+
for (const call of expired) {
|
|
237
|
+
this.storage.delete(call.id);
|
|
238
|
+
}
|
|
239
|
+
return expired;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Clear all pending durable calls
|
|
244
|
+
*/
|
|
245
|
+
clearPendingCalls(): void {
|
|
246
|
+
this.storage.clear();
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Create a factory function for DurableRpcPeer instances
|
|
252
|
+
*
|
|
253
|
+
* Pre-configures the durable storage and actor, returning a function
|
|
254
|
+
* that only needs RPC options to create a new peer.
|
|
255
|
+
*
|
|
256
|
+
* @param durableOptions - Durable configuration (storage, actor, timeout)
|
|
257
|
+
* @returns Factory function that creates DurableRpcPeer instances
|
|
258
|
+
*
|
|
259
|
+
* @example
|
|
260
|
+
* ```ts
|
|
261
|
+
* const createPeer = createDurableRpcPeerFactory({
|
|
262
|
+
* storage: new SqlPendingCallStorage(sql),
|
|
263
|
+
* actor: this,
|
|
264
|
+
* });
|
|
265
|
+
*
|
|
266
|
+
* // Later, create peers for each connection
|
|
267
|
+
* const peer = createPeer({
|
|
268
|
+
* ws,
|
|
269
|
+
* localSchema,
|
|
270
|
+
* remoteSchema,
|
|
271
|
+
* provider,
|
|
272
|
+
* });
|
|
273
|
+
* ```
|
|
274
|
+
*/
|
|
275
|
+
export const createDurableRpcPeerFactory =
|
|
276
|
+
<TActor>(durableOptions: DurableRpcPeerOptions<TActor>) =>
|
|
277
|
+
<TLocalSchema extends RpcSchema, TRemoteSchema extends RpcSchema>(
|
|
278
|
+
rpcOptions: RpcPeerOptions<TLocalSchema, TRemoteSchema>,
|
|
279
|
+
) =>
|
|
280
|
+
new DurableRpcPeer({ ...durableOptions, ...rpcOptions });
|
package/src/protocol.ts
ADDED
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wire Protocol Definitions
|
|
3
|
+
*
|
|
4
|
+
* Defines the message format for bidirectional RPC over WebSocket.
|
|
5
|
+
* Messages can be JSON-encoded (string) or binary-encoded (Uint8Array).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as z from "zod";
|
|
9
|
+
import {
|
|
10
|
+
createJsonCodec,
|
|
11
|
+
isStringCodec,
|
|
12
|
+
type WireCodec,
|
|
13
|
+
} from "./codecs/index.js";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* RPC Request - sent when calling a remote method
|
|
17
|
+
*/
|
|
18
|
+
export const RpcRequestSchema = z.object({
|
|
19
|
+
type: z.literal("rpc:request"),
|
|
20
|
+
id: z.string(),
|
|
21
|
+
method: z.string(),
|
|
22
|
+
params: z.unknown(),
|
|
23
|
+
});
|
|
24
|
+
export type RpcRequest = z.infer<typeof RpcRequestSchema>;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* RPC Response - sent as success response to a request
|
|
28
|
+
*/
|
|
29
|
+
export const RpcResponseSchema = z.object({
|
|
30
|
+
type: z.literal("rpc:response"),
|
|
31
|
+
id: z.string(),
|
|
32
|
+
result: z.unknown(),
|
|
33
|
+
});
|
|
34
|
+
export type RpcResponse = z.infer<typeof RpcResponseSchema>;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* RPC Error - sent when a request fails
|
|
38
|
+
*/
|
|
39
|
+
export const RpcErrorSchema = z.object({
|
|
40
|
+
type: z.literal("rpc:error"),
|
|
41
|
+
id: z.string(),
|
|
42
|
+
code: z.number(),
|
|
43
|
+
message: z.string(),
|
|
44
|
+
data: z.unknown().optional(),
|
|
45
|
+
});
|
|
46
|
+
export type RpcError = z.infer<typeof RpcErrorSchema>;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* RPC Event - fire-and-forget event (no response expected)
|
|
50
|
+
*/
|
|
51
|
+
export const RpcEventSchema = z.object({
|
|
52
|
+
type: z.literal("rpc:event"),
|
|
53
|
+
event: z.string(),
|
|
54
|
+
data: z.unknown(),
|
|
55
|
+
});
|
|
56
|
+
export type RpcEvent = z.infer<typeof RpcEventSchema>;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Union of all RPC message types
|
|
60
|
+
*/
|
|
61
|
+
export const RpcMessageSchema = z.union([
|
|
62
|
+
RpcRequestSchema,
|
|
63
|
+
RpcResponseSchema,
|
|
64
|
+
RpcErrorSchema,
|
|
65
|
+
RpcEventSchema,
|
|
66
|
+
]);
|
|
67
|
+
export type RpcMessage = z.infer<typeof RpcMessageSchema>;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Standard RPC error codes (JSON-RPC 2.0 compatible)
|
|
71
|
+
*/
|
|
72
|
+
export const RpcErrorCodes = {
|
|
73
|
+
PARSE_ERROR: -32700,
|
|
74
|
+
INVALID_REQUEST: -32600,
|
|
75
|
+
METHOD_NOT_FOUND: -32601,
|
|
76
|
+
INVALID_PARAMS: -32602,
|
|
77
|
+
INTERNAL_ERROR: -32603,
|
|
78
|
+
// Custom codes (-32000 to -32099 reserved for implementation-defined errors)
|
|
79
|
+
TIMEOUT: -32000,
|
|
80
|
+
CONNECTION_CLOSED: -32001,
|
|
81
|
+
VALIDATION_ERROR: -32002,
|
|
82
|
+
} as const;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Default JSON codec for RPC messages
|
|
86
|
+
*
|
|
87
|
+
* Encodes RPC messages to JSON strings with validation on decode.
|
|
88
|
+
*
|
|
89
|
+
* @example
|
|
90
|
+
* ```ts
|
|
91
|
+
* // Encode a message
|
|
92
|
+
* const json = RpcMessageCodec.encode(createRequest("1", "ping", {}));
|
|
93
|
+
*
|
|
94
|
+
* // Decode and validate
|
|
95
|
+
* const message = RpcMessageCodec.decode(json);
|
|
96
|
+
* ```
|
|
97
|
+
*/
|
|
98
|
+
export const RpcMessageCodec = createJsonCodec(RpcMessageSchema);
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Type alias for RPC wire codecs
|
|
102
|
+
*
|
|
103
|
+
* Wire codecs can encode to string (text frames) or Uint8Array (binary frames).
|
|
104
|
+
*/
|
|
105
|
+
export type RpcWireCodec = WireCodec<typeof RpcMessageSchema>;
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Wire data type - inferred from codec
|
|
109
|
+
*/
|
|
110
|
+
type WireDataOf<T extends RpcWireCodec> =
|
|
111
|
+
T extends z.ZodCodec<infer A>
|
|
112
|
+
? A extends z.ZodType<infer V>
|
|
113
|
+
? V
|
|
114
|
+
: never
|
|
115
|
+
: never;
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Wire data type - inferred from codec
|
|
119
|
+
*/
|
|
120
|
+
type WireInputOf<T extends RpcWireCodec> =
|
|
121
|
+
T extends z.ZodCodec<any, infer B>
|
|
122
|
+
? B extends z.ZodType<infer V>
|
|
123
|
+
? V
|
|
124
|
+
: never
|
|
125
|
+
: never;
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Wire input types accepted by decode methods
|
|
129
|
+
*
|
|
130
|
+
* Includes Node.js ws library's RawData type (Buffer | ArrayBuffer | Buffer[])
|
|
131
|
+
* for seamless integration with the ws package.
|
|
132
|
+
*/
|
|
133
|
+
export type WireInput = string | ArrayBuffer | Uint8Array | Uint8Array[];
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Protocol interface returned by createProtocol
|
|
137
|
+
*/
|
|
138
|
+
export interface RpcProtocol<TWire extends RpcWireCodec = RpcWireCodec> {
|
|
139
|
+
/** The underlying codec */
|
|
140
|
+
readonly codec: TWire;
|
|
141
|
+
|
|
142
|
+
/** Create and encode an RPC request */
|
|
143
|
+
createRequest(id: string, method: string, params: unknown): WireDataOf<TWire>;
|
|
144
|
+
|
|
145
|
+
/** Create and encode an RPC response */
|
|
146
|
+
createResponse(id: string, result: unknown): WireDataOf<TWire>;
|
|
147
|
+
|
|
148
|
+
/** Create and encode an RPC error */
|
|
149
|
+
createError(
|
|
150
|
+
id: string,
|
|
151
|
+
code: number,
|
|
152
|
+
message: string,
|
|
153
|
+
data?: unknown,
|
|
154
|
+
): WireDataOf<TWire>;
|
|
155
|
+
|
|
156
|
+
/** Create and encode an RPC event */
|
|
157
|
+
createEvent(event: string, data: unknown): WireDataOf<TWire>;
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Decode wire data to an RPC message (throws on invalid)
|
|
161
|
+
*
|
|
162
|
+
* Accepts string, ArrayBuffer, Uint8Array (including Node.js Buffer),
|
|
163
|
+
* or Uint8Array[] (for ws library's fragmented messages).
|
|
164
|
+
*/
|
|
165
|
+
decodeMessage(data: WireInput): RpcMessage;
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Safely decode wire data (returns null on invalid)
|
|
169
|
+
*
|
|
170
|
+
* Accepts string, ArrayBuffer, Uint8Array (including Node.js Buffer),
|
|
171
|
+
* or Uint8Array[] (for ws library's fragmented messages).
|
|
172
|
+
*/
|
|
173
|
+
safeDecodeMessage(data: WireInput): RpcMessage | null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Create a protocol instance with bound encode/decode functions
|
|
178
|
+
*
|
|
179
|
+
* @param codec - Wire codec for serialization (defaults to JSON)
|
|
180
|
+
* @returns Protocol object with pre-bound encode/decode methods
|
|
181
|
+
*
|
|
182
|
+
* @example
|
|
183
|
+
* ```ts
|
|
184
|
+
* // JSON protocol (default)
|
|
185
|
+
* const protocol = createProtocol();
|
|
186
|
+
*
|
|
187
|
+
* // MessagePack protocol
|
|
188
|
+
* import { createMsgpackCodec } from "@igoforth/ws-rpc/codecs/msgpack";
|
|
189
|
+
* const protocol = createProtocol(createMsgpackCodec(RpcMessageSchema));
|
|
190
|
+
*
|
|
191
|
+
* // Use in peer
|
|
192
|
+
* const wire = protocol.createRequest("1", "ping", {});
|
|
193
|
+
* ws.send(wire); // string or Uint8Array depending on codec
|
|
194
|
+
*
|
|
195
|
+
* const message = protocol.decodeMessage(event.data);
|
|
196
|
+
* ```
|
|
197
|
+
*/
|
|
198
|
+
export function createProtocol<
|
|
199
|
+
TWire extends RpcWireCodec = typeof RpcMessageCodec,
|
|
200
|
+
>(codec: TWire = RpcMessageCodec as TWire): RpcProtocol<TWire> {
|
|
201
|
+
const isString = isStringCodec(codec);
|
|
202
|
+
const textDecoder = new TextDecoder();
|
|
203
|
+
const textEncoder = new TextEncoder();
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Normalize input for the codec type.
|
|
207
|
+
* String codecs need string input (decode ArrayBuffer via TextDecoder).
|
|
208
|
+
* Binary codecs need Uint8Array input.
|
|
209
|
+
*
|
|
210
|
+
* Handles ws library's RawData (Buffer | ArrayBuffer | Buffer[]):
|
|
211
|
+
* - Buffer extends Uint8Array, so it's handled as Uint8Array
|
|
212
|
+
* - Buffer[] (fragmented messages) are concatenated
|
|
213
|
+
*/
|
|
214
|
+
const normalizeInput = (data: WireInput): string | Uint8Array => {
|
|
215
|
+
// Handle Uint8Array[] (ws fragmented messages) first
|
|
216
|
+
if (Array.isArray(data)) {
|
|
217
|
+
const totalLength = data.reduce((sum, buf) => sum + buf.byteLength, 0);
|
|
218
|
+
const result = new Uint8Array(totalLength);
|
|
219
|
+
let offset = 0;
|
|
220
|
+
for (const buf of data) {
|
|
221
|
+
result.set(buf, offset);
|
|
222
|
+
offset += buf.byteLength;
|
|
223
|
+
}
|
|
224
|
+
// Now decode the concatenated buffer
|
|
225
|
+
return isString ? textDecoder.decode(result) : result;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (isString) {
|
|
229
|
+
// String codec - decode binary to string if needed
|
|
230
|
+
if (typeof data === "string") return data;
|
|
231
|
+
if (data instanceof ArrayBuffer) {
|
|
232
|
+
return textDecoder.decode(data);
|
|
233
|
+
}
|
|
234
|
+
// Uint8Array (including Node.js Buffer)
|
|
235
|
+
return textDecoder.decode(data);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Binary codec - convert to Uint8Array
|
|
239
|
+
if (typeof data === "string") {
|
|
240
|
+
return textEncoder.encode(data);
|
|
241
|
+
}
|
|
242
|
+
if (data instanceof ArrayBuffer) {
|
|
243
|
+
return new Uint8Array(data);
|
|
244
|
+
}
|
|
245
|
+
// Uint8Array (including Node.js Buffer) - return as-is
|
|
246
|
+
return data;
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
return {
|
|
250
|
+
codec,
|
|
251
|
+
|
|
252
|
+
createRequest(id, method, params) {
|
|
253
|
+
return codec.encode({
|
|
254
|
+
type: "rpc:request",
|
|
255
|
+
id,
|
|
256
|
+
method,
|
|
257
|
+
params,
|
|
258
|
+
}) as WireDataOf<TWire>;
|
|
259
|
+
},
|
|
260
|
+
|
|
261
|
+
createResponse(id, result) {
|
|
262
|
+
return codec.encode({
|
|
263
|
+
type: "rpc:response",
|
|
264
|
+
id,
|
|
265
|
+
result,
|
|
266
|
+
}) as WireDataOf<TWire>;
|
|
267
|
+
},
|
|
268
|
+
|
|
269
|
+
createError(id, code, message, data) {
|
|
270
|
+
return codec.encode({
|
|
271
|
+
type: "rpc:error",
|
|
272
|
+
id,
|
|
273
|
+
code,
|
|
274
|
+
message,
|
|
275
|
+
data,
|
|
276
|
+
}) as WireDataOf<TWire>;
|
|
277
|
+
},
|
|
278
|
+
|
|
279
|
+
createEvent(event, data) {
|
|
280
|
+
return codec.encode({
|
|
281
|
+
type: "rpc:event",
|
|
282
|
+
event,
|
|
283
|
+
data,
|
|
284
|
+
}) as WireDataOf<TWire>;
|
|
285
|
+
},
|
|
286
|
+
|
|
287
|
+
decodeMessage(data) {
|
|
288
|
+
return codec.decode(normalizeInput(data) as WireInputOf<TWire>);
|
|
289
|
+
},
|
|
290
|
+
|
|
291
|
+
safeDecodeMessage(data) {
|
|
292
|
+
try {
|
|
293
|
+
return codec.decode(normalizeInput(data) as WireInputOf<TWire>);
|
|
294
|
+
} catch {
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
},
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Default JSON protocol instance
|
|
303
|
+
*
|
|
304
|
+
* Pre-configured with JSON codec for convenience.
|
|
305
|
+
*/
|
|
306
|
+
export const JsonProtocol = createProtocol(RpcMessageCodec);
|