@statewalker/webrun-http-browser 0.3.2 → 0.3.4

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 (92) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +321 -50
  3. package/demo/demo-1.html +109 -194
  4. package/demo/demo-2.html +119 -580
  5. package/dist/core/data-calls.d.ts +5 -0
  6. package/dist/core/data-calls.d.ts.map +1 -0
  7. package/dist/core/data-channels.d.ts +17 -0
  8. package/dist/core/data-channels.d.ts.map +1 -0
  9. package/dist/core/index.d.ts +6 -0
  10. package/dist/core/index.d.ts.map +1 -0
  11. package/dist/core/message-target.d.ts +16 -0
  12. package/dist/core/message-target.d.ts.map +1 -0
  13. package/dist/core/registry.d.ts +18 -0
  14. package/dist/core/registry.d.ts.map +1 -0
  15. package/dist/http/http-send-recieve.d.ts +23 -0
  16. package/dist/http/http-send-recieve.d.ts.map +1 -0
  17. package/dist/http/index.d.ts +4 -0
  18. package/dist/http/index.d.ts.map +1 -0
  19. package/dist/index.d.ts +4 -0
  20. package/dist/index.d.ts.map +1 -0
  21. package/dist/index.js +2541 -807
  22. package/dist/relay/index-sw.d.ts +7 -0
  23. package/dist/relay/index-sw.d.ts.map +1 -0
  24. package/dist/relay/index.d.ts +69 -0
  25. package/dist/relay/index.d.ts.map +1 -0
  26. package/dist/relay/split-service-url.d.ts +13 -0
  27. package/dist/relay/split-service-url.d.ts.map +1 -0
  28. package/dist/relay-sw.d.ts +2 -0
  29. package/dist/relay-sw.d.ts.map +1 -0
  30. package/dist/relay-sw.js +878 -0
  31. package/dist/sw/http-sw-dispatcher.d.ts +28 -0
  32. package/dist/sw/http-sw-dispatcher.d.ts.map +1 -0
  33. package/dist/sw/index.d.ts +3 -0
  34. package/dist/sw/index.d.ts.map +1 -0
  35. package/dist/sw/sw-dispatcher.d.ts +74 -0
  36. package/dist/sw/sw-dispatcher.d.ts.map +1 -0
  37. package/dist/sw-worker.d.ts +2 -0
  38. package/dist/sw-worker.d.ts.map +1 -0
  39. package/dist/sw-worker.js +813 -0
  40. package/dist/sw.d.ts +2 -0
  41. package/dist/sw.d.ts.map +1 -0
  42. package/dist/sw.js +1076 -0
  43. package/package.json +50 -37
  44. package/public/index.html +61 -97
  45. package/public/index.js +7 -4
  46. package/public/sw-worker.js +4 -0
  47. package/public-relay/heartbeat.js +4 -3
  48. package/public-relay/relay-sw.js +4 -6
  49. package/public-relay/relay.html +16 -11
  50. package/src/core/data-calls.ts +46 -0
  51. package/src/core/data-channels.ts +224 -0
  52. package/src/core/index.ts +9 -0
  53. package/src/core/message-target.ts +18 -0
  54. package/src/core/registry.ts +54 -0
  55. package/src/http/http-send-recieve.ts +81 -0
  56. package/src/http/index.ts +7 -0
  57. package/src/index.ts +3 -0
  58. package/src/relay/index-sw.ts +145 -0
  59. package/src/relay/index.ts +261 -0
  60. package/src/relay/split-service-url.ts +30 -0
  61. package/src/relay-sw.ts +7 -0
  62. package/src/sw/http-sw-dispatcher.ts +122 -0
  63. package/src/sw/index.ts +2 -0
  64. package/src/sw/sw-dispatcher.ts +307 -0
  65. package/src/sw-worker.ts +7 -0
  66. package/src/sw.ts +1 -0
  67. package/CHANGELOG.md +0 -1
  68. package/demo/demo-3.html +0 -167
  69. package/dist/index-sw-umd.js +0 -798
  70. package/dist/index-sw-umd.min.js +0 -2
  71. package/dist/index-sw.js +0 -791
  72. package/dist/index-sw.min.js +0 -2
  73. package/dist/index-umd.js +0 -853
  74. package/dist/index-umd.min.js +0 -2
  75. package/dist/index.min.js +0 -2
  76. package/index.js +0 -1
  77. package/src/core/data-calls.js +0 -36
  78. package/src/core/data-channels.js +0 -192
  79. package/src/core/data-send-recieve.js +0 -28
  80. package/src/core/errors.js +0 -13
  81. package/src/core/index.js +0 -4
  82. package/src/http/HttpError.js +0 -59
  83. package/src/http/http-send-recieve.js +0 -40
  84. package/src/http/http-stubs.js +0 -135
  85. package/src/http/index.js +0 -3
  86. package/src/http/readable-streams.js +0 -33
  87. package/src/index.js +0 -7
  88. package/src/relay/index-sw.js +0 -216
  89. package/src/relay/index.js +0 -263
  90. package/src/relay/splitServiceUrl.js +0 -24
  91. package/src/sw/http-sw-dispatcher.js +0 -128
  92. package/src/sw/sw-dispatcher.js +0 -264
