@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.js CHANGED
@@ -1525,7 +1525,7 @@ async function fetchOverDuplex(call, request, options = {}) {
1525
1525
  else await release?.();
1526
1526
  return new Response(null, responseInit);
1527
1527
  }
1528
- return new Response(asyncIterableToReadable(withAbort(respBody, request.signal)), responseInit);
1528
+ return new Response(responseBody(respBody, request.signal, release), responseInit);
1529
1529
  }
1530
1530
  /**
1531
1531
  * Wrap a `(Request) => Promise<Response>` handler as a `Duplex` so it can be
@@ -1656,15 +1656,78 @@ function trackingEnd(source) {
1656
1656
  settle
1657
1657
  };
1658
1658
  }
1659
- async function* withAbort(iter, signal) {
1660
- if (!signal) {
1661
- yield* iter;
1662
- return;
1663
- }
1664
- for await (const chunk of iter) {
1665
- if (signal.aborted) throw abortReason$1(signal);
1666
- yield chunk;
1667
- }
1659
+ /**
1660
+ * How long a cancelled body waits for the read in flight before it abandons the call anyway.
1661
+ * The read's answer tells a body that has ended (the call is finished: an upload the handler
1662
+ * reads after answering goes on) from one that has not; a peer that sends nothing more never
1663
+ * answers it. A body whose end comes later than this after the cancel is abandoned too, so a
1664
+ * handler still streaming its response and still reading the upload then loses the upload.
1665
+ */
1666
+ const CANCEL_GRACE_MS = 1e3;
1667
+ /**
1668
+ * The response body as a stream that the request's signal can end at any time.
1669
+ *
1670
+ * An abort errors the stream with the abort's reason at once — a read waiting
1671
+ * for a peer that sends nothing more rejects then, not when a next chunk
1672
+ * arrives, which may be never — and abandons the call (`release`), so on a
1673
+ * multiplexed transport its port is given back whether or not anyone still
1674
+ * reads the body. The listener is removed once the body ends, however it ends:
1675
+ * read to its end, cancelled, failed or aborted. A body nobody reads or cancels
1676
+ * keeps it until the signal aborts (and keeps the call open until then anyway).
1677
+ * A cancel abandons the call too — unless the read in flight comes back with the
1678
+ * body's end, which finishes it instead (see `cancel`).
1679
+ */
1680
+ function responseBody(body, signal, release) {
1681
+ const it = body[Symbol.asyncIterator]();
1682
+ let controller;
1683
+ let ended = false;
1684
+ let reading = false;
1685
+ const end = () => {
1686
+ ended = true;
1687
+ signal?.removeEventListener("abort", onAbort);
1688
+ };
1689
+ const onAbort = () => {
1690
+ if (ended) return;
1691
+ end();
1692
+ try {
1693
+ controller.error(abortReason$1(signal));
1694
+ } catch {}
1695
+ release?.();
1696
+ };
1697
+ return new ReadableStream({
1698
+ start(c) {
1699
+ controller = c;
1700
+ if (signal?.aborted) onAbort();
1701
+ else signal?.addEventListener("abort", onAbort, { once: true });
1702
+ },
1703
+ async pull(c) {
1704
+ try {
1705
+ reading = true;
1706
+ const { done, value } = await it.next().finally(() => {
1707
+ reading = false;
1708
+ });
1709
+ if (ended) return;
1710
+ if (done) {
1711
+ end();
1712
+ c.close();
1713
+ return;
1714
+ }
1715
+ c.enqueue(value);
1716
+ } catch (err) {
1717
+ if (ended) return;
1718
+ end();
1719
+ c.error(err);
1720
+ }
1721
+ },
1722
+ cancel() {
1723
+ end();
1724
+ const returned = Promise.resolve(it.return?.()).catch(() => {});
1725
+ if (!reading) return;
1726
+ const grace = setTimeout(() => void release?.(), CANCEL_GRACE_MS);
1727
+ grace.unref?.();
1728
+ returned.finally(() => clearTimeout(grace));
1729
+ }
1730
+ });
1668
1731
  }
1669
1732
  //#endregion
1670
1733
  //#region ../webrun-rpc/dist/index.js
@@ -1789,8 +1852,11 @@ function listenPort(port, handler, { channelName = "", log = () => {} } = {}) {
1789
1852
  error
1790
1853
  });
1791
1854
  };
1792
- port.addEventListener("message", onMessage);
1793
- return () => port.removeEventListener("message", onMessage);
1855
+ const listener = (event) => {
1856
+ onMessage(event);
1857
+ };
1858
+ port.addEventListener("message", listener);
1859
+ return () => port.removeEventListener("message", listener);
1794
1860
  }
