@statewalker/webrun-http-browser 0.7.1 → 0.9.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/dist/sw-worker.js CHANGED
@@ -1653,7 +1653,7 @@
1653
1653
  else await release?.();
1654
1654
  return new Response(null, responseInit);
1655
1655
  }
1656
- return new Response(asyncIterableToReadable(withAbort(respBody, request.signal)), responseInit);
1656
+ return new Response(responseBody(respBody, request.signal, release), responseInit);
1657
1657
  }
1658
1658
  /**
1659
1659
  * Wrap a `(Request) => Promise<Response>` handler as a `Duplex` so it can be
@@ -1784,15 +1784,78 @@
1784
1784
  settle
1785
1785
  };
1786
1786
  }
1787
- async function* withAbort(iter, signal) {
1788
- if (!signal) {
1789
- yield* iter;
1790
- return;
1791
- }
1792
- for await (const chunk of iter) {
1793
- if (signal.aborted) throw abortReason$1(signal);
1794
- yield chunk;
1795
- }
1787
+ /**
1788
+ * How long a cancelled body waits for the read in flight before it abandons the call anyway.
1789
+ * The read's answer tells a body that has ended (the call is finished: an upload the handler
1790
+ * reads after answering goes on) from one that has not; a peer that sends nothing more never
1791
+ * answers it. A body whose end comes later than this after the cancel is abandoned too, so a
1792
+ * handler still streaming its response and still reading the upload then loses the upload.
1793
+ */
1794
+ const CANCEL_GRACE_MS = 1e3;
1795
+ /**
1796
+ * The response body as a stream that the request's signal can end at any time.
1797
+ *
1798
+ * An abort errors the stream with the abort's reason at once — a read waiting
1799
+ * for a peer that sends nothing more rejects then, not when a next chunk
1800
+ * arrives, which may be never — and abandons the call (`release`), so on a
1801
+ * multiplexed transport its port is given back whether or not anyone still
1802
+ * reads the body. The listener is removed once the body ends, however it ends:
1803
+ * read to its end, cancelled, failed or aborted. A body nobody reads or cancels
1804
+ * keeps it until the signal aborts (and keeps the call open until then anyway).
1805
+ * A cancel abandons the call too — unless the read in flight comes back with the
1806
+ * body's end, which finishes it instead (see `cancel`).
1807
+ */
1808
+ function responseBody(body, signal, release) {
1809
+ const it = body[Symbol.asyncIterator]();
1810
+ let controller;
1811
+ let ended = false;
1812
+ let reading = false;
1813
+ const end = () => {
1814
+ ended = true;
1815
+ signal?.removeEventListener("abort", onAbort);
1816
+ };
1817
+ const onAbort = () => {
1818
+ if (ended) return;
1819
+ end();
1820
+ try {
1821
+ controller.error(abortReason$1(signal));
1822
+ } catch {}
1823
+ release?.();
1824
+ };
1825
+ return new ReadableStream({
1826
+ start(c) {
1827
+ controller = c;
1828
+ if (signal?.aborted) onAbort();
1829
+ else signal?.addEventListener("abort", onAbort, { once: true });
1830
+ },
1831
+ async pull(c) {
1832
+ try {
1833
+ reading = true;
1834
+ const { done, value } = await it.next().finally(() => {
1835
+ reading = false;
1836
+ });
1837
+ if (ended) return;
1838
+ if (done) {
1839
+ end();
1840
+ c.close();
1841
+ return;
1842
+ }
1843
+ c.enqueue(value);
1844
+ } catch (err) {
1845
+ if (ended) return;
1846
+ end();
1847
+ c.error(err);
1848
+ }
1849
+ },
1850
+ cancel() {
1851
+ end();
1852
+ const returned = Promise.resolve(it.return?.()).catch(() => {});
1853
+ if (!reading) return;
1854
+ const grace = setTimeout(() => void release?.(), CANCEL_GRACE_MS);
1855
+ grace.unref?.();
1856
+ returned.finally(() => clearTimeout(grace));
1857
+ }
1858
+ });
1796
1859
  }
1797
1860
  //#endregion
1798
1861
  //#region src/core/data-channels.ts
@@ -2120,8 +2183,11 @@
2120
2183
  error
2121
2184
  });
2122
2185
  };
2123
- port.addEventListener("message", onMessage);
2124
- return () => port.removeEventListener("message", onMessage);
2186
+ const listener = (event) => {
2187
+ onMessage(event);
2188
+ };
2189
+ port.addEventListener("message", listener);
2190
+ return () => port.removeEventListener("message", listener);
2125
2191
  }
