@statewalker/webrun-http-browser 0.7.1 → 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/dist/sw-worker.js CHANGED
@@ -1394,7 +1394,11 @@
1394
1394
  */
1395
1395
  function httpServe(handler, options = {}) {
1396
1396
  const codec = options.codec ?? defaultCodec;
1397
- return async function* httpHandlerDuplex(input) {
1397
+ return (input) => {
1398
+ const abandoned = new AbortController();
1399
+ return abortingOnReturn(exchange(input, { signal: abandoned.signal }), (reason) => abandoned.abort(reason ?? /* @__PURE__ */ new Error("webrun-http-streams: the exchange was abandoned before it was over")));
1400
+ };
1401
+ async function* exchange(input, context) {
1398
1402
  let decoded;
1399
1403
  try {
1400
1404
  decoded = await codec.decodeRequest(input);
@@ -1406,7 +1410,7 @@
1406
1410
  const replyCodec = decoded.codec ?? codec;
1407
1411
  let result;
1408
1412
  try {
1409
- result = await handler(decoded.envelope, decoded.body);
1413
+ result = await handler(decoded.envelope, decoded.body, context);
1410
1414
  } catch (error) {
1411
1415
  yield* encodeErrorResponse(replyCodec, error, decoded.envelope.method);
1412
1416
  return;
@@ -1421,7 +1425,36 @@
1421
1425
  await result.release?.();
1422
1426
  } catch {}
1423
1427
  }
1428
+ }
1429
+ }
1430
+ /**
1431
+ * `gen`, except that `return()` and `throw()` from its consumer call `abandon`
1432
+ * at once — before they are queued behind a `next()` still parked inside the
1433
+ * generator (the handler's `await`) — unless the generator already finished.
1434
+ */
1435
+ function abortingOnReturn(gen, abandon) {
1436
+ let finished = false;
1437
+ const settle = (r) => {
1438
+ if (r.done) finished = true;
1439
+ return r;
1440
+ };
1441
+ const wrapped = {
1442
+ next: (...args) => gen.next(...args).then(settle),
1443
+ return(value) {
1444
+ if (!finished) abandon();
1445
+ finished = true;
1446
+ return gen.return(value);
1447
+ },
1448
+ throw(error) {
1449
+ if (!finished) abandon(error);
1450
+ finished = true;
1451
+ return gen.throw(error);
1452
+ },
1453
+ [Symbol.asyncIterator]() {
1454
+ return wrapped;
1455
+ }
1424
1456
  };
1457
+ return asyncDisposable$2(wrapped, () => wrapped.return(void 0));
1425
1458
  }
1426
1459
  /**
1427
1460
  * The one place that knows a runtime may not implement request body streams,
@@ -1653,17 +1686,18 @@
1653
1686
  else await release?.();
1654
1687
  return new Response(null, responseInit);
1655
1688
  }
1656
- return new Response(asyncIterableToReadable(withAbort(respBody, request.signal)), responseInit);
1689
+ return new Response(responseBody(respBody, request.signal, release), responseInit);
1657
1690
  }
1658
1691
  /**
1659
1692
  * Wrap a `(Request) => Promise<Response>` handler as a `Duplex` so it can be
1660
1693
  * registered with any `webrun-streams-*` adapter's `serve`.
1661
1694
  */
1662
1695
  function serveFetchOverDuplex(handler, options = {}) {
1663
- return httpServe(async (env, body) => {
1696
+ return httpServe(async (env, body, exchange) => {
1664
1697
  const reqInit = {
1665
1698
  method: env.method,
1666
- headers: forwardableHeaders(env.headers)
1699
+ headers: forwardableHeaders(env.headers),
1700
+ signal: exchange?.signal
1667
1701
  };
1668
1702
  let upload;
1669
1703
  if (env.method !== "GET" && env.method !== "HEAD") {
@@ -1784,15 +1818,78 @@
1784
1818
  settle
1785
1819
  };
1786
1820
  }
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
- }
1821
+ /**
1822
+ * How long a cancelled body waits for the read in flight before it abandons the call anyway.
1823
+ * The read's answer tells a body that has ended (the call is finished: an upload the handler
1824
+ * reads after answering goes on) from one that has not; a peer that sends nothing more never
1825
+ * answers it. A body whose end comes later than this after the cancel is abandoned too, so a
1826
+ * handler still streaming its response and still reading the upload then loses the upload.
1827
+ */
1828
+ const CANCEL_GRACE_MS = 1e3;
1829
+ /**
1830
+ * The response body as a stream that the request's signal can end at any time.
1831
+ *
1832
+ * An abort errors the stream with the abort's reason at once — a read waiting
1833
+ * for a peer that sends nothing more rejects then, not when a next chunk
1834
+ * arrives, which may be never — and abandons the call (`release`), so on a
1835
+ * multiplexed transport its port is given back whether or not anyone still
1836
+ * reads the body. The listener is removed once the body ends, however it ends:
1837
+ * read to its end, cancelled, failed or aborted. A body nobody reads or cancels
1838
+ * keeps it until the signal aborts (and keeps the call open until then anyway).
1839
+ * A cancel abandons the call too — unless the read in flight comes back with the
1840
+ * body's end, which finishes it instead (see `cancel`).
1841
+ */
1842
+ function responseBody(body, signal, release) {
1843
+ const it = body[Symbol.asyncIterator]();
1844
+ let controller;
1845
+ let ended = false;
1846
+ let reading = false;
1847
+ const end = () => {
1848
+ ended = true;
1849
+ signal?.removeEventListener("abort", onAbort);
1850
+ };
1851
+ const onAbort = () => {
1852
+ if (ended) return;
1853
+ end();
1854
+ try {
1855
+ controller.error(abortReason$1(signal));
1856
+ } catch {}
1857
+ release?.();
1858
+ };
1859
+ return new ReadableStream({
1860
+ start(c) {
1861
+ controller = c;
1862
+ if (signal?.aborted) onAbort();
1863
+ else signal?.addEventListener("abort", onAbort, { once: true });
1864
+ },
1865
+ async pull(c) {
1866
+ try {
1867
+ reading = true;
1868
+ const { done, value } = await it.next().finally(() => {
1869
+ reading = false;
1870
+ });
1871
+ if (ended) return;
1872
+ if (done) {
1873
+ end();
1874
+ c.close();
1875
+ return;
1876
+ }
1877
+ c.enqueue(value);
1878
+ } catch (err) {
1879
+ if (ended) return;
1880
+ end();
1881
+ c.error(err);
1882
+ }
1883
+ },
1884
+ cancel() {
1885
+ end();
1886
+ const returned = Promise.resolve(it.return?.()).catch(() => {});
1887
+ if (!reading) return;
1888
+ const grace = setTimeout(() => void release?.(), CANCEL_GRACE_MS);
1889
+ grace.unref?.();
1890
+ returned.finally(() => clearTimeout(grace));
1891
+ }
1892
+ });
1796
1893
  }
1797
1894
  //#endregion
1798
1895
  //#region src/core/data-channels.ts
@@ -2120,8 +2217,11 @@
2120
2217
  error
2121
2218
  });
2122
2219
  };
2123
- port.addEventListener("message", onMessage);
2124
- return () => port.removeEventListener("message", onMessage);
2220
+ const listener = (event) => {
2221
+ onMessage(event);
2222
+ };
2223
+ port.addEventListener("message", listener);
2224
+ return () => port.removeEventListener("message", listener);
2125
2225
  }
