Compare commits

..

23 Commits

Author SHA1 Message Date
fc00655cef v6.4.0
Some checks failed
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-03-22 08:44:49 +00:00
f4db131ede feat(collections): add new collection APIs, iterator support, and tree serialization utilities 2026-03-22 08:44:49 +00:00
20182a00f8 v6.3.1
Some checks failed
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-03-01 19:21:42 +00:00
ddf4e698c9 fix(classes): cleanup resources, add cancellable timeouts, and fix bugs in several core utility classes 2026-03-01 19:21:42 +00:00
597e9e15c3 v6.3.0
Some checks failed
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-03-01 12:29:44 +00:00
03a33195bc feat(tooling): update build tooling, developer dependencies, npmextra configuration, and expand README documentation 2026-03-01 12:29:44 +00:00
47d339bb2b v6.2.3
Some checks failed
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-03-01 12:18:16 +00:00
bf3b4e832a fix(interestmap): remove interest from InterestMap immediately after fulfillment 2026-03-01 12:18:16 +00:00
685f4ebb3b 6.2.2 2025-04-25 19:25:25 +00:00
2ff7efe6d8 fix(docs): Update @push.rocks/tapbundle dependency and refine AsyncExecutionStack documentation examples 2025-04-25 19:25:25 +00:00
99072d5fdf 6.2.1 2025-04-25 19:15:23 +00:00
4442ddffcd fix(AsyncExecutionStack tests): Refactor AsyncExecutionStack tests: update non-exclusive concurrency assertions and clean up test logic 2025-04-25 19:15:23 +00:00
7c5b3825ac 6.2.0 2025-04-25 18:57:26 +00:00
84babb3cd4 feat(AsyncExecutionStack): Improve non-exclusive task management with concurrency limit controls and enhanced monitoring in AsyncExecutionStack. 2025-04-25 18:57:26 +00:00
5d9624bd56 6.1.0 2024-10-13 20:40:40 +02:00
32397a97cd feat(BackpressuredArray): Add method to check if items are present in BackpressuredArray 2024-10-13 20:40:40 +02:00
753b829d18 update description 2024-05-29 14:11:13 +02:00
4be2784bf4 6.0.15 2024-04-18 21:55:34 +02:00
cd08cf370c fix(core): update 2024-04-18 21:55:33 +02:00
b64fe567a8 update tsconfig 2024-04-14 13:39:10 +02:00
2a68c9ad90 update npmextra.json: githost 2024-04-01 21:33:17 +02:00
aa978413d1 update npmextra.json: githost 2024-04-01 19:57:26 +02:00
f24636ba80 update npmextra.json: githost 2024-03-30 21:46:23 +01:00
30 changed files with 11142 additions and 4633 deletions

113
changelog.md Normal file
View File

