From 495b4f8c5b356c3ef277e988cfa5f50fdeaa0384 Mon Sep 17 00:00:00 2001 From: Jakub Jankiewicz Date: Sat, 5 Oct 2024 19:29:03 +0200 Subject: [PATCH] add public method `respond` --- CHANGELOG.md | 4 ++++ Makefile | 2 +- README.md | 57 ++++++++++++++++++++++++++++++++++++++++++++++- index.js | 5 ++++- index.min.js | 6 ++--- index.umd.js | 9 +++++--- index.umd.min.js | 6 ++--- package-lock.json | 4 ++-- package.json | 2 +- 9 files changed, 80 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 55e3652..1676d62 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.18.0 +### Features +* add public method `respond` where you can use your own `Response` object + ## 0.17.0 ### Features * allow using glob as domain name diff --git a/Makefile b/Makefile index 0005e57..dac7b24 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -VERSION=0.17.0 +VERSION=0.18.0 DATE=`date -uR` YEAR=`date +%Y` diff --git a/README.md b/README.md index 98f42e5..ca5a43c 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ alt="Logo of Wayne library - it represents construction worker helmet and text with the name of the library" /> -[![npm](https://img.shields.io/badge/npm-0.17.0-blue.svg)](https://www.npmjs.com/package/@jcubic/wayne) +[![npm](https://img.shields.io/badge/npm-0.18.0-blue.svg)](https://www.npmjs.com/package/@jcubic/wayne) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://makeapullrequest.com) [![jSDelivr](https://data.jsdelivr.com/v1/package/npm/@jcubic/wayne/badge)](https://www.jsdelivr.com/package/npm/@jcubic/wayne) @@ -528,6 +528,59 @@ import require$$0 from"/npm/jquery@3.7.1/+esm" And needs to be imported from jsDelivr, the same if you import CSS file. See [example of loading jQuery Terminal](https://jcubic.github.io/wayne/esm/) where this code is used. +### PWA + +To use with PWA and cache you need to use a custom middleware: + +```javascript +const app = new Wayne(); + +app.use(async (req, res, next) => { + const url = new URL(req.url); + + const cache = await get_cache(); + const cache_response = await cache.match(req); + + if (cache_response) { + if (navigator.onLine) { + const net_response = await fetch(req); + cache.put(req, net_response.clone()); + res.respond(net_response); + } else { + res.respond(cache_response); + } + } else { + next(); + } +}); + +const cache_url = [ + '/', + 'https://cdn.jsdelivr.net/npm/@jcubic/wayne/index.umd.min.js', + 'https://cdn.jsdelivr.net/npm/idb-keyval@6/dist/umd.js' +]; + +self.addEventListener('install', (event) => { + event.waitUntil(cache_all()); +}); + +function get_cache() { + return caches.open('pwa-assets'); +} + +async function cache_all() { + const cache = await get_cache(); + return cache.addAll(cache_url); +} +``` + +This approach is recommended by the answer to this StackOverflow question: + +* [Service-worker force update of new assets](https://stackoverflow.com/a/33266296/387194) + +It always fetch a new value of the assets when there is internet connection and serve cached value +when user is offline. When there are no cached value it do default action (which can be normal +fetch outside of cache or Wayne route). ## First load @@ -554,6 +607,7 @@ by [Jake Archibald](https://twitter.com/jaffathecake). * [Download demo](https://jcubic.github.io/wayne/download/). * [Source Code Syntax highlight demo](https://jcubic.github.io/wayne/code/). * [Using with React and Vite](https://jcubic.github.io/react-wayne-auth/) +* [PWA/Cache](https://jcubic.github.io/wayne/pwa/) The source code for the demos can be found [in the docs' directory at the gh-pages branch](https://github.com/jcubic/wayne/tree/gh-pages/docs). @@ -604,6 +658,7 @@ each of those methods accepts string as the first argument. The second argument Additional methods: * `redirect()` - accept URL or optional first argument that is the number of HTTP code +* `respond(res)` - accept Response object in case you want to use a different [Response object](https://developer.mozilla.org/en-US/docs/Web/API/Response). * `sse([options])` - function creates Server-Sent Event stream, the return object has a method `send` that sends a new event. * `fetch(url | Request)` - method will send a normal HTTP request to the server and return the result to the client. You can use the default Request object from the route. * `download(data, { filename })` - a method that can be used to trigger file download. The data can be a `string` or `arrayBuffer` you can use native fetch API and call `await res.text()` or `await res.arrayBuffer()` and pass the result as data. diff --git a/index.js b/index.js index 1ccedef..8497000 100644 --- a/index.js +++ b/index.js @@ -1,5 +1,5 @@ /* - * Wayne - Server Worker Routing library (v. 0.17.0) + * Wayne - Server Worker Routing library (v. 0.18.0) * * Copyright (c) 2022-2024 Jakub T. Jankiewicz * Released under MIT license @@ -101,6 +101,9 @@ export class HTTPResponse { json(data, init) { this.send(JSON.stringify(data), { type: 'application/json', ...init }); } + respond(response) { + this._resolve(response); + } blob(blob, init = {}) { this._resolve(new Response(blob, init)); } diff --git a/index.min.js b/index.min.js index 56393bf..4ecf5b7 100644 --- a/index.min.js +++ b/index.min.js @@ -1,9 +1,9 @@ /* - * Wayne - Server Worker Routing library (v. 0.17.0) + * Wayne - Server Worker Routing library (v. 0.18.0) * * Copyright (c) 2022-2024 Jakub T. Jankiewicz * Released under MIT license * - * Fri, 26 Jul 2024 23:46:08 +0000 + * Sat, 05 Oct 2024 17:28:37 +0000 */ -const root_url=get_root_path();const root_url_re=new RegExp("^"+escape_re(root_url));function same_origin(origin){return origin===self.location.origin}function get_root_path(){if(self.registration){const url=new URL(registration.scope);return url.pathname.replace(/\/$/,"")}return location.pathname.replace(/\/[^\/]+$/,"")}function normalize_url(url){return url.replace(root_url_re,"")}function escape_re(str){if(typeof str=="string"){var special=/([\^\$\[\]\(\)\{\}\+\*\.\|\?])/g;return str.replace(special,"\\$1")}}function is_function(arg){return typeof arg==="function"}function is_promise(arg){return arg&&typeof arg==="object"&&is_function(arg.then)}function isPromiseFs(fs){const test=targetFs=>{try{return targetFs.readFile().catch(e=>e)}catch(e){return e}};return is_promise(test(fs))}const commands=["stat","readdir","readFile"];function bind_fs(fs){const result={};if(isPromiseFs(fs)){for(const command of commands){result[command]=fs[command].bind(fs)}}else{for(const command of commands){result[command]=function(...args){return new Promise((resolve,reject)=>{fs[command](...args,function(err,data){if(err){reject(err)}else{resolve(data)}})})}}}return result}export class HTTPResponse{constructor(resolve,reject){this._resolve=resolve;this._reject=reject}html(data,init){this.send(data,{type:"text/html",...init})}text(data,init){this.send(data,init)}json(data,init){this.send(JSON.stringify(data),{type:"application/json",...init})}blob(blob,init={}){this._resolve(new Response(blob,init))}send(data,{type="text/plain",...init}={}){if(![undefined,null].includes(data)){data=new Blob([data],{type:type})}this.blob(data,init)}async fetch(arg){if(typeof arg==="string"){const _res=await fetch(arg);const type=_res.headers.get("Content-Type")??"application/octet-stream";this.send(await _res.arrayBuffer(),{type:type})}else if(arg instanceof Request){return fetch(arg).then(this._resolve).catch(this._reject)}}download(content,{filename="download",type="text/plain",...init}={}){const headers={"Content-Disposition":`attachment; filename="${filename}"`};this.send(content,{type:type,headers:headers,...init})}redirect(code,url){if(url===undefined){url=code;code=302}if(!url.match(/https?:\/\//)){url=root_url+url}this._resolve(Response.redirect(url,code))}sse({onClose}={}){let send,close,stream,defunct;stream=new ReadableStream({cancel(){defunct=true;trigger(onClose)},start:controller=>{send=function(event){if(!defunct){const chunk=createChunk(event);const payload=(new TextEncoder).encode(chunk);controller.enqueue(payload)}};close=function close(){controller.close();stream=null;trigger(onClose)}}});this._resolve(new Response(stream,{headers:{"Content-Type":"text/event-stream; charset=utf-8","Transfer-Encoding":"chunked",Connection:"keep-alive"}}));return{send:send,close:close}}}export function RouteParser(){const name_re="[a-zA-Z_][a-zA-Z_0-9]*";const self=this;const open_tag="{";const close_tag="}";const glob="*";const glob_re="(.*?)";const number="\\d";const optional="?";const open_group="(";const close_group=")";const plus="+";const dot=".";self.route_parser=function(open,close){const routes={};const tag_re=new RegExp("("+escape_re(open)+name_re+escape_re(close)+")","g");const tokenizer_re=new RegExp(["(",escape_re(open),name_re,escape_re(close),"|",escape_re(glob),"|",escape_re(number),"|",escape_re(dot),"|",escape_re(optional),"|",escape_re(open_group),"|",escape_re(close_group),"|",escape_re(plus),")"].join(""),"g");const clear_re=new RegExp(escape_re(open)+"("+name_re+")"+escape_re(close),"g");return function(str){const result=[];let index=0;let parentheses=0;str=str.split(tokenizer_re).map(function(chunk,i,chunks){if(chunk===open_group){parentheses++}else if(chunk===close_group){parentheses--}if([open_group,plus,close_group,optional,dot,number].includes(chunk)){return chunk}else if(chunk===glob){result.push(index++);return glob_re}else if(chunk.match(tag_re)){result.push(chunk.replace(clear_re,"$1"));return"([^\\/]+)"}else{return chunk}}).join("");if(parentheses!==0){throw new Error(`Wayne: Unbalanced parentheses in an expression: ${str}`)}return{re:str,names:result}}};const parse=self.route_parser(open_tag,close_tag);self.parse=parse;self.pick=function(routes,url,origin){let input;let keys;if(routes instanceof Array){input={};keys=routes;routes.map(function(route){input[route]=route})}else{keys=Object.keys(routes);input=routes}const results=[];for(let i=keys.length;i--;){const key=keys[i];const route=input[key];let pattern;const re=/:\/\/([^\/]+)(\/.*)/;let m=key.match(re);if(m){const key_origin=m[1];if(key_origin.match(/\*/)){const re=new RegExp(key_origin.replace(/\*/g,glob_re));if(!origin.match(re)){continue}}else{const url=new URL(key);if(url.origin!==origin){continue}}pattern=m[2]}else if(!same_origin(origin)){continue}else{pattern=key}const parts=parse(pattern);route.forEach(({handler,options})=>{const caseSensitive=options.caseSensitive??true;m=url.match(new RegExp("^"+parts.re+"$",caseSensitive?"":"i"));if(m){const matched=m.slice(1);const data={};if(matched.length){parts.names.forEach((name,i)=>{data[name]=matched[i]})}results.push({pattern:key,handler:handler,data:data})}})}return results}}function html(content){return["","","",'',"Wayne Service Worker","","",...content,"",""].join("\n")}function error500(error){var output=html(["

