@statewalker/webrun-http-browser 0.7.0 → 0.9.0
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 +946 -120
- 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 +809 -132
- package/dist/relay-worker.js +809 -132
- package/dist/sw/protocol.d.ts +2 -0
- package/dist/sw/protocol.d.ts.map +1 -1
- package/dist/sw/sw-http-adapter.d.ts +2 -0
- package/dist/sw/sw-http-adapter.d.ts.map +1 -1
- package/dist/sw/worker-service.d.ts.map +1 -1
- package/dist/sw-worker.js +929 -118
- package/dist/sw.js +942 -119
- package/dist/worker.js +938 -119
- package/package.json +4 -4
- package/src/link/async-dispose.ts +22 -0
- package/src/link/http-link.ts +64 -12
- package/src/sw/protocol.ts +4 -3
- package/src/sw/sw-http-adapter.ts +7 -0
- package/src/sw/worker-service.ts +14 -1
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;
|
|
2191
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));
|
|
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,17 +2584,21 @@ 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
|
-
return new Response(
|
|
2601
|
+
return new Response(responseBody(respBody, request.signal, release), responseInit);
|
|
2514
2602
|
}
|
|
2515
2603
|
/**
|
|
2516
2604
|
* Wrap a `(Request) => Promise<Response>` handler as a `Duplex` so it can be
|
|
@@ -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,32 +2634,173 @@ 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
|
}
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
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
|
+
};
|
|
2560
2731
|
}
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2732
|
+
/**
|
|
2733
|
+
* How long a cancelled body waits for the read in flight before it abandons the call anyway.
|
|
2734
|
+
* The read's answer tells a body that has ended (the call is finished: an upload the handler
|
|
2735
|
+
* reads after answering goes on) from one that has not; a peer that sends nothing more never
|
|
2736
|
+
* answers it. A body whose end comes later than this after the cancel is abandoned too, so a
|
|
2737
|
+
* handler still streaming its response and still reading the upload then loses the upload.
|
|
2738
|
+
*/
|
|
2739
|
+
const CANCEL_GRACE_MS = 1e3;
|
|
2740
|
+
/**
|
|
2741
|
+
* The response body as a stream that the request's signal can end at any time.
|
|
2742
|
+
*
|
|
2743
|
+
* An abort errors the stream with the abort's reason at once — a read waiting
|
|
2744
|
+
* for a peer that sends nothing more rejects then, not when a next chunk
|
|
2745
|
+
* arrives, which may be never — and abandons the call (`release`), so on a
|
|
2746
|
+
* multiplexed transport its port is given back whether or not anyone still
|
|
2747
|
+
* reads the body. The listener is removed once the body ends, however it ends:
|
|
2748
|
+
* read to its end, cancelled, failed or aborted. A body nobody reads or cancels
|
|
2749
|
+
* keeps it until the signal aborts (and keeps the call open until then anyway).
|
|
2750
|
+
* A cancel abandons the call too — unless the read in flight comes back with the
|
|
2751
|
+
* body's end, which finishes it instead (see `cancel`).
|
|
2752
|
+
*/
|
|
2753
|
+
function responseBody(body, signal, release) {
|
|
2754
|
+
const it = body[Symbol.asyncIterator]();
|
|
2755
|
+
let controller;
|
|
2756
|
+
let ended = false;
|
|
2757
|
+
let reading = false;
|
|
2758
|
+
const end = () => {
|
|
2759
|
+
ended = true;
|
|
2760
|
+
signal?.removeEventListener("abort", onAbort);
|
|
2761
|
+
};
|
|
2762
|
+
const onAbort = () => {
|
|
2763
|
+
if (ended) return;
|
|
2764
|
+
end();
|
|
2765
|
+
try {
|
|
2766
|
+
controller.error(abortReason$1(signal));
|
|
2767
|
+
} catch {}
|
|
2768
|
+
release?.();
|
|
2769
|
+
};
|
|
2770
|
+
return new ReadableStream({
|
|
2771
|
+
start(c) {
|
|
2772
|
+
controller = c;
|
|
2773
|
+
if (signal?.aborted) onAbort();
|
|
2774
|
+
else signal?.addEventListener("abort", onAbort, { once: true });
|
|
2775
|
+
},
|
|
2776
|
+
async pull(c) {
|
|
2777
|
+
try {
|
|
2778
|
+
reading = true;
|
|
2779
|
+
const { done, value } = await it.next().finally(() => {
|
|
2780
|
+
reading = false;
|
|
2781
|
+
});
|
|
2782
|
+
if (ended) return;
|
|
2783
|
+
if (done) {
|
|
2784
|
+
end();
|
|
2785
|
+
c.close();
|
|
2786
|
+
return;
|
|
2787
|
+
}
|
|
2788
|
+
c.enqueue(value);
|
|
2789
|
+
} catch (err) {
|
|
2790
|
+
if (ended) return;
|
|
2791
|
+
end();
|
|
2792
|
+
c.error(err);
|
|
2793
|
+
}
|
|
2794
|
+
},
|
|
2795
|
+
cancel() {
|
|
2796
|
+
end();
|
|
2797
|
+
const returned = Promise.resolve(it.return?.()).catch(() => {});
|
|
2798
|
+
if (!reading) return;
|
|
2799
|
+
const grace = setTimeout(() => void release?.(), CANCEL_GRACE_MS);
|
|
2800
|
+
grace.unref?.();
|
|
2801
|
+
returned.finally(() => clearTimeout(grace));
|
|
2802
|
+
}
|
|
2803
|
+
});
|
|
2567
2804
|
}
|
|
2568
2805
|
/**
|
|
2569
2806
|
* Host a `SiteHandler` over a `Connect/Serve` pair. The `start(serve, params)`
|
|
@@ -2850,8 +3087,11 @@ function listenPort(port, handler, { channelName = "", log = () => {} } = {}) {
|
|
|
2850
3087
|
error
|
|
2851
3088
|
});
|
|
2852
3089
|
};
|
|
2853
|
-
|
|
2854
|
-
|
|
3090
|
+
const listener = (event) => {
|
|
3091
|
+
onMessage(event);
|
|
3092
|
+
};
|
|
3093
|
+
port.addEventListener("message", listener);
|
|
3094
|
+
return () => port.removeEventListener("message", listener);
|
|
2855
3095
|
}
|
|
2856
3096
|
/**
|
|
2857
3097
|
* Wraps an async iterable so that an `AbortSignal` firing causes the wrapper
|
|
@@ -2881,6 +3121,69 @@ async function* throughAbort(input, signal) {
|
|
|
2881
3121
|
}
|
|
2882
3122
|
}
|
|
2883
3123
|
/**
|
|
3124
|
+
* Makes `target` disposable with `await using`: installs `dispose` as its
|
|
3125
|
+
* `[Symbol.asyncDispose]`, where the runtime defines the symbol. Hand-written
|
|
3126
|
+
* iterators lose what a native generator gets from `AsyncIteratorPrototype`;
|
|
3127
|
+
* this gives it back. A no-op where the symbol is missing. Kept off the
|
|
3128
|
+
* declared types, which would require consumers to compile with the
|
|
3129
|
+
* `esnext.disposable` lib.
|
|
3130
|
+
*/
|
|
3131
|
+
function asyncDisposable$1(target, dispose) {
|
|
3132
|
+
const asyncDispose = Symbol.asyncDispose;
|
|
3133
|
+
if (asyncDispose && !(asyncDispose in target)) Object.defineProperty(target, asyncDispose, {
|
|
3134
|
+
value: async () => {
|
|
3135
|
+
await dispose();
|
|
3136
|
+
},
|
|
3137
|
+
configurable: true,
|
|
3138
|
+
writable: true
|
|
3139
|
+
});
|
|
3140
|
+
return target;
|
|
3141
|
+
}
|
|
3142
|
+
/**
|
|
3143
|
+
* `gen`, except that `return()` and `throw()` take effect at once.
|
|
3144
|
+
*
|
|
3145
|
+
* On an async generator both are queued behind a `next()` that is still
|
|
3146
|
+
* pending: a consumer that gives up on a stream while it waits for the peer's
|
|
3147
|
+
* next chunk — an aborted fetch whose response head has not come, say — cannot
|
|
3148
|
+
* end it until that chunk arrives, which for a peer that never answers is
|
|
3149
|
+
* never. The stream's `finally` (which closes its port and tells the peer)
|
|
3150
|
+
* then never runs, and on a multiplexer the port is kept for good.
|
|
3151
|
+
*
|
|
3152
|
+
* So `abandon` runs first, synchronously: it must make the pending `next()`
|
|
3153
|
+
* settle (abort the stream), which then resolves done, after which the
|
|
3154
|
+
* generator's own `return()` / `throw()` proceeds as usual. On a generator
|
|
3155
|
+
* suspended at a `yield` it is the same ending, a step earlier. `abandon` runs once, whichever comes first.
|
|
3156
|
+
*/
|
|
3157
|
+
function returningAtOnce(gen, abandon) {
|
|
3158
|
+
let abandoned = false;
|
|
3159
|
+
const giveUp = (reason) => {
|
|
3160
|
+
if (abandoned) return;
|
|
3161
|
+
abandoned = true;
|
|
3162
|
+
abandon(reason);
|
|
3163
|
+
};
|
|
3164
|
+
const wrapped = {
|
|
3165
|
+
next: (...args) => gen.next(...args).catch((error) => {
|
|
3166
|
+
if (abandoned) return {
|
|
3167
|
+
done: true,
|
|
3168
|
+
value: void 0
|
|
3169
|
+
};
|
|
3170
|
+
throw error;
|
|
3171
|
+
}),
|
|
3172
|
+
return(value) {
|
|
3173
|
+
giveUp();
|
|
3174
|
+
return gen.return(value);
|
|
3175
|
+
},
|
|
3176
|
+
throw(error) {
|
|
3177
|
+
giveUp(error);
|
|
3178
|
+
return gen.throw(error);
|
|
3179
|
+
},
|
|
3180
|
+
[Symbol.asyncIterator]() {
|
|
3181
|
+
return wrapped;
|
|
3182
|
+
}
|
|
3183
|
+
};
|
|
3184
|
+
return asyncDisposable$1(wrapped, () => wrapped.return(void 0));
|
|
3185
|
+
}
|
|
3186
|
+
/**
|
|
2884
3187
|
* The `type` of the out-of-band notice a side posts when it abandons a stream.
|
|
2885
3188
|
*
|
|
2886
3189
|
* Layer 1's `close` is not observable to layer 2 — a closed virtual port drops
|
|
@@ -2976,7 +3279,7 @@ function runCallerSide(port, input, options) {
|
|
|
2976
3279
|
const inbound = receiveChunks(port, CHANNEL_OUT, controller, clock.touch);
|
|
2977
3280
|
const pump = sendChunks(port, CHANNEL_IN, input, options, controller.signal, clock.touch);
|
|
2978
3281
|
pump.catch(() => {});
|
|
2979
|
-
return (async function* () {
|
|
3282
|
+
return returningAtOnce((async function* () {
|
|
2980
3283
|
try {
|
|
2981
3284
|
yield* inbound.stream;
|
|
2982
3285
|
} finally {
|
|
@@ -2990,7 +3293,9 @@ function runCallerSide(port, input, options) {
|
|
|
2990
3293
|
await port.close?.();
|
|
2991
3294
|
} catch {}
|
|
2992
3295
|
}
|
|
2993
|
-
})()
|
|
3296
|
+
})(), () => {
|
|
3297
|
+
if (!controller.signal.aborted) controller.abort(/* @__PURE__ */ new Error("webrun-rpc: the caller abandoned the stream"));
|
|
3298
|
+
});
|
|
2994
3299
|
}
|
|
2995
3300
|
/**
|
|
2996
3301
|
* Listens for the peer's abort notice, and can post our own. The notice is a
|
|
@@ -3191,6 +3496,33 @@ async function sendChunks(port, channelName, output, { maxMessageSize, log }, si
|
|
|
3191
3496
|
}
|
|
3192
3497
|
}
|
|
3193
3498
|
/**
|
|
3499
|
+
* Runs `fn` on a later task, never a microtask: a producer looping on
|
|
3500
|
+
* `whenPortReady` must let the transport's own events run. `setImmediate`
|
|
3501
|
+
* where it exists; elsewhere a `MessageChannel`, because nested `setTimeout`s
|
|
3502
|
+
* are clamped to 4 ms in browsers.
|
|
3503
|
+
*/
|
|
3504
|
+
const setImmediateFn = globalThis.setImmediate;
|
|
3505
|
+
/** @internal */
|
|
3506
|
+
const nextTask = setImmediateFn ? (fn) => {
|
|
3507
|
+
setImmediateFn(fn);
|
|
3508
|
+
} : (() => {
|
|
3509
|
+
let channel;
|
|
3510
|
+
const queue = [];
|
|
3511
|
+
return (fn) => {
|
|
3512
|
+
if (!channel) {
|
|
3513
|
+
channel = new MessageChannel();
|
|
3514
|
+
channel.port1.onmessage = () => queue.shift()?.();
|
|
3515
|
+
}
|
|
3516
|
+
queue.push(fn);
|
|
3517
|
+
channel.port2.postMessage(0);
|
|
3518
|
+
};
|
|
3519
|
+
})();
|
|
3520
|
+
const states = /* @__PURE__ */ new WeakMap();
|
|
3521
|
+
/** @internal Registered by the multiplexer for each port with a window. */
|
|
3522
|
+
function setPortSendState(port, state) {
|
|
3523
|
+
states.set(port, state);
|
|
3524
|
+
}
|
|
3525
|
+
/**
|
|
3194
3526
|
* One virtual port.
|
|
3195
3527
|
*
|
|
3196
3528
|
* `deliver` and `markClosed` are deliberately not on `port`: the consumer holds
|
|
@@ -3205,6 +3537,14 @@ function newVirtualPort(send, requestClose) {
|
|
|
3205
3537
|
if (closing.signal.aborted) return;
|
|
3206
3538
|
closing.abort(reason ?? /* @__PURE__ */ new Error("webrun-rpc: the port was closed"));
|
|
3207
3539
|
};
|
|
3540
|
+
const close = (reason) => {
|
|
3541
|
+
if (closed) return;
|
|
3542
|
+
closed = true;
|
|
3543
|
+
const notify = requestClose;
|
|
3544
|
+
listeners.clear();
|
|
3545
|
+
notify(reason);
|
|
3546
|
+
abort(reason);
|
|
3547
|
+
};
|
|
3208
3548
|
const port = {
|
|
3209
3549
|
addEventListener(_type, listener) {
|
|
3210
3550
|
listeners.add(listener);
|
|
@@ -3216,24 +3556,28 @@ function newVirtualPort(send, requestClose) {
|
|
|
3216
3556
|
if (closed) return;
|
|
3217
3557
|
send(message, transfer);
|
|
3218
3558
|
},
|
|
3219
|
-
close
|
|
3220
|
-
if (closed) return;
|
|
3221
|
-
closed = true;
|
|
3222
|
-
const notify = requestClose;
|
|
3223
|
-
listeners.clear();
|
|
3224
|
-
notify();
|
|
3225
|
-
abort();
|
|
3226
|
-
}
|
|
3559
|
+
close
|
|
3227
3560
|
};
|
|
3228
3561
|
setPortCloseSignal(port, closing.signal);
|
|
3562
|
+
const dispose = Symbol.dispose;
|
|
3563
|
+
if (dispose) Object.defineProperty(port, dispose, {
|
|
3564
|
+
value: () => close(),
|
|
3565
|
+
configurable: true
|
|
3566
|
+
});
|
|
3229
3567
|
return {
|
|
3230
3568
|
port,
|
|
3231
3569
|
deliver(payload) {
|
|
3232
3570
|
if (closed) return;
|
|
3233
3571
|
const event = new MessageEvent("message", { data: payload });
|
|
3572
|
+
let pending;
|
|
3234
3573
|
for (const listener of [...listeners]) try {
|
|
3235
|
-
listener(event);
|
|
3574
|
+
const result = listener(event);
|
|
3575
|
+
if (result && typeof result.then === "function") {
|
|
3576
|
+
pending ??= [];
|
|
3577
|
+
pending.push(result);
|
|
3578
|
+
}
|
|
3236
3579
|
} catch {}
|
|
3580
|
+
return pending ? Promise.allSettled(pending) : void 0;
|
|
3237
3581
|
},
|
|
3238
3582
|
markClosed(reason) {
|
|
3239
3583
|
closed = true;
|
|
@@ -3242,53 +3586,309 @@ function newVirtualPort(send, requestClose) {
|
|
|
3242
3586
|
},
|
|
3243
3587
|
isClosed() {
|
|
3244
3588
|
return closed;
|
|
3589
|
+
},
|
|
3590
|
+
hasListeners() {
|
|
3591
|
+
return listeners.size > 0;
|
|
3245
3592
|
}
|
|
3246
3593
|
};
|
|
3247
3594
|
}
|
|
3248
3595
|
/**
|
|
3249
|
-
* The
|
|
3250
|
-
*
|
|
3596
|
+
* The id of keepalive's `ping` / `pong`: one no port ever takes. An older mux
|
|
3597
|
+
* whose codec passes unknown envelopes through reads them as a close of that
|
|
3598
|
+
* id, which must therefore not be a real port (0 is the initiator's first).
|
|
3251
3599
|
*/
|
|
3252
|
-
const
|
|
3600
|
+
const KEEPALIVE_ID = Number.MAX_SAFE_INTEGER;
|
|
3601
|
+
/** The longest delay a timer holds; past it `setInterval` fires at once. */
|
|
3602
|
+
const MAX_TIMER_MS = 2 ** 31 - 1;
|
|
3253
3603
|
/** Ceiling on concurrently open virtual ports. Bounds the id table only. */
|
|
3254
3604
|
const DEFAULT_MAX_PORTS = 1024;
|
|
3255
3605
|
/**
|
|
3606
|
+
* What a message costs a port's window when the codec cannot say
|
|
3607
|
+
* (`PortCodec.encode`): its bytes and string lengths, found in its values up
|
|
3608
|
+
* to 4 levels deep (at most 256 values), and at least 64. Both ends compute it
|
|
3609
|
+
* from the same structured value, so they agree.
|
|
3610
|
+
*/
|
|
3611
|
+
function estimateMessageSize(payload) {
|
|
3612
|
+
let budget = 256;
|
|
3613
|
+
const walk = (v, depth) => {
|
|
3614
|
+
if (budget-- <= 0) return 0;
|
|
3615
|
+
if (typeof v === "string") return v.length;
|
|
3616
|
+
if (v instanceof ArrayBuffer) return v.byteLength;
|
|
3617
|
+
if (ArrayBuffer.isView(v)) return v.byteLength;
|
|
3618
|
+
if (v === null || typeof v !== "object" || depth >= 4) return 8;
|
|
3619
|
+
let n = 16;
|
|
3620
|
+
if (Array.isArray(v)) for (const x of v) n += walk(x, depth + 1);
|
|
3621
|
+
else for (const k in v) n += k.length + walk(v[k], depth + 1);
|
|
3622
|
+
return n;
|
|
3623
|
+
};
|
|
3624
|
+
return Math.max(64, walk(payload, 0));
|
|
3625
|
+
}
|
|
3626
|
+
/**
|
|
3256
3627
|
* The default `PortMux`: emulates multiplexing over a single port.
|
|
3257
3628
|
*
|
|
3258
3629
|
* A transport that already multiplexes natively supplies its own `PortMux`
|
|
3259
3630
|
* instead — this implementation is for transports that offer one pipe.
|
|
3260
3631
|
*/
|
|
3261
3632
|
function multiplexPort(port, options) {
|
|
3262
|
-
const { codec, onPort, side = "initiator", maxPorts = DEFAULT_MAX_PORTS, maxMessageSize } = options;
|
|
3633
|
+
const { codec, onPort, side = "initiator", maxPorts = DEFAULT_MAX_PORTS, maxMessageSize, signal, window, frameSize, keepalive, onLeak } = options;
|
|
3634
|
+
const misses = keepalive?.misses ?? 3;
|
|
3635
|
+
if (keepalive && !(Number.isFinite(keepalive.intervalMs) && keepalive.intervalMs > 0 && keepalive.intervalMs <= MAX_TIMER_MS && Number.isSafeInteger(misses) && misses > 0)) throw new RangeError(`webrun-rpc: keepalive needs an intervalMs in (0, ${MAX_TIMER_MS}] and a positive integer misses`);
|
|
3636
|
+
if (frameSize != null && !(Number.isSafeInteger(frameSize) && frameSize > 0)) throw new RangeError(`webrun-rpc: frameSize must be a positive integer, got ${frameSize}`);
|
|
3263
3637
|
const open = /* @__PURE__ */ new Map();
|
|
3638
|
+
const leaks = onLeak ? new FinalizationRegistry(({ id, record, release }) => {
|
|
3639
|
+
const entry = open.get(id);
|
|
3640
|
+
if (entry?.handle !== record) return;
|
|
3641
|
+
const { handle: _, credit: __, partial: ___, ...info } = entry;
|
|
3642
|
+
if (!record.hasListeners()) {
|
|
3643
|
+
release("leaked");
|
|
3644
|
+
record.markClosed(/* @__PURE__ */ new Error("webrun-rpc: the port was dropped without close()"));
|
|
3645
|
+
}
|
|
3646
|
+
try {
|
|
3647
|
+
onLeak?.(info);
|
|
3648
|
+
} catch {}
|
|
3649
|
+
}) : void 0;
|
|
3650
|
+
const touch = (id) => {
|
|
3651
|
+
const entry = open.get(id);
|
|
3652
|
+
if (entry) entry.lastActivity = Date.now();
|
|
3653
|
+
};
|
|
3264
3654
|
let nextId = side === "initiator" ? 0 : 1;
|
|
3265
3655
|
let muxClosed = false;
|
|
3656
|
+
/** This end counts encoded bytes and reassembles frames. */
|
|
3657
|
+
const exactCodec = codec.encode != null && codec.decode != null;
|
|
3266
3658
|
const post = (envelope, transfer) => {
|
|
3267
3659
|
if (muxClosed) return;
|
|
3660
|
+
codecPost(envelope, transfer);
|
|
3661
|
+
};
|
|
3662
|
+
/** Like `post`, also while closing: the mux's own close notices. */
|
|
3663
|
+
const codecPost = (envelope, transfer) => {
|
|
3268
3664
|
try {
|
|
3269
3665
|
codec.post(port, envelope, transfer);
|
|
3270
3666
|
} catch {}
|
|
3271
3667
|
};
|
|
3272
|
-
const
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3668
|
+
const outgoing = /* @__PURE__ */ new Map();
|
|
3669
|
+
const ring = [];
|
|
3670
|
+
let pumping = false;
|
|
3671
|
+
const pump = async () => {
|
|
3672
|
+
pumping = true;
|
|
3673
|
+
try {
|
|
3674
|
+
while (ring.length && !muxClosed) {
|
|
3675
|
+
if (port.whenDrained) await port.whenDrained();
|
|
3676
|
+
if (muxClosed) break;
|
|
3677
|
+
const id = ring.shift();
|
|
3678
|
+
const queue = outgoing.get(id);
|
|
3679
|
+
const item = queue?.shift();
|
|
3680
|
+
if (queue?.length) ring.push(id);
|
|
3681
|
+
else outgoing.delete(id);
|
|
3682
|
+
item?.();
|
|
3683
|
+
}
|
|
3684
|
+
} catch (err) {
|
|
3685
|
+
close(err).catch(() => {});
|
|
3686
|
+
} finally {
|
|
3687
|
+
pumping = false;
|
|
3688
|
+
}
|
|
3689
|
+
};
|
|
3690
|
+
const sendOnPort = (id, item) => {
|
|
3691
|
+
if (frameSize == null) {
|
|
3692
|
+
item();
|
|
3693
|
+
return;
|
|
3694
|
+
}
|
|
3695
|
+
let queue = outgoing.get(id);
|
|
3696
|
+
if (!queue) {
|
|
3697
|
+
queue = [];
|
|
3698
|
+
outgoing.set(id, queue);
|
|
3699
|
+
ring.push(id);
|
|
3700
|
+
}
|
|
3701
|
+
queue.push(item);
|
|
3702
|
+
if (!pumping) pump();
|
|
3703
|
+
};
|
|
3704
|
+
const dropOutgoing = (id) => {
|
|
3705
|
+
if (!outgoing.delete(id)) return;
|
|
3706
|
+
const at = ring.indexOf(id);
|
|
3707
|
+
if (at >= 0) ring.splice(at, 1);
|
|
3708
|
+
};
|
|
3709
|
+
const postBytes = (bytes) => {
|
|
3710
|
+
if (muxClosed) return;
|
|
3711
|
+
try {
|
|
3712
|
+
port.postMessage(bytes);
|
|
3713
|
+
} catch {}
|
|
3714
|
+
};
|
|
3715
|
+
const releaseWaiters = (c) => {
|
|
3716
|
+
for (const w of c?.waiters.splice(0) ?? []) w.resolve();
|
|
3717
|
+
};
|
|
3718
|
+
const flush = (c) => {
|
|
3719
|
+
while (c.queue.length) {
|
|
3720
|
+
const next = c.queue[0];
|
|
3721
|
+
if (c.enforced && c.outstanding > 0 && c.outstanding + next.size > c.window) break;
|
|
3722
|
+
c.queue.shift();
|
|
3723
|
+
c.queued -= next.size;
|
|
3724
|
+
c.outstanding += next.size;
|
|
3725
|
+
next.send();
|
|
3726
|
+
}
|
|
3727
|
+
for (const w of [...c.waiters]) if (c.queued <= w.below) {
|
|
3728
|
+
c.waiters.splice(c.waiters.indexOf(w), 1);
|
|
3729
|
+
w.resolve();
|
|
3730
|
+
}
|
|
3731
|
+
};
|
|
3732
|
+
const grant = (id, c, bytes) => {
|
|
3733
|
+
c.unconsumed -= bytes;
|
|
3734
|
+
c.toGrant += bytes;
|
|
3735
|
+
const send = () => {
|
|
3736
|
+
c.grantScheduled = false;
|
|
3737
|
+
if (!c.toGrant || !open.has(id)) return;
|
|
3279
3738
|
post({
|
|
3739
|
+
type: "credit",
|
|
3740
|
+
id,
|
|
3741
|
+
bytes: c.toGrant
|
|
3742
|
+
});
|
|
3743
|
+
c.toGrant = 0;
|
|
3744
|
+
};
|
|
3745
|
+
if (c.toGrant >= c.window / 4) {
|
|
3746
|
+
send();
|
|
3747
|
+
return;
|
|
3748
|
+
}
|
|
3749
|
+
if (c.grantScheduled) return;
|
|
3750
|
+
c.grantScheduled = true;
|
|
3751
|
+
setTimeout(send, 0);
|
|
3752
|
+
};
|
|
3753
|
+
const attach = (id, meta, portWindow, opener, exact) => {
|
|
3754
|
+
const c = typeof portWindow === "number" && Number.isSafeInteger(portWindow) && portWindow > 0 ? {
|
|
3755
|
+
window: portWindow,
|
|
3756
|
+
enforced: false,
|
|
3757
|
+
opener,
|
|
3758
|
+
exact,
|
|
3759
|
+
outstanding: 0,
|
|
3760
|
+
queue: [],
|
|
3761
|
+
queued: 0,
|
|
3762
|
+
unconsumed: 0,
|
|
3763
|
+
waiters: [],
|
|
3764
|
+
toGrant: 0,
|
|
3765
|
+
grantScheduled: false
|
|
3766
|
+
} : void 0;
|
|
3767
|
+
const self = newVirtualPort((payload, transfer) => {
|
|
3768
|
+
touch(id);
|
|
3769
|
+
const envelope = {
|
|
3770
|
+
type: "message",
|
|
3771
|
+
id,
|
|
3772
|
+
payload
|
|
3773
|
+
};
|
|
3774
|
+
if (!c) {
|
|
3775
|
+
post(envelope, transfer);
|
|
3776
|
+
return;
|
|
3777
|
+
}
|
|
3778
|
+
const bytes = c.exact ? codec.encode?.(envelope) : void 0;
|
|
3779
|
+
const size = bytes?.byteLength ?? estimateMessageSize(payload);
|
|
3780
|
+
const waits = frameSize != null || c.queue.length > 0 || c.enforced && c.outstanding > 0 && c.outstanding + size > c.window;
|
|
3781
|
+
const held = !bytes && waits ? {
|
|
3782
|
+
...envelope,
|
|
3783
|
+
payload: structuredClone(payload, { transfer })
|
|
3784
|
+
} : void 0;
|
|
3785
|
+
const send = () => {
|
|
3786
|
+
if (!bytes) {
|
|
3787
|
+
sendOnPort(id, held ? () => post(held) : () => post(envelope, transfer));
|
|
3788
|
+
return;
|
|
3789
|
+
}
|
|
3790
|
+
if (frameSize != null && c.enforced && c.exact && bytes.byteLength > frameSize) {
|
|
3791
|
+
for (let at = 0; at < bytes.byteLength; at += frameSize) {
|
|
3792
|
+
const data = bytes.subarray(at, at + frameSize);
|
|
3793
|
+
const frame = at + frameSize >= bytes.byteLength ? {
|
|
3794
|
+
type: "frame",
|
|
3795
|
+
id,
|
|
3796
|
+
data,
|
|
3797
|
+
last: true
|
|
3798
|
+
} : {
|
|
3799
|
+
type: "frame",
|
|
3800
|
+
id,
|
|
3801
|
+
data
|
|
3802
|
+
};
|
|
3803
|
+
sendOnPort(id, () => post(frame));
|
|
3804
|
+
}
|
|
3805
|
+
return;
|
|
3806
|
+
}
|
|
3807
|
+
sendOnPort(id, () => postBytes(bytes));
|
|
3808
|
+
};
|
|
3809
|
+
c.queue.push({
|
|
3810
|
+
send,
|
|
3811
|
+
size
|
|
3812
|
+
});
|
|
3813
|
+
c.queued += size;
|
|
3814
|
+
flush(c);
|
|
3815
|
+
}, (reason) => {
|
|
3816
|
+
release(reason);
|
|
3817
|
+
record.markClosed(reason);
|
|
3818
|
+
});
|
|
3819
|
+
function release(reason) {
|
|
3820
|
+
open.delete(id);
|
|
3821
|
+
leaks?.unregister(record);
|
|
3822
|
+
if (c?.queue.length) {
|
|
3823
|
+
for (const item of c.queue.splice(0)) item.send();
|
|
3824
|
+
c.queued = 0;
|
|
3825
|
+
}
|
|
3826
|
+
sendOnPort(id, () => post({
|
|
3280
3827
|
type: "close",
|
|
3281
3828
|
id,
|
|
3282
3829
|
reason
|
|
3830
|
+
}));
|
|
3831
|
+
releaseWaiters(c);
|
|
3832
|
+
}
|
|
3833
|
+
const { markClosed, isClosed, hasListeners } = self;
|
|
3834
|
+
if (c) {
|
|
3835
|
+
const deliver = self.deliver;
|
|
3836
|
+
self.deliver = (payload, size = estimateMessageSize(payload)) => {
|
|
3837
|
+
c.unconsumed += size;
|
|
3838
|
+
const done = deliver(payload);
|
|
3839
|
+
if (done) done.then(() => grant(id, c, size));
|
|
3840
|
+
else grant(id, c, size);
|
|
3841
|
+
return done;
|
|
3842
|
+
};
|
|
3843
|
+
setPortSendState(self.port, {
|
|
3844
|
+
bufferedAmount: () => c.queued,
|
|
3845
|
+
ready: (below = 0) => new Promise((resolve) => {
|
|
3846
|
+
if (c.queued <= below || isClosed()) nextTask(resolve);
|
|
3847
|
+
else c.waiters.push({
|
|
3848
|
+
below,
|
|
3849
|
+
resolve: () => nextTask(resolve)
|
|
3850
|
+
});
|
|
3851
|
+
})
|
|
3283
3852
|
});
|
|
3284
|
-
|
|
3285
|
-
|
|
3853
|
+
}
|
|
3854
|
+
const record = {
|
|
3855
|
+
deliver: self.deliver,
|
|
3856
|
+
markClosed,
|
|
3857
|
+
isClosed,
|
|
3858
|
+
hasListeners,
|
|
3859
|
+
close: (reason) => {
|
|
3860
|
+
if (isClosed()) return;
|
|
3861
|
+
release(reason);
|
|
3862
|
+
markClosed(reason);
|
|
3863
|
+
}
|
|
3864
|
+
};
|
|
3865
|
+
leaks?.register(self.port, {
|
|
3866
|
+
id,
|
|
3867
|
+
record,
|
|
3868
|
+
release
|
|
3869
|
+
}, record);
|
|
3870
|
+
const now = Date.now();
|
|
3871
|
+
open.set(id, {
|
|
3872
|
+
id,
|
|
3873
|
+
meta,
|
|
3874
|
+
openedAt: now,
|
|
3875
|
+
lastActivity: now,
|
|
3876
|
+
handle: record,
|
|
3877
|
+
credit: c
|
|
3286
3878
|
});
|
|
3287
|
-
open.set(id, self);
|
|
3288
3879
|
return self;
|
|
3289
3880
|
};
|
|
3290
|
-
const handleEnvelope = (envelope) => {
|
|
3881
|
+
const handleEnvelope = (envelope, event) => {
|
|
3291
3882
|
if (muxClosed) return;
|
|
3883
|
+
heardSincePing = true;
|
|
3884
|
+
if (envelope.type === "ping" || envelope.type === "pong") {
|
|
3885
|
+
peerKeepsAlive = true;
|
|
3886
|
+
if (envelope.type === "ping") post({
|
|
3887
|
+
type: "pong",
|
|
3888
|
+
id: KEEPALIVE_ID
|
|
3889
|
+
});
|
|
3890
|
+
return;
|
|
3891
|
+
}
|
|
3292
3892
|
if (envelope.type === "open") {
|
|
3293
3893
|
if (open.has(envelope.id)) return;
|
|
3294
3894
|
if (open.size >= maxPorts) {
|
|
@@ -3299,7 +3899,18 @@ function multiplexPort(port, options) {
|
|
|
3299
3899
|
});
|
|
3300
3900
|
return;
|
|
3301
3901
|
}
|
|
3302
|
-
const handle = attach(envelope.id);
|
|
3902
|
+
const handle = attach(envelope.id, envelope.meta, envelope.window, false, envelope.exact === true && exactCodec);
|
|
3903
|
+
const credit = open.get(envelope.id)?.credit;
|
|
3904
|
+
if (credit) post(credit.exact ? {
|
|
3905
|
+
type: "credit",
|
|
3906
|
+
id: envelope.id,
|
|
3907
|
+
bytes: 0,
|
|
3908
|
+
exact: true
|
|
3909
|
+
} : {
|
|
3910
|
+
type: "credit",
|
|
3911
|
+
id: envelope.id,
|
|
3912
|
+
bytes: 0
|
|
3913
|
+
});
|
|
3303
3914
|
let accepted = false;
|
|
3304
3915
|
if (onPort) try {
|
|
3305
3916
|
accepted = onPort(handle.port, envelope.meta) !== false;
|
|
@@ -3314,57 +3925,191 @@ function multiplexPort(port, options) {
|
|
|
3314
3925
|
id: envelope.id,
|
|
3315
3926
|
reason: "rejected"
|
|
3316
3927
|
});
|
|
3928
|
+
return;
|
|
3929
|
+
}
|
|
3930
|
+
return;
|
|
3931
|
+
}
|
|
3932
|
+
const entry = open.get(envelope.id);
|
|
3933
|
+
if (envelope.type === "credit") {
|
|
3934
|
+
const c = entry?.credit;
|
|
3935
|
+
if (!c || !(envelope.bytes >= 0)) return;
|
|
3936
|
+
if (!c.enforced && c.opener) {
|
|
3937
|
+
if (c.exact && envelope.exact !== true) {
|
|
3938
|
+
c.exact = false;
|
|
3939
|
+
c.outstanding = 0;
|
|
3940
|
+
}
|
|
3941
|
+
post({
|
|
3942
|
+
type: "credit",
|
|
3943
|
+
id: envelope.id,
|
|
3944
|
+
bytes: 0
|
|
3945
|
+
});
|
|
3317
3946
|
}
|
|
3947
|
+
c.enforced = true;
|
|
3948
|
+
c.outstanding = Math.max(0, c.outstanding - envelope.bytes);
|
|
3949
|
+
flush(c);
|
|
3318
3950
|
return;
|
|
3319
3951
|
}
|
|
3320
|
-
const handle = open.get(envelope.id);
|
|
3321
3952
|
if (envelope.type === "message") {
|
|
3322
|
-
|
|
3323
|
-
|
|
3953
|
+
const sizeOf = (c) => (c.exact ? byteLengthOf(event.data) : void 0) ?? estimateMessageSize(envelope.payload);
|
|
3954
|
+
if (!entry) return;
|
|
3955
|
+
touch(envelope.id);
|
|
3956
|
+
entry.handle.deliver(envelope.payload, entry.credit ? sizeOf(entry.credit) : void 0);
|
|
3324
3957
|
return;
|
|
3325
3958
|
}
|
|
3326
|
-
if (
|
|
3959
|
+
if (envelope.type === "frame") {
|
|
3960
|
+
if (!entry?.credit?.exact || !codec.decode || !(envelope.data instanceof Uint8Array)) return;
|
|
3961
|
+
touch(envelope.id);
|
|
3962
|
+
const partial = entry.partial ?? {
|
|
3963
|
+
chunks: [],
|
|
3964
|
+
bytes: 0
|
|
3965
|
+
};
|
|
3966
|
+
entry.partial = partial;
|
|
3967
|
+
partial.chunks.push(envelope.data);
|
|
3968
|
+
partial.bytes += envelope.data.byteLength;
|
|
3969
|
+
if (!envelope.last) return;
|
|
3970
|
+
entry.partial = void 0;
|
|
3971
|
+
const whole = new Uint8Array(partial.bytes);
|
|
3972
|
+
let at = 0;
|
|
3973
|
+
for (const chunk of partial.chunks) {
|
|
3974
|
+
whole.set(chunk, at);
|
|
3975
|
+
at += chunk.byteLength;
|
|
3976
|
+
}
|
|
3977
|
+
const inner = codec.decode(whole);
|
|
3978
|
+
if (inner?.type !== "message" || inner.id !== envelope.id) return;
|
|
3979
|
+
entry.handle.deliver(inner.payload, whole.byteLength);
|
|
3980
|
+
return;
|
|
3981
|
+
}
|
|
3982
|
+
dropOutgoing(envelope.id);
|
|
3983
|
+
if (!entry) return;
|
|
3327
3984
|
open.delete(envelope.id);
|
|
3328
|
-
|
|
3985
|
+
releaseWaiters(entry.credit);
|
|
3986
|
+
entry.handle.markClosed(/* @__PURE__ */ new Error("webrun-rpc: the peer closed the port"));
|
|
3329
3987
|
};
|
|
3330
3988
|
const listener = (event) => {
|
|
3331
3989
|
const envelope = codec.read(event);
|
|
3332
|
-
if (envelope) handleEnvelope(envelope);
|
|
3990
|
+
if (envelope) handleEnvelope(envelope, event);
|
|
3991
|
+
};
|
|
3992
|
+
let peerKeepsAlive = false;
|
|
3993
|
+
let heardSincePing = true;
|
|
3994
|
+
let unanswered = 0;
|
|
3995
|
+
let pinger;
|
|
3996
|
+
const tick = () => {
|
|
3997
|
+
if (!keepalive) return;
|
|
3998
|
+
const backlogged = (port.bufferedAmount ?? 0) > 0;
|
|
3999
|
+
if (heardSincePing) unanswered = 0;
|
|
4000
|
+
else if (peerKeepsAlive && !backlogged) unanswered++;
|
|
4001
|
+
if (unanswered >= misses) {
|
|
4002
|
+
close(/* @__PURE__ */ new Error(`webrun-rpc: the peer did not answer ${misses} keepalive pings, ${keepalive.intervalMs} ms apart`)).catch(() => {});
|
|
4003
|
+
return;
|
|
4004
|
+
}
|
|
4005
|
+
heardSincePing = false;
|
|
4006
|
+
post({
|
|
4007
|
+
type: "ping",
|
|
4008
|
+
id: KEEPALIVE_ID
|
|
4009
|
+
});
|
|
4010
|
+
};
|
|
4011
|
+
const close = async (reason) => {
|
|
4012
|
+
if (muxClosed) return;
|
|
4013
|
+
clearInterval(pinger);
|
|
4014
|
+
muxClosed = true;
|
|
4015
|
+
signal?.removeEventListener("abort", onTransportAbort);
|
|
4016
|
+
port.removeEventListener("message", listener);
|
|
4017
|
+
const closedNow = /* @__PURE__ */ new Set();
|
|
4018
|
+
for (const [id, { handle, credit }] of [...open]) {
|
|
4019
|
+
closedNow.add(id);
|
|
4020
|
+
codecPost({
|
|
4021
|
+
type: "close",
|
|
4022
|
+
id
|
|
4023
|
+
});
|
|
4024
|
+
open.delete(id);
|
|
4025
|
+
releaseWaiters(credit);
|
|
4026
|
+
handle.markClosed(reason ?? /* @__PURE__ */ new Error("webrun-rpc: the multiplexer was closed"));
|
|
4027
|
+
}
|
|
4028
|
+
for (const id of outgoing.keys()) if (!closedNow.has(id)) codecPost({
|
|
4029
|
+
type: "close",
|
|
4030
|
+
id
|
|
4031
|
+
});
|
|
4032
|
+
outgoing.clear();
|
|
4033
|
+
ring.length = 0;
|
|
4034
|
+
await port.close?.();
|
|
4035
|
+
};
|
|
4036
|
+
const onTransportAbort = () => {
|
|
4037
|
+
close(signal?.reason).catch(() => {});
|
|
3333
4038
|
};
|
|
3334
4039
|
port.addEventListener("message", listener);
|
|
3335
4040
|
port.start?.();
|
|
4041
|
+
if (keepalive) {
|
|
4042
|
+
pinger = setInterval(tick, keepalive.intervalMs);
|
|
4043
|
+
pinger.unref?.();
|
|
4044
|
+
}
|
|
4045
|
+
if (signal?.aborted) close(signal.reason).catch(() => {});
|
|
4046
|
+
else signal?.addEventListener("abort", onTransportAbort, { once: true });
|
|
3336
4047
|
return {
|
|
3337
4048
|
maxMessageSize,
|
|
3338
|
-
async openPort(meta) {
|
|
4049
|
+
async openPort(meta, { signal: portSignal } = {}) {
|
|
3339
4050
|
if (muxClosed) throw new Error("webrun-rpc: the multiplexer is closed");
|
|
3340
|
-
if (
|
|
4051
|
+
if (portSignal?.aborted) throw portSignal.reason;
|
|
4052
|
+
if (open.size >= maxPorts) throw new RangeError(`webrun-rpc: maxPorts (${maxPorts}) reached; open ports by kind: ${describeKinds(open.values())}`);
|
|
3341
4053
|
while (open.has(nextId)) nextId += 2;
|
|
3342
4054
|
const id = nextId;
|
|
3343
4055
|
nextId += 2;
|
|
3344
|
-
const handle = attach(id);
|
|
3345
|
-
post({
|
|
4056
|
+
const handle = attach(id, meta, window, true, exactCodec);
|
|
4057
|
+
post(window ? exactCodec ? {
|
|
4058
|
+
type: "open",
|
|
4059
|
+
id,
|
|
4060
|
+
meta,
|
|
4061
|
+
window,
|
|
4062
|
+
exact: true
|
|
4063
|
+
} : {
|
|
4064
|
+
type: "open",
|
|
4065
|
+
id,
|
|
4066
|
+
meta,
|
|
4067
|
+
window
|
|
4068
|
+
} : {
|
|
3346
4069
|
type: "open",
|
|
3347
4070
|
id,
|
|
3348
4071
|
meta
|
|
3349
4072
|
});
|
|
4073
|
+
if (portSignal) {
|
|
4074
|
+
const record = open.get(id)?.handle;
|
|
4075
|
+
const onAbort = () => {
|
|
4076
|
+
if (record && open.get(id)?.handle === record) record.close(portSignal.reason);
|
|
4077
|
+
};
|
|
4078
|
+
portSignal.addEventListener("abort", onAbort, { once: true });
|
|
4079
|
+
getPortCloseSignal(handle.port)?.addEventListener("abort", () => portSignal.removeEventListener("abort", onAbort), { once: true });
|
|
4080
|
+
}
|
|
3350
4081
|
return handle.port;
|
|
3351
4082
|
},
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
}
|
|
3362
|
-
muxClosed = true;
|
|
3363
|
-
port.removeEventListener("message", listener);
|
|
3364
|
-
await port.close?.();
|
|
4083
|
+
close,
|
|
4084
|
+
ports() {
|
|
4085
|
+
return [...open.values()].map(({ handle: _, credit, ...info }) => credit ? {
|
|
4086
|
+
...info,
|
|
4087
|
+
...credit.enforced ? { window: credit.window } : {},
|
|
4088
|
+
queued: credit.queued,
|
|
4089
|
+
outstanding: credit.outstanding,
|
|
4090
|
+
unconsumed: credit.unconsumed
|
|
4091
|
+
} : info);
|
|
3365
4092
|
}
|
|
3366
4093
|
};
|
|
3367
4094
|
}
|
|
4095
|
+
/** The byte length of a received message's data, or `undefined` if it is not bytes. */
|
|
4096
|
+
function byteLengthOf(data) {
|
|
4097
|
+
if (data instanceof ArrayBuffer || ArrayBuffer.isView(data)) return data.byteLength;
|
|
4098
|
+
}
|
|
4099
|
+
/**
|
|
4100
|
+
* `"http: 1000, ws: 24"`: the three commonest `meta.kind`s among `entries`, so
|
|
4101
|
+
* a full table names who filled it. A port without a kind counts as `"?"`.
|
|
4102
|
+
*/
|
|
4103
|
+
function describeKinds(entries) {
|
|
4104
|
+
const counts = /* @__PURE__ */ new Map();
|
|
4105
|
+
for (const { meta } of entries) {
|
|
4106
|
+
const kind = meta?.kind;
|
|
4107
|
+
const name = typeof kind === "string" ? kind : "?";
|
|
4108
|
+
counts.set(name, (counts.get(name) ?? 0) + 1);
|
|
4109
|
+
}
|
|
4110
|
+
if (!counts.size) return "none";
|
|
4111
|
+
return [...counts].sort((x, y) => y[1] - x[1]).slice(0, 3).map(([name, n]) => `${name}: ${n}`).join(", ");
|
|
4112
|
+
}
|
|
3368
4113
|
/**
|
|
3369
4114
|
* Announced with every port this pair opens. Layer 1 never reads `meta` — it is
|
|
3370
4115
|
* here so a peer running something else over the same multiplexer can tell a
|
|
@@ -3386,9 +4131,22 @@ const connect = async ({ mux: factory, timeout }) => {
|
|
|
3386
4131
|
maxMessageSize: mux.maxMessageSize,
|
|
3387
4132
|
timeout
|
|
3388
4133
|
};
|
|
3389
|
-
const call = (input) =>
|
|
3390
|
-
|
|
3391
|
-
|
|
4134
|
+
const call = (input) => {
|
|
4135
|
+
let stream;
|
|
4136
|
+
let abandoned = false;
|
|
4137
|
+
return returningAtOnce((async function* () {
|
|
4138
|
+
const port = await mux.openPort(STREAM_META);
|
|
4139
|
+
if (abandoned) {
|
|
4140
|
+
await port.close?.();
|
|
4141
|
+
return;
|
|
4142
|
+
}
|
|
4143
|
+
stream = duplexOverPort(port, streamOptions)(input);
|
|
4144
|
+
yield* stream;
|
|
4145
|
+
})(), () => {
|
|
4146
|
+
abandoned = true;
|
|
4147
|
+
stream?.return(void 0).catch(() => {});
|
|
4148
|
+
});
|
|
4149
|
+
};
|
|
3392
4150
|
return {
|
|
3393
4151
|
call,
|
|
3394
4152
|
async close() {
|
|
@@ -3458,7 +4216,7 @@ function isEnvelope(value) {
|
|
|
3458
4216
|
const candidate = value;
|
|
3459
4217
|
if (typeof candidate.id !== "number") return false;
|
|
3460
4218
|
if (!Number.isInteger(candidate.id) || candidate.id < 0) return false;
|
|
3461
|
-
return candidate.type === "open" || candidate.type === "message" || candidate.type === "close";
|
|
4219
|
+
return candidate.type === "open" || candidate.type === "message" || candidate.type === "close" || candidate.type === "credit" || candidate.type === "frame" || candidate.type === "ping" || candidate.type === "pong";
|
|
3462
4220
|
}
|
|
3463
4221
|
/**
|
|
3464
4222
|
* For ports whose messages are structured values — a real `MessagePort`, a
|
|
@@ -3479,6 +4237,27 @@ const structuredCodec = {
|
|
|
3479
4237
|
}
|
|
3480
4238
|
};
|
|
3481
4239
|
//#endregion
|
|
4240
|
+
//#region src/link/async-dispose.ts
|
|
4241
|
+
/**
|
|
4242
|
+
* Makes `target` disposable with `await using`: installs `dispose` as its
|
|
4243
|
+
* `[Symbol.asyncDispose]`, where the runtime defines the symbol. Hand-written
|
|
4244
|
+
* iterators lose what a native generator gets from `AsyncIteratorPrototype`;
|
|
4245
|
+
* this gives it back. A no-op where the symbol is missing. Kept off the
|
|
4246
|
+
* declared types, which would require consumers to compile with the
|
|
4247
|
+
* `esnext.disposable` lib.
|
|
4248
|
+
*/
|
|
4249
|
+
function asyncDisposable(target, dispose) {
|
|
4250
|
+
const asyncDispose = Symbol.asyncDispose;
|
|
4251
|
+
if (asyncDispose && !(asyncDispose in target)) Object.defineProperty(target, asyncDispose, {
|
|
4252
|
+
value: async () => {
|
|
4253
|
+
await dispose();
|
|
4254
|
+
},
|
|
4255
|
+
configurable: true,
|
|
4256
|
+
writable: true
|
|
4257
|
+
});
|
|
4258
|
+
return target;
|
|
4259
|
+
}
|
|
4260
|
+
//#endregion
|
|
3482
4261
|
//#region src/link/http-link.ts
|
|
3483
4262
|
/**
|
|
3484
4263
|
* The version of the HTTP-over-`MessagePort` protocol this package speaks,
|
|
@@ -3508,11 +4287,12 @@ var LinkLostError = class extends Error {
|
|
|
3508
4287
|
*/
|
|
3509
4288
|
const LINK_CLOSE = "webrun-http:link-close";
|
|
3510
4289
|
/** Opens the calling end of a link on `port` (one port per link). */
|
|
3511
|
-
async function connectHttpLink(port, { streamTimeout } = {}) {
|
|
4290
|
+
async function connectHttpLink(port, { streamTimeout, maxPorts } = {}) {
|
|
3512
4291
|
const { call, close } = await connect({
|
|
3513
4292
|
mux: overPipe(port, {
|
|
3514
4293
|
codec: structuredCodec,
|
|
3515
|
-
side: "initiator"
|
|
4294
|
+
side: "initiator",
|
|
4295
|
+
maxPorts
|
|
3516
4296
|
}),
|
|
3517
4297
|
timeout: streamTimeout
|
|
3518
4298
|
});
|
|
@@ -3528,14 +4308,15 @@ async function connectHttpLink(port, { streamTimeout } = {}) {
|
|
|
3528
4308
|
* that abandons the requests still running (their handlers see the
|
|
3529
4309
|
* cancellation) and releases the port.
|
|
3530
4310
|
*/
|
|
3531
|
-
async function serveHttpLink(port, handler, { streamTimeout } = {}) {
|
|
4311
|
+
async function serveHttpLink(port, handler, { streamTimeout, maxPorts, requestIdleMs } = {}) {
|
|
3532
4312
|
const teardown = await serve({
|
|
3533
4313
|
mux: overPipe(port, {
|
|
3534
4314
|
codec: structuredCodec,
|
|
3535
|
-
side: "responder"
|
|
4315
|
+
side: "responder",
|
|
4316
|
+
maxPorts
|
|
3536
4317
|
}),
|
|
3537
4318
|
timeout: streamTimeout
|
|
3538
|
-
}, serveFetchOverDuplex(async (request) => await handler(request)));
|
|
4319
|
+
}, serveFetchOverDuplex(async (request) => await handler(request), { requestIdleMs }));
|
|
3539
4320
|
const onClose = (event) => {
|
|
3540
4321
|
if (event.data?.type === LINK_CLOSE) close();
|
|
3541
4322
|
};
|
|
@@ -3560,7 +4341,7 @@ function trackedLink(send, release) {
|
|
|
3560
4341
|
inflight--;
|
|
3561
4342
|
if (inflight === 0) for (const resolve of idleWaiters.splice(0)) resolve();
|
|
3562
4343
|
};
|
|
3563
|
-
|
|
4344
|
+
const link = {
|
|
3564
4345
|
get inflight() {
|
|
3565
4346
|
return inflight;
|
|
3566
4347
|
},
|
|
@@ -3597,7 +4378,7 @@ function trackedLink(send, release) {
|
|
|
3597
4378
|
finish();
|
|
3598
4379
|
return response;
|
|
3599
4380
|
}
|
|
3600
|
-
return new Response(guardBody(response.body, lost, () => lostReason, finish), {
|
|
4381
|
+
return new Response(guardBody(response.body, lost, () => lostReason, finish, request.signal), {
|
|
3601
4382
|
status: response.status,
|
|
3602
4383
|
statusText: response.statusText,
|
|
3603
4384
|
headers: response.headers
|
|
@@ -3613,35 +4394,68 @@ function trackedLink(send, release) {
|
|
|
3613
4394
|
} catch {}
|
|
3614
4395
|
}
|
|
3615
4396
|
};
|
|
4397
|
+
return asyncDisposable(link, () => link.close());
|
|
3616
4398
|
}
|
|
3617
4399
|
/**
|
|
3618
|
-
* A response body that reports its end (done, cancelled or
|
|
3619
|
-
* with the link's reason when the link is lost mid-stream.
|
|
4400
|
+
* A response body that reports its end (done, cancelled, failed or aborted) and
|
|
4401
|
+
* fails with the link's reason when the link is lost mid-stream. An abort of the
|
|
4402
|
+
* request's signal ends it at once, read or not: the exchange is over (the layer
|
|
4403
|
+
* below has given its port back), so the request leaves `inflight` then.
|
|
3620
4404
|
*/
|
|
3621
|
-
function guardBody(body, lost, reason, finish) {
|
|
4405
|
+
function guardBody(body, lost, reason, finish, signal) {
|
|
3622
4406
|
const reader = body.getReader();
|
|
4407
|
+
let controller;
|
|
4408
|
+
let ended = false;
|
|
4409
|
+
const end = () => {
|
|
4410
|
+
ended = true;
|
|
4411
|
+
signal?.removeEventListener("abort", onAbort);
|
|
4412
|
+
finish();
|
|
4413
|
+
};
|
|
4414
|
+
const onAbort = () => {
|
|
4415
|
+
if (ended) return;
|
|
4416
|
+
end();
|
|
4417
|
+
reader.cancel().catch(() => {});
|
|
4418
|
+
try {
|
|
4419
|
+
controller.error(abortError(signal));
|
|
4420
|
+
} catch {}
|
|
4421
|
+
};
|
|
3623
4422
|
return new ReadableStream({
|
|
4423
|
+
start(c) {
|
|
4424
|
+
controller = c;
|
|
4425
|
+
if (signal?.aborted) onAbort();
|
|
4426
|
+
else signal?.addEventListener("abort", onAbort, { once: true });
|
|
4427
|
+
},
|
|
3624
4428
|
async pull(controller) {
|
|
3625
4429
|
try {
|
|
3626
4430
|
const { done, value } = await Promise.race([reader.read(), lost]);
|
|
4431
|
+
if (ended) return;
|
|
3627
4432
|
if (done) {
|
|
3628
|
-
|
|
4433
|
+
end();
|
|
3629
4434
|
controller.close();
|
|
3630
4435
|
return;
|
|
3631
4436
|
}
|
|
3632
4437
|
controller.enqueue(value);
|
|
3633
4438
|
} catch (error) {
|
|
3634
|
-
|
|
4439
|
+
if (ended) return;
|
|
4440
|
+
end();
|
|
3635
4441
|
reader.cancel().catch(() => {});
|
|
3636
4442
|
controller.error(reason() ?? error);
|
|
3637
4443
|
}
|
|
3638
4444
|
},
|
|
3639
4445
|
async cancel(why) {
|
|
3640
|
-
|
|
4446
|
+
end();
|
|
3641
4447
|
await reader.cancel(why).catch(() => {});
|
|
3642
4448
|
}
|
|
3643
4449
|
});
|
|
3644
4450
|
}
|
|
4451
|
+
/** The signal's reason as an `Error` (`AbortError` when it gave none). */
|
|
4452
|
+
function abortError(signal) {
|
|
4453
|
+
const why = signal.reason;
|
|
4454
|
+
if (why instanceof Error) return why;
|
|
4455
|
+
const err = new Error(why === void 0 ? "Aborted" : String(why));
|
|
4456
|
+
err.name = "AbortError";
|
|
4457
|
+
return err;
|
|
4458
|
+
}
|
|
3645
4459
|
//#endregion
|
|
3646
4460
|
//#region src/link/locks.ts
|
|
3647
4461
|
function lockManager() {
|
|
@@ -6362,6 +7176,8 @@ var WorkerLinkImpl = class {
|
|
|
6362
7176
|
_ownerLock;
|
|
6363
7177
|
_attaching;
|
|
6364
7178
|
_detached = false;
|
|
7179
|
+
/** The `seq` of the last link sent to the worker. */
|
|
7180
|
+
_sent = 0;
|
|
6365
7181
|
constructor(_adapter, _control, init) {
|
|
6366
7182
|
this._adapter = _adapter;
|
|
6367
7183
|
this._control = _control;
|
|
@@ -6373,6 +7189,7 @@ var WorkerLinkImpl = class {
|
|
|
6373
7189
|
_control.onmessage = (event) => {
|
|
6374
7190
|
const message = event.data;
|
|
6375
7191
|
if (message?.type === "relink") {
|
|
7192
|
+
if (message.seen !== void 0 && message.seen < this._sent) return;
|
|
6376
7193
|
this._adapter._poke(WAKE_MESSAGE);
|
|
6377
7194
|
this.attach().catch(() => {});
|
|
6378
7195
|
} else if (message?.type === "keepalive") this._adapter._poke(KEEPALIVE_MESSAGE);
|
|
@@ -6419,7 +7236,8 @@ var WorkerLinkImpl = class {
|
|
|
6419
7236
|
this._control.postMessage({
|
|
6420
7237
|
type: "link",
|
|
6421
7238
|
key: this.key,
|
|
6422
|
-
routerLock: reply.routerLock
|
|
7239
|
+
routerLock: reply.routerLock,
|
|
7240
|
+
seq: ++this._sent
|
|
6423
7241
|
}, [
|
|
6424
7242
|
down.port2,
|
|
6425
7243
|
ctl.port2,
|
|
@@ -6520,12 +7338,18 @@ function serveHttpInWorker(handler, options = {}) {
|
|
|
6520
7338
|
const ownerLock = newLockName("webrun-http-owner");
|
|
6521
7339
|
const held = holdLock(ownerLock);
|
|
6522
7340
|
const controls = /* @__PURE__ */ new Set();
|
|
7341
|
+
const seen = /* @__PURE__ */ new Map();
|
|
6523
7342
|
const tellPages = (message) => {
|
|
6524
7343
|
for (const control of controls) control.postMessage(message);
|
|
6525
7344
|
};
|
|
6526
7345
|
const endpoint = new HandlerEndpoint(handler, {
|
|
6527
7346
|
...options,
|
|
6528
|
-
requestRelink: () =>
|
|
7347
|
+
requestRelink: () => {
|
|
7348
|
+
for (const control of controls) control.postMessage({
|
|
7349
|
+
type: "relink",
|
|
7350
|
+
seen: seen.get(control) ?? 0
|
|
7351
|
+
});
|
|
7352
|
+
},
|
|
6529
7353
|
keepAlive: () => tellPages({ type: "keepalive" })
|
|
6530
7354
|
});
|
|
6531
7355
|
const onMessage = (event) => {
|
|
@@ -6537,6 +7361,7 @@ function serveHttpInWorker(handler, options = {}) {
|
|
|
6537
7361
|
control.onmessage = (message) => {
|
|
6538
7362
|
const msg = message.data;
|
|
6539
7363
|
if (msg?.type === "link") {
|
|
7364
|
+
if (msg.seq !== void 0) seen.set(control, msg.seq);
|
|
6540
7365
|
const [down, ctl, up] = message.ports;
|
|
6541
7366
|
if (down) endpoint.link(msg.key, {
|
|
6542
7367
|
down,
|
|
@@ -6577,6 +7402,7 @@ function serveHttpInWorker(handler, options = {}) {
|
|
|
6577
7402
|
await endpoint.close();
|
|
6578
7403
|
for (const control of controls) control.close();
|
|
6579
7404
|
controls.clear();
|
|
7405
|
+
seen.clear();
|
|
6580
7406
|
(await held)();
|
|
6581
7407
|
}
|
|
6582
7408
|
};
|