@statewalker/webrun-http-browser 0.9.0 → 0.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +155 -10
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/service-worker-control.d.ts +2 -1
- package/dist/core/service-worker-control.d.ts.map +1 -1
- package/dist/core/sw-scope.d.ts +31 -0
- package/dist/core/sw-scope.d.ts.map +1 -0
- package/dist/index.js +908 -221
- package/dist/link/http-link.d.ts +16 -3
- package/dist/link/http-link.d.ts.map +1 -1
- package/dist/relay/index-sw.d.ts +8 -2
- package/dist/relay/index-sw.d.ts.map +1 -1
- package/dist/relay/index.d.ts +1 -1
- package/dist/relay/index.d.ts.map +1 -1
- package/dist/relay-sw.js +470 -81
- package/dist/relay-worker.d.ts +1 -0
- package/dist/relay-worker.d.ts.map +1 -1
- package/dist/relay-worker.js +470 -81
- package/dist/sw/handler-endpoint.d.ts +5 -2
- package/dist/sw/handler-endpoint.d.ts.map +1 -1
- package/dist/sw/http-sw-dispatcher.d.ts +114 -7
- package/dist/sw/http-sw-dispatcher.d.ts.map +1 -1
- package/dist/sw/index.d.ts +1 -0
- package/dist/sw/index.d.ts.map +1 -1
- package/dist/sw/protocol.d.ts +3 -0
- package/dist/sw/protocol.d.ts.map +1 -1
- package/dist/sw/sw-dispatcher.d.ts +5 -2
- package/dist/sw/sw-dispatcher.d.ts.map +1 -1
- package/dist/sw/sw-http-adapter.d.ts +19 -4
- package/dist/sw/sw-http-adapter.d.ts.map +1 -1
- package/dist/sw/worker-service.d.ts +5 -2
- package/dist/sw/worker-service.d.ts.map +1 -1
- package/dist/sw-worker.js +681 -141
- package/dist/sw.js +896 -218
- package/dist/worker.js +689 -115
- package/dist/ws/server.d.ts +50 -2
- package/dist/ws/server.d.ts.map +1 -1
- package/dist/ws/socket-core.d.ts +5 -2
- package/dist/ws/socket-core.d.ts.map +1 -1
- package/dist/ws/upgrade.d.ts +12 -1
- package/dist/ws/upgrade.d.ts.map +1 -1
- package/package.json +4 -3
- package/src/core/index.ts +1 -0
- package/src/core/service-worker-control.ts +5 -3
- package/src/core/sw-scope.ts +27 -0
- package/src/link/http-link.ts +20 -4
- package/src/relay/index-sw.ts +15 -2
- package/src/relay/index.ts +2 -2
- package/src/relay-worker.ts +1 -0
- package/src/sw/handler-endpoint.ts +17 -4
- package/src/sw/http-sw-dispatcher.ts +267 -67
- package/src/sw/index.ts +5 -0
- package/src/sw/protocol.ts +11 -1
- package/src/sw/sw-dispatcher.ts +17 -11
- package/src/sw/sw-http-adapter.ts +36 -10
- package/src/sw/worker-service.ts +11 -4
- package/src/ws/server.ts +166 -11
- package/src/ws/socket-core.ts +7 -2
- package/src/ws/upgrade.ts +22 -4
package/dist/sw.js
CHANGED
|
@@ -1266,7 +1266,11 @@ async function drain(output) {
|
|
|
1266
1266
|
*/
|
|
1267
1267
|
function httpServe(handler, options = {}) {
|
|
1268
1268
|
const codec = options.codec ?? defaultCodec;
|
|
1269
|
-
return
|
|
1269
|
+
return (input) => {
|
|
1270
|
+
const abandoned = new AbortController();
|
|
1271
|
+
return abortingOnReturn(exchange(input, { signal: abandoned.signal }), (reason) => abandoned.abort(reason ?? /* @__PURE__ */ new Error("webrun-http-streams: the exchange was abandoned before it was over")));
|
|
1272
|
+
};
|
|
1273
|
+
async function* exchange(input, context) {
|
|
1270
1274
|
let decoded;
|
|
1271
1275
|
try {
|
|
1272
1276
|
decoded = await codec.decodeRequest(input);
|
|
@@ -1278,7 +1282,7 @@ function httpServe(handler, options = {}) {
|
|
|
1278
1282
|
const replyCodec = decoded.codec ?? codec;
|
|
1279
1283
|
let result;
|
|
1280
1284
|
try {
|
|
1281
|
-
result = await handler(decoded.envelope, decoded.body);
|
|
1285
|
+
result = await handler(decoded.envelope, decoded.body, context);
|
|
1282
1286
|
} catch (error) {
|
|
1283
1287
|
yield* encodeErrorResponse(replyCodec, error, decoded.envelope.method);
|
|
1284
1288
|
return;
|
|
@@ -1293,7 +1297,36 @@ function httpServe(handler, options = {}) {
|
|
|
1293
1297
|
await result.release?.();
|
|
1294
1298
|
} catch {}
|
|
1295
1299
|
}
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
/**
|
|
1303
|
+
* `gen`, except that `return()` and `throw()` from its consumer call `abandon`
|
|
1304
|
+
* at once — before they are queued behind a `next()` still parked inside the
|
|
1305
|
+
* generator (the handler's `await`) — unless the generator already finished.
|
|
1306
|
+
*/
|
|
1307
|
+
function abortingOnReturn(gen, abandon) {
|
|
1308
|
+
let finished = false;
|
|
1309
|
+
const settle = (r) => {
|
|
1310
|
+
if (r.done) finished = true;
|
|
1311
|
+
return r;
|
|
1296
1312
|
};
|
|
1313
|
+
const wrapped = {
|
|
1314
|
+
next: (...args) => gen.next(...args).then(settle),
|
|
1315
|
+
return(value) {
|
|
1316
|
+
if (!finished) abandon();
|
|
1317
|
+
finished = true;
|
|
1318
|
+
return gen.return(value);
|
|
1319
|
+
},
|
|
1320
|
+
throw(error) {
|
|
1321
|
+
if (!finished) abandon(error);
|
|
1322
|
+
finished = true;
|
|
1323
|
+
return gen.throw(error);
|
|
1324
|
+
},
|
|
1325
|
+
[Symbol.asyncIterator]() {
|
|
1326
|
+
return wrapped;
|
|
1327
|
+
}
|
|
1328
|
+
};
|
|
1329
|
+
return asyncDisposable$2(wrapped, () => wrapped.return(void 0));
|
|
1297
1330
|
}
|
|
1298
1331
|
/**
|
|
1299
1332
|
* The one place that knows a runtime may not implement request body streams,
|
|
@@ -1532,10 +1565,11 @@ async function fetchOverDuplex(call, request, options = {}) {
|
|
|
1532
1565
|
* registered with any `webrun-streams-*` adapter's `serve`.
|
|
1533
1566
|
*/
|
|
1534
1567
|
function serveFetchOverDuplex(handler, options = {}) {
|
|
1535
|
-
return httpServe(async (env, body) => {
|
|
1568
|
+
return httpServe(async (env, body, exchange) => {
|
|
1536
1569
|
const reqInit = {
|
|
1537
1570
|
method: env.method,
|
|
1538
|
-
headers: forwardableHeaders(env.headers)
|
|
1571
|
+
headers: forwardableHeaders(env.headers),
|
|
1572
|
+
signal: exchange?.signal
|
|
1539
1573
|
};
|
|
1540
1574
|
let upload;
|
|
1541
1575
|
if (env.method !== "GET" && env.method !== "HEAD") {
|
|
@@ -1886,6 +1920,99 @@ async function* throughAbort(input, signal) {
|
|
|
1886
1920
|
}
|
|
1887
1921
|
}
|
|
1888
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
|
+
/**
|
|
1889
2016
|
* Makes `target` disposable with `await using`: installs `dispose` as its
|
|
1890
2017
|
* `[Symbol.asyncDispose]`, where the runtime defines the symbol. Hand-written
|
|
1891
2018
|
* iterators lose what a native generator gets from `AsyncIteratorPrototype`;
|
|
@@ -1957,6 +2084,61 @@ function returningAtOnce(gen, abandon) {
|
|
|
1957
2084
|
* tests and adapters assert on it.
|
|
1958
2085
|
*/
|
|
1959
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
|
+
};
|
|
1960
2142
|
/** Caller's input travels on this channel; the handler listens on it. */
|
|
1961
2143
|
const CHANNEL_IN = "in";
|
|
1962
2144
|
/** The handler's output travels on this channel; the caller listens on it. */
|
|
@@ -1969,7 +2151,9 @@ const CHANNEL_OUT = "out";
|
|
|
1969
2151
|
* same way on the other channel. Within each direction the next chunk is never
|
|
1970
2152
|
* sent until the previous one has been delivered *and* pulled past by the
|
|
1971
2153
|
* consumer (spec D11) — the reply to a chunk call *is* the confirmation, and
|
|
1972
|
-
* `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.
|
|
1973
2157
|
*
|
|
1974
2158
|
* A stream port carries exactly one invocation. To make several calls, open
|
|
1975
2159
|
* several ports: `mux.openPort({ kind: "stream" })` per call.
|
|
@@ -1986,22 +2170,49 @@ function serveDuplexOverPort(port, handler, options = {}) {
|
|
|
1986
2170
|
const notice = installAbortNotice(port, controller);
|
|
1987
2171
|
abortOnPortClose(port, controller);
|
|
1988
2172
|
const clock = installStreamTimeout(controller, options.timeout);
|
|
1989
|
-
const inbound = receiveChunks(port, CHANNEL_IN, controller, clock.touch);
|
|
2173
|
+
const inbound = receiveChunks(port, CHANNEL_IN, controller, clock.touch, notice);
|
|
1990
2174
|
let output;
|
|
1991
2175
|
try {
|
|
1992
2176
|
output = handler(inbound.stream);
|
|
1993
2177
|
} catch (err) {
|
|
1994
|
-
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, () => {});
|
|
1995
2179
|
pump.catch(() => {});
|
|
1996
2180
|
disarmClockWhenBothSidesSettle(clock, pump, inbound.ended);
|
|
1997
2181
|
return teardownOnce(controller, notice, clock, inbound, void 0);
|
|
1998
2182
|
}
|
|
1999
|
-
|
|
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);
|
|
2000
2188
|
pump.catch(() => {});
|
|
2001
2189
|
disarmClockWhenBothSidesSettle(clock, pump, inbound.ended);
|
|
2190
|
+
endServedStream(port, sent, inbound.over);
|
|
2002
2191
|
return teardownOnce(controller, notice, clock, inbound, output);
|
|
2003
2192
|
}
|
|
2004
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
|
+
/**
|
|
2005
2216
|
* A stream that completes normally has nobody left to call the returned
|
|
2006
2217
|
* teardown — the caller only knows the stream ended, not that it must also
|
|
2007
2218
|
* dispose the serve-side handle. Without this, the last `touch()`'s timer
|
|
@@ -2041,22 +2252,31 @@ function runCallerSide(port, input, options) {
|
|
|
2041
2252
|
const notice = installAbortNotice(port, controller);
|
|
2042
2253
|
abortOnPortClose(port, controller);
|
|
2043
2254
|
const clock = installStreamTimeout(controller, options.timeout);
|
|
2044
|
-
const inbound = receiveChunks(port, CHANNEL_OUT, controller, clock.touch);
|
|
2045
|
-
|
|
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
|
+
});
|
|
2046
2260
|
pump.catch(() => {});
|
|
2047
2261
|
return returningAtOnce((async function* () {
|
|
2262
|
+
let received = false;
|
|
2048
2263
|
try {
|
|
2049
2264
|
yield* inbound.stream;
|
|
2265
|
+
received = true;
|
|
2050
2266
|
} finally {
|
|
2267
|
+
const ended = received && sent;
|
|
2051
2268
|
if (!controller.signal.aborted) controller.abort(/* @__PURE__ */ new Error("webrun-rpc: the caller abandoned the stream"));
|
|
2052
|
-
notice.post();
|
|
2269
|
+
if (!ended) notice.post();
|
|
2053
2270
|
notice.stop();
|
|
2054
2271
|
clock.stop();
|
|
2055
2272
|
inbound.stop();
|
|
2056
2273
|
await pump.catch(() => {});
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2274
|
+
if (!(ended && endPort(port))) {
|
|
2275
|
+
notice.post();
|
|
2276
|
+
try {
|
|
2277
|
+
await port.close?.();
|
|
2278
|
+
} catch {}
|
|
2279
|
+
}
|
|
2060
2280
|
}
|
|
2061
2281
|
})(), () => {
|
|
2062
2282
|
if (!controller.signal.aborted) controller.abort(/* @__PURE__ */ new Error("webrun-rpc: the caller abandoned the stream"));
|
|
@@ -2150,7 +2370,7 @@ function installStreamTimeout(controller, timeout) {
|
|
|
2150
2370
|
* the local consumer has not started iterating, an inbound chunk waits for it —
|
|
2151
2371
|
* which is the correct backpressure, and different from having no listener.
|
|
2152
2372
|
*/
|
|
2153
|
-
function receiveChunks(port, channelName, controller, touch) {
|
|
2373
|
+
function receiveChunks(port, channelName, controller, touch, notice) {
|
|
2154
2374
|
let deliver;
|
|
2155
2375
|
const waiting = [];
|
|
2156
2376
|
let finished = false;
|
|
@@ -2160,25 +2380,64 @@ function receiveChunks(port, channelName, controller, touch) {
|
|
|
2160
2380
|
const ended = new Promise((resolve) => {
|
|
2161
2381
|
resolveEnded = resolve;
|
|
2162
2382
|
});
|
|
2383
|
+
let resolveOver = () => {};
|
|
2384
|
+
const over = new Promise((resolve) => {
|
|
2385
|
+
resolveOver = resolve;
|
|
2386
|
+
});
|
|
2163
2387
|
let aborted = false;
|
|
2164
2388
|
let abortReason;
|
|
2165
2389
|
const ready = () => deliver || finished ? Promise.resolve() : new Promise((r) => waiting.push(r));
|
|
2166
2390
|
const wake = () => {
|
|
2167
2391
|
for (const r of waiting.splice(0)) r();
|
|
2168
2392
|
};
|
|
2169
|
-
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);
|
|
2170
2437
|
touch();
|
|
2171
2438
|
if (poison) throw poison;
|
|
2172
|
-
if (
|
|
2173
|
-
|
|
2174
|
-
controller.abort(poison);
|
|
2175
|
-
setTimeout(() => {
|
|
2176
|
-
try {
|
|
2177
|
-
port.close?.();
|
|
2178
|
-
} catch {}
|
|
2179
|
-
}, 0);
|
|
2180
|
-
throw poison;
|
|
2181
|
-
}
|
|
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");
|
|
2182
2441
|
outstanding = true;
|
|
2183
2442
|
try {
|
|
2184
2443
|
await ready();
|
|
@@ -2188,15 +2447,60 @@ function receiveChunks(port, channelName, controller, touch) {
|
|
|
2188
2447
|
value,
|
|
2189
2448
|
error: error ? deserializeError(error) : void 0
|
|
2190
2449
|
});
|
|
2450
|
+
if (done && !error) resolveOver();
|
|
2191
2451
|
} finally {
|
|
2192
2452
|
outstanding = false;
|
|
2193
2453
|
}
|
|
2194
2454
|
if (done) resolveEnded();
|
|
2195
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);
|
|
2196
2499
|
const detach = () => {
|
|
2197
2500
|
finished = true;
|
|
2198
2501
|
wake();
|
|
2199
2502
|
off();
|
|
2503
|
+
port.removeEventListener("message", onChunk);
|
|
2200
2504
|
controller.signal.removeEventListener("abort", onAbort);
|
|
2201
2505
|
resolveEnded();
|
|
2202
2506
|
};
|
|
@@ -2226,21 +2530,35 @@ function receiveChunks(port, channelName, controller, touch) {
|
|
|
2226
2530
|
return detach;
|
|
2227
2531
|
}),
|
|
2228
2532
|
stop: detach,
|
|
2229
|
-
ended
|
|
2533
|
+
ended,
|
|
2534
|
+
over
|
|
2230
2535
|
};
|
|
2231
2536
|
}
|
|
2232
2537
|
/**
|
|
2233
2538
|
* The sending half of one direction: one `callPort` per chunk, one call
|
|
2234
|
-
* 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.
|
|
2235
2543
|
*/
|
|
2236
|
-
async function sendChunks(port, channelName, output, { maxMessageSize, log }, signal, touch) {
|
|
2237
|
-
const
|
|
2544
|
+
async function sendChunks(port, channelName, output, { maxMessageSize, log }, signal, touch, onSent) {
|
|
2545
|
+
const framed = maxMessageSize ? toChunks(maxMessageSize)(output) : output;
|
|
2546
|
+
const stream = throughAbort(framed, signal);
|
|
2547
|
+
const producer = output;
|
|
2548
|
+
const returnProducer = () => void Promise.resolve(producer.return?.()).catch(() => {});
|
|
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 });
|
|
2238
2556
|
try {
|
|
2239
2557
|
await sendIterator(async ({ done, value, error }) => {
|
|
2240
2558
|
if (signal.aborted) return;
|
|
2241
2559
|
const chunk = {
|
|
2242
2560
|
done,
|
|
2243
|
-
value,
|
|
2561
|
+
value: done ? value : chunkBytes(value),
|
|
2244
2562
|
error: error === void 0 ? void 0 : serializeError(error)
|
|
2245
2563
|
};
|
|
2246
2564
|
log?.("[duplexOverPort] send", {
|
|
@@ -2248,46 +2566,34 @@ async function sendChunks(port, channelName, output, { maxMessageSize, log }, si
|
|
|
2248
2566
|
done,
|
|
2249
2567
|
size: value?.byteLength
|
|
2250
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
|
+
}
|
|
2251
2580
|
await callPort(port, chunk, {
|
|
2252
2581
|
channelName,
|
|
2253
2582
|
timeout: NO_TIMEOUT,
|
|
2254
2583
|
signal
|
|
2255
2584
|
});
|
|
2256
2585
|
touch();
|
|
2586
|
+
if (done && !error) onSent();
|
|
2257
2587
|
}, stream);
|
|
2258
2588
|
} catch (err) {
|
|
2259
2589
|
if (signal.aborted) return;
|
|
2260
2590
|
throw err;
|
|
2591
|
+
} finally {
|
|
2592
|
+
signal.removeEventListener("abort", returnProducer);
|
|
2593
|
+
signal.removeEventListener("abort", onAborted);
|
|
2261
2594
|
}
|
|
2262
2595
|
}
|
|
2263
2596
|
/**
|
|
2264
|
-
* Runs `fn` on a later task, never a microtask: a producer looping on
|
|
2265
|
-
* `whenPortReady` must let the transport's own events run. `setImmediate`
|
|
2266
|
-
* where it exists; elsewhere a `MessageChannel`, because nested `setTimeout`s
|
|
2267
|
-
* are clamped to 4 ms in browsers.
|
|
2268
|
-
*/
|
|
2269
|
-
const setImmediateFn = globalThis.setImmediate;
|
|
2270
|
-
/** @internal */
|
|
2271
|
-
const nextTask = setImmediateFn ? (fn) => {
|
|
2272
|
-
setImmediateFn(fn);
|
|
2273
|
-
} : (() => {
|
|
2274
|
-
let channel;
|
|
2275
|
-
const queue = [];
|
|
2276
|
-
return (fn) => {
|
|
2277
|
-
if (!channel) {
|
|
2278
|
-
channel = new MessageChannel();
|
|
2279
|
-
channel.port1.onmessage = () => queue.shift()?.();
|
|
2280
|
-
}
|
|
2281
|
-
queue.push(fn);
|
|
2282
|
-
channel.port2.postMessage(0);
|
|
2283
|
-
};
|
|
2284
|
-
})();
|
|
2285
|
-
const states = /* @__PURE__ */ new WeakMap();
|
|
2286
|
-
/** @internal Registered by the multiplexer for each port with a window. */
|
|
2287
|
-
function setPortSendState(port, state) {
|
|
2288
|
-
states.set(port, state);
|
|
2289
|
-
}
|
|
2290
|
-
/**
|
|
2291
2597
|
* One virtual port.
|
|
2292
2598
|
*
|
|
2293
2599
|
* `deliver` and `markClosed` are deliberately not on `port`: the consumer holds
|
|
@@ -2365,6 +2671,21 @@ function newVirtualPort(send, requestClose) {
|
|
|
2365
2671
|
const KEEPALIVE_ID = Number.MAX_SAFE_INTEGER;
|
|
2366
2672
|
/** The longest delay a timer holds; past it `setInterval` fires at once. */
|
|
2367
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
|
+
}
|
|
2368
2689
|
/** Ceiling on concurrently open virtual ports. Bounds the id table only. */
|
|
2369
2690
|
const DEFAULT_MAX_PORTS = 1024;
|
|
2370
2691
|
/**
|
|
@@ -2396,10 +2717,18 @@ function estimateMessageSize(payload) {
|
|
|
2396
2717
|
*/
|
|
2397
2718
|
function multiplexPort(port, options) {
|
|
2398
2719
|
const { codec, onPort, side = "initiator", maxPorts = DEFAULT_MAX_PORTS, maxMessageSize, signal, window, frameSize, keepalive, onLeak } = options;
|
|
2720
|
+
const maxReassembledBytes = options.maxReassembledBytes ?? 67108864;
|
|
2399
2721
|
const misses = keepalive?.misses ?? 3;
|
|
2400
2722
|
if (keepalive && !(Number.isFinite(keepalive.intervalMs) && keepalive.intervalMs > 0 && keepalive.intervalMs <= MAX_TIMER_MS && Number.isSafeInteger(misses) && misses > 0)) throw new RangeError(`webrun-rpc: keepalive needs an intervalMs in (0, ${MAX_TIMER_MS}] and a positive integer misses`);
|
|
2401
2723
|
if (frameSize != null && !(Number.isSafeInteger(frameSize) && frameSize > 0)) throw new RangeError(`webrun-rpc: frameSize must be a positive integer, got ${frameSize}`);
|
|
2724
|
+
if (!(Number.isSafeInteger(maxReassembledBytes) && maxReassembledBytes > 0)) throw new RangeError(`webrun-rpc: maxReassembledBytes must be a positive integer, got ${maxReassembledBytes}`);
|
|
2402
2725
|
const open = /* @__PURE__ */ new Map();
|
|
2726
|
+
let reassembling = 0;
|
|
2727
|
+
const freePartial = (entry) => {
|
|
2728
|
+
if (!entry?.partial) return;
|
|
2729
|
+
reassembling -= entry.partial.charged;
|
|
2730
|
+
entry.partial = void 0;
|
|
2731
|
+
};
|
|
2403
2732
|
const leaks = onLeak ? new FinalizationRegistry(({ id, record, release }) => {
|
|
2404
2733
|
const entry = open.get(id);
|
|
2405
2734
|
if (entry?.handle !== record) return;
|
|
@@ -2420,6 +2749,8 @@ function multiplexPort(port, options) {
|
|
|
2420
2749
|
let muxClosed = false;
|
|
2421
2750
|
/** This end counts encoded bytes and reassembles frames. */
|
|
2422
2751
|
const exactCodec = codec.encode != null && codec.decode != null;
|
|
2752
|
+
/** What this end offers on a windowed port. */
|
|
2753
|
+
const caps = ownCaps(codec);
|
|
2423
2754
|
const post = (envelope, transfer) => {
|
|
2424
2755
|
if (muxClosed) return;
|
|
2425
2756
|
codecPost(envelope, transfer);
|
|
@@ -2477,13 +2808,39 @@ function multiplexPort(port, options) {
|
|
|
2477
2808
|
port.postMessage(bytes);
|
|
2478
2809
|
} catch {}
|
|
2479
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
|
+
};
|
|
2480
2837
|
const releaseWaiters = (c) => {
|
|
2481
2838
|
for (const w of c?.waiters.splice(0) ?? []) w.resolve();
|
|
2482
2839
|
};
|
|
2483
2840
|
const flush = (c) => {
|
|
2484
2841
|
while (c.queue.length) {
|
|
2485
2842
|
const next = c.queue[0];
|
|
2486
|
-
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;
|
|
2487
2844
|
c.queue.shift();
|
|
2488
2845
|
c.queued -= next.size;
|
|
2489
2846
|
c.outstanding += next.size;
|
|
@@ -2521,15 +2878,20 @@ function multiplexPort(port, options) {
|
|
|
2521
2878
|
enforced: false,
|
|
2522
2879
|
opener,
|
|
2523
2880
|
exact,
|
|
2881
|
+
caps: /* @__PURE__ */ new Set(),
|
|
2524
2882
|
outstanding: 0,
|
|
2525
2883
|
queue: [],
|
|
2526
2884
|
queued: 0,
|
|
2527
2885
|
unconsumed: 0,
|
|
2528
2886
|
waiters: [],
|
|
2529
2887
|
toGrant: 0,
|
|
2530
|
-
grantScheduled: false
|
|
2888
|
+
grantScheduled: false,
|
|
2889
|
+
ending: false,
|
|
2890
|
+
endSent: false,
|
|
2891
|
+
peerEnded: false
|
|
2531
2892
|
} : void 0;
|
|
2532
2893
|
const self = newVirtualPort((payload, transfer) => {
|
|
2894
|
+
if (c?.ending) return;
|
|
2533
2895
|
touch(id);
|
|
2534
2896
|
const envelope = {
|
|
2535
2897
|
type: "message",
|
|
@@ -2547,12 +2909,12 @@ function multiplexPort(port, options) {
|
|
|
2547
2909
|
...envelope,
|
|
2548
2910
|
payload: structuredClone(payload, { transfer })
|
|
2549
2911
|
} : void 0;
|
|
2550
|
-
const send = () => {
|
|
2912
|
+
const send = (whole = false) => {
|
|
2551
2913
|
if (!bytes) {
|
|
2552
2914
|
sendOnPort(id, held ? () => post(held) : () => post(envelope, transfer));
|
|
2553
2915
|
return;
|
|
2554
2916
|
}
|
|
2555
|
-
if (frameSize != null && c.enforced && c.exact && bytes.byteLength > frameSize) {
|
|
2917
|
+
if (!whole && frameSize != null && c.enforced && c.exact && bytes.byteLength > frameSize) {
|
|
2556
2918
|
for (let at = 0; at < bytes.byteLength; at += frameSize) {
|
|
2557
2919
|
const data = bytes.subarray(at, at + frameSize);
|
|
2558
2920
|
const frame = at + frameSize >= bytes.byteLength ? {
|
|
@@ -2582,10 +2944,11 @@ function multiplexPort(port, options) {
|
|
|
2582
2944
|
record.markClosed(reason);
|
|
2583
2945
|
});
|
|
2584
2946
|
function release(reason) {
|
|
2947
|
+
freePartial(open.get(id));
|
|
2585
2948
|
open.delete(id);
|
|
2586
2949
|
leaks?.unregister(record);
|
|
2587
2950
|
if (c?.queue.length) {
|
|
2588
|
-
for (const item of c.queue.splice(0)) item.send();
|
|
2951
|
+
for (const item of c.queue.splice(0)) item.send(true);
|
|
2589
2952
|
c.queued = 0;
|
|
2590
2953
|
}
|
|
2591
2954
|
sendOnPort(id, () => post({
|
|
@@ -2615,6 +2978,38 @@ function multiplexPort(port, options) {
|
|
|
2615
2978
|
});
|
|
2616
2979
|
})
|
|
2617
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
|
+
}));
|
|
2618
3013
|
}
|
|
2619
3014
|
const record = {
|
|
2620
3015
|
deliver: self.deliver,
|
|
@@ -2666,16 +3061,24 @@ function multiplexPort(port, options) {
|
|
|
2666
3061
|
}
|
|
2667
3062
|
const handle = attach(envelope.id, envelope.meta, envelope.window, false, envelope.exact === true && exactCodec);
|
|
2668
3063
|
const credit = open.get(envelope.id)?.credit;
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
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
|
+
}
|
|
2679
3082
|
let accepted = false;
|
|
2680
3083
|
if (onPort) try {
|
|
2681
3084
|
accepted = onPort(handle.port, envelope.meta) !== false;
|
|
@@ -2703,6 +3106,7 @@ function multiplexPort(port, options) {
|
|
|
2703
3106
|
c.exact = false;
|
|
2704
3107
|
c.outstanding = 0;
|
|
2705
3108
|
}
|
|
3109
|
+
c.caps = sharedCaps(caps, envelope.caps) ?? /* @__PURE__ */ new Set();
|
|
2706
3110
|
post({
|
|
2707
3111
|
type: "credit",
|
|
2708
3112
|
id: envelope.id,
|
|
@@ -2714,6 +3118,10 @@ function multiplexPort(port, options) {
|
|
|
2714
3118
|
flush(c);
|
|
2715
3119
|
return;
|
|
2716
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
|
+
}
|
|
2717
3125
|
if (envelope.type === "message") {
|
|
2718
3126
|
const sizeOf = (c) => (c.exact ? byteLengthOf(event.data) : void 0) ?? estimateMessageSize(envelope.payload);
|
|
2719
3127
|
if (!entry) return;
|
|
@@ -2724,15 +3132,29 @@ function multiplexPort(port, options) {
|
|
|
2724
3132
|
if (envelope.type === "frame") {
|
|
2725
3133
|
if (!entry?.credit?.exact || !codec.decode || !(envelope.data instanceof Uint8Array)) return;
|
|
2726
3134
|
touch(envelope.id);
|
|
3135
|
+
const c = entry.credit;
|
|
3136
|
+
const owed = c.unconsumed + c.toGrant;
|
|
2727
3137
|
const partial = entry.partial ?? {
|
|
2728
3138
|
chunks: [],
|
|
2729
|
-
bytes: 0
|
|
3139
|
+
bytes: 0,
|
|
3140
|
+
owed,
|
|
3141
|
+
charged: 0,
|
|
3142
|
+
...owed > 0 ? { room: c.window - owed } : {}
|
|
2730
3143
|
};
|
|
2731
3144
|
entry.partial = partial;
|
|
3145
|
+
const size = envelope.data.byteLength;
|
|
3146
|
+
const charged = partial.room == null ? Math.max(0, partial.bytes + size - c.window) : 0;
|
|
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;
|
|
3148
|
+
if (overrun) {
|
|
3149
|
+
failPort(envelope.id, entry, /* @__PURE__ */ new RangeError(`webrun-rpc: a framed message passed ${overrun}`), "max-reassembled-bytes");
|
|
3150
|
+
return;
|
|
3151
|
+
}
|
|
3152
|
+
partial.bytes += size;
|
|
3153
|
+
reassembling += charged - partial.charged;
|
|
3154
|
+
partial.charged = charged;
|
|
2732
3155
|
partial.chunks.push(envelope.data);
|
|
2733
|
-
partial.bytes += envelope.data.byteLength;
|
|
2734
3156
|
if (!envelope.last) return;
|
|
2735
|
-
entry
|
|
3157
|
+
freePartial(entry);
|
|
2736
3158
|
const whole = new Uint8Array(partial.bytes);
|
|
2737
3159
|
let at = 0;
|
|
2738
3160
|
for (const chunk of partial.chunks) {
|
|
@@ -2744,8 +3166,22 @@ function multiplexPort(port, options) {
|
|
|
2744
3166
|
entry.handle.deliver(inner.payload, whole.byteLength);
|
|
2745
3167
|
return;
|
|
2746
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
|
+
}
|
|
2747
3182
|
dropOutgoing(envelope.id);
|
|
2748
3183
|
if (!entry) return;
|
|
3184
|
+
freePartial(entry);
|
|
2749
3185
|
open.delete(envelope.id);
|
|
2750
3186
|
releaseWaiters(entry.credit);
|
|
2751
3187
|
entry.handle.markClosed(/* @__PURE__ */ new Error("webrun-rpc: the peer closed the port"));
|
|
@@ -2824,12 +3260,14 @@ function multiplexPort(port, options) {
|
|
|
2824
3260
|
id,
|
|
2825
3261
|
meta,
|
|
2826
3262
|
window,
|
|
2827
|
-
exact: true
|
|
3263
|
+
exact: true,
|
|
3264
|
+
caps: [...caps]
|
|
2828
3265
|
} : {
|
|
2829
3266
|
type: "open",
|
|
2830
3267
|
id,
|
|
2831
3268
|
meta,
|
|
2832
|
-
window
|
|
3269
|
+
window,
|
|
3270
|
+
caps: [...caps]
|
|
2833
3271
|
} : {
|
|
2834
3272
|
type: "open",
|
|
2835
3273
|
id,
|
|
@@ -2936,14 +3374,17 @@ const serve = async ({ mux: factory, timeout }, handler) => {
|
|
|
2936
3374
|
const mux = await factory((port) => {
|
|
2937
3375
|
let off;
|
|
2938
3376
|
let finished = false;
|
|
2939
|
-
const tracked = (input) =>
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
3377
|
+
const tracked = (input) => {
|
|
3378
|
+
const output = handler(input);
|
|
3379
|
+
return returningAtOnce((async function* () {
|
|
3380
|
+
try {
|
|
3381
|
+
yield* output;
|
|
3382
|
+
} finally {
|
|
3383
|
+
finished = true;
|
|
3384
|
+
if (off) live.delete(off);
|
|
3385
|
+
}
|
|
3386
|
+
})(), () => void output.return(void 0).catch(() => {}));
|
|
3387
|
+
};
|
|
2947
3388
|
off = serveDuplexOverPort(port, tracked, streamOptions);
|
|
2948
3389
|
if (!finished) live.add(off);
|
|
2949
3390
|
});
|
|
@@ -2981,7 +3422,7 @@ function isEnvelope(value) {
|
|
|
2981
3422
|
const candidate = value;
|
|
2982
3423
|
if (typeof candidate.id !== "number") return false;
|
|
2983
3424
|
if (!Number.isInteger(candidate.id) || candidate.id < 0) return false;
|
|
2984
|
-
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";
|
|
2985
3426
|
}
|
|
2986
3427
|
/**
|
|
2987
3428
|
* For ports whose messages are structured values — a real `MessagePort`, a
|
|
@@ -2993,6 +3434,7 @@ function isEnvelope(value) {
|
|
|
2993
3434
|
* not send anything a byte codec could not also carry.
|
|
2994
3435
|
*/
|
|
2995
3436
|
const structuredCodec = {
|
|
3437
|
+
halfClose: true,
|
|
2996
3438
|
post(port, envelope, transfer) {
|
|
2997
3439
|
if (transfer && transfer.length > 0) port.postMessage(envelope, transfer);
|
|
2998
3440
|
else port.postMessage(envelope);
|
|
@@ -3030,10 +3472,15 @@ function asyncDisposable(target, dispose) {
|
|
|
3030
3472
|
* own `handleStreams` / `sendStream` (no backpressure); version 2 is
|
|
3031
3473
|
* webrun-rpc's `connect` / `serve` over `overPipe` with webrun-http-streams'
|
|
3032
3474
|
* `fetchOverDuplex` / `serveFetchOverDuplex`: one port per link, calls
|
|
3033
|
-
* multiplexed on it, each call a stream with
|
|
3034
|
-
* 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.
|
|
3035
3480
|
*/
|
|
3036
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;
|
|
3037
3484
|
/**
|
|
3038
3485
|
* Why a request failed when its link went away under it: the far side (the
|
|
3039
3486
|
* handler, or the ServiceWorker) was lost. The request may or may not have
|
|
@@ -3052,12 +3499,13 @@ var LinkLostError = class extends Error {
|
|
|
3052
3499
|
*/
|
|
3053
3500
|
const LINK_CLOSE = "webrun-http:link-close";
|
|
3054
3501
|
/** Opens the calling end of a link on `port` (one port per link). */
|
|
3055
|
-
async function connectHttpLink(port, { streamTimeout, maxPorts } = {}) {
|
|
3502
|
+
async function connectHttpLink(port, { streamTimeout, maxPorts, window = DEFAULT_HTTP_LINK_WINDOW } = {}) {
|
|
3056
3503
|
const { call, close } = await connect({
|
|
3057
3504
|
mux: overPipe(port, {
|
|
3058
3505
|
codec: structuredCodec,
|
|
3059
3506
|
side: "initiator",
|
|
3060
|
-
maxPorts
|
|
3507
|
+
maxPorts,
|
|
3508
|
+
window
|
|
3061
3509
|
}),
|
|
3062
3510
|
timeout: streamTimeout
|
|
3063
3511
|
});
|
|
@@ -3588,6 +4036,7 @@ var PortSocketCore = class extends EventTarget {
|
|
|
3588
4036
|
code,
|
|
3589
4037
|
reason
|
|
3590
4038
|
});
|
|
4039
|
+
if (!Number.isFinite(this._closeTimeoutMs)) return;
|
|
3591
4040
|
this._closeTimer = this._timers.setTimeout(() => this._finish(1006, "", false), this._closeTimeoutMs);
|
|
3592
4041
|
}
|
|
3593
4042
|
_onCloseFrame(code, reason) {
|
|
@@ -3666,7 +4115,8 @@ function isApplicationCloseCode(code) {
|
|
|
3666
4115
|
* `whenDrained(limit)` waits for it to fall: the backpressure a producer
|
|
3667
4116
|
* applies.
|
|
3668
4117
|
* - When the client's pings stop for three heartbeat periods (a closed tab, a
|
|
3669
|
-
* 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.
|
|
3670
4120
|
*/
|
|
3671
4121
|
var ServerWebSocket = class extends PortSocketCore {
|
|
3672
4122
|
static CONNECTING = 0;
|
|
@@ -3675,55 +4125,150 @@ var ServerWebSocket = class extends PortSocketCore {
|
|
|
3675
4125
|
static CLOSED = 3;
|
|
3676
4126
|
url;
|
|
3677
4127
|
protocol;
|
|
4128
|
+
_binaryType = "arraybuffer";
|
|
4129
|
+
_options;
|
|
3678
4130
|
_accepted = false;
|
|
4131
|
+
_detached = false;
|
|
3679
4132
|
_queue = [];
|
|
3680
4133
|
_heartbeatMs = DEFAULT_HEARTBEAT_MS;
|
|
3681
4134
|
_lastPing = Date.now();
|
|
4135
|
+
/** Whether a ping (with a period a timer takes) arrived already. */
|
|
4136
|
+
_pinged = false;
|
|
3682
4137
|
_watch;
|
|
3683
4138
|
constructor(port, options = {}) {
|
|
3684
4139
|
super({
|
|
3685
4140
|
timers: options.timers,
|
|
3686
4141
|
closeTimeoutMs: options.closeTimeoutMs
|
|
3687
4142
|
});
|
|
4143
|
+
this._options = options;
|
|
3688
4144
|
this.url = options.url ?? "";
|
|
3689
4145
|
this.protocol = options.protocol ?? "";
|
|
3690
4146
|
this._attach(port);
|
|
3691
4147
|
this._state = 1;
|
|
3692
|
-
this._watchHeartbeat();
|
|
4148
|
+
if (options.watch !== "first-ping") this._watchHeartbeat();
|
|
3693
4149
|
}
|
|
3694
4150
|
/** Whether `accept()` was called. */
|
|
3695
4151
|
get accepted() {
|
|
3696
4152
|
return this._accepted;
|
|
3697
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
|
+
}
|
|
3698
4217
|
/** Starts delivering events, the queued ones first. */
|
|
3699
4218
|
accept() {
|
|
4219
|
+
if (this._detached) throw new DOMException("This end was detached; it cannot be accepted.", "InvalidStateError");
|
|
3700
4220
|
if (this._accepted) return;
|
|
3701
4221
|
this._accepted = true;
|
|
3702
|
-
for (const frame of this._queue.splice(0))
|
|
4222
|
+
for (const frame of this._queue.splice(0)) this._deliver(frame);
|
|
3703
4223
|
}
|
|
3704
4224
|
send(data) {
|
|
3705
|
-
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");
|
|
3706
4227
|
this._sendData(isBinary(data) ? toArrayBuffer(data) : String(data));
|
|
3707
4228
|
}
|
|
3708
|
-
close(code
|
|
3709
|
-
|
|
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");
|
|
3710
4240
|
if (utf8Length(reason) > 123) throw new DOMException("The close reason is longer than 123 bytes.", "SyntaxError");
|
|
3711
|
-
this._startClose(code, reason);
|
|
3712
4241
|
}
|
|
3713
4242
|
_deliver(frame) {
|
|
3714
4243
|
if (!this._accepted) {
|
|
3715
4244
|
this._queue.push(frame);
|
|
3716
4245
|
return;
|
|
3717
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
|
+
}
|
|
3718
4252
|
super._deliver(frame);
|
|
3719
4253
|
}
|
|
3720
4254
|
_dispatchMessage(data) {
|
|
3721
|
-
this.
|
|
4255
|
+
const delivered = typeof data !== "string" && this._binaryType === "blob" ? new Blob([data]) : data;
|
|
4256
|
+
this.dispatchEvent(new MessageEvent("message", { data: delivered }));
|
|
3722
4257
|
}
|
|
3723
4258
|
_onPing(hb) {
|
|
3724
4259
|
this._post({ t: "q" });
|
|
3725
4260
|
this._lastPing = Date.now();
|
|
3726
|
-
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)) {
|
|
3727
4272
|
this._heartbeatMs = hb;
|
|
3728
4273
|
this._watchHeartbeat();
|
|
3729
4274
|
}
|
|
@@ -3745,6 +4290,11 @@ function isSendableCloseCode(code) {
|
|
|
3745
4290
|
if (code >= 3e3 && code <= 4999) return true;
|
|
3746
4291
|
return code >= 1e3 && code <= 1014 && code !== 1004 && code !== 1005 && code !== 1006;
|
|
3747
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
|
+
}
|
|
3748
4298
|
//#endregion
|
|
3749
4299
|
//#region src/ws/upgrade.ts
|
|
3750
4300
|
/**
|
|
@@ -3776,6 +4326,7 @@ function newWebSocketUpgradeRequest(url, { protocols = [], headers } = {}) {
|
|
|
3776
4326
|
all.set("connection", "Upgrade");
|
|
3777
4327
|
all.set("sec-websocket-version", "13");
|
|
3778
4328
|
if (!all.has("sec-websocket-key")) all.set("sec-websocket-key", randomKey());
|
|
4329
|
+
all.delete("sec-websocket-protocol");
|
|
3779
4330
|
if (protocols.length > 0) all.set("sec-websocket-protocol", protocols.join(", "));
|
|
3780
4331
|
if (!all.has("x-webrun-ws-version")) all.set(WS_VERSION_HEADER, `2`);
|
|
3781
4332
|
return withHeaders(new Request(httpUrl, {
|
|
@@ -3835,6 +4386,23 @@ function upgradeWebSocket(request, options = {}) {
|
|
|
3835
4386
|
url: request.url,
|
|
3836
4387
|
protocol
|
|
3837
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 = "") {
|
|
3838
4406
|
const response = new Response(null, {
|
|
3839
4407
|
status: 200,
|
|
3840
4408
|
headers: protocol ? { "sec-websocket-protocol": protocol } : {}
|
|
@@ -3848,23 +4416,16 @@ function upgradeWebSocket(request, options = {}) {
|
|
|
3848
4416
|
value: "Switching Protocols",
|
|
3849
4417
|
configurable: true
|
|
3850
4418
|
},
|
|
3851
|
-
webSocket: {
|
|
3852
|
-
value: socket,
|
|
3853
|
-
configurable: true
|
|
3854
|
-
},
|
|
3855
4419
|
[UPGRADE]: {
|
|
3856
4420
|
value: {
|
|
3857
|
-
port
|
|
4421
|
+
port,
|
|
3858
4422
|
protocol
|
|
3859
4423
|
},
|
|
3860
4424
|
configurable: true,
|
|
3861
4425
|
writable: true
|
|
3862
4426
|
}
|
|
3863
4427
|
});
|
|
3864
|
-
return
|
|
3865
|
-
socket,
|
|
3866
|
-
response
|
|
3867
|
-
};
|
|
4428
|
+
return response;
|
|
3868
4429
|
}
|
|
3869
4430
|
/**
|
|
3870
4431
|
* The port to hand to the caller, for a response from `upgradeWebSocket`.
|
|
@@ -3932,6 +4493,51 @@ function randomKey() {
|
|
|
3932
4493
|
return btoa(String.fromCharCode(...bytes));
|
|
3933
4494
|
}
|
|
3934
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
|
|
3935
4541
|
//#region src/sw/handler-endpoint.ts
|
|
3936
4542
|
/** The message calls through a lost ServiceWorker fail with. */
|
|
3937
4543
|
const ROUTER_LOST_MESSAGE = "The ServiceWorker was lost while this call was in flight";
|
|
@@ -4015,8 +4621,11 @@ var HandlerEndpoint = class {
|
|
|
4015
4621
|
this._startKeepAlive();
|
|
4016
4622
|
return await link.up.send(request);
|
|
4017
4623
|
}
|
|
4018
|
-
/**
|
|
4019
|
-
|
|
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) {
|
|
4020
4629
|
const link = await this._linkWith("ctl");
|
|
4021
4630
|
const id = this._sockets.newId();
|
|
4022
4631
|
const answer = this._sockets.wait(id, this.options.wsConnectTimeoutMs ?? 15e3, `the socket request for ${url}`);
|
|
@@ -4024,7 +4633,8 @@ var HandlerEndpoint = class {
|
|
|
4024
4633
|
t: "ws-open",
|
|
4025
4634
|
id,
|
|
4026
4635
|
url,
|
|
4027
|
-
protocols
|
|
4636
|
+
protocols,
|
|
4637
|
+
headers: headerPairs(headers)
|
|
4028
4638
|
});
|
|
4029
4639
|
return await answer;
|
|
4030
4640
|
}
|
|
@@ -4443,8 +5053,9 @@ async function awaitServiceWorkerControl(registration, { timeout = DEFAULT_SERVI
|
|
|
4443
5053
|
* Returns a function that stops answering.
|
|
4444
5054
|
*/
|
|
4445
5055
|
function handleClaimRequests(self) {
|
|
4446
|
-
|
|
4447
|
-
|
|
5056
|
+
const sw = self;
|
|
5057
|
+
return handleChannelCalls(sw, CLAIM_CALL, (event) => {
|
|
5058
|
+
const claimed = sw.clients.claim().then(() => true);
|
|
4448
5059
|
event.waitUntil?.(claimed);
|
|
4449
5060
|
return claimed;
|
|
4450
5061
|
});
|
|
@@ -4716,43 +5327,22 @@ async function sendHttpRequest(communicationPort, request) {
|
|
|
4716
5327
|
})(request);
|
|
4717
5328
|
}
|
|
4718
5329
|
//#endregion
|
|
4719
|
-
//#region src/sw/
|
|
4720
|
-
/**
|
|
4721
|
-
* The messages between pages, Dedicated Workers and the same-origin
|
|
4722
|
-
* ServiceWorker. Internal: the shapes may change between versions, which is
|
|
4723
|
-
* why every hand-shake carries `HTTP_LINK_PROTOCOL`.
|
|
4724
|
-
*
|
|
4725
|
-
* THREE PORTS PER HANDLER LINK. A page makes the channels and hands one end of
|
|
4726
|
-
* each to the handler (itself, or a worker) and the other to the
|
|
4727
|
-
* ServiceWorker; after that it carries nothing:
|
|
4728
|
-
*
|
|
4729
|
-
* - `down`: ServiceWorker → handler, the requests the handler serves;
|
|
4730
|
-
* - `ctl`: a raw port next to them, for what HTTP over a stream cannot carry,
|
|
4731
|
-
* transferred `MessagePort`s (WebSocket hand-over);
|
|
4732
|
-
* - `up` (workers only): handler → ServiceWorker, the requests the handler
|
|
4733
|
-
* sends through the router (to other handlers).
|
|
4734
|
-
*/
|
|
4735
|
-
/** Page → ServiceWorker: attach a handler link. The name is the one version 1 used. */
|
|
4736
|
-
const ATTACH_CALL = "UPDATE_COMMUNICATION_PORT";
|
|
4737
|
-
/** Page → ServiceWorker: remove a handler link now. */
|
|
4738
|
-
const DETACH_CALL = "webrun-http:detach";
|
|
4739
|
-
/** Page → ServiceWorker: stop routing to a link, let its requests finish, then remove it. */
|
|
4740
|
-
const DRAIN_CALL = "webrun-http:drain";
|
|
4741
|
-
/** Page → ServiceWorker: open a WebSocket to a handler. */
|
|
4742
|
-
const WS_CONNECT_CALL = "webrun-http:ws-connect";
|
|
4743
|
-
/** ServiceWorker → pages: attach your links again (the ServiceWorker restarted). */
|
|
4744
|
-
const NEED_LINKS_CALL = "webrun-http:need-links";
|
|
5330
|
+
//#region src/sw/http-sw-dispatcher.ts
|
|
4745
5331
|
/**
|
|
4746
|
-
*
|
|
4747
|
-
*
|
|
4748
|
-
* 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.
|
|
4749
5334
|
*/
|
|
4750
|
-
|
|
4751
|
-
const
|
|
4752
|
-
|
|
4753
|
-
const
|
|
4754
|
-
|
|
4755
|
-
|
|
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
|
+
}
|
|
4756
5346
|
/** One attached handler link, as the ServiceWorker holds it. */
|
|
4757
5347
|
var HandlerEntry = class {
|
|
4758
5348
|
key;
|
|
@@ -4792,7 +5382,8 @@ const TIMED_OUT = Symbol("timed-out");
|
|
|
4792
5382
|
* (`SwHttpAdapter.register`) or in Dedicated Workers
|
|
4793
5383
|
* (`SwHttpAdapter.attachWorker` + `serveHttpInWorker`).
|
|
4794
5384
|
*
|
|
4795
|
-
* 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
|
|
4796
5387
|
* `route` option picks one per request. Requests and responses stream with
|
|
4797
5388
|
* backpressure in both directions, and an aborted fetch reaches the handler
|
|
4798
5389
|
* as a cancellation (webrun-rpc + webrun-http-streams).
|
|
@@ -4811,6 +5402,8 @@ const TIMED_OUT = Symbol("timed-out");
|
|
|
4811
5402
|
*/
|
|
4812
5403
|
var SwHttpDispatcher = class {
|
|
4813
5404
|
self;
|
|
5405
|
+
/** `self`, with the WebWorker lib's types this module compiles with. */
|
|
5406
|
+
_sw;
|
|
4814
5407
|
log;
|
|
4815
5408
|
options;
|
|
4816
5409
|
table = new HandlerTable();
|
|
@@ -4833,19 +5426,22 @@ var SwHttpDispatcher = class {
|
|
|
4833
5426
|
constructor(options) {
|
|
4834
5427
|
this.options = options;
|
|
4835
5428
|
this.self = options.self;
|
|
5429
|
+
this._sw = options.self;
|
|
4836
5430
|
this.log = options.log ?? (() => {});
|
|
4837
5431
|
if (locksAvailable()) this.routerLock = newLockName("webrun-http-router");
|
|
4838
5432
|
}
|
|
4839
5433
|
get scope() {
|
|
4840
|
-
return `${new URL("./", this.
|
|
5434
|
+
return this._sw.registration?.scope ?? `${new URL("./", this._sw.location.href)}`;
|
|
4841
5435
|
}
|
|
4842
|
-
/** The key `url` belongs to
|
|
5436
|
+
/** The key `url` belongs to (the `key` option; by default its first path segment under the scope). */
|
|
4843
5437
|
keyOf(url) {
|
|
4844
|
-
|
|
4845
|
-
|
|
4846
|
-
|
|
4847
|
-
|
|
4848
|
-
|
|
5438
|
+
let parsed;
|
|
5439
|
+
try {
|
|
5440
|
+
parsed = new URL(`${url}`);
|
|
5441
|
+
} catch {
|
|
5442
|
+
return;
|
|
5443
|
+
}
|
|
5444
|
+
return (this.options.key ?? scopeKey)(parsed, this.scope);
|
|
4849
5445
|
}
|
|
4850
5446
|
/** Whether `key` has ever been served on this worker. */
|
|
4851
5447
|
isClaimedKey(key) {
|
|
@@ -4856,7 +5452,7 @@ var SwHttpDispatcher = class {
|
|
|
4856
5452
|
return this.claimedKeys.has(key) || this.table.all(key).length > 0;
|
|
4857
5453
|
}
|
|
4858
5454
|
start() {
|
|
4859
|
-
const self = this.
|
|
5455
|
+
const self = this._sw;
|
|
4860
5456
|
if (this.routerLock) this._routerHeld = holdLock(this.routerLock).then(() => {});
|
|
4861
5457
|
this._restored = this._restoreClaimedKeys();
|
|
4862
5458
|
const latch = () => {
|
|
@@ -4876,8 +5472,8 @@ var SwHttpDispatcher = class {
|
|
|
4876
5472
|
return true;
|
|
4877
5473
|
}), handleChannelCalls(self, WS_CONNECT_CALL, async (event, params) => {
|
|
4878
5474
|
if (!this._isOwnClient(event)) throw new Error("Only this origin's pages may open sockets");
|
|
4879
|
-
const { url, protocols } = params;
|
|
4880
|
-
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"));
|
|
4881
5477
|
return withTransfer(connection, [connection.port]);
|
|
4882
5478
|
}));
|
|
4883
5479
|
const onMessage = (event) => {
|
|
@@ -4895,10 +5491,10 @@ var SwHttpDispatcher = class {
|
|
|
4895
5491
|
this.log("Claim control over all clients.");
|
|
4896
5492
|
event.waitUntil(self.clients.claim());
|
|
4897
5493
|
};
|
|
4898
|
-
const onFetch = (event) => this.
|
|
5494
|
+
const onFetch = (event) => void this.handleFetch(event);
|
|
4899
5495
|
self.addEventListener("install", onInstall);
|
|
4900
5496
|
self.addEventListener("activate", onActivate);
|
|
4901
|
-
self.addEventListener("fetch", onFetch);
|
|
5497
|
+
if (this.options.handleFetch !== false) self.addEventListener("fetch", onFetch);
|
|
4902
5498
|
this._cleanup.push(() => {
|
|
4903
5499
|
self.removeEventListener("install", onInstall);
|
|
4904
5500
|
self.removeEventListener("activate", onActivate);
|
|
@@ -4913,37 +5509,60 @@ var SwHttpDispatcher = class {
|
|
|
4913
5509
|
}
|
|
4914
5510
|
/**
|
|
4915
5511
|
* Answers `request` as a fetch under the scope would be: through the
|
|
4916
|
-
* handler its key routes to. A url
|
|
4917
|
-
* 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.
|
|
4918
5514
|
*/
|
|
4919
5515
|
async dispatch(request) {
|
|
4920
5516
|
const key = this.keyOf(request.url);
|
|
4921
5517
|
if (key === void 0) return await fetch(request);
|
|
4922
5518
|
await this._restored.catch(() => {});
|
|
4923
5519
|
const entry = await this._acquire(request, key);
|
|
4924
|
-
if (entry === TIMED_OUT) return
|
|
4925
|
-
status: 503,
|
|
4926
|
-
statusText: "Service Unavailable (no handler attached in time)"
|
|
4927
|
-
});
|
|
5520
|
+
if (entry === TIMED_OUT) return await this._failed(request, { kind: "timeout" });
|
|
4928
5521
|
if (!entry) {
|
|
4929
5522
|
if (!this.serves(key)) return await fetch(request);
|
|
4930
|
-
return
|
|
4931
|
-
status: 404,
|
|
4932
|
-
statusText: "Not Found (no active handler)"
|
|
4933
|
-
});
|
|
5523
|
+
return await this._failed(request, { kind: "gone" });
|
|
4934
5524
|
}
|
|
4935
5525
|
try {
|
|
4936
5526
|
return await entry.link.send(request);
|
|
4937
5527
|
} catch (error) {
|
|
4938
5528
|
this.log("[dispatch] failed", key, error);
|
|
4939
|
-
return
|
|
4940
|
-
|
|
4941
|
-
|
|
4942
|
-
headers: { "content-type": "text/plain; charset=utf-8" }
|
|
5529
|
+
return await this._failed(request, {
|
|
5530
|
+
kind: "lost",
|
|
5531
|
+
error
|
|
4943
5532
|
});
|
|
4944
5533
|
}
|
|
4945
5534
|
}
|
|
4946
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
|
+
/**
|
|
4947
5566
|
* Opens a socket to the handler `url` routes to: the handler accepts or
|
|
4948
5567
|
* refuses it like an HTTP upgrade, and the socket's port comes back. After
|
|
4949
5568
|
* that the socket runs between the caller and the handler directly; this
|
|
@@ -4955,7 +5574,7 @@ var SwHttpDispatcher = class {
|
|
|
4955
5574
|
headers
|
|
4956
5575
|
});
|
|
4957
5576
|
const key = this.keyOf(request.url);
|
|
4958
|
-
if (key === void 0) throw new Error(`${url}
|
|
5577
|
+
if (key === void 0) throw new Error(`${url} belongs to no key of this ServiceWorker`);
|
|
4959
5578
|
await this._restored.catch(() => {});
|
|
4960
5579
|
const entry = await this._acquire(request, key);
|
|
4961
5580
|
if (!entry || entry === TIMED_OUT) throw new Error(`No handler serves ${url}`);
|
|
@@ -4971,20 +5590,47 @@ var SwHttpDispatcher = class {
|
|
|
4971
5590
|
});
|
|
4972
5591
|
return await answer;
|
|
4973
5592
|
}
|
|
4974
|
-
|
|
4975
|
-
|
|
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;
|
|
4976
5611
|
const key = this.keyOf(request.url);
|
|
4977
|
-
if (key === void 0) return;
|
|
5612
|
+
if (key === void 0) return false;
|
|
4978
5613
|
if (this._restoreSettled) {
|
|
4979
|
-
if (this.serves(key))
|
|
4980
|
-
|
|
4981
|
-
|
|
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;
|
|
4982
5623
|
}
|
|
4983
5624
|
event.respondWith((async () => {
|
|
4984
5625
|
await this._restored.catch(() => {});
|
|
4985
|
-
if (
|
|
4986
|
-
return await this.
|
|
5626
|
+
if (this.serves(key)) return await this.dispatch(request);
|
|
5627
|
+
return navigation ? await this._unserved(request) : await fetch(request);
|
|
4987
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);
|
|
4988
5634
|
}
|
|
4989
5635
|
async _attach(event, params, ports) {
|
|
4990
5636
|
const clientId = event.source?.id;
|
|
@@ -5003,8 +5649,11 @@ var SwHttpDispatcher = class {
|
|
|
5003
5649
|
}
|
|
5004
5650
|
const ctl = params.ctl ? rest.shift() : void 0;
|
|
5005
5651
|
const up = params.up ? rest.shift() : void 0;
|
|
5006
|
-
const streamTimeout = this.options
|
|
5007
|
-
const link = await connectHttpLink(down, {
|
|
5652
|
+
const { streamTimeout, window } = this.options;
|
|
5653
|
+
const link = await connectHttpLink(down, {
|
|
5654
|
+
streamTimeout,
|
|
5655
|
+
window
|
|
5656
|
+
});
|
|
5008
5657
|
const entry = new HandlerEntry(params.key, params.id ?? clientId ?? crypto.randomUUID(), params.owner ?? crypto.randomUUID(), params.meta, link, ctl, clientId, params.ownerLock);
|
|
5009
5658
|
if (up) entry.closeUp = await serveHttpLink(up, (request) => this.dispatch(request), { streamTimeout });
|
|
5010
5659
|
if (ctl) ctl.onmessage = (message) => this._onControl(entry, message.data);
|
|
@@ -5083,20 +5732,35 @@ var SwHttpDispatcher = class {
|
|
|
5083
5732
|
* its key, waiting for one when there is none.
|
|
5084
5733
|
*/
|
|
5085
5734
|
async _acquire(request, key) {
|
|
5735
|
+
const deadline = Date.now() + (this.options.attachTimeoutMs ?? 15e3);
|
|
5086
5736
|
const picked = await this._pick(request, key);
|
|
5087
5737
|
if (picked) return picked;
|
|
5088
|
-
if (this.table.all(key).length
|
|
5089
|
-
|
|
5090
|
-
|
|
5091
|
-
|
|
5092
|
-
|
|
5093
|
-
|
|
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
|
+
}
|
|
5094
5758
|
}
|
|
5095
5759
|
async _pick(request, key) {
|
|
5096
5760
|
let live = this.table.live(key);
|
|
5097
5761
|
for (const entry of live) {
|
|
5098
5762
|
if (entry.ownerLock || !entry.clientId) continue;
|
|
5099
|
-
if (!await this.
|
|
5763
|
+
if (!await this._sw.clients.get(entry.clientId)) this._close(entry, new LinkLostError(HANDLER_LOST_MESSAGE));
|
|
5100
5764
|
}
|
|
5101
5765
|
live = this.table.live(key);
|
|
5102
5766
|
if (live.length === 0) return void 0;
|
|
@@ -5120,7 +5784,7 @@ var SwHttpDispatcher = class {
|
|
|
5120
5784
|
}
|
|
5121
5785
|
async _linkRound(key) {
|
|
5122
5786
|
const timeout = this.options.relinkTimeoutMs ?? 3e3;
|
|
5123
|
-
const windows = await this.
|
|
5787
|
+
const windows = await this._sw.clients.matchAll({
|
|
5124
5788
|
type: "window",
|
|
5125
5789
|
includeUncontrolled: true
|
|
5126
5790
|
});
|
|
@@ -5158,7 +5822,7 @@ var SwHttpDispatcher = class {
|
|
|
5158
5822
|
entry.sockets.fail(message.id, new Error(message.message));
|
|
5159
5823
|
return;
|
|
5160
5824
|
case "ws-open":
|
|
5161
|
-
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({
|
|
5162
5826
|
t: "ws-opened",
|
|
5163
5827
|
id: message.id,
|
|
5164
5828
|
protocol,
|
|
@@ -5175,7 +5839,7 @@ var SwHttpDispatcher = class {
|
|
|
5175
5839
|
const source = event.source;
|
|
5176
5840
|
if (!source?.url) return false;
|
|
5177
5841
|
try {
|
|
5178
|
-
return new URL(source.url).origin === this.
|
|
5842
|
+
return new URL(source.url).origin === this._sw.location.origin;
|
|
5179
5843
|
} catch {
|
|
5180
5844
|
return false;
|
|
5181
5845
|
}
|
|
@@ -5199,6 +5863,7 @@ var SwHttpDispatcher = class {
|
|
|
5199
5863
|
* that stops it.
|
|
5200
5864
|
*/
|
|
5201
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()");
|
|
5202
5867
|
const dispatcher = new SwHttpDispatcher(options);
|
|
5203
5868
|
dispatcher.start();
|
|
5204
5869
|
return () => {
|
|
@@ -5373,6 +6038,8 @@ var SwPortHandler = class {
|
|
|
5373
6038
|
*/
|
|
5374
6039
|
var SwPortDispatcher = class {
|
|
5375
6040
|
self;
|
|
6041
|
+
/** `self`, with the WebWorker lib's types this module compiles with. */
|
|
6042
|
+
_sw;
|
|
5376
6043
|
log;
|
|
5377
6044
|
handlersIndex = /* @__PURE__ */ new Map();
|
|
5378
6045
|
/**
|
|
@@ -5386,10 +6053,11 @@ var SwPortDispatcher = class {
|
|
|
5386
6053
|
_activationPromise;
|
|
5387
6054
|
constructor({ self, log = () => {} }) {
|
|
5388
6055
|
this.self = self;
|
|
6056
|
+
this._sw = self;
|
|
5389
6057
|
this.log = log;
|
|
5390
6058
|
}
|
|
5391
6059
|
get scope() {
|
|
5392
|
-
return `${new URL("./", this.
|
|
6060
|
+
return this._sw.registration?.scope ?? `${new URL("./", this._sw.location.href)}`;
|
|
5393
6061
|
}
|
|
5394
6062
|
/**
|
|
5395
6063
|
* Look up the active channel for a key. Returns `undefined` when there
|
|
@@ -5402,7 +6070,7 @@ var SwPortDispatcher = class {
|
|
|
5402
6070
|
if (channelInfo.key !== key) continue;
|
|
5403
6071
|
const clientId = channelInfo.clientId;
|
|
5404
6072
|
if (!clientId) continue;
|
|
5405
|
-
if (!await this.
|
|
6073
|
+
if (!await this._sw.clients.get(clientId)) {
|
|
5406
6074
|
this.handlersIndex.delete(clientId);
|
|
5407
6075
|
continue;
|
|
5408
6076
|
}
|
|
@@ -5414,8 +6082,8 @@ var SwPortDispatcher = class {
|
|
|
5414
6082
|
return this.claimedKeys.has(key);
|
|
5415
6083
|
}
|
|
5416
6084
|
start() {
|
|
5417
|
-
const stopClaims = handleClaimRequests(this.
|
|
5418
|
-
const stopPortUpdates = handleChannelCalls(this.
|
|
6085
|
+
const stopClaims = handleClaimRequests(this._sw);
|
|
6086
|
+
const stopPortUpdates = handleChannelCalls(this._sw, "UPDATE_COMMUNICATION_PORT", async (event, channelInfo, port) => {
|
|
5419
6087
|
const clientId = event.source?.id;
|
|
5420
6088
|
this.log("[UPDATE_COMMUNICATION_PORT]", clientId, channelInfo);
|
|
5421
6089
|
await this._updateChannelInfo({
|
|
@@ -5429,14 +6097,14 @@ var SwPortDispatcher = class {
|
|
|
5429
6097
|
stopClaims();
|
|
5430
6098
|
stopPortUpdates();
|
|
5431
6099
|
};
|
|
5432
|
-
this.
|
|
6100
|
+
this._sw.addEventListener("install", (event) => {
|
|
5433
6101
|
this.log("Skip waiting on install.", event);
|
|
5434
|
-
this.
|
|
6102
|
+
this._sw.skipWaiting();
|
|
5435
6103
|
});
|
|
5436
|
-
this.
|
|
6104
|
+
this._sw.addEventListener("activate", (event) => {
|
|
5437
6105
|
this.log("Claim control over all clients.", event);
|
|
5438
6106
|
event.waitUntil((async () => {
|
|
5439
|
-
await this.
|
|
6107
|
+
await this._sw.clients.claim();
|
|
5440
6108
|
await this._checkActivation();
|
|
5441
6109
|
})());
|
|
5442
6110
|
});
|
|
@@ -5486,7 +6154,7 @@ var SwPortDispatcher = class {
|
|
|
5486
6154
|
for (const clientId of this.handlersIndex.keys()) ids.add(clientId);
|
|
5487
6155
|
const index = /* @__PURE__ */ new Map();
|
|
5488
6156
|
for (const clientId of ids) {
|
|
5489
|
-
const client = await this.
|
|
6157
|
+
const client = await this._sw.clients.get(clientId);
|
|
5490
6158
|
if (client) index.set(clientId, client);
|
|
5491
6159
|
else this.handlersIndex.delete(clientId);
|
|
5492
6160
|
}
|
|
@@ -5766,7 +6434,10 @@ var SwHttpAdapter = class {
|
|
|
5766
6434
|
return this.options.key;
|
|
5767
6435
|
}
|
|
5768
6436
|
get scope() {
|
|
5769
|
-
|
|
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;
|
|
5770
6441
|
}
|
|
5771
6442
|
/**
|
|
5772
6443
|
* The scope as a url on the worker's origin. Resolved against the worker's
|
|
@@ -5777,6 +6448,8 @@ var SwHttpAdapter = class {
|
|
|
5777
6448
|
* sits next to it).
|
|
5778
6449
|
*/
|
|
5779
6450
|
get rootUrl() {
|
|
6451
|
+
const registration = this.options.registration;
|
|
6452
|
+
if (registration && !this.options.serviceWorkerUrl) return new URL(this.scope, registration.scope);
|
|
5780
6453
|
const base = this.options.serviceWorkerUrl ? this.serviceWorkerUrl : import.meta.url;
|
|
5781
6454
|
return new URL(this.scope, base);
|
|
5782
6455
|
}
|
|
@@ -5798,7 +6471,10 @@ var SwHttpAdapter = class {
|
|
|
5798
6471
|
*/
|
|
5799
6472
|
async start() {
|
|
5800
6473
|
if (!this._registrationPromise) this._registrationPromise = (async () => {
|
|
5801
|
-
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
|
+
});
|
|
5802
6478
|
this._registration = registration;
|
|
5803
6479
|
const stopListening = handleChannelCalls(navigator.serviceWorker, NEED_LINKS_CALL, async (_event, params) => {
|
|
5804
6480
|
await this.relink(params?.routerLock);
|
|
@@ -5828,7 +6504,7 @@ var SwHttpAdapter = class {
|
|
|
5828
6504
|
})();
|
|
5829
6505
|
return this._registrationPromise;
|
|
5830
6506
|
}
|
|
5831
|
-
/** Detaches every link, and unregisters the worker. */
|
|
6507
|
+
/** Detaches every link, and unregisters the worker (unless the page passed its `registration`). */
|
|
5832
6508
|
async stop() {
|
|
5833
6509
|
for (const cleanup of this._cleanup.splice(0)) cleanup();
|
|
5834
6510
|
for (const worker of [...this._workers]) await worker.detach().catch(() => {});
|
|
@@ -5845,7 +6521,7 @@ var SwHttpAdapter = class {
|
|
|
5845
6521
|
this._registration = void 0;
|
|
5846
6522
|
this._registrationPromise = void 0;
|
|
5847
6523
|
try {
|
|
5848
|
-
await registration?.unregister();
|
|
6524
|
+
if (!this.options.registration) await registration?.unregister();
|
|
5849
6525
|
} catch (error) {
|
|
5850
6526
|
console.log("Service Worker unregistration failed: ", error);
|
|
5851
6527
|
}
|
|
@@ -5919,10 +6595,10 @@ var SwHttpAdapter = class {
|
|
|
5919
6595
|
/**
|
|
5920
6596
|
* Opens the transport of a socket to the handler `url` routes to: a handler
|
|
5921
6597
|
* of this page directly, any other through the ServiceWorker. Resolves once
|
|
5922
|
-
* the handler accepted.
|
|
5923
|
-
* `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)`.
|
|
5924
6600
|
*/
|
|
5925
|
-
connectWebSocket = async (url, protocols = []) => {
|
|
6601
|
+
connectWebSocket = async (url, protocols = [], headers) => {
|
|
5926
6602
|
const httpUrl = new URL(`${url}`, globalThis.location?.href);
|
|
5927
6603
|
if (httpUrl.protocol === "ws:") httpUrl.protocol = "http:";
|
|
5928
6604
|
else if (httpUrl.protocol === "wss:") httpUrl.protocol = "https:";
|
|
@@ -5930,7 +6606,8 @@ var SwHttpAdapter = class {
|
|
|
5930
6606
|
if (local) {
|
|
5931
6607
|
const result = await serveWebSocketUpgrade(local, {
|
|
5932
6608
|
url: `${httpUrl}`,
|
|
5933
|
-
protocols
|
|
6609
|
+
protocols,
|
|
6610
|
+
headers
|
|
5934
6611
|
});
|
|
5935
6612
|
if (!result.ok) throw new Error(result.message);
|
|
5936
6613
|
return {
|
|
@@ -5941,7 +6618,8 @@ var SwHttpAdapter = class {
|
|
|
5941
6618
|
await this.start();
|
|
5942
6619
|
return await callChannelWithin(this._callTimeout(), this._worker(), WS_CONNECT_CALL, {
|
|
5943
6620
|
url: `${httpUrl}`,
|
|
5944
|
-
protocols
|
|
6621
|
+
protocols,
|
|
6622
|
+
headers: headerPairs(headers)
|
|
5945
6623
|
});
|
|
5946
6624
|
};
|
|
5947
6625
|
/**
|
|
@@ -6237,11 +6915,11 @@ function serveHttpInWorker(handler, options = {}) {
|
|
|
6237
6915
|
};
|
|
6238
6916
|
scope.addEventListener("message", onMessage);
|
|
6239
6917
|
const baseUrl = () => globalThis.location?.href;
|
|
6240
|
-
const connectWebSocket = (url, protocols = []) => {
|
|
6918
|
+
const connectWebSocket = (url, protocols = [], headers) => {
|
|
6241
6919
|
const httpUrl = new URL(`${url}`, baseUrl());
|
|
6242
6920
|
if (httpUrl.protocol === "ws:") httpUrl.protocol = "http:";
|
|
6243
6921
|
else if (httpUrl.protocol === "wss:") httpUrl.protocol = "https:";
|
|
6244
|
-
return endpoint.connectWebSocket(`${httpUrl}`, protocols);
|
|
6922
|
+
return endpoint.connectWebSocket(`${httpUrl}`, protocols, headers);
|
|
6245
6923
|
};
|
|
6246
6924
|
let WebSocketClass;
|
|
6247
6925
|
return {
|
|
@@ -6268,4 +6946,4 @@ function serveHttpInWorker(handler, options = {}) {
|
|
|
6268
6946
|
};
|
|
6269
6947
|
}
|
|
6270
6948
|
//#endregion
|
|
6271
|
-
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 };
|