@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/worker.js CHANGED
@@ -647,7 +647,7 @@ const LAST_CHUNK = /* @__PURE__ */ new Uint8Array([
647
647
  13,
648
648
  10
649
649
  ]);
650
- const encoder = /* @__PURE__ */ new TextEncoder();
650
+ const encoder$2 = /* @__PURE__ */ new TextEncoder();
651
651
  /**
652
652
  * Chunk sizes are written in HEXADECIMAL — a 21-byte chunk is `15`. Writing
653
653
  * them in decimal is the defect note 15 found in @libp2p/http, and it is the
@@ -660,7 +660,7 @@ const encoder = /* @__PURE__ */ new TextEncoder();
660
660
  async function* encodeChunked(body) {
661
661
  for await (const chunk of body) {
662
662
  if (chunk.byteLength === 0) continue;
663
- yield encoder.encode(`${chunk.byteLength.toString(16)}\r\n`);
663
+ yield encoder$2.encode(`${chunk.byteLength.toString(16)}\r\n`);
664
664
  yield chunk;
665
665
  yield CRLF;
666
666
  }
@@ -1806,6 +1806,99 @@ async function* throughAbort(input, signal) {
1806
1806
  }
1807
1807
  }
1808
1808
  /**
1809
+ * Runs `fn` on a later task, never a microtask: a producer looping on
1810
+ * `whenPortReady` must let the transport's own events run. `setImmediate`
1811
+ * where it exists; elsewhere a `MessageChannel`, because nested `setTimeout`s
1812
+ * are clamped to 4 ms in browsers.
1813
+ */
1814
+ const setImmediateFn = globalThis.setImmediate;
1815
+ /** @internal */
1816
+ const nextTask = setImmediateFn ? (fn) => {
1817
+ setImmediateFn(fn);
1818
+ } : (() => {
1819
+ let channel;
1820
+ const queue = [];
1821
+ return (fn) => {
1822
+ if (!channel) {
1823
+ channel = new MessageChannel();
1824
+ channel.port1.onmessage = () => queue.shift()?.();
1825
+ }
1826
+ queue.push(fn);
1827
+ channel.port2.postMessage(0);
1828
+ };
1829
+ })();
1830
+ const states = /* @__PURE__ */ new WeakMap();
1831
+ /** @internal Registered by the multiplexer for each port with a window. */
1832
+ function setPortSendState(port, state) {
1833
+ states.set(port, state);
1834
+ }
1835
+ /**
1836
+ * Bytes queued on `port` because its window is spent (0 for a port without
1837
+ * flow control). A sender that should not queue without bound waits for
1838
+ * `whenPortReady` before posting more.
1839
+ */
1840
+ function portBufferedAmount(port) {
1841
+ return states.get(port)?.bufferedAmount() ?? 0;
1842
+ }
1843
+ /**
1844
+ * Resolves once at most `below` bytes are queued on `port`, always on a later
1845
+ * task (for a port without flow control, on the next one).
1846
+ */
1847
+ function whenPortReady(port, below = 0) {
1848
+ return states.get(port)?.ready(below) ?? new Promise((resolve) => nextTask(resolve));
1849
+ }
1850
+ const agreements = /* @__PURE__ */ new WeakMap();
1851
+ /** @internal Registered by the multiplexer for each port with a window. */
1852
+ function setPortAgreement(port, agreement) {
1853
+ agreements.set(port, agreement);
1854
+ }
1855
+ /**
1856
+ * @internal What has been agreed with the peer on `port` so far; `undefined`
1857
+ * on a port without a window. Not exported from the package.
1858
+ */
1859
+ function portAgreement(port) {
1860
+ return agreements.get(port)?.();
1861
+ }
1862
+ const ends = /* @__PURE__ */ new WeakMap();
1863
+ /** @internal Registered by the multiplexer for each port with a window. */
1864
+ function setPortEndState(port, state) {
1865
+ ends.set(port, state);
1866
+ }
1867
+ /**
1868
+ * @internal End this side's direction of `port`: an `end` is queued behind
1869
+ * everything already posted on it, and anything posted afterwards is dropped.
1870
+ * `false`, and nothing changes, when `end` was not agreed (no window, the
1871
+ * handshake not answered yet, or a peer that does not speak it): the caller
1872
+ * then closes the port. Never waits on the peer. Not exported from the package.
1873
+ */
1874
+ function endPort(port) {
1875
+ return ends.get(port)?.end() ?? false;
1876
+ }
1877
+ /**
1878
+ * @internal Resolves once the peer has ended its direction of `port`. It never
1879
+ * settles otherwise: not on a port where `end` is not agreed when it is asked
1880
+ * (no window, the handshake not answered yet, a peer or codec without it), and
1881
+ * not when the port closes first, so a caller races it against the port's
1882
+ * close signal. Not exported from the package.
1883
+ */
1884
+ function whenPortEnded(port) {
1885
+ let waiting = peerEnds.get(port);
1886
+ if (!waiting) {
1887
+ let resolve = () => {};
1888
+ const promise = new Promise((r) => {
1889
+ resolve = r;
1890
+ });
1891
+ if (!ends.get(port)?.watchPeerEnd(resolve)) return promise;
1892
+ waiting = {
1893
+ promise,
1894
+ resolve
1895
+ };
1896
+ peerEnds.set(port, waiting);
1897
+ }
1898
+ return waiting.promise;
1899
+ }
1900
+ const peerEnds = /* @__PURE__ */ new WeakMap();
1901
+ /**
1809
1902
  * Makes `target` disposable with `await using`: installs `dispose` as its
1810
1903
  * `[Symbol.asyncDispose]`, where the runtime defines the symbol. Hand-written
1811
1904
  * iterators lose what a native generator gets from `AsyncIteratorPrototype`;
@@ -1877,6 +1970,61 @@ function returningAtOnce(gen, abandon) {
1877
1970
  * tests and adapters assert on it.
1878
1971
  */
1879
1972
  const STREAM_ABORT = "webrun-rpc:stream-abort";
