@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/sw.js
CHANGED
|
@@ -706,7 +706,7 @@ const LAST_CHUNK = /* @__PURE__ */ new Uint8Array([
|
|
|
706
706
|
13,
|
|
707
707
|
10
|
|
708
708
|
]);
|
|
709
|
-
const encoder = /* @__PURE__ */ new TextEncoder();
|
|
709
|
+
const encoder$2 = /* @__PURE__ */ new TextEncoder();
|
|
710
710
|
/**
|
|
711
711
|
* Chunk sizes are written in HEXADECIMAL — a 21-byte chunk is `15`. Writing
|
|
712
712
|
* them in decimal is the defect note 15 found in @libp2p/http, and it is the
|
|
@@ -719,7 +719,7 @@ const encoder = /* @__PURE__ */ new TextEncoder();
|
|
|
719
719
|
async function* encodeChunked(body) {
|
|
720
720
|
for await (const chunk of body) {
|
|
721
721
|
if (chunk.byteLength === 0) continue;
|
|
722
|
-
yield encoder.encode(`${chunk.byteLength.toString(16)}\r\n`);
|
|
722
|
+
yield encoder$2.encode(`${chunk.byteLength.toString(16)}\r\n`);
|
|
723
723
|
yield chunk;
|
|
724
724
|
yield CRLF;
|
|
725
725
|
}
|
|
@@ -1920,6 +1920,99 @@ async function* throughAbort(input, signal) {
|
|
|
1920
1920
|
}
|
|
1921
1921
|
}
|
|
1922
1922
|
/**
|
|
1923
|
+
* Runs `fn` on a later task, never a microtask: a producer looping on
|
|
1924
|
+
* `whenPortReady` must let the transport's own events run. `setImmediate`
|
|
1925
|
+
* where it exists; elsewhere a `MessageChannel`, because nested `setTimeout`s
|
|
1926
|
+
* are clamped to 4 ms in browsers.
|
|
1927
|
+
*/
|
|
1928
|
+
const setImmediateFn = globalThis.setImmediate;
|
|
1929
|
+
/** @internal */
|
|
1930
|
+
const nextTask = setImmediateFn ? (fn) => {
|
|
1931
|
+
setImmediateFn(fn);
|
|
1932
|
+
} : (() => {
|
|
1933
|
+
let channel;
|
|
1934
|
+
const queue = [];
|
|
1935
|
+
return (fn) => {
|
|
1936
|
+
if (!channel) {
|
|
1937
|
+
channel = new MessageChannel();
|
|
1938
|
+
channel.port1.onmessage = () => queue.shift()?.();
|
|
1939
|
+
}
|
|
1940
|
+
queue.push(fn);
|
|
1941
|
+
channel.port2.postMessage(0);
|
|
1942
|
+
};
|
|
1943
|
+
})();
|
|
1944
|
+
const states = /* @__PURE__ */ new WeakMap();
|
|
1945
|
+
/** @internal Registered by the multiplexer for each port with a window. */
|
|
1946
|
+
function setPortSendState(port, state) {
|
|
1947
|
+
states.set(port, state);
|
|
1948
|
+
}
|
|
1949
|
+
/**
|
|
1950
|
+
* Bytes queued on `port` because its window is spent (0 for a port without
|
|
1951
|
+
* flow control). A sender that should not queue without bound waits for
|
|
1952
|
+
* `whenPortReady` before posting more.
|
|
1953
|
+
*/
|
|
1954
|
+
function portBufferedAmount(port) {
|
|
1955
|
+
return states.get(port)?.bufferedAmount() ?? 0;
|
|
1956
|
+
}
|
|
1957
|
+
/**
|
|
1958
|
+
* Resolves once at most `below` bytes are queued on `port`, always on a later
|
|
1959
|
+
* task (for a port without flow control, on the next one).
|
|
1960
|
+
*/
|
|
1961
|
+
function whenPortReady(port, below = 0) {
|
|
1962
|
+
return states.get(port)?.ready(below) ?? new Promise((resolve) => nextTask(resolve));
|
|
1963
|
+
}
|
|
1964
|
+
const agreements = /* @__PURE__ */ new WeakMap();
|
|
1965
|
+
/** @internal Registered by the multiplexer for each port with a window. */
|
|
1966
|
+
function setPortAgreement(port, agreement) {
|
|
1967
|
+
agreements.set(port, agreement);
|
|
1968
|
+
}
|
|
1969
|
+
/**
|
|
1970
|
+
* @internal What has been agreed with the peer on `port` so far; `undefined`
|
|
1971
|
+
* on a port without a window. Not exported from the package.
|
|
1972
|
+
*/
|
|
1973
|
+
function portAgreement(port) {
|
|
1974
|
+
return agreements.get(port)?.();
|
|
1975
|
+
}
|
|
1976
|
+
const ends = /* @__PURE__ */ new WeakMap();
|
|
1977
|
+
/** @internal Registered by the multiplexer for each port with a window. */
|
|
1978
|
+
function setPortEndState(port, state) {
|
|
1979
|
+
ends.set(port, state);
|
|
1980
|
+
}
|
|
1981
|
+
/**
|
|
1982
|
+
* @internal End this side's direction of `port`: an `end` is queued behind
|
|
1983
|
+
* everything already posted on it, and anything posted afterwards is dropped.
|
|
1984
|
+
* `false`, and nothing changes, when `end` was not agreed (no window, the
|
|
1985
|
+
* handshake not answered yet, or a peer that does not speak it): the caller
|
|
1986
|
+
* then closes the port. Never waits on the peer. Not exported from the package.
|
|
1987
|
+
*/
|
|
1988
|
+
function endPort(port) {
|
|
1989
|
+
return ends.get(port)?.end() ?? false;
|
|
1990
|
+
}
|
|
1991
|
+
/**
|
|
1992
|
+
* @internal Resolves once the peer has ended its direction of `port`. It never
|
|
1993
|
+
* settles otherwise: not on a port where `end` is not agreed when it is asked
|
|
1994
|
+
* (no window, the handshake not answered yet, a peer or codec without it), and
|
|
1995
|
+
* not when the port closes first, so a caller races it against the port's
|
|
1996
|
+
* close signal. Not exported from the package.
|
|
1997
|
+
*/
|
|
1998
|
+
function whenPortEnded(port) {
|
|
1999
|
+
let waiting = peerEnds.get(port);
|
|
2000
|
+
if (!waiting) {
|
|
2001
|
+
let resolve = () => {};
|
|
2002
|
+
const promise = new Promise((r) => {
|
|
2003
|
+
resolve = r;
|
|
2004
|
+
});
|
|
2005
|
+
if (!ends.get(port)?.watchPeerEnd(resolve)) return promise;
|
|
2006
|
+
waiting = {
|
|
2007
|
+
promise,
|
|
2008
|
+
resolve
|
|
2009
|
+
};
|
|
2010
|
+
peerEnds.set(port, waiting);
|
|
2011
|
+
}
|
|
2012
|
+
return waiting.promise;
|
|
2013
|
+
}
|
|
2014
|
+
const peerEnds = /* @__PURE__ */ new WeakMap();
|
|
2015
|
+
/**
|
|
1923
2016
|
* Makes `target` disposable with `await using`: installs `dispose` as its
|
|
1924
2017
|
* `[Symbol.asyncDispose]`, where the runtime defines the symbol. Hand-written
|
|
1925
2018
|
* iterators lose what a native generator gets from `AsyncIteratorPrototype`;
|
|
@@ -1991,6 +2084,61 @@ function returningAtOnce(gen, abandon) {
|
|
|
1991
2084
|
* tests and adapters assert on it.
|
|
1992
2085
|
*/
|
|
1993
2086
|
const STREAM_ABORT = "webrun-rpc:stream-abort";
|
|
2087
|
+
/**
|
|
2088
|
+
* The `type` of a pipelined chunk: a plain port message, not a `callPort`
|
|
2089
|
+
* request, sent only on a port where both ends agreed `pipeline`. The window's
|
|
2090
|
+
* credit is its confirmation.
|
|
2091
|
+
*/
|
|
2092
|
+
const CHUNK$1 = "webrun-rpc:chunk";
|
|
2093
|
+
/**
|
|
2094
|
+
* What a pipelined chunk counts beyond its bytes, against the window, on the
|
|
2095
|
+
* receiving side. Both codecs' windows count more for the chunk's envelope
|
|
2096
|
+
* (the estimate at least 64, msgpack's keys alone more than this), so an honest
|
|
2097
|
+
* sender never makes the receiver count past the window; a sender of empty
|
|
2098
|
+
* chunks still cannot have it hold an unbounded number of them.
|
|
2099
|
+
*/
|
|
2100
|
+
const CHUNK_OVERHEAD = 32;
|
|
2101
|
+
/**
|
|
2102
|
+
* The bytes of `view` as a plain `Uint8Array` that owns its whole buffer:
|
|
2103
|
+
* `view` itself when it already is one, else a copy. Structured clone carries
|
|
2104
|
+
* and keeps a view's whole `ArrayBuffer`, and a byte codec may decode into
|
|
2105
|
+
* views of the frame, while the window and the receiver's count see only the
|
|
2106
|
+
* view: a 4 KiB `subarray` of a 4 MiB buffer would cost 4 MiB per chunk.
|
|
2107
|
+
* Copied with the `Uint8Array` constructor, not `slice()`: a Node `Buffer`'s
|
|
2108
|
+
* `slice()` is another view on the same memory.
|
|
2109
|
+
*/
|
|
2110
|
+
function ownBytes(view) {
|
|
2111
|
+
if (view instanceof Uint8Array && view.constructor === Uint8Array && view.byteOffset === 0 && view.byteLength === view.buffer.byteLength) return view;
|
|
2112
|
+
return new Uint8Array(new Uint8Array(view.buffer, view.byteOffset, view.byteLength));
|
|
2113
|
+
}
|
|
2114
|
+
/**
|
|
2115
|
+
* A producer's chunk as bytes this end may send, in either mode: any view or
|
|
2116
|
+
* `ArrayBuffer`, normalised by `ownBytes`. Anything else fails the stream here,
|
|
2117
|
+
* as the producer's own error, rather than reaching a peer that would refuse it.
|
|
2118
|
+
*/
|
|
2119
|
+
/**
|
|
2120
|
+
* A received confirmed chunk's value, as its own bytes, put back in place of
|
|
2121
|
+
* the original in `chunk`: `listenPort` keeps the request until the handler
|
|
2122
|
+
* returns, and the original would keep its whole buffer that long. A function
|
|
2123
|
+
* of its own, so that no temporary of the suspended handler holds the
|
|
2124
|
+
* original either.
|
|
2125
|
+
*/
|
|
2126
|
+
function adoptValue(chunk) {
|
|
2127
|
+
const original = chunk.value;
|
|
2128
|
+
if (ArrayBuffer.isView(original)) chunk.value = ownBytes(original);
|
|
2129
|
+
return chunk.value;
|
|
2130
|
+
}
|
|
2131
|
+
function chunkBytes(value) {
|
|
2132
|
+
if (ArrayBuffer.isView(value)) return ownBytes(value);
|
|
2133
|
+
if (value instanceof ArrayBuffer || Object.prototype.toString.call(value) === "[object ArrayBuffer]") return new Uint8Array(value);
|
|
2134
|
+
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}`);
|
|
2135
|
+
}
|
|
2136
|
+
const MIXED_MESSAGE = "webrun-rpc: peer mixed confirmed and pipelined chunks; the stream port is closed";
|
|
2137
|
+
/** This end may pipeline its own chunks: agreed, and the window holds them. */
|
|
2138
|
+
const mayPipeline = (port) => {
|
|
2139
|
+
const agreement = portAgreement(port);
|
|
2140
|
+
return !!agreement?.enforced && agreement.caps.has("pipeline");
|
|
2141
|
+
};
|
|
1994
2142
|
/** Caller's input travels on this channel; the handler listens on it. */
|
|
1995
2143
|
const CHANNEL_IN = "in";
|
|
1996
2144
|
/** The handler's output travels on this channel; the caller listens on it. */
|
|
@@ -2003,7 +2151,9 @@ const CHANNEL_OUT = "out";
|
|
|
2003
2151
|
* same way on the other channel. Within each direction the next chunk is never
|
|
2004
2152
|
* sent until the previous one has been delivered *and* pulled past by the
|
|
2005
2153
|
* consumer (spec D11) — the reply to a chunk call *is* the confirmation, and
|
|
2006
|
-
* `listenPort` withholds it until then.
|
|
2154
|
+
* `listenPort` withholds it until then. On a port whose window both ends
|
|
2155
|
+
* speak and that agreed `pipeline`, chunks go as plain messages instead, and
|
|
2156
|
+
* the window's credit is the confirmation: up to a window is in flight.
|
|
2007
2157
|
*
|
|
2008
2158
|
* A stream port carries exactly one invocation. To make several calls, open
|
|
2009
2159
|
* several ports: `mux.openPort({ kind: "stream" })` per call.
|
|
@@ -2020,22 +2170,49 @@ function serveDuplexOverPort(port, handler, options = {}) {
|
|
|
2020
2170
|
const notice = installAbortNotice(port, controller);
|
|
2021
2171
|
abortOnPortClose(port, controller);
|
|
2022
2172
|
const clock = installStreamTimeout(controller, options.timeout);
|
|
2023
|
-
const inbound = receiveChunks(port, CHANNEL_IN, controller, clock.touch);
|
|
2173
|
+
const inbound = receiveChunks(port, CHANNEL_IN, controller, clock.touch, notice);
|
|
2024
2174
|
let output;
|
|
2025
2175
|
try {
|
|
2026
2176
|
output = handler(inbound.stream);
|
|
2027
2177
|
} catch (err) {
|
|
2028
|
-
const pump = sendChunks(port, CHANNEL_OUT, failing(err), options, controller.signal, clock.touch);
|
|
2178
|
+
const pump = sendChunks(port, CHANNEL_OUT, failing(err), options, controller.signal, clock.touch, () => {});
|
|
2029
2179
|
pump.catch(() => {});
|
|
2030
2180
|
disarmClockWhenBothSidesSettle(clock, pump, inbound.ended);
|
|
2031
2181
|
return teardownOnce(controller, notice, clock, inbound, void 0);
|
|
2032
2182
|
}
|
|
2033
|
-
|
|
2183
|
+
let onSent = () => {};
|
|
2184
|
+
const sent = new Promise((resolve) => {
|
|
2185
|
+
onSent = resolve;
|
|
2186
|
+
});
|
|
2187
|
+
const pump = sendChunks(port, CHANNEL_OUT, output, options, controller.signal, clock.touch, onSent);
|
|
2034
2188
|
pump.catch(() => {});
|
|
2035
2189
|
disarmClockWhenBothSidesSettle(clock, pump, inbound.ended);
|
|
2190
|
+
endServedStream(port, sent, inbound.over);
|
|
2036
2191
|
return teardownOnce(controller, notice, clock, inbound, output);
|
|
2037
2192
|
}
|
|
2038
2193
|
/**
|
|
2194
|
+
* The serving side's normal finish. Once the handler's last chunk has gone
|
|
2195
|
+
* without an error, and no more input is coming (`over`: the caller's last
|
|
2196
|
+
* chunk arrived without an error and was read, or the caller ended its
|
|
2197
|
+
* direction behind it, for a handler that did not read all its input), this
|
|
2198
|
+
* side ends its direction too, where `end` was agreed. Without `end` nothing
|
|
2199
|
+
* happens here: the caller closes the port. A stream that never gets that far
|
|
2200
|
+
* was abandoned, and the caller's notice and `close` free the port; this then
|
|
2201
|
+
* never resumes. It needs no race against the port's close signal: nothing
|
|
2202
|
+
* awaits it, and what keeps it waiting is the input's watcher, a listener of
|
|
2203
|
+
* the port that a close drops, after which the wait is collected.
|
|
2204
|
+
*
|
|
2205
|
+
* Nothing this side still posts is lost behind its `end`. The caller ends only
|
|
2206
|
+
* once its own last chunk was confirmed, so no confirmation is owed to it
|
|
2207
|
+
* here, and what the freed port's close signal aborts has settled: the last
|
|
2208
|
+
* chunk has left, and input nobody reads is dropped, as on a close.
|
|
2209
|
+
*/
|
|
2210
|
+
async function endServedStream(port, sent, over) {
|
|
2211
|
+
await sent;
|
|
2212
|
+
await over;
|
|
2213
|
+
endPort(port);
|
|
2214
|
+
}
|
|
2215
|
+
/**
|
|
2039
2216
|
* A stream that completes normally has nobody left to call the returned
|
|
2040
2217
|
* teardown — the caller only knows the stream ended, not that it must also
|
|
2041
2218
|
* dispose the serve-side handle. Without this, the last `touch()`'s timer
|
|
@@ -2075,22 +2252,31 @@ function runCallerSide(port, input, options) {
|
|
|
2075
2252
|
const notice = installAbortNotice(port, controller);
|
|
2076
2253
|
abortOnPortClose(port, controller);
|
|
2077
2254
|
const clock = installStreamTimeout(controller, options.timeout);
|
|
2078
|
-
const inbound = receiveChunks(port, CHANNEL_OUT, controller, clock.touch);
|
|
2079
|
-
|
|
2255
|
+
const inbound = receiveChunks(port, CHANNEL_OUT, controller, clock.touch, notice);
|
|
2256
|
+
let sent = false;
|
|
2257
|
+
const pump = sendChunks(port, CHANNEL_IN, input, options, controller.signal, clock.touch, () => {
|
|
2258
|
+
sent = true;
|
|
2259
|
+
});
|
|
2080
2260
|
pump.catch(() => {});
|
|
2081
2261
|
return returningAtOnce((async function* () {
|
|
2262
|
+
let received = false;
|
|
2082
2263
|
try {
|
|
2083
2264
|
yield* inbound.stream;
|
|
2265
|
+
received = true;
|
|
2084
2266
|
} finally {
|
|
2267
|
+
const ended = received && sent;
|
|
2085
2268
|
if (!controller.signal.aborted) controller.abort(/* @__PURE__ */ new Error("webrun-rpc: the caller abandoned the stream"));
|
|
2086
|
-
notice.post();
|
|
2269
|
+
if (!ended) notice.post();
|
|
2087
2270
|
notice.stop();
|
|
2088
2271
|
clock.stop();
|
|
2089
2272
|
inbound.stop();
|
|
2090
2273
|
await pump.catch(() => {});
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2274
|
+
if (!(ended && endPort(port))) {
|
|
2275
|
+
notice.post();
|
|
2276
|
+
try {
|
|
2277
|
+
await port.close?.();
|
|
2278
|
+
} catch {}
|
|
2279
|
+
}
|
|
2094
2280
|
}
|
|
2095
2281
|
})(), () => {
|
|
2096
2282
|
if (!controller.signal.aborted) controller.abort(/* @__PURE__ */ new Error("webrun-rpc: the caller abandoned the stream"));
|
|
@@ -2184,7 +2370,7 @@ function installStreamTimeout(controller, timeout) {
|
|
|
2184
2370
|
* the local consumer has not started iterating, an inbound chunk waits for it —
|
|
2185
2371
|
* which is the correct backpressure, and different from having no listener.
|
|
2186
2372
|
*/
|
|
2187
|
-
function receiveChunks(port, channelName, controller, touch) {
|
|
2373
|
+
function receiveChunks(port, channelName, controller, touch, notice) {
|
|
2188
2374
|
let deliver;
|
|
2189
2375
|
const waiting = [];
|
|
2190
2376
|
let finished = false;
|
|
@@ -2194,25 +2380,64 @@ function receiveChunks(port, channelName, controller, touch) {
|
|
|
2194
2380
|
const ended = new Promise((resolve) => {
|
|
2195
2381
|
resolveEnded = resolve;
|
|
2196
2382
|
});
|
|
2383
|
+
let resolveOver = () => {};
|
|
2384
|
+
const over = new Promise((resolve) => {
|
|
2385
|
+
resolveOver = resolve;
|
|
2386
|
+
});
|
|
2197
2387
|
let aborted = false;
|
|
2198
2388
|
let abortReason;
|
|
2199
2389
|
const ready = () => deliver || finished ? Promise.resolve() : new Promise((r) => waiting.push(r));
|
|
2200
2390
|
const wake = () => {
|
|
2201
2391
|
for (const r of waiting.splice(0)) r();
|
|
2202
2392
|
};
|
|
2203
|
-
const
|
|
2393
|
+
const violate = (message) => {
|
|
2394
|
+
poison = new Error(message);
|
|
2395
|
+
controller.abort(poison);
|
|
2396
|
+
setTimeout(() => {
|
|
2397
|
+
try {
|
|
2398
|
+
port.close?.();
|
|
2399
|
+
} catch {}
|
|
2400
|
+
}, 0);
|
|
2401
|
+
return poison;
|
|
2402
|
+
};
|
|
2403
|
+
let lastChunkArrived = false;
|
|
2404
|
+
let watching = false;
|
|
2405
|
+
const watchPeerEnd = () => {
|
|
2406
|
+
if (watching) return true;
|
|
2407
|
+
if (!portAgreement(port)?.caps.has("end")) return false;
|
|
2408
|
+
watching = true;
|
|
2409
|
+
whenPortEnded(port).then(() => {
|
|
2410
|
+
port.removeEventListener("message", watcher);
|
|
2411
|
+
if (lastChunkArrived) {
|
|
2412
|
+
resolveOver();
|
|
2413
|
+
return;
|
|
2414
|
+
}
|
|
2415
|
+
notice.post();
|
|
2416
|
+
violate("webrun-rpc: the peer ended its direction without finishing it; the stream port is closed");
|
|
2417
|
+
});
|
|
2418
|
+
return true;
|
|
2419
|
+
};
|
|
2420
|
+
const watcher = (event) => {
|
|
2421
|
+
const data = event.data;
|
|
2422
|
+
if (data?.channelName !== channelName) return;
|
|
2423
|
+
const chunk = data.type === CHUNK$1 ? data.chunk : data.type === "request" ? data.params : void 0;
|
|
2424
|
+
if (!chunk) return;
|
|
2425
|
+
if (!watchPeerEnd()) {
|
|
2426
|
+
port.removeEventListener("message", watcher);
|
|
2427
|
+
return;
|
|
2428
|
+
}
|
|
2429
|
+
if (chunk.done && !chunk.error) lastChunkArrived = true;
|
|
2430
|
+
};
|
|
2431
|
+
port.addEventListener("message", watcher);
|
|
2432
|
+
watchPeerEnd();
|
|
2433
|
+
let held = 0;
|
|
2434
|
+
const off = listenPort(port, async (params) => {
|
|
2435
|
+
const { done, error } = params;
|
|
2436
|
+
const value = adoptValue(params);
|
|
2204
2437
|
touch();
|
|
2205
2438
|
if (poison) throw poison;
|
|
2206
|
-
if (
|
|
2207
|
-
|
|
2208
|
-
controller.abort(poison);
|
|
2209
|
-
setTimeout(() => {
|
|
2210
|
-
try {
|
|
2211
|
-
port.close?.();
|
|
2212
|
-
} catch {}
|
|
2213
|
-
}, 0);
|
|
2214
|
-
throw poison;
|
|
2215
|
-
}
|
|
2439
|
+
if (held > 0) throw violate(MIXED_MESSAGE);
|
|
2440
|
+
if (outstanding) throw violate("webrun-rpc: peer sent a second unconfirmed chunk; the stream port is closed");
|
|
2216
2441
|
outstanding = true;
|
|
2217
2442
|
try {
|
|
2218
2443
|
await ready();
|
|
@@ -2222,15 +2447,60 @@ function receiveChunks(port, channelName, controller, touch) {
|
|
|
2222
2447
|
value,
|
|
2223
2448
|
error: error ? deserializeError(error) : void 0
|
|
2224
2449
|
});
|
|
2450
|
+
if (done && !error) resolveOver();
|
|
2225
2451
|
} finally {
|
|
2226
2452
|
outstanding = false;
|
|
2227
2453
|
}
|
|
2228
2454
|
if (done) resolveEnded();
|
|
2229
2455
|
}, { channelName });
|
|
2456
|
+
const onChunk = (event) => {
|
|
2457
|
+
const data = event.data;
|
|
2458
|
+
if (data?.type !== CHUNK$1 || data.channelName !== channelName || !data.chunk) return;
|
|
2459
|
+
if (poison) return;
|
|
2460
|
+
const agreement = portAgreement(port);
|
|
2461
|
+
if (!agreement?.caps.has("pipeline")) {
|
|
2462
|
+
violate("webrun-rpc: peer sent a pipelined chunk where pipelining was not agreed");
|
|
2463
|
+
return;
|
|
2464
|
+
}
|
|
2465
|
+
if (outstanding) {
|
|
2466
|
+
violate(MIXED_MESSAGE);
|
|
2467
|
+
return;
|
|
2468
|
+
}
|
|
2469
|
+
if (data.chunk.value !== void 0 && !ArrayBuffer.isView(data.chunk.value)) {
|
|
2470
|
+
violate("webrun-rpc: peer sent a pipelined chunk whose value is not bytes");
|
|
2471
|
+
return;
|
|
2472
|
+
}
|
|
2473
|
+
if (held > agreement.window) {
|
|
2474
|
+
violate(`webrun-rpc: peer sent past the window (${agreement.window} bytes) without waiting for credit; the stream port is closed`);
|
|
2475
|
+
return;
|
|
2476
|
+
}
|
|
2477
|
+
touch();
|
|
2478
|
+
const { done, error } = data.chunk;
|
|
2479
|
+
const value = data.chunk.value && ownBytes(data.chunk.value);
|
|
2480
|
+
const size = (value?.byteLength ?? 0) + CHUNK_OVERHEAD;
|
|
2481
|
+
held += size;
|
|
2482
|
+
return (async () => {
|
|
2483
|
+
try {
|
|
2484
|
+
await ready();
|
|
2485
|
+
if (finished) return;
|
|
2486
|
+
await deliver?.({
|
|
2487
|
+
done,
|
|
2488
|
+
value,
|
|
2489
|
+
error: error ? deserializeError(error) : void 0
|
|
2490
|
+
});
|
|
2491
|
+
if (done && !error) resolveOver();
|
|
2492
|
+
if (done) resolveEnded();
|
|
2493
|
+
} finally {
|
|
2494
|
+
held -= size;
|
|
2495
|
+
}
|
|
2496
|
+
})();
|
|
2497
|
+
};
|
|
2498
|
+
port.addEventListener("message", onChunk);
|
|
2230
2499
|
const detach = () => {
|
|
2231
2500
|
finished = true;
|
|
2232
2501
|
wake();
|
|
2233
2502
|
off();
|
|
2503
|
+
port.removeEventListener("message", onChunk);
|
|
2234
2504
|
controller.signal.removeEventListener("abort", onAbort);
|
|
2235
2505
|
resolveEnded();
|
|
2236
2506
|
};
|
|
@@ -2260,25 +2530,35 @@ function receiveChunks(port, channelName, controller, touch) {
|
|
|
2260
2530
|
return detach;
|
|
2261
2531
|
}),
|
|
2262
2532
|
stop: detach,
|
|
2263
|
-
ended
|
|
2533
|
+
ended,
|
|
2534
|
+
over
|
|
2264
2535
|
};
|
|
2265
2536
|
}
|
|
2266
2537
|
/**
|
|
2267
2538
|
* The sending half of one direction: one `callPort` per chunk, one call
|
|
2268
|
-
* outstanding at a time (spec D11/D12), with no per-chunk deadline (spec D8)
|
|
2539
|
+
* outstanding at a time (spec D11/D12), with no per-chunk deadline (spec D8) —
|
|
2540
|
+
* or, once pipelining is agreed, plain chunk messages held back by the window.
|
|
2541
|
+
* `onSent` runs once the end of the output has left without an error: posted
|
|
2542
|
+
* when pipelined, confirmed when not.
|
|
2269
2543
|
*/
|
|
2270
|
-
async function sendChunks(port, channelName, output, { maxMessageSize, log }, signal, touch) {
|
|
2544
|
+
async function sendChunks(port, channelName, output, { maxMessageSize, log }, signal, touch, onSent) {
|
|
2271
2545
|
const framed = maxMessageSize ? toChunks(maxMessageSize)(output) : output;
|
|
2272
2546
|
const stream = throughAbort(framed, signal);
|
|
2273
2547
|
const producer = output;
|
|
2274
2548
|
const returnProducer = () => void Promise.resolve(producer.return?.()).catch(() => {});
|
|
2275
2549
|
if (framed !== output) signal.addEventListener("abort", returnProducer, { once: true });
|
|
2550
|
+
let onAborted = () => {};
|
|
2551
|
+
const aborted = new Promise((resolve) => {
|
|
2552
|
+
onAborted = () => resolve();
|
|
2553
|
+
});
|
|
2554
|
+
if (signal.aborted) onAborted();
|
|
2555
|
+
else signal.addEventListener("abort", onAborted, { once: true });
|
|
2276
2556
|
try {
|
|
2277
2557
|
await sendIterator(async ({ done, value, error }) => {
|
|
2278
2558
|
if (signal.aborted) return;
|
|
2279
2559
|
const chunk = {
|
|
2280
2560
|
done,
|
|
2281
|
-
value,
|
|
2561
|
+
value: done ? value : chunkBytes(value),
|
|
2282
2562
|
error: error === void 0 ? void 0 : serializeError(error)
|
|
2283
2563
|
};
|
|
2284
2564
|
log?.("[duplexOverPort] send", {
|
|
@@ -2286,48 +2566,34 @@ async function sendChunks(port, channelName, output, { maxMessageSize, log }, si
|
|
|
2286
2566
|
done,
|
|
2287
2567
|
size: value?.byteLength
|
|
2288
2568
|
});
|
|
2569
|
+
if (mayPipeline(port)) {
|
|
2570
|
+
port.postMessage({
|
|
2571
|
+
type: CHUNK$1,
|
|
2572
|
+
channelName,
|
|
2573
|
+
chunk
|
|
2574
|
+
});
|
|
2575
|
+
touch();
|
|
2576
|
+
if (done && !error) onSent();
|
|
2577
|
+
if (portBufferedAmount(port) > 0) await Promise.race([whenPortReady(port), aborted]);
|
|
2578
|
+
return;
|
|
2579
|
+
}
|
|
2289
2580
|
await callPort(port, chunk, {
|
|
2290
2581
|
channelName,
|
|
2291
2582
|
timeout: NO_TIMEOUT,
|
|
2292
2583
|
signal
|
|
2293
2584
|
});
|
|
2294
2585
|
touch();
|
|
2586
|
+
if (done && !error) onSent();
|
|
2295
2587
|
}, stream);
|
|
2296
2588
|
} catch (err) {
|
|
2297
2589
|
if (signal.aborted) return;
|
|
2298
2590
|
throw err;
|
|
2299
2591
|
} finally {
|
|
2300
2592
|
signal.removeEventListener("abort", returnProducer);
|
|
2593
|
+
signal.removeEventListener("abort", onAborted);
|
|
2301
2594
|
}
|
|
2302
2595
|
}
|
|
2303
2596
|
/**
|
|
2304
|
-
* Runs `fn` on a later task, never a microtask: a producer looping on
|
|
2305
|
-
* `whenPortReady` must let the transport's own events run. `setImmediate`
|
|
2306
|
-
* where it exists; elsewhere a `MessageChannel`, because nested `setTimeout`s
|
|
2307
|
-
* are clamped to 4 ms in browsers.
|
|
2308
|
-
*/
|
|
2309
|
-
const setImmediateFn = globalThis.setImmediate;
|
|
2310
|
-
/** @internal */
|
|
2311
|
-
const nextTask = setImmediateFn ? (fn) => {
|
|
2312
|
-
setImmediateFn(fn);
|
|
2313
|
-
} : (() => {
|
|
2314
|
-
let channel;
|
|
2315
|
-
const queue = [];
|
|
2316
|
-
return (fn) => {
|
|
2317
|
-
if (!channel) {
|
|
2318
|
-
channel = new MessageChannel();
|
|
2319
|
-
channel.port1.onmessage = () => queue.shift()?.();
|
|
2320
|
-
}
|
|
2321
|
-
queue.push(fn);
|
|
2322
|
-
channel.port2.postMessage(0);
|
|
2323
|
-
};
|
|
2324
|
-
})();
|
|
2325
|
-
const states = /* @__PURE__ */ new WeakMap();
|
|
2326
|
-
/** @internal Registered by the multiplexer for each port with a window. */
|
|
2327
|
-
function setPortSendState(port, state) {
|
|
2328
|
-
states.set(port, state);
|
|
2329
|
-
}
|
|
2330
|
-
/**
|
|
2331
2597
|
* One virtual port.
|
|
2332
2598
|
*
|
|
2333
2599
|
* `deliver` and `markClosed` are deliberately not on `port`: the consumer holds
|
|
@@ -2405,6 +2671,21 @@ function newVirtualPort(send, requestClose) {
|
|
|
2405
2671
|
const KEEPALIVE_ID = Number.MAX_SAFE_INTEGER;
|
|
2406
2672
|
/** The longest delay a timer holds; past it `setInterval` fires at once. */
|
|
2407
2673
|
const MAX_TIMER_MS = 2 ** 31 - 1;
|
|
2674
|
+
/**
|
|
2675
|
+
* What a windowed port offers in its `open`: every capability this end
|
|
2676
|
+
* speaks. `end` only over a codec that reads it (`PortCodec.halfClose`).
|
|
2677
|
+
*/
|
|
2678
|
+
function ownCaps(codec) {
|
|
2679
|
+
return codec.halfClose ? ["pipeline", "end"] : ["pipeline"];
|
|
2680
|
+
}
|
|
2681
|
+
/**
|
|
2682
|
+
* The capabilities in a peer's `caps` that this end speaks too, or
|
|
2683
|
+
* `undefined` when the peer listed none (it is then treated as an older peer).
|
|
2684
|
+
*/
|
|
2685
|
+
function sharedCaps(own, listed) {
|
|
2686
|
+
if (!Array.isArray(listed)) return void 0;
|
|
2687
|
+
return new Set(own.filter((cap) => listed.includes(cap)));
|
|
2688
|
+
}
|
|
2408
2689
|
/** Ceiling on concurrently open virtual ports. Bounds the id table only. */
|
|
2409
2690
|
const DEFAULT_MAX_PORTS = 1024;
|
|
2410
2691
|
/**
|
|
@@ -2468,6 +2749,8 @@ function multiplexPort(port, options) {
|
|
|
2468
2749
|
let muxClosed = false;
|
|
2469
2750
|
/** This end counts encoded bytes and reassembles frames. */
|
|
2470
2751
|
const exactCodec = codec.encode != null && codec.decode != null;
|
|
2752
|
+
/** What this end offers on a windowed port. */
|
|
2753
|
+
const caps = ownCaps(codec);
|
|
2471
2754
|
const post = (envelope, transfer) => {
|
|
2472
2755
|
if (muxClosed) return;
|
|
2473
2756
|
codecPost(envelope, transfer);
|
|
@@ -2525,13 +2808,39 @@ function multiplexPort(port, options) {
|
|
|
2525
2808
|
port.postMessage(bytes);
|
|
2526
2809
|
} catch {}
|
|
2527
2810
|
};
|
|
2811
|
+
/**
|
|
2812
|
+
* Both directions have ended: free the id as a close would, with no `close`
|
|
2813
|
+
* on the wire (each end frees it on its own once it has seen both ends).
|
|
2814
|
+
* Aborting the close signal is how the port's consumer learns it is gone.
|
|
2815
|
+
*/
|
|
2816
|
+
const freeIfEnded = (id, c) => {
|
|
2817
|
+
const entry = open.get(id);
|
|
2818
|
+
if (!c.endSent || !c.peerEnded || entry?.credit !== c) return;
|
|
2819
|
+
freePartial(entry);
|
|
2820
|
+
open.delete(id);
|
|
2821
|
+
leaks?.unregister(entry.handle);
|
|
2822
|
+
entry.handle.markClosed(/* @__PURE__ */ new Error("webrun-rpc: the port ended in both directions"));
|
|
2823
|
+
};
|
|
2824
|
+
/** A peer that broke the port's protocol: hold none of its traffic, and close. */
|
|
2825
|
+
const failPort = (id, entry, error, reason) => {
|
|
2826
|
+
freePartial(entry);
|
|
2827
|
+
dropOutgoing(id);
|
|
2828
|
+
open.delete(id);
|
|
2829
|
+
releaseWaiters(entry.credit);
|
|
2830
|
+
entry.handle.markClosed(error);
|
|
2831
|
+
post({
|
|
2832
|
+
type: "close",
|
|
2833
|
+
id,
|
|
2834
|
+
reason
|
|
2835
|
+
});
|
|
2836
|
+
};
|
|
2528
2837
|
const releaseWaiters = (c) => {
|
|
2529
2838
|
for (const w of c?.waiters.splice(0) ?? []) w.resolve();
|
|
2530
2839
|
};
|
|
2531
2840
|
const flush = (c) => {
|
|
2532
2841
|
while (c.queue.length) {
|
|
2533
2842
|
const next = c.queue[0];
|
|
2534
|
-
if (c.enforced && c.outstanding > 0 && c.outstanding + next.size > c.window) break;
|
|
2843
|
+
if (next.size > 0 && c.enforced && c.outstanding > 0 && c.outstanding + next.size > c.window) break;
|
|
2535
2844
|
c.queue.shift();
|
|
2536
2845
|
c.queued -= next.size;
|
|
2537
2846
|
c.outstanding += next.size;
|
|
@@ -2569,15 +2878,20 @@ function multiplexPort(port, options) {
|
|
|
2569
2878
|
enforced: false,
|
|
2570
2879
|
opener,
|
|
2571
2880
|
exact,
|
|
2881
|
+
caps: /* @__PURE__ */ new Set(),
|
|
2572
2882
|
outstanding: 0,
|
|
2573
2883
|
queue: [],
|
|
2574
2884
|
queued: 0,
|
|
2575
2885
|
unconsumed: 0,
|
|
2576
2886
|
waiters: [],
|
|
2577
2887
|
toGrant: 0,
|
|
2578
|
-
grantScheduled: false
|
|
2888
|
+
grantScheduled: false,
|
|
2889
|
+
ending: false,
|
|
2890
|
+
endSent: false,
|
|
2891
|
+
peerEnded: false
|
|
2579
2892
|
} : void 0;
|
|
2580
2893
|
const self = newVirtualPort((payload, transfer) => {
|
|
2894
|
+
if (c?.ending) return;
|
|
2581
2895
|
touch(id);
|
|
2582
2896
|
const envelope = {
|
|
2583
2897
|
type: "message",
|
|
@@ -2664,6 +2978,38 @@ function multiplexPort(port, options) {
|
|
|
2664
2978
|
});
|
|
2665
2979
|
})
|
|
2666
2980
|
});
|
|
2981
|
+
setPortEndState(self.port, {
|
|
2982
|
+
end: () => {
|
|
2983
|
+
if (isClosed() || !c.caps.has("end")) return false;
|
|
2984
|
+
if (c.ending) return true;
|
|
2985
|
+
c.ending = true;
|
|
2986
|
+
c.queue.push({
|
|
2987
|
+
size: 0,
|
|
2988
|
+
send: () => sendOnPort(id, () => {
|
|
2989
|
+
if (open.get(id)?.credit !== c) return;
|
|
2990
|
+
post({
|
|
2991
|
+
type: "end",
|
|
2992
|
+
id
|
|
2993
|
+
});
|
|
2994
|
+
c.endSent = true;
|
|
2995
|
+
freeIfEnded(id, c);
|
|
2996
|
+
})
|
|
2997
|
+
});
|
|
2998
|
+
flush(c);
|
|
2999
|
+
return true;
|
|
3000
|
+
},
|
|
3001
|
+
watchPeerEnd: (resolve) => {
|
|
3002
|
+
if (!c.caps.has("end")) return false;
|
|
3003
|
+
if (c.peerEnded) resolve();
|
|
3004
|
+
else c.onPeerEnd = new WeakRef(resolve);
|
|
3005
|
+
return true;
|
|
3006
|
+
}
|
|
3007
|
+
});
|
|
3008
|
+
setPortAgreement(self.port, () => ({
|
|
3009
|
+
window: c.window,
|
|
3010
|
+
caps: c.caps,
|
|
3011
|
+
enforced: c.enforced
|
|
3012
|
+
}));
|
|
2667
3013
|
}
|
|
2668
3014
|
const record = {
|
|
2669
3015
|
deliver: self.deliver,
|
|
@@ -2715,16 +3061,24 @@ function multiplexPort(port, options) {
|
|
|
2715
3061
|
}
|
|
2716
3062
|
const handle = attach(envelope.id, envelope.meta, envelope.window, false, envelope.exact === true && exactCodec);
|
|
2717
3063
|
const credit = open.get(envelope.id)?.credit;
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
3064
|
+
const listed = sharedCaps(caps, envelope.caps);
|
|
3065
|
+
if (credit) {
|
|
3066
|
+
const ack = credit.exact ? {
|
|
3067
|
+
type: "credit",
|
|
3068
|
+
id: envelope.id,
|
|
3069
|
+
bytes: 0,
|
|
3070
|
+
exact: true
|
|
3071
|
+
} : {
|
|
3072
|
+
type: "credit",
|
|
3073
|
+
id: envelope.id,
|
|
3074
|
+
bytes: 0
|
|
3075
|
+
};
|
|
3076
|
+
if (listed) credit.caps = listed;
|
|
3077
|
+
post(listed ? {
|
|
3078
|
+
...ack,
|
|
3079
|
+
caps: [...listed]
|
|
3080
|
+
} : ack);
|
|
3081
|
+
}
|
|
2728
3082
|
let accepted = false;
|
|
2729
3083
|
if (onPort) try {
|
|
2730
3084
|
accepted = onPort(handle.port, envelope.meta) !== false;
|
|
@@ -2752,6 +3106,7 @@ function multiplexPort(port, options) {
|
|
|
2752
3106
|
c.exact = false;
|
|
2753
3107
|
c.outstanding = 0;
|
|
2754
3108
|
}
|
|
3109
|
+
c.caps = sharedCaps(caps, envelope.caps) ?? /* @__PURE__ */ new Set();
|
|
2755
3110
|
post({
|
|
2756
3111
|
type: "credit",
|
|
2757
3112
|
id: envelope.id,
|
|
@@ -2763,6 +3118,10 @@ function multiplexPort(port, options) {
|
|
|
2763
3118
|
flush(c);
|
|
2764
3119
|
return;
|
|
2765
3120
|
}
|
|
3121
|
+
if ((envelope.type === "message" || envelope.type === "frame") && entry?.credit?.peerEnded) {
|
|
3122
|
+
failPort(envelope.id, entry, /* @__PURE__ */ new Error("webrun-rpc: the peer sent on a port after its end"), "protocol");
|
|
3123
|
+
return;
|
|
3124
|
+
}
|
|
2766
3125
|
if (envelope.type === "message") {
|
|
2767
3126
|
const sizeOf = (c) => (c.exact ? byteLengthOf(event.data) : void 0) ?? estimateMessageSize(envelope.payload);
|
|
2768
3127
|
if (!entry) return;
|
|
@@ -2787,16 +3146,7 @@ function multiplexPort(port, options) {
|
|
|
2787
3146
|
const charged = partial.room == null ? Math.max(0, partial.bytes + size - c.window) : 0;
|
|
2788
3147
|
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;
|
|
2789
3148
|
if (overrun) {
|
|
2790
|
-
|
|
2791
|
-
dropOutgoing(envelope.id);
|
|
2792
|
-
open.delete(envelope.id);
|
|
2793
|
-
releaseWaiters(c);
|
|
2794
|
-
entry.handle.markClosed(/* @__PURE__ */ new RangeError(`webrun-rpc: a framed message passed ${overrun}`));
|
|
2795
|
-
post({
|
|
2796
|
-
type: "close",
|
|
2797
|
-
id: envelope.id,
|
|
2798
|
-
reason: "max-reassembled-bytes"
|
|
2799
|
-
});
|
|
3149
|
+
failPort(envelope.id, entry, /* @__PURE__ */ new RangeError(`webrun-rpc: a framed message passed ${overrun}`), "max-reassembled-bytes");
|
|
2800
3150
|
return;
|
|
2801
3151
|
}
|
|
2802
3152
|
partial.bytes += size;
|
|
@@ -2816,6 +3166,19 @@ function multiplexPort(port, options) {
|
|
|
2816
3166
|
entry.handle.deliver(inner.payload, whole.byteLength);
|
|
2817
3167
|
return;
|
|
2818
3168
|
}
|
|
3169
|
+
if (envelope.type === "end") {
|
|
3170
|
+
if (!entry) return;
|
|
3171
|
+
const c = entry.credit;
|
|
3172
|
+
if (!c?.caps.has("end")) {
|
|
3173
|
+
failPort(envelope.id, entry, /* @__PURE__ */ new Error("webrun-rpc: the peer ended a port where end was not agreed"), "protocol");
|
|
3174
|
+
return;
|
|
3175
|
+
}
|
|
3176
|
+
touch(envelope.id);
|
|
3177
|
+
c.peerEnded = true;
|
|
3178
|
+
c.onPeerEnd?.deref()?.();
|
|
3179
|
+
freeIfEnded(envelope.id, c);
|
|
3180
|
+
return;
|
|
3181
|
+
}
|
|
2819
3182
|
dropOutgoing(envelope.id);
|
|
2820
3183
|
if (!entry) return;
|
|
2821
3184
|
freePartial(entry);
|
|
@@ -2897,12 +3260,14 @@ function multiplexPort(port, options) {
|
|
|
2897
3260
|
id,
|
|
2898
3261
|
meta,
|
|
2899
3262
|
window,
|
|
2900
|
-
exact: true
|
|
3263
|
+
exact: true,
|
|
3264
|
+
caps: [...caps]
|
|
2901
3265
|
} : {
|
|
2902
3266
|
type: "open",
|
|
2903
3267
|
id,
|
|
2904
3268
|
meta,
|
|
2905
|
-
window
|
|
3269
|
+
window,
|
|
3270
|
+
caps: [...caps]
|
|
2906
3271
|
} : {
|
|
2907
3272
|
type: "open",
|
|
2908
3273
|
id,
|
|
@@ -3057,7 +3422,7 @@ function isEnvelope(value) {
|
|
|
3057
3422
|
const candidate = value;
|
|
3058
3423
|
if (typeof candidate.id !== "number") return false;
|
|
3059
3424
|
if (!Number.isInteger(candidate.id) || candidate.id < 0) return false;
|
|
3060
|
-
return candidate.type === "open" || candidate.type === "message" || candidate.type === "close" || candidate.type === "credit" || candidate.type === "frame" || candidate.type === "ping" || candidate.type === "pong";
|
|
3425
|
+
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";
|
|
3061
3426
|
}
|
|
3062
3427
|
/**
|
|
3063
3428
|
* For ports whose messages are structured values — a real `MessagePort`, a
|
|
@@ -3069,6 +3434,7 @@ function isEnvelope(value) {
|
|
|
3069
3434
|
* not send anything a byte codec could not also carry.
|
|
3070
3435
|
*/
|
|
3071
3436
|
const structuredCodec = {
|
|
3437
|
+
halfClose: true,
|
|
3072
3438
|
post(port, envelope, transfer) {
|
|
3073
3439
|
if (transfer && transfer.length > 0) port.postMessage(envelope, transfer);
|
|
3074
3440
|
else port.postMessage(envelope);
|
|
@@ -3106,10 +3472,15 @@ function asyncDisposable(target, dispose) {
|
|
|
3106
3472
|
* own `handleStreams` / `sendStream` (no backpressure); version 2 is
|
|
3107
3473
|
* webrun-rpc's `connect` / `serve` over `overPipe` with webrun-http-streams'
|
|
3108
3474
|
* `fetchOverDuplex` / `serveFetchOverDuplex`: one port per link, calls
|
|
3109
|
-
* multiplexed on it, each call a stream with
|
|
3110
|
-
* directions, and cancellation that reaches the handler.
|
|
3475
|
+
* multiplexed on it, each call a stream with backpressure in both
|
|
3476
|
+
* directions, and cancellation that reaches the handler. A call's port with a
|
|
3477
|
+
* window (`HttpLinkOptions.window`) pipelines its chunks where both ends speak
|
|
3478
|
+
* it, and goes one confirmed chunk at a time otherwise, so the window needs
|
|
3479
|
+
* no new version.
|
|
3111
3480
|
*/
|
|
3112
3481
|
const HTTP_LINK_PROTOCOL = "webrun-http-link/2";
|
|
3482
|
+
/** The calling end's default `HttpLinkOptions.window`: 256 KiB. */
|
|
3483
|
+
const DEFAULT_HTTP_LINK_WINDOW = 262144;
|
|
3113
3484
|
/**
|
|
3114
3485
|
* Why a request failed when its link went away under it: the far side (the
|
|
3115
3486
|
* handler, or the ServiceWorker) was lost. The request may or may not have
|
|
@@ -3128,12 +3499,13 @@ var LinkLostError = class extends Error {
|
|
|
3128
3499
|
*/
|
|
3129
3500
|
const LINK_CLOSE = "webrun-http:link-close";
|
|
3130
3501
|
/** Opens the calling end of a link on `port` (one port per link). */
|
|
3131
|
-
async function connectHttpLink(port, { streamTimeout, maxPorts } = {}) {
|
|
3502
|
+
async function connectHttpLink(port, { streamTimeout, maxPorts, window = DEFAULT_HTTP_LINK_WINDOW } = {}) {
|
|
3132
3503
|
const { call, close } = await connect({
|
|
3133
3504
|
mux: overPipe(port, {
|
|
3134
3505
|
codec: structuredCodec,
|
|
3135
3506
|
side: "initiator",
|
|
3136
|
-
maxPorts
|
|
3507
|
+
maxPorts,
|
|
3508
|
+
window
|
|
3137
3509
|
}),
|
|
3138
3510
|
timeout: streamTimeout
|
|
3139
3511
|
});
|
|
@@ -3495,20 +3867,24 @@ function postFrame(port, frame) {
|
|
|
3495
3867
|
function messageSize(data) {
|
|
3496
3868
|
return typeof data === "string" ? utf8Length(data) : data.byteLength;
|
|
3497
3869
|
}
|
|
3498
|
-
|
|
3870
|
+
const encoder = new TextEncoder();
|
|
3871
|
+
/** Characters encoded per `encodeInto` call: at most 3 bytes each, so the scratch stays at 96 KiB. */
|
|
3872
|
+
const CHUNK = 32768;
|
|
3873
|
+
const scratch = new Uint8Array(CHUNK * 3);
|
|
3874
|
+
/**
|
|
3875
|
+
* The UTF-8 length of `text`, as `TextEncoder` would encode it (a lone surrogate counts as the 3
|
|
3876
|
+
* bytes of U+FFFD). Every message is measured on both ends (`bufferedAmount` and the byte acks), so
|
|
3877
|
+
* this runs natively: `encodeInto` a reused buffer, a chunk at a time, never splitting a surrogate
|
|
3878
|
+
* pair across two chunks. A loop over the characters in JavaScript took about 30 times as long.
|
|
3879
|
+
*/
|
|
3499
3880
|
function utf8Length(text) {
|
|
3500
3881
|
let length = 0;
|
|
3501
|
-
for (let
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
if (next >= 56320 && next <= 57343) {
|
|
3508
|
-
length += 4;
|
|
3509
|
-
i++;
|
|
3510
|
-
} else length += 3;
|
|
3511
|
-
} else length += 3;
|
|
3882
|
+
for (let start = 0; start < text.length;) {
|
|
3883
|
+
let end = Math.min(start + CHUNK, text.length);
|
|
3884
|
+
const last = text.charCodeAt(end - 1);
|
|
3885
|
+
if (end < text.length && last >= 55296 && last <= 56319) end--;
|
|
3886
|
+
length += encoder.encodeInto(text.slice(start, end), scratch).written;
|
|
3887
|
+
start = end;
|
|
3512
3888
|
}
|
|
3513
3889
|
return length;
|
|
3514
3890
|
}
|
|
@@ -3664,6 +4040,7 @@ var PortSocketCore = class extends EventTarget {
|
|
|
3664
4040
|
code,
|
|
3665
4041
|
reason
|
|
3666
4042
|
});
|
|
4043
|
+
if (!Number.isFinite(this._closeTimeoutMs)) return;
|
|
3667
4044
|
this._closeTimer = this._timers.setTimeout(() => this._finish(1006, "", false), this._closeTimeoutMs);
|
|
3668
4045
|
}
|
|
3669
4046
|
_onCloseFrame(code, reason) {
|
|
@@ -3742,7 +4119,8 @@ function isApplicationCloseCode(code) {
|
|
|
3742
4119
|
* `whenDrained(limit)` waits for it to fall: the backpressure a producer
|
|
3743
4120
|
* applies.
|
|
3744
4121
|
* - When the client's pings stop for three heartbeat periods (a closed tab, a
|
|
3745
|
-
* terminated worker), the socket closes with 1006.
|
|
4122
|
+
* terminated worker), the socket closes with 1006. With `watch: "first-ping"`
|
|
4123
|
+
* the watch starts only once the client pings.
|
|
3746
4124
|
*/
|
|
3747
4125
|
var ServerWebSocket = class extends PortSocketCore {
|
|
3748
4126
|
static CONNECTING = 0;
|
|
@@ -3751,55 +4129,150 @@ var ServerWebSocket = class extends PortSocketCore {
|
|
|
3751
4129
|
static CLOSED = 3;
|
|
3752
4130
|
url;
|
|
3753
4131
|
protocol;
|
|
4132
|
+
_binaryType = "arraybuffer";
|
|
4133
|
+
_options;
|
|
3754
4134
|
_accepted = false;
|
|
4135
|
+
_detached = false;
|
|
3755
4136
|
_queue = [];
|
|
3756
4137
|
_heartbeatMs = DEFAULT_HEARTBEAT_MS;
|
|
3757
4138
|
_lastPing = Date.now();
|
|
4139
|
+
/** Whether a ping (with a period a timer takes) arrived already. */
|
|
4140
|
+
_pinged = false;
|
|
3758
4141
|
_watch;
|
|
3759
4142
|
constructor(port, options = {}) {
|
|
3760
4143
|
super({
|
|
3761
4144
|
timers: options.timers,
|
|
3762
4145
|
closeTimeoutMs: options.closeTimeoutMs
|
|
3763
4146
|
});
|
|
4147
|
+
this._options = options;
|
|
3764
4148
|
this.url = options.url ?? "";
|
|
3765
4149
|
this.protocol = options.protocol ?? "";
|
|
3766
4150
|
this._attach(port);
|
|
3767
4151
|
this._state = 1;
|
|
3768
|
-
this._watchHeartbeat();
|
|
4152
|
+
if (options.watch !== "first-ping") this._watchHeartbeat();
|
|
3769
4153
|
}
|
|
3770
4154
|
/** Whether `accept()` was called. */
|
|
3771
4155
|
get accepted() {
|
|
3772
4156
|
return this._accepted;
|
|
3773
4157
|
}
|
|
4158
|
+
/** How binary messages arrive: `ArrayBuffer`s (the default) or `Blob`s. Other values are ignored. */
|
|
4159
|
+
get binaryType() {
|
|
4160
|
+
return this._binaryType;
|
|
4161
|
+
}
|
|
4162
|
+
set binaryType(value) {
|
|
4163
|
+
if (value === "blob" || value === "arraybuffer") this._binaryType = value;
|
|
4164
|
+
}
|
|
4165
|
+
/**
|
|
4166
|
+
* Hands this end's transport to another holder before it was accepted: the port to give the
|
|
4167
|
+
* client (a pair's end returned in a 101, say), with the frames that reached this end first
|
|
4168
|
+
* delivered ahead of the rest. This end is closed then, with no event, and answers nothing.
|
|
4169
|
+
*/
|
|
4170
|
+
detach() {
|
|
4171
|
+
const port = this._port;
|
|
4172
|
+
if (!port || this._accepted || this._state !== 1) throw new DOMException("Only an open end that was not accepted can be detached.", "InvalidStateError");
|
|
4173
|
+
this._detached = true;
|
|
4174
|
+
this._port = void 0;
|
|
4175
|
+
port.onmessage = null;
|
|
4176
|
+
let owed = this.bufferedAmount;
|
|
4177
|
+
this._state = 3;
|
|
4178
|
+
this._onClosed();
|
|
4179
|
+
this._wakeDrainWaiters();
|
|
4180
|
+
const queued = this._queue.splice(0);
|
|
4181
|
+
if (queued.length === 0 && owed === 0) return port;
|
|
4182
|
+
const relay = new MessageChannel();
|
|
4183
|
+
const holder = relay.port2;
|
|
4184
|
+
const closing = /* @__PURE__ */ new Set();
|
|
4185
|
+
const end = () => {
|
|
4186
|
+
port.onmessage = null;
|
|
4187
|
+
holder.onmessage = null;
|
|
4188
|
+
port.close();
|
|
4189
|
+
holder.close();
|
|
4190
|
+
};
|
|
4191
|
+
const sawClose = (way) => {
|
|
4192
|
+
closing.add(way);
|
|
4193
|
+
if (closing.size === 2) end();
|
|
4194
|
+
};
|
|
4195
|
+
for (const frame of queued) {
|
|
4196
|
+
postFrame(holder, frame);
|
|
4197
|
+
if (frame.t === "c") closing.add("in");
|
|
4198
|
+
}
|
|
4199
|
+
holder.onmessage = (event) => {
|
|
4200
|
+
port.postMessage(event.data, transferables(event.data));
|
|
4201
|
+
if (event.data?.t === "c") sawClose("out");
|
|
4202
|
+
};
|
|
4203
|
+
port.onmessage = (event) => {
|
|
4204
|
+
const data = event.data;
|
|
4205
|
+
if (data?.t === "a" && typeof data.n === "number" && owed > 0) {
|
|
4206
|
+
const absorbed = Math.min(owed, data.n);
|
|
4207
|
+
owed -= absorbed;
|
|
4208
|
+
if (data.n > absorbed) holder.postMessage({
|
|
4209
|
+
t: "a",
|
|
4210
|
+
n: data.n - absorbed
|
|
4211
|
+
});
|
|
4212
|
+
return;
|
|
4213
|
+
}
|
|
4214
|
+
holder.postMessage(event.data, transferables(event.data));
|
|
4215
|
+
if (data?.t === "c") sawClose("in");
|
|
4216
|
+
};
|
|
4217
|
+
port.addEventListener?.("close", end);
|
|
4218
|
+
holder.addEventListener?.("close", end);
|
|
4219
|
+
return relay.port1;
|
|
4220
|
+
}
|
|
3774
4221
|
/** Starts delivering events, the queued ones first. */
|
|
3775
4222
|
accept() {
|
|
4223
|
+
if (this._detached) throw new DOMException("This end was detached; it cannot be accepted.", "InvalidStateError");
|
|
3776
4224
|
if (this._accepted) return;
|
|
3777
4225
|
this._accepted = true;
|
|
3778
|
-
for (const frame of this._queue.splice(0))
|
|
4226
|
+
for (const frame of this._queue.splice(0)) this._deliver(frame);
|
|
3779
4227
|
}
|
|
3780
4228
|
send(data) {
|
|
3781
|
-
if (
|
|
4229
|
+
if (this._detached) throw new DOMException("This end was detached; it cannot send.", "InvalidStateError");
|
|
4230
|
+
if (!this._accepted && !this._options.sendBeforeAccept) throw new DOMException("accept() the socket before sending on it.", "InvalidStateError");
|
|
3782
4231
|
this._sendData(isBinary(data) ? toArrayBuffer(data) : String(data));
|
|
3783
4232
|
}
|
|
3784
|
-
close(code
|
|
3785
|
-
|
|
4233
|
+
close(code, reason = "") {
|
|
4234
|
+
this.checkClose(code, reason);
|
|
4235
|
+
this._startClose(code ?? this._options.defaultCloseCode ?? 1e3, reason);
|
|
4236
|
+
}
|
|
4237
|
+
/**
|
|
4238
|
+
* Refuses a `close()` the endpoint may not make: by default a code outside RFC 6455 §7.4's
|
|
4239
|
+
* sendable ones, or a reason over 123 bytes. A subclass with another platform's rules
|
|
4240
|
+
* overrides it.
|
|
4241
|
+
*/
|
|
4242
|
+
checkClose(code, reason) {
|
|
4243
|
+
if (code !== void 0 && !isSendableCloseCode(code)) throw new DOMException(`${code} is not a close code an endpoint may send.`, "InvalidAccessError");
|
|
3786
4244
|
if (utf8Length(reason) > 123) throw new DOMException("The close reason is longer than 123 bytes.", "SyntaxError");
|
|
3787
|
-
this._startClose(code, reason);
|
|
3788
4245
|
}
|
|
3789
4246
|
_deliver(frame) {
|
|
3790
4247
|
if (!this._accepted) {
|
|
3791
4248
|
this._queue.push(frame);
|
|
3792
4249
|
return;
|
|
3793
4250
|
}
|
|
4251
|
+
if (frame.t === "m" && this._state === 2 && this._options.deliverWhileClosing) {
|
|
4252
|
+
this._dispatchMessage(frame.d);
|
|
4253
|
+
this._consumed(messageSize(frame.d));
|
|
4254
|
+
return;
|
|
4255
|
+
}
|
|
3794
4256
|
super._deliver(frame);
|
|
3795
4257
|
}
|
|
3796
4258
|
_dispatchMessage(data) {
|
|
3797
|
-
this.
|
|
4259
|
+
const delivered = typeof data !== "string" && this._binaryType === "blob" ? new Blob([data]) : data;
|
|
4260
|
+
this.dispatchEvent(new MessageEvent("message", { data: delivered }));
|
|
3798
4261
|
}
|
|
3799
4262
|
_onPing(hb) {
|
|
3800
4263
|
this._post({ t: "q" });
|
|
3801
4264
|
this._lastPing = Date.now();
|
|
3802
|
-
if (
|
|
4265
|
+
if (this._state === 3) return;
|
|
4266
|
+
const keep = this._options.heartbeatPeriod === "first" && this._pinged;
|
|
4267
|
+
if (!(hb > 0 && hb < 2 ** 31 - 1)) {
|
|
4268
|
+
if (!keep && this._watch !== void 0) {
|
|
4269
|
+
this._timers.clearInterval(this._watch);
|
|
4270
|
+
this._watch = void 0;
|
|
4271
|
+
}
|
|
4272
|
+
return;
|
|
4273
|
+
}
|
|
4274
|
+
this._pinged = true;
|
|
4275
|
+
if (!keep && (this._watch === void 0 || hb !== this._heartbeatMs)) {
|
|
3803
4276
|
this._heartbeatMs = hb;
|
|
3804
4277
|
this._watchHeartbeat();
|
|
3805
4278
|
}
|
|
@@ -3821,6 +4294,11 @@ function isSendableCloseCode(code) {
|
|
|
3821
4294
|
if (code >= 3e3 && code <= 4999) return true;
|
|
3822
4295
|
return code >= 1e3 && code <= 1014 && code !== 1004 && code !== 1005 && code !== 1006;
|
|
3823
4296
|
}
|
|
4297
|
+
/** The buffers a relayed frame carries, to transfer rather than copy. */
|
|
4298
|
+
function transferables(data) {
|
|
4299
|
+
const d = data?.d;
|
|
4300
|
+
return d instanceof ArrayBuffer ? [d] : [];
|
|
4301
|
+
}
|
|
3824
4302
|
//#endregion
|
|
3825
4303
|
//#region src/ws/upgrade.ts
|
|
3826
4304
|
/**
|
|
@@ -3852,6 +4330,7 @@ function newWebSocketUpgradeRequest(url, { protocols = [], headers } = {}) {
|
|
|
3852
4330
|
all.set("connection", "Upgrade");
|
|
3853
4331
|
all.set("sec-websocket-version", "13");
|
|
3854
4332
|
if (!all.has("sec-websocket-key")) all.set("sec-websocket-key", randomKey());
|
|
4333
|
+
all.delete("sec-websocket-protocol");
|
|
3855
4334
|
if (protocols.length > 0) all.set("sec-websocket-protocol", protocols.join(", "));
|
|
3856
4335
|
if (!all.has("x-webrun-ws-version")) all.set(WS_VERSION_HEADER, `2`);
|
|
3857
4336
|
return withHeaders(new Request(httpUrl, {
|
|
@@ -3911,6 +4390,23 @@ function upgradeWebSocket(request, options = {}) {
|
|
|
3911
4390
|
url: request.url,
|
|
3912
4391
|
protocol
|
|
3913
4392
|
});
|
|
4393
|
+
const response = webSocketUpgradeResponse(channel.port2, protocol);
|
|
4394
|
+
Object.defineProperty(response, "webSocket", {
|
|
4395
|
+
value: socket,
|
|
4396
|
+
configurable: true
|
|
4397
|
+
});
|
|
4398
|
+
return {
|
|
4399
|
+
socket,
|
|
4400
|
+
response
|
|
4401
|
+
};
|
|
4402
|
+
}
|
|
4403
|
+
/**
|
|
4404
|
+
* A 101 for a socket whose server end the handler runs itself, on the other end of `port`
|
|
4405
|
+
* (a server of its own over this package's frames, say): what `upgradeWebSocket` returns, minus
|
|
4406
|
+
* the `ServerWebSocket`. The host hands `port` to the caller as it would for `upgradeWebSocket`
|
|
4407
|
+
* (`takePort`), and `protocol` (`""` for none) is the subprotocol picked.
|
|
4408
|
+
*/
|
|
4409
|
+
function webSocketUpgradeResponse(port, protocol = "") {
|
|
3914
4410
|
const response = new Response(null, {
|
|
3915
4411
|
status: 200,
|
|
3916
4412
|
headers: protocol ? { "sec-websocket-protocol": protocol } : {}
|
|
@@ -3924,23 +4420,16 @@ function upgradeWebSocket(request, options = {}) {
|
|
|
3924
4420
|
value: "Switching Protocols",
|
|
3925
4421
|
configurable: true
|
|
3926
4422
|
},
|
|
3927
|
-
webSocket: {
|
|
3928
|
-
value: socket,
|
|
3929
|
-
configurable: true
|
|
3930
|
-
},
|
|
3931
4423
|
[UPGRADE]: {
|
|
3932
4424
|
value: {
|
|
3933
|
-
port
|
|
4425
|
+
port,
|
|
3934
4426
|
protocol
|
|
3935
4427
|
},
|
|
3936
4428
|
configurable: true,
|
|
3937
4429
|
writable: true
|
|
3938
4430
|
}
|
|
3939
4431
|
});
|
|
3940
|
-
return
|
|
3941
|
-
socket,
|
|
3942
|
-
response
|
|
3943
|
-
};
|
|
4432
|
+
return response;
|
|
3944
4433
|
}
|
|
3945
4434
|
/**
|
|
3946
4435
|
* The port to hand to the caller, for a response from `upgradeWebSocket`.
|
|
@@ -4008,6 +4497,51 @@ function randomKey() {
|
|
|
4008
4497
|
return btoa(String.fromCharCode(...bytes));
|
|
4009
4498
|
}
|
|
4010
4499
|
//#endregion
|
|
4500
|
+
//#region src/sw/protocol.ts
|
|
4501
|
+
/**
|
|
4502
|
+
* The messages between pages, Dedicated Workers and the same-origin
|
|
4503
|
+
* ServiceWorker. Internal: the shapes may change between versions, which is
|
|
4504
|
+
* why every hand-shake carries `HTTP_LINK_PROTOCOL`.
|
|
4505
|
+
*
|
|
4506
|
+
* THREE PORTS PER HANDLER LINK. A page makes the channels and hands one end of
|
|
4507
|
+
* each to the handler (itself, or a worker) and the other to the
|
|
4508
|
+
* ServiceWorker; after that it carries nothing:
|
|
4509
|
+
*
|
|
4510
|
+
* - `down`: ServiceWorker → handler, the requests the handler serves;
|
|
4511
|
+
* - `ctl`: a raw port next to them, for what HTTP over a stream cannot carry,
|
|
4512
|
+
* transferred `MessagePort`s (WebSocket hand-over);
|
|
4513
|
+
* - `up` (workers only): handler → ServiceWorker, the requests the handler
|
|
4514
|
+
* sends through the router (to other handlers).
|
|
4515
|
+
*/
|
|
4516
|
+
/** Page → ServiceWorker: attach a handler link. The name is the one version 1 used. */
|
|
4517
|
+
const ATTACH_CALL = "UPDATE_COMMUNICATION_PORT";
|
|
4518
|
+
/** Page → ServiceWorker: remove a handler link now. */
|
|
4519
|
+
const DETACH_CALL = "webrun-http:detach";
|
|
4520
|
+
/** Page → ServiceWorker: stop routing to a link, let its requests finish, then remove it. */
|
|
4521
|
+
const DRAIN_CALL = "webrun-http:drain";
|
|
4522
|
+
/** Page → ServiceWorker: open a WebSocket to a handler. */
|
|
4523
|
+
const WS_CONNECT_CALL = "webrun-http:ws-connect";
|
|
4524
|
+
/** ServiceWorker → pages: attach your links again (the ServiceWorker restarted). */
|
|
4525
|
+
const NEED_LINKS_CALL = "webrun-http:need-links";
|
|
4526
|
+
/**
|
|
4527
|
+
* Page → ServiceWorker, plain messages (no reply). A message to the
|
|
4528
|
+
* ServiceWorker object is an event: it starts a stopped worker and resets its
|
|
4529
|
+
* idle timer. Port messages do neither.
|
|
4530
|
+
*/
|
|
4531
|
+
const WAKE_MESSAGE = "webrun-http:wake";
|
|
4532
|
+
const KEEPALIVE_MESSAGE = "webrun-http:keepalive";
|
|
4533
|
+
/** Page ↔ worker: what `attachWorker` and `serveHttpInWorker` exchange. */
|
|
4534
|
+
const WORKER_HELLO = "webrun-http:worker-hello";
|
|
4535
|
+
/** `headers` as pairs a `postMessage` can carry (a `Headers` object cannot be cloned). */
|
|
4536
|
+
function headerPairs(headers) {
|
|
4537
|
+
if (headers === void 0) return void 0;
|
|
4538
|
+
const pairs = [];
|
|
4539
|
+
new Headers(headers).forEach((value, name) => {
|
|
4540
|
+
pairs.push([name, value]);
|
|
4541
|
+
});
|
|
4542
|
+
return pairs;
|
|
4543
|
+
}
|
|
4544
|
+
//#endregion
|
|
4011
4545
|
//#region src/sw/handler-endpoint.ts
|
|
4012
4546
|
/** The message calls through a lost ServiceWorker fail with. */
|
|
4013
4547
|
const ROUTER_LOST_MESSAGE = "The ServiceWorker was lost while this call was in flight";
|
|
@@ -4091,8 +4625,11 @@ var HandlerEndpoint = class {
|
|
|
4091
4625
|
this._startKeepAlive();
|
|
4092
4626
|
return await link.up.send(request);
|
|
4093
4627
|
}
|
|
4094
|
-
/**
|
|
4095
|
-
|
|
4628
|
+
/**
|
|
4629
|
+
* Opens a socket to the handler `url` routes to, through the ServiceWorker. The handler sees
|
|
4630
|
+
* `headers` on the upgrade request, and so does a `route` function choosing it.
|
|
4631
|
+
*/
|
|
4632
|
+
async connectWebSocket(url, protocols = [], headers) {
|
|
4096
4633
|
const link = await this._linkWith("ctl");
|
|
4097
4634
|
const id = this._sockets.newId();
|
|
4098
4635
|
const answer = this._sockets.wait(id, this.options.wsConnectTimeoutMs ?? 15e3, `the socket request for ${url}`);
|
|
@@ -4100,7 +4637,8 @@ var HandlerEndpoint = class {
|
|
|
4100
4637
|
t: "ws-open",
|
|
4101
4638
|
id,
|
|
4102
4639
|
url,
|
|
4103
|
-
protocols
|
|
4640
|
+
protocols,
|
|
4641
|
+
headers: headerPairs(headers)
|
|
4104
4642
|
});
|
|
4105
4643
|
return await answer;
|
|
4106
4644
|
}
|
|
@@ -4519,8 +5057,9 @@ async function awaitServiceWorkerControl(registration, { timeout = DEFAULT_SERVI
|
|
|
4519
5057
|
* Returns a function that stops answering.
|
|
4520
5058
|
*/
|
|
4521
5059
|
function handleClaimRequests(self) {
|
|
4522
|
-
|
|
4523
|
-
|
|
5060
|
+
const sw = self;
|
|
5061
|
+
return handleChannelCalls(sw, CLAIM_CALL, (event) => {
|
|
5062
|
+
const claimed = sw.clients.claim().then(() => true);
|
|
4524
5063
|
event.waitUntil?.(claimed);
|
|
4525
5064
|
return claimed;
|
|
4526
5065
|
});
|
|
@@ -4792,43 +5331,22 @@ async function sendHttpRequest(communicationPort, request) {
|
|
|
4792
5331
|
})(request);
|
|
4793
5332
|
}
|
|
4794
5333
|
//#endregion
|
|
4795
|
-
//#region src/sw/
|
|
4796
|
-
/**
|
|
4797
|
-
* The messages between pages, Dedicated Workers and the same-origin
|
|
4798
|
-
* ServiceWorker. Internal: the shapes may change between versions, which is
|
|
4799
|
-
* why every hand-shake carries `HTTP_LINK_PROTOCOL`.
|
|
4800
|
-
*
|
|
4801
|
-
* THREE PORTS PER HANDLER LINK. A page makes the channels and hands one end of
|
|
4802
|
-
* each to the handler (itself, or a worker) and the other to the
|
|
4803
|
-
* ServiceWorker; after that it carries nothing:
|
|
4804
|
-
*
|
|
4805
|
-
* - `down`: ServiceWorker → handler, the requests the handler serves;
|
|
4806
|
-
* - `ctl`: a raw port next to them, for what HTTP over a stream cannot carry,
|
|
4807
|
-
* transferred `MessagePort`s (WebSocket hand-over);
|
|
4808
|
-
* - `up` (workers only): handler → ServiceWorker, the requests the handler
|
|
4809
|
-
* sends through the router (to other handlers).
|
|
4810
|
-
*/
|
|
4811
|
-
/** Page → ServiceWorker: attach a handler link. The name is the one version 1 used. */
|
|
4812
|
-
const ATTACH_CALL = "UPDATE_COMMUNICATION_PORT";
|
|
4813
|
-
/** Page → ServiceWorker: remove a handler link now. */
|
|
4814
|
-
const DETACH_CALL = "webrun-http:detach";
|
|
4815
|
-
/** Page → ServiceWorker: stop routing to a link, let its requests finish, then remove it. */
|
|
4816
|
-
const DRAIN_CALL = "webrun-http:drain";
|
|
4817
|
-
/** Page → ServiceWorker: open a WebSocket to a handler. */
|
|
4818
|
-
const WS_CONNECT_CALL = "webrun-http:ws-connect";
|
|
4819
|
-
/** ServiceWorker → pages: attach your links again (the ServiceWorker restarted). */
|
|
4820
|
-
const NEED_LINKS_CALL = "webrun-http:need-links";
|
|
5334
|
+
//#region src/sw/http-sw-dispatcher.ts
|
|
4821
5335
|
/**
|
|
4822
|
-
*
|
|
4823
|
-
*
|
|
4824
|
-
* idle timer. Port messages do neither.
|
|
5336
|
+
* The default key: the url's first path segment under `scope`, decoded; none outside it, at the
|
|
5337
|
+
* scope itself, or for a segment that does not decode.
|
|
4825
5338
|
*/
|
|
4826
|
-
|
|
4827
|
-
const
|
|
4828
|
-
|
|
4829
|
-
const
|
|
4830
|
-
|
|
4831
|
-
|
|
5339
|
+
function scopeKey(url, scope) {
|
|
5340
|
+
const href = `${url}`;
|
|
5341
|
+
if (!href.startsWith(scope)) return void 0;
|
|
5342
|
+
const key = href.substring(scope.length).replace(/[?#].*$/, "").split("/")[0];
|
|
5343
|
+
if (!key) return void 0;
|
|
5344
|
+
try {
|
|
5345
|
+
return decodeURIComponent(key);
|
|
5346
|
+
} catch {
|
|
5347
|
+
return;
|
|
5348
|
+
}
|
|
5349
|
+
}
|
|
4832
5350
|
/** One attached handler link, as the ServiceWorker holds it. */
|
|
4833
5351
|
var HandlerEntry = class {
|
|
4834
5352
|
key;
|
|
@@ -4868,7 +5386,8 @@ const TIMED_OUT = Symbol("timed-out");
|
|
|
4868
5386
|
* (`SwHttpAdapter.register`) or in Dedicated Workers
|
|
4869
5387
|
* (`SwHttpAdapter.attachWorker` + `serveHttpInWorker`).
|
|
4870
5388
|
*
|
|
4871
|
-
* A url `<scope>/<key>/...` belongs to the handlers of `key
|
|
5389
|
+
* A url `<scope>/<key>/...` belongs to the handlers of `key` (the `key`
|
|
5390
|
+
* option maps urls to keys some other way). Of those, the
|
|
4872
5391
|
* `route` option picks one per request. Requests and responses stream with
|
|
4873
5392
|
* backpressure in both directions, and an aborted fetch reaches the handler
|
|
4874
5393
|
* as a cancellation (webrun-rpc + webrun-http-streams).
|
|
@@ -4887,6 +5406,8 @@ const TIMED_OUT = Symbol("timed-out");
|
|
|
4887
5406
|
*/
|
|
4888
5407
|
var SwHttpDispatcher = class {
|
|
4889
5408
|
self;
|
|
5409
|
+
/** `self`, with the WebWorker lib's types this module compiles with. */
|
|
5410
|
+
_sw;
|
|
4890
5411
|
log;
|
|
4891
5412
|
options;
|
|
4892
5413
|
table = new HandlerTable();
|
|
@@ -4909,19 +5430,22 @@ var SwHttpDispatcher = class {
|
|
|
4909
5430
|
constructor(options) {
|
|
4910
5431
|
this.options = options;
|
|
4911
5432
|
this.self = options.self;
|
|
5433
|
+
this._sw = options.self;
|
|
4912
5434
|
this.log = options.log ?? (() => {});
|
|
4913
5435
|
if (locksAvailable()) this.routerLock = newLockName("webrun-http-router");
|
|
4914
5436
|
}
|
|
4915
5437
|
get scope() {
|
|
4916
|
-
return `${new URL("./", this.
|
|
5438
|
+
return this._sw.registration?.scope ?? `${new URL("./", this._sw.location.href)}`;
|
|
4917
5439
|
}
|
|
4918
|
-
/** The key `url` belongs to
|
|
5440
|
+
/** The key `url` belongs to (the `key` option; by default its first path segment under the scope). */
|
|
4919
5441
|
keyOf(url) {
|
|
4920
|
-
|
|
4921
|
-
|
|
4922
|
-
|
|
4923
|
-
|
|
4924
|
-
|
|
5442
|
+
let parsed;
|
|
5443
|
+
try {
|
|
5444
|
+
parsed = new URL(`${url}`);
|
|
5445
|
+
} catch {
|
|
5446
|
+
return;
|
|
5447
|
+
}
|
|
5448
|
+
return (this.options.key ?? scopeKey)(parsed, this.scope);
|
|
4925
5449
|
}
|
|
4926
5450
|
/** Whether `key` has ever been served on this worker. */
|
|
4927
5451
|
isClaimedKey(key) {
|
|
@@ -4932,7 +5456,7 @@ var SwHttpDispatcher = class {
|
|
|
4932
5456
|
return this.claimedKeys.has(key) || this.table.all(key).length > 0;
|
|
4933
5457
|
}
|
|
4934
5458
|
start() {
|
|
4935
|
-
const self = this.
|
|
5459
|
+
const self = this._sw;
|
|
4936
5460
|
if (this.routerLock) this._routerHeld = holdLock(this.routerLock).then(() => {});
|
|
4937
5461
|
this._restored = this._restoreClaimedKeys();
|
|
4938
5462
|
const latch = () => {
|
|
@@ -4952,8 +5476,8 @@ var SwHttpDispatcher = class {
|
|
|
4952
5476
|
return true;
|
|
4953
5477
|
}), handleChannelCalls(self, WS_CONNECT_CALL, async (event, params) => {
|
|
4954
5478
|
if (!this._isOwnClient(event)) throw new Error("Only this origin's pages may open sockets");
|
|
4955
|
-
const { url, protocols } = params;
|
|
4956
|
-
const connection = await this.connectWebSocket(url, protocols ?? []);
|
|
5479
|
+
const { url, protocols, headers } = params;
|
|
5480
|
+
const connection = await this.connectWebSocket(url, protocols ?? [], this._socketHeaders(headers, "page"));
|
|
4957
5481
|
return withTransfer(connection, [connection.port]);
|
|
4958
5482
|
}));
|
|
4959
5483
|
const onMessage = (event) => {
|
|
@@ -4971,10 +5495,10 @@ var SwHttpDispatcher = class {
|
|
|
4971
5495
|
this.log("Claim control over all clients.");
|
|
4972
5496
|
event.waitUntil(self.clients.claim());
|
|
4973
5497
|
};
|
|
4974
|
-
const onFetch = (event) => this.
|
|
5498
|
+
const onFetch = (event) => void this.handleFetch(event);
|
|
4975
5499
|
self.addEventListener("install", onInstall);
|
|
4976
5500
|
self.addEventListener("activate", onActivate);
|
|
4977
|
-
self.addEventListener("fetch", onFetch);
|
|
5501
|
+
if (this.options.handleFetch !== false) self.addEventListener("fetch", onFetch);
|
|
4978
5502
|
this._cleanup.push(() => {
|
|
4979
5503
|
self.removeEventListener("install", onInstall);
|
|
4980
5504
|
self.removeEventListener("activate", onActivate);
|
|
@@ -4989,37 +5513,60 @@ var SwHttpDispatcher = class {
|
|
|
4989
5513
|
}
|
|
4990
5514
|
/**
|
|
4991
5515
|
* Answers `request` as a fetch under the scope would be: through the
|
|
4992
|
-
* handler its key routes to. A url
|
|
4993
|
-
* worker never served, goes to the network.
|
|
5516
|
+
* handler its key routes to. A url with no key (by default, one outside the
|
|
5517
|
+
* scope), or of a key this worker never served, goes to the network.
|
|
4994
5518
|
*/
|
|
4995
5519
|
async dispatch(request) {
|
|
4996
5520
|
const key = this.keyOf(request.url);
|
|
4997
5521
|
if (key === void 0) return await fetch(request);
|
|
4998
5522
|
await this._restored.catch(() => {});
|
|
4999
5523
|
const entry = await this._acquire(request, key);
|
|
5000
|
-
if (entry === TIMED_OUT) return
|
|
5001
|
-
status: 503,
|
|
5002
|
-
statusText: "Service Unavailable (no handler attached in time)"
|
|
5003
|
-
});
|
|
5524
|
+
if (entry === TIMED_OUT) return await this._failed(request, { kind: "timeout" });
|
|
5004
5525
|
if (!entry) {
|
|
5005
5526
|
if (!this.serves(key)) return await fetch(request);
|
|
5006
|
-
return
|
|
5007
|
-
status: 404,
|
|
5008
|
-
statusText: "Not Found (no active handler)"
|
|
5009
|
-
});
|
|
5527
|
+
return await this._failed(request, { kind: "gone" });
|
|
5010
5528
|
}
|
|
5011
5529
|
try {
|
|
5012
5530
|
return await entry.link.send(request);
|
|
5013
5531
|
} catch (error) {
|
|
5014
5532
|
this.log("[dispatch] failed", key, error);
|
|
5015
|
-
return
|
|
5016
|
-
|
|
5017
|
-
|
|
5018
|
-
headers: { "content-type": "text/plain; charset=utf-8" }
|
|
5533
|
+
return await this._failed(request, {
|
|
5534
|
+
kind: "lost",
|
|
5535
|
+
error
|
|
5019
5536
|
});
|
|
5020
5537
|
}
|
|
5021
5538
|
}
|
|
5022
5539
|
/**
|
|
5540
|
+
* After `start()`: settles once the keys this worker served before a restart are read back
|
|
5541
|
+
* (whatever the outcome). Until then `serves()` may answer `false` for a key it will serve.
|
|
5542
|
+
*/
|
|
5543
|
+
get ready() {
|
|
5544
|
+
return this._restored.then(() => {}, () => {});
|
|
5545
|
+
}
|
|
5546
|
+
_socketHeaders(headers, from) {
|
|
5547
|
+
return this.options.socketHeaders ? this.options.socketHeaders(headers, from) : headers;
|
|
5548
|
+
}
|
|
5549
|
+
async _failed(request, failure) {
|
|
5550
|
+
if (this.options.failure) try {
|
|
5551
|
+
return await this.options.failure(request, failure);
|
|
5552
|
+
} catch (error) {
|
|
5553
|
+
this.log("[dispatch] the failure option threw; answering by default", error);
|
|
5554
|
+
}
|
|
5555
|
+
if (failure.kind === "timeout") return new Response(null, {
|
|
5556
|
+
status: 503,
|
|
5557
|
+
statusText: "Service Unavailable (no handler attached in time)"
|
|
5558
|
+
});
|
|
5559
|
+
if (failure.kind === "gone") return new Response(null, {
|
|
5560
|
+
status: 404,
|
|
5561
|
+
statusText: "Not Found (no active handler)"
|
|
5562
|
+
});
|
|
5563
|
+
return new Response(errorMessage(failure.error), {
|
|
5564
|
+
status: 502,
|
|
5565
|
+
statusText: "Bad Gateway",
|
|
5566
|
+
headers: { "content-type": "text/plain; charset=utf-8" }
|
|
5567
|
+
});
|
|
5568
|
+
}
|
|
5569
|
+
/**
|
|
5023
5570
|
* Opens a socket to the handler `url` routes to: the handler accepts or
|
|
5024
5571
|
* refuses it like an HTTP upgrade, and the socket's port comes back. After
|
|
5025
5572
|
* that the socket runs between the caller and the handler directly; this
|
|
@@ -5031,7 +5578,7 @@ var SwHttpDispatcher = class {
|
|
|
5031
5578
|
headers
|
|
5032
5579
|
});
|
|
5033
5580
|
const key = this.keyOf(request.url);
|
|
5034
|
-
if (key === void 0) throw new Error(`${url}
|
|
5581
|
+
if (key === void 0) throw new Error(`${url} belongs to no key of this ServiceWorker`);
|
|
5035
5582
|
await this._restored.catch(() => {});
|
|
5036
5583
|
const entry = await this._acquire(request, key);
|
|
5037
5584
|
if (!entry || entry === TIMED_OUT) throw new Error(`No handler serves ${url}`);
|
|
@@ -5047,20 +5594,47 @@ var SwHttpDispatcher = class {
|
|
|
5047
5594
|
});
|
|
5048
5595
|
return await answer;
|
|
5049
5596
|
}
|
|
5050
|
-
|
|
5051
|
-
|
|
5597
|
+
/**
|
|
5598
|
+
* Answers `event` if it is the dispatcher's, with `request` (default `event.request`; a host
|
|
5599
|
+
* may pass a rewritten one, with headers it adds). Returns whether it called `respondWith`;
|
|
5600
|
+
* when it did not, the event is the caller's to answer or to leave to the network. For a host
|
|
5601
|
+
* that listens to `fetch` itself (`handleFetch: false`); call it synchronously in the event's
|
|
5602
|
+
* listener, as `respondWith` must be.
|
|
5603
|
+
*
|
|
5604
|
+
* It takes a same-origin request whose key (the `key` option) a handler serves, and also:
|
|
5605
|
+
* - a navigation under the scope whose key nobody serves (answered by `unserved`);
|
|
5606
|
+
* - before the stored keys are read (the first requests after a restart), every request with
|
|
5607
|
+
* a key, since only then is it known whose it is: one nobody serves gets what it would have
|
|
5608
|
+
* got after, `unserved` for a navigation and the network for anything else.
|
|
5609
|
+
* So a host with a page of its own for urls nobody serves gives it as `unserved`, rather than
|
|
5610
|
+
* deciding before calling this.
|
|
5611
|
+
*/
|
|
5612
|
+
handleFetch(event, request = event.request) {
|
|
5613
|
+
const navigation = event.request.mode === "navigate";
|
|
5614
|
+
if (new URL(request.url).origin !== this._sw.location.origin) return false;
|
|
5052
5615
|
const key = this.keyOf(request.url);
|
|
5053
|
-
if (key === void 0) return;
|
|
5616
|
+
if (key === void 0) return false;
|
|
5054
5617
|
if (this._restoreSettled) {
|
|
5055
|
-
if (this.serves(key))
|
|
5056
|
-
|
|
5057
|
-
|
|
5618
|
+
if (this.serves(key)) {
|
|
5619
|
+
event.respondWith(this.dispatch(request));
|
|
5620
|
+
return true;
|
|
5621
|
+
}
|
|
5622
|
+
if (navigation) {
|
|
5623
|
+
event.respondWith(this._unserved(request));
|
|
5624
|
+
return true;
|
|
5625
|
+
}
|
|
5626
|
+
return false;
|
|
5058
5627
|
}
|
|
5059
5628
|
event.respondWith((async () => {
|
|
5060
5629
|
await this._restored.catch(() => {});
|
|
5061
|
-
if (
|
|
5062
|
-
return await this.
|
|
5630
|
+
if (this.serves(key)) return await this.dispatch(request);
|
|
5631
|
+
return navigation ? await this._unserved(request) : await fetch(request);
|
|
5063
5632
|
})());
|
|
5633
|
+
return true;
|
|
5634
|
+
}
|
|
5635
|
+
/** What a request the dispatcher took, and no handler serves, gets: `unserved`, or the network. */
|
|
5636
|
+
async _unserved(request) {
|
|
5637
|
+
return await (this.options.unserved ?? fetch)(request);
|
|
5064
5638
|
}
|
|
5065
5639
|
async _attach(event, params, ports) {
|
|
5066
5640
|
const clientId = event.source?.id;
|
|
@@ -5079,8 +5653,11 @@ var SwHttpDispatcher = class {
|
|
|
5079
5653
|
}
|
|
5080
5654
|
const ctl = params.ctl ? rest.shift() : void 0;
|
|
5081
5655
|
const up = params.up ? rest.shift() : void 0;
|
|
5082
|
-
const streamTimeout = this.options
|
|
5083
|
-
const link = await connectHttpLink(down, {
|
|
5656
|
+
const { streamTimeout, window } = this.options;
|
|
5657
|
+
const link = await connectHttpLink(down, {
|
|
5658
|
+
streamTimeout,
|
|
5659
|
+
window
|
|
5660
|
+
});
|
|
5084
5661
|
const entry = new HandlerEntry(params.key, params.id ?? clientId ?? crypto.randomUUID(), params.owner ?? crypto.randomUUID(), params.meta, link, ctl, clientId, params.ownerLock);
|
|
5085
5662
|
if (up) entry.closeUp = await serveHttpLink(up, (request) => this.dispatch(request), { streamTimeout });
|
|
5086
5663
|
if (ctl) ctl.onmessage = (message) => this._onControl(entry, message.data);
|
|
@@ -5159,20 +5736,35 @@ var SwHttpDispatcher = class {
|
|
|
5159
5736
|
* its key, waiting for one when there is none.
|
|
5160
5737
|
*/
|
|
5161
5738
|
async _acquire(request, key) {
|
|
5739
|
+
const deadline = Date.now() + (this.options.attachTimeoutMs ?? 15e3);
|
|
5162
5740
|
const picked = await this._pick(request, key);
|
|
5163
5741
|
if (picked) return picked;
|
|
5164
|
-
if (this.table.all(key).length
|
|
5165
|
-
|
|
5166
|
-
|
|
5167
|
-
|
|
5168
|
-
|
|
5169
|
-
|
|
5742
|
+
if (this.table.all(key).length === 0) {
|
|
5743
|
+
const vacantUntil = this._vacant.get(key) ?? 0;
|
|
5744
|
+
if (vacantUntil > Date.now()) return await this._waitToPick(request, key, vacantUntil);
|
|
5745
|
+
if (!this.serves(key)) return void 0;
|
|
5746
|
+
await this._askForLinks(key);
|
|
5747
|
+
const relinked = await this._pick(request, key);
|
|
5748
|
+
if (relinked) return relinked;
|
|
5749
|
+
if (this.table.all(key).length === 0) return void 0;
|
|
5750
|
+
}
|
|
5751
|
+
return await this._waitToPick(request, key, deadline);
|
|
5752
|
+
}
|
|
5753
|
+
/** Waits for attaches until `route` accepts a handler, or `until` passes. */
|
|
5754
|
+
async _waitToPick(request, key, until) {
|
|
5755
|
+
for (;;) {
|
|
5756
|
+
const left = until - Date.now();
|
|
5757
|
+
if (left <= 0) return TIMED_OUT;
|
|
5758
|
+
await this.table.waitFor(key, Math.min(left, 1e3), this.table.live(key).length > 0);
|
|
5759
|
+
const picked = await this._pick(request, key);
|
|
5760
|
+
if (picked) return picked;
|
|
5761
|
+
}
|
|
5170
5762
|
}
|
|
5171
5763
|
async _pick(request, key) {
|
|
5172
5764
|
let live = this.table.live(key);
|
|
5173
5765
|
for (const entry of live) {
|
|
5174
5766
|
if (entry.ownerLock || !entry.clientId) continue;
|
|
5175
|
-
if (!await this.
|
|
5767
|
+
if (!await this._sw.clients.get(entry.clientId)) this._close(entry, new LinkLostError(HANDLER_LOST_MESSAGE));
|
|
5176
5768
|
}
|
|
5177
5769
|
live = this.table.live(key);
|
|
5178
5770
|
if (live.length === 0) return void 0;
|
|
@@ -5196,7 +5788,7 @@ var SwHttpDispatcher = class {
|
|
|
5196
5788
|
}
|
|
5197
5789
|
async _linkRound(key) {
|
|
5198
5790
|
const timeout = this.options.relinkTimeoutMs ?? 3e3;
|
|
5199
|
-
const windows = await this.
|
|
5791
|
+
const windows = await this._sw.clients.matchAll({
|
|
5200
5792
|
type: "window",
|
|
5201
5793
|
includeUncontrolled: true
|
|
5202
5794
|
});
|
|
@@ -5234,7 +5826,7 @@ var SwHttpDispatcher = class {
|
|
|
5234
5826
|
entry.sockets.fail(message.id, new Error(message.message));
|
|
5235
5827
|
return;
|
|
5236
5828
|
case "ws-open":
|
|
5237
|
-
this.connectWebSocket(message.url, message.protocols).then(({ port, protocol }) => entry.ctl?.postMessage({
|
|
5829
|
+
this.connectWebSocket(message.url, message.protocols, this._socketHeaders(message.headers, "handler")).then(({ port, protocol }) => entry.ctl?.postMessage({
|
|
5238
5830
|
t: "ws-opened",
|
|
5239
5831
|
id: message.id,
|
|
5240
5832
|
protocol,
|
|
@@ -5251,7 +5843,7 @@ var SwHttpDispatcher = class {
|
|
|
5251
5843
|
const source = event.source;
|
|
5252
5844
|
if (!source?.url) return false;
|
|
5253
5845
|
try {
|
|
5254
|
-
return new URL(source.url).origin === this.
|
|
5846
|
+
return new URL(source.url).origin === this._sw.location.origin;
|
|
5255
5847
|
} catch {
|
|
5256
5848
|
return false;
|
|
5257
5849
|
}
|
|
@@ -5275,6 +5867,7 @@ var SwHttpDispatcher = class {
|
|
|
5275
5867
|
* that stops it.
|
|
5276
5868
|
*/
|
|
5277
5869
|
function startHttpDispatcher(options) {
|
|
5870
|
+
if (options.handleFetch === false) throw new Error("startHttpDispatcher: handleFetch: false needs a dispatcher to call handleFetch on; construct it instead: new SwHttpDispatcher({ self, handleFetch: false }).start()");
|
|
5278
5871
|
const dispatcher = new SwHttpDispatcher(options);
|
|
5279
5872
|
dispatcher.start();
|
|
5280
5873
|
return () => {
|
|
@@ -5449,6 +6042,8 @@ var SwPortHandler = class {
|
|
|
5449
6042
|
*/
|
|
5450
6043
|
var SwPortDispatcher = class {
|
|
5451
6044
|
self;
|
|
6045
|
+
/** `self`, with the WebWorker lib's types this module compiles with. */
|
|
6046
|
+
_sw;
|
|
5452
6047
|
log;
|
|
5453
6048
|
handlersIndex = /* @__PURE__ */ new Map();
|
|
5454
6049
|
/**
|
|
@@ -5462,10 +6057,11 @@ var SwPortDispatcher = class {
|
|
|
5462
6057
|
_activationPromise;
|
|
5463
6058
|
constructor({ self, log = () => {} }) {
|
|
5464
6059
|
this.self = self;
|
|
6060
|
+
this._sw = self;
|
|
5465
6061
|
this.log = log;
|
|
5466
6062
|
}
|
|
5467
6063
|
get scope() {
|
|
5468
|
-
return `${new URL("./", this.
|
|
6064
|
+
return this._sw.registration?.scope ?? `${new URL("./", this._sw.location.href)}`;
|
|
5469
6065
|
}
|
|
5470
6066
|
/**
|
|
5471
6067
|
* Look up the active channel for a key. Returns `undefined` when there
|
|
@@ -5478,7 +6074,7 @@ var SwPortDispatcher = class {
|
|
|
5478
6074
|
if (channelInfo.key !== key) continue;
|
|
5479
6075
|
const clientId = channelInfo.clientId;
|
|
5480
6076
|
if (!clientId) continue;
|
|
5481
|
-
if (!await this.
|
|
6077
|
+
if (!await this._sw.clients.get(clientId)) {
|
|
5482
6078
|
this.handlersIndex.delete(clientId);
|
|
5483
6079
|
continue;
|
|
5484
6080
|
}
|
|
@@ -5490,8 +6086,8 @@ var SwPortDispatcher = class {
|
|
|
5490
6086
|
return this.claimedKeys.has(key);
|
|
5491
6087
|
}
|
|
5492
6088
|
start() {
|
|
5493
|
-
const stopClaims = handleClaimRequests(this.
|
|
5494
|
-
const stopPortUpdates = handleChannelCalls(this.
|
|
6089
|
+
const stopClaims = handleClaimRequests(this._sw);
|
|
6090
|
+
const stopPortUpdates = handleChannelCalls(this._sw, "UPDATE_COMMUNICATION_PORT", async (event, channelInfo, port) => {
|
|
5495
6091
|
const clientId = event.source?.id;
|
|
5496
6092
|
this.log("[UPDATE_COMMUNICATION_PORT]", clientId, channelInfo);
|
|
5497
6093
|
await this._updateChannelInfo({
|
|
@@ -5505,14 +6101,14 @@ var SwPortDispatcher = class {
|
|
|
5505
6101
|
stopClaims();
|
|
5506
6102
|
stopPortUpdates();
|
|
5507
6103
|
};
|
|
5508
|
-
this.
|
|
6104
|
+
this._sw.addEventListener("install", (event) => {
|
|
5509
6105
|
this.log("Skip waiting on install.", event);
|
|
5510
|
-
this.
|
|
6106
|
+
this._sw.skipWaiting();
|
|
5511
6107
|
});
|
|
5512
|
-
this.
|
|
6108
|
+
this._sw.addEventListener("activate", (event) => {
|
|
5513
6109
|
this.log("Claim control over all clients.", event);
|
|
5514
6110
|
event.waitUntil((async () => {
|
|
5515
|
-
await this.
|
|
6111
|
+
await this._sw.clients.claim();
|
|
5516
6112
|
await this._checkActivation();
|
|
5517
6113
|
})());
|
|
5518
6114
|
});
|
|
@@ -5562,7 +6158,7 @@ var SwPortDispatcher = class {
|
|
|
5562
6158
|
for (const clientId of this.handlersIndex.keys()) ids.add(clientId);
|
|
5563
6159
|
const index = /* @__PURE__ */ new Map();
|
|
5564
6160
|
for (const clientId of ids) {
|
|
5565
|
-
const client = await this.
|
|
6161
|
+
const client = await this._sw.clients.get(clientId);
|
|
5566
6162
|
if (client) index.set(clientId, client);
|
|
5567
6163
|
else this.handlersIndex.delete(clientId);
|
|
5568
6164
|
}
|
|
@@ -5842,7 +6438,10 @@ var SwHttpAdapter = class {
|
|
|
5842
6438
|
return this.options.key;
|
|
5843
6439
|
}
|
|
5844
6440
|
get scope() {
|
|
5845
|
-
|
|
6441
|
+
if (this.options.scope) return this.options.scope;
|
|
6442
|
+
const registration = this.options.registration;
|
|
6443
|
+
if (registration) return new URL(registration.scope).pathname;
|
|
6444
|
+
return new URL("./", this.serviceWorkerUrl).pathname;
|
|
5846
6445
|
}
|
|
5847
6446
|
/**
|
|
5848
6447
|
* The scope as a url on the worker's origin. Resolved against the worker's
|
|
@@ -5853,6 +6452,8 @@ var SwHttpAdapter = class {
|
|
|
5853
6452
|
* sits next to it).
|
|
5854
6453
|
*/
|
|
5855
6454
|
get rootUrl() {
|
|
6455
|
+
const registration = this.options.registration;
|
|
6456
|
+
if (registration && !this.options.serviceWorkerUrl) return new URL(this.scope, registration.scope);
|
|
5856
6457
|
const base = this.options.serviceWorkerUrl ? this.serviceWorkerUrl : import.meta.url;
|
|
5857
6458
|
return new URL(this.scope, base);
|
|
5858
6459
|
}
|
|
@@ -5874,7 +6475,10 @@ var SwHttpAdapter = class {
|
|
|
5874
6475
|
*/
|
|
5875
6476
|
async start() {
|
|
5876
6477
|
if (!this._registrationPromise) this._registrationPromise = (async () => {
|
|
5877
|
-
const registration = await navigator.serviceWorker.register(this.serviceWorkerUrl, {
|
|
6478
|
+
const registration = this.options.registration ?? await navigator.serviceWorker.register(this.serviceWorkerUrl, {
|
|
6479
|
+
scope: this.scope,
|
|
6480
|
+
...this.options.type ? { type: this.options.type } : {}
|
|
6481
|
+
});
|
|
5878
6482
|
this._registration = registration;
|
|
5879
6483
|
const stopListening = handleChannelCalls(navigator.serviceWorker, NEED_LINKS_CALL, async (_event, params) => {
|
|
5880
6484
|
await this.relink(params?.routerLock);
|
|
@@ -5904,7 +6508,7 @@ var SwHttpAdapter = class {
|
|
|
5904
6508
|
})();
|
|
5905
6509
|
return this._registrationPromise;
|
|
5906
6510
|
}
|
|
5907
|
-
/** Detaches every link, and unregisters the worker. */
|
|
6511
|
+
/** Detaches every link, and unregisters the worker (unless the page passed its `registration`). */
|
|
5908
6512
|
async stop() {
|
|
5909
6513
|
for (const cleanup of this._cleanup.splice(0)) cleanup();
|
|
5910
6514
|
for (const worker of [...this._workers]) await worker.detach().catch(() => {});
|
|
@@ -5921,7 +6525,7 @@ var SwHttpAdapter = class {
|
|
|
5921
6525
|
this._registration = void 0;
|
|
5922
6526
|
this._registrationPromise = void 0;
|
|
5923
6527
|
try {
|
|
5924
|
-
await registration?.unregister();
|
|
6528
|
+
if (!this.options.registration) await registration?.unregister();
|
|
5925
6529
|
} catch (error) {
|
|
5926
6530
|
console.log("Service Worker unregistration failed: ", error);
|
|
5927
6531
|
}
|
|
@@ -5995,10 +6599,10 @@ var SwHttpAdapter = class {
|
|
|
5995
6599
|
/**
|
|
5996
6600
|
* Opens the transport of a socket to the handler `url` routes to: a handler
|
|
5997
6601
|
* of this page directly, any other through the ServiceWorker. Resolves once
|
|
5998
|
-
* the handler accepted.
|
|
5999
|
-
* `createWebSocketClass(adapter.connectWebSocket)`.
|
|
6602
|
+
* the handler accepted. The handler sees `headers` on the upgrade request. An arrow function,
|
|
6603
|
+
* so it can be handed around: `createWebSocketClass(adapter.connectWebSocket)`.
|
|
6000
6604
|
*/
|
|
6001
|
-
connectWebSocket = async (url, protocols = []) => {
|
|
6605
|
+
connectWebSocket = async (url, protocols = [], headers) => {
|
|
6002
6606
|
const httpUrl = new URL(`${url}`, globalThis.location?.href);
|
|
6003
6607
|
if (httpUrl.protocol === "ws:") httpUrl.protocol = "http:";
|
|
6004
6608
|
else if (httpUrl.protocol === "wss:") httpUrl.protocol = "https:";
|
|
@@ -6006,7 +6610,8 @@ var SwHttpAdapter = class {
|
|
|
6006
6610
|
if (local) {
|
|
6007
6611
|
const result = await serveWebSocketUpgrade(local, {
|
|
6008
6612
|
url: `${httpUrl}`,
|
|
6009
|
-
protocols
|
|
6613
|
+
protocols,
|
|
6614
|
+
headers
|
|
6010
6615
|
});
|
|
6011
6616
|
if (!result.ok) throw new Error(result.message);
|
|
6012
6617
|
return {
|
|
@@ -6017,7 +6622,8 @@ var SwHttpAdapter = class {
|
|
|
6017
6622
|
await this.start();
|
|
6018
6623
|
return await callChannelWithin(this._callTimeout(), this._worker(), WS_CONNECT_CALL, {
|
|
6019
6624
|
url: `${httpUrl}`,
|
|
6020
|
-
protocols
|
|
6625
|
+
protocols,
|
|
6626
|
+
headers: headerPairs(headers)
|
|
6021
6627
|
});
|
|
6022
6628
|
};
|
|
6023
6629
|
/**
|
|
@@ -6313,11 +6919,11 @@ function serveHttpInWorker(handler, options = {}) {
|
|
|
6313
6919
|
};
|
|
6314
6920
|
scope.addEventListener("message", onMessage);
|
|
6315
6921
|
const baseUrl = () => globalThis.location?.href;
|
|
6316
|
-
const connectWebSocket = (url, protocols = []) => {
|
|
6922
|
+
const connectWebSocket = (url, protocols = [], headers) => {
|
|
6317
6923
|
const httpUrl = new URL(`${url}`, baseUrl());
|
|
6318
6924
|
if (httpUrl.protocol === "ws:") httpUrl.protocol = "http:";
|
|
6319
6925
|
else if (httpUrl.protocol === "wss:") httpUrl.protocol = "https:";
|
|
6320
|
-
return endpoint.connectWebSocket(`${httpUrl}`, protocols);
|
|
6926
|
+
return endpoint.connectWebSocket(`${httpUrl}`, protocols, headers);
|
|
6321
6927
|
};
|
|
6322
6928
|
let WebSocketClass;
|
|
6323
6929
|
return {
|
|
@@ -6344,4 +6950,4 @@ function serveHttpInWorker(handler, options = {}) {
|
|
|
6344
6950
|
};
|
|
6345
6951
|
}
|
|
6346
6952
|
//#endregion
|
|
6347
|
-
export { DEFAULT_CLOSE_TIMEOUT_MS, DEFAULT_HEARTBEAT_MS, HANDLER_LOST_MESSAGE, HTTP_LINK_PROTOCOL, HandlerEndpoint, HandlerTable, LinkLostError, PortRendezvous, ROUTER_LOST_MESSAGE, ServerWebSocket, SwHttpAdapter, SwHttpDispatcher, SwPortDispatcher, SwPortHandler, WS_PORT_PROTOCOL_VERSION, WS_VERSION_HEADER, WsCloseEvent, connectHttpLink, connectWebSocketThroughServiceWorker, createWebSocketClass, holdLock, isWebSocketUpgrade, leastInflightRoute, locksAvailable, newLockName, newWebSocketUpgradeRequest, requestedProtocols, serveHttpInWorker, serveHttpLink, serveWebSocketUpgrade, serviceWorkerWebSocketClass, startHttpDispatcher, takePort, takeUpgrade, trackedLink, upgradeWebSocket, watchLock, withHeaders };
|
|
6953
|
+
export { DEFAULT_CLOSE_TIMEOUT_MS, DEFAULT_HEARTBEAT_MS, HANDLER_LOST_MESSAGE, HTTP_LINK_PROTOCOL, HandlerEndpoint, HandlerTable, LinkLostError, PortRendezvous, ROUTER_LOST_MESSAGE, ServerWebSocket, SwHttpAdapter, SwHttpDispatcher, SwPortDispatcher, SwPortHandler, WS_PORT_PROTOCOL_VERSION, WS_VERSION_HEADER, WsCloseEvent, connectHttpLink, connectWebSocketThroughServiceWorker, createWebSocketClass, holdLock, isWebSocketUpgrade, leastInflightRoute, locksAvailable, newLockName, newWebSocketUpgradeRequest, requestedProtocols, scopeKey, serveHttpInWorker, serveHttpLink, serveWebSocketUpgrade, serviceWorkerWebSocketClass, startHttpDispatcher, takePort, takeUpgrade, trackedLink, upgradeWebSocket, watchLock, webSocketUpgradeResponse, withHeaders };
|