@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.
@@ -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,7 +1061,7 @@ 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
  });
@@ -1118,41 +1115,105 @@ async function throwIfPeerError(result, output) {
1118
1115
  */
1119
1116
  async function httpFetch(call, env, body, options = {}) {
1120
1117
  const codec = options.codec ?? defaultCodec;
1118
+ const { signal } = options;
1119
+ if (signal?.aborted) throw abortReason$1(signal);
1121
1120
  const output = call(codec.encodeRequest(env, body));
1122
- const decoded = await codec.decodeResponse(output, { method: env.method });
1123
- const release = async () => {
1124
- try {
1125
- await output.return?.(void 0);
1126
- } catch {}
1121
+ let released;
1122
+ const release = () => {
1123
+ released ??= (async () => {
1124
+ try {
1125
+ await output.return?.(void 0);
1126
+ } catch {}
1127
+ })();
1128
+ return released;
1129
+ };
1130
+ let finished = false;
1131
+ const finish = () => {
1132
+ if (finished || released) return;
1133
+ finished = true;
1134
+ if (signal?.aborted) {
1135
+ release();
1136
+ return;
1137
+ }
1138
+ const onAbort = () => void release();
1139
+ signal?.addEventListener("abort", onAbort, { once: true });
1140
+ drain(output).finally(() => signal?.removeEventListener("abort", onAbort));
1127
1141
  };
1142
+ let decoded;
1143
+ try {
1144
+ decoded = await untilAborted(codec.decodeResponse(output, { method: env.method }), signal);
1145
+ } catch (error) {
1146
+ if (signal?.aborted) release();
1147
+ else await release();
1148
+ throw error;
1149
+ }
1128
1150
  const result = {
1129
1151
  envelope: decoded.envelope,
1130
- body: releasingOnExit(decoded.body, release),
1131
- release
1152
+ body: releasingOnExit(decoded.body, finish, release),
1153
+ release,
1154
+ finish
1132
1155
  };
1133
1156
  await throwIfPeerError(result, output);
1134
1157
  return result;
1135
1158
  }
1159
+ /** `promise`, rejected with the signal's reason if it aborts first. */
1160
+ function untilAborted(promise, signal) {
1161
+ if (!signal) return promise;
1162
+ return new Promise((resolve, reject) => {
1163
+ const onAbort = () => reject(abortReason$1(signal));
1164
+ if (signal.aborted) return onAbort();
1165
+ signal.addEventListener("abort", onAbort, { once: true });
1166
+ promise.then((value) => {
1167
+ signal.removeEventListener("abort", onAbort);
1168
+ resolve(value);
1169
+ }, (error) => {
1170
+ signal.removeEventListener("abort", onAbort);
1171
+ reject(error);
1172
+ });
1173
+ });
1174
+ }
1175
+ /** The signal's reason as an `Error` (`AbortError` when it gave none). */
1176
+ function abortReason$1(signal) {
1177
+ const reason = signal.reason;
1178
+ if (reason instanceof Error) return reason;
1179
+ const err = new Error(reason === void 0 ? "Aborted" : String(reason));
1180
+ err.name = "AbortError";
1181
+ return err;
1182
+ }
1136
1183
  /**
1137
- * `body`, but leaving it before its end releases the call.
1184
+ * `body`, ending the call however it is left: read to its end, or left early.
1138
1185
  *
1139
1186
  * The body the codec hands back is a generator of its own over a `ByteReader`,
1140
- * and `.return()` on it never reaches the call's `output` (see
1141
- * `throwIfPeerError` for why the codec must not do that itself). So a caller
1142
- * that stopped reading — an aborted `fetch`, a cancelled response stream —
1143
- * left the peer's producer parked forever, holding its handler open. Safe here
1144
- * for the same reason it is safe in `throwIfPeerError`: `output` is this
1145
- * call's own response-only generator, and the request was fully sent.
1146
- */
1147
- async function* releasingOnExit(body, release) {
1187
+ * and its ending never reaches the call's `output` (see `throwIfPeerError` for
1188
+ * why the codec must not do that itself). A caller that stopped reading — an
1189
+ * aborted `fetch`, a cancelled response stream — left the peer's producer
1190
+ * parked forever; and one that read the body to its end left the call itself
1191
+ * open, which on a multiplexed transport is one port per exchange until the
1192
+ * multiplexer refuses to open more (`maxPorts`).
1193
+ *
1194
+ * Left early, the call is abandoned (`.return()`): the peer's producer is
1195
+ * released. Read to its end, the call is not cut short but read to ITS end, in
1196
+ * the background: the peer ends it once its side of the exchange is over,
1197
+ * which may be later than the response — a handler that answered before it
1198
+ * read the request body is still receiving it (`HttpDataHandlerResult.settled`),
1199
+ * and abandoning the call would abort that upload.
1200
+ */
1201
+ async function* releasingOnExit(body, finish, release) {
1148
1202
  let completed = false;
1149
1203
  try {
1150
1204
  yield* body;
1151
1205
  completed = true;
1152
1206
  } finally {
1153
- if (!completed) await release();
1207
+ if (completed) finish();
1208
+ else await release();
1154
1209
  }
1155
1210
  }
1211
+ /** Reads `output` to its end, discarding what is left, ignoring a failure. */
1212
+ async function drain(output) {
1213
+ try {
1214
+ for (let r = await output.next(); !r.done; r = await output.next());
1215
+ } catch {}
1216
+ }
1156
1217
  /**
1157
1218
  * The one place that knows a runtime may not implement request body streams,
1158
1219
  * and the buffering both fallbacks need. `fetch.ts` and `http-stubs.ts` each
@@ -1344,14 +1405,18 @@ async function fetchOverDuplex(call, request, options = {}) {
1344
1405
  const buffered = new Uint8Array(await request.arrayBuffer());
1345
1406
  if (buffered.byteLength > 0) body = [buffered];
1346
1407
  }
1347
- const { envelope, body: respBody, release } = await httpFetch(call, env, body, options);
1408
+ const { envelope, body: respBody, release, finish } = await httpFetch(call, env, body, {
1409
+ ...options,
1410
+ signal: request.signal ?? void 0
1411
+ });
1348
1412
  const responseInit = {
1349
1413
  status: envelope.status,
1350
1414
  statusText: envelope.statusText,
1351
1415
  headers: forwardableHeaders(envelope.headers)
1352
1416
  };
1353
1417
  if (request.method === "HEAD" || request.method === "OPTIONS" || NULL_BODY_STATUSES.has(envelope.status)) {
1354
- await release?.();
1418
+ if (finish) finish();
1419
+ else await release?.();
1355
1420
  return new Response(null, responseInit);
1356
1421
  }
1357
1422
  return new Response(asyncIterableToReadable(withAbort(respBody, request.signal)), responseInit);
@@ -1366,13 +1431,6 @@ async function* withAbort(iter, signal) {
1366
1431
  yield chunk;
1367
1432
  }
1368
1433
  }
1369
- function abortReason$1(signal) {
1370
- const reason = signal.reason;
1371
- if (reason instanceof Error) return reason;
1372
- const err = new Error(reason === void 0 ? "Aborted" : String(reason));
1373
- err.name = "AbortError";
1374
- return err;
1375
- }
1376
1434
  var HttpError = class HttpError extends Error {
1377
1435
  status;
1378
1436
  statusText;
@@ -1747,6 +1805,69 @@ async function* throughAbort(input, signal) {
1747
1805
  }
1748
1806
  }
1749
1807
  /**
1808
+ * Makes `target` disposable with `await using`: installs `dispose` as its
1809
+ * `[Symbol.asyncDispose]`, where the runtime defines the symbol. Hand-written
1810
+ * iterators lose what a native generator gets from `AsyncIteratorPrototype`;
1811
+ * this gives it back. A no-op where the symbol is missing. Kept off the
1812
+ * declared types, which would require consumers to compile with the
1813
+ * `esnext.disposable` lib.
1814
+ */
1815
+ function asyncDisposable$1(target, dispose) {
1816
+ const asyncDispose = Symbol.asyncDispose;
1817
+ if (asyncDispose && !(asyncDispose in target)) Object.defineProperty(target, asyncDispose, {
1818
+ value: async () => {
1819
+ await dispose();
1820
+ },
1821
+ configurable: true,
1822
+ writable: true
1823
+ });
1824
+ return target;
1825
+ }
1826
+ /**
1827
+ * `gen`, except that `return()` and `throw()` take effect at once.
1828
+ *
1829
+ * On an async generator both are queued behind a `next()` that is still
1830
+ * pending: a consumer that gives up on a stream while it waits for the peer's
1831
+ * next chunk — an aborted fetch whose response head has not come, say — cannot
1832
+ * end it until that chunk arrives, which for a peer that never answers is
1833
+ * never. The stream's `finally` (which closes its port and tells the peer)
1834
+ * then never runs, and on a multiplexer the port is kept for good.
1835
+ *
1836
+ * So `abandon` runs first, synchronously: it must make the pending `next()`
1837
+ * settle (abort the stream), which then resolves done, after which the
1838
+ * generator's own `return()` / `throw()` proceeds as usual. On a generator
1839
+ * suspended at a `yield` it is the same ending, a step earlier. `abandon` runs once, whichever comes first.
1840
+ */
1841
+ function returningAtOnce(gen, abandon) {
1842
+ let abandoned = false;
1843
+ const giveUp = (reason) => {
1844
+ if (abandoned) return;
1845
+ abandoned = true;
1846
+ abandon(reason);
1847
+ };
1848
+ const wrapped = {
1849
+ next: (...args) => gen.next(...args).catch((error) => {
1850
+ if (abandoned) return {
1851
+ done: true,
1852
+ value: void 0
1853
+ };
1854
+ throw error;
1855
+ }),
1856
+ return(value) {
1857
+ giveUp();
1858
+ return gen.return(value);
1859
+ },
1860
+ throw(error) {
1861
+ giveUp(error);
1862
+ return gen.throw(error);
1863
+ },
1864
+ [Symbol.asyncIterator]() {
1865
+ return wrapped;
1866
+ }
1867
+ };
1868
+ return asyncDisposable$1(wrapped, () => wrapped.return(void 0));
1869
+ }
1870
+ /**
1750
1871
  * The `type` of the out-of-band notice a side posts when it abandons a stream.
1751
1872
  *
1752
1873
  * Layer 1's `close` is not observable to layer 2 — a closed virtual port drops
@@ -1783,7 +1904,7 @@ function runCallerSide(port, input, options) {
1783
1904
  const inbound = receiveChunks(port, CHANNEL_OUT, controller, clock.touch);
1784
1905
  const pump = sendChunks(port, CHANNEL_IN, input, options, controller.signal, clock.touch);
1785
1906
  pump.catch(() => {});
1786
- return (async function* () {
1907
+ return returningAtOnce((async function* () {
1787
1908
  try {
1788
1909
  yield* inbound.stream;
1789
1910
  } finally {
@@ -1797,7 +1918,9 @@ function runCallerSide(port, input, options) {
1797
1918
  await port.close?.();
1798
1919
  } catch {}
1799
1920
  }
1800
- })();
1921
+ })(), () => {
1922
+ if (!controller.signal.aborted) controller.abort(/* @__PURE__ */ new Error("webrun-rpc: the caller abandoned the stream"));
1923
+ });
1801
1924
  }
