@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/worker.js
CHANGED
|
@@ -1207,7 +1207,11 @@ async function drain(output) {
|
|
|
1207
1207
|
*/
|
|
1208
1208
|
function httpServe(handler, options = {}) {
|
|
1209
1209
|
const codec = options.codec ?? defaultCodec;
|
|
1210
|
-
return
|
|
1210
|
+
return (input) => {
|
|
1211
|
+
const abandoned = new AbortController();
|
|
1212
|
+
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")));
|
|
1213
|
+
};
|
|
1214
|
+
async function* exchange(input, context) {
|
|
1211
1215
|
let decoded;
|
|
1212
1216
|
try {
|
|
1213
1217
|
decoded = await codec.decodeRequest(input);
|
|
@@ -1219,7 +1223,7 @@ function httpServe(handler, options = {}) {
|
|
|
1219
1223
|
const replyCodec = decoded.codec ?? codec;
|
|
1220
1224
|
let result;
|
|
1221
1225
|
try {
|
|
1222
|
-
result = await handler(decoded.envelope, decoded.body);
|
|
1226
|
+
result = await handler(decoded.envelope, decoded.body, context);
|
|
1223
1227
|
} catch (error) {
|
|
1224
1228
|
yield* encodeErrorResponse(replyCodec, error, decoded.envelope.method);
|
|
1225
1229
|
return;
|
|
@@ -1234,7 +1238,36 @@ function httpServe(handler, options = {}) {
|
|
|
1234
1238
|
await result.release?.();
|
|
1235
1239
|
} catch {}
|
|
1236
1240
|
}
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
/**
|
|
1244
|
+
* `gen`, except that `return()` and `throw()` from its consumer call `abandon`
|
|
1245
|
+
* at once — before they are queued behind a `next()` still parked inside the
|
|
1246
|
+
* generator (the handler's `await`) — unless the generator already finished.
|
|
1247
|
+
*/
|
|
1248
|
+
function abortingOnReturn(gen, abandon) {
|
|
1249
|
+
let finished = false;
|
|
1250
|
+
const settle = (r) => {
|
|
1251
|
+
if (r.done) finished = true;
|
|
1252
|
+
return r;
|
|
1253
|
+
};
|
|
1254
|
+
const wrapped = {
|
|
1255
|
+
next: (...args) => gen.next(...args).then(settle),
|
|
1256
|
+
return(value) {
|
|
1257
|
+
if (!finished) abandon();
|
|
1258
|
+
finished = true;
|
|
1259
|
+
return gen.return(value);
|
|
1260
|
+
},
|
|
1261
|
+
throw(error) {
|
|
1262
|
+
if (!finished) abandon(error);
|
|
1263
|
+
finished = true;
|
|
1264
|
+
return gen.throw(error);
|
|
1265
|
+
},
|
|
1266
|
+
[Symbol.asyncIterator]() {
|
|
1267
|
+
return wrapped;
|
|
1268
|
+
}
|
|
1237
1269
|
};
|
|
1270
|
+
return asyncDisposable$2(wrapped, () => wrapped.return(void 0));
|
|
1238
1271
|
}
|
|
1239
1272
|
/**
|
|
1240
1273
|
* The one place that knows a runtime may not implement request body streams,
|
|
@@ -1418,10 +1451,11 @@ async function fetchOverDuplex(call, request, options = {}) {
|
|
|
1418
1451
|
* registered with any `webrun-streams-*` adapter's `serve`.
|
|
1419
1452
|
*/
|
|
1420
1453
|
function serveFetchOverDuplex(handler, options = {}) {
|
|
1421
|
-
return httpServe(async (env, body) => {
|
|
1454
|
+
return httpServe(async (env, body, exchange) => {
|
|
1422
1455
|
const reqInit = {
|
|
1423
1456
|
method: env.method,
|
|
1424
|
-
headers: forwardableHeaders(env.headers)
|
|
1457
|
+
headers: forwardableHeaders(env.headers),
|
|
1458
|
+
signal: exchange?.signal
|
|
1425
1459
|
};
|
|
1426
1460
|
let upload;
|
|
1427
1461
|
if (env.method !== "GET" && env.method !== "HEAD") {
|
|
@@ -1772,6 +1806,99 @@ async function* throughAbort(input, signal) {
|
|
|
1772
1806
|
}
|
|
1773
1807
|
}
|
|
1774
1808
|
/**
|
|
1809
|
+
* Runs `fn` on a later task, never a microtask: a producer looping on
|
|
1810
|
+
* `whenPortReady` must let the transport's own events run. `setImmediate`
|
|
1811
|
+
* where it exists; elsewhere a `MessageChannel`, because nested `setTimeout`s
|
|
1812
|
+
* are clamped to 4 ms in browsers.
|
|
1813
|
+
*/
|
|
1814
|
+
const setImmediateFn = globalThis.setImmediate;
|
|
1815
|
+
/** @internal */
|
|
1816
|
+
const nextTask = setImmediateFn ? (fn) => {
|
|
1817
|
+
setImmediateFn(fn);
|
|
1818
|
+
} : (() => {
|
|
1819
|
+
let channel;
|
|
1820
|
+
const queue = [];
|
|
1821
|
+
return (fn) => {
|
|
1822
|
+
if (!channel) {
|
|
1823
|
+
channel = new MessageChannel();
|
|
1824
|
+
channel.port1.onmessage = () => queue.shift()?.();
|
|
1825
|
+
}
|
|
1826
|
+
queue.push(fn);
|
|
1827
|
+
channel.port2.postMessage(0);
|
|
1828
|
+
};
|
|
1829
|
+
})();
|
|
1830
|
+
const states = /* @__PURE__ */ new WeakMap();
|
|
1831
|
+
/** @internal Registered by the multiplexer for each port with a window. */
|
|
1832
|
+
function setPortSendState(port, state) {
|
|
1833
|
+
states.set(port, state);
|
|
1834
|
+
}
|
|
1835
|
+
/**
|
|
1836
|
+
* Bytes queued on `port` because its window is spent (0 for a port without
|
|
1837
|
+
* flow control). A sender that should not queue without bound waits for
|
|
1838
|
+
* `whenPortReady` before posting more.
|
|
1839
|
+
*/
|
|
1840
|
+
function portBufferedAmount(port) {
|
|
1841
|
+
return states.get(port)?.bufferedAmount() ?? 0;
|
|
1842
|
+
}
|
|
1843
|
+
/**
|
|
1844
|
+
* Resolves once at most `below` bytes are queued on `port`, always on a later
|
|
1845
|
+
* task (for a port without flow control, on the next one).
|
|
1846
|
+
*/
|
|
1847
|
+
function whenPortReady(port, below = 0) {
|
|
1848
|
+
return states.get(port)?.ready(below) ?? new Promise((resolve) => nextTask(resolve));
|
|
1849
|
+
}
|
|
1850
|
+
const agreements = /* @__PURE__ */ new WeakMap();
|
|
1851
|
+
/** @internal Registered by the multiplexer for each port with a window. */
|
|
1852
|
+
function setPortAgreement(port, agreement) {
|
|
1853
|
+
agreements.set(port, agreement);
|
|
1854
|
+
}
|
|
1855
|
+
/**
|
|
1856
|
+
* @internal What has been agreed with the peer on `port` so far; `undefined`
|
|
1857
|
+
* on a port without a window. Not exported from the package.
|
|
1858
|
+
*/
|
|
1859
|
+
function portAgreement(port) {
|
|
1860
|
+
return agreements.get(port)?.();
|
|
1861
|
+
}
|
|
1862
|
+
const ends = /* @__PURE__ */ new WeakMap();
|
|
1863
|
+
/** @internal Registered by the multiplexer for each port with a window. */
|
|
1864
|
+
function setPortEndState(port, state) {
|
|
1865
|
+
ends.set(port, state);
|
|
1866
|
+
}
|
|
1867
|
+
/**
|
|
1868
|
+
* @internal End this side's direction of `port`: an `end` is queued behind
|
|
1869
|
+
* everything already posted on it, and anything posted afterwards is dropped.
|
|
1870
|
+
* `false`, and nothing changes, when `end` was not agreed (no window, the
|
|
1871
|
+
* handshake not answered yet, or a peer that does not speak it): the caller
|
|
1872
|
+
* then closes the port. Never waits on the peer. Not exported from the package.
|
|
1873
|
+
*/
|
|
1874
|
+
function endPort(port) {
|
|
1875
|
+
return ends.get(port)?.end() ?? false;
|
|
1876
|
+
}
|
|
1877
|
+
/**
|
|
1878
|
+
* @internal Resolves once the peer has ended its direction of `port`. It never
|
|
1879
|
+
* settles otherwise: not on a port where `end` is not agreed when it is asked
|
|
1880
|
+
* (no window, the handshake not answered yet, a peer or codec without it), and
|
|
1881
|
+
* not when the port closes first, so a caller races it against the port's
|
|
1882
|
+
* close signal. Not exported from the package.
|
|
1883
|
+
*/
|
|
1884
|
+
function whenPortEnded(port) {
|
|
1885
|
+
let waiting = peerEnds.get(port);
|
|
1886
|
+
if (!waiting) {
|
|
1887
|
+
let resolve = () => {};
|
|
1888
|
+
const promise = new Promise((r) => {
|
|
1889
|
+
resolve = r;
|
|
1890
|
+
});
|
|
1891
|
+
if (!ends.get(port)?.watchPeerEnd(resolve)) return promise;
|
|
1892
|
+
waiting = {
|
|
1893
|
+
promise,
|
|
1894
|
+
resolve
|
|
1895
|
+
};
|
|
1896
|
+
peerEnds.set(port, waiting);
|
|
1897
|
+
}
|
|
1898
|
+
return waiting.promise;
|
|
1899
|
+
}
|
|
1900
|
+
const peerEnds = /* @__PURE__ */ new WeakMap();
|
|
1901
|
+
/**
|
|
1775
1902
|
* Makes `target` disposable with `await using`: installs `dispose` as its
|
|
1776
1903
|
* `[Symbol.asyncDispose]`, where the runtime defines the symbol. Hand-written
|
|
1777
1904
|
* iterators lose what a native generator gets from `AsyncIteratorPrototype`;
|
|
@@ -1843,6 +1970,61 @@ function returningAtOnce(gen, abandon) {
|
|
|
1843
1970
|
* tests and adapters assert on it.
|
|
1844
1971
|
*/
|
|
1845
1972
|
const STREAM_ABORT = "webrun-rpc:stream-abort";
|
|
1973
|
+
/**
|
|
1974
|
+
* The `type` of a pipelined chunk: a plain port message, not a `callPort`
|
|
1975
|
+
* request, sent only on a port where both ends agreed `pipeline`. The window's
|
|
1976
|
+
* credit is its confirmation.
|
|
1977
|
+
*/
|
|
1978
|
+
const CHUNK = "webrun-rpc:chunk";
|
|
1979
|
+
/**
|
|
1980
|
+
* What a pipelined chunk counts beyond its bytes, against the window, on the
|
|
1981
|
+
* receiving side. Both codecs' windows count more for the chunk's envelope
|
|
1982
|
+
* (the estimate at least 64, msgpack's keys alone more than this), so an honest
|
|
1983
|
+
* sender never makes the receiver count past the window; a sender of empty
|
|
1984
|
+
* chunks still cannot have it hold an unbounded number of them.
|
|
1985
|
+
*/
|
|
1986
|
+
const CHUNK_OVERHEAD = 32;
|
|
1987
|
+
/**
|
|
1988
|
+
* The bytes of `view` as a plain `Uint8Array` that owns its whole buffer:
|
|
1989
|
+
* `view` itself when it already is one, else a copy. Structured clone carries
|
|
1990
|
+
* and keeps a view's whole `ArrayBuffer`, and a byte codec may decode into
|
|
1991
|
+
* views of the frame, while the window and the receiver's count see only the
|
|
1992
|
+
* view: a 4 KiB `subarray` of a 4 MiB buffer would cost 4 MiB per chunk.
|
|
1993
|
+
* Copied with the `Uint8Array` constructor, not `slice()`: a Node `Buffer`'s
|
|
1994
|
+
* `slice()` is another view on the same memory.
|
|
1995
|
+
*/
|
|
1996
|
+
function ownBytes(view) {
|
|
1997
|
+
if (view instanceof Uint8Array && view.constructor === Uint8Array && view.byteOffset === 0 && view.byteLength === view.buffer.byteLength) return view;
|
|
1998
|
+
return new Uint8Array(new Uint8Array(view.buffer, view.byteOffset, view.byteLength));
|
|
1999
|
+
}
|
|
2000
|
+
/**
|
|
2001
|
+
* A producer's chunk as bytes this end may send, in either mode: any view or
|
|
2002
|
+
* `ArrayBuffer`, normalised by `ownBytes`. Anything else fails the stream here,
|
|
2003
|
+
* as the producer's own error, rather than reaching a peer that would refuse it.
|
|
2004
|
+
*/
|
|
2005
|
+
/**
|
|
2006
|
+
* A received confirmed chunk's value, as its own bytes, put back in place of
|
|
2007
|
+
* the original in `chunk`: `listenPort` keeps the request until the handler
|
|
2008
|
+
* returns, and the original would keep its whole buffer that long. A function
|
|
2009
|
+
* of its own, so that no temporary of the suspended handler holds the
|
|
2010
|
+
* original either.
|
|
2011
|
+
*/
|
|
2012
|
+
function adoptValue(chunk) {
|
|
2013
|
+
const original = chunk.value;
|
|
2014
|
+
if (ArrayBuffer.isView(original)) chunk.value = ownBytes(original);
|
|
2015
|
+
return chunk.value;
|
|
2016
|
+
}
|
|
2017
|
+
function chunkBytes(value) {
|
|
2018
|
+
if (ArrayBuffer.isView(value)) return ownBytes(value);
|
|
2019
|
+
if (value instanceof ArrayBuffer || Object.prototype.toString.call(value) === "[object ArrayBuffer]") return new Uint8Array(value);
|
|
2020
|
+
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}`);
|
|
2021
|
+
}
|
|
2022
|
+
const MIXED_MESSAGE = "webrun-rpc: peer mixed confirmed and pipelined chunks; the stream port is closed";
|
|
2023
|
+
/** This end may pipeline its own chunks: agreed, and the window holds them. */
|
|
2024
|
+
const mayPipeline = (port) => {
|
|
2025
|
+
const agreement = portAgreement(port);
|
|
2026
|
+
return !!agreement?.enforced && agreement.caps.has("pipeline");
|
|
2027
|
+
};
|
|
1846
2028
|
/** Caller's input travels on this channel; the handler listens on it. */
|
|
1847
2029
|
const CHANNEL_IN = "in";
|
|
1848
2030
|
/** The handler's output travels on this channel; the caller listens on it. */
|
|
@@ -1855,7 +2037,9 @@ const CHANNEL_OUT = "out";
|
|
|
1855
2037
|
* same way on the other channel. Within each direction the next chunk is never
|
|
1856
2038
|
* sent until the previous one has been delivered *and* pulled past by the
|
|
1857
2039
|
* consumer (spec D11) — the reply to a chunk call *is* the confirmation, and
|
|
1858
|
-
* `listenPort` withholds it until then.
|
|
2040
|
+
* `listenPort` withholds it until then. On a port whose window both ends
|
|
2041
|
+
* speak and that agreed `pipeline`, chunks go as plain messages instead, and
|
|
2042
|
+
* the window's credit is the confirmation: up to a window is in flight.
|
|
1859
2043
|
*
|
|
1860
2044
|
* A stream port carries exactly one invocation. To make several calls, open
|
|
1861
2045
|
* several ports: `mux.openPort({ kind: "stream" })` per call.
|
|
@@ -1872,22 +2056,49 @@ function serveDuplexOverPort(port, handler, options = {}) {
|
|
|
1872
2056
|
const notice = installAbortNotice(port, controller);
|
|
1873
2057
|
abortOnPortClose(port, controller);
|
|
1874
2058
|
const clock = installStreamTimeout(controller, options.timeout);
|
|
1875
|
-
const inbound = receiveChunks(port, CHANNEL_IN, controller, clock.touch);
|
|
2059
|
+
const inbound = receiveChunks(port, CHANNEL_IN, controller, clock.touch, notice);
|
|
1876
2060
|
let output;
|
|
1877
2061
|
try {
|
|
1878
2062
|
output = handler(inbound.stream);
|
|
1879
2063
|
} catch (err) {
|
|
1880
|
-
const pump = sendChunks(port, CHANNEL_OUT, failing(err), options, controller.signal, clock.touch);
|
|
2064
|
+
const pump = sendChunks(port, CHANNEL_OUT, failing(err), options, controller.signal, clock.touch, () => {});
|
|
1881
2065
|
pump.catch(() => {});
|
|
1882
2066
|
disarmClockWhenBothSidesSettle(clock, pump, inbound.ended);
|
|
1883
2067
|
return teardownOnce(controller, notice, clock, inbound, void 0);
|
|
1884
2068
|
}
|
|
1885
|
-
|
|
2069
|
+
let onSent = () => {};
|
|
2070
|
+
const sent = new Promise((resolve) => {
|
|
2071
|
+
onSent = resolve;
|
|
2072
|
+
});
|
|
2073
|
+
const pump = sendChunks(port, CHANNEL_OUT, output, options, controller.signal, clock.touch, onSent);
|
|
1886
2074
|
pump.catch(() => {});
|
|
1887
2075
|
disarmClockWhenBothSidesSettle(clock, pump, inbound.ended);
|
|
2076
|
+
endServedStream(port, sent, inbound.over);
|
|
1888
2077
|
return teardownOnce(controller, notice, clock, inbound, output);
|
|
1889
2078
|
}
|
|
1890
2079
|
/**
|
|
2080
|
+
* The serving side's normal finish. Once the handler's last chunk has gone
|
|
2081
|
+
* without an error, and no more input is coming (`over`: the caller's last
|
|
2082
|
+
* chunk arrived without an error and was read, or the caller ended its
|
|
2083
|
+
* direction behind it, for a handler that did not read all its input), this
|
|
2084
|
+
* side ends its direction too, where `end` was agreed. Without `end` nothing
|
|
2085
|
+
* happens here: the caller closes the port. A stream that never gets that far
|
|
2086
|
+
* was abandoned, and the caller's notice and `close` free the port; this then
|
|
2087
|
+
* never resumes. It needs no race against the port's close signal: nothing
|
|
2088
|
+
* awaits it, and what keeps it waiting is the input's watcher, a listener of
|
|
2089
|
+
* the port that a close drops, after which the wait is collected.
|
|
2090
|
+
*
|
|
2091
|
+
* Nothing this side still posts is lost behind its `end`. The caller ends only
|
|
2092
|
+
* once its own last chunk was confirmed, so no confirmation is owed to it
|
|
2093
|
+
* here, and what the freed port's close signal aborts has settled: the last
|
|
2094
|
+
* chunk has left, and input nobody reads is dropped, as on a close.
|
|
2095
|
+
*/
|
|
2096
|
+
async function endServedStream(port, sent, over) {
|
|
2097
|
+
await sent;
|
|
2098
|
+
await over;
|
|
2099
|
+
endPort(port);
|
|
2100
|
+
}
|
|
2101
|
+
/**
|
|
1891
2102
|
* A stream that completes normally has nobody left to call the returned
|
|
1892
2103
|
* teardown — the caller only knows the stream ended, not that it must also
|
|
1893
2104
|
* dispose the serve-side handle. Without this, the last `touch()`'s timer
|
|
@@ -1927,22 +2138,31 @@ function runCallerSide(port, input, options) {
|
|
|
1927
2138
|
const notice = installAbortNotice(port, controller);
|
|
1928
2139
|
abortOnPortClose(port, controller);
|
|
1929
2140
|
const clock = installStreamTimeout(controller, options.timeout);
|
|
1930
|
-
const inbound = receiveChunks(port, CHANNEL_OUT, controller, clock.touch);
|
|
1931
|
-
|
|
2141
|
+
const inbound = receiveChunks(port, CHANNEL_OUT, controller, clock.touch, notice);
|
|
2142
|
+
let sent = false;
|
|
2143
|
+
const pump = sendChunks(port, CHANNEL_IN, input, options, controller.signal, clock.touch, () => {
|
|
2144
|
+
sent = true;
|
|
2145
|
+
});
|
|
1932
2146
|
pump.catch(() => {});
|
|
1933
2147
|
return returningAtOnce((async function* () {
|
|
2148
|
+
let received = false;
|
|
1934
2149
|
try {
|
|
1935
2150
|
yield* inbound.stream;
|
|
2151
|
+
received = true;
|
|
1936
2152
|
} finally {
|
|
2153
|
+
const ended = received && sent;
|
|
1937
2154
|
if (!controller.signal.aborted) controller.abort(/* @__PURE__ */ new Error("webrun-rpc: the caller abandoned the stream"));
|
|
1938
|
-
notice.post();
|
|
2155
|
+
if (!ended) notice.post();
|
|
1939
2156
|
notice.stop();
|
|
1940
2157
|
clock.stop();
|
|
1941
2158
|
inbound.stop();
|
|
1942
2159
|
await pump.catch(() => {});
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
2160
|
+
if (!(ended && endPort(port))) {
|
|
2161
|
+
notice.post();
|
|
2162
|
+
try {
|
|
2163
|
+
await port.close?.();
|
|
2164
|
+
} catch {}
|
|
2165
|
+
}
|
|
1946
2166
|
}
|
|
1947
2167
|
})(), () => {
|
|
1948
2168
|
if (!controller.signal.aborted) controller.abort(/* @__PURE__ */ new Error("webrun-rpc: the caller abandoned the stream"));
|
|
@@ -2036,7 +2256,7 @@ function installStreamTimeout(controller, timeout) {
|
|
|
2036
2256
|
* the local consumer has not started iterating, an inbound chunk waits for it —
|
|
2037
2257
|
* which is the correct backpressure, and different from having no listener.
|
|
2038
2258
|
*/
|
|
2039
|
-
function receiveChunks(port, channelName, controller, touch) {
|
|
2259
|
+
function receiveChunks(port, channelName, controller, touch, notice) {
|
|
2040
2260
|
let deliver;
|
|
2041
2261
|
const waiting = [];
|
|
2042
2262
|
let finished = false;
|
|
@@ -2046,25 +2266,64 @@ function receiveChunks(port, channelName, controller, touch) {
|
|
|
2046
2266
|
const ended = new Promise((resolve) => {
|
|
2047
2267
|
resolveEnded = resolve;
|
|
2048
2268
|
});
|
|
2269
|
+
let resolveOver = () => {};
|
|
2270
|
+
const over = new Promise((resolve) => {
|
|
2271
|
+
resolveOver = resolve;
|
|
2272
|
+
});
|
|
2049
2273
|
let aborted = false;
|
|
2050
2274
|
let abortReason;
|
|
2051
2275
|
const ready = () => deliver || finished ? Promise.resolve() : new Promise((r) => waiting.push(r));
|
|
2052
2276
|
const wake = () => {
|
|
2053
2277
|
for (const r of waiting.splice(0)) r();
|
|
2054
2278
|
};
|
|
2055
|
-
const
|
|
2279
|
+
const violate = (message) => {
|
|
2280
|
+
poison = new Error(message);
|
|
2281
|
+
controller.abort(poison);
|
|
2282
|
+
setTimeout(() => {
|
|
2283
|
+
try {
|
|
2284
|
+
port.close?.();
|
|
2285
|
+
} catch {}
|
|
2286
|
+
}, 0);
|
|
2287
|
+
return poison;
|
|
2288
|
+
};
|
|
2289
|
+
let lastChunkArrived = false;
|
|
2290
|
+
let watching = false;
|
|
2291
|
+
const watchPeerEnd = () => {
|
|
2292
|
+
if (watching) return true;
|
|
2293
|
+
if (!portAgreement(port)?.caps.has("end")) return false;
|
|
2294
|
+
watching = true;
|
|
2295
|
+
whenPortEnded(port).then(() => {
|
|
2296
|
+
port.removeEventListener("message", watcher);
|
|
2297
|
+
if (lastChunkArrived) {
|
|
2298
|
+
resolveOver();
|
|
2299
|
+
return;
|
|
2300
|
+
}
|
|
2301
|
+
notice.post();
|
|
2302
|
+
violate("webrun-rpc: the peer ended its direction without finishing it; the stream port is closed");
|
|
2303
|
+
});
|
|
2304
|
+
return true;
|
|
2305
|
+
};
|
|
2306
|
+
const watcher = (event) => {
|
|
2307
|
+
const data = event.data;
|
|
2308
|
+
if (data?.channelName !== channelName) return;
|
|
2309
|
+
const chunk = data.type === CHUNK ? data.chunk : data.type === "request" ? data.params : void 0;
|
|
2310
|
+
if (!chunk) return;
|
|
2311
|
+
if (!watchPeerEnd()) {
|
|
2312
|
+
port.removeEventListener("message", watcher);
|
|
2313
|
+
return;
|
|
2314
|
+
}
|
|
2315
|
+
if (chunk.done && !chunk.error) lastChunkArrived = true;
|
|
2316
|
+
};
|
|
2317
|
+
port.addEventListener("message", watcher);
|
|
2318
|
+
watchPeerEnd();
|
|
2319
|
+
let held = 0;
|
|
2320
|
+
const off = listenPort(port, async (params) => {
|
|
2321
|
+
const { done, error } = params;
|
|
2322
|
+
const value = adoptValue(params);
|
|
2056
2323
|
touch();
|
|
2057
2324
|
if (poison) throw poison;
|
|
2058
|
-
if (
|
|
2059
|
-
|
|
2060
|
-
controller.abort(poison);
|
|
2061
|
-
setTimeout(() => {
|
|
2062
|
-
try {
|
|
2063
|
-
port.close?.();
|
|
2064
|
-
} catch {}
|
|
2065
|
-
}, 0);
|
|
2066
|
-
throw poison;
|
|
2067
|
-
}
|
|
2325
|
+
if (held > 0) throw violate(MIXED_MESSAGE);
|
|
2326
|
+
if (outstanding) throw violate("webrun-rpc: peer sent a second unconfirmed chunk; the stream port is closed");
|
|
2068
2327
|
outstanding = true;
|
|
2069
2328
|
try {
|
|
2070
2329
|
await ready();
|
|
@@ -2074,15 +2333,60 @@ function receiveChunks(port, channelName, controller, touch) {
|
|
|
2074
2333
|
value,
|
|
2075
2334
|
error: error ? deserializeError(error) : void 0
|
|
2076
2335
|
});
|
|
2336
|
+
if (done && !error) resolveOver();
|
|
2077
2337
|
} finally {
|
|
2078
2338
|
outstanding = false;
|
|
2079
2339
|
}
|
|
2080
2340
|
if (done) resolveEnded();
|
|
2081
2341
|
}, { channelName });
|
|
2342
|
+
const onChunk = (event) => {
|
|
2343
|
+
const data = event.data;
|
|
2344
|
+
if (data?.type !== CHUNK || data.channelName !== channelName || !data.chunk) return;
|
|
2345
|
+
if (poison) return;
|
|
2346
|
+
const agreement = portAgreement(port);
|
|
2347
|
+
if (!agreement?.caps.has("pipeline")) {
|
|
2348
|
+
violate("webrun-rpc: peer sent a pipelined chunk where pipelining was not agreed");
|
|
2349
|
+
return;
|
|
2350
|
+
}
|
|
2351
|
+
if (outstanding) {
|
|
2352
|
+
violate(MIXED_MESSAGE);
|
|
2353
|
+
return;
|
|
2354
|
+
}
|
|
2355
|
+
if (data.chunk.value !== void 0 && !ArrayBuffer.isView(data.chunk.value)) {
|
|
2356
|
+
violate("webrun-rpc: peer sent a pipelined chunk whose value is not bytes");
|
|
2357
|
+
return;
|
|
2358
|
+
}
|
|
2359
|
+
if (held > agreement.window) {
|
|
2360
|
+
violate(`webrun-rpc: peer sent past the window (${agreement.window} bytes) without waiting for credit; the stream port is closed`);
|
|
2361
|
+
return;
|
|
2362
|
+
}
|
|
2363
|
+
touch();
|
|
2364
|
+
const { done, error } = data.chunk;
|
|
2365
|
+
const value = data.chunk.value && ownBytes(data.chunk.value);
|
|
2366
|
+
const size = (value?.byteLength ?? 0) + CHUNK_OVERHEAD;
|
|
2367
|
+
held += size;
|
|
2368
|
+
return (async () => {
|
|
2369
|
+
try {
|
|
2370
|
+
await ready();
|
|
2371
|
+
if (finished) return;
|
|
2372
|
+
await deliver?.({
|
|
2373
|
+
done,
|
|
2374
|
+
value,
|
|
2375
|
+
error: error ? deserializeError(error) : void 0
|
|
2376
|
+
});
|
|
2377
|
+
if (done && !error) resolveOver();
|
|
2378
|
+
if (done) resolveEnded();
|
|
2379
|
+
} finally {
|
|
2380
|
+
held -= size;
|
|
2381
|
+
}
|
|
2382
|
+
})();
|
|
2383
|
+
};
|
|
2384
|
+
port.addEventListener("message", onChunk);
|
|
2082
2385
|
const detach = () => {
|
|
2083
2386
|
finished = true;
|
|
2084
2387
|
wake();
|
|
2085
2388
|
off();
|
|
2389
|
+
port.removeEventListener("message", onChunk);
|
|
2086
2390
|
controller.signal.removeEventListener("abort", onAbort);
|
|
2087
2391
|
resolveEnded();
|
|
2088
2392
|
};
|
|
@@ -2112,21 +2416,35 @@ function receiveChunks(port, channelName, controller, touch) {
|
|
|
2112
2416
|
return detach;
|
|
2113
2417
|
}),
|
|
2114
2418
|
stop: detach,
|
|
2115
|
-
ended
|
|
2419
|
+
ended,
|
|
2420
|
+
over
|
|
2116
2421
|
};
|
|
2117
2422
|
}
|
|
2118
2423
|
/**
|
|
2119
2424
|
* The sending half of one direction: one `callPort` per chunk, one call
|
|
2120
|
-
* outstanding at a time (spec D11/D12), with no per-chunk deadline (spec D8)
|
|
2425
|
+
* outstanding at a time (spec D11/D12), with no per-chunk deadline (spec D8) —
|
|
2426
|
+
* or, once pipelining is agreed, plain chunk messages held back by the window.
|
|
2427
|
+
* `onSent` runs once the end of the output has left without an error: posted
|
|
2428
|
+
* when pipelined, confirmed when not.
|
|
2121
2429
|
*/
|
|
2122
|
-
async function sendChunks(port, channelName, output, { maxMessageSize, log }, signal, touch) {
|
|
2123
|
-
const
|
|
2430
|
+
async function sendChunks(port, channelName, output, { maxMessageSize, log }, signal, touch, onSent) {
|
|
2431
|
+
const framed = maxMessageSize ? toChunks(maxMessageSize)(output) : output;
|
|
2432
|
+
const stream = throughAbort(framed, signal);
|
|
2433
|
+
const producer = output;
|
|
2434
|
+
const returnProducer = () => void Promise.resolve(producer.return?.()).catch(() => {});
|
|
2435
|
+
if (framed !== output) signal.addEventListener("abort", returnProducer, { once: true });
|
|
2436
|
+
let onAborted = () => {};
|
|
2437
|
+
const aborted = new Promise((resolve) => {
|
|
2438
|
+
onAborted = () => resolve();
|
|
2439
|
+
});
|
|
2440
|
+
if (signal.aborted) onAborted();
|
|
2441
|
+
else signal.addEventListener("abort", onAborted, { once: true });
|
|
2124
2442
|
try {
|
|
2125
2443
|
await sendIterator(async ({ done, value, error }) => {
|
|
2126
2444
|
if (signal.aborted) return;
|
|
2127
2445
|
const chunk = {
|
|
2128
2446
|
done,
|
|
2129
|
-
value,
|
|
2447
|
+
value: done ? value : chunkBytes(value),
|
|
2130
2448
|
error: error === void 0 ? void 0 : serializeError(error)
|
|
2131
2449
|
};
|
|
2132
2450
|
log?.("[duplexOverPort] send", {
|
|
@@ -2134,46 +2452,34 @@ async function sendChunks(port, channelName, output, { maxMessageSize, log }, si
|
|
|
2134
2452
|
done,
|
|
2135
2453
|
size: value?.byteLength
|
|
2136
2454
|
});
|
|
2455
|
+
if (mayPipeline(port)) {
|
|
2456
|
+
port.postMessage({
|
|
2457
|
+
type: CHUNK,
|
|
2458
|
+
channelName,
|
|
2459
|
+
chunk
|
|
2460
|
+
});
|
|
2461
|
+
touch();
|
|
2462
|
+
if (done && !error) onSent();
|
|
2463
|
+
if (portBufferedAmount(port) > 0) await Promise.race([whenPortReady(port), aborted]);
|
|
2464
|
+
return;
|
|
2465
|
+
}
|
|
2137
2466
|
await callPort(port, chunk, {
|
|
2138
2467
|
channelName,
|
|
2139
2468
|
timeout: NO_TIMEOUT,
|
|
2140
2469
|
signal
|
|
2141
2470
|
});
|
|
2142
2471
|
touch();
|
|
2472
|
+
if (done && !error) onSent();
|
|
2143
2473
|
}, stream);
|
|
2144
2474
|
} catch (err) {
|
|
2145
2475
|
if (signal.aborted) return;
|
|
2146
2476
|
throw err;
|
|
2477
|
+
} finally {
|
|
2478
|
+
signal.removeEventListener("abort", returnProducer);
|
|
2479
|
+
signal.removeEventListener("abort", onAborted);
|
|
2147
2480
|
}
|
|
2148
2481
|
}
|
|
2149
2482
|
/**
|
|
2150
|
-
* Runs `fn` on a later task, never a microtask: a producer looping on
|
|
2151
|
-
* `whenPortReady` must let the transport's own events run. `setImmediate`
|
|
2152
|
-
* where it exists; elsewhere a `MessageChannel`, because nested `setTimeout`s
|
|
2153
|
-
* are clamped to 4 ms in browsers.
|
|
2154
|
-
*/
|
|
2155
|
-
const setImmediateFn = globalThis.setImmediate;
|
|
2156
|
-
/** @internal */
|
|
2157
|
-
const nextTask = setImmediateFn ? (fn) => {
|
|
2158
|
-
setImmediateFn(fn);
|
|
2159
|
-
} : (() => {
|
|
2160
|
-
let channel;
|
|
2161
|
-
const queue = [];
|
|
2162
|
-
return (fn) => {
|
|
2163
|
-
if (!channel) {
|
|
2164
|
-
channel = new MessageChannel();
|
|
2165
|
-
channel.port1.onmessage = () => queue.shift()?.();
|
|
2166
|
-
}
|
|
2167
|
-
queue.push(fn);
|
|
2168
|
-
channel.port2.postMessage(0);
|
|
2169
|
-
};
|
|
2170
|
-
})();
|
|
2171
|
-
const states = /* @__PURE__ */ new WeakMap();
|
|
2172
|
-
/** @internal Registered by the multiplexer for each port with a window. */
|
|
2173
|
-
function setPortSendState(port, state) {
|
|
2174
|
-
states.set(port, state);
|
|
2175
|
-
}
|
|
2176
|
-
/**
|
|
2177
2483
|
* One virtual port.
|
|
2178
2484
|
*
|
|
2179
2485
|
* `deliver` and `markClosed` are deliberately not on `port`: the consumer holds
|
|
@@ -2251,6 +2557,21 @@ function newVirtualPort(send, requestClose) {
|
|
|
2251
2557
|
const KEEPALIVE_ID = Number.MAX_SAFE_INTEGER;
|
|
2252
2558
|
/** The longest delay a timer holds; past it `setInterval` fires at once. */
|
|
2253
2559
|
const MAX_TIMER_MS = 2 ** 31 - 1;
|
|
2560
|
+
/**
|
|
2561
|
+
* What a windowed port offers in its `open`: every capability this end
|
|
2562
|
+
* speaks. `end` only over a codec that reads it (`PortCodec.halfClose`).
|
|
2563
|
+
*/
|
|
2564
|
+
function ownCaps(codec) {
|
|
2565
|
+
return codec.halfClose ? ["pipeline", "end"] : ["pipeline"];
|
|
2566
|
+
}
|
|
2567
|
+
/**
|
|
2568
|
+
* The capabilities in a peer's `caps` that this end speaks too, or
|
|
2569
|
+
* `undefined` when the peer listed none (it is then treated as an older peer).
|
|
2570
|
+
*/
|
|
2571
|
+
function sharedCaps(own, listed) {
|
|
2572
|
+
if (!Array.isArray(listed)) return void 0;
|
|
2573
|
+
return new Set(own.filter((cap) => listed.includes(cap)));
|
|
2574
|
+
}
|
|
2254
2575
|
/** Ceiling on concurrently open virtual ports. Bounds the id table only. */
|
|
2255
2576
|
const DEFAULT_MAX_PORTS = 1024;
|
|
2256
2577
|
/**
|
|
@@ -2282,10 +2603,18 @@ function estimateMessageSize(payload) {
|
|
|
2282
2603
|
*/
|
|
2283
2604
|
function multiplexPort(port, options) {
|
|
2284
2605
|
const { codec, onPort, side = "initiator", maxPorts = DEFAULT_MAX_PORTS, maxMessageSize, signal, window, frameSize, keepalive, onLeak } = options;
|
|
2606
|
+
const maxReassembledBytes = options.maxReassembledBytes ?? 67108864;
|
|
2285
2607
|
const misses = keepalive?.misses ?? 3;
|
|
2286
2608
|
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`);
|
|
2287
2609
|
if (frameSize != null && !(Number.isSafeInteger(frameSize) && frameSize > 0)) throw new RangeError(`webrun-rpc: frameSize must be a positive integer, got ${frameSize}`);
|
|
2610
|
+
if (!(Number.isSafeInteger(maxReassembledBytes) && maxReassembledBytes > 0)) throw new RangeError(`webrun-rpc: maxReassembledBytes must be a positive integer, got ${maxReassembledBytes}`);
|
|
2288
2611
|
const open = /* @__PURE__ */ new Map();
|
|
2612
|
+
let reassembling = 0;
|
|
2613
|
+
const freePartial = (entry) => {
|
|
2614
|
+
if (!entry?.partial) return;
|
|
2615
|
+
reassembling -= entry.partial.charged;
|
|
2616
|
+
entry.partial = void 0;
|
|
2617
|
+
};
|
|
2289
2618
|
const leaks = onLeak ? new FinalizationRegistry(({ id, record, release }) => {
|
|
2290
2619
|
const entry = open.get(id);
|
|
2291
2620
|
if (entry?.handle !== record) return;
|
|
@@ -2306,6 +2635,8 @@ function multiplexPort(port, options) {
|
|
|
2306
2635
|
let muxClosed = false;
|
|
2307
2636
|
/** This end counts encoded bytes and reassembles frames. */
|
|
2308
2637
|
const exactCodec = codec.encode != null && codec.decode != null;
|
|
2638
|
+
/** What this end offers on a windowed port. */
|
|
2639
|
+
const caps = ownCaps(codec);
|
|
2309
2640
|
const post = (envelope, transfer) => {
|
|
2310
2641
|
if (muxClosed) return;
|
|
2311
2642
|
codecPost(envelope, transfer);
|
|
@@ -2363,13 +2694,39 @@ function multiplexPort(port, options) {
|
|
|
2363
2694
|
port.postMessage(bytes);
|
|
2364
2695
|
} catch {}
|
|
2365
2696
|
};
|
|
2697
|
+
/**
|
|
2698
|
+
* Both directions have ended: free the id as a close would, with no `close`
|
|
2699
|
+
* on the wire (each end frees it on its own once it has seen both ends).
|
|
2700
|
+
* Aborting the close signal is how the port's consumer learns it is gone.
|
|
2701
|
+
*/
|
|
2702
|
+
const freeIfEnded = (id, c) => {
|
|
2703
|
+
const entry = open.get(id);
|
|
2704
|
+
if (!c.endSent || !c.peerEnded || entry?.credit !== c) return;
|
|
2705
|
+
freePartial(entry);
|
|
2706
|
+
open.delete(id);
|
|
2707
|
+
leaks?.unregister(entry.handle);
|
|
2708
|
+
entry.handle.markClosed(/* @__PURE__ */ new Error("webrun-rpc: the port ended in both directions"));
|
|
2709
|
+
};
|
|
2710
|
+
/** A peer that broke the port's protocol: hold none of its traffic, and close. */
|
|
2711
|
+
const failPort = (id, entry, error, reason) => {
|
|
2712
|
+
freePartial(entry);
|
|
2713
|
+
dropOutgoing(id);
|
|
2714
|
+
open.delete(id);
|
|
2715
|
+
releaseWaiters(entry.credit);
|
|
2716
|
+
entry.handle.markClosed(error);
|
|
2717
|
+
post({
|
|
2718
|
+
type: "close",
|
|
2719
|
+
id,
|
|
2720
|
+
reason
|
|
2721
|
+
});
|
|
2722
|
+
};
|
|
2366
2723
|
const releaseWaiters = (c) => {
|
|
2367
2724
|
for (const w of c?.waiters.splice(0) ?? []) w.resolve();
|
|
2368
2725
|
};
|
|
2369
2726
|
const flush = (c) => {
|
|
2370
2727
|
while (c.queue.length) {
|
|
2371
2728
|
const next = c.queue[0];
|
|
2372
|
-
if (c.enforced && c.outstanding > 0 && c.outstanding + next.size > c.window) break;
|
|
2729
|
+
if (next.size > 0 && c.enforced && c.outstanding > 0 && c.outstanding + next.size > c.window) break;
|
|
2373
2730
|
c.queue.shift();
|
|
2374
2731
|
c.queued -= next.size;
|
|
2375
2732
|
c.outstanding += next.size;
|
|
@@ -2407,15 +2764,20 @@ function multiplexPort(port, options) {
|
|
|
2407
2764
|
enforced: false,
|
|
2408
2765
|
opener,
|
|
2409
2766
|
exact,
|
|
2767
|
+
caps: /* @__PURE__ */ new Set(),
|
|
2410
2768
|
outstanding: 0,
|
|
2411
2769
|
queue: [],
|
|
2412
2770
|
queued: 0,
|
|
2413
2771
|
unconsumed: 0,
|
|
2414
2772
|
waiters: [],
|
|
2415
2773
|
toGrant: 0,
|
|
2416
|
-
grantScheduled: false
|
|
2774
|
+
grantScheduled: false,
|
|
2775
|
+
ending: false,
|
|
2776
|
+
endSent: false,
|
|
2777
|
+
peerEnded: false
|
|
2417
2778
|
} : void 0;
|
|
2418
2779
|
const self = newVirtualPort((payload, transfer) => {
|
|
2780
|
+
if (c?.ending) return;
|
|
2419
2781
|
touch(id);
|
|
2420
2782
|
const envelope = {
|
|
2421
2783
|
type: "message",
|
|
@@ -2433,12 +2795,12 @@ function multiplexPort(port, options) {
|
|
|
2433
2795
|
...envelope,
|
|
2434
2796
|
payload: structuredClone(payload, { transfer })
|
|
2435
2797
|
} : void 0;
|
|
2436
|
-
const send = () => {
|
|
2798
|
+
const send = (whole = false) => {
|
|
2437
2799
|
if (!bytes) {
|
|
2438
2800
|
sendOnPort(id, held ? () => post(held) : () => post(envelope, transfer));
|
|
2439
2801
|
return;
|
|
2440
2802
|
}
|
|
2441
|
-
if (frameSize != null && c.enforced && c.exact && bytes.byteLength > frameSize) {
|
|
2803
|
+
if (!whole && frameSize != null && c.enforced && c.exact && bytes.byteLength > frameSize) {
|
|
2442
2804
|
for (let at = 0; at < bytes.byteLength; at += frameSize) {
|
|
2443
2805
|
const data = bytes.subarray(at, at + frameSize);
|
|
2444
2806
|
const frame = at + frameSize >= bytes.byteLength ? {
|
|
@@ -2468,10 +2830,11 @@ function multiplexPort(port, options) {
|
|
|
2468
2830
|
record.markClosed(reason);
|
|
2469
2831
|
});
|
|
2470
2832
|
function release(reason) {
|
|
2833
|
+
freePartial(open.get(id));
|
|
2471
2834
|
open.delete(id);
|
|
2472
2835
|
leaks?.unregister(record);
|
|
2473
2836
|
if (c?.queue.length) {
|
|
2474
|
-
for (const item of c.queue.splice(0)) item.send();
|
|
2837
|
+
for (const item of c.queue.splice(0)) item.send(true);
|
|
2475
2838
|
c.queued = 0;
|
|
2476
2839
|
}
|
|
2477
2840
|
sendOnPort(id, () => post({
|
|
@@ -2501,6 +2864,38 @@ function multiplexPort(port, options) {
|
|
|
2501
2864
|
});
|
|
2502
2865
|
})
|
|
2503
2866
|
});
|
|
2867
|
+
setPortEndState(self.port, {
|
|
2868
|
+
end: () => {
|
|
2869
|
+
if (isClosed() || !c.caps.has("end")) return false;
|
|
2870
|
+
if (c.ending) return true;
|
|
2871
|
+
c.ending = true;
|
|
2872
|
+
c.queue.push({
|
|
2873
|
+
size: 0,
|
|
2874
|
+
send: () => sendOnPort(id, () => {
|
|
2875
|
+
if (open.get(id)?.credit !== c) return;
|
|
2876
|
+
post({
|
|
2877
|
+
type: "end",
|
|
2878
|
+
id
|
|
2879
|
+
});
|
|
2880
|
+
c.endSent = true;
|
|
2881
|
+
freeIfEnded(id, c);
|
|
2882
|
+
})
|
|
2883
|
+
});
|
|
2884
|
+
flush(c);
|
|
2885
|
+
return true;
|
|
2886
|
+
},
|
|
2887
|
+
watchPeerEnd: (resolve) => {
|
|
2888
|
+
if (!c.caps.has("end")) return false;
|
|
2889
|
+
if (c.peerEnded) resolve();
|
|
2890
|
+
else c.onPeerEnd = new WeakRef(resolve);
|
|
2891
|
+
return true;
|
|
2892
|
+
}
|
|
2893
|
+
});
|
|
2894
|
+
setPortAgreement(self.port, () => ({
|
|
2895
|
+
window: c.window,
|
|
2896
|
+
caps: c.caps,
|
|
2897
|
+
enforced: c.enforced
|
|
2898
|
+
}));
|
|
2504
2899
|
}
|
|
2505
2900
|
const record = {
|
|
2506
2901
|
deliver: self.deliver,
|
|
@@ -2552,16 +2947,24 @@ function multiplexPort(port, options) {
|
|
|
2552
2947
|
}
|
|
2553
2948
|
const handle = attach(envelope.id, envelope.meta, envelope.window, false, envelope.exact === true && exactCodec);
|
|
2554
2949
|
const credit = open.get(envelope.id)?.credit;
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2950
|
+
const listed = sharedCaps(caps, envelope.caps);
|
|
2951
|
+
if (credit) {
|
|
2952
|
+
const ack = credit.exact ? {
|
|
2953
|
+
type: "credit",
|
|
2954
|
+
id: envelope.id,
|
|
2955
|
+
bytes: 0,
|
|
2956
|
+
exact: true
|
|
2957
|
+
} : {
|
|
2958
|
+
type: "credit",
|
|
2959
|
+
id: envelope.id,
|
|
2960
|
+
bytes: 0
|
|
2961
|
+
};
|
|
2962
|
+
if (listed) credit.caps = listed;
|
|
2963
|
+
post(listed ? {
|
|
2964
|
+
...ack,
|
|
2965
|
+
caps: [...listed]
|
|
2966
|
+
} : ack);
|
|
2967
|
+
}
|
|
2565
2968
|
let accepted = false;
|
|
2566
2969
|
if (onPort) try {
|
|
2567
2970
|
accepted = onPort(handle.port, envelope.meta) !== false;
|
|
@@ -2589,6 +2992,7 @@ function multiplexPort(port, options) {
|
|
|
2589
2992
|
c.exact = false;
|
|
2590
2993
|
c.outstanding = 0;
|
|
2591
2994
|
}
|
|
2995
|
+
c.caps = sharedCaps(caps, envelope.caps) ?? /* @__PURE__ */ new Set();
|
|
2592
2996
|
post({
|
|
2593
2997
|
type: "credit",
|
|
2594
2998
|
id: envelope.id,
|
|
@@ -2600,6 +3004,10 @@ function multiplexPort(port, options) {
|
|
|
2600
3004
|
flush(c);
|
|
2601
3005
|
return;
|
|
2602
3006
|
}
|
|
3007
|
+
if ((envelope.type === "message" || envelope.type === "frame") && entry?.credit?.peerEnded) {
|
|
3008
|
+
failPort(envelope.id, entry, /* @__PURE__ */ new Error("webrun-rpc: the peer sent on a port after its end"), "protocol");
|
|
3009
|
+
return;
|
|
3010
|
+
}
|
|
2603
3011
|
if (envelope.type === "message") {
|
|
2604
3012
|
const sizeOf = (c) => (c.exact ? byteLengthOf(event.data) : void 0) ?? estimateMessageSize(envelope.payload);
|
|
2605
3013
|
if (!entry) return;
|
|
@@ -2610,15 +3018,29 @@ function multiplexPort(port, options) {
|
|
|
2610
3018
|
if (envelope.type === "frame") {
|
|
2611
3019
|
if (!entry?.credit?.exact || !codec.decode || !(envelope.data instanceof Uint8Array)) return;
|
|
2612
3020
|
touch(envelope.id);
|
|
3021
|
+
const c = entry.credit;
|
|
3022
|
+
const owed = c.unconsumed + c.toGrant;
|
|
2613
3023
|
const partial = entry.partial ?? {
|
|
2614
3024
|
chunks: [],
|
|
2615
|
-
bytes: 0
|
|
3025
|
+
bytes: 0,
|
|
3026
|
+
owed,
|
|
3027
|
+
charged: 0,
|
|
3028
|
+
...owed > 0 ? { room: c.window - owed } : {}
|
|
2616
3029
|
};
|
|
2617
3030
|
entry.partial = partial;
|
|
3031
|
+
const size = envelope.data.byteLength;
|
|
3032
|
+
const charged = partial.room == null ? Math.max(0, partial.bytes + size - c.window) : 0;
|
|
3033
|
+
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;
|
|
3034
|
+
if (overrun) {
|
|
3035
|
+
failPort(envelope.id, entry, /* @__PURE__ */ new RangeError(`webrun-rpc: a framed message passed ${overrun}`), "max-reassembled-bytes");
|
|
3036
|
+
return;
|
|
3037
|
+
}
|
|
3038
|
+
partial.bytes += size;
|
|
3039
|
+
reassembling += charged - partial.charged;
|
|
3040
|
+
partial.charged = charged;
|
|
2618
3041
|
partial.chunks.push(envelope.data);
|
|
2619
|
-
partial.bytes += envelope.data.byteLength;
|
|
2620
3042
|
if (!envelope.last) return;
|
|
2621
|
-
entry
|
|
3043
|
+
freePartial(entry);
|
|
2622
3044
|
const whole = new Uint8Array(partial.bytes);
|
|
2623
3045
|
let at = 0;
|
|
2624
3046
|
for (const chunk of partial.chunks) {
|
|
@@ -2630,8 +3052,22 @@ function multiplexPort(port, options) {
|
|
|
2630
3052
|
entry.handle.deliver(inner.payload, whole.byteLength);
|
|
2631
3053
|
return;
|
|
2632
3054
|
}
|
|
3055
|
+
if (envelope.type === "end") {
|
|
3056
|
+
if (!entry) return;
|
|
3057
|
+
const c = entry.credit;
|
|
3058
|
+
if (!c?.caps.has("end")) {
|
|
3059
|
+
failPort(envelope.id, entry, /* @__PURE__ */ new Error("webrun-rpc: the peer ended a port where end was not agreed"), "protocol");
|
|
3060
|
+
return;
|
|
3061
|
+
}
|
|
3062
|
+
touch(envelope.id);
|
|
3063
|
+
c.peerEnded = true;
|
|
3064
|
+
c.onPeerEnd?.deref()?.();
|
|
3065
|
+
freeIfEnded(envelope.id, c);
|
|
3066
|
+
return;
|
|
3067
|
+
}
|
|
2633
3068
|
dropOutgoing(envelope.id);
|
|
2634
3069
|
if (!entry) return;
|
|
3070
|
+
freePartial(entry);
|
|
2635
3071
|
open.delete(envelope.id);
|
|
2636
3072
|
releaseWaiters(entry.credit);
|
|
2637
3073
|
entry.handle.markClosed(/* @__PURE__ */ new Error("webrun-rpc: the peer closed the port"));
|
|
@@ -2710,12 +3146,14 @@ function multiplexPort(port, options) {
|
|
|
2710
3146
|
id,
|
|
2711
3147
|
meta,
|
|
2712
3148
|
window,
|
|
2713
|
-
exact: true
|
|
3149
|
+
exact: true,
|
|
3150
|
+
caps: [...caps]
|
|
2714
3151
|
} : {
|
|
2715
3152
|
type: "open",
|
|
2716
3153
|
id,
|
|
2717
3154
|
meta,
|
|
2718
|
-
window
|
|
3155
|
+
window,
|
|
3156
|
+
caps: [...caps]
|
|
2719
3157
|
} : {
|
|
2720
3158
|
type: "open",
|
|
2721
3159
|
id,
|
|
@@ -2822,14 +3260,17 @@ const serve = async ({ mux: factory, timeout }, handler) => {
|
|
|
2822
3260
|
const mux = await factory((port) => {
|
|
2823
3261
|
let off;
|
|
2824
3262
|
let finished = false;
|
|
2825
|
-
const tracked = (input) =>
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
3263
|
+
const tracked = (input) => {
|
|
3264
|
+
const output = handler(input);
|
|
3265
|
+
return returningAtOnce((async function* () {
|
|
3266
|
+
try {
|
|
3267
|
+
yield* output;
|
|
3268
|
+
} finally {
|
|
3269
|
+
finished = true;
|
|
3270
|
+
if (off) live.delete(off);
|
|
3271
|
+
}
|
|
3272
|
+
})(), () => void output.return(void 0).catch(() => {}));
|
|
3273
|
+
};
|
|
2833
3274
|
off = serveDuplexOverPort(port, tracked, streamOptions);
|
|
2834
3275
|
if (!finished) live.add(off);
|
|
2835
3276
|
});
|
|
@@ -2867,7 +3308,7 @@ function isEnvelope(value) {
|
|
|
2867
3308
|
const candidate = value;
|
|
2868
3309
|
if (typeof candidate.id !== "number") return false;
|
|
2869
3310
|
if (!Number.isInteger(candidate.id) || candidate.id < 0) return false;
|
|
2870
|
-
return candidate.type === "open" || candidate.type === "message" || candidate.type === "close" || candidate.type === "credit" || candidate.type === "frame" || candidate.type === "ping" || candidate.type === "pong";
|
|
3311
|
+
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";
|
|
2871
3312
|
}
|
|
2872
3313
|
/**
|
|
2873
3314
|
* For ports whose messages are structured values — a real `MessagePort`, a
|
|
@@ -2879,6 +3320,7 @@ function isEnvelope(value) {
|
|
|
2879
3320
|
* not send anything a byte codec could not also carry.
|
|
2880
3321
|
*/
|
|
2881
3322
|
const structuredCodec = {
|
|
3323
|
+
halfClose: true,
|
|
2882
3324
|
post(port, envelope, transfer) {
|
|
2883
3325
|
if (transfer && transfer.length > 0) port.postMessage(envelope, transfer);
|
|
2884
3326
|
else port.postMessage(envelope);
|
|
@@ -2916,10 +3358,15 @@ function asyncDisposable(target, dispose) {
|
|
|
2916
3358
|
* own `handleStreams` / `sendStream` (no backpressure); version 2 is
|
|
2917
3359
|
* webrun-rpc's `connect` / `serve` over `overPipe` with webrun-http-streams'
|
|
2918
3360
|
* `fetchOverDuplex` / `serveFetchOverDuplex`: one port per link, calls
|
|
2919
|
-
* multiplexed on it, each call a stream with
|
|
2920
|
-
* directions, and cancellation that reaches the handler.
|
|
3361
|
+
* multiplexed on it, each call a stream with backpressure in both
|
|
3362
|
+
* directions, and cancellation that reaches the handler. A call's port with a
|
|
3363
|
+
* window (`HttpLinkOptions.window`) pipelines its chunks where both ends speak
|
|
3364
|
+
* it, and goes one confirmed chunk at a time otherwise, so the window needs
|
|
3365
|
+
* no new version.
|
|
2921
3366
|
*/
|
|
2922
3367
|
const HTTP_LINK_PROTOCOL = "webrun-http-link/2";
|
|
3368
|
+
/** The calling end's default `HttpLinkOptions.window`: 256 KiB. */
|
|
3369
|
+
const DEFAULT_HTTP_LINK_WINDOW = 262144;
|
|
2923
3370
|
/**
|
|
2924
3371
|
* Why a request failed when its link went away under it: the far side (the
|
|
2925
3372
|
* handler, or the ServiceWorker) was lost. The request may or may not have
|
|
@@ -2938,12 +3385,13 @@ var LinkLostError = class extends Error {
|
|
|
2938
3385
|
*/
|
|
2939
3386
|
const LINK_CLOSE = "webrun-http:link-close";
|
|
2940
3387
|
/** Opens the calling end of a link on `port` (one port per link). */
|
|
2941
|
-
async function connectHttpLink(port, { streamTimeout, maxPorts } = {}) {
|
|
3388
|
+
async function connectHttpLink(port, { streamTimeout, maxPorts, window = DEFAULT_HTTP_LINK_WINDOW } = {}) {
|
|
2942
3389
|
const { call, close } = await connect({
|
|
2943
3390
|
mux: overPipe(port, {
|
|
2944
3391
|
codec: structuredCodec,
|
|
2945
3392
|
side: "initiator",
|
|
2946
|
-
maxPorts
|
|
3393
|
+
maxPorts,
|
|
3394
|
+
window
|
|
2947
3395
|
}),
|
|
2948
3396
|
timeout: streamTimeout
|
|
2949
3397
|
});
|
|
@@ -3399,6 +3847,7 @@ var PortSocketCore = class extends EventTarget {
|
|
|
3399
3847
|
code,
|
|
3400
3848
|
reason
|
|
3401
3849
|
});
|
|
3850
|
+
if (!Number.isFinite(this._closeTimeoutMs)) return;
|
|
3402
3851
|
this._closeTimer = this._timers.setTimeout(() => this._finish(1006, "", false), this._closeTimeoutMs);
|
|
3403
3852
|
}
|
|
3404
3853
|
_onCloseFrame(code, reason) {
|
|
@@ -3764,7 +4213,8 @@ var PortRendezvous = class {
|
|
|
3764
4213
|
* `whenDrained(limit)` waits for it to fall: the backpressure a producer
|
|
3765
4214
|
* applies.
|
|
3766
4215
|
* - When the client's pings stop for three heartbeat periods (a closed tab, a
|
|
3767
|
-
* terminated worker), the socket closes with 1006.
|
|
4216
|
+
* terminated worker), the socket closes with 1006. With `watch: "first-ping"`
|
|
4217
|
+
* the watch starts only once the client pings.
|
|
3768
4218
|
*/
|
|
3769
4219
|
var ServerWebSocket = class extends PortSocketCore {
|
|
3770
4220
|
static CONNECTING = 0;
|
|
@@ -3773,55 +4223,150 @@ var ServerWebSocket = class extends PortSocketCore {
|
|
|
3773
4223
|
static CLOSED = 3;
|
|
3774
4224
|
url;
|
|
3775
4225
|
protocol;
|
|
4226
|
+
_binaryType = "arraybuffer";
|
|
4227
|
+
_options;
|
|
3776
4228
|
_accepted = false;
|
|
4229
|
+
_detached = false;
|
|
3777
4230
|
_queue = [];
|
|
3778
4231
|
_heartbeatMs = DEFAULT_HEARTBEAT_MS;
|
|
3779
4232
|
_lastPing = Date.now();
|
|
4233
|
+
/** Whether a ping (with a period a timer takes) arrived already. */
|
|
4234
|
+
_pinged = false;
|
|
3780
4235
|
_watch;
|
|
3781
4236
|
constructor(port, options = {}) {
|
|
3782
4237
|
super({
|
|
3783
4238
|
timers: options.timers,
|
|
3784
4239
|
closeTimeoutMs: options.closeTimeoutMs
|
|
3785
4240
|
});
|
|
4241
|
+
this._options = options;
|
|
3786
4242
|
this.url = options.url ?? "";
|
|
3787
4243
|
this.protocol = options.protocol ?? "";
|
|
3788
4244
|
this._attach(port);
|
|
3789
4245
|
this._state = 1;
|
|
3790
|
-
this._watchHeartbeat();
|
|
4246
|
+
if (options.watch !== "first-ping") this._watchHeartbeat();
|
|
3791
4247
|
}
|
|
3792
4248
|
/** Whether `accept()` was called. */
|
|
3793
4249
|
get accepted() {
|
|
3794
4250
|
return this._accepted;
|
|
3795
4251
|
}
|
|
4252
|
+
/** How binary messages arrive: `ArrayBuffer`s (the default) or `Blob`s. Other values are ignored. */
|
|
4253
|
+
get binaryType() {
|
|
4254
|
+
return this._binaryType;
|
|
4255
|
+
}
|
|
4256
|
+
set binaryType(value) {
|
|
4257
|
+
if (value === "blob" || value === "arraybuffer") this._binaryType = value;
|
|
4258
|
+
}
|
|
4259
|
+
/**
|
|
4260
|
+
* Hands this end's transport to another holder before it was accepted: the port to give the
|
|
4261
|
+
* client (a pair's end returned in a 101, say), with the frames that reached this end first
|
|
4262
|
+
* delivered ahead of the rest. This end is closed then, with no event, and answers nothing.
|
|
4263
|
+
*/
|
|
4264
|
+
detach() {
|
|
4265
|
+
const port = this._port;
|
|
4266
|
+
if (!port || this._accepted || this._state !== 1) throw new DOMException("Only an open end that was not accepted can be detached.", "InvalidStateError");
|
|
4267
|
+
this._detached = true;
|
|
4268
|
+
this._port = void 0;
|
|
4269
|
+
port.onmessage = null;
|
|
4270
|
+
let owed = this.bufferedAmount;
|
|
4271
|
+
this._state = 3;
|
|
4272
|
+
this._onClosed();
|
|
4273
|
+
this._wakeDrainWaiters();
|
|
4274
|
+
const queued = this._queue.splice(0);
|
|
4275
|
+
if (queued.length === 0 && owed === 0) return port;
|
|
4276
|
+
const relay = new MessageChannel();
|
|
4277
|
+
const holder = relay.port2;
|
|
4278
|
+
const closing = /* @__PURE__ */ new Set();
|
|
4279
|
+
const end = () => {
|
|
4280
|
+
port.onmessage = null;
|
|
4281
|
+
holder.onmessage = null;
|
|
4282
|
+
port.close();
|
|
4283
|
+
holder.close();
|
|
4284
|
+
};
|
|
4285
|
+
const sawClose = (way) => {
|
|
4286
|
+
closing.add(way);
|
|
4287
|
+
if (closing.size === 2) end();
|
|
4288
|
+
};
|
|
4289
|
+
for (const frame of queued) {
|
|
4290
|
+
postFrame(holder, frame);
|
|
4291
|
+
if (frame.t === "c") closing.add("in");
|
|
4292
|
+
}
|
|
4293
|
+
holder.onmessage = (event) => {
|
|
4294
|
+
port.postMessage(event.data, transferables(event.data));
|
|
4295
|
+
if (event.data?.t === "c") sawClose("out");
|
|
4296
|
+
};
|
|
4297
|
+
port.onmessage = (event) => {
|
|
4298
|
+
const data = event.data;
|
|
4299
|
+
if (data?.t === "a" && typeof data.n === "number" && owed > 0) {
|
|
4300
|
+
const absorbed = Math.min(owed, data.n);
|
|
4301
|
+
owed -= absorbed;
|
|
4302
|
+
if (data.n > absorbed) holder.postMessage({
|
|
4303
|
+
t: "a",
|
|
4304
|
+
n: data.n - absorbed
|
|
4305
|
+
});
|
|
4306
|
+
return;
|
|
4307
|
+
}
|
|
4308
|
+
holder.postMessage(event.data, transferables(event.data));
|
|
4309
|
+
if (data?.t === "c") sawClose("in");
|
|
4310
|
+
};
|
|
4311
|
+
port.addEventListener?.("close", end);
|
|
4312
|
+
holder.addEventListener?.("close", end);
|
|
4313
|
+
return relay.port1;
|
|
4314
|
+
}
|
|
3796
4315
|
/** Starts delivering events, the queued ones first. */
|
|
3797
4316
|
accept() {
|
|
4317
|
+
if (this._detached) throw new DOMException("This end was detached; it cannot be accepted.", "InvalidStateError");
|
|
3798
4318
|
if (this._accepted) return;
|
|
3799
4319
|
this._accepted = true;
|
|
3800
|
-
for (const frame of this._queue.splice(0))
|
|
4320
|
+
for (const frame of this._queue.splice(0)) this._deliver(frame);
|
|
3801
4321
|
}
|
|
3802
4322
|
send(data) {
|
|
3803
|
-
if (
|
|
4323
|
+
if (this._detached) throw new DOMException("This end was detached; it cannot send.", "InvalidStateError");
|
|
4324
|
+
if (!this._accepted && !this._options.sendBeforeAccept) throw new DOMException("accept() the socket before sending on it.", "InvalidStateError");
|
|
3804
4325
|
this._sendData(isBinary(data) ? toArrayBuffer(data) : String(data));
|
|
3805
4326
|
}
|
|
3806
|
-
close(code
|
|
3807
|
-
|
|
4327
|
+
close(code, reason = "") {
|
|
4328
|
+
this.checkClose(code, reason);
|
|
4329
|
+
this._startClose(code ?? this._options.defaultCloseCode ?? 1e3, reason);
|
|
4330
|
+
}
|
|
4331
|
+
/**
|
|
4332
|
+
* Refuses a `close()` the endpoint may not make: by default a code outside RFC 6455 §7.4's
|
|
4333
|
+
* sendable ones, or a reason over 123 bytes. A subclass with another platform's rules
|
|
4334
|
+
* overrides it.
|
|
4335
|
+
*/
|
|
4336
|
+
checkClose(code, reason) {
|
|
4337
|
+
if (code !== void 0 && !isSendableCloseCode(code)) throw new DOMException(`${code} is not a close code an endpoint may send.`, "InvalidAccessError");
|
|
3808
4338
|
if (utf8Length(reason) > 123) throw new DOMException("The close reason is longer than 123 bytes.", "SyntaxError");
|
|
3809
|
-
this._startClose(code, reason);
|
|
3810
4339
|
}
|
|
3811
4340
|
_deliver(frame) {
|
|
3812
4341
|
if (!this._accepted) {
|
|
3813
4342
|
this._queue.push(frame);
|
|
3814
4343
|
return;
|
|
3815
4344
|
}
|
|
4345
|
+
if (frame.t === "m" && this._state === 2 && this._options.deliverWhileClosing) {
|
|
4346
|
+
this._dispatchMessage(frame.d);
|
|
4347
|
+
this._consumed(messageSize(frame.d));
|
|
4348
|
+
return;
|
|
4349
|
+
}
|
|
3816
4350
|
super._deliver(frame);
|
|
3817
4351
|
}
|
|
3818
4352
|
_dispatchMessage(data) {
|
|
3819
|
-
this.
|
|
4353
|
+
const delivered = typeof data !== "string" && this._binaryType === "blob" ? new Blob([data]) : data;
|
|
4354
|
+
this.dispatchEvent(new MessageEvent("message", { data: delivered }));
|
|
3820
4355
|
}
|
|
3821
4356
|
_onPing(hb) {
|
|
3822
4357
|
this._post({ t: "q" });
|
|
3823
4358
|
this._lastPing = Date.now();
|
|
3824
|
-
if (
|
|
4359
|
+
if (this._state === 3) return;
|
|
4360
|
+
const keep = this._options.heartbeatPeriod === "first" && this._pinged;
|
|
4361
|
+
if (!(hb > 0 && hb < 2 ** 31 - 1)) {
|
|
4362
|
+
if (!keep && this._watch !== void 0) {
|
|
4363
|
+
this._timers.clearInterval(this._watch);
|
|
4364
|
+
this._watch = void 0;
|
|
4365
|
+
}
|
|
4366
|
+
return;
|
|
4367
|
+
}
|
|
4368
|
+
this._pinged = true;
|
|
4369
|
+
if (!keep && (this._watch === void 0 || hb !== this._heartbeatMs)) {
|
|
3825
4370
|
this._heartbeatMs = hb;
|
|
3826
4371
|
this._watchHeartbeat();
|
|
3827
4372
|
}
|
|
@@ -3843,6 +4388,11 @@ function isSendableCloseCode(code) {
|
|
|
3843
4388
|
if (code >= 3e3 && code <= 4999) return true;
|
|
3844
4389
|
return code >= 1e3 && code <= 1014 && code !== 1004 && code !== 1005 && code !== 1006;
|
|
3845
4390
|
}
|
|
4391
|
+
/** The buffers a relayed frame carries, to transfer rather than copy. */
|
|
4392
|
+
function transferables(data) {
|
|
4393
|
+
const d = data?.d;
|
|
4394
|
+
return d instanceof ArrayBuffer ? [d] : [];
|
|
4395
|
+
}
|
|
3846
4396
|
//#endregion
|
|
3847
4397
|
//#region src/ws/upgrade.ts
|
|
3848
4398
|
/**
|
|
@@ -3874,6 +4424,7 @@ function newWebSocketUpgradeRequest(url, { protocols = [], headers } = {}) {
|
|
|
3874
4424
|
all.set("connection", "Upgrade");
|
|
3875
4425
|
all.set("sec-websocket-version", "13");
|
|
3876
4426
|
if (!all.has("sec-websocket-key")) all.set("sec-websocket-key", randomKey());
|
|
4427
|
+
all.delete("sec-websocket-protocol");
|
|
3877
4428
|
if (protocols.length > 0) all.set("sec-websocket-protocol", protocols.join(", "));
|
|
3878
4429
|
if (!all.has("x-webrun-ws-version")) all.set(WS_VERSION_HEADER, `2`);
|
|
3879
4430
|
return withHeaders(new Request(httpUrl, {
|
|
@@ -3933,6 +4484,23 @@ function upgradeWebSocket(request, options = {}) {
|
|
|
3933
4484
|
url: request.url,
|
|
3934
4485
|
protocol
|
|
3935
4486
|
});
|
|
4487
|
+
const response = webSocketUpgradeResponse(channel.port2, protocol);
|
|
4488
|
+
Object.defineProperty(response, "webSocket", {
|
|
4489
|
+
value: socket,
|
|
4490
|
+
configurable: true
|
|
4491
|
+
});
|
|
4492
|
+
return {
|
|
4493
|
+
socket,
|
|
4494
|
+
response
|
|
4495
|
+
};
|
|
4496
|
+
}
|
|
4497
|
+
/**
|
|
4498
|
+
* A 101 for a socket whose server end the handler runs itself, on the other end of `port`
|
|
4499
|
+
* (a server of its own over this package's frames, say): what `upgradeWebSocket` returns, minus
|
|
4500
|
+
* the `ServerWebSocket`. The host hands `port` to the caller as it would for `upgradeWebSocket`
|
|
4501
|
+
* (`takePort`), and `protocol` (`""` for none) is the subprotocol picked.
|
|
4502
|
+
*/
|
|
4503
|
+
function webSocketUpgradeResponse(port, protocol = "") {
|
|
3936
4504
|
const response = new Response(null, {
|
|
3937
4505
|
status: 200,
|
|
3938
4506
|
headers: protocol ? { "sec-websocket-protocol": protocol } : {}
|
|
@@ -3946,23 +4514,16 @@ function upgradeWebSocket(request, options = {}) {
|
|
|
3946
4514
|
value: "Switching Protocols",
|
|
3947
4515
|
configurable: true
|
|
3948
4516
|
},
|
|
3949
|
-
webSocket: {
|
|
3950
|
-
value: socket,
|
|
3951
|
-
configurable: true
|
|
3952
|
-
},
|
|
3953
4517
|
[UPGRADE]: {
|
|
3954
4518
|
value: {
|
|
3955
|
-
port
|
|
4519
|
+
port,
|
|
3956
4520
|
protocol
|
|
3957
4521
|
},
|
|
3958
4522
|
configurable: true,
|
|
3959
4523
|
writable: true
|
|
3960
4524
|
}
|
|
3961
4525
|
});
|
|
3962
|
-
return
|
|
3963
|
-
socket,
|
|
3964
|
-
response
|
|
3965
|
-
};
|
|
4526
|
+
return response;
|
|
3966
4527
|
}
|
|
3967
4528
|
/**
|
|
3968
4529
|
* The port to hand to the caller, for a response from `upgradeWebSocket`.
|
|
@@ -4029,6 +4590,15 @@ function randomKey() {
|
|
|
4029
4590
|
crypto.getRandomValues(bytes);
|
|
4030
4591
|
return btoa(String.fromCharCode(...bytes));
|
|
4031
4592
|
}
|
|
4593
|
+
/** `headers` as pairs a `postMessage` can carry (a `Headers` object cannot be cloned). */
|
|
4594
|
+
function headerPairs(headers) {
|
|
4595
|
+
if (headers === void 0) return void 0;
|
|
4596
|
+
const pairs = [];
|
|
4597
|
+
new Headers(headers).forEach((value, name) => {
|
|
4598
|
+
pairs.push([name, value]);
|
|
4599
|
+
});
|
|
4600
|
+
return pairs;
|
|
4601
|
+
}
|
|
4032
4602
|
//#endregion
|
|
4033
4603
|
//#region src/sw/handler-endpoint.ts
|
|
4034
4604
|
/** The message calls through a lost ServiceWorker fail with. */
|
|
@@ -4113,8 +4683,11 @@ var HandlerEndpoint = class {
|
|
|
4113
4683
|
this._startKeepAlive();
|
|
4114
4684
|
return await link.up.send(request);
|
|
4115
4685
|
}
|
|
4116
|
-
/**
|
|
4117
|
-
|
|
4686
|
+
/**
|
|
4687
|
+
* Opens a socket to the handler `url` routes to, through the ServiceWorker. The handler sees
|
|
4688
|
+
* `headers` on the upgrade request, and so does a `route` function choosing it.
|
|
4689
|
+
*/
|
|
4690
|
+
async connectWebSocket(url, protocols = [], headers) {
|
|
4118
4691
|
const link = await this._linkWith("ctl");
|
|
4119
4692
|
const id = this._sockets.newId();
|
|
4120
4693
|
const answer = this._sockets.wait(id, this.options.wsConnectTimeoutMs ?? 15e3, `the socket request for ${url}`);
|
|
@@ -4122,7 +4695,8 @@ var HandlerEndpoint = class {
|
|
|
4122
4695
|
t: "ws-open",
|
|
4123
4696
|
id,
|
|
4124
4697
|
url,
|
|
4125
|
-
protocols
|
|
4698
|
+
protocols,
|
|
4699
|
+
headers: headerPairs(headers)
|
|
4126
4700
|
});
|
|
4127
4701
|
return await answer;
|
|
4128
4702
|
}
|
|
@@ -4292,11 +4866,11 @@ function serveHttpInWorker(handler, options = {}) {
|
|
|
4292
4866
|
};
|
|
4293
4867
|
scope.addEventListener("message", onMessage);
|
|
4294
4868
|
const baseUrl = () => globalThis.location?.href;
|
|
4295
|
-
const connectWebSocket = (url, protocols = []) => {
|
|
4869
|
+
const connectWebSocket = (url, protocols = [], headers) => {
|
|
4296
4870
|
const httpUrl = new URL(`${url}`, baseUrl());
|
|
4297
4871
|
if (httpUrl.protocol === "ws:") httpUrl.protocol = "http:";
|
|
4298
4872
|
else if (httpUrl.protocol === "wss:") httpUrl.protocol = "https:";
|
|
4299
|
-
return endpoint.connectWebSocket(`${httpUrl}`, protocols);
|
|
4873
|
+
return endpoint.connectWebSocket(`${httpUrl}`, protocols, headers);
|
|
4300
4874
|
};
|
|
4301
4875
|
let WebSocketClass;
|
|
4302
4876
|
return {
|
|
@@ -4323,4 +4897,4 @@ function serveHttpInWorker(handler, options = {}) {
|
|
|
4323
4897
|
};
|
|
4324
4898
|
}
|
|
4325
4899
|
//#endregion
|
|
4326
|
-
export { DEFAULT_CLOSE_TIMEOUT_MS, DEFAULT_HEARTBEAT_MS, HTTP_LINK_PROTOCOL, LinkLostError, ServerWebSocket, WS_PORT_PROTOCOL_VERSION, WS_VERSION_HEADER, WsCloseEvent, connectHttpLink, createWebSocketClass, isWebSocketUpgrade, newWebSocketUpgradeRequest, requestedProtocols, serveHttpInWorker, serveHttpLink, serveWebSocketUpgrade, takePort, takeUpgrade, trackedLink, upgradeWebSocket, withHeaders };
|
|
4900
|
+
export { DEFAULT_CLOSE_TIMEOUT_MS, DEFAULT_HEARTBEAT_MS, HTTP_LINK_PROTOCOL, LinkLostError, ServerWebSocket, WS_PORT_PROTOCOL_VERSION, WS_VERSION_HEADER, WsCloseEvent, connectHttpLink, createWebSocketClass, isWebSocketUpgrade, newWebSocketUpgradeRequest, requestedProtocols, serveHttpInWorker, serveHttpLink, serveWebSocketUpgrade, takePort, takeUpgrade, trackedLink, upgradeWebSocket, webSocketUpgradeResponse, withHeaders };
|