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