@statewalker/webrun-http-browser 0.3.3 → 0.4.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 +406 -51
- package/demo/demo-1.html +109 -194
- package/demo/demo-2.html +118 -299
- package/dist/core/data-calls.d.ts +5 -0
- package/dist/core/data-calls.d.ts.map +1 -0
- package/dist/core/data-channels.d.ts +17 -0
- package/dist/core/data-channels.d.ts.map +1 -0
- package/dist/core/index.d.ts +6 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/message-target.d.ts +2 -0
- package/dist/core/message-target.d.ts.map +1 -0
- package/dist/core/registry.d.ts +18 -0
- package/dist/core/registry.d.ts.map +1 -0
- package/dist/http/http-send-recieve.d.ts +36 -0
- package/dist/http/http-send-recieve.d.ts.map +1 -0
- package/dist/http/index.d.ts +4 -0
- package/dist/http/index.d.ts.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2670 -807
- package/dist/relay/index-sw.d.ts +7 -0
- package/dist/relay/index-sw.d.ts.map +1 -0
- package/dist/relay/index.d.ts +69 -0
- package/dist/relay/index.d.ts.map +1 -0
- package/dist/relay/split-service-url.d.ts +13 -0
- package/dist/relay/split-service-url.d.ts.map +1 -0
- package/dist/relay-sw.d.ts +2 -0
- package/dist/relay-sw.d.ts.map +1 -0
- package/dist/relay-sw.js +1660 -0
- package/dist/sw/http-sw-dispatcher.d.ts +28 -0
- package/dist/sw/http-sw-dispatcher.d.ts.map +1 -0
- package/dist/sw/index.d.ts +3 -0
- package/dist/sw/index.d.ts.map +1 -0
- package/dist/sw/sw-dispatcher.d.ts +74 -0
- package/dist/sw/sw-dispatcher.d.ts.map +1 -0
- package/dist/sw-worker.d.ts +2 -0
- package/dist/sw-worker.d.ts.map +1 -0
- package/dist/sw-worker.js +1597 -0
- package/dist/sw.d.ts +2 -0
- package/dist/sw.d.ts.map +1 -0
- package/dist/sw.js +1883 -0
- package/package.json +51 -37
- package/public/index.html +61 -97
- package/public/index.js +7 -4
- package/public/sw-worker.js +4 -0
- package/public-relay/heartbeat.js +4 -3
- package/public-relay/relay-sw.js +4 -6
- package/public-relay/relay.html +16 -11
- package/src/core/data-calls.ts +46 -0
- package/src/core/data-channels.ts +262 -0
- package/src/core/index.ts +9 -0
- package/src/core/message-target.ts +8 -0
- package/src/core/registry.ts +54 -0
- package/src/http/http-send-recieve.ts +94 -0
- package/src/http/index.ts +7 -0
- package/src/index.ts +3 -0
- package/src/relay/index-sw.ts +145 -0
- package/src/relay/index.ts +261 -0
- package/src/relay/split-service-url.ts +30 -0
- package/src/relay-sw.ts +7 -0
- package/src/sw/http-sw-dispatcher.ts +122 -0
- package/src/sw/index.ts +2 -0
- package/src/sw/sw-dispatcher.ts +332 -0
- package/src/sw-worker.ts +7 -0
- package/src/sw.ts +1 -0
- package/CHANGELOG.md +0 -1
- package/LICENSE +0 -21
- package/demo/demo-2 copy.html +0 -598
- package/demo/demo-3.html +0 -167
- package/dist/index-sw-umd.js +0 -798
- package/dist/index-sw-umd.min.js +0 -2
- package/dist/index-sw.js +0 -791
- package/dist/index-sw.min.js +0 -2
- package/dist/index-umd.js +0 -853
- package/dist/index-umd.min.js +0 -2
- package/dist/index.min.js +0 -2
- package/index.js +0 -1
- package/src/core/data-calls.js +0 -36
- package/src/core/data-channels.js +0 -192
- package/src/core/data-send-recieve.js +0 -28
- package/src/core/errors.js +0 -13
- package/src/core/index.js +0 -4
- package/src/http/HttpError.js +0 -59
- package/src/http/http-send-recieve.js +0 -40
- package/src/http/http-stubs.js +0 -135
- package/src/http/index.js +0 -3
- package/src/http/readable-streams.js +0 -33
- package/src/index.js +0 -7
- package/src/relay/index-sw.js +0 -216
- package/src/relay/index.js +0 -263
- package/src/relay/splitServiceUrl.js +0 -24
- package/src/sw/http-sw-dispatcher.js +0 -128
- package/src/sw/sw-dispatcher.js +0 -264
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Re-exported from @statewalker/webrun-rpc, which owns the port layer, so this
|
|
2
|
+
// package's five internal importers and its public API are unchanged.
|
|
3
|
+
export type {
|
|
4
|
+
MessageListener,
|
|
5
|
+
MessageSink,
|
|
6
|
+
MessageSource,
|
|
7
|
+
MessageTarget,
|
|
8
|
+
} from "@statewalker/webrun-rpc";
|
|
@@ -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,94 @@
|
|
|
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
|
+
* Serve an `HttpHandler` over a `MessageTarget`, using this package's own
|
|
43
|
+
* `handleStreams` transport.
|
|
44
|
+
*
|
|
45
|
+
* @deprecated Prefer the port stack in `@statewalker/webrun-rpc`: open a port
|
|
46
|
+
* (`multiplexPort` over one pipe, or `transferPortMux` where the platform can
|
|
47
|
+
* transfer a real `MessagePort`), turn it into a `Duplex` with
|
|
48
|
+
* `serveDuplexOverPort`, and serve HTTP on that with
|
|
49
|
+
* `httpServe(handler, options)` from `@statewalker/webrun-http-streams`.
|
|
50
|
+
*
|
|
51
|
+
* That path has backpressure; **this one does not.** `sendStream`'s chunk
|
|
52
|
+
* sender discards the promise it is given, so a fast producer over a slow
|
|
53
|
+
* consumer accumulates without bound. It also has no per-stream timeout and no
|
|
54
|
+
* chunking to a transport's message ceiling.
|
|
55
|
+
*
|
|
56
|
+
* Kept for existing ServiceWorker setups built on the `MessageTarget` surface.
|
|
57
|
+
*/
|
|
58
|
+
export function handleHttpRequests(
|
|
59
|
+
communicationPort: MessageTarget,
|
|
60
|
+
handler: HttpHandler,
|
|
61
|
+
): () => void {
|
|
62
|
+
const serverStub = newHttpServerStub(handler);
|
|
63
|
+
return handleStreams<Uint8Array>(communicationPort, async (it) => {
|
|
64
|
+
const envelope = await httpFromIterator<SerializedHttpRequest>(it);
|
|
65
|
+
const response = await serverStub(envelope);
|
|
66
|
+
return httpToIterator(response);
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Ship a `Request` over a `MessageTarget` and await the `Response`, using this
|
|
72
|
+
* package's own `sendStream` transport.
|
|
73
|
+
*
|
|
74
|
+
* @deprecated Prefer the port stack in `@statewalker/webrun-rpc`: open a port
|
|
75
|
+
* (`multiplexPort` over one pipe, or `transferPortMux` where the platform can
|
|
76
|
+
* transfer a real `MessagePort`), turn it into a `Duplex` with
|
|
77
|
+
* `duplexOverPort`, and drive HTTP over it with `httpFetch` from
|
|
78
|
+
* `@statewalker/webrun-http-streams`.
|
|
79
|
+
*
|
|
80
|
+
* Same caveat as {@link handleHttpRequests}: the transport underneath this
|
|
81
|
+
* helper has no backpressure, no per-stream timeout, and no chunking to a
|
|
82
|
+
* transport's message ceiling. Kept for existing ServiceWorker setups.
|
|
83
|
+
*/
|
|
84
|
+
export async function sendHttpRequest(
|
|
85
|
+
communicationPort: MessageTarget,
|
|
86
|
+
request: Request,
|
|
87
|
+
): Promise<Response> {
|
|
88
|
+
const clientStub = newHttpClientStub(async (req) => {
|
|
89
|
+
return await httpFromIterator<SerializedHttpResponse>(
|
|
90
|
+
sendStream<Uint8Array>(communicationPort, httpToIterator(req)),
|
|
91
|
+
);
|
|
92
|
+
});
|
|
93
|
+
return await clientStub(request);
|
|
94
|
+
}
|
|
@@ -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,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
|
+
}
|