├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── cjs ├── index.js └── remoted.js ├── esm ├── index.js └── remoted.js ├── examples ├── workway-node.zip ├── workway-node │ ├── README.md │ ├── index.js │ ├── package.json │ ├── workers │ │ └── os.js │ └── www │ │ ├── index.html │ │ └── js │ │ ├── 3rd │ │ ├── hyperhtml.js │ │ └── workway.js │ │ ├── index.js │ │ └── poly │ │ ├── es6-promise.js │ │ ├── event-targe.js │ │ └── poorlyfills.js ├── workway-promise-reject.zip └── workway-promise-reject │ ├── README.md │ ├── index.html │ ├── index.js │ ├── package.json │ └── workers │ └── index.js ├── index.js ├── min.js ├── node ├── client.js └── index.js ├── package.json ├── partial └── worker.js ├── remoted.js ├── test ├── background.js ├── circular │ ├── analyzer.js │ └── index.html ├── client.js ├── foo.html ├── foo.js ├── index.html ├── index.js ├── node.html ├── node.js ├── webworker.js └── workers │ ├── foo.js │ ├── other.js │ └── test.js └── worker.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | package-lock.json 4 | ^worker.js 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules/* 2 | examples/* 3 | partial/* 4 | test/* 5 | .DS_Store 6 | .gitignore 7 | .travis.yml 8 | package-lock.json 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - stable 4 | git: 5 | depth: 1 6 | branches: 7 | only: 8 | - master 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (c) 2018, 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 | # workway DEPRECATED - See [coincident](https://github.com/WebReflection/coincident#coincidentserver) 2 | 3 | A general purpose, Web Worker driven, remote namespace with classes and methods. 4 | 5 | 6 | - - - 7 | 8 | ## Announcement: Meet proxied-worker & proxied-node 9 | 10 | There is a new, very similar, yet different, project, in case you're looking to simply drive generic Workers instances, or namespaces, from a client/main thread: [proxied-worker](https://github.com/WebReflection/proxied-worker#readme). 11 | 12 | It has [a NodeJS counterpart module](https://github.com/WebReflection/proxied-node#readme) too! 13 | 14 | The main difference with these projects is: 15 | 16 | * classes have a working constructor 17 | * heap is automatically cleaned on both client/server 18 | * it uses *Proxy* and *FinalizationRegistry* so these are not as compatible as *workway* is with legacy browsers 19 | 20 | - - - 21 | 22 | 23 | ## Key Features 24 | 25 | * no eval at all, no scope issues, 100% CSP friendly 26 | * no Proxy at all neither, compatible with IE 10, iOS 8, Android 4.4, BB OS 10, and [every other browser](https://webreflection.github.io/workway/test/) 27 | * 100LOC client squeezed in about 0.5K once compressed 28 | * you expose non blocking namespaces to the main thread, not the other way around 29 | * it **works on NodeJS** too 🎉 30 | 31 | 32 | 33 | ## Example (client side only) 34 | A basic **firebase.js** client to show the user name. 35 | ```js 36 | workway('/workers/firebase.js').then( 37 | async function ({worker, namespace}) { 38 | await namespace.initializeApp({ 39 | apiKey: "", 40 | authDomain: ".firebaseapp.com", 41 | databaseURL: "https://.firebaseio.com", 42 | projectId: "", 43 | storageBucket: ".appspot.com", 44 | messagingSenderId: "" 45 | }); 46 | const fb = new namespace.FirebaseUser(); 47 | const name = await fb.name(); 48 | console.log(name); // will log the user name, if any 49 | 50 | // the worker can be regularly used like any other worker 51 | worker.postMessage('all good'); 52 | } 53 | ); 54 | 55 | // you can also pass in an existing Worker instance (useful if you're using 56 | // Webpack's worker-loader and don't have access to the output file path): 57 | import Worker from 'worker-loader!./Worker.js'; 58 | workway(new Worker()).then(... 59 | ``` 60 | 61 | The **workers/firebase.js** worker that exposes some info. 62 | ```js 63 | // top import to ensure a transparent communication channel 64 | importScripts('https://unpkg.com/workway/worker.js'); 65 | 66 | // any other needed import for this worker 67 | importScripts(...[ 68 | 'app', 'auth', 'database', 'firestore', 'messaging', 'functions' 69 | ].map(name => `https://www.gstatic.com/firebasejs/5.0.1/firebase-${name}.js`)); 70 | 71 | // expose a namespaces as an object 72 | // with any sort of serializable value 73 | // and also methods or classes 74 | workway({ 75 | 76 | // any serializable data is OK (nested too) 77 | timestamp: Date.now(), 78 | 79 | // methods are OK too, each method 80 | // accepts serializable arguments and 81 | // can return a value and/or a promise 82 | initializeApp(config) { 83 | firebase.initializeApp(config); 84 | }, 85 | 86 | // classes are also fine, as long as 87 | // these respect RemoteClass conventions 88 | FirebaseUser: class FirebaseUser { 89 | constructor() { 90 | this.uid = firebase.auth().currentUser.uid; 91 | } 92 | name() { 93 | return firebase.database() 94 | .ref('/users/' + this.uid) 95 | .once('value') 96 | .then(snapshot => (( 97 | snapshot.val() && snapshot.val().username 98 | ) || 'Anonymous')); 99 | } 100 | } 101 | }); 102 | 103 | // this worker can be regularly used like any other worker 104 | // the passed event will never be one handled by `workway` 105 | self.onmessage = event => { 106 | console.log(event.data); 107 | }; 108 | ``` 109 | 110 | ## Example (NodeJS) 111 | 112 | To have NodeJS driven workers you need the regular client side `workway.js` file, plus `/pocket.io/pocket.io.js` and `/workway@node.js` that are both handled by this module. 113 | 114 | ```html 115 | 116 | 117 | 118 | ``` 119 | 120 | This is a `js/os.js` file for the client side. 121 | ```js 122 | workway('node://os.js').then(({worker, namespace:os}) => { 123 | os.getNetworkInterfaces().then(console.log); 124 | }); 125 | ``` 126 | 127 | Please note the client file needs EventTarget, Promise, and WeakMap constructors. 128 | If your target browsers don't have these features, you can use the following polyfills on top of your HTML file. 129 | 130 | ```html 131 | 136 | ``` 137 | 138 | 139 | Following a `workers/os.js` file to serve via NodeJS. 140 | ```js 141 | // note: you require a facade here via 'workway' 142 | var workway = require('workway'); 143 | workway(require('os')); 144 | ``` 145 | 146 | An express / node based bootstrap. 147 | ```js 148 | var express = require('express'); 149 | 150 | // note: you require the real module as 'workway/node' 151 | var workway = require('workway/node'); 152 | // authorize / expose a specific folder 153 | // that contains web driven workers 154 | workway.authorize(__dirname + '/workers'); 155 | 156 | var app = workway.app(express()); 157 | app.use(express.static(__dirname + '/www')); 158 | app.listen(8080); 159 | ``` 160 | 161 | ### NodeJS extra features & gotchas 162 | 163 | * exact same API (actually exact same code) of the real Web Worker based client side 164 | * circular objects are supported out of the box via [flatted](https://github.com/WebReflection/flatted#flatted) on both ways 165 | * the `self` global (but sandboxed) variable points at the global 166 | * the `self.workway` method is already there, feel free to use it instead of requiring it from workers 167 | * the `self.addEventListener` and `self.remoteEventListener` are normalized to work like on the front end side: do not use emitter methods directly with your node workers or messages and errors might not be signaled as expected 168 | 169 | 170 | ## The RemoteClass convention 171 | 172 | Classes exposed through `workway` namespace must follow these rules: 173 | 174 | * no constructor arguments; use methods to eventually forward extra details from the client 175 | * methods can accept only serializable arguments and can return either a serializable value or a promise that will resolve as serializable data 176 | * properties set on the **client** side must be serializable and will be **reflected** into related worker instances whenever methods are invoked 177 | * properties set in the **worker** will **not** be **reflected** on the client side so that what's defined in the worker, stays in the worker 178 | * every method invocation returns a Promise, even if the method returned value is not 179 | * multiple methods invocation at once are possible, but there is no guarantee of the order. Use promises features or `await` each call if sequential methods calls depend on previous results. 180 | 181 | 182 | 183 | ## Compatibility 184 | 185 | The code is written in a ES5 friendly syntax, and it's guaranteed to work in IE 10 or above, and mostly every mobile browser on platforms such iOS 8+, Android 4.4+, Blackberry OS 10+, or Windows Phone 8+. 186 | 187 | You can test live your browser through the **[live test page](https://webreflection.github.io/workway/test/index.html)**. 188 | 189 | Please note in IE 10/11 or other old browser cases, you might need to provide polyfills on both client and worker side. 190 | 191 | Feel free to choose the polyfill you prefer. 192 | 193 | Following just as example: 194 | 195 | ```html 196 | 197 | 203 | 204 | 205 | ``` 206 | 207 | Or on top of your generic worker. 208 | ```js 209 | // import the polyfill you prefer for either IE11 or IE10 210 | if (!self.Promise) importScripts('https://cdn.jsdelivr.net/npm/es6-promise@4/dist/es6-promise.auto.min.js'); 211 | if (!self.WeakMap) importScripts('https://unpkg.com/poorlyfills@0.1.1/min.js'); 212 | 213 | // now import workway/worker.js before any other worker script 214 | importScripts('https://unpkg.com/workway/worker.js'); 215 | 216 | // ... the rest of the code ... 217 | ``` 218 | 219 | 220 | 221 | ## About Recursive data 222 | 223 | If you need to invoke a method passing an object that might contain recursive data you can serialize it upfront and parse it once received. 224 | 225 | ```js 226 | // main thread app.js side 227 | import workway from 'https://unpkg.com/workway/esm'; 228 | import {stringify} from 'https://unpkg.com/flatted/esm'; 229 | 230 | workway('analyzer.js').then(({namespace}) => { 231 | const data = {arr: []}; 232 | data.arr.push(data); 233 | data.data = data; 234 | namespace.analyze(stringify(data)) 235 | .then( 236 | state => document.body.textContent = state, 237 | console.error 238 | ); 239 | }); 240 | 241 | 242 | 243 | // worker side: analyzer.js 244 | importScripts( 245 | 'https://unpkg.com/workway/worker.js', 246 | 'https://unpkg.com/flatted' 247 | ); 248 | 249 | workway({ 250 | analyze(circular) { 251 | const data = Flatted.parse(circular); 252 | return 'OK'; 253 | } 254 | }); 255 | ``` 256 | 257 | You can [test above example right here](https://webreflection.github.io/workway/test/circular/). 258 | 259 | 260 | 261 | ### Extra Info 262 | 263 | * the client side of this package fits in just 100 LOC 264 | * the client side of this project weights 0.5K via brotli, 0.6K via gzip 265 | * the client side source of truth of this project is its root `./index.js` 266 | * the only worker related code is in `./worker.js` root file 267 | * the ESM version of this module is in `esm/index.js` 268 | * the CJS version of this module is in `cjs/index.js` 269 | * the browser version of this module is in `min.js` 270 | -------------------------------------------------------------------------------- /cjs/index.js: -------------------------------------------------------------------------------- 1 | function workway(file) {'use strict'; 2 | /*! (c) 2018 Andrea Giammarchi (ISC) */ 3 | return new Promise(function (res) { 4 | function uid() { return ++i + Math.random(); } 5 | var i = 0; 6 | var channel = uid(); 7 | var messages = {}; 8 | var worker = typeof file === 'string' ? new Worker(file) : file; 9 | worker.addEventListener('message', function (event) { 10 | if (event.data.channel !== channel) return; 11 | event.stopImmediatePropagation(); 12 | var namespace = event.data.namespace; 13 | if (namespace) { 14 | var Class = function (info) { 15 | var path = info.path; 16 | var methods = info.methods; 17 | var statics = info.statics; 18 | var wm = new WeakMap; 19 | function RemoteClass() { wm.set(this, uid()); } 20 | methods.forEach(function (method) { 21 | RemoteClass.prototype[method] = function () { 22 | return send({ 23 | args: slice.call(arguments), 24 | path: path, 25 | method: method, 26 | object: { 27 | id: wm.get(this), 28 | value: this 29 | } 30 | }); 31 | }; 32 | }); 33 | statics.methods.forEach(function (method) { 34 | RemoteClass[method] = function () { 35 | return send({ 36 | args: slice.call(arguments), 37 | path: path, 38 | method: method 39 | }); 40 | }; 41 | }); 42 | statics.values.forEach(function (pair) { 43 | RemoteClass[pair[0]] = pair[1]; 44 | }); 45 | return RemoteClass; 46 | }; 47 | var callback = function (path) { 48 | return function remoteCallback() { 49 | return send({ 50 | args: slice.call(arguments), 51 | path: path 52 | }); 53 | }; 54 | }; 55 | var send = function (message) { 56 | var resolve, reject; 57 | var promise = new Promise(function (res, rej) { 58 | resolve = res; 59 | reject = rej; 60 | }); 61 | promise.resolve = resolve; 62 | promise.reject = reject; 63 | messages[message.id = uid()] = promise; 64 | worker.postMessage({ 65 | channel: channel, 66 | message: message 67 | }); 68 | return promise; 69 | }; 70 | var slice = [].slice; 71 | (function update(namespace) { 72 | Object.keys(namespace).forEach(function (key) { 73 | var info = namespace[key]; 74 | switch (info.type) { 75 | case 'class': namespace[key] = Class(info); break; 76 | case 'function': namespace[key] = callback(info.path); break; 77 | case 'object': update(namespace[key] = info.value); break; 78 | default: namespace[key] = info.value; 79 | } 80 | }); 81 | }(namespace)); 82 | res({ 83 | worker: worker, 84 | namespace: namespace 85 | }); 86 | } else { 87 | var message = event.data.message; 88 | var id = message.id; 89 | var promise = messages[id]; 90 | delete messages[id]; 91 | if (message.hasOwnProperty('error')) { 92 | var error, facade = message.error; 93 | if (facade.hasOwnProperty('source')) 94 | error = facade.source; 95 | else { 96 | error = new Error(facade.message); 97 | error.stack = facade.stack; 98 | } 99 | promise.reject(error); 100 | } 101 | else 102 | promise.resolve(message.result); 103 | } 104 | }); 105 | worker.postMessage({channel: channel}); 106 | }); 107 | } 108 | module.exports = workway; 109 | -------------------------------------------------------------------------------- /cjs/remoted.js: -------------------------------------------------------------------------------- 1 | function remoted(object) { 2 | return function $(object, current, remote) { 3 | Object.keys(object).forEach(function (key) { 4 | var value = object[key]; 5 | var path = current.concat(key); 6 | if (typeof value === 'function') { 7 | remote[key] = /^[A-Z]/.test(key) ? 8 | { 9 | type: 'class', 10 | path: path, 11 | methods: Object.getOwnPropertyNames(value.prototype) 12 | .filter(no(['constructor'])) 13 | .concat('destroy'), 14 | statics: Object.getOwnPropertyNames(value) 15 | .filter(no([ 16 | 'arguments', 'callee', 'caller', 17 | 'length', 'name', 'prototype' 18 | ])) 19 | .reduce( 20 | function (info, key) { 21 | if (typeof value[key] === 'function') { 22 | info.methods.push(key); 23 | } else { 24 | info.values.push([key, value[key]]); 25 | } 26 | return info; 27 | }, 28 | { 29 | methods: [], 30 | values: [] 31 | } 32 | ) 33 | } : 34 | { 35 | type: 'function', 36 | path: path 37 | }; 38 | } else if (remote.toString.call(value) === '[object Object]') { 39 | remote[key] = { 40 | type: 'object', 41 | path: path, 42 | value: {} 43 | }; 44 | $(value, path, remote[key].value); 45 | } else if (value !== void 0) { 46 | remote[key] = { 47 | type: 'any', 48 | path: path, 49 | value: value 50 | }; 51 | } 52 | }); 53 | return remote; 54 | }(object, [], {}); 55 | function no(within) { 56 | return function (what) { 57 | return within.indexOf(what) < 0; 58 | }; 59 | } 60 | } 61 | module.exports = remoted; 62 | -------------------------------------------------------------------------------- /esm/index.js: -------------------------------------------------------------------------------- 1 | function workway(file) {'use strict'; 2 | /*! (c) 2018 Andrea Giammarchi (ISC) */ 3 | return new Promise(function (res) { 4 | function uid() { return ++i + Math.random(); } 5 | var i = 0; 6 | var channel = uid(); 7 | var messages = {}; 8 | var worker = typeof file === 'string' ? new Worker(file) : file; 9 | worker.addEventListener('message', function (event) { 10 | if (event.data.channel !== channel) return; 11 | event.stopImmediatePropagation(); 12 | var namespace = event.data.namespace; 13 | if (namespace) { 14 | var Class = function (info) { 15 | var path = info.path; 16 | var methods = info.methods; 17 | var statics = info.statics; 18 | var wm = new WeakMap; 19 | function RemoteClass() { wm.set(this, uid()); } 20 | methods.forEach(function (method) { 21 | RemoteClass.prototype[method] = function () { 22 | return send({ 23 | args: slice.call(arguments), 24 | path: path, 25 | method: method, 26 | object: { 27 | id: wm.get(this), 28 | value: this 29 | } 30 | }); 31 | }; 32 | }); 33 | statics.methods.forEach(function (method) { 34 | RemoteClass[method] = function () { 35 | return send({ 36 | args: slice.call(arguments), 37 | path: path, 38 | method: method 39 | }); 40 | }; 41 | }); 42 | statics.values.forEach(function (pair) { 43 | RemoteClass[pair[0]] = pair[1]; 44 | }); 45 | return RemoteClass; 46 | }; 47 | var callback = function (path) { 48 | return function remoteCallback() { 49 | return send({ 50 | args: slice.call(arguments), 51 | path: path 52 | }); 53 | }; 54 | }; 55 | var send = function (message) { 56 | var resolve, reject; 57 | var promise = new Promise(function (res, rej) { 58 | resolve = res; 59 | reject = rej; 60 | }); 61 | promise.resolve = resolve; 62 | promise.reject = reject; 63 | messages[message.id = uid()] = promise; 64 | worker.postMessage({ 65 | channel: channel, 66 | message: message 67 | }); 68 | return promise; 69 | }; 70 | var slice = [].slice; 71 | (function update(namespace) { 72 | Object.keys(namespace).forEach(function (key) { 73 | var info = namespace[key]; 74 | switch (info.type) { 75 | case 'class': namespace[key] = Class(info); break; 76 | case 'function': namespace[key] = callback(info.path); break; 77 | case 'object': update(namespace[key] = info.value); break; 78 | default: namespace[key] = info.value; 79 | } 80 | }); 81 | }(namespace)); 82 | res({ 83 | worker: worker, 84 | namespace: namespace 85 | }); 86 | } else { 87 | var message = event.data.message; 88 | var id = message.id; 89 | var promise = messages[id]; 90 | delete messages[id]; 91 | if (message.hasOwnProperty('error')) { 92 | var error, facade = message.error; 93 | if (facade.hasOwnProperty('source')) 94 | error = facade.source; 95 | else { 96 | error = new Error(facade.message); 97 | error.stack = facade.stack; 98 | } 99 | promise.reject(error); 100 | } 101 | else 102 | promise.resolve(message.result); 103 | } 104 | }); 105 | worker.postMessage({channel: channel}); 106 | }); 107 | } 108 | export default workway; 109 | -------------------------------------------------------------------------------- /esm/remoted.js: -------------------------------------------------------------------------------- 1 | function remoted(object) { 2 | return function $(object, current, remote) { 3 | Object.keys(object).forEach(function (key) { 4 | var value = object[key]; 5 | var path = current.concat(key); 6 | if (typeof value === 'function') { 7 | remote[key] = /^[A-Z]/.test(key) ? 8 | { 9 | type: 'class', 10 | path: path, 11 | methods: Object.getOwnPropertyNames(value.prototype) 12 | .filter(no(['constructor'])) 13 | .concat('destroy'), 14 | statics: Object.getOwnPropertyNames(value) 15 | .filter(no([ 16 | 'arguments', 'callee', 'caller', 17 | 'length', 'name', 'prototype' 18 | ])) 19 | .reduce( 20 | function (info, key) { 21 | if (typeof value[key] === 'function') { 22 | info.methods.push(key); 23 | } else { 24 | info.values.push([key, value[key]]); 25 | } 26 | return info; 27 | }, 28 | { 29 | methods: [], 30 | values: [] 31 | } 32 | ) 33 | } : 34 | { 35 | type: 'function', 36 | path: path 37 | }; 38 | } else if (remote.toString.call(value) === '[object Object]') { 39 | remote[key] = { 40 | type: 'object', 41 | path: path, 42 | value: {} 43 | }; 44 | $(value, path, remote[key].value); 45 | } else if (value !== void 0) { 46 | remote[key] = { 47 | type: 'any', 48 | path: path, 49 | value: value 50 | }; 51 | } 52 | }); 53 | return remote; 54 | }(object, [], {}); 55 | function no(within) { 56 | return function (what) { 57 | return within.indexOf(what) < 0; 58 | }; 59 | } 60 | } 61 | export default remoted; 62 | -------------------------------------------------------------------------------- /examples/workway-node.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WebReflection/workway/0b7ce770619955733d668e3b2810c707d26d0fa5/examples/workway-node.zip -------------------------------------------------------------------------------- /examples/workway-node/README.md: -------------------------------------------------------------------------------- 1 | # Node.js workway demo 2 | 3 | This demo shows CPUs and current memory consumption. 4 | 5 | ```sh 6 | npm install 7 | # now open that localhost page 8 | ``` 9 | -------------------------------------------------------------------------------- /examples/workway-node/index.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const express = require('express'); 3 | const workway = require('workway/node'); 4 | 5 | // which folder should be reachable from the Web ? 6 | workway.authorize(path.join(__dirname, 'workers')); 7 | 8 | // create an app through workway 9 | const app = workway.app(express()); 10 | app.use(express.static(path.join(__dirname, 'www'))); 11 | app.listen(process.env.PORT || 8080, function () { 12 | console.log(`http://localhost:${this.address().port}/`); 13 | }); 14 | -------------------------------------------------------------------------------- /examples/workway-node/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "description": "A basic workway driven NodeJS example", 4 | "scripts": { 5 | "postinstall": "npm start", 6 | "start": "node index.js" 7 | }, 8 | "author": "Andrea Giammarchi", 9 | "license": "ISC", 10 | "dependencies": { 11 | "express": "^4.16.3", 12 | "workway": "^0.5.2" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /examples/workway-node/workers/os.js: -------------------------------------------------------------------------------- 1 | const workway = require('workway'); 2 | 3 | // export any namespace or even modules 4 | workway(require('os')); 5 | -------------------------------------------------------------------------------- /examples/workway-node/www/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | workway meets NodeJS 5 | 6 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /examples/workway-node/www/js/3rd/hyperhtml.js: -------------------------------------------------------------------------------- 1 | /*! (c) Andrea Giammarchi (ISC) */var hyperHTML=function(e){"use strict";var t=document.defaultView,r=/^area|base|br|col|embed|hr|img|input|keygen|link|menuitem|meta|param|source|track|wbr$/i,l="ownerSVGElement",c="http://www.w3.org/2000/svg",s="connected",f="dis"+s,d=/^style|textarea$/i,b="_hyper: "+(Math.random()*new Date|0)+";",h="\x3c!--"+b+"--\x3e",v=t.Event;try{new v("Event")}catch(e){v=function(e){var t=document.createEvent("Event");return t.initEvent(e,!1,!1),t}}var n,i=t.Map||function(){var n=[],r=[];return{get:function(e){return r[n.indexOf(e)]},set:function(e,t){r[n.push(e)-1]=t}}},o=0,p=t.WeakMap||function(){var n=b+o++;return{get:function(e){return e[n]},set:function(e,t){Object.defineProperty(e,n,{configurable:!0,value:t})}}},a=t.WeakSet||function(){var t=new p;return{add:function(e){t.set(e,!0)},has:function(e){return!0===t.get(e)}}},u=Array.isArray||(n={}.toString,function(e){return"[object Array]"===n.call(e)}),m=b.trim||function(){return this.replace(/^\s+|\s+$/g,"")};function g(){return this}var w=function(e,t){var n="_"+e+"$";return{get:function(){return this[n]||(this[e]=t.call(this,e))},set:function(e){Object.defineProperty(this,n,{configurable:!0,value:e})}}},y={},N=[],x=y.hasOwnProperty,E=0,C=function(e,t){e in y||(E=N.push(e)),y[e]=t},j=function(e,t){for(var n=0;n\"'=]+",M="[ "+O+"]+"+k,D="<([A-Za-z]+[A-Za-z0-9:_-]*)((?:",$="(?:=(?:'[^']*?'|\"[^\"]*?\"|<[^>]*?>|"+k+"))?)",P=new RegExp(D+M+$+"+)([ "+O+"]*/?>)","g"),B=new RegExp(D+M+$+"*)([ "+O+"]*/>)","g"),R=T(document),H="append"in R,_="content"in A(document,"template");R.appendChild(L(R,"g")),R.appendChild(L(R,""));var z=1===R.cloneNode(!0).childNodes.length,F="importNode"in document,Z=H?function(e,t){e.append.apply(e,t)}:function(e,t){for(var n=t.length,r=0;r"+t+"",Z(r,K.call(n.querySelectorAll(i)))}else n.innerHTML=t,Z(r,K.call(n.childNodes));return r},Y=_?function(e,t){var n=T(e),r=S(e).createElementNS(c,"svg");return r.innerHTML=t,Z(n,K.call(r.childNodes)),n}:function(e,t){var n=T(e),r=A(e,"div");return r.innerHTML=''+t+"",Z(n,K.call(r.firstChild.childNodes)),n};function ee(e){this.childNodes=e,this.length=e.length,this.first=e[0],this.last=e[this.length-1]}ee.prototype.insert=function(){var e=T(this.first);return Z(e,this.childNodes),e},ee.prototype.remove=function(){var e=this.first,t=this.last;if(2===this.length)t.parentNode.removeChild(t);else{var n=S(e).createRange();n.setStartBefore(this.childNodes[1]),n.setEndAfter(t),n.deleteContents()}return e};var te=function(e,t,n){e.unshift(e.indexOf.call(t.childNodes,n))},ne=function(e,t,n){return{type:e,name:n,node:t,path:function(e){var t=[],n=void 0;switch(e.nodeType){case 1:case 11:n=e;break;case 8:n=e.parentNode,te(t,n,e);break;default:n=e.ownerElement}for(e=n;n=n.parentNode;e=n)te(t,n,e);return t}(t)}},re=function(e,t){for(var n=t.length,r=0;r"},$e=new p,Pe=function(n){var r=void 0,i=void 0,o=void 0,a=void 0,u=void 0;return function(e){e=Q(e);var t=a!==e;return t&&(a=e,o=T(document),i="svg"===n?document.createElementNS(c,"svg"):o,u=Ae.bind(i)),u.apply(null,arguments),t&&("svg"===n&&Z(o,K.call(i.childNodes)),r=Re(o)),r}},Be=function(e,t){var n=t.indexOf(":"),r=$e.get(e),i=t;return-1 { 2 | // cpus are not going to change (not the showed part) 3 | const cpus = await os.cpus(); 4 | // grab other info and render the view 5 | grabAndShow(); 6 | function grabAndShow() { 7 | Promise.all([ 8 | os.freemem(), 9 | os.totalmem() 10 | ]).then(render); 11 | } 12 | // show all the details 13 | function render([freemem, totalmem]) { 14 | hyperHTML(document.body)` 15 | Summary of ${cpus.length} 16 | CPU${cpus.length === 1 ? '' : 's'} 17 |
    18 | ${cpus.map(cpu => hyperHTML(cpu)` 19 |
  • ${cpu.model}
  • `)} 20 |