2126
2226
  /**
2127
2227
  * Wraps an async iterable so that an `AbortSignal` firing causes the wrapper
@@ -2499,7 +2599,11 @@
2499
2599
  * outstanding at a time (spec D11/D12), with no per-chunk deadline (spec D8).
2500
2600
  */
2501
2601
  async function sendChunks(port, channelName, output, { maxMessageSize, log }, signal, touch) {
2502
- const stream = throughAbort(maxMessageSize ? toChunks(maxMessageSize)(output) : output, signal);
2602
+ const framed = maxMessageSize ? toChunks(maxMessageSize)(output) : output;
2603
+ const stream = throughAbort(framed, signal);
2604
+ const producer = output;
2605
+ const returnProducer = () => void Promise.resolve(producer.return?.()).catch(() => {});
2606
+ if (framed !== output) signal.addEventListener("abort", returnProducer, { once: true });
2503
2607
  try {
2504
2608
  await sendIterator(async ({ done, value, error }) => {
2505
2609
  if (signal.aborted) return;
@@ -2523,9 +2627,38 @@
2523
2627
  } catch (err) {
2524
2628
  if (signal.aborted) return;
2525
2629
  throw err;
2630
+ } finally {
2631
+ signal.removeEventListener("abort", returnProducer);
2526
2632
  }
2527
2633
  }
2528
2634
  /**
2635
+ * Runs `fn` on a later task, never a microtask: a producer looping on
2636
+ * `whenPortReady` must let the transport's own events run. `setImmediate`
2637
+ * where it exists; elsewhere a `MessageChannel`, because nested `setTimeout`s
2638
+ * are clamped to 4 ms in browsers.
2639
+ */
2640
+ const setImmediateFn = globalThis.setImmediate;
2641
+ /** @internal */
2642
+ const nextTask = setImmediateFn ? (fn) => {
2643
+ setImmediateFn(fn);
2644
+ } : (() => {
2645
+ let channel;
2646
+ const queue = [];
2647
+ return (fn) => {
2648
+ if (!channel) {
2649
+ channel = new MessageChannel();
2650
+ channel.port1.onmessage = () => queue.shift()?.();
2651
+ }
2652
+ queue.push(fn);
2653
+ channel.port2.postMessage(0);
2654
+ };
2655
+ })();
2656
+ const states = /* @__PURE__ */ new WeakMap();
2657
+ /** @internal Registered by the multiplexer for each port with a window. */
2658
+ function setPortSendState(port, state) {
2659
+ states.set(port, state);
2660
+ }
2661
+ /**
2529
2662
  * One virtual port.
2530
2663
  *
2531
2664
  * `deliver` and `markClosed` are deliberately not on `port`: the consumer holds
@@ -2540,6 +2673,14 @@
2540
2673
  if (closing.signal.aborted) return;
2541
2674
  closing.abort(reason ?? /* @__PURE__ */ new Error("webrun-rpc: the port was closed"));
2542
2675
  };
