fix(classes): cleanup resources, add cancellable timeouts, and fix bugs in several core utility classes
This commit is contained in:
13
changelog.md
13
changelog.md
@@ -1,5 +1,18 @@
|
|||||||
# 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)
|
## 2026-03-01 - 6.3.0 - feat(tooling)
|
||||||
update build tooling, developer dependencies, npmextra configuration, and expand README documentation
|
update build tooling, developer dependencies, npmextra configuration, and expand README documentation
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@push.rocks/lik',
|
name: '@push.rocks/lik',
|
||||||
version: '6.3.0',
|
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.'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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 = [];
|
||||||
@@ -14,7 +15,7 @@ export class BackpressuredArray<T> {
|
|||||||
push(item: T): boolean {
|
push(item: T): boolean {
|
||||||
this.data.push(item);
|
this.data.push(item);
|
||||||
this.itemsAvailable.next('itemsAvailable');
|
this.itemsAvailable.next('itemsAvailable');
|
||||||
|
|
||||||
const spaceAvailable = this.checkSpaceAvailable();
|
const spaceAvailable = this.checkSpaceAvailable();
|
||||||
if (spaceAvailable) {
|
if (spaceAvailable) {
|
||||||
this.hasSpace.next('hasSpace');
|
this.hasSpace.next('hasSpace');
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 = [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user