1795
1861
  /**
1796
1862
  * Wraps an async iterable so that an `AbortSignal` firing causes the wrapper
@@ -2195,6 +2261,33 @@ async function sendChunks(port, channelName, output, { maxMessageSize, log }, si
2195
2261
  }
2196
2262
  }
2197
2263
  /**
2264
+ * Runs `fn` on a later task, never a microtask: a producer looping on
2265
+ * `whenPortReady` must let the transport's own events run. `setImmediate`
2266
+ * where it exists; elsewhere a `MessageChannel`, because nested `setTimeout`s
2267
+ * are clamped to 4 ms in browsers.
2268
+ */
2269
+ const setImmediateFn = globalThis.setImmediate;
2270
+ /** @internal */
2271
+ const nextTask = setImmediateFn ? (fn) => {
2272
+ setImmediateFn(fn);
2273
+ } : (() => {
2274
+ let channel;
2275
+ const queue = [];
2276
+ return (fn) => {
2277
+ if (!channel) {
2278
+ channel = new MessageChannel();
2279
+ channel.port1.onmessage = () => queue.shift()?.();
2280
+ }
2281
+ queue.push(fn);
2282
+ channel.port2.postMessage(0);
2283
+ };
2284
+ })();
2285
+ const states = /* @__PURE__ */ new WeakMap();
2286
+ /** @internal Registered by the multiplexer for each port with a window. */
2287
+ function setPortSendState(port, state) {
2288
+ states.set(port, state);
2289
+ }
2290
+ /**
2198
2291
  * One virtual port.
2199
2292
  *
2200
2293
  * `deliver` and `markClosed` are deliberately not on `port`: the consumer holds
@@ -2209,6 +2302,14 @@ function newVirtualPort(send, requestClose) {
2209
2302
  if (closing.signal.aborted) return;
2210
2303
  closing.abort(reason ?? /* @__PURE__ */ new Error("webrun-rpc: the port was closed"));
2211
2304
  };