@@ -0,0 +1,113 @@
# 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
- Replace one-shot delayFor usage with plugins.smartdelay.Timeout in AsyncExecutionStack so timeouts are cancellable and properly cleaned up on success or error
- Add destroy() to BackpressuredArray to complete subjects and unblock waiters; waitForSpace/waitForItems now respect destruction to avoid hangs
- Make Interest instances cancel mark-lost timers and guard against double-destroy; destruction now clears fulfillment store and resolves default fulfillment without mutual recursion
- Add InterestMap.destroy() to clean up all interests and complete observable
- ObjectMap: removeMappedUnique now returns removed object and emits a remove event; wipe now emits remove events for cleared entries and destroy() completes eventSubject
- StringMap.destroy() clears stored strings and pending triggers
- TimedAggregtor: add stop(flushRemaining) and isStopped guards to stop timer chain and optionally flush remaining items
- LoopTracker: add reset() and destroy() helpers to clear and destroy internal maps
- Fix compareTreePosition to call symbolTree.compareTreePosition instead of recursively calling itself
## 2026-03-01 - 6.3.0 - feat(tooling)
update build tooling, developer dependencies, npmextra configuration, and expand README documentation
- Bump devDependencies for @git.zone toolchain and related packages (@git.zone/tsbuild, tsbundle, tsrun, tstest, @push.rocks/tapbundle, @types/node)
- Bump runtime deps: @push.rocks/smartrx and @push.rocks/smarttime
- Adjust npm build script: remove trailing 'npm' argument from tsbundle invocation
- Rework npmextra.json: rename/unify keys to @git.zone/* scoped entries, add release registries and accessLevel, add tsbundle bundle configuration, and reorganize CI/tool settings
- Significant README rewrite: expanded descriptions, clearer usage examples and API snippets, formatting and example updates
## 2026-03-01 - 6.2.3 - fix(interestmap)
remove interest from InterestMap immediately after fulfillment
- Call destroy() in fullfillInterest to remove the interest entry from the InterestMap right after resolving interestDeferred.
- Prevents stale entries and ensures immediate cleanup of fulfilled interests
## 2025-04-25 - 6.2.2 - fix(docs)
Update @push.rocks/tapbundle dependency and refine AsyncExecutionStack documentation examples
- Bump @push.rocks/tapbundle from ^5.0.8 to ^5.5.6 in package.json
- Improve README documentation for AsyncExecutionStack with clearer examples for exclusive and non-exclusive task execution
- Demonstrate usage of concurrency controls in AsyncExecutionStack
## 2025-04-25 - 6.2.1 - fix(AsyncExecutionStack tests)
Refactor AsyncExecutionStack tests: update non-exclusive concurrency assertions and clean up test logic
- Replace 'toBe' with 'toEqual' for active and pending counts to ensure consistency
- Simplify default non-exclusive concurrency test by asserting Infinity is non-finite using toBeFalse
- Adjust test comments and scheduling for clarity in concurrency behavior
## 2025-04-25 - 6.2.0 - feat(AsyncExecutionStack)
Improve non-exclusive task management with concurrency limit controls and enhanced monitoring in AsyncExecutionStack.
- Added methods to set and get non-exclusive concurrency limits and statistics (setNonExclusiveMaxConcurrency, getActiveNonExclusiveCount, getPendingNonExclusiveCount, and getNonExclusiveMaxConcurrency).
- Integrated proper waiting and release mechanisms for non-exclusive slots.
- Extended test coverage to validate concurrency limits and ensure correct behavior.
## 2024-10-13 - 6.1.0 - feat(BackpressuredArray)
Add method to check if items are present in BackpressuredArray
- Implemented a new method `checkHasItems` in the BackpressuredArray class to determine if the array contains any items.
## 2024-05-29 to 2024-04-18 - 6.0.15
Minor updates were made to documentation and descriptions.
- Update project description
## 2024-04-18 to 2024-02-25 - 6.0.14
Several updates were made to configurations and json files.
- Updated core components in the codebase
- Modified tsconfig settings
- Revised npmextra.json with githost configurations
## 2024-02-25 to 2024-02-23 - 6.0.13
No relevant changes.
## 2024-02-23 to 2023-11-13 - 6.0.12 to 6.0.8
Multiple core updates were performed to ensure stability and performance.
- Fixed various issues in core components
## 2023-11-13 to 2023-08-14 - 6.0.7 to 6.0.3
Minor internal core updates.
## 2023-08-14 to 2023-07-12 - 6.0.2
Implemented a switch to a new organizational scheme.
## 2023-01-18 to 2022-05-27 - 6.0.0
Updated core functionalities; introduced breaking changes for compatibility with ECMAScript modules.
- Core updates
- Switching from CommonJS to ECMAScript modules
## 2022-05-27 to 2022-05-27 - 5.0.6 to 5.0.0
Minor updates and a significant change in `objectmap` behavior to support async operations.
- Included async behaviors in objectmap as a breaking change
## 2020-05-04 to 2020-02-17 - 4.0.0
Refactored ObjectMap; introduced new features.
- Refactored ObjectMap with concat functionality as a breaking change
- Added .clean() to FastMap
## 2020-02-17 to 2020-02-06 - 3.0.19 to 3.0.15
Enhancements and new functionality in ObjectMap.
- Added object mapping enhancements
- Introduced object map with unique keys

View File

@@ -1,17 +1,49 @@
{
"npmci": {
"npmGlobalTools": [],
"npmAccessLevel": "public"
},
"gitzone": {
"@git.zone/cli": {
"projectType": "npm",
"module": {
"githost": "gitlab.com",
"githost": "code.foss.global",
"gitscope": "push.rocks",
"gitrepo": "lik",
"description": "light little helpers for node",
"description": "Provides a collection of lightweight helpers and utilities for Node.js projects.",
"npmPackagename": "@push.rocks/lik",
"license": "MIT"
"license": "MIT",
"keywords": [
"Utilities",
"Helpers",
"Typescript",
"Node.js",
"String manipulation",
"Object management",
"Execution control",
"Data structures",
"Asynchronous programming",
"Event handling",
"Data aggregation"
]
},
"release": {
"registries": [
"https://verdaccio.lossless.digital",
"https://registry.npmjs.org"
],
"accessLevel": "public"
}
},
"@git.zone/tsdoc": {
"legal": "\n## License and Legal Information\n\nThis 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. \n\n**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.\n\n### Trademarks\n\nThis 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.\n\n### Company Information\n\nTask Venture Capital GmbH \nRegistered at District court Bremen HRB 35230 HB, Germany\n\nFor any legal inquiries or if you require further information, please contact us via email at hello@task.vc.\n\nBy 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.\n"
},
"@git.zone/tsbundle": {
"bundles": [
{
"from": "./ts/index.ts",
"to": "./dist_bundle/bundle.js",
"outputMode": "bundle",
"bundler": "esbuild"
}
]
},
"@ship.zone/szci": {
"npmGlobalTools": []
}
}

View File

@@ -1,42 +1,41 @@
{
"name": "@push.rocks/lik",
"version": "6.0.14",
"version": "6.4.0",
"private": false,
"description": "light little helpers for node",
"description": "Provides a collection of lightweight helpers and utilities for Node.js projects.",
"main": "dist_ts/index.js",
"typings": "dist_ts/index.d.ts",
"type": "module",
"scripts": {
"test": "(tstest test/)",
"build": "(tsbuild --web --allowimplicitany && tsbundle npm)",
"build": "(tsbuild --web --allowimplicitany && tsbundle)",
"buildDocs": "tsdoc"
},
"repository": {
"type": "git",
"url": "git+ssh://git@gitlab.com/pushrocks/lik.git"
"url": "https://code.foss.global/push.rocks/lik.git"
},
"author": "Lossless GmbH",
"license": "MIT",
"bugs": {
"url": "https://gitlab.com/pushrocks/lik/issues"
},
"homepage": "https://gitlab.com/pushrocks/lik#README",
"homepage": "https://code.foss.global/push.rocks/lik",
"devDependencies": {
"@git.zone/tsbuild": "^2.1.66",
"@git.zone/tsbundle": "^2.0.8",
"@git.zone/tsrun": "^1.2.44",
"@git.zone/tstest": "^1.0.77",
"@pushrocks/tapbundle": "^5.0.8",
"@types/node": "^20.9.0"
"@git.zone/tsbuild": "^4.3.0",
"@git.zone/tsbundle": "^2.9.1",
"@git.zone/tsrun": "^2.0.1",
"@git.zone/tstest": "^3.5.0",
"@push.rocks/tapbundle": "^6.0.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/smartrx": "^3.0.7",
"@push.rocks/smarttime": "^4.0.6",
"@types/minimatch": "^5.1.2",
"@types/symbol-tree": "^3.2.5",
"@push.rocks/smartpromise": "^4.2.3",
"@push.rocks/smartrx": "^3.0.10",
"@push.rocks/smarttime": "^4.2.3",
"symbol-tree": "^3.2.4"
},
"files": [
@@ -53,5 +52,26 @@
],
"browserslist": [
"last 1 chrome versions"
]
],
"keywords": [
"Utilities",
"Helpers",
"Typescript",
"Node.js",
"String manipulation",
"Object management",
"Execution control",
"Data structures",
"Asynchronous programming",
"Event handling",
"Data aggregation"
],
"pnpm": {
"onlyBuiltDependencies": [
"esbuild",
"mongodb-memory-server",
"puppeteer"
]
},
"packageManager": "pnpm@10.7.0+sha512.6b865ad4b62a1d9842b61d674a393903b871d9244954f652b8842c2b553c72176b278f64c463e52d40fff8aba385c235c8c9ecf5cc7de4fd78b8bb6d49633ab6"
}

13734
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

1
readme.hints.md Normal file
View File

@@ -0,0 +1 @@

436
readme.md
View File

@@ -1,52 +1,418 @@
# @push.rocks/lik
light little helpers for node
## Availabililty and Links
* [npmjs.org (npm package)](https://www.npmjs.com/package/@push.rocks/lik)
* [gitlab.com (source)](https://gitlab.com/push.rocks/lik)
* [github.com (source mirror)](https://github.com/push.rocks/lik)
* [docs (typedoc)](https://push.rocks.gitlab.io/lik/)
⚡ 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.
## Status for master
## Install
Status Category | Status Badge
-- | --
GitLab Pipelines | [![pipeline status](https://gitlab.com/push.rocks/lik/badges/master/pipeline.svg)](https://lossless.cloud)
GitLab Pipline Test Coverage | [![coverage report](https://gitlab.com/push.rocks/lik/badges/master/coverage.svg)](https://lossless.cloud)
npm | [![npm downloads per month](https://badgen.net/npm/dy/@push.rocks/lik)](https://lossless.cloud)
Snyk | [![Known Vulnerabilities](https://badgen.net/snyk/push.rocks/lik)](https://lossless.cloud)
TypeScript Support | [![TypeScript](https://badgen.net/badge/TypeScript/>=%203.x/blue?icon=typescript)](https://lossless.cloud)
node Support | [![node](https://img.shields.io/badge/node->=%2010.x.x-blue.svg)](https://nodejs.org/dist/latest-v10.x/docs/api/)
Code Style | [![Code Style](https://badgen.net/badge/style/prettier/purple)](https://lossless.cloud)
PackagePhobia (total standalone install weight) | [![PackagePhobia](https://badgen.net/packagephobia/install/@push.rocks/lik)](https://lossless.cloud)
PackagePhobia (package size on registry) | [![PackagePhobia](https://badgen.net/packagephobia/publish/@push.rocks/lik)](https://lossless.cloud)
BundlePhobia (total size when bundled) | [![BundlePhobia](https://badgen.net/bundlephobia/minzip/@push.rocks/lik)](https://lossless.cloud)
```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
Use TypeScript for best in class instellisense.
`@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.
```javascript
// import any tool that you need from lik
import { Stringmap, Objectmap, Observablemap } from 'lik';
---
### 🔒 AsyncExecutionStack
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: 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: 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]);
// 🎚️ 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
```
### class Stringmap
**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).
Stringmap allows you to keep track of strings. It allows you to register triggers for certain events
like when a certain string is removed or added to the map
---
### class Objectmap
### 🌊 BackpressuredArray
Sometimes you need to keep track of objects, but implementing logic for removing, finding or updating is tedious.
Objectmap takes care of keeping track of objects for you.
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.
## Contribution
```typescript
import { BackpressuredArray } from '@push.rocks/lik';
We are always happy for code contributions. If you are not the code contributing type that is ok. Still, maintaining Open Source repositories takes considerable time and thought. If you like the quality of what we do and our modules are useful to you we would appreciate a little monthly contribution: You can [contribute one time](https://lossless.link/contribute-onetime) or [contribute monthly](https://lossless.link/contribute). :)
const buffer = new BackpressuredArray<string>(16); // high water mark
For further information read the linked docs at the top of this readme.
// Producer side
const hasSpace = buffer.push('item1');
if (!hasSpace) {
await buffer.waitForSpace(); // blocks until consumer drains enough
}
## Legal
> MIT licensed | **&copy;** [Task Venture Capital GmbH](https://task.vc)
| By using this npm module you agree to our [privacy policy](https://lossless.gmbH/privacy)
// Consumer side
await buffer.waitForItems(); // blocks until something is available
const item = buffer.shift(); // grab the oldest item
// 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 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<{ name: string; score: number }>();
// 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']
// Force overwrite existing key
map.addToMap('player1', { name: 'Alice', score: 200 }, { force: true });
// 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.score > 95);
// Reset
map.clean();
```
---
### 🎯 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, Response>(
(id) => id, // comparison function for deduplication
{ markLostAfterDefault: 30000 } // auto-destroy unfulfilled interests after 30s
);
// Express interest (returns existing Interest if one matches)
const interest = await interestMap.addInterest('user:42');
// The interest is a promise-like object
interest.interestFullfilled.then((response) => {
console.log('Got it!', response);
});
// Somewhere else — fulfill the interest for everyone waiting
const found = interestMap.findInterest('user:42');
found.fullfillInterest(apiResponse);
// Provide a default fulfillment value (returned if interest is destroyed unfulfilled)
const interest2 = await interestMap.addInterest('user:99', fallbackResponse);
// 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();
```
**Interest lifecycle:** Created → (optionally) `markLost()` starts a 10s destruction timer → `renew()` resets the timer → `fullfillInterest(value)` resolves all waiters → `destroy()` cleans up.
---
### 📏 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 recentLogs = new LimitedArray<string>(100);
recentLogs.addOne('request received');
recentLogs.addMany(['processed', 'response sent']);
console.log(recentLogs.array.length); // never exceeds 100
// 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 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>();
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, 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; }
const users = new ObjectMap<IUser>();
// Auto-keyed add (returns generated key)
const key = users.add({ id: 1, name: 'Alice' });
users.addArray([{ id: 2, name: 'Bob' }, { id: 3, name: 'Carol' }]);
// 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);
// Find and remove in one step
const removed = await users.findOneAndRemove(async (u) => u.id === 3);
const removedSync = users.findOneAndRemoveSync((u) => u.id === 2);
// FIFO-style pop
const first = users.getOneAndRemove();
// 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
// 🔔 Observe mutations
users.eventSubject.subscribe((event) => {
// event.operation: 'add' | 'remove'
// event.payload: the object
});
// 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
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 tags = new Stringmap();
tags.addString('feature:dark-mode');
tags.addStringArray(['feature:i18n', 'bug:login']);
tags.checkString('feature:dark-mode'); // true
tags.checkMinimatch('feature:*'); // true (glob matching!)
tags.checkIsEmpty(); // false
tags.removeString('bug:login');
tags.getStringArray(); // cloned array of current strings
// 🔔 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 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 { TimedAggregator } from '@push.rocks/lik';
// Also available as: import { TimedAggregtor } from '@push.rocks/lik'; (legacy spelling)
const batcher = new TimedAggregator<string>({
aggregationIntervalInMillis: 5000,
functionForAggregation: (batch) => {
console.log(`Processing ${batch.length} items:`, batch);
},
});
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
```
**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.
---
### 🌳 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';
interface INode { name: string; }
const tree = new Tree<INode>();
const root: INode = { name: 'root' };
tree.initialize(root);
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.parent(child1); // root
tree.firstChild(root); // child1
tree.lastChild(root); // child2
tree.nextSibling(child1); // child2
tree.previousSibling(child2); // child1
tree.hasChildren(child1); // true
// 📊 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
// 🔁 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 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 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
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.

View File

@@ -1,4 +1,4 @@
import { tap, expect } from '@pushrocks/tapbundle';
import { tap, expect } from '@push.rocks/tapbundle';
import * as lik from '../ts/index.js';
let testAsyncExecutionStack: lik.AsyncExecutionStack;
@@ -26,4 +26,66 @@ tap.test('should run in parallel', async (toolsArg) => {
}, 0);
});
await tap.start();
// Test default non-exclusive has no concurrency limit property (Infinity)
tap.test('default non-exclusive has no concurrency limit', () => {
const stack = new lik.AsyncExecutionStack();
// default maxConcurrency is Infinity (not finite)
expect(Number.isFinite(stack.getNonExclusiveMaxConcurrency())).toBeFalse();
});
// Test respecting a non-exclusive concurrency limit
tap.test('non-exclusive respects maxConcurrency', async (tools) => {
const stack = new lik.AsyncExecutionStack();
stack.setNonExclusiveMaxConcurrency(2);
const activeCounts: number[] = [];
const tasks: Promise<void>[] = [];
for (let i = 0; i < 5; i++) {
tasks.push(
stack.getNonExclusiveExecutionSlot(async () => {
activeCounts.push(stack.getActiveNonExclusiveCount());
await tools.delayFor(50);
})
);
}
await Promise.all(tasks);
// never more than 2 at once
const maxActive = Math.max(...activeCounts);
expect(maxActive).toBeLessThanOrEqual(2);
});
// Test concurrency stats (active vs pending) for non-exclusive tasks
tap.test('non-exclusive concurrency stats reflect active and pending', async (tools) => {
const stack = new lik.AsyncExecutionStack();
stack.setNonExclusiveMaxConcurrency(2);
// initially, no tasks
expect(stack.getActiveNonExclusiveCount()).toEqual(0);
expect(stack.getPendingNonExclusiveCount()).toEqual(0);
// enqueue four tasks
const p1 = stack.getNonExclusiveExecutionSlot(async () => {
await tools.delayFor(30);
});
const p2 = stack.getNonExclusiveExecutionSlot(async () => {
await tools.delayFor(30);
});
const p3 = stack.getNonExclusiveExecutionSlot(async () => {
await tools.delayFor(30);
});
const p4 = stack.getNonExclusiveExecutionSlot(async () => {
await tools.delayFor(30);
});
// wait for first task to finish and scheduling of next batch
await p1;
await tools.delayFor(0);
// second batch: two active, one pending (4 tasks, limit=2)
expect(stack.getActiveNonExclusiveCount()).toEqual(2);
expect(stack.getPendingNonExclusiveCount()).toEqual(1);
// wait for remaining tasks to complete
await Promise.all([p2, p3, p4]);
// after completion, counts reset
expect(stack.getActiveNonExclusiveCount()).toEqual(0);
expect(stack.getPendingNonExclusiveCount()).toEqual(0);
});
export default tap.start();

View 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();

View File

@@ -1,10 +1,10 @@
import { tap, expect } from '@pushrocks/tapbundle';
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');
});
await tap.start();
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();

View File

@@ -1,4 +1,4 @@
import { tap, expect } from '@pushrocks/tapbundle';
import { tap, expect } from '@push.rocks/tapbundle';
import * as lik from '../ts/index.js';
let testInterestmap: lik.InterestMap<number, number>;
@@ -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);
});
await tap.start();
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();

View File

@@ -1,4 +1,4 @@
import { tap, expect } from '@pushrocks/tapbundle';
import { tap, expect } from '@push.rocks/tapbundle';
import { LimitedArray } from '../ts/index.js';
@@ -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);
});
await tap.start();
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();

View File

@@ -1,5 +1,5 @@
// import test framework
import { expect, tap } from '@pushrocks/tapbundle';
import { expect, tap } from '@push.rocks/tapbundle';
// import the module
import * as lik from '../ts/index.js';
@@ -21,4 +21,14 @@ tap.test('should add objects once and return true', async () => {
expect(myLoopTracker.checkAndTrack(object2)).toBeFalse();
});
await tap.start();
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();

View File

@@ -1,5 +1,5 @@
// import test framework
import { expect, tap } from '@pushrocks/tapbundle';
import { expect, tap } from '@push.rocks/tapbundle';
// import the module
import * as lik from '../ts/index.js';
@@ -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);
});
await tap.start();
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();

View File

@@ -1,5 +1,5 @@
// import test framework
import { expect, tap } from '@pushrocks/tapbundle';
import { expect, tap } from '@push.rocks/tapbundle';
// import the module
import * as lik from '../ts/index.js';
@@ -83,4 +83,4 @@ tap.test('lik.Stringmap.empty() -> should remove wipe and then notify', async ()
testStringmap.wipe();
});
await tap.start();
export default tap.start();

View File

@@ -1,23 +1,81 @@
// import test framework
import { expect, tap } from '@pushrocks/tapbundle';
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();
});
await tap.start();
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();

View File

@@ -1,4 +1,4 @@
import { tap, expect } from '@pushrocks/tapbundle';
import { tap, expect } from '@push.rocks/tapbundle';
import * as lik from '../ts/index.js';
class TestClass {
@@ -33,8 +33,119 @@ tap.test('should add other objects in a hierachy', async () => {
testTree.appendChild(testInstance, testInstance4);
});
tap.test("should create a JSON object that reflects a tree's hierachy", async () => {
const jsonTreet = testTree.toJsonWithHierachy(testInstance);
tap.test('hasChildren returns correct value', async () => {
expect(testTree.hasChildren(testInstance)).toBeTrue();
expect(testTree.hasChildren(testInstance2)).toBeFalse();
});
await tap.start();
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 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();

View File

@@ -1,8 +1,8 @@
/**
* autocreated commitinfo by @pushrocks/commitinfo
* autocreated commitinfo by @push.rocks/commitinfo
*/
export const commitinfo = {
name: '@push.rocks/lik',
version: '6.0.14',
description: 'light little helpers for node'
version: '6.4.0',
description: 'Provides a collection of lightweight helpers and utilities for Node.js projects.'
}

