UNPKG

alm

Version:

The best IDE for TypeScript

344 lines (343 loc) 14.6 kB
"use strict"; var __extends = (this && this.__extends) || (function () { var extendStatics = Object.setPrototypeOf || ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; return function (d, b) { extendStatics(d, b); function __() { this.constructor = d; } d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); }; })(); Object.defineProperty(exports, "__esModule", { value: true }); // This code is designed to be used by both the parent and the child var cp = require("child_process"); var path = require("path"); exports.resolve = Promise.resolve.bind(Promise); /** * The main function you should call from master */ function startWorker(config) { var parent = new Parent(); parent.startWorker(config.workerPath, showError, [], config.onCrashRestart || (function () { return null; })); function showError(error) { if (error) { console.error('Failed to start a worker:', error); } } var worker = parent.sendAllToIpc(config.workerContract); parent.registerAllFunctionsExportedFromAsResponders(config.masterImplementation); return { parent: parent, worker: worker }; } exports.startWorker = startWorker; /** * The main function you should call from worker */ function runWorker(config) { var child = new Child(); child.registerAllFunctionsExportedFromAsResponders(config.workerImplementation); var master = child.sendAllToIpc(config.masterContract); return { child: child, master: master }; } exports.runWorker = runWorker; /** Creates a Guid (UUID v4) */ function createId() { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); return v.toString(16); }); } /** Used by parent and child for keepalive */ var orphanExitCode = 100; var RequesterResponder = /** @class */ (function () { function RequesterResponder() { var _this = this; ///////////////////////////////// REQUESTOR ///////////////////////// this.currentListeners = {}; /** Only relevant when we only want the last of this type */ this.currentLastOfType = {}; this.pendingRequests = []; this.pendingRequestsChanged = function (pending) { return null; }; /** * This is used by both the request and the reponse */ this.sendToIpcHeart = function (data, message) { // If we don't have a child exit if (!_this.sendTarget()) { console.log('PARENT ERR: no child when you tried to send :', message); return Promise.reject(new Error("No worker active to recieve message: " + message)); } // Initialize if this is the first call of this type if (!_this.currentListeners[message]) _this.currentListeners[message] = {}; // Create an id unique to this call and store the defered against it var id = createId(); var promise = new Promise(function (resolve, reject) { _this.currentListeners[message][id] = { resolve: resolve, reject: reject, promise: promise }; }); // Send data to worker _this.pendingRequests.push(message); _this.pendingRequestsChanged(_this.pendingRequests); _this.sendTarget().send({ message: message, id: id, data: data, request: true }); return promise; }; ////////////////////////////////// RESPONDER //////////////////////// this.responders = {}; this.processRequest = function (m) { var parsed = m; if (!parsed.message || !_this.responders[parsed.message]) { // TODO: handle this error scenario. Either the message is invalid or we do not have a registered responder return; } var message = parsed.message; var responsePromise; try { responsePromise = _this.responders[message](parsed.data); } catch (err) { responsePromise = Promise.reject({ method: message, message: err.message, stack: err.stack, details: err.details || {} }); } responsePromise .then(function (response) { // console.log('I have the response for:',parsed.message) _this.sendTarget().send({ message: message, /** Note: to process a request we just pass the id as we recieve it */ id: parsed.id, data: response, error: null, request: false }); // console.log('I sent the response', parsed.message); }) .catch(function (error) { _this.sendTarget().send({ message: message, /** Note: to process a request we just pass the id as we recieve it */ id: parsed.id, data: null, error: error, request: false }); }); }; } /** process a message from the child */ RequesterResponder.prototype.processResponse = function (m) { var parsed = m; this.pendingRequests.shift(); this.pendingRequestsChanged(this.pendingRequests.slice()); if (!parsed.message || !parsed.id) { console.log('PARENT ERR: Invalid JSON data from child:', m); } else if (!this.currentListeners[parsed.message] || !this.currentListeners[parsed.message][parsed.id]) { console.log('PARENT ERR: No one was listening:', parsed.message, parsed.data); } else { if (parsed.error) { this.currentListeners[parsed.message][parsed.id].reject(parsed.error); console.log(parsed.error); console.log("======================= STACK (" + parsed.error.method + ") =========================="); console.log(parsed.error.stack); } else { this.currentListeners[parsed.message][parsed.id].resolve(parsed.data); } delete this.currentListeners[parsed.message][parsed.id]; // If there is current last one queued then that needs to be resurrected if (this.currentLastOfType[parsed.message]) { var last_1 = this.currentLastOfType[parsed.message]; delete this.currentLastOfType[parsed.message]; var lastPromise = this.sendToIpcHeart(last_1.data, parsed.message); lastPromise.then(function (res) { return last_1.defer.resolve(res); }, function (rej) { return last_1.defer.reject(rej); }); } } }; /** * Send all the member functions to IPC */ RequesterResponder.prototype.sendAllToIpc = function (contract) { var _this = this; var toret = {}; Object.keys(contract).forEach(function (key) { toret[key] = _this.sendToIpc(contract[key], key); }); return toret; }; /** * Takes a sync named function * and returns a function that will execute this function by name using IPC * (will only work if the process on the other side has this function as a registered responder) */ RequesterResponder.prototype.sendToIpc = function (func, name) { var _this = this; name = func.name || name; if (!name) { console.error('NO NAME for function', func.toString()); throw new Error('Name not specified for function: \n' + func.toString()); } return function (data) { return _this.sendToIpcHeart(data, name); }; }; /** * If there are more than one pending then we only want the last one as they come in. * All others will get the default value */ RequesterResponder.prototype.sendToIpcOnlyLast = function (func, defaultResponse) { var _this = this; return function (data) { var message = func.name; // If we don't have a child exit if (!_this.sendTarget()) { console.log('PARENT ERR: no child when you tried to send :', message); return Promise.reject(new Error("No worker active to recieve message: " + message)); } // Allow if this is the only call of this type if (!Object.keys(_this.currentListeners[message] || {}).length) { return _this.sendToIpcHeart(data, message); } else { // Note: // The last needs to continue once the current one finishes // That is done in our response handler // If there is already something queued as last. // Then it is no longer last and needs to be fed a default value if (_this.currentLastOfType[message]) { _this.currentLastOfType[message].defer.resolve(defaultResponse); } // this needs to be the new last var promise_1 = new Promise(function (resolve, reject) { _this.currentLastOfType[message] = { data: data, defer: { promise: promise_1, resolve: resolve, reject: reject } }; }); return promise_1; } }; }; RequesterResponder.prototype.addToResponders = function (func, name) { name = func.name || name; if (!name) { console.error('NO NAME for function', func.toString()); throw new Error('Name not specified for function: \n' + func.toString()); } this.responders[name] = func; }; RequesterResponder.prototype.registerAllFunctionsExportedFromAsResponders = function (aModule) { var _this = this; Object.keys(aModule) .filter(function (funcName) { return typeof aModule[funcName] == 'function'; }) .forEach(function (funcName) { return _this.addToResponders(aModule[funcName], funcName); }); }; return RequesterResponder; }()); /** The parent */ var Parent = /** @class */ (function (_super) { __extends(Parent, _super); function Parent() { var _this = _super !== null && _super.apply(this, arguments) || this; _this.node = process.execPath; /** If we get this error then the situation if fairly hopeless */ _this.gotENOENTonSpawnNode = false; _this.sendTarget = function () { return _this.child; }; _this.stopped = false; return _this; } /** start worker */ Parent.prototype.startWorker = function (childJsPath, terminalError, customArguments, onCrashRestart) { var _this = this; if (customArguments === void 0) { customArguments = []; } try { var fileName_1 = path.basename(childJsPath); this.child = cp.fork(childJsPath, customArguments, { cwd: path.dirname(childJsPath), env: process.env }); this.child.on('error', function (error) { var err = error; if (err.code === "ENOENT" && err.path === _this.node) { _this.gotENOENTonSpawnNode = true; } console.log('CHILD ERR ONERROR:', err.message, err.stack, err); _this.child = null; }); this.child.on('message', function (message) { // console.log('PARENT: A child asked me', message.message) if (message.request) { _this.processRequest(message); } else { _this.processResponse(message); } }); this.child.on('close', function (code) { if (_this.stopped) { return; } // Handle process dropping // If orphaned then Definitely restart if (code === orphanExitCode) { _this.startWorker(childJsPath, terminalError, customArguments, onCrashRestart); onCrashRestart(); } else if (_this.gotENOENTonSpawnNode) { terminalError(new Error('gotENOENTonSpawnNode')); } else { console.log(fileName_1 + " worker restarting. Don't know why it stopped with code:", code); _this.startWorker(childJsPath, terminalError, customArguments, onCrashRestart); onCrashRestart(); } }); } catch (err) { terminalError(err); } }; /** stop worker */ Parent.prototype.stopWorker = function () { this.stopped = true; if (!this.child) return; try { this.child.kill('SIGTERM'); } catch (ex) { console.error('failed to kill worker child'); } this.child = null; }; return Parent; }(RequesterResponder)); exports.Parent = Parent; var Child = /** @class */ (function (_super) { __extends(Child, _super); function Child() { var _this = _super.call(this) || this; _this.sendTarget = function () { return process; }; _this.connected = true; // Keep alive _this.keepAlive(); process.on('exit', function () { return _this.connected = false; }); // Start listening process.on('message', function (message) { // console.error('--------CHILD: parent told me :-/', message.message) if (message.request) { _this.processRequest(message); } else { _this.processResponse(message); } }); return _this; } /** keep the child process alive while its connected and die otherwise */ Child.prototype.keepAlive = function () { var _this = this; setInterval(function () { // We have been orphaned if (!_this.connected) { process.exit(orphanExitCode); } }, 1000); }; return Child; }(RequesterResponder)); exports.Child = Child;