@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 +327 -51
- package/dist/link/async-dispose.d.ts +10 -0
- package/dist/link/async-dispose.d.ts.map +1 -0
- package/dist/link/http-link.d.ts +16 -2
- package/dist/link/http-link.d.ts.map +1 -1
- package/dist/relay-sw.js +203 -44
- package/dist/relay-worker.js +203 -44
- package/dist/sw-worker.js +324 -51
- package/dist/sw.js +323 -50
- package/dist/worker.js +324 -51
- package/package.json +4 -4
- package/src/link/async-dispose.ts +22 -0
- package/src/link/http-link.ts +22 -6
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
|
-
|
|
2187
|
-
const release =
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
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`,
|
|
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
|
|
2205
|
-
*
|
|
2206
|
-
*
|
|
2207
|
-
*
|
|
2208
|
-
*
|
|
2209
|
-
*
|
|
2210
|
-
|
|
2211
|
-
|
|
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 (
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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) =>
|
|
3390
|
-
|
|
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
|
-
|
|
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"}
|
package/dist/link/http-link.d.ts
CHANGED
|
@@ -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":"
|
|
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"}
|