@triflux/remote 10.28.1 → 10.30.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.
@@ -21,6 +21,7 @@
21
21
  import { createHash, randomBytes } from "node:crypto";
22
22
  import net from "node:net";
23
23
 
24
+ import { JsonRpcDispatchBase } from "./jsonrpc-core.mjs";
24
25
  import {
25
26
  JsonRpcProtocolError,
26
27
  JsonRpcTransportError,
@@ -74,7 +75,7 @@ function encodeClientFrame(opcode, payload) {
74
75
  /**
75
76
  * JSON-RPC 2.0 client over WebSocket-over-UDS.
76
77
  */
77
- export class JsonRpcWsUdsClient {
78
+ export class JsonRpcWsUdsClient extends JsonRpcDispatchBase {
78
79
  /**
79
80
  * @param {object} options
80
81
  * @param {string} options.socketPath Absolute path to the AF_UNIX socket.
@@ -91,8 +92,8 @@ export class JsonRpcWsUdsClient {
91
92
  if (typeof socketPath !== "string" || !socketPath) {
92
93
  throw new TypeError("JsonRpcWsUdsClient requires a socketPath string");
93
94
  }
95
+ super({ onError, closedMessage: CLOSED_MESSAGE });
94
96
  this._socketPath = socketPath;
95
- this._onError = typeof onError === "function" ? onError : null;
96
97
  this._maxFrameSize =
97
98
  Number.isFinite(maxFrameSize) && maxFrameSize > 0
98
99
  ? maxFrameSize
@@ -104,11 +105,6 @@ export class JsonRpcWsUdsClient {
104
105
  /** @type {'idle'|'running'|'closing'|'closed'} */
105
106
  this._state = "idle";
106
107
  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
108
 
113
109
  // RX framing state
114
110
  this._rxBuf = Buffer.alloc(0);
@@ -272,125 +268,40 @@ export class JsonRpcWsUdsClient {
272
268
  });
273
269
  }
274
270
 
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;
271
+ // --- internals ---------------------------------------------------------
289
272
 
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
- });
273
+ _encodeAndSend(frame) {
274
+ this._sendText(JSON.stringify(frame));
313
275
  }
314
276
 
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;
277
+ _onEnterClosing() {
278
+ // best-effort WS close frame
324
279
  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);
280
+ this._socket?.write(encodeClientFrame(OP_CLOSE, Buffer.alloc(0)));
281
+ } catch {}
282
+ // Arm a deadline so a peer that never replies with close/EOF cannot pin
283
+ // the socket + pending requests open forever.
284
+ this._closingTimer = setTimeout(() => {
285
+ if (this._state === "closing") this._closeWith("closed");
286
+ }, CLOSING_DEADLINE_MS);
287
+ if (typeof this._closingTimer.unref === "function") {
288
+ this._closingTimer.unref();
346
289
  }
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
290
  }
355
291
 
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;
292
+ _beforeRejectPending() {
293
+ if (this._closingTimer) {
294
+ clearTimeout(this._closingTimer);
295
+ this._closingTimer = null;
378
296
  }
379
- this._closeWith("closed");
380
297
  }
381
298
 
382
- /** @returns {boolean} */
383
- isOpen() {
384
- return this._state === "running";
385
- }
386
-
387
- /** @returns {'idle'|'running'|'closing'|'closed'} */
388
- getState() {
389
- return this._state;
299
+ _teardownTransport() {
300
+ try {
301
+ this._socket?.destroy();
302
+ } catch {}
390
303
  }
391
304
 
392
- // --- internals ---------------------------------------------------------
393
-
394
305
  _sendText(text) {
395
306
  if (this._state === "closed") throw new Error(CLOSED_MESSAGE);
396
307
  const frame = encodeClientFrame(OP_TEXT, Buffer.from(text, "utf8"));
@@ -511,7 +422,7 @@ export class JsonRpcWsUdsClient {
511
422
  return;
512
423
  }
513
424
  if (fin) {
514
- this._handleMessage(payload.toString("utf8"));
425
+ this._handleInboundMessage(payload.toString("utf8"));
515
426
  return;
516
427
  }
517
428
  this._fragmentOpcode = opcode;
@@ -540,7 +451,7 @@ export class JsonRpcWsUdsClient {
540
451
  this._fragmentChunks = [];
541
452
  this._fragmentOpcode = null;
542
453
  this._fragmentSize = 0;
543
- this._handleMessage(full.toString("utf8"));
454
+ this._handleInboundMessage(full.toString("utf8"));
544
455
  return;
545
456
  }
546
457
 
@@ -553,96 +464,6 @@ export class JsonRpcWsUdsClient {
553
464
  this._closeWith("closed", err);
554
465
  }
555
466
 
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
467
  _handleStreamError(which, err) {
647
468
  if (this._state === "closed") return;
648
469
  const base = err instanceof Error ? err : new Error(String(err));
@@ -653,34 +474,6 @@ export class JsonRpcWsUdsClient {
653
474
  this._emitError(wrapped);
654
475
  this._closeWith("closed", wrapped);
655
476
  }
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
477
  }
685
478
 
686
479
  export default JsonRpcWsUdsClient;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@triflux/remote",
3
- "version": "10.28.1",
3
+ "version": "10.30.0",
4
4
  "description": "triflux remote — team mode, psmux, MCP workers, SQLite store.",
5
5
  "type": "module",
6
6
  "main": "hub/index.mjs",