@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-worker.js CHANGED
@@ -74,8 +74,6 @@
74
74
  const payload = typeof error === "string" ? { message: error } : error;
75
75
  return Object.assign(new Error(payload.message), payload);
76
76
  }
77
- new TextEncoder();
78
- new TextDecoder();
79
77
  /**
80
78
  * The newAsyncGenerator function creates async generators from callback-based initialization
81
79
  * functions, providing a bridge between imperative event handling and declarative async
@@ -228,7 +226,6 @@
228
226
  drainQueue();
229
227
  }
230
228
  }
231
- new TextEncoder();
232
229
  /**
233
230
  * The iterator ↔ `ReadableStream` boundary.
234
231
  *
@@ -574,8 +571,8 @@
574
571
  const NEWLINE = 10;
575
572
  /** Mirrors the HTTP/1.1 codec's default `maxHeaderBytes`. */
576
573
  const MAX_ENVELOPE_BYTES = 65536;
577
- const encoder$1 = new TextEncoder();
578
- const decoder = new TextDecoder();
574
+ const encoder$1 = /* @__PURE__ */ new TextEncoder();
575
+ const decoder = /* @__PURE__ */ new TextDecoder();
579
576
  /**
580
577
  * Encode an HTTP envelope plus optional body as one continuous byte stream:
581
578
  *
@@ -829,15 +826,15 @@
829
826
  };
830
827
  return { kind: "none" };
831
828
  }
832
- const CRLF = new Uint8Array([13, 10]);
833
- const LAST_CHUNK = new Uint8Array([
829
+ const CRLF = /* @__PURE__ */ new Uint8Array([13, 10]);
830
+ const LAST_CHUNK = /* @__PURE__ */ new Uint8Array([
834
831
  48,
835
832
  13,
836
833
  10,
837
834
  13,
838
835
  10
839
836
  ]);
840
- const encoder = new TextEncoder();
837
+ const encoder = /* @__PURE__ */ new TextEncoder();
841
838
  /**
842
839
  * Chunk sizes are written in HEXADECIMAL — a 21-byte chunk is `15`. Writing
843
840
  * them in decimal is the defect note 15 found in @libp2p/http, and it is the
@@ -1142,7 +1139,7 @@
1142
1139
  decodeResponse: (input, o) => decodeResponse(input, opts, o.method)
1143
1140
  };
1144
1141
  }
1145
- const httpCodec = newHttpCodec();
1142
+ const httpCodec = /* @__PURE__ */ newHttpCodec();
1146
1143
  /**
1147
1144
  * Dispatches on byte 0. The formats are self-identifying — a JSON envelope
1148
1145
  * always begins `{`, which is not a token character and so can never begin an
@@ -1192,10 +1189,29 @@
1192
1189
  * Writes HTTP/1.1; accepts HTTP/1.1 or the legacy JSON envelope. Used whenever
1193
1190
  * no `codec` option is supplied.
1194
1191
  */