21 |
22 |

Using ${ 23 | ((100 * (totalmem - freemem)) / totalmem).toFixed(2) 24 | }% of memory

`; 25 | // update the same view in a second 26 | setTimeout(grabAndShow, 1000); 27 | } 28 | }); -------------------------------------------------------------------------------- /examples/workway-node/www/js/poly/es6-promise.js: -------------------------------------------------------------------------------- 1 | !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):t.ES6Promise=e()}(this,function(){"use strict";function t(t){var e=typeof t;return null!==t&&("object"===e||"function"===e)}function e(t){return"function"==typeof t}function n(t){B=t}function r(t){G=t}function o(){return function(){return process.nextTick(a)}}function i(){return"undefined"!=typeof z?function(){z(a)}:c()}function s(){var t=0,e=new J(a),n=document.createTextNode("");return e.observe(n,{characterData:!0}),function(){n.data=t=++t%2}}function u(){var t=new MessageChannel;return t.port1.onmessage=a,function(){return t.port2.postMessage(0)}}function c(){var t=setTimeout;return function(){return t(a,1)}}function a(){for(var t=0;t 2 | 3 | 8 | 9 | 10 | 11 | 18 | -------------------------------------------------------------------------------- /examples/workway-promise-reject/index.js: -------------------------------------------------------------------------------- 1 | var PORT = process.env.PORT || 3000; 2 | 3 | var path = require('path'); 4 | var express = require('express'); 5 | var workway = require('workway/node').authorize( 6 | path.resolve(__dirname, 'workers') 7 | ); 8 | 9 | var app = workway.app(express()); 10 | app.get('/', function (req, res) { 11 | res.writeHead(200, 'OK', { 12 | 'Content-Type': 'text/html; charset=utf-8' 13 | }); 14 | res.end(require('fs').readFileSync(path.resolve(__dirname, 'index.html'))); 15 | }); 16 | app.listen(PORT, () => { 17 | console.log('listening on http://localhost:' + PORT); 18 | }); 19 | -------------------------------------------------------------------------------- /examples/workway-promise-reject/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "workway-demo", 3 | "private": true, 4 | "dependencies": { 5 | "express": "^4.16.3", 6 | "workway": "^0.5.2" 7 | }, 8 | "scripts": { 9 | "start": "node index.js", 10 | "postinstall": "npm start" 11 | }, 12 | "author": "Andrea Giammarchi", 13 | "license": "ISC" 14 | } 15 | -------------------------------------------------------------------------------- /examples/workway-promise-reject/workers/index.js: -------------------------------------------------------------------------------- 1 | const util = require('util'); 2 | 3 | const workway = require('workway'); 4 | 5 | function foo(message, callback) { 6 | callback(42); // forcing always an error 7 | } 8 | 9 | const fooPromisified = util.promisify(foo); 10 | 11 | workway({ 12 | fooPromisified 13 | }); 14 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | function workway(file) {'use strict'; 2 | /*! (c) 2018 Andrea Giammarchi (ISC) */ 3 | return new Promise(function (res) { 4 | function uid() { return ++i + Math.random(); } 5 | var i = 0; 6 | var channel = uid(); 7 | var messages = {}; 8 | var worker = typeof file === 'string' ? new Worker(file) : file; 9 | worker.addEventListener('message', function (event) { 10 | if (event.data.channel !== channel) return; 11 | event.stopImmediatePropagation(); 12 | var namespace = event.data.namespace; 13 | if (namespace) { 14 | var Class = function (info) { 15 | var path = info.path; 16 | var methods = info.methods; 17 | var statics = info.statics; 18 | var wm = new WeakMap; 19 | function RemoteClass() { wm.set(this, uid()); } 20 | methods.forEach(function (method) { 21 | RemoteClass.prototype[method] = function () { 22 | return send({ 23 | args: slice.call(arguments), 24 | path: path, 25 | method: method, 26 | object: { 27 | id: wm.get(this), 28 | value: this 29 | } 30 | }); 31 | }; 32 | }); 33 | statics.methods.forEach(function (method) { 34 | RemoteClass[method] = function () { 35 | return send({ 36 | args: slice.call(arguments), 37 | path: path, 38 | method: method 39 | }); 40 | }; 41 | }); 42 | statics.values.forEach(function (pair) { 43 | RemoteClass[pair[0]] = pair[1]; 44 | }); 45 | return RemoteClass; 46 | }; 47 | var callback = function (path) { 48 | return function remoteCallback() { 49 | return send({ 50 | args: slice.call(arguments), 51 | path: path 52 | }); 53 | }; 54 | }; 55 | var send = function (message) { 56 | var resolve, reject; 57 | var promise = new Promise(function (res, rej) { 58 | resolve = res; 59 | reject = rej; 60 | }); 61 | promise.resolve = resolve; 62 | promise.reject = reject; 63 | messages[message.id = uid()] = promise; 64 | worker.postMessage({ 65 | channel: channel, 66 | message: message 67 | }); 68 | return promise; 69 | }; 70 | var slice = [].slice; 71 | (function update(namespace) { 72 | Object.keys(namespace).forEach(function (key) { 73 | var info = namespace[key]; 74 | switch (info.type) { 75 | case 'class': namespace[key] = Class(info); break; 76 | case 'function': namespace[key] = callback(info.path); break; 77 | case 'object': update(namespace[key] = info.value); break; 78 | default: namespace[key] = info.value; 79 | } 80 | }); 81 | }(namespace)); 82 | res({ 83 | worker: worker, 84 | namespace: namespace 85 | }); 86 | } else { 87 | var message = event.data.message; 88 | var id = message.id; 89 | var promise = messages[id]; 90 | delete messages[id]; 91 | if (message.hasOwnProperty('error')) { 92 | var error, facade = message.error; 93 | if (facade.hasOwnProperty('source')) 94 | error = facade.source; 95 | else { 96 | error = new Error(facade.message); 97 | error.stack = facade.stack; 98 | } 99 | promise.reject(error); 100 | } 101 | else 102 | promise.resolve(message.result); 103 | } 104 | }); 105 | worker.postMessage({channel: channel}); 106 | }); 107 | } 108 | -------------------------------------------------------------------------------- /min.js: -------------------------------------------------------------------------------- 1 | function workway(t){"use strict"; 2 | /*! (c) 2018 Andrea Giammarchi (ISC) */return new Promise(function(u){function f(){return++e+Math.random()}var e=0,h=f(),l={},p="string"==typeof t?new Worker(t):t;p.addEventListener("message",function(e){if(e.data.channel===h){e.stopImmediatePropagation();var t=e.data.namespace;if(t){var o=function(e){var a,r,t=new Promise(function(e,t){a=e,r=t});return t.resolve=a,t.reject=r,l[e.id=f()]=t,p.postMessage({channel:h,message:e}),t},c=[].slice;!function a(r){Object.keys(r).forEach(function(e){var t=r[e];switch(t.type){case"class":r[e]=function(e){var t=e.path,a=e.methods,r=e.statics,n=new WeakMap;function s(){n.set(this,f())}return a.forEach(function(e){s.prototype[e]=function(){return o({args:c.call(arguments),path:t,method:e,object:{id:n.get(this),value:this}})}}),r.methods.forEach(function(e){s[e]=function(){return o({args:c.call(arguments),path:t,method:e})}}),r.values.forEach(function(e){s[e[0]]=e[1]}),s}(t);break;case"function":r[e]=function(e){return function(){return o({args:c.call(arguments),path:e})}}(t.path);break;case"object":a(r[e]=t.value);break;default:r[e]=t.value}})}(t),u({worker:p,namespace:t})}else{var a=e.data.message,r=a.id,n=l[r];if(delete l[r],a.hasOwnProperty("error")){var s,i=a.error;i.hasOwnProperty("source")?s=i.source:(s=new Error(i.message)).stack=i.stack,n.reject(s)}else n.resolve(a.result)}}}),p.postMessage({channel:h})})} -------------------------------------------------------------------------------- /node/client.js: -------------------------------------------------------------------------------- 1 | window.workway = (function (Worker, workway, SECRET) { 2 | 3 | /*! Copyright 2018 Andrea Giammarchi - @WebReflection 4 | * 5 | * Permission to use, copy, modify, and/or distribute this software 6 | * for any purpose with or without fee is hereby granted, 7 | * provided that the above copyright notice 8 | * and this permission notice appear in all copies. 9 | * 10 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS 11 | * ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING 12 | * ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. 13 | * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, 14 | * DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR 15 | * ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, 16 | * DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, 17 | * NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 18 | * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 19 | */ 20 | 21 | // ${JSON} 22 | 23 | var instances = []; 24 | var sockets = new WeakMap(); 25 | 26 | addEventListener( 27 | 'beforeunload', 28 | function () { 29 | while (instances.length) instances[0].terminate(); 30 | }, 31 | false 32 | ); 33 | 34 | function NodeWorker(ep) { 35 | var socket = io({JSON: Flatted}); 36 | var self = new EventTarget; 37 | self.postMessage = this.postMessage; 38 | self.terminate = this.terminate; 39 | instances.push(self); 40 | sockets.set(self, socket); 41 | socket.on(SECRET + ':error', this.onerror.bind(self)); 42 | socket.on(SECRET + ':message', this.onmessage.bind(self)); 43 | socket.emit(SECRET + ':setup', ep); 44 | return self; 45 | } 46 | 47 | function createEvent(type) { 48 | var event = document.createEvent('Event'); 49 | event.initEvent(type, false, true); 50 | event.stopImmediatePropagation = event.stopImmediatePropagation || 51 | event.stopPropagation; 52 | return event; 53 | } 54 | 55 | NodeWorker.prototype = { 56 | 57 | onerror: function (error) { 58 | var event = createEvent('error'); 59 | event.message = error.message; 60 | event.stack = error.stack; 61 | this.dispatchEvent(event); 62 | if (this.onerror) this.onerror(event); 63 | }, 64 | 65 | onmessage: function (data) { 66 | var event = createEvent('message'); 67 | event.data = data; 68 | this.dispatchEvent(event); 69 | if (this.onmessage) this.onmessage(event); 70 | }, 71 | 72 | postMessage: function (message) { 73 | sockets.get(this).emit(SECRET, message); 74 | }, 75 | 76 | terminate: function () { 77 | instances.splice(instances.indexOf(this), 1); 78 | sockets.get(this).disconnect(); 79 | } 80 | 81 | }; 82 | 83 | return function (file) { 84 | if (/^node:\/\/([\w._-]+)/.test(file)) { 85 | file = RegExp.$1; 86 | window.Worker = NodeWorker; 87 | var promise = workway(file); 88 | window.Worker = Worker; 89 | return promise; 90 | } else 91 | return workway(file); 92 | }; 93 | 94 | }(Worker, workway, '${SECRET}')); 95 | -------------------------------------------------------------------------------- /node/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | // used core modules 4 | var EventEmitter = require('events').EventEmitter; 5 | var crypto = require('crypto'); 6 | var fs = require('fs'); 7 | var http = require('http'); 8 | var path = require('path'); 9 | var vm = require('vm'); 10 | 11 | // extra dependencies 12 | var Flatted = require('flatted'); 13 | var pocketIO = require('pocket.io'); 14 | var workerjs = fs.readFileSync( 15 | path.resolve(__dirname, '..', 'worker.js') 16 | ).toString(); 17 | 18 | // used as communication channel 19 | var SECRET = crypto.randomBytes(32).toString('hex'); 20 | 21 | // the client side is enriched at runtime 22 | var jsContent = fs.readFileSync(path.join(__dirname, 'client.js')) 23 | .toString() 24 | .replace( 25 | /\$\{(SECRET|JSON)\}/g, 26 | function ($0, $1) { return this[$1]; }.bind({ 27 | SECRET: SECRET, 28 | JSON: fs.readFileSync(require.resolve('flatted/min.js')).toString() 29 | }) 30 | ); 31 | 32 | var workers = ''; 33 | 34 | process.on('unhandledRejection', uncaught); 35 | process.on('uncaughtException', uncaught); 36 | 37 | // return a new Worker sandbox 38 | function createSandbox(filename, socket) { 39 | var self = new EventEmitter; 40 | var listeners = new WeakMap; 41 | self.addEventListener = function (type, listener) { 42 | if (!listeners.has(listener)) { 43 | var facade = function (event) { 44 | if (!event.canceled) listener.apply(this, arguments); 45 | }; 46 | listeners.set(listener, facade); 47 | self.on(type, facade); 48 | } 49 | }; 50 | self.removeEventListener = function (type, listener) { 51 | self.removeListener(type, listeners.get(listener)); 52 | }; 53 | self.__filename = filename; 54 | self.__dirname = path.dirname(filename); 55 | self.postMessage = function postMessage(data) { message(socket, data); }; 56 | self.console = console; 57 | self.process = process; 58 | self.Buffer = Buffer; 59 | self.clearImmediate = clearImmediate; 60 | self.clearInterval = clearInterval; 61 | self.clearTimeout = clearTimeout; 62 | self.setImmediate = setImmediate; 63 | self.setInterval = setInterval; 64 | self.setTimeout = setTimeout; 65 | self.module = module; 66 | self.global = self; 67 | self.self = self; 68 | self.require = function (file) { 69 | switch (true) { 70 | case file === 'workway': 71 | return self.workway; 72 | case /^[./]/.test(file): 73 | file = path.resolve(self.__dirname, file); 74 | default: 75 | return require(file); 76 | } 77 | }; 78 | return self; 79 | } 80 | 81 | function cleanedStack(stack) { 82 | return ''.replace.call(stack, /:uid-\d+-[a-f0-9]{16}/g, ''); 83 | } 84 | 85 | // notify the socket there was an error 86 | function error(socket, err) { 87 | socket.emit(SECRET + ':error', { 88 | message: err.message, 89 | stack: cleanedStack(err.stack) 90 | }); 91 | } 92 | 93 | // send serialized data to the client 94 | function message(socket, data) { 95 | socket.emit(SECRET + ':message', data); 96 | } 97 | 98 | // used to send /node-worker.js client file 99 | function responder(request, response, next) { 100 | response.writeHead(200, 'OK', { 101 | 'Content-Type': 'application/javascript' 102 | }); 103 | response.end(jsContent); 104 | if (next) next(); 105 | } 106 | 107 | uid.i = 0; 108 | uid.map = Object.create(null); 109 | uid.delete = function (sandbox) { 110 | Object.keys(uid.map).some(function (key) { 111 | var found = uid.map[key] === sandbox; 112 | if (found) delete uid.map[key]; 113 | return found; 114 | }); 115 | }; 116 | 117 | function uid(filename, socket) { 118 | var id = filename + ':uid-'.concat(++uid.i, '-', crypto.randomBytes(8).toString('hex')); 119 | uid.map[id] = socket; 120 | return id; 121 | } 122 | 123 | function uncaught(err) { 124 | console.error(err); 125 | if (/([\S]+?(:uid-\d+-[a-f0-9]{16}))/.test(err.stack)) { 126 | var socket = uid.map[RegExp.$1]; 127 | if (socket) error(socket, err); 128 | } 129 | } 130 | 131 | function Event(data) { 132 | this.canceled = false; 133 | this.data = data; 134 | } 135 | 136 | Event.prototype.stopImmediatePropagation = function () { 137 | this.canceled = true; 138 | }; 139 | 140 | module.exports = { 141 | authorize: function (folder) { 142 | if (workers.length) throw new Error('workway already authorized'); 143 | workers = folder; 144 | return this; 145 | }, 146 | app: function (app) { 147 | var io; 148 | var native = app instanceof http.Server; 149 | if (native) { 150 | io = pocketIO(app, {JSON: Flatted}); 151 | var request = app._events.request; 152 | app._events.request = function (req) { 153 | return /^\/workway@node\.js(?:\?|$)/.test(req.url) ? 154 | responder.apply(this, arguments) : 155 | request.apply(this, arguments); 156 | }; 157 | } else { 158 | var wrap = http.Server(app); 159 | io = pocketIO(wrap, {JSON: Flatted}); 160 | app.get('/workway@node.js', responder); 161 | Object.defineProperty(app, 'listen', { 162 | configurable: true, 163 | value: function () { 164 | wrap.listen.apply(wrap, arguments); 165 | return app; 166 | } 167 | }); 168 | } 169 | io.on('connection', function (socket) { 170 | var sandbox; 171 | var queue = []; 172 | function message(data) { 173 | if (sandbox) { 174 | try { 175 | var event = new Event(data); 176 | sandbox.emit('message', event); 177 | if ('onmessage' in sandbox && !event.canceled) { 178 | sandbox.onmessage(event); 179 | } 180 | } catch(err) { 181 | error(socket, err); 182 | } 183 | } 184 | else queue.push(data); 185 | } 186 | socket.on(SECRET, message); 187 | socket.on(SECRET + ':setup', function (worker) { 188 | var filename = path.resolve(path.join(workers, worker)); 189 | if (filename.indexOf(workers)) { 190 | error(socket, { 191 | message: 'Unauthorized worker: ' + worker, 192 | stack: '' 193 | }); 194 | } else { 195 | fs.readFile(filename, function (err, content) { 196 | if (err) { 197 | error(socket, { 198 | message: 'Worker not found: ' + worker, 199 | stack: err.stack 200 | }); 201 | } else { 202 | sandbox = createSandbox(filename, socket); 203 | vm.createContext(sandbox); 204 | vm.runInContext(workerjs, sandbox); 205 | vm.runInContext(content, sandbox, { 206 | filename: uid(worker, socket), 207 | displayErrors: true 208 | }); 209 | while (queue.length) { 210 | setTimeout(message, queue.length * 100, queue.pop()); 211 | } 212 | } 213 | }); 214 | } 215 | }); 216 | socket.on('disconnect', function () { 217 | uid.delete(socket); 218 | sandbox = null; 219 | }); 220 | }); 221 | return app; 222 | } 223 | }; 224 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "workway", 3 | "version": "0.5.5", 4 | "description": "A general purpose, Web Worker driven, remote namespace with classes and methods.", 5 | "unpkg": "min.js", 6 | "main": "cjs/index.js", 7 | "module": "esm/index.js", 8 | "scripts": { 9 | "$": "npm-dollar", 10 | "build": "npm-dollar build", 11 | "bundle": "npm-dollar bundle", 12 | "size": "npm-dollar size", 13 | "test": "npm-dollar test" 14 | }, 15 | "$": { 16 | "build": [ 17 | "$ remoted", 18 | "$ bundle", 19 | "$ test", 20 | "$ size" 21 | ], 22 | "bundle": { 23 | "cjs": [ 24 | "cp index.js cjs/", 25 | "echo 'module.exports = workway;' >> cjs/index.js" 26 | ], 27 | "esm": [ 28 | "cp index.js esm/", 29 | "echo 'export default workway;' >> esm/index.js" 30 | ], 31 | "min": "uglifyjs index.js --comments=/^!/ -m -c -o min.js", 32 | "node": [ 33 | [ 34 | "node -e 'fs.writeFileSync(\"worker.js\",", 35 | "fs.readFileSync(\"partial/worker.js\").toString().replace(/^(\\s*)\\/\\/js:(\\w+)/gm,", 36 | "(o,s,k)=>fs.readFileSync(k+\".js\").toString().trim().replace(/^/gm,s)))'" 37 | ] 38 | ] 39 | }, 40 | "remoted": { 41 | "cjs": [ 42 | "cp remoted.js cjs/", 43 | "echo 'module.exports = remoted;' >> cjs/remoted.js" 44 | ], 45 | "esm": [ 46 | "cp remoted.js esm/", 47 | "echo 'export default remoted;' >> esm/remoted.js" 48 | ] 49 | }, 50 | "size": [ 51 | [ 52 | "cat index.js |", 53 | "wc -c;cat min.js |", 54 | "wc -c;gzip -c9 min.js |", 55 | "wc -c;cat min.js |", 56 | "brotli |", 57 | "wc -c && rm -f min.js.br" 58 | ] 59 | ], 60 | "test": [ 61 | "cd test", 62 | "node index.js", 63 | "echo $(tput bold)OK$(tput sgr0)" 64 | ] 65 | }, 66 | "author": "Andrea Giammarchi", 67 | "license": "ISC", 68 | "devDependencies": { 69 | "express": "^4.17.1", 70 | "npm-dollar": "^2.2.1", 71 | "uglify-js": "^3.6.0" 72 | }, 73 | "repository": { 74 | "type": "git", 75 | "url": "git+https://github.com/WebReflection/workway.git" 76 | }, 77 | "keywords": [ 78 | "web", 79 | "worker", 80 | "remote", 81 | "namespace", 82 | "driven" 83 | ], 84 | "bugs": { 85 | "url": "https://github.com/WebReflection/workway/issues" 86 | }, 87 | "homepage": "https://github.com/WebReflection/workway#readme", 88 | "dependencies": { 89 | "flatted": "^2.0.0", 90 | "pocket.io": "^0.1.4" 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /partial/worker.js: -------------------------------------------------------------------------------- 1 | (function () {'use strict'; 2 | /*! (c) 2018 Andrea Giammarchi (ISC) */ 3 | function walkThrough(O, K) { return O[K]; } 4 | var namespace; 5 | var channels = {}; 6 | var instances = {}; 7 | var onceExposed = new Promise(function (resolve) { 8 | self.workway = function workway(exposed) { 9 | return Promise.resolve(exposed).then(function (result) { 10 | namespace = result; 11 | resolve(remoted(result)); 12 | }); 13 | }; 14 | //js:remoted 15 | }); 16 | self.addEventListener('message', function (event) { 17 | var method; 18 | var data = event.data; 19 | var channel = data.channel; 20 | var message = data.message; 21 | if (channels[channel]) { 22 | event.stopImmediatePropagation(); 23 | var id = message.id; 24 | var path = message.path; 25 | var args = message.args; 26 | var resolved = function (result) { send({result: result}); }; 27 | var rejected = function (error) { 28 | if ( 29 | error != null && 30 | typeof error === 'object' && 31 | 'message' in error 32 | ) 33 | send({error: { 34 | stack: error.stack, 35 | message: error.message 36 | }}); 37 | else 38 | send({error: {source: error}}); 39 | }; 40 | var send = function (message) { 41 | message.id = id; 42 | self.postMessage({ 43 | channel: channel, 44 | message: message 45 | }); 46 | }; 47 | try { 48 | if (message.hasOwnProperty('method')) { 49 | method = message.method; 50 | var Class = path.reduce(walkThrough, namespace); 51 | if (!Class) 52 | return rejected('Unknown Class ' + path.join('.')); 53 | if (message.hasOwnProperty('object')) { 54 | var object = message.object; 55 | var instance = instances[object.id] || 56 | (instances[object.id] = new Class); 57 | if (method === 'destroy') 58 | delete instances[object.id]; 59 | else { 60 | Object.keys(object.value) 61 | .forEach(function (key) { 62 | instance[key] = object.value[key]; 63 | }); 64 | Promise.resolve(instance[method].apply(instance, args)) 65 | .then(resolved, rejected); 66 | } 67 | } else { 68 | Promise.resolve(Class[method].apply(Class, args)) 69 | .then(resolved, rejected); 70 | } 71 | } else { 72 | var context = path.slice(0, -1).reduce(walkThrough, namespace); 73 | if (!context) 74 | return rejected('Unknown namespace ' + path.slice(0, -1).join('.')); 75 | method = path[path.length - 1]; 76 | if (typeof context[method] !== 'function') 77 | return rejected('Unknown method ' + path.join('.')); 78 | Promise.resolve(context[method].apply(context, args)) 79 | .then(resolved, rejected); 80 | } 81 | } catch(error) { 82 | rejected(error); 83 | } 84 | } else if (/^(-?\d+\.\d+)$/.test(channel)) { 85 | channels[channel] = true; 86 | event.stopImmediatePropagation(); 87 | onceExposed.then(function (namespace) { 88 | self.postMessage({ 89 | channel: channel, 90 | namespace: namespace 91 | }); 92 | }); 93 | } 94 | }); 95 | }()); 96 | -------------------------------------------------------------------------------- /remoted.js: -------------------------------------------------------------------------------- 1 | function remoted(object) { 2 | return function $(object, current, remote) { 3 | Object.keys(object).forEach(function (key) { 4 | var value = object[key]; 5 | var path = current.concat(key); 6 | if (typeof value === 'function') { 7 | remote[key] = /^[A-Z]/.test(key) ? 8 | { 9 | type: 'class', 10 | path: path, 11 | methods: Object.getOwnPropertyNames(value.prototype) 12 | .filter(no(['constructor'])) 13 | .concat('destroy'), 14 | statics: Object.getOwnPropertyNames(value) 15 | .filter(no([ 16 | 'arguments', 'callee', 'caller', 17 | 'length', 'name', 'prototype' 18 | ])) 19 | .reduce( 20 | function (info, key) { 21 | if (typeof value[key] === 'function') { 22 | info.methods.push(key); 23 | } else { 24 | info.values.push([key, value[key]]); 25 | } 26 | return info; 27 | }, 28 | { 29 | methods: [], 30 | values: [] 31 | } 32 | ) 33 | } : 34 | { 35 | type: 'function', 36 | path: path 37 | }; 38 | } else if (remote.toString.call(value) === '[object Object]') { 39 | remote[key] = { 40 | type: 'object', 41 | path: path, 42 | value: {} 43 | }; 44 | $(value, path, remote[key].value); 45 | } else if (value !== void 0) { 46 | remote[key] = { 47 | type: 'any', 48 | path: path, 49 | value: value 50 | }; 51 | } 52 | }); 53 | return remote; 54 | }(object, [], {}); 55 | function no(within) { 56 | return function (what) { 57 | return within.indexOf(what) < 0; 58 | }; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /test/background.js: -------------------------------------------------------------------------------- 1 | // a Blackberry gotcha, no console in workers 2 | if (!self.console) self.console = {log: function () {}, error: function () {}}; 3 | // import the polyfill you prefer for IE11 or IE10 4 | if (!self.Promise) importScripts('https://cdn.jsdelivr.net/npm/es6-promise@4/dist/es6-promise.auto.min.js'); 5 | if (!self.WeakMap) importScripts('https://unpkg.com/poorlyfills@0.1.1/min.js'); 6 | 7 | // to avoid any possible issue with messages 8 | // import the remote utility n the top of your worker 9 | importScripts('../worker.js'); 10 | 11 | // ES2015 classes would work too 12 | // ( not using class for IE11 tests ) 13 | function Test() {} 14 | Test.method = function () { 15 | console.log('Test.method'); 16 | console.log('arguments', [].slice.call(arguments)); 17 | return Math.random(); 18 | }; 19 | Test.prototype.method = function () { 20 | console.log('Test.prototype.method'); 21 | console.log('instance', JSON.stringify(this)); 22 | console.log('arguments', [].slice.call(arguments)); 23 | return Math.random(); 24 | }; 25 | 26 | // expose a namespace with serializable data 27 | // but also classes and utilities as methods/functions 28 | workway({ 29 | test: 123, 30 | array: [1, 2, 3], 31 | object: {a: 'a'}, 32 | nested: { 33 | method: function () { 34 | console.log('method'); 35 | console.log('arguments', [].slice.call(arguments)); 36 | return Math.random(); 37 | }, 38 | Test: Test 39 | } 40 | }); 41 | 42 | // you can regularly post any sort of message, or listen 43 | // to anything you want to 44 | self.addEventListener('message', function (event) { 45 | console.log(event.type, event.data); 46 | if (event.data.hasOwnProperty('echo')) 47 | self.postMessage(event.data.echo); 48 | }); 49 | -------------------------------------------------------------------------------- /test/circular/analyzer.js: -------------------------------------------------------------------------------- 1 | importScripts('https://unpkg.com/workway/worker.js'); 2 | 3 | workway({ 4 | analyze(circular) { 5 | return circular; 6 | } 7 | }); 8 | -------------------------------------------------------------------------------- /test/circular/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 28 | -------------------------------------------------------------------------------- /test/client.js: -------------------------------------------------------------------------------- 1 | workway('./background.js').then(function (info) { 2 | // {worker, namespace} 3 | var worker = info.worker; 4 | var namespace = info.namespace; 5 | 6 | // you can either use addEventListener 7 | // or onmessage and onerror and 8 | // these will never receive remote events, 9 | // only user messages, and same goes if you post messages 10 | // the remote logic won't ever be affected 11 | worker.onmessage = function (event) { showData(event.data); }; 12 | worker.addEventListener('message', console.log.bind(console)); 13 | 14 | // you can also send regular messages 15 | // without affecting namespace operations 16 | worker.postMessage({echo: 'hello remote'}); 17 | 18 | // classes can have static methods 19 | // static values, and regular prototypal methods 20 | // however there are few limitations such inheritance 21 | // and constructor arguments, which is always, and only 22 | // the unique identifier used to pair local/remote instances 23 | var instance = new namespace.nested.Test; 24 | 25 | // properties can be added, as long as these are serializable 26 | instance.test = Math.random(); 27 | 28 | // and every method of the class returns a Promise 29 | // that will resolve once the instance has been updated 30 | // (properties) and the method invoked, 31 | // with serializable arguments 32 | instance.method(1, 2, 3) 33 | .then(showData, console.error.bind(console)) 34 | .then(function () { instance.destroy(); }); 35 | 36 | function showData(data) { 37 | document.body.appendChild( 38 | document.createElement('pre') 39 | ).textContent = JSON.stringify(data, null, ' '); 40 | } 41 | 42 | }); 43 | -------------------------------------------------------------------------------- /test/foo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /test/foo.js: -------------------------------------------------------------------------------- 1 | var PORT = process.env.PORT || 3000; 2 | 3 | var path = require('path'); 4 | var express = require('express'); 5 | var workway = require('../node').authorize( 6 | path.resolve(__dirname, 'workers') 7 | ); 8 | 9 | var app = workway.app(express()); 10 | app.get('/', function (req, res) { 11 | res.writeHead(200, 'OK', { 12 | 'Content-Type': 'text/html; charset=utf-8' 13 | }); 14 | res.end(require('fs').readFileSync(path.resolve(__dirname, 'foo.html'))); 15 | }); 16 | app.get('/workway.js', function (req, res) { 17 | res.writeHead(200, 'OK', { 18 | 'Content-Type': 'application/javascript; charset=utf-8' 19 | }); 20 | res.end(require('fs').readFileSync(path.resolve(__dirname, '..', 'min.js'))); 21 | }); 22 | app.listen(PORT, () => { 23 | console.log('listening on http://localhost:' + PORT); 24 | }); 25 | -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | require('./webworker'); 2 | global.workway = require('../cjs/index'); 3 | require('./client'); 4 | -------------------------------------------------------------------------------- /test/node.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /test/node.js: -------------------------------------------------------------------------------- 1 | var PORT = process.env.PORT || 3000; 2 | 3 | var path = require('path'); 4 | var express = require('express'); 5 | var workway = require('../node').authorize( 6 | path.resolve(__dirname, 'workers') 7 | ); 8 | 9 | var app = workway.app(express()); 10 | app.get('/', function (req, res) { 11 | res.writeHead(200, 'OK', { 12 | 'Content-Type': 'text/html; charset=utf-8' 13 | }); 14 | res.end(require('fs').readFileSync(path.resolve(__dirname, 'node.html'))); 15 | }); 16 | app.get('/workway.js', function (req, res) { 17 | res.writeHead(200, 'OK', { 18 | 'Content-Type': 'application/javascript; charset=utf-8' 19 | }); 20 | res.end(require('fs').readFileSync(path.resolve(__dirname, '..', 'index.js'))); 21 | }); 22 | app.listen(PORT, () => { 23 | console.log('listening on http://localhost:' + PORT); 24 | }); 25 | -------------------------------------------------------------------------------- /test/webworker.js: -------------------------------------------------------------------------------- 1 | const workers = []; 2 | 3 | class Event { 4 | constructor(data) { 5 | this._stopImmediatePropagation = false; 6 | this.type = 'message'; 7 | this.data = data; 8 | } 9 | stopImmediatePropagation() { 10 | this._stopImmediatePropagation = true; 11 | } 12 | } 13 | 14 | global.Worker = class Worker extends require('events').EventEmitter { 15 | constructor(file) { 16 | workers.push(super()); 17 | require(file); 18 | } 19 | addEventListener(type, listener) { 20 | this.on(type, listener); 21 | } 22 | postMessage(data) { 23 | process.emit('message', new Event(data)); 24 | } 25 | }; 26 | 27 | global.document = { 28 | body: { 29 | appendChild: Object 30 | }, 31 | createElement() { 32 | return {set textContent(value) { 33 | console.log(value); 34 | }}; 35 | } 36 | }; 37 | global.importScripts = require; 38 | global.self = new Proxy( 39 | { 40 | addEventListener(type, listener) { 41 | process.on(type, listener); 42 | }, 43 | postMessage(data) { 44 | workers.forEach(worker => worker.emit('message', new Event(data))); 45 | } 46 | }, 47 | { 48 | get: (self, key) => self[key] || global[key], 49 | set: (self, key, value) => { 50 | global[key] = value; 51 | return true; 52 | } 53 | } 54 | ); 55 | -------------------------------------------------------------------------------- /test/workers/foo.js: -------------------------------------------------------------------------------- 1 | const util = require('util'); 2 | 3 | const workway = require('workway'); 4 | 5 | function foo(message, callback) { 6 | callback(42); // forcing always an error 7 | } 8 | 9 | const fooPromisified = util.promisify(foo); 10 | 11 | workway({ 12 | fooPromisified 13 | }); -------------------------------------------------------------------------------- /test/workers/other.js: -------------------------------------------------------------------------------- 1 | console.log(__filename); 2 | -------------------------------------------------------------------------------- /test/workers/test.js: -------------------------------------------------------------------------------- 1 | var workway = require('workway'); 2 | 3 | // require('./other'); 4 | 5 | workway({ 6 | os: require('os'), 7 | ping: function () { 8 | self.postMessage('pong'); 9 | } 10 | }); 11 | 12 | self.addEventListener('message', function (event) { 13 | console.log(event.data); 14 | }); 15 | -------------------------------------------------------------------------------- /worker.js: -------------------------------------------------------------------------------- 1 | (function () {'use strict'; 2 | /*! (c) 2018 Andrea Giammarchi (ISC) */ 3 | function walkThrough(O, K) { return O[K]; } 4 | var namespace; 5 | var channels = {}; 6 | var instances = {}; 7 | var onceExposed = new Promise(function (resolve) { 8 | self.workway = function workway(exposed) { 9 | return Promise.resolve(exposed).then(function (result) { 10 | namespace = result; 11 | resolve(remoted(result)); 12 | }); 13 | }; 14 | function remoted(object) { 15 | return function $(object, current, remote) { 16 | Object.keys(object).forEach(function (key) { 17 | var value = object[key]; 18 | var path = current.concat(key); 19 | if (typeof value === 'function') { 20 | remote[key] = /^[A-Z]/.test(key) ? 21 | { 22 | type: 'class', 23 | path: path, 24 | methods: Object.getOwnPropertyNames(value.prototype) 25 | .filter(no(['constructor'])) 26 | .concat('destroy'), 27 | statics: Object.getOwnPropertyNames(value) 28 | .filter(no([ 29 | 'arguments', 'callee', 'caller', 30 | 'length', 'name', 'prototype' 31 | ])) 32 | .reduce( 33 | function (info, key) { 34 | if (typeof value[key] === 'function') { 35 | info.methods.push(key); 36 | } else { 37 | info.values.push([key, value[key]]); 38 | } 39 | return info; 40 | }, 41 | { 42 | methods: [], 43 | values: [] 44 | } 45 | ) 46 | } : 47 | { 48 | type: 'function', 49 | path: path 50 | }; 51 | } else if (remote.toString.call(value) === '[object Object]') { 52 | remote[key] = { 53 | type: 'object', 54 | path: path, 55 | value: {} 56 | }; 57 | $(value, path, remote[key].value); 58 | } else if (value !== void 0) { 59 | remote[key] = { 60 | type: 'any', 61 | path: path, 62 | value: value 63 | }; 64 | } 65 | }); 66 | return remote; 67 | }(object, [], {}); 68 | function no(within) { 69 | return function (what) { 70 | return within.indexOf(what) < 0; 71 | }; 72 | } 73 | } 74 | }); 75 | self.addEventListener('message', function (event) { 76 | var method; 77 | var data = event.data; 78 | var channel = data.channel; 79 | var message = data.message; 80 | if (channels[channel]) { 81 | event.stopImmediatePropagation(); 82 | var id = message.id; 83 | var path = message.path; 84 | var args = message.args; 85 | var resolved = function (result) { send({result: result}); }; 86 | var rejected = function (error) { 87 | if ( 88 | error != null && 89 | typeof error === 'object' && 90 | 'message' in error 91 | ) 92 | send({error: { 93 | stack: error.stack, 94 | message: error.message 95 | }}); 96 | else 97 | send({error: {source: error}}); 98 | }; 99 | var send = function (message) { 100 | message.id = id; 101 | self.postMessage({ 102 | channel: channel, 103 | message: message 104 | }); 105 | }; 106 | try { 107 | if (message.hasOwnProperty('method')) { 108 | method = message.method; 109 | var Class = path.reduce(walkThrough, namespace); 110 | if (!Class) 111 | return rejected('Unknown Class ' + path.join('.')); 112 | if (message.hasOwnProperty('object')) { 113 | var object = message.object; 114 | var instance = instances[object.id] || 115 | (instances[object.id] = new Class); 116 | if (method === 'destroy') 117 | delete instances[object.id]; 118 | else { 119 | Object.keys(object.value) 120 | .forEach(function (key) { 121 | instance[key] = object.value[key]; 122 | }); 123 | Promise.resolve(instance[method].apply(instance, args)) 124 | .then(resolved, rejected); 125 | } 126 | } else { 127 | Promise.resolve(Class[method].apply(Class, args)) 128 | .then(resolved, rejected); 129 | } 130 | } else { 131 | var context = path.slice(0, -1).reduce(walkThrough, namespace); 132 | if (!context) 133 | return rejected('Unknown namespace ' + path.slice(0, -1).join('.')); 134 | method = path[path.length - 1]; 135 | if (typeof context[method] !== 'function') 136 | return rejected('Unknown method ' + path.join('.')); 137 | Promise.resolve(context[method].apply(context, args)) 138 | .then(resolved, rejected); 139 | } 140 | } catch(error) { 141 | rejected(error); 142 | } 143 | } else if (/^(-?\d+\.\d+)$/.test(channel)) { 144 | channels[channel] = true; 145 | event.stopImmediatePropagation(); 146 | onceExposed.then(function (namespace) { 147 | self.postMessage({ 148 | channel: channel, 149 | namespace: namespace 150 | }); 151 | }); 152 | } 153 | }); 154 | }()); 155 | --------------------------------------------------------------------------------