@statewalker/webrun-rpc 0.4.3 → 0.5.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.
- package/dist/async-dispose.d.ts +10 -0
- package/dist/async-dispose.d.ts.map +1 -0
- package/dist/connect-serve.d.ts.map +1 -1
- package/dist/duplex-over-port.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +148 -29
- package/dist/multiplex-port.d.ts +5 -0
- package/dist/multiplex-port.d.ts.map +1 -1
- package/dist/return-at-once.d.ts +17 -0
- package/dist/return-at-once.d.ts.map +1 -0
- package/dist/virtual-port.d.ts +6 -2
- package/dist/virtual-port.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/async-dispose.ts +22 -0
- package/src/close-signal.ts +1 -1
- package/src/connect-serve.ts +19 -3
- package/src/duplex-over-port.ts +36 -1
- package/src/index.ts +1 -1
- package/src/multiplex-port.ts +9 -3
- package/src/return-at-once.ts +51 -0
- package/src/virtual-port.ts +21 -3
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Makes `target` disposable with `await using`: installs `dispose` as its
|
|
3
|
+
* `[Symbol.asyncDispose]`, where the runtime defines the symbol. Hand-written
|
|
4
|
+
* iterators lose what a native generator gets from `AsyncIteratorPrototype`;
|
|
5
|
+
* this gives it back. A no-op where the symbol is missing. Kept off the
|
|
6
|
+
* declared types, which would require consumers to compile with the
|
|
7
|
+
* `esnext.disposable` lib.
|
|
8
|
+
*/
|
|
9
|
+
export declare function asyncDisposable<T extends object>(target: T, dispose: () => Promise<unknown>): T;
|
|
10
|
+
//# sourceMappingURL=async-dispose.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"async-dispose.d.ts","sourceRoot":"","sources":["../src/async-dispose.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,CAAC,SAAS,MAAM,EAAE,MAAM,EAAE,CAAC,EAAE,OAAO,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAa/F"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"connect-serve.d.ts","sourceRoot":"","sources":["../src/connect-serve.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAU,KAAK,EAAE,MAAM,6BAA6B,CAAC;AAM1E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEzD,OAAO,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"connect-serve.d.ts","sourceRoot":"","sources":["../src/connect-serve.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAU,KAAK,EAAE,MAAM,6BAA6B,CAAC;AAM1E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEzD,OAAO,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAW1D,uEAAuE;AACvE,MAAM,MAAM,MAAM,GAAG,CAAC,IAAI,EAAE,aAAa,EAAE,IAAI,CAAC,EAAE,OAAO,KAAK,OAAO,GAAG,SAAS,CAAC;AAElF;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,MAAM,cAAc,GAAG,CAAC,MAAM,CAAC,EAAE,MAAM,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;AAE7E,MAAM,WAAW,UAAU;IACzB;;;;;;;;;;OAUG;IACH,GAAG,EAAE,cAAc,CAAC;IACpB;;;yEAGqE;IACrE,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,OAAO,EAAE,OAAO,CAAC,UAAU,CAyCvC,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,KAAK,EAAE,KAAK,CAAC,UAAU,CAqDnC,CAAC;AAEF,MAAM,WAAW,eAAe;IAC9B,mFAAmF;IACnF,KAAK,EAAE,SAAS,CAAC;IACjB;;;;OAIG;IACH,IAAI,CAAC,EAAE,WAAW,GAAG,WAAW,CAAC;IACjC,oEAAoE;IACpE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;;;GAKG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,aAAa,EAAE,OAAO,EAAE,eAAe,GAAG,cAAc,CAEtF;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,cAAc,GAAG,cAAc,CAEjE"}
|
|
@@ -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;
|
|
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;AAIzD;;;;;;;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";
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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;
|
|
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
|
@@ -468,6 +468,73 @@ async function* callBidi(port, input, { options = {}, ...params } = {}) {
|
|
|
468
468
|
if (outerError !== void 0) throw outerError;
|
|
469
469
|
}
|
|
470
470
|
//#endregion
|
|
471
|
+
//#region src/async-dispose.ts
|
|
472
|
+
/**
|
|
473
|
+
* Makes `target` disposable with `await using`: installs `dispose` as its
|
|
474
|
+
* `[Symbol.asyncDispose]`, where the runtime defines the symbol. Hand-written
|
|
475
|
+
* iterators lose what a native generator gets from `AsyncIteratorPrototype`;
|
|
476
|
+
* this gives it back. A no-op where the symbol is missing. Kept off the
|
|
477
|
+
* declared types, which would require consumers to compile with the
|
|
478
|
+
* `esnext.disposable` lib.
|
|
479
|
+
*/
|
|
480
|
+
function asyncDisposable(target, dispose) {
|
|
481
|
+
const asyncDispose = Symbol.asyncDispose;
|
|
482
|
+
if (asyncDispose && !(asyncDispose in target)) Object.defineProperty(target, asyncDispose, {
|
|
483
|
+
value: async () => {
|
|
484
|
+
await dispose();
|
|
485
|
+
},
|
|
486
|
+
configurable: true,
|
|
487
|
+
writable: true
|
|
488
|
+
});
|
|
489
|
+
return target;
|
|
490
|
+
}
|
|
491
|
+
//#endregion
|
|
492
|
+
//#region src/return-at-once.ts
|
|
493
|
+
/**
|
|
494
|
+
* `gen`, except that `return()` and `throw()` take effect at once.
|
|
495
|
+
*
|
|
496
|
+
* On an async generator both are queued behind a `next()` that is still
|
|
497
|
+
* pending: a consumer that gives up on a stream while it waits for the peer's
|
|
498
|
+
* next chunk — an aborted fetch whose response head has not come, say — cannot
|
|
499
|
+
* end it until that chunk arrives, which for a peer that never answers is
|
|
500
|
+
* never. The stream's `finally` (which closes its port and tells the peer)
|
|
501
|
+
* then never runs, and on a multiplexer the port is kept for good.
|
|
502
|
+
*
|
|
503
|
+
* So `abandon` runs first, synchronously: it must make the pending `next()`
|
|
504
|
+
* settle (abort the stream), which then resolves done, after which the
|
|
505
|
+
* generator's own `return()` / `throw()` proceeds as usual. On a generator
|
|
506
|
+
* suspended at a `yield` it is the same ending, a step earlier. `abandon` runs once, whichever comes first.
|
|
507
|
+
*/
|
|
508
|
+
function returningAtOnce(gen, abandon) {
|
|
509
|
+
let abandoned = false;
|
|
510
|
+
const giveUp = (reason) => {
|
|
511
|
+
if (abandoned) return;
|
|
512
|
+
abandoned = true;
|
|
513
|
+
abandon(reason);
|
|
514
|
+
};
|
|
515
|
+
const wrapped = {
|
|
516
|
+
next: (...args) => gen.next(...args).catch((error) => {
|
|
517
|
+
if (abandoned) return {
|
|
518
|
+
done: true,
|
|
519
|
+
value: void 0
|
|
520
|
+
};
|
|
521
|
+
throw error;
|
|
522
|
+
}),
|
|
523
|
+
return(value) {
|
|
524
|
+
giveUp();
|
|
525
|
+
return gen.return(value);
|
|
526
|
+
},
|
|
527
|
+
throw(error) {
|
|
528
|
+
giveUp(error);
|
|
529
|
+
return gen.throw(error);
|
|
530
|
+
},
|
|
531
|
+
[Symbol.asyncIterator]() {
|
|
532
|
+
return wrapped;
|
|
533
|
+
}
|
|
534
|
+
};
|
|
535
|
+
return asyncDisposable(wrapped, () => wrapped.return(void 0));
|
|
536
|
+
}
|
|
537
|
+
//#endregion
|
|
471
538
|
//#region src/duplex-over-port.ts
|
|
472
539
|
/**
|
|
473
540
|
* The `type` of the out-of-band notice a side posts when it abandons a stream.
|
|
@@ -505,6 +572,7 @@ function duplexOverPort(port, options = {}) {
|
|
|
505
572
|
function serveDuplexOverPort(port, handler, options = {}) {
|
|
506
573
|
const controller = new AbortController();
|
|
507
574
|
const notice = installAbortNotice(port, controller);
|
|
575
|
+
abortOnPortClose(port, controller);
|
|
508
576
|
const clock = installStreamTimeout(controller, options.timeout);
|
|
509
577
|
const inbound = receiveChunks(port, CHANNEL_IN, controller, clock.touch);
|
|
510
578
|
let output;
|
|
@@ -559,11 +627,12 @@ function teardownOnce(controller, notice, clock, inbound, output) {
|
|
|
559
627
|
function runCallerSide(port, input, options) {
|
|
560
628
|
const controller = new AbortController();
|
|
561
629
|
const notice = installAbortNotice(port, controller);
|
|
630
|
+
abortOnPortClose(port, controller);
|
|
562
631
|
const clock = installStreamTimeout(controller, options.timeout);
|
|
563
632
|
const inbound = receiveChunks(port, CHANNEL_OUT, controller, clock.touch);
|
|
564
633
|
const pump = sendChunks(port, CHANNEL_IN, input, options, controller.signal, clock.touch);
|
|
565
634
|
pump.catch(() => {});
|
|
566
|
-
return (async function* () {
|
|
635
|
+
return returningAtOnce((async function* () {
|
|
567
636
|
try {
|
|
568
637
|
yield* inbound.stream;
|
|
569
638
|
} finally {
|
|
@@ -577,7 +646,9 @@ function runCallerSide(port, input, options) {
|
|
|
577
646
|
await port.close?.();
|
|
578
647
|
} catch {}
|
|
579
648
|
}
|
|
580
|
-
})()
|
|
649
|
+
})(), () => {
|
|
650
|
+
if (!controller.signal.aborted) controller.abort(/* @__PURE__ */ new Error("webrun-rpc: the caller abandoned the stream"));
|
|
651
|
+
});
|
|
581
652
|
}
|
|
582
653
|
/**
|
|
583
654
|
* Listens for the peer's abort notice, and can post our own. The notice is a
|
|
@@ -607,6 +678,27 @@ function installAbortNotice(port, controller) {
|
|
|
607
678
|
};
|
|
608
679
|
}
|
|
609
680
|
/**
|
|
681
|
+
* Aborts the stream when its port closes underneath it — the multiplexer was
|
|
682
|
+
* closed, or the peer closed the port — for ports that report it (see
|
|
683
|
+
* `getPortCloseSignal`). Without this a stream waiting for the peer's next
|
|
684
|
+
* chunk would wait forever on a port that can no longer deliver one: the
|
|
685
|
+
* outbound half's `callPort` already fails on the close signal, but nothing
|
|
686
|
+
* failed the inbound half.
|
|
687
|
+
*/
|
|
688
|
+
function abortOnPortClose(port, controller) {
|
|
689
|
+
const closed = getPortCloseSignal(port);
|
|
690
|
+
if (!closed) return;
|
|
691
|
+
const onClose = () => {
|
|
692
|
+
if (!controller.signal.aborted) controller.abort(closed.reason);
|
|
693
|
+
};
|
|
694
|
+
if (closed.aborted) {
|
|
695
|
+
onClose();
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
closed.addEventListener("abort", onClose, { once: true });
|
|
699
|
+
controller.signal.addEventListener("abort", () => closed.removeEventListener("abort", onClose), { once: true });
|
|
700
|
+
}
|
|
701
|
+
/**
|
|
610
702
|
* The per-stream inactivity timeout (spec D8). Reset by any chunk in either
|
|
611
703
|
* direction; elapsing aborts the stream. Unset, zero or non-finite installs no
|
|
612
704
|
* timer at all, which is the default: a slow consumer is throttled, not failed.
|
|
@@ -768,26 +860,34 @@ async function sendChunks(port, channelName, output, { maxMessageSize, log }, si
|
|
|
768
860
|
function newVirtualPort(send, requestClose) {
|
|
769
861
|
const listeners = /* @__PURE__ */ new Set();
|
|
770
862
|
let closed = false;
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
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
|
-
}
|
|
863
|
+
const closing = new AbortController();
|
|
864
|
+
const abort = (reason) => {
|
|
865
|
+
if (closing.signal.aborted) return;
|
|
866
|
+
closing.abort(reason ?? /* @__PURE__ */ new Error("webrun-rpc: the port was closed"));
|
|
867
|
+
};
|
|
868
|
+
const port = {
|
|
869
|
+
addEventListener(_type, listener) {
|
|
870
|
+
listeners.add(listener);
|
|
790
871
|
},
|
|
872
|
+
removeEventListener(_type, listener) {
|
|
873
|
+
listeners.delete(listener);
|
|
874
|
+
},
|
|
875
|
+
postMessage(message, transfer) {
|
|
876
|
+
if (closed) return;
|
|
877
|
+
send(message, transfer);
|
|
878
|
+
},
|
|
879
|
+
close() {
|
|
880
|
+
if (closed) return;
|
|
881
|
+
closed = true;
|
|
882
|
+
const notify = requestClose;
|
|
883
|
+
listeners.clear();
|
|
884
|
+
notify();
|
|
885
|
+
abort();
|
|
886
|
+
}
|
|
887
|
+
};
|
|
888
|
+
setPortCloseSignal(port, closing.signal);
|
|
889
|
+
return {
|
|
890
|
+
port,
|
|
791
891
|
deliver(payload) {
|
|
792
892
|
if (closed) return;
|
|
793
893
|
const event = new MessageEvent("message", { data: payload });
|
|
@@ -795,9 +895,10 @@ function newVirtualPort(send, requestClose) {
|
|
|
795
895
|
listener(event);
|
|
796
896
|
} catch {}
|
|
797
897
|
},
|
|
798
|
-
markClosed() {
|
|
898
|
+
markClosed(reason) {
|
|
799
899
|
closed = true;
|
|
800
900
|
listeners.clear();
|
|
901
|
+
abort(reason);
|
|
801
902
|
},
|
|
802
903
|
isClosed() {
|
|
803
904
|
return closed;
|
|
@@ -806,6 +907,11 @@ function newVirtualPort(send, requestClose) {
|
|
|
806
907
|
}
|
|
807
908
|
//#endregion
|
|
808
909
|
//#region src/multiplex-port.ts
|
|
910
|
+
/**
|
|
911
|
+
* The message of the error that fails the calls still in flight on a
|
|
912
|
+
* multiplexer when it is closed (`PortMux.close()`).
|
|
913
|
+
*/
|
|
914
|
+
const MUX_CLOSED_MESSAGE = "webrun-rpc: the multiplexer was closed";
|
|
809
915
|
/** Ceiling on concurrently open virtual ports. Bounds the id table only. */
|
|
810
916
|
const DEFAULT_MAX_PORTS = 1024;
|
|
811
917
|
/**
|
|
@@ -864,7 +970,7 @@ function multiplexPort(port, options) {
|
|
|
864
970
|
}
|
|
865
971
|
if (!accepted) {
|
|
866
972
|
open.delete(envelope.id);
|
|
867
|
-
handle.markClosed();
|
|
973
|
+
handle.markClosed(/* @__PURE__ */ new Error("webrun-rpc: the port was rejected"));
|
|
868
974
|
post({
|
|
869
975
|
type: "close",
|
|
870
976
|
id: envelope.id,
|
|
@@ -881,7 +987,7 @@ function multiplexPort(port, options) {
|
|
|
881
987
|
}
|
|
882
988
|
if (!handle) return;
|
|
883
989
|
open.delete(envelope.id);
|
|
884
|
-
handle.markClosed();
|
|
990
|
+
handle.markClosed(/* @__PURE__ */ new Error("webrun-rpc: the peer closed the port"));
|
|
885
991
|
};
|
|
886
992
|
const listener = (event) => {
|
|
887
993
|
const envelope = codec.read(event);
|
|
@@ -913,7 +1019,7 @@ function multiplexPort(port, options) {
|
|
|
913
1019
|
id
|
|
914
1020
|
});
|
|
915
1021
|
open.delete(id);
|
|
916
|
-
handle.markClosed();
|
|
1022
|
+
handle.markClosed(/* @__PURE__ */ new Error(MUX_CLOSED_MESSAGE));
|
|
917
1023
|
}
|
|
918
1024
|
muxClosed = true;
|
|
919
1025
|
port.removeEventListener("message", listener);
|
|
@@ -944,9 +1050,22 @@ const connect = async ({ mux: factory, timeout }) => {
|
|
|
944
1050
|
maxMessageSize: mux.maxMessageSize,
|
|
945
1051
|
timeout
|
|
946
1052
|
};
|
|
947
|
-
const call = (input) =>
|
|
948
|
-
|
|
949
|
-
|
|
1053
|
+
const call = (input) => {
|
|
1054
|
+
let stream;
|
|
1055
|
+
let abandoned = false;
|
|
1056
|
+
return returningAtOnce((async function* () {
|
|
1057
|
+
const port = await mux.openPort(STREAM_META);
|
|
1058
|
+
if (abandoned) {
|
|
1059
|
+
await port.close?.();
|
|
1060
|
+
return;
|
|
1061
|
+
}
|
|
1062
|
+
stream = duplexOverPort(port, streamOptions)(input);
|
|
1063
|
+
yield* stream;
|
|
1064
|
+
})(), () => {
|
|
1065
|
+
abandoned = true;
|
|
1066
|
+
stream?.return(void 0).catch(() => {});
|
|
1067
|
+
});
|
|
1068
|
+
};
|
|
950
1069
|
return {
|
|
951
1070
|
call,
|
|
952
1071
|
async close() {
|
|
@@ -1177,4 +1296,4 @@ function transferPortMux(target, options = {}) {
|
|
|
1177
1296
|
};
|
|
1178
1297
|
}
|
|
1179
1298
|
//#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 };
|
|
1299
|
+
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 };
|
package/dist/multiplex-port.d.ts
CHANGED
|
@@ -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"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `gen`, except that `return()` and `throw()` take effect at once.
|
|
3
|
+
*
|
|
4
|
+
* On an async generator both are queued behind a `next()` that is still
|
|
5
|
+
* pending: a consumer that gives up on a stream while it waits for the peer's
|
|
6
|
+
* next chunk — an aborted fetch whose response head has not come, say — cannot
|
|
7
|
+
* end it until that chunk arrives, which for a peer that never answers is
|
|
8
|
+
* never. The stream's `finally` (which closes its port and tells the peer)
|
|
9
|
+
* then never runs, and on a multiplexer the port is kept for good.
|
|
10
|
+
*
|
|
11
|
+
* So `abandon` runs first, synchronously: it must make the pending `next()`
|
|
12
|
+
* settle (abort the stream), which then resolves done, after which the
|
|
13
|
+
* generator's own `return()` / `throw()` proceeds as usual. On a generator
|
|
14
|
+
* suspended at a `yield` it is the same ending, a step earlier. `abandon` runs once, whichever comes first.
|
|
15
|
+
*/
|
|
16
|
+
export declare function returningAtOnce<T>(gen: AsyncGenerator<T>, abandon: (reason?: unknown) => void): AsyncGenerator<T>;
|
|
17
|
+
//# sourceMappingURL=return-at-once.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"return-at-once.d.ts","sourceRoot":"","sources":["../src/return-at-once.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAC/B,GAAG,EAAE,cAAc,CAAC,CAAC,CAAC,EACtB,OAAO,EAAE,CAAC,MAAM,CAAC,EAAE,OAAO,KAAK,IAAI,GAClC,cAAc,CAAC,CAAC,CAAC,CA8BnB"}
|
package/dist/virtual-port.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
8
|
-
|
|
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":"
|
|
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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@statewalker/webrun-rpc",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
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.",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"src"
|
|
31
31
|
],
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@statewalker/webrun-streams": "^0.2.
|
|
33
|
+
"@statewalker/webrun-streams": "^0.2.3"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"@biomejs/biome": "^2.5.15",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"rolldown": "^1.2.12",
|
|
40
40
|
"typescript": "^7.0.2",
|
|
41
41
|
"vitest": "^5.0.3",
|
|
42
|
-
"@statewalker/webrun-streams-conformance": "^0.3.
|
|
42
|
+
"@statewalker/webrun-streams-conformance": "^0.3.3"
|
|
43
43
|
},
|
|
44
44
|
"sideEffects": false,
|
|
45
45
|
"publishConfig": {
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Makes `target` disposable with `await using`: installs `dispose` as its
|
|
3
|
+
* `[Symbol.asyncDispose]`, where the runtime defines the symbol. Hand-written
|
|
4
|
+
* iterators lose what a native generator gets from `AsyncIteratorPrototype`;
|
|
5
|
+
* this gives it back. A no-op where the symbol is missing. Kept off the
|
|
6
|
+
* declared types, which would require consumers to compile with the
|
|
7
|
+
* `esnext.disposable` lib.
|
|
8
|
+
*/
|
|
9
|
+
export function asyncDisposable<T extends object>(target: T, dispose: () => Promise<unknown>): T {
|
|
10
|
+
// Read here, not at module level: a top-level read is code a bundler keeps.
|
|
11
|
+
const asyncDispose = (Symbol as { asyncDispose?: symbol }).asyncDispose;
|
|
12
|
+
if (asyncDispose && !(asyncDispose in target)) {
|
|
13
|
+
Object.defineProperty(target, asyncDispose, {
|
|
14
|
+
value: async () => {
|
|
15
|
+
await dispose();
|
|
16
|
+
},
|
|
17
|
+
configurable: true,
|
|
18
|
+
writable: true,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
return target;
|
|
22
|
+
}
|
package/src/close-signal.ts
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
*/
|
|
23
23
|
import type { MessageTarget } from "./message-target.js";
|
|
24
24
|
|
|
25
|
-
const closeSignals = new WeakMap<MessageTarget, AbortSignal>();
|
|
25
|
+
const closeSignals = /* @__PURE__ */ new WeakMap<MessageTarget, AbortSignal>();
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
28
|
* Internal: register a port's close signal. Called once by `bindBytesToPort`.
|
package/src/connect-serve.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
import type { MessageTarget } from "./message-target.js";
|
|
8
8
|
import { multiplexPort } from "./multiplex-port.js";
|
|
9
9
|
import type { PortCodec, PortMux } from "./port-types.js";
|
|
10
|
+
import { returningAtOnce } from "./return-at-once.js";
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* Announced with every port this pair opens. Layer 1 never reads `meta` — it is
|
|
@@ -74,19 +75,34 @@ export const connect: Connect<PortParams> = async ({ mux: factory, timeout }) =>
|
|
|
74
75
|
const mux = await factory();
|
|
75
76
|
const streamOptions: DuplexOverPortOptions = { maxMessageSize: mux.maxMessageSize, timeout };
|
|
76
77
|
|
|
77
|
-
const call: Duplex = (input) =>
|
|
78
|
+
const call: Duplex = (input) => {
|
|
79
|
+
let stream: AsyncGenerator<Uint8Array> | undefined;
|
|
80
|
+
let abandoned = false;
|
|
78
81
|
// The port is opened on the consumer's first pull, not when `call` is
|
|
79
82
|
// invoked. A caller that builds a stream and then drops it without
|
|
80
83
|
// iterating therefore costs the peer nothing — under eager opening it
|
|
81
84
|
// would burn a port (and over libp2p, a whole stream) for a call that
|
|
82
85
|
// never arrives.
|
|
83
|
-
(async function* () {
|
|
86
|
+
const outer = (async function* () {
|
|
84
87
|
const port = await mux.openPort(STREAM_META);
|
|
88
|
+
if (abandoned) {
|
|
89
|
+
// Given up while the port was being opened.
|
|
90
|
+
await port.close?.();
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
stream = duplexOverPort(port, streamOptions)(input);
|
|
85
94
|
// `yield*` and not a hand-rolled pump: delegation already forwards
|
|
86
95
|
// `return()` and `throw()` into `duplexOverPort`'s generator, which is
|
|
87
96
|
// exactly the cancellation path `Duplex` specifies.
|
|
88
|
-
yield*
|
|
97
|
+
yield* stream;
|
|
89
98
|
})();
|
|
99
|
+
// Delegation forwards them only once the pending `next()` settles, though:
|
|
100
|
+
// giving up while the peer has not answered must reach the stream now.
|
|
101
|
+
return returningAtOnce(outer, () => {
|
|
102
|
+
abandoned = true;
|
|
103
|
+
void stream?.return(undefined).catch(() => {});
|
|
104
|
+
});
|
|
105
|
+
};
|
|
90
106
|
|
|
91
107
|
return {
|
|
92
108
|
call,
|
package/src/duplex-over-port.ts
CHANGED
|
@@ -9,8 +9,10 @@ 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";
|
|
15
|
+
import { returningAtOnce } from "./return-at-once.js";
|
|
14
16
|
import { throughAbort } from "./through-abort.js";
|
|
15
17
|
|
|
16
18
|
/**
|
|
@@ -82,6 +84,7 @@ export function serveDuplexOverPort(
|
|
|
82
84
|
): () => void {
|
|
83
85
|
const controller = new AbortController();
|
|
84
86
|
const notice = installAbortNotice(port, controller);
|
|
87
|
+
abortOnPortClose(port, controller);
|
|
85
88
|
const clock = installStreamTimeout(controller, options.timeout);
|
|
86
89
|
const inbound = receiveChunks(port, CHANNEL_IN, controller, clock.touch);
|
|
87
90
|
let output: AsyncGenerator<Uint8Array>;
|
|
@@ -166,6 +169,7 @@ function runCallerSide(
|
|
|
166
169
|
): AsyncGenerator<Uint8Array> {
|
|
167
170
|
const controller = new AbortController();
|
|
168
171
|
const notice = installAbortNotice(port, controller);
|
|
172
|
+
abortOnPortClose(port, controller);
|
|
169
173
|
const clock = installStreamTimeout(controller, options.timeout);
|
|
170
174
|
const inbound = receiveChunks(port, CHANNEL_OUT, controller, clock.touch);
|
|
171
175
|
const pump = sendChunks(port, CHANNEL_IN, input, options, controller.signal, clock.touch);
|
|
@@ -174,7 +178,7 @@ function runCallerSide(
|
|
|
174
178
|
// the consumer's contract is the inbound half.
|
|
175
179
|
});
|
|
176
180
|
|
|
177
|
-
|
|
181
|
+
const stream = (async function* () {
|
|
178
182
|
try {
|
|
179
183
|
yield* inbound.stream;
|
|
180
184
|
} finally {
|
|
@@ -197,6 +201,13 @@ function runCallerSide(
|
|
|
197
201
|
}
|
|
198
202
|
}
|
|
199
203
|
})();
|
|
204
|
+
// A caller that gives up while it waits for the peer's next chunk ends the
|
|
205
|
+
// stream now: the abort settles that wait, and the `finally` above runs.
|
|
206
|
+
return returningAtOnce(stream, () => {
|
|
207
|
+
if (!controller.signal.aborted) {
|
|
208
|
+
controller.abort(new Error("webrun-rpc: the caller abandoned the stream"));
|
|
209
|
+
}
|
|
210
|
+
});
|
|
200
211
|
}
|
|
201
212
|
|
|
202
213
|
/**
|
|
@@ -234,6 +245,30 @@ function installAbortNotice(
|
|
|
234
245
|
};
|
|
235
246
|
}
|
|
236
247
|
|
|
248
|
+
/**
|
|
249
|
+
* Aborts the stream when its port closes underneath it — the multiplexer was
|
|
250
|
+
* closed, or the peer closed the port — for ports that report it (see
|
|
251
|
+
* `getPortCloseSignal`). Without this a stream waiting for the peer's next
|
|
252
|
+
* chunk would wait forever on a port that can no longer deliver one: the
|
|
253
|
+
* outbound half's `callPort` already fails on the close signal, but nothing
|
|
254
|
+
* failed the inbound half.
|
|
255
|
+
*/
|
|
256
|
+
function abortOnPortClose(port: MessageTarget, controller: AbortController): void {
|
|
257
|
+
const closed = getPortCloseSignal(port);
|
|
258
|
+
if (!closed) return;
|
|
259
|
+
const onClose = () => {
|
|
260
|
+
if (!controller.signal.aborted) controller.abort(closed.reason);
|
|
261
|
+
};
|
|
262
|
+
if (closed.aborted) {
|
|
263
|
+
onClose();
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
closed.addEventListener("abort", onClose, { once: true });
|
|
267
|
+
controller.signal.addEventListener("abort", () => closed.removeEventListener("abort", onClose), {
|
|
268
|
+
once: true,
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
|
|
237
272
|
/**
|
|
238
273
|
* The per-stream inactivity timeout (spec D8). Reset by any chunk in either
|
|
239
274
|
* 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";
|
package/src/multiplex-port.ts
CHANGED
|
@@ -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);
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { asyncDisposable } from "./async-dispose.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `gen`, except that `return()` and `throw()` take effect at once.
|
|
5
|
+
*
|
|
6
|
+
* On an async generator both are queued behind a `next()` that is still
|
|
7
|
+
* pending: a consumer that gives up on a stream while it waits for the peer's
|
|
8
|
+
* next chunk — an aborted fetch whose response head has not come, say — cannot
|
|
9
|
+
* end it until that chunk arrives, which for a peer that never answers is
|
|
10
|
+
* never. The stream's `finally` (which closes its port and tells the peer)
|
|
11
|
+
* then never runs, and on a multiplexer the port is kept for good.
|
|
12
|
+
*
|
|
13
|
+
* So `abandon` runs first, synchronously: it must make the pending `next()`
|
|
14
|
+
* settle (abort the stream), which then resolves done, after which the
|
|
15
|
+
* generator's own `return()` / `throw()` proceeds as usual. On a generator
|
|
16
|
+
* suspended at a `yield` it is the same ending, a step earlier. `abandon` runs once, whichever comes first.
|
|
17
|
+
*/
|
|
18
|
+
export function returningAtOnce<T>(
|
|
19
|
+
gen: AsyncGenerator<T>,
|
|
20
|
+
abandon: (reason?: unknown) => void,
|
|
21
|
+
): AsyncGenerator<T> {
|
|
22
|
+
let abandoned = false;
|
|
23
|
+
const giveUp = (reason?: unknown) => {
|
|
24
|
+
if (abandoned) return;
|
|
25
|
+
abandoned = true;
|
|
26
|
+
abandon(reason);
|
|
27
|
+
};
|
|
28
|
+
const wrapped: AsyncGenerator<T> = {
|
|
29
|
+
// A `next()` that was pending when the consumer gave up ends as the
|
|
30
|
+
// stream does — done — rather than rejecting with the abort it caused: a
|
|
31
|
+
// consumer that raced it against a timer may hold no handler for it.
|
|
32
|
+
next: (...args: [] | [unknown]) =>
|
|
33
|
+
gen.next(...args).catch((error: unknown) => {
|
|
34
|
+
if (abandoned) return { done: true, value: undefined } as IteratorResult<T>;
|
|
35
|
+
throw error;
|
|
36
|
+
}),
|
|
37
|
+
return(value) {
|
|
38
|
+
giveUp();
|
|
39
|
+
return gen.return(value);
|
|
40
|
+
},
|
|
41
|
+
throw(error) {
|
|
42
|
+
giveUp(error);
|
|
43
|
+
return gen.throw(error);
|
|
44
|
+
},
|
|
45
|
+
[Symbol.asyncIterator]() {
|
|
46
|
+
return wrapped;
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
// `await using` ends the stream as leaving a `for await` early does.
|
|
50
|
+
return asyncDisposable(wrapped, () => wrapped.return(undefined));
|
|
51
|
+
}
|
package/src/virtual-port.ts
CHANGED
|
@@ -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
|
-
/**
|
|
9
|
-
|
|
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;
|