@statewalker/webrun-http-browser 0.12.1 → 0.12.3

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/index.js CHANGED
@@ -1747,7 +1747,7 @@ const LAST_CHUNK = /* @__PURE__ */ new Uint8Array([
1747
1747
  13,
1748
1748
  10
1749
1749
  ]);
1750
- const encoder = /* @__PURE__ */ new TextEncoder();
1750
+ const encoder$2 = /* @__PURE__ */ new TextEncoder();
1751
1751
  /**
1752
1752
  * Chunk sizes are written in HEXADECIMAL — a 21-byte chunk is `15`. Writing
1753
1753
  * them in decimal is the defect note 15 found in @libp2p/http, and it is the
@@ -1760,7 +1760,7 @@ const encoder = /* @__PURE__ */ new TextEncoder();
1760
1760
  async function* encodeChunked(body) {
1761
1761
  for await (const chunk of body) {
1762
1762
  if (chunk.byteLength === 0) continue;
1763
- yield encoder.encode(`${chunk.byteLength.toString(16)}\r\n`);
1763
+ yield encoder$2.encode(`${chunk.byteLength.toString(16)}\r\n`);
1764
1764
  yield chunk;
1765
1765
  yield CRLF;
1766
1766
  }
@@ -2230,7 +2230,10 @@ async function httpFetch(call, env, body, options = {}) {
2230
2230
  }
2231
2231
  const onAbort = () => void release();
2232
2232
  signal?.addEventListener("abort", onAbort, { once: true });
2233
- drain(output).finally(() => signal?.removeEventListener("abort", onAbort));
2233
+ options.keepAlive;
2234
+ drain(output).finally(() => {
2235
+ signal?.removeEventListener("abort", onAbort);
2236
+ });
2234
2237
  };
2235
2238
  let decoded;
