@statewalker/webrun-rpc 0.7.0 → 0.10.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.
- package/README.md +189 -12
- package/dist/connect-serve.d.ts +2 -0
- package/dist/connect-serve.d.ts.map +1 -1
- package/dist/duplex-over-port.d.ts +3 -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 +496 -101
- package/dist/multiplex-port.d.ts +2 -0
- package/dist/multiplex-port.d.ts.map +1 -1
- package/dist/port-credit.d.ts +51 -0
- package/dist/port-credit.d.ts.map +1 -1
- package/dist/port-types.d.ts +37 -2
- package/dist/port-types.d.ts.map +1 -1
- package/dist/structured-codec.d.ts.map +1 -1
- package/package.json +5 -1
- package/src/connect-serve.ts +20 -9
- package/src/duplex-over-port.ts +345 -40
- package/src/index.ts +6 -1
- package/src/multiplex-port.ts +238 -21
- package/src/port-credit.ts +86 -0
- package/src/port-types.ts +32 -3
- package/src/structured-codec.ts +5 -1
package/dist/index.js
CHANGED
|
@@ -471,6 +471,101 @@ async function* callBidi(port, input, { options = {}, ...params } = {}) {
|
|
|
471
471
|
if (outerError !== void 0) throw outerError;
|
|
472
472
|
}
|
|
473
473
|
//#endregion
|
|
474
|
+
//#region src/port-credit.ts
|
|
475
|
+
/**
|
|
476
|
+
* Runs `fn` on a later task, never a microtask: a producer looping on
|
|
477
|
+
* `whenPortReady` must let the transport's own events run. `setImmediate`
|
|
478
|
+
* where it exists; elsewhere a `MessageChannel`, because nested `setTimeout`s
|
|
479
|
+
* are clamped to 4 ms in browsers.
|
|
480
|
+
*/
|
|
481
|
+
const setImmediateFn = globalThis.setImmediate;
|
|
482
|
+
/** @internal */
|
|
483
|
+
const nextTask = setImmediateFn ? (fn) => {
|
|
484
|
+
setImmediateFn(fn);
|
|
485
|
+
} : (() => {
|
|
486
|
+
let channel;
|
|
487
|
+
const queue = [];
|
|
488
|
+
return (fn) => {
|
|
489
|
+
if (!channel) {
|
|
490
|
+
channel = new MessageChannel();
|
|
491
|
+
channel.port1.onmessage = () => queue.shift()?.();
|
|
492
|
+
}
|
|
493
|
+
queue.push(fn);
|
|
494
|
+
channel.port2.postMessage(0);
|
|
495
|
+
};
|
|
496
|
+
})();
|
|
497
|
+
const states = /* @__PURE__ */ new WeakMap();
|
|
498
|
+
/** @internal Registered by the multiplexer for each port with a window. */
|
|
499
|
+
function setPortSendState(port, state) {
|
|
500
|
+
states.set(port, state);
|
|
501
|
+
}
|
|
502
|
+
/**
|
|
503
|
+
* Bytes queued on `port` because its window is spent (0 for a port without
|
|
504
|
+
* flow control). A sender that should not queue without bound waits for
|
|
505
|
+
* `whenPortReady` before posting more.
|
|
506
|
+
*/
|
|
507
|
+
function portBufferedAmount(port) {
|
|
508
|
+
return states.get(port)?.bufferedAmount() ?? 0;
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* Resolves once at most `below` bytes are queued on `port`, always on a later
|
|
512
|
+
* task (for a port without flow control, on the next one).
|
|
513
|
+
*/
|
|
514
|
+
function whenPortReady(port, below = 0) {
|
|
515
|
+
return states.get(port)?.ready(below) ?? new Promise((resolve) => nextTask(resolve));
|
|
516
|
+
}
|
|
517
|
+
const agreements = /* @__PURE__ */ new WeakMap();
|
|
518
|
+
/** @internal Registered by the multiplexer for each port with a window. */
|
|
519
|
+
function setPortAgreement(port, agreement) {
|
|
520
|
+
agreements.set(port, agreement);
|
|
521
|
+
}
|
|
522
|
+
/**
|
|
523
|
+
* @internal What has been agreed with the peer on `port` so far; `undefined`
|
|
524
|
+
* on a port without a window. Not exported from the package.
|
|
525
|
+
*/
|
|
526
|
+
function portAgreement(port) {
|
|
527
|
+
return agreements.get(port)?.();
|
|
528
|
+
}
|
|
529
|
+
const ends = /* @__PURE__ */ new WeakMap();
|
|
530
|
+
/** @internal Registered by the multiplexer for each port with a window. */
|
|
531
|
+
function setPortEndState(port, state) {
|
|
532
|
+
ends.set(port, state);
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* @internal End this side's direction of `port`: an `end` is queued behind
|
|
536
|
+
* everything already posted on it, and anything posted afterwards is dropped.
|
|
537
|
+
* `false`, and nothing changes, when `end` was not agreed (no window, the
|
|
538
|
+
* handshake not answered yet, or a peer that does not speak it): the caller
|
|
539
|
+
* then closes the port. Never waits on the peer. Not exported from the package.
|
|
540
|
+
*/
|
|
541
|
+
function endPort(port) {
|
|
542
|
+
return ends.get(port)?.end() ?? false;
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* @internal Resolves once the peer has ended its direction of `port`. It never
|
|
546
|
+
* settles otherwise: not on a port where `end` is not agreed when it is asked
|
|
547
|
+
* (no window, the handshake not answered yet, a peer or codec without it), and
|
|
548
|
+
* not when the port closes first, so a caller races it against the port's
|
|
549
|
+
* close signal. Not exported from the package.
|
|
550
|
+
*/
|
|
551
|
+
function whenPortEnded(port) {
|
|
552
|
+
let waiting = peerEnds.get(port);
|
|
553
|
+
if (!waiting) {
|
|
554
|
+
let resolve = () => {};
|
|
555
|
+
const promise = new Promise((r) => {
|
|
556
|
+
resolve = r;
|
|
557
|
+
});
|
|
558
|
+
if (!ends.get(port)?.watchPeerEnd(resolve)) return promise;
|
|
559
|
+
waiting = {
|
|
560
|
+
promise,
|
|
561
|
+
resolve
|
|
562
|
+
};
|
|
563
|
+
peerEnds.set(port, waiting);
|
|
564
|
+
}
|
|
565
|
+
return waiting.promise;
|
|
566
|
+
}
|
|
567
|
+
const peerEnds = /* @__PURE__ */ new WeakMap();
|
|
568
|
+
//#endregion
|
|
474
569
|
//#region src/async-dispose.ts
|
|
475
570
|
/**
|
|
476
571
|
* Makes `target` disposable with `await using`: installs `dispose` as its
|
|
@@ -548,6 +643,61 @@ function returningAtOnce(gen, abandon) {
|
|
|
548
643
|
* tests and adapters assert on it.
|
|
549
644
|
*/
|
|
550
645
|
const STREAM_ABORT = "webrun-rpc:stream-abort";
|
|
646
|
+
/**
|
|
647
|
+
* The `type` of a pipelined chunk: a plain port message, not a `callPort`
|
|
648
|
+
* request, sent only on a port where both ends agreed `pipeline`. The window's
|
|
649
|
+
* credit is its confirmation.
|
|
650
|
+
*/
|
|
651
|
+
const CHUNK = "webrun-rpc:chunk";
|
|
652
|
+
/**
|
|
653
|
+
* What a pipelined chunk counts beyond its bytes, against the window, on the
|
|
654
|
+
* receiving side. Both codecs' windows count more for the chunk's envelope
|
|
655
|
+
* (the estimate at least 64, msgpack's keys alone more than this), so an honest
|
|
656
|
+
* sender never makes the receiver count past the window; a sender of empty
|
|
657
|
+
* chunks still cannot have it hold an unbounded number of them.
|
|
658
|
+
*/
|
|
659
|
+
const CHUNK_OVERHEAD = 32;
|
|
660
|
+
/**
|
|
661
|
+
* The bytes of `view` as a plain `Uint8Array` that owns its whole buffer:
|
|
662
|
+
* `view` itself when it already is one, else a copy. Structured clone carries
|
|
663
|
+
* and keeps a view's whole `ArrayBuffer`, and a byte codec may decode into
|
|
664
|
+
* views of the frame, while the window and the receiver's count see only the
|
|
665
|
+
* view: a 4 KiB `subarray` of a 4 MiB buffer would cost 4 MiB per chunk.
|
|
666
|
+
* Copied with the `Uint8Array` constructor, not `slice()`: a Node `Buffer`'s
|
|
667
|
+
* `slice()` is another view on the same memory.
|
|
668
|
+
*/
|
|
669
|
+
function ownBytes(view) {
|
|
670
|
+
if (view instanceof Uint8Array && view.constructor === Uint8Array && view.byteOffset === 0 && view.byteLength === view.buffer.byteLength) return view;
|
|
671
|
+
return new Uint8Array(new Uint8Array(view.buffer, view.byteOffset, view.byteLength));
|
|
672
|
+
}
|
|
673
|
+
/**
|
|
674
|
+
* A producer's chunk as bytes this end may send, in either mode: any view or
|
|
675
|
+
* `ArrayBuffer`, normalised by `ownBytes`. Anything else fails the stream here,
|
|
676
|
+
* as the producer's own error, rather than reaching a peer that would refuse it.
|
|
677
|
+
*/
|
|
678
|
+
/**
|
|
679
|
+
* A received confirmed chunk's value, as its own bytes, put back in place of
|
|
680
|
+
* the original in `chunk`: `listenPort` keeps the request until the handler
|
|
681
|
+
* returns, and the original would keep its whole buffer that long. A function
|
|
682
|
+
* of its own, so that no temporary of the suspended handler holds the
|
|
683
|
+
* original either.
|
|
684
|
+
*/
|
|
685
|
+
function adoptValue(chunk) {
|
|
686
|
+
const original = chunk.value;
|
|
687
|
+
if (ArrayBuffer.isView(original)) chunk.value = ownBytes(original);
|
|
688
|
+
return chunk.value;
|
|
689
|
+
}
|
|
690
|
+
function chunkBytes(value) {
|
|
691
|
+
if (ArrayBuffer.isView(value)) return ownBytes(value);
|
|
692
|
+
if (value instanceof ArrayBuffer || Object.prototype.toString.call(value) === "[object ArrayBuffer]") return new Uint8Array(value);
|
|
693
|
+
throw new TypeError(`webrun-rpc: a stream chunk must be bytes (a typed array, a DataView or an ArrayBuffer), got ${value === null ? "null" : typeof value}`);
|
|
694
|
+
}
|
|
695
|
+
const MIXED_MESSAGE = "webrun-rpc: peer mixed confirmed and pipelined chunks; the stream port is closed";
|
|
696
|
+
/** This end may pipeline its own chunks: agreed, and the window holds them. */
|
|
697
|
+
const mayPipeline = (port) => {
|
|
698
|
+
const agreement = portAgreement(port);
|
|
699
|
+
return !!agreement?.enforced && agreement.caps.has("pipeline");
|
|
700
|
+
};
|
|
551
701
|
/** Caller's input travels on this channel; the handler listens on it. */
|
|
552
702
|
const CHANNEL_IN = "in";
|
|
553
703
|
/** The handler's output travels on this channel; the caller listens on it. */
|
|
@@ -560,7 +710,9 @@ const CHANNEL_OUT = "out";
|
|
|
560
710
|
* same way on the other channel. Within each direction the next chunk is never
|
|
561
711
|
* sent until the previous one has been delivered *and* pulled past by the
|
|
562
712
|
* consumer (spec D11) — the reply to a chunk call *is* the confirmation, and
|
|
563
|
-
* `listenPort` withholds it until then.
|
|
713
|
+
* `listenPort` withholds it until then. On a port whose window both ends
|
|
714
|
+
* speak and that agreed `pipeline`, chunks go as plain messages instead, and
|
|
715
|
+
* the window's credit is the confirmation: up to a window is in flight.
|
|
564
716
|
*
|
|
565
717
|
* A stream port carries exactly one invocation. To make several calls, open
|
|
566
718
|
* several ports: `mux.openPort({ kind: "stream" })` per call.
|
|
@@ -577,22 +729,49 @@ function serveDuplexOverPort(port, handler, options = {}) {
|
|
|
577
729
|
const notice = installAbortNotice(port, controller);
|
|
578
730
|
abortOnPortClose(port, controller);
|
|
579
731
|
const clock = installStreamTimeout(controller, options.timeout);
|
|
580
|
-
const inbound = receiveChunks(port, CHANNEL_IN, controller, clock.touch);
|
|
732
|
+
const inbound = receiveChunks(port, CHANNEL_IN, controller, clock.touch, notice);
|
|
581
733
|
let output;
|
|
582
734
|
try {
|
|
583
735
|
output = handler(inbound.stream);
|
|
584
736
|
} catch (err) {
|
|
585
|
-
const pump = sendChunks(port, CHANNEL_OUT, failing(err), options, controller.signal, clock.touch);
|
|
737
|
+
const pump = sendChunks(port, CHANNEL_OUT, failing(err), options, controller.signal, clock.touch, () => {});
|
|
586
738
|
pump.catch(() => {});
|
|
587
739
|
disarmClockWhenBothSidesSettle(clock, pump, inbound.ended);
|
|
588
740
|
return teardownOnce(controller, notice, clock, inbound, void 0);
|
|
589
741
|
}
|
|
590
|
-
|
|
742
|
+
let onSent = () => {};
|
|
743
|
+
const sent = new Promise((resolve) => {
|
|
744
|
+
onSent = resolve;
|
|
745
|
+
});
|
|
746
|
+
const pump = sendChunks(port, CHANNEL_OUT, output, options, controller.signal, clock.touch, onSent);
|
|
591
747
|
pump.catch(() => {});
|
|
592
748
|
disarmClockWhenBothSidesSettle(clock, pump, inbound.ended);
|
|
749
|
+
endServedStream(port, sent, inbound.over);
|
|
593
750
|
return teardownOnce(controller, notice, clock, inbound, output);
|
|
594
751
|
}
|
|
595
752
|
/**
|
|
753
|
+
* The serving side's normal finish. Once the handler's last chunk has gone
|
|
754
|
+
* without an error, and no more input is coming (`over`: the caller's last
|
|
755
|
+
* chunk arrived without an error and was read, or the caller ended its
|
|
756
|
+
* direction behind it, for a handler that did not read all its input), this
|
|
757
|
+
* side ends its direction too, where `end` was agreed. Without `end` nothing
|
|
758
|
+
* happens here: the caller closes the port. A stream that never gets that far
|
|
759
|
+
* was abandoned, and the caller's notice and `close` free the port; this then
|
|
760
|
+
* never resumes. It needs no race against the port's close signal: nothing
|
|
761
|
+
* awaits it, and what keeps it waiting is the input's watcher, a listener of
|
|
762
|
+
* the port that a close drops, after which the wait is collected.
|
|
763
|
+
*
|
|
764
|
+
* Nothing this side still posts is lost behind its `end`. The caller ends only
|
|
765
|
+
* once its own last chunk was confirmed, so no confirmation is owed to it
|
|
766
|
+
* here, and what the freed port's close signal aborts has settled: the last
|
|
767
|
+
* chunk has left, and input nobody reads is dropped, as on a close.
|
|
768
|
+
*/
|
|
769
|
+
async function endServedStream(port, sent, over) {
|
|
770
|
+
await sent;
|
|
771
|
+
await over;
|
|
772
|
+
endPort(port);
|
|
773
|
+
}
|
|
774
|
+
/**
|
|
596
775
|
* A stream that completes normally has nobody left to call the returned
|
|
597
776
|
* teardown — the caller only knows the stream ended, not that it must also
|
|
598
777
|
* dispose the serve-side handle. Without this, the last `touch()`'s timer
|
|
@@ -632,22 +811,31 @@ function runCallerSide(port, input, options) {
|
|
|
632
811
|
const notice = installAbortNotice(port, controller);
|
|
633
812
|
abortOnPortClose(port, controller);
|
|
634
813
|
const clock = installStreamTimeout(controller, options.timeout);
|
|
635
|
-
const inbound = receiveChunks(port, CHANNEL_OUT, controller, clock.touch);
|
|
636
|
-
|
|
814
|
+
const inbound = receiveChunks(port, CHANNEL_OUT, controller, clock.touch, notice);
|
|
815
|
+
let sent = false;
|
|
816
|
+
const pump = sendChunks(port, CHANNEL_IN, input, options, controller.signal, clock.touch, () => {
|
|
817
|
+
sent = true;
|
|
818
|
+
});
|
|
637
819
|
pump.catch(() => {});
|
|
638
820
|
return returningAtOnce((async function* () {
|
|
821
|
+
let received = false;
|
|
639
822
|
try {
|
|
640
823
|
yield* inbound.stream;
|
|
824
|
+
received = true;
|
|
641
825
|
} finally {
|
|
826
|
+
const ended = received && sent;
|
|
642
827
|
if (!controller.signal.aborted) controller.abort(/* @__PURE__ */ new Error("webrun-rpc: the caller abandoned the stream"));
|
|
643
|
-
notice.post();
|
|
828
|
+
if (!ended) notice.post();
|
|
644
829
|
notice.stop();
|
|
645
830
|
clock.stop();
|
|
646
831
|
inbound.stop();
|
|
647
832
|
await pump.catch(() => {});
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
833
|
+
if (!(ended && endPort(port))) {
|
|
834
|
+
notice.post();
|
|
835
|
+
try {
|
|
836
|
+
await port.close?.();
|
|
837
|
+
} catch {}
|
|
838
|
+
}
|
|
651
839
|
}
|
|
652
840
|
})(), () => {
|
|
653
841
|
if (!controller.signal.aborted) controller.abort(/* @__PURE__ */ new Error("webrun-rpc: the caller abandoned the stream"));
|
|
@@ -741,7 +929,7 @@ function installStreamTimeout(controller, timeout) {
|
|
|
741
929
|
* the local consumer has not started iterating, an inbound chunk waits for it —
|
|
742
930
|
* which is the correct backpressure, and different from having no listener.
|
|
743
931
|
*/
|
|
744
|
-
function receiveChunks(port, channelName, controller, touch) {
|
|
932
|
+
function receiveChunks(port, channelName, controller, touch, notice) {
|
|
745
933
|
let deliver;
|
|
746
934
|
const waiting = [];
|
|
747
935
|
let finished = false;
|
|
@@ -751,25 +939,64 @@ function receiveChunks(port, channelName, controller, touch) {
|
|
|
751
939
|
const ended = new Promise((resolve) => {
|
|
752
940
|
resolveEnded = resolve;
|
|
753
941
|
});
|
|
942
|
+
let resolveOver = () => {};
|
|
943
|
+
const over = new Promise((resolve) => {
|
|
944
|
+
resolveOver = resolve;
|
|
945
|
+
});
|
|
754
946
|
let aborted = false;
|
|
755
947
|
let abortReason;
|
|
756
948
|
const ready = () => deliver || finished ? Promise.resolve() : new Promise((r) => waiting.push(r));
|
|
757
949
|
const wake = () => {
|
|
758
950
|
for (const r of waiting.splice(0)) r();
|
|
759
951
|
};
|
|
760
|
-
const
|
|
952
|
+
const violate = (message) => {
|
|
953
|
+
poison = new Error(message);
|
|
954
|
+
controller.abort(poison);
|
|
955
|
+
setTimeout(() => {
|
|
956
|
+
try {
|
|
957
|
+
port.close?.();
|
|
958
|
+
} catch {}
|
|
959
|
+
}, 0);
|
|
960
|
+
return poison;
|
|
961
|
+
};
|
|
962
|
+
let lastChunkArrived = false;
|
|
963
|
+
let watching = false;
|
|
964
|
+
const watchPeerEnd = () => {
|
|
965
|
+
if (watching) return true;
|
|
966
|
+
if (!portAgreement(port)?.caps.has("end")) return false;
|
|
967
|
+
watching = true;
|
|
968
|
+
whenPortEnded(port).then(() => {
|
|
969
|
+
port.removeEventListener("message", watcher);
|
|
970
|
+
if (lastChunkArrived) {
|
|
971
|
+
resolveOver();
|
|
972
|
+
return;
|
|
973
|
+
}
|
|
974
|
+
notice.post();
|
|
975
|
+
violate("webrun-rpc: the peer ended its direction without finishing it; the stream port is closed");
|
|
976
|
+
});
|
|
977
|
+
return true;
|
|
978
|
+
};
|
|
979
|
+
const watcher = (event) => {
|
|
980
|
+
const data = event.data;
|
|
981
|
+
if (data?.channelName !== channelName) return;
|
|
982
|
+
const chunk = data.type === CHUNK ? data.chunk : data.type === "request" ? data.params : void 0;
|
|
983
|
+
if (!chunk) return;
|
|
984
|
+
if (!watchPeerEnd()) {
|
|
985
|
+
port.removeEventListener("message", watcher);
|
|
986
|
+
return;
|
|
987
|
+
}
|
|
988
|
+
if (chunk.done && !chunk.error) lastChunkArrived = true;
|
|
989
|
+
};
|
|
990
|
+
port.addEventListener("message", watcher);
|
|
991
|
+
watchPeerEnd();
|
|
992
|
+
let held = 0;
|
|
993
|
+
const off = listenPort(port, async (params) => {
|
|
994
|
+
const { done, error } = params;
|
|
995
|
+
const value = adoptValue(params);
|
|
761
996
|
touch();
|
|
762
997
|
if (poison) throw poison;
|
|
763
|
-
if (
|
|
764
|
-
|
|
765
|
-
controller.abort(poison);
|
|
766
|
-
setTimeout(() => {
|
|
767
|
-
try {
|
|
768
|
-
port.close?.();
|
|
769
|
-
} catch {}
|
|
770
|
-
}, 0);
|
|
771
|
-
throw poison;
|
|
772
|
-
}
|
|
998
|
+
if (held > 0) throw violate(MIXED_MESSAGE);
|
|
999
|
+
if (outstanding) throw violate("webrun-rpc: peer sent a second unconfirmed chunk; the stream port is closed");
|
|
773
1000
|
outstanding = true;
|
|
774
1001
|
try {
|
|
775
1002
|
await ready();
|
|
@@ -779,15 +1006,60 @@ function receiveChunks(port, channelName, controller, touch) {
|
|
|
779
1006
|
value,
|
|
780
1007
|
error: error ? deserializeError(error) : void 0
|
|
781
1008
|
});
|
|
1009
|
+
if (done && !error) resolveOver();
|
|
782
1010
|
} finally {
|
|
783
1011
|
outstanding = false;
|
|
784
1012
|
}
|
|
785
1013
|
if (done) resolveEnded();
|
|
786
1014
|
}, { channelName });
|
|
1015
|
+
const onChunk = (event) => {
|
|
1016
|
+
const data = event.data;
|
|
1017
|
+
if (data?.type !== CHUNK || data.channelName !== channelName || !data.chunk) return;
|
|
1018
|
+
if (poison) return;
|
|
1019
|
+
const agreement = portAgreement(port);
|
|
1020
|
+
if (!agreement?.caps.has("pipeline")) {
|
|
1021
|
+
violate("webrun-rpc: peer sent a pipelined chunk where pipelining was not agreed");
|
|
1022
|
+
return;
|
|
1023
|
+
}
|
|
1024
|
+
if (outstanding) {
|
|
1025
|
+
violate(MIXED_MESSAGE);
|
|
1026
|
+
return;
|
|
1027
|
+
}
|
|
1028
|
+
if (data.chunk.value !== void 0 && !ArrayBuffer.isView(data.chunk.value)) {
|
|
1029
|
+
violate("webrun-rpc: peer sent a pipelined chunk whose value is not bytes");
|
|
1030
|
+
return;
|
|
1031
|
+
}
|
|
1032
|
+
if (held > agreement.window) {
|
|
1033
|
+
violate(`webrun-rpc: peer sent past the window (${agreement.window} bytes) without waiting for credit; the stream port is closed`);
|
|
1034
|
+
return;
|
|
1035
|
+
}
|
|
1036
|
+
touch();
|
|
1037
|
+
const { done, error } = data.chunk;
|
|
1038
|
+
const value = data.chunk.value && ownBytes(data.chunk.value);
|
|
1039
|
+
const size = (value?.byteLength ?? 0) + CHUNK_OVERHEAD;
|
|
1040
|
+
held += size;
|
|
1041
|
+
return (async () => {
|
|
1042
|
+
try {
|
|
1043
|
+
await ready();
|
|
1044
|
+
if (finished) return;
|
|
1045
|
+
await deliver?.({
|
|
1046
|
+
done,
|
|
1047
|
+
value,
|
|
1048
|
+
error: error ? deserializeError(error) : void 0
|
|
1049
|
+
});
|
|
1050
|
+
if (done && !error) resolveOver();
|
|
1051
|
+
if (done) resolveEnded();
|
|
1052
|
+
} finally {
|
|
1053
|
+
held -= size;
|
|
1054
|
+
}
|
|
1055
|
+
})();
|
|
1056
|
+
};
|
|
1057
|
+
port.addEventListener("message", onChunk);
|
|
787
1058
|
const detach = () => {
|
|
788
1059
|
finished = true;
|
|
789
1060
|
wake();
|
|
790
1061
|
off();
|
|
1062
|
+
port.removeEventListener("message", onChunk);
|
|
791
1063
|
controller.signal.removeEventListener("abort", onAbort);
|
|
792
1064
|
resolveEnded();
|
|
793
1065
|
};
|
|
@@ -817,21 +1089,35 @@ function receiveChunks(port, channelName, controller, touch) {
|
|
|
817
1089
|
return detach;
|
|
818
1090
|
}),
|
|
819
1091
|
stop: detach,
|
|
820
|
-
ended
|
|
1092
|
+
ended,
|
|
1093
|
+
over
|
|
821
1094
|
};
|
|
822
1095
|
}
|
|
823
1096
|
/**
|
|
824
1097
|
* The sending half of one direction: one `callPort` per chunk, one call
|
|
825
|
-
* outstanding at a time (spec D11/D12), with no per-chunk deadline (spec D8)
|
|
1098
|
+
* outstanding at a time (spec D11/D12), with no per-chunk deadline (spec D8) —
|
|
1099
|
+
* or, once pipelining is agreed, plain chunk messages held back by the window.
|
|
1100
|
+
* `onSent` runs once the end of the output has left without an error: posted
|
|
1101
|
+
* when pipelined, confirmed when not.
|
|
826
1102
|
*/
|
|
827
|
-
async function sendChunks(port, channelName, output, { maxMessageSize, log }, signal, touch) {
|
|
828
|
-
const
|
|
1103
|
+
async function sendChunks(port, channelName, output, { maxMessageSize, log }, signal, touch, onSent) {
|
|
1104
|
+
const framed = maxMessageSize ? toChunks(maxMessageSize)(output) : output;
|
|
1105
|
+
const stream = throughAbort(framed, signal);
|
|
1106
|
+
const producer = output;
|
|
1107
|
+
const returnProducer = () => void Promise.resolve(producer.return?.()).catch(() => {});
|
|
1108
|
+
if (framed !== output) signal.addEventListener("abort", returnProducer, { once: true });
|
|
1109
|
+
let onAborted = () => {};
|
|
1110
|
+
const aborted = new Promise((resolve) => {
|
|
1111
|
+
onAborted = () => resolve();
|
|
1112
|
+
});
|
|
1113
|
+
if (signal.aborted) onAborted();
|
|
1114
|
+
else signal.addEventListener("abort", onAborted, { once: true });
|
|
829
1115
|
try {
|
|
830
1116
|
await sendIterator(async ({ done, value, error }) => {
|
|
831
1117
|
if (signal.aborted) return;
|
|
832
1118
|
const chunk = {
|
|
833
1119
|
done,
|
|
834
|
-
value,
|
|
1120
|
+
value: done ? value : chunkBytes(value),
|
|
835
1121
|
error: error === void 0 ? void 0 : serializeError(error)
|
|
836
1122
|
};
|
|
837
1123
|
log?.("[duplexOverPort] send", {
|
|
@@ -839,63 +1125,34 @@ async function sendChunks(port, channelName, output, { maxMessageSize, log }, si
|
|
|
839
1125
|
done,
|
|
840
1126
|
size: value?.byteLength
|
|
841
1127
|
});
|
|
1128
|
+
if (mayPipeline(port)) {
|
|
1129
|
+
port.postMessage({
|
|
1130
|
+
type: CHUNK,
|
|
1131
|
+
channelName,
|
|
1132
|
+
chunk
|
|
1133
|
+
});
|
|
1134
|
+
touch();
|
|
1135
|
+
if (done && !error) onSent();
|
|
1136
|
+
if (portBufferedAmount(port) > 0) await Promise.race([whenPortReady(port), aborted]);
|
|
1137
|
+
return;
|
|
1138
|
+
}
|
|
842
1139
|
await callPort(port, chunk, {
|
|
843
1140
|
channelName,
|
|
844
1141
|
timeout: NO_TIMEOUT,
|
|
845
1142
|
signal
|
|
846
1143
|
});
|
|
847
1144
|
touch();
|
|
1145
|
+
if (done && !error) onSent();
|
|
848
1146
|
}, stream);
|
|
849
1147
|
} catch (err) {
|
|
850
1148
|
if (signal.aborted) return;
|
|
851
1149
|
throw err;
|
|
1150
|
+
} finally {
|
|
1151
|
+
signal.removeEventListener("abort", returnProducer);
|
|
1152
|
+
signal.removeEventListener("abort", onAborted);
|
|
852
1153
|
}
|
|
853
1154
|
}
|
|
854
1155
|
//#endregion
|
|
855
|
-
//#region src/port-credit.ts
|
|
856
|
-
/**
|
|
857
|
-
* Runs `fn` on a later task, never a microtask: a producer looping on
|
|
858
|
-
* `whenPortReady` must let the transport's own events run. `setImmediate`
|
|
859
|
-
* where it exists; elsewhere a `MessageChannel`, because nested `setTimeout`s
|
|
860
|
-
* are clamped to 4 ms in browsers.
|
|
861
|
-
*/
|
|
862
|
-
const setImmediateFn = globalThis.setImmediate;
|
|
863
|
-
/** @internal */
|
|
864
|
-
const nextTask = setImmediateFn ? (fn) => {
|
|
865
|
-
setImmediateFn(fn);
|
|
866
|
-
} : (() => {
|
|
867
|
-
let channel;
|
|
868
|
-
const queue = [];
|
|
869
|
-
return (fn) => {
|
|
870
|
-
if (!channel) {
|
|
871
|
-
channel = new MessageChannel();
|
|
872
|
-
channel.port1.onmessage = () => queue.shift()?.();
|
|
873
|
-
}
|
|
874
|
-
queue.push(fn);
|
|
875
|
-
channel.port2.postMessage(0);
|
|
876
|
-
};
|
|
877
|
-
})();
|
|
878
|
-
const states = /* @__PURE__ */ new WeakMap();
|
|
879
|
-
/** @internal Registered by the multiplexer for each port with a window. */
|
|
880
|
-
function setPortSendState(port, state) {
|
|
881
|
-
states.set(port, state);
|
|
882
|
-
}
|
|
883
|
-
/**
|
|
884
|
-
* Bytes queued on `port` because its window is spent (0 for a port without
|
|
885
|
-
* flow control). A sender that should not queue without bound waits for
|
|
886
|
-
* `whenPortReady` before posting more.
|
|
887
|
-
*/
|
|
888
|
-
function portBufferedAmount(port) {
|
|
889
|
-
return states.get(port)?.bufferedAmount() ?? 0;
|
|
890
|
-
}
|
|
891
|
-
/**
|
|
892
|
-
* Resolves once at most `below` bytes are queued on `port`, always on a later
|
|
893
|
-
* task (for a port without flow control, on the next one).
|
|
894
|
-
*/
|
|
895
|
-
function whenPortReady(port, below = 0) {
|
|
896
|
-
return states.get(port)?.ready(below) ?? new Promise((resolve) => nextTask(resolve));
|
|
897
|
-
}
|
|
898
|
-
//#endregion
|
|
899
1156
|
//#region src/virtual-port.ts
|
|
900
1157
|
/**
|
|
901
1158
|
* One virtual port.
|
|
@@ -982,8 +1239,25 @@ const MUX_CLOSED_MESSAGE = "webrun-rpc: the multiplexer was closed";
|
|
|
982
1239
|
const KEEPALIVE_ID = Number.MAX_SAFE_INTEGER;
|
|
983
1240
|
/** The longest delay a timer holds; past it `setInterval` fires at once. */
|
|
984
1241
|
const MAX_TIMER_MS = 2 ** 31 - 1;
|
|
1242
|
+
/**
|
|
1243
|
+
* What a windowed port offers in its `open`: every capability this end
|
|
1244
|
+
* speaks. `end` only over a codec that reads it (`PortCodec.halfClose`).
|
|
1245
|
+
*/
|
|
1246
|
+
function ownCaps(codec) {
|
|
1247
|
+
return codec.halfClose ? ["pipeline", "end"] : ["pipeline"];
|
|
1248
|
+
}
|
|
1249
|
+
/**
|
|
1250
|
+
* The capabilities in a peer's `caps` that this end speaks too, or
|
|
1251
|
+
* `undefined` when the peer listed none (it is then treated as an older peer).
|
|
1252
|
+
*/
|
|
1253
|
+
function sharedCaps(own, listed) {
|
|
1254
|
+
if (!Array.isArray(listed)) return void 0;
|
|
1255
|
+
return new Set(own.filter((cap) => listed.includes(cap)));
|
|
1256
|
+
}
|
|
985
1257
|
/** Ceiling on concurrently open virtual ports. Bounds the id table only. */
|
|
986
1258
|
const DEFAULT_MAX_PORTS = 1024;
|
|
1259
|
+
/** Default ceiling on one message reassembled from frames (`maxReassembledBytes`). */
|
|
1260
|
+
const DEFAULT_MAX_REASSEMBLED_BYTES = 67108864;
|
|
987
1261
|
/**
|
|
988
1262
|
* What a message costs a port's window when the codec cannot say
|
|
989
1263
|
* (`PortCodec.encode`): its bytes and string lengths, found in its values up
|
|
@@ -1013,10 +1287,18 @@ function estimateMessageSize(payload) {
|
|
|
1013
1287
|
*/
|
|
1014
1288
|
function multiplexPort(port, options) {
|
|
1015
1289
|
const { codec, onPort, side = "initiator", maxPorts = DEFAULT_MAX_PORTS, maxMessageSize, signal, window, frameSize, keepalive, onLeak } = options;
|
|
1290
|
+
const maxReassembledBytes = options.maxReassembledBytes ?? 67108864;
|
|
1016
1291
|
const misses = keepalive?.misses ?? 3;
|
|
1017
1292
|
if (keepalive && !(Number.isFinite(keepalive.intervalMs) && keepalive.intervalMs > 0 && keepalive.intervalMs <= MAX_TIMER_MS && Number.isSafeInteger(misses) && misses > 0)) throw new RangeError(`webrun-rpc: keepalive needs an intervalMs in (0, ${MAX_TIMER_MS}] and a positive integer misses`);
|
|
1018
1293
|
if (frameSize != null && !(Number.isSafeInteger(frameSize) && frameSize > 0)) throw new RangeError(`webrun-rpc: frameSize must be a positive integer, got ${frameSize}`);
|
|
1294
|
+
if (!(Number.isSafeInteger(maxReassembledBytes) && maxReassembledBytes > 0)) throw new RangeError(`webrun-rpc: maxReassembledBytes must be a positive integer, got ${maxReassembledBytes}`);
|
|
1019
1295
|
const open = /* @__PURE__ */ new Map();
|
|
1296
|
+
let reassembling = 0;
|
|
1297
|
+
const freePartial = (entry) => {
|
|
1298
|
+
if (!entry?.partial) return;
|
|
1299
|
+
reassembling -= entry.partial.charged;
|
|
1300
|
+
entry.partial = void 0;
|
|
1301
|
+
};
|
|
1020
1302
|
const leaks = onLeak ? new FinalizationRegistry(({ id, record, release }) => {
|
|
1021
1303
|
const entry = open.get(id);
|
|
1022
1304
|
if (entry?.handle !== record) return;
|
|
@@ -1037,6 +1319,8 @@ function multiplexPort(port, options) {
|
|
|
1037
1319
|
let muxClosed = false;
|
|
1038
1320
|
/** This end counts encoded bytes and reassembles frames. */
|
|
1039
1321
|
const exactCodec = codec.encode != null && codec.decode != null;
|
|
1322
|
+
/** What this end offers on a windowed port. */
|
|
1323
|
+
const caps = ownCaps(codec);
|
|
1040
1324
|
const post = (envelope, transfer) => {
|
|
1041
1325
|
if (muxClosed) return;
|
|
1042
1326
|
codecPost(envelope, transfer);
|
|
@@ -1094,13 +1378,39 @@ function multiplexPort(port, options) {
|
|
|
1094
1378
|
port.postMessage(bytes);
|
|
1095
1379
|
} catch {}
|
|
1096
1380
|
};
|
|
1381
|
+
/**
|
|
1382
|
+
* Both directions have ended: free the id as a close would, with no `close`
|
|
1383
|
+
* on the wire (each end frees it on its own once it has seen both ends).
|
|
1384
|
+
* Aborting the close signal is how the port's consumer learns it is gone.
|
|
1385
|
+
*/
|
|
1386
|
+
const freeIfEnded = (id, c) => {
|
|
1387
|
+
const entry = open.get(id);
|
|
1388
|
+
if (!c.endSent || !c.peerEnded || entry?.credit !== c) return;
|
|
1389
|
+
freePartial(entry);
|
|
1390
|
+
open.delete(id);
|
|
1391
|
+
leaks?.unregister(entry.handle);
|
|
1392
|
+
entry.handle.markClosed(/* @__PURE__ */ new Error("webrun-rpc: the port ended in both directions"));
|
|
1393
|
+
};
|
|
1394
|
+
/** A peer that broke the port's protocol: hold none of its traffic, and close. */
|
|
1395
|
+
const failPort = (id, entry, error, reason) => {
|
|
1396
|
+
freePartial(entry);
|
|
1397
|
+
dropOutgoing(id);
|
|
1398
|
+
open.delete(id);
|
|
1399
|
+
releaseWaiters(entry.credit);
|
|
1400
|
+
entry.handle.markClosed(error);
|
|
1401
|
+
post({
|
|
1402
|
+
type: "close",
|
|
1403
|
+
id,
|
|
1404
|
+
reason
|
|
1405
|
+
});
|
|
1406
|
+
};
|
|
1097
1407
|
const releaseWaiters = (c) => {
|
|
1098
1408
|
for (const w of c?.waiters.splice(0) ?? []) w.resolve();
|
|
1099
1409
|
};
|
|
1100
1410
|
const flush = (c) => {
|
|
1101
1411
|
while (c.queue.length) {
|
|
1102
1412
|
const next = c.queue[0];
|
|
1103
|
-
if (c.enforced && c.outstanding > 0 && c.outstanding + next.size > c.window) break;
|
|
1413
|
+
if (next.size > 0 && c.enforced && c.outstanding > 0 && c.outstanding + next.size > c.window) break;
|
|
1104
1414
|
c.queue.shift();
|
|
1105
1415
|
c.queued -= next.size;
|
|
1106
1416
|
c.outstanding += next.size;
|
|
@@ -1138,15 +1448,20 @@ function multiplexPort(port, options) {
|
|
|
1138
1448
|
enforced: false,
|
|
1139
1449
|
opener,
|
|
1140
1450
|
exact,
|
|
1451
|
+
caps: /* @__PURE__ */ new Set(),
|
|
1141
1452
|
outstanding: 0,
|
|
1142
1453
|
queue: [],
|
|
1143
1454
|
queued: 0,
|
|
1144
1455
|
unconsumed: 0,
|
|
1145
1456
|
waiters: [],
|
|
1146
1457
|
toGrant: 0,
|
|
1147
|
-
grantScheduled: false
|
|
1458
|
+
grantScheduled: false,
|
|
1459
|
+
ending: false,
|
|
1460
|
+
endSent: false,
|
|
1461
|
+
peerEnded: false
|
|
1148
1462
|
} : void 0;
|
|
1149
1463
|
const self = newVirtualPort((payload, transfer) => {
|
|
1464
|
+
if (c?.ending) return;
|
|
1150
1465
|
touch(id);
|
|
1151
1466
|
const envelope = {
|
|
1152
1467
|
type: "message",
|
|
@@ -1164,12 +1479,12 @@ function multiplexPort(port, options) {
|
|
|
1164
1479
|
...envelope,
|
|
1165
1480
|
payload: structuredClone(payload, { transfer })
|
|
1166
1481
|
} : void 0;
|
|
1167
|
-
const send = () => {
|
|
1482
|
+
const send = (whole = false) => {
|
|
1168
1483
|
if (!bytes) {
|
|
1169
1484
|
sendOnPort(id, held ? () => post(held) : () => post(envelope, transfer));
|
|
1170
1485
|
return;
|
|
1171
1486
|
}
|
|
1172
|
-
if (frameSize != null && c.enforced && c.exact && bytes.byteLength > frameSize) {
|
|
1487
|
+
if (!whole && frameSize != null && c.enforced && c.exact && bytes.byteLength > frameSize) {
|
|
1173
1488
|
for (let at = 0; at < bytes.byteLength; at += frameSize) {
|
|
1174
1489
|
const data = bytes.subarray(at, at + frameSize);
|
|
1175
1490
|
const frame = at + frameSize >= bytes.byteLength ? {
|
|
@@ -1199,10 +1514,11 @@ function multiplexPort(port, options) {
|
|
|
1199
1514
|
record.markClosed(reason);
|
|
1200
1515
|
});
|
|
1201
1516
|
function release(reason) {
|
|
1517
|
+
freePartial(open.get(id));
|
|
1202
1518
|
open.delete(id);
|
|
1203
1519
|
leaks?.unregister(record);
|
|
1204
1520
|
if (c?.queue.length) {
|
|
1205
|
-
for (const item of c.queue.splice(0)) item.send();
|
|
1521
|
+
for (const item of c.queue.splice(0)) item.send(true);
|
|
1206
1522
|
c.queued = 0;
|
|
1207
1523
|
}
|
|
1208
1524
|
sendOnPort(id, () => post({
|
|
@@ -1232,6 +1548,38 @@ function multiplexPort(port, options) {
|
|
|
1232
1548
|
});
|
|
1233
1549
|
})
|
|
1234
1550
|
});
|
|
1551
|
+
setPortEndState(self.port, {
|
|
1552
|
+
end: () => {
|
|
1553
|
+
if (isClosed() || !c.caps.has("end")) return false;
|
|
1554
|
+
if (c.ending) return true;
|
|
1555
|
+
c.ending = true;
|
|
1556
|
+
c.queue.push({
|
|
1557
|
+
size: 0,
|
|
1558
|
+
send: () => sendOnPort(id, () => {
|
|
1559
|
+
if (open.get(id)?.credit !== c) return;
|
|
1560
|
+
post({
|
|
1561
|
+
type: "end",
|
|
1562
|
+
id
|
|
1563
|
+
});
|
|
1564
|
+
c.endSent = true;
|
|
1565
|
+
freeIfEnded(id, c);
|
|
1566
|
+
})
|
|
1567
|
+
});
|
|
1568
|
+
flush(c);
|
|
1569
|
+
return true;
|
|
1570
|
+
},
|
|
1571
|
+
watchPeerEnd: (resolve) => {
|
|
1572
|
+
if (!c.caps.has("end")) return false;
|
|
1573
|
+
if (c.peerEnded) resolve();
|
|
1574
|
+
else c.onPeerEnd = new WeakRef(resolve);
|
|
1575
|
+
return true;
|
|
1576
|
+
}
|
|
1577
|
+
});
|
|
1578
|
+
setPortAgreement(self.port, () => ({
|
|
1579
|
+
window: c.window,
|
|
1580
|
+
caps: c.caps,
|
|
1581
|
+
enforced: c.enforced
|
|
1582
|
+
}));
|
|
1235
1583
|
}
|
|
1236
1584
|
const record = {
|
|
1237
1585
|
deliver: self.deliver,
|
|
@@ -1283,16 +1631,24 @@ function multiplexPort(port, options) {
|
|
|
1283
1631
|
}
|
|
1284
1632
|
const handle = attach(envelope.id, envelope.meta, envelope.window, false, envelope.exact === true && exactCodec);
|
|
1285
1633
|
const credit = open.get(envelope.id)?.credit;
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1634
|
+
const listed = sharedCaps(caps, envelope.caps);
|
|
1635
|
+
if (credit) {
|
|
1636
|
+
const ack = credit.exact ? {
|
|
1637
|
+
type: "credit",
|
|
1638
|
+
id: envelope.id,
|
|
1639
|
+
bytes: 0,
|
|
1640
|
+
exact: true
|
|
1641
|
+
} : {
|
|
1642
|
+
type: "credit",
|
|
1643
|
+
id: envelope.id,
|
|
1644
|
+
bytes: 0
|
|
1645
|
+
};
|
|
1646
|
+
if (listed) credit.caps = listed;
|
|
1647
|
+
post(listed ? {
|
|
1648
|
+
...ack,
|
|
1649
|
+
caps: [...listed]
|
|
1650
|
+
} : ack);
|
|
1651
|
+
}
|
|
1296
1652
|
let accepted = false;
|
|
1297
1653
|
if (onPort) try {
|
|
1298
1654
|
accepted = onPort(handle.port, envelope.meta) !== false;
|
|
@@ -1320,6 +1676,7 @@ function multiplexPort(port, options) {
|
|
|
1320
1676
|
c.exact = false;
|
|
1321
1677
|
c.outstanding = 0;
|
|
1322
1678
|
}
|
|
1679
|
+
c.caps = sharedCaps(caps, envelope.caps) ?? /* @__PURE__ */ new Set();
|
|
1323
1680
|
post({
|
|
1324
1681
|
type: "credit",
|
|
1325
1682
|
id: envelope.id,
|
|
@@ -1331,6 +1688,10 @@ function multiplexPort(port, options) {
|
|
|
1331
1688
|
flush(c);
|
|
1332
1689
|
return;
|
|
1333
1690
|
}
|
|
1691
|
+
if ((envelope.type === "message" || envelope.type === "frame") && entry?.credit?.peerEnded) {
|
|
1692
|
+
failPort(envelope.id, entry, /* @__PURE__ */ new Error("webrun-rpc: the peer sent on a port after its end"), "protocol");
|
|
1693
|
+
return;
|
|
1694
|
+
}
|
|
1334
1695
|
if (envelope.type === "message") {
|
|
1335
1696
|
const sizeOf = (c) => (c.exact ? byteLengthOf(event.data) : void 0) ?? estimateMessageSize(envelope.payload);
|
|
1336
1697
|
if (!entry) return;
|
|
@@ -1341,15 +1702,29 @@ function multiplexPort(port, options) {
|
|
|
1341
1702
|
if (envelope.type === "frame") {
|
|
1342
1703
|
if (!entry?.credit?.exact || !codec.decode || !(envelope.data instanceof Uint8Array)) return;
|
|
1343
1704
|
touch(envelope.id);
|
|
1705
|
+
const c = entry.credit;
|
|
1706
|
+
const owed = c.unconsumed + c.toGrant;
|
|
1344
1707
|
const partial = entry.partial ?? {
|
|
1345
1708
|
chunks: [],
|
|
1346
|
-
bytes: 0
|
|
1709
|
+
bytes: 0,
|
|
1710
|
+
owed,
|
|
1711
|
+
charged: 0,
|
|
1712
|
+
...owed > 0 ? { room: c.window - owed } : {}
|
|
1347
1713
|
};
|
|
1348
1714
|
entry.partial = partial;
|
|
1715
|
+
const size = envelope.data.byteLength;
|
|
1716
|
+
const charged = partial.room == null ? Math.max(0, partial.bytes + size - c.window) : 0;
|
|
1717
|
+
const overrun = partial.room != null && partial.bytes + size > partial.room ? `what the window left (${partial.room} of ${c.window} bytes, ${partial.owed} still owed)` : reassembling - partial.charged + charged > maxReassembledBytes ? `maxReassembledBytes (${maxReassembledBytes} bytes past the window, across all ports)` : void 0;
|
|
1718
|
+
if (overrun) {
|
|
1719
|
+
failPort(envelope.id, entry, /* @__PURE__ */ new RangeError(`webrun-rpc: a framed message passed ${overrun}`), "max-reassembled-bytes");
|
|
1720
|
+
return;
|
|
1721
|
+
}
|
|
1722
|
+
partial.bytes += size;
|
|
1723
|
+
reassembling += charged - partial.charged;
|
|
1724
|
+
partial.charged = charged;
|
|
1349
1725
|
partial.chunks.push(envelope.data);
|
|
1350
|
-
partial.bytes += envelope.data.byteLength;
|
|
1351
1726
|
if (!envelope.last) return;
|
|
1352
|
-
entry
|
|
1727
|
+
freePartial(entry);
|
|
1353
1728
|
const whole = new Uint8Array(partial.bytes);
|
|
1354
1729
|
let at = 0;
|
|
1355
1730
|
for (const chunk of partial.chunks) {
|
|
@@ -1361,8 +1736,22 @@ function multiplexPort(port, options) {
|
|
|
1361
1736
|
entry.handle.deliver(inner.payload, whole.byteLength);
|
|
1362
1737
|
return;
|
|
1363
1738
|
}
|
|
1739
|
+
if (envelope.type === "end") {
|
|
1740
|
+
if (!entry) return;
|
|
1741
|
+
const c = entry.credit;
|
|
1742
|
+
if (!c?.caps.has("end")) {
|
|
1743
|
+
failPort(envelope.id, entry, /* @__PURE__ */ new Error("webrun-rpc: the peer ended a port where end was not agreed"), "protocol");
|
|
1744
|
+
return;
|
|
1745
|
+
}
|
|
1746
|
+
touch(envelope.id);
|
|
1747
|
+
c.peerEnded = true;
|
|
1748
|
+
c.onPeerEnd?.deref()?.();
|
|
1749
|
+
freeIfEnded(envelope.id, c);
|
|
1750
|
+
return;
|
|
1751
|
+
}
|
|
1364
1752
|
dropOutgoing(envelope.id);
|
|
1365
1753
|
if (!entry) return;
|
|
1754
|
+
freePartial(entry);
|
|
1366
1755
|
open.delete(envelope.id);
|
|
1367
1756
|
releaseWaiters(entry.credit);
|
|
1368
1757
|
entry.handle.markClosed(/* @__PURE__ */ new Error("webrun-rpc: the peer closed the port"));
|
|
@@ -1441,12 +1830,14 @@ function multiplexPort(port, options) {
|
|
|
1441
1830
|
id,
|
|
1442
1831
|
meta,
|
|
1443
1832
|
window,
|
|
1444
|
-
exact: true
|
|
1833
|
+
exact: true,
|
|
1834
|
+
caps: [...caps]
|
|
1445
1835
|
} : {
|
|
1446
1836
|
type: "open",
|
|
1447
1837
|
id,
|
|
1448
1838
|
meta,
|
|
1449
|
-
window
|
|
1839
|
+
window,
|
|
1840
|
+
caps: [...caps]
|
|
1450
1841
|
} : {
|
|
1451
1842
|
type: "open",
|
|
1452
1843
|
id,
|
|
@@ -1555,14 +1946,17 @@ const serve = async ({ mux: factory, timeout }, handler) => {
|
|
|
1555
1946
|
const mux = await factory((port) => {
|
|
1556
1947
|
let off;
|
|
1557
1948
|
let finished = false;
|
|
1558
|
-
const tracked = (input) =>
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1949
|
+
const tracked = (input) => {
|
|
1950
|
+
const output = handler(input);
|
|
1951
|
+
return returningAtOnce((async function* () {
|
|
1952
|
+
try {
|
|
1953
|
+
yield* output;
|
|
1954
|
+
} finally {
|
|
1955
|
+
finished = true;
|
|
1956
|
+
if (off) live.delete(off);
|
|
1957
|
+
}
|
|
1958
|
+
})(), () => void output.return(void 0).catch(() => {}));
|
|
1959
|
+
};
|
|
1566
1960
|
off = serveDuplexOverPort(port, tracked, streamOptions);
|
|
1567
1961
|
if (!finished) live.add(off);
|
|
1568
1962
|
});
|
|
@@ -1666,7 +2060,7 @@ function isEnvelope(value) {
|
|
|
1666
2060
|
const candidate = value;
|
|
1667
2061
|
if (typeof candidate.id !== "number") return false;
|
|
1668
2062
|
if (!Number.isInteger(candidate.id) || candidate.id < 0) return false;
|
|
1669
|
-
return candidate.type === "open" || candidate.type === "message" || candidate.type === "close" || candidate.type === "credit" || candidate.type === "frame" || candidate.type === "ping" || candidate.type === "pong";
|
|
2063
|
+
return candidate.type === "open" || candidate.type === "message" || candidate.type === "close" || candidate.type === "credit" || candidate.type === "frame" || candidate.type === "ping" || candidate.type === "pong" || candidate.type === "end";
|
|
1670
2064
|
}
|
|
1671
2065
|
/**
|
|
1672
2066
|
* For ports whose messages are structured values — a real `MessagePort`, a
|
|
@@ -1678,6 +2072,7 @@ function isEnvelope(value) {
|
|
|
1678
2072
|
* not send anything a byte codec could not also carry.
|
|
1679
2073
|
*/
|
|
1680
2074
|
const structuredCodec = {
|
|
2075
|
+
halfClose: true,
|
|
1681
2076
|
post(port, envelope, transfer) {
|
|
1682
2077
|
if (transfer && transfer.length > 0) port.postMessage(envelope, transfer);
|
|
1683
2078
|
else port.postMessage(envelope);
|
|
@@ -1780,4 +2175,4 @@ function transferPortMux(target, options = {}) {
|
|
|
1780
2175
|
};
|
|
1781
2176
|
}
|
|
1782
2177
|
//#endregion
|
|
1783
|
-
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, portBufferedAmount, postCancelChannel, recieve, send, serve, serveDuplexOverPort, setPortCloseSignal, structuredCodec, transferPortMux, whenPortReady };
|
|
2178
|
+
export { CANCEL_CHANNEL_TYPE, DEFAULT_MAX_PORTS, DEFAULT_MAX_REASSEMBLED_BYTES, MUX_CLOSED_MESSAGE, NO_TIMEOUT, PORT_TRANSFER, STREAM_ABORT, byteChannelFromMessagePort, callBidi, callPort, connect, duplexOverPort, getPortCloseSignal, ioHandle, ioSend, listenBidi, listenCancelChannel, listenPort, multiplexPort, overPipe, overPorts, portBufferedAmount, postCancelChannel, recieve, send, serve, serveDuplexOverPort, setPortCloseSignal, structuredCodec, transferPortMux, whenPortReady };
|