Wayne: 500 Server Error

","

Service worker give 500 error

",`

${error.message||error}

`,`
${error.stack||""}
`]);return[output,{status:500,statusText:"500 Server Error"}]}function dir(prefix,path,list){var output=html(["

Wayne

",`

Content of ${path}

`,"
    ",...list.map(name=>{return`
  • ${name}
  • `}),"
"]);return[output,{status:404,statusText:"404 Page Not Found"}]}function error404(path){var output=html(["

Wayne: 404 File Not Found

",`

File ${path} not found`]);return[output,{status:404,statusText:"404 Page Not Found"}]}function createChunk({data,event,retry,id}){return Object.entries({event:event,id:id,data:data,retry:retry}).filter(([,value])=>value).map(([key,value])=>`${key}: ${value}`).join("\n")+"\n\n"}function trigger(maybeFn,...args){if(typeof maybeFn==="function"){maybeFn(...args)}}function chain_handlers(handlers,callback){if(handlers.length){return new Promise((resolve,reject)=>{let i=0;(async function recur(){const handler=handlers[i];if(!handler){return resolve()}try{await callback(handler,function next(){i++;recur()})}catch(error){reject(error)}})()})}}async function list_dir({fs,path},path_name){const names=await fs.readdir(path_name);return Promise.all(names.map(async name=>{const fullname=path.join(path_name,name);const stat=await fs.stat(fullname);if(stat.isDirectory()){return`${name}/`}return name}))}export function FileSystem(options){let{path,prefix="",test,dir=()=>"/",fs,mime,default_file="index.html"}=options;fs=bind_fs(fs);const parser=new RouteParser;if(prefix&&!prefix.startsWith("/")){prefix=`/${prefix}`}if(!test){test=url=>url.pathname.startsWith(prefix)}async function serve(res,path_name){const ext=path.extname(path_name);const type=mime.getType(ext);const data=await fs.readFile(path_name);res.send(data,{type:type})}return async function(req,res,next){const method=req.method;const url=new URL(req.url);let path_name=normalize_url(decodeURIComponent(url.pathname));url.pathname=path_name;if(!(same_origin(url.origin)&&await test(url))){return next()}if(req.method!=="GET"){return res.send("Method Not Allowed",{status:405})}if(prefix){path_name=path_name.substring(prefix.length)}if(!path_name){path_name="/"}path_name=path.join(await dir(),path_name);try{const stat=await fs.stat(path_name);if(stat.isFile()){await serve(res,path_name)}else if(stat.isDirectory()){const default_path=path.join(path_name,default_file);const stat=await fs.stat(default_path);if(stat.isFile()){await serve(res,default_path)}else{res.html(...dir(prefix,path_name,await list_dir({fs:fs,path:path},path_name)))}}}catch(e){console.log(e.stack);if(typeof stat==="undefined"){res.html(...error404(path_name))}else{res.html(...error500(error))}}}}function pluck(name){return function(object){return object[name]}}function handlers(arr){return arr.map(pluck("handler"))}export class Wayne{constructor({filter=()=>true}={}){this._er_handlers=[];this._middlewares=[];this._routes={};this._timeout=5*60*1e3;this._parser=new RouteParser;self.addEventListener("fetch",event=>{if(filter(event.request)===false){return}const promise=new Promise(async(resolve,reject)=>{const req=event.request;try{const res=new HTTPResponse(resolve,reject);await chain_handlers(this._middlewares,function(fn,next){return fn(req,res,next)});const method=req.method;const url=new URL(req.url);const path=normalize_url(url.pathname);const origin=url.origin;const routes=this._routes[method];if(routes){const match=this._parser.pick(routes,path,origin);const have_wildcard=match.length>1&&match.find(route=>{return!!route.pattern.match(/\*/)});if(match.length){let selected_route;if(have_wildcard){selected_route=match.find(route=>{return!route.pattern.match(/\*/)})}if(!(have_wildcard&&selected_route)){selected_route=match[0]}const fns=[...this._middlewares,...handlers(match)];req.params=selected_route.data;setTimeout(function(){reject("Timeout Error")},this._timeout);await chain_handlers(fns,(fn,next)=>{return fn(req,res,next)});return}}if(event.request.cache==="only-if-cached"&&event.request.mode!=="same-origin"){return}fetch(event.request).then(resolve).catch(reject)}catch(error){this._handle_error(resolve,req,error)}});event.respondWith(promise.catch(()=>{}))});["GET","POST","DELETE","PATCH","PUT"].forEach(method=>{this[method.toLowerCase()]=this.method(method)})}_handle_error(resolve,req,error){const res=new HTTPResponse(resolve);if(this._er_handlers.length){chain_handlers(this._er_handlers,function(handler,next){handler(error,req,res,next)},function(error){res.html(...error500(error))})}else{res.html(...error500(error))}}use(...fns){fns.forEach(fn=>{if(typeof fn==="function"){if(fn.length===4){this._er_handlers.push(fn)}else if(fn.length===3){this._middlewares.push(fn)}}})}method(method){return function(url,handler,options={}){if(!this._routes[method]){this._routes[method]={}}const routes=this._routes[method];if(!routes[url]){routes[url]=[]}routes[url].push({handler:handler,options:options});return this}}}export function rpc(channel,methods){channel.addEventListener("message",async function handler(message){if(Object.keys(message.data).includes("method","id","args")){const{method,id,args}=message.data;try{const result=await methods[method](...args);channel.postMessage({id:id,result:result})}catch(error){channel.postMessage({id:id,error:error})}}})}let rpc_id=0;export function send(channel,method,args){return new Promise((resolve,reject)=>{const id=++rpc_id;const payload={id:id,method:method,args:args};channel.addEventListener("message",function handler(message){if(id==message.data.id){const data=message.data;channel.removeEventListener("message",handler);if(data.error){reject(data.error)}else{resolve(message.data)}}});channel.postMessage(payload)})} +const root_url=get_root_path();const root_url_re=new RegExp("^"+escape_re(root_url));function same_origin(origin){return origin===self.location.origin}function get_root_path(){if(self.registration){const url=new URL(registration.scope);return url.pathname.replace(/\/$/,"")}return location.pathname.replace(/\/[^\/]+$/,"")}function normalize_url(url){return url.replace(root_url_re,"")}function escape_re(str){if(typeof str=="string"){var special=/([\^\$\[\]\(\)\{\}\+\*\.\|\?])/g;return str.replace(special,"\\$1")}}function is_function(arg){return typeof arg==="function"}function is_promise(arg){return arg&&typeof arg==="object"&&is_function(arg.then)}function isPromiseFs(fs){const test=targetFs=>{try{return targetFs.readFile().catch(e=>e)}catch(e){return e}};return is_promise(test(fs))}const commands=["stat","readdir","readFile"];function bind_fs(fs){const result={};if(isPromiseFs(fs)){for(const command of commands){result[command]=fs[command].bind(fs)}}else{for(const command of commands){result[command]=function(...args){return new Promise((resolve,reject)=>{fs[command](...args,function(err,data){if(err){reject(err)}else{resolve(data)}})})}}}return result}export class HTTPResponse{constructor(resolve,reject){this._resolve=resolve;this._reject=reject}html(data,init){this.send(data,{type:"text/html",...init})}text(data,init){this.send(data,init)}json(data,init){this.send(JSON.stringify(data),{type:"application/json",...init})}respond(response){this._resolve(response)}blob(blob,init={}){this._resolve(new Response(blob,init))}send(data,{type="text/plain",...init}={}){if(![undefined,null].includes(data)){data=new Blob([data],{type:type})}this.blob(data,init)}async fetch(arg){if(typeof arg==="string"){const _res=await fetch(arg);const type=_res.headers.get("Content-Type")??"application/octet-stream";this.send(await _res.arrayBuffer(),{type:type})}else if(arg instanceof Request){return fetch(arg).then(this._resolve).catch(this._reject)}}download(content,{filename="download",type="text/plain",...init}={}){const headers={"Content-Disposition":`attachment; filename="${filename}"`};this.send(content,{type:type,headers:headers,...init})}redirect(code,url){if(url===undefined){url=code;code=302}if(!url.match(/https?:\/\//)){url=root_url+url}this._resolve(Response.redirect(url,code))}sse({onClose}={}){let send,close,stream,defunct;stream=new ReadableStream({cancel(){defunct=true;trigger(onClose)},start:controller=>{send=function(event){if(!defunct){const chunk=createChunk(event);const payload=(new TextEncoder).encode(chunk);controller.enqueue(payload)}};close=function close(){controller.close();stream=null;trigger(onClose)}}});this._resolve(new Response(stream,{headers:{"Content-Type":"text/event-stream; charset=utf-8","Transfer-Encoding":"chunked",Connection:"keep-alive"}}));return{send:send,close:close}}}export function RouteParser(){const name_re="[a-zA-Z_][a-zA-Z_0-9]*";const self=this;const open_tag="{";const close_tag="}";const glob="*";const glob_re="(.*?)";const number="\\d";const optional="?";const open_group="(";const close_group=")";const plus="+";const dot=".";self.route_parser=function(open,close){const routes={};const tag_re=new RegExp("("+escape_re(open)+name_re+escape_re(close)+")","g");const tokenizer_re=new RegExp(["(",escape_re(open),name_re,escape_re(close),"|",escape_re(glob),"|",escape_re(number),"|",escape_re(dot),"|",escape_re(optional),"|",escape_re(open_group),"|",escape_re(close_group),"|",escape_re(plus),")"].join(""),"g");const clear_re=new RegExp(escape_re(open)+"("+name_re+")"+escape_re(close),"g");return function(str){const result=[];let index=0;let parentheses=0;str=str.split(tokenizer_re).map(function(chunk,i,chunks){if(chunk===open_group){parentheses++}else if(chunk===close_group){parentheses--}if([open_group,plus,close_group,optional,dot,number].includes(chunk)){return chunk}else if(chunk===glob){result.push(index++);return glob_re}else if(chunk.match(tag_re)){result.push(chunk.replace(clear_re,"$1"));return"([^\\/]+)"}else{return chunk}}).join("");if(parentheses!==0){throw new Error(`Wayne: Unbalanced parentheses in an expression: ${str}`)}return{re:str,names:result}}};const parse=self.route_parser(open_tag,close_tag);self.parse=parse;self.pick=function(routes,url,origin){let input;let keys;if(routes instanceof Array){input={};keys=routes;routes.map(function(route){input[route]=route})}else{keys=Object.keys(routes);input=routes}const results=[];for(let i=keys.length;i--;){const key=keys[i];const route=input[key];let pattern;const re=/:\/\/([^\/]+)(\/.*)/;let m=key.match(re);if(m){const key_origin=m[1];if(key_origin.match(/\*/)){const re=new RegExp(key_origin.replace(/\*/g,glob_re));if(!origin.match(re)){continue}}else{const url=new URL(key);if(url.origin!==origin){continue}}pattern=m[2]}else if(!same_origin(origin)){continue}else{pattern=key}const parts=parse(pattern);route.forEach(({handler,options})=>{const caseSensitive=options.caseSensitive??true;m=url.match(new RegExp("^"+parts.re+"$",caseSensitive?"":"i"));if(m){const matched=m.slice(1);const data={};if(matched.length){parts.names.forEach((name,i)=>{data[name]=matched[i]})}results.push({pattern:key,handler:handler,data:data})}})}return results}}function html(content){return["","","",'',"Wayne Service Worker","","",...content,"",""].join("\n")}function error500(error){var output=html(["

Wayne: 500 Server Error

","

Service worker give 500 error

",`