1195
- const defaultCodec = newSniffingCodec({
1192
+ const defaultCodec = /* @__PURE__ */ newSniffingCodec({
1196
1193
  write: httpCodec,
1197
1194
  accept: [httpCodec, jsonEnvelopeCodec]
1198
1195
  });
1196
+ /**
1197
+ * Makes `target` disposable with `await using`: installs `dispose` as its
1198
+ * `[Symbol.asyncDispose]`, where the runtime defines the symbol. Hand-written
1199
+ * iterators lose what a native generator gets from `AsyncIteratorPrototype`;
1200
+ * this gives it back. A no-op where the symbol is missing. Kept off the
1201
+ * declared types, which would require consumers to compile with the
1202
+ * `esnext.disposable` lib.
1203
+ */
1204
+ function asyncDisposable$2(target, dispose) {
1205
+ const asyncDispose = Symbol.asyncDispose;
1206
+ if (asyncDispose && !(asyncDispose in target)) Object.defineProperty(target, asyncDispose, {
1207
+ value: async () => {
1208
+ await dispose();
1209
+ },
1210
+ configurable: true,
1211
+ writable: true
1212
+ });
1213
+ return target;
1214
+ }
1199
1215
  /** Carries the serialized JS error between two webrun peers. */
1200
1216
  const PEER_ERROR_HEADER = "x-webrun-error";
1201
1217
  /** Hard cap on the peer-error header (M3): see `encodeErrorResponse`. */
@@ -1273,41 +1289,105 @@
1273
1289
  */
1274
1290
  async function httpFetch(call, env, body, options = {}) {
1275
1291
  const codec = options.codec ?? defaultCodec;
1292
+ const { signal } = options;
1293
+ if (signal?.aborted) throw abortReason$1(signal);
1276
1294
  const output = call(codec.encodeRequest(env, body));
1277
- const decoded = await codec.decodeResponse(output, { method: env.method });
1278
- const release = async () => {
1279
- try {
1280
- await output.return?.(void 0);
1281
- } catch {}
1295
+ let released;
1296
+ const release = () => {
1297
+ released ??= (async () => {
1298
+ try {
1299
+ await output.return?.(void 0);
1300
+ } catch {}
1301
+ })();
1302
+ return released;
1282
1303
  };
1304
+ let finished = false;
1305
+ const finish = () => {
1306
+ if (finished || released) return;
1307
+ finished = true;
1308
+ if (signal?.aborted) {
1309
+ release();
1310
+ return;
1311
+ }
1312
+ const onAbort = () => void release();
1313
+ signal?.addEventListener("abort", onAbort, { once: true });
1314
+ drain(output).finally(() => signal?.removeEventListener("abort", onAbort));
1315
+ };
1316
+ let decoded;
1317
+ try {
1318
+ decoded = await untilAborted(codec.decodeResponse(output, { method: env.method }), signal);
1319
+ } catch (error) {
1320
+ if (signal?.aborted) release();
1321
+ else await release();
1322
+ throw error;
1323
+ }
1283
1324
  const result = {
1284
1325
  envelope: decoded.envelope,
1285
- body: releasingOnExit(decoded.body, release),
1286
- release
1326
+ body: releasingOnExit(decoded.body, finish, release),
1327
+ release,
1328
+ finish
1287
1329
  };
1288
1330
  await throwIfPeerError(result, output);
1289
1331
  return result;
1290
1332
  }
1333
+ /** `promise`, rejected with the signal's reason if it aborts first. */
1334
+ function untilAborted(promise, signal) {
1335
+ if (!signal) return promise;
1336
+ return new Promise((resolve, reject) => {
1337
+ const onAbort = () => reject(abortReason$1(signal));
1338
+ if (signal.aborted) return onAbort();
1339
+ signal.addEventListener("abort", onAbort, { once: true });
1340
+ promise.then((value) => {
1341
+ signal.removeEventListener("abort", onAbort);
1342
+ resolve(value);
1343
+ }, (error) => {
1344
+ signal.removeEventListener("abort", onAbort);
1345
+ reject(error);
1346
+ });
1347
+ });
1348
+ }
1349
+ /** The signal's reason as an `Error` (`AbortError` when it gave none). */
1350
+ function abortReason$1(signal) {
1351
+ const reason = signal.reason;
1352
+ if (reason instanceof Error) return reason;
1353
+ const err = new Error(reason === void 0 ? "Aborted" : String(reason));
1354
+ err.name = "AbortError";
1355
+ return err;
1356
+ }
1291
1357
  /**
1292
- * `body`, but leaving it before its end releases the call.
1358
+ * `body`, ending the call however it is left: read to its end, or left early.
1293
1359
  *
1294
1360
  * The body the codec hands back is a generator of its own over a `ByteReader`,
1295
- * and `.return()` on it never reaches the call's `output` (see
1296
- * `throwIfPeerError` for why the codec must not do that itself). So a caller
1297
- * that stopped reading — an aborted `fetch`, a cancelled response stream —
1298
- * left the peer's producer parked forever, holding its handler open. Safe here
1299
- * for the same reason it is safe in `throwIfPeerError`: `output` is this
1300
- * call's own response-only generator, and the request was fully sent.
1301
- */
1302
- async function* releasingOnExit(body, release) {
1361
+ * and its ending never reaches the call's `output` (see `throwIfPeerError` for
1362
+ * why the codec must not do that itself). A caller that stopped reading — an
1363
+ * aborted `fetch`, a cancelled response stream — left the peer's producer
1364
+ * parked forever; and one that read the body to its end left the call itself
1365
+ * open, which on a multiplexed transport is one port per exchange until the
1366
+ * multiplexer refuses to open more (`maxPorts`).
1367
+ *
1368
+ * Left early, the call is abandoned (`.return()`): the peer's producer is
1369
+ * released. Read to its end, the call is not cut short but read to ITS end, in
1370
+ * the background: the peer ends it once its side of the exchange is over,
1371
+ * which may be later than the response — a handler that answered before it
1372
+ * read the request body is still receiving it (`HttpDataHandlerResult.settled`),
1373
+ * and abandoning the call would abort that upload.
1374
+ */
1375
+ async function* releasingOnExit(body, finish, release) {
1303
1376
  let completed = false;
1304
1377
  try {
1305
1378
  yield* body;
1306
1379
  completed = true;
1307
1380
  } finally {
1308
- if (!completed) await release();
1381
+ if (completed) finish();
1382
+ else await release();
1309
1383
  }
1310
1384
  }
1385
+ /** Reads `output` to its end, discarding what is left, ignoring a failure. */
1386
+ async function drain(output) {
1387
+ try {
1388
+ for (let r = await output.next(); !r.done; r = await output.next());
1389
+ } catch {}
1390
+ }
1311
1391
  /**
1312
1392
  * Wrap an HTTP handler as a `Duplex` so it can be registered with any
1313
1393
  * `webrun-streams-*` adapter's `serve`.
@@ -1335,6 +1415,7 @@
1335
1415
  try {
1336
1416
  yield* replyCodec.encodeResponse(result.envelope, result.body, { method: decoded.envelope.method });
1337
1417
  completed = true;
1418
+ await result.settled?.();
1338
1419
  } finally {
1339
1420
  if (!completed) try {
1340
1421
  await result.release?.();
@@ -1558,14 +1639,18 @@
1558
1639
  const buffered = new Uint8Array(await request.arrayBuffer());
1559
1640
  if (buffered.byteLength > 0) body = [buffered];
1560
1641
  }
1561
- const { envelope, body: respBody, release } = await httpFetch(call, env, body, options);
1642
+ const { envelope, body: respBody, release, finish } = await httpFetch(call, env, body, {
1643
+ ...options,
1644
+ signal: request.signal ?? void 0
1645
+ });
1562
1646
  const responseInit = {
1563
1647
  status: envelope.status,
1564
1648
  statusText: envelope.statusText,
1565
1649
  headers: forwardableHeaders(envelope.headers)
1566
1650
  };
1567
1651
  if (request.method === "HEAD" || request.method === "OPTIONS" || NULL_BODY_STATUSES.has(envelope.status)) {
1568
- await release?.();
1652
+ if (finish) finish();
1653
+ else await release?.();
1569
1654
  return new Response(null, responseInit);
1570
1655
  }
1571
1656
  return new Response(asyncIterableToReadable(withAbort(respBody, request.signal)), responseInit);
@@ -1580,13 +1665,21 @@
1580
1665
  method: env.method,
1581
1666
  headers: forwardableHeaders(env.headers)
1582
1667
  };
1668
+ let upload;
1583
1669
  if (env.method !== "GET" && env.method !== "HEAD") {
1584
1670
  if (supportsRequestStreams()) {
1585
- reqInit.body = asyncIterableToReadable(body);
1671
+ upload = trackingEnd(body);
1672
+ reqInit.body = asyncIterableToReadable(upload.source);
1586
1673
  reqInit.duplex = "half";
1587
1674
  } else reqInit.body = await collectBytes(body);
1588
1675
  }
1589
- const response = await handler(new Request(env.url, reqInit));
1676
+ const request = new Request(env.url, reqInit);
1677
+ const response = await handler(request);
1678
+ const settled = async () => {
1679
+ if (!upload) return;
1680
+ if (!request.bodyUsed && !request.body?.locked) request.body?.cancel().catch(() => {});
1681
+ await upload.settle(options.requestIdleMs ?? 3e4);
1682
+ };
1590
1683
  const respEnv = {
1591
1684
  status: response.status,
1592
1685
  statusText: response.statusText,
@@ -1596,16 +1689,101 @@
1596
1689
  await response.body?.cancel();
1597
1690
  return {
1598
1691
  envelope: respEnv,
1599
- body: void 0
1692
+ body: void 0,
1693
+ settled
1600
1694
  };
1601
1695
  }
1602
1696
  return {
1603
1697
  envelope: respEnv,
1604
1698
  body: response.body ? readableToAsyncIterable(response.body) : void 0,
1605
- release: () => response.body?.cancel()
1699
+ release: () => response.body?.cancel(),
1700
+ settled
1606
1701
  };
1607
1702
  }, options);
1608
1703
  }
1704
+ /**
1705
+ * `source`, and `settle(idleMs)`: resolves once it is done — read to its end,
1706
+ * failed, or abandoned (`return()` included before the first `next()`, which a
1707
+ * generator's `finally` would not see) — or once nobody has asked it for a
1708
+ * chunk for `idleMs`, in which case it is abandoned: later reads fail.
1709
+ *
1710
+ * Idle counts only while no `next()` is pending: a reader waiting for a slow
1711
+ * sender is still reading.
1712
+ */
1713
+ function trackingEnd(source) {
1714
+ let end = () => {};
1715
+ let isEnded = false;
1716
+ const ended = new Promise((resolve) => {
1717
+ end = () => {
1718
+ isEnded = true;
1719
+ resolve();
1720
+ };
1721
+ });
1722
+ let abandoned;
1723
+ let pending = 0;
1724
+ let idleSince = Date.now();
1725
+ const it = source[Symbol.asyncIterator]();
1726
+ const abandon = (reason) => {
1727
+ abandoned ??= reason;
1728
+ end();
1729
+ Promise.resolve(it.return?.(void 0)).catch(() => {});
1730
+ };
1731
+ const tracked = {
1732
+ [Symbol.asyncIterator]() {
1733
+ return tracked;
1734
+ },
1735
+ async next() {
1736
+ if (abandoned) throw abandoned;
1737
+ pending++;
1738
+ try {
1739
+ const r = await it.next();
1740
+ if (r.done) end();
1741
+ return r;
1742
+ } catch (error) {
1743
+ end();
1744
+ throw error;
1745
+ } finally {
1746
+ if (--pending === 0) idleSince = Date.now();
1747
+ }
1748
+ },
1749
+ async return(value) {
1750
+ end();
1751
+ try {
1752
+ await it.return?.(value);
1753
+ } catch {}
1754
+ return {
1755
+ done: true,
1756
+ value: void 0
1757
+ };
1758
+ }
1759
+ };
1760
+ asyncDisposable$2(tracked, () => tracked.return?.() ?? Promise.resolve());
1761
+ const settle = (idleMs) => {
1762
+ if (isEnded || !Number.isFinite(idleMs)) return ended;
1763
+ return new Promise((resolve) => {
1764
+ let timer;
1765
+ const check = () => {
1766
+ if (isEnded) return resolve();
1767
+ const idle = pending === 0 ? Date.now() - idleSince : 0;
1768
+ if (idle >= idleMs) {
1769
+ abandon(/* @__PURE__ */ new Error(`webrun-http-streams: the request body was left unread for ${idleMs} ms after the response; abandoned`));
1770
+ return resolve();
1771
+ }
1772
+ timer = setTimeout(check, Math.max(1, Math.min(idleMs - idle, 1e3)));
1773
+ timer.unref?.();
1774
+ };
1775
+ ended.then(() => {
1776
+ clearTimeout(timer);
1777
+ resolve();
1778
+ });
1779
+ check();
1780
+ });
1781
+ };
1782
+ return {
1783
+ source: tracked,
1784
+ settle
1785
+ };
1786
+ }
1609
1787
  async function* withAbort(iter, signal) {
1610
1788
  if (!signal) {
1611
1789
  yield* iter;
@@ -1616,13 +1794,6 @@
1616
1794
  yield chunk;
1617
1795
  }
1618
1796
  }
1619
- function abortReason$1(signal) {
1620
- const reason = signal.reason;
1621
- if (reason instanceof Error) return reason;
1622
- const err = new Error(reason === void 0 ? "Aborted" : String(reason));
1623
- err.name = "AbortError";
1624
- return err;
1625
- }
1626
1797
  //#endregion
1627
1798
  //#region src/core/data-channels.ts
1628
1799
  const MESSAGE_TYPE_REQUEST = "REQUEST";
@@ -1980,6 +2151,69 @@
1980
2151
  }
1981
2152
  }
1982
2153
  /**
2154
+ * Makes `target` disposable with `await using`: installs `dispose` as its
2155
+ * `[Symbol.asyncDispose]`, where the runtime defines the symbol. Hand-written
2156
+ * iterators lose what a native generator gets from `AsyncIteratorPrototype`;
2157
+ * this gives it back. A no-op where the symbol is missing. Kept off the
2158
+ * declared types, which would require consumers to compile with the
2159
+ * `esnext.disposable` lib.
2160
+ */
2161
+ function asyncDisposable$1(target, dispose) {
2162
+ const asyncDispose = Symbol.asyncDispose;
2163
+ if (asyncDispose && !(asyncDispose in target)) Object.defineProperty(target, asyncDispose, {
2164
+ value: async () => {
2165
+ await dispose();
2166
+ },
2167
+ configurable: true,
2168
+ writable: true
2169
+ });
2170
+ return target;
2171
+ }
2172
+ /**
2173
+ * `gen`, except that `return()` and `throw()` take effect at once.
2174
+ *
2175
+ * On an async generator both are queued behind a `next()` that is still
2176
+ * pending: a consumer that gives up on a stream while it waits for the peer's
2177
+ * next chunk — an aborted fetch whose response head has not come, say — cannot
2178
+ * end it until that chunk arrives, which for a peer that never answers is
2179
+ * never. The stream's `finally` (which closes its port and tells the peer)
2180
+ * then never runs, and on a multiplexer the port is kept for good.
2181
+ *
2182
+ * So `abandon` runs first, synchronously: it must make the pending `next()`
2183
+ * settle (abort the stream), which then resolves done, after which the
2184
+ * generator's own `return()` / `throw()` proceeds as usual. On a generator
2185
+ * suspended at a `yield` it is the same ending, a step earlier. `abandon` runs once, whichever comes first.
2186
+ */
2187
+ function returningAtOnce(gen, abandon) {
2188
+ let abandoned = false;
2189
+ const giveUp = (reason) => {
2190
+ if (abandoned) return;
2191
+ abandoned = true;
2192
+ abandon(reason);
2193
+ };
2194
+ const wrapped = {
2195
+ next: (...args) => gen.next(...args).catch((error) => {
2196
+ if (abandoned) return {
2197
+ done: true,
2198
+ value: void 0
2199
+ };
2200
+ throw error;
2201
+ }),
2202
+ return(value) {
2203
+ giveUp();
2204
+ return gen.return(value);
2205
+ },
2206
+ throw(error) {
2207
+ giveUp(error);
2208
+ return gen.throw(error);
2209
+ },
2210
+ [Symbol.asyncIterator]() {
2211
+ return wrapped;
2212
+ }
2213
+ };
2214
+ return asyncDisposable$1(wrapped, () => wrapped.return(void 0));
2215
+ }
2216
+ /**
1983
2217
  * The `type` of the out-of-band notice a side posts when it abandons a stream.
1984
2218
  *
1985
2219
  * Layer 1's `close` is not observable to layer 2 — a closed virtual port drops
@@ -2075,7 +2309,7 @@
2075
2309
  const inbound = receiveChunks(port, CHANNEL_OUT, controller, clock.touch);
2076
2310
  const pump = sendChunks(port, CHANNEL_IN, input, options, controller.signal, clock.touch);
2077
2311
  pump.catch(() => {});
2078
- return (async function* () {
2312
+ return returningAtOnce((async function* () {
2079
2313
  try {
2080
2314
  yield* inbound.stream;
2081
2315
  } finally {
@@ -2089,7 +2323,9 @@
2089
2323
  await port.close?.();
2090
2324
  } catch {}
2091
2325
  }
2092
- })();
2326
+ })(), () => {
2327
+ if (!controller.signal.aborted) controller.abort(/* @__PURE__ */ new Error("webrun-rpc: the caller abandoned the stream"));
2328
+ });
2093
2329
  }
