@statewalker/webrun-http-browser 0.7.1 → 0.10.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 +716 -84
- package/dist/relay-sw.js +647 -90
- package/dist/relay-worker.js +647 -90
- 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 +695 -81
- package/dist/sw.js +709 -83
- package/dist/worker.js +704 -82
- package/package.json +3 -3
- package/src/link/http-link.ts +42 -6
- 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/sw.js
CHANGED
|
@@ -1266,7 +1266,11 @@ async function drain(output) {
|
|
|
1266
1266
|
*/
|
|
1267
1267
|
function httpServe(handler, options = {}) {
|
|
1268
1268
|
const codec = options.codec ?? defaultCodec;
|
|
1269
|
-
return
|
|
1269
|
+
return (input) => {
|
|
1270
|
+
const abandoned = new AbortController();
|
|
1271
|
+
return abortingOnReturn(exchange(input, { signal: abandoned.signal }), (reason) => abandoned.abort(reason ?? /* @__PURE__ */ new Error("webrun-http-streams: the exchange was abandoned before it was over")));
|
|
1272
|
+
};
|
|
1273
|
+
async function* exchange(input, context) {
|
|
1270
1274
|
let decoded;
|
|
1271
1275
|
try {
|
|
1272
1276
|
decoded = await codec.decodeRequest(input);
|
|
@@ -1278,7 +1282,7 @@ function httpServe(handler, options = {}) {
|
|
|
1278
1282
|
const replyCodec = decoded.codec ?? codec;
|
|
1279
1283
|
let result;
|
|
1280
1284
|
try {
|
|
1281
|
-
result = await handler(decoded.envelope, decoded.body);
|
|
1285
|
+
result = await handler(decoded.envelope, decoded.body, context);
|
|
1282
1286
|
} catch (error) {
|
|
1283
1287
|
yield* encodeErrorResponse(replyCodec, error, decoded.envelope.method);
|
|
1284
1288
|
return;
|
|
@@ -1293,7 +1297,36 @@ function httpServe(handler, options = {}) {
|
|
|
1293
1297
|
await result.release?.();
|
|
1294
1298
|
} catch {}
|
|
1295
1299
|
}
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
/**
|
|
1303
|
+
* `gen`, except that `return()` and `throw()` from its consumer call `abandon`
|
|
1304
|
+
* at once — before they are queued behind a `next()` still parked inside the
|
|
1305
|
+
* generator (the handler's `await`) — unless the generator already finished.
|
|
1306
|
+
*/
|
|
1307
|
+
function abortingOnReturn(gen, abandon) {
|
|
1308
|
+
let finished = false;
|
|
1309
|
+
const settle = (r) => {
|
|
1310
|
+
if (r.done) finished = true;
|
|
1311
|
+
return r;
|
|
1312
|
+
};
|
|
1313
|
+
const wrapped = {
|
|
1314
|
+
next: (...args) => gen.next(...args).then(settle),
|
|
1315
|
+
return(value) {
|
|
1316
|
+
if (!finished) abandon();
|
|
1317
|
+
finished = true;
|
|
1318
|
+
return gen.return(value);
|
|
1319
|
+
},
|
|
1320
|
+
throw(error) {
|
|
1321
|
+
if (!finished) abandon(error);
|
|
1322
|
+
finished = true;
|
|
1323
|
+
return gen.throw(error);
|
|
1324
|
+
},
|
|
1325
|
+
[Symbol.asyncIterator]() {
|
|
1326
|
+
return wrapped;
|
|
1327
|
+
}
|
|
1296
1328
|
};
|
|
1329
|
+
return asyncDisposable$2(wrapped, () => wrapped.return(void 0));
|
|
1297
1330
|
}
|
|
1298
1331
|
/**
|
|
1299
1332
|
* The one place that knows a runtime may not implement request body streams,
|
|
@@ -1525,17 +1558,18 @@ async function fetchOverDuplex(call, request, options = {}) {
|
|
|
1525
1558
|
else await release?.();
|
|
1526
1559
|
return new Response(null, responseInit);
|
|
1527
1560
|
}
|
|
1528
|
-
return new Response(
|
|
1561
|
+
return new Response(responseBody(respBody, request.signal, release), responseInit);
|
|
1529
1562
|
}
|
|
1530
1563
|
/**
|
|
1531
1564
|
* Wrap a `(Request) => Promise<Response>` handler as a `Duplex` so it can be
|
|
1532
1565
|
* registered with any `webrun-streams-*` adapter's `serve`.
|
|
1533
1566
|
*/
|
|
1534
1567
|
function serveFetchOverDuplex(handler, options = {}) {
|
|
1535
|
-
return httpServe(async (env, body) => {
|
|
1568
|
+
return httpServe(async (env, body, exchange) => {
|
|
1536
1569
|
const reqInit = {
|
|
1537
1570
|
method: env.method,
|
|
1538
|
-
headers: forwardableHeaders(env.headers)
|
|
1571
|
+
headers: forwardableHeaders(env.headers),
|
|
1572
|
+
signal: exchange?.signal
|
|
1539
1573
|
};
|
|
1540
1574
|
let upload;
|
|
1541
1575
|
if (env.method !== "GET" && env.method !== "HEAD") {
|
|
@@ -1656,15 +1690,78 @@ function trackingEnd(source) {
|
|
|
1656
1690
|
settle
|
|
1657
1691
|
};
|
|
1658
1692
|
}
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1693
|
+
/**
|
|
1694
|
+
* How long a cancelled body waits for the read in flight before it abandons the call anyway.
|
|
1695
|
+
* The read's answer tells a body that has ended (the call is finished: an upload the handler
|
|
1696
|
+
* reads after answering goes on) from one that has not; a peer that sends nothing more never
|
|
1697
|
+
* answers it. A body whose end comes later than this after the cancel is abandoned too, so a
|
|
1698
|
+
* handler still streaming its response and still reading the upload then loses the upload.
|
|
1699
|
+
*/
|
|
1700
|
+
const CANCEL_GRACE_MS = 1e3;
|
|
1701
|
+
/**
|
|
1702
|
+
* The response body as a stream that the request's signal can end at any time.
|
|
1703
|
+
*
|
|
1704
|
+
* An abort errors the stream with the abort's reason at once — a read waiting
|
|
1705
|
+
* for a peer that sends nothing more rejects then, not when a next chunk
|
|
1706
|
+
* arrives, which may be never — and abandons the call (`release`), so on a
|
|
1707
|
+
* multiplexed transport its port is given back whether or not anyone still
|
|
1708
|
+
* reads the body. The listener is removed once the body ends, however it ends:
|
|
1709
|
+
* read to its end, cancelled, failed or aborted. A body nobody reads or cancels
|
|
1710
|
+
* keeps it until the signal aborts (and keeps the call open until then anyway).
|
|
1711
|
+
* A cancel abandons the call too — unless the read in flight comes back with the
|
|
1712
|
+
* body's end, which finishes it instead (see `cancel`).
|
|
1713
|
+
*/
|
|
1714
|
+
function responseBody(body, signal, release) {
|
|
1715
|
+
const it = body[Symbol.asyncIterator]();
|
|
1716
|
+
let controller;
|
|
1717
|
+
let ended = false;
|
|
1718
|
+
let reading = false;
|
|
1719
|
+
const end = () => {
|
|
1720
|
+
ended = true;
|
|
1721
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1722
|
+
};
|
|
1723
|
+
const onAbort = () => {
|
|
1724
|
+
if (ended) return;
|
|
1725
|
+
end();
|
|
1726
|
+
try {
|
|
1727
|
+
controller.error(abortReason$1(signal));
|
|
1728
|
+
} catch {}
|
|
1729
|
+
release?.();
|
|
1730
|
+
};
|
|
1731
|
+
return new ReadableStream({
|
|
1732
|
+
start(c) {
|
|
1733
|
+
controller = c;
|
|
1734
|
+
if (signal?.aborted) onAbort();
|
|
1735
|
+
else signal?.addEventListener("abort", onAbort, { once: true });
|
|
1736
|
+
},
|
|
1737
|
+
async pull(c) {
|
|
1738
|
+
try {
|
|
1739
|
+
reading = true;
|
|
1740
|
+
const { done, value } = await it.next().finally(() => {
|
|
1741
|
+
reading = false;
|
|
1742
|
+
});
|
|
1743
|
+
if (ended) return;
|
|
1744
|
+
if (done) {
|
|
1745
|
+
end();
|
|
1746
|
+
c.close();
|
|
1747
|
+
return;
|
|
1748
|
+
}
|
|
1749
|
+
c.enqueue(value);
|
|
1750
|
+
} catch (err) {
|
|
1751
|
+
if (ended) return;
|
|
1752
|
+
end();
|
|
1753
|
+
c.error(err);
|
|
1754
|
+
}
|
|
1755
|
+
},
|
|
1756
|
+
cancel() {
|
|
1757
|
+
end();
|
|
1758
|
+
const returned = Promise.resolve(it.return?.()).catch(() => {});
|
|
1759
|
+
if (!reading) return;
|
|
1760
|
+
const grace = setTimeout(() => void release?.(), CANCEL_GRACE_MS);
|
|
1761
|
+
grace.unref?.();
|
|
1762
|
+
returned.finally(() => clearTimeout(grace));
|
|
1763
|
+
}
|
|
1764
|
+
});
|
|
1668
1765
|
}
|
|
1669
1766
|
//#endregion
|
|
1670
1767
|
//#region ../webrun-rpc/dist/index.js
|
|
@@ -1789,8 +1886,11 @@ function listenPort(port, handler, { channelName = "", log = () => {} } = {}) {
|
|
|
1789
1886
|
error
|
|
1790
1887
|
});
|
|
1791
1888
|
};
|
|
1792
|
-
|
|
1793
|
-
|
|
1889
|
+
const listener = (event) => {
|
|
1890
|
+
onMessage(event);
|
|
1891
|
+
};
|
|
1892
|
+
port.addEventListener("message", listener);
|
|
1893
|
+
return () => port.removeEventListener("message", listener);
|
|
1794
1894
|
}
|
|
1795
1895
|
/**
|
|
1796
1896
|
* Wraps an async iterable so that an `AbortSignal` firing causes the wrapper
|
|
@@ -2168,7 +2268,11 @@ function receiveChunks(port, channelName, controller, touch) {
|
|
|
2168
2268
|
* outstanding at a time (spec D11/D12), with no per-chunk deadline (spec D8).
|
|
2169
2269
|
*/
|
|
2170
2270
|
async function sendChunks(port, channelName, output, { maxMessageSize, log }, signal, touch) {
|
|
2171
|
-
const
|
|
2271
|
+
const framed = maxMessageSize ? toChunks(maxMessageSize)(output) : output;
|
|
2272
|
+
const stream = throughAbort(framed, signal);
|
|
2273
|
+
const producer = output;
|
|
2274
|
+
const returnProducer = () => void Promise.resolve(producer.return?.()).catch(() => {});
|
|
2275
|
+
if (framed !== output) signal.addEventListener("abort", returnProducer, { once: true });
|
|
2172
2276
|
try {
|
|
2173
2277
|
await sendIterator(async ({ done, value, error }) => {
|
|
2174
2278
|
if (signal.aborted) return;
|
|
@@ -2192,9 +2296,38 @@ async function sendChunks(port, channelName, output, { maxMessageSize, log }, si
|
|
|
2192
2296
|
} catch (err) {
|
|
2193
2297
|
if (signal.aborted) return;
|
|
2194
2298
|
throw err;
|
|
2299
|
+
} finally {
|
|
2300
|
+
signal.removeEventListener("abort", returnProducer);
|
|
2195
2301
|
}
|
|
2196
2302
|
}
|
|
2197
2303
|
/**
|
|
2304
|
+
* Runs `fn` on a later task, never a microtask: a producer looping on
|
|
2305
|
+
* `whenPortReady` must let the transport's own events run. `setImmediate`
|
|
2306
|
+
* where it exists; elsewhere a `MessageChannel`, because nested `setTimeout`s
|
|
2307
|
+
* are clamped to 4 ms in browsers.
|
|
2308
|
+
*/
|
|
2309
|
+
const setImmediateFn = globalThis.setImmediate;
|
|
2310
|
+
/** @internal */
|
|
2311
|
+
const nextTask = setImmediateFn ? (fn) => {
|
|
2312
|
+
setImmediateFn(fn);
|
|
2313
|
+
} : (() => {
|
|
2314
|
+
let channel;
|
|
2315
|
+
const queue = [];
|
|
2316
|
+
return (fn) => {
|
|
2317
|
+
if (!channel) {
|
|
2318
|
+
channel = new MessageChannel();
|
|
2319
|
+
channel.port1.onmessage = () => queue.shift()?.();
|
|
2320
|
+
}
|
|
2321
|
+
queue.push(fn);
|
|
2322
|
+
channel.port2.postMessage(0);
|
|
2323
|
+
};
|
|
2324
|
+
})();
|
|
2325
|
+
const states = /* @__PURE__ */ new WeakMap();
|
|
2326
|
+
/** @internal Registered by the multiplexer for each port with a window. */
|
|
2327
|
+
function setPortSendState(port, state) {
|
|
2328
|
+
states.set(port, state);
|
|
2329
|
+
}
|
|
2330
|
+
/**
|
|
2198
2331
|
* One virtual port.
|
|
2199
2332
|
*
|
|
2200
2333
|
* `deliver` and `markClosed` are deliberately not on `port`: the consumer holds
|
|
@@ -2209,6 +2342,14 @@ function newVirtualPort(send, requestClose) {
|
|
|
2209
2342
|
if (closing.signal.aborted) return;
|
|
2210
2343
|
closing.abort(reason ?? /* @__PURE__ */ new Error("webrun-rpc: the port was closed"));
|
|
2211
2344
|
};
|
|
2345
|
+
const close = (reason) => {
|
|
2346
|
+
if (closed) return;
|
|
2347
|
+
closed = true;
|
|
2348
|
+
const notify = requestClose;
|
|
2349
|
+
listeners.clear();
|
|
2350
|
+
notify(reason);
|
|
2351
|
+
abort(reason);
|
|
2352
|
+
};
|
|
2212
2353
|
const port = {
|
|
2213
2354
|
addEventListener(_type, listener) {
|
|
2214
2355
|
listeners.add(listener);
|
|
@@ -2220,24 +2361,28 @@ function newVirtualPort(send, requestClose) {
|
|
|
2220
2361
|
if (closed) return;
|
|
2221
2362
|
send(message, transfer);
|
|
2222
2363
|
},
|
|
2223
|
-
close
|
|
2224
|
-
if (closed) return;
|
|
2225
|
-
closed = true;
|
|
2226
|
-
const notify = requestClose;
|
|
2227
|
-
listeners.clear();
|
|
2228
|
-
notify();
|
|
2229
|
-
abort();
|
|
2230
|
-
}
|
|
2364
|
+
close
|
|
2231
2365
|
};
|
|
2232
2366
|
setPortCloseSignal(port, closing.signal);
|
|
2367
|
+
const dispose = Symbol.dispose;
|
|
2368
|
+
if (dispose) Object.defineProperty(port, dispose, {
|
|
2369
|
+
value: () => close(),
|
|
2370
|
+
configurable: true
|
|
2371
|
+
});
|
|
2233
2372
|
return {
|
|
2234
2373
|
port,
|
|
2235
2374
|
deliver(payload) {
|
|
2236
2375
|
if (closed) return;
|
|
2237
2376
|
const event = new MessageEvent("message", { data: payload });
|
|
2377
|
+
let pending;
|
|
2238
2378
|
for (const listener of [...listeners]) try {
|
|
2239
|
-
listener(event);
|
|
2379
|
+
const result = listener(event);
|
|
2380
|
+
if (result && typeof result.then === "function") {
|
|
2381
|
+
pending ??= [];
|
|
2382
|
+
pending.push(result);
|
|
2383
|
+
}
|
|
2240
2384
|
} catch {}
|
|
2385
|
+
return pending ? Promise.allSettled(pending) : void 0;
|
|
2241
2386
|
},
|
|
2242
2387
|
markClosed(reason) {
|
|
2243
2388
|
closed = true;
|
|
@@ -2246,53 +2391,318 @@ function newVirtualPort(send, requestClose) {
|
|
|
2246
2391
|
},
|
|
2247
2392
|
isClosed() {
|
|
2248
2393
|
return closed;
|
|
2394
|
+
},
|
|
2395
|
+
hasListeners() {
|
|
2396
|
+
return listeners.size > 0;
|
|
2249
2397
|
}
|
|
2250
2398
|
};
|
|
2251
2399
|
}
|
|
2252
2400
|
/**
|
|
2253
|
-
* The
|
|
2254
|
-
*
|
|
2401
|
+
* The id of keepalive's `ping` / `pong`: one no port ever takes. An older mux
|
|
2402
|
+
* whose codec passes unknown envelopes through reads them as a close of that
|
|
2403
|
+
* id, which must therefore not be a real port (0 is the initiator's first).
|
|
2255
2404
|
*/
|
|
2256
|
-
const
|
|
2405
|
+
const KEEPALIVE_ID = Number.MAX_SAFE_INTEGER;
|
|
2406
|
+
/** The longest delay a timer holds; past it `setInterval` fires at once. */
|
|
2407
|
+
const MAX_TIMER_MS = 2 ** 31 - 1;
|
|
2257
2408
|
/** Ceiling on concurrently open virtual ports. Bounds the id table only. */
|
|
2258
2409
|
const DEFAULT_MAX_PORTS = 1024;
|
|
2259
2410
|
/**
|
|
2411
|
+
* What a message costs a port's window when the codec cannot say
|
|
2412
|
+
* (`PortCodec.encode`): its bytes and string lengths, found in its values up
|
|
2413
|
+
* to 4 levels deep (at most 256 values), and at least 64. Both ends compute it
|
|
2414
|
+
* from the same structured value, so they agree.
|
|
2415
|
+
*/
|
|
2416
|
+
function estimateMessageSize(payload) {
|
|
2417
|
+
let budget = 256;
|
|
2418
|
+
const walk = (v, depth) => {
|
|
2419
|
+
if (budget-- <= 0) return 0;
|
|
2420
|
+
if (typeof v === "string") return v.length;
|
|
2421
|
+
if (v instanceof ArrayBuffer) return v.byteLength;
|
|
2422
|
+
if (ArrayBuffer.isView(v)) return v.byteLength;
|
|
2423
|
+
if (v === null || typeof v !== "object" || depth >= 4) return 8;
|
|
2424
|
+
let n = 16;
|
|
2425
|
+
if (Array.isArray(v)) for (const x of v) n += walk(x, depth + 1);
|
|
2426
|
+
else for (const k in v) n += k.length + walk(v[k], depth + 1);
|
|
2427
|
+
return n;
|
|
2428
|
+
};
|
|
2429
|
+
return Math.max(64, walk(payload, 0));
|
|
2430
|
+
}
|
|
2431
|
+
/**
|
|
2260
2432
|
* The default `PortMux`: emulates multiplexing over a single port.
|
|
2261
2433
|
*
|
|
2262
2434
|
* A transport that already multiplexes natively supplies its own `PortMux`
|
|
2263
2435
|
* instead — this implementation is for transports that offer one pipe.
|
|
2264
2436
|
*/
|
|
2265
2437
|
function multiplexPort(port, options) {
|
|
2266
|
-
const { codec, onPort, side = "initiator", maxPorts = DEFAULT_MAX_PORTS, maxMessageSize } = options;
|
|
2438
|
+
const { codec, onPort, side = "initiator", maxPorts = DEFAULT_MAX_PORTS, maxMessageSize, signal, window, frameSize, keepalive, onLeak } = options;
|
|
2439
|
+
const maxReassembledBytes = options.maxReassembledBytes ?? 67108864;
|
|
2440
|
+
const misses = keepalive?.misses ?? 3;
|
|
2441
|
+
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`);
|
|
2442
|
+
if (frameSize != null && !(Number.isSafeInteger(frameSize) && frameSize > 0)) throw new RangeError(`webrun-rpc: frameSize must be a positive integer, got ${frameSize}`);
|
|
2443
|
+
if (!(Number.isSafeInteger(maxReassembledBytes) && maxReassembledBytes > 0)) throw new RangeError(`webrun-rpc: maxReassembledBytes must be a positive integer, got ${maxReassembledBytes}`);
|
|
2267
2444
|
const open = /* @__PURE__ */ new Map();
|
|
2445
|
+
let reassembling = 0;
|
|
2446
|
+
const freePartial = (entry) => {
|
|
2447
|
+
if (!entry?.partial) return;
|
|
2448
|
+
reassembling -= entry.partial.charged;
|
|
2449
|
+
entry.partial = void 0;
|
|
2450
|
+
};
|
|
2451
|
+
const leaks = onLeak ? new FinalizationRegistry(({ id, record, release }) => {
|
|
2452
|
+
const entry = open.get(id);
|
|
2453
|
+
if (entry?.handle !== record) return;
|
|
2454
|
+
const { handle: _, credit: __, partial: ___, ...info } = entry;
|
|
2455
|
+
if (!record.hasListeners()) {
|
|
2456
|
+
release("leaked");
|
|
2457
|
+
record.markClosed(/* @__PURE__ */ new Error("webrun-rpc: the port was dropped without close()"));
|
|
2458
|
+
}
|
|
2459
|
+
try {
|
|
2460
|
+
onLeak?.(info);
|
|
2461
|
+
} catch {}
|
|
2462
|
+
}) : void 0;
|
|
2463
|
+
const touch = (id) => {
|
|
2464
|
+
const entry = open.get(id);
|
|
2465
|
+
if (entry) entry.lastActivity = Date.now();
|
|
2466
|
+
};
|
|
2268
2467
|
let nextId = side === "initiator" ? 0 : 1;
|
|
2269
2468
|
let muxClosed = false;
|
|
2469
|
+
/** This end counts encoded bytes and reassembles frames. */
|
|
2470
|
+
const exactCodec = codec.encode != null && codec.decode != null;
|
|
2270
2471
|
const post = (envelope, transfer) => {
|
|
2271
2472
|
if (muxClosed) return;
|
|
2473
|
+
codecPost(envelope, transfer);
|
|
2474
|
+
};
|
|
2475
|
+
/** Like `post`, also while closing: the mux's own close notices. */
|
|
2476
|
+
const codecPost = (envelope, transfer) => {
|
|
2272
2477
|
try {
|
|
2273
2478
|
codec.post(port, envelope, transfer);
|
|
2274
2479
|
} catch {}
|
|
2275
2480
|
};
|
|
2276
|
-
const
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2481
|
+
const outgoing = /* @__PURE__ */ new Map();
|
|
2482
|
+
const ring = [];
|
|
2483
|
+
let pumping = false;
|
|
2484
|
+
const pump = async () => {
|
|
2485
|
+
pumping = true;
|
|
2486
|
+
try {
|
|
2487
|
+
while (ring.length && !muxClosed) {
|
|
2488
|
+
if (port.whenDrained) await port.whenDrained();
|
|
2489
|
+
if (muxClosed) break;
|
|
2490
|
+
const id = ring.shift();
|
|
2491
|
+
const queue = outgoing.get(id);
|
|
2492
|
+
const item = queue?.shift();
|
|
2493
|
+
if (queue?.length) ring.push(id);
|
|
2494
|
+
else outgoing.delete(id);
|
|
2495
|
+
item?.();
|
|
2496
|
+
}
|
|
2497
|
+
} catch (err) {
|
|
2498
|
+
close(err).catch(() => {});
|
|
2499
|
+
} finally {
|
|
2500
|
+
pumping = false;
|
|
2501
|
+
}
|
|
2502
|
+
};
|
|
2503
|
+
const sendOnPort = (id, item) => {
|
|
2504
|
+
if (frameSize == null) {
|
|
2505
|
+
item();
|
|
2506
|
+
return;
|
|
2507
|
+
}
|
|
2508
|
+
let queue = outgoing.get(id);
|
|
2509
|
+
if (!queue) {
|
|
2510
|
+
queue = [];
|
|
2511
|
+
outgoing.set(id, queue);
|
|
2512
|
+
ring.push(id);
|
|
2513
|
+
}
|
|
2514
|
+
queue.push(item);
|
|
2515
|
+
if (!pumping) pump();
|
|
2516
|
+
};
|
|
2517
|
+
const dropOutgoing = (id) => {
|
|
2518
|
+
if (!outgoing.delete(id)) return;
|
|
2519
|
+
const at = ring.indexOf(id);
|
|
2520
|
+
if (at >= 0) ring.splice(at, 1);
|
|
2521
|
+
};
|
|
2522
|
+
const postBytes = (bytes) => {
|
|
2523
|
+
if (muxClosed) return;
|
|
2524
|
+
try {
|
|
2525
|
+
port.postMessage(bytes);
|
|
2526
|
+
} catch {}
|
|
2527
|
+
};
|
|
2528
|
+
const releaseWaiters = (c) => {
|
|
2529
|
+
for (const w of c?.waiters.splice(0) ?? []) w.resolve();
|
|
2530
|
+
};
|
|
2531
|
+
const flush = (c) => {
|
|
2532
|
+
while (c.queue.length) {
|
|
2533
|
+
const next = c.queue[0];
|
|
2534
|
+
if (c.enforced && c.outstanding > 0 && c.outstanding + next.size > c.window) break;
|
|
2535
|
+
c.queue.shift();
|
|
2536
|
+
c.queued -= next.size;
|
|
2537
|
+
c.outstanding += next.size;
|
|
2538
|
+
next.send();
|
|
2539
|
+
}
|
|
2540
|
+
for (const w of [...c.waiters]) if (c.queued <= w.below) {
|
|
2541
|
+
c.waiters.splice(c.waiters.indexOf(w), 1);
|
|
2542
|
+
w.resolve();
|
|
2543
|
+
}
|
|
2544
|
+
};
|
|
2545
|
+
const grant = (id, c, bytes) => {
|
|
2546
|
+
c.unconsumed -= bytes;
|
|
2547
|
+
c.toGrant += bytes;
|
|
2548
|
+
const send = () => {
|
|
2549
|
+
c.grantScheduled = false;
|
|
2550
|
+
if (!c.toGrant || !open.has(id)) return;
|
|
2283
2551
|
post({
|
|
2552
|
+
type: "credit",
|
|
2553
|
+
id,
|
|
2554
|
+
bytes: c.toGrant
|
|
2555
|
+
});
|
|
2556
|
+
c.toGrant = 0;
|
|
2557
|
+
};
|
|
2558
|
+
if (c.toGrant >= c.window / 4) {
|
|
2559
|
+
send();
|
|
2560
|
+
return;
|
|
2561
|
+
}
|
|
2562
|
+
if (c.grantScheduled) return;
|
|
2563
|
+
c.grantScheduled = true;
|
|
2564
|
+
setTimeout(send, 0);
|
|
2565
|
+
};
|
|
2566
|
+
const attach = (id, meta, portWindow, opener, exact) => {
|
|
2567
|
+
const c = typeof portWindow === "number" && Number.isSafeInteger(portWindow) && portWindow > 0 ? {
|
|
2568
|
+
window: portWindow,
|
|
2569
|
+
enforced: false,
|
|
2570
|
+
opener,
|
|
2571
|
+
exact,
|
|
2572
|
+
outstanding: 0,
|
|
2573
|
+
queue: [],
|
|
2574
|
+
queued: 0,
|
|
2575
|
+
unconsumed: 0,
|
|
2576
|
+
waiters: [],
|
|
2577
|
+
toGrant: 0,
|
|
2578
|
+
grantScheduled: false
|
|
2579
|
+
} : void 0;
|
|
2580
|
+
const self = newVirtualPort((payload, transfer) => {
|
|
2581
|
+
touch(id);
|
|
2582
|
+
const envelope = {
|
|
2583
|
+
type: "message",
|
|
2584
|
+
id,
|
|
2585
|
+
payload
|
|
2586
|
+
};
|
|
2587
|
+
if (!c) {
|
|
2588
|
+
post(envelope, transfer);
|
|
2589
|
+
return;
|
|
2590
|
+
}
|
|
2591
|
+
const bytes = c.exact ? codec.encode?.(envelope) : void 0;
|
|
2592
|
+
const size = bytes?.byteLength ?? estimateMessageSize(payload);
|
|
2593
|
+
const waits = frameSize != null || c.queue.length > 0 || c.enforced && c.outstanding > 0 && c.outstanding + size > c.window;
|
|
2594
|
+
const held = !bytes && waits ? {
|
|
2595
|
+
...envelope,
|
|
2596
|
+
payload: structuredClone(payload, { transfer })
|
|
2597
|
+
} : void 0;
|
|
2598
|
+
const send = (whole = false) => {
|
|
2599
|
+
if (!bytes) {
|
|
2600
|
+
sendOnPort(id, held ? () => post(held) : () => post(envelope, transfer));
|
|
2601
|
+
return;
|
|
2602
|
+
}
|
|
2603
|
+
if (!whole && frameSize != null && c.enforced && c.exact && bytes.byteLength > frameSize) {
|
|
2604
|
+
for (let at = 0; at < bytes.byteLength; at += frameSize) {
|
|
2605
|
+
const data = bytes.subarray(at, at + frameSize);
|
|
2606
|
+
const frame = at + frameSize >= bytes.byteLength ? {
|
|
2607
|
+
type: "frame",
|
|
2608
|
+
id,
|
|
2609
|
+
data,
|
|
2610
|
+
last: true
|
|
2611
|
+
} : {
|
|
2612
|
+
type: "frame",
|
|
2613
|
+
id,
|
|
2614
|
+
data
|
|
2615
|
+
};
|
|
2616
|
+
sendOnPort(id, () => post(frame));
|
|
2617
|
+
}
|
|
2618
|
+
return;
|
|
2619
|
+
}
|
|
2620
|
+
sendOnPort(id, () => postBytes(bytes));
|
|
2621
|
+
};
|
|
2622
|
+
c.queue.push({
|
|
2623
|
+
send,
|
|
2624
|
+
size
|
|
2625
|
+
});
|
|
2626
|
+
c.queued += size;
|
|
2627
|
+
flush(c);
|
|
2628
|
+
}, (reason) => {
|
|
2629
|
+
release(reason);
|
|
2630
|
+
record.markClosed(reason);
|
|
2631
|
+
});
|
|
2632
|
+
function release(reason) {
|
|
2633
|
+
freePartial(open.get(id));
|
|
2634
|
+
open.delete(id);
|
|
2635
|
+
leaks?.unregister(record);
|
|
2636
|
+
if (c?.queue.length) {
|
|
2637
|
+
for (const item of c.queue.splice(0)) item.send(true);
|
|
2638
|
+
c.queued = 0;
|
|
2639
|
+
}
|
|
2640
|
+
sendOnPort(id, () => post({
|
|
2284
2641
|
type: "close",
|
|
2285
2642
|
id,
|
|
2286
2643
|
reason
|
|
2644
|
+
}));
|
|
2645
|
+
releaseWaiters(c);
|
|
2646
|
+
}
|
|
2647
|
+
const { markClosed, isClosed, hasListeners } = self;
|
|
2648
|
+
if (c) {
|
|
2649
|
+
const deliver = self.deliver;
|
|
2650
|
+
self.deliver = (payload, size = estimateMessageSize(payload)) => {
|
|
2651
|
+
c.unconsumed += size;
|
|
2652
|
+
const done = deliver(payload);
|
|
2653
|
+
if (done) done.then(() => grant(id, c, size));
|
|
2654
|
+
else grant(id, c, size);
|
|
2655
|
+
return done;
|
|
2656
|
+
};
|
|
2657
|
+
setPortSendState(self.port, {
|
|
2658
|
+
bufferedAmount: () => c.queued,
|
|
2659
|
+
ready: (below = 0) => new Promise((resolve) => {
|
|
2660
|
+
if (c.queued <= below || isClosed()) nextTask(resolve);
|
|
2661
|
+
else c.waiters.push({
|
|
2662
|
+
below,
|
|
2663
|
+
resolve: () => nextTask(resolve)
|
|
2664
|
+
});
|
|
2665
|
+
})
|
|
2287
2666
|
});
|
|
2288
|
-
|
|
2289
|
-
|
|
2667
|
+
}
|
|
2668
|
+
const record = {
|
|
2669
|
+
deliver: self.deliver,
|
|
2670
|
+
markClosed,
|
|
2671
|
+
isClosed,
|
|
2672
|
+
hasListeners,
|
|
2673
|
+
close: (reason) => {
|
|
2674
|
+
if (isClosed()) return;
|
|
2675
|
+
release(reason);
|
|
2676
|
+
markClosed(reason);
|
|
2677
|
+
}
|
|
2678
|
+
};
|
|
2679
|
+
leaks?.register(self.port, {
|
|
2680
|
+
id,
|
|
2681
|
+
record,
|
|
2682
|
+
release
|
|
2683
|
+
}, record);
|
|
2684
|
+
const now = Date.now();
|
|
2685
|
+
open.set(id, {
|
|
2686
|
+
id,
|
|
2687
|
+
meta,
|
|
2688
|
+
openedAt: now,
|
|
2689
|
+
lastActivity: now,
|
|
2690
|
+
handle: record,
|
|
2691
|
+
credit: c
|
|
2290
2692
|
});
|
|
2291
|
-
open.set(id, self);
|
|
2292
2693
|
return self;
|
|
2293
2694
|
};
|
|
2294
|
-
const handleEnvelope = (envelope) => {
|
|
2695
|
+
const handleEnvelope = (envelope, event) => {
|
|
2295
2696
|
if (muxClosed) return;
|
|
2697
|
+
heardSincePing = true;
|
|
2698
|
+
if (envelope.type === "ping" || envelope.type === "pong") {
|
|
2699
|
+
peerKeepsAlive = true;
|
|
2700
|
+
if (envelope.type === "ping") post({
|
|
2701
|
+
type: "pong",
|
|
2702
|
+
id: KEEPALIVE_ID
|
|
2703
|
+
});
|
|
2704
|
+
return;
|
|
2705
|
+
}
|
|
2296
2706
|
if (envelope.type === "open") {
|
|
2297
2707
|
if (open.has(envelope.id)) return;
|
|
2298
2708
|
if (open.size >= maxPorts) {
|
|
@@ -2303,7 +2713,18 @@ function multiplexPort(port, options) {
|
|
|
2303
2713
|
});
|
|
2304
2714
|
return;
|
|
2305
2715
|
}
|
|
2306
|
-
const handle = attach(envelope.id);
|
|
2716
|
+
const handle = attach(envelope.id, envelope.meta, envelope.window, false, envelope.exact === true && exactCodec);
|
|
2717
|
+
const credit = open.get(envelope.id)?.credit;
|
|
2718
|
+
if (credit) post(credit.exact ? {
|
|
2719
|
+
type: "credit",
|
|
2720
|
+
id: envelope.id,
|
|
2721
|
+
bytes: 0,
|
|
2722
|
+
exact: true
|
|
2723
|
+
} : {
|
|
2724
|
+
type: "credit",
|
|
2725
|
+
id: envelope.id,
|
|
2726
|
+
bytes: 0
|
|
2727
|
+
});
|
|
2307
2728
|
let accepted = false;
|
|
2308
2729
|
if (onPort) try {
|
|
2309
2730
|
accepted = onPort(handle.port, envelope.meta) !== false;
|
|
@@ -2318,57 +2739,215 @@ function multiplexPort(port, options) {
|
|
|
2318
2739
|
id: envelope.id,
|
|
2319
2740
|
reason: "rejected"
|
|
2320
2741
|
});
|
|
2742
|
+
return;
|
|
2321
2743
|
}
|
|
2322
2744
|
return;
|
|
2323
2745
|
}
|
|
2324
|
-
const
|
|
2746
|
+
const entry = open.get(envelope.id);
|
|
2747
|
+
if (envelope.type === "credit") {
|
|
2748
|
+
const c = entry?.credit;
|
|
2749
|
+
if (!c || !(envelope.bytes >= 0)) return;
|
|
2750
|
+
if (!c.enforced && c.opener) {
|
|
2751
|
+
if (c.exact && envelope.exact !== true) {
|
|
2752
|
+
c.exact = false;
|
|
2753
|
+
c.outstanding = 0;
|
|
2754
|
+
}
|
|
2755
|
+
post({
|
|
2756
|
+
type: "credit",
|
|
2757
|
+
id: envelope.id,
|
|
2758
|
+
bytes: 0
|
|
2759
|
+
});
|
|
2760
|
+
}
|
|
2761
|
+
c.enforced = true;
|
|
2762
|
+
c.outstanding = Math.max(0, c.outstanding - envelope.bytes);
|
|
2763
|
+
flush(c);
|
|
2764
|
+
return;
|
|
2765
|
+
}
|
|
2325
2766
|
if (envelope.type === "message") {
|
|
2326
|
-
|
|
2327
|
-
|
|
2767
|
+
const sizeOf = (c) => (c.exact ? byteLengthOf(event.data) : void 0) ?? estimateMessageSize(envelope.payload);
|
|
2768
|
+
if (!entry) return;
|
|
2769
|
+
touch(envelope.id);
|
|
2770
|
+
entry.handle.deliver(envelope.payload, entry.credit ? sizeOf(entry.credit) : void 0);
|
|
2771
|
+
return;
|
|
2772
|
+
}
|
|
2773
|
+
if (envelope.type === "frame") {
|
|
2774
|
+
if (!entry?.credit?.exact || !codec.decode || !(envelope.data instanceof Uint8Array)) return;
|
|
2775
|
+
touch(envelope.id);
|
|
2776
|
+
const c = entry.credit;
|
|
2777
|
+
const owed = c.unconsumed + c.toGrant;
|
|
2778
|
+
const partial = entry.partial ?? {
|
|
2779
|
+
chunks: [],
|
|
2780
|
+
bytes: 0,
|
|
2781
|
+
owed,
|
|
2782
|
+
charged: 0,
|
|
2783
|
+
...owed > 0 ? { room: c.window - owed } : {}
|
|
2784
|
+
};
|
|
2785
|
+
entry.partial = partial;
|
|
2786
|
+
const size = envelope.data.byteLength;
|
|
2787
|
+
const charged = partial.room == null ? Math.max(0, partial.bytes + size - c.window) : 0;
|
|
2788
|
+
const overrun = partial.room != null && partial.bytes + size > partial.room ? `what the window left (${partial.room} of ${c.window} bytes, ${partial.owed} still owed)` : reassembling - partial.charged + charged > maxReassembledBytes ? `maxReassembledBytes (${maxReassembledBytes} bytes past the window, across all ports)` : void 0;
|
|
2789
|
+
if (overrun) {
|
|
2790
|
+
freePartial(entry);
|
|
2791
|
+
dropOutgoing(envelope.id);
|
|
2792
|
+
open.delete(envelope.id);
|
|
2793
|
+
releaseWaiters(c);
|
|
2794
|
+
entry.handle.markClosed(/* @__PURE__ */ new RangeError(`webrun-rpc: a framed message passed ${overrun}`));
|
|
2795
|
+
post({
|
|
2796
|
+
type: "close",
|
|
2797
|
+
id: envelope.id,
|
|
2798
|
+
reason: "max-reassembled-bytes"
|
|
2799
|
+
});
|
|
2800
|
+
return;
|
|
2801
|
+
}
|
|
2802
|
+
partial.bytes += size;
|
|
2803
|
+
reassembling += charged - partial.charged;
|
|
2804
|
+
partial.charged = charged;
|
|
2805
|
+
partial.chunks.push(envelope.data);
|
|
2806
|
+
if (!envelope.last) return;
|
|
2807
|
+
freePartial(entry);
|
|
2808
|
+
const whole = new Uint8Array(partial.bytes);
|
|
2809
|
+
let at = 0;
|
|
2810
|
+
for (const chunk of partial.chunks) {
|
|
2811
|
+
whole.set(chunk, at);
|
|
2812
|
+
at += chunk.byteLength;
|
|
2813
|
+
}
|
|
2814
|
+
const inner = codec.decode(whole);
|
|
2815
|
+
if (inner?.type !== "message" || inner.id !== envelope.id) return;
|
|
2816
|
+
entry.handle.deliver(inner.payload, whole.byteLength);
|
|
2328
2817
|
return;
|
|
2329
2818
|
}
|
|
2330
|
-
|
|
2819
|
+
dropOutgoing(envelope.id);
|
|
2820
|
+
if (!entry) return;
|
|
2821
|
+
freePartial(entry);
|
|
2331
2822
|
open.delete(envelope.id);
|
|
2332
|
-
|
|
2823
|
+
releaseWaiters(entry.credit);
|
|
2824
|
+
entry.handle.markClosed(/* @__PURE__ */ new Error("webrun-rpc: the peer closed the port"));
|
|
2333
2825
|
};
|
|
2334
2826
|
const listener = (event) => {
|
|
2335
2827
|
const envelope = codec.read(event);
|
|
2336
|
-
if (envelope) handleEnvelope(envelope);
|
|
2828
|
+
if (envelope) handleEnvelope(envelope, event);
|
|
2829
|
+
};
|
|
2830
|
+
let peerKeepsAlive = false;
|
|
2831
|
+
let heardSincePing = true;
|
|
2832
|
+
let unanswered = 0;
|
|
2833
|
+
let pinger;
|
|
2834
|
+
const tick = () => {
|
|
2835
|
+
if (!keepalive) return;
|
|
2836
|
+
const backlogged = (port.bufferedAmount ?? 0) > 0;
|
|
2837
|
+
if (heardSincePing) unanswered = 0;
|
|
2838
|
+
else if (peerKeepsAlive && !backlogged) unanswered++;
|
|
2839
|
+
if (unanswered >= misses) {
|
|
2840
|
+
close(/* @__PURE__ */ new Error(`webrun-rpc: the peer did not answer ${misses} keepalive pings, ${keepalive.intervalMs} ms apart`)).catch(() => {});
|
|
2841
|
+
return;
|
|
2842
|
+
}
|
|
2843
|
+
heardSincePing = false;
|
|
2844
|
+
post({
|
|
2845
|
+
type: "ping",
|
|
2846
|
+
id: KEEPALIVE_ID
|
|
2847
|
+
});
|
|
2848
|
+
};
|
|
2849
|
+
const close = async (reason) => {
|
|
2850
|
+
if (muxClosed) return;
|
|
2851
|
+
clearInterval(pinger);
|
|
2852
|
+
muxClosed = true;
|
|
2853
|
+
signal?.removeEventListener("abort", onTransportAbort);
|
|
2854
|
+
port.removeEventListener("message", listener);
|
|
2855
|
+
const closedNow = /* @__PURE__ */ new Set();
|
|
2856
|
+
for (const [id, { handle, credit }] of [...open]) {
|
|
2857
|
+
closedNow.add(id);
|
|
2858
|
+
codecPost({
|
|
2859
|
+
type: "close",
|
|
2860
|
+
id
|
|
2861
|
+
});
|
|
2862
|
+
open.delete(id);
|
|
2863
|
+
releaseWaiters(credit);
|
|
2864
|
+
handle.markClosed(reason ?? /* @__PURE__ */ new Error("webrun-rpc: the multiplexer was closed"));
|
|
2865
|
+
}
|
|
2866
|
+
for (const id of outgoing.keys()) if (!closedNow.has(id)) codecPost({
|
|
2867
|
+
type: "close",
|
|
2868
|
+
id
|
|
2869
|
+
});
|
|
2870
|
+
outgoing.clear();
|
|
2871
|
+
ring.length = 0;
|
|
2872
|
+
await port.close?.();
|
|
2873
|
+
};
|
|
2874
|
+
const onTransportAbort = () => {
|
|
2875
|
+
close(signal?.reason).catch(() => {});
|
|
2337
2876
|
};
|
|
2338
2877
|
port.addEventListener("message", listener);
|
|
2339
2878
|
port.start?.();
|
|
2879
|
+
if (keepalive) {
|
|
2880
|
+
pinger = setInterval(tick, keepalive.intervalMs);
|
|
2881
|
+
pinger.unref?.();
|
|
2882
|
+
}
|
|
2883
|
+
if (signal?.aborted) close(signal.reason).catch(() => {});
|
|
2884
|
+
else signal?.addEventListener("abort", onTransportAbort, { once: true });
|
|
2340
2885
|
return {
|
|
2341
2886
|
maxMessageSize,
|
|
2342
|
-
async openPort(meta) {
|
|
2887
|
+
async openPort(meta, { signal: portSignal } = {}) {
|
|
2343
2888
|
if (muxClosed) throw new Error("webrun-rpc: the multiplexer is closed");
|
|
2344
|
-
if (
|
|
2889
|
+
if (portSignal?.aborted) throw portSignal.reason;
|
|
2890
|
+
if (open.size >= maxPorts) throw new RangeError(`webrun-rpc: maxPorts (${maxPorts}) reached; open ports by kind: ${describeKinds(open.values())}`);
|
|
2345
2891
|
while (open.has(nextId)) nextId += 2;
|
|
2346
2892
|
const id = nextId;
|
|
2347
2893
|
nextId += 2;
|
|
2348
|
-
const handle = attach(id);
|
|
2349
|
-
post({
|
|
2894
|
+
const handle = attach(id, meta, window, true, exactCodec);
|
|
2895
|
+
post(window ? exactCodec ? {
|
|
2896
|
+
type: "open",
|
|
2897
|
+
id,
|
|
2898
|
+
meta,
|
|
2899
|
+
window,
|
|
2900
|
+
exact: true
|
|
2901
|
+
} : {
|
|
2902
|
+
type: "open",
|
|
2903
|
+
id,
|
|
2904
|
+
meta,
|
|
2905
|
+
window
|
|
2906
|
+
} : {
|
|
2350
2907
|
type: "open",
|
|
2351
2908
|
id,
|
|
2352
2909
|
meta
|
|
2353
2910
|
});
|
|
2911
|
+
if (portSignal) {
|
|
2912
|
+
const record = open.get(id)?.handle;
|
|
2913
|
+
const onAbort = () => {
|
|
2914
|
+
if (record && open.get(id)?.handle === record) record.close(portSignal.reason);
|
|
2915
|
+
};
|
|
2916
|
+
portSignal.addEventListener("abort", onAbort, { once: true });
|
|
2917
|
+
getPortCloseSignal(handle.port)?.addEventListener("abort", () => portSignal.removeEventListener("abort", onAbort), { once: true });
|
|
2918
|
+
}
|
|
2354
2919
|
return handle.port;
|
|
2355
2920
|
},
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
}
|
|
2366
|
-
muxClosed = true;
|
|
2367
|
-
port.removeEventListener("message", listener);
|
|
2368
|
-
await port.close?.();
|
|
2921
|
+
close,
|
|
2922
|
+
ports() {
|
|
2923
|
+
return [...open.values()].map(({ handle: _, credit, ...info }) => credit ? {
|
|
2924
|
+
...info,
|
|
2925
|
+
...credit.enforced ? { window: credit.window } : {},
|
|
2926
|
+
queued: credit.queued,
|
|
2927
|
+
outstanding: credit.outstanding,
|
|
2928
|
+
unconsumed: credit.unconsumed
|
|
2929
|
+
} : info);
|
|
2369
2930
|
}
|
|
2370
2931
|
};
|
|
2371
2932
|
}
|
|
2933
|
+
/** The byte length of a received message's data, or `undefined` if it is not bytes. */
|
|
2934
|
+
function byteLengthOf(data) {
|
|
2935
|
+
if (data instanceof ArrayBuffer || ArrayBuffer.isView(data)) return data.byteLength;
|
|
2936
|
+
}
|
|
2937
|
+
/**
|
|
2938
|
+
* `"http: 1000, ws: 24"`: the three commonest `meta.kind`s among `entries`, so
|
|
2939
|
+
* a full table names who filled it. A port without a kind counts as `"?"`.
|
|
2940
|
+
*/
|
|
2941
|
+
function describeKinds(entries) {
|
|
2942
|
+
const counts = /* @__PURE__ */ new Map();
|
|
2943
|
+
for (const { meta } of entries) {
|
|
2944
|
+
const kind = meta?.kind;
|
|
2945
|
+
const name = typeof kind === "string" ? kind : "?";
|
|
2946
|
+
counts.set(name, (counts.get(name) ?? 0) + 1);
|
|
2947
|
+
}
|
|
2948
|
+
if (!counts.size) return "none";
|
|
2949
|
+
return [...counts].sort((x, y) => y[1] - x[1]).slice(0, 3).map(([name, n]) => `${name}: ${n}`).join(", ");
|
|
2950
|
+
}
|
|
2372
2951
|
/**
|
|
2373
2952
|
* Announced with every port this pair opens. Layer 1 never reads `meta` — it is
|
|
2374
2953
|
* here so a peer running something else over the same multiplexer can tell a
|
|
@@ -2430,14 +3009,17 @@ const serve = async ({ mux: factory, timeout }, handler) => {
|
|
|
2430
3009
|
const mux = await factory((port) => {
|
|
2431
3010
|
let off;
|
|
2432
3011
|
let finished = false;
|
|
2433
|
-
const tracked = (input) =>
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
3012
|
+
const tracked = (input) => {
|
|
3013
|
+
const output = handler(input);
|
|
3014
|
+
return returningAtOnce((async function* () {
|
|
3015
|
+
try {
|
|
3016
|
+
yield* output;
|
|
3017
|
+
} finally {
|
|
3018
|
+
finished = true;
|
|
3019
|
+
if (off) live.delete(off);
|
|
3020
|
+
}
|
|
3021
|
+
})(), () => void output.return(void 0).catch(() => {}));
|
|
3022
|
+
};
|
|
2441
3023
|
off = serveDuplexOverPort(port, tracked, streamOptions);
|
|
2442
3024
|
if (!finished) live.add(off);
|
|
2443
3025
|
});
|
|
@@ -2475,7 +3057,7 @@ function isEnvelope(value) {
|
|
|
2475
3057
|
const candidate = value;
|
|
2476
3058
|
if (typeof candidate.id !== "number") return false;
|
|
2477
3059
|
if (!Number.isInteger(candidate.id) || candidate.id < 0) return false;
|
|
2478
|
-
return candidate.type === "open" || candidate.type === "message" || candidate.type === "close";
|
|
3060
|
+
return candidate.type === "open" || candidate.type === "message" || candidate.type === "close" || candidate.type === "credit" || candidate.type === "frame" || candidate.type === "ping" || candidate.type === "pong";
|
|
2479
3061
|
}
|
|
2480
3062
|
/**
|
|
2481
3063
|
* For ports whose messages are structured values — a real `MessagePort`, a
|
|
@@ -2637,7 +3219,7 @@ function trackedLink(send, release) {
|
|
|
2637
3219
|
finish();
|
|
2638
3220
|
return response;
|
|
2639
3221
|
}
|
|
2640
|
-
return new Response(guardBody(response.body, lost, () => lostReason, finish), {
|
|
3222
|
+
return new Response(guardBody(response.body, lost, () => lostReason, finish, request.signal), {
|
|
2641
3223
|
status: response.status,
|
|
2642
3224
|
statusText: response.statusText,
|
|
2643
3225
|
headers: response.headers
|
|
@@ -2656,33 +3238,65 @@ function trackedLink(send, release) {
|
|
|
2656
3238
|
return asyncDisposable(link, () => link.close());
|
|
2657
3239
|
}
|
|
2658
3240
|
/**
|
|
2659
|
-
* A response body that reports its end (done, cancelled or
|
|
2660
|
-
* with the link's reason when the link is lost mid-stream.
|
|
3241
|
+
* A response body that reports its end (done, cancelled, failed or aborted) and
|
|
3242
|
+
* fails with the link's reason when the link is lost mid-stream. An abort of the
|
|
3243
|
+
* request's signal ends it at once, read or not: the exchange is over (the layer
|
|
3244
|
+
* below has given its port back), so the request leaves `inflight` then.
|
|
2661
3245
|
*/
|
|
2662
|
-
function guardBody(body, lost, reason, finish) {
|
|
3246
|
+
function guardBody(body, lost, reason, finish, signal) {
|
|
2663
3247
|
const reader = body.getReader();
|
|
3248
|
+
let controller;
|
|
3249
|
+
let ended = false;
|
|
3250
|
+
const end = () => {
|
|
3251
|
+
ended = true;
|
|
3252
|
+
signal?.removeEventListener("abort", onAbort);
|
|
3253
|
+
finish();
|
|
3254
|
+
};
|
|
3255
|
+
const onAbort = () => {
|
|
3256
|
+
if (ended) return;
|
|
3257
|
+
end();
|
|
3258
|
+
reader.cancel().catch(() => {});
|
|
3259
|
+
try {
|
|
3260
|
+
controller.error(abortError(signal));
|
|
3261
|
+
} catch {}
|
|
3262
|
+
};
|
|
2664
3263
|
return new ReadableStream({
|
|
3264
|
+
start(c) {
|
|
3265
|
+
controller = c;
|
|
3266
|
+
if (signal?.aborted) onAbort();
|
|
3267
|
+
else signal?.addEventListener("abort", onAbort, { once: true });
|
|
3268
|
+
},
|
|
2665
3269
|
async pull(controller) {
|
|
2666
3270
|
try {
|
|
2667
3271
|
const { done, value } = await Promise.race([reader.read(), lost]);
|
|
3272
|
+
if (ended) return;
|
|
2668
3273
|
if (done) {
|
|
2669
|
-
|
|
3274
|
+
end();
|
|
2670
3275
|
controller.close();
|
|
2671
3276
|
return;
|
|
2672
3277
|
}
|
|
2673
3278
|
controller.enqueue(value);
|
|
2674
3279
|
} catch (error) {
|
|
2675
|
-
|
|
3280
|
+
if (ended) return;
|
|
3281
|
+
end();
|
|
2676
3282
|
reader.cancel().catch(() => {});
|
|
2677
3283
|
controller.error(reason() ?? error);
|
|
2678
3284
|
}
|
|
2679
3285
|
},
|
|
2680
3286
|
async cancel(why) {
|
|
2681
|
-
|
|
3287
|
+
end();
|
|
2682
3288
|
await reader.cancel(why).catch(() => {});
|
|
2683
3289
|
}
|
|
2684
3290
|
});
|
|
2685
3291
|
}
|
|
3292
|
+
/** The signal's reason as an `Error` (`AbortError` when it gave none). */
|
|
3293
|
+
function abortError(signal) {
|
|
3294
|
+
const why = signal.reason;
|
|
3295
|
+
if (why instanceof Error) return why;
|
|
3296
|
+
const err = new Error(why === void 0 ? "Aborted" : String(why));
|
|
3297
|
+
err.name = "AbortError";
|
|
3298
|
+
return err;
|
|
3299
|
+
}
|
|
2686
3300
|
//#endregion
|
|
2687
3301
|
//#region src/link/locks.ts
|
|
2688
3302
|
function lockManager() {
|
|
@@ -5498,6 +6112,8 @@ var WorkerLinkImpl = class {
|
|
|
5498
6112
|
_ownerLock;
|
|
5499
6113
|
_attaching;
|
|
5500
6114
|
_detached = false;
|
|
6115
|
+
/** The `seq` of the last link sent to the worker. */
|
|
6116
|
+
_sent = 0;
|
|
5501
6117
|
constructor(_adapter, _control, init) {
|
|
5502
6118
|
this._adapter = _adapter;
|
|
5503
6119
|
this._control = _control;
|
|
@@ -5509,6 +6125,7 @@ var WorkerLinkImpl = class {
|
|
|
5509
6125
|
_control.onmessage = (event) => {
|
|
5510
6126
|
const message = event.data;
|
|
5511
6127
|
if (message?.type === "relink") {
|
|
6128
|
+
if (message.seen !== void 0 && message.seen < this._sent) return;
|
|
5512
6129
|
this._adapter._poke(WAKE_MESSAGE);
|
|
5513
6130
|
this.attach().catch(() => {});
|
|
5514
6131
|
} else if (message?.type === "keepalive") this._adapter._poke(KEEPALIVE_MESSAGE);
|
|
@@ -5555,7 +6172,8 @@ var WorkerLinkImpl = class {
|
|
|
5555
6172
|
this._control.postMessage({
|
|
5556
6173
|
type: "link",
|
|
5557
6174
|
key: this.key,
|
|
5558
|
-
routerLock: reply.routerLock
|
|
6175
|
+
routerLock: reply.routerLock,
|
|
6176
|
+
seq: ++this._sent
|
|
5559
6177
|
}, [
|
|
5560
6178
|
down.port2,
|
|
5561
6179
|
ctl.port2,
|
|
@@ -5656,12 +6274,18 @@ function serveHttpInWorker(handler, options = {}) {
|
|
|
5656
6274
|
const ownerLock = newLockName("webrun-http-owner");
|
|
5657
6275
|
const held = holdLock(ownerLock);
|
|
5658
6276
|
const controls = /* @__PURE__ */ new Set();
|
|
6277
|
+
const seen = /* @__PURE__ */ new Map();
|
|
5659
6278
|
const tellPages = (message) => {
|
|
5660
6279
|
for (const control of controls) control.postMessage(message);
|
|
5661
6280
|
};
|
|
5662
6281
|
const endpoint = new HandlerEndpoint(handler, {
|
|
5663
6282
|
...options,
|
|
5664
|
-
requestRelink: () =>
|
|
6283
|
+
requestRelink: () => {
|
|
6284
|
+
for (const control of controls) control.postMessage({
|
|
6285
|
+
type: "relink",
|
|
6286
|
+
seen: seen.get(control) ?? 0
|
|
6287
|
+
});
|
|
6288
|
+
},
|
|
5665
6289
|
keepAlive: () => tellPages({ type: "keepalive" })
|
|
5666
6290
|
});
|
|
5667
6291
|
const onMessage = (event) => {
|
|
@@ -5673,6 +6297,7 @@ function serveHttpInWorker(handler, options = {}) {
|
|
|
5673
6297
|
control.onmessage = (message) => {
|
|
5674
6298
|
const msg = message.data;
|
|
5675
6299
|
if (msg?.type === "link") {
|
|
6300
|
+
if (msg.seq !== void 0) seen.set(control, msg.seq);
|
|
5676
6301
|
const [down, ctl, up] = message.ports;
|
|
5677
6302
|
if (down) endpoint.link(msg.key, {
|
|
5678
6303
|
down,
|
|
@@ -5713,6 +6338,7 @@ function serveHttpInWorker(handler, options = {}) {
|
|
|
5713
6338
|
await endpoint.close();
|
|
5714
6339
|
for (const control of controls) control.close();
|
|
5715
6340
|
controls.clear();
|
|
6341
|
+
seen.clear();
|
|
5716
6342
|
(await held)();
|
|
5717
6343
|
}
|
|
5718
6344
|
};
|