├── .gitignore ├── .npmignore ├── .npmrc ├── LICENSE ├── README.md ├── client.js ├── esm ├── client.js └── server.js ├── module.js ├── package.json ├── rollup ├── client.config.js └── server.config.js ├── server.js └── test ├── index.html ├── index.js ├── service.html ├── shared.html └── worker.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .nyc_output 3 | coverage/ 4 | node_modules/ 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .nyc_output 3 | .eslintrc.json 4 | .travis.yml 5 | coverage/ 6 | node_modules/ 7 | rollup/ 8 | test/ 9 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (c) 2021, Andrea Giammarchi, @WebReflection 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 10 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 11 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 12 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 13 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE 14 | OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 15 | PERFORMANCE OF THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # proxied-worker DEPRECATED - See [coincident](https://github.com/WebReflection/coincident#coincidentserver) 2 | 3 | **Social Media Photo by [Ricardo Gomez Angel](https://unsplash.com/@ripato) on [Unsplash](https://unsplash.com/)** 4 | 5 | A tiny utility to asynchronously drive a namespace exposed through a Shared/Service/Worker: 6 | 7 | * property access 8 | * functions invokes 9 | * instances creation ... 10 | * ... and instances methods invokes, or properties access 11 | 12 | Instances reflected on the client are automatically cleared up on the worker though a dedicated *FinalizationRegistry*. 13 | 14 | It is also possible, since `v0.5.0`, to use functions as arguments, although these are stored "*forever*", so use this feature with caution. 15 | Bear in mind, the context is currently not propagated from the Worker, so if it's strictly needed, bind the listener before passing it as-is. 16 | 17 | 18 | ### Related + NodeJS 19 | 20 | This module is a modern simplification of [workway](https://github.com/WebReflection/workway#readme), heavily inspired by [electroff](https://github.com/WebReflection/electroff#readme), but also **[available for NodeJS too](https://github.com/WebReflection/proxied-node#readme)** as a safer, lighter, and easier alternative. 21 | 22 | 23 | ## Compatibility / Requirements 24 | 25 | This module works with latest browsers, as long as the following APIs are available: 26 | 27 | * [FinalizationRegistry](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/FinalizationRegistry) 28 | * [Proxy](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy) 29 | * [Worker](https://developer.mozilla.org/en-US/docs/Web/API/Worker/Worker) 30 | 31 | **[Live Demo](https://webreflection.github.io/proxied-worker/test/)** 32 | 33 | ## API 34 | 35 | The exported namespace provides a mechanism to await any part of it, including a top level `addEventListener` and `removeEventListener`, to allow listening to custom `postMessage` notifications from the Service/Shared/Worker. 36 | 37 | See [worker.js](./test/worker.js) to better understand how this works. 38 | 39 | 40 | ## Example 41 | 42 | ```js 43 | // client.js 44 | import ProxiedWorker from 'https://unpkg.com/proxied-worker/client'; 45 | 46 | // point at the file that exports a namespace 47 | const nmsp = ProxiedWorker('./worker.js'); 48 | 49 | // custom notifications from the Worker 50 | nmsp.addEventListener('message', ({data: {action}}) => { 51 | if (action === 'greetings') 52 | console.log('Worker said hello 👋'); 53 | }); 54 | 55 | // v0.5.0+ use listenres like features 56 | nmsp.on('listener', (action, type) => { 57 | console.log(action, 'called with type', type); 58 | }); 59 | 60 | // access its properties 61 | console.log(await nmsp.test); 62 | 63 | // or its helpers 64 | console.log(await nmsp.sum(1, 2)); 65 | await nmsp.delayed(); 66 | 67 | // or create instances 68 | const instance = await new nmsp.Class('🍻'); 69 | // and invoke their methods 70 | console.log(await instance.sum(1, 2)); 71 | 72 | // - - - - - - - - - - - - - - - - - - - - - - 73 | 74 | // worker.js 75 | importScripts('https://unpkg.com/proxied-worker/server'); 76 | 77 | ProxiedWorker({ 78 | test: 'OK', 79 | sum(a, b) { 80 | return a + b; 81 | }, 82 | on(type, callback) { 83 | setTimeout(() => { 84 | callback('Event', type); 85 | }); 86 | }, 87 | async delayed() { 88 | console.log('context', this.test); 89 | postMessage({action: 'greetings'}); 90 | return await new Promise($ => setTimeout($, 500, Math.random())); 91 | }, 92 | Class: class { 93 | constructor(name) { 94 | this.name = name; 95 | } 96 | sum(a, b) { 97 | console.log(this.name, a, b); 98 | return a + b; 99 | } 100 | } 101 | }); 102 | ``` 103 | 104 | Alternatively, if the browser supports workers as module: 105 | 106 | ```js 107 | // client.js 108 | import ProxiedWorker from 'https://unpkg.com/proxied-worker/client'; 109 | const nmsp = ProxiedWorker('./worker.js', {type: 'module'}); 110 | 111 | // worker.js 112 | import ProxiedWorker from 'https://unpkg.com/proxied-worker/module'; 113 | ProxiedWorker({ 114 | // ... 115 | }); 116 | ``` 117 | 118 | 119 | ## As SharedWorker 120 | 121 | The `ProxiedWorker` signature is similar to a `Worker` one, plus an extra third argument that is the constructor to use. 122 | 123 | In order to have a `SharedWorker`, this code might be used: 124 | 125 | ```js 126 | // client.js 127 | import ProxiedWorker from 'https://unpkg.com/proxied-worker/client'; 128 | const nmsp = ProxiedWorker('./shared-worker.js', {type: 'module'}, SharedWorker); 129 | 130 | // shared-worker.js 131 | import ProxiedWorker from 'https://unpkg.com/proxied-worker/module'; 132 | ProxiedWorker({ 133 | // ... 134 | }); 135 | ``` 136 | 137 | 138 | ## As ServiceWorker 139 | 140 | Similarly to a `SharedWorker`, it is also possible to register and use a `ServiceWorker` to compute heavy tasks. 141 | 142 | ```js 143 | // client.js 144 | import ProxiedWorker from 'https://unpkg.com/proxied-worker/client'; 145 | const nmsp = ProxiedWorker('./service-worker.js', {scope: '/'}, ServiceWorker); 146 | 147 | // service-worker.js 148 | importScripts('https://unpkg.com/proxied-worker/server'); 149 | 150 | ProxiedWorker({ 151 | // ... 152 | }); 153 | ``` 154 | -------------------------------------------------------------------------------- /client.js: -------------------------------------------------------------------------------- 1 | /*! (c) Andrea Giammarchi - ISC */ 2 | const{navigator:e,ServiceWorker:t,SharedWorker:r,Worker:n}=globalThis,{isArray:s}=Array,{random:i}=Math,o=[],a=[],c=({data:{id:e,args:t}})=>{if(s(t)){const r=o.indexOf(e);-1r instanceof t?e.serviceWorker:r;let l=0;const u=(e,t,r,n=null,u=(e=>e))=>new Promise(((g,h)=>{const p=`proxied-worker:${t}:${l++}`,f=d(e);if(f.addEventListener("message",(function e({data:{id:t,result:r,error:n}}){t===p&&(f.removeEventListener("message",e),null!=n?h(new Error(n)):g(u(r)))})),s(n)){r.push(n);for(let e=0,{length:t}=n;enew Proxy(h.bind({id:e,list:t}),g),c=new FinalizationRegistry((e=>{l.then((t=>t.postMessage({id:`proxied-worker:${e}:-0`,list:[]})))})),l=new Promise((n=>{if(o===r){const{port:e}=new o(s,i);e.start(),n(e)}else o===t?e.serviceWorker.register(s,i).then((({installing:e,waiting:t,active:r})=>n(e||t||r))):n(new o(s,i))})),g={apply(e,t,r){const{id:n,list:s}=e();return l.then((e=>u(e,n,["apply"].concat(s),r)))},construct(e,t){const{id:r,list:n}=e();return l.then((e=>u(e,r,["new"].concat(n),t,(e=>{const t=a(e,[]);return c.register(t,e),t}))))},get(e,t){const{id:r,list:n}=e(),{length:s}=n;switch(t){case"then":return s?(e,t)=>l.then((s=>u(s,r,["get"].concat(n)).then(e,t))):void 0;case"addEventListener":case"removeEventListener":if(!s&&!r)return(...e)=>l.then((r=>{d(r)[t](...e)}))}return a(r,n.concat(t))}};return a("",[])}function h(){return this}export{g as default}; 3 | -------------------------------------------------------------------------------- /esm/client.js: -------------------------------------------------------------------------------- 1 | /*! (c) Andrea Giammarchi - ISC */ 2 | 3 | const {navigator, ServiceWorker, SharedWorker, Worker} = globalThis; 4 | const {isArray} = Array; 5 | const {random} = Math; 6 | 7 | const ids = []; 8 | const cbs = []; 9 | 10 | const callbacks = ({data: {id, args}}) => { 11 | if (isArray(args)) { 12 | const i = ids.indexOf(id); 13 | if (-1 < i) 14 | cbs[i](...args); 15 | } 16 | }; 17 | 18 | const worker = $ => $ instanceof ServiceWorker ? navigator.serviceWorker : $; 19 | 20 | let uid = 0; 21 | const post = ( 22 | port, instance, list, 23 | args = null, 24 | $ = o => o 25 | ) => new Promise((ok, err) => { 26 | const id = `proxied-worker:${instance}:${uid++}`; 27 | const target = worker(port); 28 | target.addEventListener('message', function message({ 29 | data: {id: wid, result, error} 30 | }) { 31 | if (wid === id) { 32 | target.removeEventListener('message', message); 33 | if (error != null) 34 | err(new Error(error)); 35 | else 36 | ok($(result)); 37 | } 38 | }); 39 | if (isArray(args)) { 40 | list.push(args); 41 | for (let i = 0, {length} = args; i < length; i++) { 42 | switch (typeof args[i]) { 43 | case 'string': 44 | args[i] = '$' + args[i]; 45 | break; 46 | case 'function': 47 | target.addEventListener('message', callbacks); 48 | let index = cbs.indexOf(args[i]); 49 | if (index < 0) { 50 | index = cbs.push(args[i]) - 1; 51 | ids[index] = `proxied-worker:cb:${uid++ + random()}`; 52 | } 53 | args[i] = ids[index]; 54 | break; 55 | } 56 | } 57 | } 58 | port.postMessage({id, list}); 59 | }); 60 | 61 | /** 62 | * Returns a proxied namespace that can await every property, method, 63 | * or create instances within the Worker. 64 | * @param {string} path the Worker file that exports the namespace. 65 | * @returns {Proxy} 66 | */ 67 | export default function ProxiedWorker( 68 | path, 69 | options = {type: 'classic'}, 70 | Kind = Worker 71 | ) { 72 | 73 | const create = (id, list) => new Proxy(Proxied.bind({id, list}), handler); 74 | 75 | const registry = new FinalizationRegistry(instance => { 76 | bus.then(port => port.postMessage({ 77 | id: `proxied-worker:${instance}:-0`, 78 | list: [] 79 | })); 80 | }); 81 | 82 | const bus = new Promise($ => { 83 | if (Kind === SharedWorker) { 84 | const {port} = new Kind(path, options); 85 | port.start(); 86 | $(port); 87 | } 88 | else if (Kind === ServiceWorker) 89 | navigator.serviceWorker.register(path, options).then( 90 | ({installing, waiting, active}) => $(installing || waiting || active) 91 | ); 92 | else 93 | $(new Kind(path, options)); 94 | }); 95 | 96 | const handler = { 97 | apply(target, _, args) { 98 | const {id, list} = target(); 99 | return bus.then(port => post( 100 | port, id, ['apply'].concat(list), args) 101 | ); 102 | }, 103 | construct(target, args) { 104 | const {id, list} = target(); 105 | return bus.then( 106 | port => post( 107 | port, 108 | id, 109 | ['new'].concat(list), 110 | args, 111 | result => { 112 | const proxy = create(result, []); 113 | registry.register(proxy, result); 114 | return proxy; 115 | } 116 | ) 117 | ); 118 | }, 119 | get(target, key) { 120 | const {id, list} = target(); 121 | const {length} = list; 122 | switch (key) { 123 | case 'then': 124 | return length ? 125 | (ok, err) => bus.then( 126 | port => post(port, id, ['get'].concat(list)).then(ok, err) 127 | ) : 128 | void 0; 129 | case 'addEventListener': 130 | case 'removeEventListener': 131 | if (!length && !id) 132 | return (...args) => bus.then(port => { 133 | worker(port)[key](...args); 134 | }); 135 | } 136 | return create(id, list.concat(key)); 137 | } 138 | }; 139 | 140 | return create('', []); 141 | }; 142 | 143 | function Proxied() { 144 | return this; 145 | } 146 | -------------------------------------------------------------------------------- /esm/server.js: -------------------------------------------------------------------------------- 1 | /*! (c) Andrea Giammarchi - ISC */ 2 | 3 | const APPLY = 'apply'; 4 | const GET = 'get'; 5 | const NEW = 'new'; 6 | 7 | let uid = 0; 8 | 9 | /** 10 | * Exports a namespace object with methods, properties, or classes. 11 | * @param {object} Namespace the exported namespace. 12 | */ 13 | globalThis.ProxiedWorker = function ProxiedWorker(Namespace) { 14 | const instances = new WeakMap; 15 | 16 | addEventListener('connect', ({ports = []}) => { 17 | for (const port of ports) { 18 | port.addEventListener('message', message.bind(port)); 19 | port.start(); 20 | } 21 | }); 22 | 23 | addEventListener('message', message.bind(globalThis)); 24 | 25 | async function loopThrough(_, $, list) { 26 | const action = list.shift(); 27 | let {length} = list; 28 | 29 | if (action !== GET) 30 | length--; 31 | if (action === APPLY) 32 | length--; 33 | 34 | for (let i = 0; i < length; i++) 35 | $ = await $[list[i]]; 36 | 37 | if (action === NEW) { 38 | const instance = new $(...list.pop().map(args, _)); 39 | instances.get(this).set($ = String(uid++), instance); 40 | } 41 | else if (action === APPLY) { 42 | $ = await $[list[length]](...list.pop().map(args, _)); 43 | } 44 | 45 | return $; 46 | } 47 | 48 | async function message(event) { 49 | const {source, data: {id, list}} = event; 50 | if (!/^proxied-worker:([^:]*?):-?\d+$/.test(id)) 51 | return; 52 | 53 | const instance = RegExp.$1; 54 | const bus = source || this; 55 | 56 | if (!instances.has(this)) 57 | instances.set(this, new Map); 58 | 59 | let result, error; 60 | if (instance.length) { 61 | const ref = instances.get(this); 62 | if (list.length) { 63 | try { 64 | result = await loopThrough.call(this, bus, ref.get(instance), list); 65 | } 66 | catch ({message}) { 67 | error = message; 68 | } 69 | } 70 | else { 71 | ref.delete(instance); 72 | return; 73 | } 74 | } 75 | else { 76 | try { 77 | result = await loopThrough.call(this, bus, Namespace, list); 78 | } 79 | catch ({message}) { 80 | error = message; 81 | } 82 | } 83 | 84 | bus.postMessage({id, result, error}); 85 | } 86 | }; 87 | 88 | const cbs = new Map; 89 | function args(id) { 90 | if (typeof id === 'string') { 91 | if (/^proxied-worker:cb:/.test(id)) { 92 | if (!cbs.has(id)) 93 | cbs.set(id, (...args) => { this.postMessage({id, args}); }); 94 | return cbs.get(id); 95 | } 96 | return id.slice(1); 97 | } 98 | return id; 99 | } 100 | -------------------------------------------------------------------------------- /module.js: -------------------------------------------------------------------------------- 1 | /*! (c) Andrea Giammarchi - ISC */ 2 | let t=0;export default function(e){const n=new WeakMap;async function a(e,a,i){const r=i.shift();let{length:o}=i;"get"!==r&&o--,"apply"===r&&o--;for(let t=0;t{for(const e of t)e.addEventListener("message",i.bind(e)),e.start()})),addEventListener("message",i.bind(globalThis))};const e=new Map;function s(t){return"string"==typeof t?/^proxied-worker:cb:/.test(t)?(e.has(t)||e.set(t,((...e)=>{this.postMessage({id:t,args:e})})),e.get(t)):t.slice(1):t} 3 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "proxied-worker", 3 | "version": "0.5.3", 4 | "description": "A tiny utility to asynchronously drive a namespace exposed through a Shared/Service/Worker", 5 | "scripts": { 6 | "build": "npm run rollup:client && npm run rollup:server && npm run rollup:module && npm run size", 7 | "rollup:client": "rollup --config rollup/client.config.js", 8 | "rollup:module": "cat server.js | sed 's/globalThis.ProxiedWorker=/export default /' > module.js", 9 | "rollup:server": "rollup --config rollup/server.config.js && sed -i.bck 's/^var /self./' server.js && rm -rf server.js.bck", 10 | "coveralls": "c8 report --reporter=text-lcov | coveralls", 11 | "size": "cat client.js | brotli | wc -c && cat server.js | brotli | wc -c", 12 | "test": "c8 node test/index.js", 13 | "start": "node server.js" 14 | }, 15 | "keywords": [ 16 | "service", 17 | "shared", 18 | "worker", 19 | "namespace", 20 | "proxy" 21 | ], 22 | "author": "Andrea Giammarchi", 23 | "license": "ISC", 24 | "devDependencies": { 25 | "@rollup/plugin-node-resolve": "^13.0.5", 26 | "ascjs": "^5.0.1", 27 | "c8": "^7.10.0", 28 | "coveralls": "^3.1.1", 29 | "rollup": "^2.58.0", 30 | "rollup-plugin-terser": "^7.0.2" 31 | }, 32 | "type": "module", 33 | "exports": { 34 | "./client": "./client.js", 35 | "./module": "./module.js", 36 | "./server": "./server.js", 37 | "./package.json": "./package.json" 38 | }, 39 | "repository": { 40 | "type": "git", 41 | "url": "git+https://github.com/WebReflection/proxied-worker.git" 42 | }, 43 | "bugs": { 44 | "url": "https://github.com/WebReflection/proxied-worker/issues" 45 | }, 46 | "homepage": "https://github.com/WebReflection/proxied-worker#readme" 47 | } 48 | -------------------------------------------------------------------------------- /rollup/client.config.js: -------------------------------------------------------------------------------- 1 | import {nodeResolve} from '@rollup/plugin-node-resolve'; 2 | import {terser} from 'rollup-plugin-terser'; 3 | 4 | export default { 5 | input: './esm/client.js', 6 | plugins: [ 7 | nodeResolve(), 8 | terser() 9 | ], 10 | output: { 11 | esModule: false, 12 | exports: 'named', 13 | file: './client.js', 14 | format: 'module', 15 | name: 'ProxiedWorker' 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /rollup/server.config.js: -------------------------------------------------------------------------------- 1 | import {nodeResolve} from '@rollup/plugin-node-resolve'; 2 | import {terser} from 'rollup-plugin-terser'; 3 | 4 | export default { 5 | input: './esm/server.js', 6 | plugins: [ 7 | nodeResolve(), 8 | terser() 9 | ], 10 | output: { 11 | esModule: false, 12 | exports: 'named', 13 | file: './server.js', 14 | format: 'module', 15 | name: 'ProxiedWorker' 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | /*! (c) Andrea Giammarchi - ISC */ 2 | let t=0;globalThis.ProxiedWorker=function(e){const n=new WeakMap;async function a(e,a,i){const r=i.shift();let{length:o}=i;"get"!==r&&o--,"apply"===r&&o--;for(let t=0;t{for(const e of t)e.addEventListener("message",i.bind(e)),e.start()})),addEventListener("message",i.bind(globalThis))};const e=new Map;function s(t){return"string"==typeof t?/^proxied-worker:cb:/.test(t)?(e.has(t)||e.set(t,((...e)=>{this.postMessage({id:t,args:e})})),e.get(t)):t.slice(1):t} 3 | -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | proxied-worker 7 | 51 | 52 | 53 |

