├── .gitignore ├── .npmignore ├── LICENSE.txt ├── README.md ├── client.js ├── package.json ├── server.js ├── test ├── express.js ├── node.js └── test.html └── workers ├── consolemd.js └── echo.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules/* 2 | test/* 3 | workers/* 4 | .gitignore 5 | package-lock.json -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (c) 2017, 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 | # node-worker: DEPRECATED thanks to [coincident]([https://github.com/WebReflection/workway#workway--](https://github.com/WebReflection/coincident#coincidentserver)) 🎉 2 | 3 | [![License: ISC](https://img.shields.io/badge/License-ISC-yellow.svg)](https://opensource.org/licenses/ISC) [![donate](https://img.shields.io/badge/$-donate-ff69b4.svg?maxAge=2592000&style=flat)](https://github.com/WebReflection/donate) 4 | 5 | Web Worker like API to drive NodeJS files 6 | 7 | `npm install @webreflection/node-worker` 8 | 9 | ### Concept 10 | 11 | The aim of this project is to provide an alternative to [Electron](https://electron.atom.io/) environment. 12 | This might be particularly useful in those platforms with constrains such Raspberry Pi Zero or 1. 13 | 14 | The module is based on the standard [Web Worker](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers) API. 15 | 16 | You can `postMessage(data)` and receive `onmessage = (data) => {}` on both client and server. 17 | 18 | All workers files must be inside a `workers` directory within the application folder. 19 | 20 | Every worker is a [sandboxed VM](https://nodejs.org/api/vm.html) and it runs on the backend: nothing is shared directly with the browser. 21 | 22 | ### Basic Example 23 | 24 | **NodeJS** 25 | ```js 26 | var index = require('fs').readFileSync(__dirname + '/index.html'); 27 | 28 | var http = require('http').createServer(handler); 29 | var nodeWorker = require('@webreflection/node-worker'); 30 | 31 | var app = nodeWorker(http); 32 | app.listen(process.env.PORT); 33 | 34 | function handler(req, res) { 35 | res.writeHead(200, 'OK', { 36 | 'Content-Type': 'text/html' 37 | }); 38 | res.end(index); 39 | } 40 | ``` 41 | 42 | **Express** 43 | ```js 44 | var index = require('fs').readFileSync(__dirname + '/index.html'); 45 | 46 | var express = require('express'); 47 | var nodeWorker = require('@webreflection/node-worker'); 48 | 49 | var app = nodeWorker(express()); 50 | app.get('/', handler); 51 | app.listen(process.env.PORT); 52 | 53 | function handler(req, res) { 54 | res.writeHead(200, 'OK', { 55 | 'Content-Type': 'text/html' 56 | }); 57 | res.end(index); 58 | } 59 | ``` 60 | 61 | **Demo index.html** 62 | ```html 63 | 64 | 65 | 66 | 67 | 77 | ``` 78 | 79 | **workers/echo.js** 80 | ```js 81 | // simple echo 82 | // when some data arrives 83 | // same data goes back 84 | onmessage = function (e) { 85 | postMessage(e.data); 86 | }; 87 | ``` 88 | 89 | You can clone and run `npm test` after an `npm install`. 90 | -------------------------------------------------------------------------------- /client.js: -------------------------------------------------------------------------------- 1 | var NodeWorker = (function (SECRET, io, sockets) {'use strict'; 2 | 3 | // ${JSON} 4 | 5 | var instances = []; 6 | var sPO = Object.setPrototypeOf || 7 | function (o, p) { 8 | o.__proto__ = p; 9 | return o; 10 | }; 11 | 12 | function error(data) { 13 | this.onerror(sPO(JSON.parse(data), Error.prototype)); 14 | } 15 | 16 | function message(data) { 17 | this.onmessage(JSON.parse(data)); 18 | } 19 | 20 | function NodeWorker(worker) { 21 | /*! Copyright 2017 Andrea Giammarchi - @WebReflection 22 | * 23 | * Permission to use, copy, modify, and/or distribute this software 24 | * for any purpose with or without fee is hereby granted, 25 | * provided that the above copyright notice 26 | * and this permission notice appear in all copies. 27 | * 28 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS 29 | * ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING 30 | * ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. 31 | * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, 32 | * DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR 33 | * ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, 34 | * DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, 35 | * NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 36 | * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 37 | */ 38 | if (-1 < instances.indexOf(this)) { 39 | throw new Error('invalid invoke'); 40 | } 41 | var socket = io(); 42 | instances.push(this); 43 | sockets.set(this, socket); 44 | socket.on(SECRET + ':error', error.bind(this)); 45 | socket.on(SECRET + ':message', message.bind(this)); 46 | socket.emit(SECRET + ':setup', worker); 47 | } 48 | 49 | Object.defineProperties( 50 | NodeWorker.prototype, 51 | { 52 | postMessage: { 53 | configurable: true, 54 | value: function postMessage(message) { 55 | sockets.get(this).emit(SECRET, JSON.stringify(message)); 56 | } 57 | }, 58 | terminate: { 59 | configurable: true, 60 | value: function terminate() { 61 | instances.splice(instances.indexOf(this), 1); 62 | sockets.get(this).destroy(); 63 | } 64 | }, 65 | onerror: { 66 | configurable: true, 67 | writable: true, 68 | value: function onerror() {} 69 | }, 70 | onmessage: { 71 | configurable: true, 72 | writable: true, 73 | value: function onmessage() {} 74 | } 75 | } 76 | ); 77 | 78 | addEventListener( 79 | 'beforeunload', 80 | function () { 81 | while (instances.length) instances[0].terminate(); 82 | }, 83 | false 84 | ); 85 | 86 | return NodeWorker; 87 | 88 | }( 89 | '${SECRET}', 90 | io, 91 | typeof WeakMap === 'undefined' ? 92 | { 93 | get: function (obj) { return obj.__NodeWorker; }, 94 | set: function (obj, value) { 95 | Object.defineProperty(obj, '__NodeWorker', { 96 | configurable: true, 97 | value: value 98 | }); 99 | } 100 | } : 101 | new WeakMap() 102 | )); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@webreflection/node-worker", 3 | "version": "0.4.1", 4 | "description": "Web Worker like API to drive NodeJS files", 5 | "main": "server.js", 6 | "scripts": { 7 | "test": "node test/express.js" 8 | }, 9 | "author": "Andrea Giammarchi", 10 | "license": "ISC", 11 | "devDependencies": { 12 | "consolemd": "^0.1.2", 13 | "express": "^4.15.3" 14 | }, 15 | "dependencies": { 16 | "flatted": "^0.2.2", 17 | "socket.io": "^2.0.3", 18 | "socket.io-client": "^2.0.3" 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "git+https://github.com/WebReflection/node-worker.git" 23 | }, 24 | "keywords": [ 25 | "node", 26 | "nodejs", 27 | "electron", 28 | "web", 29 | "worker", 30 | "alternative" 31 | ], 32 | "bugs": { 33 | "url": "https://github.com/WebReflection/node-worker/issues" 34 | }, 35 | "homepage": "https://github.com/WebReflection/node-worker#readme" 36 | } 37 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | // core modules 2 | var crypto = require('crypto'); 3 | var fs = require('fs'); 4 | var http = require('http'); 5 | var path = require('path'); 6 | var vm = require('vm'); 7 | 8 | // dependencies 9 | var socketIO = require('socket.io'); 10 | var JSON = require('flatted'); 11 | 12 | // local constants / variables 13 | // used as communication channel 14 | var SECRET = crypto.randomBytes(32).toString('hex'); 15 | 16 | var jsClient = { 17 | SECRET: SECRET, 18 | JSON: fs.readFileSync(require.resolve('flatted/min.js')) 19 | .toString() 20 | .replace( 21 | /var \w+\s*=/, 22 | 'var JSON = (function(JSON){return ') + '}(window.JSON));' 23 | }; 24 | 25 | var jsContent = fs.readFileSync(path.join(__dirname, 'client.js')) 26 | .toString() 27 | .replace(/\$\{(SECRET|JSON)\}/g, function ($0, $1) { 28 | return jsClient[$1]; 29 | }); 30 | 31 | var workers = path.resolve(path.join(process.cwd(), 'workers')); 32 | 33 | // return a new Worker sandbox 34 | function createSandbox(filename, socket) { 35 | var sandbox = { 36 | __filename: filename, 37 | __dirname: path.dirname(filename), 38 | postMessage: function postMessage(data) { message(socket, data); }, 39 | console: console, 40 | process: process, 41 | Buffer: Buffer, 42 | clearImmediate: clearImmediate, 43 | clearInterval: clearInterval, 44 | clearTimeout: clearTimeout, 45 | setImmediate: setImmediate, 46 | setInterval: setInterval, 47 | setTimeout: setTimeout, 48 | module: module, 49 | require: require 50 | }; 51 | return (sandbox.global = sandbox); 52 | } 53 | 54 | // notify the socket there was an error 55 | function error(socket, error) { 56 | socket.emit(SECRET + ':error', JSON.stringify(error)); 57 | } 58 | 59 | // send serialized data to the client 60 | function message(socket, data) { 61 | socket.emit(SECRET + ':message', JSON.stringify({data: data})); 62 | } 63 | 64 | // used to send /node-worker.js client file 65 | function responder(request, response, next) { 66 | response.writeHead(200, 'OK', { 67 | 'Content-Type': 'application/javascript' 68 | }); 69 | response.end(jsContent); 70 | if (next) next(); 71 | } 72 | 73 | uid.i = 0; 74 | uid.map = Object.create(null); 75 | uid.delete = function (sandbox) { 76 | Object.keys(uid.map).some(function (key) { 77 | var found = uid.map[key] === sandbox; 78 | if (found) delete uid.map[key]; 79 | return found; 80 | }); 81 | }; 82 | function uid(filename, socket) { 83 | var id = filename + ':uid-'.concat(++uid.i, '-', crypto.randomBytes(8).toString('hex')); 84 | uid.map[id] = socket; 85 | return id; 86 | } 87 | 88 | process.on('uncaughtException', function (err) { 89 | if (/\(([\S]+?(:uid-\d+-[a-f0-9]{16}))/.test(err.stack)) { 90 | var socket = uid.map[RegExp.$1]; 91 | var secret = RegExp.$2; 92 | if (socket) { 93 | error(socket, { 94 | message: err.message, 95 | stack: ''.replace.call(err.stack, secret, '') 96 | }); 97 | } 98 | } 99 | }); 100 | 101 | module.exports = function (app) { 102 | var io; 103 | var native = app instanceof http.Server; 104 | if (native) { 105 | io = socketIO(app); 106 | var request = app._events.request; 107 | app._events.request = function (req) { 108 | return /^\/node-worker\.js(?:\?|$)/.test(req.url) ? 109 | responder.apply(this, arguments) : 110 | request.apply(this, arguments); 111 | }; 112 | } else { 113 | var wrap = http.Server(app); 114 | io = socketIO(wrap); 115 | app.get('/node-worker.js', responder); 116 | Object.defineProperty(app, 'listen', { 117 | configurable: true, 118 | value: function () { 119 | wrap.listen.apply(wrap, arguments); 120 | return app; 121 | } 122 | }); 123 | } 124 | io.on('connection', function (socket) { 125 | var sandbox; 126 | var queue = []; 127 | function message(data) { 128 | if (sandbox) { 129 | if ('onmessage' in sandbox) { 130 | try { 131 | sandbox.onmessage({data: JSON.parse(data)}); 132 | } catch(err) { 133 | error(socket, {message: err.message, stack: err.stack}); 134 | } 135 | } 136 | } 137 | else queue.push(data); 138 | } 139 | socket.on(SECRET, message); 140 | socket.on(SECRET + ':setup', function (worker) { 141 | var filename = path.resolve(path.join(workers, worker)); 142 | if (filename.indexOf(workers)) { 143 | error(socket, { 144 | message: 'Unauthorized worker: ' + worker, 145 | stack: '' 146 | }); 147 | } else { 148 | fs.readFile(filename, function (err, content) { 149 | if (err) { 150 | error(socket, { 151 | message: 'Worker not found: ' + worker, 152 | stack: err.stack 153 | }); 154 | } else { 155 | sandbox = createSandbox(filename, socket); 156 | vm.createContext(sandbox); 157 | vm.runInContext(content, sandbox, { 158 | filename: uid(worker, socket), 159 | displayErrors: true 160 | }); 161 | while (queue.length) { 162 | setTimeout(message, queue.length * 100, queue.pop()); 163 | } 164 | } 165 | }); 166 | } 167 | }); 168 | socket.on('disconnect', function () { 169 | uid.delete(socket); 170 | sandbox = null; 171 | }); 172 | }); 173 | return app; 174 | }; 175 | -------------------------------------------------------------------------------- /test/express.js: -------------------------------------------------------------------------------- 1 | var PORT = process.env.PORT || 3000; 2 | 3 | var express = require('express'); 4 | var nodeWorker = require('../server.js'); 5 | 6 | var app = nodeWorker(express()); 7 | app.get('/', function (req, res) { 8 | res.writeHead(200, 'OK', { 9 | 'Content-Type': 'text/html' 10 | }); 11 | res.end(require('fs').readFileSync(__dirname + '/test.html')); 12 | }); 13 | app.listen(PORT, () => { 14 | console.log('listening on http://localhost:' + PORT); 15 | }); -------------------------------------------------------------------------------- /test/node.js: -------------------------------------------------------------------------------- 1 | var PORT = process.env.PORT || 3000; 2 | 3 | var http = require('http').createServer(handler); 4 | var nodeWorker = require('../server.js'); 5 | var app = nodeWorker(http); 6 | app.listen(PORT, () => { 7 | console.log('listening on http://localhost:' + PORT); 8 | }); 9 | 10 | function handler(req, res) { 11 | res.writeHead(200, 'OK', { 12 | 'Content-Type': 'text/html' 13 | }); 14 | res.end(require('fs').readFileSync(__dirname + '/test.html')); 15 | } -------------------------------------------------------------------------------- /test/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /workers/consolemd.js: -------------------------------------------------------------------------------- 1 | // require(module) example 2 | var consolemd = require('consolemd'); 3 | 4 | // it will log on node via consolemd 5 | // and its Markdown capabilities 6 | onmessage = function (event) { 7 | consolemd.log(event.data); 8 | }; -------------------------------------------------------------------------------- /workers/echo.js: -------------------------------------------------------------------------------- 1 | // simple echo 2 | // when some data arrives 3 | // same data goes back 4 | onmessage = function (e) { 5 | setTimeout(() => shenanigans(), 1000); 6 | postMessage(e.data); 7 | }; 8 | 9 | process.on('uncaughtException', console.error); 10 | --------------------------------------------------------------------------------