@statewalker/webrun-http-browser 0.4.2 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1125,6 +1125,192 @@ function newRegistry(onError = console.error) {
1125
1125
  });
1126
1126
  }
1127
1127
  //#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
+ //#region src/core/service-worker-control.ts
1152
+ /**
1153
+ * How long, by default, the page waits for its ServiceWorker to activate and
1154
+ * to take control before giving up. Generous: a first install downloads and
1155
+ * evaluates the worker script, which on a slow link takes seconds.
1156
+ */
1157
+ const DEFAULT_SERVICE_WORKER_TIMEOUT = 3e4;
1158
+ /**
1159
+ * How long the page waits for `controllerchange` once the worker has
1160
+ * answered the `CLAIM` request. `clients.claim()` resolves only after the
1161
+ * browser has queued that event, so this is a grace period for delivery, not
1162
+ * a second budget.
1163
+ */
1164
+ const CLAIM_GRACE_MS = 1e3;
1165
+ /** Channel call a page sends to ask its ServiceWorker to `clients.claim()` it. */
1166
+ const CLAIM_CALL = "CLAIM";
1167
+ /**
1168
+ * Why a page could not get a working ServiceWorker. `reason` says which wait
1169
+ * failed; check it (or `name`) rather than `instanceof`, because each of this
1170
+ * package's bundles carries its own copy of this class.
1171
+ */
1172
+ var ServiceWorkerControlError = class extends Error {
1173
+ reason;
1174
+ constructor(reason, message) {
1175
+ super(message);
1176
+ this.name = "ServiceWorkerControlError";
1177
+ this.reason = reason;
1178
+ }
1179
+ };
1180
+ /**
1181
+ * Resolves with the registration's worker once it is `activated`. Rejects
1182
+ * with a `ServiceWorkerControlError` (`reason: "activation-timeout"`) if that
1183
+ * takes longer than `timeout` — an install that throws or never finishes
1184
+ * would otherwise leave the caller waiting forever.
1185
+ */
1186
+ async function awaitActiveServiceWorker(registration, { timeout = DEFAULT_SERVICE_WORKER_TIMEOUT } = {}) {
1187
+ return await withDeadline(Date.now() + timeout, waitForActivated(registration), () => {
1188
+ const worker = registration.installing ?? registration.waiting ?? registration.active;
1189
+ return new ServiceWorkerControlError("activation-timeout", `ServiceWorker ${scriptUrl(worker)} (scope ${registration.scope}) did not activate within ${timeout} ms` + (worker ? `; it is "${worker.state}"` : "") + ". Check the worker script for errors during install (DevTools → Application → Service Workers), or raise the `timeout` option.");
1190
+ });
1191
+ }
1192
+ /**
1193
+ * Resolves with the ServiceWorker that controls this page, once `registration`
1194
+ * has an activated worker and the page is controlled by it.
1195
+ *
1196
+ * A page can stay uncontrolled while its worker is active, and then no
1197
+ * `controllerchange` ever fires on its own:
1198
+ * - a hard reload (Ctrl+Shift+R) bypasses the worker for that load, and the
1199
+ * worker's `clients.claim()` already ran when it activated;
1200
+ * - Firefox can leave a page loaded while the worker is running uncontrolled.
1201
+ *
1202
+ * So when the page is uncontrolled, this asks the active worker to claim it
1203
+ * again (a `CLAIM` channel call — this package's workers answer it) and waits
1204
+ * for `controllerchange`. Every wait is bounded by `timeout`. If control never
1205
+ * comes it reloads once (`reloadIfUncontrolled`) or rejects with a
1206
+ * `ServiceWorkerControlError` (`reason: "uncontrolled"`) that says what
1207
+ * happened and what to do.
1208
+ */
1209
+ async function awaitServiceWorkerControl(registration, { timeout = DEFAULT_SERVICE_WORKER_TIMEOUT, reloadIfUncontrolled = false, container = navigator.serviceWorker } = {}) {
1210
+ const deadline = Date.now() + timeout;
1211
+ const controlled = waitForController(container);
1212
+ try {
1213
+ const active = await awaitActiveServiceWorker(registration, { timeout });
1214
+ let controller = container.controller;
1215
+ if (!controller) controller = await withDeadline(deadline, (async () => {
1216
+ await Promise.race([callChannel(active, CLAIM_CALL, {}), controlled.promise]);
1217
+ return await Promise.race([controlled.promise, delay(CLAIM_GRACE_MS, null)]);
1218
+ })(), () => null);
1219
+ if (controller) {
1220
+ forgetReload(registration);
1221
+ return controller;
1222
+ }
1223
+ if (reloadIfUncontrolled && markReload(registration)) {
1224
+ location.reload();
1225
+ return await new Promise(() => {});
1226
+ }
1227
+ forgetReload(registration);
1228
+ throw new ServiceWorkerControlError("uncontrolled", `This page is not controlled by its ServiceWorker ${scriptUrl(active)} (scope ${registration.scope}), although the worker is active, and the worker did not take control when asked (clients.claim()) within ${timeout} ms. This happens after a hard reload (Ctrl+Shift+R / Cmd+Shift+R), which bypasses ServiceWorkers for that load, and in Firefox for some pages opened while the worker was already running. Requests from this page would not reach the worker. Reload the page normally, pass \`reloadIfUncontrolled: true\` to do that automatically, check that the page is inside the worker's scope, and that the worker answers the "${CLAIM_CALL}" request (this package's workers do).`);
1229
+ } finally {
1230
+ controlled.cancel();
1231
+ }
1232
+ }
1233
+ /**
1234
+ * ServiceWorker side: answers the page's `CLAIM` request with
1235
+ * `clients.claim()`, which takes over every uncontrolled client in scope.
1236
+ * Returns a function that stops answering.
1237
+ */
1238
+ function handleClaimRequests(self) {
1239
+ return handleChannelCalls(self, CLAIM_CALL, (event) => {
1240
+ const claimed = self.clients.claim().then(() => true);
1241
+ event.waitUntil?.(claimed);
1242
+ return claimed;
1243
+ });
1244
+ }
1245
+ function waitForActivated(registration) {
1246
+ return new Promise((resolve) => {
1247
+ const watched = /* @__PURE__ */ new Set();
1248
+ const check = () => {
1249
+ const active = registration.active;
1250
+ if (active?.state === "activated") {
1251
+ registration.removeEventListener("updatefound", watch);
1252
+ for (const worker of watched) worker.removeEventListener("statechange", check);
1253
+ resolve(active);
1254
+ return;
1255
+ }
1256
+ watch();
1257
+ };
1258
+ function watch() {
1259
+ for (const worker of [
1260
+ registration.installing,
1261
+ registration.waiting,
1262
+ registration.active
1263
+ ]) {
1264
+ if (!worker || watched.has(worker)) continue;
1265
+ watched.add(worker);
1266
+ worker.addEventListener("statechange", check);
1267
+ }
1268
+ }
1269
+ registration.addEventListener("updatefound", watch);
1270
+ check();
1271
+ });
1272
+ }
1273
+ function waitForController(container) {
1274
+ let cancel = () => {};
1275
+ return {
1276
+ promise: new Promise((resolve) => {
1277
+ const onChange = () => {
1278
+ if (!container.controller) return;
1279
+ cancel();
1280
+ resolve(container.controller);
1281
+ };
1282
+ cancel = () => container.removeEventListener("controllerchange", onChange);
1283
+ container.addEventListener("controllerchange", onChange);
1284
+ }),
1285
+ cancel
1286
+ };
1287
+ }
1288
+ function delay(ms, value) {
1289
+ return new Promise((resolve) => setTimeout(() => resolve(value), ms));
1290
+ }
1291
+ function scriptUrl(worker) {
1292
+ return worker?.scriptURL ? `"${worker.scriptURL}"` : "(no worker)";
1293
+ }
1294
+ function reloadKey(registration) {
1295
+ return `webrun-http-browser:reloaded-uncontrolled:${registration.scope}`;
1296
+ }
1297
+ /** Records the reload about to happen. `false` if one already happened, or if it cannot be recorded. */
1298
+ function markReload(registration) {
1299
+ try {
1300
+ const key = reloadKey(registration);
1301
+ if (sessionStorage.getItem(key)) return false;
1302
+ sessionStorage.setItem(key, "1");
1303
+ return true;
1304
+ } catch {
1305
+ return false;
1306
+ }
1307
+ }
1308
+ function forgetReload(registration) {
1309
+ try {
1310
+ sessionStorage.removeItem(reloadKey(registration));
1311
+ } catch {}
1312
+ }
1313
+ //#endregion
1128
1314
  //#region ../webrun-http-streams/dist/index.js
