@statewalker/webrun-http-browser 0.5.0 → 0.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -49,7 +49,8 @@ npm install @statewalker/webrun-http-browser
49
49
  | --- | --- |
50
50
  | `@statewalker/webrun-http-browser` | Page-side relay API: `newRemoteRelayChannel`, `initHttpService`, `callHttpService`, `splitServiceUrl`, `initServiceWorker`, `newServiceWorkerPort`, `getRelayWindowMessageHandler`; the MessagePort call primitives (`callChannel`, `handleChannelCalls`, `newInvokationChannel`, `sendStream`, `handleStreams`, `newRegistry`); plus everything re-exported from `@statewalker/webrun-http-streams` (`HttpError`, the client/server stubs), `@statewalker/webrun-streams` (stream and error helpers) and the `MessageTarget` family from `@statewalker/webrun-rpc` |
51
51
  | `@statewalker/webrun-http-browser/sw` | Same-origin adapter classes: `SwHttpAdapter` (page), `SwHttpDispatcher` (SW), `startHttpDispatcher` bootstrap; `start()` options `timeout` and `reloadIfUncontrolled` |
52
- | `@statewalker/webrun-http-browser/relay-sw` | IIFE bundle of the relay SW runtime — load via `importScripts` from a loader script in your relay origin |
52
+ | `@statewalker/webrun-http-browser/relay-sw` | IIFE bundle of the relay SW runtime — load via `importScripts` from a loader script in your relay origin. No declarations: it takes its options from `self.RELAY_OPTIONS` |
53
+ | `@statewalker/webrun-http-browser/relay-worker` | The same runtime as a typed ES module, for a host that bundles its own relay worker: `startRelayServiceWorker(self, options)`, plus `RelayServiceWorkerOptions` and `MountSpec` to type them (including a hand-written `self.RELAY_OPTIONS`) |
53
54
  | `@statewalker/webrun-http-browser/sw-worker` | IIFE bundle of the same-origin SW runtime — ditto, for same-origin apps |
54
55
 
55
56
  ## Examples