1973
+ /**
1974
+ * The `type` of a pipelined chunk: a plain port message, not a `callPort`
1975
+ * request, sent only on a port where both ends agreed `pipeline`. The window's
1976
+ * credit is its confirmation.
1977
+ */
1978
+ const CHUNK$1 = "webrun-rpc:chunk";
1979
+ /**
1980
+ * What a pipelined chunk counts beyond its bytes, against the window, on the
1981
+ * receiving side. Both codecs' windows count more for the chunk's envelope
1982
+ * (the estimate at least 64, msgpack's keys alone more than this), so an honest
1983
+ * sender never makes the receiver count past the window; a sender of empty
1984
+ * chunks still cannot have it hold an unbounded number of them.
1985
+ */
1986
+ const CHUNK_OVERHEAD = 32;
1987
+ /**
1988
+ * The bytes of `view` as a plain `Uint8Array` that owns its whole buffer:
1989
+ * `view` itself when it already is one, else a copy. Structured clone carries
1990
+ * and keeps a view's whole `ArrayBuffer`, and a byte codec may decode into
1991
+ * views of the frame, while the window and the receiver's count see only the
1992
+ * view: a 4 KiB `subarray` of a 4 MiB buffer would cost 4 MiB per chunk.
1993
+ * Copied with the `Uint8Array` constructor, not `slice()`: a Node `Buffer`'s
1994
+ * `slice()` is another view on the same memory.
1995
+ */
1996
+ function ownBytes(view) {
1997
+ if (view instanceof Uint8Array && view.constructor === Uint8Array && view.byteOffset === 0 && view.byteLength === view.buffer.byteLength) return view;
1998
+ return new Uint8Array(new Uint8Array(view.buffer, view.byteOffset, view.byteLength));
1999
+ }
2000
+ /**
2001
+ * A producer's chunk as bytes this end may send, in either mode: any view or
2002
+ * `ArrayBuffer`, normalised by `ownBytes`. Anything else fails the stream here,
2003
+ * as the producer's own error, rather than reaching a peer that would refuse it.
2004
+ */
2005
+ /**
2006
+ * A received confirmed chunk's value, as its own bytes, put back in place of
2007
+ * the original in `chunk`: `listenPort` keeps the request until the handler
2008
+ * returns, and the original would keep its whole buffer that long. A function
2009
+ * of its own, so that no temporary of the suspended handler holds the
2010
+ * original either.
2011
+ */
2012
+ function adoptValue(chunk) {
2013
+ const original = chunk.value;
2014
+ if (ArrayBuffer.isView(original)) chunk.value = ownBytes(original);
2015
+ return chunk.value;
2016
+ }
2017
+ function chunkBytes(value) {
2018
+ if (ArrayBuffer.isView(value)) return ownBytes(value);
2019
+ if (value instanceof ArrayBuffer || Object.prototype.toString.call(value) === "[object ArrayBuffer]") return new Uint8Array(value);
2020
+ 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}`);
2021
+ }
2022
+ const MIXED_MESSAGE = "webrun-rpc: peer mixed confirmed and pipelined chunks; the stream port is closed";
2023
+ /** This end may pipeline its own chunks: agreed, and the window holds them. */
2024
+ const mayPipeline = (port) => {
2025
+ const agreement = portAgreement(port);
2026
+ return !!agreement?.enforced && agreement.caps.has("pipeline");
2027
+ };
1880
2028
  /** Caller's input travels on this channel; the handler listens on it. */
1881
2029
  const CHANNEL_IN = "in";
1882
2030
  /** The handler's output travels on this channel; the caller listens on it. */
@@ -1889,7 +2037,9 @@ const CHANNEL_OUT = "out";
1889
2037
  * same way on the other channel. Within each direction the next chunk is never
1890
2038
  * sent until the previous one has been delivered *and* pulled past by the
1891
2039
  * consumer (spec D11) — the reply to a chunk call *is* the confirmation, and
1892
- * `listenPort` withholds it until then.
2040
+ * `listenPort` withholds it until then. On a port whose window both ends
2041
+ * speak and that agreed `pipeline`, chunks go as plain messages instead, and
2042
+ * the window's credit is the confirmation: up to a window is in flight.
1893
2043
  *
1894
2044
  * A stream port carries exactly one invocation. To make several calls, open
1895
2045
  * several ports: `mux.openPort({ kind: "stream" })` per call.
@@ -1906,22 +2056,49 @@ function serveDuplexOverPort(port, handler, options = {}) {
1906
2056
  const notice = installAbortNotice(port, controller);
1907
2057
  abortOnPortClose(port, controller);
1908
2058
  const clock = installStreamTimeout(controller, options.timeout);
1909
- const inbound = receiveChunks(port, CHANNEL_IN, controller, clock.touch);
2059
+ const inbound = receiveChunks(port, CHANNEL_IN, controller, clock.touch, notice);
1910
2060
  let output;
1911
2061
  try {
1912
2062
  output = handler(inbound.stream);
1913
2063
  } catch (err) {
1914
- const pump = sendChunks(port, CHANNEL_OUT, failing(err), options, controller.signal, clock.touch);
2064
+ const pump = sendChunks(port, CHANNEL_OUT, failing(err), options, controller.signal, clock.touch, () => {});
1915
2065
  pump.catch(() => {});
1916
2066
  disarmClockWhenBothSidesSettle(clock, pump, inbound.ended);
1917
2067
  return teardownOnce(controller, notice, clock, inbound, void 0);
1918
2068
  }
1919
- const pump = sendChunks(port, CHANNEL_OUT, output, options, controller.signal, clock.touch);
2069
+ let onSent = () => {};
2070
+ const sent = new Promise((resolve) => {
2071
+ onSent = resolve;
2072
+ });
2073
+ const pump = sendChunks(port, CHANNEL_OUT, output, options, controller.signal, clock.touch, onSent);
1920
2074
  pump.catch(() => {});
1921
2075
  disarmClockWhenBothSidesSettle(clock, pump, inbound.ended);
2076
+ endServedStream(port, sent, inbound.over);
1922
2077
  return teardownOnce(controller, notice, clock, inbound, output);
1923
2078
  }
1924
2079
  /**
2080
+ * The serving side's normal finish. Once the handler's last chunk has gone
2081
+ * without an error, and no more input is coming (`over`: the caller's last
2082
+ * chunk arrived without an error and was read, or the caller ended its
2083
+ * direction behind it, for a handler that did not read all its input), this
2084
+ * side ends its direction too, where `end` was agreed. Without `end` nothing
2085
+ * happens here: the caller closes the port. A stream that never gets that far
2086
+ * was abandoned, and the caller's notice and `close` free the port; this then
2087
+ * never resumes. It needs no race against the port's close signal: nothing
2088
+ * awaits it, and what keeps it waiting is the input's watcher, a listener of
2089
+ * the port that a close drops, after which the wait is collected.
2090
+ *
2091
+ * Nothing this side still posts is lost behind its `end`. The caller ends only
2092
+ * once its own last chunk was confirmed, so no confirmation is owed to it
2093
+ * here, and what the freed port's close signal aborts has settled: the last
2094
+ * chunk has left, and input nobody reads is dropped, as on a close.
2095
+ */
2096
+ async function endServedStream(port, sent, over) {
2097
+ await sent;
2098
+ await over;
2099
+ endPort(port);
2100
+ }
2101
+ /**
1925
2102
  * A stream that completes normally has nobody left to call the returned
1926
2103
  * teardown — the caller only knows the stream ended, not that it must also
1927
2104
  * dispose the serve-side handle. Without this, the last `touch()`'s timer
@@ -1961,22 +2138,31 @@ function runCallerSide(port, input, options) {
1961
2138
  const notice = installAbortNotice(port, controller);
1962
2139
  abortOnPortClose(port, controller);
1963
2140
  const clock = installStreamTimeout(controller, options.timeout);
1964
- const inbound = receiveChunks(port, CHANNEL_OUT, controller, clock.touch);
1965
- const pump = sendChunks(port, CHANNEL_IN, input, options, controller.signal, clock.touch);
2141
+ const inbound = receiveChunks(port, CHANNEL_OUT, controller, clock.touch, notice);
2142
+ let sent = false;
2143
+ const pump = sendChunks(port, CHANNEL_IN, input, options, controller.signal, clock.touch, () => {
2144
+ sent = true;
2145
+ });
1966
2146
  pump.catch(() => {});
1967
2147
  return returningAtOnce((async function* () {
2148
+ let received = false;
1968
2149
  try {
1969
2150
  yield* inbound.stream;
2151
+ received = true;
1970
2152
  } finally {
2153
+ const ended = received && sent;
1971
2154
  if (!controller.signal.aborted) controller.abort(/* @__PURE__ */ new Error("webrun-rpc: the caller abandoned the stream"));
1972
- notice.post();
2155
+ if (!ended) notice.post();
1973
2156
  notice.stop();
1974
2157
  clock.stop();
1975
2158
  inbound.stop();
1976
2159
  await pump.catch(() => {});
1977
- try {
1978
- await port.close?.();
1979
- } catch {}
2160
+ if (!(ended && endPort(port))) {
2161
+ notice.post();
2162
+ try {
2163
+ await port.close?.();
2164
+ } catch {}
2165
+ }
1980
2166
  }
1981
2167
  })(), () => {
1982
2168
  if (!controller.signal.aborted) controller.abort(/* @__PURE__ */ new Error("webrun-rpc: the caller abandoned the stream"));
@@ -2070,7 +2256,7 @@ function installStreamTimeout(controller, timeout) {
2070
2256
  * the local consumer has not started iterating, an inbound chunk waits for it —
2071
2257
  * which is the correct backpressure, and different from having no listener.
2072
2258
  */
2073
- function receiveChunks(port, channelName, controller, touch) {
2259
+ function receiveChunks(port, channelName, controller, touch, notice) {
2074
2260
  let deliver;
2075
2261
  const waiting = [];
2076
2262
  let finished = false;
@@ -2080,25 +2266,64 @@ function receiveChunks(port, channelName, controller, touch) {
2080
2266
  const ended = new Promise((resolve) => {
2081
2267
  resolveEnded = resolve;
2082
2268
  });
2269
+ let resolveOver = () => {};
2270
+ const over = new Promise((resolve) => {
2271
+ resolveOver = resolve;
2272
+ });
2083
2273
  let aborted = false;
2084
2274
  let abortReason;
2085
2275
  const ready = () => deliver || finished ? Promise.resolve() : new Promise((r) => waiting.push(r));
2086
2276
  const wake = () => {
2087
2277
  for (const r of waiting.splice(0)) r();
2088
2278
  };
2089
- const off = listenPort(port, async ({ done, value, error }) => {
2279
+ const violate = (message) => {
2280
+ poison = new Error(message);
2281
+ controller.abort(poison);
2282
+ setTimeout(() => {
2283
+ try {
2284
+ port.close?.();
2285
+ } catch {}
2286
+ }, 0);
2287
+ return poison;
2288
+ };
2289
+ let lastChunkArrived = false;
2290
+ let watching = false;
2291
+ const watchPeerEnd = () => {
2292
+ if (watching) return true;
2293
+ if (!portAgreement(port)?.caps.has("end")) return false;
2294
+ watching = true;
2295
+ whenPortEnded(port).then(() => {
2296
+ port.removeEventListener("message", watcher);
2297
+ if (lastChunkArrived) {
2298
+ resolveOver();
2299
+ return;
2300
+ }
2301
+ notice.post();
2302
+ violate("webrun-rpc: the peer ended its direction without finishing it; the stream port is closed");
2303
+ });
2304
+ return true;
2305
+ };
2306
+ const watcher = (event) => {
2307
+ const data = event.data;
2308
+ if (data?.channelName !== channelName) return;
2309
+ const chunk = data.type === CHUNK$1 ? data.chunk : data.type === "request" ? data.params : void 0;
2310
+ if (!chunk) return;
2311
+ if (!watchPeerEnd()) {
2312
+ port.removeEventListener("message", watcher);
2313
+ return;
2314
+ }
2315
+ if (chunk.done && !chunk.error) lastChunkArrived = true;
2316
+ };
2317
+ port.addEventListener("message", watcher);
2318
+ watchPeerEnd();
2319
+ let held = 0;
2320
+ const off = listenPort(port, async (params) => {
2321
+ const { done, error } = params;
2322
+ const value = adoptValue(params);
2090
2323
  touch();
2091
2324
  if (poison) throw poison;
2092
- if (outstanding) {
2093
- poison = /* @__PURE__ */ new Error("webrun-rpc: peer sent a second unconfirmed chunk; the stream port is closed");
2094
- controller.abort(poison);
2095
- setTimeout(() => {
2096
- try {
2097
- port.close?.();
2098
- } catch {}
2099
- }, 0);
2100
- throw poison;
2101
- }
2325
+ if (held > 0) throw violate(MIXED_MESSAGE);
2326
+ if (outstanding) throw violate("webrun-rpc: peer sent a second unconfirmed chunk; the stream port is closed");
2102
2327
  outstanding = true;
2103
2328
  try {
2104
2329
  await ready();
@@ -2108,15 +2333,60 @@ function receiveChunks(port, channelName, controller, touch) {
2108
2333
  value,
2109
2334
  error: error ? deserializeError(error) : void 0
2110
2335
  });
2336
+ if (done && !error) resolveOver();
2111
2337
  } finally {
2112
2338
  outstanding = false;
2113
2339
  }
2114
2340
  if (done) resolveEnded();
2115
2341
  }, { channelName });
2342
+ const onChunk = (event) => {
2343
+ const data = event.data;
2344
+ if (data?.type !== CHUNK$1 || data.channelName !== channelName || !data.chunk) return;
2345
+ if (poison) return;
2346
+ const agreement = portAgreement(port);
2347
+ if (!agreement?.caps.has("pipeline")) {
2348
+ violate("webrun-rpc: peer sent a pipelined chunk where pipelining was not agreed");
2349
+ return;
2350
+ }
2351
+ if (outstanding) {
2352
+ violate(MIXED_MESSAGE);
2353
+ return;
2354
+ }
2355
+ if (data.chunk.value !== void 0 && !ArrayBuffer.isView(data.chunk.value)) {
2356
+ violate("webrun-rpc: peer sent a pipelined chunk whose value is not bytes");
2357
+ return;
2358
+ }
2359
+ if (held > agreement.window) {
2360
+ violate(`webrun-rpc: peer sent past the window (${agreement.window} bytes) without waiting for credit; the stream port is closed`);
2361
+ return;
2362
+ }
2363
+ touch();
2364
+ const { done, error } = data.chunk;
2365
+ const value = data.chunk.value && ownBytes(data.chunk.value);
2366
+ const size = (value?.byteLength ?? 0) + CHUNK_OVERHEAD;
2367
+ held += size;
2368
+ return (async () => {
2369
+ try {
2370
+ await ready();
2371
+ if (finished) return;
2372
+ await deliver?.({
2373
+ done,
2374
+ value,
2375
+ error: error ? deserializeError(error) : void 0
2376
+ });
2377
+ if (done && !error) resolveOver();
2378
+ if (done) resolveEnded();
2379
+ } finally {
2380
+ held -= size;
2381
+ }
2382
+ })();
2383
+ };
2384
+ port.addEventListener("message", onChunk);
2116
2385
  const detach = () => {
2117
2386
  finished = true;
2118
2387
  wake();
2119
2388
  off();
2389
+ port.removeEventListener("message", onChunk);
2120
2390
  controller.signal.removeEventListener("abort", onAbort);
2121
2391
  resolveEnded();
2122
2392
  };
@@ -2146,25 +2416,35 @@ function receiveChunks(port, channelName, controller, touch) {
2146
2416
  return detach;
2147
2417
  }),
2148
2418
  stop: detach,
2149
- ended
2419
+ ended,
2420
+ over
2150
2421
  };
2151
2422
  }
2152
2423
  /**
2153
2424
  * The sending half of one direction: one `callPort` per chunk, one call
2154
- * outstanding at a time (spec D11/D12), with no per-chunk deadline (spec D8).
2425
+ * outstanding at a time (spec D11/D12), with no per-chunk deadline (spec D8) —
2426
+ * or, once pipelining is agreed, plain chunk messages held back by the window.
2427
+ * `onSent` runs once the end of the output has left without an error: posted
2428
+ * when pipelined, confirmed when not.
2155
2429
  */
2156
- async function sendChunks(port, channelName, output, { maxMessageSize, log }, signal, touch) {
2430
+ async function sendChunks(port, channelName, output, { maxMessageSize, log }, signal, touch, onSent) {
2157
2431
  const framed = maxMessageSize ? toChunks(maxMessageSize)(output) : output;
2158
2432
  const stream = throughAbort(framed, signal);
2159
2433
  const producer = output;
2160
2434
  const returnProducer = () => void Promise.resolve(producer.return?.()).catch(() => {});
2161
2435
  if (framed !== output) signal.addEventListener("abort", returnProducer, { once: true });
2436
+ let onAborted = () => {};
2437
+ const aborted = new Promise((resolve) => {
2438
+ onAborted = () => resolve();
2439
+ });
2440
+ if (signal.aborted) onAborted();
2441
+ else signal.addEventListener("abort", onAborted, { once: true });
2162
2442
  try {
2163
2443
  await sendIterator(async ({ done, value, error }) => {
2164
2444
  if (signal.aborted) return;
2165
2445
  const chunk = {
2166
2446
  done,
2167
- value,
2447
+ value: done ? value : chunkBytes(value),
2168
2448
  error: error === void 0 ? void 0 : serializeError(error)
2169
2449
  };
2170
2450
  log?.("[duplexOverPort] send", {
@@ -2172,48 +2452,34 @@ async function sendChunks(port, channelName, output, { maxMessageSize, log }, si
2172
2452
  done,
2173
2453
  size: value?.byteLength
2174
2454
  });
2455
+ if (mayPipeline(port)) {
2456
+ port.postMessage({
2457
+ type: CHUNK$1,
2458
+ channelName,
2459
+ chunk
2460
+ });
2461
+ touch();
2462
+ if (done && !error) onSent();
2463
+ if (portBufferedAmount(port) > 0) await Promise.race([whenPortReady(port), aborted]);
2464
+ return;
2465
+ }
2175
2466
  await callPort(port, chunk, {
2176
2467
  channelName,
2177
2468
  timeout: NO_TIMEOUT,
2178
2469
  signal
2179
2470
  });
2180
2471
  touch();
2472
+ if (done && !error) onSent();
2181
2473
  }, stream);
2182
2474
  } catch (err) {
2183
2475
  if (signal.aborted) return;
2184
2476
  throw err;
2185
2477
  } finally {
2186
2478
  signal.removeEventListener("abort", returnProducer);
2479
+ signal.removeEventListener("abort", onAborted);
2187
2480
  }
2188
2481
  }
2189
2482
  /**
2190
- * Runs `fn` on a later task, never a microtask: a producer looping on
2191
- * `whenPortReady` must let the transport's own events run. `setImmediate`
2192
- * where it exists; elsewhere a `MessageChannel`, because nested `setTimeout`s
2193
- * are clamped to 4 ms in browsers.
2194
- */
2195
- const setImmediateFn = globalThis.setImmediate;
2196
- /** @internal */
2197
- const nextTask = setImmediateFn ? (fn) => {
2198
- setImmediateFn(fn);
2199
- } : (() => {
2200
- let channel;
2201
- const queue = [];
2202
- return (fn) => {
2203
- if (!channel) {
2204
- channel = new MessageChannel();
2205
- channel.port1.onmessage = () => queue.shift()?.();
2206
- }
2207
- queue.push(fn);
2208
- channel.port2.postMessage(0);
2209
- };
2210
- })();
2211
- const states = /* @__PURE__ */ new WeakMap();
2212
- /** @internal Registered by the multiplexer for each port with a window. */
2213
- function setPortSendState(port, state) {
2214
- states.set(port, state);
2215
- }
2216
- /**
2217
2483
  * One virtual port.
2218
2484
  *
2219
2485
  * `deliver` and `markClosed` are deliberately not on `port`: the consumer holds
@@ -2291,6 +2557,21 @@ function newVirtualPort(send, requestClose) {
2291
2557
  const KEEPALIVE_ID = Number.MAX_SAFE_INTEGER;
2292
2558
  /** The longest delay a timer holds; past it `setInterval` fires at once. */
2293
2559
  const MAX_TIMER_MS = 2 ** 31 - 1;
2560
+ /**
2561
+ * What a windowed port offers in its `open`: every capability this end
2562
+ * speaks. `end` only over a codec that reads it (`PortCodec.halfClose`).
2563
+ */
2564
+ function ownCaps(codec) {
2565
+ return codec.halfClose ? ["pipeline", "end"] : ["pipeline"];
2566
+ }
2567
+ /**
2568
+ * The capabilities in a peer's `caps` that this end speaks too, or
2569
+ * `undefined` when the peer listed none (it is then treated as an older peer).
2570
+ */
2571
+ function sharedCaps(own, listed) {
2572
+ if (!Array.isArray(listed)) return void 0;
2573
+ return new Set(own.filter((cap) => listed.includes(cap)));
2574
+ }
2294
2575
  /** Ceiling on concurrently open virtual ports. Bounds the id table only. */
2295
2576
  const DEFAULT_MAX_PORTS = 1024;
2296
2577
  /**
@@ -2354,6 +2635,8 @@ function multiplexPort(port, options) {
2354
2635
  let muxClosed = false;
2355
2636
  /** This end counts encoded bytes and reassembles frames. */
2356
2637
  const exactCodec = codec.encode != null && codec.decode != null;
2638
+ /** What this end offers on a windowed port. */
2639
+ const caps = ownCaps(codec);
2357
2640
  const post = (envelope, transfer) => {
2358
2641
  if (muxClosed) return;
2359
2642
  codecPost(envelope, transfer);
@@ -2411,13 +2694,39 @@ function multiplexPort(port, options) {
2411
2694
  port.postMessage(bytes);
2412
2695
  } catch {}
2413
2696
  };
2697
+ /**
2698
+ * Both directions have ended: free the id as a close would, with no `close`
2699
+ * on the wire (each end frees it on its own once it has seen both ends).
2700
+ * Aborting the close signal is how the port's consumer learns it is gone.
2701
+ */
2702
+ const freeIfEnded = (id, c) => {
2703
+ const entry = open.get(id);
2704
+ if (!c.endSent || !c.peerEnded || entry?.credit !== c) return;
2705
+ freePartial(entry);
2706
+ open.delete(id);
2707
+ leaks?.unregister(entry.handle);
2708
+ entry.handle.markClosed(/* @__PURE__ */ new Error("webrun-rpc: the port ended in both directions"));
2709
+ };
2710
+ /** A peer that broke the port's protocol: hold none of its traffic, and close. */
2711
+ const failPort = (id, entry, error, reason) => {
2712
+ freePartial(entry);
2713
+ dropOutgoing(id);
2714
+ open.delete(id);
2715
+ releaseWaiters(entry.credit);
2716
+ entry.handle.markClosed(error);
2717
+ post({
2718
+ type: "close",
2719
+ id,
2720
+ reason
2721
+ });
2722
+ };
2414
2723
  const releaseWaiters = (c) => {
2415
2724
  for (const w of c?.waiters.splice(0) ?? []) w.resolve();
2416
2725
  };
2417
2726
  const flush = (c) => {
2418
2727
  while (c.queue.length) {
2419
2728
  const next = c.queue[0];
2420
- if (c.enforced && c.outstanding > 0 && c.outstanding + next.size > c.window) break;
2729
+ if (next.size > 0 && c.enforced && c.outstanding > 0 && c.outstanding + next.size > c.window) break;
2421
2730
  c.queue.shift();
2422
2731
  c.queued -= next.size;
2423
2732
  c.outstanding += next.size;
@@ -2455,15 +2764,20 @@ function multiplexPort(port, options) {
2455
2764
  enforced: false,
2456
2765
  opener,
2457
2766
  exact,
2767
+ caps: /* @__PURE__ */ new Set(),
2458
2768
  outstanding: 0,
2459
2769
  queue: [],
2460
2770
  queued: 0,
2461
2771
  unconsumed: 0,
2462
2772
  waiters: [],
2463
2773
  toGrant: 0,
2464
- grantScheduled: false
2774
+ grantScheduled: false,
2775
+ ending: false,
2776
+ endSent: false,
2777
+ peerEnded: false
2465
2778
  } : void 0;
2466
2779
  const self = newVirtualPort((payload, transfer) => {
2780
+ if (c?.ending) return;
2467
2781
  touch(id);
2468
2782
  const envelope = {
2469
2783
  type: "message",
@@ -2550,6 +2864,38 @@ function multiplexPort(port, options) {
2550
2864
  });
2551
2865
  })
2552
2866
  });
2867
+ setPortEndState(self.port, {
2868
+ end: () => {
2869
+ if (isClosed() || !c.caps.has("end")) return false;
2870
+ if (c.ending) return true;
2871
+ c.ending = true;
2872
+ c.queue.push({
2873
+ size: 0,
2874
+ send: () => sendOnPort(id, () => {
2875
+ if (open.get(id)?.credit !== c) return;
2876
+ post({
2877
+ type: "end",
2878
+ id
2879
+ });
2880
+ c.endSent = true;
2881
+ freeIfEnded(id, c);
2882
+ })
2883
+ });
2884
+ flush(c);
2885
+ return true;
2886
+ },
2887
+ watchPeerEnd: (resolve) => {
2888
+ if (!c.caps.has("end")) return false;
2889
+ if (c.peerEnded) resolve();
2890
+ else c.onPeerEnd = new WeakRef(resolve);
2891
+ return true;
2892
+ }
2893
+ });
2894
+ setPortAgreement(self.port, () => ({
2895
+ window: c.window,
2896
+ caps: c.caps,
2897
+ enforced: c.enforced
2898
+ }));
2553
2899
  }
2554
2900
  const record = {
2555
2901
  deliver: self.deliver,
@@ -2601,16 +2947,24 @@ function multiplexPort(port, options) {
2601
2947
  }
2602
2948
  const handle = attach(envelope.id, envelope.meta, envelope.window, false, envelope.exact === true && exactCodec);
2603
2949
  const credit = open.get(envelope.id)?.credit;
2604
- if (credit) post(credit.exact ? {
2605
- type: "credit",
2606
- id: envelope.id,
2607
- bytes: 0,
2608
- exact: true
2609
- } : {
2610
- type: "credit",
2611
- id: envelope.id,
2612
- bytes: 0
2613
- });
2950
+ const listed = sharedCaps(caps, envelope.caps);
2951
+ if (credit) {
2952
+ const ack = credit.exact ? {
2953
+ type: "credit",
2954
+ id: envelope.id,
2955
+ bytes: 0,
2956
+ exact: true
2957
+ } : {
2958
+ type: "credit",
2959
+ id: envelope.id,
2960
+ bytes: 0
2961
+ };
2962
+ if (listed) credit.caps = listed;
2963
+ post(listed ? {
2964
+ ...ack,
2965
+ caps: [...listed]
2966
+ } : ack);
2967
+ }
2614
2968
  let accepted = false;
2615
2969
  if (onPort) try {
2616
2970
  accepted = onPort(handle.port, envelope.meta) !== false;
@@ -2638,6 +2992,7 @@ function multiplexPort(port, options) {
2638
2992
  c.exact = false;
2639
2993
  c.outstanding = 0;
2640
2994
  }
2995
+ c.caps = sharedCaps(caps, envelope.caps) ?? /* @__PURE__ */ new Set();
2641
2996
  post({
2642
2997
  type: "credit",
2643
2998
  id: envelope.id,
@@ -2649,6 +3004,10 @@ function multiplexPort(port, options) {
2649
3004
  flush(c);
2650
3005
  return;
2651
3006
  }
3007
+ if ((envelope.type === "message" || envelope.type === "frame") && entry?.credit?.peerEnded) {
3008
+ failPort(envelope.id, entry, /* @__PURE__ */ new Error("webrun-rpc: the peer sent on a port after its end"), "protocol");
3009
+ return;
3010
+ }
2652
3011
  if (envelope.type === "message") {
2653
3012
  const sizeOf = (c) => (c.exact ? byteLengthOf(event.data) : void 0) ?? estimateMessageSize(envelope.payload);
2654
3013
  if (!entry) return;
@@ -2673,16 +3032,7 @@ function multiplexPort(port, options) {
2673
3032
  const charged = partial.room == null ? Math.max(0, partial.bytes + size - c.window) : 0;
2674
3033
  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;
2675
3034
  if (overrun) {
2676
- freePartial(entry);
2677
- dropOutgoing(envelope.id);
2678
- open.delete(envelope.id);
2679
- releaseWaiters(c);
2680
- entry.handle.markClosed(/* @__PURE__ */ new RangeError(`webrun-rpc: a framed message passed ${overrun}`));
2681
- post({
2682
- type: "close",
2683
- id: envelope.id,
2684
- reason: "max-reassembled-bytes"
2685
- });
3035
+ failPort(envelope.id, entry, /* @__PURE__ */ new RangeError(`webrun-rpc: a framed message passed ${overrun}`), "max-reassembled-bytes");
2686
3036
  return;
2687
3037
  }
2688
3038
  partial.bytes += size;
@@ -2702,6 +3052,19 @@ function multiplexPort(port, options) {
2702
3052
  entry.handle.deliver(inner.payload, whole.byteLength);
2703
3053
  return;
2704
3054
  }
3055
+ if (envelope.type === "end") {
3056
+ if (!entry) return;
3057
+ const c = entry.credit;
3058
+ if (!c?.caps.has("end")) {
3059
+ failPort(envelope.id, entry, /* @__PURE__ */ new Error("webrun-rpc: the peer ended a port where end was not agreed"), "protocol");
3060
+ return;
3061
+ }
3062
+ touch(envelope.id);
3063
+ c.peerEnded = true;
3064
+ c.onPeerEnd?.deref()?.();
3065
+ freeIfEnded(envelope.id, c);
3066
+ return;
3067
+ }
2705
3068
  dropOutgoing(envelope.id);
2706
3069
  if (!entry) return;
2707
3070
  freePartial(entry);
@@ -2783,12 +3146,14 @@ function multiplexPort(port, options) {
2783
3146
  id,
2784
3147
  meta,
2785
3148
  window,
2786
- exact: true
3149
+ exact: true,
3150
+ caps: [...caps]
2787
3151
  } : {
2788
3152
  type: "open",
2789
3153
  id,
2790
3154
  meta,
2791
- window
3155
+ window,
3156
+ caps: [...caps]
2792
3157
  } : {
2793
3158
  type: "open",
2794
3159
  id,
@@ -2943,7 +3308,7 @@ function isEnvelope(value) {
2943
3308
  const candidate = value;
2944
3309
  if (typeof candidate.id !== "number") return false;
2945
3310
  if (!Number.isInteger(candidate.id) || candidate.id < 0) return false;
2946
- return candidate.type === "open" || candidate.type === "message" || candidate.type === "close" || candidate.type === "credit" || candidate.type === "frame" || candidate.type === "ping" || candidate.type === "pong";
3311
+ 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";
2947
3312
  }
2948
3313
  /**
2949
3314
  * For ports whose messages are structured values — a real `MessagePort`, a
@@ -2955,6 +3320,7 @@ function isEnvelope(value) {
2955
3320
  * not send anything a byte codec could not also carry.
2956
3321
  */
2957
3322
  const structuredCodec = {
3323
+ halfClose: true,
2958
3324
  post(port, envelope, transfer) {
2959
3325
  if (transfer && transfer.length > 0) port.postMessage(envelope, transfer);
2960
3326
  else port.postMessage(envelope);
@@ -2992,10 +3358,15 @@ function asyncDisposable(target, dispose) {
2992
3358
  * own `handleStreams` / `sendStream` (no backpressure); version 2 is
2993
3359
  * webrun-rpc's `connect` / `serve` over `overPipe` with webrun-http-streams'
2994
3360
  * `fetchOverDuplex` / `serveFetchOverDuplex`: one port per link, calls
2995
- * multiplexed on it, each call a stream with a one-chunk window in both
2996
- * directions, and cancellation that reaches the handler.
3361
+ * multiplexed on it, each call a stream with backpressure in both
3362
+ * directions, and cancellation that reaches the handler. A call's port with a
3363
+ * window (`HttpLinkOptions.window`) pipelines its chunks where both ends speak
3364
+ * it, and goes one confirmed chunk at a time otherwise, so the window needs
3365
+ * no new version.
2997
3366
  */
2998
3367
  const HTTP_LINK_PROTOCOL = "webrun-http-link/2";
3368
+ /** The calling end's default `HttpLinkOptions.window`: 256 KiB. */
3369
+ const DEFAULT_HTTP_LINK_WINDOW = 262144;
2999
3370
  /**
3000
3371
  * Why a request failed when its link went away under it: the far side (the
3001
3372
  * handler, or the ServiceWorker) was lost. The request may or may not have
@@ -3014,12 +3385,13 @@ var LinkLostError = class extends Error {
3014
3385
  */
3015
3386
  const LINK_CLOSE = "webrun-http:link-close";
3016
3387
  /** Opens the calling end of a link on `port` (one port per link). */
3017
- async function connectHttpLink(port, { streamTimeout, maxPorts } = {}) {
3388
+ async function connectHttpLink(port, { streamTimeout, maxPorts, window = DEFAULT_HTTP_LINK_WINDOW } = {}) {
3018
3389
  const { call, close } = await connect({
3019
3390
  mux: overPipe(port, {
3020
3391
  codec: structuredCodec,
3021
3392
  side: "initiator",
3022
- maxPorts
3393
+ maxPorts,
3394
+ window
3023
3395
  }),
3024
3396
  timeout: streamTimeout
3025
3397
  });
@@ -3306,20 +3678,24 @@ function postFrame(port, frame) {
3306
3678
  function messageSize(data) {
3307
3679
  return typeof data === "string" ? utf8Length(data) : data.byteLength;
3308
3680
  }
3309
- /** The UTF-8 length of `text`, without encoding it. */
3681
+ const encoder = new TextEncoder();
3682
+ /** Characters encoded per `encodeInto` call: at most 3 bytes each, so the scratch stays at 96 KiB. */
3683
+ const CHUNK = 32768;
3684
+ const scratch = new Uint8Array(CHUNK * 3);
3685
+ /**
3686
+ * The UTF-8 length of `text`, as `TextEncoder` would encode it (a lone surrogate counts as the 3
3687
+ * bytes of U+FFFD). Every message is measured on both ends (`bufferedAmount` and the byte acks), so
3688
+ * this runs natively: `encodeInto` a reused buffer, a chunk at a time, never splitting a surrogate
3689
+ * pair across two chunks. A loop over the characters in JavaScript took about 30 times as long.
3690
+ */
3310
3691
  function utf8Length(text) {
3311
3692
  let length = 0;
3312
- for (let i = 0; i < text.length; i++) {
3313
- const code = text.charCodeAt(i);
3314
- if (code < 128) length += 1;
3315
- else if (code < 2048) length += 2;
3316
- else if (code >= 55296 && code <= 56319 && i + 1 < text.length) {
3317
- const next = text.charCodeAt(i + 1);
3318
- if (next >= 56320 && next <= 57343) {
3319
- length += 4;
3320
- i++;
3321
- } else length += 3;
3322
- } else length += 3;
3693
+ for (let start = 0; start < text.length;) {
3694
+ let end = Math.min(start + CHUNK, text.length);
3695
+ const last = text.charCodeAt(end - 1);
3696
+ if (end < text.length && last >= 55296 && last <= 56319) end--;
3697
+ length += encoder.encodeInto(text.slice(start, end), scratch).written;
3698
+ start = end;
3323
3699
  }
3324
3700
  return length;
3325
3701
  }
@@ -3475,6 +3851,7 @@ var PortSocketCore = class extends EventTarget {
3475
3851
  code,
3476
3852
  reason
3477
3853
  });
3854
+ if (!Number.isFinite(this._closeTimeoutMs)) return;
3478
3855
  this._closeTimer = this._timers.setTimeout(() => this._finish(1006, "", false), this._closeTimeoutMs);
3479
3856
  }
3480
3857
  _onCloseFrame(code, reason) {
@@ -3840,7 +4217,8 @@ var PortRendezvous = class {
3840
4217
  * `whenDrained(limit)` waits for it to fall: the backpressure a producer
3841
4218
  * applies.
3842
4219
  * - When the client's pings stop for three heartbeat periods (a closed tab, a
3843
- * terminated worker), the socket closes with 1006.
4220
+ * terminated worker), the socket closes with 1006. With `watch: "first-ping"`
4221
+ * the watch starts only once the client pings.
3844
4222
  */
3845
4223
  var ServerWebSocket = class extends PortSocketCore {
3846
4224
  static CONNECTING = 0;
@@ -3849,55 +4227,150 @@ var ServerWebSocket = class extends PortSocketCore {
3849
4227
  static CLOSED = 3;
3850
4228
  url;
3851
4229
  protocol;
4230
+ _binaryType = "arraybuffer";
4231
+ _options;
3852
4232
  _accepted = false;
4233
+ _detached = false;
3853
4234
  _queue = [];
3854
4235
  _heartbeatMs = DEFAULT_HEARTBEAT_MS;
3855
4236
  _lastPing = Date.now();
4237
+ /** Whether a ping (with a period a timer takes) arrived already. */
4238
+ _pinged = false;
3856
4239
  _watch;
3857
4240
  constructor(port, options = {}) {
3858
4241
  super({
3859
4242
  timers: options.timers,
3860
4243
  closeTimeoutMs: options.closeTimeoutMs
3861
4244
  });
4245
+ this._options = options;
3862
4246
  this.url = options.url ?? "";
3863
4247
  this.protocol = options.protocol ?? "";
3864
4248
  this._attach(port);
3865
4249
  this._state = 1;
3866
- this._watchHeartbeat();
4250
+ if (options.watch !== "first-ping") this._watchHeartbeat();
3867
4251
  }
3868
4252
  /** Whether `accept()` was called. */
3869
4253
  get accepted() {
3870
4254
  return this._accepted;
3871
4255
  }
4256
+ /** How binary messages arrive: `ArrayBuffer`s (the default) or `Blob`s. Other values are ignored. */
4257
+ get binaryType() {
4258
+ return this._binaryType;
4259
+ }
4260
+ set binaryType(value) {
4261
+ if (value === "blob" || value === "arraybuffer") this._binaryType = value;
4262
+ }
4263
+ /**
4264
+ * Hands this end's transport to another holder before it was accepted: the port to give the
4265
+ * client (a pair's end returned in a 101, say), with the frames that reached this end first
4266
+ * delivered ahead of the rest. This end is closed then, with no event, and answers nothing.
4267
+ */
4268
+ detach() {
4269
+ const port = this._port;
4270
+ if (!port || this._accepted || this._state !== 1) throw new DOMException("Only an open end that was not accepted can be detached.", "InvalidStateError");
4271
+ this._detached = true;
4272
+ this._port = void 0;
4273
+ port.onmessage = null;
4274
+ let owed = this.bufferedAmount;
4275
+ this._state = 3;
4276
+ this._onClosed();
4277
+ this._wakeDrainWaiters();
4278
+ const queued = this._queue.splice(0);
4279
+ if (queued.length === 0 && owed === 0) return port;
4280
+ const relay = new MessageChannel();
4281
+ const holder = relay.port2;
4282
+ const closing = /* @__PURE__ */ new Set();
4283
+ const end = () => {
4284
+ port.onmessage = null;
4285
+ holder.onmessage = null;
4286
+ port.close();
4287
+ holder.close();
4288
+ };
4289
+ const sawClose = (way) => {
4290
+ closing.add(way);
4291
+ if (closing.size === 2) end();
4292
+ };
4293
+ for (const frame of queued) {
4294
+ postFrame(holder, frame);
4295
+ if (frame.t === "c") closing.add("in");
4296
+ }
4297
+ holder.onmessage = (event) => {
4298
+ port.postMessage(event.data, transferables(event.data));
4299
+ if (event.data?.t === "c") sawClose("out");
4300
+ };
4301
+ port.onmessage = (event) => {
4302
+ const data = event.data;
4303
+ if (data?.t === "a" && typeof data.n === "number" && owed > 0) {
4304
+ const absorbed = Math.min(owed, data.n);
4305
+ owed -= absorbed;
4306
+ if (data.n > absorbed) holder.postMessage({
4307
+ t: "a",
4308
+ n: data.n - absorbed
4309
+ });
4310
+ return;
4311
+ }
4312
+ holder.postMessage(event.data, transferables(event.data));
4313
+ if (data?.t === "c") sawClose("in");
4314
+ };
4315
+ port.addEventListener?.("close", end);
4316
+ holder.addEventListener?.("close", end);
4317
+ return relay.port1;
4318
+ }
3872
4319
  /** Starts delivering events, the queued ones first. */
3873
4320
  accept() {
4321
+ if (this._detached) throw new DOMException("This end was detached; it cannot be accepted.", "InvalidStateError");
3874
4322
  if (this._accepted) return;
3875
4323
  this._accepted = true;
3876
- for (const frame of this._queue.splice(0)) super._deliver(frame);
4324
+ for (const frame of this._queue.splice(0)) this._deliver(frame);
3877
4325
  }
3878
4326
  send(data) {
3879
- if (!this._accepted) throw new DOMException("accept() the socket before sending on it.", "InvalidStateError");
4327
+ if (this._detached) throw new DOMException("This end was detached; it cannot send.", "InvalidStateError");
4328
+ if (!this._accepted && !this._options.sendBeforeAccept) throw new DOMException("accept() the socket before sending on it.", "InvalidStateError");
3880
4329
  this._sendData(isBinary(data) ? toArrayBuffer(data) : String(data));
3881
4330
  }
3882
- close(code = 1e3, reason = "") {
3883
- if (!isSendableCloseCode(code)) throw new DOMException(`${code} is not a close code an endpoint may send.`, "InvalidAccessError");
4331
+ close(code, reason = "") {
4332
+ this.checkClose(code, reason);
4333
+ this._startClose(code ?? this._options.defaultCloseCode ?? 1e3, reason);
4334
+ }
4335
+ /**
4336
+ * Refuses a `close()` the endpoint may not make: by default a code outside RFC 6455 §7.4's
4337
+ * sendable ones, or a reason over 123 bytes. A subclass with another platform's rules
4338
+ * overrides it.
4339
+ */
4340
+ checkClose(code, reason) {
4341
+ if (code !== void 0 && !isSendableCloseCode(code)) throw new DOMException(`${code} is not a close code an endpoint may send.`, "InvalidAccessError");
3884
4342
  if (utf8Length(reason) > 123) throw new DOMException("The close reason is longer than 123 bytes.", "SyntaxError");
3885
- this._startClose(code, reason);
3886
4343
  }
3887
4344
  _deliver(frame) {
3888
4345
  if (!this._accepted) {
3889
4346
  this._queue.push(frame);
3890
4347
  return;
3891
4348
  }
4349
+ if (frame.t === "m" && this._state === 2 && this._options.deliverWhileClosing) {
4350
+ this._dispatchMessage(frame.d);
4351
+ this._consumed(messageSize(frame.d));
4352
+ return;
4353
+ }
3892
4354
  super._deliver(frame);
3893
4355
  }
3894
4356
  _dispatchMessage(data) {
3895
- this.dispatchEvent(new MessageEvent("message", { data }));
4357
+ const delivered = typeof data !== "string" && this._binaryType === "blob" ? new Blob([data]) : data;
4358
+ this.dispatchEvent(new MessageEvent("message", { data: delivered }));
3896
4359
  }
3897
4360
  _onPing(hb) {
3898
4361
  this._post({ t: "q" });
3899
4362
  this._lastPing = Date.now();
3900
- if (hb !== this._heartbeatMs) {
4363
+ if (this._state === 3) return;
4364
+ const keep = this._options.heartbeatPeriod === "first" && this._pinged;
4365
+ if (!(hb > 0 && hb < 2 ** 31 - 1)) {
4366
+ if (!keep && this._watch !== void 0) {
4367
+ this._timers.clearInterval(this._watch);
4368
+ this._watch = void 0;
4369
+ }
4370
+ return;
4371
+ }
4372
+ this._pinged = true;
4373
+ if (!keep && (this._watch === void 0 || hb !== this._heartbeatMs)) {
3901
4374
  this._heartbeatMs = hb;
3902
4375
  this._watchHeartbeat();
3903
4376
  }
@@ -3919,6 +4392,11 @@ function isSendableCloseCode(code) {
3919
4392
  if (code >= 3e3 && code <= 4999) return true;
3920
4393
  return code >= 1e3 && code <= 1014 && code !== 1004 && code !== 1005 && code !== 1006;
3921
4394
  }
4395
+ /** The buffers a relayed frame carries, to transfer rather than copy. */
4396
+ function transferables(data) {
4397
+ const d = data?.d;
4398
+ return d instanceof ArrayBuffer ? [d] : [];
4399
+ }
3922
4400
  //#endregion
3923
4401
  //#region src/ws/upgrade.ts
3924
4402
  /**
@@ -3950,6 +4428,7 @@ function newWebSocketUpgradeRequest(url, { protocols = [], headers } = {}) {
3950
4428
  all.set("connection", "Upgrade");
3951
4429
  all.set("sec-websocket-version", "13");
3952
4430
  if (!all.has("sec-websocket-key")) all.set("sec-websocket-key", randomKey());
4431
+ all.delete("sec-websocket-protocol");
3953
4432
  if (protocols.length > 0) all.set("sec-websocket-protocol", protocols.join(", "));
3954
4433
  if (!all.has("x-webrun-ws-version")) all.set(WS_VERSION_HEADER, `2`);
3955
4434
  return withHeaders(new Request(httpUrl, {
@@ -4009,6 +4488,23 @@ function upgradeWebSocket(request, options = {}) {
4009
4488
  url: request.url,
4010
4489
  protocol
4011
4490
  });
4491
+ const response = webSocketUpgradeResponse(channel.port2, protocol);
4492
+ Object.defineProperty(response, "webSocket", {
4493
+ value: socket,
4494
+ configurable: true
4495
+ });
4496
+ return {
4497
+ socket,
4498
+ response
4499
+ };
4500
+ }
4501
+ /**
4502
+ * A 101 for a socket whose server end the handler runs itself, on the other end of `port`
4503
+ * (a server of its own over this package's frames, say): what `upgradeWebSocket` returns, minus
4504
+ * the `ServerWebSocket`. The host hands `port` to the caller as it would for `upgradeWebSocket`
4505
+ * (`takePort`), and `protocol` (`""` for none) is the subprotocol picked.
4506
+ */
4507
+ function webSocketUpgradeResponse(port, protocol = "") {
4012
4508
  const response = new Response(null, {
4013
4509
  status: 200,
4014
4510
  headers: protocol ? { "sec-websocket-protocol": protocol } : {}
@@ -4022,23 +4518,16 @@ function upgradeWebSocket(request, options = {}) {
4022
4518
  value: "Switching Protocols",
4023
4519
  configurable: true
4024
4520
  },
4025
- webSocket: {
4026
- value: socket,
4027
- configurable: true
4028
- },
4029
4521
  [UPGRADE]: {
4030
4522
  value: {
4031
- port: channel.port2,
4523
+ port,
4032
4524
  protocol
4033
4525
  },
4034
4526
  configurable: true,
4035
4527
  writable: true
4036
4528
  }
4037
4529
  });
4038
- return {
4039
- socket,
4040
- response
4041
- };
4530
+ return response;
4042
4531
  }
4043
4532
  /**
4044
4533
  * The port to hand to the caller, for a response from `upgradeWebSocket`.
@@ -4105,6 +4594,15 @@ function randomKey() {
4105
4594
  crypto.getRandomValues(bytes);
4106
4595
  return btoa(String.fromCharCode(...bytes));
4107
4596
  }
4597
+ /** `headers` as pairs a `postMessage` can carry (a `Headers` object cannot be cloned). */
4598
+ function headerPairs(headers) {
4599
+ if (headers === void 0) return void 0;
4600
+ const pairs = [];
4601
+ new Headers(headers).forEach((value, name) => {
4602
+ pairs.push([name, value]);
4603
+ });
4604
+ return pairs;
4605
+ }
4108
4606
  //#endregion
4109
4607
  //#region src/sw/handler-endpoint.ts
4110
4608
  /** The message calls through a lost ServiceWorker fail with. */
@@ -4189,8 +4687,11 @@ var HandlerEndpoint = class {
4189
4687
  this._startKeepAlive();
4190
4688
  return await link.up.send(request);
4191
4689
  }
4192
- /** Opens a socket to the handler `url` routes to, through the ServiceWorker. */
4193
- async connectWebSocket(url, protocols = []) {
4690
+ /**
4691
+ * Opens a socket to the handler `url` routes to, through the ServiceWorker. The handler sees
4692
+ * `headers` on the upgrade request, and so does a `route` function choosing it.
4693
+ */
4694
+ async connectWebSocket(url, protocols = [], headers) {
4194
4695
  const link = await this._linkWith("ctl");
4195
4696
  const id = this._sockets.newId();
4196
4697
  const answer = this._sockets.wait(id, this.options.wsConnectTimeoutMs ?? 15e3, `the socket request for ${url}`);
@@ -4198,7 +4699,8 @@ var HandlerEndpoint = class {
4198
4699
  t: "ws-open",
4199
4700
  id,
4200
4701
  url,
4201
- protocols
4702
+ protocols,
4703
+ headers: headerPairs(headers)
4202
4704
  });
4203
4705
  return await answer;
4204
4706
  }
@@ -4368,11 +4870,11 @@ function serveHttpInWorker(handler, options = {}) {
4368
4870
  };
4369
4871
  scope.addEventListener("message", onMessage);
4370
4872
  const baseUrl = () => globalThis.location?.href;
4371
- const connectWebSocket = (url, protocols = []) => {
4873
+ const connectWebSocket = (url, protocols = [], headers) => {
4372
4874
  const httpUrl = new URL(`${url}`, baseUrl());
4373
4875
  if (httpUrl.protocol === "ws:") httpUrl.protocol = "http:";
4374
4876
  else if (httpUrl.protocol === "wss:") httpUrl.protocol = "https:";
4375
- return endpoint.connectWebSocket(`${httpUrl}`, protocols);
4877
+ return endpoint.connectWebSocket(`${httpUrl}`, protocols, headers);
4376
4878
  };
4377
4879
  let WebSocketClass;
4378
4880
  return {
@@ -4399,4 +4901,4 @@ function serveHttpInWorker(handler, options = {}) {
4399
4901
  };
4400
4902
  }
4401
4903
  //#endregion
4402
- export { DEFAULT_CLOSE_TIMEOUT_MS, DEFAULT_HEARTBEAT_MS, HTTP_LINK_PROTOCOL, LinkLostError, ServerWebSocket, WS_PORT_PROTOCOL_VERSION, WS_VERSION_HEADER, WsCloseEvent, connectHttpLink, createWebSocketClass, isWebSocketUpgrade, newWebSocketUpgradeRequest, requestedProtocols, serveHttpInWorker, serveHttpLink, serveWebSocketUpgrade, takePort, takeUpgrade, trackedLink, upgradeWebSocket, withHeaders };
4904
+ export { DEFAULT_CLOSE_TIMEOUT_MS, DEFAULT_HEARTBEAT_MS, HTTP_LINK_PROTOCOL, LinkLostError, ServerWebSocket, WS_PORT_PROTOCOL_VERSION, WS_VERSION_HEADER, WsCloseEvent, connectHttpLink, createWebSocketClass, isWebSocketUpgrade, newWebSocketUpgradeRequest, requestedProtocols, serveHttpInWorker, serveHttpLink, serveWebSocketUpgrade, takePort, takeUpgrade, trackedLink, upgradeWebSocket, webSocketUpgradeResponse, withHeaders };