@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
|
@@ -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(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,s,...r){return new Promise(((a,o)=>{const i=new MessageChannel;i.port1.onmessage=e=>{const{result:n,error:s}=e.data;s?o(t(s)):a(n)},e.postMessage({type:n,params:s},[i.port2,...r])}))}function s(t,n,s){const r=async t=>{if(!t.data||t.data.type!==n)return;const[r,...a]=t.ports;let o={};try{const e=t.data.params;o.result=await s(t,e,...a)}catch(t){o.error=e(t)}r.postMessage(o)};t.addEventListener("message",r);try{t.start&&t.start()}catch(e){}return()=>t.removeEventListener("message",r)}function r(e=console.error){let t=0;const n={},s=s=>{const r=t++;return n[r]=Object.assign((function(t){try{return delete n[r],!t&&s&&s()}catch(t){e(t)}}),{action:s})},r=e=>Object.values(n).forEach((t=>t.action===e&&t(!0))),a=()=>Object.values(n).forEach((e=>e()));return Object.assign([s,a,r],{register:s,clear:a,unregister:r})}async function*a(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 o(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 s=new MessageChannel;e.postMessage({type:"START_CALL",params:n},[s.port2]);const r=l(s.port1);try{await r.start(),r.sendAll(t),yield*r.recieveAll()}finally{await r.close()}}function c(e,t){const n=async e=>{const{type:n,params:s}=e.data||{};if("START_CALL"!==n)return;const r=l(e.ports[0]);try{await r.start();const e=r.recieveAll(),n=await t(e,s);await r.sendAll(n)}finally{await r.close()}};e.addEventListener("message",n);try{e.start&&e.start()}catch(e){}return()=>e.removeEventListener("message",n)}function l(e){let t=[],n=[];const s=async e=>{for(let n of t)await n(e)},r=u(e,s);return{start:()=>r.start(),close:async()=>{await s({done:!0}),[...n].forEach((e=>e.return&&e.return())),await r.close()},recieveAll:async function*(){yield*a((e=>(t.push(e),()=>t=t.filter((t=>t!==e)))))},sendAll:async function(e){await o(r.invoke,async function*(){try{n.push(e),yield*e}finally{n=n.filter((t=>t!==e))}}())}}}function u(n,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:u}=c;if(l===r)try{const{request:e}=c;let t=await s(e,...o.ports||[]);const[r,...i]=Array.isArray(t)?t:void 0!==t?[t]:[];n.postMessage({type:a,callId:u,response:r},i)}catch(t){n.postMessage({type:a,callId:u,error:e(t)})}else if(l===a){const{callId:e,response:n,error:s}=c,r=i[e];delete i[e],r&&(s?r.reject(t(s)):r.resolve(n))}};return{start:async()=>{n.addEventListener(o,l),await n.start()},close:async()=>{n.removeEventListener(o,l),await n.close()},invoke:async function(e={},...t){const s=++c;return new Promise(((a,o)=>{try{i[s]={resolve:a,reject:o},n.postMessage({type:r,callId:s,request:e},t)}catch(t){o(t),delete e[s]}}))}}}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*h(e){const t=await e.getReader();let n,s;for(;({done:n,value:s}=await t.read())&&!n;)yield await s}function f(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?h(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?a=d(e):e&&e.return&&e.return()}else o={status:404,statusText:"Error 404: Not Found"};return new Response(a,o)}}function y(e){return async t=>{const{options:n,content:s}=await t;let r;const{url:a,mode:o}=n;delete n.mode,delete n.url,"GET"!==n.method&&"HEAD"!==n.method&&"OPTIONS"!==n.method?(r=d(s),n.duplex="half"):s&&s.return&&s.return();const i=new Request(a,{...n,body:r}),c=await e(i);return{options:{status:c.status,statusText:c.statusText,headers:Object.fromEntries([...c.headers])},content:await h(await c.body)}}}async function*p(e){const{options:t,content:n}=await e,s=new TextEncoder;yield s.encode(JSON.stringify(t)),yield*n}async function w(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}}function g(e,t){const n=y(t);return c(e,(e=>p(n(w(e)))))}async function v(e,t){const n=f((async t=>await w(i(e,p(t)))));return await n(t)}function m(e){return new Promise(((t,n)=>{e.oncomplete=e.onsuccess=()=>t(e.result),e.onabort=e.onerror=()=>n(e.error)}))}let _;function I(){return _||(_=function(e,t){const n=indexedDB.open(e);n.onupgradeneeded=()=>n.result.createObjectStore(t);const s=m(n);return(e,n)=>s.then((s=>n(s.transaction(t,e).objectStore(t))))}("keyval-store","keyval")),_}class C{constructor(e){if(this.options=e,!this.key)throw new Error("Key is not defined.");if(!this.scope)throw new Error("Scope is not defined")}get key(){return this.options.key}get scope(){return this.options.scope||new URL("./",this.serviceWorkerUrl).pathname}get rootUrl(){return new URL(this.scope,import.meta.url)}get serviceWorkerUrl(){if(!this._serviceWorkerUrl){let e=this.options.serviceWorkerUrl?new URL(this.options.serviceWorkerUrl):new URL("./index-sw.js",this.rootUrl);this._serviceWorkerUrl=e+""}return this._serviceWorkerUrl}async _newCommunicationChannel(){const e=new MessageChannel;return[e.port1,e.port2]}async _setCommunicationPort(e){await this.options.bindPort(e)}async _updateCommunicationChannel(){if(!this._serviceWorker)return;const[e,t]=await this._newCommunicationChannel();await this._setCommunicationPort(e),await n(this._serviceWorker,"UPDATE_COMMUNICATION_PORT",this._getRegistrationInfo(),t)}_getRegistrationInfo(){return{key:this.key}}async start(){return this._registrationPromise=this._registrationPromise||(async()=>{const[e,t]=r();this._cleanupRegistrations=t,e((()=>this._cleanupRegistrations=null));const n=await navigator.serviceWorker.register(this.serviceWorkerUrl,{scope:this.scope});e((()=>n.unregister())),e(s(navigator.serviceWorker,"UPDATE_COMMUNICATION_PORT",(async(e,t,n)=>(console.log("[UPDATE_COMMUNICATION_PORT]",n),this._setCommunicationPort(n),this._getRegistrationInfo())))),this._serviceWorker=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}}))}(),await async function(e){const t=()=>"activated"===e.state;await new Promise((async n=>{t()?n():e.onstatechange=()=>{t()&&(e.onstatechange=null,n())}}))}(this._serviceWorker),await this._updateCommunicationChannel()})()}async stop(){this._cleanupRegistrations&&this._cleanupRegistrations();const e=await navigator.serviceWorker.getRegistrations();for(let t of e)try{await t.unregister()}catch(e){console.log("Service Worker registration failed: ",e)}}}class k{constructor({self:e,log:t}){this.self=e,this.log=t,this.handlersIndex=new Map}get scope(){return new URL("./",import.meta.url)+""}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=s(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,s]of e.entries()){const e=new MessageChannel,r=e.port1,a=await n(s,"UPDATE_COMMUNICATION_PORT",{},e.port2);await this._updateChannelInfo({...a,port:r,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=I()){return t("readonly",(t=>m(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=I()){return n("readwrite",(n=>(n.put(t,e),m(n.transaction))))}("clientIds",[...t.keys()].sort()),t}}class E extends C{constructor(e){super(e),this._handlers=new Map}async _setCommunicationPort(e){this._cleanupRequestChannel&&this._cleanupRequestChannel(),this._cleanupRequestChannel=g(e,this._handleHttpRequest.bind(this))}async _handleHttpRequest(e){const t=e.url;let n;for(let e of this._handlers.entries()){const[s]=e;if(0===t.indexOf(s)){n=e[1];break}}return n?n(e):new Response(null,{status:404,statusMessage:"Error 404: Not found"})}async register(e,t){e="./"+(e||"").replace(/^[\.\/]+/,"");const n=this.rootUrl,s=new URL(e,n)+"";return this._handlers.set(s,t),{baseUrl:s,prefix:e,async remove(){this._handlers.delete(s)}}}}class O extends k{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 v(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"})}})())}}function R({self:e,log:t}){const n=new O({self:e,log:t});return n.start(),()=>{n.stop()}}export{E as SwHttpAdapter,O as SwHttpDispatcher,k as SwPortDispatcher,C as SwPortHandler,n as callChannel,t as deserializeError,h as fromReadableStream,s as handleChannelCalls,g as handleHttpRequests,c as handleStreams,l as newChannel,f as newHttpClientStub,y as newHttpServerStub,u as newInvokationChannel,r as newRegistry,a as recieveData,o as sendData,v as sendHttpRequest,i as sendStream,e as serializeError,R as startHttpDispatcher,d as toReadableStream};
|
package/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './src/index.js';
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@statewalker/webrun-http-browser",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Service-worker-based HTTP server simulation for browsers",
|
|
5
|
+
"keywords": [],
|
|
6
|
+
"homepage": "https://github.com/statewalker/statewalker-webrun-http-browser",
|
|
7
|
+
"author": {
|
|
8
|
+
"name": "Mikhail Kotelnikov",
|
|
9
|
+
"email": "mikhail.kotelnikov@gmail.com"
|
|
10
|
+
},
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"type": "module",
|
|
13
|
+
"files": [
|
|
14
|
+
"dist/**/package.json",
|
|
15
|
+
"dist/**/*.js",
|
|
16
|
+
"public",
|
|
17
|
+
"public-relay",
|
|
18
|
+
"src/**/*.js",
|
|
19
|
+
"index.js"
|
|
20
|
+
],
|
|
21
|
+
"module": "src/index.js",
|
|
22
|
+
"main": "src/index.js",
|
|
23
|
+
"jsdelivr": "dist/index.js",
|
|
24
|
+
"unpkg": "dist/index.js",
|
|
25
|
+
"exports": {
|
|
26
|
+
"umd": "./dist/index-umd.min.js",
|
|
27
|
+
"default": "./src/index.js"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@statewalker/utils": "^0.4.2",
|
|
31
|
+
"idb-keyval": "^6.2.0"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@statewalker/rollup": "^0.1.6",
|
|
35
|
+
"eslint": "^8",
|
|
36
|
+
"expect.js": "^0.3",
|
|
37
|
+
"mocha": "^10",
|
|
38
|
+
"rollup": "^3"
|
|
39
|
+
},
|
|
40
|
+
"repository": {
|
|
41
|
+
"type": "git",
|
|
42
|
+
"url": "git@github.com:statewalker/statewalker-webrun-http-browser.git"
|
|
43
|
+
},
|
|
44
|
+
"scripts": {
|
|
45
|
+
"eslint": "eslint src",
|
|
46
|
+
"rollup": "rollup -c",
|
|
47
|
+
"watch": "rollup -c --watch",
|
|
48
|
+
"test": "mocha -R spec ./test/index.js",
|
|
49
|
+
"prepublishOnly": "rm -rf dist && yarn test && yarn rollup"
|
|
50
|
+
},
|
|
51
|
+
"sideEffects": false,
|
|
52
|
+
"publishConfig": {
|
|
53
|
+
"access": "public"
|
|
54
|
+
}
|
|
55
|
+
}
|
package/public/abc.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
qlsdjflqsjdfl
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
importScripts("../dist/index-sw-umd.js")
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset='utf-8'>
|
|
6
|
+
<meta http-equiv='X-UA-Compatible' content='IE=edge'>
|
|
7
|
+
<title>Page Title</title>
|
|
8
|
+
<meta name='viewport' content='width=device-width, initial-scale=1'>
|
|
9
|
+
</head>
|
|
10
|
+
|
|
11
|
+
<body>
|
|
12
|
+
<div id="info"></div>
|
|
13
|
+
<div id="messages" style="display: block; border: 1px solid blue; padding: 0.5em;"></div>
|
|
14
|
+
<div id="counter"></div>
|
|
15
|
+
|
|
16
|
+
<button id="button">Load Data</button>
|
|
17
|
+
<div id="panel"></div>
|
|
18
|
+
|
|
19
|
+
<script type="module">
|
|
20
|
+
import newHttpAdapter from "./index.js";
|
|
21
|
+
import { v4 as uuidv4 } from "https://unpkg.com/uuid@9.0.0/dist/esm-browser/index.js";
|
|
22
|
+
|
|
23
|
+
document.addEventListener("DOMContentLoaded", main);
|
|
24
|
+
// Promise.resolve().then(main).catch(console.error);
|
|
25
|
+
|
|
26
|
+
async function main() {
|
|
27
|
+
|
|
28
|
+
const key = uuidv4();
|
|
29
|
+
const prefix = `/${key}/`; // "/sw-http/"
|
|
30
|
+
const httpAdapter= newHttpAdapter({ key });
|
|
31
|
+
await httpAdapter.start();
|
|
32
|
+
|
|
33
|
+
function showMessage(message) {
|
|
34
|
+
const elm = document.body.querySelector("#messages");
|
|
35
|
+
elm.innerHTML = message;
|
|
36
|
+
}
|
|
37
|
+
showMessage("No messages yet... :(")
|
|
38
|
+
|
|
39
|
+
window.addEventListener("message", receiveMessage, false);
|
|
40
|
+
function receiveMessage(event) {
|
|
41
|
+
// const [port] = event.ports || [];
|
|
42
|
+
if (event.ports) {
|
|
43
|
+
event.ports[0]?.postMessage("Message back from the IFrame");
|
|
44
|
+
console.log('EVENT WITH PORTS:', event, event.ports);
|
|
45
|
+
}
|
|
46
|
+
showMessage(`Message from "${event.origin}". Data:"${JSON.stringify(event.data)}" !`)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const registration = await httpAdapter.register(prefix, async (request) => {
|
|
50
|
+
const requestUrl = new URL(request.url);
|
|
51
|
+
const elm = document.querySelector("#counter");
|
|
52
|
+
elm.innerHTML = `URL: ${requestUrl}<br />Last Request Time: ${Date.now()}`;
|
|
53
|
+
|
|
54
|
+
const { pathname } = requestUrl;
|
|
55
|
+
if (request.method === 'GET' && pathname.match(/\.js$/)) {
|
|
56
|
+
const code = `export default function sayHello() { return 'Hello, there ${Date.now()}!'; }`
|
|
57
|
+
return new Response(code, {
|
|
58
|
+
headers: {
|
|
59
|
+
"Content-Type": "text/javascript",
|
|
60
|
+
"X-Foo-Bar": "baz"
|
|
61
|
+
}
|
|
62
|
+
})
|
|
63
|
+
} else if (request.method === 'POST') {
|
|
64
|
+
const body = await request.text();
|
|
65
|
+
console.log('[client] Body:', body);
|
|
66
|
+
const obj = {
|
|
67
|
+
url: request.url + '',
|
|
68
|
+
method: request.method,
|
|
69
|
+
message: body.toUpperCase(),
|
|
70
|
+
headers: Object.fromEntries([...request.headers])
|
|
71
|
+
}
|
|
72
|
+
return new Response(JSON.stringify(obj, null, 2), {
|
|
73
|
+
headers: {
|
|
74
|
+
"Content-Type": "application/json",
|
|
75
|
+
"X-Foo-Bar": "baz"
|
|
76
|
+
}
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return new Response("Hello, world " + Date.now(), {
|
|
81
|
+
headers: {
|
|
82
|
+
"Content-Type": "text/plain",
|
|
83
|
+
"X-Foo-Bar": "baz"
|
|
84
|
+
}
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
const { baseUrl } = registration;
|
|
90
|
+
|
|
91
|
+
const btn = document.querySelector("#button");
|
|
92
|
+
const panel = document.querySelector("#panel");
|
|
93
|
+
const info = document.querySelector("#info").innerText = baseUrl;
|
|
94
|
+
|
|
95
|
+
btn.addEventListener("click", async (ev) => {
|
|
96
|
+
ev.preventDefault();
|
|
97
|
+
ev.stopPropagation();
|
|
98
|
+
const fetchUrl = new URL("./data-calls.js", baseUrl) + '';
|
|
99
|
+
console.log('Fetch URL:', fetchUrl);
|
|
100
|
+
const res = await fetch(fetchUrl, {
|
|
101
|
+
// method: 'POST',
|
|
102
|
+
// body: "Hello, there!",
|
|
103
|
+
// headers: {
|
|
104
|
+
// "Content-Type": "text/plain"
|
|
105
|
+
// }
|
|
106
|
+
});
|
|
107
|
+
const content = await res.text();
|
|
108
|
+
panel.innerText = content;
|
|
109
|
+
})
|
|
110
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
111
|
+
|
|
112
|
+
// (async function () {
|
|
113
|
+
// try {
|
|
114
|
+
// const scriptUrl = new URL("./xyz/sayHello.js", baseUrl) + '';
|
|
115
|
+
// console.log('>TF', scriptUrl);
|
|
116
|
+
// await new Promise(r => setTimeout(r, 2000));
|
|
117
|
+
// const { default: sayHello } = await import(scriptUrl);
|
|
118
|
+
// console.log(sayHello);
|
|
119
|
+
// console.log(sayHello())
|
|
120
|
+
// } catch (error) {
|
|
121
|
+
// console.error(error);
|
|
122
|
+
// }
|
|
123
|
+
// })();
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
</script>
|
|
129
|
+
|
|
130
|
+
</body>
|
|
131
|
+
|
|
132
|
+
</html>
|
package/public/index.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import * as workerTimer from "https://unpkg.com/worker-timers@7.0.74/build/es2019/module.js?module";
|
|
2
|
+
|
|
3
|
+
export function startHeartbit({
|
|
4
|
+
timeout = 10 * 1000,
|
|
5
|
+
url = "./ping", // 10 seconds
|
|
6
|
+
}) {
|
|
7
|
+
return callPeriodically(async () => {
|
|
8
|
+
const res = await fetch(url);
|
|
9
|
+
}, timeout);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async function callPeriodically(action, timeout) {
|
|
13
|
+
let timerId, stopped = false;
|
|
14
|
+
async function run() {
|
|
15
|
+
try {
|
|
16
|
+
await action();
|
|
17
|
+
} catch (err) {
|
|
18
|
+
console.error(err);
|
|
19
|
+
}
|
|
20
|
+
timerId = (!stopped) ? workerTimer.setTimeout(run, timeout) : 0;
|
|
21
|
+
}
|
|
22
|
+
run();
|
|
23
|
+
return () => {
|
|
24
|
+
stopped = true;
|
|
25
|
+
if (timerId) {
|
|
26
|
+
workerTimer.clearTimeout(timerId);
|
|
27
|
+
timerId = 0;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import {
|
|
2
|
+
callChannel,
|
|
3
|
+
handleChannelCalls,
|
|
4
|
+
handleHttpRequests,
|
|
5
|
+
newRegistry,
|
|
6
|
+
sendHttpRequest,
|
|
7
|
+
} from "../dist/index.js";
|
|
8
|
+
|
|
9
|
+
export function newServiceWorkerPort() {
|
|
10
|
+
// Bind the service worker to
|
|
11
|
+
const channel = new MessageChannel();
|
|
12
|
+
channel.port1.onmessage = (event) => {
|
|
13
|
+
navigator.serviceWorker.controller.postMessage(event.data, event.ports);
|
|
14
|
+
};
|
|
15
|
+
navigator.serviceWorker.addEventListener("message", (event) => {
|
|
16
|
+
channel.port1.postMessage(event.data, event.ports);
|
|
17
|
+
});
|
|
18
|
+
return channel.port2;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function initServiceWorker(swUrl) {
|
|
22
|
+
const {
|
|
23
|
+
installing,
|
|
24
|
+
waiting,
|
|
25
|
+
active,
|
|
26
|
+
} = await navigator.serviceWorker.register(swUrl, {
|
|
27
|
+
type: "module",
|
|
28
|
+
});
|
|
29
|
+
(installing || waiting || active).addEventListener("statechange", (e) => {
|
|
30
|
+
console.log("state", e.target.state);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const serviceWorker = navigator.serviceWorker;
|
|
34
|
+
await new Promise((resolve) => {
|
|
35
|
+
// Resolve right away if this page is already controlled.
|
|
36
|
+
if (serviceWorker.controller) {
|
|
37
|
+
resolve();
|
|
38
|
+
} else {
|
|
39
|
+
const onChange = () => {
|
|
40
|
+
resolve();
|
|
41
|
+
serviceWorker.removeEventListener("controllerchange", onChange);
|
|
42
|
+
serviceWorker.oncontrollerchange = null;
|
|
43
|
+
};
|
|
44
|
+
serviceWorker.addEventListener("controllerchange", onChange);
|
|
45
|
+
serviceWorker.oncontrollerchange = onChange;
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
const worker = serviceWorker.controller;
|
|
49
|
+
const isActivated = () => worker.state === "activated";
|
|
50
|
+
return await new Promise(async (resolve) => {
|
|
51
|
+
if (isActivated()) resolve(worker);
|
|
52
|
+
else {
|
|
53
|
+
worker.onstatechange = () => {
|
|
54
|
+
if (!isActivated()) return;
|
|
55
|
+
worker.onstatechange = null;
|
|
56
|
+
resolve(worker);
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function initHttpService(handler, {
|
|
63
|
+
key,
|
|
64
|
+
port,
|
|
65
|
+
}) {
|
|
66
|
+
return await registerConnectionsHandler({
|
|
67
|
+
key,
|
|
68
|
+
communicationPort: port,
|
|
69
|
+
handler: async (event, data, callPort) => {
|
|
70
|
+
handleHttpRequests(callPort, handler);
|
|
71
|
+
return true;
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function callHttpService(request, {
|
|
77
|
+
key,
|
|
78
|
+
port,
|
|
79
|
+
}) {
|
|
80
|
+
const callPort = await initializeConnection({
|
|
81
|
+
key,
|
|
82
|
+
communicationPort: port,
|
|
83
|
+
});
|
|
84
|
+
const result = await sendHttpRequest(callPort, request);
|
|
85
|
+
return result;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export async function newRemoteRelayChannel({
|
|
89
|
+
baseUrl = new URL("./", import.meta.url),
|
|
90
|
+
url = new URL("./relay.html", baseUrl),
|
|
91
|
+
container = document.body,
|
|
92
|
+
} = {}) {
|
|
93
|
+
const messageChannel = new MessageChannel();
|
|
94
|
+
const { iframe, promise } = await newIFrame(url);
|
|
95
|
+
Object.assign(iframe.style, {
|
|
96
|
+
// height: "100px"
|
|
97
|
+
position: "fixed",
|
|
98
|
+
width: "1px",
|
|
99
|
+
height: "1px",
|
|
100
|
+
top: "-1000",
|
|
101
|
+
left: "-1000",
|
|
102
|
+
display: "block",
|
|
103
|
+
opacity: "0",
|
|
104
|
+
border: "none",
|
|
105
|
+
outline: "none",
|
|
106
|
+
});
|
|
107
|
+
container.appendChild(iframe);
|
|
108
|
+
promise.then(() => {
|
|
109
|
+
iframe.contentWindow.postMessage(
|
|
110
|
+
{
|
|
111
|
+
type: "CONNECT",
|
|
112
|
+
},
|
|
113
|
+
"*",
|
|
114
|
+
[messageChannel.port1],
|
|
115
|
+
);
|
|
116
|
+
});
|
|
117
|
+
return {
|
|
118
|
+
port: messageChannel.port2,
|
|
119
|
+
close: () => {
|
|
120
|
+
container.parentElement && container.parentElement.removeChild(container);
|
|
121
|
+
messageChannel.port1.close();
|
|
122
|
+
messageChannel.port2.close();
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
async function newIFrame(url) {
|
|
127
|
+
const iframe = document.createElement("iframe");
|
|
128
|
+
iframe.src = url;
|
|
129
|
+
Object.assign(iframe.style, {
|
|
130
|
+
padding: 0,
|
|
131
|
+
margin: 0,
|
|
132
|
+
border: "none",
|
|
133
|
+
outline: "none",
|
|
134
|
+
width: "100%",
|
|
135
|
+
height: "100%",
|
|
136
|
+
});
|
|
137
|
+
return {
|
|
138
|
+
iframe,
|
|
139
|
+
promise: new Promise((resolve, reject) => {
|
|
140
|
+
try {
|
|
141
|
+
iframe.onerror = reject;
|
|
142
|
+
iframe.onload = () => resolve(iframe);
|
|
143
|
+
} catch (error) {
|
|
144
|
+
reject(error);
|
|
145
|
+
}
|
|
146
|
+
}),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
// -------------------------------------------------------
|
|
151
|
+
|
|
152
|
+
export async function initializeConnection({
|
|
153
|
+
key,
|
|
154
|
+
communicationPort,
|
|
155
|
+
...options
|
|
156
|
+
}) {
|
|
157
|
+
const channel = new MessageChannel();
|
|
158
|
+
const accepted = await callChannel(
|
|
159
|
+
communicationPort,
|
|
160
|
+
"CONNECT",
|
|
161
|
+
{ key, ...options },
|
|
162
|
+
channel.port2,
|
|
163
|
+
);
|
|
164
|
+
if (!accepted) {
|
|
165
|
+
channel.port1.close();
|
|
166
|
+
channel.port2.close();
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
return channel.port1;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export async function registerConnectionsHandler({
|
|
173
|
+
key,
|
|
174
|
+
handler,
|
|
175
|
+
communicationPort,
|
|
176
|
+
}) {
|
|
177
|
+
const [register, cleanup] = newRegistry();
|
|
178
|
+
await callChannel(communicationPort, "REGISTER", { key });
|
|
179
|
+
register(() => callChannel(communicationPort, "UNREGISTER", { key }));
|
|
180
|
+
register(
|
|
181
|
+
handleChannelCalls(
|
|
182
|
+
communicationPort,
|
|
183
|
+
"CONNECT",
|
|
184
|
+
handler,
|
|
185
|
+
),
|
|
186
|
+
);
|
|
187
|
+
return cleanup;
|
|
188
|
+
}
|