UNPKG

@shahadul-17/dispatcher

Version:

Defines a mechanism for parallel processing and CPU intensive tasks in Node.js

252 lines 13.3 kB
"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