├── .gitignore ├── test ├── worker-master-order.js ├── worker-get-msg.js ├── worker-master-3.js ├── worker-custom-1.js ├── worker-emit-fn-1.js ├── worker-emit-fn-3.js ├── worker-custom-3.js ├── worker-master-1.js ├── worker-custom-2.js ├── worker-emit-fn-2.js ├── worker-master-2.js ├── run-tests.sh ├── worker-to-worker.js ├── worker-master-4.js ├── worker-bad-msg.js ├── fork.js ├── worker-worker-test.js ├── master-test.js ├── custom-hub-test.js └── master-worker-test.js ├── .travis.yml ├── lib ├── index.js ├── listener.js ├── globals.js ├── fork.js └── hub.js ├── package.json ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | -------------------------------------------------------------------------------- /test/worker-master-order.js: -------------------------------------------------------------------------------- 1 | const hub = require('..'); 2 | 3 | hub.emit('the-event'); 4 | hub.emit('after'); 5 | -------------------------------------------------------------------------------- /test/worker-get-msg.js: -------------------------------------------------------------------------------- 1 | const hub = require('..'); 2 | 3 | hub.on('bad', () => { 4 | hub.emit('done'); 5 | }); 6 | -------------------------------------------------------------------------------- /test/worker-master-3.js: -------------------------------------------------------------------------------- 1 | const hub = require('..'); 2 | hub.emitLocal('local', { hi: 2 }); 3 | hub.emitRemote('remote', 'yes!'); 4 | -------------------------------------------------------------------------------- /test/worker-custom-1.js: -------------------------------------------------------------------------------- 1 | const hub = require('..').createHub('one'); 2 | 3 | hub.on('ping', () => { 4 | hub.emit('pong'); 5 | }); 6 | -------------------------------------------------------------------------------- /test/worker-emit-fn-1.js: -------------------------------------------------------------------------------- 1 | const hub = require('..'); 2 | 3 | hub.on('my-done', (done) => { 4 | hub.emit('same-done', done); 5 | }); 6 | -------------------------------------------------------------------------------- /test/worker-emit-fn-3.js: -------------------------------------------------------------------------------- 1 | const hub = require('..'); 2 | 3 | hub.on('bad-good', (good, bad) => { 4 | bad(); 5 | good(); 6 | bad(); 7 | }); 8 | -------------------------------------------------------------------------------- /test/worker-custom-3.js: -------------------------------------------------------------------------------- 1 | const hub = require('..').createHub('tres'); 2 | 3 | hub.on('i am', () => { 4 | hub.emit('nap'); 5 | }); 6 | hub.emitRemote('i am'); 7 | -------------------------------------------------------------------------------- /test/worker-master-1.js: -------------------------------------------------------------------------------- 1 | const hub = require('..'); 2 | 3 | hub.emit('yesmaster', { hello: 'there' }); 4 | hub.once('work', () => { 5 | hub.emit('donemaster'); 6 | }); 7 | -------------------------------------------------------------------------------- /test/worker-custom-2.js: -------------------------------------------------------------------------------- 1 | const hub = require('..'); 2 | const h = hub.createHub('two'); 3 | 4 | hub.on('two', () => { 5 | h.emit('error', ':('); 6 | }); 7 | h.on('two', () => { 8 | h.emit('success'); 9 | }); 10 | -------------------------------------------------------------------------------- /test/worker-emit-fn-2.js: -------------------------------------------------------------------------------- 1 | const hub = require('..'); 2 | 3 | hub.emitRemote('one-worker', 1, (sum) => { 4 | hub.emit('sum', sum); 5 | }, 3); 6 | 7 | hub.on('one-worker', (a, fn, c) => { 8 | fn(a + c); 9 | }); 10 | -------------------------------------------------------------------------------- /test/worker-master-2.js: -------------------------------------------------------------------------------- 1 | const hub = require('..'); 2 | 3 | hub.on('set bar', (value) => { 4 | hub.removeAllListeners('foo bar'); 5 | hub.emit('result', value.toUpperCase()); 6 | }); 7 | hub.get('value', (value) => { 8 | hub.set('foo', value + 42); 9 | }); 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 6 4 | - 8 5 | - 10 6 | notifications: 7 | email: 8 | on_success: change 9 | on_failure: change 10 | sudo: false 11 | after_success: 12 | - npm install -g codecov 13 | - codecov -f coverage/full/lcov.info 14 | -------------------------------------------------------------------------------- /test/run-tests.sh: -------------------------------------------------------------------------------- 1 | rm -rf coverage/ 2 | istanbul cover --report json --dir coverage/each/master --print none \ 3 | ./node_modules/.bin/_mocha -- -t 4000 test/*-test.js 4 | istanbul report --root coverage/each --dir coverage/full lcov 5 | istanbul report --root coverage text-summary 6 | -------------------------------------------------------------------------------- /test/worker-to-worker.js: -------------------------------------------------------------------------------- 1 | const cluster = require('cluster'); 2 | const hub = require('..'); 3 | 4 | hub.on('fromworker', () => { 5 | hub.reset(); 6 | cluster.worker.disconnect(); 7 | }); 8 | hub.on('allready', () => { 9 | hub.emitRemote('fromworker'); 10 | }); 11 | hub.emit('imready'); 12 | -------------------------------------------------------------------------------- /test/worker-master-4.js: -------------------------------------------------------------------------------- 1 | const hub = require('..'); 2 | 3 | hub.removeAllListeners(); 4 | hub.many(2, 'call', () => { 5 | hub.removeAllListeners('call'); 6 | hub.removeAllListeners(); 7 | hub.off('call'); 8 | hub.emit('hi'); 9 | }); 10 | hub.on('call', () => {}); 11 | hub.removeAllListeners('strike'); 12 | const f = () => {}; 13 | hub.on('nothing', f); 14 | hub.on('nothing', f); 15 | hub.off('nothing', f); 16 | -------------------------------------------------------------------------------- /test/worker-bad-msg.js: -------------------------------------------------------------------------------- 1 | const hub = require('..'); 2 | const commands = require('../lib/globals').commands; 3 | const path = require('path'); 4 | 5 | // Send msg to master. 6 | const dir = path.resolve(__dirname, '../lib'); 7 | process.send({ dir }); 8 | process.send({ dir, cmd: 2, hub: 'nonexistant' }); 9 | process.send({ dir, hub: '', cmd: commands.FN, key: -1 }); 10 | process.send({ cmd: 'nope' }); 11 | hub.on('ok', () => { 12 | hub.emit('done'); 13 | }); 14 | hub.emit('ready'); 15 | -------------------------------------------------------------------------------- /test/fork.js: -------------------------------------------------------------------------------- 1 | const cluster = require('cluster'); 2 | const path = require('path'); 3 | 4 | 5 | // Forks a new worker with istanbul test coverage. 6 | module.exports = (filename) => { 7 | const filepath = path.join(__dirname, filename); 8 | cluster.setupMaster(process.env.running_under_istanbul ? { 9 | exec: './node_modules/.bin/istanbul', 10 | args: [ 11 | 'cover', '--report', 'json', 12 | '--dir', `./coverage/each/workers/${filename}`, 13 | '--print', 'none', '--include-pid', filepath, '--' 14 | ].concat(process.argv.slice(2)) 15 | } : { exec: filepath }); 16 | return cluster.fork(); 17 | }; 18 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | const cluster = require('cluster'); 2 | const Hub = require('./hub'); 3 | const hubs = require('./globals').hubs; 4 | const onReady = require('./globals').onReady; 5 | 6 | 7 | /** 8 | * Require fork.js to overwrite cluster.fork() 9 | */ 10 | if (cluster.isMaster) { 11 | require('./fork'); 12 | } else { 13 | require('./listener'); 14 | } 15 | 16 | /** 17 | * Export an intance of a hub. This can be used if the clusterhub user 18 | * doesn't wanna bother creating a new hub. It will be considered the global 19 | * hub. 20 | */ 21 | const globalHub = module.exports = new Hub(); 22 | globalHub.Hub = Hub; 23 | globalHub.createHub = (id) => hubs.has(id) ? hubs.get(id) : new Hub(id); 24 | globalHub.ready = onReady; 25 | -------------------------------------------------------------------------------- /test/worker-worker-test.js: -------------------------------------------------------------------------------- 1 | const hub = require('..'); 2 | const cluster = require('cluster'); 3 | const fork = require('./fork'); 4 | const WORKERS = 2; 5 | 6 | 7 | if (cluster.isWorker) throw Error('file should not run under worker'); 8 | 9 | describe('Worker to worker inter-communication', () => { 10 | beforeEach(() => { 11 | for (let i = 0; i < WORKERS; i++) { 12 | fork('worker-to-worker.js'); 13 | } 14 | 15 | let n = WORKERS; 16 | hub.on('imready', () => { 17 | if (--n === 0) hub.emit('allready'); 18 | }); 19 | }); 20 | afterEach(cluster.disconnect); 21 | afterEach(() => hub.reset()); 22 | 23 | it('Waits for workers to finish and exit', (done) => { 24 | let n = WORKERS; 25 | cluster.on('exit', () => { 26 | if (--n === 0) done(); 27 | }); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "clusterhub", 3 | "description": "Easily and efficiently sync data in your cluster applications.", 4 | "keywords": [ 5 | "cluster", 6 | "load balance", 7 | "database", 8 | "multi process", 9 | "sync" 10 | ], 11 | "version": "1.1.0", 12 | "repository": { 13 | "type": "git", 14 | "url": "git://github.com/fent/clusterhub.git" 15 | }, 16 | "author": "fent (https://github.com/fent)", 17 | "main": "./lib/index.js", 18 | "files": [ 19 | "lib" 20 | ], 21 | "scripts": { 22 | "test": "bash test/run-tests.sh" 23 | }, 24 | "directories": { 25 | "lib": "./lib" 26 | }, 27 | "engines": { 28 | "node": ">=6" 29 | }, 30 | "dependencies": { 31 | "eventvat": "^0.2.1" 32 | }, 33 | "devDependencies": { 34 | "istanbul": "^0.4.5", 35 | "mocha": "^6.0.0" 36 | }, 37 | "license": "MIT" 38 | } 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (C) 2012 by fent 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /lib/listener.js: -------------------------------------------------------------------------------- 1 | const commands = require('./globals').commands; 2 | const hubs = require('./globals').hubs; 3 | const ready = require('./globals').ready; 4 | const parseFuncs = require('./globals').parseFuncs; 5 | 6 | 7 | /** 8 | * Listen for events from master. 9 | */ 10 | process.on('message', (msg) => { 11 | if (msg.dir !== __dirname) return; 12 | if (msg.cmd === commands.READY) { 13 | return ready(); 14 | } 15 | 16 | // Check if hub exists. 17 | const hub = hubs.get(msg.hub); 18 | let fn; 19 | if (!hub) return; 20 | 21 | switch (msg.cmd) { 22 | case commands.CMD: 23 | parseFuncs(hub, msg); 24 | hub.emitLocal(msg.event, ...msg.args); 25 | break; 26 | 27 | // It can be a response to another command too. 28 | case commands.CB: 29 | fn = hub._callbacks.get(msg.key); 30 | if (fn) { 31 | fn(...msg.args); 32 | hub._callbacks.delete(msg.key); 33 | } 34 | break; 35 | 36 | case commands.FN: 37 | hub._callFunc(msg); 38 | break; 39 | } 40 | }); 41 | 42 | // Let master know this worker is ready to receive messages. 43 | process.send({ cmd: commands.ONLINE, dir: __dirname }); 44 | -------------------------------------------------------------------------------- /test/master-test.js: -------------------------------------------------------------------------------- 1 | const hub = require('..'); 2 | const assert = require('assert'); 3 | 4 | 5 | describe('Listen', () => { 6 | it('Emits to self', (done) => { 7 | hub.on('e', (a, b, c) => { 8 | assert.equal(a, 1); 9 | assert.equal(b, 2); 10 | assert.equal(c, 3); 11 | done(); 12 | }); 13 | 14 | hub.emit('e', 1, 2, 3); 15 | }); 16 | 17 | describe('And unlisten', () => { 18 | it('Does not emit unlistened to event', (done) => { 19 | hub.on('a', done); 20 | hub.on('b', done); 21 | hub.off('b'); 22 | hub.emit('a'); 23 | hub.emit('b'); 24 | }); 25 | }); 26 | 27 | describe('once', () => { 28 | it('Emits only once', (done) => { 29 | hub.prependOnceListener('ih', () => { 30 | hub.once('hi', done); 31 | hub.emit('hi'); 32 | hub.emit('hi'); 33 | hub.emit('hi'); 34 | }); 35 | hub.emit('ih'); 36 | hub.emit('ih'); 37 | hub.emit('ih'); 38 | hub.emit('ih'); 39 | }); 40 | }); 41 | }); 42 | 43 | describe('Local EventVat database', () => { 44 | 45 | it('Can update and access db', (done) => { 46 | assert.ok(!hub.get('foo')); 47 | hub.set('foo', 'bar', (rs) => { 48 | assert.ok(rs); 49 | done(); 50 | }); 51 | }); 52 | 53 | it('Emits events on function calls', (done) => { 54 | hub.on('incr one', () => { 55 | hub.get('one', 1); 56 | done(); 57 | }); 58 | 59 | hub.incr('one'); 60 | }); 61 | 62 | }); 63 | -------------------------------------------------------------------------------- /test/custom-hub-test.js: -------------------------------------------------------------------------------- 1 | const hub = require('..'); 2 | const cluster = require('cluster'); 3 | const fork = require('./fork'); 4 | 5 | 6 | describe('Create a hub with a custom id', () => { 7 | afterEach(cluster.disconnect); 8 | 9 | describe('Master to master', () => { 10 | it('Hub listens and emits events', (done) => { 11 | afterEach(() => h.destroy()); 12 | const h = hub.createHub('1'); 13 | h.on('myevent', done); 14 | h.emit('myevent'); 15 | }); 16 | }); 17 | 18 | describe('Master to worker', () => { 19 | it('Albe to send and receive messages', (done) => { 20 | fork('worker-custom-1.js'); 21 | const h = hub.createHub('one'); 22 | after(() => h.destroy()); 23 | h.emit('ping'); 24 | h.on('pong', done); 25 | }); 26 | 27 | it('Custom hubs don\'t receive messages from other hubs', (done) => { 28 | fork('worker-custom-2.js'); 29 | hub.createHub('two'); 30 | const h = hub.createHub('two'); 31 | after(() => h.destroy()); 32 | h.emit('two'); 33 | h.on('success', done); 34 | }); 35 | }); 36 | 37 | describe('Worker to worker', () => { 38 | it('Two workers communicate with each other', (done) => { 39 | const h = hub.createHub('tres'); 40 | after(() => h.destroy()); 41 | const workers = 2; 42 | for (let i = 0; i < workers; i++) { 43 | fork('worker-custom-3.js'); 44 | } 45 | let n = workers; 46 | h.on('nap', () => { 47 | if (--n === 0) done(); 48 | }); 49 | }); 50 | }); 51 | 52 | }); 53 | -------------------------------------------------------------------------------- /lib/globals.js: -------------------------------------------------------------------------------- 1 | const isWorker = require('cluster').isWorker; 2 | 3 | 4 | // Command constants. 5 | exports.commands = { 6 | EVENT : 0, // Event emitted 7 | ON : 1, // Worker listening to event 8 | OFF : 2, // Worker stops listening to event 9 | OFFALL : 3, // Worker stops listsening to all events of name 10 | ONLINE : 4, // Worker is online 11 | READY : 5, // All workers are online and connected to master 12 | CB : 6, // Worker receives callback to a db method 13 | FN : 7, // Emitted function is called 14 | }; 15 | 16 | 17 | /** 18 | * Keep track of hubs and workers. 19 | */ 20 | exports.hubs = new Map(); 21 | const workers = exports.workers = []; 22 | const msgqueue = exports.msgqueue = []; 23 | const onready = []; 24 | 25 | 26 | /** 27 | * Returns true if all workers are online and ready. 28 | */ 29 | exports.allOnline = () => { 30 | return workers.length && workers.every(obj => obj.online); 31 | }; 32 | 33 | /** 34 | * Returns true if all workers are online and ready to communicate. 35 | */ 36 | exports.allReady = () => { 37 | return workers.length && workers.every(obj => obj.ready); 38 | }; 39 | 40 | /** 41 | * Calls `fn` when all workers are online and ready. 42 | */ 43 | exports.onReady = (fn) => { 44 | if (exports.allReady() && !msgqueue.length) { 45 | fn(); 46 | } else { 47 | onready.push(fn); 48 | } 49 | }; 50 | 51 | /** 52 | * When all workers are online, this tells all hubs in the current process. 53 | */ 54 | exports.ready = () => { 55 | let listener; 56 | while ((listener = onready.shift()) != null) listener(); 57 | if (!exports.allReady() && isWorker) { 58 | process.send({ cmd: exports.commands.READY, dir: __dirname }); 59 | } 60 | }; 61 | 62 | /** 63 | * If a message received contains references to functions from another process, 64 | * add a function to the args that when called, will let the other process know. 65 | * 66 | * If the function is garbage collected, it lets the other process know 67 | * that it's safe to delete it from its hub. 68 | * 69 | * @param {Hub} hub 70 | * @param {Object} msg 71 | * @param {!cluster.Worker} worker 72 | */ 73 | exports.parseFuncs = (hub, msg, worker) => { 74 | const send = worker ? 75 | (msg) => hub._sendWorker(worker, msg) : 76 | (msg) => hub._sendMaster(msg); 77 | if (msg.funcs) { 78 | for (let i = msg.funcs.length - 1; i >= 0; i--) { 79 | const func = msg.funcs[i]; 80 | const fn = (...args) => send({ 81 | cmd : exports.commands.FN, 82 | key : func.key, 83 | args, 84 | }); 85 | msg.args.splice(func.i, 0, fn); 86 | } 87 | } 88 | }; 89 | -------------------------------------------------------------------------------- /lib/fork.js: -------------------------------------------------------------------------------- 1 | const cluster = require('cluster'); 2 | const commands = require('./globals').commands; 3 | const hubs = require('./globals').hubs; 4 | const workers = require('./globals').workers; 5 | const allOnline = require('./globals').allOnline; 6 | const allReady = require('./globals').allReady; 7 | const ready = require('./globals').ready; 8 | const parseFuncs = require('./globals').parseFuncs; 9 | const msgqueue = require('./globals').msgqueue; 10 | const Hub = require('./hub'); 11 | 12 | 13 | cluster.on('fork', (worker) => { 14 | const events = new Map(); 15 | 16 | const ononline = () => { 17 | obj.online = true; 18 | 19 | // Check if all workers are online. 20 | if (!allOnline()) return; 21 | 22 | // Tell all workers hub is ready. 23 | workers.filter(child => !child.ready).forEach((child) => { 24 | child.worker.send({ cmd: commands.READY, dir: __dirname }); 25 | }); 26 | }; 27 | 28 | const onready = () => { 29 | obj.ready = true; 30 | if (allReady()) { 31 | // Process any messages that were buffered while hubs were not ready. 32 | let fn; 33 | while ((fn = msgqueue.shift())) fn(); 34 | ready(); 35 | } 36 | }; 37 | 38 | const oncmd = (msg) => { 39 | const hub = hubs.get(msg.hub); 40 | let db, result; 41 | parseFuncs(hub, msg, worker); 42 | switch (msg.cmd) { 43 | // If this is an emitted event, distribute it amongst all workers 44 | // who are listening for the event. Except the one who sent it. 45 | case commands.EVENT: 46 | hub._sendWorkers(msg.event, msg.args, worker); 47 | hub.emitLocal(msg.event, ...msg.args); 48 | break; 49 | 50 | // If it's on/off, add/remove counters to know if this worker should 51 | // get notified of any events or not. 52 | case commands.ON: 53 | if (events.has(msg.event)) { 54 | events.set(msg.event, events.get(msg.event) + 1); 55 | } else { 56 | events.set(msg.event, 1); 57 | } 58 | break; 59 | 60 | case commands.OFF: 61 | if (events.has(msg.event)) { 62 | let n = events.get(msg.event) - 1; 63 | events.set(msg.event, n); 64 | if (n === 0) { 65 | events.delete(msg.event); 66 | } 67 | } 68 | break; 69 | 70 | case commands.OFFALL: 71 | if (msg.event) { 72 | events.delete(msg.event); 73 | } else { 74 | events.clear(); 75 | } 76 | break; 77 | 78 | case commands.FN: 79 | hub._callFunc(msg); 80 | break; 81 | 82 | // Can be a EventVat command 83 | // in that case, execute it on the EventVat instance for this hub. 84 | default: 85 | db = hubs.get(msg.hub)._db; 86 | result = db[msg.cmd](...msg.args); 87 | 88 | // If key is given, then a callback is waiting for the result. 89 | if (msg.key) { 90 | hub._sendWorker(worker, { 91 | cmd : commands.CB, 92 | key : msg.key, 93 | args : [result], 94 | }); 95 | } 96 | } 97 | }; 98 | 99 | worker.on('message', (msg) => { 100 | if (msg.dir !== __dirname) return; 101 | if (msg.cmd === commands.ONLINE) { 102 | ononline(); 103 | return; 104 | } 105 | 106 | if (msg.cmd === commands.READY) { 107 | onready(); 108 | return; 109 | } 110 | 111 | if (msg.hub == null || msg.cmd == null) return; 112 | if (!hubs.has(msg.hub)) { 113 | new Hub(msg.hub); 114 | } 115 | 116 | if (allReady()) { 117 | oncmd(msg); 118 | } else { 119 | msgqueue.push(oncmd.bind(this, msg)); 120 | } 121 | }); 122 | 123 | worker.on('disconnect', () => { 124 | workers.splice(workers.indexOf(obj), 1); 125 | }); 126 | 127 | let obj = { 128 | worker, 129 | events, 130 | online: false, 131 | ready: false, 132 | }; 133 | workers.push(obj); 134 | }); 135 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # clusterhub 2 | 3 | An attempt at giving multi process node programs a simple and efficient way to share data. 4 | 5 | [![Build Status](https://secure.travis-ci.org/fent/clusterhub.svg)](http://travis-ci.org/fent/clusterhub) 6 | [![Dependency Status](https://david-dm.org/fent/clusterhub.svg)](https://david-dm.org/fent/clusterhub) 7 | [![codecov](https://codecov.io/gh/fent/clusterhub/branch/master/graph/badge.svg)](https://codecov.io/gh/fent/clusterhub) 8 | 9 | 10 | # Usage 11 | 12 | ```js 13 | const cluster = require('cluster'); 14 | const numCPUs = require('os').cpus().length; 15 | const hub = require('clusterhub'); 16 | 17 | if (cluster.isMaster) { 18 | // Fork workers. 19 | for (let i = 0; i < numCPUs; i++) { 20 | cluster.fork(); 21 | } 22 | 23 | } else { 24 | hub.on('event', (data) => { 25 | // do something with `data` 26 | }); 27 | 28 | // emit event to all workers 29 | hub.emit('event', { foo: 'bar' }); 30 | } 31 | ``` 32 | 33 | # Features 34 | 35 | * Efficient event emitter system. Clusterhub will not send an event to a process that isn't listening for it. Events from the same process of a listener will be emitted synchronously. 36 | * In process database. Each hub has its own instance of a redis-like database powered by [EventVat][eventvat]. 37 | 38 | # Motive 39 | 40 | Node.js is a perfect candidate to developing [Date Intensive Real-time Applications](http://video.nextconf.eu/video/1914374/nodejs-digs-dirt-about). Load balancing in these applications can become complicated when having to share data between processes. 41 | 42 | A remote database can be an easy solution for this, but it's not the most optimal. Communicating with a local process is several times faster than opening remote requests from a database. And even if the database is hosted locally, the overhead of communicating with yet another program is lessened. 43 | 44 | Note that this module is experimental. It currently works by using a process's internal messaging system. 45 | 46 | ## Made with Clusterhub 47 | 48 | * [socket.io-clusterhub](https://github.com/fent/socket.io-clusterhub) - Sync data between multi-process socket.io applications. 49 | * [clusterchat](https://github.com/fent/clusterchat) - A multi-process chat that shows off socket.io-clusterhub. 50 | 51 | # API 52 | 53 | ### hub.createHub(id) 54 | Clusterhub already comes with a default global hub. But you can use this if you want to create more. 55 | 56 | ### Hub#destroy() 57 | Call to disable hub from emitting and receiving remote messages/commands. 58 | 59 | Additionally, all functions from the regular [EventEmitter](http://nodejs.org/docs/latest/api/events.html#events.EventEmitter) are included. Plus a couple of extras. 60 | 61 | ### Hub#emitLocal(event, ...args) 62 | Emit an event only to the current process. 63 | 64 | ### Hub#emitRemote(event, ...args) 65 | Emit an event only to other worker processes and master. Or only to workers if the current process is the master. 66 | 67 | ```js 68 | hub.on('remotehello', () => { 69 | // Hello from another process. 70 | }); 71 | 72 | hub.emitRemote('remotehello', { hello: 'there' }); 73 | ``` 74 | 75 | All functions from [EventVat][eventvat] are included as well. Their returned value can be accessed by providing a callback as the last argument. Or optionally by its returned value if called by the master. 76 | 77 | [eventvat]: https://github.com/hij1nx/EventVat 78 | 79 | #### worker process 80 | 81 | ```js 82 | hub.set('foo', 'bar', () => { 83 | hub.get('foo', (val) => { 84 | console.log(val === 'bar'); // true 85 | }); 86 | }); 87 | ``` 88 | 89 | #### master process 90 | ```js 91 | let returnedVal = hub.incr('foo', (val) => { 92 | // Can be given a callback for consistency. 93 | console.log(val === 1); // true 94 | }); 95 | 96 | // But since it's the master process it has direct access to the database. 97 | console.log(returnedVal === 1); // true 98 | ``` 99 | 100 | 101 | # Install 102 | 103 | npm install clusterhub 104 | 105 | 106 | # Tests 107 | Tests are written with [mocha](https://mochajs.org) 108 | 109 | ```bash 110 | npm test 111 | ``` 112 | -------------------------------------------------------------------------------- /test/master-worker-test.js: -------------------------------------------------------------------------------- 1 | const hub = require('..'); 2 | const commands = require('../lib/globals').commands; 3 | const cluster = require('cluster'); 4 | const assert = require('assert'); 5 | const path = require('path'); 6 | const fork = require('./fork'); 7 | 8 | 9 | if (cluster.isWorker) throw Error('file should not run under worker'); 10 | 11 | describe('Communicate from master to workers', () => { 12 | afterEach(cluster.disconnect); 13 | afterEach(() => hub.reset()); 14 | 15 | it('Listens for event from a worker and responds', (done) => { 16 | fork('worker-master-1.js'); 17 | hub.on('yesmaster', (data) => { 18 | assert.deepEqual(data, { hello: 'there' }); 19 | hub.emit('work', 'now'); 20 | hub.emit('work', 'now'); 21 | hub.on('donemaster', done); 22 | }); 23 | }); 24 | 25 | it('Listens for set and get event', (done) => { 26 | fork('worker-master-2.js'); 27 | hub.on('set foo', (value) => { 28 | assert.equal(value, 66); 29 | hub.set('bar', 'dog'); 30 | }); 31 | hub.on('result', (value) => { 32 | assert.equal(value, 'DOG'); 33 | done(); 34 | }); 35 | hub.set('value', 24); 36 | }); 37 | 38 | it('Does not emit worker to worker events', (done) => { 39 | fork('worker-master-3.js'); 40 | hub.on('local', () => { throw Error('should not emit'); }); 41 | hub.on('remote', (value) => { 42 | assert.equal(value, 'yes!'); 43 | done(); 44 | }); 45 | }); 46 | 47 | it('Listens and unlistens to events in order', (done) => { 48 | fork('worker-master-4.js'); 49 | hub.removeAllListeners('hi'); 50 | const f = () => {}; 51 | hub.on('hi', f); 52 | hub.on('hi', () => { 53 | hub.off('hi', f); 54 | hub.off('hi', f); 55 | done(); 56 | }); 57 | hub.off('hi', () => {}); 58 | hub.emit('call'); 59 | }); 60 | 61 | it('Listeners are called in order', (done) => { 62 | fork('worker-master-order.js'); 63 | const results = []; 64 | hub.on('the-event', () => results.push(1)); 65 | hub.on('the-event', () => results.push(2)); 66 | hub.prependListener('the-event', () => results.push(3)); 67 | let doneCall = false; 68 | hub.on('after', () => { 69 | assert.equal(doneCall, true); 70 | }); 71 | hub.prependManyListener(2, 'after', () => { 72 | doneCall = true; 73 | assert.deepEqual(results, [3, 1, 2]); 74 | done(); 75 | }); 76 | }); 77 | 78 | describe('Send a badly formatted messages to each other', () => { 79 | it('Master and worker ignore it', (done) => { 80 | const worker = fork('worker-bad-msg.js'); 81 | hub.on('done', done); 82 | hub.ready(() => { 83 | const dir = path.resolve(__dirname, '../lib'); 84 | worker.send({ dir, hub: 'none' }); 85 | worker.send({ dir, hub: '', cmd: commands.CB, key: -1 }); 86 | worker.send({ dir, hub: '', cmd: commands.FN, key: -1 }); 87 | worker.send({}); 88 | hub.emit('ok'); 89 | }); 90 | }); 91 | }); 92 | 93 | describe('Send a message from master first', () => { 94 | it('Worker should receive it after it connects', (done) => { 95 | fork('worker-get-msg.js'); 96 | hub.on('done', done); 97 | hub.emit('bad'); 98 | }); 99 | }); 100 | 101 | describe('Emit event with a callback', () => { 102 | it('Function gets sent to worker then back to master', (done) => { 103 | fork('worker-emit-fn-1.js'); 104 | hub.emit('my-done', done); 105 | hub.on('same-done', (done2) => done2()); 106 | }); 107 | 108 | it('Function gets sent between workers', (done) => { 109 | const WORKERS = 2; 110 | for (let i = 0; i < WORKERS; i++) { 111 | fork('worker-emit-fn-2.js'); 112 | } 113 | let n = WORKERS; 114 | hub.on('sum', (sum) => { 115 | assert.equal(sum, 4); 116 | if (--n === 0) done(); 117 | }); 118 | }); 119 | 120 | it('Deleted func does nothing', (done) => { 121 | fork('worker-emit-fn-3.js'); 122 | hub._maxFuncs = 1; 123 | after(() => hub._maxFuncs = 100); 124 | let n = 2; 125 | hub.emit('bad-good', done, () => { 126 | if (--n == 0) throw Error('should not be called'); 127 | }); 128 | }); 129 | }); 130 | 131 | }); 132 | -------------------------------------------------------------------------------- /lib/hub.js: -------------------------------------------------------------------------------- 1 | const cluster = require('cluster'); 2 | const EventVat = require('eventvat'); 3 | const commands = require('./globals').commands; 4 | const hubs = require('./globals').hubs; 5 | const onReady = require('./globals').onReady; 6 | const workers = require('./globals').workers; 7 | const isMaster = cluster.isMaster; 8 | const isWorker = cluster.isWorker; 9 | 10 | 11 | module.exports = class Hub { 12 | /** 13 | * @constructor 14 | * @param {string} id 15 | */ 16 | constructor(id) { 17 | this._id = id || ''; 18 | hubs.set(this._id, this); 19 | this._listeners = new Map(); 20 | this._funcs = new Map(); 21 | this._funcsHistory = new Set(); 22 | this._maxFuncs = 100; 23 | 24 | if (isMaster) { 25 | this._db = new EventVat(); 26 | this._db.onAny((event, ...args) => { 27 | this._sendWorkers(event, args); 28 | this.emitLocal(event, ...args); 29 | }); 30 | 31 | } else { 32 | this._callbacks = new Map(); 33 | } 34 | 35 | // Attach all commands from EventVat to Hub. This sends a command to the 36 | // master process to deal with hub data. 37 | Object.keys(EventVat.prototype).forEach((cmd) => { 38 | if (typeof this[cmd] === 'function') return; 39 | this[cmd] = (...args) => { 40 | let cb; 41 | if (typeof args[args.length - 1] === 'function') { 42 | cb = args.pop(); 43 | } 44 | 45 | if (isMaster) { 46 | let rs = this._db[cmd](...args); 47 | if (cb) process.nextTick(() => { cb(rs); }); 48 | return rs; 49 | 50 | } else { 51 | const key = this._keyFunc(this._callbacks, cb); 52 | this._sendMaster({ cmd, args, key }); 53 | } 54 | }; 55 | }); 56 | 57 | // Define some aliases. 58 | this.publish = this.emit; 59 | this.broadcast = this.emitRemote; 60 | this.addListener = this.on; 61 | this.subscribe = this.on; 62 | this.removeListener = this.off; 63 | this.unsubscribe = this.off; 64 | 65 | } 66 | 67 | /** 68 | * Sends an event to all workers. 69 | * 70 | * @param {string} event 71 | * @param {Array.} args 72 | * @param {!cluster.Worker} origin 73 | */ 74 | _sendWorkers(event, args, origin) { 75 | onReady(() => { 76 | args = args.slice(); 77 | const funcs = this._keyArgFuncs(args); 78 | workers 79 | .filter(child => child.worker !== origin && child.events.has(event)) 80 | .forEach((child) => { 81 | this._sendWorker(child.worker, { event, args, funcs }); 82 | }); 83 | }); 84 | } 85 | 86 | /** 87 | * Send message to a single worker. 88 | * 89 | * @param {cluster.Worker} worker 90 | * @param {Object} msg 91 | */ 92 | _sendWorker(worker, msg) { 93 | msg.dir = __dirname; 94 | msg.hub = this._id; 95 | worker.send(msg); 96 | } 97 | 98 | /** 99 | * Sends message to master. 100 | * 101 | * @param {Object} msg 102 | */ 103 | _sendMaster(msg) { 104 | msg.dir = __dirname; 105 | msg.hub = this._id; 106 | if (msg.args) { 107 | msg.args = msg.args.slice(); 108 | msg.funcs = this._keyArgFuncs(msg.args); 109 | } 110 | process.send(msg); 111 | } 112 | 113 | /** 114 | * Assigns a key to a function so that it can be called from another process. 115 | * 116 | * @param {Map} map 117 | * @param {Function} fn 118 | * @return {string} key 119 | */ 120 | _keyFunc(map, fn) { 121 | let key; 122 | if (fn) { 123 | while (map.has((key = Math.ceil(Math.random() * 20000)))); 124 | map.set(key, fn); 125 | } 126 | return key; 127 | } 128 | 129 | 130 | /** 131 | * Save references for arguments that are functions, 132 | * letting them be called by other processes. 133 | * 134 | * @param {Array.} args 135 | * @return {Array.= 0; i--) { 140 | const arg = args[i]; 141 | if (typeof arg === 'function') { 142 | args.splice(i, 1); 143 | funcs.push({ i, key: this._keyFunc(this._funcs, arg) }); 144 | } 145 | } 146 | return funcs.length ? funcs : null; 147 | } 148 | 149 | /** 150 | * Calls a function that was called by another process. 151 | * 152 | * @param {Object} msg 153 | */ 154 | _callFunc(msg) { 155 | const fn = this._funcs.get(msg.key); 156 | if (fn) { 157 | fn(...msg.args); 158 | this._funcsHistory.delete(msg.key); 159 | this._funcsHistory.add(msg.key); 160 | if (this._funcsHistory.size > this._maxFuncs) { 161 | const key = this._funcsHistory.keys().next().value; 162 | this._funcs.delete(key); 163 | this._funcsHistory.delete(key); 164 | } 165 | } 166 | } 167 | 168 | /** 169 | * Emits event to all workers and the master in the hub. 170 | * 171 | * @param {string} event 172 | * @param {Object} ...args 173 | */ 174 | emit(event, ...args) { 175 | this.emitRemote(event, ...args); 176 | this.emitLocal(event, ...args); 177 | } 178 | 179 | /** 180 | * Emits an event only to the current process. 181 | * 182 | * @param {string} event 183 | * @param {Array.} ...args 184 | */ 185 | emitLocal(event, ...args) { 186 | // Check if there are listeners for this event. 187 | if (!this._listeners.has(event)) return; 188 | this._listeners.get(event).forEach((listener) => { 189 | listener(...args); 190 | }); 191 | } 192 | 193 | /** 194 | * Emits an event only to all other workes in the hub including master. 195 | * 196 | * @param {string} event 197 | * @param {Object} ...args 198 | */ 199 | emitRemote(event, ...args) { 200 | if (isWorker) { 201 | this._sendMaster({ cmd: commands.EVENT, event, args }); 202 | } else { 203 | this._sendWorkers(event, args); 204 | } 205 | } 206 | 207 | /** 208 | * @param {string} event 209 | * @param {Function(...args)} listener 210 | * @param {boolean} append 211 | */ 212 | _on(event, listener, append) { 213 | if (!this._listeners.has(event)) this._listeners.set(event, []); 214 | this._listeners.get(event)[append ? 'push' : 'unshift'](listener); 215 | 216 | if (isWorker) { 217 | this._sendMaster({ cmd: commands.ON, event }); 218 | } 219 | } 220 | 221 | /** 222 | * Starts listening to an event within the hub. 223 | * 224 | * @param {string} event The event to listen for. 225 | * @param {Function(...args)} listener The function that gets called 226 | * when one of the workers emits it. 227 | */ 228 | on(event, listener) { 229 | this._on(event, listener, true); 230 | } 231 | 232 | /** 233 | * Adds the listener at the beginner of the list of listeners, 234 | * calling it before other listeners. 235 | * 236 | * @param {string} event 237 | * @param {Function(...args)} listener 238 | */ 239 | prependListener(event, listener) { 240 | this._on(event, listener, false); 241 | } 242 | 243 | /** 244 | * Removes a listener from listening to an event. 245 | * 246 | * @param {string} event 247 | * @param {Function} listener 248 | */ 249 | off(event, listener) { 250 | if (!this._listeners.has(event)) return; 251 | 252 | // Remove local listener. 253 | let listeners = this._listeners.get(event); 254 | let i = listeners 255 | .findIndex(liss => liss === listener || liss.listener === listener); 256 | if (i > -1) { 257 | listeners.splice(i, 1); 258 | 259 | // Tell master there is one less listener for this event. 260 | if (isWorker) { 261 | this._sendMaster({ cmd: commands.OFF, event }); 262 | } 263 | } 264 | } 265 | 266 | /** 267 | * Listens for n number of the event and then stops listening. 268 | * 269 | * @param {number} n 270 | * @param {string} event 271 | * @param {Function(...args)} listener 272 | * @param {boolean} append 273 | */ 274 | _many(n, event, listener, append) { 275 | const wrapper = (...args) => { 276 | if (--n === 0) this.off(event, listener); 277 | listener(...args); 278 | }; 279 | wrapper.listener = listener; 280 | this[append ? 'on' : 'prependListener'](event, wrapper); 281 | } 282 | 283 | /** 284 | * Listens for n number of the event and then stops listening. 285 | * 286 | * @param {number} n 287 | * @param {string} event 288 | * @param {Function(...args)} listener 289 | */ 290 | many(n, event, listener) { 291 | this._many(n, event, listener, true); 292 | } 293 | 294 | /** 295 | * @param {number} n 296 | * @param {string} event 297 | * @param {Function(...args)} listener 298 | */ 299 | prependManyListener(n, event, listener) { 300 | this._many(n, event, listener, false); 301 | } 302 | 303 | /** 304 | * Shortcut for `many(1, event, listener)` 305 | * 306 | * @param {string} event 307 | * @param {Function(...args)} listener 308 | */ 309 | once(event, listener) { 310 | this._many(1, event, listener, true); 311 | } 312 | 313 | /** 314 | * @param {string} event 315 | * @param {Function(...args)} listener 316 | */ 317 | prependOnceListener(event, listener) { 318 | this._many(1, event, listener, false); 319 | } 320 | 321 | /** 322 | * Removes all listeners for the event. 323 | * 324 | * @param {string} event 325 | */ 326 | removeAllListeners(event) { 327 | if (event) { 328 | this._listeners.delete(event); 329 | } else { 330 | this._listeners.clear(); 331 | } 332 | 333 | if (isWorker) { 334 | this._sendMaster({ cmd: commands.OFFALL, event }); 335 | } 336 | } 337 | 338 | /** 339 | * Removes all listeners and clears the db. 340 | */ 341 | reset() { 342 | this.removeAllListeners(); 343 | this._funcs.clear(); 344 | this._funcsHistory.clear(); 345 | if (isMaster) { 346 | this._db.die(); 347 | } else { 348 | this._callbacks.clear(); 349 | } 350 | } 351 | 352 | /** 353 | * Removes Hub instance from memory. 354 | */ 355 | destroy() { 356 | this.reset(); 357 | hubs.delete(this._id); 358 | } 359 | }; 360 | --------------------------------------------------------------------------------