From 9e358be84ade30877687c4b0f5333fecb36f309d Mon Sep 17 00:00:00 2001 From: Martin Karkowski Date: Sun, 6 Nov 2022 20:25:01 +0100 Subject: [PATCH] updating to version 1.5.0. See Changelog --- CHANGELOG.md | 22 +- contribute/VERSION | 2 +- lib/eventEmitter/nopeEventEmitter.ts | 11 + lib/index.browser.ts | 2 + lib/index.nodejs.ts | 5 +- lib/logger/nopeLogger.ts | 5 +- lib/plugins/ackMessages.spec.ts | 37 ++ lib/plugins/ackMessages.ts | 226 +++++++++++ lib/plugins/hello.ts | 34 ++ lib/plugins/index.ts | 10 + lib/plugins/plugin.spec.ts | 67 ++++ lib/plugins/plugin.ts | 380 +++++++++++++++++++ lib/types/index.ts | 4 + lib/types/nope/nopeEventEmitter.interface.ts | 10 + lib/types/nope/nopeObservable.interface.ts | 11 +- 15 files changed, 812 insertions(+), 14 deletions(-) create mode 100644 lib/plugins/ackMessages.spec.ts create mode 100644 lib/plugins/ackMessages.ts create mode 100644 lib/plugins/hello.ts create mode 100644 lib/plugins/index.ts create mode 100644 lib/plugins/plugin.spec.ts create mode 100644 lib/plugins/plugin.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index a1df54b..2b08db9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -319,7 +319,7 @@ Inital commit, which is working with the browser - Dispatcher Health is only checked if required. - `lib\dispatcher\InstanceManager\InstanceManager.ts`: - Made: `getServiceName` public - - `lib/dispatcher/RpcManager/NopeRpcManager.ts`: + - `lib\dispatcher\RpcManager\NopeRpcManager.ts`: - The following functions are now async: - `_sendAvailableServices` -> it is awaited in some functions now (hasnt before) - `unregisterService` -> now returns a boolean for sucess @@ -331,27 +331,35 @@ Inital commit, which is working with the browser - `lib\dispatcher\ConnectivityManager\ConnectivityManager.ts`: - Fixing Master Assignment. - Only sending one Status on init. - - `lib/dispatcher/InstanceManager/InstanceManager.ts`: + - `lib\dispatcher\InstanceManager\InstanceManager.ts`: - Fixing pathes of `constructors` variable. Now `amountOf` etc is working - Fixing pathes of `constructorExists`. Now working with Type-Name. - Only sending one Status on init. - - `lib/helpers/mapMethods.ts`: + - `lib\helpers\mapMethods.ts`: - Fixing `tranformMap` in the case of only a `pathExtractedValue` or `pathExtractedKey` is given. - - `lib/helpers/objectMethods.ts`: + - `lib\helpers\objectMethods.ts`: - fixing `rgetattr` -> Now correctly returns "null" in all cases. - `lib\demo`: - Fixing imports of demo instances. - Modified: - `lib\types`: - renamed `IFunctionOptions` to `IServiceOptions` - - `lib/types/nope/nopeModule.interface.ts`: + - `lib\types\nope\nopeModule.interface.ts`: - `listMethods` now returns a different array, where the attribute is named `method` instead of `func` -> Adaptions affect `BaseModule` and `GenericModule` - Added: - Added Tests for the Properties of NopeRpcManager, NopeConnectivityManager - - `lib/helpers`: + - `lib\helpers`: - `PriorityList`: -> List, which sorts the items based on a given priority. - `LimitedList`: -> Ring-Like list. If the max amount of items is reached, the oldest one will be removed - `ParallelPriorityTaskQueue` -> A Task-Queue (Parallel and if desired with priority) - `generateHash` -> A function to generate a hash - \ No newline at end of file +# 1.5.0 + - Modified: + - `lib\logger`: + - Adding colors to log. + - `lib\eventEmitter`: + - Adding the possibility of a timeout in `waitFor` + - Added: + - `lib\plugins`: + - Added a full fetch plugin-system for javascript. That allows the user to customize different aspects of the lib. For an plugin see: `lib\plugins\ackMessages.ts` as an example. \ No newline at end of file diff --git a/contribute/VERSION b/contribute/VERSION index 7b5753f..3e1ad72 100644 --- a/contribute/VERSION +++ b/contribute/VERSION @@ -1 +1 @@ -1.4.6 \ No newline at end of file +1.5.0 \ No newline at end of file diff --git a/lib/eventEmitter/nopeEventEmitter.ts b/lib/eventEmitter/nopeEventEmitter.ts index 5afe788..240836e 100644 --- a/lib/eventEmitter/nopeEventEmitter.ts +++ b/lib/eventEmitter/nopeEventEmitter.ts @@ -358,6 +358,7 @@ export class NopeEventEmitter< let resolved = false; let subscription: INopeObserver = null; + let timeout = null; return new Promise((resolve, reject) => { const finish = (error: any, test: boolean, data: G) => { @@ -366,6 +367,10 @@ export class NopeEventEmitter< reject(error); } + if (timeout) { + clearTimeout(timeout); + } + // Unsubscribe the Subscription. if (test && subscription) { subscription.unsubscribe(); @@ -402,6 +407,12 @@ export class NopeEventEmitter< subscription = _this.subscribe((data, opts) => { checkData(data, opts); }); + + if (options?.timeout > 0) { + timeout = setTimeout(() => { + finish(Error("Timeout.!"), false, null); + }, options.timeout); + } } catch (e) { reject(e); } diff --git a/lib/index.browser.ts b/lib/index.browser.ts index 034ebf3..805d86d 100644 --- a/lib/index.browser.ts +++ b/lib/index.browser.ts @@ -17,6 +17,7 @@ import * as promise from "./promise/index"; import * as pubSub from "./pubSub"; import * as types from "./types/index"; import * as ui from "./ui/index.browser"; +import * as plugins from "./plugins/index"; export * from "./communication/index.browser"; export * from "./decorators"; @@ -46,4 +47,5 @@ export { promise, pubSub, ui, + plugins, }; diff --git a/lib/index.nodejs.ts b/lib/index.nodejs.ts index 68666f9..6f3358a 100644 --- a/lib/index.nodejs.ts +++ b/lib/index.nodejs.ts @@ -9,13 +9,13 @@ export { runNopeBackend, } from "./cli/runNopeBackend"; export * from "./communication/index.nodejs"; -export * as communcation from "./communication/index.nodejs"; +export * as communication from "./communication/index.nodejs"; export * from "./decorators"; export * as decorators from "./decorators"; export * as dispatcher from "./dispatcher"; export * from "./dispatcher/index"; export * from "./eventEmitter"; -export * as eventEmitters from "./eventEmitter"; +export * as eventEmitter from "./eventEmitter"; export * from "./helpers/index.nodejs"; export * as helpers from "./helpers/index.nodejs"; export * from "./loader/index.nodejs"; @@ -26,6 +26,7 @@ export * from "./module/index"; export * as modules from "./module/index"; export * as observables from "./observables"; export * from "./observables/index"; +export * as plugins from "./plugins"; export * as promises from "./promise"; export * from "./promise/index"; export * from "./pubSub"; diff --git a/lib/logger/nopeLogger.ts b/lib/logger/nopeLogger.ts index 9de8bae..b06d840 100644 --- a/lib/logger/nopeLogger.ts +++ b/lib/logger/nopeLogger.ts @@ -68,9 +68,10 @@ export const formatMsgForConsole = RUNNINGINNODE "-", colors.BgWhite + colors.FgBlack, context.name, - colors.Reset, + colorMatching[context.level.name], ":", ...message, + colors.Reset, ]; } : function (message, context) { @@ -101,8 +102,10 @@ Logger.useDefaults({ colors.BgWhite + colors.FgBlack, context.name, colors.Reset, + colorMatching[context.level.name], ":" ); + messages.push(colors.Reset); } : function (messages, context) { messages.unshift( diff --git a/lib/plugins/ackMessages.spec.ts b/lib/plugins/ackMessages.spec.ts new file mode 100644 index 0000000..55c43ab --- /dev/null +++ b/lib/plugins/ackMessages.spec.ts @@ -0,0 +1,37 @@ +import { describe, it } from "mocha"; + +describe("Plugins", function () { + // Describe the required Test: + describe("AckMessage", function () { + it("by-name", async function () { + delete require.cache[require.resolve("../index.nodejs")]; + const nope = require("../index.nodejs"); + nope.plugins.installPlugins(nope as any, "ackMessages", false); + + const loader = await nope.runNopeBackend({ + skipLoadingConfig: true, + log: "error", + }); + + await loader.dispatcher.ready.waitFor(); + const err = Error("This should not be raised!"); + try { + await loader.dispatcher.communicator.emit( + "hello", + { data: "test" }, + "wont be there", + 1000 + ); + throw err; + } catch (e) { + if (e === err) { + delete require.cache[require.resolve("../index.nodejs")]; + throw err; + } + } + await loader.dispatcher.dispose(); + + delete require.cache[require.resolve("../index.nodejs")]; + }); + }); +}); diff --git a/lib/plugins/ackMessages.ts b/lib/plugins/ackMessages.ts new file mode 100644 index 0000000..c9ddcd5 --- /dev/null +++ b/lib/plugins/ackMessages.ts @@ -0,0 +1,226 @@ +import { plugin } from "./plugin"; +import { Bridge as OrgBridge } from "../communication/index.browser"; +import { NopeConnectivityManager as OrgConnectivityManager } from "../dispatcher/ConnectivityManager"; +import { ValidLoggerDefinition } from "../logger/getLogger"; +import { INopeEventEmitter, NopeEventEmitter } from "../eventEmitter"; +import { + EventnameToEventType, + IEventAdditionalData, + INopeINopeConnectivityOptions, + INopeObservable, + toConstructor, +} from "../types"; +import { generateId, difference } from "../helpers/index.browser"; + +export const extend = plugin( + [ + "communication.Bridge", + "dispatcher.connectivityManager.NopeConnectivityManager", + ], + ( + clBridge: toConstructor, + clConnectivityManager: toConstructor + ) => { + interface AckMessage { + messageId: string; + dispatcherId: string; + } + + class Bridge extends clBridge { + /** + * Helper to forward received messages. + * + * @protected + * @type {INopeEventEmitter} + * @memberof Bridge + */ + protected _onMessageReceived: INopeEventEmitter; + /** + * Map storing the messages, where we expect an Acknowledgement. + * + * @protected + * @memberof Bridge + */ + protected _openMessages: Map< + string, + { + received: Set; + target: Set; + } + >; + + public onTransportError: INopeEventEmitter; + public defaultTargets: Array; + public ackReplyId: string; + + constructor(id?: string, logger?: ValidLoggerDefinition) { + super(id, logger); + + this._onMessageReceived = new NopeEventEmitter(); + this._openMessages = new Map(); + this.defaultTargets = []; + this.ackReplyId = null; + this.onTransportError = new NopeEventEmitter(); + + this.onTransportError.subscribe((err) => { + if (this._logger) { + this._logger.error("Failed to receive an acknowledge message!"); + this._logger.error(err); + } else { + console.error("Failed to receive an acknowledge message!"); + console.error(err); + } + }); + + this.on("ackMessage" as any, (msg) => + this._onMessageReceived.emit(msg) + ).catch((err) => { + if (this._logger) { + this._logger.error("Failed to subscribe to 'ackMessage'"); + this._logger.error(err); + } else { + console.error("Failed to subscribe to 'ackMessage'"); + console.error(err); + } + }); + } + + public async emit( + eventname: T, + data: EventnameToEventType[T], + target: string | Array = null, + timeout: number = 0 + ): Promise { + if ((eventname as any) !== "ackMessage" && this.ackReplyId) { + // Firstly we try to define the Target. + let targetToUse = new Set(); + + if (target === null) { + if (this.defaultTargets) { + targetToUse = new Set(this.defaultTargets); + } else if (data.target) { + targetToUse.add(data.target); + } + } else { + if (typeof target === "string") { + targetToUse.add(target); + } else if (Array.isArray(target)) { + target.map((item) => targetToUse.add(item)); + } + } + + if (targetToUse.size) { + const messageId = generateId(); + data.messageId = messageId; + + // We will define a Promise, which will wait for the ackknowledge ment. + const promise = this._onMessageReceived.waitFor( + (msg) => { + // If the Message is still open we try to + // close it. + if (this._openMessages.has(msg.messageId)) { + const target = this._openMessages.get(msg.messageId).target; + const received = this._openMessages.get( + msg.messageId + ).received; + received.add(msg.dispatcherId); + + // Therefore we determine the difference between + // the targets and + if (difference(target, received).size === 0) { + this._openMessages.delete(msg.messageId); + return true; + } + } + + return false; + }, + { + timeout, + } + ); + + // Now lets call emit + const res = await super.emit(eventname, data); + + // And now we will await the + // Wait - For result. + await promise; + + return res; + } + } + + return await super.emit(eventname, data); + } + + public async on( + eventname: T, + cb: (data: EventnameToEventType[T]) => void + ): Promise { + if ((eventname as string) === "ackMessage") { + return await super.on(eventname, cb); + } else { + return await super.on(eventname, (msg) => { + cb(msg); + + if (msg.messageId && this.ackReplyId) { + this.emit("ackMessage" as any, { + messageId: msg.messageId, + dispatcherId: this.ackReplyId, + }).catch((err) => { + if (this._logger) { + this._logger.error("Failed to emit an acknowledge message!"); + this._logger.error(err); + } else { + console.error("Failed to emit an acknowledge message!"); + console.error(err); + } + }); + } + }); + } + } + } + + class NopeConnectivityManager extends clConnectivityManager { + public forceAckMessage: boolean; + + constructor( + options: INopeINopeConnectivityOptions, + _generateObservable: () => INopeObservable< + T, + T, + T, + IEventAdditionalData + >, + id?: string + ) { + super(options, _generateObservable, id); + + (this._communicator as Bridge).ackReplyId = this.id; + this.forceAckMessage = true; + + this.dispatchers.data.subscribe((dispatchers) => { + if (this.forceAckMessage) { + (this._communicator as Bridge).defaultTargets = dispatchers; + } + }); + } + } + + return [ + { + adapted: Bridge, + name: "Bridge", + path: "communication.Bridge", + }, + { + adapted: NopeConnectivityManager, + name: "NopeConnectivityManager", + path: "dispatcher.connectivityManager.NopeConnectivityManager", + }, + ]; + }, + "ackMessages" +); diff --git a/lib/plugins/hello.ts b/lib/plugins/hello.ts new file mode 100644 index 0000000..2469596 --- /dev/null +++ b/lib/plugins/hello.ts @@ -0,0 +1,34 @@ +import { plugin } from "./plugin"; +import { NopeConnectivityManager as OrgConnectivityManager } from "../dispatcher/ConnectivityManager"; +import { toConstructor } from "../types"; + +export const extend = plugin( + [ + "dispatcher.connectivityManager.NopeConnectivityManager", + "helpers.ids.generateId", + ], + (clConnectivityManager: toConstructor, orgGenId) => { + class NopeConnectivityManager extends clConnectivityManager { + public hello(name: string) { + return `Hello ${name}!`; + } + } + + return [ + { + adapted: NopeConnectivityManager, + name: "NopeConnectivityManager", + path: "dispatcher.connectivityManager.NopeConnectivityManager", + }, + { + adapted: (...args) => { + const id = orgGenId(...args); + return id; + }, + name: "generateId", + path: "helpers.ids.generateId", + }, + ]; + }, + "hello" +); diff --git a/lib/plugins/index.ts b/lib/plugins/index.ts new file mode 100644 index 0000000..0f24a6c --- /dev/null +++ b/lib/plugins/index.ts @@ -0,0 +1,10 @@ +import { plugin, installPlugins, allPlugins } from "./plugin"; +export { plugin, installPlugins, allPlugins }; + +import { extend as ackMessages } from "./ackMessages"; +import { extend as hello } from "./hello"; + +export const availablePlugins = { + ackMessages, + hello, +}; diff --git a/lib/plugins/plugin.spec.ts b/lib/plugins/plugin.spec.ts new file mode 100644 index 0000000..f3c1f3d --- /dev/null +++ b/lib/plugins/plugin.spec.ts @@ -0,0 +1,67 @@ +import { assert } from "chai"; +import { describe, it } from "mocha"; +import { availablePlugins } from "./index"; +import { allPlugins, installPlugins, plugin } from "./plugin"; + +describe("PluginSystem", function () { + // Describe the required Test: + + it("List Plugins", function () { + assert(allPlugins().length == 2, "There Should be to Plugins"); + }); + describe("load single Plugins", function () { + it("by-name", function () { + delete require.cache[require.resolve("../index.nodejs")]; + const nope = require("../index.nodejs"); + installPlugins(nope, "hello", false); + delete require.cache[require.resolve("../index.nodejs")]; + }); + it("by-path", function () { + delete require.cache[require.resolve("../index.nodejs")]; + const nope = require("../index.nodejs"); + installPlugins(nope, "plugins.availablePlugins.hello", false); + delete require.cache[require.resolve("../index.nodejs")]; + }); + it("by-plugin", function () { + delete require.cache[require.resolve("../index.nodejs")]; + const nope = require("../index.nodejs"); + installPlugins(nope, availablePlugins.hello, false); + delete require.cache[require.resolve("../index.nodejs")]; + }); + it("dynamic-plugin", function () { + delete require.cache[require.resolve("../index.nodejs")]; + const nope = require("../index.nodejs"); + const extend = plugin("generateId", (org) => { + return [ + { + adapted: (...args) => { + return org(...args); + }, + name: "generateId", + path: "generateId", + }, + ]; + }); + installPlugins(nope, extend, false); + delete require.cache[require.resolve("../index.nodejs")]; + }); + }); + describe("load single Plugins", function () { + it("single-list-item", function () { + delete require.cache[require.resolve("../index.nodejs")]; + const nope = require("../index.nodejs"); + installPlugins(nope, ["hello"], false); + delete require.cache[require.resolve("../index.nodejs")]; + }); + it("by-path", function () { + delete require.cache[require.resolve("../index.nodejs")]; + const nope = require("../index.nodejs"); + installPlugins( + nope, + ["hello", "plugins.availablePlugins.hello", availablePlugins.hello], + false + ); + delete require.cache[require.resolve("../index.nodejs")]; + }); + }); +}); diff --git a/lib/plugins/plugin.ts b/lib/plugins/plugin.ts new file mode 100644 index 0000000..105baaa --- /dev/null +++ b/lib/plugins/plugin.ts @@ -0,0 +1,380 @@ +import { rgetattr, rsetattr, getType } from "../helpers/objectMethods"; +import { union } from "../helpers/setMethods"; +import { getNopeLogger } from "../logger/index.browser"; +import { getSingleton } from "../helpers/singletonMethod"; + +let COUNTER = 0; +const SPLITCHAR = "."; +const PLUGIN_STORE = getSingleton("nope.plugins", () => { + return new Map(); +}); + +const ABORT_INSPECTION_TESTERS = [ + (item) => { + const type = typeof item; + + const not = [ + "string", + "number", + "bigint", + "boolean", + "symbol", + "undefined", + "function", + ]; + + return not.includes(type); + }, + (item) => Array.isArray(item), +]; + +function shouldAbort(item) { + for (const test of ABORT_INSPECTION_TESTERS) { + if (test(item)) { + return true; + } + } + return false; +} + +function recursiveForEachModule( + obj: any, + prefix: string = "", + map: Map = null, + splitchar: string = SPLITCHAR, + maxDepth = Infinity, + level = 0 +): any { + if (map === null) { + map = new Map(); + } + + map.set(prefix, obj); + + if (level > maxDepth) { + return map; + } + + if (shouldAbort(obj)) { + return map; + } + + // Create an Array with the Keys. + const keys = Object.getOwnPropertyNames(obj); + + // If there are Keys => It is a List or a Default Object + if (keys.length > 0) { + for (const _key of keys) { + // Define the variable, containing the path + const path = prefix === "" ? _key : prefix + splitchar + _key; + map = recursiveForEachModule( + obj[_key], + path, + map, + splitchar, + maxDepth, + level + 1 + ); + } + } + + return map; +} + +/** + * Flattens an Object to a Map. + * + * For Instance: + * + * data = {a : { b : { c : 1, d: "hallo"}}} + * + * // Normal Call + * res = flatteObject(data) + * => res = {"a.b.c":1,"a.b.d":"hallo"} + * + * // With a Selected prefix 'additional.name' + * res = flatteObject(data,{prefix:'additional.name'}) + * => res = {"additional.name.a.b.c":1,"additional.name.a.b.d":"hallo"} + * + * @export + * @param {*} lib The Data that should be converted + * @param {string} [prefix=''] An additional prefix. + * @returns {Map} The flatten Object + */ +function flattenLibrary( + lib: any, + options: { + prefix?: string; + splitchar?: string; + maxDepth?: number; + } = {} +): Map { + const optionsToUse = Object.assign( + { + prefix: "", + splitchar: SPLITCHAR, + maxDepth: Infinity, + }, + options + ); + + return recursiveForEachModule( + lib, + optionsToUse.prefix, + new Map(), + options.splitchar, + options.maxDepth, + 0 + ); +} + +function listOccourence( + lib, + options: { + splitchar?: string; + maxDepth?: number; + } = {} +) { + const optionsToUse = Object.assign( + { + splitchar: SPLITCHAR, + maxDepth: Infinity, + }, + options + ); + + const flattend = flattenLibrary(lib, optionsToUse); + + const occourence = new Map>(); + + for (const key of flattend.keys()) { + const split = key.split(optionsToUse.splitchar); + const last = split[split.length - 1]; + + if (!occourence.has(last)) { + occourence.set(last, new Set()); + } + + occourence.get(last).add(key); + } + + return { + flattend, + occourence, + }; +} + +/** + * Helper to install an addon. + * @param library + * @param item + * @param replacer + * @returns + */ +function implementChanges(library, item: string | any, replacer) { + const { occourence, flattend } = listOccourence(library); + const failed = new Array<{ error: any; destination: string }>(); + if (occourence.has(item)) { + for (const destination of occourence.get(item)) { + try { + rsetattr(library, destination, replacer, SPLITCHAR); + } catch (error) { + failed.push({ + error, + destination, + }); + } + } + } + + return library; +} + +export type Plugin = ExtendFunction & { + install: (lib: string | NodeModule) => Set; + base: string[]; + pluginName: string; +}; + +export function isPlugin(plug: Plugin): plug is Plugin { + if (typeof plug !== "function") { + return false; + } + if ((plug as Plugin).install === undefined) { + return false; + } + if ((plug as Plugin).pluginName === undefined) { + return false; + } + return true; +} + +export type ExtendFunction = ( + ...args +) => Array<{ path: string; name: string; adapted: any }>; + +export function plugin( + base: string | string[], + extend: ExtendFunction, + name = "" +): Plugin { + if (!Array.isArray(base)) { + base = [base]; + } + if (name === "") { + try { + name = `anonymousPlugin${COUNTER++}@${arguments.callee.name}`; + } catch (e) { + name = `anonymousPlugin${COUNTER++}`; + } + } + + (extend as Plugin).base = base; + (extend as Plugin).pluginName = name; + (extend as Plugin).install = (lib: string | NodeModule) => { + if (typeof lib == "string") { + lib = require(lib); + } + + const itemsToUpdate = (base as string[]).map((item) => + rgetattr(lib, item, false, ".") + ); + + if (itemsToUpdate.includes(false)) { + throw Error( + "Faild to grap some of the given base elements. Please check parameter 'base'" + ); + } + + let modified = new Set(); + + // Now apply the addon: + const adaptions = extend(...itemsToUpdate); + + if (!Array.isArray(adaptions)) { + throw Error("Return-Type of the Plugin doesnt match."); + } + + for (const { path, name, adapted } of adaptions) { + lib = implementChanges(lib, path, adapted); + modified = union(modified, checkRequireCache(name, adapted)); + } + + return modified; + }; + + // Store our Plugin as store. + PLUGIN_STORE.instance.set(name, extend as Plugin); + + return extend as Plugin; +} + +/** + * Helper function to install Plugins. + * @param lib The Library to modify. + * @param plugins The Plugins install. This can be the registered names, pathes in the library or the plugin itself. + * @param log Flag to control the log information. + */ +export function installPlugins( + lib: string | NodeModule, + plugins: string | Plugin | Array, + log: boolean = true +) { + let modified = new Set(); + + if (!Array.isArray(plugins)) { + plugins = [plugins]; + } + + if (typeof lib == "string") { + lib = require(lib); + } + + const pluginsToUse = new Array(); + + // In this loop we ensure that we load the correct plugin. + for (const plug of plugins) { + if (typeof plug === "string") { + // The Plugin is provided as String. + // 1. Check if the name is present: + + if (PLUGIN_STORE.instance.has(plug)) { + pluginsToUse.push(PLUGIN_STORE.instance.get(plug)); + } else if (isPlugin(rgetattr(lib, plug, false, "."))) { + pluginsToUse.push(rgetattr(lib, plug, false, ".")); + } else { + const p = require(plug as string).extend; + if (isPlugin(p)) { + pluginsToUse.push(PLUGIN_STORE.instance.get(plug)); + } else { + throw Error( + "Cannot find plugin '" + + plug + + "'. If this is a file, make shure the plugin is exported as 'extend'" + ); + } + } + } else if (isPlugin(plug)) { + pluginsToUse.push(plug); + } + } + + let used_plugins_str = + "Plugins used!\n\n" + + "-".repeat(50) + + "\nPLUGIN INSTALLTION REPORT:\n" + + "-".repeat(50) + + "\n\nInstalled the following plugins:"; + let used_bases_str = "\n\nThe following source have been modified:"; + let used_bases = new Set(); + + for (const plug of pluginsToUse) { + // Store the Plugin + used_plugins_str += "\n\t- " + plug.pluginName; + // Store the modified elements: + plug.base.map((item) => used_bases.add(item)); + // Update the modified sources + modified = union(modified, plug.install(lib)); + } + + Array.from(used_bases).map((item) => (used_bases_str += "\n\t- " + item)); + + const end_str = "\n\nWatchout this may change the default behavior!\n\n"; + + const to_print = used_plugins_str + used_bases_str + end_str; + + if (log) { + const logger = getNopeLogger("plugin-system", "debug"); + logger.warn(to_print); + } +} + +/** + * Helper to list all Plugins + * @returns List of recognized Plugins + */ +export function allPlugins() { + return Array.from(PLUGIN_STORE.instance.keys()); +} + +function checkRequireCache(name: string, adapted: any) { + const modified = new Set(); + for (const absFileName in require.cache) { + const mod = require.cache[absFileName]; + + if (mod.loaded) { + const exportedItems = Object.getOwnPropertyNames(mod.exports); + if (exportedItems.includes(name)) { + try { + mod.exports[name] = adapted; + modified.add(absFileName); + } catch (e) { + // We are not allowed to reassign + // exported members only. + } + } + } + } + + return modified; +} diff --git a/lib/types/index.ts b/lib/types/index.ts index 0db9191..0d214c5 100644 --- a/lib/types/index.ts +++ b/lib/types/index.ts @@ -10,3 +10,7 @@ export * from "./IJSONSchema"; export * from "./nope/index"; export * from "./ui/index"; export { nope, ui }; + +export type toConstructor = { + new (...args): T; +}; diff --git a/lib/types/nope/nopeEventEmitter.interface.ts b/lib/types/nope/nopeEventEmitter.interface.ts index 660d348..263ff97 100644 --- a/lib/types/nope/nopeEventEmitter.interface.ts +++ b/lib/types/nope/nopeEventEmitter.interface.ts @@ -16,11 +16,21 @@ export type IWaitForCallback< export interface INopeWaitForEventOptions { subscriptionMode?: "immediate" | "sync"; triggerMode?: Array<"sub" | "super" | "direct">; + /** + * Timeout in *ms* after the waifFor fails. + */ + timeout?: number; } export interface INopeObserver extends Subscription { options: INopeSubscriptionOptions; + /** + * Pauses the Subscription + */ pause(): void; + /** + * Unpauses the Subscription + */ unpause(): void; } diff --git a/lib/types/nope/nopeObservable.interface.ts b/lib/types/nope/nopeObservable.interface.ts index 11c686d..944efc9 100644 --- a/lib/types/nope/nopeObservable.interface.ts +++ b/lib/types/nope/nopeObservable.interface.ts @@ -17,12 +17,17 @@ import { INopeEventEmitter } from "."; import { IEventAdditionalData, IEventCallback, + INopeWaitForEventOptions, } from "./nopeEventEmitter.interface"; -export interface INopeWaitForObservableChangeOptions { +export interface INopeWaitForObservableChangeOptions + extends INopeWaitForEventOptions { + /** + * Directly test the current value. + * Otherwise waits for an updated + * value. + */ testCurrent?: boolean; - subscriptionMode?: "immediate" | "sync"; - triggerMode?: Array<"sub" | "super" | "direct">; } export interface INopePartialObserver<