@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.
@@ -1367,26 +1367,6 @@ async function* readableToAsyncIterable(stream) {
1367
1367
  } catch {}
1368
1368
  }
1369
1369
  }
1370
- function asyncIterableToReadable(iter) {
1371
- const it = iter[Symbol.asyncIterator]();
1372
- return new ReadableStream({
1373
- async pull(controller) {
1374
- try {
1375
- const { done, value } = await it.next();
1376
- if (done) {
1377
- controller.close();
1378
- return;
1379
- }
1380
- controller.enqueue(value);
1381
- } catch (err) {
1382
- controller.error(err);
1383
- }
1384
- },
1385
- async cancel() {
1386
- await it.return?.();
1387
- }
1388
- });
1389
- }
1390
1370
  /**
1391
1371
  * Run a `Request` through a `Duplex` call and reconstruct the `Response` on
1392
1372
  * the other side. The request's `signal` is plumbed into the body iteration —
@@ -1419,17 +1399,80 @@ async function fetchOverDuplex(call, request, options = {}) {
1419
1399
  else await release?.();
1420
1400
  return new Response(null, responseInit);
1421
1401
  }
1422
- return new Response(asyncIterableToReadable(withAbort(respBody, request.signal)), responseInit);
1402
+ return new Response(responseBody(respBody, request.signal, release), responseInit);
1423
1403
  }
1424
- async function* withAbort(iter, signal) {
1425
- if (!signal) {
1426
- yield* iter;
1427
- return;
1428
- }
1429
- for await (const chunk of iter) {
1430
- if (signal.aborted) throw abortReason$1(signal);
1431
- yield chunk;
1432
- }
1404
+ /**
1405
+ * How long a cancelled body waits for the read in flight before it abandons the call anyway.
1406
+ * The read's answer tells a body that has ended (the call is finished: an upload the handler
1407
+ * reads after answering goes on) from one that has not; a peer that sends nothing more never
1408
+ * answers it. A body whose end comes later than this after the cancel is abandoned too, so a
1409
+ * handler still streaming its response and still reading the upload then loses the upload.
1410
+ */
1411
+ const CANCEL_GRACE_MS = 1e3;
1412
+ /**
1413
+ * The response body as a stream that the request's signal can end at any time.
1414
+ *
1415
+ * An abort errors the stream with the abort's reason at once — a read waiting
1416
+ * for a peer that sends nothing more rejects then, not when a next chunk
1417
+ * arrives, which may be never — and abandons the call (`release`), so on a
1418
+ * multiplexed transport its port is given back whether or not anyone still
1419
+ * reads the body. The listener is removed once the body ends, however it ends:
1420
+ * read to its end, cancelled, failed or aborted. A body nobody reads or cancels
1421
+ * keeps it until the signal aborts (and keeps the call open until then anyway).
1422
+ * A cancel abandons the call too — unless the read in flight comes back with the
1423
+ * body's end, which finishes it instead (see `cancel`).
1424
+ */
1425
+ function responseBody(body, signal, release) {
1426
+ const it = body[Symbol.asyncIterator]();
1427
+ let controller;
1428
+ let ended = false;
1429
+ let reading = false;
1430
+ const end = () => {
1431
+ ended = true;
1432
+ signal?.removeEventListener("abort", onAbort);
1433
+ };
1434
+ const onAbort = () => {
1435
+ if (ended) return;
1436
+ end();
1437
+ try {
1438
+ controller.error(abortReason$1(signal));
1439
+ } catch {}
1440
+ release?.();
1441
+ };
1442
+ return new ReadableStream({
1443
+ start(c) {
1444
+ controller = c;
1445
+ if (signal?.aborted) onAbort();
1446
+ else signal?.addEventListener("abort", onAbort, { once: true });
1447
+ },
1448
+ async pull(c) {
1449
+ try {
1450
+ reading = true;
1451
+ const { done, value } = await it.next().finally(() => {
1452
+ reading = false;
1453
+ });
1454
+ if (ended) return;
1455
+ if (done) {
1456
+ end();
1457
+ c.close();
1458
+ return;
1459
+ }
1460
+ c.enqueue(value);
1461
+ } catch (err) {
1462
+ if (ended) return;
1463
+ end();
1464
+ c.error(err);
1465
+ }
1466
+ },
1467
+ cancel() {
1468
+ end();
1469
+ const returned = Promise.resolve(it.return?.()).catch(() => {});
1470
+ if (!reading) return;
1471
+ const grace = setTimeout(() => void release?.(), CANCEL_GRACE_MS);
1472
+ grace.unref?.();
1473
+ returned.finally(() => clearTimeout(grace));
1474
+ }
1475
+ });
1433
1476
  }
1434
1477
  var HttpError = class HttpError extends Error {
1435
1478
  status;
@@ -1774,8 +1817,11 @@ function listenPort(port, handler, { channelName = "", log = () => {} } = {}) {
1774
1817
  error
1775
1818
  });
1776
1819
  };
