@statewalker/webrun-http-browser 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.min.js CHANGED
@@ -1,2 +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};
1
+ // @statewalker/webrun-http-browser v0.2.1 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,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=>{t=await t;const n=["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}),{}),r=t.body?y(t.body):[],a=await e({options:n,content:r});let o=null,s={};if(a){s=a.options;const e=a.content;"HEAD"!==n.method&&"OPTIONS"!==n.method?o=d(e):e&&e.return&&e.return()}else s={status:404,statusText:"Error 404: Not Found"};return new Response(o,s)}}function w(e){return async t=>{const{options:n,content:r}=await t;let a;const{url:o,mode:s}=n;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 c=new Request(o,{...n,body:a}),i=await e(c);return{options:{status:i.status,statusText:i.statusText,headers:Object.fromEntries([...i.headers])},content:await y(await i.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 x({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])})),{port:r.port2,close:()=>{n.parentElement&&n.parentElement.removeChild(n),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 x({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,x 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.1.0",
3
+ "version": "0.2.1",
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",
package/public/index.html CHANGED
@@ -17,8 +17,9 @@
17
17
  <div id="panel"></div>
18
18
 
19
19
  <script type="module">
20
- import newHttpAdapter from "./index.js";
21
20
  import { v4 as uuidv4 } from "https://unpkg.com/uuid@9.0.0/dist/esm-browser/index.js";
21
+ import * as relayLib from "../dist/index.js";
22
+ import { callPeriodically } from "../public-relay/heartbeat.js";
22
23
 
23
24
  document.addEventListener("DOMContentLoaded", main);
24
25
  // Promise.resolve().then(main).catch(console.error);
@@ -26,102 +27,69 @@
26
27
  async function main() {
27
28
 
28
29
  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
- }
30
+ const [register, cleanup] = relayLib.newRegistry();
31
+
32
+ const options = {
33
+ // url: new URL(`../public-relay/relay.html?${Math.random()}`, import.meta.url),
34
+ // container: document.body,
35
+ };
36
+ const serviceKey = "abc";
37
+
38
+ // ---------------------------
39
+ // Server-side part
40
+ const serverPort = await relayLib.newRemoteRelayChannel(options);
41
+ register(serverPort.close);
79
42
 
80
- return new Response("Hello, world " + Date.now(), {
43
+ const serviceHandler = async (request) => {
44
+ const info = relayLib.splitServiceUrl(request.url);
45
+
46
+ const result = {
47
+ message: "Hello Wonderful World!",
48
+ ...info
49
+ }
50
+ return new Response(JSON.stringify(result, null, 2), {
81
51
  headers: {
82
- "Content-Type": "text/plain",
83
- "X-Foo-Bar": "baz"
52
+ "Content-Type": "application/json"
84
53
  }
85
54
  })
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
-
55
+ };
56
+ register(await relayLib.initHttpService(serviceHandler, {
57
+ key: serviceKey,
58
+ port: serverPort.port
59
+ }))
60
+
61
+ // ---------------------------
62
+ // Call the defined endpoint.
63
+
64
+ // We can not use the "fech" method - to be able
65
+ // to use it this page should be served from the same root URL
66
+ // as the service worker managing pages.
67
+ // To deal with this constraint we use the connection
68
+ // port allowing to perform direct calls using
69
+ // the explicit service key on the client.
70
+ const clientPort = await relayLib.newRemoteRelayChannel(options);
71
+ register(clientPort.close);
72
+
73
+ const request = new Request(`http://www.foo.bar/titi/toto`);
74
+ const response = await relayLib.callHttpService(request, {
75
+ key: serviceKey,
76
+ port: clientPort.port
77
+ });
78
+ const json = await response.json();
79
+ console.log('* Response:', json);
80
+
81
+
82
+ // // ---------------------------
83
+ // // Start hearbeting calls - to avoid removal of the underlying ServiceWorker
84
+ // // const url = new URL(`../public-relay/~.ping`, import.meta.url);
85
+ // register(callPeriodically(async () => {
86
+ // const response = await relayLib.callHttpService(new Request("~sys/ping/"), {
87
+ // key: serviceKey,
88
+ // port: clientPort.port
89
+ // })
90
+ // const result = await response.json();
91
+ // console.log(result);
92
+ // }, 10000))
125
93
 
126
94
  }
127
95
 
@@ -9,7 +9,7 @@ export function startHeartbit({
9
9
  }, timeout);
10
10
  }
11
11
 
