@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.
- package/CHANGELOG.md +1 -0
- package/LICENSE +21 -0
- package/README.md +5 -0
- package/dist/index-sw-umd.js +651 -0
- package/dist/index-sw-umd.min.js +2 -0
- package/dist/index-sw.js +643 -0
- package/dist/index-sw.min.js +2 -0
- package/dist/index-umd.js +974 -0
- package/dist/index-umd.min.js +2 -0
- package/dist/index.js +945 -0
- package/dist/index.min.js +2 -0
- package/index.js +1 -0
- package/package.json +55 -0
- package/public/abc.txt +1 -0
- package/public/index-service-worker.js +1 -0
- package/public/index.html +132 -0
- package/public/index.js +9 -0
- package/public-relay/heartbeat.js +30 -0
- package/public-relay/relay-lib.js +188 -0
- package/public-relay/relay-sw.js +343 -0
- package/public-relay/relay.html +33 -0
- package/src/data-calls.js +36 -0
- package/src/data-channels.js +176 -0
- package/src/data-send-recieve.js +28 -0
- package/src/errors.js +13 -0
- package/src/http-send-recieve.js +40 -0
- package/src/http-stubs.js +126 -0
- package/src/http-sw-dispatcher.js +128 -0
- package/src/index.js +12 -0
- package/src/readable-streams.js +33 -0
- package/src/sw-dispatcher.js +264 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# @statewalker/webrun-http-browser
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2022 statewalker
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# @statewalker/webrun-http-browser
|
|
2
|
+
|
|
3
|
+
This module simulates HTTP server using Service Workers.
|
|
4
|
+
It allows to develop, test, run and debug server-side code directly in the browser.
|
|
5
|
+
After that the same code can be deployed in Deno / Deno Deploy / Cloudflare / Node JS environments (with adapters).
|
|
@@ -0,0 +1,651 @@
|
|
|
1
|
+
// @statewalker/webrun-http-browser v0.1.0 https://github.com/statewalker/statewalker-webrun-http-browser Copyright 2023 Mikhail Kotelnikov
|
|
2
|
+
(function (global, factory) {
|
|
3
|
+
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
|
|
4
|
+
typeof define === 'function' && define.amd ? define(['exports'], factory) :
|
|
5
|
+
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory((global.statewalker = global.statewalker || {}, global.statewalker["webrun-http-browser"] = global.statewalker["webrun-http-browser"] || {})));
|
|
6
|
+
})(this, (function (exports) { 'use strict';
|
|
7
|
+
|
|
8
|
+
function serializeError(error) {
|
|
9
|
+
return Object.assign({
|
|
10
|
+
message: error,
|
|
11
|
+
stack: error.stack,
|
|
12
|
+
}, error);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function deserializeError(error) {
|
|
16
|
+
if (typeof error === "string") {
|
|
17
|
+
error = { message: error };
|
|
18
|
+
}
|
|
19
|
+
return Object.assign(new Error(error.message), error);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// See https://github.com/agenjs/agen-utils/blob/main/src/iterator.js
|
|
23
|
+
async function* iterate(init, queue = []) {
|
|
24
|
+
let promise, notify, push = async (error, value, done) => {
|
|
25
|
+
const slot = { error, value, done };
|
|
26
|
+
slot.promise = new Promise(n => slot.notify = n);
|
|
27
|
+
await queue.push(slot);
|
|
28
|
+
notify && notify();
|
|
29
|
+
notify = null;
|
|
30
|
+
return slot.promise;
|
|
31
|
+
};
|
|
32
|
+
const next = (value) => push(undefined, value, false);
|
|
33
|
+
const complete = () => push(undefined, undefined, true);
|
|
34
|
+
const error = err => push(err, undefined, true);
|
|
35
|
+
const unsubscribe = init(Object.assign([next, complete, error], { next, complete, error }));
|
|
36
|
+
let slot;
|
|
37
|
+
try {
|
|
38
|
+
while (true) {
|
|
39
|
+
slot = await queue.shift();
|
|
40
|
+
if (slot) {
|
|
41
|
+
try {
|
|
42
|
+
if (slot.error) { throw slot.error; }
|
|
43
|
+
else if (slot.done) { break; }
|
|
44
|
+
else { yield slot.value; }
|
|
45
|
+
} finally {
|
|
46
|
+
slot.notify(true);
|
|
47
|
+
}
|
|
48
|
+
} else {
|
|
49
|
+
await (promise = notify ? promise : new Promise(n => notify = n));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
} finally {
|
|
53
|
+
notify && notify();
|
|
54
|
+
push = () => false; // Stop pushing in the queue...
|
|
55
|
+
((typeof unsubscribe === 'function') && (await unsubscribe()));
|
|
56
|
+
while (slot = await queue.shift()) { slot.notify(false); }
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function* recieveData(onMessage) {
|
|
61
|
+
yield* iterate((iterator) => {
|
|
62
|
+
return onMessage(async ({ done = true, value, error } = {}) => {
|
|
63
|
+
if (error) {
|
|
64
|
+
await iterator.error(error);
|
|
65
|
+
} else if (done) {
|
|
66
|
+
await iterator.complete();
|
|
67
|
+
} else {
|
|
68
|
+
await iterator.next(value);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function sendData(send, it) {
|
|
75
|
+
let error;
|
|
76
|
+
try {
|
|
77
|
+
for await (let value of it) {
|
|
78
|
+
await send({ done: false, value });
|
|
79
|
+
}
|
|
80
|
+
} catch (err) {
|
|
81
|
+
error = err;
|
|
82
|
+
} finally {
|
|
83
|
+
await send({ done: true, error });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function* sendStream(communicationPort, input, params = {}) {
|
|
88
|
+
const messageChannel = new MessageChannel();
|
|
89
|
+
communicationPort.postMessage({ type: "START_CALL", params }, [
|
|
90
|
+
messageChannel.port2,
|
|
91
|
+
]);
|
|
92
|
+
|
|
93
|
+
const channel = newChannel(messageChannel.port1);
|
|
94
|
+
try {
|
|
95
|
+
await channel.start();
|
|
96
|
+
channel.sendAll(input);
|
|
97
|
+
yield* channel.recieveAll();
|
|
98
|
+
} finally {
|
|
99
|
+
await channel.close();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* This method creates and returns methods allowing to send and recieve data over the specified port.
|
|
105
|
+
* The main characteristics of the returned methods is that they properly manage backpressure - sender
|
|
106
|
+
* iterators are waiting to send the next value until the previous one is consumed on the other side.
|
|
107
|
+
* @param {*} port
|
|
108
|
+
* @returns an object with the following methods:
|
|
109
|
+
* * `start` - starts the connection over the specified port
|
|
110
|
+
* * `close`- stops all iterators and closes the port
|
|
111
|
+
* * `recieveAll() : AsyncIterator` - returns an AsyncIterator over all values send by the peer
|
|
112
|
+
* * `sendAll(it : AsyncIterator) : Promise<any>` - sends all values defined by the given
|
|
113
|
+
* AsyncIterator to the peer
|
|
114
|
+
*/
|
|
115
|
+
function newChannel(port) {
|
|
116
|
+
let listeners = [];
|
|
117
|
+
let iterators = [];
|
|
118
|
+
|
|
119
|
+
const notifyAll = async (data) => {
|
|
120
|
+
for (let listener of listeners) {
|
|
121
|
+
await listener(data);
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
const channel = newInvokationChannel(port, notifyAll);
|
|
125
|
+
|
|
126
|
+
const start = () => channel.start();
|
|
127
|
+
|
|
128
|
+
const close = async () => {
|
|
129
|
+
await notifyAll({ done: true });
|
|
130
|
+
[...iterators].forEach((it) => it.return && it.return());
|
|
131
|
+
await channel.close();
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
async function* recieveAll() {
|
|
135
|
+
yield* recieveData((listener) => {
|
|
136
|
+
listeners.push(listener);
|
|
137
|
+
return () => listeners = listeners.filter((l) => l !== listener);
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function sendAll(it) {
|
|
142
|
+
await sendData(
|
|
143
|
+
channel.invoke,
|
|
144
|
+
(async function* () {
|
|
145
|
+
try {
|
|
146
|
+
iterators.push(it);
|
|
147
|
+
yield* it;
|
|
148
|
+
} finally {
|
|
149
|
+
iterators = iterators.filter((i) => i !== it);
|
|
150
|
+
}
|
|
151
|
+
})(),
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
start,
|
|
156
|
+
close,
|
|
157
|
+
recieveAll,
|
|
158
|
+
sendAll,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function newInvokationChannel(port, handler = () => {
|
|
163
|
+
throw new Error("Handler not implemented");
|
|
164
|
+
}) {
|
|
165
|
+
const MESSAGE_TYPE_REQUEST = "REQUEST";
|
|
166
|
+
const MESSAGE_TYPE_RESPONSE = "RESPONSE";
|
|
167
|
+
const EVENT_MESSAGE = "message";
|
|
168
|
+
|
|
169
|
+
const requests = {};
|
|
170
|
+
let requestCounter = 0;
|
|
171
|
+
const listener = async (event) => {
|
|
172
|
+
const data = event.data || {};
|
|
173
|
+
const { type, callId } = data;
|
|
174
|
+
if (type === MESSAGE_TYPE_REQUEST) {
|
|
175
|
+
try {
|
|
176
|
+
const { request } = data;
|
|
177
|
+
let result = await handler(request, ...(event.ports || []));
|
|
178
|
+
const [response, ...transfers] = Array.isArray(result)
|
|
179
|
+
? result
|
|
180
|
+
: result !== undefined
|
|
181
|
+
? [result]
|
|
182
|
+
: [];
|
|
183
|
+
port.postMessage(
|
|
184
|
+
{ type: MESSAGE_TYPE_RESPONSE, callId, response },
|
|
185
|
+
transfers,
|
|
186
|
+
);
|
|
187
|
+
} catch (error) {
|
|
188
|
+
port.postMessage({
|
|
189
|
+
type: MESSAGE_TYPE_RESPONSE,
|
|
190
|
+
callId,
|
|
191
|
+
error: serializeError(error),
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
} else if (type === MESSAGE_TYPE_RESPONSE) {
|
|
195
|
+
const { callId, response, error } = data;
|
|
196
|
+
const request = requests[callId];
|
|
197
|
+
delete requests[callId];
|
|
198
|
+
if (request) {
|
|
199
|
+
error
|
|
200
|
+
? request.reject(deserializeError(error))
|
|
201
|
+
: request.resolve(response);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
const start = async () => {
|
|
207
|
+
port.addEventListener(EVENT_MESSAGE, listener);
|
|
208
|
+
await port.start();
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
const close = async () => {
|
|
212
|
+
port.removeEventListener(EVENT_MESSAGE, listener);
|
|
213
|
+
await port.close();
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const invoke = async function (request = {}, ...transfers) {
|
|
217
|
+
const callId = ++requestCounter;
|
|
218
|
+
return new Promise((resolve, reject) => {
|
|
219
|
+
try {
|
|
220
|
+
requests[callId] = { resolve, reject };
|
|
221
|
+
port.postMessage(
|
|
222
|
+
{ type: MESSAGE_TYPE_REQUEST, callId, request },
|
|
223
|
+
transfers,
|
|
224
|
+
);
|
|
225
|
+
} catch (error) {
|
|
226
|
+
reject(error);
|
|
227
|
+
delete request[callId];
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
return {
|
|
233
|
+
start,
|
|
234
|
+
close,
|
|
235
|
+
invoke,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function toReadableStream(it) {
|
|
240
|
+
return new ReadableStream({
|
|
241
|
+
type: "bytes",
|
|
242
|
+
async pull(controller) {
|
|
243
|
+
let handled = false;
|
|
244
|
+
try {
|
|
245
|
+
while (true) {
|
|
246
|
+
const slot = await it.next();
|
|
247
|
+
if (!slot || slot.done) {
|
|
248
|
+
break;
|
|
249
|
+
}
|
|
250
|
+
controller.enqueue(await slot.value);
|
|
251
|
+
}
|
|
252
|
+
} catch (error) {
|
|
253
|
+
handled = true;
|
|
254
|
+
controller.error(error);
|
|
255
|
+
} finally {
|
|
256
|
+
!handled && controller.close();
|
|
257
|
+
}
|
|
258
|
+
},
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
async function* fromReadableStream(stream) {
|
|
263
|
+
const reader = await stream.getReader();
|
|
264
|
+
let done, value;
|
|
265
|
+
while (({ done, value } = await reader.read())) {
|
|
266
|
+
if (done) {
|
|
267
|
+
break;
|
|
268
|
+
}
|
|
269
|
+
yield await value;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* This method returns an HTTP handlers transforming
|
|
275
|
+
* HTTP Request instances to binary representations and
|
|
276
|
+
* sending them to a transport layer.
|
|
277
|
+
* Results of these transport ivokations are deserialized
|
|
278
|
+
* as meaningful Response instances and returned back to the caller.
|
|
279
|
+
*
|
|
280
|
+
* The specified `send` method is used to dispatch serialized
|
|
281
|
+
* requests to return the serialized remote results.
|
|
282
|
+
* This transport method accepts and returns objects with
|
|
283
|
+
* the same structure:
|
|
284
|
+
* * `options` - contains serialized request or response parameters
|
|
285
|
+
* * `content` - an async iterator over binary blocks for
|
|
286
|
+
* the request or response
|
|
287
|
+
*
|
|
288
|
+
* @param {*} send transport function accepting serialized requests
|
|
289
|
+
* and returning back serialized responses
|
|
290
|
+
* @returns an async handler accepting standard
|
|
291
|
+
* HTTP `Request` instances and returning `Response` instances
|
|
292
|
+
*/
|
|
293
|
+
function newHttpClientStub(send) {
|
|
294
|
+
return async (request) => {
|
|
295
|
+
request = await request;
|
|
296
|
+
const fields = [
|
|
297
|
+
"url",
|
|
298
|
+
"method",
|
|
299
|
+
"mode",
|
|
300
|
+
"credentials",
|
|
301
|
+
"cache",
|
|
302
|
+
"redirect",
|
|
303
|
+
"referrer",
|
|
304
|
+
"referrerPolicy",
|
|
305
|
+
"integrity",
|
|
306
|
+
"keepalive",
|
|
307
|
+
];
|
|
308
|
+
// request = await request.clone();
|
|
309
|
+
const requestOptions = fields.reduce((options, key) => {
|
|
310
|
+
const val = request[key];
|
|
311
|
+
if (val !== undefined) options[key] = val;
|
|
312
|
+
return options;
|
|
313
|
+
}, {});
|
|
314
|
+
const requestContent = (request.body)
|
|
315
|
+
? fromReadableStream(request.body)
|
|
316
|
+
: [];
|
|
317
|
+
const result = await send({
|
|
318
|
+
options: requestOptions,
|
|
319
|
+
content: requestContent,
|
|
320
|
+
});
|
|
321
|
+
let responseBody = null;
|
|
322
|
+
let responseOptions = {};
|
|
323
|
+
if (!result) {
|
|
324
|
+
responseOptions = {
|
|
325
|
+
status: 404,
|
|
326
|
+
statusText: "Error 404: Not Found",
|
|
327
|
+
};
|
|
328
|
+
} else {
|
|
329
|
+
responseOptions = result.options;
|
|
330
|
+
const responseContent = result.content;
|
|
331
|
+
if (
|
|
332
|
+
requestOptions.method !== "HEAD" &&
|
|
333
|
+
requestOptions.method !== "OPTIONS"
|
|
334
|
+
) {
|
|
335
|
+
responseBody = toReadableStream(responseContent);
|
|
336
|
+
} else if (responseContent && responseContent.return) {
|
|
337
|
+
responseContent.return();
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
return new Response(responseBody, responseOptions);
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
async function* httpToIterator(params) {
|
|
345
|
+
const { options, content } = await params;
|
|
346
|
+
const encoder = new TextEncoder();
|
|
347
|
+
yield encoder.encode(JSON.stringify(options));
|
|
348
|
+
yield* content;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
async function httpFromIterator(it) {
|
|
352
|
+
it = await it;
|
|
353
|
+
const { done, value } = await it.next();
|
|
354
|
+
let options = {};
|
|
355
|
+
if (!done && value) {
|
|
356
|
+
const decoder = new TextDecoder();
|
|
357
|
+
const str = decoder.decode(value);
|
|
358
|
+
options = JSON.parse(str);
|
|
359
|
+
}
|
|
360
|
+
return {
|
|
361
|
+
options,
|
|
362
|
+
content: it,
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
async function sendHttpRequest(communicationPort, request) {
|
|
367
|
+
const clientStub = newHttpClientStub(async (req) => {
|
|
368
|
+
return await httpFromIterator(
|
|
369
|
+
sendStream(communicationPort, httpToIterator(req)),
|
|
370
|
+
);
|
|
371
|
+
});
|
|
372
|
+
return await clientStub(request);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
async function callChannel(mainChannel, callType, params, ...transfers) {
|
|
376
|
+
return new Promise((resolve, reject) => {
|
|
377
|
+
const channel = new MessageChannel();
|
|
378
|
+
channel.port1.onmessage = (ev) => {
|
|
379
|
+
const { result, error } = ev.data;
|
|
380
|
+
if (error) reject(deserializeError(error));
|
|
381
|
+
else resolve(result);
|
|
382
|
+
};
|
|
383
|
+
mainChannel.postMessage({ type: callType, params }, [
|
|
384
|
+
channel.port2,
|
|
385
|
+
...transfers,
|
|
386
|
+
]);
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function handleChannelCalls(mainChannel, callType, handler) {
|
|
391
|
+
const messageListener = async (event) => {
|
|
392
|
+
if (!event.data || event.data.type !== callType) return;
|
|
393
|
+
const [port, ...transfers] = event.ports;
|
|
394
|
+
let response = {};
|
|
395
|
+
try {
|
|
396
|
+
const params = event.data.params;
|
|
397
|
+
response.result = await handler(event, params, ...transfers);
|
|
398
|
+
} catch (error) {
|
|
399
|
+
response.error = serializeError(error);
|
|
400
|
+
}
|
|
401
|
+
port.postMessage(response);
|
|
402
|
+
};
|
|
403
|
+
mainChannel.addEventListener("message", messageListener);
|
|
404
|
+
try {
|
|
405
|
+
mainChannel.start && mainChannel.start();
|
|
406
|
+
} catch (e) {}
|
|
407
|
+
return () => mainChannel.removeEventListener("message", messageListener);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function promisifyRequest(request) {
|
|
411
|
+
return new Promise((resolve, reject) => {
|
|
412
|
+
// @ts-ignore - file size hacks
|
|
413
|
+
request.oncomplete = request.onsuccess = () => resolve(request.result);
|
|
414
|
+
// @ts-ignore - file size hacks
|
|
415
|
+
request.onabort = request.onerror = () => reject(request.error);
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
function createStore(dbName, storeName) {
|
|
419
|
+
const request = indexedDB.open(dbName);
|
|
420
|
+
request.onupgradeneeded = () => request.result.createObjectStore(storeName);
|
|
421
|
+
const dbp = promisifyRequest(request);
|
|
422
|
+
return (txMode, callback) => dbp.then((db) => callback(db.transaction(storeName, txMode).objectStore(storeName)));
|
|
423
|
+
}
|
|
424
|
+
let defaultGetStoreFunc;
|
|
425
|
+
function defaultGetStore() {
|
|
426
|
+
if (!defaultGetStoreFunc) {
|
|
427
|
+
defaultGetStoreFunc = createStore('keyval-store', 'keyval');
|
|
428
|
+
}
|
|
429
|
+
return defaultGetStoreFunc;
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* Get a value by its key.
|
|
433
|
+
*
|
|
434
|
+
* @param key
|
|
435
|
+
* @param customStore Method to get a custom store. Use with caution (see the docs).
|
|
436
|
+
*/
|
|
437
|
+
function get(key, customStore = defaultGetStore()) {
|
|
438
|
+
return customStore('readonly', (store) => promisifyRequest(store.get(key)));
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* Set a value with a key.
|
|
442
|
+
*
|
|
443
|
+
* @param key
|
|
444
|
+
* @param value
|
|
445
|
+
* @param customStore Method to get a custom store. Use with caution (see the docs).
|
|
446
|
+
*/
|
|
447
|
+
function set(key, value, customStore = defaultGetStore()) {
|
|
448
|
+
return customStore('readwrite', (store) => {
|
|
449
|
+
store.put(value, key);
|
|
450
|
+
return promisifyRequest(store.transaction);
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
class SwPortDispatcher {
|
|
455
|
+
constructor({ self, log }) {
|
|
456
|
+
this.self = self;
|
|
457
|
+
this.log = log;
|
|
458
|
+
// Contains mapping between path prefixes and clientIds
|
|
459
|
+
this.handlersIndex = new Map();
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
get scope() {
|
|
463
|
+
return new URL("./", (typeof document === 'undefined' && typeof location === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : typeof document === 'undefined' ? location.href : (document.currentScript && document.currentScript.src || new URL('index-sw-umd.js', document.baseURI).href))) + "";
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
async loadChannelInfo(key) {
|
|
467
|
+
await this._checkActivation();
|
|
468
|
+
let result;
|
|
469
|
+
for (let channelInfo of this.handlersIndex.values()) {
|
|
470
|
+
if (channelInfo.key !== key) continue;
|
|
471
|
+
const clientId = channelInfo.clientId;
|
|
472
|
+
const client = await self.clients.get(clientId);
|
|
473
|
+
if (!client) {
|
|
474
|
+
// Client gone. Remove the corresponding handlers from the index.
|
|
475
|
+
this.handlersIndex.delete(clientId);
|
|
476
|
+
throw Object.assign(new Error("Error 410: Resource Gone"), {
|
|
477
|
+
status: 410,
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
result = channelInfo;
|
|
481
|
+
break;
|
|
482
|
+
}
|
|
483
|
+
return result;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
start() {
|
|
487
|
+
this._cleanup = handleChannelCalls(
|
|
488
|
+
this.self,
|
|
489
|
+
"UPDATE_COMMUNICATION_PORT",
|
|
490
|
+
async (event, channelInfo, port) => {
|
|
491
|
+
const clientId = event.source.id;
|
|
492
|
+
this.log("[UPDATE_COMMUNICATION_PORT]", clientId, channelInfo);
|
|
493
|
+
await this._updateChannelInfo({ ...channelInfo, port, clientId });
|
|
494
|
+
return { ...channelInfo };
|
|
495
|
+
},
|
|
496
|
+
);
|
|
497
|
+
this.self.onmessage = () => {};
|
|
498
|
+
|
|
499
|
+
this.self.addEventListener("install", (event) => {
|
|
500
|
+
this.log("Skip waiting on install.", event);
|
|
501
|
+
this.self.skipWaiting();
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
this.self.addEventListener("activate", async (event) => {
|
|
505
|
+
this.log(
|
|
506
|
+
"Claim control over all clients.",
|
|
507
|
+
event,
|
|
508
|
+
clients,
|
|
509
|
+
this.self.clients,
|
|
510
|
+
);
|
|
511
|
+
event.waitUntil((async () => {
|
|
512
|
+
await this.self.clients.claim();
|
|
513
|
+
await this._checkActivation();
|
|
514
|
+
})());
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
async stop() {
|
|
519
|
+
if (this._cleanup) {
|
|
520
|
+
await this._cleanup();
|
|
521
|
+
this._cleanup = undefined;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
async _checkActivation() {
|
|
526
|
+
return this._activationPromise = this._activationPromise ||
|
|
527
|
+
this._activate();
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
async _activate() {
|
|
531
|
+
this.log("[checkActivation]");
|
|
532
|
+
let clientsIndex = await this._loadClientIds();
|
|
533
|
+
for (let [clientId, client] of clientsIndex.entries()) {
|
|
534
|
+
const messageChannel = new MessageChannel();
|
|
535
|
+
const port = messageChannel.port1;
|
|
536
|
+
const channelInfo = await callChannel(
|
|
537
|
+
client,
|
|
538
|
+
"UPDATE_COMMUNICATION_PORT",
|
|
539
|
+
{},
|
|
540
|
+
messageChannel.port2,
|
|
541
|
+
);
|
|
542
|
+
await this._updateChannelInfo({ ...channelInfo, port, clientId });
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
async _updateChannelInfo(channelInfo) {
|
|
547
|
+
this.log("[updateClientInfo]", channelInfo);
|
|
548
|
+
this.handlersIndex.set(channelInfo.clientId, channelInfo);
|
|
549
|
+
await this._updateClientIds([]);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
async _loadClientIds() {
|
|
553
|
+
const clientIds = new Set((await get("clientIds")) || []);
|
|
554
|
+
return await this._updateClientIds(clientIds);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
async _updateClientIds(clientIds) {
|
|
558
|
+
clientIds = new Set(clientIds);
|
|
559
|
+
for (let clientId of this.handlersIndex.keys()) {
|
|
560
|
+
clientIds.add(clientId);
|
|
561
|
+
}
|
|
562
|
+
const index = new Map();
|
|
563
|
+
for (let clientId of clientIds) {
|
|
564
|
+
const client = await this.self.clients.get(clientId);
|
|
565
|
+
if (client) index.set(clientId, client);
|
|
566
|
+
else this.handlersIndex.delete(clientId);
|
|
567
|
+
}
|
|
568
|
+
await set("clientIds", [...index.keys()].sort());
|
|
569
|
+
return index;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
class SwHttpDispatcher extends SwPortDispatcher {
|
|
574
|
+
constructor(options) {
|
|
575
|
+
super(options);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
start() {
|
|
579
|
+
super.start();
|
|
580
|
+
this.self.addEventListener("fetch", this._handleFetchEvent.bind(this));
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
_handleFetchEvent(event) {
|
|
584
|
+
event.respondWith((async () => {
|
|
585
|
+
const request = event.request;
|
|
586
|
+
try {
|
|
587
|
+
const requestUrl = request.url;
|
|
588
|
+
const rootUrl = this.scope;
|
|
589
|
+
let channelInfo;
|
|
590
|
+
if (requestUrl.indexOf(rootUrl) === 0) {
|
|
591
|
+
const key = requestUrl
|
|
592
|
+
.substring(rootUrl.length)
|
|
593
|
+
.replace(/^\/?([^\/]+).*$/, "$1");
|
|
594
|
+
// console.log(`* rootUrl="${rootUrl}" requestUrl="${requestUrl}" key="${key}"`);
|
|
595
|
+
channelInfo = await this.loadChannelInfo(key);
|
|
596
|
+
}
|
|
597
|
+
if (channelInfo && channelInfo.port) {
|
|
598
|
+
return await sendHttpRequest(channelInfo.port, request);
|
|
599
|
+
} else {
|
|
600
|
+
const newRequest = new Request(requestUrl, {
|
|
601
|
+
method: request.method,
|
|
602
|
+
headers: request.headers,
|
|
603
|
+
body: request.body,
|
|
604
|
+
referrer: request.referrer,
|
|
605
|
+
referrerPolicy: request.referrerPolicy,
|
|
606
|
+
// mode: request.mode,
|
|
607
|
+
credentials: request.credentials,
|
|
608
|
+
cache: request.cache,
|
|
609
|
+
redirect: request.redirect,
|
|
610
|
+
integrity: request.integrity,
|
|
611
|
+
});
|
|
612
|
+
return await fetch(newRequest);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// const requestUrl = request.url;
|
|
616
|
+
// let url = requestUrl.replace(
|
|
617
|
+
// /^.*\/[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}(.*)$/,
|
|
618
|
+
// "$1",
|
|
619
|
+
// );
|
|
620
|
+
// if (url !== requestUrl) {
|
|
621
|
+
// url = new URL(
|
|
622
|
+
// "/packages/webrun-http-browser/src" + url,
|
|
623
|
+
// self.location.href,
|
|
624
|
+
// );
|
|
625
|
+
// }
|
|
626
|
+
} catch (error) {
|
|
627
|
+
console.error(error);
|
|
628
|
+
return new Response(null, {
|
|
629
|
+
status: 500,
|
|
630
|
+
statusText: "Error 500: Internal error",
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
})());
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
function startHttpDispatcher({ self, log }) {
|
|
637
|
+
const dispatcher = new SwHttpDispatcher({ self, log });
|
|
638
|
+
dispatcher.start();
|
|
639
|
+
return () => {
|
|
640
|
+
dispatcher.stop();
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
const result = startHttpDispatcher({
|
|
645
|
+
self,
|
|
646
|
+
log: console.log.bind(console, "[sw]"),
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
exports.result = result;
|
|
650
|
+
|
|
651
|
+
}));
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
// @statewalker/webrun-http-browser v0.1.0 https://github.com/statewalker/statewalker-webrun-http-browser Copyright 2023 Mikhail Kotelnikov
|
|
2
|
+
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t(((e="undefined"!=typeof globalThis?globalThis:e||self).statewalker=e.statewalker||{},e.statewalker["webrun-http-browser"]=e.statewalker["webrun-http-browser"]||{}))}(this,(function(e){"use strict";function t(e){return Object.assign({message:e,stack:e.stack},e)}function n(e){return"string"==typeof e&&(e={message:e}),Object.assign(new Error(e.message),e)}async function*s(e){yield*async function*(e,t=[]){let n,s,r=async(e,n,r)=>{const a={error:e,value:n,done:r};return a.promise=new Promise((e=>a.notify=e)),await t.push(a),s&&s(),s=null,a.promise};const a=e=>r(void 0,e,!1),o=()=>r(void 0,void 0,!0),i=e=>r(e,void 0,!0),c=e(Object.assign([a,o,i],{next:a,complete:o,error:i}));let l;try{for(;;)if(l=await t.shift(),l)try{if(l.error)throw l.error;if(l.done)break;yield l.value}finally{l.notify(!0)}else await(n=s?n:new Promise((e=>s=e)))}finally{for(s&&s(),r=()=>!1,"function"==typeof c&&await c();l=await t.shift();)l.notify(!1)}}((t=>e((async({done:e=!0,value:n,error:s}={})=>{s?await t.error(s):e?await t.complete():await t.next(n)}))))}async function*r(e,r,a={}){const o=new MessageChannel;e.postMessage({type:"START_CALL",params:a},[o.port2]);const i=function(e){let r=[],a=[];const o=async e=>{for(let t of r)await t(e)},i=function(e,s=(()=>{throw new Error("Handler not implemented")})){const r="REQUEST",a="RESPONSE",o="message",i={};let c=0;const l=async o=>{const c=o.data||{},{type:l,callId:d}=c;if(l===r)try{const{request:t}=c;let n=await s(t,...o.ports||[]);const[r,...i]=Array.isArray(n)?n:void 0!==n?[n]:[];e.postMessage({type:a,callId:d,response:r},i)}catch(n){e.postMessage({type:a,callId:d,error:t(n)})}else if(l===a){const{callId:e,response:t,error:s}=c,r=i[e];delete i[e],r&&(s?r.reject(n(s)):r.resolve(t))}},d=async()=>{e.addEventListener(o,l),await e.start()},u=async()=>{e.removeEventListener(o,l),await e.close()},f=async function(t={},...n){const s=++c;return new Promise(((a,o)=>{try{i[s]={resolve:a,reject:o},e.postMessage({type:r,callId:s,request:t},n)}catch(e){o(e),delete t[s]}}))};return{start:d,close:u,invoke:f}}(e,o),c=()=>i.start(),l=async()=>{await o({done:!0}),[...a].forEach((e=>e.return&&e.return())),await i.close()};async function*d(){yield*s((e=>(r.push(e),()=>r=r.filter((t=>t!==e)))))}async function u(e){await async function(e,t){let n;try{for await(let n of t)await e({done:!1,value:n})}catch(e){n=e}finally{await e({done:!0,error:n})}}(i.invoke,async function*(){try{a.push(e),yield*e}finally{a=a.filter((t=>t!==e))}}())}return{start:c,close:l,recieveAll:d,sendAll:u}}(o.port1);try{await i.start(),i.sendAll(r),yield*i.recieveAll()}finally{await i.close()}}function a(e){return async t=>{t=await t;const n=["url","method","mode","credentials","cache","redirect","referrer","referrerPolicy","integrity","keepalive"].reduce(((e,n)=>{const s=t[n];return void 0!==s&&(e[n]=s),e}),{}),s=t.body?async function*(e){const t=await e.getReader();let n,s;for(;({done:n,value:s}=await t.read())&&!n;)yield await s}(t.body):[],r=await e({options:n,content:s});let a=null,o={};if(r){o=r.options;const e=r.content;"HEAD"!==n.method&&"OPTIONS"!==n.method?(i=e,a=new ReadableStream({type:"bytes",async pull(e){let t=!1;try{for(;;){const t=await i.next();if(!t||t.done)break;e.enqueue(await t.value)}}catch(n){t=!0,e.error(n)}finally{!t&&e.close()}}})):e&&e.return&&e.return()}else o={status:404,statusText:"Error 404: Not Found"};var i;return new Response(a,o)}}async function o(e,t){const n=a((async t=>await async function(e){e=await e;const{done:t,value:n}=await e.next();let s={};if(!t&&n){const e=(new TextDecoder).decode(n);s=JSON.parse(e)}return{options:s,content:e}}(r(e,async function*(e){const{options:t,content:n}=await e,s=new TextEncoder;yield s.encode(JSON.stringify(t)),yield*n}(t)))));return await n(t)}async function i(e,t,s,...r){return new Promise(((a,o)=>{const i=new MessageChannel;i.port1.onmessage=e=>{const{result:t,error:s}=e.data;s?o(n(s)):a(t)},e.postMessage({type:t,params:s},[i.port2,...r])}))}function c(e){return new Promise(((t,n)=>{e.oncomplete=e.onsuccess=()=>t(e.result),e.onabort=e.onerror=()=>n(e.error)}))}let l;function d(){return l||(l=function(e,t){const n=indexedDB.open(e);n.onupgradeneeded=()=>n.result.createObjectStore(t);const s=c(n);return(e,n)=>s.then((s=>n(s.transaction(t,e).objectStore(t))))}("keyval-store","keyval")),l}class u{constructor({self:e,log:t}){this.self=e,this.log=t,this.handlersIndex=new Map}get scope(){return new URL("./","undefined"==typeof document&&"undefined"==typeof location?require("url").pathToFileURL(__filename).href:"undefined"==typeof document?location.href:document.currentScript&&document.currentScript.src||new URL("index-sw-umd.min.js",document.baseURI).href)+""}async loadChannelInfo(e){let t;await this._checkActivation();for(let n of this.handlersIndex.values()){if(n.key!==e)continue;const s=n.clientId;if(!await self.clients.get(s))throw this.handlersIndex.delete(s),Object.assign(new Error("Error 410: Resource Gone"),{status:410});t=n;break}return t}start(){this._cleanup=function(e,n,s){const r=async e=>{if(!e.data||e.data.type!==n)return;const[r,...a]=e.ports;let o={};try{const t=e.data.params;o.result=await s(e,t,...a)}catch(e){o.error=t(e)}r.postMessage(o)};e.addEventListener("message",r);try{e.start&&e.start()}catch(e){}return()=>e.removeEventListener("message",r)}(this.self,"UPDATE_COMMUNICATION_PORT",(async(e,t,n)=>{const s=e.source.id;return this.log("[UPDATE_COMMUNICATION_PORT]",s,t),await this._updateChannelInfo({...t,port:n,clientId:s}),{...t}})),this.self.onmessage=()=>{},this.self.addEventListener("install",(e=>{this.log("Skip waiting on install.",e),this.self.skipWaiting()})),this.self.addEventListener("activate",(async e=>{this.log("Claim control over all clients.",e,clients,this.self.clients),e.waitUntil((async()=>{await this.self.clients.claim(),await this._checkActivation()})())}))}async stop(){this._cleanup&&(await this._cleanup(),this._cleanup=void 0)}async _checkActivation(){return this._activationPromise=this._activationPromise||this._activate()}async _activate(){this.log("[checkActivation]");let e=await this._loadClientIds();for(let[t,n]of e.entries()){const e=new MessageChannel,s=e.port1,r=await i(n,"UPDATE_COMMUNICATION_PORT",{},e.port2);await this._updateChannelInfo({...r,port:s,clientId:t})}}async _updateChannelInfo(e){this.log("[updateClientInfo]",e),this.handlersIndex.set(e.clientId,e),await this._updateClientIds([])}async _loadClientIds(){const e=new Set(await function(e,t=d()){return t("readonly",(t=>c(t.get(e))))}("clientIds")||[]);return await this._updateClientIds(e)}async _updateClientIds(e){e=new Set(e);for(let t of this.handlersIndex.keys())e.add(t);const t=new Map;for(let n of e){const e=await this.self.clients.get(n);e?t.set(n,e):this.handlersIndex.delete(n)}return await function(e,t,n=d()){return n("readwrite",(n=>(n.put(t,e),c(n.transaction))))}("clientIds",[...t.keys()].sort()),t}}class f extends u{constructor(e){super(e)}start(){super.start(),this.self.addEventListener("fetch",this._handleFetchEvent.bind(this))}_handleFetchEvent(e){e.respondWith((async()=>{const t=e.request;try{const e=t.url,n=this.scope;let s;if(0===e.indexOf(n)){const t=e.substring(n.length).replace(/^\/?([^\/]+).*$/,"$1");s=await this.loadChannelInfo(t)}if(s&&s.port)return await o(s.port,t);{const n=new Request(e,{method:t.method,headers:t.headers,body:t.body,referrer:t.referrer,referrerPolicy:t.referrerPolicy,credentials:t.credentials,cache:t.cache,redirect:t.redirect,integrity:t.integrity});return await fetch(n)}}catch(e){return console.error(e),new Response(null,{status:500,statusText:"Error 500: Internal error"})}})())}}const h=function({self:e,log:t}){const n=new f({self:e,log:t});return n.start(),()=>{n.stop()}}({self:self,log:console.log.bind(console,"[sw]")});e.result=h}));
|