@shahadul-17/dispatcher
Version:
Defines a mechanism for parallel processing and CPU intensive tasks in Node.js
252 lines • 13.3 kB
JavaScript
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Dispatcher = void 0;
const utilities_1 = require("@shahadul-17/utilities");
const uid_generator_1 = require("@shahadul-17/uid-generator");
const dispatcher_ipc_payload_flag_e_1 = require("./dispatcher-ipc-payload-flag.e");
const process_1 = require("./process");
const logger_1 = require("@shahadul-17/logger");
const CHILD_PROCESS_FILE_NAME_WITHOUT_EXTENSION = "dispatcher-child-process";
class Dispatcher {
constructor(options) {
this.isStarting = false;
this._isStarted = false;
this.logger = new logger_1.Logger(Dispatcher.name);
this.uidGenerator = uid_generator_1.UIDGenerator.create();
this._options = options;
this.options.serviceInitializerPath = utilities_1.StringUtilities.getDefaultIfUndefinedOrNullOrEmpty(this.options.serviceInitializerPath, utilities_1.StringUtilities.getEmptyString(), true);
if (!utilities_1.FileUtilities.exists('file', this.options.serviceInitializerPath)) {
throw new Error("Invalid service initializer path provided.");
}
if (!utilities_1.NumberUtilities.isPositiveNumber(this.options.processCount)) {
this.options.processCount = 1;
}
this.processes = new Array(this.options.processCount);
// binding methods...
this.log = this.log.bind(this);
this.getLeastBusyProcess = this.getLeastBusyProcess.bind(this);
this.onChildProcessLogReceivedAsync = this.onChildProcessLogReceivedAsync.bind(this);
this.onChildProcessResponseReceivedAsync = this.onChildProcessResponseReceivedAsync.bind(this);
this.onEventOccurredAsync = this.onEventOccurredAsync.bind(this);
this.dispatchAsync = this.dispatchAsync.bind(this);
this.get = this.get.bind(this);
this.startAsync = this.startAsync.bind(this);
this.stopAsync = this.stopAsync.bind(this);
}
get isStarted() {
return this._isStarted;
}
get processCount() {
return this.options.processCount;
}
get options() {
return this._options;
}
log(logLevel, ...parameters) {
this.logger.log(logLevel, ...parameters);
}
getLeastBusyProcess() {
let leastBusyProcess = this.processes[0];
// we shall iterate over all the processes...
for (let i = 1; i < this.processes.length; ++i) {
const process = this.processes[i];
// if the task count of the process is greater than
// or equal to our current least busy process's task count,
// we shall skip this iteration...
if (process.taskCount >= leastBusyProcess.taskCount) {
continue;
}
// otherwise, we shall assign the process as our least busy process...
leastBusyProcess = process;
}
// we must increment the process's task count by 1...
leastBusyProcess.incrementTaskCount();
return leastBusyProcess;
}
dispatchAsync(taskInformation) {
return new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () {
if (!this.isStarted) {
return reject(new Error("Dispatcher is not started yet."));
}
if (typeof taskInformation.serviceType !== "function") {
return reject(new Error("No service provided."));
}
taskInformation.methodName = utilities_1.StringUtilities.getDefaultIfUndefinedOrNullOrEmpty(taskInformation.methodName, utilities_1.StringUtilities.getEmptyString(), true);
if (utilities_1.StringUtilities.isEmpty(taskInformation.methodName)) {
return reject(new Error("Service method name not provided."));
}
// first we shall generate a unique payload ID...
const payloadId = this.uidGenerator.generate();
// we shall retrieve the least busy process...
const process = this.getLeastBusyProcess();
// then we shall prepare the payload to be sent to the process...
const payload = {
flag: dispatcher_ipc_payload_flag_e_1.DispatcherIpcPayloadFlag.Dispatch,
payloadId: payloadId,
methodName: taskInformation.methodName,
methodArguments: taskInformation.methodArguments,
serviceName: taskInformation.serviceType.name,
serviceScopeName: taskInformation.serviceScopeName,
processId: process.processId,
};
const dataReceiveCallbackFunctionName = `onResponseReceivedAsync_${payloadId}_${process.processId}`;
// NOTE: THIS OBJECT IS USED SO THAT WE CAN GENERATE UNIQUE FUNCTION NAMES.
// THIS WILL BE USED WHILE REMOVING THE FUNCTION FROM THE EVENT LISTENERS...
const callbackFunctionContainer = {
// this function gets executed when data is received from the process...
[dataReceiveCallbackFunctionName]: function (eventArguments) {
return __awaiter(this, void 0, void 0, function* () {
// if the event arguments do not contain data, we'll not proceed any further...
if (!utilities_1.ObjectUtilities.isObject(eventArguments.data)) {
return;
}
const payload = eventArguments.data;
// if the payload ID does not match our generated payload ID, we'll not proceed any further...
if (payload.payloadId !== payloadId) {
return;
}
// now, we can safely remove the event listener...
process.removeEventListener(callbackFunctionContainer[dataReceiveCallbackFunctionName], process_1.ProcessEventType.DataReceive);
if (payload.flag === dispatcher_ipc_payload_flag_e_1.DispatcherIpcPayloadFlag.Dispatch) {
// we'll decrement the process's task count...
process.decrementTaskCount();
return resolve(payload.result);
}
else if (payload.flag === dispatcher_ipc_payload_flag_e_1.DispatcherIpcPayloadFlag.Error) {
// we'll decrement the process's task count...
process.decrementTaskCount();
if (!utilities_1.ObjectUtilities.isObject(payload.result)) {
return reject(new Error("An unexpected error occurred while executing the service method."));
}
const message = utilities_1.StringUtilities.getDefaultIfUndefinedOrNullOrEmpty(payload.result.message, "An unexpected error occurred while executing the service method.", true);
const stack = utilities_1.StringUtilities.getDefaultIfUndefinedOrNullOrEmpty(payload.result.stack, utilities_1.StringUtilities.getEmptyString(), true);
const error = new Error(message);
if (!utilities_1.StringUtilities.isEmpty(stack)) {
error.stack = stack;
}
return reject(error);
}
});
},
};
// before sending payload to the process, we must first add a listener to the data receive event of the process...
process.addEventListener(process_1.ProcessEventType.DataReceive, callbackFunctionContainer[dataReceiveCallbackFunctionName]);
// now we'll send the payload to the process...
yield process.sendAsync(payload);
}));
}
get(serviceType, scopeName) {
const context = this;
const serviceProxy = new Proxy(utilities_1.ObjectUtilities.getEmptyObject(), {
get(_, property) {
return function (...args) {
return context.dispatchAsync({
serviceType: serviceType,
serviceScopeName: scopeName,
methodName: property,
returnType: utilities_1.ObjectUtilities.getEmptyObject(),
methodArguments: Array.isArray(args) ? args : undefined,
});
};
},
});
return serviceProxy;
}
onChildProcessLogReceivedAsync(payload) {
return __awaiter(this, void 0, void 0, function* () {
const { logLevel, parameters, } = payload.result;
this.log(logLevel, `[Process ${payload.processId}]`, ...parameters);
});
}
onChildProcessResponseReceivedAsync(payload) {
return __awaiter(this, void 0, void 0, function* () { });
}
onEventOccurredAsync(eventArguments) {
return __awaiter(this, void 0, void 0, function* () {
if (eventArguments.type === process_1.ProcessEventType.DataReceive) {
if (!utilities_1.ObjectUtilities.isObject(eventArguments.data)) {
return;
}
const payload = eventArguments.data;
if (payload.flag === dispatcher_ipc_payload_flag_e_1.DispatcherIpcPayloadFlag.Log) {
yield this.onChildProcessLogReceivedAsync(payload);
}
else if (payload.flag === dispatcher_ipc_payload_flag_e_1.DispatcherIpcPayloadFlag.Dispatch) {
yield this.onChildProcessResponseReceivedAsync(payload);
}
else if (payload.flag === dispatcher_ipc_payload_flag_e_1.DispatcherIpcPayloadFlag.Error) {
// if the payload does not contain an ID...
if (utilities_1.StringUtilities.isUndefinedOrNullOrEmpty(payload.payloadId, true)) {
this.log(logger_1.LogLevel.Error, `[Process ${eventArguments.processId}]: The following error occurred.`, payload.result);
}
}
}
else if (eventArguments.type === process_1.ProcessEventType.Error) {
this.log(logger_1.LogLevel.Error, `[Process ${eventArguments.processId}]: The following error occurred.`, eventArguments.error);
}
});
}
startAsync() {
return __awaiter(this, void 0, void 0, function* () {
if (this.isStarting) {
return;
}
this.isStarting = true;
const processEventTypes = [
process_1.ProcessEventType.Spawn,
process_1.ProcessEventType.Disconnect,
process_1.ProcessEventType.DataReceive,
process_1.ProcessEventType.Error,
process_1.ProcessEventType.Exit,
process_1.ProcessEventType.Close,
];
const promises = new Array(this.options.processCount);
for (let i = 0; i < promises.length; ++i) {
const process = new process_1.Process({
processId: i,
processFileNameWithoutExtension: CHILD_PROCESS_FILE_NAME_WITHOUT_EXTENSION,
commandLineArguments: {
serviceInitializerPath: this.options.serviceInitializerPath,
serviceInitializerClassName: this.options.serviceInitializerClassName,
},
});
for (const processEventType of processEventTypes) {
process.addEventListener(processEventType, this.onEventOccurredAsync);
}
promises[i] = process.spawnAsync();
}
let processes;
try {
processes = yield Promise.all(promises);
}
catch (error) {
this.isStarting = false;
throw error;
}
for (let i = 0; i < processes.length; ++i) {
this.processes[i] = processes[i];
}
this._isStarted = true;
});
}
stopAsync() {
return __awaiter(this, void 0, void 0, function* () {
this._isStarted = false;
});
}
static createInstance(options) {
const dispatcher = new Dispatcher(options);
return dispatcher;
}
}
exports.Dispatcher = Dispatcher;
//# sourceMappingURL=dispatcher.js.map