@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/index.js CHANGED
@@ -2598,7 +2598,7 @@ async function fetchOverDuplex(call, request, options = {}) {
2598
2598
  else await release?.();
2599
2599
  return new Response(null, responseInit);
2600
2600
  }
2601
- return new Response(asyncIterableToReadable(withAbort(respBody, request.signal)), responseInit);
2601
+ return new Response(responseBody(respBody, request.signal, release), responseInit);
2602
2602
  }
2603
2603
  /**
2604
2604
  * Wrap a `(Request) => Promise<Response>` handler as a `Duplex` so it can be
@@ -2729,15 +2729,78 @@ function trackingEnd(source) {
2729
2729
  settle
2730
2730
  };
2731
2731
  }
2732
- async function* withAbort(iter, signal) {
2733
- if (!signal) {
2734
- yield* iter;
2735
- return;
2736
- }
2737
- for await (const chunk of iter) {
2738
- if (signal.aborted) throw abortReason$1(signal);
2739
- yield chunk;
2740
- }
2732
+ /**
2733
+ * How long a cancelled body waits for the read in flight before it abandons the call anyway.
2734
+ * The read's answer tells a body that has ended (the call is finished: an upload the handler
2735
+ * reads after answering goes on) from one that has not; a peer that sends nothing more never
2736
+ * answers it. A body whose end comes later than this after the cancel is abandoned too, so a
2737
+ * handler still streaming its response and still reading the upload then loses the upload.
2738
+ */
2739
+ const CANCEL_GRACE_MS = 1e3;
2740
+ /**
2741
+ * The response body as a stream that the request's signal can end at any time.
2742
+ *
2743
+ * An abort errors the stream with the abort's reason at once — a read waiting
2744
+ * for a peer that sends nothing more rejects then, not when a next chunk
2745
+ * arrives, which may be never — and abandons the call (`release`), so on a
2746
+ * multiplexed transport its port is given back whether or not anyone still
2747
+ * reads the body. The listener is removed once the body ends, however it ends:
2748
+ * read to its end, cancelled, failed or aborted. A body nobody reads or cancels
2749
+ * keeps it until the signal aborts (and keeps the call open until then anyway).
2750
+ * A cancel abandons the call too — unless the read in flight comes back with the
2751
+ * body's end, which finishes it instead (see `cancel`).
2752
+ */
2753
+ function responseBody(body, signal, release) {
2754
+ const it = body[Symbol.asyncIterator]();
2755
+ let controller;
2756
+ let ended = false;
2757
+ let reading = false;
2758
+ const end = () => {
2759
+ ended = true;
2760
+ signal?.removeEventListener("abort", onAbort);
2761
+ };
2762
+ const onAbort = () => {
2763
+ if (ended) return;
2764
+ end();
2765
+ try {
2766
+ controller.error(abortReason$1(signal));
2767
+ } catch {}
2768
+ release?.();
2769
+ };
2770
+ return new ReadableStream({
2771
+ start(c) {
2772
+ controller = c;
2773
+ if (signal?.aborted) onAbort();
2774
+ else signal?.addEventListener("abort", onAbort, { once: true });
2775
+ },
2776
+ async pull(c) {
2777
+ try {
2778
+ reading = true;
2779
+ const { done, value } = await it.next().finally(() => {
2780
+ reading = false;
2781
+ });
2782
+ if (ended) return;
2783
+ if (done) {
2784
+ end();
2785
+ c.close();
2786
+ return;
2787
+ }
2788
+ c.enqueue(value);
2789
+ } catch (err) {
2790
+ if (ended) return;
2791
+ end();
2792
+ c.error(err);
2793
+ }
2794
+ },
2795
+ cancel() {
2796
+ end();
2797
+ const returned = Promise.resolve(it.return?.()).catch(() => {});
2798
+ if (!reading) return;
2799
+ const grace = setTimeout(() => void release?.(), CANCEL_GRACE_MS);
2800
+ grace.unref?.();
2801
+ returned.finally(() => clearTimeout(grace));
2802
+ }
2803
+ });
2741
2804
  }
