Compare commits
5 Commits
97a6031d32
...
fe6b83fa5e
Author | SHA1 | Date | |
---|---|---|---|
fe6b83fa5e | |||
7e4d979d01 | |||
9e358be84a | |||
169f24cc81 | |||
bdd257545b |
25
CHANGELOG.md
25
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,22 +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`:
|
||||
- `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
|
||||
|
||||
|
||||
# 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.
|
16
README.md
16
README.md
@ -24,8 +24,8 @@ To install `NoPE` just extecute the `00-install.bat`-Batch-File in the root-dire
|
||||
Make shure you run the following tasks
|
||||
|
||||
1. `npm install`
|
||||
2. `npx tsc -p ./tsconfigBackend.json --pretty`
|
||||
3. `npm link`
|
||||
2. `./00-compile.bat` (on windows) or `./00-compile.sh` (on linux)
|
||||
3. `./05-link.bat` (on windows) or `./05-link.sh` (on linux)
|
||||
|
||||
# Usage
|
||||
|
||||
@ -37,10 +37,14 @@ After installation you can use the following cli-tool:
|
||||
|
||||
Please select the option you want. Therefore add one of the following options:
|
||||
|
||||
- `help` - Opens the Help
|
||||
- `run` - Start a NoPE-Backend.
|
||||
- `init` - Initialize a new project. This project is empty.
|
||||
- `scan` - Trys to update the configuration file.
|
||||
- `help` Show this help.
|
||||
- `run` Start a NoPE-Backend.
|
||||
- `init` Initialize a new project. This project is empty.
|
||||
- `conf` Trys to update the configuration file.
|
||||
- `scan-ui` Scans and extracts the provided uis.
|
||||
- `upload-ui` Uploads the determined ui-file
|
||||
- `service` Generate Helper Files to provide services
|
||||
- `repl` Opens an interactive console.
|
||||
|
||||
# Changelog
|
||||
|
||||
|
@ -1 +1 @@
|
||||
1.4.6
|
||||
1.5.0
|
@ -26,7 +26,7 @@ export type callable<T> = {
|
||||
export function exportAsNopeService<T>(
|
||||
func: T,
|
||||
options: IexportAsNopeServiceParameters
|
||||
) {
|
||||
): T & { options: IexportAsNopeServiceParameters } {
|
||||
// Only add the element if it doesnt exists.
|
||||
if (!CONTAINER.services.has(options.id)) {
|
||||
CONTAINER.services.set(options.id, {
|
||||
@ -37,5 +37,8 @@ export function exportAsNopeService<T>(
|
||||
uri: options.id || (func as any).name,
|
||||
});
|
||||
}
|
||||
return func;
|
||||
|
||||
(func as any).options = options;
|
||||
|
||||
return func as any;
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ export class HelloWorldModuleWithDecorators
|
||||
extends InjectableNopeBaseModule
|
||||
implements IHelloWorlModule
|
||||
{
|
||||
// @ts-ignore
|
||||
@nopeProperty({
|
||||
mode: ["publish"],
|
||||
topic: "testProp",
|
||||
@ -18,6 +19,7 @@ export class HelloWorldModuleWithDecorators
|
||||
})
|
||||
public testProp = new NopeObservable<string>();
|
||||
|
||||
// @ts-ignore
|
||||
@nopeProperty({
|
||||
mode: ["publish"],
|
||||
topic: "currentTime",
|
||||
|
@ -358,6 +358,7 @@ export class NopeEventEmitter<
|
||||
|
||||
let resolved = false;
|
||||
let subscription: INopeObserver = null;
|
||||
let timeout = null;
|
||||
|
||||
return new Promise<G>((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);
|
||||
}
|
||||
|
@ -73,11 +73,24 @@ export async function createPath(path: string) {
|
||||
return path;
|
||||
}
|
||||
|
||||
export async function deletePath(_dir_path: string): Promise<void> {
|
||||
if (!(await exists(_dir_path))) {
|
||||
/**
|
||||
* Deletes the complete Path recursevly.
|
||||
* > *WARNING: * Deletes Everything in the Folder.
|
||||
*
|
||||
* Example:
|
||||
* `deletePath('C:\\Test');`
|
||||
*
|
||||
* This deletes all Files and Subfolders in the given Path.
|
||||
* Furthermore the Folder Test itself is removed.
|
||||
*
|
||||
* @export
|
||||
* @param {string} dir_path
|
||||
*/
|
||||
export async function deletePath(dir_path: string): Promise<void> {
|
||||
if (!(await exists(dir_path))) {
|
||||
throw new URIError("path doesnt exits.");
|
||||
}
|
||||
const _totalPath = _dir_path;
|
||||
const _totalPath = dir_path;
|
||||
/** Sort the Pathes according to their length. For instance:
|
||||
* _pathes = ['C:\\Test\\Sub', 'C:\\Test\\Sub\\SubSub']
|
||||
*
|
||||
@ -104,7 +117,7 @@ export async function deletePath(_dir_path: string): Promise<void> {
|
||||
await rmdir(_path);
|
||||
}
|
||||
|
||||
await rmdir(_dir_path);
|
||||
await rmdir(dir_path);
|
||||
}
|
||||
|
||||
/**
|
||||
|
28
lib/helpers/hash.ts
Normal file
28
lib/helpers/hash.ts
Normal file
@ -0,0 +1,28 @@
|
||||
/**
|
||||
* @author M.Karkowski
|
||||
* @email M.Karkowski@zema.de
|
||||
*/
|
||||
|
||||
import { stringifyWithFunctions } from "./jsonMethods";
|
||||
|
||||
/**
|
||||
* Function to generate a Hash
|
||||
* @param obj the Object, that should be hashed
|
||||
*/
|
||||
export function generateHash(obj: any) {
|
||||
// Convert the object to String
|
||||
const str = typeof obj === "string" ? obj : stringifyWithFunctions(obj);
|
||||
|
||||
// Define Vars.
|
||||
let hash = 0,
|
||||
i,
|
||||
chr;
|
||||
if (str.length === 0) return hash.toString();
|
||||
for (i = 0; i < str.length; i++) {
|
||||
chr = str.charCodeAt(i);
|
||||
hash = (hash << 5) - hash + chr;
|
||||
hash |= 0; // Convert to 32bit integer
|
||||
}
|
||||
|
||||
return hash.toString();
|
||||
}
|
@ -8,34 +8,41 @@ import * as async from "./async";
|
||||
import * as descriptors from "./descriptors";
|
||||
import * as functions from "./functionMethods";
|
||||
import * as subject from "./getSubject";
|
||||
import * as hashs from "./hash";
|
||||
import * as ids from "./idMethods";
|
||||
import * as json from "./jsonMethods";
|
||||
import * as schema from "./jsonSchemaMethods";
|
||||
import * as lazy from "./lazyMethods";
|
||||
import * as limit from "./limit";
|
||||
import * as lists from "./lists";
|
||||
import * as objects from "./objectMethods";
|
||||
import * as pathes from "./pathMatchingMethods";
|
||||
import * as runtime from "./runtimeMethods";
|
||||
import * as sets from "./setMethods";
|
||||
import * as singletons from "./singletonMethod";
|
||||
import * as strings from "./stringMethods";
|
||||
import * as taskQueues from "./taskQueue";
|
||||
|
||||
export * from "./arrayMethods";
|
||||
export * from "./async";
|
||||
export * from "./descriptors";
|
||||
export * from "./functionMethods";
|
||||
export * from "./getSubject";
|
||||
export * from "./hash";
|
||||
export * from "./idMethods";
|
||||
export * from "./jsonMethods";
|
||||
export * from "./jsonSchemaMethods";
|
||||
export * from "./lazyMethods";
|
||||
export * from "./limit";
|
||||
export * from "./lists";
|
||||
export * from "./objectMethods";
|
||||
export * from "./pathMatchingMethods";
|
||||
export * from "./runtimeMethods";
|
||||
export * from "./setMethods";
|
||||
export * from "./singletonMethod";
|
||||
export * from "./stringMethods";
|
||||
export * from "./taskQueue";
|
||||
|
||||
export {
|
||||
async,
|
||||
arrays,
|
||||
@ -53,4 +60,7 @@ export {
|
||||
descriptors,
|
||||
functions,
|
||||
limit,
|
||||
hashs,
|
||||
taskQueues,
|
||||
lists,
|
||||
};
|
||||
|
259
lib/helpers/lists.ts
Normal file
259
lib/helpers/lists.ts
Normal file
@ -0,0 +1,259 @@
|
||||
import { dynamicSort, extractListElement } from "./arrayMethods";
|
||||
|
||||
/**
|
||||
* A Priority List. All Items are sorted by a Priority Number.
|
||||
*
|
||||
* @export
|
||||
* @class PriorityList
|
||||
*/
|
||||
export class PriorityList<T> {
|
||||
private _priority_list = new Array<{ priority: number; data: T }>();
|
||||
private _list = new Array<T>();
|
||||
private _updated = false;
|
||||
|
||||
/**
|
||||
* Function to returns a sorted List containing only the Value
|
||||
*
|
||||
* @returns {Array<T>} Sorted List containing the Values.
|
||||
* @memberof PriorityList
|
||||
*/
|
||||
public list(): Array<T> {
|
||||
return extractListElement(this._priority_list, "data");
|
||||
}
|
||||
|
||||
protected _sort(): void {
|
||||
// Sort the List based on the element priority
|
||||
this._priority_list.sort(dynamicSort("priority", true));
|
||||
|
||||
// Adapt the _list element :
|
||||
this._list = extractListElement(this._priority_list, "data");
|
||||
this._updated = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds Data to the Priority List
|
||||
* @param _priority lower => lower priority
|
||||
* @param _data data which are stored
|
||||
*/
|
||||
public push(_priority: number, _data: T): void {
|
||||
// Add the Element with the given priority to the list
|
||||
this._updated = false;
|
||||
this._priority_list.push({ priority: _priority, data: _data });
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Element with the lowest priority
|
||||
*
|
||||
* @param {boolean} [remove=true] Flag to remove the item. Defaults to true. Otherwise it remains in the list.
|
||||
* @return {(T | null)}
|
||||
* @memberof PriorityList
|
||||
*/
|
||||
public highest(remove = true): T | null {
|
||||
if (!this._updated) {
|
||||
this._sort();
|
||||
}
|
||||
const _ret = this._priority_list[remove ? "splice" : "slice"](0, 1)[0];
|
||||
return _ret ? _ret.data : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Element with the highest priority
|
||||
* @param {boolean} [remove=true] Flag to remove the item. Defaults to true. Otherwise it remains in the list.
|
||||
* @return {(T | null)}
|
||||
* @memberof PriorityList
|
||||
*/
|
||||
public lowest(remove = true): T | null {
|
||||
if (!this._updated) {
|
||||
this._sort();
|
||||
}
|
||||
let _ret: { priority: number; data: T } | undefined = undefined;
|
||||
if (remove) {
|
||||
_ret = this._priority_list.pop();
|
||||
} else {
|
||||
_ret = this._priority_list[this._list.length - 1];
|
||||
}
|
||||
|
||||
return _ret ? _ret.data : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Length of the Priority list
|
||||
*
|
||||
* @readonly
|
||||
* @type {number}
|
||||
* @memberof PriorityList
|
||||
*/
|
||||
public get length(): number {
|
||||
return this._priority_list.length;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Limited List. This list at max contains a specific amount of elements.
|
||||
* After the max number of elements has been added, the first element added
|
||||
* will be removed.
|
||||
*/
|
||||
export class LimitedList<T> {
|
||||
/**
|
||||
* Element containing the list
|
||||
*
|
||||
* @private
|
||||
* @type {Array<T>}
|
||||
* @memberof LimitedList
|
||||
*/
|
||||
private _list: Array<T>;
|
||||
/**
|
||||
* Internal Pointer, showing the actual item.
|
||||
*
|
||||
* @private
|
||||
* @type {number}
|
||||
* @memberof LimitedList
|
||||
*/
|
||||
private _pointer: number;
|
||||
|
||||
constructor(public maxLength: number) {
|
||||
this._pointer = -1;
|
||||
this._list = new Array<T>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds Data to the Stack. The Pointer is getting adapted.
|
||||
*
|
||||
* @param {T} data
|
||||
* @returns
|
||||
* @memberof LimitedList
|
||||
*/
|
||||
push(data: T) {
|
||||
// Check if the Maximum length is achieved
|
||||
if (this._list.length >= this.maxLength) {
|
||||
// Remove the First Element
|
||||
this._list = this._list.slice(1, this._pointer + 1);
|
||||
}
|
||||
|
||||
// Store the Content
|
||||
const ret = this._list.push(data);
|
||||
|
||||
// Adapt the Pointer
|
||||
this._pointer = this._list.length - 1;
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Contains the Length of the list.
|
||||
*
|
||||
* @readonly
|
||||
* @memberof LimitedList
|
||||
*/
|
||||
public get length() {
|
||||
return this._list.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current pointer.
|
||||
*
|
||||
* @readonly
|
||||
* @memberof LimitedList
|
||||
*/
|
||||
public get currentPointer() {
|
||||
return this._pointer;
|
||||
}
|
||||
|
||||
last(): T | null {
|
||||
if (this._list.length > 0) {
|
||||
this._pointer = this._list.length - 1;
|
||||
return this._list[this._pointer];
|
||||
}
|
||||
|
||||
// No data available.
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Pointer to the first item.
|
||||
* @returns
|
||||
*/
|
||||
first(): T | null {
|
||||
this._pointer = this._list.length - 1;
|
||||
|
||||
if (this._pointer >= 0 && this._pointer < this._list.length) {
|
||||
return this._list[this._pointer];
|
||||
}
|
||||
|
||||
// No data available.
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the last item. Adapts the pointer and the
|
||||
* current item is the last item.
|
||||
* example:
|
||||
* l = limited.last()
|
||||
* c = limited.current()
|
||||
*
|
||||
* l == c -> True
|
||||
* @returns The last element.
|
||||
*/
|
||||
previous(): T | null {
|
||||
// Check if the Pointer is in the defined Range
|
||||
if (this._pointer - 1 >= 0 && this._pointer - 1 < this._list.length) {
|
||||
return this._list[--this._pointer];
|
||||
}
|
||||
|
||||
// No data available.
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current item, the pointer is showing at.
|
||||
* @returns
|
||||
*/
|
||||
current(): T | null {
|
||||
// Check if the Pointer is in the defined Range
|
||||
if (this._pointer >= 0 && this._pointer < this._list.length) {
|
||||
return this._list[this._pointer];
|
||||
}
|
||||
|
||||
/** No data available any more */
|
||||
return null;
|
||||
}
|
||||
|
||||
next(): T | null {
|
||||
/** Check if the Pointer is in the defined Range */
|
||||
if (this._pointer + 1 >= 0 && this._pointer + 1 < this._list.length) {
|
||||
return this._list[++this._pointer];
|
||||
}
|
||||
|
||||
/** No data available any more */
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pops the last element. If there is no element undefined is returned.
|
||||
* @returns The last element.
|
||||
*/
|
||||
pop(current = false): T {
|
||||
if (current) {
|
||||
const ret = this._list.splice(this._pointer, 1)[0];
|
||||
return ret;
|
||||
}
|
||||
|
||||
const ret = this._list.pop();
|
||||
// Adapt the Pointer
|
||||
this._pointer = this._list.length - 1;
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to iterate over all items.
|
||||
* @param callbackFn
|
||||
* @param thisArg
|
||||
*/
|
||||
public forEach(
|
||||
callbackFn: (item: T, index: number, array: Array<T>) => void,
|
||||
thisArg?: any
|
||||
) {
|
||||
this._list.forEach(callbackFn, thisArg);
|
||||
}
|
||||
}
|
132
lib/helpers/taskQueue.spec.ts
Normal file
132
lib/helpers/taskQueue.spec.ts
Normal file
@ -0,0 +1,132 @@
|
||||
/**
|
||||
* @author Martin Karkowski
|
||||
* @email m.karkowski@zema.de
|
||||
* @desc [description]
|
||||
*/
|
||||
|
||||
import { sleep } from "./async";
|
||||
import { assert, expect } from "chai";
|
||||
import { describe, it } from "mocha";
|
||||
import { ParallelPriorityTaskQueue } from "./taskQueue";
|
||||
|
||||
describe("PriorityTaskQueue", function () {
|
||||
// Describe the required Test:
|
||||
|
||||
describe("Async Functions", function () {
|
||||
it("no parallel execution - no priority", async function () {
|
||||
let called: string[] = [];
|
||||
|
||||
async function delayed(ret: string) {
|
||||
await sleep(25);
|
||||
called.push(ret);
|
||||
return ret;
|
||||
}
|
||||
|
||||
const queue = new ParallelPriorityTaskQueue();
|
||||
queue.maxParallel = 1;
|
||||
queue.usePriority = false;
|
||||
|
||||
const promises = [
|
||||
queue.execute(delayed, ["first"], 5),
|
||||
queue.execute(delayed, ["second"], 10),
|
||||
];
|
||||
|
||||
const start = Date.now();
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
const diff = Date.now() - start;
|
||||
|
||||
assert(diff > 40, "Functions should be called after each other");
|
||||
assert(called[0] == "first", "First should be the first entry");
|
||||
assert(called[1] == "second", "First should be the first entry");
|
||||
});
|
||||
it("parallel execution - no priority", async function () {
|
||||
let called: string[] = [];
|
||||
|
||||
async function delayed(ret: string) {
|
||||
await sleep(25);
|
||||
called.push(ret);
|
||||
return ret;
|
||||
}
|
||||
|
||||
const queue = new ParallelPriorityTaskQueue();
|
||||
queue.maxParallel = 10;
|
||||
queue.usePriority = false;
|
||||
|
||||
const promises = [
|
||||
queue.execute(delayed, ["first"], 5),
|
||||
queue.execute(delayed, ["second"], 10),
|
||||
];
|
||||
|
||||
const start = Date.now();
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
const diff = Date.now() - start;
|
||||
|
||||
assert(diff < 40, "Functions should be called parallel");
|
||||
assert(called[0] == "first", "First should be the first entry");
|
||||
assert(called[1] == "second", "First should be the first entry");
|
||||
});
|
||||
it("no parallel execution - with priority", async function () {
|
||||
let called: string[] = [];
|
||||
|
||||
async function delayed(ret: string) {
|
||||
await sleep(25);
|
||||
called.push(ret);
|
||||
return ret;
|
||||
}
|
||||
|
||||
const queue = new ParallelPriorityTaskQueue();
|
||||
queue.maxParallel = 1;
|
||||
queue.usePriority = true;
|
||||
|
||||
const promises = [
|
||||
queue.execute(delayed, ["first"], 5),
|
||||
queue.execute(delayed, ["second"], 10),
|
||||
queue.execute(delayed, ["third"], 15),
|
||||
];
|
||||
|
||||
const start = Date.now();
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
const diff = Date.now() - start;
|
||||
|
||||
assert(diff > 40, "Functions should be called after each other");
|
||||
assert(called[1] == "third", "second should be the third entry");
|
||||
assert(called[2] == "second", "third should be the second entry");
|
||||
});
|
||||
it("parallel execution - with priority", async function () {
|
||||
let called: string[] = [];
|
||||
|
||||
async function delayed(ret: string) {
|
||||
await sleep(25);
|
||||
called.push(ret);
|
||||
return ret;
|
||||
}
|
||||
|
||||
const queue = new ParallelPriorityTaskQueue();
|
||||
queue.maxParallel = 10;
|
||||
queue.usePriority = true;
|
||||
|
||||
const promises = [
|
||||
queue.execute(delayed, ["first"], 5),
|
||||
queue.execute(delayed, ["second"], 10),
|
||||
queue.execute(delayed, ["third"], 15),
|
||||
];
|
||||
|
||||
const start = Date.now();
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
const diff = Date.now() - start;
|
||||
|
||||
assert(diff < 40, "Functions should be called parallel");
|
||||
assert(called[0] == "first", "First should be the first entry");
|
||||
assert(called[1] == "second", "second should be the second entry");
|
||||
assert(called[2] == "third", "third should be the third entry");
|
||||
});
|
||||
});
|
||||
});
|
155
lib/helpers/taskQueue.ts
Normal file
155
lib/helpers/taskQueue.ts
Normal file
@ -0,0 +1,155 @@
|
||||
import { NopePromise } from "../promise";
|
||||
import { isAsyncFunction } from "./async";
|
||||
import { PriorityList } from "./lists";
|
||||
|
||||
/**
|
||||
* A Task-Queue. This could be used to make parallel
|
||||
* Request run sequentially. For Instance during
|
||||
* Saving and Reading Vars to achive a consistent set
|
||||
* of Data.
|
||||
*
|
||||
* Usage:
|
||||
* // Create a Queue
|
||||
* const _queue = new PriorityTaskQueue();
|
||||
* // Create a Function
|
||||
* const _func = (_input: string, _cb) => {
|
||||
* console.log("Hallo ", _input)
|
||||
* _cb(null, null);
|
||||
* };
|
||||
*
|
||||
* await _queue.execute(_func, 'Welt');
|
||||
*
|
||||
* @export
|
||||
* @class TaskQeue
|
||||
*/
|
||||
export class ParallelPriorityTaskQueue {
|
||||
protected _queue = new PriorityList<{
|
||||
func: (...args) => void;
|
||||
cancel: () => void;
|
||||
args: any;
|
||||
resolve: (data) => void;
|
||||
reject: (err) => void;
|
||||
}>();
|
||||
|
||||
protected _runningTasks = 0;
|
||||
protected _counter = 0;
|
||||
|
||||
public maxParallel = 1;
|
||||
public usePriority = true;
|
||||
|
||||
/**
|
||||
* Executes the given Task. If now Task is running it is executed immediatelly,
|
||||
* otherwise it is pushed in the queue and call if the other tasks are call.
|
||||
*
|
||||
* @param {any} _func The Function which should be called.
|
||||
* @param {any} _param The Data which should be used for the call.
|
||||
* @param {any} _callback The Callback, which should be called after
|
||||
* @memberof TaskQeue
|
||||
*/
|
||||
public execute<T>(
|
||||
func: (...args) => T | Promise<T>,
|
||||
args: any[],
|
||||
priority: number = 0,
|
||||
cancel: () => void = () => {}
|
||||
): NopePromise<T> {
|
||||
let resolve, reject;
|
||||
|
||||
const promise = new NopePromise<T>((res, rej) => {
|
||||
resolve = res;
|
||||
reject = rej;
|
||||
});
|
||||
|
||||
promise.cancel = cancel;
|
||||
|
||||
// Check whether the Execution is activ:
|
||||
if (
|
||||
this._runningTasks < this.maxParallel &&
|
||||
this._queue.length < this.maxParallel
|
||||
) {
|
||||
this._runningTasks++;
|
||||
this._execute({
|
||||
func,
|
||||
args,
|
||||
cancel,
|
||||
resolve,
|
||||
reject,
|
||||
});
|
||||
} else {
|
||||
// Extend the Queue.
|
||||
this._queue.push(this.usePriority ? priority : this._counter++, {
|
||||
func,
|
||||
args,
|
||||
cancel,
|
||||
resolve,
|
||||
reject,
|
||||
});
|
||||
}
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
protected _execute(data: {
|
||||
func: (...args) => any | Promise<any>;
|
||||
args: any[];
|
||||
cancel: () => void;
|
||||
resolve: (data) => void;
|
||||
reject: (err) => void;
|
||||
}) {
|
||||
// Verify whether there is an CancelHandler, if yes.
|
||||
// Register at the Cancel-Handler. Thereby the next
|
||||
// function is call if the currently running Task is
|
||||
// aborted.
|
||||
if (data.cancel) {
|
||||
data.args.push(() => {
|
||||
data.cancel();
|
||||
data.reject(Error("Canceled"));
|
||||
this._finish();
|
||||
});
|
||||
}
|
||||
if (isAsyncFunction(data.func)) {
|
||||
(data.func as (...args: any[]) => Promise<any>)(...data.args)
|
||||
.then((res) => {
|
||||
data.resolve(res);
|
||||
this._finish();
|
||||
})
|
||||
.catch((err) => {
|
||||
data.reject(err);
|
||||
this._finish();
|
||||
});
|
||||
} else {
|
||||
try {
|
||||
const res = (data.func as (...args: any[]) => any)(...data.args);
|
||||
data.resolve(res);
|
||||
} catch (err) {
|
||||
data.reject(err);
|
||||
}
|
||||
this._finish();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal Function to Finish all Tasks.
|
||||
*
|
||||
* @protected
|
||||
* @memberof PriorityTaskQueue
|
||||
*/
|
||||
protected _finish() {
|
||||
// Remove one Element.
|
||||
this._runningTasks--;
|
||||
|
||||
if (this._runningTasks < 0) {
|
||||
this._runningTasks = 0;
|
||||
}
|
||||
// Remove the First Task
|
||||
const task = this._queue.highest();
|
||||
|
||||
// Call the Function with the adapted Callback, if there is a Task Left open.
|
||||
if (task) {
|
||||
this._execute(task);
|
||||
}
|
||||
}
|
||||
|
||||
public get length(): number {
|
||||
return this._queue.length;
|
||||
}
|
||||
}
|
@ -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,
|
||||
};
|
||||
|
@ -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";
|
||||
|
@ -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(
|
||||
|
37
lib/plugins/ackMessages.spec.ts
Normal file
37
lib/plugins/ackMessages.spec.ts
Normal file
@ -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")];
|
||||
});
|
||||
});
|
||||
});
|
226
lib/plugins/ackMessages.ts
Normal file
226
lib/plugins/ackMessages.ts
Normal file
@ -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<OrgBridge>,
|
||||
clConnectivityManager: toConstructor<OrgConnectivityManager>
|
||||
) => {
|
||||
interface AckMessage {
|
||||
messageId: string;
|
||||
dispatcherId: string;
|
||||
}
|
||||
|
||||
class Bridge extends clBridge {
|
||||
/**
|
||||
* Helper to forward received messages.
|
||||
*
|
||||
* @protected
|
||||
* @type {INopeEventEmitter<AckMessage>}
|
||||
* @memberof Bridge
|
||||
*/
|
||||
protected _onMessageReceived: INopeEventEmitter<AckMessage>;
|
||||
/**
|
||||
* Map storing the messages, where we expect an Acknowledgement.
|
||||
*
|
||||
* @protected
|
||||
* @memberof Bridge
|
||||
*/
|
||||
protected _openMessages: Map<
|
||||
string,
|
||||
{
|
||||
received: Set<string>;
|
||||
target: Set<string>;
|
||||
}
|
||||
>;
|
||||
|
||||
public onTransportError: INopeEventEmitter;
|
||||
public defaultTargets: Array<string>;
|
||||
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<T extends keyof EventnameToEventType>(
|
||||
eventname: T,
|
||||
data: EventnameToEventType[T],
|
||||
target: string | Array<string> = null,
|
||||
timeout: number = 0
|
||||
): Promise<void> {
|
||||
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<T extends keyof EventnameToEventType>(
|
||||
eventname: T,
|
||||
cb: (data: EventnameToEventType[T]) => void
|
||||
): Promise<void> {
|
||||
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: <T>() => 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"
|
||||
);
|
34
lib/plugins/hello.ts
Normal file
34
lib/plugins/hello.ts
Normal file
@ -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<OrgConnectivityManager>, 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"
|
||||
);
|
10
lib/plugins/index.ts
Normal file
10
lib/plugins/index.ts
Normal file
@ -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,
|
||||
};
|
67
lib/plugins/plugin.spec.ts
Normal file
67
lib/plugins/plugin.spec.ts
Normal file
@ -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")];
|
||||
});
|
||||
});
|
||||
});
|
380
lib/plugins/plugin.ts
Normal file
380
lib/plugins/plugin.ts
Normal file
@ -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<string, Plugin>();
|
||||
});
|
||||
|
||||
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<string, any> = 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<string, any>} The flatten Object
|
||||
*/
|
||||
function flattenLibrary(
|
||||
lib: any,
|
||||
options: {
|
||||
prefix?: string;
|
||||
splitchar?: string;
|
||||
maxDepth?: number;
|
||||
} = {}
|
||||
): Map<string, any> {
|
||||
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<string, Set<string>>();
|
||||
|
||||
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<string>;
|
||||
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<string>();
|
||||
|
||||
// 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<Plugin | string>,
|
||||
log: boolean = true
|
||||
) {
|
||||
let modified = new Set<string>();
|
||||
|
||||
if (!Array.isArray(plugins)) {
|
||||
plugins = [plugins];
|
||||
}
|
||||
|
||||
if (typeof lib == "string") {
|
||||
lib = require(lib);
|
||||
}
|
||||
|
||||
const pluginsToUse = new Array<Plugin>();
|
||||
|
||||
// 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<string>();
|
||||
|
||||
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<string>();
|
||||
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;
|
||||
}
|
@ -10,3 +10,7 @@ export * from "./IJSONSchema";
|
||||
export * from "./nope/index";
|
||||
export * from "./ui/index";
|
||||
export { nope, ui };
|
||||
|
||||
export type toConstructor<T> = {
|
||||
new (...args): T;
|
||||
};
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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<
|
||||
|
@ -183,19 +183,7 @@ export interface IUiDefinition {
|
||||
*/
|
||||
ui?: IClassDescription["ui"];
|
||||
/**
|
||||
* Name of the Package
|
||||
*/
|
||||
package: string;
|
||||
/**
|
||||
* Path of the defintio file.
|
||||
*/
|
||||
path?: string;
|
||||
/**
|
||||
* Class identifier
|
||||
*/
|
||||
class: string;
|
||||
/**
|
||||
* The Methods of the class
|
||||
* Definition of the UI.
|
||||
*/
|
||||
methods: {
|
||||
[index: string]: IServiceOptions["ui"];
|
||||
|
@ -46,13 +46,6 @@ export async function writeUiFile(
|
||||
// Iterate over the classes.
|
||||
for (const cls of item.package.providedClasses) {
|
||||
const itemToAdd: IUiDefinition["classes"][0] = {
|
||||
// The Class Name
|
||||
class: cls.description.name,
|
||||
// The Package Name
|
||||
package: item.package.nameOfPackage,
|
||||
// The Path of he File.
|
||||
path: item.path,
|
||||
// The defined UI defintions.
|
||||
ui: cls.ui,
|
||||
// Define the Methods elements
|
||||
methods: {},
|
||||
@ -74,7 +67,12 @@ export async function writeUiFile(
|
||||
) {
|
||||
// If an ui definition exists, we want
|
||||
// to export it and store it in our file.
|
||||
uiFile.classes[itemToAdd.class] = itemToAdd;
|
||||
const ui = itemToAdd.ui || {};
|
||||
|
||||
uiFile.classes[cls.description.name] = {
|
||||
...ui,
|
||||
methods: itemToAdd.methods,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -302,7 +300,10 @@ export async function uploadUi(args: Partial<UploadArgs>) {
|
||||
return result;
|
||||
}
|
||||
|
||||
async function getContentOfNewestFile() {
|
||||
async function getContentOfNewestFile(): Promise<{
|
||||
functions: any;
|
||||
classes: any;
|
||||
}> {
|
||||
// Get all Possible Files
|
||||
const _files = await getFiles((item, scope) => {
|
||||
return item.identifier === "ui-definition";
|
||||
@ -330,7 +331,10 @@ export async function uploadUi(args: Partial<UploadArgs>) {
|
||||
logger.error(e);
|
||||
}
|
||||
}
|
||||
return {};
|
||||
return {
|
||||
functions: {},
|
||||
classes: {},
|
||||
};
|
||||
}
|
||||
|
||||
logger.info(
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "nope",
|
||||
"version": "1.4.5",
|
||||
"version": "1.5.0",
|
||||
"description": "NoPE Runtime for Nodejs. For Browser-Support please use nope-browser",
|
||||
"files": [
|
||||
"dist-nodejs/**/*",
|
||||
|
Loading…
Reference in New Issue
Block a user