1129
1315
  const CR = 13;
1130
1316
  const LF = 10;
@@ -2474,37 +2660,78 @@ async function sendHttpRequest(communicationPort, request) {
2474
2660
  */
2475
2661
  function splitServiceUrl(url, separator = "~") {
2476
2662
  const str = `${url}`;
2477
- const idx = str.indexOf(separator);
2478
- let baseUrl = "";
2479
- let key = "";
2480
- let path = "";
2481
- if (idx >= 0) {
2482
- baseUrl = str.substring(0, idx + separator.length);
2483
- str.substring(idx + separator.length).replace(/^([^/]+)/, (match, $1) => {
2484
- baseUrl += match;
2485
- if (baseUrl.length < str.length) baseUrl += "/";
2486
- key = $1;
2487
- path = str.substring(baseUrl.length);
2488
- return "";
2489
- });
2490
- }
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;
2491
2706
  return {
2492
2707
  url: str,
2493
2708
  key,
2494
- baseUrl,
2495
- path
2709
+ baseUrl: `${prefix === "" ? rooted ? "/" : "" : `${prefix}/`}${separator}${key}${slash < 0 ? "" : "/"}`,
2710
+ path: slash < 0 ? "" : rest.slice(slash + 1)
2496
2711
  };
2497
2712
  }
2498
2713
  //#endregion
2499
2714
  //#region src/relay/index.ts
2500
2715
  /**
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
+ /**
2501
2726
  * Returns a MessagePort that transparently bridges messages to/from the
2502
- * ServiceWorker controlling this page.
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.
2503
2730
  */
2504
- function newServiceWorkerPort() {
2731
+ function newServiceWorkerPort(registration) {
2505
2732
  const channel = new MessageChannel();
2506
2733
  channel.port1.onmessage = (event) => {
2507
- navigator.serviceWorker.controller?.postMessage(event.data, [...event.ports]);
2734
+ (navigator.serviceWorker.controller ?? registration?.active)?.postMessage(event.data, [...event.ports]);
2508
2735
  };
2509
2736
  navigator.serviceWorker.addEventListener("message", (event) => {
2510
2737
  channel.port1.postMessage(event.data, [...event.ports]);
@@ -2512,53 +2739,35 @@ function newServiceWorkerPort() {
2512
2739
  return channel.port2;
2513
2740
  }
2514
2741
  /**
2515
- * Registers a ServiceWorker and resolves once it's activated and controlling the page.
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`.
2516
2748
  */
2517
- async function initServiceWorker({ swUrl, scopeUrl, type }) {
2518
- await navigator.serviceWorker.register(swUrl, {
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, {
2519
2754
  type,
2520
2755
  scope: scopeUrl
2521
2756
  });
2522
- const worker = await getServiceWorkerController();
2523
- await awaitServiceWorkerActivation(worker);
2524
- return worker;
2525
- }
2526
- function getServiceWorkerController() {
2527
- return new Promise((resolve) => {
2528
- const container = navigator.serviceWorker;
2529
- if (container.controller) {
2530
- resolve(container.controller);
2531
- return;
2532
- }
2533
- const onChange = () => {
2534
- if (!container.controller) return;
2535
- resolve(container.controller);
2536
- container.removeEventListener("controllerchange", onChange);
2537
- };
2538
- container.addEventListener("controllerchange", onChange);
2539
- });
2540
- }
2541
- function awaitServiceWorkerActivation(worker) {
2542
- return new Promise((resolve) => {
2543
- if (worker.state === "activated") {
2544
- resolve();
2545
- return;
2546
- }
2547
- const onStateChange = () => {
2548
- if (worker.state !== "activated") return;
2549
- worker.removeEventListener("statechange", onStateChange);
2550
- resolve();
2551
- };
2552
- worker.addEventListener("statechange", onStateChange);
2553
- });
2757
+ const active = await awaitActiveServiceWorker(registration, { timeout });
2758
+ return {
2759
+ registration,
2760
+ worker: navigator.serviceWorker.controller ?? active
2761
+ };
2554
2762
  }
2555
2763
  /**
2556
2764
  * Registers `handler` as the server for the given service `key` on the relay.
2557
2765
  * Returns a cleanup function that unregisters the service.
2558
2766
  */
2559
- async function initHttpService(handler, { key, port }) {
2767
+ async function initHttpService(handler, { key, path, port }) {
2560
2768
  return await registerConnectionsHandler({
2561
2769
  key,
2770
+ path,
2562
2771
  communicationPort: port,
2563
2772
  handler: async (_event, _data, callPort) => {
2564
2773
  handleHttpRequests(callPort, handler);
@@ -2581,9 +2790,11 @@ async function callHttpService(request, { key, port }) {
2581
2790
  /**
2582
2791
  * Returns a `window.onmessage` handler for use inside the relay iframe:
2583
2792
  * it accepts a CONNECT message, starts the relay ServiceWorker, and bridges
2584
- * the parent's MessagePort with the SW.
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.
2585
2796
  */
2586
- function getRelayWindowMessageHandler({ swUrl = `${new URL("./index-sw.js", import.meta.url)}`, scopeUrl = `${new URL("../", import.meta.url)}` } = {}) {
2797
+ function getRelayWindowMessageHandler({ swUrl = `${new URL("./index-sw.js", moduleUrl)}`, scopeUrl = `${new URL("../", moduleUrl)}`, timeout } = {}) {
2587
2798
  let externalPort;
2588
2799
  return async (ev) => {
2589
2800
  if (ev.data?.type !== "CONNECT") return;
@@ -2594,11 +2805,19 @@ function getRelayWindowMessageHandler({ swUrl = `${new URL("./index-sw.js", impo
2594
2805
  return;
2595
2806
  }
2596
2807
  externalPort = newExternalPort;
2597
- await initServiceWorker({
2598
- swUrl,
2599
- scopeUrl
2600
- });
2601
- const serviceWorkerPort = newServiceWorkerPort();
2808
+ let registration;
2809
+ 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 });
2818
+ throw error;
2819
+ }
2820
+ const serviceWorkerPort = newServiceWorkerPort(registration);
2602
2821
  serviceWorkerPort.onmessage = (event) => {
2603
2822
  externalPort?.postMessage(event.data, [...event.ports]);
2604
2823
  };
@@ -2611,7 +2830,7 @@ function getRelayWindowMessageHandler({ swUrl = `${new URL("./index-sw.js", impo
2611
2830
  * Embeds a hidden relay iframe, establishes a MessageChannel with it, and
2612
2831
  * returns the port to be used with `initHttpService` / `callHttpService`.
2613
2832
  */
2614
- async function newRemoteRelayChannel({ baseUrl = new URL("../public-relay/", import.meta.url), url = new URL("relay.html", baseUrl), container = document.body } = {}) {
2833
+ async function newRemoteRelayChannel({ baseUrl = new URL("../public-relay/", moduleUrl), url = new URL("relay.html", baseUrl), container = document.body } = {}) {
2615
2834
  const messageChannel = new MessageChannel();
2616
2835
  const { iframe, promise } = newIFrame(url);
2617
2836
  Object.assign(iframe.style, {
@@ -2670,14 +2889,56 @@ async function initializeConnection({ key, communicationPort, ...options }) {
2670
2889
  }
2671
2890
  return channel.port1;
2672
2891
  }
2673
- async function registerConnectionsHandler({ key, handler, communicationPort }) {
2892
+ async function registerConnectionsHandler({ key, path, handler, communicationPort }) {
2674
2893
  const [register, cleanup] = newRegistry();
2675
- await callChannel(communicationPort, "REGISTER", { key });
2894
+ await callChannel(communicationPort, "REGISTER", path == null ? { key } : {
2895
+ key,
2896
+ path
2897
+ });
2676
2898
  register(() => callChannel(communicationPort, "UNREGISTER", { key }));
2677
- register(handleChannelCalls(communicationPort, "CONNECT", async (event, data, port) => {
2899
+ register(handleKeyedChannelCalls(communicationPort, "CONNECT", key, async (event, data, port) => {
2678
2900
  return await handler(event, data, port);
2679
2901
  }));
2680
2902
  return cleanup;
2681
2903
  }
2904
+ /**
2905
+ * `handleChannelCalls`, but only for calls whose `params.key` is `key`.
2906
+ *
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.
2913
+ *
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.
2919
+ *
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.
2924
+ */
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 = {};
2932
+ try {
2933
+ response.result = await handler(event, data.params, ...transfers);
2934
+ } catch (error) {
2935
+ response.error = serializeError(error);
2936
+ }
2937
+ port?.postMessage(response);
2938
+ };
2939
+ target.addEventListener("message", listener);
2940
+ target.start?.();
2941
+ return () => target.removeEventListener("message", listener);
2942
+ }
2682
2943
  //#endregion
2683
- export { DuplexSiteBuilder, HttpError, HttpParseError, PEER_ERROR_HEADER, TransportClosedError, callChannel, callHttpService, collect, collectBytes, collectString, decodeJsonl, decodeMessage, decodeText, defaultCodec, deserializeError, emulateMux, encodeJsonl, encodeMessage, encodeText, fetchOverDuplex, fromReadableStream, getRelayWindowMessageHandler, handleChannelCalls, 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 };
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 };
@@ -1,7 +1,92 @@
1
+ import { type MountSpec, type MountTable } from "./mount-table.js";
2
+ /** What the registry keeps per service key. */
3
+ export interface RegisteredClient {
4
+ clientId: string;
5
+ /** Where the service is mounted. Absent means `/~<key>/`, as before mounts. */
6
+ path?: string;
7
+ }
8
+ /**
9
+ * One stored registry entry, whatever shape it is on disk.
10
+ *
11
+ * BEFORE MOUNTS THE VALUE WAS A BARE CLIENT ID. A browser that ran the earlier
12
+ * worker still holds that shape, and reading it as an object would drop the id
13
+ * and quietly unregister every service the visitor had.
14
+ */
15
+ export declare function readStoredEntry(value: unknown): RegisteredClient | undefined;
16
+ /**
17
+ * Which service, if any, should answer `url`.
18
+ *
19
+ * `undefined` means NOT THE RELAY'S, and the caller must not call
20
+ * `respondWith`: the request then goes to the network, which is how a host
21
+ * keeps serving its own files from its own origin. Answering 404 here instead
22
+ * would make a root mount fatal.
23
+ */
24
+ export declare function resolveServiceKey(url: URL, table: MountTable, selfOrigin: string): string | undefined;
25
+ /**
26
+ * Waits for the mount table to be restored from the registry before routing
27
+ * `url` — but a restore failure must never wedge every fetch. `restored`
28
+ * rejecting (blocked storage, quota, private-mode edge cases) would otherwise
29
+ * propagate straight to `respondWith` on every request, including ones that
30
+ * should reach the network, which breaks the one rule this file exists to
31
+ * uphold. So: log the failure and route with whatever the in-memory table
32
+ * already holds — possibly empty, never fatal.
33
+ */
34
+ export declare function resolveAfterRestore(restored: Promise<void>, url: URL, table: MountTable, selfOrigin: string): Promise<string | undefined>;
35
+ /**
36
+ * What REGISTER does to the mount table: set it when `path` is given, or
37
+ * remove any earlier mount when it is not. A path-less re-registration
38
+ * reverts a service to `/~<key>/` addressing, and a stale prefix left behind
39
+ * would keep routing requests to a mount that no longer exists.
40
+ *
41
+ * A REGISTRATION ONLY TOUCHES WHAT A REGISTRATION MADE. `hostKeys` names the
42
+ * mounts the host declared itself, in `options.mounts`. Those are the host's
43
+ * build-time decision and a page may not undo it: the documented static flow
44
+ * -- host declares `{ key: "app", path: "/" }`, page calls `initHttpService(h,
45
+ * { key: "app", port })` with no path -- would otherwise have its very first
46
+ * registration delete the host's own mount and leave the origin unmounted. A
47
+ * path-ful REGISTER naming a host key is ignored for the same reason (it would
48
+ * replace a `match` predicate with a prefix of the page's choosing).
49
+ */
50
+ export declare function applyRegisteredMount(table: MountTable, key: string, path: string | undefined, hostKeys?: ReadonlySet<string>): void;
51
+ /**
52
+ * What UNREGISTER does to the mount table: drop the mount a registration
53
+ * made. A host-declared mount stays, for the reason `applyRegisteredMount`
54
+ * gives -- a page tearing down its service must not take the host's table
55
+ * with it.
56
+ */
57
+ export declare function removeRegisteredMount(table: MountTable, key: string, hostKeys?: ReadonlySet<string>): void;
58
+ export interface RelayServiceWorkerOptions {
59
+ /** A fixed table, for a host that knows its services at build time. */
60
+ mounts?: Array<{
61
+ key: string;
62
+ } & MountSpec>;
63
+ /** Paths the relay never claims. Checked before the table. */
64
+ exclude?: (url: URL) => boolean;
65
+ /** Refuse a registration from the wrong client. Default: everyone may. */
66
+ canRegister?: (client: Client, key: string) => boolean | Promise<boolean>;
67
+ /** Default `"last-wins"`, the behaviour before this option existed. */
68
+ takeover?: "first-wins" | "last-wins";
69
+ /** Stamp headers on responses the relay makes. Not applied to network fetches. */
70
+ decorateResponse?: (response: Response, request: Request) => Response;
71
+ }
72
+ /**
73
+ * May `candidateId` take the key?
74
+ *
75
+ * `last-wins` is what the relay has always done and stays the default. With
76
+ * `first-wins`, a LIVE holder keeps its key: on an origin whose name is
77
+ * guessable, a second page proves nothing by existing. A holder that reloaded
78
+ * is no longer live, so a host's own re-registration is never blocked.
79
+ */
80
+ export declare function mayRegister(args: {
81
+ current?: RegisteredClient;
82
+ candidateId: string;
83
+ isCurrentLive: boolean;
84
+ takeover: "first-wins" | "last-wins";
85
+ }): boolean;
1
86
  /**
2
87
  * Boots the relay ServiceWorker: routes fetches shaped `<origin>/~<key>/…` to
3
88
  * the client that registered `key`, and exposes REGISTER/UNREGISTER/CONNECT
4
89
  * channel calls used by the page-side relay client.
5
90
  */
6
- export declare function startRelayServiceWorker(self: ServiceWorkerGlobalScope): () => void;
91
+ export declare function startRelayServiceWorker(self: ServiceWorkerGlobalScope, options?: RelayServiceWorkerOptions): () => void;
7
92
  //# sourceMappingURL=index-sw.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index-sw.d.ts","sourceRoot":"","sources":["../../src/relay/index-sw.ts"],"names":[],"mappings":"AAOA;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,wBAAwB,GAAG,MAAM,IAAI,CAwElF"}
1
+ {"version":3,"file":"index-sw.d.ts","sourceRoot":"","sources":["../../src/relay/index-sw.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,KAAK,SAAS,EAAE,KAAK,UAAU,EAAiB,MAAM,kBAAkB,CAAC;AAGlF,+CAA+C;AAC/C,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,+EAA+E;IAC/E,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,gBAAgB,GAAG,SAAS,CAM5E;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAC/B,GAAG,EAAE,GAAG,EACR,KAAK,EAAE,UAAU,EACjB,UAAU,EAAE,MAAM,GACjB,MAAM,GAAG,SAAS,CAUpB;AAED;;;;;;;;GAQG;AACH,wBAAsB,mBAAmB,CACvC,QAAQ,EAAE,OAAO,CAAC,IAAI,CAAC,EACvB,GAAG,EAAE,GAAG,EACR,KAAK,EAAE,UAAU,EACjB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAO7B;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,UAAU,EACjB,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,MAAM,GAAG,SAAS,EACxB,QAAQ,GAAE,WAAW,CAAC,MAAM,CAAa,GACxC,IAAI,CAON;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,UAAU,EACjB,GAAG,EAAE,MAAM,EACX,QAAQ,GAAE,WAAW,CAAC,MAAM,CAAa,GACxC,IAAI,CAGN;AAED,MAAM,WAAW,yBAAyB;IACxC,uEAAuE;IACvE,MAAM,CAAC,EAAE,KAAK,CAAC;QAAE,GAAG,EAAE,MAAM,CAAA;KAAE,GAAG,SAAS,CAAC,CAAC;IAC5C,8DAA8D;IAC9D,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,OAAO,CAAC;IAChC,0EAA0E;IAC1E,WAAW,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC1E,uEAAuE;IACvE,QAAQ,CAAC,EAAE,YAAY,GAAG,WAAW,CAAC;IACtC,kFAAkF;IAClF,gBAAgB,CAAC,EAAE,CAAC,QAAQ,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAO,KAAK,QAAQ,CAAC;CACvE;AAED;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE;IAChC,OAAO,CAAC,EAAE,gBAAgB,CAAC;IAC3B,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,OAAO,CAAC;IACvB,QAAQ,EAAE,YAAY,GAAG,WAAW,CAAC;CACtC,GAAG,OAAO,CAIV;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CACrC,IAAI,EAAE,wBAAwB,EAC9B,OAAO,GAAE,yBAA8B,GACtC,MAAM,IAAI,CAwJZ"}
@@ -3,27 +3,45 @@ import type { MessageTarget } from "../core/message-target.js";
3
3
  export * from "./split-service-url.js";
4
4
  /**
5
5
  * Returns a MessagePort that transparently bridges messages to/from the
6
- * ServiceWorker controlling this page.
6
+ * page's ServiceWorker: the one controlling the page, or — when the page is
7
+ * not controlled (a hard reload, or a page Firefox left uncontrolled) — the
8
+ * active worker of `registration`, which answers messages all the same.
7
9
  */
8
- export declare function newServiceWorkerPort(): MessagePort;
10
+ export declare function newServiceWorkerPort(registration?: ServiceWorkerRegistration): MessagePort;
9
11
  export interface InitServiceWorkerOptions {
10
12
  swUrl: string;
11
13
  scopeUrl?: string;
12
14
  type?: WorkerType;
15
+ /**
16
+ * Upper bound, in ms, for the wait for the worker to activate; past it the
17
+ * promise rejects with a `ServiceWorkerControlError`. Default
18
+ * `DEFAULT_SERVICE_WORKER_TIMEOUT` (30 s).
19
+ */
20
+ timeout?: number;
13
21
  }
14
22
  /**
15
- * Registers a ServiceWorker and resolves once it's activated and controlling the page.
23
+ * Registers a ServiceWorker and resolves with it once it is activated: the
24
+ * worker controlling the page, or the registration's active worker when the
25
+ * page is not controlled. Messaging works either way, which is all the relay
26
+ * needs; nothing here waits for control, because an uncontrolled page (hard
27
+ * reload; Firefox) may never get it. Rejects with a
28
+ * `ServiceWorkerControlError` if activation takes longer than `timeout`.
16
29
  */
17
- export declare function initServiceWorker({ swUrl, scopeUrl, type, }: InitServiceWorkerOptions): Promise<ServiceWorker>;
30
+ export declare function initServiceWorker(options: InitServiceWorkerOptions): Promise<ServiceWorker>;
18
31
  export interface ServiceOptions {
19
32
  key: string;
33
+ /**
34
+ * Where this service is mounted on the relay origin, e.g. `/` or `/peers/`.
35
+ * Omitted, the service stays reachable at `/~<key>/`, as before mounts.
36
+ */
37
+ path?: string;
20
38
  port: MessageTarget;
21
39
  }
22
40
  /**
23
41
  * Registers `handler` as the server for the given service `key` on the relay.
24
42
  * Returns a cleanup function that unregisters the service.
25
43
  */
26
- export declare function initHttpService(handler: HttpHandler, { key, port }: ServiceOptions): Promise<() => void>;
44
+ export declare function initHttpService(handler: HttpHandler, { key, path, port }: ServiceOptions): Promise<() => void>;
27
45
  /**
28
46
  * Sends a `Request` to the service registered under `key` on the relay and
29
47
  * resolves with the corresponding `Response`.
@@ -32,13 +50,17 @@ export declare function callHttpService(request: Request, { key, port }: Service
32
50
  export interface RelayWindowHandlerOptions {
33
51
  swUrl?: string;
34
52
  scopeUrl?: string;
53
+ /** Passed to `initServiceWorker`: how long to wait for the relay worker to activate. */
54
+ timeout?: number;
35
55
  }
36
56
  /**
37
57
  * Returns a `window.onmessage` handler for use inside the relay iframe:
38
58
  * it accepts a CONNECT message, starts the relay ServiceWorker, and bridges
39
- * the parent's MessagePort with the SW.
59
+ * the parent's MessagePort with the SW. If the worker cannot be started, every
60
+ * call the parent makes on that port is answered with the error, so the
61
+ * parent's `initHttpService` / `callHttpService` reject instead of waiting.
40
62
  */
41
- export declare function getRelayWindowMessageHandler({ swUrl, scopeUrl, }?: RelayWindowHandlerOptions): (ev: MessageEvent) => Promise<void>;
63
+ export declare function getRelayWindowMessageHandler({ swUrl, scopeUrl, timeout, }?: RelayWindowHandlerOptions): (ev: MessageEvent) => Promise<void>;
42
64
  export interface RemoteRelayChannelOptions {
43
65
  baseUrl?: URL;
44
66
  url?: URL;
@@ -62,8 +84,10 @@ export interface InitializeConnectionOptions {
62
84
  export declare function initializeConnection({ key, communicationPort, ...options }: InitializeConnectionOptions): Promise<MessagePort | null>;
63
85
  export interface RegisterConnectionsHandlerOptions {
64
86
  key: string;
87
+ /** Where this service is mounted; see `ServiceOptions.path`. */
88
+ path?: string;
65
89
  handler: (event: MessageEvent, data: unknown, port: MessagePort) => boolean | Promise<boolean>;
66
90
  communicationPort: MessageTarget;
67
91
  }
68
- export declare function registerConnectionsHandler({ key, handler, communicationPort, }: RegisterConnectionsHandlerOptions): Promise<() => void>;
92
+ export declare function registerConnectionsHandler({ key, path, handler, communicationPort, }: RegisterConnectionsHandlerOptions): Promise<() => void>;
69
93
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/relay/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAC;AAEpE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAI/D,cAAc,wBAAwB,CAAC;AAEvC;;;GAGG;AACH,wBAAgB,oBAAoB,IAAI,WAAW,CASlD;AAED,MAAM,WAAW,wBAAwB;IACvC,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,UAAU,CAAC;CACnB;AAED;;GAEG;AACH,wBAAsB,iBAAiB,CAAC,EACtC,KAAK,EACL,QAAQ,EACR,IAAI,GACL,EAAE,wBAAwB,GAAG,OAAO,CAAC,aAAa,CAAC,CAKnD;AAiCD,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,aAAa,CAAC;CACrB;AAED;;;GAGG;AACH,wBAAsB,eAAe,CACnC,OAAO,EAAE,WAAW,EACpB,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE,cAAc,GAC5B,OAAO,CAAC,MAAM,IAAI,CAAC,CASrB;AAED;;;GAGG;AACH,wBAAsB,eAAe,CACnC,OAAO,EAAE,OAAO,EAChB,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE,cAAc,GAC5B,OAAO,CAAC,QAAQ,CAAC,CAInB;AAED,MAAM,WAAW,yBAAyB;IACxC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;GAIG;AACH,wBAAgB,4BAA4B,CAAC,EAC3C,KAAsD,EACtD,QAA+C,GAChD,GAAE,yBAA8B,GAAG,CAAC,EAAE,EAAE,YAAY,KAAK,OAAO,CAAC,IAAI,CAAC,CAoBtE;AAED,MAAM,WAAW,yBAAyB;IACxC,OAAO,CAAC,EAAE,GAAG,CAAC;IACd,GAAG,CAAC,EAAE,GAAG,CAAC;IACV,SAAS,CAAC,EAAE,WAAW,CAAC;CACzB;AAED,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,GAAG,CAAC;IACb,IAAI,EAAE,WAAW,CAAC;IAClB,KAAK,IAAI,IAAI,CAAC;CACf;AAED;;;GAGG;AACH,wBAAsB,qBAAqB,CAAC,EAC1C,OAAsD,EACtD,GAAoC,EACpC,SAAyB,GAC1B,GAAE,yBAA8B,GAAG,OAAO,CAAC,kBAAkB,CAAC,CA+C9D;AAED,MAAM,WAAW,2BAA2B;IAC1C,GAAG,EAAE,MAAM,CAAC;IACZ,iBAAiB,EAAE,aAAa,CAAC;IACjC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,wBAAsB,oBAAoB,CAAC,EACzC,GAAG,EACH,iBAAiB,EACjB,GAAG,OAAO,EACX,EAAE,2BAA2B,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAc3D;AAED,MAAM,WAAW,iCAAiC;IAChD,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,CAAC,KAAK,EAAE,YAAY,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,WAAW,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC/F,iBAAiB,EAAE,aAAa,CAAC;CAClC;AAED,wBAAsB,0BAA0B,CAAC,EAC/C,GAAG,EACH,OAAO,EACP,iBAAiB,GAClB,EAAE,iCAAiC,GAAG,OAAO,CAAC,MAAM,IAAI,CAAC,CAUzD"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/relay/index.ts"],"names":[],"mappings":"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"}