@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/relay-worker.js
CHANGED
|
@@ -1367,26 +1367,6 @@ async function* readableToAsyncIterable(stream) {
|
|
|
1367
1367
|
} catch {}
|
|
1368
1368
|
}
|
|
1369
1369
|
}
|
|
1370
|
-
function asyncIterableToReadable(iter) {
|
|
1371
|
-
const it = iter[Symbol.asyncIterator]();
|
|
1372
|
-
return new ReadableStream({
|
|
1373
|
-
async pull(controller) {
|
|
1374
|
-
try {
|
|
1375
|
-
const { done, value } = await it.next();
|
|
1376
|
-
if (done) {
|
|
1377
|
-
controller.close();
|
|
1378
|
-
return;
|
|
1379
|
-
}
|
|
1380
|
-
controller.enqueue(value);
|
|
1381
|
-
} catch (err) {
|
|
1382
|
-
controller.error(err);
|
|
1383
|
-
}
|
|
1384
|
-
},
|
|
1385
|
-
async cancel() {
|
|
1386
|
-
await it.return?.();
|
|
1387
|
-
}
|
|
1388
|
-
});
|
|
1389
|
-
}
|
|
1390
1370
|
/**
|
|
1391
1371
|
* Run a `Request` through a `Duplex` call and reconstruct the `Response` on
|
|
1392
1372
|
* the other side. The request's `signal` is plumbed into the body iteration —
|
|
@@ -1419,17 +1399,80 @@ async function fetchOverDuplex(call, request, options = {}) {
|
|
|
1419
1399
|
else await release?.();
|
|
1420
1400
|
return new Response(null, responseInit);
|
|
1421
1401
|
}
|
|
1422
|
-
return new Response(
|
|
1402
|
+
return new Response(responseBody(respBody, request.signal, release), responseInit);
|
|
1423
1403
|
}
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1404
|
+
/**
|
|
1405
|
+
* How long a cancelled body waits for the read in flight before it abandons the call anyway.
|
|
1406
|
+
* The read's answer tells a body that has ended (the call is finished: an upload the handler
|
|
1407
|
+
* reads after answering goes on) from one that has not; a peer that sends nothing more never
|
|
1408
|
+
* answers it. A body whose end comes later than this after the cancel is abandoned too, so a
|
|
1409
|
+
* handler still streaming its response and still reading the upload then loses the upload.
|
|
1410
|
+
*/
|
|
1411
|
+
const CANCEL_GRACE_MS = 1e3;
|
|
1412
|
+
/**
|
|
1413
|
+
* The response body as a stream that the request's signal can end at any time.
|
|
1414
|
+
*
|
|
1415
|
+
* An abort errors the stream with the abort's reason at once — a read waiting
|
|
1416
|
+
* for a peer that sends nothing more rejects then, not when a next chunk
|
|
1417
|
+
* arrives, which may be never — and abandons the call (`release`), so on a
|
|
1418
|
+
* multiplexed transport its port is given back whether or not anyone still
|
|
1419
|
+
* reads the body. The listener is removed once the body ends, however it ends:
|
|
1420
|
+
* read to its end, cancelled, failed or aborted. A body nobody reads or cancels
|
|
1421
|
+
* keeps it until the signal aborts (and keeps the call open until then anyway).
|
|
1422
|
+
* A cancel abandons the call too — unless the read in flight comes back with the
|
|
1423
|
+
* body's end, which finishes it instead (see `cancel`).
|
|
1424
|
+
*/
|
|
1425
|
+
function responseBody(body, signal, release) {
|
|
1426
|
+
const it = body[Symbol.asyncIterator]();
|
|
1427
|
+
let controller;
|
|
1428
|
+
let ended = false;
|
|
1429
|
+
let reading = false;
|
|
1430
|
+
const end = () => {
|
|
1431
|
+
ended = true;
|
|
1432
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1433
|
+
};
|
|
1434
|
+
const onAbort = () => {
|
|
1435
|
+
if (ended) return;
|
|
1436
|
+
end();
|
|
1437
|
+
try {
|
|
1438
|
+
controller.error(abortReason$1(signal));
|
|
1439
|
+
} catch {}
|
|
1440
|
+
release?.();
|
|
1441
|
+
};
|
|
1442
|
+
return new ReadableStream({
|
|
1443
|
+
start(c) {
|
|
1444
|
+
controller = c;
|
|
1445
|
+
if (signal?.aborted) onAbort();
|
|
1446
|
+
else signal?.addEventListener("abort", onAbort, { once: true });
|
|
1447
|
+
},
|
|
1448
|
+
async pull(c) {
|
|
1449
|
+
try {
|
|
1450
|
+
reading = true;
|
|
1451
|
+
const { done, value } = await it.next().finally(() => {
|
|
1452
|
+
reading = false;
|
|
1453
|
+
});
|
|
1454
|
+
if (ended) return;
|
|
1455
|
+
if (done) {
|
|
1456
|
+
end();
|
|
1457
|
+
c.close();
|
|
1458
|
+
return;
|
|
1459
|
+
}
|
|
1460
|
+
c.enqueue(value);
|
|
1461
|
+
} catch (err) {
|
|
1462
|
+
if (ended) return;
|
|
1463
|
+
end();
|
|
1464
|
+
c.error(err);
|
|
1465
|
+
}
|
|
1466
|
+
},
|
|
1467
|
+
cancel() {
|
|
1468
|
+
end();
|
|
1469
|
+
const returned = Promise.resolve(it.return?.()).catch(() => {});
|
|
1470
|
+
if (!reading) return;
|
|
1471
|
+
const grace = setTimeout(() => void release?.(), CANCEL_GRACE_MS);
|
|
1472
|
+
grace.unref?.();
|
|
1473
|
+
returned.finally(() => clearTimeout(grace));
|
|
1474
|
+
}
|
|
1475
|
+
});
|
|
1433
1476
|
}
|
|
1434
1477
|
var HttpError = class HttpError extends Error {
|
|
1435
1478
|
status;
|
|
@@ -1774,8 +1817,11 @@ function listenPort(port, handler, { channelName = "", log = () => {} } = {}) {
|
|
|
1774
1817
|
error
|
|
1775
1818
|
});
|
|
1776
1819
|
};
|
|
1777
|
-
|
|
1778
|
-
|
|
1820
|
+
const listener = (event) => {
|
|
1821
|
+
onMessage(event);
|
|
1822
|
+
};
|
|
1823
|
+
port.addEventListener("message", listener);
|
|
1824
|
+
return () => port.removeEventListener("message", listener);
|
|
1779
1825
|
}
|
|
1780
1826
|
/**
|
|
1781
1827
|
* Wraps an async iterable so that an `AbortSignal` firing causes the wrapper
|
|
@@ -2094,7 +2140,11 @@ function receiveChunks(port, channelName, controller, touch) {
|
|
|
2094
2140
|
* outstanding at a time (spec D11/D12), with no per-chunk deadline (spec D8).
|
|
2095
2141
|
*/
|
|
2096
2142
|
async function sendChunks(port, channelName, output, { maxMessageSize, log }, signal, touch) {
|
|
2097
|
-
const
|
|
2143
|
+
const framed = maxMessageSize ? toChunks(maxMessageSize)(output) : output;
|
|
2144
|
+
const stream = throughAbort(framed, signal);
|
|
2145
|
+
const producer = output;
|
|
2146
|
+
const returnProducer = () => void Promise.resolve(producer.return?.()).catch(() => {});
|
|
2147
|
+
if (framed !== output) signal.addEventListener("abort", returnProducer, { once: true });
|
|
2098
2148
|
try {
|
|
2099
2149
|
await sendIterator(async ({ done, value, error }) => {
|
|
2100
2150
|
if (signal.aborted) return;
|
|
@@ -2118,7 +2168,36 @@ async function sendChunks(port, channelName, output, { maxMessageSize, log }, si
|
|
|
2118
2168
|
} catch (err) {
|
|
2119
2169
|
if (signal.aborted) return;
|
|
2120
2170
|
throw err;
|
|
2121
|
-
}
|
|
2171
|
+
} finally {
|
|
2172
|
+
signal.removeEventListener("abort", returnProducer);
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
/**
|
|
2176
|
+
* Runs `fn` on a later task, never a microtask: a producer looping on
|
|
2177
|
+
* `whenPortReady` must let the transport's own events run. `setImmediate`
|
|
2178
|
+
* where it exists; elsewhere a `MessageChannel`, because nested `setTimeout`s
|
|
2179
|
+
* are clamped to 4 ms in browsers.
|
|
2180
|
+
*/
|
|
2181
|
+
const setImmediateFn = globalThis.setImmediate;
|
|
2182
|
+
/** @internal */
|
|
2183
|
+
const nextTask = setImmediateFn ? (fn) => {
|
|
2184
|
+
setImmediateFn(fn);
|
|
2185
|
+
} : (() => {
|
|
2186
|
+
let channel;
|
|
2187
|
+
const queue = [];
|
|
2188
|
+
return (fn) => {
|
|
2189
|
+
if (!channel) {
|
|
2190
|
+
channel = new MessageChannel();
|
|
2191
|
+
channel.port1.onmessage = () => queue.shift()?.();
|
|
2192
|
+
}
|
|
2193
|
+
queue.push(fn);
|
|
2194
|
+
channel.port2.postMessage(0);
|
|
2195
|
+
};
|
|
2196
|
+
})();
|
|
2197
|
+
const states = /* @__PURE__ */ new WeakMap();
|
|
2198
|
+
/** @internal Registered by the multiplexer for each port with a window. */
|
|
2199
|
+
function setPortSendState(port, state) {
|
|
2200
|
+
states.set(port, state);
|
|
2122
2201
|
}
|
|
2123
2202
|
/**
|
|
2124
2203
|
* One virtual port.
|
|
@@ -2135,6 +2214,14 @@ function newVirtualPort(send, requestClose) {
|
|
|
2135
2214
|
if (closing.signal.aborted) return;
|
|
2136
2215
|
closing.abort(reason ?? /* @__PURE__ */ new Error("webrun-rpc: the port was closed"));
|
|
2137
2216
|
};
|
|
2217
|
+
const close = (reason) => {
|
|
2218
|
+
if (closed) return;
|
|
2219
|
+
closed = true;
|
|
2220
|
+
const notify = requestClose;
|
|
2221
|
+
listeners.clear();
|
|
2222
|
+
notify(reason);
|
|
2223
|
+
abort(reason);
|
|
2224
|
+
};
|
|
2138
2225
|
const port = {
|
|
2139
2226
|
addEventListener(_type, listener) {
|
|
2140
2227
|
listeners.add(listener);
|
|
@@ -2146,24 +2233,28 @@ function newVirtualPort(send, requestClose) {
|
|
|
2146
2233
|
if (closed) return;
|
|
2147
2234
|
send(message, transfer);
|
|
2148
2235
|
},
|
|
2149
|
-
close
|
|
2150
|
-
if (closed) return;
|
|
2151
|
-
closed = true;
|
|
2152
|
-
const notify = requestClose;
|
|
2153
|
-
listeners.clear();
|
|
2154
|
-
notify();
|
|
2155
|
-
abort();
|
|
2156
|
-
}
|
|
2236
|
+
close
|
|
2157
2237
|
};
|
|
2158
2238
|
setPortCloseSignal(port, closing.signal);
|
|
2239
|
+
const dispose = Symbol.dispose;
|
|
2240
|
+
if (dispose) Object.defineProperty(port, dispose, {
|
|
2241
|
+
value: () => close(),
|
|
2242
|
+
configurable: true
|
|
2243
|
+
});
|
|
2159
2244
|
return {
|
|
2160
2245
|
port,
|
|
2161
2246
|
deliver(payload) {
|
|
2162
2247
|
if (closed) return;
|
|
2163
2248
|
const event = new MessageEvent("message", { data: payload });
|
|
2249
|
+
let pending;
|
|
2164
2250
|
for (const listener of [...listeners]) try {
|
|
2165
|
-
listener(event);
|
|
2251
|
+
const result = listener(event);
|
|
2252
|
+
if (result && typeof result.then === "function") {
|
|
2253
|
+
pending ??= [];
|
|
2254
|
+
pending.push(result);
|
|
2255
|
+
}
|
|
2166
2256
|
} catch {}
|
|
2257
|
+
return pending ? Promise.allSettled(pending) : void 0;
|
|
2167
2258
|
},
|
|
2168
2259
|
markClosed(reason) {
|
|
2169
2260
|
closed = true;
|
|
@@ -2172,53 +2263,318 @@ function newVirtualPort(send, requestClose) {
|
|
|
2172
2263
|
},
|
|
2173
2264
|
isClosed() {
|
|
2174
2265
|
return closed;
|
|
2266
|
+
},
|
|
2267
|
+
hasListeners() {
|
|
2268
|
+
return listeners.size > 0;
|
|
2175
2269
|
}
|
|
2176
2270
|
};
|
|
2177
2271
|
}
|
|
2178
2272
|
/**
|
|
2179
|
-
* The
|
|
2180
|
-
*
|
|
2273
|
+
* The id of keepalive's `ping` / `pong`: one no port ever takes. An older mux
|
|
2274
|
+
* whose codec passes unknown envelopes through reads them as a close of that
|
|
2275
|
+
* id, which must therefore not be a real port (0 is the initiator's first).
|
|
2181
2276
|
*/
|
|
2182
|
-
const
|
|
2277
|
+
const KEEPALIVE_ID = Number.MAX_SAFE_INTEGER;
|
|
2278
|
+
/** The longest delay a timer holds; past it `setInterval` fires at once. */
|
|
2279
|
+
const MAX_TIMER_MS = 2 ** 31 - 1;
|
|
2183
2280
|
/** Ceiling on concurrently open virtual ports. Bounds the id table only. */
|
|
2184
2281
|
const DEFAULT_MAX_PORTS = 1024;
|
|
2185
2282
|
/**
|
|
2283
|
+
* What a message costs a port's window when the codec cannot say
|
|
2284
|
+
* (`PortCodec.encode`): its bytes and string lengths, found in its values up
|
|
2285
|
+
* to 4 levels deep (at most 256 values), and at least 64. Both ends compute it
|
|
2286
|
+
* from the same structured value, so they agree.
|
|
2287
|
+
*/
|
|
2288
|
+
function estimateMessageSize(payload) {
|
|
2289
|
+
let budget = 256;
|
|
2290
|
+
const walk = (v, depth) => {
|
|
2291
|
+
if (budget-- <= 0) return 0;
|
|
2292
|
+
if (typeof v === "string") return v.length;
|
|
2293
|
+
if (v instanceof ArrayBuffer) return v.byteLength;
|
|
2294
|
+
if (ArrayBuffer.isView(v)) return v.byteLength;
|
|
2295
|
+
if (v === null || typeof v !== "object" || depth >= 4) return 8;
|
|
2296
|
+
let n = 16;
|
|
2297
|
+
if (Array.isArray(v)) for (const x of v) n += walk(x, depth + 1);
|
|
2298
|
+
else for (const k in v) n += k.length + walk(v[k], depth + 1);
|
|
2299
|
+
return n;
|
|
2300
|
+
};
|
|
2301
|
+
return Math.max(64, walk(payload, 0));
|
|
2302
|
+
}
|
|
2303
|
+
/**
|
|
2186
2304
|
* The default `PortMux`: emulates multiplexing over a single port.
|
|
2187
2305
|
*
|
|
2188
2306
|
* A transport that already multiplexes natively supplies its own `PortMux`
|
|
2189
2307
|
* instead — this implementation is for transports that offer one pipe.
|
|
2190
2308
|
*/
|
|
2191
2309
|
function multiplexPort(port, options) {
|
|
2192
|
-
const { codec, onPort, side = "initiator", maxPorts = DEFAULT_MAX_PORTS, maxMessageSize } = options;
|
|
2310
|
+
const { codec, onPort, side = "initiator", maxPorts = DEFAULT_MAX_PORTS, maxMessageSize, signal, window, frameSize, keepalive, onLeak } = options;
|
|
2311
|
+
const maxReassembledBytes = options.maxReassembledBytes ?? 67108864;
|
|
2312
|
+
const misses = keepalive?.misses ?? 3;
|
|
2313
|
+
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`);
|
|
2314
|
+
if (frameSize != null && !(Number.isSafeInteger(frameSize) && frameSize > 0)) throw new RangeError(`webrun-rpc: frameSize must be a positive integer, got ${frameSize}`);
|
|
2315
|
+
if (!(Number.isSafeInteger(maxReassembledBytes) && maxReassembledBytes > 0)) throw new RangeError(`webrun-rpc: maxReassembledBytes must be a positive integer, got ${maxReassembledBytes}`);
|
|
2193
2316
|
const open = /* @__PURE__ */ new Map();
|
|
2317
|
+
let reassembling = 0;
|
|
2318
|
+
const freePartial = (entry) => {
|
|
2319
|
+
if (!entry?.partial) return;
|
|
2320
|
+
reassembling -= entry.partial.charged;
|
|
2321
|
+
entry.partial = void 0;
|
|
2322
|
+
};
|
|
2323
|
+
const leaks = onLeak ? new FinalizationRegistry(({ id, record, release }) => {
|
|
2324
|
+
const entry = open.get(id);
|
|
2325
|
+
if (entry?.handle !== record) return;
|
|
2326
|
+
const { handle: _, credit: __, partial: ___, ...info } = entry;
|
|
2327
|
+
if (!record.hasListeners()) {
|
|
2328
|
+
release("leaked");
|
|
2329
|
+
record.markClosed(/* @__PURE__ */ new Error("webrun-rpc: the port was dropped without close()"));
|
|
2330
|
+
}
|
|
2331
|
+
try {
|
|
2332
|
+
onLeak?.(info);
|
|
2333
|
+
} catch {}
|
|
2334
|
+
}) : void 0;
|
|
2335
|
+
const touch = (id) => {
|
|
2336
|
+
const entry = open.get(id);
|
|
2337
|
+
if (entry) entry.lastActivity = Date.now();
|
|
2338
|
+
};
|
|
2194
2339
|
let nextId = side === "initiator" ? 0 : 1;
|
|
2195
2340
|
let muxClosed = false;
|
|
2341
|
+
/** This end counts encoded bytes and reassembles frames. */
|
|
2342
|
+
const exactCodec = codec.encode != null && codec.decode != null;
|
|
2196
2343
|
const post = (envelope, transfer) => {
|
|
2197
2344
|
if (muxClosed) return;
|
|
2345
|
+
codecPost(envelope, transfer);
|
|
2346
|
+
};
|
|
2347
|
+
/** Like `post`, also while closing: the mux's own close notices. */
|
|
2348
|
+
const codecPost = (envelope, transfer) => {
|
|
2198
2349
|
try {
|
|
2199
2350
|
codec.post(port, envelope, transfer);
|
|
2200
2351
|
} catch {}
|
|
2201
2352
|
};
|
|
2202
|
-
const
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2353
|
+
const outgoing = /* @__PURE__ */ new Map();
|
|
2354
|
+
const ring = [];
|
|
2355
|
+
let pumping = false;
|
|
2356
|
+
const pump = async () => {
|
|
2357
|
+
pumping = true;
|
|
2358
|
+
try {
|
|
2359
|
+
while (ring.length && !muxClosed) {
|
|
2360
|
+
if (port.whenDrained) await port.whenDrained();
|
|
2361
|
+
if (muxClosed) break;
|
|
2362
|
+
const id = ring.shift();
|
|
2363
|
+
const queue = outgoing.get(id);
|
|
2364
|
+
const item = queue?.shift();
|
|
2365
|
+
if (queue?.length) ring.push(id);
|
|
2366
|
+
else outgoing.delete(id);
|
|
2367
|
+
item?.();
|
|
2368
|
+
}
|
|
2369
|
+
} catch (err) {
|
|
2370
|
+
close(err).catch(() => {});
|
|
2371
|
+
} finally {
|
|
2372
|
+
pumping = false;
|
|
2373
|
+
}
|
|
2374
|
+
};
|
|
2375
|
+
const sendOnPort = (id, item) => {
|
|
2376
|
+
if (frameSize == null) {
|
|
2377
|
+
item();
|
|
2378
|
+
return;
|
|
2379
|
+
}
|
|
2380
|
+
let queue = outgoing.get(id);
|
|
2381
|
+
if (!queue) {
|
|
2382
|
+
queue = [];
|
|
2383
|
+
outgoing.set(id, queue);
|
|
2384
|
+
ring.push(id);
|
|
2385
|
+
}
|
|
2386
|
+
queue.push(item);
|
|
2387
|
+
if (!pumping) pump();
|
|
2388
|
+
};
|
|
2389
|
+
const dropOutgoing = (id) => {
|
|
2390
|
+
if (!outgoing.delete(id)) return;
|
|
2391
|
+
const at = ring.indexOf(id);
|
|
2392
|
+
if (at >= 0) ring.splice(at, 1);
|
|
2393
|
+
};
|
|
2394
|
+
const postBytes = (bytes) => {
|
|
2395
|
+
if (muxClosed) return;
|
|
2396
|
+
try {
|
|
2397
|
+
port.postMessage(bytes);
|
|
2398
|
+
} catch {}
|
|
2399
|
+
};
|
|
2400
|
+
const releaseWaiters = (c) => {
|
|
2401
|
+
for (const w of c?.waiters.splice(0) ?? []) w.resolve();
|
|
2402
|
+
};
|
|
2403
|
+
const flush = (c) => {
|
|
2404
|
+
while (c.queue.length) {
|
|
2405
|
+
const next = c.queue[0];
|
|
2406
|
+
if (c.enforced && c.outstanding > 0 && c.outstanding + next.size > c.window) break;
|
|
2407
|
+
c.queue.shift();
|
|
2408
|
+
c.queued -= next.size;
|
|
2409
|
+
c.outstanding += next.size;
|
|
2410
|
+
next.send();
|
|
2411
|
+
}
|
|
2412
|
+
for (const w of [...c.waiters]) if (c.queued <= w.below) {
|
|
2413
|
+
c.waiters.splice(c.waiters.indexOf(w), 1);
|
|
2414
|
+
w.resolve();
|
|
2415
|
+
}
|
|
2416
|
+
};
|
|
2417
|
+
const grant = (id, c, bytes) => {
|
|
2418
|
+
c.unconsumed -= bytes;
|
|
2419
|
+
c.toGrant += bytes;
|
|
2420
|
+
const send = () => {
|
|
2421
|
+
c.grantScheduled = false;
|
|
2422
|
+
if (!c.toGrant || !open.has(id)) return;
|
|
2209
2423
|
post({
|
|
2424
|
+
type: "credit",
|
|
2425
|
+
id,
|
|
2426
|
+
bytes: c.toGrant
|
|
2427
|
+
});
|
|
2428
|
+
c.toGrant = 0;
|
|
2429
|
+
};
|
|
2430
|
+
if (c.toGrant >= c.window / 4) {
|
|
2431
|
+
send();
|
|
2432
|
+
return;
|
|
2433
|
+
}
|
|
2434
|
+
if (c.grantScheduled) return;
|
|
2435
|
+
c.grantScheduled = true;
|
|
2436
|
+
setTimeout(send, 0);
|
|
2437
|
+
};
|
|
2438
|
+
const attach = (id, meta, portWindow, opener, exact) => {
|
|
2439
|
+
const c = typeof portWindow === "number" && Number.isSafeInteger(portWindow) && portWindow > 0 ? {
|
|
2440
|
+
window: portWindow,
|
|
2441
|
+
enforced: false,
|
|
2442
|
+
opener,
|
|
2443
|
+
exact,
|
|
2444
|
+
outstanding: 0,
|
|
2445
|
+
queue: [],
|
|
2446
|
+
queued: 0,
|
|
2447
|
+
unconsumed: 0,
|
|
2448
|
+
waiters: [],
|
|
2449
|
+
toGrant: 0,
|
|
2450
|
+
grantScheduled: false
|
|
2451
|
+
} : void 0;
|
|
2452
|
+
const self = newVirtualPort((payload, transfer) => {
|
|
2453
|
+
touch(id);
|
|
2454
|
+
const envelope = {
|
|
2455
|
+
type: "message",
|
|
2456
|
+
id,
|
|
2457
|
+
payload
|
|
2458
|
+
};
|
|
2459
|
+
if (!c) {
|
|
2460
|
+
post(envelope, transfer);
|
|
2461
|
+
return;
|
|
2462
|
+
}
|
|
2463
|
+
const bytes = c.exact ? codec.encode?.(envelope) : void 0;
|
|
2464
|
+
const size = bytes?.byteLength ?? estimateMessageSize(payload);
|
|
2465
|
+
const waits = frameSize != null || c.queue.length > 0 || c.enforced && c.outstanding > 0 && c.outstanding + size > c.window;
|
|
2466
|
+
const held = !bytes && waits ? {
|
|
2467
|
+
...envelope,
|
|
2468
|
+
payload: structuredClone(payload, { transfer })
|
|
2469
|
+
} : void 0;
|
|
2470
|
+
const send = (whole = false) => {
|
|
2471
|
+
if (!bytes) {
|
|
2472
|
+
sendOnPort(id, held ? () => post(held) : () => post(envelope, transfer));
|
|
2473
|
+
return;
|
|
2474
|
+
}
|
|
2475
|
+
if (!whole && frameSize != null && c.enforced && c.exact && bytes.byteLength > frameSize) {
|
|
2476
|
+
for (let at = 0; at < bytes.byteLength; at += frameSize) {
|
|
2477
|
+
const data = bytes.subarray(at, at + frameSize);
|
|
2478
|
+
const frame = at + frameSize >= bytes.byteLength ? {
|
|
2479
|
+
type: "frame",
|
|
2480
|
+
id,
|
|
2481
|
+
data,
|
|
2482
|
+
last: true
|
|
2483
|
+
} : {
|
|
2484
|
+
type: "frame",
|
|
2485
|
+
id,
|
|
2486
|
+
data
|
|
2487
|
+
};
|
|
2488
|
+
sendOnPort(id, () => post(frame));
|
|
2489
|
+
}
|
|
2490
|
+
return;
|
|
2491
|
+
}
|
|
2492
|
+
sendOnPort(id, () => postBytes(bytes));
|
|
2493
|
+
};
|
|
2494
|
+
c.queue.push({
|
|
2495
|
+
send,
|
|
2496
|
+
size
|
|
2497
|
+
});
|
|
2498
|
+
c.queued += size;
|
|
2499
|
+
flush(c);
|
|
2500
|
+
}, (reason) => {
|
|
2501
|
+
release(reason);
|
|
2502
|
+
record.markClosed(reason);
|
|
2503
|
+
});
|
|
2504
|
+
function release(reason) {
|
|
2505
|
+
freePartial(open.get(id));
|
|
2506
|
+
open.delete(id);
|
|
2507
|
+
leaks?.unregister(record);
|
|
2508
|
+
if (c?.queue.length) {
|
|
2509
|
+
for (const item of c.queue.splice(0)) item.send(true);
|
|
2510
|
+
c.queued = 0;
|
|
2511
|
+
}
|
|
2512
|
+
sendOnPort(id, () => post({
|
|
2210
2513
|
type: "close",
|
|
2211
2514
|
id,
|
|
2212
2515
|
reason
|
|
2516
|
+
}));
|
|
2517
|
+
releaseWaiters(c);
|
|
2518
|
+
}
|
|
2519
|
+
const { markClosed, isClosed, hasListeners } = self;
|
|
2520
|
+
if (c) {
|
|
2521
|
+
const deliver = self.deliver;
|
|
2522
|
+
self.deliver = (payload, size = estimateMessageSize(payload)) => {
|
|
2523
|
+
c.unconsumed += size;
|
|
2524
|
+
const done = deliver(payload);
|
|
2525
|
+
if (done) done.then(() => grant(id, c, size));
|
|
2526
|
+
else grant(id, c, size);
|
|
2527
|
+
return done;
|
|
2528
|
+
};
|
|
2529
|
+
setPortSendState(self.port, {
|
|
2530
|
+
bufferedAmount: () => c.queued,
|
|
2531
|
+
ready: (below = 0) => new Promise((resolve) => {
|
|
2532
|
+
if (c.queued <= below || isClosed()) nextTask(resolve);
|
|
2533
|
+
else c.waiters.push({
|
|
2534
|
+
below,
|
|
2535
|
+
resolve: () => nextTask(resolve)
|
|
2536
|
+
});
|
|
2537
|
+
})
|
|
2213
2538
|
});
|
|
2214
|
-
|
|
2215
|
-
|
|
2539
|
+
}
|
|
2540
|
+
const record = {
|
|
2541
|
+
deliver: self.deliver,
|
|
2542
|
+
markClosed,
|
|
2543
|
+
isClosed,
|
|
2544
|
+
hasListeners,
|
|
2545
|
+
close: (reason) => {
|
|
2546
|
+
if (isClosed()) return;
|
|
2547
|
+
release(reason);
|
|
2548
|
+
markClosed(reason);
|
|
2549
|
+
}
|
|
2550
|
+
};
|
|
2551
|
+
leaks?.register(self.port, {
|
|
2552
|
+
id,
|
|
2553
|
+
record,
|
|
2554
|
+
release
|
|
2555
|
+
}, record);
|
|
2556
|
+
const now = Date.now();
|
|
2557
|
+
open.set(id, {
|
|
2558
|
+
id,
|
|
2559
|
+
meta,
|
|
2560
|
+
openedAt: now,
|
|
2561
|
+
lastActivity: now,
|
|
2562
|
+
handle: record,
|
|
2563
|
+
credit: c
|
|
2216
2564
|
});
|
|
2217
|
-
open.set(id, self);
|
|
2218
2565
|
return self;
|
|
2219
2566
|
};
|
|
2220
|
-
const handleEnvelope = (envelope) => {
|
|
2567
|
+
const handleEnvelope = (envelope, event) => {
|
|
2221
2568
|
if (muxClosed) return;
|
|
2569
|
+
heardSincePing = true;
|
|
2570
|
+
if (envelope.type === "ping" || envelope.type === "pong") {
|
|
2571
|
+
peerKeepsAlive = true;
|
|
2572
|
+
if (envelope.type === "ping") post({
|
|
2573
|
+
type: "pong",
|
|
2574
|
+
id: KEEPALIVE_ID
|
|
2575
|
+
});
|
|
2576
|
+
return;
|
|
2577
|
+
}
|
|
2222
2578
|
if (envelope.type === "open") {
|
|
2223
2579
|
if (open.has(envelope.id)) return;
|
|
2224
2580
|
if (open.size >= maxPorts) {
|
|
@@ -2229,7 +2585,18 @@ function multiplexPort(port, options) {
|
|
|
2229
2585
|
});
|
|
2230
2586
|
return;
|
|
2231
2587
|
}
|
|
2232
|
-
const handle = attach(envelope.id);
|
|
2588
|
+
const handle = attach(envelope.id, envelope.meta, envelope.window, false, envelope.exact === true && exactCodec);
|
|
2589
|
+
const credit = open.get(envelope.id)?.credit;
|
|
2590
|
+
if (credit) post(credit.exact ? {
|
|
2591
|
+
type: "credit",
|
|
2592
|
+
id: envelope.id,
|
|
2593
|
+
bytes: 0,
|
|
2594
|
+
exact: true
|
|
2595
|
+
} : {
|
|
2596
|
+
type: "credit",
|
|
2597
|
+
id: envelope.id,
|
|
2598
|
+
bytes: 0
|
|
2599
|
+
});
|
|
2233
2600
|
let accepted = false;
|
|
2234
2601
|
if (onPort) try {
|
|
2235
2602
|
accepted = onPort(handle.port, envelope.meta) !== false;
|
|
@@ -2244,57 +2611,215 @@ function multiplexPort(port, options) {
|
|
|
2244
2611
|
id: envelope.id,
|
|
2245
2612
|
reason: "rejected"
|
|
2246
2613
|
});
|
|
2614
|
+
return;
|
|
2615
|
+
}
|
|
2616
|
+
return;
|
|
2617
|
+
}
|
|
2618
|
+
const entry = open.get(envelope.id);
|
|
2619
|
+
if (envelope.type === "credit") {
|
|
2620
|
+
const c = entry?.credit;
|
|
2621
|
+
if (!c || !(envelope.bytes >= 0)) return;
|
|
2622
|
+
if (!c.enforced && c.opener) {
|
|
2623
|
+
if (c.exact && envelope.exact !== true) {
|
|
2624
|
+
c.exact = false;
|
|
2625
|
+
c.outstanding = 0;
|
|
2626
|
+
}
|
|
2627
|
+
post({
|
|
2628
|
+
type: "credit",
|
|
2629
|
+
id: envelope.id,
|
|
2630
|
+
bytes: 0
|
|
2631
|
+
});
|
|
2247
2632
|
}
|
|
2633
|
+
c.enforced = true;
|
|
2634
|
+
c.outstanding = Math.max(0, c.outstanding - envelope.bytes);
|
|
2635
|
+
flush(c);
|
|
2248
2636
|
return;
|
|
2249
2637
|
}
|
|
2250
|
-
const handle = open.get(envelope.id);
|
|
2251
2638
|
if (envelope.type === "message") {
|
|
2252
|
-
|
|
2253
|
-
|
|
2639
|
+
const sizeOf = (c) => (c.exact ? byteLengthOf(event.data) : void 0) ?? estimateMessageSize(envelope.payload);
|
|
2640
|
+
if (!entry) return;
|
|
2641
|
+
touch(envelope.id);
|
|
2642
|
+
entry.handle.deliver(envelope.payload, entry.credit ? sizeOf(entry.credit) : void 0);
|
|
2643
|
+
return;
|
|
2644
|
+
}
|
|
2645
|
+
if (envelope.type === "frame") {
|
|
2646
|
+
if (!entry?.credit?.exact || !codec.decode || !(envelope.data instanceof Uint8Array)) return;
|
|
2647
|
+
touch(envelope.id);
|
|
2648
|
+
const c = entry.credit;
|
|
2649
|
+
const owed = c.unconsumed + c.toGrant;
|
|
2650
|
+
const partial = entry.partial ?? {
|
|
2651
|
+
chunks: [],
|
|
2652
|
+
bytes: 0,
|
|
2653
|
+
owed,
|
|
2654
|
+
charged: 0,
|
|
2655
|
+
...owed > 0 ? { room: c.window - owed } : {}
|
|
2656
|
+
};
|
|
2657
|
+
entry.partial = partial;
|
|
2658
|
+
const size = envelope.data.byteLength;
|
|
2659
|
+
const charged = partial.room == null ? Math.max(0, partial.bytes + size - c.window) : 0;
|
|
2660
|
+
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;
|
|
2661
|
+
if (overrun) {
|
|
2662
|
+
freePartial(entry);
|
|
2663
|
+
dropOutgoing(envelope.id);
|
|
2664
|
+
open.delete(envelope.id);
|
|
2665
|
+
releaseWaiters(c);
|
|
2666
|
+
entry.handle.markClosed(/* @__PURE__ */ new RangeError(`webrun-rpc: a framed message passed ${overrun}`));
|
|
2667
|
+
post({
|
|
2668
|
+
type: "close",
|
|
2669
|
+
id: envelope.id,
|
|
2670
|
+
reason: "max-reassembled-bytes"
|
|
2671
|
+
});
|
|
2672
|
+
return;
|
|
2673
|
+
}
|
|
2674
|
+
partial.bytes += size;
|
|
2675
|
+
reassembling += charged - partial.charged;
|
|
2676
|
+
partial.charged = charged;
|
|
2677
|
+
partial.chunks.push(envelope.data);
|
|
2678
|
+
if (!envelope.last) return;
|
|
2679
|
+
freePartial(entry);
|
|
2680
|
+
const whole = new Uint8Array(partial.bytes);
|
|
2681
|
+
let at = 0;
|
|
2682
|
+
for (const chunk of partial.chunks) {
|
|
2683
|
+
whole.set(chunk, at);
|
|
2684
|
+
at += chunk.byteLength;
|
|
2685
|
+
}
|
|
2686
|
+
const inner = codec.decode(whole);
|
|
2687
|
+
if (inner?.type !== "message" || inner.id !== envelope.id) return;
|
|
2688
|
+
entry.handle.deliver(inner.payload, whole.byteLength);
|
|
2254
2689
|
return;
|
|
2255
2690
|
}
|
|
2256
|
-
|
|
2691
|
+
dropOutgoing(envelope.id);
|
|
2692
|
+
if (!entry) return;
|
|
2693
|
+
freePartial(entry);
|
|
2257
2694
|
open.delete(envelope.id);
|
|
2258
|
-
|
|
2695
|
+
releaseWaiters(entry.credit);
|
|
2696
|
+
entry.handle.markClosed(/* @__PURE__ */ new Error("webrun-rpc: the peer closed the port"));
|
|
2259
2697
|
};
|
|
2260
2698
|
const listener = (event) => {
|
|
2261
2699
|
const envelope = codec.read(event);
|
|
2262
|
-
if (envelope) handleEnvelope(envelope);
|
|
2700
|
+
if (envelope) handleEnvelope(envelope, event);
|
|
2701
|
+
};
|
|
2702
|
+
let peerKeepsAlive = false;
|
|
2703
|
+
let heardSincePing = true;
|
|
2704
|
+
let unanswered = 0;
|
|
2705
|
+
let pinger;
|
|
2706
|
+
const tick = () => {
|
|
2707
|
+
if (!keepalive) return;
|
|
2708
|
+
const backlogged = (port.bufferedAmount ?? 0) > 0;
|
|
2709
|
+
if (heardSincePing) unanswered = 0;
|
|
2710
|
+
else if (peerKeepsAlive && !backlogged) unanswered++;
|
|
2711
|
+
if (unanswered >= misses) {
|
|
2712
|
+
close(/* @__PURE__ */ new Error(`webrun-rpc: the peer did not answer ${misses} keepalive pings, ${keepalive.intervalMs} ms apart`)).catch(() => {});
|
|
2713
|
+
return;
|
|
2714
|
+
}
|
|
2715
|
+
heardSincePing = false;
|
|
2716
|
+
post({
|
|
2717
|
+
type: "ping",
|
|
2718
|
+
id: KEEPALIVE_ID
|
|
2719
|
+
});
|
|
2720
|
+
};
|
|
2721
|
+
const close = async (reason) => {
|
|
2722
|
+
if (muxClosed) return;
|
|
2723
|
+
clearInterval(pinger);
|
|
2724
|
+
muxClosed = true;
|
|
2725
|
+
signal?.removeEventListener("abort", onTransportAbort);
|
|
2726
|
+
port.removeEventListener("message", listener);
|
|
2727
|
+
const closedNow = /* @__PURE__ */ new Set();
|
|
2728
|
+
for (const [id, { handle, credit }] of [...open]) {
|
|
2729
|
+
closedNow.add(id);
|
|
2730
|
+
codecPost({
|
|
2731
|
+
type: "close",
|
|
2732
|
+
id
|
|
2733
|
+
});
|
|
2734
|
+
open.delete(id);
|
|
2735
|
+
releaseWaiters(credit);
|
|
2736
|
+
handle.markClosed(reason ?? /* @__PURE__ */ new Error("webrun-rpc: the multiplexer was closed"));
|
|
2737
|
+
}
|
|
2738
|
+
for (const id of outgoing.keys()) if (!closedNow.has(id)) codecPost({
|
|
2739
|
+
type: "close",
|
|
2740
|
+
id
|
|
2741
|
+
});
|
|
2742
|
+
outgoing.clear();
|
|
2743
|
+
ring.length = 0;
|
|
2744
|
+
await port.close?.();
|
|
2745
|
+
};
|
|
2746
|
+
const onTransportAbort = () => {
|
|
2747
|
+
close(signal?.reason).catch(() => {});
|
|
2263
2748
|
};
|
|
2264
2749
|
port.addEventListener("message", listener);
|
|
2265
2750
|
port.start?.();
|
|
2751
|
+
if (keepalive) {
|
|
2752
|
+
pinger = setInterval(tick, keepalive.intervalMs);
|
|
2753
|
+
pinger.unref?.();
|
|
2754
|
+
}
|
|
2755
|
+
if (signal?.aborted) close(signal.reason).catch(() => {});
|
|
2756
|
+
else signal?.addEventListener("abort", onTransportAbort, { once: true });
|
|
2266
2757
|
return {
|
|
2267
2758
|
maxMessageSize,
|
|
2268
|
-
async openPort(meta) {
|
|
2759
|
+
async openPort(meta, { signal: portSignal } = {}) {
|
|
2269
2760
|
if (muxClosed) throw new Error("webrun-rpc: the multiplexer is closed");
|
|
2270
|
-
if (
|
|
2761
|
+
if (portSignal?.aborted) throw portSignal.reason;
|
|
2762
|
+
if (open.size >= maxPorts) throw new RangeError(`webrun-rpc: maxPorts (${maxPorts}) reached; open ports by kind: ${describeKinds(open.values())}`);
|
|
2271
2763
|
while (open.has(nextId)) nextId += 2;
|
|
2272
2764
|
const id = nextId;
|
|
2273
2765
|
nextId += 2;
|
|
2274
|
-
const handle = attach(id);
|
|
2275
|
-
post({
|
|
2766
|
+
const handle = attach(id, meta, window, true, exactCodec);
|
|
2767
|
+
post(window ? exactCodec ? {
|
|
2768
|
+
type: "open",
|
|
2769
|
+
id,
|
|
2770
|
+
meta,
|
|
2771
|
+
window,
|
|
2772
|
+
exact: true
|
|
2773
|
+
} : {
|
|
2774
|
+
type: "open",
|
|
2775
|
+
id,
|
|
2776
|
+
meta,
|
|
2777
|
+
window
|
|
2778
|
+
} : {
|
|
2276
2779
|
type: "open",
|
|
2277
2780
|
id,
|
|
2278
2781
|
meta
|
|
2279
2782
|
});
|
|
2783
|
+
if (portSignal) {
|
|
2784
|
+
const record = open.get(id)?.handle;
|
|
2785
|
+
const onAbort = () => {
|
|
2786
|
+
if (record && open.get(id)?.handle === record) record.close(portSignal.reason);
|
|
2787
|
+
};
|
|
2788
|
+
portSignal.addEventListener("abort", onAbort, { once: true });
|
|
2789
|
+
getPortCloseSignal(handle.port)?.addEventListener("abort", () => portSignal.removeEventListener("abort", onAbort), { once: true });
|
|
2790
|
+
}
|
|
2280
2791
|
return handle.port;
|
|
2281
2792
|
},
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
}
|
|
2292
|
-
muxClosed = true;
|
|
2293
|
-
port.removeEventListener("message", listener);
|
|
2294
|
-
await port.close?.();
|
|
2793
|
+
close,
|
|
2794
|
+
ports() {
|
|
2795
|
+
return [...open.values()].map(({ handle: _, credit, ...info }) => credit ? {
|
|
2796
|
+
...info,
|
|
2797
|
+
...credit.enforced ? { window: credit.window } : {},
|
|
2798
|
+
queued: credit.queued,
|
|
2799
|
+
outstanding: credit.outstanding,
|
|
2800
|
+
unconsumed: credit.unconsumed
|
|
2801
|
+
} : info);
|
|
2295
2802
|
}
|
|
2296
2803
|
};
|
|
2297
2804
|
}
|
|
2805
|
+
/** The byte length of a received message's data, or `undefined` if it is not bytes. */
|
|
2806
|
+
function byteLengthOf(data) {
|
|
2807
|
+
if (data instanceof ArrayBuffer || ArrayBuffer.isView(data)) return data.byteLength;
|
|
2808
|
+
}
|
|
2809
|
+
/**
|
|
2810
|
+
* `"http: 1000, ws: 24"`: the three commonest `meta.kind`s among `entries`, so
|
|
2811
|
+
* a full table names who filled it. A port without a kind counts as `"?"`.
|
|
2812
|
+
*/
|
|
2813
|
+
function describeKinds(entries) {
|
|
2814
|
+
const counts = /* @__PURE__ */ new Map();
|
|
2815
|
+
for (const { meta } of entries) {
|
|
2816
|
+
const kind = meta?.kind;
|
|
2817
|
+
const name = typeof kind === "string" ? kind : "?";
|
|
2818
|
+
counts.set(name, (counts.get(name) ?? 0) + 1);
|
|
2819
|
+
}
|
|
2820
|
+
if (!counts.size) return "none";
|
|
2821
|
+
return [...counts].sort((x, y) => y[1] - x[1]).slice(0, 3).map(([name, n]) => `${name}: ${n}`).join(", ");
|
|
2822
|
+
}
|
|
2298
2823
|
/**
|
|
2299
2824
|
* Announced with every port this pair opens. Layer 1 never reads `meta` — it is
|
|
2300
2825
|
* here so a peer running something else over the same multiplexer can tell a
|
|
@@ -2356,7 +2881,7 @@ function isEnvelope(value) {
|
|
|
2356
2881
|
const candidate = value;
|
|
2357
2882
|
if (typeof candidate.id !== "number") return false;
|
|
2358
2883
|
if (!Number.isInteger(candidate.id) || candidate.id < 0) return false;
|
|
2359
|
-
return candidate.type === "open" || candidate.type === "message" || candidate.type === "close";
|
|
2884
|
+
return candidate.type === "open" || candidate.type === "message" || candidate.type === "close" || candidate.type === "credit" || candidate.type === "frame" || candidate.type === "ping" || candidate.type === "pong";
|
|
2360
2885
|
}
|
|
2361
2886
|
/**
|
|
2362
2887
|
* For ports whose messages are structured values — a real `MessagePort`, a
|
|
@@ -2494,7 +3019,7 @@ function trackedLink(send, release) {
|
|
|
2494
3019
|
finish();
|
|
2495
3020
|
return response;
|
|
2496
3021
|
}
|
|
2497
|
-
return new Response(guardBody(response.body, lost, () => lostReason, finish), {
|
|
3022
|
+
return new Response(guardBody(response.body, lost, () => lostReason, finish, request.signal), {
|
|
2498
3023
|
status: response.status,
|
|
2499
3024
|
statusText: response.statusText,
|
|
2500
3025
|
headers: response.headers
|
|
@@ -2513,33 +3038,65 @@ function trackedLink(send, release) {
|
|
|
2513
3038
|
return asyncDisposable(link, () => link.close());
|
|
2514
3039
|
}
|
|
2515
3040
|
/**
|
|
2516
|
-
* A response body that reports its end (done, cancelled or
|
|
2517
|
-
* with the link's reason when the link is lost mid-stream.
|
|
3041
|
+
* A response body that reports its end (done, cancelled, failed or aborted) and
|
|
3042
|
+
* fails with the link's reason when the link is lost mid-stream. An abort of the
|
|
3043
|
+
* request's signal ends it at once, read or not: the exchange is over (the layer
|
|
3044
|
+
* below has given its port back), so the request leaves `inflight` then.
|
|
2518
3045
|
*/
|
|
2519
|
-
function guardBody(body, lost, reason, finish) {
|
|
3046
|
+
function guardBody(body, lost, reason, finish, signal) {
|
|
2520
3047
|
const reader = body.getReader();
|
|
3048
|
+
let controller;
|
|
3049
|
+
let ended = false;
|
|
3050
|
+
const end = () => {
|
|
3051
|
+
ended = true;
|
|
3052
|
+
signal?.removeEventListener("abort", onAbort);
|
|
3053
|
+
finish();
|
|
3054
|
+
};
|
|
3055
|
+
const onAbort = () => {
|
|
3056
|
+
if (ended) return;
|
|
3057
|
+
end();
|
|
3058
|
+
reader.cancel().catch(() => {});
|
|
3059
|
+
try {
|
|
3060
|
+
controller.error(abortError(signal));
|
|
3061
|
+
} catch {}
|
|
3062
|
+
};
|
|
2521
3063
|
return new ReadableStream({
|
|
3064
|
+
start(c) {
|
|
3065
|
+
controller = c;
|
|
3066
|
+
if (signal?.aborted) onAbort();
|
|
3067
|
+
else signal?.addEventListener("abort", onAbort, { once: true });
|
|
3068
|
+
},
|
|
2522
3069
|
async pull(controller) {
|
|
2523
3070
|
try {
|
|
2524
3071
|
const { done, value } = await Promise.race([reader.read(), lost]);
|
|
3072
|
+
if (ended) return;
|
|
2525
3073
|
if (done) {
|
|
2526
|
-
|
|
3074
|
+
end();
|
|
2527
3075
|
controller.close();
|
|
2528
3076
|
return;
|
|
2529
3077
|
}
|
|
2530
3078
|
controller.enqueue(value);
|
|
2531
3079
|
} catch (error) {
|
|
2532
|
-
|
|
3080
|
+
if (ended) return;
|
|
3081
|
+
end();
|
|
2533
3082
|
reader.cancel().catch(() => {});
|
|
2534
3083
|
controller.error(reason() ?? error);
|
|
2535
3084
|
}
|
|
2536
3085
|
},
|
|
2537
3086
|
async cancel(why) {
|
|
2538
|
-
|
|
3087
|
+
end();
|
|
2539
3088
|
await reader.cancel(why).catch(() => {});
|
|
2540
3089
|
}
|
|
2541
3090
|
});
|
|
2542
3091
|
}
|
|
3092
|
+
/** The signal's reason as an `Error` (`AbortError` when it gave none). */
|
|
3093
|
+
function abortError(signal) {
|
|
3094
|
+
const why = signal.reason;
|
|
3095
|
+
if (why instanceof Error) return why;
|
|
3096
|
+
const err = new Error(why === void 0 ? "Aborted" : String(why));
|
|
3097
|
+
err.name = "AbortError";
|
|
3098
|
+
return err;
|
|
3099
|
+
}
|
|
2543
3100
|
//#endregion
|
|
2544
3101
|
//#region src/sw/protocol.ts
|
|
2545
3102
|
/** Page → ServiceWorker: open a WebSocket to a handler. */
|