@statewalker/webrun-http-browser 0.7.1 → 0.9.0

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