@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/src/index.ts
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @igoforth/ws-rpc
|
|
3
|
+
*
|
|
4
|
+
* Bidirectional RPC over WebSocket with Zod schema validation, TypeScript inference,
|
|
5
|
+
* and Cloudflare Durable Object support.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```ts
|
|
9
|
+
* // Client
|
|
10
|
+
* import { RpcClient } from "@igoforth/ws-rpc/adapters/client";
|
|
11
|
+
*
|
|
12
|
+
* const client = new RpcClient({
|
|
13
|
+
* url: "wss://example.com/ws",
|
|
14
|
+
* localSchema: ClientSchema,
|
|
15
|
+
* remoteSchema: ServerSchema,
|
|
16
|
+
* provider: { clientMethod: async (input) => { ... } },
|
|
17
|
+
* });
|
|
18
|
+
*
|
|
19
|
+
* // Server (Cloudflare Durable Object)
|
|
20
|
+
* import { withRpc } from "@igoforth/ws-rpc/adapters/cloudflare-do";
|
|
21
|
+
*
|
|
22
|
+
* class MyDO extends withRpc(Actor, {
|
|
23
|
+
* localSchema: ServerSchema,
|
|
24
|
+
* remoteSchema: ClientSchema,
|
|
25
|
+
* }) { ... }
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
// Errors
|
|
30
|
+
export {
|
|
31
|
+
RpcConnectionClosed,
|
|
32
|
+
RpcError,
|
|
33
|
+
RpcMethodNotFoundError,
|
|
34
|
+
RpcRemoteError,
|
|
35
|
+
RpcTimeoutError,
|
|
36
|
+
RpcValidationError,
|
|
37
|
+
} from "./errors.js";
|
|
38
|
+
// Protocol
|
|
39
|
+
export {
|
|
40
|
+
createProtocol,
|
|
41
|
+
JsonProtocol,
|
|
42
|
+
type RpcError as RpcErrorMessage,
|
|
43
|
+
RpcErrorCodes,
|
|
44
|
+
RpcErrorSchema,
|
|
45
|
+
type RpcEvent,
|
|
46
|
+
RpcEventSchema,
|
|
47
|
+
type RpcMessage,
|
|
48
|
+
RpcMessageCodec,
|
|
49
|
+
RpcMessageSchema,
|
|
50
|
+
type RpcProtocol,
|
|
51
|
+
type RpcRequest,
|
|
52
|
+
RpcRequestSchema,
|
|
53
|
+
type RpcResponse,
|
|
54
|
+
RpcResponseSchema,
|
|
55
|
+
type RpcWireCodec,
|
|
56
|
+
} from "./protocol.js";
|
|
57
|
+
// Schema utilities
|
|
58
|
+
export {
|
|
59
|
+
type Driver,
|
|
60
|
+
type EventDef,
|
|
61
|
+
type EventHandler,
|
|
62
|
+
event,
|
|
63
|
+
type InferEventData,
|
|
64
|
+
type InferEvents,
|
|
65
|
+
type InferInput,
|
|
66
|
+
type InferMethods,
|
|
67
|
+
type InferOutput,
|
|
68
|
+
type MethodDef,
|
|
69
|
+
method,
|
|
70
|
+
type Provider,
|
|
71
|
+
type RpcSchema,
|
|
72
|
+
} from "./schema.js";
|
|
73
|
+
// Storage
|
|
74
|
+
export {
|
|
75
|
+
type AsyncPendingCallStorage,
|
|
76
|
+
type MaybePromise,
|
|
77
|
+
MemoryPendingCallStorage,
|
|
78
|
+
type PendingCall,
|
|
79
|
+
type PendingCallStorage,
|
|
80
|
+
SqlPendingCallStorage,
|
|
81
|
+
type StorageMode,
|
|
82
|
+
type SyncPendingCallStorage,
|
|
83
|
+
} from "./storage/index.js";
|
|
84
|
+
// Core types and WebSocket interfaces
|
|
85
|
+
export {
|
|
86
|
+
type IEventController,
|
|
87
|
+
type IMethodController,
|
|
88
|
+
type IMinWebSocket,
|
|
89
|
+
type IRpcConnection,
|
|
90
|
+
type IRpcOptions,
|
|
91
|
+
type IWebSocket,
|
|
92
|
+
type IWebSocketServer,
|
|
93
|
+
type WebSocketOptions,
|
|
94
|
+
WebSocketReadyState,
|
|
95
|
+
type WebSocketServerOptions,
|
|
96
|
+
} from "./types.js";
|
|
97
|
+
// Utilities
|
|
98
|
+
export {
|
|
99
|
+
calculateReconnectDelay,
|
|
100
|
+
defaultReconnectOptions,
|
|
101
|
+
type ReconnectOptions,
|
|
102
|
+
} from "./utils/index.js";
|
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RPC Peer
|
|
3
|
+
*
|
|
4
|
+
* Core bidirectional RPC implementation. Both client and server are "peers"
|
|
5
|
+
* that can call methods on each other.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { v7 as uuidv7 } from "uuid";
|
|
9
|
+
import {
|
|
10
|
+
RpcConnectionClosed,
|
|
11
|
+
RpcMethodNotFoundError,
|
|
12
|
+
RpcRemoteError,
|
|
13
|
+
RpcTimeoutError,
|
|
14
|
+
RpcValidationError,
|
|
15
|
+
} from "../errors.js";
|
|
16
|
+
import {
|
|
17
|
+
JsonProtocol,
|
|
18
|
+
type RpcError,
|
|
19
|
+
RpcErrorCodes,
|
|
20
|
+
type RpcEvent,
|
|
21
|
+
type RpcProtocol,
|
|
22
|
+
type RpcRequest,
|
|
23
|
+
type RpcResponse,
|
|
24
|
+
type WireInput,
|
|
25
|
+
} from "../protocol.js";
|
|
26
|
+
import type {
|
|
27
|
+
Driver,
|
|
28
|
+
EventDef,
|
|
29
|
+
EventHandler,
|
|
30
|
+
InferEventData,
|
|
31
|
+
Provider,
|
|
32
|
+
RpcSchema,
|
|
33
|
+
StringKeys,
|
|
34
|
+
} from "../schema.js";
|
|
35
|
+
import type { IMinWebSocket, IRpcOptions } from "../types.js";
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Pending request tracking
|
|
39
|
+
*/
|
|
40
|
+
interface PendingRequest {
|
|
41
|
+
resolve: (result: unknown) => void;
|
|
42
|
+
reject: (error: Error) => void;
|
|
43
|
+
timeout: ReturnType<typeof setTimeout>;
|
|
44
|
+
method: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Options for creating an RpcPeer
|
|
49
|
+
*/
|
|
50
|
+
export interface RpcPeerOptions<
|
|
51
|
+
TLocalSchema extends RpcSchema,
|
|
52
|
+
TRemoteSchema extends RpcSchema,
|
|
53
|
+
> extends IRpcOptions<TLocalSchema, TRemoteSchema> {
|
|
54
|
+
/** Unique identifier for this peer (auto-generated if not provided) */
|
|
55
|
+
id?: string;
|
|
56
|
+
/** WebSocket instance */
|
|
57
|
+
ws: IMinWebSocket;
|
|
58
|
+
/** Implementation of local methods */
|
|
59
|
+
provider: Partial<Provider<TLocalSchema>>;
|
|
60
|
+
/**
|
|
61
|
+
* Protocol for encoding/decoding messages
|
|
62
|
+
*
|
|
63
|
+
* Defaults to JSON. Use createProtocol() with a binary codec for better performance.
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* ```ts
|
|
67
|
+
* import { createProtocol, RpcMessageSchema } from "@igoforth/ws-rpc/protocol";
|
|
68
|
+
* import { createMsgpackCodec } from "@igoforth/ws-rpc/codecs/msgpack";
|
|
69
|
+
*
|
|
70
|
+
* const peer = new RpcPeer({
|
|
71
|
+
* protocol: createProtocol(createMsgpackCodec(RpcMessageSchema)),
|
|
72
|
+
* // ...
|
|
73
|
+
* });
|
|
74
|
+
* ```
|
|
75
|
+
*/
|
|
76
|
+
protocol?: RpcProtocol | undefined;
|
|
77
|
+
/** Handler for incoming events */
|
|
78
|
+
onEvent?: EventHandler<TRemoteSchema> | undefined;
|
|
79
|
+
/** Generate unique request IDs */
|
|
80
|
+
generateId?: (() => string) | undefined;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Bidirectional RPC peer
|
|
85
|
+
*
|
|
86
|
+
* Both sides of a WebSocket connection are "peers" - they each implement
|
|
87
|
+
* some methods (provider) and can call methods on the other side (driver).
|
|
88
|
+
*/
|
|
89
|
+
export class RpcPeer<
|
|
90
|
+
TLocalSchema extends RpcSchema,
|
|
91
|
+
TRemoteSchema extends RpcSchema,
|
|
92
|
+
> {
|
|
93
|
+
/** Unique identifier for this peer */
|
|
94
|
+
readonly id: string;
|
|
95
|
+
/** WebSocket instance - protected for subclass access */
|
|
96
|
+
protected readonly ws: IMinWebSocket;
|
|
97
|
+
/** Protocol instance - protected for subclass access */
|
|
98
|
+
protected readonly protocol: RpcProtocol;
|
|
99
|
+
private readonly localSchema: TLocalSchema;
|
|
100
|
+
private readonly remoteSchema: TRemoteSchema;
|
|
101
|
+
private readonly provider: Partial<Provider<TLocalSchema>>;
|
|
102
|
+
private readonly onEventHandler?: RpcPeerOptions<
|
|
103
|
+
TLocalSchema,
|
|
104
|
+
TRemoteSchema
|
|
105
|
+
>["onEvent"];
|
|
106
|
+
private readonly defaultTimeout: number;
|
|
107
|
+
private readonly generateId: () => string;
|
|
108
|
+
private readonly pendingRequests = new Map<string, PendingRequest>();
|
|
109
|
+
private requestCounter = 0;
|
|
110
|
+
private closed = false;
|
|
111
|
+
|
|
112
|
+
/** Proxy for calling remote methods */
|
|
113
|
+
readonly driver: Driver<TRemoteSchema>;
|
|
114
|
+
|
|
115
|
+
constructor(options: RpcPeerOptions<TLocalSchema, TRemoteSchema>) {
|
|
116
|
+
this.id = options.id ?? uuidv7();
|
|
117
|
+
this.ws = options.ws;
|
|
118
|
+
this.protocol = options.protocol ?? JsonProtocol;
|
|
119
|
+
this.localSchema = options.localSchema;
|
|
120
|
+
this.remoteSchema = options.remoteSchema;
|
|
121
|
+
this.provider = options.provider;
|
|
122
|
+
this.onEventHandler = options.onEvent;
|
|
123
|
+
this.defaultTimeout = options.timeout ?? 30000;
|
|
124
|
+
this.generateId = options.generateId ?? (() => `${++this.requestCounter}`);
|
|
125
|
+
|
|
126
|
+
// Create driver proxy for calling remote methods
|
|
127
|
+
this.driver = this.createDriver();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Create a proxy that allows calling remote methods
|
|
132
|
+
*/
|
|
133
|
+
private createDriver(): Driver<TRemoteSchema> {
|
|
134
|
+
const methods = this.remoteSchema.methods ?? {};
|
|
135
|
+
const driver: Record<string, (input: unknown) => Promise<unknown>> = {};
|
|
136
|
+
|
|
137
|
+
for (const methodName of Object.keys(methods)) {
|
|
138
|
+
driver[methodName] = (input: unknown) =>
|
|
139
|
+
this.callMethod(methodName, input);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return driver as Driver<TRemoteSchema>;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Call a remote method and wait for the response (used by driver proxy)
|
|
147
|
+
*/
|
|
148
|
+
private async callMethod(
|
|
149
|
+
method: string,
|
|
150
|
+
input: unknown,
|
|
151
|
+
timeout?: number,
|
|
152
|
+
): Promise<unknown> {
|
|
153
|
+
if (this.closed || this.ws.readyState !== 1) {
|
|
154
|
+
throw new RpcConnectionClosed();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const methodDef = this.remoteSchema.methods?.[method];
|
|
158
|
+
if (!methodDef) {
|
|
159
|
+
throw new RpcMethodNotFoundError(method);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Validate input against schema
|
|
163
|
+
const parseResult = methodDef.input.safeParse(input);
|
|
164
|
+
if (!parseResult.success) {
|
|
165
|
+
throw new RpcValidationError(
|
|
166
|
+
`Invalid input for method '${method}'`,
|
|
167
|
+
parseResult.error,
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const id = this.generateId();
|
|
172
|
+
const timeoutMs = timeout ?? this.defaultTimeout;
|
|
173
|
+
|
|
174
|
+
return new Promise((resolve, reject) => {
|
|
175
|
+
const timeoutHandle = setTimeout(() => {
|
|
176
|
+
this.pendingRequests.delete(id);
|
|
177
|
+
reject(new RpcTimeoutError(method, timeoutMs));
|
|
178
|
+
}, timeoutMs);
|
|
179
|
+
|
|
180
|
+
this.pendingRequests.set(id, {
|
|
181
|
+
resolve,
|
|
182
|
+
reject,
|
|
183
|
+
timeout: timeoutHandle,
|
|
184
|
+
method,
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
this.ws.send(this.protocol.createRequest(id, method, parseResult.data));
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Emit an event to the remote peer (fire-and-forget)
|
|
193
|
+
*/
|
|
194
|
+
emit<K extends StringKeys<TLocalSchema["events"]>>(
|
|
195
|
+
event: K,
|
|
196
|
+
data: TLocalSchema["events"] extends Record<string, EventDef>
|
|
197
|
+
? InferEventData<TLocalSchema["events"][K]>
|
|
198
|
+
: never,
|
|
199
|
+
): void {
|
|
200
|
+
if (this.closed || this.ws.readyState !== 1) {
|
|
201
|
+
console.warn(`Cannot emit event '${String(event)}': connection closed`);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const eventName = event;
|
|
206
|
+
const eventDef = this.localSchema.events?.[eventName];
|
|
207
|
+
if (!eventDef) {
|
|
208
|
+
console.warn(`Unknown event '${eventName}'`);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Validate data against schema
|
|
213
|
+
const parseResult = eventDef.data.safeParse(data);
|
|
214
|
+
if (!parseResult.success) {
|
|
215
|
+
console.warn(`Invalid data for event '${eventName}':`, parseResult.error);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
this.ws.send(this.protocol.createEvent(eventName, parseResult.data));
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Handle an incoming WebSocket message
|
|
224
|
+
*
|
|
225
|
+
* Accepts string, ArrayBuffer, Uint8Array (including Node.js Buffer),
|
|
226
|
+
* or Uint8Array[] (for ws library's fragmented messages).
|
|
227
|
+
*
|
|
228
|
+
* @example
|
|
229
|
+
* ```ts
|
|
230
|
+
* // Works directly with ws library's message event
|
|
231
|
+
* ws.on("message", (data) => peer.handleMessage(data));
|
|
232
|
+
* ```
|
|
233
|
+
*/
|
|
234
|
+
handleMessage(data: WireInput): void {
|
|
235
|
+
const message = this.protocol.safeDecodeMessage(data);
|
|
236
|
+
if (!message) {
|
|
237
|
+
console.error("Failed to parse RPC message");
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
switch (message.type) {
|
|
242
|
+
case "rpc:request":
|
|
243
|
+
void this.handleRequest(message);
|
|
244
|
+
break;
|
|
245
|
+
case "rpc:response":
|
|
246
|
+
this.handleResponse(message);
|
|
247
|
+
break;
|
|
248
|
+
case "rpc:error":
|
|
249
|
+
this.handleError(message);
|
|
250
|
+
break;
|
|
251
|
+
case "rpc:event":
|
|
252
|
+
this.handleEvent(message);
|
|
253
|
+
break;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Handle an incoming RPC request
|
|
259
|
+
*/
|
|
260
|
+
private async handleRequest(request: RpcRequest): Promise<void> {
|
|
261
|
+
const { id, method, params } = request;
|
|
262
|
+
|
|
263
|
+
const methodDef = this.localSchema.methods?.[method];
|
|
264
|
+
if (!methodDef) {
|
|
265
|
+
this.sendError(
|
|
266
|
+
id,
|
|
267
|
+
RpcErrorCodes.METHOD_NOT_FOUND,
|
|
268
|
+
`Method '${method}' not found`,
|
|
269
|
+
);
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Validate input
|
|
274
|
+
const parseResult = methodDef.input.safeParse(params);
|
|
275
|
+
if (!parseResult.success) {
|
|
276
|
+
this.sendError(
|
|
277
|
+
id,
|
|
278
|
+
RpcErrorCodes.INVALID_PARAMS,
|
|
279
|
+
`Invalid params for '${method}'`,
|
|
280
|
+
parseResult.error,
|
|
281
|
+
);
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Get handler from provider
|
|
286
|
+
const handler = this.provider[method as keyof typeof this.provider] as
|
|
287
|
+
| ((input: unknown) => Promise<unknown>)
|
|
288
|
+
| undefined;
|
|
289
|
+
if (!handler) {
|
|
290
|
+
this.sendError(
|
|
291
|
+
id,
|
|
292
|
+
RpcErrorCodes.METHOD_NOT_FOUND,
|
|
293
|
+
`Method '${method}' not implemented`,
|
|
294
|
+
);
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
try {
|
|
299
|
+
// Call handler with correct `this` context
|
|
300
|
+
const result = await handler.call(this.provider, parseResult.data);
|
|
301
|
+
|
|
302
|
+
// Validate output
|
|
303
|
+
const outputResult = methodDef.output.safeParse(result);
|
|
304
|
+
if (!outputResult.success) {
|
|
305
|
+
this.sendError(
|
|
306
|
+
id,
|
|
307
|
+
RpcErrorCodes.INTERNAL_ERROR,
|
|
308
|
+
`Invalid output from '${method}'`,
|
|
309
|
+
outputResult.error,
|
|
310
|
+
);
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
this.ws.send(this.protocol.createResponse(id, outputResult.data));
|
|
315
|
+
} catch (error) {
|
|
316
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
317
|
+
this.sendError(id, RpcErrorCodes.INTERNAL_ERROR, message);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Handle an incoming RPC response
|
|
323
|
+
*/
|
|
324
|
+
private handleResponse(response: RpcResponse): void {
|
|
325
|
+
const pending = this.pendingRequests.get(response.id);
|
|
326
|
+
if (!pending) {
|
|
327
|
+
console.warn(`Received response for unknown request: ${response.id}`);
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
clearTimeout(pending.timeout);
|
|
332
|
+
this.pendingRequests.delete(response.id);
|
|
333
|
+
pending.resolve(response.result);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Handle an incoming RPC error
|
|
338
|
+
*/
|
|
339
|
+
private handleError(error: RpcError): void {
|
|
340
|
+
const pending = this.pendingRequests.get(error.id);
|
|
341
|
+
if (!pending) {
|
|
342
|
+
console.warn(`Received error for unknown request: ${error.id}`);
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
clearTimeout(pending.timeout);
|
|
347
|
+
this.pendingRequests.delete(error.id);
|
|
348
|
+
pending.reject(
|
|
349
|
+
new RpcRemoteError(pending.method, error.code, error.message, error.data),
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Handle an incoming event
|
|
355
|
+
*/
|
|
356
|
+
private handleEvent(event: RpcEvent): void {
|
|
357
|
+
if (!this.onEventHandler) {
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const eventDef = this.remoteSchema.events?.[event.event];
|
|
362
|
+
if (!eventDef) {
|
|
363
|
+
console.warn(`Unknown event: ${event.event}`);
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Validate event data
|
|
368
|
+
const parseResult = eventDef.data.safeParse(event.data);
|
|
369
|
+
if (!parseResult.success) {
|
|
370
|
+
console.warn(
|
|
371
|
+
`Invalid data for event '${event.event}':`,
|
|
372
|
+
parseResult.error,
|
|
373
|
+
);
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Cast is safe because we validated against the schema
|
|
378
|
+
(this.onEventHandler as (event: string, data: unknown) => void)(
|
|
379
|
+
event.event,
|
|
380
|
+
parseResult.data,
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Send an error response
|
|
386
|
+
*/
|
|
387
|
+
private sendError(
|
|
388
|
+
id: string,
|
|
389
|
+
code: number,
|
|
390
|
+
message: string,
|
|
391
|
+
data?: unknown,
|
|
392
|
+
): void {
|
|
393
|
+
if (this.ws.readyState !== 1) return;
|
|
394
|
+
this.ws.send(this.protocol.createError(id, code, message, data));
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Mark the peer as closed and reject all pending requests
|
|
399
|
+
*/
|
|
400
|
+
close(): void {
|
|
401
|
+
this.closed = true;
|
|
402
|
+
|
|
403
|
+
// Reject all pending requests
|
|
404
|
+
for (const [, pending] of this.pendingRequests) {
|
|
405
|
+
clearTimeout(pending.timeout);
|
|
406
|
+
pending.reject(new RpcConnectionClosed());
|
|
407
|
+
}
|
|
408
|
+
this.pendingRequests.clear();
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Check if the peer connection is open
|
|
413
|
+
*/
|
|
414
|
+
get isOpen(): boolean {
|
|
415
|
+
return !this.closed && this.ws.readyState === 1;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Get the underlying WebSocket
|
|
420
|
+
*
|
|
421
|
+
* Use for advanced scenarios like DurableRpcPeer integration.
|
|
422
|
+
*/
|
|
423
|
+
getWebSocket(): IMinWebSocket {
|
|
424
|
+
return this.ws;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Get the number of pending requests
|
|
429
|
+
*/
|
|
430
|
+
get pendingCount(): number {
|
|
431
|
+
return this.pendingRequests.size;
|
|
432
|
+
}
|
|
433
|
+
}
|