2742
2805
  /**
2743
2806
  * Host a `SiteHandler` over a `Connect/Serve` pair. The `start(serve, params)`
@@ -3024,8 +3087,11 @@ function listenPort(port, handler, { channelName = "", log = () => {} } = {}) {
3024
3087
  error
3025
3088
  });
3026
3089
  };
3027
- port.addEventListener("message", onMessage);
3028
- return () => port.removeEventListener("message", onMessage);
3090
+ const listener = (event) => {
3091
+ onMessage(event);
3092
+ };
3093
+ port.addEventListener("message", listener);
3094
+ return () => port.removeEventListener("message", listener);
3029
3095
  }
3030
3096
  /**
3031
3097
  * Wraps an async iterable so that an `AbortSignal` firing causes the wrapper
@@ -3430,6 +3496,33 @@ async function sendChunks(port, channelName, output, { maxMessageSize, log }, si
3430
3496
  }
3431
3497
  }
3432
3498
  /**
3499
+ * Runs `fn` on a later task, never a microtask: a producer looping on
3500
+ * `whenPortReady` must let the transport's own events run. `setImmediate`
3501
+ * where it exists; elsewhere a `MessageChannel`, because nested `setTimeout`s
3502
+ * are clamped to 4 ms in browsers.
3503
+ */
3504
+ const setImmediateFn = globalThis.setImmediate;
3505
+ /** @internal */
3506
+ const nextTask = setImmediateFn ? (fn) => {
3507
+ setImmediateFn(fn);
3508
+ } : (() => {
3509
+ let channel;
3510
+ const queue = [];
3511
+ return (fn) => {
3512
+ if (!channel) {
3513
+ channel = new MessageChannel();
3514
+ channel.port1.onmessage = () => queue.shift()?.();
3515
+ }
3516
+ queue.push(fn);
3517
+ channel.port2.postMessage(0);
3518
+ };
3519
+ })();
3520
+ const states = /* @__PURE__ */ new WeakMap();
3521
+ /** @internal Registered by the multiplexer for each port with a window. */
3522
+ function setPortSendState(port, state) {
3523
+ states.set(port, state);
3524
+ }
3525
+ /**
3433
3526
  * One virtual port.
3434
3527
  *
3435
3528
  * `deliver` and `markClosed` are deliberately not on `port`: the consumer holds
@@ -3444,6 +3537,14 @@ function newVirtualPort(send, requestClose) {
3444
3537
  if (closing.signal.aborted) return;
3445
3538
  closing.abort(reason ?? /* @__PURE__ */ new Error("webrun-rpc: the port was closed"));
3446
3539
  };