@@ -101,6 +102,125 @@ const res = await callHttpService(
101
102
  serving a mini site; [`demo/demo-2.html`](./demo/demo-2.html) pipes a
102
103
  local-disk folder (File System Access API) through it.
103
104
 
105
+ ### Mounting a service at a path
106
+
107
+ A service can claim a path prefix instead of living at `/~<key>/`. Both the
108
+ key and the path are the caller's, and several services can share **one**
109
+ relay connection — the shape mounts exist for is an app at the origin root
110
+ and, say, a gateway one level down, both reachable through the same iframe:
111
+
112
+ ```ts
113
+ const connection = await newRemoteRelayChannel(/* … */);
114
+
115
+ await initHttpService(appHandler, { key: "app", path: "/", port: connection.port });
116
+ await initHttpService(meshHandler, { key: "mesh", path: "/peers/", port: connection.port });
117
+ ```
118
+
119
+ A `CONNECT` is routed to the service named in its `key`, so the two never see
120
+ each other's calls even though they share one port. (Earlier builds routed
121
+ every `CONNECT` on a connection to every registered service, which collided
122
+ whenever more than one service shared a port — fixed before this shipped.)
123
+
124
+ The worker routes by the longest matching path prefix, so a catch-all at `/`
125
+ does not shadow `/peers/`, and registration order does not matter. A service
126
+ registered with no `path` is reachable at `/~<key>/`, exactly as before —
127
+ unless the host declared that key in `mounts`, in which case the host's mount
128
+ stands and the page need not repeat it.
129
+
130
+ **A request that matches no mount is not the relay's** — the worker does not
131
+ answer it at all, so the browser performs it exactly as it would with no
132
+ worker installed. That is what lets a host serve its own files from the same
133
+ origin, and it is why a root mount needs `exclude`.
134
+
135
+ **The matched prefix is NOT stripped.** A handler mounted at `/peers/`
136
+ receives `/peers/12D3Koo/llm`, not `/12D3Koo/llm` — the request reaches it
137
+ with the path the browser asked for, whichever mount matched. A handler that
138
+ wants to route relative to its mount keeps its own `basePath` and strips the
139
+ prefix itself.
140
+
141
+ These options are read by `startRelayServiceWorker`, which a host that
142
+ bundles its own relay worker imports from
143
+ `@statewalker/webrun-http-browser/relay-worker` and calls directly (see the
144
+ options table below for the prebuilt-bundle equivalent):
145
+
146
+ ```ts
147
+ import { startRelayServiceWorker } from "@statewalker/webrun-http-browser/relay-worker";
148
+
149
+ startRelayServiceWorker(self, {
150
+ exclude: (url) =>
151
+ url.pathname === "/index.html" ||
152
+ url.pathname === "/relay.html" ||
153
+ url.pathname === "/relay-sw.js",
154
+ takeover: "first-wins",
155
+ canRegister: (client, _key) => new URL(client.url).pathname === "/relay.html",
156
+ decorateResponse: (response) => withMyHeaders(response),
157
+ });
158
+ ```
159
+
160
+ > **Mounting at `/` — set `takeover: "first-wins"` and `canRegister`.**
161
+ > The default is `takeover: "last-wins"` with no `canRegister`, which is what
162
+ > the relay has always done: the last page to REGISTER a key gets it. Before
163
+ > mounts the worst that bought a rogue or buggy same-origin page was
164
+ > `/~<key>/`; with mounts it can claim the **origin root**, and the mount is
165
+ > persisted in IndexedDB, so it outlives the page and every worker restart.
166
+ > On an origin where more than the host's own page can reach the relay,
167
+ > `takeover: "first-wins"` keeps a live holder's key and `canRegister` says
168
+ > which client may ask for it — set both, together, for any mount at `/`.
169
+
170
+ A root mount claims *every* path under the worker's scope, including the
171
+ host's own navigation. If `exclude` only covers the relay page and its
172
+ worker script, a reload requests the host's own entry page — say
173
+ `/index.html` — through the mount too, the mount has no handler for it, and
174
+ the origin cannot come back. `exclude` needs three things, always: the relay
175
+ page, the worker script, and the host's own entry page. That third one is
176
+ easy to miss because nothing fails until the first reload.
177
+
178
+ An excluded page is one the worker does not answer, and in Firefox such a
179
+ page can load **uncontrolled** even while the worker is running — then its
180
+ own `fetch()` never reaches the worker and its mounts look dead. The remedy
181
+ is the one this package already ships for that case: call
182
+ `awaitServiceWorkerControl(registration)` (exported from the package root)
183
+ before relying on `fetch()` from an excluded page. A page *served by* a mount
184
+ is a navigation the worker answers, so it is controlled from its first byte
185
+ and needs nothing.
186
+
187
+ #### `self.RELAY_OPTIONS` — options for the prebuilt worker
188
+
189
+ `dist/relay-sw.js` is an IIFE loaded via classic `importScripts`, so a host
190
+ that uses the shipped worker (rather than building its own from
191
+ `startRelayServiceWorker`) cannot pass options as arguments. It reads them
192
+ instead from `self.RELAY_OPTIONS`, which the host's own tiny worker script
193
+ sets *before* importing the bundle:
194
+
195
+ ```js
196
+ // relay-sw.js — served next to your relay page.
197
+ self.RELAY_OPTIONS = {
198
+ exclude: (url) => url.pathname === "/index.html" || url.pathname === "/relay.html" || url.pathname === "/relay-sw.js",
199
+ takeover: "first-wins",
200
+ };
201
+ importScripts("/path/to/node_modules/@statewalker/webrun-http-browser/dist/relay-sw.js");
202
+ ```
203
+
204
+ This is the only way a prebuilt-worker host reaches `exclude`, `takeover`,
205
+ `canRegister` or `decorateResponse` — omit it and the worker boots with `{}`,
206
+ exactly as it did before mounts. The bundle ships no declarations, so to type
207
+ that object (in a TypeScript loader script, or to check it before shipping)
208
+ import the type from the runtime entry:
209
+
210
+ ```ts
211
+ import type { RelayServiceWorkerOptions } from "@statewalker/webrun-http-browser/relay-worker";
212
+
213
+ declare const self: ServiceWorkerGlobalScope & { RELAY_OPTIONS?: RelayServiceWorkerOptions };
214
+ ```
215
+
216
+ | Option | Default | What it does |
217
+ | --- | --- | --- |
218
+ | `mounts` | none | A fixed table, for a host that knows its services at build time. Each entry is `{ key, path? , match? }`. A key declared here is the host's: a page's REGISTER or UNREGISTER for the same key never replaces or removes it, so the page can register with no `path` of its own. |
219
+ | `exclude` | none | Paths the relay never claims — neither through the table nor through the `/~<key>/` spelling. Checked first. |
220
+ | `canRegister` | everyone | Refuse a registration from the wrong page. |
221
+ | `takeover` | `"last-wins"` | `"first-wins"` keeps a live holder's key. |
222
+ | `decorateResponse` | none | Stamp headers on responses the relay makes; not applied to network fetches. |
223
+
104
224
  ### Same-origin mode