1777
- port.addEventListener("message", onMessage);
1778
- return () => port.removeEventListener("message", onMessage);
1820
+ const listener = (event) => {
1821
+ onMessage(event);
1822
+ };
1823
+ port.addEventListener("message", listener);
1824
+ return () => port.removeEventListener("message", listener);
1779
1825
  }
1780
1826
  /**
1781
1827
  * Wraps an async iterable so that an `AbortSignal` firing causes the wrapper
@@ -2094,7 +2140,11 @@ function receiveChunks(port, channelName, controller, touch) {
2094
2140
  * outstanding at a time (spec D11/D12), with no per-chunk deadline (spec D8).
2095
2141
  */
2096
2142
  async function sendChunks(port, channelName, output, { maxMessageSize, log }, signal, touch) {
2097
- const stream = throughAbort(maxMessageSize ? toChunks(maxMessageSize)(output) : output, signal);
2143
+ const framed = maxMessageSize ? toChunks(maxMessageSize)(output) : output;
2144
+ const stream = throughAbort(framed, signal);
2145
+ const producer = output;
2146
+ const returnProducer = () => void Promise.resolve(producer.return?.()).catch(() => {});
2147
+ if (framed !== output) signal.addEventListener("abort", returnProducer, { once: true });
2098
2148
  try {
2099
2149
  await sendIterator(async ({ done, value, error }) => {
2100
2150
  if (signal.aborted) return;
@@ -2118,7 +2168,36 @@ async function sendChunks(port, channelName, output, { maxMessageSize, log }, si
2118
2168
  } catch (err) {
2119
2169
  if (signal.aborted) return;
2120
2170
  throw err;
2121
- }
2171
+ } finally {
2172
+ signal.removeEventListener("abort", returnProducer);
2173
+ }
2174
+ }
2175
+ /**
2176
+ * Runs `fn` on a later task, never a microtask: a producer looping on
2177
+ * `whenPortReady` must let the transport's own events run. `setImmediate`
2178
+ * where it exists; elsewhere a `MessageChannel`, because nested `setTimeout`s
2179
+ * are clamped to 4 ms in browsers.
2180
+ */
2181
+ const setImmediateFn = globalThis.setImmediate;
2182
+ /** @internal */
2183
+ const nextTask = setImmediateFn ? (fn) => {
2184
+ setImmediateFn(fn);
2185
+ } : (() => {
2186
+ let channel;
2187
+ const queue = [];
2188
+ return (fn) => {
2189
+ if (!channel) {
2190
+ channel = new MessageChannel();
2191
+ channel.port1.onmessage = () => queue.shift()?.();
2192
+ }
2193
+ queue.push(fn);
2194
+ channel.port2.postMessage(0);
2195
+ };
2196
+ })();
2197
+ const states = /* @__PURE__ */ new WeakMap();
2198
+ /** @internal Registered by the multiplexer for each port with a window. */
2199
+ function setPortSendState(port, state) {
2200
+ states.set(port, state);
2122
2201
  }
2123
2202
  /**
2124
2203
  * One virtual port.
@@ -2135,6 +2214,14 @@ function newVirtualPort(send, requestClose) {
2135
2214
  if (closing.signal.aborted) return;
2136
2215
  closing.abort(reason ?? /* @__PURE__ */ new Error("webrun-rpc: the port was closed"));
2137
2216
  };