12
- async function callPeriodically(action, timeout) {
12
+ export async function callPeriodically(action, timeout) {
13
13
  let timerId, stopped = false;
14
14
  async function run() {
15
15
  try {
@@ -1,343 +1,6 @@
1
- import {
2
- // serializeError, deserializeError,
3
- callChannel,
4
- handleChannelCalls,
5
- newRegistry,
6
- sendHttpRequest,
7
- } from "../dist/index.js";
8
- import { registerConnectionsHandler } from "./relay-lib.js";
9
-
10
- if (typeof self.skipWaiting === "function") {
11
- console.log("self.skipWaiting() is supported.");
12
- self.addEventListener("install", (e) => {
13
- // See https://slightlyoff.github.io/ServiceWorker/spec/service_worker/index.html#service-worker-global-scope-skipwaiting
14
- e.waitUntil(self.skipWaiting());
15
- });
16
- } else {
17
- console.log("self.skipWaiting() is not supported.");
18
- }
19
-
20
- if (self.clients && (typeof self.clients.claim === "function")) {
21
- console.log("self.clients.claim() is supported.");
22
- self.addEventListener("activate", (e) => {
23
- // See https://slightlyoff.github.io/ServiceWorker/spec/service_worker/index.html#clients-claim-method
24
- e.waitUntil(self.clients.claim());
25
- });
26
- } else {
27
- console.log("self.clients.claim() is not supported.");
28
- }
29
-
30
- const baseUrl = new URL("./", import.meta.url) + "";
31
- const clientsRegistry = newClientsRegistry(self);
32
-
33
- // This listener handles the following message types:
34
- // * registration: registers new clients as service providers
35
- // * handles service calls rounting:
36
- // - it gets the clientKey defined in the call
37
- // - it uses the serviceKey to get the service-specifc channel
38
- // - it creates a new message channel and sent its ports to peers
39
-
40
- const [register, clear] = newRegistry();
41
-
42
- register(handleChannelCalls(self, "REGISTER", async (event, data) => {
43
- return await clientsRegistry.addClient(data.key, event.source);
44
- }));
45
- register(handleChannelCalls(self, "UNREGISTER", async (event, data) => {
46
- return await clientsRegistry.removeClient(data.key);
47
- }));
48
-
49
- // Transfers all parameters (and ports) to the target client.
50
- register(handleChannelCalls(self, "CONNECT", async (event, data, port) => {
51
- const { key } = data;
52
- const client = await clientsRegistry.getClient(key);
53
- if (!client) {
54
- throw new Error(`Target client was not found. Target key: "${key}".`);
55
- }
56
- return await callChannel(client, "CONNECT", data, port);
57
- }));
58
-
59
- function splitServiceUrl(url, separator = "~") {
60
- let key = "";
61
- let path = "";
62
- let str = url + "";
63
- const idx = str.indexOf(separator);
64
- let baseUrl = "";
65
- if (idx >= 0) {
66
- baseUrl = str.substring(0, idx + separator.length);
67
- str.substring(idx + separator.length)
68
- .replace(/^([^\/]+)/, (match, $1) => {
69
- baseUrl += match;
70
- if (baseUrl.length < str.length) { baseUrl += "/"; }
71
- key = $1;
72
- path = str.substring(baseUrl.length);
73
- return "";
74
- });
75
- }
76
- return {
77
- url,
78
- key,
79
- baseUrl,
80
- path
81
- };
82
- }
83
- self.addEventListener("fetch", (event) => {
84
- const request = event.request;
85
- const params = splitServiceUrl(request.url);
86
- const { key } = params;
87
- if (key) {
88
- event.respondWith((async () => {
89
- try {
90
- const channel = new MessageChannel();
91
- const client = await clientsRegistry.getClient(key);
92
- if (!client) {
93
- throw HttpError.errorResourceGone(params);
94
- }
95
- const data = {
96
- type: "http",
97
- key,
98
- };
99
- if (!await callChannel(client, "CONNECT", data, channel.port2)) {
100
- throw HttpError.errorForbidden(params);
101
- }
102
- return await sendHttpRequest(channel.port1, request);
103
- } catch (error) {
104
- error = HttpError.fromError(error);
105
- const options = error.getResponseOptions(params);
106
- return new Response(JSON.stringify(options), {
107
- ...options,
108
- headers: {
109
- "Content-Type": "text/javascript",
110
- },
111
- });
112
- }
113
- })());
114
- }
115
- });
116
- // self.onmessage = (event) => {
117
- // console.log("SW MESSAGE:", event, event.ports);
118
- // const [port] = event.ports || [];
119
- // if (port) {
120
- // port.onmessage = (ev) => {
121
- // // const data = ev.data || {};
122
- // console.log("Reply from the other side of the moon", ev);
123
- // // port.postMessage({
124
- // // message: "Reply from the other side",
125
- // // ...data
126
- // // })
127
- // };
128
- // port.postMessage({ message: "Message from the Service Worker HELL!" });
129
- // }
130
- // };
131
-
132
- class HttpError extends Error {
133
- constructor(options) {
134
- super(options.message);
135
- this.status = options.status;
136
- this.statusText = options.statusText;
137
- }
138
- getResponseOptions(options = {}) {
139
- return Object.assign(this.toJson(), options);
140
- }
141
- toJson() {
142
- return {
143
- status: this.status,
144
- statusText: this.statusText,
145
- message: this.message,
146
- };
147
- }
148
-
149
- static fromError(error) {
150
- if (!(error instanceof HttpError)) {
151
- error = new HttpError(Object.assign({
152
- status: 500,
153
- statusText: "Bad Request",
154
- reason: error.message,
155
- }));
156
- }
157
- return error;
158
- }
159
-
160
- static errorResourceNotFound(options = {}) {
161
- return new HttpError({
162
- status: 404,
163
- statusText: "Error 404: Resource not found",
164
- ...options,
165
- });
166
- }
167
- static errorForbidden(options = {}) {
168
- return new HttpError({
169
- status: 403,
170
- statusText: "Error 403: Forbidden",
171
- ...options,
172
- });
173
- }
174
-
175
- static errorResourceGone(options = {}) {
176
- return new HttpError({
177
- status: 410,
178
- statusText: "Error 410: Resource Gone",
179
- ...options,
180
- });
181
- }
182
-
183
- static errorInternalError(options) {
184
- return new HttpError({
185
- ...options,
186
- status: 500,
187
- statusText: "Error 500: Internal error",
188
- });
189
- }
190
- }
191
-
192
- // self.addEventListener("fetch", (event) => {
193
- // console.log("fetching resource", event);
194
- // const request = event.request;
195
- // const requestUrl = request.url;
196
- // if (requestUrl.match(/\/ping$/)) {
197
- // event.respondWith((async () => {
198
- // return new Response("pong", { status: 200 });
199
- // })());
200
- // } else if (requestUrl.indexOf(baseUrl) === 0) {
201
- // const clientKey = requestUrl
202
- // .substring(baseUrl.length)
203
- // .replace(/^\/?([^\/]+).*$/, "$1");
204
- // if (clientKey) {
205
- // event.respondWith((async () => {
206
- // try {
207
- // const client = await clientsRegistry.getClient(clientKey);
208
- // if (!client) { throw HttpError.errorResourceGone(); }
209
- // const port = portsRegistry.getClientPort(client, "http");
210
-
211
- // return new Response("pong", { status: 200 });
212
- // } catch (error) {
213
- // if (!(error instanceof HttpError)) {
214
- // error = HttpError.errorInternalError(error);
215
- // }
216
- // console.error(error);
217
- // return new Response(null, error.getResponseOptions());
218
- // }
219
- // })());
220
- // }
221
- // }
222
- // // const response = new Response(
223
- // // `<p>This is a response that comes from your service worker! ${Date.now()}</p>`,
224
- // // {
225
- // // headers: { "Content-Type": "text/html" },
226
- // // },
227
- // // );
228
- // // event.respondWith(response);
229
- // });
230
-
231
- // --------------------------------------------
232
- // errors.js
233
-
234
- // --------------------------------------------
235
-
236
- function newClientsRegistry(self) {
237
- const KEY = "clientsIds";
238
-
239
- let _index;
240
- async function loadClientsIndex() {
241
- if (!_index) {
242
- const entries = (await get(KEY)) || [];
243
- _index = Object.fromEntries(entries);
244
- }
245
- return _index;
246
- }
247
- async function storeClientsIndex() {
248
- const index = await loadClientsIndex();
249
- const entries = Object.entries(index);
250
- await set(KEY, entries);
251
- return index;
252
- }
253
-
254
- async function addClient(clientKey, client) {
255
- const index = await loadClientsIndex();
256
- const clientId = client.id;
257
- let result = false;
258
- if (!(clientId in index)) {
259
- index[clientKey] = clientId;
260
- await storeClientsIndex();
261
- result = true;
262
- }
263
- return result;
264
- }
265
-
266
- async function removeClient(clientKey) {
267
- const index = await loadClientsIndex();
268
- let result = false;
269
- if (clientKey in index) {
270
- delete index[clientKey];
271
- await storeClientsIndex();
272
- result = true;
273
- }
274
- return result;
275
- }
276
-
277
- async function getClient(clientKey) {
278
- const index = await loadClientsIndex();
279
- const clientId = index[clientKey];
280
- const client = await self.clients.get(clientId);
281
- if (!client) {
282
- delete index[clientKey];
283
- await storeClientsIndex();
284
- }
285
- return client;
286
- }
287
-
288
- return {
289
- getClient,
290
- addClient,
291
- removeClient,
292
- };
293
- }
294
- // ----------------------------------------------------
295
- // Content of the "idb-keyval" package
296
- // import { get, set } from "idb-keyval";
297
-
298
- function promisifyRequest(request) {
299
- return new Promise((resolve, reject) => {
300
- // @ts-ignore - file size hacks
301
- request.oncomplete = request.onsuccess = () => resolve(request.result);
302
- // @ts-ignore - file size hacks
303
- request.onabort = request.onerror = () => reject(request.error);
304
- });
305
- }
306
- function createStore(dbName, storeName) {
307
- const request = indexedDB.open(dbName);
308
- request.onupgradeneeded = () => request.result.createObjectStore(storeName);
309
- const dbp = promisifyRequest(request);
310
- return (txMode, callback) =>
311
- dbp.then((db) =>
312
- callback(db.transaction(storeName, txMode).objectStore(storeName))
313
- );
314
- }
315
- let defaultGetStoreFunc;
316
- function defaultGetStore() {
317
- if (!defaultGetStoreFunc) {
318
- defaultGetStoreFunc = createStore("keyval-store", "keyval");
319
- }
320
- return defaultGetStoreFunc;
321
- }
322
- /**
323
- * Get a value by its key.
324
- *
325
- * @param key
326
- * @param customStore Method to get a custom store. Use with caution (see the docs).
327
- */
328
- function get(key, customStore = defaultGetStore()) {
329
- return customStore("readonly", (store) => promisifyRequest(store.get(key)));
330
- }
331
- /**
332
- * Set a value with a key.
333
- *
334
- * @param key
335
- * @param value
336
- * @param customStore Method to get a custom store. Use with caution (see the docs).
337
- */
338
- function set(key, value, customStore = defaultGetStore()) {
339
- return customStore("readwrite", (store) => {
340
- store.put(value, key);
341
- return promisifyRequest(store.transaction);
342
- });
343
- }
1
+ // It is important to include the Service Worker in this file:
2
+ // the service worker controls all resources starting from the
3
+ // base URL of the running script.
4
+ // By including the script directly (without this file) you change
5
+ // the base URL of the controlled resources, and it is not what you want!
6
+ importScripts("../dist/index-sw.js");
@@ -1,33 +1,12 @@
1
1
  <!DOCTYPE html>
2
2
  <html>
3
-
4
3
  <body>
5
4
  <script type="module">
6
- import { newServiceWorkerPort, initServiceWorker } from "./relay-lib.js";
7
-
8
- const swUrl = new URL("./relay-sw.js", import.meta.url) + "";
9
-
10
- let externalPort;
11
- window.onmessage = async (ev) => {
12
- if (ev.data.type === "CONNECT") {
13
- const newExternalPort = (ev.ports || [])[0];
14
- if (externalPort) {
15
- newExternalPort.close();
16
- return;
17
- }
18
- externalPort = newExternalPort;
19
- await initServiceWorker(swUrl);
20
- const serviceWorkerPort = newServiceWorkerPort();
21
- serviceWorkerPort.onmessage = (event) => {
22
- externalPort.postMessage(event.data, event.ports);
23
- };
24
-
25
- externalPort.onmessage = (event) => {
26
- serviceWorkerPort.postMessage(event.data, event.ports);
27
- };
28
- }
29
- };
5
+ import { getRelayWindowMessageHandler } from "../dist/index.js";
6
+ window.onmessage = getRelayWindowMessageHandler({
7
+ swUrl : new URL("./relay-sw.js", import.meta.url) + "",
8
+ scopeUrl : new URL("./", import.meta.url) + "",
9
+ });
30
10
  </script>
31
11
  </body>
32
-
33
12
  </html>