@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/index.js CHANGED
@@ -178,8 +178,8 @@ const TYPE_CLOSE = 6;
178
178
  const DEFAULT_MAX_STREAMS = 256;
179
179
  const DEFAULT_MTU = 65536;
180
180
  const DEFAULT_MAX_STREAM_BUFFER = 8388608;
181
- const textEncoder = new TextEncoder();
182
- const textDecoder = new TextDecoder();
181
+ const textEncoder = /* @__PURE__ */ new TextEncoder();
182
+ const textDecoder = /* @__PURE__ */ new TextDecoder();
183
183
  function emulateMux(channel, opts = {}) {
184
184
  const maxStreams = opts.maxStreams ?? DEFAULT_MAX_STREAMS;
185
185
  const mtu = opts.mtu ?? DEFAULT_MTU;
@@ -716,7 +716,7 @@ async function* newAsyncGenerator(init, skipValues = false) {
716
716
  drainQueue();
717
717
  }
718
718
  }
719
- const utf8 = new TextEncoder();
719
+ const utf8 = /* @__PURE__ */ new TextEncoder();
720
720
  /**
721
721
  * Coerce common byte-like inputs into a `Uint8Array`. Strings encode as UTF-8.
722
722
  * Blobs return a `Promise<Uint8Array>`; every other input returns synchronously.
@@ -1483,8 +1483,8 @@ var HttpParseError = class extends Error {
1483
1483
  const NEWLINE = 10;
1484
1484
  /** Mirrors the HTTP/1.1 codec's default `maxHeaderBytes`. */
1485
1485
  const MAX_ENVELOPE_BYTES = 65536;
1486
- const encoder$1 = new TextEncoder();
1487
- const decoder = new TextDecoder();
1486
+ const encoder$1 = /* @__PURE__ */ new TextEncoder();
1487
+ const decoder = /* @__PURE__ */ new TextDecoder();
1488
1488
  /**
1489
1489
  * Encode an HTTP envelope plus optional body as one continuous byte stream:
1490
1490
  *
@@ -1738,15 +1738,15 @@ function resolveFraming(headers, version) {
1738
1738
  };
1739
1739
  return { kind: "none" };
1740
1740
  }
1741
- const CRLF = new Uint8Array([13, 10]);
1742
- const LAST_CHUNK = new Uint8Array([
1741
+ const CRLF = /* @__PURE__ */ new Uint8Array([13, 10]);
1742
+ const LAST_CHUNK = /* @__PURE__ */ new Uint8Array([
1743
1743
  48,
1744
1744
  13,
1745
1745
  10,
1746
1746
  13,
1747
1747
  10
1748
1748
  ]);
1749
- const encoder = new TextEncoder();
1749
+ const encoder = /* @__PURE__ */ new TextEncoder();
1750
1750
  /**
1751
1751
  * Chunk sizes are written in HEXADECIMAL — a 21-byte chunk is `15`. Writing
1752
1752
  * them in decimal is the defect note 15 found in @libp2p/http, and it is the
@@ -2051,7 +2051,7 @@ function newHttpCodec(options = {}) {
2051
2051
  decodeResponse: (input, o) => decodeResponse(input, opts, o.method)
2052
2052
  };
2053
2053
  }
2054
- const httpCodec = newHttpCodec();
2054
+ const httpCodec = /* @__PURE__ */ newHttpCodec();
2055
2055
  /**
2056
2056
  * Dispatches on byte 0. The formats are self-identifying — a JSON envelope
2057
2057
  * always begins `{`, which is not a token character and so can never begin an
@@ -2101,10 +2101,29 @@ function newSniffingCodec(options) {
2101
2101
  * Writes HTTP/1.1; accepts HTTP/1.1 or the legacy JSON envelope. Used whenever
2102
2102
  * no `codec` option is supplied.
2103
2103
  */