@@ -0,0 +1,54 @@
1
+ export type CleanupAction = () => unknown;
2
+
3
+ interface CleanupFn {
4
+ (skip?: boolean): unknown;
5
+ action: CleanupAction;
6
+ }
7
+
8
+ export interface Registry {
9
+ register(action: CleanupAction): CleanupFn;
10
+ unregister(action: CleanupAction): void;
11
+ clear(): void;
12
+ }
13
+
14
+ export type NewRegistryResult = [
15
+ register: Registry["register"],
16
+ clear: Registry["clear"],
17
+ unregister: Registry["unregister"],
18
+ ] &
19
+ Registry;
20
+
21
+ export function newRegistry(onError: (error: unknown) => void = console.error): NewRegistryResult {
22
+ let counter = 0;
23
+ const registrations: Record<number, CleanupFn> = {};
24
+
25
+ const register: Registry["register"] = (action) => {
26
+ const id = counter++;
27
+ const fn: CleanupFn = Object.assign(
28
+ (skip?: boolean) => {
29
+ try {
30
+ delete registrations[id];
31
+ return skip ? undefined : action();
32
+ } catch (error) {
33
+ onError(error);
34
+ }
35
+ },
36
+ { action },
37
+ );
38
+ registrations[id] = fn;
39
+ return fn;
40
+ };
41
+
42
+ const unregister: Registry["unregister"] = (action) => {
43
+ for (const r of Object.values(registrations)) {
44
+ if (r.action === action) r(true);
45
+ }
46
+ };
47
+
48
+ const clear: Registry["clear"] = () => {
49
+ for (const r of Object.values(registrations)) r();
50
+ };
51
+
52
+ const tuple = [register, clear, unregister] as NewRegistryResult;
53
+ return Object.assign(tuple, { register, clear, unregister });
54
+ }
@@ -0,0 +1,81 @@
1
+ import {
2
+ type HttpHandler,
3
+ newHttpClientStub,
4
+ newHttpServerStub,
5
+ type SerializedHttpEnvelope,
6
+ type SerializedHttpRequest,
7
+ type SerializedHttpResponse,
8
+ } from "@statewalker/webrun-http-streams";
9
+ import { handleStreams, sendStream } from "../core/data-channels.js";
10
+ import type { MessageTarget } from "../core/message-target.js";
11
+
12
+ type AnyEnvelope = SerializedHttpEnvelope<SerializedHttpRequest | SerializedHttpResponse>;
13
+
14
+ async function* httpToIterator(
15
+ envelopeOrPromise: AnyEnvelope | Promise<AnyEnvelope>,
16
+ ): AsyncGenerator<Uint8Array, void, unknown> {
17
+ const { options, content } = await envelopeOrPromise;
18
+ const encoder = new TextEncoder();
19
+ yield encoder.encode(JSON.stringify(options));
20
+ yield* content;
21
+ }
22
+
23
+ async function httpFromIterator<Options>(
24
+ iterable: AsyncIterable<Uint8Array> | Promise<AsyncIterable<Uint8Array>>,
25
+ ): Promise<SerializedHttpEnvelope<Options>> {
26
+ const it = (await iterable)[Symbol.asyncIterator]();
27
+ const { done, value } = await it.next();
28
+ let options = {} as Options;
29
+ if (!done && value) {
30
+ const str = new TextDecoder().decode(value);
31
+ options = JSON.parse(str) as Options;
32
+ }
33
+ const content: AsyncIterable<Uint8Array> = {
34
+ [Symbol.asyncIterator]() {
35
+ return it;
36
+ },
37
+ };
38
+ return { options, content };
39
+ }
40
+
41
+ /**
42
+ * @deprecated For new code, prefer the `MessagePort`-based stack from
43
+ * `@statewalker/webrun-http-port`. Once a `MessagePort` is established between
44
+ * page and worker, `httpServe(port, handler)` provides equivalent semantics
45
+ * with `callBidi` multiplexing, full-duplex streaming, and `AbortSignal`.
46
+ * This helper remains for existing ServiceWorker setups that still consume the
47
+ * `MessageTarget` surface; it will be reimplemented on top of
48
+ * `webrun-http-port` in a follow-up release.
49
+ */
50
+ export function handleHttpRequests(
51
+ communicationPort: MessageTarget,
52
+ handler: HttpHandler,
53
+ ): () => void {
54
+ const serverStub = newHttpServerStub(handler);
55
+ return handleStreams<Uint8Array>(communicationPort, async (it) => {
56
+ const envelope = await httpFromIterator<SerializedHttpRequest>(it);
57
+ const response = await serverStub(envelope);
58
+ return httpToIterator(response);
59
+ });
60
+ }
61
+
62
+ /**
63
+ * @deprecated For new code, prefer the `MessagePort`-based stack from
64
+ * `@statewalker/webrun-http-port/fetch`. Once the page and SW share a
65
+ * `MessagePort`, `fetchOverPort(port, request)` provides the same
66
+ * `Request → Response` semantics with multiplexing via `callBidi`, JSONL
67
+ * envelope framing, and native `AbortSignal` support. This helper remains for
68
+ * existing ServiceWorker setups; it will be reimplemented on top of
69
+ * `webrun-http-port` in a follow-up release.
70
+ */
71
+ export async function sendHttpRequest(
72
+ communicationPort: MessageTarget,
73
+ request: Request,
74
+ ): Promise<Response> {
75
+ const clientStub = newHttpClientStub(async (req) => {
76
+ return await httpFromIterator<SerializedHttpResponse>(
77
+ sendStream<Uint8Array>(communicationPort, httpToIterator(req)),
78
+ );
79
+ });
80
+ return await clientStub(request);
81
+ }
@@ -0,0 +1,7 @@
1
+ // HTTP primitives (HttpError, client/server stubs, SerializedHttpRequest/Response
2
+ // types) live in `@statewalker/webrun-http-streams`. Readable-stream helpers and
3
+ // serializable errors come from `@statewalker/webrun-streams`. Re-exported for
4
+ // back-compat so existing imports from this package's main entry keep working.
5
+ export * from "@statewalker/webrun-http-streams";
6
+ export * from "@statewalker/webrun-streams";
7
+ export * from "./http-send-recieve.js";
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ export * from "./core/index.js";
2
+ export * from "./http/index.js";
3
+ export * from "./relay/index.js";
@@ -0,0 +1,145 @@
1
+ import { HttpError } from "@statewalker/webrun-http-streams";
2
+ import { get, set } from "idb-keyval";
3
+ import { callChannel, handleChannelCalls } from "../core/data-calls.js";
4
+ import { newRegistry } from "../core/registry.js";
5
+ import { sendHttpRequest } from "../http/http-send-recieve.js";
6
+ import { splitServiceUrl } from "./split-service-url.js";
7
+
8
+ /**
9
+ * Boots the relay ServiceWorker: routes fetches shaped `<origin>/~<key>/…` to
10
+ * the client that registered `key`, and exposes REGISTER/UNREGISTER/CONNECT
11
+ * channel calls used by the page-side relay client.
12
+ */
13
+ export function startRelayServiceWorker(self: ServiceWorkerGlobalScope): () => void {
14
+ const [register, clear] = newRegistry();
15
+
16
+ if (typeof self.skipWaiting === "function") {
17
+ self.addEventListener("install", (e: ExtendableEvent) => {
18
+ e.waitUntil(self.skipWaiting());
19
+ });
20
+ }
21
+
22
+ if (self.clients && typeof self.clients.claim === "function") {
23
+ self.addEventListener("activate", (e: ExtendableEvent) => {
24
+ e.waitUntil(self.clients.claim());
25
+ });
26
+ }
27
+
28
+ const clientsRegistry = newClientsRegistry({ self });
29
+
30
+ register(
31
+ handleChannelCalls(self, "REGISTER", async (event, data) => {
32
+ const source = event.source as Client | null;
33
+ if (!source) return false;
34
+ const { key } = data as { key: string };
35
+ return await clientsRegistry.addClient(key, source);
36
+ }),
37
+ );
38
+ register(
39
+ handleChannelCalls(self, "UNREGISTER", async (_event, data) => {
40
+ const { key } = data as { key: string };
41
+ return await clientsRegistry.removeClient(key);
42
+ }),
43
+ );
44
+ register(
45
+ handleChannelCalls(self, "CONNECT", async (_event, data, port: MessagePort) => {
46
+ const { key } = data as { key: string };
47
+ const client = await clientsRegistry.getClient(key);
48
+ if (!client) throw new Error(`Target client was not found. Target key: "${key}".`);
49
+ return await callChannel<boolean>(client, "CONNECT", data, port);
50
+ }),
51
+ );
52
+
53
+ const fetchListener = (event: FetchEvent) => {
54
+ const request = event.request;
55
+ const params = splitServiceUrl(request.url);
56
+ const { key } = params;
57
+ if (!key) return;
58
+
59
+ event.respondWith(
60
+ (async (): Promise<Response> => {
61
+ try {
62
+ const channel = new MessageChannel();
63
+ const client = await clientsRegistry.getClient(key);
64
+ if (!client) throw HttpError.errorResourceGone(params);
65
+ const data = { type: "http", key };
66
+ const accepted = await callChannel<boolean>(client, "CONNECT", data, channel.port2);
67
+ if (!accepted) throw HttpError.errorForbidden(params);
68
+ return await sendHttpRequest(channel.port1, request);
69
+ } catch (error) {
70
+ const httpError = HttpError.fromError(error);
71
+ const options = httpError.getResponseOptions(params);
72
+ return new Response(JSON.stringify(options), {
73
+ status: httpError.status ?? 500,
74
+ statusText: httpError.statusText ?? "Internal Error",
75
+ headers: { "Content-Type": "application/json" },
76
+ });
77
+ }
78
+ })(),
79
+ );
80
+ };
81
+ self.addEventListener("fetch", fetchListener);
82
+ register(() => self.removeEventListener("fetch", fetchListener));
83
+
84
+ return clear;
85
+ }
86
+
87
+ interface ClientsRegistryOptions {
88
+ self: ServiceWorkerGlobalScope;
89
+ key?: string;
90
+ }
91
+
92
+ interface ClientsRegistry {
93
+ addClient(clientKey: string, client: Client): Promise<boolean>;
94
+ removeClient(clientKey: string): Promise<boolean>;
95
+ getClient(clientKey: string): Promise<Client | undefined>;
96
+ }
97
+
98
+ function newClientsRegistry({ self, key = "clientsIds" }: ClientsRegistryOptions): ClientsRegistry {
99
+ let _index: Record<string, string> | undefined;
100
+
101
+ async function loadClientsIndex(): Promise<Record<string, string>> {
102
+ if (!_index) {
103
+ const entries = ((await get<Array<[string, string]>>(key)) ?? []) as Array<[string, string]>;
104
+ _index = Object.fromEntries(entries);
105
+ }
106
+ return _index;
107
+ }
108
+
109
+ async function storeClientsIndex(): Promise<Record<string, string>> {
110
+ const index = await loadClientsIndex();
111
+ await set(key, Object.entries(index));
112
+ return index;
113
+ }
114
+
115
+ async function addClient(clientKey: string, client: Client): Promise<boolean> {
116
+ const index = await loadClientsIndex();
117
+ const clientId = client.id;
118
+ if (index[clientKey] === clientId) return false;
119
+ index[clientKey] = clientId;
120
+ await storeClientsIndex();
121
+ return true;
122
+ }
123
+
124
+ async function removeClient(clientKey: string): Promise<boolean> {
125
+ const index = await loadClientsIndex();
126
+ if (!(clientKey in index)) return false;
127
+ delete index[clientKey];
128
+ await storeClientsIndex();
129
+ return true;
130
+ }
131
+
132
+ async function getClient(clientKey: string): Promise<Client | undefined> {
133
+ const index = await loadClientsIndex();
134
+ const clientId = index[clientKey];
135
+ if (!clientId) return undefined;
136
+ const client = await self.clients.get(clientId);
137
+ if (!client) {
138
+ delete index[clientKey];
139
+ await storeClientsIndex();
140
+ }
141
+ return client ?? undefined;
142
+ }
143
+
144
+ return { getClient, addClient, removeClient };
145
+ }
@@ -0,0 +1,261 @@
1
+ import type { HttpHandler } from "@statewalker/webrun-http-streams";
2
+ import { callChannel, handleChannelCalls } from "../core/data-calls.js";
3
+ import type { MessageTarget } from "../core/message-target.js";
4
+ import { newRegistry } from "../core/registry.js";
5
+ import { handleHttpRequests, sendHttpRequest } from "../http/http-send-recieve.js";
6
+
7
+ export * from "./split-service-url.js";
8
+
9
+ /**
10
+ * Returns a MessagePort that transparently bridges messages to/from the
11
+ * ServiceWorker controlling this page.
12
+ */
13
+ export function newServiceWorkerPort(): MessagePort {
14
+ const channel = new MessageChannel();
15
+ channel.port1.onmessage = (event) => {
16
+ navigator.serviceWorker.controller?.postMessage(event.data, [...event.ports]);
17
+ };
18
+ navigator.serviceWorker.addEventListener("message", (event) => {
19
+ channel.port1.postMessage(event.data, [...event.ports]);
20
+ });
21
+ return channel.port2;
22
+ }
23
+
24
+ export interface InitServiceWorkerOptions {
25
+ swUrl: string;
26
+ scopeUrl?: string;
27
+ type?: WorkerType;
28
+ }
29
+
30
+ /**
31
+ * Registers a ServiceWorker and resolves once it's activated and controlling the page.
32
+ */
33
+ export async function initServiceWorker({
34
+ swUrl,
35
+ scopeUrl,
36
+ type,
37
+ }: InitServiceWorkerOptions): Promise<ServiceWorker> {
38
+ await navigator.serviceWorker.register(swUrl, { type, scope: scopeUrl });
39
+ const worker = await getServiceWorkerController();
40
+ await awaitServiceWorkerActivation(worker);
41
+ return worker;
42
+ }
43
+
44
+ function getServiceWorkerController(): Promise<ServiceWorker> {
45
+ return new Promise((resolve) => {
46
+ const container = navigator.serviceWorker;
47
+ if (container.controller) {
48
+ resolve(container.controller);
49
+ return;
50
+ }
51
+ const onChange = () => {
52
+ if (!container.controller) return;
53
+ resolve(container.controller);
54
+ container.removeEventListener("controllerchange", onChange);
55
+ };
56
+ container.addEventListener("controllerchange", onChange);
57
+ });
58
+ }
59
+
60
+ function awaitServiceWorkerActivation(worker: ServiceWorker): Promise<void> {
61
+ return new Promise((resolve) => {
62
+ if (worker.state === "activated") {
63
+ resolve();
64
+ return;
65
+ }
66
+ const onStateChange = () => {
67
+ if (worker.state !== "activated") return;
68
+ worker.removeEventListener("statechange", onStateChange);
69
+ resolve();
70
+ };
71
+ worker.addEventListener("statechange", onStateChange);
72
+ });
73
+ }
74
+
75
+ export interface ServiceOptions {
76
+ key: string;
77
+ port: MessageTarget;
78
+ }
79
+
80
+ /**
81
+ * Registers `handler` as the server for the given service `key` on the relay.
82
+ * Returns a cleanup function that unregisters the service.
83
+ */
84
+ export async function initHttpService(
85
+ handler: HttpHandler,
86
+ { key, port }: ServiceOptions,
87
+ ): Promise<() => void> {
88
+ return await registerConnectionsHandler({
89
+ key,
90
+ communicationPort: port,
91
+ handler: async (_event, _data, callPort) => {
92
+ handleHttpRequests(callPort, handler);
93
+ return true;
94
+ },
95
+ });
96
+ }
97
+
98
+ /**
99
+ * Sends a `Request` to the service registered under `key` on the relay and
100
+ * resolves with the corresponding `Response`.
101
+ */
102
+ export async function callHttpService(
103
+ request: Request,
104
+ { key, port }: ServiceOptions,
105
+ ): Promise<Response> {
106
+ const callPort = await initializeConnection({ key, communicationPort: port });
107
+ if (!callPort) throw new Error(`No service with key "${key}"`);
108
+ return await sendHttpRequest(callPort, request);
109
+ }
110
+
111
+ export interface RelayWindowHandlerOptions {
112
+ swUrl?: string;
113
+ scopeUrl?: string;
114
+ }
115
+
116
+ /**
117
+ * Returns a `window.onmessage` handler for use inside the relay iframe:
118
+ * it accepts a CONNECT message, starts the relay ServiceWorker, and bridges
119
+ * the parent's MessagePort with the SW.
120
+ */
121
+ export function getRelayWindowMessageHandler({
122
+ swUrl = `${new URL("./index-sw.js", import.meta.url)}`,
123
+ scopeUrl = `${new URL("../", import.meta.url)}`,
124
+ }: RelayWindowHandlerOptions = {}): (ev: MessageEvent) => Promise<void> {
125
+ let externalPort: MessagePort | undefined;
126
+ return async (ev) => {
127
+ if (ev.data?.type !== "CONNECT") return;
128
+ const newExternalPort = ev.ports?.[0];
129
+ if (!newExternalPort) return;
130
+ if (externalPort) {
131
+ newExternalPort.close();
132
+ return;
133
+ }
134
+ externalPort = newExternalPort;
135
+ await initServiceWorker({ swUrl, scopeUrl });
136
+ const serviceWorkerPort = newServiceWorkerPort();
137
+ serviceWorkerPort.onmessage = (event) => {
138
+ externalPort?.postMessage(event.data, [...event.ports]);
139
+ };
140
+ externalPort.onmessage = (event) => {
141
+ serviceWorkerPort.postMessage(event.data, [...event.ports]);
142
+ };
143
+ };
144
+ }
145
+
146
+ export interface RemoteRelayChannelOptions {
147
+ baseUrl?: URL;
148
+ url?: URL;
149
+ container?: HTMLElement;
150
+ }
151
+
152
+ export interface RemoteRelayChannel {
153
+ baseUrl: URL;
154
+ port: MessagePort;
155
+ close(): void;
156
+ }
157
+
158
+ /**
159
+ * Embeds a hidden relay iframe, establishes a MessageChannel with it, and
160
+ * returns the port to be used with `initHttpService` / `callHttpService`.
161
+ */
162
+ export async function newRemoteRelayChannel({
163
+ baseUrl = new URL("../public-relay/", import.meta.url),
164
+ url = new URL("relay.html", baseUrl),
165
+ container = document.body,
166
+ }: RemoteRelayChannelOptions = {}): Promise<RemoteRelayChannel> {
167
+ const messageChannel = new MessageChannel();
168
+ const { iframe, promise } = newIFrame(url);
169
+ Object.assign(iframe.style, {
170
+ position: "fixed",
171
+ width: "1px",
172
+ height: "1px",
173
+ top: "-1000px",
174
+ left: "-1000px",
175
+ display: "block",
176
+ opacity: "0",
177
+ border: "none",
178
+ outline: "none",
179
+ });
180
+ container.appendChild(iframe);
181
+ promise.then(() => {
182
+ iframe.contentWindow?.postMessage({ type: "CONNECT" }, "*", [messageChannel.port1]);
183
+ });
184
+ return {
185
+ baseUrl,
186
+ port: messageChannel.port2,
187
+ close: () => {
188
+ iframe.parentElement?.removeChild(iframe);
189
+ messageChannel.port1.close();
190
+ messageChannel.port2.close();
191
+ },
192
+ };
193
+
194
+ function newIFrame(src: URL): { iframe: HTMLIFrameElement; promise: Promise<HTMLIFrameElement> } {
195
+ const iframe = document.createElement("iframe");
196
+ iframe.src = `${src}`;
197
+ Object.assign(iframe.style, {
198
+ padding: "0",
199
+ margin: "0",
200
+ border: "none",
201
+ outline: "none",
202
+ width: "100%",
203
+ height: "100%",
204
+ });
205
+ return {
206
+ iframe,
207
+ promise: new Promise<HTMLIFrameElement>((resolve, reject) => {
208
+ iframe.onerror = () => reject(new Error(`Failed to load ${src}`));
209
+ iframe.onload = () => resolve(iframe);
210
+ }),
211
+ };
212
+ }
213
+ }
214
+
215
+ export interface InitializeConnectionOptions {
216
+ key: string;
217
+ communicationPort: MessageTarget;
218
+ [key: string]: unknown;
219
+ }
220
+
221
+ export async function initializeConnection({
222
+ key,
223
+ communicationPort,
224
+ ...options
225
+ }: InitializeConnectionOptions): Promise<MessagePort | null> {
226
+ const channel = new MessageChannel();
227
+ const accepted = await callChannel<boolean>(
228
+ communicationPort,
229
+ "CONNECT",
230
+ { key, ...options },
231
+ channel.port2,
232
+ );
233
+ if (!accepted) {
234
+ channel.port1.close();
235
+ channel.port2.close();
236
+ return null;
237
+ }
238
+ return channel.port1;
239
+ }
240
+
241
+ export interface RegisterConnectionsHandlerOptions {
242
+ key: string;
243
+ handler: (event: MessageEvent, data: unknown, port: MessagePort) => boolean | Promise<boolean>;
244
+ communicationPort: MessageTarget;
245
+ }
246
+
247
+ export async function registerConnectionsHandler({
248
+ key,
249
+ handler,
250
+ communicationPort,
251
+ }: RegisterConnectionsHandlerOptions): Promise<() => void> {
252
+ const [register, cleanup] = newRegistry();
253
+ await callChannel(communicationPort, "REGISTER", { key });
254
+ register(() => callChannel(communicationPort, "UNREGISTER", { key }));
255
+ register(
256
+ handleChannelCalls(communicationPort, "CONNECT", async (event, data, port) => {
257
+ return await handler(event, data, port);
258
+ }),
259
+ );
260
+ return cleanup;
261
+ }
@@ -0,0 +1,30 @@
1
+ export interface SplitServiceUrl {
2
+ url: string;
3
+ key: string;
4
+ baseUrl: string;
5
+ path: string;
6
+ [extra: string]: unknown;
7
+ }
8
+
9
+ /**
10
+ * Splits a URL of the form `<base>/<separator><key>/<path>` into parts.
11
+ * Example: `https://host/~FS/a/b` → `{ baseUrl: "https://host/~FS/", key: "FS", path: "a/b" }`.
12
+ */
13
+ export function splitServiceUrl(url: URL | string, separator = "~"): SplitServiceUrl {
14
+ const str = `${url}`;
15
+ const idx = str.indexOf(separator);
16
+ let baseUrl = "";
17
+ let key = "";
18
+ let path = "";
19
+ if (idx >= 0) {
20
+ baseUrl = str.substring(0, idx + separator.length);
21
+ str.substring(idx + separator.length).replace(/^([^/]+)/, (match, $1) => {
22
+ baseUrl += match;
23
+ if (baseUrl.length < str.length) baseUrl += "/";
24
+ key = $1;
25
+ path = str.substring(baseUrl.length);
26
+ return "";
27
+ });
28
+ }
29
+ return { url: str, key, baseUrl, path };
30
+ }
@@ -0,0 +1,7 @@
1
+ /// <reference lib="webworker" />
2
+
3
+ import { startRelayServiceWorker } from "./relay/index-sw.js";
4
+
5
+ declare const self: ServiceWorkerGlobalScope;
6
+
7
+ startRelayServiceWorker(self);