@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.
Files changed (59) hide show
  1. package/README.md +155 -10
  2. package/dist/core/index.d.ts +1 -0
  3. package/dist/core/index.d.ts.map +1 -1
  4. package/dist/core/service-worker-control.d.ts +2 -1
  5. package/dist/core/service-worker-control.d.ts.map +1 -1
  6. package/dist/core/sw-scope.d.ts +31 -0
  7. package/dist/core/sw-scope.d.ts.map +1 -0
  8. package/dist/index.js +908 -221
  9. package/dist/link/http-link.d.ts +16 -3
  10. package/dist/link/http-link.d.ts.map +1 -1
  11. package/dist/relay/index-sw.d.ts +8 -2
  12. package/dist/relay/index-sw.d.ts.map +1 -1
  13. package/dist/relay/index.d.ts +1 -1
  14. package/dist/relay/index.d.ts.map +1 -1
  15. package/dist/relay-sw.js +470 -81
  16. package/dist/relay-worker.d.ts +1 -0
  17. package/dist/relay-worker.d.ts.map +1 -1
  18. package/dist/relay-worker.js +470 -81
  19. package/dist/sw/handler-endpoint.d.ts +5 -2
  20. package/dist/sw/handler-endpoint.d.ts.map +1 -1
  21. package/dist/sw/http-sw-dispatcher.d.ts +114 -7
  22. package/dist/sw/http-sw-dispatcher.d.ts.map +1 -1
  23. package/dist/sw/index.d.ts +1 -0
  24. package/dist/sw/index.d.ts.map +1 -1
  25. package/dist/sw/protocol.d.ts +3 -0
  26. package/dist/sw/protocol.d.ts.map +1 -1
  27. package/dist/sw/sw-dispatcher.d.ts +5 -2
  28. package/dist/sw/sw-dispatcher.d.ts.map +1 -1
  29. package/dist/sw/sw-http-adapter.d.ts +19 -4
  30. package/dist/sw/sw-http-adapter.d.ts.map +1 -1
  31. package/dist/sw/worker-service.d.ts +5 -2
  32. package/dist/sw/worker-service.d.ts.map +1 -1
  33. package/dist/sw-worker.js +681 -141
  34. package/dist/sw.js +896 -218
  35. package/dist/worker.js +689 -115
  36. package/dist/ws/server.d.ts +50 -2
  37. package/dist/ws/server.d.ts.map +1 -1
  38. package/dist/ws/socket-core.d.ts +5 -2
  39. package/dist/ws/socket-core.d.ts.map +1 -1
  40. package/dist/ws/upgrade.d.ts +12 -1
  41. package/dist/ws/upgrade.d.ts.map +1 -1
  42. package/package.json +4 -3
  43. package/src/core/index.ts +1 -0
  44. package/src/core/service-worker-control.ts +5 -3
  45. package/src/core/sw-scope.ts +27 -0
  46. package/src/link/http-link.ts +20 -4
  47. package/src/relay/index-sw.ts +15 -2
  48. package/src/relay/index.ts +2 -2
  49. package/src/relay-worker.ts +1 -0
  50. package/src/sw/handler-endpoint.ts +17 -4
  51. package/src/sw/http-sw-dispatcher.ts +267 -67
  52. package/src/sw/index.ts +5 -0
  53. package/src/sw/protocol.ts +11 -1
  54. package/src/sw/sw-dispatcher.ts +17 -11
  55. package/src/sw/sw-http-adapter.ts +36 -10
  56. package/src/sw/worker-service.ts +11 -4
  57. package/src/ws/server.ts +166 -11
  58. package/src/ws/socket-core.ts +7 -2
  59. 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 async function* httpHandlerDuplex(input) {
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
- const pump = sendChunks(port, CHANNEL_OUT, output, options, controller.signal, clock.touch);
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
- const pump = sendChunks(port, CHANNEL_IN, input, options, controller.signal, clock.touch);
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
- try {
2058
- await port.close?.();
2059
- } catch {}
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 off = listenPort(port, async ({ done, value, error }) => {
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 (outstanding) {
2173
- poison = /* @__PURE__ */ new Error("webrun-rpc: peer sent a second unconfirmed chunk; the stream port is closed");
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 stream = throughAbort(maxMessageSize ? toChunks(maxMessageSize)(output) : output, signal);
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
- if (credit) post(credit.exact ? {
2670
- type: "credit",
2671
- id: envelope.id,
2672
- bytes: 0,
2673
- exact: true
2674
- } : {
2675
- type: "credit",
2676
- id: envelope.id,
2677
- bytes: 0
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.partial = void 0;
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) => (async function* () {
2940
- try {
2941
- yield* handler(input);
2942
- } finally {
2943
- finished = true;
2944
- if (off) live.delete(off);
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 a one-chunk window in both
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)) super._deliver(frame);
4222
+ for (const frame of this._queue.splice(0)) this._deliver(frame);
3703
4223
  }
3704
4224
  send(data) {
3705
- if (!this._accepted) throw new DOMException("accept() the socket before sending on it.", "InvalidStateError");
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 = 1e3, reason = "") {
3709
- if (!isSendableCloseCode(code)) throw new DOMException(`${code} is not a close code an endpoint may send.`, "InvalidAccessError");
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.dispatchEvent(new MessageEvent("message", { data }));
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 (hb !== this._heartbeatMs) {
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: channel.port2,
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
- /** Opens a socket to the handler `url` routes to, through the ServiceWorker. */
4019
- async connectWebSocket(url, protocols = []) {
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
- return handleChannelCalls(self, CLAIM_CALL, (event) => {
4447
- const claimed = self.clients.claim().then(() => true);
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/protocol.ts
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
- * Page → ServiceWorker, plain messages (no reply). A message to the
4747
- * ServiceWorker object is an event: it starts a stopped worker and resets its
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
- const WAKE_MESSAGE = "webrun-http:wake";
4751
- const KEEPALIVE_MESSAGE = "webrun-http:keepalive";
4752
- /** Page ↔ worker: what `attachWorker` and `serveHttpInWorker` exchange. */
4753
- const WORKER_HELLO = "webrun-http:worker-hello";
4754
- //#endregion
4755
- //#region src/sw/http-sw-dispatcher.ts
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`. Of those, the
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.self.location.href)}`;
5434
+ return this._sw.registration?.scope ?? `${new URL("./", this._sw.location.href)}`;
4841
5435
  }
4842
- /** The key `url` belongs to: its first path segment under the scope. */
5436
+ /** The key `url` belongs to (the `key` option; by default its first path segment under the scope). */
4843
5437
  keyOf(url) {
4844
- const href = `${url}`;
4845
- const scope = this.scope;
4846
- if (!href.startsWith(scope)) return void 0;
4847
- const key = href.substring(scope.length).replace(/[?#].*$/, "").split("/")[0];
4848
- return key ? decodeURIComponent(key) : void 0;
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.self;
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._onFetch(event);
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 outside the scope, or of a key this
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 new Response(null, {
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 new Response(null, {
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 new Response(errorMessage(error), {
4940
- status: 502,
4941
- statusText: "Bad Gateway",
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} is outside this ServiceWorker's scope`);
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
- _onFetch(event) {
4975
- const request = event.request;
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)) event.respondWith(this.dispatch(request));
4980
- else if (request.mode === "navigate") event.respondWith(fetch(request));
4981
- return;
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 (!this.serves(key)) return await fetch(request);
4986
- return await this.dispatch(request);
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.streamTimeout;
5007
- const link = await connectHttpLink(down, { streamTimeout });
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 > 0) return await this.table.waitFor(key, this.options.attachTimeoutMs ?? 15e3, this.table.live(key).length > 0) ? await this._pick(request, key) ?? TIMED_OUT : TIMED_OUT;
5089
- const vacantUntil = this._vacant.get(key) ?? 0;
5090
- if (vacantUntil > Date.now()) return await this.table.waitFor(key, vacantUntil - Date.now()) ? await this._pick(request, key) ?? TIMED_OUT : TIMED_OUT;
5091
- if (!this.serves(key)) return void 0;
5092
- await this._askForLinks(key);
5093
- return await this._pick(request, key);
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.self.clients.get(entry.clientId)) this._close(entry, new LinkLostError(HANDLER_LOST_MESSAGE));
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.self.clients.matchAll({
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.self.location.origin;
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.self.location.href)}`;
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.self.clients.get(clientId)) {
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.self);
5418
- const stopPortUpdates = handleChannelCalls(this.self, "UPDATE_COMMUNICATION_PORT", async (event, channelInfo, port) => {
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.self.addEventListener("install", (event) => {
6100
+ this._sw.addEventListener("install", (event) => {
5433
6101
  this.log("Skip waiting on install.", event);
5434
- this.self.skipWaiting();
6102
+ this._sw.skipWaiting();
5435
6103
  });
5436
- this.self.addEventListener("activate", (event) => {
6104
+ this._sw.addEventListener("activate", (event) => {
5437
6105
  this.log("Claim control over all clients.", event);
5438
6106
  event.waitUntil((async () => {
5439
- await this.self.clients.claim();
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.self.clients.get(clientId);
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
- return this.options.scope ?? new URL("./", this.serviceWorkerUrl).pathname;
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, { scope: this.scope });
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. An arrow function, so it can be handed around:
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 };