2104
- const defaultCodec = newSniffingCodec({
2104
+ const defaultCodec = /* @__PURE__ */ newSniffingCodec({
2105
2105
  write: httpCodec,
2106
2106
  accept: [httpCodec, jsonEnvelopeCodec]
2107
2107
  });
2108
+ /**
2109
+ * Makes `target` disposable with `await using`: installs `dispose` as its
2110
+ * `[Symbol.asyncDispose]`, where the runtime defines the symbol. Hand-written
2111
+ * iterators lose what a native generator gets from `AsyncIteratorPrototype`;
2112
+ * this gives it back. A no-op where the symbol is missing. Kept off the
2113
+ * declared types, which would require consumers to compile with the
2114
+ * `esnext.disposable` lib.
2115
+ */
2116
+ function asyncDisposable$2(target, dispose) {
2117
+ const asyncDispose = Symbol.asyncDispose;
2118
+ if (asyncDispose && !(asyncDispose in target)) Object.defineProperty(target, asyncDispose, {
2119
+ value: async () => {
2120
+ await dispose();
2121
+ },
2122
+ configurable: true,
2123
+ writable: true
2124
+ });
2125
+ return target;
2126
+ }
2108
2127
  /** Carries the serialized JS error between two webrun peers. */
2109
2128
  const PEER_ERROR_HEADER = "x-webrun-error";
2110
2129
  /** Hard cap on the peer-error header (M3): see `encodeErrorResponse`. */
@@ -2182,41 +2201,105 @@ async function throwIfPeerError(result, output) {
2182
2201
  */
2183
2202
  async function httpFetch(call, env, body, options = {}) {
2184
2203
  const codec = options.codec ?? defaultCodec;
2204
+ const { signal } = options;
2205
+ if (signal?.aborted) throw abortReason$1(signal);
2185
2206
  const output = call(codec.encodeRequest(env, body));
2186
- const decoded = await codec.decodeResponse(output, { method: env.method });
2187
- const release = async () => {
2188
- try {
2189
- await output.return?.(void 0);
2190
- } catch {}
2207
+ let released;
2208
+ const release = () => {
2209
+ released ??= (async () => {
2210
+ try {
2211
+ await output.return?.(void 0);
2212
+ } catch {}
2213
+ })();
2214
+ return released;
2215
+ };
2216
+ let finished = false;
2217
+ const finish = () => {
2218
+ if (finished || released) return;
2219
+ finished = true;
2220
+ if (signal?.aborted) {
2221
+ release();
2222
+ return;
2223
+ }
2224
+ const onAbort = () => void release();
2225
+ signal?.addEventListener("abort", onAbort, { once: true });
2226
+ drain(output).finally(() => signal?.removeEventListener("abort", onAbort));
2191
2227
  };
2228
+ let decoded;
2229
+ try {
2230
+ decoded = await untilAborted(codec.decodeResponse(output, { method: env.method }), signal);
2231
+ } catch (error) {
2232
+ if (signal?.aborted) release();
2233
+ else await release();
2234
+ throw error;
2235
+ }
2192
2236
  const result = {
2193
2237
  envelope: decoded.envelope,
2194
- body: releasingOnExit(decoded.body, release),
2195
- release
2238
+ body: releasingOnExit(decoded.body, finish, release),
2239
+ release,
2240
+ finish
2196
2241
  };
2197
2242
  await throwIfPeerError(result, output);
2198
2243
  return result;
2199
2244
  }
2245
+ /** `promise`, rejected with the signal's reason if it aborts first. */
2246
+ function untilAborted(promise, signal) {
2247
+ if (!signal) return promise;
2248
+ return new Promise((resolve, reject) => {
2249
+ const onAbort = () => reject(abortReason$1(signal));
2250
+ if (signal.aborted) return onAbort();
2251
+ signal.addEventListener("abort", onAbort, { once: true });
2252
+ promise.then((value) => {
2253
+ signal.removeEventListener("abort", onAbort);
2254
+ resolve(value);
2255
+ }, (error) => {
2256
+ signal.removeEventListener("abort", onAbort);
2257
+ reject(error);
2258
+ });
2259
+ });
2260
+ }
2261
+ /** The signal's reason as an `Error` (`AbortError` when it gave none). */
2262
+ function abortReason$1(signal) {
2263
+ const reason = signal.reason;
2264
+ if (reason instanceof Error) return reason;
2265
+ const err = new Error(reason === void 0 ? "Aborted" : String(reason));
2266
+ err.name = "AbortError";
2267
+ return err;
2268
+ }
2200
2269
  /**
2201
- * `body`, but leaving it before its end releases the call.
2270
+ * `body`, ending the call however it is left: read to its end, or left early.
2202
2271
  *
2203
2272
  * The body the codec hands back is a generator of its own over a `ByteReader`,
2204
- * and `.return()` on it never reaches the call's `output` (see
2205
- * `throwIfPeerError` for why the codec must not do that itself). So a caller
2206
- * that stopped reading — an aborted `fetch`, a cancelled response stream —
2207
- * left the peer's producer parked forever, holding its handler open. Safe here
2208
- * for the same reason it is safe in `throwIfPeerError`: `output` is this
2209
- * call's own response-only generator, and the request was fully sent.
2210
- */
2211
- async function* releasingOnExit(body, release) {
2273
+ * and its ending never reaches the call's `output` (see `throwIfPeerError` for
2274
+ * why the codec must not do that itself). A caller that stopped reading — an
2275
+ * aborted `fetch`, a cancelled response stream — left the peer's producer
2276
+ * parked forever; and one that read the body to its end left the call itself
2277
+ * open, which on a multiplexed transport is one port per exchange until the
2278
+ * multiplexer refuses to open more (`maxPorts`).
2279
+ *
2280
+ * Left early, the call is abandoned (`.return()`): the peer's producer is
2281
+ * released. Read to its end, the call is not cut short but read to ITS end, in
2282
+ * the background: the peer ends it once its side of the exchange is over,
2283
+ * which may be later than the response — a handler that answered before it
2284
+ * read the request body is still receiving it (`HttpDataHandlerResult.settled`),
2285
+ * and abandoning the call would abort that upload.
2286
+ */
2287
+ async function* releasingOnExit(body, finish, release) {
2212
2288
  let completed = false;
2213
2289
  try {
2214
2290
  yield* body;
2215
2291
  completed = true;
2216
2292
  } finally {
2217
- if (!completed) await release();
2293
+ if (completed) finish();
2294
+ else await release();
2218
2295
  }
2219
2296
  }
2297
+ /** Reads `output` to its end, discarding what is left, ignoring a failure. */
2298
+ async function drain(output) {
2299
+ try {
2300
+ for (let r = await output.next(); !r.done; r = await output.next());
2301
+ } catch {}
2302
+ }
2220
2303
  /**
2221
2304
  * Wrap an HTTP handler as a `Duplex` so it can be registered with any
2222
2305
  * `webrun-streams-*` adapter's `serve`.
@@ -2244,6 +2327,7 @@ function httpServe(handler, options = {}) {
2244
2327
  try {
2245
2328
  yield* replyCodec.encodeResponse(result.envelope, result.body, { method: decoded.envelope.method });
2246
2329
  completed = true;
2330
+ await result.settled?.();
2247
2331
  } finally {
2248
2332
  if (!completed) try {
2249
2333
  await result.release?.();
@@ -2500,14 +2584,18 @@ async function fetchOverDuplex(call, request, options = {}) {
2500
2584
  const buffered = new Uint8Array(await request.arrayBuffer());
2501
2585
  if (buffered.byteLength > 0) body = [buffered];
2502
2586
  }
2503
- const { envelope, body: respBody, release } = await httpFetch(call, env, body, options);
2587
+ const { envelope, body: respBody, release, finish } = await httpFetch(call, env, body, {
2588
+ ...options,
2589
+ signal: request.signal ?? void 0
2590
+ });
2504
2591
  const responseInit = {
2505
2592
  status: envelope.status,
2506
2593
  statusText: envelope.statusText,
2507
2594
  headers: forwardableHeaders(envelope.headers)
2508
2595
  };
2509
2596
  if (request.method === "HEAD" || request.method === "OPTIONS" || NULL_BODY_STATUSES.has(envelope.status)) {
2510
- await release?.();
2597
+ if (finish) finish();
2598
+ else await release?.();
2511
2599
  return new Response(null, responseInit);
2512
2600
  }
2513
2601
  return new Response(asyncIterableToReadable(withAbort(respBody, request.signal)), responseInit);
@@ -2522,13 +2610,21 @@ function serveFetchOverDuplex(handler, options = {}) {
2522
2610
  method: env.method,
2523
2611
  headers: forwardableHeaders(env.headers)
2524
2612
  };
2613
+ let upload;
2525
2614
  if (env.method !== "GET" && env.method !== "HEAD") {
2526
2615
  if (supportsRequestStreams()) {
2527
- reqInit.body = asyncIterableToReadable(body);
2616
+ upload = trackingEnd(body);
2617
+ reqInit.body = asyncIterableToReadable(upload.source);
2528
2618
  reqInit.duplex = "half";
2529
2619
  } else reqInit.body = await collectBytes$1(body);
2530
2620
  }
2531
- const response = await handler(new Request(env.url, reqInit));
2621
+ const request = new Request(env.url, reqInit);
2622
+ const response = await handler(request);
2623
+ const settled = async () => {
2624
+ if (!upload) return;
2625
+ if (!request.bodyUsed && !request.body?.locked) request.body?.cancel().catch(() => {});
2626
+ await upload.settle(options.requestIdleMs ?? 3e4);
2627
+ };
2532
2628
  const respEnv = {
2533
2629
  status: response.status,
2534
2630
  statusText: response.statusText,
@@ -2538,16 +2634,101 @@ function serveFetchOverDuplex(handler, options = {}) {
2538
2634
  await response.body?.cancel();
2539
2635
  return {
2540
2636
  envelope: respEnv,
2541
- body: void 0
2637
+ body: void 0,
2638
+ settled
2542
2639
  };
2543
2640
  }
2544
2641
  return {
2545
2642
  envelope: respEnv,
2546
2643
  body: response.body ? readableToAsyncIterable(response.body) : void 0,
2547
- release: () => response.body?.cancel()
2644
+ release: () => response.body?.cancel(),
2645
+ settled
2548
2646
  };
2549
2647
  }, options);
2550
2648
  }
2649
+ /**
2650
+ * `source`, and `settle(idleMs)`: resolves once it is done — read to its end,
2651
+ * failed, or abandoned (`return()` included before the first `next()`, which a
2652
+ * generator's `finally` would not see) — or once nobody has asked it for a
2653
+ * chunk for `idleMs`, in which case it is abandoned: later reads fail.
2654
+ *
2655
+ * Idle counts only while no `next()` is pending: a reader waiting for a slow
2656
+ * sender is still reading.
2657
+ */
2658
+ function trackingEnd(source) {
2659
+ let end = () => {};
2660
+ let isEnded = false;
2661
+ const ended = new Promise((resolve) => {
2662
+ end = () => {
2663
+ isEnded = true;
2664
+ resolve();
2665
+ };
2666
+ });
2667
+ let abandoned;
2668
+ let pending = 0;
2669
+ let idleSince = Date.now();
2670
+ const it = source[Symbol.asyncIterator]();
2671
+ const abandon = (reason) => {
2672
+ abandoned ??= reason;
2673
+ end();
2674
+ Promise.resolve(it.return?.(void 0)).catch(() => {});
2675
+ };
2676
+ const tracked = {
2677
+ [Symbol.asyncIterator]() {
2678
+ return tracked;
2679
+ },
2680
+ async next() {
2681
+ if (abandoned) throw abandoned;
2682
+ pending++;
2683
+ try {
2684
+ const r = await it.next();
2685
+ if (r.done) end();
2686
+ return r;
2687
+ } catch (error) {
2688
+ end();
2689
+ throw error;
2690
+ } finally {
2691
+ if (--pending === 0) idleSince = Date.now();
2692
+ }
2693
+ },
2694
+ async return(value) {
2695
+ end();
2696
+ try {
2697
+ await it.return?.(value);
2698
+ } catch {}
2699
+ return {
2700
+ done: true,
2701
+ value: void 0
2702
+ };
2703
+ }
2704
+ };
2705
+ asyncDisposable$2(tracked, () => tracked.return?.() ?? Promise.resolve());
2706
+ const settle = (idleMs) => {
2707
+ if (isEnded || !Number.isFinite(idleMs)) return ended;
2708
+ return new Promise((resolve) => {
2709
+ let timer;
2710
+ const check = () => {
2711
+ if (isEnded) return resolve();
2712
+ const idle = pending === 0 ? Date.now() - idleSince : 0;
2713
+ if (idle >= idleMs) {
2714
+ abandon(/* @__PURE__ */ new Error(`webrun-http-streams: the request body was left unread for ${idleMs} ms after the response; abandoned`));
2715
+ return resolve();
2716
+ }
2717
+ timer = setTimeout(check, Math.max(1, Math.min(idleMs - idle, 1e3)));
2718
+ timer.unref?.();
2719
+ };
2720
+ ended.then(() => {
2721
+ clearTimeout(timer);
2722
+ resolve();
2723
+ });
2724
+ check();
2725
+ });
2726
+ };
2727
+ return {
2728
+ source: tracked,
2729
+ settle
2730
+ };
2731
+ }
2551
2732
  async function* withAbort(iter, signal) {
2552
2733
  if (!signal) {
2553
2734
  yield* iter;
@@ -2558,13 +2739,6 @@ async function* withAbort(iter, signal) {
2558
2739
  yield chunk;
2559
2740
  }
2560
2741
  }
2561
- function abortReason$1(signal) {
2562
- const reason = signal.reason;
2563
- if (reason instanceof Error) return reason;
2564
- const err = new Error(reason === void 0 ? "Aborted" : String(reason));
2565
- err.name = "AbortError";
2566
- return err;
2567
- }
2568
2742
  /**
2569
2743
  * Host a `SiteHandler` over a `Connect/Serve` pair. The `start(serve, params)`
2570
2744
  * call hands the handler to any `webrun-streams-*` adapter's `serve`.
@@ -2881,6 +3055,69 @@ async function* throughAbort(input, signal) {
2881
3055
  }
2882
3056
  }
2883
3057
  /**
3058
+ * Makes `target` disposable with `await using`: installs `dispose` as its
3059
+ * `[Symbol.asyncDispose]`, where the runtime defines the symbol. Hand-written
3060
+ * iterators lose what a native generator gets from `AsyncIteratorPrototype`;
3061
+ * this gives it back. A no-op where the symbol is missing. Kept off the
3062
+ * declared types, which would require consumers to compile with the
3063
+ * `esnext.disposable` lib.
3064
+ */
3065
+ function asyncDisposable$1(target, dispose) {
3066
+ const asyncDispose = Symbol.asyncDispose;
3067
+ if (asyncDispose && !(asyncDispose in target)) Object.defineProperty(target, asyncDispose, {
3068
+ value: async () => {
3069
+ await dispose();
3070
+ },
3071
+ configurable: true,
3072
+ writable: true
3073
+ });
3074
+ return target;
3075
+ }
3076
+ /**
3077
+ * `gen`, except that `return()` and `throw()` take effect at once.
3078
+ *
3079
+ * On an async generator both are queued behind a `next()` that is still
3080
+ * pending: a consumer that gives up on a stream while it waits for the peer's
3081
+ * next chunk — an aborted fetch whose response head has not come, say — cannot
3082
+ * end it until that chunk arrives, which for a peer that never answers is
3083
+ * never. The stream's `finally` (which closes its port and tells the peer)
3084
+ * then never runs, and on a multiplexer the port is kept for good.
3085
+ *
3086
+ * So `abandon` runs first, synchronously: it must make the pending `next()`
3087
+ * settle (abort the stream), which then resolves done, after which the
3088
+ * generator's own `return()` / `throw()` proceeds as usual. On a generator
3089
+ * suspended at a `yield` it is the same ending, a step earlier. `abandon` runs once, whichever comes first.
3090
+ */
3091
+ function returningAtOnce(gen, abandon) {
3092
+ let abandoned = false;
3093
+ const giveUp = (reason) => {
3094
+ if (abandoned) return;
3095
+ abandoned = true;
3096
+ abandon(reason);
3097
+ };
3098
+ const wrapped = {
3099
+ next: (...args) => gen.next(...args).catch((error) => {
3100
+ if (abandoned) return {
3101
+ done: true,
3102
+ value: void 0
3103
+ };
3104
+ throw error;
3105
+ }),
3106
+ return(value) {
3107
+ giveUp();
3108
+ return gen.return(value);
3109
+ },
3110
+ throw(error) {
3111
+ giveUp(error);
3112
+ return gen.throw(error);
3113
+ },
3114
+ [Symbol.asyncIterator]() {
3115
+ return wrapped;
3116
+ }
3117
+ };
3118
+ return asyncDisposable$1(wrapped, () => wrapped.return(void 0));
3119
+ }
3120
+ /**
2884
3121
  * The `type` of the out-of-band notice a side posts when it abandons a stream.
2885
3122
  *
2886
3123
  * Layer 1's `close` is not observable to layer 2 — a closed virtual port drops
@@ -2976,7 +3213,7 @@ function runCallerSide(port, input, options) {
2976
3213
  const inbound = receiveChunks(port, CHANNEL_OUT, controller, clock.touch);
2977
3214
  const pump = sendChunks(port, CHANNEL_IN, input, options, controller.signal, clock.touch);
2978
3215
  pump.catch(() => {});
2979
- return (async function* () {
3216
+ return returningAtOnce((async function* () {
2980
3217
  try {
2981
3218
  yield* inbound.stream;
2982
3219
  } finally {
@@ -2990,7 +3227,9 @@ function runCallerSide(port, input, options) {
2990
3227
  await port.close?.();
2991
3228
  } catch {}
2992
3229
  }
2993
- })();
3230
+ })(), () => {
3231
+ if (!controller.signal.aborted) controller.abort(/* @__PURE__ */ new Error("webrun-rpc: the caller abandoned the stream"));
3232
+ });
2994
3233
  }
2995
3234
  /**
2996
3235
  * Listens for the peer's abort notice, and can post our own. The notice is a
@@ -3386,9 +3625,22 @@ const connect = async ({ mux: factory, timeout }) => {
3386
3625
  maxMessageSize: mux.maxMessageSize,
3387
3626
  timeout
3388
3627
  };
3389
- const call = (input) => (async function* () {
3390
- yield* duplexOverPort(await mux.openPort(STREAM_META), streamOptions)(input);
3391
- })();
3628
+ const call = (input) => {
3629
+ let stream;
3630
+ let abandoned = false;
3631
+ return returningAtOnce((async function* () {
3632
+ const port = await mux.openPort(STREAM_META);
3633
+ if (abandoned) {
3634
+ await port.close?.();
3635
+ return;
3636
+ }
3637
+ stream = duplexOverPort(port, streamOptions)(input);
3638
+ yield* stream;
3639
+ })(), () => {
3640
+ abandoned = true;
3641
+ stream?.return(void 0).catch(() => {});
3642
+ });
3643
+ };
3392
3644
  return {
3393
3645
  call,
3394
3646
  async close() {
@@ -3479,6 +3731,27 @@ const structuredCodec = {
3479
3731
  }
3480
3732
  };
3481
3733
  //#endregion
3734
+ //#region src/link/async-dispose.ts
3735
+ /**
3736
+ * Makes `target` disposable with `await using`: installs `dispose` as its
3737
+ * `[Symbol.asyncDispose]`, where the runtime defines the symbol. Hand-written
3738
+ * iterators lose what a native generator gets from `AsyncIteratorPrototype`;
3739
+ * this gives it back. A no-op where the symbol is missing. Kept off the
3740
+ * declared types, which would require consumers to compile with the
3741
+ * `esnext.disposable` lib.
3742
+ */
3743
+ function asyncDisposable(target, dispose) {
3744
+ const asyncDispose = Symbol.asyncDispose;
3745
+ if (asyncDispose && !(asyncDispose in target)) Object.defineProperty(target, asyncDispose, {
3746
+ value: async () => {
3747
+ await dispose();
3748
+ },
3749
+ configurable: true,
3750
+ writable: true
3751
+ });
3752
+ return target;
3753
+ }
3754
+ //#endregion
3482
3755
  //#region src/link/http-link.ts
3483
3756
  /**
3484
3757
  * The version of the HTTP-over-`MessagePort` protocol this package speaks,
@@ -3508,11 +3781,12 @@ var LinkLostError = class extends Error {
3508
3781
  */
3509
3782
  const LINK_CLOSE = "webrun-http:link-close";
3510
3783
  /** Opens the calling end of a link on `port` (one port per link). */
3511
- async function connectHttpLink(port, { streamTimeout } = {}) {
3784
+ async function connectHttpLink(port, { streamTimeout, maxPorts } = {}) {
3512
3785
  const { call, close } = await connect({
3513
3786
  mux: overPipe(port, {
3514
3787
  codec: structuredCodec,
3515
- side: "initiator"
3788
+ side: "initiator",
3789
+ maxPorts
3516
3790
  }),
3517
3791
  timeout: streamTimeout
3518
3792
  });
@@ -3528,14 +3802,15 @@ async function connectHttpLink(port, { streamTimeout } = {}) {
3528
3802
  * that abandons the requests still running (their handlers see the
3529
3803
  * cancellation) and releases the port.
3530
3804
  */
3531
- async function serveHttpLink(port, handler, { streamTimeout } = {}) {
3805
+ async function serveHttpLink(port, handler, { streamTimeout, maxPorts, requestIdleMs } = {}) {
3532
3806
  const teardown = await serve({
3533
3807
  mux: overPipe(port, {
3534
3808
  codec: structuredCodec,
3535
- side: "responder"
3809
+ side: "responder",
3810
+ maxPorts
3536
3811
  }),
3537
3812
  timeout: streamTimeout
3538
- }, serveFetchOverDuplex(async (request) => await handler(request)));
3813
+ }, serveFetchOverDuplex(async (request) => await handler(request), { requestIdleMs }));
3539
3814
  const onClose = (event) => {
3540
3815
  if (event.data?.type === LINK_CLOSE) close();
3541
3816
  };
@@ -3560,7 +3835,7 @@ function trackedLink(send, release) {
3560
3835
  inflight--;
3561
3836
  if (inflight === 0) for (const resolve of idleWaiters.splice(0)) resolve();
3562
3837
  };
3563
- return {
3838
+ const link = {
3564
3839
  get inflight() {
3565
3840
  return inflight;
3566
3841
  },
@@ -3613,6 +3888,7 @@ function trackedLink(send, release) {
3613
3888
  } catch {}
3614
3889
  }
3615
3890
  };
3891
+ return asyncDisposable(link, () => link.close());
3616
3892
  }
3617
3893
  /**
3618
3894
  * A response body that reports its end (done, cancelled or failed) and fails
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Makes `target` disposable with `await using`: installs `dispose` as its
3
+ * `[Symbol.asyncDispose]`, where the runtime defines the symbol. Hand-written
4
+ * iterators lose what a native generator gets from `AsyncIteratorPrototype`;
5
+ * this gives it back. A no-op where the symbol is missing. Kept off the
6
+ * declared types, which would require consumers to compile with the
7
+ * `esnext.disposable` lib.
8
+ */
9
+ export declare function asyncDisposable<T extends object>(target: T, dispose: () => Promise<unknown>): T;
10
+ //# sourceMappingURL=async-dispose.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"async-dispose.d.ts","sourceRoot":"","sources":["../../src/link/async-dispose.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,CAAC,SAAS,MAAM,EAAE,MAAM,EAAE,CAAC,EAAE,OAAO,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAa/F"}
@@ -15,6 +15,20 @@ export interface HttpLinkOptions {
15
15
  * none: a slow consumer is throttled, never failed.
16
16
  */
17
17
  streamTimeout?: number;
18
+ /**
19
+ * Ceiling on the requests in flight on the link at once: webrun-rpc's
20
+ * `maxPorts`, one multiplexed port per request (default 1024). A request
21
+ * past it fails on the calling end; one the answering end has no room for is
22
+ * refused. Every exchange gives its port back when it ends, however it ends.
23
+ */
24
+ maxPorts?: number;
25
+ /**
26
+ * The answering end only: once a response is out, how long the request body
27
+ * may go unread before it is abandoned and the exchange ends (default 30 s;
28
+ * webrun-http-streams' `requestIdleMs`). A handler that answers first and
29
+ * reads the upload afterwards keeps it as long as it keeps reading.
30
+ */
31
+ requestIdleMs?: number;
18
32
  }
19
33
  /** The calling end of a link: sends requests, gets responses. */
20
34
  export interface HttpLink {
@@ -43,13 +57,13 @@ export declare class LinkLostError extends Error {
43
57
  constructor(message?: string);
44
58
  }
45
59
  /** Opens the calling end of a link on `port` (one port per link). */
46
- export declare function connectHttpLink(port: MessagePort, { streamTimeout }?: HttpLinkOptions): Promise<HttpLink>;
60
+ export declare function connectHttpLink(port: MessagePort, { streamTimeout, maxPorts }?: HttpLinkOptions): Promise<HttpLink>;
47
61
  /**
48
62
  * Serves `handler` on the answering end of a link. Resolves with a teardown
49
63
  * that abandons the requests still running (their handlers see the
50
64
  * cancellation) and releases the port.
51
65
  */
52
- export declare function serveHttpLink(port: MessagePort, handler: (request: Request) => Response | Promise<Response>, { streamTimeout }?: HttpLinkOptions): Promise<() => Promise<void>>;
66
+ export declare function serveHttpLink(port: MessagePort, handler: (request: Request) => Response | Promise<Response>, { streamTimeout, maxPorts, requestIdleMs }?: HttpLinkOptions): Promise<() => Promise<void>>;
53
67
  /**
54
68
  * Wraps a `send` so the link knows its requests in flight (for draining and
55
69
  * load-based routing) and can fail them all at once when it is closed.
@@ -1 +1 @@
1
- {"version":3,"file":"http-link.d.ts","sourceRoot":"","sources":["../../src/link/http-link.ts"],"names":[],"mappings":"AAGA;;;;;;;;GAQG;AACH,eAAO,MAAM,kBAAkB,uBAAuB,CAAC;AAEvD,MAAM,WAAW,eAAe;IAC9B;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,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,GAAE,eAAoB,GACtC,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,GAAE,eAAoB,GACtC,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,CAuEV"}
1
+ {"version":3,"file":"http-link.d.ts","sourceRoot":"","sources":["../../src/link/http-link.ts"],"names":[],"mappings":"AAIA;;;;;;;;GAQG;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;CACxB;AAED,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,GAAE,eAAoB,GAChD,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"}