@statewalker/webrun-http-browser 0.7.0 → 0.7.1

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/sw.js CHANGED
@@ -22,8 +22,6 @@ function deserializeError(error) {
22
22
  const payload = typeof error === "string" ? { message: error } : error;
23
23
  return Object.assign(new Error(payload.message), payload);
24
24
  }
25
- new TextEncoder();
26
- new TextDecoder();
27
25
  /**
28
26
  * The newAsyncGenerator function creates async generators from callback-based initialization
29
27
  * functions, providing a bridge between imperative event handling and declarative async
@@ -176,7 +174,6 @@ async function* newAsyncGenerator(init, skipValues = false) {
176
174
  drainQueue();
177
175
  }
178
176
  }
179
- new TextEncoder();
180
177
  /**
181
178
  * The iterator ↔ `ReadableStream` boundary.
182
179
  *
@@ -446,8 +443,8 @@ var HttpParseError = class extends Error {
446
443
  const NEWLINE = 10;
447
444
  /** Mirrors the HTTP/1.1 codec's default `maxHeaderBytes`. */
448
445
  const MAX_ENVELOPE_BYTES = 65536;
449
- const encoder$1 = new TextEncoder();
450
- const decoder = new TextDecoder();
446
+ const encoder$1 = /* @__PURE__ */ new TextEncoder();
447
+ const decoder = /* @__PURE__ */ new TextDecoder();
451
448
  /**
452
449
  * Encode an HTTP envelope plus optional body as one continuous byte stream:
453
450
  *
@@ -701,15 +698,15 @@ function resolveFraming(headers, version) {
701
698
  };
702
699
  return { kind: "none" };
703
700
  }
704
- const CRLF = new Uint8Array([13, 10]);
705
- const LAST_CHUNK = new Uint8Array([
701
+ const CRLF = /* @__PURE__ */ new Uint8Array([13, 10]);
702
+ const LAST_CHUNK = /* @__PURE__ */ new Uint8Array([
706
703
  48,
707
704
  13,
708
705
  10,
709
706
  13,
710
707
  10
711
708
  ]);
712
- const encoder = new TextEncoder();
709
+ const encoder = /* @__PURE__ */ new TextEncoder();
713
710
  /**
714
711
  * Chunk sizes are written in HEXADECIMAL — a 21-byte chunk is `15`. Writing
715
712
  * them in decimal is the defect note 15 found in @libp2p/http, and it is the
@@ -1014,7 +1011,7 @@ function newHttpCodec(options = {}) {
1014
1011
  decodeResponse: (input, o) => decodeResponse(input, opts, o.method)
1015
1012
  };
1016
1013
  }
1017
- const httpCodec = newHttpCodec();
1014
+ const httpCodec = /* @__PURE__ */ newHttpCodec();
1018
1015
  /**
1019
1016
  * Dispatches on byte 0. The formats are self-identifying — a JSON envelope
1020
1017
  * always begins `{`, which is not a token character and so can never begin an
@@ -1064,10 +1061,29 @@ function newSniffingCodec(options) {
1064
1061
  * Writes HTTP/1.1; accepts HTTP/1.1 or the legacy JSON envelope. Used whenever
1065
1062
  * no `codec` option is supplied.
1066
1063
  */
