@statewalker/webrun-http-browser 0.9.0 → 0.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +155 -10
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/service-worker-control.d.ts +2 -1
- package/dist/core/service-worker-control.d.ts.map +1 -1
- package/dist/core/sw-scope.d.ts +31 -0
- package/dist/core/sw-scope.d.ts.map +1 -0
- package/dist/index.js +908 -221
- package/dist/link/http-link.d.ts +16 -3
- package/dist/link/http-link.d.ts.map +1 -1
- package/dist/relay/index-sw.d.ts +8 -2
- package/dist/relay/index-sw.d.ts.map +1 -1
- package/dist/relay/index.d.ts +1 -1
- package/dist/relay/index.d.ts.map +1 -1
- package/dist/relay-sw.js +470 -81
- package/dist/relay-worker.d.ts +1 -0
- package/dist/relay-worker.d.ts.map +1 -1
- package/dist/relay-worker.js +470 -81
- package/dist/sw/handler-endpoint.d.ts +5 -2
- package/dist/sw/handler-endpoint.d.ts.map +1 -1
- package/dist/sw/http-sw-dispatcher.d.ts +114 -7
- package/dist/sw/http-sw-dispatcher.d.ts.map +1 -1
- package/dist/sw/index.d.ts +1 -0
- package/dist/sw/index.d.ts.map +1 -1
- package/dist/sw/protocol.d.ts +3 -0
- package/dist/sw/protocol.d.ts.map +1 -1
- package/dist/sw/sw-dispatcher.d.ts +5 -2
- package/dist/sw/sw-dispatcher.d.ts.map +1 -1
- package/dist/sw/sw-http-adapter.d.ts +19 -4
- package/dist/sw/sw-http-adapter.d.ts.map +1 -1
- package/dist/sw/worker-service.d.ts +5 -2
- package/dist/sw/worker-service.d.ts.map +1 -1
- package/dist/sw-worker.js +681 -141
- package/dist/sw.js +896 -218
- package/dist/worker.js +689 -115
- package/dist/ws/server.d.ts +50 -2
- package/dist/ws/server.d.ts.map +1 -1
- package/dist/ws/socket-core.d.ts +5 -2
- package/dist/ws/socket-core.d.ts.map +1 -1
- package/dist/ws/upgrade.d.ts +12 -1
- package/dist/ws/upgrade.d.ts.map +1 -1
- package/package.json +4 -3
- package/src/core/index.ts +1 -0
- package/src/core/service-worker-control.ts +5 -3
- package/src/core/sw-scope.ts +27 -0
- package/src/link/http-link.ts +20 -4
- package/src/relay/index-sw.ts +15 -2
- package/src/relay/index.ts +2 -2
- package/src/relay-worker.ts +1 -0
- package/src/sw/handler-endpoint.ts +17 -4
- package/src/sw/http-sw-dispatcher.ts +267 -67
- package/src/sw/index.ts +5 -0
- package/src/sw/protocol.ts +11 -1
- package/src/sw/sw-dispatcher.ts +17 -11
- package/src/sw/sw-http-adapter.ts +36 -10
- package/src/sw/worker-service.ts +11 -4
- package/src/ws/server.ts +166 -11
- package/src/ws/socket-core.ts +7 -2
- package/src/ws/upgrade.ts +22 -4
package/dist/relay-worker.js
CHANGED
|
@@ -1688,8 +1688,9 @@ const CLAIM_CALL = "CLAIM";
|
|
|
1688
1688
|
* Returns a function that stops answering.
|
|
1689
1689
|
*/
|
|
1690
1690
|
function handleClaimRequests(self) {
|
|
1691
|
-
|
|
1692
|
-
|
|
1691
|
+
const sw = self;
|
|
1692
|
+
return handleChannelCalls(sw, CLAIM_CALL, (event) => {
|
|
1693
|
+
const claimed = sw.clients.claim().then(() => true);
|
|
1693
1694
|
event.waitUntil?.(claimed);
|
|
1694
1695
|
return claimed;
|
|
1695
1696
|
});
|
|
@@ -1851,6 +1852,99 @@ async function* throughAbort(input, signal) {
|
|
|
1851
1852
|
}
|
|
1852
1853
|
}
|
|
1853
1854
|
/**
|
|
1855
|
+
* Runs `fn` on a later task, never a microtask: a producer looping on
|
|
1856
|
+
* `whenPortReady` must let the transport's own events run. `setImmediate`
|
|
1857
|
+
* where it exists; elsewhere a `MessageChannel`, because nested `setTimeout`s
|
|
1858
|
+
* are clamped to 4 ms in browsers.
|
|
1859
|
+
*/
|
|
1860
|
+
const setImmediateFn = globalThis.setImmediate;
|
|
1861
|
+
/** @internal */
|
|
1862
|
+
const nextTask = setImmediateFn ? (fn) => {
|
|
1863
|
+
setImmediateFn(fn);
|
|
1864
|
+
} : (() => {
|
|
1865
|
+
let channel;
|
|
1866
|
+
const queue = [];
|
|
1867
|
+
return (fn) => {
|
|
1868
|
+
if (!channel) {
|
|
1869
|
+
channel = new MessageChannel();
|
|
1870
|
+
channel.port1.onmessage = () => queue.shift()?.();
|
|
1871
|
+
}
|
|
1872
|
+
queue.push(fn);
|
|
1873
|
+
channel.port2.postMessage(0);
|
|
1874
|
+
};
|
|
1875
|
+
})();
|
|
1876
|
+
const states = /* @__PURE__ */ new WeakMap();
|
|
1877
|
+
/** @internal Registered by the multiplexer for each port with a window. */
|
|
1878
|
+
function setPortSendState(port, state) {
|
|
1879
|
+
states.set(port, state);
|
|
1880
|
+
}
|
|
1881
|
+
/**
|
|
1882
|
+
* Bytes queued on `port` because its window is spent (0 for a port without
|
|
1883
|
+
* flow control). A sender that should not queue without bound waits for
|
|
1884
|
+
* `whenPortReady` before posting more.
|
|
1885
|
+
*/
|
|
1886
|
+
function portBufferedAmount(port) {
|
|
1887
|
+
return states.get(port)?.bufferedAmount() ?? 0;
|
|
1888
|
+
}
|
|
1889
|
+
/**
|
|
1890
|
+
* Resolves once at most `below` bytes are queued on `port`, always on a later
|
|
1891
|
+
* task (for a port without flow control, on the next one).
|
|
1892
|
+
*/
|
|
1893
|
+
function whenPortReady(port, below = 0) {
|
|
1894
|
+
return states.get(port)?.ready(below) ?? new Promise((resolve) => nextTask(resolve));
|
|
1895
|
+
}
|
|
1896
|
+
const agreements = /* @__PURE__ */ new WeakMap();
|
|
1897
|
+
/** @internal Registered by the multiplexer for each port with a window. */
|
|
1898
|
+
function setPortAgreement(port, agreement) {
|
|
1899
|
+
agreements.set(port, agreement);
|
|
1900
|
+
}
|
|
1901
|
+
/**
|
|
1902
|
+
* @internal What has been agreed with the peer on `port` so far; `undefined`
|
|
1903
|
+
* on a port without a window. Not exported from the package.
|
|
1904
|
+
*/
|
|
1905
|
+
function portAgreement(port) {
|
|
1906
|
+
return agreements.get(port)?.();
|
|
1907
|
+
}
|
|
1908
|
+
const ends = /* @__PURE__ */ new WeakMap();
|
|
1909
|
+
/** @internal Registered by the multiplexer for each port with a window. */
|
|
1910
|
+
function setPortEndState(port, state) {
|
|
1911
|
+
ends.set(port, state);
|
|
1912
|
+
}
|
|
1913
|
+
/**
|
|
1914
|
+
* @internal End this side's direction of `port`: an `end` is queued behind
|
|
1915
|
+
* everything already posted on it, and anything posted afterwards is dropped.
|
|
1916
|
+
* `false`, and nothing changes, when `end` was not agreed (no window, the
|
|
1917
|
+
* handshake not answered yet, or a peer that does not speak it): the caller
|
|
1918
|
+
* then closes the port. Never waits on the peer. Not exported from the package.
|
|
1919
|
+
*/
|
|
1920
|
+
function endPort(port) {
|
|
1921
|
+
return ends.get(port)?.end() ?? false;
|
|
1922
|
+
}
|
|
1923
|
+
/**
|
|
1924
|
+
* @internal Resolves once the peer has ended its direction of `port`. It never
|
|
1925
|
+
* settles otherwise: not on a port where `end` is not agreed when it is asked
|
|
1926
|
+
* (no window, the handshake not answered yet, a peer or codec without it), and
|
|
1927
|
+
* not when the port closes first, so a caller races it against the port's
|
|
1928
|
+
* close signal. Not exported from the package.
|
|
1929
|
+
*/
|
|
1930
|
+
function whenPortEnded(port) {
|
|
1931
|
+
let waiting = peerEnds.get(port);
|
|
1932
|
+
if (!waiting) {
|
|
1933
|
+
let resolve = () => {};
|
|
1934
|
+
const promise = new Promise((r) => {
|
|
1935
|
+
resolve = r;
|
|
1936
|
+
});
|
|
1937
|
+
if (!ends.get(port)?.watchPeerEnd(resolve)) return promise;
|
|
1938
|
+
waiting = {
|
|
1939
|
+
promise,
|
|
1940
|
+
resolve
|
|
1941
|
+
};
|
|
1942
|
+
peerEnds.set(port, waiting);
|
|
1943
|
+
}
|
|
1944
|
+
return waiting.promise;
|
|
1945
|
+
}
|
|
1946
|
+
const peerEnds = /* @__PURE__ */ new WeakMap();
|
|
1947
|
+
/**
|
|
1854
1948
|
* Makes `target` disposable with `await using`: installs `dispose` as its
|
|
1855
1949
|
* `[Symbol.asyncDispose]`, where the runtime defines the symbol. Hand-written
|
|
1856
1950
|
* iterators lose what a native generator gets from `AsyncIteratorPrototype`;
|
|
@@ -1922,6 +2016,61 @@ function returningAtOnce(gen, abandon) {
|
|
|
1922
2016
|
* tests and adapters assert on it.
|
|
1923
2017
|
*/
|
|
1924
2018
|
const STREAM_ABORT = "webrun-rpc:stream-abort";
|
|
2019
|
+
/**
|
|
2020
|
+
* The `type` of a pipelined chunk: a plain port message, not a `callPort`
|
|
2021
|
+
* request, sent only on a port where both ends agreed `pipeline`. The window's
|
|
2022
|
+
* credit is its confirmation.
|
|
2023
|
+
*/
|
|
2024
|
+
const CHUNK = "webrun-rpc:chunk";
|
|
2025
|
+
/**
|
|
2026
|
+
* What a pipelined chunk counts beyond its bytes, against the window, on the
|
|
2027
|
+
* receiving side. Both codecs' windows count more for the chunk's envelope
|
|
2028
|
+
* (the estimate at least 64, msgpack's keys alone more than this), so an honest
|
|
2029
|
+
* sender never makes the receiver count past the window; a sender of empty
|
|
2030
|
+
* chunks still cannot have it hold an unbounded number of them.
|
|
2031
|
+
*/
|
|
2032
|
+
const CHUNK_OVERHEAD = 32;
|
|
2033
|
+
/**
|
|
2034
|
+
* The bytes of `view` as a plain `Uint8Array` that owns its whole buffer:
|
|
2035
|
+
* `view` itself when it already is one, else a copy. Structured clone carries
|
|
2036
|
+
* and keeps a view's whole `ArrayBuffer`, and a byte codec may decode into
|
|
2037
|
+
* views of the frame, while the window and the receiver's count see only the
|
|
2038
|
+
* view: a 4 KiB `subarray` of a 4 MiB buffer would cost 4 MiB per chunk.
|
|
2039
|
+
* Copied with the `Uint8Array` constructor, not `slice()`: a Node `Buffer`'s
|
|
2040
|
+
* `slice()` is another view on the same memory.
|
|
2041
|
+
*/
|
|
2042
|
+
function ownBytes(view) {
|
|
2043
|
+
if (view instanceof Uint8Array && view.constructor === Uint8Array && view.byteOffset === 0 && view.byteLength === view.buffer.byteLength) return view;
|
|
2044
|
+
return new Uint8Array(new Uint8Array(view.buffer, view.byteOffset, view.byteLength));
|
|
2045
|
+
}
|
|
2046
|
+
/**
|
|
2047
|
+
* A producer's chunk as bytes this end may send, in either mode: any view or
|
|
2048
|
+
* `ArrayBuffer`, normalised by `ownBytes`. Anything else fails the stream here,
|
|
2049
|
+
* as the producer's own error, rather than reaching a peer that would refuse it.
|
|
2050
|
+
*/
|
|
2051
|
+
/**
|
|
2052
|
+
* A received confirmed chunk's value, as its own bytes, put back in place of
|
|
2053
|
+
* the original in `chunk`: `listenPort` keeps the request until the handler
|
|
2054
|
+
* returns, and the original would keep its whole buffer that long. A function
|
|
2055
|
+
* of its own, so that no temporary of the suspended handler holds the
|
|
2056
|
+
* original either.
|
|
2057
|
+
*/
|
|
2058
|
+
function adoptValue(chunk) {
|
|
2059
|
+
const original = chunk.value;
|
|
2060
|
+
if (ArrayBuffer.isView(original)) chunk.value = ownBytes(original);
|
|
2061
|
+
return chunk.value;
|
|
2062
|
+
}
|
|
2063
|
+
function chunkBytes(value) {
|
|
2064
|
+
if (ArrayBuffer.isView(value)) return ownBytes(value);
|
|
2065
|
+
if (value instanceof ArrayBuffer || Object.prototype.toString.call(value) === "[object ArrayBuffer]") return new Uint8Array(value);
|
|
2066
|
+
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}`);
|
|
2067
|
+
}
|
|
2068
|
+
const MIXED_MESSAGE = "webrun-rpc: peer mixed confirmed and pipelined chunks; the stream port is closed";
|
|
2069
|
+
/** This end may pipeline its own chunks: agreed, and the window holds them. */
|
|
2070
|
+
const mayPipeline = (port) => {
|
|
2071
|
+
const agreement = portAgreement(port);
|
|
2072
|
+
return !!agreement?.enforced && agreement.caps.has("pipeline");
|
|
2073
|
+
};
|
|
1925
2074
|
/** Caller's input travels on this channel; the handler listens on it. */
|
|
1926
2075
|
const CHANNEL_IN = "in";
|
|
1927
2076
|
/** The handler's output travels on this channel; the caller listens on it. */
|
|
@@ -1934,7 +2083,9 @@ const CHANNEL_OUT = "out";
|
|
|
1934
2083
|
* same way on the other channel. Within each direction the next chunk is never
|
|
1935
2084
|
* sent until the previous one has been delivered *and* pulled past by the
|
|
1936
2085
|
* consumer (spec D11) — the reply to a chunk call *is* the confirmation, and
|
|
1937
|
-
* `listenPort` withholds it until then.
|
|
2086
|
+
* `listenPort` withholds it until then. On a port whose window both ends
|
|
2087
|
+
* speak and that agreed `pipeline`, chunks go as plain messages instead, and
|
|
2088
|
+
* the window's credit is the confirmation: up to a window is in flight.
|
|
1938
2089
|
*
|
|
1939
2090
|
* A stream port carries exactly one invocation. To make several calls, open
|
|
1940
2091
|
* several ports: `mux.openPort({ kind: "stream" })` per call.
|
|
@@ -1947,22 +2098,31 @@ function runCallerSide(port, input, options) {
|
|
|
1947
2098
|
const notice = installAbortNotice(port, controller);
|
|
1948
2099
|
abortOnPortClose(port, controller);
|
|
1949
2100
|
const clock = installStreamTimeout(controller, options.timeout);
|
|
1950
|
-
const inbound = receiveChunks(port, CHANNEL_OUT, controller, clock.touch);
|
|
1951
|
-
|
|
2101
|
+
const inbound = receiveChunks(port, CHANNEL_OUT, controller, clock.touch, notice);
|
|
2102
|
+
let sent = false;
|
|
2103
|
+
const pump = sendChunks(port, CHANNEL_IN, input, options, controller.signal, clock.touch, () => {
|
|
2104
|
+
sent = true;
|
|
2105
|
+
});
|
|
1952
2106
|
pump.catch(() => {});
|
|
1953
2107
|
return returningAtOnce((async function* () {
|
|
2108
|
+
let received = false;
|
|
1954
2109
|
try {
|
|
1955
2110
|
yield* inbound.stream;
|
|
2111
|
+
received = true;
|
|
1956
2112
|
} finally {
|
|
2113
|
+
const ended = received && sent;
|
|
1957
2114
|
if (!controller.signal.aborted) controller.abort(/* @__PURE__ */ new Error("webrun-rpc: the caller abandoned the stream"));
|
|
1958
|
-
notice.post();
|
|
2115
|
+
if (!ended) notice.post();
|
|
1959
2116
|
notice.stop();
|
|
1960
2117
|
clock.stop();
|
|
1961
2118
|
inbound.stop();
|
|
1962
2119
|
await pump.catch(() => {});
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
2120
|
+
if (!(ended && endPort(port))) {
|
|
2121
|
+
notice.post();
|
|
2122
|
+
try {
|
|
2123
|
+
await port.close?.();
|
|
2124
|
+
} catch {}
|
|
2125
|
+
}
|
|
1966
2126
|
}
|
|
1967
2127
|
})(), () => {
|
|
1968
2128
|
if (!controller.signal.aborted) controller.abort(/* @__PURE__ */ new Error("webrun-rpc: the caller abandoned the stream"));
|
|
@@ -2056,7 +2216,7 @@ function installStreamTimeout(controller, timeout) {
|
|
|
2056
2216
|
* the local consumer has not started iterating, an inbound chunk waits for it —
|
|
2057
2217
|
* which is the correct backpressure, and different from having no listener.
|
|
2058
2218
|
*/
|
|
2059
|
-
function receiveChunks(port, channelName, controller, touch) {
|
|
2219
|
+
function receiveChunks(port, channelName, controller, touch, notice) {
|
|
2060
2220
|
let deliver;
|
|
2061
2221
|
const waiting = [];
|
|
2062
2222
|
let finished = false;
|
|
@@ -2066,25 +2226,64 @@ function receiveChunks(port, channelName, controller, touch) {
|
|
|
2066
2226
|
const ended = new Promise((resolve) => {
|
|
2067
2227
|
resolveEnded = resolve;
|
|
2068
2228
|
});
|
|
2229
|
+
let resolveOver = () => {};
|
|
2230
|
+
const over = new Promise((resolve) => {
|
|
2231
|
+
resolveOver = resolve;
|
|
2232
|
+
});
|
|
2069
2233
|
let aborted = false;
|
|
2070
2234
|
let abortReason;
|
|
2071
2235
|
const ready = () => deliver || finished ? Promise.resolve() : new Promise((r) => waiting.push(r));
|
|
2072
2236
|
const wake = () => {
|
|
2073
2237
|
for (const r of waiting.splice(0)) r();
|
|
2074
2238
|
};
|
|
2075
|
-
const
|
|
2239
|
+
const violate = (message) => {
|
|
2240
|
+
poison = new Error(message);
|
|
2241
|
+
controller.abort(poison);
|
|
2242
|
+
setTimeout(() => {
|
|
2243
|
+
try {
|
|
2244
|
+
port.close?.();
|
|
2245
|
+
} catch {}
|
|
2246
|
+
}, 0);
|
|
2247
|
+
return poison;
|
|
2248
|
+
};
|
|
2249
|
+
let lastChunkArrived = false;
|
|
2250
|
+
let watching = false;
|
|
2251
|
+
const watchPeerEnd = () => {
|
|
2252
|
+
if (watching) return true;
|
|
2253
|
+
if (!portAgreement(port)?.caps.has("end")) return false;
|
|
2254
|
+
watching = true;
|
|
2255
|
+
whenPortEnded(port).then(() => {
|
|
2256
|
+
port.removeEventListener("message", watcher);
|
|
2257
|
+
if (lastChunkArrived) {
|
|
2258
|
+
resolveOver();
|
|
2259
|
+
return;
|
|
2260
|
+
}
|
|
2261
|
+
notice.post();
|
|
2262
|
+
violate("webrun-rpc: the peer ended its direction without finishing it; the stream port is closed");
|
|
2263
|
+
});
|
|
2264
|
+
return true;
|
|
2265
|
+
};
|
|
2266
|
+
const watcher = (event) => {
|
|
2267
|
+
const data = event.data;
|
|
2268
|
+
if (data?.channelName !== channelName) return;
|
|
2269
|
+
const chunk = data.type === CHUNK ? data.chunk : data.type === "request" ? data.params : void 0;
|
|
2270
|
+
if (!chunk) return;
|
|
2271
|
+
if (!watchPeerEnd()) {
|
|
2272
|
+
port.removeEventListener("message", watcher);
|
|
2273
|
+
return;
|
|
2274
|
+
}
|
|
2275
|
+
if (chunk.done && !chunk.error) lastChunkArrived = true;
|
|
2276
|
+
};
|
|
2277
|
+
port.addEventListener("message", watcher);
|
|
2278
|
+
watchPeerEnd();
|
|
2279
|
+
let held = 0;
|
|
2280
|
+
const off = listenPort(port, async (params) => {
|
|
2281
|
+
const { done, error } = params;
|
|
2282
|
+
const value = adoptValue(params);
|
|
2076
2283
|
touch();
|
|
2077
2284
|
if (poison) throw poison;
|
|
2078
|
-
if (
|
|
2079
|
-
|
|
2080
|
-
controller.abort(poison);
|
|
2081
|
-
setTimeout(() => {
|
|
2082
|
-
try {
|
|
2083
|
-
port.close?.();
|
|
2084
|
-
} catch {}
|
|
2085
|
-
}, 0);
|
|
2086
|
-
throw poison;
|
|
2087
|
-
}
|
|
2285
|
+
if (held > 0) throw violate(MIXED_MESSAGE);
|
|
2286
|
+
if (outstanding) throw violate("webrun-rpc: peer sent a second unconfirmed chunk; the stream port is closed");
|
|
2088
2287
|
outstanding = true;
|
|
2089
2288
|
try {
|
|
2090
2289
|
await ready();
|
|
@@ -2094,15 +2293,60 @@ function receiveChunks(port, channelName, controller, touch) {
|
|
|
2094
2293
|
value,
|
|
2095
2294
|
error: error ? deserializeError(error) : void 0
|
|
2096
2295
|
});
|
|
2296
|
+
if (done && !error) resolveOver();
|
|
2097
2297
|
} finally {
|
|
2098
2298
|
outstanding = false;
|
|
2099
2299
|
}
|
|
2100
2300
|
if (done) resolveEnded();
|
|
2101
2301
|
}, { channelName });
|
|
2302
|
+
const onChunk = (event) => {
|
|
2303
|
+
const data = event.data;
|
|
2304
|
+
if (data?.type !== CHUNK || data.channelName !== channelName || !data.chunk) return;
|
|
2305
|
+
if (poison) return;
|
|
2306
|
+
const agreement = portAgreement(port);
|
|
2307
|
+
if (!agreement?.caps.has("pipeline")) {
|
|
2308
|
+
violate("webrun-rpc: peer sent a pipelined chunk where pipelining was not agreed");
|
|
2309
|
+
return;
|
|
2310
|
+
}
|
|
2311
|
+
if (outstanding) {
|
|
2312
|
+
violate(MIXED_MESSAGE);
|
|
2313
|
+
return;
|
|
2314
|
+
}
|
|
2315
|
+
if (data.chunk.value !== void 0 && !ArrayBuffer.isView(data.chunk.value)) {
|
|
2316
|
+
violate("webrun-rpc: peer sent a pipelined chunk whose value is not bytes");
|
|
2317
|
+
return;
|
|
2318
|
+
}
|
|
2319
|
+
if (held > agreement.window) {
|
|
2320
|
+
violate(`webrun-rpc: peer sent past the window (${agreement.window} bytes) without waiting for credit; the stream port is closed`);
|
|
2321
|
+
return;
|
|
2322
|
+
}
|
|
2323
|
+
touch();
|
|
2324
|
+
const { done, error } = data.chunk;
|
|
2325
|
+
const value = data.chunk.value && ownBytes(data.chunk.value);
|
|
2326
|
+
const size = (value?.byteLength ?? 0) + CHUNK_OVERHEAD;
|
|
2327
|
+
held += size;
|
|
2328
|
+
return (async () => {
|
|
2329
|
+
try {
|
|
2330
|
+
await ready();
|
|
2331
|
+
if (finished) return;
|
|
2332
|
+
await deliver?.({
|
|
2333
|
+
done,
|
|
2334
|
+
value,
|
|
2335
|
+
error: error ? deserializeError(error) : void 0
|
|
2336
|
+
});
|
|
2337
|
+
if (done && !error) resolveOver();
|
|
2338
|
+
if (done) resolveEnded();
|
|
2339
|
+
} finally {
|
|
2340
|
+
held -= size;
|
|
2341
|
+
}
|
|
2342
|
+
})();
|
|
2343
|
+
};
|
|
2344
|
+
port.addEventListener("message", onChunk);
|
|
2102
2345
|
const detach = () => {
|
|
2103
2346
|
finished = true;
|
|
2104
2347
|
wake();
|
|
2105
2348
|
off();
|
|
2349
|
+
port.removeEventListener("message", onChunk);
|
|
2106
2350
|
controller.signal.removeEventListener("abort", onAbort);
|
|
2107
2351
|
resolveEnded();
|
|
2108
2352
|
};
|
|
@@ -2132,21 +2376,35 @@ function receiveChunks(port, channelName, controller, touch) {
|
|
|
2132
2376
|
return detach;
|
|
2133
2377
|
}),
|
|
2134
2378
|
stop: detach,
|
|
2135
|
-
ended
|
|
2379
|
+
ended,
|
|
2380
|
+
over
|
|
2136
2381
|
};
|
|
2137
2382
|
}
|
|
2138
2383
|
/**
|
|
2139
2384
|
* The sending half of one direction: one `callPort` per chunk, one call
|
|
2140
|
-
* outstanding at a time (spec D11/D12), with no per-chunk deadline (spec D8)
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2385
|
+
* outstanding at a time (spec D11/D12), with no per-chunk deadline (spec D8) —
|
|
2386
|
+
* or, once pipelining is agreed, plain chunk messages held back by the window.
|
|
2387
|
+
* `onSent` runs once the end of the output has left without an error: posted
|
|
2388
|
+
* when pipelined, confirmed when not.
|
|
2389
|
+
*/
|
|
2390
|
+
async function sendChunks(port, channelName, output, { maxMessageSize, log }, signal, touch, onSent) {
|
|
2391
|
+
const framed = maxMessageSize ? toChunks(maxMessageSize)(output) : output;
|
|
2392
|
+
const stream = throughAbort(framed, signal);
|
|
2393
|
+
const producer = output;
|
|
2394
|
+
const returnProducer = () => void Promise.resolve(producer.return?.()).catch(() => {});
|
|
2395
|
+
if (framed !== output) signal.addEventListener("abort", returnProducer, { once: true });
|
|
2396
|
+
let onAborted = () => {};
|
|
2397
|
+
const aborted = new Promise((resolve) => {
|
|
2398
|
+
onAborted = () => resolve();
|
|
2399
|
+
});
|
|
2400
|
+
if (signal.aborted) onAborted();
|
|
2401
|
+
else signal.addEventListener("abort", onAborted, { once: true });
|
|
2144
2402
|
try {
|
|
2145
2403
|
await sendIterator(async ({ done, value, error }) => {
|
|
2146
2404
|
if (signal.aborted) return;
|
|
2147
2405
|
const chunk = {
|
|
2148
2406
|
done,
|
|
2149
|
-
value,
|
|
2407
|
+
value: done ? value : chunkBytes(value),
|
|
2150
2408
|
error: error === void 0 ? void 0 : serializeError(error)
|
|
2151
2409
|
};
|
|
2152
2410
|
log?.("[duplexOverPort] send", {
|
|
@@ -2154,46 +2412,34 @@ async function sendChunks(port, channelName, output, { maxMessageSize, log }, si
|
|
|
2154
2412
|
done,
|
|
2155
2413
|
size: value?.byteLength
|
|
2156
2414
|
});
|
|
2415
|
+
if (mayPipeline(port)) {
|
|
2416
|
+
port.postMessage({
|
|
2417
|
+
type: CHUNK,
|
|
2418
|
+
channelName,
|
|
2419
|
+
chunk
|
|
2420
|
+
});
|
|
2421
|
+
touch();
|
|
2422
|
+
if (done && !error) onSent();
|
|
2423
|
+
if (portBufferedAmount(port) > 0) await Promise.race([whenPortReady(port), aborted]);
|
|
2424
|
+
return;
|
|
2425
|
+
}
|
|
2157
2426
|
await callPort(port, chunk, {
|
|
2158
2427
|
channelName,
|
|
2159
2428
|
timeout: NO_TIMEOUT,
|
|
2160
2429
|
signal
|
|
2161
2430
|
});
|
|
2162
2431
|
touch();
|
|
2432
|
+
if (done && !error) onSent();
|
|
2163
2433
|
}, stream);
|
|
2164
2434
|
} catch (err) {
|
|
2165
2435
|
if (signal.aborted) return;
|
|
2166
2436
|
throw err;
|
|
2437
|
+
} finally {
|
|
2438
|
+
signal.removeEventListener("abort", returnProducer);
|
|
2439
|
+
signal.removeEventListener("abort", onAborted);
|
|
2167
2440
|
}
|
|
2168
2441
|
}
|
|
2169
2442
|
/**
|
|
2170
|
-
* Runs `fn` on a later task, never a microtask: a producer looping on
|
|
2171
|
-
* `whenPortReady` must let the transport's own events run. `setImmediate`
|
|
2172
|
-
* where it exists; elsewhere a `MessageChannel`, because nested `setTimeout`s
|
|
2173
|
-
* are clamped to 4 ms in browsers.
|
|
2174
|
-
*/
|
|
2175
|
-
const setImmediateFn = globalThis.setImmediate;
|
|
2176
|
-
/** @internal */
|
|
2177
|
-
const nextTask = setImmediateFn ? (fn) => {
|
|
2178
|
-
setImmediateFn(fn);
|
|
2179
|
-
} : (() => {
|
|
2180
|
-
let channel;
|
|
2181
|
-
const queue = [];
|
|
2182
|
-
return (fn) => {
|
|
2183
|
-
if (!channel) {
|
|
2184
|
-
channel = new MessageChannel();
|
|
2185
|
-
channel.port1.onmessage = () => queue.shift()?.();
|
|
2186
|
-
}
|
|
2187
|
-
queue.push(fn);
|
|
2188
|
-
channel.port2.postMessage(0);
|
|
2189
|
-
};
|
|
2190
|
-
})();
|
|
2191
|
-
const states = /* @__PURE__ */ new WeakMap();
|
|
2192
|
-
/** @internal Registered by the multiplexer for each port with a window. */
|
|
2193
|
-
function setPortSendState(port, state) {
|
|
2194
|
-
states.set(port, state);
|
|
2195
|
-
}
|
|
2196
|
-
/**
|
|
2197
2443
|
* One virtual port.
|
|
2198
2444
|
*
|
|
2199
2445
|
* `deliver` and `markClosed` are deliberately not on `port`: the consumer holds
|
|
@@ -2271,6 +2517,21 @@ function newVirtualPort(send, requestClose) {
|
|
|
2271
2517
|
const KEEPALIVE_ID = Number.MAX_SAFE_INTEGER;
|
|
2272
2518
|
/** The longest delay a timer holds; past it `setInterval` fires at once. */
|
|
2273
2519
|
const MAX_TIMER_MS = 2 ** 31 - 1;
|
|
2520
|
+
/**
|
|
2521
|
+
* What a windowed port offers in its `open`: every capability this end
|
|
2522
|
+
* speaks. `end` only over a codec that reads it (`PortCodec.halfClose`).
|
|
2523
|
+
*/
|
|
2524
|
+
function ownCaps(codec) {
|
|
2525
|
+
return codec.halfClose ? ["pipeline", "end"] : ["pipeline"];
|
|
2526
|
+
}
|
|
2527
|
+
/**
|
|
2528
|
+
* The capabilities in a peer's `caps` that this end speaks too, or
|
|
2529
|
+
* `undefined` when the peer listed none (it is then treated as an older peer).
|
|
2530
|
+
*/
|
|
2531
|
+
function sharedCaps(own, listed) {
|
|
2532
|
+
if (!Array.isArray(listed)) return void 0;
|
|
2533
|
+
return new Set(own.filter((cap) => listed.includes(cap)));
|
|
2534
|
+
}
|
|
2274
2535
|
/** Ceiling on concurrently open virtual ports. Bounds the id table only. */
|
|
2275
2536
|
const DEFAULT_MAX_PORTS = 1024;
|
|
2276
2537
|
/**
|
|
@@ -2302,10 +2563,18 @@ function estimateMessageSize(payload) {
|
|
|
2302
2563
|
*/
|
|
2303
2564
|
function multiplexPort(port, options) {
|
|
2304
2565
|
const { codec, onPort, side = "initiator", maxPorts = DEFAULT_MAX_PORTS, maxMessageSize, signal, window, frameSize, keepalive, onLeak } = options;
|
|
2566
|
+
const maxReassembledBytes = options.maxReassembledBytes ?? 67108864;
|
|
2305
2567
|
const misses = keepalive?.misses ?? 3;
|
|
2306
2568
|
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`);
|
|
2307
2569
|
if (frameSize != null && !(Number.isSafeInteger(frameSize) && frameSize > 0)) throw new RangeError(`webrun-rpc: frameSize must be a positive integer, got ${frameSize}`);
|
|
2570
|
+
if (!(Number.isSafeInteger(maxReassembledBytes) && maxReassembledBytes > 0)) throw new RangeError(`webrun-rpc: maxReassembledBytes must be a positive integer, got ${maxReassembledBytes}`);
|
|
2308
2571
|
const open = /* @__PURE__ */ new Map();
|
|
2572
|
+
let reassembling = 0;
|
|
2573
|
+
const freePartial = (entry) => {
|
|
2574
|
+
if (!entry?.partial) return;
|
|
2575
|
+
reassembling -= entry.partial.charged;
|
|
2576
|
+
entry.partial = void 0;
|
|
2577
|
+
};
|
|
2309
2578
|
const leaks = onLeak ? new FinalizationRegistry(({ id, record, release }) => {
|
|
2310
2579
|
const entry = open.get(id);
|
|
2311
2580
|
if (entry?.handle !== record) return;
|
|
@@ -2326,6 +2595,8 @@ function multiplexPort(port, options) {
|
|
|
2326
2595
|
let muxClosed = false;
|
|
2327
2596
|
/** This end counts encoded bytes and reassembles frames. */
|
|
2328
2597
|
const exactCodec = codec.encode != null && codec.decode != null;
|
|
2598
|
+
/** What this end offers on a windowed port. */
|
|
2599
|
+
const caps = ownCaps(codec);
|
|
2329
2600
|
const post = (envelope, transfer) => {
|
|
2330
2601
|
if (muxClosed) return;
|
|
2331
2602
|
codecPost(envelope, transfer);
|
|
@@ -2383,13 +2654,39 @@ function multiplexPort(port, options) {
|
|
|
2383
2654
|
port.postMessage(bytes);
|
|
2384
2655
|
} catch {}
|
|
2385
2656
|
};
|
|
2657
|
+
/**
|
|
2658
|
+
* Both directions have ended: free the id as a close would, with no `close`
|
|
2659
|
+
* on the wire (each end frees it on its own once it has seen both ends).
|
|
2660
|
+
* Aborting the close signal is how the port's consumer learns it is gone.
|
|
2661
|
+
*/
|
|
2662
|
+
const freeIfEnded = (id, c) => {
|
|
2663
|
+
const entry = open.get(id);
|
|
2664
|
+
if (!c.endSent || !c.peerEnded || entry?.credit !== c) return;
|
|
2665
|
+
freePartial(entry);
|
|
2666
|
+
open.delete(id);
|
|
2667
|
+
leaks?.unregister(entry.handle);
|
|
2668
|
+
entry.handle.markClosed(/* @__PURE__ */ new Error("webrun-rpc: the port ended in both directions"));
|
|
2669
|
+
};
|
|
2670
|
+
/** A peer that broke the port's protocol: hold none of its traffic, and close. */
|
|
2671
|
+
const failPort = (id, entry, error, reason) => {
|
|
2672
|
+
freePartial(entry);
|
|
2673
|
+
dropOutgoing(id);
|
|
2674
|
+
open.delete(id);
|
|
2675
|
+
releaseWaiters(entry.credit);
|
|
2676
|
+
entry.handle.markClosed(error);
|
|
2677
|
+
post({
|
|
2678
|
+
type: "close",
|
|
2679
|
+
id,
|
|
2680
|
+
reason
|
|
2681
|
+
});
|
|
2682
|
+
};
|
|
2386
2683
|
const releaseWaiters = (c) => {
|
|
2387
2684
|
for (const w of c?.waiters.splice(0) ?? []) w.resolve();
|
|
2388
2685
|
};
|
|
2389
2686
|
const flush = (c) => {
|
|
2390
2687
|
while (c.queue.length) {
|
|
2391
2688
|
const next = c.queue[0];
|
|
2392
|
-
if (c.enforced && c.outstanding > 0 && c.outstanding + next.size > c.window) break;
|
|
2689
|
+
if (next.size > 0 && c.enforced && c.outstanding > 0 && c.outstanding + next.size > c.window) break;
|
|
2393
2690
|
c.queue.shift();
|
|
2394
2691
|
c.queued -= next.size;
|
|
2395
2692
|
c.outstanding += next.size;
|
|
@@ -2427,15 +2724,20 @@ function multiplexPort(port, options) {
|
|
|
2427
2724
|
enforced: false,
|
|
2428
2725
|
opener,
|
|
2429
2726
|
exact,
|
|
2727
|
+
caps: /* @__PURE__ */ new Set(),
|
|
2430
2728
|
outstanding: 0,
|
|
2431
2729
|
queue: [],
|
|
2432
2730
|
queued: 0,
|
|
2433
2731
|
unconsumed: 0,
|
|
2434
2732
|
waiters: [],
|
|
2435
2733
|
toGrant: 0,
|
|
2436
|
-
grantScheduled: false
|
|
2734
|
+
grantScheduled: false,
|
|
2735
|
+
ending: false,
|
|
2736
|
+
endSent: false,
|
|
2737
|
+
peerEnded: false
|
|
2437
2738
|
} : void 0;
|
|
2438
2739
|
const self = newVirtualPort((payload, transfer) => {
|
|
2740
|
+
if (c?.ending) return;
|
|
2439
2741
|
touch(id);
|
|
2440
2742
|
const envelope = {
|
|
2441
2743
|
type: "message",
|
|
@@ -2453,12 +2755,12 @@ function multiplexPort(port, options) {
|
|
|
2453
2755
|
...envelope,
|
|
2454
2756
|
payload: structuredClone(payload, { transfer })
|
|
2455
2757
|
} : void 0;
|
|
2456
|
-
const send = () => {
|
|
2758
|
+
const send = (whole = false) => {
|
|
2457
2759
|
if (!bytes) {
|
|
2458
2760
|
sendOnPort(id, held ? () => post(held) : () => post(envelope, transfer));
|
|
2459
2761
|
return;
|
|
2460
2762
|
}
|
|
2461
|
-
if (frameSize != null && c.enforced && c.exact && bytes.byteLength > frameSize) {
|
|
2763
|
+
if (!whole && frameSize != null && c.enforced && c.exact && bytes.byteLength > frameSize) {
|
|
2462
2764
|
for (let at = 0; at < bytes.byteLength; at += frameSize) {
|
|
2463
2765
|
const data = bytes.subarray(at, at + frameSize);
|
|
2464
2766
|
const frame = at + frameSize >= bytes.byteLength ? {
|
|
@@ -2488,10 +2790,11 @@ function multiplexPort(port, options) {
|
|
|
2488
2790
|
record.markClosed(reason);
|
|
2489
2791
|
});
|
|
2490
2792
|
function release(reason) {
|
|
2793
|
+
freePartial(open.get(id));
|
|
2491
2794
|
open.delete(id);
|
|
2492
2795
|
leaks?.unregister(record);
|
|
2493
2796
|
if (c?.queue.length) {
|
|
2494
|
-
for (const item of c.queue.splice(0)) item.send();
|
|
2797
|
+
for (const item of c.queue.splice(0)) item.send(true);
|
|
2495
2798
|
c.queued = 0;
|
|
2496
2799
|
}
|
|
2497
2800
|
sendOnPort(id, () => post({
|
|
@@ -2521,6 +2824,38 @@ function multiplexPort(port, options) {
|
|
|
2521
2824
|
});
|
|
2522
2825
|
})
|
|
2523
2826
|
});
|
|
2827
|
+
setPortEndState(self.port, {
|
|
2828
|
+
end: () => {
|
|
2829
|
+
if (isClosed() || !c.caps.has("end")) return false;
|
|
2830
|
+
if (c.ending) return true;
|
|
2831
|
+
c.ending = true;
|
|
2832
|
+
c.queue.push({
|
|
2833
|
+
size: 0,
|
|
2834
|
+
send: () => sendOnPort(id, () => {
|
|
2835
|
+
if (open.get(id)?.credit !== c) return;
|
|
2836
|
+
post({
|
|
2837
|
+
type: "end",
|
|
2838
|
+
id
|
|
2839
|
+
});
|
|
2840
|
+
c.endSent = true;
|
|
2841
|
+
freeIfEnded(id, c);
|
|
2842
|
+
})
|
|
2843
|
+
});
|
|
2844
|
+
flush(c);
|
|
2845
|
+
return true;
|
|
2846
|
+
},
|
|
2847
|
+
watchPeerEnd: (resolve) => {
|
|
2848
|
+
if (!c.caps.has("end")) return false;
|
|
2849
|
+
if (c.peerEnded) resolve();
|
|
2850
|
+
else c.onPeerEnd = new WeakRef(resolve);
|
|
2851
|
+
return true;
|
|
2852
|
+
}
|
|
2853
|
+
});
|
|
2854
|
+
setPortAgreement(self.port, () => ({
|
|
2855
|
+
window: c.window,
|
|
2856
|
+
caps: c.caps,
|
|
2857
|
+
enforced: c.enforced
|
|
2858
|
+
}));
|
|
2524
2859
|
}
|
|
2525
2860
|
const record = {
|
|
2526
2861
|
deliver: self.deliver,
|
|
@@ -2572,16 +2907,24 @@ function multiplexPort(port, options) {
|
|
|
2572
2907
|
}
|
|
2573
2908
|
const handle = attach(envelope.id, envelope.meta, envelope.window, false, envelope.exact === true && exactCodec);
|
|
2574
2909
|
const credit = open.get(envelope.id)?.credit;
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2910
|
+
const listed = sharedCaps(caps, envelope.caps);
|
|
2911
|
+
if (credit) {
|
|
2912
|
+
const ack = credit.exact ? {
|
|
2913
|
+
type: "credit",
|
|
2914
|
+
id: envelope.id,
|
|
2915
|
+
bytes: 0,
|
|
2916
|
+
exact: true
|
|
2917
|
+
} : {
|
|
2918
|
+
type: "credit",
|
|
2919
|
+
id: envelope.id,
|
|
2920
|
+
bytes: 0
|
|
2921
|
+
};
|
|
2922
|
+
if (listed) credit.caps = listed;
|
|
2923
|
+
post(listed ? {
|
|
2924
|
+
...ack,
|
|
2925
|
+
caps: [...listed]
|
|
2926
|
+
} : ack);
|
|
2927
|
+
}
|
|
2585
2928
|
let accepted = false;
|
|
2586
2929
|
if (onPort) try {
|
|
2587
2930
|
accepted = onPort(handle.port, envelope.meta) !== false;
|
|
@@ -2609,6 +2952,7 @@ function multiplexPort(port, options) {
|
|
|
2609
2952
|
c.exact = false;
|
|
2610
2953
|
c.outstanding = 0;
|
|
2611
2954
|
}
|
|
2955
|
+
c.caps = sharedCaps(caps, envelope.caps) ?? /* @__PURE__ */ new Set();
|
|
2612
2956
|
post({
|
|
2613
2957
|
type: "credit",
|
|
2614
2958
|
id: envelope.id,
|
|
@@ -2620,6 +2964,10 @@ function multiplexPort(port, options) {
|
|
|
2620
2964
|
flush(c);
|
|
2621
2965
|
return;
|
|
2622
2966
|
}
|
|
2967
|
+
if ((envelope.type === "message" || envelope.type === "frame") && entry?.credit?.peerEnded) {
|
|
2968
|
+
failPort(envelope.id, entry, /* @__PURE__ */ new Error("webrun-rpc: the peer sent on a port after its end"), "protocol");
|
|
2969
|
+
return;
|
|
2970
|
+
}
|
|
2623
2971
|
if (envelope.type === "message") {
|
|
2624
2972
|
const sizeOf = (c) => (c.exact ? byteLengthOf(event.data) : void 0) ?? estimateMessageSize(envelope.payload);
|
|
2625
2973
|
if (!entry) return;
|
|
@@ -2630,15 +2978,29 @@ function multiplexPort(port, options) {
|
|
|
2630
2978
|
if (envelope.type === "frame") {
|
|
2631
2979
|
if (!entry?.credit?.exact || !codec.decode || !(envelope.data instanceof Uint8Array)) return;
|
|
2632
2980
|
touch(envelope.id);
|
|
2981
|
+
const c = entry.credit;
|
|
2982
|
+
const owed = c.unconsumed + c.toGrant;
|
|
2633
2983
|
const partial = entry.partial ?? {
|
|
2634
2984
|
chunks: [],
|
|
2635
|
-
bytes: 0
|
|
2985
|
+
bytes: 0,
|
|
2986
|
+
owed,
|
|
2987
|
+
charged: 0,
|
|
2988
|
+
...owed > 0 ? { room: c.window - owed } : {}
|
|
2636
2989
|
};
|
|
2637
2990
|
entry.partial = partial;
|
|
2991
|
+
const size = envelope.data.byteLength;
|
|
2992
|
+
const charged = partial.room == null ? Math.max(0, partial.bytes + size - c.window) : 0;
|
|
2993
|
+
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;
|
|
2994
|
+
if (overrun) {
|
|
2995
|
+
failPort(envelope.id, entry, /* @__PURE__ */ new RangeError(`webrun-rpc: a framed message passed ${overrun}`), "max-reassembled-bytes");
|
|
2996
|
+
return;
|
|
2997
|
+
}
|
|
2998
|
+
partial.bytes += size;
|
|
2999
|
+
reassembling += charged - partial.charged;
|
|
3000
|
+
partial.charged = charged;
|
|
2638
3001
|
partial.chunks.push(envelope.data);
|
|
2639
|
-
partial.bytes += envelope.data.byteLength;
|
|
2640
3002
|
if (!envelope.last) return;
|
|
2641
|
-
entry
|
|
3003
|
+
freePartial(entry);
|
|
2642
3004
|
const whole = new Uint8Array(partial.bytes);
|
|
2643
3005
|
let at = 0;
|
|
2644
3006
|
for (const chunk of partial.chunks) {
|
|
@@ -2650,8 +3012,22 @@ function multiplexPort(port, options) {
|
|
|
2650
3012
|
entry.handle.deliver(inner.payload, whole.byteLength);
|
|
2651
3013
|
return;
|
|
2652
3014
|
}
|
|
3015
|
+
if (envelope.type === "end") {
|
|
3016
|
+
if (!entry) return;
|
|
3017
|
+
const c = entry.credit;
|
|
3018
|
+
if (!c?.caps.has("end")) {
|
|
3019
|
+
failPort(envelope.id, entry, /* @__PURE__ */ new Error("webrun-rpc: the peer ended a port where end was not agreed"), "protocol");
|
|
3020
|
+
return;
|
|
3021
|
+
}
|
|
3022
|
+
touch(envelope.id);
|
|
3023
|
+
c.peerEnded = true;
|
|
3024
|
+
c.onPeerEnd?.deref()?.();
|
|
3025
|
+
freeIfEnded(envelope.id, c);
|
|
3026
|
+
return;
|
|
3027
|
+
}
|
|
2653
3028
|
dropOutgoing(envelope.id);
|
|
2654
3029
|
if (!entry) return;
|
|
3030
|
+
freePartial(entry);
|
|
2655
3031
|
open.delete(envelope.id);
|
|
2656
3032
|
releaseWaiters(entry.credit);
|
|
2657
3033
|
entry.handle.markClosed(/* @__PURE__ */ new Error("webrun-rpc: the peer closed the port"));
|
|
@@ -2730,12 +3106,14 @@ function multiplexPort(port, options) {
|
|
|
2730
3106
|
id,
|
|
2731
3107
|
meta,
|
|
2732
3108
|
window,
|
|
2733
|
-
exact: true
|
|
3109
|
+
exact: true,
|
|
3110
|
+
caps: [...caps]
|
|
2734
3111
|
} : {
|
|
2735
3112
|
type: "open",
|
|
2736
3113
|
id,
|
|
2737
3114
|
meta,
|
|
2738
|
-
window
|
|
3115
|
+
window,
|
|
3116
|
+
caps: [...caps]
|
|
2739
3117
|
} : {
|
|
2740
3118
|
type: "open",
|
|
2741
3119
|
id,
|
|
@@ -2842,7 +3220,7 @@ function isEnvelope(value) {
|
|
|
2842
3220
|
const candidate = value;
|
|
2843
3221
|
if (typeof candidate.id !== "number") return false;
|
|
2844
3222
|
if (!Number.isInteger(candidate.id) || candidate.id < 0) return false;
|
|
2845
|
-
return candidate.type === "open" || candidate.type === "message" || candidate.type === "close" || candidate.type === "credit" || candidate.type === "frame" || candidate.type === "ping" || candidate.type === "pong";
|
|
3223
|
+
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";
|
|
2846
3224
|
}
|
|
2847
3225
|
/**
|
|
2848
3226
|
* For ports whose messages are structured values — a real `MessagePort`, a
|
|
@@ -2854,6 +3232,7 @@ function isEnvelope(value) {
|
|
|
2854
3232
|
* not send anything a byte codec could not also carry.
|
|
2855
3233
|
*/
|
|
2856
3234
|
const structuredCodec = {
|
|
3235
|
+
halfClose: true,
|
|
2857
3236
|
post(port, envelope, transfer) {
|
|
2858
3237
|
if (transfer && transfer.length > 0) port.postMessage(envelope, transfer);
|
|
2859
3238
|
else port.postMessage(envelope);
|
|
@@ -2891,10 +3270,15 @@ function asyncDisposable(target, dispose) {
|
|
|
2891
3270
|
* own `handleStreams` / `sendStream` (no backpressure); version 2 is
|
|
2892
3271
|
* webrun-rpc's `connect` / `serve` over `overPipe` with webrun-http-streams'
|
|
2893
3272
|
* `fetchOverDuplex` / `serveFetchOverDuplex`: one port per link, calls
|
|
2894
|
-
* multiplexed on it, each call a stream with
|
|
2895
|
-
* directions, and cancellation that reaches the handler.
|
|
3273
|
+
* multiplexed on it, each call a stream with backpressure in both
|
|
3274
|
+
* directions, and cancellation that reaches the handler. A call's port with a
|
|
3275
|
+
* window (`HttpLinkOptions.window`) pipelines its chunks where both ends speak
|
|
3276
|
+
* it, and goes one confirmed chunk at a time otherwise, so the window needs
|
|
3277
|
+
* no new version.
|
|
2896
3278
|
*/
|
|
2897
3279
|
const HTTP_LINK_PROTOCOL = "webrun-http-link/2";
|
|
3280
|
+
/** The calling end's default `HttpLinkOptions.window`: 256 KiB. */
|
|
3281
|
+
const DEFAULT_HTTP_LINK_WINDOW = 262144;
|
|
2898
3282
|
/**
|
|
2899
3283
|
* Why a request failed when its link went away under it: the far side (the
|
|
2900
3284
|
* handler, or the ServiceWorker) was lost. The request may or may not have
|
|
@@ -2913,12 +3297,13 @@ var LinkLostError = class extends Error {
|
|
|
2913
3297
|
*/
|
|
2914
3298
|
const LINK_CLOSE = "webrun-http:link-close";
|
|
2915
3299
|
/** Opens the calling end of a link on `port` (one port per link). */
|
|
2916
|
-
async function connectHttpLink(port, { streamTimeout, maxPorts } = {}) {
|
|
3300
|
+
async function connectHttpLink(port, { streamTimeout, maxPorts, window = DEFAULT_HTTP_LINK_WINDOW } = {}) {
|
|
2917
3301
|
const { call, close } = await connect({
|
|
2918
3302
|
mux: overPipe(port, {
|
|
2919
3303
|
codec: structuredCodec,
|
|
2920
3304
|
side: "initiator",
|
|
2921
|
-
maxPorts
|
|
3305
|
+
maxPorts,
|
|
3306
|
+
window
|
|
2922
3307
|
}),
|
|
2923
3308
|
timeout: streamTimeout
|
|
2924
3309
|
});
|
|
@@ -3482,7 +3867,8 @@ function mayRegister(args) {
|
|
|
3482
3867
|
* the client that registered `key`, and exposes REGISTER/UNREGISTER/CONNECT
|
|
3483
3868
|
* channel calls used by the page-side relay client.
|
|
3484
3869
|
*/
|
|
3485
|
-
function startRelayServiceWorker(
|
|
3870
|
+
function startRelayServiceWorker(scope, options = {}) {
|
|
3871
|
+
const self = scope;
|
|
3486
3872
|
const [register, clear] = newRegistry();
|
|
3487
3873
|
const mounts = newMountTable({ exclude: options.exclude });
|
|
3488
3874
|
const hostKeys = /* @__PURE__ */ new Set();
|
|
@@ -3581,7 +3967,10 @@ function startRelayServiceWorker(self, options = {}) {
|
|
|
3581
3967
|
wire: HTTP_LINK_PROTOCOL
|
|
3582
3968
|
}, channel.port2);
|
|
3583
3969
|
if (!reply) throw HttpError.errorForbidden(params);
|
|
3584
|
-
const response = await sendOverConnection(channel.port1, reply, request, {
|
|
3970
|
+
const response = await sendOverConnection(channel.port1, reply, request, {
|
|
3971
|
+
streamTimeout: options.streamTimeout,
|
|
3972
|
+
window: options.window
|
|
3973
|
+
});
|
|
3585
3974
|
return options.decorateResponse?.(response, request) ?? response;
|
|
3586
3975
|
} catch (error) {
|
|
3587
3976
|
const httpError = HttpError.fromError(error);
|