alm
Version:
The best IDE for TypeScript
344 lines (343 loc) • 14.6 kB
JavaScript
;
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;