@statewalker/webrun-http-browser 0.10.0 → 0.12.2
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 +834 -225
- 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 +433 -83
- package/dist/relay-worker.d.ts +1 -0
- package/dist/relay-worker.d.ts.map +1 -1
- package/dist/relay-worker.js +433 -83
- 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 +596 -132
- package/dist/sw.js +829 -223
- package/dist/worker.js +622 -120
- package/dist/ws/frames.d.ts +6 -1
- package/dist/ws/frames.d.ts.map +1 -1
- 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 +5 -4
- 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/frames.ts +18 -12
- 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
|
});
|
|
@@ -2251,6 +2252,99 @@
|
|
|
2251
2252
|
}
|
|
2252
2253
|
}
|
|
2253
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
|
+
/**
|
|
2254
2348
|
* Makes `target` disposable with `await using`: installs `dispose` as its
|
|
2255
2349
|
* `[Symbol.asyncDispose]`, where the runtime defines the symbol. Hand-written
|
|
2256
2350
|
* iterators lose what a native generator gets from `AsyncIteratorPrototype`;
|
|
@@ -2322,6 +2416,61 @@
|
|
|
2322
2416
|
* tests and adapters assert on it.
|
|
2323
2417
|
*/
|
|
2324
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
|
+
};
|
|
2325
2474
|
/** Caller's input travels on this channel; the handler listens on it. */
|
|
2326
2475
|
const CHANNEL_IN = "in";
|
|
2327
2476
|
/** The handler's output travels on this channel; the caller listens on it. */
|
|
@@ -2334,7 +2483,9 @@
|
|
|
2334
2483
|
* same way on the other channel. Within each direction the next chunk is never
|
|
2335
2484
|
* sent until the previous one has been delivered *and* pulled past by the
|
|
2336
2485
|
* consumer (spec D11) — the reply to a chunk call *is* the confirmation, and
|
|
2337
|
-
* `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.
|
|
2338
2489
|
*
|
|
2339
2490
|
* A stream port carries exactly one invocation. To make several calls, open
|
|
2340
2491
|
* several ports: `mux.openPort({ kind: "stream" })` per call.
|
|
@@ -2351,22 +2502,49 @@
|
|
|
2351
2502
|
const notice = installAbortNotice(port, controller);
|
|
2352
2503
|
abortOnPortClose(port, controller);
|
|
2353
2504
|
const clock = installStreamTimeout(controller, options.timeout);
|
|
2354
|
-
const inbound = receiveChunks(port, CHANNEL_IN, controller, clock.touch);
|
|
2505
|
+
const inbound = receiveChunks(port, CHANNEL_IN, controller, clock.touch, notice);
|
|
2355
2506
|
let output;
|
|
2356
2507
|
try {
|
|
2357
2508
|
output = handler(inbound.stream);
|
|
2358
2509
|
} catch (err) {
|
|
2359
|
-
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, () => {});
|
|
2360
2511
|
pump.catch(() => {});
|
|
2361
2512
|
disarmClockWhenBothSidesSettle(clock, pump, inbound.ended);
|
|
2362
2513
|
return teardownOnce(controller, notice, clock, inbound, void 0);
|
|
2363
2514
|
}
|
|
2364
|
-
|
|
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);
|
|
2365
2520
|
pump.catch(() => {});
|
|
2366
2521
|
disarmClockWhenBothSidesSettle(clock, pump, inbound.ended);
|
|
2522
|
+
endServedStream(port, sent, inbound.over);
|
|
2367
2523
|
return teardownOnce(controller, notice, clock, inbound, output);
|
|
2368
2524
|
}
|
|
2369
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
|
+
/**
|
|
2370
2548
|
* A stream that completes normally has nobody left to call the returned
|
|
2371
2549
|
* teardown — the caller only knows the stream ended, not that it must also
|
|
2372
2550
|
* dispose the serve-side handle. Without this, the last `touch()`'s timer
|
|
@@ -2406,22 +2584,31 @@
|
|
|
2406
2584
|
const notice = installAbortNotice(port, controller);
|
|
2407
2585
|
abortOnPortClose(port, controller);
|
|
2408
2586
|
const clock = installStreamTimeout(controller, options.timeout);
|
|
2409
|
-
const inbound = receiveChunks(port, CHANNEL_OUT, controller, clock.touch);
|
|
2410
|
-
|
|
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
|
+
});
|
|
2411
2592
|
pump.catch(() => {});
|
|
2412
2593
|
return returningAtOnce((async function* () {
|
|
2594
|
+
let received = false;
|
|
2413
2595
|
try {
|
|
2414
2596
|
yield* inbound.stream;
|
|
2597
|
+
received = true;
|
|
2415
2598
|
} finally {
|
|
2599
|
+
const ended = received && sent;
|
|
2416
2600
|
if (!controller.signal.aborted) controller.abort(/* @__PURE__ */ new Error("webrun-rpc: the caller abandoned the stream"));
|
|
2417
|
-
notice.post();
|
|
2601
|
+
if (!ended) notice.post();
|
|
2418
2602
|
notice.stop();
|
|
2419
2603
|
clock.stop();
|
|
2420
2604
|
inbound.stop();
|
|
2421
2605
|
await pump.catch(() => {});
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2606
|
+
if (!(ended && endPort(port))) {
|
|
2607
|
+
notice.post();
|
|
2608
|
+
try {
|
|
2609
|
+
await port.close?.();
|
|
2610
|
+
} catch {}
|
|
2611
|
+
}
|
|
2425
2612
|
}
|
|
2426
2613
|
})(), () => {
|
|
2427
2614
|
if (!controller.signal.aborted) controller.abort(/* @__PURE__ */ new Error("webrun-rpc: the caller abandoned the stream"));
|
|
@@ -2515,7 +2702,7 @@
|
|
|
2515
2702
|
* the local consumer has not started iterating, an inbound chunk waits for it —
|
|
2516
2703
|
* which is the correct backpressure, and different from having no listener.
|
|
2517
2704
|
*/
|
|
2518
|
-
function receiveChunks(port, channelName, controller, touch) {
|
|
2705
|
+
function receiveChunks(port, channelName, controller, touch, notice) {
|
|
2519
2706
|
let deliver;
|
|
2520
2707
|
const waiting = [];
|
|
2521
2708
|
let finished = false;
|
|
@@ -2525,25 +2712,64 @@
|
|
|
2525
2712
|
const ended = new Promise((resolve) => {
|
|
2526
2713
|
resolveEnded = resolve;
|
|
2527
2714
|
});
|
|
2715
|
+
let resolveOver = () => {};
|
|
2716
|
+
const over = new Promise((resolve) => {
|
|
2717
|
+
resolveOver = resolve;
|
|
2718
|
+
});
|
|
2528
2719
|
let aborted = false;
|
|
2529
2720
|
let abortReason;
|
|
2530
2721
|
const ready = () => deliver || finished ? Promise.resolve() : new Promise((r) => waiting.push(r));
|
|
2531
2722
|
const wake = () => {
|
|
2532
2723
|
for (const r of waiting.splice(0)) r();
|
|
2533
2724
|
};
|
|
2534
|
-
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);
|
|
2535
2769
|
touch();
|
|
2536
2770
|
if (poison) throw poison;
|
|
2537
|
-
if (
|
|
2538
|
-
|
|
2539
|
-
controller.abort(poison);
|
|
2540
|
-
setTimeout(() => {
|
|
2541
|
-
try {
|
|
2542
|
-
port.close?.();
|
|
2543
|
-
} catch {}
|
|
2544
|
-
}, 0);
|
|
2545
|
-
throw poison;
|
|
2546
|
-
}
|
|
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");
|
|
2547
2773
|
outstanding = true;
|
|
2548
2774
|
try {
|
|
2549
2775
|
await ready();
|
|
@@ -2553,15 +2779,60 @@
|
|
|
2553
2779
|
value,
|
|
2554
2780
|
error: error ? deserializeError(error) : void 0
|
|
2555
2781
|
});
|
|
2782
|
+
if (done && !error) resolveOver();
|
|
2556
2783
|
} finally {
|
|
2557
2784
|
outstanding = false;
|
|
2558
2785
|
}
|
|
2559
2786
|
if (done) resolveEnded();
|
|
2560
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);
|
|
2561
2831
|
const detach = () => {
|
|
2562
2832
|
finished = true;
|
|
2563
2833
|
wake();
|
|
2564
2834
|
off();
|
|
2835
|
+
port.removeEventListener("message", onChunk);
|
|
2565
2836
|
controller.signal.removeEventListener("abort", onAbort);
|
|
2566
2837
|
resolveEnded();
|
|
2567
2838
|
};
|
|
@@ -2591,25 +2862,35 @@
|
|
|
2591
2862
|
return detach;
|
|
2592
2863
|
}),
|
|
2593
2864
|
stop: detach,
|
|
2594
|
-
ended
|
|
2865
|
+
ended,
|
|
2866
|
+
over
|
|
2595
2867
|
};
|
|
2596
2868
|
}
|
|
2597
2869
|
/**
|
|
2598
2870
|
* The sending half of one direction: one `callPort` per chunk, one call
|
|
2599
|
-
* 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.
|
|
2600
2875
|
*/
|
|
2601
|
-
async function sendChunks(port, channelName, output, { maxMessageSize, log }, signal, touch) {
|
|
2876
|
+
async function sendChunks(port, channelName, output, { maxMessageSize, log }, signal, touch, onSent) {
|
|
2602
2877
|
const framed = maxMessageSize ? toChunks(maxMessageSize)(output) : output;
|
|
2603
2878
|
const stream = throughAbort(framed, signal);
|
|
2604
2879
|
const producer = output;
|
|
2605
2880
|
const returnProducer = () => void Promise.resolve(producer.return?.()).catch(() => {});
|
|
2606
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 });
|
|
2607
2888
|
try {
|
|
2608
2889
|
await sendIterator(async ({ done, value, error }) => {
|
|
2609
2890
|
if (signal.aborted) return;
|
|
2610
2891
|
const chunk = {
|
|
2611
2892
|
done,
|
|
2612
|
-
value,
|
|
2893
|
+
value: done ? value : chunkBytes(value),
|
|
2613
2894
|
error: error === void 0 ? void 0 : serializeError(error)
|
|
2614
2895
|
};
|
|
2615
2896
|
log?.("[duplexOverPort] send", {
|
|
@@ -2617,48 +2898,34 @@
|
|
|
2617
2898
|
done,
|
|
2618
2899
|
size: value?.byteLength
|
|
2619
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
|
+
}
|
|
2620
2912
|
await callPort(port, chunk, {
|
|
2621
2913
|
channelName,
|
|
2622
2914
|
timeout: NO_TIMEOUT,
|
|
2623
2915
|
signal
|
|
2624
2916
|
});
|
|
2625
2917
|
touch();
|
|
2918
|
+
if (done && !error) onSent();
|
|
2626
2919
|
}, stream);
|
|
2627
2920
|
} catch (err) {
|
|
2628
2921
|
if (signal.aborted) return;
|
|
2629
2922
|
throw err;
|
|
2630
2923
|
} finally {
|
|
2631
2924
|
signal.removeEventListener("abort", returnProducer);
|
|
2925
|
+
signal.removeEventListener("abort", onAborted);
|
|
2632
2926
|
}
|
|
2633
2927
|
}
|
|
2634
2928
|
/**
|
|
2635
|
-
* Runs `fn` on a later task, never a microtask: a producer looping on
|
|
2636
|
-
* `whenPortReady` must let the transport's own events run. `setImmediate`
|
|
2637
|
-
* where it exists; elsewhere a `MessageChannel`, because nested `setTimeout`s
|
|
2638
|
-
* are clamped to 4 ms in browsers.
|
|
2639
|
-
*/
|
|
2640
|
-
const setImmediateFn = globalThis.setImmediate;
|
|
2641
|
-
/** @internal */
|
|
2642
|
-
const nextTask = setImmediateFn ? (fn) => {
|
|
2643
|
-
setImmediateFn(fn);
|
|
2644
|
-
} : (() => {
|
|
2645
|
-
let channel;
|
|
2646
|
-
const queue = [];
|
|
2647
|
-
return (fn) => {
|
|
2648
|
-
if (!channel) {
|
|
2649
|
-
channel = new MessageChannel();
|
|
2650
|
-
channel.port1.onmessage = () => queue.shift()?.();
|
|
2651
|
-
}
|
|
2652
|
-
queue.push(fn);
|
|
2653
|
-
channel.port2.postMessage(0);
|
|
2654
|
-
};
|
|
2655
|
-
})();
|
|
2656
|
-
const states = /* @__PURE__ */ new WeakMap();
|
|
2657
|
-
/** @internal Registered by the multiplexer for each port with a window. */
|
|
2658
|
-
function setPortSendState(port, state) {
|
|
2659
|
-
states.set(port, state);
|
|
2660
|
-
}
|
|
2661
|
-
/**
|
|
2662
2929
|
* One virtual port.
|
|
2663
2930
|
*
|
|
2664
2931
|
* `deliver` and `markClosed` are deliberately not on `port`: the consumer holds
|
|
@@ -2736,6 +3003,21 @@
|
|
|
2736
3003
|
const KEEPALIVE_ID = Number.MAX_SAFE_INTEGER;
|
|
2737
3004
|
/** The longest delay a timer holds; past it `setInterval` fires at once. */
|
|
2738
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
|
+
}
|
|
2739
3021
|
/** Ceiling on concurrently open virtual ports. Bounds the id table only. */
|
|
2740
3022
|
const DEFAULT_MAX_PORTS = 1024;
|
|
2741
3023
|
/**
|
|
@@ -2799,6 +3081,8 @@
|
|
|
2799
3081
|
let muxClosed = false;
|
|
2800
3082
|
/** This end counts encoded bytes and reassembles frames. */
|
|
2801
3083
|
const exactCodec = codec.encode != null && codec.decode != null;
|
|
3084
|
+
/** What this end offers on a windowed port. */
|
|
3085
|
+
const caps = ownCaps(codec);
|
|
2802
3086
|
const post = (envelope, transfer) => {
|
|
2803
3087
|
if (muxClosed) return;
|
|
2804
3088
|
codecPost(envelope, transfer);
|
|
@@ -2856,13 +3140,39 @@
|
|
|
2856
3140
|
port.postMessage(bytes);
|
|
2857
3141
|
} catch {}
|
|
2858
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
|
+
};
|
|
2859
3169
|
const releaseWaiters = (c) => {
|
|
2860
3170
|
for (const w of c?.waiters.splice(0) ?? []) w.resolve();
|
|
2861
3171
|
};
|
|
2862
3172
|
const flush = (c) => {
|
|
2863
3173
|
while (c.queue.length) {
|
|
2864
3174
|
const next = c.queue[0];
|
|
2865
|
-
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;
|
|
2866
3176
|
c.queue.shift();
|
|
2867
3177
|
c.queued -= next.size;
|
|
2868
3178
|
c.outstanding += next.size;
|
|
@@ -2900,15 +3210,20 @@
|
|
|
2900
3210
|
enforced: false,
|
|
2901
3211
|
opener,
|
|
2902
3212
|
exact,
|
|
3213
|
+
caps: /* @__PURE__ */ new Set(),
|
|
2903
3214
|
outstanding: 0,
|
|
2904
3215
|
queue: [],
|
|
2905
3216
|
queued: 0,
|
|
2906
3217
|
unconsumed: 0,
|
|
2907
3218
|
waiters: [],
|
|
2908
3219
|
toGrant: 0,
|
|
2909
|
-
grantScheduled: false
|
|
3220
|
+
grantScheduled: false,
|
|
3221
|
+
ending: false,
|
|
3222
|
+
endSent: false,
|
|
3223
|
+
peerEnded: false
|
|
2910
3224
|
} : void 0;
|
|
2911
3225
|
const self = newVirtualPort((payload, transfer) => {
|
|
3226
|
+
if (c?.ending) return;
|
|
2912
3227
|
touch(id);
|
|
2913
3228
|
const envelope = {
|
|
2914
3229
|
type: "message",
|
|
@@ -2995,6 +3310,38 @@
|
|
|
2995
3310
|
});
|
|
2996
3311
|
})
|
|
2997
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
|
+
}));
|
|
2998
3345
|
}
|
|
2999
3346
|
const record = {
|
|
3000
3347
|
deliver: self.deliver,
|
|
@@ -3046,16 +3393,24 @@
|
|
|
3046
3393
|
}
|
|
3047
3394
|
const handle = attach(envelope.id, envelope.meta, envelope.window, false, envelope.exact === true && exactCodec);
|
|
3048
3395
|
const credit = open.get(envelope.id)?.credit;
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
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
|
+
}
|
|
3059
3414
|
let accepted = false;
|
|
3060
3415
|
if (onPort) try {
|
|
3061
3416
|
accepted = onPort(handle.port, envelope.meta) !== false;
|
|
@@ -3083,6 +3438,7 @@
|
|
|
3083
3438
|
c.exact = false;
|
|
3084
3439
|
c.outstanding = 0;
|
|
3085
3440
|
}
|
|
3441
|
+
c.caps = sharedCaps(caps, envelope.caps) ?? /* @__PURE__ */ new Set();
|
|
3086
3442
|
post({
|
|
3087
3443
|
type: "credit",
|
|
3088
3444
|
id: envelope.id,
|
|
@@ -3094,6 +3450,10 @@
|
|
|
3094
3450
|
flush(c);
|
|
3095
3451
|
return;
|
|
3096
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
|
+
}
|
|
3097
3457
|
if (envelope.type === "message") {
|
|
3098
3458
|
const sizeOf = (c) => (c.exact ? byteLengthOf(event.data) : void 0) ?? estimateMessageSize(envelope.payload);
|
|
3099
3459
|
if (!entry) return;
|
|
@@ -3118,16 +3478,7 @@
|
|
|
3118
3478
|
const charged = partial.room == null ? Math.max(0, partial.bytes + size - c.window) : 0;
|
|
3119
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;
|
|
3120
3480
|
if (overrun) {
|
|
3121
|
-
|
|
3122
|
-
dropOutgoing(envelope.id);
|
|
3123
|
-
open.delete(envelope.id);
|
|
3124
|
-
releaseWaiters(c);
|
|
3125
|
-
entry.handle.markClosed(/* @__PURE__ */ new RangeError(`webrun-rpc: a framed message passed ${overrun}`));
|
|
3126
|
-
post({
|
|
3127
|
-
type: "close",
|
|
3128
|
-
id: envelope.id,
|
|
3129
|
-
reason: "max-reassembled-bytes"
|
|
3130
|
-
});
|
|
3481
|
+
failPort(envelope.id, entry, /* @__PURE__ */ new RangeError(`webrun-rpc: a framed message passed ${overrun}`), "max-reassembled-bytes");
|
|
3131
3482
|
return;
|
|
3132
3483
|
}
|
|
3133
3484
|
partial.bytes += size;
|
|
@@ -3147,6 +3498,19 @@
|
|
|
3147
3498
|
entry.handle.deliver(inner.payload, whole.byteLength);
|
|
3148
3499
|
return;
|
|
3149
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
|
+
}
|
|
3150
3514
|
dropOutgoing(envelope.id);
|
|
3151
3515
|
if (!entry) return;
|
|
3152
3516
|
freePartial(entry);
|
|
@@ -3228,12 +3592,14 @@
|
|
|
3228
3592
|
id,
|
|
3229
3593
|
meta,
|
|
3230
3594
|
window,
|
|
3231
|
-
exact: true
|
|
3595
|
+
exact: true,
|
|
3596
|
+
caps: [...caps]
|
|
3232
3597
|
} : {
|
|
3233
3598
|
type: "open",
|
|
3234
3599
|
id,
|
|
3235
3600
|
meta,
|
|
3236
|
-
window
|
|
3601
|
+
window,
|
|
3602
|
+
caps: [...caps]
|
|
3237
3603
|
} : {
|
|
3238
3604
|
type: "open",
|
|
3239
3605
|
id,
|
|
@@ -3388,7 +3754,7 @@
|
|
|
3388
3754
|
const candidate = value;
|
|
3389
3755
|
if (typeof candidate.id !== "number") return false;
|
|
3390
3756
|
if (!Number.isInteger(candidate.id) || candidate.id < 0) return false;
|
|
3391
|
-
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";
|
|
3392
3758
|
}
|
|
3393
3759
|
/**
|
|
3394
3760
|
* For ports whose messages are structured values — a real `MessagePort`, a
|
|
@@ -3400,6 +3766,7 @@
|
|
|
3400
3766
|
* not send anything a byte codec could not also carry.
|
|
3401
3767
|
*/
|
|
3402
3768
|
const structuredCodec = {
|
|
3769
|
+
halfClose: true,
|
|
3403
3770
|
post(port, envelope, transfer) {
|
|
3404
3771
|
if (transfer && transfer.length > 0) port.postMessage(envelope, transfer);
|
|
3405
3772
|
else port.postMessage(envelope);
|
|
@@ -3437,10 +3804,15 @@
|
|
|
3437
3804
|
* own `handleStreams` / `sendStream` (no backpressure); version 2 is
|
|
3438
3805
|
* webrun-rpc's `connect` / `serve` over `overPipe` with webrun-http-streams'
|
|
3439
3806
|
* `fetchOverDuplex` / `serveFetchOverDuplex`: one port per link, calls
|
|
3440
|
-
* multiplexed on it, each call a stream with
|
|
3441
|
-
* 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.
|
|
3442
3812
|
*/
|
|
3443
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;
|
|
3444
3816
|
/**
|
|
3445
3817
|
* Why a request failed when its link went away under it: the far side (the
|
|
3446
3818
|
* handler, or the ServiceWorker) was lost. The request may or may not have
|
|
@@ -3459,12 +3831,13 @@
|
|
|
3459
3831
|
*/
|
|
3460
3832
|
const LINK_CLOSE = "webrun-http:link-close";
|
|
3461
3833
|
/** Opens the calling end of a link on `port` (one port per link). */
|
|
3462
|
-
async function connectHttpLink(port, { streamTimeout, maxPorts } = {}) {
|
|
3834
|
+
async function connectHttpLink(port, { streamTimeout, maxPorts, window = DEFAULT_HTTP_LINK_WINDOW } = {}) {
|
|
3463
3835
|
const { call, close } = await connect({
|
|
3464
3836
|
mux: overPipe(port, {
|
|
3465
3837
|
codec: structuredCodec,
|
|
3466
3838
|
side: "initiator",
|
|
3467
|
-
maxPorts
|
|
3839
|
+
maxPorts,
|
|
3840
|
+
window
|
|
3468
3841
|
}),
|
|
3469
3842
|
timeout: streamTimeout
|
|
3470
3843
|
});
|
|
@@ -3775,6 +4148,7 @@
|
|
|
3775
4148
|
all.set("connection", "Upgrade");
|
|
3776
4149
|
all.set("sec-websocket-version", "13");
|
|
3777
4150
|
if (!all.has("sec-websocket-key")) all.set("sec-websocket-key", randomKey());
|
|
4151
|
+
all.delete("sec-websocket-protocol");
|
|
3778
4152
|
if (protocols.length > 0) all.set("sec-websocket-protocol", protocols.join(", "));
|
|
3779
4153
|
if (!all.has("x-webrun-ws-version")) all.set(WS_VERSION_HEADER, `2`);
|
|
3780
4154
|
return withHeaders(new Request(httpUrl, {
|
|
@@ -3907,6 +4281,21 @@
|
|
|
3907
4281
|
const NEED_LINKS_CALL = "webrun-http:need-links";
|
|
3908
4282
|
//#endregion
|
|
3909
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
|
+
}
|
|
3910
4299
|
/** One attached handler link, as the ServiceWorker holds it. */
|
|
3911
4300
|
var HandlerEntry = class {
|
|
3912
4301
|
key;
|
|
@@ -3946,7 +4335,8 @@
|
|
|
3946
4335
|
* (`SwHttpAdapter.register`) or in Dedicated Workers
|
|
3947
4336
|
* (`SwHttpAdapter.attachWorker` + `serveHttpInWorker`).
|
|
3948
4337
|
*
|
|
3949
|
-
* 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
|
|
3950
4340
|
* `route` option picks one per request. Requests and responses stream with
|
|
3951
4341
|
* backpressure in both directions, and an aborted fetch reaches the handler
|
|
3952
4342
|
* as a cancellation (webrun-rpc + webrun-http-streams).
|
|
@@ -3965,6 +4355,8 @@
|
|
|
3965
4355
|
*/
|
|
3966
4356
|
var SwHttpDispatcher = class {
|
|
3967
4357
|
self;
|
|
4358
|
+
/** `self`, with the WebWorker lib's types this module compiles with. */
|
|
4359
|
+
_sw;
|
|
3968
4360
|
log;
|
|
3969
4361
|
options;
|
|
3970
4362
|
table = new HandlerTable();
|
|
@@ -3987,19 +4379,22 @@
|
|
|
3987
4379
|
constructor(options) {
|
|
3988
4380
|
this.options = options;
|
|
3989
4381
|
this.self = options.self;
|
|
4382
|
+
this._sw = options.self;
|
|
3990
4383
|
this.log = options.log ?? (() => {});
|
|
3991
4384
|
if (locksAvailable()) this.routerLock = newLockName("webrun-http-router");
|
|
3992
4385
|
}
|
|
3993
4386
|
get scope() {
|
|
3994
|
-
return `${new URL("./", this.
|
|
4387
|
+
return this._sw.registration?.scope ?? `${new URL("./", this._sw.location.href)}`;
|
|
3995
4388
|
}
|
|
3996
|
-
/** The key `url` belongs to
|
|
4389
|
+
/** The key `url` belongs to (the `key` option; by default its first path segment under the scope). */
|
|
3997
4390
|
keyOf(url) {
|
|
3998
|
-
|
|
3999
|
-
|
|
4000
|
-
|
|
4001
|
-
|
|
4002
|
-
|
|
4391
|
+
let parsed;
|
|
4392
|
+
try {
|
|
4393
|
+
parsed = new URL(`${url}`);
|
|
4394
|
+
} catch {
|
|
4395
|
+
return;
|
|
4396
|
+
}
|
|
4397
|
+
return (this.options.key ?? scopeKey)(parsed, this.scope);
|
|
4003
4398
|
}
|
|
4004
4399
|
/** Whether `key` has ever been served on this worker. */
|
|
4005
4400
|
isClaimedKey(key) {
|
|
@@ -4010,7 +4405,7 @@
|
|
|
4010
4405
|
return this.claimedKeys.has(key) || this.table.all(key).length > 0;
|
|
4011
4406
|
}
|
|
4012
4407
|
start() {
|
|
4013
|
-
const self = this.
|
|
4408
|
+
const self = this._sw;
|
|
4014
4409
|
if (this.routerLock) this._routerHeld = holdLock(this.routerLock).then(() => {});
|
|
4015
4410
|
this._restored = this._restoreClaimedKeys();
|
|
4016
4411
|
const latch = () => {
|
|
@@ -4030,8 +4425,8 @@
|
|
|
4030
4425
|
return true;
|
|
4031
4426
|
}), handleChannelCalls(self, WS_CONNECT_CALL, async (event, params) => {
|
|
4032
4427
|
if (!this._isOwnClient(event)) throw new Error("Only this origin's pages may open sockets");
|
|
4033
|
-
const { url, protocols } = params;
|
|
4034
|
-
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"));
|
|
4035
4430
|
return withTransfer(connection, [connection.port]);
|
|
4036
4431
|
}));
|
|
4037
4432
|
const onMessage = (event) => {
|
|
@@ -4049,10 +4444,10 @@
|
|
|
4049
4444
|
this.log("Claim control over all clients.");
|
|
4050
4445
|
event.waitUntil(self.clients.claim());
|
|
4051
4446
|
};
|
|
4052
|
-
const onFetch = (event) => this.
|
|
4447
|
+
const onFetch = (event) => void this.handleFetch(event);
|
|
4053
4448
|
self.addEventListener("install", onInstall);
|
|
4054
4449
|
self.addEventListener("activate", onActivate);
|
|
4055
|
-
self.addEventListener("fetch", onFetch);
|
|
4450
|
+
if (this.options.handleFetch !== false) self.addEventListener("fetch", onFetch);
|
|
4056
4451
|
this._cleanup.push(() => {
|
|
4057
4452
|
self.removeEventListener("install", onInstall);
|
|
4058
4453
|
self.removeEventListener("activate", onActivate);
|
|
@@ -4067,37 +4462,60 @@
|
|
|
4067
4462
|
}
|
|
4068
4463
|
/**
|
|
4069
4464
|
* Answers `request` as a fetch under the scope would be: through the
|
|
4070
|
-
* handler its key routes to. A url
|
|
4071
|
-
* 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.
|
|
4072
4467
|
*/
|
|
4073
4468
|
async dispatch(request) {
|
|
4074
4469
|
const key = this.keyOf(request.url);
|
|
4075
4470
|
if (key === void 0) return await fetch(request);
|
|
4076
4471
|
await this._restored.catch(() => {});
|
|
4077
4472
|
const entry = await this._acquire(request, key);
|
|
4078
|
-
if (entry === TIMED_OUT) return
|
|
4079
|
-
status: 503,
|
|
4080
|
-
statusText: "Service Unavailable (no handler attached in time)"
|
|
4081
|
-
});
|
|
4473
|
+
if (entry === TIMED_OUT) return await this._failed(request, { kind: "timeout" });
|
|
4082
4474
|
if (!entry) {
|
|
4083
4475
|
if (!this.serves(key)) return await fetch(request);
|
|
4084
|
-
return
|
|
4085
|
-
status: 404,
|
|
4086
|
-
statusText: "Not Found (no active handler)"
|
|
4087
|
-
});
|
|
4476
|
+
return await this._failed(request, { kind: "gone" });
|
|
4088
4477
|
}
|
|
4089
4478
|
try {
|
|
4090
4479
|
return await entry.link.send(request);
|
|
4091
4480
|
} catch (error) {
|
|
4092
4481
|
this.log("[dispatch] failed", key, error);
|
|
4093
|
-
return
|
|
4094
|
-
|
|
4095
|
-
|
|
4096
|
-
headers: { "content-type": "text/plain; charset=utf-8" }
|
|
4482
|
+
return await this._failed(request, {
|
|
4483
|
+
kind: "lost",
|
|
4484
|
+
error
|
|
4097
4485
|
});
|
|
4098
4486
|
}
|
|
4099
4487
|
}
|
|
4100
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
|
+
/**
|
|
4101
4519
|
* Opens a socket to the handler `url` routes to: the handler accepts or
|
|
4102
4520
|
* refuses it like an HTTP upgrade, and the socket's port comes back. After
|
|
4103
4521
|
* that the socket runs between the caller and the handler directly; this
|
|
@@ -4109,7 +4527,7 @@
|
|
|
4109
4527
|
headers
|
|
4110
4528
|
});
|
|
4111
4529
|
const key = this.keyOf(request.url);
|
|
4112
|
-
if (key === void 0) throw new Error(`${url}
|
|
4530
|
+
if (key === void 0) throw new Error(`${url} belongs to no key of this ServiceWorker`);
|
|
4113
4531
|
await this._restored.catch(() => {});
|
|
4114
4532
|
const entry = await this._acquire(request, key);
|
|
4115
4533
|
if (!entry || entry === TIMED_OUT) throw new Error(`No handler serves ${url}`);
|
|
@@ -4125,20 +4543,47 @@
|
|
|
4125
4543
|
});
|
|
4126
4544
|
return await answer;
|
|
4127
4545
|
}
|
|
4128
|
-
|
|
4129
|
-
|
|
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;
|
|
4130
4564
|
const key = this.keyOf(request.url);
|
|
4131
|
-
if (key === void 0) return;
|
|
4565
|
+
if (key === void 0) return false;
|
|
4132
4566
|
if (this._restoreSettled) {
|
|
4133
|
-
if (this.serves(key))
|
|
4134
|
-
|
|
4135
|
-
|
|
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;
|
|
4136
4576
|
}
|
|
4137
4577
|
event.respondWith((async () => {
|
|
4138
4578
|
await this._restored.catch(() => {});
|
|
4139
|
-
if (
|
|
4140
|
-
return await this.
|
|
4579
|
+
if (this.serves(key)) return await this.dispatch(request);
|
|
4580
|
+
return navigation ? await this._unserved(request) : await fetch(request);
|
|
4141
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);
|
|
4142
4587
|
}
|
|
4143
4588
|
async _attach(event, params, ports) {
|
|
4144
4589
|
const clientId = event.source?.id;
|
|
@@ -4157,8 +4602,11 @@
|
|
|
4157
4602
|
}
|
|
4158
4603
|
const ctl = params.ctl ? rest.shift() : void 0;
|
|
4159
4604
|
const up = params.up ? rest.shift() : void 0;
|
|
4160
|
-
const streamTimeout = this.options
|
|
4161
|
-
const link = await connectHttpLink(down, {
|
|
4605
|
+
const { streamTimeout, window } = this.options;
|
|
4606
|
+
const link = await connectHttpLink(down, {
|
|
4607
|
+
streamTimeout,
|
|
4608
|
+
window
|
|
4609
|
+
});
|
|
4162
4610
|
const entry = new HandlerEntry(params.key, params.id ?? clientId ?? crypto.randomUUID(), params.owner ?? crypto.randomUUID(), params.meta, link, ctl, clientId, params.ownerLock);
|
|
4163
4611
|
if (up) entry.closeUp = await serveHttpLink(up, (request) => this.dispatch(request), { streamTimeout });
|
|
4164
4612
|
if (ctl) ctl.onmessage = (message) => this._onControl(entry, message.data);
|
|
@@ -4237,20 +4685,35 @@
|
|
|
4237
4685
|
* its key, waiting for one when there is none.
|
|
4238
4686
|
*/
|
|
4239
4687
|
async _acquire(request, key) {
|
|
4688
|
+
const deadline = Date.now() + (this.options.attachTimeoutMs ?? 15e3);
|
|
4240
4689
|
const picked = await this._pick(request, key);
|
|
4241
4690
|
if (picked) return picked;
|
|
4242
|
-
if (this.table.all(key).length
|
|
4243
|
-
|
|
4244
|
-
|
|
4245
|
-
|
|
4246
|
-
|
|
4247
|
-
|
|
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
|
+
}
|
|
4248
4711
|
}
|
|
4249
4712
|
async _pick(request, key) {
|
|
4250
4713
|
let live = this.table.live(key);
|
|
4251
4714
|
for (const entry of live) {
|
|
4252
4715
|
if (entry.ownerLock || !entry.clientId) continue;
|
|
4253
|
-
if (!await this.
|
|
4716
|
+
if (!await this._sw.clients.get(entry.clientId)) this._close(entry, new LinkLostError(HANDLER_LOST_MESSAGE));
|
|
4254
4717
|
}
|
|
4255
4718
|
live = this.table.live(key);
|
|
4256
4719
|
if (live.length === 0) return void 0;
|
|
@@ -4274,7 +4737,7 @@
|
|
|
4274
4737
|
}
|
|
4275
4738
|
async _linkRound(key) {
|
|
4276
4739
|
const timeout = this.options.relinkTimeoutMs ?? 3e3;
|
|
4277
|
-
const windows = await this.
|
|
4740
|
+
const windows = await this._sw.clients.matchAll({
|
|
4278
4741
|
type: "window",
|
|
4279
4742
|
includeUncontrolled: true
|
|
4280
4743
|
});
|
|
@@ -4312,7 +4775,7 @@
|
|
|
4312
4775
|
entry.sockets.fail(message.id, new Error(message.message));
|
|
4313
4776
|
return;
|
|
4314
4777
|
case "ws-open":
|
|
4315
|
-
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({
|
|
4316
4779
|
t: "ws-opened",
|
|
4317
4780
|
id: message.id,
|
|
4318
4781
|
protocol,
|
|
@@ -4329,7 +4792,7 @@
|
|
|
4329
4792
|
const source = event.source;
|
|
4330
4793
|
if (!source?.url) return false;
|
|
4331
4794
|
try {
|
|
4332
|
-
return new URL(source.url).origin === this.
|
|
4795
|
+
return new URL(source.url).origin === this._sw.location.origin;
|
|
4333
4796
|
} catch {
|
|
4334
4797
|
return false;
|
|
4335
4798
|
}
|
|
@@ -4353,6 +4816,7 @@
|
|
|
4353
4816
|
* that stops it.
|
|
4354
4817
|
*/
|
|
4355
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()");
|
|
4356
4820
|
const dispatcher = new SwHttpDispatcher(options);
|
|
4357
4821
|
dispatcher.start();
|
|
4358
4822
|
return () => {
|