├── .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 | [](https://opensource.org/licenses/ISC) [](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 |
--------------------------------------------------------------------------------