Repository: WebReflection/workway Branch: master Commit: 0b7ce7706199 Files: 47 Total size: 83.3 KB Directory structure: gitextract_vwqyj7po/ ├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── cjs/ │ ├── index.js │ └── remoted.js ├── esm/ │ ├── index.js │ └── remoted.js ├── examples/ │ ├── 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/ │ ├── 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 ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ .DS_Store node_modules/ package-lock.json ^worker.js ================================================ FILE: .npmignore ================================================ node_modules/* examples/* partial/* test/* .DS_Store .gitignore .travis.yml package-lock.json ================================================ FILE: .travis.yml ================================================ language: node_js node_js: - stable git: depth: 1 branches: only: - master ================================================ FILE: LICENSE ================================================ ISC License Copyright (c) 2018, Andrea Giammarchi, @WebReflection Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ================================================ FILE: README.md ================================================ # workway DEPRECATED - See [coincident](https://github.com/WebReflection/coincident#coincidentserver) A general purpose, Web Worker driven, remote namespace with classes and methods. - - - ## Announcement: Meet proxied-worker & proxied-node 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). It has [a NodeJS counterpart module](https://github.com/WebReflection/proxied-node#readme) too! The main difference with these projects is: * classes have a working constructor * heap is automatically cleaned on both client/server * it uses *Proxy* and *FinalizationRegistry* so these are not as compatible as *workway* is with legacy browsers - - - ## Key Features * no eval at all, no scope issues, 100% CSP friendly * 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/) * 100LOC client squeezed in about 0.5K once compressed * you expose non blocking namespaces to the main thread, not the other way around * it **works on NodeJS** too 🎉 ## Example (client side only) A basic **firebase.js** client to show the user name. ```js workway('/workers/firebase.js').then( async function ({worker, namespace}) { await namespace.initializeApp({ apiKey: "", authDomain: ".firebaseapp.com", databaseURL: "https://.firebaseio.com", projectId: "", storageBucket: ".appspot.com", messagingSenderId: "" }); const fb = new namespace.FirebaseUser(); const name = await fb.name(); console.log(name); // will log the user name, if any // the worker can be regularly used like any other worker worker.postMessage('all good'); } ); // you can also pass in an existing Worker instance (useful if you're using // Webpack's worker-loader and don't have access to the output file path): import Worker from 'worker-loader!./Worker.js'; workway(new Worker()).then(... ``` The **workers/firebase.js** worker that exposes some info. ```js // top import to ensure a transparent communication channel importScripts('https://unpkg.com/workway/worker.js'); // any other needed import for this worker importScripts(...[ 'app', 'auth', 'database', 'firestore', 'messaging', 'functions' ].map(name => `https://www.gstatic.com/firebasejs/5.0.1/firebase-${name}.js`)); // expose a namespaces as an object // with any sort of serializable value // and also methods or classes workway({ // any serializable data is OK (nested too) timestamp: Date.now(), // methods are OK too, each method // accepts serializable arguments and // can return a value and/or a promise initializeApp(config) { firebase.initializeApp(config); }, // classes are also fine, as long as // these respect RemoteClass conventions FirebaseUser: class FirebaseUser { constructor() { this.uid = firebase.auth().currentUser.uid; } name() { return firebase.database() .ref('/users/' + this.uid) .once('value') .then(snapshot => (( snapshot.val() && snapshot.val().username ) || 'Anonymous')); } } }); // this worker can be regularly used like any other worker // the passed event will never be one handled by `workway` self.onmessage = event => { console.log(event.data); }; ``` ## Example (NodeJS) 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. ```html ``` This is a `js/os.js` file for the client side. ```js workway('node://os.js').then(({worker, namespace:os}) => { os.getNetworkInterfaces().then(console.log); }); ``` Please note the client file needs EventTarget, Promise, and WeakMap constructors. If your target browsers don't have these features, you can use the following polyfills on top of your HTML file. ```html ``` Following a `workers/os.js` file to serve via NodeJS. ```js // note: you require a facade here via 'workway' var workway = require('workway'); workway(require('os')); ``` An express / node based bootstrap. ```js var express = require('express'); // note: you require the real module as 'workway/node' var workway = require('workway/node'); // authorize / expose a specific folder // that contains web driven workers workway.authorize(__dirname + '/workers'); var app = workway.app(express()); app.use(express.static(__dirname + '/www')); app.listen(8080); ``` ### NodeJS extra features & gotchas * exact same API (actually exact same code) of the real Web Worker based client side * circular objects are supported out of the box via [flatted](https://github.com/WebReflection/flatted#flatted) on both ways * the `self` global (but sandboxed) variable points at the global * the `self.workway` method is already there, feel free to use it instead of requiring it from workers * 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 ## The RemoteClass convention Classes exposed through `workway` namespace must follow these rules: * no constructor arguments; use methods to eventually forward extra details from the client * methods can accept only serializable arguments and can return either a serializable value or a promise that will resolve as serializable data * properties set on the **client** side must be serializable and will be **reflected** into related worker instances whenever methods are invoked * 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 * every method invocation returns a Promise, even if the method returned value is not * 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. ## Compatibility 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+. You can test live your browser through the **[live test page](https://webreflection.github.io/workway/test/index.html)**. Please note in IE 10/11 or other old browser cases, you might need to provide polyfills on both client and worker side. Feel free to choose the polyfill you prefer. Following just as example: ```html ``` Or on top of your generic worker. ```js // import the polyfill you prefer for either IE11 or IE10 if (!self.Promise) importScripts('https://cdn.jsdelivr.net/npm/es6-promise@4/dist/es6-promise.auto.min.js'); if (!self.WeakMap) importScripts('https://unpkg.com/poorlyfills@0.1.1/min.js'); // now import workway/worker.js before any other worker script importScripts('https://unpkg.com/workway/worker.js'); // ... the rest of the code ... ``` ## About Recursive data 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. ```js // main thread app.js side import workway from 'https://unpkg.com/workway/esm'; import {stringify} from 'https://unpkg.com/flatted/esm'; workway('analyzer.js').then(({namespace}) => { const data = {arr: []}; data.arr.push(data); data.data = data; namespace.analyze(stringify(data)) .then( state => document.body.textContent = state, console.error ); }); // worker side: analyzer.js importScripts( 'https://unpkg.com/workway/worker.js', 'https://unpkg.com/flatted' ); workway({ analyze(circular) { const data = Flatted.parse(circular); return 'OK'; } }); ``` You can [test above example right here](https://webreflection.github.io/workway/test/circular/). ### Extra Info * the client side of this package fits in just 100 LOC * the client side of this project weights 0.5K via brotli, 0.6K via gzip * the client side source of truth of this project is its root `./index.js` * the only worker related code is in `./worker.js` root file * the ESM version of this module is in `esm/index.js` * the CJS version of this module is in `cjs/index.js` * the browser version of this module is in `min.js` ================================================ FILE: cjs/index.js ================================================ function workway(file) {'use strict'; /*! (c) 2018 Andrea Giammarchi (ISC) */ return new Promise(function (res) { function uid() { return ++i + Math.random(); } var i = 0; var channel = uid(); var messages = {}; var worker = typeof file === 'string' ? new Worker(file) : file; worker.addEventListener('message', function (event) { if (event.data.channel !== channel) return; event.stopImmediatePropagation(); var namespace = event.data.namespace; if (namespace) { var Class = function (info) { var path = info.path; var methods = info.methods; var statics = info.statics; var wm = new WeakMap; function RemoteClass() { wm.set(this, uid()); } methods.forEach(function (method) { RemoteClass.prototype[method] = function () { return send({ args: slice.call(arguments), path: path, method: method, object: { id: wm.get(this), value: this } }); }; }); statics.methods.forEach(function (method) { RemoteClass[method] = function () { return send({ args: slice.call(arguments), path: path, method: method }); }; }); statics.values.forEach(function (pair) { RemoteClass[pair[0]] = pair[1]; }); return RemoteClass; }; var callback = function (path) { return function remoteCallback() { return send({ args: slice.call(arguments), path: path }); }; }; var send = function (message) { var resolve, reject; var promise = new Promise(function (res, rej) { resolve = res; reject = rej; }); promise.resolve = resolve; promise.reject = reject; messages[message.id = uid()] = promise; worker.postMessage({ channel: channel, message: message }); return promise; }; var slice = [].slice; (function update(namespace) { Object.keys(namespace).forEach(function (key) { var info = namespace[key]; switch (info.type) { case 'class': namespace[key] = Class(info); break; case 'function': namespace[key] = callback(info.path); break; case 'object': update(namespace[key] = info.value); break; default: namespace[key] = info.value; } }); }(namespace)); res({ worker: worker, namespace: namespace }); } else { var message = event.data.message; var id = message.id; var promise = messages[id]; delete messages[id]; if (message.hasOwnProperty('error')) { var error, facade = message.error; if (facade.hasOwnProperty('source')) error = facade.source; else { error = new Error(facade.message); error.stack = facade.stack; } promise.reject(error); } else promise.resolve(message.result); } }); worker.postMessage({channel: channel}); }); } module.exports = workway; ================================================ FILE: cjs/remoted.js ================================================ function remoted(object) { return function $(object, current, remote) { Object.keys(object).forEach(function (key) { var value = object[key]; var path = current.concat(key); if (typeof value === 'function') { remote[key] = /^[A-Z]/.test(key) ? { type: 'class', path: path, methods: Object.getOwnPropertyNames(value.prototype) .filter(no(['constructor'])) .concat('destroy'), statics: Object.getOwnPropertyNames(value) .filter(no([ 'arguments', 'callee', 'caller', 'length', 'name', 'prototype' ])) .reduce( function (info, key) { if (typeof value[key] === 'function') { info.methods.push(key); } else { info.values.push([key, value[key]]); } return info; }, { methods: [], values: [] } ) } : { type: 'function', path: path }; } else if (remote.toString.call(value) === '[object Object]') { remote[key] = { type: 'object', path: path, value: {} }; $(value, path, remote[key].value); } else if (value !== void 0) { remote[key] = { type: 'any', path: path, value: value }; } }); return remote; }(object, [], {}); function no(within) { return function (what) { return within.indexOf(what) < 0; }; } } module.exports = remoted; ================================================ FILE: esm/index.js ================================================ function workway(file) {'use strict'; /*! (c) 2018 Andrea Giammarchi (ISC) */ return new Promise(function (res) { function uid() { return ++i + Math.random(); } var i = 0; var channel = uid(); var messages = {}; var worker = typeof file === 'string' ? new Worker(file) : file; worker.addEventListener('message', function (event) { if (event.data.channel !== channel) return; event.stopImmediatePropagation(); var namespace = event.data.namespace; if (namespace) { var Class = function (info) { var path = info.path; var methods = info.methods; var statics = info.statics; var wm = new WeakMap; function RemoteClass() { wm.set(this, uid()); } methods.forEach(function (method) { RemoteClass.prototype[method] = function () { return send({ args: slice.call(arguments), path: path, method: method, object: { id: wm.get(this), value: this } }); }; }); statics.methods.forEach(function (method) { RemoteClass[method] = function () { return send({ args: slice.call(arguments), path: path, method: method }); }; }); statics.values.forEach(function (pair) { RemoteClass[pair[0]] = pair[1]; }); return RemoteClass; }; var callback = function (path) { return function remoteCallback() { return send({ args: slice.call(arguments), path: path }); }; }; var send = function (message) { var resolve, reject; var promise = new Promise(function (res, rej) { resolve = res; reject = rej; }); promise.resolve = resolve; promise.reject = reject; messages[message.id = uid()] = promise; worker.postMessage({ channel: channel, message: message }); return promise; }; var slice = [].slice; (function update(namespace) { Object.keys(namespace).forEach(function (key) { var info = namespace[key]; switch (info.type) { case 'class': namespace[key] = Class(info); break; case 'function': namespace[key] = callback(info.path); break; case 'object': update(namespace[key] = info.value); break; default: namespace[key] = info.value; } }); }(namespace)); res({ worker: worker, namespace: namespace }); } else { var message = event.data.message; var id = message.id; var promise = messages[id]; delete messages[id]; if (message.hasOwnProperty('error')) { var error, facade = message.error; if (facade.hasOwnProperty('source')) error = facade.source; else { error = new Error(facade.message); error.stack = facade.stack; } promise.reject(error); } else promise.resolve(message.result); } }); worker.postMessage({channel: channel}); }); } export default workway; ================================================ FILE: esm/remoted.js ================================================ function remoted(object) { return function $(object, current, remote) { Object.keys(object).forEach(function (key) { var value = object[key]; var path = current.concat(key); if (typeof value === 'function') { remote[key] = /^[A-Z]/.test(key) ? { type: 'class', path: path, methods: Object.getOwnPropertyNames(value.prototype) .filter(no(['constructor'])) .concat('destroy'), statics: Object.getOwnPropertyNames(value) .filter(no([ 'arguments', 'callee', 'caller', 'length', 'name', 'prototype' ])) .reduce( function (info, key) { if (typeof value[key] === 'function') { info.methods.push(key); } else { info.values.push([key, value[key]]); } return info; }, { methods: [], values: [] } ) } : { type: 'function', path: path }; } else if (remote.toString.call(value) === '[object Object]') { remote[key] = { type: 'object', path: path, value: {} }; $(value, path, remote[key].value); } else if (value !== void 0) { remote[key] = { type: 'any', path: path, value: value }; } }); return remote; }(object, [], {}); function no(within) { return function (what) { return within.indexOf(what) < 0; }; } } export default remoted; ================================================ FILE: examples/workway-node/README.md ================================================ # Node.js workway demo This demo shows CPUs and current memory consumption. ```sh npm install # now open that localhost page ``` ================================================ FILE: examples/workway-node/index.js ================================================ const path = require('path'); const express = require('express'); const workway = require('workway/node'); // which folder should be reachable from the Web ? workway.authorize(path.join(__dirname, 'workers')); // create an app through workway const app = workway.app(express()); app.use(express.static(path.join(__dirname, 'www'))); app.listen(process.env.PORT || 8080, function () { console.log(`http://localhost:${this.address().port}/`); }); ================================================ FILE: examples/workway-node/package.json ================================================ { "private": true, "description": "A basic workway driven NodeJS example", "scripts": { "postinstall": "npm start", "start": "node index.js" }, "author": "Andrea Giammarchi", "license": "ISC", "dependencies": { "express": "^4.16.3", "workway": "^0.5.2" } } ================================================ FILE: examples/workway-node/workers/os.js ================================================ const workway = require('workway'); // export any namespace or even modules workway(require('os')); ================================================ FILE: examples/workway-node/www/index.html ================================================ workway meets NodeJS ================================================ FILE: examples/workway-node/www/js/3rd/hyperhtml.js ================================================ /*! (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 { // cpus are not going to change (not the showed part) const cpus = await os.cpus(); // grab other info and render the view grabAndShow(); function grabAndShow() { Promise.all([ os.freemem(), os.totalmem() ]).then(render); } // show all the details function render([freemem, totalmem]) { hyperHTML(document.body)` Summary of ${cpus.length} CPU${cpus.length === 1 ? '' : 's'}
    ${cpus.map(cpu => hyperHTML(cpu)`
  • ${cpu.model}
  • `)}

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

