/** * @author Martin Karkowski * @email m.karkowski@zema.de */ import { ILogger } from "js-logger"; import { getEmitterPath, getMethodPath, getPropertyPath, isEmitterPathCorrect, isMethodPathCorrect, isPropertyPathCorrect, } from "../helpers/dispatcherPathes"; import { deepClone } from "../helpers/objectMethods"; import { getNopeLogger } from "../logger/getLogger"; import { INopeCore, INopeEventEmitter } from "../types/nope"; import { IAuthor, IEventOptions, IFunctionOptions, INopeModule, INopeModuleDescription, IVersion, } from "../types/nope/nopeModule.interface"; import { INopeObservable } from "../types/nope/nopeObservable.interface"; /** * Base Implementation of a Module. * * The Module is used to share information and data. Although it implements the * the Basic behavior to fullfill a given traget. * * @export * @class BaseModule * @implements {INopeModule} */ export class NopeBaseModule implements INopeModule { /** * Return the Class Identifier. */ public get type(): string { return Object.getPrototypeOf(this).constructor.name; } /** * A Description of the Module. This is used to Describe roughly * what the module is capable of * doing. * * @type {string} * @memberof BaseModule */ public description: string; /** * A Description of the Author. Use to Mail etc. * * @type {IAuthor} * @memberof BaseModule */ public author: IAuthor; /** * Description of the provided Version of the Module. * * @type {IVersion} * @memberof BaseModule */ public version: IVersion; protected _registeredFunctions: Map< string, { func: (...args: any[]) => Promise; options: IFunctionOptions; } >; protected _registeredProperties: Map< string, { observable: INopeObservable; options: IEventOptions; } >; protected _registeredEvents: Map< string, { emitter: INopeEventEmitter; options: IEventOptions; } >; /** * Public getter for the functions * * @readonly * @memberof BaseModule */ public get functions() { const ret: { [index: string]: IFunctionOptions } = {}; for (const [name, funcs] of this._registeredFunctions.entries()) { ret[name] = funcs.options; } return ret; } /** * Public get to receive a Description of the Properties * * @readonly * @memberof BaseModule */ public get properties() { const ret: { [index: string]: IEventOptions } = {}; for (const [name, funcs] of this._registeredProperties.entries()) { ret[name] = funcs.options; } return ret; } /** * Public get to receive a Description of the Properties * * @readonly * @memberof BaseModule */ public get events() { const ret: { [index: string]: IEventOptions } = {}; for (const [name, funcs] of this._registeredEvents.entries()) { ret[name] = funcs.options; } return ret; } /** * The Identifier of the Module. * * @type {string} * @memberof BaseModule */ public identifier: string; public _markedElements: Array<{ accessor: string; options: IEventOptions | IFunctionOptions; type: "method" | "prop" | "event"; }>; protected _logger: ILogger; /** * Creates an instance of BaseModule. * @memberof BaseModule */ constructor(protected _core: INopeCore) { this.description = null; this.author = null; this.version = null; this.identifier = null; this._registeredFunctions = new Map(); this._registeredProperties = new Map(); this._registeredEvents = new Map(); this.uiLinks = []; this._logger = getNopeLogger("BaseModule"); } public uiLinks: { name: string; description: string; link: string }[]; /** * Helper Function to register an Observable (a Property.) * * @template T Type of the Property * @template S Setter Type of the Property * @template G Getter Type of the Property * @param {string} name Name, which should be used to register the element. The Name will ALLWAYS (automatically) be assembled using the modules identifier an then the name. * @param {INopeObservable} observable The Observable representing the Property * @param {IEventOptions} options The Options used to define the registration. * @return {*} {Promise} * @memberof NopeBaseModule */ public async registerProperty( name: string, observable: INopeObservable, options: IEventOptions ): Promise { // Unregister the Function await this.unregisterProperty(name); // Adapt the Topics if ( typeof options.topic === "string" && !isPropertyPathCorrect(this.identifier, options.topic as string) ) { options.topic = getPropertyPath(this.identifier, options.topic); } else if (typeof options.topic === "object") { if ( options.topic.subscribe && !isPropertyPathCorrect(this.identifier, options.topic.subscribe) ) { options.topic.subscribe = getPropertyPath( this.identifier, options.topic.subscribe ); } if ( options.topic.publish && !isPropertyPathCorrect(this.identifier, options.topic.publish) ) { options.topic.publish = getPropertyPath( this.identifier, options.topic.publish ); } } const _observable = await this._core.dataDistributor.register( observable, options ); // Register the new Property. this._registeredProperties.set(name, { observable: _observable, options, }); } /** * Helper Function to register an Event(Emitter) (a Property.) * * @template T Type of the Event(Emitter) * @template S Setter Type of the Event(Emitter) * @template G Getter Type of the Event(Emitter) * @param {string} name Name, which should be used to register the element. The Name will ALLWAYS (automatically) be assembled using the modules identifier an then the name. * @param {INopeObservable} emitter The Event(Emitter) representing the Property * @param {IEventOptions} options The Options used to define the registration. * @return {*} {Promise} * @memberof NopeBaseModule */ public async registerEvent( name: string, emitter: INopeObservable, options: IEventOptions ): Promise { // Unregister the Function await this.unregisterEvents(name); // Adapt the Topics if ( typeof options.topic === "string" && !isEmitterPathCorrect(this.identifier, options.topic as string) ) { options.topic = getEmitterPath(this.identifier, options.topic); } else if (typeof options.topic === "object") { if ( options.topic.subscribe && !isEmitterPathCorrect(this.identifier, options.topic.subscribe) ) { options.topic.subscribe = getEmitterPath( this.identifier, options.topic.subscribe ); } if ( options.topic.publish && !isEmitterPathCorrect(this.identifier, options.topic.publish) ) { options.topic.publish = getEmitterPath( this.identifier, options.topic.publish ); } } const _emitter = await this._core.eventDistributor.register( emitter, options ); // Register the new Property. this._registeredEvents.set(name, { emitter: _emitter, options, }); } /** * Function used to register a Method. This Method will be available in the shared network. * * @param {string} name Name of the Method, which is used during registration at the dispatcher * @param {(...args: any[]) => Promise} func The function itself. It must be async. * @param {IFunctionOptions} options The Options, used for registering. * @return {*} {Promise} * @memberof NopeBaseModule */ public async registerMethod( name: string, func: (...args: any[]) => Promise, options: IFunctionOptions ): Promise { // Unregister the Function await this.unregisterFunction(name); // Adapt the Method ID if (options.id) { if (!isMethodPathCorrect(this.identifier, options.id)) { options.id = getMethodPath(this.identifier, options.id); } } else { options.id = getMethodPath(this.identifier, name); } const _func = await this._core.rpcManager.registerService(func, options); // Register the new Function. this._registeredFunctions.set(name, { func: _func, options, }); } /** * Unregister a Function * * @param {string} name Name of the function used during registering. * @return {*} {Promise} * @memberof NopeBaseModule */ public async unregisterFunction(name: string): Promise { // Test if the Method is already registerd, // If so => unregister it first. if (this._registeredFunctions.has(name)) { this._core.rpcManager.unregisterService( this._registeredFunctions.get(name).func ); } } /** * Helper Function to unregister an Observable (a Property.) * * @param {string} name Name of the Property, that has been used to register. * @return {*} {Promise} * @memberof NopeBaseModule */ public async unregisterEvents(name: string): Promise { // Test if the Property is already registerd, // If so => unregister it first. if (this._registeredEvents.has(name)) { this._core.eventDistributor.unregister( this._registeredEvents.get(name).emitter ); } } /** * Helper Function to unregister an Observable (a Property.) * * @param {string} name Name of the Property, that has been used to register. * @return {*} {Promise} * @memberof NopeBaseModule */ public async unregisterProperty(name: string): Promise { // Test if the Property is already registerd, // If so => unregister it first. if (this._registeredProperties.has(name)) { this._core.dataDistributor.unregister( this._registeredProperties.get(name).observable ); } } /** * Function to return all available Methods. * * @return {*} {Promise<{ func: (...args: any[]) => Promise; options: IFunctionOptions; }[]>} * @memberof NopeBaseModule */ public async listFunctions(): Promise< { func: (...args: any[]) => Promise; options: IFunctionOptions }[] > { return Array.from(this._registeredFunctions.values()); } /** * Function used to list all available Properties. * * @return {*} {Promise, options: IPropertyOptions }>>} * @memberof NopeBaseModule */ public async listProperties(): Promise< Array<{ observable: INopeObservable; options: IEventOptions }> > { return Array.from(this._registeredProperties.values()); } /** * An init Function. Used to initialize the Element. * * @return {*} {Promise} * @memberof NopeBaseModule */ public async init(...args): Promise { // In this base Implementation, check if every requried property is set // correctly. If not => raise an error. if (this.type === null) { throw Error("Please Provide a Name for the Module before initializing"); } if (this.description === null) { throw Error( "Please Provide a Description for the Module before initializing" ); } if (this.author === null) { throw Error( "Please Provide an Author for the Module before initializing" ); } if (this.version === null) { throw Error( "Please Provide a Version for the Module before initializing" ); } if (this.identifier === null) { throw Error( "Please Provide an Identifier for the Module before initializing" ); } if (this._markedElements) { const _this = this; for (const entry of deepClone(this._markedElements)) { switch (entry.type) { case "method": await this.registerMethod( entry.accessor, (...args) => { return _this[entry.accessor](...args); }, entry.options as IFunctionOptions ); break; case "prop": await this.registerProperty( entry.accessor, _this[entry.accessor], entry.options as IEventOptions ); break; case "event": await this.registerEvent( entry.accessor, _this[entry.accessor], entry.options as IEventOptions ); } } } } /** * Function, which is used to unregister the element. * * @memberof NopeBaseModule */ public async dispose() { // Unregister all Methods and Functions for (const name of this._registeredFunctions.keys()) { await this.unregisterFunction(name); } // Remove all known Functions this._registeredFunctions.clear(); // Unregister all Properties. for (const name of this._registeredProperties.keys()) { await this.unregisterProperty(name); } // Remove all known Properties. this._registeredProperties.clear(); } /** * Helper Function to extract the used identifiert of Property * * @param {(((...args) => Promise) | INopeObservable)} propOrFunc The Property or the Function to receive the Name. * @return {*} {string} * @memberof NopeBaseModule */ public getIdentifierOf( propOrFunc: ((...args) => Promise) | INopeObservable, type: "topicToPublish" | "topicToSubscribe" = null ): string { // To Extract the name of the Property or the Function, we will iterate over // the registered properties and the regiestered functions. If the prop or the // function matches ==> return the name otherwise we throw an error. for (const [ name, { observable, options }, ] of this._registeredProperties.entries()) { if (observable == propOrFunc) { const _subTopic = typeof options.topic === "string" ? options.topic : options.topic.subscribe || null; const _pubTopic = typeof options.topic === "string" ? options.topic : options.topic.publish || null; switch (type) { case "topicToPublish": if (_pubTopic === null) { throw Error("No topic for publishing is defined."); } return _pubTopic; case "topicToSubscribe": if (_subTopic === null) { throw Error("No topic for subscribing is defined."); } return _subTopic; default: if (typeof options.topic === "string") { return options.topic; } throw Error( "Prop uses different name for subscribing and publishing. Please specify using the 'type' identier to select" ); } } } for (const [ name, { func, options }, ] of this._registeredFunctions.entries()) { if (func == propOrFunc) { return options.id; } } throw Error("Element not found or registered"); } /** * Helper function to extract an description of the Module. * * @return {INopeModuleDescription} a parsed description * @memberof NopeBaseModule */ public toDescription(): INopeModuleDescription { const ret: INopeModuleDescription = { author: this.author, description: this.description, functions: this.functions, events: this.events, identifier: this.identifier, properties: this.properties, type: this.type, version: this.version, uiLinks: this.uiLinks, }; return ret; } }