@statewalker/webrun-http-browser 0.7.1 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +716 -84
- package/dist/relay-sw.js +647 -90
- package/dist/relay-worker.js +647 -90
- package/dist/sw/protocol.d.ts +2 -0
- package/dist/sw/protocol.d.ts.map +1 -1
- package/dist/sw/sw-http-adapter.d.ts +2 -0
- package/dist/sw/sw-http-adapter.d.ts.map +1 -1
- package/dist/sw/worker-service.d.ts.map +1 -1
- package/dist/sw-worker.js +695 -81
- package/dist/sw.js +709 -83
- package/dist/worker.js +704 -82
- package/package.json +3 -3
- package/src/link/http-link.ts +42 -6
- package/src/sw/protocol.ts +4 -3
- package/src/sw/sw-http-adapter.ts +7 -0
- package/src/sw/worker-service.ts +14 -1
package/dist/index.js
CHANGED
|
@@ -2126,6 +2126,12 @@ function asyncDisposable$2(target, dispose) {
|
|
|
2126
2126
|
}
|
|
2127
2127
|
/** Carries the serialized JS error between two webrun peers. */
|
|
2128
2128
|
const PEER_ERROR_HEADER = "x-webrun-error";
|
|
2129
|
+
/**
|
|
2130
|
+
* The reason of `HttpExchange.signal` when the exchange is abandoned with no
|
|
2131
|
+
* error of its own (`return()`); one abandoned by `throw(error)` aborts with
|
|
2132
|
+
* that error.
|
|
2133
|
+
*/
|
|
2134
|
+
const EXCHANGE_ABANDONED_MESSAGE = "webrun-http-streams: the exchange was abandoned before it was over";
|
|
2129
2135
|
/** Hard cap on the peer-error header (M3): see `encodeErrorResponse`. */
|
|
2130
2136
|
const MAX_DETAIL_CHARS = 4096;
|
|
2131
2137
|
/**
|
|
@@ -2306,7 +2312,11 @@ async function drain(output) {
|
|
|
2306
2312
|
*/
|
|
2307
2313
|
function httpServe(handler, options = {}) {
|
|
2308
2314
|
const codec = options.codec ?? defaultCodec;
|
|
2309
|
-
return
|
|
2315
|
+
return (input) => {
|
|
2316
|
+
const abandoned = new AbortController();
|
|
2317
|
+
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")));
|
|
2318
|
+
};
|
|
2319
|
+
async function* exchange(input, context) {
|
|
2310
2320
|
let decoded;
|
|
2311
2321
|
try {
|
|
2312
2322
|
decoded = await codec.decodeRequest(input);
|
|
@@ -2318,7 +2328,7 @@ function httpServe(handler, options = {}) {
|
|
|
2318
2328
|
const replyCodec = decoded.codec ?? codec;
|
|
2319
2329
|
let result;
|
|
2320
2330
|
try {
|
|
2321
|
-
result = await handler(decoded.envelope, decoded.body);
|
|
2331
|
+
result = await handler(decoded.envelope, decoded.body, context);
|
|
2322
2332
|
} catch (error) {
|
|
2323
2333
|
yield* encodeErrorResponse(replyCodec, error, decoded.envelope.method);
|
|
2324
2334
|
return;
|
|
@@ -2333,7 +2343,36 @@ function httpServe(handler, options = {}) {
|
|
|
2333
2343
|
await result.release?.();
|
|
2334
2344
|
} catch {}
|
|
2335
2345
|
}
|
|
2346
|
+
}
|
|
2347
|
+
}
|
|
2348
|
+
/**
|
|
2349
|
+
* `gen`, except that `return()` and `throw()` from its consumer call `abandon`
|
|
2350
|
+
* at once — before they are queued behind a `next()` still parked inside the
|
|
2351
|
+
* generator (the handler's `await`) — unless the generator already finished.
|
|
2352
|
+
*/
|
|
2353
|
+
function abortingOnReturn(gen, abandon) {
|
|
2354
|
+
let finished = false;
|
|
2355
|
+
const settle = (r) => {
|
|
2356
|
+
if (r.done) finished = true;
|
|
2357
|
+
return r;
|
|
2358
|
+
};
|
|
2359
|
+
const wrapped = {
|
|
2360
|
+
next: (...args) => gen.next(...args).then(settle),
|
|
2361
|
+
return(value) {
|
|
2362
|
+
if (!finished) abandon();
|
|
2363
|
+
finished = true;
|
|
2364
|
+
return gen.return(value);
|
|
2365
|
+
},
|
|
2366
|
+
throw(error) {
|
|
2367
|
+
if (!finished) abandon(error);
|
|
2368
|
+
finished = true;
|
|
2369
|
+
return gen.throw(error);
|
|
2370
|
+
},
|
|
2371
|
+
[Symbol.asyncIterator]() {
|
|
2372
|
+
return wrapped;
|
|
2373
|
+
}
|
|
2336
2374
|
};
|
|
2375
|
+
return asyncDisposable$2(wrapped, () => wrapped.return(void 0));
|
|
2337
2376
|
}
|
|
2338
2377
|
/**
|
|
2339
2378
|
* The one place that knows a runtime may not implement request body streams,
|
|
@@ -2598,17 +2637,18 @@ async function fetchOverDuplex(call, request, options = {}) {
|
|
|
2598
2637
|
else await release?.();
|
|
2599
2638
|
return new Response(null, responseInit);
|
|
2600
2639
|
}
|
|
2601
|
-
return new Response(
|
|
2640
|
+
return new Response(responseBody(respBody, request.signal, release), responseInit);
|
|
2602
2641
|
}
|
|
2603
2642
|
/**
|
|
2604
2643
|
* Wrap a `(Request) => Promise<Response>` handler as a `Duplex` so it can be
|
|
2605
2644
|
* registered with any `webrun-streams-*` adapter's `serve`.
|
|
2606
2645
|
*/
|
|
2607
2646
|
function serveFetchOverDuplex(handler, options = {}) {
|
|
2608
|
-
return httpServe(async (env, body) => {
|
|
2647
|
+
return httpServe(async (env, body, exchange) => {
|
|
2609
2648
|
const reqInit = {
|
|
2610
2649
|
method: env.method,
|
|
2611
|
-
headers: forwardableHeaders(env.headers)
|
|
2650
|
+
headers: forwardableHeaders(env.headers),
|
|
2651
|
+
signal: exchange?.signal
|
|
2612
2652
|
};
|
|
2613
2653
|
let upload;
|
|
2614
2654
|
if (env.method !== "GET" && env.method !== "HEAD") {
|
|
@@ -2729,15 +2769,78 @@ function trackingEnd(source) {
|
|
|
2729
2769
|
settle
|
|
2730
2770
|
};
|
|
2731
2771
|
}
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2772
|
+
/**
|
|
2773
|
+
* How long a cancelled body waits for the read in flight before it abandons the call anyway.
|
|
2774
|
+
* The read's answer tells a body that has ended (the call is finished: an upload the handler
|
|
2775
|
+
* reads after answering goes on) from one that has not; a peer that sends nothing more never
|
|
2776
|
+
* answers it. A body whose end comes later than this after the cancel is abandoned too, so a
|
|
2777
|
+
* handler still streaming its response and still reading the upload then loses the upload.
|
|
2778
|
+
*/
|
|
2779
|
+
const CANCEL_GRACE_MS = 1e3;
|
|
2780
|
+
/**
|
|
2781
|
+
* The response body as a stream that the request's signal can end at any time.
|
|
2782
|
+
*
|
|
2783
|
+
* An abort errors the stream with the abort's reason at once — a read waiting
|
|
2784
|
+
* for a peer that sends nothing more rejects then, not when a next chunk
|
|
2785
|
+
* arrives, which may be never — and abandons the call (`release`), so on a
|
|
2786
|
+
* multiplexed transport its port is given back whether or not anyone still
|
|
2787
|
+
* reads the body. The listener is removed once the body ends, however it ends:
|
|
2788
|
+
* read to its end, cancelled, failed or aborted. A body nobody reads or cancels
|
|
2789
|
+
* keeps it until the signal aborts (and keeps the call open until then anyway).
|
|
2790
|
+
* A cancel abandons the call too — unless the read in flight comes back with the
|
|
2791
|
+
* body's end, which finishes it instead (see `cancel`).
|
|
2792
|
+
*/
|
|
2793
|
+
function responseBody(body, signal, release) {
|
|
2794
|
+
const it = body[Symbol.asyncIterator]();
|
|
2795
|
+
let controller;
|
|
2796
|
+
let ended = false;
|
|
2797
|
+
let reading = false;
|
|
2798
|
+
const end = () => {
|
|
2799
|
+
ended = true;
|
|
2800
|
+
signal?.removeEventListener("abort", onAbort);
|
|
2801
|
+
};
|
|
2802
|
+
const onAbort = () => {
|
|
2803
|
+
if (ended) return;
|
|
2804
|
+
end();
|
|
2805
|
+
try {
|
|
2806
|
+
controller.error(abortReason$1(signal));
|
|
2807
|
+
} catch {}
|
|
2808
|
+
release?.();
|
|
2809
|
+
};
|
|
2810
|
+
return new ReadableStream({
|
|
2811
|
+
start(c) {
|
|
2812
|
+
controller = c;
|
|
2813
|
+
if (signal?.aborted) onAbort();
|
|
2814
|
+
else signal?.addEventListener("abort", onAbort, { once: true });
|
|
2815
|
+
},
|
|
2816
|
+
async pull(c) {
|
|
2817
|
+
try {
|
|
2818
|
+
reading = true;
|
|
2819
|
+
const { done, value } = await it.next().finally(() => {
|
|
2820
|
+
reading = false;
|
|
2821
|
+
});
|
|
2822
|
+
if (ended) return;
|
|
2823
|
+
if (done) {
|
|
2824
|
+
end();
|
|
2825
|
+
c.close();
|
|
2826
|
+
return;
|
|
2827
|
+
}
|
|
2828
|
+
c.enqueue(value);
|
|
2829
|
+
} catch (err) {
|
|
2830
|
+
if (ended) return;
|
|
2831
|
+
end();
|
|
2832
|
+
c.error(err);
|
|
2833
|
+
}
|
|
2834
|
+
},
|
|
2835
|
+
cancel() {
|
|
2836
|
+
end();
|
|
2837
|
+
const returned = Promise.resolve(it.return?.()).catch(() => {});
|
|
2838
|
+
if (!reading) return;
|
|
2839
|
+
const grace = setTimeout(() => void release?.(), CANCEL_GRACE_MS);
|
|
2840
|
+
grace.unref?.();
|
|
2841
|
+
returned.finally(() => clearTimeout(grace));
|
|
2842
|
+
}
|
|
2843
|
+
});
|
|
2741
2844
|
}
|
|
2742
2845
|
/**
|
|
2743
2846
|
* Host a `SiteHandler` over a `Connect/Serve` pair. The `start(serve, params)`
|
|
@@ -3024,8 +3127,11 @@ function listenPort(port, handler, { channelName = "", log = () => {} } = {}) {
|
|
|
3024
3127
|
error
|
|
3025
3128
|
});
|
|
3026
3129
|
};
|
|
3027
|
-
|
|
3028
|
-
|
|
3130
|
+
const listener = (event) => {
|
|
3131
|
+
onMessage(event);
|
|
3132
|
+
};
|
|
3133
|
+
port.addEventListener("message", listener);
|
|
3134
|
+
return () => port.removeEventListener("message", listener);
|
|
3029
3135
|
}
|
|
3030
3136
|
/**
|
|
3031
3137
|
* Wraps an async iterable so that an `AbortSignal` firing causes the wrapper
|
|
@@ -3403,7 +3509,11 @@ function receiveChunks(port, channelName, controller, touch) {
|
|
|
3403
3509
|
* outstanding at a time (spec D11/D12), with no per-chunk deadline (spec D8).
|
|
3404
3510
|
*/
|
|
3405
3511
|
async function sendChunks(port, channelName, output, { maxMessageSize, log }, signal, touch) {
|
|
3406
|
-
const
|
|
3512
|
+
const framed = maxMessageSize ? toChunks(maxMessageSize)(output) : output;
|
|
3513
|
+
const stream = throughAbort(framed, signal);
|
|
3514
|
+
const producer = output;
|
|
3515
|
+
const returnProducer = () => void Promise.resolve(producer.return?.()).catch(() => {});
|
|
3516
|
+
if (framed !== output) signal.addEventListener("abort", returnProducer, { once: true });
|
|
3407
3517
|
try {
|
|
3408
3518
|
await sendIterator(async ({ done, value, error }) => {
|
|
3409
3519
|
if (signal.aborted) return;
|
|
@@ -3427,9 +3537,38 @@ async function sendChunks(port, channelName, output, { maxMessageSize, log }, si
|
|
|
3427
3537
|
} catch (err) {
|
|
3428
3538
|
if (signal.aborted) return;
|
|
3429
3539
|
throw err;
|
|
3540
|
+
} finally {
|
|
3541
|
+
signal.removeEventListener("abort", returnProducer);
|
|
3430
3542
|
}
|
|
3431
3543
|
}
|
|
3432
3544
|
/**
|
|
3545
|
+
* Runs `fn` on a later task, never a microtask: a producer looping on
|
|
3546
|
+
* `whenPortReady` must let the transport's own events run. `setImmediate`
|
|
3547
|
+
* where it exists; elsewhere a `MessageChannel`, because nested `setTimeout`s
|
|
3548
|
+
* are clamped to 4 ms in browsers.
|
|
3549
|
+
*/
|
|
3550
|
+
const setImmediateFn = globalThis.setImmediate;
|
|
3551
|
+
/** @internal */
|
|
3552
|
+
const nextTask = setImmediateFn ? (fn) => {
|
|
3553
|
+
setImmediateFn(fn);
|
|
3554
|
+
} : (() => {
|
|
3555
|
+
let channel;
|
|
3556
|
+
const queue = [];
|
|
3557
|
+
return (fn) => {
|
|
3558
|
+
if (!channel) {
|
|
3559
|
+
channel = new MessageChannel();
|
|
3560
|
+
channel.port1.onmessage = () => queue.shift()?.();
|
|
3561
|
+
}
|
|
3562
|
+
queue.push(fn);
|
|
3563
|
+
channel.port2.postMessage(0);
|
|
3564
|
+
};
|
|
3565
|
+
})();
|
|
3566
|
+
const states = /* @__PURE__ */ new WeakMap();
|
|
3567
|
+
/** @internal Registered by the multiplexer for each port with a window. */
|
|
3568
|
+
function setPortSendState(port, state) {
|
|
3569
|
+
states.set(port, state);
|
|
3570
|
+
}
|
|
3571
|
+
/**
|
|
3433
3572
|
* One virtual port.
|
|
3434
3573
|
*
|
|
3435
3574
|
* `deliver` and `markClosed` are deliberately not on `port`: the consumer holds
|
|
@@ -3444,6 +3583,14 @@ function newVirtualPort(send, requestClose) {
|
|
|
3444
3583
|
if (closing.signal.aborted) return;
|
|
3445
3584
|
closing.abort(reason ?? /* @__PURE__ */ new Error("webrun-rpc: the port was closed"));
|
|
3446
3585
|
};
|
|
3586
|
+
const close = (reason) => {
|
|
3587
|
+
if (closed) return;
|
|
3588
|
+
closed = true;
|
|
3589
|
+
const notify = requestClose;
|
|
3590
|
+
listeners.clear();
|
|
3591
|
+
notify(reason);
|
|
3592
|
+
abort(reason);
|
|
3593
|
+
};
|
|
3447
3594
|
const port = {
|
|
3448
3595
|
addEventListener(_type, listener) {
|
|
3449
3596
|
listeners.add(listener);
|
|
@@ -3455,24 +3602,28 @@ function newVirtualPort(send, requestClose) {
|
|
|
3455
3602
|
if (closed) return;
|
|
3456
3603
|
send(message, transfer);
|
|
3457
3604
|
},
|
|
3458
|
-
close
|
|
3459
|
-
if (closed) return;
|
|
3460
|
-
closed = true;
|
|
3461
|
-
const notify = requestClose;
|
|
3462
|
-
listeners.clear();
|
|
3463
|
-
notify();
|
|
3464
|
-
abort();
|
|
3465
|
-
}
|
|
3605
|
+
close
|
|
3466
3606
|
};
|
|
3467
3607
|
setPortCloseSignal(port, closing.signal);
|
|
3608
|
+
const dispose = Symbol.dispose;
|
|
3609
|
+
if (dispose) Object.defineProperty(port, dispose, {
|
|
3610
|
+
value: () => close(),
|
|
3611
|
+
configurable: true
|
|
3612
|
+
});
|
|
3468
3613
|
return {
|
|
3469
3614
|
port,
|
|
3470
3615
|
deliver(payload) {
|
|
3471
3616
|
if (closed) return;
|
|
3472
3617
|
const event = new MessageEvent("message", { data: payload });
|
|
3618
|
+
let pending;
|
|
3473
3619
|
for (const listener of [...listeners]) try {
|
|
3474
|
-
listener(event);
|
|
3620
|
+
const result = listener(event);
|
|
3621
|
+
if (result && typeof result.then === "function") {
|
|
3622
|
+
pending ??= [];
|
|
3623
|
+
pending.push(result);
|
|
3624
|
+
}
|
|
3475
3625
|
} catch {}
|
|
3626
|
+
return pending ? Promise.allSettled(pending) : void 0;
|
|
3476
3627
|
},
|
|
3477
3628
|
markClosed(reason) {
|
|
3478
3629
|
closed = true;
|
|
@@ -3481,53 +3632,318 @@ function newVirtualPort(send, requestClose) {
|
|
|
3481
3632
|
},
|
|
3482
3633
|
isClosed() {
|
|
3483
3634
|
return closed;
|
|
3635
|
+
},
|
|
3636
|
+
hasListeners() {
|
|
3637
|
+
return listeners.size > 0;
|
|
3484
3638
|
}
|
|
3485
3639
|
};
|
|
3486
3640
|
}
|
|
3487
3641
|
/**
|
|
3488
|
-
* The
|
|
3489
|
-
*
|
|
3642
|
+
* The id of keepalive's `ping` / `pong`: one no port ever takes. An older mux
|
|
3643
|
+
* whose codec passes unknown envelopes through reads them as a close of that
|
|
3644
|
+
* id, which must therefore not be a real port (0 is the initiator's first).
|
|
3490
3645
|
*/
|
|
3491
|
-
const
|
|
3646
|
+
const KEEPALIVE_ID = Number.MAX_SAFE_INTEGER;
|
|
3647
|
+
/** The longest delay a timer holds; past it `setInterval` fires at once. */
|
|
3648
|
+
const MAX_TIMER_MS = 2 ** 31 - 1;
|
|
3492
3649
|
/** Ceiling on concurrently open virtual ports. Bounds the id table only. */
|
|
3493
3650
|
const DEFAULT_MAX_PORTS = 1024;
|
|
3494
3651
|
/**
|
|
3652
|
+
* What a message costs a port's window when the codec cannot say
|
|
3653
|
+
* (`PortCodec.encode`): its bytes and string lengths, found in its values up
|
|
3654
|
+
* to 4 levels deep (at most 256 values), and at least 64. Both ends compute it
|
|
3655
|
+
* from the same structured value, so they agree.
|
|
3656
|
+
*/
|
|
3657
|
+
function estimateMessageSize(payload) {
|
|
3658
|
+
let budget = 256;
|
|
3659
|
+
const walk = (v, depth) => {
|
|
3660
|
+
if (budget-- <= 0) return 0;
|
|
3661
|
+
if (typeof v === "string") return v.length;
|
|
3662
|
+
if (v instanceof ArrayBuffer) return v.byteLength;
|
|
3663
|
+
if (ArrayBuffer.isView(v)) return v.byteLength;
|
|
3664
|
+
if (v === null || typeof v !== "object" || depth >= 4) return 8;
|
|
3665
|
+
let n = 16;
|
|
3666
|
+
if (Array.isArray(v)) for (const x of v) n += walk(x, depth + 1);
|
|
3667
|
+
else for (const k in v) n += k.length + walk(v[k], depth + 1);
|
|
3668
|
+
return n;
|
|
3669
|
+
};
|
|
3670
|
+
return Math.max(64, walk(payload, 0));
|
|
3671
|
+
}
|
|
3672
|
+
/**
|
|
3495
3673
|
* The default `PortMux`: emulates multiplexing over a single port.
|
|
3496
3674
|
*
|
|
3497
3675
|
* A transport that already multiplexes natively supplies its own `PortMux`
|
|
3498
3676
|
* instead — this implementation is for transports that offer one pipe.
|
|
3499
3677
|
*/
|
|
3500
3678
|
function multiplexPort(port, options) {
|
|
3501
|
-
const { codec, onPort, side = "initiator", maxPorts = DEFAULT_MAX_PORTS, maxMessageSize } = options;
|
|
3679
|
+
const { codec, onPort, side = "initiator", maxPorts = DEFAULT_MAX_PORTS, maxMessageSize, signal, window, frameSize, keepalive, onLeak } = options;
|
|
3680
|
+
const maxReassembledBytes = options.maxReassembledBytes ?? 67108864;
|
|
3681
|
+
const misses = keepalive?.misses ?? 3;
|
|
3682
|
+
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`);
|
|
3683
|
+
if (frameSize != null && !(Number.isSafeInteger(frameSize) && frameSize > 0)) throw new RangeError(`webrun-rpc: frameSize must be a positive integer, got ${frameSize}`);
|
|
3684
|
+
if (!(Number.isSafeInteger(maxReassembledBytes) && maxReassembledBytes > 0)) throw new RangeError(`webrun-rpc: maxReassembledBytes must be a positive integer, got ${maxReassembledBytes}`);
|
|
3502
3685
|
const open = /* @__PURE__ */ new Map();
|
|
3686
|
+
let reassembling = 0;
|
|
3687
|
+
const freePartial = (entry) => {
|
|
3688
|
+
if (!entry?.partial) return;
|
|
3689
|
+
reassembling -= entry.partial.charged;
|
|
3690
|
+
entry.partial = void 0;
|
|
3691
|
+
};
|
|
3692
|
+
const leaks = onLeak ? new FinalizationRegistry(({ id, record, release }) => {
|
|
3693
|
+
const entry = open.get(id);
|
|
3694
|
+
if (entry?.handle !== record) return;
|
|
3695
|
+
const { handle: _, credit: __, partial: ___, ...info } = entry;
|
|
3696
|
+
if (!record.hasListeners()) {
|
|
3697
|
+
release("leaked");
|
|
3698
|
+
record.markClosed(/* @__PURE__ */ new Error("webrun-rpc: the port was dropped without close()"));
|
|
3699
|
+
}
|
|
3700
|
+
try {
|
|
3701
|
+
onLeak?.(info);
|
|
3702
|
+
} catch {}
|
|
3703
|
+
}) : void 0;
|
|
3704
|
+
const touch = (id) => {
|
|
3705
|
+
const entry = open.get(id);
|
|
3706
|
+
if (entry) entry.lastActivity = Date.now();
|
|
3707
|
+
};
|
|
3503
3708
|
let nextId = side === "initiator" ? 0 : 1;
|
|
3504
3709
|
let muxClosed = false;
|
|
3710
|
+
/** This end counts encoded bytes and reassembles frames. */
|
|
3711
|
+
const exactCodec = codec.encode != null && codec.decode != null;
|
|
3505
3712
|
const post = (envelope, transfer) => {
|
|
3506
3713
|
if (muxClosed) return;
|
|
3714
|
+
codecPost(envelope, transfer);
|
|
3715
|
+
};
|
|
3716
|
+
/** Like `post`, also while closing: the mux's own close notices. */
|
|
3717
|
+
const codecPost = (envelope, transfer) => {
|
|
3507
3718
|
try {
|
|
3508
3719
|
codec.post(port, envelope, transfer);
|
|
3509
3720
|
} catch {}
|
|
3510
3721
|
};
|
|
3511
|
-
const
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
3722
|
+
const outgoing = /* @__PURE__ */ new Map();
|
|
3723
|
+
const ring = [];
|
|
3724
|
+
let pumping = false;
|
|
3725
|
+
const pump = async () => {
|
|
3726
|
+
pumping = true;
|
|
3727
|
+
try {
|
|
3728
|
+
while (ring.length && !muxClosed) {
|
|
3729
|
+
if (port.whenDrained) await port.whenDrained();
|
|
3730
|
+
if (muxClosed) break;
|
|
3731
|
+
const id = ring.shift();
|
|
3732
|
+
const queue = outgoing.get(id);
|
|
3733
|
+
const item = queue?.shift();
|
|
3734
|
+
if (queue?.length) ring.push(id);
|
|
3735
|
+
else outgoing.delete(id);
|
|
3736
|
+
item?.();
|
|
3737
|
+
}
|
|
3738
|
+
} catch (err) {
|
|
3739
|
+
close(err).catch(() => {});
|
|
3740
|
+
} finally {
|
|
3741
|
+
pumping = false;
|
|
3742
|
+
}
|
|
3743
|
+
};
|
|
3744
|
+
const sendOnPort = (id, item) => {
|
|
3745
|
+
if (frameSize == null) {
|
|
3746
|
+
item();
|
|
3747
|
+
return;
|
|
3748
|
+
}
|
|
3749
|
+
let queue = outgoing.get(id);
|
|
3750
|
+
if (!queue) {
|
|
3751
|
+
queue = [];
|
|
3752
|
+
outgoing.set(id, queue);
|
|
3753
|
+
ring.push(id);
|
|
3754
|
+
}
|
|
3755
|
+
queue.push(item);
|
|
3756
|
+
if (!pumping) pump();
|
|
3757
|
+
};
|
|
3758
|
+
const dropOutgoing = (id) => {
|
|
3759
|
+
if (!outgoing.delete(id)) return;
|
|
3760
|
+
const at = ring.indexOf(id);
|
|
3761
|
+
if (at >= 0) ring.splice(at, 1);
|
|
3762
|
+
};
|
|
3763
|
+
const postBytes = (bytes) => {
|
|
3764
|
+
if (muxClosed) return;
|
|
3765
|
+
try {
|
|
3766
|
+
port.postMessage(bytes);
|
|
3767
|
+
} catch {}
|
|
3768
|
+
};
|
|
3769
|
+
const releaseWaiters = (c) => {
|
|
3770
|
+
for (const w of c?.waiters.splice(0) ?? []) w.resolve();
|
|
3771
|
+
};
|
|
3772
|
+
const flush = (c) => {
|
|
3773
|
+
while (c.queue.length) {
|
|
3774
|
+
const next = c.queue[0];
|
|
3775
|
+
if (c.enforced && c.outstanding > 0 && c.outstanding + next.size > c.window) break;
|
|
3776
|
+
c.queue.shift();
|
|
3777
|
+
c.queued -= next.size;
|
|
3778
|
+
c.outstanding += next.size;
|
|
3779
|
+
next.send();
|
|
3780
|
+
}
|
|
3781
|
+
for (const w of [...c.waiters]) if (c.queued <= w.below) {
|
|
3782
|
+
c.waiters.splice(c.waiters.indexOf(w), 1);
|
|
3783
|
+
w.resolve();
|
|
3784
|
+
}
|
|
3785
|
+
};
|
|
3786
|
+
const grant = (id, c, bytes) => {
|
|
3787
|
+
c.unconsumed -= bytes;
|
|
3788
|
+
c.toGrant += bytes;
|
|
3789
|
+
const send = () => {
|
|
3790
|
+
c.grantScheduled = false;
|
|
3791
|
+
if (!c.toGrant || !open.has(id)) return;
|
|
3518
3792
|
post({
|
|
3793
|
+
type: "credit",
|
|
3794
|
+
id,
|
|
3795
|
+
bytes: c.toGrant
|
|
3796
|
+
});
|
|
3797
|
+
c.toGrant = 0;
|
|
3798
|
+
};
|
|
3799
|
+
if (c.toGrant >= c.window / 4) {
|
|
3800
|
+
send();
|
|
3801
|
+
return;
|
|
3802
|
+
}
|
|
3803
|
+
if (c.grantScheduled) return;
|
|
3804
|
+
c.grantScheduled = true;
|
|
3805
|
+
setTimeout(send, 0);
|
|
3806
|
+
};
|
|
3807
|
+
const attach = (id, meta, portWindow, opener, exact) => {
|
|
3808
|
+
const c = typeof portWindow === "number" && Number.isSafeInteger(portWindow) && portWindow > 0 ? {
|
|
3809
|
+
window: portWindow,
|
|
3810
|
+
enforced: false,
|
|
3811
|
+
opener,
|
|
3812
|
+
exact,
|
|
3813
|
+
outstanding: 0,
|
|
3814
|
+
queue: [],
|
|
3815
|
+
queued: 0,
|
|
3816
|
+
unconsumed: 0,
|
|
3817
|
+
waiters: [],
|
|
3818
|
+
toGrant: 0,
|
|
3819
|
+
grantScheduled: false
|
|
3820
|
+
} : void 0;
|
|
3821
|
+
const self = newVirtualPort((payload, transfer) => {
|
|
3822
|
+
touch(id);
|
|
3823
|
+
const envelope = {
|
|
3824
|
+
type: "message",
|
|
3825
|
+
id,
|
|
3826
|
+
payload
|
|
3827
|
+
};
|
|
3828
|
+
if (!c) {
|
|
3829
|
+
post(envelope, transfer);
|
|
3830
|
+
return;
|
|
3831
|
+
}
|
|
3832
|
+
const bytes = c.exact ? codec.encode?.(envelope) : void 0;
|
|
3833
|
+
const size = bytes?.byteLength ?? estimateMessageSize(payload);
|
|
3834
|
+
const waits = frameSize != null || c.queue.length > 0 || c.enforced && c.outstanding > 0 && c.outstanding + size > c.window;
|
|
3835
|
+
const held = !bytes && waits ? {
|
|
3836
|
+
...envelope,
|
|
3837
|
+
payload: structuredClone(payload, { transfer })
|
|
3838
|
+
} : void 0;
|
|
3839
|
+
const send = (whole = false) => {
|
|
3840
|
+
if (!bytes) {
|
|
3841
|
+
sendOnPort(id, held ? () => post(held) : () => post(envelope, transfer));
|
|
3842
|
+
return;
|
|
3843
|
+
}
|
|
3844
|
+
if (!whole && frameSize != null && c.enforced && c.exact && bytes.byteLength > frameSize) {
|
|
3845
|
+
for (let at = 0; at < bytes.byteLength; at += frameSize) {
|
|
3846
|
+
const data = bytes.subarray(at, at + frameSize);
|
|
3847
|
+
const frame = at + frameSize >= bytes.byteLength ? {
|
|
3848
|
+
type: "frame",
|
|
3849
|
+
id,
|
|
3850
|
+
data,
|
|
3851
|
+
last: true
|
|
3852
|
+
} : {
|
|
3853
|
+
type: "frame",
|
|
3854
|
+
id,
|
|
3855
|
+
data
|
|
3856
|
+
};
|
|
3857
|
+
sendOnPort(id, () => post(frame));
|
|
3858
|
+
}
|
|
3859
|
+
return;
|
|
3860
|
+
}
|
|
3861
|
+
sendOnPort(id, () => postBytes(bytes));
|
|
3862
|
+
};
|
|
3863
|
+
c.queue.push({
|
|
3864
|
+
send,
|
|
3865
|
+
size
|
|
3866
|
+
});
|
|
3867
|
+
c.queued += size;
|
|
3868
|
+
flush(c);
|
|
3869
|
+
}, (reason) => {
|
|
3870
|
+
release(reason);
|
|
3871
|
+
record.markClosed(reason);
|
|
3872
|
+
});
|
|
3873
|
+
function release(reason) {
|
|
3874
|
+
freePartial(open.get(id));
|
|
3875
|
+
open.delete(id);
|
|
3876
|
+
leaks?.unregister(record);
|
|
3877
|
+
if (c?.queue.length) {
|
|
3878
|
+
for (const item of c.queue.splice(0)) item.send(true);
|
|
3879
|
+
c.queued = 0;
|
|
3880
|
+
}
|
|
3881
|
+
sendOnPort(id, () => post({
|
|
3519
3882
|
type: "close",
|
|
3520
3883
|
id,
|
|
3521
3884
|
reason
|
|
3885
|
+
}));
|
|
3886
|
+
releaseWaiters(c);
|
|
3887
|
+
}
|
|
3888
|
+
const { markClosed, isClosed, hasListeners } = self;
|
|
3889
|
+
if (c) {
|
|
3890
|
+
const deliver = self.deliver;
|
|
3891
|
+
self.deliver = (payload, size = estimateMessageSize(payload)) => {
|
|
3892
|
+
c.unconsumed += size;
|
|
3893
|
+
const done = deliver(payload);
|
|
3894
|
+
if (done) done.then(() => grant(id, c, size));
|
|
3895
|
+
else grant(id, c, size);
|
|
3896
|
+
return done;
|
|
3897
|
+
};
|
|
3898
|
+
setPortSendState(self.port, {
|
|
3899
|
+
bufferedAmount: () => c.queued,
|
|
3900
|
+
ready: (below = 0) => new Promise((resolve) => {
|
|
3901
|
+
if (c.queued <= below || isClosed()) nextTask(resolve);
|
|
3902
|
+
else c.waiters.push({
|
|
3903
|
+
below,
|
|
3904
|
+
resolve: () => nextTask(resolve)
|
|
3905
|
+
});
|
|
3906
|
+
})
|
|
3522
3907
|
});
|
|
3523
|
-
|
|
3524
|
-
|
|
3908
|
+
}
|
|
3909
|
+
const record = {
|
|
3910
|
+
deliver: self.deliver,
|
|
3911
|
+
markClosed,
|
|
3912
|
+
isClosed,
|
|
3913
|
+
hasListeners,
|
|
3914
|
+
close: (reason) => {
|
|
3915
|
+
if (isClosed()) return;
|
|
3916
|
+
release(reason);
|
|
3917
|
+
markClosed(reason);
|
|
3918
|
+
}
|
|
3919
|
+
};
|
|
3920
|
+
leaks?.register(self.port, {
|
|
3921
|
+
id,
|
|
3922
|
+
record,
|
|
3923
|
+
release
|
|
3924
|
+
}, record);
|
|
3925
|
+
const now = Date.now();
|
|
3926
|
+
open.set(id, {
|
|
3927
|
+
id,
|
|
3928
|
+
meta,
|
|
3929
|
+
openedAt: now,
|
|
3930
|
+
lastActivity: now,
|
|
3931
|
+
handle: record,
|
|
3932
|
+
credit: c
|
|
3525
3933
|
});
|
|
3526
|
-
open.set(id, self);
|
|
3527
3934
|
return self;
|
|
3528
3935
|
};
|
|
3529
|
-
const handleEnvelope = (envelope) => {
|
|
3936
|
+
const handleEnvelope = (envelope, event) => {
|
|
3530
3937
|
if (muxClosed) return;
|
|
3938
|
+
heardSincePing = true;
|
|
3939
|
+
if (envelope.type === "ping" || envelope.type === "pong") {
|
|
3940
|
+
peerKeepsAlive = true;
|
|
3941
|
+
if (envelope.type === "ping") post({
|
|
3942
|
+
type: "pong",
|
|
3943
|
+
id: KEEPALIVE_ID
|
|
3944
|
+
});
|
|
3945
|
+
return;
|
|
3946
|
+
}
|
|
3531
3947
|
if (envelope.type === "open") {
|
|
3532
3948
|
if (open.has(envelope.id)) return;
|
|
3533
3949
|
if (open.size >= maxPorts) {
|
|
@@ -3538,7 +3954,18 @@ function multiplexPort(port, options) {
|
|
|
3538
3954
|
});
|
|
3539
3955
|
return;
|
|
3540
3956
|
}
|
|
3541
|
-
const handle = attach(envelope.id);
|
|
3957
|
+
const handle = attach(envelope.id, envelope.meta, envelope.window, false, envelope.exact === true && exactCodec);
|
|
3958
|
+
const credit = open.get(envelope.id)?.credit;
|
|
3959
|
+
if (credit) post(credit.exact ? {
|
|
3960
|
+
type: "credit",
|
|
3961
|
+
id: envelope.id,
|
|
3962
|
+
bytes: 0,
|
|
3963
|
+
exact: true
|
|
3964
|
+
} : {
|
|
3965
|
+
type: "credit",
|
|
3966
|
+
id: envelope.id,
|
|
3967
|
+
bytes: 0
|
|
3968
|
+
});
|
|
3542
3969
|
let accepted = false;
|
|
3543
3970
|
if (onPort) try {
|
|
3544
3971
|
accepted = onPort(handle.port, envelope.meta) !== false;
|
|
@@ -3553,57 +3980,215 @@ function multiplexPort(port, options) {
|
|
|
3553
3980
|
id: envelope.id,
|
|
3554
3981
|
reason: "rejected"
|
|
3555
3982
|
});
|
|
3983
|
+
return;
|
|
3556
3984
|
}
|
|
3557
3985
|
return;
|
|
3558
3986
|
}
|
|
3559
|
-
const
|
|
3987
|
+
const entry = open.get(envelope.id);
|
|
3988
|
+
if (envelope.type === "credit") {
|
|
3989
|
+
const c = entry?.credit;
|
|
3990
|
+
if (!c || !(envelope.bytes >= 0)) return;
|
|
3991
|
+
if (!c.enforced && c.opener) {
|
|
3992
|
+
if (c.exact && envelope.exact !== true) {
|
|
3993
|
+
c.exact = false;
|
|
3994
|
+
c.outstanding = 0;
|
|
3995
|
+
}
|
|
3996
|
+
post({
|
|
3997
|
+
type: "credit",
|
|
3998
|
+
id: envelope.id,
|
|
3999
|
+
bytes: 0
|
|
4000
|
+
});
|
|
4001
|
+
}
|
|
4002
|
+
c.enforced = true;
|
|
4003
|
+
c.outstanding = Math.max(0, c.outstanding - envelope.bytes);
|
|
4004
|
+
flush(c);
|
|
4005
|
+
return;
|
|
4006
|
+
}
|
|
3560
4007
|
if (envelope.type === "message") {
|
|
3561
|
-
|
|
3562
|
-
|
|
4008
|
+
const sizeOf = (c) => (c.exact ? byteLengthOf(event.data) : void 0) ?? estimateMessageSize(envelope.payload);
|
|
4009
|
+
if (!entry) return;
|
|
4010
|
+
touch(envelope.id);
|
|
4011
|
+
entry.handle.deliver(envelope.payload, entry.credit ? sizeOf(entry.credit) : void 0);
|
|
4012
|
+
return;
|
|
4013
|
+
}
|
|
4014
|
+
if (envelope.type === "frame") {
|
|
4015
|
+
if (!entry?.credit?.exact || !codec.decode || !(envelope.data instanceof Uint8Array)) return;
|
|
4016
|
+
touch(envelope.id);
|
|
4017
|
+
const c = entry.credit;
|
|
4018
|
+
const owed = c.unconsumed + c.toGrant;
|
|
4019
|
+
const partial = entry.partial ?? {
|
|
4020
|
+
chunks: [],
|
|
4021
|
+
bytes: 0,
|
|
4022
|
+
owed,
|
|
4023
|
+
charged: 0,
|
|
4024
|
+
...owed > 0 ? { room: c.window - owed } : {}
|
|
4025
|
+
};
|
|
4026
|
+
entry.partial = partial;
|
|
4027
|
+
const size = envelope.data.byteLength;
|
|
4028
|
+
const charged = partial.room == null ? Math.max(0, partial.bytes + size - c.window) : 0;
|
|
4029
|
+
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;
|
|
4030
|
+
if (overrun) {
|
|
4031
|
+
freePartial(entry);
|
|
4032
|
+
dropOutgoing(envelope.id);
|
|
4033
|
+
open.delete(envelope.id);
|
|
4034
|
+
releaseWaiters(c);
|
|
4035
|
+
entry.handle.markClosed(/* @__PURE__ */ new RangeError(`webrun-rpc: a framed message passed ${overrun}`));
|
|
4036
|
+
post({
|
|
4037
|
+
type: "close",
|
|
4038
|
+
id: envelope.id,
|
|
4039
|
+
reason: "max-reassembled-bytes"
|
|
4040
|
+
});
|
|
4041
|
+
return;
|
|
4042
|
+
}
|
|
4043
|
+
partial.bytes += size;
|
|
4044
|
+
reassembling += charged - partial.charged;
|
|
4045
|
+
partial.charged = charged;
|
|
4046
|
+
partial.chunks.push(envelope.data);
|
|
4047
|
+
if (!envelope.last) return;
|
|
4048
|
+
freePartial(entry);
|
|
4049
|
+
const whole = new Uint8Array(partial.bytes);
|
|
4050
|
+
let at = 0;
|
|
4051
|
+
for (const chunk of partial.chunks) {
|
|
4052
|
+
whole.set(chunk, at);
|
|
4053
|
+
at += chunk.byteLength;
|
|
4054
|
+
}
|
|
4055
|
+
const inner = codec.decode(whole);
|
|
4056
|
+
if (inner?.type !== "message" || inner.id !== envelope.id) return;
|
|
4057
|
+
entry.handle.deliver(inner.payload, whole.byteLength);
|
|
3563
4058
|
return;
|
|
3564
4059
|
}
|
|
3565
|
-
|
|
4060
|
+
dropOutgoing(envelope.id);
|
|
4061
|
+
if (!entry) return;
|
|
4062
|
+
freePartial(entry);
|
|
3566
4063
|
open.delete(envelope.id);
|
|
3567
|
-
|
|
4064
|
+
releaseWaiters(entry.credit);
|
|
4065
|
+
entry.handle.markClosed(/* @__PURE__ */ new Error("webrun-rpc: the peer closed the port"));
|
|
3568
4066
|
};
|
|
3569
4067
|
const listener = (event) => {
|
|
3570
4068
|
const envelope = codec.read(event);
|
|
3571
|
-
if (envelope) handleEnvelope(envelope);
|
|
4069
|
+
if (envelope) handleEnvelope(envelope, event);
|
|
4070
|
+
};
|
|
4071
|
+
let peerKeepsAlive = false;
|
|
4072
|
+
let heardSincePing = true;
|
|
4073
|
+
let unanswered = 0;
|
|
4074
|
+
let pinger;
|
|
4075
|
+
const tick = () => {
|
|
4076
|
+
if (!keepalive) return;
|
|
4077
|
+
const backlogged = (port.bufferedAmount ?? 0) > 0;
|
|
4078
|
+
if (heardSincePing) unanswered = 0;
|
|
4079
|
+
else if (peerKeepsAlive && !backlogged) unanswered++;
|
|
4080
|
+
if (unanswered >= misses) {
|
|
4081
|
+
close(/* @__PURE__ */ new Error(`webrun-rpc: the peer did not answer ${misses} keepalive pings, ${keepalive.intervalMs} ms apart`)).catch(() => {});
|
|
4082
|
+
return;
|
|
4083
|
+
}
|
|
4084
|
+
heardSincePing = false;
|
|
4085
|
+
post({
|
|
4086
|
+
type: "ping",
|
|
4087
|
+
id: KEEPALIVE_ID
|
|
4088
|
+
});
|
|
4089
|
+
};
|
|
4090
|
+
const close = async (reason) => {
|
|
4091
|
+
if (muxClosed) return;
|
|
4092
|
+
clearInterval(pinger);
|
|
4093
|
+
muxClosed = true;
|
|
4094
|
+
signal?.removeEventListener("abort", onTransportAbort);
|
|
4095
|
+
port.removeEventListener("message", listener);
|
|
4096
|
+
const closedNow = /* @__PURE__ */ new Set();
|
|
4097
|
+
for (const [id, { handle, credit }] of [...open]) {
|
|
4098
|
+
closedNow.add(id);
|
|
4099
|
+
codecPost({
|
|
4100
|
+
type: "close",
|
|
4101
|
+
id
|
|
4102
|
+
});
|
|
4103
|
+
open.delete(id);
|
|
4104
|
+
releaseWaiters(credit);
|
|
4105
|
+
handle.markClosed(reason ?? /* @__PURE__ */ new Error("webrun-rpc: the multiplexer was closed"));
|
|
4106
|
+
}
|
|
4107
|
+
for (const id of outgoing.keys()) if (!closedNow.has(id)) codecPost({
|
|
4108
|
+
type: "close",
|
|
4109
|
+
id
|
|
4110
|
+
});
|
|
4111
|
+
outgoing.clear();
|
|
4112
|
+
ring.length = 0;
|
|
4113
|
+
await port.close?.();
|
|
4114
|
+
};
|
|
4115
|
+
const onTransportAbort = () => {
|
|
4116
|
+
close(signal?.reason).catch(() => {});
|
|
3572
4117
|
};
|
|
3573
4118
|
port.addEventListener("message", listener);
|
|
3574
4119
|
port.start?.();
|
|
4120
|
+
if (keepalive) {
|
|
4121
|
+
pinger = setInterval(tick, keepalive.intervalMs);
|
|
4122
|
+
pinger.unref?.();
|
|
4123
|
+
}
|
|
4124
|
+
if (signal?.aborted) close(signal.reason).catch(() => {});
|
|
4125
|
+
else signal?.addEventListener("abort", onTransportAbort, { once: true });
|
|
3575
4126
|
return {
|
|
3576
4127
|
maxMessageSize,
|
|
3577
|
-
async openPort(meta) {
|
|
4128
|
+
async openPort(meta, { signal: portSignal } = {}) {
|
|
3578
4129
|
if (muxClosed) throw new Error("webrun-rpc: the multiplexer is closed");
|
|
3579
|
-
if (
|
|
4130
|
+
if (portSignal?.aborted) throw portSignal.reason;
|
|
4131
|
+
if (open.size >= maxPorts) throw new RangeError(`webrun-rpc: maxPorts (${maxPorts}) reached; open ports by kind: ${describeKinds(open.values())}`);
|
|
3580
4132
|
while (open.has(nextId)) nextId += 2;
|
|
3581
4133
|
const id = nextId;
|
|
3582
4134
|
nextId += 2;
|
|
3583
|
-
const handle = attach(id);
|
|
3584
|
-
post({
|
|
4135
|
+
const handle = attach(id, meta, window, true, exactCodec);
|
|
4136
|
+
post(window ? exactCodec ? {
|
|
4137
|
+
type: "open",
|
|
4138
|
+
id,
|
|
4139
|
+
meta,
|
|
4140
|
+
window,
|
|
4141
|
+
exact: true
|
|
4142
|
+
} : {
|
|
4143
|
+
type: "open",
|
|
4144
|
+
id,
|
|
4145
|
+
meta,
|
|
4146
|
+
window
|
|
4147
|
+
} : {
|
|
3585
4148
|
type: "open",
|
|
3586
4149
|
id,
|
|
3587
4150
|
meta
|
|
3588
4151
|
});
|
|
4152
|
+
if (portSignal) {
|
|
4153
|
+
const record = open.get(id)?.handle;
|
|
4154
|
+
const onAbort = () => {
|
|
4155
|
+
if (record && open.get(id)?.handle === record) record.close(portSignal.reason);
|
|
4156
|
+
};
|
|
4157
|
+
portSignal.addEventListener("abort", onAbort, { once: true });
|
|
4158
|
+
getPortCloseSignal(handle.port)?.addEventListener("abort", () => portSignal.removeEventListener("abort", onAbort), { once: true });
|
|
4159
|
+
}
|
|
3589
4160
|
return handle.port;
|
|
3590
4161
|
},
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
}
|
|
3601
|
-
muxClosed = true;
|
|
3602
|
-
port.removeEventListener("message", listener);
|
|
3603
|
-
await port.close?.();
|
|
4162
|
+
close,
|
|
4163
|
+
ports() {
|
|
4164
|
+
return [...open.values()].map(({ handle: _, credit, ...info }) => credit ? {
|
|
4165
|
+
...info,
|
|
4166
|
+
...credit.enforced ? { window: credit.window } : {},
|
|
4167
|
+
queued: credit.queued,
|
|
4168
|
+
outstanding: credit.outstanding,
|
|
4169
|
+
unconsumed: credit.unconsumed
|
|
4170
|
+
} : info);
|
|
3604
4171
|
}
|
|
3605
4172
|
};
|
|
3606
4173
|
}
|
|
4174
|
+
/** The byte length of a received message's data, or `undefined` if it is not bytes. */
|
|
4175
|
+
function byteLengthOf(data) {
|
|
4176
|
+
if (data instanceof ArrayBuffer || ArrayBuffer.isView(data)) return data.byteLength;
|
|
4177
|
+
}
|
|
4178
|
+
/**
|
|
4179
|
+
* `"http: 1000, ws: 24"`: the three commonest `meta.kind`s among `entries`, so
|
|
4180
|
+
* a full table names who filled it. A port without a kind counts as `"?"`.
|
|
4181
|
+
*/
|
|
4182
|
+
function describeKinds(entries) {
|
|
4183
|
+
const counts = /* @__PURE__ */ new Map();
|
|
4184
|
+
for (const { meta } of entries) {
|
|
4185
|
+
const kind = meta?.kind;
|
|
4186
|
+
const name = typeof kind === "string" ? kind : "?";
|
|
4187
|
+
counts.set(name, (counts.get(name) ?? 0) + 1);
|
|
4188
|
+
}
|
|
4189
|
+
if (!counts.size) return "none";
|
|
4190
|
+
return [...counts].sort((x, y) => y[1] - x[1]).slice(0, 3).map(([name, n]) => `${name}: ${n}`).join(", ");
|
|
4191
|
+
}
|
|
3607
4192
|
/**
|
|
3608
4193
|
* Announced with every port this pair opens. Layer 1 never reads `meta` — it is
|
|
3609
4194
|
* here so a peer running something else over the same multiplexer can tell a
|
|
@@ -3665,14 +4250,17 @@ const serve = async ({ mux: factory, timeout }, handler) => {
|
|
|
3665
4250
|
const mux = await factory((port) => {
|
|
3666
4251
|
let off;
|
|
3667
4252
|
let finished = false;
|
|
3668
|
-
const tracked = (input) =>
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
4253
|
+
const tracked = (input) => {
|
|
4254
|
+
const output = handler(input);
|
|
4255
|
+
return returningAtOnce((async function* () {
|
|
4256
|
+
try {
|
|
4257
|
+
yield* output;
|
|
4258
|
+
} finally {
|
|
4259
|
+
finished = true;
|
|
4260
|
+
if (off) live.delete(off);
|
|
4261
|
+
}
|
|
4262
|
+
})(), () => void output.return(void 0).catch(() => {}));
|
|
4263
|
+
};
|
|
3676
4264
|
off = serveDuplexOverPort(port, tracked, streamOptions);
|
|
3677
4265
|
if (!finished) live.add(off);
|
|
3678
4266
|
});
|
|
@@ -3710,7 +4298,7 @@ function isEnvelope(value) {
|
|
|
3710
4298
|
const candidate = value;
|
|
3711
4299
|
if (typeof candidate.id !== "number") return false;
|
|
3712
4300
|
if (!Number.isInteger(candidate.id) || candidate.id < 0) return false;
|
|
3713
|
-
return candidate.type === "open" || candidate.type === "message" || candidate.type === "close";
|
|
4301
|
+
return candidate.type === "open" || candidate.type === "message" || candidate.type === "close" || candidate.type === "credit" || candidate.type === "frame" || candidate.type === "ping" || candidate.type === "pong";
|
|
3714
4302
|
}
|
|
3715
4303
|
/**
|
|
3716
4304
|
* For ports whose messages are structured values — a real `MessagePort`, a
|
|
@@ -3872,7 +4460,7 @@ function trackedLink(send, release) {
|
|
|
3872
4460
|
finish();
|
|
3873
4461
|
return response;
|
|
3874
4462
|
}
|
|
3875
|
-
return new Response(guardBody(response.body, lost, () => lostReason, finish), {
|
|
4463
|
+
return new Response(guardBody(response.body, lost, () => lostReason, finish, request.signal), {
|
|
3876
4464
|
status: response.status,
|
|
3877
4465
|
statusText: response.statusText,
|
|
3878
4466
|
headers: response.headers
|
|
@@ -3891,33 +4479,65 @@ function trackedLink(send, release) {
|
|
|
3891
4479
|
return asyncDisposable(link, () => link.close());
|
|
3892
4480
|
}
|
|
3893
4481
|
/**
|
|
3894
|
-
* A response body that reports its end (done, cancelled or
|
|
3895
|
-
* with the link's reason when the link is lost mid-stream.
|
|
4482
|
+
* A response body that reports its end (done, cancelled, failed or aborted) and
|
|
4483
|
+
* fails with the link's reason when the link is lost mid-stream. An abort of the
|
|
4484
|
+
* request's signal ends it at once, read or not: the exchange is over (the layer
|
|
4485
|
+
* below has given its port back), so the request leaves `inflight` then.
|
|
3896
4486
|
*/
|
|
3897
|
-
function guardBody(body, lost, reason, finish) {
|
|
4487
|
+
function guardBody(body, lost, reason, finish, signal) {
|
|
3898
4488
|
const reader = body.getReader();
|
|
4489
|
+
let controller;
|
|
4490
|
+
let ended = false;
|
|
4491
|
+
const end = () => {
|
|
4492
|
+
ended = true;
|
|
4493
|
+
signal?.removeEventListener("abort", onAbort);
|
|
4494
|
+
finish();
|
|
4495
|
+
};
|
|
4496
|
+
const onAbort = () => {
|
|
4497
|
+
if (ended) return;
|
|
4498
|
+
end();
|
|
4499
|
+
reader.cancel().catch(() => {});
|
|
4500
|
+
try {
|
|
4501
|
+
controller.error(abortError(signal));
|
|
4502
|
+
} catch {}
|
|
4503
|
+
};
|
|
3899
4504
|
return new ReadableStream({
|
|
4505
|
+
start(c) {
|
|
4506
|
+
controller = c;
|
|
4507
|
+
if (signal?.aborted) onAbort();
|
|
4508
|
+
else signal?.addEventListener("abort", onAbort, { once: true });
|
|
4509
|
+
},
|
|
3900
4510
|
async pull(controller) {
|
|
3901
4511
|
try {
|
|
3902
4512
|
const { done, value } = await Promise.race([reader.read(), lost]);
|
|
4513
|
+
if (ended) return;
|
|
3903
4514
|
if (done) {
|
|
3904
|
-
|
|
4515
|
+
end();
|
|
3905
4516
|
controller.close();
|
|
3906
4517
|
return;
|
|
3907
4518
|
}
|
|
3908
4519
|
controller.enqueue(value);
|
|
3909
4520
|
} catch (error) {
|
|
3910
|
-
|
|
4521
|
+
if (ended) return;
|
|
4522
|
+
end();
|
|
3911
4523
|
reader.cancel().catch(() => {});
|
|
3912
4524
|
controller.error(reason() ?? error);
|
|
3913
4525
|
}
|
|
3914
4526
|
},
|
|
3915
4527
|
async cancel(why) {
|
|
3916
|
-
|
|
4528
|
+
end();
|
|
3917
4529
|
await reader.cancel(why).catch(() => {});
|
|
3918
4530
|
}
|
|
3919
4531
|
});
|
|
3920
4532
|
}
|
|
4533
|
+
/** The signal's reason as an `Error` (`AbortError` when it gave none). */
|
|
4534
|
+
function abortError(signal) {
|
|
4535
|
+
const why = signal.reason;
|
|
4536
|
+
if (why instanceof Error) return why;
|
|
4537
|
+
const err = new Error(why === void 0 ? "Aborted" : String(why));
|
|
4538
|
+
err.name = "AbortError";
|
|
4539
|
+
return err;
|
|
4540
|
+
}
|
|
3921
4541
|
//#endregion
|
|
3922
4542
|
//#region src/link/locks.ts
|
|
3923
4543
|
function lockManager() {
|
|
@@ -6638,6 +7258,8 @@ var WorkerLinkImpl = class {
|
|
|
6638
7258
|
_ownerLock;
|
|
6639
7259
|
_attaching;
|
|
6640
7260
|
_detached = false;
|
|
7261
|
+
/** The `seq` of the last link sent to the worker. */
|
|
7262
|
+
_sent = 0;
|
|
6641
7263
|
constructor(_adapter, _control, init) {
|
|
6642
7264
|
this._adapter = _adapter;
|
|
6643
7265
|
this._control = _control;
|
|
@@ -6649,6 +7271,7 @@ var WorkerLinkImpl = class {
|
|
|
6649
7271
|
_control.onmessage = (event) => {
|
|
6650
7272
|
const message = event.data;
|
|
6651
7273
|
if (message?.type === "relink") {
|
|
7274
|
+
if (message.seen !== void 0 && message.seen < this._sent) return;
|
|
6652
7275
|
this._adapter._poke(WAKE_MESSAGE);
|
|
6653
7276
|
this.attach().catch(() => {});
|
|
6654
7277
|
} else if (message?.type === "keepalive") this._adapter._poke(KEEPALIVE_MESSAGE);
|
|
@@ -6695,7 +7318,8 @@ var WorkerLinkImpl = class {
|
|
|
6695
7318
|
this._control.postMessage({
|
|
6696
7319
|
type: "link",
|
|
6697
7320
|
key: this.key,
|
|
6698
|
-
routerLock: reply.routerLock
|
|
7321
|
+
routerLock: reply.routerLock,
|
|
7322
|
+
seq: ++this._sent
|
|
6699
7323
|
}, [
|
|
6700
7324
|
down.port2,
|
|
6701
7325
|
ctl.port2,
|
|
@@ -6796,12 +7420,18 @@ function serveHttpInWorker(handler, options = {}) {
|
|
|
6796
7420
|
const ownerLock = newLockName("webrun-http-owner");
|
|
6797
7421
|
const held = holdLock(ownerLock);
|
|
6798
7422
|
const controls = /* @__PURE__ */ new Set();
|
|
7423
|
+
const seen = /* @__PURE__ */ new Map();
|
|
6799
7424
|
const tellPages = (message) => {
|
|
6800
7425
|
for (const control of controls) control.postMessage(message);
|
|
6801
7426
|
};
|
|
6802
7427
|
const endpoint = new HandlerEndpoint(handler, {
|
|
6803
7428
|
...options,
|
|
6804
|
-
requestRelink: () =>
|
|
7429
|
+
requestRelink: () => {
|
|
7430
|
+
for (const control of controls) control.postMessage({
|
|
7431
|
+
type: "relink",
|
|
7432
|
+
seen: seen.get(control) ?? 0
|
|
7433
|
+
});
|
|
7434
|
+
},
|
|
6805
7435
|
keepAlive: () => tellPages({ type: "keepalive" })
|
|
6806
7436
|
});
|
|
6807
7437
|
const onMessage = (event) => {
|
|
@@ -6813,6 +7443,7 @@ function serveHttpInWorker(handler, options = {}) {
|
|
|
6813
7443
|
control.onmessage = (message) => {
|
|
6814
7444
|
const msg = message.data;
|
|
6815
7445
|
if (msg?.type === "link") {
|
|
7446
|
+
if (msg.seq !== void 0) seen.set(control, msg.seq);
|
|
6816
7447
|
const [down, ctl, up] = message.ports;
|
|
6817
7448
|
if (down) endpoint.link(msg.key, {
|
|
6818
7449
|
down,
|
|
@@ -6853,9 +7484,10 @@ function serveHttpInWorker(handler, options = {}) {
|
|
|
6853
7484
|
await endpoint.close();
|
|
6854
7485
|
for (const control of controls) control.close();
|
|
6855
7486
|
controls.clear();
|
|
7487
|
+
seen.clear();
|
|
6856
7488
|
(await held)();
|
|
6857
7489
|
}
|
|
6858
7490
|
};
|
|
6859
7491
|
}
|
|
6860
7492
|
//#endregion
|
|
6861
|
-
export { CLAIM_CALL, DEFAULT_CLOSE_TIMEOUT_MS, DEFAULT_HEARTBEAT_MS, DEFAULT_SERVICE_WORKER_TIMEOUT, DuplexSiteBuilder, HANDLER_LOST_MESSAGE, HTTP_LINK_PROTOCOL, HandlerEndpoint, HandlerTable, HttpError, HttpParseError, LinkLostError, PEER_ERROR_HEADER, PortRendezvous, ROUTER_LOST_MESSAGE, ServerWebSocket, ServiceWorkerControlError, SwHttpAdapter, SwHttpDispatcher, SwPortDispatcher, SwPortHandler, TransportClosedError, WS_PORT_PROTOCOL_VERSION, WS_VERSION_HEADER, WsCloseEvent, answerConnect, awaitActiveServiceWorker, awaitServiceWorkerControl, callChannel, callChannelWithin, callHttpService, callWebSocketService, collect, collectBytes, collectString, connectHttpLink, connectWebSocketThroughServiceWorker, createWebSocketClass, decodeJsonl, decodeMessage, decodeText, defaultCodec, deserializeError, emulateMux, encodeJsonl, encodeMessage, encodeText, fetchOverDuplex, fromReadableStream, getRelayWindowMessageHandler, handleChannelCalls, handleClaimRequests, handleHttpRequests, handleStreams, holdLock, httpCodec, httpFetch, httpServe, initHttpService, initServiceWorker, initializeConnection, isWebSocketUpgrade, joinLines, jsonEnvelopeCodec, leastInflightRoute, locksAvailable, map, newAsyncGenerator, newCreditGrantor, newCreditLedger, newHttpClientStub, newHttpCodec, newHttpServerStub, newInvokationChannel, newLockName, newRegistry, newRemoteRelayChannel, newServiceWorkerPort, newSniffingCodec, newWebSocketUpgradeRequest, normalizeToUint8Array, recieveIterator, registerConnectionsHandler, relayWebSocketClass, requestedProtocols, sendHttpRequest, sendIterator, sendOverConnection, sendStream, serializeError, serveFetchOverDuplex, serveHttpInWorker, serveHttpLink, serveWebSocketUpgrade, serviceWorkerWebSocketClass, splitLines, splitServiceUrl, startHttpDispatcher, takePort, takeUpgrade, toChunks, toReadableStream, trackedLink, unwrapTransfer, upgradeWebSocket, watchLock, withHeaders, withTransfer };
|
|
7493
|
+
export { CLAIM_CALL, DEFAULT_CLOSE_TIMEOUT_MS, DEFAULT_HEARTBEAT_MS, DEFAULT_SERVICE_WORKER_TIMEOUT, DuplexSiteBuilder, EXCHANGE_ABANDONED_MESSAGE, HANDLER_LOST_MESSAGE, HTTP_LINK_PROTOCOL, HandlerEndpoint, HandlerTable, HttpError, HttpParseError, LinkLostError, PEER_ERROR_HEADER, PortRendezvous, ROUTER_LOST_MESSAGE, ServerWebSocket, ServiceWorkerControlError, SwHttpAdapter, SwHttpDispatcher, SwPortDispatcher, SwPortHandler, TransportClosedError, WS_PORT_PROTOCOL_VERSION, WS_VERSION_HEADER, WsCloseEvent, answerConnect, awaitActiveServiceWorker, awaitServiceWorkerControl, callChannel, callChannelWithin, callHttpService, callWebSocketService, collect, collectBytes, collectString, connectHttpLink, connectWebSocketThroughServiceWorker, createWebSocketClass, decodeJsonl, decodeMessage, decodeText, defaultCodec, deserializeError, emulateMux, encodeJsonl, encodeMessage, encodeText, fetchOverDuplex, fromReadableStream, getRelayWindowMessageHandler, handleChannelCalls, handleClaimRequests, handleHttpRequests, handleStreams, holdLock, httpCodec, httpFetch, httpServe, initHttpService, initServiceWorker, initializeConnection, isWebSocketUpgrade, joinLines, jsonEnvelopeCodec, leastInflightRoute, locksAvailable, map, newAsyncGenerator, newCreditGrantor, newCreditLedger, newHttpClientStub, newHttpCodec, newHttpServerStub, newInvokationChannel, newLockName, newRegistry, newRemoteRelayChannel, newServiceWorkerPort, newSniffingCodec, newWebSocketUpgradeRequest, normalizeToUint8Array, recieveIterator, registerConnectionsHandler, relayWebSocketClass, requestedProtocols, sendHttpRequest, sendIterator, sendOverConnection, sendStream, serializeError, serveFetchOverDuplex, serveHttpInWorker, serveHttpLink, serveWebSocketUpgrade, serviceWorkerWebSocketClass, splitLines, splitServiceUrl, startHttpDispatcher, takePort, takeUpgrade, toChunks, toReadableStream, trackedLink, unwrapTransfer, upgradeWebSocket, watchLock, withHeaders, withTransfer };
|