@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/worker.js CHANGED
@@ -1411,7 +1411,7 @@ async function fetchOverDuplex(call, request, options = {}) {
1411
1411
  else await release?.();
1412
1412
  return new Response(null, responseInit);
1413
1413
  }
1414
- return new Response(asyncIterableToReadable(withAbort(respBody, request.signal)), responseInit);
1414
+ return new Response(responseBody(respBody, request.signal, release), responseInit);
1415
1415
  }
1416
1416
  /**
1417
1417
  * Wrap a `(Request) => Promise<Response>` handler as a `Duplex` so it can be
@@ -1542,15 +1542,78 @@ function trackingEnd(source) {
1542
1542
  settle
1543
1543
  };
1544
1544
  }
1545
- async function* withAbort(iter, signal) {
1546
- if (!signal) {
1547
- yield* iter;
1548
- return;
1549
- }
1550
- for await (const chunk of iter) {
1551
- if (signal.aborted) throw abortReason$1(signal);
1552
- yield chunk;
1553
- }
1545
+ /**
1546
+ * How long a cancelled body waits for the read in flight before it abandons the call anyway.
1547
+ * The read's answer tells a body that has ended (the call is finished: an upload the handler
1548
+ * reads after answering goes on) from one that has not; a peer that sends nothing more never
1549
+ * answers it. A body whose end comes later than this after the cancel is abandoned too, so a
1550
+ * handler still streaming its response and still reading the upload then loses the upload.
1551
+ */
1552
+ const CANCEL_GRACE_MS = 1e3;
1553
+ /**
1554
+ * The response body as a stream that the request's signal can end at any time.
1555
+ *
1556
+ * An abort errors the stream with the abort's reason at once — a read waiting
1557
+ * for a peer that sends nothing more rejects then, not when a next chunk
1558
+ * arrives, which may be never — and abandons the call (`release`), so on a
1559
+ * multiplexed transport its port is given back whether or not anyone still
1560
+ * reads the body. The listener is removed once the body ends, however it ends:
1561
+ * read to its end, cancelled, failed or aborted. A body nobody reads or cancels
1562
+ * keeps it until the signal aborts (and keeps the call open until then anyway).
1563
+ * A cancel abandons the call too — unless the read in flight comes back with the
1564
+ * body's end, which finishes it instead (see `cancel`).
1565
+ */
1566
+ function responseBody(body, signal, release) {
1567
+ const it = body[Symbol.asyncIterator]();
1568
+ let controller;
1569
+ let ended = false;
1570
+ let reading = false;
1571
+ const end = () => {
1572
+ ended = true;
1573
+ signal?.removeEventListener("abort", onAbort);
1574
+ };
1575
+ const onAbort = () => {
1576
+ if (ended) return;
1577
+ end();
1578
+ try {
1579
+ controller.error(abortReason$1(signal));
1580
+ } catch {}
1581
+ release?.();
1582
+ };
1583
+ return new ReadableStream({
1584
+ start(c) {
1585
+ controller = c;
1586
+ if (signal?.aborted) onAbort();
1587
+ else signal?.addEventListener("abort", onAbort, { once: true });
1588
+ },
1589
+ async pull(c) {
1590
+ try {
1591
+ reading = true;
1592
+ const { done, value } = await it.next().finally(() => {
1593
+ reading = false;
1594
+ });
1595
+ if (ended) return;
1596
+ if (done) {
1597
+ end();
1598
+ c.close();
1599
+ return;
1600
+ }
1601
+ c.enqueue(value);
1602
+ } catch (err) {
1603
+ if (ended) return;
1604
+ end();
1605
+ c.error(err);
1606
+ }
1607
+ },
1608
+ cancel() {
1609
+ end();
1610
+ const returned = Promise.resolve(it.return?.()).catch(() => {});
1611
+ if (!reading) return;
1612
+ const grace = setTimeout(() => void release?.(), CANCEL_GRACE_MS);
1613
+ grace.unref?.();
1614
+ returned.finally(() => clearTimeout(grace));
1615
+ }
1616
+ });
1554
1617
  }
1555
1618
  //#endregion
1556
1619
  //#region ../webrun-rpc/dist/index.js
@@ -1675,8 +1738,11 @@ function listenPort(port, handler, { channelName = "", log = () => {} } = {}) {
1675
1738
  error
1676
1739
  });
1677
1740
  };
1678
- port.addEventListener("message", onMessage);
1679
- return () => port.removeEventListener("message", onMessage);
1741
+ const listener = (event) => {
1742
+ onMessage(event);
1743
+ };
1744
+ port.addEventListener("message", listener);
1745
+ return () => port.removeEventListener("message", listener);
1680
1746
  }