Read in console.

54 |

55 | 56 |

57 | 58 | 59 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | import ProxiedWorker from '../client.js'; 2 | -------------------------------------------------------------------------------- /test/service.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | proxied-worker 7 | 51 | 52 | 53 |

Read in console.

54 |

55 | 56 |

57 | 58 | 59 | -------------------------------------------------------------------------------- /test/shared.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | proxied-worker 7 | 51 | 52 | 53 |

Read in console.

54 |

55 | 56 |

57 | 58 | 59 | -------------------------------------------------------------------------------- /test/worker.js: -------------------------------------------------------------------------------- 1 | importScripts('../esm/server.js') 2 | 3 | const isServiceWorker = 'clients' in self; 4 | const clients = []; 5 | 6 | // Example: ServiceWorker interaction 7 | if (isServiceWorker) 8 | addEventListener('activate', event => { 9 | event.waitUntil(clients.claim()); 10 | }); 11 | // SharedWorker fallback 12 | else 13 | addEventListener('connect', ({source}) => { 14 | clients.push(source); 15 | }); 16 | 17 | ProxiedWorker({ 18 | test: 'OK', 19 | sum(a, b) { 20 | return a + b; 21 | }, 22 | on(type, cb) { 23 | setTimeout(cb, 500, type); 24 | }, 25 | notify() { 26 | setTimeout( 27 | async () => { 28 | console.log('notifying'); 29 | const data = {id: 'notify', args: [1, 2, 3]}; 30 | // ServiceWorker claimed clients 31 | if (isServiceWorker) { 32 | for (const client of await self.clients.matchAll({type: 'all'})) 33 | client.postMessage(data); 34 | } 35 | // SharedWorker claimed clients 36 | else if (clients.length) { 37 | for (const client of clients) 38 | client.postMessage(data); 39 | } 40 | // Worker fallback 41 | else 42 | postMessage(data); 43 | }, 44 | 1000 45 | ); 46 | }, 47 | async delayed() { 48 | console.log('context', this.test); 49 | return await new Promise($ => setTimeout($, 500, Math.random())); 50 | }, 51 | Class: class { 52 | constructor(name) { 53 | this.name = name; 54 | } 55 | sum(a, b) { 56 | console.log(this.name, a, b); 57 | return a + b; 58 | } 59 | } 60 | }); 61 | --------------------------------------------------------------------------------