@triflux/remote 10.27.0 → 10.28.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,686 @@
1
+ // hub/workers/lib/jsonrpc-ws-uds.mjs
2
+ // JSON-RPC 2.0 client over WebSocket-over-Unix-Domain-Socket.
3
+ //
4
+ // codex `app-server --listen unix://PATH` does NOT speak newline-delimited JSON
5
+ // on the socket — it speaks WebSocket (RFC 6455) after a standard HTTP/1.1
6
+ // Upgrade handshake, one JSON-RPC message per text frame. Confirmed live against
7
+ // codex-cli 0.135.0 (see experiments/native-bridge-feasibility/
8
+ // codex-app-server-uds-probe.mjs). This is the UDS sibling of the stdio
9
+ // `JsonRpcStdioClient`: same public surface (request/notify/onNotification/
10
+ // close/isOpen), different framing.
11
+ //
12
+ // Wire framing notes:
13
+ // - client -> server frames MUST be masked (RFC 6455 §5.3).
14
+ // - server -> client frames are unmasked.
15
+ // - one JSON-RPC object per text frame; fragmented frames are reassembled.
16
+ // - outbound JSON-RPC omits the `"jsonrpc":"2.0"` header (OpenAI App Server
17
+ // JSONL variant), matching JsonRpcStdioClient.
18
+ //
19
+ // Zero new dependencies — minimal hand-rolled WS client (no `ws` package).
20
+
21
+ import { createHash, randomBytes } from "node:crypto";
22
+ import net from "node:net";
23
+
24
+ import {
25
+ JsonRpcProtocolError,
26
+ JsonRpcTransportError,
27
+ } from "./jsonrpc-stdio.mjs";
28
+
29
+ const WS_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
30
+ const DEFAULT_MAX_FRAME_SIZE = 16 * 1024 * 1024; // 16 MiB
31
+ const DEFAULT_CONNECT_TIMEOUT_MS = 10_000;
32
+ // After a graceful close("closing") we wait this long for the peer's close/EOF
33
+ // before force-destroying the socket so a silent peer cannot pin it open.
34
+ const CLOSING_DEADLINE_MS = 2_000;
35
+ const MAX_CONTROL_PAYLOAD = 125; // RFC 6455 §5.5: control frames <=125 bytes
36
+ const CLOSED_MESSAGE = "JsonRpcWsUdsClient closed";
37
+
38
+ // RFC 6455 opcodes
39
+ const OP_CONTINUATION = 0x0;
40
+ const OP_TEXT = 0x1;
41
+ const OP_BINARY = 0x2;
42
+ const OP_CLOSE = 0x8;
43
+ const OP_PING = 0x9;
44
+ const OP_PONG = 0xa;
45
+
46
+ /**
47
+ * Encode a single client->server WebSocket frame (always masked).
48
+ * @param {number} opcode
49
+ * @param {Buffer} payload
50
+ * @returns {Buffer}
51
+ */
52
+ function encodeClientFrame(opcode, payload) {
53
+ const len = payload.length;
54
+ const mask = randomBytes(4);
55
+ let header;
56
+ if (len < 126) {
57
+ header = Buffer.from([0x80 | opcode, 0x80 | len]);
58
+ } else if (len < 65536) {
59
+ header = Buffer.alloc(4);
60
+ header[0] = 0x80 | opcode;
61
+ header[1] = 0x80 | 126;
62
+ header.writeUInt16BE(len, 2);
63
+ } else {
64
+ header = Buffer.alloc(10);
65
+ header[0] = 0x80 | opcode;
66
+ header[1] = 0x80 | 127;
67
+ header.writeBigUInt64BE(BigInt(len), 2);
68
+ }
69
+ const masked = Buffer.allocUnsafe(len);
70
+ for (let i = 0; i < len; i++) masked[i] = payload[i] ^ mask[i % 4];
71
+ return Buffer.concat([header, mask, masked]);
72
+ }
73
+
74
+ /**
75
+ * JSON-RPC 2.0 client over WebSocket-over-UDS.
76
+ */
77
+ export class JsonRpcWsUdsClient {
78
+ /**
79
+ * @param {object} options
80
+ * @param {string} options.socketPath Absolute path to the AF_UNIX socket.
81
+ * @param {(err: Error) => void} [options.onError]
82
+ * @param {number} [options.maxFrameSize] Max bytes per inbound frame payload.
83
+ * @param {number} [options.connectTimeoutMs]
84
+ */
85
+ constructor({
86
+ socketPath,
87
+ onError,
88
+ maxFrameSize = DEFAULT_MAX_FRAME_SIZE,
89
+ connectTimeoutMs = DEFAULT_CONNECT_TIMEOUT_MS,
90
+ } = {}) {
91
+ if (typeof socketPath !== "string" || !socketPath) {
92
+ throw new TypeError("JsonRpcWsUdsClient requires a socketPath string");
93
+ }
94
+ this._socketPath = socketPath;
95
+ this._onError = typeof onError === "function" ? onError : null;
96
+ this._maxFrameSize =
97
+ Number.isFinite(maxFrameSize) && maxFrameSize > 0
98
+ ? maxFrameSize
99
+ : DEFAULT_MAX_FRAME_SIZE;
100
+ this._connectTimeoutMs = Number.isFinite(connectTimeoutMs)
101
+ ? connectTimeoutMs
102
+ : DEFAULT_CONNECT_TIMEOUT_MS;
103
+
104
+ /** @type {'idle'|'running'|'closing'|'closed'} */
105
+ this._state = "idle";
106
+ this._socket = null;
107
+ this._nextRequestId = 1;
108
+ /** @type {Map<number, { resolve: Function, reject: Function, timer: any, method: string }>} */
109
+ this._pendingRequests = new Map();
110
+ /** @type {Map<string, Set<Function>>} */
111
+ this._notificationHandlers = new Map();
112
+
113
+ // RX framing state
114
+ this._rxBuf = Buffer.alloc(0);
115
+ /** @type {Buffer[]} accumulated payloads of a fragmented message */
116
+ this._fragmentChunks = [];
117
+ /** @type {number|null} opcode of the in-progress fragmented message */
118
+ this._fragmentOpcode = null;
119
+ this._fragmentSize = 0;
120
+ /** @type {ReturnType<typeof setTimeout>|null} */
121
+ this._closingTimer = null;
122
+ }
123
+
124
+ /**
125
+ * Open the socket and complete the WebSocket HTTP Upgrade handshake.
126
+ * Resolves once the connection is in the `running` state.
127
+ * @returns {Promise<void>}
128
+ */
129
+ connect() {
130
+ if (this._state !== "idle") {
131
+ return Promise.reject(
132
+ new Error(
133
+ `JsonRpcWsUdsClient.connect() invalid in state ${this._state}`,
134
+ ),
135
+ );
136
+ }
137
+ return new Promise((resolve, reject) => {
138
+ const key = randomBytes(16).toString("base64");
139
+ const expectedAccept = createHash("sha1")
140
+ .update(key + WS_GUID)
141
+ .digest("base64");
142
+ let handshakeBuf = Buffer.alloc(0);
143
+ let settled = false;
144
+
145
+ const socket = net.connect(this._socketPath);
146
+ this._socket = socket;
147
+
148
+ const timer = setTimeout(() => {
149
+ if (settled) return;
150
+ settled = true;
151
+ try {
152
+ socket.destroy();
153
+ } catch {}
154
+ this._state = "closed";
155
+ reject(
156
+ new JsonRpcTransportError(
157
+ `WebSocket-over-UDS connect timed out after ${this._connectTimeoutMs}ms: ${this._socketPath}`,
158
+ ),
159
+ );
160
+ }, this._connectTimeoutMs);
161
+
162
+ const onConnectError = (err) => {
163
+ if (settled) return;
164
+ settled = true;
165
+ clearTimeout(timer);
166
+ this._state = "closed";
167
+ reject(
168
+ new JsonRpcTransportError(
169
+ `WebSocket-over-UDS connect error: ${err?.message || err}`,
170
+ { cause: err },
171
+ ),
172
+ );
173
+ };
174
+
175
+ socket.once("error", onConnectError);
176
+ socket.on("connect", () => {
177
+ const req =
178
+ "GET / HTTP/1.1\r\n" +
179
+ "Host: localhost\r\n" +
180
+ "Upgrade: websocket\r\n" +
181
+ "Connection: Upgrade\r\n" +
182
+ `Sec-WebSocket-Key: ${key}\r\n` +
183
+ "Sec-WebSocket-Version: 13\r\n\r\n";
184
+ socket.write(req);
185
+ });
186
+
187
+ const onHandshakeData = (chunk) => {
188
+ if (settled) return;
189
+ handshakeBuf = Buffer.concat([handshakeBuf, chunk]);
190
+ const sep = handshakeBuf.indexOf("\r\n\r\n");
191
+ if (sep < 0) {
192
+ if (handshakeBuf.length > 64 * 1024) {
193
+ settled = true;
194
+ clearTimeout(timer);
195
+ socket.removeListener("error", onConnectError);
196
+ socket.removeListener("data", onHandshakeData);
197
+ try {
198
+ socket.destroy();
199
+ } catch {}
200
+ this._state = "closed";
201
+ reject(
202
+ new JsonRpcProtocolError(
203
+ "WebSocket handshake response headers exceeded 64 KiB",
204
+ ),
205
+ );
206
+ }
207
+ return;
208
+ }
209
+ const rawHeaders = handshakeBuf.subarray(0, sep).toString("utf8");
210
+ const leftover = handshakeBuf.subarray(sep + 4);
211
+
212
+ const lines = rawHeaders.split("\r\n");
213
+ const ok101 = /^HTTP\/1\.\d 101/.test(lines[0] || "");
214
+ const headerMap = new Map();
215
+ for (let i = 1; i < lines.length; i++) {
216
+ const idx = lines[i].indexOf(":");
217
+ if (idx < 0) continue;
218
+ headerMap.set(
219
+ lines[i].slice(0, idx).trim().toLowerCase(),
220
+ lines[i].slice(idx + 1).trim(),
221
+ );
222
+ }
223
+ const acceptOk =
224
+ headerMap.get("sec-websocket-accept") === expectedAccept;
225
+
226
+ if (!ok101 || !acceptOk) {
227
+ settled = true;
228
+ clearTimeout(timer);
229
+ socket.removeListener("error", onConnectError);
230
+ socket.removeListener("data", onHandshakeData);
231
+ try {
232
+ socket.destroy();
233
+ } catch {}
234
+ this._state = "closed";
235
+ reject(
236
+ new JsonRpcProtocolError(
237
+ `WebSocket upgrade rejected (status101=${ok101}, acceptValid=${acceptOk})`,
238
+ ),
239
+ );
240
+ return;
241
+ }
242
+
243
+ // Handshake complete — switch to running and wire frame handling.
244
+ settled = true;
245
+ clearTimeout(timer);
246
+ socket.removeListener("error", onConnectError);
247
+ socket.removeListener("data", onHandshakeData);
248
+ this._state = "running";
249
+
250
+ socket.on("data", (buf) => this._onSocketData(buf));
251
+ socket.on("error", (err) => this._handleStreamError("socket", err));
252
+ socket.on("close", () => {
253
+ if (this._state === "running") {
254
+ const err = new JsonRpcTransportError(
255
+ "WebSocket-over-UDS closed unexpectedly (EOF during running)",
256
+ );
257
+ this._emitError(err);
258
+ this._closeWith("closed", err);
259
+ } else if (this._state !== "closed") {
260
+ this._closeWith("closed");
261
+ }
262
+ });
263
+
264
+ if (leftover.length > 0) {
265
+ this._rxBuf = Buffer.concat([this._rxBuf, leftover]);
266
+ this._drainFrames();
267
+ }
268
+ resolve();
269
+ };
270
+
271
+ socket.on("data", onHandshakeData);
272
+ });
273
+ }
274
+
275
+ /**
276
+ * Issue a JSON-RPC request and resolve with the server's `result`.
277
+ * @param {string} method
278
+ * @param {unknown} params
279
+ * @param {number} [timeoutMs=60000]
280
+ * @returns {Promise<any>}
281
+ */
282
+ request(method, params, timeoutMs = 60000) {
283
+ if (this._state !== "running") {
284
+ return Promise.reject(new Error(CLOSED_MESSAGE));
285
+ }
286
+ const id = this._nextRequestId++;
287
+ const frame = { id, method };
288
+ if (params !== undefined) frame.params = params;
289
+
290
+ return new Promise((resolve, reject) => {
291
+ let timer = null;
292
+ if (Number.isFinite(timeoutMs) && timeoutMs > 0) {
293
+ timer = setTimeout(() => {
294
+ const pending = this._pendingRequests.get(id);
295
+ if (!pending) return;
296
+ this._pendingRequests.delete(id);
297
+ reject(
298
+ new Error(
299
+ `JSON-RPC request timed out after ${timeoutMs}ms: ${method}`,
300
+ ),
301
+ );
302
+ }, timeoutMs);
303
+ }
304
+ this._pendingRequests.set(id, { resolve, reject, timer, method });
305
+ try {
306
+ this._sendText(JSON.stringify(frame));
307
+ } catch (err) {
308
+ this._pendingRequests.delete(id);
309
+ if (timer) clearTimeout(timer);
310
+ reject(err);
311
+ }
312
+ });
313
+ }
314
+
315
+ /**
316
+ * Send a JSON-RPC notification (no id, no response).
317
+ * @param {string} method
318
+ * @param {unknown} [params]
319
+ */
320
+ notify(method, params) {
321
+ if (this._state !== "running") return;
322
+ const frame = { method };
323
+ if (params !== undefined) frame.params = params;
324
+ try {
325
+ this._sendText(JSON.stringify(frame));
326
+ } catch (err) {
327
+ this._emitError(err instanceof Error ? err : new Error(String(err)));
328
+ }
329
+ }
330
+
331
+ /**
332
+ * Subscribe to inbound notifications. `"*"` = catch-all receiving
333
+ * `(params, method)`; targeted handlers receive `(params)`.
334
+ * @param {string} method
335
+ * @param {(params: any, method?: string) => void} callback
336
+ * @returns {() => void} unsubscribe
337
+ */
338
+ onNotification(method, callback) {
339
+ if (typeof callback !== "function") {
340
+ throw new TypeError("onNotification requires a callback function");
341
+ }
342
+ let set = this._notificationHandlers.get(method);
343
+ if (!set) {
344
+ set = new Set();
345
+ this._notificationHandlers.set(method, set);
346
+ }
347
+ set.add(callback);
348
+ return () => {
349
+ const handlers = this._notificationHandlers.get(method);
350
+ if (!handlers) return;
351
+ handlers.delete(callback);
352
+ if (handlers.size === 0) this._notificationHandlers.delete(method);
353
+ };
354
+ }
355
+
356
+ /**
357
+ * Close the client. `reason === "closing"` transitions to an intermediate
358
+ * graceful state where a subsequent EOF is treated as normal. Idempotent.
359
+ * @param {string} [reason]
360
+ */
361
+ close(reason) {
362
+ if (this._state === "closed") return;
363
+ if (reason === "closing" && this._state === "running") {
364
+ this._state = "closing";
365
+ // best-effort WS close frame
366
+ try {
367
+ this._socket?.write(encodeClientFrame(OP_CLOSE, Buffer.alloc(0)));
368
+ } catch {}
369
+ // Arm a deadline so a peer that never replies with close/EOF cannot pin
370
+ // the socket + pending requests open forever.
371
+ this._closingTimer = setTimeout(() => {
372
+ if (this._state === "closing") this._closeWith("closed");
373
+ }, CLOSING_DEADLINE_MS);
374
+ if (typeof this._closingTimer.unref === "function") {
375
+ this._closingTimer.unref();
376
+ }
377
+ return;
378
+ }
379
+ this._closeWith("closed");
380
+ }
381
+
382
+ /** @returns {boolean} */
383
+ isOpen() {
384
+ return this._state === "running";
385
+ }
386
+
387
+ /** @returns {'idle'|'running'|'closing'|'closed'} */
388
+ getState() {
389
+ return this._state;
390
+ }
391
+
392
+ // --- internals ---------------------------------------------------------
393
+
394
+ _sendText(text) {
395
+ if (this._state === "closed") throw new Error(CLOSED_MESSAGE);
396
+ const frame = encodeClientFrame(OP_TEXT, Buffer.from(text, "utf8"));
397
+ this._socket.write(frame, (err) => {
398
+ if (err) this._handleStreamError("socket-write", err);
399
+ });
400
+ }
401
+
402
+ _onSocketData(chunk) {
403
+ if (this._state === "closed") return;
404
+ this._rxBuf = Buffer.concat([this._rxBuf, chunk]);
405
+ this._drainFrames();
406
+ }
407
+
408
+ /**
409
+ * Parse as many complete WebSocket frames as are buffered. Control frames
410
+ * (ping/pong/close) are handled inline; text/continuation frames are
411
+ * reassembled into a full JSON-RPC message before dispatch.
412
+ */
413
+ _drainFrames() {
414
+ while (this._rxBuf.length >= 2) {
415
+ const b0 = this._rxBuf[0];
416
+ const b1 = this._rxBuf[1];
417
+ const fin = (b0 & 0x80) !== 0;
418
+ const opcode = b0 & 0x0f;
419
+ const masked = (b1 & 0x80) !== 0;
420
+ let len = b1 & 0x7f;
421
+ let cursor = 2;
422
+
423
+ if (len === 126) {
424
+ if (this._rxBuf.length < cursor + 2) return;
425
+ len = this._rxBuf.readUInt16BE(cursor);
426
+ cursor += 2;
427
+ } else if (len === 127) {
428
+ if (this._rxBuf.length < cursor + 8) return;
429
+ const big = this._rxBuf.readBigUInt64BE(cursor);
430
+ if (big > BigInt(this._maxFrameSize)) {
431
+ const err = new JsonRpcProtocolError(
432
+ `WebSocket frame payload ${big} exceeds max ${this._maxFrameSize}`,
433
+ );
434
+ this._emitError(err);
435
+ this._closeWith("closed", err);
436
+ return;
437
+ }
438
+ len = Number(big);
439
+ cursor += 8;
440
+ }
441
+
442
+ if (len > this._maxFrameSize) {
443
+ const err = new JsonRpcProtocolError(
444
+ `WebSocket frame payload ${len} exceeds max ${this._maxFrameSize}`,
445
+ );
446
+ this._emitError(err);
447
+ this._closeWith("closed", err);
448
+ return;
449
+ }
450
+
451
+ // RFC 6455 §5.1: server -> client frames MUST NOT be masked.
452
+ if (masked) {
453
+ this._protocolClose("WebSocket server frame is masked (RFC 6455 §5.1)");
454
+ return;
455
+ }
456
+ if (this._rxBuf.length < cursor + len) return; // wait for full payload
457
+
458
+ // Detach from rxBuf before we slice it off.
459
+ const payload = Buffer.from(this._rxBuf.subarray(cursor, cursor + len));
460
+ this._rxBuf = this._rxBuf.subarray(cursor + len);
461
+
462
+ this._handleFrame(fin, opcode, payload);
463
+ if (this._state === "closed") return;
464
+ }
465
+ }
466
+
467
+ _handleFrame(fin, opcode, payload) {
468
+ // Control frames (>=0x8): must be FIN and <=125 bytes (RFC 6455 §5.5).
469
+ if (opcode >= 0x8) {
470
+ if (!fin || payload.length > MAX_CONTROL_PAYLOAD) {
471
+ this._protocolClose(
472
+ `Invalid control frame (opcode 0x${opcode.toString(16)}, fin=${fin}, len=${payload.length})`,
473
+ );
474
+ return;
475
+ }
476
+ if (opcode === OP_PING) {
477
+ try {
478
+ this._socket?.write(encodeClientFrame(OP_PONG, payload));
479
+ } catch {}
480
+ return;
481
+ }
482
+ if (opcode === OP_PONG) return;
483
+ if (opcode === OP_CLOSE) {
484
+ if (this._state === "running") {
485
+ const err = new JsonRpcTransportError(
486
+ "WebSocket server sent close frame",
487
+ );
488
+ this._emitError(err);
489
+ this._closeWith("closed", err);
490
+ } else {
491
+ this._closeWith("closed");
492
+ }
493
+ return;
494
+ }
495
+ this._protocolClose(`Unknown control opcode 0x${opcode.toString(16)}`);
496
+ return;
497
+ }
498
+
499
+ // Data frames: text / binary (message start) or continuation.
500
+ if (opcode === OP_TEXT || opcode === OP_BINARY) {
501
+ if (this._fragmentOpcode !== null) {
502
+ this._protocolClose(
503
+ "New data frame received while a fragmented message is active",
504
+ );
505
+ return;
506
+ }
507
+ if (opcode === OP_BINARY) {
508
+ this._protocolClose(
509
+ "Binary frames are not supported (expected text JSON-RPC)",
510
+ );
511
+ return;
512
+ }
513
+ if (fin) {
514
+ this._handleMessage(payload.toString("utf8"));
515
+ return;
516
+ }
517
+ this._fragmentOpcode = opcode;
518
+ this._fragmentChunks = [payload];
519
+ this._fragmentSize = payload.length;
520
+ return;
521
+ }
522
+
523
+ if (opcode === OP_CONTINUATION) {
524
+ if (this._fragmentOpcode === null) {
525
+ this._protocolClose(
526
+ "Continuation frame with no active fragmented message",
527
+ );
528
+ return;
529
+ }
530
+ this._fragmentSize += payload.length;
531
+ if (this._fragmentSize > this._maxFrameSize) {
532
+ this._protocolClose(
533
+ `Fragmented message ${this._fragmentSize} exceeds max ${this._maxFrameSize}`,
534
+ );
535
+ return;
536
+ }
537
+ this._fragmentChunks.push(payload);
538
+ if (!fin) return;
539
+ const full = Buffer.concat(this._fragmentChunks);
540
+ this._fragmentChunks = [];
541
+ this._fragmentOpcode = null;
542
+ this._fragmentSize = 0;
543
+ this._handleMessage(full.toString("utf8"));
544
+ return;
545
+ }
546
+
547
+ this._protocolClose(`Unknown WebSocket opcode 0x${opcode.toString(16)}`);
548
+ }
549
+
550
+ _protocolClose(message) {
551
+ const err = new JsonRpcProtocolError(message);
552
+ this._emitError(err);
553
+ this._closeWith("closed", err);
554
+ }
555
+
556
+ _handleMessage(line) {
557
+ if (this._state === "closed") return;
558
+ if (line.length === 0) return;
559
+
560
+ let frame;
561
+ try {
562
+ frame = JSON.parse(line);
563
+ } catch (err) {
564
+ const pErr = new JsonRpcProtocolError(
565
+ `JSON-RPC parse error: ${err instanceof Error ? err.message : String(err)}`,
566
+ { cause: err },
567
+ );
568
+ this._emitError(pErr);
569
+ if (this._state === "running") this._closeWith("closed", pErr);
570
+ return;
571
+ }
572
+
573
+ if (!frame || typeof frame !== "object") {
574
+ const pErr = new JsonRpcProtocolError(
575
+ "JSON-RPC protocol error: frame is not an object",
576
+ );
577
+ this._emitError(pErr);
578
+ if (this._state === "running") this._closeWith("closed", pErr);
579
+ return;
580
+ }
581
+
582
+ if (
583
+ Object.hasOwn(frame, "id") &&
584
+ frame.id !== null &&
585
+ (Object.hasOwn(frame, "result") || Object.hasOwn(frame, "error"))
586
+ ) {
587
+ this._dispatchResponse(frame);
588
+ return;
589
+ }
590
+
591
+ if (typeof frame.method === "string" && !Object.hasOwn(frame, "id")) {
592
+ this._dispatchNotification(frame);
593
+ return;
594
+ }
595
+
596
+ this._emitError(
597
+ new JsonRpcProtocolError(
598
+ "JSON-RPC protocol error: unrecognized frame shape",
599
+ ),
600
+ );
601
+ }
602
+
603
+ _dispatchResponse(frame) {
604
+ const pending = this._pendingRequests.get(frame.id);
605
+ if (!pending) return;
606
+ this._pendingRequests.delete(frame.id);
607
+ if (pending.timer) clearTimeout(pending.timer);
608
+
609
+ if (Object.hasOwn(frame, "error") && frame.error) {
610
+ const { code, message, data } = frame.error;
611
+ const err = new Error(
612
+ `JSON-RPC error${typeof code === "number" ? ` ${code}` : ""}: ${message || "unknown"}`,
613
+ );
614
+ if (code !== undefined) err.code = code;
615
+ if (data !== undefined) err.data = data;
616
+ pending.reject(err);
617
+ return;
618
+ }
619
+ pending.resolve(frame.result);
620
+ }
621
+
622
+ _dispatchNotification(frame) {
623
+ const { method, params } = frame;
624
+ const targeted = this._notificationHandlers.get(method);
625
+ if (targeted && targeted.size > 0) {
626
+ for (const cb of targeted) {
627
+ try {
628
+ cb(params);
629
+ } catch (err) {
630
+ this._emitError(err instanceof Error ? err : new Error(String(err)));
631
+ }
632
+ }
633
+ }
634
+ const wildcard = this._notificationHandlers.get("*");
635
+ if (wildcard && wildcard.size > 0) {
636
+ for (const cb of wildcard) {
637
+ try {
638
+ cb(params, method);
639
+ } catch (err) {
640
+ this._emitError(err instanceof Error ? err : new Error(String(err)));
641
+ }
642
+ }
643
+ }
644
+ }
645
+
646
+ _handleStreamError(which, err) {
647
+ if (this._state === "closed") return;
648
+ const base = err instanceof Error ? err : new Error(String(err));
649
+ const wrapped = new JsonRpcTransportError(
650
+ `WebSocket-over-UDS stream error on ${which}: ${base.message}`,
651
+ { cause: base },
652
+ );
653
+ this._emitError(wrapped);
654
+ this._closeWith("closed", wrapped);
655
+ }
656
+
657
+ _closeWith(target, rejectReason = null) {
658
+ if (this._state === "closed") return;
659
+ this._state = target;
660
+ if (this._closingTimer) {
661
+ clearTimeout(this._closingTimer);
662
+ this._closingTimer = null;
663
+ }
664
+ const rejectErr =
665
+ rejectReason instanceof Error ? rejectReason : new Error(CLOSED_MESSAGE);
666
+ for (const [, pending] of this._pendingRequests) {
667
+ if (pending.timer) clearTimeout(pending.timer);
668
+ pending.reject(rejectErr);
669
+ }
670
+ this._pendingRequests.clear();
671
+ try {
672
+ this._socket?.destroy();
673
+ } catch {}
674
+ }
675
+
676
+ _emitError(err) {
677
+ if (!this._onError) return;
678
+ try {
679
+ this._onError(err);
680
+ } catch {
681
+ /* never throw out of the dispatch loop */
682
+ }
683
+ }
684
+ }
685
+
686
+ export default JsonRpcWsUdsClient;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@triflux/remote",
3
- "version": "10.27.0",
3
+ "version": "10.28.1",
4
4
  "description": "triflux remote — team mode, psmux, MCP workers, SQLite store.",
5
5
  "type": "module",
6
6
  "main": "hub/index.mjs",