Compare commits

..

4 Commits

Author SHA1 Message Date
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
15 changed files with 3756 additions and 1319 deletions

View File

@@ -1,5 +1,27 @@
# Changelog # Changelog
## 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) ## 2026-03-01 - 6.2.3 - fix(interestmap)
remove interest from InterestMap immediately after fulfillment remove interest from InterestMap immediately after fulfillment

View File

@@ -1,9 +1,5 @@
{ {
"npmci": { "@git.zone/cli": {
"npmGlobalTools": [],
"npmAccessLevel": "public"
},
"gitzone": {
"projectType": "npm", "projectType": "npm",
"module": { "module": {
"githost": "code.foss.global", "githost": "code.foss.global",
@@ -25,9 +21,29 @@
"Event handling", "Event handling",
"Data aggregation" "Data aggregation"
] ]
},
"release": {
"registries": [
"https://verdaccio.lossless.digital",
"https://registry.npmjs.org"
],
"accessLevel": "public"
} }
}, },
"tsdoc": { "@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" "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,6 +1,6 @@
{ {
"name": "@push.rocks/lik", "name": "@push.rocks/lik",
"version": "6.2.3", "version": "6.3.1",
"private": false, "private": false,
"description": "Provides a collection of lightweight helpers and utilities for Node.js projects.", "description": "Provides a collection of lightweight helpers and utilities for Node.js projects.",
"main": "dist_ts/index.js", "main": "dist_ts/index.js",
@@ -8,7 +8,7 @@
"type": "module", "type": "module",
"scripts": { "scripts": {
"test": "(tstest test/)", "test": "(tstest test/)",
"build": "(tsbuild --web --allowimplicitany && tsbundle npm)", "build": "(tsbuild --web --allowimplicitany && tsbundle)",
"buildDocs": "tsdoc" "buildDocs": "tsdoc"
}, },
"repository": { "repository": {
@@ -22,19 +22,19 @@
}, },
"homepage": "https://code.foss.global/push.rocks/lik", "homepage": "https://code.foss.global/push.rocks/lik",
"devDependencies": { "devDependencies": {
"@git.zone/tsbuild": "^2.1.72", "@git.zone/tsbuild": "^4.1.2",
"@git.zone/tsbundle": "^2.0.15", "@git.zone/tsbundle": "^2.9.0",
"@git.zone/tsrun": "^1.2.44", "@git.zone/tsrun": "^2.0.1",
"@git.zone/tstest": "^1.0.90", "@git.zone/tstest": "^3.1.8",
"@push.rocks/tapbundle": "^5.5.6", "@push.rocks/tapbundle": "^6.0.3",
"@types/node": "^22.13.9" "@types/node": "^25.3.3"
}, },
"dependencies": { "dependencies": {
"@push.rocks/smartdelay": "^3.0.5", "@push.rocks/smartdelay": "^3.0.5",
"@push.rocks/smartmatch": "^2.0.0", "@push.rocks/smartmatch": "^2.0.0",
"@push.rocks/smartpromise": "^4.0.3", "@push.rocks/smartpromise": "^4.0.3",
"@push.rocks/smartrx": "^3.0.7", "@push.rocks/smartrx": "^3.0.10",
"@push.rocks/smarttime": "^4.0.6", "@push.rocks/smarttime": "^4.2.3",
"@types/minimatch": "^5.1.2", "@types/minimatch": "^5.1.2",
"@types/symbol-tree": "^3.2.5", "@types/symbol-tree": "^3.2.5",
"symbol-tree": "^3.2.4" "symbol-tree": "^3.2.4"

4388
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

394
readme.md
View File

@@ -1,193 +1,265 @@
# @push.rocks/lik # @push.rocks/lik
light little helpers for node
A collection of lightweight utility classes for TypeScript/Node.js projects, providing efficient data structures and async helpers.
## Install ## Install
To install `@push.rocks/lik`, you can use npm (Node Package Manager). Simply run the following command in your terminal:
```bash ```bash
npm install @push.rocks/lik --save npm install @push.rocks/lik
``` ```
This will download `@push.rocks/lik` and add it to your project's `package.json` dependencies.
## Usage ## Usage
`@push.rocks/lik` is a versatile package offering a variety of helper classes designed to simplify common tasks in Node.js development. From managing asynchronous operations to handling collections efficiently, this library provides lightweight solutions to enhance your coding workflow. Below, we explore several key features of `@push.rocks/lik`, presenting TypeScript examples to demonstrate their practical application in real-world scenarios. `@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.
### AsyncExecutionStack ### AsyncExecutionStack
`AsyncExecutionStack` provides controlled execution of asynchronous tasks in two modes: Controls execution of asynchronous tasks in two modes:
- **Exclusive tasks**: run one at a time in insertion order, blocking all other tasks until complete. - **Exclusive**: tasks run one at a time, blocking all others until complete.
- **Non-exclusive tasks**: run in parallel, up to an optional concurrency limit. - **Non-exclusive**: tasks run in parallel with optional concurrency limits.
Create a stack:
```typescript ```typescript
import { AsyncExecutionStack } from '@push.rocks/lik'; import { AsyncExecutionStack } from '@push.rocks/lik';
const stack = new AsyncExecutionStack(); const stack = new AsyncExecutionStack();
```
Exclusive tasks execute sequentially and block subsequent tasks (with an optional timeout): // Exclusive execution (sequential, blocks other tasks)
await stack.getExclusiveExecutionSlot(async () => {
// critical section work
}, 5000); // optional timeout in ms
```typescript // Non-exclusive execution (parallel)
await stack.getExclusiveExecutionSlot(
async () => {
// exclusive work
},
5000 // optional timeout in milliseconds
);
```
Non-exclusive tasks run in parallel up to the concurrency limit (default: unlimited). Each call returns a promise you can await:
```typescript
const p1 = stack.getNonExclusiveExecutionSlot(async () => { const p1 = stack.getNonExclusiveExecutionSlot(async () => {
// work 1 // concurrent work 1
}, 2000); });
const p2 = stack.getNonExclusiveExecutionSlot(async () => { const p2 = stack.getNonExclusiveExecutionSlot(async () => {
// work 2 // concurrent work 2
}, 2000); });
await Promise.all([p1, p2]); await Promise.all([p1, p2]);
// Control concurrency
stack.setNonExclusiveMaxConcurrency(3);
console.log(stack.getNonExclusiveMaxConcurrency()); // 3
console.log(stack.getActiveNonExclusiveCount()); // currently running
console.log(stack.getPendingNonExclusiveCount()); // waiting for slots
``` ```
Control concurrency and inspect status: ### BackpressuredArray
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.
```typescript ```typescript
// Set maximum concurrent non-exclusive tasks (minimum 1) import { BackpressuredArray } from '@push.rocks/lik';
stack.setNonExclusiveMaxConcurrency(3);
console.log(stack.getNonExclusiveMaxConcurrency()); // 3 const buffer = new BackpressuredArray<string>(16); // high water mark of 16
console.log(stack.getActiveNonExclusiveCount()); // currently running tasks
console.log(stack.getPendingNonExclusiveCount()); // tasks waiting for slots // Producer: push items, returns false when full
const hasSpace = buffer.push('item1');
if (!hasSpace) {
await buffer.waitForSpace(); // wait until consumer frees space
}
// Consumer: shift items out
await buffer.waitForItems(); // wait until items are available
const item = buffer.shift();
// Check state
buffer.checkSpaceAvailable(); // true if below high water mark
buffer.checkHasItems(); // true if items exist
``` ```
### FastMap ### FastMap
`FastMap` offers a high-performance, key-value map optimized for rapid access and modifications, ideal for scenarios requiring frequent read/write operations. A high-performance key-value map optimized for rapid lookups and modifications.
```typescript ```typescript
import { FastMap } from '@push.rocks/lik'; import { FastMap } from '@push.rocks/lik';
const myFastMap = new FastMap<string>(); const map = new FastMap<string>();
// Add items map.addToMap('key1', 'value1');
myFastMap.addToMap('key1', 'value1'); map.addToMap('key2', 'value2');
myFastMap.addToMap('key2', 'value2');
// Retrieve item const value = map.getByKey('key1'); // 'value1'
const value = myFastMap.getByKey('key1'); // 'value1' map.isUniqueKey('key1'); // false (already exists)
map.removeFromMap('key1');
// Merge maps
const otherMap = new FastMap<string>();
otherMap.addToMap('key3', 'value3');
const merged = map.concat(otherMap);
// Or merge in place
map.addAllFromOther(otherMap);
// Async find
const found = await map.find(async (item) => item === 'value2');
``` ```
### LimitedArray ### InterestMap
`LimitedArray` enforces a maximum size on an array, automatically managing its length to prevent it from exceeding a defined limit. Manages subscriptions/interests in events or entities. Multiple parties can express interest in the same thing; the interest is deduplicated and fulfilled once.
```typescript
import { LimitedArray } from '@push.rocks/lik';
const myLimitedArray = new LimitedArray<number>(5); // limit size to 5
myLimitedArray.addMany([1, 2, 3, 4, 5, 6]);
console.log(myLimitedArray.array); // [2, 3, 4, 5, 6]
```
### LoopTracker
`LoopTracker` helps detect and prevent infinite loops by tracking object references during iterations.
```typescript
import { LoopTracker } from '@push.rocks/lik';
const myLoopTracker = new LoopTracker<object>();
const obj1 = {};
if (myLoopTracker.checkAndTrack(obj1)) {
// proceed with operation
} else {
// potential loop detected
}
```
### ObjectMap
`ObjectMap` facilitates object management, providing functionalities for adding, finding, and removing objects with ease.
```typescript
import { ObjectMap } from '@push.rocks/lik';
interface MyObject {
id: number;
name: string;
}
const myObjectMap = new ObjectMap<MyObject>();
const obj: MyObject = { id: 1, name: 'Test Object' };
// Add object
myObjectMap.add(obj);
// Find object
const found = myObjectMap.findSync(item => item.id === 1);
```
### StringMap
`StringMap` simplifies string collection management, allowing you to add, remove, and query strings effectively.
```typescript
import { Stringmap } from '@push.rocks/lik';
const myStringMap = new Stringmap();
// Add strings
myStringMap.addString('hello');
myStringMap.addStringArray(['world', 'example']);
// Check string presence
const exists = myStringMap.checkString('hello'); // true
```
### TimedAggregator
`TimedAggregator` batches operations over a specified time interval, useful for aggregating logs, metrics, or any data points over time.
```typescript
import { TimedAggregtor } from '@push.rocks/lik';
const myAggregator = new TimedAggregtor<string>({
aggregationIntervalInMillis: 5000, // 5 seconds
functionForAggregation: (items) => {
console.log('Aggregated items:', items);
}
});
// Add items
myAggregator.add('item1');
myAggregator.add('item2');
// After 5 seconds, the functionForAggregation will log the aggregated items
```
### InterestMap and Tree
`InterestMap` provides a structure for managing subscriptions or interests in certain events or entities, optimizing notification mechanisms.
```typescript ```typescript
import { InterestMap } from '@push.rocks/lik'; import { InterestMap } from '@push.rocks/lik';
const myInterestMap = new InterestMap<string, number>((str) => str); const interestMap = new InterestMap<string, number>(
(str) => str, // comparison function to deduplicate interests
{ markLostAfterDefault: 30000 } // optional: auto-mark lost after 30s
);
myInterestMap.addInterest('event1').then((interest) => { // Express interest
interest.fullfillInterest(42); const interest = await interestMap.addInterest('event1');
// Wait for fulfillment
interest.interestFullfilled.then((result) => {
console.log('Got result:', result);
}); });
// Fulfill from elsewhere
const found = interestMap.findInterest('event1');
found.fullfillInterest(42);
// Check and manage interests
interestMap.checkInterest('event1'); // true/false
interestMap.informLostInterest('event1'); // starts destruction timer
// Observable stream of new interests
interestMap.interestObservable; // ObservableIntake<Interest>
``` ```
`Tree` offers a way to handle hierarchical data structures, allowing for the composition and traversal of tree-like data sets. ### LimitedArray
An array that automatically enforces a maximum size, discarding oldest items when the limit is exceeded.
```typescript
import { LimitedArray } from '@push.rocks/lik';
const arr = new LimitedArray<number>(5);
arr.addMany([1, 2, 3, 4, 5, 6]);
console.log(arr.array.length); // 5 (oldest items dropped)
arr.addOne(7);
arr.setLimit(3); // dynamically adjust limit
// Compute average (for numeric arrays)
const numArr = new LimitedArray<number>(10);
numArr.addMany([10, 20, 30]);
console.log(numArr.getAverage()); // 20
```
### LoopTracker
Detects and prevents infinite loops by tracking object references during iterations.
```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!)
```
### ObjectMap
A managed collection of objects with add/remove/find operations and event notifications via RxJS.
```typescript
import { ObjectMap } from '@push.rocks/lik';
interface IUser {
id: number;
name: string;
}
const users = new ObjectMap<IUser>();
// Add objects
const key = users.add({ id: 1, name: 'Alice' });
users.addArray([{ id: 2, name: 'Bob' }, { id: 3, name: 'Carol' }]);
// Find objects
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);
// Direct add/get by unique key
users.addMappedUnique('admin', { id: 99, name: 'Admin' });
const admin = users.getMappedUnique('admin');
// Get one and remove (FIFO-style)
const first = users.getOneAndRemove();
// Iterate, check, and manage
await users.forEach((u) => console.log(u.name));
users.checkForObject(alice); // true/false
users.isEmpty(); // true/false
users.getArray(); // cloned array of all objects
users.wipe(); // remove all
// Listen for changes
users.eventSubject.subscribe((event) => {
console.log(event.operation, event.payload); // 'add' | 'remove'
});
// Merge object maps
const merged = users.concat(otherObjectMap);
users.addAllFromOther(otherObjectMap);
```
### Stringmap
Manages a collection of strings with add/remove/query operations and minimatch pattern matching.
```typescript
import { Stringmap } from '@push.rocks/lik';
const strings = new Stringmap();
strings.addString('hello');
strings.addStringArray(['world', 'example']);
strings.checkString('hello'); // true
strings.checkMinimatch('hel*'); // true (glob matching)
strings.checkIsEmpty(); // false
strings.removeString('hello');
strings.getStringArray(); // ['world', 'example']
// Register trigger that fires when condition is met
await strings.registerUntilTrue((arr) => arr.length === 0);
strings.wipe(); // triggers the above
```
### TimedAggregator
Batches items over a time interval, then processes them in bulk. Useful for aggregating logs, metrics, or events.
```typescript
import { TimedAggregtor } from '@push.rocks/lik';
const aggregator = new TimedAggregtor<string>({
aggregationIntervalInMillis: 5000,
functionForAggregation: (items) => {
console.log('Batch:', items);
},
});
aggregator.add('event1');
aggregator.add('event2');
// After 5 seconds: Batch: ['event1', 'event2']
```
### Tree
A typed wrapper around `symbol-tree` for managing hierarchical data structures with parent/child/sibling relationships.
```typescript ```typescript
import { Tree } from '@push.rocks/lik'; import { Tree } from '@push.rocks/lik';
@@ -196,25 +268,33 @@ class TreeNode {
constructor(public value: string) {} constructor(public value: string) {}
} }
const myTree = new Tree<TreeNode>(); const tree = new Tree<TreeNode>();
const rootNode = new TreeNode('root'); const root = new TreeNode('root');
myTree.initialize(rootNode); tree.initialize(root);
// Add child nodes const child1 = new TreeNode('child1');
const childNode = new TreeNode('child'); const child2 = new TreeNode('child2');
myTree.appendChild(rootNode, childNode); tree.appendChild(root, child1);
tree.appendChild(root, child2);
// Navigate
tree.hasChildren(root); // true
tree.firstChild(root); // child1
tree.lastChild(root); // child2
tree.nextSibling(child1); // child2
tree.parent(child1); // root
// Query
tree.childrenCount(root); // 2
tree.index(child2); // 1
tree.childrenToArray(root, {}); // [child1, child2]
tree.treeToArray(root, {}); // full tree as array
// Mutate
tree.insertBefore(child2, new TreeNode('between'));
tree.remove(child2);
``` ```
### Utilizing @push.rocks/lik in Your Project
With `@push.rocks/lik`, you gain access to a comprehensive set of lightweight utilities that can significantly simplify and expedite the development process in Node.js environments. By leveraging the library's classes and functions, you can implement efficient data structures, manage asynchronous operations gracefully, and streamline complex logic with ease.
By integrating `@push.rocks/lik` into your project, you'll benefit from improved code clarity, reduced boilerplate, and enhanced performance, allowing you to focus on developing the core functionalities of your application. Whether you're managing various collections, executing asynchronous tasks in controlled manners, or dealing with hierarchical data, `@push.rocks/lik` provides the tools you need to achieve your objectives with minimal overhead.
Remember, continuous exploration of `@push.rocks/lik`'s capabilities and experimenting with its various components in different scenarios will help you unlock its full potential. As your familiarity with the library grows, you'll discover even more ways to optimize your codebase and streamline your development workflow.
## License and Legal Information ## 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 that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository.

View File

@@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@push.rocks/lik', name: '@push.rocks/lik',
version: '6.2.3', version: '6.3.1',
description: 'Provides a collection of lightweight helpers and utilities for Node.js projects.' description: 'Provides a collection of lightweight helpers and utilities for Node.js projects.'
} }

View File

@@ -97,13 +97,20 @@ export class AsyncExecutionStack {
private async executeExclusiveSlot(slot: IExecutionSlot<any>) { private async executeExclusiveSlot(slot: IExecutionSlot<any>) {
try { try {
if (slot.timeout) { if (slot.timeout) {
const result = await Promise.race([ const timeoutInstance = new plugins.smartdelay.Timeout(slot.timeout);
slot.funcToExecute(), try {
plugins.smartdelay.delayFor(slot.timeout).then(() => { const result = await Promise.race([
throw new Error('Timeout reached'); slot.funcToExecute(),
}), timeoutInstance.promise.then(() => {
]); throw new Error('Timeout reached');
slot.executionDeferred.resolve(result); }),
]);
timeoutInstance.cancel();
slot.executionDeferred.resolve(result);
} catch (error) {
timeoutInstance.cancel();
throw error;
}
} else { } else {
const result = await slot.funcToExecute(); const result = await slot.funcToExecute();
slot.executionDeferred.resolve(result); slot.executionDeferred.resolve(result);
@@ -120,11 +127,18 @@ export class AsyncExecutionStack {
try { try {
// execute with optional timeout // execute with optional timeout
if (slot.timeout) { if (slot.timeout) {
const result = await Promise.race([ const timeoutInstance = new plugins.smartdelay.Timeout(slot.timeout);
slot.funcToExecute(), try {
plugins.smartdelay.delayFor(slot.timeout).then(() => { throw new Error('Timeout reached'); }), const result = await Promise.race([
]); slot.funcToExecute(),
slot.executionDeferred.resolve(result); timeoutInstance.promise.then(() => { throw new Error('Timeout reached'); }),
]);
timeoutInstance.cancel();
slot.executionDeferred.resolve(result);
} catch (error) {
timeoutInstance.cancel();
throw error;
}
} else { } else {
const result = await slot.funcToExecute(); const result = await slot.funcToExecute();
slot.executionDeferred.resolve(result); slot.executionDeferred.resolve(result);

View File

@@ -5,6 +5,7 @@ export class BackpressuredArray<T> {
private highWaterMark: number; private highWaterMark: number;
public hasSpace = new plugins.smartrx.rxjs.Subject<'hasSpace'>(); public hasSpace = new plugins.smartrx.rxjs.Subject<'hasSpace'>();
private itemsAvailable = new plugins.smartrx.rxjs.Subject<'itemsAvailable'>(); private itemsAvailable = new plugins.smartrx.rxjs.Subject<'itemsAvailable'>();
private isDestroyed = false;
constructor(highWaterMark: number = 16) { constructor(highWaterMark: number = 16) {
this.data = []; this.data = [];
@@ -40,12 +41,17 @@ export class BackpressuredArray<T> {
waitForSpace(): Promise<void> { waitForSpace(): Promise<void> {
return new Promise<void>((resolve) => { return new Promise<void>((resolve) => {
if (this.checkSpaceAvailable()) { if (this.checkSpaceAvailable() || this.isDestroyed) {
resolve(); resolve();
} else { } else {
const subscription = this.hasSpace.subscribe(() => { const subscription = this.hasSpace.subscribe({
subscription.unsubscribe(); next: () => {
resolve(); subscription.unsubscribe();
resolve();
},
complete: () => {
resolve();
},
}); });
} }
}); });
@@ -53,14 +59,28 @@ export class BackpressuredArray<T> {
waitForItems(): Promise<void> { waitForItems(): Promise<void> {
return new Promise<void>((resolve) => { return new Promise<void>((resolve) => {
if (this.data.length > 0) { if (this.data.length > 0 || this.isDestroyed) {
resolve(); resolve();
} else { } else {
const subscription = this.itemsAvailable.subscribe(() => { const subscription = this.itemsAvailable.subscribe({
subscription.unsubscribe(); next: () => {
resolve(); subscription.unsubscribe();
resolve();
},
complete: () => {
resolve();
},
}); });
} }
}); });
} }
/**
* destroys the BackpressuredArray, completing all subjects
*/
public destroy() {
this.isDestroyed = true;
this.hasSpace.complete();
this.itemsAvailable.complete();
}
} }

View File

@@ -15,12 +15,18 @@ export class Interest<DTInterestId, DTInterestFullfillment> {
public comparisonFunc: IInterestComparisonFunc<DTInterestId>; public comparisonFunc: IInterestComparisonFunc<DTInterestId>;
public destructionTimer = new plugins.smarttime.Timer(10000); public destructionTimer = new plugins.smarttime.Timer(10000);
public isFullfilled = false; public isFullfilled = false;
private isDestroyed = false;
/** /**
* a generic store to store objects in that are needed for fullfillment; * a generic store to store objects in that are needed for fullfillment;
*/ */
public fullfillmentStore: any[] = []; 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 * quick access to a string that makes the interest comparable for checking for similar interests
*/ */
@@ -39,12 +45,9 @@ export class Interest<DTInterestId, DTInterestFullfillment> {
this.isFullfilled = true; this.isFullfilled = true;
this.fullfillmentStore = []; this.fullfillmentStore = [];
this.interestDeferred.resolve(objectArg); this.interestDeferred.resolve(objectArg);
this.destroy(); // Remove from InterestMap immediately after fulfillment this.destroy();
} }
/**
*
*/
constructor( constructor(
interestMapArg: InterestMap<DTInterestId, DTInterestFullfillment>, interestMapArg: InterestMap<DTInterestId, DTInterestFullfillment>,
interestArg: DTInterestId, interestArg: DTInterestId,
@@ -57,10 +60,17 @@ export class Interest<DTInterestId, DTInterestFullfillment> {
this.options = optionsArg; this.options = optionsArg;
this.destructionTimer.completed.then(() => { this.destructionTimer.completed.then(() => {
this.destroy(); if (!this.isDestroyed) {
this.destroy();
}
}); });
if (this.options?.markLostAfterDefault) { 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();
}
});
} }
} }
@@ -72,9 +82,28 @@ export class Interest<DTInterestId, DTInterestFullfillment> {
* self destructs the interest * self destructs the interest
*/ */
public destroy() { 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); 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

@@ -70,6 +70,8 @@ export class InterestMap<DTInterestId, DTInterestFullfillment> {
if (!returnInterest) { if (!returnInterest) {
returnInterest = newInterest; returnInterest = newInterest;
this.interestObjectMap.add(returnInterest); this.interestObjectMap.add(returnInterest);
} else {
newInterest.destroy(); // clean up abandoned Interest's timers
} }
this.interestObservable.push(returnInterest); this.interestObservable.push(returnInterest);
return returnInterest; return returnInterest;
@@ -131,4 +133,16 @@ export class InterestMap<DTInterestId, DTInterestFullfillment> {
}); });
return interest; // if an interest is found, the interest is returned, otherwise interest is null return interest; // if an interest is found, the interest is returned, otherwise interest is 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.interestObservable.signalComplete();
}
} }

View File

@@ -20,4 +20,18 @@ export class LoopTracker<T> {
return false; 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

@@ -62,8 +62,15 @@ export class ObjectMap<T> {
* remove key * remove key
* @param functionArg * @param functionArg
*/ */
public removeMappedUnique(uniqueKey: string) { public removeMappedUnique(uniqueKey: string): T {
const object = this.getMappedUnique(uniqueKey); const object = this.fastMap.removeFromMap(uniqueKey);
if (object !== undefined) {
this.eventSubject.next({
operation: 'remove',
payload: object,
});
}
return object;
} }
/** /**
@@ -220,8 +227,13 @@ export class ObjectMap<T> {
* wipe Objectmap * wipe Objectmap
*/ */
public wipe() { public wipe() {
for (const keyArg of this.fastMap.getKeys()) { const keys = this.fastMap.getKeys();
this.fastMap.removeFromMap(keyArg); for (const keyArg of keys) {
const removedObject = this.fastMap.removeFromMap(keyArg);
this.eventSubject.next({
operation: 'remove',
payload: removedObject,
});
} }
} }
@@ -243,4 +255,12 @@ export class ObjectMap<T> {
public addAllFromOther(objectMapArg: ObjectMap<T>) { public addAllFromOther(objectMapArg: ObjectMap<T>) {
this.fastMap.addAllFromOther(objectMapArg.fastMap); this.fastMap.addAllFromOther(objectMapArg.fastMap);
} }
/**
* destroys the ObjectMap, completing the eventSubject and clearing all entries
*/
public destroy() {
this.wipe();
this.eventSubject.complete();
}
} }

View File

@@ -116,4 +116,12 @@ export class Stringmap {
}); });
this._triggerUntilTrueFunctionArray = filteredArray; 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> { export class TimedAggregtor<T> {
public options: ITimedAggregatorOptions<T>; public options: ITimedAggregatorOptions<T>;
private storageArray: T[] = []; private storageArray: T[] = [];
private isStopped = false;
constructor(optionsArg: ITimedAggregatorOptions<T>) { constructor(optionsArg: ITimedAggregatorOptions<T>) {
this.options = optionsArg; this.options = optionsArg;
@@ -15,9 +16,16 @@ export class TimedAggregtor<T> {
private aggregationTimer: plugins.smarttime.Timer; private aggregationTimer: plugins.smarttime.Timer;
private checkAggregationStatus() { private checkAggregationStatus() {
if (this.isStopped) {
return;
}
const addAggregationTimer = () => { const addAggregationTimer = () => {
this.aggregationTimer = new plugins.smarttime.Timer(this.options.aggregationIntervalInMillis); this.aggregationTimer = new plugins.smarttime.Timer(this.options.aggregationIntervalInMillis);
this.aggregationTimer.completed.then(() => { this.aggregationTimer.completed.then(() => {
if (this.isStopped) {
this.aggregationTimer = null;
return;
}
const aggregateForProcessing = this.storageArray; const aggregateForProcessing = this.storageArray;
if (aggregateForProcessing.length === 0) { if (aggregateForProcessing.length === 0) {
this.aggregationTimer = null; this.aggregationTimer = null;
@@ -35,7 +43,29 @@ export class TimedAggregtor<T> {
} }
public add(aggregationArg: T) { public add(aggregationArg: T) {
if (this.isStopped) {
return;
}
this.storageArray.push(aggregationArg); this.storageArray.push(aggregationArg);
this.checkAggregationStatus(); 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 = [];
}
}
} }

View File

@@ -95,7 +95,7 @@ export class Tree<T> {
} }
compareTreePosition(leftArg: T, rightArg: T): number { compareTreePosition(leftArg: T, rightArg: T): number {
return this.compareTreePosition(leftArg, rightArg); return this.symbolTree.compareTreePosition(leftArg, rightArg);
} }
remove(removeObjectArg: T): T { remove(removeObjectArg: T): T {