1067
- const defaultCodec = newSniffingCodec({
1064
+ const defaultCodec = /* @__PURE__ */ newSniffingCodec({
1068
1065
  write: httpCodec,
1069
1066
  accept: [httpCodec, jsonEnvelopeCodec]
1070
1067
  });
1068
+ /**
1069
+ * Makes `target` disposable with `await using`: installs `dispose` as its
1070
+ * `[Symbol.asyncDispose]`, where the runtime defines the symbol. Hand-written
1071
+ * iterators lose what a native generator gets from `AsyncIteratorPrototype`;
1072
+ * this gives it back. A no-op where the symbol is missing. Kept off the
1073
+ * declared types, which would require consumers to compile with the
1074
+ * `esnext.disposable` lib.
1075
+ */
1076
+ function asyncDisposable$2(target, dispose) {
1077
+ const asyncDispose = Symbol.asyncDispose;
1078
+ if (asyncDispose && !(asyncDispose in target)) Object.defineProperty(target, asyncDispose, {
1079
+ value: async () => {
1080
+ await dispose();
1081
+ },
1082
+ configurable: true,
1083
+ writable: true
1084
+ });
1085
+ return target;
1086
+ }
1071
1087
  /** Carries the serialized JS error between two webrun peers. */
1072
1088
  const PEER_ERROR_HEADER = "x-webrun-error";
1073
1089
  /** Hard cap on the peer-error header (M3): see `encodeErrorResponse`. */
@@ -1145,41 +1161,105 @@ async function throwIfPeerError(result, output) {
1145
1161
  */
1146
1162
  async function httpFetch(call, env, body, options = {}) {
1147
1163
  const codec = options.codec ?? defaultCodec;
1164
+ const { signal } = options;
1165
+ if (signal?.aborted) throw abortReason$1(signal);
1148
1166
  const output = call(codec.encodeRequest(env, body));
1149
- const decoded = await codec.decodeResponse(output, { method: env.method });
1150
- const release = async () => {
1151
- try {
1152
- await output.return?.(void 0);
1153
- } catch {}
1167
+ let released;
1168
+ const release = () => {
1169
+ released ??= (async () => {
1170
+ try {
1171
+ await output.return?.(void 0);
1172
+ } catch {}
1173
+ })();
1174
+ return released;
1154
1175
  };
1176
+ let finished = false;
1177
+ const finish = () => {
1178
+ if (finished || released) return;
1179
+ finished = true;
1180
+ if (signal?.aborted) {
1181
+ release();
1182
+ return;
1183
+ }
1184
+ const onAbort = () => void release();
1185
+ signal?.addEventListener("abort", onAbort, { once: true });
1186
+ drain(output).finally(() => signal?.removeEventListener("abort", onAbort));
1187
+ };
1188
+ let decoded;
1189
+ try {
1190
+ decoded = await untilAborted(codec.decodeResponse(output, { method: env.method }), signal);
1191
+ } catch (error) {
1192
+ if (signal?.aborted) release();
1193
+ else await release();
1194
+ throw error;
1195
+ }
1155
1196
  const result = {
1156
1197
  envelope: decoded.envelope,
1157
- body: releasingOnExit(decoded.body, release),
1158
- release
1198
+ body: releasingOnExit(decoded.body, finish, release),
1199
+ release,
1200
+ finish
1159
1201
  };
1160
1202
  await throwIfPeerError(result, output);
1161
1203
  return result;
1162
1204
  }
1205
+ /** `promise`, rejected with the signal's reason if it aborts first. */
1206
+ function untilAborted(promise, signal) {
1207
+ if (!signal) return promise;
1208
+ return new Promise((resolve, reject) => {
1209
+ const onAbort = () => reject(abortReason$1(signal));
1210
+ if (signal.aborted) return onAbort();
1211
+ signal.addEventListener("abort", onAbort, { once: true });
1212
+ promise.then((value) => {
1213
+ signal.removeEventListener("abort", onAbort);
1214
+ resolve(value);
1215
+ }, (error) => {
1216
+ signal.removeEventListener("abort", onAbort);
1217
+ reject(error);
1218
+ });
1219
+ });
1220
+ }
1221
+ /** The signal's reason as an `Error` (`AbortError` when it gave none). */
1222
+ function abortReason$1(signal) {
1223
+ const reason = signal.reason;
1224
+ if (reason instanceof Error) return reason;
1225
+ const err = new Error(reason === void 0 ? "Aborted" : String(reason));
1226
+ err.name = "AbortError";
1227
+ return err;
1228
+ }
1163
1229
  /**
1164
- * `body`, but leaving it before its end releases the call.
1230
+ * `body`, ending the call however it is left: read to its end, or left early.
1165
1231
  *
1166
1232
  * The body the codec hands back is a generator of its own over a `ByteReader`,
1167
- * and `.return()` on it never reaches the call's `output` (see
1168
- * `throwIfPeerError` for why the codec must not do that itself). So a caller
1169
- * that stopped reading — an aborted `fetch`, a cancelled response stream —
1170
- * left the peer's producer parked forever, holding its handler open. Safe here
1171
- * for the same reason it is safe in `throwIfPeerError`: `output` is this
1172
- * call's own response-only generator, and the request was fully sent.
1233
+ * and its ending never reaches the call's `output` (see `throwIfPeerError` for
1234
+ * why the codec must not do that itself). A caller that stopped reading — an
1235
+ * aborted `fetch`, a cancelled response stream — left the peer's producer
1236
+ * parked forever; and one that read the body to its end left the call itself
1237
+ * open, which on a multiplexed transport is one port per exchange until the
1238
+ * multiplexer refuses to open more (`maxPorts`).
1239
+ *
1240
+ * Left early, the call is abandoned (`.return()`): the peer's producer is
1241
+ * released. Read to its end, the call is not cut short but read to ITS end, in
1242
+ * the background: the peer ends it once its side of the exchange is over,
1243
+ * which may be later than the response — a handler that answered before it
1244
+ * read the request body is still receiving it (`HttpDataHandlerResult.settled`),
1245
+ * and abandoning the call would abort that upload.
1173
1246
  */
1174
- async function* releasingOnExit(body, release) {
1247
+ async function* releasingOnExit(body, finish, release) {
1175
1248
  let completed = false;
1176
1249
  try {
1177
1250
  yield* body;
1178
1251
  completed = true;
1179
1252
  } finally {
1180
- if (!completed) await release();
1253
+ if (completed) finish();
1254
+ else await release();
1181
1255
  }
1182
1256
  }
1257
+ /** Reads `output` to its end, discarding what is left, ignoring a failure. */
1258
+ async function drain(output) {
1259
+ try {
1260
+ for (let r = await output.next(); !r.done; r = await output.next());
1261
+ } catch {}
1262
+ }
1183
1263
  /**
1184
1264
  * Wrap an HTTP handler as a `Duplex` so it can be registered with any
1185
1265
  * `webrun-streams-*` adapter's `serve`.
@@ -1207,6 +1287,7 @@ function httpServe(handler, options = {}) {
1207
1287
  try {
1208
1288
  yield* replyCodec.encodeResponse(result.envelope, result.body, { method: decoded.envelope.method });
1209
1289
  completed = true;
1290
+ await result.settled?.();
1210
1291
  } finally {
1211
1292
  if (!completed) try {
1212
1293
  await result.release?.();
@@ -1430,14 +1511,18 @@ async function fetchOverDuplex(call, request, options = {}) {
1430
1511
  const buffered = new Uint8Array(await request.arrayBuffer());
1431
1512
  if (buffered.byteLength > 0) body = [buffered];
1432
1513
  }
1433
- const { envelope, body: respBody, release } = await httpFetch(call, env, body, options);
1514
+ const { envelope, body: respBody, release, finish } = await httpFetch(call, env, body, {
1515
+ ...options,
1516
+ signal: request.signal ?? void 0
1517
+ });
1434
1518
  const responseInit = {
1435
1519
  status: envelope.status,
1436
1520
  statusText: envelope.statusText,
1437
1521
  headers: forwardableHeaders(envelope.headers)
1438
1522
  };
1439
1523
  if (request.method === "HEAD" || request.method === "OPTIONS" || NULL_BODY_STATUSES.has(envelope.status)) {
1440
- await release?.();
1524
+ if (finish) finish();
1525
+ else await release?.();
1441
1526
  return new Response(null, responseInit);
1442
1527
  }
1443
1528
  return new Response(asyncIterableToReadable(withAbort(respBody, request.signal)), responseInit);
@@ -1452,13 +1537,21 @@ function serveFetchOverDuplex(handler, options = {}) {
1452
1537
  method: env.method,
1453
1538
  headers: forwardableHeaders(env.headers)
1454
1539
  };
1540
+ let upload;
1455
1541
  if (env.method !== "GET" && env.method !== "HEAD") {
1456
1542
  if (supportsRequestStreams()) {
1457
- reqInit.body = asyncIterableToReadable(body);
1543
+ upload = trackingEnd(body);
1544
+ reqInit.body = asyncIterableToReadable(upload.source);
1458
1545
  reqInit.duplex = "half";
1459
1546
  } else reqInit.body = await collectBytes(body);
1460
1547
  }
1461
- const response = await handler(new Request(env.url, reqInit));
1548
+ const request = new Request(env.url, reqInit);
1549
+ const response = await handler(request);
1550
+ const settled = async () => {
1551
+ if (!upload) return;
1552
+ if (!request.bodyUsed && !request.body?.locked) request.body?.cancel().catch(() => {});
1553
+ await upload.settle(options.requestIdleMs ?? 3e4);
1554
+ };
1462
1555
  const respEnv = {
1463
1556
  status: response.status,
1464
1557
  statusText: response.statusText,
@@ -1468,16 +1561,101 @@ function serveFetchOverDuplex(handler, options = {}) {
1468
1561
  await response.body?.cancel();
1469
1562
  return {
1470
1563
  envelope: respEnv,
1471
- body: void 0
1564
+ body: void 0,
1565
+ settled
1472
1566
  };
1473
1567
  }
1474
1568
  return {
1475
1569
  envelope: respEnv,
1476
1570
  body: response.body ? readableToAsyncIterable(response.body) : void 0,
1477
- release: () => response.body?.cancel()
1571
+ release: () => response.body?.cancel(),
1572
+ settled
1478
1573
  };
1479
1574
  }, options);
1480
1575
  }
1576
+ /**
1577
+ * `source`, and `settle(idleMs)`: resolves once it is done — read to its end,
1578
+ * failed, or abandoned (`return()` included before the first `next()`, which a
1579
+ * generator's `finally` would not see) — or once nobody has asked it for a
1580
+ * chunk for `idleMs`, in which case it is abandoned: later reads fail.
1581
+ *
1582
+ * Idle counts only while no `next()` is pending: a reader waiting for a slow
1583
+ * sender is still reading.
1584
+ */
1585
+ function trackingEnd(source) {
1586
+ let end = () => {};
1587
+ let isEnded = false;
1588
+ const ended = new Promise((resolve) => {
1589
+ end = () => {
1590
+ isEnded = true;
1591
+ resolve();
1592
+ };
1593
+ });
1594
+ let abandoned;
1595
+ let pending = 0;
1596
+ let idleSince = Date.now();
1597
+ const it = source[Symbol.asyncIterator]();
1598
+ const abandon = (reason) => {
1599
+ abandoned ??= reason;
1600
+ end();
1601
+ Promise.resolve(it.return?.(void 0)).catch(() => {});
1602
+ };
1603
+ const tracked = {
1604
+ [Symbol.asyncIterator]() {
1605
+ return tracked;
1606
+ },
1607
+ async next() {
1608
+ if (abandoned) throw abandoned;
1609
+ pending++;
1610
+ try {
1611
+ const r = await it.next();
1612
+ if (r.done) end();
1613
+ return r;
1614
+ } catch (error) {
1615
+ end();
1616
+ throw error;
1617
+ } finally {
1618
+ if (--pending === 0) idleSince = Date.now();
1619
+ }
1620
+ },
1621
+ async return(value) {
1622
+ end();
1623
+ try {
1624
+ await it.return?.(value);
1625
+ } catch {}
1626
+ return {
1627
+ done: true,
1628
+ value: void 0
1629
+ };
1630
+ }
1631
+ };
1632
+ asyncDisposable$2(tracked, () => tracked.return?.() ?? Promise.resolve());
1633
+ const settle = (idleMs) => {
1634
+ if (isEnded || !Number.isFinite(idleMs)) return ended;
1635
+ return new Promise((resolve) => {
1636
+ let timer;
1637
+ const check = () => {
1638
+ if (isEnded) return resolve();
1639
+ const idle = pending === 0 ? Date.now() - idleSince : 0;
1640
+ if (idle >= idleMs) {
1641
+ abandon(/* @__PURE__ */ new Error(`webrun-http-streams: the request body was left unread for ${idleMs} ms after the response; abandoned`));
1642
+ return resolve();
1643
+ }
1644
+ timer = setTimeout(check, Math.max(1, Math.min(idleMs - idle, 1e3)));
1645
+ timer.unref?.();
1646
+ };
1647
+ ended.then(() => {
1648
+ clearTimeout(timer);
1649
+ resolve();
1650
+ });
1651
+ check();
1652
+ });
1653
+ };
1654
+ return {
1655
+ source: tracked,
1656
+ settle
1657
+ };
1658
+ }
1481
1659
  async function* withAbort(iter, signal) {
1482
1660
  if (!signal) {
1483
1661
  yield* iter;
@@ -1488,13 +1666,6 @@ async function* withAbort(iter, signal) {
1488
1666
  yield chunk;
1489
1667
  }
1490
1668
  }
1491
- function abortReason$1(signal) {
1492
- const reason = signal.reason;
1493
- if (reason instanceof Error) return reason;
1494
- const err = new Error(reason === void 0 ? "Aborted" : String(reason));
1495
- err.name = "AbortError";
1496
- return err;
1497
- }
1498
1669
  //#endregion
1499
1670
  //#region ../webrun-rpc/dist/index.js
1500
1671
  const closeSignals = /* @__PURE__ */ new WeakMap();
@@ -1649,6 +1820,69 @@ async function* throughAbort(input, signal) {
1649
1820
  }
1650
1821
  }
1651
1822
  /**
1823
+ * Makes `target` disposable with `await using`: installs `dispose` as its
1824
+ * `[Symbol.asyncDispose]`, where the runtime defines the symbol. Hand-written
1825
+ * iterators lose what a native generator gets from `AsyncIteratorPrototype`;
1826
+ * this gives it back. A no-op where the symbol is missing. Kept off the
1827
+ * declared types, which would require consumers to compile with the
1828
+ * `esnext.disposable` lib.
1829
+ */
1830
+ function asyncDisposable$1(target, dispose) {
1831
+ const asyncDispose = Symbol.asyncDispose;
1832
+ if (asyncDispose && !(asyncDispose in target)) Object.defineProperty(target, asyncDispose, {
1833
+ value: async () => {
1834
+ await dispose();
1835
+ },
1836
+ configurable: true,
1837
+ writable: true
1838
+ });
1839
+ return target;
1840
+ }
1841
+ /**
1842
+ * `gen`, except that `return()` and `throw()` take effect at once.
1843
+ *
1844
+ * On an async generator both are queued behind a `next()` that is still
1845
+ * pending: a consumer that gives up on a stream while it waits for the peer's
1846
+ * next chunk — an aborted fetch whose response head has not come, say — cannot
1847
+ * end it until that chunk arrives, which for a peer that never answers is
1848
+ * never. The stream's `finally` (which closes its port and tells the peer)
1849
+ * then never runs, and on a multiplexer the port is kept for good.
1850
+ *
1851
+ * So `abandon` runs first, synchronously: it must make the pending `next()`
1852
+ * settle (abort the stream), which then resolves done, after which the
1853
+ * generator's own `return()` / `throw()` proceeds as usual. On a generator
1854
+ * suspended at a `yield` it is the same ending, a step earlier. `abandon` runs once, whichever comes first.
1855
+ */
1856
+ function returningAtOnce(gen, abandon) {
1857
+ let abandoned = false;
1858
+ const giveUp = (reason) => {
1859
+ if (abandoned) return;
1860
+ abandoned = true;
1861
+ abandon(reason);
1862
+ };
1863
+ const wrapped = {
1864
+ next: (...args) => gen.next(...args).catch((error) => {
1865
+ if (abandoned) return {
1866
+ done: true,
1867
+ value: void 0
1868
+ };
1869
+ throw error;
1870
+ }),
1871
+ return(value) {
1872
+ giveUp();
1873
+ return gen.return(value);
1874
+ },
1875
+ throw(error) {
1876
+ giveUp(error);
1877
+ return gen.throw(error);
1878
+ },
1879
+ [Symbol.asyncIterator]() {
1880
+ return wrapped;
1881
+ }
1882
+ };
1883
+ return asyncDisposable$1(wrapped, () => wrapped.return(void 0));
1884
+ }
1885
+ /**
1652
1886
  * The `type` of the out-of-band notice a side posts when it abandons a stream.
1653
1887
  *
1654
1888
  * Layer 1's `close` is not observable to layer 2 — a closed virtual port drops
@@ -1744,7 +1978,7 @@ function runCallerSide(port, input, options) {
1744
1978
  const inbound = receiveChunks(port, CHANNEL_OUT, controller, clock.touch);
1745
1979
  const pump = sendChunks(port, CHANNEL_IN, input, options, controller.signal, clock.touch);
1746
1980
  pump.catch(() => {});
1747
- return (async function* () {
1981
+ return returningAtOnce((async function* () {
1748
1982
  try {
1749
1983
  yield* inbound.stream;
1750
1984
  } finally {
@@ -1758,7 +1992,9 @@ function runCallerSide(port, input, options) {
1758
1992
  await port.close?.();
1759
1993
  } catch {}
1760
1994
  }
1761
- })();
1995
+ })(), () => {
1996
+ if (!controller.signal.aborted) controller.abort(/* @__PURE__ */ new Error("webrun-rpc: the caller abandoned the stream"));
1997
+ });
1762
1998
  }
1763
1999
  /**
1764
2000
  * Listens for the peer's abort notice, and can post our own. The notice is a
@@ -2154,9 +2390,22 @@ const connect = async ({ mux: factory, timeout }) => {
2154
2390
  maxMessageSize: mux.maxMessageSize,
2155
2391
  timeout
2156
2392
  };
2157
- const call = (input) => (async function* () {
2158
- yield* duplexOverPort(await mux.openPort(STREAM_META), streamOptions)(input);
2159
- })();
2393
+ const call = (input) => {
2394
+ let stream;
2395
+ let abandoned = false;
2396
+ return returningAtOnce((async function* () {
2397
+ const port = await mux.openPort(STREAM_META);
2398
+ if (abandoned) {
2399
+ await port.close?.();
2400
+ return;
2401
+ }
2402
+ stream = duplexOverPort(port, streamOptions)(input);
2403
+ yield* stream;
2404
+ })(), () => {
2405
+ abandoned = true;
2406
+ stream?.return(void 0).catch(() => {});
2407
+ });
2408
+ };
2160
2409
  return {
2161
2410
  call,
2162
2411
  async close() {
@@ -2247,6 +2496,27 @@ const structuredCodec = {
2247
2496
  }
2248
2497
  };
2249
2498
  //#endregion
2499
+ //#region src/link/async-dispose.ts
2500
+ /**
2501
+ * Makes `target` disposable with `await using`: installs `dispose` as its
2502
+ * `[Symbol.asyncDispose]`, where the runtime defines the symbol. Hand-written
2503
+ * iterators lose what a native generator gets from `AsyncIteratorPrototype`;
2504
+ * this gives it back. A no-op where the symbol is missing. Kept off the
2505
+ * declared types, which would require consumers to compile with the
2506
+ * `esnext.disposable` lib.
2507
+ */
2508
+ function asyncDisposable(target, dispose) {
2509
+ const asyncDispose = Symbol.asyncDispose;
2510
+ if (asyncDispose && !(asyncDispose in target)) Object.defineProperty(target, asyncDispose, {
2511
+ value: async () => {
2512
+ await dispose();
2513
+ },
2514
+ configurable: true,
2515
+ writable: true
2516
+ });
2517
+ return target;
2518
+ }
2519
+ //#endregion
2250
2520
  //#region src/link/http-link.ts
2251
2521
  /**
2252
2522
  * The version of the HTTP-over-`MessagePort` protocol this package speaks,
@@ -2276,11 +2546,12 @@ var LinkLostError = class extends Error {
2276
2546
  */
2277
2547
  const LINK_CLOSE = "webrun-http:link-close";
2278
2548
  /** Opens the calling end of a link on `port` (one port per link). */
2279
- async function connectHttpLink(port, { streamTimeout } = {}) {
2549
+ async function connectHttpLink(port, { streamTimeout, maxPorts } = {}) {
2280
2550
  const { call, close } = await connect({
2281
2551
  mux: overPipe(port, {
2282
2552
  codec: structuredCodec,
2283
- side: "initiator"
2553
+ side: "initiator",
2554
+ maxPorts
2284
2555
  }),
2285
2556
  timeout: streamTimeout
2286
2557
  });
@@ -2296,14 +2567,15 @@ async function connectHttpLink(port, { streamTimeout } = {}) {
2296
2567
  * that abandons the requests still running (their handlers see the
2297
2568
  * cancellation) and releases the port.
2298
2569
  */
2299
- async function serveHttpLink(port, handler, { streamTimeout } = {}) {
2570
+ async function serveHttpLink(port, handler, { streamTimeout, maxPorts, requestIdleMs } = {}) {
2300
2571
  const teardown = await serve({
2301
2572
  mux: overPipe(port, {
2302
2573
  codec: structuredCodec,
2303
- side: "responder"
2574
+ side: "responder",
2575
+ maxPorts
2304
2576
  }),
2305
2577
  timeout: streamTimeout
2306
- }, serveFetchOverDuplex(async (request) => await handler(request)));
2578
+ }, serveFetchOverDuplex(async (request) => await handler(request), { requestIdleMs }));
2307
2579
  const onClose = (event) => {
2308
2580
  if (event.data?.type === LINK_CLOSE) close();
2309
2581
  };
@@ -2328,7 +2600,7 @@ function trackedLink(send, release) {
2328
2600
  inflight--;
2329
2601
  if (inflight === 0) for (const resolve of idleWaiters.splice(0)) resolve();
2330
2602
  };
2331
- return {
2603
+ const link = {
2332
2604
  get inflight() {
2333
2605
  return inflight;
2334
2606
  },
@@ -2381,6 +2653,7 @@ function trackedLink(send, release) {
2381
2653
  } catch {}
2382
2654
  }
2383
2655
  };
2656
+ return asyncDisposable(link, () => link.close());
2384
2657
  }
2385
2658
  /**
2386
2659
  * A response body that reports its end (done, cancelled or failed) and fails