feat(collections): add new collection APIs, iterator support, and tree serialization utilities
This commit is contained in:
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@push.rocks/lik',
|
||||
version: '6.3.1',
|
||||
version: '6.4.0',
|
||||
description: 'Provides a collection of lightweight helpers and utilities for Node.js projects.'
|
||||
}
|
||||
|
||||
@@ -12,6 +12,10 @@ export class BackpressuredArray<T> {
|
||||
this.highWaterMark = highWaterMark;
|
||||
}
|
||||
|
||||
public get length(): number {
|
||||
return this.data.length;
|
||||
}
|
||||
|
||||
push(item: T): boolean {
|
||||
this.data.push(item);
|
||||
this.itemsAvailable.next('itemsAvailable');
|
||||
@@ -23,6 +27,13 @@ export class BackpressuredArray<T> {
|
||||
return spaceAvailable;
|
||||
}
|
||||
|
||||
pushMany(items: T[]): boolean {
|
||||
for (const item of items) {
|
||||
this.push(item);
|
||||
}
|
||||
return this.checkSpaceAvailable();
|
||||
}
|
||||
|
||||
shift(): T | undefined {
|
||||
const item = this.data.shift();
|
||||
if (this.checkSpaceAvailable()) {
|
||||
@@ -31,6 +42,10 @@ export class BackpressuredArray<T> {
|
||||
return item;
|
||||
}
|
||||
|
||||
peek(): T | undefined {
|
||||
return this.data[0];
|
||||
}
|
||||
|
||||
checkSpaceAvailable(): boolean {
|
||||
return this.data.length < this.highWaterMark;
|
||||
}
|
||||
@@ -75,6 +90,10 @@ export class BackpressuredArray<T> {
|
||||
});
|
||||
}
|
||||
|
||||
public [Symbol.iterator](): Iterator<T> {
|
||||
return this.data[Symbol.iterator]();
|
||||
}
|
||||
|
||||
/**
|
||||
* destroys the BackpressuredArray, completing all subjects
|
||||
*/
|
||||
|
||||
@@ -9,10 +9,18 @@ import * as plugins from './classes.plugins.js';
|
||||
* fast map allows for very quick lookups of objects with a unique key
|
||||
*/
|
||||
export class FastMap<T> {
|
||||
private mapObject: { [key: string]: T } = {};
|
||||
private mapObject = new Map<string, T>();
|
||||
|
||||
public isUniqueKey(keyArg: string): boolean {
|
||||
return this.mapObject[keyArg] ? false : true;
|
||||
return !this.mapObject.has(keyArg);
|
||||
}
|
||||
|
||||
public has(keyArg: string): boolean {
|
||||
return this.mapObject.has(keyArg);
|
||||
}
|
||||
|
||||
public get size(): number {
|
||||
return this.mapObject.size;
|
||||
}
|
||||
|
||||
public addToMap(
|
||||
@@ -23,35 +31,37 @@ export class FastMap<T> {
|
||||
}
|
||||
): boolean {
|
||||
if (this.isUniqueKey(keyArg) || (optionsArg && optionsArg.force)) {
|
||||
this.mapObject[keyArg] = objectArg;
|
||||
this.mapObject.set(keyArg, objectArg);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public getByKey(keyArg: string) {
|
||||
return this.mapObject[keyArg];
|
||||
public getByKey(keyArg: string): T | undefined {
|
||||
return this.mapObject.get(keyArg);
|
||||
}
|
||||
|
||||
public removeFromMap(keyArg: string): T {
|
||||
const removedItem = this.getByKey(keyArg);
|
||||
delete this.mapObject[keyArg];
|
||||
const removedItem = this.mapObject.get(keyArg);
|
||||
this.mapObject.delete(keyArg);
|
||||
return removedItem;
|
||||
}
|
||||
|
||||
public getKeys() {
|
||||
const keys: string[] = [];
|
||||
for (const keyArg in this.mapObject) {
|
||||
if (this.mapObject[keyArg]) {
|
||||
keys.push(keyArg);
|
||||
}
|
||||
}
|
||||
return keys;
|
||||
public getKeys(): string[] {
|
||||
return Array.from(this.mapObject.keys());
|
||||
}
|
||||
|
||||
public values(): T[] {
|
||||
return Array.from(this.mapObject.values());
|
||||
}
|
||||
|
||||
public entries(): [string, T][] {
|
||||
return Array.from(this.mapObject.entries());
|
||||
}
|
||||
|
||||
public clean() {
|
||||
this.mapObject = {};
|
||||
this.mapObject.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -94,4 +104,8 @@ export class FastMap<T> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public [Symbol.iterator](): Iterator<[string, T]> {
|
||||
return this.mapObject.entries();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,11 @@ export class InterestMap<DTInterestId, DTInterestFullfillment> {
|
||||
*/
|
||||
private interestObjectMap = new ObjectMap<Interest<DTInterestId, DTInterestFullfillment>>();
|
||||
|
||||
/**
|
||||
* O(1) lookup of interests by their comparison string
|
||||
*/
|
||||
private interestsByComparisonString = new Map<string, Interest<DTInterestId, DTInterestFullfillment>>();
|
||||
|
||||
/**
|
||||
* a function to compare interests
|
||||
*/
|
||||
@@ -49,29 +54,23 @@ export class InterestMap<DTInterestId, DTInterestFullfillment> {
|
||||
): Promise<Interest<DTInterestId, DTInterestFullfillment>> {
|
||||
const comparisonString = this.comparisonFunc(interestId);
|
||||
let returnInterest: Interest<DTInterestId, DTInterestFullfillment>;
|
||||
const newInterest = new Interest<DTInterestId, DTInterestFullfillment>(
|
||||
this,
|
||||
interestId,
|
||||
this.comparisonFunc,
|
||||
{
|
||||
markLostAfterDefault: this.options.markLostAfterDefault,
|
||||
defaultFullfillment: defaultFullfillmentArg,
|
||||
}
|
||||
);
|
||||
let interestExists = false;
|
||||
await this.interestObjectMap.forEach((interestArg) => {
|
||||
if (!interestExists && interestArg.comparisonString === newInterest.comparisonString) {
|
||||
console.log('info', `interest already exists for ${newInterest.comparisonString}`);
|
||||
interestExists = true;
|
||||
returnInterest = interestArg;
|
||||
returnInterest.renew();
|
||||
}
|
||||
});
|
||||
if (!returnInterest) {
|
||||
returnInterest = newInterest;
|
||||
this.interestObjectMap.add(returnInterest);
|
||||
|
||||
const existingInterest = this.interestsByComparisonString.get(comparisonString);
|
||||
if (existingInterest) {
|
||||
returnInterest = existingInterest;
|
||||
returnInterest.renew();
|
||||
} else {
|
||||
newInterest.destroy(); // clean up abandoned Interest's timers
|
||||
returnInterest = new Interest<DTInterestId, DTInterestFullfillment>(
|
||||
this,
|
||||
interestId,
|
||||
this.comparisonFunc,
|
||||
{
|
||||
markLostAfterDefault: this.options.markLostAfterDefault,
|
||||
defaultFullfillment: defaultFullfillmentArg,
|
||||
}
|
||||
);
|
||||
this.interestObjectMap.add(returnInterest);
|
||||
this.interestsByComparisonString.set(comparisonString, returnInterest);
|
||||
}
|
||||
this.interestObservable.push(returnInterest);
|
||||
return returnInterest;
|
||||
@@ -83,9 +82,10 @@ export class InterestMap<DTInterestId, DTInterestFullfillment> {
|
||||
* removes an interest from the interest map
|
||||
*/
|
||||
public removeInterest(interestArg: Interest<DTInterestId, DTInterestFullfillment>) {
|
||||
const interestToRemove = this.interestObjectMap.findOneAndRemoveSync((interestArg2) => {
|
||||
this.interestObjectMap.findOneAndRemoveSync((interestArg2) => {
|
||||
return interestArg.comparisonString === interestArg2.comparisonString;
|
||||
});
|
||||
this.interestsByComparisonString.delete(interestArg.comparisonString);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -101,14 +101,7 @@ export class InterestMap<DTInterestId, DTInterestFullfillment> {
|
||||
* @param comparisonStringArg
|
||||
*/
|
||||
public checkInterestByString(comparisonStringArg: string): boolean {
|
||||
const foundInterest = this.interestObjectMap.findSync((interest) => {
|
||||
return interest.comparisonString === comparisonStringArg;
|
||||
});
|
||||
if (foundInterest) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
return this.interestsByComparisonString.has(comparisonStringArg);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -128,10 +121,7 @@ export class InterestMap<DTInterestId, DTInterestFullfillment> {
|
||||
*/
|
||||
public findInterest(interestId: DTInterestId): Interest<DTInterestId, DTInterestFullfillment> {
|
||||
const comparableString = this.comparisonFunc(interestId);
|
||||
const interest = this.interestObjectMap.findSync((interestArg) => {
|
||||
return interestArg.comparisonString === comparableString;
|
||||
});
|
||||
return interest; // if an interest is found, the interest is returned, otherwise interest is null
|
||||
return this.interestsByComparisonString.get(comparableString) ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -143,6 +133,7 @@ export class InterestMap<DTInterestId, DTInterestFullfillment> {
|
||||
interest.destroy();
|
||||
}
|
||||
this.interestObjectMap.wipe();
|
||||
this.interestsByComparisonString.clear();
|
||||
this.interestObservable.signalComplete();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,10 @@ export class LimitedArray<T> {
|
||||
this.arrayLimit = limitArg;
|
||||
}
|
||||
|
||||
public get length(): number {
|
||||
return this.array.length;
|
||||
}
|
||||
|
||||
addOne(objectArg: T) {
|
||||
this.array.unshift(objectArg);
|
||||
if (this.array.length > this.arrayLimit) {
|
||||
@@ -28,6 +32,9 @@ export class LimitedArray<T> {
|
||||
}
|
||||
|
||||
getAverage(): number {
|
||||
if (this.array.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
if (typeof this.array[0] === 'number') {
|
||||
let sum = 0;
|
||||
for (let localNumber of this.array) {
|
||||
@@ -39,4 +46,25 @@ export class LimitedArray<T> {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
remove(item: T): boolean {
|
||||
const idx = this.array.indexOf(item);
|
||||
if (idx !== -1) {
|
||||
this.array.splice(idx, 1);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.array.length = 0;
|
||||
}
|
||||
|
||||
getArray(): T[] {
|
||||
return [...this.array];
|
||||
}
|
||||
|
||||
public [Symbol.iterator](): Iterator<T> {
|
||||
return this.array[Symbol.iterator]();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ export interface IObjectMapEventData<T> {
|
||||
*/
|
||||
export class ObjectMap<T> {
|
||||
private fastMap = new FastMap<T>();
|
||||
private reverseMap = new Map<T, string>();
|
||||
|
||||
// events
|
||||
public eventSubject = new plugins.smartrx.rxjs.Subject<IObjectMapEventData<T>>();
|
||||
@@ -42,12 +43,20 @@ export class ObjectMap<T> {
|
||||
// nothing here
|
||||
}
|
||||
|
||||
/**
|
||||
* the number of objects in the map
|
||||
*/
|
||||
public get length(): number {
|
||||
return this.fastMap.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* adds an object mapped to a string
|
||||
* the string must be unique
|
||||
*/
|
||||
addMappedUnique(uniqueKeyArg: string, objectArg: T) {
|
||||
this.fastMap.addToMap(uniqueKeyArg, objectArg);
|
||||
this.reverseMap.set(objectArg, uniqueKeyArg);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -65,6 +74,7 @@ export class ObjectMap<T> {
|
||||
public removeMappedUnique(uniqueKey: string): T {
|
||||
const object = this.fastMap.removeFromMap(uniqueKey);
|
||||
if (object !== undefined) {
|
||||
this.reverseMap.delete(object);
|
||||
this.eventSubject.next({
|
||||
operation: 'remove',
|
||||
payload: object,
|
||||
@@ -75,19 +85,14 @@ export class ObjectMap<T> {
|
||||
|
||||
/**
|
||||
* add object to Objectmap
|
||||
* returns false if the object is already in the map
|
||||
* returns true if the object was added successfully
|
||||
* returns the key for the object (existing or new)
|
||||
*/
|
||||
public add(objectArg: T): string {
|
||||
// lets search for an existing unique key
|
||||
for (const keyArg of this.fastMap.getKeys()) {
|
||||
const object = this.fastMap.getByKey(keyArg);
|
||||
if (object === objectArg) {
|
||||
return keyArg;
|
||||
}
|
||||
const existingKey = this.reverseMap.get(objectArg);
|
||||
if (existingKey !== undefined) {
|
||||
return existingKey;
|
||||
}
|
||||
|
||||
// otherwise lets create it
|
||||
const uniqueKey = uni('key');
|
||||
this.addMappedUnique(uniqueKey, objectArg);
|
||||
this.eventSubject.next({
|
||||
@@ -110,23 +115,14 @@ export class ObjectMap<T> {
|
||||
* check if object is in Objectmap
|
||||
*/
|
||||
public checkForObject(objectArg: T): boolean {
|
||||
return !!this.getKeyForObject(objectArg);
|
||||
return this.reverseMap.has(objectArg);
|
||||
}
|
||||
|
||||
/**
|
||||
* get key for object
|
||||
* @param findFunction
|
||||
*/
|
||||
public getKeyForObject(objectArg: T) {
|
||||
let foundKey: string = null;
|
||||
for (const keyArg of this.fastMap.getKeys()) {
|
||||
if (!foundKey && this.fastMap.getByKey(keyArg) === objectArg) {
|
||||
foundKey = keyArg;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return foundKey;
|
||||
public getKeyForObject(objectArg: T): string | null {
|
||||
return this.reverseMap.get(objectArg) ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -181,6 +177,7 @@ export class ObjectMap<T> {
|
||||
} else {
|
||||
const keyToUse = keys[0];
|
||||
const removedItem = this.fastMap.removeFromMap(keyToUse);
|
||||
this.reverseMap.delete(removedItem);
|
||||
this.eventSubject.next({
|
||||
operation: 'remove',
|
||||
payload: removedItem,
|
||||
@@ -193,27 +190,24 @@ export class ObjectMap<T> {
|
||||
* returns a cloned array of all the objects currently in the Objectmap
|
||||
*/
|
||||
public getArray(): T[] {
|
||||
const returnArray: any[] = [];
|
||||
for (const keyArg of this.fastMap.getKeys()) {
|
||||
returnArray.push(this.fastMap.getByKey(keyArg));
|
||||
}
|
||||
return returnArray;
|
||||
return this.fastMap.values();
|
||||
}
|
||||
|
||||
/**
|
||||
* check if Objectmap ist empty
|
||||
*/
|
||||
public isEmpty(): boolean {
|
||||
return this.fastMap.getKeys().length === 0;
|
||||
return this.fastMap.size === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* remove object from Objectmap
|
||||
*/
|
||||
public remove(objectArg: T): T {
|
||||
if (this.checkForObject(objectArg)) {
|
||||
const keyArg = this.getKeyForObject(objectArg);
|
||||
const keyArg = this.reverseMap.get(objectArg);
|
||||
if (keyArg !== undefined) {
|
||||
const removedObject = this.fastMap.removeFromMap(keyArg);
|
||||
this.reverseMap.delete(removedObject);
|
||||
this.eventSubject.next({
|
||||
operation: 'remove',
|
||||
payload: removedObject,
|
||||
@@ -230,6 +224,7 @@ export class ObjectMap<T> {
|
||||
const keys = this.fastMap.getKeys();
|
||||
for (const keyArg of keys) {
|
||||
const removedObject = this.fastMap.removeFromMap(keyArg);
|
||||
this.reverseMap.delete(removedObject);
|
||||
this.eventSubject.next({
|
||||
operation: 'remove',
|
||||
payload: removedObject,
|
||||
@@ -244,6 +239,10 @@ export class ObjectMap<T> {
|
||||
const concattedObjectMap = new ObjectMap<T>();
|
||||
concattedObjectMap.fastMap.addAllFromOther(this.fastMap);
|
||||
concattedObjectMap.fastMap.addAllFromOther(objectMapArg.fastMap);
|
||||
// rebuild reverse map for the concatenated map
|
||||
for (const key of concattedObjectMap.fastMap.getKeys()) {
|
||||
concattedObjectMap.reverseMap.set(concattedObjectMap.fastMap.getByKey(key), key);
|
||||
}
|
||||
return concattedObjectMap;
|
||||
}
|
||||
|
||||
@@ -254,6 +253,26 @@ export class ObjectMap<T> {
|
||||
*/
|
||||
public addAllFromOther(objectMapArg: ObjectMap<T>) {
|
||||
this.fastMap.addAllFromOther(objectMapArg.fastMap);
|
||||
// rebuild reverse map
|
||||
for (const key of objectMapArg.fastMap.getKeys()) {
|
||||
this.reverseMap.set(objectMapArg.fastMap.getByKey(key), key);
|
||||
}
|
||||
}
|
||||
|
||||
public map<U>(fn: (item: T) => U): U[] {
|
||||
return this.getArray().map(fn);
|
||||
}
|
||||
|
||||
public filter(fn: (item: T) => boolean): T[] {
|
||||
return this.getArray().filter(fn);
|
||||
}
|
||||
|
||||
public reduce<U>(fn: (acc: U, item: T) => U, initial: U): U {
|
||||
return this.getArray().reduce(fn, initial);
|
||||
}
|
||||
|
||||
public [Symbol.iterator](): Iterator<T> {
|
||||
return this.getArray()[Symbol.iterator]();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -261,6 +280,7 @@ export class ObjectMap<T> {
|
||||
*/
|
||||
public destroy() {
|
||||
this.wipe();
|
||||
this.reverseMap.clear();
|
||||
this.eventSubject.complete();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,11 +31,7 @@ export class Stringmap {
|
||||
* removes a string from Stringmap
|
||||
*/
|
||||
removeString(stringArg: string) {
|
||||
for (const keyArg in this._stringArray) {
|
||||
if (this._stringArray[keyArg] === stringArg) {
|
||||
this._stringArray.splice(parseInt(keyArg), 1);
|
||||
}
|
||||
}
|
||||
this._stringArray = this._stringArray.filter(s => s !== stringArg);
|
||||
this.notifyTrigger();
|
||||
}
|
||||
|
||||
|
||||
@@ -68,4 +68,11 @@ export class TimedAggregtor<T> {
|
||||
this.storageArray = [];
|
||||
}
|
||||
}
|
||||
|
||||
public restart(): void {
|
||||
this.isStopped = false;
|
||||
}
|
||||
}
|
||||
|
||||
// correctly-spelled alias
|
||||
export { TimedAggregtor as TimedAggregator };
|
||||
|
||||
@@ -75,15 +75,15 @@ export class Tree<T> {
|
||||
}
|
||||
|
||||
nextSiblingsIterator(objectArg: T) {
|
||||
return this.symbolTree.nextSiblingsIterator();
|
||||
return this.symbolTree.nextSiblingsIterator(objectArg);
|
||||
}
|
||||
|
||||
ancestorsIterator(objectArg: T) {
|
||||
this.symbolTree.ancestorsIterator();
|
||||
ancestorsIterator(objectArg: T): Iterable<T> {
|
||||
return this.symbolTree.ancestorsIterator(objectArg);
|
||||
}
|
||||
|
||||
treeIterator(rootArg: T, optionsArg: any): Iterable<T> {
|
||||
return this.symbolTree.treeIterator(rootArg);
|
||||
treeIterator(rootArg: T, optionsArg?: any): Iterable<T> {
|
||||
return this.symbolTree.treeIterator(rootArg, optionsArg);
|
||||
}
|
||||
|
||||
index(childArg: T): number {
|
||||
@@ -119,23 +119,48 @@ export class Tree<T> {
|
||||
}
|
||||
|
||||
// ===========================================
|
||||
// Functionionality that extends symbol-tree
|
||||
// Functionality that extends symbol-tree
|
||||
// ===========================================
|
||||
|
||||
/**
|
||||
* returns a branch of the tree as JSON
|
||||
* can be user
|
||||
* returns a branch of the tree as a recursive JSON structure
|
||||
*/
|
||||
toJsonWithHierachy(rootElement: T) {
|
||||
const treeIterable = this.treeIterator(rootElement, {});
|
||||
for (const treeItem of treeIterable) {
|
||||
console.log(treeItem);
|
||||
}
|
||||
toJsonWithHierachy(rootElement: T): ITreeNode<T> {
|
||||
const buildNode = (element: T): ITreeNode<T> => {
|
||||
const children: ITreeNode<T>[] = [];
|
||||
if (this.hasChildren(element)) {
|
||||
const childrenArray = this.childrenToArray(element, {});
|
||||
for (const child of childrenArray) {
|
||||
children.push(buildNode(child));
|
||||
}
|
||||
}
|
||||
return { data: element, children };
|
||||
};
|
||||
return buildNode(rootElement);
|
||||
}
|
||||
|
||||
/**
|
||||
* builds a tree from a JSON with hierachy
|
||||
* @param rootElement
|
||||
* builds a tree from a recursive JSON structure
|
||||
* @param jsonRoot the root node in ITreeNode format
|
||||
* @param reviver optional function to reconstruct T from serialized data
|
||||
*/
|
||||
fromJsonWithHierachy(rootElement: T) {}
|
||||
fromJsonWithHierachy(jsonRoot: ITreeNode<T>, reviver?: (data: any) => T): T {
|
||||
const buildTree = (node: ITreeNode<T>, parentElement?: T): T => {
|
||||
const element = reviver ? reviver(node.data) : node.data;
|
||||
this.initialize(element);
|
||||
if (parentElement) {
|
||||
this.appendChild(parentElement, element);
|
||||
}
|
||||
for (const childNode of node.children) {
|
||||
buildTree(childNode, element);
|
||||
}
|
||||
return element;
|
||||
};
|
||||
return buildTree(jsonRoot);
|
||||
}
|
||||
}
|
||||
|
||||
export interface ITreeNode<T> {
|
||||
data: T;
|
||||
children: ITreeNode<T>[];
|
||||
}
|
||||
|
||||
@@ -8,4 +8,5 @@ export * from './classes.looptracker.js';
|
||||
export * from './classes.objectmap.js';
|
||||
export * from './classes.stringmap.js';
|
||||
export * from './classes.timedaggregator.js';
|
||||
export { TimedAggregator } from './classes.timedaggregator.js';
|
||||
export * from './classes.tree.js';
|
||||
|
||||
Reference in New Issue
Block a user