2676
+ const close = (reason) => {
2677
+ if (closed) return;
2678
+ closed = true;
2679
+ const notify = requestClose;
2680
+ listeners.clear();
2681
+ notify(reason);
2682
+ abort(reason);
2683
+ };
2543
2684
  const port = {
2544
2685
  addEventListener(_type, listener) {
2545
2686
  listeners.add(listener);
@@ -2551,24 +2692,28 @@
2551
2692
  if (closed) return;
2552
2693
  send(message, transfer);
2553
2694
  },
2554
- close() {
2555
- if (closed) return;
2556
- closed = true;
2557
- const notify = requestClose;
2558
- listeners.clear();
2559
- notify();
2560
- abort();
2561
- }
2695
+ close
2562
2696
  };
2563
2697
  setPortCloseSignal(port, closing.signal);
2698
+ const dispose = Symbol.dispose;
2699
+ if (dispose) Object.defineProperty(port, dispose, {
2700
+ value: () => close(),
2701
+ configurable: true
2702
+ });
2564
2703
  return {
2565
2704
  port,
2566
2705
  deliver(payload) {
2567
2706
  if (closed) return;
2568
2707
  const event = new MessageEvent("message", { data: payload });
2708
+ let pending;
2569
2709
  for (const listener of [...listeners]) try {
2570
- listener(event);
2710
+ const result = listener(event);
2711
+ if (result && typeof result.then === "function") {
2712
+ pending ??= [];
2713
+ pending.push(result);
2714
+ }
2571
2715
  } catch {}
2716
+ return pending ? Promise.allSettled(pending) : void 0;
2572
2717
  },
2573
2718
  markClosed(reason) {
2574
2719
  closed = true;
@@ -2577,53 +2722,318 @@
2577
2722
  },
2578
2723
  isClosed() {
2579
2724
  return closed;
2725
+ },
2726
+ hasListeners() {
2727
+ return listeners.size > 0;
2580
2728
  }
2581
2729
  };
2582
2730
  }
2583
2731
  /**
2584
- * The message of the error that fails the calls still in flight on a
2585
- * multiplexer when it is closed (`PortMux.close()`).
2732
+ * The id of keepalive's `ping` / `pong`: one no port ever takes. An older mux
2733
+ * whose codec passes unknown envelopes through reads them as a close of that
2734
+ * id, which must therefore not be a real port (0 is the initiator's first).
2586
2735
  */
2587
- const MUX_CLOSED_MESSAGE = "webrun-rpc: the multiplexer was closed";
2736
+ const KEEPALIVE_ID = Number.MAX_SAFE_INTEGER;
2737
+ /** The longest delay a timer holds; past it `setInterval` fires at once. */
2738
+ const MAX_TIMER_MS = 2 ** 31 - 1;
2588
2739
  /** Ceiling on concurrently open virtual ports. Bounds the id table only. */
2589
2740
  const DEFAULT_MAX_PORTS = 1024;
2590
2741
  /**
2742
+ * What a message costs a port's window when the codec cannot say
2743
+ * (`PortCodec.encode`): its bytes and string lengths, found in its values up
2744
+ * to 4 levels deep (at most 256 values), and at least 64. Both ends compute it
2745
+ * from the same structured value, so they agree.
2746
+ */
2747
+ function estimateMessageSize(payload) {
2748
+ let budget = 256;
2749
+ const walk = (v, depth) => {
2750
+ if (budget-- <= 0) return 0;
2751
+ if (typeof v === "string") return v.length;
2752
+ if (v instanceof ArrayBuffer) return v.byteLength;
2753
+ if (ArrayBuffer.isView(v)) return v.byteLength;
2754
+ if (v === null || typeof v !== "object" || depth >= 4) return 8;
2755
+ let n = 16;
2756
+ if (Array.isArray(v)) for (const x of v) n += walk(x, depth + 1);
2757
+ else for (const k in v) n += k.length + walk(v[k], depth + 1);
2758
+ return n;
2759
+ };
2760
+ return Math.max(64, walk(payload, 0));
2761
+ }
2762
+ /**
2591
2763
  * The default `PortMux`: emulates multiplexing over a single port.
2592
2764
  *
2593
2765
  * A transport that already multiplexes natively supplies its own `PortMux`
2594
2766
  * instead — this implementation is for transports that offer one pipe.
2595
2767
  */
