feat(collections): add new collection APIs, iterator support, and tree serialization utilities
This commit is contained in:
@@ -1,5 +1,14 @@
|
||||
# Changelog
|
||||
|
||||
## 2026-03-22 - 6.4.0 - feat(collections)
|
||||
add new collection APIs, iterator support, and tree serialization utilities
|
||||
|
||||
- adds new convenience methods and properties across BackpressuredArray, FastMap, LimitedArray, ObjectMap, and Tree, including length/size accessors, iterators, batch operations, and utility helpers
|
||||
- improves lookup performance by replacing object scans with Map-backed indexing in FastMap, ObjectMap, and InterestMap
|
||||
- adds TimedAggregator restart support and exports a correctly spelled TimedAggregator alias
|
||||
- fixes Tree iterator methods and implements JSON hierarchy serialization/deserialization
|
||||
- expands test coverage substantially for collection classes and related utilities
|
||||
|
||||
## 2026-03-01 - 6.3.1 - fix(classes)
|
||||
cleanup resources, add cancellable timeouts, and fix bugs in several core utility classes
|
||||
|
||||
|
||||
13
package.json
13
package.json
@@ -22,21 +22,20 @@
|
||||
},
|
||||
"homepage": "https://code.foss.global/push.rocks/lik",
|
||||
"devDependencies": {
|
||||
"@git.zone/tsbuild": "^4.1.2",
|
||||
"@git.zone/tsbundle": "^2.9.0",
|
||||
"@git.zone/tsbuild": "^4.3.0",
|
||||
"@git.zone/tsbundle": "^2.9.1",
|
||||
"@git.zone/tsrun": "^2.0.1",
|
||||
"@git.zone/tstest": "^3.1.8",
|
||||
"@git.zone/tstest": "^3.5.0",
|
||||
"@push.rocks/tapbundle": "^6.0.3",
|
||||
"@types/node": "^25.3.3"
|
||||
"@types/node": "^25.5.0",
|
||||
"@types/symbol-tree": "^3.2.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@push.rocks/smartdelay": "^3.0.5",
|
||||
"@push.rocks/smartmatch": "^2.0.0",
|
||||
"@push.rocks/smartpromise": "^4.0.3",
|
||||
"@push.rocks/smartpromise": "^4.2.3",
|
||||
"@push.rocks/smartrx": "^3.0.10",
|
||||
"@push.rocks/smarttime": "^4.2.3",
|
||||
"@types/minimatch": "^5.1.2",
|
||||
"@types/symbol-tree": "^3.2.5",
|
||||
"symbol-tree": "^3.2.4"
|
||||
},
|
||||
"files": [
|
||||
|
||||
3489
pnpm-lock.yaml
generated
3489
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
399
readme.md
399
readme.md
@@ -1,188 +1,241 @@
|
||||
# @push.rocks/lik
|
||||
|
||||
A collection of lightweight utility classes for TypeScript/Node.js projects, providing efficient data structures and async helpers.
|
||||
⚡ A lean, fully-typed collection of utility classes for TypeScript — efficient data structures, async execution control, and reactive helpers that work seamlessly in both Node.js and the browser.
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
# pnpm (recommended)
|
||||
pnpm install @push.rocks/lik
|
||||
|
||||
# npm
|
||||
npm install @push.rocks/lik
|
||||
```
|
||||
|
||||
This is a pure ESM package. Use `import` syntax in your TypeScript/JavaScript projects.
|
||||
|
||||
## Issue Reporting and Security
|
||||
|
||||
For reporting bugs, issues, or security vulnerabilities, please visit [community.foss.global/](https://community.foss.global/). This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a [code.foss.global/](https://code.foss.global/) account to submit Pull Requests directly.
|
||||
|
||||
## Usage
|
||||
|
||||
`@push.rocks/lik` provides a set of focused helper classes for common programming tasks: managing collections, controlling async execution, tracking interests, and more. All classes are fully typed and work in both Node.js and browser environments.
|
||||
`@push.rocks/lik` gives you **10 focused utility classes** — each solving one problem well. No bloat, no deep dependency trees, just clean tools for everyday TypeScript.
|
||||
|
||||
### AsyncExecutionStack
|
||||
---
|
||||
|
||||
Controls execution of asynchronous tasks in two modes:
|
||||
### 🔒 AsyncExecutionStack
|
||||
|
||||
- **Exclusive**: tasks run one at a time, blocking all others until complete.
|
||||
- **Non-exclusive**: tasks run in parallel with optional concurrency limits.
|
||||
Control async task execution with **exclusive** (sequential, mutex-like) and **non-exclusive** (parallel with concurrency limits) modes. Think of it as a lightweight task scheduler.
|
||||
|
||||
```typescript
|
||||
import { AsyncExecutionStack } from '@push.rocks/lik';
|
||||
|
||||
const stack = new AsyncExecutionStack();
|
||||
|
||||
// Exclusive execution (sequential, blocks other tasks)
|
||||
await stack.getExclusiveExecutionSlot(async () => {
|
||||
// critical section work
|
||||
// 🔐 Exclusive: only one task runs at a time (mutex-style)
|
||||
const result = await stack.getExclusiveExecutionSlot(async () => {
|
||||
// critical section — no other task runs until this resolves
|
||||
return await doSomethingImportant();
|
||||
}, 5000); // optional timeout in ms
|
||||
|
||||
// Non-exclusive execution (parallel)
|
||||
const p1 = stack.getNonExclusiveExecutionSlot(async () => {
|
||||
// concurrent work 1
|
||||
});
|
||||
const p2 = stack.getNonExclusiveExecutionSlot(async () => {
|
||||
// concurrent work 2
|
||||
});
|
||||
await Promise.all([p1, p2]);
|
||||
// 🚀 Non-exclusive: tasks run in parallel
|
||||
const p1 = stack.getNonExclusiveExecutionSlot(async () => fetchUser(1));
|
||||
const p2 = stack.getNonExclusiveExecutionSlot(async () => fetchUser(2));
|
||||
const p3 = stack.getNonExclusiveExecutionSlot(async () => fetchUser(3));
|
||||
await Promise.all([p1, p2, p3]);
|
||||
|
||||
// Control concurrency
|
||||
stack.setNonExclusiveMaxConcurrency(3);
|
||||
console.log(stack.getNonExclusiveMaxConcurrency()); // 3
|
||||
console.log(stack.getActiveNonExclusiveCount()); // currently running
|
||||
console.log(stack.getPendingNonExclusiveCount()); // waiting for slots
|
||||
// 🎚️ Concurrency control for non-exclusive tasks
|
||||
stack.setNonExclusiveMaxConcurrency(3); // max 3 in parallel
|
||||
stack.getNonExclusiveMaxConcurrency(); // 3
|
||||
stack.getActiveNonExclusiveCount(); // how many are running right now
|
||||
stack.getPendingNonExclusiveCount(); // how many are waiting for a slot
|
||||
```
|
||||
|
||||
### BackpressuredArray
|
||||
**Key behavior:** Exclusive and non-exclusive slots are processed in order. When an exclusive slot comes up, it waits for all running non-exclusive tasks to finish, then runs alone. Non-exclusive tasks arriving together are batched and run in parallel (respecting the concurrency limit).
|
||||
|
||||
An array with backpressure support using RxJS subjects. Useful for producer/consumer patterns where you need to throttle the producer when the consumer can't keep up.
|
||||
---
|
||||
|
||||
### 🌊 BackpressuredArray
|
||||
|
||||
A bounded buffer with **backpressure** — perfect for producer/consumer patterns where you need to throttle the producer when the consumer falls behind. Uses RxJS subjects under the hood.
|
||||
|
||||
```typescript
|
||||
import { BackpressuredArray } from '@push.rocks/lik';
|
||||
|
||||
const buffer = new BackpressuredArray<string>(16); // high water mark of 16
|
||||
const buffer = new BackpressuredArray<string>(16); // high water mark
|
||||
|
||||
// Producer: push items, returns false when full
|
||||
// Producer side
|
||||
const hasSpace = buffer.push('item1');
|
||||
if (!hasSpace) {
|
||||
await buffer.waitForSpace(); // wait until consumer frees space
|
||||
await buffer.waitForSpace(); // blocks until consumer drains enough
|
||||
}
|
||||
|
||||
// Consumer: shift items out
|
||||
await buffer.waitForItems(); // wait until items are available
|
||||
const item = buffer.shift();
|
||||
// Consumer side
|
||||
await buffer.waitForItems(); // blocks until something is available
|
||||
const item = buffer.shift(); // grab the oldest item
|
||||
|
||||
// Check state
|
||||
buffer.checkSpaceAvailable(); // true if below high water mark
|
||||
buffer.checkHasItems(); // true if items exist
|
||||
// Introspect
|
||||
buffer.checkSpaceAvailable(); // true if below high water mark
|
||||
buffer.checkHasItems(); // true if items exist
|
||||
|
||||
// Teardown
|
||||
buffer.destroy(); // completes all internal subjects
|
||||
```
|
||||
|
||||
### FastMap
|
||||
---
|
||||
|
||||
A high-performance key-value map optimized for rapid lookups and modifications.
|
||||
### ⚡ FastMap
|
||||
|
||||
A high-performance string-keyed map. Faster than native `Map` for simple key-value lookups thanks to plain-object backing. Supports merging, concatenation, and async search.
|
||||
|
||||
```typescript
|
||||
import { FastMap } from '@push.rocks/lik';
|
||||
|
||||
const map = new FastMap<string>();
|
||||
const map = new FastMap<{ name: string; score: number }>();
|
||||
|
||||
map.addToMap('key1', 'value1');
|
||||
map.addToMap('key2', 'value2');
|
||||
// CRUD
|
||||
map.addToMap('player1', { name: 'Alice', score: 100 });
|
||||
map.addToMap('player2', { name: 'Bob', score: 85 });
|
||||
map.getByKey('player1'); // { name: 'Alice', score: 100 }
|
||||
map.isUniqueKey('player1'); // false (already exists)
|
||||
map.removeFromMap('player2'); // returns the removed object
|
||||
map.getKeys(); // ['player1']
|
||||
|
||||
const value = map.getByKey('key1'); // 'value1'
|
||||
map.isUniqueKey('key1'); // false (already exists)
|
||||
map.removeFromMap('key1');
|
||||
// Force overwrite existing key
|
||||
map.addToMap('player1', { name: 'Alice', score: 200 }, { force: true });
|
||||
|
||||
// Merge maps
|
||||
const otherMap = new FastMap<string>();
|
||||
otherMap.addToMap('key3', 'value3');
|
||||
const merged = map.concat(otherMap);
|
||||
|
||||
// Or merge in place
|
||||
map.addAllFromOther(otherMap);
|
||||
// Merge two maps
|
||||
const otherMap = new FastMap<{ name: string; score: number }>();
|
||||
otherMap.addToMap('player3', { name: 'Carol', score: 90 });
|
||||
const merged = map.concat(otherMap); // new FastMap with all entries
|
||||
map.addAllFromOther(otherMap); // merge in-place
|
||||
|
||||
// Async find
|
||||
const found = await map.find(async (item) => item === 'value2');
|
||||
const found = await map.find(async (item) => item.score > 95);
|
||||
|
||||
// Reset
|
||||
map.clean();
|
||||
```
|
||||
|
||||
### InterestMap
|
||||
---
|
||||
|
||||
Manages subscriptions/interests in events or entities. Multiple parties can express interest in the same thing; the interest is deduplicated and fulfilled once.
|
||||
### 🎯 InterestMap & Interest
|
||||
|
||||
A deduplicating interest/subscription tracker. Multiple callers can express interest in the same thing — the `InterestMap` deduplicates them and fulfills all waiters at once. Great for caching layers, resource pooling, or subscription fan-out.
|
||||
|
||||
```typescript
|
||||
import { InterestMap } from '@push.rocks/lik';
|
||||
|
||||
const interestMap = new InterestMap<string, number>(
|
||||
(str) => str, // comparison function to deduplicate interests
|
||||
{ markLostAfterDefault: 30000 } // optional: auto-mark lost after 30s
|
||||
const interestMap = new InterestMap<string, Response>(
|
||||
(id) => id, // comparison function for deduplication
|
||||
{ markLostAfterDefault: 30000 } // auto-destroy unfulfilled interests after 30s
|
||||
);
|
||||
|
||||
// Express interest
|
||||
const interest = await interestMap.addInterest('event1');
|
||||
// Express interest (returns existing Interest if one matches)
|
||||
const interest = await interestMap.addInterest('user:42');
|
||||
|
||||
// Wait for fulfillment
|
||||
interest.interestFullfilled.then((result) => {
|
||||
console.log('Got result:', result);
|
||||
// The interest is a promise-like object
|
||||
interest.interestFullfilled.then((response) => {
|
||||
console.log('Got it!', response);
|
||||
});
|
||||
|
||||
// Fulfill from elsewhere
|
||||
const found = interestMap.findInterest('event1');
|
||||
found.fullfillInterest(42);
|
||||
// Somewhere else — fulfill the interest for everyone waiting
|
||||
const found = interestMap.findInterest('user:42');
|
||||
found.fullfillInterest(apiResponse);
|
||||
|
||||
// Check and manage interests
|
||||
interestMap.checkInterest('event1'); // true/false
|
||||
interestMap.informLostInterest('event1'); // starts destruction timer
|
||||
// Provide a default fulfillment value (returned if interest is destroyed unfulfilled)
|
||||
const interest2 = await interestMap.addInterest('user:99', fallbackResponse);
|
||||
|
||||
// Observable stream of new interests
|
||||
interestMap.interestObservable; // ObservableIntake<Interest>
|
||||
// Check / manage
|
||||
interestMap.checkInterest('user:42'); // true if active
|
||||
interestMap.informLostInterest('user:42'); // starts the destruction timer
|
||||
|
||||
// Observable stream of all new interests as they arrive
|
||||
interestMap.interestObservable.subscribe((interest) => {
|
||||
console.log('New interest:', interest.comparisonString);
|
||||
});
|
||||
|
||||
// Clean up everything
|
||||
interestMap.destroy();
|
||||
```
|
||||
|
||||
### LimitedArray
|
||||
**Interest lifecycle:** Created → (optionally) `markLost()` starts a 10s destruction timer → `renew()` resets the timer → `fullfillInterest(value)` resolves all waiters → `destroy()` cleans up.
|
||||
|
||||
An array that automatically enforces a maximum size, discarding oldest items when the limit is exceeded.
|
||||
---
|
||||
|
||||
### 📏 LimitedArray
|
||||
|
||||
A fixed-capacity array that automatically discards the oldest items when the limit is exceeded. Ideal for rolling logs, recent-history buffers, or sliding-window metrics.
|
||||
|
||||
```typescript
|
||||
import { LimitedArray } from '@push.rocks/lik';
|
||||
|
||||
const arr = new LimitedArray<number>(5);
|
||||
const recentLogs = new LimitedArray<string>(100);
|
||||
|
||||
arr.addMany([1, 2, 3, 4, 5, 6]);
|
||||
console.log(arr.array.length); // 5 (oldest items dropped)
|
||||
recentLogs.addOne('request received');
|
||||
recentLogs.addMany(['processed', 'response sent']);
|
||||
|
||||
arr.addOne(7);
|
||||
arr.setLimit(3); // dynamically adjust limit
|
||||
console.log(recentLogs.array.length); // never exceeds 100
|
||||
|
||||
// Compute average (for numeric arrays)
|
||||
const numArr = new LimitedArray<number>(10);
|
||||
numArr.addMany([10, 20, 30]);
|
||||
console.log(numArr.getAverage()); // 20
|
||||
// Dynamically adjust the limit
|
||||
recentLogs.setLimit(50); // trims immediately if over
|
||||
|
||||
// Built-in average for numeric arrays
|
||||
const latencies = new LimitedArray<number>(1000);
|
||||
latencies.addMany([12, 15, 9, 22, 18]);
|
||||
console.log(latencies.getAverage()); // 15.2
|
||||
```
|
||||
|
||||
### LoopTracker
|
||||
---
|
||||
|
||||
Detects and prevents infinite loops by tracking object references during iterations.
|
||||
### 🔄 LoopTracker
|
||||
|
||||
Detects infinite loops by tracking which objects have already been visited during a traversal. Lightweight guard for recursive algorithms.
|
||||
|
||||
```typescript
|
||||
import { LoopTracker } from '@push.rocks/lik';
|
||||
|
||||
const tracker = new LoopTracker<object>();
|
||||
const obj1 = {};
|
||||
|
||||
tracker.checkAndTrack(obj1); // true (first time, tracked)
|
||||
tracker.checkAndTrack(obj1); // false (already seen - loop detected!)
|
||||
function traverse(node: any) {
|
||||
if (!tracker.checkAndTrack(node)) {
|
||||
console.warn('Cycle detected — skipping');
|
||||
return;
|
||||
}
|
||||
// safe to process this node
|
||||
for (const child of node.children) {
|
||||
traverse(child);
|
||||
}
|
||||
}
|
||||
|
||||
tracker.reset(); // clear for reuse
|
||||
tracker.destroy(); // free resources
|
||||
```
|
||||
|
||||
### ObjectMap
|
||||
---
|
||||
|
||||
A managed collection of objects with add/remove/find operations and event notifications via RxJS.
|
||||
### 📦 ObjectMap
|
||||
|
||||
A managed, observable object collection. Add, remove, find, and iterate objects — with **RxJS event notifications** for every mutation. Supports both auto-keyed and manually-keyed entries.
|
||||
|
||||
```typescript
|
||||
import { ObjectMap } from '@push.rocks/lik';
|
||||
|
||||
interface IUser {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
interface IUser { id: number; name: string; }
|
||||
|
||||
const users = new ObjectMap<IUser>();
|
||||
|
||||
// Add objects
|
||||
// Auto-keyed add (returns generated key)
|
||||
const key = users.add({ id: 1, name: 'Alice' });
|
||||
users.addArray([{ id: 2, name: 'Bob' }, { id: 3, name: 'Carol' }]);
|
||||
|
||||
// Find objects
|
||||
// Manually-keyed add/get/remove
|
||||
users.addMappedUnique('admin', { id: 99, name: 'Admin' });
|
||||
users.getMappedUnique('admin');
|
||||
users.removeMappedUnique('admin');
|
||||
|
||||
// Find (sync and async)
|
||||
const alice = users.findSync((u) => u.id === 1);
|
||||
const bob = await users.find(async (u) => u.id === 2);
|
||||
|
||||
@@ -190,126 +243,176 @@ const bob = await users.find(async (u) => u.id === 2);
|
||||
const removed = await users.findOneAndRemove(async (u) => u.id === 3);
|
||||
const removedSync = users.findOneAndRemoveSync((u) => u.id === 2);
|
||||
|
||||
// Direct add/get by unique key
|
||||
users.addMappedUnique('admin', { id: 99, name: 'Admin' });
|
||||
const admin = users.getMappedUnique('admin');
|
||||
|
||||
// Get one and remove (FIFO-style)
|
||||
// FIFO-style pop
|
||||
const first = users.getOneAndRemove();
|
||||
|
||||
// Iterate, check, and manage
|
||||
// Iterate and inspect
|
||||
await users.forEach((u) => console.log(u.name));
|
||||
users.checkForObject(alice); // true/false
|
||||
users.getKeyForObject(alice); // the internal key string
|
||||
users.isEmpty(); // true/false
|
||||
users.getArray(); // cloned array of all objects
|
||||
users.wipe(); // remove all
|
||||
|
||||
// Listen for changes
|
||||
// 🔔 Observe mutations
|
||||
users.eventSubject.subscribe((event) => {
|
||||
console.log(event.operation, event.payload); // 'add' | 'remove'
|
||||
// event.operation: 'add' | 'remove'
|
||||
// event.payload: the object
|
||||
});
|
||||
|
||||
// Merge object maps
|
||||
const merged = users.concat(otherObjectMap);
|
||||
users.addAllFromOther(otherObjectMap);
|
||||
// Merge
|
||||
const merged = users.concat(otherObjectMap); // new ObjectMap
|
||||
users.addAllFromOther(otherObjectMap); // merge in-place
|
||||
|
||||
// Teardown
|
||||
users.wipe(); // remove all entries (fires 'remove' events)
|
||||
users.destroy(); // wipe + complete eventSubject
|
||||
```
|
||||
|
||||
### Stringmap
|
||||
---
|
||||
|
||||
Manages a collection of strings with add/remove/query operations and minimatch pattern matching.
|
||||
### 🔤 Stringmap
|
||||
|
||||
A string collection with add/remove/query operations and **glob pattern matching** (via minimatch). Supports reactive triggers that fire when a condition becomes true.
|
||||
|
||||
```typescript
|
||||
import { Stringmap } from '@push.rocks/lik';
|
||||
|
||||
const strings = new Stringmap();
|
||||
const tags = new Stringmap();
|
||||
|
||||
strings.addString('hello');
|
||||
strings.addStringArray(['world', 'example']);
|
||||
tags.addString('feature:dark-mode');
|
||||
tags.addStringArray(['feature:i18n', 'bug:login']);
|
||||
|
||||
strings.checkString('hello'); // true
|
||||
strings.checkMinimatch('hel*'); // true (glob matching)
|
||||
strings.checkIsEmpty(); // false
|
||||
tags.checkString('feature:dark-mode'); // true
|
||||
tags.checkMinimatch('feature:*'); // true (glob matching!)
|
||||
tags.checkIsEmpty(); // false
|
||||
|
||||
strings.removeString('hello');
|
||||
strings.getStringArray(); // ['world', 'example']
|
||||
tags.removeString('bug:login');
|
||||
tags.getStringArray(); // cloned array of current strings
|
||||
|
||||
// Register trigger that fires when condition is met
|
||||
await strings.registerUntilTrue((arr) => arr.length === 0);
|
||||
strings.wipe(); // triggers the above
|
||||
// 🔔 Trigger: resolves when condition is met
|
||||
const waitForEmpty = tags.registerUntilTrue((arr) => arr.length === 0);
|
||||
tags.wipe(); // triggers the above → waitForEmpty resolves
|
||||
|
||||
// Clean up
|
||||
tags.destroy();
|
||||
```
|
||||
|
||||
### TimedAggregator
|
||||
---
|
||||
|
||||
Batches items over a time interval, then processes them in bulk. Useful for aggregating logs, metrics, or events.
|
||||
### ⏱️ TimedAggregator
|
||||
|
||||
Batches incoming items over a configurable time window, then processes the entire batch at once. Perfect for log aggregation, metric flushing, or debounced event processing.
|
||||
|
||||
```typescript
|
||||
import { TimedAggregtor } from '@push.rocks/lik';
|
||||
import { TimedAggregator } from '@push.rocks/lik';
|
||||
// Also available as: import { TimedAggregtor } from '@push.rocks/lik'; (legacy spelling)
|
||||
|
||||
const aggregator = new TimedAggregtor<string>({
|
||||
const batcher = new TimedAggregator<string>({
|
||||
aggregationIntervalInMillis: 5000,
|
||||
functionForAggregation: (items) => {
|
||||
console.log('Batch:', items);
|
||||
functionForAggregation: (batch) => {
|
||||
console.log(`Processing ${batch.length} items:`, batch);
|
||||
},
|
||||
});
|
||||
|
||||
aggregator.add('event1');
|
||||
aggregator.add('event2');
|
||||
// After 5 seconds: Batch: ['event1', 'event2']
|
||||
batcher.add('event-a');
|
||||
batcher.add('event-b');
|
||||
batcher.add('event-c');
|
||||
// After 5 seconds → "Processing 3 items: ['event-a', 'event-b', 'event-c']"
|
||||
|
||||
// Stop and flush any remaining items
|
||||
batcher.stop(true); // true = flush remaining immediately
|
||||
|
||||
// Restart after stopping
|
||||
batcher.restart();
|
||||
batcher.add('event-d'); // timer starts again
|
||||
```
|
||||
|
||||
### Tree
|
||||
**How it works:** The timer starts when the first item arrives. When it fires, all accumulated items are passed to `functionForAggregation` and a new timer starts. If no items arrive, the timer doesn't restart — it's lazy.
|
||||
|
||||
A typed wrapper around `symbol-tree` for managing hierarchical data structures with parent/child/sibling relationships.
|
||||
---
|
||||
|
||||
### 🌳 Tree
|
||||
|
||||
A typed wrapper around [`symbol-tree`](https://www.npmjs.com/package/symbol-tree) for managing hierarchical data. Full parent/child/sibling navigation, ordered iteration, and structural mutation.
|
||||
|
||||
```typescript
|
||||
import { Tree } from '@push.rocks/lik';
|
||||
|
||||
class TreeNode {
|
||||
constructor(public value: string) {}
|
||||
}
|
||||
interface INode { name: string; }
|
||||
|
||||
const tree = new Tree<TreeNode>();
|
||||
const root = new TreeNode('root');
|
||||
const tree = new Tree<INode>();
|
||||
const root: INode = { name: 'root' };
|
||||
tree.initialize(root);
|
||||
|
||||
const child1 = new TreeNode('child1');
|
||||
const child2 = new TreeNode('child2');
|
||||
const child1: INode = { name: 'child1' };
|
||||
const child2: INode = { name: 'child2' };
|
||||
const grandchild: INode = { name: 'grandchild' };
|
||||
|
||||
tree.appendChild(root, child1);
|
||||
tree.appendChild(root, child2);
|
||||
tree.appendChild(child1, grandchild);
|
||||
|
||||
// Navigate
|
||||
tree.hasChildren(root); // true
|
||||
// 🧭 Navigate
|
||||
tree.parent(child1); // root
|
||||
tree.firstChild(root); // child1
|
||||
tree.lastChild(root); // child2
|
||||
tree.nextSibling(child1); // child2
|
||||
tree.parent(child1); // root
|
||||
tree.previousSibling(child2); // child1
|
||||
tree.hasChildren(child1); // true
|
||||
|
||||
// Query
|
||||
tree.childrenCount(root); // 2
|
||||
tree.index(child2); // 1
|
||||
tree.childrenToArray(root, {}); // [child1, child2]
|
||||
tree.treeToArray(root, {}); // full tree as array
|
||||
// 📊 Query
|
||||
tree.childrenCount(root); // 2
|
||||
tree.index(child2); // 1
|
||||
tree.childrenToArray(root, {}); // [child1, child2]
|
||||
tree.ancestorsToArray(grandchild, {}); // [child1, root]
|
||||
tree.treeToArray(root, {}); // entire tree as flat array
|
||||
|
||||
// Mutate
|
||||
tree.insertBefore(child2, new TreeNode('between'));
|
||||
// 🔁 Iterate
|
||||
for (const node of tree.treeIterator(root, {})) {
|
||||
console.log(node.name);
|
||||
}
|
||||
|
||||
// ✂️ Mutate
|
||||
tree.insertBefore(child2, { name: 'between' });
|
||||
tree.insertAfter(child1, { name: 'after1' });
|
||||
tree.prependChild(root, { name: 'new-first' });
|
||||
tree.remove(child2);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API at a Glance
|
||||
|
||||
| Class | Purpose | Key Feature |
|
||||
|-------|---------|-------------|
|
||||
| `AsyncExecutionStack` | Async task scheduling | Exclusive/non-exclusive modes with concurrency limits |
|
||||
| `BackpressuredArray` | Bounded buffer | Producer/consumer backpressure via RxJS |
|
||||
| `FastMap` | Key-value store | O(1) lookups, merge/concat support |
|
||||
| `InterestMap` | Deduplicated subscriptions | Multiple waiters, single fulfillment |
|
||||
| `LimitedArray` | Rolling buffer | Auto-trim + built-in average |
|
||||
| `LoopTracker` | Cycle detection | Track-and-check in one call |
|
||||
| `ObjectMap` | Observable collection | RxJS event stream on every mutation |
|
||||
| `Stringmap` | String set | Glob matching + reactive triggers |
|
||||
| `TimedAggregator` | Batch processor | Time-windowed aggregation with flush |
|
||||
| `Tree` | Hierarchical data | Full navigation, iteration, mutation |
|
||||
|
||||
## License and Legal Information
|
||||
|
||||
This repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository.
|
||||
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](./LICENSE) file.
|
||||
|
||||
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
|
||||
|
||||
### Trademarks
|
||||
|
||||
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.
|
||||
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH or third parties, and are not included within the scope of the MIT license granted herein.
|
||||
|
||||
Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.
|
||||
|
||||
### Company Information
|
||||
|
||||
Task Venture Capital GmbH
|
||||
Registered at District court Bremen HRB 35230 HB, Germany
|
||||
Registered at District Court Bremen HRB 35230 HB, Germany
|
||||
|
||||
For any legal inquiries or if you require further information, please contact us via email at hello@task.vc.
|
||||
For any legal inquiries or further information, please contact us via email at hello@task.vc.
|
||||
|
||||
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.
|
||||
|
||||
118
test/test.backpressuredarray.both.ts
Normal file
118
test/test.backpressuredarray.both.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import { tap, expect } from '@push.rocks/tapbundle';
|
||||
import * as lik from '../ts/index.js';
|
||||
|
||||
let testArray: lik.BackpressuredArray<string>;
|
||||
|
||||
tap.test('should create a BackpressuredArray with default high water mark', async () => {
|
||||
testArray = new lik.BackpressuredArray<string>();
|
||||
expect(testArray).toBeInstanceOf(lik.BackpressuredArray);
|
||||
expect(testArray.length).toEqual(0);
|
||||
});
|
||||
|
||||
tap.test('should push items and return true while under high water mark', async () => {
|
||||
const arr = new lik.BackpressuredArray<number>(4);
|
||||
expect(arr.push(1)).toBeTrue();
|
||||
expect(arr.push(2)).toBeTrue();
|
||||
expect(arr.push(3)).toBeTrue();
|
||||
expect(arr.length).toEqual(3);
|
||||
});
|
||||
|
||||
tap.test('should return false when at high water mark', async () => {
|
||||
const arr = new lik.BackpressuredArray<number>(2);
|
||||
arr.push(1);
|
||||
const result = arr.push(2);
|
||||
expect(result).toBeFalse();
|
||||
expect(arr.length).toEqual(2);
|
||||
});
|
||||
|
||||
tap.test('should shift items correctly', async () => {
|
||||
const arr = new lik.BackpressuredArray<string>(4);
|
||||
arr.push('a');
|
||||
arr.push('b');
|
||||
expect(arr.shift()).toEqual('a');
|
||||
expect(arr.shift()).toEqual('b');
|
||||
expect(arr.shift()).toBeUndefined();
|
||||
});
|
||||
|
||||
tap.test('should peek without removing', async () => {
|
||||
const arr = new lik.BackpressuredArray<string>(4);
|
||||
arr.push('first');
|
||||
arr.push('second');
|
||||
expect(arr.peek()).toEqual('first');
|
||||
expect(arr.length).toEqual(2);
|
||||
});
|
||||
|
||||
tap.test('should peek return undefined on empty', async () => {
|
||||
const arr = new lik.BackpressuredArray<string>(4);
|
||||
expect(arr.peek()).toBeUndefined();
|
||||
});
|
||||
|
||||
tap.test('should pushMany items', async () => {
|
||||
const arr = new lik.BackpressuredArray<number>(10);
|
||||
const result = arr.pushMany([1, 2, 3]);
|
||||
expect(arr.length).toEqual(3);
|
||||
expect(result).toBeTrue();
|
||||
});
|
||||
|
||||
tap.test('checkHasItems returns correct boolean', async () => {
|
||||
const arr = new lik.BackpressuredArray<number>(4);
|
||||
expect(arr.checkHasItems()).toBeFalse();
|
||||
arr.push(1);
|
||||
expect(arr.checkHasItems()).toBeTrue();
|
||||
});
|
||||
|
||||
tap.test('checkSpaceAvailable works correctly', async () => {
|
||||
const arr = new lik.BackpressuredArray<number>(2);
|
||||
expect(arr.checkSpaceAvailable()).toBeTrue();
|
||||
arr.push(1);
|
||||
expect(arr.checkSpaceAvailable()).toBeTrue();
|
||||
arr.push(2);
|
||||
expect(arr.checkSpaceAvailable()).toBeFalse();
|
||||
});
|
||||
|
||||
tap.test('waitForItems resolves when items are pushed', async () => {
|
||||
const arr = new lik.BackpressuredArray<string>(4);
|
||||
let resolved = false;
|
||||
const waitPromise = arr.waitForItems().then(() => {
|
||||
resolved = true;
|
||||
});
|
||||
arr.push('hello');
|
||||
await waitPromise;
|
||||
expect(resolved).toBeTrue();
|
||||
});
|
||||
|
||||
tap.test('waitForSpace resolves when items are shifted', async () => {
|
||||
const arr = new lik.BackpressuredArray<number>(1);
|
||||
arr.push(1);
|
||||
let resolved = false;
|
||||
const waitPromise = arr.waitForSpace().then(() => {
|
||||
resolved = true;
|
||||
});
|
||||
arr.shift();
|
||||
await waitPromise;
|
||||
expect(resolved).toBeTrue();
|
||||
});
|
||||
|
||||
tap.test('Symbol.iterator works', async () => {
|
||||
const arr = new lik.BackpressuredArray<number>(10);
|
||||
arr.pushMany([10, 20, 30]);
|
||||
const collected: number[] = [];
|
||||
for (const item of arr) {
|
||||
collected.push(item);
|
||||
}
|
||||
expect(collected).toEqual([10, 20, 30]);
|
||||
});
|
||||
|
||||
tap.test('destroy completes subjects and unblocks waiters', async () => {
|
||||
const arr = new lik.BackpressuredArray<number>(1);
|
||||
arr.push(1);
|
||||
let spaceResolved = false;
|
||||
const waitPromise = arr.waitForSpace().then(() => {
|
||||
spaceResolved = true;
|
||||
});
|
||||
arr.destroy();
|
||||
await waitPromise;
|
||||
expect(spaceResolved).toBeTrue();
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
@@ -1,10 +1,10 @@
|
||||
import { tap, expect } from '@push.rocks/tapbundle';
|
||||
|
||||
import * as lik from '../ts/index.js';
|
||||
|
||||
tap.test('should create a valid fastmap', async () => {
|
||||
const fastmap = new lik.FastMap();
|
||||
expect(fastmap).toBeInstanceOf(lik.FastMap);
|
||||
expect(fastmap.size).toEqual(0);
|
||||
});
|
||||
|
||||
tap.test('should find an entry', async () => {
|
||||
@@ -27,4 +27,125 @@ tap.test('should find an entry', async () => {
|
||||
expect(result.value1).toEqual('heyho3');
|
||||
});
|
||||
|
||||
tap.test('isUniqueKey returns true for new key, false for existing', async () => {
|
||||
const map = new lik.FastMap<string>();
|
||||
expect(map.isUniqueKey('foo')).toBeTrue();
|
||||
map.addToMap('foo', 'bar');
|
||||
expect(map.isUniqueKey('foo')).toBeFalse();
|
||||
});
|
||||
|
||||
tap.test('has() works correctly', async () => {
|
||||
const map = new lik.FastMap<number>();
|
||||
expect(map.has('x')).toBeFalse();
|
||||
map.addToMap('x', 42);
|
||||
expect(map.has('x')).toBeTrue();
|
||||
});
|
||||
|
||||
tap.test('addToMap with force overwrites existing key', async () => {
|
||||
const map = new lik.FastMap<string>();
|
||||
map.addToMap('key1', 'original');
|
||||
const withoutForce = map.addToMap('key1', 'new');
|
||||
expect(withoutForce).toBeFalse();
|
||||
expect(map.getByKey('key1')).toEqual('original');
|
||||
|
||||
const withForce = map.addToMap('key1', 'new', { force: true });
|
||||
expect(withForce).toBeTrue();
|
||||
expect(map.getByKey('key1')).toEqual('new');
|
||||
});
|
||||
|
||||
tap.test('getByKey returns undefined for missing key', async () => {
|
||||
const map = new lik.FastMap<string>();
|
||||
expect(map.getByKey('nonexistent')).toBeUndefined();
|
||||
});
|
||||
|
||||
tap.test('removeFromMap removes and returns item', async () => {
|
||||
const map = new lik.FastMap<string>();
|
||||
map.addToMap('a', 'hello');
|
||||
const removed = map.removeFromMap('a');
|
||||
expect(removed).toEqual('hello');
|
||||
expect(map.has('a')).toBeFalse();
|
||||
expect(map.size).toEqual(0);
|
||||
});
|
||||
|
||||
tap.test('getKeys returns all keys', async () => {
|
||||
const map = new lik.FastMap<number>();
|
||||
map.addToMap('a', 1);
|
||||
map.addToMap('b', 2);
|
||||
map.addToMap('c', 3);
|
||||
const keys = map.getKeys();
|
||||
expect(keys.length).toEqual(3);
|
||||
expect(keys).toContain('a');
|
||||
expect(keys).toContain('b');
|
||||
expect(keys).toContain('c');
|
||||
});
|
||||
|
||||
tap.test('values returns all values', async () => {
|
||||
const map = new lik.FastMap<number>();
|
||||
map.addToMap('x', 10);
|
||||
map.addToMap('y', 20);
|
||||
const vals = map.values();
|
||||
expect(vals.length).toEqual(2);
|
||||
expect(vals).toContain(10);
|
||||
expect(vals).toContain(20);
|
||||
});
|
||||
|
||||
tap.test('entries returns key-value pairs', async () => {
|
||||
const map = new lik.FastMap<string>();
|
||||
map.addToMap('k1', 'v1');
|
||||
const entries = map.entries();
|
||||
expect(entries.length).toEqual(1);
|
||||
expect(entries[0][0]).toEqual('k1');
|
||||
expect(entries[0][1]).toEqual('v1');
|
||||
});
|
||||
|
||||
tap.test('clean empties the map', async () => {
|
||||
const map = new lik.FastMap<string>();
|
||||
map.addToMap('a', 'b');
|
||||
map.addToMap('c', 'd');
|
||||
map.clean();
|
||||
expect(map.size).toEqual(0);
|
||||
expect(map.getKeys().length).toEqual(0);
|
||||
});
|
||||
|
||||
tap.test('concat merges two maps', async () => {
|
||||
const map1 = new lik.FastMap<number>();
|
||||
map1.addToMap('a', 1);
|
||||
const map2 = new lik.FastMap<number>();
|
||||
map2.addToMap('b', 2);
|
||||
const merged = map1.concat(map2);
|
||||
expect(merged.size).toEqual(2);
|
||||
expect(merged.getByKey('a')).toEqual(1);
|
||||
expect(merged.getByKey('b')).toEqual(2);
|
||||
});
|
||||
|
||||
tap.test('addAllFromOther merges in place', async () => {
|
||||
const map1 = new lik.FastMap<number>();
|
||||
map1.addToMap('a', 1);
|
||||
const map2 = new lik.FastMap<number>();
|
||||
map2.addToMap('b', 2);
|
||||
map2.addToMap('a', 99);
|
||||
map1.addAllFromOther(map2);
|
||||
expect(map1.size).toEqual(2);
|
||||
expect(map1.getByKey('a')).toEqual(99);
|
||||
expect(map1.getByKey('b')).toEqual(2);
|
||||
});
|
||||
|
||||
tap.test('Symbol.iterator works with for...of', async () => {
|
||||
const map = new lik.FastMap<number>();
|
||||
map.addToMap('x', 1);
|
||||
map.addToMap('y', 2);
|
||||
const collected: [string, number][] = [];
|
||||
for (const entry of map) {
|
||||
collected.push(entry);
|
||||
}
|
||||
expect(collected.length).toEqual(2);
|
||||
});
|
||||
|
||||
tap.test('find returns undefined when no match', async () => {
|
||||
const map = new lik.FastMap<number>();
|
||||
map.addToMap('a', 1);
|
||||
const result = await map.find(async (item) => item === 999);
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
|
||||
@@ -15,11 +15,51 @@ tap.test('should create an interest', async () => {
|
||||
});
|
||||
|
||||
tap.test('should return an already existing interest', async () => {
|
||||
await testInterestmap.addInterest(3);
|
||||
const interest3a = await testInterestmap.addInterest(3);
|
||||
const interest3b = await testInterestmap.addInterest(3);
|
||||
expect(interest3a === interest3b).toBeTrue();
|
||||
});
|
||||
|
||||
tap.test('should be able to inform about a lost interest', async () => {
|
||||
testInterestmap.informLostInterest(3);
|
||||
});
|
||||
|
||||
tap.test('checkInterest returns true for existing', async () => {
|
||||
expect(testInterestmap.checkInterest(4)).toBeTrue();
|
||||
});
|
||||
|
||||
tap.test('checkInterest returns false for non-existing', async () => {
|
||||
expect(testInterestmap.checkInterest(999)).toBeFalse();
|
||||
});
|
||||
|
||||
tap.test('findInterest returns the interest', async () => {
|
||||
const interest = testInterestmap.findInterest(4);
|
||||
expect(interest).not.toBeNull();
|
||||
expect(interest.originalInterest).toEqual(4);
|
||||
});
|
||||
|
||||
tap.test('findInterest returns null for non-existing', async () => {
|
||||
const interest = testInterestmap.findInterest(888);
|
||||
expect(interest).toBeNull();
|
||||
});
|
||||
|
||||
tap.test('fullfillInterest resolves the promise', async () => {
|
||||
const im = new lik.InterestMap<string, string>((s) => s);
|
||||
const interest = await im.addInterest('hello');
|
||||
interest.fullfillInterest('world');
|
||||
const result = await interest.interestFullfilled;
|
||||
expect(result).toEqual('world');
|
||||
expect(interest.isFullfilled).toBeTrue();
|
||||
im.destroy();
|
||||
});
|
||||
|
||||
tap.test('destroy cleans up interestmap', async () => {
|
||||
const im = new lik.InterestMap<string, string>((s) => s);
|
||||
await im.addInterest('a');
|
||||
await im.addInterest('b');
|
||||
im.destroy();
|
||||
expect(im.checkInterest('a')).toBeFalse();
|
||||
expect(im.checkInterest('b')).toBeFalse();
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
|
||||
@@ -7,11 +7,83 @@ let testLimitedArray: LimitedArray<string>;
|
||||
tap.test('should create a LimitedArray', async () => {
|
||||
testLimitedArray = new LimitedArray(6);
|
||||
expect(testLimitedArray).toBeInstanceOf(LimitedArray);
|
||||
expect(testLimitedArray.length).toEqual(0);
|
||||
});
|
||||
|
||||
tap.test('should never be longer than the set length', async () => {
|
||||
testLimitedArray.addMany(['hi', 'this', 'is', 'quite', 'a', 'long', 'string', ':)']);
|
||||
expect(testLimitedArray.array.length < 7).toBeTrue();
|
||||
expect(testLimitedArray.length).toEqual(6);
|
||||
});
|
||||
|
||||
tap.test('addOne respects limit', async () => {
|
||||
const arr = new LimitedArray<number>(3);
|
||||
arr.addOne(1);
|
||||
arr.addOne(2);
|
||||
arr.addOne(3);
|
||||
arr.addOne(4);
|
||||
expect(arr.length).toEqual(3);
|
||||
});
|
||||
|
||||
tap.test('setLimit truncates when lowered', async () => {
|
||||
const arr = new LimitedArray<number>(5);
|
||||
arr.addMany([1, 2, 3, 4, 5]);
|
||||
expect(arr.length).toEqual(5);
|
||||
arr.setLimit(2);
|
||||
expect(arr.length).toEqual(2);
|
||||
});
|
||||
|
||||
tap.test('getAverage returns correct average for numbers', async () => {
|
||||
const arr = new LimitedArray<number>(10);
|
||||
arr.addMany([10, 20, 30]);
|
||||
expect(arr.getAverage()).toEqual(20);
|
||||
});
|
||||
|
||||
tap.test('getAverage returns 0 for empty array', async () => {
|
||||
const arr = new LimitedArray<number>(10);
|
||||
expect(arr.getAverage()).toEqual(0);
|
||||
});
|
||||
|
||||
tap.test('getAverage returns null for non-number array', async () => {
|
||||
const arr = new LimitedArray<string>(10);
|
||||
arr.addOne('hello');
|
||||
expect(arr.getAverage()).toBeNull();
|
||||
});
|
||||
|
||||
tap.test('remove removes an item', async () => {
|
||||
const arr = new LimitedArray<string>(10);
|
||||
arr.addMany(['a', 'b', 'c']);
|
||||
const removed = arr.remove('b');
|
||||
expect(removed).toBeTrue();
|
||||
expect(arr.length).toEqual(2);
|
||||
const notFound = arr.remove('zzz');
|
||||
expect(notFound).toBeFalse();
|
||||
});
|
||||
|
||||
tap.test('clear empties the array', async () => {
|
||||
const arr = new LimitedArray<number>(10);
|
||||
arr.addMany([1, 2, 3]);
|
||||
arr.clear();
|
||||
expect(arr.length).toEqual(0);
|
||||
});
|
||||
|
||||
tap.test('getArray returns a copy', async () => {
|
||||
const arr = new LimitedArray<number>(10);
|
||||
arr.addMany([1, 2, 3]);
|
||||
const copy = arr.getArray();
|
||||
expect(copy.length).toEqual(3);
|
||||
copy.push(99);
|
||||
expect(arr.length).toEqual(3);
|
||||
});
|
||||
|
||||
tap.test('Symbol.iterator enables for...of', async () => {
|
||||
const arr = new LimitedArray<number>(10);
|
||||
arr.addMany([5, 10, 15]);
|
||||
const collected: number[] = [];
|
||||
for (const item of arr) {
|
||||
collected.push(item);
|
||||
}
|
||||
expect(collected.length).toEqual(3);
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
|
||||
@@ -21,4 +21,14 @@ tap.test('should add objects once and return true', async () => {
|
||||
expect(myLoopTracker.checkAndTrack(object2)).toBeFalse();
|
||||
});
|
||||
|
||||
tap.test('reset allows re-tracking', async () => {
|
||||
myLoopTracker.reset();
|
||||
expect(myLoopTracker.checkAndTrack(object1)).toBeTrue();
|
||||
expect(myLoopTracker.checkAndTrack(object1)).toBeFalse();
|
||||
});
|
||||
|
||||
tap.test('destroy cleans up', async () => {
|
||||
myLoopTracker.destroy();
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
|
||||
@@ -22,6 +22,8 @@ let testObject2: ITestObject = {
|
||||
tap.test('new lik.Objectmap() -> should correctly instantiate an Objectmap', async () => {
|
||||
testObjectmap = new lik.ObjectMap<ITestObject>();
|
||||
expect(testObjectmap).toBeInstanceOf(lik.ObjectMap);
|
||||
expect(testObjectmap.length).toEqual(0);
|
||||
expect(testObjectmap.isEmpty()).toBeTrue();
|
||||
});
|
||||
|
||||
tap.test('lik.Objectmap.add() -> should correctly add an object to Objectmap', async () => {
|
||||
@@ -30,6 +32,14 @@ tap.test('lik.Objectmap.add() -> should correctly add an object to Objectmap', a
|
||||
expect(testObjectmap.checkForObject(testObject1)).toBeTrue();
|
||||
// tslint:disable-next-line:no-unused-expression
|
||||
expect(testObjectmap.checkForObject(testObject2)).toBeFalse();
|
||||
expect(testObjectmap.length).toEqual(1);
|
||||
});
|
||||
|
||||
tap.test('add same object twice returns same key', async () => {
|
||||
const key1 = testObjectmap.add(testObject1);
|
||||
const key2 = testObjectmap.add(testObject1);
|
||||
expect(key1).toEqual(key2);
|
||||
expect(testObjectmap.length).toEqual(1);
|
||||
});
|
||||
|
||||
tap.test('lik.Objectmap.remove() -> should correctly remove an object to Objectmap', async () => {
|
||||
@@ -73,4 +83,127 @@ tap.test('should get one object and then remove it', async () => {
|
||||
expect(testObjectmap.getArray()).not.toContain(oneObject);
|
||||
});
|
||||
|
||||
tap.test('addMappedUnique and getMappedUnique work', async () => {
|
||||
const map = new lik.ObjectMap<string>();
|
||||
map.addMappedUnique('myKey', 'myValue');
|
||||
expect(map.getMappedUnique('myKey')).toEqual('myValue');
|
||||
});
|
||||
|
||||
tap.test('removeMappedUnique works', async () => {
|
||||
const map = new lik.ObjectMap<string>();
|
||||
map.addMappedUnique('k1', 'v1');
|
||||
const removed = map.removeMappedUnique('k1');
|
||||
expect(removed).toEqual('v1');
|
||||
expect(map.getMappedUnique('k1')).toBeUndefined();
|
||||
});
|
||||
|
||||
tap.test('addArray adds multiple objects', async () => {
|
||||
const map = new lik.ObjectMap<{ id: number }>();
|
||||
const items = [{ id: 1 }, { id: 2 }, { id: 3 }];
|
||||
map.addArray(items);
|
||||
expect(map.length).toEqual(3);
|
||||
for (const item of items) {
|
||||
expect(map.checkForObject(item)).toBeTrue();
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('findSync works', async () => {
|
||||
const map = new lik.ObjectMap<{ name: string }>();
|
||||
const obj = { name: 'target' };
|
||||
map.add({ name: 'other' });
|
||||
map.add(obj);
|
||||
const found = map.findSync((item) => item.name === 'target');
|
||||
expect(found).toEqual(obj);
|
||||
});
|
||||
|
||||
tap.test('findOneAndRemove works', async () => {
|
||||
const map = new lik.ObjectMap<{ val: number }>();
|
||||
map.add({ val: 1 });
|
||||
map.add({ val: 2 });
|
||||
const removed = await map.findOneAndRemove(async (item) => item.val === 1);
|
||||
expect(removed.val).toEqual(1);
|
||||
expect(map.length).toEqual(1);
|
||||
});
|
||||
|
||||
tap.test('isEmpty returns correct state', async () => {
|
||||
const map = new lik.ObjectMap<string>();
|
||||
expect(map.isEmpty()).toBeTrue();
|
||||
map.add('hello');
|
||||
expect(map.isEmpty()).toBeFalse();
|
||||
});
|
||||
|
||||
tap.test('wipe clears all entries', async () => {
|
||||
const map = new lik.ObjectMap<number>();
|
||||
map.add(1);
|
||||
map.add(2);
|
||||
map.add(3);
|
||||
map.wipe();
|
||||
expect(map.isEmpty()).toBeTrue();
|
||||
expect(map.length).toEqual(0);
|
||||
});
|
||||
|
||||
tap.test('concat creates merged map', async () => {
|
||||
const map1 = new lik.ObjectMap<string>();
|
||||
map1.add('a');
|
||||
const map2 = new lik.ObjectMap<string>();
|
||||
map2.add('b');
|
||||
const merged = map1.concat(map2);
|
||||
expect(merged.length).toEqual(2);
|
||||
});
|
||||
|
||||
tap.test('map/filter/reduce work', async () => {
|
||||
const map = new lik.ObjectMap<{ val: number }>();
|
||||
map.add({ val: 1 });
|
||||
map.add({ val: 2 });
|
||||
map.add({ val: 3 });
|
||||
|
||||
const mapped = map.map((item) => item.val * 2);
|
||||
expect(mapped.length).toEqual(3);
|
||||
expect(mapped).toContain(2);
|
||||
expect(mapped).toContain(4);
|
||||
expect(mapped).toContain(6);
|
||||
|
||||
const filtered = map.filter((item) => item.val > 1);
|
||||
expect(filtered.length).toEqual(2);
|
||||
|
||||
const sum = map.reduce((acc, item) => acc + item.val, 0);
|
||||
expect(sum).toEqual(6);
|
||||
});
|
||||
|
||||
tap.test('Symbol.iterator enables for...of', async () => {
|
||||
const map = new lik.ObjectMap<number>();
|
||||
map.add(10);
|
||||
map.add(20);
|
||||
const collected: number[] = [];
|
||||
for (const item of map) {
|
||||
collected.push(item);
|
||||
}
|
||||
expect(collected.length).toEqual(2);
|
||||
expect(collected).toContain(10);
|
||||
expect(collected).toContain(20);
|
||||
});
|
||||
|
||||
tap.test('eventSubject emits add and remove events', async () => {
|
||||
const map = new lik.ObjectMap<string>();
|
||||
const events: Array<{ operation: string; payload: string }> = [];
|
||||
map.eventSubject.subscribe((event) => {
|
||||
events.push(event);
|
||||
});
|
||||
map.add('x');
|
||||
map.remove('x');
|
||||
expect(events.length).toEqual(2);
|
||||
expect(events[0].operation).toEqual('add');
|
||||
expect(events[1].operation).toEqual('remove');
|
||||
});
|
||||
|
||||
tap.test('destroy wipes and completes eventSubject', async () => {
|
||||
const map = new lik.ObjectMap<number>();
|
||||
map.add(1);
|
||||
let completed = false;
|
||||
map.eventSubject.subscribe({ complete: () => { completed = true; } });
|
||||
map.destroy();
|
||||
expect(map.isEmpty()).toBeTrue();
|
||||
expect(completed).toBeTrue();
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
|
||||
@@ -4,20 +4,78 @@ import { expect, tap } from '@push.rocks/tapbundle';
|
||||
// import the module
|
||||
import * as lik from '../ts/index.js';
|
||||
|
||||
let testTimedAggregator: lik.TimedAggregtor<string>;
|
||||
|
||||
tap.test('should create a timed aggregaotor', async (tools) => {
|
||||
testTimedAggregator = new lik.TimedAggregtor<string>({
|
||||
aggregationIntervalInMillis: 1000,
|
||||
tap.test('should create a timed aggregator and aggregate items', async (tools) => {
|
||||
const batches: string[][] = [];
|
||||
const aggregator = new lik.TimedAggregtor<string>({
|
||||
aggregationIntervalInMillis: 200,
|
||||
functionForAggregation: (aggregation) => {
|
||||
console.log(aggregation);
|
||||
batches.push(aggregation);
|
||||
},
|
||||
});
|
||||
testTimedAggregator.add('This');
|
||||
testTimedAggregator.add('is a whole sentence.');
|
||||
await tools.delayFor(1001);
|
||||
testTimedAggregator.add('This one is another.');
|
||||
await tools.delayFor(2000);
|
||||
aggregator.add('first');
|
||||
aggregator.add('second');
|
||||
await tools.delayFor(300);
|
||||
expect(batches.length).toEqual(1);
|
||||
expect(batches[0]).toContain('first');
|
||||
expect(batches[0]).toContain('second');
|
||||
|
||||
aggregator.add('third');
|
||||
await tools.delayFor(300);
|
||||
expect(batches.length).toEqual(2);
|
||||
expect(batches[1]).toContain('third');
|
||||
aggregator.stop();
|
||||
});
|
||||
|
||||
tap.test('stop() prevents further aggregation', async (tools) => {
|
||||
const batches: number[][] = [];
|
||||
const aggregator = new lik.TimedAggregtor<number>({
|
||||
aggregationIntervalInMillis: 100,
|
||||
functionForAggregation: (items) => {
|
||||
batches.push(items);
|
||||
},
|
||||
});
|
||||
aggregator.add(1);
|
||||
aggregator.stop();
|
||||
aggregator.add(2);
|
||||
await tools.delayFor(200);
|
||||
expect(batches.length).toEqual(0);
|
||||
});
|
||||
|
||||
tap.test('stop(true) flushes remaining items', async () => {
|
||||
const batches: number[][] = [];
|
||||
const aggregator = new lik.TimedAggregtor<number>({
|
||||
aggregationIntervalInMillis: 5000,
|
||||
functionForAggregation: (items) => {
|
||||
batches.push(items);
|
||||
},
|
||||
});
|
||||
aggregator.add(10);
|
||||
aggregator.add(20);
|
||||
aggregator.stop(true);
|
||||
expect(batches.length).toEqual(1);
|
||||
expect(batches[0]).toEqual([10, 20]);
|
||||
});
|
||||
|
||||
tap.test('restart allows adding again after stop', async (tools) => {
|
||||
const batches: string[][] = [];
|
||||
const aggregator = new lik.TimedAggregtor<string>({
|
||||
aggregationIntervalInMillis: 100,
|
||||
functionForAggregation: (items) => {
|
||||
batches.push(items);
|
||||
},
|
||||
});
|
||||
aggregator.add('a');
|
||||
aggregator.stop();
|
||||
aggregator.restart();
|
||||
aggregator.add('b');
|
||||
await tools.delayFor(200);
|
||||
expect(batches.length).toEqual(1);
|
||||
expect(batches[0]).toContain('b');
|
||||
aggregator.stop();
|
||||
});
|
||||
|
||||
tap.test('TimedAggregator alias exists', async () => {
|
||||
expect(lik.TimedAggregator).toEqual(lik.TimedAggregtor);
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
|
||||
@@ -33,8 +33,119 @@ tap.test('should add other objects in a hierachy', async () => {
|
||||
testTree.appendChild(testInstance, testInstance4);
|
||||
});
|
||||
|
||||
tap.test('hasChildren returns correct value', async () => {
|
||||
expect(testTree.hasChildren(testInstance)).toBeTrue();
|
||||
expect(testTree.hasChildren(testInstance2)).toBeFalse();
|
||||
});
|
||||
|
||||
tap.test('firstChild and lastChild work', async () => {
|
||||
const first = testTree.firstChild(testInstance);
|
||||
expect(first.hey).toEqual('second');
|
||||
const last = testTree.lastChild(testInstance);
|
||||
expect(last.hey).toEqual('fourth');
|
||||
});
|
||||
|
||||
tap.test('parent returns correct parent', async () => {
|
||||
const parent = testTree.parent(testInstance2);
|
||||
expect(parent.hey).toEqual('first');
|
||||
});
|
||||
|
||||
tap.test('nextSibling and previousSibling work', async () => {
|
||||
const next = testTree.nextSibling(testInstance2);
|
||||
expect(next.hey).toEqual('third');
|
||||
const prev = testTree.previousSibling(testInstance3);
|
||||
expect(prev.hey).toEqual('second');
|
||||
});
|
||||
|
||||
tap.test('childrenCount returns correct count', async () => {
|
||||
expect(testTree.childrenCount(testInstance)).toEqual(3);
|
||||
expect(testTree.childrenCount(testInstance2)).toEqual(0);
|
||||
});
|
||||
|
||||
tap.test('index returns sibling index', async () => {
|
||||
expect(testTree.index(testInstance2)).toEqual(0);
|
||||
expect(testTree.index(testInstance3)).toEqual(1);
|
||||
expect(testTree.index(testInstance4)).toEqual(2);
|
||||
});
|
||||
|
||||
tap.test('childrenToArray returns children', async () => {
|
||||
const children = testTree.childrenToArray(testInstance, {});
|
||||
expect(children.length).toEqual(3);
|
||||
expect(children[0].hey).toEqual('second');
|
||||
});
|
||||
|
||||
tap.test('insertBefore works', async () => {
|
||||
testTree.initialize(testInstance5);
|
||||
testTree.insertBefore(testInstance3, testInstance5);
|
||||
const idx = testTree.index(testInstance5);
|
||||
expect(idx).toEqual(1);
|
||||
expect(testTree.nextSibling(testInstance5).hey).toEqual('third');
|
||||
});
|
||||
|
||||
tap.test('insertAfter works', async () => {
|
||||
testTree.initialize(testInstance6);
|
||||
testTree.insertAfter(testInstance3, testInstance6);
|
||||
expect(testTree.previousSibling(testInstance6).hey).toEqual('third');
|
||||
});
|
||||
|
||||
tap.test('remove detaches node', async () => {
|
||||
const countBefore = testTree.childrenCount(testInstance);
|
||||
testTree.remove(testInstance6);
|
||||
expect(testTree.childrenCount(testInstance)).toEqual(countBefore - 1);
|
||||
});
|
||||
|
||||
tap.test('treeIterator with options works', async () => {
|
||||
const items: TestClass[] = [];
|
||||
for (const item of testTree.treeIterator(testInstance, {})) {
|
||||
items.push(item);
|
||||
}
|
||||
expect(items.length).toBeGreaterThan(1);
|
||||
expect(items[0].hey).toEqual('first');
|
||||
});
|
||||
|
||||
tap.test('nextSiblingsIterator works (bug was fixed)', async () => {
|
||||
const siblings: TestClass[] = [];
|
||||
for (const item of testTree.nextSiblingsIterator(testInstance2)) {
|
||||
siblings.push(item);
|
||||
}
|
||||
expect(siblings.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
tap.test('ancestorsIterator works (bug was fixed)', async () => {
|
||||
const ancestors: TestClass[] = [];
|
||||
for (const item of testTree.ancestorsIterator(testInstance2)) {
|
||||
ancestors.push(item);
|
||||
}
|
||||
expect(ancestors.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
tap.test("should create a JSON object that reflects a tree's hierachy", async () => {
|
||||
const jsonTreet = testTree.toJsonWithHierachy(testInstance);
|
||||
const jsonTree = testTree.toJsonWithHierachy(testInstance);
|
||||
expect(jsonTree).toHaveProperty('data');
|
||||
expect(jsonTree).toHaveProperty('children');
|
||||
expect(jsonTree.data.hey).toEqual('first');
|
||||
expect(jsonTree.children.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
tap.test('fromJsonWithHierachy rebuilds a tree', async () => {
|
||||
const newTree = new lik.Tree<{ name: string }>();
|
||||
const jsonRoot = {
|
||||
data: { name: 'root' },
|
||||
children: [
|
||||
{ data: { name: 'child1' }, children: [] },
|
||||
{
|
||||
data: { name: 'child2' },
|
||||
children: [{ data: { name: 'grandchild' }, children: [] }],
|
||||
},
|
||||
],
|
||||
};
|
||||
const root = newTree.fromJsonWithHierachy(jsonRoot);
|
||||
expect(root.name).toEqual('root');
|
||||
expect(newTree.hasChildren(root)).toBeTrue();
|
||||
expect(newTree.childrenCount(root)).toEqual(2);
|
||||
const children = newTree.childrenToArray(root, {});
|
||||
expect(children[1].name).toEqual('child2');
|
||||
expect(newTree.hasChildren(children[1])).toBeTrue();
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"experimentalDecorators": true,
|
||||
"useDefineForClassFields": false,
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
|
||||
Reference in New Issue
Block a user