feat(collections): add new collection APIs, iterator support, and tree serialization utilities

This commit is contained in:
2026-03-22 08:44:49 +00:00
parent 20182a00f8
commit f4db131ede
23 changed files with 2251 additions and 2657 deletions

View File

@@ -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.'
}

View File

@@ -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
*/

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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]();
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}

View File

@@ -68,4 +68,11 @@ export class TimedAggregtor<T> {
this.storageArray = [];
}
}
public restart(): void {
this.isStopped = false;
}
}
// correctly-spelled alias
export { TimedAggregtor as TimedAggregator };

View File

@@ -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>[];
}

View File

@@ -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';