Compare commits
9 Commits
Author | SHA1 | Date | |
---|---|---|---|
685f4ebb3b | |||
2ff7efe6d8 | |||
99072d5fdf | |||
4442ddffcd | |||
7c5b3825ac | |||
84babb3cd4 | |||
5d9624bd56 | |||
32397a97cd | |||
753b829d18 |
76
changelog.md
Normal file
76
changelog.md
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
## 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
|
20
package.json
20
package.json
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@push.rocks/lik",
|
"name": "@push.rocks/lik",
|
||||||
"version": "6.0.15",
|
"version": "6.2.2",
|
||||||
"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.5.6",
|
||||||
"@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"
|
||||||
}
|
}
|
||||||
|
11509
pnpm-lock.yaml
generated
11509
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
51
readme.md
51
readme.md
@ -17,22 +17,53 @@ This will download `@push.rocks/lik` and add it to your project's `package.json`
|
|||||||
|
|
||||||
### AsyncExecutionStack
|
### AsyncExecutionStack
|
||||||
|
|
||||||
`AsyncExecutionStack` allows for managing asynchronous task execution with exclusive and non-exclusive slots, ensuring effective handling of resource-intensive operations.
|
`AsyncExecutionStack` provides controlled execution of asynchronous tasks in two modes:
|
||||||
|
|
||||||
|
- **Exclusive tasks**: run one at a time in insertion order, blocking all other tasks until complete.
|
||||||
|
- **Non-exclusive tasks**: run in parallel, up to an optional concurrency limit.
|
||||||
|
|
||||||
|
Create a stack:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { AsyncExecutionStack } from '@push.rocks/lik';
|
import { AsyncExecutionStack } from '@push.rocks/lik';
|
||||||
|
|
||||||
const myAsyncStack = new AsyncExecutionStack();
|
const stack = new AsyncExecutionStack();
|
||||||
|
```
|
||||||
|
|
||||||
// Exclusive execution ensures this task doesn't run in parallel with others
|
Exclusive tasks execute sequentially and block subsequent tasks (with an optional timeout):
|
||||||
await myAsyncStack.getExclusiveExecutionSlot(async () => {
|
|
||||||
// some asynchronous operation
|
|
||||||
}, 2500);
|
|
||||||
|
|
||||||
// Non-exclusive slots can run in parallel
|
```typescript
|
||||||
myAsyncStack.getNonExclusiveExecutionSlot(async () => {
|
await stack.getExclusiveExecutionSlot(
|
||||||
// another asynchronous operation
|
async () => {
|
||||||
}, 1500);
|
// 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 () => {
|
||||||
|
// work 1
|
||||||
|
}, 2000);
|
||||||
|
|
||||||
|
const p2 = stack.getNonExclusiveExecutionSlot(async () => {
|
||||||
|
// work 2
|
||||||
|
}, 2000);
|
||||||
|
|
||||||
|
await Promise.all([p1, p2]);
|
||||||
|
```
|
||||||
|
|
||||||
|
Control concurrency and inspect status:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Set maximum concurrent non-exclusive tasks (minimum 1)
|
||||||
|
stack.setNonExclusiveMaxConcurrency(3);
|
||||||
|
|
||||||
|
console.log(stack.getNonExclusiveMaxConcurrency()); // 3
|
||||||
|
console.log(stack.getActiveNonExclusiveCount()); // currently running tasks
|
||||||
|
console.log(stack.getPendingNonExclusiveCount()); // tasks waiting for slots
|
||||||
```
|
```
|
||||||
|
|
||||||
### FastMap
|
### FastMap
|
||||||
|
@ -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();
|
||||||
|
@ -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.2',
|
||||||
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.'
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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()) {
|
||||||
|
Reference in New Issue
Block a user