UNPKG

nope-js-node

Version:

NoPE Runtime for Nodejs. For Browser-Support please use nope-browser

844 lines (843 loc) 35.5 kB
"use strict"; /** * @author Martin Karkowski * @email m.karkowski@zema.de * @create date 2020-01-03 11:52:00 * @modify date 2022-01-05 17:51:09 * @desc [description] */ Object.defineProperty(exports, "__esModule", { value: true }); exports.NopeRpcManager = void 0; const index_1 = require("../../eventEmitter/index"); const async_1 = require("../../helpers/async"); const idMethods_1 = require("../../helpers/idMethods"); const mergedData_1 = require("../../helpers/mergedData"); const objectMethods_1 = require("../../helpers/objectMethods"); const path_1 = require("../../helpers/path"); const gc_1 = require("../../helpers/gc"); const getLogger_1 = require("../../logger/getLogger"); const index_browser_1 = require("../../logger/index.browser"); const nopePromise_1 = require("../../promise/nopePromise"); const index_2 = require("../ConnectivityManager/index"); /** * A Dispatcher to perform a function on a Remote * Dispatcher. Therefore a Task is created and forwarded * to the remote. * * For a detailled description please checkout {@link INopeRpcManager} * * @export * @class nopeDispatcher */ class NopeRpcManager { /** * Creates an instance of NopeRpcManager. * @param {INopeDispatcherOptions} options The Options, used by the rpc-manager. * @param {<T>() => INopeObservable<T>} _generateObservable helper to generate an nope observable. might be used to replace the default observable. * @param {ValidSelectorFunction} _defaultSelector Default selector see {@link INopeRpcManager.performCall} * @param {string} [_id=null] A Provided a for the rpc-manager * @param {INopeConnectivityManager} [_connectivityManager=null] A {@link INopeConnectivityManager} used to listen for new and dead dispatchers */ constructor(options, _generateObservable, _defaultSelector, _id = null, _connectivityManager = null) { this.options = options; this._generateObservable = _generateObservable; this._defaultSelector = _defaultSelector; this._id = _id; this._connectivityManager = _connectivityManager; this._communicator = options.communicator; if (_id == null) { this._id = (0, idMethods_1.generateId)(); } if (_connectivityManager == null) { // Creating a new Status-Manager. this._connectivityManager = new index_2.NopeConnectivityManager(options, _generateObservable, this._id); } this._logger = (0, getLogger_1.defineNopeLogger)(options.logger, `core.rpc-manager`); // Flag to show if the system is ready or not. this.ready = this._generateObservable(); this.ready.setContent(false); this.__warned = false; // Define A Proxy for accessing methods easier. const _this = this; const _handlerWithOptions = { get(target, name) { return (options, ...args) => { return _this.performCall(name, args, options); }; }, }; // Define the Proxy without the Options const _handlerWithoutOptions = { get(target, name) { return (...args) => { return _this.performCall(name, args); }; }, }; this.methodInterfaceWithOptions = new Proxy({}, _handlerWithOptions); this.methodInterface = new Proxy({}, _handlerWithoutOptions); this.services = new mergedData_1.MapBasedMergeData(this._mappingOfDispatchersAndServices, "services/+", "services/+/id"); this.onCancelTask = new index_1.NopeEventEmitter(); if (this._logger) { this._logger.info("manager created id=", this._id); } this.reset(); this._init().catch((error) => { if (_this._logger) { _this._logger.error("Failed to intialize the Dispatcher", error); } }); (0, gc_1.registerGarbageCallback)(this, this.dispose.bind(this)); } /** * Function, which will be called, if an dispatcher is updated. * This may leads to service that has been removed or added. * This change emitted on see {@link INopeRpcManager.services} * * @author M.Karkowski * @param {IAvailableServicesMsg} msg The Update Message see {@link IAvailableServicesMsg} * @memberof NopeRpcManager */ updateDispatcher(msg) { this._mappingOfDispatchersAndServices.set(msg.dispatcher, msg); this.services.update(); } /** * Internal Method to handle the rpcs requests. * * @protected * @param {IRequestRpcMsg} data The provided data of the request * @param {(...args) => Promise<any>} [_function] * @return {Promise<void>} * @memberof NopeRpcManager */ async _handleExternalRequest(data, _function = null) { var _a, _b; try { // Try to get the function if not provided: if (typeof _function !== "function") { _function = (_a = this._registeredServices.get(data.functionId)) === null || _a === void 0 ? void 0 : _a.func; } if ((_b = this._logger) === null || _b === void 0 ? void 0 : _b.enabledFor(index_browser_1.DEBUG)) { // If there is a Logger: this._logger.debug(`Dispatcher "${this._id}" received request: "${data.functionId}" -> task: "${data.taskId}"`); } const _this = this; if (typeof _function === "function") { // Now we check, if we have to perform test, whether // we are allowed to execute the task: if (data.target && data.target !== this._id) { return; } // Callbacks const cbs = []; const observer = _this.onCancelTask.subscribe((cancelEvent) => { if (cancelEvent.taskId == data.taskId) { // Call Every Callback. cbs.map((cb) => { return cb(cancelEvent.reason); }); // Although we are allowed to Cancel the Subscription observer.unsubscribe(); } }); // Only if the Function is present extract the arguments etc. const args = []; // First extract the basic arguments data.params.map((item) => { args[item.idx] = item.data; }); // Perform the Task it self. const _resultPromise = _function(...args); if (typeof (_resultPromise === null || _resultPromise === void 0 ? void 0 : _resultPromise.cancel) === "function") { // Push the Callback to the Result. cbs.push((reason) => { return _resultPromise.cancel(reason); }); } // Store, who has requested the task. _this._runningExternalRequestedTasks.set(data.taskId, data.requestedBy); let _result = null; try { // Wait for the Result to finish. _result = await _resultPromise; // Unsubscribe from Task-Cancelation observer.unsubscribe(); } catch (error) { // Unsubscribe from Task-Cancelation observer.unsubscribe(); // Delte the Task. _this._runningExternalRequestedTasks.delete(data.taskId); // Now throw the Error again. throw error; } // Delte the Task. _this._runningExternalRequestedTasks.delete(data.taskId); // Define the Result message const result = { result: typeof _result !== "undefined" ? _result : null, taskId: data.taskId, sink: data.resultSink, }; // Use the communicator to publish the result. await this._communicator.emit("rpcResponse", result); } } catch (error) { if (this._logger) { // If there is a Logger: this._logger.error(`Dispatcher "${this._id}" failed with request: "${data.taskId}"`); this._logger.error(error); } // Remove the requested task. this._runningExternalRequestedTasks.delete(data.taskId); // An Error occourd => Forward the Error. const result = { error: { error, msg: error.toString(), }, taskId: data.taskId, }; // Send the Error via the communicator to the remote. await this._communicator.emit("rpcResponse", result); } } /** * Internal Function to handle responses. In Generale, * the dispatcher checks if there is an open task with * the provided id. If so => finish the promise. * * @protected * @param {IRpcResponseMsg} data The Data provided to handle the Response. * @return {boolean} Returns a boolean, indicating whether a corresponding task was found or not. * @memberof nopeDispatcher */ _handleExternalResponse(data) { try { // Extract the Task const task = this._runningInternalRequestedTasks.get(data.taskId); // Delete the Task: this._runningInternalRequestedTasks.delete(data.taskId); // Based on the Result of the Remote => proceed. // Either throw an error or forward the result if (task && data.error) { if (this._logger) { this._logger.error(`Failed with task ${data.taskId}`); this._logger.error(`Reason: ${data.error.msg}`); this._logger.error(data.error); } task.reject(data.error); // Clearout the Timer if (task.timeout) { clearTimeout(task.timeout); } return true; } if (task) { task.resolve(data.result); // Clearout the Timer if (task.timeout) { clearTimeout(task.timeout); } return true; } } catch (e) { this._logger.error("Error during handling an external response"); this._logger.error(e); } return false; } /** * Function used to update the Available Services. * * @protected * @memberof nopeDispatcher */ async _sendAvailableServices() { var _a; // Define the Message const message = { dispatcher: this._id, services: Array.from(this._registeredServices.values()).map((item) => { return item.options; }), }; if ((_a = this._logger) === null || _a === void 0 ? void 0 : _a.enabledFor(index_browser_1.DEBUG)) { this._logger.debug("sending available services"); } // Send the Message. await this._communicator.emit("servicesChanged", message); } /** * Internal Function, used to initialize the Dispatcher. * It subscribes to the "Messages" of the communicator. * * @protected * @memberof nopeDispatcher */ async _init() { const _this = this; this.ready.setContent(false); // Wait until the Element is connected. await this._communicator.connected.waitFor(); await this._connectivityManager.ready.waitFor(); // Subscribe to the availableServices of Remotes. // If there is a new Service => udpate the External Services await this._communicator.on("servicesChanged", (data) => { try { _this.updateDispatcher(data); } catch (e) { this._logger.error("Error during handling an ServicesChanged"); this._logger.error(e); } }); await this._communicator.on("rpcRequest", (data) => { _this._handleExternalRequest(data); }); await this._communicator.on("rpcResponse", (data) => { _this._handleExternalResponse(data); }); // We will listen on Cancelations. await this._communicator.on("taskCancelation", (event) => { if (event.dispatcher === _this._id) { _this.onCancelTask.emit(event); } }); // Now we listen to unregisteredServices await this._communicator.on("rpcUnregister", (msg) => { if (_this._registeredServices.has(msg.identifier)) { _this.unregisterService(msg.identifier); } }); await this._communicator.on("bonjour", (msg) => { if (msg.dispatcherId !== _this._id) { // If there are dispatchers online, // We will emit our available services. _this ._sendAvailableServices() .then((_) => { }) .catch((e) => { var _a; if ((_a = _this._logger) === null || _a === void 0 ? void 0 : _a.enabledFor(index_browser_1.ERROR)) { // If there is a Logger: _this._logger.error(`Dispatcher "${_this._id}" failed to emit available services`); } }); } }); // We will use our connecitity-manager to listen to changes. this._connectivityManager.dispatchers.onChange.subscribe((changes) => { if (changes.added.length) { // If there are dispatchers online, // We will emit our available services. _this ._sendAvailableServices() .then((_) => { }) .catch((e) => { var _a; if ((_a = _this._logger) === null || _a === void 0 ? void 0 : _a.enabledFor(index_browser_1.ERROR)) { // If there is a Logger: _this._logger.error(`Dispatcher "${_this._id}" failed to emit available services`); } }); } if (changes.removed.length) { // Remove the dispatchers. changes.removed.map((item) => { return _this.removeDispatcher(item); }); } }); if (this._logger) { this._logger.info("core.rpc-manager", this._id, "initialized"); } this.ready.setContent(true); } /** * Helper to remove a dispatcher. This leads to * closing all open task related to this dispatcher -> * Exceptions should be thrown. Additional, internal * task, requested by the dispatcher will be canceled. * * @author M.Karkowski * @param {string} dispatcher * @memberof NopeRpcManager */ removeDispatcher(dispatcher) { // Delete the Generators of the Instances. this._mappingOfDispatchersAndServices.delete(dispatcher); this.services.update(); // Now we need to cancel every Task of the dispatcher, // which isnt present any more. this.cancelRunningTasksOfDispatcher(dispatcher, new Error("Dispatcher has been removed! Tasks cannot be executed any more.")); // Stop executing the requested Tasks. this.cancelRequestedTasksOfDispatcher(dispatcher, new Error("Dispatcher has been removed! Tasks are not required any more.")); } /** * Function to cancel an indivual Task. This might be the case, if a * connection to a specific dispatcher is lost or might have a user-based reason. * * @param {string} taskId The Id of the Task. Which should be canceled. * @param {Error} reason The Reason, why the Task should be canceled (In general shoudl be something meaning full) * @return {*} Flag, that indicates, whether cancelation was sucessfull or not. * @memberof nopeDispatcher */ async cancelTask(taskId, reason, quiet = false) { if (this._runningInternalRequestedTasks.has(taskId)) { const task = this._runningInternalRequestedTasks.get(taskId); // Delete the task this._runningInternalRequestedTasks.delete(taskId); // Propagate the Cancellation (internally): task.reject(reason); // Propagate the Cancellation externally. // Therefore use the desired Mode. await this._communicator.emit("taskCancelation", { dispatcher: this._id, reason, taskId, quiet, }); // Indicate a successful cancelation. return true; } // Task hasnt been found => Cancel the Task. return false; } async _cancelHelper(toCancel, reason) { if (toCancel.size) { const promises = []; for (const taskId of toCancel) { promises.push(this.cancelTask(taskId, reason)); } await Promise.all(promises); } } /** * Helper Function, used to close all tasks with a specific service. * * @protected * @param {string} serviceName The Name of the Service. * @param {Error} reason The provided Reason, why cancelation is reuqired. * @memberof nopeDispatcher */ async cancelRunningTasksOfService(serviceName, reason) { // Provide a List containing all Tasks, that has to be canceled const toCancel = new Set(); // Filter all Tasks that shoud be canceled. for (const [id, task] of this._runningInternalRequestedTasks.entries()) { // Therefore compare the reuqired Service by the Task if (task.serviceName === serviceName) { // if the service matches, put it to our list. toCancel.add(id); } } return await this._cancelHelper(toCancel, reason); } /** * Helper to cancel all Tasks which have been requested by a Dispatcher. * * @author M.Karkowski * @param {string} dispatcher * @param {Error} reason * @memberof NopeRpcManager */ async cancelRequestedTasksOfDispatcher(dispatcher, reason) { const toCancel = new Set(); for (const [taskId, requestedBy,] of this._runningExternalRequestedTasks.entries()) { if (requestedBy == dispatcher) { toCancel.add(taskId); } } return await this._cancelHelper(toCancel, reason); } /** * Cancels all Tasks of the given Dispatcher. * see {@link NopeRpcManager.cancelTask} * * @author M.Karkowski * @param {string} dispatcher * @param {Error} reason * @memberof NopeRpcManager */ async cancelRunningTasksOfDispatcher(dispatcher, reason) { // Provide a List containing all Tasks, that has to be canceled const toCancel = new Set(); // Filter all Tasks that shoud be canceled. for (const [id, task] of this._runningInternalRequestedTasks.entries()) { // Therefore compare the reuqired Service by the Task if (task.target === dispatcher) { // if the service matches, put it to our list. toCancel.add(id); } } return await this._cancelHelper(toCancel, reason); } /** * Function to test if a specific Service exists. * * @param {string} id The Id of the Serivce * @return {boolean} The result of the Test. True if either local or remotly a service is known. * @memberof nopeDispatcher */ serviceExists(id) { return this.services.amountOf.has(id); } /** * Simple checker, to test, if this rpc-mananger is providing a service with the given id. * * @param id The id of the service, which is used during registration * @return {boolean} The result */ isProviding(id) { return this._registeredServices.has(id); } /** * Function to adapt a Request name. * Only used internally * * @protected * @param {string} id the original ID * @return {string} the adapted ID. * @memberof nopeDispatcher */ _getServiceName(id, type) { return id.startsWith(`${type}/`) ? id : `${type}/${id}`; } /** * Function to unregister a Function from the Dispatcher * @param {(((...args) => void) | string | number)} func The Function to unregister * @return {boolean} Flag, whether the element was removed (only if found) or not. * @memberof nopeDispatcher */ async unregisterService(func) { var _a; const _id = typeof func === "string" ? this.options.forceUsingValidVarNames ? (0, path_1.varifyPath)(func) : func : func.id || "0"; const res = this._registeredServices.delete(_id); if ((_a = this._logger) === null || _a === void 0 ? void 0 : _a.enabledFor(index_browser_1.DEBUG)) { // If there is a Logger: this._logger.debug(`Dispatcher "${this._id}" unregistered: "${_id}"`); } // Publish the Available Services. await this._sendAvailableServices(); return res; } adaptServiceId(name) { if (name.startsWith(`nope${objectMethods_1.SPLITCHAR}service${objectMethods_1.SPLITCHAR}`)) { return name; } return `nope${objectMethods_1.SPLITCHAR}service${objectMethods_1.SPLITCHAR}${name}`; } /** * Function to register a Function in the Dispatcher * * @param {(...args) => Promise<any>} func The function which should be called if a request is mapped to the Function. * @param {{ * // Flag to enable unregistering the function after calling. * deleteAfterCalling?: boolean, * // Instead of generating a uuid an id could be provided * id?: string; * }} [options={}] Options to enhance the registered ID and enabling unregistering the Element after calling it. * @return {(...args) => Promise<any>} The registered Function * @memberof nopeDispatcher */ async registerService(func, options) { var _a; const _this = this; // Define / Use the ID of the Function. let _id = options.id || (0, idMethods_1.generateId)(); _id = options.addNopeServiceIdPrefix ? this.adaptServiceId(_id) : _id; _id = this.options.forceUsingValidVarNames ? (0, path_1.varifyPath)(_id) : _id; // Make shure we assign our id options.id = _id; if (this.isProviding(options.id) && this._registeredServices.get(options.id).func != func) { const err = Error(`The service "${_id}" is already declared!`); this._logger.error(`The service "${_id}" is already declared!`); this._logger.error(err); throw err; } let _func = func; if (!this.__warned && !(0, async_1.isAsyncFunction)(func) && this._logger) { this._logger.warn("!!! You have provided synchronous functions. They may break NoPE. Use them with care !!!"); this._logger.warn(`The service "${_id}" is synchronous!`); this.__warned = true; } // Define a ID for the Function _func.id = _id; // Define the callback. _func.unregister = () => { return _this.unregisterService(_id); }; // Register the Function this._registeredServices.set(_func.id, { options: options, func: _func, }); // Publish the Available Services. await this._sendAvailableServices(); if ((_a = this._logger) === null || _a === void 0 ? void 0 : _a.enabledFor(index_browser_1.DEBUG)) { // If there is a Logger: this._logger.debug(`Dispatcher "${this._id}" registered: "${_id}"`); } // Return the Function. return _func; } /** * Function which is used to perform a call on the remote. * * @author M.Karkowski * @template T * @param {string} serviceName serviceName The Name / ID of the Function * @param {any[]} params * @param {(Partial<ICallOptions> & { * selector?: ValidSelectorFunction; * quiet?: boolean; * })} [options={}] Options for the Call. You can assign a different selector. * @return {*} {INopePromise<T>} The result of the call * @memberof nopeDispatcher */ _performCall(serviceName, params, options = {}) { var _a; // Our implemented Shortcut to speed things up. if (this.services.amountOf.get(serviceName) === 1 && this._registeredServices.has(serviceName)) { const func = this._registeredServices.get(serviceName).func; const perhapsPromise = func(...params); const isAsync = (0, async_1.isAsyncFunction)(func); // Define a simple promise. const ret = new nopePromise_1.NopePromise((resolve, reject) => { if (options.timeout > 0) { setTimeout(reject, options.timeout, new Error(`TIMEOUT. The Service allowed execution time of ${options.timeout.toString()}[ms] has been excided`)); } if (!isAsync) { resolve(perhapsPromise); } else { perhapsPromise.then(resolve).catch(reject); } }); // Assign the cancel function. ret.cancel = (reason) => { if (isAsync && typeof perhapsPromise.cancel == "function") { perhapsPromise.cancel(reason); } }; return ret; } // Get a Call Id const _taskId = (0, idMethods_1.generateId)(); const _this = this; const _options = { resultSink: this._getServiceName(serviceName, "response"), ...options, }; const clear = () => { // Remove the task: if (_this._runningInternalRequestedTasks.has(_taskId)) { const task = _this._runningInternalRequestedTasks.get(_taskId); // Remove the Timeout. if (task.timeout) { clearTimeout(task.timeout); } // Remove the Task itself _this._runningInternalRequestedTasks.delete(_taskId); } }; if ((_a = _this._logger) === null || _a === void 0 ? void 0 : _a.enabledFor(index_browser_1.DEBUG)) { _this._logger.debug(`Dispatcher "${this._id}" requesting externally Function "${serviceName}" with task: "${_taskId}"`); } // Define a Callback-Function, which will expect the Task. const ret = new nopePromise_1.NopePromise(async (resolve, reject) => { var _a; try { const taskRequest = { resolve, reject, clear, serviceName, timeout: null, target: null, }; // Register the Handlers, _this._runningInternalRequestedTasks.set(_taskId, taskRequest); // Define a Task-Request const packet = { functionId: serviceName, params: [], taskId: _taskId, resultSink: _options.resultSink, requestedBy: _this._id, }; for (const [idx, contentOfParameter] of params.entries()) { packet.params.push({ idx, data: contentOfParameter, }); } if (!_this.serviceExists(serviceName)) { // Create an Error: const error = new Error(`No Service Provider known for "${serviceName}"`); if (_this._logger) { _this._logger.error(`No Service Provider known for "${serviceName}"`); _this._logger.error(error); } throw error; } if (_this.options.forceUsingSelectors || this.services.amountOf.get(serviceName) > 1) { // Fixing the Selection if (typeof options.target === "string") { taskRequest.target = options.target; } else if (typeof (options === null || options === void 0 ? void 0 : options.selector) === "function") { const dispatcherToUse = await options.selector({ rpcManager: this, serviceName, }); // Assign the Selector: taskRequest.target = dispatcherToUse; } else { const dispatcherToUse = await this._defaultSelector({ rpcManager: this, serviceName, }); // Assign the Selector: taskRequest.target = dispatcherToUse; } packet.target = taskRequest.target; } else { taskRequest.target = Array.from(this.services.keyMappingReverse.get(serviceName))[0]; } // Send the Message to the specific element: await _this._communicator.emit("rpcRequest", packet); if ((_a = _this._logger) === null || _a === void 0 ? void 0 : _a.enabledFor(index_browser_1.DEBUG)) { _this._logger.debug(`Dispatcher "${this._id}" putting task "${_taskId}" on: "${_this._getServiceName(packet.functionId, "request")}"`); } // If there is a timeout => if (options.timeout > 0) { taskRequest.timeout = setTimeout(() => { _this .cancelTask(_taskId, new Error(`TIMEOUT. The Service allowed execution time of ${options.timeout.toString()}[ms] has been excided`), false) .catch((e) => { _this._logger.error(`Failed to cancel the task ${_taskId}.`); }); }, options.timeout); } } catch (e) { // Clear all Elements of the Function: clear(); // Throw the error. reject(e); } }); ret.taskId = _taskId; ret.cancel = (reason) => { _this.cancelTask(_taskId, reason); }; return ret; } /** * Function which is used to perform a call on the remote. * Please see {@link INopeRpcManager.performCall} for more Info. */ performCall(serviceName, params, options) { if (Array.isArray(serviceName)) { if (Array.isArray(options) && options.length !== serviceName.length) { throw Error("Array Length must match."); } const promises = serviceName.map((service, idx) => { return this._performCall(service, params, Array.isArray(options) ? options[idx] : options); }); return Promise.all(promises); } else { if (Array.isArray(options)) { // Throw an error. throw Error("Expecting a single Value for the options"); } return this._performCall(serviceName, params, options); } } /** * Function to clear all pending tasks * * @memberof nopeDispatcher */ clearTasks() { if (this._runningInternalRequestedTasks) { this._runningInternalRequestedTasks.clear(); } else this._runningInternalRequestedTasks = new Map(); } /** * Function to unregister all Functions of the Dispatcher. * * @memberof nopeDispatcher */ unregisterAll() { if (this._registeredServices) { const toUnregister = Array.from(this._registeredServices.keys()); for (const id of toUnregister) { this.unregisterService(id); } this._registeredServices.clear(); } else { this._registeredServices = new Map(); } } /** * Function to reset the Dispatcher. * * @memberof nopeDispatcher */ reset() { this.clearTasks(); this.unregisterAll(); this._mappingOfDispatchersAndServices = new Map(); this.services.update(this._mappingOfDispatchersAndServices); this._runningExternalRequestedTasks = new Map(); } async dispose() { this.clearTasks(); this.unregisterAll(); } /** * Describes the Data. * @returns */ toDescription() { const ret = { services: { all: this.services.data.getContent(), internal: Array.from(this._registeredServices.values()), }, task: { executing: Array.from(this._runningExternalRequestedTasks.values()), requested: Array.from(this._runningInternalRequestedTasks.entries()).map((item) => { return { id: item[0], service: item[1].serviceName, target: item[1].target, timeout: item[1].timeout, }; }), }, }; return ret; } } exports.NopeRpcManager = NopeRpcManager;