@statewalker/webrun-http-browser 0.6.5 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/README.md +302 -52
  2. package/dist/core/data-calls.d.ts +17 -0
  3. package/dist/core/data-calls.d.ts.map +1 -1
  4. package/dist/core/service-worker-control.d.ts +3 -1
  5. package/dist/core/service-worker-control.d.ts.map +1 -1
  6. package/dist/index.d.ts +3 -0
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +4218 -301
  9. package/dist/link/async-dispose.d.ts +10 -0
  10. package/dist/link/async-dispose.d.ts.map +1 -0
  11. package/dist/link/errors.d.ts +2 -0
  12. package/dist/link/errors.d.ts.map +1 -0
  13. package/dist/link/http-link.d.ts +72 -0
  14. package/dist/link/http-link.d.ts.map +1 -0
  15. package/dist/link/index.d.ts +4 -0
  16. package/dist/link/index.d.ts.map +1 -0
  17. package/dist/link/locks.d.ts +39 -0
  18. package/dist/link/locks.d.ts.map +1 -0
  19. package/dist/link/rendezvous.d.ts +21 -0
  20. package/dist/link/rendezvous.d.ts.map +1 -0
  21. package/dist/relay/index-sw.d.ts +12 -2
  22. package/dist/relay/index-sw.d.ts.map +1 -1
  23. package/dist/relay/index.d.ts +66 -8
  24. package/dist/relay/index.d.ts.map +1 -1
  25. package/dist/relay-sw.js +1328 -64
  26. package/dist/relay-worker.js +1329 -64
  27. package/dist/sw/handler-endpoint.d.ts +81 -0
  28. package/dist/sw/handler-endpoint.d.ts.map +1 -0
  29. package/dist/sw/handler-table.d.ts +53 -0
  30. package/dist/sw/handler-table.d.ts.map +1 -0
  31. package/dist/sw/http-sw-dispatcher.d.ts +162 -21
  32. package/dist/sw/http-sw-dispatcher.d.ts.map +1 -1
  33. package/dist/sw/index.d.ts +5 -0
  34. package/dist/sw/index.d.ts.map +1 -1
  35. package/dist/sw/protocol.d.ts +115 -0
  36. package/dist/sw/protocol.d.ts.map +1 -0
  37. package/dist/sw/sw-dispatcher.d.ts.map +1 -1
  38. package/dist/sw/sw-http-adapter.d.ts +199 -0
  39. package/dist/sw/sw-http-adapter.d.ts.map +1 -0
  40. package/dist/sw/sw-websocket.d.ts +16 -0
  41. package/dist/sw/sw-websocket.d.ts.map +1 -0
  42. package/dist/sw/worker-service.d.ts +45 -0
  43. package/dist/sw/worker-service.d.ts.map +1 -0
  44. package/dist/sw/worker-url.d.ts +9 -0
  45. package/dist/sw/worker-url.d.ts.map +1 -0
  46. package/dist/sw-worker.js +2403 -265
  47. package/dist/sw.d.ts +2 -0
  48. package/dist/sw.d.ts.map +1 -1
  49. package/dist/sw.js +4189 -546
  50. package/dist/worker.d.ts +10 -0
  51. package/dist/worker.d.ts.map +1 -0
  52. package/dist/worker.js +3780 -0
  53. package/dist/ws/client.d.ts +73 -0
  54. package/dist/ws/client.d.ts.map +1 -0
  55. package/dist/ws/frames.d.ts +68 -0
  56. package/dist/ws/frames.d.ts.map +1 -0
  57. package/dist/ws/index.d.ts +6 -0
  58. package/dist/ws/index.d.ts.map +1 -0
  59. package/dist/ws/server.d.ts +53 -0
  60. package/dist/ws/server.d.ts.map +1 -0
  61. package/dist/ws/socket-core.d.ts +91 -0
  62. package/dist/ws/socket-core.d.ts.map +1 -0
  63. package/dist/ws/upgrade.d.ts +93 -0
  64. package/dist/ws/upgrade.d.ts.map +1 -0
  65. package/package.json +10 -5
  66. package/src/core/data-calls.ts +52 -2
  67. package/src/core/service-worker-control.ts +7 -1
  68. package/src/index.ts +3 -0
  69. package/src/link/async-dispose.ts +22 -0
  70. package/src/link/errors.ts +3 -0
  71. package/src/link/http-link.ts +238 -0
  72. package/src/link/index.ts +3 -0
  73. package/src/link/locks.ts +97 -0
  74. package/src/link/rendezvous.ts +60 -0
  75. package/src/relay/index-sw.ts +84 -12
  76. package/src/relay/index.ts +203 -27
  77. package/src/sw/handler-endpoint.ts +278 -0
  78. package/src/sw/handler-table.ts +130 -0
  79. package/src/sw/http-sw-dispatcher.ts +649 -90
  80. package/src/sw/index.ts +5 -0
  81. package/src/sw/protocol.ts +103 -0
  82. package/src/sw/sw-dispatcher.ts +1 -20
  83. package/src/sw/sw-http-adapter.ts +613 -0
  84. package/src/sw/sw-websocket.ts +43 -0
  85. package/src/sw/worker-service.ts +133 -0
  86. package/src/sw/worker-url.ts +19 -0
  87. package/src/sw-worker.ts +8 -3
  88. package/src/sw.ts +2 -0
  89. package/src/worker.ts +9 -0
  90. package/src/ws/client.ts +346 -0
  91. package/src/ws/frames.ts +119 -0
  92. package/src/ws/index.ts +11 -0
  93. package/src/ws/server.ts +132 -0
  94. package/src/ws/socket-core.ts +251 -0
  95. package/src/ws/upgrade.ts +200 -0
package/dist/index.js CHANGED
@@ -178,8 +178,8 @@ const TYPE_CLOSE = 6;
178
178
  const DEFAULT_MAX_STREAMS = 256;
179
179
  const DEFAULT_MTU = 65536;
180
180
  const DEFAULT_MAX_STREAM_BUFFER = 8388608;
181
- const textEncoder = new TextEncoder();
182
- const textDecoder = new TextDecoder();
181
+ const textEncoder = /* @__PURE__ */ new TextEncoder();
182
+ const textDecoder = /* @__PURE__ */ new TextDecoder();
183
183
  function emulateMux(channel, opts = {}) {
184
184
  const maxStreams = opts.maxStreams ?? DEFAULT_MAX_STREAMS;
185
185
  const mtu = opts.mtu ?? DEFAULT_MTU;
@@ -716,7 +716,7 @@ async function* newAsyncGenerator(init, skipValues = false) {
716
716
  drainQueue();
717
717
  }
718
718
  }
719
- const utf8 = new TextEncoder();
719
+ const utf8 = /* @__PURE__ */ new TextEncoder();
720
720
  /**
721
721
  * Coerce common byte-like inputs into a `Uint8Array`. Strings encode as UTF-8.
722
722
  * Blobs return a `Promise<Uint8Array>`; every other input returns synchronously.
@@ -884,7 +884,56 @@ function toChunks(size = DEFAULT_CHUNK_SIZE) {
884
884
  };
885
885
  }
886
886
  //#endregion
887
+ //#region src/core/deadline.ts
888
+ /**
889
+ * Settles like `promise` if it settles before `deadline` (a `Date.now()`
890
+ * value). Otherwise calls `onTimeout`: an `Error` it returns rejects, any
891
+ * other value resolves. Internal; not re-exported.
892
+ */
893
+ function withDeadline(deadline, promise, onTimeout) {
894
+ return new Promise((resolve, reject) => {
895
+ const timer = setTimeout(() => {
896
+ const outcome = onTimeout();
897
+ if (outcome instanceof Error) reject(outcome);
898
+ else resolve(outcome);
899
+ }, Math.max(0, deadline - Date.now()));
900
+ promise.then((value) => {
901
+ clearTimeout(timer);
902
+ resolve(value);
903
+ }, (error) => {
904
+ clearTimeout(timer);
905
+ reject(error);
906
+ });
907
+ });
908
+ }
909
+ //#endregion
887
910
  //#region src/core/data-calls.ts
911
+ const TRANSFER = Symbol.for("webrun-http-browser:transfer");
912
+ /**
913
+ * A channel call's result that carries transferables, a `MessagePort` say:
914
+ * return `withTransfer({ port }, [port])` from a `handleChannelCalls` handler
915
+ * and `callChannel` resolves with `{ port }`.
916
+ */
917
+ function withTransfer(value, transfer) {
918
+ return {
919
+ [TRANSFER]: true,
920
+ value,
921
+ transfer
922
+ };
923
+ }
924
+ function isTransferResult(value) {
925
+ return typeof value === "object" && value !== null && TRANSFER in value;
926
+ }
927
+ /** Splits a handler's result into the value to post and its transfer list. */
928
+ function unwrapTransfer(result) {
929
+ return isTransferResult(result) ? {
930
+ value: result.value,
931
+ transfer: result.transfer
932
+ } : {
933
+ value: result,
934
+ transfer: []
935
+ };
936
+ }
888
937
  function callChannel(target, callType, params, ...transfers) {
889
938
  return new Promise((resolve, reject) => {
890
939
  const channel = new MessageChannel();
@@ -904,17 +953,27 @@ function handleChannelCalls(target, callType, handler) {
904
953
  if (!event.data || event.data.type !== callType) return;
905
954
  const [port, ...transfers] = event.ports ?? [];
906
955
  const response = {};
956
+ let transfer = [];
907
957
  try {
908
- response.result = await handler(event, event.data.params, ...transfers);
958
+ const result = await handler(event, event.data.params, ...transfers);
959
+ ({value: response.result, transfer} = unwrapTransfer(result));
909
960
  } catch (error) {
910
961
  response.error = serializeError(error);
911
962
  }
912
- port?.postMessage(response);
963
+ port?.postMessage(response, transfer);
913
964
  };
914
965
  target.addEventListener("message", listener);
915
966
  target.start?.();
916
967
  return () => target.removeEventListener("message", listener);
917
968
  }
969
+ /**
970
+ * `callChannel`, failing after `ms` instead of waiting for ever: a peer that
971
+ * died (a ServiceWorker stopped mid-call) or that does not know `callType` (an
972
+ * older version) never answers.
973
+ */
974
+ function callChannelWithin(ms, target, callType, params, ...transfers) {
975
+ return withDeadline(Date.now() + ms, callChannel(target, callType, params, ...transfers), () => /* @__PURE__ */ new Error(`No answer to ${callType} within ${ms} ms`));
976
+ }
918
977
  //#endregion
919
978
  //#region src/core/data-channels.ts
920
979
  const MESSAGE_TYPE_REQUEST = "REQUEST";
@@ -1125,29 +1184,6 @@ function newRegistry(onError = console.error) {
1125
1184
  });
1126
1185
  }
1127
1186
  //#endregion
1128
- //#region src/core/deadline.ts
1129
- /**
1130
- * Settles like `promise` if it settles before `deadline` (a `Date.now()`
1131
- * value). Otherwise calls `onTimeout`: an `Error` it returns rejects, any
1132
- * other value resolves. Internal; not re-exported.
1133
- */
1134
- function withDeadline(deadline, promise, onTimeout) {
1135
- return new Promise((resolve, reject) => {
1136
- const timer = setTimeout(() => {
1137
- const outcome = onTimeout();
1138
- if (outcome instanceof Error) reject(outcome);
1139
- else resolve(outcome);
1140
- }, Math.max(0, deadline - Date.now()));
1141
- promise.then((value) => {
1142
- clearTimeout(timer);
1143
- resolve(value);
1144
- }, (error) => {
1145
- clearTimeout(timer);
1146
- reject(error);
1147
- });
1148
- });
1149
- }
1150
- //#endregion
1151
1187
  //#region src/core/service-worker-control.ts
1152
1188
  /**
1153
1189
  * How long, by default, the page waits for its ServiceWorker to activate and
@@ -1447,8 +1483,8 @@ var HttpParseError = class extends Error {
1447
1483
  const NEWLINE = 10;
1448
1484
  /** Mirrors the HTTP/1.1 codec's default `maxHeaderBytes`. */
1449
1485
  const MAX_ENVELOPE_BYTES = 65536;
1450
- const encoder$1 = new TextEncoder();
1451
- const decoder = new TextDecoder();
1486
+ const encoder$1 = /* @__PURE__ */ new TextEncoder();
1487
+ const decoder = /* @__PURE__ */ new TextDecoder();
1452
1488
  /**
1453
1489
  * Encode an HTTP envelope plus optional body as one continuous byte stream:
1454
1490
  *
@@ -1702,15 +1738,15 @@ function resolveFraming(headers, version) {
1702
1738
  };
1703
1739
  return { kind: "none" };
1704
1740
  }
1705
- const CRLF = new Uint8Array([13, 10]);
1706
- const LAST_CHUNK = new Uint8Array([
1741
+ const CRLF = /* @__PURE__ */ new Uint8Array([13, 10]);
1742
+ const LAST_CHUNK = /* @__PURE__ */ new Uint8Array([
1707
1743
  48,
1708
1744
  13,
1709
1745
  10,
1710
1746
  13,
1711
1747
  10
1712
1748
  ]);
1713
- const encoder = new TextEncoder();
1749
+ const encoder = /* @__PURE__ */ new TextEncoder();
1714
1750
  /**
1715
1751
  * Chunk sizes are written in HEXADECIMAL — a 21-byte chunk is `15`. Writing
1716
1752
  * them in decimal is the defect note 15 found in @libp2p/http, and it is the
@@ -2015,7 +2051,7 @@ function newHttpCodec(options = {}) {
2015
2051
  decodeResponse: (input, o) => decodeResponse(input, opts, o.method)
2016
2052
  };
2017
2053
  }
2018
- const httpCodec = newHttpCodec();
2054
+ const httpCodec = /* @__PURE__ */ newHttpCodec();
2019
2055
  /**
2020
2056
  * Dispatches on byte 0. The formats are self-identifying — a JSON envelope
2021
2057
  * always begins `{`, which is not a token character and so can never begin an
@@ -2065,10 +2101,29 @@ function newSniffingCodec(options) {
2065
2101
  * Writes HTTP/1.1; accepts HTTP/1.1 or the legacy JSON envelope. Used whenever
2066
2102
  * no `codec` option is supplied.
2067
2103
  */
