Compare commits

..

7 Commits

7 changed files with 8467 additions and 3260 deletions

69
changelog.md Normal file
View File

@ -0,0 +1,69 @@
# Changelog
## 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,6 +1,6 @@
{ {
"name": "@push.rocks/lik", "name": "@push.rocks/lik",
"version": "6.0.15", "version": "6.2.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",
@ -13,21 +13,21 @@
}, },
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git+ssh://git@gitlab.com/pushrocks/lik.git" "url": "https://code.foss.global/push.rocks/lik.git"
}, },
"author": "Lossless GmbH", "author": "Lossless GmbH",
"license": "MIT", "license": "MIT",
"bugs": { "bugs": {
"url": "https://gitlab.com/pushrocks/lik/issues" "url": "https://gitlab.com/pushrocks/lik/issues"
}, },
"homepage": "https://gitlab.com/pushrocks/lik#README", "homepage": "https://code.foss.global/push.rocks/lik",
"devDependencies": { "devDependencies": {
"@git.zone/tsbuild": "^2.1.72", "@git.zone/tsbuild": "^2.1.72",
"@git.zone/tsbundle": "^2.0.15", "@git.zone/tsbundle": "^2.0.15",
"@git.zone/tsrun": "^1.2.44", "@git.zone/tsrun": "^1.2.44",
"@git.zone/tstest": "^1.0.90", "@git.zone/tstest": "^1.0.90",
"@push.rocks/tapbundle": "^5.0.8", "@push.rocks/tapbundle": "^5.0.8",
"@types/node": "^20.12.7" "@types/node": "^22.13.9"
}, },
"dependencies": { "dependencies": {
"@push.rocks/smartdelay": "^3.0.5", "@push.rocks/smartdelay": "^3.0.5",
@ -66,5 +66,13 @@
"Asynchronous programming", "Asynchronous programming",
"Event handling", "Event handling",
"Data aggregation" "Data aggregation"
],
"pnpm": {
"onlyBuiltDependencies": [
"esbuild",
"mongodb-memory-server",
"puppeteer"
] ]
},
"packageManager": "pnpm@10.7.0+sha512.6b865ad4b62a1d9842b61d674a393903b871d9244954f652b8842c2b553c72176b278f64c463e52d40fff8aba385c235c8c9ecf5cc7de4fd78b8bb6d49633ab6"
} }

11441
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -26,4 +26,66 @@ tap.test('should run in parallel', async (toolsArg) => {
}, 0); }, 0);
}); });
// 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(); export default tap.start();

View File

@ -1,8 +1,8 @@
/** /**
* autocreated commitinfo by @pushrocks/commitinfo * autocreated commitinfo by @push.rocks/commitinfo
*/ */
export const commitinfo = { export const commitinfo = {
name: '@push.rocks/lik', name: '@push.rocks/lik',
version: '6.0.15', version: '6.2.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

@ -10,6 +10,12 @@ interface IExecutionSlot<T> {
export class AsyncExecutionStack { export class AsyncExecutionStack {
private executionSlots: IExecutionSlot<any>[] = []; private executionSlots: IExecutionSlot<any>[] = [];
private isProcessing = false; 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>( public async getExclusiveExecutionSlot<T = any>(
funcArg: () => Promise<T>, funcArg: () => Promise<T>,
@ -42,6 +48,28 @@ export class AsyncExecutionStack {
this.processExecutionSlots(); this.processExecutionSlots();
return executionDeferred.promise; 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() { private async processExecutionSlots() {
if (this.isProcessing) { if (this.isProcessing) {
@ -87,13 +115,14 @@ export class AsyncExecutionStack {
private async executeNonExclusiveSlots(slots: IExecutionSlot<any>[]) { private async executeNonExclusiveSlots(slots: IExecutionSlot<any>[]) {
const promises = slots.map(async (slot) => { const promises = slots.map(async (slot) => {
// wait for an available non-exclusive slot
await this.waitForNonExclusiveSlot();
try { try {
// execute with optional timeout
if (slot.timeout) { if (slot.timeout) {
const result = await Promise.race([ const result = await Promise.race([
slot.funcToExecute(), slot.funcToExecute(),
plugins.smartdelay.delayFor(slot.timeout).then(() => { plugins.smartdelay.delayFor(slot.timeout).then(() => { throw new Error('Timeout reached'); }),
throw new Error('Timeout reached');
}),
]); ]);
slot.executionDeferred.resolve(result); slot.executionDeferred.resolve(result);
} else { } else {
@ -102,9 +131,33 @@ export class AsyncExecutionStack {
} }
} catch (error) { } catch (error) {
slot.executionDeferred.reject(error); slot.executionDeferred.reject(error);
} finally {
this.releaseNonExclusiveSlot();
} }
}); });
await Promise.all(promises); 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

@ -34,6 +34,10 @@ export class BackpressuredArray<T> {
return this.data.length < this.highWaterMark; return this.data.length < this.highWaterMark;
} }
public checkHasItems(): boolean {
return this.data.length > 0;
}
waitForSpace(): Promise<void> { waitForSpace(): Promise<void> {
return new Promise<void>((resolve) => { return new Promise<void>((resolve) => {
if (this.checkSpaceAvailable()) { if (this.checkSpaceAvailable()) {