2217
+ const close = (reason) => {
2218
+ if (closed) return;
2219
+ closed = true;
2220
+ const notify = requestClose;
2221
+ listeners.clear();
2222
+ notify(reason);
2223
+ abort(reason);
2224
+ };
2138
2225
  const port = {
2139
2226
  addEventListener(_type, listener) {
2140
2227
  listeners.add(listener);
@@ -2146,24 +2233,28 @@ function newVirtualPort(send, requestClose) {
2146
2233
  if (closed) return;
2147
2234
  send(message, transfer);
2148
2235
  },
2149
- close() {
2150
- if (closed) return;
2151
- closed = true;
2152
- const notify = requestClose;
2153
- listeners.clear();
2154
- notify();
2155
- abort();
2156
- }
2236
+ close
2157
2237
  };
2158
2238
  setPortCloseSignal(port, closing.signal);
2239
+ const dispose = Symbol.dispose;
2240
+ if (dispose) Object.defineProperty(port, dispose, {
2241
+ value: () => close(),
2242
+ configurable: true
2243
+ });
2159
2244
  return {
2160
2245
  port,
2161
2246
  deliver(payload) {
2162
2247
  if (closed) return;
2163
2248
  const event = new MessageEvent("message", { data: payload });
2249
+ let pending;
2164
2250
  for (const listener of [...listeners]) try {
2165
- listener(event);
2251
+ const result = listener(event);
2252
+ if (result && typeof result.then === "function") {
2253
+ pending ??= [];
2254
+ pending.push(result);
2255
+ }
2166
2256
  } catch {}
2257
+ return pending ? Promise.allSettled(pending) : void 0;
2167
2258
  },
2168
2259
  markClosed(reason) {
2169
2260
  closed = true;
@@ -2172,53 +2263,318 @@ function newVirtualPort(send, requestClose) {
2172
2263
  },
2173
2264
  isClosed() {
2174
2265
  return closed;
2266
+ },
2267
+ hasListeners() {
2268
+ return listeners.size > 0;
2175
2269
  }
2176
2270
  };
2177
2271
  }
2178
2272
  /**
2179
- * The message of the error that fails the calls still in flight on a
2180
- * multiplexer when it is closed (`PortMux.close()`).
2273
+ * The id of keepalive's `ping` / `pong`: one no port ever takes. An older mux
2274
+ * whose codec passes unknown envelopes through reads them as a close of that
2275
+ * id, which must therefore not be a real port (0 is the initiator's first).
2181
2276
  */
2182
- const MUX_CLOSED_MESSAGE = "webrun-rpc: the multiplexer was closed";
2277
+ const KEEPALIVE_ID = Number.MAX_SAFE_INTEGER;
2278
+ /** The longest delay a timer holds; past it `setInterval` fires at once. */
2279
+ const MAX_TIMER_MS = 2 ** 31 - 1;
2183
2280
  /** Ceiling on concurrently open virtual ports. Bounds the id table only. */
2184
2281
  const DEFAULT_MAX_PORTS = 1024;
2185
2282
  /**
2283
+ * What a message costs a port's window when the codec cannot say
2284
+ * (`PortCodec.encode`): its bytes and string lengths, found in its values up
2285
+ * to 4 levels deep (at most 256 values), and at least 64. Both ends compute it
2286
+ * from the same structured value, so they agree.
2287
+ */
2288
+ function estimateMessageSize(payload) {
2289
+ let budget = 256;
2290
+ const walk = (v, depth) => {
2291
+ if (budget-- <= 0) return 0;
2292
+ if (typeof v === "string") return v.length;
2293
+ if (v instanceof ArrayBuffer) return v.byteLength;
2294
+ if (ArrayBuffer.isView(v)) return v.byteLength;
2295
+ if (v === null || typeof v !== "object" || depth >= 4) return 8;
2296
+ let n = 16;
2297
+ if (Array.isArray(v)) for (const x of v) n += walk(x, depth + 1);
2298
+ else for (const k in v) n += k.length + walk(v[k], depth + 1);
2299
+ return n;
2300
+ };
2301
+ return Math.max(64, walk(payload, 0));
2302
+ }
2303
+ /**
2186
2304
  * The default `PortMux`: emulates multiplexing over a single port.
2187
2305
  *
2188
2306
  * A transport that already multiplexes natively supplies its own `PortMux`
2189
2307
  * instead — this implementation is for transports that offer one pipe.
2190
2308
  */
2191
2309
  function multiplexPort(port, options) {
2192
- const { codec, onPort, side = "initiator", maxPorts = DEFAULT_MAX_PORTS, maxMessageSize } = options;
2310
+ const { codec, onPort, side = "initiator", maxPorts = DEFAULT_MAX_PORTS, maxMessageSize, signal, window, frameSize, keepalive, onLeak } = options;
2311
+ const maxReassembledBytes = options.maxReassembledBytes ?? 67108864;
2312
+ const misses = keepalive?.misses ?? 3;
2313
+ 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`);
2314
+ if (frameSize != null && !(Number.isSafeInteger(frameSize) && frameSize > 0)) throw new RangeError(`webrun-rpc: frameSize must be a positive integer, got ${frameSize}`);
2315
+ if (!(Number.isSafeInteger(maxReassembledBytes) && maxReassembledBytes > 0)) throw new RangeError(`webrun-rpc: maxReassembledBytes must be a positive integer, got ${maxReassembledBytes}`);
2193
2316
  const open = /* @__PURE__ */ new Map();
2317
+ let reassembling = 0;
2318
+ const freePartial = (entry) => {
2319
+ if (!entry?.partial) return;
2320
+ reassembling -= entry.partial.charged;
2321
+ entry.partial = void 0;
2322
+ };
2323
+ const leaks = onLeak ? new FinalizationRegistry(({ id, record, release }) => {
2324
+ const entry = open.get(id);
2325
+ if (entry?.handle !== record) return;
2326
+ const { handle: _, credit: __, partial: ___, ...info } = entry;
2327
+ if (!record.hasListeners()) {
2328
+ release("leaked");
2329
+ record.markClosed(/* @__PURE__ */ new Error("webrun-rpc: the port was dropped without close()"));
2330
+ }
2331
+ try {
2332
+ onLeak?.(info);
2333
+ } catch {}
2334
+ }) : void 0;
2335
+ const touch = (id) => {
2336
+ const entry = open.get(id);
2337
+ if (entry) entry.lastActivity = Date.now();
2338
+ };
2194
2339
  let nextId = side === "initiator" ? 0 : 1;
2195
2340
  let muxClosed = false;
2341
+ /** This end counts encoded bytes and reassembles frames. */
2342
+ const exactCodec = codec.encode != null && codec.decode != null;
2196
2343
  const post = (envelope, transfer) => {
2197
2344
  if (muxClosed) return;
2345
+ codecPost(envelope, transfer);
2346
+ };
2347
+ /** Like `post`, also while closing: the mux's own close notices. */
2348
+ const codecPost = (envelope, transfer) => {
2198
2349
  try {
2199
2350
  codec.post(port, envelope, transfer);
2200
2351
  } catch {}
2201
2352
  };
2202
- const attach = (id) => {
2203
- let self;
2204
- self = newVirtualPort((payload, transfer) => post({
2205
- type: "message",
2206
- id,
2207
- payload
2208
- }, transfer), (reason) => {
2353
+ const outgoing = /* @__PURE__ */ new Map();
2354
+ const ring = [];
2355
+ let pumping = false;
2356
+ const pump = async () => {
2357
+ pumping = true;
2358
+ try {
2359
+ while (ring.length && !muxClosed) {
2360
+ if (port.whenDrained) await port.whenDrained();
2361
+ if (muxClosed) break;
2362
+ const id = ring.shift();
2363
+ const queue = outgoing.get(id);
2364
+ const item = queue?.shift();
2365
+ if (queue?.length) ring.push(id);
2366
+ else outgoing.delete(id);
2367
+ item?.();
2368
+ }
2369
+ } catch (err) {
2370
+ close(err).catch(() => {});
2371
+ } finally {
2372
+ pumping = false;
2373
+ }
2374
+ };
2375
+ const sendOnPort = (id, item) => {
2376
+ if (frameSize == null) {
2377
+ item();
2378
+ return;
2379
+ }
2380
+ let queue = outgoing.get(id);
2381
+ if (!queue) {
2382
+ queue = [];
2383
+ outgoing.set(id, queue);
2384
+ ring.push(id);
2385
+ }
2386
+ queue.push(item);
2387
+ if (!pumping) pump();
2388
+ };
2389
+ const dropOutgoing = (id) => {
2390
+ if (!outgoing.delete(id)) return;
2391
+ const at = ring.indexOf(id);
2392
+ if (at >= 0) ring.splice(at, 1);
2393
+ };
2394
+ const postBytes = (bytes) => {
2395
+ if (muxClosed) return;
2396
+ try {
2397
+ port.postMessage(bytes);
2398
+ } catch {}
2399
+ };
2400
+ const releaseWaiters = (c) => {
2401
+ for (const w of c?.waiters.splice(0) ?? []) w.resolve();
2402
+ };
2403
+ const flush = (c) => {
2404
+ while (c.queue.length) {
2405
+ const next = c.queue[0];
2406
+ if (c.enforced && c.outstanding > 0 && c.outstanding + next.size > c.window) break;
2407
+ c.queue.shift();
2408
+ c.queued -= next.size;
2409
+ c.outstanding += next.size;
2410
+ next.send();
2411
+ }
2412
+ for (const w of [...c.waiters]) if (c.queued <= w.below) {
2413
+ c.waiters.splice(c.waiters.indexOf(w), 1);
2414
+ w.resolve();
2415
+ }
2416
+ };
2417
+ const grant = (id, c, bytes) => {
2418
+ c.unconsumed -= bytes;
2419
+ c.toGrant += bytes;
2420
+ const send = () => {
2421
+ c.grantScheduled = false;
2422
+ if (!c.toGrant || !open.has(id)) return;
2209
2423
  post({
2424
+ type: "credit",
2425
+ id,
2426
+ bytes: c.toGrant
2427
+ });
2428
+ c.toGrant = 0;
2429
+ };
2430
+ if (c.toGrant >= c.window / 4) {
2431
+ send();
2432
+ return;
2433
+ }
2434
+ if (c.grantScheduled) return;
2435
+ c.grantScheduled = true;
2436
+ setTimeout(send, 0);
2437
+ };
2438
+ const attach = (id, meta, portWindow, opener, exact) => {
2439
+ const c = typeof portWindow === "number" && Number.isSafeInteger(portWindow) && portWindow > 0 ? {
2440
+ window: portWindow,
2441
+ enforced: false,
2442
+ opener,
2443
+ exact,
2444
+ outstanding: 0,
2445
+ queue: [],
2446
+ queued: 0,
2447
+ unconsumed: 0,
2448
+ waiters: [],
2449
+ toGrant: 0,
2450
+ grantScheduled: false
2451
+ } : void 0;
2452
+ const self = newVirtualPort((payload, transfer) => {
2453
+ touch(id);
2454
+ const envelope = {
2455
+ type: "message",
2456
+ id,
2457
+ payload
2458
+ };
2459
+ if (!c) {
2460
+ post(envelope, transfer);
2461
+ return;
2462
+ }
2463
+ const bytes = c.exact ? codec.encode?.(envelope) : void 0;
2464
+ const size = bytes?.byteLength ?? estimateMessageSize(payload);
2465
+ const waits = frameSize != null || c.queue.length > 0 || c.enforced && c.outstanding > 0 && c.outstanding + size > c.window;
2466
+ const held = !bytes && waits ? {
2467
+ ...envelope,
2468
+ payload: structuredClone(payload, { transfer })
2469
+ } : void 0;
2470
+ const send = (whole = false) => {
2471
+ if (!bytes) {
2472
+ sendOnPort(id, held ? () => post(held) : () => post(envelope, transfer));
2473
+ return;
2474
+ }
2475
+ if (!whole && frameSize != null && c.enforced && c.exact && bytes.byteLength > frameSize) {
2476
+ for (let at = 0; at < bytes.byteLength; at += frameSize) {
2477
+ const data = bytes.subarray(at, at + frameSize);
2478
+ const frame = at + frameSize >= bytes.byteLength ? {
2479
+ type: "frame",
2480
+ id,
2481
+ data,
2482
+ last: true
2483
+ } : {
2484
+ type: "frame",
2485
+ id,
2486
+ data
2487
+ };
2488
+ sendOnPort(id, () => post(frame));
2489
+ }
2490
+ return;
2491
+ }
2492
+ sendOnPort(id, () => postBytes(bytes));
2493
+ };
2494
+ c.queue.push({
2495
+ send,
2496
+ size
2497
+ });
2498
+ c.queued += size;
2499
+ flush(c);
2500
+ }, (reason) => {
2501
+ release(reason);
2502
+ record.markClosed(reason);
2503
+ });
2504
+ function release(reason) {
2505
+ freePartial(open.get(id));
2506
+ open.delete(id);
2507
+ leaks?.unregister(record);
2508
+ if (c?.queue.length) {
2509
+ for (const item of c.queue.splice(0)) item.send(true);
2510
+ c.queued = 0;
2511
+ }
2512
+ sendOnPort(id, () => post({
2210
2513
  type: "close",
2211
2514
  id,
2212
2515
  reason
2516
+ }));
2517
+ releaseWaiters(c);
2518
+ }
2519
+ const { markClosed, isClosed, hasListeners } = self;
2520
+ if (c) {
2521
+ const deliver = self.deliver;
2522
+ self.deliver = (payload, size = estimateMessageSize(payload)) => {
2523
+ c.unconsumed += size;
2524
+ const done = deliver(payload);
2525
+ if (done) done.then(() => grant(id, c, size));
2526
+ else grant(id, c, size);
2527
+ return done;
2528
+ };
2529
+ setPortSendState(self.port, {
2530
+ bufferedAmount: () => c.queued,
2531
+ ready: (below = 0) => new Promise((resolve) => {
2532
+ if (c.queued <= below || isClosed()) nextTask(resolve);
2533
+ else c.waiters.push({
2534
+ below,
2535
+ resolve: () => nextTask(resolve)
2536
+ });
2537
+ })
2213
2538
  });
2214
- open.delete(id);
2215
- self.markClosed();
2539
+ }
2540
+ const record = {
2541
+ deliver: self.deliver,
2542
+ markClosed,
2543
+ isClosed,
2544
+ hasListeners,
2545
+ close: (reason) => {
2546
+ if (isClosed()) return;
2547
+ release(reason);
2548
+ markClosed(reason);
2549
+ }
2550
+ };
2551
+ leaks?.register(self.port, {
2552
+ id,
2553
+ record,
2554
+ release
2555
+ }, record);
2556
+ const now = Date.now();
2557
+ open.set(id, {
2558
+ id,
2559
+ meta,
2560
+ openedAt: now,
2561
+ lastActivity: now,
2562
+ handle: record,
2563
+ credit: c
2216
2564
  });
2217
- open.set(id, self);
2218
2565
  return self;
2219
2566
  };
2220
- const handleEnvelope = (envelope) => {
2567
+ const handleEnvelope = (envelope, event) => {
2221
2568
  if (muxClosed) return;
2569
+ heardSincePing = true;
2570
+ if (envelope.type === "ping" || envelope.type === "pong") {
2571
+ peerKeepsAlive = true;
2572
+ if (envelope.type === "ping") post({
2573
+ type: "pong",
2574
+ id: KEEPALIVE_ID
2575
+ });
2576
+ return;
2577
+ }
2222
2578
  if (envelope.type === "open") {
2223
2579
  if (open.has(envelope.id)) return;
2224
2580
  if (open.size >= maxPorts) {
@@ -2229,7 +2585,18 @@ function multiplexPort(port, options) {
2229
2585
  });
2230
2586
  return;
2231
2587
  }
2232
- const handle = attach(envelope.id);
2588
+ const handle = attach(envelope.id, envelope.meta, envelope.window, false, envelope.exact === true && exactCodec);
2589
+ const credit = open.get(envelope.id)?.credit;
2590
+ if (credit) post(credit.exact ? {
2591
+ type: "credit",
2592
+ id: envelope.id,
2593
+ bytes: 0,
2594
+ exact: true
2595
+ } : {
2596
+ type: "credit",
2597
+ id: envelope.id,
2598
+ bytes: 0
2599
+ });
2233
2600
  let accepted = false;
2234
2601
  if (onPort) try {
2235
2602
  accepted = onPort(handle.port, envelope.meta) !== false;
@@ -2244,57 +2611,215 @@ function multiplexPort(port, options) {
2244
2611
  id: envelope.id,
2245
2612
  reason: "rejected"
2246
2613
  });
2614
+ return;
2615
+ }
2616
+ return;
2617
+ }
2618
+ const entry = open.get(envelope.id);
2619
+ if (envelope.type === "credit") {
2620
+ const c = entry?.credit;
2621
+ if (!c || !(envelope.bytes >= 0)) return;
2622
+ if (!c.enforced && c.opener) {
2623
+ if (c.exact && envelope.exact !== true) {
2624
+ c.exact = false;
2625
+ c.outstanding = 0;
2626
+ }
2627
+ post({
2628
+ type: "credit",
2629
+ id: envelope.id,
2630
+ bytes: 0
2631
+ });
2247
2632
  }
2633
+ c.enforced = true;
2634
+ c.outstanding = Math.max(0, c.outstanding - envelope.bytes);
2635
+ flush(c);
2248
2636
  return;
2249
2637
  }
2250
- const handle = open.get(envelope.id);
2251
2638
  if (envelope.type === "message") {
2252
- if (!handle) return;
2253
- handle.deliver(envelope.payload);
2639
+ const sizeOf = (c) => (c.exact ? byteLengthOf(event.data) : void 0) ?? estimateMessageSize(envelope.payload);
2640
+ if (!entry) return;
2641
+ touch(envelope.id);
2642
+ entry.handle.deliver(envelope.payload, entry.credit ? sizeOf(entry.credit) : void 0);
2643
+ return;
2644
+ }
2645
+ if (envelope.type === "frame") {
2646
+ if (!entry?.credit?.exact || !codec.decode || !(envelope.data instanceof Uint8Array)) return;
2647
+ touch(envelope.id);
2648
+ const c = entry.credit;
2649
+ const owed = c.unconsumed + c.toGrant;
2650
+ const partial = entry.partial ?? {
2651
+ chunks: [],
2652
+ bytes: 0,
2653
+ owed,
2654
+ charged: 0,
2655
+ ...owed > 0 ? { room: c.window - owed } : {}
2656
+ };
2657
+ entry.partial = partial;
2658
+ const size = envelope.data.byteLength;
2659
+ const charged = partial.room == null ? Math.max(0, partial.bytes + size - c.window) : 0;
2660
+ 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;
2661
+ if (overrun) {
2662
+ freePartial(entry);
2663
+ dropOutgoing(envelope.id);
2664
+ open.delete(envelope.id);
2665
+ releaseWaiters(c);
2666
+ entry.handle.markClosed(/* @__PURE__ */ new RangeError(`webrun-rpc: a framed message passed ${overrun}`));
2667
+ post({
2668
+ type: "close",
2669
+ id: envelope.id,
2670
+ reason: "max-reassembled-bytes"
2671
+ });
2672
+ return;
2673
+ }
2674
+ partial.bytes += size;
2675
+ reassembling += charged - partial.charged;
2676
+ partial.charged = charged;
2677
+ partial.chunks.push(envelope.data);
2678
+ if (!envelope.last) return;
2679
+ freePartial(entry);
2680
+ const whole = new Uint8Array(partial.bytes);
2681
+ let at = 0;
2682
+ for (const chunk of partial.chunks) {
2683
+ whole.set(chunk, at);
2684
+ at += chunk.byteLength;
2685
+ }
2686
+ const inner = codec.decode(whole);
2687
+ if (inner?.type !== "message" || inner.id !== envelope.id) return;
2688
+ entry.handle.deliver(inner.payload, whole.byteLength);
2254
2689
  return;
2255
2690
  }
2256
- if (!handle) return;
2691
+ dropOutgoing(envelope.id);
2692
+ if (!entry) return;
2693
+ freePartial(entry);
2257
2694
  open.delete(envelope.id);
2258
- handle.markClosed(/* @__PURE__ */ new Error("webrun-rpc: the peer closed the port"));
2695
+ releaseWaiters(entry.credit);
2696
+ entry.handle.markClosed(/* @__PURE__ */ new Error("webrun-rpc: the peer closed the port"));
2259
2697
  };
2260
2698
  const listener = (event) => {
2261
2699
  const envelope = codec.read(event);
2262
- if (envelope) handleEnvelope(envelope);
2700
+ if (envelope) handleEnvelope(envelope, event);
2701
+ };
2702
+ let peerKeepsAlive = false;
2703
+ let heardSincePing = true;
2704
+ let unanswered = 0;
2705
+ let pinger;
2706
+ const tick = () => {
2707
+ if (!keepalive) return;
2708
+ const backlogged = (port.bufferedAmount ?? 0) > 0;
2709
+ if (heardSincePing) unanswered = 0;
2710
+ else if (peerKeepsAlive && !backlogged) unanswered++;
2711
+ if (unanswered >= misses) {
2712
+ close(/* @__PURE__ */ new Error(`webrun-rpc: the peer did not answer ${misses} keepalive pings, ${keepalive.intervalMs} ms apart`)).catch(() => {});
2713
+ return;
2714
+ }
2715
+ heardSincePing = false;
2716
+ post({
2717
+ type: "ping",
2718
+ id: KEEPALIVE_ID
2719
+ });
2720
+ };
2721
+ const close = async (reason) => {
2722
+ if (muxClosed) return;
2723
+ clearInterval(pinger);
2724
+ muxClosed = true;
2725
+ signal?.removeEventListener("abort", onTransportAbort);
2726
+ port.removeEventListener("message", listener);
2727
+ const closedNow = /* @__PURE__ */ new Set();
2728
+ for (const [id, { handle, credit }] of [...open]) {
2729
+ closedNow.add(id);
2730
+ codecPost({
2731
+ type: "close",
2732
+ id
2733
+ });
2734
+ open.delete(id);
2735
+ releaseWaiters(credit);
2736
+ handle.markClosed(reason ?? /* @__PURE__ */ new Error("webrun-rpc: the multiplexer was closed"));
2737
+ }
2738
+ for (const id of outgoing.keys()) if (!closedNow.has(id)) codecPost({
2739
+ type: "close",
2740
+ id
2741
+ });
2742
+ outgoing.clear();
2743
+ ring.length = 0;
2744
+ await port.close?.();
2745
+ };
2746
+ const onTransportAbort = () => {
2747
+ close(signal?.reason).catch(() => {});
2263
2748
  };
2264
2749
  port.addEventListener("message", listener);
2265
2750
  port.start?.();
2751
+ if (keepalive) {
2752
+ pinger = setInterval(tick, keepalive.intervalMs);
2753
+ pinger.unref?.();
2754
+ }
2755
+ if (signal?.aborted) close(signal.reason).catch(() => {});
2756
+ else signal?.addEventListener("abort", onTransportAbort, { once: true });
2266
2757
  return {
2267
2758
  maxMessageSize,
2268
- async openPort(meta) {
2759
+ async openPort(meta, { signal: portSignal } = {}) {
2269
2760
  if (muxClosed) throw new Error("webrun-rpc: the multiplexer is closed");
2270
- if (open.size >= maxPorts) throw new RangeError(`webrun-rpc: maxPorts (${maxPorts}) reached`);
2761
+ if (portSignal?.aborted) throw portSignal.reason;
2762
+ if (open.size >= maxPorts) throw new RangeError(`webrun-rpc: maxPorts (${maxPorts}) reached; open ports by kind: ${describeKinds(open.values())}`);
2271
2763
  while (open.has(nextId)) nextId += 2;
2272
2764
  const id = nextId;
2273
2765
  nextId += 2;
2274
- const handle = attach(id);
2275
- post({
2766
+ const handle = attach(id, meta, window, true, exactCodec);
2767
+ post(window ? exactCodec ? {
2768
+ type: "open",
2769
+ id,
2770
+ meta,
2771
+ window,
2772
+ exact: true
2773
+ } : {
2774
+ type: "open",
2775
+ id,
2776
+ meta,
2777
+ window
2778
+ } : {
2276
2779
  type: "open",
2277
2780
  id,
2278
2781
  meta
2279
2782
  });
2783
+ if (portSignal) {
2784
+ const record = open.get(id)?.handle;
2785
+ const onAbort = () => {
2786
+ if (record && open.get(id)?.handle === record) record.close(portSignal.reason);
2787
+ };
2788
+ portSignal.addEventListener("abort", onAbort, { once: true });
2789
+ getPortCloseSignal(handle.port)?.addEventListener("abort", () => portSignal.removeEventListener("abort", onAbort), { once: true });
2790
+ }
2280
2791
  return handle.port;
2281
2792
  },
2282
- async close() {
2283
- if (muxClosed) return;
2284
- for (const [id, handle] of [...open]) {
2285
- post({
2286
- type: "close",
2287
- id
2288
- });
2289
- open.delete(id);
2290
- handle.markClosed(/* @__PURE__ */ new Error(MUX_CLOSED_MESSAGE));
2291
- }
2292
- muxClosed = true;
2293
- port.removeEventListener("message", listener);
2294
- await port.close?.();
2793
+ close,
2794
+ ports() {
2795
+ return [...open.values()].map(({ handle: _, credit, ...info }) => credit ? {
2796
+ ...info,
2797
+ ...credit.enforced ? { window: credit.window } : {},
2798
+ queued: credit.queued,
2799
+ outstanding: credit.outstanding,
2800
+ unconsumed: credit.unconsumed
2801
+ } : info);
2295
2802
  }
2296
2803
  };
2297
2804
  }
2805
+ /** The byte length of a received message's data, or `undefined` if it is not bytes. */
2806
+ function byteLengthOf(data) {
2807
+ if (data instanceof ArrayBuffer || ArrayBuffer.isView(data)) return data.byteLength;
2808
+ }
2809
+ /**
2810
+ * `"http: 1000, ws: 24"`: the three commonest `meta.kind`s among `entries`, so
2811
+ * a full table names who filled it. A port without a kind counts as `"?"`.
2812
+ */
2813
+ function describeKinds(entries) {
2814
+ const counts = /* @__PURE__ */ new Map();
2815
+ for (const { meta } of entries) {
2816
+ const kind = meta?.kind;
2817
+ const name = typeof kind === "string" ? kind : "?";
2818
+ counts.set(name, (counts.get(name) ?? 0) + 1);
2819
+ }
2820
+ if (!counts.size) return "none";
2821
+ return [...counts].sort((x, y) => y[1] - x[1]).slice(0, 3).map(([name, n]) => `${name}: ${n}`).join(", ");
2822
+ }
2298
2823
  /**
2299
2824
  * Announced with every port this pair opens. Layer 1 never reads `meta` — it is
2300
2825
  * here so a peer running something else over the same multiplexer can tell a
@@ -2356,7 +2881,7 @@ function isEnvelope(value) {
2356
2881
  const candidate = value;
2357
2882
  if (typeof candidate.id !== "number") return false;
2358
2883
  if (!Number.isInteger(candidate.id) || candidate.id < 0) return false;
2359
- return candidate.type === "open" || candidate.type === "message" || candidate.type === "close";
2884
+ return candidate.type === "open" || candidate.type === "message" || candidate.type === "close" || candidate.type === "credit" || candidate.type === "frame" || candidate.type === "ping" || candidate.type === "pong";
2360
2885
  }
2361
2886
  /**
2362
2887
  * For ports whose messages are structured values — a real `MessagePort`, a
@@ -2494,7 +3019,7 @@ function trackedLink(send, release) {
2494
3019
  finish();
2495
3020
  return response;
2496
3021
  }
2497
- return new Response(guardBody(response.body, lost, () => lostReason, finish), {
3022
+ return new Response(guardBody(response.body, lost, () => lostReason, finish, request.signal), {
2498
3023
  status: response.status,
2499
3024
  statusText: response.statusText,
2500
3025
  headers: response.headers
@@ -2513,33 +3038,65 @@ function trackedLink(send, release) {
2513
3038
  return asyncDisposable(link, () => link.close());
2514
3039
  }
2515
3040
  /**
2516
- * A response body that reports its end (done, cancelled or failed) and fails
2517
- * with the link's reason when the link is lost mid-stream.
3041
+ * A response body that reports its end (done, cancelled, failed or aborted) and
3042
+ * fails with the link's reason when the link is lost mid-stream. An abort of the
3043
+ * request's signal ends it at once, read or not: the exchange is over (the layer
3044
+ * below has given its port back), so the request leaves `inflight` then.
2518
3045
  */
2519
- function guardBody(body, lost, reason, finish) {
3046
+ function guardBody(body, lost, reason, finish, signal) {
2520
3047
  const reader = body.getReader();
3048
+ let controller;
3049
+ let ended = false;
3050
+ const end = () => {
3051
+ ended = true;
3052
+ signal?.removeEventListener("abort", onAbort);
3053
+ finish();
3054
+ };
3055
+ const onAbort = () => {
3056
+ if (ended) return;
3057
+ end();
3058
+ reader.cancel().catch(() => {});
3059
+ try {
3060
+ controller.error(abortError(signal));
3061
+ } catch {}
3062
+ };
2521
3063
  return new ReadableStream({
3064
+ start(c) {
3065
+ controller = c;
3066
+ if (signal?.aborted) onAbort();
3067
+ else signal?.addEventListener("abort", onAbort, { once: true });
3068
+ },
2522
3069
  async pull(controller) {
2523
3070
  try {
2524
3071
  const { done, value } = await Promise.race([reader.read(), lost]);
3072
+ if (ended) return;
2525
3073
  if (done) {
2526
- finish();
3074
+ end();
2527
3075
  controller.close();
2528
3076
  return;
2529
3077
  }
2530
3078
  controller.enqueue(value);
2531
3079
  } catch (error) {
2532
- finish();
3080
+ if (ended) return;
3081
+ end();
2533
3082
  reader.cancel().catch(() => {});
2534
3083
  controller.error(reason() ?? error);
2535
3084
  }
2536
3085
  },
2537
3086
  async cancel(why) {
2538
- finish();
3087
+ end();
2539
3088
  await reader.cancel(why).catch(() => {});
2540
3089
  }
2541
3090
  });
2542
3091
  }
3092
+ /** The signal's reason as an `Error` (`AbortError` when it gave none). */
3093
+ function abortError(signal) {
3094
+ const why = signal.reason;
3095
+ if (why instanceof Error) return why;
3096
+ const err = new Error(why === void 0 ? "Aborted" : String(why));
3097
+ err.name = "AbortError";
3098
+ return err;
3099
+ }
2543
3100
  //#endregion
2544
3101
  //#region src/sw/protocol.ts
2545
3102
  /** Page → ServiceWorker: open a WebSocket to a handler. */