@statewalker/webrun-http-browser 0.10.0 → 0.12.2

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