1681
1747
  /**
1682
1748
  * Wraps an async iterable so that an `AbortSignal` firing causes the wrapper
@@ -2081,6 +2147,33 @@ async function sendChunks(port, channelName, output, { maxMessageSize, log }, si
2081
2147
  }
2082
2148
  }
2083
2149
  /**
2150
+ * Runs `fn` on a later task, never a microtask: a producer looping on
2151
+ * `whenPortReady` must let the transport's own events run. `setImmediate`
2152
+ * where it exists; elsewhere a `MessageChannel`, because nested `setTimeout`s
2153
+ * are clamped to 4 ms in browsers.
2154
+ */
2155
+ const setImmediateFn = globalThis.setImmediate;
2156
+ /** @internal */
2157
+ const nextTask = setImmediateFn ? (fn) => {
2158
+ setImmediateFn(fn);
2159
+ } : (() => {
2160
+ let channel;
2161
+ const queue = [];
2162
+ return (fn) => {
2163
+ if (!channel) {
2164
+ channel = new MessageChannel();
2165
+ channel.port1.onmessage = () => queue.shift()?.();
2166
+ }
2167
+ queue.push(fn);
2168
+ channel.port2.postMessage(0);
2169
+ };
2170
+ })();
2171
+ const states = /* @__PURE__ */ new WeakMap();
2172
+ /** @internal Registered by the multiplexer for each port with a window. */
2173
+ function setPortSendState(port, state) {
2174
+ states.set(port, state);
2175
+ }
2176
+ /**
2084
2177
  * One virtual port.
2085
2178
  *
2086
2179
  * `deliver` and `markClosed` are deliberately not on `port`: the consumer holds
@@ -2095,6 +2188,14 @@ function newVirtualPort(send, requestClose) {
2095
2188
  if (closing.signal.aborted) return;
2096
2189
  closing.abort(reason ?? /* @__PURE__ */ new Error("webrun-rpc: the port was closed"));
2097
2190
  };
