@statewalker/webrun-http-browser 0.10.0 → 0.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +155 -10
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/service-worker-control.d.ts +2 -1
- package/dist/core/service-worker-control.d.ts.map +1 -1
- package/dist/core/sw-scope.d.ts +31 -0
- package/dist/core/sw-scope.d.ts.map +1 -0
- package/dist/index.js +816 -211
- 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 +811 -209
- package/dist/worker.js +604 -106
- 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 +3 -2
- package/src/core/index.ts +1 -0
- package/src/core/service-worker-control.ts +5 -3
- package/src/core/sw-scope.ts +27 -0
- package/src/link/http-link.ts +20 -4
- package/src/relay/index-sw.ts +15 -2
- package/src/relay/index.ts +2 -2
- package/src/relay-worker.ts +1 -0
- package/src/sw/handler-endpoint.ts +17 -4
- package/src/sw/http-sw-dispatcher.ts +267 -67
- package/src/sw/index.ts +5 -0
- package/src/sw/protocol.ts +11 -1
- package/src/sw/sw-dispatcher.ts +17 -11
- package/src/sw/sw-http-adapter.ts +36 -10
- package/src/sw/worker-service.ts +11 -4
- package/src/ws/server.ts +166 -11
- package/src/ws/socket-core.ts +7 -2
- package/src/ws/upgrade.ts +22 -4
package/dist/sw.js
CHANGED
|
@@ -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 = "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 ? 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 || 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,
|
|
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
|
});
|
|
@@ -3664,6 +4036,7 @@ var PortSocketCore = class extends EventTarget {
|
|
|
3664
4036
|
code,
|
|
3665
4037
|
reason
|
|
3666
4038
|
});
|
|
4039
|
+
if (!Number.isFinite(this._closeTimeoutMs)) return;
|
|
3667
4040
|
this._closeTimer = this._timers.setTimeout(() => this._finish(1006, "", false), this._closeTimeoutMs);
|
|
3668
4041
|
}
|
|
3669
4042
|
_onCloseFrame(code, reason) {
|
|
@@ -3742,7 +4115,8 @@ function isApplicationCloseCode(code) {
|
|
|
3742
4115
|
* `whenDrained(limit)` waits for it to fall: the backpressure a producer
|
|
3743
4116
|
* applies.
|
|
3744
4117
|
* - When the client's pings stop for three heartbeat periods (a closed tab, a
|
|
3745
|
-
* terminated worker), the socket closes with 1006.
|
|
4118
|
+
* terminated worker), the socket closes with 1006. With `watch: "first-ping"`
|
|
4119
|
+
* the watch starts only once the client pings.
|
|
3746
4120
|
*/
|
|
3747
4121
|
var ServerWebSocket = class extends PortSocketCore {
|
|
3748
4122
|
static CONNECTING = 0;
|
|
@@ -3751,55 +4125,150 @@ var ServerWebSocket = class extends PortSocketCore {
|
|
|
3751
4125
|
static CLOSED = 3;
|
|
3752
4126
|
url;
|
|
3753
4127
|
protocol;
|
|
4128
|
+
_binaryType = "arraybuffer";
|
|
4129
|
+
_options;
|
|
3754
4130
|
_accepted = false;
|
|
4131
|
+
_detached = false;
|
|
3755
4132
|
_queue = [];
|
|
3756
4133
|
_heartbeatMs = DEFAULT_HEARTBEAT_MS;
|
|
3757
4134
|
_lastPing = Date.now();
|
|
4135
|
+
/** Whether a ping (with a period a timer takes) arrived already. */
|
|
4136
|
+
_pinged = false;
|
|
3758
4137
|
_watch;
|
|
3759
4138
|
constructor(port, options = {}) {
|
|
3760
4139
|
super({
|
|
3761
4140
|
timers: options.timers,
|
|
3762
4141
|
closeTimeoutMs: options.closeTimeoutMs
|
|
3763
4142
|
});
|
|
4143
|
+
this._options = options;
|
|
3764
4144
|
this.url = options.url ?? "";
|
|
3765
4145
|
this.protocol = options.protocol ?? "";
|
|
3766
4146
|
this._attach(port);
|
|
3767
4147
|
this._state = 1;
|
|
3768
|
-
this._watchHeartbeat();
|
|
4148
|
+
if (options.watch !== "first-ping") this._watchHeartbeat();
|
|
3769
4149
|
}
|
|
3770
4150
|
/** Whether `accept()` was called. */
|
|
3771
4151
|
get accepted() {
|
|
3772
4152
|
return this._accepted;
|
|
3773
4153
|
}
|
|
4154
|
+
/** How binary messages arrive: `ArrayBuffer`s (the default) or `Blob`s. Other values are ignored. */
|
|
4155
|
+
get binaryType() {
|
|
4156
|
+
return this._binaryType;
|
|
4157
|
+
}
|
|
4158
|
+
set binaryType(value) {
|
|
4159
|
+
if (value === "blob" || value === "arraybuffer") this._binaryType = value;
|
|
4160
|
+
}
|
|
4161
|
+
/**
|
|
4162
|
+
* Hands this end's transport to another holder before it was accepted: the port to give the
|
|
4163
|
+
* client (a pair's end returned in a 101, say), with the frames that reached this end first
|
|
4164
|
+
* delivered ahead of the rest. This end is closed then, with no event, and answers nothing.
|
|
4165
|
+
*/
|
|
4166
|
+
detach() {
|
|
4167
|
+
const port = this._port;
|
|
4168
|
+
if (!port || this._accepted || this._state !== 1) throw new DOMException("Only an open end that was not accepted can be detached.", "InvalidStateError");
|
|
4169
|
+
this._detached = true;
|
|
4170
|
+
this._port = void 0;
|
|
4171
|
+
port.onmessage = null;
|
|
4172
|
+
let owed = this.bufferedAmount;
|
|
4173
|
+
this._state = 3;
|
|
4174
|
+
this._onClosed();
|
|
4175
|
+
this._wakeDrainWaiters();
|
|
4176
|
+
const queued = this._queue.splice(0);
|
|
4177
|
+
if (queued.length === 0 && owed === 0) return port;
|
|
4178
|
+
const relay = new MessageChannel();
|
|
4179
|
+
const holder = relay.port2;
|
|
4180
|
+
const closing = /* @__PURE__ */ new Set();
|
|
4181
|
+
const end = () => {
|
|
4182
|
+
port.onmessage = null;
|
|
4183
|
+
holder.onmessage = null;
|
|
4184
|
+
port.close();
|
|
4185
|
+
holder.close();
|
|
4186
|
+
};
|
|
4187
|
+
const sawClose = (way) => {
|
|
4188
|
+
closing.add(way);
|
|
4189
|
+
if (closing.size === 2) end();
|
|
4190
|
+
};
|
|
4191
|
+
for (const frame of queued) {
|
|
4192
|
+
postFrame(holder, frame);
|
|
4193
|
+
if (frame.t === "c") closing.add("in");
|
|
4194
|
+
}
|
|
4195
|
+
holder.onmessage = (event) => {
|
|
4196
|
+
port.postMessage(event.data, transferables(event.data));
|
|
4197
|
+
if (event.data?.t === "c") sawClose("out");
|
|
4198
|
+
};
|
|
4199
|
+
port.onmessage = (event) => {
|
|
4200
|
+
const data = event.data;
|
|
4201
|
+
if (data?.t === "a" && typeof data.n === "number" && owed > 0) {
|
|
4202
|
+
const absorbed = Math.min(owed, data.n);
|
|
4203
|
+
owed -= absorbed;
|
|
4204
|
+
if (data.n > absorbed) holder.postMessage({
|
|
4205
|
+
t: "a",
|
|
4206
|
+
n: data.n - absorbed
|
|
4207
|
+
});
|
|
4208
|
+
return;
|
|
4209
|
+
}
|
|
4210
|
+
holder.postMessage(event.data, transferables(event.data));
|
|
4211
|
+
if (data?.t === "c") sawClose("in");
|
|
4212
|
+
};
|
|
4213
|
+
port.addEventListener?.("close", end);
|
|
4214
|
+
holder.addEventListener?.("close", end);
|
|
4215
|
+
return relay.port1;
|
|
4216
|
+
}
|
|
3774
4217
|
/** Starts delivering events, the queued ones first. */
|
|
3775
4218
|
accept() {
|
|
4219
|
+
if (this._detached) throw new DOMException("This end was detached; it cannot be accepted.", "InvalidStateError");
|
|
3776
4220
|
if (this._accepted) return;
|
|
3777
4221
|
this._accepted = true;
|
|
3778
|
-
for (const frame of this._queue.splice(0))
|
|
4222
|
+
for (const frame of this._queue.splice(0)) this._deliver(frame);
|
|
3779
4223
|
}
|
|
3780
4224
|
send(data) {
|
|
3781
|
-
if (
|
|
4225
|
+
if (this._detached) throw new DOMException("This end was detached; it cannot send.", "InvalidStateError");
|
|
4226
|
+
if (!this._accepted && !this._options.sendBeforeAccept) throw new DOMException("accept() the socket before sending on it.", "InvalidStateError");
|
|
3782
4227
|
this._sendData(isBinary(data) ? toArrayBuffer(data) : String(data));
|
|
3783
4228
|
}
|
|
3784
|
-
close(code
|
|
3785
|
-
|
|
4229
|
+
close(code, reason = "") {
|
|
4230
|
+
this.checkClose(code, reason);
|
|
4231
|
+
this._startClose(code ?? this._options.defaultCloseCode ?? 1e3, reason);
|
|
4232
|
+
}
|
|
4233
|
+
/**
|
|
4234
|
+
* Refuses a `close()` the endpoint may not make: by default a code outside RFC 6455 §7.4's
|
|
4235
|
+
* sendable ones, or a reason over 123 bytes. A subclass with another platform's rules
|
|
4236
|
+
* overrides it.
|
|
4237
|
+
*/
|
|
4238
|
+
checkClose(code, reason) {
|
|
4239
|
+
if (code !== void 0 && !isSendableCloseCode(code)) throw new DOMException(`${code} is not a close code an endpoint may send.`, "InvalidAccessError");
|
|
3786
4240
|
if (utf8Length(reason) > 123) throw new DOMException("The close reason is longer than 123 bytes.", "SyntaxError");
|
|
3787
|
-
this._startClose(code, reason);
|
|
3788
4241
|
}
|
|
3789
4242
|
_deliver(frame) {
|
|
3790
4243
|
if (!this._accepted) {
|
|
3791
4244
|
this._queue.push(frame);
|
|
3792
4245
|
return;
|
|
3793
4246
|
}
|
|
4247
|
+
if (frame.t === "m" && this._state === 2 && this._options.deliverWhileClosing) {
|
|
4248
|
+
this._dispatchMessage(frame.d);
|
|
4249
|
+
this._consumed(messageSize(frame.d));
|
|
4250
|
+
return;
|
|
4251
|
+
}
|
|
3794
4252
|
super._deliver(frame);
|
|
3795
4253
|
}
|
|
3796
4254
|
_dispatchMessage(data) {
|
|
3797
|
-
this.
|
|
4255
|
+
const delivered = typeof data !== "string" && this._binaryType === "blob" ? new Blob([data]) : data;
|
|
4256
|
+
this.dispatchEvent(new MessageEvent("message", { data: delivered }));
|
|
3798
4257
|
}
|
|
3799
4258
|
_onPing(hb) {
|
|
3800
4259
|
this._post({ t: "q" });
|
|
3801
4260
|
this._lastPing = Date.now();
|
|
3802
|
-
if (
|
|
4261
|
+
if (this._state === 3) return;
|
|
4262
|
+
const keep = this._options.heartbeatPeriod === "first" && this._pinged;
|
|
4263
|
+
if (!(hb > 0 && hb < 2 ** 31 - 1)) {
|
|
4264
|
+
if (!keep && this._watch !== void 0) {
|
|
4265
|
+
this._timers.clearInterval(this._watch);
|
|
4266
|
+
this._watch = void 0;
|
|
4267
|
+
}
|
|
4268
|
+
return;
|
|
4269
|
+
}
|
|
4270
|
+
this._pinged = true;
|
|
4271
|
+
if (!keep && (this._watch === void 0 || hb !== this._heartbeatMs)) {
|
|
3803
4272
|
this._heartbeatMs = hb;
|
|
3804
4273
|
this._watchHeartbeat();
|
|
3805
4274
|
}
|
|
@@ -3821,6 +4290,11 @@ function isSendableCloseCode(code) {
|
|
|
3821
4290
|
if (code >= 3e3 && code <= 4999) return true;
|
|
3822
4291
|
return code >= 1e3 && code <= 1014 && code !== 1004 && code !== 1005 && code !== 1006;
|
|
3823
4292
|
}
|
|
4293
|
+
/** The buffers a relayed frame carries, to transfer rather than copy. */
|
|
4294
|
+
function transferables(data) {
|
|
4295
|
+
const d = data?.d;
|
|
4296
|
+
return d instanceof ArrayBuffer ? [d] : [];
|
|
4297
|
+
}
|
|
3824
4298
|
//#endregion
|
|
3825
4299
|
//#region src/ws/upgrade.ts
|
|
3826
4300
|
/**
|
|
@@ -3852,6 +4326,7 @@ function newWebSocketUpgradeRequest(url, { protocols = [], headers } = {}) {
|
|
|
3852
4326
|
all.set("connection", "Upgrade");
|
|
3853
4327
|
all.set("sec-websocket-version", "13");
|
|
3854
4328
|
if (!all.has("sec-websocket-key")) all.set("sec-websocket-key", randomKey());
|
|
4329
|
+
all.delete("sec-websocket-protocol");
|
|
3855
4330
|
if (protocols.length > 0) all.set("sec-websocket-protocol", protocols.join(", "));
|
|
3856
4331
|
if (!all.has("x-webrun-ws-version")) all.set(WS_VERSION_HEADER, `2`);
|
|
3857
4332
|
return withHeaders(new Request(httpUrl, {
|
|
@@ -3911,6 +4386,23 @@ function upgradeWebSocket(request, options = {}) {
|
|
|
3911
4386
|
url: request.url,
|
|
3912
4387
|
protocol
|
|
3913
4388
|
});
|
|
4389
|
+
const response = webSocketUpgradeResponse(channel.port2, protocol);
|
|
4390
|
+
Object.defineProperty(response, "webSocket", {
|
|
4391
|
+
value: socket,
|
|
4392
|
+
configurable: true
|
|
4393
|
+
});
|
|
4394
|
+
return {
|
|
4395
|
+
socket,
|
|
4396
|
+
response
|
|
4397
|
+
};
|
|
4398
|
+
}
|
|
4399
|
+
/**
|
|
4400
|
+
* A 101 for a socket whose server end the handler runs itself, on the other end of `port`
|
|
4401
|
+
* (a server of its own over this package's frames, say): what `upgradeWebSocket` returns, minus
|
|
4402
|
+
* the `ServerWebSocket`. The host hands `port` to the caller as it would for `upgradeWebSocket`
|
|
4403
|
+
* (`takePort`), and `protocol` (`""` for none) is the subprotocol picked.
|
|
4404
|
+
*/
|
|
4405
|
+
function webSocketUpgradeResponse(port, protocol = "") {
|
|
3914
4406
|
const response = new Response(null, {
|
|
3915
4407
|
status: 200,
|
|
3916
4408
|
headers: protocol ? { "sec-websocket-protocol": protocol } : {}
|
|
@@ -3924,23 +4416,16 @@ function upgradeWebSocket(request, options = {}) {
|
|
|
3924
4416
|
value: "Switching Protocols",
|
|
3925
4417
|
configurable: true
|
|
3926
4418
|
},
|
|
3927
|
-
webSocket: {
|
|
3928
|
-
value: socket,
|
|
3929
|
-
configurable: true
|
|
3930
|
-
},
|
|
3931
4419
|
[UPGRADE]: {
|
|
3932
4420
|
value: {
|
|
3933
|
-
port
|
|
4421
|
+
port,
|
|
3934
4422
|
protocol
|
|
3935
4423
|
},
|
|
3936
4424
|
configurable: true,
|
|
3937
4425
|
writable: true
|
|
3938
4426
|
}
|
|
3939
4427
|
});
|
|
3940
|
-
return
|
|
3941
|
-
socket,
|
|
3942
|
-
response
|
|
3943
|
-
};
|
|
4428
|
+
return response;
|
|
3944
4429
|
}
|
|
3945
4430
|
/**
|
|
3946
4431
|
* The port to hand to the caller, for a response from `upgradeWebSocket`.
|
|
@@ -4008,6 +4493,51 @@ function randomKey() {
|
|
|
4008
4493
|
return btoa(String.fromCharCode(...bytes));
|
|
4009
4494
|
}
|
|
4010
4495
|
//#endregion
|
|
4496
|
+
//#region src/sw/protocol.ts
|
|
4497
|
+
/**
|
|
4498
|
+
* The messages between pages, Dedicated Workers and the same-origin
|
|
4499
|
+
* ServiceWorker. Internal: the shapes may change between versions, which is
|
|
4500
|
+
* why every hand-shake carries `HTTP_LINK_PROTOCOL`.
|
|
4501
|
+
*
|
|
4502
|
+
* THREE PORTS PER HANDLER LINK. A page makes the channels and hands one end of
|
|
4503
|
+
* each to the handler (itself, or a worker) and the other to the
|
|
4504
|
+
* ServiceWorker; after that it carries nothing:
|
|
4505
|
+
*
|
|
4506
|
+
* - `down`: ServiceWorker → handler, the requests the handler serves;
|
|
4507
|
+
* - `ctl`: a raw port next to them, for what HTTP over a stream cannot carry,
|
|
4508
|
+
* transferred `MessagePort`s (WebSocket hand-over);
|
|
4509
|
+
* - `up` (workers only): handler → ServiceWorker, the requests the handler
|
|
4510
|
+
* sends through the router (to other handlers).
|
|
4511
|
+
*/
|
|
4512
|
+
/** Page → ServiceWorker: attach a handler link. The name is the one version 1 used. */
|
|
4513
|
+
const ATTACH_CALL = "UPDATE_COMMUNICATION_PORT";
|
|
4514
|
+
/** Page → ServiceWorker: remove a handler link now. */
|
|
4515
|
+
const DETACH_CALL = "webrun-http:detach";
|
|
4516
|
+
/** Page → ServiceWorker: stop routing to a link, let its requests finish, then remove it. */
|
|
4517
|
+
const DRAIN_CALL = "webrun-http:drain";
|
|
4518
|
+
/** Page → ServiceWorker: open a WebSocket to a handler. */
|
|
4519
|
+
const WS_CONNECT_CALL = "webrun-http:ws-connect";
|
|
4520
|
+
/** ServiceWorker → pages: attach your links again (the ServiceWorker restarted). */
|
|
4521
|
+
const NEED_LINKS_CALL = "webrun-http:need-links";
|
|
4522
|
+
/**
|
|
4523
|
+
* Page → ServiceWorker, plain messages (no reply). A message to the
|
|
4524
|
+
* ServiceWorker object is an event: it starts a stopped worker and resets its
|
|
4525
|
+
* idle timer. Port messages do neither.
|
|
4526
|
+
*/
|
|
4527
|
+
const WAKE_MESSAGE = "webrun-http:wake";
|
|
4528
|
+
const KEEPALIVE_MESSAGE = "webrun-http:keepalive";
|
|
4529
|
+
/** Page ↔ worker: what `attachWorker` and `serveHttpInWorker` exchange. */
|
|
4530
|
+
const WORKER_HELLO = "webrun-http:worker-hello";
|
|
4531
|
+
/** `headers` as pairs a `postMessage` can carry (a `Headers` object cannot be cloned). */
|
|
4532
|
+
function headerPairs(headers) {
|
|
4533
|
+
if (headers === void 0) return void 0;
|
|
4534
|
+
const pairs = [];
|
|
4535
|
+
new Headers(headers).forEach((value, name) => {
|
|
4536
|
+
pairs.push([name, value]);
|
|
4537
|
+
});
|
|
4538
|
+
return pairs;
|
|
4539
|
+
}
|
|
4540
|
+
//#endregion
|
|
4011
4541
|
//#region src/sw/handler-endpoint.ts
|
|
4012
4542
|
/** The message calls through a lost ServiceWorker fail with. */
|
|
4013
4543
|
const ROUTER_LOST_MESSAGE = "The ServiceWorker was lost while this call was in flight";
|
|
@@ -4091,8 +4621,11 @@ var HandlerEndpoint = class {
|
|
|
4091
4621
|
this._startKeepAlive();
|
|
4092
4622
|
return await link.up.send(request);
|
|
4093
4623
|
}
|
|
4094
|
-
/**
|
|
4095
|
-
|
|
4624
|
+
/**
|
|
4625
|
+
* Opens a socket to the handler `url` routes to, through the ServiceWorker. The handler sees
|
|
4626
|
+
* `headers` on the upgrade request, and so does a `route` function choosing it.
|
|
4627
|
+
*/
|
|
4628
|
+
async connectWebSocket(url, protocols = [], headers) {
|
|
4096
4629
|
const link = await this._linkWith("ctl");
|
|
4097
4630
|
const id = this._sockets.newId();
|
|
4098
4631
|
const answer = this._sockets.wait(id, this.options.wsConnectTimeoutMs ?? 15e3, `the socket request for ${url}`);
|
|
@@ -4100,7 +4633,8 @@ var HandlerEndpoint = class {
|
|
|
4100
4633
|
t: "ws-open",
|
|
4101
4634
|
id,
|
|
4102
4635
|
url,
|
|
4103
|
-
protocols
|
|
4636
|
+
protocols,
|
|
4637
|
+
headers: headerPairs(headers)
|
|
4104
4638
|
});
|
|
4105
4639
|
return await answer;
|
|
4106
4640
|
}
|
|
@@ -4519,8 +5053,9 @@ async function awaitServiceWorkerControl(registration, { timeout = DEFAULT_SERVI
|
|
|
4519
5053
|
* Returns a function that stops answering.
|
|
4520
5054
|
*/
|
|
4521
5055
|
function handleClaimRequests(self) {
|
|
4522
|
-
|
|
4523
|
-
|
|
5056
|
+
const sw = self;
|
|
5057
|
+
return handleChannelCalls(sw, CLAIM_CALL, (event) => {
|
|
5058
|
+
const claimed = sw.clients.claim().then(() => true);
|
|
4524
5059
|
event.waitUntil?.(claimed);
|
|
4525
5060
|
return claimed;
|
|
4526
5061
|
});
|
|
@@ -4792,43 +5327,22 @@ async function sendHttpRequest(communicationPort, request) {
|
|
|
4792
5327
|
})(request);
|
|
4793
5328
|
}
|
|
4794
5329
|
//#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";
|
|
5330
|
+
//#region src/sw/http-sw-dispatcher.ts
|
|
4821
5331
|
/**
|
|
4822
|
-
*
|
|
4823
|
-
*
|
|
4824
|
-
* idle timer. Port messages do neither.
|
|
5332
|
+
* The default key: the url's first path segment under `scope`, decoded; none outside it, at the
|
|
5333
|
+
* scope itself, or for a segment that does not decode.
|
|
4825
5334
|
*/
|
|
4826
|
-
|
|
4827
|
-
const
|
|
4828
|
-
|
|
4829
|
-
const
|
|
4830
|
-
|
|
4831
|
-
|
|
5335
|
+
function scopeKey(url, scope) {
|
|
5336
|
+
const href = `${url}`;
|
|
5337
|
+
if (!href.startsWith(scope)) return void 0;
|
|
5338
|
+
const key = href.substring(scope.length).replace(/[?#].*$/, "").split("/")[0];
|
|
5339
|
+
if (!key) return void 0;
|
|
5340
|
+
try {
|
|
5341
|
+
return decodeURIComponent(key);
|
|
5342
|
+
} catch {
|
|
5343
|
+
return;
|
|
5344
|
+
}
|
|
5345
|
+
}
|
|
4832
5346
|
/** One attached handler link, as the ServiceWorker holds it. */
|
|
4833
5347
|
var HandlerEntry = class {
|
|
4834
5348
|
key;
|
|
@@ -4868,7 +5382,8 @@ const TIMED_OUT = Symbol("timed-out");
|
|
|
4868
5382
|
* (`SwHttpAdapter.register`) or in Dedicated Workers
|
|
4869
5383
|
* (`SwHttpAdapter.attachWorker` + `serveHttpInWorker`).
|
|
4870
5384
|
*
|
|
4871
|
-
* A url `<scope>/<key>/...` belongs to the handlers of `key
|
|
5385
|
+
* A url `<scope>/<key>/...` belongs to the handlers of `key` (the `key`
|
|
5386
|
+
* option maps urls to keys some other way). Of those, the
|
|
4872
5387
|
* `route` option picks one per request. Requests and responses stream with
|
|
4873
5388
|
* backpressure in both directions, and an aborted fetch reaches the handler
|
|
4874
5389
|
* as a cancellation (webrun-rpc + webrun-http-streams).
|
|
@@ -4887,6 +5402,8 @@ const TIMED_OUT = Symbol("timed-out");
|
|
|
4887
5402
|
*/
|
|
4888
5403
|
var SwHttpDispatcher = class {
|
|
4889
5404
|
self;
|
|
5405
|
+
/** `self`, with the WebWorker lib's types this module compiles with. */
|
|
5406
|
+
_sw;
|
|
4890
5407
|
log;
|
|
4891
5408
|
options;
|
|
4892
5409
|
table = new HandlerTable();
|
|
@@ -4909,19 +5426,22 @@ var SwHttpDispatcher = class {
|
|
|
4909
5426
|
constructor(options) {
|
|
4910
5427
|
this.options = options;
|
|
4911
5428
|
this.self = options.self;
|
|
5429
|
+
this._sw = options.self;
|
|
4912
5430
|
this.log = options.log ?? (() => {});
|
|
4913
5431
|
if (locksAvailable()) this.routerLock = newLockName("webrun-http-router");
|
|
4914
5432
|
}
|
|
4915
5433
|
get scope() {
|
|
4916
|
-
return `${new URL("./", this.
|
|
5434
|
+
return this._sw.registration?.scope ?? `${new URL("./", this._sw.location.href)}`;
|
|
4917
5435
|
}
|
|
4918
|
-
/** The key `url` belongs to
|
|
5436
|
+
/** The key `url` belongs to (the `key` option; by default its first path segment under the scope). */
|
|
4919
5437
|
keyOf(url) {
|
|
4920
|
-
|
|
4921
|
-
|
|
4922
|
-
|
|
4923
|
-
|
|
4924
|
-
|
|
5438
|
+
let parsed;
|
|
5439
|
+
try {
|
|
5440
|
+
parsed = new URL(`${url}`);
|
|
5441
|
+
} catch {
|
|
5442
|
+
return;
|
|
5443
|
+
}
|
|
5444
|
+
return (this.options.key ?? scopeKey)(parsed, this.scope);
|
|
4925
5445
|
}
|
|
4926
5446
|
/** Whether `key` has ever been served on this worker. */
|
|
4927
5447
|
isClaimedKey(key) {
|
|
@@ -4932,7 +5452,7 @@ var SwHttpDispatcher = class {
|
|
|
4932
5452
|
return this.claimedKeys.has(key) || this.table.all(key).length > 0;
|
|
4933
5453
|
}
|
|
4934
5454
|
start() {
|
|
4935
|
-
const self = this.
|
|
5455
|
+
const self = this._sw;
|
|
4936
5456
|
if (this.routerLock) this._routerHeld = holdLock(this.routerLock).then(() => {});
|
|
4937
5457
|
this._restored = this._restoreClaimedKeys();
|
|
4938
5458
|
const latch = () => {
|
|
@@ -4952,8 +5472,8 @@ var SwHttpDispatcher = class {
|
|
|
4952
5472
|
return true;
|
|
4953
5473
|
}), handleChannelCalls(self, WS_CONNECT_CALL, async (event, params) => {
|
|
4954
5474
|
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 ?? []);
|
|
5475
|
+
const { url, protocols, headers } = params;
|
|
5476
|
+
const connection = await this.connectWebSocket(url, protocols ?? [], this._socketHeaders(headers, "page"));
|
|
4957
5477
|
return withTransfer(connection, [connection.port]);
|
|
4958
5478
|
}));
|
|
4959
5479
|
const onMessage = (event) => {
|
|
@@ -4971,10 +5491,10 @@ var SwHttpDispatcher = class {
|
|
|
4971
5491
|
this.log("Claim control over all clients.");
|
|
4972
5492
|
event.waitUntil(self.clients.claim());
|
|
4973
5493
|
};
|
|
4974
|
-
const onFetch = (event) => this.
|
|
5494
|
+
const onFetch = (event) => void this.handleFetch(event);
|
|
4975
5495
|
self.addEventListener("install", onInstall);
|
|
4976
5496
|
self.addEventListener("activate", onActivate);
|
|
4977
|
-
self.addEventListener("fetch", onFetch);
|
|
5497
|
+
if (this.options.handleFetch !== false) self.addEventListener("fetch", onFetch);
|
|
4978
5498
|
this._cleanup.push(() => {
|
|
4979
5499
|
self.removeEventListener("install", onInstall);
|
|
4980
5500
|
self.removeEventListener("activate", onActivate);
|
|
@@ -4989,37 +5509,60 @@ var SwHttpDispatcher = class {
|
|
|
4989
5509
|
}
|
|
4990
5510
|
/**
|
|
4991
5511
|
* 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.
|
|
5512
|
+
* handler its key routes to. A url with no key (by default, one outside the
|
|
5513
|
+
* scope), or of a key this worker never served, goes to the network.
|
|
4994
5514
|
*/
|
|
4995
5515
|
async dispatch(request) {
|
|
4996
5516
|
const key = this.keyOf(request.url);
|
|
4997
5517
|
if (key === void 0) return await fetch(request);
|
|
4998
5518
|
await this._restored.catch(() => {});
|
|
4999
5519
|
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
|
-
});
|
|
5520
|
+
if (entry === TIMED_OUT) return await this._failed(request, { kind: "timeout" });
|
|
5004
5521
|
if (!entry) {
|
|
5005
5522
|
if (!this.serves(key)) return await fetch(request);
|
|
5006
|
-
return
|
|
5007
|
-
status: 404,
|
|
5008
|
-
statusText: "Not Found (no active handler)"
|
|
5009
|
-
});
|
|
5523
|
+
return await this._failed(request, { kind: "gone" });
|
|
5010
5524
|
}
|
|
5011
5525
|
try {
|
|
5012
5526
|
return await entry.link.send(request);
|
|
5013
5527
|
} catch (error) {
|
|
5014
5528
|
this.log("[dispatch] failed", key, error);
|
|
5015
|
-
return
|
|
5016
|
-
|
|
5017
|
-
|
|
5018
|
-
headers: { "content-type": "text/plain; charset=utf-8" }
|
|
5529
|
+
return await this._failed(request, {
|
|
5530
|
+
kind: "lost",
|
|
5531
|
+
error
|
|
5019
5532
|
});
|
|
5020
5533
|
}
|
|
5021
5534
|
}
|
|
5022
5535
|
/**
|
|
5536
|
+
* After `start()`: settles once the keys this worker served before a restart are read back
|
|
5537
|
+
* (whatever the outcome). Until then `serves()` may answer `false` for a key it will serve.
|
|
5538
|
+
*/
|
|
5539
|
+
get ready() {
|
|
5540
|
+
return this._restored.then(() => {}, () => {});
|
|
5541
|
+
}
|
|
5542
|
+
_socketHeaders(headers, from) {
|
|
5543
|
+
return this.options.socketHeaders ? this.options.socketHeaders(headers, from) : headers;
|
|
5544
|
+
}
|
|
5545
|
+
async _failed(request, failure) {
|
|
5546
|
+
if (this.options.failure) try {
|
|
5547
|
+
return await this.options.failure(request, failure);
|
|
5548
|
+
} catch (error) {
|
|
5549
|
+
this.log("[dispatch] the failure option threw; answering by default", error);
|
|
5550
|
+
}
|
|
5551
|
+
if (failure.kind === "timeout") return new Response(null, {
|
|
5552
|
+
status: 503,
|
|
5553
|
+
statusText: "Service Unavailable (no handler attached in time)"
|
|
5554
|
+
});
|
|
5555
|
+
if (failure.kind === "gone") return new Response(null, {
|
|
5556
|
+
status: 404,
|
|
5557
|
+
statusText: "Not Found (no active handler)"
|
|
5558
|
+
});
|
|
5559
|
+
return new Response(errorMessage(failure.error), {
|
|
5560
|
+
status: 502,
|
|
5561
|
+
statusText: "Bad Gateway",
|
|
5562
|
+
headers: { "content-type": "text/plain; charset=utf-8" }
|
|
5563
|
+
});
|
|
5564
|
+
}
|
|
5565
|
+
/**
|
|
5023
5566
|
* Opens a socket to the handler `url` routes to: the handler accepts or
|
|
5024
5567
|
* refuses it like an HTTP upgrade, and the socket's port comes back. After
|
|
5025
5568
|
* that the socket runs between the caller and the handler directly; this
|
|
@@ -5031,7 +5574,7 @@ var SwHttpDispatcher = class {
|
|
|
5031
5574
|
headers
|
|
5032
5575
|
});
|
|
5033
5576
|
const key = this.keyOf(request.url);
|
|
5034
|
-
if (key === void 0) throw new Error(`${url}
|
|
5577
|
+
if (key === void 0) throw new Error(`${url} belongs to no key of this ServiceWorker`);
|
|
5035
5578
|
await this._restored.catch(() => {});
|
|
5036
5579
|
const entry = await this._acquire(request, key);
|
|
5037
5580
|
if (!entry || entry === TIMED_OUT) throw new Error(`No handler serves ${url}`);
|
|
@@ -5047,20 +5590,47 @@ var SwHttpDispatcher = class {
|
|
|
5047
5590
|
});
|
|
5048
5591
|
return await answer;
|
|
5049
5592
|
}
|
|
5050
|
-
|
|
5051
|
-
|
|
5593
|
+
/**
|
|
5594
|
+
* Answers `event` if it is the dispatcher's, with `request` (default `event.request`; a host
|
|
5595
|
+
* may pass a rewritten one, with headers it adds). Returns whether it called `respondWith`;
|
|
5596
|
+
* when it did not, the event is the caller's to answer or to leave to the network. For a host
|
|
5597
|
+
* that listens to `fetch` itself (`handleFetch: false`); call it synchronously in the event's
|
|
5598
|
+
* listener, as `respondWith` must be.
|
|
5599
|
+
*
|
|
5600
|
+
* It takes a same-origin request whose key (the `key` option) a handler serves, and also:
|
|
5601
|
+
* - a navigation under the scope whose key nobody serves (answered by `unserved`);
|
|
5602
|
+
* - before the stored keys are read (the first requests after a restart), every request with
|
|
5603
|
+
* a key, since only then is it known whose it is: one nobody serves gets what it would have
|
|
5604
|
+
* got after, `unserved` for a navigation and the network for anything else.
|
|
5605
|
+
* So a host with a page of its own for urls nobody serves gives it as `unserved`, rather than
|
|
5606
|
+
* deciding before calling this.
|
|
5607
|
+
*/
|
|
5608
|
+
handleFetch(event, request = event.request) {
|
|
5609
|
+
const navigation = event.request.mode === "navigate";
|
|
5610
|
+
if (new URL(request.url).origin !== this._sw.location.origin) return false;
|
|
5052
5611
|
const key = this.keyOf(request.url);
|
|
5053
|
-
if (key === void 0) return;
|
|
5612
|
+
if (key === void 0) return false;
|
|
5054
5613
|
if (this._restoreSettled) {
|
|
5055
|
-
if (this.serves(key))
|
|
5056
|
-
|
|
5057
|
-
|
|
5614
|
+
if (this.serves(key)) {
|
|
5615
|
+
event.respondWith(this.dispatch(request));
|
|
5616
|
+
return true;
|
|
5617
|
+
}
|
|
5618
|
+
if (navigation) {
|
|
5619
|
+
event.respondWith(this._unserved(request));
|
|
5620
|
+
return true;
|
|
5621
|
+
}
|
|
5622
|
+
return false;
|
|
5058
5623
|
}
|
|
5059
5624
|
event.respondWith((async () => {
|
|
5060
5625
|
await this._restored.catch(() => {});
|
|
5061
|
-
if (
|
|
5062
|
-
return await this.
|
|
5626
|
+
if (this.serves(key)) return await this.dispatch(request);
|
|
5627
|
+
return navigation ? await this._unserved(request) : await fetch(request);
|
|
5063
5628
|
})());
|
|
5629
|
+
return true;
|
|
5630
|
+
}
|
|
5631
|
+
/** What a request the dispatcher took, and no handler serves, gets: `unserved`, or the network. */
|
|
5632
|
+
async _unserved(request) {
|
|
5633
|
+
return await (this.options.unserved ?? fetch)(request);
|
|
5064
5634
|
}
|
|
5065
5635
|
async _attach(event, params, ports) {
|
|
5066
5636
|
const clientId = event.source?.id;
|
|
@@ -5079,8 +5649,11 @@ var SwHttpDispatcher = class {
|
|
|
5079
5649
|
}
|
|
5080
5650
|
const ctl = params.ctl ? rest.shift() : void 0;
|
|
5081
5651
|
const up = params.up ? rest.shift() : void 0;
|
|
5082
|
-
const streamTimeout = this.options
|
|
5083
|
-
const link = await connectHttpLink(down, {
|
|
5652
|
+
const { streamTimeout, window } = this.options;
|
|
5653
|
+
const link = await connectHttpLink(down, {
|
|
5654
|
+
streamTimeout,
|
|
5655
|
+
window
|
|
5656
|
+
});
|
|
5084
5657
|
const entry = new HandlerEntry(params.key, params.id ?? clientId ?? crypto.randomUUID(), params.owner ?? crypto.randomUUID(), params.meta, link, ctl, clientId, params.ownerLock);
|
|
5085
5658
|
if (up) entry.closeUp = await serveHttpLink(up, (request) => this.dispatch(request), { streamTimeout });
|
|
5086
5659
|
if (ctl) ctl.onmessage = (message) => this._onControl(entry, message.data);
|
|
@@ -5159,20 +5732,35 @@ var SwHttpDispatcher = class {
|
|
|
5159
5732
|
* its key, waiting for one when there is none.
|
|
5160
5733
|
*/
|
|
5161
5734
|
async _acquire(request, key) {
|
|
5735
|
+
const deadline = Date.now() + (this.options.attachTimeoutMs ?? 15e3);
|
|
5162
5736
|
const picked = await this._pick(request, key);
|
|
5163
5737
|
if (picked) return picked;
|
|
5164
|
-
if (this.table.all(key).length
|
|
5165
|
-
|
|
5166
|
-
|
|
5167
|
-
|
|
5168
|
-
|
|
5169
|
-
|
|
5738
|
+
if (this.table.all(key).length === 0) {
|
|
5739
|
+
const vacantUntil = this._vacant.get(key) ?? 0;
|
|
5740
|
+
if (vacantUntil > Date.now()) return await this._waitToPick(request, key, vacantUntil);
|
|
5741
|
+
if (!this.serves(key)) return void 0;
|
|
5742
|
+
await this._askForLinks(key);
|
|
5743
|
+
const relinked = await this._pick(request, key);
|
|
5744
|
+
if (relinked) return relinked;
|
|
5745
|
+
if (this.table.all(key).length === 0) return void 0;
|
|
5746
|
+
}
|
|
5747
|
+
return await this._waitToPick(request, key, deadline);
|
|
5748
|
+
}
|
|
5749
|
+
/** Waits for attaches until `route` accepts a handler, or `until` passes. */
|
|
5750
|
+
async _waitToPick(request, key, until) {
|
|
5751
|
+
for (;;) {
|
|
5752
|
+
const left = until - Date.now();
|
|
5753
|
+
if (left <= 0) return TIMED_OUT;
|
|
5754
|
+
await this.table.waitFor(key, Math.min(left, 1e3), this.table.live(key).length > 0);
|
|
5755
|
+
const picked = await this._pick(request, key);
|
|
5756
|
+
if (picked) return picked;
|
|
5757
|
+
}
|
|
5170
5758
|
}
|
|
5171
5759
|
async _pick(request, key) {
|
|
5172
5760
|
let live = this.table.live(key);
|
|
5173
5761
|
for (const entry of live) {
|
|
5174
5762
|
if (entry.ownerLock || !entry.clientId) continue;
|
|
5175
|
-
if (!await this.
|
|
5763
|
+
if (!await this._sw.clients.get(entry.clientId)) this._close(entry, new LinkLostError(HANDLER_LOST_MESSAGE));
|
|
5176
5764
|
}
|
|
5177
5765
|
live = this.table.live(key);
|
|
5178
5766
|
if (live.length === 0) return void 0;
|
|
@@ -5196,7 +5784,7 @@ var SwHttpDispatcher = class {
|
|
|
5196
5784
|
}
|
|
5197
5785
|
async _linkRound(key) {
|
|
5198
5786
|
const timeout = this.options.relinkTimeoutMs ?? 3e3;
|
|
5199
|
-
const windows = await this.
|
|
5787
|
+
const windows = await this._sw.clients.matchAll({
|
|
5200
5788
|
type: "window",
|
|
5201
5789
|
includeUncontrolled: true
|
|
5202
5790
|
});
|
|
@@ -5234,7 +5822,7 @@ var SwHttpDispatcher = class {
|
|
|
5234
5822
|
entry.sockets.fail(message.id, new Error(message.message));
|
|
5235
5823
|
return;
|
|
5236
5824
|
case "ws-open":
|
|
5237
|
-
this.connectWebSocket(message.url, message.protocols).then(({ port, protocol }) => entry.ctl?.postMessage({
|
|
5825
|
+
this.connectWebSocket(message.url, message.protocols, this._socketHeaders(message.headers, "handler")).then(({ port, protocol }) => entry.ctl?.postMessage({
|
|
5238
5826
|
t: "ws-opened",
|
|
5239
5827
|
id: message.id,
|
|
5240
5828
|
protocol,
|
|
@@ -5251,7 +5839,7 @@ var SwHttpDispatcher = class {
|
|
|
5251
5839
|
const source = event.source;
|
|
5252
5840
|
if (!source?.url) return false;
|
|
5253
5841
|
try {
|
|
5254
|
-
return new URL(source.url).origin === this.
|
|
5842
|
+
return new URL(source.url).origin === this._sw.location.origin;
|
|
5255
5843
|
} catch {
|
|
5256
5844
|
return false;
|
|
5257
5845
|
}
|
|
@@ -5275,6 +5863,7 @@ var SwHttpDispatcher = class {
|
|
|
5275
5863
|
* that stops it.
|
|
5276
5864
|
*/
|
|
5277
5865
|
function startHttpDispatcher(options) {
|
|
5866
|
+
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
5867
|
const dispatcher = new SwHttpDispatcher(options);
|
|
5279
5868
|
dispatcher.start();
|
|
5280
5869
|
return () => {
|
|
@@ -5449,6 +6038,8 @@ var SwPortHandler = class {
|
|
|
5449
6038
|
*/
|
|
5450
6039
|
var SwPortDispatcher = class {
|
|
5451
6040
|
self;
|
|
6041
|
+
/** `self`, with the WebWorker lib's types this module compiles with. */
|
|
6042
|
+
_sw;
|
|
5452
6043
|
log;
|
|
5453
6044
|
handlersIndex = /* @__PURE__ */ new Map();
|
|
5454
6045
|
/**
|
|
@@ -5462,10 +6053,11 @@ var SwPortDispatcher = class {
|
|
|
5462
6053
|
_activationPromise;
|
|
5463
6054
|
constructor({ self, log = () => {} }) {
|
|
5464
6055
|
this.self = self;
|
|
6056
|
+
this._sw = self;
|
|
5465
6057
|
this.log = log;
|
|
5466
6058
|
}
|
|
5467
6059
|
get scope() {
|
|
5468
|
-
return `${new URL("./", this.
|
|
6060
|
+
return this._sw.registration?.scope ?? `${new URL("./", this._sw.location.href)}`;
|
|
5469
6061
|
}
|
|
5470
6062
|
/**
|
|
5471
6063
|
* Look up the active channel for a key. Returns `undefined` when there
|
|
@@ -5478,7 +6070,7 @@ var SwPortDispatcher = class {
|
|
|
5478
6070
|
if (channelInfo.key !== key) continue;
|
|
5479
6071
|
const clientId = channelInfo.clientId;
|
|
5480
6072
|
if (!clientId) continue;
|
|
5481
|
-
if (!await this.
|
|
6073
|
+
if (!await this._sw.clients.get(clientId)) {
|
|
5482
6074
|
this.handlersIndex.delete(clientId);
|
|
5483
6075
|
continue;
|
|
5484
6076
|
}
|
|
@@ -5490,8 +6082,8 @@ var SwPortDispatcher = class {
|
|
|
5490
6082
|
return this.claimedKeys.has(key);
|
|
5491
6083
|
}
|
|
5492
6084
|
start() {
|
|
5493
|
-
const stopClaims = handleClaimRequests(this.
|
|
5494
|
-
const stopPortUpdates = handleChannelCalls(this.
|
|
6085
|
+
const stopClaims = handleClaimRequests(this._sw);
|
|
6086
|
+
const stopPortUpdates = handleChannelCalls(this._sw, "UPDATE_COMMUNICATION_PORT", async (event, channelInfo, port) => {
|
|
5495
6087
|
const clientId = event.source?.id;
|
|
5496
6088
|
this.log("[UPDATE_COMMUNICATION_PORT]", clientId, channelInfo);
|
|
5497
6089
|
await this._updateChannelInfo({
|
|
@@ -5505,14 +6097,14 @@ var SwPortDispatcher = class {
|
|
|
5505
6097
|
stopClaims();
|
|
5506
6098
|
stopPortUpdates();
|
|
5507
6099
|
};
|
|
5508
|
-
this.
|
|
6100
|
+
this._sw.addEventListener("install", (event) => {
|
|
5509
6101
|
this.log("Skip waiting on install.", event);
|
|
5510
|
-
this.
|
|
6102
|
+
this._sw.skipWaiting();
|
|
5511
6103
|
});
|
|
5512
|
-
this.
|
|
6104
|
+
this._sw.addEventListener("activate", (event) => {
|
|
5513
6105
|
this.log("Claim control over all clients.", event);
|
|
5514
6106
|
event.waitUntil((async () => {
|
|
5515
|
-
await this.
|
|
6107
|
+
await this._sw.clients.claim();
|
|
5516
6108
|
await this._checkActivation();
|
|
5517
6109
|
})());
|
|
5518
6110
|
});
|
|
@@ -5562,7 +6154,7 @@ var SwPortDispatcher = class {
|
|
|
5562
6154
|
for (const clientId of this.handlersIndex.keys()) ids.add(clientId);
|
|
5563
6155
|
const index = /* @__PURE__ */ new Map();
|
|
5564
6156
|
for (const clientId of ids) {
|
|
5565
|
-
const client = await this.
|
|
6157
|
+
const client = await this._sw.clients.get(clientId);
|
|
5566
6158
|
if (client) index.set(clientId, client);
|
|
5567
6159
|
else this.handlersIndex.delete(clientId);
|
|
5568
6160
|
}
|
|
@@ -5842,7 +6434,10 @@ var SwHttpAdapter = class {
|
|
|
5842
6434
|
return this.options.key;
|
|
5843
6435
|
}
|
|
5844
6436
|
get scope() {
|
|
5845
|
-
|
|
6437
|
+
if (this.options.scope) return this.options.scope;
|
|
6438
|
+
const registration = this.options.registration;
|
|
6439
|
+
if (registration) return new URL(registration.scope).pathname;
|
|
6440
|
+
return new URL("./", this.serviceWorkerUrl).pathname;
|
|
5846
6441
|
}
|
|
5847
6442
|
/**
|
|
5848
6443
|
* The scope as a url on the worker's origin. Resolved against the worker's
|
|
@@ -5853,6 +6448,8 @@ var SwHttpAdapter = class {
|
|
|
5853
6448
|
* sits next to it).
|
|
5854
6449
|
*/
|
|
5855
6450
|
get rootUrl() {
|
|
6451
|
+
const registration = this.options.registration;
|
|
6452
|
+
if (registration && !this.options.serviceWorkerUrl) return new URL(this.scope, registration.scope);
|
|
5856
6453
|
const base = this.options.serviceWorkerUrl ? this.serviceWorkerUrl : import.meta.url;
|
|
5857
6454
|
return new URL(this.scope, base);
|
|
5858
6455
|
}
|
|
@@ -5874,7 +6471,10 @@ var SwHttpAdapter = class {
|
|
|
5874
6471
|
*/
|
|
5875
6472
|
async start() {
|
|
5876
6473
|
if (!this._registrationPromise) this._registrationPromise = (async () => {
|
|
5877
|
-
const registration = await navigator.serviceWorker.register(this.serviceWorkerUrl, {
|
|
6474
|
+
const registration = this.options.registration ?? await navigator.serviceWorker.register(this.serviceWorkerUrl, {
|
|
6475
|
+
scope: this.scope,
|
|
6476
|
+
...this.options.type ? { type: this.options.type } : {}
|
|
6477
|
+
});
|
|
5878
6478
|
this._registration = registration;
|
|
5879
6479
|
const stopListening = handleChannelCalls(navigator.serviceWorker, NEED_LINKS_CALL, async (_event, params) => {
|
|
5880
6480
|
await this.relink(params?.routerLock);
|
|
@@ -5904,7 +6504,7 @@ var SwHttpAdapter = class {
|
|
|
5904
6504
|
})();
|
|
5905
6505
|
return this._registrationPromise;
|
|
5906
6506
|
}
|
|
5907
|
-
/** Detaches every link, and unregisters the worker. */
|
|
6507
|
+
/** Detaches every link, and unregisters the worker (unless the page passed its `registration`). */
|
|
5908
6508
|
async stop() {
|
|
5909
6509
|
for (const cleanup of this._cleanup.splice(0)) cleanup();
|
|
5910
6510
|
for (const worker of [...this._workers]) await worker.detach().catch(() => {});
|
|
@@ -5921,7 +6521,7 @@ var SwHttpAdapter = class {
|
|
|
5921
6521
|
this._registration = void 0;
|
|
5922
6522
|
this._registrationPromise = void 0;
|
|
5923
6523
|
try {
|
|
5924
|
-
await registration?.unregister();
|
|
6524
|
+
if (!this.options.registration) await registration?.unregister();
|
|
5925
6525
|
} catch (error) {
|
|
5926
6526
|
console.log("Service Worker unregistration failed: ", error);
|
|
5927
6527
|
}
|
|
@@ -5995,10 +6595,10 @@ var SwHttpAdapter = class {
|
|
|
5995
6595
|
/**
|
|
5996
6596
|
* Opens the transport of a socket to the handler `url` routes to: a handler
|
|
5997
6597
|
* of this page directly, any other through the ServiceWorker. Resolves once
|
|
5998
|
-
* the handler accepted.
|
|
5999
|
-
* `createWebSocketClass(adapter.connectWebSocket)`.
|
|
6598
|
+
* the handler accepted. The handler sees `headers` on the upgrade request. An arrow function,
|
|
6599
|
+
* so it can be handed around: `createWebSocketClass(adapter.connectWebSocket)`.
|
|
6000
6600
|
*/
|
|
6001
|
-
connectWebSocket = async (url, protocols = []) => {
|
|
6601
|
+
connectWebSocket = async (url, protocols = [], headers) => {
|
|
6002
6602
|
const httpUrl = new URL(`${url}`, globalThis.location?.href);
|
|
6003
6603
|
if (httpUrl.protocol === "ws:") httpUrl.protocol = "http:";
|
|
6004
6604
|
else if (httpUrl.protocol === "wss:") httpUrl.protocol = "https:";
|
|
@@ -6006,7 +6606,8 @@ var SwHttpAdapter = class {
|
|
|
6006
6606
|
if (local) {
|
|
6007
6607
|
const result = await serveWebSocketUpgrade(local, {
|
|
6008
6608
|
url: `${httpUrl}`,
|
|
6009
|
-
protocols
|
|
6609
|
+
protocols,
|
|
6610
|
+
headers
|
|
6010
6611
|
});
|
|
6011
6612
|
if (!result.ok) throw new Error(result.message);
|
|
6012
6613
|
return {
|
|
@@ -6017,7 +6618,8 @@ var SwHttpAdapter = class {
|
|
|
6017
6618
|
await this.start();
|
|
6018
6619
|
return await callChannelWithin(this._callTimeout(), this._worker(), WS_CONNECT_CALL, {
|
|
6019
6620
|
url: `${httpUrl}`,
|
|
6020
|
-
protocols
|
|
6621
|
+
protocols,
|
|
6622
|
+
headers: headerPairs(headers)
|
|
6021
6623
|
});
|
|
6022
6624
|
};
|
|
6023
6625
|
/**
|
|
@@ -6313,11 +6915,11 @@ function serveHttpInWorker(handler, options = {}) {
|
|
|
6313
6915
|
};
|
|
6314
6916
|
scope.addEventListener("message", onMessage);
|
|
6315
6917
|
const baseUrl = () => globalThis.location?.href;
|
|
6316
|
-
const connectWebSocket = (url, protocols = []) => {
|
|
6918
|
+
const connectWebSocket = (url, protocols = [], headers) => {
|
|
6317
6919
|
const httpUrl = new URL(`${url}`, baseUrl());
|
|
6318
6920
|
if (httpUrl.protocol === "ws:") httpUrl.protocol = "http:";
|
|
6319
6921
|
else if (httpUrl.protocol === "wss:") httpUrl.protocol = "https:";
|
|
6320
|
-
return endpoint.connectWebSocket(`${httpUrl}`, protocols);
|
|
6922
|
+
return endpoint.connectWebSocket(`${httpUrl}`, protocols, headers);
|
|
6321
6923
|
};
|
|
6322
6924
|
let WebSocketClass;
|
|
6323
6925
|
return {
|
|
@@ -6344,4 +6946,4 @@ function serveHttpInWorker(handler, options = {}) {
|
|
|
6344
6946
|
};
|
|
6345
6947
|
}
|
|
6346
6948
|
//#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 };
|
|
6949
|
+
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 };
|