@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/worker.js
CHANGED
|
@@ -1207,7 +1207,11 @@ async function drain(output) {
|
|
|
1207
1207
|
*/
|
|
1208
1208
|
function httpServe(handler, options = {}) {
|
|
1209
1209
|
const codec = options.codec ?? defaultCodec;
|
|
1210
|
-
return
|
|
1210
|
+
return (input) => {
|
|
1211
|
+
const abandoned = new AbortController();
|
|
1212
|
+
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")));
|
|
1213
|
+
};
|
|
1214
|
+
async function* exchange(input, context) {
|
|
1211
1215
|
let decoded;
|
|
1212
1216
|
try {
|
|
1213
1217
|
decoded = await codec.decodeRequest(input);
|
|
@@ -1219,7 +1223,7 @@ function httpServe(handler, options = {}) {
|
|
|
1219
1223
|
const replyCodec = decoded.codec ?? codec;
|
|
1220
1224
|
let result;
|
|
1221
1225
|
try {
|
|
1222
|
-
result = await handler(decoded.envelope, decoded.body);
|
|
1226
|
+
result = await handler(decoded.envelope, decoded.body, context);
|
|
1223
1227
|
} catch (error) {
|
|
1224
1228
|
yield* encodeErrorResponse(replyCodec, error, decoded.envelope.method);
|
|
1225
1229
|
return;
|
|
@@ -1234,7 +1238,36 @@ function httpServe(handler, options = {}) {
|
|
|
1234
1238
|
await result.release?.();
|
|
1235
1239
|
} catch {}
|
|
1236
1240
|
}
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
/**
|
|
1244
|
+
* `gen`, except that `return()` and `throw()` from its consumer call `abandon`
|
|
1245
|
+
* at once — before they are queued behind a `next()` still parked inside the
|
|
1246
|
+
* generator (the handler's `await`) — unless the generator already finished.
|
|
1247
|
+
*/
|
|
1248
|
+
function abortingOnReturn(gen, abandon) {
|
|
1249
|
+
let finished = false;
|
|
1250
|
+
const settle = (r) => {
|
|
1251
|
+
if (r.done) finished = true;
|
|
1252
|
+
return r;
|
|
1253
|
+
};
|
|
1254
|
+
const wrapped = {
|
|
1255
|
+
next: (...args) => gen.next(...args).then(settle),
|
|
1256
|
+
return(value) {
|
|
1257
|
+
if (!finished) abandon();
|
|
1258
|
+
finished = true;
|
|
1259
|
+
return gen.return(value);
|
|
1260
|
+
},
|
|
1261
|
+
throw(error) {
|
|
1262
|
+
if (!finished) abandon(error);
|
|
1263
|
+
finished = true;
|
|
1264
|
+
return gen.throw(error);
|
|
1265
|
+
},
|
|
1266
|
+
[Symbol.asyncIterator]() {
|
|
1267
|
+
return wrapped;
|
|
1268
|
+
}
|
|
1237
1269
|
};
|
|
1270
|
+
return asyncDisposable$2(wrapped, () => wrapped.return(void 0));
|
|
1238
1271
|
}
|
|
1239
1272
|
/**
|
|
1240
1273
|
* The one place that knows a runtime may not implement request body streams,
|
|
@@ -1411,17 +1444,18 @@ async function fetchOverDuplex(call, request, options = {}) {
|
|
|
1411
1444
|
else await release?.();
|
|
1412
1445
|
return new Response(null, responseInit);
|
|
1413
1446
|
}
|
|
1414
|
-
return new Response(
|
|
1447
|
+
return new Response(responseBody(respBody, request.signal, release), responseInit);
|
|
1415
1448
|
}
|
|
1416
1449
|
/**
|
|
1417
1450
|
* Wrap a `(Request) => Promise<Response>` handler as a `Duplex` so it can be
|
|
1418
1451
|
* registered with any `webrun-streams-*` adapter's `serve`.
|
|
1419
1452
|
*/
|
|
1420
1453
|
function serveFetchOverDuplex(handler, options = {}) {
|
|
1421
|
-
return httpServe(async (env, body) => {
|
|
1454
|
+
return httpServe(async (env, body, exchange) => {
|
|
1422
1455
|
const reqInit = {
|
|
1423
1456
|
method: env.method,
|
|
1424
|
-
headers: forwardableHeaders(env.headers)
|
|
1457
|
+
headers: forwardableHeaders(env.headers),
|
|
1458
|
+
signal: exchange?.signal
|
|
1425
1459
|
};
|
|
1426
1460
|
let upload;
|
|
1427
1461
|
if (env.method !== "GET" && env.method !== "HEAD") {
|
|
@@ -1542,15 +1576,78 @@ function trackingEnd(source) {
|
|
|
1542
1576
|
settle
|
|
1543
1577
|
};
|
|
1544
1578
|
}
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1579
|
+
/**
|
|
1580
|
+
* How long a cancelled body waits for the read in flight before it abandons the call anyway.
|
|
1581
|
+
* The read's answer tells a body that has ended (the call is finished: an upload the handler
|
|
1582
|
+
* reads after answering goes on) from one that has not; a peer that sends nothing more never
|
|
1583
|
+
* answers it. A body whose end comes later than this after the cancel is abandoned too, so a
|
|
1584
|
+
* handler still streaming its response and still reading the upload then loses the upload.
|
|
1585
|
+
*/
|
|
1586
|
+
const CANCEL_GRACE_MS = 1e3;
|
|
1587
|
+
/**
|
|
1588
|
+
* The response body as a stream that the request's signal can end at any time.
|
|
1589
|
+
*
|
|
1590
|
+
* An abort errors the stream with the abort's reason at once — a read waiting
|
|
1591
|
+
* for a peer that sends nothing more rejects then, not when a next chunk
|
|
1592
|
+
* arrives, which may be never — and abandons the call (`release`), so on a
|
|
1593
|
+
* multiplexed transport its port is given back whether or not anyone still
|
|
1594
|
+
* reads the body. The listener is removed once the body ends, however it ends:
|
|
1595
|
+
* read to its end, cancelled, failed or aborted. A body nobody reads or cancels
|
|
1596
|
+
* keeps it until the signal aborts (and keeps the call open until then anyway).
|
|
1597
|
+
* A cancel abandons the call too — unless the read in flight comes back with the
|
|
1598
|
+
* body's end, which finishes it instead (see `cancel`).
|
|
1599
|
+
*/
|
|
1600
|
+
function responseBody(body, signal, release) {
|
|
1601
|
+
const it = body[Symbol.asyncIterator]();
|
|
1602
|
+
let controller;
|
|
1603
|
+
let ended = false;
|
|
1604
|
+
let reading = false;
|
|
1605
|
+
const end = () => {
|
|
1606
|
+
ended = true;
|
|
1607
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1608
|
+
};
|
|
1609
|
+
const onAbort = () => {
|
|
1610
|
+
if (ended) return;
|
|
1611
|
+
end();
|
|
1612
|
+
try {
|
|
1613
|
+
controller.error(abortReason$1(signal));
|
|
1614
|
+
} catch {}
|
|
1615
|
+
release?.();
|
|
1616
|
+
};
|
|
1617
|
+
return new ReadableStream({
|
|
1618
|
+
start(c) {
|
|
1619
|
+
controller = c;
|
|
1620
|
+
if (signal?.aborted) onAbort();
|
|
1621
|
+
else signal?.addEventListener("abort", onAbort, { once: true });
|
|
1622
|
+
},
|
|
1623
|
+
async pull(c) {
|
|
1624
|
+
try {
|
|
1625
|
+
reading = true;
|
|
1626
|
+
const { done, value } = await it.next().finally(() => {
|
|
1627
|
+
reading = false;
|
|
1628
|
+
});
|
|
1629
|
+
if (ended) return;
|
|
1630
|
+
if (done) {
|
|
1631
|
+
end();
|
|
1632
|
+
c.close();
|
|
1633
|
+
return;
|
|
1634
|
+
}
|
|
1635
|
+
c.enqueue(value);
|
|
1636
|
+
} catch (err) {
|
|
1637
|
+
if (ended) return;
|
|
1638
|
+
end();
|
|
1639
|
+
c.error(err);
|
|
1640
|
+
}
|
|
1641
|
+
},
|
|
1642
|
+
cancel() {
|
|
1643
|
+
end();
|
|
1644
|
+
const returned = Promise.resolve(it.return?.()).catch(() => {});
|
|
1645
|
+
if (!reading) return;
|
|
1646
|
+
const grace = setTimeout(() => void release?.(), CANCEL_GRACE_MS);
|
|
1647
|
+
grace.unref?.();
|
|
1648
|
+
returned.finally(() => clearTimeout(grace));
|
|
1649
|
+
}
|
|
1650
|
+
});
|
|
1554
1651
|
}
|
|
1555
1652
|
//#endregion
|
|
1556
1653
|
//#region ../webrun-rpc/dist/index.js
|
|
@@ -1675,8 +1772,11 @@ function listenPort(port, handler, { channelName = "", log = () => {} } = {}) {
|
|
|
1675
1772
|
error
|
|
1676
1773
|
});
|
|
1677
1774
|
};
|
|
1678
|
-
|
|
1679
|
-
|
|
1775
|
+
const listener = (event) => {
|
|
1776
|
+
onMessage(event);
|
|
1777
|
+
};
|
|
1778
|
+
port.addEventListener("message", listener);
|
|
1779
|
+
return () => port.removeEventListener("message", listener);
|
|
1680
1780
|
}
|
|
1681
1781
|
/**
|
|
1682
1782
|
* Wraps an async iterable so that an `AbortSignal` firing causes the wrapper
|
|
@@ -2054,7 +2154,11 @@ function receiveChunks(port, channelName, controller, touch) {
|
|
|
2054
2154
|
* outstanding at a time (spec D11/D12), with no per-chunk deadline (spec D8).
|
|
2055
2155
|
*/
|
|
2056
2156
|
async function sendChunks(port, channelName, output, { maxMessageSize, log }, signal, touch) {
|
|
2057
|
-
const
|
|
2157
|
+
const framed = maxMessageSize ? toChunks(maxMessageSize)(output) : output;
|
|
2158
|
+
const stream = throughAbort(framed, signal);
|
|
2159
|
+
const producer = output;
|
|
2160
|
+
const returnProducer = () => void Promise.resolve(producer.return?.()).catch(() => {});
|
|
2161
|
+
if (framed !== output) signal.addEventListener("abort", returnProducer, { once: true });
|
|
2058
2162
|
try {
|
|
2059
2163
|
await sendIterator(async ({ done, value, error }) => {
|
|
2060
2164
|
if (signal.aborted) return;
|
|
@@ -2078,9 +2182,38 @@ async function sendChunks(port, channelName, output, { maxMessageSize, log }, si
|
|
|
2078
2182
|
} catch (err) {
|
|
2079
2183
|
if (signal.aborted) return;
|
|
2080
2184
|
throw err;
|
|
2185
|
+
} finally {
|
|
2186
|
+
signal.removeEventListener("abort", returnProducer);
|
|
2081
2187
|
}
|
|
2082
2188
|
}
|
|
2083
2189
|
/**
|
|
2190
|
+
* Runs `fn` on a later task, never a microtask: a producer looping on
|
|
2191
|
+
* `whenPortReady` must let the transport's own events run. `setImmediate`
|
|
2192
|
+
* where it exists; elsewhere a `MessageChannel`, because nested `setTimeout`s
|
|
2193
|
+
* are clamped to 4 ms in browsers.
|
|
2194
|
+
*/
|
|
2195
|
+
const setImmediateFn = globalThis.setImmediate;
|
|
2196
|
+
/** @internal */
|
|
2197
|
+
const nextTask = setImmediateFn ? (fn) => {
|
|
2198
|
+
setImmediateFn(fn);
|
|
2199
|
+
} : (() => {
|
|
2200
|
+
let channel;
|
|
2201
|
+
const queue = [];
|
|
2202
|
+
return (fn) => {
|
|
2203
|
+
if (!channel) {
|
|
2204
|
+
channel = new MessageChannel();
|
|
2205
|
+
channel.port1.onmessage = () => queue.shift()?.();
|
|
2206
|
+
}
|
|
2207
|
+
queue.push(fn);
|
|
2208
|
+
channel.port2.postMessage(0);
|
|
2209
|
+
};
|
|
2210
|
+
})();
|
|
2211
|
+
const states = /* @__PURE__ */ new WeakMap();
|
|
2212
|
+
/** @internal Registered by the multiplexer for each port with a window. */
|
|
2213
|
+
function setPortSendState(port, state) {
|
|
2214
|
+
states.set(port, state);
|
|
2215
|
+
}
|
|
2216
|
+
/**
|
|
2084
2217
|
* One virtual port.
|
|
2085
2218
|
*
|
|
2086
2219
|
* `deliver` and `markClosed` are deliberately not on `port`: the consumer holds
|
|
@@ -2095,6 +2228,14 @@ function newVirtualPort(send, requestClose) {
|
|
|
2095
2228
|
if (closing.signal.aborted) return;
|
|
2096
2229
|
closing.abort(reason ?? /* @__PURE__ */ new Error("webrun-rpc: the port was closed"));
|
|
2097
2230
|
};
|
|
2231
|
+
const close = (reason) => {
|
|
2232
|
+
if (closed) return;
|
|
2233
|
+
closed = true;
|
|
2234
|
+
const notify = requestClose;
|
|
2235
|
+
listeners.clear();
|
|
2236
|
+
notify(reason);
|
|
2237
|
+
abort(reason);
|
|
2238
|
+
};
|
|
2098
2239
|
const port = {
|
|
2099
2240
|
addEventListener(_type, listener) {
|
|
2100
2241
|
listeners.add(listener);
|
|
@@ -2106,24 +2247,28 @@ function newVirtualPort(send, requestClose) {
|
|
|
2106
2247
|
if (closed) return;
|
|
2107
2248
|
send(message, transfer);
|
|
2108
2249
|
},
|
|
2109
|
-
close
|
|
2110
|
-
if (closed) return;
|
|
2111
|
-
closed = true;
|
|
2112
|
-
const notify = requestClose;
|
|
2113
|
-
listeners.clear();
|
|
2114
|
-
notify();
|
|
2115
|
-
abort();
|
|
2116
|
-
}
|
|
2250
|
+
close
|
|
2117
2251
|
};
|
|
2118
2252
|
setPortCloseSignal(port, closing.signal);
|
|
2253
|
+
const dispose = Symbol.dispose;
|
|
2254
|
+
if (dispose) Object.defineProperty(port, dispose, {
|
|
2255
|
+
value: () => close(),
|
|
2256
|
+
configurable: true
|
|
2257
|
+
});
|
|
2119
2258
|
return {
|
|
2120
2259
|
port,
|
|
2121
2260
|
deliver(payload) {
|
|
2122
2261
|
if (closed) return;
|
|
2123
2262
|
const event = new MessageEvent("message", { data: payload });
|
|
2263
|
+
let pending;
|
|
2124
2264
|
for (const listener of [...listeners]) try {
|
|
2125
|
-
listener(event);
|
|
2265
|
+
const result = listener(event);
|
|
2266
|
+
if (result && typeof result.then === "function") {
|
|
2267
|
+
pending ??= [];
|
|
2268
|
+
pending.push(result);
|
|
2269
|
+
}
|
|
2126
2270
|
} catch {}
|
|
2271
|
+
return pending ? Promise.allSettled(pending) : void 0;
|
|
2127
2272
|
},
|
|
2128
2273
|
markClosed(reason) {
|
|
2129
2274
|
closed = true;
|
|
@@ -2132,53 +2277,318 @@ function newVirtualPort(send, requestClose) {
|
|
|
2132
2277
|
},
|
|
2133
2278
|
isClosed() {
|
|
2134
2279
|
return closed;
|
|
2280
|
+
},
|
|
2281
|
+
hasListeners() {
|
|
2282
|
+
return listeners.size > 0;
|
|
2135
2283
|
}
|
|
2136
2284
|
};
|
|
2137
2285
|
}
|
|
2138
2286
|
/**
|
|
2139
|
-
* The
|
|
2140
|
-
*
|
|
2287
|
+
* The id of keepalive's `ping` / `pong`: one no port ever takes. An older mux
|
|
2288
|
+
* whose codec passes unknown envelopes through reads them as a close of that
|
|
2289
|
+
* id, which must therefore not be a real port (0 is the initiator's first).
|
|
2141
2290
|
*/
|
|
2142
|
-
const
|
|
2291
|
+
const KEEPALIVE_ID = Number.MAX_SAFE_INTEGER;
|
|
2292
|
+
/** The longest delay a timer holds; past it `setInterval` fires at once. */
|
|
2293
|
+
const MAX_TIMER_MS = 2 ** 31 - 1;
|
|
2143
2294
|
/** Ceiling on concurrently open virtual ports. Bounds the id table only. */
|
|
2144
2295
|
const DEFAULT_MAX_PORTS = 1024;
|
|
2145
2296
|
/**
|
|
2297
|
+
* What a message costs a port's window when the codec cannot say
|
|
2298
|
+
* (`PortCodec.encode`): its bytes and string lengths, found in its values up
|
|
2299
|
+
* to 4 levels deep (at most 256 values), and at least 64. Both ends compute it
|
|
2300
|
+
* from the same structured value, so they agree.
|
|
2301
|
+
*/
|
|
2302
|
+
function estimateMessageSize(payload) {
|
|
2303
|
+
let budget = 256;
|
|
2304
|
+
const walk = (v, depth) => {
|
|
2305
|
+
if (budget-- <= 0) return 0;
|
|
2306
|
+
if (typeof v === "string") return v.length;
|
|
2307
|
+
if (v instanceof ArrayBuffer) return v.byteLength;
|
|
2308
|
+
if (ArrayBuffer.isView(v)) return v.byteLength;
|
|
2309
|
+
if (v === null || typeof v !== "object" || depth >= 4) return 8;
|
|
2310
|
+
let n = 16;
|
|
2311
|
+
if (Array.isArray(v)) for (const x of v) n += walk(x, depth + 1);
|
|
2312
|
+
else for (const k in v) n += k.length + walk(v[k], depth + 1);
|
|
2313
|
+
return n;
|
|
2314
|
+
};
|
|
2315
|
+
return Math.max(64, walk(payload, 0));
|
|
2316
|
+
}
|
|
2317
|
+
/**
|
|
2146
2318
|
* The default `PortMux`: emulates multiplexing over a single port.
|
|
2147
2319
|
*
|
|
2148
2320
|
* A transport that already multiplexes natively supplies its own `PortMux`
|
|
2149
2321
|
* instead — this implementation is for transports that offer one pipe.
|
|
2150
2322
|
*/
|
|
2151
2323
|
function multiplexPort(port, options) {
|
|
2152
|
-
const { codec, onPort, side = "initiator", maxPorts = DEFAULT_MAX_PORTS, maxMessageSize } = options;
|
|
2324
|
+
const { codec, onPort, side = "initiator", maxPorts = DEFAULT_MAX_PORTS, maxMessageSize, signal, window, frameSize, keepalive, onLeak } = options;
|
|
2325
|
+
const maxReassembledBytes = options.maxReassembledBytes ?? 67108864;
|
|
2326
|
+
const misses = keepalive?.misses ?? 3;
|
|
2327
|
+
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`);
|
|
2328
|
+
if (frameSize != null && !(Number.isSafeInteger(frameSize) && frameSize > 0)) throw new RangeError(`webrun-rpc: frameSize must be a positive integer, got ${frameSize}`);
|
|
2329
|
+
if (!(Number.isSafeInteger(maxReassembledBytes) && maxReassembledBytes > 0)) throw new RangeError(`webrun-rpc: maxReassembledBytes must be a positive integer, got ${maxReassembledBytes}`);
|
|
2153
2330
|
const open = /* @__PURE__ */ new Map();
|
|
2331
|
+
let reassembling = 0;
|
|
2332
|
+
const freePartial = (entry) => {
|
|
2333
|
+
if (!entry?.partial) return;
|
|
2334
|
+
reassembling -= entry.partial.charged;
|
|
2335
|
+
entry.partial = void 0;
|
|
2336
|
+
};
|
|
2337
|
+
const leaks = onLeak ? new FinalizationRegistry(({ id, record, release }) => {
|
|
2338
|
+
const entry = open.get(id);
|
|
2339
|
+
if (entry?.handle !== record) return;
|
|
2340
|
+
const { handle: _, credit: __, partial: ___, ...info } = entry;
|
|
2341
|
+
if (!record.hasListeners()) {
|
|
2342
|
+
release("leaked");
|
|
2343
|
+
record.markClosed(/* @__PURE__ */ new Error("webrun-rpc: the port was dropped without close()"));
|
|
2344
|
+
}
|
|
2345
|
+
try {
|
|
2346
|
+
onLeak?.(info);
|
|
2347
|
+
} catch {}
|
|
2348
|
+
}) : void 0;
|
|
2349
|
+
const touch = (id) => {
|
|
2350
|
+
const entry = open.get(id);
|
|
2351
|
+
if (entry) entry.lastActivity = Date.now();
|
|
2352
|
+
};
|
|
2154
2353
|
let nextId = side === "initiator" ? 0 : 1;
|
|
2155
2354
|
let muxClosed = false;
|
|
2355
|
+
/** This end counts encoded bytes and reassembles frames. */
|
|
2356
|
+
const exactCodec = codec.encode != null && codec.decode != null;
|
|
2156
2357
|
const post = (envelope, transfer) => {
|
|
2157
2358
|
if (muxClosed) return;
|
|
2359
|
+
codecPost(envelope, transfer);
|
|
2360
|
+
};
|
|
2361
|
+
/** Like `post`, also while closing: the mux's own close notices. */
|
|
2362
|
+
const codecPost = (envelope, transfer) => {
|
|
2158
2363
|
try {
|
|
2159
2364
|
codec.post(port, envelope, transfer);
|
|
2160
2365
|
} catch {}
|
|
2161
2366
|
};
|
|
2162
|
-
const
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2367
|
+
const outgoing = /* @__PURE__ */ new Map();
|
|
2368
|
+
const ring = [];
|
|
2369
|
+
let pumping = false;
|
|
2370
|
+
const pump = async () => {
|
|
2371
|
+
pumping = true;
|
|
2372
|
+
try {
|
|
2373
|
+
while (ring.length && !muxClosed) {
|
|
2374
|
+
if (port.whenDrained) await port.whenDrained();
|
|
2375
|
+
if (muxClosed) break;
|
|
2376
|
+
const id = ring.shift();
|
|
2377
|
+
const queue = outgoing.get(id);
|
|
2378
|
+
const item = queue?.shift();
|
|
2379
|
+
if (queue?.length) ring.push(id);
|
|
2380
|
+
else outgoing.delete(id);
|
|
2381
|
+
item?.();
|
|
2382
|
+
}
|
|
2383
|
+
} catch (err) {
|
|
2384
|
+
close(err).catch(() => {});
|
|
2385
|
+
} finally {
|
|
2386
|
+
pumping = false;
|
|
2387
|
+
}
|
|
2388
|
+
};
|
|
2389
|
+
const sendOnPort = (id, item) => {
|
|
2390
|
+
if (frameSize == null) {
|
|
2391
|
+
item();
|
|
2392
|
+
return;
|
|
2393
|
+
}
|
|
2394
|
+
let queue = outgoing.get(id);
|
|
2395
|
+
if (!queue) {
|
|
2396
|
+
queue = [];
|
|
2397
|
+
outgoing.set(id, queue);
|
|
2398
|
+
ring.push(id);
|
|
2399
|
+
}
|
|
2400
|
+
queue.push(item);
|
|
2401
|
+
if (!pumping) pump();
|
|
2402
|
+
};
|
|
2403
|
+
const dropOutgoing = (id) => {
|
|
2404
|
+
if (!outgoing.delete(id)) return;
|
|
2405
|
+
const at = ring.indexOf(id);
|
|
2406
|
+
if (at >= 0) ring.splice(at, 1);
|
|
2407
|
+
};
|
|
2408
|
+
const postBytes = (bytes) => {
|
|
2409
|
+
if (muxClosed) return;
|
|
2410
|
+
try {
|
|
2411
|
+
port.postMessage(bytes);
|
|
2412
|
+
} catch {}
|
|
2413
|
+
};
|
|
2414
|
+
const releaseWaiters = (c) => {
|
|
2415
|
+
for (const w of c?.waiters.splice(0) ?? []) w.resolve();
|
|
2416
|
+
};
|
|
2417
|
+
const flush = (c) => {
|
|
2418
|
+
while (c.queue.length) {
|
|
2419
|
+
const next = c.queue[0];
|
|
2420
|
+
if (c.enforced && c.outstanding > 0 && c.outstanding + next.size > c.window) break;
|
|
2421
|
+
c.queue.shift();
|
|
2422
|
+
c.queued -= next.size;
|
|
2423
|
+
c.outstanding += next.size;
|
|
2424
|
+
next.send();
|
|
2425
|
+
}
|
|
2426
|
+
for (const w of [...c.waiters]) if (c.queued <= w.below) {
|
|
2427
|
+
c.waiters.splice(c.waiters.indexOf(w), 1);
|
|
2428
|
+
w.resolve();
|
|
2429
|
+
}
|
|
2430
|
+
};
|
|
2431
|
+
const grant = (id, c, bytes) => {
|
|
2432
|
+
c.unconsumed -= bytes;
|
|
2433
|
+
c.toGrant += bytes;
|
|
2434
|
+
const send = () => {
|
|
2435
|
+
c.grantScheduled = false;
|
|
2436
|
+
if (!c.toGrant || !open.has(id)) return;
|
|
2169
2437
|
post({
|
|
2438
|
+
type: "credit",
|
|
2439
|
+
id,
|
|
2440
|
+
bytes: c.toGrant
|
|
2441
|
+
});
|
|
2442
|
+
c.toGrant = 0;
|
|
2443
|
+
};
|
|
2444
|
+
if (c.toGrant >= c.window / 4) {
|
|
2445
|
+
send();
|
|
2446
|
+
return;
|
|
2447
|
+
}
|
|
2448
|
+
if (c.grantScheduled) return;
|
|
2449
|
+
c.grantScheduled = true;
|
|
2450
|
+
setTimeout(send, 0);
|
|
2451
|
+
};
|
|
2452
|
+
const attach = (id, meta, portWindow, opener, exact) => {
|
|
2453
|
+
const c = typeof portWindow === "number" && Number.isSafeInteger(portWindow) && portWindow > 0 ? {
|
|
2454
|
+
window: portWindow,
|
|
2455
|
+
enforced: false,
|
|
2456
|
+
opener,
|
|
2457
|
+
exact,
|
|
2458
|
+
outstanding: 0,
|
|
2459
|
+
queue: [],
|
|
2460
|
+
queued: 0,
|
|
2461
|
+
unconsumed: 0,
|
|
2462
|
+
waiters: [],
|
|
2463
|
+
toGrant: 0,
|
|
2464
|
+
grantScheduled: false
|
|
2465
|
+
} : void 0;
|
|
2466
|
+
const self = newVirtualPort((payload, transfer) => {
|
|
2467
|
+
touch(id);
|
|
2468
|
+
const envelope = {
|
|
2469
|
+
type: "message",
|
|
2470
|
+
id,
|
|
2471
|
+
payload
|
|
2472
|
+
};
|
|
2473
|
+
if (!c) {
|
|
2474
|
+
post(envelope, transfer);
|
|
2475
|
+
return;
|
|
2476
|
+
}
|
|
2477
|
+
const bytes = c.exact ? codec.encode?.(envelope) : void 0;
|
|
2478
|
+
const size = bytes?.byteLength ?? estimateMessageSize(payload);
|
|
2479
|
+
const waits = frameSize != null || c.queue.length > 0 || c.enforced && c.outstanding > 0 && c.outstanding + size > c.window;
|
|
2480
|
+
const held = !bytes && waits ? {
|
|
2481
|
+
...envelope,
|
|
2482
|
+
payload: structuredClone(payload, { transfer })
|
|
2483
|
+
} : void 0;
|
|
2484
|
+
const send = (whole = false) => {
|
|
2485
|
+
if (!bytes) {
|
|
2486
|
+
sendOnPort(id, held ? () => post(held) : () => post(envelope, transfer));
|
|
2487
|
+
return;
|
|
2488
|
+
}
|
|
2489
|
+
if (!whole && frameSize != null && c.enforced && c.exact && bytes.byteLength > frameSize) {
|
|
2490
|
+
for (let at = 0; at < bytes.byteLength; at += frameSize) {
|
|
2491
|
+
const data = bytes.subarray(at, at + frameSize);
|
|
2492
|
+
const frame = at + frameSize >= bytes.byteLength ? {
|
|
2493
|
+
type: "frame",
|
|
2494
|
+
id,
|
|
2495
|
+
data,
|
|
2496
|
+
last: true
|
|
2497
|
+
} : {
|
|
2498
|
+
type: "frame",
|
|
2499
|
+
id,
|
|
2500
|
+
data
|
|
2501
|
+
};
|
|
2502
|
+
sendOnPort(id, () => post(frame));
|
|
2503
|
+
}
|
|
2504
|
+
return;
|
|
2505
|
+
}
|
|
2506
|
+
sendOnPort(id, () => postBytes(bytes));
|
|
2507
|
+
};
|
|
2508
|
+
c.queue.push({
|
|
2509
|
+
send,
|
|
2510
|
+
size
|
|
2511
|
+
});
|
|
2512
|
+
c.queued += size;
|
|
2513
|
+
flush(c);
|
|
2514
|
+
}, (reason) => {
|
|
2515
|
+
release(reason);
|
|
2516
|
+
record.markClosed(reason);
|
|
2517
|
+
});
|
|
2518
|
+
function release(reason) {
|
|
2519
|
+
freePartial(open.get(id));
|
|
2520
|
+
open.delete(id);
|
|
2521
|
+
leaks?.unregister(record);
|
|
2522
|
+
if (c?.queue.length) {
|
|
2523
|
+
for (const item of c.queue.splice(0)) item.send(true);
|
|
2524
|
+
c.queued = 0;
|
|
2525
|
+
}
|
|
2526
|
+
sendOnPort(id, () => post({
|
|
2170
2527
|
type: "close",
|
|
2171
2528
|
id,
|
|
2172
2529
|
reason
|
|
2530
|
+
}));
|
|
2531
|
+
releaseWaiters(c);
|
|
2532
|
+
}
|
|
2533
|
+
const { markClosed, isClosed, hasListeners } = self;
|
|
2534
|
+
if (c) {
|
|
2535
|
+
const deliver = self.deliver;
|
|
2536
|
+
self.deliver = (payload, size = estimateMessageSize(payload)) => {
|
|
2537
|
+
c.unconsumed += size;
|
|
2538
|
+
const done = deliver(payload);
|
|
2539
|
+
if (done) done.then(() => grant(id, c, size));
|
|
2540
|
+
else grant(id, c, size);
|
|
2541
|
+
return done;
|
|
2542
|
+
};
|
|
2543
|
+
setPortSendState(self.port, {
|
|
2544
|
+
bufferedAmount: () => c.queued,
|
|
2545
|
+
ready: (below = 0) => new Promise((resolve) => {
|
|
2546
|
+
if (c.queued <= below || isClosed()) nextTask(resolve);
|
|
2547
|
+
else c.waiters.push({
|
|
2548
|
+
below,
|
|
2549
|
+
resolve: () => nextTask(resolve)
|
|
2550
|
+
});
|
|
2551
|
+
})
|
|
2173
2552
|
});
|
|
2174
|
-
|
|
2175
|
-
|
|
2553
|
+
}
|
|
2554
|
+
const record = {
|
|
2555
|
+
deliver: self.deliver,
|
|
2556
|
+
markClosed,
|
|
2557
|
+
isClosed,
|
|
2558
|
+
hasListeners,
|
|
2559
|
+
close: (reason) => {
|
|
2560
|
+
if (isClosed()) return;
|
|
2561
|
+
release(reason);
|
|
2562
|
+
markClosed(reason);
|
|
2563
|
+
}
|
|
2564
|
+
};
|
|
2565
|
+
leaks?.register(self.port, {
|
|
2566
|
+
id,
|
|
2567
|
+
record,
|
|
2568
|
+
release
|
|
2569
|
+
}, record);
|
|
2570
|
+
const now = Date.now();
|
|
2571
|
+
open.set(id, {
|
|
2572
|
+
id,
|
|
2573
|
+
meta,
|
|
2574
|
+
openedAt: now,
|
|
2575
|
+
lastActivity: now,
|
|
2576
|
+
handle: record,
|
|
2577
|
+
credit: c
|
|
2176
2578
|
});
|
|
2177
|
-
open.set(id, self);
|
|
2178
2579
|
return self;
|
|
2179
2580
|
};
|
|
2180
|
-
const handleEnvelope = (envelope) => {
|
|
2581
|
+
const handleEnvelope = (envelope, event) => {
|
|
2181
2582
|
if (muxClosed) return;
|
|
2583
|
+
heardSincePing = true;
|
|
2584
|
+
if (envelope.type === "ping" || envelope.type === "pong") {
|
|
2585
|
+
peerKeepsAlive = true;
|
|
2586
|
+
if (envelope.type === "ping") post({
|
|
2587
|
+
type: "pong",
|
|
2588
|
+
id: KEEPALIVE_ID
|
|
2589
|
+
});
|
|
2590
|
+
return;
|
|
2591
|
+
}
|
|
2182
2592
|
if (envelope.type === "open") {
|
|
2183
2593
|
if (open.has(envelope.id)) return;
|
|
2184
2594
|
if (open.size >= maxPorts) {
|
|
@@ -2189,7 +2599,18 @@ function multiplexPort(port, options) {
|
|
|
2189
2599
|
});
|
|
2190
2600
|
return;
|
|
2191
2601
|
}
|
|
2192
|
-
const handle = attach(envelope.id);
|
|
2602
|
+
const handle = attach(envelope.id, envelope.meta, envelope.window, false, envelope.exact === true && exactCodec);
|
|
2603
|
+
const credit = open.get(envelope.id)?.credit;
|
|
2604
|
+
if (credit) post(credit.exact ? {
|
|
2605
|
+
type: "credit",
|
|
2606
|
+
id: envelope.id,
|
|
2607
|
+
bytes: 0,
|
|
2608
|
+
exact: true
|
|
2609
|
+
} : {
|
|
2610
|
+
type: "credit",
|
|
2611
|
+
id: envelope.id,
|
|
2612
|
+
bytes: 0
|
|
2613
|
+
});
|
|
2193
2614
|
let accepted = false;
|
|
2194
2615
|
if (onPort) try {
|
|
2195
2616
|
accepted = onPort(handle.port, envelope.meta) !== false;
|
|
@@ -2204,57 +2625,215 @@ function multiplexPort(port, options) {
|
|
|
2204
2625
|
id: envelope.id,
|
|
2205
2626
|
reason: "rejected"
|
|
2206
2627
|
});
|
|
2628
|
+
return;
|
|
2207
2629
|
}
|
|
2208
2630
|
return;
|
|
2209
2631
|
}
|
|
2210
|
-
const
|
|
2632
|
+
const entry = open.get(envelope.id);
|
|
2633
|
+
if (envelope.type === "credit") {
|
|
2634
|
+
const c = entry?.credit;
|
|
2635
|
+
if (!c || !(envelope.bytes >= 0)) return;
|
|
2636
|
+
if (!c.enforced && c.opener) {
|
|
2637
|
+
if (c.exact && envelope.exact !== true) {
|
|
2638
|
+
c.exact = false;
|
|
2639
|
+
c.outstanding = 0;
|
|
2640
|
+
}
|
|
2641
|
+
post({
|
|
2642
|
+
type: "credit",
|
|
2643
|
+
id: envelope.id,
|
|
2644
|
+
bytes: 0
|
|
2645
|
+
});
|
|
2646
|
+
}
|
|
2647
|
+
c.enforced = true;
|
|
2648
|
+
c.outstanding = Math.max(0, c.outstanding - envelope.bytes);
|
|
2649
|
+
flush(c);
|
|
2650
|
+
return;
|
|
2651
|
+
}
|
|
2211
2652
|
if (envelope.type === "message") {
|
|
2212
|
-
|
|
2213
|
-
|
|
2653
|
+
const sizeOf = (c) => (c.exact ? byteLengthOf(event.data) : void 0) ?? estimateMessageSize(envelope.payload);
|
|
2654
|
+
if (!entry) return;
|
|
2655
|
+
touch(envelope.id);
|
|
2656
|
+
entry.handle.deliver(envelope.payload, entry.credit ? sizeOf(entry.credit) : void 0);
|
|
2214
2657
|
return;
|
|
2215
2658
|
}
|
|
2216
|
-
if (
|
|
2659
|
+
if (envelope.type === "frame") {
|
|
2660
|
+
if (!entry?.credit?.exact || !codec.decode || !(envelope.data instanceof Uint8Array)) return;
|
|
2661
|
+
touch(envelope.id);
|
|
2662
|
+
const c = entry.credit;
|
|
2663
|
+
const owed = c.unconsumed + c.toGrant;
|
|
2664
|
+
const partial = entry.partial ?? {
|
|
2665
|
+
chunks: [],
|
|
2666
|
+
bytes: 0,
|
|
2667
|
+
owed,
|
|
2668
|
+
charged: 0,
|
|
2669
|
+
...owed > 0 ? { room: c.window - owed } : {}
|
|
2670
|
+
};
|
|
2671
|
+
entry.partial = partial;
|
|
2672
|
+
const size = envelope.data.byteLength;
|
|
2673
|
+
const charged = partial.room == null ? Math.max(0, partial.bytes + size - c.window) : 0;
|
|
2674
|
+
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;
|
|
2675
|
+
if (overrun) {
|
|
2676
|
+
freePartial(entry);
|
|
2677
|
+
dropOutgoing(envelope.id);
|
|
2678
|
+
open.delete(envelope.id);
|
|
2679
|
+
releaseWaiters(c);
|
|
2680
|
+
entry.handle.markClosed(/* @__PURE__ */ new RangeError(`webrun-rpc: a framed message passed ${overrun}`));
|
|
2681
|
+
post({
|
|
2682
|
+
type: "close",
|
|
2683
|
+
id: envelope.id,
|
|
2684
|
+
reason: "max-reassembled-bytes"
|
|
2685
|
+
});
|
|
2686
|
+
return;
|
|
2687
|
+
}
|
|
2688
|
+
partial.bytes += size;
|
|
2689
|
+
reassembling += charged - partial.charged;
|
|
2690
|
+
partial.charged = charged;
|
|
2691
|
+
partial.chunks.push(envelope.data);
|
|
2692
|
+
if (!envelope.last) return;
|
|
2693
|
+
freePartial(entry);
|
|
2694
|
+
const whole = new Uint8Array(partial.bytes);
|
|
2695
|
+
let at = 0;
|
|
2696
|
+
for (const chunk of partial.chunks) {
|
|
2697
|
+
whole.set(chunk, at);
|
|
2698
|
+
at += chunk.byteLength;
|
|
2699
|
+
}
|
|
2700
|
+
const inner = codec.decode(whole);
|
|
2701
|
+
if (inner?.type !== "message" || inner.id !== envelope.id) return;
|
|
2702
|
+
entry.handle.deliver(inner.payload, whole.byteLength);
|
|
2703
|
+
return;
|
|
2704
|
+
}
|
|
2705
|
+
dropOutgoing(envelope.id);
|
|
2706
|
+
if (!entry) return;
|
|
2707
|
+
freePartial(entry);
|
|
2217
2708
|
open.delete(envelope.id);
|
|
2218
|
-
|
|
2709
|
+
releaseWaiters(entry.credit);
|
|
2710
|
+
entry.handle.markClosed(/* @__PURE__ */ new Error("webrun-rpc: the peer closed the port"));
|
|
2219
2711
|
};
|
|
2220
2712
|
const listener = (event) => {
|
|
2221
2713
|
const envelope = codec.read(event);
|
|
2222
|
-
if (envelope) handleEnvelope(envelope);
|
|
2714
|
+
if (envelope) handleEnvelope(envelope, event);
|
|
2715
|
+
};
|
|
2716
|
+
let peerKeepsAlive = false;
|
|
2717
|
+
let heardSincePing = true;
|
|
2718
|
+
let unanswered = 0;
|
|
2719
|
+
let pinger;
|
|
2720
|
+
const tick = () => {
|
|
2721
|
+
if (!keepalive) return;
|
|
2722
|
+
const backlogged = (port.bufferedAmount ?? 0) > 0;
|
|
2723
|
+
if (heardSincePing) unanswered = 0;
|
|
2724
|
+
else if (peerKeepsAlive && !backlogged) unanswered++;
|
|
2725
|
+
if (unanswered >= misses) {
|
|
2726
|
+
close(/* @__PURE__ */ new Error(`webrun-rpc: the peer did not answer ${misses} keepalive pings, ${keepalive.intervalMs} ms apart`)).catch(() => {});
|
|
2727
|
+
return;
|
|
2728
|
+
}
|
|
2729
|
+
heardSincePing = false;
|
|
2730
|
+
post({
|
|
2731
|
+
type: "ping",
|
|
2732
|
+
id: KEEPALIVE_ID
|
|
2733
|
+
});
|
|
2734
|
+
};
|
|
2735
|
+
const close = async (reason) => {
|
|
2736
|
+
if (muxClosed) return;
|
|
2737
|
+
clearInterval(pinger);
|
|
2738
|
+
muxClosed = true;
|
|
2739
|
+
signal?.removeEventListener("abort", onTransportAbort);
|
|
2740
|
+
port.removeEventListener("message", listener);
|
|
2741
|
+
const closedNow = /* @__PURE__ */ new Set();
|
|
2742
|
+
for (const [id, { handle, credit }] of [...open]) {
|
|
2743
|
+
closedNow.add(id);
|
|
2744
|
+
codecPost({
|
|
2745
|
+
type: "close",
|
|
2746
|
+
id
|
|
2747
|
+
});
|
|
2748
|
+
open.delete(id);
|
|
2749
|
+
releaseWaiters(credit);
|
|
2750
|
+
handle.markClosed(reason ?? /* @__PURE__ */ new Error("webrun-rpc: the multiplexer was closed"));
|
|
2751
|
+
}
|
|
2752
|
+
for (const id of outgoing.keys()) if (!closedNow.has(id)) codecPost({
|
|
2753
|
+
type: "close",
|
|
2754
|
+
id
|
|
2755
|
+
});
|
|
2756
|
+
outgoing.clear();
|
|
2757
|
+
ring.length = 0;
|
|
2758
|
+
await port.close?.();
|
|
2759
|
+
};
|
|
2760
|
+
const onTransportAbort = () => {
|
|
2761
|
+
close(signal?.reason).catch(() => {});
|
|
2223
2762
|
};
|
|
2224
2763
|
port.addEventListener("message", listener);
|
|
2225
2764
|
port.start?.();
|
|
2765
|
+
if (keepalive) {
|
|
2766
|
+
pinger = setInterval(tick, keepalive.intervalMs);
|
|
2767
|
+
pinger.unref?.();
|
|
2768
|
+
}
|
|
2769
|
+
if (signal?.aborted) close(signal.reason).catch(() => {});
|
|
2770
|
+
else signal?.addEventListener("abort", onTransportAbort, { once: true });
|
|
2226
2771
|
return {
|
|
2227
2772
|
maxMessageSize,
|
|
2228
|
-
async openPort(meta) {
|
|
2773
|
+
async openPort(meta, { signal: portSignal } = {}) {
|
|
2229
2774
|
if (muxClosed) throw new Error("webrun-rpc: the multiplexer is closed");
|
|
2230
|
-
if (
|
|
2775
|
+
if (portSignal?.aborted) throw portSignal.reason;
|
|
2776
|
+
if (open.size >= maxPorts) throw new RangeError(`webrun-rpc: maxPorts (${maxPorts}) reached; open ports by kind: ${describeKinds(open.values())}`);
|
|
2231
2777
|
while (open.has(nextId)) nextId += 2;
|
|
2232
2778
|
const id = nextId;
|
|
2233
2779
|
nextId += 2;
|
|
2234
|
-
const handle = attach(id);
|
|
2235
|
-
post({
|
|
2780
|
+
const handle = attach(id, meta, window, true, exactCodec);
|
|
2781
|
+
post(window ? exactCodec ? {
|
|
2782
|
+
type: "open",
|
|
2783
|
+
id,
|
|
2784
|
+
meta,
|
|
2785
|
+
window,
|
|
2786
|
+
exact: true
|
|
2787
|
+
} : {
|
|
2788
|
+
type: "open",
|
|
2789
|
+
id,
|
|
2790
|
+
meta,
|
|
2791
|
+
window
|
|
2792
|
+
} : {
|
|
2236
2793
|
type: "open",
|
|
2237
2794
|
id,
|
|
2238
2795
|
meta
|
|
2239
2796
|
});
|
|
2797
|
+
if (portSignal) {
|
|
2798
|
+
const record = open.get(id)?.handle;
|
|
2799
|
+
const onAbort = () => {
|
|
2800
|
+
if (record && open.get(id)?.handle === record) record.close(portSignal.reason);
|
|
2801
|
+
};
|
|
2802
|
+
portSignal.addEventListener("abort", onAbort, { once: true });
|
|
2803
|
+
getPortCloseSignal(handle.port)?.addEventListener("abort", () => portSignal.removeEventListener("abort", onAbort), { once: true });
|
|
2804
|
+
}
|
|
2240
2805
|
return handle.port;
|
|
2241
2806
|
},
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
}
|
|
2252
|
-
muxClosed = true;
|
|
2253
|
-
port.removeEventListener("message", listener);
|
|
2254
|
-
await port.close?.();
|
|
2807
|
+
close,
|
|
2808
|
+
ports() {
|
|
2809
|
+
return [...open.values()].map(({ handle: _, credit, ...info }) => credit ? {
|
|
2810
|
+
...info,
|
|
2811
|
+
...credit.enforced ? { window: credit.window } : {},
|
|
2812
|
+
queued: credit.queued,
|
|
2813
|
+
outstanding: credit.outstanding,
|
|
2814
|
+
unconsumed: credit.unconsumed
|
|
2815
|
+
} : info);
|
|
2255
2816
|
}
|
|
2256
2817
|
};
|
|
2257
2818
|
}
|
|
2819
|
+
/** The byte length of a received message's data, or `undefined` if it is not bytes. */
|
|
2820
|
+
function byteLengthOf(data) {
|
|
2821
|
+
if (data instanceof ArrayBuffer || ArrayBuffer.isView(data)) return data.byteLength;
|
|
2822
|
+
}
|
|
2823
|
+
/**
|
|
2824
|
+
* `"http: 1000, ws: 24"`: the three commonest `meta.kind`s among `entries`, so
|
|
2825
|
+
* a full table names who filled it. A port without a kind counts as `"?"`.
|
|
2826
|
+
*/
|
|
2827
|
+
function describeKinds(entries) {
|
|
2828
|
+
const counts = /* @__PURE__ */ new Map();
|
|
2829
|
+
for (const { meta } of entries) {
|
|
2830
|
+
const kind = meta?.kind;
|
|
2831
|
+
const name = typeof kind === "string" ? kind : "?";
|
|
2832
|
+
counts.set(name, (counts.get(name) ?? 0) + 1);
|
|
2833
|
+
}
|
|
2834
|
+
if (!counts.size) return "none";
|
|
2835
|
+
return [...counts].sort((x, y) => y[1] - x[1]).slice(0, 3).map(([name, n]) => `${name}: ${n}`).join(", ");
|
|
2836
|
+
}
|
|
2258
2837
|
/**
|
|
2259
2838
|
* Announced with every port this pair opens. Layer 1 never reads `meta` — it is
|
|
2260
2839
|
* here so a peer running something else over the same multiplexer can tell a
|
|
@@ -2316,14 +2895,17 @@ const serve = async ({ mux: factory, timeout }, handler) => {
|
|
|
2316
2895
|
const mux = await factory((port) => {
|
|
2317
2896
|
let off;
|
|
2318
2897
|
let finished = false;
|
|
2319
|
-
const tracked = (input) =>
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2898
|
+
const tracked = (input) => {
|
|
2899
|
+
const output = handler(input);
|
|
2900
|
+
return returningAtOnce((async function* () {
|
|
2901
|
+
try {
|
|
2902
|
+
yield* output;
|
|
2903
|
+
} finally {
|
|
2904
|
+
finished = true;
|
|
2905
|
+
if (off) live.delete(off);
|
|
2906
|
+
}
|
|
2907
|
+
})(), () => void output.return(void 0).catch(() => {}));
|
|
2908
|
+
};
|
|
2327
2909
|
off = serveDuplexOverPort(port, tracked, streamOptions);
|
|
2328
2910
|
if (!finished) live.add(off);
|
|
2329
2911
|
});
|
|
@@ -2361,7 +2943,7 @@ function isEnvelope(value) {
|
|
|
2361
2943
|
const candidate = value;
|
|
2362
2944
|
if (typeof candidate.id !== "number") return false;
|
|
2363
2945
|
if (!Number.isInteger(candidate.id) || candidate.id < 0) return false;
|
|
2364
|
-
return candidate.type === "open" || candidate.type === "message" || candidate.type === "close";
|
|
2946
|
+
return candidate.type === "open" || candidate.type === "message" || candidate.type === "close" || candidate.type === "credit" || candidate.type === "frame" || candidate.type === "ping" || candidate.type === "pong";
|
|
2365
2947
|
}
|
|
2366
2948
|
/**
|
|
2367
2949
|
* For ports whose messages are structured values — a real `MessagePort`, a
|
|
@@ -2523,7 +3105,7 @@ function trackedLink(send, release) {
|
|
|
2523
3105
|
finish();
|
|
2524
3106
|
return response;
|
|
2525
3107
|
}
|
|
2526
|
-
return new Response(guardBody(response.body, lost, () => lostReason, finish), {
|
|
3108
|
+
return new Response(guardBody(response.body, lost, () => lostReason, finish, request.signal), {
|
|
2527
3109
|
status: response.status,
|
|
2528
3110
|
statusText: response.statusText,
|
|
2529
3111
|
headers: response.headers
|
|
@@ -2542,33 +3124,65 @@ function trackedLink(send, release) {
|
|
|
2542
3124
|
return asyncDisposable(link, () => link.close());
|
|
2543
3125
|
}
|
|
2544
3126
|
/**
|
|
2545
|
-
* A response body that reports its end (done, cancelled or
|
|
2546
|
-
* with the link's reason when the link is lost mid-stream.
|
|
3127
|
+
* A response body that reports its end (done, cancelled, failed or aborted) and
|
|
3128
|
+
* fails with the link's reason when the link is lost mid-stream. An abort of the
|
|
3129
|
+
* request's signal ends it at once, read or not: the exchange is over (the layer
|
|
3130
|
+
* below has given its port back), so the request leaves `inflight` then.
|
|
2547
3131
|
*/
|
|
2548
|
-
function guardBody(body, lost, reason, finish) {
|
|
3132
|
+
function guardBody(body, lost, reason, finish, signal) {
|
|
2549
3133
|
const reader = body.getReader();
|
|
3134
|
+
let controller;
|
|
3135
|
+
let ended = false;
|
|
3136
|
+
const end = () => {
|
|
3137
|
+
ended = true;
|
|
3138
|
+
signal?.removeEventListener("abort", onAbort);
|
|
3139
|
+
finish();
|
|
3140
|
+
};
|
|
3141
|
+
const onAbort = () => {
|
|
3142
|
+
if (ended) return;
|
|
3143
|
+
end();
|
|
3144
|
+
reader.cancel().catch(() => {});
|
|
3145
|
+
try {
|
|
3146
|
+
controller.error(abortError(signal));
|
|
3147
|
+
} catch {}
|
|
3148
|
+
};
|
|
2550
3149
|
return new ReadableStream({
|
|
3150
|
+
start(c) {
|
|
3151
|
+
controller = c;
|
|
3152
|
+
if (signal?.aborted) onAbort();
|
|
3153
|
+
else signal?.addEventListener("abort", onAbort, { once: true });
|
|
3154
|
+
},
|
|
2551
3155
|
async pull(controller) {
|
|
2552
3156
|
try {
|
|
2553
3157
|
const { done, value } = await Promise.race([reader.read(), lost]);
|
|
3158
|
+
if (ended) return;
|
|
2554
3159
|
if (done) {
|
|
2555
|
-
|
|
3160
|
+
end();
|
|
2556
3161
|
controller.close();
|
|
2557
3162
|
return;
|
|
2558
3163
|
}
|
|
2559
3164
|
controller.enqueue(value);
|
|
2560
3165
|
} catch (error) {
|
|
2561
|
-
|
|
3166
|
+
if (ended) return;
|
|
3167
|
+
end();
|
|
2562
3168
|
reader.cancel().catch(() => {});
|
|
2563
3169
|
controller.error(reason() ?? error);
|
|
2564
3170
|
}
|
|
2565
3171
|
},
|
|
2566
3172
|
async cancel(why) {
|
|
2567
|
-
|
|
3173
|
+
end();
|
|
2568
3174
|
await reader.cancel(why).catch(() => {});
|
|
2569
3175
|
}
|
|
2570
3176
|
});
|
|
2571
3177
|
}
|
|
3178
|
+
/** The signal's reason as an `Error` (`AbortError` when it gave none). */
|
|
3179
|
+
function abortError(signal) {
|
|
3180
|
+
const why = signal.reason;
|
|
3181
|
+
if (why instanceof Error) return why;
|
|
3182
|
+
const err = new Error(why === void 0 ? "Aborted" : String(why));
|
|
3183
|
+
err.name = "AbortError";
|
|
3184
|
+
return err;
|
|
3185
|
+
}
|
|
2572
3186
|
//#endregion
|
|
2573
3187
|
//#region src/link/locks.ts
|
|
2574
3188
|
function lockManager() {
|
|
@@ -3715,12 +4329,18 @@ function serveHttpInWorker(handler, options = {}) {
|
|
|
3715
4329
|
const ownerLock = newLockName("webrun-http-owner");
|
|
3716
4330
|
const held = holdLock(ownerLock);
|
|
3717
4331
|
const controls = /* @__PURE__ */ new Set();
|
|
4332
|
+
const seen = /* @__PURE__ */ new Map();
|
|
3718
4333
|
const tellPages = (message) => {
|
|
3719
4334
|
for (const control of controls) control.postMessage(message);
|
|
3720
4335
|
};
|
|
3721
4336
|
const endpoint = new HandlerEndpoint(handler, {
|
|
3722
4337
|
...options,
|
|
3723
|
-
requestRelink: () =>
|
|
4338
|
+
requestRelink: () => {
|
|
4339
|
+
for (const control of controls) control.postMessage({
|
|
4340
|
+
type: "relink",
|
|
4341
|
+
seen: seen.get(control) ?? 0
|
|
4342
|
+
});
|
|
4343
|
+
},
|
|
3724
4344
|
keepAlive: () => tellPages({ type: "keepalive" })
|
|
3725
4345
|
});
|
|
3726
4346
|
const onMessage = (event) => {
|
|
@@ -3732,6 +4352,7 @@ function serveHttpInWorker(handler, options = {}) {
|
|
|
3732
4352
|
control.onmessage = (message) => {
|
|
3733
4353
|
const msg = message.data;
|
|
3734
4354
|
if (msg?.type === "link") {
|
|
4355
|
+
if (msg.seq !== void 0) seen.set(control, msg.seq);
|
|
3735
4356
|
const [down, ctl, up] = message.ports;
|
|
3736
4357
|
if (down) endpoint.link(msg.key, {
|
|
3737
4358
|
down,
|
|
@@ -3772,6 +4393,7 @@ function serveHttpInWorker(handler, options = {}) {
|
|
|
3772
4393
|
await endpoint.close();
|
|
3773
4394
|
for (const control of controls) control.close();
|
|
3774
4395
|
controls.clear();
|
|
4396
|
+
seen.clear();
|
|
3775
4397
|
(await held)();
|
|
3776
4398
|
}
|
|
3777
4399
|
};
|