@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.js CHANGED
@@ -1266,7 +1266,11 @@ async function drain(output) {
1266
1266
  */
1267
1267
  function httpServe(handler, options = {}) {
1268
1268
  const codec = options.codec ?? defaultCodec;
1269
- return async function* httpHandlerDuplex(input) {
1269
+ return (input) => {
1270
+ const abandoned = new AbortController();
1271
+ 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")));
1272
+ };
1273
+ async function* exchange(input, context) {
1270
1274
  let decoded;
1271
1275
  try {
1272
1276
  decoded = await codec.decodeRequest(input);
@@ -1278,7 +1282,7 @@ function httpServe(handler, options = {}) {
1278
1282
  const replyCodec = decoded.codec ?? codec;
1279
1283
  let result;
1280
1284
  try {
1281
- result = await handler(decoded.envelope, decoded.body);
1285
+ result = await handler(decoded.envelope, decoded.body, context);
1282
1286
  } catch (error) {
1283
1287
  yield* encodeErrorResponse(replyCodec, error, decoded.envelope.method);
1284
1288
  return;
@@ -1293,7 +1297,36 @@ function httpServe(handler, options = {}) {
1293
1297
  await result.release?.();
1294
1298
  } catch {}
1295
1299
  }
1300
+ }
1301
+ }
1302
+ /**
1303
+ * `gen`, except that `return()` and `throw()` from its consumer call `abandon`
1304
+ * at once — before they are queued behind a `next()` still parked inside the
1305
+ * generator (the handler's `await`) — unless the generator already finished.
1306
+ */
1307
+ function abortingOnReturn(gen, abandon) {
1308
+ let finished = false;
1309
+ const settle = (r) => {
1310
+ if (r.done) finished = true;
1311
+ return r;
1312
+ };
1313
+ const wrapped = {
1314
+ next: (...args) => gen.next(...args).then(settle),
1315
+ return(value) {
1316
+ if (!finished) abandon();
1317
+ finished = true;
1318
+ return gen.return(value);
1319
+ },
1320
+ throw(error) {
1321
+ if (!finished) abandon(error);
1322
+ finished = true;
1323
+ return gen.throw(error);
1324
+ },
1325
+ [Symbol.asyncIterator]() {
1326
+ return wrapped;
1327
+ }
1296
1328
  };
1329
+ return asyncDisposable$2(wrapped, () => wrapped.return(void 0));
1297
1330
  }
1298
1331
  /**
1299
1332
  * The one place that knows a runtime may not implement request body streams,
@@ -1525,17 +1558,18 @@ async function fetchOverDuplex(call, request, options = {}) {
1525
1558
  else await release?.();
1526
1559
  return new Response(null, responseInit);
1527
1560
  }
1528
- return new Response(asyncIterableToReadable(withAbort(respBody, request.signal)), responseInit);
1561
+ return new Response(responseBody(respBody, request.signal, release), responseInit);
1529
1562
  }
1530
1563
  /**
1531
1564
  * Wrap a `(Request) => Promise<Response>` handler as a `Duplex` so it can be
1532
1565
  * registered with any `webrun-streams-*` adapter's `serve`.
1533
1566
  */
1534
1567
  function serveFetchOverDuplex(handler, options = {}) {
1535
- return httpServe(async (env, body) => {
1568
+ return httpServe(async (env, body, exchange) => {
1536
1569
  const reqInit = {
1537
1570
  method: env.method,
1538
- headers: forwardableHeaders(env.headers)
1571
+ headers: forwardableHeaders(env.headers),
1572
+ signal: exchange?.signal
1539
1573
  };
1540
1574
  let upload;
1541
1575
  if (env.method !== "GET" && env.method !== "HEAD") {
@@ -1656,15 +1690,78 @@ function trackingEnd(source) {
1656
1690
  settle
1657
1691
  };
1658
1692
  }
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
- }
1693
+ /**
1694
+ * How long a cancelled body waits for the read in flight before it abandons the call anyway.
1695
+ * The read's answer tells a body that has ended (the call is finished: an upload the handler
1696
+ * reads after answering goes on) from one that has not; a peer that sends nothing more never
1697
+ * answers it. A body whose end comes later than this after the cancel is abandoned too, so a
1698
+ * handler still streaming its response and still reading the upload then loses the upload.
1699
+ */
1700
+ const CANCEL_GRACE_MS = 1e3;
1701
+ /**
1702
+ * The response body as a stream that the request's signal can end at any time.
1703
+ *
1704
+ * An abort errors the stream with the abort's reason at once — a read waiting
1705
+ * for a peer that sends nothing more rejects then, not when a next chunk
1706
+ * arrives, which may be never — and abandons the call (`release`), so on a
1707
+ * multiplexed transport its port is given back whether or not anyone still
1708
+ * reads the body. The listener is removed once the body ends, however it ends:
1709
+ * read to its end, cancelled, failed or aborted. A body nobody reads or cancels
1710
+ * keeps it until the signal aborts (and keeps the call open until then anyway).
1711
+ * A cancel abandons the call too — unless the read in flight comes back with the
1712
+ * body's end, which finishes it instead (see `cancel`).
1713
+ */
1714
+ function responseBody(body, signal, release) {
1715
+ const it = body[Symbol.asyncIterator]();
1716
+ let controller;
1717
+ let ended = false;
1718
+ let reading = false;
1719
+ const end = () => {
1720
+ ended = true;
1721
+ signal?.removeEventListener("abort", onAbort);
1722
+ };
1723
+ const onAbort = () => {
1724
+ if (ended) return;
1725
+ end();
1726
+ try {
1727
+ controller.error(abortReason$1(signal));
1728
+ } catch {}
1729
+ release?.();
1730
+ };
1731
+ return new ReadableStream({
1732
+ start(c) {
1733
+ controller = c;
1734
+ if (signal?.aborted) onAbort();
1735
+ else signal?.addEventListener("abort", onAbort, { once: true });
1736
+ },
1737
+ async pull(c) {
1738
+ try {
1739
+ reading = true;
1740
+ const { done, value } = await it.next().finally(() => {
1741
+ reading = false;
1742
+ });
1743
+ if (ended) return;
1744
+ if (done) {
1745
+ end();
1746
+ c.close();
1747
+ return;
1748
+ }
1749
+ c.enqueue(value);
1750
+ } catch (err) {
1751
+ if (ended) return;
1752
+ end();
1753
+ c.error(err);
1754
+ }
1755
+ },
1756
+ cancel() {
1757
+ end();
1758
+ const returned = Promise.resolve(it.return?.()).catch(() => {});
1759
+ if (!reading) return;
1760
+ const grace = setTimeout(() => void release?.(), CANCEL_GRACE_MS);
1761
+ grace.unref?.();
1762
+ returned.finally(() => clearTimeout(grace));
1763
+ }
1764
+ });
1668
1765
  }
1669
1766
  //#endregion
1670
1767
  //#region ../webrun-rpc/dist/index.js
@@ -1789,8 +1886,11 @@ function listenPort(port, handler, { channelName = "", log = () => {} } = {}) {
1789
1886
  error
1790
1887
  });
1791
1888
  };
1792
- port.addEventListener("message", onMessage);
1793
- return () => port.removeEventListener("message", onMessage);
1889
+ const listener = (event) => {
1890
+ onMessage(event);
1891
+ };
1892
+ port.addEventListener("message", listener);
1893
+ return () => port.removeEventListener("message", listener);
1794
1894
  }
1795
1895
  /**
1796
1896
  * Wraps an async iterable so that an `AbortSignal` firing causes the wrapper
@@ -2168,7 +2268,11 @@ function receiveChunks(port, channelName, controller, touch) {
2168
2268
  * outstanding at a time (spec D11/D12), with no per-chunk deadline (spec D8).
2169
2269
  */
2170
2270
  async function sendChunks(port, channelName, output, { maxMessageSize, log }, signal, touch) {
2171
- const stream = throughAbort(maxMessageSize ? toChunks(maxMessageSize)(output) : output, signal);
2271
+ const framed = maxMessageSize ? toChunks(maxMessageSize)(output) : output;
2272
+ const stream = throughAbort(framed, signal);
2273
+ const producer = output;
2274
+ const returnProducer = () => void Promise.resolve(producer.return?.()).catch(() => {});
2275
+ if (framed !== output) signal.addEventListener("abort", returnProducer, { once: true });
2172
2276
  try {
2173
2277
  await sendIterator(async ({ done, value, error }) => {
2174
2278
  if (signal.aborted) return;
@@ -2192,9 +2296,38 @@ async function sendChunks(port, channelName, output, { maxMessageSize, log }, si
2192
2296
  } catch (err) {
2193
2297
  if (signal.aborted) return;
2194
2298
  throw err;
2299
+ } finally {
2300
+ signal.removeEventListener("abort", returnProducer);
2195
2301
  }
2196
2302
  }
2197
2303
  /**
2304
+ * Runs `fn` on a later task, never a microtask: a producer looping on
2305
+ * `whenPortReady` must let the transport's own events run. `setImmediate`
2306
+ * where it exists; elsewhere a `MessageChannel`, because nested `setTimeout`s
2307
+ * are clamped to 4 ms in browsers.
2308
+ */
2309
+ const setImmediateFn = globalThis.setImmediate;
2310
+ /** @internal */
2311
+ const nextTask = setImmediateFn ? (fn) => {
2312
+ setImmediateFn(fn);
2313
+ } : (() => {
2314
+ let channel;
2315
+ const queue = [];
2316
+ return (fn) => {
2317
+ if (!channel) {
2318
+ channel = new MessageChannel();
2319
+ channel.port1.onmessage = () => queue.shift()?.();
2320
+ }
2321
+ queue.push(fn);
2322
+ channel.port2.postMessage(0);
2323
+ };
2324
+ })();
2325
+ const states = /* @__PURE__ */ new WeakMap();
2326
+ /** @internal Registered by the multiplexer for each port with a window. */
2327
+ function setPortSendState(port, state) {
2328
+ states.set(port, state);
2329
+ }
2330
+ /**
2198
2331
  * One virtual port.
2199
2332
  *
2200
2333
  * `deliver` and `markClosed` are deliberately not on `port`: the consumer holds
@@ -2209,6 +2342,14 @@ function newVirtualPort(send, requestClose) {
2209
2342
  if (closing.signal.aborted) return;
2210
2343
  closing.abort(reason ?? /* @__PURE__ */ new Error("webrun-rpc: the port was closed"));
2211
2344
  };
2345
+ const close = (reason) => {
2346
+ if (closed) return;
2347
+ closed = true;
2348
+ const notify = requestClose;
2349
+ listeners.clear();
2350
+ notify(reason);
2351
+ abort(reason);
2352
+ };
2212
2353
  const port = {
2213
2354
  addEventListener(_type, listener) {
2214
2355
  listeners.add(listener);
@@ -2220,24 +2361,28 @@ function newVirtualPort(send, requestClose) {
2220
2361
  if (closed) return;
2221
2362
  send(message, transfer);
2222
2363
  },
2223
- close() {
2224
- if (closed) return;
2225
- closed = true;
2226
- const notify = requestClose;
2227
- listeners.clear();
2228
- notify();
2229
- abort();
2230
- }
2364
+ close
2231
2365
  };
2232
2366
  setPortCloseSignal(port, closing.signal);
2367
+ const dispose = Symbol.dispose;
2368
+ if (dispose) Object.defineProperty(port, dispose, {
2369
+ value: () => close(),
2370
+ configurable: true
2371
+ });
2233
2372
  return {
2234
2373
  port,
2235
2374
  deliver(payload) {
2236
2375
  if (closed) return;
2237
2376
  const event = new MessageEvent("message", { data: payload });
2377
+ let pending;
2238
2378
  for (const listener of [...listeners]) try {
2239
- listener(event);
2379
+ const result = listener(event);
2380
+ if (result && typeof result.then === "function") {
2381
+ pending ??= [];
2382
+ pending.push(result);
2383
+ }
2240
2384
  } catch {}
2385
+ return pending ? Promise.allSettled(pending) : void 0;
2241
2386
  },
2242
2387
  markClosed(reason) {
2243
2388
  closed = true;
@@ -2246,53 +2391,318 @@ function newVirtualPort(send, requestClose) {
2246
2391
  },
2247
2392
  isClosed() {
2248
2393
  return closed;
2394
+ },
2395
+ hasListeners() {
2396
+ return listeners.size > 0;
2249
2397
  }
2250
2398
  };
2251
2399
  }
2252
2400
  /**
2253
- * The message of the error that fails the calls still in flight on a
2254
- * multiplexer when it is closed (`PortMux.close()`).
2401
+ * The id of keepalive's `ping` / `pong`: one no port ever takes. An older mux
2402
+ * whose codec passes unknown envelopes through reads them as a close of that
2403
+ * id, which must therefore not be a real port (0 is the initiator's first).
2255
2404
  */
2256
- const MUX_CLOSED_MESSAGE = "webrun-rpc: the multiplexer was closed";
2405
+ const KEEPALIVE_ID = Number.MAX_SAFE_INTEGER;
2406
+ /** The longest delay a timer holds; past it `setInterval` fires at once. */
2407
+ const MAX_TIMER_MS = 2 ** 31 - 1;
2257
2408
  /** Ceiling on concurrently open virtual ports. Bounds the id table only. */
2258
2409
  const DEFAULT_MAX_PORTS = 1024;
2259
2410
  /**
2411
+ * What a message costs a port's window when the codec cannot say
2412
+ * (`PortCodec.encode`): its bytes and string lengths, found in its values up
2413
+ * to 4 levels deep (at most 256 values), and at least 64. Both ends compute it
2414
+ * from the same structured value, so they agree.
2415
+ */
2416
+ function estimateMessageSize(payload) {
2417
+ let budget = 256;
2418
+ const walk = (v, depth) => {
2419
+ if (budget-- <= 0) return 0;
2420
+ if (typeof v === "string") return v.length;
2421
+ if (v instanceof ArrayBuffer) return v.byteLength;
2422
+ if (ArrayBuffer.isView(v)) return v.byteLength;
2423
+ if (v === null || typeof v !== "object" || depth >= 4) return 8;
2424
+ let n = 16;
2425
+ if (Array.isArray(v)) for (const x of v) n += walk(x, depth + 1);
2426
+ else for (const k in v) n += k.length + walk(v[k], depth + 1);
2427
+ return n;
2428
+ };
2429
+ return Math.max(64, walk(payload, 0));
2430
+ }
2431
+ /**
2260
2432
  * The default `PortMux`: emulates multiplexing over a single port.
2261
2433
  *
2262
2434
  * A transport that already multiplexes natively supplies its own `PortMux`
2263
2435
  * instead — this implementation is for transports that offer one pipe.
2264
2436
  */
2265
2437
  function multiplexPort(port, options) {
2266
- const { codec, onPort, side = "initiator", maxPorts = DEFAULT_MAX_PORTS, maxMessageSize } = options;
2438
+ const { codec, onPort, side = "initiator", maxPorts = DEFAULT_MAX_PORTS, maxMessageSize, signal, window, frameSize, keepalive, onLeak } = options;
2439
+ const maxReassembledBytes = options.maxReassembledBytes ?? 67108864;
2440
+ const misses = keepalive?.misses ?? 3;
2441
+ 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`);
2442
+ if (frameSize != null && !(Number.isSafeInteger(frameSize) && frameSize > 0)) throw new RangeError(`webrun-rpc: frameSize must be a positive integer, got ${frameSize}`);
2443
+ if (!(Number.isSafeInteger(maxReassembledBytes) && maxReassembledBytes > 0)) throw new RangeError(`webrun-rpc: maxReassembledBytes must be a positive integer, got ${maxReassembledBytes}`);
2267
2444
  const open = /* @__PURE__ */ new Map();
2445
+ let reassembling = 0;
2446
+ const freePartial = (entry) => {
2447
+ if (!entry?.partial) return;
2448
+ reassembling -= entry.partial.charged;
2449
+ entry.partial = void 0;
2450
+ };
2451
+ const leaks = onLeak ? new FinalizationRegistry(({ id, record, release }) => {
2452
+ const entry = open.get(id);
2453
+ if (entry?.handle !== record) return;
2454
+ const { handle: _, credit: __, partial: ___, ...info } = entry;
2455
+ if (!record.hasListeners()) {
2456
+ release("leaked");
2457
+ record.markClosed(/* @__PURE__ */ new Error("webrun-rpc: the port was dropped without close()"));
2458
+ }
2459
+ try {
2460
+ onLeak?.(info);
2461
+ } catch {}
2462
+ }) : void 0;
2463
+ const touch = (id) => {
2464
+ const entry = open.get(id);
2465
+ if (entry) entry.lastActivity = Date.now();
2466
+ };
2268
2467
  let nextId = side === "initiator" ? 0 : 1;
2269
2468
  let muxClosed = false;
2469
+ /** This end counts encoded bytes and reassembles frames. */
2470
+ const exactCodec = codec.encode != null && codec.decode != null;
2270
2471
  const post = (envelope, transfer) => {
2271
2472
  if (muxClosed) return;
2473
+ codecPost(envelope, transfer);
2474
+ };
2475
+ /** Like `post`, also while closing: the mux's own close notices. */
2476
+ const codecPost = (envelope, transfer) => {
2272
2477
  try {
2273
2478
  codec.post(port, envelope, transfer);
2274
2479
  } catch {}
2275
2480
  };
2276
- const attach = (id) => {
2277
- let self;
2278
- self = newVirtualPort((payload, transfer) => post({
2279
- type: "message",
2280
- id,
2281
- payload
2282
- }, transfer), (reason) => {
2481
+ const outgoing = /* @__PURE__ */ new Map();
2482
+ const ring = [];
2483
+ let pumping = false;
2484
+ const pump = async () => {
2485
+ pumping = true;
2486
+ try {
2487
+ while (ring.length && !muxClosed) {
2488
+ if (port.whenDrained) await port.whenDrained();
2489
+ if (muxClosed) break;
2490
+ const id = ring.shift();
2491
+ const queue = outgoing.get(id);
2492
+ const item = queue?.shift();
2493
+ if (queue?.length) ring.push(id);
2494
+ else outgoing.delete(id);
2495
+ item?.();
2496
+ }
2497
+ } catch (err) {
2498
+ close(err).catch(() => {});
2499
+ } finally {
2500
+ pumping = false;
2501
+ }
2502
+ };
2503
+ const sendOnPort = (id, item) => {
2504
+ if (frameSize == null) {
2505
+ item();
2506
+ return;
2507
+ }
2508
+ let queue = outgoing.get(id);
2509
+ if (!queue) {
2510
+ queue = [];
2511
+ outgoing.set(id, queue);
2512
+ ring.push(id);
2513
+ }
2514
+ queue.push(item);
2515
+ if (!pumping) pump();
2516
+ };
2517
+ const dropOutgoing = (id) => {
2518
+ if (!outgoing.delete(id)) return;
2519
+ const at = ring.indexOf(id);
2520
+ if (at >= 0) ring.splice(at, 1);
2521
+ };
2522
+ const postBytes = (bytes) => {
2523
+ if (muxClosed) return;
2524
+ try {
2525
+ port.postMessage(bytes);
2526
+ } catch {}
2527
+ };
2528
+ const releaseWaiters = (c) => {
2529
+ for (const w of c?.waiters.splice(0) ?? []) w.resolve();
2530
+ };
2531
+ const flush = (c) => {
2532
+ while (c.queue.length) {
2533
+ const next = c.queue[0];
2534
+ if (c.enforced && c.outstanding > 0 && c.outstanding + next.size > c.window) break;
2535
+ c.queue.shift();
2536
+ c.queued -= next.size;
2537
+ c.outstanding += next.size;
2538
+ next.send();
2539
+ }
2540
+ for (const w of [...c.waiters]) if (c.queued <= w.below) {
2541
+ c.waiters.splice(c.waiters.indexOf(w), 1);
2542
+ w.resolve();
2543
+ }
2544
+ };
2545
+ const grant = (id, c, bytes) => {
2546
+ c.unconsumed -= bytes;
2547
+ c.toGrant += bytes;
2548
+ const send = () => {
2549
+ c.grantScheduled = false;
2550
+ if (!c.toGrant || !open.has(id)) return;
2283
2551
  post({
2552
+ type: "credit",
2553
+ id,
2554
+ bytes: c.toGrant
2555
+ });
2556
+ c.toGrant = 0;
2557
+ };
2558
+ if (c.toGrant >= c.window / 4) {
2559
+ send();
2560
+ return;
2561
+ }
2562
+ if (c.grantScheduled) return;
2563
+ c.grantScheduled = true;
2564
+ setTimeout(send, 0);
2565
+ };
2566
+ const attach = (id, meta, portWindow, opener, exact) => {
2567
+ const c = typeof portWindow === "number" && Number.isSafeInteger(portWindow) && portWindow > 0 ? {
2568
+ window: portWindow,
2569
+ enforced: false,
2570
+ opener,
2571
+ exact,
2572
+ outstanding: 0,
2573
+ queue: [],
2574
+ queued: 0,
2575
+ unconsumed: 0,
2576
+ waiters: [],
2577
+ toGrant: 0,
2578
+ grantScheduled: false
2579
+ } : void 0;
2580
+ const self = newVirtualPort((payload, transfer) => {
2581
+ touch(id);
2582
+ const envelope = {
2583
+ type: "message",
2584
+ id,
2585
+ payload
2586
+ };
2587
+ if (!c) {
2588
+ post(envelope, transfer);
2589
+ return;
2590
+ }
2591
+ const bytes = c.exact ? codec.encode?.(envelope) : void 0;
2592
+ const size = bytes?.byteLength ?? estimateMessageSize(payload);
2593
+ const waits = frameSize != null || c.queue.length > 0 || c.enforced && c.outstanding > 0 && c.outstanding + size > c.window;
2594
+ const held = !bytes && waits ? {
2595
+ ...envelope,
2596
+ payload: structuredClone(payload, { transfer })
2597
+ } : void 0;
2598
+ const send = (whole = false) => {
2599
+ if (!bytes) {
2600
+ sendOnPort(id, held ? () => post(held) : () => post(envelope, transfer));
2601
+ return;
2602
+ }
2603
+ if (!whole && frameSize != null && c.enforced && c.exact && bytes.byteLength > frameSize) {
2604
+ for (let at = 0; at < bytes.byteLength; at += frameSize) {
2605
+ const data = bytes.subarray(at, at + frameSize);
2606
+ const frame = at + frameSize >= bytes.byteLength ? {
2607
+ type: "frame",
2608
+ id,
2609
+ data,
2610
+ last: true
2611
+ } : {
2612
+ type: "frame",
2613
+ id,
2614
+ data
2615
+ };
2616
+ sendOnPort(id, () => post(frame));
2617
+ }
2618
+ return;
2619
+ }
2620
+ sendOnPort(id, () => postBytes(bytes));
2621
+ };
2622
+ c.queue.push({
2623
+ send,
2624
+ size
2625
+ });
2626
+ c.queued += size;
2627
+ flush(c);
2628
+ }, (reason) => {
2629
+ release(reason);
2630
+ record.markClosed(reason);
2631
+ });
2632
+ function release(reason) {
2633
+ freePartial(open.get(id));
2634
+ open.delete(id);
2635
+ leaks?.unregister(record);
2636
+ if (c?.queue.length) {
2637
+ for (const item of c.queue.splice(0)) item.send(true);
2638
+ c.queued = 0;
2639
+ }
2640
+ sendOnPort(id, () => post({
2284
2641
  type: "close",
2285
2642
  id,
2286
2643
  reason
2644
+ }));
2645
+ releaseWaiters(c);
2646
+ }
2647
+ const { markClosed, isClosed, hasListeners } = self;
2648
+ if (c) {
2649
+ const deliver = self.deliver;
2650
+ self.deliver = (payload, size = estimateMessageSize(payload)) => {
2651
+ c.unconsumed += size;
2652
+ const done = deliver(payload);
2653
+ if (done) done.then(() => grant(id, c, size));
2654
+ else grant(id, c, size);
2655
+ return done;
2656
+ };
2657
+ setPortSendState(self.port, {
2658
+ bufferedAmount: () => c.queued,
2659
+ ready: (below = 0) => new Promise((resolve) => {
2660
+ if (c.queued <= below || isClosed()) nextTask(resolve);
2661
+ else c.waiters.push({
2662
+ below,
2663
+ resolve: () => nextTask(resolve)
2664
+ });
2665
+ })
2287
2666
  });
2288
- open.delete(id);
2289
- self.markClosed();
2667
+ }
2668
+ const record = {
2669
+ deliver: self.deliver,
2670
+ markClosed,
2671
+ isClosed,
2672
+ hasListeners,
2673
+ close: (reason) => {
2674
+ if (isClosed()) return;
2675
+ release(reason);
2676
+ markClosed(reason);
2677
+ }
2678
+ };
2679
+ leaks?.register(self.port, {
2680
+ id,
2681
+ record,
2682
+ release
2683
+ }, record);
2684
+ const now = Date.now();
2685
+ open.set(id, {
2686
+ id,
2687
+ meta,
2688
+ openedAt: now,
2689
+ lastActivity: now,
2690
+ handle: record,
2691
+ credit: c
2290
2692
  });
2291
- open.set(id, self);
2292
2693
  return self;
2293
2694
  };
2294
- const handleEnvelope = (envelope) => {
2695
+ const handleEnvelope = (envelope, event) => {
2295
2696
  if (muxClosed) return;
2697
+ heardSincePing = true;
2698
+ if (envelope.type === "ping" || envelope.type === "pong") {
2699
+ peerKeepsAlive = true;
2700
+ if (envelope.type === "ping") post({
2701
+ type: "pong",
2702
+ id: KEEPALIVE_ID
2703
+ });
2704
+ return;
2705
+ }
2296
2706
  if (envelope.type === "open") {
2297
2707
  if (open.has(envelope.id)) return;
2298
2708
  if (open.size >= maxPorts) {
@@ -2303,7 +2713,18 @@ function multiplexPort(port, options) {
2303
2713
  });
2304
2714
  return;
2305
2715
  }
2306
- const handle = attach(envelope.id);
2716
+ const handle = attach(envelope.id, envelope.meta, envelope.window, false, envelope.exact === true && exactCodec);
2717
+ const credit = open.get(envelope.id)?.credit;
2718
+ if (credit) post(credit.exact ? {
2719
+ type: "credit",
2720
+ id: envelope.id,
2721
+ bytes: 0,
2722
+ exact: true
2723
+ } : {
2724
+ type: "credit",
2725
+ id: envelope.id,
2726
+ bytes: 0
2727
+ });
2307
2728
  let accepted = false;
2308
2729
  if (onPort) try {
2309
2730
  accepted = onPort(handle.port, envelope.meta) !== false;
@@ -2318,57 +2739,215 @@ function multiplexPort(port, options) {
2318
2739
  id: envelope.id,
2319
2740
  reason: "rejected"
2320
2741
  });
2742
+ return;
2321
2743
  }
2322
2744
  return;
2323
2745
  }
2324
- const handle = open.get(envelope.id);
2746
+ const entry = open.get(envelope.id);
2747
+ if (envelope.type === "credit") {
2748
+ const c = entry?.credit;
2749
+ if (!c || !(envelope.bytes >= 0)) return;
2750
+ if (!c.enforced && c.opener) {
2751
+ if (c.exact && envelope.exact !== true) {
2752
+ c.exact = false;
2753
+ c.outstanding = 0;
2754
+ }
2755
+ post({
2756
+ type: "credit",
2757
+ id: envelope.id,
2758
+ bytes: 0
2759
+ });
2760
+ }
2761
+ c.enforced = true;
2762
+ c.outstanding = Math.max(0, c.outstanding - envelope.bytes);
2763
+ flush(c);
2764
+ return;
2765
+ }
2325
2766
  if (envelope.type === "message") {
2326
- if (!handle) return;
2327
- handle.deliver(envelope.payload);
2767
+ const sizeOf = (c) => (c.exact ? byteLengthOf(event.data) : void 0) ?? estimateMessageSize(envelope.payload);
2768
+ if (!entry) return;
2769
+ touch(envelope.id);
2770
+ entry.handle.deliver(envelope.payload, entry.credit ? sizeOf(entry.credit) : void 0);
2771
+ return;
2772
+ }
2773
+ if (envelope.type === "frame") {
2774
+ if (!entry?.credit?.exact || !codec.decode || !(envelope.data instanceof Uint8Array)) return;
2775
+ touch(envelope.id);
2776
+ const c = entry.credit;
2777
+ const owed = c.unconsumed + c.toGrant;
2778
+ const partial = entry.partial ?? {
2779
+ chunks: [],
2780
+ bytes: 0,
2781
+ owed,
2782
+ charged: 0,
2783
+ ...owed > 0 ? { room: c.window - owed } : {}
2784
+ };
2785
+ entry.partial = partial;
2786
+ const size = envelope.data.byteLength;
2787
+ const charged = partial.room == null ? Math.max(0, partial.bytes + size - c.window) : 0;
2788
+ 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;
2789
+ if (overrun) {
2790
+ freePartial(entry);
2791
+ dropOutgoing(envelope.id);
2792
+ open.delete(envelope.id);
2793
+ releaseWaiters(c);
2794
+ entry.handle.markClosed(/* @__PURE__ */ new RangeError(`webrun-rpc: a framed message passed ${overrun}`));
2795
+ post({
2796
+ type: "close",
2797
+ id: envelope.id,
2798
+ reason: "max-reassembled-bytes"
2799
+ });
2800
+ return;
2801
+ }
2802
+ partial.bytes += size;
2803
+ reassembling += charged - partial.charged;
2804
+ partial.charged = charged;
2805
+ partial.chunks.push(envelope.data);
2806
+ if (!envelope.last) return;
2807
+ freePartial(entry);
2808
+ const whole = new Uint8Array(partial.bytes);
2809
+ let at = 0;
2810
+ for (const chunk of partial.chunks) {
2811
+ whole.set(chunk, at);
2812
+ at += chunk.byteLength;
2813
+ }
2814
+ const inner = codec.decode(whole);
2815
+ if (inner?.type !== "message" || inner.id !== envelope.id) return;
2816
+ entry.handle.deliver(inner.payload, whole.byteLength);
2328
2817
  return;
2329
2818
  }
2330
- if (!handle) return;
2819
+ dropOutgoing(envelope.id);
2820
+ if (!entry) return;
2821
+ freePartial(entry);
2331
2822
  open.delete(envelope.id);
2332
- handle.markClosed(/* @__PURE__ */ new Error("webrun-rpc: the peer closed the port"));
2823
+ releaseWaiters(entry.credit);
2824
+ entry.handle.markClosed(/* @__PURE__ */ new Error("webrun-rpc: the peer closed the port"));
2333
2825
  };
2334
2826
  const listener = (event) => {
2335
2827
  const envelope = codec.read(event);
2336
- if (envelope) handleEnvelope(envelope);
2828
+ if (envelope) handleEnvelope(envelope, event);
2829
+ };
2830
+ let peerKeepsAlive = false;
2831
+ let heardSincePing = true;
2832
+ let unanswered = 0;
2833
+ let pinger;
2834
+ const tick = () => {
2835
+ if (!keepalive) return;
2836
+ const backlogged = (port.bufferedAmount ?? 0) > 0;
2837
+ if (heardSincePing) unanswered = 0;
2838
+ else if (peerKeepsAlive && !backlogged) unanswered++;
2839
+ if (unanswered >= misses) {
2840
+ close(/* @__PURE__ */ new Error(`webrun-rpc: the peer did not answer ${misses} keepalive pings, ${keepalive.intervalMs} ms apart`)).catch(() => {});
2841
+ return;
2842
+ }
2843
+ heardSincePing = false;
2844
+ post({
2845
+ type: "ping",
2846
+ id: KEEPALIVE_ID
2847
+ });
2848
+ };
2849
+ const close = async (reason) => {
2850
+ if (muxClosed) return;
2851
+ clearInterval(pinger);
2852
+ muxClosed = true;
2853
+ signal?.removeEventListener("abort", onTransportAbort);
2854
+ port.removeEventListener("message", listener);
2855
+ const closedNow = /* @__PURE__ */ new Set();
2856
+ for (const [id, { handle, credit }] of [...open]) {
2857
+ closedNow.add(id);
2858
+ codecPost({
2859
+ type: "close",
2860
+ id
2861
+ });
2862
+ open.delete(id);
2863
+ releaseWaiters(credit);
2864
+ handle.markClosed(reason ?? /* @__PURE__ */ new Error("webrun-rpc: the multiplexer was closed"));
2865
+ }
2866
+ for (const id of outgoing.keys()) if (!closedNow.has(id)) codecPost({
2867
+ type: "close",
2868
+ id
2869
+ });
2870
+ outgoing.clear();
2871
+ ring.length = 0;
2872
+ await port.close?.();
2873
+ };
2874
+ const onTransportAbort = () => {
2875
+ close(signal?.reason).catch(() => {});
2337
2876
  };
2338
2877
  port.addEventListener("message", listener);
2339
2878
  port.start?.();
2879
+ if (keepalive) {
2880
+ pinger = setInterval(tick, keepalive.intervalMs);
2881
+ pinger.unref?.();
2882
+ }
2883
+ if (signal?.aborted) close(signal.reason).catch(() => {});
2884
+ else signal?.addEventListener("abort", onTransportAbort, { once: true });
2340
2885
  return {
2341
2886
  maxMessageSize,
2342
- async openPort(meta) {
2887
+ async openPort(meta, { signal: portSignal } = {}) {
2343
2888
  if (muxClosed) throw new Error("webrun-rpc: the multiplexer is closed");
2344
- if (open.size >= maxPorts) throw new RangeError(`webrun-rpc: maxPorts (${maxPorts}) reached`);
2889
+ if (portSignal?.aborted) throw portSignal.reason;
2890
+ if (open.size >= maxPorts) throw new RangeError(`webrun-rpc: maxPorts (${maxPorts}) reached; open ports by kind: ${describeKinds(open.values())}`);
2345
2891
  while (open.has(nextId)) nextId += 2;
2346
2892
  const id = nextId;
2347
2893
  nextId += 2;
2348
- const handle = attach(id);
2349
- post({
2894
+ const handle = attach(id, meta, window, true, exactCodec);
2895
+ post(window ? exactCodec ? {
2896
+ type: "open",
2897
+ id,
2898
+ meta,
2899
+ window,
2900
+ exact: true
2901
+ } : {
2902
+ type: "open",
2903
+ id,
2904
+ meta,
2905
+ window
2906
+ } : {
2350
2907
  type: "open",
2351
2908
  id,
2352
2909
  meta
2353
2910
  });
2911
+ if (portSignal) {
2912
+ const record = open.get(id)?.handle;
2913
+ const onAbort = () => {
2914
+ if (record && open.get(id)?.handle === record) record.close(portSignal.reason);
2915
+ };
2916
+ portSignal.addEventListener("abort", onAbort, { once: true });
2917
+ getPortCloseSignal(handle.port)?.addEventListener("abort", () => portSignal.removeEventListener("abort", onAbort), { once: true });
2918
+ }
2354
2919
  return handle.port;
2355
2920
  },
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?.();
2921
+ close,
2922
+ ports() {
2923
+ return [...open.values()].map(({ handle: _, credit, ...info }) => credit ? {
2924
+ ...info,
2925
+ ...credit.enforced ? { window: credit.window } : {},
2926
+ queued: credit.queued,
2927
+ outstanding: credit.outstanding,
2928
+ unconsumed: credit.unconsumed
2929
+ } : info);
2369
2930
  }
2370
2931
  };
2371
2932
  }
2933
+ /** The byte length of a received message's data, or `undefined` if it is not bytes. */
2934
+ function byteLengthOf(data) {
2935
+ if (data instanceof ArrayBuffer || ArrayBuffer.isView(data)) return data.byteLength;
2936
+ }
2937
+ /**
2938
+ * `"http: 1000, ws: 24"`: the three commonest `meta.kind`s among `entries`, so
2939
+ * a full table names who filled it. A port without a kind counts as `"?"`.
2940
+ */
2941
+ function describeKinds(entries) {
2942
+ const counts = /* @__PURE__ */ new Map();
2943
+ for (const { meta } of entries) {
2944
+ const kind = meta?.kind;
2945
+ const name = typeof kind === "string" ? kind : "?";
2946
+ counts.set(name, (counts.get(name) ?? 0) + 1);
2947
+ }
2948
+ if (!counts.size) return "none";
2949
+ return [...counts].sort((x, y) => y[1] - x[1]).slice(0, 3).map(([name, n]) => `${name}: ${n}`).join(", ");
2950
+ }
2372
2951
  /**
2373
2952
  * Announced with every port this pair opens. Layer 1 never reads `meta` — it is
2374
2953
  * here so a peer running something else over the same multiplexer can tell a
@@ -2430,14 +3009,17 @@ const serve = async ({ mux: factory, timeout }, handler) => {
2430
3009
  const mux = await factory((port) => {
2431
3010
  let off;
2432
3011
  let finished = false;
2433
- const tracked = (input) => (async function* () {
2434
- try {
2435
- yield* handler(input);
2436
- } finally {
2437
- finished = true;
2438
- if (off) live.delete(off);
2439
- }
2440
- })();
3012
+ const tracked = (input) => {
3013
+ const output = handler(input);
3014
+ return returningAtOnce((async function* () {
3015
+ try {
3016
+ yield* output;
3017
+ } finally {
3018
+ finished = true;
3019
+ if (off) live.delete(off);
3020
+ }
3021
+ })(), () => void output.return(void 0).catch(() => {}));
3022
+ };
2441
3023
  off = serveDuplexOverPort(port, tracked, streamOptions);
2442
3024
  if (!finished) live.add(off);
2443
3025
  });
@@ -2475,7 +3057,7 @@ function isEnvelope(value) {
2475
3057
  const candidate = value;
2476
3058
  if (typeof candidate.id !== "number") return false;
2477
3059
  if (!Number.isInteger(candidate.id) || candidate.id < 0) return false;
2478
- return candidate.type === "open" || candidate.type === "message" || candidate.type === "close";
3060
+ return candidate.type === "open" || candidate.type === "message" || candidate.type === "close" || candidate.type === "credit" || candidate.type === "frame" || candidate.type === "ping" || candidate.type === "pong";
2479
3061
  }
2480
3062
  /**
2481
3063
  * For ports whose messages are structured values — a real `MessagePort`, a
@@ -2637,7 +3219,7 @@ function trackedLink(send, release) {
2637
3219
  finish();
2638
3220
  return response;
2639
3221
  }
2640
- return new Response(guardBody(response.body, lost, () => lostReason, finish), {
3222
+ return new Response(guardBody(response.body, lost, () => lostReason, finish, request.signal), {
2641
3223
  status: response.status,
2642
3224
  statusText: response.statusText,
2643
3225
  headers: response.headers
@@ -2656,33 +3238,65 @@ function trackedLink(send, release) {
2656
3238
  return asyncDisposable(link, () => link.close());
2657
3239
  }
2658
3240
  /**
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.
3241
+ * A response body that reports its end (done, cancelled, failed or aborted) and
3242
+ * fails with the link's reason when the link is lost mid-stream. An abort of the
3243
+ * request's signal ends it at once, read or not: the exchange is over (the layer
3244
+ * below has given its port back), so the request leaves `inflight` then.
2661
3245
  */
2662
- function guardBody(body, lost, reason, finish) {
3246
+ function guardBody(body, lost, reason, finish, signal) {
2663
3247
  const reader = body.getReader();
3248
+ let controller;
3249
+ let ended = false;
3250
+ const end = () => {
3251
+ ended = true;
3252
+ signal?.removeEventListener("abort", onAbort);
3253
+ finish();
3254
+ };
3255
+ const onAbort = () => {
3256
+ if (ended) return;
3257
+ end();
3258
+ reader.cancel().catch(() => {});
3259
+ try {
3260
+ controller.error(abortError(signal));
3261
+ } catch {}
3262
+ };
2664
3263
  return new ReadableStream({
3264
+ start(c) {
3265
+ controller = c;
3266
+ if (signal?.aborted) onAbort();
3267
+ else signal?.addEventListener("abort", onAbort, { once: true });
3268
+ },
2665
3269
  async pull(controller) {
2666
3270
  try {
2667
3271
  const { done, value } = await Promise.race([reader.read(), lost]);
3272
+ if (ended) return;
2668
3273
  if (done) {
2669
- finish();
3274
+ end();
2670
3275
  controller.close();
2671
3276
  return;
2672
3277
  }
2673
3278
  controller.enqueue(value);
2674
3279
  } catch (error) {
2675
- finish();
3280
+ if (ended) return;
3281
+ end();
2676
3282
  reader.cancel().catch(() => {});
2677
3283
  controller.error(reason() ?? error);
2678
3284
  }
2679
3285
  },
2680
3286
  async cancel(why) {
2681
- finish();
3287
+ end();
2682
3288
  await reader.cancel(why).catch(() => {});
2683
3289
  }
2684
3290
  });
2685
3291
  }
3292
+ /** The signal's reason as an `Error` (`AbortError` when it gave none). */
3293
+ function abortError(signal) {
3294
+ const why = signal.reason;
3295
+ if (why instanceof Error) return why;
3296
+ const err = new Error(why === void 0 ? "Aborted" : String(why));
3297
+ err.name = "AbortError";
3298
+ return err;
3299
+ }
2686
3300
  //#endregion
2687
3301
  //#region src/link/locks.ts
2688
3302
  function lockManager() {
@@ -5498,6 +6112,8 @@ var WorkerLinkImpl = class {
5498
6112
  _ownerLock;
5499
6113
  _attaching;
5500
6114
  _detached = false;
6115
+ /** The `seq` of the last link sent to the worker. */
6116
+ _sent = 0;
5501
6117
  constructor(_adapter, _control, init) {
5502
6118
  this._adapter = _adapter;
5503
6119
  this._control = _control;
@@ -5509,6 +6125,7 @@ var WorkerLinkImpl = class {
5509
6125
  _control.onmessage = (event) => {
5510
6126
  const message = event.data;
5511
6127
  if (message?.type === "relink") {
6128
+ if (message.seen !== void 0 && message.seen < this._sent) return;
5512
6129
  this._adapter._poke(WAKE_MESSAGE);
5513
6130
  this.attach().catch(() => {});
5514
6131
  } else if (message?.type === "keepalive") this._adapter._poke(KEEPALIVE_MESSAGE);
@@ -5555,7 +6172,8 @@ var WorkerLinkImpl = class {
5555
6172
  this._control.postMessage({
5556
6173
  type: "link",
5557
6174
  key: this.key,
5558
- routerLock: reply.routerLock
6175
+ routerLock: reply.routerLock,
6176
+ seq: ++this._sent
5559
6177
  }, [
5560
6178
  down.port2,
5561
6179
  ctl.port2,
@@ -5656,12 +6274,18 @@ function serveHttpInWorker(handler, options = {}) {
5656
6274
  const ownerLock = newLockName("webrun-http-owner");
5657
6275
  const held = holdLock(ownerLock);
5658
6276
  const controls = /* @__PURE__ */ new Set();
6277
+ const seen = /* @__PURE__ */ new Map();
5659
6278
  const tellPages = (message) => {
5660
6279
  for (const control of controls) control.postMessage(message);
5661
6280
  };
5662
6281
  const endpoint = new HandlerEndpoint(handler, {
5663
6282
  ...options,
5664
- requestRelink: () => tellPages({ type: "relink" }),
6283
+ requestRelink: () => {
6284
+ for (const control of controls) control.postMessage({
6285
+ type: "relink",
6286
+ seen: seen.get(control) ?? 0
6287
+ });
6288
+ },
5665
6289
  keepAlive: () => tellPages({ type: "keepalive" })
5666
6290
  });
5667
6291
  const onMessage = (event) => {
@@ -5673,6 +6297,7 @@ function serveHttpInWorker(handler, options = {}) {
5673
6297
  control.onmessage = (message) => {
5674
6298
  const msg = message.data;
5675
6299
  if (msg?.type === "link") {
6300
+ if (msg.seq !== void 0) seen.set(control, msg.seq);
5676
6301
  const [down, ctl, up] = message.ports;
5677
6302
  if (down) endpoint.link(msg.key, {
5678
6303
  down,
@@ -5713,6 +6338,7 @@ function serveHttpInWorker(handler, options = {}) {
5713
6338
  await endpoint.close();
5714
6339
  for (const control of controls) control.close();
5715
6340
  controls.clear();
6341
+ seen.clear();
5716
6342
  (await held)();
5717
6343
  }
5718
6344
  };