@statewalker/webrun-http-browser 0.3.2 → 0.3.3

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 CHANGED
@@ -6,8 +6,9 @@ After that the same code can be deployed in Deno / Deno Deploy / Cloudflare / No
6
6
 
7
7
  ## Demo
8
8
 
9
- * [Demo 1](./demo/demo-1.html) - a dynamic web site with a server-side API, a HTML page and a CSS file
10
9
  * [https://observablehq.com/@kotelnikov/webrun-http-service](https://observablehq.com/@kotelnikov/webrun-http-service) - an Observable page demonstrating how it works. You can play with the code here.
10
+ * [Demo 1](./demo/demo-1.html) - a dynamic web site with a server-side API, a HTML page and a CSS file
11
+ * [Demo 2](./demo/demo-2.html) - a virtual file server exposing your local disk content
11
12
 
12
13
  ## Features
13
14
 
@@ -0,0 +1,598 @@
1
+ <!DOCTYPE HTML>
2
+ <html>
3
+
4
+ <head>
5
+ <meta charset="utf-8" />
6
+ <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
7
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
8
+ </head>
9
+
10
+ <body>
11
+ <div id="view"></div>
12
+
13
+ <script type="module">
14
+ import * as WebRunHttp from "https://unpkg.com/@statewalker/webrun-http-browser@0.3/dist/index.js";
15
+ import { Hono } from "https://unpkg.com/hono@3.5/dist/index.js";
16
+ import mime from "https://esm.sh/mime-types";
17
+ import {
18
+ BrowserFilesApi,
19
+ FilesApi,
20
+ isHandlerAccessible,
21
+ MemFilesApi,
22
+ verifyPermission,
23
+ } from "https://unpkg.com/@statewalker/webrun-files@0.2.0/dist/index.js";
24
+
25
+ Promise.resolve().then(main).catch(console.error);
26
+ async function main() {
27
+ // Load a Hono request router:
28
+ const router = getRouter();
29
+
30
+ // Create an HTTP Server with the specified name.
31
+ // It uses the given Hono router to dispatch requests
32
+ // between endpoints.
33
+ const serviceBaseUrl = await startHttpService({
34
+ serviceKey: "MY_STATIC_SITE_LAUNCHER",
35
+ router
36
+ })
37
+ const siteUrl = `${serviceBaseUrl}/fs/index.html`;
38
+
39
+ // Now we can visualize the site in an iframe iframe:
40
+ const container = document.querySelector("#view");
41
+
42
+ // We also append a reference to this site.
43
+ // It allows to open the site in a separate window:
44
+ const a = document.createElement("a");
45
+ a.target = "_blank";
46
+ a.href = siteUrl;
47
+ a.innerText = siteUrl;
48
+ container.appendChild(a);
49
+ }
50
+
51
+ async function startHttpService({
52
+ serviceKey,
53
+ router
54
+ }) {
55
+ // Step 1: Start a ServiceWorker and open a MessageChannel used to recieve HTTP requests and send responses:
56
+ const connection = await WebRunHttp.newRemoteRelayChannel();
57
+
58
+ // Step 2: Define an HTTP handler
59
+ const serviceBaseUrl = `${connection.baseUrl}~${serviceKey}`;
60
+ const app = new Hono({
61
+ getPath: (req) => req.url.substring(serviceBaseUrl.length)
62
+ });
63
+ // Add the router
64
+ app.route("/", router);
65
+ const httpHandler = (request) => app.fetch(request);
66
+
67
+ // Step 3: Instantiate a "server" using the communcation channel with the ServiceWorker:
68
+ const close = await WebRunHttp.initHttpService(httpHandler, {
69
+ key: serviceKey, // Service prefix
70
+ port: connection.port
71
+ });
72
+
73
+ return serviceBaseUrl;
74
+ }
75
+
76
+ function getRouter() {
77
+ const router = new Hono();
78
+ const files = [...document.querySelectorAll("[data-file]")].reduce((index, elm) => {
79
+ const fileName = elm.getAttribute("data-file");
80
+ const value = elm.innerText.replaceAll("<\\/script>", "</" + "script>");
81
+ index[fileName] = value;
82
+ return index;
83
+ }, {})
84
+ router.get("/*", newStaticFilesDispatcher(files))
85
+ return router;
86
+ }
87
+
88
+ function newStaticFilesDispatcher(files) {
89
+ return function showStaticFiles(context) {
90
+ let { url, path } = context.req;
91
+ path = path.replace(/#.*$/, "");
92
+ if (path.match(/\/$/)) {
93
+ path = path + "index.html";
94
+ }
95
+ const result = files[path];
96
+ if (!result) {
97
+ return context.notFound();
98
+ }
99
+ const mimeType = mime.lookup(path) || "application/octet-stream";
100
+ context.header("Content-Type", mimeType);
101
+ context.status(201);
102
+ if (typeof result === "object") {
103
+ return context.json(result);
104
+ } else {
105
+ return context.body(result);
106
+ }
107
+ };
108
+ }
109
+ </script>
110
+
111
+ <script type="template/file" data-file="/fs/files-tree.css">
112
+ .tree {
113
+ --tree-line-width: 1px;
114
+ --tree-line-color: silver;
115
+ --tree-connector-radius: 0.1em;
116
+ --tree-connector-length: 0.5em;
117
+ --tree-connector-height: 0.6em;
118
+ --tree-content-padding: 0.5em;
119
+ position: relative;
120
+ }
121
+ .tree .tree-node {
122
+ position: relative;
123
+ padding-inline-start: var(--tree-connector-length);
124
+ border-inline-start: var(--tree-line-width) solid var(--tree-line-color);
125
+ }
126
+ .tree .tree-node:before {
127
+ content: "";
128
+ position: absolute;
129
+ top: 0;
130
+ left: 0;
131
+ right: 0;
132
+ width: var(--tree-connector-length);
133
+ height: var(--tree-connector-height);
134
+ max-height: 100%;
135
+ margin-inline-start: calc(0em - var(--tree-line-width));
136
+ border-bottom: var(--tree-line-width) solid var(--tree-line-color);
137
+ border-inline-start: var(--tree-line-width) solid var(--tree-line-color);
138
+ border-end-start-radius: var(--tree-connector-radius);
139
+ }
140
+ .tree .tree-node:last-child {
141
+ border-inline-start: var(--tree-line-width) solid transparent;
142
+ }
143
+ .tree .tree-node > .tree-node-content {
144
+ position: relative;
145
+ padding-inline-start: var(--tree-content-padding);
146
+ }
147
+ .tree .tree-node > .tree-node-content:before {
148
+ content: "";
149
+ position: absolute;
150
+ top: var(--tree-connector-height);
151
+ left: 0;
152
+ right: 0;
153
+ width: var(--tree-line-width);
154
+ height: calc(100% - var(--tree-connector-height));
155
+ border-inline-start: var(--tree-line-width) solid var(--tree-line-color);
156
+ }
157
+ .tree .tree-node > .tree-node-content:last-child:before {
158
+ border-inline-start: var(--tree-line-width) solid transparent;
159
+ }
160
+
161
+ .tree {
162
+ --tree-content-padding: 0.1em;
163
+ padding: 1em 1em 1em 2em;
164
+ }
165
+ .tree .tree-node > .tree-node-content {
166
+ display: flex;
167
+ gap: 0.2em;
168
+ }
169
+ .tree .tree-node > .tree-node-content > .icon-dir,
170
+ .tree .tree-node > .tree-node-content > .icon-file {
171
+ display: block;
172
+ width: 1.5em;
173
+ color: gray;
174
+ }
175
+ </script>
176
+ <script type="template/file" data-file="/fs/index.html">
177
+ <!DOCTYPE HTML>
178
+ <html>
179
+ <head>
180
+ <meta charset="utf-8" />
181
+ <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
182
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
183
+ <link crossorigin="anonymous" media="all" rel="stylesheet" href="./files-tree.css" />
184
+ </head>
185
+ <body>
186
+ <h3>Local File System</h3>
187
+ <p>
188
+ This page allows you to get read/write access to your local file system and develop the whole site directly in the browser without an HTTP server.
189
+ </p>
190
+ <div>
191
+ <button id="fs-open">Open Workspace</button>
192
+ <button id="fs-redraw-tree" style="display: none;">Redraw Files Tree</button>
193
+ </div>
194
+ <div id="fs-view"></div>
195
+
196
+ <script type="module" src="./js/index.js"><\/script>
197
+ </body>
198
+ </html>
199
+ </script>
200
+
201
+ <script data-file="/fs/js/deps.js" type="template/file">
202
+ export * as WebRunHttp from "https://unpkg.com/@statewalker/webrun-http-browser@0.3/dist/index.js";
203
+ export { Hono } from "https://unpkg.com/hono@3.5/dist/index.js";
204
+ export {
205
+ BrowserFilesApi,
206
+ verifyPermission,
207
+ } from "https://unpkg.com/@statewalker/webrun-files@0.2.0/dist/index.js";
208
+ export * as mime from "https://esm.sh/mime-types";
209
+ </script>
210
+
211
+
212
+ <script data-file="/fs/js/openRepository.js" type="template/file">
213
+ import { BrowserFilesApi, verifyPermission } from "./deps.js";
214
+ export async function openRepository(readwrite = true) {
215
+ const handler = await window.showDirectoryPicker();
216
+ if (!(await verifyPermission(handler, readwrite))) {
217
+ throw new Error("Access was not granted");
218
+ }
219
+ const fs = new BrowserFilesApi({ rootHandler : handler });
220
+ return fs;
221
+ }
222
+ </script>
223
+
224
+ <script data-file="/fs/js/startHttpService.js" type="template/file">
225
+ import { WebRunHttp, Hono } from "./deps.js";
226
+
227
+ export async function startHttpService({
228
+ serviceKey,
229
+ router
230
+ }) {
231
+ // Step 1: Start a ServiceWorker and open a MessageChannel used to recieve HTTP requests and send responses:
232
+ const connection = await WebRunHttp.newRemoteRelayChannel();
233
+
234
+ // Step 2: Define an HTTP handler
235
+ const serviceBaseUrl = `${connection.baseUrl}~${serviceKey}`;
236
+ const app = new Hono({
237
+ getPath: (req) => req.url.substring(serviceBaseUrl.length)
238
+ });
239
+ // Add the router
240
+ app.route("/", router);
241
+ const httpHandler = (request) => app.fetch(request);
242
+
243
+ // Step 3: Instantiate a "server" using the communcation channel with the ServiceWorker:
244
+ const close = await WebRunHttp.initHttpService(httpHandler, {
245
+ key: serviceKey, // Service prefix
246
+ port: connection.port
247
+ });
248
+
249
+ return serviceBaseUrl;
250
+ }
251
+ </script>
252
+
253
+ <script data-file="/fs/js/newFilesRouter.js" type="template/file">
254
+ import { Hono, WebRunHttp } from "./deps.js";
255
+
256
+ export function newFilesRouter(fs) {
257
+ const router = new Hono();
258
+ router.get("/*", async (context) => {
259
+ let { url, path } = context.req;
260
+ path = path.replace(/#.*$/, "");
261
+ if (path.match(/\/$/)) {
262
+ path = path + "index.html";
263
+ }
264
+ const fileInfo = await fs.stats(path);
265
+ if (!fileInfo || (fileInfo.kind !== "file")) {
266
+ return context.notFound();
267
+ }
268
+ const content = await fs.read(fileInfo);
269
+ const responseBody = await WebRunHttp.toReadableStream(content);
270
+ return new Response(responseBody, {
271
+ status: 200,
272
+ headers: {
273
+ "Content-Type": fileInfo.type,
274
+ },
275
+ });
276
+ });
277
+ return router;
278
+ }
279
+
280
+ </script>
281
+
282
+ <script data-file="/fs/js/index.js" type="template/file">
283
+ console.log("Hell-o!");
284
+ const SERVICE_KEY = "FS"
285
+ import { Hono, WebRunHttp } from "./deps.js";
286
+ import { startHttpService } from "./startHttpService.js";
287
+ import { openRepository } from "./openRepository.js";
288
+ import { newFilesRouter } from "./newFilesRouter.js";
289
+
290
+ Promise.resolve().then(main).catch(console.error);
291
+
292
+ async function main() {
293
+ const openBtn = document.querySelector("#fs-open");
294
+ const refreshBtn = document.querySelector("#fs-redraw-tree");
295
+ let close;
296
+ openBtn.addEventListener("click", async () => {
297
+ if (close) close();
298
+ const fs = await openRepository();
299
+ const router = await newFilesRouter(fs);
300
+ const serviceBaseUrl = await startHttpService({
301
+ serviceKey : SERVICE_KEY,
302
+ router
303
+ })
304
+
305
+ openBtn.parentElement.style.display = "none";
306
+ refreshBtn.style.display = "block";
307
+ renderFilesExplorer(fs, serviceBaseUrl);
308
+ refreshBtn.addEventListener("click", (event) => {
309
+ renderFilesExplorer(fs, serviceBaseUrl);
310
+ });
311
+ })
312
+ }
313
+
314
+ async function renderFilesExplorer(fs, baseUrl) {
315
+ const container = document.querySelector("#fs-view");
316
+ container.innerHTML = "";
317
+ container.classList.add("tree");
318
+ for await (let file of fs.list("/", { recursive: false })) {
319
+ renderFile({
320
+ fs,
321
+ file,
322
+ baseUrl,
323
+ container
324
+ })
325
+ }
326
+ }
327
+
328
+ function renderFile({
329
+ fs,
330
+ file,
331
+ container,
332
+ baseUrl
333
+ }) {
334
+ let path = file.path;
335
+ const isDirectory = file.kind === "directory";
336
+ if (isDirectory) {
337
+ if (path[path.length - 1] !== '/') {
338
+ path += '/';
339
+ }
340
+ }
341
+
342
+ const treeNodeElm = newElement("tree-node", container);
343
+ const treeNodeContentElm = newElement("tree-node-content", treeNodeElm);
344
+ const iconElm = newElement("tree-node-icon", treeNodeContentElm);
345
+ const iconCls = isDirectory ? "icon-dir" : "icon-file";
346
+ iconElm.classList.add(iconCls);
347
+ iconElm.innerText = isDirectory ? 'dir' : 'file';
348
+
349
+ const linkElm = newElement("file-name", treeNodeContentElm, "a");
350
+ const fileUrl = baseUrl + path;
351
+ linkElm.setAttribute("href", fileUrl);
352
+ linkElm.setAttribute("target", "_blank");
353
+ linkElm.innerText = file.name;
354
+
355
+ if (isDirectory) {
356
+ let opened = false;
357
+ linkElm.addEventListener("click", async (ev) => {
358
+ ev.preventDefault();
359
+ ev.stopPropagation();
360
+ opened = !opened;
361
+ if (opened) {
362
+ for await (let child of fs.list(file, { recursive: false })) {
363
+ renderFile({
364
+ fs,
365
+ file : child,
366
+ baseUrl,
367
+ container : treeNodeElm
368
+ })
369
+ }
370
+ } else {
371
+ let elm = treeNodeElm.querySelector(".tree-node-content");
372
+ elm = elm?.nextElementSibling;
373
+ while (elm) {
374
+ const next = elm.nextElementSibling;
375
+ treeNodeElm.removeChild(elm);
376
+ elm = next;
377
+ }
378
+ }
379
+ })
380
+ }
381
+
382
+ function newElement(className, parent, name = "div") {
383
+ const div = document.createElement(name);
384
+ className
385
+ .split(/\\s+/)
386
+ .map(s => s.trim())
387
+ .forEach(cls => div.classList.add(cls));
388
+ if (parent) {
389
+ parent.appendChild(div);
390
+ }
391
+ return div;
392
+ }
393
+ }
394
+
395
+ </script>
396
+
397
+
398
+ <script type="template/file" data-file="/fs/index-1.html">
399
+ <!DOCTYPE HTML>
400
+ <html>
401
+ <head>
402
+ <meta charset="utf-8" />
403
+ <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
404
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
405
+ <link crossorigin="anonymous" media="all" rel="stylesheet" href="./files-tree.css" />
406
+ </head>
407
+ <body>
408
+ <h3>Local File System</h3>
409
+ <p>
410
+ This page allows you to get read/write access to your local file system and develop the whole site directly in the browser without an HTTP server.
411
+ </p>
412
+
413
+ <div>
414
+ <button id="fs-open">Open File System</button>
415
+ <div><small>When you click on this button the browser will ask you to give access to the directory you want to explore in your browser.</small></div>
416
+ </div>
417
+
418
+ <div>
419
+ <button id="fs-redraw-tree" style="display: none;">Refresh</button>
420
+ <div id="fs-view"></div>
421
+ </div>
422
+
423
+ <script type="module">
424
+ const SERVICE_KEY = "FS"
425
+ const ENDPOINT_BASE_URL = "https://unpkg.com/@statewalker/webrun-http-browser@0.2.1/public-relay"
426
+ import {
427
+ BrowserFilesApi,
428
+ FilesApi,
429
+ isHandlerAccessible,
430
+ MemFilesApi,
431
+ verifyPermission,
432
+ } from "https://unpkg.com/@statewalker/webrun-files@0.2.0/dist/index.js";
433
+ import * as relayLib from "https://unpkg.com/@statewalker/webrun-http-browser@0.2.1/dist/index.js";
434
+ import { lookup } from "https://esm.sh/mime-types";
435
+
436
+
437
+ Promise.resolve().then(main).catch(console.error);
438
+
439
+ async function main() {
440
+ const openBtn = document.querySelector("#fs-open");
441
+ const refreshBtn = document.querySelector("#fs-redraw-tree");
442
+ let close;
443
+ openBtn.addEventListener("click", async () => {
444
+ if (close) close();
445
+ const fs = await openRepository();
446
+ const channel = await relayLib.newRemoteRelayChannel();
447
+ close = () => channel.close();
448
+ await startFileServer(SERVICE_KEY, fs, channel.port);
449
+ openBtn.parentElement.style.display = "none";
450
+ refreshBtn.style.display = "block";
451
+ renderFilesExplorer(fs);
452
+ refreshBtn.addEventListener("click", (event) => {
453
+ renderFilesExplorer(fs);
454
+ });
455
+ })
456
+ }
457
+
458
+
459
+ async function renderFilesExplorer(fs) {
460
+ const baseUrl = \`\${ENDPOINT_BASE_URL}/~\${SERVICE_KEY}\`;
461
+ const container = document.querySelector("#fs-view");
462
+ container.innerHTML = "";
463
+ container.classList.add("tree");
464
+ for await (let file of fs.list("/", { recursive: false })) {
465
+ renderFile({
466
+ fs,
467
+ file,
468
+ baseUrl,
469
+ container
470
+ })
471
+ }
472
+ }
473
+
474
+ function renderFile({
475
+ fs,
476
+ file,
477
+ container,
478
+ baseUrl
479
+ }) {
480
+ let path = file.path;
481
+ const isDirectory = file.kind === "directory";
482
+ if (isDirectory) {
483
+ if (path[path.length - 1] !== '/') {
484
+ path += '/';
485
+ }
486
+ }
487
+
488
+ const treeNodeElm = newElement("tree-node", container);
489
+ const treeNodeContentElm = newElement("tree-node-content", treeNodeElm);
490
+ const iconElm = newElement("tree-node-icon", treeNodeContentElm);
491
+ const iconCls = isDirectory ? "icon-dir" : "icon-file";
492
+ iconElm.classList.add(iconCls);
493
+ iconElm.innerText = isDirectory ? 'dir' : 'file';
494
+
495
+ const linkElm = newElement("file-name", treeNodeContentElm, "a");
496
+ const fileUrl = baseUrl + path;
497
+ linkElm.setAttribute("href", fileUrl);
498
+ linkElm.setAttribute("target", "_blank");
499
+ linkElm.innerText = file.name;
500
+
501
+ if (isDirectory) {
502
+ let opened = false;
503
+ linkElm.addEventListener("click", async (ev) => {
504
+ ev.preventDefault();
505
+ ev.stopPropagation();
506
+ opened = !opened;
507
+ if (opened) {
508
+ for await (let child of fs.list(file, { recursive: false })) {
509
+ renderFile({
510
+ fs,
511
+ file : child,
512
+ baseUrl,
513
+ container : treeNodeElm
514
+ })
515
+ }
516
+ } else {
517
+ let elm = treeNodeElm.querySelector(".tree-node-content");
518
+ elm = elm?.nextElementSibling;
519
+ while (elm) {
520
+ const next = elm.nextElementSibling;
521
+ treeNodeElm.removeChild(elm);
522
+ elm = next;
523
+ }
524
+ }
525
+ })
526
+ }
527
+
528
+ function newElement(className, parent, name = "div") {
529
+ const div = document.createElement(name);
530
+ className
531
+ .split(/\\s+/)
532
+ .map(s => s.trim())
533
+ .forEach(cls => div.classList.add(cls));
534
+ if (parent) {
535
+ parent.appendChild(div);
536
+ }
537
+ return div;
538
+ }
539
+ }
540
+
541
+ async function openRepository(readwrite = true) {
542
+ const handler = await window.showDirectoryPicker();
543
+ if (!(await verifyPermission(handler, readwrite))) {
544
+ throw new Error("Access was not granted");
545
+ }
546
+ const fs = new BrowserFilesApi({ rootHandler : handler });
547
+ return fs;
548
+ }
549
+
550
+ async function startFileServer(serviceKey, fs, port) {
551
+ await relayLib.initHttpService(async (req) => {
552
+ try {
553
+ let path = new URL(req.url).pathname;
554
+ path = path.replace(/^[^~]+~[^\\/]+?(\\/.*)$/, '$1');
555
+ if (path[path.length - 1] === '/') {
556
+ path += 'index.html';
557
+ }
558
+ const mimeType = lookup(path.toLowerCase()) || "application/octet-stream";
559
+ const fileInfo = await fs.stats(path);
560
+ if (!fileInfo || (fileInfo.kind !== "file")) {
561
+ return new Response(JSON.stringify({
562
+ status: 404,
563
+ statusText: "File not found. Path: '" + path + "'.",
564
+ }, null, 2), {
565
+ status: 404,
566
+ statusText : "File not found. Path: '" + path + "'.",
567
+ headers : {
568
+ "Content-Type": "application/json"
569
+ }
570
+ });
571
+ }
572
+ const content = await fs.read(fileInfo);
573
+ const responseBody = await relayLib.toReadableStream(content);
574
+ return new Response(responseBody, {
575
+ status: 200,
576
+ headers: {
577
+ "Content-Type": fileInfo.type,
578
+ },
579
+ });
580
+ } catch (error) {
581
+ console.error(error);
582
+ return new Response(JSON.stringify(error), {
583
+ status: 500,
584
+ statusText: error.message,
585
+ });
586
+ }
587
+ }, {
588
+ key: serviceKey,
589
+ port,
590
+ });
591
+ }
592
+ <\/script>
593
+ </body>
594
+ </html>
595
+ </script>
596
+ </body>
597
+
598
+ </html>
package/demo/demo-2.html CHANGED
@@ -5,110 +5,8 @@
5
5
  <meta charset="utf-8" />
6
6
  <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
7
7
  <meta name="viewport" content="width=device-width, initial-scale=1" />
8
- </head>
9
-
10
- <body>
11
- <div id="view"></div>
12
-
13
- <script type="module">
14
- import * as WebRunHttp from "https://unpkg.com/@statewalker/webrun-http-browser@0.3/dist/index.js";
15
- import { Hono } from "https://unpkg.com/hono@3.5/dist/index.js";
16
- import mime from "https://esm.sh/mime-types";
17
- import {
18
- BrowserFilesApi,
19
- FilesApi,
20
- isHandlerAccessible,
21
- MemFilesApi,
22
- verifyPermission,
23
- } from "https://unpkg.com/@statewalker/webrun-files@0.2.0/dist/index.js";
24
-
25
- Promise.resolve().then(main).catch(console.error);
26
- async function main() {
27
- // Load a Hono request router:
28
- const router = getRouter();
29
-
30
- // Create an HTTP Server with the specified name.
31
- // It uses the given Hono router to dispatch requests
32
- // between endpoints.
33
- const serviceBaseUrl = await startHttpService({
34
- serviceKey: "MY_STATIC_SITE_LAUNCHER",
35
- router
36
- })
37
- const siteUrl = `${serviceBaseUrl}/fs/index.html`;
38
-
39
- // Now we can visualize the site in an iframe iframe:
40
- const container = document.querySelector("#view");
41
-
42
- // We also append a reference to this site.
43
- // It allows to open the site in a separate window:
44
- const a = document.createElement("a");
45
- a.target = "_blank";
46
- a.href = siteUrl;
47
- a.innerText = siteUrl;
48
- container.appendChild(a);
49
- }
50
-
51
- async function startHttpService({
52
- serviceKey,
53
- router
54
- }) {
55
- // Step 1: Start a ServiceWorker and open a MessageChannel used to recieve HTTP requests and send responses:
56
- const connection = await WebRunHttp.newRemoteRelayChannel();
57
-
58
- // Step 2: Define an HTTP handler
59
- const serviceBaseUrl = `${connection.baseUrl}~${serviceKey}`;
60
- const app = new Hono({
61
- getPath: (req) => req.url.substring(serviceBaseUrl.length)
62
- });
63
- // Add the router
64
- app.route("/", router);
65
- const httpHandler = (request) => app.fetch(request);
66
-
67
- // Step 3: Instantiate a "server" using the communcation channel with the ServiceWorker:
68
- const close = await WebRunHttp.initHttpService(httpHandler, {
69
- key: serviceKey, // Service prefix
70
- port: connection.port
71
- });
72
-
73
- return serviceBaseUrl;
74
- }
75
-
76
- function getRouter() {
77
- const router = new Hono();
78
- const files = [...document.querySelectorAll("[data-file]")].reduce((index, elm) => {
79
- const fileName = elm.getAttribute("data-file");
80
- const value = elm.innerText.replaceAll("<\\/script>", "</" + "script>");
81
- index[fileName] = value;
82
- return index;
83
- }, {})
84
- router.get("/*", newStaticFilesDispatcher(files))
85
- return router;
86
- }
87
-
88
- function newStaticFilesDispatcher(files) {
89
- return function showStaticFiles(context) {
90
- let { url, path } = context.req;
91
- path = path.replace(/#.*$/, "");
92
- if (path.match(/\/$/)) {
93
- path = path + "index.html";
94
- }
95
- const result = files[path];
96
- if (!result) {
97
- return context.notFound();
98
- }
99
- const mimeType = mime.lookup(path) || "application/octet-stream";
100
- context.header("Content-Type", mimeType);
101
- context.status(201);
102
- if (typeof result === "object") {
103
- return context.json(result);
104
- } else {
105
- return context.body(result);
106
- }
107
- };
108
- }
109
- </script>
110
-
111
- <script type="template/file" data-file="/fs/files-tree.css">
8
+ <style>
9
+ /* CSS styles for files tree. */
112
10
  .tree {
113
11
  --tree-line-width: 1px;
114
12
  --tree-line-color: silver;
@@ -172,16 +70,35 @@
172
70
  width: 1.5em;
173
71
  color: gray;
174
72
  }
175
- </script>
176
- <script type="template/file" data-file="/fs/index.html">
177
- <!DOCTYPE HTML>
178
- <html>
179
- <head>
180
- <meta charset="utf-8" />
181
- <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
182
- <meta name="viewport" content="width=device-width, initial-scale=1" />
183
- <link crossorigin="anonymous" media="all" rel="stylesheet" href="./files-tree.css" />
73
+
74
+ /* */
75
+ .files-panel {
76
+ display: flex;
77
+ align-items: stretch;
78
+ align-content: stretch;
79
+ justify-content: stretch;
80
+ min-height: 25em;
81
+ gap: 1em;
82
+ width: 100%;
83
+ }
84
+ .files-panel .files-explorer {
85
+ min-width: 15em;
86
+ border: 1px solid #eee;
87
+ flex: none;
88
+ }
89
+ .files-panel .files-preview {
90
+ flex: auto;
91
+ border: 1px solid red;
92
+ }
93
+ .files-panel .files-preview iframe {
94
+ width: 100%;
95
+ height: calc(100% - 1em);
96
+ border: none;
97
+ outline: none;
98
+ }
99
+ </style>
184
100
  </head>
101
+
185
102
  <body>
186
103
  <h3>Local File System</h3>
187
104
  <p>
@@ -191,69 +108,67 @@
191
108
  <button id="fs-open">Open Workspace</button>
192
109
  <button id="fs-redraw-tree" style="display: none;">Redraw Files Tree</button>
193
110
  </div>
194
- <div id="fs-view"></div>
195
-
196
- <script type="module" src="./js/index.js"><\/script>
197
- </body>
198
- </html>
199
- </script>
111
+ <div class="files-panel">
112
+ <div class="files-explorer">
113
+ Files Tree:
114
+ <div id="fs-view"></div>
115
+ </div>
116
+ <div class="files-preview">
117
+ Files Preview:
118
+ <iframe id="fs-preview" name="fs-preview" width="100%" height="100%" src=""></iframe>
119
+ </div>
120
+ </div>
200
121
 
201
- <script data-file="/fs/js/deps.js" type="template/file">
202
- export * as WebRunHttp from "https://unpkg.com/@statewalker/webrun-http-browser@0.3/dist/index.js";
203
- export { Hono } from "https://unpkg.com/hono@3.5/dist/index.js";
204
- export {
122
+ <script type="module">
123
+ import * as WebRunHttp from "https://unpkg.com/@statewalker/webrun-http-browser@0.3/dist/index.js";
124
+ import { Hono } from "https://unpkg.com/hono@3.5/dist/index.js";
125
+ import mime from "https://esm.sh/mime-types";
126
+ import {
205
127
  BrowserFilesApi,
206
128
  verifyPermission,
207
129
  } from "https://unpkg.com/@statewalker/webrun-files@0.2.0/dist/index.js";
208
- export * as mime from "https://esm.sh/mime-types";
209
- </script>
210
-
211
-
212
- <script data-file="/fs/js/openRepository.js" type="template/file">
213
- import { BrowserFilesApi, verifyPermission } from "./deps.js";
214
- export async function openRepository(readwrite = true) {
215
- const handler = await window.showDirectoryPicker();
216
- if (!(await verifyPermission(handler, readwrite))) {
217
- throw new Error("Access was not granted");
218
- }
219
- const fs = new BrowserFilesApi({ rootHandler : handler });
220
- return fs;
221
- }
222
- </script>
223
-
224
- <script data-file="/fs/js/startHttpService.js" type="template/file">
225
- import { WebRunHttp, Hono } from "./deps.js";
226
130
 
227
- export async function startHttpService({
228
- serviceKey,
229
- router
230
- }) {
231
- // Step 1: Start a ServiceWorker and open a MessageChannel used to recieve HTTP requests and send responses:
232
- const connection = await WebRunHttp.newRemoteRelayChannel();
131
+ Promise.resolve().then(main).catch(console.error);
132
+ async function main() {
233
133
 
234
- // Step 2: Define an HTTP handler
235
- const serviceBaseUrl = `${connection.baseUrl}~${serviceKey}`;
236
- const app = new Hono({
237
- getPath: (req) => req.url.substring(serviceBaseUrl.length)
238
- });
239
- // Add the router
240
- app.route("/", router);
241
- const httpHandler = (request) => app.fetch(request);
134
+ const openBtn = document.querySelector("#fs-open");
135
+ const refreshBtn = document.querySelector("#fs-redraw-tree");
136
+ let close;
137
+ openBtn.addEventListener("click", async () => {
138
+ if (close) close();
139
+ // Open a local folder:
140
+ const fs = await openRepository();
141
+ // Create a dispatcher between files.
142
+ // This dispatcher get the requested path, read the
143
+ // file content and return it to the caller with
144
+ // a correct mime type:
145
+ const router = await newFilesRouter(fs);
242
146
 
243
- // Step 3: Instantiate a "server" using the communcation channel with the ServiceWorker:
244
- const close = await WebRunHttp.initHttpService(httpHandler, {
245
- key: serviceKey, // Service prefix
246
- port: connection.port
247
- });
147
+ // Create an HTTP Server with the specified name.
148
+ // It uses the given Hono router to dispatch requests
149
+ // between endpoints.
150
+ const SERVICE_KEY = "FS"
151
+ const serviceBaseUrl = await startHttpService({
152
+ serviceKey : SERVICE_KEY,
153
+ router
154
+ })
248
155
 
249
- return serviceBaseUrl;
156
+ openBtn.style.display = "none";
157
+ refreshBtn.style.display = "block";
158
+ const target = "fs-preview";
159
+ const filesRenderOptions = {
160
+ fs,
161
+ baseUrl : serviceBaseUrl,
162
+ target
163
+ };
164
+ renderFilesExplorer(filesRenderOptions);
165
+ refreshBtn.addEventListener("click", (event) => {
166
+ renderFilesExplorer(filesRenderOptions);
167
+ });
168
+ })
250
169
  }
251
- </script>
252
-
253
- <script data-file="/fs/js/newFilesRouter.js" type="template/file">
254
- import { Hono, WebRunHttp } from "./deps.js";
255
170
 
256
- export function newFilesRouter(fs) {
171
+ function newFilesRouter(fs) {
257
172
  const router = new Hono();
258
173
  router.get("/*", async (context) => {
259
174
  let { url, path } = context.req;
@@ -277,187 +192,43 @@
277
192
  return router;
278
193
  }
279
194
 
280
- </script>
281
-
282
- <script data-file="/fs/js/index.js" type="template/file">
283
- console.log("Hell-o!");
284
- const SERVICE_KEY = "FS"
285
- import { Hono, WebRunHttp } from "./deps.js";
286
- import { startHttpService } from "./startHttpService.js";
287
- import { openRepository } from "./openRepository.js";
288
- import { newFilesRouter } from "./newFilesRouter.js";
289
-
290
- Promise.resolve().then(main).catch(console.error);
291
-
292
- async function main() {
293
- const openBtn = document.querySelector("#fs-open");
294
- const refreshBtn = document.querySelector("#fs-redraw-tree");
295
- let close;
296
- openBtn.addEventListener("click", async () => {
297
- if (close) close();
298
- const fs = await openRepository();
299
- const router = await newFilesRouter(fs);
300
- const serviceBaseUrl = await startHttpService({
301
- serviceKey : SERVICE_KEY,
302
- router
303
- })
304
-
305
- openBtn.parentElement.style.display = "none";
306
- refreshBtn.style.display = "block";
307
- renderFilesExplorer(fs, serviceBaseUrl);
308
- refreshBtn.addEventListener("click", (event) => {
309
- renderFilesExplorer(fs, serviceBaseUrl);
310
- });
311
- })
312
- }
313
-
314
- async function renderFilesExplorer(fs, baseUrl) {
315
- const container = document.querySelector("#fs-view");
316
- container.innerHTML = "";
317
- container.classList.add("tree");
318
- for await (let file of fs.list("/", { recursive: false })) {
319
- renderFile({
320
- fs,
321
- file,
322
- baseUrl,
323
- container
324
- })
195
+ async function openRepository(readwrite = true) {
196
+ const handler = await window.showDirectoryPicker();
197
+ if (!(await verifyPermission(handler, readwrite))) {
198
+ throw new Error("Access was not granted");
325
199
  }
200
+ const fs = new BrowserFilesApi({ rootHandler : handler });
201
+ return fs;
326
202
  }
327
203
 
328
- function renderFile({
329
- fs,
330
- file,
331
- container,
332
- baseUrl
204
+ async function startHttpService({
205
+ serviceKey,
206
+ router
333
207
  }) {
334
- let path = file.path;
335
- const isDirectory = file.kind === "directory";
336
- if (isDirectory) {
337
- if (path[path.length - 1] !== '/') {
338
- path += '/';
339
- }
340
- }
341
-
342
- const treeNodeElm = newElement("tree-node", container);
343
- const treeNodeContentElm = newElement("tree-node-content", treeNodeElm);
344
- const iconElm = newElement("tree-node-icon", treeNodeContentElm);
345
- const iconCls = isDirectory ? "icon-dir" : "icon-file";
346
- iconElm.classList.add(iconCls);
347
- iconElm.innerText = isDirectory ? 'dir' : 'file';
348
-
349
- const linkElm = newElement("file-name", treeNodeContentElm, "a");
350
- const fileUrl = baseUrl + path;
351
- linkElm.setAttribute("href", fileUrl);
352
- linkElm.setAttribute("target", "_blank");
353
- linkElm.innerText = file.name;
354
-
355
- if (isDirectory) {
356
- let opened = false;
357
- linkElm.addEventListener("click", async (ev) => {
358
- ev.preventDefault();
359
- ev.stopPropagation();
360
- opened = !opened;
361
- if (opened) {
362
- for await (let child of fs.list(file, { recursive: false })) {
363
- renderFile({
364
- fs,
365
- file : child,
366
- baseUrl,
367
- container : treeNodeElm
368
- })
369
- }
370
- } else {
371
- let elm = treeNodeElm.querySelector(".tree-node-content");
372
- elm = elm?.nextElementSibling;
373
- while (elm) {
374
- const next = elm.nextElementSibling;
375
- treeNodeElm.removeChild(elm);
376
- elm = next;
377
- }
378
- }
379
- })
380
- }
381
-
382
- function newElement(className, parent, name = "div") {
383
- const div = document.createElement(name);
384
- className
385
- .split(/\\s+/)
386
- .map(s => s.trim())
387
- .forEach(cls => div.classList.add(cls));
388
- if (parent) {
389
- parent.appendChild(div);
390
- }
391
- return div;
392
- }
393
- }
394
-
395
- </script>
396
-
397
-
398
- <script type="template/file" data-file="/fs/index-1.html">
399
- <!DOCTYPE HTML>
400
- <html>
401
- <head>
402
- <meta charset="utf-8" />
403
- <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
404
- <meta name="viewport" content="width=device-width, initial-scale=1" />
405
- <link crossorigin="anonymous" media="all" rel="stylesheet" href="./files-tree.css" />
406
- </head>
407
- <body>
408
- <h3>Local File System</h3>
409
- <p>
410
- This page allows you to get read/write access to your local file system and develop the whole site directly in the browser without an HTTP server.
411
- </p>
412
-
413
- <div>
414
- <button id="fs-open">Open File System</button>
415
- <div><small>When you click on this button the browser will ask you to give access to the directory you want to explore in your browser.</small></div>
416
- </div>
417
-
418
- <div>
419
- <button id="fs-redraw-tree" style="display: none;">Refresh</button>
420
- <div id="fs-view"></div>
421
- </div>
422
-
423
- <script type="module">
424
- const SERVICE_KEY = "FS"
425
- const ENDPOINT_BASE_URL = "https://unpkg.com/@statewalker/webrun-http-browser@0.2.1/public-relay"
426
- import {
427
- BrowserFilesApi,
428
- FilesApi,
429
- isHandlerAccessible,
430
- MemFilesApi,
431
- verifyPermission,
432
- } from "https://unpkg.com/@statewalker/webrun-files@0.2.0/dist/index.js";
433
- import * as relayLib from "https://unpkg.com/@statewalker/webrun-http-browser@0.2.1/dist/index.js";
434
- import { lookup } from "https://esm.sh/mime-types";
208
+ // Step 1: Start a ServiceWorker and open a MessageChannel used to recieve HTTP requests and send responses:
209
+ const connection = await WebRunHttp.newRemoteRelayChannel();
435
210
 
211
+ // Step 2: Define an HTTP handler
212
+ const serviceBaseUrl = `${connection.baseUrl}~${serviceKey}`;
213
+ const app = new Hono({
214
+ getPath: (req) => req.url.substring(serviceBaseUrl.length)
215
+ });
216
+ // Add the router
217
+ app.route("/", router);
218
+ const httpHandler = (request) => app.fetch(request);
436
219
 
437
- Promise.resolve().then(main).catch(console.error);
220
+ // Step 3: Instantiate a "server" using the communcation channel with the ServiceWorker:
221
+ const close = await WebRunHttp.initHttpService(httpHandler, {
222
+ key: serviceKey, // Service prefix
223
+ port: connection.port
224
+ });
438
225
 
439
- async function main() {
440
- const openBtn = document.querySelector("#fs-open");
441
- const refreshBtn = document.querySelector("#fs-redraw-tree");
442
- let close;
443
- openBtn.addEventListener("click", async () => {
444
- if (close) close();
445
- const fs = await openRepository();
446
- const channel = await relayLib.newRemoteRelayChannel();
447
- close = () => channel.close();
448
- await startFileServer(SERVICE_KEY, fs, channel.port);
449
- openBtn.parentElement.style.display = "none";
450
- refreshBtn.style.display = "block";
451
- renderFilesExplorer(fs);
452
- refreshBtn.addEventListener("click", (event) => {
453
- renderFilesExplorer(fs);
454
- });
455
- })
226
+ return serviceBaseUrl;
456
227
  }
457
228
 
229
+ // Files tree visualization
458
230
 
459
- async function renderFilesExplorer(fs) {
460
- const baseUrl = \`\${ENDPOINT_BASE_URL}/~\${SERVICE_KEY}\`;
231
+ async function renderFilesExplorer({ fs, baseUrl, target = "_blank" }) {
461
232
  const container = document.querySelector("#fs-view");
462
233
  container.innerHTML = "";
463
234
  container.classList.add("tree");
@@ -466,7 +237,8 @@ This page allows you to get read/write access to your local file system and deve
466
237
  fs,
467
238
  file,
468
239
  baseUrl,
469
- container
240
+ container,
241
+ target
470
242
  })
471
243
  }
472
244
  }
@@ -475,7 +247,8 @@ This page allows you to get read/write access to your local file system and deve
475
247
  fs,
476
248
  file,
477
249
  container,
478
- baseUrl
250
+ baseUrl,
251
+ target
479
252
  }) {
480
253
  let path = file.path;
481
254
  const isDirectory = file.kind === "directory";
@@ -495,7 +268,7 @@ This page allows you to get read/write access to your local file system and deve
495
268
  const linkElm = newElement("file-name", treeNodeContentElm, "a");
496
269
  const fileUrl = baseUrl + path;
497
270
  linkElm.setAttribute("href", fileUrl);
498
- linkElm.setAttribute("target", "_blank");
271
+ linkElm.setAttribute("target", target);
499
272
  linkElm.innerText = file.name;
500
273
 
501
274
  if (isDirectory) {
@@ -510,7 +283,8 @@ This page allows you to get read/write access to your local file system and deve
510
283
  fs,
511
284
  file : child,
512
285
  baseUrl,
513
- container : treeNodeElm
286
+ container : treeNodeElm,
287
+ target
514
288
  })
515
289
  }
516
290
  } else {
@@ -538,60 +312,6 @@ This page allows you to get read/write access to your local file system and deve
538
312
  }
539
313
  }
540
314
 
541
- async function openRepository(readwrite = true) {
542
- const handler = await window.showDirectoryPicker();
543
- if (!(await verifyPermission(handler, readwrite))) {
544
- throw new Error("Access was not granted");
545
- }
546
- const fs = new BrowserFilesApi({ rootHandler : handler });
547
- return fs;
548
- }
549
-
550
- async function startFileServer(serviceKey, fs, port) {
551
- await relayLib.initHttpService(async (req) => {
552
- try {
553
- let path = new URL(req.url).pathname;
554
- path = path.replace(/^[^~]+~[^\\/]+?(\\/.*)$/, '$1');
555
- if (path[path.length - 1] === '/') {
556
- path += 'index.html';
557
- }
558
- const mimeType = lookup(path.toLowerCase()) || "application/octet-stream";
559
- const fileInfo = await fs.stats(path);
560
- if (!fileInfo || (fileInfo.kind !== "file")) {
561
- return new Response(JSON.stringify({
562
- status: 404,
563
- statusText: "File not found. Path: '" + path + "'.",
564
- }, null, 2), {
565
- status: 404,
566
- statusText : "File not found. Path: '" + path + "'.",
567
- headers : {
568
- "Content-Type": "application/json"
569
- }
570
- });
571
- }
572
- const content = await fs.read(fileInfo);
573
- const responseBody = await relayLib.toReadableStream(content);
574
- return new Response(responseBody, {
575
- status: 200,
576
- headers: {
577
- "Content-Type": fileInfo.type,
578
- },
579
- });
580
- } catch (error) {
581
- console.error(error);
582
- return new Response(JSON.stringify(error), {
583
- status: 500,
584
- statusText: error.message,
585
- });
586
- }
587
- }, {
588
- key: serviceKey,
589
- port,
590
- });
591
- }
592
- <\/script>
593
- </body>
594
- </html>
595
315
  </script>
596
316
  </body>
597
317
 
@@ -1,4 +1,4 @@
1
- // @statewalker/webrun-http-browser v0.3.2 https://github.com/statewalker/statewalker-webrun-http-browser Copyright 2023 Mikhail Kotelnikov
1
+ // @statewalker/webrun-http-browser v0.3.3 https://github.com/statewalker/statewalker-webrun-http-browser Copyright 2023 Mikhail Kotelnikov
2
2
  (function (factory) {
3
3
  typeof define === 'function' && define.amd ? define(factory) :
4
4
  factory();
@@ -1,2 +1,2 @@
1
- // @statewalker/webrun-http-browser v0.3.2 https://github.com/statewalker/statewalker-webrun-http-browser Copyright 2023 Mikhail Kotelnikov
1
+ // @statewalker/webrun-http-browser v0.3.3 https://github.com/statewalker/statewalker-webrun-http-browser Copyright 2023 Mikhail Kotelnikov
2
2
  !function(t){"function"==typeof define&&define.amd?define(t):t()}((function(){"use strict";function t(t){return new Promise(((e,n)=>{t.oncomplete=t.onsuccess=()=>e(t.result),t.onabort=t.onerror=()=>n(t.error)}))}let e;function n(){return e||(e=function(e,n){const r=indexedDB.open(e);r.onupgradeneeded=()=>r.result.createObjectStore(n);const s=t(r);return(t,e)=>s.then((r=>e(r.transaction(n,t).objectStore(n))))}("keyval-store","keyval")),e}function r(e,r=n()){return r("readonly",(n=>t(n.get(e))))}function s(e,r,s=n()){return s("readwrite",(n=>(n.put(r,e),t(n.transaction))))}function a(t){return Object.assign({message:t,stack:t.stack},t)}function o(t){return"string"==typeof t&&(t={message:t}),Object.assign(new Error(t.message),t)}async function i(t,e,n,...r){return new Promise(((s,a)=>{const i=new MessageChannel;i.port1.onmessage=t=>{const{result:e,error:n}=t.data;n?a(o(n)):s(e)},t.postMessage({type:e,params:n},[i.port2,...r])}))}function c(t,e,n){const r=async t=>{if(!t.data||t.data.type!==e)return;const[r,...s]=t.ports;let o={};try{const e=t.data.params;o.result=await n(t,e,...s)}catch(t){o.error=a(t)}r.postMessage(o)};t.addEventListener("message",r);try{t.start&&t.start()}catch(t){}return()=>t.removeEventListener("message",r)}async function*l(t){yield*async function*(t,e=[]){let n,r,s=async(t,n,s)=>{const a={error:t,value:n,done:s};return a.promise=new Promise((t=>a.notify=t)),await e.push(a),r&&r(),r=null,a.promise};const a=t=>s(void 0,t,!1),o=()=>s(void 0,void 0,!0),i=t=>s(t,void 0,!0),c=t(Object.assign([a,o,i],{next:a,complete:o,error:i}));let l;try{for(;;)if(l=await e.shift(),l)try{if(l.error)throw l.error;if(l.done)break;yield l.value}finally{l.notify(!0)}else await(n=r?n:new Promise((t=>r=t)))}finally{for(r&&r(),s=()=>!1,"function"==typeof c&&await c();l=await e.shift();)l.notify(!1)}}((e=>t((async({done:t=!0,value:n,error:r}={})=>{r?await e.error(r):t?await e.complete():await e.next(n)}))))}async function*u(t,e,n={}){const r=new MessageChannel;t.postMessage({type:"START_CALL",params:n},[r.port2]);const s=function(t){let e=[],n=[];const r=async t=>{for(let n of e)await n(t)},s=d({port:t,handler:r});async function*a(){yield*l((t=>(e.push(t),()=>e=e.filter((e=>e!==t)))))}async function o(t){await async function(t,e){let n;try{for await(let n of e)await t({done:!1,value:n})}catch(t){n=t}finally{await t({done:!0,error:n})}}(s.invoke,async function*(){try{n.push(t),yield*t}finally{n=n.filter((e=>e!==t))}}())}return{start:()=>s.start(),close:async()=>{await r({done:!0}),[...n].forEach((t=>t.return&&t.return())),await s.close()},recieveAll:a,sendAll:o}}(r.port1);try{await s.start(),s.sendAll(e),yield*s.recieveAll()}finally{await s.close()}}function d({port:t,handler:e=(()=>{throw new Error("Handler not implemented")}),onError:n=console.error,newCallId:r=(()=>d.__counter=(d.__counter||0)+1)}){const s="REQUEST",i="RESPONSE",c="message",l={},u=async n=>{const r=n.data||{},{type:c,callId:u}=r;if(c===s)try{const{request:s}=r;let a=await e(s,...n.ports||[]);const[o,...c]=Array.isArray(a)?a:void 0!==a?[a]:[];t.postMessage({type:i,callId:u,response:o},c)}catch(e){t.postMessage({type:i,callId:u,error:a(e)})}else if(c===i){const{callId:t,response:e,error:n}=r,s=l[t];delete l[t],s&&(n?s.reject(o(n)):s.resolve(e))}};return{start:async()=>{try{t.addEventListener(c,u),t.start&&await t.start()}catch(t){n(t)}},close:async()=>{try{t.removeEventListener(c,u),t.close&&await t.close()}catch(t){n(t)}},invoke:async function(e={},...n){const a=r();return new Promise(((r,o)=>{try{l[a]={resolve:r,reject:o},t.postMessage({type:s,callId:a,request:e},n)}catch(t){o(t),delete e[a]}}))}}}function f(t){return async e=>{const n=[...(e=await e).headers].map((([t,e])=>[t,e])),r=["url","method","mode","credentials","cache","redirect","referrer","referrerPolicy","integrity","keepalive"].reduce(((t,n)=>{const r=e[n];return void 0!==r&&(t[n]=r),t}),{headers:n}),s=e.body?async function*(t){const e=await t.getReader();let n,r;for(;({done:n,value:r}=await e.read())&&!n;)yield await r}(e.body):[],a=await t({options:r,content:s});let o=null,i={};if(a){i=a.options;const t=a.content;"HEAD"!==r.method&&"OPTIONS"!==r.method?(c=t,o=new ReadableStream({type:"bytes",async pull(t){let e=!1;try{for(;;){const e=await c.next();if(!e||e.done)break;t.enqueue(await e.value)}}catch(n){e=!0,t.error(n)}finally{!e&&t.close()}}})):t&&t.return&&t.return()}else i={status:404,statusText:"Error 404: Not Found"};var c;return new Response(o,i)}}async function y(t,e){const n=f((async e=>await async function(t){t=await t;const{done:e,value:n}=await t.next();let r={};if(!e&&n){const t=(new TextDecoder).decode(n);r=JSON.parse(t)}return{options:r,content:t}}(u(t,async function*(t){const{options:e,content:n}=await t,r=new TextEncoder;yield r.encode(JSON.stringify(e)),yield*n}(e)))));return await n(e)}class w extends Error{constructor(t){super(t.message),this.status=t.status,this.statusText=t.statusText}getResponseOptions(t={}){return Object.assign(this.toJson(),t)}toJson(){return{status:this.status,statusText:this.statusText,message:this.message}}static fromError(t){return t instanceof w||(t=new w(Object.assign({status:500,statusText:"Bad Request",reason:t.message}))),t}static errorResourceNotFound(t={}){return new w({status:404,statusText:"Error 404: Resource not found",...t})}static errorForbidden(t={}){return new w({status:403,statusText:"Error 403: Forbidden",...t})}static errorResourceGone(t={}){return new w({status:410,statusText:"Error 410: Resource Gone",...t})}static errorInternalError(t){return new w({...t,status:500,statusText:"Error 500: Internal error"})}}!function(t){const[e,n]=function(t=console.error){let e=0;const n={},r=r=>{const s=e++;return n[s]=Object.assign((function(e){try{return delete n[s],!e&&r&&r()}catch(e){t(e)}}),{action:r})},s=t=>Object.values(n).forEach((e=>e.action===t&&e(!0))),a=()=>Object.values(n).forEach((t=>t()));return Object.assign([r,a,s],{register:r,clear:a,unregister:s})}();"function"==typeof t.skipWaiting?(console.log("self.skipWaiting() is supported."),t.addEventListener("install",(e=>{e.waitUntil(t.skipWaiting())}))):console.log("self.skipWaiting() is not supported."),t.clients&&"function"==typeof t.clients.claim?(console.log("self.clients.claim() is supported."),t.addEventListener("activate",(e=>{e.waitUntil(t.clients.claim())}))):console.log("self.clients.claim() is not supported.");const a=function({self:t,key:e="clientsIds",get:n,set:r}){let s;async function a(){if(!s){const t=await n(e)||[];s=Object.fromEntries(t)}return s}async function o(){const t=await a(),n=Object.entries(t);return await r(e,n),t}async function i(t,e){const n=await a(),r=e.id;let s=!1;return r in n||(n[t]=r,await o(),s=!0),s}async function c(t){const e=await a();let n=!1;return t in e&&(delete e[t],await o(),n=!0),n}async function l(e){const n=await a(),r=n[e],s=await t.clients.get(r);return s||(delete n[e],await o()),s}return{getClient:l,addClient:i,removeClient:c}}({self:t,get:r,set:s});e(c(t,"REGISTER",(async(t,e)=>await a.addClient(e.key,t.source)))),e(c(t,"UNREGISTER",(async(t,e)=>await a.removeClient(e.key)))),e(c(t,"CONNECT",(async(t,e,n)=>{const{key:r}=e,s=await a.getClient(r);if(!s)throw new Error(`Target client was not found. Target key: "${r}".`);return await i(s,"CONNECT",e,n)})));const o=t=>{const e=t.request,n=function(t,e="~"){let n="",r="",s=t+"";const a=s.indexOf(e);let o="";return a>=0&&(o=s.substring(0,a+e.length),s.substring(a+e.length).replace(/^([^\/]+)/,((t,e)=>(o+=t,o.length<s.length&&(o+="/"),n=e,r=s.substring(o.length),"")))),{url:t,key:n,baseUrl:o,path:r}}(e.url),{key:r}=n;if(r){const s=async()=>{try{const t=new MessageChannel,s=await a.getClient(r);if(!s)throw w.errorResourceGone(n);const o={type:"http",key:r};if(!await i(s,"CONNECT",o,t.port2))throw w.errorForbidden(n);return await y(t.port1,e)}catch(t){const e=(t=w.fromError(t)).getResponseOptions(n);return new Response(JSON.stringify(e),{...e,headers:{"Content-Type":"text/javascript"}})}};t.respondWith(s())}};t.addEventListener("fetch",o),e((()=>t.removeEventListener("fetch",o)))}(self)}));
package/dist/index-sw.js CHANGED
@@ -1,4 +1,4 @@
1
- // @statewalker/webrun-http-browser v0.3.2 https://github.com/statewalker/statewalker-webrun-http-browser Copyright 2023 Mikhail Kotelnikov
1
+ // @statewalker/webrun-http-browser v0.3.3 https://github.com/statewalker/statewalker-webrun-http-browser Copyright 2023 Mikhail Kotelnikov
2
2
  // See https://github.com/agenjs/agen-utils/blob/main/src/iterator.js
3
3
  async function* iterate(init, queue = []) {
4
4
  let promise, notify, push = async (error, value, done) => {
@@ -1,2 +1,2 @@
1
- // @statewalker/webrun-http-browser v0.3.2 https://github.com/statewalker/statewalker-webrun-http-browser Copyright 2023 Mikhail Kotelnikov
1
+ // @statewalker/webrun-http-browser v0.3.3 https://github.com/statewalker/statewalker-webrun-http-browser Copyright 2023 Mikhail Kotelnikov
2
2
  function t(t){return new Promise(((e,n)=>{t.oncomplete=t.onsuccess=()=>e(t.result),t.onabort=t.onerror=()=>n(t.error)}))}let e;function n(){return e||(e=function(e,n){const r=indexedDB.open(e);r.onupgradeneeded=()=>r.result.createObjectStore(n);const s=t(r);return(t,e)=>s.then((r=>e(r.transaction(n,t).objectStore(n))))}("keyval-store","keyval")),e}function r(e,r=n()){return r("readonly",(n=>t(n.get(e))))}function s(e,r,s=n()){return s("readwrite",(n=>(n.put(r,e),t(n.transaction))))}function a(t){return Object.assign({message:t,stack:t.stack},t)}function o(t){return"string"==typeof t&&(t={message:t}),Object.assign(new Error(t.message),t)}async function i(t,e,n,...r){return new Promise(((s,a)=>{const i=new MessageChannel;i.port1.onmessage=t=>{const{result:e,error:n}=t.data;n?a(o(n)):s(e)},t.postMessage({type:e,params:n},[i.port2,...r])}))}function c(t,e,n){const r=async t=>{if(!t.data||t.data.type!==e)return;const[r,...s]=t.ports;let o={};try{const e=t.data.params;o.result=await n(t,e,...s)}catch(t){o.error=a(t)}r.postMessage(o)};t.addEventListener("message",r);try{t.start&&t.start()}catch(t){}return()=>t.removeEventListener("message",r)}async function*l(t){yield*async function*(t,e=[]){let n,r,s=async(t,n,s)=>{const a={error:t,value:n,done:s};return a.promise=new Promise((t=>a.notify=t)),await e.push(a),r&&r(),r=null,a.promise};const a=t=>s(void 0,t,!1),o=()=>s(void 0,void 0,!0),i=t=>s(t,void 0,!0),c=t(Object.assign([a,o,i],{next:a,complete:o,error:i}));let l;try{for(;;)if(l=await e.shift(),l)try{if(l.error)throw l.error;if(l.done)break;yield l.value}finally{l.notify(!0)}else await(n=r?n:new Promise((t=>r=t)))}finally{for(r&&r(),s=()=>!1,"function"==typeof c&&await c();l=await e.shift();)l.notify(!1)}}((e=>t((async({done:t=!0,value:n,error:r}={})=>{r?await e.error(r):t?await e.complete():await e.next(n)}))))}async function*u(t,e,n={}){const r=new MessageChannel;t.postMessage({type:"START_CALL",params:n},[r.port2]);const s=function(t){let e=[],n=[];const r=async t=>{for(let n of e)await n(t)},s=d({port:t,handler:r});async function*a(){yield*l((t=>(e.push(t),()=>e=e.filter((e=>e!==t)))))}async function o(t){await async function(t,e){let n;try{for await(let n of e)await t({done:!1,value:n})}catch(t){n=t}finally{await t({done:!0,error:n})}}(s.invoke,async function*(){try{n.push(t),yield*t}finally{n=n.filter((e=>e!==t))}}())}return{start:()=>s.start(),close:async()=>{await r({done:!0}),[...n].forEach((t=>t.return&&t.return())),await s.close()},recieveAll:a,sendAll:o}}(r.port1);try{await s.start(),s.sendAll(e),yield*s.recieveAll()}finally{await s.close()}}function d({port:t,handler:e=(()=>{throw new Error("Handler not implemented")}),onError:n=console.error,newCallId:r=(()=>d.__counter=(d.__counter||0)+1)}){const s="REQUEST",i="RESPONSE",c="message",l={},u=async n=>{const r=n.data||{},{type:c,callId:u}=r;if(c===s)try{const{request:s}=r;let a=await e(s,...n.ports||[]);const[o,...c]=Array.isArray(a)?a:void 0!==a?[a]:[];t.postMessage({type:i,callId:u,response:o},c)}catch(e){t.postMessage({type:i,callId:u,error:a(e)})}else if(c===i){const{callId:t,response:e,error:n}=r,s=l[t];delete l[t],s&&(n?s.reject(o(n)):s.resolve(e))}};return{start:async()=>{try{t.addEventListener(c,u),t.start&&await t.start()}catch(t){n(t)}},close:async()=>{try{t.removeEventListener(c,u),t.close&&await t.close()}catch(t){n(t)}},invoke:async function(e={},...n){const a=r();return new Promise(((r,o)=>{try{l[a]={resolve:r,reject:o},t.postMessage({type:s,callId:a,request:e},n)}catch(t){o(t),delete e[a]}}))}}}function f(t){return async e=>{const n=[...(e=await e).headers].map((([t,e])=>[t,e])),r=["url","method","mode","credentials","cache","redirect","referrer","referrerPolicy","integrity","keepalive"].reduce(((t,n)=>{const r=e[n];return void 0!==r&&(t[n]=r),t}),{headers:n}),s=e.body?async function*(t){const e=await t.getReader();let n,r;for(;({done:n,value:r}=await e.read())&&!n;)yield await r}(e.body):[],a=await t({options:r,content:s});let o=null,i={};if(a){i=a.options;const t=a.content;"HEAD"!==r.method&&"OPTIONS"!==r.method?(c=t,o=new ReadableStream({type:"bytes",async pull(t){let e=!1;try{for(;;){const e=await c.next();if(!e||e.done)break;t.enqueue(await e.value)}}catch(n){e=!0,t.error(n)}finally{!e&&t.close()}}})):t&&t.return&&t.return()}else i={status:404,statusText:"Error 404: Not Found"};var c;return new Response(o,i)}}async function y(t,e){const n=f((async e=>await async function(t){t=await t;const{done:e,value:n}=await t.next();let r={};if(!e&&n){const t=(new TextDecoder).decode(n);r=JSON.parse(t)}return{options:r,content:t}}(u(t,async function*(t){const{options:e,content:n}=await t,r=new TextEncoder;yield r.encode(JSON.stringify(e)),yield*n}(e)))));return await n(e)}class w extends Error{constructor(t){super(t.message),this.status=t.status,this.statusText=t.statusText}getResponseOptions(t={}){return Object.assign(this.toJson(),t)}toJson(){return{status:this.status,statusText:this.statusText,message:this.message}}static fromError(t){return t instanceof w||(t=new w(Object.assign({status:500,statusText:"Bad Request",reason:t.message}))),t}static errorResourceNotFound(t={}){return new w({status:404,statusText:"Error 404: Resource not found",...t})}static errorForbidden(t={}){return new w({status:403,statusText:"Error 403: Forbidden",...t})}static errorResourceGone(t={}){return new w({status:410,statusText:"Error 410: Resource Gone",...t})}static errorInternalError(t){return new w({...t,status:500,statusText:"Error 500: Internal error"})}}!function(t){const[e,n]=function(t=console.error){let e=0;const n={},r=r=>{const s=e++;return n[s]=Object.assign((function(e){try{return delete n[s],!e&&r&&r()}catch(e){t(e)}}),{action:r})},s=t=>Object.values(n).forEach((e=>e.action===t&&e(!0))),a=()=>Object.values(n).forEach((t=>t()));return Object.assign([r,a,s],{register:r,clear:a,unregister:s})}();"function"==typeof t.skipWaiting?(console.log("self.skipWaiting() is supported."),t.addEventListener("install",(e=>{e.waitUntil(t.skipWaiting())}))):console.log("self.skipWaiting() is not supported."),t.clients&&"function"==typeof t.clients.claim?(console.log("self.clients.claim() is supported."),t.addEventListener("activate",(e=>{e.waitUntil(t.clients.claim())}))):console.log("self.clients.claim() is not supported.");const a=function({self:t,key:e="clientsIds",get:n,set:r}){let s;async function a(){if(!s){const t=await n(e)||[];s=Object.fromEntries(t)}return s}async function o(){const t=await a(),n=Object.entries(t);return await r(e,n),t}async function i(t,e){const n=await a(),r=e.id;let s=!1;return r in n||(n[t]=r,await o(),s=!0),s}async function c(t){const e=await a();let n=!1;return t in e&&(delete e[t],await o(),n=!0),n}async function l(e){const n=await a(),r=n[e],s=await t.clients.get(r);return s||(delete n[e],await o()),s}return{getClient:l,addClient:i,removeClient:c}}({self:t,get:r,set:s});e(c(t,"REGISTER",(async(t,e)=>await a.addClient(e.key,t.source)))),e(c(t,"UNREGISTER",(async(t,e)=>await a.removeClient(e.key)))),e(c(t,"CONNECT",(async(t,e,n)=>{const{key:r}=e,s=await a.getClient(r);if(!s)throw new Error(`Target client was not found. Target key: "${r}".`);return await i(s,"CONNECT",e,n)})));const o=t=>{const e=t.request,n=function(t,e="~"){let n="",r="",s=t+"";const a=s.indexOf(e);let o="";return a>=0&&(o=s.substring(0,a+e.length),s.substring(a+e.length).replace(/^([^\/]+)/,((t,e)=>(o+=t,o.length<s.length&&(o+="/"),n=e,r=s.substring(o.length),"")))),{url:t,key:n,baseUrl:o,path:r}}(e.url),{key:r}=n;if(r){const s=async()=>{try{const t=new MessageChannel,s=await a.getClient(r);if(!s)throw w.errorResourceGone(n);const o={type:"http",key:r};if(!await i(s,"CONNECT",o,t.port2))throw w.errorForbidden(n);return await y(t.port1,e)}catch(t){const e=(t=w.fromError(t)).getResponseOptions(n);return new Response(JSON.stringify(e),{...e,headers:{"Content-Type":"text/javascript"}})}};t.respondWith(s())}};t.addEventListener("fetch",o),e((()=>t.removeEventListener("fetch",o)))}(self);
package/dist/index-umd.js CHANGED
@@ -1,4 +1,4 @@
1
- // @statewalker/webrun-http-browser v0.3.2 https://github.com/statewalker/statewalker-webrun-http-browser Copyright 2023 Mikhail Kotelnikov
1
+ // @statewalker/webrun-http-browser v0.3.3 https://github.com/statewalker/statewalker-webrun-http-browser Copyright 2023 Mikhail Kotelnikov
2
2
  (function (global, factory) {
3
3
  typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
4
4
  typeof define === 'function' && define.amd ? define(['exports'], factory) :
@@ -1,2 +1,2 @@
1
- // @statewalker/webrun-http-browser v0.3.2 https://github.com/statewalker/statewalker-webrun-http-browser Copyright 2023 Mikhail Kotelnikov
1
+ // @statewalker/webrun-http-browser v0.3.3 https://github.com/statewalker/statewalker-webrun-http-browser Copyright 2023 Mikhail Kotelnikov
2
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 r(e,t,r,...o){return new Promise(((a,s)=>{const c=new MessageChannel;c.port1.onmessage=e=>{const{result:t,error:r}=e.data;r?s(n(r)):a(t)},e.postMessage({type:t,params:r},[c.port2,...o])}))}function o(e,n,r){const o=async e=>{if(!e.data||e.data.type!==n)return;const[o,...a]=e.ports;let s={};try{const t=e.data.params;s.result=await r(e,t,...a)}catch(e){s.error=t(e)}o.postMessage(s)};e.addEventListener("message",o);try{e.start&&e.start()}catch(e){}return()=>e.removeEventListener("message",o)}function a(e=console.error){let t=0;const n={},r=r=>{const o=t++;return n[o]=Object.assign((function(t){try{return delete n[o],!t&&r&&r()}catch(t){e(t)}}),{action:r})},o=e=>Object.values(n).forEach((t=>t.action===e&&t(!0))),a=()=>Object.values(n).forEach((e=>e()));return Object.assign([r,a,o],{register:r,clear:a,unregister:o})}async function*s(e){yield*async function*(e,t=[]){let n,r,o=async(e,n,o)=>{const a={error:e,value:n,done:o};return a.promise=new Promise((e=>a.notify=e)),await t.push(a),r&&r(),r=null,a.promise};const a=e=>o(void 0,e,!1),s=()=>o(void 0,void 0,!0),c=e=>o(e,void 0,!0),i=e(Object.assign([a,s,c],{next:a,complete:s,error:c}));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=r?n:new Promise((e=>r=e)))}finally{for(r&&r(),o=()=>!1,"function"==typeof i&&await i();l=await t.shift();)l.notify(!1)}}((t=>e((async({done:e=!0,value:n,error:r}={})=>{r?await t.error(r):e?await t.complete():await t.next(n)}))))}async function c(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})}}async function*i(e,t,n={}){const r=new MessageChannel;e.postMessage({type:"START_CALL",params:n},[r.port2]);const o=u(r.port1);try{await o.start(),o.sendAll(t),yield*o.recieveAll()}finally{await o.close()}}function l(e,t){const n=async e=>{const{type:n,params:r}=e.data||{};if("START_CALL"!==n)return;const o=u(e.ports[0]);try{await o.start();const e=o.recieveAll(),n=await t(e,r);await o.sendAll(n)}finally{await o.close()}};e.addEventListener("message",n);try{e.start&&e.start()}catch(e){}return()=>e.removeEventListener("message",n)}function u(e){let t=[],n=[];const r=async e=>{for(let n of t)await n(e)},o=d({port:e,handler:r});return{start:()=>o.start(),close:async()=>{await r({done:!0}),[...n].forEach((e=>e.return&&e.return())),await o.close()},recieveAll:async function*(){yield*s((e=>(t.push(e),()=>t=t.filter((t=>t!==e)))))},sendAll:async function(e){await c(o.invoke,async function*(){try{n.push(e),yield*e}finally{n=n.filter((t=>t!==e))}}())}}}function d({port:e,handler:r=(()=>{throw new Error("Handler not implemented")}),onError:o=console.error,newCallId:a=(()=>d.__counter=(d.__counter||0)+1)}){const s="REQUEST",c="RESPONSE",i="message",l={},u=async o=>{const a=o.data||{},{type:i,callId:u}=a;if(i===s)try{const{request:t}=a;let n=await r(t,...o.ports||[]);const[s,...i]=Array.isArray(n)?n:void 0!==n?[n]:[];e.postMessage({type:c,callId:u,response:s},i)}catch(n){e.postMessage({type:c,callId:u,error:t(n)})}else if(i===c){const{callId:e,response:t,error:r}=a,o=l[e];delete l[e],o&&(r?o.reject(n(r)):o.resolve(t))}};return{start:async()=>{try{e.addEventListener(i,u),e.start&&await e.start()}catch(e){o(e)}},close:async()=>{try{e.removeEventListener(i,u),e.close&&await e.close()}catch(e){o(e)}},invoke:async function(t={},...n){const r=a();return new Promise(((o,a)=>{try{l[r]={resolve:o,reject:a},e.postMessage({type:s,callId:r,request:t},n)}catch(e){a(e),delete t[r]}}))}}}function f(e){return new ReadableStream({type:"bytes",async pull(t){let n=!1;try{for(;;){const n=await e.next();if(!n||n.done)break;t.enqueue(await n.value)}}catch(e){n=!0,t.error(e)}finally{!n&&t.close()}}})}async function*p(e){const t=await e.getReader();let n,r;for(;({done:n,value:r}=await t.read())&&!n;)yield await r}function y(e){return async t=>{const n=[...(t=await t).headers].map((([e,t])=>[e,t])),r=["url","method","mode","credentials","cache","redirect","referrer","referrerPolicy","integrity","keepalive"].reduce(((e,n)=>{const r=t[n];return void 0!==r&&(e[n]=r),e}),{headers:n}),o=t.body?p(t.body):[],a=await e({options:r,content:o});let s=null,c={};if(a){c=a.options;const e=a.content;"HEAD"!==r.method&&"OPTIONS"!==r.method?s=f(e):e&&e.return&&e.return()}else c={status:404,statusText:"Error 404: Not Found"};return new Response(s,c)}}function w(e){return async t=>{const{options:n,content:r}=await t;let o;const{url:a,mode:s,headers:c=[]}=n,i=new Headers;for(let[e,t]of c)i.append(e,t);delete n.mode,delete n.url,"GET"!==n.method&&"HEAD"!==n.method&&"OPTIONS"!==n.method?(o=f(r),n.duplex="half"):r&&r.return&&r.return();const l=new Request(a,{...n,headers:i,body:o}),u=await e(l);return{options:{status:u.status,statusText:u.statusText,headers:Object.fromEntries([...u.headers])},content:await p(await u.body)}}}async function*m(e){const{options:t,content:n}=await e,r=new TextEncoder;yield r.encode(JSON.stringify(t)),yield*n}async function h(e){e=await e;const{done:t,value:n}=await e.next();let r={};if(!t&&n){const e=(new TextDecoder).decode(n);r=JSON.parse(e)}return{options:r,content:e}}function g(e,t){const n=w(t);return l(e,(e=>m(n(h(e)))))}async function v(e,t){const n=y((async t=>await h(i(e,m(t)))));return await n(t)}function b(){const e=new MessageChannel;return e.port1.onmessage=e=>{navigator.serviceWorker.controller.postMessage(e.data,e.ports)},navigator.serviceWorker.addEventListener("message",(t=>{e.port1.postMessage(t.data,t.ports)})),e.port2}async function E({swUrl:e,scopeUrl:t,type:n}){await navigator.serviceWorker.register(e,{type:n,scope:t});const r=await async function(){return await new Promise((e=>{const t=navigator.serviceWorker;if(t.controller)e(t.controller);else{const n=()=>{e(t.controller),t.removeEventListener("controllerchange",n),t.oncontrollerchange=null};t.addEventListener("controllerchange",n),t.oncontrollerchange=n}}))}();return await async function(e){const t=()=>"activated"===e.state;await new Promise((async n=>{t()?n():e.onstatechange=()=>{t()&&(e.onstatechange=null,n())}}))}(r),r}async function R({key:e,communicationPort:t,...n}){const o=new MessageChannel;return await r(t,"CONNECT",{key:e,...n},o.port2)?o.port1:(o.port1.close(),o.port2.close(),null)}async function k({key:e,handler:t,communicationPort:n}){const[s,c]=a();return await r(n,"REGISTER",{key:e}),s((()=>r(n,"UNREGISTER",{key:e}))),s(o(n,"CONNECT",t)),c}e.callChannel=r,e.callHttpService=async function(e,{key:t,port:n}){const r=await R({key:t,communicationPort:n});return await v(r,e)},e.deserializeError=n,e.fromReadableStream=p,e.getRelayWindowMessageHandler=function({swUrl:e=new URL("./index-sw.js","undefined"==typeof document&&"undefined"==typeof location?new(require("url").URL)("file:"+__filename).href:"undefined"==typeof document?location.href:document.currentScript&&document.currentScript.src||new URL("index-umd.min.js",document.baseURI).href)+"",scopeUrl:t=new URL("../","undefined"==typeof document&&"undefined"==typeof location?new(require("url").URL)("file:"+__filename).href:"undefined"==typeof document?location.href:document.currentScript&&document.currentScript.src||new URL("index-umd.min.js",document.baseURI).href)+""}={}){let n;return async r=>{if("CONNECT"===r.data.type){const o=(r.ports||[])[0];if(n)return void o.close();n=o,await E({swUrl:e,scopeUrl:t});const a=b();a.onmessage=e=>{n.postMessage(e.data,e.ports)},n.onmessage=e=>{a.postMessage(e.data,e.ports)}}}},e.handleChannelCalls=o,e.handleHttpRequests=g,e.handleStreams=l,e.initHttpService=async function(e,{key:t,port:n}){return await k({key:t,communicationPort:n,handler:async(t,n,r)=>(g(r,e),!0)})},e.initServiceWorker=E,e.initServiceWorker1=async function({swUrl:e,scopeUrl:t,type:n}){const{installing:r,waiting:o,active:a}=await navigator.serviceWorker.register(e,{scope:t});(r||o||a).addEventListener("statechange",(e=>{console.log("state",e.target.state)}));const s=navigator.serviceWorker;await new Promise((e=>{if(s.controller)e();else{const t=()=>{e(),s.removeEventListener("controllerchange",t),s.oncontrollerchange=null};s.addEventListener("controllerchange",t),s.oncontrollerchange=t}}));const c=s.controller,i=()=>"activated"===c.state;return await new Promise((async e=>{i()?e(c):c.onstatechange=()=>{i()&&(c.onstatechange=null,e(c))}}))},e.initializeConnection=R,e.newHttpClientStub=y,e.newHttpServerStub=w,e.newInvokationChannel=d,e.newRegistry=a,e.newRemoteRelayChannel=async function({baseUrl:e=new URL("../public-relay/","undefined"==typeof document&&"undefined"==typeof location?new(require("url").URL)("file:"+__filename).href:"undefined"==typeof document?location.href:document.currentScript&&document.currentScript.src||new URL("index-umd.min.js",document.baseURI).href),url:t=new URL("relay.html",e),container:n=document.body}={}){const r=new MessageChannel,{iframe:o,promise:a}=await async function(e){const t=document.createElement("iframe");return t.src=e,Object.assign(t.style,{padding:0,margin:0,border:"none",outline:"none",width:"100%",height:"100%"}),{iframe:t,promise:new Promise(((e,n)=>{try{t.onerror=n,t.onload=()=>e(t)}catch(e){n(e)}}))}}(t);return Object.assign(o.style,{position:"fixed",width:"1px",height:"1px",top:"-1000",left:"-1000",display:"block",opacity:"0",border:"none",outline:"none"}),n.appendChild(o),a.then((()=>{o.contentWindow.postMessage({type:"CONNECT"},"*",[r.port1])})),{baseUrl:e,port:r.port2,close:()=>{o.parentElement&&o.parentElement.removeChild(o),r.port1.close(),r.port2.close()}}},e.newServiceWorkerPort=b,e.recieveData=s,e.registerConnectionsHandler=k,e.sendData=c,e.sendHttpRequest=v,e.sendStream=i,e.serializeError=t,e.splitServiceUrl=function(e,t="~"){let n="",r="",o=e+"";const a=o.indexOf(t);let s="";return a>=0&&(s=o.substring(0,a+t.length),o.substring(a+t.length).replace(/^([^\/]+)/,((e,t)=>(s+=e,s.length<o.length&&(s+="/"),n=t,r=o.substring(s.length),"")))),{url:e,key:n,baseUrl:s,path:r}},e.toReadableStream=f}));
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- // @statewalker/webrun-http-browser v0.3.2 https://github.com/statewalker/statewalker-webrun-http-browser Copyright 2023 Mikhail Kotelnikov
1
+ // @statewalker/webrun-http-browser v0.3.3 https://github.com/statewalker/statewalker-webrun-http-browser Copyright 2023 Mikhail Kotelnikov
2
2
  function serializeError(error) {
3
3
  return Object.assign({
4
4
  message: error,
package/dist/index.min.js CHANGED
@@ -1,2 +1,2 @@
1
- // @statewalker/webrun-http-browser v0.3.2 https://github.com/statewalker/statewalker-webrun-http-browser Copyright 2023 Mikhail Kotelnikov
1
+ // @statewalker/webrun-http-browser v0.3.3 https://github.com/statewalker/statewalker-webrun-http-browser Copyright 2023 Mikhail Kotelnikov
2
2
  function e(e){return Object.assign({message:e,stack:e.stack},e)}function t(e){return"string"==typeof e&&(e={message:e}),Object.assign(new Error(e.message),e)}async function n(e,n,r,...a){return new Promise(((o,s)=>{const c=new MessageChannel;c.port1.onmessage=e=>{const{result:n,error:r}=e.data;r?s(t(r)):o(n)},e.postMessage({type:n,params:r},[c.port2,...a])}))}function r(t,n,r){const a=async t=>{if(!t.data||t.data.type!==n)return;const[a,...o]=t.ports;let s={};try{const e=t.data.params;s.result=await r(t,e,...o)}catch(t){s.error=e(t)}a.postMessage(s)};t.addEventListener("message",a);try{t.start&&t.start()}catch(e){}return()=>t.removeEventListener("message",a)}function a(e=console.error){let t=0;const n={},r=r=>{const a=t++;return n[a]=Object.assign((function(t){try{return delete n[a],!t&&r&&r()}catch(t){e(t)}}),{action:r})},a=e=>Object.values(n).forEach((t=>t.action===e&&t(!0))),o=()=>Object.values(n).forEach((e=>e()));return Object.assign([r,o,a],{register:r,clear:o,unregister:a})}async function*o(e){yield*async function*(e,t=[]){let n,r,a=async(e,n,a)=>{const o={error:e,value:n,done:a};return o.promise=new Promise((e=>o.notify=e)),await t.push(o),r&&r(),r=null,o.promise};const o=e=>a(void 0,e,!1),s=()=>a(void 0,void 0,!0),c=e=>a(e,void 0,!0),i=e(Object.assign([o,s,c],{next:o,complete:s,error:c}));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=r?n:new Promise((e=>r=e)))}finally{for(r&&r(),a=()=>!1,"function"==typeof i&&await i();l=await t.shift();)l.notify(!1)}}((t=>e((async({done:e=!0,value:n,error:r}={})=>{r?await t.error(r):e?await t.complete():await t.next(n)}))))}async function s(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})}}async function*c(e,t,n={}){const r=new MessageChannel;e.postMessage({type:"START_CALL",params:n},[r.port2]);const a=l(r.port1);try{await a.start(),a.sendAll(t),yield*a.recieveAll()}finally{await a.close()}}function i(e,t){const n=async e=>{const{type:n,params:r}=e.data||{};if("START_CALL"!==n)return;const a=l(e.ports[0]);try{await a.start();const e=a.recieveAll(),n=await t(e,r);await a.sendAll(n)}finally{await a.close()}};e.addEventListener("message",n);try{e.start&&e.start()}catch(e){}return()=>e.removeEventListener("message",n)}function l(e){let t=[],n=[];const r=async e=>{for(let n of t)await n(e)},a=u({port:e,handler:r});return{start:()=>a.start(),close:async()=>{await r({done:!0}),[...n].forEach((e=>e.return&&e.return())),await a.close()},recieveAll:async function*(){yield*o((e=>(t.push(e),()=>t=t.filter((t=>t!==e)))))},sendAll:async function(e){await s(a.invoke,async function*(){try{n.push(e),yield*e}finally{n=n.filter((t=>t!==e))}}())}}}function u({port:n,handler:r=(()=>{throw new Error("Handler not implemented")}),onError:a=console.error,newCallId:o=(()=>u.__counter=(u.__counter||0)+1)}){const s="REQUEST",c="RESPONSE",i="message",l={},d=async a=>{const o=a.data||{},{type:i,callId:u}=o;if(i===s)try{const{request:e}=o;let t=await r(e,...a.ports||[]);const[s,...i]=Array.isArray(t)?t:void 0!==t?[t]:[];n.postMessage({type:c,callId:u,response:s},i)}catch(t){n.postMessage({type:c,callId:u,error:e(t)})}else if(i===c){const{callId:e,response:n,error:r}=o,a=l[e];delete l[e],a&&(r?a.reject(t(r)):a.resolve(n))}};return{start:async()=>{try{n.addEventListener(i,d),n.start&&await n.start()}catch(e){a(e)}},close:async()=>{try{n.removeEventListener(i,d),n.close&&await n.close()}catch(e){a(e)}},invoke:async function(e={},...t){const r=o();return new Promise(((a,o)=>{try{l[r]={resolve:a,reject:o},n.postMessage({type:s,callId:r,request:e},t)}catch(t){o(t),delete e[r]}}))}}}function d(e){return new ReadableStream({type:"bytes",async pull(t){let n=!1;try{for(;;){const n=await e.next();if(!n||n.done)break;t.enqueue(await n.value)}}catch(e){n=!0,t.error(e)}finally{!n&&t.close()}}})}async function*y(e){const t=await e.getReader();let n,r;for(;({done:n,value:r}=await t.read())&&!n;)yield await r}function p(e){return async t=>{const n=[...(t=await t).headers].map((([e,t])=>[e,t])),r=["url","method","mode","credentials","cache","redirect","referrer","referrerPolicy","integrity","keepalive"].reduce(((e,n)=>{const r=t[n];return void 0!==r&&(e[n]=r),e}),{headers:n}),a=t.body?y(t.body):[],o=await e({options:r,content:a});let s=null,c={};if(o){c=o.options;const e=o.content;"HEAD"!==r.method&&"OPTIONS"!==r.method?s=d(e):e&&e.return&&e.return()}else c={status:404,statusText:"Error 404: Not Found"};return new Response(s,c)}}function w(e){return async t=>{const{options:n,content:r}=await t;let a;const{url:o,mode:s,headers:c=[]}=n,i=new Headers;for(let[e,t]of c)i.append(e,t);delete n.mode,delete n.url,"GET"!==n.method&&"HEAD"!==n.method&&"OPTIONS"!==n.method?(a=d(r),n.duplex="half"):r&&r.return&&r.return();const l=new Request(o,{...n,headers:i,body:a}),u=await e(l);return{options:{status:u.status,statusText:u.statusText,headers:Object.fromEntries([...u.headers])},content:await y(await u.body)}}}async function*f(e){const{options:t,content:n}=await e,r=new TextEncoder;yield r.encode(JSON.stringify(t)),yield*n}async function g(e){e=await e;const{done:t,value:n}=await e.next();let r={};if(!t&&n){const e=(new TextDecoder).decode(n);r=JSON.parse(e)}return{options:r,content:e}}function m(e,t){const n=w(t);return i(e,(e=>f(n(g(e)))))}async function h(e,t){const n=p((async t=>await g(c(e,f(t)))));return await n(t)}function v(e,t="~"){let n="",r="",a=e+"";const o=a.indexOf(t);let s="";return o>=0&&(s=a.substring(0,o+t.length),a.substring(o+t.length).replace(/^([^\/]+)/,((e,t)=>(s+=e,s.length<a.length&&(s+="/"),n=t,r=a.substring(s.length),"")))),{url:e,key:n,baseUrl:s,path:r}}function E(){const e=new MessageChannel;return e.port1.onmessage=e=>{navigator.serviceWorker.controller.postMessage(e.data,e.ports)},navigator.serviceWorker.addEventListener("message",(t=>{e.port1.postMessage(t.data,t.ports)})),e.port2}async function b({swUrl:e,scopeUrl:t,type:n}){const{installing:r,waiting:a,active:o}=await navigator.serviceWorker.register(e,{scope:t});(r||a||o).addEventListener("statechange",(e=>{console.log("state",e.target.state)}));const s=navigator.serviceWorker;await new Promise((e=>{if(s.controller)e();else{const t=()=>{e(),s.removeEventListener("controllerchange",t),s.oncontrollerchange=null};s.addEventListener("controllerchange",t),s.oncontrollerchange=t}}));const c=s.controller,i=()=>"activated"===c.state;return await new Promise((async e=>{i()?e(c):c.onstatechange=()=>{i()&&(c.onstatechange=null,e(c))}}))}async function k({swUrl:e,scopeUrl:t,type:n}){await navigator.serviceWorker.register(e,{type:n,scope:t});const r=await async function(){return await new Promise((e=>{const t=navigator.serviceWorker;if(t.controller)e(t.controller);else{const n=()=>{e(t.controller),t.removeEventListener("controllerchange",n),t.oncontrollerchange=null};t.addEventListener("controllerchange",n),t.oncontrollerchange=n}}))}();return await async function(e){const t=()=>"activated"===e.state;await new Promise((async n=>{t()?n():e.onstatechange=()=>{t()&&(e.onstatechange=null,n())}}))}(r),r}async function O(e,{key:t,port:n}){return await U({key:t,communicationPort:n,handler:async(t,n,r)=>(m(r,e),!0)})}async function L(e,{key:t,port:n}){const r=await P({key:t,communicationPort:n});return await h(r,e)}function T({swUrl:e=new URL("./index-sw.js",import.meta.url)+"",scopeUrl:t=new URL("../",import.meta.url)+""}={}){let n;return async r=>{if("CONNECT"===r.data.type){const a=(r.ports||[])[0];if(n)return void a.close();n=a,await k({swUrl:e,scopeUrl:t});const o=E();o.onmessage=e=>{n.postMessage(e.data,e.ports)},n.onmessage=e=>{o.postMessage(e.data,e.ports)}}}}async function C({baseUrl:e=new URL("../public-relay/",import.meta.url),url:t=new URL("relay.html",e),container:n=document.body}={}){const r=new MessageChannel,{iframe:a,promise:o}=await async function(e){const t=document.createElement("iframe");return t.src=e,Object.assign(t.style,{padding:0,margin:0,border:"none",outline:"none",width:"100%",height:"100%"}),{iframe:t,promise:new Promise(((e,n)=>{try{t.onerror=n,t.onload=()=>e(t)}catch(e){n(e)}}))}}(t);return Object.assign(a.style,{position:"fixed",width:"1px",height:"1px",top:"-1000",left:"-1000",display:"block",opacity:"0",border:"none",outline:"none"}),n.appendChild(a),o.then((()=>{a.contentWindow.postMessage({type:"CONNECT"},"*",[r.port1])})),{baseUrl:e,port:r.port2,close:()=>{a.parentElement&&a.parentElement.removeChild(a),r.port1.close(),r.port2.close()}}}async function P({key:e,communicationPort:t,...r}){const a=new MessageChannel;return await n(t,"CONNECT",{key:e,...r},a.port2)?a.port1:(a.port1.close(),a.port2.close(),null)}async function U({key:e,handler:t,communicationPort:o}){const[s,c]=a();return await n(o,"REGISTER",{key:e}),s((()=>n(o,"UNREGISTER",{key:e}))),s(r(o,"CONNECT",t)),c}export{n as callChannel,L as callHttpService,t as deserializeError,y as fromReadableStream,T as getRelayWindowMessageHandler,r as handleChannelCalls,m as handleHttpRequests,i as handleStreams,O as initHttpService,k as initServiceWorker,b as initServiceWorker1,P as initializeConnection,p as newHttpClientStub,w as newHttpServerStub,u as newInvokationChannel,a as newRegistry,C as newRemoteRelayChannel,E as newServiceWorkerPort,o as recieveData,U as registerConnectionsHandler,s as sendData,h as sendHttpRequest,c as sendStream,e as serializeError,v as splitServiceUrl,d as toReadableStream};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@statewalker/webrun-http-browser",
3
- "version": "0.3.2",
3
+ "version": "0.3.3",
4
4
  "description": "Service-worker-based HTTP server simulation for browsers",
5
5
  "keywords": [],
6
6
  "homepage": "https://github.com/statewalker/statewalker-webrun-http-browser",