@statewalker/webrun-http-browser 0.10.0 → 0.12.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +155 -10
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/service-worker-control.d.ts +2 -1
- package/dist/core/service-worker-control.d.ts.map +1 -1
- package/dist/core/sw-scope.d.ts +31 -0
- package/dist/core/sw-scope.d.ts.map +1 -0
- package/dist/index.js +834 -225
- package/dist/link/http-link.d.ts +16 -3
- package/dist/link/http-link.d.ts.map +1 -1
- package/dist/relay/index-sw.d.ts +8 -2
- package/dist/relay/index-sw.d.ts.map +1 -1
- package/dist/relay/index.d.ts +1 -1
- package/dist/relay/index.d.ts.map +1 -1
- package/dist/relay-sw.js +433 -83
- package/dist/relay-worker.d.ts +1 -0
- package/dist/relay-worker.d.ts.map +1 -1
- package/dist/relay-worker.js +433 -83
- package/dist/sw/handler-endpoint.d.ts +5 -2
- package/dist/sw/handler-endpoint.d.ts.map +1 -1
- package/dist/sw/http-sw-dispatcher.d.ts +114 -7
- package/dist/sw/http-sw-dispatcher.d.ts.map +1 -1
- package/dist/sw/index.d.ts +1 -0
- package/dist/sw/index.d.ts.map +1 -1
- package/dist/sw/protocol.d.ts +3 -0
- package/dist/sw/protocol.d.ts.map +1 -1
- package/dist/sw/sw-dispatcher.d.ts +5 -2
- package/dist/sw/sw-dispatcher.d.ts.map +1 -1
- package/dist/sw/sw-http-adapter.d.ts +19 -4
- package/dist/sw/sw-http-adapter.d.ts.map +1 -1
- package/dist/sw/worker-service.d.ts +5 -2
- package/dist/sw/worker-service.d.ts.map +1 -1
- package/dist/sw-worker.js +596 -132
- package/dist/sw.js +829 -223
- package/dist/worker.js +622 -120
- package/dist/ws/frames.d.ts +6 -1
- package/dist/ws/frames.d.ts.map +1 -1
- package/dist/ws/server.d.ts +50 -2
- package/dist/ws/server.d.ts.map +1 -1
- package/dist/ws/socket-core.d.ts +5 -2
- package/dist/ws/socket-core.d.ts.map +1 -1
- package/dist/ws/upgrade.d.ts +12 -1
- package/dist/ws/upgrade.d.ts.map +1 -1
- package/package.json +5 -4
- package/src/core/index.ts +1 -0
- package/src/core/service-worker-control.ts +5 -3
- package/src/core/sw-scope.ts +27 -0
- package/src/link/http-link.ts +20 -4
- package/src/relay/index-sw.ts +15 -2
- package/src/relay/index.ts +2 -2
- package/src/relay-worker.ts +1 -0
- package/src/sw/handler-endpoint.ts +17 -4
- package/src/sw/http-sw-dispatcher.ts +267 -67
- package/src/sw/index.ts +5 -0
- package/src/sw/protocol.ts +11 -1
- package/src/sw/sw-dispatcher.ts +17 -11
- package/src/sw/sw-http-adapter.ts +36 -10
- package/src/sw/worker-service.ts +11 -4
- package/src/ws/frames.ts +18 -12
- package/src/ws/server.ts +166 -11
- package/src/ws/socket-core.ts +7 -2
- package/src/ws/upgrade.ts +22 -4
package/dist/worker.js
CHANGED
|
@@ -647,7 +647,7 @@ const LAST_CHUNK = /* @__PURE__ */ new Uint8Array([
|
|
|
647
647
|
13,
|
|
648
648
|
10
|
|
649
649
|
]);
|
|
650
|
-
const encoder = /* @__PURE__ */ new TextEncoder();
|
|
650
|
+
const encoder$2 = /* @__PURE__ */ new TextEncoder();
|
|
651
651
|
/**
|
|
652
652
|
* Chunk sizes are written in HEXADECIMAL — a 21-byte chunk is `15`. Writing
|
|
653
653
|
* them in decimal is the defect note 15 found in @libp2p/http, and it is the
|
|
@@ -660,7 +660,7 @@ const encoder = /* @__PURE__ */ new TextEncoder();
|
|
|
660
660
|
async function* encodeChunked(body) {
|
|
661
661
|
for await (const chunk of body) {
|
|
662
662
|
if (chunk.byteLength === 0) continue;
|
|
663
|
-
yield encoder.encode(`${chunk.byteLength.toString(16)}\r\n`);
|
|
663
|
+
yield encoder$2.encode(`${chunk.byteLength.toString(16)}\r\n`);
|
|
664
664
|
yield chunk;
|
|
665
665
|
yield CRLF;
|
|
666
666
|
}
|
|
@@ -1806,6 +1806,99 @@ async function* throughAbort(input, signal) {
|
|
|
1806
1806
|
}
|
|
1807
1807
|
}
|
|
1808
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
|
+
/**
|
|
1809
1902
|
* Makes `target` disposable with `await using`: installs `dispose` as its
|
|
1810
1903
|
* `[Symbol.asyncDispose]`, where the runtime defines the symbol. Hand-written
|
|
1811
1904
|
* iterators lose what a native generator gets from `AsyncIteratorPrototype`;
|
|
@@ -1877,6 +1970,61 @@ function returningAtOnce(gen, abandon) {
|
|
|
1877
1970
|
* tests and adapters assert on it.
|
|
1878
1971
|
*/
|
|
1879
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$1 = "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
|
+
};
|
|
1880
2028
|
/** Caller's input travels on this channel; the handler listens on it. */
|
|
1881
2029
|
const CHANNEL_IN = "in";
|
|
1882
2030
|
/** The handler's output travels on this channel; the caller listens on it. */
|
|
@@ -1889,7 +2037,9 @@ const CHANNEL_OUT = "out";
|
|
|
1889
2037
|
* same way on the other channel. Within each direction the next chunk is never
|
|
1890
2038
|
* sent until the previous one has been delivered *and* pulled past by the
|
|
1891
2039
|
* consumer (spec D11) — the reply to a chunk call *is* the confirmation, and
|
|
1892
|
-
* `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.
|
|
1893
2043
|
*
|
|
1894
2044
|
* A stream port carries exactly one invocation. To make several calls, open
|
|
1895
2045
|
* several ports: `mux.openPort({ kind: "stream" })` per call.
|
|
@@ -1906,22 +2056,49 @@ function serveDuplexOverPort(port, handler, options = {}) {
|
|
|
1906
2056
|
const notice = installAbortNotice(port, controller);
|
|
1907
2057
|
abortOnPortClose(port, controller);
|
|
1908
2058
|
const clock = installStreamTimeout(controller, options.timeout);
|
|
1909
|
-
const inbound = receiveChunks(port, CHANNEL_IN, controller, clock.touch);
|
|
2059
|
+
const inbound = receiveChunks(port, CHANNEL_IN, controller, clock.touch, notice);
|
|
1910
2060
|
let output;
|
|
1911
2061
|
try {
|
|
1912
2062
|
output = handler(inbound.stream);
|
|
1913
2063
|
} catch (err) {
|
|
1914
|
-
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, () => {});
|
|
1915
2065
|
pump.catch(() => {});
|
|
1916
2066
|
disarmClockWhenBothSidesSettle(clock, pump, inbound.ended);
|
|
1917
2067
|
return teardownOnce(controller, notice, clock, inbound, void 0);
|
|
1918
2068
|
}
|
|
1919
|
-
|
|
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);
|
|
1920
2074
|
pump.catch(() => {});
|
|
1921
2075
|
disarmClockWhenBothSidesSettle(clock, pump, inbound.ended);
|
|
2076
|
+
endServedStream(port, sent, inbound.over);
|
|
1922
2077
|
return teardownOnce(controller, notice, clock, inbound, output);
|
|
1923
2078
|
}
|
|
1924
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
|
+
/**
|
|
1925
2102
|
* A stream that completes normally has nobody left to call the returned
|
|
1926
2103
|
* teardown — the caller only knows the stream ended, not that it must also
|
|
1927
2104
|
* dispose the serve-side handle. Without this, the last `touch()`'s timer
|
|
@@ -1961,22 +2138,31 @@ function runCallerSide(port, input, options) {
|
|
|
1961
2138
|
const notice = installAbortNotice(port, controller);
|
|
1962
2139
|
abortOnPortClose(port, controller);
|
|
1963
2140
|
const clock = installStreamTimeout(controller, options.timeout);
|
|
1964
|
-
const inbound = receiveChunks(port, CHANNEL_OUT, controller, clock.touch);
|
|
1965
|
-
|
|
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
|
+
});
|
|
1966
2146
|
pump.catch(() => {});
|
|
1967
2147
|
return returningAtOnce((async function* () {
|
|
2148
|
+
let received = false;
|
|
1968
2149
|
try {
|
|
1969
2150
|
yield* inbound.stream;
|
|
2151
|
+
received = true;
|
|
1970
2152
|
} finally {
|
|
2153
|
+
const ended = received && sent;
|
|
1971
2154
|
if (!controller.signal.aborted) controller.abort(/* @__PURE__ */ new Error("webrun-rpc: the caller abandoned the stream"));
|
|
1972
|
-
notice.post();
|
|
2155
|
+
if (!ended) notice.post();
|
|
1973
2156
|
notice.stop();
|
|
1974
2157
|
clock.stop();
|
|
1975
2158
|
inbound.stop();
|
|
1976
2159
|
await pump.catch(() => {});
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
2160
|
+
if (!(ended && endPort(port))) {
|
|
2161
|
+
notice.post();
|
|
2162
|
+
try {
|
|
2163
|
+
await port.close?.();
|
|
2164
|
+
} catch {}
|
|
2165
|
+
}
|
|
1980
2166
|
}
|
|
1981
2167
|
})(), () => {
|
|
1982
2168
|
if (!controller.signal.aborted) controller.abort(/* @__PURE__ */ new Error("webrun-rpc: the caller abandoned the stream"));
|
|
@@ -2070,7 +2256,7 @@ function installStreamTimeout(controller, timeout) {
|
|
|
2070
2256
|
* the local consumer has not started iterating, an inbound chunk waits for it —
|
|
2071
2257
|
* which is the correct backpressure, and different from having no listener.
|
|
2072
2258
|
*/
|
|
2073
|
-
function receiveChunks(port, channelName, controller, touch) {
|
|
2259
|
+
function receiveChunks(port, channelName, controller, touch, notice) {
|
|
2074
2260
|
let deliver;
|
|
2075
2261
|
const waiting = [];
|
|
2076
2262
|
let finished = false;
|
|
@@ -2080,25 +2266,64 @@ function receiveChunks(port, channelName, controller, touch) {
|
|
|
2080
2266
|
const ended = new Promise((resolve) => {
|
|
2081
2267
|
resolveEnded = resolve;
|
|
2082
2268
|
});
|
|
2269
|
+
let resolveOver = () => {};
|
|
2270
|
+
const over = new Promise((resolve) => {
|
|
2271
|
+
resolveOver = resolve;
|
|
2272
|
+
});
|
|
2083
2273
|
let aborted = false;
|
|
2084
2274
|
let abortReason;
|
|
2085
2275
|
const ready = () => deliver || finished ? Promise.resolve() : new Promise((r) => waiting.push(r));
|
|
2086
2276
|
const wake = () => {
|
|
2087
2277
|
for (const r of waiting.splice(0)) r();
|
|
2088
2278
|
};
|
|
2089
|
-
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$1 ? 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);
|
|
2090
2323
|
touch();
|
|
2091
2324
|
if (poison) throw poison;
|
|
2092
|
-
if (
|
|
2093
|
-
|
|
2094
|
-
controller.abort(poison);
|
|
2095
|
-
setTimeout(() => {
|
|
2096
|
-
try {
|
|
2097
|
-
port.close?.();
|
|
2098
|
-
} catch {}
|
|
2099
|
-
}, 0);
|
|
2100
|
-
throw poison;
|
|
2101
|
-
}
|
|
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");
|
|
2102
2327
|
outstanding = true;
|
|
2103
2328
|
try {
|
|
2104
2329
|
await ready();
|
|
@@ -2108,15 +2333,60 @@ function receiveChunks(port, channelName, controller, touch) {
|
|
|
2108
2333
|
value,
|
|
2109
2334
|
error: error ? deserializeError(error) : void 0
|
|
2110
2335
|
});
|
|
2336
|
+
if (done && !error) resolveOver();
|
|
2111
2337
|
} finally {
|
|
2112
2338
|
outstanding = false;
|
|
2113
2339
|
}
|
|
2114
2340
|
if (done) resolveEnded();
|
|
2115
2341
|
}, { channelName });
|
|
2342
|
+
const onChunk = (event) => {
|
|
2343
|
+
const data = event.data;
|
|
2344
|
+
if (data?.type !== CHUNK$1 || 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);
|
|
2116
2385
|
const detach = () => {
|
|
2117
2386
|
finished = true;
|
|
2118
2387
|
wake();
|
|
2119
2388
|
off();
|
|
2389
|
+
port.removeEventListener("message", onChunk);
|
|
2120
2390
|
controller.signal.removeEventListener("abort", onAbort);
|
|
2121
2391
|
resolveEnded();
|
|
2122
2392
|
};
|
|
@@ -2146,25 +2416,35 @@ function receiveChunks(port, channelName, controller, touch) {
|
|
|
2146
2416
|
return detach;
|
|
2147
2417
|
}),
|
|
2148
2418
|
stop: detach,
|
|
2149
|
-
ended
|
|
2419
|
+
ended,
|
|
2420
|
+
over
|
|
2150
2421
|
};
|
|
2151
2422
|
}
|
|
2152
2423
|
/**
|
|
2153
2424
|
* The sending half of one direction: one `callPort` per chunk, one call
|
|
2154
|
-
* 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.
|
|
2155
2429
|
*/
|
|
2156
|
-
async function sendChunks(port, channelName, output, { maxMessageSize, log }, signal, touch) {
|
|
2430
|
+
async function sendChunks(port, channelName, output, { maxMessageSize, log }, signal, touch, onSent) {
|
|
2157
2431
|
const framed = maxMessageSize ? toChunks(maxMessageSize)(output) : output;
|
|
2158
2432
|
const stream = throughAbort(framed, signal);
|
|
2159
2433
|
const producer = output;
|
|
2160
2434
|
const returnProducer = () => void Promise.resolve(producer.return?.()).catch(() => {});
|
|
2161
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 });
|
|
2162
2442
|
try {
|
|
2163
2443
|
await sendIterator(async ({ done, value, error }) => {
|
|
2164
2444
|
if (signal.aborted) return;
|
|
2165
2445
|
const chunk = {
|
|
2166
2446
|
done,
|
|
2167
|
-
value,
|
|
2447
|
+
value: done ? value : chunkBytes(value),
|
|
2168
2448
|
error: error === void 0 ? void 0 : serializeError(error)
|
|
2169
2449
|
};
|
|
2170
2450
|
log?.("[duplexOverPort] send", {
|
|
@@ -2172,48 +2452,34 @@ async function sendChunks(port, channelName, output, { maxMessageSize, log }, si
|
|
|
2172
2452
|
done,
|
|
2173
2453
|
size: value?.byteLength
|
|
2174
2454
|
});
|
|
2455
|
+
if (mayPipeline(port)) {
|
|
2456
|
+
port.postMessage({
|
|
2457
|
+
type: CHUNK$1,
|
|
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
|
+
}
|
|
2175
2466
|
await callPort(port, chunk, {
|
|
2176
2467
|
channelName,
|
|
2177
2468
|
timeout: NO_TIMEOUT,
|
|
2178
2469
|
signal
|
|
2179
2470
|
});
|
|
2180
2471
|
touch();
|
|
2472
|
+
if (done && !error) onSent();
|
|
2181
2473
|
}, stream);
|
|
2182
2474
|
} catch (err) {
|
|
2183
2475
|
if (signal.aborted) return;
|
|
2184
2476
|
throw err;
|
|
2185
2477
|
} finally {
|
|
2186
2478
|
signal.removeEventListener("abort", returnProducer);
|
|
2479
|
+
signal.removeEventListener("abort", onAborted);
|
|
2187
2480
|
}
|
|
2188
2481
|
}
|
|
2189
2482
|
/**
|
|
2190
|
-
* Runs `fn` on a later task, never a microtask: a producer looping on
|
|
2191
|
-
* `whenPortReady` must let the transport's own events run. `setImmediate`
|
|
2192
|
-
* where it exists; elsewhere a `MessageChannel`, because nested `setTimeout`s
|
|
2193
|
-
* are clamped to 4 ms in browsers.
|
|
2194
|
-
*/
|
|
2195
|
-
const setImmediateFn = globalThis.setImmediate;
|
|
2196
|
-
/** @internal */
|
|
2197
|
-
const nextTask = setImmediateFn ? (fn) => {
|
|
2198
|
-
setImmediateFn(fn);
|
|
2199
|
-
} : (() => {
|
|
2200
|
-
let channel;
|
|
2201
|
-
const queue = [];
|
|
2202
|
-
return (fn) => {
|
|
2203
|
-
if (!channel) {
|
|
2204
|
-
channel = new MessageChannel();
|
|
2205
|
-
channel.port1.onmessage = () => queue.shift()?.();
|
|
2206
|
-
}
|
|
2207
|
-
queue.push(fn);
|
|
2208
|
-
channel.port2.postMessage(0);
|
|
2209
|
-
};
|
|
2210
|
-
})();
|
|
2211
|
-
const states = /* @__PURE__ */ new WeakMap();
|
|
2212
|
-
/** @internal Registered by the multiplexer for each port with a window. */
|
|
2213
|
-
function setPortSendState(port, state) {
|
|
2214
|
-
states.set(port, state);
|
|
2215
|
-
}
|
|
2216
|
-
/**
|
|
2217
2483
|
* One virtual port.
|
|
2218
2484
|
*
|
|
2219
2485
|
* `deliver` and `markClosed` are deliberately not on `port`: the consumer holds
|
|
@@ -2291,6 +2557,21 @@ function newVirtualPort(send, requestClose) {
|
|
|
2291
2557
|
const KEEPALIVE_ID = Number.MAX_SAFE_INTEGER;
|
|
2292
2558
|
/** The longest delay a timer holds; past it `setInterval` fires at once. */
|
|
2293
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
|
+
}
|
|
2294
2575
|
/** Ceiling on concurrently open virtual ports. Bounds the id table only. */
|
|
2295
2576
|
const DEFAULT_MAX_PORTS = 1024;
|
|
2296
2577
|
/**
|
|
@@ -2354,6 +2635,8 @@ function multiplexPort(port, options) {
|
|
|
2354
2635
|
let muxClosed = false;
|
|
2355
2636
|
/** This end counts encoded bytes and reassembles frames. */
|
|
2356
2637
|
const exactCodec = codec.encode != null && codec.decode != null;
|
|
2638
|
+
/** What this end offers on a windowed port. */
|
|
2639
|
+
const caps = ownCaps(codec);
|
|
2357
2640
|
const post = (envelope, transfer) => {
|
|
2358
2641
|
if (muxClosed) return;
|
|
2359
2642
|
codecPost(envelope, transfer);
|
|
@@ -2411,13 +2694,39 @@ function multiplexPort(port, options) {
|
|
|
2411
2694
|
port.postMessage(bytes);
|
|
2412
2695
|
} catch {}
|
|
2413
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
|
+
};
|
|
2414
2723
|
const releaseWaiters = (c) => {
|
|
2415
2724
|
for (const w of c?.waiters.splice(0) ?? []) w.resolve();
|
|
2416
2725
|
};
|
|
2417
2726
|
const flush = (c) => {
|
|
2418
2727
|
while (c.queue.length) {
|
|
2419
2728
|
const next = c.queue[0];
|
|
2420
|
-
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;
|
|
2421
2730
|
c.queue.shift();
|
|
2422
2731
|
c.queued -= next.size;
|
|
2423
2732
|
c.outstanding += next.size;
|
|
@@ -2455,15 +2764,20 @@ function multiplexPort(port, options) {
|
|
|
2455
2764
|
enforced: false,
|
|
2456
2765
|
opener,
|
|
2457
2766
|
exact,
|
|
2767
|
+
caps: /* @__PURE__ */ new Set(),
|
|
2458
2768
|
outstanding: 0,
|
|
2459
2769
|
queue: [],
|
|
2460
2770
|
queued: 0,
|
|
2461
2771
|
unconsumed: 0,
|
|
2462
2772
|
waiters: [],
|
|
2463
2773
|
toGrant: 0,
|
|
2464
|
-
grantScheduled: false
|
|
2774
|
+
grantScheduled: false,
|
|
2775
|
+
ending: false,
|
|
2776
|
+
endSent: false,
|
|
2777
|
+
peerEnded: false
|
|
2465
2778
|
} : void 0;
|
|
2466
2779
|
const self = newVirtualPort((payload, transfer) => {
|
|
2780
|
+
if (c?.ending) return;
|
|
2467
2781
|
touch(id);
|
|
2468
2782
|
const envelope = {
|
|
2469
2783
|
type: "message",
|
|
@@ -2550,6 +2864,38 @@ function multiplexPort(port, options) {
|
|
|
2550
2864
|
});
|
|
2551
2865
|
})
|
|
2552
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
|
+
}));
|
|
2553
2899
|
}
|
|
2554
2900
|
const record = {
|
|
2555
2901
|
deliver: self.deliver,
|
|
@@ -2601,16 +2947,24 @@ function multiplexPort(port, options) {
|
|
|
2601
2947
|
}
|
|
2602
2948
|
const handle = attach(envelope.id, envelope.meta, envelope.window, false, envelope.exact === true && exactCodec);
|
|
2603
2949
|
const credit = open.get(envelope.id)?.credit;
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
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
|
+
}
|
|
2614
2968
|
let accepted = false;
|
|
2615
2969
|
if (onPort) try {
|
|
2616
2970
|
accepted = onPort(handle.port, envelope.meta) !== false;
|
|
@@ -2638,6 +2992,7 @@ function multiplexPort(port, options) {
|
|
|
2638
2992
|
c.exact = false;
|
|
2639
2993
|
c.outstanding = 0;
|
|
2640
2994
|
}
|
|
2995
|
+
c.caps = sharedCaps(caps, envelope.caps) ?? /* @__PURE__ */ new Set();
|
|
2641
2996
|
post({
|
|
2642
2997
|
type: "credit",
|
|
2643
2998
|
id: envelope.id,
|
|
@@ -2649,6 +3004,10 @@ function multiplexPort(port, options) {
|
|
|
2649
3004
|
flush(c);
|
|
2650
3005
|
return;
|
|
2651
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
|
+
}
|
|
2652
3011
|
if (envelope.type === "message") {
|
|
2653
3012
|
const sizeOf = (c) => (c.exact ? byteLengthOf(event.data) : void 0) ?? estimateMessageSize(envelope.payload);
|
|
2654
3013
|
if (!entry) return;
|
|
@@ -2673,16 +3032,7 @@ function multiplexPort(port, options) {
|
|
|
2673
3032
|
const charged = partial.room == null ? Math.max(0, partial.bytes + size - c.window) : 0;
|
|
2674
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;
|
|
2675
3034
|
if (overrun) {
|
|
2676
|
-
|
|
2677
|
-
dropOutgoing(envelope.id);
|
|
2678
|
-
open.delete(envelope.id);
|
|
2679
|
-
releaseWaiters(c);
|
|
2680
|
-
entry.handle.markClosed(/* @__PURE__ */ new RangeError(`webrun-rpc: a framed message passed ${overrun}`));
|
|
2681
|
-
post({
|
|
2682
|
-
type: "close",
|
|
2683
|
-
id: envelope.id,
|
|
2684
|
-
reason: "max-reassembled-bytes"
|
|
2685
|
-
});
|
|
3035
|
+
failPort(envelope.id, entry, /* @__PURE__ */ new RangeError(`webrun-rpc: a framed message passed ${overrun}`), "max-reassembled-bytes");
|
|
2686
3036
|
return;
|
|
2687
3037
|
}
|
|
2688
3038
|
partial.bytes += size;
|
|
@@ -2702,6 +3052,19 @@ function multiplexPort(port, options) {
|
|
|
2702
3052
|
entry.handle.deliver(inner.payload, whole.byteLength);
|
|
2703
3053
|
return;
|
|
2704
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
|
+
}
|
|
2705
3068
|
dropOutgoing(envelope.id);
|
|
2706
3069
|
if (!entry) return;
|
|
2707
3070
|
freePartial(entry);
|
|
@@ -2783,12 +3146,14 @@ function multiplexPort(port, options) {
|
|
|
2783
3146
|
id,
|
|
2784
3147
|
meta,
|
|
2785
3148
|
window,
|
|
2786
|
-
exact: true
|
|
3149
|
+
exact: true,
|
|
3150
|
+
caps: [...caps]
|
|
2787
3151
|
} : {
|
|
2788
3152
|
type: "open",
|
|
2789
3153
|
id,
|
|
2790
3154
|
meta,
|
|
2791
|
-
window
|
|
3155
|
+
window,
|
|
3156
|
+
caps: [...caps]
|
|
2792
3157
|
} : {
|
|
2793
3158
|
type: "open",
|
|
2794
3159
|
id,
|
|
@@ -2943,7 +3308,7 @@ function isEnvelope(value) {
|
|
|
2943
3308
|
const candidate = value;
|
|
2944
3309
|
if (typeof candidate.id !== "number") return false;
|
|
2945
3310
|
if (!Number.isInteger(candidate.id) || candidate.id < 0) return false;
|
|
2946
|
-
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";
|
|
2947
3312
|
}
|
|
2948
3313
|
/**
|
|
2949
3314
|
* For ports whose messages are structured values — a real `MessagePort`, a
|
|
@@ -2955,6 +3320,7 @@ function isEnvelope(value) {
|
|
|
2955
3320
|
* not send anything a byte codec could not also carry.
|
|
2956
3321
|
*/
|
|
2957
3322
|
const structuredCodec = {
|
|
3323
|
+
halfClose: true,
|
|
2958
3324
|
post(port, envelope, transfer) {
|
|
2959
3325
|
if (transfer && transfer.length > 0) port.postMessage(envelope, transfer);
|
|
2960
3326
|
else port.postMessage(envelope);
|
|
@@ -2992,10 +3358,15 @@ function asyncDisposable(target, dispose) {
|
|
|
2992
3358
|
* own `handleStreams` / `sendStream` (no backpressure); version 2 is
|
|
2993
3359
|
* webrun-rpc's `connect` / `serve` over `overPipe` with webrun-http-streams'
|
|
2994
3360
|
* `fetchOverDuplex` / `serveFetchOverDuplex`: one port per link, calls
|
|
2995
|
-
* multiplexed on it, each call a stream with
|
|
2996
|
-
* 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.
|
|
2997
3366
|
*/
|
|
2998
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;
|
|
2999
3370
|
/**
|
|
3000
3371
|
* Why a request failed when its link went away under it: the far side (the
|
|
3001
3372
|
* handler, or the ServiceWorker) was lost. The request may or may not have
|
|
@@ -3014,12 +3385,13 @@ var LinkLostError = class extends Error {
|
|
|
3014
3385
|
*/
|
|
3015
3386
|
const LINK_CLOSE = "webrun-http:link-close";
|
|
3016
3387
|
/** Opens the calling end of a link on `port` (one port per link). */
|
|
3017
|
-
async function connectHttpLink(port, { streamTimeout, maxPorts } = {}) {
|
|
3388
|
+
async function connectHttpLink(port, { streamTimeout, maxPorts, window = DEFAULT_HTTP_LINK_WINDOW } = {}) {
|
|
3018
3389
|
const { call, close } = await connect({
|
|
3019
3390
|
mux: overPipe(port, {
|
|
3020
3391
|
codec: structuredCodec,
|
|
3021
3392
|
side: "initiator",
|
|
3022
|
-
maxPorts
|
|
3393
|
+
maxPorts,
|
|
3394
|
+
window
|
|
3023
3395
|
}),
|
|
3024
3396
|
timeout: streamTimeout
|
|
3025
3397
|
});
|
|
@@ -3306,20 +3678,24 @@ function postFrame(port, frame) {
|
|
|
3306
3678
|
function messageSize(data) {
|
|
3307
3679
|
return typeof data === "string" ? utf8Length(data) : data.byteLength;
|
|
3308
3680
|
}
|
|
3309
|
-
|
|
3681
|
+
const encoder = new TextEncoder();
|
|
3682
|
+
/** Characters encoded per `encodeInto` call: at most 3 bytes each, so the scratch stays at 96 KiB. */
|
|
3683
|
+
const CHUNK = 32768;
|
|
3684
|
+
const scratch = new Uint8Array(CHUNK * 3);
|
|
3685
|
+
/**
|
|
3686
|
+
* The UTF-8 length of `text`, as `TextEncoder` would encode it (a lone surrogate counts as the 3
|
|
3687
|
+
* bytes of U+FFFD). Every message is measured on both ends (`bufferedAmount` and the byte acks), so
|
|
3688
|
+
* this runs natively: `encodeInto` a reused buffer, a chunk at a time, never splitting a surrogate
|
|
3689
|
+
* pair across two chunks. A loop over the characters in JavaScript took about 30 times as long.
|
|
3690
|
+
*/
|
|
3310
3691
|
function utf8Length(text) {
|
|
3311
3692
|
let length = 0;
|
|
3312
|
-
for (let
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
if (next >= 56320 && next <= 57343) {
|
|
3319
|
-
length += 4;
|
|
3320
|
-
i++;
|
|
3321
|
-
} else length += 3;
|
|
3322
|
-
} else length += 3;
|
|
3693
|
+
for (let start = 0; start < text.length;) {
|
|
3694
|
+
let end = Math.min(start + CHUNK, text.length);
|
|
3695
|
+
const last = text.charCodeAt(end - 1);
|
|
3696
|
+
if (end < text.length && last >= 55296 && last <= 56319) end--;
|
|
3697
|
+
length += encoder.encodeInto(text.slice(start, end), scratch).written;
|
|
3698
|
+
start = end;
|
|
3323
3699
|
}
|
|
3324
3700
|
return length;
|
|
3325
3701
|
}
|
|
@@ -3475,6 +3851,7 @@ var PortSocketCore = class extends EventTarget {
|
|
|
3475
3851
|
code,
|
|
3476
3852
|
reason
|
|
3477
3853
|
});
|
|
3854
|
+
if (!Number.isFinite(this._closeTimeoutMs)) return;
|
|
3478
3855
|
this._closeTimer = this._timers.setTimeout(() => this._finish(1006, "", false), this._closeTimeoutMs);
|
|
3479
3856
|
}
|
|
3480
3857
|
_onCloseFrame(code, reason) {
|
|
@@ -3840,7 +4217,8 @@ var PortRendezvous = class {
|
|
|
3840
4217
|
* `whenDrained(limit)` waits for it to fall: the backpressure a producer
|
|
3841
4218
|
* applies.
|
|
3842
4219
|
* - When the client's pings stop for three heartbeat periods (a closed tab, a
|
|
3843
|
-
* terminated worker), the socket closes with 1006.
|
|
4220
|
+
* terminated worker), the socket closes with 1006. With `watch: "first-ping"`
|
|
4221
|
+
* the watch starts only once the client pings.
|
|
3844
4222
|
*/
|
|
3845
4223
|
var ServerWebSocket = class extends PortSocketCore {
|
|
3846
4224
|
static CONNECTING = 0;
|
|
@@ -3849,55 +4227,150 @@ var ServerWebSocket = class extends PortSocketCore {
|
|
|
3849
4227
|
static CLOSED = 3;
|
|
3850
4228
|
url;
|
|
3851
4229
|
protocol;
|
|
4230
|
+
_binaryType = "arraybuffer";
|
|
4231
|
+
_options;
|
|
3852
4232
|
_accepted = false;
|
|
4233
|
+
_detached = false;
|
|
3853
4234
|
_queue = [];
|
|
3854
4235
|
_heartbeatMs = DEFAULT_HEARTBEAT_MS;
|
|
3855
4236
|
_lastPing = Date.now();
|
|
4237
|
+
/** Whether a ping (with a period a timer takes) arrived already. */
|
|
4238
|
+
_pinged = false;
|
|
3856
4239
|
_watch;
|
|
3857
4240
|
constructor(port, options = {}) {
|
|
3858
4241
|
super({
|
|
3859
4242
|
timers: options.timers,
|
|
3860
4243
|
closeTimeoutMs: options.closeTimeoutMs
|
|
3861
4244
|
});
|
|
4245
|
+
this._options = options;
|
|
3862
4246
|
this.url = options.url ?? "";
|
|
3863
4247
|
this.protocol = options.protocol ?? "";
|
|
3864
4248
|
this._attach(port);
|
|
3865
4249
|
this._state = 1;
|
|
3866
|
-
this._watchHeartbeat();
|
|
4250
|
+
if (options.watch !== "first-ping") this._watchHeartbeat();
|
|
3867
4251
|
}
|
|
3868
4252
|
/** Whether `accept()` was called. */
|
|
3869
4253
|
get accepted() {
|
|
3870
4254
|
return this._accepted;
|
|
3871
4255
|
}
|
|
4256
|
+
/** How binary messages arrive: `ArrayBuffer`s (the default) or `Blob`s. Other values are ignored. */
|
|
4257
|
+
get binaryType() {
|
|
4258
|
+
return this._binaryType;
|
|
4259
|
+
}
|
|
4260
|
+
set binaryType(value) {
|
|
4261
|
+
if (value === "blob" || value === "arraybuffer") this._binaryType = value;
|
|
4262
|
+
}
|
|
4263
|
+
/**
|
|
4264
|
+
* Hands this end's transport to another holder before it was accepted: the port to give the
|
|
4265
|
+
* client (a pair's end returned in a 101, say), with the frames that reached this end first
|
|
4266
|
+
* delivered ahead of the rest. This end is closed then, with no event, and answers nothing.
|
|
4267
|
+
*/
|
|
4268
|
+
detach() {
|
|
4269
|
+
const port = this._port;
|
|
4270
|
+
if (!port || this._accepted || this._state !== 1) throw new DOMException("Only an open end that was not accepted can be detached.", "InvalidStateError");
|
|
4271
|
+
this._detached = true;
|
|
4272
|
+
this._port = void 0;
|
|
4273
|
+
port.onmessage = null;
|
|
4274
|
+
let owed = this.bufferedAmount;
|
|
4275
|
+
this._state = 3;
|
|
4276
|
+
this._onClosed();
|
|
4277
|
+
this._wakeDrainWaiters();
|
|
4278
|
+
const queued = this._queue.splice(0);
|
|
4279
|
+
if (queued.length === 0 && owed === 0) return port;
|
|
4280
|
+
const relay = new MessageChannel();
|
|
4281
|
+
const holder = relay.port2;
|
|
4282
|
+
const closing = /* @__PURE__ */ new Set();
|
|
4283
|
+
const end = () => {
|
|
4284
|
+
port.onmessage = null;
|
|
4285
|
+
holder.onmessage = null;
|
|
4286
|
+
port.close();
|
|
4287
|
+
holder.close();
|
|
4288
|
+
};
|
|
4289
|
+
const sawClose = (way) => {
|
|
4290
|
+
closing.add(way);
|
|
4291
|
+
if (closing.size === 2) end();
|
|
4292
|
+
};
|
|
4293
|
+
for (const frame of queued) {
|
|
4294
|
+
postFrame(holder, frame);
|
|
4295
|
+
if (frame.t === "c") closing.add("in");
|
|
4296
|
+
}
|
|
4297
|
+
holder.onmessage = (event) => {
|
|
4298
|
+
port.postMessage(event.data, transferables(event.data));
|
|
4299
|
+
if (event.data?.t === "c") sawClose("out");
|
|
4300
|
+
};
|
|
4301
|
+
port.onmessage = (event) => {
|
|
4302
|
+
const data = event.data;
|
|
4303
|
+
if (data?.t === "a" && typeof data.n === "number" && owed > 0) {
|
|
4304
|
+
const absorbed = Math.min(owed, data.n);
|
|
4305
|
+
owed -= absorbed;
|
|
4306
|
+
if (data.n > absorbed) holder.postMessage({
|
|
4307
|
+
t: "a",
|
|
4308
|
+
n: data.n - absorbed
|
|
4309
|
+
});
|
|
4310
|
+
return;
|
|
4311
|
+
}
|
|
4312
|
+
holder.postMessage(event.data, transferables(event.data));
|
|
4313
|
+
if (data?.t === "c") sawClose("in");
|
|
4314
|
+
};
|
|
4315
|
+
port.addEventListener?.("close", end);
|
|
4316
|
+
holder.addEventListener?.("close", end);
|
|
4317
|
+
return relay.port1;
|
|
4318
|
+
}
|
|
3872
4319
|
/** Starts delivering events, the queued ones first. */
|
|
3873
4320
|
accept() {
|
|
4321
|
+
if (this._detached) throw new DOMException("This end was detached; it cannot be accepted.", "InvalidStateError");
|
|
3874
4322
|
if (this._accepted) return;
|
|
3875
4323
|
this._accepted = true;
|
|
3876
|
-
for (const frame of this._queue.splice(0))
|
|
4324
|
+
for (const frame of this._queue.splice(0)) this._deliver(frame);
|
|
3877
4325
|
}
|
|
3878
4326
|
send(data) {
|
|
3879
|
-
if (
|
|
4327
|
+
if (this._detached) throw new DOMException("This end was detached; it cannot send.", "InvalidStateError");
|
|
4328
|
+
if (!this._accepted && !this._options.sendBeforeAccept) throw new DOMException("accept() the socket before sending on it.", "InvalidStateError");
|
|
3880
4329
|
this._sendData(isBinary(data) ? toArrayBuffer(data) : String(data));
|
|
3881
4330
|
}
|
|
3882
|
-
close(code
|
|
3883
|
-
|
|
4331
|
+
close(code, reason = "") {
|
|
4332
|
+
this.checkClose(code, reason);
|
|
4333
|
+
this._startClose(code ?? this._options.defaultCloseCode ?? 1e3, reason);
|
|
4334
|
+
}
|
|
4335
|
+
/**
|
|
4336
|
+
* Refuses a `close()` the endpoint may not make: by default a code outside RFC 6455 §7.4's
|
|
4337
|
+
* sendable ones, or a reason over 123 bytes. A subclass with another platform's rules
|
|
4338
|
+
* overrides it.
|
|
4339
|
+
*/
|
|
4340
|
+
checkClose(code, reason) {
|
|
4341
|
+
if (code !== void 0 && !isSendableCloseCode(code)) throw new DOMException(`${code} is not a close code an endpoint may send.`, "InvalidAccessError");
|
|
3884
4342
|
if (utf8Length(reason) > 123) throw new DOMException("The close reason is longer than 123 bytes.", "SyntaxError");
|
|
3885
|
-
this._startClose(code, reason);
|
|
3886
4343
|
}
|
|
3887
4344
|
_deliver(frame) {
|
|
3888
4345
|
if (!this._accepted) {
|
|
3889
4346
|
this._queue.push(frame);
|
|
3890
4347
|
return;
|
|
3891
4348
|
}
|
|
4349
|
+
if (frame.t === "m" && this._state === 2 && this._options.deliverWhileClosing) {
|
|
4350
|
+
this._dispatchMessage(frame.d);
|
|
4351
|
+
this._consumed(messageSize(frame.d));
|
|
4352
|
+
return;
|
|
4353
|
+
}
|
|
3892
4354
|
super._deliver(frame);
|
|
3893
4355
|
}
|
|
3894
4356
|
_dispatchMessage(data) {
|
|
3895
|
-
this.
|
|
4357
|
+
const delivered = typeof data !== "string" && this._binaryType === "blob" ? new Blob([data]) : data;
|
|
4358
|
+
this.dispatchEvent(new MessageEvent("message", { data: delivered }));
|
|
3896
4359
|
}
|
|
3897
4360
|
_onPing(hb) {
|
|
3898
4361
|
this._post({ t: "q" });
|
|
3899
4362
|
this._lastPing = Date.now();
|
|
3900
|
-
if (
|
|
4363
|
+
if (this._state === 3) return;
|
|
4364
|
+
const keep = this._options.heartbeatPeriod === "first" && this._pinged;
|
|
4365
|
+
if (!(hb > 0 && hb < 2 ** 31 - 1)) {
|
|
4366
|
+
if (!keep && this._watch !== void 0) {
|
|
4367
|
+
this._timers.clearInterval(this._watch);
|
|
4368
|
+
this._watch = void 0;
|
|
4369
|
+
}
|
|
4370
|
+
return;
|
|
4371
|
+
}
|
|
4372
|
+
this._pinged = true;
|
|
4373
|
+
if (!keep && (this._watch === void 0 || hb !== this._heartbeatMs)) {
|
|
3901
4374
|
this._heartbeatMs = hb;
|
|
3902
4375
|
this._watchHeartbeat();
|
|
3903
4376
|
}
|
|
@@ -3919,6 +4392,11 @@ function isSendableCloseCode(code) {
|
|
|
3919
4392
|
if (code >= 3e3 && code <= 4999) return true;
|
|
3920
4393
|
return code >= 1e3 && code <= 1014 && code !== 1004 && code !== 1005 && code !== 1006;
|
|
3921
4394
|
}
|
|
4395
|
+
/** The buffers a relayed frame carries, to transfer rather than copy. */
|
|
4396
|
+
function transferables(data) {
|
|
4397
|
+
const d = data?.d;
|
|
4398
|
+
return d instanceof ArrayBuffer ? [d] : [];
|
|
4399
|
+
}
|
|
3922
4400
|
//#endregion
|
|
3923
4401
|
//#region src/ws/upgrade.ts
|
|
3924
4402
|
/**
|
|
@@ -3950,6 +4428,7 @@ function newWebSocketUpgradeRequest(url, { protocols = [], headers } = {}) {
|
|
|
3950
4428
|
all.set("connection", "Upgrade");
|
|
3951
4429
|
all.set("sec-websocket-version", "13");
|
|
3952
4430
|
if (!all.has("sec-websocket-key")) all.set("sec-websocket-key", randomKey());
|
|
4431
|
+
all.delete("sec-websocket-protocol");
|
|
3953
4432
|
if (protocols.length > 0) all.set("sec-websocket-protocol", protocols.join(", "));
|
|
3954
4433
|
if (!all.has("x-webrun-ws-version")) all.set(WS_VERSION_HEADER, `2`);
|
|
3955
4434
|
return withHeaders(new Request(httpUrl, {
|
|
@@ -4009,6 +4488,23 @@ function upgradeWebSocket(request, options = {}) {
|
|
|
4009
4488
|
url: request.url,
|
|
4010
4489
|
protocol
|
|
4011
4490
|
});
|
|
4491
|
+
const response = webSocketUpgradeResponse(channel.port2, protocol);
|
|
4492
|
+
Object.defineProperty(response, "webSocket", {
|
|
4493
|
+
value: socket,
|
|
4494
|
+
configurable: true
|
|
4495
|
+
});
|
|
4496
|
+
return {
|
|
4497
|
+
socket,
|
|
4498
|
+
response
|
|
4499
|
+
};
|
|
4500
|
+
}
|
|
4501
|
+
/**
|
|
4502
|
+
* A 101 for a socket whose server end the handler runs itself, on the other end of `port`
|
|
4503
|
+
* (a server of its own over this package's frames, say): what `upgradeWebSocket` returns, minus
|
|
4504
|
+
* the `ServerWebSocket`. The host hands `port` to the caller as it would for `upgradeWebSocket`
|
|
4505
|
+
* (`takePort`), and `protocol` (`""` for none) is the subprotocol picked.
|
|
4506
|
+
*/
|
|
4507
|
+
function webSocketUpgradeResponse(port, protocol = "") {
|
|
4012
4508
|
const response = new Response(null, {
|
|
4013
4509
|
status: 200,
|
|
4014
4510
|
headers: protocol ? { "sec-websocket-protocol": protocol } : {}
|
|
@@ -4022,23 +4518,16 @@ function upgradeWebSocket(request, options = {}) {
|
|
|
4022
4518
|
value: "Switching Protocols",
|
|
4023
4519
|
configurable: true
|
|
4024
4520
|
},
|
|
4025
|
-
webSocket: {
|
|
4026
|
-
value: socket,
|
|
4027
|
-
configurable: true
|
|
4028
|
-
},
|
|
4029
4521
|
[UPGRADE]: {
|
|
4030
4522
|
value: {
|
|
4031
|
-
port
|
|
4523
|
+
port,
|
|
4032
4524
|
protocol
|
|
4033
4525
|
},
|
|
4034
4526
|
configurable: true,
|
|
4035
4527
|
writable: true
|
|
4036
4528
|
}
|
|
4037
4529
|
});
|
|
4038
|
-
return
|
|
4039
|
-
socket,
|
|
4040
|
-
response
|
|
4041
|
-
};
|
|
4530
|
+
return response;
|
|
4042
4531
|
}
|
|
4043
4532
|
/**
|
|
4044
4533
|
* The port to hand to the caller, for a response from `upgradeWebSocket`.
|
|
@@ -4105,6 +4594,15 @@ function randomKey() {
|
|
|
4105
4594
|
crypto.getRandomValues(bytes);
|
|
4106
4595
|
return btoa(String.fromCharCode(...bytes));
|
|
4107
4596
|
}
|
|
4597
|
+
/** `headers` as pairs a `postMessage` can carry (a `Headers` object cannot be cloned). */
|
|
4598
|
+
function headerPairs(headers) {
|
|
4599
|
+
if (headers === void 0) return void 0;
|
|
4600
|
+
const pairs = [];
|
|
4601
|
+
new Headers(headers).forEach((value, name) => {
|
|
4602
|
+
pairs.push([name, value]);
|
|
4603
|
+
});
|
|
4604
|
+
return pairs;
|
|
4605
|
+
}
|
|
4108
4606
|
//#endregion
|
|
4109
4607
|
//#region src/sw/handler-endpoint.ts
|
|
4110
4608
|
/** The message calls through a lost ServiceWorker fail with. */
|
|
@@ -4189,8 +4687,11 @@ var HandlerEndpoint = class {
|
|
|
4189
4687
|
this._startKeepAlive();
|
|
4190
4688
|
return await link.up.send(request);
|
|
4191
4689
|
}
|
|
4192
|
-
/**
|
|
4193
|
-
|
|
4690
|
+
/**
|
|
4691
|
+
* Opens a socket to the handler `url` routes to, through the ServiceWorker. The handler sees
|
|
4692
|
+
* `headers` on the upgrade request, and so does a `route` function choosing it.
|
|
4693
|
+
*/
|
|
4694
|
+
async connectWebSocket(url, protocols = [], headers) {
|
|
4194
4695
|
const link = await this._linkWith("ctl");
|
|
4195
4696
|
const id = this._sockets.newId();
|
|
4196
4697
|
const answer = this._sockets.wait(id, this.options.wsConnectTimeoutMs ?? 15e3, `the socket request for ${url}`);
|
|
@@ -4198,7 +4699,8 @@ var HandlerEndpoint = class {
|
|
|
4198
4699
|
t: "ws-open",
|
|
4199
4700
|
id,
|
|
4200
4701
|
url,
|
|
4201
|
-
protocols
|
|
4702
|
+
protocols,
|
|
4703
|
+
headers: headerPairs(headers)
|
|
4202
4704
|
});
|
|
4203
4705
|
return await answer;
|
|
4204
4706
|
}
|
|
@@ -4368,11 +4870,11 @@ function serveHttpInWorker(handler, options = {}) {
|
|
|
4368
4870
|
};
|
|
4369
4871
|
scope.addEventListener("message", onMessage);
|
|
4370
4872
|
const baseUrl = () => globalThis.location?.href;
|
|
4371
|
-
const connectWebSocket = (url, protocols = []) => {
|
|
4873
|
+
const connectWebSocket = (url, protocols = [], headers) => {
|
|
4372
4874
|
const httpUrl = new URL(`${url}`, baseUrl());
|
|
4373
4875
|
if (httpUrl.protocol === "ws:") httpUrl.protocol = "http:";
|
|
4374
4876
|
else if (httpUrl.protocol === "wss:") httpUrl.protocol = "https:";
|
|
4375
|
-
return endpoint.connectWebSocket(`${httpUrl}`, protocols);
|
|
4877
|
+
return endpoint.connectWebSocket(`${httpUrl}`, protocols, headers);
|
|
4376
4878
|
};
|
|
4377
4879
|
let WebSocketClass;
|
|
4378
4880
|
return {
|
|
@@ -4399,4 +4901,4 @@ function serveHttpInWorker(handler, options = {}) {
|
|
|
4399
4901
|
};
|
|
4400
4902
|
}
|
|
4401
4903
|
//#endregion
|
|
4402
|
-
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 };
|
|
4904
|
+
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 };
|