2305
+ const close = (reason) => {
2306
+ if (closed) return;
2307
+ closed = true;
2308
+ const notify = requestClose;
2309
+ listeners.clear();
2310
+ notify(reason);
2311
+ abort(reason);
2312
+ };
2212
2313
  const port = {
2213
2314
  addEventListener(_type, listener) {
2214
2315
  listeners.add(listener);
@@ -2220,24 +2321,28 @@ function newVirtualPort(send, requestClose) {
2220
2321
  if (closed) return;
2221
2322
  send(message, transfer);
2222
2323
  },
2223
- close() {
2224
- if (closed) return;
2225
- closed = true;
2226
- const notify = requestClose;
2227
- listeners.clear();
2228
- notify();
2229
- abort();
2230
- }
2324
+ close
2231
2325
  };
2232
2326
  setPortCloseSignal(port, closing.signal);
2327
+ const dispose = Symbol.dispose;
2328
+ if (dispose) Object.defineProperty(port, dispose, {
2329
+ value: () => close(),
2330
+ configurable: true
2331
+ });
2233
2332
  return {
2234
2333
  port,
2235
2334
  deliver(payload) {
2236
2335
  if (closed) return;
2237
2336
  const event = new MessageEvent("message", { data: payload });
2337
+ let pending;
2238
2338
  for (const listener of [...listeners]) try {
2239
- listener(event);
2339
+ const result = listener(event);
2340
+ if (result && typeof result.then === "function") {
2341
+ pending ??= [];
2342
+ pending.push(result);
2343
+ }
2240
2344
  } catch {}
2345
+ return pending ? Promise.allSettled(pending) : void 0;
2241
2346
  },
2242
2347
  markClosed(reason) {
2243
2348
  closed = true;
@@ -2246,53 +2351,309 @@ function newVirtualPort(send, requestClose) {
2246
2351
  },
2247
2352
  isClosed() {
2248
2353
  return closed;
2354
+ },
2355
+ hasListeners() {
2356
+ return listeners.size > 0;
2249
2357
  }
2250
2358
  };
2251
2359
  }
2252
2360
  /**
2253
- * The message of the error that fails the calls still in flight on a
2254
- * multiplexer when it is closed (`PortMux.close()`).
2361
+ * The id of keepalive's `ping` / `pong`: one no port ever takes. An older mux
2362
+ * whose codec passes unknown envelopes through reads them as a close of that
2363
+ * id, which must therefore not be a real port (0 is the initiator's first).
2255
2364
  */
2256
- const MUX_CLOSED_MESSAGE = "webrun-rpc: the multiplexer was closed";
2365
+ const KEEPALIVE_ID = Number.MAX_SAFE_INTEGER;
2366
+ /** The longest delay a timer holds; past it `setInterval` fires at once. */
2367
+ const MAX_TIMER_MS = 2 ** 31 - 1;
2257
2368
  /** Ceiling on concurrently open virtual ports. Bounds the id table only. */
2258
2369
  const DEFAULT_MAX_PORTS = 1024;
2259
2370
  /**
2371
+ * What a message costs a port's window when the codec cannot say
2372
+ * (`PortCodec.encode`): its bytes and string lengths, found in its values up
2373
+ * to 4 levels deep (at most 256 values), and at least 64. Both ends compute it
2374
+ * from the same structured value, so they agree.
2375
+ */
2376
+ function estimateMessageSize(payload) {
2377
+ let budget = 256;
2378
+ const walk = (v, depth) => {
2379
+ if (budget-- <= 0) return 0;
2380
+ if (typeof v === "string") return v.length;
2381
+ if (v instanceof ArrayBuffer) return v.byteLength;
2382
+ if (ArrayBuffer.isView(v)) return v.byteLength;
2383
+ if (v === null || typeof v !== "object" || depth >= 4) return 8;
2384
+ let n = 16;
2385
+ if (Array.isArray(v)) for (const x of v) n += walk(x, depth + 1);
2386
+ else for (const k in v) n += k.length + walk(v[k], depth + 1);
2387
+ return n;
2388
+ };
2389
+ return Math.max(64, walk(payload, 0));
2390
+ }
2391
+ /**
2260
2392
  * The default `PortMux`: emulates multiplexing over a single port.
2261
2393
  *
2262
2394
  * A transport that already multiplexes natively supplies its own `PortMux`
2263
2395
  * instead — this implementation is for transports that offer one pipe.
2264
2396
  */
2265
2397
  function multiplexPort(port, options) {
2266
- const { codec, onPort, side = "initiator", maxPorts = DEFAULT_MAX_PORTS, maxMessageSize } = options;
2398
+ const { codec, onPort, side = "initiator", maxPorts = DEFAULT_MAX_PORTS, maxMessageSize, signal, window, frameSize, keepalive, onLeak } = options;
2399
+ const misses = keepalive?.misses ?? 3;
2400
+ 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`);
2401
+ if (frameSize != null && !(Number.isSafeInteger(frameSize) && frameSize > 0)) throw new RangeError(`webrun-rpc: frameSize must be a positive integer, got ${frameSize}`);
2267
2402
  const open = /* @__PURE__ */ new Map();
2403
+ const leaks = onLeak ? new FinalizationRegistry(({ id, record, release }) => {
2404
+ const entry = open.get(id);
2405
+ if (entry?.handle !== record) return;
2406
+ const { handle: _, credit: __, partial: ___, ...info } = entry;
2407
+ if (!record.hasListeners()) {
2408
+ release("leaked");
2409
+ record.markClosed(/* @__PURE__ */ new Error("webrun-rpc: the port was dropped without close()"));
2410
+ }
2411
+ try {
2412
+ onLeak?.(info);
2413
+ } catch {}
2414
+ }) : void 0;
2415
+ const touch = (id) => {
2416
+ const entry = open.get(id);
2417
+ if (entry) entry.lastActivity = Date.now();
2418
+ };
2268
2419
  let nextId = side === "initiator" ? 0 : 1;
2269
2420
  let muxClosed = false;
2421
+ /** This end counts encoded bytes and reassembles frames. */
2422
+ const exactCodec = codec.encode != null && codec.decode != null;
2270
2423
  const post = (envelope, transfer) => {
2271
2424
  if (muxClosed) return;
2425
+ codecPost(envelope, transfer);
2426
+ };
2427
+ /** Like `post`, also while closing: the mux's own close notices. */
2428
+ const codecPost = (envelope, transfer) => {
2272
2429
  try {
2273
2430
  codec.post(port, envelope, transfer);
2274
2431
  } catch {}
2275
2432
  };
2276
- const attach = (id) => {
2277
- let self;
2278
- self = newVirtualPort((payload, transfer) => post({
2279
- type: "message",
2280
- id,
2281
- payload
2282
- }, transfer), (reason) => {
2433
+ const outgoing = /* @__PURE__ */ new Map();
2434
+ const ring = [];
2435
+ let pumping = false;
2436
+ const pump = async () => {
2437
+ pumping = true;
2438
+ try {
2439
+ while (ring.length && !muxClosed) {
2440
+ if (port.whenDrained) await port.whenDrained();
2441
+ if (muxClosed) break;
2442
+ const id = ring.shift();
2443
+ const queue = outgoing.get(id);
2444
+ const item = queue?.shift();
2445
+ if (queue?.length) ring.push(id);
2446
+ else outgoing.delete(id);
2447
+ item?.();
2448
+ }
2449
+ } catch (err) {
2450
+ close(err).catch(() => {});
2451
+ } finally {
2452
+ pumping = false;
2453
+ }
2454
+ };
2455
+ const sendOnPort = (id, item) => {
2456
+ if (frameSize == null) {
2457
+ item();
2458
+ return;
2459
+ }
2460
+ let queue = outgoing.get(id);
2461
+ if (!queue) {
2462
+ queue = [];
2463
+ outgoing.set(id, queue);
2464
+ ring.push(id);
2465
+ }
2466
+ queue.push(item);
2467
+ if (!pumping) pump();
2468
+ };
2469
+ const dropOutgoing = (id) => {
2470
+ if (!outgoing.delete(id)) return;
2471
+ const at = ring.indexOf(id);
2472
+ if (at >= 0) ring.splice(at, 1);
2473
+ };
2474
+ const postBytes = (bytes) => {
2475
+ if (muxClosed) return;
2476
+ try {
2477
+ port.postMessage(bytes);
2478
+ } catch {}
2479
+ };
2480
+ const releaseWaiters = (c) => {
2481
+ for (const w of c?.waiters.splice(0) ?? []) w.resolve();
2482
+ };
2483
+ const flush = (c) => {
2484
+ while (c.queue.length) {
2485
+ const next = c.queue[0];
2486
+ if (c.enforced && c.outstanding > 0 && c.outstanding + next.size > c.window) break;
2487
+ c.queue.shift();
2488
+ c.queued -= next.size;
2489
+ c.outstanding += next.size;
2490
+ next.send();
2491
+ }
2492
+ for (const w of [...c.waiters]) if (c.queued <= w.below) {
2493
+ c.waiters.splice(c.waiters.indexOf(w), 1);
2494
+ w.resolve();
2495
+ }
2496
+ };
2497
+ const grant = (id, c, bytes) => {
2498
+ c.unconsumed -= bytes;
2499
+ c.toGrant += bytes;
2500
+ const send = () => {
2501
+ c.grantScheduled = false;
2502
+ if (!c.toGrant || !open.has(id)) return;
2283
2503
  post({
2504
+ type: "credit",
2505
+ id,
2506
+ bytes: c.toGrant
2507
+ });
2508
+ c.toGrant = 0;
2509
+ };
2510
+ if (c.toGrant >= c.window / 4) {
2511
+ send();
2512
+ return;
2513
+ }
2514
+ if (c.grantScheduled) return;
2515
+ c.grantScheduled = true;
2516
+ setTimeout(send, 0);
2517
+ };
2518
+ const attach = (id, meta, portWindow, opener, exact) => {
2519
+ const c = typeof portWindow === "number" && Number.isSafeInteger(portWindow) && portWindow > 0 ? {
2520
+ window: portWindow,
2521
+ enforced: false,
2522
+ opener,
2523
+ exact,
2524
+ outstanding: 0,
2525
+ queue: [],
2526
+ queued: 0,
2527
+ unconsumed: 0,
2528
+ waiters: [],
2529
+ toGrant: 0,
2530
+ grantScheduled: false
2531
+ } : void 0;
2532
+ const self = newVirtualPort((payload, transfer) => {
2533
+ touch(id);
2534
+ const envelope = {
2535
+ type: "message",
2536
+ id,
2537
+ payload
2538
+ };
2539
+ if (!c) {
2540
+ post(envelope, transfer);
2541
+ return;
2542
+ }
2543
+ const bytes = c.exact ? codec.encode?.(envelope) : void 0;
2544
+ const size = bytes?.byteLength ?? estimateMessageSize(payload);
2545
+ const waits = frameSize != null || c.queue.length > 0 || c.enforced && c.outstanding > 0 && c.outstanding + size > c.window;
2546
+ const held = !bytes && waits ? {
2547
+ ...envelope,
2548
+ payload: structuredClone(payload, { transfer })
2549
+ } : void 0;
2550
+ const send = () => {
2551
+ if (!bytes) {
2552
+ sendOnPort(id, held ? () => post(held) : () => post(envelope, transfer));
2553
+ return;
2554
+ }
2555
+ if (frameSize != null && c.enforced && c.exact && bytes.byteLength > frameSize) {
2556
+ for (let at = 0; at < bytes.byteLength; at += frameSize) {
2557
+ const data = bytes.subarray(at, at + frameSize);
2558
+ const frame = at + frameSize >= bytes.byteLength ? {
2559
+ type: "frame",
2560
+ id,
2561
+ data,
2562
+ last: true
2563
+ } : {
2564
+ type: "frame",
2565
+ id,
2566
+ data
2567
+ };
2568
+ sendOnPort(id, () => post(frame));
2569
+ }
2570
+ return;
2571
+ }
2572
+ sendOnPort(id, () => postBytes(bytes));
2573
+ };
2574
+ c.queue.push({
2575
+ send,
2576
+ size
2577
+ });
2578
+ c.queued += size;
2579
+ flush(c);
2580
+ }, (reason) => {
2581
+ release(reason);
2582
+ record.markClosed(reason);
2583
+ });
2584
+ function release(reason) {
2585
+ open.delete(id);
2586
+ leaks?.unregister(record);
2587
+ if (c?.queue.length) {
2588
+ for (const item of c.queue.splice(0)) item.send();
2589
+ c.queued = 0;
2590
+ }
2591
+ sendOnPort(id, () => post({
2284
2592
  type: "close",
2285
2593
  id,
2286
2594
  reason
2595
+ }));
2596
+ releaseWaiters(c);
2597
+ }
2598
+ const { markClosed, isClosed, hasListeners } = self;
2599
+ if (c) {
2600
+ const deliver = self.deliver;
2601
+ self.deliver = (payload, size = estimateMessageSize(payload)) => {
2602
+ c.unconsumed += size;
2603
+ const done = deliver(payload);
2604
+ if (done) done.then(() => grant(id, c, size));
2605
+ else grant(id, c, size);
2606
+ return done;
2607
+ };
2608
+ setPortSendState(self.port, {
2609
+ bufferedAmount: () => c.queued,
2610
+ ready: (below = 0) => new Promise((resolve) => {
2611
+ if (c.queued <= below || isClosed()) nextTask(resolve);
2612
+ else c.waiters.push({
2613
+ below,
2614
+ resolve: () => nextTask(resolve)
2615
+ });
2616
+ })
2287
2617
  });
2288
- open.delete(id);
2289
- self.markClosed();
2618
+ }
2619
+ const record = {
2620
+ deliver: self.deliver,
2621
+ markClosed,
2622
+ isClosed,
2623
+ hasListeners,
2624
+ close: (reason) => {
2625
+ if (isClosed()) return;
2626
+ release(reason);
2627
+ markClosed(reason);
2628
+ }
2629
+ };
2630
+ leaks?.register(self.port, {
2631
+ id,
2632
+ record,
2633
+ release
2634
+ }, record);
2635
+ const now = Date.now();
2636
+ open.set(id, {
2637
+ id,
2638
+ meta,
2639
+ openedAt: now,
2640
+ lastActivity: now,
2641
+ handle: record,
2642
+ credit: c
2290
2643
  });
2291
- open.set(id, self);
2292
2644
  return self;
2293
2645
  };
2294
- const handleEnvelope = (envelope) => {
2646
+ const handleEnvelope = (envelope, event) => {
2295
2647
  if (muxClosed) return;
2648
+ heardSincePing = true;
2649
+ if (envelope.type === "ping" || envelope.type === "pong") {
2650
+ peerKeepsAlive = true;
2651
+ if (envelope.type === "ping") post({
2652
+ type: "pong",
2653
+ id: KEEPALIVE_ID
2654
+ });
2655
+ return;
2656
+ }
2296
2657
  if (envelope.type === "open") {
2297
2658
  if (open.has(envelope.id)) return;
2298
2659
  if (open.size >= maxPorts) {
@@ -2303,7 +2664,18 @@ function multiplexPort(port, options) {
2303
2664
  });
2304
2665
  return;
2305
2666
  }
2306
- const handle = attach(envelope.id);
2667
+ const handle = attach(envelope.id, envelope.meta, envelope.window, false, envelope.exact === true && exactCodec);
2668
+ const credit = open.get(envelope.id)?.credit;
2669
+ if (credit) post(credit.exact ? {
2670
+ type: "credit",
2671
+ id: envelope.id,
2672
+ bytes: 0,
2673
+ exact: true
2674
+ } : {
2675
+ type: "credit",
2676
+ id: envelope.id,
2677
+ bytes: 0
2678
+ });
2307
2679
  let accepted = false;
2308
2680
  if (onPort) try {
2309
2681
  accepted = onPort(handle.port, envelope.meta) !== false;
@@ -2318,57 +2690,191 @@ function multiplexPort(port, options) {
2318
2690
  id: envelope.id,
2319
2691
  reason: "rejected"
2320
2692
  });
2693
+ return;
2694
+ }
2695
+ return;
2696
+ }
2697
+ const entry = open.get(envelope.id);
2698
+ if (envelope.type === "credit") {
2699
+ const c = entry?.credit;
2700
+ if (!c || !(envelope.bytes >= 0)) return;
2701
+ if (!c.enforced && c.opener) {
2702
+ if (c.exact && envelope.exact !== true) {
2703
+ c.exact = false;
2704
+ c.outstanding = 0;
2705
+ }
2706
+ post({
2707
+ type: "credit",
2708
+ id: envelope.id,
2709
+ bytes: 0
2710
+ });
2321
2711
  }
2712
+ c.enforced = true;
2713
+ c.outstanding = Math.max(0, c.outstanding - envelope.bytes);
2714
+ flush(c);
2322
2715
  return;
2323
2716
  }
2324
- const handle = open.get(envelope.id);
2325
2717
  if (envelope.type === "message") {
2326
- if (!handle) return;
2327
- handle.deliver(envelope.payload);
2718
+ const sizeOf = (c) => (c.exact ? byteLengthOf(event.data) : void 0) ?? estimateMessageSize(envelope.payload);
2719
+ if (!entry) return;
2720
+ touch(envelope.id);
2721
+ entry.handle.deliver(envelope.payload, entry.credit ? sizeOf(entry.credit) : void 0);
2328
2722
  return;
2329
2723
  }
2330
- if (!handle) return;
2724
+ if (envelope.type === "frame") {
2725
+ if (!entry?.credit?.exact || !codec.decode || !(envelope.data instanceof Uint8Array)) return;
2726
+ touch(envelope.id);
2727
+ const partial = entry.partial ?? {
2728
+ chunks: [],
2729
+ bytes: 0
2730
+ };
2731
+ entry.partial = partial;
2732
+ partial.chunks.push(envelope.data);
2733
+ partial.bytes += envelope.data.byteLength;
2734
+ if (!envelope.last) return;
2735
+ entry.partial = void 0;
2736
+ const whole = new Uint8Array(partial.bytes);
2737
+ let at = 0;
2738
+ for (const chunk of partial.chunks) {
2739
+ whole.set(chunk, at);
2740
+ at += chunk.byteLength;
2741
+ }
2742
+ const inner = codec.decode(whole);
2743
+ if (inner?.type !== "message" || inner.id !== envelope.id) return;
2744
+ entry.handle.deliver(inner.payload, whole.byteLength);
2745
+ return;
2746
+ }
2747
+ dropOutgoing(envelope.id);
2748
+ if (!entry) return;
2331
2749
  open.delete(envelope.id);
2332
- handle.markClosed(/* @__PURE__ */ new Error("webrun-rpc: the peer closed the port"));
2750
+ releaseWaiters(entry.credit);
2751
+ entry.handle.markClosed(/* @__PURE__ */ new Error("webrun-rpc: the peer closed the port"));
2333
2752
  };
2334
2753
  const listener = (event) => {
2335
2754
  const envelope = codec.read(event);
2336
- if (envelope) handleEnvelope(envelope);
2755
+ if (envelope) handleEnvelope(envelope, event);
2756
+ };
2757
+ let peerKeepsAlive = false;
2758
+ let heardSincePing = true;
2759
+ let unanswered = 0;
2760
+ let pinger;
2761
+ const tick = () => {
2762
+ if (!keepalive) return;
2763
+ const backlogged = (port.bufferedAmount ?? 0) > 0;
2764
+ if (heardSincePing) unanswered = 0;
2765
+ else if (peerKeepsAlive && !backlogged) unanswered++;
2766
+ if (unanswered >= misses) {
2767
+ close(/* @__PURE__ */ new Error(`webrun-rpc: the peer did not answer ${misses} keepalive pings, ${keepalive.intervalMs} ms apart`)).catch(() => {});
2768
+ return;
2769
+ }
2770
+ heardSincePing = false;
2771
+ post({
2772
+ type: "ping",
2773
+ id: KEEPALIVE_ID
2774
+ });
2775
+ };
2776
+ const close = async (reason) => {
2777
+ if (muxClosed) return;
2778
+ clearInterval(pinger);
2779
+ muxClosed = true;
2780
+ signal?.removeEventListener("abort", onTransportAbort);
2781
+ port.removeEventListener("message", listener);
2782
+ const closedNow = /* @__PURE__ */ new Set();
2783
+ for (const [id, { handle, credit }] of [...open]) {
2784
+ closedNow.add(id);
2785
+ codecPost({
2786
+ type: "close",
2787
+ id
2788
+ });
2789
+ open.delete(id);
2790
+ releaseWaiters(credit);
2791
+ handle.markClosed(reason ?? /* @__PURE__ */ new Error("webrun-rpc: the multiplexer was closed"));
2792
+ }
2793
+ for (const id of outgoing.keys()) if (!closedNow.has(id)) codecPost({
2794
+ type: "close",
2795
+ id
2796
+ });
2797
+ outgoing.clear();
2798
+ ring.length = 0;
2799
+ await port.close?.();
2800
+ };
2801
+ const onTransportAbort = () => {
2802
+ close(signal?.reason).catch(() => {});
2337
2803
  };
2338
2804
  port.addEventListener("message", listener);
2339
2805
  port.start?.();
2806
+ if (keepalive) {
2807
+ pinger = setInterval(tick, keepalive.intervalMs);
2808
+ pinger.unref?.();
2809
+ }
2810
+ if (signal?.aborted) close(signal.reason).catch(() => {});
2811
+ else signal?.addEventListener("abort", onTransportAbort, { once: true });
2340
2812
  return {
2341
2813
  maxMessageSize,
2342
- async openPort(meta) {
2814
+ async openPort(meta, { signal: portSignal } = {}) {
2343
2815
  if (muxClosed) throw new Error("webrun-rpc: the multiplexer is closed");
2344
- if (open.size >= maxPorts) throw new RangeError(`webrun-rpc: maxPorts (${maxPorts}) reached`);
2816
+ if (portSignal?.aborted) throw portSignal.reason;
2817
+ if (open.size >= maxPorts) throw new RangeError(`webrun-rpc: maxPorts (${maxPorts}) reached; open ports by kind: ${describeKinds(open.values())}`);
2345
2818
  while (open.has(nextId)) nextId += 2;
2346
2819
  const id = nextId;
2347
2820
  nextId += 2;
2348
- const handle = attach(id);
2349
- post({
2821
+ const handle = attach(id, meta, window, true, exactCodec);
2822
+ post(window ? exactCodec ? {
2823
+ type: "open",
2824
+ id,
2825
+ meta,
2826
+ window,
2827
+ exact: true
2828
+ } : {
2829
+ type: "open",
2830
+ id,
2831
+ meta,
2832
+ window
2833
+ } : {
2350
2834
  type: "open",
2351
2835
  id,
2352
2836
  meta
2353
2837
  });
2838
+ if (portSignal) {
2839
+ const record = open.get(id)?.handle;
2840
+ const onAbort = () => {
2841
+ if (record && open.get(id)?.handle === record) record.close(portSignal.reason);
2842
+ };
2843
+ portSignal.addEventListener("abort", onAbort, { once: true });
2844
+ getPortCloseSignal(handle.port)?.addEventListener("abort", () => portSignal.removeEventListener("abort", onAbort), { once: true });
2845
+ }
2354
2846
  return handle.port;
2355
2847
  },
2356
- async close() {
2357
- if (muxClosed) return;
2358
- for (const [id, handle] of [...open]) {
2359
- post({
2360
- type: "close",
2361
- id
2362
- });
2363
- open.delete(id);
2364
- handle.markClosed(/* @__PURE__ */ new Error(MUX_CLOSED_MESSAGE));
2365
- }
2366
- muxClosed = true;
2367
- port.removeEventListener("message", listener);
2368
- await port.close?.();
2848
+ close,
2849
+ ports() {
2850
+ return [...open.values()].map(({ handle: _, credit, ...info }) => credit ? {
2851
+ ...info,
2852
+ ...credit.enforced ? { window: credit.window } : {},
2853
+ queued: credit.queued,
2854
+ outstanding: credit.outstanding,
2855
+ unconsumed: credit.unconsumed
2856
+ } : info);
2369
2857
  }
2370
2858
  };
2371
2859
  }
2860
+ /** The byte length of a received message's data, or `undefined` if it is not bytes. */
2861
+ function byteLengthOf(data) {
2862
+ if (data instanceof ArrayBuffer || ArrayBuffer.isView(data)) return data.byteLength;
2863
+ }
2864
+ /**
2865
+ * `"http: 1000, ws: 24"`: the three commonest `meta.kind`s among `entries`, so
2866
+ * a full table names who filled it. A port without a kind counts as `"?"`.
2867
+ */
2868
+ function describeKinds(entries) {
2869
+ const counts = /* @__PURE__ */ new Map();
2870
+ for (const { meta } of entries) {
2871
+ const kind = meta?.kind;
2872
+ const name = typeof kind === "string" ? kind : "?";
2873
+ counts.set(name, (counts.get(name) ?? 0) + 1);
2874
+ }
2875
+ if (!counts.size) return "none";
2876
+ return [...counts].sort((x, y) => y[1] - x[1]).slice(0, 3).map(([name, n]) => `${name}: ${n}`).join(", ");
2877
+ }
2372
2878
  /**
2373
2879
  * Announced with every port this pair opens. Layer 1 never reads `meta` — it is
2374
2880
  * here so a peer running something else over the same multiplexer can tell a
@@ -2475,7 +2981,7 @@ function isEnvelope(value) {
2475
2981
  const candidate = value;
2476
2982
  if (typeof candidate.id !== "number") return false;
2477
2983
  if (!Number.isInteger(candidate.id) || candidate.id < 0) return false;
2478
- return candidate.type === "open" || candidate.type === "message" || candidate.type === "close";
2984
+ return candidate.type === "open" || candidate.type === "message" || candidate.type === "close" || candidate.type === "credit" || candidate.type === "frame" || candidate.type === "ping" || candidate.type === "pong";
2479
2985
  }
2480
2986
  /**
2481
2987
  * For ports whose messages are structured values — a real `MessagePort`, a
@@ -2637,7 +3143,7 @@ function trackedLink(send, release) {
2637
3143
  finish();
2638
3144
  return response;
2639
3145
  }
2640
- return new Response(guardBody(response.body, lost, () => lostReason, finish), {
3146
+ return new Response(guardBody(response.body, lost, () => lostReason, finish, request.signal), {
2641
3147
  status: response.status,
2642
3148
  statusText: response.statusText,
2643
3149
  headers: response.headers
@@ -2656,33 +3162,65 @@ function trackedLink(send, release) {
2656
3162
  return asyncDisposable(link, () => link.close());
2657
3163
  }
2658
3164
  /**
2659
- * A response body that reports its end (done, cancelled or failed) and fails
2660
- * with the link's reason when the link is lost mid-stream.
3165
+ * A response body that reports its end (done, cancelled, failed or aborted) and
3166
+ * fails with the link's reason when the link is lost mid-stream. An abort of the
3167
+ * request's signal ends it at once, read or not: the exchange is over (the layer
3168
+ * below has given its port back), so the request leaves `inflight` then.
2661
3169
  */
2662
- function guardBody(body, lost, reason, finish) {
3170
+ function guardBody(body, lost, reason, finish, signal) {
2663
3171
  const reader = body.getReader();
3172
+ let controller;
3173
+ let ended = false;
3174
+ const end = () => {
3175
+ ended = true;
3176
+ signal?.removeEventListener("abort", onAbort);
3177
+ finish();
3178
+ };
3179
+ const onAbort = () => {
3180
+ if (ended) return;
3181
+ end();
3182
+ reader.cancel().catch(() => {});
3183
+ try {
3184
+ controller.error(abortError(signal));
3185
+ } catch {}
3186
+ };
2664
3187
  return new ReadableStream({
3188
+ start(c) {
3189
+ controller = c;
3190
+ if (signal?.aborted) onAbort();
3191
+ else signal?.addEventListener("abort", onAbort, { once: true });
3192
+ },
2665
3193
  async pull(controller) {
2666
3194
  try {
2667
3195
  const { done, value } = await Promise.race([reader.read(), lost]);
3196
+ if (ended) return;
2668
3197
  if (done) {
2669
- finish();
3198
+ end();
2670
3199
  controller.close();
2671
3200
  return;
2672
3201
  }
2673
3202
  controller.enqueue(value);
2674
3203
  } catch (error) {
2675
- finish();
3204
+ if (ended) return;
3205
+ end();
2676
3206
  reader.cancel().catch(() => {});
2677
3207
  controller.error(reason() ?? error);
2678
3208
  }
2679
3209
  },
2680
3210
  async cancel(why) {
2681
- finish();
3211
+ end();
2682
3212
  await reader.cancel(why).catch(() => {});
2683
3213
  }
2684
3214
  });
2685
3215
  }
3216
+ /** The signal's reason as an `Error` (`AbortError` when it gave none). */
3217
+ function abortError(signal) {
3218
+ const why = signal.reason;
3219
+ if (why instanceof Error) return why;
3220
+ const err = new Error(why === void 0 ? "Aborted" : String(why));
3221
+ err.name = "AbortError";
3222
+ return err;
3223
+ }
2686
3224
  //#endregion
2687
3225
  //#region src/link/locks.ts
2688
3226
  function lockManager() {
@@ -5498,6 +6036,8 @@ var WorkerLinkImpl = class {
5498
6036
  _ownerLock;
5499
6037
  _attaching;
5500
6038
  _detached = false;
6039
+ /** The `seq` of the last link sent to the worker. */
6040
+ _sent = 0;
5501
6041
  constructor(_adapter, _control, init) {
5502
6042
  this._adapter = _adapter;
5503
6043
  this._control = _control;
@@ -5509,6 +6049,7 @@ var WorkerLinkImpl = class {
5509
6049
  _control.onmessage = (event) => {
5510
6050
  const message = event.data;
5511
6051
  if (message?.type === "relink") {
6052
+ if (message.seen !== void 0 && message.seen < this._sent) return;
5512
6053
  this._adapter._poke(WAKE_MESSAGE);
5513
6054
  this.attach().catch(() => {});
5514
6055
  } else if (message?.type === "keepalive") this._adapter._poke(KEEPALIVE_MESSAGE);
@@ -5555,7 +6096,8 @@ var WorkerLinkImpl = class {
5555
6096
  this._control.postMessage({
5556
6097
  type: "link",
5557
6098
  key: this.key,
5558
- routerLock: reply.routerLock
6099
+ routerLock: reply.routerLock,
6100
+ seq: ++this._sent
5559
6101
  }, [
5560
6102
  down.port2,
5561
6103
  ctl.port2,
@@ -5656,12 +6198,18 @@ function serveHttpInWorker(handler, options = {}) {
5656
6198
  const ownerLock = newLockName("webrun-http-owner");
5657
6199
  const held = holdLock(ownerLock);
5658
6200
  const controls = /* @__PURE__ */ new Set();
6201
+ const seen = /* @__PURE__ */ new Map();
5659
6202
  const tellPages = (message) => {
5660
6203
  for (const control of controls) control.postMessage(message);
5661
6204
  };
5662
6205
  const endpoint = new HandlerEndpoint(handler, {
5663
6206
  ...options,
5664
- requestRelink: () => tellPages({ type: "relink" }),
6207
+ requestRelink: () => {
6208
+ for (const control of controls) control.postMessage({
6209
+ type: "relink",
6210
+ seen: seen.get(control) ?? 0
6211
+ });
6212
+ },
5665
6213
  keepAlive: () => tellPages({ type: "keepalive" })
5666
6214
  });
5667
6215
  const onMessage = (event) => {
@@ -5673,6 +6221,7 @@ function serveHttpInWorker(handler, options = {}) {
5673
6221
  control.onmessage = (message) => {
5674
6222
  const msg = message.data;
5675
6223
  if (msg?.type === "link") {
6224
+ if (msg.seq !== void 0) seen.set(control, msg.seq);
5676
6225
  const [down, ctl, up] = message.ports;
5677
6226
  if (down) endpoint.link(msg.key, {
5678
6227
  down,
@@ -5713,6 +6262,7 @@ function serveHttpInWorker(handler, options = {}) {
5713
6262
  await endpoint.close();
5714
6263
  for (const control of controls) control.close();
5715
6264
  controls.clear();
6265
+ seen.clear();
5716
6266
  (await held)();
5717
6267
  }
5718
6268
  };