@statewalker/webrun-rpc 0.4.2 → 0.5.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.
@@ -1 +1 @@
1
- {"version":3,"file":"duplex-over-port.d.ts","sourceRoot":"","sources":["../src/duplex-over-port.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,MAAM,EAOZ,MAAM,6BAA6B,CAAC;AAGrC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAGzD;;;;;;;GAOG;AACH,eAAO,MAAM,YAAY,4BAA4B,CAAC;AActD,MAAM,WAAW,qBAAqB;IACpC;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,6CAA6C;IAC7C,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,CAAC;CACpC;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,aAAa,EAAE,OAAO,GAAE,qBAA0B,GAAG,MAAM,CAE/F;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,aAAa,EACnB,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,qBAA0B,GAClC,MAAM,IAAI,CA6BZ"}
1
+ {"version":3,"file":"duplex-over-port.d.ts","sourceRoot":"","sources":["../src/duplex-over-port.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,MAAM,EAOZ,MAAM,6BAA6B,CAAC;AAIrC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAGzD;;;;;;;GAOG;AACH,eAAO,MAAM,YAAY,4BAA4B,CAAC;AActD,MAAM,WAAW,qBAAqB;IACpC;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,6CAA6C;IAC7C,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,CAAC;CACpC;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,aAAa,EAAE,OAAO,GAAE,qBAA0B,GAAG,MAAM,CAE/F;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,aAAa,EACnB,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,qBAA0B,GAClC,MAAM,IAAI,CA8BZ"}
package/dist/index.d.ts CHANGED
@@ -10,7 +10,7 @@ export * from "./io-send.js";
10
10
  export * from "./listen-bidi.js";
11
11
  export * from "./listen-port.js";
12
12
  export * from "./message-target.js";
13
- export { DEFAULT_MAX_PORTS, multiplexPort } from "./multiplex-port.js";
13
+ export { DEFAULT_MAX_PORTS, MUX_CLOSED_MESSAGE, multiplexPort } from "./multiplex-port.js";
14
14
  export type { PortCodec, PortEnvelope, PortMux, PortMuxOptions } from "./port-types.js";
15
15
  export * from "./recieve.js";
16
16
  export * from "./send.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,0BAA0B,EAAE,MAAM,mBAAmB,CAAC;AAC/D,cAAc,gBAAgB,CAAC;AAC/B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,qBAAqB,CAAC;AACpC,OAAO,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAC3E,OAAO,EACL,OAAO,EACP,KAAK,MAAM,EACX,KAAK,eAAe,EACpB,QAAQ,EACR,SAAS,EACT,KAAK,cAAc,EACnB,KAAK,UAAU,EACf,KAAK,GACN,MAAM,oBAAoB,CAAC;AAC5B,cAAc,uBAAuB,CAAC;AACtC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,cAAc,CAAC;AAC7B,cAAc,kBAAkB,CAAC;AACjC,cAAc,kBAAkB,CAAC;AACjC,cAAc,qBAAqB,CAAC;AACpC,OAAO,EAAE,iBAAiB,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACvE,YAAY,EAAE,SAAS,EAAE,YAAY,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACxF,cAAc,cAAc,CAAC;AAC7B,cAAc,WAAW,CAAC;AAC1B,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,cAAc,wBAAwB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,0BAA0B,EAAE,MAAM,mBAAmB,CAAC;AAC/D,cAAc,gBAAgB,CAAC;AAC/B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,qBAAqB,CAAC;AACpC,OAAO,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAC3E,OAAO,EACL,OAAO,EACP,KAAK,MAAM,EACX,KAAK,eAAe,EACpB,QAAQ,EACR,SAAS,EACT,KAAK,cAAc,EACnB,KAAK,UAAU,EACf,KAAK,GACN,MAAM,oBAAoB,CAAC;AAC5B,cAAc,uBAAuB,CAAC;AACtC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,cAAc,CAAC;AAC7B,cAAc,kBAAkB,CAAC;AACjC,cAAc,kBAAkB,CAAC;AACjC,cAAc,qBAAqB,CAAC;AACpC,OAAO,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAC3F,YAAY,EAAE,SAAS,EAAE,YAAY,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACxF,cAAc,cAAc,CAAC;AAC7B,cAAc,WAAW,CAAC;AAC1B,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,cAAc,wBAAwB,CAAC"}
package/dist/index.js CHANGED
@@ -505,6 +505,7 @@ function duplexOverPort(port, options = {}) {
505
505
  function serveDuplexOverPort(port, handler, options = {}) {
506
506
  const controller = new AbortController();
507
507
  const notice = installAbortNotice(port, controller);
508
+ abortOnPortClose(port, controller);
508
509
  const clock = installStreamTimeout(controller, options.timeout);
509
510
  const inbound = receiveChunks(port, CHANNEL_IN, controller, clock.touch);
510
511
  let output;
@@ -559,6 +560,7 @@ function teardownOnce(controller, notice, clock, inbound, output) {
559
560
  function runCallerSide(port, input, options) {
560
561
  const controller = new AbortController();
561
562
  const notice = installAbortNotice(port, controller);
563
+ abortOnPortClose(port, controller);
562
564
  const clock = installStreamTimeout(controller, options.timeout);
563
565
  const inbound = receiveChunks(port, CHANNEL_OUT, controller, clock.touch);
564
566
  const pump = sendChunks(port, CHANNEL_IN, input, options, controller.signal, clock.touch);
@@ -607,6 +609,27 @@ function installAbortNotice(port, controller) {
607
609
  };
608
610
  }
609
611
  /**
612
+ * Aborts the stream when its port closes underneath it — the multiplexer was
613
+ * closed, or the peer closed the port — for ports that report it (see
614
+ * `getPortCloseSignal`). Without this a stream waiting for the peer's next
615
+ * chunk would wait forever on a port that can no longer deliver one: the
616
+ * outbound half's `callPort` already fails on the close signal, but nothing
617
+ * failed the inbound half.
618
+ */
619
+ function abortOnPortClose(port, controller) {
620
+ const closed = getPortCloseSignal(port);
621
+ if (!closed) return;
622
+ const onClose = () => {
623
+ if (!controller.signal.aborted) controller.abort(closed.reason);
624
+ };
625
+ if (closed.aborted) {
626
+ onClose();
627
+ return;
628
+ }
629
+ closed.addEventListener("abort", onClose, { once: true });
630
+ controller.signal.addEventListener("abort", () => closed.removeEventListener("abort", onClose), { once: true });
631
+ }
632
+ /**
610
633
  * The per-stream inactivity timeout (spec D8). Reset by any chunk in either
611
634
  * direction; elapsing aborts the stream. Unset, zero or non-finite installs no
612
635
  * timer at all, which is the default: a slow consumer is throttled, not failed.
@@ -768,26 +791,34 @@ async function sendChunks(port, channelName, output, { maxMessageSize, log }, si
768
791
  function newVirtualPort(send, requestClose) {
769
792
  const listeners = /* @__PURE__ */ new Set();
770
793
  let closed = false;
771
- return {
772
- port: {
773
- addEventListener(_type, listener) {
774
- listeners.add(listener);
775
- },
776
- removeEventListener(_type, listener) {
777
- listeners.delete(listener);
778
- },
779
- postMessage(message, transfer) {
780
- if (closed) return;
781
- send(message, transfer);
782
- },
783
- close() {
784
- if (closed) return;
785
- closed = true;
786
- const notify = requestClose;
787
- listeners.clear();
788
- notify();
789
- }
794
+ const closing = new AbortController();
795
+ const abort = (reason) => {
796
+ if (closing.signal.aborted) return;
797
+ closing.abort(reason ?? /* @__PURE__ */ new Error("webrun-rpc: the port was closed"));
798
+ };
799
+ const port = {
800
+ addEventListener(_type, listener) {
801
+ listeners.add(listener);
790
802
  },
803
+ removeEventListener(_type, listener) {
804
+ listeners.delete(listener);
805
+ },
806
+ postMessage(message, transfer) {
807
+ if (closed) return;
808
+ send(message, transfer);
809
+ },
810
+ close() {
811
+ if (closed) return;
812
+ closed = true;
813
+ const notify = requestClose;
814
+ listeners.clear();
815
+ notify();
816
+ abort();
817
+ }
818
+ };
819
+ setPortCloseSignal(port, closing.signal);
820
+ return {
821
+ port,
791
822
  deliver(payload) {
792
823
  if (closed) return;
793
824
  const event = new MessageEvent("message", { data: payload });
@@ -795,9 +826,10 @@ function newVirtualPort(send, requestClose) {
795
826
  listener(event);
796
827
  } catch {}
797
828
  },
798
- markClosed() {
829
+ markClosed(reason) {
799
830
  closed = true;
800
831
  listeners.clear();
832
+ abort(reason);
801
833
  },
802
834
  isClosed() {
803
835
  return closed;
@@ -806,6 +838,11 @@ function newVirtualPort(send, requestClose) {
806
838
  }
807
839
  //#endregion
808
840
  //#region src/multiplex-port.ts
841
+ /**
842
+ * The message of the error that fails the calls still in flight on a
843
+ * multiplexer when it is closed (`PortMux.close()`).
844
+ */
845
+ const MUX_CLOSED_MESSAGE = "webrun-rpc: the multiplexer was closed";
809
846
  /** Ceiling on concurrently open virtual ports. Bounds the id table only. */
810
847
  const DEFAULT_MAX_PORTS = 1024;
811
848
  /**
@@ -864,7 +901,7 @@ function multiplexPort(port, options) {
864
901
  }
865
902
  if (!accepted) {
866
903
  open.delete(envelope.id);
867
- handle.markClosed();
904
+ handle.markClosed(/* @__PURE__ */ new Error("webrun-rpc: the port was rejected"));
868
905
  post({
869
906
  type: "close",
870
907
  id: envelope.id,
@@ -881,7 +918,7 @@ function multiplexPort(port, options) {
881
918
  }
882
919
  if (!handle) return;
883
920
  open.delete(envelope.id);
884
- handle.markClosed();
921
+ handle.markClosed(/* @__PURE__ */ new Error("webrun-rpc: the peer closed the port"));
885
922
  };
886
923
  const listener = (event) => {
887
924
  const envelope = codec.read(event);
@@ -913,7 +950,7 @@ function multiplexPort(port, options) {
913
950
  id
914
951
  });
915
952
  open.delete(id);
916
- handle.markClosed();
953
+ handle.markClosed(/* @__PURE__ */ new Error(MUX_CLOSED_MESSAGE));
917
954
  }
918
955
  muxClosed = true;
919
956
  port.removeEventListener("message", listener);
@@ -1177,4 +1214,4 @@ function transferPortMux(target, options = {}) {
1177
1214
  };
1178
1215
  }
1179
1216
  //#endregion
1180
- export { CANCEL_CHANNEL_TYPE, DEFAULT_MAX_PORTS, NO_TIMEOUT, PORT_TRANSFER, STREAM_ABORT, byteChannelFromMessagePort, callBidi, callPort, connect, duplexOverPort, getPortCloseSignal, ioHandle, ioSend, listenBidi, listenCancelChannel, listenPort, multiplexPort, overPipe, overPorts, postCancelChannel, recieve, send, serve, serveDuplexOverPort, setPortCloseSignal, structuredCodec, transferPortMux };
1217
+ export { CANCEL_CHANNEL_TYPE, DEFAULT_MAX_PORTS, MUX_CLOSED_MESSAGE, NO_TIMEOUT, PORT_TRANSFER, STREAM_ABORT, byteChannelFromMessagePort, callBidi, callPort, connect, duplexOverPort, getPortCloseSignal, ioHandle, ioSend, listenBidi, listenCancelChannel, listenPort, multiplexPort, overPipe, overPorts, postCancelChannel, recieve, send, serve, serveDuplexOverPort, setPortCloseSignal, structuredCodec, transferPortMux };
@@ -1,5 +1,10 @@
1
1
  import type { MessageTarget } from "./message-target.js";
2
2
  import type { PortMux, PortMuxOptions } from "./port-types.js";
3
+ /**
4
+ * The message of the error that fails the calls still in flight on a
5
+ * multiplexer when it is closed (`PortMux.close()`).
6
+ */
7
+ export declare const MUX_CLOSED_MESSAGE = "webrun-rpc: the multiplexer was closed";
3
8
  /** Ceiling on concurrently open virtual ports. Bounds the id table only. */
4
9
  export declare const DEFAULT_MAX_PORTS = 1024;
5
10
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"multiplex-port.d.ts","sourceRoot":"","sources":["../src/multiplex-port.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,KAAK,EAAgB,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAG7E,4EAA4E;AAC5E,eAAO,MAAM,iBAAiB,OAAO,CAAC;AAEtC;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,aAAa,EAAE,OAAO,EAAE,cAAc,GAAG,OAAO,CAwHnF"}
1
+ {"version":3,"file":"multiplex-port.d.ts","sourceRoot":"","sources":["../src/multiplex-port.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,KAAK,EAAgB,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAG7E;;;GAGG;AACH,eAAO,MAAM,kBAAkB,2CAA2C,CAAC;AAE3E,4EAA4E;AAC5E,eAAO,MAAM,iBAAiB,OAAO,CAAC;AAEtC;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,aAAa,EAAE,OAAO,EAAE,cAAc,GAAG,OAAO,CAwHnF"}
@@ -4,8 +4,12 @@ export interface VirtualPortHandle {
4
4
  port: MessageTarget;
5
5
  /** Multiplexer-only: hand an inbound payload to the consumer's listeners. */
6
6
  deliver(payload: unknown): void;
7
- /** Multiplexer-only: the port is finished; drop listeners and go inert. */
8
- markClosed(): void;
7
+ /**
8
+ * Multiplexer-only: the port is finished; drop listeners and go inert.
9
+ * `reason` becomes the reason of the port's close signal (see
10
+ * `getPortCloseSignal`), which is what fails the calls still waiting on it.
11
+ */
12
+ markClosed(reason?: unknown): void;
9
13
  isClosed(): boolean;
10
14
  }
11
15
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"virtual-port.d.ts","sourceRoot":"","sources":["../src/virtual-port.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAmB,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAE1E,MAAM,WAAW,iBAAiB;IAChC,4EAA4E;IAC5E,IAAI,EAAE,aAAa,CAAC;IACpB,6EAA6E;IAC7E,OAAO,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;IAChC,2EAA2E;IAC3E,UAAU,IAAI,IAAI,CAAC;IACnB,QAAQ,IAAI,OAAO,CAAC;CACrB;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAC5B,IAAI,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,CAAC,EAAE,YAAY,EAAE,KAAK,IAAI,EAC3D,YAAY,EAAE,CAAC,MAAM,CAAC,EAAE,OAAO,KAAK,IAAI,GACvC,iBAAiB,CAgDnB"}
1
+ {"version":3,"file":"virtual-port.d.ts","sourceRoot":"","sources":["../src/virtual-port.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAmB,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAE1E,MAAM,WAAW,iBAAiB;IAChC,4EAA4E;IAC5E,IAAI,EAAE,aAAa,CAAC;IACpB,6EAA6E;IAC7E,OAAO,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;IAChC;;;;OAIG;IACH,UAAU,CAAC,MAAM,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;IACnC,QAAQ,IAAI,OAAO,CAAC;CACrB;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAC5B,IAAI,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,CAAC,EAAE,YAAY,EAAE,KAAK,IAAI,EAC3D,YAAY,EAAE,CAAC,MAAM,CAAC,EAAE,OAAO,KAAK,IAAI,GACvC,iBAAiB,CA6DnB"}
package/package.json CHANGED
@@ -1,10 +1,13 @@
1
1
  {
2
2
  "name": "@statewalker/webrun-rpc",
3
- "version": "0.4.2",
3
+ "version": "0.5.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Ports and RPC over them: a port multiplexer, and typed request/response and streaming primitives over any MessageTarget.",
7
- "homepage": "https://github.com/statewalker/webrun-wire",
7
+ "bugs": {
8
+ "url": "https://github.com/statewalker/webrun-wire/issues"
9
+ },
10
+ "homepage": "https://github.com/statewalker/webrun-wire/tree/main/packages/webrun-rpc#readme",
8
11
  "author": {
9
12
  "name": "Mikhail Kotelnikov",
10
13
  "email": "mikhail.kotelnikov@gmail.com"
@@ -12,7 +15,7 @@
12
15
  "license": "MIT",
13
16
  "repository": {
14
17
  "type": "git",
15
- "url": "git@github.com:statewalker/webrun-wire.git",
18
+ "url": "git+https://github.com/statewalker/webrun-wire.git",
16
19
  "directory": "packages/webrun-rpc"
17
20
  },
18
21
  "exports": {
@@ -27,7 +30,7 @@
27
30
  "src"
28
31
  ],
29
32
  "dependencies": {
30
- "@statewalker/webrun-streams": "^0.2.1"
33
+ "@statewalker/webrun-streams": "^0.2.2"
31
34
  },
32
35
  "devDependencies": {
33
36
  "@biomejs/biome": "^2.5.15",
@@ -36,7 +39,7 @@
36
39
  "rolldown": "^1.2.12",
37
40
  "typescript": "^7.0.2",
38
41
  "vitest": "^5.0.3",
39
- "@statewalker/webrun-streams-conformance": "^0.3.1"
42
+ "@statewalker/webrun-streams-conformance": "^0.3.2"
40
43
  },
41
44
  "sideEffects": false,
42
45
  "publishConfig": {
@@ -9,6 +9,7 @@ import {
9
9
  toChunks,
10
10
  } from "@statewalker/webrun-streams";
11
11
  import { callPort, NO_TIMEOUT } from "./call-port.js";
12
+ import { getPortCloseSignal } from "./close-signal.js";
12
13
  import { listenPort } from "./listen-port.js";
13
14
  import type { MessageTarget } from "./message-target.js";
14
15
  import { throughAbort } from "./through-abort.js";
@@ -82,6 +83,7 @@ export function serveDuplexOverPort(
82
83
  ): () => void {
83
84
  const controller = new AbortController();
84
85
  const notice = installAbortNotice(port, controller);
86
+ abortOnPortClose(port, controller);
85
87
  const clock = installStreamTimeout(controller, options.timeout);
86
88
  const inbound = receiveChunks(port, CHANNEL_IN, controller, clock.touch);
87
89
  let output: AsyncGenerator<Uint8Array>;
@@ -166,6 +168,7 @@ function runCallerSide(
166
168
  ): AsyncGenerator<Uint8Array> {
167
169
  const controller = new AbortController();
168
170
  const notice = installAbortNotice(port, controller);
171
+ abortOnPortClose(port, controller);
169
172
  const clock = installStreamTimeout(controller, options.timeout);
170
173
  const inbound = receiveChunks(port, CHANNEL_OUT, controller, clock.touch);
171
174
  const pump = sendChunks(port, CHANNEL_IN, input, options, controller.signal, clock.touch);
@@ -234,6 +237,30 @@ function installAbortNotice(
234
237
  };
235
238
  }
236
239
 
240
+ /**
241
+ * Aborts the stream when its port closes underneath it — the multiplexer was
242
+ * closed, or the peer closed the port — for ports that report it (see
243
+ * `getPortCloseSignal`). Without this a stream waiting for the peer's next
244
+ * chunk would wait forever on a port that can no longer deliver one: the
245
+ * outbound half's `callPort` already fails on the close signal, but nothing
246
+ * failed the inbound half.
247
+ */
248
+ function abortOnPortClose(port: MessageTarget, controller: AbortController): void {
249
+ const closed = getPortCloseSignal(port);
250
+ if (!closed) return;
251
+ const onClose = () => {
252
+ if (!controller.signal.aborted) controller.abort(closed.reason);
253
+ };
254
+ if (closed.aborted) {
255
+ onClose();
256
+ return;
257
+ }
258
+ closed.addEventListener("abort", onClose, { once: true });
259
+ controller.signal.addEventListener("abort", () => closed.removeEventListener("abort", onClose), {
260
+ once: true,
261
+ });
262
+ }
263
+
237
264
  /**
238
265
  * The per-stream inactivity timeout (spec D8). Reset by any chunk in either
239
266
  * direction; elapsing aborts the stream. Unset, zero or non-finite installs no
package/src/index.ts CHANGED
@@ -21,7 +21,7 @@ export * from "./io-send.js";
21
21
  export * from "./listen-bidi.js";
22
22
  export * from "./listen-port.js";
23
23
  export * from "./message-target.js";
24
- export { DEFAULT_MAX_PORTS, multiplexPort } from "./multiplex-port.js";
24
+ export { DEFAULT_MAX_PORTS, MUX_CLOSED_MESSAGE, multiplexPort } from "./multiplex-port.js";
25
25
  export type { PortCodec, PortEnvelope, PortMux, PortMuxOptions } from "./port-types.js";
26
26
  export * from "./recieve.js";
27
27
  export * from "./send.js";
@@ -2,6 +2,12 @@ import type { MessageTarget } from "./message-target.js";
2
2
  import type { PortEnvelope, PortMux, PortMuxOptions } from "./port-types.js";
3
3
  import { newVirtualPort, type VirtualPortHandle } from "./virtual-port.js";
4
4
 
5
+ /**
6
+ * The message of the error that fails the calls still in flight on a
7
+ * multiplexer when it is closed (`PortMux.close()`).
8
+ */
9
+ export const MUX_CLOSED_MESSAGE = "webrun-rpc: the multiplexer was closed";
10
+
5
11
  /** Ceiling on concurrently open virtual ports. Bounds the id table only. */
6
12
  export const DEFAULT_MAX_PORTS = 1024;
7
13
 
@@ -70,7 +76,7 @@ export function multiplexPort(port: MessageTarget, options: PortMuxOptions): Por
70
76
  }
71
77
  if (!accepted) {
72
78
  open.delete(envelope.id);
73
- handle.markClosed();
79
+ handle.markClosed(new Error("webrun-rpc: the port was rejected"));
74
80
  post({ type: "close", id: envelope.id, reason: "rejected" });
75
81
  }
76
82
  return;
@@ -89,7 +95,7 @@ export function multiplexPort(port: MessageTarget, options: PortMuxOptions): Por
89
95
 
90
96
  if (!handle) return;
91
97
  open.delete(envelope.id);
92
- handle.markClosed();
98
+ handle.markClosed(new Error("webrun-rpc: the peer closed the port"));
93
99
  };
94
100
 
95
101
  const listener = (event: MessageEvent): void => {
@@ -124,7 +130,7 @@ export function multiplexPort(port: MessageTarget, options: PortMuxOptions): Por
124
130
  for (const [id, handle] of [...open]) {
125
131
  post({ type: "close", id });
126
132
  open.delete(id);
127
- handle.markClosed();
133
+ handle.markClosed(new Error(MUX_CLOSED_MESSAGE));
128
134
  }
129
135
  muxClosed = true;
130
136
  port.removeEventListener("message", listener);
@@ -1,3 +1,4 @@
1
+ import { setPortCloseSignal } from "./close-signal.js";
1
2
  import type { MessageListener, MessageTarget } from "./message-target.js";
2
3
 
3
4
  export interface VirtualPortHandle {
@@ -5,8 +6,12 @@ export interface VirtualPortHandle {
5
6
  port: MessageTarget;
6
7
  /** Multiplexer-only: hand an inbound payload to the consumer's listeners. */
7
8
  deliver(payload: unknown): void;
8
- /** Multiplexer-only: the port is finished; drop listeners and go inert. */
9
- markClosed(): void;
9
+ /**
10
+ * Multiplexer-only: the port is finished; drop listeners and go inert.
11
+ * `reason` becomes the reason of the port's close signal (see
12
+ * `getPortCloseSignal`), which is what fails the calls still waiting on it.
13
+ */
14
+ markClosed(reason?: unknown): void;
10
15
  isClosed(): boolean;
11
16
  }
12
17
 
@@ -23,6 +28,16 @@ export function newVirtualPort(
23
28
  ): VirtualPortHandle {
24
29
  const listeners = new Set<MessageListener>();
25
30
  let closed = false;
31
+ // A virtual port knows when it is finished, unlike a raw `MessagePort`. Its
32
+ // close signal is what lets the calls still waiting on it — `callPort`, and
33
+ // the streams of `duplexOverPort` — fail at once instead of hanging on a
34
+ // port that has gone inert. Without it, closing a multiplexer left every call
35
+ // in flight on it pending forever.
36
+ const closing = new AbortController();
37
+ const abort = (reason?: unknown) => {
38
+ if (closing.signal.aborted) return;
39
+ closing.abort(reason ?? new Error("webrun-rpc: the port was closed"));
40
+ };
26
41
 
27
42
  const port: MessageTarget = {
28
43
  addEventListener(_type, listener) {
@@ -41,8 +56,10 @@ export function newVirtualPort(
41
56
  const notify = requestClose;
42
57
  listeners.clear();
43
58
  notify();
59
+ abort();
44
60
  },
45
61
  };
62
+ setPortCloseSignal(port, closing.signal);
46
63
 
47
64
  return {
48
65
  port,
@@ -60,9 +77,10 @@ export function newVirtualPort(
60
77
  }
61
78
  }
62
79
  },
63
- markClosed() {
80
+ markClosed(reason) {
64
81
  closed = true;
65
82
  listeners.clear();
83
+ abort(reason);
66
84
  },
67
85
  isClosed() {
68
86
  return closed;