@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,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 });
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Peer Exports
3
+ */
4
+
5
+ export {
6
+ RpcPeer,
7
+ type RpcPeerOptions,
8
+ } from "./default.js";
9
+ export {
10
+ type CallContext,
11
+ DurableRpcPeer,
12
+ type DurableRpcPeerOptions,
13
+ } from "./durable.js";
@@ -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);