2191
+ const close = (reason) => {
2192
+ if (closed) return;
2193
+ closed = true;
2194
+ const notify = requestClose;
2195
+ listeners.clear();
2196
+ notify(reason);
2197
+ abort(reason);
2198
+ };
2098
2199
  const port = {
2099
2200
  addEventListener(_type, listener) {
2100
2201
  listeners.add(listener);
@@ -2106,24 +2207,28 @@ function newVirtualPort(send, requestClose) {
2106
2207
  if (closed) return;
2107
2208
  send(message, transfer);
2108
2209
  },
2109
- close() {
2110
- if (closed) return;
2111
- closed = true;
2112
- const notify = requestClose;
2113
- listeners.clear();
2114
- notify();
2115
- abort();
2116
- }
2210
+ close
2117
2211
  };
2118
2212
  setPortCloseSignal(port, closing.signal);
2213
+ const dispose = Symbol.dispose;
2214
+ if (dispose) Object.defineProperty(port, dispose, {
2215
+ value: () => close(),
2216
+ configurable: true
2217
+ });
2119
2218
  return {
2120
2219
  port,
2121
2220
  deliver(payload) {
2122
2221
  if (closed) return;
2123
2222
  const event = new MessageEvent("message", { data: payload });
2223
+ let pending;
2124
2224
  for (const listener of [...listeners]) try {
2125
- listener(event);
2225
+ const result = listener(event);
2226
+ if (result && typeof result.then === "function") {
2227
+ pending ??= [];
2228
+ pending.push(result);
2229
+ }
2126
2230
  } catch {}
2231
+ return pending ? Promise.allSettled(pending) : void 0;
2127
2232
  },
2128
2233
  markClosed(reason) {
2129
2234
  closed = true;
@@ -2132,53 +2237,309 @@ function newVirtualPort(send, requestClose) {
2132
2237
  },
2133
2238
  isClosed() {
2134
2239
  return closed;
2240
+ },
2241
+ hasListeners() {
2242
+ return listeners.size > 0;
2135
2243
  }
2136
2244
  };
2137
2245
  }
2138
2246
  /**
2139
- * The message of the error that fails the calls still in flight on a
2140
- * multiplexer when it is closed (`PortMux.close()`).
2247
+ * The id of keepalive's `ping` / `pong`: one no port ever takes. An older mux
2248
+ * whose codec passes unknown envelopes through reads them as a close of that
2249
+ * id, which must therefore not be a real port (0 is the initiator's first).
2141
2250
  */
2142
- const MUX_CLOSED_MESSAGE = "webrun-rpc: the multiplexer was closed";
2251
+ const KEEPALIVE_ID = Number.MAX_SAFE_INTEGER;
2252
+ /** The longest delay a timer holds; past it `setInterval` fires at once. */
2253
+ const MAX_TIMER_MS = 2 ** 31 - 1;
2143
2254
  /** Ceiling on concurrently open virtual ports. Bounds the id table only. */
2144
2255
  const DEFAULT_MAX_PORTS = 1024;
2145
2256
  /**
2257
+ * What a message costs a port's window when the codec cannot say
2258
+ * (`PortCodec.encode`): its bytes and string lengths, found in its values up
2259
+ * to 4 levels deep (at most 256 values), and at least 64. Both ends compute it
2260
+ * from the same structured value, so they agree.
2261
+ */
2262
+ function estimateMessageSize(payload) {
2263
+ let budget = 256;
2264
+ const walk = (v, depth) => {
2265
+ if (budget-- <= 0) return 0;
2266
+ if (typeof v === "string") return v.length;
2267
+ if (v instanceof ArrayBuffer) return v.byteLength;
2268
+ if (ArrayBuffer.isView(v)) return v.byteLength;
2269
+ if (v === null || typeof v !== "object" || depth >= 4) return 8;
2270
+ let n = 16;
2271
+ if (Array.isArray(v)) for (const x of v) n += walk(x, depth + 1);
2272
+ else for (const k in v) n += k.length + walk(v[k], depth + 1);
2273
+ return n;
2274
+ };
2275
+ return Math.max(64, walk(payload, 0));
2276
+ }
2277
+ /**
2146
2278
  * The default `PortMux`: emulates multiplexing over a single port.
2147
2279
  *
2148
2280
  * A transport that already multiplexes natively supplies its own `PortMux`
2149
2281
  * instead — this implementation is for transports that offer one pipe.
2150
2282
  */
2151
2283
  function multiplexPort(port, options) {
2152
- const { codec, onPort, side = "initiator", maxPorts = DEFAULT_MAX_PORTS, maxMessageSize } = options;
2284
+ const { codec, onPort, side = "initiator", maxPorts = DEFAULT_MAX_PORTS, maxMessageSize, signal, window, frameSize, keepalive, onLeak } = options;
2285
+ const misses = keepalive?.misses ?? 3;
2286
+ 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`);
2287
+ if (frameSize != null && !(Number.isSafeInteger(frameSize) && frameSize > 0)) throw new RangeError(`webrun-rpc: frameSize must be a positive integer, got ${frameSize}`);
2153
2288
  const open = /* @__PURE__ */ new Map();
2289
+ const leaks = onLeak ? new FinalizationRegistry(({ id, record, release }) => {
2290
+ const entry = open.get(id);
2291
+ if (entry?.handle !== record) return;
2292
+ const { handle: _, credit: __, partial: ___, ...info } = entry;
2293
+ if (!record.hasListeners()) {
2294
+ release("leaked");
2295
+ record.markClosed(/* @__PURE__ */ new Error("webrun-rpc: the port was dropped without close()"));
2296
+ }
2297
+ try {
2298
+ onLeak?.(info);
2299
+ } catch {}
2300
+ }) : void 0;
2301
+ const touch = (id) => {
2302
+ const entry = open.get(id);
2303
+ if (entry) entry.lastActivity = Date.now();
2304
+ };
2154
2305
  let nextId = side === "initiator" ? 0 : 1;
2155
2306
  let muxClosed = false;
2307
+ /** This end counts encoded bytes and reassembles frames. */
2308
+ const exactCodec = codec.encode != null && codec.decode != null;
2156
2309
  const post = (envelope, transfer) => {
2157
2310
  if (muxClosed) return;
2311
+ codecPost(envelope, transfer);
2312
+ };
2313
+ /** Like `post`, also while closing: the mux's own close notices. */
2314
+ const codecPost = (envelope, transfer) => {
2158
2315
  try {
2159
2316
  codec.post(port, envelope, transfer);
2160
2317
  } catch {}
2161
2318
  };
2162
- const attach = (id) => {
2163
- let self;
2164
- self = newVirtualPort((payload, transfer) => post({
2165
- type: "message",
2166
- id,
2167
- payload
2168
- }, transfer), (reason) => {
2319
+ const outgoing = /* @__PURE__ */ new Map();
2320
+ const ring = [];
2321
+ let pumping = false;
2322
+ const pump = async () => {
2323
+ pumping = true;
2324
+ try {
2325
+ while (ring.length && !muxClosed) {
2326
+ if (port.whenDrained) await port.whenDrained();
2327
+ if (muxClosed) break;
2328
+ const id = ring.shift();
2329
+ const queue = outgoing.get(id);
2330
+ const item = queue?.shift();
2331
+ if (queue?.length) ring.push(id);
2332
+ else outgoing.delete(id);
2333
+ item?.();
2334
+ }
2335
+ } catch (err) {
2336
+ close(err).catch(() => {});
2337
+ } finally {
2338
+ pumping = false;
2339
+ }
2340
+ };
2341
+ const sendOnPort = (id, item) => {
2342
+ if (frameSize == null) {
2343
+ item();
2344
+ return;
2345
+ }
2346
+ let queue = outgoing.get(id);
2347
+ if (!queue) {
2348
+ queue = [];
2349
+ outgoing.set(id, queue);
2350
+ ring.push(id);
2351
+ }
2352
+ queue.push(item);
2353
+ if (!pumping) pump();
2354
+ };
2355
+ const dropOutgoing = (id) => {
2356
+ if (!outgoing.delete(id)) return;
2357
+ const at = ring.indexOf(id);
2358
+ if (at >= 0) ring.splice(at, 1);
2359
+ };
2360
+ const postBytes = (bytes) => {
2361
+ if (muxClosed) return;
2362
+ try {
2363
+ port.postMessage(bytes);
2364
+ } catch {}
2365
+ };
2366
+ const releaseWaiters = (c) => {
2367
+ for (const w of c?.waiters.splice(0) ?? []) w.resolve();
2368
+ };
2369
+ const flush = (c) => {
2370
+ while (c.queue.length) {
2371
+ const next = c.queue[0];
2372
+ if (c.enforced && c.outstanding > 0 && c.outstanding + next.size > c.window) break;
2373
+ c.queue.shift();
2374
+ c.queued -= next.size;
2375
+ c.outstanding += next.size;
2376
+ next.send();
2377
+ }
2378
+ for (const w of [...c.waiters]) if (c.queued <= w.below) {
2379
+ c.waiters.splice(c.waiters.indexOf(w), 1);
2380
+ w.resolve();
2381
+ }
2382
+ };
2383
+ const grant = (id, c, bytes) => {
2384
+ c.unconsumed -= bytes;
2385
+ c.toGrant += bytes;
2386
+ const send = () => {
2387
+ c.grantScheduled = false;
2388
+ if (!c.toGrant || !open.has(id)) return;
2169
2389
  post({
2390
+ type: "credit",
2391
+ id,
2392
+ bytes: c.toGrant
2393
+ });
2394
+ c.toGrant = 0;
2395
+ };
2396
+ if (c.toGrant >= c.window / 4) {
2397
+ send();
2398
+ return;
2399
+ }
2400
+ if (c.grantScheduled) return;
2401
+ c.grantScheduled = true;
2402
+ setTimeout(send, 0);
2403
+ };
2404
+ const attach = (id, meta, portWindow, opener, exact) => {
2405
+ const c = typeof portWindow === "number" && Number.isSafeInteger(portWindow) && portWindow > 0 ? {
2406
+ window: portWindow,
2407
+ enforced: false,
2408
+ opener,
2409
+ exact,
2410
+ outstanding: 0,
2411
+ queue: [],
2412
+ queued: 0,
2413
+ unconsumed: 0,
2414
+ waiters: [],
2415
+ toGrant: 0,
2416
+ grantScheduled: false
2417
+ } : void 0;
2418
+ const self = newVirtualPort((payload, transfer) => {
2419
+ touch(id);
2420
+ const envelope = {
2421
+ type: "message",
2422
+ id,
2423
+ payload
2424
+ };
2425
+ if (!c) {
2426
+ post(envelope, transfer);
2427
+ return;
2428
+ }
2429
+ const bytes = c.exact ? codec.encode?.(envelope) : void 0;
2430
+ const size = bytes?.byteLength ?? estimateMessageSize(payload);
2431
+ const waits = frameSize != null || c.queue.length > 0 || c.enforced && c.outstanding > 0 && c.outstanding + size > c.window;
2432
+ const held = !bytes && waits ? {
2433
+ ...envelope,
2434
+ payload: structuredClone(payload, { transfer })
2435
+ } : void 0;
2436
+ const send = () => {
2437
+ if (!bytes) {
2438
+ sendOnPort(id, held ? () => post(held) : () => post(envelope, transfer));
2439
+ return;
2440
+ }
2441
+ if (frameSize != null && c.enforced && c.exact && bytes.byteLength > frameSize) {
2442
+ for (let at = 0; at < bytes.byteLength; at += frameSize) {
2443
+ const data = bytes.subarray(at, at + frameSize);
2444
+ const frame = at + frameSize >= bytes.byteLength ? {
2445
+ type: "frame",
2446
+ id,
2447
+ data,
2448
+ last: true
2449
+ } : {
2450
+ type: "frame",
2451
+ id,
2452
+ data
2453
+ };
2454
+ sendOnPort(id, () => post(frame));
2455
+ }
2456
+ return;
2457
+ }
2458
+ sendOnPort(id, () => postBytes(bytes));
2459
+ };
2460
+ c.queue.push({
2461
+ send,
2462
+ size
2463
+ });
2464
+ c.queued += size;
2465
+ flush(c);
2466
+ }, (reason) => {
2467
+ release(reason);
2468
+ record.markClosed(reason);
2469
+ });
2470
+ function release(reason) {
2471
+ open.delete(id);
2472
+ leaks?.unregister(record);
2473
+ if (c?.queue.length) {
2474
+ for (const item of c.queue.splice(0)) item.send();
2475
+ c.queued = 0;
2476
+ }
2477
+ sendOnPort(id, () => post({
2170
2478
  type: "close",
2171
2479
  id,
2172
2480
  reason
2481
+ }));
2482
+ releaseWaiters(c);
2483
+ }
2484
+ const { markClosed, isClosed, hasListeners } = self;
2485
+ if (c) {
2486
+ const deliver = self.deliver;
2487
+ self.deliver = (payload, size = estimateMessageSize(payload)) => {
2488
+ c.unconsumed += size;
2489
+ const done = deliver(payload);
2490
+ if (done) done.then(() => grant(id, c, size));
2491
+ else grant(id, c, size);
2492
+ return done;
2493
+ };
2494
+ setPortSendState(self.port, {
2495
+ bufferedAmount: () => c.queued,
2496
+ ready: (below = 0) => new Promise((resolve) => {
2497
+ if (c.queued <= below || isClosed()) nextTask(resolve);
2498
+ else c.waiters.push({
2499
+ below,
2500
+ resolve: () => nextTask(resolve)
2501
+ });
2502
+ })
2173
2503
  });
2174
- open.delete(id);
2175
- self.markClosed();
2504
+ }
2505
+ const record = {
2506
+ deliver: self.deliver,
2507
+ markClosed,
2508
+ isClosed,
2509
+ hasListeners,
2510
+ close: (reason) => {
2511
+ if (isClosed()) return;
2512
+ release(reason);
2513
+ markClosed(reason);
2514
+ }
2515
+ };
2516
+ leaks?.register(self.port, {
2517
+ id,
2518
+ record,
2519
+ release
2520
+ }, record);
2521
+ const now = Date.now();
2522
+ open.set(id, {
2523
+ id,
2524
+ meta,
2525
+ openedAt: now,
2526
+ lastActivity: now,
2527
+ handle: record,
2528
+ credit: c
2176
2529
  });
2177
- open.set(id, self);
2178
2530
  return self;
2179
2531
  };
2180
- const handleEnvelope = (envelope) => {
2532
+ const handleEnvelope = (envelope, event) => {
2181
2533
  if (muxClosed) return;
2534
+ heardSincePing = true;
2535
+ if (envelope.type === "ping" || envelope.type === "pong") {
2536
+ peerKeepsAlive = true;
2537
+ if (envelope.type === "ping") post({
2538
+ type: "pong",
2539
+ id: KEEPALIVE_ID
2540
+ });
2541
+ return;
2542
+ }
2182
2543
  if (envelope.type === "open") {
2183
2544
  if (open.has(envelope.id)) return;
2184
2545
  if (open.size >= maxPorts) {
@@ -2189,7 +2550,18 @@ function multiplexPort(port, options) {
2189
2550
  });
2190
2551
  return;
2191
2552
  }
2192
- const handle = attach(envelope.id);
2553
+ const handle = attach(envelope.id, envelope.meta, envelope.window, false, envelope.exact === true && exactCodec);
2554
+ const credit = open.get(envelope.id)?.credit;
2555
+ if (credit) post(credit.exact ? {
2556
+ type: "credit",
2557
+ id: envelope.id,
2558
+ bytes: 0,
2559
+ exact: true
2560
+ } : {
2561
+ type: "credit",
2562
+ id: envelope.id,
2563
+ bytes: 0
2564
+ });
2193
2565
  let accepted = false;
2194
2566
  if (onPort) try {
2195
2567
  accepted = onPort(handle.port, envelope.meta) !== false;
@@ -2204,57 +2576,191 @@ function multiplexPort(port, options) {
2204
2576
  id: envelope.id,
2205
2577
  reason: "rejected"
2206
2578
  });
2579
+ return;
2207
2580
  }
2208
2581
  return;
2209
2582
  }
2210
- const handle = open.get(envelope.id);
2583
+ const entry = open.get(envelope.id);
2584
+ if (envelope.type === "credit") {
2585
+ const c = entry?.credit;
2586
+ if (!c || !(envelope.bytes >= 0)) return;
2587
+ if (!c.enforced && c.opener) {
2588
+ if (c.exact && envelope.exact !== true) {
2589
+ c.exact = false;
2590
+ c.outstanding = 0;
2591
+ }
2592
+ post({
2593
+ type: "credit",
2594
+ id: envelope.id,
2595
+ bytes: 0
2596
+ });
2597
+ }
2598
+ c.enforced = true;
2599
+ c.outstanding = Math.max(0, c.outstanding - envelope.bytes);
2600
+ flush(c);
2601
+ return;
2602
+ }
2211
2603
  if (envelope.type === "message") {
2212
- if (!handle) return;
2213
- handle.deliver(envelope.payload);
2604
+ const sizeOf = (c) => (c.exact ? byteLengthOf(event.data) : void 0) ?? estimateMessageSize(envelope.payload);
2605
+ if (!entry) return;
2606
+ touch(envelope.id);
2607
+ entry.handle.deliver(envelope.payload, entry.credit ? sizeOf(entry.credit) : void 0);
2608
+ return;
2609
+ }
2610
+ if (envelope.type === "frame") {
2611
+ if (!entry?.credit?.exact || !codec.decode || !(envelope.data instanceof Uint8Array)) return;
2612
+ touch(envelope.id);
2613
+ const partial = entry.partial ?? {
2614
+ chunks: [],
2615
+ bytes: 0
2616
+ };
2617
+ entry.partial = partial;
2618
+ partial.chunks.push(envelope.data);
2619
+ partial.bytes += envelope.data.byteLength;
2620
+ if (!envelope.last) return;
2621
+ entry.partial = void 0;
2622
+ const whole = new Uint8Array(partial.bytes);
2623
+ let at = 0;
2624
+ for (const chunk of partial.chunks) {
2625
+ whole.set(chunk, at);
2626
+ at += chunk.byteLength;
2627
+ }
2628
+ const inner = codec.decode(whole);
2629
+ if (inner?.type !== "message" || inner.id !== envelope.id) return;
2630
+ entry.handle.deliver(inner.payload, whole.byteLength);
2214
2631
  return;
2215
2632
  }
2216
- if (!handle) return;
2633
+ dropOutgoing(envelope.id);
2634
+ if (!entry) return;
2217
2635
  open.delete(envelope.id);
2218
- handle.markClosed(/* @__PURE__ */ new Error("webrun-rpc: the peer closed the port"));
2636
+ releaseWaiters(entry.credit);
2637
+ entry.handle.markClosed(/* @__PURE__ */ new Error("webrun-rpc: the peer closed the port"));
2219
2638
  };
2220
2639
  const listener = (event) => {
2221
2640
  const envelope = codec.read(event);
2222
- if (envelope) handleEnvelope(envelope);
2641
+ if (envelope) handleEnvelope(envelope, event);
2642
+ };
2643
+ let peerKeepsAlive = false;
2644
+ let heardSincePing = true;
2645
+ let unanswered = 0;
2646
+ let pinger;
2647
+ const tick = () => {
2648
+ if (!keepalive) return;
2649
+ const backlogged = (port.bufferedAmount ?? 0) > 0;
2650
+ if (heardSincePing) unanswered = 0;
2651
+ else if (peerKeepsAlive && !backlogged) unanswered++;
2652
+ if (unanswered >= misses) {
2653
+ close(/* @__PURE__ */ new Error(`webrun-rpc: the peer did not answer ${misses} keepalive pings, ${keepalive.intervalMs} ms apart`)).catch(() => {});
2654
+ return;
2655
+ }
2656
+ heardSincePing = false;
2657
+ post({
2658
+ type: "ping",
2659
+ id: KEEPALIVE_ID
2660
+ });
2661
+ };
2662
+ const close = async (reason) => {
2663
+ if (muxClosed) return;
2664
+ clearInterval(pinger);
2665
+ muxClosed = true;
2666
+ signal?.removeEventListener("abort", onTransportAbort);
2667
+ port.removeEventListener("message", listener);
2668
+ const closedNow = /* @__PURE__ */ new Set();
2669
+ for (const [id, { handle, credit }] of [...open]) {
2670
+ closedNow.add(id);
2671
+ codecPost({
2672
+ type: "close",
2673
+ id
2674
+ });
2675
+ open.delete(id);
2676
+ releaseWaiters(credit);
2677
+ handle.markClosed(reason ?? /* @__PURE__ */ new Error("webrun-rpc: the multiplexer was closed"));
2678
+ }
2679
+ for (const id of outgoing.keys()) if (!closedNow.has(id)) codecPost({
2680
+ type: "close",
2681
+ id
2682
+ });
2683
+ outgoing.clear();
2684
+ ring.length = 0;
2685
+ await port.close?.();
2686
+ };
2687
+ const onTransportAbort = () => {
2688
+ close(signal?.reason).catch(() => {});
2223
2689
  };
2224
2690
  port.addEventListener("message", listener);
2225
2691
  port.start?.();
2692
+ if (keepalive) {
2693
+ pinger = setInterval(tick, keepalive.intervalMs);
2694
+ pinger.unref?.();
2695
+ }
2696
+ if (signal?.aborted) close(signal.reason).catch(() => {});
2697
+ else signal?.addEventListener("abort", onTransportAbort, { once: true });
2226
2698
  return {
2227
2699
  maxMessageSize,
2228
- async openPort(meta) {
2700
+ async openPort(meta, { signal: portSignal } = {}) {
2229
2701
  if (muxClosed) throw new Error("webrun-rpc: the multiplexer is closed");
2230
- if (open.size >= maxPorts) throw new RangeError(`webrun-rpc: maxPorts (${maxPorts}) reached`);
2702
+ if (portSignal?.aborted) throw portSignal.reason;
2703
+ if (open.size >= maxPorts) throw new RangeError(`webrun-rpc: maxPorts (${maxPorts}) reached; open ports by kind: ${describeKinds(open.values())}`);
2231
2704
  while (open.has(nextId)) nextId += 2;
2232
2705
  const id = nextId;
2233
2706
  nextId += 2;
2234
- const handle = attach(id);
2235
- post({
2707
+ const handle = attach(id, meta, window, true, exactCodec);
2708
+ post(window ? exactCodec ? {
2709
+ type: "open",
2710
+ id,
2711
+ meta,
2712
+ window,
2713
+ exact: true
2714
+ } : {
2715
+ type: "open",
2716
+ id,
2717
+ meta,
2718
+ window
2719
+ } : {
2236
2720
  type: "open",
2237
2721
  id,
2238
2722
  meta
2239
2723
  });
2724
+ if (portSignal) {
2725
+ const record = open.get(id)?.handle;
2726
+ const onAbort = () => {
2727
+ if (record && open.get(id)?.handle === record) record.close(portSignal.reason);
2728
+ };
2729
+ portSignal.addEventListener("abort", onAbort, { once: true });
2730
+ getPortCloseSignal(handle.port)?.addEventListener("abort", () => portSignal.removeEventListener("abort", onAbort), { once: true });
2731
+ }
2240
2732
  return handle.port;
2241
2733
  },
2242
- async close() {
2243
- if (muxClosed) return;
2244
- for (const [id, handle] of [...open]) {
2245
- post({
2246
- type: "close",
2247
- id
2248
- });
2249
- open.delete(id);
2250
- handle.markClosed(/* @__PURE__ */ new Error(MUX_CLOSED_MESSAGE));
2251
- }
2252
- muxClosed = true;
2253
- port.removeEventListener("message", listener);
2254
- await port.close?.();
2734
+ close,
2735
+ ports() {
2736
+ return [...open.values()].map(({ handle: _, credit, ...info }) => credit ? {
2737
+ ...info,
2738
+ ...credit.enforced ? { window: credit.window } : {},
2739
+ queued: credit.queued,
2740
+ outstanding: credit.outstanding,
2741
+ unconsumed: credit.unconsumed
2742
+ } : info);
2255
2743
  }
2256
2744
  };
2257
2745
  }
2746
+ /** The byte length of a received message's data, or `undefined` if it is not bytes. */
2747
+ function byteLengthOf(data) {
2748
+ if (data instanceof ArrayBuffer || ArrayBuffer.isView(data)) return data.byteLength;
2749
+ }
2750
+ /**
2751
+ * `"http: 1000, ws: 24"`: the three commonest `meta.kind`s among `entries`, so
2752
+ * a full table names who filled it. A port without a kind counts as `"?"`.
2753
+ */
2754
+ function describeKinds(entries) {
2755
+ const counts = /* @__PURE__ */ new Map();
2756
+ for (const { meta } of entries) {
2757
+ const kind = meta?.kind;
2758
+ const name = typeof kind === "string" ? kind : "?";
2759
+ counts.set(name, (counts.get(name) ?? 0) + 1);
2760
+ }
2761
+ if (!counts.size) return "none";
2762
+ return [...counts].sort((x, y) => y[1] - x[1]).slice(0, 3).map(([name, n]) => `${name}: ${n}`).join(", ");
2763
+ }
2258
2764
  /**
2259
2765
  * Announced with every port this pair opens. Layer 1 never reads `meta` — it is
2260
2766
  * here so a peer running something else over the same multiplexer can tell a
@@ -2361,7 +2867,7 @@ function isEnvelope(value) {
2361
2867
  const candidate = value;
2362
2868
  if (typeof candidate.id !== "number") return false;
2363
2869
  if (!Number.isInteger(candidate.id) || candidate.id < 0) return false;
2364
- return candidate.type === "open" || candidate.type === "message" || candidate.type === "close";
2870
+ return candidate.type === "open" || candidate.type === "message" || candidate.type === "close" || candidate.type === "credit" || candidate.type === "frame" || candidate.type === "ping" || candidate.type === "pong";
2365
2871
  }
2366
2872
  /**
2367
2873
  * For ports whose messages are structured values — a real `MessagePort`, a
@@ -2523,7 +3029,7 @@ function trackedLink(send, release) {
2523
3029
  finish();
2524
3030
  return response;
2525
3031
  }
2526
- return new Response(guardBody(response.body, lost, () => lostReason, finish), {
3032
+ return new Response(guardBody(response.body, lost, () => lostReason, finish, request.signal), {
2527
3033
  status: response.status,
2528
3034
  statusText: response.statusText,
2529
3035
  headers: response.headers
@@ -2542,33 +3048,65 @@ function trackedLink(send, release) {
2542
3048
  return asyncDisposable(link, () => link.close());
2543
3049
  }
2544
3050
  /**
2545
- * A response body that reports its end (done, cancelled or failed) and fails
2546
- * with the link's reason when the link is lost mid-stream.
3051
+ * A response body that reports its end (done, cancelled, failed or aborted) and
3052
+ * fails with the link's reason when the link is lost mid-stream. An abort of the
3053
+ * request's signal ends it at once, read or not: the exchange is over (the layer
3054
+ * below has given its port back), so the request leaves `inflight` then.
2547
3055
  */
2548
- function guardBody(body, lost, reason, finish) {
3056
+ function guardBody(body, lost, reason, finish, signal) {
2549
3057
  const reader = body.getReader();
3058
+ let controller;
3059
+ let ended = false;
3060
+ const end = () => {
3061
+ ended = true;
3062
+ signal?.removeEventListener("abort", onAbort);
3063
+ finish();
3064
+ };
3065
+ const onAbort = () => {
3066
+ if (ended) return;
3067
+ end();
3068
+ reader.cancel().catch(() => {});
3069
+ try {
3070
+ controller.error(abortError(signal));
3071
+ } catch {}
3072
+ };
2550
3073
  return new ReadableStream({
3074
+ start(c) {
3075
+ controller = c;
3076
+ if (signal?.aborted) onAbort();
3077
+ else signal?.addEventListener("abort", onAbort, { once: true });
3078
+ },
2551
3079
  async pull(controller) {
2552
3080
  try {
2553
3081
  const { done, value } = await Promise.race([reader.read(), lost]);
3082
+ if (ended) return;
2554
3083
  if (done) {
2555
- finish();
3084
+ end();
2556
3085
  controller.close();
2557
3086
  return;
2558
3087
  }
2559
3088
  controller.enqueue(value);
2560
3089
  } catch (error) {
2561
- finish();
3090
+ if (ended) return;
3091
+ end();
2562
3092
  reader.cancel().catch(() => {});
2563
3093
  controller.error(reason() ?? error);
2564
3094
  }
2565
3095
  },
2566
3096
  async cancel(why) {
2567
- finish();
3097
+ end();
2568
3098
  await reader.cancel(why).catch(() => {});
2569
3099
  }
2570
3100
  });
2571
3101
  }
3102
+ /** The signal's reason as an `Error` (`AbortError` when it gave none). */
3103
+ function abortError(signal) {
3104
+ const why = signal.reason;
3105
+ if (why instanceof Error) return why;
3106
+ const err = new Error(why === void 0 ? "Aborted" : String(why));
3107
+ err.name = "AbortError";
3108
+ return err;
3109
+ }
2572
3110
  //#endregion
2573
3111
  //#region src/link/locks.ts
2574
3112
  function lockManager() {
@@ -3715,12 +4253,18 @@ function serveHttpInWorker(handler, options = {}) {
3715
4253
  const ownerLock = newLockName("webrun-http-owner");
3716
4254
  const held = holdLock(ownerLock);
3717
4255
  const controls = /* @__PURE__ */ new Set();
4256
+ const seen = /* @__PURE__ */ new Map();
3718
4257
  const tellPages = (message) => {
3719
4258
  for (const control of controls) control.postMessage(message);
3720
4259
  };
3721
4260
  const endpoint = new HandlerEndpoint(handler, {
3722
4261
  ...options,
3723
- requestRelink: () => tellPages({ type: "relink" }),
4262
+ requestRelink: () => {
4263
+ for (const control of controls) control.postMessage({
4264
+ type: "relink",
4265
+ seen: seen.get(control) ?? 0
4266
+ });
4267
+ },
3724
4268
  keepAlive: () => tellPages({ type: "keepalive" })
3725
4269
  });
3726
4270
  const onMessage = (event) => {
@@ -3732,6 +4276,7 @@ function serveHttpInWorker(handler, options = {}) {
3732
4276
  control.onmessage = (message) => {
3733
4277
  const msg = message.data;
3734
4278
  if (msg?.type === "link") {
4279
+ if (msg.seq !== void 0) seen.set(control, msg.seq);
3735
4280
  const [down, ctl, up] = message.ports;
3736
4281
  if (down) endpoint.link(msg.key, {
3737
4282
  down,
@@ -3772,6 +4317,7 @@ function serveHttpInWorker(handler, options = {}) {
3772
4317
  await endpoint.close();
3773
4318
  for (const control of controls) control.close();
3774
4319
  controls.clear();
4320
+ seen.clear();
3775
4321
  (await held)();
3776
4322
  }
3777
4323
  };