@statewalker/webrun-http-browser 0.1.0 → 0.2.1

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.
@@ -51,7 +51,7 @@ export function handleStreams(communicationPort, handler) {
51
51
  * * `sendAll(it : AsyncIterator) : Promise<any>` - sends all values defined by the given
52
52
  * AsyncIterator to the peer
53
53
  */
54
- export function newChannel(port) {
54
+ function newChannel(port) {
55
55
  let listeners = [];
56
56
  let iterators = [];
57
57
 
@@ -60,7 +60,10 @@ export function newChannel(port) {
60
60
  await listener(data);
61
61
  }
62
62
  };
63
- const channel = newInvokationChannel(port, notifyAll);
63
+ const channel = newInvokationChannel({
64
+ port,
65
+ handler: notifyAll,
66
+ });
64
67
 
65
68
  const start = () => channel.start();
66
69
 
@@ -98,15 +101,20 @@ export function newChannel(port) {
98
101
  };
99
102
  }
100
103
 
101
- export function newInvokationChannel(port, handler = () => {
102
- throw new Error("Handler not implemented");
104
+ export function newInvokationChannel({
105
+ port,
106
+ handler = () => {
107
+ throw new Error("Handler not implemented");
108
+ },
109
+ onError = console.error,
110
+ newCallId = () =>
111
+ newInvokationChannel.__counter = (newInvokationChannel.__counter || 0) + 1,
103
112
  }) {
104
113
  const MESSAGE_TYPE_REQUEST = "REQUEST";
105
114
  const MESSAGE_TYPE_RESPONSE = "RESPONSE";
106
115
  const EVENT_MESSAGE = "message";
107
116
 
108
117
  const requests = {};
109
- let requestCounter = 0;
110
118
  const listener = async (event) => {
111
119
  const data = event.data || {};
112
120
  const { type, callId } = data;
@@ -143,17 +151,25 @@ export function newInvokationChannel(port, handler = () => {
143
151
  };
144
152
 
145
153
  const start = async () => {
146
- port.addEventListener(EVENT_MESSAGE, listener);
147
- await port.start();
154
+ try {
155
+ port.addEventListener(EVENT_MESSAGE, listener);
156
+ port.start && await port.start();
157
+ } catch (e) {
158
+ onError(e);
159
+ }
148
160
  };
149
161
 
150
162
  const close = async () => {
151
- port.removeEventListener(EVENT_MESSAGE, listener);
152
- await port.close();
163
+ try {
164
+ port.removeEventListener(EVENT_MESSAGE, listener);
165
+ port.close && await port.close();
166
+ } catch (e) {
167
+ onError(e);
168
+ }
153
169
  };
154
170
 
155
171
  const invoke = async function (request = {}, ...transfers) {
156
- const callId = ++requestCounter;
172
+ const callId = newCallId();
157
173
  return new Promise((resolve, reject) => {
158
174
  try {
159
175
  requests[callId] = { resolve, reject };
@@ -0,0 +1,4 @@
1
+ export * from "./data-calls.js";
2
+ export * from "./data-channels.js";
3
+ export * from "./data-send-recieve.js";
4
+ export * from "./errors.js";
@@ -0,0 +1,59 @@
1
+ export class HttpError extends Error {
2
+ constructor(options) {
3
+ super(options.message);
4
+ this.status = options.status;
5
+ this.statusText = options.statusText;
6
+ }
7
+ getResponseOptions(options = {}) {
8
+ return Object.assign(this.toJson(), options);
9
+ }
10
+ toJson() {
11
+ return {
12
+ status: this.status,
13
+ statusText: this.statusText,
14
+ message: this.message,
15
+ };
16
+ }
17
+
18
+ static fromError(error) {
19
+ if (!(error instanceof HttpError)) {
20
+ error = new HttpError(Object.assign({
21
+ status: 500,
22
+ statusText: "Bad Request",
23
+ reason: error.message,
24
+ }));
25
+ }
26
+ return error;
27
+ }
28
+
29
+ static errorResourceNotFound(options = {}) {
30
+ return new HttpError({
31
+ status: 404,
32
+ statusText: "Error 404: Resource not found",
33
+ ...options,
34
+ });
35
+ }
36
+ static errorForbidden(options = {}) {
37
+ return new HttpError({
38
+ status: 403,
39
+ statusText: "Error 403: Forbidden",
40
+ ...options,
41
+ });
42
+ }
43
+
44
+ static errorResourceGone(options = {}) {
45
+ return new HttpError({
46
+ status: 410,
47
+ statusText: "Error 410: Resource Gone",
48
+ ...options,
49
+ });
50
+ }
51
+
52
+ static errorInternalError(options) {
53
+ return new HttpError({
54
+ ...options,
55
+ status: 500,
56
+ statusText: "Error 500: Internal error",
57
+ });
58
+ }
59
+ }
@@ -1,4 +1,4 @@
1
- import { handleStreams, sendStream } from "./data-channels.js";
1
+ import { handleStreams, sendStream } from "../core/data-channels.js";
2
2
  import { newHttpClientStub, newHttpServerStub } from "./http-stubs.js";
3
3
 
4
4
  async function* httpToIterator(params) {
@@ -0,0 +1,3 @@
1
+ export * from "./http-send-recieve.js";
2
+ export * from "./http-stubs.js";
3
+ export * from "./readable-streams.js";
package/src/index.js CHANGED
@@ -1,12 +1,7 @@
1
- export * from "./data-calls.js";
2
- export * from "./data-channels.js";
3
- export * from "./data-send-recieve.js";
4
- export * from "./errors.js";
5
- export * from "./http-send-recieve.js";
6
- export * from "./http-stubs.js";
7
- export * from "./http-sw-dispatcher.js";
8
- export * from "./readable-streams.js";
9
- export * from "./sw-dispatcher.js";
1
+ export * from "./core/index.js"
2
+ export * from "./http/index.js";
3
+ // export * from "./sw/index.js";
4
+ export * from "./relay/index.js";
10
5
 
11
6
  import { newRegistry } from "@statewalker/utils";
12
7
  export { newRegistry };
@@ -0,0 +1,216 @@
1
+ import { newRegistry } from "@statewalker/utils";
2
+ import { get, set } from "idb-keyval";
3
+ import { callChannel, handleChannelCalls } from "../core/data-calls.js";
4
+ import { sendHttpRequest } from "../http/http-send-recieve.js";
5
+ import { splitServiceUrl } from "./splitServiceUrl.js";
6
+ import { HttpError } from "../http/HttpError.js";
7
+
8
+ export function startRelayServiceWorker(self) {
9
+ const [register, clear] = newRegistry();
10
+
11
+ if (typeof self.skipWaiting === "function") {
12
+ console.log("self.skipWaiting() is supported.");
13
+ self.addEventListener("install", (e) => {
14
+ // See https://slightlyoff.github.io/ServiceWorker/spec/service_worker/index.html#service-worker-global-scope-skipwaiting
15
+ e.waitUntil(self.skipWaiting());
16
+ });
17
+ } else {
18
+ console.log("self.skipWaiting() is not supported.");
19
+ }
20
+
21
+ if (self.clients && (typeof self.clients.claim === "function")) {
22
+ console.log("self.clients.claim() is supported.");
23
+ self.addEventListener("activate", (e) => {
24
+ // See https://slightlyoff.github.io/ServiceWorker/spec/service_worker/index.html#clients-claim-method
25
+ e.waitUntil(self.clients.claim());
26
+ });
27
+ } else {
28
+ console.log("self.clients.claim() is not supported.");
29
+ }
30
+
31
+ const clientsRegistry = newClientsRegistry({
32
+ self,
33
+ get,
34
+ set,
35
+ });
36
+
37
+ // This listener handles the following message types:
38
+ // * registration: registers new clients as service providers
39
+ // * handles service calls rounting:
40
+ // - it gets the clientKey defined in the call
41
+ // - it uses the serviceKey to get the service-specifc channel
42
+ // - it creates a new message channel and sent its ports to peers
43
+
44
+ register(handleChannelCalls(self, "REGISTER", async (event, data) => {
45
+ return await clientsRegistry.addClient(data.key, event.source);
46
+ }));
47
+ register(handleChannelCalls(self, "UNREGISTER", async (event, data) => {
48
+ return await clientsRegistry.removeClient(data.key);
49
+ }));
50
+
51
+ // Transfers all parameters (and ports) to the target client.
52
+ register(handleChannelCalls(self, "CONNECT", async (event, data, port) => {
53
+ const { key } = data;
54
+ const client = await clientsRegistry.getClient(key);
55
+ if (!client) {
56
+ throw new Error(`Target client was not found. Target key: "${key}".`);
57
+ }
58
+ return await callChannel(client, "CONNECT", data, port);
59
+ }));
60
+
61
+ const fetchListener = (event) => {
62
+ const request = event.request;
63
+ const params = splitServiceUrl(request.url);
64
+ const { key } = params;
65
+ if (key) {
66
+ const handler = async () => {
67
+ try {
68
+ const channel = new MessageChannel();
69
+ const client = await clientsRegistry.getClient(key);
70
+ if (!client) {
71
+ throw HttpError.errorResourceGone(params);
72
+ }
73
+ const data = {
74
+ type: "http",
75
+ key,
76
+ };
77
+ if (!await callChannel(client, "CONNECT", data, channel.port2)) {
78
+ throw HttpError.errorForbidden(params);
79
+ }
80
+ return await sendHttpRequest(channel.port1, request);
81
+ } catch (error) {
82
+ error = HttpError.fromError(error);
83
+ const options = error.getResponseOptions(params);
84
+ return new Response(JSON.stringify(options), {
85
+ ...options,
86
+ headers: {
87
+ "Content-Type": "text/javascript",
88
+ },
89
+ });
90
+ }
91
+ };
92
+ event.respondWith(handler());
93
+ }
94
+ };
95
+ self.addEventListener("fetch", fetchListener);
96
+ register(() => self.removeEventListener("fetch", fetchListener));
97
+
98
+ return clear;
99
+ }
100
+
101
+ // --------------------------------------------
102
+ // errors.js
103
+
104
+ // --------------------------------------------
105
+
106
+ function newClientsRegistry({
107
+ self,
108
+ key = "clientsIds",
109
+ get,
110
+ set,
111
+ }) {
112
+ let _index;
113
+ async function loadClientsIndex() {
114
+ if (!_index) {
115
+ const entries = (await get(key)) || [];
116
+ _index = Object.fromEntries(entries);
117
+ }
118
+ return _index;
119
+ }
120
+ async function storeClientsIndex() {
121
+ const index = await loadClientsIndex();
122
+ const entries = Object.entries(index);
123
+ await set(key, entries);
124
+ return index;
125
+ }
126
+
127
+ async function addClient(clientKey, client) {
128
+ const index = await loadClientsIndex();
129
+ const clientId = client.id;
130
+ let result = false;
131
+ if (!(clientId in index)) {
132
+ index[clientKey] = clientId;
133
+ await storeClientsIndex();
134
+ result = true;
135
+ }
136
+ return result;
137
+ }
138
+
139
+ async function removeClient(clientKey) {
140
+ const index = await loadClientsIndex();
141
+ let result = false;
142
+ if (clientKey in index) {
143
+ delete index[clientKey];
144
+ await storeClientsIndex();
145
+ result = true;
146
+ }
147
+ return result;
148
+ }
149
+
150
+ async function getClient(clientKey) {
151
+ const index = await loadClientsIndex();
152
+ const clientId = index[clientKey];
153
+ const client = await self.clients.get(clientId);
154
+ if (!client) {
155
+ delete index[clientKey];
156
+ await storeClientsIndex();
157
+ }
158
+ return client;
159
+ }
160
+
161
+ return {
162
+ getClient,
163
+ addClient,
164
+ removeClient,
165
+ };
166
+ }
167
+ // ----------------------------------------------------
168
+ // Content of the "idb-keyval" package
169
+ // import { get, set } from "idb-keyval";
170
+
171
+ // function promisifyRequest(request) {
172
+ // return new Promise((resolve, reject) => {
173
+ // // @ts-ignore - file size hacks
174
+ // request.oncomplete = request.onsuccess = () => resolve(request.result);
175
+ // // @ts-ignore - file size hacks
176
+ // request.onabort = request.onerror = () => reject(request.error);
177
+ // });
178
+ // }
179
+ // function createStore(dbName, storeName) {
180
+ // const request = indexedDB.open(dbName);
181
+ // request.onupgradeneeded = () => request.result.createObjectStore(storeName);
182
+ // const dbp = promisifyRequest(request);
183
+ // return (txMode, callback) =>
184
+ // dbp.then((db) =>
185
+ // callback(db.transaction(storeName, txMode).objectStore(storeName))
186
+ // );
187
+ // }
188
+ // let defaultGetStoreFunc;
189
+ // function defaultGetStore() {
190
+ // if (!defaultGetStoreFunc) {
191
+ // defaultGetStoreFunc = createStore("keyval-store", "keyval");
192
+ // }
193
+ // return defaultGetStoreFunc;
194
+ // }
195
+ // /**
196
+ // * Get a value by its key.
197
+ // *
198
+ // * @param key
199
+ // * @param customStore Method to get a custom store. Use with caution (see the docs).
200
+ // */
201
+ // function get(key, customStore = defaultGetStore()) {
202
+ // return customStore("readonly", (store) => promisifyRequest(store.get(key)));
203
+ // }
204
+ // /**
205
+ // * Set a value with a key.
206
+ // *
207
+ // * @param key
208
+ // * @param value
209
+ // * @param customStore Method to get a custom store. Use with caution (see the docs).
210
+ // */
211
+ // function set(key, value, customStore = defaultGetStore()) {
212
+ // return customStore("readwrite", (store) => {
213
+ // store.put(value, key);
214
+ // return promisifyRequest(store.transaction);
215
+ // });
216
+ // }
@@ -1,10 +1,10 @@
1
+ import { newRegistry } from "@statewalker/utils";
2
+ import { callChannel, handleChannelCalls } from "../core/data-calls.js";
1
3
  import {
2
- callChannel,
3
- handleChannelCalls,
4
4
  handleHttpRequests,
5
- newRegistry,
6
5
  sendHttpRequest,
7
- } from "../dist/index.js";
6
+ } from "../http/http-send-recieve.js";
7
+ export * from "./splitServiceUrl.js";
8
8
 
9
9
  export function newServiceWorkerPort() {
10
10
  // Bind the service worker to
@@ -18,13 +18,14 @@ export function newServiceWorkerPort() {
18
18
  return channel.port2;
19
19
  }
20
20
 
21
- export async function initServiceWorker(swUrl) {
21
+ export async function initServiceWorker1({ swUrl, scopeUrl, type }) {
22
22
  const {
23
23
  installing,
24
24
  waiting,
25
25
  active,
26
26
  } = await navigator.serviceWorker.register(swUrl, {
27
- type: "module",
27
+ // type: "module",
28
+ scope: scopeUrl,
28
29
  });
29
30
  (installing || waiting || active).addEventListener("statechange", (e) => {
30
31
  console.log("state", e.target.state);
@@ -59,6 +60,50 @@ export async function initServiceWorker(swUrl) {
59
60
  });
60
61
  }
61
62
 
63
+ export async function initServiceWorker({ swUrl, scopeUrl, type }) {
64
+ await navigator.serviceWorker.register(swUrl, {
65
+ type,
66
+ scope: scopeUrl,
67
+ });
68
+ const worker = await getServiceWorkerController();
69
+ await awaitServiceWorkerActivation(worker);
70
+ return worker;
71
+
72
+ async function getServiceWorkerController() {
73
+ return await new Promise((resolve) => {
74
+ // Resolve right away if this page is already controlled.
75
+ const serviceWorker = navigator.serviceWorker;
76
+ if (serviceWorker.controller) {
77
+ resolve(serviceWorker.controller);
78
+ } else {
79
+ const onChange = () => {
80
+ resolve(serviceWorker.controller);
81
+ serviceWorker.removeEventListener("controllerchange", onChange);
82
+ serviceWorker.oncontrollerchange = null;
83
+ };
84
+ serviceWorker.addEventListener("controllerchange", onChange);
85
+ serviceWorker.oncontrollerchange = onChange;
86
+ }
87
+ });
88
+ }
89
+
90
+ // ------------------------------
91
+ // Await until the service is ready
92
+ async function awaitServiceWorkerActivation(worker) {
93
+ const isActivated = () => worker.state === "activated";
94
+ await new Promise(async (resolve) => {
95
+ if (isActivated()) resolve();
96
+ else {
97
+ worker.onstatechange = () => {
98
+ if (!isActivated()) return;
99
+ worker.onstatechange = null;
100
+ resolve();
101
+ };
102
+ }
103
+ });
104
+ }
105
+ }
106
+
62
107
  export async function initHttpService(handler, {
63
108
  key,
64
109
  port,
@@ -85,9 +130,38 @@ export async function callHttpService(request, {
85
130
  return result;
86
131
  }
87
132
 
133
+ export function getRelayWindowMessageHandler({
134
+ swUrl = new URL("./index-sw.js", import.meta.url) + "",
135
+ scopeUrl = new URL("../", import.meta.url) + "",
136
+ } = {}) {
137
+ let externalPort;
138
+ return async (ev) => {
139
+ if (ev.data.type === "CONNECT") {
140
+ const newExternalPort = (ev.ports || [])[0];
141
+ if (externalPort) {
142
+ newExternalPort.close();
143
+ return;
144
+ }
145
+ externalPort = newExternalPort;
146
+ await initServiceWorker({
147
+ swUrl,
148
+ scopeUrl,
149
+ });
150
+ const serviceWorkerPort = newServiceWorkerPort();
151
+ serviceWorkerPort.onmessage = (event) => {
152
+ externalPort.postMessage(event.data, event.ports);
153
+ };
154
+
155
+ externalPort.onmessage = (event) => {
156
+ serviceWorkerPort.postMessage(event.data, event.ports);
157
+ };
158
+ }
159
+ };
160
+ }
161
+
88
162
  export async function newRemoteRelayChannel({
89
- baseUrl = new URL("./", import.meta.url),
90
- url = new URL("./relay.html", baseUrl),
163
+ baseUrl = new URL("../public-relay/", import.meta.url),
164
+ url = new URL("relay.html", baseUrl),
91
165
  container = document.body,
92
166
  } = {}) {
93
167
  const messageChannel = new MessageChannel();
@@ -0,0 +1,24 @@
1
+ export function splitServiceUrl(url, separator = "~") {
2
+ let key = "";
3
+ let path = "";
4
+ let str = url + "";
5
+ const idx = str.indexOf(separator);
6
+ let baseUrl = "";
7
+ if (idx >= 0) {
8
+ baseUrl = str.substring(0, idx + separator.length);
9
+ str.substring(idx + separator.length)
10
+ .replace(/^([^\/]+)/, (match, $1) => {
11
+ baseUrl += match;
12
+ if (baseUrl.length < str.length) baseUrl += "/";
13
+ key = $1;
14
+ path = str.substring(baseUrl.length);
15
+ return "";
16
+ });
17
+ }
18
+ return {
19
+ url,
20
+ key,
21
+ baseUrl,
22
+ path,
23
+ };
24
+ }
@@ -1,4 +1,4 @@
1
- import { handleHttpRequests, sendHttpRequest } from "./http-send-recieve.js";
1
+ import { handleHttpRequests, sendHttpRequest } from "../http/http-send-recieve.js";
2
2
  import { SwPortDispatcher, SwPortHandler } from "./sw-dispatcher.js";
3
3
 
4
4
  export class SwHttpAdapter extends SwPortHandler {
@@ -1,6 +1,6 @@
1
1
  import { newRegistry } from "@statewalker/utils";
2
- import { callChannel, handleChannelCalls } from "./data-calls.js";
3
2
  import { get, set } from "idb-keyval";
3
+ import { callChannel, handleChannelCalls } from "../core/data-calls.js";
4
4
 
5
5
  export class SwPortHandler {
6
6
  constructor(options) {
package/public/abc.txt DELETED
@@ -1 +0,0 @@
1
- qlsdjflqsjdfl
@@ -1 +0,0 @@
1
- importScripts("../dist/index-sw-umd.js")
File without changes
File without changes
File without changes