2094
2330
  /**
2095
2331
  * Listens for the peer's abort notice, and can post our own. The notice is a
@@ -2485,9 +2721,22 @@
2485
2721
  maxMessageSize: mux.maxMessageSize,
2486
2722
  timeout
2487
2723
  };
2488
- const call = (input) => (async function* () {
2489
- yield* duplexOverPort(await mux.openPort(STREAM_META), streamOptions)(input);
2490
- })();
2724
+ const call = (input) => {
2725
+ let stream;
2726
+ let abandoned = false;
2727
+ return returningAtOnce((async function* () {
2728
+ const port = await mux.openPort(STREAM_META);
2729
+ if (abandoned) {
2730
+ await port.close?.();
2731
+ return;
2732
+ }
2733
+ stream = duplexOverPort(port, streamOptions)(input);
2734
+ yield* stream;
2735
+ })(), () => {
2736
+ abandoned = true;
2737
+ stream?.return(void 0).catch(() => {});
2738
+ });
2739
+ };
2491
2740
  return {
2492
2741
  call,
2493
2742
  async close() {
@@ -2578,6 +2827,27 @@
2578
2827
  }
2579
2828
  };
2580
2829
  //#endregion
2830
+ //#region src/link/async-dispose.ts
2831
+ /**
2832
+ * Makes `target` disposable with `await using`: installs `dispose` as its
2833
+ * `[Symbol.asyncDispose]`, where the runtime defines the symbol. Hand-written
2834
+ * iterators lose what a native generator gets from `AsyncIteratorPrototype`;
2835
+ * this gives it back. A no-op where the symbol is missing. Kept off the
2836
+ * declared types, which would require consumers to compile with the
2837
+ * `esnext.disposable` lib.
2838
+ */
2839
+ function asyncDisposable(target, dispose) {
2840
+ const asyncDispose = Symbol.asyncDispose;
2841
+ if (asyncDispose && !(asyncDispose in target)) Object.defineProperty(target, asyncDispose, {
2842
+ value: async () => {
2843
+ await dispose();
2844
+ },
2845
+ configurable: true,
2846
+ writable: true
2847
+ });
2848
+ return target;
2849
+ }
2850
+ //#endregion
2581
2851
  //#region src/link/http-link.ts