2126
2192
  /**
2127
2193
  * Wraps an async iterable so that an `AbortSignal` firing causes the wrapper
@@ -2526,6 +2592,33 @@
2526
2592
  }
2527
2593
  }
2528
2594
  /**
2595
+ * Runs `fn` on a later task, never a microtask: a producer looping on
2596
+ * `whenPortReady` must let the transport's own events run. `setImmediate`
2597
+ * where it exists; elsewhere a `MessageChannel`, because nested `setTimeout`s
2598
+ * are clamped to 4 ms in browsers.
2599
+ */
2600
+ const setImmediateFn = globalThis.setImmediate;
2601
+ /** @internal */
2602
+ const nextTask = setImmediateFn ? (fn) => {
2603
+ setImmediateFn(fn);
2604
+ } : (() => {
2605
+ let channel;
2606
+ const queue = [];
2607
+ return (fn) => {
2608
+ if (!channel) {
2609
+ channel = new MessageChannel();
2610
+ channel.port1.onmessage = () => queue.shift()?.();
2611
+ }
2612
+ queue.push(fn);
2613
+ channel.port2.postMessage(0);
2614
+ };
2615
+ })();
2616
+ const states = /* @__PURE__ */ new WeakMap();
2617
+ /** @internal Registered by the multiplexer for each port with a window. */
2618
+ function setPortSendState(port, state) {
2619
+ states.set(port, state);
2620
+ }
2621
+ /**
2529
2622
  * One virtual port.
2530
2623
  *
2531
2624
  * `deliver` and `markClosed` are deliberately not on `port`: the consumer holds
@@ -2540,6 +2633,14 @@
2540
2633
  if (closing.signal.aborted) return;
2541
2634
  closing.abort(reason ?? /* @__PURE__ */ new Error("webrun-rpc: the port was closed"));
2542
2635
  };
2636
+ const close = (reason) => {
2637
+ if (closed) return;
2638
+ closed = true;
2639
+ const notify = requestClose;
2640
+ listeners.clear();
2641
+ notify(reason);
2642
+ abort(reason);
2643
+ };
2543
2644
  const port = {
2544
2645
  addEventListener(_type, listener) {
2545
2646
  listeners.add(listener);
@@ -2551,24 +2652,28 @@
2551
2652
  if (closed) return;
2552
2653
  send(message, transfer);
2553
2654
  },
2554
- close() {
2555
- if (closed) return;
2556
- closed = true;
2557
- const notify = requestClose;
2558
- listeners.clear();
2559
- notify();
2560
- abort();
2561
- }
2655
+ close
2562
2656
  };
2563
2657
  setPortCloseSignal(port, closing.signal);
2658
+ const dispose = Symbol.dispose;
2659
+ if (dispose) Object.defineProperty(port, dispose, {
2660
+ value: () => close(),
2661
+ configurable: true
2662
+ });
2564
2663
  return {
2565
2664
  port,
2566
2665
  deliver(payload) {
2567
2666
  if (closed) return;
2568
2667
  const event = new MessageEvent("message", { data: payload });
2668
+ let pending;
2569
2669
  for (const listener of [...listeners]) try {
2570
- listener(event);
2670
+ const result = listener(event);
2671
+ if (result && typeof result.then === "function") {
2672
+ pending ??= [];
2673
+ pending.push(result);
2674
+ }
2571
2675
  } catch {}
2676
+ return pending ? Promise.allSettled(pending) : void 0;
2572
2677
  },
2573
2678
  markClosed(reason) {
2574
2679
  closed = true;
@@ -2577,53 +2682,309 @@
2577
2682
  },
2578
2683
  isClosed() {
2579
2684
  return closed;
2685
+ },
2686
+ hasListeners() {
2687
+ return listeners.size > 0;
2580
2688
  }
2581
2689
  };
2582
2690
  }
2583
2691
  /**
2584
- * The message of the error that fails the calls still in flight on a
2585
- * multiplexer when it is closed (`PortMux.close()`).
2692
+ * The id of keepalive's `ping` / `pong`: one no port ever takes. An older mux
2693
+ * whose codec passes unknown envelopes through reads them as a close of that
2694
+ * id, which must therefore not be a real port (0 is the initiator's first).
2586
2695
  */
2587
- const MUX_CLOSED_MESSAGE = "webrun-rpc: the multiplexer was closed";
2696
+ const KEEPALIVE_ID = Number.MAX_SAFE_INTEGER;
2697
+ /** The longest delay a timer holds; past it `setInterval` fires at once. */
2698
+ const MAX_TIMER_MS = 2 ** 31 - 1;
2588
2699
  /** Ceiling on concurrently open virtual ports. Bounds the id table only. */