3540
+ const close = (reason) => {
3541
+ if (closed) return;
3542
+ closed = true;
3543
+ const notify = requestClose;
3544
+ listeners.clear();
3545
+ notify(reason);
3546
+ abort(reason);
3547
+ };
3447
3548
  const port = {
3448
3549
  addEventListener(_type, listener) {
3449
3550
  listeners.add(listener);
@@ -3455,24 +3556,28 @@ function newVirtualPort(send, requestClose) {
3455
3556
  if (closed) return;
3456
3557
  send(message, transfer);
3457
3558
  },
3458
- close() {
3459
- if (closed) return;
3460
- closed = true;
3461
- const notify = requestClose;
3462
- listeners.clear();
3463
- notify();
3464
- abort();
3465
- }
3559
+ close
3466
3560
  };
3467
3561
  setPortCloseSignal(port, closing.signal);
3562
+ const dispose = Symbol.dispose;
3563
+ if (dispose) Object.defineProperty(port, dispose, {
3564
+ value: () => close(),
3565
+ configurable: true
3566
+ });
3468
3567
  return {
3469
3568
  port,
3470
3569
  deliver(payload) {
3471
3570
  if (closed) return;
3472
3571
  const event = new MessageEvent("message", { data: payload });
3572
+ let pending;
3473
3573
  for (const listener of [...listeners]) try {
3474
- listener(event);
3574
+ const result = listener(event);
3575
+ if (result && typeof result.then === "function") {
3576
+ pending ??= [];
3577
+ pending.push(result);
3578
+ }
3475
3579
  } catch {}
3580
+ return pending ? Promise.allSettled(pending) : void 0;
3476
3581
  },
3477
3582
  markClosed(reason) {
3478
3583
  closed = true;
@@ -3481,53 +3586,309 @@ function newVirtualPort(send, requestClose) {
3481
3586
  },
3482
3587
  isClosed() {
3483
3588
  return closed;
3589
+ },
3590
+ hasListeners() {
3591
+ return listeners.size > 0;
3484
3592
  }
3485
3593
  };
3486
3594
  }
3487
3595
  /**
3488
- * The message of the error that fails the calls still in flight on a
3489
- * multiplexer when it is closed (`PortMux.close()`).
3596
+ * The id of keepalive's `ping` / `pong`: one no port ever takes. An older mux
3597
+ * whose codec passes unknown envelopes through reads them as a close of that
3598
+ * id, which must therefore not be a real port (0 is the initiator's first).
3490
3599
  */
3491
- const MUX_CLOSED_MESSAGE = "webrun-rpc: the multiplexer was closed";
3600
+ const KEEPALIVE_ID = Number.MAX_SAFE_INTEGER;
3601
+ /** The longest delay a timer holds; past it `setInterval` fires at once. */
3602
+ const MAX_TIMER_MS = 2 ** 31 - 1;
3492
3603
  /** Ceiling on concurrently open virtual ports. Bounds the id table only. */
3493
3604
  const DEFAULT_MAX_PORTS = 1024;
3494
3605
  /**
3606
+ * What a message costs a port's window when the codec cannot say
3607
+ * (`PortCodec.encode`): its bytes and string lengths, found in its values up
3608
+ * to 4 levels deep (at most 256 values), and at least 64. Both ends compute it
3609
+ * from the same structured value, so they agree.
3610
+ */
3611
+ function estimateMessageSize(payload) {
3612
+ let budget = 256;
3613
+ const walk = (v, depth) => {
3614
+ if (budget-- <= 0) return 0;
3615
+ if (typeof v === "string") return v.length;
3616
+ if (v instanceof ArrayBuffer) return v.byteLength;
3617
+ if (ArrayBuffer.isView(v)) return v.byteLength;
3618
+ if (v === null || typeof v !== "object" || depth >= 4) return 8;
3619
+ let n = 16;
3620
+ if (Array.isArray(v)) for (const x of v) n += walk(x, depth + 1);
3621
+ else for (const k in v) n += k.length + walk(v[k], depth + 1);
3622
+ return n;
3623
+ };
3624
+ return Math.max(64, walk(payload, 0));
3625
+ }
3626
+ /**
3495
3627
  * The default `PortMux`: emulates multiplexing over a single port.
3496
3628
  *
3497
3629
  * A transport that already multiplexes natively supplies its own `PortMux`
3498
3630
  * instead — this implementation is for transports that offer one pipe.
3499
3631
  */
3500
3632
  function multiplexPort(port, options) {
3501
- const { codec, onPort, side = "initiator", maxPorts = DEFAULT_MAX_PORTS, maxMessageSize } = options;
3633
+ const { codec, onPort, side = "initiator", maxPorts = DEFAULT_MAX_PORTS, maxMessageSize, signal, window, frameSize, keepalive, onLeak } = options;
3634
+ const misses = keepalive?.misses ?? 3;
3635
+ 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`);
3636
+ if (frameSize != null && !(Number.isSafeInteger(frameSize) && frameSize > 0)) throw new RangeError(`webrun-rpc: frameSize must be a positive integer, got ${frameSize}`);
3502
3637
  const open = /* @__PURE__ */ new Map();
3638
+ const leaks = onLeak ? new FinalizationRegistry(({ id, record, release }) => {
3639
+ const entry = open.get(id);
3640
+ if (entry?.handle !== record) return;
3641
+ const { handle: _, credit: __, partial: ___, ...info } = entry;
3642
+ if (!record.hasListeners()) {
3643
+ release("leaked");
3644
+ record.markClosed(/* @__PURE__ */ new Error("webrun-rpc: the port was dropped without close()"));
3645
+ }
3646
+ try {
3647
+ onLeak?.(info);
3648
+ } catch {}
3649
+ }) : void 0;
3650
+ const touch = (id) => {
3651
+ const entry = open.get(id);
3652
+ if (entry) entry.lastActivity = Date.now();
3653
+ };
3503
3654
  let nextId = side === "initiator" ? 0 : 1;
3504
3655
  let muxClosed = false;
3656
+ /** This end counts encoded bytes and reassembles frames. */
3657
+ const exactCodec = codec.encode != null && codec.decode != null;
3505
3658
  const post = (envelope, transfer) => {
3506
3659
  if (muxClosed) return;
3660
+ codecPost(envelope, transfer);
3661
+ };
3662
+ /** Like `post`, also while closing: the mux's own close notices. */
3663
+ const codecPost = (envelope, transfer) => {
3507
3664
  try {
3508
3665
  codec.post(port, envelope, transfer);
3509
3666
  } catch {}
3510
3667
  };
3511
- const attach = (id) => {
3512
- let self;
3513
- self = newVirtualPort((payload, transfer) => post({
3514
- type: "message",
3515
- id,
3516
- payload
3517
- }, transfer), (reason) => {
3668
+ const outgoing = /* @__PURE__ */ new Map();
3669
+ const ring = [];
3670
+ let pumping = false;
3671
+ const pump = async () => {
3672
+ pumping = true;
3673
+ try {
3674
+ while (ring.length && !muxClosed) {
3675
+ if (port.whenDrained) await port.whenDrained();
3676
+ if (muxClosed) break;
3677
+ const id = ring.shift();
3678
+ const queue = outgoing.get(id);
3679
+ const item = queue?.shift();
3680
+ if (queue?.length) ring.push(id);
3681
+ else outgoing.delete(id);
3682
+ item?.();
3683
+ }
3684
+ } catch (err) {
3685
+ close(err).catch(() => {});
3686
+ } finally {
3687
+ pumping = false;
3688
+ }
3689
+ };
3690
+ const sendOnPort = (id, item) => {
3691
+ if (frameSize == null) {
3692
+ item();
3693
+ return;
3694
+ }
3695
+ let queue = outgoing.get(id);
3696
+ if (!queue) {
3697
+ queue = [];
3698
+ outgoing.set(id, queue);
3699
+ ring.push(id);
3700
+ }
3701
+ queue.push(item);
3702
+ if (!pumping) pump();
3703
+ };
3704
+ const dropOutgoing = (id) => {
3705
+ if (!outgoing.delete(id)) return;
3706
+ const at = ring.indexOf(id);
3707
+ if (at >= 0) ring.splice(at, 1);
3708
+ };
3709
+ const postBytes = (bytes) => {
3710
+ if (muxClosed) return;
3711
+ try {
3712
+ port.postMessage(bytes);
3713
+ } catch {}
3714
+ };
3715
+ const releaseWaiters = (c) => {
3716
+ for (const w of c?.waiters.splice(0) ?? []) w.resolve();
3717
+ };
3718
+ const flush = (c) => {
3719
+ while (c.queue.length) {
3720
+ const next = c.queue[0];
3721
+ if (c.enforced && c.outstanding > 0 && c.outstanding + next.size > c.window) break;
3722
+ c.queue.shift();
3723
+ c.queued -= next.size;
3724
+ c.outstanding += next.size;
3725
+ next.send();
3726
+ }
3727
+ for (const w of [...c.waiters]) if (c.queued <= w.below) {
3728
+ c.waiters.splice(c.waiters.indexOf(w), 1);
3729
+ w.resolve();
3730
+ }
3731
+ };
3732
+ const grant = (id, c, bytes) => {
3733
+ c.unconsumed -= bytes;
3734
+ c.toGrant += bytes;
3735
+ const send = () => {
3736
+ c.grantScheduled = false;
3737
+ if (!c.toGrant || !open.has(id)) return;
3518
3738
  post({
3739
+ type: "credit",
3740
+ id,
3741
+ bytes: c.toGrant
3742
+ });
3743
+ c.toGrant = 0;
3744
+ };
3745
+ if (c.toGrant >= c.window / 4) {
3746
+ send();
3747
+ return;
3748
+ }
3749
+ if (c.grantScheduled) return;
3750
+ c.grantScheduled = true;
3751
+ setTimeout(send, 0);
3752
+ };
3753
+ const attach = (id, meta, portWindow, opener, exact) => {
3754
+ const c = typeof portWindow === "number" && Number.isSafeInteger(portWindow) && portWindow > 0 ? {
3755
+ window: portWindow,
3756
+ enforced: false,
3757
+ opener,
3758
+ exact,
3759
+ outstanding: 0,
3760
+ queue: [],
3761
+ queued: 0,
3762
+ unconsumed: 0,
3763
+ waiters: [],
3764
+ toGrant: 0,
3765
+ grantScheduled: false
3766
+ } : void 0;
3767
+ const self = newVirtualPort((payload, transfer) => {
3768
+ touch(id);
3769
+ const envelope = {
3770
+ type: "message",
3771
+ id,
3772
+ payload
3773
+ };
3774
+ if (!c) {
3775
+ post(envelope, transfer);
3776
+ return;
3777
+ }
3778
+ const bytes = c.exact ? codec.encode?.(envelope) : void 0;
3779
+ const size = bytes?.byteLength ?? estimateMessageSize(payload);
3780
+ const waits = frameSize != null || c.queue.length > 0 || c.enforced && c.outstanding > 0 && c.outstanding + size > c.window;
3781
+ const held = !bytes && waits ? {
3782
+ ...envelope,
3783
+ payload: structuredClone(payload, { transfer })
3784
+ } : void 0;
3785
+ const send = () => {
3786
+ if (!bytes) {
3787
+ sendOnPort(id, held ? () => post(held) : () => post(envelope, transfer));
3788
+ return;
3789
+ }
3790
+ if (frameSize != null && c.enforced && c.exact && bytes.byteLength > frameSize) {
3791
+ for (let at = 0; at < bytes.byteLength; at += frameSize) {
3792
+ const data = bytes.subarray(at, at + frameSize);
3793
+ const frame = at + frameSize >= bytes.byteLength ? {
3794
+ type: "frame",
3795
+ id,
3796
+ data,
3797
+ last: true
3798
+ } : {
3799
+ type: "frame",
3800
+ id,
3801
+ data
3802
+ };
3803
+ sendOnPort(id, () => post(frame));
3804
+ }
3805
+ return;
3806
+ }
3807
+ sendOnPort(id, () => postBytes(bytes));
3808
+ };
3809
+ c.queue.push({
3810
+ send,
3811
+ size
3812
+ });
3813
+ c.queued += size;
3814
+ flush(c);
3815
+ }, (reason) => {
3816
+ release(reason);
3817
+ record.markClosed(reason);
3818
+ });
3819
+ function release(reason) {
3820
+ open.delete(id);
3821
+ leaks?.unregister(record);
3822
+ if (c?.queue.length) {
3823
+ for (const item of c.queue.splice(0)) item.send();
3824
+ c.queued = 0;
3825
+ }
3826
+ sendOnPort(id, () => post({
3519
3827
  type: "close",
3520
3828
  id,
3521
3829
  reason
3830
+ }));
3831
+ releaseWaiters(c);
3832
+ }
3833
+ const { markClosed, isClosed, hasListeners } = self;
3834
+ if (c) {
3835
+ const deliver = self.deliver;
3836
+ self.deliver = (payload, size = estimateMessageSize(payload)) => {
3837
+ c.unconsumed += size;
3838
+ const done = deliver(payload);
3839
+ if (done) done.then(() => grant(id, c, size));
3840
+ else grant(id, c, size);
3841
+ return done;
3842
+ };
3843
+ setPortSendState(self.port, {
3844
+ bufferedAmount: () => c.queued,
3845
+ ready: (below = 0) => new Promise((resolve) => {
3846
+ if (c.queued <= below || isClosed()) nextTask(resolve);
3847
+ else c.waiters.push({
3848
+ below,
3849
+ resolve: () => nextTask(resolve)
3850
+ });
3851
+ })
3522
3852
  });
3523
- open.delete(id);
3524
- self.markClosed();
3853
+ }
3854
+ const record = {
3855
+ deliver: self.deliver,
3856
+ markClosed,
3857
+ isClosed,
3858
+ hasListeners,
3859
+ close: (reason) => {
3860
+ if (isClosed()) return;
3861
+ release(reason);
3862
+ markClosed(reason);
3863
+ }
3864
+ };
3865
+ leaks?.register(self.port, {
3866
+ id,
3867
+ record,
3868
+ release
3869
+ }, record);
3870
+ const now = Date.now();
3871
+ open.set(id, {
3872
+ id,
3873
+ meta,
3874
+ openedAt: now,
3875
+ lastActivity: now,
3876
+ handle: record,
3877
+ credit: c
3525
3878
  });
3526
- open.set(id, self);
3527
3879
  return self;
3528
3880
  };
3529
- const handleEnvelope = (envelope) => {
3881
+ const handleEnvelope = (envelope, event) => {
3530
3882
  if (muxClosed) return;
3883
+ heardSincePing = true;
3884
+ if (envelope.type === "ping" || envelope.type === "pong") {
3885
+ peerKeepsAlive = true;
3886
+ if (envelope.type === "ping") post({
3887
+ type: "pong",
3888
+ id: KEEPALIVE_ID
3889
+ });
3890
+ return;
3891
+ }
3531
3892
  if (envelope.type === "open") {
3532
3893
  if (open.has(envelope.id)) return;
3533
3894
  if (open.size >= maxPorts) {
@@ -3538,7 +3899,18 @@ function multiplexPort(port, options) {
3538
3899
  });
3539
3900
  return;
3540
3901
  }
3541
- const handle = attach(envelope.id);
3902
+ const handle = attach(envelope.id, envelope.meta, envelope.window, false, envelope.exact === true && exactCodec);
3903
+ const credit = open.get(envelope.id)?.credit;
3904
+ if (credit) post(credit.exact ? {
3905
+ type: "credit",
3906
+ id: envelope.id,
3907
+ bytes: 0,
3908
+ exact: true
3909
+ } : {
3910
+ type: "credit",
3911
+ id: envelope.id,
3912
+ bytes: 0
3913
+ });
3542
3914
  let accepted = false;
3543
3915
  if (onPort) try {
3544
3916
  accepted = onPort(handle.port, envelope.meta) !== false;
@@ -3553,57 +3925,191 @@ function multiplexPort(port, options) {
3553
3925
  id: envelope.id,
3554
3926
  reason: "rejected"
3555
3927
  });
3928
+ return;
3556
3929
  }
3557
3930
  return;
3558
3931
  }
3559
- const handle = open.get(envelope.id);
3932
+ const entry = open.get(envelope.id);
3933
+ if (envelope.type === "credit") {
3934
+ const c = entry?.credit;
3935
+ if (!c || !(envelope.bytes >= 0)) return;
3936
+ if (!c.enforced && c.opener) {
3937
+ if (c.exact && envelope.exact !== true) {
3938
+ c.exact = false;
3939
+ c.outstanding = 0;
3940
+ }
3941
+ post({
3942
+ type: "credit",
3943
+ id: envelope.id,
3944
+ bytes: 0
3945
+ });
3946
+ }
3947
+ c.enforced = true;
3948
+ c.outstanding = Math.max(0, c.outstanding - envelope.bytes);
3949
+ flush(c);
3950
+ return;
3951
+ }
3560
3952
  if (envelope.type === "message") {
3561
- if (!handle) return;
3562
- handle.deliver(envelope.payload);
3953
+ const sizeOf = (c) => (c.exact ? byteLengthOf(event.data) : void 0) ?? estimateMessageSize(envelope.payload);
3954
+ if (!entry) return;
3955
+ touch(envelope.id);
3956
+ entry.handle.deliver(envelope.payload, entry.credit ? sizeOf(entry.credit) : void 0);
3563
3957
  return;
3564
3958
  }
3565
- if (!handle) return;
3959
+ if (envelope.type === "frame") {
3960
+ if (!entry?.credit?.exact || !codec.decode || !(envelope.data instanceof Uint8Array)) return;
3961
+ touch(envelope.id);
3962
+ const partial = entry.partial ?? {
3963
+ chunks: [],
3964
+ bytes: 0
3965
+ };
3966
+ entry.partial = partial;
3967
+ partial.chunks.push(envelope.data);
3968
+ partial.bytes += envelope.data.byteLength;
3969
+ if (!envelope.last) return;
3970
+ entry.partial = void 0;
3971
+ const whole = new Uint8Array(partial.bytes);
3972
+ let at = 0;
3973
+ for (const chunk of partial.chunks) {
3974
+ whole.set(chunk, at);
3975
+ at += chunk.byteLength;
3976
+ }
3977
+ const inner = codec.decode(whole);
3978
+ if (inner?.type !== "message" || inner.id !== envelope.id) return;
3979
+ entry.handle.deliver(inner.payload, whole.byteLength);
3980
+ return;
3981
+ }
3982
+ dropOutgoing(envelope.id);
3983
+ if (!entry) return;
3566
3984
  open.delete(envelope.id);
3567
- handle.markClosed(/* @__PURE__ */ new Error("webrun-rpc: the peer closed the port"));
3985
+ releaseWaiters(entry.credit);
3986
+ entry.handle.markClosed(/* @__PURE__ */ new Error("webrun-rpc: the peer closed the port"));
3568
3987
  };
3569
3988
  const listener = (event) => {
3570
3989
  const envelope = codec.read(event);
3571
- if (envelope) handleEnvelope(envelope);
3990
+ if (envelope) handleEnvelope(envelope, event);
3991
+ };
3992
+ let peerKeepsAlive = false;
3993
+ let heardSincePing = true;
3994
+ let unanswered = 0;
3995
+ let pinger;
3996
+ const tick = () => {
3997
+ if (!keepalive) return;
3998
+ const backlogged = (port.bufferedAmount ?? 0) > 0;
3999
+ if (heardSincePing) unanswered = 0;
4000
+ else if (peerKeepsAlive && !backlogged) unanswered++;
4001
+ if (unanswered >= misses) {
4002
+ close(/* @__PURE__ */ new Error(`webrun-rpc: the peer did not answer ${misses} keepalive pings, ${keepalive.intervalMs} ms apart`)).catch(() => {});
4003
+ return;
4004
+ }
4005
+ heardSincePing = false;
4006
+ post({
4007
+ type: "ping",
4008
+ id: KEEPALIVE_ID
4009
+ });
4010
+ };
4011
+ const close = async (reason) => {
4012
+ if (muxClosed) return;
4013
+ clearInterval(pinger);
4014
+ muxClosed = true;
4015
+ signal?.removeEventListener("abort", onTransportAbort);
4016
+ port.removeEventListener("message", listener);
4017
+ const closedNow = /* @__PURE__ */ new Set();
4018
+ for (const [id, { handle, credit }] of [...open]) {
4019
+ closedNow.add(id);
4020
+ codecPost({
4021
+ type: "close",
4022
+ id
4023
+ });
4024
+ open.delete(id);
4025
+ releaseWaiters(credit);
4026
+ handle.markClosed(reason ?? /* @__PURE__ */ new Error("webrun-rpc: the multiplexer was closed"));
4027
+ }
4028
+ for (const id of outgoing.keys()) if (!closedNow.has(id)) codecPost({
4029
+ type: "close",
4030
+ id
4031
+ });
4032
+ outgoing.clear();
4033
+ ring.length = 0;
4034
+ await port.close?.();
4035
+ };
4036
+ const onTransportAbort = () => {
4037
+ close(signal?.reason).catch(() => {});
3572
4038
  };
3573
4039
  port.addEventListener("message", listener);
3574
4040
  port.start?.();
4041
+ if (keepalive) {
4042
+ pinger = setInterval(tick, keepalive.intervalMs);
4043
+ pinger.unref?.();
4044
+ }
4045
+ if (signal?.aborted) close(signal.reason).catch(() => {});
4046
+ else signal?.addEventListener("abort", onTransportAbort, { once: true });
3575
4047
  return {
3576
4048
  maxMessageSize,
3577
- async openPort(meta) {
4049
+ async openPort(meta, { signal: portSignal } = {}) {
3578
4050
  if (muxClosed) throw new Error("webrun-rpc: the multiplexer is closed");
3579
- if (open.size >= maxPorts) throw new RangeError(`webrun-rpc: maxPorts (${maxPorts}) reached`);
4051
+ if (portSignal?.aborted) throw portSignal.reason;
4052
+ if (open.size >= maxPorts) throw new RangeError(`webrun-rpc: maxPorts (${maxPorts}) reached; open ports by kind: ${describeKinds(open.values())}`);
3580
4053
  while (open.has(nextId)) nextId += 2;
3581
4054
  const id = nextId;
3582
4055
  nextId += 2;
3583
- const handle = attach(id);
3584
- post({
4056
+ const handle = attach(id, meta, window, true, exactCodec);
4057
+ post(window ? exactCodec ? {
4058
+ type: "open",
4059
+ id,
4060
+ meta,
4061
+ window,
4062
+ exact: true
4063
+ } : {
4064
+ type: "open",
4065
+ id,
4066
+ meta,
4067
+ window
4068
+ } : {
3585
4069
  type: "open",
3586
4070
  id,
3587
4071
  meta
3588
4072
  });
4073
+ if (portSignal) {
4074
+ const record = open.get(id)?.handle;
4075
+ const onAbort = () => {
4076
+ if (record && open.get(id)?.handle === record) record.close(portSignal.reason);
4077
+ };
4078
+ portSignal.addEventListener("abort", onAbort, { once: true });
4079
+ getPortCloseSignal(handle.port)?.addEventListener("abort", () => portSignal.removeEventListener("abort", onAbort), { once: true });
4080
+ }
3589
4081
  return handle.port;
3590
4082
  },
3591
- async close() {
3592
- if (muxClosed) return;
3593
- for (const [id, handle] of [...open]) {
3594
- post({
3595
- type: "close",
3596
- id
3597
- });
3598
- open.delete(id);
3599
- handle.markClosed(/* @__PURE__ */ new Error(MUX_CLOSED_MESSAGE));
3600
- }
3601
- muxClosed = true;
3602
- port.removeEventListener("message", listener);
3603
- await port.close?.();
4083
+ close,
4084
+ ports() {
4085
+ return [...open.values()].map(({ handle: _, credit, ...info }) => credit ? {
4086
+ ...info,
4087
+ ...credit.enforced ? { window: credit.window } : {},
4088
+ queued: credit.queued,
4089
+ outstanding: credit.outstanding,
4090
+ unconsumed: credit.unconsumed
4091
+ } : info);
3604
4092
  }
3605
4093
  };
3606
4094
  }
4095
+ /** The byte length of a received message's data, or `undefined` if it is not bytes. */
4096
+ function byteLengthOf(data) {
4097
+ if (data instanceof ArrayBuffer || ArrayBuffer.isView(data)) return data.byteLength;
4098
+ }
4099
+ /**
4100
+ * `"http: 1000, ws: 24"`: the three commonest `meta.kind`s among `entries`, so
4101
+ * a full table names who filled it. A port without a kind counts as `"?"`.
4102
+ */
4103
+ function describeKinds(entries) {
4104
+ const counts = /* @__PURE__ */ new Map();
4105
+ for (const { meta } of entries) {
4106
+ const kind = meta?.kind;
4107
+ const name = typeof kind === "string" ? kind : "?";
4108
+ counts.set(name, (counts.get(name) ?? 0) + 1);
4109
+ }
4110
+ if (!counts.size) return "none";
4111
+ return [...counts].sort((x, y) => y[1] - x[1]).slice(0, 3).map(([name, n]) => `${name}: ${n}`).join(", ");
4112
+ }
3607
4113
  /**
3608
4114
  * Announced with every port this pair opens. Layer 1 never reads `meta` — it is
3609
4115
  * here so a peer running something else over the same multiplexer can tell a
@@ -3710,7 +4216,7 @@ function isEnvelope(value) {
3710
4216
  const candidate = value;
3711
4217
  if (typeof candidate.id !== "number") return false;
3712
4218
  if (!Number.isInteger(candidate.id) || candidate.id < 0) return false;
3713
- return candidate.type === "open" || candidate.type === "message" || candidate.type === "close";
4219
+ return candidate.type === "open" || candidate.type === "message" || candidate.type === "close" || candidate.type === "credit" || candidate.type === "frame" || candidate.type === "ping" || candidate.type === "pong";
3714
4220
  }
3715
4221
  /**
3716
4222
  * For ports whose messages are structured values — a real `MessagePort`, a
@@ -3872,7 +4378,7 @@ function trackedLink(send, release) {
3872
4378
  finish();
3873
4379
  return response;
3874
4380
  }
3875
- return new Response(guardBody(response.body, lost, () => lostReason, finish), {
4381
+ return new Response(guardBody(response.body, lost, () => lostReason, finish, request.signal), {
3876
4382
  status: response.status,
3877
4383
  statusText: response.statusText,
3878
4384
  headers: response.headers
@@ -3891,33 +4397,65 @@ function trackedLink(send, release) {
3891
4397
  return asyncDisposable(link, () => link.close());
3892
4398
  }
3893
4399
  /**
3894
- * A response body that reports its end (done, cancelled or failed) and fails
3895
- * with the link's reason when the link is lost mid-stream.
4400
+ * A response body that reports its end (done, cancelled, failed or aborted) and
4401
+ * fails with the link's reason when the link is lost mid-stream. An abort of the
4402
+ * request's signal ends it at once, read or not: the exchange is over (the layer
4403
+ * below has given its port back), so the request leaves `inflight` then.
3896
4404
  */
3897
- function guardBody(body, lost, reason, finish) {
4405
+ function guardBody(body, lost, reason, finish, signal) {
3898
4406
  const reader = body.getReader();
4407
+ let controller;
4408
+ let ended = false;
4409
+ const end = () => {
4410
+ ended = true;
4411
+ signal?.removeEventListener("abort", onAbort);
4412
+ finish();
4413
+ };
4414
+ const onAbort = () => {
4415
+ if (ended) return;
4416
+ end();
4417
+ reader.cancel().catch(() => {});
4418
+ try {
4419
+ controller.error(abortError(signal));
4420
+ } catch {}
4421
+ };
3899
4422
  return new ReadableStream({
4423
+ start(c) {
4424
+ controller = c;
4425
+ if (signal?.aborted) onAbort();
4426
+ else signal?.addEventListener("abort", onAbort, { once: true });
4427
+ },
3900
4428
  async pull(controller) {
3901
4429
  try {
3902
4430
  const { done, value } = await Promise.race([reader.read(), lost]);
4431
+ if (ended) return;
3903
4432
  if (done) {
3904
- finish();
4433
+ end();
3905
4434
  controller.close();
3906
4435
  return;
3907
4436
  }
3908
4437
  controller.enqueue(value);
3909
4438
  } catch (error) {
3910
- finish();
4439
+ if (ended) return;
4440
+ end();
3911
4441
  reader.cancel().catch(() => {});
3912
4442
  controller.error(reason() ?? error);
3913
4443
  }
3914
4444
  },
3915
4445
  async cancel(why) {
3916
- finish();
4446
+ end();
3917
4447
  await reader.cancel(why).catch(() => {});
3918
4448
  }
3919
4449
  });
3920
4450
  }
4451
+ /** The signal's reason as an `Error` (`AbortError` when it gave none). */
4452
+ function abortError(signal) {
4453
+ const why = signal.reason;
4454
+ if (why instanceof Error) return why;
4455
+ const err = new Error(why === void 0 ? "Aborted" : String(why));
4456
+ err.name = "AbortError";
4457
+ return err;
4458
+ }
3921
4459
  //#endregion
3922
4460
  //#region src/link/locks.ts
3923
4461
  function lockManager() {
@@ -6638,6 +7176,8 @@ var WorkerLinkImpl = class {
6638
7176
  _ownerLock;
6639
7177
  _attaching;
6640
7178
  _detached = false;
7179
+ /** The `seq` of the last link sent to the worker. */
7180
+ _sent = 0;
6641
7181
  constructor(_adapter, _control, init) {
6642
7182
  this._adapter = _adapter;
6643
7183
  this._control = _control;
@@ -6649,6 +7189,7 @@ var WorkerLinkImpl = class {
6649
7189
  _control.onmessage = (event) => {
6650
7190
  const message = event.data;
6651
7191
  if (message?.type === "relink") {
7192
+ if (message.seen !== void 0 && message.seen < this._sent) return;
6652
7193
  this._adapter._poke(WAKE_MESSAGE);
6653
7194
  this.attach().catch(() => {});
6654
7195
  } else if (message?.type === "keepalive") this._adapter._poke(KEEPALIVE_MESSAGE);
@@ -6695,7 +7236,8 @@ var WorkerLinkImpl = class {
6695
7236
  this._control.postMessage({
6696
7237
  type: "link",
6697
7238
  key: this.key,
6698
- routerLock: reply.routerLock
7239
+ routerLock: reply.routerLock,
7240
+ seq: ++this._sent
6699
7241
  }, [
6700
7242
  down.port2,
6701
7243
  ctl.port2,
@@ -6796,12 +7338,18 @@ function serveHttpInWorker(handler, options = {}) {
6796
7338
  const ownerLock = newLockName("webrun-http-owner");
6797
7339
  const held = holdLock(ownerLock);
6798
7340
  const controls = /* @__PURE__ */ new Set();
7341
+ const seen = /* @__PURE__ */ new Map();
6799
7342
  const tellPages = (message) => {
6800
7343
  for (const control of controls) control.postMessage(message);
6801
7344
  };
6802
7345
  const endpoint = new HandlerEndpoint(handler, {
6803
7346
  ...options,
6804
- requestRelink: () => tellPages({ type: "relink" }),
7347
+ requestRelink: () => {
7348
+ for (const control of controls) control.postMessage({
7349
+ type: "relink",
7350
+ seen: seen.get(control) ?? 0
7351
+ });
7352
+ },
6805
7353
  keepAlive: () => tellPages({ type: "keepalive" })
6806
7354
  });
6807
7355
  const onMessage = (event) => {
@@ -6813,6 +7361,7 @@ function serveHttpInWorker(handler, options = {}) {
6813
7361
  control.onmessage = (message) => {
6814
7362
  const msg = message.data;
6815
7363
  if (msg?.type === "link") {
7364
+ if (msg.seq !== void 0) seen.set(control, msg.seq);
6816
7365
  const [down, ctl, up] = message.ports;
6817
7366
  if (down) endpoint.link(msg.key, {
6818
7367
  down,
@@ -6853,6 +7402,7 @@ function serveHttpInWorker(handler, options = {}) {
6853
7402
  await endpoint.close();
6854
7403
  for (const control of controls) control.close();
6855
7404
  controls.clear();
7405
+ seen.clear();
6856
7406
  (await held)();
6857
7407
  }
6858
7408
  };