${error.message||error}

`,`
${error.stack||""}
`]);return[output,{status:500,statusText:"500 Server Error"}]}function dir(prefix,path,list){var output=html(["

Wayne

",`

Content of ${path}

`,"
    ",...list.map(name=>{return`
  • ${name}
  • `}),"
"]);return[output,{status:404,statusText:"404 Page Not Found"}]}function error404(path){var output=html(["

Wayne: 404 File Not Found

",`

File ${path} not found`]);return[output,{status:404,statusText:"404 Page Not Found"}]}function createChunk({data,event,retry,id}){return Object.entries({event:event,id:id,data:data,retry:retry}).filter(([,value])=>value).map(([key,value])=>`${key}: ${value}`).join("\n")+"\n\n"}function trigger(maybeFn,...args){if(typeof maybeFn==="function"){maybeFn(...args)}}function chain_handlers(handlers,callback){if(handlers.length){return new Promise((resolve,reject)=>{let i=0;(async function recur(){const handler=handlers[i];if(!handler){return resolve()}try{await callback(handler,function next(){i++;recur()})}catch(error){reject(error)}})()})}}async function list_dir({fs,path},path_name){const names=await fs.readdir(path_name);return Promise.all(names.map(async name=>{const fullname=path.join(path_name,name);const stat=await fs.stat(fullname);if(stat.isDirectory()){return`${name}/`}return name}))}export function FileSystem(options){let{path,prefix="",test,dir=()=>"/",fs,mime,default_file="index.html"}=options;fs=bind_fs(fs);const parser=new RouteParser;if(prefix&&!prefix.startsWith("/")){prefix=`/${prefix}`}if(!test){test=url=>url.pathname.startsWith(prefix)}async function serve(res,path_name){const ext=path.extname(path_name);const type=mime.getType(ext);const data=await fs.readFile(path_name);res.send(data,{type:type})}return async function(req,res,next){const method=req.method;const url=new URL(req.url);let path_name=normalize_url(decodeURIComponent(url.pathname));url.pathname=path_name;if(!(same_origin(url.origin)&&await test(url))){return next()}if(req.method!=="GET"){return res.send("Method Not Allowed",{status:405})}if(prefix){path_name=path_name.substring(prefix.length)}if(!path_name){path_name="/"}path_name=path.join(await dir(),path_name);try{const stat=await fs.stat(path_name);if(stat.isFile()){await serve(res,path_name)}else if(stat.isDirectory()){const default_path=path.join(path_name,default_file);const stat=await fs.stat(default_path);if(stat.isFile()){await serve(res,default_path)}else{res.html(...dir(prefix,path_name,await list_dir({fs:fs,path:path},path_name)))}}}catch(e){console.log(e.stack);if(typeof stat==="undefined"){res.html(...error404(path_name))}else{res.html(...error500(error))}}}}function pluck(name){return function(object){return object[name]}}function handlers(arr){return arr.map(pluck("handler"))}export class Wayne{constructor({filter=()=>true}={}){this._er_handlers=[];this._middlewares=[];this._routes={};this._timeout=5*60*1e3;this._parser=new RouteParser;self.addEventListener("fetch",event=>{if(filter(event.request)===false){return}const promise=new Promise(async(resolve,reject)=>{const req=event.request;try{const res=new HTTPResponse(resolve,reject);await chain_handlers(this._middlewares,function(fn,next){return fn(req,res,next)});const method=req.method;const url=new URL(req.url);const path=normalize_url(url.pathname);const origin=url.origin;const routes=this._routes[method];if(routes){const match=this._parser.pick(routes,path,origin);const have_wildcard=match.length>1&&match.find(route=>{return!!route.pattern.match(/\*/)});if(match.length){let selected_route;if(have_wildcard){selected_route=match.find(route=>{return!route.pattern.match(/\*/)})}if(!(have_wildcard&&selected_route)){selected_route=match[0]}const fns=[...this._middlewares,...handlers(match)];req.params=selected_route.data;setTimeout(function(){reject("Timeout Error")},this._timeout);await chain_handlers(fns,(fn,next)=>{return fn(req,res,next)});return}}if(event.request.cache==="only-if-cached"&&event.request.mode!=="same-origin"){return}fetch(event.request).then(resolve).catch(reject)}catch(error){this._handle_error(resolve,req,error)}});event.respondWith(promise.catch(()=>{}))});["GET","POST","DELETE","PATCH","PUT"].forEach(method=>{this[method.toLowerCase()]=this.method(method)})}_handle_error(resolve,req,error){const res=new HTTPResponse(resolve);if(this._er_handlers.length){chain_handlers(this._er_handlers,function(handler,next){handler(error,req,res,next)},function(error){res.html(...error500(error))})}else{res.html(...error500(error))}}use(...fns){fns.forEach(fn=>{if(typeof fn==="function"){if(fn.length===4){this._er_handlers.push(fn)}else if(fn.length===3){this._middlewares.push(fn)}}})}method(method){return function(url,handler,options={}){if(!this._routes[method]){this._routes[method]={}}const routes=this._routes[method];if(!routes[url]){routes[url]=[]}routes[url].push({handler:handler,options:options});return this}}}export function rpc(channel,methods){channel.addEventListener("message",async function handler(message){if(Object.keys(message.data).includes("method","id","args")){const{method,id,args}=message.data;try{const result=await methods[method](...args);channel.postMessage({id:id,result:result})}catch(error){channel.postMessage({id:id,error:error})}}})}let rpc_id=0;export function send(channel,method,args){return new Promise((resolve,reject)=>{const id=++rpc_id;const payload={id:id,method:method,args:args};channel.addEventListener("message",function handler(message){if(id==message.data.id){const data=message.data;channel.removeEventListener("message",handler);if(data.error){reject(data.error)}else{resolve(message.data)}}});channel.postMessage(payload)})} diff --git a/index.umd.js b/index.umd.js index 3cdd3ac..123e828 100644 --- a/index.umd.js +++ b/index.umd.js @@ -1,10 +1,10 @@ /* - * Wayne - Server Worker Routing library (v. 0.17.0) + * Wayne - Server Worker Routing library (v. 0.18.0) * * Copyright (c) 2022-2024 Jakub T. Jankiewicz * Released under MIT license * - * Fri, 26 Jul 2024 23:46:08 +0000 + * Sat, 05 Oct 2024 17:28:37 +0000 */ (function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.wayne = f()}})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i * Released under MIT license @@ -116,6 +116,9 @@ class HTTPResponse { ...init }); } + respond(response) { + this._resolve(response); + } blob(blob, init = {}) { this._resolve(new Response(blob, init)); } diff --git a/index.umd.min.js b/index.umd.min.js index fcbb0c7..5fab73c 100644 --- a/index.umd.min.js +++ b/index.umd.min.js @@ -1,9 +1,9 @@ /* - * Wayne - Server Worker Routing library (v. 0.17.0) + * Wayne - Server Worker Routing library (v. 0.18.0) * * Copyright (c) 2022-2024 Jakub T. Jankiewicz * Released under MIT license * - * Fri, 26 Jul 2024 23:46:08 +0000 + * Sat, 05 Oct 2024 17:28:37 +0000 */ -(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.wayne=f()}})(function(){var define,module,exports;return function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i{try{return targetFs.readFile().catch(e=>e)}catch(e){return e}};return is_promise(test(fs))}const commands=["stat","readdir","readFile"];function bind_fs(fs){const result={};if(isPromiseFs(fs)){for(const command of commands){result[command]=fs[command].bind(fs)}}else{for(const command of commands){result[command]=function(...args){return new Promise((resolve,reject)=>{fs[command](...args,function(err,data){if(err){reject(err)}else{resolve(data)}})})}}}return result}class HTTPResponse{constructor(resolve,reject){this._resolve=resolve;this._reject=reject}html(data,init){this.send(data,{type:"text/html",...init})}text(data,init){this.send(data,init)}json(data,init){this.send(JSON.stringify(data),{type:"application/json",...init})}blob(blob,init={}){this._resolve(new Response(blob,init))}send(data,{type="text/plain",...init}={}){if(![undefined,null].includes(data)){data=new Blob([data],{type:type})}this.blob(data,init)}async fetch(arg){if(typeof arg==="string"){const _res=await fetch(arg);const type=_res.headers.get("Content-Type")??"application/octet-stream";this.send(await _res.arrayBuffer(),{type:type})}else if(arg instanceof Request){return fetch(arg).then(this._resolve).catch(this._reject)}}download(content,{filename="download",type="text/plain",...init}={}){const headers={"Content-Disposition":`attachment; filename="${filename}"`};this.send(content,{type:type,headers:headers,...init})}redirect(code,url){if(url===undefined){url=code;code=302}if(!url.match(/https?:\/\//)){url=root_url+url}this._resolve(Response.redirect(url,code))}sse({onClose}={}){let send,close,stream,defunct;stream=new ReadableStream({cancel(){defunct=true;trigger(onClose)},start:controller=>{send=function(event){if(!defunct){const chunk=createChunk(event);const payload=(new TextEncoder).encode(chunk);controller.enqueue(payload)}};close=function close(){controller.close();stream=null;trigger(onClose)}}});this._resolve(new Response(stream,{headers:{"Content-Type":"text/event-stream; charset=utf-8","Transfer-Encoding":"chunked",Connection:"keep-alive"}}));return{send:send,close:close}}}exports.HTTPResponse=HTTPResponse;function RouteParser(){const name_re="[a-zA-Z_][a-zA-Z_0-9]*";const self=this;const open_tag="{";const close_tag="}";const glob="*";const glob_re="(.*?)";const number="\\d";const optional="?";const open_group="(";const close_group=")";const plus="+";const dot=".";self.route_parser=function(open,close){const routes={};const tag_re=new RegExp("("+escape_re(open)+name_re+escape_re(close)+")","g");const tokenizer_re=new RegExp(["(",escape_re(open),name_re,escape_re(close),"|",escape_re(glob),"|",escape_re(number),"|",escape_re(dot),"|",escape_re(optional),"|",escape_re(open_group),"|",escape_re(close_group),"|",escape_re(plus),")"].join(""),"g");const clear_re=new RegExp(escape_re(open)+"("+name_re+")"+escape_re(close),"g");return function(str){const result=[];let index=0;let parentheses=0;str=str.split(tokenizer_re).map(function(chunk,i,chunks){if(chunk===open_group){parentheses++}else if(chunk===close_group){parentheses--}if([open_group,plus,close_group,optional,dot,number].includes(chunk)){return chunk}else if(chunk===glob){result.push(index++);return glob_re}else if(chunk.match(tag_re)){result.push(chunk.replace(clear_re,"$1"));return"([^\\/]+)"}else{return chunk}}).join("");if(parentheses!==0){throw new Error(`Wayne: Unbalanced parentheses in an expression: ${str}`)}return{re:str,names:result}}};const parse=self.route_parser(open_tag,close_tag);self.parse=parse;self.pick=function(routes,url,origin){let input;let keys;if(routes instanceof Array){input={};keys=routes;routes.map(function(route){input[route]=route})}else{keys=Object.keys(routes);input=routes}const results=[];for(let i=keys.length;i--;){const key=keys[i];const route=input[key];let pattern;const re=/:\/\/([^\/]+)(\/.*)/;let m=key.match(re);if(m){const key_origin=m[1];if(key_origin.match(/\*/)){const re=new RegExp(key_origin.replace(/\*/g,glob_re));if(!origin.match(re)){continue}}else{const url=new URL(key);if(url.origin!==origin){continue}}pattern=m[2]}else if(!same_origin(origin)){continue}else{pattern=key}const parts=parse(pattern);route.forEach(({handler,options})=>{const caseSensitive=options.caseSensitive??true;m=url.match(new RegExp("^"+parts.re+"$",caseSensitive?"":"i"));if(m){const matched=m.slice(1);const data={};if(matched.length){parts.names.forEach((name,i)=>{data[name]=matched[i]})}results.push({pattern:key,handler:handler,data:data})}})}return results}}function html(content){return["","","",'',"Wayne Service Worker","","",...content,"",""].join("\n")}function error500(error){var output=html(["

Wayne: 500 Server Error

","

Service worker give 500 error

",`

