nope/lib/dispatcher/RpcManager/NopeRpcManager.ts

1092 lines
30 KiB
TypeScript
Raw Normal View History

2021-12-04 07:25:26 +00:00
/**
* @author Martin Karkowski
* @email m.karkowski@zema.de
* @create date 2020-01-03 11:52:00
* @modify date 2022-01-05 17:51:09
2021-12-04 07:25:26 +00:00
* @desc [description]
*/
import { ILogger } from "js-logger";
import { NopeEventEmitter } from "../../eventEmitter/index";
import { isAsyncFunction } from "../../helpers/async";
import { generateId } from "../../helpers/idMethods";
import { MapBasedMergeData } from "../../helpers/mergedData";
import { SPLITCHAR } from "../../helpers/objectMethods";
import { defineNopeLogger } from "../../logger/getLogger";
import { DEBUG } from "../../logger/index.browser";
import { NopePromise } from "../../promise/nopePromise";
2021-12-04 07:25:26 +00:00
import {
IAvailableServicesMsg,
ICallOptions,
ICommunicationBridge,
IExtraData,
2022-01-26 12:03:03 +00:00
IFunctionOptions,
2022-01-03 15:46:36 +00:00
IMapBasedMergeData,
INopeDispatcherOptions,
2021-12-04 07:25:26 +00:00
INopeEventEmitter,
INopeObservable,
INopePromise,
INopeRpcManager,
IRequestRpcMsg,
IRpcResponseMsg,
2022-01-03 15:46:36 +00:00
ITaskCancelationMsg,
ValidCallOptions,
2022-01-03 15:46:36 +00:00
ValidSelectorFunction,
} from "../../types/nope/index";
import {
INopeConnectivityManager,
NopeConnectivityManager,
} from "../ConnectivityManager/index";
2021-12-04 07:25:26 +00:00
/**
* A Dispatcher to perform a function on a Remote
* Dispatcher. Therefore a Task is created and forwarded
* to the remote.
*
* @export
* @class nopeDispatcher
*/
2022-01-26 12:03:03 +00:00
export class NopeRpcManager<T extends IFunctionOptions = IFunctionOptions>
implements INopeRpcManager
{
2021-12-04 07:25:26 +00:00
protected _logger: ILogger;
/**
* Internal Element to store the registered Functions
*
* @protected
* @memberof nopeDispatcher
*/
2022-01-03 15:27:05 +00:00
protected _registeredServices: Map<
2021-12-04 07:25:26 +00:00
string,
{
2021-12-23 10:49:15 +00:00
options: T;
2021-12-04 07:25:26 +00:00
func: (...args) => Promise<any>;
}
>;
protected _communicatorCallbacks: Map<
string,
{
registeredId: string;
type: "request" | "response";
cb: (data) => any;
}
>;
/**
* A Mapping of the Services a dispatcher is hosting.
* Key = Dispatcher-ID
* Value = Available Services
*
* @protected
* @type {Map<
* string,
* IAvailableServicesMsg
* >}
* @memberof nopeDispatcher
*/
protected _mappingOfDispatchersAndServices: Map<
string,
IAvailableServicesMsg
>;
/**
* Proxy for accessing the Methods. This proxy provides additional
* options, which can be used to detail the calls.
*
* @author M.Karkowski
* @memberof NopeRpcManager
*/
public methodInterfaceWithOptions: {
2022-01-03 18:13:51 +00:00
[index: string]: <T>(
options: Partial<ICallOptions>,
...args
) => INopePromise<T>;
2021-12-04 07:25:26 +00:00
};
/**
* Proxy for accessing the Methods. This proxy provides additional
* options, which can be used to detail the calls.
*
* @author M.Karkowski
* @memberof NopeRpcManager
*/
public methodInterface: { [index: string]: <T>(...args) => INopePromise<T> };
/**
* Element showing the available services.
2021-12-23 10:48:06 +00:00
* Its more or less a map, that maps the
* services with their dispatchers.
2021-12-04 07:25:26 +00:00
*
2022-07-07 07:14:22 +00:00
* OriginalKey = Dispatcher ID (string);
* OriginalValue = Original Message (IAvailableServicesMsg);
* ExtractedKey = Function ID (string);
* ExtractedValue = FunctionOptions (T);
2021-12-04 07:25:26 +00:00
*
* @author M.Karkowski
* @type {IMapBasedMergeData<string>}
* @memberof INopeRpcManager
*/
2021-12-23 10:49:15 +00:00
public readonly services: IMapBasedMergeData<
2022-07-07 07:14:22 +00:00
string, // Dispatcher ID
IAvailableServicesMsg, // Original Message
string, // Function ID
T // Function Options
2021-12-23 10:49:15 +00:00
>;
2021-12-04 07:25:26 +00:00
/**
* An event Emitter, which will be called when a task is getting
* canceled.
*
* @author M.Karkowski
* @type {INopeEventEmitter<ITaskCancelationMsg>}
* @memberof NopeRpcManager
*/
public readonly onCancelTask: INopeEventEmitter<ITaskCancelationMsg>;
/**
* Internal Element to store the running tasks.
*
* @protected
* @memberof nopeDispatcher
*/
protected _runningInternalRequestedTasks: Map<
string,
{
resolve: (value: any) => void;
reject: (error: any) => void;
clear: () => void;
serviceName: string;
timeout?: any;
2021-12-23 10:49:15 +00:00
target: string;
2021-12-04 07:25:26 +00:00
}
>;
2021-12-23 10:48:06 +00:00
/**
* List, with external tasks, that are running.
* key = task-id
* value = id of the requester
2021-12-23 10:48:06 +00:00
*/
protected _runningExternalRequestedTasks: Map<string, string>;
2021-12-04 07:25:26 +00:00
2021-12-23 10:48:06 +00:00
/**
* Flag to show an inital warning
*/
2021-12-04 07:25:26 +00:00
protected __warned: boolean;
2022-01-04 11:40:40 +00:00
/**
* The used Communication interface
*
*/
protected readonly _communicator: ICommunicationBridge;
2022-01-04 11:40:40 +00:00
/**
* Flag to indicate, that the system is ready.
*
* @author M.Karkowski
* @type {INopeObservable<boolean>}
* @memberof NopeRpcManager
*/
public readonly ready: INopeObservable<boolean>;
2021-12-04 07:25:26 +00:00
/**
* Creates an instance of nopeDispatcher.
* @param {nopeRpcDispatcherOptions} options The Options, used by the Dispatcher.
* @param {() => INopeObservable<IExternalEventMsg>} _generateObservable A Helper, to generate Observables.
* @memberof nopeDispatcher
*/
constructor(
public options: INopeDispatcherOptions,
2022-01-03 15:27:05 +00:00
protected _generateObservable: <T>() => INopeObservable<T>,
protected _defaultSelector: ValidSelectorFunction,
protected readonly _id: string = null,
protected _connectivityManager: INopeConnectivityManager = null
2021-12-04 07:25:26 +00:00
) {
this._communicator = options.communicator;
2021-12-04 07:25:26 +00:00
if (_id == null) {
this._id = generateId();
2022-01-03 15:27:05 +00:00
}
2021-12-04 07:25:26 +00:00
if (_connectivityManager == null) {
// Creating a new Status-Manager.
this._connectivityManager = new NopeConnectivityManager(
options,
_generateObservable,
this._id
);
}
this._logger = defineNopeLogger(options.logger, `core.rpc-manager`);
2022-01-03 18:13:51 +00:00
// Flag to show if the system is ready or not.
this.ready = this._generateObservable();
this.ready.setContent(false);
2021-12-04 07:25:26 +00:00
this.__warned = false;
// Define A Proxy for accessing methods easier.
const _this = this;
const _handlerWithOptions = {
get(target, name) {
return (options: ICallOptions, ...args) =>
_this.performCall(name, args, options);
},
};
// Define the Proxy without the Options
const _handlerWithoutOptions = {
get(target, name) {
return (...args) => _this.performCall(name, args);
},
};
this.methodInterfaceWithOptions = new Proxy({}, _handlerWithOptions);
this.methodInterface = new Proxy({}, _handlerWithoutOptions);
this.services = new MapBasedMergeData(
this._mappingOfDispatchersAndServices,
2022-07-07 07:14:22 +00:00
"services/+",
"services/+/id"
2021-12-04 07:25:26 +00:00
);
this.onCancelTask = new NopeEventEmitter();
if (this._logger) {
this._logger.info("manager created id=", this._id);
2021-12-04 07:25:26 +00:00
}
2021-12-04 07:25:26 +00:00
this.reset();
this._init().catch((error) => {
if (_this._logger) {
_this._logger.error("Failed to intialize the Dispatcher", error);
}
});
}
/**
* Function, which will be called, if an dispatcher matches
*
* @author M.Karkowski
* @param {IAvailableServicesMsg} msg
* @memberof NopeRpcManager
*/
public updateDispatcher(msg: IAvailableServicesMsg): void {
this._mappingOfDispatchersAndServices.set(msg.dispatcher, msg);
this.services.update();
}
/**
2022-01-04 11:40:40 +00:00
* Internal Method to handle the rpcs requests.
2021-12-04 07:25:26 +00:00
*
* @protected
* @param {IRequestRpcMsg} data The provided data of the request
2022-01-04 11:40:40 +00:00
* @param {(...args) => Promise<any>} [_function]
* @return {*} {Promise<void>}
* @memberof NopeRpcManager
2021-12-04 07:25:26 +00:00
*/
protected async _handleExternalRequest(
data: IRequestRpcMsg & { target?: string },
_function: (...args) => Promise<any> = null
2021-12-04 07:25:26 +00:00
): Promise<void> {
try {
// Try to get the function if not provided:
if (typeof _function !== "function") {
2022-01-03 15:27:05 +00:00
_function = this._registeredServices.get(data.functionId)?.func;
2021-12-04 07:25:26 +00:00
}
if (this._logger?.enabledFor(DEBUG)) {
2021-12-04 07:25:26 +00:00
// If there is a Logger:
this._logger.debug(
`Dispatcher "${this._id}" received request: "${data.functionId}" -> task: "${data.taskId}"`
2021-12-04 07:25:26 +00:00
);
}
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) {
2022-01-03 15:46:36 +00:00
return;
2021-12-04 07:25:26 +00:00
}
// Callbacks
const cbs: Array<(reason) => void> = [];
const observer = _this.onCancelTask.subscribe((cancelEvent) => {
if (cancelEvent.taskId == data.taskId) {
// Call Every Callback.
cbs.map((cb) => 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 as INopePromise<any>)?.cancel === "function"
) {
// Push the Callback to the Result.
cbs.push((reason) =>
(_resultPromise as INopePromise<any>).cancel(reason)
);
}
// Store, who has requested the task.
_this._runningExternalRequestedTasks.set(data.taskId, data.requestedBy);
let _result: any = 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();
// Now throw the Error again.
throw error;
}
2021-12-04 07:25:26 +00:00
// Define the Result message
const result: IRpcResponseMsg = {
2021-12-04 07:25:26 +00:00
result: typeof _result !== "undefined" ? _result : null,
taskId: data.taskId,
sink: data.resultSink,
2021-12-04 07:25:26 +00:00
};
// Use the communicator to publish the result.
await this._communicator.emit("RpcResponse", result);
2021-12-04 07:25:26 +00:00
}
} catch (error) {
if (this._logger) {
// If there is a Logger:
this._logger.error(
`Dispatcher "${this._id}" failed with request: "${data.taskId}"`
2021-12-04 07:25:26 +00:00
);
this._logger.error(error);
}
// An Error occourd => Forward the Error.
const result: IRpcResponseMsg = {
2021-12-04 07:25:26 +00:00
error: {
error,
msg: error.toString(),
},
taskId: data.taskId,
};
// Send the Error via the communicator to the remote.
await this._communicator.emit("RpcResponse", result);
2021-12-04 07:25:26 +00:00
}
}
/**
* 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.
2021-12-04 07:25:26 +00:00
* @return {boolean} Returns a boolean, indicating whether a corresponding task was found or not.
* @memberof nopeDispatcher
*/
protected _handleExternalResponse(data: IRpcResponseMsg): boolean {
2021-12-04 07:25:26 +00:00
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
*/
protected _sendAvailableServices(): void {
// Define the Message
const message: IAvailableServicesMsg = {
dispatcher: this._id,
2022-07-07 07:14:22 +00:00
services: Array.from(this._registeredServices.values()).map(
(item) => item.options
),
2021-12-04 07:25:26 +00:00
};
if (this._logger?.enabledFor(DEBUG)) {
2021-12-04 07:25:26 +00:00
this._logger.debug("sending available services");
}
// Send the Message.
this._communicator.emit("ServicesChanged", message);
2021-12-04 07:25:26 +00:00
}
/**
* Internal Function, used to initialize the Dispatcher.
* It subscribes to the "Messages" of the communicator.
*
* @protected
* @memberof nopeDispatcher
*/
protected async _init(): Promise<void> {
const _this = this;
2022-01-03 18:13:51 +00:00
this.ready.setContent(false);
2021-12-04 07:25:26 +00:00
// Wait until the Element is connected.
await this._communicator.connected.waitFor();
await this._connectivityManager.ready.waitFor();
2021-12-04 07:25:26 +00:00
// Subscribe to the availableServices of Remotes.
// If there is a new Service => udpate the External Services
await this._communicator.on("ServicesChanged", (data) => {
2021-12-04 07:25:26 +00:00
try {
_this.updateDispatcher(data);
} catch (e) {
this._logger.error("Error during handling an onNewServicesAvailable");
this._logger.error(e);
}
});
await this._communicator.on("RpcRequest", (data) =>
_this._handleExternalRequest(data)
);
await this._communicator.on("RpcResponse", (data) =>
_this._handleExternalResponse(data)
);
2021-12-04 07:25:26 +00:00
// We will listen on Cancelations.
await this._communicator.on("TaskCancelation", (event) => {
if (event.dispatcher !== _this._id) {
2021-12-04 07:25:26 +00:00
_this.onCancelTask.emit(event);
}
});
// Now we listen to unregisteredServices
await this._communicator.on("RpcUnregister", (msg) => {
2022-01-03 15:27:05 +00:00
if (_this._registeredServices.has(msg.identifier)) {
_this._unregisterService(msg.identifier);
2021-12-04 07:25:26 +00:00
}
});
// 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();
}
if (changes.removed.length) {
// Remove the dispatchers.
changes.removed.map((item) => _this.removeDispatcher(item));
}
});
2021-12-04 07:25:26 +00:00
if (this._logger) {
this._logger.info("core.rpc-manager", this._id, "initialized");
2021-12-04 07:25:26 +00:00
}
2022-01-03 18:13:51 +00:00
this.ready.setContent(true);
2021-12-04 07:25:26 +00:00
}
/**
* Helper to remove a dispatcher.
*
* @author M.Karkowski
* @param {string} dispatcher
* @memberof NopeRpcManager
*/
2021-12-04 07:25:26 +00:00
public removeDispatcher(dispatcher: string): void {
// 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 cannot be executed any more."
)
);
2021-12-04 07:25:26 +00:00
}
/**
* Function to cancel an indivual Task.
*
* @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
*/
public async cancelTask(
taskId: string,
reason: Error,
quite = false
): Promise<boolean> {
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,
2021-12-04 07:25:26 +00:00
reason,
taskId,
quite,
});
// Indicate a successful cancelation.
return true;
}
// Task hasnt been found => Cancel the Task.
return false;
}
/**
* Internal 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
*/
2021-12-23 10:48:06 +00:00
public async cancelRunningTasksOfService(serviceName: string, reason: Error) {
2021-12-04 07:25:26 +00:00
// Provide a List containing all Tasks, that has to be canceled
2021-12-23 10:48:06 +00:00
const _tasksToCancel: string[] = [];
2021-12-04 07:25:26 +00:00
// 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.
2021-12-23 10:48:06 +00:00
_tasksToCancel.push(id);
2021-12-04 07:25:26 +00:00
}
}
2021-12-23 10:49:15 +00:00
const promises: Promise<any>[] = [];
2021-12-23 10:48:06 +00:00
2021-12-04 07:25:26 +00:00
if (_tasksToCancel.length > 0) {
// First remove all Tasks.
// Then cancel them to avoid side effects
2021-12-23 10:49:15 +00:00
for (const id of _tasksToCancel) {
promises.push(this.cancelTask(id, reason));
2021-12-04 07:25:26 +00:00
}
2021-12-23 10:48:06 +00:00
}
2021-12-04 07:25:26 +00:00
2021-12-23 10:49:15 +00:00
await Promise.all(promises);
2021-12-23 10:48:06 +00:00
}
/**
* Helper to cancel all Tasks which have been requested by a Dispatcher.
*
* @author M.Karkowski
* @param {string} dispatcher
* @param {Error} reason
* @memberof NopeRpcManager
*/
public async cancelRequestedTasksOfDispatcher(
dispatcher: string,
reason: Error
) {
const toCancel = new Set<string>();
for (const [
taskId,
requestedBy,
] of this._runningExternalRequestedTasks.entries()) {
if (requestedBy == dispatcher) {
toCancel.add(taskId);
}
}
const promises: Promise<any>[] = [];
for (const taskId of toCancel) {
promises.push(this.cancelTask(taskId, reason));
}
if (promises.length) {
await Promise.all(promises);
}
}
2021-12-23 10:48:06 +00:00
/**
* Cancels all Tasks of the given Dispatcher
*
* @author M.Karkowski
* @param {string} dispatcher
* @param {Error} reason
* @memberof NopeRpcManager
*/
2021-12-23 10:49:15 +00:00
public async cancelRunningTasksOfDispatcher(
2021-12-23 10:48:06 +00:00
dispatcher: string,
reason: Error
): Promise<void> {
// Provide a List containing all Tasks, that has to be canceled
const _tasksToCancel: string[] = [];
// 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.
_tasksToCancel.push(id);
2021-12-04 07:25:26 +00:00
}
}
2021-12-23 10:49:15 +00:00
const promises: Promise<any>[] = [];
2021-12-23 10:48:06 +00:00
if (_tasksToCancel.length > 0) {
// First remove all Tasks.
// Then cancel them to avoid side effects
2021-12-23 10:49:15 +00:00
for (const id of _tasksToCancel) {
promises.push(this.cancelTask(id, reason));
2021-12-04 07:25:26 +00:00
}
}
2021-12-23 10:48:06 +00:00
2021-12-23 10:49:15 +00:00
await Promise.all(promises);
2021-12-04 07:25:26 +00:00
}
/**
* 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
*/
public serviceExists(id: string): boolean {
return this.services.amountOf.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
*/
protected _getServiceName(id: string, type: "request" | "response"): string {
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
*/
protected _unregisterService(func: ((...args) => void) | string): boolean {
const _id =
typeof func === "string" ? func : ((func as any).id as string) || "0";
this._communicatorCallbacks.delete(_id);
// Publish the Available Services.
this._sendAvailableServices();
if (this._logger?.enabledFor(DEBUG)) {
// If there is a Logger:
this._logger.debug(`Dispatcher "${this._id}" unregistered: "${_id}"`);
}
return this._registeredServices.delete(_id);
}
public adaptServiceId(name: string) {
if (name.startsWith(`nope${SPLITCHAR}service${SPLITCHAR}`)) {
return name;
}
return `nope${SPLITCHAR}service${SPLITCHAR}${name}`;
}
2021-12-04 07:25:26 +00:00
/**
* 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
*/
2022-01-03 15:27:05 +00:00
public registerService(
2021-12-04 07:25:26 +00:00
func: (...args) => Promise<any>,
options: {
// We dont want to add a prefix
addNopeServiceIdPrefix?: boolean;
2022-01-26 12:03:03 +00:00
} & T
2021-12-04 07:25:26 +00:00
): (...args) => Promise<any> {
const _this = this;
// Define / Use the ID of the Function.
let _id = options.id || generateId();
_id = options.addNopeServiceIdPrefix ? this.adaptServiceId(_id) : _id;
2021-12-04 07:25:26 +00:00
2022-07-07 07:14:22 +00:00
// Make shure we assign our id
options.id = _id;
2021-12-04 07:25:26 +00:00
let _func = func;
if (!this.__warned && !isAsyncFunction(func)) {
this._logger.warn(
"!!! You have provided synchronous functions. They may break NoPE. Use them with care !!!"
);
2022-01-26 12:03:03 +00:00
this._logger.warn(`The service "${_id}" is synchronous!`);
2021-12-04 07:25:26 +00:00
this.__warned = true;
}
// Define a ID for the Function
2022-01-03 15:27:05 +00:00
(_func as any).id = _id;
2021-12-04 07:25:26 +00:00
// Define the callback.
2022-01-03 15:27:05 +00:00
(_func as any).unregister = () => _this._unregisterService(_id);
2021-12-04 07:25:26 +00:00
// Reister the Function
2022-01-03 15:27:05 +00:00
this._registeredServices.set((_func as any).id, {
2021-12-23 10:48:06 +00:00
options: options as T,
2021-12-04 07:25:26 +00:00
func: _func,
});
2021-12-23 10:48:06 +00:00
// Publish the Available Services.
this._sendAvailableServices();
2021-12-04 07:25:26 +00:00
if (this._logger?.enabledFor(DEBUG)) {
2021-12-23 10:48:06 +00:00
// If there is a Logger:
this._logger.debug(`Dispatcher "${this._id}" registered: "${_id}"`);
2021-12-04 07:25:26 +00:00
}
// Return the Function.
return _func;
}
2022-01-03 15:46:36 +00:00
public async unregisterService(
func: string | ((...args: any[]) => any)
): Promise<void> {
this._unregisterService(func);
2022-01-03 15:27:05 +00:00
}
2021-12-04 07:25:26 +00:00
/**
* 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> & {
2022-01-03 15:27:05 +00:00
* selector?: ValidSelectorFunction;
2021-12-04 07:25:26 +00:00
* quite?: boolean;
* })} [options={}] Options for the Call. You can assign a different selector.
* @return {*} {INopePromise<T>} The result of the call
* @memberof nopeDispatcher
*/
protected _performCall<T>(
2021-12-04 07:25:26 +00:00
serviceName: string,
params: any[],
options: ValidCallOptions = {}
2021-12-04 07:25:26 +00:00
): INopePromise<T> {
// Get a Call Id
const _taskId = generateId();
const _this = this;
const _options = {
deletableCallbacks: [],
paramsHasNoCallback: false,
dynamicCallback: false,
resultSink: this._getServiceName(serviceName, "response"),
...options,
} as ICallOptions;
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 (_this._logger?.enabledFor(DEBUG)) {
2021-12-04 07:25:26 +00:00
_this._logger.debug(`Clearing Callbacks from ${_taskId}`);
}
};
if (_this._logger?.enabledFor(DEBUG)) {
2021-12-04 07:25:26 +00:00
_this._logger.debug(
`Dispatcher "${this._id}" requesting externally Function "${serviceName}" with task: "${_taskId}"`
2021-12-04 07:25:26 +00:00
);
}
// Define a Callback-Function, which will expect the Task.
const ret = new NopePromise<T>(async (resolve, reject) => {
try {
const requestedTask: any = {
resolve,
reject,
clear,
serviceName,
timeout: null,
};
// Register the Handlers,
_this._runningInternalRequestedTasks.set(_taskId, requestedTask);
// Define a Task-Request
const taskRequest: IRequestRpcMsg & IExtraData = {
2021-12-04 07:25:26 +00:00
functionId: serviceName,
params: [],
taskId: _taskId,
resultSink: _options.resultSink,
requestedBy: _this._id,
2021-12-04 07:25:26 +00:00
};
2022-01-03 15:27:05 +00:00
for (const [idx, contentOfParameter] of params.entries()) {
taskRequest.params.push({
idx,
data: contentOfParameter,
});
2021-12-04 07:25:26 +00:00
}
2022-01-03 15:27:05 +00:00
if (!_this.serviceExists(serviceName)) {
2021-12-04 07:25:26 +00:00
// Create an Error:
const error = new Error(
`No Service Provider known for "${serviceName}"`
);
2022-01-03 15:27:05 +00:00
if (_this._logger) {
2021-12-04 07:25:26 +00:00
_this._logger.error(
`No Service Provider known for "${serviceName}"`
);
_this._logger.error(error);
}
throw error;
}
if (
2022-01-03 15:27:05 +00:00
_this.options.forceUsingSelectors ||
this.services.amountOf.get(serviceName) > 1
2021-12-04 07:25:26 +00:00
) {
if (typeof options.target !== "string") {
taskRequest.target = options.target;
} else if (typeof options?.selector === "function") {
2022-01-03 15:27:05 +00:00
const dispatcherToUse = await options.selector({
2022-01-03 15:46:36 +00:00
rpcManager: this,
2022-01-04 11:40:40 +00:00
serviceName,
2022-01-03 15:46:36 +00:00
});
2022-01-03 15:27:05 +00:00
2021-12-04 07:25:26 +00:00
// Assign the Selector:
2022-01-03 15:27:05 +00:00
taskRequest.target = dispatcherToUse;
2021-12-04 07:25:26 +00:00
} else {
2022-01-03 15:27:05 +00:00
const dispatcherToUse = await this._defaultSelector({
2022-01-03 15:46:36 +00:00
rpcManager: this,
2022-01-04 11:40:40 +00:00
serviceName,
2022-01-03 15:46:36 +00:00
});
2022-01-03 15:27:05 +00:00
2021-12-04 07:25:26 +00:00
// Assign the Selector:
2022-01-03 15:27:05 +00:00
taskRequest.target = dispatcherToUse;
2021-12-04 07:25:26 +00:00
}
}
// Send the Message to the specific element:
await _this._communicator.emit("RpcRequest", taskRequest);
2021-12-04 07:25:26 +00:00
if (_this._logger?.enabledFor(DEBUG)) {
2021-12-04 07:25:26 +00:00
_this._logger.debug(
2022-01-03 15:46:36 +00:00
`Dispatcher "${
this._id
2021-12-04 07:25:26 +00:00
}" putting task "${_taskId}" on: "${_this._getServiceName(
taskRequest.functionId,
"request"
)}"`
);
}
// If there is a timeout =>
if (options.timeout > 0) {
requestedTask.timeout = setTimeout(() => {
_this.cancelTask(
_taskId,
new Error(
`TIMEOUT. The Service allowed execution time of ${options.timeout.toString()}[ms] has been excided`
),
2022-01-03 15:27:05 +00:00
false
2021-12-04 07:25:26 +00:00
);
}, 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.
*
* @author M.Karkowski
* @template T
* @param {string} serviceName serviceName The Name / ID of the Function
* @param {any[]} params
* @param {(Partial<ICallOptions> & {
* selector?: ValidSelectorFunction;
* quite?: boolean;
* })} [options={}] Options for the Call. You can assign a different selector.
* @return {*} {INopePromise<T>} The result of the call
* @memberof nopeDispatcher
*/
public performCall<T>(
serviceName: string | string[],
params: unknown[],
options?: ValidCallOptions | ValidCallOptions[]
): INopePromise<T> {
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) as unknown as INopePromise<T>;
} else {
if (Array.isArray(options)) {
// Throw an error.
throw Error("Expecting a single Value for the options");
}
return this._performCall(serviceName, params, options);
}
}
2021-12-04 07:25:26 +00:00
/**
* Function to clear all pending tasks
*
* @memberof nopeDispatcher
*/
public clearTasks(): void {
if (this._runningInternalRequestedTasks) {
this._runningInternalRequestedTasks.clear();
} else this._runningInternalRequestedTasks = new Map();
}
/**
* Function to unregister all Functions of the Dispatcher.
*
* @memberof nopeDispatcher
*/
public unregisterAll(): void {
2022-01-03 15:27:05 +00:00
if (this._registeredServices) {
for (const id of this._registeredServices.keys()) {
this._unregisterService(id);
2021-12-04 07:25:26 +00:00
}
2022-01-03 15:27:05 +00:00
this._registeredServices.clear();
2021-12-04 07:25:26 +00:00
} else {
2022-01-03 15:27:05 +00:00
this._registeredServices = new Map();
2021-12-04 07:25:26 +00:00
}
// Reset the Callbacks.
this._communicatorCallbacks = new Map();
}
/**
* Function to reset the Dispatcher.
*
* @memberof nopeDispatcher
*/
public reset(): void {
this.clearTasks();
this.unregisterAll();
2021-12-04 07:25:26 +00:00
this._mappingOfDispatchersAndServices = new Map();
2022-01-03 18:13:51 +00:00
this.services.update(this._mappingOfDispatchersAndServices);
2021-12-04 07:25:26 +00:00
this._runningExternalRequestedTasks = new Map();
2021-12-04 07:25:26 +00:00
}
public async dispose(): Promise<void> {}
2021-12-04 07:25:26 +00:00
}