nope/lib/dispatcher/nopeDispatcher.ts

1316 lines
37 KiB
TypeScript
Raw Normal View History

2020-09-10 16:21:01 +00:00
import { generateId } from '../helpers/idMethods';
2020-09-13 10:35:04 +00:00
import { Logger } from "winston";
2020-09-17 06:05:45 +00:00
import { nopeObservable, pipe, observableCallback } from '../observables/nopeObservable';
import { observable } from 'rxjs';
2020-09-10 16:21:01 +00:00
2020-09-11 07:59:23 +00:00
/**
* A Layer to communicate.
*
* @export
* @interface ICommunicationInterface
*/
2020-08-21 16:38:21 +00:00
export interface ICommunicationInterface {
2020-09-13 10:35:04 +00:00
readonly subscriptionMode?: 'individual' | 'generic',
readonly resultSharing?: 'individual' | 'generic',
2020-09-13 10:35:04 +00:00
/**
* Funciton to emit a RPC Request.
*
* @param {string} name Name of the request
* @param {requestTaskMsg} request The Request.
* @memberof ICommunicationInterface
*/
emitRpcRequest(name: string, request: requestTaskMsg): void;
2020-09-11 07:59:23 +00:00
/**
* Function used to subscribe to updates. If there exist
* some state change in the Communication Interface it should
* be provided on the registered functions
*
* @param {string} name The Id of the Method.
2020-09-13 10:35:04 +00:00
* @param {responseTaskMsg} result
* @memberof ICommunicationInterface
*/
emitRpcResult(name: string, result: responseTaskMsg): void;
2020-09-13 10:35:04 +00:00
/**
* Function used to subscribe to RPC Results. Each method / function
* can provide a unique eventname for the results.
*
* @param {string} name The Id of the Method.
* @param {(result: responseTaskMsg) => void} cb The callback which should be called
* @memberof ICommunicationInterface
*/
onRpcResult(name: string, cb: (result: responseTaskMsg) => void): void;
/**
* Function used to unsubscribe from Rpc-Results
*
* @param {string} name The Id of the Method.
* @param {(result: responseTaskMsg) => void} cb The callback which should be called
* @memberof ICommunicationInterface
*/
offRpcResponse(name: string, cb: (result: responseTaskMsg) => void): void;
2020-09-13 10:35:04 +00:00
/**
* Function to register RPC-Request.
*
* @param {string} name the name to listen for the request.
* @param {(data: requestTaskMsg) => void} cb The callback which should be called, if a request for the method is detected
2020-09-11 07:59:23 +00:00
* @memberof ICommunicationInterface
*/
2020-09-13 10:35:04 +00:00
onRpcRequest(name: string, cb: (data: requestTaskMsg) => void): void;
/**
2020-09-13 10:35:04 +00:00
* Unregister a listener for a RPC-Request.
*
2020-09-13 10:35:04 +00:00
* @param {string} name
* @param {(data: requestTaskMsg) => void} cb
* @memberof ICommunicationInterface
*/
2020-09-13 10:35:04 +00:00
offRpcRequest(name: string, cb: (data: requestTaskMsg) => void): void;
2020-09-11 07:59:23 +00:00
/**
2020-09-13 10:35:04 +00:00
* Function to Emit new Services. They will then be shared to other sytems.
2020-09-11 07:59:23 +00:00
*
2020-09-13 10:35:04 +00:00
* @param {availableServices} services
2020-09-11 07:59:23 +00:00
* @memberof ICommunicationInterface
*/
2020-09-13 10:35:04 +00:00
emitNewServicesAvailable(services: availableServices): void;
/**
* Function to register a new Callback, which will be called if new Services are available.
*
* @param {() => void} cb The Callback to Call.
* @memberof ICommunicationInterface
*/
onNewServicesAvailable(cb: (services: availableServices) => void);
2020-09-17 06:05:45 +00:00
/**
* Function to Emit new Services. They will then be shared to other sytems.
*
* @param {availableTopics} topics
* @memberof ICommunicationInterface
*/
emitNewTopicsAvailable(topics: availableTopics): void;
/**
* Function to register a new Callback, which will be called if new Services are available.
*
* @param {(topics: availableTopics) => void} cb The Callback to Call.
* @memberof ICommunicationInterface
*/
onNewTopicsAvailable(cb: (topics: availableTopics) => void);
/**
* Function to subscribe to an event
*
* @param {string} event The Event name (Usually the Topic.)
* @param {(data: externalEvent) => void} cb The Callback which should be used to call if there are new Events.
* @memberof ICommunicationInterface
*/
onEvent(event: string, cb: (data: externalEvent) => void): void;
/**
* Function used to emit an event on the given event channel
*
* @param {string} event The Name of the Event
* @param {externalEvent} data A datapacket describing the Event
* @memberof ICommunicationInterface
*/
emitEvent(event: string, data: externalEvent): void
/**
* Function to unregister an event listener
*
* @param {string} event The Name of the Event
* @param {(data: externalEvent) => void} cb The desired Callback
* @memberof ICommunicationInterface
*/
offEvent(event: string, cb: (data: externalEvent) => void): void;
/**
* Function, to subscribe to "Bonjour"-Messages of the Dispatcher Dispatchers.
*
* @param {(dispatcher: string) => void} cb The callback which should be called.
* @memberof ICommunicationInterface
*/
onBonjour(cb: (dispatcher: string) => void): void;
/**
* Function to emit a Bonjour message.
*
* @param {string} dispatcher The ID of the new Dispatcher.
* @memberof ICommunicationInterface
*/
emitBonjour(dispatcher: string): void;
}
export type availableTopics = {
/**
* The Id of the Dispatcher
*
* @type {string}
*/
dispatcher: string,
/**
* The List of published Topics (only the registered ones).
*
* @type {string[]}
*/
published: string[]
/**
* The List of subscribed Topics (only the registered ones).
*
* @type {string[]}
*/
subscribed: string[]
}
export type externalEvent = {
type: 'event'
data: any,
topic: string,
sender: string,
timestamp?: number
2020-08-21 16:38:21 +00:00
}
2020-09-13 10:35:04 +00:00
export type requestOfService = {
2020-09-12 07:56:17 +00:00
type: 'requestOfService'
2020-09-12 20:23:55 +00:00
taskId: string,
2020-09-12 07:56:17 +00:00
functionId: string,
}
export type requestTaskMsg = {
type: 'requestOfTask',
/**
* UUID of a Task
*
* @type {string}
*/
2020-09-12 20:23:55 +00:00
taskId: string,
/**
* ID of the Function, on which it is available.
*
* @type {string}
*/
functionId: string,
/**
* The Parameters
*
* @type {{
* idx: number,
* data: any
* }[]}
*/
2020-08-21 16:38:21 +00:00
params: {
/**
* Index of the Parameter
*
* @type {number}
*/
2020-08-21 16:38:21 +00:00
idx: number,
data: any
}[]
/**
* Callbacks, that are available in a Dispatcher.
*
* @type {(({
* functionId: string,
* idx: number,
* deleteAfterCalling: boolean,
* } & callOptions)[])}
*/
callbacks: ({
2020-08-21 16:38:21 +00:00
functionId: string,
idx: number,
deleteAfterCalling: boolean,
} & callOptions)[]
/**
* Element, allowing to describe where the result should be hosted.
*
* @type {string}
*/
resultSink: string
2020-09-15 05:58:54 +00:00
}
export type callOptions = {
2020-09-12 20:23:55 +00:00
/**
* Array containing indexes of callbacks which could be deleted
*
* @type {Array<number>}
*/
deletableCallbacks?: Array<number>;
2020-09-12 20:23:55 +00:00
/**
* Flag, showing that the parameters set has no callback
*
* @type {boolean}
*/
2020-09-12 07:56:17 +00:00
paramsHasNoCallback?: boolean;
2020-09-12 20:23:55 +00:00
/**
* Flag to disable error-testing
*
* @type {boolean}
*/
dynamicCallback?: boolean;
/**
* Element, allowing to describe where the result should be hosted.
*
* @type {string}
*/
resultSink: string
2020-08-21 16:38:21 +00:00
}
2020-09-12 07:56:17 +00:00
export type responseTaskMsg = {
2020-08-21 16:38:21 +00:00
type: 'response',
2020-09-12 20:23:55 +00:00
/**
* ID of the Task.
*
* @type {string}
*/
taskId: string,
/**
* Property containing the result. Is
* only present, if no error exists.
*
* @type {*}
*/
2020-08-21 16:38:21 +00:00
result?: any,
2020-09-12 20:23:55 +00:00
/**
* Property containing the error, if
* it occourd.
*
* @type {*}
*/
2020-08-21 16:38:21 +00:00
error?: any
}
export type availableServices = {
2020-09-12 20:23:55 +00:00
/**
* The Id of the Dispatcher
*
* @type {string}
*/
dispatcher: string,
2020-09-12 20:23:55 +00:00
/**
* The List of registered Service.
*
* @type {string[]}
*/
services: string[]
}
2020-09-17 06:05:45 +00:00
export type nopeRpcDispatcherOptions = {
communicator: ICommunicationInterface
}
2020-08-21 16:38:21 +00:00
/**
2020-09-11 07:59:23 +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
2020-08-21 16:38:21 +00:00
*/
2020-08-23 07:18:11 +00:00
export class nopeDispatcher {
2020-08-21 16:38:21 +00:00
public readonly id: string;
2020-09-13 10:35:04 +00:00
protected _logger: Logger;
2020-09-11 07:59:23 +00:00
/**
* Internal Element to store the registered Functions
*
* @protected
* @memberof nopeDispatcher
*/
protected _definedFunctions: Map<string, (...args) => Promise<any>>;
2020-09-12 07:56:17 +00:00
protected _remotlyCalledFunctions: Set<string>;
2020-09-13 10:35:04 +00:00
protected _communicatorCallbacks: Map<string, {
registeredId: string,
type: 'request' | 'response'
cb: (data) => any
2020-09-13 10:35:04 +00:00
}>;
protected _communicator: ICommunicationInterface;
protected _mappingOfRemoteDispatchersAndFunctions: Map<string, availableServices>;
protected _externalServices: Set<string>;
2020-09-12 20:23:55 +00:00
public methodInterfaceWithOptions: { [index: string]: <T>(optins: callOptions, ...args) => Promise<T> }
public methodInterface: { [index: string]: <T>(...args) => Promise<T> }
2020-09-11 07:59:23 +00:00
2020-09-17 06:05:45 +00:00
protected _mappingOfRemoteDispatchersAndTopics: Map<string, availableTopics>;
protected _externalSubscribed: Set<string>;
protected _externalPublished: Set<string>;
2020-09-11 07:59:23 +00:00
/**
* Internal Element to store the running tasks.
*
* @protected
* @memberof nopeDispatcher
*/
2020-09-12 20:23:55 +00:00
protected _runningTasks: Map<string, {
2020-08-21 16:38:21 +00:00
resolve: (value: any) => void;
reject: (error: any) => void;
}>;
readonly _subscriptionMode: 'individual' | 'generic';
readonly _resultSharing: 'individual' | 'generic';
2020-09-11 07:59:23 +00:00
/**
* Creates an instance of nopeDispatcher.
* @param {ICommunicationInterface} communicator The Communication Layer which should be used.
* @memberof nopeDispatcher
*/
2020-09-17 06:05:45 +00:00
constructor(public options: nopeRpcDispatcherOptions) {
2020-09-13 10:35:04 +00:00
this._communicator = options.communicator;
this._subscriptionMode = this._communicator.subscriptionMode || 'generic';
this._resultSharing = this._communicator.resultSharing || 'generic';
this.id = generateId();
2020-09-13 10:35:04 +00:00
/**
* Define A Proxy for accessing methods easier.
*/
const _this = this;
const _handlerWithOptions = {
get(target, name) {
return async (options: callOptions, ...args) => {
return _this.performCall(name, args, options);
}
}
}
const _handlerWithoutOptions = {
get(target, name) {
return async (...args) => {
return _this.performCall(name, args);
}
}
}
this.methodInterfaceWithOptions = new Proxy({}, _handlerWithOptions);
this.methodInterface = new Proxy({}, _handlerWithoutOptions);
2020-08-21 16:38:21 +00:00
this.reset();
this.init();
}
2020-09-11 07:59:23 +00:00
/**
* Internal Method to handle some requests.
*
* @protected
2020-09-12 07:56:17 +00:00
* @param {requestTaskMsg} data The provided data of the request
* @param {*} [_function=this._definedFunctions.get(data.functionId)] The Function can be provided
2020-09-11 07:59:23 +00:00
* @return {Promise<void>}
* @memberof nopeDispatcher
*/
2020-09-12 07:56:17 +00:00
protected async _handleExternalRequest(data: requestTaskMsg, _function?: (...args) => Promise<any>): Promise<void> {
2020-08-21 16:38:21 +00:00
try {
// Try to get the function if not provided:
if (typeof _function !== 'function') {
_function = this._definedFunctions.get(data.functionId);
}
2020-08-21 16:38:21 +00:00
const _this = this;
2020-09-11 07:59:23 +00:00
2020-08-21 16:38:21 +00:00
if (typeof _function === 'function') {
2020-09-11 07:59:23 +00:00
// Only if the Function is present extract the arguments etc.
2020-08-21 16:38:21 +00:00
const args = [];
2020-09-11 07:59:23 +00:00
// First extract the basic arguments
2020-08-21 16:38:21 +00:00
data.params.map(item => args[item.idx] = item.data);
2020-09-11 07:59:23 +00:00
// Add the Callbacks. Therefore create a function which will
// trigger the remote.
2020-09-12 07:56:17 +00:00
data.callbacks.map(options => args[options.idx] = async (..._args) => {
return await _this.performCall(options.functionId, _args, options);
2020-08-21 16:38:21 +00:00
});
2020-09-11 07:59:23 +00:00
// Perform the Task it self.
2020-09-10 16:21:01 +00:00
const _result = await _function(...args);
2020-09-11 07:59:23 +00:00
// Define the Result message
2020-09-12 07:56:17 +00:00
const result: responseTaskMsg = {
2020-09-10 16:21:01 +00:00
result: typeof (_result) !== 'undefined' ? _result : null,
2020-08-21 16:38:21 +00:00
taskId: data.taskId,
type: 'response'
}
2020-09-11 07:59:23 +00:00
// Use the communicator to publish the event.
2020-09-17 06:05:45 +00:00
this._communicator.emitRpcResult(data.resultSink, result);
2020-08-21 16:38:21 +00:00
}
} catch (error) {
2020-09-11 07:59:23 +00:00
// An Error occourd => Forward the Error.
2020-09-12 07:56:17 +00:00
const result: responseTaskMsg = {
2020-08-21 16:38:21 +00:00
error,
taskId: data.taskId,
type: 'response'
}
2020-09-11 07:59:23 +00:00
// Send the Error via the communicator to the remote.
this._communicator.emitRpcResult(data.resultSink, result);
2020-08-21 16:38:21 +00:00
}
}
2020-09-11 07:59:23 +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
2020-09-12 07:56:17 +00:00
* @param {responseTaskMsg} data The Data provided to handle the Response.
* @return {boolean} Returns a boolean, indicating whether a corresponding task was found or not.
2020-09-11 07:59:23 +00:00
* @memberof nopeDispatcher
*/
protected _handleExternalResponse(data: responseTaskMsg): boolean {
2020-08-21 16:38:21 +00:00
try {
// Extract the Task
const task = this._runningTasks.get(data.taskId);
// Delete the Task:
this._runningTasks.delete(data.taskId);
// Based on the Result of the Remote =>
if (task && data.error) {
task.reject(data.error);
return true;
2020-08-21 16:38:21 +00:00
}
2020-08-25 22:11:26 +00:00
if (task) {
task.resolve(data.result);
return true;
2020-08-21 16:38:21 +00:00
}
} catch (e) {
}
return false;
2020-08-21 16:38:21 +00:00
}
2020-09-11 07:59:23 +00:00
/**
* Internal Function, used to initialize the Dispatcher.
* It subscribes to the "Messages" of the communicator.
*
* @protected
* @memberof nopeDispatcher
*/
protected init(): void {
const _this = this;
// Based on the Mode of the Subscription =>
// either create indivdual topics for the methods
// or use the generice function.
switch (this._subscriptionMode) {
case 'individual':
// Iterate over the Defined Functions.
for (const [id, cb] of this._definedFunctions.entries()) {
// Subscribe the Function
this._subscribeToFunction(id, cb);
}
break;
case 'generic':
// Add a generic Subscription for callbacks:
2020-09-13 10:35:04 +00:00
this._communicator.onRpcRequest('request', (data: requestTaskMsg) => {
2020-09-12 07:56:17 +00:00
if (data.type === 'requestOfTask') {
_this._handleExternalRequest(data);
}
});
2020-09-13 10:35:04 +00:00
// Subscribe to Responses
this._communicator.onRpcResult('response', (data: responseTaskMsg) => {
if (data.type === 'response') {
_this._handleExternalResponse(data);
}
});
break;
}
2020-09-13 10:35:04 +00:00
// Subscribe to the availableServices of Remotes.
// If there is a new Service => udpate the External Services
2020-09-17 06:05:45 +00:00
this._communicator.onNewServicesAvailable(data => {
2020-09-13 10:35:04 +00:00
try {
2020-09-17 06:05:45 +00:00
if (data.dispatcher !== _this.id) {
_this._mappingOfRemoteDispatchersAndFunctions.set(data.dispatcher, data);
_this._updateExternalServices();
}
2020-09-13 10:35:04 +00:00
} catch (e) {
2020-08-21 16:38:21 +00:00
}
2020-09-17 06:05:45 +00:00
});
this._communicator.onNewTopicsAvailable(data => {
try {
if (data.dispatcher !== _this.id) {
_this._mappingOfRemoteDispatchersAndTopics.set(data.dispatcher, data);
_this._updateExternalTopics();
}
} catch (e) {
}
});
this._communicator.onBonjour((dispatcher: string) => {
if (_this.id !== dispatcher){
_this._sendAvailableServices();
_this._sendAvailableTopic();
}
});
this._communicator.emitBonjour(this.id);
2020-09-13 10:35:04 +00:00
}
/**
* Function to update the used Services.
*
* @protected
* @memberof serviceRegistry
*/
protected _updateExternalServices() {
const _this = this;
// Clear the Services
this._externalServices.clear();
for (const dispatcherInfo of this._mappingOfRemoteDispatchersAndFunctions.values()) {
dispatcherInfo.services.map(service => _this._externalServices.add(service));
}
2020-09-13 10:35:04 +00:00
if (this._logger?.isDebugEnabled()) {
// If there is a Logger:
this._logger.debug(
'new services found: \n' +
JSON.stringify(
Array.from(this._externalServices),
undefined,
4
)
);
2020-09-17 06:05:45 +00:00
}
}
/**
* Internal Function to update the Listing of external Topcis.
* This Function creates a list containing all subscriptions
* and publishers which are external.
*
* @protected
* @memberof nopeDispatcher
*/
protected _updateExternalTopics() {
const _this = this;
// Clear the Services
this._externalPublished.clear();
this._externalSubscribed.clear();
for (const dispatcherInfo of this._mappingOfRemoteDispatchersAndTopics.values()) {
dispatcherInfo.published.map(topic => _this._externalPublished.add(topic));
dispatcherInfo.subscribed.map(topic => _this._externalSubscribed.add(topic));
}
if (this._logger?.isDebugEnabled()) {
// If there is a Logger:
this._logger.debug(
'new topics found: \n' +
JSON.stringify(
Array.from(this._externalSubscribed),
undefined,
4
)
);
}
2020-09-13 10:35:04 +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
*/
2020-09-17 06:05:45 +00:00
public serviceExists(id: string) {
2020-09-13 10:35:04 +00:00
return this._definedFunctions.has(id) || this._externalServices.has(id);
2020-08-21 16:38:21 +00:00
}
2020-09-17 06:05:45 +00:00
public subscriptionExists(topic: string){
return this._externalSubscribed.has(topic);
}
/**
* Function to adapt a Request name.
* Only used internally
*
* @protected
* @param {string} id the original ID
* @return {string} the adapted ID.
* @memberof nopeDispatcher
*/
2020-09-17 06:05:45 +00:00
protected _getServiceName(id: string, type: 'request' | 'response'): string {
2020-09-13 10:35:04 +00:00
return id.startsWith(`${type}/`) ? id : `${type}/${id}`;
}
/**
* Internal Helper Function to subscribe to a Function element.
*
* @protected
* @param {string} id the Id of the function
* @param {(...args) => Promise<any>} _cb The Callback of the Function
* @return {void} the adapted ID.
* @memberof nopeDispatcher
*/
protected _subscribeToFunction(id: string, _cb: (...args) => Promise<any>): void {
2020-09-17 06:05:45 +00:00
const _req = this._getServiceName(id, 'request');
if (
this._subscriptionMode === 'individual' &&
!this._communicatorCallbacks.has(id)
) {
const _this = this;
// Define a Function.
const cb = (data: requestTaskMsg) => {
2020-09-12 07:56:17 +00:00
if (data.type === 'requestOfTask') {
_this._handleExternalRequest(data, _cb);
}
};
// Add the Callback.
this._communicatorCallbacks.set(id, {
registeredId: _req,
type: 'request',
cb
});
// Register Functions.
this._communicator.onRpcRequest(_req, cb);
}
}
2020-09-17 06:05:45 +00:00
protected _subscribeToResult(id: string, deleteAfterCalling: boolean): void {
2020-09-17 06:05:45 +00:00
const _res = this._getServiceName(id, 'response');
if (
this._subscriptionMode === 'individual' &&
!this._communicatorCallbacks.has(id)
) {
const _this = this;
// Define a Function.
const cb = (data: responseTaskMsg) => {
2020-09-13 10:35:04 +00:00
if (data.type === 'response') {
if (_this._handleExternalResponse(data)) {
2020-09-17 06:05:45 +00:00
_this._removeRpcSubscription(id);
}
2020-09-13 10:35:04 +00:00
}
};
// Add the Callback.
this._communicatorCallbacks.set(id, {
registeredId: _res,
type: 'response',
cb
2020-09-13 10:35:04 +00:00
});
// Register Functions.
this._communicator.onRpcResult(_res, cb);
}
}
2020-08-21 16:38:21 +00:00
/**
2020-09-11 07:59:23 +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
2020-08-21 16:38:21 +00:00
*/
public registerFunction(func: (...args) => Promise<any>, options: {
2020-09-11 07:59:23 +00:00
/** Flag to enable unregistering the function after calling. */
2020-08-21 16:38:21 +00:00
deleteAfterCalling?: boolean,
2020-09-11 07:59:23 +00:00
/** Instead of generating a uuid an id could be provided */
2020-08-21 16:38:21 +00:00
id?: string;
/** Flag to enable / disable sending to registery */
preventSendingToRegistery?: boolean;
2020-09-11 07:59:23 +00:00
} = {}): (...args) => Promise<any> {
2020-08-21 16:38:21 +00:00
const _this = this;
2020-09-13 10:35:04 +00:00
// Define / Use the ID of the Function.
const _id = options.id || generateId();
2020-08-21 16:38:21 +00:00
let _func = func;
if (options.deleteAfterCalling) {
_func = async (...args) => {
// Unregister the Method
2020-09-12 07:56:17 +00:00
_this.unregistFunction(_id, {
preventSendingToRegistery: options.preventSendingToRegistery
});
2020-08-21 16:38:21 +00:00
// Return the Result of the Original Function.
return await func(...args);
}
}
// Define a ID for the Function
_func['id'] = _id;
// Define the callback.
_func['unregister'] = () => _this.unregistFunction(_id);
2020-08-21 16:38:21 +00:00
// Reister the Function
this._definedFunctions.set(_func['id'], _func);
// Register the Callback:
this._subscribeToFunction(_id, _func);
2020-09-12 07:56:17 +00:00
if (!options.preventSendingToRegistery) {
// Publish the Available Services.
this._sendAvailableServices();
2020-09-12 07:56:17 +00:00
}
2020-08-21 16:38:21 +00:00
// Return the Function.
return _func;
}
/**
* Function to unregister a Function from the Dispatcher
2020-09-11 07:59:23 +00:00
* @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
2020-08-21 16:38:21 +00:00
*/
2020-09-12 07:56:17 +00:00
public unregistFunction(func: ((...args) => void) | string, options: {
/** Flag to enable / disable sending to registery */
preventSendingToRegistery?: boolean;
} = {}): boolean {
const _id = typeof func === 'string' ? func : func['id'] as string || '0';
2020-09-17 06:05:45 +00:00
this._removeRpcSubscription(_id);
if (!options.preventSendingToRegistery) {
// Publish the Available Services.
this._sendAvailableServices();
}
return this._definedFunctions.delete(_id);
}
2020-09-17 06:05:45 +00:00
protected _externallySubscribeObservables: Map<string, {
observable: nopeObservable<externalEvent>,
cb: (...arg) => void,
}>;
protected _internallySubscribeObservables: Map<string, Set<nopeObservable<any>>>;
/**
* Creates an Event listener (if required)
*
* @protected
* @param {string} event The Event to Listen.
* @return {nopeObservable<externalEvent>} An Listener on the Communication Channel.
* @memberof nopeDispatcher
*/
protected _subscribeToEvent(event: string) {
const item = this._externallySubscribeObservables.get(event) || {
observable: new nopeObservable<externalEvent>(),
cb: () => { },
};
if (!item.observable.hasSubscriptions) {
const _this = this;
const cb = (data: externalEvent) => {
item.observable.setContent(data, _this.id);
};
this._communicator.onEvent(event, cb);
item.cb = cb;
}
// Set the Items.
this._externallySubscribeObservables.set(event, item);
return item.observable;
}
/**
* Function to unsubscribe from an event of the channel.
*
* @protected
* @param {string} event
* @memberof nopeDispatcher
*/
protected _unsubscribeEvent(event: string) {
const item = this._externallySubscribeObservables.get(event) || {
observable: new nopeObservable<any>(),
cb: () => { },
count: 0,
};
if (item && item.observable.hasSubscriptions) {
this._communicator.offEvent(event, item.cb);
this._externallySubscribeObservables.delete(event);
}
}
/**
* Function to register a Oberservable into the element.
*
* @template T
* @template K
* @template S
* @template G
* @param {nopeObservable<T,S,G>} observable
* @param {({
* mode: 'subscribe' | 'publish' | Array<'subscribe' | 'publish'>,
* topic: string | {
* subscribe?: string,
* publish?: string,
* },
* pipe?:{
* pipe?: pipe<externalEvent,K>,
* scope?: { [index: string]: any }
* },
* preventSendingToRegistery?: boolean
* })} options
* @return {*} {nopeObservable<T,S,G>}
* @memberof nopeDispatcher
*/
public registerObservable<T, K, S = T, G = T>(observable: nopeObservable<T, S, G>, options: {
mode: 'subscribe' | 'publish' | Array<'subscribe' | 'publish'>,
topic: string | {
subscribe?: string,
publish?: string,
},
pipe?: {
pipe?: pipe<externalEvent, K>,
scope?: { [index: string]: any }
},
preventSendingToRegistery?: boolean
}): nopeObservable<T, S, G> {
// Reference to itself
const _this = this;
// Extract the Topics, pipe and scope.
const _subTopic = typeof options.topic === 'string' ? options.topic : options.topic.subscribe || null;
const _pubTopic = typeof options.topic === 'string' ? options.topic : options.topic.publish || null;
const _pipe = typeof options.pipe === 'function' ? options.pipe || null : null;
const _scope = typeof options.pipe === 'object' ? options.pipe.scope || null : null
// A Flag, indicating, whether the topic is new or not.
let newElement = false;
// Test if the Item should be subscribe or not.
if (options.mode == 'subscribe' || (Array.isArray(options.mode) && options.mode.includes('subscribe'))) {
newElement = newElement || !this._externallySubscribeObservables.has(_subTopic);
const _externalSource = this._subscribeToEvent(_subTopic);
if (_pipe) {
const observer = _externalSource.enhancedSubscription((data: externalEvent) => {
// Test if the Content, which has been forwared in here inst the own dispathcer.
if (data.sender != _this.id){
observable.setContent(data.data, _this.id, data.timestamp);
}
}, {
scope: _scope,
pipe: _pipe
});
const dispose = observable.dispose;
observable.dispose = () => {
// Kill the Observer;
observer.unsubscribe();
// Unsubscribe the Event
_this._unsubscribeEvent(_subTopic);
// Call the original Dispose function;
dispose.apply(observable);
}
} else {
const observer = _externalSource.subscribe({
next(data: externalEvent) {
if (_this.id !== data.sender) {
observable.setContent(data.data, _this.id, data.timestamp);
}
},
complete() {
observable.observable.complete();
},
error(err) {
observable.observable.error(err);
}
});
// Overwrite the Original Dispose Function.
const dispose = observable.dispose;
observable.dispose = () => {
// Kill the Observer;
observer.unsubscribe();
// Unsubscribe the Event
_this._unsubscribeEvent(_subTopic);
// Call the original Dispose function;
dispose.apply(observable);
}
}
}
if (options.mode == 'publish' || (Array.isArray(options.mode) && options.mode.includes('publish'))) {
const cb = (data, sender?, timestamp?, ...args) => {
// Only Publish data, if there exists a Subscription.
if (_this.subscriptionExists(_pubTopic) && _this.id !== sender) {
// Use the Communicator to emit the Event.
_this._communicator.emitEvent(_pubTopic, {
data:data,
topic: _pubTopic,
sender: _this.id,
type: 'event',
timestamp
});
}
}
// Update the Flag.
newElement = newElement || !this._internallySubscribeObservables.has(_pubTopic);
// Register the Internally Subscribed Element.
const _set = this._internallySubscribeObservables.get(_pubTopic) || new Set();
_set.add(observable);
this._internallySubscribeObservables.set(_pubTopic, _set);
if (_pipe) {
const observer = observable.enhancedSubscription(cb, {
scope: _scope,
pipe: _pipe
});
// Overwrite the Original Dispose Function.
const dispose = observable.dispose;
observable.dispose = () => {
// Kill the Observer;
observer.unsubscribe();
// Unsubscribe the Event
_this._unsubscribeEvent(_subTopic);
// Unregister the Internally Subscribed Element.
const _set = _this._internallySubscribeObservables.get(_pubTopic) || new Set();
_set.delete(observable);
if (_set.size > 0) {
_this._internallySubscribeObservables.set(_pubTopic, _set);
} else {
_this._internallySubscribeObservables.delete(_pubTopic);
// Optionally send an update.
if (!options.preventSendingToRegistery) {
// Publish the Available Services.
_this._sendAvailableTopic();
}
}
// Call the original Dispose function;
dispose.apply(observable);
}
} else {
const observer = observable.subscribe(cb);
// Overwrite the Original Dispose Function.
const dispose = observable.dispose;
observable.dispose = () => {
// Kill the Observer;
observer.unsubscribe();
// Unsubscribe the Event
_this._unsubscribeEvent(_subTopic);
// Unregister the Internally Subscribed Element.
const _set = _this._internallySubscribeObservables.get(_pubTopic) || new Set();
_set.delete(observable);
if (_set.size > 0) {
_this._internallySubscribeObservables.set(_pubTopic, _set);
} else {
_this._internallySubscribeObservables.delete(_pubTopic);
// Optionally send an update.
if (!options.preventSendingToRegistery) {
// Publish the Available Services.
_this._sendAvailableTopic();
}
}
// Call the original Dispose function;
dispose.apply(observable);
}
}
}
if (!options.preventSendingToRegistery && newElement) {
// Publish the Available Services.
this._sendAvailableTopic();
}
// Return the Function.
return observable;
}
protected _removeRpcSubscription(_id: string) {
// Try to unregister the Callback from the communcator:
if (this._communicatorCallbacks.has(_id)) {
2020-09-13 10:35:04 +00:00
const _callbacks = this._communicatorCallbacks.get(_id);
switch (_callbacks.type) {
case 'request':
// Unregister the RPC-Request-Listener
this._communicator.offRpcRequest(_callbacks.registeredId, _callbacks.cb);
break;
case 'response':
// Unregister the RPC-Response-Listener
this._communicator.offRpcResponse(_callbacks.registeredId, _callbacks.cb);
break;
}
2020-09-13 10:35:04 +00:00
// Remove the Callback
this._communicatorCallbacks.delete(_id);
}
2020-08-21 16:38:21 +00:00
}
/**
* Function used to update the Available Services.
*
* @protected
* @memberof nopeDispatcher
*/
protected _sendAvailableServices() {
// Define the Message
const message: availableServices = {
dispatcher: this.id,
services: Array.from(this._definedFunctions.keys())
}
// Send the Message.
2020-09-13 10:35:04 +00:00
this._communicator.emitNewServicesAvailable(message);
}
2020-09-17 06:05:45 +00:00
/**
* Function to emit the available topics.
*
* @protected
* @memberof nopeDispatcher
*/
protected _sendAvailableTopic() {
// Define the Message
const message: availableTopics = {
dispatcher: this.id,
published: Array.from(this._internallySubscribeObservables.keys()),
subscribed: Array.from(this._externallySubscribeObservables.keys())
}
// Send the Message.
this._communicator.emitNewTopicsAvailable(message);
}
2020-09-11 07:59:23 +00:00
/**
* Function which is used to perform a call on the remote.
*
* @template T
* @param {string} functionName The Name / ID of the Function
* @param {any[]} params The provided Parameters.
* @param {({
* deletableCallbacks: Array<number>;
* })} [options={
2020-09-12 07:56:17 +00:00
* deletableCallbacks: [],
* paramsHasNoCallback: false,
* preventErrorTest: false
* }] You could additiona Optionf for the callback.
2020-09-11 07:59:23 +00:00
* @return {*} {Promise<T>} The result of the Operation
* @memberof nopeDispatcher
*/
public performCall<T>(functionName: string, params: any[], options: Partial<callOptions> = {}): Promise<T> {
2020-08-21 16:38:21 +00:00
// Get a Call Id
2020-09-10 16:21:01 +00:00
const _taskId = generateId();
2020-09-12 07:56:17 +00:00
const _registeredIdx: Array<string> = [];
2020-08-21 16:38:21 +00:00
const _this = this;
const _options = Object.assign({
deletableCallbacks: [],
paramsHasNoCallback: false,
dynamicCallback: false,
2020-09-17 06:05:45 +00:00
resultSink: (this._resultSharing === 'generic' ? 'response' : this._getServiceName(functionName, 'response')) as string
} as callOptions, options) as callOptions;
this._subscribeToResult(functionName, _options.dynamicCallback)
2020-08-21 16:38:21 +00:00
// Define a Callback-Function, which will expect the Task.
return new Promise<T>((resolve, reject) => {
try {
// Register the Handlers,
_this._runningTasks.set(_taskId, {
resolve,
reject
});
2020-09-12 07:56:17 +00:00
// Define a Task-Request
const taskRequest: requestTaskMsg = {
2020-09-13 10:35:04 +00:00
functionId: functionName,
2020-09-12 07:56:17 +00:00
params,
callbacks: [],
taskId: _taskId,
type: 'requestOfTask',
resultSink: _options.resultSink
2020-09-12 07:56:17 +00:00
}
2020-08-21 16:38:21 +00:00
2020-09-12 07:56:17 +00:00
// Test if there is no Callback integrated
if (!options.paramsHasNoCallback) {
// If so, the parameters has to be detailled:
2020-08-21 16:38:21 +00:00
2020-09-12 07:56:17 +00:00
// 1. Reset the Params list:
taskRequest.params = [];
2020-08-21 16:38:21 +00:00
2020-09-12 07:56:17 +00:00
// 2. Iterate over all Parameters and
// Determin Callbacks. Based on the Parameter-
// Type assign it either to packet.params (
// for parsable Parameters) and packet.callbacks
// (for callback Parameters)
for (const [idx, contentOfParameter] of params.entries()) {
2020-09-12 07:56:17 +00:00
// Test if the parameter is a Function
if (typeof contentOfParameter !== "function") {
taskRequest.params.push({
idx,
2020-09-12 07:56:17 +00:00
data: contentOfParameter
});
} else {
2020-09-12 07:56:17 +00:00
// The Parameter is a Callback => store a
// Description of the Callback and register
// the callback inside of the Dispatcher
const deleteAfterCalling = _options.deletableCallbacks.includes(idx);
2020-09-12 07:56:17 +00:00
const _func = _this.registerFunction(contentOfParameter, {
deleteAfterCalling,
preventSendingToRegistery: true,
});
_registeredIdx.push(_func['id']);
// Register the Callback
2020-09-12 07:56:17 +00:00
taskRequest.callbacks.push({
functionId: _func['id'],
idx,
deleteAfterCalling,
dynamicCallback: true,
deletableCallbacks: [],
2020-09-17 06:05:45 +00:00
resultSink: _this._resultSharing === 'generic' ? 'response' : _this._getServiceName(_func['id'], 'response')
});
}
2020-08-21 16:38:21 +00:00
}
2020-09-12 07:56:17 +00:00
}
2020-08-21 16:38:21 +00:00
if (!_options.dynamicCallback && !_this.serviceExists(functionName)) {
2020-09-12 07:56:17 +00:00
2020-09-13 10:35:04 +00:00
// Create an Error:
const error = new Error('No Service Provider known for "' + functionName + '"');
2020-09-17 06:05:45 +00:00
if (_this._logger) {
_this._logger.error('No Service Provider known for "' + functionName + '"');
_this._logger.error(error)
2020-09-13 10:35:04 +00:00
}
2020-09-12 07:56:17 +00:00
2020-09-17 06:05:45 +00:00
throw error;
2020-09-12 07:56:17 +00:00
}
// Send the Message to the specific element:
if (_this._subscriptionMode === 'individual') {
2020-09-17 06:05:45 +00:00
_this._communicator.emitRpcRequest(_this._getServiceName(taskRequest.functionId, 'request'), taskRequest);
2020-09-12 07:56:17 +00:00
} else {
2020-09-13 10:35:04 +00:00
_this._communicator.emitRpcRequest('request', taskRequest);
2020-08-21 16:38:21 +00:00
}
} catch (e) {
// Delete all Callbacks.
_registeredIdx.map(id => _this.unregistFunction(id));
// Remove the task:
if (_this._runningTasks.has(_taskId))
_this._runningTasks.delete(_taskId);
2020-08-21 16:38:21 +00:00
// Throw an error.
reject(e);
}
});
}
2020-09-11 07:59:23 +00:00
/**
* Function to clear all pending tasks
*
* @memberof nopeDispatcher
*/
public clearTasks(): void {
2020-08-21 16:38:21 +00:00
if (this._runningTasks)
this._runningTasks.clear();
else
2020-09-12 20:23:55 +00:00
this._runningTasks = new Map<string, {
2020-08-21 16:38:21 +00:00
resolve: (value: any) => void;
reject: (error: any) => void;
}>();
}
2020-09-11 07:59:23 +00:00
/**
* Function to unregister all Functions of the Dispatcher.
*
* @memberof nopeDispatcher
*/
public unregisterAll(): void {
if (this._definedFunctions) {
for (const id of this._definedFunctions.keys()) {
this.unregistFunction(id)
}
2020-08-21 16:38:21 +00:00
this._definedFunctions.clear();
} else {
2020-08-21 16:38:21 +00:00
this._definedFunctions = new Map<string, (...args) => Promise<any>>();
}
// Reset the Callbacks.
2020-09-13 10:35:04 +00:00
this._communicatorCallbacks = new Map();
2020-08-21 16:38:21 +00:00
}
2020-09-11 07:59:23 +00:00
/**
* Function to reset the Dispatcher.
*
* @memberof nopeDispatcher
*/
public reset(): void {
2020-09-13 10:35:04 +00:00
this._remotlyCalledFunctions = new Set();
2020-09-17 06:05:45 +00:00
2020-09-13 10:35:04 +00:00
this._mappingOfRemoteDispatchersAndFunctions = new Map();
2020-09-17 06:05:45 +00:00
this._mappingOfRemoteDispatchersAndTopics = new Map();
this._externalServices = new Set();
this._externalPublished = new Set();
this._externalSubscribed = new Set();
this._internallySubscribeObservables = new Map();
this._externallySubscribeObservables = new Map();
2020-08-21 16:38:21 +00:00
this.clearTasks();
this.unregisterAll();
}
}