2582
2852
  /**
2583
2853
  * The version of the HTTP-over-`MessagePort` protocol this package speaks,
@@ -2607,11 +2877,12 @@
2607
2877
  */
2608
2878
  const LINK_CLOSE = "webrun-http:link-close";
2609
2879
  /** Opens the calling end of a link on `port` (one port per link). */
2610
- async function connectHttpLink(port, { streamTimeout } = {}) {
2880
+ async function connectHttpLink(port, { streamTimeout, maxPorts } = {}) {
2611
2881
  const { call, close } = await connect({
2612
2882
  mux: overPipe(port, {
2613
2883
  codec: structuredCodec,
2614
- side: "initiator"
2884
+ side: "initiator",
2885
+ maxPorts
2615
2886
  }),
2616
2887
  timeout: streamTimeout
2617
2888
  });
@@ -2627,14 +2898,15 @@
2627
2898
  * that abandons the requests still running (their handlers see the
2628
2899
  * cancellation) and releases the port.
2629
2900
  */
2630
- async function serveHttpLink(port, handler, { streamTimeout } = {}) {
2901
+ async function serveHttpLink(port, handler, { streamTimeout, maxPorts, requestIdleMs } = {}) {
2631
2902
  const teardown = await serve({
2632
2903
  mux: overPipe(port, {
2633
2904
  codec: structuredCodec,
2634
- side: "responder"
2905
+ side: "responder",
2906
+ maxPorts
2635
2907
  }),
2636
2908
  timeout: streamTimeout
2637
- }, serveFetchOverDuplex(async (request) => await handler(request)));
2909
+ }, serveFetchOverDuplex(async (request) => await handler(request), { requestIdleMs }));
2638
2910
  const onClose = (event) => {
2639
2911
  if (event.data?.type === LINK_CLOSE) close();
2640
2912
  };
@@ -2659,7 +2931,7 @@
2659
2931
  inflight--;
2660
2932
  if (inflight === 0) for (const resolve of idleWaiters.splice(0)) resolve();
2661
2933
  };
2662
- return {
2934
+ const link = {
2663
2935
  get inflight() {
2664
2936
  return inflight;
2665
2937
  },
@@ -2712,6 +2984,7 @@
2712
2984
  } catch {}
2713
2985
  }
2714
2986
  };
2987
+ return asyncDisposable(link, () => link.close());
2715
2988
  }
2716
2989
  /**
2717
2990
  * A response body that reports its end (done, cancelled or failed) and fails