@statewalker/webrun-http-browser 0.1.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.
@@ -0,0 +1,343 @@
1
+ import {
2
+ // serializeError, deserializeError,
3
+ callChannel,
4
+ handleChannelCalls,
5
+ newRegistry,
6
+ sendHttpRequest,
7
+ } from "../dist/index.js";
8
+ import { registerConnectionsHandler } from "./relay-lib.js";
9
+
10
+ if (typeof self.skipWaiting === "function") {
11
+ console.log("self.skipWaiting() is supported.");
12
+ self.addEventListener("install", (e) => {
13
+ // See https://slightlyoff.github.io/ServiceWorker/spec/service_worker/index.html#service-worker-global-scope-skipwaiting
14
+ e.waitUntil(self.skipWaiting());
15
+ });
16
+ } else {
17
+ console.log("self.skipWaiting() is not supported.");
18
+ }
19
+
20
+ if (self.clients && (typeof self.clients.claim === "function")) {
21
+ console.log("self.clients.claim() is supported.");
22
+ self.addEventListener("activate", (e) => {
23
+ // See https://slightlyoff.github.io/ServiceWorker/spec/service_worker/index.html#clients-claim-method
24
+ e.waitUntil(self.clients.claim());
25
+ });
26
+ } else {
27
+ console.log("self.clients.claim() is not supported.");
28
+ }
29
+
30
+ const baseUrl = new URL("./", import.meta.url) + "";
31
+ const clientsRegistry = newClientsRegistry(self);
32
+
33
+ // This listener handles the following message types:
34
+ // * registration: registers new clients as service providers
35
+ // * handles service calls rounting:
36
+ // - it gets the clientKey defined in the call
37
+ // - it uses the serviceKey to get the service-specifc channel
38
+ // - it creates a new message channel and sent its ports to peers
39
+
40
+ const [register, clear] = newRegistry();
41
+
42
+ register(handleChannelCalls(self, "REGISTER", async (event, data) => {
43
+ return await clientsRegistry.addClient(data.key, event.source);
44
+ }));
45
+ register(handleChannelCalls(self, "UNREGISTER", async (event, data) => {
46
+ return await clientsRegistry.removeClient(data.key);
47
+ }));
48
+
49
+ // Transfers all parameters (and ports) to the target client.
50
+ register(handleChannelCalls(self, "CONNECT", async (event, data, port) => {
51
+ const { key } = data;
52
+ const client = await clientsRegistry.getClient(key);
53
+ if (!client) {
54
+ throw new Error(`Target client was not found. Target key: "${key}".`);
55
+ }
56
+ return await callChannel(client, "CONNECT", data, port);
57
+ }));
58
+
59
+ function splitServiceUrl(url, separator = "~") {
60
+ let key = "";
61
+ let path = "";
62
+ let str = url + "";
63
+ const idx = str.indexOf(separator);
64
+ let baseUrl = "";
65
+ if (idx >= 0) {
66
+ baseUrl = str.substring(0, idx + separator.length);
67
+ str.substring(idx + separator.length)
68
+ .replace(/^([^\/]+)/, (match, $1) => {
69
+ baseUrl += match;
70
+ if (baseUrl.length < str.length) { baseUrl += "/"; }
71
+ key = $1;
72
+ path = str.substring(baseUrl.length);
73
+ return "";
74
+ });
75
+ }
76
+ return {
77
+ url,
78
+ key,
79
+ baseUrl,
80
+ path
81
+ };
82
+ }
83
+ self.addEventListener("fetch", (event) => {
84
+ const request = event.request;
85
+ const params = splitServiceUrl(request.url);
86
+ const { key } = params;
87
+ if (key) {
88
+ event.respondWith((async () => {
89
+ try {
90
+ const channel = new MessageChannel();
91
+ const client = await clientsRegistry.getClient(key);
92
+ if (!client) {
93
+ throw HttpError.errorResourceGone(params);
94
+ }
95
+ const data = {
96
+ type: "http",
97
+ key,
98
+ };
99
+ if (!await callChannel(client, "CONNECT", data, channel.port2)) {
100
+ throw HttpError.errorForbidden(params);
101
+ }
102
+ return await sendHttpRequest(channel.port1, request);
103
+ } catch (error) {
104
+ error = HttpError.fromError(error);
105
+ const options = error.getResponseOptions(params);
106
+ return new Response(JSON.stringify(options), {
107
+ ...options,
108
+ headers: {
109
+ "Content-Type": "text/javascript",
110
+ },
111
+ });
112
+ }
113
+ })());
114
+ }
115
+ });
116
+ // self.onmessage = (event) => {
117
+ // console.log("SW MESSAGE:", event, event.ports);
118
+ // const [port] = event.ports || [];
119
+ // if (port) {
120
+ // port.onmessage = (ev) => {
121
+ // // const data = ev.data || {};
122
+ // console.log("Reply from the other side of the moon", ev);
123
+ // // port.postMessage({
124
+ // // message: "Reply from the other side",
125
+ // // ...data
126
+ // // })
127
+ // };
128
+ // port.postMessage({ message: "Message from the Service Worker HELL!" });
129
+ // }
130
+ // };
131
+
132
+ class HttpError extends Error {
133
+ constructor(options) {
134
+ super(options.message);
135
+ this.status = options.status;
136
+ this.statusText = options.statusText;
137
+ }
138
+ getResponseOptions(options = {}) {
139
+ return Object.assign(this.toJson(), options);
140
+ }
141
+ toJson() {
142
+ return {
143
+ status: this.status,
144
+ statusText: this.statusText,
145
+ message: this.message,
146
+ };
147
+ }
148
+
149
+ static fromError(error) {
150
+ if (!(error instanceof HttpError)) {
151
+ error = new HttpError(Object.assign({
152
+ status: 500,
153
+ statusText: "Bad Request",
154
+ reason: error.message,
155
+ }));
156
+ }
157
+ return error;
158
+ }
159
+
160
+ static errorResourceNotFound(options = {}) {
161
+ return new HttpError({
162
+ status: 404,
163
+ statusText: "Error 404: Resource not found",
164
+ ...options,
165
+ });
166
+ }
167
+ static errorForbidden(options = {}) {
168
+ return new HttpError({
169
+ status: 403,
170
+ statusText: "Error 403: Forbidden",
171
+ ...options,
172
+ });
173
+ }
174
+
175
+ static errorResourceGone(options = {}) {
176
+ return new HttpError({
177
+ status: 410,
178
+ statusText: "Error 410: Resource Gone",
179
+ ...options,
180
+ });
181
+ }
182
+
183
+ static errorInternalError(options) {
184
+ return new HttpError({
185
+ ...options,
186
+ status: 500,
187
+ statusText: "Error 500: Internal error",
188
+ });
189
+ }
190
+ }
191
+
192
+ // self.addEventListener("fetch", (event) => {
193
+ // console.log("fetching resource", event);
194
+ // const request = event.request;
195
+ // const requestUrl = request.url;
196
+ // if (requestUrl.match(/\/ping$/)) {
197
+ // event.respondWith((async () => {
198
+ // return new Response("pong", { status: 200 });
199
+ // })());
200
+ // } else if (requestUrl.indexOf(baseUrl) === 0) {
201
+ // const clientKey = requestUrl
202
+ // .substring(baseUrl.length)
203
+ // .replace(/^\/?([^\/]+).*$/, "$1");
204
+ // if (clientKey) {
205
+ // event.respondWith((async () => {
206
+ // try {
207
+ // const client = await clientsRegistry.getClient(clientKey);
208
+ // if (!client) { throw HttpError.errorResourceGone(); }
209
+ // const port = portsRegistry.getClientPort(client, "http");
210
+
211
+ // return new Response("pong", { status: 200 });
212
+ // } catch (error) {
213
+ // if (!(error instanceof HttpError)) {
214
+ // error = HttpError.errorInternalError(error);
215
+ // }
216
+ // console.error(error);
217
+ // return new Response(null, error.getResponseOptions());
218
+ // }
219
+ // })());
220
+ // }
221
+ // }
222
+ // // const response = new Response(
223
+ // // `<p>This is a response that comes from your service worker! ${Date.now()}</p>`,
224
+ // // {
225
+ // // headers: { "Content-Type": "text/html" },
226
+ // // },
227
+ // // );
228
+ // // event.respondWith(response);
229
+ // });
230
+
231
+ // --------------------------------------------
232
+ // errors.js
233
+
234
+ // --------------------------------------------
235
+
236
+ function newClientsRegistry(self) {
237
+ const KEY = "clientsIds";
238
+
239
+ let _index;
240
+ async function loadClientsIndex() {
241
+ if (!_index) {
242
+ const entries = (await get(KEY)) || [];
243
+ _index = Object.fromEntries(entries);
244
+ }
245
+ return _index;
246
+ }
247
+ async function storeClientsIndex() {
248
+ const index = await loadClientsIndex();
249
+ const entries = Object.entries(index);
250
+ await set(KEY, entries);
251
+ return index;
252
+ }
253
+
254
+ async function addClient(clientKey, client) {
255
+ const index = await loadClientsIndex();
256
+ const clientId = client.id;
257
+ let result = false;
258
+ if (!(clientId in index)) {
259
+ index[clientKey] = clientId;
260
+ await storeClientsIndex();
261
+ result = true;
262
+ }
263
+ return result;
264
+ }
265
+
266
+ async function removeClient(clientKey) {
267
+ const index = await loadClientsIndex();
268
+ let result = false;
269
+ if (clientKey in index) {
270
+ delete index[clientKey];
271
+ await storeClientsIndex();
272
+ result = true;
273
+ }
274
+ return result;
275
+ }
276
+
277
+ async function getClient(clientKey) {
278
+ const index = await loadClientsIndex();
279
+ const clientId = index[clientKey];
280
+ const client = await self.clients.get(clientId);
281
+ if (!client) {
282
+ delete index[clientKey];
283
+ await storeClientsIndex();
284
+ }
285
+ return client;
286
+ }
287
+
288
+ return {
289
+ getClient,
290
+ addClient,
291
+ removeClient,
292
+ };
293
+ }
294
+ // ----------------------------------------------------
295
+ // Content of the "idb-keyval" package
296
+ // import { get, set } from "idb-keyval";
297
+
298
+ function promisifyRequest(request) {
299
+ return new Promise((resolve, reject) => {
300
+ // @ts-ignore - file size hacks
301
+ request.oncomplete = request.onsuccess = () => resolve(request.result);
302
+ // @ts-ignore - file size hacks
303
+ request.onabort = request.onerror = () => reject(request.error);
304
+ });
305
+ }
306
+ function createStore(dbName, storeName) {
307
+ const request = indexedDB.open(dbName);
308
+ request.onupgradeneeded = () => request.result.createObjectStore(storeName);
309
+ const dbp = promisifyRequest(request);
310
+ return (txMode, callback) =>
311
+ dbp.then((db) =>
312
+ callback(db.transaction(storeName, txMode).objectStore(storeName))
313
+ );
314
+ }
315
+ let defaultGetStoreFunc;
316
+ function defaultGetStore() {
317
+ if (!defaultGetStoreFunc) {
318
+ defaultGetStoreFunc = createStore("keyval-store", "keyval");
319
+ }
320
+ return defaultGetStoreFunc;
321
+ }
322
+ /**
323
+ * Get a value by its key.
324
+ *
325
+ * @param key
326
+ * @param customStore Method to get a custom store. Use with caution (see the docs).
327
+ */
328
+ function get(key, customStore = defaultGetStore()) {
329
+ return customStore("readonly", (store) => promisifyRequest(store.get(key)));
330
+ }
331
+ /**
332
+ * Set a value with a key.
333
+ *
334
+ * @param key
335
+ * @param value
336
+ * @param customStore Method to get a custom store. Use with caution (see the docs).
337
+ */
338
+ function set(key, value, customStore = defaultGetStore()) {
339
+ return customStore("readwrite", (store) => {
340
+ store.put(value, key);
341
+ return promisifyRequest(store.transaction);
342
+ });
343
+ }
@@ -0,0 +1,33 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+
4
+ <body>
5
+ <script type="module">
6
+ import { newServiceWorkerPort, initServiceWorker } from "./relay-lib.js";
7
+
8
+ const swUrl = new URL("./relay-sw.js", import.meta.url) + "";
9
+
10
+ let externalPort;
11
+ window.onmessage = async (ev) => {
12
+ if (ev.data.type === "CONNECT") {
13
+ const newExternalPort = (ev.ports || [])[0];
14
+ if (externalPort) {
15
+ newExternalPort.close();
16
+ return;
17
+ }
18
+ externalPort = newExternalPort;
19
+ await initServiceWorker(swUrl);
20
+ const serviceWorkerPort = newServiceWorkerPort();
21
+ serviceWorkerPort.onmessage = (event) => {
22
+ externalPort.postMessage(event.data, event.ports);
23
+ };
24
+
25
+ externalPort.onmessage = (event) => {
26
+ serviceWorkerPort.postMessage(event.data, event.ports);
27
+ };
28
+ }
29
+ };
30
+ </script>
31
+ </body>
32
+
33
+ </html>
@@ -0,0 +1,36 @@
1
+ import { deserializeError, serializeError } from "./errors.js";
2
+
3
+ export async function callChannel(mainChannel, callType, params, ...transfers) {
4
+ return new Promise((resolve, reject) => {
5
+ const channel = new MessageChannel();
6
+ channel.port1.onmessage = (ev) => {
7
+ const { result, error } = ev.data;
8
+ if (error) reject(deserializeError(error));
9
+ else resolve(result);
10
+ };
11
+ mainChannel.postMessage({ type: callType, params }, [
12
+ channel.port2,
13
+ ...transfers,
14
+ ]);
15
+ });
16
+ }
17
+
18
+ export function handleChannelCalls(mainChannel, callType, handler) {
19
+ const messageListener = async (event) => {
20
+ if (!event.data || event.data.type !== callType) return;
21
+ const [port, ...transfers] = event.ports;
22
+ let response = {};
23
+ try {
24
+ const params = event.data.params;
25
+ response.result = await handler(event, params, ...transfers);
26
+ } catch (error) {
27
+ response.error = serializeError(error);
28
+ }
29
+ port.postMessage(response);
30
+ };
31
+ mainChannel.addEventListener("message", messageListener);
32
+ try {
33
+ mainChannel.start && mainChannel.start();
34
+ } catch (e) {}
35
+ return () => mainChannel.removeEventListener("message", messageListener);
36
+ }
@@ -0,0 +1,176 @@
1
+ import { deserializeError, serializeError } from "./errors.js";
2
+ import { recieveData, sendData } from "./data-send-recieve.js";
3
+
4
+ export async function* sendStream(communicationPort, input, params = {}) {
5
+ const messageChannel = new MessageChannel();
6
+ communicationPort.postMessage({ type: "START_CALL", params }, [
7
+ messageChannel.port2,
8
+ ]);
9
+
10
+ const channel = newChannel(messageChannel.port1);
11
+ try {
12
+ await channel.start();
13
+ channel.sendAll(input);
14
+ yield* channel.recieveAll();
15
+ } finally {
16
+ await channel.close();
17
+ }
18
+ }
19
+
20
+ export function handleStreams(communicationPort, handler) {
21
+ const listener = async (event) => {
22
+ const { type, params } = event.data || {};
23
+ if (type !== "START_CALL") return;
24
+ const port = event.ports[0];
25
+ const channel = newChannel(port);
26
+ try {
27
+ await channel.start();
28
+ const input = channel.recieveAll();
29
+ const response = await handler(input, params);
30
+ await channel.sendAll(response);
31
+ } finally {
32
+ await channel.close();
33
+ }
34
+ };
35
+ communicationPort.addEventListener("message", listener);
36
+ try {
37
+ communicationPort.start && communicationPort.start();
38
+ } catch (e) {}
39
+ return () => communicationPort.removeEventListener("message", listener);
40
+ }
41
+
42
+ /**
43
+ * This method creates and returns methods allowing to send and recieve data over the specified port.
44
+ * The main characteristics of the returned methods is that they properly manage backpressure - sender
45
+ * iterators are waiting to send the next value until the previous one is consumed on the other side.
46
+ * @param {*} port
47
+ * @returns an object with the following methods:
48
+ * * `start` - starts the connection over the specified port
49
+ * * `close`- stops all iterators and closes the port
50
+ * * `recieveAll() : AsyncIterator` - returns an AsyncIterator over all values send by the peer
51
+ * * `sendAll(it : AsyncIterator) : Promise<any>` - sends all values defined by the given
52
+ * AsyncIterator to the peer
53
+ */
54
+ export function newChannel(port) {
55
+ let listeners = [];
56
+ let iterators = [];
57
+
58
+ const notifyAll = async (data) => {
59
+ for (let listener of listeners) {
60
+ await listener(data);
61
+ }
62
+ };
63
+ const channel = newInvokationChannel(port, notifyAll);
64
+
65
+ const start = () => channel.start();
66
+
67
+ const close = async () => {
68
+ await notifyAll({ done: true });
69
+ [...iterators].forEach((it) => it.return && it.return());
70
+ await channel.close();
71
+ };
72
+
73
+ async function* recieveAll() {
74
+ yield* recieveData((listener) => {
75
+ listeners.push(listener);
76
+ return () => listeners = listeners.filter((l) => l !== listener);
77
+ });
78
+ }
79
+
80
+ async function sendAll(it) {
81
+ await sendData(
82
+ channel.invoke,
83
+ (async function* () {
84
+ try {
85
+ iterators.push(it);
86
+ yield* it;
87
+ } finally {
88
+ iterators = iterators.filter((i) => i !== it);
89
+ }
90
+ })(),
91
+ );
92
+ }
93
+ return {
94
+ start,
95
+ close,
96
+ recieveAll,
97
+ sendAll,
98
+ };
99
+ }
100
+
101
+ export function newInvokationChannel(port, handler = () => {
102
+ throw new Error("Handler not implemented");
103
+ }) {
104
+ const MESSAGE_TYPE_REQUEST = "REQUEST";
105
+ const MESSAGE_TYPE_RESPONSE = "RESPONSE";
106
+ const EVENT_MESSAGE = "message";
107
+
108
+ const requests = {};
109
+ let requestCounter = 0;
110
+ const listener = async (event) => {
111
+ const data = event.data || {};
112
+ const { type, callId } = data;
113
+ if (type === MESSAGE_TYPE_REQUEST) {
114
+ try {
115
+ const { request } = data;
116
+ let result = await handler(request, ...(event.ports || []));
117
+ const [response, ...transfers] = Array.isArray(result)
118
+ ? result
119
+ : result !== undefined
120
+ ? [result]
121
+ : [];
122
+ port.postMessage(
123
+ { type: MESSAGE_TYPE_RESPONSE, callId, response },
124
+ transfers,
125
+ );
126
+ } catch (error) {
127
+ port.postMessage({
128
+ type: MESSAGE_TYPE_RESPONSE,
129
+ callId,
130
+ error: serializeError(error),
131
+ });
132
+ }
133
+ } else if (type === MESSAGE_TYPE_RESPONSE) {
134
+ const { callId, response, error } = data;
135
+ const request = requests[callId];
136
+ delete requests[callId];
137
+ if (request) {
138
+ error
139
+ ? request.reject(deserializeError(error))
140
+ : request.resolve(response);
141
+ }
142
+ }
143
+ };
144
+
145
+ const start = async () => {
146
+ port.addEventListener(EVENT_MESSAGE, listener);
147
+ await port.start();
148
+ };
149
+
150
+ const close = async () => {
151
+ port.removeEventListener(EVENT_MESSAGE, listener);
152
+ await port.close();
153
+ };
154
+
155
+ const invoke = async function (request = {}, ...transfers) {
156
+ const callId = ++requestCounter;
157
+ return new Promise((resolve, reject) => {
158
+ try {
159
+ requests[callId] = { resolve, reject };
160
+ port.postMessage(
161
+ { type: MESSAGE_TYPE_REQUEST, callId, request },
162
+ transfers,
163
+ );
164
+ } catch (error) {
165
+ reject(error);
166
+ delete request[callId];
167
+ }
168
+ });
169
+ };
170
+
171
+ return {
172
+ start,
173
+ close,
174
+ invoke,
175
+ };
176
+ }
@@ -0,0 +1,28 @@
1
+ import { iterate } from "@statewalker/utils";
2
+
3
+ export async function* recieveData(onMessage) {
4
+ yield* iterate((iterator) => {
5
+ return onMessage(async ({ done = true, value, error } = {}) => {
6
+ if (error) {
7
+ await iterator.error(error);
8
+ } else if (done) {
9
+ await iterator.complete();
10
+ } else {
11
+ await iterator.next(value);
12
+ }
13
+ });
14
+ });
15
+ }
16
+
17
+ export async function sendData(send, it) {
18
+ let error;
19
+ try {
20
+ for await (let value of it) {
21
+ await send({ done: false, value });
22
+ }
23
+ } catch (err) {
24
+ error = err;
25
+ } finally {
26
+ await send({ done: true, error });
27
+ }
28
+ }
package/src/errors.js ADDED
@@ -0,0 +1,13 @@
1
+ export function serializeError(error) {
2
+ return Object.assign({
3
+ message: error,
4
+ stack: error.stack,
5
+ }, error);
6
+ }
7
+
8
+ export function deserializeError(error) {
9
+ if (typeof error === "string") {
10
+ error = { message: error };
11
+ }
12
+ return Object.assign(new Error(error.message), error);
13
+ }
@@ -0,0 +1,40 @@
1
+ import { handleStreams, sendStream } from "./data-channels.js";
2
+ import { newHttpClientStub, newHttpServerStub } from "./http-stubs.js";
3
+
4
+ async function* httpToIterator(params) {
5
+ const { options, content } = await params;
6
+ const encoder = new TextEncoder();
7
+ yield encoder.encode(JSON.stringify(options));
8
+ yield* content;
9
+ }
10
+
11
+ async function httpFromIterator(it) {
12
+ it = await it;
13
+ const { done, value } = await it.next();
14
+ let options = {};
15
+ if (!done && value) {
16
+ const decoder = new TextDecoder();
17
+ const str = decoder.decode(value);
18
+ options = JSON.parse(str);
19
+ }
20
+ return {
21
+ options,
22
+ content: it,
23
+ };
24
+ }
25
+
26
+ export function handleHttpRequests(communicationPort, handler) {
27
+ const serverStub = newHttpServerStub(handler);
28
+ return handleStreams(communicationPort, (it) => {
29
+ return httpToIterator(serverStub(httpFromIterator(it)));
30
+ });
31
+ }
32
+
33
+ export async function sendHttpRequest(communicationPort, request) {
34
+ const clientStub = newHttpClientStub(async (req) => {
35
+ return await httpFromIterator(
36
+ sendStream(communicationPort, httpToIterator(req)),
37
+ );
38
+ });
39
+ return await clientStub(request);
40
+ }