@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.
@@ -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
@@ -2121,6 +2167,33 @@ async function sendChunks(port, channelName, output, { maxMessageSize, log }, si
2121
2167
  }
2122
2168
  }
2123
2169
  /**
2170
+ * Runs `fn` on a later task, never a microtask: a producer looping on
2171
+ * `whenPortReady` must let the transport's own events run. `setImmediate`
2172
+ * where it exists; elsewhere a `MessageChannel`, because nested `setTimeout`s
2173
+ * are clamped to 4 ms in browsers.
2174
+ */
2175
+ const setImmediateFn = globalThis.setImmediate;
2176
+ /** @internal */
2177
+ const nextTask = setImmediateFn ? (fn) => {
2178
+ setImmediateFn(fn);
2179
+ } : (() => {
2180
+ let channel;
2181
+ const queue = [];
2182
+ return (fn) => {
2183
+ if (!channel) {
2184
+ channel = new MessageChannel();
2185
+ channel.port1.onmessage = () => queue.shift()?.();
2186
+ }
2187
+ queue.push(fn);
2188
+ channel.port2.postMessage(0);
2189
+ };
2190
+ })();
2191
+ const states = /* @__PURE__ */ new WeakMap();
2192
+ /** @internal Registered by the multiplexer for each port with a window. */
2193
+ function setPortSendState(port, state) {
2194
+ states.set(port, state);
2195
+ }
2196
+ /**
2124
2197
  * One virtual port.
2125
2198
  *
2126
2199
  * `deliver` and `markClosed` are deliberately not on `port`: the consumer holds
@@ -2135,6 +2208,14 @@ function newVirtualPort(send, requestClose) {
2135
2208
  if (closing.signal.aborted) return;
2136
2209
  closing.abort(reason ?? /* @__PURE__ */ new Error("webrun-rpc: the port was closed"));
2137
2210
  };