2589
2700
  const DEFAULT_MAX_PORTS = 1024;
2590
2701
  /**
2702
+ * What a message costs a port's window when the codec cannot say
2703
+ * (`PortCodec.encode`): its bytes and string lengths, found in its values up
2704
+ * to 4 levels deep (at most 256 values), and at least 64. Both ends compute it
2705
+ * from the same structured value, so they agree.
2706
+ */
2707
+ function estimateMessageSize(payload) {
2708
+ let budget = 256;
2709
+ const walk = (v, depth) => {
2710
+ if (budget-- <= 0) return 0;
2711
+ if (typeof v === "string") return v.length;
2712
+ if (v instanceof ArrayBuffer) return v.byteLength;
2713
+ if (ArrayBuffer.isView(v)) return v.byteLength;
2714
+ if (v === null || typeof v !== "object" || depth >= 4) return 8;
2715
+ let n = 16;
2716
+ if (Array.isArray(v)) for (const x of v) n += walk(x, depth + 1);
2717
+ else for (const k in v) n += k.length + walk(v[k], depth + 1);
2718
+ return n;
2719
+ };
2720
+ return Math.max(64, walk(payload, 0));
2721
+ }
2722
+ /**
2591
2723
  * The default `PortMux`: emulates multiplexing over a single port.
2592
2724
  *
2593
2725
  * A transport that already multiplexes natively supplies its own `PortMux`
2594
2726
  * instead — this implementation is for transports that offer one pipe.
2595
2727
  */
