@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/worker.js CHANGED
@@ -1207,7 +1207,11 @@ async function drain(output) {
1207
1207
  */
1208
1208
  function httpServe(handler, options = {}) {
1209
1209
  const codec = options.codec ?? defaultCodec;
1210
- return async function* httpHandlerDuplex(input) {
1210
+ return (input) => {
1211
+ const abandoned = new AbortController();
1212
+ return abortingOnReturn(exchange(input, { signal: abandoned.signal }), (reason) => abandoned.abort(reason ?? /* @__PURE__ */ new Error("webrun-http-streams: the exchange was abandoned before it was over")));
1213
+ };
1214
+ async function* exchange(input, context) {
1211
1215
  let decoded;
1212
1216
  try {
1213
1217
  decoded = await codec.decodeRequest(input);
@@ -1219,7 +1223,7 @@ function httpServe(handler, options = {}) {
1219
1223
  const replyCodec = decoded.codec ?? codec;
1220
1224
  let result;
1221
1225
  try {
1222
- result = await handler(decoded.envelope, decoded.body);
1226
+ result = await handler(decoded.envelope, decoded.body, context);
1223
1227
  } catch (error) {
1224
1228
  yield* encodeErrorResponse(replyCodec, error, decoded.envelope.method);
1225
1229
  return;
@@ -1234,7 +1238,36 @@ function httpServe(handler, options = {}) {
1234
1238
  await result.release?.();
1235
1239
  } catch {}
1236
1240
  }
1241
+ }
1242
+ }
1243
+ /**
1244
+ * `gen`, except that `return()` and `throw()` from its consumer call `abandon`
1245
+ * at once — before they are queued behind a `next()` still parked inside the
1246
+ * generator (the handler's `await`) — unless the generator already finished.
1247
+ */
1248
+ function abortingOnReturn(gen, abandon) {
1249
+ let finished = false;
1250
+ const settle = (r) => {
1251
+ if (r.done) finished = true;
1252
+ return r;
1253
+ };
1254
+ const wrapped = {
1255
+ next: (...args) => gen.next(...args).then(settle),
1256
+ return(value) {
1257
+ if (!finished) abandon();
1258
+ finished = true;
1259
+ return gen.return(value);
1260
+ },
1261
+ throw(error) {
1262
+ if (!finished) abandon(error);
1263
+ finished = true;
1264
+ return gen.throw(error);
1265
+ },
1266
+ [Symbol.asyncIterator]() {
1267
+ return wrapped;
1268
+ }
1237
1269
  };
1270
+ return asyncDisposable$2(wrapped, () => wrapped.return(void 0));
1238
1271
  }
1239
1272
  /**
1240
1273
  * The one place that knows a runtime may not implement request body streams,
@@ -1411,17 +1444,18 @@ async function fetchOverDuplex(call, request, options = {}) {
1411
1444
  else await release?.();
1412
1445
  return new Response(null, responseInit);
1413
1446
  }
1414
- return new Response(asyncIterableToReadable(withAbort(respBody, request.signal)), responseInit);
1447
+ return new Response(responseBody(respBody, request.signal, release), responseInit);
1415
1448
  }
1416
1449
  /**
1417
1450
  * Wrap a `(Request) => Promise<Response>` handler as a `Duplex` so it can be
1418
1451
  * registered with any `webrun-streams-*` adapter's `serve`.
1419
1452
  */
1420
1453
  function serveFetchOverDuplex(handler, options = {}) {
1421
- return httpServe(async (env, body) => {
1454
+ return httpServe(async (env, body, exchange) => {
1422
1455
  const reqInit = {
1423
1456
  method: env.method,
1424
- headers: forwardableHeaders(env.headers)
1457
+ headers: forwardableHeaders(env.headers),
1458
+ signal: exchange?.signal
1425
1459
  };
1426
1460
  let upload;
1427
1461
  if (env.method !== "GET" && env.method !== "HEAD") {
@@ -1542,15 +1576,78 @@ function trackingEnd(source) {
1542
1576
  settle
1543
1577
  };
1544
1578
  }
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
- }
1579
+ /**
1580
+ * How long a cancelled body waits for the read in flight before it abandons the call anyway.
1581
+ * The read's answer tells a body that has ended (the call is finished: an upload the handler
1582
+ * reads after answering goes on) from one that has not; a peer that sends nothing more never
1583
+ * answers it. A body whose end comes later than this after the cancel is abandoned too, so a
1584
+ * handler still streaming its response and still reading the upload then loses the upload.
1585
+ */
1586
+ const CANCEL_GRACE_MS = 1e3;
1587
+ /**
1588
+ * The response body as a stream that the request's signal can end at any time.
1589
+ *
1590
+ * An abort errors the stream with the abort's reason at once — a read waiting
1591
+ * for a peer that sends nothing more rejects then, not when a next chunk
1592
+ * arrives, which may be never — and abandons the call (`release`), so on a
1593
+ * multiplexed transport its port is given back whether or not anyone still
1594
+ * reads the body. The listener is removed once the body ends, however it ends:
1595
+ * read to its end, cancelled, failed or aborted. A body nobody reads or cancels
1596
+ * keeps it until the signal aborts (and keeps the call open until then anyway).
1597
+ * A cancel abandons the call too — unless the read in flight comes back with the
1598
+ * body's end, which finishes it instead (see `cancel`).
1599
+ */
1600
+ function responseBody(body, signal, release) {
1601
+ const it = body[Symbol.asyncIterator]();
1602
+ let controller;
1603
+ let ended = false;
1604
+ let reading = false;
1605
+ const end = () => {
1606
+ ended = true;
1607
+ signal?.removeEventListener("abort", onAbort);
1608
+ };
1609
+ const onAbort = () => {
1610
+ if (ended) return;
1611
+ end();
1612
+ try {
1613
+ controller.error(abortReason$1(signal));
1614
+ } catch {}
1615
+ release?.();
1616
+ };
1617
+ return new ReadableStream({
1618
+ start(c) {
1619
+ controller = c;
1620
+ if (signal?.aborted) onAbort();
1621
+ else signal?.addEventListener("abort", onAbort, { once: true });
1622
+ },
1623
+ async pull(c) {
1624
+ try {
1625
+ reading = true;
1626
+ const { done, value } = await it.next().finally(() => {
1627
+ reading = false;
1628
+ });
1629
+ if (ended) return;
1630
+ if (done) {
1631
+ end();
1632
+ c.close();
1633
+ return;
1634
+ }
1635
+ c.enqueue(value);
1636
+ } catch (err) {
1637
+ if (ended) return;
1638
+ end();
1639
+ c.error(err);
1640
+ }
1641
+ },
1642
+ cancel() {
1643
+ end();
1644
+ const returned = Promise.resolve(it.return?.()).catch(() => {});
1645
+ if (!reading) return;
1646
+ const grace = setTimeout(() => void release?.(), CANCEL_GRACE_MS);
1647
+ grace.unref?.();
1648
+ returned.finally(() => clearTimeout(grace));
1649
+ }
1650
+ });
1554
1651
  }
1555
1652
  //#endregion
1556
1653
  //#region ../webrun-rpc/dist/index.js
@@ -1675,8 +1772,11 @@ function listenPort(port, handler, { channelName = "", log = () => {} } = {}) {
1675
1772
  error
1676
1773
  });
1677
1774
  };
1678
- port.addEventListener("message", onMessage);
1679
- return () => port.removeEventListener("message", onMessage);
1775
+ const listener = (event) => {
1776
+ onMessage(event);
1777
+ };
1778
+ port.addEventListener("message", listener);
1779
+ return () => port.removeEventListener("message", listener);
1680
1780
  }
1681
1781
  /**
1682
1782
  * Wraps an async iterable so that an `AbortSignal` firing causes the wrapper
@@ -2054,7 +2154,11 @@ function receiveChunks(port, channelName, controller, touch) {
2054
2154
  * outstanding at a time (spec D11/D12), with no per-chunk deadline (spec D8).
2055
2155
  */
2056
2156
  async function sendChunks(port, channelName, output, { maxMessageSize, log }, signal, touch) {
2057
- const stream = throughAbort(maxMessageSize ? toChunks(maxMessageSize)(output) : output, signal);
2157
+ const framed = maxMessageSize ? toChunks(maxMessageSize)(output) : output;
2158
+ const stream = throughAbort(framed, signal);
2159
+ const producer = output;
2160
+ const returnProducer = () => void Promise.resolve(producer.return?.()).catch(() => {});
2161
+ if (framed !== output) signal.addEventListener("abort", returnProducer, { once: true });
2058
2162
  try {
2059
2163
  await sendIterator(async ({ done, value, error }) => {
2060
2164
  if (signal.aborted) return;
@@ -2078,9 +2182,38 @@ async function sendChunks(port, channelName, output, { maxMessageSize, log }, si
2078
2182
  } catch (err) {
2079
2183
  if (signal.aborted) return;
2080
2184
  throw err;
2185
+ } finally {
2186
+ signal.removeEventListener("abort", returnProducer);
2081
2187
  }
2082
2188
  }
2083
2189
  /**
2190
+ * Runs `fn` on a later task, never a microtask: a producer looping on
2191
+ * `whenPortReady` must let the transport's own events run. `setImmediate`
2192
+ * where it exists; elsewhere a `MessageChannel`, because nested `setTimeout`s
2193
+ * are clamped to 4 ms in browsers.
2194
+ */
2195
+ const setImmediateFn = globalThis.setImmediate;
2196
+ /** @internal */
2197
+ const nextTask = setImmediateFn ? (fn) => {
2198
+ setImmediateFn(fn);
2199
+ } : (() => {
2200
+ let channel;
2201
+ const queue = [];
2202
+ return (fn) => {
2203
+ if (!channel) {
2204
+ channel = new MessageChannel();
2205
+ channel.port1.onmessage = () => queue.shift()?.();
2206
+ }
2207
+ queue.push(fn);
2208
+ channel.port2.postMessage(0);
2209
+ };
2210
+ })();
2211
+ const states = /* @__PURE__ */ new WeakMap();
2212
+ /** @internal Registered by the multiplexer for each port with a window. */
2213
+ function setPortSendState(port, state) {
2214
+ states.set(port, state);
2215
+ }
2216
+ /**
2084
2217
  * One virtual port.
2085
2218
  *
2086
2219
  * `deliver` and `markClosed` are deliberately not on `port`: the consumer holds
@@ -2095,6 +2228,14 @@ function newVirtualPort(send, requestClose) {
2095
2228
  if (closing.signal.aborted) return;
2096
2229
  closing.abort(reason ?? /* @__PURE__ */ new Error("webrun-rpc: the port was closed"));
2097
2230
  };
2231
+ const close = (reason) => {
2232
+ if (closed) return;
2233
+ closed = true;
2234
+ const notify = requestClose;
2235
+ listeners.clear();
2236
+ notify(reason);
2237
+ abort(reason);
2238
+ };
2098
2239
  const port = {
2099
2240
  addEventListener(_type, listener) {
2100
2241
  listeners.add(listener);
@@ -2106,24 +2247,28 @@ function newVirtualPort(send, requestClose) {
2106
2247
  if (closed) return;
2107
2248
  send(message, transfer);
2108
2249
  },
2109
- close() {
2110
- if (closed) return;
2111
- closed = true;
2112
- const notify = requestClose;
2113
- listeners.clear();
2114
- notify();
2115
- abort();
2116
- }
2250
+ close
2117
2251
  };
2118
2252
  setPortCloseSignal(port, closing.signal);
2253
+ const dispose = Symbol.dispose;
2254
+ if (dispose) Object.defineProperty(port, dispose, {
2255
+ value: () => close(),
2256
+ configurable: true
2257
+ });
2119
2258
  return {
2120
2259
  port,
2121
2260
  deliver(payload) {
2122
2261
  if (closed) return;
2123
2262
  const event = new MessageEvent("message", { data: payload });
2263
+ let pending;
2124
2264
  for (const listener of [...listeners]) try {
2125
- listener(event);
2265
+ const result = listener(event);
2266
+ if (result && typeof result.then === "function") {
2267
+ pending ??= [];
2268
+ pending.push(result);
2269
+ }
2126
2270
  } catch {}
2271
+ return pending ? Promise.allSettled(pending) : void 0;
2127
2272
  },
2128
2273
  markClosed(reason) {
2129
2274
  closed = true;
@@ -2132,53 +2277,318 @@ function newVirtualPort(send, requestClose) {
2132
2277
  },
2133
2278
  isClosed() {
2134
2279
  return closed;
2280
+ },
2281
+ hasListeners() {
2282
+ return listeners.size > 0;
2135
2283
  }
2136
2284
  };
2137
2285
  }
2138
2286
  /**
2139
- * The message of the error that fails the calls still in flight on a
2140
- * multiplexer when it is closed (`PortMux.close()`).
2287
+ * The id of keepalive's `ping` / `pong`: one no port ever takes. An older mux
2288
+ * whose codec passes unknown envelopes through reads them as a close of that
2289
+ * id, which must therefore not be a real port (0 is the initiator's first).
2141
2290
  */
2142
- const MUX_CLOSED_MESSAGE = "webrun-rpc: the multiplexer was closed";
2291
+ const KEEPALIVE_ID = Number.MAX_SAFE_INTEGER;
2292
+ /** The longest delay a timer holds; past it `setInterval` fires at once. */
2293
+ const MAX_TIMER_MS = 2 ** 31 - 1;
2143
2294
  /** Ceiling on concurrently open virtual ports. Bounds the id table only. */
2144
2295
  const DEFAULT_MAX_PORTS = 1024;
2145
2296
  /**
2297
+ * What a message costs a port's window when the codec cannot say
2298
+ * (`PortCodec.encode`): its bytes and string lengths, found in its values up
2299
+ * to 4 levels deep (at most 256 values), and at least 64. Both ends compute it
2300
+ * from the same structured value, so they agree.
2301
+ */
2302
+ function estimateMessageSize(payload) {
2303
+ let budget = 256;
2304
+ const walk = (v, depth) => {
2305
+ if (budget-- <= 0) return 0;
2306
+ if (typeof v === "string") return v.length;
2307
+ if (v instanceof ArrayBuffer) return v.byteLength;
2308
+ if (ArrayBuffer.isView(v)) return v.byteLength;
2309
+ if (v === null || typeof v !== "object" || depth >= 4) return 8;
2310
+ let n = 16;
2311
+ if (Array.isArray(v)) for (const x of v) n += walk(x, depth + 1);
2312
+ else for (const k in v) n += k.length + walk(v[k], depth + 1);
2313
+ return n;
2314
+ };
2315
+ return Math.max(64, walk(payload, 0));
2316
+ }
2317
+ /**
2146
2318
  * The default `PortMux`: emulates multiplexing over a single port.
2147
2319
  *
2148
2320
  * A transport that already multiplexes natively supplies its own `PortMux`
2149
2321
  * instead — this implementation is for transports that offer one pipe.
2150
2322
  */
2151
2323
  function multiplexPort(port, options) {
2152
- const { codec, onPort, side = "initiator", maxPorts = DEFAULT_MAX_PORTS, maxMessageSize } = options;
2324
+ const { codec, onPort, side = "initiator", maxPorts = DEFAULT_MAX_PORTS, maxMessageSize, signal, window, frameSize, keepalive, onLeak } = options;
2325
+ const maxReassembledBytes = options.maxReassembledBytes ?? 67108864;
2326
+ const misses = keepalive?.misses ?? 3;
2327
+ 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`);
2328
+ if (frameSize != null && !(Number.isSafeInteger(frameSize) && frameSize > 0)) throw new RangeError(`webrun-rpc: frameSize must be a positive integer, got ${frameSize}`);
2329
+ if (!(Number.isSafeInteger(maxReassembledBytes) && maxReassembledBytes > 0)) throw new RangeError(`webrun-rpc: maxReassembledBytes must be a positive integer, got ${maxReassembledBytes}`);
2153
2330
  const open = /* @__PURE__ */ new Map();
2331
+ let reassembling = 0;
2332
+ const freePartial = (entry) => {
2333
+ if (!entry?.partial) return;
2334
+ reassembling -= entry.partial.charged;
2335
+ entry.partial = void 0;
2336
+ };
2337
+ const leaks = onLeak ? new FinalizationRegistry(({ id, record, release }) => {
2338
+ const entry = open.get(id);
2339
+ if (entry?.handle !== record) return;
2340
+ const { handle: _, credit: __, partial: ___, ...info } = entry;
2341
+ if (!record.hasListeners()) {
2342
+ release("leaked");
2343
+ record.markClosed(/* @__PURE__ */ new Error("webrun-rpc: the port was dropped without close()"));
2344
+ }
2345
+ try {
2346
+ onLeak?.(info);
2347
+ } catch {}
2348
+ }) : void 0;
2349
+ const touch = (id) => {
2350
+ const entry = open.get(id);
2351
+ if (entry) entry.lastActivity = Date.now();
2352
+ };
2154
2353
  let nextId = side === "initiator" ? 0 : 1;
2155
2354
  let muxClosed = false;
2355
+ /** This end counts encoded bytes and reassembles frames. */
2356
+ const exactCodec = codec.encode != null && codec.decode != null;
2156
2357
  const post = (envelope, transfer) => {
2157
2358
  if (muxClosed) return;
2359
+ codecPost(envelope, transfer);
2360
+ };
2361
+ /** Like `post`, also while closing: the mux's own close notices. */
2362
+ const codecPost = (envelope, transfer) => {
2158
2363
  try {
2159
2364
  codec.post(port, envelope, transfer);
2160
2365
  } catch {}
2161
2366
  };
2162
- const attach = (id) => {
2163
- let self;
2164
- self = newVirtualPort((payload, transfer) => post({
2165
- type: "message",
2166
- id,
2167
- payload
2168
- }, transfer), (reason) => {
2367
+ const outgoing = /* @__PURE__ */ new Map();
2368
+ const ring = [];
2369
+ let pumping = false;
2370
+ const pump = async () => {
2371
+ pumping = true;
2372
+ try {
2373
+ while (ring.length && !muxClosed) {
2374
+ if (port.whenDrained) await port.whenDrained();
2375
+ if (muxClosed) break;
2376
+ const id = ring.shift();
2377
+ const queue = outgoing.get(id);
2378
+ const item = queue?.shift();
2379
+ if (queue?.length) ring.push(id);
2380
+ else outgoing.delete(id);
2381
+ item?.();
2382
+ }
2383
+ } catch (err) {
2384
+ close(err).catch(() => {});
2385
+ } finally {
2386
+ pumping = false;
2387
+ }
2388
+ };
2389
+ const sendOnPort = (id, item) => {
2390
+ if (frameSize == null) {
2391
+ item();
2392
+ return;
2393
+ }
2394
+ let queue = outgoing.get(id);
2395
+ if (!queue) {
2396
+ queue = [];
2397
+ outgoing.set(id, queue);
2398
+ ring.push(id);
2399
+ }
2400
+ queue.push(item);
2401
+ if (!pumping) pump();
2402
+ };
2403
+ const dropOutgoing = (id) => {
2404
+ if (!outgoing.delete(id)) return;
2405
+ const at = ring.indexOf(id);
2406
+ if (at >= 0) ring.splice(at, 1);
2407
+ };
2408
+ const postBytes = (bytes) => {
2409
+ if (muxClosed) return;
2410
+ try {
2411
+ port.postMessage(bytes);
2412
+ } catch {}
2413
+ };
2414
+ const releaseWaiters = (c) => {
2415
+ for (const w of c?.waiters.splice(0) ?? []) w.resolve();
2416
+ };
2417
+ const flush = (c) => {
2418
+ while (c.queue.length) {
2419
+ const next = c.queue[0];
2420
+ if (c.enforced && c.outstanding > 0 && c.outstanding + next.size > c.window) break;
2421
+ c.queue.shift();
2422
+ c.queued -= next.size;
2423
+ c.outstanding += next.size;
2424
+ next.send();
2425
+ }
2426
+ for (const w of [...c.waiters]) if (c.queued <= w.below) {
2427
+ c.waiters.splice(c.waiters.indexOf(w), 1);
2428
+ w.resolve();
2429
+ }
2430
+ };
2431
+ const grant = (id, c, bytes) => {
2432
+ c.unconsumed -= bytes;
2433
+ c.toGrant += bytes;
2434
+ const send = () => {
2435
+ c.grantScheduled = false;
2436
+ if (!c.toGrant || !open.has(id)) return;
2169
2437
  post({
2438
+ type: "credit",
2439
+ id,
2440
+ bytes: c.toGrant
2441
+ });
2442
+ c.toGrant = 0;
2443
+ };
2444
+ if (c.toGrant >= c.window / 4) {
2445
+ send();
2446
+ return;
2447
+ }
2448
+ if (c.grantScheduled) return;
2449
+ c.grantScheduled = true;
2450
+ setTimeout(send, 0);
2451
+ };
2452
+ const attach = (id, meta, portWindow, opener, exact) => {
2453
+ const c = typeof portWindow === "number" && Number.isSafeInteger(portWindow) && portWindow > 0 ? {
2454
+ window: portWindow,
2455
+ enforced: false,
2456
+ opener,
2457
+ exact,
2458
+ outstanding: 0,
2459
+ queue: [],
2460
+ queued: 0,
2461
+ unconsumed: 0,
2462
+ waiters: [],
2463
+ toGrant: 0,
2464
+ grantScheduled: false
2465
+ } : void 0;
2466
+ const self = newVirtualPort((payload, transfer) => {
2467
+ touch(id);
2468
+ const envelope = {
2469
+ type: "message",
2470
+ id,
2471
+ payload
2472
+ };
2473
+ if (!c) {
2474
+ post(envelope, transfer);
2475
+ return;
2476
+ }
2477
+ const bytes = c.exact ? codec.encode?.(envelope) : void 0;
2478
+ const size = bytes?.byteLength ?? estimateMessageSize(payload);
2479
+ const waits = frameSize != null || c.queue.length > 0 || c.enforced && c.outstanding > 0 && c.outstanding + size > c.window;
2480
+ const held = !bytes && waits ? {
2481
+ ...envelope,
2482
+ payload: structuredClone(payload, { transfer })
2483
+ } : void 0;
2484
+ const send = (whole = false) => {
2485
+ if (!bytes) {
2486
+ sendOnPort(id, held ? () => post(held) : () => post(envelope, transfer));
2487
+ return;
2488
+ }
2489
+ if (!whole && frameSize != null && c.enforced && c.exact && bytes.byteLength > frameSize) {
2490
+ for (let at = 0; at < bytes.byteLength; at += frameSize) {
2491
+ const data = bytes.subarray(at, at + frameSize);
2492
+ const frame = at + frameSize >= bytes.byteLength ? {
2493
+ type: "frame",
2494
+ id,
2495
+ data,
2496
+ last: true
2497
+ } : {
2498
+ type: "frame",
2499
+ id,
2500
+ data
2501
+ };
2502
+ sendOnPort(id, () => post(frame));
2503
+ }
2504
+ return;
2505
+ }
2506
+ sendOnPort(id, () => postBytes(bytes));
2507
+ };
2508
+ c.queue.push({
2509
+ send,
2510
+ size
2511
+ });
2512
+ c.queued += size;
2513
+ flush(c);
2514
+ }, (reason) => {
2515
+ release(reason);
2516
+ record.markClosed(reason);
2517
+ });
2518
+ function release(reason) {
2519
+ freePartial(open.get(id));
2520
+ open.delete(id);
2521
+ leaks?.unregister(record);
2522
+ if (c?.queue.length) {
2523
+ for (const item of c.queue.splice(0)) item.send(true);
2524
+ c.queued = 0;
2525
+ }
2526
+ sendOnPort(id, () => post({
2170
2527
  type: "close",
2171
2528
  id,
2172
2529
  reason
2530
+ }));
2531
+ releaseWaiters(c);
2532
+ }
2533
+ const { markClosed, isClosed, hasListeners } = self;
2534
+ if (c) {
2535
+ const deliver = self.deliver;
2536
+ self.deliver = (payload, size = estimateMessageSize(payload)) => {
2537
+ c.unconsumed += size;
2538
+ const done = deliver(payload);
2539
+ if (done) done.then(() => grant(id, c, size));
2540
+ else grant(id, c, size);
2541
+ return done;
2542
+ };
2543
+ setPortSendState(self.port, {
2544
+ bufferedAmount: () => c.queued,
2545
+ ready: (below = 0) => new Promise((resolve) => {
2546
+ if (c.queued <= below || isClosed()) nextTask(resolve);
2547
+ else c.waiters.push({
2548
+ below,
2549
+ resolve: () => nextTask(resolve)
2550
+ });
2551
+ })
2173
2552
  });
2174
- open.delete(id);
2175
- self.markClosed();
2553
+ }
2554
+ const record = {
2555
+ deliver: self.deliver,
2556
+ markClosed,
2557
+ isClosed,
2558
+ hasListeners,
2559
+ close: (reason) => {
2560
+ if (isClosed()) return;
2561
+ release(reason);
2562
+ markClosed(reason);
2563
+ }
2564
+ };
2565
+ leaks?.register(self.port, {
2566
+ id,
2567
+ record,
2568
+ release
2569
+ }, record);
2570
+ const now = Date.now();
2571
+ open.set(id, {
2572
+ id,
2573
+ meta,
2574
+ openedAt: now,
2575
+ lastActivity: now,
2576
+ handle: record,
2577
+ credit: c
2176
2578
  });
2177
- open.set(id, self);
2178
2579
  return self;
2179
2580
  };
2180
- const handleEnvelope = (envelope) => {
2581
+ const handleEnvelope = (envelope, event) => {
2181
2582
  if (muxClosed) return;
2583
+ heardSincePing = true;
2584
+ if (envelope.type === "ping" || envelope.type === "pong") {
2585
+ peerKeepsAlive = true;
2586
+ if (envelope.type === "ping") post({
2587
+ type: "pong",
2588
+ id: KEEPALIVE_ID
2589
+ });
2590
+ return;
2591
+ }
2182
2592
  if (envelope.type === "open") {
2183
2593
  if (open.has(envelope.id)) return;
2184
2594
  if (open.size >= maxPorts) {
@@ -2189,7 +2599,18 @@ function multiplexPort(port, options) {
2189
2599
  });
2190
2600
  return;
2191
2601
  }
2192
- const handle = attach(envelope.id);
2602
+ const handle = attach(envelope.id, envelope.meta, envelope.window, false, envelope.exact === true && exactCodec);
2603
+ const credit = open.get(envelope.id)?.credit;
2604
+ if (credit) post(credit.exact ? {
2605
+ type: "credit",
2606
+ id: envelope.id,
2607
+ bytes: 0,
2608
+ exact: true
2609
+ } : {
2610
+ type: "credit",
2611
+ id: envelope.id,
2612
+ bytes: 0
2613
+ });
2193
2614
  let accepted = false;
2194
2615
  if (onPort) try {
2195
2616
  accepted = onPort(handle.port, envelope.meta) !== false;
@@ -2204,57 +2625,215 @@ function multiplexPort(port, options) {
2204
2625
  id: envelope.id,
2205
2626
  reason: "rejected"
2206
2627
  });
2628
+ return;
2207
2629
  }
2208
2630
  return;
2209
2631
  }
2210
- const handle = open.get(envelope.id);
2632
+ const entry = open.get(envelope.id);
2633
+ if (envelope.type === "credit") {
2634
+ const c = entry?.credit;
2635
+ if (!c || !(envelope.bytes >= 0)) return;
2636
+ if (!c.enforced && c.opener) {
2637
+ if (c.exact && envelope.exact !== true) {
2638
+ c.exact = false;
2639
+ c.outstanding = 0;
2640
+ }
2641
+ post({
2642
+ type: "credit",
2643
+ id: envelope.id,
2644
+ bytes: 0
2645
+ });
2646
+ }
2647
+ c.enforced = true;
2648
+ c.outstanding = Math.max(0, c.outstanding - envelope.bytes);
2649
+ flush(c);
2650
+ return;
2651
+ }
2211
2652
  if (envelope.type === "message") {
2212
- if (!handle) return;
2213
- handle.deliver(envelope.payload);
2653
+ const sizeOf = (c) => (c.exact ? byteLengthOf(event.data) : void 0) ?? estimateMessageSize(envelope.payload);
2654
+ if (!entry) return;
2655
+ touch(envelope.id);
2656
+ entry.handle.deliver(envelope.payload, entry.credit ? sizeOf(entry.credit) : void 0);
2214
2657
  return;
2215
2658
  }
2216
- if (!handle) return;
2659
+ if (envelope.type === "frame") {
2660
+ if (!entry?.credit?.exact || !codec.decode || !(envelope.data instanceof Uint8Array)) return;
2661
+ touch(envelope.id);
2662
+ const c = entry.credit;
2663
+ const owed = c.unconsumed + c.toGrant;
2664
+ const partial = entry.partial ?? {
2665
+ chunks: [],
2666
+ bytes: 0,
2667
+ owed,
2668
+ charged: 0,
2669
+ ...owed > 0 ? { room: c.window - owed } : {}
2670
+ };
2671
+ entry.partial = partial;
2672
+ const size = envelope.data.byteLength;
2673
+ const charged = partial.room == null ? Math.max(0, partial.bytes + size - c.window) : 0;
2674
+ 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;
2675
+ if (overrun) {
2676
+ freePartial(entry);
2677
+ dropOutgoing(envelope.id);
2678
+ open.delete(envelope.id);
2679
+ releaseWaiters(c);
2680
+ entry.handle.markClosed(/* @__PURE__ */ new RangeError(`webrun-rpc: a framed message passed ${overrun}`));
2681
+ post({
2682
+ type: "close",
2683
+ id: envelope.id,
2684
+ reason: "max-reassembled-bytes"
2685
+ });
2686
+ return;
2687
+ }
2688
+ partial.bytes += size;
2689
+ reassembling += charged - partial.charged;
2690
+ partial.charged = charged;
2691
+ partial.chunks.push(envelope.data);
2692
+ if (!envelope.last) return;
2693
+ freePartial(entry);
2694
+ const whole = new Uint8Array(partial.bytes);
2695
+ let at = 0;
2696
+ for (const chunk of partial.chunks) {
2697
+ whole.set(chunk, at);
2698
+ at += chunk.byteLength;
2699
+ }
2700
+ const inner = codec.decode(whole);
2701
+ if (inner?.type !== "message" || inner.id !== envelope.id) return;
2702
+ entry.handle.deliver(inner.payload, whole.byteLength);
2703
+ return;
2704
+ }
2705
+ dropOutgoing(envelope.id);
2706
+ if (!entry) return;
2707
+ freePartial(entry);
2217
2708
  open.delete(envelope.id);
2218
- handle.markClosed(/* @__PURE__ */ new Error("webrun-rpc: the peer closed the port"));
2709
+ releaseWaiters(entry.credit);
2710
+ entry.handle.markClosed(/* @__PURE__ */ new Error("webrun-rpc: the peer closed the port"));
2219
2711
  };
2220
2712
  const listener = (event) => {
2221
2713
  const envelope = codec.read(event);
2222
- if (envelope) handleEnvelope(envelope);
2714
+ if (envelope) handleEnvelope(envelope, event);
2715
+ };
2716
+ let peerKeepsAlive = false;
2717
+ let heardSincePing = true;
2718
+ let unanswered = 0;
2719
+ let pinger;
2720
+ const tick = () => {
2721
+ if (!keepalive) return;
2722
+ const backlogged = (port.bufferedAmount ?? 0) > 0;
2723
+ if (heardSincePing) unanswered = 0;
2724
+ else if (peerKeepsAlive && !backlogged) unanswered++;
2725
+ if (unanswered >= misses) {
2726
+ close(/* @__PURE__ */ new Error(`webrun-rpc: the peer did not answer ${misses} keepalive pings, ${keepalive.intervalMs} ms apart`)).catch(() => {});
2727
+ return;
2728
+ }
2729
+ heardSincePing = false;
2730
+ post({
2731
+ type: "ping",
2732
+ id: KEEPALIVE_ID
2733
+ });
2734
+ };
2735
+ const close = async (reason) => {
2736
+ if (muxClosed) return;
2737
+ clearInterval(pinger);
2738
+ muxClosed = true;
2739
+ signal?.removeEventListener("abort", onTransportAbort);
2740
+ port.removeEventListener("message", listener);
2741
+ const closedNow = /* @__PURE__ */ new Set();
2742
+ for (const [id, { handle, credit }] of [...open]) {
2743
+ closedNow.add(id);
2744
+ codecPost({
2745
+ type: "close",
2746
+ id
2747
+ });
2748
+ open.delete(id);
2749
+ releaseWaiters(credit);
2750
+ handle.markClosed(reason ?? /* @__PURE__ */ new Error("webrun-rpc: the multiplexer was closed"));
2751
+ }
2752
+ for (const id of outgoing.keys()) if (!closedNow.has(id)) codecPost({
2753
+ type: "close",
2754
+ id
2755
+ });
2756
+ outgoing.clear();
2757
+ ring.length = 0;
2758
+ await port.close?.();
2759
+ };
2760
+ const onTransportAbort = () => {
2761
+ close(signal?.reason).catch(() => {});
2223
2762
  };
2224
2763
  port.addEventListener("message", listener);
2225
2764
  port.start?.();
2765
+ if (keepalive) {
2766
+ pinger = setInterval(tick, keepalive.intervalMs);
2767
+ pinger.unref?.();
2768
+ }
2769
+ if (signal?.aborted) close(signal.reason).catch(() => {});
2770
+ else signal?.addEventListener("abort", onTransportAbort, { once: true });
2226
2771
  return {
2227
2772
  maxMessageSize,
2228
- async openPort(meta) {
2773
+ async openPort(meta, { signal: portSignal } = {}) {
2229
2774
  if (muxClosed) throw new Error("webrun-rpc: the multiplexer is closed");
2230
- if (open.size >= maxPorts) throw new RangeError(`webrun-rpc: maxPorts (${maxPorts}) reached`);
2775
+ if (portSignal?.aborted) throw portSignal.reason;
2776
+ if (open.size >= maxPorts) throw new RangeError(`webrun-rpc: maxPorts (${maxPorts}) reached; open ports by kind: ${describeKinds(open.values())}`);
2231
2777
  while (open.has(nextId)) nextId += 2;
2232
2778
  const id = nextId;
2233
2779
  nextId += 2;
2234
- const handle = attach(id);
2235
- post({
2780
+ const handle = attach(id, meta, window, true, exactCodec);
2781
+ post(window ? exactCodec ? {
2782
+ type: "open",
2783
+ id,
2784
+ meta,
2785
+ window,
2786
+ exact: true
2787
+ } : {
2788
+ type: "open",
2789
+ id,
2790
+ meta,
2791
+ window
2792
+ } : {
2236
2793
  type: "open",
2237
2794
  id,
2238
2795
  meta
2239
2796
  });
2797
+ if (portSignal) {
2798
+ const record = open.get(id)?.handle;
2799
+ const onAbort = () => {
2800
+ if (record && open.get(id)?.handle === record) record.close(portSignal.reason);
2801
+ };
2802
+ portSignal.addEventListener("abort", onAbort, { once: true });
2803
+ getPortCloseSignal(handle.port)?.addEventListener("abort", () => portSignal.removeEventListener("abort", onAbort), { once: true });
2804
+ }
2240
2805
  return handle.port;
2241
2806
  },
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?.();
2807
+ close,
2808
+ ports() {
2809
+ return [...open.values()].map(({ handle: _, credit, ...info }) => credit ? {
2810
+ ...info,
2811
+ ...credit.enforced ? { window: credit.window } : {},
2812
+ queued: credit.queued,
2813
+ outstanding: credit.outstanding,
2814
+ unconsumed: credit.unconsumed
2815
+ } : info);
2255
2816
  }
2256
2817
  };
2257
2818
  }
2819
+ /** The byte length of a received message's data, or `undefined` if it is not bytes. */
2820
+ function byteLengthOf(data) {
2821
+ if (data instanceof ArrayBuffer || ArrayBuffer.isView(data)) return data.byteLength;
2822
+ }
2823
+ /**
2824
+ * `"http: 1000, ws: 24"`: the three commonest `meta.kind`s among `entries`, so
2825
+ * a full table names who filled it. A port without a kind counts as `"?"`.
2826
+ */
2827
+ function describeKinds(entries) {
2828
+ const counts = /* @__PURE__ */ new Map();
2829
+ for (const { meta } of entries) {
2830
+ const kind = meta?.kind;
2831
+ const name = typeof kind === "string" ? kind : "?";
2832
+ counts.set(name, (counts.get(name) ?? 0) + 1);
2833
+ }
2834
+ if (!counts.size) return "none";
2835
+ return [...counts].sort((x, y) => y[1] - x[1]).slice(0, 3).map(([name, n]) => `${name}: ${n}`).join(", ");
2836
+ }
2258
2837
  /**
2259
2838
  * Announced with every port this pair opens. Layer 1 never reads `meta` — it is
2260
2839
  * here so a peer running something else over the same multiplexer can tell a
@@ -2316,14 +2895,17 @@ const serve = async ({ mux: factory, timeout }, handler) => {
2316
2895
  const mux = await factory((port) => {
2317
2896
  let off;
2318
2897
  let finished = false;
2319
- const tracked = (input) => (async function* () {
2320
- try {
2321
- yield* handler(input);
2322
- } finally {
2323
- finished = true;
2324
- if (off) live.delete(off);
2325
- }
2326
- })();
2898
+ const tracked = (input) => {
2899
+ const output = handler(input);
2900
+ return returningAtOnce((async function* () {
2901
+ try {
2902
+ yield* output;
2903
+ } finally {
2904
+ finished = true;
2905
+ if (off) live.delete(off);
2906
+ }
2907
+ })(), () => void output.return(void 0).catch(() => {}));
2908
+ };
2327
2909
  off = serveDuplexOverPort(port, tracked, streamOptions);
2328
2910
  if (!finished) live.add(off);
2329
2911
  });
@@ -2361,7 +2943,7 @@ function isEnvelope(value) {
2361
2943
  const candidate = value;
2362
2944
  if (typeof candidate.id !== "number") return false;
2363
2945
  if (!Number.isInteger(candidate.id) || candidate.id < 0) return false;
2364
- return candidate.type === "open" || candidate.type === "message" || candidate.type === "close";
2946
+ return candidate.type === "open" || candidate.type === "message" || candidate.type === "close" || candidate.type === "credit" || candidate.type === "frame" || candidate.type === "ping" || candidate.type === "pong";
2365
2947
  }
2366
2948
  /**
2367
2949
  * For ports whose messages are structured values — a real `MessagePort`, a
@@ -2523,7 +3105,7 @@ function trackedLink(send, release) {
2523
3105
  finish();
2524
3106
  return response;
2525
3107
  }
2526
- return new Response(guardBody(response.body, lost, () => lostReason, finish), {
3108
+ return new Response(guardBody(response.body, lost, () => lostReason, finish, request.signal), {
2527
3109
  status: response.status,
2528
3110
  statusText: response.statusText,
2529
3111
  headers: response.headers
@@ -2542,33 +3124,65 @@ function trackedLink(send, release) {
2542
3124
  return asyncDisposable(link, () => link.close());
2543
3125
  }
2544
3126
  /**
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.
3127
+ * A response body that reports its end (done, cancelled, failed or aborted) and
3128
+ * fails with the link's reason when the link is lost mid-stream. An abort of the
3129
+ * request's signal ends it at once, read or not: the exchange is over (the layer
3130
+ * below has given its port back), so the request leaves `inflight` then.
2547
3131
  */
2548
- function guardBody(body, lost, reason, finish) {
3132
+ function guardBody(body, lost, reason, finish, signal) {
2549
3133
  const reader = body.getReader();
3134
+ let controller;
3135
+ let ended = false;
3136
+ const end = () => {
3137
+ ended = true;
3138
+ signal?.removeEventListener("abort", onAbort);
3139
+ finish();
3140
+ };
3141
+ const onAbort = () => {
3142
+ if (ended) return;
3143
+ end();
3144
+ reader.cancel().catch(() => {});
3145
+ try {
3146
+ controller.error(abortError(signal));
3147
+ } catch {}
3148
+ };
2550
3149
  return new ReadableStream({
3150
+ start(c) {
3151
+ controller = c;
3152
+ if (signal?.aborted) onAbort();
3153
+ else signal?.addEventListener("abort", onAbort, { once: true });
3154
+ },
2551
3155
  async pull(controller) {
2552
3156
  try {
2553
3157
  const { done, value } = await Promise.race([reader.read(), lost]);
3158
+ if (ended) return;
2554
3159
  if (done) {
2555
- finish();
3160
+ end();
2556
3161
  controller.close();
2557
3162
  return;
2558
3163
  }
2559
3164
  controller.enqueue(value);
2560
3165
  } catch (error) {
2561
- finish();
3166
+ if (ended) return;
3167
+ end();
2562
3168
  reader.cancel().catch(() => {});
2563
3169
  controller.error(reason() ?? error);
2564
3170
  }
2565
3171
  },
2566
3172
  async cancel(why) {
2567
- finish();
3173
+ end();
2568
3174
  await reader.cancel(why).catch(() => {});
2569
3175
  }
2570
3176
  });
2571
3177
  }
3178
+ /** The signal's reason as an `Error` (`AbortError` when it gave none). */
3179
+ function abortError(signal) {
3180
+ const why = signal.reason;
3181
+ if (why instanceof Error) return why;
3182
+ const err = new Error(why === void 0 ? "Aborted" : String(why));
3183
+ err.name = "AbortError";
3184
+ return err;
3185
+ }
2572
3186
  //#endregion
2573
3187
  //#region src/link/locks.ts
2574
3188
  function lockManager() {
@@ -3715,12 +4329,18 @@ function serveHttpInWorker(handler, options = {}) {
3715
4329
  const ownerLock = newLockName("webrun-http-owner");
3716
4330
  const held = holdLock(ownerLock);
3717
4331
  const controls = /* @__PURE__ */ new Set();
4332
+ const seen = /* @__PURE__ */ new Map();
3718
4333
  const tellPages = (message) => {
3719
4334
  for (const control of controls) control.postMessage(message);
3720
4335
  };
3721
4336
  const endpoint = new HandlerEndpoint(handler, {
3722
4337
  ...options,
3723
- requestRelink: () => tellPages({ type: "relink" }),
4338
+ requestRelink: () => {
4339
+ for (const control of controls) control.postMessage({
4340
+ type: "relink",
4341
+ seen: seen.get(control) ?? 0
4342
+ });
4343
+ },
3724
4344
  keepAlive: () => tellPages({ type: "keepalive" })
3725
4345
  });
3726
4346
  const onMessage = (event) => {
@@ -3732,6 +4352,7 @@ function serveHttpInWorker(handler, options = {}) {
3732
4352
  control.onmessage = (message) => {
3733
4353
  const msg = message.data;
3734
4354
  if (msg?.type === "link") {
4355
+ if (msg.seq !== void 0) seen.set(control, msg.seq);
3735
4356
  const [down, ctl, up] = message.ports;
3736
4357
  if (down) endpoint.link(msg.key, {
3737
4358
  down,
@@ -3772,6 +4393,7 @@ function serveHttpInWorker(handler, options = {}) {
3772
4393
  await endpoint.close();
3773
4394
  for (const control of controls) control.close();
3774
4395
  controls.clear();
4396
+ seen.clear();
3775
4397
  (await held)();
3776
4398
  }
3777
4399
  };