1802
1925
  /**
1803
1926
  * Listens for the peer's abort notice, and can post our own. The notice is a
@@ -2193,9 +2316,22 @@ const connect = async ({ mux: factory, timeout }) => {
2193
2316
  maxMessageSize: mux.maxMessageSize,
2194
2317
  timeout
2195
2318
  };
2196
- const call = (input) => (async function* () {
2197
- yield* duplexOverPort(await mux.openPort(STREAM_META), streamOptions)(input);
2198
- })();
2319
+ const call = (input) => {
2320
+ let stream;
2321
+ let abandoned = false;
2322
+ return returningAtOnce((async function* () {
2323
+ const port = await mux.openPort(STREAM_META);
2324
+ if (abandoned) {
2325
+ await port.close?.();
2326
+ return;
2327
+ }
2328
+ stream = duplexOverPort(port, streamOptions)(input);
2329
+ yield* stream;
2330
+ })(), () => {
2331
+ abandoned = true;
2332
+ stream?.return(void 0).catch(() => {});
2333
+ });
2334
+ };
2199
2335
  return {
2200
2336
  call,
2201
2337
  async close() {
@@ -2241,6 +2377,27 @@ const structuredCodec = {
2241
2377
  }
2242
2378
  };
2243
2379
  //#endregion
2380
+ //#region src/link/async-dispose.ts
2381
+ /**
2382
+ * Makes `target` disposable with `await using`: installs `dispose` as its
2383
+ * `[Symbol.asyncDispose]`, where the runtime defines the symbol. Hand-written
2384
+ * iterators lose what a native generator gets from `AsyncIteratorPrototype`;
2385
+ * this gives it back. A no-op where the symbol is missing. Kept off the
2386
+ * declared types, which would require consumers to compile with the
2387
+ * `esnext.disposable` lib.
2388
+ */
2389
+ function asyncDisposable(target, dispose) {
2390
+ const asyncDispose = Symbol.asyncDispose;
2391
+ if (asyncDispose && !(asyncDispose in target)) Object.defineProperty(target, asyncDispose, {
2392
+ value: async () => {
2393
+ await dispose();
2394
+ },
2395
+ configurable: true,
2396
+ writable: true
2397
+ });
2398
+ return target;
2399
+ }
2400
+ //#endregion
2244
2401
  //#region src/link/http-link.ts
