@statewalker/webrun-http-browser 0.9.0 → 0.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +155 -10
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/service-worker-control.d.ts +2 -1
- package/dist/core/service-worker-control.d.ts.map +1 -1
- package/dist/core/sw-scope.d.ts +31 -0
- package/dist/core/sw-scope.d.ts.map +1 -0
- package/dist/index.js +908 -221
- package/dist/link/http-link.d.ts +16 -3
- package/dist/link/http-link.d.ts.map +1 -1
- package/dist/relay/index-sw.d.ts +8 -2
- package/dist/relay/index-sw.d.ts.map +1 -1
- package/dist/relay/index.d.ts +1 -1
- package/dist/relay/index.d.ts.map +1 -1
- package/dist/relay-sw.js +470 -81
- package/dist/relay-worker.d.ts +1 -0
- package/dist/relay-worker.d.ts.map +1 -1
- package/dist/relay-worker.js +470 -81
- package/dist/sw/handler-endpoint.d.ts +5 -2
- package/dist/sw/handler-endpoint.d.ts.map +1 -1
- package/dist/sw/http-sw-dispatcher.d.ts +114 -7
- package/dist/sw/http-sw-dispatcher.d.ts.map +1 -1
- package/dist/sw/index.d.ts +1 -0
- package/dist/sw/index.d.ts.map +1 -1
- package/dist/sw/protocol.d.ts +3 -0
- package/dist/sw/protocol.d.ts.map +1 -1
- package/dist/sw/sw-dispatcher.d.ts +5 -2
- package/dist/sw/sw-dispatcher.d.ts.map +1 -1
- package/dist/sw/sw-http-adapter.d.ts +19 -4
- package/dist/sw/sw-http-adapter.d.ts.map +1 -1
- package/dist/sw/worker-service.d.ts +5 -2
- package/dist/sw/worker-service.d.ts.map +1 -1
- package/dist/sw-worker.js +681 -141
- package/dist/sw.js +896 -218
- package/dist/worker.js +689 -115
- package/dist/ws/server.d.ts +50 -2
- package/dist/ws/server.d.ts.map +1 -1
- package/dist/ws/socket-core.d.ts +5 -2
- package/dist/ws/socket-core.d.ts.map +1 -1
- package/dist/ws/upgrade.d.ts +12 -1
- package/dist/ws/upgrade.d.ts.map +1 -1
- package/package.json +4 -3
- package/src/core/index.ts +1 -0
- package/src/core/service-worker-control.ts +5 -3
- package/src/core/sw-scope.ts +27 -0
- package/src/link/http-link.ts +20 -4
- package/src/relay/index-sw.ts +15 -2
- package/src/relay/index.ts +2 -2
- package/src/relay-worker.ts +1 -0
- package/src/sw/handler-endpoint.ts +17 -4
- package/src/sw/http-sw-dispatcher.ts +267 -67
- package/src/sw/index.ts +5 -0
- package/src/sw/protocol.ts +11 -1
- package/src/sw/sw-dispatcher.ts +17 -11
- package/src/sw/sw-http-adapter.ts +36 -10
- package/src/sw/worker-service.ts +11 -4
- package/src/ws/server.ts +166 -11
- package/src/ws/socket-core.ts +7 -2
- package/src/ws/upgrade.ts +22 -4
package/dist/sw-worker.js
CHANGED
|
@@ -428,8 +428,9 @@
|
|
|
428
428
|
* Returns a function that stops answering.
|
|
429
429
|
*/
|
|
430
430
|
function handleClaimRequests(self) {
|
|
431
|
-
|
|
432
|
-
|
|
431
|
+
const sw = self;
|
|
432
|
+
return handleChannelCalls(sw, CLAIM_CALL, (event) => {
|
|
433
|
+
const claimed = sw.clients.claim().then(() => true);
|
|
433
434
|
event.waitUntil?.(claimed);
|
|
434
435
|
return claimed;
|
|
435
436
|
});
|
|
@@ -1394,7 +1395,11 @@
|
|
|
1394
1395
|
*/
|
|
1395
1396
|
function httpServe(handler, options = {}) {
|
|
1396
1397
|
const codec = options.codec ?? defaultCodec;
|
|
1397
|
-
return
|
|
1398
|
+
return (input) => {
|
|
1399
|
+
const abandoned = new AbortController();
|
|
1400
|
+
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")));
|
|
1401
|
+
};
|
|
1402
|
+
async function* exchange(input, context) {
|
|
1398
1403
|
let decoded;
|
|
1399
1404
|
try {
|
|
1400
1405
|
decoded = await codec.decodeRequest(input);
|
|
@@ -1406,7 +1411,7 @@
|
|
|
1406
1411
|
const replyCodec = decoded.codec ?? codec;
|
|
1407
1412
|
let result;
|
|
1408
1413
|
try {
|
|
1409
|
-
result = await handler(decoded.envelope, decoded.body);
|
|
1414
|
+
result = await handler(decoded.envelope, decoded.body, context);
|
|
1410
1415
|
} catch (error) {
|
|
1411
1416
|
yield* encodeErrorResponse(replyCodec, error, decoded.envelope.method);
|
|
1412
1417
|
return;
|
|
@@ -1421,7 +1426,36 @@
|
|
|
1421
1426
|
await result.release?.();
|
|
1422
1427
|
} catch {}
|
|
1423
1428
|
}
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
/**
|
|
1432
|
+
* `gen`, except that `return()` and `throw()` from its consumer call `abandon`
|
|
1433
|
+
* at once — before they are queued behind a `next()` still parked inside the
|
|
1434
|
+
* generator (the handler's `await`) — unless the generator already finished.
|
|
1435
|
+
*/
|
|
1436
|
+
function abortingOnReturn(gen, abandon) {
|
|
1437
|
+
let finished = false;
|
|
1438
|
+
const settle = (r) => {
|
|
1439
|
+
if (r.done) finished = true;
|
|
1440
|
+
return r;
|
|
1441
|
+
};
|
|
1442
|
+
const wrapped = {
|
|
1443
|
+
next: (...args) => gen.next(...args).then(settle),
|
|
1444
|
+
return(value) {
|
|
1445
|
+
if (!finished) abandon();
|
|
1446
|
+
finished = true;
|
|
1447
|
+
return gen.return(value);
|
|
1448
|
+
},
|
|
1449
|
+
throw(error) {
|
|
1450
|
+
if (!finished) abandon(error);
|
|
1451
|
+
finished = true;
|
|
1452
|
+
return gen.throw(error);
|
|
1453
|
+
},
|
|
1454
|
+
[Symbol.asyncIterator]() {
|
|
1455
|
+
return wrapped;
|
|
1456
|
+
}
|
|
1424
1457
|
};
|
|
1458
|
+
return asyncDisposable$2(wrapped, () => wrapped.return(void 0));
|
|
1425
1459
|
}
|
|
1426
1460
|
/**
|
|
1427
1461
|
* The one place that knows a runtime may not implement request body streams,
|
|
@@ -1660,10 +1694,11 @@
|
|
|
1660
1694
|
* registered with any `webrun-streams-*` adapter's `serve`.
|
|
1661
1695
|
*/
|
|
1662
1696
|
function serveFetchOverDuplex(handler, options = {}) {
|
|
1663
|
-
return httpServe(async (env, body) => {
|
|
1697
|
+
return httpServe(async (env, body, exchange) => {
|
|
1664
1698
|
const reqInit = {
|
|
1665
1699
|
method: env.method,
|
|
1666
|
-
headers: forwardableHeaders(env.headers)
|
|
1700
|
+
headers: forwardableHeaders(env.headers),
|
|
1701
|
+
signal: exchange?.signal
|
|
1667
1702
|
};
|
|
1668
1703
|
let upload;
|
|
1669
1704
|
if (env.method !== "GET" && env.method !== "HEAD") {
|
|
@@ -2217,6 +2252,99 @@
|
|
|
2217
2252
|
}
|
|
2218
2253
|
}
|
|
2219
2254
|
/**
|
|
2255
|
+
* Runs `fn` on a later task, never a microtask: a producer looping on
|
|
2256
|
+
* `whenPortReady` must let the transport's own events run. `setImmediate`
|
|
2257
|
+
* where it exists; elsewhere a `MessageChannel`, because nested `setTimeout`s
|
|
2258
|
+
* are clamped to 4 ms in browsers.
|
|
2259
|
+
*/
|
|
2260
|
+
const setImmediateFn = globalThis.setImmediate;
|
|
2261
|
+
/** @internal */
|
|
2262
|
+
const nextTask = setImmediateFn ? (fn) => {
|
|
2263
|
+
setImmediateFn(fn);
|
|
2264
|
+
} : (() => {
|
|
2265
|
+
let channel;
|
|
2266
|
+
const queue = [];
|
|
2267
|
+
return (fn) => {
|
|
2268
|
+
if (!channel) {
|
|
2269
|
+
channel = new MessageChannel();
|
|
2270
|
+
channel.port1.onmessage = () => queue.shift()?.();
|
|
2271
|
+
}
|
|
2272
|
+
queue.push(fn);
|
|
2273
|
+
channel.port2.postMessage(0);
|
|
2274
|
+
};
|
|
2275
|
+
})();
|
|
2276
|
+
const states = /* @__PURE__ */ new WeakMap();
|
|
2277
|
+
/** @internal Registered by the multiplexer for each port with a window. */
|
|
2278
|
+
function setPortSendState(port, state) {
|
|
2279
|
+
states.set(port, state);
|
|
2280
|
+
}
|
|
2281
|
+
/**
|
|
2282
|
+
* Bytes queued on `port` because its window is spent (0 for a port without
|
|
2283
|
+
* flow control). A sender that should not queue without bound waits for
|
|
2284
|
+
* `whenPortReady` before posting more.
|
|
2285
|
+
*/
|
|
2286
|
+
function portBufferedAmount(port) {
|
|
2287
|
+
return states.get(port)?.bufferedAmount() ?? 0;
|
|
2288
|
+
}
|
|
2289
|
+
/**
|
|
2290
|
+
* Resolves once at most `below` bytes are queued on `port`, always on a later
|
|
2291
|
+
* task (for a port without flow control, on the next one).
|
|
2292
|
+
*/
|
|
2293
|
+
function whenPortReady(port, below = 0) {
|
|
2294
|
+
return states.get(port)?.ready(below) ?? new Promise((resolve) => nextTask(resolve));
|
|
2295
|
+
}
|
|
2296
|
+
const agreements = /* @__PURE__ */ new WeakMap();
|
|
2297
|
+
/** @internal Registered by the multiplexer for each port with a window. */
|
|
2298
|
+
function setPortAgreement(port, agreement) {
|
|
2299
|
+
agreements.set(port, agreement);
|
|
2300
|
+
}
|
|
2301
|
+
/**
|
|
2302
|
+
* @internal What has been agreed with the peer on `port` so far; `undefined`
|
|
2303
|
+
* on a port without a window. Not exported from the package.
|
|
2304
|
+
*/
|
|
2305
|
+
function portAgreement(port) {
|
|
2306
|
+
return agreements.get(port)?.();
|
|
2307
|
+
}
|
|
2308
|
+
const ends = /* @__PURE__ */ new WeakMap();
|
|
2309
|
+
/** @internal Registered by the multiplexer for each port with a window. */
|
|
2310
|
+
function setPortEndState(port, state) {
|
|
2311
|
+
ends.set(port, state);
|
|
2312
|
+
}
|
|
2313
|
+
/**
|
|
2314
|
+
* @internal End this side's direction of `port`: an `end` is queued behind
|
|
2315
|
+
* everything already posted on it, and anything posted afterwards is dropped.
|
|
2316
|
+
* `false`, and nothing changes, when `end` was not agreed (no window, the
|
|
2317
|
+
* handshake not answered yet, or a peer that does not speak it): the caller
|
|
2318
|
+
* then closes the port. Never waits on the peer. Not exported from the package.
|
|
2319
|
+
*/
|
|
2320
|
+
function endPort(port) {
|
|
2321
|
+
return ends.get(port)?.end() ?? false;
|
|
2322
|
+
}
|
|
2323
|
+
/**
|
|
2324
|
+
* @internal Resolves once the peer has ended its direction of `port`. It never
|
|
2325
|
+
* settles otherwise: not on a port where `end` is not agreed when it is asked
|
|
2326
|
+
* (no window, the handshake not answered yet, a peer or codec without it), and
|
|
2327
|
+
* not when the port closes first, so a caller races it against the port's
|
|
2328
|
+
* close signal. Not exported from the package.
|
|
2329
|
+
*/
|
|
2330
|
+
function whenPortEnded(port) {
|
|
2331
|
+
let waiting = peerEnds.get(port);
|
|
2332
|
+
if (!waiting) {
|
|
2333
|
+
let resolve = () => {};
|
|
2334
|
+
const promise = new Promise((r) => {
|
|
2335
|
+
resolve = r;
|
|
2336
|
+
});
|
|
2337
|
+
if (!ends.get(port)?.watchPeerEnd(resolve)) return promise;
|
|
2338
|
+
waiting = {
|
|
2339
|
+
promise,
|
|
2340
|
+
resolve
|
|
2341
|
+
};
|
|
2342
|
+
peerEnds.set(port, waiting);
|
|
2343
|
+
}
|
|
2344
|
+
return waiting.promise;
|
|
2345
|
+
}
|
|
2346
|
+
const peerEnds = /* @__PURE__ */ new WeakMap();
|
|
2347
|
+
/**
|
|
2220
2348
|
* Makes `target` disposable with `await using`: installs `dispose` as its
|
|
2221
2349
|
* `[Symbol.asyncDispose]`, where the runtime defines the symbol. Hand-written
|
|
2222
2350
|
* iterators lose what a native generator gets from `AsyncIteratorPrototype`;
|
|
@@ -2288,6 +2416,61 @@
|
|
|
2288
2416
|
* tests and adapters assert on it.
|
|
2289
2417
|
*/
|
|
2290
2418
|
const STREAM_ABORT = "webrun-rpc:stream-abort";
|
|
2419
|
+
/**
|
|
2420
|
+
* The `type` of a pipelined chunk: a plain port message, not a `callPort`
|
|
2421
|
+
* request, sent only on a port where both ends agreed `pipeline`. The window's
|
|
2422
|
+
* credit is its confirmation.
|
|
2423
|
+
*/
|
|
2424
|
+
const CHUNK = "webrun-rpc:chunk";
|
|
2425
|
+
/**
|
|
2426
|
+
* What a pipelined chunk counts beyond its bytes, against the window, on the
|
|
2427
|
+
* receiving side. Both codecs' windows count more for the chunk's envelope
|
|
2428
|
+
* (the estimate at least 64, msgpack's keys alone more than this), so an honest
|
|
2429
|
+
* sender never makes the receiver count past the window; a sender of empty
|
|
2430
|
+
* chunks still cannot have it hold an unbounded number of them.
|
|
2431
|
+
*/
|
|
2432
|
+
const CHUNK_OVERHEAD = 32;
|
|
2433
|
+
/**
|
|
2434
|
+
* The bytes of `view` as a plain `Uint8Array` that owns its whole buffer:
|
|
2435
|
+
* `view` itself when it already is one, else a copy. Structured clone carries
|
|
2436
|
+
* and keeps a view's whole `ArrayBuffer`, and a byte codec may decode into
|
|
2437
|
+
* views of the frame, while the window and the receiver's count see only the
|
|
2438
|
+
* view: a 4 KiB `subarray` of a 4 MiB buffer would cost 4 MiB per chunk.
|
|
2439
|
+
* Copied with the `Uint8Array` constructor, not `slice()`: a Node `Buffer`'s
|
|
2440
|
+
* `slice()` is another view on the same memory.
|
|
2441
|
+
*/
|
|
2442
|
+
function ownBytes(view) {
|
|
2443
|
+
if (view instanceof Uint8Array && view.constructor === Uint8Array && view.byteOffset === 0 && view.byteLength === view.buffer.byteLength) return view;
|
|
2444
|
+
return new Uint8Array(new Uint8Array(view.buffer, view.byteOffset, view.byteLength));
|
|
2445
|
+
}
|
|
2446
|
+
/**
|
|
2447
|
+
* A producer's chunk as bytes this end may send, in either mode: any view or
|
|
2448
|
+
* `ArrayBuffer`, normalised by `ownBytes`. Anything else fails the stream here,
|
|
2449
|
+
* as the producer's own error, rather than reaching a peer that would refuse it.
|
|
2450
|
+
*/
|
|
2451
|
+
/**
|
|
2452
|
+
* A received confirmed chunk's value, as its own bytes, put back in place of
|
|
2453
|
+
* the original in `chunk`: `listenPort` keeps the request until the handler
|
|
2454
|
+
* returns, and the original would keep its whole buffer that long. A function
|
|
2455
|
+
* of its own, so that no temporary of the suspended handler holds the
|
|
2456
|
+
* original either.
|
|
2457
|
+
*/
|
|
2458
|
+
function adoptValue(chunk) {
|
|
2459
|
+
const original = chunk.value;
|
|
2460
|
+
if (ArrayBuffer.isView(original)) chunk.value = ownBytes(original);
|
|
2461
|
+
return chunk.value;
|
|
2462
|
+
}
|
|
2463
|
+
function chunkBytes(value) {
|
|
2464
|
+
if (ArrayBuffer.isView(value)) return ownBytes(value);
|
|
2465
|
+
if (value instanceof ArrayBuffer || Object.prototype.toString.call(value) === "[object ArrayBuffer]") return new Uint8Array(value);
|
|
2466
|
+
throw new TypeError(`webrun-rpc: a stream chunk must be bytes (a typed array, a DataView or an ArrayBuffer), got ${value === null ? "null" : typeof value}`);
|
|
2467
|
+
}
|
|
2468
|
+
const MIXED_MESSAGE = "webrun-rpc: peer mixed confirmed and pipelined chunks; the stream port is closed";
|
|
2469
|
+
/** This end may pipeline its own chunks: agreed, and the window holds them. */
|
|
2470
|
+
const mayPipeline = (port) => {
|
|
2471
|
+
const agreement = portAgreement(port);
|
|
2472
|
+
return !!agreement?.enforced && agreement.caps.has("pipeline");
|
|
2473
|
+
};
|
|
2291
2474
|
/** Caller's input travels on this channel; the handler listens on it. */
|
|
2292
2475
|
const CHANNEL_IN = "in";
|
|
2293
2476
|
/** The handler's output travels on this channel; the caller listens on it. */
|
|
@@ -2300,7 +2483,9 @@
|
|
|
2300
2483
|
* same way on the other channel. Within each direction the next chunk is never
|
|
2301
2484
|
* sent until the previous one has been delivered *and* pulled past by the
|
|
2302
2485
|
* consumer (spec D11) — the reply to a chunk call *is* the confirmation, and
|
|
2303
|
-
* `listenPort` withholds it until then.
|
|
2486
|
+
* `listenPort` withholds it until then. On a port whose window both ends
|
|
2487
|
+
* speak and that agreed `pipeline`, chunks go as plain messages instead, and
|
|
2488
|
+
* the window's credit is the confirmation: up to a window is in flight.
|
|
2304
2489
|
*
|
|
2305
2490
|
* A stream port carries exactly one invocation. To make several calls, open
|
|
2306
2491
|
* several ports: `mux.openPort({ kind: "stream" })` per call.
|
|
@@ -2317,22 +2502,49 @@
|
|
|
2317
2502
|
const notice = installAbortNotice(port, controller);
|
|
2318
2503
|
abortOnPortClose(port, controller);
|
|
2319
2504
|
const clock = installStreamTimeout(controller, options.timeout);
|
|
2320
|
-
const inbound = receiveChunks(port, CHANNEL_IN, controller, clock.touch);
|
|
2505
|
+
const inbound = receiveChunks(port, CHANNEL_IN, controller, clock.touch, notice);
|
|
2321
2506
|
let output;
|
|
2322
2507
|
try {
|
|
2323
2508
|
output = handler(inbound.stream);
|
|
2324
2509
|
} catch (err) {
|
|
2325
|
-
const pump = sendChunks(port, CHANNEL_OUT, failing(err), options, controller.signal, clock.touch);
|
|
2510
|
+
const pump = sendChunks(port, CHANNEL_OUT, failing(err), options, controller.signal, clock.touch, () => {});
|
|
2326
2511
|
pump.catch(() => {});
|
|
2327
2512
|
disarmClockWhenBothSidesSettle(clock, pump, inbound.ended);
|
|
2328
2513
|
return teardownOnce(controller, notice, clock, inbound, void 0);
|
|
2329
2514
|
}
|
|
2330
|
-
|
|
2515
|
+
let onSent = () => {};
|
|
2516
|
+
const sent = new Promise((resolve) => {
|
|
2517
|
+
onSent = resolve;
|
|
2518
|
+
});
|
|
2519
|
+
const pump = sendChunks(port, CHANNEL_OUT, output, options, controller.signal, clock.touch, onSent);
|
|
2331
2520
|
pump.catch(() => {});
|
|
2332
2521
|
disarmClockWhenBothSidesSettle(clock, pump, inbound.ended);
|
|
2522
|
+
endServedStream(port, sent, inbound.over);
|
|
2333
2523
|
return teardownOnce(controller, notice, clock, inbound, output);
|
|
2334
2524
|
}
|
|
2335
2525
|
/**
|
|
2526
|
+
* The serving side's normal finish. Once the handler's last chunk has gone
|
|
2527
|
+
* without an error, and no more input is coming (`over`: the caller's last
|
|
2528
|
+
* chunk arrived without an error and was read, or the caller ended its
|
|
2529
|
+
* direction behind it, for a handler that did not read all its input), this
|
|
2530
|
+
* side ends its direction too, where `end` was agreed. Without `end` nothing
|
|
2531
|
+
* happens here: the caller closes the port. A stream that never gets that far
|
|
2532
|
+
* was abandoned, and the caller's notice and `close` free the port; this then
|
|
2533
|
+
* never resumes. It needs no race against the port's close signal: nothing
|
|
2534
|
+
* awaits it, and what keeps it waiting is the input's watcher, a listener of
|
|
2535
|
+
* the port that a close drops, after which the wait is collected.
|
|
2536
|
+
*
|
|
2537
|
+
* Nothing this side still posts is lost behind its `end`. The caller ends only
|
|
2538
|
+
* once its own last chunk was confirmed, so no confirmation is owed to it
|
|
2539
|
+
* here, and what the freed port's close signal aborts has settled: the last
|
|
2540
|
+
* chunk has left, and input nobody reads is dropped, as on a close.
|
|
2541
|
+
*/
|
|
2542
|
+
async function endServedStream(port, sent, over) {
|
|
2543
|
+
await sent;
|
|
2544
|
+
await over;
|
|
2545
|
+
endPort(port);
|
|
2546
|
+
}
|
|
2547
|
+
/**
|
|
2336
2548
|
* A stream that completes normally has nobody left to call the returned
|
|
2337
2549
|
* teardown — the caller only knows the stream ended, not that it must also
|
|
2338
2550
|
* dispose the serve-side handle. Without this, the last `touch()`'s timer
|
|
@@ -2372,22 +2584,31 @@
|
|
|
2372
2584
|
const notice = installAbortNotice(port, controller);
|
|
2373
2585
|
abortOnPortClose(port, controller);
|
|
2374
2586
|
const clock = installStreamTimeout(controller, options.timeout);
|
|
2375
|
-
const inbound = receiveChunks(port, CHANNEL_OUT, controller, clock.touch);
|
|
2376
|
-
|
|
2587
|
+
const inbound = receiveChunks(port, CHANNEL_OUT, controller, clock.touch, notice);
|
|
2588
|
+
let sent = false;
|
|
2589
|
+
const pump = sendChunks(port, CHANNEL_IN, input, options, controller.signal, clock.touch, () => {
|
|
2590
|
+
sent = true;
|
|
2591
|
+
});
|
|
2377
2592
|
pump.catch(() => {});
|
|
2378
2593
|
return returningAtOnce((async function* () {
|
|
2594
|
+
let received = false;
|
|
2379
2595
|
try {
|
|
2380
2596
|
yield* inbound.stream;
|
|
2597
|
+
received = true;
|
|
2381
2598
|
} finally {
|
|
2599
|
+
const ended = received && sent;
|
|
2382
2600
|
if (!controller.signal.aborted) controller.abort(/* @__PURE__ */ new Error("webrun-rpc: the caller abandoned the stream"));
|
|
2383
|
-
notice.post();
|
|
2601
|
+
if (!ended) notice.post();
|
|
2384
2602
|
notice.stop();
|
|
2385
2603
|
clock.stop();
|
|
2386
2604
|
inbound.stop();
|
|
2387
2605
|
await pump.catch(() => {});
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2606
|
+
if (!(ended && endPort(port))) {
|
|
2607
|
+
notice.post();
|
|
2608
|
+
try {
|
|
2609
|
+
await port.close?.();
|
|
2610
|
+
} catch {}
|
|
2611
|
+
}
|
|
2391
2612
|
}
|
|
2392
2613
|
})(), () => {
|
|
2393
2614
|
if (!controller.signal.aborted) controller.abort(/* @__PURE__ */ new Error("webrun-rpc: the caller abandoned the stream"));
|
|
@@ -2481,7 +2702,7 @@
|
|
|
2481
2702
|
* the local consumer has not started iterating, an inbound chunk waits for it —
|
|
2482
2703
|
* which is the correct backpressure, and different from having no listener.
|
|
2483
2704
|
*/
|
|
2484
|
-
function receiveChunks(port, channelName, controller, touch) {
|
|
2705
|
+
function receiveChunks(port, channelName, controller, touch, notice) {
|
|
2485
2706
|
let deliver;
|
|
2486
2707
|
const waiting = [];
|
|
2487
2708
|
let finished = false;
|
|
@@ -2491,25 +2712,64 @@
|
|
|
2491
2712
|
const ended = new Promise((resolve) => {
|
|
2492
2713
|
resolveEnded = resolve;
|
|
2493
2714
|
});
|
|
2715
|
+
let resolveOver = () => {};
|
|
2716
|
+
const over = new Promise((resolve) => {
|
|
2717
|
+
resolveOver = resolve;
|
|
2718
|
+
});
|
|
2494
2719
|
let aborted = false;
|
|
2495
2720
|
let abortReason;
|
|
2496
2721
|
const ready = () => deliver || finished ? Promise.resolve() : new Promise((r) => waiting.push(r));
|
|
2497
2722
|
const wake = () => {
|
|
2498
2723
|
for (const r of waiting.splice(0)) r();
|
|
2499
2724
|
};
|
|
2500
|
-
const
|
|
2725
|
+
const violate = (message) => {
|
|
2726
|
+
poison = new Error(message);
|
|
2727
|
+
controller.abort(poison);
|
|
2728
|
+
setTimeout(() => {
|
|
2729
|
+
try {
|
|
2730
|
+
port.close?.();
|
|
2731
|
+
} catch {}
|
|
2732
|
+
}, 0);
|
|
2733
|
+
return poison;
|
|
2734
|
+
};
|
|
2735
|
+
let lastChunkArrived = false;
|
|
2736
|
+
let watching = false;
|
|
2737
|
+
const watchPeerEnd = () => {
|
|
2738
|
+
if (watching) return true;
|
|
2739
|
+
if (!portAgreement(port)?.caps.has("end")) return false;
|
|
2740
|
+
watching = true;
|
|
2741
|
+
whenPortEnded(port).then(() => {
|
|
2742
|
+
port.removeEventListener("message", watcher);
|
|
2743
|
+
if (lastChunkArrived) {
|
|
2744
|
+
resolveOver();
|
|
2745
|
+
return;
|
|
2746
|
+
}
|
|
2747
|
+
notice.post();
|
|
2748
|
+
violate("webrun-rpc: the peer ended its direction without finishing it; the stream port is closed");
|
|
2749
|
+
});
|
|
2750
|
+
return true;
|
|
2751
|
+
};
|
|
2752
|
+
const watcher = (event) => {
|
|
2753
|
+
const data = event.data;
|
|
2754
|
+
if (data?.channelName !== channelName) return;
|
|
2755
|
+
const chunk = data.type === CHUNK ? data.chunk : data.type === "request" ? data.params : void 0;
|
|
2756
|
+
if (!chunk) return;
|
|
2757
|
+
if (!watchPeerEnd()) {
|
|
2758
|
+
port.removeEventListener("message", watcher);
|
|
2759
|
+
return;
|
|
2760
|
+
}
|
|
2761
|
+
if (chunk.done && !chunk.error) lastChunkArrived = true;
|
|
2762
|
+
};
|
|
2763
|
+
port.addEventListener("message", watcher);
|
|
2764
|
+
watchPeerEnd();
|
|
2765
|
+
let held = 0;
|
|
2766
|
+
const off = listenPort(port, async (params) => {
|
|
2767
|
+
const { done, error } = params;
|
|
2768
|
+
const value = adoptValue(params);
|
|
2501
2769
|
touch();
|
|
2502
2770
|
if (poison) throw poison;
|
|
2503
|
-
if (
|
|
2504
|
-
|
|
2505
|
-
controller.abort(poison);
|
|
2506
|
-
setTimeout(() => {
|
|
2507
|
-
try {
|
|
2508
|
-
port.close?.();
|
|
2509
|
-
} catch {}
|
|
2510
|
-
}, 0);
|
|
2511
|
-
throw poison;
|
|
2512
|
-
}
|
|
2771
|
+
if (held > 0) throw violate(MIXED_MESSAGE);
|
|
2772
|
+
if (outstanding) throw violate("webrun-rpc: peer sent a second unconfirmed chunk; the stream port is closed");
|
|
2513
2773
|
outstanding = true;
|
|
2514
2774
|
try {
|
|
2515
2775
|
await ready();
|
|
@@ -2519,15 +2779,60 @@
|
|
|
2519
2779
|
value,
|
|
2520
2780
|
error: error ? deserializeError(error) : void 0
|
|
2521
2781
|
});
|
|
2782
|
+
if (done && !error) resolveOver();
|
|
2522
2783
|
} finally {
|
|
2523
2784
|
outstanding = false;
|
|
2524
2785
|
}
|
|
2525
2786
|
if (done) resolveEnded();
|
|
2526
2787
|
}, { channelName });
|
|
2788
|
+
const onChunk = (event) => {
|
|
2789
|
+
const data = event.data;
|
|
2790
|
+
if (data?.type !== CHUNK || data.channelName !== channelName || !data.chunk) return;
|
|
2791
|
+
if (poison) return;
|
|
2792
|
+
const agreement = portAgreement(port);
|
|
2793
|
+
if (!agreement?.caps.has("pipeline")) {
|
|
2794
|
+
violate("webrun-rpc: peer sent a pipelined chunk where pipelining was not agreed");
|
|
2795
|
+
return;
|
|
2796
|
+
}
|
|
2797
|
+
if (outstanding) {
|
|
2798
|
+
violate(MIXED_MESSAGE);
|
|
2799
|
+
return;
|
|
2800
|
+
}
|
|
2801
|
+
if (data.chunk.value !== void 0 && !ArrayBuffer.isView(data.chunk.value)) {
|
|
2802
|
+
violate("webrun-rpc: peer sent a pipelined chunk whose value is not bytes");
|
|
2803
|
+
return;
|
|
2804
|
+
}
|
|
2805
|
+
if (held > agreement.window) {
|
|
2806
|
+
violate(`webrun-rpc: peer sent past the window (${agreement.window} bytes) without waiting for credit; the stream port is closed`);
|
|
2807
|
+
return;
|
|
2808
|
+
}
|
|
2809
|
+
touch();
|
|
2810
|
+
const { done, error } = data.chunk;
|
|
2811
|
+
const value = data.chunk.value && ownBytes(data.chunk.value);
|
|
2812
|
+
const size = (value?.byteLength ?? 0) + CHUNK_OVERHEAD;
|
|
2813
|
+
held += size;
|
|
2814
|
+
return (async () => {
|
|
2815
|
+
try {
|
|
2816
|
+
await ready();
|
|
2817
|
+
if (finished) return;
|
|
2818
|
+
await deliver?.({
|
|
2819
|
+
done,
|
|
2820
|
+
value,
|
|
2821
|
+
error: error ? deserializeError(error) : void 0
|
|
2822
|
+
});
|
|
2823
|
+
if (done && !error) resolveOver();
|
|
2824
|
+
if (done) resolveEnded();
|
|
2825
|
+
} finally {
|
|
2826
|
+
held -= size;
|
|
2827
|
+
}
|
|
2828
|
+
})();
|
|
2829
|
+
};
|
|
2830
|
+
port.addEventListener("message", onChunk);
|
|
2527
2831
|
const detach = () => {
|
|
2528
2832
|
finished = true;
|
|
2529
2833
|
wake();
|
|
2530
2834
|
off();
|
|
2835
|
+
port.removeEventListener("message", onChunk);
|
|
2531
2836
|
controller.signal.removeEventListener("abort", onAbort);
|
|
2532
2837
|
resolveEnded();
|
|
2533
2838
|
};
|
|
@@ -2557,21 +2862,35 @@
|
|
|
2557
2862
|
return detach;
|
|
2558
2863
|
}),
|
|
2559
2864
|
stop: detach,
|
|
2560
|
-
ended
|
|
2865
|
+
ended,
|
|
2866
|
+
over
|
|
2561
2867
|
};
|
|
2562
2868
|
}
|
|
2563
2869
|
/**
|
|
2564
2870
|
* The sending half of one direction: one `callPort` per chunk, one call
|
|
2565
|
-
* outstanding at a time (spec D11/D12), with no per-chunk deadline (spec D8)
|
|
2871
|
+
* outstanding at a time (spec D11/D12), with no per-chunk deadline (spec D8) —
|
|
2872
|
+
* or, once pipelining is agreed, plain chunk messages held back by the window.
|
|
2873
|
+
* `onSent` runs once the end of the output has left without an error: posted
|
|
2874
|
+
* when pipelined, confirmed when not.
|
|
2566
2875
|
*/
|
|
2567
|
-
async function sendChunks(port, channelName, output, { maxMessageSize, log }, signal, touch) {
|
|
2568
|
-
const
|
|
2876
|
+
async function sendChunks(port, channelName, output, { maxMessageSize, log }, signal, touch, onSent) {
|
|
2877
|
+
const framed = maxMessageSize ? toChunks(maxMessageSize)(output) : output;
|
|
2878
|
+
const stream = throughAbort(framed, signal);
|
|
2879
|
+
const producer = output;
|
|
2880
|
+
const returnProducer = () => void Promise.resolve(producer.return?.()).catch(() => {});
|
|
2881
|
+
if (framed !== output) signal.addEventListener("abort", returnProducer, { once: true });
|
|
2882
|
+
let onAborted = () => {};
|
|
2883
|
+
const aborted = new Promise((resolve) => {
|
|
2884
|
+
onAborted = () => resolve();
|
|
2885
|
+
});
|
|
2886
|
+
if (signal.aborted) onAborted();
|
|
2887
|
+
else signal.addEventListener("abort", onAborted, { once: true });
|
|
2569
2888
|
try {
|
|
2570
2889
|
await sendIterator(async ({ done, value, error }) => {
|
|
2571
2890
|
if (signal.aborted) return;
|
|
2572
2891
|
const chunk = {
|
|
2573
2892
|
done,
|
|
2574
|
-
value,
|
|
2893
|
+
value: done ? value : chunkBytes(value),
|
|
2575
2894
|
error: error === void 0 ? void 0 : serializeError(error)
|
|
2576
2895
|
};
|
|
2577
2896
|
log?.("[duplexOverPort] send", {
|
|
@@ -2579,46 +2898,34 @@
|
|
|
2579
2898
|
done,
|
|
2580
2899
|
size: value?.byteLength
|
|
2581
2900
|
});
|
|
2901
|
+
if (mayPipeline(port)) {
|
|
2902
|
+
port.postMessage({
|
|
2903
|
+
type: CHUNK,
|
|
2904
|
+
channelName,
|
|
2905
|
+
chunk
|
|
2906
|
+
});
|
|
2907
|
+
touch();
|
|
2908
|
+
if (done && !error) onSent();
|
|
2909
|
+
if (portBufferedAmount(port) > 0) await Promise.race([whenPortReady(port), aborted]);
|
|
2910
|
+
return;
|
|
2911
|
+
}
|
|
2582
2912
|
await callPort(port, chunk, {
|
|
2583
2913
|
channelName,
|
|
2584
2914
|
timeout: NO_TIMEOUT,
|
|
2585
2915
|
signal
|
|
2586
2916
|
});
|
|
2587
2917
|
touch();
|
|
2918
|
+
if (done && !error) onSent();
|
|
2588
2919
|
}, stream);
|
|
2589
2920
|
} catch (err) {
|
|
2590
2921
|
if (signal.aborted) return;
|
|
2591
2922
|
throw err;
|
|
2923
|
+
} finally {
|
|
2924
|
+
signal.removeEventListener("abort", returnProducer);
|
|
2925
|
+
signal.removeEventListener("abort", onAborted);
|
|
2592
2926
|
}
|
|
2593
2927
|
}
|
|
2594
2928
|
/**
|
|
2595
|
-
* Runs `fn` on a later task, never a microtask: a producer looping on
|
|
2596
|
-
* `whenPortReady` must let the transport's own events run. `setImmediate`
|
|
2597
|
-
* where it exists; elsewhere a `MessageChannel`, because nested `setTimeout`s
|
|
2598
|
-
* are clamped to 4 ms in browsers.
|
|
2599
|
-
*/
|
|
2600
|
-
const setImmediateFn = globalThis.setImmediate;
|
|
2601
|
-
/** @internal */
|
|
2602
|
-
const nextTask = setImmediateFn ? (fn) => {
|
|
2603
|
-
setImmediateFn(fn);
|
|
2604
|
-
} : (() => {
|
|
2605
|
-
let channel;
|
|
2606
|
-
const queue = [];
|
|
2607
|
-
return (fn) => {
|
|
2608
|
-
if (!channel) {
|
|
2609
|
-
channel = new MessageChannel();
|
|
2610
|
-
channel.port1.onmessage = () => queue.shift()?.();
|
|
2611
|
-
}
|
|
2612
|
-
queue.push(fn);
|
|
2613
|
-
channel.port2.postMessage(0);
|
|
2614
|
-
};
|
|
2615
|
-
})();
|
|
2616
|
-
const states = /* @__PURE__ */ new WeakMap();
|
|
2617
|
-
/** @internal Registered by the multiplexer for each port with a window. */
|
|
2618
|
-
function setPortSendState(port, state) {
|
|
2619
|
-
states.set(port, state);
|
|
2620
|
-
}
|
|
2621
|
-
/**
|
|
2622
2929
|
* One virtual port.
|
|
2623
2930
|
*
|
|
2624
2931
|
* `deliver` and `markClosed` are deliberately not on `port`: the consumer holds
|
|
@@ -2696,6 +3003,21 @@
|
|
|
2696
3003
|
const KEEPALIVE_ID = Number.MAX_SAFE_INTEGER;
|
|
2697
3004
|
/** The longest delay a timer holds; past it `setInterval` fires at once. */
|
|
2698
3005
|
const MAX_TIMER_MS = 2 ** 31 - 1;
|
|
3006
|
+
/**
|
|
3007
|
+
* What a windowed port offers in its `open`: every capability this end
|
|
3008
|
+
* speaks. `end` only over a codec that reads it (`PortCodec.halfClose`).
|
|
3009
|
+
*/
|
|
3010
|
+
function ownCaps(codec) {
|
|
3011
|
+
return codec.halfClose ? ["pipeline", "end"] : ["pipeline"];
|
|
3012
|
+
}
|
|
3013
|
+
/**
|
|
3014
|
+
* The capabilities in a peer's `caps` that this end speaks too, or
|
|
3015
|
+
* `undefined` when the peer listed none (it is then treated as an older peer).
|
|
3016
|
+
*/
|
|
3017
|
+
function sharedCaps(own, listed) {
|
|
3018
|
+
if (!Array.isArray(listed)) return void 0;
|
|
3019
|
+
return new Set(own.filter((cap) => listed.includes(cap)));
|
|
3020
|
+
}
|
|
2699
3021
|
/** Ceiling on concurrently open virtual ports. Bounds the id table only. */
|
|
2700
3022
|
const DEFAULT_MAX_PORTS = 1024;
|
|
2701
3023
|
/**
|
|
@@ -2727,10 +3049,18 @@
|
|
|
2727
3049
|
*/
|
|
2728
3050
|
function multiplexPort(port, options) {
|
|
2729
3051
|
const { codec, onPort, side = "initiator", maxPorts = DEFAULT_MAX_PORTS, maxMessageSize, signal, window, frameSize, keepalive, onLeak } = options;
|
|
3052
|
+
const maxReassembledBytes = options.maxReassembledBytes ?? 67108864;
|
|
2730
3053
|
const misses = keepalive?.misses ?? 3;
|
|
2731
3054
|
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`);
|
|
2732
3055
|
if (frameSize != null && !(Number.isSafeInteger(frameSize) && frameSize > 0)) throw new RangeError(`webrun-rpc: frameSize must be a positive integer, got ${frameSize}`);
|
|
3056
|
+
if (!(Number.isSafeInteger(maxReassembledBytes) && maxReassembledBytes > 0)) throw new RangeError(`webrun-rpc: maxReassembledBytes must be a positive integer, got ${maxReassembledBytes}`);
|
|
2733
3057
|
const open = /* @__PURE__ */ new Map();
|
|
3058
|
+
let reassembling = 0;
|
|
3059
|
+
const freePartial = (entry) => {
|
|
3060
|
+
if (!entry?.partial) return;
|
|
3061
|
+
reassembling -= entry.partial.charged;
|
|
3062
|
+
entry.partial = void 0;
|
|
3063
|
+
};
|
|
2734
3064
|
const leaks = onLeak ? new FinalizationRegistry(({ id, record, release }) => {
|
|
2735
3065
|
const entry = open.get(id);
|
|
2736
3066
|
if (entry?.handle !== record) return;
|
|
@@ -2751,6 +3081,8 @@
|
|
|
2751
3081
|
let muxClosed = false;
|
|
2752
3082
|
/** This end counts encoded bytes and reassembles frames. */
|
|
2753
3083
|
const exactCodec = codec.encode != null && codec.decode != null;
|
|
3084
|
+
/** What this end offers on a windowed port. */
|
|
3085
|
+
const caps = ownCaps(codec);
|
|
2754
3086
|
const post = (envelope, transfer) => {
|
|
2755
3087
|
if (muxClosed) return;
|
|
2756
3088
|
codecPost(envelope, transfer);
|
|
@@ -2808,13 +3140,39 @@
|
|
|
2808
3140
|
port.postMessage(bytes);
|
|
2809
3141
|
} catch {}
|
|
2810
3142
|
};
|
|
3143
|
+
/**
|
|
3144
|
+
* Both directions have ended: free the id as a close would, with no `close`
|
|
3145
|
+
* on the wire (each end frees it on its own once it has seen both ends).
|
|
3146
|
+
* Aborting the close signal is how the port's consumer learns it is gone.
|
|
3147
|
+
*/
|
|
3148
|
+
const freeIfEnded = (id, c) => {
|
|
3149
|
+
const entry = open.get(id);
|
|
3150
|
+
if (!c.endSent || !c.peerEnded || entry?.credit !== c) return;
|
|
3151
|
+
freePartial(entry);
|
|
3152
|
+
open.delete(id);
|
|
3153
|
+
leaks?.unregister(entry.handle);
|
|
3154
|
+
entry.handle.markClosed(/* @__PURE__ */ new Error("webrun-rpc: the port ended in both directions"));
|
|
3155
|
+
};
|
|
3156
|
+
/** A peer that broke the port's protocol: hold none of its traffic, and close. */
|
|
3157
|
+
const failPort = (id, entry, error, reason) => {
|
|
3158
|
+
freePartial(entry);
|
|
3159
|
+
dropOutgoing(id);
|
|
3160
|
+
open.delete(id);
|
|
3161
|
+
releaseWaiters(entry.credit);
|
|
3162
|
+
entry.handle.markClosed(error);
|
|
3163
|
+
post({
|
|
3164
|
+
type: "close",
|
|
3165
|
+
id,
|
|
3166
|
+
reason
|
|
3167
|
+
});
|
|
3168
|
+
};
|
|
2811
3169
|
const releaseWaiters = (c) => {
|
|
2812
3170
|
for (const w of c?.waiters.splice(0) ?? []) w.resolve();
|
|
2813
3171
|
};
|
|
2814
3172
|
const flush = (c) => {
|
|
2815
3173
|
while (c.queue.length) {
|
|
2816
3174
|
const next = c.queue[0];
|
|
2817
|
-
if (c.enforced && c.outstanding > 0 && c.outstanding + next.size > c.window) break;
|
|
3175
|
+
if (next.size > 0 && c.enforced && c.outstanding > 0 && c.outstanding + next.size > c.window) break;
|
|
2818
3176
|
c.queue.shift();
|
|
2819
3177
|
c.queued -= next.size;
|
|
2820
3178
|
c.outstanding += next.size;
|
|
@@ -2852,15 +3210,20 @@
|
|
|
2852
3210
|
enforced: false,
|
|
2853
3211
|
opener,
|
|
2854
3212
|
exact,
|
|
3213
|
+
caps: /* @__PURE__ */ new Set(),
|
|
2855
3214
|
outstanding: 0,
|
|
2856
3215
|
queue: [],
|
|
2857
3216
|
queued: 0,
|
|
2858
3217
|
unconsumed: 0,
|
|
2859
3218
|
waiters: [],
|
|
2860
3219
|
toGrant: 0,
|
|
2861
|
-
grantScheduled: false
|
|
3220
|
+
grantScheduled: false,
|
|
3221
|
+
ending: false,
|
|
3222
|
+
endSent: false,
|
|
3223
|
+
peerEnded: false
|
|
2862
3224
|
} : void 0;
|
|
2863
3225
|
const self = newVirtualPort((payload, transfer) => {
|
|
3226
|
+
if (c?.ending) return;
|
|
2864
3227
|
touch(id);
|
|
2865
3228
|
const envelope = {
|
|
2866
3229
|
type: "message",
|
|
@@ -2878,12 +3241,12 @@
|
|
|
2878
3241
|
...envelope,
|
|
2879
3242
|
payload: structuredClone(payload, { transfer })
|
|
2880
3243
|
} : void 0;
|
|
2881
|
-
const send = () => {
|
|
3244
|
+
const send = (whole = false) => {
|
|
2882
3245
|
if (!bytes) {
|
|
2883
3246
|
sendOnPort(id, held ? () => post(held) : () => post(envelope, transfer));
|
|
2884
3247
|
return;
|
|
2885
3248
|
}
|
|
2886
|
-
if (frameSize != null && c.enforced && c.exact && bytes.byteLength > frameSize) {
|
|
3249
|
+
if (!whole && frameSize != null && c.enforced && c.exact && bytes.byteLength > frameSize) {
|
|
2887
3250
|
for (let at = 0; at < bytes.byteLength; at += frameSize) {
|
|
2888
3251
|
const data = bytes.subarray(at, at + frameSize);
|
|
2889
3252
|
const frame = at + frameSize >= bytes.byteLength ? {
|
|
@@ -2913,10 +3276,11 @@
|
|
|
2913
3276
|
record.markClosed(reason);
|
|
2914
3277
|
});
|
|
2915
3278
|
function release(reason) {
|
|
3279
|
+
freePartial(open.get(id));
|
|
2916
3280
|
open.delete(id);
|
|
2917
3281
|
leaks?.unregister(record);
|
|
2918
3282
|
if (c?.queue.length) {
|
|
2919
|
-
for (const item of c.queue.splice(0)) item.send();
|
|
3283
|
+
for (const item of c.queue.splice(0)) item.send(true);
|
|
2920
3284
|
c.queued = 0;
|
|
2921
3285
|
}
|
|
2922
3286
|
sendOnPort(id, () => post({
|
|
@@ -2946,6 +3310,38 @@
|
|
|
2946
3310
|
});
|
|
2947
3311
|
})
|
|
2948
3312
|
});
|
|
3313
|
+
setPortEndState(self.port, {
|
|
3314
|
+
end: () => {
|
|
3315
|
+
if (isClosed() || !c.caps.has("end")) return false;
|
|
3316
|
+
if (c.ending) return true;
|
|
3317
|
+
c.ending = true;
|
|
3318
|
+
c.queue.push({
|
|
3319
|
+
size: 0,
|
|
3320
|
+
send: () => sendOnPort(id, () => {
|
|
3321
|
+
if (open.get(id)?.credit !== c) return;
|
|
3322
|
+
post({
|
|
3323
|
+
type: "end",
|
|
3324
|
+
id
|
|
3325
|
+
});
|
|
3326
|
+
c.endSent = true;
|
|
3327
|
+
freeIfEnded(id, c);
|
|
3328
|
+
})
|
|
3329
|
+
});
|
|
3330
|
+
flush(c);
|
|
3331
|
+
return true;
|
|
3332
|
+
},
|
|
3333
|
+
watchPeerEnd: (resolve) => {
|
|
3334
|
+
if (!c.caps.has("end")) return false;
|
|
3335
|
+
if (c.peerEnded) resolve();
|
|
3336
|
+
else c.onPeerEnd = new WeakRef(resolve);
|
|
3337
|
+
return true;
|
|
3338
|
+
}
|
|
3339
|
+
});
|
|
3340
|
+
setPortAgreement(self.port, () => ({
|
|
3341
|
+
window: c.window,
|
|
3342
|
+
caps: c.caps,
|
|
3343
|
+
enforced: c.enforced
|
|
3344
|
+
}));
|
|
2949
3345
|
}
|
|
2950
3346
|
const record = {
|
|
2951
3347
|
deliver: self.deliver,
|
|
@@ -2997,16 +3393,24 @@
|
|
|
2997
3393
|
}
|
|
2998
3394
|
const handle = attach(envelope.id, envelope.meta, envelope.window, false, envelope.exact === true && exactCodec);
|
|
2999
3395
|
const credit = open.get(envelope.id)?.credit;
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3396
|
+
const listed = sharedCaps(caps, envelope.caps);
|
|
3397
|
+
if (credit) {
|
|
3398
|
+
const ack = credit.exact ? {
|
|
3399
|
+
type: "credit",
|
|
3400
|
+
id: envelope.id,
|
|
3401
|
+
bytes: 0,
|
|
3402
|
+
exact: true
|
|
3403
|
+
} : {
|
|
3404
|
+
type: "credit",
|
|
3405
|
+
id: envelope.id,
|
|
3406
|
+
bytes: 0
|
|
3407
|
+
};
|
|
3408
|
+
if (listed) credit.caps = listed;
|
|
3409
|
+
post(listed ? {
|
|
3410
|
+
...ack,
|
|
3411
|
+
caps: [...listed]
|
|
3412
|
+
} : ack);
|
|
3413
|
+
}
|
|
3010
3414
|
let accepted = false;
|
|
3011
3415
|
if (onPort) try {
|
|
3012
3416
|
accepted = onPort(handle.port, envelope.meta) !== false;
|
|
@@ -3034,6 +3438,7 @@
|
|
|
3034
3438
|
c.exact = false;
|
|
3035
3439
|
c.outstanding = 0;
|
|
3036
3440
|
}
|
|
3441
|
+
c.caps = sharedCaps(caps, envelope.caps) ?? /* @__PURE__ */ new Set();
|
|
3037
3442
|
post({
|
|
3038
3443
|
type: "credit",
|
|
3039
3444
|
id: envelope.id,
|
|
@@ -3045,6 +3450,10 @@
|
|
|
3045
3450
|
flush(c);
|
|
3046
3451
|
return;
|
|
3047
3452
|
}
|
|
3453
|
+
if ((envelope.type === "message" || envelope.type === "frame") && entry?.credit?.peerEnded) {
|
|
3454
|
+
failPort(envelope.id, entry, /* @__PURE__ */ new Error("webrun-rpc: the peer sent on a port after its end"), "protocol");
|
|
3455
|
+
return;
|
|
3456
|
+
}
|
|
3048
3457
|
if (envelope.type === "message") {
|
|
3049
3458
|
const sizeOf = (c) => (c.exact ? byteLengthOf(event.data) : void 0) ?? estimateMessageSize(envelope.payload);
|
|
3050
3459
|
if (!entry) return;
|
|
@@ -3055,15 +3464,29 @@
|
|
|
3055
3464
|
if (envelope.type === "frame") {
|
|
3056
3465
|
if (!entry?.credit?.exact || !codec.decode || !(envelope.data instanceof Uint8Array)) return;
|
|
3057
3466
|
touch(envelope.id);
|
|
3467
|
+
const c = entry.credit;
|
|
3468
|
+
const owed = c.unconsumed + c.toGrant;
|
|
3058
3469
|
const partial = entry.partial ?? {
|
|
3059
3470
|
chunks: [],
|
|
3060
|
-
bytes: 0
|
|
3471
|
+
bytes: 0,
|
|
3472
|
+
owed,
|
|
3473
|
+
charged: 0,
|
|
3474
|
+
...owed > 0 ? { room: c.window - owed } : {}
|
|
3061
3475
|
};
|
|
3062
3476
|
entry.partial = partial;
|
|
3477
|
+
const size = envelope.data.byteLength;
|
|
3478
|
+
const charged = partial.room == null ? Math.max(0, partial.bytes + size - c.window) : 0;
|
|
3479
|
+
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;
|
|
3480
|
+
if (overrun) {
|
|
3481
|
+
failPort(envelope.id, entry, /* @__PURE__ */ new RangeError(`webrun-rpc: a framed message passed ${overrun}`), "max-reassembled-bytes");
|
|
3482
|
+
return;
|
|
3483
|
+
}
|
|
3484
|
+
partial.bytes += size;
|
|
3485
|
+
reassembling += charged - partial.charged;
|
|
3486
|
+
partial.charged = charged;
|
|
3063
3487
|
partial.chunks.push(envelope.data);
|
|
3064
|
-
partial.bytes += envelope.data.byteLength;
|
|
3065
3488
|
if (!envelope.last) return;
|
|
3066
|
-
entry
|
|
3489
|
+
freePartial(entry);
|
|
3067
3490
|
const whole = new Uint8Array(partial.bytes);
|
|
3068
3491
|
let at = 0;
|
|
3069
3492
|
for (const chunk of partial.chunks) {
|
|
@@ -3075,8 +3498,22 @@
|
|
|
3075
3498
|
entry.handle.deliver(inner.payload, whole.byteLength);
|
|
3076
3499
|
return;
|
|
3077
3500
|
}
|
|
3501
|
+
if (envelope.type === "end") {
|
|
3502
|
+
if (!entry) return;
|
|
3503
|
+
const c = entry.credit;
|
|
3504
|
+
if (!c?.caps.has("end")) {
|
|
3505
|
+
failPort(envelope.id, entry, /* @__PURE__ */ new Error("webrun-rpc: the peer ended a port where end was not agreed"), "protocol");
|
|
3506
|
+
return;
|
|
3507
|
+
}
|
|
3508
|
+
touch(envelope.id);
|
|
3509
|
+
c.peerEnded = true;
|
|
3510
|
+
c.onPeerEnd?.deref()?.();
|
|
3511
|
+
freeIfEnded(envelope.id, c);
|
|
3512
|
+
return;
|
|
3513
|
+
}
|
|
3078
3514
|
dropOutgoing(envelope.id);
|
|
3079
3515
|
if (!entry) return;
|
|
3516
|
+
freePartial(entry);
|
|
3080
3517
|
open.delete(envelope.id);
|
|
3081
3518
|
releaseWaiters(entry.credit);
|
|
3082
3519
|
entry.handle.markClosed(/* @__PURE__ */ new Error("webrun-rpc: the peer closed the port"));
|
|
@@ -3155,12 +3592,14 @@
|
|
|
3155
3592
|
id,
|
|
3156
3593
|
meta,
|
|
3157
3594
|
window,
|
|
3158
|
-
exact: true
|
|
3595
|
+
exact: true,
|
|
3596
|
+
caps: [...caps]
|
|
3159
3597
|
} : {
|
|
3160
3598
|
type: "open",
|
|
3161
3599
|
id,
|
|
3162
3600
|
meta,
|
|
3163
|
-
window
|
|
3601
|
+
window,
|
|
3602
|
+
caps: [...caps]
|
|
3164
3603
|
} : {
|
|
3165
3604
|
type: "open",
|
|
3166
3605
|
id,
|
|
@@ -3267,14 +3706,17 @@
|
|
|
3267
3706
|
const mux = await factory((port) => {
|
|
3268
3707
|
let off;
|
|
3269
3708
|
let finished = false;
|
|
3270
|
-
const tracked = (input) =>
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3709
|
+
const tracked = (input) => {
|
|
3710
|
+
const output = handler(input);
|
|
3711
|
+
return returningAtOnce((async function* () {
|
|
3712
|
+
try {
|
|
3713
|
+
yield* output;
|
|
3714
|
+
} finally {
|
|
3715
|
+
finished = true;
|
|
3716
|
+
if (off) live.delete(off);
|
|
3717
|
+
}
|
|
3718
|
+
})(), () => void output.return(void 0).catch(() => {}));
|
|
3719
|
+
};
|
|
3278
3720
|
off = serveDuplexOverPort(port, tracked, streamOptions);
|
|
3279
3721
|
if (!finished) live.add(off);
|
|
3280
3722
|
});
|
|
@@ -3312,7 +3754,7 @@
|
|
|
3312
3754
|
const candidate = value;
|
|
3313
3755
|
if (typeof candidate.id !== "number") return false;
|
|
3314
3756
|
if (!Number.isInteger(candidate.id) || candidate.id < 0) return false;
|
|
3315
|
-
return candidate.type === "open" || candidate.type === "message" || candidate.type === "close" || candidate.type === "credit" || candidate.type === "frame" || candidate.type === "ping" || candidate.type === "pong";
|
|
3757
|
+
return candidate.type === "open" || candidate.type === "message" || candidate.type === "close" || candidate.type === "credit" || candidate.type === "frame" || candidate.type === "ping" || candidate.type === "pong" || candidate.type === "end";
|
|
3316
3758
|
}
|
|
3317
3759
|
/**
|
|
3318
3760
|
* For ports whose messages are structured values — a real `MessagePort`, a
|
|
@@ -3324,6 +3766,7 @@
|
|
|
3324
3766
|
* not send anything a byte codec could not also carry.
|
|
3325
3767
|
*/
|
|
3326
3768
|
const structuredCodec = {
|
|
3769
|
+
halfClose: true,
|
|
3327
3770
|
post(port, envelope, transfer) {
|
|
3328
3771
|
if (transfer && transfer.length > 0) port.postMessage(envelope, transfer);
|
|
3329
3772
|
else port.postMessage(envelope);
|
|
@@ -3361,10 +3804,15 @@
|
|
|
3361
3804
|
* own `handleStreams` / `sendStream` (no backpressure); version 2 is
|
|
3362
3805
|
* webrun-rpc's `connect` / `serve` over `overPipe` with webrun-http-streams'
|
|
3363
3806
|
* `fetchOverDuplex` / `serveFetchOverDuplex`: one port per link, calls
|
|
3364
|
-
* multiplexed on it, each call a stream with
|
|
3365
|
-
* directions, and cancellation that reaches the handler.
|
|
3807
|
+
* multiplexed on it, each call a stream with backpressure in both
|
|
3808
|
+
* directions, and cancellation that reaches the handler. A call's port with a
|
|
3809
|
+
* window (`HttpLinkOptions.window`) pipelines its chunks where both ends speak
|
|
3810
|
+
* it, and goes one confirmed chunk at a time otherwise, so the window needs
|
|
3811
|
+
* no new version.
|
|
3366
3812
|
*/
|
|
3367
3813
|
const HTTP_LINK_PROTOCOL = "webrun-http-link/2";
|
|
3814
|
+
/** The calling end's default `HttpLinkOptions.window`: 256 KiB. */
|
|
3815
|
+
const DEFAULT_HTTP_LINK_WINDOW = 262144;
|
|
3368
3816
|
/**
|
|
3369
3817
|
* Why a request failed when its link went away under it: the far side (the
|
|
3370
3818
|
* handler, or the ServiceWorker) was lost. The request may or may not have
|
|
@@ -3383,12 +3831,13 @@
|
|
|
3383
3831
|
*/
|
|
3384
3832
|
const LINK_CLOSE = "webrun-http:link-close";
|
|
3385
3833
|
/** Opens the calling end of a link on `port` (one port per link). */
|
|
3386
|
-
async function connectHttpLink(port, { streamTimeout, maxPorts } = {}) {
|
|
3834
|
+
async function connectHttpLink(port, { streamTimeout, maxPorts, window = DEFAULT_HTTP_LINK_WINDOW } = {}) {
|
|
3387
3835
|
const { call, close } = await connect({
|
|
3388
3836
|
mux: overPipe(port, {
|
|
3389
3837
|
codec: structuredCodec,
|
|
3390
3838
|
side: "initiator",
|
|
3391
|
-
maxPorts
|
|
3839
|
+
maxPorts,
|
|
3840
|
+
window
|
|
3392
3841
|
}),
|
|
3393
3842
|
timeout: streamTimeout
|
|
3394
3843
|
});
|
|
@@ -3699,6 +4148,7 @@
|
|
|
3699
4148
|
all.set("connection", "Upgrade");
|
|
3700
4149
|
all.set("sec-websocket-version", "13");
|
|
3701
4150
|
if (!all.has("sec-websocket-key")) all.set("sec-websocket-key", randomKey());
|
|
4151
|
+
all.delete("sec-websocket-protocol");
|
|
3702
4152
|
if (protocols.length > 0) all.set("sec-websocket-protocol", protocols.join(", "));
|
|
3703
4153
|
if (!all.has("x-webrun-ws-version")) all.set(WS_VERSION_HEADER, `2`);
|
|
3704
4154
|
return withHeaders(new Request(httpUrl, {
|
|
@@ -3831,6 +4281,21 @@
|
|
|
3831
4281
|
const NEED_LINKS_CALL = "webrun-http:need-links";
|
|
3832
4282
|
//#endregion
|
|
3833
4283
|
//#region src/sw/http-sw-dispatcher.ts
|
|
4284
|
+
/**
|
|
4285
|
+
* The default key: the url's first path segment under `scope`, decoded; none outside it, at the
|
|
4286
|
+
* scope itself, or for a segment that does not decode.
|
|
4287
|
+
*/
|
|
4288
|
+
function scopeKey(url, scope) {
|
|
4289
|
+
const href = `${url}`;
|
|
4290
|
+
if (!href.startsWith(scope)) return void 0;
|
|
4291
|
+
const key = href.substring(scope.length).replace(/[?#].*$/, "").split("/")[0];
|
|
4292
|
+
if (!key) return void 0;
|
|
4293
|
+
try {
|
|
4294
|
+
return decodeURIComponent(key);
|
|
4295
|
+
} catch {
|
|
4296
|
+
return;
|
|
4297
|
+
}
|
|
4298
|
+
}
|
|
3834
4299
|
/** One attached handler link, as the ServiceWorker holds it. */
|
|
3835
4300
|
var HandlerEntry = class {
|
|
3836
4301
|
key;
|
|
@@ -3870,7 +4335,8 @@
|
|
|
3870
4335
|
* (`SwHttpAdapter.register`) or in Dedicated Workers
|
|
3871
4336
|
* (`SwHttpAdapter.attachWorker` + `serveHttpInWorker`).
|
|
3872
4337
|
*
|
|
3873
|
-
* A url `<scope>/<key>/...` belongs to the handlers of `key
|
|
4338
|
+
* A url `<scope>/<key>/...` belongs to the handlers of `key` (the `key`
|
|
4339
|
+
* option maps urls to keys some other way). Of those, the
|
|
3874
4340
|
* `route` option picks one per request. Requests and responses stream with
|
|
3875
4341
|
* backpressure in both directions, and an aborted fetch reaches the handler
|
|
3876
4342
|
* as a cancellation (webrun-rpc + webrun-http-streams).
|
|
@@ -3889,6 +4355,8 @@
|
|
|
3889
4355
|
*/
|
|
3890
4356
|
var SwHttpDispatcher = class {
|
|
3891
4357
|
self;
|
|
4358
|
+
/** `self`, with the WebWorker lib's types this module compiles with. */
|
|
4359
|
+
_sw;
|
|
3892
4360
|
log;
|
|
3893
4361
|
options;
|
|
3894
4362
|
table = new HandlerTable();
|
|
@@ -3911,19 +4379,22 @@
|
|
|
3911
4379
|
constructor(options) {
|
|
3912
4380
|
this.options = options;
|
|
3913
4381
|
this.self = options.self;
|
|
4382
|
+
this._sw = options.self;
|
|
3914
4383
|
this.log = options.log ?? (() => {});
|
|
3915
4384
|
if (locksAvailable()) this.routerLock = newLockName("webrun-http-router");
|
|
3916
4385
|
}
|
|
3917
4386
|
get scope() {
|
|
3918
|
-
return `${new URL("./", this.
|
|
4387
|
+
return this._sw.registration?.scope ?? `${new URL("./", this._sw.location.href)}`;
|
|
3919
4388
|
}
|
|
3920
|
-
/** The key `url` belongs to
|
|
4389
|
+
/** The key `url` belongs to (the `key` option; by default its first path segment under the scope). */
|
|
3921
4390
|
keyOf(url) {
|
|
3922
|
-
|
|
3923
|
-
|
|
3924
|
-
|
|
3925
|
-
|
|
3926
|
-
|
|
4391
|
+
let parsed;
|
|
4392
|
+
try {
|
|
4393
|
+
parsed = new URL(`${url}`);
|
|
4394
|
+
} catch {
|
|
4395
|
+
return;
|
|
4396
|
+
}
|
|
4397
|
+
return (this.options.key ?? scopeKey)(parsed, this.scope);
|
|
3927
4398
|
}
|
|
3928
4399
|
/** Whether `key` has ever been served on this worker. */
|
|
3929
4400
|
isClaimedKey(key) {
|
|
@@ -3934,7 +4405,7 @@
|
|
|
3934
4405
|
return this.claimedKeys.has(key) || this.table.all(key).length > 0;
|
|
3935
4406
|
}
|
|
3936
4407
|
start() {
|
|
3937
|
-
const self = this.
|
|
4408
|
+
const self = this._sw;
|
|
3938
4409
|
if (this.routerLock) this._routerHeld = holdLock(this.routerLock).then(() => {});
|
|
3939
4410
|
this._restored = this._restoreClaimedKeys();
|
|
3940
4411
|
const latch = () => {
|
|
@@ -3954,8 +4425,8 @@
|
|
|
3954
4425
|
return true;
|
|
3955
4426
|
}), handleChannelCalls(self, WS_CONNECT_CALL, async (event, params) => {
|
|
3956
4427
|
if (!this._isOwnClient(event)) throw new Error("Only this origin's pages may open sockets");
|
|
3957
|
-
const { url, protocols } = params;
|
|
3958
|
-
const connection = await this.connectWebSocket(url, protocols ?? []);
|
|
4428
|
+
const { url, protocols, headers } = params;
|
|
4429
|
+
const connection = await this.connectWebSocket(url, protocols ?? [], this._socketHeaders(headers, "page"));
|
|
3959
4430
|
return withTransfer(connection, [connection.port]);
|
|
3960
4431
|
}));
|
|
3961
4432
|
const onMessage = (event) => {
|
|
@@ -3973,10 +4444,10 @@
|
|
|
3973
4444
|
this.log("Claim control over all clients.");
|
|
3974
4445
|
event.waitUntil(self.clients.claim());
|
|
3975
4446
|
};
|
|
3976
|
-
const onFetch = (event) => this.
|
|
4447
|
+
const onFetch = (event) => void this.handleFetch(event);
|
|
3977
4448
|
self.addEventListener("install", onInstall);
|
|
3978
4449
|
self.addEventListener("activate", onActivate);
|
|
3979
|
-
self.addEventListener("fetch", onFetch);
|
|
4450
|
+
if (this.options.handleFetch !== false) self.addEventListener("fetch", onFetch);
|
|
3980
4451
|
this._cleanup.push(() => {
|
|
3981
4452
|
self.removeEventListener("install", onInstall);
|
|
3982
4453
|
self.removeEventListener("activate", onActivate);
|
|
@@ -3991,37 +4462,60 @@
|
|
|
3991
4462
|
}
|
|
3992
4463
|
/**
|
|
3993
4464
|
* Answers `request` as a fetch under the scope would be: through the
|
|
3994
|
-
* handler its key routes to. A url
|
|
3995
|
-
* worker never served, goes to the network.
|
|
4465
|
+
* handler its key routes to. A url with no key (by default, one outside the
|
|
4466
|
+
* scope), or of a key this worker never served, goes to the network.
|
|
3996
4467
|
*/
|
|
3997
4468
|
async dispatch(request) {
|
|
3998
4469
|
const key = this.keyOf(request.url);
|
|
3999
4470
|
if (key === void 0) return await fetch(request);
|
|
4000
4471
|
await this._restored.catch(() => {});
|
|
4001
4472
|
const entry = await this._acquire(request, key);
|
|
4002
|
-
if (entry === TIMED_OUT) return
|
|
4003
|
-
status: 503,
|
|
4004
|
-
statusText: "Service Unavailable (no handler attached in time)"
|
|
4005
|
-
});
|
|
4473
|
+
if (entry === TIMED_OUT) return await this._failed(request, { kind: "timeout" });
|
|
4006
4474
|
if (!entry) {
|
|
4007
4475
|
if (!this.serves(key)) return await fetch(request);
|
|
4008
|
-
return
|
|
4009
|
-
status: 404,
|
|
4010
|
-
statusText: "Not Found (no active handler)"
|
|
4011
|
-
});
|
|
4476
|
+
return await this._failed(request, { kind: "gone" });
|
|
4012
4477
|
}
|
|
4013
4478
|
try {
|
|
4014
4479
|
return await entry.link.send(request);
|
|
4015
4480
|
} catch (error) {
|
|
4016
4481
|
this.log("[dispatch] failed", key, error);
|
|
4017
|
-
return
|
|
4018
|
-
|
|
4019
|
-
|
|
4020
|
-
headers: { "content-type": "text/plain; charset=utf-8" }
|
|
4482
|
+
return await this._failed(request, {
|
|
4483
|
+
kind: "lost",
|
|
4484
|
+
error
|
|
4021
4485
|
});
|
|
4022
4486
|
}
|
|
4023
4487
|
}
|
|
4024
4488
|
/**
|
|
4489
|
+
* After `start()`: settles once the keys this worker served before a restart are read back
|
|
4490
|
+
* (whatever the outcome). Until then `serves()` may answer `false` for a key it will serve.
|
|
4491
|
+
*/
|
|
4492
|
+
get ready() {
|
|
4493
|
+
return this._restored.then(() => {}, () => {});
|
|
4494
|
+
}
|
|
4495
|
+
_socketHeaders(headers, from) {
|
|
4496
|
+
return this.options.socketHeaders ? this.options.socketHeaders(headers, from) : headers;
|
|
4497
|
+
}
|
|
4498
|
+
async _failed(request, failure) {
|
|
4499
|
+
if (this.options.failure) try {
|
|
4500
|
+
return await this.options.failure(request, failure);
|
|
4501
|
+
} catch (error) {
|
|
4502
|
+
this.log("[dispatch] the failure option threw; answering by default", error);
|
|
4503
|
+
}
|
|
4504
|
+
if (failure.kind === "timeout") return new Response(null, {
|
|
4505
|
+
status: 503,
|
|
4506
|
+
statusText: "Service Unavailable (no handler attached in time)"
|
|
4507
|
+
});
|
|
4508
|
+
if (failure.kind === "gone") return new Response(null, {
|
|
4509
|
+
status: 404,
|
|
4510
|
+
statusText: "Not Found (no active handler)"
|
|
4511
|
+
});
|
|
4512
|
+
return new Response(errorMessage(failure.error), {
|
|
4513
|
+
status: 502,
|
|
4514
|
+
statusText: "Bad Gateway",
|
|
4515
|
+
headers: { "content-type": "text/plain; charset=utf-8" }
|
|
4516
|
+
});
|
|
4517
|
+
}
|
|
4518
|
+
/**
|
|
4025
4519
|
* Opens a socket to the handler `url` routes to: the handler accepts or
|
|
4026
4520
|
* refuses it like an HTTP upgrade, and the socket's port comes back. After
|
|
4027
4521
|
* that the socket runs between the caller and the handler directly; this
|
|
@@ -4033,7 +4527,7 @@
|
|
|
4033
4527
|
headers
|
|
4034
4528
|
});
|
|
4035
4529
|
const key = this.keyOf(request.url);
|
|
4036
|
-
if (key === void 0) throw new Error(`${url}
|
|
4530
|
+
if (key === void 0) throw new Error(`${url} belongs to no key of this ServiceWorker`);
|
|
4037
4531
|
await this._restored.catch(() => {});
|
|
4038
4532
|
const entry = await this._acquire(request, key);
|
|
4039
4533
|
if (!entry || entry === TIMED_OUT) throw new Error(`No handler serves ${url}`);
|
|
@@ -4049,20 +4543,47 @@
|
|
|
4049
4543
|
});
|
|
4050
4544
|
return await answer;
|
|
4051
4545
|
}
|
|
4052
|
-
|
|
4053
|
-
|
|
4546
|
+
/**
|
|
4547
|
+
* Answers `event` if it is the dispatcher's, with `request` (default `event.request`; a host
|
|
4548
|
+
* may pass a rewritten one, with headers it adds). Returns whether it called `respondWith`;
|
|
4549
|
+
* when it did not, the event is the caller's to answer or to leave to the network. For a host
|
|
4550
|
+
* that listens to `fetch` itself (`handleFetch: false`); call it synchronously in the event's
|
|
4551
|
+
* listener, as `respondWith` must be.
|
|
4552
|
+
*
|
|
4553
|
+
* It takes a same-origin request whose key (the `key` option) a handler serves, and also:
|
|
4554
|
+
* - a navigation under the scope whose key nobody serves (answered by `unserved`);
|
|
4555
|
+
* - before the stored keys are read (the first requests after a restart), every request with
|
|
4556
|
+
* a key, since only then is it known whose it is: one nobody serves gets what it would have
|
|
4557
|
+
* got after, `unserved` for a navigation and the network for anything else.
|
|
4558
|
+
* So a host with a page of its own for urls nobody serves gives it as `unserved`, rather than
|
|
4559
|
+
* deciding before calling this.
|
|
4560
|
+
*/
|
|
4561
|
+
handleFetch(event, request = event.request) {
|
|
4562
|
+
const navigation = event.request.mode === "navigate";
|
|
4563
|
+
if (new URL(request.url).origin !== this._sw.location.origin) return false;
|
|
4054
4564
|
const key = this.keyOf(request.url);
|
|
4055
|
-
if (key === void 0) return;
|
|
4565
|
+
if (key === void 0) return false;
|
|
4056
4566
|
if (this._restoreSettled) {
|
|
4057
|
-
if (this.serves(key))
|
|
4058
|
-
|
|
4059
|
-
|
|
4567
|
+
if (this.serves(key)) {
|
|
4568
|
+
event.respondWith(this.dispatch(request));
|
|
4569
|
+
return true;
|
|
4570
|
+
}
|
|
4571
|
+
if (navigation) {
|
|
4572
|
+
event.respondWith(this._unserved(request));
|
|
4573
|
+
return true;
|
|
4574
|
+
}
|
|
4575
|
+
return false;
|
|
4060
4576
|
}
|
|
4061
4577
|
event.respondWith((async () => {
|
|
4062
4578
|
await this._restored.catch(() => {});
|
|
4063
|
-
if (
|
|
4064
|
-
return await this.
|
|
4579
|
+
if (this.serves(key)) return await this.dispatch(request);
|
|
4580
|
+
return navigation ? await this._unserved(request) : await fetch(request);
|
|
4065
4581
|
})());
|
|
4582
|
+
return true;
|
|
4583
|
+
}
|
|
4584
|
+
/** What a request the dispatcher took, and no handler serves, gets: `unserved`, or the network. */
|
|
4585
|
+
async _unserved(request) {
|
|
4586
|
+
return await (this.options.unserved ?? fetch)(request);
|
|
4066
4587
|
}
|
|
4067
4588
|
async _attach(event, params, ports) {
|
|
4068
4589
|
const clientId = event.source?.id;
|
|
@@ -4081,8 +4602,11 @@
|
|
|
4081
4602
|
}
|
|
4082
4603
|
const ctl = params.ctl ? rest.shift() : void 0;
|
|
4083
4604
|
const up = params.up ? rest.shift() : void 0;
|
|
4084
|
-
const streamTimeout = this.options
|
|
4085
|
-
const link = await connectHttpLink(down, {
|
|
4605
|
+
const { streamTimeout, window } = this.options;
|
|
4606
|
+
const link = await connectHttpLink(down, {
|
|
4607
|
+
streamTimeout,
|
|
4608
|
+
window
|
|
4609
|
+
});
|
|
4086
4610
|
const entry = new HandlerEntry(params.key, params.id ?? clientId ?? crypto.randomUUID(), params.owner ?? crypto.randomUUID(), params.meta, link, ctl, clientId, params.ownerLock);
|
|
4087
4611
|
if (up) entry.closeUp = await serveHttpLink(up, (request) => this.dispatch(request), { streamTimeout });
|
|
4088
4612
|
if (ctl) ctl.onmessage = (message) => this._onControl(entry, message.data);
|
|
@@ -4161,20 +4685,35 @@
|
|
|
4161
4685
|
* its key, waiting for one when there is none.
|
|
4162
4686
|
*/
|
|
4163
4687
|
async _acquire(request, key) {
|
|
4688
|
+
const deadline = Date.now() + (this.options.attachTimeoutMs ?? 15e3);
|
|
4164
4689
|
const picked = await this._pick(request, key);
|
|
4165
4690
|
if (picked) return picked;
|
|
4166
|
-
if (this.table.all(key).length
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
4691
|
+
if (this.table.all(key).length === 0) {
|
|
4692
|
+
const vacantUntil = this._vacant.get(key) ?? 0;
|
|
4693
|
+
if (vacantUntil > Date.now()) return await this._waitToPick(request, key, vacantUntil);
|
|
4694
|
+
if (!this.serves(key)) return void 0;
|
|
4695
|
+
await this._askForLinks(key);
|
|
4696
|
+
const relinked = await this._pick(request, key);
|
|
4697
|
+
if (relinked) return relinked;
|
|
4698
|
+
if (this.table.all(key).length === 0) return void 0;
|
|
4699
|
+
}
|
|
4700
|
+
return await this._waitToPick(request, key, deadline);
|
|
4701
|
+
}
|
|
4702
|
+
/** Waits for attaches until `route` accepts a handler, or `until` passes. */
|
|
4703
|
+
async _waitToPick(request, key, until) {
|
|
4704
|
+
for (;;) {
|
|
4705
|
+
const left = until - Date.now();
|
|
4706
|
+
if (left <= 0) return TIMED_OUT;
|
|
4707
|
+
await this.table.waitFor(key, Math.min(left, 1e3), this.table.live(key).length > 0);
|
|
4708
|
+
const picked = await this._pick(request, key);
|
|
4709
|
+
if (picked) return picked;
|
|
4710
|
+
}
|
|
4172
4711
|
}
|
|
4173
4712
|
async _pick(request, key) {
|
|
4174
4713
|
let live = this.table.live(key);
|
|
4175
4714
|
for (const entry of live) {
|
|
4176
4715
|
if (entry.ownerLock || !entry.clientId) continue;
|
|
4177
|
-
if (!await this.
|
|
4716
|
+
if (!await this._sw.clients.get(entry.clientId)) this._close(entry, new LinkLostError(HANDLER_LOST_MESSAGE));
|
|
4178
4717
|
}
|
|
4179
4718
|
live = this.table.live(key);
|
|
4180
4719
|
if (live.length === 0) return void 0;
|
|
@@ -4198,7 +4737,7 @@
|
|
|
4198
4737
|
}
|
|
4199
4738
|
async _linkRound(key) {
|
|
4200
4739
|
const timeout = this.options.relinkTimeoutMs ?? 3e3;
|
|
4201
|
-
const windows = await this.
|
|
4740
|
+
const windows = await this._sw.clients.matchAll({
|
|
4202
4741
|
type: "window",
|
|
4203
4742
|
includeUncontrolled: true
|
|
4204
4743
|
});
|
|
@@ -4236,7 +4775,7 @@
|
|
|
4236
4775
|
entry.sockets.fail(message.id, new Error(message.message));
|
|
4237
4776
|
return;
|
|
4238
4777
|
case "ws-open":
|
|
4239
|
-
this.connectWebSocket(message.url, message.protocols).then(({ port, protocol }) => entry.ctl?.postMessage({
|
|
4778
|
+
this.connectWebSocket(message.url, message.protocols, this._socketHeaders(message.headers, "handler")).then(({ port, protocol }) => entry.ctl?.postMessage({
|
|
4240
4779
|
t: "ws-opened",
|
|
4241
4780
|
id: message.id,
|
|
4242
4781
|
protocol,
|
|
@@ -4253,7 +4792,7 @@
|
|
|
4253
4792
|
const source = event.source;
|
|
4254
4793
|
if (!source?.url) return false;
|
|
4255
4794
|
try {
|
|
4256
|
-
return new URL(source.url).origin === this.
|
|
4795
|
+
return new URL(source.url).origin === this._sw.location.origin;
|
|
4257
4796
|
} catch {
|
|
4258
4797
|
return false;
|
|
4259
4798
|
}
|
|
@@ -4277,6 +4816,7 @@
|
|
|
4277
4816
|
* that stops it.
|
|
4278
4817
|
*/
|
|
4279
4818
|
function startHttpDispatcher(options) {
|
|
4819
|
+
if (options.handleFetch === false) throw new Error("startHttpDispatcher: handleFetch: false needs a dispatcher to call handleFetch on; construct it instead: new SwHttpDispatcher({ self, handleFetch: false }).start()");
|
|
4280
4820
|
const dispatcher = new SwHttpDispatcher(options);
|
|
4281
4821
|
dispatcher.start();
|
|
4282
4822
|
return () => {
|