105
225
 
106
226
  Your page registers its own SW, handlers are local to the page:
@@ -277,13 +397,15 @@ imports keep working after those extractions. Its own surface is below.
277
397
  | `newRemoteRelayChannel(opts?)` | function | Embeds the hidden relay iframe, handshakes a `MessageChannel`, resolves a `RemoteRelayChannel`. |
278
398
  | `RemoteRelayChannelOptions` | interface | `baseUrl`, `url`, `container` — where the relay lives and what to append the iframe to. |
279
399
  | `RemoteRelayChannel` | interface | `{ baseUrl, port, close() }`. |
280
- | `initHttpService(handler, opts)` | function | Registers `handler` as the server for a service `key` on the relay. Returns a cleanup. |
400
+ | `initHttpService(handler, opts)` | function | Registers `handler` as the server for a service `key` on the relay, optionally mounted at `path`. Several services may share one `port`. Returns a cleanup. |
281
401
  | `callHttpService(request, opts)` | function | Sends a `Request` to the service under `key`; resolves its `Response`. |
282
- | `ServiceOptions` | interface | `{ key: string; port: MessageTarget }` — shared by the two above. |
402
+ | `ServiceOptions` | interface | `{ key: string; path?: string; port: MessageTarget }` — shared by the two above. `path` mounts the service (see [Mounting a service at a path](#mounting-a-service-at-a-path)); omitted, it stays at `/~<key>/`. |
283
403
  | `getRelayWindowMessageHandler(opts?)` | function | The `window.onmessage` handler that runs *inside* the relay iframe. |
284
404
  | `RelayWindowHandlerOptions` | interface | `swUrl`, `scopeUrl` for that handler. |
285
- | `splitServiceUrl(url, separator?)` | function | Splits a relay URL into service key + remaining path (default separator `~`). |
405
+ | `splitServiceUrl(url, separator?)` | function | Splits a relay URL into service key + remaining path (default separator `~`); anchored to the pathname, so a query string like `?q=~foo` is never read as a service. |
286
406
  | `SplitServiceUrl` | interface | Its result shape. |
407
+ | `startRelayServiceWorker(self, opts?)` | function | The SW side of the relay: routes `fetch` to the client that registered a mount, and answers `REGISTER`/`UNREGISTER`/`CONNECT`. Not re-exported from the package root — it is what the prebuilt `dist/relay-sw.js` calls internally; see [`self.RELAY_OPTIONS`](#selfrelay_options--options-for-the-prebuilt-worker). |
408
+ | `RelayServiceWorkerOptions` | interface | `{ mounts?, exclude?, canRegister?, takeover?, decorateResponse? }` — see the options table in [Mounting a service at a path](#mounting-a-service-at-a-path). |
287
409
 
288
410
  ### ServiceWorker lifecycle
289
411
 
@@ -341,7 +463,7 @@ imports keep working after those extractions. Its own surface is below.
341
463
  | Entry | Purpose |
342
464
  | --- | --- |
343
465
  | `@statewalker/webrun-http-browser/sw` | `SwHttpAdapter` — the same-origin ServiceWorker adapter. |
344
- | `@statewalker/webrun-http-browser/relay-sw` | IIFE relay SW runtime, loadable via `importScripts(...)`. |
466
+ | `@statewalker/webrun-http-browser/relay-sw` | IIFE relay SW runtime, loadable via `importScripts(...)`. Reads its options from `self.RELAY_OPTIONS`, set before the `importScripts` call. |
345
467
  | `@statewalker/webrun-http-browser/sw-worker` | IIFE same-origin SW runtime, loadable via `importScripts(...)`. |
346
468
 
347
469
  ## Internals
package/dist/index.js CHANGED
@@ -2660,25 +2660,54 @@ async function sendHttpRequest(communicationPort, request) {
2660
2660
  */
2661
2661
  function splitServiceUrl(url, separator = "~") {
2662
2662
  const str = `${url}`;
2663
- const idx = str.indexOf(separator);
2664
- let baseUrl = "";
2665
- let key = "";
2666
- let path = "";
2667
- if (idx >= 0) {
2668
- baseUrl = str.substring(0, idx + separator.length);
2669
- str.substring(idx + separator.length).replace(/^([^/]+)/, (match, $1) => {
2670
- baseUrl += match;
2671
- if (baseUrl.length < str.length) baseUrl += "/";
2672
- key = $1;
2673
- path = str.substring(baseUrl.length);
2674
- return "";
2675
- });
2676
- }
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;
2677
2706
  return {
2678
2707
  url: str,
2679
2708
  key,
2680
- baseUrl,
2681
- path
2709
+ baseUrl: `${prefix === "" ? rooted ? "/" : "" : `${prefix}/`}${separator}${key}${slash < 0 ? "" : "/"}`,
2710
+ path: slash < 0 ? "" : rest.slice(slash + 1)
2682
2711
  };
2683
2712
  }
2684
2713
  //#endregion
@@ -2735,9 +2764,10 @@ async function registerServiceWorker({ swUrl, scopeUrl, type, timeout = DEFAULT_
2735
2764
  * Registers `handler` as the server for the given service `key` on the relay.
2736
2765
  * Returns a cleanup function that unregisters the service.
2737
2766
  */
2738
- async function initHttpService(handler, { key, port }) {
2767
+ async function initHttpService(handler, { key, path, port }) {
2739
2768
  return await registerConnectionsHandler({
2740
2769
  key,
2770
+ path,
2741
2771
  communicationPort: port,
2742
2772
  handler: async (_event, _data, callPort) => {
2743
2773
  handleHttpRequests(callPort, handler);
@@ -2859,14 +2889,56 @@ async function initializeConnection({ key, communicationPort, ...options }) {
2859
2889
  }
2860
2890
  return channel.port1;
2861
2891
  }
2862
- async function registerConnectionsHandler({ key, handler, communicationPort }) {
2892
+ async function registerConnectionsHandler({ key, path, handler, communicationPort }) {
2863
2893
  const [register, cleanup] = newRegistry();
2864
- await callChannel(communicationPort, "REGISTER", { key });
2894
+ await callChannel(communicationPort, "REGISTER", path == null ? { key } : {
2895
+ key,
2896
+ path
2897
+ });
2865
2898
  register(() => callChannel(communicationPort, "UNREGISTER", { key }));
2866
- register(handleChannelCalls(communicationPort, "CONNECT", async (event, data, port) => {
2899
+ register(handleKeyedChannelCalls(communicationPort, "CONNECT", key, async (event, data, port) => {
2867
2900
  return await handler(event, data, port);
2868
2901
  }));
2869
2902
  return cleanup;
2870
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
+ }
2871
2943
  //#endregion
2872
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":"AAQA;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,wBAAwB,GAAG,MAAM,IAAI,CA4ElF"}
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"}
@@ -30,13 +30,18 @@ export interface InitServiceWorkerOptions {
30
30
  export declare function initServiceWorker(options: InitServiceWorkerOptions): Promise<ServiceWorker>;
31
31
  export interface ServiceOptions {
32
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;
33
38
  port: MessageTarget;
34
39
  }
35
40
  /**
36
41
  * Registers `handler` as the server for the given service `key` on the relay.
37
42
  * Returns a cleanup function that unregisters the service.
38
43
  */
39
- export declare function initHttpService(handler: HttpHandler, { key, port }: ServiceOptions): Promise<() => void>;
44
+ export declare function initHttpService(handler: HttpHandler, { key, path, port }: ServiceOptions): Promise<() => void>;
40
45
  /**
41
46
  * Sends a `Request` to the service registered under `key` on the relay and
42
47
  * resolves with the corresponding `Response`.
@@ -79,8 +84,10 @@ export interface InitializeConnectionOptions {
79
84
  export declare function initializeConnection({ key, communicationPort, ...options }: InitializeConnectionOptions): Promise<MessagePort | null>;
80
85
  export interface RegisterConnectionsHandlerOptions {
81
86
  key: string;
87
+ /** Where this service is mounted; see `ServiceOptions.path`. */
88
+ path?: string;
82
89
  handler: (event: MessageEvent, data: unknown, port: MessagePort) => boolean | Promise<boolean>;
83
90
  communicationPort: MessageTarget;
84
91
  }
85
- export declare function registerConnectionsHandler({ key, handler, communicationPort, }: RegisterConnectionsHandlerOptions): Promise<() => void>;
92
+ export declare function registerConnectionsHandler({ key, path, handler, communicationPort, }: RegisterConnectionsHandlerOptions): Promise<() => void>;
86
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;AAGpE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAQ/D,cAAc,wBAAwB,CAAC;AAavC;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,YAAY,CAAC,EAAE,yBAAyB,GAAG,WAAW,CAU1F;AAED,MAAM,WAAW,wBAAwB;IACvC,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;;GAOG;AACH,wBAAsB,iBAAiB,CAAC,OAAO,EAAE,wBAAwB,GAAG,OAAO,CAAC,aAAa,CAAC,CAEjG;AAgBD,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;IAClB,wFAAwF;IACxF,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;GAMG;AACH,wBAAgB,4BAA4B,CAAC,EAC3C,KAAgD,EAChD,QAAyC,EACzC,OAAO,GACR,GAAE,yBAA8B,GAAG,CAAC,EAAE,EAAE,YAAY,KAAK,OAAO,CAAC,IAAI,CAAC,CA2BtE;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,OAAgD,EAChD,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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"}
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Which registered service owns a URL.
3
+ *
4
+ * A pure lookup: no ServiceWorker, no storage, no I/O, so the routing rules
5
+ * can be tested as arithmetic rather than through a browser.
6
+ *
7
+ * TWO KINDS OF MOUNT, AND WHY BOTH. A `path` is a prefix, which is all most
8
+ * hosts need and costs nothing to match. A `match` predicate is the escape
9
+ * hatch for anything richer -- a host that wants URLPattern brings it and pays
10
+ * for it; this file must stay dependency-free, because it runs in a
11
+ * ServiceWorker that has to start fast.
12
+ *
13
+ * SPECIFICITY, NOT REGISTRATION ORDER. Prefixes are tried longest-first, so a
14
+ * catch-all at "/" cannot swallow "/peers/" and a host need not register in a
15
+ * careful order. Predicates are opaque -- nothing can be said about how
16
+ * specific they are -- so they are tried after every prefix, in the order they
17
+ * were registered.
18
+ */
19
+ export interface MountSpec {
20
+ /** A path prefix, e.g. `/peers/`. `/` is the whole origin. */
21
+ path?: string;
22
+ /** Anything richer. Consulted only when no prefix matches. */
23
+ match?: (url: URL) => boolean;
24
+ }
25
+ export interface MountTable {
26
+ /** Add or replace the mount for `key`. */
27
+ set(key: string, spec: MountSpec): void;
28
+ remove(key: string): void;
29
+ /** The key that owns `url`, or `undefined` — meaning "not the relay's". */
30
+ find(url: URL): string | undefined;
31
+ /**
32
+ * Is `url` reserved by `exclude`? `find` already applies it, but the relay
33
+ * has a SECOND route — the `/~<key>/` spelling, which does not go through
34
+ * the table at all — and "an excluded path is never claimed" has to hold
35
+ * for both. The predicate lives here so there is one copy of it.
36
+ */
37
+ excludes(url: URL): boolean;
38
+ }
39
+ export interface MountTableOptions {
40
+ /**
41
+ * Paths the relay never claims, checked BEFORE the table. A root mount
42
+ * matches every path, so a host with files of its own (a relay page, a
43
+ * worker, hashed assets) is unusable without this.
44
+ */
45
+ exclude?: (url: URL) => boolean;
46
+ }
47
+ export declare function newMountTable(options?: MountTableOptions): MountTable;
48
+ //# sourceMappingURL=mount-table.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mount-table.d.ts","sourceRoot":"","sources":["../../src/relay/mount-table.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,MAAM,WAAW,SAAS;IACxB,8DAA8D;IAC9D,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,8DAA8D;IAC9D,KAAK,CAAC,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,OAAO,CAAC;CAC/B;AAED,MAAM,WAAW,UAAU;IACzB,0CAA0C;IAC1C,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,GAAG,IAAI,CAAC;IACxC,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,2EAA2E;IAC3E,IAAI,CAAC,GAAG,EAAE,GAAG,GAAG,MAAM,GAAG,SAAS,CAAC;IACnC;;;;;OAKG;IACH,QAAQ,CAAC,GAAG,EAAE,GAAG,GAAG,OAAO,CAAC;CAC7B;AAED,MAAM,WAAW,iBAAiB;IAChC;;;;OAIG;IACH,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,OAAO,CAAC;CACjC;AAyBD,wBAAgB,aAAa,CAAC,OAAO,GAAE,iBAAsB,GAAG,UAAU,CAyCzE"}
@@ -1 +1 @@
1
- {"version":3,"file":"split-service-url.d.ts","sourceRoot":"","sources":["../../src/relay/split-service-url.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,eAAe;IAC9B,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;CAC1B;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,GAAG,GAAG,MAAM,EAAE,SAAS,SAAM,GAAG,eAAe,CAiBnF"}
1
+ {"version":3,"file":"split-service-url.d.ts","sourceRoot":"","sources":["../../src/relay/split-service-url.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,eAAe;IAC9B,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;CAC1B;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,GAAG,GAAG,MAAM,EAAE,SAAS,SAAM,GAAG,eAAe,CA6EnF"}