├── .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 |
--------------------------------------------------------------------------------