`; // update the same view in a second setTimeout(grabAndShow, 1000); } }); ================================================ FILE: examples/workway-node/www/js/poly/es6-promise.js ================================================ !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 ================================================ FILE: examples/workway-promise-reject/index.js ================================================ var PORT = process.env.PORT || 3000; var path = require('path'); var express = require('express'); var workway = require('workway/node').authorize( path.resolve(__dirname, 'workers') ); var app = workway.app(express()); app.get('/', function (req, res) { res.writeHead(200, 'OK', { 'Content-Type': 'text/html; charset=utf-8' }); res.end(require('fs').readFileSync(path.resolve(__dirname, 'index.html'))); }); app.listen(PORT, () => { console.log('listening on http://localhost:' + PORT); }); ================================================ FILE: examples/workway-promise-reject/package.json ================================================ { "name": "workway-demo", "private": true, "dependencies": { "express": "^4.16.3", "workway": "^0.5.2" }, "scripts": { "start": "node index.js", "postinstall": "npm start" }, "author": "Andrea Giammarchi", "license": "ISC" } ================================================ FILE: examples/workway-promise-reject/workers/index.js ================================================ const util = require('util'); const workway = require('workway'); function foo(message, callback) { callback(42); // forcing always an error } const fooPromisified = util.promisify(foo); workway({ fooPromisified }); ================================================ FILE: index.js ================================================ function workway(file) {'use strict'; /*! (c) 2018 Andrea Giammarchi (ISC) */ return new Promise(function (res) { function uid() { return ++i + Math.random(); } var i = 0; var channel = uid(); var messages = {}; var worker = typeof file === 'string' ? new Worker(file) : file; worker.addEventListener('message', function (event) { if (event.data.channel !== channel) return; event.stopImmediatePropagation(); var namespace = event.data.namespace; if (namespace) { var Class = function (info) { var path = info.path; var methods = info.methods; var statics = info.statics; var wm = new WeakMap; function RemoteClass() { wm.set(this, uid()); } methods.forEach(function (method) { RemoteClass.prototype[method] = function () { return send({ args: slice.call(arguments), path: path, method: method, object: { id: wm.get(this), value: this } }); }; }); statics.methods.forEach(function (method) { RemoteClass[method] = function () { return send({ args: slice.call(arguments), path: path, method: method }); }; }); statics.values.forEach(function (pair) { RemoteClass[pair[0]] = pair[1]; }); return RemoteClass; }; var callback = function (path) { return function remoteCallback() { return send({ args: slice.call(arguments), path: path }); }; }; var send = function (message) { var resolve, reject; var promise = new Promise(function (res, rej) { resolve = res; reject = rej; }); promise.resolve = resolve; promise.reject = reject; messages[message.id = uid()] = promise; worker.postMessage({ channel: channel, message: message }); return promise; }; var slice = [].slice; (function update(namespace) { Object.keys(namespace).forEach(function (key) { var info = namespace[key]; switch (info.type) { case 'class': namespace[key] = Class(info); break; case 'function': namespace[key] = callback(info.path); break; case 'object': update(namespace[key] = info.value); break; default: namespace[key] = info.value; } }); }(namespace)); res({ worker: worker, namespace: namespace }); } else { var message = event.data.message; var id = message.id; var promise = messages[id]; delete messages[id]; if (message.hasOwnProperty('error')) { var error, facade = message.error; if (facade.hasOwnProperty('source')) error = facade.source; else { error = new Error(facade.message); error.stack = facade.stack; } promise.reject(error); } else promise.resolve(message.result); } }); worker.postMessage({channel: channel}); }); } ================================================ FILE: min.js ================================================ function workway(t){"use strict"; /*! (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})})} ================================================ FILE: node/client.js ================================================ window.workway = (function (Worker, workway, SECRET) { /*! Copyright 2018 Andrea Giammarchi - @WebReflection * * Permission to use, copy, modify, and/or distribute this software * for any purpose with or without fee is hereby granted, * provided that the above copyright notice * and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS * ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING * ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, * DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR * ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, * DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, * NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ // ${JSON} var instances = []; var sockets = new WeakMap(); addEventListener( 'beforeunload', function () { while (instances.length) instances[0].terminate(); }, false ); function NodeWorker(ep) { var socket = io({JSON: Flatted}); var self = new EventTarget; self.postMessage = this.postMessage; self.terminate = this.terminate; instances.push(self); sockets.set(self, socket); socket.on(SECRET + ':error', this.onerror.bind(self)); socket.on(SECRET + ':message', this.onmessage.bind(self)); socket.emit(SECRET + ':setup', ep); return self; } function createEvent(type) { var event = document.createEvent('Event'); event.initEvent(type, false, true); event.stopImmediatePropagation = event.stopImmediatePropagation || event.stopPropagation; return event; } NodeWorker.prototype = { onerror: function (error) { var event = createEvent('error'); event.message = error.message; event.stack = error.stack; this.dispatchEvent(event); if (this.onerror) this.onerror(event); }, onmessage: function (data) { var event = createEvent('message'); event.data = data; this.dispatchEvent(event); if (this.onmessage) this.onmessage(event); }, postMessage: function (message) { sockets.get(this).emit(SECRET, message); }, terminate: function () { instances.splice(instances.indexOf(this), 1); sockets.get(this).disconnect(); } }; return function (file) { if (/^node:\/\/([\w._-]+)/.test(file)) { file = RegExp.$1; window.Worker = NodeWorker; var promise = workway(file); window.Worker = Worker; return promise; } else return workway(file); }; }(Worker, workway, '${SECRET}')); ================================================ FILE: node/index.js ================================================ "use strict"; // used core modules var EventEmitter = require('events').EventEmitter; var crypto = require('crypto'); var fs = require('fs'); var http = require('http'); var path = require('path'); var vm = require('vm'); // extra dependencies var Flatted = require('flatted'); var pocketIO = require('pocket.io'); var workerjs = fs.readFileSync( path.resolve(__dirname, '..', 'worker.js') ).toString(); // used as communication channel var SECRET = crypto.randomBytes(32).toString('hex'); // the client side is enriched at runtime var jsContent = fs.readFileSync(path.join(__dirname, 'client.js')) .toString() .replace( /\$\{(SECRET|JSON)\}/g, function ($0, $1) { return this[$1]; }.bind({ SECRET: SECRET, JSON: fs.readFileSync(require.resolve('flatted/min.js')).toString() }) ); var workers = ''; process.on('unhandledRejection', uncaught); process.on('uncaughtException', uncaught); // return a new Worker sandbox function createSandbox(filename, socket) { var self = new EventEmitter; var listeners = new WeakMap; self.addEventListener = function (type, listener) { if (!listeners.has(listener)) { var facade = function (event) { if (!event.canceled) listener.apply(this, arguments); }; listeners.set(listener, facade); self.on(type, facade); } }; self.removeEventListener = function (type, listener) { self.removeListener(type, listeners.get(listener)); }; self.__filename = filename; self.__dirname = path.dirname(filename); self.postMessage = function postMessage(data) { message(socket, data); }; self.console = console; self.process = process; self.Buffer = Buffer; self.clearImmediate = clearImmediate; self.clearInterval = clearInterval; self.clearTimeout = clearTimeout; self.setImmediate = setImmediate; self.setInterval = setInterval; self.setTimeout = setTimeout; self.module = module; self.global = self; self.self = self; self.require = function (file) { switch (true) { case file === 'workway': return self.workway; case /^[./]/.test(file): file = path.resolve(self.__dirname, file); default: return require(file); } }; return self; } function cleanedStack(stack) { return ''.replace.call(stack, /:uid-\d+-[a-f0-9]{16}/g, ''); } // notify the socket there was an error function error(socket, err) { socket.emit(SECRET + ':error', { message: err.message, stack: cleanedStack(err.stack) }); } // send serialized data to the client function message(socket, data) { socket.emit(SECRET + ':message', data); } // used to send /node-worker.js client file function responder(request, response, next) { response.writeHead(200, 'OK', { 'Content-Type': 'application/javascript' }); response.end(jsContent); if (next) next(); } uid.i = 0; uid.map = Object.create(null); uid.delete = function (sandbox) { Object.keys(uid.map).some(function (key) { var found = uid.map[key] === sandbox; if (found) delete uid.map[key]; return found; }); }; function uid(filename, socket) { var id = filename + ':uid-'.concat(++uid.i, '-', crypto.randomBytes(8).toString('hex')); uid.map[id] = socket; return id; } function uncaught(err) { console.error(err); if (/([\S]+?(:uid-\d+-[a-f0-9]{16}))/.test(err.stack)) { var socket = uid.map[RegExp.$1]; if (socket) error(socket, err); } } function Event(data) { this.canceled = false; this.data = data; } Event.prototype.stopImmediatePropagation = function () { this.canceled = true; }; module.exports = { authorize: function (folder) { if (workers.length) throw new Error('workway already authorized'); workers = folder; return this; }, app: function (app) { var io; var native = app instanceof http.Server; if (native) { io = pocketIO(app, {JSON: Flatted}); var request = app._events.request; app._events.request = function (req) { return /^\/workway@node\.js(?:\?|$)/.test(req.url) ? responder.apply(this, arguments) : request.apply(this, arguments); }; } else { var wrap = http.Server(app); io = pocketIO(wrap, {JSON: Flatted}); app.get('/workway@node.js', responder); Object.defineProperty(app, 'listen', { configurable: true, value: function () { wrap.listen.apply(wrap, arguments); return app; } }); } io.on('connection', function (socket) { var sandbox; var queue = []; function message(data) { if (sandbox) { try { var event = new Event(data); sandbox.emit('message', event); if ('onmessage' in sandbox && !event.canceled) { sandbox.onmessage(event); } } catch(err) { error(socket, err); } } else queue.push(data); } socket.on(SECRET, message); socket.on(SECRET + ':setup', function (worker) { var filename = path.resolve(path.join(workers, worker)); if (filename.indexOf(workers)) { error(socket, { message: 'Unauthorized worker: ' + worker, stack: '' }); } else { fs.readFile(filename, function (err, content) { if (err) { error(socket, { message: 'Worker not found: ' + worker, stack: err.stack }); } else { sandbox = createSandbox(filename, socket); vm.createContext(sandbox); vm.runInContext(workerjs, sandbox); vm.runInContext(content, sandbox, { filename: uid(worker, socket), displayErrors: true }); while (queue.length) { setTimeout(message, queue.length * 100, queue.pop()); } } }); } }); socket.on('disconnect', function () { uid.delete(socket); sandbox = null; }); }); return app; } }; ================================================ FILE: package.json ================================================ { "name": "workway", "version": "0.5.5", "description": "A general purpose, Web Worker driven, remote namespace with classes and methods.", "unpkg": "min.js", "main": "cjs/index.js", "module": "esm/index.js", "scripts": { "$": "npm-dollar", "build": "npm-dollar build", "bundle": "npm-dollar bundle", "size": "npm-dollar size", "test": "npm-dollar test" }, "$": { "build": [ "$ remoted", "$ bundle", "$ test", "$ size" ], "bundle": { "cjs": [ "cp index.js cjs/", "echo 'module.exports = workway;' >> cjs/index.js" ], "esm": [ "cp index.js esm/", "echo 'export default workway;' >> esm/index.js" ], "min": "uglifyjs index.js --comments=/^!/ -m -c -o min.js", "node": [ [ "node -e 'fs.writeFileSync(\"worker.js\",", "fs.readFileSync(\"partial/worker.js\").toString().replace(/^(\\s*)\\/\\/js:(\\w+)/gm,", "(o,s,k)=>fs.readFileSync(k+\".js\").toString().trim().replace(/^/gm,s)))'" ] ] }, "remoted": { "cjs": [ "cp remoted.js cjs/", "echo 'module.exports = remoted;' >> cjs/remoted.js" ], "esm": [ "cp remoted.js esm/", "echo 'export default remoted;' >> esm/remoted.js" ] }, "size": [ [ "cat index.js |", "wc -c;cat min.js |", "wc -c;gzip -c9 min.js |", "wc -c;cat min.js |", "brotli |", "wc -c && rm -f min.js.br" ] ], "test": [ "cd test", "node index.js", "echo $(tput bold)OK$(tput sgr0)" ] }, "author": "Andrea Giammarchi", "license": "ISC", "devDependencies": { "express": "^4.17.1", "npm-dollar": "^2.2.1", "uglify-js": "^3.6.0" }, "repository": { "type": "git", "url": "git+https://github.com/WebReflection/workway.git" }, "keywords": [ "web", "worker", "remote", "namespace", "driven" ], "bugs": { "url": "https://github.com/WebReflection/workway/issues" }, "homepage": "https://github.com/WebReflection/workway#readme", "dependencies": { "flatted": "^2.0.0", "pocket.io": "^0.1.4" } } ================================================ FILE: partial/worker.js ================================================ (function () {'use strict'; /*! (c) 2018 Andrea Giammarchi (ISC) */ function walkThrough(O, K) { return O[K]; } var namespace; var channels = {}; var instances = {}; var onceExposed = new Promise(function (resolve) { self.workway = function workway(exposed) { return Promise.resolve(exposed).then(function (result) { namespace = result; resolve(remoted(result)); }); }; //js:remoted }); self.addEventListener('message', function (event) { var method; var data = event.data; var channel = data.channel; var message = data.message; if (channels[channel]) { event.stopImmediatePropagation(); var id = message.id; var path = message.path; var args = message.args; var resolved = function (result) { send({result: result}); }; var rejected = function (error) { if ( error != null && typeof error === 'object' && 'message' in error ) send({error: { stack: error.stack, message: error.message }}); else send({error: {source: error}}); }; var send = function (message) { message.id = id; self.postMessage({ channel: channel, message: message }); }; try { if (message.hasOwnProperty('method')) { method = message.method; var Class = path.reduce(walkThrough, namespace); if (!Class) return rejected('Unknown Class ' + path.join('.')); if (message.hasOwnProperty('object')) { var object = message.object; var instance = instances[object.id] || (instances[object.id] = new Class); if (method === 'destroy') delete instances[object.id]; else { Object.keys(object.value) .forEach(function (key) { instance[key] = object.value[key]; }); Promise.resolve(instance[method].apply(instance, args)) .then(resolved, rejected); } } else { Promise.resolve(Class[method].apply(Class, args)) .then(resolved, rejected); } } else { var context = path.slice(0, -1).reduce(walkThrough, namespace); if (!context) return rejected('Unknown namespace ' + path.slice(0, -1).join('.')); method = path[path.length - 1]; if (typeof context[method] !== 'function') return rejected('Unknown method ' + path.join('.')); Promise.resolve(context[method].apply(context, args)) .then(resolved, rejected); } } catch(error) { rejected(error); } } else if (/^(-?\d+\.\d+)$/.test(channel)) { channels[channel] = true; event.stopImmediatePropagation(); onceExposed.then(function (namespace) { self.postMessage({ channel: channel, namespace: namespace }); }); } }); }()); ================================================ FILE: remoted.js ================================================ function remoted(object) { return function $(object, current, remote) { Object.keys(object).forEach(function (key) { var value = object[key]; var path = current.concat(key); if (typeof value === 'function') { remote[key] = /^[A-Z]/.test(key) ? { type: 'class', path: path, methods: Object.getOwnPropertyNames(value.prototype) .filter(no(['constructor'])) .concat('destroy'), statics: Object.getOwnPropertyNames(value) .filter(no([ 'arguments', 'callee', 'caller', 'length', 'name', 'prototype' ])) .reduce( function (info, key) { if (typeof value[key] === 'function') { info.methods.push(key); } else { info.values.push([key, value[key]]); } return info; }, { methods: [], values: [] } ) } : { type: 'function', path: path }; } else if (remote.toString.call(value) === '[object Object]') { remote[key] = { type: 'object', path: path, value: {} }; $(value, path, remote[key].value); } else if (value !== void 0) { remote[key] = { type: 'any', path: path, value: value }; } }); return remote; }(object, [], {}); function no(within) { return function (what) { return within.indexOf(what) < 0; }; } } ================================================ FILE: test/background.js ================================================ // a Blackberry gotcha, no console in workers if (!self.console) self.console = {log: function () {}, error: function () {}}; // import the polyfill you prefer for IE11 or IE10 if (!self.Promise) importScripts('https://cdn.jsdelivr.net/npm/es6-promise@4/dist/es6-promise.auto.min.js'); if (!self.WeakMap) importScripts('https://unpkg.com/poorlyfills@0.1.1/min.js'); // to avoid any possible issue with messages // import the remote utility n the top of your worker importScripts('../worker.js'); // ES2015 classes would work too // ( not using class for IE11 tests ) function Test() {} Test.method = function () { console.log('Test.method'); console.log('arguments', [].slice.call(arguments)); return Math.random(); }; Test.prototype.method = function () { console.log('Test.prototype.method'); console.log('instance', JSON.stringify(this)); console.log('arguments', [].slice.call(arguments)); return Math.random(); }; // expose a namespace with serializable data // but also classes and utilities as methods/functions workway({ test: 123, array: [1, 2, 3], object: {a: 'a'}, nested: { method: function () { console.log('method'); console.log('arguments', [].slice.call(arguments)); return Math.random(); }, Test: Test } }); // you can regularly post any sort of message, or listen // to anything you want to self.addEventListener('message', function (event) { console.log(event.type, event.data); if (event.data.hasOwnProperty('echo')) self.postMessage(event.data.echo); }); ================================================ FILE: test/circular/analyzer.js ================================================ importScripts('https://unpkg.com/workway/worker.js'); workway({ analyze(circular) { return circular; } }); ================================================ FILE: test/circular/index.html ================================================ ================================================ FILE: test/client.js ================================================ workway('./background.js').then(function (info) { // {worker, namespace} var worker = info.worker; var namespace = info.namespace; // you can either use addEventListener // or onmessage and onerror and // these will never receive remote events, // only user messages, and same goes if you post messages // the remote logic won't ever be affected worker.onmessage = function (event) { showData(event.data); }; worker.addEventListener('message', console.log.bind(console)); // you can also send regular messages // without affecting namespace operations worker.postMessage({echo: 'hello remote'}); // classes can have static methods // static values, and regular prototypal methods // however there are few limitations such inheritance // and constructor arguments, which is always, and only // the unique identifier used to pair local/remote instances var instance = new namespace.nested.Test; // properties can be added, as long as these are serializable instance.test = Math.random(); // and every method of the class returns a Promise // that will resolve once the instance has been updated // (properties) and the method invoked, // with serializable arguments instance.method(1, 2, 3) .then(showData, console.error.bind(console)) .then(function () { instance.destroy(); }); function showData(data) { document.body.appendChild( document.createElement('pre') ).textContent = JSON.stringify(data, null, ' '); } }); ================================================ FILE: test/foo.html ================================================ ================================================ FILE: test/foo.js ================================================ var PORT = process.env.PORT || 3000; var path = require('path'); var express = require('express'); var workway = require('../node').authorize( path.resolve(__dirname, 'workers') ); var app = workway.app(express()); app.get('/', function (req, res) { res.writeHead(200, 'OK', { 'Content-Type': 'text/html; charset=utf-8' }); res.end(require('fs').readFileSync(path.resolve(__dirname, 'foo.html'))); }); app.get('/workway.js', function (req, res) { res.writeHead(200, 'OK', { 'Content-Type': 'application/javascript; charset=utf-8' }); res.end(require('fs').readFileSync(path.resolve(__dirname, '..', 'min.js'))); }); app.listen(PORT, () => { console.log('listening on http://localhost:' + PORT); }); ================================================ FILE: test/index.html ================================================ ================================================ FILE: test/index.js ================================================ require('./webworker'); global.workway = require('../cjs/index'); require('./client'); ================================================ FILE: test/node.html ================================================ ================================================ FILE: test/node.js ================================================ var PORT = process.env.PORT || 3000; var path = require('path'); var express = require('express'); var workway = require('../node').authorize( path.resolve(__dirname, 'workers') ); var app = workway.app(express()); app.get('/', function (req, res) { res.writeHead(200, 'OK', { 'Content-Type': 'text/html; charset=utf-8' }); res.end(require('fs').readFileSync(path.resolve(__dirname, 'node.html'))); }); app.get('/workway.js', function (req, res) { res.writeHead(200, 'OK', { 'Content-Type': 'application/javascript; charset=utf-8' }); res.end(require('fs').readFileSync(path.resolve(__dirname, '..', 'index.js'))); }); app.listen(PORT, () => { console.log('listening on http://localhost:' + PORT); }); ================================================ FILE: test/webworker.js ================================================ const workers = []; class Event { constructor(data) { this._stopImmediatePropagation = false; this.type = 'message'; this.data = data; } stopImmediatePropagation() { this._stopImmediatePropagation = true; } } global.Worker = class Worker extends require('events').EventEmitter { constructor(file) { workers.push(super()); require(file); } addEventListener(type, listener) { this.on(type, listener); } postMessage(data) { process.emit('message', new Event(data)); } }; global.document = { body: { appendChild: Object }, createElement() { return {set textContent(value) { console.log(value); }}; } }; global.importScripts = require; global.self = new Proxy( { addEventListener(type, listener) { process.on(type, listener); }, postMessage(data) { workers.forEach(worker => worker.emit('message', new Event(data))); } }, { get: (self, key) => self[key] || global[key], set: (self, key, value) => { global[key] = value; return true; } } ); ================================================ FILE: test/workers/foo.js ================================================ const util = require('util'); const workway = require('workway'); function foo(message, callback) { callback(42); // forcing always an error } const fooPromisified = util.promisify(foo); workway({ fooPromisified }); ================================================ FILE: test/workers/other.js ================================================ console.log(__filename); ================================================ FILE: test/workers/test.js ================================================ var workway = require('workway'); // require('./other'); workway({ os: require('os'), ping: function () { self.postMessage('pong'); } }); self.addEventListener('message', function (event) { console.log(event.data); }); ================================================ FILE: worker.js ================================================ (function () {'use strict'; /*! (c) 2018 Andrea Giammarchi (ISC) */ function walkThrough(O, K) { return O[K]; } var namespace; var channels = {}; var instances = {}; var onceExposed = new Promise(function (resolve) { self.workway = function workway(exposed) { return Promise.resolve(exposed).then(function (result) { namespace = result; resolve(remoted(result)); }); }; function remoted(object) { return function $(object, current, remote) { Object.keys(object).forEach(function (key) { var value = object[key]; var path = current.concat(key); if (typeof value === 'function') { remote[key] = /^[A-Z]/.test(key) ? { type: 'class', path: path, methods: Object.getOwnPropertyNames(value.prototype) .filter(no(['constructor'])) .concat('destroy'), statics: Object.getOwnPropertyNames(value) .filter(no([ 'arguments', 'callee', 'caller', 'length', 'name', 'prototype' ])) .reduce( function (info, key) { if (typeof value[key] === 'function') { info.methods.push(key); } else { info.values.push([key, value[key]]); } return info; }, { methods: [], values: [] } ) } : { type: 'function', path: path }; } else if (remote.toString.call(value) === '[object Object]') { remote[key] = { type: 'object', path: path, value: {} }; $(value, path, remote[key].value); } else if (value !== void 0) { remote[key] = { type: 'any', path: path, value: value }; } }); return remote; }(object, [], {}); function no(within) { return function (what) { return within.indexOf(what) < 0; }; } } }); self.addEventListener('message', function (event) { var method; var data = event.data; var channel = data.channel; var message = data.message; if (channels[channel]) { event.stopImmediatePropagation(); var id = message.id; var path = message.path; var args = message.args; var resolved = function (result) { send({result: result}); }; var rejected = function (error) { if ( error != null && typeof error === 'object' && 'message' in error ) send({error: { stack: error.stack, message: error.message }}); else send({error: {source: error}}); }; var send = function (message) { message.id = id; self.postMessage({ channel: channel, message: message }); }; try { if (message.hasOwnProperty('method')) { method = message.method; var Class = path.reduce(walkThrough, namespace); if (!Class) return rejected('Unknown Class ' + path.join('.')); if (message.hasOwnProperty('object')) { var object = message.object; var instance = instances[object.id] || (instances[object.id] = new Class); if (method === 'destroy') delete instances[object.id]; else { Object.keys(object.value) .forEach(function (key) { instance[key] = object.value[key]; }); Promise.resolve(instance[method].apply(instance, args)) .then(resolved, rejected); } } else { Promise.resolve(Class[method].apply(Class, args)) .then(resolved, rejected); } } else { var context = path.slice(0, -1).reduce(walkThrough, namespace); if (!context) return rejected('Unknown namespace ' + path.slice(0, -1).join('.')); method = path[path.length - 1]; if (typeof context[method] !== 'function') return rejected('Unknown method ' + path.join('.')); Promise.resolve(context[method].apply(context, args)) .then(resolved, rejected); } } catch(error) { rejected(error); } } else if (/^(-?\d+\.\d+)$/.test(channel)) { channels[channel] = true; event.stopImmediatePropagation(); onceExposed.then(function (namespace) { self.postMessage({ channel: channel, namespace: namespace }); }); } }); }());