@openrois/sdk 0.0.0-stage → 0.1.0-alpha.1

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.
@@ -0,0 +1,534 @@
1
+ // src/emitter.ts
2
+ var TypedEmitter = class {
3
+ // No prototype, so an event name that only JavaScript callers can pass, such as
4
+ // "constructor", finds nothing.
5
+ listeners = /* @__PURE__ */ Object.create(null);
6
+ /** Add a listener. It stays until off() removes it. */
7
+ on(event, listener) {
8
+ (this.listeners[event] ??= []).push(listener);
9
+ return this;
10
+ }
11
+ /** Remove a listener added with on(). */
12
+ off(event, listener) {
13
+ const added = this.listeners[event] ?? [];
14
+ const index = added.lastIndexOf(listener);
15
+ if (index !== -1) {
16
+ added.splice(index, 1);
17
+ }
18
+ return this;
19
+ }
20
+ /**
21
+ * Call the listeners of an event. A listener that throws, or an async listener that
22
+ * rejects, is reported as a fault, because the caller is the reader of a socket and
23
+ * must carry on with the next message.
24
+ */
25
+ emit(event, ...args) {
26
+ const listeners = [...this.listeners[event] ?? []];
27
+ if (event === "error" && listeners.length === 0) {
28
+ console.error("OpenRoIS:", args[0]);
29
+ }
30
+ for (const listener of listeners) {
31
+ try {
32
+ const returned = listener(...args);
33
+ if (returned instanceof Promise) {
34
+ returned.catch((fault) => this.reportFault(event, fault));
35
+ }
36
+ } catch (fault) {
37
+ this.reportFault(event, fault);
38
+ }
39
+ }
40
+ }
41
+ /** Pass on what a listener threw. An "error" listener that throws can only be logged. */
42
+ reportFault(event, fault) {
43
+ if (event === "error") {
44
+ console.error("OpenRoIS:", fault);
45
+ return;
46
+ }
47
+ const error = fault instanceof Error ? fault : new Error(String(fault));
48
+ this.emit("error", ...[error]);
49
+ }
50
+ };
51
+
52
+ // src/jsonrpc.ts
53
+ import { z } from "zod";
54
+ import { JsonRpcErrorCode } from "@openrois/interfaces";
55
+ var JSONRPC_VERSION = "2.0";
56
+ var JsonRpcIdSchema = z.union([z.string(), z.number(), z.null()]);
57
+ var JsonRpcMethodSchema = z.string().min(3);
58
+ var JsonRpcParamsSchema = z.union([
59
+ z.record(z.string(), z.unknown()),
60
+ z.array(z.unknown())
61
+ ]);
62
+ var JsonRpcErrorObjectSchema = z.object({
63
+ /** Numeric error code. */
64
+ code: z.number().int(),
65
+ /** Short human-readable summary of the error. */
66
+ message: z.string(),
67
+ /** Optional additional data, defined by the engine. */
68
+ data: z.unknown().optional()
69
+ }).strict();
70
+ var JsonRpcRequestSchema = z.object({
71
+ jsonrpc: z.literal(JSONRPC_VERSION),
72
+ /** Unique identifier chosen by the caller and echoed back in the response. */
73
+ id: JsonRpcIdSchema,
74
+ /** Namespaced method name, for example "rois.command.search". */
75
+ method: JsonRpcMethodSchema,
76
+ /** Named parameter object for the method. Omitted when the method takes none. */
77
+ params: JsonRpcParamsSchema.optional()
78
+ }).strict();
79
+ var JsonRpcResponseSchema = z.object({
80
+ jsonrpc: z.literal(JSONRPC_VERSION),
81
+ /** Matches the `id` of the originating request. */
82
+ id: JsonRpcIdSchema,
83
+ /** Return value of the method. Its shape is method-specific. */
84
+ result: z.unknown()
85
+ }).strict();
86
+ var JsonRpcErrorSchema = z.object({
87
+ jsonrpc: z.literal(JSONRPC_VERSION),
88
+ /** Matches the originating request `id`, or null for parse-level failures. */
89
+ id: JsonRpcIdSchema,
90
+ /** Structured error payload. */
91
+ error: JsonRpcErrorObjectSchema
92
+ }).strict();
93
+ var JsonRpcNotificationSchema = z.object({
94
+ jsonrpc: z.literal(JSONRPC_VERSION),
95
+ /** Namespaced notification name, for example "rois.event.notify_event". */
96
+ method: JsonRpcMethodSchema,
97
+ /** Notification params. Their shape depends on the method. */
98
+ params: JsonRpcParamsSchema.optional()
99
+ }).strict();
100
+
101
+ // src/transport.ts
102
+ var TransportState = /* @__PURE__ */ ((TransportState2) => {
103
+ TransportState2["DISCONNECTED"] = "DISCONNECTED";
104
+ TransportState2["CONNECTING"] = "CONNECTING";
105
+ TransportState2["CONNECTED"] = "CONNECTED";
106
+ TransportState2["CLOSING"] = "CLOSING";
107
+ return TransportState2;
108
+ })(TransportState || {});
109
+ var DEFAULT_REQUEST_TIMEOUT = 3e4;
110
+ var DEFAULT_CONNECT_TIMEOUT = 1e4;
111
+ var MAX_CLOSE_REASON_BYTES = 123;
112
+ function globalWebSocketFactory(url) {
113
+ const { WebSocket } = globalThis;
114
+ if (WebSocket === void 0) {
115
+ throw new Error("This runtime has no global WebSocket. Pass a webSocketFactory.");
116
+ }
117
+ return new WebSocket(url);
118
+ }
119
+ var TransportError = class extends Error {
120
+ constructor(message) {
121
+ super(message);
122
+ this.name = "TransportError";
123
+ }
124
+ };
125
+ var ConnectionError = class extends TransportError {
126
+ /** The URL that the transport tried to connect to, if available. */
127
+ url;
128
+ constructor(message, url = "") {
129
+ super(message);
130
+ this.name = "ConnectionError";
131
+ this.url = url;
132
+ }
133
+ };
134
+ var RequestTimeoutError = class extends TransportError {
135
+ /** The JSON-RPC method that timed out. */
136
+ method;
137
+ /** The request id that timed out. */
138
+ requestId;
139
+ constructor(method, requestId, timeoutMs) {
140
+ super(
141
+ `Request timed out after ${timeoutMs}ms: ${method} (id: ${requestId})`
142
+ );
143
+ this.name = "RequestTimeoutError";
144
+ this.method = method;
145
+ this.requestId = requestId;
146
+ }
147
+ };
148
+ var RpcError = class extends TransportError {
149
+ /** The numeric JSON-RPC error code (e.g. -32601 for MethodNotFound). */
150
+ code;
151
+ /** Optional additional error data from the gateway. */
152
+ data;
153
+ constructor(errorObject) {
154
+ super(`RPC error ${errorObject.code}: ${errorObject.message}`);
155
+ this.name = "RpcError";
156
+ this.code = errorObject.code;
157
+ this.data = errorObject.data;
158
+ }
159
+ };
160
+ var WebSocketTransport = class extends TypedEmitter {
161
+ /** Current connection state. */
162
+ _state = "DISCONNECTED" /* DISCONNECTED */;
163
+ /** The underlying WebSocket instance, or null when disconnected. */
164
+ ws = null;
165
+ /** Map of request id -> pending request awaiting a response. */
166
+ pending = /* @__PURE__ */ new Map();
167
+ /** Counter for generating unique request ids within this connection. */
168
+ nextId = 1;
169
+ /** Resolved configuration with defaults applied. */
170
+ options;
171
+ /** The URL this transport is connected (or connecting) to. */
172
+ url = "";
173
+ /** Ends the connect() that is in progress, or null when none is. */
174
+ abortConnect = null;
175
+ constructor(options) {
176
+ super();
177
+ this.options = {
178
+ requestTimeout: options?.requestTimeout ?? DEFAULT_REQUEST_TIMEOUT,
179
+ connectTimeout: options?.connectTimeout ?? DEFAULT_CONNECT_TIMEOUT,
180
+ webSocketFactory: options?.webSocketFactory ?? globalWebSocketFactory
181
+ };
182
+ }
183
+ // -------------------------------------------------------------------------
184
+ // Public API
185
+ // -------------------------------------------------------------------------
186
+ /** The current connection state. */
187
+ get state() {
188
+ return this._state;
189
+ }
190
+ /**
191
+ * Open a WebSocket connection to the gateway.
192
+ *
193
+ * Resolves when the connection is established and ready for messages.
194
+ * Rejects with ConnectionError if the connection fails or times out, or if
195
+ * close() runs first.
196
+ *
197
+ * @param url - The WebSocket URL, e.g. "ws://localhost:8765" or
198
+ * "wss://gateway.example.com".
199
+ */
200
+ async connect(url) {
201
+ if (this._state !== "DISCONNECTED" /* DISCONNECTED */) {
202
+ throw new ConnectionError(
203
+ `Cannot connect: transport is in state ${this._state}`,
204
+ url
205
+ );
206
+ }
207
+ this.url = url;
208
+ this._state = "CONNECTING" /* CONNECTING */;
209
+ return new Promise((resolve, reject) => {
210
+ let socket;
211
+ try {
212
+ socket = this.options.webSocketFactory(url);
213
+ } catch (err) {
214
+ this._state = "DISCONNECTED" /* DISCONNECTED */;
215
+ const message = err instanceof Error ? err.message : String(err);
216
+ reject(new ConnectionError(
217
+ `Failed to create WebSocket: ${message}`,
218
+ url
219
+ ));
220
+ return;
221
+ }
222
+ this.ws = socket;
223
+ const fail = (message) => {
224
+ clearTimeout(connectTimer);
225
+ this.abortConnect = null;
226
+ this.cleanupSocket();
227
+ this._state = "DISCONNECTED" /* DISCONNECTED */;
228
+ reject(new ConnectionError(message, url));
229
+ };
230
+ const abort = (message) => {
231
+ fail(message);
232
+ try {
233
+ socket.close();
234
+ } catch {
235
+ }
236
+ };
237
+ const connectTimer = setTimeout(() => {
238
+ abort(`Connection timed out after ${this.options.connectTimeout}ms`);
239
+ }, this.options.connectTimeout);
240
+ this.abortConnect = abort;
241
+ socket.onopen = () => {
242
+ clearTimeout(connectTimer);
243
+ this.abortConnect = null;
244
+ this._state = "CONNECTED" /* CONNECTED */;
245
+ this.emit("open");
246
+ resolve();
247
+ };
248
+ socket.onerror = (event) => {
249
+ if (this._state === "CONNECTING" /* CONNECTING */) {
250
+ fail(`WebSocket connection failed: ${event.message ?? "unknown error"}`);
251
+ }
252
+ };
253
+ socket.onclose = (event) => {
254
+ if (this._state === "CONNECTING" /* CONNECTING */) {
255
+ fail(`WebSocket closed during connection: code ${event.code}`);
256
+ return;
257
+ }
258
+ this.handleClose(event.code, event.reason);
259
+ };
260
+ socket.onmessage = (event) => {
261
+ const raw = typeof event.data === "string" ? event.data : String(event.data);
262
+ this.handleMessage(raw);
263
+ };
264
+ });
265
+ }
266
+ /**
267
+ * Send a JSON-RPC request to the gateway and wait for the response.
268
+ *
269
+ * Returns a Promise that resolves with the `result` field from the
270
+ * success response, or rejects with:
271
+ * - RpcError: the gateway sent a JSON-RPC error response.
272
+ * - RequestTimeoutError: no response within the timeout window.
273
+ * - ConnectionError: the connection dropped while waiting.
274
+ * - TransportError: the transport is not connected.
275
+ *
276
+ * The caller is responsible for validating the returned result with the
277
+ * catalog schema for the method:
278
+ * const result = await transport.send("rois.command.search", { condition: "" });
279
+ * const parsed = RoISMethodSchemas["rois.command.search"].result.parse(result);
280
+ *
281
+ * @param method - The JSON-RPC method name, e.g. "rois.command.search".
282
+ * @param params - Optional named parameters for the method.
283
+ * @returns The `result` field from the gateway's success response.
284
+ */
285
+ async send(method, params) {
286
+ if (this._state !== "CONNECTED" /* CONNECTED */ || !this.ws) {
287
+ throw new TransportError(
288
+ `Cannot send: transport is in state ${this._state}`
289
+ );
290
+ }
291
+ const id = this.generateId();
292
+ const request = {
293
+ jsonrpc: JSONRPC_VERSION,
294
+ id,
295
+ method,
296
+ ...params !== void 0 ? { params } : {}
297
+ };
298
+ const json = JSON.stringify(request);
299
+ this.ws.send(json);
300
+ return new Promise((resolve, reject) => {
301
+ const timer = setTimeout(() => {
302
+ this.pending.delete(id);
303
+ reject(new RequestTimeoutError(method, id, this.options.requestTimeout));
304
+ }, this.options.requestTimeout);
305
+ this.pending.set(id, { resolve, reject, method, timer });
306
+ });
307
+ }
308
+ /**
309
+ * Close the WebSocket connection.
310
+ *
311
+ * Any pending requests are rejected with a ConnectionError.
312
+ * Safe to call when already disconnected (no-op).
313
+ *
314
+ * During connect(), this ends the attempt instead: connect() rejects with a
315
+ * ConnectionError and no "close" event fires, because no connection opened.
316
+ *
317
+ * @param code - WebSocket close code (default: 1000 for normal closure): 1000, or
318
+ * 3000 to 4999, which is what the WebSocket API lets a client send.
319
+ * @param reason - Human-readable close reason, at most 123 bytes of UTF-8.
320
+ * @throws TransportError for any other code or reason, in every state, and a
321
+ * connection stays open.
322
+ */
323
+ close(code = 1e3, reason = "client closed") {
324
+ if (code !== 1e3 && !(code >= 3e3 && code <= 4999)) {
325
+ throw new TransportError(`Cannot close with code ${code}: use 1000, or 3000 to 4999`);
326
+ }
327
+ if (new TextEncoder().encode(reason).length > MAX_CLOSE_REASON_BYTES) {
328
+ throw new TransportError(
329
+ `Cannot close with this reason: it is over ${MAX_CLOSE_REASON_BYTES} bytes of UTF-8`
330
+ );
331
+ }
332
+ if (this._state === "DISCONNECTED" /* DISCONNECTED */ || !this.ws) {
333
+ return;
334
+ }
335
+ if (this.abortConnect) {
336
+ this.abortConnect("Connection closed by client");
337
+ return;
338
+ }
339
+ this.ws.close(code, reason);
340
+ this._state = "CLOSING" /* CLOSING */;
341
+ this.rejectAllPending(
342
+ new ConnectionError("Connection closed by client", this.url)
343
+ );
344
+ this.cleanupSocket();
345
+ this._state = "DISCONNECTED" /* DISCONNECTED */;
346
+ this.emit("close", code, reason);
347
+ }
348
+ // -------------------------------------------------------------------------
349
+ // Private: message handling
350
+ // -------------------------------------------------------------------------
351
+ /**
352
+ * Process a raw message string from the WebSocket.
353
+ *
354
+ * Parses the JSON, classifies the message type, and dispatches to the
355
+ * appropriate handler (response correlation or notification routing).
356
+ */
357
+ handleMessage(raw) {
358
+ let parsed;
359
+ try {
360
+ parsed = JSON.parse(raw);
361
+ } catch {
362
+ this.emit("error", new TransportError(
363
+ `Received malformed JSON from gateway: ${raw.slice(0, 200)}`
364
+ ));
365
+ return;
366
+ }
367
+ if (typeof parsed !== "object" || parsed === null) {
368
+ this.emit("error", new TransportError(
369
+ "Received non-object JSON from gateway"
370
+ ));
371
+ return;
372
+ }
373
+ const msg = parsed;
374
+ if ("id" in msg && "result" in msg) {
375
+ this.handleResponse(msg);
376
+ return;
377
+ }
378
+ if ("id" in msg && "error" in msg) {
379
+ this.handleErrorResponse(msg);
380
+ return;
381
+ }
382
+ if ("method" in msg && !("id" in msg)) {
383
+ this.handleNotification(msg);
384
+ return;
385
+ }
386
+ this.emit("error", new TransportError(
387
+ `Received unrecognized message shape from gateway: ${raw.slice(0, 200)}`
388
+ ));
389
+ }
390
+ /**
391
+ * Handle a success response by resolving the matching pending request.
392
+ */
393
+ handleResponse(msg) {
394
+ const parseResult = JsonRpcResponseSchema.safeParse(msg);
395
+ if (!parseResult.success) {
396
+ this.emit("error", new TransportError(
397
+ `Received invalid success response: ${parseResult.error.message}`
398
+ ));
399
+ return;
400
+ }
401
+ const response = parseResult.data;
402
+ const id = String(response.id);
403
+ const pendingRequest = this.pending.get(id);
404
+ if (!pendingRequest) {
405
+ this.emit("error", new TransportError(
406
+ `Received response for unknown request id: ${id}`
407
+ ));
408
+ return;
409
+ }
410
+ clearTimeout(pendingRequest.timer);
411
+ this.pending.delete(id);
412
+ pendingRequest.resolve(response.result);
413
+ }
414
+ /**
415
+ * Handle an error response by rejecting the matching pending request.
416
+ */
417
+ handleErrorResponse(msg) {
418
+ const parseResult = JsonRpcErrorSchema.safeParse(msg);
419
+ if (!parseResult.success) {
420
+ this.emit("error", new TransportError(
421
+ `Received invalid error response: ${parseResult.error.message}`
422
+ ));
423
+ return;
424
+ }
425
+ const errorResponse = parseResult.data;
426
+ const id = String(errorResponse.id);
427
+ const pendingRequest = this.pending.get(id);
428
+ if (!pendingRequest) {
429
+ this.emit("error", new TransportError(
430
+ `Received error response for unknown request id: ${id}`
431
+ ));
432
+ return;
433
+ }
434
+ clearTimeout(pendingRequest.timer);
435
+ this.pending.delete(id);
436
+ pendingRequest.reject(new RpcError(errorResponse.error));
437
+ }
438
+ /**
439
+ * Handle a notification by emitting it as an event. It goes out only as
440
+ * "notification", never under its method name, because a server could then
441
+ * fake the "close" event by naming a notification after it.
442
+ */
443
+ handleNotification(msg) {
444
+ const parseResult = JsonRpcNotificationSchema.safeParse(msg);
445
+ if (!parseResult.success) {
446
+ this.emit("error", new TransportError(
447
+ `Received invalid notification: ${parseResult.error.message}`
448
+ ));
449
+ return;
450
+ }
451
+ this.emit("notification", parseResult.data);
452
+ }
453
+ // -------------------------------------------------------------------------
454
+ // Private: connection lifecycle
455
+ // -------------------------------------------------------------------------
456
+ /**
457
+ * Handle WebSocket close after the connection was established.
458
+ *
459
+ * If the close was not initiated by the client (i.e. the state is still
460
+ * CONNECTED), this is an unexpected disconnect. All pending requests are
461
+ * rejected.
462
+ */
463
+ handleClose(code, reason) {
464
+ const wasConnected = this._state === "CONNECTED" /* CONNECTED */;
465
+ this.rejectAllPending(
466
+ new ConnectionError(
467
+ `WebSocket closed unexpectedly: code ${code}, reason: ${reason || "none"}`,
468
+ this.url
469
+ )
470
+ );
471
+ this.cleanupSocket();
472
+ this._state = "DISCONNECTED" /* DISCONNECTED */;
473
+ if (wasConnected) {
474
+ this.emit("close", code, reason);
475
+ }
476
+ }
477
+ /**
478
+ * Reject all pending requests with the given error.
479
+ *
480
+ * Called on disconnect (expected or unexpected) so no Promise is left
481
+ * hanging forever.
482
+ */
483
+ rejectAllPending(error) {
484
+ for (const pending of this.pending.values()) {
485
+ clearTimeout(pending.timer);
486
+ pending.reject(error);
487
+ }
488
+ this.pending.clear();
489
+ }
490
+ /**
491
+ * Null out the WebSocket reference and remove event handlers.
492
+ *
493
+ * Prevents stale handlers from firing after the transport considers
494
+ * itself disconnected. The error handler stays as a no-op, because the `ws`
495
+ * package throws a late socket error that has no handler, and it raises one
496
+ * when a socket closes before its handshake ends.
497
+ */
498
+ cleanupSocket() {
499
+ if (this.ws) {
500
+ this.ws.onopen = null;
501
+ this.ws.onclose = null;
502
+ this.ws.onmessage = null;
503
+ this.ws.onerror = () => {
504
+ };
505
+ this.ws = null;
506
+ }
507
+ }
508
+ // -------------------------------------------------------------------------
509
+ // Private: utilities
510
+ // -------------------------------------------------------------------------
511
+ /**
512
+ * Generate a unique request id.
513
+ *
514
+ * Uses a simple incrementing counter prefixed with "req-". Ids are unique
515
+ * within a single transport instance. The counter resets if you create a
516
+ * new transport.
517
+ */
518
+ generateId() {
519
+ const id = `req-${this.nextId}`;
520
+ this.nextId += 1;
521
+ return id;
522
+ }
523
+ };
524
+
525
+ export {
526
+ TypedEmitter,
527
+ TransportState,
528
+ TransportError,
529
+ ConnectionError,
530
+ RequestTimeoutError,
531
+ RpcError,
532
+ WebSocketTransport
533
+ };
534
+ //# sourceMappingURL=chunk-AEO3U7Y7.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/emitter.ts","../src/jsonrpc.ts","../src/transport.ts"],"sourcesContent":["/**\n * A small typed event emitter for RoISClient and WebSocketTransport.\n *\n * Node's EventEmitter would need the `events` package in browsers, its types need\n * @types/node, and it throws on an \"error\" event with no listener, so one bad message\n * from a server would end the process. Here a fault is an event that nothing can turn\n * into a throw: it goes to the \"error\" listeners, or to the console if there are none.\n */\n\n/** The events of an emitter: each name maps to its listener's arguments. */\nexport type EmitterEvents<Events> = Record<keyof Events, unknown[]> & {\n error: [error: Error];\n};\n\n/** Base class of whatever emits events. Only the class itself can emit. */\nexport class TypedEmitter<Events extends EmitterEvents<Events>> {\n // No prototype, so an event name that only JavaScript callers can pass, such as\n // \"constructor\", finds nothing.\n private readonly listeners: { [E in keyof Events]?: ((...args: Events[E]) => void)[] } =\n Object.create(null);\n\n /** Add a listener. It stays until off() removes it. */\n on<E extends keyof Events>(event: E, listener: (...args: Events[E]) => void): this {\n (this.listeners[event] ??= []).push(listener);\n return this;\n }\n\n /** Remove a listener added with on(). */\n off<E extends keyof Events>(event: E, listener: (...args: Events[E]) => void): this {\n const added = this.listeners[event] ?? [];\n const index = added.lastIndexOf(listener);\n if (index !== -1) {\n added.splice(index, 1);\n }\n return this;\n }\n\n /**\n * Call the listeners of an event. A listener that throws, or an async listener that\n * rejects, is reported as a fault, because the caller is the reader of a socket and\n * must carry on with the next message.\n */\n protected emit<E extends keyof Events>(event: E, ...args: Events[E]): void {\n // A copy, so a listener can remove itself without skipping the next one.\n const listeners = [...(this.listeners[event] ?? [])];\n if (event === \"error\" && listeners.length === 0) {\n console.error(\"OpenRoIS:\", args[0]);\n }\n for (const listener of listeners) {\n try {\n const returned: unknown = listener(...args);\n if (returned instanceof Promise) {\n returned.catch((fault: unknown) => this.reportFault(event, fault));\n }\n } catch (fault) {\n this.reportFault(event, fault);\n }\n }\n }\n\n /** Pass on what a listener threw. An \"error\" listener that throws can only be logged. */\n private reportFault(event: keyof Events, fault: unknown): void {\n if (event === \"error\") {\n console.error(\"OpenRoIS:\", fault);\n return;\n }\n const error = fault instanceof Error ? fault : new Error(String(fault));\n this.emit(\"error\", ...([error] as unknown as Events[\"error\"]));\n }\n}\n","/**\n * JSON-RPC 2.0 envelopes for the OpenRoIS SDK.\n *\n * Defines the four message types exchanged with an engine over WebSocket:\n *\n * JsonRpcRequest - A call that expects a reply (has `id`)\n * JsonRpcResponse - A successful reply to a request\n * JsonRpcError - A failed reply to a request\n * JsonRpcNotification - A push with no reply expected (no `id`)\n *\n * This module knows nothing about RoIS. The method names, the params and result\n * of each method, the notifications and the JSON-RPC error codes come from the\n * method catalog in @openrois/interfaces (RoISMethods, RoISMethodSchemas,\n * RoISNotifications, JsonRpcErrorCode). Section 17 of docs/rois-reference.md\n * describes the binding.\n *\n * A RoIS failure is a normal JsonRpcResponse whose result has a return_code\n * other than OK. A JsonRpcError only reports a protocol fault: unparseable\n * JSON, an invalid request, an unknown method, params that fail validation, or\n * an internal error.\n *\n * Source: JSON-RPC 2.0 Specification (https://www.jsonrpc.org/specification)\n */\n\nimport { z } from \"zod\";\n\nexport { JsonRpcErrorCode } from \"@openrois/interfaces\";\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\n/** The only valid JSON-RPC version string per the spec. */\nexport const JSONRPC_VERSION = \"2.0\" as const;\n\n// ---------------------------------------------------------------------------\n// Shared primitives\n// ---------------------------------------------------------------------------\n\n/**\n * Valid JSON-RPC request/response ID.\n *\n * The spec allows string, number, or null. Null is reserved for parse-error\n * responses where the original id could not be determined.\n */\nexport const JsonRpcIdSchema = z.union([z.string(), z.number(), z.null()]);\nexport type JsonRpcId = z.infer<typeof JsonRpcIdSchema>;\n\n/**\n * Method name, for example \"rois.command.search\".\n *\n * Names beginning with \"rois.\" are reserved for OpenRoIS operations.\n * Names beginning with \"rpc.\" are reserved by the JSON-RPC spec itself.\n */\nexport const JsonRpcMethodSchema = z.string().min(3);\nexport type JsonRpcMethod = z.infer<typeof JsonRpcMethodSchema>;\n\n/**\n * Params field: named params (an object) or positional params (an array).\n *\n * OpenRoIS always sends named params, shaped by the catalog's params model for\n * the method. Arrays are accepted for spec compliance but are not sent.\n */\nexport const JsonRpcParamsSchema = z.union([\n z.record(z.string(), z.unknown()),\n z.array(z.unknown()),\n]);\nexport type JsonRpcParams = z.infer<typeof JsonRpcParamsSchema>;\n\n// ---------------------------------------------------------------------------\n// Error object (used inside JsonRpcError)\n// ---------------------------------------------------------------------------\n\n/**\n * The error object carried inside a JsonRpcError message.\n *\n * Maps to the Error Object defined in section 5.1 of the JSON-RPC 2.0 spec.\n * The standard codes are in JsonRpcErrorCode. Codes from -32000 to -32099 are\n * left to the engine.\n */\nexport const JsonRpcErrorObjectSchema = z.object({\n /** Numeric error code. */\n code: z.number().int(),\n /** Short human-readable summary of the error. */\n message: z.string(),\n /** Optional additional data, defined by the engine. */\n data: z.unknown().optional(),\n}).strict();\nexport type JsonRpcErrorObject = z.infer<typeof JsonRpcErrorObjectSchema>;\n\n// ---------------------------------------------------------------------------\n// The four message envelopes\n// ---------------------------------------------------------------------------\n\n/**\n * A JSON-RPC 2.0 Request. Always carries an `id`, which the reply echoes.\n *\n * Example:\n * {\n * \"jsonrpc\": \"2.0\",\n * \"id\": \"req-2\",\n * \"method\": \"rois.query.query\",\n * \"params\": {\n * \"query_type\": \"component_status\",\n * \"condition\": \"component_ref = 'reachy_real/head'\"\n * }\n * }\n */\nexport const JsonRpcRequestSchema = z.object({\n jsonrpc: z.literal(JSONRPC_VERSION),\n /** Unique identifier chosen by the caller and echoed back in the response. */\n id: JsonRpcIdSchema,\n /** Namespaced method name, for example \"rois.command.search\". */\n method: JsonRpcMethodSchema,\n /** Named parameter object for the method. Omitted when the method takes none. */\n params: JsonRpcParamsSchema.optional(),\n}).strict();\nexport type JsonRpcRequest = z.infer<typeof JsonRpcRequestSchema>;\n\n/**\n * A JSON-RPC 2.0 Success Response. The `result` is the catalog's result model\n * for the method, validated by the caller.\n *\n * Example:\n * {\n * \"jsonrpc\": \"2.0\",\n * \"id\": \"req-1\",\n * \"result\": {\n * \"return_code\": \"OK\",\n * \"component_ref_list\": [\"reachy_real/head\", \"reachy_sim/head\"]\n * }\n * }\n */\nexport const JsonRpcResponseSchema = z.object({\n jsonrpc: z.literal(JSONRPC_VERSION),\n /** Matches the `id` of the originating request. */\n id: JsonRpcIdSchema,\n /** Return value of the method. Its shape is method-specific. */\n result: z.unknown(),\n}).strict();\nexport type JsonRpcResponse = z.infer<typeof JsonRpcResponseSchema>;\n\n/**\n * A JSON-RPC 2.0 Error Response, for protocol faults only. The `id` matches the\n * originating request, or is null if the request could not be parsed at all.\n *\n * Example:\n * {\n * \"jsonrpc\": \"2.0\",\n * \"id\": \"req-5\",\n * \"error\": { \"code\": -32601, \"message\": \"Method not found: rois.stream.connect_stream\" }\n * }\n */\nexport const JsonRpcErrorSchema = z.object({\n jsonrpc: z.literal(JSONRPC_VERSION),\n /** Matches the originating request `id`, or null for parse-level failures. */\n id: JsonRpcIdSchema,\n /** Structured error payload. */\n error: JsonRpcErrorObjectSchema,\n}).strict();\nexport type JsonRpcError = z.infer<typeof JsonRpcErrorSchema>;\n\n/**\n * A JSON-RPC 2.0 Notification: a push from the engine with no `id`, so the\n * client never replies. The `params` are the catalog's params model for the\n * notification.\n *\n * Example:\n * {\n * \"jsonrpc\": \"2.0\",\n * \"method\": \"rois.command.completed\",\n * \"params\": { \"command_id\": \"3f2a9c1e-5b7d-4e2a-9f10-6c8d2b4a7e01\", \"status\": \"OK\" }\n * }\n */\nexport const JsonRpcNotificationSchema = z.object({\n jsonrpc: z.literal(JSONRPC_VERSION),\n /** Namespaced notification name, for example \"rois.event.notify_event\". */\n method: JsonRpcMethodSchema,\n /** Notification params. Their shape depends on the method. */\n params: JsonRpcParamsSchema.optional(),\n}).strict();\nexport type JsonRpcNotification = z.infer<typeof JsonRpcNotificationSchema>;\n\n// ---------------------------------------------------------------------------\n// Union of every message\n// ---------------------------------------------------------------------------\n\n/**\n * Any JSON-RPC 2.0 message. The transport classifies an incoming message by\n * its fields: `id` and `result` is a response, `id` and `error` is an error,\n * and `method` without `id` is a notification.\n */\nexport type JsonRpcMessage =\n | JsonRpcRequest\n | JsonRpcResponse\n | JsonRpcError\n | JsonRpcNotification;\n","/**\n * WebSocket transport layer for the OpenRoIS SDK.\n *\n * Carries the JSON-RPC messages between RoISClient and an engine.\n * Responsibilities:\n *\n * - Establish and manage a WebSocket connection (browser or Node.js).\n * - Serialize outgoing JSON-RPC requests and deserialize incoming messages.\n * - Correlate responses to pending requests by matching the `id` field.\n * - Route server-push notifications to listeners.\n * - Handle connection errors, timeouts, and unexpected disconnects.\n *\n * The transport does NOT validate the inner payload of `params` or `result`.\n * RoISClient does that with the catalog schemas from @openrois/interfaces\n * (RoISMethodSchemas and RoISNotificationSchemas).\n *\n * Usage (both Node.js and Browser):\n * const transport = new WebSocketTransport();\n * await transport.connect(\"ws://localhost:8765\");\n * const result = await transport.send(\"rois.command.search\", { condition: \"\" });\n * transport.close();\n *\n * The transport uses the global WebSocket, which browsers and Node.js 22 both\n * have. TransportOptions.webSocketFactory replaces it, for example to pass\n * headers with the `ws` package.\n *\n * Architecture: docs/architecture.md section 5 (Client & Service Application SDK)\n */\n\nimport { TypedEmitter } from \"./emitter\";\nimport {\n JsonRpcResponseSchema,\n JsonRpcErrorSchema,\n JsonRpcNotificationSchema,\n JSONRPC_VERSION,\n type JsonRpcRequest,\n type JsonRpcErrorObject,\n type JsonRpcNotification,\n} from \"./jsonrpc\";\n\n// ---------------------------------------------------------------------------\n// WebSocket abstraction\n// ---------------------------------------------------------------------------\n\n/**\n * Minimal WebSocket interface satisfied by the global WebSocket of browsers and\n * Node.js, and by the `ws` package.\n *\n * We use the property-based event handler style (onopen, onmessage, etc.)\n * because all of them support it identically. The transport assigns these\n * handlers after creating the socket.\n */\nexport interface WebSocketLike {\n send(data: string): void;\n close(code?: number, reason?: string): void;\n readonly readyState: number;\n onopen: ((event: { type: string }) => void) | null;\n onclose: ((event: { code: number; reason: string; type: string }) => void) | null;\n onmessage: ((event: { data: unknown; type: string }) => void) | null;\n onerror: ((event: { error?: unknown; message?: string; type: string }) => void) | null;\n}\n\n// ---------------------------------------------------------------------------\n// Transport state\n// ---------------------------------------------------------------------------\n\n/**\n * Connection lifecycle states for the transport.\n *\n * State transitions:\n * DISCONNECTED -> CONNECTING -> CONNECTED -> CLOSING -> DISCONNECTED\n * -> DISCONNECTED (unexpected close)\n */\nexport enum TransportState {\n /** No active connection. Initial state and state after close. */\n DISCONNECTED = \"DISCONNECTED\",\n /** connect() has been called, and the WebSocket has not opened yet. */\n CONNECTING = \"CONNECTING\",\n /** WebSocket is open and ready to send/receive messages. */\n CONNECTED = \"CONNECTED\",\n /** close() has been called, and the WebSocket has not finished closing yet. */\n CLOSING = \"CLOSING\",\n}\n\n// ---------------------------------------------------------------------------\n// Configuration\n// ---------------------------------------------------------------------------\n\n/**\n * Options for configuring the WebSocket transport.\n */\nexport interface TransportOptions {\n /**\n * Maximum time in milliseconds to wait for a response to a request.\n * If the gateway does not reply within this window, the Promise returned\n * by send() rejects with a RequestTimeoutError. Default: 30000 (30s).\n */\n requestTimeout?: number;\n\n /**\n * Maximum time in milliseconds to wait for the WebSocket connection to\n * open during connect(). Default: 10000 (10s).\n */\n connectTimeout?: number;\n\n /**\n * Optional factory function that creates a WebSocket instance for a given URL.\n *\n * By default, the transport uses the global WebSocket. Override this if you\n * need custom WebSocket configuration (headers, protocols, proxy settings,\n * etc.) or run somewhere that has no global WebSocket.\n *\n * Example (Node.js, `ws` package):\n * webSocketFactory: (url) =>\n * new WebSocket(url, { perMessageDeflate: false }) as unknown as WebSocketLike\n */\n webSocketFactory?: (url: string) => WebSocketLike;\n}\n\n/** Resolved options with defaults applied. */\ninterface ResolvedOptions {\n requestTimeout: number;\n connectTimeout: number;\n webSocketFactory: (url: string) => WebSocketLike;\n}\n\n/** Default timeout for request/response round-trips (30 seconds). */\nconst DEFAULT_REQUEST_TIMEOUT = 30_000;\n\n/** Default timeout for the initial WebSocket connection (10 seconds). */\nconst DEFAULT_CONNECT_TIMEOUT = 10_000;\n\n/** The most bytes of UTF-8 that a close frame has room for as its reason. */\nconst MAX_CLOSE_REASON_BYTES = 123;\n\n/** Open a socket with the global WebSocket of the runtime. */\nfunction globalWebSocketFactory(url: string): WebSocketLike {\n // Read through a narrow type so that the published types need no DOM library.\n const { WebSocket } = globalThis as { WebSocket?: new (url: string) => WebSocketLike };\n if (WebSocket === undefined) {\n throw new Error(\"This runtime has no global WebSocket. Pass a webSocketFactory.\");\n }\n return new WebSocket(url);\n}\n\n// ---------------------------------------------------------------------------\n// Error classes\n// ---------------------------------------------------------------------------\n\n/**\n * Base error for all transport-level failures.\n *\n * Callers can catch TransportError to handle any transport problem, or catch\n * a specific subclass for finer control.\n */\nexport class TransportError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"TransportError\";\n }\n}\n\n/**\n * Raised when the WebSocket connection cannot be established or is lost\n * unexpectedly.\n */\nexport class ConnectionError extends TransportError {\n /** The URL that the transport tried to connect to, if available. */\n readonly url: string;\n\n constructor(message: string, url: string = \"\") {\n super(message);\n this.name = \"ConnectionError\";\n this.url = url;\n }\n}\n\n/**\n * Raised when a send() call does not receive a response within the\n * configured timeout window.\n */\nexport class RequestTimeoutError extends TransportError {\n /** The JSON-RPC method that timed out. */\n readonly method: string;\n /** The request id that timed out. */\n readonly requestId: string;\n\n constructor(method: string, requestId: string, timeoutMs: number) {\n super(\n `Request timed out after ${timeoutMs}ms: ${method} (id: ${requestId})`\n );\n this.name = \"RequestTimeoutError\";\n this.method = method;\n this.requestId = requestId;\n }\n}\n\n/**\n * Raised when the engine responds with a JSON-RPC error object.\n *\n * This reports a protocol fault: unparseable JSON, an invalid request, an\n * unknown method, params that fail validation, or an internal error. A RoIS\n * failure is not an RpcError: it is a normal result with a return_code other\n * than OK, which RoISClient raises as a RoISError.\n */\nexport class RpcError extends TransportError {\n /** The numeric JSON-RPC error code (e.g. -32601 for MethodNotFound). */\n readonly code: number;\n /** Optional additional error data from the gateway. */\n readonly data: unknown;\n\n constructor(errorObject: JsonRpcErrorObject) {\n super(`RPC error ${errorObject.code}: ${errorObject.message}`);\n this.name = \"RpcError\";\n this.code = errorObject.code;\n this.data = errorObject.data;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Internal types\n// ---------------------------------------------------------------------------\n\n/**\n * Tracks a single in-flight request waiting for a response.\n *\n * When a response (or error) with a matching `id` arrives, the transport\n * calls resolve or reject and clears the timeout timer.\n */\ninterface PendingRequest {\n resolve: (result: unknown) => void;\n reject: (error: Error) => void;\n method: string;\n timer: ReturnType<typeof setTimeout>;\n}\n\n// ---------------------------------------------------------------------------\n// Transport events\n// ---------------------------------------------------------------------------\n\n/**\n * The events of WebSocketTransport, with their listener arguments.\n *\n * \"open\" - The WebSocket connection opened successfully.\n * \"notification\" - A JSON-RPC notification arrived from the gateway, with any\n * method, in or outside the catalog.\n * \"close\" - The WebSocket connection closed.\n * \"error\" - A message from the gateway could not be used.\n */\nexport type TransportEvents = {\n open: [];\n notification: [notification: JsonRpcNotification];\n close: [code: number, reason: string];\n error: [error: Error];\n};\n\n// ---------------------------------------------------------------------------\n// WebSocketTransport\n// ---------------------------------------------------------------------------\n\n/**\n * Manages a single WebSocket connection to the OpenRoIS gateway.\n *\n * The transport handles the JSON-RPC 2.0 wire protocol: serialization,\n * deserialization, request/response correlation, notification routing,\n * and error handling. RoISClient calls transport.send() and listens to the\n * transport events.\n *\n * One transport instance manages one connection. To reconnect, close the\n * existing connection and call connect() again.\n */\nexport class WebSocketTransport extends TypedEmitter<TransportEvents> {\n /** Current connection state. */\n private _state: TransportState = TransportState.DISCONNECTED;\n\n /** The underlying WebSocket instance, or null when disconnected. */\n private ws: WebSocketLike | null = null;\n\n /** Map of request id -> pending request awaiting a response. */\n private pending: Map<string, PendingRequest> = new Map();\n\n /** Counter for generating unique request ids within this connection. */\n private nextId: number = 1;\n\n /** Resolved configuration with defaults applied. */\n private options: ResolvedOptions;\n\n /** The URL this transport is connected (or connecting) to. */\n private url: string = \"\";\n\n /** Ends the connect() that is in progress, or null when none is. */\n private abortConnect: ((message: string) => void) | null = null;\n\n constructor(options?: TransportOptions) {\n super();\n this.options = {\n requestTimeout: options?.requestTimeout ?? DEFAULT_REQUEST_TIMEOUT,\n connectTimeout: options?.connectTimeout ?? DEFAULT_CONNECT_TIMEOUT,\n webSocketFactory: options?.webSocketFactory ?? globalWebSocketFactory,\n };\n }\n\n // -------------------------------------------------------------------------\n // Public API\n // -------------------------------------------------------------------------\n\n /** The current connection state. */\n get state(): TransportState {\n return this._state;\n }\n\n /**\n * Open a WebSocket connection to the gateway.\n *\n * Resolves when the connection is established and ready for messages.\n * Rejects with ConnectionError if the connection fails or times out, or if\n * close() runs first.\n *\n * @param url - The WebSocket URL, e.g. \"ws://localhost:8765\" or\n * \"wss://gateway.example.com\".\n */\n async connect(url: string): Promise<void> {\n // Guard: do not allow connecting when already connected or connecting.\n if (this._state !== TransportState.DISCONNECTED) {\n throw new ConnectionError(\n `Cannot connect: transport is in state ${this._state}`,\n url,\n );\n }\n\n this.url = url;\n this._state = TransportState.CONNECTING;\n\n return new Promise<void>((resolve, reject) => {\n let socket: WebSocketLike;\n try {\n socket = this.options.webSocketFactory(url);\n } catch (err) {\n this._state = TransportState.DISCONNECTED;\n const message = err instanceof Error ? err.message : String(err);\n reject(new ConnectionError(\n `Failed to create WebSocket: ${message}`,\n url,\n ));\n return;\n }\n this.ws = socket;\n\n // Every way out of the attempt stops its timer, so the timer can never act on a\n // connection that a later connect() opened.\n const fail = (message: string): void => {\n clearTimeout(connectTimer);\n this.abortConnect = null;\n this.cleanupSocket();\n this._state = TransportState.DISCONNECTED;\n reject(new ConnectionError(message, url));\n };\n\n // Close it as well, or the handshake can still finish after the caller gave up,\n // and leave an open connection that nothing holds.\n const abort = (message: string): void => {\n fail(message);\n try {\n socket.close();\n } catch {\n // Ignore errors from closing a socket that is failing on its own.\n }\n };\n\n // Start the connection timeout timer.\n const connectTimer = setTimeout(() => {\n abort(`Connection timed out after ${this.options.connectTimeout}ms`);\n }, this.options.connectTimeout);\n this.abortConnect = abort;\n\n // Handle successful connection.\n socket.onopen = () => {\n clearTimeout(connectTimer);\n this.abortConnect = null;\n this._state = TransportState.CONNECTED;\n this.emit(\"open\");\n resolve();\n };\n\n // Handle connection failure (fires before onopen). After the connection\n // opened, a socket error says nothing that the close event after it does not.\n socket.onerror = (event) => {\n if (this._state === TransportState.CONNECTING) {\n fail(`WebSocket connection failed: ${event.message ?? \"unknown error\"}`);\n }\n };\n\n // Handle unexpected close during connection attempt.\n socket.onclose = (event) => {\n if (this._state === TransportState.CONNECTING) {\n fail(`WebSocket closed during connection: code ${event.code}`);\n return;\n }\n this.handleClose(event.code, event.reason);\n };\n\n // Wire up the message handler.\n socket.onmessage = (event) => {\n // Every implementation delivers the payload as event.data. In the\n // `ws` package a binary frame arrives as a Buffer, so we convert it.\n const raw = typeof event.data === \"string\"\n ? event.data\n : String(event.data);\n this.handleMessage(raw);\n };\n });\n }\n\n /**\n * Send a JSON-RPC request to the gateway and wait for the response.\n *\n * Returns a Promise that resolves with the `result` field from the\n * success response, or rejects with:\n * - RpcError: the gateway sent a JSON-RPC error response.\n * - RequestTimeoutError: no response within the timeout window.\n * - ConnectionError: the connection dropped while waiting.\n * - TransportError: the transport is not connected.\n *\n * The caller is responsible for validating the returned result with the\n * catalog schema for the method:\n * const result = await transport.send(\"rois.command.search\", { condition: \"\" });\n * const parsed = RoISMethodSchemas[\"rois.command.search\"].result.parse(result);\n *\n * @param method - The JSON-RPC method name, e.g. \"rois.command.search\".\n * @param params - Optional named parameters for the method.\n * @returns The `result` field from the gateway's success response.\n */\n async send(method: string, params?: Record<string, unknown>): Promise<unknown> {\n // Guard: must be connected to send.\n if (this._state !== TransportState.CONNECTED || !this.ws) {\n throw new TransportError(\n `Cannot send: transport is in state ${this._state}`\n );\n }\n\n const id = this.generateId();\n\n // Build the JSON-RPC request envelope.\n const request: JsonRpcRequest = {\n jsonrpc: JSONRPC_VERSION,\n id,\n method,\n ...(params !== undefined ? { params } : {}),\n };\n\n // Serialize and send over the wire.\n const json = JSON.stringify(request);\n this.ws.send(json);\n\n // Return a Promise that will be resolved or rejected when the\n // matching response arrives (or the timeout fires).\n return new Promise<unknown>((resolve, reject) => {\n // Start the request timeout timer.\n const timer = setTimeout(() => {\n this.pending.delete(id);\n reject(new RequestTimeoutError(method, id, this.options.requestTimeout));\n }, this.options.requestTimeout);\n\n // Store the pending request so handleMessage can resolve it.\n this.pending.set(id, { resolve, reject, method, timer });\n });\n }\n\n /**\n * Close the WebSocket connection.\n *\n * Any pending requests are rejected with a ConnectionError.\n * Safe to call when already disconnected (no-op).\n *\n * During connect(), this ends the attempt instead: connect() rejects with a\n * ConnectionError and no \"close\" event fires, because no connection opened.\n *\n * @param code - WebSocket close code (default: 1000 for normal closure): 1000, or\n * 3000 to 4999, which is what the WebSocket API lets a client send.\n * @param reason - Human-readable close reason, at most 123 bytes of UTF-8.\n * @throws TransportError for any other code or reason, in every state, and a\n * connection stays open.\n */\n close(code: number = 1000, reason: string = \"client closed\"): void {\n // Checked here for every kind of socket. The global WebSocket throws on these, but\n // the `ws` package can throw halfway, with a socket that neither works nor closes.\n if (code !== 1000 && !(code >= 3000 && code <= 4999)) {\n throw new TransportError(`Cannot close with code ${code}: use 1000, or 3000 to 4999`);\n }\n if (new TextEncoder().encode(reason).length > MAX_CLOSE_REASON_BYTES) {\n throw new TransportError(\n `Cannot close with this reason: it is over ${MAX_CLOSE_REASON_BYTES} bytes of UTF-8`\n );\n }\n\n if (this._state === TransportState.DISCONNECTED || !this.ws) {\n return;\n }\n\n if (this.abortConnect) {\n this.abortConnect(\"Connection closed by client\");\n return;\n }\n\n // First, so that a socket that throws leaves the transport as it was, and nothing\n // reports a close that never happened.\n this.ws.close(code, reason);\n\n this._state = TransportState.CLOSING;\n this.rejectAllPending(\n new ConnectionError(\"Connection closed by client\", this.url)\n );\n\n this.cleanupSocket();\n this._state = TransportState.DISCONNECTED;\n this.emit(\"close\", code, reason);\n }\n\n // -------------------------------------------------------------------------\n // Private: message handling\n // -------------------------------------------------------------------------\n\n /**\n * Process a raw message string from the WebSocket.\n *\n * Parses the JSON, classifies the message type, and dispatches to the\n * appropriate handler (response correlation or notification routing).\n */\n private handleMessage(raw: string): void {\n // Step 1: Parse the raw JSON string into a plain object.\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n this.emit(\"error\", new TransportError(\n `Received malformed JSON from gateway: ${raw.slice(0, 200)}`\n ));\n return;\n }\n\n // Step 2: Classify by field presence and dispatch.\n // The JSON-RPC 2.0 spec defines the message type by which fields are present:\n // - Has `id` + `result` -> Success Response\n // - Has `id` + `error` -> Error Response\n // - Has `method`, no `id` -> Notification (server push)\n // - Has `method` + `id` -> Request (unexpected from gateway)\n if (typeof parsed !== \"object\" || parsed === null) {\n this.emit(\"error\", new TransportError(\n \"Received non-object JSON from gateway\"\n ));\n return;\n }\n\n const msg = parsed as Record<string, unknown>;\n\n // Check for Response (success): has id and result.\n if (\"id\" in msg && \"result\" in msg) {\n this.handleResponse(msg);\n return;\n }\n\n // Check for Error Response: has id and error.\n if (\"id\" in msg && \"error\" in msg) {\n this.handleErrorResponse(msg);\n return;\n }\n\n // Check for Notification: has method but no id.\n if (\"method\" in msg && !(\"id\" in msg)) {\n this.handleNotification(msg);\n return;\n }\n\n // Unrecognized message shape.\n this.emit(\"error\", new TransportError(\n `Received unrecognized message shape from gateway: ${raw.slice(0, 200)}`\n ));\n }\n\n /**\n * Handle a success response by resolving the matching pending request.\n */\n private handleResponse(msg: Record<string, unknown>): void {\n // Validate the envelope structure.\n const parseResult = JsonRpcResponseSchema.safeParse(msg);\n if (!parseResult.success) {\n this.emit(\"error\", new TransportError(\n `Received invalid success response: ${parseResult.error.message}`\n ));\n return;\n }\n\n const response = parseResult.data;\n const id = String(response.id);\n const pendingRequest = this.pending.get(id);\n\n if (!pendingRequest) {\n // Response for an unknown id. Could be a duplicate or a response\n // that arrived after the timeout already fired.\n this.emit(\"error\", new TransportError(\n `Received response for unknown request id: ${id}`\n ));\n return;\n }\n\n // Clean up and resolve.\n clearTimeout(pendingRequest.timer);\n this.pending.delete(id);\n pendingRequest.resolve(response.result);\n }\n\n /**\n * Handle an error response by rejecting the matching pending request.\n */\n private handleErrorResponse(msg: Record<string, unknown>): void {\n // Validate the envelope structure.\n const parseResult = JsonRpcErrorSchema.safeParse(msg);\n if (!parseResult.success) {\n this.emit(\"error\", new TransportError(\n `Received invalid error response: ${parseResult.error.message}`\n ));\n return;\n }\n\n const errorResponse = parseResult.data;\n const id = String(errorResponse.id);\n const pendingRequest = this.pending.get(id);\n\n if (!pendingRequest) {\n this.emit(\"error\", new TransportError(\n `Received error response for unknown request id: ${id}`\n ));\n return;\n }\n\n // Clean up and reject with a structured RpcError.\n clearTimeout(pendingRequest.timer);\n this.pending.delete(id);\n pendingRequest.reject(new RpcError(errorResponse.error));\n }\n\n /**\n * Handle a notification by emitting it as an event. It goes out only as\n * \"notification\", never under its method name, because a server could then\n * fake the \"close\" event by naming a notification after it.\n */\n private handleNotification(msg: Record<string, unknown>): void {\n // Validate the envelope structure.\n const parseResult = JsonRpcNotificationSchema.safeParse(msg);\n if (!parseResult.success) {\n this.emit(\"error\", new TransportError(\n `Received invalid notification: ${parseResult.error.message}`\n ));\n return;\n }\n\n this.emit(\"notification\", parseResult.data);\n }\n\n // -------------------------------------------------------------------------\n // Private: connection lifecycle\n // -------------------------------------------------------------------------\n\n /**\n * Handle WebSocket close after the connection was established.\n *\n * If the close was not initiated by the client (i.e. the state is still\n * CONNECTED), this is an unexpected disconnect. All pending requests are\n * rejected.\n */\n private handleClose(code: number, reason: string): void {\n const wasConnected = this._state === TransportState.CONNECTED;\n\n this.rejectAllPending(\n new ConnectionError(\n `WebSocket closed unexpectedly: code ${code}, reason: ${reason || \"none\"}`,\n this.url,\n )\n );\n\n this.cleanupSocket();\n this._state = TransportState.DISCONNECTED;\n\n if (wasConnected) {\n this.emit(\"close\", code, reason);\n }\n }\n\n /**\n * Reject all pending requests with the given error.\n *\n * Called on disconnect (expected or unexpected) so no Promise is left\n * hanging forever.\n */\n private rejectAllPending(error: Error): void {\n for (const pending of this.pending.values()) {\n clearTimeout(pending.timer);\n pending.reject(error);\n }\n this.pending.clear();\n }\n\n /**\n * Null out the WebSocket reference and remove event handlers.\n *\n * Prevents stale handlers from firing after the transport considers\n * itself disconnected. The error handler stays as a no-op, because the `ws`\n * package throws a late socket error that has no handler, and it raises one\n * when a socket closes before its handshake ends.\n */\n private cleanupSocket(): void {\n if (this.ws) {\n this.ws.onopen = null;\n this.ws.onclose = null;\n this.ws.onmessage = null;\n this.ws.onerror = () => {};\n this.ws = null;\n }\n }\n\n // -------------------------------------------------------------------------\n // Private: utilities\n // -------------------------------------------------------------------------\n\n /**\n * Generate a unique request id.\n *\n * Uses a simple incrementing counter prefixed with \"req-\". Ids are unique\n * within a single transport instance. The counter resets if you create a\n * new transport.\n */\n private generateId(): string {\n const id = `req-${this.nextId}`;\n this.nextId += 1;\n return id;\n }\n}\n"],"mappings":"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","names":["TransportState"]}