${error.message||error}

`,`
${error.stack||""}
`]);return[output,{status:500,statusText:"500 Server Error"}]}function dir(prefix,path,list){var output=html(["

Wayne

",`

Content of ${path}

`,"
    ",...list.map(name=>{return`
  • ${name}
  • `}),"
"]);return[output,{status:404,statusText:"404 Page Not Found"}]}function error404(path){var output=html(["

Wayne: 404 File Not Found

",`

File ${path} not found`]);return[output,{status:404,statusText:"404 Page Not Found"}]}function createChunk({data,event,retry,id}){return Object.entries({event:event,id:id,data:data,retry:retry}).filter(([,value])=>value).map(([key,value])=>`${key}: ${value}`).join("\n")+"\n\n"}function trigger(maybeFn,...args){if(typeof maybeFn==="function"){maybeFn(...args)}}function chain_handlers(handlers,callback){if(handlers.length){return new Promise((resolve,reject)=>{let i=0;(async function recur(){const handler=handlers[i];if(!handler){return resolve()}try{await callback(handler,function next(){i++;recur()})}catch(error){reject(error)}})()})}}async function list_dir({fs,path},path_name){const names=await fs.readdir(path_name);return Promise.all(names.map(async name=>{const fullname=path.join(path_name,name);const stat=await fs.stat(fullname);if(stat.isDirectory()){return`${name}/`}return name}))}function FileSystem(options){let{path,prefix="",test,dir=()=>"/",fs,mime,default_file="index.html"}=options;fs=bind_fs(fs);const parser=new RouteParser;if(prefix&&!prefix.startsWith("/")){prefix=`/${prefix}`}if(!test){test=url=>url.pathname.startsWith(prefix)}async function serve(res,path_name){const ext=path.extname(path_name);const type=mime.getType(ext);const data=await fs.readFile(path_name);res.send(data,{type:type})}return async function(req,res,next){const method=req.method;const url=new URL(req.url);let path_name=normalize_url(decodeURIComponent(url.pathname));url.pathname=path_name;if(!(same_origin(url.origin)&&await test(url))){return next()}if(req.method!=="GET"){return res.send("Method Not Allowed",{status:405})}if(prefix){path_name=path_name.substring(prefix.length)}if(!path_name){path_name="/"}path_name=path.join(await dir(),path_name);try{const stat=await fs.stat(path_name);if(stat.isFile()){await serve(res,path_name)}else if(stat.isDirectory()){const default_path=path.join(path_name,default_file);const stat=await fs.stat(default_path);if(stat.isFile()){await serve(res,default_path)}else{res.html(...dir(prefix,path_name,await list_dir({fs:fs,path:path},path_name)))}}}catch(e){console.log(e.stack);if(typeof stat==="undefined"){res.html(...error404(path_name))}else{res.html(...error500(error))}}}}function pluck(name){return function(object){return object[name]}}function handlers(arr){return arr.map(pluck("handler"))}class Wayne{constructor({filter=()=>true}={}){this._er_handlers=[];this._middlewares=[];this._routes={};this._timeout=5*60*1e3;this._parser=new RouteParser;self.addEventListener("fetch",event=>{if(filter(event.request)===false){return}const promise=new Promise(async(resolve,reject)=>{const req=event.request;try{const res=new HTTPResponse(resolve,reject);await chain_handlers(this._middlewares,function(fn,next){return fn(req,res,next)});const method=req.method;const url=new URL(req.url);const path=normalize_url(url.pathname);const origin=url.origin;const routes=this._routes[method];if(routes){const match=this._parser.pick(routes,path,origin);const have_wildcard=match.length>1&&match.find(route=>{return!!route.pattern.match(/\*/)});if(match.length){let selected_route;if(have_wildcard){selected_route=match.find(route=>{return!route.pattern.match(/\*/)})}if(!(have_wildcard&&selected_route)){selected_route=match[0]}const fns=[...this._middlewares,...handlers(match)];req.params=selected_route.data;setTimeout(function(){reject("Timeout Error")},this._timeout);await chain_handlers(fns,(fn,next)=>{return fn(req,res,next)});return}}if(event.request.cache==="only-if-cached"&&event.request.mode!=="same-origin"){return}fetch(event.request).then(resolve).catch(reject)}catch(error){this._handle_error(resolve,req,error)}});event.respondWith(promise.catch(()=>{}))});["GET","POST","DELETE","PATCH","PUT"].forEach(method=>{this[method.toLowerCase()]=this.method(method)})}_handle_error(resolve,req,error){const res=new HTTPResponse(resolve);if(this._er_handlers.length){chain_handlers(this._er_handlers,function(handler,next){handler(error,req,res,next)},function(error){res.html(...error500(error))})}else{res.html(...error500(error))}}use(...fns){fns.forEach(fn=>{if(typeof fn==="function"){if(fn.length===4){this._er_handlers.push(fn)}else if(fn.length===3){this._middlewares.push(fn)}}})}method(method){return function(url,handler,options={}){if(!this._routes[method]){this._routes[method]={}}const routes=this._routes[method];if(!routes[url]){routes[url]=[]}routes[url].push({handler:handler,options:options});return this}}}exports.Wayne=Wayne;function rpc(channel,methods){channel.addEventListener("message",async function handler(message){if(Object.keys(message.data).includes("method","id","args")){const{method,id,args}=message.data;try{const result=await methods[method](...args);channel.postMessage({id:id,result:result})}catch(error){channel.postMessage({id:id,error:error})}}})}let rpc_id=0;function send(channel,method,args){return new Promise((resolve,reject)=>{const id=++rpc_id;const payload={id:id,method:method,args:args};channel.addEventListener("message",function handler(message){if(id==message.data.id){const data=message.data;channel.removeEventListener("message",handler);if(data.error){reject(data.error)}else{resolve(message.data)}}});channel.postMessage(payload)})}},{}]},{},[1])(1)}); +(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.wayne=f()}})(function(){var define,module,exports;return function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i{try{return targetFs.readFile().catch(e=>e)}catch(e){return e}};return is_promise(test(fs))}const commands=["stat","readdir","readFile"];function bind_fs(fs){const result={};if(isPromiseFs(fs)){for(const command of commands){result[command]=fs[command].bind(fs)}}else{for(const command of commands){result[command]=function(...args){return new Promise((resolve,reject)=>{fs[command](...args,function(err,data){if(err){reject(err)}else{resolve(data)}})})}}}return result}class HTTPResponse{constructor(resolve,reject){this._resolve=resolve;this._reject=reject}html(data,init){this.send(data,{type:"text/html",...init})}text(data,init){this.send(data,init)}json(data,init){this.send(JSON.stringify(data),{type:"application/json",...init})}respond(response){this._resolve(response)}blob(blob,init={}){this._resolve(new Response(blob,init))}send(data,{type="text/plain",...init}={}){if(![undefined,null].includes(data)){data=new Blob([data],{type:type})}this.blob(data,init)}async fetch(arg){if(typeof arg==="string"){const _res=await fetch(arg);const type=_res.headers.get("Content-Type")??"application/octet-stream";this.send(await _res.arrayBuffer(),{type:type})}else if(arg instanceof Request){return fetch(arg).then(this._resolve).catch(this._reject)}}download(content,{filename="download",type="text/plain",...init}={}){const headers={"Content-Disposition":`attachment; filename="${filename}"`};this.send(content,{type:type,headers:headers,...init})}redirect(code,url){if(url===undefined){url=code;code=302}if(!url.match(/https?:\/\//)){url=root_url+url}this._resolve(Response.redirect(url,code))}sse({onClose}={}){let send,close,stream,defunct;stream=new ReadableStream({cancel(){defunct=true;trigger(onClose)},start:controller=>{send=function(event){if(!defunct){const chunk=createChunk(event);const payload=(new TextEncoder).encode(chunk);controller.enqueue(payload)}};close=function close(){controller.close();stream=null;trigger(onClose)}}});this._resolve(new Response(stream,{headers:{"Content-Type":"text/event-stream; charset=utf-8","Transfer-Encoding":"chunked",Connection:"keep-alive"}}));return{send:send,close:close}}}exports.HTTPResponse=HTTPResponse;function RouteParser(){const name_re="[a-zA-Z_][a-zA-Z_0-9]*";const self=this;const open_tag="{";const close_tag="}";const glob="*";const glob_re="(.*?)";const number="\\d";const optional="?";const open_group="(";const close_group=")";const plus="+";const dot=".";self.route_parser=function(open,close){const routes={};const tag_re=new RegExp("("+escape_re(open)+name_re+escape_re(close)+")","g");const tokenizer_re=new RegExp(["(",escape_re(open),name_re,escape_re(close),"|",escape_re(glob),"|",escape_re(number),"|",escape_re(dot),"|",escape_re(optional),"|",escape_re(open_group),"|",escape_re(close_group),"|",escape_re(plus),")"].join(""),"g");const clear_re=new RegExp(escape_re(open)+"("+name_re+")"+escape_re(close),"g");return function(str){const result=[];let index=0;let parentheses=0;str=str.split(tokenizer_re).map(function(chunk,i,chunks){if(chunk===open_group){parentheses++}else if(chunk===close_group){parentheses--}if([open_group,plus,close_group,optional,dot,number].includes(chunk)){return chunk}else if(chunk===glob){result.push(index++);return glob_re}else if(chunk.match(tag_re)){result.push(chunk.replace(clear_re,"$1"));return"([^\\/]+)"}else{return chunk}}).join("");if(parentheses!==0){throw new Error(`Wayne: Unbalanced parentheses in an expression: ${str}`)}return{re:str,names:result}}};const parse=self.route_parser(open_tag,close_tag);self.parse=parse;self.pick=function(routes,url,origin){let input;let keys;if(routes instanceof Array){input={};keys=routes;routes.map(function(route){input[route]=route})}else{keys=Object.keys(routes);input=routes}const results=[];for(let i=keys.length;i--;){const key=keys[i];const route=input[key];let pattern;const re=/:\/\/([^\/]+)(\/.*)/;let m=key.match(re);if(m){const key_origin=m[1];if(key_origin.match(/\*/)){const re=new RegExp(key_origin.replace(/\*/g,glob_re));if(!origin.match(re)){continue}}else{const url=new URL(key);if(url.origin!==origin){continue}}pattern=m[2]}else if(!same_origin(origin)){continue}else{pattern=key}const parts=parse(pattern);route.forEach(({handler,options})=>{const caseSensitive=options.caseSensitive??true;m=url.match(new RegExp("^"+parts.re+"$",caseSensitive?"":"i"));if(m){const matched=m.slice(1);const data={};if(matched.length){parts.names.forEach((name,i)=>{data[name]=matched[i]})}results.push({pattern:key,handler:handler,data:data})}})}return results}}function html(content){return["","","",'',"Wayne Service Worker","","",...content,"",""].join("\n")}function error500(error){var output=html(["

Wayne: 500 Server Error

","

Service worker give 500 error

",`

