UNPKG

wtfork

Version:

Subscribe to and publish events between parent and child node processes using the standard node event emitter api or call parent methods directly from the child process and vice versa.

330 lines (277 loc) 10.7 kB
'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); exports.fork = fork; var _events = require('events'); var _child_process = require('child_process'); var _path = require('path'); var _cuid = require('cuid'); var _cuid2 = _interopRequireDefault(_cuid); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } } /** * Returns all the method names from a provided class/object. * @param classOrObject * @returns {Array.<*>} * @private */ function _getMethods(classOrObject) { /* eslint arrow-body-style:0 */ // some weirdness going on here. var methods = Object.getOwnPropertyNames(classOrObject).filter(function (p) { return !p.startsWith('_') && typeof classOrObject[p] === 'function'; }); if (!methods.length) { // try getProtoTypeOf as it's potentially a class var _iteratorNormalCompletion = true; var _didIteratorError = false; var _iteratorError = undefined; try { for (var _iterator = Object.getOwnPropertyNames(Object.getPrototypeOf(classOrObject))[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { var name = _step.value; var method = classOrObject[name]; if (!method instanceof Function) continue; if (method === classOrObject) continue; if (name === 'constructor') continue; if (name.startsWith('_')) continue; // exclude private methods methods.push(name); } } catch (err) { _didIteratorError = true; _iteratorError = err; } finally { try { if (!_iteratorNormalCompletion && _iterator.return) { _iterator.return(); } } finally { if (_didIteratorError) { throw _iteratorError; } } } } return methods; } /** * Converts an Error into an IPC emitable object * @param error * @returns {{message: *, type: *, stack: *}} * @private */ function _extractError(error) { return { message: error.message, type: error.constructor.name, stack: error.stack }; } /** * Converts an transmitted object back into an Error * Only preserves global error types. * @param obj * @returns Error * @private */ function _convertToError(obj) { var error = global[obj.type] ? new global[obj.type](obj.message) : new Error(obj.message); error.stack = obj.stack; return error; } /** * Creates a stub function to allow method calls via ipc. * @param target * @param method * @param emitter * @returns Function * @private */ function _ipcMethodWrapper(target, method, emitter) { return function stubbyMcStubFace() { for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { args[_key] = arguments[_key]; } // boatyMcBoatFace for pres // we create a new id per function call so we can track that specific methods invocation // to allow multiple calls of the same function var callId = (0, _cuid2.default)(); return new Promise(function (resolve, reject) { var type = '' + (target === 'parent' ? 'child_to_parent' : 'parent_to_child'); var data = { func_name: method, call_id: callId, args: args }; // subscribe to responder event emitter.once('wtfork:' + data.func_name + ':' + data.call_id, function (result) { if (result.reject) { return reject(_convertToError(result.reject)); } return resolve(result.resolve); }); // send the event to initiate the call emitter.send('wtfork:' + type + ':method_call', data); }); }; } /** * Notifies the parent of the child process methods based on * a provided class or object of methods. * @param classOrObject * @private */ function _setChildMethods(classOrObject) { process.parent._childMethods = classOrObject; process.parent.send('wtfork:set_child_methods', { wtfork: { methods: _getMethods(classOrObject) } }); } /* eslint no-param-reassign:0 */ /** * Fork a child process, internally calls node child_process fork. * @param path * @param args * @param options * @param classOrObject */ function fork(path, args, options, classOrObject) { var childId = (0, _cuid2.default)(); if (!options) options = {}; if (!options.env) options.env = {}; if (!options.execArgv) options.execArgv = []; if (!args) args = []; Object.assign(options.env, { WTFORK_CHILD: childId, WTFORK_PATH: (0, _path.resolve)(process.cwd(), path) }); if (classOrObject) { // set the parent methods available to the child options.env.WTFORK_PARENT_METHODS = _getMethods(classOrObject); } // create the child process var childProcess = (0, _child_process.fork)((0, _path.resolve)(__dirname), args, options); // create the helper emitter and send method childProcess.child = new _events.EventEmitter(); // buffer any events prior to a ready state childProcess.child.buffer = []; // where the child method stubs get created childProcess.child.methods = {}; // not really used much but I have plans \o/ childProcess.child.id = childId; // internal ref to the provided classOrObject childProcess.child._parentMethods = classOrObject || {}; // setup internal routing of process messages sent via wtfork childProcess.on('message', function (msg) { // only route wtfork messages that are bound to this child's id if (msg && msg.wtfork && msg.wtfork.child_id === childProcess.child.id) { childProcess.child.emit(msg.wtfork.channel, msg.wtfork.data || {}); } }); // send wrapper method childProcess.child.send = function send(channel, data) { // pre ready state lets just buffer all outbound if (!childProcess.child.ready) { return childProcess.child.buffer.push([channel, data]); } // forward to child process return childProcess.send({ wtfork: { child_id: childProcess.child.id, channel: channel, data: data } }); }; // set stub methods when the child call set methods. childProcess.child.on('wtfork:set_child_methods', function (data) { data.wtfork.methods.forEach(function (name) { childProcess.child.methods[name] = _ipcMethodWrapper('child', name, childProcess.child); }); }); // again, not really used yet childProcess.child.on('wtfork:child_ready', function () { childProcess.child.ready = true; // replay buffered events as we're now ready childProcess.child.buffer.forEach(function (event) { var _childProcess$child; return (_childProcess$child = childProcess.child).send.apply(_childProcess$child, _toConsumableArray(event)); }); childProcess.child.buffer = []; }); // relay method calls childProcess.child.on('wtfork:child_to_parent:method_call', function (methodData) { if (childProcess.child._parentMethods[methodData.func_name]) { var _childProcess$child$_; (_childProcess$child$_ = childProcess.child._parentMethods)[methodData.func_name].apply(_childProcess$child$_, _toConsumableArray(methodData.args)).then(function (response) { childProcess.child.send('wtfork:' + methodData.func_name + ':' + methodData.call_id, { resolve: response }); }).catch(function (error) { childProcess.child.send('wtfork:' + methodData.func_name + ':' + methodData.call_id, { reject: _extractError(error) }); }); } }); return childProcess; } exports.default = { fork: fork }; // TODO babel add exports plugin not working at the moment for some reason ?? module.exports = exports.default; // Below code sets up the child process.parent functionality // only if the env variable is present - automatically added by the internal fork if (process.env.WTFORK_CHILD && !process.parent) { // create a new emitter to be used as an internal messaging router from the parent process process.parent = new _events.EventEmitter(); // internal ref to the provided classOrObject process.parent._childMethods = {}; // where the parent method stubs get created process.parent.methods = {}; process.parent.setChildMethods = _setChildMethods; // the parent provided it's methods so lets create some stubs if (process.env.WTFORK_PARENT_METHODS) { process.env.WTFORK_PARENT_METHODS.split(',').forEach(function (name) { process.parent.methods[name] = _ipcMethodWrapper('parent', name, process.parent); }); } // for usage later process.parent.child_id = process.env.WTFORK_CHILD; // override the emitter so we can intercept and forward relevant messages // onto the parent process via process.send process.parent.send = function send(channel, data) { // forward to parent process return process.send({ wtfork: { child_id: process.parent.child_id, channel: channel, data: data } }); }; // setup internal routing of process messages sent via wtfork process.on('message', function (msg) { // only route wtfork messages that are bound to this child's id if (msg && msg.wtfork && msg.wtfork.child_id === process.parent.child_id) { process.parent.emit(msg.wtfork.channel, msg.wtfork.data || {}); } }); // relay method calls process.parent.on('wtfork:parent_to_child:method_call', function (methodData) { if (process.parent._childMethods[methodData.func_name]) { var _process$parent$_chil; (_process$parent$_chil = process.parent._childMethods)[methodData.func_name].apply(_process$parent$_chil, _toConsumableArray(methodData.args)).then(function (response) { process.parent.send('wtfork:' + methodData.func_name + ':' + methodData.call_id, { resolve: response }); }).catch(function (error) { process.parent.send('wtfork:' + methodData.func_name + ':' + methodData.call_id, { reject: _extractError(error) }); }); } }); // now load the actual child module var childModule = require(process.env.WTFORK_PATH); // support export default _setChildMethods(childModule.default ? childModule.default : childModule); // tell the parent we're ready - not really used at the moment though process.parent.send('wtfork:child_ready', process.parent.child_id); } // TODO merge method relay functionality from parent and child, duplicating logic at the moment // TODO merge event relay functionality from parent and child, duplicating logic at the moment