/** * @author Martin Karkowski * @email m.karkowski@zema.de * @create date 2018-07-01 09:10:35 * @modify date 2020-12-02 08:57:36 * @desc [description] */ import { Container, injectable, interfaces } from "inversify"; import { flatten } from "lodash"; import "reflect-metadata"; import { arraysEqual } from "../helpers/arrayMethods"; import { RUNNINGINNODE } from "../helpers/runtimeMethods"; import { getNopeLogger } from "../logger/getLogger"; import { DISPATCHER_INSTANCE } from "../symbols/identifiers"; import { INopeDispatcher } from "../types/nope/nopeDispatcher.interface"; import { INopeModule } from "../types/nope/nopeModule.interface"; import { IDescriptor, INopeActivationHanlder, IPackageDescription } from "../types/nope/nopePackage.interface"; import { INopePackageLoader } from "../types/nope/nopePackageLoader.interface"; /** * Helper Class to Build an inversify Container. * * @export * @class NopePackageLoader * @implements {INopePackageLoader} */ @injectable() export class NopePackageLoader implements INopePackageLoader { /** * Array containing multipl Activation Handlers * * @protected * @memberof NopePackageLoader */ protected _actionHandlers = new Map(); public _logger = getNopeLogger("package-loader", "debug"); public get activationHandlers() { return Array.from(this._actionHandlers.values()); } /** * Adds an Activation Handler. (Those are called, after an Object has been created) * * @param {(context: interfaces.Context, element: any) => any} func The Corresponding Method which will be called. * @memberof NopePackageLoader */ public async addActivationHandler( func: INopeActivationHanlder | Array ) { if (!Array.isArray(func)) { func = [func]; } for (const _func of func) { const _name = _func.name; if (!this._actionHandlers.has(_name)) this._actionHandlers.set(_name, _func); else { this._logger.warn("Trying to Add Activation Handler twice!"); } } } private _dispatcher: INopeDispatcher = null; public get dispatcher() { // Define a Lazy - Getter for an Modul: if (this._dispatcher === null) { this._dispatcher = this.container.get(DISPATCHER_INSTANCE); } return this._dispatcher; } /** * Adds the Container to the given Container. * * @param {Container} container the Container, that should be merged * @memberof NopePackageLoader */ public addContainers(container: Container) { this.container = Container.merge(this.container, container) as Container; } /** * Function which will perform all Activation Handlers. * * @protected * @param {interfaces.Context} _context * @param {*} _element * @returns * @memberof NopePackageLoader */ protected _onActivation(_context: interfaces.Context, _element: any) { /** Perform the Handlers on the Object */ for (const _handler of this._actionHandlers.values()) { _element = _handler(_context, _element); } return _element; } public availableElements: IDescriptor[]; protected _compareElements(_a: IDescriptor, _b: IDescriptor) { if ( _a.options && _a.options.addition && _b.options && _b.options.addition ) { return ( _b.options.addition.name === _a.options.addition.name && arraysEqual(_b.options.addition.args, _a.options.addition.args) ); } return true; } /** * Internal Method to Test, whether an Element exists or not. * * @protected * @param {IDescriptor} _item * @return {*} {boolean} * @memberof NopePackageLoader */ protected _hasElement(_item: IDescriptor): boolean { for (const _element of this.availableElements) { try { if (Array.isArray(_element.selector)) { if ( arraysEqual( _element.selector, Array.isArray(_item.selector) ? _item.selector : [_item.selector] ) && this._compareElements(_element, _item) ) { return true; } } else if ( !Array.isArray(_item.selector) && _element.selector === _item.selector && this._compareElements(_element, _item) ) { return true; } else if ( _item.factorySelector && _item.factorySelector === _element.factorySelector && this._compareElements(_element, _item) ) { return true; } } catch (e) { this._logger.error( e, "Error During Check", _item.factorySelector, _element.factorySelector ); } } return false; } /** * Method to add an Element to the Build * * @param {IDescriptor[]} _elements Definition containing the Elements that should be added * @memberof NopePackageLoader */ public async addDescription( _elements: IDescriptor[], _instance: NopePackageLoader | null = null ) { if (_instance === null) { for (const _element of _elements) { if (!this._hasElement(_element)) { this.availableElements.push(_element); this._addElement(_element); } else { this._logger.warn("Using the Same Selector / Factory of", _element); if (RUNNINGINNODE) { this.availableElements.push(_element); this._addElement(_element); } } } } else { for (const _element of _elements) { if (!this._hasElement(_element)) { this.availableElements.push(_element); this._linkExternalBuilder(_element, _instance); } else { this._logger.warn("Using the Same Selector / Factory of", _element); } } } } /** * Internal Helper Function to Merge an external Builder for creating Tasks. * * @protected * @param {IDescriptor} _element * @param {NopePackageLoader} _instance * @memberof NopePackageLoader */ protected _linkExternalBuilder( _element: IDescriptor, _instance: NopePackageLoader ) { /** Bind Factory */ if (!Array.isArray(_element.selector)) { _element.selector = [_element.selector] as Array; } if (_element.factorySelector && !Array.isArray(_element.factorySelector)) { _element.factorySelector = [_element.factorySelector] as Array< symbol | string >; } /** Add al instances. */ for (const _selector of _element.selector as | Array | Array) { this.container.bind(_selector).toDynamicValue(() => { return _instance.container.get(_selector); }); } for (const _selector of _element.factorySelector as | Array | Array) { this.container.bind(_selector).toDynamicValue(() => { return _instance.container.get(_selector); }); } } /** * Internal Funcitont to add an Description. * * @protected * @param {IDescriptor} _element * @memberof NopePackageLoader */ protected _addElement(_element: IDescriptor): void { if (_element) { const _self = this; const _dict = { whenTargetTagged: "getTagged", whenTargetNamed: "getNamed" }; /** Define Option if required */ if (!_element.options) { _element.options = {}; } if (_element.selector) { /** Make an Array */ if (!Array.isArray(_element.selector)) { _element.selector = [_element.selector]; } } else { _element.selector = []; } /** Make an Array */ if ( _element.factorySelector && !Array.isArray(_element.factorySelector) ) { _element.factorySelector = [_element.factorySelector]; } /** Select the Method */ const _method = _element.options.toConstant ? "toConstantValue" : "to"; /** Check if a specific Scope is given */ if (_element.options.scope != undefined) { /** A specified Scope for Inversify is given */ if (_element.options.addition) { /** Add all instances. */ for (const [_index, _selector] of (_element.selector as Array< symbol | string >).entries()) { /** Firstly bind to the First Selector */ if (_index === 0) { this._logger.debug("adding selector", _selector.toString()); this.container .bind(_selector) [_method](_element.type) [_element.options.scope]() [_element.options.addition.name]( ..._element.options.addition.args ) .onActivation((_context, _element) => { return _self._onActivation(_context, _element); }); } else { this._logger.debug("adding selector", _selector.toString()); /** Afterwards redirect to the Se */ this.container.bind(_selector).toDynamicValue((context) => { /** Create the First Element and return it. */ return context.container[_dict[_element.options.addition.name]]( (_element.selector as Array)[0], ..._element.options.addition.args ); }); } } } else { /** Add all instances. */ for (const [_index, _selector] of (_element.selector as Array< symbol | string >).entries()) { /** Firstly bind to the First Selector */ if (_index === 0) { this._logger.debug("adding selector", _selector.toString()); this.container .bind(_selector) [_method](_element.type) [_element.options.scope as string]() .onActivation((_context, _element) => { return _self._onActivation(_context, _element); }); } else { this._logger.debug("adding selector", _selector.toString()); /** Afterwards redirect to the Get the Element with the First Tag.*/ this.container.bind(_selector).toDynamicValue((context) => { /** Create the First Element and return it. */ return context.container.get( (_element.selector as Array)[0] ); }); } } } } else { /** No Scope is given */ if (_element.options.addition) { /** Add all instances. */ for (const [_index, _selector] of (_element.selector as Array< symbol | string >).entries()) { /** Firstly bind to the First Selector */ if (_index === 0) { this._logger.debug("adding selector", _selector.toString()); this.container .bind(_selector) [_method](_element.type) [_element.options.addition.name as string]( ..._element.options.addition.args ) .onActivation((_context, _element) => { return _self._onActivation(_context, _element); }); } else { this._logger.debug("adding selector", _selector.toString()); /** Afterwards redirect to the Se */ this.container.bind(_selector).toDynamicValue((context) => { /** Create the First Element and return it. */ return context.container[_dict[_element.options.addition.name]]( (_element.selector as Array)[0], ..._element.options.addition.args ); }); } } } else { /** Add all instances. */ for (const [_index, _selector] of (_element.selector as Array< symbol | string >).entries()) { /** Firstly bind to the First Selector */ if (_index === 0) { this._logger.debug("adding selector", _selector.toString()); this.container .bind(_selector) [_method](_element.type) .onActivation((_context, _element) => { return _self._onActivation(_context, _element); }); } else { this._logger.debug("adding selector", _selector.toString()); /** Afterwards redirect to the Get the Element with the First Tag.*/ this.container.bind(_selector).toDynamicValue((context) => { /** Create the First Element and return it. */ return context.container.get( (_element.selector as Array)[0] ); }); } } } } /** Add the Factory if neccessary */ if (_element.factorySelector) { /** Generate a Dict for converting the Instanciation Values to the Factory Methods. */ if (_element.options.factoryCallback) { if (_element.options.addition) { if (Array.isArray(_element.options.addition)) { } else { const firstArg = _element.options.addition.args[0]; const secondArg = _element.options.addition.args[1]; for (const _selector of _element.factorySelector as Array< symbol | string >) { this.container .bind(_selector) .toFactory(_element.options.factoryCallback) [_element.options.addition.name](firstArg, secondArg); } } } else { for (const _selector of _element.factorySelector as Array< symbol | string >) { console.log(_selector, _element); this.container .bind(_selector) .toFactory(_element.options.factoryCallback); } } } else { if (_element.options.addition) { if (Array.isArray(_element.options.addition)) { } else { const firstArg = _element.options.addition.args[0]; const secondArg = _element.options.addition.args[1]; for (const _selector of _element.factorySelector as Array< symbol | string >) { this.container .bind(_selector) .toFactory((context: interfaces.Context) => { return () => { /** Call get function on the Container */ return context.container[ _dict[_element.options.addition.name] ]( /** Return the First Element */ (_element.selector as Array)[0], ..._element.options.addition.args ); }; }) [_element.options.addition.name](firstArg, secondArg); } } } else { for (const _selector of _element.factorySelector as Array< symbol | string >) { this.container .bind(_selector) .toFactory((context: interfaces.Context) => { return () => { return context.container.get( (_element.selector as Array)[0] ); }; }); } } } } } } /** * The Inversify-Container see https://github.com/inversify/InversifyJS * * @type {Container} * @memberof NopePackageLoader */ public container: Container; public async reset(): Promise { /** Generate the container */ this.container = new Container(); const _globalConfig: any = {}; this.container.bind("global.config").toConstantValue(_globalConfig); this.availableElements = new Array(); } constructor() { this.reset(); } public packages: { [index: string]: IPackageDescription } = {}; protected _instances = new Map(); /** * Loader Function. This function will register all provided functions, * create the desired instances. Additionally it will add all descriptors. * * @param {IPackageDescription} element * @memberof NopePackageLoader */ async addPackage(element: IPackageDescription): Promise { if (this.packages[element.nameOfPackage] !== undefined) { throw Error( "Already loaded a Package with the name \"" + element.nameOfPackage + "\" !" ); } this._logger.info("loading package " + element.nameOfPackage); // Store the Package this.packages[element.nameOfPackage] = element; // Firstly add all Descriptors: await this.addDescription( element.providedClasses.map((item) => item.description) ); // Load the Activation Handlers: await this.addActivationHandler(element.activationHandlers); const _this = this; // Based on the provided settings register a generator Function for the Instances: for (const cl of element.providedClasses) { // Based on the Defintion extract the corresponding selector: let selector: string | symbol = null; let factory = false; if (cl.description.factorySelector) { // Firstly try to use a Factory Selector => new instances are generated instead of only // one singleton Object. selector = Array.isArray(cl.description.factorySelector) ? cl.description.factorySelector[0] : cl.description.factorySelector; factory = true; } else if (cl.description.selector) { // Otherwise select the Selector of the Singleton. selector = Array.isArray(cl.description.selector) ? cl.description.selector[0] : cl.description.selector; } if (selector && cl.settings?.allowInstanceGeneration) { this._logger.info( "Adding an instance generator for " + cl.description.name ); // Register an Instance Generator, only if allowed await this.dispatcher.provideInstanceGeneratorForExternalDispatchers( cl.description.name, async (_, identifier) => { const currentAmount = _this._instances.get(selector) || 0; if ( cl.settings.allowInstanceGeneration && currentAmount < (cl.settings.maxAmountOfInstance || Infinity) ) { // Define the Instance: const instance = factory ? _this.container.get<() => INopeModule>(selector)() : _this.container.get(selector); instance.identifier = identifier; // Update the Used Instance _this._instances.set(selector, currentAmount + 1); // Return the instance. return instance; } throw Error("Not allowed to create instances"); } ); } } // Iterate over the provided Functions: for (const func of element.providedFunctions) { await this.dispatcher.registerFunction(func.function, func.options); } } /** * Function to initialize all the instances. * * @param {boolean} [testRequirements=true] * @memberof NopePackageLoader */ async generateInstances(testRequirements = true): Promise { const _this = this; if (this._logger) { this._logger.info("Package Loader generates the instances."); } if (testRequirements) { const availablePackages = Object.getOwnPropertyNames(this.packages); // First extract all required Packages const reuqiredPackages = Array.from( new Set( flatten( availablePackages.map( (name) => _this.packages[name].requiredPackages ) ) ) ); // Now Check if every Package is present. reuqiredPackages.map((_package) => { if (!availablePackages.includes(_package)) { throw Error("Packages are not known"); } }); } for (const name in this.packages) { const definitions = this.packages[name].defaultInstances; for (const definition of definitions) { if (this._logger?.enabledFor) { this._logger.debug( "Requesting Generating Instance \"" + definition.options.identifier + "\" of type \"" + definition.options.type + "\"" ); } const instance = await this.dispatcher.generateInstance( definition.options ); if (this._logger?.enabledFor) { this._logger.debug( "Sucessfully Generated the Instance \"" + definition.options.identifier + "\" of type \"" + definition.options.type + "\"" ); } // Store the Function, that the instance will be disposed on leaving. this._disposeDefaultInstance.push(() => instance.dispose()); } } if (this._logger) { this._logger.info("generated all defined Instances"); } } protected _disposeDefaultInstance: Array<() => Promise> = []; async dispose(): Promise { // Start all dispose Values const promises = this._disposeDefaultInstance.map((cb) => cb()); // Wait for all to finish! await Promise.all(promises); this._disposeDefaultInstance = []; } }