${error.message||error}

`,`
${error.stack||""}
`]);return[output,{status:500,statusText:"500 Server Error"}]}function dir(prefix,path,list){var output=html(["

Wayne

",`

Content of ${path}

`,"
    ",...list.map(name=>{return`
  • ${name}
  • `}),"
"]);return[output,{status:404,statusText:"404 Page Not Found"}]}function error404(path){var output=html(["

Wayne: 404 File Not Found

",`

File ${path} not found`]);return[output,{status:404,statusText:"404 Page Not Found"}]}function createChunk({data,event,retry,id}){return Object.entries({event:event,id:id,data:data,retry:retry}).filter(([,value])=>value).map(([key,value])=>`${key}: ${value}`).join("\n")+"\n\n"}function trigger(maybeFn,...args){if(typeof maybeFn==="function"){maybeFn(...args)}}function chain_handlers(handlers,callback){if(handlers.length){return new Promise((resolve,reject)=>{let i=0;(async function recur(){const handler=handlers[i];if(!handler){return resolve()}try{await callback(handler,function next(){i++;recur()})}catch(error){reject(error)}})()})}}async function list_dir({fs,path},path_name){const names=await fs.readdir(path_name);return Promise.all(names.map(async name=>{const fullname=path.join(path_name,name);const stat=await fs.stat(fullname);if(stat.isDirectory()){return`${name}/`}return name}))}function FileSystem(options){let{path,prefix="",test,dir=()=>"/",fs,mime,default_file="index.html"}=options;fs=bind_fs(fs);const parser=new RouteParser;if(prefix&&!prefix.startsWith("/")){prefix=`/${prefix}`}if(!test){test=url=>url.pathname.startsWith(prefix)}async function serve(res,path_name){const ext=path.extname(path_name);const type=mime.getType(ext);const data=await fs.readFile(path_name);res.send(data,{type:type})}return async function(req,res,next){const method=req.method;const url=new URL(req.url);let path_name=normalize_url(decodeURIComponent(url.pathname));url.pathname=path_name;if(!(same_origin(url.origin)&&await test(url))){return next()}if(req.method!=="GET"){return res.send("Method Not Allowed",{status:405})}if(prefix){path_name=path_name.substring(prefix.length)}if(!path_name){path_name="/"}path_name=path.join(await dir(),path_name);try{const stat=await fs.stat(path_name);if(stat.isFile()){await serve(res,path_name)}else if(stat.isDirectory()){const default_path=path.join(path_name,default_file);const stat=await fs.stat(default_path);if(stat.isFile()){await serve(res,default_path)}else{res.html(...dir(prefix,path_name,await list_dir({fs:fs,path:path},path_name)))}}}catch(e){console.log(e.stack);if(typeof stat==="undefined"){res.html(...error404(path_name))}else{res.html(...error500(error))}}}}function pluck(name){return function(object){return object[name]}}function handlers(arr){return arr.map(pluck("handler"))}class Wayne{constructor({filter=()=>true}={}){this._er_handlers=[];this._middlewares=[];this._routes={};this._timeout=5*60*1e3;this._parser=new RouteParser;self.addEventListener("fetch",event=>{if(filter(event.request)===false){return}const promise=new Promise(async(resolve,reject)=>{const req=event.request;try{const res=new HTTPResponse(resolve,reject);await chain_handlers(this._middlewares,function(fn,next){return fn(req,res,next)});const method=req.method;const url=new URL(req.url);const path=normalize_url(url.pathname);const origin=url.origin;const routes=this._routes[method];if(routes){const match=this._parser.pick(routes,path,origin);const have_wildcard=match.length>1&&match.find(route=>{return!!route.pattern.match(/\*/)});if(match.length){let selected_route;if(have_wildcard){selected_route=match.find(route=>{return!route.pattern.match(/\*/)})}if(!(have_wildcard&&selected_route)){selected_route=match[0]}const fns=[...this._middlewares,...handlers(match)];req.params=selected_route.data;setTimeout(function(){reject("Timeout Error")},this._timeout);await chain_handlers(fns,(fn,next)=>{return fn(req,res,next)});return}}if(event.request.cache==="only-if-cached"&&event.request.mode!=="same-origin"){return}fetch(event.request).then(resolve).catch(reject)}catch(error){this._handle_error(resolve,req,error)}});event.respondWith(promise.catch(()=>{}))});["GET","POST","DELETE","PATCH","PUT"].forEach(method=>{this[method.toLowerCase()]=this.method(method)})}_handle_error(resolve,req,error){const res=new HTTPResponse(resolve);if(this._er_handlers.length){chain_handlers(this._er_handlers,function(handler,next){handler(error,req,res,next)},function(error){res.html(...error500(error))})}else{res.html(...error500(error))}}use(...fns){fns.forEach(fn=>{if(typeof fn==="function"){if(fn.length===4){this._er_handlers.push(fn)}else if(fn.length===3){this._middlewares.push(fn)}}})}method(method){return function(url,handler,options={}){if(!this._routes[method]){this._routes[method]={}}const routes=this._routes[method];if(!routes[url]){routes[url]=[]}routes[url].push({handler:handler,options:options});return this}}}exports.Wayne=Wayne;function rpc(channel,methods){channel.addEventListener("message",async function handler(message){if(Object.keys(message.data).includes("method","id","args")){const{method,id,args}=message.data;try{const result=await methods[method](...args);channel.postMessage({id:id,result:result})}catch(error){channel.postMessage({id:id,error:error})}}})}let rpc_id=0;function send(channel,method,args){return new Promise((resolve,reject)=>{const id=++rpc_id;const payload={id:id,method:method,args:args};channel.addEventListener("message",function handler(message){if(id==message.data.id){const data=message.data;channel.removeEventListener("message",handler);if(data.error){reject(data.error)}else{resolve(message.data)}}});channel.postMessage(payload)})}},{}]},{},[1])(1)}); diff --git a/package-lock.json b/package-lock.json index e75bc88..0921e2f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@jcubic/wayne", - "version": "0.17.0", + "version": "0.18.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@jcubic/wayne", - "version": "0.17.0", + "version": "0.18.0", "license": "MIT", "devDependencies": { "@babel/core": "^7.19.0", diff --git a/package.json b/package.json index 40e7d0a..50cb8e3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@jcubic/wayne", - "version": "0.17.0", + "version": "0.18.0", "description": "Service Worker Routing for in browser HTTP requests", "type": "module", "main": "index.min.js",