2068
- const defaultCodec = newSniffingCodec({
2104
+ const defaultCodec = /* @__PURE__ */ newSniffingCodec({
2069
2105
  write: httpCodec,
2070
2106
  accept: [httpCodec, jsonEnvelopeCodec]
2071
2107
  });
2108
+ /**
2109
+ * Makes `target` disposable with `await using`: installs `dispose` as its
2110
+ * `[Symbol.asyncDispose]`, where the runtime defines the symbol. Hand-written
2111
+ * iterators lose what a native generator gets from `AsyncIteratorPrototype`;
2112
+ * this gives it back. A no-op where the symbol is missing. Kept off the
2113
+ * declared types, which would require consumers to compile with the
2114
+ * `esnext.disposable` lib.
2115
+ */
2116
+ function asyncDisposable$2(target, dispose) {
2117
+ const asyncDispose = Symbol.asyncDispose;
2118
+ if (asyncDispose && !(asyncDispose in target)) Object.defineProperty(target, asyncDispose, {
2119
+ value: async () => {
2120
+ await dispose();
2121
+ },
2122
+ configurable: true,
2123
+ writable: true
2124
+ });
2125
+ return target;
2126
+ }
2072
2127
  /** Carries the serialized JS error between two webrun peers. */
2073
2128
  const PEER_ERROR_HEADER = "x-webrun-error";
2074
2129
  /** Hard cap on the peer-error header (M3): see `encodeErrorResponse`. */
@@ -2146,11 +2201,105 @@ async function throwIfPeerError(result, output) {
2146
2201
  */
2147
2202
  async function httpFetch(call, env, body, options = {}) {
2148
2203
  const codec = options.codec ?? defaultCodec;
2204
+ const { signal } = options;
2205
+ if (signal?.aborted) throw abortReason$1(signal);
2149
2206
  const output = call(codec.encodeRequest(env, body));
2150
- const result = await codec.decodeResponse(output, { method: env.method });
2207
+ let released;
2208
+ const release = () => {
2209
+ released ??= (async () => {
2210
+ try {
2211
+ await output.return?.(void 0);
2212
+ } catch {}
2213
+ })();
2214
+ return released;
2215
+ };
2216
+ let finished = false;
2217
+ const finish = () => {
2218
+ if (finished || released) return;
2219
+ finished = true;
2220
+ if (signal?.aborted) {
2221
+ release();
2222
+ return;
2223
+ }
2224
+ const onAbort = () => void release();
2225
+ signal?.addEventListener("abort", onAbort, { once: true });
2226
+ drain(output).finally(() => signal?.removeEventListener("abort", onAbort));
2227
+ };
2228
+ let decoded;
2229
+ try {
2230
+ decoded = await untilAborted(codec.decodeResponse(output, { method: env.method }), signal);
2231
+ } catch (error) {
2232
+ if (signal?.aborted) release();
2233
+ else await release();
2234
+ throw error;
2235
+ }
2236
+ const result = {
2237
+ envelope: decoded.envelope,
2238
+ body: releasingOnExit(decoded.body, finish, release),
2239
+ release,
2240
+ finish
2241
+ };
2151
2242
  await throwIfPeerError(result, output);
2152
2243
  return result;
2153
2244
  }
2245
+ /** `promise`, rejected with the signal's reason if it aborts first. */
2246
+ function untilAborted(promise, signal) {
2247
+ if (!signal) return promise;
2248
+ return new Promise((resolve, reject) => {
2249
+ const onAbort = () => reject(abortReason$1(signal));
2250
+ if (signal.aborted) return onAbort();
2251
+ signal.addEventListener("abort", onAbort, { once: true });
2252
+ promise.then((value) => {
2253
+ signal.removeEventListener("abort", onAbort);
2254
+ resolve(value);
2255
+ }, (error) => {
2256
+ signal.removeEventListener("abort", onAbort);
2257
+ reject(error);
2258
+ });
2259
+ });
2260
+ }
2261
+ /** The signal's reason as an `Error` (`AbortError` when it gave none). */
2262
+ function abortReason$1(signal) {
2263
+ const reason = signal.reason;
2264
+ if (reason instanceof Error) return reason;
2265
+ const err = new Error(reason === void 0 ? "Aborted" : String(reason));
2266
+ err.name = "AbortError";
2267
+ return err;
2268
+ }
2269
+ /**
2270
+ * `body`, ending the call however it is left: read to its end, or left early.
2271
+ *
2272
+ * The body the codec hands back is a generator of its own over a `ByteReader`,
2273
+ * and its ending never reaches the call's `output` (see `throwIfPeerError` for
2274
+ * why the codec must not do that itself). A caller that stopped reading — an
2275
+ * aborted `fetch`, a cancelled response stream — left the peer's producer
2276
+ * parked forever; and one that read the body to its end left the call itself
2277
+ * open, which on a multiplexed transport is one port per exchange until the
2278
+ * multiplexer refuses to open more (`maxPorts`).
2279
+ *
2280
+ * Left early, the call is abandoned (`.return()`): the peer's producer is
2281
+ * released. Read to its end, the call is not cut short but read to ITS end, in
2282
+ * the background: the peer ends it once its side of the exchange is over,
2283
+ * which may be later than the response — a handler that answered before it
2284
+ * read the request body is still receiving it (`HttpDataHandlerResult.settled`),
2285
+ * and abandoning the call would abort that upload.
2286
+ */
2287
+ async function* releasingOnExit(body, finish, release) {
2288
+ let completed = false;
2289
+ try {
2290
+ yield* body;
2291
+ completed = true;
2292
+ } finally {
2293
+ if (completed) finish();
2294
+ else await release();
2295
+ }
2296
+ }
2297
+ /** Reads `output` to its end, discarding what is left, ignoring a failure. */
2298
+ async function drain(output) {
2299
+ try {
2300
+ for (let r = await output.next(); !r.done; r = await output.next());
2301
+ } catch {}
2302
+ }
2154
2303
  /**
2155
2304
  * Wrap an HTTP handler as a `Duplex` so it can be registered with any
2156
2305
  * `webrun-streams-*` adapter's `serve`.
@@ -2174,7 +2323,16 @@ function httpServe(handler, options = {}) {
2174
2323
  yield* encodeErrorResponse(replyCodec, error, decoded.envelope.method);
2175
2324
  return;
2176
2325
  }
2177
- yield* replyCodec.encodeResponse(result.envelope, result.body, { method: decoded.envelope.method });
2326
+ let completed = false;
2327
+ try {
2328
+ yield* replyCodec.encodeResponse(result.envelope, result.body, { method: decoded.envelope.method });
2329
+ completed = true;
2330
+ await result.settled?.();
2331
+ } finally {
2332
+ if (!completed) try {
2333
+ await result.release?.();
2334
+ } catch {}
2335
+ }
2178
2336
  };
2179
2337
  }
2180
2338
  /**
@@ -2414,7 +2572,7 @@ function asyncIterableToReadable(iter) {
2414
2572
  * abort terminates the underlying call.
2415
2573
  */
2416
2574
  async function fetchOverDuplex(call, request, options = {}) {
2417
- if (request.signal?.aborted) throw abortReason(request.signal);
2575
+ if (request.signal?.aborted) throw abortReason$1(request.signal);
2418
2576
  const env = {
2419
2577
  url: request.url,
2420
2578
  method: request.method,
@@ -2426,14 +2584,18 @@ async function fetchOverDuplex(call, request, options = {}) {
2426
2584
  const buffered = new Uint8Array(await request.arrayBuffer());
2427
2585
  if (buffered.byteLength > 0) body = [buffered];
2428
2586
  }
2429
- const { envelope, body: respBody } = await httpFetch(call, env, body, options);
2587
+ const { envelope, body: respBody, release, finish } = await httpFetch(call, env, body, {
2588
+ ...options,
2589
+ signal: request.signal ?? void 0
2590
+ });
2430
2591
  const responseInit = {
2431
2592
  status: envelope.status,
2432
2593
  statusText: envelope.statusText,
2433
2594
  headers: forwardableHeaders(envelope.headers)
2434
2595
  };
2435
2596
  if (request.method === "HEAD" || request.method === "OPTIONS" || NULL_BODY_STATUSES.has(envelope.status)) {
2436
- await respBody.return?.();
2597
+ if (finish) finish();
2598
+ else await release?.();
2437
2599
  return new Response(null, responseInit);
2438
2600
  }
2439
2601
  return new Response(asyncIterableToReadable(withAbort(respBody, request.signal)), responseInit);
@@ -2448,13 +2610,21 @@ function serveFetchOverDuplex(handler, options = {}) {
2448
2610
  method: env.method,
2449
2611
  headers: forwardableHeaders(env.headers)
2450
2612
  };
2613
+ let upload;
2451
2614
  if (env.method !== "GET" && env.method !== "HEAD") {
2452
2615
  if (supportsRequestStreams()) {
2453
- reqInit.body = asyncIterableToReadable(body);
2616
+ upload = trackingEnd(body);
2617
+ reqInit.body = asyncIterableToReadable(upload.source);
2454
2618
  reqInit.duplex = "half";
2455
2619
  } else reqInit.body = await collectBytes$1(body);
2456
2620
  }
2457
- const response = await handler(new Request(env.url, reqInit));
2621
+ const request = new Request(env.url, reqInit);
2622
+ const response = await handler(request);
2623
+ const settled = async () => {
2624
+ if (!upload) return;
2625
+ if (!request.bodyUsed && !request.body?.locked) request.body?.cancel().catch(() => {});
2626
+ await upload.settle(options.requestIdleMs ?? 3e4);
2627
+ };
2458
2628
  const respEnv = {
2459
2629
  status: response.status,
2460
2630
  statusText: response.statusText,
@@ -2464,32 +2634,111 @@ function serveFetchOverDuplex(handler, options = {}) {
2464
2634
  await response.body?.cancel();
2465
2635
  return {
2466
2636
  envelope: respEnv,
2467
- body: void 0
2637
+ body: void 0,
2638
+ settled
2468
2639
  };
2469
2640
  }
2470
2641
  return {
2471
2642
  envelope: respEnv,
2472
- body: response.body ? readableToAsyncIterable(response.body) : void 0
2643
+ body: response.body ? readableToAsyncIterable(response.body) : void 0,
2644
+ release: () => response.body?.cancel(),
2645
+ settled
2473
2646
  };
2474
2647
  }, options);
2475
2648
  }
2649
+ /**
2650
+ * `source`, and `settle(idleMs)`: resolves once it is done — read to its end,
2651
+ * failed, or abandoned (`return()` included before the first `next()`, which a
2652
+ * generator's `finally` would not see) — or once nobody has asked it for a
2653
+ * chunk for `idleMs`, in which case it is abandoned: later reads fail.
2654
+ *
2655
+ * Idle counts only while no `next()` is pending: a reader waiting for a slow
2656
+ * sender is still reading.
2657
+ */
2658
+ function trackingEnd(source) {
2659
+ let end = () => {};
2660
+ let isEnded = false;
2661
+ const ended = new Promise((resolve) => {
2662
+ end = () => {
2663
+ isEnded = true;
2664
+ resolve();
2665
+ };
2666
+ });
2667
+ let abandoned;
2668
+ let pending = 0;
2669
+ let idleSince = Date.now();
2670
+ const it = source[Symbol.asyncIterator]();
2671
+ const abandon = (reason) => {
2672
+ abandoned ??= reason;
2673
+ end();
2674
+ Promise.resolve(it.return?.(void 0)).catch(() => {});
2675
+ };
2676
+ const tracked = {
2677
+ [Symbol.asyncIterator]() {
2678
+ return tracked;
2679
+ },
2680
+ async next() {
2681
+ if (abandoned) throw abandoned;
2682
+ pending++;
2683
+ try {
2684
+ const r = await it.next();
2685
+ if (r.done) end();
2686
+ return r;
2687
+ } catch (error) {
2688
+ end();
2689
+ throw error;
2690
+ } finally {
2691
+ if (--pending === 0) idleSince = Date.now();
2692
+ }
2693
+ },
2694
+ async return(value) {
2695
+ end();
2696
+ try {
2697
+ await it.return?.(value);
2698
+ } catch {}
2699
+ return {
2700
+ done: true,
2701
+ value: void 0
2702
+ };
2703
+ }
2704
+ };
2705
+ asyncDisposable$2(tracked, () => tracked.return?.() ?? Promise.resolve());
2706
+ const settle = (idleMs) => {
2707
+ if (isEnded || !Number.isFinite(idleMs)) return ended;
2708
+ return new Promise((resolve) => {
2709
+ let timer;
2710
+ const check = () => {
2711
+ if (isEnded) return resolve();
2712
+ const idle = pending === 0 ? Date.now() - idleSince : 0;
2713
+ if (idle >= idleMs) {
2714
+ abandon(/* @__PURE__ */ new Error(`webrun-http-streams: the request body was left unread for ${idleMs} ms after the response; abandoned`));
2715
+ return resolve();
2716
+ }
2717
+ timer = setTimeout(check, Math.max(1, Math.min(idleMs - idle, 1e3)));
2718
+ timer.unref?.();
2719
+ };
2720
+ ended.then(() => {
2721
+ clearTimeout(timer);
2722
+ resolve();
2723
+ });
2724
+ check();
2725
+ });
2726
+ };
2727
+ return {
2728
+ source: tracked,
2729
+ settle
2730
+ };
2731
+ }
2476
2732
  async function* withAbort(iter, signal) {
2477
2733
  if (!signal) {
2478
2734
  yield* iter;
2479
2735
  return;
2480
2736
  }
2481
2737
  for await (const chunk of iter) {
2482
- if (signal.aborted) throw abortReason(signal);
2738
+ if (signal.aborted) throw abortReason$1(signal);
2483
2739
  yield chunk;
2484
2740
  }
2485
2741
  }
2486
- function abortReason(signal) {
2487
- const reason = signal.reason;
2488
- if (reason instanceof Error) return reason;
2489
- const err = new Error(reason === void 0 ? "Aborted" : String(reason));
2490
- err.name = "AbortError";
2491
- return err;
2492
- }
2493
2742
  /**
2494
2743
  * Host a `SiteHandler` over a `Connect/Serve` pair. The `start(serve, params)`
2495
2744
  * call hands the handler to any `webrun-streams-*` adapter's `serve`.
@@ -2653,292 +2902,3960 @@ async function sendHttpRequest(communicationPort, request) {
2653
2902
  })(request);
2654
2903
  }
2655
2904
  //#endregion
2656
- //#region src/relay/split-service-url.ts
2905
+ //#region ../webrun-rpc/dist/index.js
2906
+ const closeSignals = /* @__PURE__ */ new WeakMap();
2657
2907
  /**
2658
- * Splits a URL of the form `<base>/<separator><key>/<path>` into parts.
2659
- * Example: `https://host/~FS/a/b` → `{ baseUrl: "https://host/~FS/", key: "FS", path: "a/b" }`.
2908
+ * Internal: register a port's close signal. Called once by `bindBytesToPort`.
2909
+ *
2910
+ * @internal
2660
2911
  */
2661
- function splitServiceUrl(url, separator = "~") {
2662
- const str = `${url}`;
2663
- const empty = {
2664
- url: str,
2665
- key: "",
2666
- baseUrl: "",
2667
- path: ""
2668
- };
2669
- const hashIdx = str.indexOf("#");
2670
- const queryIdx = str.indexOf("?");
2671
- let strippedEnd = str.length;
2672
- if (hashIdx >= 0) strippedEnd = Math.min(strippedEnd, hashIdx);
2673
- if (queryIdx >= 0) strippedEnd = Math.min(strippedEnd, queryIdx);
2674
- const stripped = str.slice(0, strippedEnd);
2675
- let prefixEnd = 0;
2676
- const schemeMatch = stripped.match(/^[a-zA-Z][a-zA-Z0-9+\-.]*:\/\//);
2677
- if (schemeMatch) {
2678
- prefixEnd = schemeMatch[0].length;
2679
- const slashIdx = stripped.indexOf("/", prefixEnd);
2680
- if (slashIdx >= 0) prefixEnd = slashIdx;
2681
- else return empty;
2682
- } else if (stripped.startsWith("//")) {
2683
- prefixEnd = 2;
2684
- const slashIdx = stripped.indexOf("/", prefixEnd);
2685
- if (slashIdx >= 0) prefixEnd = slashIdx;
2686
- else return empty;
2687
- }
2688
- const prefix = stripped.slice(0, prefixEnd);
2689
- const pathPart = stripped.slice(prefixEnd);
2690
- let keyStart;
2691
- let rooted = false;
2692
- if (prefix === "") {
2693
- if (pathPart.startsWith(`/${separator}`)) {
2694
- rooted = true;
2695
- keyStart = separator.length + 1;
2696
- } else if (pathPart.startsWith(separator)) keyStart = separator.length;
2697
- else return empty;
2698
- } else {
2699
- if (!pathPart.startsWith(`/${separator}`)) return empty;
2700
- keyStart = separator.length + 1;
2701
- }
2702
- const rest = pathPart.slice(keyStart);
2703
- const slash = rest.indexOf("/");
2704
- const key = slash < 0 ? rest : rest.slice(0, slash);
2705
- if (key === "") return empty;
2706
- return {
2707
- url: str,
2708
- key,
2709
- baseUrl: `${prefix === "" ? rooted ? "/" : "" : `${prefix}/`}${separator}${key}${slash < 0 ? "" : "/"}`,
2710
- path: slash < 0 ? "" : rest.slice(slash + 1)
2711
- };
2912
+ function setPortCloseSignal(port, signal) {
2913
+ closeSignals.set(port, signal);
2712
2914
  }
2713
- //#endregion
2714
- //#region src/relay/index.ts
2715
2915
  /**
2716
- * The URL of this module, kept in a variable on purpose. Bundlers (Vite
2717
- * among them) rewrite every literal `new URL("<path>", import.meta.url)` into
2718
- * an emitted asset at build time, before tree-shaking — so the defaults below
2719
- * made every Vite consumer of this entry emit a dead copy of the package's
2720
- * own `dist/index.js` (`"../"` resolves to the package, hence to its `main`).
2721
- * Resolving against a variable is the same URL at run time and invisible to
2722
- * that transform.
2723
- */
2724
- const moduleUrl = import.meta.url;
2725
- /**
2726
- * Returns a MessagePort that transparently bridges messages to/from the
2727
- * page's ServiceWorker: the one controlling the page, or — when the page is
2728
- * not controlled (a hard reload, or a page Firefox left uncontrolled) — the
2729
- * active worker of `registration`, which answers messages all the same.
2916
+ * Return the AbortSignal that fires when `port`'s transport closes, or
2917
+ * `undefined` if `port` is not transport-backed (e.g., a raw
2918
+ * `MessageChannel().port1`).
2730
2919
  */
2731
- function newServiceWorkerPort(registration) {
2732
- const channel = new MessageChannel();
2733
- channel.port1.onmessage = (event) => {
2734
- (navigator.serviceWorker.controller ?? registration?.active)?.postMessage(event.data, [...event.ports]);
2735
- };
2736
- navigator.serviceWorker.addEventListener("message", (event) => {
2737
- channel.port1.postMessage(event.data, [...event.ports]);
2738
- });
2739
- return channel.port2;
2920
+ function getPortCloseSignal(port) {
2921
+ return closeSignals.get(port);
2740
2922
  }
2741
2923
  /**
2742
- * Registers a ServiceWorker and resolves with it once it is activated: the
2743
- * worker controlling the page, or the registration's active worker when the
2744
- * page is not controlled. Messaging works either way, which is all the relay
2745
- * needs; nothing here waits for control, because an uncontrolled page (hard
2746
- * reload; Firefox) may never get it. Rejects with a
2747
- * `ServiceWorkerControlError` if activation takes longer than `timeout`.
2924
+ * Pass as `timeout` to install no deadline at all. Used by the stream tier,
2925
+ * where the deadline belongs to the stream rather than to one chunk (spec D8):
2926
+ * a slow consumer is throttled, never failed.
2748
2927
  */
2749
- async function initServiceWorker(options) {
2750
- return (await registerServiceWorker(options)).worker;
2751
- }
2752
- async function registerServiceWorker({ swUrl, scopeUrl, type, timeout = DEFAULT_SERVICE_WORKER_TIMEOUT }) {
2753
- const registration = await navigator.serviceWorker.register(swUrl, {
2754
- type,
2755
- scope: scopeUrl
2756
- });
2757
- const active = await awaitActiveServiceWorker(registration, { timeout });
2758
- return {
2759
- registration,
2760
- worker: navigator.serviceWorker.controller ?? active
2761
- };
2762
- }
2928
+ const NO_TIMEOUT = Number.POSITIVE_INFINITY;
2763
2929
  /**
2764
- * Registers `handler` as the server for the given service `key` on the relay.
2765
- * Returns a cleanup function that unregisters the service.
2930
+ * Asynchronous request/response over any `MessageTarget`.
2931
+ *
2932
+ * Sends `params` to the peer listening with `listenPort`, waits up to
2933
+ * `timeout` ms for a matching reply, and either resolves with the result or
2934
+ * rejects with the deserialised error.
2766
2935
  */
2767
- async function initHttpService(handler, { key, path, port }) {
2768
- return await registerConnectionsHandler({
2769
- key,
2770
- path,
2771
- communicationPort: port,
2772
- handler: async (_event, _data, callPort) => {
2773
- handleHttpRequests(callPort, handler);
2774
- return true;
2936
+ function callPort(port, params, { timeout = 1e3, channelName = "", log = () => {}, newCallId = () => `call-${Date.now()}-${String(Math.random()).substring(2)}`, signal } = {}) {
2937
+ const callId = newCallId();
2938
+ log("[callPort]", {
2939
+ channelName,
2940
+ callId,
2941
+ params
2942
+ });
2943
+ const combinedSignal = combineSignals(signal, getPortCloseSignal(port));
2944
+ let timerId;
2945
+ let onMessage;
2946
+ let onAbort;
2947
+ const promise = new Promise((resolve, reject) => {
2948
+ if (combinedSignal?.aborted) {
2949
+ reject(abortReason(combinedSignal));
2950
+ return;
2951
+ }
2952
+ if (Number.isFinite(timeout) && timeout > 0) timerId = setTimeout(() => reject(/* @__PURE__ */ new Error(`Call timeout. CallId: "${callId}".`)), timeout);
2953
+ onMessage = (event) => {
2954
+ const data = event.data;
2955
+ if (!data) return;
2956
+ if (data.channelName !== channelName) return;
2957
+ if (data.callId !== callId) return;
2958
+ if (data.type === "response:error") reject(deserializeError(data.error));
2959
+ else if (data.type === "response:result") resolve(data.result);
2960
+ };
2961
+ port.addEventListener("message", onMessage);
2962
+ if (combinedSignal) {
2963
+ onAbort = () => reject(abortReason(combinedSignal));
2964
+ combinedSignal.addEventListener("abort", onAbort, { once: true });
2775
2965
  }
2776
2966
  });
2777
- }
2778
- /**
2779
- * Sends a `Request` to the service registered under `key` on the relay and
2780
- * resolves with the corresponding `Response`.
2781
- */
2782
- async function callHttpService(request, { key, port }) {
2783
- const callPort = await initializeConnection({
2784
- key,
2785
- communicationPort: port
2967
+ promise.catch(() => {}).finally(() => {
2968
+ if (timerId !== void 0) clearTimeout(timerId);
2969
+ if (onMessage) port.removeEventListener("message", onMessage);
2970
+ if (onAbort && combinedSignal) combinedSignal.removeEventListener("abort", onAbort);
2971
+ });
2972
+ port.postMessage({
2973
+ type: "request",
2974
+ channelName,
2975
+ callId,
2976
+ params
2786
2977
  });
2787
- if (!callPort) throw new Error(`No service with key "${key}"`);
2788
- return await sendHttpRequest(callPort, request);
2978
+ return promise;
2789
2979
  }
2790
- /**
2791
- * Returns a `window.onmessage` handler for use inside the relay iframe:
2792
- * it accepts a CONNECT message, starts the relay ServiceWorker, and bridges
2793
- * the parent's MessagePort with the SW. If the worker cannot be started, every
2794
- * call the parent makes on that port is answered with the error, so the
2795
- * parent's `initHttpService` / `callHttpService` reject instead of waiting.
2980
+ function abortReason(signal) {
2981
+ const reason = signal.reason;
2982
+ if (reason instanceof Error) return reason;
2983
+ const err = new Error(reason === void 0 ? "Aborted" : String(reason));
2984
+ err.name = "AbortError";
2985
+ return err;
2986
+ }
2987
+ function combineSignals(...signals) {
2988
+ const live = signals.filter((s) => s !== void 0);
2989
+ if (live.length === 0) return void 0;
2990
+ if (live.length === 1) return live[0];
2991
+ return AbortSignal.any(live);
2992
+ }
2993
+ /**
2994
+ * Installs `handler` as the server side of a `callPort` / `listenPort`
2995
+ * request/response pair on `port`.
2996
+ *
2997
+ * Returns a cleanup function that removes the listener.
2796
2998
  */
2797
- function getRelayWindowMessageHandler({ swUrl = `${new URL("./relay-sw.js", moduleUrl)}`, scopeUrl = `${new URL("./", moduleUrl)}`, timeout } = {}) {
2798
- let externalPort;
2799
- return async (ev) => {
2800
- if (ev.data?.type !== "CONNECT") return;
2801
- const newExternalPort = ev.ports?.[0];
2802
- if (!newExternalPort) return;
2803
- if (externalPort) {
2804
- newExternalPort.close();
2805
- return;
2806
- }
2807
- externalPort = newExternalPort;
2808
- let registration;
2999
+ function listenPort(port, handler, { channelName = "", log = () => {} } = {}) {
3000
+ const onMessage = async (event) => {
3001
+ const data = event.data;
3002
+ if (!data || data.channelName !== channelName || data.type !== "request") return;
3003
+ const { callId, params } = data;
3004
+ log("[listenPort]", {
3005
+ channelName,
3006
+ callId,
3007
+ params
3008
+ });
3009
+ let result;
3010
+ let error;
3011
+ let type;
2809
3012
  try {
2810
- ({registration} = await registerServiceWorker({
2811
- swUrl,
2812
- scopeUrl,
2813
- timeout
2814
- }));
2815
- } catch (error) {
2816
- const serialized = serializeError(error);
2817
- externalPort.onmessage = (event) => event.ports[0]?.postMessage({ error: serialized });
3013
+ result = await handler(params);
3014
+ type = "response:result";
3015
+ } catch (e) {
3016
+ error = serializeError(e);
3017
+ type = "response:error";
3018
+ }
3019
+ port.postMessage({
3020
+ callId,
3021
+ channelName,
3022
+ type,
3023
+ result,
3024
+ error
3025
+ });
3026
+ };
3027
+ port.addEventListener("message", onMessage);
3028
+ return () => port.removeEventListener("message", onMessage);
3029
+ }
3030
+ /**
3031
+ * Wraps an async iterable so that an `AbortSignal` firing causes the wrapper
3032
+ * to return cleanly, forwarding `return()` to the underlying iterator so the
3033
+ * producer (e.g., a user-supplied generator) sees its own `finally` blocks
3034
+ * run immediately rather than waiting for the next yield.
3035
+ */
3036
+ async function* throughAbort(input, signal) {
3037
+ const iter = input[Symbol.asyncIterator] ? input[Symbol.asyncIterator]() : input[Symbol.iterator]();
3038
+ const onAbort = () => {
3039
+ iter.return?.(void 0);
3040
+ };
3041
+ if (signal.aborted) {
3042
+ iter.return?.(void 0);
3043
+ return;
3044
+ }
3045
+ signal.addEventListener("abort", onAbort, { once: true });
3046
+ try {
3047
+ while (true) {
3048
+ const r = await iter.next();
3049
+ if (r.done) return;
3050
+ if (signal.aborted) return;
3051
+ yield r.value;
3052
+ }
3053
+ } finally {
3054
+ signal.removeEventListener("abort", onAbort);
3055
+ }
3056
+ }
3057
+ /**
3058
+ * Makes `target` disposable with `await using`: installs `dispose` as its
3059
+ * `[Symbol.asyncDispose]`, where the runtime defines the symbol. Hand-written
3060
+ * iterators lose what a native generator gets from `AsyncIteratorPrototype`;
3061
+ * this gives it back. A no-op where the symbol is missing. Kept off the
3062
+ * declared types, which would require consumers to compile with the
3063
+ * `esnext.disposable` lib.
3064
+ */
3065
+ function asyncDisposable$1(target, dispose) {
3066
+ const asyncDispose = Symbol.asyncDispose;
3067
+ if (asyncDispose && !(asyncDispose in target)) Object.defineProperty(target, asyncDispose, {
3068
+ value: async () => {
3069
+ await dispose();
3070
+ },
3071
+ configurable: true,
3072
+ writable: true
3073
+ });
3074
+ return target;
3075
+ }
3076
+ /**
3077
+ * `gen`, except that `return()` and `throw()` take effect at once.
3078
+ *
3079
+ * On an async generator both are queued behind a `next()` that is still
3080
+ * pending: a consumer that gives up on a stream while it waits for the peer's
3081
+ * next chunk — an aborted fetch whose response head has not come, say — cannot
3082
+ * end it until that chunk arrives, which for a peer that never answers is
3083
+ * never. The stream's `finally` (which closes its port and tells the peer)
3084
+ * then never runs, and on a multiplexer the port is kept for good.
3085
+ *
3086
+ * So `abandon` runs first, synchronously: it must make the pending `next()`
3087
+ * settle (abort the stream), which then resolves done, after which the
3088
+ * generator's own `return()` / `throw()` proceeds as usual. On a generator
3089
+ * suspended at a `yield` it is the same ending, a step earlier. `abandon` runs once, whichever comes first.
3090
+ */
3091
+ function returningAtOnce(gen, abandon) {
3092
+ let abandoned = false;
3093
+ const giveUp = (reason) => {
3094
+ if (abandoned) return;
3095
+ abandoned = true;
3096
+ abandon(reason);
3097
+ };
3098
+ const wrapped = {
3099
+ next: (...args) => gen.next(...args).catch((error) => {
3100
+ if (abandoned) return {
3101
+ done: true,
3102
+ value: void 0
3103
+ };
2818
3104
  throw error;
3105
+ }),
3106
+ return(value) {
3107
+ giveUp();
3108
+ return gen.return(value);
3109
+ },
3110
+ throw(error) {
3111
+ giveUp(error);
3112
+ return gen.throw(error);
3113
+ },
3114
+ [Symbol.asyncIterator]() {
3115
+ return wrapped;
2819
3116
  }
2820
- const serviceWorkerPort = newServiceWorkerPort(registration);
2821
- serviceWorkerPort.onmessage = (event) => {
2822
- externalPort?.postMessage(event.data, [...event.ports]);
2823
- };
2824
- externalPort.onmessage = (event) => {
2825
- serviceWorkerPort.postMessage(event.data, [...event.ports]);
2826
- };
2827
3117
  };
3118
+ return asyncDisposable$1(wrapped, () => wrapped.return(void 0));
2828
3119
  }
2829
3120
  /**
2830
- * Embeds a hidden relay iframe, establishes a MessageChannel with it, and
2831
- * returns the port to be used with `initHttpService` / `callHttpService`.
3121
+ * The `type` of the out-of-band notice a side posts when it abandons a stream.
3122
+ *
3123
+ * Layer 1's `close` is not observable to layer 2 — a closed virtual port drops
3124
+ * its listeners silently and is indistinguishable from a working port nobody
3125
+ * is answering — so the peer would otherwise wait forever. Exported because
3126
+ * tests and adapters assert on it.
2832
3127
  */
2833
- async function newRemoteRelayChannel({ baseUrl = new URL("../public-relay/", moduleUrl), url = new URL("relay.html", baseUrl), container = document.body } = {}) {
2834
- const messageChannel = new MessageChannel();
2835
- const { iframe, promise } = newIFrame(url);
2836
- Object.assign(iframe.style, {
2837
- position: "fixed",
2838
- width: "1px",
2839
- height: "1px",
2840
- top: "-1000px",
2841
- left: "-1000px",
2842
- display: "block",
2843
- opacity: "0",
2844
- border: "none",
2845
- outline: "none"
3128
+ const STREAM_ABORT = "webrun-rpc:stream-abort";
3129
+ /** Caller's input travels on this channel; the handler listens on it. */
3130
+ const CHANNEL_IN = "in";
3131
+ /** The handler's output travels on this channel; the caller listens on it. */
3132
+ const CHANNEL_OUT = "out";
3133
+ /**
3134
+ * One port in, one `Duplex` out (spec D9).
3135
+ *
3136
+ * The returned `Duplex` runs a single stream on `port`: the caller's `input`
3137
+ * is sent chunk by chunk with `callPort`, and the handler's output arrives the
3138
+ * same way on the other channel. Within each direction the next chunk is never
3139
+ * sent until the previous one has been delivered *and* pulled past by the
3140
+ * consumer (spec D11) — the reply to a chunk call *is* the confirmation, and
3141
+ * `listenPort` withholds it until then.
3142
+ *
3143
+ * A stream port carries exactly one invocation. To make several calls, open
3144
+ * several ports: `mux.openPort({ kind: "stream" })` per call.
3145
+ */
3146
+ function duplexOverPort(port, options = {}) {
3147
+ return (input) => runCallerSide(port, input, options);
3148
+ }
3149
+ /**
3150
+ * Installs `handler` as the serving side of one stream on `port`. Returns an
3151
+ * idempotent teardown that abandons the stream and notifies the peer.
3152
+ */
3153
+ function serveDuplexOverPort(port, handler, options = {}) {
3154
+ const controller = new AbortController();
3155
+ const notice = installAbortNotice(port, controller);
3156
+ abortOnPortClose(port, controller);
3157
+ const clock = installStreamTimeout(controller, options.timeout);
3158
+ const inbound = receiveChunks(port, CHANNEL_IN, controller, clock.touch);
3159
+ let output;
3160
+ try {
3161
+ output = handler(inbound.stream);
3162
+ } catch (err) {
3163
+ const pump = sendChunks(port, CHANNEL_OUT, failing(err), options, controller.signal, clock.touch);
3164
+ pump.catch(() => {});
3165
+ disarmClockWhenBothSidesSettle(clock, pump, inbound.ended);
3166
+ return teardownOnce(controller, notice, clock, inbound, void 0);
3167
+ }
3168
+ const pump = sendChunks(port, CHANNEL_OUT, output, options, controller.signal, clock.touch);
3169
+ pump.catch(() => {});
3170
+ disarmClockWhenBothSidesSettle(clock, pump, inbound.ended);
3171
+ return teardownOnce(controller, notice, clock, inbound, output);
3172
+ }
3173
+ /**
3174
+ * A stream that completes normally has nobody left to call the returned
3175
+ * teardown — the caller only knows the stream ended, not that it must also
3176
+ * dispose the serve-side handle. Without this, the last `touch()`'s timer
3177
+ * stays armed for up to `timeout` ms after real completion, doing nothing
3178
+ * but holding the event loop open.
3179
+ *
3180
+ * Only disarms once BOTH `pump` (our output) and `inboundEnded` (the peer's
3181
+ * declared end of input, or an abort) have settled. Disarming on `pump`
3182
+ * alone is not safe: under half-close the handler can finish producing
3183
+ * output while the caller is still sending input, and a clock stopped then
3184
+ * would leave that still-open half with no deadline at all — worse than the
3185
+ * leak this fixes.
3186
+ */
3187
+ function disarmClockWhenBothSidesSettle(clock, pump, inboundEnded) {
3188
+ Promise.allSettled([pump, inboundEnded]).then(() => {
3189
+ clock.stop();
2846
3190
  });
2847
- container.appendChild(iframe);
2848
- promise.then(() => {
2849
- iframe.contentWindow?.postMessage({ type: "CONNECT" }, "*", [messageChannel.port1]);
3191
+ }
3192
+ async function* failing(err) {
3193
+ throw err;
3194
+ }
3195
+ function teardownOnce(controller, notice, clock, inbound, output) {
3196
+ let torn = false;
3197
+ return () => {
3198
+ if (torn) return;
3199
+ torn = true;
3200
+ if (!controller.signal.aborted) controller.abort(/* @__PURE__ */ new Error("webrun-rpc: stream torn down"));
3201
+ notice.post();
3202
+ notice.stop();
3203
+ clock.stop();
3204
+ inbound.stop();
3205
+ output?.return(void 0).catch(() => {});
3206
+ };
3207
+ }
3208
+ function runCallerSide(port, input, options) {
3209
+ const controller = new AbortController();
3210
+ const notice = installAbortNotice(port, controller);
3211
+ abortOnPortClose(port, controller);
3212
+ const clock = installStreamTimeout(controller, options.timeout);
3213
+ const inbound = receiveChunks(port, CHANNEL_OUT, controller, clock.touch);
3214
+ const pump = sendChunks(port, CHANNEL_IN, input, options, controller.signal, clock.touch);
3215
+ pump.catch(() => {});
3216
+ return returningAtOnce((async function* () {
3217
+ try {
3218
+ yield* inbound.stream;
3219
+ } finally {
3220
+ if (!controller.signal.aborted) controller.abort(/* @__PURE__ */ new Error("webrun-rpc: the caller abandoned the stream"));
3221
+ notice.post();
3222
+ notice.stop();
3223
+ clock.stop();
3224
+ inbound.stop();
3225
+ await pump.catch(() => {});
3226
+ try {
3227
+ await port.close?.();
3228
+ } catch {}
3229
+ }
3230
+ })(), () => {
3231
+ if (!controller.signal.aborted) controller.abort(/* @__PURE__ */ new Error("webrun-rpc: the caller abandoned the stream"));
2850
3232
  });
3233
+ }
3234
+ /**
3235
+ * Listens for the peer's abort notice, and can post our own. The notice is a
3236
+ * plain message with no `channelName` and a `type` that is not `"request"`,
3237
+ * so neither `callPort` nor `listenPort` reacts to it, and it carries no
3238
+ * numeric `id`, so `structuredCodec` never mistakes it for a layer 1 envelope.
3239
+ */
3240
+ function installAbortNotice(port, controller) {
3241
+ const onMessage = (event) => {
3242
+ const data = event.data;
3243
+ if (!data || data.type !== "webrun-rpc:stream-abort") return;
3244
+ if (!controller.signal.aborted) controller.abort(/* @__PURE__ */ new Error("webrun-rpc: the peer abandoned the stream"));
3245
+ };
3246
+ port.addEventListener("message", onMessage);
3247
+ let posted = false;
2851
3248
  return {
2852
- baseUrl,
2853
- port: messageChannel.port2,
2854
- close: () => {
2855
- iframe.parentElement?.removeChild(iframe);
2856
- messageChannel.port1.close();
2857
- messageChannel.port2.close();
3249
+ post() {
3250
+ if (posted) return;
3251
+ posted = true;
3252
+ try {
3253
+ port.postMessage({ type: STREAM_ABORT });
3254
+ } catch {}
3255
+ },
3256
+ stop() {
3257
+ port.removeEventListener("message", onMessage);
2858
3258
  }
2859
3259
  };
2860
- function newIFrame(src) {
2861
- const iframe = document.createElement("iframe");
2862
- iframe.src = `${src}`;
2863
- Object.assign(iframe.style, {
2864
- padding: "0",
2865
- margin: "0",
2866
- border: "none",
2867
- outline: "none",
2868
- width: "100%",
2869
- height: "100%"
2870
- });
2871
- return {
2872
- iframe,
2873
- promise: new Promise((resolve, reject) => {
2874
- iframe.onerror = () => reject(/* @__PURE__ */ new Error(`Failed to load ${src}`));
2875
- iframe.onload = () => resolve(iframe);
2876
- })
2877
- };
2878
- }
2879
3260
  }
2880
- async function initializeConnection({ key, communicationPort, ...options }) {
2881
- const channel = new MessageChannel();
2882
- if (!await callChannel(communicationPort, "CONNECT", {
2883
- key,
2884
- ...options
2885
- }, channel.port2)) {
2886
- channel.port1.close();
2887
- channel.port2.close();
2888
- return null;
3261
+ /**
3262
+ * Aborts the stream when its port closes underneath it — the multiplexer was
3263
+ * closed, or the peer closed the port — for ports that report it (see
3264
+ * `getPortCloseSignal`). Without this a stream waiting for the peer's next
3265
+ * chunk would wait forever on a port that can no longer deliver one: the
3266
+ * outbound half's `callPort` already fails on the close signal, but nothing
3267
+ * failed the inbound half.
3268
+ */
3269
+ function abortOnPortClose(port, controller) {
3270
+ const closed = getPortCloseSignal(port);
3271
+ if (!closed) return;
3272
+ const onClose = () => {
3273
+ if (!controller.signal.aborted) controller.abort(closed.reason);
3274
+ };
3275
+ if (closed.aborted) {
3276
+ onClose();
3277
+ return;
2889
3278
  }
2890
- return channel.port1;
3279
+ closed.addEventListener("abort", onClose, { once: true });
3280
+ controller.signal.addEventListener("abort", () => closed.removeEventListener("abort", onClose), { once: true });
2891
3281
  }
2892
- async function registerConnectionsHandler({ key, path, handler, communicationPort }) {
2893
- const [register, cleanup] = newRegistry();
2894
- await callChannel(communicationPort, "REGISTER", path == null ? { key } : {
2895
- key,
2896
- path
2897
- });
2898
- register(() => callChannel(communicationPort, "UNREGISTER", { key }));
2899
- register(handleKeyedChannelCalls(communicationPort, "CONNECT", key, async (event, data, port) => {
2900
- return await handler(event, data, port);
2901
- }));
2902
- return cleanup;
3282
+ /**
3283
+ * The per-stream inactivity timeout (spec D8). Reset by any chunk in either
3284
+ * direction; elapsing aborts the stream. Unset, zero or non-finite installs no
3285
+ * timer at all, which is the default: a slow consumer is throttled, not failed.
3286
+ */
3287
+ function installStreamTimeout(controller, timeout) {
3288
+ if (timeout === void 0 || !Number.isFinite(timeout) || timeout <= 0) return {
3289
+ touch() {},
3290
+ stop() {}
3291
+ };
3292
+ let timer;
3293
+ let stopped = false;
3294
+ const arm = () => {
3295
+ timer = setTimeout(() => {
3296
+ if (!controller.signal.aborted) controller.abort(/* @__PURE__ */ new Error(`webrun-rpc: stream idle for ${timeout} ms`));
3297
+ }, timeout);
3298
+ };
3299
+ arm();
3300
+ return {
3301
+ touch() {
3302
+ if (stopped) return;
3303
+ if (timer !== void 0) clearTimeout(timer);
3304
+ arm();
3305
+ },
3306
+ stop() {
3307
+ stopped = true;
3308
+ if (timer !== void 0) clearTimeout(timer);
3309
+ timer = void 0;
3310
+ }
3311
+ };
2903
3312
  }
2904
3313
  /**
2905
- * `handleChannelCalls`, but only for calls whose `params.key` is `key`.
3314
+ * The receiving half of one direction.
2906
3315
  *
2907
- * ONE CONNECTION CARRIES SEVERAL SERVICES — an app at `/` and a mesh gateway
2908
- * at `/peers/` over one relay iframe is the shape mounts exist for. Plain
2909
- * `handleChannelCalls` cannot do that: every listener it has for a call type
2910
- * runs on every message, and each is handed the SAME reply port and the SAME
2911
- * transferred stream port. Two services then both serve the one channel the
2912
- * worker is reading, and its response comes back with both bodies in it.
3316
+ * The `listenPort` listener is installed **eagerly**, not lazily inside
3317
+ * `recieveIterator`'s installer, because a handler that never drains its input
3318
+ * would otherwise leave the peer's chunk calls with nobody to answer them. If
3319
+ * the local consumer has not started iterating, an inbound chunk waits for it —
3320
+ * which is the correct backpressure, and different from having no listener.
3321
+ */
3322
+ function receiveChunks(port, channelName, controller, touch) {
3323
+ let deliver;
3324
+ const waiting = [];
3325
+ let finished = false;
3326
+ let outstanding = false;
3327
+ let poison;
3328
+ let resolveEnded = () => {};
3329
+ const ended = new Promise((resolve) => {
3330
+ resolveEnded = resolve;
3331
+ });
3332
+ let aborted = false;
3333
+ let abortReason;
3334
+ const ready = () => deliver || finished ? Promise.resolve() : new Promise((r) => waiting.push(r));
3335
+ const wake = () => {
3336
+ for (const r of waiting.splice(0)) r();
3337
+ };
3338
+ const off = listenPort(port, async ({ done, value, error }) => {
3339
+ touch();
3340
+ if (poison) throw poison;
3341
+ if (outstanding) {
3342
+ poison = /* @__PURE__ */ new Error("webrun-rpc: peer sent a second unconfirmed chunk; the stream port is closed");
3343
+ controller.abort(poison);
3344
+ setTimeout(() => {
3345
+ try {
3346
+ port.close?.();
3347
+ } catch {}
3348
+ }, 0);
3349
+ throw poison;
3350
+ }
3351
+ outstanding = true;
3352
+ try {
3353
+ await ready();
3354
+ if (finished) throw poison ?? /* @__PURE__ */ new Error("webrun-rpc: the stream is closed");
3355
+ await deliver?.({
3356
+ done,
3357
+ value,
3358
+ error: error ? deserializeError(error) : void 0
3359
+ });
3360
+ } finally {
3361
+ outstanding = false;
3362
+ }
3363
+ if (done) resolveEnded();
3364
+ }, { channelName });
3365
+ const detach = () => {
3366
+ finished = true;
3367
+ wake();
3368
+ off();
3369
+ controller.signal.removeEventListener("abort", onAbort);
3370
+ resolveEnded();
3371
+ };
3372
+ const onAbort = () => {
3373
+ aborted = true;
3374
+ abortReason = controller.signal.reason;
3375
+ finished = true;
3376
+ wake();
3377
+ deliver?.({
3378
+ done: true,
3379
+ error: abortReason
3380
+ });
3381
+ resolveEnded();
3382
+ };
3383
+ controller.signal.addEventListener("abort", onAbort, { once: true });
3384
+ return {
3385
+ stream: recieveIterator((d) => {
3386
+ if (aborted) {
3387
+ d({
3388
+ done: true,
3389
+ error: abortReason
3390
+ });
3391
+ return detach;
3392
+ }
3393
+ deliver = d;
3394
+ wake();
3395
+ return detach;
3396
+ }),
3397
+ stop: detach,
3398
+ ended
3399
+ };
3400
+ }
3401
+ /**
3402
+ * The sending half of one direction: one `callPort` per chunk, one call
3403
+ * outstanding at a time (spec D11/D12), with no per-chunk deadline (spec D8).
3404
+ */
3405
+ async function sendChunks(port, channelName, output, { maxMessageSize, log }, signal, touch) {
3406
+ const stream = throughAbort(maxMessageSize ? toChunks(maxMessageSize)(output) : output, signal);
3407
+ try {
3408
+ await sendIterator(async ({ done, value, error }) => {
3409
+ if (signal.aborted) return;
3410
+ const chunk = {
3411
+ done,
3412
+ value,
3413
+ error: error === void 0 ? void 0 : serializeError(error)
3414
+ };
3415
+ log?.("[duplexOverPort] send", {
3416
+ channelName,
3417
+ done,
3418
+ size: value?.byteLength
3419
+ });
3420
+ await callPort(port, chunk, {
3421
+ channelName,
3422
+ timeout: NO_TIMEOUT,
3423
+ signal
3424
+ });
3425
+ touch();
3426
+ }, stream);
3427
+ } catch (err) {
3428
+ if (signal.aborted) return;
3429
+ throw err;
3430
+ }
3431
+ }
3432
+ /**
3433
+ * One virtual port.
2913
3434
  *
2914
- * WHY NOT A FILTER INSIDE THE HANDLER. Returning `false` for a foreign key
2915
- * does not help: `handleChannelCalls` still replies, `callChannel` resolves on
2916
- * the FIRST reply it receives, and the loser's `false` reaches the worker as
2917
- * "the client refused" — a 403, non-deterministically. A service that is not
2918
- * the addressee must stay SILENT and leave the transferred port untouched.
3435
+ * `deliver` and `markClosed` are deliberately not on `port`: the consumer holds
3436
+ * only a `MessageTarget`, so it cannot forge inbound traffic or close the port
3437
+ * out from under the multiplexer's bookkeeping.
3438
+ */
3439
+ function newVirtualPort(send, requestClose) {
3440
+ const listeners = /* @__PURE__ */ new Set();
3441
+ let closed = false;
3442
+ const closing = new AbortController();
3443
+ const abort = (reason) => {
3444
+ if (closing.signal.aborted) return;
3445
+ closing.abort(reason ?? /* @__PURE__ */ new Error("webrun-rpc: the port was closed"));
3446
+ };
3447
+ const port = {
3448
+ addEventListener(_type, listener) {
3449
+ listeners.add(listener);
3450
+ },
3451
+ removeEventListener(_type, listener) {
3452
+ listeners.delete(listener);
3453
+ },
3454
+ postMessage(message, transfer) {
3455
+ if (closed) return;
3456
+ send(message, transfer);
3457
+ },
3458
+ close() {
3459
+ if (closed) return;
3460
+ closed = true;
3461
+ const notify = requestClose;
3462
+ listeners.clear();
3463
+ notify();
3464
+ abort();
3465
+ }
3466
+ };
3467
+ setPortCloseSignal(port, closing.signal);
3468
+ return {
3469
+ port,
3470
+ deliver(payload) {
3471
+ if (closed) return;
3472
+ const event = new MessageEvent("message", { data: payload });
3473
+ for (const listener of [...listeners]) try {
3474
+ listener(event);
3475
+ } catch {}
3476
+ },
3477
+ markClosed(reason) {
3478
+ closed = true;
3479
+ listeners.clear();
3480
+ abort(reason);
3481
+ },
3482
+ isClosed() {
3483
+ return closed;
3484
+ }
3485
+ };
3486
+ }
3487
+ /**
3488
+ * The message of the error that fails the calls still in flight on a
3489
+ * multiplexer when it is closed (`PortMux.close()`).
3490
+ */
3491
+ const MUX_CLOSED_MESSAGE = "webrun-rpc: the multiplexer was closed";
3492
+ /** Ceiling on concurrently open virtual ports. Bounds the id table only. */
3493
+ const DEFAULT_MAX_PORTS = 1024;
3494
+ /**
3495
+ * The default `PortMux`: emulates multiplexing over a single port.
2919
3496
  *
2920
- * Local to this module on purpose. `handleChannelCalls` is also used where a
2921
- * call carries no key (REGISTER/UNREGISTER, and the worker's own CONNECT in
2922
- * `index-sw.ts`, which is the other direction), so its semantics must not
2923
- * change.
3497
+ * A transport that already multiplexes natively supplies its own `PortMux`
3498
+ * instead — this implementation is for transports that offer one pipe.
2924
3499
  */
2925
- function handleKeyedChannelCalls(target, callType, key, handler) {
2926
- const listener = async (event) => {
2927
- const data = event.data;
2928
- if (!data || data.type !== callType) return;
2929
- if (data.params?.key !== key) return;
2930
- const [port, ...transfers] = event.ports ?? [];
2931
- const response = {};
3500
+ function multiplexPort(port, options) {
3501
+ const { codec, onPort, side = "initiator", maxPorts = DEFAULT_MAX_PORTS, maxMessageSize } = options;
3502
+ const open = /* @__PURE__ */ new Map();
3503
+ let nextId = side === "initiator" ? 0 : 1;
3504
+ let muxClosed = false;
3505
+ const post = (envelope, transfer) => {
3506
+ if (muxClosed) return;
2932
3507
  try {
2933
- response.result = await handler(event, data.params, ...transfers);
2934
- } catch (error) {
2935
- response.error = serializeError(error);
2936
- }
2937
- port?.postMessage(response);
3508
+ codec.post(port, envelope, transfer);
3509
+ } catch {}
3510
+ };
3511
+ const attach = (id) => {
3512
+ let self;
3513
+ self = newVirtualPort((payload, transfer) => post({
3514
+ type: "message",
3515
+ id,
3516
+ payload
3517
+ }, transfer), (reason) => {
3518
+ post({
3519
+ type: "close",
3520
+ id,
3521
+ reason
3522
+ });
3523
+ open.delete(id);
3524
+ self.markClosed();
3525
+ });
3526
+ open.set(id, self);
3527
+ return self;
3528
+ };
3529
+ const handleEnvelope = (envelope) => {
3530
+ if (muxClosed) return;
3531
+ if (envelope.type === "open") {
3532
+ if (open.has(envelope.id)) return;
3533
+ if (open.size >= maxPorts) {
3534
+ post({
3535
+ type: "close",
3536
+ id: envelope.id,
3537
+ reason: "max-ports"
3538
+ });
3539
+ return;
3540
+ }
3541
+ const handle = attach(envelope.id);
3542
+ let accepted = false;
3543
+ if (onPort) try {
3544
+ accepted = onPort(handle.port, envelope.meta) !== false;
3545
+ } catch {
3546
+ accepted = false;
3547
+ }
3548
+ if (!accepted) {
3549
+ open.delete(envelope.id);
3550
+ handle.markClosed(/* @__PURE__ */ new Error("webrun-rpc: the port was rejected"));
3551
+ post({
3552
+ type: "close",
3553
+ id: envelope.id,
3554
+ reason: "rejected"
3555
+ });
3556
+ }
3557
+ return;
3558
+ }
3559
+ const handle = open.get(envelope.id);
3560
+ if (envelope.type === "message") {
3561
+ if (!handle) return;
3562
+ handle.deliver(envelope.payload);
3563
+ return;
3564
+ }
3565
+ if (!handle) return;
3566
+ open.delete(envelope.id);
3567
+ handle.markClosed(/* @__PURE__ */ new Error("webrun-rpc: the peer closed the port"));
3568
+ };
3569
+ const listener = (event) => {
3570
+ const envelope = codec.read(event);
3571
+ if (envelope) handleEnvelope(envelope);
3572
+ };
3573
+ port.addEventListener("message", listener);
3574
+ port.start?.();
3575
+ return {
3576
+ maxMessageSize,
3577
+ async openPort(meta) {
3578
+ if (muxClosed) throw new Error("webrun-rpc: the multiplexer is closed");
3579
+ if (open.size >= maxPorts) throw new RangeError(`webrun-rpc: maxPorts (${maxPorts}) reached`);
3580
+ while (open.has(nextId)) nextId += 2;
3581
+ const id = nextId;
3582
+ nextId += 2;
3583
+ const handle = attach(id);
3584
+ post({
3585
+ type: "open",
3586
+ id,
3587
+ meta
3588
+ });
3589
+ return handle.port;
3590
+ },
3591
+ async close() {
3592
+ if (muxClosed) return;
3593
+ for (const [id, handle] of [...open]) {
3594
+ post({
3595
+ type: "close",
3596
+ id
3597
+ });
3598
+ open.delete(id);
3599
+ handle.markClosed(/* @__PURE__ */ new Error(MUX_CLOSED_MESSAGE));
3600
+ }
3601
+ muxClosed = true;
3602
+ port.removeEventListener("message", listener);
3603
+ await port.close?.();
3604
+ }
3605
+ };
3606
+ }
3607
+ /**
3608
+ * Announced with every port this pair opens. Layer 1 never reads `meta` — it is
3609
+ * here so a peer running something else over the same multiplexer can tell a
3610
+ * stream port from whatever else it hands out, and so a packet capture says
3611
+ * what the port is for.
3612
+ */
3613
+ const STREAM_META = { kind: "stream" };
3614
+ /**
3615
+ * One port source in, one caller `Duplex` out.
3616
+ *
3617
+ * A call is a port: the mux allocates one, `duplexOverPort` runs the single
3618
+ * invocation on it, and closing the stream closes the port. No stream ids, no
3619
+ * framing and no credit accounting live here — the mux owns the first, and
3620
+ * `duplexOverPort` owns the one-chunk window that makes memory bounded.
3621
+ */
3622
+ const connect = async ({ mux: factory, timeout }) => {
3623
+ const mux = await factory();
3624
+ const streamOptions = {
3625
+ maxMessageSize: mux.maxMessageSize,
3626
+ timeout
3627
+ };
3628
+ const call = (input) => {
3629
+ let stream;
3630
+ let abandoned = false;
3631
+ return returningAtOnce((async function* () {
3632
+ const port = await mux.openPort(STREAM_META);
3633
+ if (abandoned) {
3634
+ await port.close?.();
3635
+ return;
3636
+ }
3637
+ stream = duplexOverPort(port, streamOptions)(input);
3638
+ yield* stream;
3639
+ })(), () => {
3640
+ abandoned = true;
3641
+ stream?.return(void 0).catch(() => {});
3642
+ });
3643
+ };
3644
+ return {
3645
+ call,
3646
+ async close() {
3647
+ await mux.close();
3648
+ }
3649
+ };
3650
+ };
3651
+ /**
3652
+ * One port source in, `handler` serving every call that arrives on it.
3653
+ *
3654
+ * Each inbound port is one invocation, so the factory's `onPort` is the accept
3655
+ * loop: it installs `handler` on the new port and nothing else. The returned
3656
+ * teardown abandons the streams still running *before* dropping the mux, so
3657
+ * the peer's callers reject with "the peer abandoned the stream" instead of
3658
+ * parking forever on a port that has silently gone inert — layer 1's close is
3659
+ * not observable to layer 2, which is why the notice has to be posted
3660
+ * deliberately.
3661
+ */
3662
+ const serve = async ({ mux: factory, timeout }, handler) => {
3663
+ const live = /* @__PURE__ */ new Set();
3664
+ let streamOptions = { timeout };
3665
+ const mux = await factory((port) => {
3666
+ let off;
3667
+ let finished = false;
3668
+ const tracked = (input) => (async function* () {
3669
+ try {
3670
+ yield* handler(input);
3671
+ } finally {
3672
+ finished = true;
3673
+ if (off) live.delete(off);
3674
+ }
3675
+ })();
3676
+ off = serveDuplexOverPort(port, tracked, streamOptions);
3677
+ if (!finished) live.add(off);
3678
+ });
3679
+ streamOptions = {
3680
+ maxMessageSize: mux.maxMessageSize,
3681
+ timeout
3682
+ };
3683
+ let torn = false;
3684
+ return async () => {
3685
+ if (torn) return;
3686
+ torn = true;
3687
+ for (const off of [...live]) {
3688
+ live.delete(off);
3689
+ try {
3690
+ off();
3691
+ } catch {}
3692
+ }
3693
+ await mux.close();
3694
+ };
3695
+ };
3696
+ /**
3697
+ * Ports over ONE PIPE OF BYTES — a `MessagePort`, a worker, a WebSocket.
3698
+ *
3699
+ * There is no second port to be had, so `multiplexPort`'s id table invents
3700
+ * them. Use this when the transport gives you exactly one channel.
3701
+ */
3702
+ function overPipe(pipe, options) {
3703
+ return (onPort) => multiplexPort(pipe, {
3704
+ ...options,
3705
+ onPort
3706
+ });
3707
+ }
3708
+ function isEnvelope(value) {
3709
+ if (typeof value !== "object" || value === null) return false;
3710
+ const candidate = value;
3711
+ if (typeof candidate.id !== "number") return false;
3712
+ if (!Number.isInteger(candidate.id) || candidate.id < 0) return false;
3713
+ return candidate.type === "open" || candidate.type === "message" || candidate.type === "close";
3714
+ }
3715
+ /**
3716
+ * For ports whose messages are structured values — a real `MessagePort`, a
3717
+ * worker, an iframe.
3718
+ *
3719
+ * Envelopes are posted as-is, so nothing is encoded, `ArrayBuffer`s move
3720
+ * zero-copy through the transfer list, and structured clone does the work the
3721
+ * platform already does well. This is a performance choice only: layer 2 may
3722
+ * not send anything a byte codec could not also carry.
3723
+ */
3724
+ const structuredCodec = {
3725
+ post(port, envelope, transfer) {
3726
+ if (transfer && transfer.length > 0) port.postMessage(envelope, transfer);
3727
+ else port.postMessage(envelope);
3728
+ },
3729
+ read(event) {
3730
+ return isEnvelope(event.data) ? event.data : void 0;
3731
+ }
3732
+ };
3733
+ //#endregion
3734
+ //#region src/link/async-dispose.ts
3735
+ /**
3736
+ * Makes `target` disposable with `await using`: installs `dispose` as its
3737
+ * `[Symbol.asyncDispose]`, where the runtime defines the symbol. Hand-written
3738
+ * iterators lose what a native generator gets from `AsyncIteratorPrototype`;
3739
+ * this gives it back. A no-op where the symbol is missing. Kept off the
3740
+ * declared types, which would require consumers to compile with the
3741
+ * `esnext.disposable` lib.
3742
+ */
3743
+ function asyncDisposable(target, dispose) {
3744
+ const asyncDispose = Symbol.asyncDispose;
3745
+ if (asyncDispose && !(asyncDispose in target)) Object.defineProperty(target, asyncDispose, {
3746
+ value: async () => {
3747
+ await dispose();
3748
+ },
3749
+ configurable: true,
3750
+ writable: true
3751
+ });
3752
+ return target;
3753
+ }
3754
+ //#endregion
3755
+ //#region src/link/http-link.ts
3756
+ /**
3757
+ * The version of the HTTP-over-`MessagePort` protocol this package speaks,
3758
+ * sent in every hand-shake that sets up a link. Version 1 was this package's
3759
+ * own `handleStreams` / `sendStream` (no backpressure); version 2 is
3760
+ * webrun-rpc's `connect` / `serve` over `overPipe` with webrun-http-streams'
3761
+ * `fetchOverDuplex` / `serveFetchOverDuplex`: one port per link, calls
3762
+ * multiplexed on it, each call a stream with a one-chunk window in both
3763
+ * directions, and cancellation that reaches the handler.
3764
+ */
3765
+ const HTTP_LINK_PROTOCOL = "webrun-http-link/2";
3766
+ /**
3767
+ * Why a request failed when its link went away under it: the far side (the
3768
+ * handler, or the ServiceWorker) was lost. The request may or may not have
3769
+ * been applied.
3770
+ */
3771
+ var LinkLostError = class extends Error {
3772
+ constructor(message = "The link was lost while this request was in flight") {
3773
+ super(`${message}; it may or may not have been applied.`);
3774
+ this.name = "LinkLostError";
3775
+ }
3776
+ };
3777
+ /**
3778
+ * Posted on a link's port by the calling end when it closes the link, so the
3779
+ * answering end can release it: a `MessagePort` never reports that its other
3780
+ * end is gone.
3781
+ */
3782
+ const LINK_CLOSE = "webrun-http:link-close";
3783
+ /** Opens the calling end of a link on `port` (one port per link). */
3784
+ async function connectHttpLink(port, { streamTimeout, maxPorts } = {}) {
3785
+ const { call, close } = await connect({
3786
+ mux: overPipe(port, {
3787
+ codec: structuredCodec,
3788
+ side: "initiator",
3789
+ maxPorts
3790
+ }),
3791
+ timeout: streamTimeout
3792
+ });
3793
+ return trackedLink((request) => fetchOverDuplex(call, request), async () => {
3794
+ try {
3795
+ port.postMessage({ type: LINK_CLOSE });
3796
+ } catch {}
3797
+ await close();
3798
+ });
3799
+ }
3800
+ /**
3801
+ * Serves `handler` on the answering end of a link. Resolves with a teardown
3802
+ * that abandons the requests still running (their handlers see the
3803
+ * cancellation) and releases the port.
3804
+ */
3805
+ async function serveHttpLink(port, handler, { streamTimeout, maxPorts, requestIdleMs } = {}) {
3806
+ const teardown = await serve({
3807
+ mux: overPipe(port, {
3808
+ codec: structuredCodec,
3809
+ side: "responder",
3810
+ maxPorts
3811
+ }),
3812
+ timeout: streamTimeout
3813
+ }, serveFetchOverDuplex(async (request) => await handler(request), { requestIdleMs }));
3814
+ const onClose = (event) => {
3815
+ if (event.data?.type === LINK_CLOSE) close();
3816
+ };
3817
+ const close = async () => {
3818
+ port.removeEventListener("message", onClose);
3819
+ await teardown();
3820
+ };
3821
+ port.addEventListener("message", onClose);
3822
+ return close;
3823
+ }
3824
+ /**
3825
+ * Wraps a `send` so the link knows its requests in flight (for draining and
3826
+ * load-based routing) and can fail them all at once when it is closed.
3827
+ */
3828
+ function trackedLink(send, release) {
3829
+ let inflight = 0;
3830
+ let closed = false;
3831
+ let lostReason;
3832
+ const failers = /* @__PURE__ */ new Set();
3833
+ const idleWaiters = [];
3834
+ const done = () => {
3835
+ inflight--;
3836
+ if (inflight === 0) for (const resolve of idleWaiters.splice(0)) resolve();
3837
+ };
3838
+ const link = {
3839
+ get inflight() {
3840
+ return inflight;
3841
+ },
3842
+ get closed() {
3843
+ return closed;
3844
+ },
3845
+ whenIdle() {
3846
+ return inflight === 0 ? Promise.resolve() : new Promise((r) => idleWaiters.push(r));
3847
+ },
3848
+ async send(request) {
3849
+ if (closed) throw lostReason ?? new LinkLostError("The link is closed");
3850
+ inflight++;
3851
+ let finished = false;
3852
+ const finish = () => {
3853
+ if (finished) return;
3854
+ finished = true;
3855
+ failers.delete(fail);
3856
+ done();
3857
+ };
3858
+ let fail = () => {};
3859
+ const lost = new Promise((_, reject) => {
3860
+ fail = reject;
3861
+ });
3862
+ failers.add(fail);
3863
+ lost.catch(() => {});
3864
+ let response;
3865
+ try {
3866
+ response = await Promise.race([send(request), lost]);
3867
+ } catch (error) {
3868
+ finish();
3869
+ throw lostReason ?? error;
3870
+ }
3871
+ if (!response.body) {
3872
+ finish();
3873
+ return response;
3874
+ }
3875
+ return new Response(guardBody(response.body, lost, () => lostReason, finish), {
3876
+ status: response.status,
3877
+ statusText: response.statusText,
3878
+ headers: response.headers
3879
+ });
3880
+ },
3881
+ async close(reason) {
3882
+ if (closed) return;
3883
+ closed = true;
3884
+ lostReason = reason ?? new LinkLostError("The link was closed");
3885
+ for (const fail of [...failers]) fail(lostReason);
3886
+ try {
3887
+ await release();
3888
+ } catch {}
3889
+ }
3890
+ };
3891
+ return asyncDisposable(link, () => link.close());
3892
+ }
3893
+ /**
3894
+ * A response body that reports its end (done, cancelled or failed) and fails
3895
+ * with the link's reason when the link is lost mid-stream.
3896
+ */
3897
+ function guardBody(body, lost, reason, finish) {
3898
+ const reader = body.getReader();
3899
+ return new ReadableStream({
3900
+ async pull(controller) {
3901
+ try {
3902
+ const { done, value } = await Promise.race([reader.read(), lost]);
3903
+ if (done) {
3904
+ finish();
3905
+ controller.close();
3906
+ return;
3907
+ }
3908
+ controller.enqueue(value);
3909
+ } catch (error) {
3910
+ finish();
3911
+ reader.cancel().catch(() => {});
3912
+ controller.error(reason() ?? error);
3913
+ }
3914
+ },
3915
+ async cancel(why) {
3916
+ finish();
3917
+ await reader.cancel(why).catch(() => {});
3918
+ }
3919
+ });
3920
+ }
3921
+ //#endregion
3922
+ //#region src/link/locks.ts
3923
+ function lockManager() {
3924
+ return globalThis.navigator?.locks;
3925
+ }
3926
+ function locksAvailable() {
3927
+ return lockManager() !== void 0;
3928
+ }
3929
+ /** A fresh lock name: `<prefix>:<uuid>`. */
3930
+ function newLockName(prefix) {
3931
+ return `${prefix}:${crypto.randomUUID()}`;
3932
+ }
3933
+ /**
3934
+ * Takes the lock `name` and holds it until the returned release is called or
3935
+ * this context dies. Resolves once the lock is held; with no Web Locks,
3936
+ * resolves at once.
3937
+ */
3938
+ async function holdLock(name) {
3939
+ const locks = lockManager();
3940
+ if (!locks) return () => {};
3941
+ let release = () => {};
3942
+ const released = new Promise((resolve) => {
3943
+ release = resolve;
3944
+ });
3945
+ await new Promise((held) => {
3946
+ locks.request(name, {}, () => {
3947
+ held();
3948
+ return released;
3949
+ }).catch(() => held());
3950
+ });
3951
+ return release;
3952
+ }
3953
+ /**
3954
+ * Calls `onLost` once the lock `name` is granted to this context — that is,
3955
+ * once whoever holds it is gone — and releases it at once. Returns a function
3956
+ * that stops watching. With no Web Locks, never calls `onLost`.
3957
+ */
3958
+ function watchLock(name, onLost) {
3959
+ const locks = lockManager();
3960
+ if (!locks) return () => {};
3961
+ const controller = new AbortController();
3962
+ locks.request(name, { signal: controller.signal }, () => {
3963
+ if (!controller.signal.aborted) onLost();
3964
+ }).catch(() => {});
3965
+ return () => controller.abort();
3966
+ }
3967
+ /**
3968
+ * The names of the locks held right now in this origin, or `undefined` where
3969
+ * that cannot be asked. A backup for `watchLock`: a grant can be late, a query
3970
+ * cannot.
3971
+ */
3972
+ async function heldLockNames() {
3973
+ const locks = lockManager();
3974
+ if (!locks?.query) return void 0;
3975
+ try {
3976
+ const { held = [] } = await locks.query();
3977
+ return new Set(held.map((lock) => lock.name ?? ""));
3978
+ } catch {
3979
+ return;
3980
+ }
3981
+ }
3982
+ //#endregion
3983
+ //#region src/link/rendezvous.ts
3984
+ /**
3985
+ * Matches replies to requests by id, for the control messages that hand
3986
+ * WebSocket ports across: each waiting request has a deadline, and all of a
3987
+ * peer's waiting requests can be failed at once when the peer is lost.
3988
+ */
3989
+ var PortRendezvous = class {
3990
+ _prefix;
3991
+ _waiting = /* @__PURE__ */ new Map();
3992
+ _counter = 0;
3993
+ constructor(_prefix = "r") {
3994
+ this._prefix = _prefix;
3995
+ }
3996
+ /** A fresh id for a request. */
3997
+ newId() {
3998
+ return `${this._prefix}:${++this._counter}`;
3999
+ }
4000
+ get size() {
4001
+ return this._waiting.size;
4002
+ }
4003
+ /** Waits for the reply to `id`, failing after `timeoutMs`. */
4004
+ wait(id, timeoutMs, describe = "the reply") {
4005
+ return new Promise((resolve, reject) => {
4006
+ const timer = setTimeout(() => {
4007
+ this._waiting.delete(id);
4008
+ reject(/* @__PURE__ */ new Error(`No answer to ${describe} within ${timeoutMs} ms`));
4009
+ }, timeoutMs);
4010
+ this._waiting.set(id, {
4011
+ resolve,
4012
+ reject,
4013
+ timer
4014
+ });
4015
+ });
4016
+ }
4017
+ /** Delivers the reply to `id`. `false` when nobody waits for it (it timed out). */
4018
+ settle(id, value) {
4019
+ const waiting = this._waiting.get(id);
4020
+ if (!waiting) return false;
4021
+ this._waiting.delete(id);
4022
+ clearTimeout(waiting.timer);
4023
+ waiting.resolve(value);
4024
+ return true;
4025
+ }
4026
+ fail(id, error) {
4027
+ const waiting = this._waiting.get(id);
4028
+ if (!waiting) return;
4029
+ this._waiting.delete(id);
4030
+ clearTimeout(waiting.timer);
4031
+ waiting.reject(error);
4032
+ }
4033
+ failAll(error) {
4034
+ for (const id of [...this._waiting.keys()]) this.fail(id, error);
4035
+ }
4036
+ };
4037
+ //#endregion
4038
+ //#region src/ws/frames.ts
4039
+ /**
4040
+ * The wire protocol of a WebSocket carried over a `MessagePort`.
4041
+ *
4042
+ * A `MessagePort` is a natural carrier for a WebSocket: messages arrive in
4043
+ * order, and strings and transferable `ArrayBuffer`s cross it as they are. The
4044
+ * protocol on top is these frames, the same in both directions:
4045
+ *
4046
+ * - `m` — one message. An `ArrayBuffer` is transferred, never copied.
4047
+ * - `c` — close. The side that closes first sends it; the other answers with
4048
+ * its own `c` (the echo), and both are then closed cleanly.
4049
+ * - `p` — ping, client to server, carrying the client's heartbeat period.
4050
+ * - `q` — pong, server to client.
4051
+ * - `a` — ack: the receiver consumed `n` more bytes of messages. This is the
4052
+ * flow control: what a side has sent and not yet seen acknowledged is its
4053
+ * `bufferedAmount`, and the server end can wait for it to fall
4054
+ * (`whenDrained`). Added in version 2; version 1 had no backpressure.
4055
+ *
4056
+ * HEARTBEAT. A `MessagePort` never reports that the other side died, so the
4057
+ * client pings every `hb` ms and closes with 1006 when pongs stop for `2·hb`;
4058
+ * the server closes with 1006 when pings stop for `3·hb`. That is how a socket
4059
+ * notices a closed tab or a terminated worker.
4060
+ *
4061
+ * Unknown frames are ignored, so a later version can add frames a peer of
4062
+ * this version skips.
4063
+ */
4064
+ /** The version of this frame protocol. Sent in the upgrade hand-shake. */
4065
+ const WS_PORT_PROTOCOL_VERSION = 2;
4066
+ /** Default heartbeat period of the client, in ms. */
4067
+ const DEFAULT_HEARTBEAT_MS = 1e4;
4068
+ /** How long a side waits for the echo of its close frame, in ms. */
4069
+ const DEFAULT_CLOSE_TIMEOUT_MS = 5e3;
4070
+ const defaultTimers = {
4071
+ setTimeout: (fn, ms) => setTimeout(fn, ms),
4072
+ clearTimeout: (id) => clearTimeout(id),
4073
+ setInterval: (fn, ms) => setInterval(fn, ms),
4074
+ clearInterval: (id) => clearInterval(id)
4075
+ };
4076
+ /** Recovers a frame from a message's data, or `undefined` for anything else. */
4077
+ function readFrame(data) {
4078
+ if (typeof data !== "object" || data === null) return void 0;
4079
+ const frame = data;
4080
+ switch (frame.t) {
4081
+ case "m": return typeof frame.d === "string" || frame.d instanceof ArrayBuffer ? frame : void 0;
4082
+ case "c": return {
4083
+ t: "c",
4084
+ code: typeof frame.code === "number" ? frame.code : 1005,
4085
+ reason: typeof frame.reason === "string" ? frame.reason : ""
4086
+ };
4087
+ case "p": {
4088
+ const hb = frame.hb;
4089
+ return {
4090
+ t: "p",
4091
+ hb: typeof hb === "number" && hb > 0 ? hb : DEFAULT_HEARTBEAT_MS
4092
+ };
4093
+ }
4094
+ case "q": return { t: "q" };
4095
+ case "a": {
4096
+ const n = frame.n;
4097
+ return typeof n === "number" && n >= 0 ? {
4098
+ t: "a",
4099
+ n
4100
+ } : void 0;
4101
+ }
4102
+ default: return;
4103
+ }
4104
+ }
4105
+ /** Posts `frame`, transferring a message's `ArrayBuffer`. */
4106
+ function postFrame(port, frame) {
4107
+ if (frame.t === "m" && frame.d instanceof ArrayBuffer) port.postMessage(frame, [frame.d]);
4108
+ else port.postMessage(frame);
4109
+ }
4110
+ /** The size a message counts for in `bufferedAmount`: UTF-8 bytes, or the buffer's length. */
4111
+ function messageSize(data) {
4112
+ return typeof data === "string" ? utf8Length(data) : data.byteLength;
4113
+ }
4114
+ /** The UTF-8 length of `text`, without encoding it. */
4115
+ function utf8Length(text) {
4116
+ let length = 0;
4117
+ for (let i = 0; i < text.length; i++) {
4118
+ const code = text.charCodeAt(i);
4119
+ if (code < 128) length += 1;
4120
+ else if (code < 2048) length += 2;
4121
+ else if (code >= 55296 && code <= 56319 && i + 1 < text.length) {
4122
+ const next = text.charCodeAt(i + 1);
4123
+ if (next >= 56320 && next <= 57343) {
4124
+ length += 4;
4125
+ i++;
4126
+ } else length += 3;
4127
+ } else length += 3;
4128
+ }
4129
+ return length;
4130
+ }
4131
+ /**
4132
+ * A `CloseEvent` for runtimes that have none: Node 22 has `Event` and
4133
+ * `MessageEvent` as globals, but not `CloseEvent`.
4134
+ */
4135
+ var WsCloseEvent = class extends Event {
4136
+ code;
4137
+ reason;
4138
+ wasClean;
4139
+ constructor(type, init = {}) {
4140
+ super(type, init);
4141
+ this.code = init.code ?? 0;
4142
+ this.reason = init.reason ?? "";
4143
+ this.wasClean = init.wasClean ?? false;
4144
+ }
4145
+ };
4146
+ /** A `close` event: the platform's `CloseEvent` where there is one. */
4147
+ function newCloseEvent(init) {
4148
+ const Native = globalThis.CloseEvent;
4149
+ return Native ? new Native("close", init) : new WsCloseEvent("close", init);
4150
+ }
4151
+ /**
4152
+ * What both ends of a socket over a `MessagePort` share: the frames, the close
4153
+ * handshake, and the byte accounting behind `bufferedAmount`.
4154
+ *
4155
+ * Not exported from the package: the two ends are `createWebSocketClass`'s
4156
+ * class and `ServerWebSocket`.
4157
+ */
4158
+ var PortSocketCore = class extends EventTarget {
4159
+ _port;
4160
+ _state = 0;
4161
+ _timers;
4162
+ _closeTimeoutMs;
4163
+ /** Bytes sent and not yet acknowledged by the peer, plus those still being prepared. */
4164
+ _inFlight = 0;
4165
+ /** Bytes of messages "sent" after the socket started closing: counted, never sent. */
4166
+ _discarded = 0;
4167
+ _toAck = 0;
4168
+ _ackScheduled = false;
4169
+ _closeTimer;
4170
+ _drainWaiters = [];
4171
+ constructor(options = {}) {
4172
+ super();
4173
+ this._timers = options.timers ?? defaultTimers;
4174
+ this._closeTimeoutMs = options.closeTimeoutMs ?? 5e3;
4175
+ }
4176
+ get readyState() {
4177
+ return this._state;
4178
+ }
4179
+ /**
4180
+ * Bytes queued by `send()` that the peer has not consumed yet. A peer
4181
+ * acknowledges what it has dispatched, so this falls as the other side reads.
4182
+ */
4183
+ get bufferedAmount() {
4184
+ return this._inFlight + this._discarded;
4185
+ }
4186
+ /**
4187
+ * Resolves once `bufferedAmount` is at most `limit` (default 0), or the
4188
+ * socket is closed. The backpressure a producer applies:
4189
+ * `if (socket.bufferedAmount > HIGH) await socket.whenDrained(LOW)`.
4190
+ */
4191
+ whenDrained(limit = 0) {
4192
+ if (this._state === 3 || this._inFlight <= limit) return Promise.resolve();
4193
+ return new Promise((resolve) => this._drainWaiters.push({
4194
+ limit,
4195
+ resolve
4196
+ }));
4197
+ }
4198
+ _attach(port) {
4199
+ this._port = port;
4200
+ port.onmessage = (event) => {
4201
+ const frame = readFrame(event.data);
4202
+ if (frame) this._onFrame(frame);
4203
+ };
4204
+ }
4205
+ _onFrame(frame) {
4206
+ switch (frame.t) {
4207
+ case "m":
4208
+ case "c":
4209
+ this._deliver(frame);
4210
+ return;
4211
+ case "a":
4212
+ this._inFlight = Math.max(0, this._inFlight - frame.n);
4213
+ this._wakeDrainWaiters();
4214
+ return;
4215
+ case "p":
4216
+ this._onPing(frame.hb);
4217
+ return;
4218
+ case "q":
4219
+ this._onPong();
4220
+ return;
4221
+ }
4222
+ }
4223
+ /** A message or close frame the socket is ready to act on. */
4224
+ _deliver(frame) {
4225
+ if (frame.t === "c") {
4226
+ this._onCloseFrame(frame.code, frame.reason);
4227
+ return;
4228
+ }
4229
+ if (this._state !== 1) return;
4230
+ this._dispatchMessage(frame.d);
4231
+ this._consumed(messageSize(frame.d));
4232
+ }
4233
+ _onPing(_hb) {}
4234
+ _onPong() {}
4235
+ _onClosed() {}
4236
+ /** Acknowledge consumed bytes, coalescing the acks of one turn into one frame. */
4237
+ _consumed(bytes) {
4238
+ this._toAck += bytes;
4239
+ if (this._ackScheduled) return;
4240
+ this._ackScheduled = true;
4241
+ queueMicrotask(() => {
4242
+ this._ackScheduled = false;
4243
+ const n = this._toAck;
4244
+ this._toAck = 0;
4245
+ if (n > 0 && this._state !== 3) this._post({
4246
+ t: "a",
4247
+ n
4248
+ });
4249
+ });
4250
+ }
4251
+ /** Reserves `bytes` in `bufferedAmount` for a message still being prepared (a Blob). */
4252
+ _reserve(bytes) {
4253
+ this._inFlight += bytes;
4254
+ }
4255
+ /** Sends one message whose bytes were reserved with `_reserve` (or `reserved = false`). */
4256
+ _sendData(data, reserved = false) {
4257
+ const size = messageSize(data);
4258
+ if (this._state !== 1) {
4259
+ if (reserved) this._inFlight = Math.max(0, this._inFlight - size);
4260
+ this._discarded += size;
4261
+ return;
4262
+ }
4263
+ if (!reserved) this._inFlight += size;
4264
+ this._post({
4265
+ t: "m",
4266
+ d: data
4267
+ });
4268
+ }
4269
+ _post(frame) {
4270
+ try {
4271
+ if (this._port) postFrame(this._port, frame);
4272
+ } catch {}
4273
+ }
4274
+ /** Starts the close handshake. */
4275
+ _startClose(code, reason) {
4276
+ if (this._state === 2 || this._state === 3) return;
4277
+ this._state = 2;
4278
+ this._post({
4279
+ t: "c",
4280
+ code,
4281
+ reason
4282
+ });
4283
+ this._closeTimer = this._timers.setTimeout(() => this._finish(1006, "", false), this._closeTimeoutMs);
4284
+ }
4285
+ _onCloseFrame(code, reason) {
4286
+ if (this._state === 3) return;
4287
+ if (this._state === 2) {
4288
+ this._finish(code, reason, true);
4289
+ return;
4290
+ }
4291
+ this._state = 2;
4292
+ this._post({
4293
+ t: "c",
4294
+ code,
4295
+ reason
4296
+ });
4297
+ this._finish(code, reason, true);
4298
+ }
4299
+ /** The socket is closed: release the port and tell the listeners. */
4300
+ _finish(code, reason, wasClean, error = false) {
4301
+ if (this._state === 3) return;
4302
+ this._state = 3;
4303
+ if (this._closeTimer !== void 0) this._timers.clearTimeout(this._closeTimer);
4304
+ this._closeTimer = void 0;
4305
+ this._onClosed();
4306
+ if (this._port) {
4307
+ this._port.onmessage = null;
4308
+ try {
4309
+ this._port.close();
4310
+ } catch {}
4311
+ }
4312
+ this._wakeDrainWaiters();
4313
+ if (error) this.dispatchEvent(new Event("error"));
4314
+ this.dispatchEvent(newCloseEvent({
4315
+ code,
4316
+ reason,
4317
+ wasClean
4318
+ }));
4319
+ }
4320
+ _wakeDrainWaiters() {
4321
+ if (this._drainWaiters.length === 0) return;
4322
+ const closed = this._state === 3;
4323
+ this._drainWaiters = this._drainWaiters.filter(({ limit, resolve }) => {
4324
+ if (!closed && this._inFlight > limit) return true;
4325
+ resolve();
4326
+ return false;
4327
+ });
4328
+ }
4329
+ };
4330
+ /** Copies `data` into a fresh `ArrayBuffer`: `send()` must never detach the caller's buffer. */
4331
+ function toArrayBuffer(data) {
4332
+ if (data instanceof ArrayBuffer) return data.slice(0);
4333
+ const copy = new Uint8Array(data.byteLength);
4334
+ copy.set(new Uint8Array(data.buffer, data.byteOffset, data.byteLength));
4335
+ return copy.buffer;
4336
+ }
4337
+ function isBinary(data) {
4338
+ return data instanceof ArrayBuffer || ArrayBuffer.isView(data);
4339
+ }
4340
+ function isBlob(data) {
4341
+ return typeof Blob !== "undefined" && data instanceof Blob;
4342
+ }
4343
+ /** Valid close codes for an application to send: 1000, or 3000–4999. */
4344
+ function isApplicationCloseCode(code) {
4345
+ return code === 1e3 || code >= 3e3 && code <= 4999;
4346
+ }
4347
+ //#endregion
4348
+ //#region src/ws/client.ts
4349
+ /** RFC 7230 `token`: what a subprotocol name may be. */
4350
+ const TOKEN = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
4351
+ /**
4352
+ * A WHATWG-compatible `WebSocket` class whose transport is a `MessagePort`
4353
+ * from `connect`. Code written against the browser's `WebSocket` runs on it
4354
+ * unchanged:
4355
+ *
4356
+ * ```ts
4357
+ * const WS = createWebSocketClass(adapter.connectWebSocket);
4358
+ * const ws = new WS(`${baseUrl}chat`, ["v1"]);
4359
+ * ws.onmessage = (event) => console.log(event.data);
4360
+ * ```
4361
+ *
4362
+ * It validates as the standard does (url, scheme, fragment, subprotocol
4363
+ * tokens and duplicates, close codes and reasons), has `binaryType`, the `on*`
4364
+ * handlers next to `addEventListener`, and the close handshake.
4365
+ * `bufferedAmount` is real: the server acknowledges what it consumed.
4366
+ *
4367
+ * The real `globalThis.WebSocket` is left alone; install the class where you
4368
+ * want it.
4369
+ */
4370
+ function createWebSocketClass(connect, options = {}) {
4371
+ const heartbeatMs = options.heartbeatMs ?? 1e4;
4372
+ class WebSocketOverPort extends PortSocketCore {
4373
+ static CONNECTING = 0;
4374
+ static OPEN = 1;
4375
+ static CLOSING = 2;
4376
+ static CLOSED = 3;
4377
+ CONNECTING = 0;
4378
+ OPEN = 1;
4379
+ CLOSING = 2;
4380
+ CLOSED = 3;
4381
+ url;
4382
+ extensions = "";
4383
+ _protocol = "";
4384
+ _binaryType = "blob";
4385
+ _handlers = {};
4386
+ _heartbeat;
4387
+ /** When the oldest ping still waiting for its pong was sent. */
4388
+ _unansweredSince;
4389
+ _onVisibility;
4390
+ /** Sends waiting for a Blob ahead of them to be read, so order is kept. */
4391
+ _chain;
4392
+ constructor(url, protocols = []) {
4393
+ super({
4394
+ timers: options.timers,
4395
+ closeTimeoutMs: options.closeTimeoutMs
4396
+ });
4397
+ this.url = parseUrl(url);
4398
+ const list = typeof protocols === "string" ? [protocols] : [...protocols];
4399
+ validateProtocols(list);
4400
+ for (const type of [
4401
+ "open",
4402
+ "message",
4403
+ "error",
4404
+ "close"
4405
+ ]) this.addEventListener(type, (event) => this._handlers[type]?.call(this, event));
4406
+ Promise.resolve().then(() => connect(this.url, list)).then((connection) => {
4407
+ try {
4408
+ this._opened(connection, list);
4409
+ } catch {
4410
+ this._failed();
4411
+ }
4412
+ }, () => this._failed());
4413
+ }
4414
+ get protocol() {
4415
+ return this._protocol;
4416
+ }
4417
+ get binaryType() {
4418
+ return this._binaryType;
4419
+ }
4420
+ set binaryType(value) {
4421
+ if (value === "blob" || value === "arraybuffer") this._binaryType = value;
4422
+ }
4423
+ get onopen() {
4424
+ return this._handlers.open ?? null;
4425
+ }
4426
+ set onopen(fn) {
4427
+ this._handlers.open = typeof fn === "function" ? fn : null;
4428
+ }
4429
+ get onmessage() {
4430
+ return this._handlers.message ?? null;
4431
+ }
4432
+ set onmessage(fn) {
4433
+ this._handlers.message = typeof fn === "function" ? fn : null;
4434
+ }
4435
+ get onerror() {
4436
+ return this._handlers.error ?? null;
4437
+ }
4438
+ set onerror(fn) {
4439
+ this._handlers.error = typeof fn === "function" ? fn : null;
4440
+ }
4441
+ get onclose() {
4442
+ return this._handlers.close ?? null;
4443
+ }
4444
+ set onclose(fn) {
4445
+ this._handlers.close = typeof fn === "function" ? fn : null;
4446
+ }
4447
+ send(data) {
4448
+ if (this._state === 0) throw new DOMException("Still in CONNECTING state.", "InvalidStateError");
4449
+ if (isBlob(data)) {
4450
+ const size = data.size;
4451
+ if (this._state !== 1) {
4452
+ this._sendData(new ArrayBuffer(size));
4453
+ return;
4454
+ }
4455
+ this._reserve(size);
4456
+ this._enqueue(async () => this._sendData(await data.arrayBuffer(), true));
4457
+ return;
4458
+ }
4459
+ const payload = isBinary(data) ? toArrayBuffer(data) : String(data);
4460
+ if (this._chain) this._enqueue(async () => this._sendData(payload));
4461
+ else this._sendData(payload);
4462
+ }
4463
+ close(code, reason) {
4464
+ if (code !== void 0 && !isApplicationCloseCode(code)) throw new DOMException(`The close code must be 1000 or in 3000-4999; ${code} is neither.`, "InvalidAccessError");
4465
+ const text = reason ?? "";
4466
+ if (utf8Length(text) > 123) throw new DOMException("The close reason is longer than 123 bytes.", "SyntaxError");
4467
+ if (this._state === 2 || this._state === 3) return;
4468
+ if (this._state === 0) {
4469
+ this._state = 2;
4470
+ return;
4471
+ }
4472
+ this._startClose(code ?? 1005, text);
4473
+ }
4474
+ _dispatchMessage(data) {
4475
+ const value = typeof data === "string" || this._binaryType === "arraybuffer" ? data : new Blob([data]);
4476
+ this.dispatchEvent(new MessageEvent("message", {
4477
+ data: value,
4478
+ origin: new URL(this.url).origin
4479
+ }));
4480
+ }
4481
+ _onPong() {
4482
+ this._unansweredSince = void 0;
4483
+ }
4484
+ _onClosed() {
4485
+ if (this._heartbeat !== void 0) this._timers.clearInterval(this._heartbeat);
4486
+ this._heartbeat = void 0;
4487
+ if (this._onVisibility) globalThis.document?.removeEventListener("visibilitychange", this._onVisibility);
4488
+ }
4489
+ _ping(hb = heartbeatMs) {
4490
+ this._unansweredSince ??= Date.now();
4491
+ this._post({
4492
+ t: "p",
4493
+ hb
4494
+ });
4495
+ }
4496
+ _opened(connection, requested) {
4497
+ const { port, protocol = "" } = connection ?? {};
4498
+ if (!port || typeof port.postMessage !== "function") {
4499
+ this._failed();
4500
+ return;
4501
+ }
4502
+ if (this._state !== 0) {
4503
+ try {
4504
+ port.postMessage({
4505
+ t: "c",
4506
+ code: 1006,
4507
+ reason: ""
4508
+ });
4509
+ port.close();
4510
+ } catch {}
4511
+ this._finish(1006, "", false, true);
4512
+ return;
4513
+ }
4514
+ if (protocol !== "" && !requested.includes(protocol)) {
4515
+ port.close();
4516
+ this._failed();
4517
+ return;
4518
+ }
4519
+ this._protocol = protocol;
4520
+ this._attach(port);
4521
+ this._state = 1;
4522
+ this._ping();
4523
+ this._heartbeat = this._timers.setInterval(() => {
4524
+ const since = this._unansweredSince;
4525
+ if (since !== void 0 && Date.now() - since > 2 * heartbeatMs) {
4526
+ this._finish(1006, "", false, true);
4527
+ return;
4528
+ }
4529
+ this._ping();
4530
+ }, heartbeatMs);
4531
+ const doc = globalThis.document;
4532
+ if (doc) {
4533
+ this._onVisibility = () => this._ping(doc.visibilityState === "hidden" ? Math.max(heartbeatMs, 7e4) : heartbeatMs);
4534
+ doc.addEventListener("visibilitychange", this._onVisibility);
4535
+ if (doc.visibilityState === "hidden") this._onVisibility();
4536
+ }
4537
+ this.dispatchEvent(new Event("open"));
4538
+ }
4539
+ _failed() {
4540
+ this._state = 2;
4541
+ this._finish(1006, "", false, true);
4542
+ }
4543
+ _enqueue(task) {
4544
+ const run = (this._chain ?? Promise.resolve()).then(task).catch(() => {});
4545
+ this._chain = run;
4546
+ run.then(() => {
4547
+ if (this._chain === run) this._chain = void 0;
4548
+ });
4549
+ }
4550
+ }
4551
+ return WebSocketOverPort;
4552
+ }
4553
+ function parseUrl(input) {
4554
+ let url;
4555
+ try {
4556
+ url = new URL(`${input}`, globalThis.location?.href);
4557
+ } catch {
4558
+ throw new DOMException(`The URL '${input}' is invalid.`, "SyntaxError");
4559
+ }
4560
+ if (url.protocol === "http:") url.protocol = "ws:";
4561
+ else if (url.protocol === "https:") url.protocol = "wss:";
4562
+ if (url.protocol !== "ws:" && url.protocol !== "wss:") throw new DOMException(`The URL's scheme must be 'ws', 'wss', 'http' or 'https'; '${url.protocol.slice(0, -1)}' is not allowed.`, "SyntaxError");
4563
+ if (url.hash !== "" || `${input}`.includes("#")) throw new DOMException(`The URL contains a fragment identifier ('${url.hash}'). Fragment identifiers are not allowed in WebSocket URLs.`, "SyntaxError");
4564
+ return url.href;
4565
+ }
4566
+ function validateProtocols(protocols) {
4567
+ const seen = /* @__PURE__ */ new Set();
4568
+ for (const protocol of protocols) {
4569
+ if (!TOKEN.test(protocol)) throw new DOMException(`The subprotocol '${protocol}' is invalid.`, "SyntaxError");
4570
+ if (seen.has(protocol)) throw new DOMException(`The subprotocol '${protocol}' is duplicated.`, "SyntaxError");
4571
+ seen.add(protocol);
4572
+ }
4573
+ }
4574
+ //#endregion
4575
+ //#region src/link/errors.ts
4576
+ function errorMessage(error) {
4577
+ return error instanceof Error ? error.message : String(error);
4578
+ }
4579
+ //#endregion
4580
+ //#region src/ws/server.ts
4581
+ /**
4582
+ * The server end of a socket over a `MessagePort`: what a handler gets from
4583
+ * `upgradeWebSocket` and talks to.
4584
+ *
4585
+ * - Events are delivered only after `accept()`; frames that arrive earlier are
4586
+ * queued. Pings are answered even before `accept()`, so a slow handler does
4587
+ * not look like a dead one.
4588
+ * - Messages arrive as strings or `ArrayBuffer`s.
4589
+ * - `bufferedAmount` counts what the client has not consumed yet, and
4590
+ * `whenDrained(limit)` waits for it to fall: the backpressure a producer
4591
+ * applies.
4592
+ * - When the client's pings stop for three heartbeat periods (a closed tab, a
4593
+ * terminated worker), the socket closes with 1006.
4594
+ */
4595
+ var ServerWebSocket = class extends PortSocketCore {
4596
+ static CONNECTING = 0;
4597
+ static OPEN = 1;
4598
+ static CLOSING = 2;
4599
+ static CLOSED = 3;
4600
+ url;
4601
+ protocol;
4602
+ _accepted = false;
4603
+ _queue = [];
4604
+ _heartbeatMs = DEFAULT_HEARTBEAT_MS;
4605
+ _lastPing = Date.now();
4606
+ _watch;
4607
+ constructor(port, options = {}) {
4608
+ super({
4609
+ timers: options.timers,
4610
+ closeTimeoutMs: options.closeTimeoutMs
4611
+ });
4612
+ this.url = options.url ?? "";
4613
+ this.protocol = options.protocol ?? "";
4614
+ this._attach(port);
4615
+ this._state = 1;
4616
+ this._watchHeartbeat();
4617
+ }
4618
+ /** Whether `accept()` was called. */
4619
+ get accepted() {
4620
+ return this._accepted;
4621
+ }
4622
+ /** Starts delivering events, the queued ones first. */
4623
+ accept() {
4624
+ if (this._accepted) return;
4625
+ this._accepted = true;
4626
+ for (const frame of this._queue.splice(0)) super._deliver(frame);
4627
+ }
4628
+ send(data) {
4629
+ if (!this._accepted) throw new DOMException("accept() the socket before sending on it.", "InvalidStateError");
4630
+ this._sendData(isBinary(data) ? toArrayBuffer(data) : String(data));
4631
+ }
4632
+ close(code = 1e3, reason = "") {
4633
+ if (!isSendableCloseCode(code)) throw new DOMException(`${code} is not a close code an endpoint may send.`, "InvalidAccessError");
4634
+ if (utf8Length(reason) > 123) throw new DOMException("The close reason is longer than 123 bytes.", "SyntaxError");
4635
+ this._startClose(code, reason);
4636
+ }
4637
+ _deliver(frame) {
4638
+ if (!this._accepted) {
4639
+ this._queue.push(frame);
4640
+ return;
4641
+ }
4642
+ super._deliver(frame);
4643
+ }
4644
+ _dispatchMessage(data) {
4645
+ this.dispatchEvent(new MessageEvent("message", { data }));
4646
+ }
4647
+ _onPing(hb) {
4648
+ this._post({ t: "q" });
4649
+ this._lastPing = Date.now();
4650
+ if (hb !== this._heartbeatMs) {
4651
+ this._heartbeatMs = hb;
4652
+ this._watchHeartbeat();
4653
+ }
4654
+ }
4655
+ _onClosed() {
4656
+ if (this._watch !== void 0) this._timers.clearInterval(this._watch);
4657
+ this._watch = void 0;
4658
+ }
4659
+ _watchHeartbeat() {
4660
+ if (this._watch !== void 0) this._timers.clearInterval(this._watch);
4661
+ this._watch = this._timers.setInterval(() => {
4662
+ if (Date.now() - this._lastPing > 3 * this._heartbeatMs) this._finish(1006, "", false, true);
4663
+ }, this._heartbeatMs);
4664
+ }
4665
+ };
4666
+ /** Codes an endpoint may put in a close frame (RFC 6455 §7.4). */
4667
+ function isSendableCloseCode(code) {
4668
+ if (!Number.isInteger(code)) return false;
4669
+ if (code >= 3e3 && code <= 4999) return true;
4670
+ return code >= 1e3 && code <= 1014 && code !== 1004 && code !== 1005 && code !== 1006;
4671
+ }
4672
+ //#endregion
4673
+ //#region src/ws/upgrade.ts
4674
+ /**
4675
+ * The upgrade, as an HTTP exchange: a `GET` with `Upgrade: websocket` goes to
4676
+ * an ordinary `(Request) => Response` handler, and a `101` answer carrying a
4677
+ * server socket means accepted. Anything else is a refusal, with its status.
4678
+ *
4679
+ * FORBIDDEN HEADERS. A browser drops `Upgrade`, `Connection` and every
4680
+ * `Sec-*` header from a `new Request()`. The upgrade request therefore carries
4681
+ * its headers as an own `headers` property, a standalone `Headers` that keeps
4682
+ * them, so handler code reads `request.headers.get("upgrade")` as usual.
4683
+ *
4684
+ * NO 101 IN A `Response`. The constructor refuses statuses below 200, so the
4685
+ * response `upgradeWebSocket` returns reports 101 through an own `status`
4686
+ * property; the port that goes back to the caller is found with `takePort`.
4687
+ */
4688
+ /** The request header carrying the client's version of the frame protocol. */
4689
+ const WS_VERSION_HEADER = "x-webrun-ws-version";
4690
+ /**
4691
+ * The `GET` a handler sees for `new WebSocket(url, protocols)`. A `ws:` or
4692
+ * `wss:` url is given as `http:` or `https:`, as a server sees it.
4693
+ */
4694
+ function newWebSocketUpgradeRequest(url, { protocols = [], headers } = {}) {
4695
+ const httpUrl = new URL(`${url}`);
4696
+ if (httpUrl.protocol === "ws:") httpUrl.protocol = "http:";
4697
+ else if (httpUrl.protocol === "wss:") httpUrl.protocol = "https:";
4698
+ const all = new Headers(headers);
4699
+ all.set("upgrade", "websocket");
4700
+ all.set("connection", "Upgrade");
4701
+ all.set("sec-websocket-version", "13");
4702
+ if (!all.has("sec-websocket-key")) all.set("sec-websocket-key", randomKey());
4703
+ if (protocols.length > 0) all.set("sec-websocket-protocol", protocols.join(", "));
4704
+ if (!all.has("x-webrun-ws-version")) all.set(WS_VERSION_HEADER, `2`);
4705
+ return withHeaders(new Request(httpUrl, {
4706
+ method: "GET",
4707
+ headers: all
4708
+ }), all);
4709
+ }
4710
+ /**
4711
+ * Re-attaches `headers` to `request` as its own `headers` property, keeping
4712
+ * the forbidden ones a browser dropped. Returns `request`.
4713
+ */
4714
+ function withHeaders(request, headers) {
4715
+ Object.defineProperty(request, "headers", {
4716
+ value: headers,
4717
+ configurable: true
4718
+ });
4719
+ return request;
4720
+ }
4721
+ /** Whether `request` asks for a WebSocket. */
4722
+ function isWebSocketUpgrade(request) {
4723
+ return request.headers.get("upgrade")?.toLowerCase() === "websocket";
4724
+ }
4725
+ /** The subprotocols `request` asks for, in order of preference. */
4726
+ function requestedProtocols(request) {
4727
+ const header = request.headers.get("sec-websocket-protocol");
4728
+ if (!header) return [];
4729
+ return header.split(",").map((protocol) => protocol.trim()).filter((protocol) => protocol !== "");
4730
+ }
4731
+ /**
4732
+ * Where a response from `upgradeWebSocket` keeps its port. A registered
4733
+ * symbol, not a module-level `WeakMap`: the handler and the host that takes
4734
+ * the port often run different bundles of this package (a page's adapter from
4735
+ * `dist/sw.js`, its handler code importing `dist/worker.js`), and each bundle
4736
+ * has its own copy of every module.
4737
+ */
4738
+ const UPGRADE = Symbol.for("webrun-http-browser:websocket-upgrade");
4739
+ /**
4740
+ * Accepts a WebSocket upgrade in a handler:
4741
+ *
4742
+ * ```ts
4743
+ * if (isWebSocketUpgrade(request)) {
4744
+ * const { socket, response } = upgradeWebSocket(request, { protocol: "v1" });
4745
+ * socket.accept();
4746
+ * socket.addEventListener("message", (event) => socket.send(event.data));
4747
+ * return response;
4748
+ * }
4749
+ * ```
4750
+ *
4751
+ * The host takes the client's port from the response with `takePort` and
4752
+ * hands it to the caller.
4753
+ */
4754
+ function upgradeWebSocket(request, options = {}) {
4755
+ const protocol = options.protocol ?? "";
4756
+ const channel = new MessageChannel();
4757
+ const socket = new ServerWebSocket(channel.port1, {
4758
+ ...options,
4759
+ url: request.url,
4760
+ protocol
4761
+ });
4762
+ const response = new Response(null, {
4763
+ status: 200,
4764
+ headers: protocol ? { "sec-websocket-protocol": protocol } : {}
4765
+ });
4766
+ Object.defineProperties(response, {
4767
+ status: {
4768
+ value: 101,
4769
+ configurable: true
4770
+ },
4771
+ statusText: {
4772
+ value: "Switching Protocols",
4773
+ configurable: true
4774
+ },
4775
+ webSocket: {
4776
+ value: socket,
4777
+ configurable: true
4778
+ },
4779
+ [UPGRADE]: {
4780
+ value: {
4781
+ port: channel.port2,
4782
+ protocol
4783
+ },
4784
+ configurable: true,
4785
+ writable: true
4786
+ }
4787
+ });
4788
+ return {
4789
+ socket,
4790
+ response
4791
+ };
4792
+ }
4793
+ /**
4794
+ * The port to hand to the caller, for a response from `upgradeWebSocket`.
4795
+ * Taken once: a second call returns `undefined`.
4796
+ */
4797
+ function takePort(response) {
4798
+ return takeUpgrade(response)?.port;
4799
+ }
4800
+ /** `takePort`, with the subprotocol the server picked. */
4801
+ function takeUpgrade(response) {
4802
+ const holder = response;
4803
+ const upgrade = holder[UPGRADE];
4804
+ if (upgrade) holder[UPGRADE] = void 0;
4805
+ return upgrade;
4806
+ }
4807
+ /**
4808
+ * Runs an upgrade through `handler`, as a host does when a caller asks for a
4809
+ * socket: builds the upgrade request, calls the handler, and either takes the
4810
+ * port of a `101` or reports the refusal. A subprotocol the caller did not ask
4811
+ * for is a refusal, as in the standard.
4812
+ */
4813
+ async function serveWebSocketUpgrade(handler, { url, protocols = [], headers }) {
4814
+ let response;
4815
+ try {
4816
+ response = await handler(newWebSocketUpgradeRequest(url, {
4817
+ protocols,
4818
+ headers
4819
+ }));
4820
+ } catch (error) {
4821
+ return {
4822
+ ok: false,
4823
+ status: 500,
4824
+ message: errorMessage(error)
4825
+ };
4826
+ }
4827
+ const upgrade = takeUpgrade(response);
4828
+ if (!upgrade || response.status !== 101) {
4829
+ upgrade?.port.close();
4830
+ try {
4831
+ await response.body?.cancel();
4832
+ } catch {}
4833
+ return {
4834
+ ok: false,
4835
+ status: response.status,
4836
+ message: `The server refused the upgrade: ${response.status} ${response.statusText}`.trim()
4837
+ };
4838
+ }
4839
+ if (upgrade.protocol !== "" && !protocols.includes(upgrade.protocol)) {
4840
+ upgrade.port.close();
4841
+ return {
4842
+ ok: false,
4843
+ status: 400,
4844
+ message: `The server picked the subprotocol '${upgrade.protocol}', which was not requested.`
4845
+ };
4846
+ }
4847
+ return {
4848
+ ok: true,
4849
+ port: upgrade.port,
4850
+ protocol: upgrade.protocol
4851
+ };
4852
+ }
4853
+ function randomKey() {
4854
+ const bytes = /* @__PURE__ */ new Uint8Array(16);
4855
+ crypto.getRandomValues(bytes);
4856
+ return btoa(String.fromCharCode(...bytes));
4857
+ }
4858
+ //#endregion
4859
+ //#region src/relay/split-service-url.ts
4860
+ /**
4861
+ * Splits a URL of the form `<base>/<separator><key>/<path>` into parts.
4862
+ * Example: `https://host/~FS/a/b` → `{ baseUrl: "https://host/~FS/", key: "FS", path: "a/b" }`.
4863
+ */
4864
+ function splitServiceUrl(url, separator = "~") {
4865
+ const str = `${url}`;
4866
+ const empty = {
4867
+ url: str,
4868
+ key: "",
4869
+ baseUrl: "",
4870
+ path: ""
4871
+ };
4872
+ const hashIdx = str.indexOf("#");
4873
+ const queryIdx = str.indexOf("?");
4874
+ let strippedEnd = str.length;
4875
+ if (hashIdx >= 0) strippedEnd = Math.min(strippedEnd, hashIdx);
4876
+ if (queryIdx >= 0) strippedEnd = Math.min(strippedEnd, queryIdx);
4877
+ const stripped = str.slice(0, strippedEnd);
4878
+ let prefixEnd = 0;
4879
+ const schemeMatch = stripped.match(/^[a-zA-Z][a-zA-Z0-9+\-.]*:\/\//);
4880
+ if (schemeMatch) {
4881
+ prefixEnd = schemeMatch[0].length;
4882
+ const slashIdx = stripped.indexOf("/", prefixEnd);
4883
+ if (slashIdx >= 0) prefixEnd = slashIdx;
4884
+ else return empty;
4885
+ } else if (stripped.startsWith("//")) {
4886
+ prefixEnd = 2;
4887
+ const slashIdx = stripped.indexOf("/", prefixEnd);
4888
+ if (slashIdx >= 0) prefixEnd = slashIdx;
4889
+ else return empty;
4890
+ }
4891
+ const prefix = stripped.slice(0, prefixEnd);
4892
+ const pathPart = stripped.slice(prefixEnd);
4893
+ let keyStart;
4894
+ let rooted = false;
4895
+ if (prefix === "") {
4896
+ if (pathPart.startsWith(`/${separator}`)) {
4897
+ rooted = true;
4898
+ keyStart = separator.length + 1;
4899
+ } else if (pathPart.startsWith(separator)) keyStart = separator.length;
4900
+ else return empty;
4901
+ } else {
4902
+ if (!pathPart.startsWith(`/${separator}`)) return empty;
4903
+ keyStart = separator.length + 1;
4904
+ }
4905
+ const rest = pathPart.slice(keyStart);
4906
+ const slash = rest.indexOf("/");
4907
+ const key = slash < 0 ? rest : rest.slice(0, slash);
4908
+ if (key === "") return empty;
4909
+ return {
4910
+ url: str,
4911
+ key,
4912
+ baseUrl: `${prefix === "" ? rooted ? "/" : "" : `${prefix}/`}${separator}${key}${slash < 0 ? "" : "/"}`,
4913
+ path: slash < 0 ? "" : rest.slice(slash + 1)
4914
+ };
4915
+ }
4916
+ //#endregion
4917
+ //#region src/relay/index.ts
4918
+ /**
4919
+ * The URL of this module, kept in a variable on purpose. Bundlers (Vite
4920
+ * among them) rewrite every literal `new URL("<path>", import.meta.url)` into
4921
+ * an emitted asset at build time, before tree-shaking — so the defaults below
4922
+ * made every Vite consumer of this entry emit a dead copy of the package's
4923
+ * own `dist/index.js` (`"../"` resolves to the package, hence to its `main`).
4924
+ * Resolving against a variable is the same URL at run time and invisible to
4925
+ * that transform.
4926
+ */
4927
+ const moduleUrl = import.meta.url;
4928
+ /**
4929
+ * Returns a MessagePort that transparently bridges messages to/from the
4930
+ * page's ServiceWorker: the one controlling the page, or — when the page is
4931
+ * not controlled (a hard reload, or a page Firefox left uncontrolled) — the
4932
+ * active worker of `registration`, which answers messages all the same.
4933
+ */
4934
+ function newServiceWorkerPort(registration) {
4935
+ const channel = new MessageChannel();
4936
+ channel.port1.onmessage = (event) => {
4937
+ (navigator.serviceWorker.controller ?? registration?.active)?.postMessage(event.data, [...event.ports]);
4938
+ };
4939
+ navigator.serviceWorker.addEventListener("message", (event) => {
4940
+ channel.port1.postMessage(event.data, [...event.ports]);
4941
+ });
4942
+ return channel.port2;
4943
+ }
4944
+ /**
4945
+ * Registers a ServiceWorker and resolves with it once it is activated: the
4946
+ * worker controlling the page, or the registration's active worker when the
4947
+ * page is not controlled. Messaging works either way, which is all the relay
4948
+ * needs; nothing here waits for control, because an uncontrolled page (hard
4949
+ * reload; Firefox) may never get it. Rejects with a
4950
+ * `ServiceWorkerControlError` if activation takes longer than `timeout`.
4951
+ */
4952
+ async function initServiceWorker(options) {
4953
+ return (await registerServiceWorker(options)).worker;
4954
+ }
4955
+ async function registerServiceWorker({ swUrl, scopeUrl, type, timeout = DEFAULT_SERVICE_WORKER_TIMEOUT }) {
4956
+ const registration = await navigator.serviceWorker.register(swUrl, {
4957
+ type,
4958
+ scope: scopeUrl
4959
+ });
4960
+ const active = await awaitActiveServiceWorker(registration, { timeout });
4961
+ return {
4962
+ registration,
4963
+ worker: navigator.serviceWorker.controller ?? active
4964
+ };
4965
+ }
4966
+ /**
4967
+ * Serves a service's side of one `CONNECT`: an HTTP link on `callPort`, or a
4968
+ * WebSocket upgrade run through `handler`.
4969
+ */
4970
+ async function answerConnect(handler, data, callPort, options = {}) {
4971
+ const params = data ?? {};
4972
+ if (params.type === "ws") {
4973
+ callPort?.close();
4974
+ const { url, protocols = [], headers } = params;
4975
+ const result = await serveWebSocketUpgrade(handler, {
4976
+ url,
4977
+ protocols,
4978
+ headers
4979
+ });
4980
+ if (!result.ok) throw new Error(result.message);
4981
+ return withTransfer({
4982
+ port: result.port,
4983
+ protocol: result.protocol
4984
+ }, [result.port]);
4985
+ }
4986
+ if (!callPort) throw new Error("A CONNECT for HTTP carries a port");
4987
+ if (params.wire === "webrun-http-link/2") {
4988
+ await serveHttpLink(callPort, handler, options);
4989
+ return {
4990
+ accepted: true,
4991
+ wire: HTTP_LINK_PROTOCOL
4992
+ };
4993
+ }
4994
+ handleHttpRequests(callPort, handler);
4995
+ return true;
4996
+ }
4997
+ /**
4998
+ * Registers `handler` as the server for the given service `key` on the relay:
4999
+ * it answers HTTP requests and WebSocket upgrades. Returns a function that
5000
+ * unregisters the service; its promise settles once the relay acknowledged.
5001
+ */
5002
+ async function initHttpService(handler, { key, path, port, streamTimeout }) {
5003
+ return await registerConnectionsHandler({
5004
+ key,
5005
+ path,
5006
+ communicationPort: port,
5007
+ handler: (_event, data, callPort) => answerConnect(handler, data, callPort, { streamTimeout })
5008
+ });
5009
+ }
5010
+ /**
5011
+ * Sends a `Request` to the service registered under `key` on the relay and
5012
+ * resolves with the corresponding `Response`, whose body streams with
5013
+ * backpressure.
5014
+ */
5015
+ async function callHttpService(request, { key, port, streamTimeout }) {
5016
+ const channel = new MessageChannel();
5017
+ const reply = await callChannel(port, "CONNECT", {
5018
+ key,
5019
+ type: "http",
5020
+ wire: HTTP_LINK_PROTOCOL
5021
+ }, channel.port2);
5022
+ if (!reply) {
5023
+ channel.port1.close();
5024
+ throw new Error(`No service with key "${key}"`);
5025
+ }
5026
+ return await sendOverConnection(channel.port1, reply, request, { streamTimeout });
5027
+ }
5028
+ /**
5029
+ * Sends `request` on a port a service accepted with `reply`: over an HTTP link
5030
+ * when the service speaks `HTTP_LINK_PROTOCOL`, over version 1's transport
5031
+ * when it answered `true`. The link is closed once the response is done.
5032
+ */
5033
+ async function sendOverConnection(port, reply, request, options = {}) {
5034
+ if (reply === true) return await sendHttpRequest(port, request);
5035
+ const link = await connectHttpLink(port, options);
5036
+ try {
5037
+ const response = await link.send(request);
5038
+ link.whenIdle().then(() => link.close());
5039
+ return response;
5040
+ } catch (error) {
5041
+ await link.close();
5042
+ throw error;
5043
+ }
5044
+ }
5045
+ /**
5046
+ * Opens the transport of a socket to the service registered under `key` on
5047
+ * the relay: the service runs the upgrade through its handler, and the
5048
+ * socket's port comes back through the relay. After that, the socket runs
5049
+ * between the caller and the service directly.
5050
+ */
5051
+ async function callWebSocketService(url, protocols, { key, port, timeout = 15e3 }) {
5052
+ const httpUrl = new URL(`${url}`, globalThis.location?.href);
5053
+ if (httpUrl.protocol === "ws:") httpUrl.protocol = "http:";
5054
+ else if (httpUrl.protocol === "wss:") httpUrl.protocol = "https:";
5055
+ const channel = new MessageChannel();
5056
+ try {
5057
+ const connection = await callChannelWithin(timeout, port, "CONNECT", {
5058
+ type: "ws",
5059
+ key,
5060
+ url: `${httpUrl}`,
5061
+ protocols
5062
+ }, channel.port2);
5063
+ if (!(connection?.port instanceof MessagePort)) throw new Error(`The service "${key}" does not accept WebSockets (an older version?)`);
5064
+ return {
5065
+ port: connection.port,
5066
+ protocol: connection.protocol ?? ""
5067
+ };
5068
+ } finally {
5069
+ channel.port1.close();
5070
+ }
5071
+ }
5072
+ /**
5073
+ * A WHATWG `WebSocket` class whose sockets reach the service registered under
5074
+ * `key` on the relay.
5075
+ */
5076
+ function relayWebSocketClass(service, options) {
5077
+ return createWebSocketClass((url, protocols) => callWebSocketService(url, protocols, service), options);
5078
+ }
5079
+ /**
5080
+ * Returns a `window.onmessage` handler for use inside the relay iframe:
5081
+ * it accepts a CONNECT message, starts the relay ServiceWorker, and bridges
5082
+ * the parent's MessagePort with the SW. If the worker cannot be started, every
5083
+ * call the parent makes on that port is answered with the error, so the
5084
+ * parent's `initHttpService` / `callHttpService` reject instead of waiting.
5085
+ */
5086
+ function getRelayWindowMessageHandler({ swUrl = `${new URL("./relay-sw.js", moduleUrl)}`, scopeUrl = `${new URL("./", moduleUrl)}`, timeout } = {}) {
5087
+ let externalPort;
5088
+ return async (ev) => {
5089
+ if (ev.data?.type !== "CONNECT") return;
5090
+ const newExternalPort = ev.ports?.[0];
5091
+ if (!newExternalPort) return;
5092
+ if (externalPort) {
5093
+ newExternalPort.close();
5094
+ return;
5095
+ }
5096
+ externalPort = newExternalPort;
5097
+ let registration;
5098
+ try {
5099
+ ({registration} = await registerServiceWorker({
5100
+ swUrl,
5101
+ scopeUrl,
5102
+ timeout
5103
+ }));
5104
+ } catch (error) {
5105
+ const serialized = serializeError(error);
5106
+ externalPort.onmessage = (event) => event.ports[0]?.postMessage({ error: serialized });
5107
+ throw error;
5108
+ }
5109
+ const serviceWorkerPort = newServiceWorkerPort(registration);
5110
+ serviceWorkerPort.onmessage = (event) => {
5111
+ externalPort?.postMessage(event.data, [...event.ports]);
5112
+ };
5113
+ externalPort.onmessage = (event) => {
5114
+ serviceWorkerPort.postMessage(event.data, [...event.ports]);
5115
+ };
5116
+ };
5117
+ }
5118
+ /**
5119
+ * Embeds a hidden relay iframe, establishes a MessageChannel with it, and
5120
+ * returns the port to be used with `initHttpService` / `callHttpService`.
5121
+ */
5122
+ async function newRemoteRelayChannel({ baseUrl = new URL("../public-relay/", moduleUrl), url = new URL("relay.html", baseUrl), container = document.body } = {}) {
5123
+ const messageChannel = new MessageChannel();
5124
+ const { iframe, promise } = newIFrame(url);
5125
+ Object.assign(iframe.style, {
5126
+ position: "fixed",
5127
+ width: "1px",
5128
+ height: "1px",
5129
+ top: "-1000px",
5130
+ left: "-1000px",
5131
+ display: "block",
5132
+ opacity: "0",
5133
+ border: "none",
5134
+ outline: "none"
5135
+ });
5136
+ container.appendChild(iframe);
5137
+ promise.then(() => {
5138
+ iframe.contentWindow?.postMessage({ type: "CONNECT" }, "*", [messageChannel.port1]);
5139
+ });
5140
+ return {
5141
+ baseUrl,
5142
+ port: messageChannel.port2,
5143
+ close: () => {
5144
+ iframe.parentElement?.removeChild(iframe);
5145
+ messageChannel.port1.close();
5146
+ messageChannel.port2.close();
5147
+ }
5148
+ };
5149
+ function newIFrame(src) {
5150
+ const iframe = document.createElement("iframe");
5151
+ iframe.src = `${src}`;
5152
+ Object.assign(iframe.style, {
5153
+ padding: "0",
5154
+ margin: "0",
5155
+ border: "none",
5156
+ outline: "none",
5157
+ width: "100%",
5158
+ height: "100%"
5159
+ });
5160
+ return {
5161
+ iframe,
5162
+ promise: new Promise((resolve, reject) => {
5163
+ iframe.onerror = () => reject(/* @__PURE__ */ new Error(`Failed to load ${src}`));
5164
+ iframe.onload = () => resolve(iframe);
5165
+ })
5166
+ };
5167
+ }
5168
+ }
5169
+ async function initializeConnection({ key, communicationPort, ...options }) {
5170
+ const channel = new MessageChannel();
5171
+ if (!await callChannel(communicationPort, "CONNECT", {
5172
+ key,
5173
+ ...options
5174
+ }, channel.port2)) {
5175
+ channel.port1.close();
5176
+ channel.port2.close();
5177
+ return null;
5178
+ }
5179
+ return channel.port1;
5180
+ }
5181
+ /**
5182
+ * Registers `key` on the relay and answers its `CONNECT`s with `handler`.
5183
+ * Resolves with a function that unregisters it. That function's promise
5184
+ * settles once the relay acknowledged the `UNREGISTER`, so a caller can stop
5185
+ * its services and only then close the connection: closing first tore the
5186
+ * ports down before the message landed, and left the mount behind.
5187
+ */
5188
+ async function registerConnectionsHandler({ key, path, handler, communicationPort }) {
5189
+ await callChannel(communicationPort, "REGISTER", path == null ? { key } : {
5190
+ key,
5191
+ path
5192
+ });
5193
+ const stopAnswering = handleKeyedChannelCalls(communicationPort, "CONNECT", key, async (event, data, port) => await handler(event, data, port));
5194
+ let stopped;
5195
+ return () => {
5196
+ stopped ??= (async () => {
5197
+ try {
5198
+ await callChannel(communicationPort, "UNREGISTER", { key });
5199
+ } catch (error) {
5200
+ console.warn(`[relay] UNREGISTER of "${key}" failed`, error);
5201
+ } finally {
5202
+ stopAnswering();
5203
+ }
5204
+ })();
5205
+ return stopped;
5206
+ };
5207
+ }
5208
+ /**
5209
+ * `handleChannelCalls`, but only for calls whose `params.key` is `key`.
5210
+ *
5211
+ * ONE CONNECTION CARRIES SEVERAL SERVICES — an app at `/` and a mesh gateway
5212
+ * at `/peers/` over one relay iframe is the shape mounts exist for. Plain
5213
+ * `handleChannelCalls` cannot do that: every listener it has for a call type
5214
+ * runs on every message, and each is handed the SAME reply port and the SAME
5215
+ * transferred stream port. Two services then both serve the one channel the
5216
+ * worker is reading, and its response comes back with both bodies in it.
5217
+ *
5218
+ * WHY NOT A FILTER INSIDE THE HANDLER. Returning `false` for a foreign key
5219
+ * does not help: `handleChannelCalls` still replies, `callChannel` resolves on
5220
+ * the FIRST reply it receives, and the loser's `false` reaches the worker as
5221
+ * "the client refused" — a 403, non-deterministically. A service that is not
5222
+ * the addressee must stay SILENT and leave the transferred port untouched.
5223
+ *
5224
+ * Local to this module on purpose. `handleChannelCalls` is also used where a
5225
+ * call carries no key (REGISTER/UNREGISTER, and the worker's own CONNECT in
5226
+ * `index-sw.ts`, which is the other direction), so its semantics must not
5227
+ * change.
5228
+ */
5229
+ function handleKeyedChannelCalls(target, callType, key, handler) {
5230
+ const listener = async (event) => {
5231
+ const data = event.data;
5232
+ if (!data || data.type !== callType) return;
5233
+ if (data.params?.key !== key) return;
5234
+ const [port, ...transfers] = event.ports ?? [];
5235
+ const response = {};
5236
+ let transfer = [];
5237
+ try {
5238
+ const result = await handler(event, data.params, ...transfers);
5239
+ ({value: response.result, transfer} = unwrapTransfer(result));
5240
+ } catch (error) {
5241
+ response.error = serializeError(error);
5242
+ }
5243
+ port?.postMessage(response, transfer);
5244
+ };
5245
+ target.addEventListener("message", listener);
5246
+ target.start?.();
5247
+ return () => target.removeEventListener("message", listener);
5248
+ }
5249
+ //#endregion
5250
+ //#region src/sw/handler-endpoint.ts
5251
+ /** The message calls through a lost ServiceWorker fail with. */
5252
+ const ROUTER_LOST_MESSAGE = "The ServiceWorker was lost while this call was in flight";
5253
+ /**
5254
+ * The handler's end of its links to the ServiceWorker, the same in a page and
5255
+ * in a Dedicated Worker.
5256
+ *
5257
+ * - Serves `handler` on each link's `down` port, and answers WebSocket
5258
+ * upgrades arriving on its `ctl` port by running them through `handler`.
5259
+ * - Sends requests through the router on `up` (`fetch`) and opens sockets to
5260
+ * other handlers over `ctl` (`connectWebSocket`).
5261
+ * - Watches the ServiceWorker's router lock. When the ServiceWorker dies, the
5262
+ * links made for it are dropped: calls in flight on them fail with
5263
+ * `LinkLostError` ("may or may not have been applied") instead of hanging,
5264
+ * and new calls wait for a link, asking for one (`requestRelink`) every
5265
+ * `relinkRetryMs` until it comes.
5266
+ * - While calls are in flight, calls `keepAlive` every `keepAliveMs`: port
5267
+ * messages are not events, so without it the browser may stop the
5268
+ * ServiceWorker in the middle of a long call.
5269
+ */
5270
+ var HandlerEndpoint = class {
5271
+ handler;
5272
+ options;
5273
+ _links = /* @__PURE__ */ new Set();
5274
+ _waiters = [];
5275
+ _sockets = new PortRendezvous("o");
5276
+ _keepAliveTimer;
5277
+ _closed = false;
5278
+ constructor(handler, options = {}) {
5279
+ this.handler = handler;
5280
+ this.options = options;
5281
+ }
5282
+ /** Whether any link is live. */
5283
+ get linked() {
5284
+ return this._links.size > 0;
5285
+ }
5286
+ /** Installs a fresh link made for the router holding `routerLock`. */
5287
+ async link(key, ports, routerLock) {
5288
+ if (this._closed) {
5289
+ for (const port of Object.values(ports)) port?.close();
5290
+ return;
5291
+ }
5292
+ const closeDown = await serveHttpLink(ports.down, this.handler, this.options);
5293
+ const up = ports.up ? await connectHttpLink(ports.up, this.options) : void 0;
5294
+ const link = {
5295
+ key,
5296
+ routerLock,
5297
+ ctl: ports.ctl,
5298
+ up,
5299
+ closeDown,
5300
+ stopWatch: () => {}
5301
+ };
5302
+ if (ports.ctl) ports.ctl.onmessage = (event) => this._onControl(link, event.data);
5303
+ if (routerLock) link.stopWatch = watchLock(routerLock, () => this._routerLost(routerLock));
5304
+ for (const old of [...this._links]) {
5305
+ if (old.key !== key) continue;
5306
+ if (old.routerLock !== routerLock) this._drop(old, new LinkLostError(ROUTER_LOST_MESSAGE));
5307
+ else if (!old.superseded) {
5308
+ old.superseded = true;
5309
+ setTimeout(() => void this._drop(old), this.options.supersededLinkMs ?? 3e4);
5310
+ }
5311
+ }
5312
+ this._links.add(link);
5313
+ for (const wake of this._waiters.splice(0)) wake();
5314
+ }
5315
+ /** Drops the links attached under `key` (all of them without a key). */
5316
+ async unlink(key, reason) {
5317
+ const dropped = [...this._links].filter((link) => key === void 0 || link.key === key);
5318
+ await Promise.all(dropped.map((link) => this._drop(link, reason)));
5319
+ }
5320
+ /** The routers this endpoint has live links for. */
5321
+ routerLocks() {
5322
+ return new Set([...this._links].map((link) => link.routerLock));
5323
+ }
5324
+ /**
5325
+ * Sends `request` through the ServiceWorker's router, as if the browser had
5326
+ * fetched it: it reaches the handler its url routes to.
5327
+ */
5328
+ async fetch(request) {
5329
+ const link = await this._linkWith("up");
5330
+ this._startKeepAlive();
5331
+ return await link.up.send(request);
5332
+ }
5333
+ /** Opens a socket to the handler `url` routes to, through the ServiceWorker. */
5334
+ async connectWebSocket(url, protocols = []) {
5335
+ const link = await this._linkWith("ctl");
5336
+ const id = this._sockets.newId();
5337
+ const answer = this._sockets.wait(id, this.options.wsConnectTimeoutMs ?? 15e3, `the socket request for ${url}`);
5338
+ link.ctl?.postMessage({
5339
+ t: "ws-open",
5340
+ id,
5341
+ url,
5342
+ protocols
5343
+ });
5344
+ return await answer;
5345
+ }
5346
+ async close() {
5347
+ this._closed = true;
5348
+ if (this._keepAliveTimer) clearInterval(this._keepAliveTimer);
5349
+ await this.unlink(void 0, new LinkLostError("The handler was closed"));
5350
+ for (const wake of this._waiters.splice(0)) wake();
5351
+ }
5352
+ async _drop(link, reason) {
5353
+ if (!this._links.delete(link)) return;
5354
+ link.stopWatch();
5355
+ await link.up?.close(reason);
5356
+ try {
5357
+ await link.closeDown();
5358
+ } catch {}
5359
+ if (link.ctl) {
5360
+ link.ctl.onmessage = null;
5361
+ link.ctl.close();
5362
+ }
5363
+ }
5364
+ _routerLost(routerLock) {
5365
+ const reason = new LinkLostError(ROUTER_LOST_MESSAGE);
5366
+ for (const link of [...this._links]) if (link.routerLock === routerLock) this._drop(link, reason);
5367
+ this._sockets.failAll(reason);
5368
+ }
5369
+ /** A live link with `port`, waiting (and asking) for one when there is none. */
5370
+ async _linkWith(port) {
5371
+ const find = () => [...this._links].filter((link) => !link.superseded).reverse().find((link) => port === "up" ? link.up && !link.up.closed : link.ctl);
5372
+ const found = find();
5373
+ if (found) return found;
5374
+ const retryMs = this.options.relinkRetryMs ?? 2e3;
5375
+ const deadline = Date.now() + (this.options.relinkTimeoutMs ?? 3e4);
5376
+ while (!this._closed) {
5377
+ this.options.requestRelink?.();
5378
+ await new Promise((resolve) => {
5379
+ const timer = setTimeout(resolve, Math.max(0, Math.min(retryMs, deadline - Date.now())));
5380
+ this._waiters.push(() => {
5381
+ clearTimeout(timer);
5382
+ resolve();
5383
+ });
5384
+ });
5385
+ const link = find();
5386
+ if (link) return link;
5387
+ if (Date.now() >= deadline) break;
5388
+ }
5389
+ throw new LinkLostError("No link to the ServiceWorker could be made for this call");
5390
+ }
5391
+ _startKeepAlive() {
5392
+ if (this._keepAliveTimer || !this.options.keepAlive) return;
5393
+ this._keepAliveTimer = setInterval(() => {
5394
+ if ([...this._links].reduce((sum, link) => sum + (link.up?.inflight ?? 0), 0) === 0) {
5395
+ clearInterval(this._keepAliveTimer);
5396
+ this._keepAliveTimer = void 0;
5397
+ return;
5398
+ }
5399
+ this.options.keepAlive?.();
5400
+ }, this.options.keepAliveMs ?? 2e4);
5401
+ }
5402
+ _onControl(link, message) {
5403
+ if (!message || typeof message !== "object") return;
5404
+ switch (message.t) {
5405
+ case "ws-connect":
5406
+ this._acceptSocket(link, message);
5407
+ return;
5408
+ case "ws-opened":
5409
+ if (!this._sockets.settle(message.id, {
5410
+ port: message.port,
5411
+ protocol: message.protocol
5412
+ })) message.port.close();
5413
+ return;
5414
+ case "ws-failed":
5415
+ this._sockets.fail(message.id, new Error(message.message));
5416
+ return;
5417
+ }
5418
+ }
5419
+ async _acceptSocket(link, { id, url, protocols, headers }) {
5420
+ const ctl = link.ctl;
5421
+ if (!ctl) return;
5422
+ try {
5423
+ const result = await serveWebSocketUpgrade(this.handler, {
5424
+ url,
5425
+ protocols,
5426
+ headers
5427
+ });
5428
+ if (result.ok) ctl.postMessage({
5429
+ t: "ws-accept",
5430
+ id,
5431
+ protocol: result.protocol,
5432
+ port: result.port
5433
+ }, [result.port]);
5434
+ else ctl.postMessage({
5435
+ t: "ws-refuse",
5436
+ id,
5437
+ status: result.status,
5438
+ message: result.message
5439
+ });
5440
+ } catch (error) {
5441
+ ctl.postMessage({
5442
+ t: "ws-refuse",
5443
+ id,
5444
+ status: 500,
5445
+ message: errorMessage(error)
5446
+ });
5447
+ }
5448
+ }
5449
+ };
5450
+ //#endregion
5451
+ //#region src/sw/handler-table.ts
5452
+ /**
5453
+ * The default route: the handler with the fewest requests in flight, the
5454
+ * oldest on a tie. With one handler per key — one tab — that is the first
5455
+ * live handler of the key, as before routing was pluggable.
5456
+ */
5457
+ const leastInflightRoute = (_request, _key, handlers) => {
5458
+ let best;
5459
+ for (const handler of handlers) if (best === void 0 || handler.inflight < best.inflight || handler.inflight === best.inflight && handler.attachedAt < best.attachedAt) best = handler;
5460
+ return best;
5461
+ };
5462
+ var HandlerTable = class {
5463
+ _entries = /* @__PURE__ */ new Map();
5464
+ _waiters = [];
5465
+ /** Adds `entry`. Returns the entry it replaces (same key and id), if any. */
5466
+ add(entry) {
5467
+ let slots = this._entries.get(entry.key);
5468
+ if (!slots) {
5469
+ slots = /* @__PURE__ */ new Map();
5470
+ this._entries.set(entry.key, slots);
5471
+ }
5472
+ const replaced = slots.get(entry.id);
5473
+ slots.set(entry.id, entry);
5474
+ if (!entry.draining) this._waiters = this._waiters.filter(({ key, resolve }) => {
5475
+ if (key !== entry.key) return true;
5476
+ resolve(true);
5477
+ return false;
5478
+ });
5479
+ return replaced;
5480
+ }
5481
+ /** Removes `entry` if it is still the one in its slot. */
5482
+ remove(entry) {
5483
+ const slots = this._entries.get(entry.key);
5484
+ if (slots?.get(entry.id) !== entry) return false;
5485
+ slots.delete(entry.id);
5486
+ if (slots.size === 0) this._entries.delete(entry.key);
5487
+ return true;
5488
+ }
5489
+ get(key, id) {
5490
+ return this._entries.get(key)?.get(id);
5491
+ }
5492
+ /** Every entry of `key`, draining or not. */
5493
+ all(key) {
5494
+ return [...this._entries.get(key)?.values() ?? []];
5495
+ }
5496
+ /** The entries of `key` that take new requests. */
5497
+ live(key) {
5498
+ return this.all(key).filter((entry) => !entry.draining);
5499
+ }
5500
+ entries() {
5501
+ return [...this._entries.values()].flatMap((slots) => [...slots.values()]);
5502
+ }
5503
+ get size() {
5504
+ let size = 0;
5505
+ for (const slots of this._entries.values()) size += slots.size;
5506
+ return size;
5507
+ }
5508
+ /**
5509
+ * Resolves `true` once a live entry for `key` is added (at once if there is
5510
+ * one, unless `next`), `false` after `ms`.
5511
+ */
5512
+ waitFor(key, ms, next = false) {
5513
+ if (!next && this.live(key).length > 0) return Promise.resolve(true);
5514
+ return new Promise((resolve) => {
5515
+ const waiter = {
5516
+ key,
5517
+ resolve: (found) => {
5518
+ clearTimeout(timer);
5519
+ resolve(found);
5520
+ }
5521
+ };
5522
+ const timer = setTimeout(() => {
5523
+ this._waiters = this._waiters.filter((w) => w !== waiter);
5524
+ resolve(false);
5525
+ }, ms);
5526
+ this._waiters.push(waiter);
5527
+ });
5528
+ }
5529
+ };
5530
+ //#endregion
5531
+ //#region ../../node_modules/.pnpm/idb-keyval@6.3.0/node_modules/idb-keyval/dist/index.js
5532
+ function promisifyRequest(request) {
5533
+ return new Promise((resolve, reject) => {
5534
+ request.oncomplete = request.onsuccess = () => resolve(request.result);
5535
+ request.onabort = request.onerror = () => reject(request.error);
5536
+ });
5537
+ }
5538
+ function createStore(dbName, storeName) {
5539
+ let dbp;
5540
+ const getDB = () => {
5541
+ if (dbp) return dbp;
5542
+ const request = indexedDB.open(dbName);
5543
+ request.onupgradeneeded = () => request.result.createObjectStore(storeName);
5544
+ dbp = promisifyRequest(request);
5545
+ dbp.then((db) => {
5546
+ db.onclose = () => dbp = void 0;
5547
+ }, () => {
5548
+ dbp = void 0;
5549
+ });
5550
+ return dbp;
5551
+ };
5552
+ return (txMode, callback) => getDB().then((db) => callback(db.transaction(storeName, txMode).objectStore(storeName)));
5553
+ }
5554
+ let defaultGetStoreFunc;
5555
+ function defaultGetStore() {
5556
+ if (!defaultGetStoreFunc) defaultGetStoreFunc = createStore("keyval-store", "keyval");
5557
+ return defaultGetStoreFunc;
5558
+ }
5559
+ /**
5560
+ * Get a value by its key.
5561
+ *
5562
+ * @param key
5563
+ * @param customStore Method to get a custom store. Use with caution (see the docs).
5564
+ */
5565
+ function get(key, customStore = defaultGetStore()) {
5566
+ return customStore("readonly", (store) => promisifyRequest(store.get(key)));
5567
+ }
5568
+ /**
5569
+ * Set a value with a key.
5570
+ *
5571
+ * @param key
5572
+ * @param value
5573
+ * @param customStore Method to get a custom store. Use with caution (see the docs).
5574
+ */
5575
+ function set(key, value, customStore = defaultGetStore()) {
5576
+ return customStore("readwrite", (store) => {
5577
+ store.put(value, key);
5578
+ return promisifyRequest(store.transaction);
5579
+ });
5580
+ }
5581
+ //#endregion
5582
+ //#region src/sw/protocol.ts
5583
+ /**
5584
+ * The messages between pages, Dedicated Workers and the same-origin
5585
+ * ServiceWorker. Internal: the shapes may change between versions, which is
5586
+ * why every hand-shake carries `HTTP_LINK_PROTOCOL`.
5587
+ *
5588
+ * THREE PORTS PER HANDLER LINK. A page makes the channels and hands one end of
5589
+ * each to the handler (itself, or a worker) and the other to the
5590
+ * ServiceWorker; after that it carries nothing:
5591
+ *
5592
+ * - `down`: ServiceWorker → handler, the requests the handler serves;
5593
+ * - `ctl`: a raw port next to them, for what HTTP over a stream cannot carry,
5594
+ * transferred `MessagePort`s (WebSocket hand-over);
5595
+ * - `up` (workers only): handler → ServiceWorker, the requests the handler
5596
+ * sends through the router (to other handlers).
5597
+ */
5598
+ /** Page → ServiceWorker: attach a handler link. The name is the one version 1 used. */
5599
+ const ATTACH_CALL = "UPDATE_COMMUNICATION_PORT";
5600
+ /** Page → ServiceWorker: remove a handler link now. */
5601
+ const DETACH_CALL = "webrun-http:detach";
5602
+ /** Page → ServiceWorker: stop routing to a link, let its requests finish, then remove it. */
5603
+ const DRAIN_CALL = "webrun-http:drain";
5604
+ /** Page → ServiceWorker: open a WebSocket to a handler. */
5605
+ const WS_CONNECT_CALL = "webrun-http:ws-connect";
5606
+ /** ServiceWorker → pages: attach your links again (the ServiceWorker restarted). */
5607
+ const NEED_LINKS_CALL = "webrun-http:need-links";
5608
+ /**
5609
+ * Page → ServiceWorker, plain messages (no reply). A message to the
5610
+ * ServiceWorker object is an event: it starts a stopped worker and resets its
5611
+ * idle timer. Port messages do neither.
5612
+ */
5613
+ const WAKE_MESSAGE = "webrun-http:wake";
5614
+ const KEEPALIVE_MESSAGE = "webrun-http:keepalive";
5615
+ /** Page ↔ worker: what `attachWorker` and `serveHttpInWorker` exchange. */
5616
+ const WORKER_HELLO = "webrun-http:worker-hello";
5617
+ //#endregion
5618
+ //#region src/sw/http-sw-dispatcher.ts
5619
+ /** One attached handler link, as the ServiceWorker holds it. */
5620
+ var HandlerEntry = class {
5621
+ key;
5622
+ id;
5623
+ owner;
5624
+ meta;
5625
+ link;
5626
+ ctl;
5627
+ clientId;
5628
+ ownerLock;
5629
+ draining = false;
5630
+ attachedAt = Date.now();
5631
+ sockets;
5632
+ stopWatch = () => {};
5633
+ closeUp;
5634
+ constructor(key, id, owner, meta, link, ctl, clientId, ownerLock) {
5635
+ this.key = key;
5636
+ this.id = id;
5637
+ this.owner = owner;
5638
+ this.meta = meta;
5639
+ this.link = link;
5640
+ this.ctl = ctl;
5641
+ this.clientId = clientId;
5642
+ this.ownerLock = ownerLock;
5643
+ this.sockets = new PortRendezvous(`s:${id}`);
5644
+ }
5645
+ get inflight() {
5646
+ return this.link.inflight;
5647
+ }
5648
+ };
5649
+ /** The reason requests fail with when their handler dies under them. */
5650
+ const HANDLER_LOST_MESSAGE = "The handler was lost while this request was in flight";
5651
+ const TIMED_OUT = Symbol("timed-out");
5652
+ /**
5653
+ * The same-origin ServiceWorker: routes `fetch` events under its scope to
5654
+ * handler links that pages attach — handlers in the pages themselves
5655
+ * (`SwHttpAdapter.register`) or in Dedicated Workers
5656
+ * (`SwHttpAdapter.attachWorker` + `serveHttpInWorker`).
5657
+ *
5658
+ * A url `<scope>/<key>/...` belongs to the handlers of `key`. Of those, the
5659
+ * `route` option picks one per request. Requests and responses stream with
5660
+ * backpressure in both directions, and an aborted fetch reaches the handler
5661
+ * as a cancellation (webrun-rpc + webrun-http-streams).
5662
+ *
5663
+ * Urls of keys this worker never served go to the network untouched: the
5664
+ * worker does not call `respondWith` for them.
5665
+ *
5666
+ * RESTARTS. The browser stops an idle ServiceWorker, and every port it held
5667
+ * dies with it. Nothing is persisted but the set of keys ever served: the
5668
+ * first request after a restart asks every window to attach its links again
5669
+ * (`NEED_LINKS`) and waits for them.
5670
+ *
5671
+ * LIVENESS. Each handler holds an owner lock; when it is granted to this
5672
+ * worker, the handler is gone and its requests in flight fail at once (502).
5673
+ * This worker holds a router lock that handlers watch the same way.
5674
+ */
5675
+ var SwHttpDispatcher = class {
5676
+ self;
5677
+ log;
5678
+ options;
5679
+ table = new HandlerTable();
5680
+ /**
5681
+ * Keys that were served at some point on this worker (sticky, kept in
5682
+ * IndexedDB). Lets a fetch tell "a site url whose handler is gone → 404"
5683
+ * from "an unknown url under the scope → the network".
5684
+ */
5685
+ claimedKeys = /* @__PURE__ */ new Set();
5686
+ /** The Web Lock this worker holds while it lives; handlers watch it. */
5687
+ routerLock;
5688
+ _routerHeld = Promise.resolve();
5689
+ _restored = Promise.resolve();
5690
+ _restoreSettled = false;
5691
+ _cleanup = [];
5692
+ _livenessTimer;
5693
+ _relinkRounds = /* @__PURE__ */ new Map();
5694
+ /** Keys whose last handler was drained: until the time stored, requests wait for the next. */
5695
+ _vacant = /* @__PURE__ */ new Map();
5696
+ constructor(options) {
5697
+ this.options = options;
5698
+ this.self = options.self;
5699
+ this.log = options.log ?? (() => {});
5700
+ if (locksAvailable()) this.routerLock = newLockName("webrun-http-router");
5701
+ }
5702
+ get scope() {
5703
+ return `${new URL("./", this.self.location.href)}`;
5704
+ }
5705
+ /** The key `url` belongs to: its first path segment under the scope. */
5706
+ keyOf(url) {
5707
+ const href = `${url}`;
5708
+ const scope = this.scope;
5709
+ if (!href.startsWith(scope)) return void 0;
5710
+ const key = href.substring(scope.length).replace(/[?#].*$/, "").split("/")[0];
5711
+ return key ? decodeURIComponent(key) : void 0;
5712
+ }
5713
+ /** Whether `key` has ever been served on this worker. */
5714
+ isClaimedKey(key) {
5715
+ return this.claimedKeys.has(key);
5716
+ }
5717
+ /** Whether requests for `key` are this worker's to answer. */
5718
+ serves(key) {
5719
+ return this.claimedKeys.has(key) || this.table.all(key).length > 0;
5720
+ }
5721
+ start() {
5722
+ const self = this.self;
5723
+ if (this.routerLock) this._routerHeld = holdLock(this.routerLock).then(() => {});
5724
+ this._restored = this._restoreClaimedKeys();
5725
+ const latch = () => {
5726
+ this._restoreSettled = true;
5727
+ };
5728
+ this._restored.then(latch, latch);
5729
+ this._cleanup.push(handleClaimRequests(self), handleChannelCalls(self, ATTACH_CALL, (event, params, ...ports) => this._attach(event, params, ports)), handleChannelCalls(self, DETACH_CALL, (_event, params) => {
5730
+ const entry = this._find(params);
5731
+ if (!entry) return false;
5732
+ this._close(entry, new LinkLostError("The handler was detached"));
5733
+ return true;
5734
+ }), handleChannelCalls(self, DRAIN_CALL, async (_event, params) => {
5735
+ const entry = this._find(params);
5736
+ if (!entry) return false;
5737
+ this._vacant.set(entry.key, Date.now() + (this.options.attachTimeoutMs ?? 15e3));
5738
+ await this._retire(entry, params.ms ?? this.options.handoverMs ?? 5e3);
5739
+ return true;
5740
+ }), handleChannelCalls(self, WS_CONNECT_CALL, async (event, params) => {
5741
+ if (!this._isOwnClient(event)) throw new Error("Only this origin's pages may open sockets");
5742
+ const { url, protocols } = params;
5743
+ const connection = await this.connectWebSocket(url, protocols ?? []);
5744
+ return withTransfer(connection, [connection.port]);
5745
+ }));
5746
+ const onMessage = (event) => {
5747
+ const type = event.data?.type;
5748
+ if (type === "webrun-http:wake") event.waitUntil(this._askForLinks());
5749
+ else if (type === "webrun-http:keepalive") event.waitUntil(Promise.resolve());
5750
+ };
5751
+ self.addEventListener("message", onMessage);
5752
+ this._cleanup.push(() => self.removeEventListener("message", onMessage));
5753
+ const onInstall = (event) => {
5754
+ this.log("Skip waiting on install.");
5755
+ event.waitUntil(self.skipWaiting());
5756
+ };
5757
+ const onActivate = (event) => {
5758
+ this.log("Claim control over all clients.");
5759
+ event.waitUntil(self.clients.claim());
5760
+ };
5761
+ const onFetch = (event) => this._onFetch(event);
5762
+ self.addEventListener("install", onInstall);
5763
+ self.addEventListener("activate", onActivate);
5764
+ self.addEventListener("fetch", onFetch);
5765
+ this._cleanup.push(() => {
5766
+ self.removeEventListener("install", onInstall);
5767
+ self.removeEventListener("activate", onActivate);
5768
+ self.removeEventListener("fetch", onFetch);
5769
+ });
5770
+ }
5771
+ async stop() {
5772
+ for (const cleanup of this._cleanup.splice(0)) cleanup();
5773
+ if (this._livenessTimer) clearInterval(this._livenessTimer);
5774
+ this._livenessTimer = void 0;
5775
+ for (const entry of this.table.entries()) this._close(entry, new LinkLostError("The ServiceWorker dispatcher stopped"));
5776
+ }
5777
+ /**
5778
+ * Answers `request` as a fetch under the scope would be: through the
5779
+ * handler its key routes to. A url outside the scope, or of a key this
5780
+ * worker never served, goes to the network.
5781
+ */
5782
+ async dispatch(request) {
5783
+ const key = this.keyOf(request.url);
5784
+ if (key === void 0) return await fetch(request);
5785
+ await this._restored.catch(() => {});
5786
+ const entry = await this._acquire(request, key);
5787
+ if (entry === TIMED_OUT) return new Response(null, {
5788
+ status: 503,
5789
+ statusText: "Service Unavailable (no handler attached in time)"
5790
+ });
5791
+ if (!entry) {
5792
+ if (!this.serves(key)) return await fetch(request);
5793
+ return new Response(null, {
5794
+ status: 404,
5795
+ statusText: "Not Found (no active handler)"
5796
+ });
5797
+ }
5798
+ try {
5799
+ return await entry.link.send(request);
5800
+ } catch (error) {
5801
+ this.log("[dispatch] failed", key, error);
5802
+ return new Response(errorMessage(error), {
5803
+ status: 502,
5804
+ statusText: "Bad Gateway",
5805
+ headers: { "content-type": "text/plain; charset=utf-8" }
5806
+ });
5807
+ }
5808
+ }
5809
+ /**
5810
+ * Opens a socket to the handler `url` routes to: the handler accepts or
5811
+ * refuses it like an HTTP upgrade, and the socket's port comes back. After
5812
+ * that the socket runs between the caller and the handler directly; this
5813
+ * worker is not on its data path.
5814
+ */
5815
+ async connectWebSocket(url, protocols = [], headers) {
5816
+ const request = newWebSocketUpgradeRequest(url, {
5817
+ protocols,
5818
+ headers
5819
+ });
5820
+ const key = this.keyOf(request.url);
5821
+ if (key === void 0) throw new Error(`${url} is outside this ServiceWorker's scope`);
5822
+ await this._restored.catch(() => {});
5823
+ const entry = await this._acquire(request, key);
5824
+ if (!entry || entry === TIMED_OUT) throw new Error(`No handler serves ${url}`);
5825
+ if (!entry.ctl) throw new Error(`The handler of ${url} does not accept sockets`);
5826
+ const id = entry.sockets.newId();
5827
+ const answer = entry.sockets.wait(id, this.options.wsConnectTimeoutMs ?? 15e3, `the socket request for ${url}`);
5828
+ entry.ctl.postMessage({
5829
+ t: "ws-connect",
5830
+ id,
5831
+ url: request.url,
5832
+ protocols,
5833
+ headers
5834
+ });
5835
+ return await answer;
5836
+ }
5837
+ _onFetch(event) {
5838
+ const request = event.request;
5839
+ const key = this.keyOf(request.url);
5840
+ if (key === void 0) return;
5841
+ if (this._restoreSettled) {
5842
+ if (this.serves(key)) event.respondWith(this.dispatch(request));
5843
+ else if (request.mode === "navigate") event.respondWith(fetch(request));
5844
+ return;
5845
+ }
5846
+ event.respondWith((async () => {
5847
+ await this._restored.catch(() => {});
5848
+ if (!this.serves(key)) return await fetch(request);
5849
+ return await this.dispatch(request);
5850
+ })());
5851
+ }
5852
+ async _attach(event, params, ports) {
5853
+ const clientId = event.source?.id;
5854
+ const [down, ...rest] = ports;
5855
+ if (!down) throw new Error("An attach carries at least one port");
5856
+ this.log("[attach]", clientId, params);
5857
+ if (params.wire === void 0) {
5858
+ const link = trackedLink((request) => sendHttpRequest(down, request), () => down.close());
5859
+ const id = clientId ?? crypto.randomUUID();
5860
+ this._add(new HandlerEntry(params.key, id, id, void 0, link, void 0, clientId, void 0));
5861
+ return { ...params };
5862
+ }
5863
+ if (params.wire !== "webrun-http-link/2") {
5864
+ for (const port of ports) port.close();
5865
+ throw new Error(`This ServiceWorker speaks ${HTTP_LINK_PROTOCOL}, the page ${params.wire}. Reload the page so that both run the same version of @statewalker/webrun-http-browser.`);
5866
+ }
5867
+ const ctl = params.ctl ? rest.shift() : void 0;
5868
+ const up = params.up ? rest.shift() : void 0;
5869
+ const streamTimeout = this.options.streamTimeout;
5870
+ const link = await connectHttpLink(down, { streamTimeout });
5871
+ const entry = new HandlerEntry(params.key, params.id ?? clientId ?? crypto.randomUUID(), params.owner ?? crypto.randomUUID(), params.meta, link, ctl, clientId, params.ownerLock);
5872
+ if (up) entry.closeUp = await serveHttpLink(up, (request) => this.dispatch(request), { streamTimeout });
5873
+ if (ctl) ctl.onmessage = (message) => this._onControl(entry, message.data);
5874
+ if (params.ownerLock) entry.stopWatch = watchLock(params.ownerLock, () => {
5875
+ this.log("[owner lost]", entry.key, entry.id);
5876
+ this._close(entry, new LinkLostError(HANDLER_LOST_MESSAGE));
5877
+ });
5878
+ this._add(entry);
5879
+ await this._routerHeld;
5880
+ return {
5881
+ accepted: HTTP_LINK_PROTOCOL,
5882
+ routerLock: this.routerLock
5883
+ };
5884
+ }
5885
+ _add(entry) {
5886
+ this._vacant.delete(entry.key);
5887
+ const replaced = this.table.add(entry);
5888
+ if (replaced && replaced !== entry) this._retire(replaced, this.options.handoverMs ?? 5e3, false);
5889
+ this._claim(entry.key);
5890
+ if (entry.ownerLock) this._watchLiveness();
5891
+ }
5892
+ /**
5893
+ * The backup of the owner-lock watches: every `livenessCheckMs`, drops the
5894
+ * handlers whose owner lock is no longer held. Runs only while some handler
5895
+ * has an owner lock, and keeps nothing awake: a timer is not an event.
5896
+ */
5897
+ _watchLiveness() {
5898
+ const period = this.options.livenessCheckMs ?? 5e3;
5899
+ if (this._livenessTimer || period <= 0) return;
5900
+ this._livenessTimer = setInterval(async () => {
5901
+ const watched = this.table.entries().filter((entry) => entry.ownerLock);
5902
+ if (watched.length === 0) {
5903
+ clearInterval(this._livenessTimer);
5904
+ this._livenessTimer = void 0;
5905
+ return;
5906
+ }
5907
+ const held = await heldLockNames();
5908
+ if (!held) return;
5909
+ for (const entry of watched) if (entry.ownerLock && !held.has(entry.ownerLock) && this.table.remove(entry)) {
5910
+ this.log("[owner gone]", entry.key, entry.id);
5911
+ this._close(entry, new LinkLostError(HANDLER_LOST_MESSAGE), false);
5912
+ }
5913
+ }, period);
5914
+ }
5915
+ /** The entry a drain or detach names, if its owner token still matches. */
5916
+ _find({ key, id, owner }) {
5917
+ const entry = this.table.get(key, id);
5918
+ return entry && entry.owner === owner ? entry : void 0;
5919
+ }
5920
+ /**
5921
+ * Stops routing to `entry`, lets its requests in flight run for up to `ms`,
5922
+ * then closes it. Requests that arrive meanwhile wait for the next owner.
5923
+ */
5924
+ async _retire(entry, ms, inTable = true) {
5925
+ entry.draining = true;
5926
+ let timer;
5927
+ await Promise.race([entry.link.whenIdle(), new Promise((resolve) => {
5928
+ timer = setTimeout(resolve, ms);
5929
+ })]);
5930
+ clearTimeout(timer);
5931
+ this._close(entry, new LinkLostError("The handler was drained"), inTable);
5932
+ }
5933
+ _close(entry, reason, inTable = true) {
5934
+ if (inTable) this.table.remove(entry);
5935
+ entry.stopWatch();
5936
+ entry.sockets.failAll(reason);
5937
+ entry.link.close(reason);
5938
+ entry.closeUp?.().catch(() => {});
5939
+ if (entry.ctl) {
5940
+ entry.ctl.onmessage = null;
5941
+ entry.ctl.close();
5942
+ }
5943
+ }
5944
+ /**
5945
+ * The handler for `request`: picked by `route` among the live handlers of
5946
+ * its key, waiting for one when there is none.
5947
+ */
5948
+ async _acquire(request, key) {
5949
+ const picked = await this._pick(request, key);
5950
+ if (picked) return picked;
5951
+ 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;
5952
+ const vacantUntil = this._vacant.get(key) ?? 0;
5953
+ if (vacantUntil > Date.now()) return await this.table.waitFor(key, vacantUntil - Date.now()) ? await this._pick(request, key) ?? TIMED_OUT : TIMED_OUT;
5954
+ if (!this.serves(key)) return void 0;
5955
+ await this._askForLinks(key);
5956
+ return await this._pick(request, key);
5957
+ }
5958
+ async _pick(request, key) {
5959
+ let live = this.table.live(key);
5960
+ for (const entry of live) {
5961
+ if (entry.ownerLock || !entry.clientId) continue;
5962
+ if (!await this.self.clients.get(entry.clientId)) this._close(entry, new LinkLostError(HANDLER_LOST_MESSAGE));
5963
+ }
5964
+ live = this.table.live(key);
5965
+ if (live.length === 0) return void 0;
5966
+ return (this.options.route ?? leastInflightRoute)(request, key, live);
5967
+ }
5968
+ /**
5969
+ * Asks every window to attach its links again, and waits until a handler
5970
+ * for `key` is back, every window answered, or `relinkTimeoutMs` passed.
5971
+ * At most one round per key every 10 s: a key whose tab is simply gone
5972
+ * should not cost every request the wait.
5973
+ */
5974
+ _askForLinks(key = "") {
5975
+ const round = this._relinkRounds.get(key);
5976
+ if (round && Date.now() - round.at < 1e4) return round.done;
5977
+ const done = this._linkRound(key);
5978
+ this._relinkRounds.set(key, {
5979
+ at: Date.now(),
5980
+ done
5981
+ });
5982
+ return done;
5983
+ }
5984
+ async _linkRound(key) {
5985
+ const timeout = this.options.relinkTimeoutMs ?? 3e3;
5986
+ const windows = await this.self.clients.matchAll({
5987
+ type: "window",
5988
+ includeUncontrolled: true
5989
+ });
5990
+ this.log("[need-links]", key, windows.length);
5991
+ const params = { routerLock: this.routerLock };
5992
+ const pending = new Set(windows);
5993
+ const answered = Promise.allSettled(windows.map((client) => callChannel(client, NEED_LINKS_CALL, params).then(() => pending.delete(client))));
5994
+ let timer;
5995
+ const timedOut = new Promise((resolve) => {
5996
+ timer = setTimeout(resolve, timeout);
5997
+ });
5998
+ await Promise.race([answered, timedOut]);
5999
+ clearTimeout(timer);
6000
+ for (const client of pending) this._askLegacyPage(client);
6001
+ }
6002
+ /** Asks a version-1 page for a fresh port, as version 1's worker did. */
6003
+ _askLegacyPage(client) {
6004
+ const channel = new MessageChannel();
6005
+ callChannel(client, ATTACH_CALL, {}, channel.port2).then((info) => {
6006
+ if (!info?.key || info.wire !== void 0) return;
6007
+ const link = trackedLink((request) => sendHttpRequest(channel.port1, request), () => channel.port1.close());
6008
+ this._add(new HandlerEntry(info.key, client.id, client.id, void 0, link, void 0, client.id, void 0));
6009
+ }, () => channel.port1.close());
6010
+ }
6011
+ _onControl(entry, message) {
6012
+ if (!message || typeof message !== "object") return;
6013
+ switch (message.t) {
6014
+ case "ws-accept":
6015
+ if (!entry.sockets.settle(message.id, {
6016
+ port: message.port,
6017
+ protocol: message.protocol
6018
+ })) message.port.close();
6019
+ return;
6020
+ case "ws-refuse":
6021
+ entry.sockets.fail(message.id, new Error(message.message));
6022
+ return;
6023
+ case "ws-open":
6024
+ this.connectWebSocket(message.url, message.protocols).then(({ port, protocol }) => entry.ctl?.postMessage({
6025
+ t: "ws-opened",
6026
+ id: message.id,
6027
+ protocol,
6028
+ port
6029
+ }, [port]), (error) => entry.ctl?.postMessage({
6030
+ t: "ws-failed",
6031
+ id: message.id,
6032
+ message: errorMessage(error)
6033
+ }));
6034
+ return;
6035
+ }
6036
+ }
6037
+ _isOwnClient(event) {
6038
+ const source = event.source;
6039
+ if (!source?.url) return false;
6040
+ try {
6041
+ return new URL(source.url).origin === this.self.location.origin;
6042
+ } catch {
6043
+ return false;
6044
+ }
6045
+ }
6046
+ async _claim(key) {
6047
+ if (!key || this.claimedKeys.has(key)) return;
6048
+ this.claimedKeys.add(key);
6049
+ try {
6050
+ await set("claimedKeys", [...this.claimedKeys].sort());
6051
+ } catch (error) {
6052
+ this.log("[claimedKeys] not stored", error);
6053
+ }
6054
+ }
6055
+ async _restoreClaimedKeys() {
6056
+ const stored = await get("claimedKeys") ?? [];
6057
+ for (const key of stored) this.claimedKeys.add(key);
6058
+ }
6059
+ };
6060
+ /**
6061
+ * Starts the same-origin dispatcher in a ServiceWorker. Returns a function
6062
+ * that stops it.
6063
+ */
6064
+ function startHttpDispatcher(options) {
6065
+ const dispatcher = new SwHttpDispatcher(options);
6066
+ dispatcher.start();
6067
+ return () => {
6068
+ dispatcher.stop();
6069
+ };
6070
+ }
6071
+ //#endregion
6072
+ //#region src/sw/worker-url.ts
6073
+ /**
6074
+ * Resolve a `serviceWorkerUrl` option the way a page would: relative to the
6075
+ * current document. Absolute urls pass through untouched. A url that cannot
6076
+ * be resolved is reported with the option name and the offending value,
6077
+ * because the raw `TypeError: Failed to construct 'URL': Invalid URL` says
6078
+ * neither.
6079
+ */
6080
+ function resolveWorkerUrl(serviceWorkerUrl) {
6081
+ const base = globalThis.location?.href;
6082
+ try {
6083
+ return new URL(serviceWorkerUrl, base);
6084
+ } catch (error) {
6085
+ throw new Error(`Invalid serviceWorkerUrl: ${JSON.stringify(serviceWorkerUrl)}` + (base ? ` (relative to ${base})` : " (no document to resolve it against)"), { cause: error });
6086
+ }
6087
+ }
6088
+ //#endregion
6089
+ //#region src/sw/sw-dispatcher.ts
6090
+ /**
6091
+ * Page-side counterpart: registers a ServiceWorker and sets up a
6092
+ * MessageChannel with it, re-establishing the connection after SW wake-ups.
6093
+ */
6094
+ var SwPortHandler = class {
6095
+ options;
6096
+ _serviceWorkerUrl;
6097
+ _serviceWorker;
6098
+ _registrationPromise;
6099
+ _registration;
6100
+ _cleanupRegistrations;
6101
+ constructor(options) {
6102
+ this.options = options;
6103
+ if (!this.key) throw new Error("Key is not defined.");
6104
+ if (!this.scope) throw new Error("Scope is not defined");
6105
+ }
6106
+ get key() {
6107
+ return this.options.key;
6108
+ }
6109
+ get scope() {
6110
+ return this.options.scope ?? new URL("./", this.serviceWorkerUrl).pathname;
6111
+ }
6112
+ /**
6113
+ * The scope as a url on the worker's origin. Resolved against the worker's
6114
+ * own url when one is given, not against this module's: a bundle that is
6115
+ * not an ES module (an IIFE, Theia's frontend) has no `import.meta.url`, and
6116
+ * `new URL(scope, undefined)` threw `Invalid URL` there. Only without a
6117
+ * worker url does the module's location decide (its default `sw-worker.js`
6118
+ * sits next to it).
6119
+ */
6120
+ get rootUrl() {
6121
+ const base = this.options.serviceWorkerUrl ? this.serviceWorkerUrl : import.meta.url;
6122
+ return new URL(this.scope, base);
6123
+ }
6124
+ get serviceWorkerUrl() {
6125
+ if (!this._serviceWorkerUrl) {
6126
+ const url = this.options.serviceWorkerUrl ? resolveWorkerUrl(this.options.serviceWorkerUrl) : new URL("./sw-worker.js", this.rootUrl);
6127
+ this._serviceWorkerUrl = `${url}`;
6128
+ }
6129
+ return this._serviceWorkerUrl;
6130
+ }
6131
+ async _newCommunicationChannel() {
6132
+ const messageChannel = new MessageChannel();
6133
+ return [messageChannel.port1, messageChannel.port2];
6134
+ }
6135
+ async _setCommunicationPort(port) {
6136
+ await this.options.bindPort(port);
6137
+ }
6138
+ async _updateCommunicationChannel() {
6139
+ if (!this._serviceWorker) return;
6140
+ const [port1, port2] = await this._newCommunicationChannel();
6141
+ await this._setCommunicationPort(port1);
6142
+ await callChannel(this._serviceWorker, "UPDATE_COMMUNICATION_PORT", this._getRegistrationInfo(), port2);
6143
+ }
6144
+ _getRegistrationInfo() {
6145
+ return { key: this.key };
6146
+ }
6147
+ /**
6148
+ * Registers the worker and waits until it is activated and controls this
6149
+ * page, then opens the port to it. The page must be controlled, because
6150
+ * only a controlled page's `fetch()` reaches the worker. When the page
6151
+ * loaded uncontrolled (a hard reload does that), the worker is asked to
6152
+ * claim it. Rejects with a `ServiceWorkerControlError` when that fails or
6153
+ * `timeout` passes (`reason` says which step); a later `start()` tries
6154
+ * again.
6155
+ */
6156
+ async start() {
6157
+ if (!this._registrationPromise) this._registrationPromise = (async () => {
6158
+ const [register, cleanup] = newRegistry();
6159
+ this._cleanupRegistrations = cleanup;
6160
+ register(() => {
6161
+ this._cleanupRegistrations = void 0;
6162
+ });
6163
+ const registration = await navigator.serviceWorker.register(this.serviceWorkerUrl, { scope: this.scope });
6164
+ this._registration = registration;
6165
+ const stopListening = handleChannelCalls(navigator.serviceWorker, "UPDATE_COMMUNICATION_PORT", async (_event, _params, port) => {
6166
+ await this._setCommunicationPort(port);
6167
+ return this._getRegistrationInfo();
6168
+ });
6169
+ register(stopListening);
6170
+ const timeout = this.options.timeout ?? 3e4;
6171
+ const deadline = Date.now() + timeout;
6172
+ try {
6173
+ this._serviceWorker = await awaitServiceWorkerControl(registration, {
6174
+ timeout,
6175
+ reloadIfUncontrolled: this.options.reloadIfUncontrolled
6176
+ });
6177
+ await withDeadline(deadline, this._updateCommunicationChannel(), () => new ServiceWorkerControlError("unresponsive", `ServiceWorker "${this._serviceWorker?.scriptURL}" controls this page but did not answer the UPDATE_COMMUNICATION_PORT handshake within ${timeout} ms. Check that the worker script runs this package's same-origin dispatcher (it importScripts \`sw-worker.js\`, or calls \`startHttpDispatcher\`).`));
6178
+ } catch (error) {
6179
+ stopListening();
6180
+ this._cleanupRegistrations = void 0;
6181
+ this._registration = void 0;
6182
+ this._registrationPromise = void 0;
6183
+ throw error;
6184
+ }
6185
+ })();
6186
+ return this._registrationPromise;
6187
+ }
6188
+ async stop() {
6189
+ this._cleanupRegistrations?.();
6190
+ const registration = this._registration;
6191
+ this._registration = void 0;
6192
+ try {
6193
+ await registration?.unregister();
6194
+ } catch (error) {
6195
+ console.log("Service Worker unregistration failed: ", error);
6196
+ }
6197
+ }
6198
+ };
6199
+ /**
6200
+ * ServiceWorker-side counterpart: maintains an index of connected clients keyed by
6201
+ * clientId, persisted in IndexedDB so it can reclaim ports across SW restarts.
6202
+ */
6203
+ var SwPortDispatcher = class {
6204
+ self;
6205
+ log;
6206
+ handlersIndex = /* @__PURE__ */ new Map();
6207
+ /**
6208
+ * Set of keys that were registered at some point on this SW (sticky —
6209
+ * survives client closures + SW wake-ups via IndexedDB). Lets the fetch
6210
+ * handler distinguish "site URL whose owner is gone → 404" from
6211
+ * "unknown URL under scope → pass through to the network".
6212
+ */
6213
+ claimedKeys = /* @__PURE__ */ new Set();
6214
+ _cleanup;
6215
+ _activationPromise;
6216
+ constructor({ self, log = () => {} }) {
6217
+ this.self = self;
6218
+ this.log = log;
6219
+ }
6220
+ get scope() {
6221
+ return `${new URL("./", this.self.location.href)}`;
6222
+ }
6223
+ /**
6224
+ * Look up the active channel for a key. Returns `undefined` when there
6225
+ * is no registration (or the registering client has gone away — stale
6226
+ * entries are pruned in place).
6227
+ */
6228
+ async loadChannelInfo(key) {
6229
+ await this._checkActivation();
6230
+ for (const channelInfo of this.handlersIndex.values()) {
6231
+ if (channelInfo.key !== key) continue;
6232
+ const clientId = channelInfo.clientId;
6233
+ if (!clientId) continue;
6234
+ if (!await this.self.clients.get(clientId)) {
6235
+ this.handlersIndex.delete(clientId);
6236
+ continue;
6237
+ }
6238
+ return channelInfo;
6239
+ }
6240
+ }
6241
+ /** Whether `key` has ever been registered on this SW. */
6242
+ isClaimedKey(key) {
6243
+ return this.claimedKeys.has(key);
6244
+ }
6245
+ start() {
6246
+ const stopClaims = handleClaimRequests(this.self);
6247
+ const stopPortUpdates = handleChannelCalls(this.self, "UPDATE_COMMUNICATION_PORT", async (event, channelInfo, port) => {
6248
+ const clientId = event.source?.id;
6249
+ this.log("[UPDATE_COMMUNICATION_PORT]", clientId, channelInfo);
6250
+ await this._updateChannelInfo({
6251
+ ...channelInfo,
6252
+ port,
6253
+ clientId
6254
+ });
6255
+ return { ...channelInfo };
6256
+ });
6257
+ this._cleanup = () => {
6258
+ stopClaims();
6259
+ stopPortUpdates();
6260
+ };
6261
+ this.self.addEventListener("install", (event) => {
6262
+ this.log("Skip waiting on install.", event);
6263
+ this.self.skipWaiting();
6264
+ });
6265
+ this.self.addEventListener("activate", (event) => {
6266
+ this.log("Claim control over all clients.", event);
6267
+ event.waitUntil((async () => {
6268
+ await this.self.clients.claim();
6269
+ await this._checkActivation();
6270
+ })());
6271
+ });
6272
+ }
6273
+ async stop() {
6274
+ if (this._cleanup) {
6275
+ this._cleanup();
6276
+ this._cleanup = void 0;
6277
+ }
6278
+ }
6279
+ _checkActivation() {
6280
+ if (!this._activationPromise) this._activationPromise = this._activate();
6281
+ return this._activationPromise;
6282
+ }
6283
+ async _activate() {
6284
+ this.log("[checkActivation]");
6285
+ const clientsIndex = await this._loadClientIds();
6286
+ for (const [clientId, client] of clientsIndex.entries()) {
6287
+ const messageChannel = new MessageChannel();
6288
+ const port = messageChannel.port1;
6289
+ const channelInfo = await callChannel(client, "UPDATE_COMMUNICATION_PORT", {}, messageChannel.port2);
6290
+ await this._updateChannelInfo({
6291
+ ...channelInfo,
6292
+ port,
6293
+ clientId
6294
+ });
6295
+ }
6296
+ }
6297
+ async _updateChannelInfo(channelInfo) {
6298
+ this.log("[updateClientInfo]", channelInfo);
6299
+ if (!channelInfo.clientId) return;
6300
+ this.handlersIndex.set(channelInfo.clientId, channelInfo);
6301
+ if (channelInfo.key && !this.claimedKeys.has(channelInfo.key)) {
6302
+ this.claimedKeys.add(channelInfo.key);
6303
+ await set("claimedKeys", [...this.claimedKeys].sort());
6304
+ }
6305
+ await this._updateClientIds([]);
6306
+ }
6307
+ async _loadClientIds() {
6308
+ const storedKeys = await get("claimedKeys") ?? [];
6309
+ for (const key of storedKeys) this.claimedKeys.add(key);
6310
+ const stored = await get("clientIds") ?? [];
6311
+ return await this._updateClientIds(new Set(stored));
6312
+ }
6313
+ async _updateClientIds(clientIds) {
6314
+ const ids = new Set(clientIds);
6315
+ for (const clientId of this.handlersIndex.keys()) ids.add(clientId);
6316
+ const index = /* @__PURE__ */ new Map();
6317
+ for (const clientId of ids) {
6318
+ const client = await this.self.clients.get(clientId);
6319
+ if (client) index.set(clientId, client);
6320
+ else this.handlersIndex.delete(clientId);
6321
+ }
6322
+ await set("clientIds", [...index.keys()].sort());
6323
+ return index;
6324
+ }
6325
+ };
6326
+ //#endregion
6327
+ //#region src/sw/sw-http-adapter.ts
6328
+ /**
6329
+ * The page side of the same-origin ServiceWorker: registers the worker, and
6330
+ * attaches handler links to it.
6331
+ *
6332
+ * - `register(prefix, handler)` serves a handler in this page, under
6333
+ * `<scope>/<key>/...`.
6334
+ * - `attachWorker(worker)` links a Dedicated Worker that runs
6335
+ * `serveHttpInWorker(handler)`: the ServiceWorker then talks to the worker
6336
+ * directly, and this page carries nothing.
6337
+ * - `connectWebSocket(url, protocols)` and `WebSocket` open sockets to
6338
+ * handlers, through the ServiceWorker or straight to a handler of this page.
6339
+ *
6340
+ * Links are attached again whenever the ServiceWorker asks (after a restart),
6341
+ * and requests in flight on a handler that dies fail at once.
6342
+ */
6343
+ var SwHttpAdapter = class {
6344
+ options;
6345
+ _handlers = /* @__PURE__ */ new Map();
6346
+ _endpoint;
6347
+ _ownerLock = newLockName("webrun-http-owner");
6348
+ _id;
6349
+ _owner = crypto.randomUUID();
6350
+ _workers = /* @__PURE__ */ new Set();
6351
+ _releaseOwner;
6352
+ _serviceWorkerUrl;
6353
+ _serviceWorker;
6354
+ _registration;
6355
+ _registrationPromise;
6356
+ _cleanup = [];
6357
+ _attaching;
6358
+ _routerLock;
6359
+ _WebSocket;
6360
+ constructor(options) {
6361
+ this.options = options;
6362
+ if (!this.key) throw new Error("Key is not defined.");
6363
+ if (!this.scope) throw new Error("Scope is not defined");
6364
+ this._id = options.id ?? `page:${crypto.randomUUID()}`;
6365
+ this._endpoint = new HandlerEndpoint((request) => this._handleHttpRequest(request), { streamTimeout: options.streamTimeout });
6366
+ }
6367
+ get key() {
6368
+ return this.options.key;
6369
+ }
6370
+ get scope() {
6371
+ return this.options.scope ?? new URL("./", this.serviceWorkerUrl).pathname;
6372
+ }
6373
+ /**
6374
+ * The scope as a url on the worker's origin. Resolved against the worker's
6375
+ * own url when one is given, not against this module's: a bundle that is
6376
+ * not an ES module (an IIFE, Theia's frontend) has no `import.meta.url`, and
6377
+ * `new URL(scope, undefined)` threw `Invalid URL` there. Only without a
6378
+ * worker url does the module's location decide (its default `sw-worker.js`
6379
+ * sits next to it).
6380
+ */
6381
+ get rootUrl() {
6382
+ const base = this.options.serviceWorkerUrl ? this.serviceWorkerUrl : import.meta.url;
6383
+ return new URL(this.scope, base);
6384
+ }
6385
+ get serviceWorkerUrl() {
6386
+ if (!this._serviceWorkerUrl) {
6387
+ const url = this.options.serviceWorkerUrl ? resolveWorkerUrl(this.options.serviceWorkerUrl) : new URL("./sw-worker.js", this.rootUrl);
6388
+ this._serviceWorkerUrl = `${url}`;
6389
+ }
6390
+ return this._serviceWorkerUrl;
6391
+ }
6392
+ /**
6393
+ * Registers the worker and waits until it is activated and controls this
6394
+ * page, then attaches this page's handler link. The page must be controlled,
6395
+ * because only a controlled page's `fetch()` reaches the worker. When the
6396
+ * page loaded uncontrolled (a hard reload does that), the worker is asked to
6397
+ * claim it. Rejects with a `ServiceWorkerControlError` when that fails or
6398
+ * `timeout` passes (`reason` says which step); a later `start()` tries
6399
+ * again.
6400
+ */
6401
+ async start() {
6402
+ if (!this._registrationPromise) this._registrationPromise = (async () => {
6403
+ const registration = await navigator.serviceWorker.register(this.serviceWorkerUrl, { scope: this.scope });
6404
+ this._registration = registration;
6405
+ const stopListening = handleChannelCalls(navigator.serviceWorker, NEED_LINKS_CALL, async (_event, params) => {
6406
+ await this.relink(params?.routerLock);
6407
+ return true;
6408
+ });
6409
+ const onControllerChange = () => {
6410
+ const controller = navigator.serviceWorker.controller;
6411
+ if (controller) this._serviceWorker = controller;
6412
+ };
6413
+ navigator.serviceWorker.addEventListener("controllerchange", onControllerChange);
6414
+ this._cleanup.push(stopListening, () => navigator.serviceWorker.removeEventListener("controllerchange", onControllerChange));
6415
+ const timeout = this.options.timeout ?? 3e4;
6416
+ const deadline = Date.now() + timeout;
6417
+ try {
6418
+ this._serviceWorker = await awaitServiceWorkerControl(registration, {
6419
+ timeout,
6420
+ reloadIfUncontrolled: this.options.reloadIfUncontrolled
6421
+ });
6422
+ this._releaseOwner ??= await holdLock(this._ownerLock);
6423
+ await withDeadline(deadline, this._attachPage(), () => new ServiceWorkerControlError("unresponsive", `ServiceWorker "${this._serviceWorker?.scriptURL}" controls this page but did not answer the UPDATE_COMMUNICATION_PORT handshake within ${timeout} ms. Check that the worker script runs this package's same-origin dispatcher (it importScripts \`sw-worker.js\`, or calls \`startHttpDispatcher\`).`));
6424
+ } catch (error) {
6425
+ for (const cleanup of this._cleanup.splice(0)) cleanup();
6426
+ this._registration = void 0;
6427
+ this._registrationPromise = void 0;
6428
+ throw error;
6429
+ }
6430
+ })();
6431
+ return this._registrationPromise;
6432
+ }
6433
+ /** Detaches every link, and unregisters the worker. */
6434
+ async stop() {
6435
+ for (const cleanup of this._cleanup.splice(0)) cleanup();
6436
+ for (const worker of [...this._workers]) await worker.detach().catch(() => {});
6437
+ const sw = this._serviceWorker;
6438
+ if (sw) await callChannel(sw, DETACH_CALL, {
6439
+ key: this.key,
6440
+ id: this._id,
6441
+ owner: this._owner
6442
+ }).catch(() => {});
6443
+ await this._endpoint.unlink();
6444
+ this._releaseOwner?.();
6445
+ this._releaseOwner = void 0;
6446
+ const registration = this._registration;
6447
+ this._registration = void 0;
6448
+ this._registrationPromise = void 0;
6449
+ try {
6450
+ await registration?.unregister();
6451
+ } catch (error) {
6452
+ console.log("Service Worker unregistration failed: ", error);
6453
+ }
6454
+ }
6455
+ /**
6456
+ * Registers an HTTP handler on a prefix. Returns the resulting base URL and a
6457
+ * disposer.
6458
+ */
6459
+ async register(prefix, handler) {
6460
+ const cleanPrefix = `./${(prefix ?? "").replace(/^[./]+/, "")}`;
6461
+ const baseUrl = `${new URL(cleanPrefix, this.rootUrl)}`;
6462
+ this._handlers.set(baseUrl, handler);
6463
+ const handlers = this._handlers;
6464
+ return {
6465
+ baseUrl,
6466
+ prefix: cleanPrefix,
6467
+ async remove() {
6468
+ handlers.delete(baseUrl);
6469
+ }
6470
+ };
6471
+ }
6472
+ /**
6473
+ * Links a Dedicated Worker that runs `serveHttpInWorker(handler)` as a
6474
+ * handler of `key`. The ServiceWorker sends it requests directly, without
6475
+ * this page relaying anything, so a busy or throttled page does not slow
6476
+ * them down.
6477
+ */
6478
+ async attachWorker(worker, options = {}) {
6479
+ await this.start();
6480
+ const controls = [];
6481
+ let retry;
6482
+ const { control, ownerLock } = await new Promise((resolve, reject) => {
6483
+ const timer = setTimeout(() => {
6484
+ clearInterval(retry);
6485
+ for (const port of controls) port.close();
6486
+ reject(/* @__PURE__ */ new Error("The worker did not answer within the timeout. Does it call serveHttpInWorker()?"));
6487
+ }, this.options.timeout ?? 3e4);
6488
+ const hello = () => {
6489
+ const channel = new MessageChannel();
6490
+ controls.push(channel.port1);
6491
+ channel.port1.onmessage = (event) => {
6492
+ const message = event.data;
6493
+ if (message?.type !== "ready") return;
6494
+ clearTimeout(timer);
6495
+ clearInterval(retry);
6496
+ for (const port of controls) if (port !== channel.port1) port.close();
6497
+ resolve({
6498
+ control: channel.port1,
6499
+ ownerLock: message.ownerLock
6500
+ });
6501
+ };
6502
+ worker.postMessage({ type: WORKER_HELLO }, [channel.port2]);
6503
+ };
6504
+ hello();
6505
+ retry = setInterval(hello, 500);
6506
+ });
6507
+ const key = options.key ?? this.key;
6508
+ const id = options.id ?? `worker:${crypto.randomUUID()}`;
6509
+ for (const old of [...this._workers]) if (old.key === key && old.id === id) old.retire();
6510
+ const link = new WorkerLinkImpl(this, control, {
6511
+ key,
6512
+ id,
6513
+ owner: crypto.randomUUID(),
6514
+ meta: options.meta,
6515
+ ownerLock
6516
+ });
6517
+ this._workers.add(link);
6518
+ await link.attach();
6519
+ return link;
6520
+ }
6521
+ /**
6522
+ * Opens the transport of a socket to the handler `url` routes to: a handler
6523
+ * of this page directly, any other through the ServiceWorker. Resolves once
6524
+ * the handler accepted. An arrow function, so it can be handed around:
6525
+ * `createWebSocketClass(adapter.connectWebSocket)`.
6526
+ */
6527
+ connectWebSocket = async (url, protocols = []) => {
6528
+ const httpUrl = new URL(`${url}`, globalThis.location?.href);
6529
+ if (httpUrl.protocol === "ws:") httpUrl.protocol = "http:";
6530
+ else if (httpUrl.protocol === "wss:") httpUrl.protocol = "https:";
6531
+ const local = this._findHandler(`${httpUrl}`);
6532
+ if (local) {
6533
+ const result = await serveWebSocketUpgrade(local, {
6534
+ url: `${httpUrl}`,
6535
+ protocols
6536
+ });
6537
+ if (!result.ok) throw new Error(result.message);
6538
+ return {
6539
+ port: result.port,
6540
+ protocol: result.protocol
6541
+ };
6542
+ }
6543
+ await this.start();
6544
+ return await callChannelWithin(this._callTimeout(), this._worker(), WS_CONNECT_CALL, {
6545
+ url: `${httpUrl}`,
6546
+ protocols
6547
+ });
6548
+ };
6549
+ /**
6550
+ * A WHATWG `WebSocket` class whose sockets reach this adapter's handlers:
6551
+ * `new adapter.WebSocket(`${baseUrl}chat`, ["v1"])`. The global
6552
+ * `WebSocket` is left alone.
6553
+ */
6554
+ get WebSocket() {
6555
+ this._WebSocket ??= createWebSocketClass(this.connectWebSocket, this.options.webSocket);
6556
+ return this._WebSocket;
6557
+ }
6558
+ /**
6559
+ * Attaches again every link not made for the router holding
6560
+ * `currentRouterLock` (every link, without it). The ServiceWorker asks for
6561
+ * this after a restart; a page may call it too.
6562
+ */
6563
+ async relink(currentRouterLock) {
6564
+ if (!this._registrationPromise) return;
6565
+ const stale = (routerLock) => currentRouterLock === void 0 || routerLock !== currentRouterLock;
6566
+ const jobs = [];
6567
+ if (stale(this._routerLock)) jobs.push(this._attachPage());
6568
+ for (const worker of this._workers) if (stale(worker.routerLock)) jobs.push(worker.attach());
6569
+ await Promise.allSettled(jobs);
6570
+ }
6571
+ /** @internal Posts a plain message to the ServiceWorker, which starts it if stopped. */
6572
+ _poke(type) {
6573
+ try {
6574
+ this._worker().postMessage({ type });
6575
+ } catch {}
6576
+ }
6577
+ /** @internal */
6578
+ _worker() {
6579
+ const worker = navigator.serviceWorker.controller ?? this._serviceWorker ?? this._registration?.active;
6580
+ if (!worker) throw new Error("The ServiceWorker is not running; call start() first");
6581
+ return worker;
6582
+ }
6583
+ /** @internal */
6584
+ _forget(link) {
6585
+ this._workers.delete(link);
6586
+ }
6587
+ /** @internal How long a call to the ServiceWorker may take. */
6588
+ _callTimeout() {
6589
+ return this.options.timeout ?? 3e4;
6590
+ }
6591
+ _attachPage() {
6592
+ this._attaching ??= (async () => {
6593
+ try {
6594
+ const down = new MessageChannel();
6595
+ const ctl = new MessageChannel();
6596
+ const params = {
6597
+ wire: HTTP_LINK_PROTOCOL,
6598
+ key: this.key,
6599
+ id: this._id,
6600
+ owner: this._owner,
6601
+ ownerLock: this._releaseOwner && locksAvailable() ? this._ownerLock : void 0,
6602
+ meta: this.options.meta,
6603
+ ctl: true
6604
+ };
6605
+ const reply = await callChannelWithin(this._callTimeout(), this._worker(), ATTACH_CALL, params, down.port1, ctl.port1);
6606
+ checkReply(reply, this._worker());
6607
+ await this._endpoint.link(this.key, {
6608
+ down: down.port2,
6609
+ ctl: ctl.port2
6610
+ }, reply.routerLock);
6611
+ this._routerLock = reply.routerLock;
6612
+ } finally {
6613
+ this._attaching = void 0;
6614
+ }
6615
+ })();
6616
+ return this._attaching;
6617
+ }
6618
+ _findHandler(url) {
6619
+ for (const [urlPrefix, handler] of this._handlers) if (url.indexOf(urlPrefix) === 0) return handler;
6620
+ }
6621
+ async _handleHttpRequest(request) {
6622
+ const handler = this._findHandler(request.url);
6623
+ if (handler) return await handler(request);
6624
+ return new Response(null, {
6625
+ status: 404,
6626
+ statusText: "Error 404: Not found"
6627
+ });
6628
+ }
6629
+ };
6630
+ var WorkerLinkImpl = class {
6631
+ _adapter;
6632
+ _control;
6633
+ key;
6634
+ id;
6635
+ owner;
6636
+ routerLock;
6637
+ _meta;
6638
+ _ownerLock;
6639
+ _attaching;
6640
+ _detached = false;
6641
+ constructor(_adapter, _control, init) {
6642
+ this._adapter = _adapter;
6643
+ this._control = _control;
6644
+ this.key = init.key;
6645
+ this.id = init.id;
6646
+ this.owner = init.owner;
6647
+ this._meta = init.meta;
6648
+ this._ownerLock = init.ownerLock;
6649
+ _control.onmessage = (event) => {
6650
+ const message = event.data;
6651
+ if (message?.type === "relink") {
6652
+ this._adapter._poke(WAKE_MESSAGE);
6653
+ this.attach().catch(() => {});
6654
+ } else if (message?.type === "keepalive") this._adapter._poke(KEEPALIVE_MESSAGE);
6655
+ };
6656
+ }
6657
+ /**
6658
+ * Forgets this link without telling the ServiceWorker: another attachment
6659
+ * took its slot, and the ServiceWorker retires it by itself.
6660
+ */
6661
+ retire() {
6662
+ this._detached = true;
6663
+ this._adapter._forget(this);
6664
+ this._control.onmessage = null;
6665
+ }
6666
+ /** Makes fresh channels and attaches them; the worker gets its ends. */
6667
+ attach() {
6668
+ if (this._detached) return Promise.resolve();
6669
+ this._attaching ??= (async () => {
6670
+ try {
6671
+ if (this._ownerLock) {
6672
+ const held = await heldLockNames();
6673
+ if (held && !held.has(this._ownerLock)) {
6674
+ this.retire();
6675
+ return;
6676
+ }
6677
+ }
6678
+ const down = new MessageChannel();
6679
+ const ctl = new MessageChannel();
6680
+ const up = new MessageChannel();
6681
+ const params = {
6682
+ wire: HTTP_LINK_PROTOCOL,
6683
+ key: this.key,
6684
+ id: this.id,
6685
+ owner: this.owner,
6686
+ ownerLock: this._ownerLock,
6687
+ meta: this._meta,
6688
+ ctl: true,
6689
+ up: true
6690
+ };
6691
+ const sw = this._adapter._worker();
6692
+ const reply = await callChannelWithin(this._adapter._callTimeout(), sw, ATTACH_CALL, params, down.port1, ctl.port1, up.port1);
6693
+ checkReply(reply, sw);
6694
+ this.routerLock = reply.routerLock;
6695
+ this._control.postMessage({
6696
+ type: "link",
6697
+ key: this.key,
6698
+ routerLock: reply.routerLock
6699
+ }, [
6700
+ down.port2,
6701
+ ctl.port2,
6702
+ up.port2
6703
+ ]);
6704
+ } finally {
6705
+ this._attaching = void 0;
6706
+ }
6707
+ })();
6708
+ return this._attaching;
6709
+ }
6710
+ async drain({ ms } = {}) {
6711
+ if (this._detached) return;
6712
+ this._detached = true;
6713
+ this._adapter._forget(this);
6714
+ try {
6715
+ await callChannel(this._adapter._worker(), DRAIN_CALL, {
6716
+ key: this.key,
6717
+ id: this.id,
6718
+ owner: this.owner,
6719
+ ms
6720
+ });
6721
+ } finally {
6722
+ this._unlink();
6723
+ }
6724
+ }
6725
+ async detach() {
6726
+ if (this._detached) return;
6727
+ this._detached = true;
6728
+ this._adapter._forget(this);
6729
+ await callChannel(this._adapter._worker(), DETACH_CALL, {
6730
+ key: this.key,
6731
+ id: this.id,
6732
+ owner: this.owner
6733
+ }).catch(() => {});
6734
+ this._unlink();
6735
+ }
6736
+ _unlink() {
6737
+ this._control.postMessage({
6738
+ type: "unlink",
6739
+ key: this.key
6740
+ });
6741
+ this._control.onmessage = null;
6742
+ }
6743
+ };
6744
+ function checkReply(reply, sw) {
6745
+ if (reply?.accepted === "webrun-http-link/2") return;
6746
+ throw new ServiceWorkerControlError("incompatible", `ServiceWorker "${sw.scriptURL}" answered the handshake but does not speak ${HTTP_LINK_PROTOCOL}: it runs an older version of @statewalker/webrun-http-browser. Reload the page once the updated worker is active.`);
6747
+ }
6748
+ //#endregion
6749
+ //#region src/sw/sw-websocket.ts
6750
+ /**
6751
+ * Opens the transport of a socket through the ServiceWorker controlling this
6752
+ * page, for a page that has no adapter of its own: content a handler serves
6753
+ * under the same-origin dispatcher, or under a relay. The ServiceWorker
6754
+ * routes `url` like a fetch, and the handler accepts or refuses the upgrade.
6755
+ */
6756
+ async function connectWebSocketThroughServiceWorker(url, protocols = [], container = navigator.serviceWorker) {
6757
+ const worker = container.controller;
6758
+ if (!worker) throw new Error("This page is not controlled by a ServiceWorker");
6759
+ const httpUrl = new URL(`${url}`, globalThis.location?.href);
6760
+ if (httpUrl.protocol === "ws:") httpUrl.protocol = "http:";
6761
+ else if (httpUrl.protocol === "wss:") httpUrl.protocol = "https:";
6762
+ return await callChannel(worker, WS_CONNECT_CALL, {
6763
+ url: `${httpUrl}`,
6764
+ protocols
6765
+ });
6766
+ }
6767
+ /**
6768
+ * A WHATWG `WebSocket` class over `connectWebSocketThroughServiceWorker`.
6769
+ * Install it where the page's code expects `WebSocket`, e.g.
6770
+ * `globalThis.WebSocket = serviceWorkerWebSocketClass()` in content that only
6771
+ * talks to its own handlers.
6772
+ */
6773
+ function serviceWorkerWebSocketClass(options) {
6774
+ return createWebSocketClass((url, protocols) => connectWebSocketThroughServiceWorker(url, protocols), options);
6775
+ }
6776
+ //#endregion
6777
+ //#region src/sw/worker-service.ts
6778
+ /**
6779
+ * Serves `handler` from a Dedicated Worker, on the links the page attaches
6780
+ * with `adapter.attachWorker(worker)`:
6781
+ *
6782
+ * ```ts
6783
+ * // worker.ts
6784
+ * const service = serveHttpInWorker(async (request) => new Response("hi"));
6785
+ * ```
6786
+ *
6787
+ * The worker holds an owner lock while it lives, so the ServiceWorker notices
6788
+ * at once when it is terminated. It watches the ServiceWorker's router lock,
6789
+ * and asks its page to attach it again when its links died and a call waits.
6790
+ * While calls through the router are in flight it asks its page every 20 s to
6791
+ * keep the ServiceWorker awake — only a message to the ServiceWorker object
6792
+ * counts as an event, and a worker cannot post one.
6793
+ */
6794
+ function serveHttpInWorker(handler, options = {}) {
6795
+ const scope = options.scope ?? globalThis;
6796
+ const ownerLock = newLockName("webrun-http-owner");
6797
+ const held = holdLock(ownerLock);
6798
+ const controls = /* @__PURE__ */ new Set();
6799
+ const tellPages = (message) => {
6800
+ for (const control of controls) control.postMessage(message);
6801
+ };
6802
+ const endpoint = new HandlerEndpoint(handler, {
6803
+ ...options,
6804
+ requestRelink: () => tellPages({ type: "relink" }),
6805
+ keepAlive: () => tellPages({ type: "keepalive" })
6806
+ });
6807
+ const onMessage = (event) => {
6808
+ const { data, ports } = event;
6809
+ if (data?.type !== "webrun-http:worker-hello") return;
6810
+ const control = ports?.[0];
6811
+ if (!control) return;
6812
+ controls.add(control);
6813
+ control.onmessage = (message) => {
6814
+ const msg = message.data;
6815
+ if (msg?.type === "link") {
6816
+ const [down, ctl, up] = message.ports;
6817
+ if (down) endpoint.link(msg.key, {
6818
+ down,
6819
+ ctl,
6820
+ up
6821
+ }, msg.routerLock);
6822
+ } else if (msg?.type === "unlink") endpoint.unlink(msg.key);
6823
+ };
6824
+ held.then(() => control.postMessage({
6825
+ type: "ready",
6826
+ ownerLock: locksAvailable() ? ownerLock : void 0
6827
+ }));
6828
+ };
6829
+ scope.addEventListener("message", onMessage);
6830
+ const baseUrl = () => globalThis.location?.href;
6831
+ const connectWebSocket = (url, protocols = []) => {
6832
+ const httpUrl = new URL(`${url}`, baseUrl());
6833
+ if (httpUrl.protocol === "ws:") httpUrl.protocol = "http:";
6834
+ else if (httpUrl.protocol === "wss:") httpUrl.protocol = "https:";
6835
+ return endpoint.connectWebSocket(`${httpUrl}`, protocols);
6836
+ };
6837
+ let WebSocketClass;
6838
+ return {
6839
+ fetch(input, init) {
6840
+ const request = input instanceof Request && init === void 0 ? input : new Request(input instanceof Request ? input : new URL(`${input}`, baseUrl()), init);
6841
+ return endpoint.fetch(request);
6842
+ },
6843
+ connectWebSocket,
6844
+ get WebSocket() {
6845
+ WebSocketClass ??= createWebSocketClass(connectWebSocket, options.webSocket);
6846
+ return WebSocketClass;
6847
+ },
6848
+ get linked() {
6849
+ return endpoint.linked;
6850
+ },
6851
+ async close() {
6852
+ scope.removeEventListener("message", onMessage);
6853
+ await endpoint.close();
6854
+ for (const control of controls) control.close();
6855
+ controls.clear();
6856
+ (await held)();
6857
+ }
2938
6858
  };
2939
- target.addEventListener("message", listener);
2940
- target.start?.();
2941
- return () => target.removeEventListener("message", listener);
2942
6859
  }
2943
6860
  //#endregion
2944
- export { CLAIM_CALL, DEFAULT_SERVICE_WORKER_TIMEOUT, DuplexSiteBuilder, HttpError, HttpParseError, PEER_ERROR_HEADER, ServiceWorkerControlError, TransportClosedError, awaitActiveServiceWorker, awaitServiceWorkerControl, callChannel, callHttpService, collect, collectBytes, collectString, decodeJsonl, decodeMessage, decodeText, defaultCodec, deserializeError, emulateMux, encodeJsonl, encodeMessage, encodeText, fetchOverDuplex, fromReadableStream, getRelayWindowMessageHandler, handleChannelCalls, handleClaimRequests, handleHttpRequests, handleStreams, httpCodec, httpFetch, httpServe, initHttpService, initServiceWorker, initializeConnection, joinLines, jsonEnvelopeCodec, map, newAsyncGenerator, newCreditGrantor, newCreditLedger, newHttpClientStub, newHttpCodec, newHttpServerStub, newInvokationChannel, newRegistry, newRemoteRelayChannel, newServiceWorkerPort, newSniffingCodec, normalizeToUint8Array, recieveIterator, registerConnectionsHandler, sendHttpRequest, sendIterator, sendStream, serializeError, serveFetchOverDuplex, splitLines, splitServiceUrl, toChunks, toReadableStream };
6861
+ export { CLAIM_CALL, DEFAULT_CLOSE_TIMEOUT_MS, DEFAULT_HEARTBEAT_MS, DEFAULT_SERVICE_WORKER_TIMEOUT, DuplexSiteBuilder, HANDLER_LOST_MESSAGE, HTTP_LINK_PROTOCOL, HandlerEndpoint, HandlerTable, HttpError, HttpParseError, LinkLostError, PEER_ERROR_HEADER, PortRendezvous, ROUTER_LOST_MESSAGE, ServerWebSocket, ServiceWorkerControlError, SwHttpAdapter, SwHttpDispatcher, SwPortDispatcher, SwPortHandler, TransportClosedError, WS_PORT_PROTOCOL_VERSION, WS_VERSION_HEADER, WsCloseEvent, answerConnect, awaitActiveServiceWorker, awaitServiceWorkerControl, callChannel, callChannelWithin, callHttpService, callWebSocketService, collect, collectBytes, collectString, connectHttpLink, connectWebSocketThroughServiceWorker, createWebSocketClass, decodeJsonl, decodeMessage, decodeText, defaultCodec, deserializeError, emulateMux, encodeJsonl, encodeMessage, encodeText, fetchOverDuplex, fromReadableStream, getRelayWindowMessageHandler, handleChannelCalls, handleClaimRequests, handleHttpRequests, handleStreams, holdLock, httpCodec, httpFetch, httpServe, initHttpService, initServiceWorker, initializeConnection, isWebSocketUpgrade, joinLines, jsonEnvelopeCodec, leastInflightRoute, locksAvailable, map, newAsyncGenerator, newCreditGrantor, newCreditLedger, newHttpClientStub, newHttpCodec, newHttpServerStub, newInvokationChannel, newLockName, newRegistry, newRemoteRelayChannel, newServiceWorkerPort, newSniffingCodec, newWebSocketUpgradeRequest, normalizeToUint8Array, recieveIterator, registerConnectionsHandler, relayWebSocketClass, requestedProtocols, sendHttpRequest, sendIterator, sendOverConnection, sendStream, serializeError, serveFetchOverDuplex, serveHttpInWorker, serveHttpLink, serveWebSocketUpgrade, serviceWorkerWebSocketClass, splitLines, splitServiceUrl, startHttpDispatcher, takePort, takeUpgrade, toChunks, toReadableStream, trackedLink, unwrapTransfer, upgradeWebSocket, watchLock, withHeaders, withTransfer };