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
JavaScript
;
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