2596
2768
  function multiplexPort(port, options) {
2597
- const { codec, onPort, side = "initiator", maxPorts = DEFAULT_MAX_PORTS, maxMessageSize } = options;
2769
+ const { codec, onPort, side = "initiator", maxPorts = DEFAULT_MAX_PORTS, maxMessageSize, signal, window, frameSize, keepalive, onLeak } = options;
2770
+ const maxReassembledBytes = options.maxReassembledBytes ?? 67108864;
2771
+ const misses = keepalive?.misses ?? 3;
2772
+ 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`);
2773
+ if (frameSize != null && !(Number.isSafeInteger(frameSize) && frameSize > 0)) throw new RangeError(`webrun-rpc: frameSize must be a positive integer, got ${frameSize}`);
2774
+ if (!(Number.isSafeInteger(maxReassembledBytes) && maxReassembledBytes > 0)) throw new RangeError(`webrun-rpc: maxReassembledBytes must be a positive integer, got ${maxReassembledBytes}`);
2598
2775
  const open = /* @__PURE__ */ new Map();
2776
+ let reassembling = 0;
2777
+ const freePartial = (entry) => {
2778
+ if (!entry?.partial) return;
2779
+ reassembling -= entry.partial.charged;
2780
+ entry.partial = void 0;
2781
+ };
2782
+ const leaks = onLeak ? new FinalizationRegistry(({ id, record, release }) => {
2783
+ const entry = open.get(id);
2784
+ if (entry?.handle !== record) return;
2785
+ const { handle: _, credit: __, partial: ___, ...info } = entry;
2786
+ if (!record.hasListeners()) {
2787
+ release("leaked");
2788
+ record.markClosed(/* @__PURE__ */ new Error("webrun-rpc: the port was dropped without close()"));
2789
+ }
2790
+ try {
2791
+ onLeak?.(info);
2792
+ } catch {}
2793
+ }) : void 0;
2794
+ const touch = (id) => {
2795
+ const entry = open.get(id);
2796
+ if (entry) entry.lastActivity = Date.now();
2797
+ };
2599
2798
  let nextId = side === "initiator" ? 0 : 1;
2600
2799
  let muxClosed = false;
2800
+ /** This end counts encoded bytes and reassembles frames. */
2801
+ const exactCodec = codec.encode != null && codec.decode != null;
2601
2802
  const post = (envelope, transfer) => {
2602
2803
  if (muxClosed) return;
2804
+ codecPost(envelope, transfer);
2805
+ };
2806
+ /** Like `post`, also while closing: the mux's own close notices. */
2807
+ const codecPost = (envelope, transfer) => {
2603
2808
  try {
2604
2809
  codec.post(port, envelope, transfer);
2605
2810
  } catch {}
2606
2811
  };
2607
- const attach = (id) => {
2608
- let self;
2609
- self = newVirtualPort((payload, transfer) => post({
2610
- type: "message",
2611
- id,
2612
- payload
2613
- }, transfer), (reason) => {
2812
+ const outgoing = /* @__PURE__ */ new Map();
2813
+ const ring = [];
2814
+ let pumping = false;
2815
+ const pump = async () => {
2816
+ pumping = true;
2817
+ try {
2818
+ while (ring.length && !muxClosed) {
2819
+ if (port.whenDrained) await port.whenDrained();
2820
+ if (muxClosed) break;
2821
+ const id = ring.shift();
2822
+ const queue = outgoing.get(id);
2823
+ const item = queue?.shift();
2824
+ if (queue?.length) ring.push(id);
2825
+ else outgoing.delete(id);
2826
+ item?.();
2827
+ }
2828
+ } catch (err) {
2829
+ close(err).catch(() => {});
2830
+ } finally {
2831
+ pumping = false;
2832
+ }
2833
+ };
2834
+ const sendOnPort = (id, item) => {
2835
+ if (frameSize == null) {
2836
+ item();
2837
+ return;
2838
+ }
2839
+ let queue = outgoing.get(id);
2840
+ if (!queue) {
2841
+ queue = [];
2842
+ outgoing.set(id, queue);
2843
+ ring.push(id);
2844
+ }
2845
+ queue.push(item);
2846
+ if (!pumping) pump();
2847
+ };
2848
+ const dropOutgoing = (id) => {
2849
+ if (!outgoing.delete(id)) return;
2850
+ const at = ring.indexOf(id);
2851
+ if (at >= 0) ring.splice(at, 1);
2852
+ };
2853
+ const postBytes = (bytes) => {
2854
+ if (muxClosed) return;
2855
+ try {
2856
+ port.postMessage(bytes);
2857
+ } catch {}
2858
+ };
2859
+ const releaseWaiters = (c) => {
2860
+ for (const w of c?.waiters.splice(0) ?? []) w.resolve();
2861
+ };
2862
+ const flush = (c) => {
2863
+ while (c.queue.length) {
2864
+ const next = c.queue[0];
2865
+ if (c.enforced && c.outstanding > 0 && c.outstanding + next.size > c.window) break;
2866
+ c.queue.shift();
2867
+ c.queued -= next.size;
2868
+ c.outstanding += next.size;
2869
+ next.send();
2870
+ }
2871
+ for (const w of [...c.waiters]) if (c.queued <= w.below) {
2872
+ c.waiters.splice(c.waiters.indexOf(w), 1);
2873
+ w.resolve();
2874
+ }
2875
+ };
2876
+ const grant = (id, c, bytes) => {
2877
+ c.unconsumed -= bytes;
2878
+ c.toGrant += bytes;
2879
+ const send = () => {
2880
+ c.grantScheduled = false;
2881
+ if (!c.toGrant || !open.has(id)) return;
2614
2882
  post({
2883
+ type: "credit",
2884
+ id,
2885
+ bytes: c.toGrant
2886
+ });
2887
+ c.toGrant = 0;
2888
+ };
2889
+ if (c.toGrant >= c.window / 4) {
2890
+ send();
2891
+ return;
2892
+ }
2893
+ if (c.grantScheduled) return;
2894
+ c.grantScheduled = true;
2895
+ setTimeout(send, 0);
2896
+ };
2897
+ const attach = (id, meta, portWindow, opener, exact) => {
2898
+ const c = typeof portWindow === "number" && Number.isSafeInteger(portWindow) && portWindow > 0 ? {
2899
+ window: portWindow,
2900
+ enforced: false,
2901
+ opener,
2902
+ exact,
2903
+ outstanding: 0,
2904
+ queue: [],
2905
+ queued: 0,
2906
+ unconsumed: 0,
2907
+ waiters: [],
2908
+ toGrant: 0,
2909
+ grantScheduled: false
2910
+ } : void 0;
2911
+ const self = newVirtualPort((payload, transfer) => {
2912
+ touch(id);
2913
+ const envelope = {
2914
+ type: "message",
2915
+ id,
2916
+ payload
2917
+ };
2918
+ if (!c) {
2919
+ post(envelope, transfer);
2920
+ return;
2921
+ }
2922
+ const bytes = c.exact ? codec.encode?.(envelope) : void 0;
2923
+ const size = bytes?.byteLength ?? estimateMessageSize(payload);
2924
+ const waits = frameSize != null || c.queue.length > 0 || c.enforced && c.outstanding > 0 && c.outstanding + size > c.window;
2925
+ const held = !bytes && waits ? {
2926
+ ...envelope,
2927
+ payload: structuredClone(payload, { transfer })
2928
+ } : void 0;
2929
+ const send = (whole = false) => {
2930
+ if (!bytes) {
2931
+ sendOnPort(id, held ? () => post(held) : () => post(envelope, transfer));
2932
+ return;
2933
+ }
2934
+ if (!whole && frameSize != null && c.enforced && c.exact && bytes.byteLength > frameSize) {
2935
+ for (let at = 0; at < bytes.byteLength; at += frameSize) {
2936
+ const data = bytes.subarray(at, at + frameSize);
2937
+ const frame = at + frameSize >= bytes.byteLength ? {
2938
+ type: "frame",
2939
+ id,
2940
+ data,
2941
+ last: true
2942
+ } : {
2943
+ type: "frame",
2944
+ id,
2945
+ data
2946
+ };
2947
+ sendOnPort(id, () => post(frame));
2948
+ }
2949
+ return;
2950
+ }
2951
+ sendOnPort(id, () => postBytes(bytes));
2952
+ };
2953
+ c.queue.push({
2954
+ send,
2955
+ size
2956
+ });
2957
+ c.queued += size;
2958
+ flush(c);
2959
+ }, (reason) => {
2960
+ release(reason);
2961
+ record.markClosed(reason);
2962
+ });
2963
+ function release(reason) {
2964
+ freePartial(open.get(id));
2965
+ open.delete(id);
2966
+ leaks?.unregister(record);
2967
+ if (c?.queue.length) {
2968
+ for (const item of c.queue.splice(0)) item.send(true);
2969
+ c.queued = 0;
2970
+ }
2971
+ sendOnPort(id, () => post({
2615
2972
  type: "close",
2616
2973
  id,
2617
2974
  reason
2975
+ }));
2976
+ releaseWaiters(c);
2977
+ }
2978
+ const { markClosed, isClosed, hasListeners } = self;
2979
+ if (c) {
2980
+ const deliver = self.deliver;
2981
+ self.deliver = (payload, size = estimateMessageSize(payload)) => {
2982
+ c.unconsumed += size;
2983
+ const done = deliver(payload);
2984
+ if (done) done.then(() => grant(id, c, size));
2985
+ else grant(id, c, size);
2986
+ return done;
2987
+ };
2988
+ setPortSendState(self.port, {
2989
+ bufferedAmount: () => c.queued,
2990
+ ready: (below = 0) => new Promise((resolve) => {
2991
+ if (c.queued <= below || isClosed()) nextTask(resolve);
2992
+ else c.waiters.push({
2993
+ below,
2994
+ resolve: () => nextTask(resolve)
2995
+ });
2996
+ })
2618
2997
  });
2619
- open.delete(id);
2620
- self.markClosed();
2998
+ }
2999
+ const record = {
3000
+ deliver: self.deliver,
3001
+ markClosed,
3002
+ isClosed,
3003
+ hasListeners,
3004
+ close: (reason) => {
3005
+ if (isClosed()) return;
3006
+ release(reason);
3007
+ markClosed(reason);
3008
+ }
3009
+ };
3010
+ leaks?.register(self.port, {
3011
+ id,
3012
+ record,
3013
+ release
3014
+ }, record);
3015
+ const now = Date.now();
3016
+ open.set(id, {
3017
+ id,
3018
+ meta,
3019
+ openedAt: now,
3020
+ lastActivity: now,
3021
+ handle: record,
3022
+ credit: c
2621
3023
  });
2622
- open.set(id, self);
2623
3024
  return self;
2624
3025
  };
2625
- const handleEnvelope = (envelope) => {
3026
+ const handleEnvelope = (envelope, event) => {
2626
3027
  if (muxClosed) return;
3028
+ heardSincePing = true;
3029
+ if (envelope.type === "ping" || envelope.type === "pong") {
3030
+ peerKeepsAlive = true;
3031
+ if (envelope.type === "ping") post({
3032
+ type: "pong",
3033
+ id: KEEPALIVE_ID
3034
+ });
3035
+ return;
3036
+ }
2627
3037
  if (envelope.type === "open") {
2628
3038
  if (open.has(envelope.id)) return;
2629
3039
  if (open.size >= maxPorts) {
@@ -2634,7 +3044,18 @@
2634
3044
  });
2635
3045
  return;
2636
3046
  }
2637
- const handle = attach(envelope.id);
3047
+ const handle = attach(envelope.id, envelope.meta, envelope.window, false, envelope.exact === true && exactCodec);
3048
+ const credit = open.get(envelope.id)?.credit;
3049
+ if (credit) post(credit.exact ? {
3050
+ type: "credit",
3051
+ id: envelope.id,
3052
+ bytes: 0,
3053
+ exact: true
3054
+ } : {
3055
+ type: "credit",
3056
+ id: envelope.id,
3057
+ bytes: 0
3058
+ });
2638
3059
  let accepted = false;
2639
3060
  if (onPort) try {
2640
3061
  accepted = onPort(handle.port, envelope.meta) !== false;
@@ -2649,57 +3070,215 @@
2649
3070
  id: envelope.id,
2650
3071
  reason: "rejected"
2651
3072
  });
3073
+ return;
2652
3074
  }
2653
3075
  return;
2654
3076
  }
2655
- const handle = open.get(envelope.id);
3077
+ const entry = open.get(envelope.id);
3078
+ if (envelope.type === "credit") {
3079
+ const c = entry?.credit;
3080
+ if (!c || !(envelope.bytes >= 0)) return;
3081
+ if (!c.enforced && c.opener) {
3082
+ if (c.exact && envelope.exact !== true) {
3083
+ c.exact = false;
3084
+ c.outstanding = 0;
3085
+ }
3086
+ post({
3087
+ type: "credit",
3088
+ id: envelope.id,
3089
+ bytes: 0
3090
+ });
3091
+ }
3092
+ c.enforced = true;
3093
+ c.outstanding = Math.max(0, c.outstanding - envelope.bytes);
3094
+ flush(c);
3095
+ return;
3096
+ }
2656
3097
  if (envelope.type === "message") {
2657
- if (!handle) return;
2658
- handle.deliver(envelope.payload);
3098
+ const sizeOf = (c) => (c.exact ? byteLengthOf(event.data) : void 0) ?? estimateMessageSize(envelope.payload);
3099
+ if (!entry) return;
3100
+ touch(envelope.id);
3101
+ entry.handle.deliver(envelope.payload, entry.credit ? sizeOf(entry.credit) : void 0);
2659
3102
  return;
2660
3103
  }
2661
- if (!handle) return;
3104
+ if (envelope.type === "frame") {
3105
+ if (!entry?.credit?.exact || !codec.decode || !(envelope.data instanceof Uint8Array)) return;
3106
+ touch(envelope.id);
3107
+ const c = entry.credit;
3108
+ const owed = c.unconsumed + c.toGrant;
3109
+ const partial = entry.partial ?? {
3110
+ chunks: [],
3111
+ bytes: 0,
3112
+ owed,
3113
+ charged: 0,
3114
+ ...owed > 0 ? { room: c.window - owed } : {}
3115
+ };
3116
+ entry.partial = partial;
3117
+ const size = envelope.data.byteLength;
3118
+ const charged = partial.room == null ? Math.max(0, partial.bytes + size - c.window) : 0;
3119
+ 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;
3120
+ if (overrun) {
3121
+ freePartial(entry);
3122
+ dropOutgoing(envelope.id);
3123
+ open.delete(envelope.id);
3124
+ releaseWaiters(c);
3125
+ entry.handle.markClosed(/* @__PURE__ */ new RangeError(`webrun-rpc: a framed message passed ${overrun}`));
3126
+ post({
3127
+ type: "close",
3128
+ id: envelope.id,
3129
+ reason: "max-reassembled-bytes"
3130
+ });
3131
+ return;
3132
+ }
3133
+ partial.bytes += size;
3134
+ reassembling += charged - partial.charged;
3135
+ partial.charged = charged;
3136
+ partial.chunks.push(envelope.data);
3137
+ if (!envelope.last) return;
3138
+ freePartial(entry);
3139
+ const whole = new Uint8Array(partial.bytes);
3140
+ let at = 0;
3141
+ for (const chunk of partial.chunks) {
3142
+ whole.set(chunk, at);
3143
+ at += chunk.byteLength;
3144
+ }
3145
+ const inner = codec.decode(whole);
3146
+ if (inner?.type !== "message" || inner.id !== envelope.id) return;
3147
+ entry.handle.deliver(inner.payload, whole.byteLength);
3148
+ return;
3149
+ }
3150
+ dropOutgoing(envelope.id);
3151
+ if (!entry) return;
3152
+ freePartial(entry);
2662
3153
  open.delete(envelope.id);
2663
- handle.markClosed(/* @__PURE__ */ new Error("webrun-rpc: the peer closed the port"));
3154
+ releaseWaiters(entry.credit);
3155
+ entry.handle.markClosed(/* @__PURE__ */ new Error("webrun-rpc: the peer closed the port"));
2664
3156
  };
2665
3157
  const listener = (event) => {
2666
3158
  const envelope = codec.read(event);
2667
- if (envelope) handleEnvelope(envelope);
3159
+ if (envelope) handleEnvelope(envelope, event);
3160
+ };
3161
+ let peerKeepsAlive = false;
3162
+ let heardSincePing = true;
3163
+ let unanswered = 0;
3164
+ let pinger;
3165
+ const tick = () => {
3166
+ if (!keepalive) return;
3167
+ const backlogged = (port.bufferedAmount ?? 0) > 0;
3168
+ if (heardSincePing) unanswered = 0;
3169
+ else if (peerKeepsAlive && !backlogged) unanswered++;
3170
+ if (unanswered >= misses) {
3171
+ close(/* @__PURE__ */ new Error(`webrun-rpc: the peer did not answer ${misses} keepalive pings, ${keepalive.intervalMs} ms apart`)).catch(() => {});
3172
+ return;
3173
+ }
3174
+ heardSincePing = false;
3175
+ post({
3176
+ type: "ping",
3177
+ id: KEEPALIVE_ID
3178
+ });
3179
+ };
3180
+ const close = async (reason) => {
3181
+ if (muxClosed) return;
3182
+ clearInterval(pinger);
3183
+ muxClosed = true;
3184
+ signal?.removeEventListener("abort", onTransportAbort);
3185
+ port.removeEventListener("message", listener);
3186
+ const closedNow = /* @__PURE__ */ new Set();
3187
+ for (const [id, { handle, credit }] of [...open]) {
3188
+ closedNow.add(id);
3189
+ codecPost({
3190
+ type: "close",
3191
+ id
3192
+ });
3193
+ open.delete(id);
3194
+ releaseWaiters(credit);
3195
+ handle.markClosed(reason ?? /* @__PURE__ */ new Error("webrun-rpc: the multiplexer was closed"));
3196
+ }
3197
+ for (const id of outgoing.keys()) if (!closedNow.has(id)) codecPost({
3198
+ type: "close",
3199
+ id
3200
+ });
3201
+ outgoing.clear();
3202
+ ring.length = 0;
3203
+ await port.close?.();
3204
+ };
3205
+ const onTransportAbort = () => {
3206
+ close(signal?.reason).catch(() => {});
2668
3207
  };
2669
3208
  port.addEventListener("message", listener);
2670
3209
  port.start?.();
3210
+ if (keepalive) {
3211
+ pinger = setInterval(tick, keepalive.intervalMs);
3212
+ pinger.unref?.();
3213
+ }
3214
+ if (signal?.aborted) close(signal.reason).catch(() => {});
3215
+ else signal?.addEventListener("abort", onTransportAbort, { once: true });
2671
3216
  return {
2672
3217
  maxMessageSize,
2673
- async openPort(meta) {
3218
+ async openPort(meta, { signal: portSignal } = {}) {
2674
3219
  if (muxClosed) throw new Error("webrun-rpc: the multiplexer is closed");
2675
- if (open.size >= maxPorts) throw new RangeError(`webrun-rpc: maxPorts (${maxPorts}) reached`);
3220
+ if (portSignal?.aborted) throw portSignal.reason;
3221
+ if (open.size >= maxPorts) throw new RangeError(`webrun-rpc: maxPorts (${maxPorts}) reached; open ports by kind: ${describeKinds(open.values())}`);
2676
3222
  while (open.has(nextId)) nextId += 2;
2677
3223
  const id = nextId;
2678
3224
  nextId += 2;
2679
- const handle = attach(id);
2680
- post({
3225
+ const handle = attach(id, meta, window, true, exactCodec);
3226
+ post(window ? exactCodec ? {
3227
+ type: "open",
3228
+ id,
3229
+ meta,
3230
+ window,
3231
+ exact: true
3232
+ } : {
3233
+ type: "open",
3234
+ id,
3235
+ meta,
3236
+ window
3237
+ } : {
2681
3238
  type: "open",
2682
3239
  id,
2683
3240
  meta
2684
3241
  });
3242
+ if (portSignal) {
3243
+ const record = open.get(id)?.handle;
3244
+ const onAbort = () => {
3245
+ if (record && open.get(id)?.handle === record) record.close(portSignal.reason);
3246
+ };
3247
+ portSignal.addEventListener("abort", onAbort, { once: true });
3248
+ getPortCloseSignal(handle.port)?.addEventListener("abort", () => portSignal.removeEventListener("abort", onAbort), { once: true });
3249
+ }
2685
3250
  return handle.port;
2686
3251
  },
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?.();
3252
+ close,
3253
+ ports() {
3254
+ return [...open.values()].map(({ handle: _, credit, ...info }) => credit ? {
3255
+ ...info,
3256
+ ...credit.enforced ? { window: credit.window } : {},
3257
+ queued: credit.queued,
3258
+ outstanding: credit.outstanding,
3259
+ unconsumed: credit.unconsumed
3260
+ } : info);
2700
3261
  }
2701
3262
  };
2702
3263
  }
3264
+ /** The byte length of a received message's data, or `undefined` if it is not bytes. */
3265
+ function byteLengthOf(data) {
3266
+ if (data instanceof ArrayBuffer || ArrayBuffer.isView(data)) return data.byteLength;
3267
+ }
3268
+ /**
3269
+ * `"http: 1000, ws: 24"`: the three commonest `meta.kind`s among `entries`, so
3270
+ * a full table names who filled it. A port without a kind counts as `"?"`.
3271
+ */
3272
+ function describeKinds(entries) {
3273
+ const counts = /* @__PURE__ */ new Map();
3274
+ for (const { meta } of entries) {
3275
+ const kind = meta?.kind;
3276
+ const name = typeof kind === "string" ? kind : "?";
3277
+ counts.set(name, (counts.get(name) ?? 0) + 1);
3278
+ }
3279
+ if (!counts.size) return "none";
3280
+ return [...counts].sort((x, y) => y[1] - x[1]).slice(0, 3).map(([name, n]) => `${name}: ${n}`).join(", ");
3281
+ }
2703
3282
  /**
2704
3283
  * Announced with every port this pair opens. Layer 1 never reads `meta` — it is
2705
3284
  * here so a peer running something else over the same multiplexer can tell a
@@ -2761,14 +3340,17 @@
2761
3340
  const mux = await factory((port) => {
2762
3341
  let off;
2763
3342
  let finished = false;
2764
- const tracked = (input) => (async function* () {
2765
- try {
2766
- yield* handler(input);
2767
- } finally {
2768
- finished = true;
2769
- if (off) live.delete(off);
2770
- }
2771
- })();
3343
+ const tracked = (input) => {
3344
+ const output = handler(input);
3345
+ return returningAtOnce((async function* () {
3346
+ try {
3347
+ yield* output;
3348
+ } finally {
3349
+ finished = true;
3350
+ if (off) live.delete(off);
3351
+ }
3352
+ })(), () => void output.return(void 0).catch(() => {}));
3353
+ };
2772
3354
  off = serveDuplexOverPort(port, tracked, streamOptions);
2773
3355
  if (!finished) live.add(off);
2774
3356
  });
@@ -2806,7 +3388,7 @@
2806
3388
  const candidate = value;
2807
3389
  if (typeof candidate.id !== "number") return false;
2808
3390
  if (!Number.isInteger(candidate.id) || candidate.id < 0) return false;
2809
- return candidate.type === "open" || candidate.type === "message" || candidate.type === "close";
3391
+ return candidate.type === "open" || candidate.type === "message" || candidate.type === "close" || candidate.type === "credit" || candidate.type === "frame" || candidate.type === "ping" || candidate.type === "pong";
2810
3392
  }
2811
3393
  /**
2812
3394
  * For ports whose messages are structured values — a real `MessagePort`, a
@@ -2968,7 +3550,7 @@
2968
3550
  finish();
2969
3551
  return response;
2970
3552
  }
2971
- return new Response(guardBody(response.body, lost, () => lostReason, finish), {
3553
+ return new Response(guardBody(response.body, lost, () => lostReason, finish, request.signal), {
2972
3554
  status: response.status,
2973
3555
  statusText: response.statusText,
2974
3556
  headers: response.headers
@@ -2987,33 +3569,65 @@
2987
3569
  return asyncDisposable(link, () => link.close());
2988
3570
  }
2989
3571
  /**
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.
3572
+ * A response body that reports its end (done, cancelled, failed or aborted) and
3573
+ * fails with the link's reason when the link is lost mid-stream. An abort of the
3574
+ * request's signal ends it at once, read or not: the exchange is over (the layer
3575
+ * below has given its port back), so the request leaves `inflight` then.
2992
3576
  */
2993
- function guardBody(body, lost, reason, finish) {
3577
+ function guardBody(body, lost, reason, finish, signal) {
2994
3578
  const reader = body.getReader();
3579
+ let controller;
3580
+ let ended = false;
3581
+ const end = () => {
3582
+ ended = true;
3583
+ signal?.removeEventListener("abort", onAbort);
3584
+ finish();
3585
+ };
3586
+ const onAbort = () => {
3587
+ if (ended) return;
3588
+ end();
3589
+ reader.cancel().catch(() => {});
3590
+ try {
3591
+ controller.error(abortError(signal));
3592
+ } catch {}
3593
+ };
2995
3594
  return new ReadableStream({
3595
+ start(c) {
3596
+ controller = c;
3597
+ if (signal?.aborted) onAbort();
3598
+ else signal?.addEventListener("abort", onAbort, { once: true });
3599
+ },
2996
3600
  async pull(controller) {
2997
3601
  try {
2998
3602
  const { done, value } = await Promise.race([reader.read(), lost]);
3603
+ if (ended) return;
2999
3604
  if (done) {
3000
- finish();
3605
+ end();
3001
3606
  controller.close();
3002
3607
  return;
3003
3608
  }
3004
3609
  controller.enqueue(value);
3005
3610
  } catch (error) {
3006
- finish();
3611
+ if (ended) return;
3612
+ end();
3007
3613
  reader.cancel().catch(() => {});
3008
3614
  controller.error(reason() ?? error);
3009
3615
  }
3010
3616
  },
3011
3617
  async cancel(why) {
3012
- finish();
3618
+ end();
3013
3619
  await reader.cancel(why).catch(() => {});
3014
3620
  }
3015
3621
  });
3016
3622
  }
3623
+ /** The signal's reason as an `Error` (`AbortError` when it gave none). */
3624
+ function abortError(signal) {
3625
+ const why = signal.reason;
3626
+ if (why instanceof Error) return why;
3627
+ const err = new Error(why === void 0 ? "Aborted" : String(why));
3628
+ err.name = "AbortError";
3629
+ return err;
3630
+ }
3017
3631
  //#endregion
3018
3632
  //#region src/link/locks.ts
3019
3633
  function lockManager() {