View File

@@ -10,6 +10,12 @@ interface IExecutionSlot<T> {
export class AsyncExecutionStack {
private executionSlots: IExecutionSlot<any>[] = [];
private isProcessing = false;
/** Maximum concurrent non-exclusive tasks (Infinity = unlimited) */
private nonExclusiveMaxConcurrency: number = Infinity;
/** Currently running non-exclusive task count */
private nonExclusiveCurrentCount: number = 0;
/** Queue of resolvers waiting for a non-exclusive slot */
private nonExclusivePendingQueue: Array<() => void> = [];
public async getExclusiveExecutionSlot<T = any>(
funcArg: () => Promise<T>,
@@ -42,6 +48,28 @@ export class AsyncExecutionStack {
this.processExecutionSlots();
return executionDeferred.promise;
}
/**
* Set the maximum number of concurrent non-exclusive tasks.
* @param concurrency minimum 1 (Infinity means unlimited)
*/
public setNonExclusiveMaxConcurrency(concurrency: number): void {
if (!Number.isFinite(concurrency) || concurrency < 1) {
throw new Error('nonExclusiveMaxConcurrency must be a finite number >= 1');
}
this.nonExclusiveMaxConcurrency = concurrency;
}
/** Get the configured max concurrency for non-exclusive tasks */
public getNonExclusiveMaxConcurrency(): number {
return this.nonExclusiveMaxConcurrency;
}
/** Number of non-exclusive tasks currently running */
public getActiveNonExclusiveCount(): number {
return this.nonExclusiveCurrentCount;
}
/** Number of non-exclusive tasks waiting for a free slot */
public getPendingNonExclusiveCount(): number {
return this.nonExclusivePendingQueue.length;
}
private async processExecutionSlots() {
if (this.isProcessing) {
@@ -69,13 +97,20 @@ export class AsyncExecutionStack {
private async executeExclusiveSlot(slot: IExecutionSlot<any>) {
try {
if (slot.timeout) {
const result = await Promise.race([
slot.funcToExecute(),
plugins.smartdelay.delayFor(slot.timeout).then(() => {
throw new Error('Timeout reached');
}),
]);
slot.executionDeferred.resolve(result);
const timeoutInstance = new plugins.smartdelay.Timeout(slot.timeout);
try {
const result = await Promise.race([
slot.funcToExecute(),
timeoutInstance.promise.then(() => {
throw new Error('Timeout reached');
}),
]);
timeoutInstance.cancel();
slot.executionDeferred.resolve(result);
} catch (error) {
timeoutInstance.cancel();
throw error;
}
} else {
const result = await slot.funcToExecute();
slot.executionDeferred.resolve(result);
@@ -87,24 +122,56 @@ export class AsyncExecutionStack {
private async executeNonExclusiveSlots(slots: IExecutionSlot<any>[]) {
const promises = slots.map(async (slot) => {
// wait for an available non-exclusive slot
await this.waitForNonExclusiveSlot();
try {
// execute with optional timeout
if (slot.timeout) {
const result = await Promise.race([
slot.funcToExecute(),
plugins.smartdelay.delayFor(slot.timeout).then(() => {
throw new Error('Timeout reached');
}),
]);
slot.executionDeferred.resolve(result);
const timeoutInstance = new plugins.smartdelay.Timeout(slot.timeout);
try {
const result = await Promise.race([
slot.funcToExecute(),
timeoutInstance.promise.then(() => { throw new Error('Timeout reached'); }),
]);
timeoutInstance.cancel();
slot.executionDeferred.resolve(result);
} catch (error) {
timeoutInstance.cancel();
throw error;
}
} else {
const result = await slot.funcToExecute();
slot.executionDeferred.resolve(result);
}
} catch (error) {
slot.executionDeferred.reject(error);
} finally {
this.releaseNonExclusiveSlot();
}
});
await Promise.all(promises);
}
/**
* Wait until a non-exclusive slot is available (respects max concurrency).
*/
private waitForNonExclusiveSlot(): Promise<void> {
if (this.nonExclusiveCurrentCount < this.nonExclusiveMaxConcurrency) {
this.nonExclusiveCurrentCount++;
return Promise.resolve();
}
return new Promise((resolve) => {
this.nonExclusivePendingQueue.push(() => {
this.nonExclusiveCurrentCount++;
resolve();
});
});
}
/** Release a non-exclusive slot and wake the next waiter, if any. */
private releaseNonExclusiveSlot(): void {
this.nonExclusiveCurrentCount--;
const next = this.nonExclusivePendingQueue.shift();
if (next) {
next();
}
}
}

View File

@@ -5,16 +5,21 @@ export class BackpressuredArray<T> {
private highWaterMark: number;
public hasSpace = new plugins.smartrx.rxjs.Subject<'hasSpace'>();
private itemsAvailable = new plugins.smartrx.rxjs.Subject<'itemsAvailable'>();
private isDestroyed = false;
constructor(highWaterMark: number = 16) {
this.data = [];
this.highWaterMark = highWaterMark;
}
public get length(): number {
return this.data.length;
}
push(item: T): boolean {
this.data.push(item);
this.itemsAvailable.next('itemsAvailable');
const spaceAvailable = this.checkSpaceAvailable();
if (spaceAvailable) {
this.hasSpace.next('hasSpace');
@@ -22,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()) {
@@ -30,18 +42,31 @@ export class BackpressuredArray<T> {
return item;
}
peek(): T | undefined {
return this.data[0];
}
checkSpaceAvailable(): boolean {
return this.data.length < this.highWaterMark;
}
public checkHasItems(): boolean {
return this.data.length > 0;
}
waitForSpace(): Promise<void> {
return new Promise<void>((resolve) => {
if (this.checkSpaceAvailable()) {
if (this.checkSpaceAvailable() || this.isDestroyed) {
resolve();
} else {
const subscription = this.hasSpace.subscribe(() => {
subscription.unsubscribe();
resolve();
const subscription = this.hasSpace.subscribe({
next: () => {
subscription.unsubscribe();
resolve();
},
complete: () => {
resolve();
},
});
}
});
@@ -49,14 +74,32 @@ export class BackpressuredArray<T> {
waitForItems(): Promise<void> {
return new Promise<void>((resolve) => {
if (this.data.length > 0) {
if (this.data.length > 0 || this.isDestroyed) {
resolve();
} else {
const subscription = this.itemsAvailable.subscribe(() => {
subscription.unsubscribe();
resolve();
const subscription = this.itemsAvailable.subscribe({
next: () => {
subscription.unsubscribe();
resolve();
},
complete: () => {
resolve();
},
});
}
});
}
public [Symbol.iterator](): Iterator<T> {
return this.data[Symbol.iterator]();
}
/**
* destroys the BackpressuredArray, completing all subjects
*/
public destroy() {
this.isDestroyed = true;
this.hasSpace.complete();
this.itemsAvailable.complete();
}
}

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

@@ -15,12 +15,18 @@ export class Interest<DTInterestId, DTInterestFullfillment> {
public comparisonFunc: IInterestComparisonFunc<DTInterestId>;
public destructionTimer = new plugins.smarttime.Timer(10000);
public isFullfilled = false;
private isDestroyed = false;
/**
* a generic store to store objects in that are needed for fullfillment;
*/
public fullfillmentStore: any[] = [];
/**
* a cancellable timeout for the markLostAfterDefault feature
*/
private markLostTimeout: InstanceType<typeof plugins.smartdelay.Timeout> | null = null;
/**
* quick access to a string that makes the interest comparable for checking for similar interests
*/
@@ -39,11 +45,9 @@ export class Interest<DTInterestId, DTInterestFullfillment> {
this.isFullfilled = true;
this.fullfillmentStore = [];
this.interestDeferred.resolve(objectArg);
this.destroy();
}
/**
*
*/
constructor(
interestMapArg: InterestMap<DTInterestId, DTInterestFullfillment>,
interestArg: DTInterestId,
@@ -56,10 +60,17 @@ export class Interest<DTInterestId, DTInterestFullfillment> {
this.options = optionsArg;
this.destructionTimer.completed.then(() => {
this.destroy();
if (!this.isDestroyed) {
this.destroy();
}
});
if (this.options?.markLostAfterDefault) {
plugins.smartdelay.delayFor(this.options.markLostAfterDefault).then(this.markLost);
this.markLostTimeout = new plugins.smartdelay.Timeout(this.options.markLostAfterDefault);
this.markLostTimeout.promise.then(() => {
if (!this.isDestroyed) {
this.markLost();
}
});
}
}
@@ -71,9 +82,28 @@ export class Interest<DTInterestId, DTInterestFullfillment> {
* self destructs the interest
*/
public destroy() {
if (this.isDestroyed) {
return;
}
this.isDestroyed = true;
// Cancel timers to release references
this.destructionTimer.reset();
if (this.markLostTimeout) {
this.markLostTimeout.cancel();
this.markLostTimeout = null;
}
// Clear the fulfillment store
this.fullfillmentStore = [];
// Remove from the InterestMap
this.interestMapRef.removeInterest(this);
if (!this.isFullfilled && this.options.defaultFullfillment) {
this.fullfillInterest(this.options.defaultFullfillment);
// Fulfill with default if not yet fulfilled (inlined to avoid mutual recursion)
if (!this.isFullfilled && this.options?.defaultFullfillment) {
this.isFullfilled = true;
this.interestDeferred.resolve(this.options.defaultFullfillment);
}
}

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,27 +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;
const existingInterest = this.interestsByComparisonString.get(comparisonString);
if (existingInterest) {
returnInterest = existingInterest;
returnInterest.renew();
} else {
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;
@@ -81,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);
}
/**
@@ -99,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);
}
/**
@@ -126,9 +121,19 @@ 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;
}
/**
* destroys the InterestMap and cleans up all resources
*/
public destroy() {
const interests = this.interestObjectMap.getArray();
for (const interest of interests) {
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

@@ -20,4 +20,18 @@ export class LoopTracker<T> {
return false;
}
}
/**
* resets the loop tracker, clearing all tracked objects
*/
public reset() {
this.referenceObjectMap.wipe();
}
/**
* destroys the loop tracker and its underlying ObjectMap
*/
public destroy() {
this.referenceObjectMap.destroy();
}
}

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);
}
/**
@@ -62,25 +71,28 @@ export class ObjectMap<T> {
* remove key
* @param functionArg
*/
public removeMappedUnique(uniqueKey: string) {
const object = this.getMappedUnique(uniqueKey);
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,
});
}
return object;
}
/**
* 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({
@@ -103,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;
}
/**
@@ -174,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,
@@ -186,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,
@@ -220,8 +221,14 @@ export class ObjectMap<T> {
* wipe Objectmap
*/
public wipe() {
for (const keyArg of this.fastMap.getKeys()) {
this.fastMap.removeFromMap(keyArg);
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,
});
}
}
@@ -232,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;
}
@@ -242,5 +253,34 @@ 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]();
}
/**
* destroys the ObjectMap, completing the eventSubject and clearing all entries
*/
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();
}
@@ -116,4 +112,12 @@ export class Stringmap {
});
this._triggerUntilTrueFunctionArray = filteredArray;
}
/**
* destroys the Stringmap, clearing all strings and pending triggers
*/
public destroy() {
this._stringArray = [];
this._triggerUntilTrueFunctionArray = [];
}
}

View File

@@ -8,6 +8,7 @@ export interface ITimedAggregatorOptions<T> {
export class TimedAggregtor<T> {
public options: ITimedAggregatorOptions<T>;
private storageArray: T[] = [];
private isStopped = false;
constructor(optionsArg: ITimedAggregatorOptions<T>) {
this.options = optionsArg;
@@ -15,9 +16,16 @@ export class TimedAggregtor<T> {
private aggregationTimer: plugins.smarttime.Timer;
private checkAggregationStatus() {
if (this.isStopped) {
return;
}
const addAggregationTimer = () => {
this.aggregationTimer = new plugins.smarttime.Timer(this.options.aggregationIntervalInMillis);
this.aggregationTimer.completed.then(() => {
if (this.isStopped) {
this.aggregationTimer = null;
return;
}
const aggregateForProcessing = this.storageArray;
if (aggregateForProcessing.length === 0) {
this.aggregationTimer = null;
@@ -35,7 +43,36 @@ export class TimedAggregtor<T> {
}
public add(aggregationArg: T) {
if (this.isStopped) {
return;
}
this.storageArray.push(aggregationArg);
this.checkAggregationStatus();
}
/**
* stops the aggregation timer chain
* @param flushRemaining if true, calls functionForAggregation with any remaining items
*/
public stop(flushRemaining: boolean = false) {
this.isStopped = true;
if (this.aggregationTimer) {
this.aggregationTimer.reset();
this.aggregationTimer = null;
}
if (flushRemaining && this.storageArray.length > 0) {
const remaining = this.storageArray;
this.storageArray = [];
this.options.functionForAggregation(remaining);
} else {
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 {
@@ -95,7 +95,7 @@ export class Tree<T> {
}
compareTreePosition(leftArg: T, rightArg: T): number {
return this.compareTreePosition(leftArg, rightArg);
return this.symbolTree.compareTreePosition(leftArg, rightArg);
}
remove(removeObjectArg: T): T {
@@ -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';

View File

@@ -1,10 +1,12 @@
{
"compilerOptions": {
"experimentalDecorators": true,
"useDefineForClassFields": false,
"target": "ES2022",
"module": "ES2022",
"moduleResolution": "nodenext",
"esModuleInterop": true
}
"module": "NodeNext",
"moduleResolution": "NodeNext",
"esModuleInterop": true,
"verbatimModuleSyntax": true
},
"exclude": [
"dist_*/**/*.d.ts"
]
}