@statewalker/webrun-http-browser 0.7.1 → 0.9.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 +620 -70
- package/dist/relay-sw.js +606 -88
- package/dist/relay-worker.js +606 -88
- 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 +606 -68
- package/dist/sw.js +620 -70
- package/dist/worker.js +615 -69
- 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/sw-worker.js
CHANGED
|
@@ -1653,7 +1653,7 @@
|
|
|
1653
1653
|
else await release?.();
|
|
1654
1654
|
return new Response(null, responseInit);
|
|
1655
1655
|
}
|
|
1656
|
-
return new Response(
|
|
1656
|
+
return new Response(responseBody(respBody, request.signal, release), responseInit);
|
|
1657
1657
|
}
|
|
1658
1658
|
/**
|
|
1659
1659
|
* Wrap a `(Request) => Promise<Response>` handler as a `Duplex` so it can be
|
|
@@ -1784,15 +1784,78 @@
|
|
|
1784
1784
|
settle
|
|
1785
1785
|
};
|
|
1786
1786
|
}
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1787
|
+
/**
|
|
1788
|
+
* How long a cancelled body waits for the read in flight before it abandons the call anyway.
|
|
1789
|
+
* The read's answer tells a body that has ended (the call is finished: an upload the handler
|
|
1790
|
+
* reads after answering goes on) from one that has not; a peer that sends nothing more never
|
|
1791
|
+
* answers it. A body whose end comes later than this after the cancel is abandoned too, so a
|
|
1792
|
+
* handler still streaming its response and still reading the upload then loses the upload.
|
|
1793
|
+
*/
|
|
1794
|
+
const CANCEL_GRACE_MS = 1e3;
|
|
1795
|
+
/**
|
|
1796
|
+
* The response body as a stream that the request's signal can end at any time.
|
|
1797
|
+
*
|
|
1798
|
+
* An abort errors the stream with the abort's reason at once — a read waiting
|
|
1799
|
+
* for a peer that sends nothing more rejects then, not when a next chunk
|
|
1800
|
+
* arrives, which may be never — and abandons the call (`release`), so on a
|
|
1801
|
+
* multiplexed transport its port is given back whether or not anyone still
|
|
1802
|
+
* reads the body. The listener is removed once the body ends, however it ends:
|
|
1803
|
+
* read to its end, cancelled, failed or aborted. A body nobody reads or cancels
|
|
1804
|
+
* keeps it until the signal aborts (and keeps the call open until then anyway).
|
|
1805
|
+
* A cancel abandons the call too — unless the read in flight comes back with the
|
|
1806
|
+
* body's end, which finishes it instead (see `cancel`).
|
|
1807
|
+
*/
|
|
1808
|
+
function responseBody(body, signal, release) {
|
|
1809
|
+
const it = body[Symbol.asyncIterator]();
|
|
1810
|
+
let controller;
|
|
1811
|
+
let ended = false;
|
|
1812
|
+
let reading = false;
|
|
1813
|
+
const end = () => {
|
|
1814
|
+
ended = true;
|
|
1815
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1816
|
+
};
|
|
1817
|
+
const onAbort = () => {
|
|
1818
|
+
if (ended) return;
|
|
1819
|
+
end();
|
|
1820
|
+
try {
|
|
1821
|
+
controller.error(abortReason$1(signal));
|
|
1822
|
+
} catch {}
|
|
1823
|
+
release?.();
|
|
1824
|
+
};
|
|
1825
|
+
return new ReadableStream({
|
|
1826
|
+
start(c) {
|
|
1827
|
+
controller = c;
|
|
1828
|
+
if (signal?.aborted) onAbort();
|
|
1829
|
+
else signal?.addEventListener("abort", onAbort, { once: true });
|
|
1830
|
+
},
|
|
1831
|
+
async pull(c) {
|
|
1832
|
+
try {
|
|
1833
|
+
reading = true;
|
|
1834
|
+
const { done, value } = await it.next().finally(() => {
|
|
1835
|
+
reading = false;
|
|
1836
|
+
});
|
|
1837
|
+
if (ended) return;
|
|
1838
|
+
if (done) {
|
|
1839
|
+
end();
|
|
1840
|
+
c.close();
|
|
1841
|
+
return;
|
|
1842
|
+
}
|
|
1843
|
+
c.enqueue(value);
|
|
1844
|
+
} catch (err) {
|
|
1845
|
+
if (ended) return;
|
|
1846
|
+
end();
|
|
1847
|
+
c.error(err);
|
|
1848
|
+
}
|
|
1849
|
+
},
|
|
1850
|
+
cancel() {
|
|
1851
|
+
end();
|
|
1852
|
+
const returned = Promise.resolve(it.return?.()).catch(() => {});
|
|
1853
|
+
if (!reading) return;
|
|
1854
|
+
const grace = setTimeout(() => void release?.(), CANCEL_GRACE_MS);
|
|
1855
|
+
grace.unref?.();
|
|
1856
|
+
returned.finally(() => clearTimeout(grace));
|
|
1857
|
+
}
|
|
1858
|
+
});
|
|
1796
1859
|
}
|
|
1797
1860
|
//#endregion
|
|
1798
1861
|
//#region src/core/data-channels.ts
|
|
@@ -2120,8 +2183,11 @@
|
|
|
2120
2183
|
error
|
|
2121
2184
|
});
|
|
2122
2185
|
};
|
|
2123
|
-
|
|
2124
|
-
|
|
2186
|
+
const listener = (event) => {
|
|
2187
|
+
onMessage(event);
|
|
2188
|
+
};
|
|
2189
|
+
port.addEventListener("message", listener);
|
|
2190
|
+
return () => port.removeEventListener("message", listener);
|
|
2125
2191
|
}
|
|
2126
2192
|
/**
|
|
2127
2193
|
* Wraps an async iterable so that an `AbortSignal` firing causes the wrapper
|
|
@@ -2526,6 +2592,33 @@
|
|
|
2526
2592
|
}
|
|
2527
2593
|
}
|
|
2528
2594
|
/**
|
|
2595
|
+
* Runs `fn` on a later task, never a microtask: a producer looping on
|
|
2596
|
+
* `whenPortReady` must let the transport's own events run. `setImmediate`
|
|
2597
|
+
* where it exists; elsewhere a `MessageChannel`, because nested `setTimeout`s
|
|
2598
|
+
* are clamped to 4 ms in browsers.
|
|
2599
|
+
*/
|
|
2600
|
+
const setImmediateFn = globalThis.setImmediate;
|
|
2601
|
+
/** @internal */
|
|
2602
|
+
const nextTask = setImmediateFn ? (fn) => {
|
|
2603
|
+
setImmediateFn(fn);
|
|
2604
|
+
} : (() => {
|
|
2605
|
+
let channel;
|
|
2606
|
+
const queue = [];
|
|
2607
|
+
return (fn) => {
|
|
2608
|
+
if (!channel) {
|
|
2609
|
+
channel = new MessageChannel();
|
|
2610
|
+
channel.port1.onmessage = () => queue.shift()?.();
|
|
2611
|
+
}
|
|
2612
|
+
queue.push(fn);
|
|
2613
|
+
channel.port2.postMessage(0);
|
|
2614
|
+
};
|
|
2615
|
+
})();
|
|
2616
|
+
const states = /* @__PURE__ */ new WeakMap();
|
|
2617
|
+
/** @internal Registered by the multiplexer for each port with a window. */
|
|
2618
|
+
function setPortSendState(port, state) {
|
|
2619
|
+
states.set(port, state);
|
|
2620
|
+
}
|
|
2621
|
+
/**
|
|
2529
2622
|
* One virtual port.
|
|
2530
2623
|
*
|
|
2531
2624
|
* `deliver` and `markClosed` are deliberately not on `port`: the consumer holds
|
|
@@ -2540,6 +2633,14 @@
|
|
|
2540
2633
|
if (closing.signal.aborted) return;
|
|
2541
2634
|
closing.abort(reason ?? /* @__PURE__ */ new Error("webrun-rpc: the port was closed"));
|
|
2542
2635
|
};
|
|
2636
|
+
const close = (reason) => {
|
|
2637
|
+
if (closed) return;
|
|
2638
|
+
closed = true;
|
|
2639
|
+
const notify = requestClose;
|
|
2640
|
+
listeners.clear();
|
|
2641
|
+
notify(reason);
|
|
2642
|
+
abort(reason);
|
|
2643
|
+
};
|
|
2543
2644
|
const port = {
|
|
2544
2645
|
addEventListener(_type, listener) {
|
|
2545
2646
|
listeners.add(listener);
|
|
@@ -2551,24 +2652,28 @@
|
|
|
2551
2652
|
if (closed) return;
|
|
2552
2653
|
send(message, transfer);
|
|
2553
2654
|
},
|
|
2554
|
-
close
|
|
2555
|
-
if (closed) return;
|
|
2556
|
-
closed = true;
|
|
2557
|
-
const notify = requestClose;
|
|
2558
|
-
listeners.clear();
|
|
2559
|
-
notify();
|
|
2560
|
-
abort();
|
|
2561
|
-
}
|
|
2655
|
+
close
|
|
2562
2656
|
};
|
|
2563
2657
|
setPortCloseSignal(port, closing.signal);
|
|
2658
|
+
const dispose = Symbol.dispose;
|
|
2659
|
+
if (dispose) Object.defineProperty(port, dispose, {
|
|
2660
|
+
value: () => close(),
|
|
2661
|
+
configurable: true
|
|
2662
|
+
});
|
|
2564
2663
|
return {
|
|
2565
2664
|
port,
|
|
2566
2665
|
deliver(payload) {
|
|
2567
2666
|
if (closed) return;
|
|
2568
2667
|
const event = new MessageEvent("message", { data: payload });
|
|
2668
|
+
let pending;
|
|
2569
2669
|
for (const listener of [...listeners]) try {
|
|
2570
|
-
listener(event);
|
|
2670
|
+
const result = listener(event);
|
|
2671
|
+
if (result && typeof result.then === "function") {
|
|
2672
|
+
pending ??= [];
|
|
2673
|
+
pending.push(result);
|
|
2674
|
+
}
|
|
2571
2675
|
} catch {}
|
|
2676
|
+
return pending ? Promise.allSettled(pending) : void 0;
|
|
2572
2677
|
},
|
|
2573
2678
|
markClosed(reason) {
|
|
2574
2679
|
closed = true;
|
|
@@ -2577,53 +2682,309 @@
|
|
|
2577
2682
|
},
|
|
2578
2683
|
isClosed() {
|
|
2579
2684
|
return closed;
|
|
2685
|
+
},
|
|
2686
|
+
hasListeners() {
|
|
2687
|
+
return listeners.size > 0;
|
|
2580
2688
|
}
|
|
2581
2689
|
};
|
|
2582
2690
|
}
|
|
2583
2691
|
/**
|
|
2584
|
-
* The
|
|
2585
|
-
*
|
|
2692
|
+
* The id of keepalive's `ping` / `pong`: one no port ever takes. An older mux
|
|
2693
|
+
* whose codec passes unknown envelopes through reads them as a close of that
|
|
2694
|
+
* id, which must therefore not be a real port (0 is the initiator's first).
|
|
2586
2695
|
*/
|
|
2587
|
-
const
|
|
2696
|
+
const KEEPALIVE_ID = Number.MAX_SAFE_INTEGER;
|
|
2697
|
+
/** The longest delay a timer holds; past it `setInterval` fires at once. */
|
|
2698
|
+
const MAX_TIMER_MS = 2 ** 31 - 1;
|
|
2588
2699
|
/** Ceiling on concurrently open virtual ports. Bounds the id table only. */
|
|
2589
2700
|
const DEFAULT_MAX_PORTS = 1024;
|
|
2590
2701
|
/**
|
|
2702
|
+
* What a message costs a port's window when the codec cannot say
|
|
2703
|
+
* (`PortCodec.encode`): its bytes and string lengths, found in its values up
|
|
2704
|
+
* to 4 levels deep (at most 256 values), and at least 64. Both ends compute it
|
|
2705
|
+
* from the same structured value, so they agree.
|
|
2706
|
+
*/
|
|
2707
|
+
function estimateMessageSize(payload) {
|
|
2708
|
+
let budget = 256;
|
|
2709
|
+
const walk = (v, depth) => {
|
|
2710
|
+
if (budget-- <= 0) return 0;
|
|
2711
|
+
if (typeof v === "string") return v.length;
|
|
2712
|
+
if (v instanceof ArrayBuffer) return v.byteLength;
|
|
2713
|
+
if (ArrayBuffer.isView(v)) return v.byteLength;
|
|
2714
|
+
if (v === null || typeof v !== "object" || depth >= 4) return 8;
|
|
2715
|
+
let n = 16;
|
|
2716
|
+
if (Array.isArray(v)) for (const x of v) n += walk(x, depth + 1);
|
|
2717
|
+
else for (const k in v) n += k.length + walk(v[k], depth + 1);
|
|
2718
|
+
return n;
|
|
2719
|
+
};
|
|
2720
|
+
return Math.max(64, walk(payload, 0));
|
|
2721
|
+
}
|
|
2722
|
+
/**
|
|
2591
2723
|
* The default `PortMux`: emulates multiplexing over a single port.
|
|
2592
2724
|
*
|
|
2593
2725
|
* A transport that already multiplexes natively supplies its own `PortMux`
|
|
2594
2726
|
* instead — this implementation is for transports that offer one pipe.
|
|
2595
2727
|
*/
|
|
2596
2728
|
function multiplexPort(port, options) {
|
|
2597
|
-
const { codec, onPort, side = "initiator", maxPorts = DEFAULT_MAX_PORTS, maxMessageSize } = options;
|
|
2729
|
+
const { codec, onPort, side = "initiator", maxPorts = DEFAULT_MAX_PORTS, maxMessageSize, signal, window, frameSize, keepalive, onLeak } = options;
|
|
2730
|
+
const misses = keepalive?.misses ?? 3;
|
|
2731
|
+
if (keepalive && !(Number.isFinite(keepalive.intervalMs) && keepalive.intervalMs > 0 && keepalive.intervalMs <= MAX_TIMER_MS && Number.isSafeInteger(misses) && misses > 0)) throw new RangeError(`webrun-rpc: keepalive needs an intervalMs in (0, ${MAX_TIMER_MS}] and a positive integer misses`);
|
|
2732
|
+
if (frameSize != null && !(Number.isSafeInteger(frameSize) && frameSize > 0)) throw new RangeError(`webrun-rpc: frameSize must be a positive integer, got ${frameSize}`);
|
|
2598
2733
|
const open = /* @__PURE__ */ new Map();
|
|
2734
|
+
const leaks = onLeak ? new FinalizationRegistry(({ id, record, release }) => {
|
|
2735
|
+
const entry = open.get(id);
|
|
2736
|
+
if (entry?.handle !== record) return;
|
|
2737
|
+
const { handle: _, credit: __, partial: ___, ...info } = entry;
|
|
2738
|
+
if (!record.hasListeners()) {
|
|
2739
|
+
release("leaked");
|
|
2740
|
+
record.markClosed(/* @__PURE__ */ new Error("webrun-rpc: the port was dropped without close()"));
|
|
2741
|
+
}
|
|
2742
|
+
try {
|
|
2743
|
+
onLeak?.(info);
|
|
2744
|
+
} catch {}
|
|
2745
|
+
}) : void 0;
|
|
2746
|
+
const touch = (id) => {
|
|
2747
|
+
const entry = open.get(id);
|
|
2748
|
+
if (entry) entry.lastActivity = Date.now();
|
|
2749
|
+
};
|
|
2599
2750
|
let nextId = side === "initiator" ? 0 : 1;
|
|
2600
2751
|
let muxClosed = false;
|
|
2752
|
+
/** This end counts encoded bytes and reassembles frames. */
|
|
2753
|
+
const exactCodec = codec.encode != null && codec.decode != null;
|
|
2601
2754
|
const post = (envelope, transfer) => {
|
|
2602
2755
|
if (muxClosed) return;
|
|
2756
|
+
codecPost(envelope, transfer);
|
|
2757
|
+
};
|
|
2758
|
+
/** Like `post`, also while closing: the mux's own close notices. */
|
|
2759
|
+
const codecPost = (envelope, transfer) => {
|
|
2603
2760
|
try {
|
|
2604
2761
|
codec.post(port, envelope, transfer);
|
|
2605
2762
|
} catch {}
|
|
2606
2763
|
};
|
|
2607
|
-
const
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2764
|
+
const outgoing = /* @__PURE__ */ new Map();
|
|
2765
|
+
const ring = [];
|
|
2766
|
+
let pumping = false;
|
|
2767
|
+
const pump = async () => {
|
|
2768
|
+
pumping = true;
|
|
2769
|
+
try {
|
|
2770
|
+
while (ring.length && !muxClosed) {
|
|
2771
|
+
if (port.whenDrained) await port.whenDrained();
|
|
2772
|
+
if (muxClosed) break;
|
|
2773
|
+
const id = ring.shift();
|
|
2774
|
+
const queue = outgoing.get(id);
|
|
2775
|
+
const item = queue?.shift();
|
|
2776
|
+
if (queue?.length) ring.push(id);
|
|
2777
|
+
else outgoing.delete(id);
|
|
2778
|
+
item?.();
|
|
2779
|
+
}
|
|
2780
|
+
} catch (err) {
|
|
2781
|
+
close(err).catch(() => {});
|
|
2782
|
+
} finally {
|
|
2783
|
+
pumping = false;
|
|
2784
|
+
}
|
|
2785
|
+
};
|
|
2786
|
+
const sendOnPort = (id, item) => {
|
|
2787
|
+
if (frameSize == null) {
|
|
2788
|
+
item();
|
|
2789
|
+
return;
|
|
2790
|
+
}
|
|
2791
|
+
let queue = outgoing.get(id);
|
|
2792
|
+
if (!queue) {
|
|
2793
|
+
queue = [];
|
|
2794
|
+
outgoing.set(id, queue);
|
|
2795
|
+
ring.push(id);
|
|
2796
|
+
}
|
|
2797
|
+
queue.push(item);
|
|
2798
|
+
if (!pumping) pump();
|
|
2799
|
+
};
|
|
2800
|
+
const dropOutgoing = (id) => {
|
|
2801
|
+
if (!outgoing.delete(id)) return;
|
|
2802
|
+
const at = ring.indexOf(id);
|
|
2803
|
+
if (at >= 0) ring.splice(at, 1);
|
|
2804
|
+
};
|
|
2805
|
+
const postBytes = (bytes) => {
|
|
2806
|
+
if (muxClosed) return;
|
|
2807
|
+
try {
|
|
2808
|
+
port.postMessage(bytes);
|
|
2809
|
+
} catch {}
|
|
2810
|
+
};
|
|
2811
|
+
const releaseWaiters = (c) => {
|
|
2812
|
+
for (const w of c?.waiters.splice(0) ?? []) w.resolve();
|
|
2813
|
+
};
|
|
2814
|
+
const flush = (c) => {
|
|
2815
|
+
while (c.queue.length) {
|
|
2816
|
+
const next = c.queue[0];
|
|
2817
|
+
if (c.enforced && c.outstanding > 0 && c.outstanding + next.size > c.window) break;
|
|
2818
|
+
c.queue.shift();
|
|
2819
|
+
c.queued -= next.size;
|
|
2820
|
+
c.outstanding += next.size;
|
|
2821
|
+
next.send();
|
|
2822
|
+
}
|
|
2823
|
+
for (const w of [...c.waiters]) if (c.queued <= w.below) {
|
|
2824
|
+
c.waiters.splice(c.waiters.indexOf(w), 1);
|
|
2825
|
+
w.resolve();
|
|
2826
|
+
}
|
|
2827
|
+
};
|
|
2828
|
+
const grant = (id, c, bytes) => {
|
|
2829
|
+
c.unconsumed -= bytes;
|
|
2830
|
+
c.toGrant += bytes;
|
|
2831
|
+
const send = () => {
|
|
2832
|
+
c.grantScheduled = false;
|
|
2833
|
+
if (!c.toGrant || !open.has(id)) return;
|
|
2614
2834
|
post({
|
|
2835
|
+
type: "credit",
|
|
2836
|
+
id,
|
|
2837
|
+
bytes: c.toGrant
|
|
2838
|
+
});
|
|
2839
|
+
c.toGrant = 0;
|
|
2840
|
+
};
|
|
2841
|
+
if (c.toGrant >= c.window / 4) {
|
|
2842
|
+
send();
|
|
2843
|
+
return;
|
|
2844
|
+
}
|
|
2845
|
+
if (c.grantScheduled) return;
|
|
2846
|
+
c.grantScheduled = true;
|
|
2847
|
+
setTimeout(send, 0);
|
|
2848
|
+
};
|
|
2849
|
+
const attach = (id, meta, portWindow, opener, exact) => {
|
|
2850
|
+
const c = typeof portWindow === "number" && Number.isSafeInteger(portWindow) && portWindow > 0 ? {
|
|
2851
|
+
window: portWindow,
|
|
2852
|
+
enforced: false,
|
|
2853
|
+
opener,
|
|
2854
|
+
exact,
|
|
2855
|
+
outstanding: 0,
|
|
2856
|
+
queue: [],
|
|
2857
|
+
queued: 0,
|
|
2858
|
+
unconsumed: 0,
|
|
2859
|
+
waiters: [],
|
|
2860
|
+
toGrant: 0,
|
|
2861
|
+
grantScheduled: false
|
|
2862
|
+
} : void 0;
|
|
2863
|
+
const self = newVirtualPort((payload, transfer) => {
|
|
2864
|
+
touch(id);
|
|
2865
|
+
const envelope = {
|
|
2866
|
+
type: "message",
|
|
2867
|
+
id,
|
|
2868
|
+
payload
|
|
2869
|
+
};
|
|
2870
|
+
if (!c) {
|
|
2871
|
+
post(envelope, transfer);
|
|
2872
|
+
return;
|
|
2873
|
+
}
|
|
2874
|
+
const bytes = c.exact ? codec.encode?.(envelope) : void 0;
|
|
2875
|
+
const size = bytes?.byteLength ?? estimateMessageSize(payload);
|
|
2876
|
+
const waits = frameSize != null || c.queue.length > 0 || c.enforced && c.outstanding > 0 && c.outstanding + size > c.window;
|
|
2877
|
+
const held = !bytes && waits ? {
|
|
2878
|
+
...envelope,
|
|
2879
|
+
payload: structuredClone(payload, { transfer })
|
|
2880
|
+
} : void 0;
|
|
2881
|
+
const send = () => {
|
|
2882
|
+
if (!bytes) {
|
|
2883
|
+
sendOnPort(id, held ? () => post(held) : () => post(envelope, transfer));
|
|
2884
|
+
return;
|
|
2885
|
+
}
|
|
2886
|
+
if (frameSize != null && c.enforced && c.exact && bytes.byteLength > frameSize) {
|
|
2887
|
+
for (let at = 0; at < bytes.byteLength; at += frameSize) {
|
|
2888
|
+
const data = bytes.subarray(at, at + frameSize);
|
|
2889
|
+
const frame = at + frameSize >= bytes.byteLength ? {
|
|
2890
|
+
type: "frame",
|
|
2891
|
+
id,
|
|
2892
|
+
data,
|
|
2893
|
+
last: true
|
|
2894
|
+
} : {
|
|
2895
|
+
type: "frame",
|
|
2896
|
+
id,
|
|
2897
|
+
data
|
|
2898
|
+
};
|
|
2899
|
+
sendOnPort(id, () => post(frame));
|
|
2900
|
+
}
|
|
2901
|
+
return;
|
|
2902
|
+
}
|
|
2903
|
+
sendOnPort(id, () => postBytes(bytes));
|
|
2904
|
+
};
|
|
2905
|
+
c.queue.push({
|
|
2906
|
+
send,
|
|
2907
|
+
size
|
|
2908
|
+
});
|
|
2909
|
+
c.queued += size;
|
|
2910
|
+
flush(c);
|
|
2911
|
+
}, (reason) => {
|
|
2912
|
+
release(reason);
|
|
2913
|
+
record.markClosed(reason);
|
|
2914
|
+
});
|
|
2915
|
+
function release(reason) {
|
|
2916
|
+
open.delete(id);
|
|
2917
|
+
leaks?.unregister(record);
|
|
2918
|
+
if (c?.queue.length) {
|
|
2919
|
+
for (const item of c.queue.splice(0)) item.send();
|
|
2920
|
+
c.queued = 0;
|
|
2921
|
+
}
|
|
2922
|
+
sendOnPort(id, () => post({
|
|
2615
2923
|
type: "close",
|
|
2616
2924
|
id,
|
|
2617
2925
|
reason
|
|
2926
|
+
}));
|
|
2927
|
+
releaseWaiters(c);
|
|
2928
|
+
}
|
|
2929
|
+
const { markClosed, isClosed, hasListeners } = self;
|
|
2930
|
+
if (c) {
|
|
2931
|
+
const deliver = self.deliver;
|
|
2932
|
+
self.deliver = (payload, size = estimateMessageSize(payload)) => {
|
|
2933
|
+
c.unconsumed += size;
|
|
2934
|
+
const done = deliver(payload);
|
|
2935
|
+
if (done) done.then(() => grant(id, c, size));
|
|
2936
|
+
else grant(id, c, size);
|
|
2937
|
+
return done;
|
|
2938
|
+
};
|
|
2939
|
+
setPortSendState(self.port, {
|
|
2940
|
+
bufferedAmount: () => c.queued,
|
|
2941
|
+
ready: (below = 0) => new Promise((resolve) => {
|
|
2942
|
+
if (c.queued <= below || isClosed()) nextTask(resolve);
|
|
2943
|
+
else c.waiters.push({
|
|
2944
|
+
below,
|
|
2945
|
+
resolve: () => nextTask(resolve)
|
|
2946
|
+
});
|
|
2947
|
+
})
|
|
2618
2948
|
});
|
|
2619
|
-
|
|
2620
|
-
|
|
2949
|
+
}
|
|
2950
|
+
const record = {
|
|
2951
|
+
deliver: self.deliver,
|
|
2952
|
+
markClosed,
|
|
2953
|
+
isClosed,
|
|
2954
|
+
hasListeners,
|
|
2955
|
+
close: (reason) => {
|
|
2956
|
+
if (isClosed()) return;
|
|
2957
|
+
release(reason);
|
|
2958
|
+
markClosed(reason);
|
|
2959
|
+
}
|
|
2960
|
+
};
|
|
2961
|
+
leaks?.register(self.port, {
|
|
2962
|
+
id,
|
|
2963
|
+
record,
|
|
2964
|
+
release
|
|
2965
|
+
}, record);
|
|
2966
|
+
const now = Date.now();
|
|
2967
|
+
open.set(id, {
|
|
2968
|
+
id,
|
|
2969
|
+
meta,
|
|
2970
|
+
openedAt: now,
|
|
2971
|
+
lastActivity: now,
|
|
2972
|
+
handle: record,
|
|
2973
|
+
credit: c
|
|
2621
2974
|
});
|
|
2622
|
-
open.set(id, self);
|
|
2623
2975
|
return self;
|
|
2624
2976
|
};
|
|
2625
|
-
const handleEnvelope = (envelope) => {
|
|
2977
|
+
const handleEnvelope = (envelope, event) => {
|
|
2626
2978
|
if (muxClosed) return;
|
|
2979
|
+
heardSincePing = true;
|
|
2980
|
+
if (envelope.type === "ping" || envelope.type === "pong") {
|
|
2981
|
+
peerKeepsAlive = true;
|
|
2982
|
+
if (envelope.type === "ping") post({
|
|
2983
|
+
type: "pong",
|
|
2984
|
+
id: KEEPALIVE_ID
|
|
2985
|
+
});
|
|
2986
|
+
return;
|
|
2987
|
+
}
|
|
2627
2988
|
if (envelope.type === "open") {
|
|
2628
2989
|
if (open.has(envelope.id)) return;
|
|
2629
2990
|
if (open.size >= maxPorts) {
|
|
@@ -2634,7 +2995,18 @@
|
|
|
2634
2995
|
});
|
|
2635
2996
|
return;
|
|
2636
2997
|
}
|
|
2637
|
-
const handle = attach(envelope.id);
|
|
2998
|
+
const handle = attach(envelope.id, envelope.meta, envelope.window, false, envelope.exact === true && exactCodec);
|
|
2999
|
+
const credit = open.get(envelope.id)?.credit;
|
|
3000
|
+
if (credit) post(credit.exact ? {
|
|
3001
|
+
type: "credit",
|
|
3002
|
+
id: envelope.id,
|
|
3003
|
+
bytes: 0,
|
|
3004
|
+
exact: true
|
|
3005
|
+
} : {
|
|
3006
|
+
type: "credit",
|
|
3007
|
+
id: envelope.id,
|
|
3008
|
+
bytes: 0
|
|
3009
|
+
});
|
|
2638
3010
|
let accepted = false;
|
|
2639
3011
|
if (onPort) try {
|
|
2640
3012
|
accepted = onPort(handle.port, envelope.meta) !== false;
|
|
@@ -2649,57 +3021,191 @@
|
|
|
2649
3021
|
id: envelope.id,
|
|
2650
3022
|
reason: "rejected"
|
|
2651
3023
|
});
|
|
3024
|
+
return;
|
|
3025
|
+
}
|
|
3026
|
+
return;
|
|
3027
|
+
}
|
|
3028
|
+
const entry = open.get(envelope.id);
|
|
3029
|
+
if (envelope.type === "credit") {
|
|
3030
|
+
const c = entry?.credit;
|
|
3031
|
+
if (!c || !(envelope.bytes >= 0)) return;
|
|
3032
|
+
if (!c.enforced && c.opener) {
|
|
3033
|
+
if (c.exact && envelope.exact !== true) {
|
|
3034
|
+
c.exact = false;
|
|
3035
|
+
c.outstanding = 0;
|
|
3036
|
+
}
|
|
3037
|
+
post({
|
|
3038
|
+
type: "credit",
|
|
3039
|
+
id: envelope.id,
|
|
3040
|
+
bytes: 0
|
|
3041
|
+
});
|
|
2652
3042
|
}
|
|
3043
|
+
c.enforced = true;
|
|
3044
|
+
c.outstanding = Math.max(0, c.outstanding - envelope.bytes);
|
|
3045
|
+
flush(c);
|
|
2653
3046
|
return;
|
|
2654
3047
|
}
|
|
2655
|
-
const handle = open.get(envelope.id);
|
|
2656
3048
|
if (envelope.type === "message") {
|
|
2657
|
-
|
|
2658
|
-
|
|
3049
|
+
const sizeOf = (c) => (c.exact ? byteLengthOf(event.data) : void 0) ?? estimateMessageSize(envelope.payload);
|
|
3050
|
+
if (!entry) return;
|
|
3051
|
+
touch(envelope.id);
|
|
3052
|
+
entry.handle.deliver(envelope.payload, entry.credit ? sizeOf(entry.credit) : void 0);
|
|
2659
3053
|
return;
|
|
2660
3054
|
}
|
|
2661
|
-
if (
|
|
3055
|
+
if (envelope.type === "frame") {
|
|
3056
|
+
if (!entry?.credit?.exact || !codec.decode || !(envelope.data instanceof Uint8Array)) return;
|
|
3057
|
+
touch(envelope.id);
|
|
3058
|
+
const partial = entry.partial ?? {
|
|
3059
|
+
chunks: [],
|
|
3060
|
+
bytes: 0
|
|
3061
|
+
};
|
|
3062
|
+
entry.partial = partial;
|
|
3063
|
+
partial.chunks.push(envelope.data);
|
|
3064
|
+
partial.bytes += envelope.data.byteLength;
|
|
3065
|
+
if (!envelope.last) return;
|
|
3066
|
+
entry.partial = void 0;
|
|
3067
|
+
const whole = new Uint8Array(partial.bytes);
|
|
3068
|
+
let at = 0;
|
|
3069
|
+
for (const chunk of partial.chunks) {
|
|
3070
|
+
whole.set(chunk, at);
|
|
3071
|
+
at += chunk.byteLength;
|
|
3072
|
+
}
|
|
3073
|
+
const inner = codec.decode(whole);
|
|
3074
|
+
if (inner?.type !== "message" || inner.id !== envelope.id) return;
|
|
3075
|
+
entry.handle.deliver(inner.payload, whole.byteLength);
|
|
3076
|
+
return;
|
|
3077
|
+
}
|
|
3078
|
+
dropOutgoing(envelope.id);
|
|
3079
|
+
if (!entry) return;
|
|
2662
3080
|
open.delete(envelope.id);
|
|
2663
|
-
|
|
3081
|
+
releaseWaiters(entry.credit);
|
|
3082
|
+
entry.handle.markClosed(/* @__PURE__ */ new Error("webrun-rpc: the peer closed the port"));
|
|
2664
3083
|
};
|
|
2665
3084
|
const listener = (event) => {
|
|
2666
3085
|
const envelope = codec.read(event);
|
|
2667
|
-
if (envelope) handleEnvelope(envelope);
|
|
3086
|
+
if (envelope) handleEnvelope(envelope, event);
|
|
3087
|
+
};
|
|
3088
|
+
let peerKeepsAlive = false;
|
|
3089
|
+
let heardSincePing = true;
|
|
3090
|
+
let unanswered = 0;
|
|
3091
|
+
let pinger;
|
|
3092
|
+
const tick = () => {
|
|
3093
|
+
if (!keepalive) return;
|
|
3094
|
+
const backlogged = (port.bufferedAmount ?? 0) > 0;
|
|
3095
|
+
if (heardSincePing) unanswered = 0;
|
|
3096
|
+
else if (peerKeepsAlive && !backlogged) unanswered++;
|
|
3097
|
+
if (unanswered >= misses) {
|
|
3098
|
+
close(/* @__PURE__ */ new Error(`webrun-rpc: the peer did not answer ${misses} keepalive pings, ${keepalive.intervalMs} ms apart`)).catch(() => {});
|
|
3099
|
+
return;
|
|
3100
|
+
}
|
|
3101
|
+
heardSincePing = false;
|
|
3102
|
+
post({
|
|
3103
|
+
type: "ping",
|
|
3104
|
+
id: KEEPALIVE_ID
|
|
3105
|
+
});
|
|
3106
|
+
};
|
|
3107
|
+
const close = async (reason) => {
|
|
3108
|
+
if (muxClosed) return;
|
|
3109
|
+
clearInterval(pinger);
|
|
3110
|
+
muxClosed = true;
|
|
3111
|
+
signal?.removeEventListener("abort", onTransportAbort);
|
|
3112
|
+
port.removeEventListener("message", listener);
|
|
3113
|
+
const closedNow = /* @__PURE__ */ new Set();
|
|
3114
|
+
for (const [id, { handle, credit }] of [...open]) {
|
|
3115
|
+
closedNow.add(id);
|
|
3116
|
+
codecPost({
|
|
3117
|
+
type: "close",
|
|
3118
|
+
id
|
|
3119
|
+
});
|
|
3120
|
+
open.delete(id);
|
|
3121
|
+
releaseWaiters(credit);
|
|
3122
|
+
handle.markClosed(reason ?? /* @__PURE__ */ new Error("webrun-rpc: the multiplexer was closed"));
|
|
3123
|
+
}
|
|
3124
|
+
for (const id of outgoing.keys()) if (!closedNow.has(id)) codecPost({
|
|
3125
|
+
type: "close",
|
|
3126
|
+
id
|
|
3127
|
+
});
|
|
3128
|
+
outgoing.clear();
|
|
3129
|
+
ring.length = 0;
|
|
3130
|
+
await port.close?.();
|
|
3131
|
+
};
|
|
3132
|
+
const onTransportAbort = () => {
|
|
3133
|
+
close(signal?.reason).catch(() => {});
|
|
2668
3134
|
};
|
|
2669
3135
|
port.addEventListener("message", listener);
|
|
2670
3136
|
port.start?.();
|
|
3137
|
+
if (keepalive) {
|
|
3138
|
+
pinger = setInterval(tick, keepalive.intervalMs);
|
|
3139
|
+
pinger.unref?.();
|
|
3140
|
+
}
|
|
3141
|
+
if (signal?.aborted) close(signal.reason).catch(() => {});
|
|
3142
|
+
else signal?.addEventListener("abort", onTransportAbort, { once: true });
|
|
2671
3143
|
return {
|
|
2672
3144
|
maxMessageSize,
|
|
2673
|
-
async openPort(meta) {
|
|
3145
|
+
async openPort(meta, { signal: portSignal } = {}) {
|
|
2674
3146
|
if (muxClosed) throw new Error("webrun-rpc: the multiplexer is closed");
|
|
2675
|
-
if (
|
|
3147
|
+
if (portSignal?.aborted) throw portSignal.reason;
|
|
3148
|
+
if (open.size >= maxPorts) throw new RangeError(`webrun-rpc: maxPorts (${maxPorts}) reached; open ports by kind: ${describeKinds(open.values())}`);
|
|
2676
3149
|
while (open.has(nextId)) nextId += 2;
|
|
2677
3150
|
const id = nextId;
|
|
2678
3151
|
nextId += 2;
|
|
2679
|
-
const handle = attach(id);
|
|
2680
|
-
post({
|
|
3152
|
+
const handle = attach(id, meta, window, true, exactCodec);
|
|
3153
|
+
post(window ? exactCodec ? {
|
|
3154
|
+
type: "open",
|
|
3155
|
+
id,
|
|
3156
|
+
meta,
|
|
3157
|
+
window,
|
|
3158
|
+
exact: true
|
|
3159
|
+
} : {
|
|
3160
|
+
type: "open",
|
|
3161
|
+
id,
|
|
3162
|
+
meta,
|
|
3163
|
+
window
|
|
3164
|
+
} : {
|
|
2681
3165
|
type: "open",
|
|
2682
3166
|
id,
|
|
2683
3167
|
meta
|
|
2684
3168
|
});
|
|
3169
|
+
if (portSignal) {
|
|
3170
|
+
const record = open.get(id)?.handle;
|
|
3171
|
+
const onAbort = () => {
|
|
3172
|
+
if (record && open.get(id)?.handle === record) record.close(portSignal.reason);
|
|
3173
|
+
};
|
|
3174
|
+
portSignal.addEventListener("abort", onAbort, { once: true });
|
|
3175
|
+
getPortCloseSignal(handle.port)?.addEventListener("abort", () => portSignal.removeEventListener("abort", onAbort), { once: true });
|
|
3176
|
+
}
|
|
2685
3177
|
return handle.port;
|
|
2686
3178
|
},
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
}
|
|
2697
|
-
muxClosed = true;
|
|
2698
|
-
port.removeEventListener("message", listener);
|
|
2699
|
-
await port.close?.();
|
|
3179
|
+
close,
|
|
3180
|
+
ports() {
|
|
3181
|
+
return [...open.values()].map(({ handle: _, credit, ...info }) => credit ? {
|
|
3182
|
+
...info,
|
|
3183
|
+
...credit.enforced ? { window: credit.window } : {},
|
|
3184
|
+
queued: credit.queued,
|
|
3185
|
+
outstanding: credit.outstanding,
|
|
3186
|
+
unconsumed: credit.unconsumed
|
|
3187
|
+
} : info);
|
|
2700
3188
|
}
|
|
2701
3189
|
};
|
|
2702
3190
|
}
|
|
3191
|
+
/** The byte length of a received message's data, or `undefined` if it is not bytes. */
|
|
3192
|
+
function byteLengthOf(data) {
|
|
3193
|
+
if (data instanceof ArrayBuffer || ArrayBuffer.isView(data)) return data.byteLength;
|
|
3194
|
+
}
|
|
3195
|
+
/**
|
|
3196
|
+
* `"http: 1000, ws: 24"`: the three commonest `meta.kind`s among `entries`, so
|
|
3197
|
+
* a full table names who filled it. A port without a kind counts as `"?"`.
|
|
3198
|
+
*/
|
|
3199
|
+
function describeKinds(entries) {
|
|
3200
|
+
const counts = /* @__PURE__ */ new Map();
|
|
3201
|
+
for (const { meta } of entries) {
|
|
3202
|
+
const kind = meta?.kind;
|
|
3203
|
+
const name = typeof kind === "string" ? kind : "?";
|
|
3204
|
+
counts.set(name, (counts.get(name) ?? 0) + 1);
|
|
3205
|
+
}
|
|
3206
|
+
if (!counts.size) return "none";
|
|
3207
|
+
return [...counts].sort((x, y) => y[1] - x[1]).slice(0, 3).map(([name, n]) => `${name}: ${n}`).join(", ");
|
|
3208
|
+
}
|
|
2703
3209
|
/**
|
|
2704
3210
|
* Announced with every port this pair opens. Layer 1 never reads `meta` — it is
|
|
2705
3211
|
* here so a peer running something else over the same multiplexer can tell a
|
|
@@ -2806,7 +3312,7 @@
|
|
|
2806
3312
|
const candidate = value;
|
|
2807
3313
|
if (typeof candidate.id !== "number") return false;
|
|
2808
3314
|
if (!Number.isInteger(candidate.id) || candidate.id < 0) return false;
|
|
2809
|
-
return candidate.type === "open" || candidate.type === "message" || candidate.type === "close";
|
|
3315
|
+
return candidate.type === "open" || candidate.type === "message" || candidate.type === "close" || candidate.type === "credit" || candidate.type === "frame" || candidate.type === "ping" || candidate.type === "pong";
|
|
2810
3316
|
}
|
|
2811
3317
|
/**
|
|
2812
3318
|
* For ports whose messages are structured values — a real `MessagePort`, a
|
|
@@ -2968,7 +3474,7 @@
|
|
|
2968
3474
|
finish();
|
|
2969
3475
|
return response;
|
|
2970
3476
|
}
|
|
2971
|
-
return new Response(guardBody(response.body, lost, () => lostReason, finish), {
|
|
3477
|
+
return new Response(guardBody(response.body, lost, () => lostReason, finish, request.signal), {
|
|
2972
3478
|
status: response.status,
|
|
2973
3479
|
statusText: response.statusText,
|
|
2974
3480
|
headers: response.headers
|
|
@@ -2987,33 +3493,65 @@
|
|
|
2987
3493
|
return asyncDisposable(link, () => link.close());
|
|
2988
3494
|
}
|
|
2989
3495
|
/**
|
|
2990
|
-
* A response body that reports its end (done, cancelled or
|
|
2991
|
-
* with the link's reason when the link is lost mid-stream.
|
|
3496
|
+
* A response body that reports its end (done, cancelled, failed or aborted) and
|
|
3497
|
+
* fails with the link's reason when the link is lost mid-stream. An abort of the
|
|
3498
|
+
* request's signal ends it at once, read or not: the exchange is over (the layer
|
|
3499
|
+
* below has given its port back), so the request leaves `inflight` then.
|
|
2992
3500
|
*/
|
|
2993
|
-
function guardBody(body, lost, reason, finish) {
|
|
3501
|
+
function guardBody(body, lost, reason, finish, signal) {
|
|
2994
3502
|
const reader = body.getReader();
|
|
3503
|
+
let controller;
|
|
3504
|
+
let ended = false;
|
|
3505
|
+
const end = () => {
|
|
3506
|
+
ended = true;
|
|
3507
|
+
signal?.removeEventListener("abort", onAbort);
|
|
3508
|
+
finish();
|
|
3509
|
+
};
|
|
3510
|
+
const onAbort = () => {
|
|
3511
|
+
if (ended) return;
|
|
3512
|
+
end();
|
|
3513
|
+
reader.cancel().catch(() => {});
|
|
3514
|
+
try {
|
|
3515
|
+
controller.error(abortError(signal));
|
|
3516
|
+
} catch {}
|
|
3517
|
+
};
|
|
2995
3518
|
return new ReadableStream({
|
|
3519
|
+
start(c) {
|
|
3520
|
+
controller = c;
|
|
3521
|
+
if (signal?.aborted) onAbort();
|
|
3522
|
+
else signal?.addEventListener("abort", onAbort, { once: true });
|
|
3523
|
+
},
|
|
2996
3524
|
async pull(controller) {
|
|
2997
3525
|
try {
|
|
2998
3526
|
const { done, value } = await Promise.race([reader.read(), lost]);
|
|
3527
|
+
if (ended) return;
|
|
2999
3528
|
if (done) {
|
|
3000
|
-
|
|
3529
|
+
end();
|
|
3001
3530
|
controller.close();
|
|
3002
3531
|
return;
|
|
3003
3532
|
}
|
|
3004
3533
|
controller.enqueue(value);
|
|
3005
3534
|
} catch (error) {
|
|
3006
|
-
|
|
3535
|
+
if (ended) return;
|
|
3536
|
+
end();
|
|
3007
3537
|
reader.cancel().catch(() => {});
|
|
3008
3538
|
controller.error(reason() ?? error);
|
|
3009
3539
|
}
|
|
3010
3540
|
},
|
|
3011
3541
|
async cancel(why) {
|
|
3012
|
-
|
|
3542
|
+
end();
|
|
3013
3543
|
await reader.cancel(why).catch(() => {});
|
|
3014
3544
|
}
|
|
3015
3545
|
});
|
|
3016
3546
|
}
|
|
3547
|
+
/** The signal's reason as an `Error` (`AbortError` when it gave none). */
|
|
3548
|
+
function abortError(signal) {
|
|
3549
|
+
const why = signal.reason;
|
|
3550
|
+
if (why instanceof Error) return why;
|
|
3551
|
+
const err = new Error(why === void 0 ? "Aborted" : String(why));
|
|
3552
|
+
err.name = "AbortError";
|
|
3553
|
+
return err;
|
|
3554
|
+
}
|
|
3017
3555
|
//#endregion
|
|
3018
3556
|
//#region src/link/locks.ts
|
|
3019
3557
|
function lockManager() {
|