This commit is contained in:
Martin Karkowski 2022-07-07 09:14:22 +02:00
parent 338a488ec7
commit 39d7fe720e
23 changed files with 1134 additions and 144 deletions

View File

@ -90,9 +90,10 @@ export class NopeConnectivityManager implements INopeConnectivityManager {
public readonly ready: INopeObservable<boolean>;
public readonly dispatchers: IMapBasedMergeData<
string,
string,
INopeStatusInfo
string, // Dispatcher ID
INopeStatusInfo, // Orginal Message
string, // Dispatcher ID
string // Dispatcher ID
>;
/**

View File

@ -114,15 +114,22 @@ export class NopeInstanceManager implements INopeInstanceManager {
* @type {IMapBasedMergeData<string>}
* @memberof NopeInstanceManager
*/
public readonly constructors: IMapBasedMergeData<string, string, string[]>;
public readonly constructors: IMapBasedMergeData<
string, // Dispatcher ID
string[],
string, // Dispatcher ID
string // Available Instances
>;
/**
* Element showing the available instances.
* Its more or less a map, that maps the
* instances with their dispatchers.
*
* T = INopeModuleDescription.
* K = dispatcher - ids
* originalKey = DispatcherID (string);
* originalValue = Available Instance Messages (IAvailableInstancesMsg);
* extractedKey = The name of the Instance (string);
* extractedValue = instance-description (INopeModuleDescription);
*
* @author M.Karkowski
* @type {IMapBasedMergeData<
@ -133,9 +140,10 @@ export class NopeInstanceManager implements INopeInstanceManager {
* @memberof NopeInstanceManager
*/
public readonly instances: IMapBasedMergeData<
INopeModuleDescription,
string,
IAvailableInstancesMsg
string, // Dispatcher ID
IAvailableInstancesMsg, // Available Instance Messages
string, // The name of the Instance?
INopeModuleDescription // The instance-description
>;
/**
@ -195,11 +203,13 @@ export class NopeInstanceManager implements INopeInstanceManager {
this._mappingOfRemoteDispatchersAndGenerators = new Map();
this.constructors = new MapBasedMergeData(
this._mappingOfRemoteDispatchersAndGenerators
);
) as MapBasedMergeData<string, string[], string, string>;
this._mappingOfRemoteDispatchersAndInstances = new Map();
this.instances = new MapBasedMergeData(
this._mappingOfRemoteDispatchersAndInstances
this._mappingOfRemoteDispatchersAndInstances,
"instances/+",
"instances/+/identifier"
);
this.internalInstances = new NopeObservable();
@ -214,11 +224,13 @@ export class NopeInstanceManager implements INopeInstanceManager {
services,
] of _this._rpcManager.services.originalData.entries()) {
// Filter the Generators based on the existing services
const generators = services.services.filter((svc) =>
svc.startsWith(
`nope${SPLITCHAR}core${SPLITCHAR}constructor${SPLITCHAR}`
const generators = services.services
.filter((svc) =>
svc?.id.startsWith(
`nope${SPLITCHAR}core${SPLITCHAR}constructor${SPLITCHAR}`
)
)
);
.map((item) => item.id);
// If the Dispatcher has a generator we will add it.
if (generators.length) {

View File

@ -63,7 +63,7 @@ describe("NopeRpcManager", function () {
await sleep(10);
// Get the Services
const services = manager.services.data.getContent();
const services = manager.services.extractedKey;
expect(services).to.include("helloWorld");
});
@ -213,7 +213,7 @@ describe("NopeRpcManager", function () {
await sleep(10);
// Get the Services
const services = caller.services.data.getContent();
const services = caller.services.extractedKey;
expect(services).to.include("helloWorld");
});
@ -228,7 +228,7 @@ describe("NopeRpcManager", function () {
await sleep(10);
// Get the Services
const services = caller.services.data.getContent();
const services = caller.services.extractedKey;
expect(services).to.include((r as any).id);
});

View File

@ -119,17 +119,20 @@ export class NopeRpcManager<T extends IFunctionOptions = IFunctionOptions>
* Its more or less a map, that maps the
* services with their dispatchers.
*
* T = services name.
* K = dispatcher - ids
* OriginalKey = Dispatcher ID (string);
* OriginalValue = Original Message (IAvailableServicesMsg);
* ExtractedKey = Function ID (string);
* ExtractedValue = FunctionOptions (T);
*
* @author M.Karkowski
* @type {IMapBasedMergeData<string>}
* @memberof INopeRpcManager
*/
public readonly services: IMapBasedMergeData<
string,
string,
IAvailableServicesMsg
string, // Dispatcher ID
IAvailableServicesMsg, // Original Message
string, // Function ID
T // Function Options
>;
/**
@ -242,7 +245,8 @@ export class NopeRpcManager<T extends IFunctionOptions = IFunctionOptions>
this.services = new MapBasedMergeData(
this._mappingOfDispatchersAndServices,
"services"
"services/+",
"services/+/id"
);
this.onCancelTask = new NopeEventEmitter();
@ -453,7 +457,9 @@ export class NopeRpcManager<T extends IFunctionOptions = IFunctionOptions>
// Define the Message
const message: IAvailableServicesMsg = {
dispatcher: this._id,
services: Array.from(this._registeredServices.keys()),
services: Array.from(this._registeredServices.values()).map(
(item) => item.options
),
};
if (this._logger?.enabledFor(DEBUG)) {
@ -783,6 +789,9 @@ export class NopeRpcManager<T extends IFunctionOptions = IFunctionOptions>
let _id = options.id || generateId();
_id = options.addNopeServiceIdPrefix ? this.adaptServiceId(_id) : _id;
// Make shure we assign our id
options.id = _id;
let _func = func;
if (!this.__warned && !isAsyncFunction(func)) {

View File

@ -31,7 +31,7 @@ export function generateSelector(
case "master":
return async (opts) => {
const masterId = core.connectivityManager.master.id;
const data = core.rpcManager.services.reverseSimplified;
const data = core.rpcManager.services.keyMappingReverse;
if (data.has(opts.serviceName)) {
const arr = Array.from(data.get(opts.serviceName));
@ -45,7 +45,7 @@ export function generateSelector(
};
case "first":
return async (opts) => {
const data = core.rpcManager.services.reverseSimplified;
const data = core.rpcManager.services.keyMappingReverse;
if (data.has(opts.serviceName)) {
const arr = Array.from(data.get(opts.serviceName));
@ -75,7 +75,7 @@ export function generateSelector(
// 1. Get the current Host name of our dispatcher
const host = core.connectivityManager.info.host.name;
return async (opts) => {
const data = core.rpcManager.services.reverseSimplified;
const data = core.rpcManager.services.keyMappingReverse;
if (data.has(opts.serviceName)) {
const items = Array.from(data.get(opts.serviceName));

View File

@ -89,7 +89,7 @@ export async function generatePingAccessors(dispatcher: INopeDispatcher) {
// Function to Ping all Services
const pingAll = async () => {
const dispatchers = Array.from(
dispatcher.connectivityManager.dispatchers.reverseSimplified.get(
dispatcher.connectivityManager.dispatchers.keyMappingReverse.get(
serviceName
)
);
@ -233,12 +233,12 @@ export async function generateDefineMaster(dispatcher: INopeDispatcher) {
const service = `nope/baseService/defineAsMaster`;
// Get the Matching Dispatchers.
const dispatchers =
dispatcher.connectivityManager.dispatchers.reverseSimplified.get(
const relevantDispatchers =
dispatcher.rpcManager.services.keyMappingReverse.get(
`nope/baseService/defineAsMaster`
);
dispatchers.delete(masterId);
const targets = Array.from(dispatchers);
relevantDispatchers.delete(masterId);
const targets = Array.from(relevantDispatchers);
await dispatcher.rpcManager.performCall(
targets.map((_) => service),

View File

@ -73,7 +73,10 @@ export class NopeDispatcher extends NopeCore implements INopeDispatcher {
.map((item) => item.identifier);
break;
case "services":
items = this.rpcManager.services.data.getContent();
items = Array.from(
// Extract the Ids of the Services
this.rpcManager.services.simplified.keys()
);
break;
case "properties":
items = this.dataDistributor.publishers.data.getContent();

View File

@ -4,7 +4,7 @@
* @desc [description]
*/
import { rgetattr } from "./objectMethods";
import { convertData, deepEqual, rgetattr } from "./objectMethods";
const __sentinal = {
unique: "value",
@ -15,12 +15,42 @@ const __sentinal = {
*
* @author M.Karkowski
* @export
* @template K
* @template V
* @param {Map<K, V>} map
* @return {*} {Set<V>}
* @template D Return Type
* @template K The Key of the Map
* @template V The Value of the Map
* @param {Map<K, V>} map The Map
* @param {string} [path=""] The Path of the Data to extract.
* @param {string} [pathKey=null] The Path of the unique key. If set to `null` -> The Item is selected directly.
* @return {*} {Set<D>}
*/
export function extractUniqueValues<D>(map: Map<any, any>, path = ""): Set<D> {
export function extractUniqueValues<D, K = any, V = any>(
map: Map<K, V>,
path = "",
pathKey: string = null
): Set<D> {
if (pathKey === null) {
pathKey = path;
}
if (path !== pathKey) {
const items = extractValues(map, path) as D[];
const itemKeys = new Set();
const ret: D[] = [];
for (const item of items) {
const key = rgetattr(item, pathKey);
if (!itemKeys.has(key)) {
itemKeys.add(key);
ret.push(item);
}
return new Set(ret);
}
}
return new Set(extractValues(map, path));
}
@ -52,62 +82,165 @@ export function extractValues<D, K>(map: Map<K, any>, path = ""): Array<D> {
}
/**
* Transform the values.
*
*
* @author M.Karkowski
* @export
* @template D
* @template K
* @param {Map<K, any>} map
* @param {string} [path=""]
* @return {*} {Map<K, D>}
* @template ExtractedValue
* @template ExtractedKey
* @template OriginalKey
* @param {Map<OriginalKey, any>} map
* @param {string} [pathExtractedValue=""]
* @param {string} [pathExtractedKey=null] Additional Path of a Key.
* @return {*} {Map<ExtractedKey, ExtractedData>}
*/
export function transformValues<D, K = any>(
map: Map<K, any>,
path = ""
): Map<K, D> {
const m = new Map<K, D>();
export function tranformMap<
ExtractedKey = string,
ExtractedValue = any,
OriginalKey = string
>(
map: Map<OriginalKey, any>,
pathExtractedValue: string,
pathExtractedKey: string,
equals: (a: ExtractedValue, b: ExtractedValue) => boolean = deepEqual
) {
const keyMapping = new Map<OriginalKey, Set<ExtractedKey>>();
const reverseKeyMapping = new Map<ExtractedKey, Set<OriginalKey>>();
const conflicts = new Map<ExtractedKey, Set<ExtractedValue>>();
const extractedMap = new Map<ExtractedKey, ExtractedValue>();
const orgKeyToExtractedValue = new Map<OriginalKey, Set<ExtractedValue>>();
const amountOf = new Map<ExtractedKey, number>();
const props: {
query: string;
key: string;
}[] = [];
if (pathExtractedKey) {
props.push({
key: "key",
query: pathExtractedKey,
});
}
if (pathExtractedValue) {
props.push({
key: "value",
query: pathExtractedValue,
});
}
// Iterate over the Entries of the Map.
// then we will extract the data stored in the Value.
for (const [k, v] of map.entries()) {
if (path) {
const data: D | typeof __sentinal = rgetattr(v, path, __sentinal);
if (data !== __sentinal) {
m.set(k, data as D);
const extracted = convertData<{ key: ExtractedKey; value: ExtractedValue }>(
v,
props
);
// Store the Key.
keyMapping.set(k, new Set());
orgKeyToExtractedValue.set(k, new Set());
for (const item of extracted) {
if (extractedMap.has(item.key)) {
// If the extracted new key has already been defined,
// we have to determine whether the stored item matches
// the allready provided definition.
if (!equals(extractedMap.get(item.key), item.value)) {
// Conflict detected
if (!conflicts.has(item.key)) {
conflicts.set(item.key, new Set());
}
// Store the conflict.
conflicts.get(item.key).add(item.value);
conflicts.get(item.key).add(extractedMap.get(item.key));
} else {
// Store the determined amount.
amountOf.set(item.key, (amountOf.get(item.key) || 0) + 1);
}
} else {
// Store the item.
extractedMap.set(item.key, item.value);
// Store the determined amount.
amountOf.set(item.key, (amountOf.get(item.key) || 0) + 1);
}
} else {
m.set(k, v as any as D);
// If the reverse haven't been set ==> create it.
if (!reverseKeyMapping.has(item.key)) {
reverseKeyMapping.set(item.key, new Set());
}
// Store the mapping of new-key --> org-key.
reverseKeyMapping.get(item.key).add(k);
// Store the mapping of org-key --> new-key.
keyMapping.get(k).add(item.key);
orgKeyToExtractedValue.get(k).add(item.value);
}
}
return m;
return {
extractedMap,
keyMapping,
conflicts,
keyMappingReverse: reverseKeyMapping,
orgKeyToExtractedValue,
amountOf,
};
}
/**
* Reverses the given map.
*
* If the path is provided, the Data is extracted based on the given path.
* If the `pathKey`, a different Key is used.
*
* @author M.Karkowski
* @export
* @template K
* @template V
* @param {Map<K, V>} map
* @param {Map<any,any>} map
* @param {string} [path=""]
* @param {string} [pathKey=null]
* @return {*} {Map<V, Set<K>>}
*/
export function reverse<K, V>(map: Map<K, V>): Map<V, Set<K>> {
export function reverse<K, V>(
map: Map<any, any>,
path: string = "",
pathKey: string = null
): Map<V, Set<K>> {
const m = new Map<V, Set<K>>();
if (pathKey === null) {
pathKey = path;
}
for (const [k, v] of map.entries()) {
if (Array.isArray(v)) {
for (const _v of v) {
let keyToUse = k;
if (pathKey) {
keyToUse = rgetattr(v, pathKey, __sentinal);
}
let valueToUse = v;
if (path) {
valueToUse = rgetattr(v, path, __sentinal);
}
if (Array.isArray(valueToUse)) {
for (const _v of valueToUse) {
if (!m.has(_v)) {
m.set(_v, new Set());
}
m.get(_v).add(k);
m.get(_v).add(keyToUse);
}
} else {
if (!m.has(v)) {
m.set(v, new Set());
if (!m.has(valueToUse)) {
m.set(valueToUse, new Set());
}
m.get(v).add(k);
m.get(valueToUse).add(keyToUse);
}
}

View File

@ -92,7 +92,7 @@ describe("MapBasedMergeData", function () {
d.update();
expect([...d.reverseSimplified.keys()]).contains("b");
expect([...d.keyMappingReverse.keys()]).contains("b");
done();
});
@ -105,7 +105,7 @@ describe("MapBasedMergeData", function () {
d.update();
expect([...d.reverseSimplified.keys()]).contains("b");
expect([...d.keyMappingReverse.keys()]).contains("b");
done();
});
@ -150,6 +150,87 @@ describe("MapBasedMergeData", function () {
});
});
it("data subscription - nested data", function (done) {
const m = new Map<string, { key: string; data: string }>();
const d_1 = new MapBasedMergeData<
string,
{ key: string; data: string },
string,
{ key: string; data: string }
>(m, "", "key");
m.set("a", { key: "keyA", data: "dataA" });
m.set("b", { key: "keyB", data: "dataB" });
d_1.update();
assert.isTrue(
d_1.amountOf.size === 2,
"The Element contains 3 different items."
);
expect([...d_1.keyMapping.keys()]).to.contain("a");
expect([...d_1.keyMappingReverse.keys()]).to.contain("keyA");
expect([...d_1.simplified.keys()]).to.contain("keyA");
expect([...d_1.keyMappingReverse.values()]).to.contain("keyA");
const d_2 = new MapBasedMergeData<
string,
{ key: string; data: string },
string,
string
>(m, "data", "key");
d_2.update();
expect([...d_1.keyMapping.keys()]).to.contain("a");
expect([...d_1.keyMappingReverse.keys()]).to.contain("keyA");
expect([...d_1.simplified.keys()]).to.contain("keyA");
expect([...d_1.keyMappingReverse.keys()]).to.contain("dataB");
done();
});
// it("data subscription - nested data array", function (done) {
// const m = new Map<string, { key: string; data: string }[]>();
// const d_1 = new MapBasedMergeData<
// string,
// { key: string; data: string }[],
// string,
// { key: string; data: string }
// >(m, "", "key");
// m.set("a", [
// { key: "keyA", data: "dataA" },
// { key: "keyB", data: "dataB" },
// ]);
// m.set("b", [{ key: "keyC", data: "dataC" }]);
// d_1.update();
// assert.isTrue(
// d_1.amountOf.size === 2,
// "The Element contains 3 different items."
// );
// expect([...d_1.keyMapping.keys()]).to.contain("a");
// expect([...d_1.keyMappingReverse.keys()]).to.contain("keyA");
// expect([...d_1.simplified.keys()]).to.contain("keyA");
// expect([...d_1.keyMappingReverse.values()]).to.contain("keyA");
// const d_2 = new MapBasedMergeData<
// string,
// { key: string; data: string },
// string,
// string
// >(m, "data", "key");
// d_2.update();
// expect([...d_1.keyMapping.keys()]).to.contain("a");
// expect([...d_1.keyMappingReverse.keys()]).to.contain("keyA");
// expect([...d_1.simplified.keys()]).to.contain("keyA");
// expect([...d_1.keyMappingReverse.keys()]).to.contain("dataB");
// done();
// });
it("data subscription. Update called twice", function (done) {
const m = new Map<string, string>();
const d = new MapBasedMergeData(m);

View File

@ -8,14 +8,11 @@ import { NopeEventEmitter } from "../eventEmitter/nopeEventEmitter";
import { determineDifference } from "../helpers/setMethods";
import { NopeObservable } from "../observables/nopeObservable";
import { INopeEventEmitter, INopeObservable } from "../types/nope";
import { IMergeData } from "../types/nope/nopeHelpers.interface";
import { countElements } from "./arrayMethods";
import {
extractUniqueValues,
extractValues,
reverse,
transformValues,
} from "./mapMethods";
IMapBasedMergeData,
IMergeData,
} from "../types/nope/nopeHelpers.interface";
import { extractUniqueValues, tranformMap } from "./mapMethods";
export class MergeData<T, D = any> implements IMergeData<T, D> {
/**
@ -92,22 +89,48 @@ export class MergeData<T, D = any> implements IMergeData<T, D> {
}
}
export class MapBasedMergeData<T, K, V>
extends MergeData<T, Map<K, V>>
implements IMergeData<T, Map<K, V>>
export class MapBasedMergeData<
OriginalKey,
OriginalValue,
ExtractedKey = OriginalKey,
ExtractedValue = OriginalValue
>
extends MergeData<ExtractedValue, Map<OriginalKey, OriginalValue>>
implements
IMapBasedMergeData<
OriginalKey,
OriginalValue,
ExtractedKey,
ExtractedValue
>
{
public amountOf: Map<T, number>;
public simplified: Map<K, T>;
public reverseSimplified: Map<T, Set<K>>;
public amountOf: Map<ExtractedKey, number>;
public simplified: Map<ExtractedKey, ExtractedValue>;
public keyMapping: Map<OriginalKey, Set<ExtractedKey>>;
public keyMappingReverse: Map<ExtractedKey, Set<OriginalKey>>;
public conflicts: Map<ExtractedKey, Set<ExtractedValue>>;
public orgKeyToExtractedValue: Map<OriginalKey, Set<ExtractedValue>>;
public extractedKey: ExtractedKey[];
public extractedValue: ExtractedValue[];
constructor(originalData: Map<K, V>, protected _path = "") {
constructor(
originalData: Map<OriginalKey, OriginalValue>,
protected _path: keyof OriginalValue | string = "",
protected _pathKey: keyof OriginalValue | string = null
) {
super(originalData, (m) => {
return extractUniqueValues(m, _path);
return extractUniqueValues(m, _path as string, _pathKey as string);
});
this.amountOf = new Map();
this.simplified = new Map();
this.reverseSimplified = new Map();
this.amountOf = new Map<ExtractedKey, number>();
this.simplified = new Map<ExtractedKey, ExtractedValue>();
this.keyMapping = new Map<OriginalKey, Set<ExtractedKey>>();
this.keyMappingReverse = new Map<ExtractedKey, Set<OriginalKey>>();
this.conflicts = new Map<ExtractedKey, Set<ExtractedValue>>();
this.orgKeyToExtractedValue = new Map<OriginalKey, Set<ExtractedValue>>();
this.extractedKey = [];
this.extractedValue = [];
}
/**
@ -117,15 +140,27 @@ export class MapBasedMergeData<T, K, V>
* @param {*} [data=this.originalData]
* @memberof MergeData
*/
public update(data: Map<K, V> = null): void {
public update(data: Map<OriginalKey, OriginalValue> = null): void {
if (data !== null) {
this.originalData = data;
}
// Now lets update the amount of the data:
this.amountOf = countElements(extractValues(this.originalData, this._path));
this.simplified = transformValues(this.originalData, this._path);
this.reverseSimplified = reverse(this.simplified);
const result = tranformMap<ExtractedKey, ExtractedValue, OriginalKey>(
this.originalData,
this._path as string,
this._pathKey as string
);
// Now assign the results to our items.
this.simplified = result.extractedMap;
this.amountOf = result.amountOf;
this.keyMapping = result.keyMapping;
this.keyMappingReverse = result.keyMappingReverse;
this.conflicts = result.conflicts;
this.orgKeyToExtractedValue = result.orgKeyToExtractedValue;
this.extractedKey = [...this.simplified.keys()];
this.extractedValue = [...this.simplified.values()];
super.update(data);
}

View File

@ -6,7 +6,13 @@
import { assert, expect } from "chai";
import { describe, it } from "mocha";
import { deepClone, flattenObject, rgetattr } from "./objectMethods";
import {
convertData,
deepClone,
flattenObject,
rgetattr,
rqueryAttr,
} from "./objectMethods";
describe("objectMethods", function () {
// Describe the required Test:
@ -164,4 +170,231 @@ describe("objectMethods", function () {
assert.isTrue(result === "test", "we expected 'test' but got: " + result);
});
});
describe("rgettattrQuery", function () {
it("query empty data", function () {
let data = {};
let result = rqueryAttr(data, "test/+");
assert.isTrue(
result.length === 0,
"we expected 0 entries but got: " + result.length.toString()
);
});
it("query data - single", function () {
let data = { deep: { nested: "test" } };
let result = rqueryAttr(data, "deep/nested");
assert.isTrue(
result.length === 1,
"we expected 1 entries but got: " + result.length.toString()
);
assert.isTrue(
result[0].path === "deep/nested",
"we expected the path to be 'deep/nested', but got" + result[0].path
);
assert.isTrue(
result[0].data === "test",
"we expected the data to be 'test', but got" + result[0].data
);
});
it("query data - singlelevel-wildcard", function () {
let data = {
deep: { nested_01: { nested_02: "test_01" }, nested_03: "test_02" },
not: { nested: "hello" },
};
let result = rqueryAttr(data, "deep/+");
assert.isTrue(
result.length === 2,
"we expected 2 entries but got: " + result.length.toString()
);
const pathes = result.map((item) => item.path);
assert.isTrue(
pathes.includes("deep/nested_01") && pathes.includes("deep/nested_03"),
`we expected the "deep/nested_01" and "deep/nested_03" have been found, but got` +
pathes.toString()
);
});
it("query data-array - singlelevel-wildcard", function () {
let data = {
array: [
{
data: 0,
},
{
data: 1,
},
],
not: { nested: "hello" },
};
let result = rqueryAttr(data, "array/+/data");
assert.isTrue(
result.length === 2,
"we expected 2 entries but got: " + result.length.toString()
);
const items = result.map((item) => item.data);
assert.isTrue(
items.includes(1) && items.includes(0),
`we expected the data contains "0" and "1", but got` + items.toString()
);
});
it("query data - multilevel-wildcard", function () {
let data = {
deep: { nested_01: { nested_02: "test_01" }, nested_03: "test_02" },
not: { nested: "hello" },
};
let result = rqueryAttr(data, "deep/#");
const pathes = result.map((item) => item.path);
assert.isTrue(
result.length === 3,
"we expected 3 entries but got: " + pathes.toString()
);
assert.isTrue(
pathes.includes("deep/nested_01") &&
pathes.includes("deep/nested_01/nested_02") &&
pathes.includes("deep/nested_03"),
`we expected the "deep/nested_01" and "deep/nested_01/nested_02" and "deep/nested_03" have been found, but got` +
pathes.toString()
);
});
});
describe("convertData", function () {
it("query empty data", function () {
let data = {};
let result = convertData(data, [
{
key: "result",
query: "a/b",
},
]);
assert.isTrue(
result.length === 0,
"we expected 0 entries but got: " +
result.length.toString() +
"\n" +
JSON.stringify(result)
);
});
it("query data - single", function () {
let data = { deep: { nested: "test" } };
let result = convertData<{ result: string }>(data, [
{
key: "result",
query: "deep/nested",
},
]);
assert.isTrue(
result.length === 1,
"we expected 1 entries but got: " +
result.length.toString() +
"\n" +
JSON.stringify(result)
);
assert.isTrue(
result[0].result === "test",
"we expected the path to be 'test', but got" +
result[0] +
"\n" +
JSON.stringify(result)
);
});
it("query data - singlelevel-wildcard", function () {
let data = { deep: { nested: "test" } };
let result = convertData<{ result: string }>(data, [
{
key: "result",
query: "deep/+",
},
]);
assert.isTrue(
result.length === 1,
"we expected 1 entries but got: " +
result.length.toString() +
"\n" +
JSON.stringify(result)
);
assert.isTrue(
result[0].result === "test",
"we expected the path to be 'test', but got" +
result[0] +
"\n" +
JSON.stringify(result)
);
});
it("query data-array - singlelevel-wildcard", function () {
let data = {
array: [
{
data1: 0,
data2: "a",
},
{
data1: 1,
data2: "a",
},
],
not: { nested: "hello" },
};
let result = convertData<{ a: number; b: string }>(data, [
{
key: "a",
query: "array/+/data1",
},
{
key: "b",
query: "array/+/data2",
},
]);
assert.isTrue(
result.length === 2,
"we expected 2 entries but got: " +
result.length.toString() +
"\n" +
JSON.stringify(result)
);
const items = result.map((item) => item.a);
assert.isTrue(
items.includes(1) && items.includes(0),
`we expected the data contains "0" and "1", but got` +
items.toString() +
"\n" +
JSON.stringify(result)
);
});
it("query data-array - test exception", function () {
let data = {
array: [
{
data1: 0,
data2: "a",
},
{
data1: 1,
data2: "a",
},
],
not: { nested: "hello" },
};
try {
let result = convertData<{ a: number; b: string }>(data, [
{
key: "a",
query: "array/+/data1",
},
{
key: "b",
query: "array/+/data2",
},
]);
assert.isTrue(false, "Failed to raise exception");
} catch (e) {}
});
});
});

View File

@ -6,6 +6,12 @@
export const SPLITCHAR = "/";
import { deepEqual as _deepEqual } from "assert";
import { getLeastCommonPathSegment } from "./path";
import {
comparePatternAndPath,
containsWildcards,
MULTI_LEVEL_WILDCARD,
} from "./pathMatchingMethods";
const _sentinel = new Object();
@ -13,10 +19,11 @@ const _sentinel = new Object();
* Function to recurvely get an Attribute of the Object.
*
* @export
* @param {*} _data
* @param {string} _path
* @param {*} [_default=_sentinel]
* @returns {*}
* @example data = [{a:1},{a:2}]; rgetattr(data, "0/a") -> 0; rgetattr(data,"hallo", "default") -> "default"
* @param {*} _data Data, where the item should be received
* @param {string} _path The path to extract
* @param {*} [_default=_sentinel] Default Object, if nothing else is provided
* @returns {*} The extracted data.
*/
export function rgetattr<T = any>(
_data: any,
@ -57,6 +64,118 @@ export function rgetattr<T = any>(
return _obj;
}
/**
* Helper to query data from an object.
* @example data = [{a:1},{a:2}]; rqueryAttr(data, "+/a") -> [{path: "0/a", data: 0},{path: "1/a", data: 1}]
* @param data The data
* @param query The query to use.
* @returns Returns an array
*/
export function rqueryAttr<T>(
data: any,
query: string
): {
path: string;
data: T;
}[] {
if (!containsWildcards(query)) {
const _sentinel = {
id: Date.now(),
};
const extractedData = rgetattr<T>(data, query, _sentinel);
if (extractedData === (_sentinel as any)) {
return [];
}
return [{ path: query, data: extractedData }];
}
let ret: {
path: string;
data: T;
}[] = [];
const multiLevel = query.includes(MULTI_LEVEL_WILDCARD);
// Determine the max depth
const maxDepth = multiLevel ? Infinity : query.split(SPLITCHAR).length;
// get the flatten object
const map = flattenObject(data, {
maxDepth,
onlyPathToSimpleValue: false,
});
// Iterate over the items and use our
// path matcher to extract the matching items.
for (const [path, value] of map.entries()) {
const r = comparePatternAndPath(query, path);
if (r.affectedOnSameLevel || (multiLevel && r.affectedByChild)) {
ret.push({
path,
data: value,
});
}
}
return ret;
}
/**
* Helper to query data from an object.
* @example data = [{a:1},{a:2}]; rqueryAttr(data, "+/a") -> [{path: "0/a", data: 0},{path: "1/a", data: 1}]
* @param data The data
* @param query The query to use.
* @returns Returns an array
*/
export function convertData<T>(
data: any,
props: {
key: string;
query: string;
}[]
): T[] {
const ret: {
[index: string]: {
path: string;
data: any;
}[];
} = {};
const commonPattern = getLeastCommonPathSegment(
props.map((item) => item.query)
);
if (!commonPattern) {
throw Error("No common pattern has been found");
}
props.map((prop) => {
ret[prop.key] = rqueryAttr(data, prop.query);
});
const helper: { [index: string]: { [index: string]: any } } = {};
for (const prop of props) {
// get the item
const items = ret[prop.key];
for (const item of items) {
const result = comparePatternAndPath(commonPattern, item.path);
if (result.pathToExtractData) {
if (helper[result.pathToExtractData] === undefined) {
helper[result.pathToExtractData] = {};
}
helper[result.pathToExtractData][prop.key] = item.data;
}
}
}
return Object.getOwnPropertyNames(helper).map((key) => helper[key]) as T[];
}
/**
* Function to Set recursely a Attribute of an Object
*
@ -213,11 +332,11 @@ export function isObjectOrArray(value: any): boolean {
* data = {a : { b : { c : 1, d: "hallo"}}}
*
* // Normal Call
* res = flatteObject(data,'')
* res = flatteObject(data)
* => res = {"a.b.c":1,"a.b.d":"hallo"}
*
* // With a Selected prefix 'additional.name'
* res = flatteObject(data,'additional.name')
* res = flatteObject(data,{prefix:'additional.name'})
* => res = {"additional.name.a.b.c":1,"additional.name.a.b.d":"hallo"}
*
* @export

178
lib/helpers/path.spec.ts Normal file
View File

@ -0,0 +1,178 @@
/**
* @author Martin Karkowski
* @email m.karkowski@zema.de
* @desc [description]
*/
import { assert } from "chai";
import { describe, it } from "mocha";
import { getLeastCommonPathSegment } from "./path";
describe("path", function () {
// Describe the required Test:
describe("getLeastCommonPathSegment", function () {
it("equal pathes", function () {
assert.equal(
"a/b",
getLeastCommonPathSegment(["a/b", "a/b"]),
`getLeastCommonPathSegment(["a/b", "a/b"])`
);
assert.equal(
"a/b",
getLeastCommonPathSegment(["a/b", "a/b/c"]),
`getLeastCommonPathSegment("a/b", "a/b/c")`
);
assert.equal(
"a/b",
getLeastCommonPathSegment(["a/b/c", "a/b", "a/b/d"]),
`getLeastCommonPathSegment("a/b/c", "a/b")`
);
assert.equal(
"a",
getLeastCommonPathSegment(["a/b", "a/+"]),
`getLeastCommonPathSegment(["a/b", "a/+"])`
);
assert.isFalse(
getLeastCommonPathSegment(["c/a", "a/+"]),
"Pathes should be different"
);
});
it("equal pathes - singlelevel", function () {
assert.equal(
"a/b",
getLeastCommonPathSegment(["a/b", "a/b"], {
considerSingleLevel: true,
}),
`getLeastCommonPathSegment(["a/b", "a/b"], { considerSingleLevel: true })`
);
assert.equal(
"a/b",
getLeastCommonPathSegment(["a/b", "a/+"], {
considerSingleLevel: true,
}),
`getLeastCommonPathSegment(["a/b", "a/+"], { considerSingleLevel: true })`
);
assert.equal(
"a/b/c",
getLeastCommonPathSegment(["a/b/c", "a/+/c"], {
considerSingleLevel: true,
}),
`getLeastCommonPathSegment(["a/b/c", "a/+/c"], { considerSingleLevel: true })`
);
assert.equal(
"a/b/c",
getLeastCommonPathSegment(["+/b/c", "a/+/c"], {
considerSingleLevel: true,
}),
`getLeastCommonPathSegment(["+/b/c", "a/+/c"], { considerSingleLevel: true })`
);
assert.equal(
"a/b/c",
getLeastCommonPathSegment(["+/+/+", "a/b/c"], {
considerSingleLevel: true,
}),
`getLeastCommonPathSegment("+/+/+", "a/b/c", { considerSingleLevel: true })`
);
assert.equal(
"a/b/c",
getLeastCommonPathSegment(["+/b/+", "a/+/c"], {
considerSingleLevel: true,
}),
`getLeastCommonPathSegment(["+/b/+", "a/+/c"], { considerSingleLevel: true })`
);
assert.equal(
"a/+/c",
getLeastCommonPathSegment(["+/+/+", "a/+/c"], {
considerSingleLevel: true,
}),
`getLeastCommonPathSegment(["+/+/+", "a/+/c"], { considerSingleLevel: true })`
);
});
it("equal pathes - multilevel", function () {
assert.equal(
"a/b",
getLeastCommonPathSegment(["a/b", "a/b"], { considerMultiLevel: true }),
`getLeastCommonPathSegment(["a/b", "a/b"], { considerMultiLevel: true }) == false`
);
assert.equal(
"a",
getLeastCommonPathSegment(["a/b", "a/+"], { considerMultiLevel: true }),
`getLeastCommonPathSegment(["a/b", "a/+"], { considerMultiLevel: true })`
);
assert.equal(
"a",
getLeastCommonPathSegment(["a/b/c", "a/+/c"], {
considerMultiLevel: true,
}),
`getLeastCommonPathSegment(["a/b/c", "a/+/c"], { considerMultiLevel: true })`
);
assert.equal(
false as any,
getLeastCommonPathSegment(["+/b/c", "a/+/c"], {
considerMultiLevel: true,
}),
`getLeastCommonPathSegment(["+/b/c", "a/+/c"], { considerMultiLevel: true })`
);
assert.equal(
false as any,
getLeastCommonPathSegment(["+/+/+", "a/b/c"], {
considerMultiLevel: true,
}),
`getLeastCommonPathSegment(["+/+/+", "a/b/c"], { considerMultiLevel: true })`
);
assert.equal(
false as any,
getLeastCommonPathSegment(["+/b/+", "a/+/c"], {
considerMultiLevel: true,
}),
`getLeastCommonPathSegment(["+/b/+", "a/+/c"], { considerMultiLevel: true })`
);
assert.equal(
"a/b/c",
getLeastCommonPathSegment(["#", "a/b/c"], {
considerMultiLevel: true,
}),
`getLeastCommonPathSegment(["#", "a/b/c"], { considerMultiLevel: true })`
);
assert.equal(
"a/b/c",
getLeastCommonPathSegment(["a/#", "a/b/c"], {
considerMultiLevel: true,
}),
`getLeastCommonPathSegment(["a/#", "a/b/c"], { considerMultiLevel: true })`
);
assert.equal(
"a/b",
getLeastCommonPathSegment(["a/#", "a/b", "a/b/c"], {
considerMultiLevel: true,
}),
`getLeastCommonPathSegment(["a/#", "a/b"], { considerMultiLevel: true })`
);
});
});
});

View File

@ -4,8 +4,97 @@
* @desc [description]
*/
import { SPLITCHAR } from "./objectMethods";
import {
MULTI_LEVEL_WILDCARD,
SINGLE_LEVEL_WILDCARD,
} from "./pathMatchingMethods";
import { replaceAll } from "./stringMethods";
export function convertPath(path: string): string {
return replaceAll(path, [".", "[", ["]", ""]], "/");
return replaceAll(path, [".", "[", ["]", ""]], SPLITCHAR);
}
/**
*
* @param pathes The Segments to compare
* @param opts Additional Options.
* @returns
*/
export function getLeastCommonPathSegment(
pathes: string[],
opts: {
considerSingleLevel?: boolean;
considerMultiLevel?: boolean;
} = {}
): string | false {
let currentPath = pathes.pop();
while (pathes.length > 0) {
let next = pathes.pop();
currentPath = _getLeastCommonPathSegment(currentPath, next, opts) as string;
if (!currentPath) {
return currentPath;
}
}
return currentPath;
}
function _getLeastCommonPathSegment(
path01: string,
path02: string,
opts: {
considerSingleLevel?: boolean;
considerMultiLevel?: boolean;
} = {}
) {
const p1 = convertPath(path01).split(SPLITCHAR);
const p2 = convertPath(path02).split(SPLITCHAR);
const ret: string[] = [];
let idx = 0;
const max = Math.max(p1.length, p2.length);
while (idx < max) {
if (p1[idx] == p2[idx]) {
// Add the Item.
ret.push(p1[idx]);
} else if (opts.considerSingleLevel) {
if (p1[idx] === SINGLE_LEVEL_WILDCARD) {
// Add the Item.
ret.push(p2[idx]);
} else if (p2[idx] === SINGLE_LEVEL_WILDCARD) {
// Add the Item.
ret.push(p1[idx]);
} else {
break;
}
} else if (opts.considerMultiLevel) {
if (p1[idx] === MULTI_LEVEL_WILDCARD) {
// Add the Item.
ret.push(...p2.slice(idx));
break;
} else if (p2[idx] === MULTI_LEVEL_WILDCARD) {
// Add the Item.
ret.push(...p1.slice(idx));
break;
} else {
break;
}
} else {
break;
}
idx += 1;
}
if (ret.length) {
return ret.join(SPLITCHAR);
}
return false;
}

View File

@ -26,6 +26,7 @@ import {
INopeEventEmitter,
INopeObserver,
INopeTopic,
IPubSubEmitterOptions,
IPubSubOptions,
IPubSubSystem,
ITopicSetContentOptions,
@ -69,10 +70,20 @@ export class PubSubSystemBase<
readonly onIncrementalDataChange: INopeEventEmitter<IIncrementalChange>;
// See interface description
readonly subscriptions: IMapBasedMergeData<string>;
readonly subscriptions: IMapBasedMergeData<
O,
IPubSubEmitterOptions<AD>,
O,
string
>;
// See interface description
readonly publishers: IMapBasedMergeData<string>;
readonly publishers: IMapBasedMergeData<
O,
IPubSubEmitterOptions<AD>,
O,
string
>;
protected _comparePatternAndPath: TcomparePatternAndPathFunc;

View File

@ -4,7 +4,10 @@
*/
import { INopeStatusInfo } from "./nopeConnectivityManager.interface";
import { INopeModuleDescription } from "./nopeModule.interface";
import {
IFunctionOptions,
INopeModuleDescription,
} from "./nopeModule.interface";
import { INopeObservable } from "./nopeObservable.interface";
import { IIncrementalChange } from "./nopePubSub.interface";
@ -203,7 +206,7 @@ export interface IAvailableServicesMsg {
*
* @type {string[]}
*/
services: string[];
services: IFunctionOptions[];
}
export type ITaskCancelationMsg = {

View File

@ -239,15 +239,20 @@ export interface INopeConnectivityManager {
* get the latest changes. Use the "data"
* field, to subscribe for the latest data.
*
* OriginalKey = Dispatcher ID;
* OriginalValue = INopeStatusInfo;
* ExtractedKey = Dispatcher ID;
* ExtractedValue = Dispatcher ID;
*
* @author M.Karkowski
* @type {IMapBasedMergeData<
* INopeStatusInfo,
* string,
* INopeStatusInfo
* >}
* @memberof INopeStatusManager
*/
dispatchers: IMapBasedMergeData<string, string, INopeStatusInfo>;
dispatchers: IMapBasedMergeData<
string, // Dispatcher ID
INopeStatusInfo, // Orginal Message
string, // Dispatcher ID
string // Dispatcher ID
>;
/**
* Options of the StatusManager.

View File

@ -40,18 +40,55 @@ export interface IMergeData<T = any, K = any> {
dispose(): void;
}
export interface IMapBasedMergeData<T = any, K = any, V = any>
extends IMergeData<T, Map<K, V>> {
export interface IMapBasedMergeData<
OriginalKey,
OriginalValue,
ExtractedKey = OriginalKey,
ExtractedValue = OriginalValue
> extends IMergeData<ExtractedValue, Map<OriginalKey, OriginalValue>> {
/**
* Adds a dinfition of the Amounts, of the elements.
*/
amountOf: Map<T, number>;
amountOf: Map<ExtractedKey, number>;
/**
* Simplifed Data Access.
*/
simplified: Map<K, T>;
simplified: Map<ExtractedKey, ExtractedValue>;
/**
* A Reverse Mapping
* Contains the Mapping of the original Key to the Extracted Key.
* key = OriginalKey;
* value = Set<ExtractedKey>;
*/
reverseSimplified: Map<T, Set<K>>;
keyMapping: Map<OriginalKey, Set<ExtractedKey>>;
/**
* Contains the Mapping of the `extracted Key` to the `original Key`.
* key = ExtractedKey;
* value = OriginalKey;
*/
keyMappingReverse: Map<ExtractedKey, Set<OriginalKey>>;
/**
* Contains conflicts.
* key = ExtractedKey;
* value = All determined different Values.
*/
conflicts: Map<ExtractedKey, Set<ExtractedValue>>;
/**
* Maps the Original Key to the Extracted value;
*/
orgKeyToExtractedValue: Map<OriginalKey, Set<ExtractedValue>>;
/**
* Contains the extracted Keys as Array.
*/
extractedKey: ExtractedKey[];
/**
* Contains the extracted Values.
*/
extractedValue: ExtractedValue[];
}

View File

@ -71,25 +71,28 @@ export interface INopeInstanceManager {
*/
readonly constructors: IMapBasedMergeData<
string, // Dispatcher ID
string[], // Available Generators of the Dispatcher
string, // Dispatcher ID
string[] // Available Instances
string // Generators-ID
>;
/**
* Overview of the available instances in the network.
*
* OriginalKey = DispatcherID (string);
* OriginalValue = Available Instance Messages (IAvailableInstancesMsg);
* ExtractedKey = The name of the Instance (string);
* ExtractedValue = instance-description (INopeModuleDescription);
*
*
* @author M.Karkowski
* @type {IMapBasedMergeData<
* INopeModuleDescription,
* string,
* IAvailableInstancesMsg
* >}
* @memberof INopeInstanceManager
*/
readonly instances: IMapBasedMergeData<
INopeModuleDescription,
string,
IAvailableInstancesMsg
string, // Dispatcher ID
IAvailableInstancesMsg, // Available Instance Messages
string, // The name of the Instance?
INopeModuleDescription // The instance-description
>;
/**

View File

@ -4,7 +4,7 @@
* @desc Defintion of a generic Module.
*/
import { TRenderConfigureServicePage } from "../ui";
import { TGetPorts, TRenderConfigureServicePage } from "../ui";
import { ICallOptions } from "./nopeCommunication.interface";
import { INopeDescriptor } from "./nopeDescriptor.interface";
import { INopeObservable } from "./nopeObservable.interface";
@ -299,6 +299,7 @@ export interface IFunctionOptions<T = any> extends Partial<ICallOptions> {
*/
ui?: {
serviceConfiguration?: TRenderConfigureServicePage<T>;
getPorts?: TGetPorts<T>;
};
/**

View File

@ -89,6 +89,18 @@ export interface IIncrementalChange extends IEventAdditionalData {
data: unknown;
}
export interface IPubSubEmitterOptions<
AD extends ITopicSetContentOptions & {
pubSubUpdate?: boolean;
} = ITopicSetContentOptions
> {
options: IEventOptions;
subTopic: string | false;
pubTopic: string | false;
callback?: IEventCallback<unknown, AD>;
observer?: INopeObserver;
}
/**
* The default Publish and Subscribe System. The Behavior may differ based on the settings.
* Your are not able to change these options, after the instance has been created.
@ -209,7 +221,12 @@ export interface IPubSubSystem<
* @type {IMapBasedMergeData<string>}
* @memberof IPubSubSystem
*/
readonly subscriptions: IMapBasedMergeData<string>;
readonly subscriptions: IMapBasedMergeData<
O,
IPubSubEmitterOptions<AD>,
O,
string
>;
/**
* List containing all publishers.
@ -218,7 +235,12 @@ export interface IPubSubSystem<
* @type {IMapBasedMergeData<string>}
* @memberof IPubSubSystem
*/
readonly publishers: IMapBasedMergeData<string>;
readonly publishers: IMapBasedMergeData<
O,
IPubSubEmitterOptions<AD>,
O,
string
>;
/**
* Disposes the Pub-Sub-System.

View File

@ -69,7 +69,9 @@ export interface IRequestTaskWithCallback extends IRequestRpcMsg {
* @export
* @interface INopeRpcManager
*/
export interface INopeRpcManager {
export interface INopeRpcManager<
T extends IFunctionOptions = IFunctionOptions
> {
/**
* Flag, to show, that the System is ready
*
@ -109,14 +111,21 @@ export interface INopeRpcManager {
/**
* Element showing the available services.
*
* T = services name.
* K = dispatcher - ids
* OriginalKey = Dispatcher ID (string);
* OriginalValue = Original Message (IAvailableServicesMsg);
* ExtractedKey = Function ID (string);
* ExtractedValue = FunctionOptions (T);
*
* @author M.Karkowski
* @type {IMapBasedMergeData<string>}
* @type {IMapBasedMergeData<T>}
* @memberof INopeRpcManager
*/
services: IMapBasedMergeData<string, string, IAvailableServicesMsg>;
services: IMapBasedMergeData<
string, // Dispatcher ID
IAvailableServicesMsg, // Original Message
string, // Function ID
T // Function Options
>;
/**
* Function, that must be called if a dispatcher is is gone. This might be the

View File

@ -6,6 +6,24 @@
import { TRenderFunctionResult } from "../layout.interface";
import { IPort } from "./INodes";
/**
* Type to define the Ports of a UI:
*/
export type TServiceGetPortsReturn = {
inputs: {
id: string;
label: String;
type: IPort["type"];
}[];
outputs: {
id: string;
label: String;
type: IPort["type"];
}[];
};
export type TGetPorts<T = any> = (data?: T) => TServiceGetPortsReturn;
export interface IServiceEditPage<T = any> extends TRenderFunctionResult {
/**
* Function, which must return the current service-data.
@ -32,19 +50,7 @@ export interface IServiceEditPage<T = any> extends TRenderFunctionResult {
type: "node" | "edge";
/**
* Function to store the changed Edge data.
* @param options adapted Edge Options.
* Helper to define the Ports.
*/
getPorts(): {
inputs: {
id: string;
label: String;
type: IPort["type"];
}[];
outputs: {
id: string;
label: String;
type: IPort["type"];
}[];
};
getPorts?: TGetPorts;
}