2596
2728
  function multiplexPort(port, options) {
2597
- const { codec, onPort, side = "initiator", maxPorts = DEFAULT_MAX_PORTS, maxMessageSize } = options;
2729
+ const { codec, onPort, side = "initiator", maxPorts = DEFAULT_MAX_PORTS, maxMessageSize, signal, window, frameSize, keepalive, onLeak } = options;
2730
+ const misses = keepalive?.misses ?? 3;
2731
+ 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`);
2732
+ if (frameSize != null && !(Number.isSafeInteger(frameSize) && frameSize > 0)) throw new RangeError(`webrun-rpc: frameSize must be a positive integer, got ${frameSize}`);
2598
2733
  const open = /* @__PURE__ */ new Map();
2734
+ const leaks = onLeak ? new FinalizationRegistry(({ id, record, release }) => {
2735
+ const entry = open.get(id);
2736
+ if (entry?.handle !== record) return;
2737
+ const { handle: _, credit: __, partial: ___, ...info } = entry;
2738
+ if (!record.hasListeners()) {
2739
+ release("leaked");
2740
+ record.markClosed(/* @__PURE__ */ new Error("webrun-rpc: the port was dropped without close()"));
2741
+ }
2742
+ try {
2743
+ onLeak?.(info);
2744
+ } catch {}
2745
+ }) : void 0;
2746
+ const touch = (id) => {
2747
+ const entry = open.get(id);
2748
+ if (entry) entry.lastActivity = Date.now();
2749
+ };
2599
2750
  let nextId = side === "initiator" ? 0 : 1;
2600
2751
  let muxClosed = false;
2752
+ /** This end counts encoded bytes and reassembles frames. */
2753
+ const exactCodec = codec.encode != null && codec.decode != null;
2601
2754
  const post = (envelope, transfer) => {
2602
2755
  if (muxClosed) return;
2756
+ codecPost(envelope, transfer);
2757
+ };
2758
+ /** Like `post`, also while closing: the mux's own close notices. */
2759
+ const codecPost = (envelope, transfer) => {
2603
2760
  try {
2604
2761
  codec.post(port, envelope, transfer);
2605
2762
  } catch {}
2606
2763
  };
2607
- const attach = (id) => {
2608
- let self;
2609
- self = newVirtualPort((payload, transfer) => post({
2610
- type: "message",
2611
- id,
2612
- payload
2613
- }, transfer), (reason) => {
2764
+ const outgoing = /* @__PURE__ */ new Map();
2765
+ const ring = [];
2766
+ let pumping = false;
2767
+ const pump = async () => {
2768
+ pumping = true;
2769
+ try {
2770
+ while (ring.length && !muxClosed) {
2771
+ if (port.whenDrained) await port.whenDrained();
2772
+ if (muxClosed) break;
2773
+ const id = ring.shift();
2774
+ const queue = outgoing.get(id);
2775
+ const item = queue?.shift();
2776
+ if (queue?.length) ring.push(id);
2777
+ else outgoing.delete(id);
2778
+ item?.();
2779
+ }
2780
+ } catch (err) {
2781
+ close(err).catch(() => {});
2782
+ } finally {
2783
+ pumping = false;
2784
+ }
2785
+ };
2786
+ const sendOnPort = (id, item) => {
2787
+ if (frameSize == null) {
2788
+ item();
2789
+ return;
2790
+ }
2791
+ let queue = outgoing.get(id);
2792
+ if (!queue) {
2793
+ queue = [];
2794
+ outgoing.set(id, queue);
2795
+ ring.push(id);
2796
+ }
2797
+ queue.push(item);
2798
+ if (!pumping) pump();
2799
+ };
2800
+ const dropOutgoing = (id) => {
2801
+ if (!outgoing.delete(id)) return;
2802
+ const at = ring.indexOf(id);
2803
+ if (at >= 0) ring.splice(at, 1);
2804
+ };
2805
+ const postBytes = (bytes) => {
2806
+ if (muxClosed) return;
2807
+ try {
2808
+ port.postMessage(bytes);
2809
+ } catch {}
2810
+ };
2811
+ const releaseWaiters = (c) => {
2812
+ for (const w of c?.waiters.splice(0) ?? []) w.resolve();
2813
+ };
2814
+ const flush = (c) => {
2815
+ while (c.queue.length) {
2816
+ const next = c.queue[0];
2817
+ if (c.enforced && c.outstanding > 0 && c.outstanding + next.size > c.window) break;
2818
+ c.queue.shift();
2819
+ c.queued -= next.size;
2820
+ c.outstanding += next.size;
2821
+ next.send();
2822
+ }
2823
+ for (const w of [...c.waiters]) if (c.queued <= w.below) {
2824
+ c.waiters.splice(c.waiters.indexOf(w), 1);
2825
+ w.resolve();
2826
+ }
2827
+ };
2828
+ const grant = (id, c, bytes) => {
2829
+ c.unconsumed -= bytes;
2830
+ c.toGrant += bytes;
2831
+ const send = () => {
2832
+ c.grantScheduled = false;
2833
+ if (!c.toGrant || !open.has(id)) return;
2614
2834
  post({
2835
+ type: "credit",
2836
+ id,
2837
+ bytes: c.toGrant
2838
+ });
2839
+ c.toGrant = 0;
2840
+ };
2841
+ if (c.toGrant >= c.window / 4) {
2842
+ send();
2843
+ return;
2844
+ }
2845
+ if (c.grantScheduled) return;
2846
+ c.grantScheduled = true;
2847
+ setTimeout(send, 0);
2848
+ };
2849
+ const attach = (id, meta, portWindow, opener, exact) => {
2850
+ const c = typeof portWindow === "number" && Number.isSafeInteger(portWindow) && portWindow > 0 ? {
2851
+ window: portWindow,
2852
+ enforced: false,
2853
+ opener,
2854
+ exact,
2855
+ outstanding: 0,
2856
+ queue: [],
2857
+ queued: 0,
2858
+ unconsumed: 0,
2859
+ waiters: [],
2860
+ toGrant: 0,
2861
+ grantScheduled: false
2862
+ } : void 0;
2863
+ const self = newVirtualPort((payload, transfer) => {
2864
+ touch(id);
2865
+ const envelope = {
2866
+ type: "message",
2867
+ id,
2868
+ payload
2869
+ };
2870
+ if (!c) {
2871
+ post(envelope, transfer);
2872
+ return;
2873
+ }
2874
+ const bytes = c.exact ? codec.encode?.(envelope) : void 0;
2875
+ const size = bytes?.byteLength ?? estimateMessageSize(payload);
2876
+ const waits = frameSize != null || c.queue.length > 0 || c.enforced && c.outstanding > 0 && c.outstanding + size > c.window;
2877
+ const held = !bytes && waits ? {
2878
+ ...envelope,
2879
+ payload: structuredClone(payload, { transfer })
2880
+ } : void 0;
2881
+ const send = () => {
2882
+ if (!bytes) {
2883
+ sendOnPort(id, held ? () => post(held) : () => post(envelope, transfer));
2884
+ return;
2885
+ }
2886
+ if (frameSize != null && c.enforced && c.exact && bytes.byteLength > frameSize) {
2887
+ for (let at = 0; at < bytes.byteLength; at += frameSize) {
2888
+ const data = bytes.subarray(at, at + frameSize);
2889
+ const frame = at + frameSize >= bytes.byteLength ? {
2890
+ type: "frame",
2891
+ id,
2892
+ data,
2893
+ last: true
2894
+ } : {
2895
+ type: "frame",
2896
+ id,
2897
+ data
2898
+ };
2899
+ sendOnPort(id, () => post(frame));
2900
+ }
2901
+ return;
2902
+ }
2903
+ sendOnPort(id, () => postBytes(bytes));
2904
+ };
2905
+ c.queue.push({
2906
+ send,
2907
+ size
2908
+ });
2909
+ c.queued += size;
2910
+ flush(c);
2911
+ }, (reason) => {
2912
+ release(reason);
2913
+ record.markClosed(reason);
2914
+ });
2915
+ function release(reason) {
2916
+ open.delete(id);
2917
+ leaks?.unregister(record);
2918
+ if (c?.queue.length) {
2919
+ for (const item of c.queue.splice(0)) item.send();
2920
+ c.queued = 0;
2921
+ }
2922
+ sendOnPort(id, () => post({
2615
2923
  type: "close",
2616
2924
  id,
2617
2925
  reason
2926
+ }));
2927
+ releaseWaiters(c);
2928
+ }
2929
+ const { markClosed, isClosed, hasListeners } = self;
2930
+ if (c) {
2931
+ const deliver = self.deliver;
2932
+ self.deliver = (payload, size = estimateMessageSize(payload)) => {
2933
+ c.unconsumed += size;
2934
+ const done = deliver(payload);
2935
+ if (done) done.then(() => grant(id, c, size));
2936
+ else grant(id, c, size);
2937
+ return done;
2938
+ };
2939
+ setPortSendState(self.port, {
2940
+ bufferedAmount: () => c.queued,
2941
+ ready: (below = 0) => new Promise((resolve) => {
2942
+ if (c.queued <= below || isClosed()) nextTask(resolve);
2943
+ else c.waiters.push({
2944
+ below,
2945
+ resolve: () => nextTask(resolve)
2946
+ });
2947
+ })
2618
2948
  });
2619
- open.delete(id);
2620
- self.markClosed();
2949
+ }
2950
+ const record = {
2951
+ deliver: self.deliver,
2952
+ markClosed,
2953
+ isClosed,
2954
+ hasListeners,
2955
+ close: (reason) => {
2956
+ if (isClosed()) return;
2957
+ release(reason);
2958
+ markClosed(reason);
2959
+ }
2960
+ };
2961
+ leaks?.register(self.port, {
2962
+ id,
2963
+ record,
2964
+ release
2965
+ }, record);
2966
+ const now = Date.now();
2967
+ open.set(id, {
2968
+ id,
2969
+ meta,
2970
+ openedAt: now,
2971
+ lastActivity: now,
2972
+ handle: record,
2973
+ credit: c
2621
2974
  });
2622
- open.set(id, self);
2623
2975
  return self;
2624
2976
  };
2625
- const handleEnvelope = (envelope) => {
2977
+ const handleEnvelope = (envelope, event) => {
2626
2978
  if (muxClosed) return;
2979
+ heardSincePing = true;
2980
+ if (envelope.type === "ping" || envelope.type === "pong") {
2981
+ peerKeepsAlive = true;
2982
+ if (envelope.type === "ping") post({
2983
+ type: "pong",
2984
+ id: KEEPALIVE_ID
2985
+ });
2986
+ return;
2987
+ }
2627
2988
  if (envelope.type === "open") {
2628
2989
  if (open.has(envelope.id)) return;
2629
2990
  if (open.size >= maxPorts) {
@@ -2634,7 +2995,18 @@
2634
2995
  });
2635
2996
  return;
2636
2997
  }
2637
- const handle = attach(envelope.id);
2998
+ const handle = attach(envelope.id, envelope.meta, envelope.window, false, envelope.exact === true && exactCodec);
2999
+ const credit = open.get(envelope.id)?.credit;
3000
+ if (credit) post(credit.exact ? {
3001
+ type: "credit",
3002
+ id: envelope.id,
3003
+ bytes: 0,
3004
+ exact: true
3005
+ } : {
3006
+ type: "credit",
3007
+ id: envelope.id,
3008
+ bytes: 0
3009
+ });
2638
3010
  let accepted = false;
2639
3011
  if (onPort) try {
2640
3012
  accepted = onPort(handle.port, envelope.meta) !== false;
@@ -2649,57 +3021,191 @@
2649
3021
  id: envelope.id,
2650
3022
  reason: "rejected"
2651
3023
  });
3024
+ return;
3025
+ }
3026
+ return;
3027
+ }
3028
+ const entry = open.get(envelope.id);
3029
+ if (envelope.type === "credit") {
3030
+ const c = entry?.credit;
3031
+ if (!c || !(envelope.bytes >= 0)) return;
3032
+ if (!c.enforced && c.opener) {
3033
+ if (c.exact && envelope.exact !== true) {
3034
+ c.exact = false;
3035
+ c.outstanding = 0;
3036
+ }
3037
+ post({
3038
+ type: "credit",
3039
+ id: envelope.id,
3040
+ bytes: 0
3041
+ });
2652
3042
  }
3043
+ c.enforced = true;
3044
+ c.outstanding = Math.max(0, c.outstanding - envelope.bytes);
3045
+ flush(c);
2653
3046
  return;
2654
3047
  }
2655
- const handle = open.get(envelope.id);
2656
3048
  if (envelope.type === "message") {
2657
- if (!handle) return;
2658
- handle.deliver(envelope.payload);
3049
+ const sizeOf = (c) => (c.exact ? byteLengthOf(event.data) : void 0) ?? estimateMessageSize(envelope.payload);
3050
+ if (!entry) return;
3051
+ touch(envelope.id);
3052
+ entry.handle.deliver(envelope.payload, entry.credit ? sizeOf(entry.credit) : void 0);
2659
3053
  return;
2660
3054
  }
2661
- if (!handle) return;
3055
+ if (envelope.type === "frame") {
3056
+ if (!entry?.credit?.exact || !codec.decode || !(envelope.data instanceof Uint8Array)) return;
3057
+ touch(envelope.id);
3058
+ const partial = entry.partial ?? {
3059
+ chunks: [],
3060
+ bytes: 0
3061
+ };
3062
+ entry.partial = partial;
3063
+ partial.chunks.push(envelope.data);
3064
+ partial.bytes += envelope.data.byteLength;
3065
+ if (!envelope.last) return;
3066
+ entry.partial = void 0;
3067
+ const whole = new Uint8Array(partial.bytes);
3068
+ let at = 0;
3069
+ for (const chunk of partial.chunks) {
3070
+ whole.set(chunk, at);
3071
+ at += chunk.byteLength;
3072
+ }
3073
+ const inner = codec.decode(whole);
3074
+ if (inner?.type !== "message" || inner.id !== envelope.id) return;
3075
+ entry.handle.deliver(inner.payload, whole.byteLength);
3076
+ return;
3077
+ }
3078
+ dropOutgoing(envelope.id);
3079
+ if (!entry) return;
2662
3080
  open.delete(envelope.id);
2663
- handle.markClosed(/* @__PURE__ */ new Error("webrun-rpc: the peer closed the port"));
3081
+ releaseWaiters(entry.credit);
3082
+ entry.handle.markClosed(/* @__PURE__ */ new Error("webrun-rpc: the peer closed the port"));
2664
3083
  };
2665
3084
  const listener = (event) => {
2666
3085
  const envelope = codec.read(event);
2667
- if (envelope) handleEnvelope(envelope);
3086
+ if (envelope) handleEnvelope(envelope, event);
3087
+ };
3088
+ let peerKeepsAlive = false;
3089
+ let heardSincePing = true;
3090
+ let unanswered = 0;
3091
+ let pinger;
3092
+ const tick = () => {
3093
+ if (!keepalive) return;
3094
+ const backlogged = (port.bufferedAmount ?? 0) > 0;
3095
+ if (heardSincePing) unanswered = 0;
3096
+ else if (peerKeepsAlive && !backlogged) unanswered++;
3097
+ if (unanswered >= misses) {
3098
+ close(/* @__PURE__ */ new Error(`webrun-rpc: the peer did not answer ${misses} keepalive pings, ${keepalive.intervalMs} ms apart`)).catch(() => {});
3099
+ return;
3100
+ }
3101
+ heardSincePing = false;
3102
+ post({
3103
+ type: "ping",
3104
+ id: KEEPALIVE_ID
3105
+ });
3106
+ };
3107
+ const close = async (reason) => {
3108
+ if (muxClosed) return;
3109
+ clearInterval(pinger);
3110
+ muxClosed = true;
3111
+ signal?.removeEventListener("abort", onTransportAbort);
3112
+ port.removeEventListener("message", listener);
3113
+ const closedNow = /* @__PURE__ */ new Set();
3114
+ for (const [id, { handle, credit }] of [...open]) {
3115
+ closedNow.add(id);
3116
+ codecPost({
3117
+ type: "close",
3118
+ id
3119
+ });
3120
+ open.delete(id);
3121
+ releaseWaiters(credit);
3122
+ handle.markClosed(reason ?? /* @__PURE__ */ new Error("webrun-rpc: the multiplexer was closed"));
3123
+ }
3124
+ for (const id of outgoing.keys()) if (!closedNow.has(id)) codecPost({
3125
+ type: "close",
3126
+ id
3127
+ });
3128
+ outgoing.clear();
3129
+ ring.length = 0;
3130
+ await port.close?.();
3131
+ };
3132
+ const onTransportAbort = () => {
3133
+ close(signal?.reason).catch(() => {});
2668
3134
  };
2669
3135
  port.addEventListener("message", listener);
2670
3136
  port.start?.();
3137
+ if (keepalive) {
3138
+ pinger = setInterval(tick, keepalive.intervalMs);
3139
+ pinger.unref?.();
3140
+ }
3141
+ if (signal?.aborted) close(signal.reason).catch(() => {});
3142
+ else signal?.addEventListener("abort", onTransportAbort, { once: true });
2671
3143
  return {
2672
3144
  maxMessageSize,
2673
- async openPort(meta) {
3145
+ async openPort(meta, { signal: portSignal } = {}) {
2674
3146
  if (muxClosed) throw new Error("webrun-rpc: the multiplexer is closed");
2675
- if (open.size >= maxPorts) throw new RangeError(`webrun-rpc: maxPorts (${maxPorts}) reached`);
3147
+ if (portSignal?.aborted) throw portSignal.reason;
3148
+ if (open.size >= maxPorts) throw new RangeError(`webrun-rpc: maxPorts (${maxPorts}) reached; open ports by kind: ${describeKinds(open.values())}`);
2676
3149
  while (open.has(nextId)) nextId += 2;
2677
3150
  const id = nextId;
2678
3151
  nextId += 2;
2679
- const handle = attach(id);
2680
- post({
3152
+ const handle = attach(id, meta, window, true, exactCodec);
3153
+ post(window ? exactCodec ? {
3154
+ type: "open",
3155
+ id,
3156
+ meta,
3157
+ window,
3158
+ exact: true
3159
+ } : {
3160
+ type: "open",
3161
+ id,
3162
+ meta,
3163
+ window
3164
+ } : {
2681
3165
  type: "open",
2682
3166
  id,
2683
3167
  meta
2684
3168
  });
3169
+ if (portSignal) {
3170
+ const record = open.get(id)?.handle;
3171
+ const onAbort = () => {
3172
+ if (record && open.get(id)?.handle === record) record.close(portSignal.reason);
3173
+ };
3174
+ portSignal.addEventListener("abort", onAbort, { once: true });
3175
+ getPortCloseSignal(handle.port)?.addEventListener("abort", () => portSignal.removeEventListener("abort", onAbort), { once: true });
3176
+ }
2685
3177
  return handle.port;
2686
3178
  },
2687
- async close() {
2688
- if (muxClosed) return;
2689
- for (const [id, handle] of [...open]) {
2690
- post({
2691
- type: "close",
2692
- id
2693
- });
2694
- open.delete(id);
2695
- handle.markClosed(/* @__PURE__ */ new Error(MUX_CLOSED_MESSAGE));
2696
- }
2697
- muxClosed = true;
2698
- port.removeEventListener("message", listener);
2699
- await port.close?.();
3179
+ close,
3180
+ ports() {
3181
+ return [...open.values()].map(({ handle: _, credit, ...info }) => credit ? {
3182
+ ...info,
3183
+ ...credit.enforced ? { window: credit.window } : {},
3184
+ queued: credit.queued,
3185
+ outstanding: credit.outstanding,
3186
+ unconsumed: credit.unconsumed
3187
+ } : info);
2700
3188
  }
2701
3189
  };
2702
3190
  }
3191
+ /** The byte length of a received message's data, or `undefined` if it is not bytes. */
3192
+ function byteLengthOf(data) {
3193
+ if (data instanceof ArrayBuffer || ArrayBuffer.isView(data)) return data.byteLength;
3194
+ }
3195
+ /**
3196
+ * `"http: 1000, ws: 24"`: the three commonest `meta.kind`s among `entries`, so
3197
+ * a full table names who filled it. A port without a kind counts as `"?"`.
3198
+ */
3199
+ function describeKinds(entries) {
3200
+ const counts = /* @__PURE__ */ new Map();
3201
+ for (const { meta } of entries) {
3202
+ const kind = meta?.kind;
3203
+ const name = typeof kind === "string" ? kind : "?";
3204
+ counts.set(name, (counts.get(name) ?? 0) + 1);
3205
+ }
3206
+ if (!counts.size) return "none";
3207
+ return [...counts].sort((x, y) => y[1] - x[1]).slice(0, 3).map(([name, n]) => `${name}: ${n}`).join(", ");
3208
+ }
2703
3209
  /**
2704
3210
  * Announced with every port this pair opens. Layer 1 never reads `meta` — it is
2705
3211
  * here so a peer running something else over the same multiplexer can tell a
@@ -2806,7 +3312,7 @@
2806
3312
  const candidate = value;
2807
3313
  if (typeof candidate.id !== "number") return false;
2808
3314
  if (!Number.isInteger(candidate.id) || candidate.id < 0) return false;
2809
- return candidate.type === "open" || candidate.type === "message" || candidate.type === "close";
3315
+ return candidate.type === "open" || candidate.type === "message" || candidate.type === "close" || candidate.type === "credit" || candidate.type === "frame" || candidate.type === "ping" || candidate.type === "pong";
2810
3316
  }
2811
3317
  /**
2812
3318
  * For ports whose messages are structured values — a real `MessagePort`, a
@@ -2968,7 +3474,7 @@
2968
3474
  finish();
2969
3475
  return response;
2970
3476
  }
2971
- return new Response(guardBody(response.body, lost, () => lostReason, finish), {
3477
+ return new Response(guardBody(response.body, lost, () => lostReason, finish, request.signal), {
2972
3478
  status: response.status,
2973
3479
  statusText: response.statusText,
2974
3480
  headers: response.headers
@@ -2987,33 +3493,65 @@
2987
3493
  return asyncDisposable(link, () => link.close());
2988
3494
  }
2989
3495
  /**
2990
- * A response body that reports its end (done, cancelled or failed) and fails
2991
- * with the link's reason when the link is lost mid-stream.
3496
+ * A response body that reports its end (done, cancelled, failed or aborted) and
3497
+ * fails with the link's reason when the link is lost mid-stream. An abort of the
3498
+ * request's signal ends it at once, read or not: the exchange is over (the layer
3499
+ * below has given its port back), so the request leaves `inflight` then.
2992
3500
  */
2993
- function guardBody(body, lost, reason, finish) {
3501
+ function guardBody(body, lost, reason, finish, signal) {
2994
3502
  const reader = body.getReader();
3503
+ let controller;
3504
+ let ended = false;
3505
+ const end = () => {
3506
+ ended = true;
3507
+ signal?.removeEventListener("abort", onAbort);
3508
+ finish();
3509
+ };
3510
+ const onAbort = () => {
3511
+ if (ended) return;
3512
+ end();
3513
+ reader.cancel().catch(() => {});
3514
+ try {
3515
+ controller.error(abortError(signal));
3516
+ } catch {}
3517
+ };
2995
3518
  return new ReadableStream({
3519
+ start(c) {
3520
+ controller = c;
3521
+ if (signal?.aborted) onAbort();
3522
+ else signal?.addEventListener("abort", onAbort, { once: true });
3523
+ },
2996
3524
  async pull(controller) {
2997
3525
  try {
2998
3526
  const { done, value } = await Promise.race([reader.read(), lost]);
3527
+ if (ended) return;
2999
3528
  if (done) {
3000
- finish();
3529
+ end();
3001
3530
  controller.close();
3002
3531
  return;
3003
3532
  }
3004
3533
  controller.enqueue(value);
3005
3534
  } catch (error) {
3006
- finish();
3535
+ if (ended) return;
3536
+ end();
3007
3537
  reader.cancel().catch(() => {});
3008
3538
  controller.error(reason() ?? error);
3009
3539
  }
3010
3540
  },
3011
3541
  async cancel(why) {
3012
- finish();
3542
+ end();
3013
3543
  await reader.cancel(why).catch(() => {});
3014
3544
  }
3015
3545
  });
3016
3546
  }
3547
+ /** The signal's reason as an `Error` (`AbortError` when it gave none). */
3548
+ function abortError(signal) {
3549
+ const why = signal.reason;
3550
+ if (why instanceof Error) return why;
3551
+ const err = new Error(why === void 0 ? "Aborted" : String(why));
3552
+ err.name = "AbortError";
3553
+ return err;
3554
+ }
3017
3555
  //#endregion
3018
3556
  //#region src/link/locks.ts
3019
3557
  function lockManager() {