2236
2239
  try {
@@ -2626,7 +2629,8 @@ async function fetchOverDuplex(call, request, options = {}) {
2626
2629
  }
2627
2630
  const { envelope, body: respBody, release, finish } = await httpFetch(call, env, body, {
2628
2631
  ...options,
2629
- signal: request.signal ?? void 0
2632
+ signal: request.signal ?? void 0,
2633
+ keepAlive: request
2630
2634
  });
2631
2635
  const responseInit = {
2632
2636
  status: envelope.status,
@@ -2638,7 +2642,7 @@ async function fetchOverDuplex(call, request, options = {}) {
2638
2642
  else await release?.();
2639
2643
  return new Response(null, responseInit);
2640
2644
  }
2641
- return new Response(responseBody(respBody, request.signal, release), responseInit);
2645
+ return new Response(responseBody(respBody, request, release), responseInit);
2642
2646
  }
2643
2647
  /**
2644
2648
  * Wrap a `(Request) => Promise<Response>` handler as a `Duplex` so it can be
@@ -2791,7 +2795,9 @@ const CANCEL_GRACE_MS = 1e3;
2791
2795
  * A cancel abandons the call too — unless the read in flight comes back with the
2792
2796
  * body's end, which finishes it instead (see `cancel`).
2793
2797
  */
2794
- function responseBody(body, signal, release) {
2798
+ function responseBody(body, request, release) {
2799
+ const keep = { request };
2800
+ const signal = request.signal ?? void 0;
2795
2801
  const it = body[Symbol.asyncIterator]();
2796
2802
  let controller;
2797
2803
  let ended = false;
@@ -2799,6 +2805,7 @@ function responseBody(body, signal, release) {
2799
2805
  const end = () => {
2800
2806
  ended = true;
2801
2807
  signal?.removeEventListener("abort", onAbort);
2808
+ keep.request = void 0;
2802
2809
  };
2803
2810
  const onAbort = () => {
2804
2811
  if (ended) return;
@@ -3331,7 +3338,7 @@ const STREAM_ABORT = "webrun-rpc:stream-abort";
3331
3338
  * request, sent only on a port where both ends agreed `pipeline`. The window's
3332
3339
  * credit is its confirmation.
3333
3340
  */
3334
- const CHUNK = "webrun-rpc:chunk";
3341
+ const CHUNK$1 = "webrun-rpc:chunk";
3335
3342
  /**
3336
3343
  * What a pipelined chunk counts beyond its bytes, against the window, on the
3337
3344
  * receiving side. Both codecs' windows count more for the chunk's envelope
@@ -3662,7 +3669,7 @@ function receiveChunks(port, channelName, controller, touch, notice) {
3662
3669
  const watcher = (event) => {
3663
3670
  const data = event.data;
3664
3671
  if (data?.channelName !== channelName) return;
3665
- const chunk = data.type === CHUNK ? data.chunk : data.type === "request" ? data.params : void 0;
3672
+ const chunk = data.type === CHUNK$1 ? data.chunk : data.type === "request" ? data.params : void 0;
3666
3673
  if (!chunk) return;
3667
3674
  if (!watchPeerEnd()) {
3668
3675
  port.removeEventListener("message", watcher);
@@ -3697,7 +3704,7 @@ function receiveChunks(port, channelName, controller, touch, notice) {
3697
3704
  }, { channelName });
3698
3705
  const onChunk = (event) => {
3699
3706
  const data = event.data;
3700
- if (data?.type !== CHUNK || data.channelName !== channelName || !data.chunk) return;
3707
+ if (data?.type !== CHUNK$1 || data.channelName !== channelName || !data.chunk) return;
3701
3708
  if (poison) return;
3702
3709
  const agreement = portAgreement(port);
3703
3710
  if (!agreement?.caps.has("pipeline")) {
@@ -3789,12 +3796,6 @@ async function sendChunks(port, channelName, output, { maxMessageSize, log }, si
3789
3796
  const producer = output;
3790
3797
  const returnProducer = () => void Promise.resolve(producer.return?.()).catch(() => {});
3791
3798
  if (framed !== output) signal.addEventListener("abort", returnProducer, { once: true });
3792
- let onAborted = () => {};
3793
- const aborted = new Promise((resolve) => {
3794
- onAborted = () => resolve();
3795
- });
3796
- if (signal.aborted) onAborted();
3797
- else signal.addEventListener("abort", onAborted, { once: true });
3798
3799
  try {
3799
3800
  await sendIterator(async ({ done, value, error }) => {
3800
3801
  if (signal.aborted) return;
@@ -3810,13 +3811,13 @@ async function sendChunks(port, channelName, output, { maxMessageSize, log }, si
3810
3811
  });
3811
3812
  if (mayPipeline(port)) {
3812
3813
  port.postMessage({
3813
- type: CHUNK,
3814
+ type: CHUNK$1,
3814
3815
  channelName,
3815
3816
  chunk
3816
3817
  });
3817
3818
  touch();
3818
3819
  if (done && !error) onSent();
3819
- if (portBufferedAmount(port) > 0) await Promise.race([whenPortReady(port), aborted]);
3820
+ if (portBufferedAmount(port) > 0) await whenReadyOrAborted(port, signal);
3820
3821
  return;
3821
3822
  }
3822
3823
  await callPort(port, chunk, {
@@ -3832,10 +3833,25 @@ async function sendChunks(port, channelName, output, { maxMessageSize, log }, si
3832
3833
  throw err;
3833
3834
  } finally {
3834
3835
  signal.removeEventListener("abort", returnProducer);
3835
- signal.removeEventListener("abort", onAborted);
3836
3836
  }
3837
3837
  }
3838
3838
  /**
3839
+ * A pipelined sender waits only for room in the window, or for the abort. The
3840
+ * abort listener lives for this one wait: a promise held for the whole stream
3841
+ * would keep one reaction per wait until the stream ends.
3842
+ */
3843
+ function whenReadyOrAborted(port, signal) {
3844
+ if (signal.aborted) return Promise.resolve();
3845
+ return new Promise((resolve) => {
3846
+ const settle = () => {
3847
+ signal.removeEventListener("abort", settle);
3848
+ resolve();
3849
+ };
3850
+ signal.addEventListener("abort", settle, { once: true });
3851
+ whenPortReady(port).then(settle, settle);
3852
+ });
3853
+ }
3854
+ /**
3839
3855
  * One virtual port.
3840
3856
  *
3841
3857
  * `deliver` and `markClosed` are deliberately not on `port`: the consumer holds
@@ -4833,7 +4849,12 @@ function trackedLink(send, release) {
4833
4849
  finish();
4834
4850
  return response;
4835
4851
  }
4836
- return new Response(guardBody(response.body, lost, () => lostReason, finish, request.signal), {
4852
+ failers.delete(fail);
4853
+ const body = guardBody(response.body, finish, request);
4854
+ fail = body.fail;
4855
+ if (closed) fail(lostReason);
4856
+ else if (!finished) failers.add(fail);
4857
+ return new Response(body.stream, {
4837
4858
  status: response.status,
4838
4859
  statusText: response.statusText,
4839
4860
  headers: response.headers
@@ -4852,56 +4873,62 @@ function trackedLink(send, release) {
4852
4873
  return asyncDisposable(link, () => link.close());
4853
4874
  }
4854
4875
  /**
4855
- * A response body that reports its end (done, cancelled, failed or aborted) and
4856
- * fails with the link's reason when the link is lost mid-stream. An abort of the
4857
- * request's signal ends it at once, read or not: the exchange is over (the layer
4858
- * below has given its port back), so the request leaves `inflight` then.
4876
+ * A response body that reports its end (done, cancelled, failed or aborted).
4877
+ * `fail(error)` ends it with `error` at once — the link calls it when it is lost
4878
+ * mid-stream. An abort of the request's signal does the same with the signal's
4879
+ * reason, read or not: the exchange is over (the layer below has given its port
4880
+ * back), so the request leaves `inflight` then. A read waits on the reader
4881
+ * alone, so nothing outlives the chunk it brings.
4859
4882
  */
4860
- function guardBody(body, lost, reason, finish, signal) {
4883
+ function guardBody(body, finish, request) {
4884
+ const keep = { request };
4885
+ const signal = request.signal ?? void 0;
4861
4886
  const reader = body.getReader();
4862
4887
  let controller;
4863
4888
  let ended = false;
4864
4889
  const end = () => {
4865
4890
  ended = true;
4866
4891
  signal?.removeEventListener("abort", onAbort);
4892
+ keep.request = void 0;
4867
4893
  finish();
4868
4894
  };
4869
- const onAbort = () => {
4895
+ const fail = (error) => {
4870
4896
  if (ended) return;
4871
4897
  end();
4872
4898
  reader.cancel().catch(() => {});
4873
4899
  try {
4874
- controller.error(abortError(signal));
4900
+ controller.error(error);
4875
4901
  } catch {}
4876
4902
  };
4877
- return new ReadableStream({
4878
- start(c) {
4879
- controller = c;
4880
- if (signal?.aborted) onAbort();
4881
- else signal?.addEventListener("abort", onAbort, { once: true });
4882
- },
4883
- async pull(controller) {
4884
- try {
4885
- const { done, value } = await Promise.race([reader.read(), lost]);
4886
- if (ended) return;
4887
- if (done) {
4888
- end();
4889
- controller.close();
4890
- return;
4903
+ const onAbort = () => fail(abortError(signal));
4904
+ return {
4905
+ stream: new ReadableStream({
4906
+ start(c) {
4907
+ controller = c;
4908
+ if (signal?.aborted) onAbort();
4909
+ else signal?.addEventListener("abort", onAbort, { once: true });
4910
+ },
4911
+ async pull(controller) {
4912
+ try {
4913
+ const { done, value } = await reader.read();
4914
+ if (ended) return;
4915
+ if (done) {
4916
+ end();
4917
+ controller.close();
4918
+ return;
4919
+ }
4920
+ controller.enqueue(value);
4921
+ } catch (error) {
4922
+ fail(error);
4891
4923
  }
4892
- controller.enqueue(value);
4893
- } catch (error) {
4894
- if (ended) return;
4924
+ },
4925
+ cancel(why) {
4895
4926
  end();
4896
- reader.cancel().catch(() => {});
4897
- controller.error(reason() ?? error);
4927
+ setTimeout(() => void reader.cancel(why).catch(() => {}), 0);
4898
4928
  }
4899
- },
4900
- async cancel(why) {
4901
- end();
4902
- await reader.cancel(why).catch(() => {});
4903
- }
4904
- });
4929
+ }),
4930
+ fail
4931
+ };
4905
4932
  }
4906
4933
  /** The signal's reason as an `Error` (`AbortError` when it gave none). */
4907
4934
  function abortError(signal) {
@@ -5104,20 +5131,24 @@ function postFrame(port, frame) {
5104
5131
  function messageSize(data) {
5105
5132
  return typeof data === "string" ? utf8Length(data) : data.byteLength;
5106
5133
  }
5107
- /** The UTF-8 length of `text`, without encoding it. */
5134
+ const encoder = new TextEncoder();
5135
+ /** Characters encoded per `encodeInto` call: at most 3 bytes each, so the scratch stays at 96 KiB. */
5136
+ const CHUNK = 32768;
5137
+ const scratch = new Uint8Array(CHUNK * 3);
5138
+ /**
5139
+ * The UTF-8 length of `text`, as `TextEncoder` would encode it (a lone surrogate counts as the 3
5140
+ * bytes of U+FFFD). Every message is measured on both ends (`bufferedAmount` and the byte acks), so
5141
+ * this runs natively: `encodeInto` a reused buffer, a chunk at a time, never splitting a surrogate
5142
+ * pair across two chunks. A loop over the characters in JavaScript took about 30 times as long.
5143
+ */
5108
5144
  function utf8Length(text) {
5109
5145
  let length = 0;
5110
- for (let i = 0; i < text.length; i++) {
5111
- const code = text.charCodeAt(i);
5112
- if (code < 128) length += 1;
5113
- else if (code < 2048) length += 2;
5114
- else if (code >= 55296 && code <= 56319 && i + 1 < text.length) {
5115
- const next = text.charCodeAt(i + 1);
5116
- if (next >= 56320 && next <= 57343) {
5117
- length += 4;
5118
- i++;
5119
- } else length += 3;
5120
- } else length += 3;
5146
+ for (let start = 0; start < text.length;) {
5147
+ let end = Math.min(start + CHUNK, text.length);
5148
+ const last = text.charCodeAt(end - 1);
5149
+ if (end < text.length && last >= 55296 && last <= 56319) end--;
5150
+ length += encoder.encodeInto(text.slice(start, end), scratch).written;
5151
+ start = end;
5121
5152
  }
5122
5153
  return length;
5123
5154
  }
@@ -8073,7 +8104,10 @@ function serveHttpInWorker(handler, options = {}) {
8073
8104
  let WebSocketClass;
8074
8105
  return {
8075
8106
  fetch(input, init) {
8076
- const request = input instanceof Request && init === void 0 ? input : new Request(input instanceof Request ? input : new URL(`${input}`, baseUrl()), init);
8107
+ const request = input instanceof Request && init === void 0 ? input : new Request(input instanceof Request ? input : new URL(`${input}`, baseUrl()), {
8108
+ ...init,
8109
+ signal: init && "signal" in init ? init.signal : input instanceof Request ? input.signal : void 0
8110
+ });
8077
8111
  return endpoint.fetch(request);
8078
8112
  },
8079
8113
  connectWebSocket,
@@ -1 +1 @@
1
- {"version":3,"file":"http-link.d.ts","sourceRoot":"","sources":["../../src/link/http-link.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,kBAAkB,uBAAuB,CAAC;AAEvD,MAAM,WAAW,eAAe;IAC9B;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;;;;;;OAQG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAKD,iEAAiE;AACjE,MAAM,WAAW,QAAQ;IACvB,0EAA0E;IAC1E,IAAI,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC1C;;;OAGG;IACH,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,2CAA2C;IAC3C,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1B;;;OAGG;IACH,KAAK,CAAC,MAAM,CAAC,EAAE,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACrC,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;CAC1B;AAED;;;;GAIG;AACH,qBAAa,aAAc,SAAQ,KAAK;IACtC,YAAY,OAAO,SAAuD,EAGzE;CACF;AASD,qEAAqE;AACrE,wBAAsB,eAAe,CACnC,IAAI,EAAE,WAAW,EACjB,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAiC,EAAE,GAAE,eAAoB,GACnF,OAAO,CAAC,QAAQ,CAAC,CAcnB;AAED;;;;GAIG;AACH,wBAAsB,aAAa,CACjC,IAAI,EAAE,WAAW,EACjB,OAAO,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,EAC3D,EAAE,aAAa,EAAE,QAAQ,EAAE,aAAa,EAAE,GAAE,eAAoB,GAC/D,OAAO,CAAC,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,CAiB9B;AAED;;;GAGG;AACH,wBAAgB,WAAW,CACzB,IAAI,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,EAC7C,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,GAClC,QAAQ,CAwEV"}
1
+ {"version":3,"file":"http-link.d.ts","sourceRoot":"","sources":["../../src/link/http-link.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,kBAAkB,uBAAuB,CAAC;AAEvD,MAAM,WAAW,eAAe;IAC9B;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;;;;;;OAQG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAKD,iEAAiE;AACjE,MAAM,WAAW,QAAQ;IACvB,0EAA0E;IAC1E,IAAI,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC1C;;;OAGG;IACH,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,2CAA2C;IAC3C,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1B;;;OAGG;IACH,KAAK,CAAC,MAAM,CAAC,EAAE,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACrC,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;CAC1B;AAED;;;;GAIG;AACH,qBAAa,aAAc,SAAQ,KAAK;IACtC,YAAY,OAAO,SAAuD,EAGzE;CACF;AASD,qEAAqE;AACrE,wBAAsB,eAAe,CACnC,IAAI,EAAE,WAAW,EACjB,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAiC,EAAE,GAAE,eAAoB,GACnF,OAAO,CAAC,QAAQ,CAAC,CAcnB;AAED;;;;GAIG;AACH,wBAAsB,aAAa,CACjC,IAAI,EAAE,WAAW,EACjB,OAAO,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,EAC3D,EAAE,aAAa,EAAE,QAAQ,EAAE,aAAa,EAAE,GAAE,eAAoB,GAC/D,OAAO,CAAC,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,CAiB9B;AAED;;;GAGG;AACH,wBAAgB,WAAW,CACzB,IAAI,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,EAC7C,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,GAClC,QAAQ,CA4EV"}
package/dist/relay-sw.js CHANGED
@@ -1138,7 +1138,10 @@
1138
1138
  }
1139
1139
  const onAbort = () => void release();
1140
1140
  signal?.addEventListener("abort", onAbort, { once: true });
1141
- drain(output).finally(() => signal?.removeEventListener("abort", onAbort));
1141
+ options.keepAlive;
1142
+ drain(output).finally(() => {
1143
+ signal?.removeEventListener("abort", onAbort);
1144
+ });
1142
1145
  };
1143
1146
  let decoded;
1144
1147
  try {
@@ -1388,7 +1391,8 @@
1388
1391
  }
1389
1392
  const { envelope, body: respBody, release, finish } = await httpFetch(call, env, body, {
1390
1393
  ...options,
1391
- signal: request.signal ?? void 0
1394
+ signal: request.signal ?? void 0,
1395
+ keepAlive: request
1392
1396
  });
1393
1397
  const responseInit = {
1394
1398
  status: envelope.status,
@@ -1400,7 +1404,7 @@
1400
1404
  else await release?.();
1401
1405
  return new Response(null, responseInit);
1402
1406
  }
1403
- return new Response(responseBody(respBody, request.signal, release), responseInit);
1407
+ return new Response(responseBody(respBody, request, release), responseInit);
1404
1408
  }
1405
1409
  /**
1406
1410
  * How long a cancelled body waits for the read in flight before it abandons the call anyway.
@@ -1423,7 +1427,9 @@
1423
1427
  * A cancel abandons the call too — unless the read in flight comes back with the
1424
1428
  * body's end, which finishes it instead (see `cancel`).
1425
1429
  */
1426
- function responseBody(body, signal, release) {
1430
+ function responseBody(body, request, release) {
1431
+ const keep = { request };
1432
+ const signal = request.signal ?? void 0;
1427
1433
  const it = body[Symbol.asyncIterator]();
1428
1434
  let controller;
1429
1435
  let ended = false;
@@ -1431,6 +1437,7 @@
1431
1437
  const end = () => {
1432
1438
  ended = true;
1433
1439
  signal?.removeEventListener("abort", onAbort);
1440
+ keep.request = void 0;
1434
1441
  };
1435
1442
  const onAbort = () => {
1436
1443
  if (ended) return;
@@ -2394,12 +2401,6 @@
2394
2401
  const producer = output;
2395
2402
  const returnProducer = () => void Promise.resolve(producer.return?.()).catch(() => {});
2396
2403
  if (framed !== output) signal.addEventListener("abort", returnProducer, { once: true });
2397
- let onAborted = () => {};
2398
- const aborted = new Promise((resolve) => {
2399
- onAborted = () => resolve();
2400
- });
2401
- if (signal.aborted) onAborted();
2402
- else signal.addEventListener("abort", onAborted, { once: true });
2403
2404
  try {
2404
2405
  await sendIterator(async ({ done, value, error }) => {
2405
2406
  if (signal.aborted) return;
@@ -2421,7 +2422,7 @@
2421
2422
  });
2422
2423
  touch();
2423
2424
  if (done && !error) onSent();
2424
- if (portBufferedAmount(port) > 0) await Promise.race([whenPortReady(port), aborted]);
2425
+ if (portBufferedAmount(port) > 0) await whenReadyOrAborted(port, signal);
2425
2426
  return;
2426
2427
  }
2427
2428
  await callPort(port, chunk, {
@@ -2437,10 +2438,25 @@
2437
2438
  throw err;
2438
2439
  } finally {
2439
2440
  signal.removeEventListener("abort", returnProducer);
2440
- signal.removeEventListener("abort", onAborted);
2441
2441
  }
2442
2442
  }
2443
2443
  /**
2444
+ * A pipelined sender waits only for room in the window, or for the abort. The
2445
+ * abort listener lives for this one wait: a promise held for the whole stream
2446
+ * would keep one reaction per wait until the stream ends.
2447
+ */
2448
+ function whenReadyOrAborted(port, signal) {
2449
+ if (signal.aborted) return Promise.resolve();
2450
+ return new Promise((resolve) => {
2451
+ const settle = () => {
2452
+ signal.removeEventListener("abort", settle);
2453
+ resolve();
2454
+ };
2455
+ signal.addEventListener("abort", settle, { once: true });
2456
+ whenPortReady(port).then(settle, settle);
2457
+ });
2458
+ }
2459
+ /**
2444
2460
  * One virtual port.
2445
2461
  *
2446
2462
  * `deliver` and `markClosed` are deliberately not on `port`: the consumer holds
@@ -3366,7 +3382,12 @@
3366
3382
  finish();
3367
3383
  return response;
3368
3384
  }
3369
- return new Response(guardBody(response.body, lost, () => lostReason, finish, request.signal), {
3385
+ failers.delete(fail);
3386
+ const body = guardBody(response.body, finish, request);
3387
+ fail = body.fail;
3388
+ if (closed) fail(lostReason);
3389
+ else if (!finished) failers.add(fail);
3390
+ return new Response(body.stream, {
3370
3391
  status: response.status,
3371
3392
  statusText: response.statusText,
3372
3393
  headers: response.headers
@@ -3385,56 +3406,62 @@
3385
3406
  return asyncDisposable(link, () => link.close());
3386
3407
  }
3387
3408
  /**
3388
- * A response body that reports its end (done, cancelled, failed or aborted) and
3389
- * fails with the link's reason when the link is lost mid-stream. An abort of the
3390
- * request's signal ends it at once, read or not: the exchange is over (the layer
3391
- * below has given its port back), so the request leaves `inflight` then.
3409
+ * A response body that reports its end (done, cancelled, failed or aborted).
3410
+ * `fail(error)` ends it with `error` at once — the link calls it when it is lost
3411
+ * mid-stream. An abort of the request's signal does the same with the signal's
3412
+ * reason, read or not: the exchange is over (the layer below has given its port
3413
+ * back), so the request leaves `inflight` then. A read waits on the reader
3414
+ * alone, so nothing outlives the chunk it brings.
3392
3415
  */
3393
- function guardBody(body, lost, reason, finish, signal) {
3416
+ function guardBody(body, finish, request) {
3417
+ const keep = { request };
3418
+ const signal = request.signal ?? void 0;
3394
3419
  const reader = body.getReader();
3395
3420
  let controller;
3396
3421
  let ended = false;
3397
3422
  const end = () => {
3398
3423
  ended = true;
3399
3424
  signal?.removeEventListener("abort", onAbort);
3425
+ keep.request = void 0;
3400
3426
  finish();
3401
3427
  };
3402
- const onAbort = () => {
3428
+ const fail = (error) => {
3403
3429
  if (ended) return;
3404
3430
  end();
3405
3431
  reader.cancel().catch(() => {});
3406
3432
  try {
3407
- controller.error(abortError(signal));
3433
+ controller.error(error);
3408
3434
  } catch {}
3409
3435
  };
3410
- return new ReadableStream({
3411
- start(c) {
3412
- controller = c;
3413
- if (signal?.aborted) onAbort();
3414
- else signal?.addEventListener("abort", onAbort, { once: true });
3415
- },
3416
- async pull(controller) {
3417
- try {
3418
- const { done, value } = await Promise.race([reader.read(), lost]);
3419
- if (ended) return;
3420
- if (done) {
3421
- end();
3422
- controller.close();
3423
- return;
3436
+ const onAbort = () => fail(abortError(signal));
3437
+ return {
3438
+ stream: new ReadableStream({
3439
+ start(c) {
3440
+ controller = c;
3441
+ if (signal?.aborted) onAbort();
3442
+ else signal?.addEventListener("abort", onAbort, { once: true });
3443
+ },
3444
+ async pull(controller) {
3445
+ try {
3446
+ const { done, value } = await reader.read();
3447
+ if (ended) return;
3448
+ if (done) {
3449
+ end();
3450
+ controller.close();
3451
+ return;
3452
+ }
3453
+ controller.enqueue(value);
3454
+ } catch (error) {
3455
+ fail(error);
3424
3456
  }
3425
- controller.enqueue(value);
3426
- } catch (error) {
3427
- if (ended) return;
3457
+ },
3458
+ cancel(why) {
3428
3459
  end();
3429
- reader.cancel().catch(() => {});
3430
- controller.error(reason() ?? error);
3460
+ setTimeout(() => void reader.cancel(why).catch(() => {}), 0);
3431
3461
  }
3432
- },
3433
- async cancel(why) {
3434
- end();
3435
- await reader.cancel(why).catch(() => {});
3436
- }
3437
- });
3462
+ }),
3463
+ fail
3464
+ };
3438
3465
  }
3439
3466
  /** The signal's reason as an `Error` (`AbortError` when it gave none). */
3440
3467
  function abortError(signal) {