2245
2402
  /**
2246
2403
  * The version of the HTTP-over-`MessagePort` protocol this package speaks,
@@ -2270,11 +2427,12 @@ var LinkLostError = class extends Error {
2270
2427
  */
2271
2428
  const LINK_CLOSE = "webrun-http:link-close";
2272
2429
  /** Opens the calling end of a link on `port` (one port per link). */
2273
- async function connectHttpLink(port, { streamTimeout } = {}) {
2430
+ async function connectHttpLink(port, { streamTimeout, maxPorts } = {}) {
2274
2431
  const { call, close } = await connect({
2275
2432
  mux: overPipe(port, {
2276
2433
  codec: structuredCodec,
2277
- side: "initiator"
2434
+ side: "initiator",
2435
+ maxPorts
2278
2436
  }),
2279
2437
  timeout: streamTimeout
2280
2438
  });
@@ -2299,7 +2457,7 @@ function trackedLink(send, release) {
2299
2457
  inflight--;
2300
2458
  if (inflight === 0) for (const resolve of idleWaiters.splice(0)) resolve();
2301
2459
  };
2302
- return {
2460
+ const link = {
2303
2461
  get inflight() {
2304
2462
  return inflight;
2305
2463
  },
@@ -2352,6 +2510,7 @@ function trackedLink(send, release) {
2352
2510
  } catch {}
2353
2511
  }
2354
2512
  };
2513
+ return asyncDisposable(link, () => link.close());
2355
2514
  }
2356
2515
  /**
2357
2516
  * A response body that reports its end (done, cancelled or failed) and fails