2211
+ const close = (reason) => {
2212
+ if (closed) return;
2213
+ closed = true;
2214
+ const notify = requestClose;
2215
+ listeners.clear();
2216
+ notify(reason);
2217
+ abort(reason);
2218
+ };
2138
2219
  const port = {
2139
2220
  addEventListener(_type, listener) {
2140
2221
  listeners.add(listener);
@@ -2146,24 +2227,28 @@ function newVirtualPort(send, requestClose) {
2146
2227
  if (closed) return;
2147
2228
  send(message, transfer);
2148
2229
  },
2149
- close() {
2150
- if (closed) return;
2151
- closed = true;
2152
- const notify = requestClose;
2153
- listeners.clear();
2154
- notify();
2155
- abort();
2156
- }
2230
+ close
2157
2231
  };
2158
2232
  setPortCloseSignal(port, closing.signal);
2233
+ const dispose = Symbol.dispose;
2234
+ if (dispose) Object.defineProperty(port, dispose, {
2235
+ value: () => close(),
2236
+ configurable: true
2237
+ });
2159
2238
  return {
2160
2239
  port,
2161
2240
  deliver(payload) {
2162
2241
  if (closed) return;
2163
2242
  const event = new MessageEvent("message", { data: payload });
2243
+ let pending;
2164
2244
  for (const listener of [...listeners]) try {
2165
- listener(event);
2245
+ const result = listener(event);
2246
+ if (result && typeof result.then === "function") {
2247
+ pending ??= [];
2248
+ pending.push(result);
2249
+ }
2166
2250
  } catch {}
2251
+ return pending ? Promise.allSettled(pending) : void 0;
2167
2252
  },
2168
2253
  markClosed(reason) {
2169
2254
  closed = true;
@@ -2172,53 +2257,309 @@ function newVirtualPort(send, requestClose) {
2172
2257
  },
2173
2258
  isClosed() {
2174
2259
  return closed;
2260
+ },
2261
+ hasListeners() {
2262
+ return listeners.size > 0;
2175
2263
  }
2176
2264
  };
2177
2265
  }
2178
2266
  /**
2179
- * The message of the error that fails the calls still in flight on a
2180
- * multiplexer when it is closed (`PortMux.close()`).
2267
+ * The id of keepalive's `ping` / `pong`: one no port ever takes. An older mux
2268
+ * whose codec passes unknown envelopes through reads them as a close of that
2269
+ * id, which must therefore not be a real port (0 is the initiator's first).
2181
2270
  */
2182
- const MUX_CLOSED_MESSAGE = "webrun-rpc: the multiplexer was closed";
2271
+ const KEEPALIVE_ID = Number.MAX_SAFE_INTEGER;
2272
+ /** The longest delay a timer holds; past it `setInterval` fires at once. */
2273
+ const MAX_TIMER_MS = 2 ** 31 - 1;
2183
2274
  /** Ceiling on concurrently open virtual ports. Bounds the id table only. */
2184
2275
  const DEFAULT_MAX_PORTS = 1024;
2185
2276
  /**
2277
+ * What a message costs a port's window when the codec cannot say
2278
+ * (`PortCodec.encode`): its bytes and string lengths, found in its values up
2279
+ * to 4 levels deep (at most 256 values), and at least 64. Both ends compute it
2280
+ * from the same structured value, so they agree.
2281
+ */
2282
+ function estimateMessageSize(payload) {
2283
+ let budget = 256;
2284
+ const walk = (v, depth) => {
2285
+ if (budget-- <= 0) return 0;
2286
+ if (typeof v === "string") return v.length;
2287
+ if (v instanceof ArrayBuffer) return v.byteLength;
2288
+ if (ArrayBuffer.isView(v)) return v.byteLength;
2289
+ if (v === null || typeof v !== "object" || depth >= 4) return 8;
2290
+ let n = 16;
2291
+ if (Array.isArray(v)) for (const x of v) n += walk(x, depth + 1);
2292
+ else for (const k in v) n += k.length + walk(v[k], depth + 1);
2293
+ return n;
2294
+ };
2295
+ return Math.max(64, walk(payload, 0));
2296
+ }
2297
+ /**
2186
2298
  * The default `PortMux`: emulates multiplexing over a single port.
2187
2299
  *
2188
2300
  * A transport that already multiplexes natively supplies its own `PortMux`
2189
2301
  * instead — this implementation is for transports that offer one pipe.
2190
2302
  */
2191
2303
  function multiplexPort(port, options) {
2192
- const { codec, onPort, side = "initiator", maxPorts = DEFAULT_MAX_PORTS, maxMessageSize } = options;
2304
+ const { codec, onPort, side = "initiator", maxPorts = DEFAULT_MAX_PORTS, maxMessageSize, signal, window, frameSize, keepalive, onLeak } = options;
2305
+ const misses = keepalive?.misses ?? 3;
2306
+ 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`);
2307
+ if (frameSize != null && !(Number.isSafeInteger(frameSize) && frameSize > 0)) throw new RangeError(`webrun-rpc: frameSize must be a positive integer, got ${frameSize}`);
2193
2308
  const open = /* @__PURE__ */ new Map();
2309
+ const leaks = onLeak ? new FinalizationRegistry(({ id, record, release }) => {
2310
+ const entry = open.get(id);
2311
+ if (entry?.handle !== record) return;
2312
+ const { handle: _, credit: __, partial: ___, ...info } = entry;
2313
+ if (!record.hasListeners()) {
2314
+ release("leaked");
2315
+ record.markClosed(/* @__PURE__ */ new Error("webrun-rpc: the port was dropped without close()"));
2316
+ }
2317
+ try {
2318
+ onLeak?.(info);
2319
+ } catch {}
2320
+ }) : void 0;
2321
+ const touch = (id) => {
2322
+ const entry = open.get(id);
2323
+ if (entry) entry.lastActivity = Date.now();
2324
+ };
2194
2325
  let nextId = side === "initiator" ? 0 : 1;
2195
2326
  let muxClosed = false;
2327
+ /** This end counts encoded bytes and reassembles frames. */
2328
+ const exactCodec = codec.encode != null && codec.decode != null;
2196
2329
  const post = (envelope, transfer) => {
2197
2330
  if (muxClosed) return;
2331
+ codecPost(envelope, transfer);
2332
+ };
2333
+ /** Like `post`, also while closing: the mux's own close notices. */
2334
+ const codecPost = (envelope, transfer) => {
2198
2335
  try {
2199
2336
  codec.post(port, envelope, transfer);
2200
2337
  } catch {}
2201
2338
  };
2202
- const attach = (id) => {
2203
- let self;
2204
- self = newVirtualPort((payload, transfer) => post({
2205
- type: "message",
2206
- id,
2207
- payload
2208
- }, transfer), (reason) => {
2339
+ const outgoing = /* @__PURE__ */ new Map();
2340
+ const ring = [];
2341
+ let pumping = false;
2342
+ const pump = async () => {
2343
+ pumping = true;
2344
+ try {
2345
+ while (ring.length && !muxClosed) {
2346
+ if (port.whenDrained) await port.whenDrained();
2347
+ if (muxClosed) break;
2348
+ const id = ring.shift();
2349
+ const queue = outgoing.get(id);
2350
+ const item = queue?.shift();
2351
+ if (queue?.length) ring.push(id);
2352
+ else outgoing.delete(id);
2353
+ item?.();
2354
+ }
2355
+ } catch (err) {
2356
+ close(err).catch(() => {});
2357
+ } finally {
2358
+ pumping = false;
2359
+ }
2360
+ };
2361
+ const sendOnPort = (id, item) => {
2362
+ if (frameSize == null) {
2363
+ item();
2364
+ return;
2365
+ }
2366
+ let queue = outgoing.get(id);
2367
+ if (!queue) {
2368
+ queue = [];
2369
+ outgoing.set(id, queue);
2370
+ ring.push(id);
2371
+ }
2372
+ queue.push(item);
2373
+ if (!pumping) pump();
2374
+ };
2375
+ const dropOutgoing = (id) => {
2376
+ if (!outgoing.delete(id)) return;
2377
+ const at = ring.indexOf(id);
2378
+ if (at >= 0) ring.splice(at, 1);
2379
+ };
2380
+ const postBytes = (bytes) => {
2381
+ if (muxClosed) return;
2382
+ try {
2383
+ port.postMessage(bytes);
2384
+ } catch {}
2385
+ };
2386
+ const releaseWaiters = (c) => {
2387
+ for (const w of c?.waiters.splice(0) ?? []) w.resolve();
2388
+ };
2389
+ const flush = (c) => {
2390
+ while (c.queue.length) {
2391
+ const next = c.queue[0];
2392
+ if (c.enforced && c.outstanding > 0 && c.outstanding + next.size > c.window) break;
2393
+ c.queue.shift();
2394
+ c.queued -= next.size;
2395
+ c.outstanding += next.size;
2396
+ next.send();
2397
+ }
2398
+ for (const w of [...c.waiters]) if (c.queued <= w.below) {
2399
+ c.waiters.splice(c.waiters.indexOf(w), 1);
2400
+ w.resolve();
2401
+ }
2402
+ };
2403
+ const grant = (id, c, bytes) => {
2404
+ c.unconsumed -= bytes;
2405
+ c.toGrant += bytes;
2406
+ const send = () => {
2407
+ c.grantScheduled = false;
2408
+ if (!c.toGrant || !open.has(id)) return;
2209
2409
  post({
2410
+ type: "credit",
2411
+ id,
2412
+ bytes: c.toGrant
2413
+ });
2414
+ c.toGrant = 0;
2415
+ };
2416
+ if (c.toGrant >= c.window / 4) {
2417
+ send();
2418
+ return;
2419
+ }
2420
+ if (c.grantScheduled) return;
2421
+ c.grantScheduled = true;
2422
+ setTimeout(send, 0);
2423
+ };
2424
+ const attach = (id, meta, portWindow, opener, exact) => {
2425
+ const c = typeof portWindow === "number" && Number.isSafeInteger(portWindow) && portWindow > 0 ? {
2426
+ window: portWindow,
2427
+ enforced: false,
2428
+ opener,
2429
+ exact,
2430
+ outstanding: 0,
2431
+ queue: [],
2432
+ queued: 0,
2433
+ unconsumed: 0,
2434
+ waiters: [],
2435
+ toGrant: 0,
2436
+ grantScheduled: false
2437
+ } : void 0;
2438
+ const self = newVirtualPort((payload, transfer) => {
2439
+ touch(id);
2440
+ const envelope = {
2441
+ type: "message",
2442
+ id,
2443
+ payload
2444
+ };
2445
+ if (!c) {
2446
+ post(envelope, transfer);
2447
+ return;
2448
+ }
2449
+ const bytes = c.exact ? codec.encode?.(envelope) : void 0;
2450
+ const size = bytes?.byteLength ?? estimateMessageSize(payload);
2451
+ const waits = frameSize != null || c.queue.length > 0 || c.enforced && c.outstanding > 0 && c.outstanding + size > c.window;
2452
+ const held = !bytes && waits ? {
2453
+ ...envelope,
2454
+ payload: structuredClone(payload, { transfer })
2455
+ } : void 0;
2456
+ const send = () => {
2457
+ if (!bytes) {
2458
+ sendOnPort(id, held ? () => post(held) : () => post(envelope, transfer));
2459
+ return;
2460
+ }
2461
+ if (frameSize != null && c.enforced && c.exact && bytes.byteLength > frameSize) {
2462
+ for (let at = 0; at < bytes.byteLength; at += frameSize) {
2463
+ const data = bytes.subarray(at, at + frameSize);
2464
+ const frame = at + frameSize >= bytes.byteLength ? {
2465
+ type: "frame",
2466
+ id,
2467
+ data,
2468
+ last: true
2469
+ } : {
2470
+ type: "frame",
2471
+ id,
2472
+ data
2473
+ };
2474
+ sendOnPort(id, () => post(frame));
2475
+ }
2476
+ return;
2477
+ }
2478
+ sendOnPort(id, () => postBytes(bytes));
2479
+ };
2480
+ c.queue.push({
2481
+ send,
2482
+ size
2483
+ });
2484
+ c.queued += size;
2485
+ flush(c);
2486
+ }, (reason) => {
2487
+ release(reason);
2488
+ record.markClosed(reason);
2489
+ });
2490
+ function release(reason) {
2491
+ open.delete(id);
2492
+ leaks?.unregister(record);
2493
+ if (c?.queue.length) {
2494
+ for (const item of c.queue.splice(0)) item.send();
2495
+ c.queued = 0;
2496
+ }
2497
+ sendOnPort(id, () => post({
2210
2498
  type: "close",
2211
2499
  id,
2212
2500
  reason
2501
+ }));
2502
+ releaseWaiters(c);
2503
+ }
2504
+ const { markClosed, isClosed, hasListeners } = self;
2505
+ if (c) {
2506
+ const deliver = self.deliver;
2507
+ self.deliver = (payload, size = estimateMessageSize(payload)) => {
2508
+ c.unconsumed += size;
2509
+ const done = deliver(payload);
2510
+ if (done) done.then(() => grant(id, c, size));
2511
+ else grant(id, c, size);
2512
+ return done;
2513
+ };
2514
+ setPortSendState(self.port, {
2515
+ bufferedAmount: () => c.queued,
2516
+ ready: (below = 0) => new Promise((resolve) => {
2517
+ if (c.queued <= below || isClosed()) nextTask(resolve);
2518
+ else c.waiters.push({
2519
+ below,
2520
+ resolve: () => nextTask(resolve)
2521
+ });
2522
+ })
2213
2523
  });
2214
- open.delete(id);
2215
- self.markClosed();
2524
+ }
2525
+ const record = {
2526
+ deliver: self.deliver,
2527
+ markClosed,
2528
+ isClosed,
2529
+ hasListeners,
2530
+ close: (reason) => {
2531
+ if (isClosed()) return;
2532
+ release(reason);
2533
+ markClosed(reason);
2534
+ }
2535
+ };
2536
+ leaks?.register(self.port, {
2537
+ id,
2538
+ record,
2539
+ release
2540
+ }, record);
2541
+ const now = Date.now();
2542
+ open.set(id, {
2543
+ id,
2544
+ meta,
2545
+ openedAt: now,
2546
+ lastActivity: now,
2547
+ handle: record,
2548
+ credit: c
2216
2549
  });
2217
- open.set(id, self);
2218
2550
  return self;
2219
2551
  };
2220
- const handleEnvelope = (envelope) => {
2552
+ const handleEnvelope = (envelope, event) => {
2221
2553
  if (muxClosed) return;
2554
+ heardSincePing = true;
2555
+ if (envelope.type === "ping" || envelope.type === "pong") {
2556
+ peerKeepsAlive = true;
2557
+ if (envelope.type === "ping") post({
2558
+ type: "pong",
2559
+ id: KEEPALIVE_ID
2560
+ });
2561
+ return;
2562
+ }
2222
2563
  if (envelope.type === "open") {
2223
2564
  if (open.has(envelope.id)) return;
2224
2565
  if (open.size >= maxPorts) {
@@ -2229,7 +2570,18 @@ function multiplexPort(port, options) {
2229
2570
  });
2230
2571
  return;
2231
2572
  }
2232
- const handle = attach(envelope.id);
2573
+ const handle = attach(envelope.id, envelope.meta, envelope.window, false, envelope.exact === true && exactCodec);
2574
+ const credit = open.get(envelope.id)?.credit;
2575
+ if (credit) post(credit.exact ? {
2576
+ type: "credit",
2577
+ id: envelope.id,
2578
+ bytes: 0,
2579
+ exact: true
2580
+ } : {
2581
+ type: "credit",
2582
+ id: envelope.id,
2583
+ bytes: 0
2584
+ });
2233
2585
  let accepted = false;
2234
2586
  if (onPort) try {
2235
2587
  accepted = onPort(handle.port, envelope.meta) !== false;
@@ -2244,57 +2596,191 @@ function multiplexPort(port, options) {
2244
2596
  id: envelope.id,
2245
2597
  reason: "rejected"
2246
2598
  });
2599
+ return;
2247
2600
  }
2248
2601
  return;
2249
2602
  }
2250
- const handle = open.get(envelope.id);
2603
+ const entry = open.get(envelope.id);
2604
+ if (envelope.type === "credit") {
2605
+ const c = entry?.credit;
2606
+ if (!c || !(envelope.bytes >= 0)) return;
2607
+ if (!c.enforced && c.opener) {
2608
+ if (c.exact && envelope.exact !== true) {
2609
+ c.exact = false;
2610
+ c.outstanding = 0;
2611
+ }
2612
+ post({
2613
+ type: "credit",
2614
+ id: envelope.id,
2615
+ bytes: 0
2616
+ });
2617
+ }
2618
+ c.enforced = true;
2619
+ c.outstanding = Math.max(0, c.outstanding - envelope.bytes);
2620
+ flush(c);
2621
+ return;
2622
+ }
2251
2623
  if (envelope.type === "message") {
2252
- if (!handle) return;
2253
- handle.deliver(envelope.payload);
2624
+ const sizeOf = (c) => (c.exact ? byteLengthOf(event.data) : void 0) ?? estimateMessageSize(envelope.payload);
2625
+ if (!entry) return;
2626
+ touch(envelope.id);
2627
+ entry.handle.deliver(envelope.payload, entry.credit ? sizeOf(entry.credit) : void 0);
2254
2628
  return;
2255
2629
  }
2256
- if (!handle) return;
2630
+ if (envelope.type === "frame") {
2631
+ if (!entry?.credit?.exact || !codec.decode || !(envelope.data instanceof Uint8Array)) return;
2632
+ touch(envelope.id);
2633
+ const partial = entry.partial ?? {
2634
+ chunks: [],
2635
+ bytes: 0
2636
+ };
2637
+ entry.partial = partial;
2638
+ partial.chunks.push(envelope.data);
2639
+ partial.bytes += envelope.data.byteLength;
2640
+ if (!envelope.last) return;
2641
+ entry.partial = void 0;
2642
+ const whole = new Uint8Array(partial.bytes);
2643
+ let at = 0;
2644
+ for (const chunk of partial.chunks) {
2645
+ whole.set(chunk, at);
2646
+ at += chunk.byteLength;
2647
+ }
2648
+ const inner = codec.decode(whole);
2649
+ if (inner?.type !== "message" || inner.id !== envelope.id) return;
2650
+ entry.handle.deliver(inner.payload, whole.byteLength);
2651
+ return;
2652
+ }
2653
+ dropOutgoing(envelope.id);
2654
+ if (!entry) return;
2257
2655
  open.delete(envelope.id);
2258
- handle.markClosed(/* @__PURE__ */ new Error("webrun-rpc: the peer closed the port"));
2656
+ releaseWaiters(entry.credit);
2657
+ entry.handle.markClosed(/* @__PURE__ */ new Error("webrun-rpc: the peer closed the port"));
2259
2658
  };
2260
2659
  const listener = (event) => {
2261
2660
  const envelope = codec.read(event);
2262
- if (envelope) handleEnvelope(envelope);
2661
+ if (envelope) handleEnvelope(envelope, event);
2662
+ };
2663
+ let peerKeepsAlive = false;
2664
+ let heardSincePing = true;
2665
+ let unanswered = 0;
2666
+ let pinger;
2667
+ const tick = () => {
2668
+ if (!keepalive) return;
2669
+ const backlogged = (port.bufferedAmount ?? 0) > 0;
2670
+ if (heardSincePing) unanswered = 0;
2671
+ else if (peerKeepsAlive && !backlogged) unanswered++;
2672
+ if (unanswered >= misses) {
2673
+ close(/* @__PURE__ */ new Error(`webrun-rpc: the peer did not answer ${misses} keepalive pings, ${keepalive.intervalMs} ms apart`)).catch(() => {});
2674
+ return;
2675
+ }
2676
+ heardSincePing = false;
2677
+ post({
2678
+ type: "ping",
2679
+ id: KEEPALIVE_ID
2680
+ });
2681
+ };
2682
+ const close = async (reason) => {
2683
+ if (muxClosed) return;
2684
+ clearInterval(pinger);
2685
+ muxClosed = true;
2686
+ signal?.removeEventListener("abort", onTransportAbort);
2687
+ port.removeEventListener("message", listener);
2688
+ const closedNow = /* @__PURE__ */ new Set();
2689
+ for (const [id, { handle, credit }] of [...open]) {
2690
+ closedNow.add(id);
2691
+ codecPost({
2692
+ type: "close",
2693
+ id
2694
+ });
2695
+ open.delete(id);
2696
+ releaseWaiters(credit);
2697
+ handle.markClosed(reason ?? /* @__PURE__ */ new Error("webrun-rpc: the multiplexer was closed"));
2698
+ }
2699
+ for (const id of outgoing.keys()) if (!closedNow.has(id)) codecPost({
2700
+ type: "close",
2701
+ id
2702
+ });
2703
+ outgoing.clear();
2704
+ ring.length = 0;
2705
+ await port.close?.();
2706
+ };
2707
+ const onTransportAbort = () => {
2708
+ close(signal?.reason).catch(() => {});
2263
2709
  };
2264
2710
  port.addEventListener("message", listener);
2265
2711
  port.start?.();
2712
+ if (keepalive) {
2713
+ pinger = setInterval(tick, keepalive.intervalMs);
2714
+ pinger.unref?.();
2715
+ }
2716
+ if (signal?.aborted) close(signal.reason).catch(() => {});
2717
+ else signal?.addEventListener("abort", onTransportAbort, { once: true });
2266
2718
  return {
2267
2719
  maxMessageSize,
2268
- async openPort(meta) {
2720
+ async openPort(meta, { signal: portSignal } = {}) {
2269
2721
  if (muxClosed) throw new Error("webrun-rpc: the multiplexer is closed");
2270
- if (open.size >= maxPorts) throw new RangeError(`webrun-rpc: maxPorts (${maxPorts}) reached`);
2722
+ if (portSignal?.aborted) throw portSignal.reason;
2723
+ if (open.size >= maxPorts) throw new RangeError(`webrun-rpc: maxPorts (${maxPorts}) reached; open ports by kind: ${describeKinds(open.values())}`);
2271
2724
  while (open.has(nextId)) nextId += 2;
2272
2725
  const id = nextId;
2273
2726
  nextId += 2;
2274
- const handle = attach(id);
2275
- post({
2727
+ const handle = attach(id, meta, window, true, exactCodec);
2728
+ post(window ? exactCodec ? {
2729
+ type: "open",
2730
+ id,
2731
+ meta,
2732
+ window,
2733
+ exact: true
2734
+ } : {
2735
+ type: "open",
2736
+ id,
2737
+ meta,
2738
+ window
2739
+ } : {
2276
2740
  type: "open",
2277
2741
  id,
2278
2742
  meta
2279
2743
  });
2744
+ if (portSignal) {
2745
+ const record = open.get(id)?.handle;
2746
+ const onAbort = () => {
2747
+ if (record && open.get(id)?.handle === record) record.close(portSignal.reason);
2748
+ };
2749
+ portSignal.addEventListener("abort", onAbort, { once: true });
2750
+ getPortCloseSignal(handle.port)?.addEventListener("abort", () => portSignal.removeEventListener("abort", onAbort), { once: true });
2751
+ }
2280
2752
  return handle.port;
2281
2753
  },
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?.();
2754
+ close,
2755
+ ports() {
2756
+ return [...open.values()].map(({ handle: _, credit, ...info }) => credit ? {
2757
+ ...info,
2758
+ ...credit.enforced ? { window: credit.window } : {},
2759
+ queued: credit.queued,
2760
+ outstanding: credit.outstanding,
2761
+ unconsumed: credit.unconsumed
2762
+ } : info);
2295
2763
  }
2296
2764
  };
2297
2765
  }
2766
+ /** The byte length of a received message's data, or `undefined` if it is not bytes. */
2767
+ function byteLengthOf(data) {
2768
+ if (data instanceof ArrayBuffer || ArrayBuffer.isView(data)) return data.byteLength;
2769
+ }
2770
+ /**
2771
+ * `"http: 1000, ws: 24"`: the three commonest `meta.kind`s among `entries`, so
2772
+ * a full table names who filled it. A port without a kind counts as `"?"`.
2773
+ */
2774
+ function describeKinds(entries) {
2775
+ const counts = /* @__PURE__ */ new Map();
2776
+ for (const { meta } of entries) {
2777
+ const kind = meta?.kind;
2778
+ const name = typeof kind === "string" ? kind : "?";
2779
+ counts.set(name, (counts.get(name) ?? 0) + 1);
2780
+ }
2781
+ if (!counts.size) return "none";
2782
+ return [...counts].sort((x, y) => y[1] - x[1]).slice(0, 3).map(([name, n]) => `${name}: ${n}`).join(", ");
2783
+ }
2298
2784
  /**
2299
2785
  * Announced with every port this pair opens. Layer 1 never reads `meta` — it is
2300
2786
  * here so a peer running something else over the same multiplexer can tell a
@@ -2356,7 +2842,7 @@ function isEnvelope(value) {
2356
2842
  const candidate = value;
2357
2843
  if (typeof candidate.id !== "number") return false;
2358
2844
  if (!Number.isInteger(candidate.id) || candidate.id < 0) return false;
2359
- return candidate.type === "open" || candidate.type === "message" || candidate.type === "close";
2845
+ return candidate.type === "open" || candidate.type === "message" || candidate.type === "close" || candidate.type === "credit" || candidate.type === "frame" || candidate.type === "ping" || candidate.type === "pong";
2360
2846
  }
2361
2847
  /**
2362
2848
  * For ports whose messages are structured values — a real `MessagePort`, a
@@ -2494,7 +2980,7 @@ function trackedLink(send, release) {
2494
2980
  finish();
2495
2981
  return response;
2496
2982
  }
2497
- return new Response(guardBody(response.body, lost, () => lostReason, finish), {
2983
+ return new Response(guardBody(response.body, lost, () => lostReason, finish, request.signal), {
2498
2984
  status: response.status,
2499
2985
  statusText: response.statusText,
2500
2986
  headers: response.headers
@@ -2513,33 +2999,65 @@ function trackedLink(send, release) {
2513
2999
  return asyncDisposable(link, () => link.close());
2514
3000
  }
2515
3001
  /**
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.
3002
+ * A response body that reports its end (done, cancelled, failed or aborted) and
3003
+ * fails with the link's reason when the link is lost mid-stream. An abort of the
3004
+ * request's signal ends it at once, read or not: the exchange is over (the layer
3005
+ * below has given its port back), so the request leaves `inflight` then.
2518
3006
  */
2519
- function guardBody(body, lost, reason, finish) {
3007
+ function guardBody(body, lost, reason, finish, signal) {
2520
3008
  const reader = body.getReader();
3009
+ let controller;
3010
+ let ended = false;
3011
+ const end = () => {
3012
+ ended = true;
3013
+ signal?.removeEventListener("abort", onAbort);
3014
+ finish();
3015
+ };
3016
+ const onAbort = () => {
3017
+ if (ended) return;
3018
+ end();
3019
+ reader.cancel().catch(() => {});
3020
+ try {
3021
+ controller.error(abortError(signal));
3022
+ } catch {}
3023
+ };
2521
3024
  return new ReadableStream({
3025
+ start(c) {
3026
+ controller = c;
3027
+ if (signal?.aborted) onAbort();
3028
+ else signal?.addEventListener("abort", onAbort, { once: true });
3029
+ },
2522
3030
  async pull(controller) {
2523
3031
  try {
2524
3032
  const { done, value } = await Promise.race([reader.read(), lost]);
3033
+ if (ended) return;
2525
3034
  if (done) {
2526
- finish();
3035
+ end();
2527
3036
  controller.close();
2528
3037
  return;
2529
3038
  }
2530
3039
  controller.enqueue(value);
2531
3040
  } catch (error) {
2532
- finish();
3041
+ if (ended) return;
3042
+ end();
2533
3043
  reader.cancel().catch(() => {});
2534
3044
  controller.error(reason() ?? error);
2535
3045
  }
2536
3046
  },
2537
3047
  async cancel(why) {
2538
- finish();
3048
+ end();
2539
3049
  await reader.cancel(why).catch(() => {});
2540
3050
  }
2541
3051
  });
2542
3052
  }
3053
+ /** The signal's reason as an `Error` (`AbortError` when it gave none). */
3054
+ function abortError(signal) {
3055
+ const why = signal.reason;
3056
+ if (why instanceof Error) return why;
3057
+ const err = new Error(why === void 0 ? "Aborted" : String(why));
3058
+ err.name = "AbortError";
3059
+ return err;
3060
+ }
2543
3061
  //#endregion
2544
3062
  //#region src/sw/protocol.ts
2545
3063
  /** Page → ServiceWorker: open a WebSocket to a handler. */