UNPKG

clusterhub

Version:

Easily and efficiently sync data in your cluster applications.

360 lines (324 loc) 8.48 kB
const cluster = require('cluster'); const EventVat = require('eventvat'); const commands = require('./globals').commands; const hubs = require('./globals').hubs; const onReady = require('./globals').onReady; const workers = require('./globals').workers; const isMaster = cluster.isMaster; const isWorker = cluster.isWorker; module.exports = class Hub { /** * @constructor * @param {string} id */ constructor(id) { this._id = id || ''; hubs.set(this._id, this); this._listeners = new Map(); this._funcs = new Map(); this._funcsHistory = new Set(); this._maxFuncs = 100; if (isMaster) { this._db = new EventVat(); this._db.onAny((event, ...args) => { this._sendWorkers(event, args); this.emitLocal(event, ...args); }); } else { this._callbacks = new Map(); } // Attach all commands from EventVat to Hub. This sends a command to the // master process to deal with hub data. Object.keys(EventVat.prototype).forEach((cmd) => { if (typeof this[cmd] === 'function') return; this[cmd] = (...args) => { let cb; if (typeof args[args.length - 1] === 'function') { cb = args.pop(); } if (isMaster) { let rs = this._db[cmd](...args); if (cb) process.nextTick(() => { cb(rs); }); return rs; } else { const key = this._keyFunc(this._callbacks, cb); this._sendMaster({ cmd, args, key }); } }; }); // Define some aliases. this.publish = this.emit; this.broadcast = this.emitRemote; this.addListener = this.on; this.subscribe = this.on; this.removeListener = this.off; this.unsubscribe = this.off; } /** * Sends an event to all workers. * * @param {string} event * @param {Array.<Object>} args * @param {!cluster.Worker} origin */ _sendWorkers(event, args, origin) { onReady(() => { args = args.slice(); const funcs = this._keyArgFuncs(args); workers .filter(child => child.worker !== origin && child.events.has(event)) .forEach((child) => { this._sendWorker(child.worker, { event, args, funcs }); }); }); } /** * Send message to a single worker. * * @param {cluster.Worker} worker * @param {Object} msg */ _sendWorker(worker, msg) { msg.dir = __dirname; msg.hub = this._id; worker.send(msg); } /** * Sends message to master. * * @param {Object} msg */ _sendMaster(msg) { msg.dir = __dirname; msg.hub = this._id; if (msg.args) { msg.args = msg.args.slice(); msg.funcs = this._keyArgFuncs(msg.args); } process.send(msg); } /** * Assigns a key to a function so that it can be called from another process. * * @param {Map} map * @param {Function} fn * @return {string} key */ _keyFunc(map, fn) { let key; if (fn) { while (map.has((key = Math.ceil(Math.random() * 20000)))); map.set(key, fn); } return key; } /** * Save references for arguments that are functions, * letting them be called by other processes. * * @param {Array.<Object>} args * @return {Array.<Object} Array of indices and keys for reference. */ _keyArgFuncs(args) { const funcs = []; for (let i = args.length - 1; i >= 0; i--) { const arg = args[i]; if (typeof arg === 'function') { args.splice(i, 1); funcs.push({ i, key: this._keyFunc(this._funcs, arg) }); } } return funcs.length ? funcs : null; } /** * Calls a function that was called by another process. * * @param {Object} msg */ _callFunc(msg) { const fn = this._funcs.get(msg.key); if (fn) { fn(...msg.args); this._funcsHistory.delete(msg.key); this._funcsHistory.add(msg.key); if (this._funcsHistory.size > this._maxFuncs) { const key = this._funcsHistory.keys().next().value; this._funcs.delete(key); this._funcsHistory.delete(key); } } } /** * Emits event to all workers and the master in the hub. * * @param {string} event * @param {Object} ...args */ emit(event, ...args) { this.emitRemote(event, ...args); this.emitLocal(event, ...args); } /** * Emits an event only to the current process. * * @param {string} event * @param {Array.<Object>} ...args */ emitLocal(event, ...args) { // Check if there are listeners for this event. if (!this._listeners.has(event)) return; this._listeners.get(event).forEach((listener) => { listener(...args); }); } /** * Emits an event only to all other workes in the hub including master. * * @param {string} event * @param {Object} ...args */ emitRemote(event, ...args) { if (isWorker) { this._sendMaster({ cmd: commands.EVENT, event, args }); } else { this._sendWorkers(event, args); } } /** * @param {string} event * @param {Function(...args)} listener * @param {boolean} append */ _on(event, listener, append) { if (!this._listeners.has(event)) this._listeners.set(event, []); this._listeners.get(event)[append ? 'push' : 'unshift'](listener); if (isWorker) { this._sendMaster({ cmd: commands.ON, event }); } } /** * Starts listening to an event within the hub. * * @param {string} event The event to listen for. * @param {Function(...args)} listener The function that gets called * when one of the workers emits it. */ on(event, listener) { this._on(event, listener, true); } /** * Adds the listener at the beginner of the list of listeners, * calling it before other listeners. * * @param {string} event * @param {Function(...args)} listener */ prependListener(event, listener) { this._on(event, listener, false); } /** * Removes a listener from listening to an event. * * @param {string} event * @param {Function} listener */ off(event, listener) { if (!this._listeners.has(event)) return; // Remove local listener. let listeners = this._listeners.get(event); let i = listeners .findIndex(liss => liss === listener || liss.listener === listener); if (i > -1) { listeners.splice(i, 1); // Tell master there is one less listener for this event. if (isWorker) { this._sendMaster({ cmd: commands.OFF, event }); } } } /** * Listens for n number of the event and then stops listening. * * @param {number} n * @param {string} event * @param {Function(...args)} listener * @param {boolean} append */ _many(n, event, listener, append) { const wrapper = (...args) => { if (--n === 0) this.off(event, listener); listener(...args); }; wrapper.listener = listener; this[append ? 'on' : 'prependListener'](event, wrapper); } /** * Listens for n number of the event and then stops listening. * * @param {number} n * @param {string} event * @param {Function(...args)} listener */ many(n, event, listener) { this._many(n, event, listener, true); } /** * @param {number} n * @param {string} event * @param {Function(...args)} listener */ prependManyListener(n, event, listener) { this._many(n, event, listener, false); } /** * Shortcut for `many(1, event, listener)` * * @param {string} event * @param {Function(...args)} listener */ once(event, listener) { this._many(1, event, listener, true); } /** * @param {string} event * @param {Function(...args)} listener */ prependOnceListener(event, listener) { this._many(1, event, listener, false); } /** * Removes all listeners for the event. * * @param {string} event */ removeAllListeners(event) { if (event) { this._listeners.delete(event); } else { this._listeners.clear(); } if (isWorker) { this._sendMaster({ cmd: commands.OFFALL, event }); } } /** * Removes all listeners and clears the db. */ reset() { this.removeAllListeners(); this._funcs.clear(); this._funcsHistory.clear(); if (isMaster) { this._db.die(); } else { this._callbacks.clear(); } } /** * Removes Hub instance from memory. */ destroy() { this.reset(); hubs.delete(this._id); } };