initial commit
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 Pierre-Henri
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,96 @@
|
||||
# Async Utils
|
||||
|
||||
A TypeScript library providing utility classes and functions to simplify working with asynchronous operations and promises.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
yarn add github:worph/async-utils#main
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
- **Lazy<T>**: A utility type for lazy initialization of asynchronous operations.
|
||||
- **ListenerCleaner**: Manages cleanup functions, allowing for easy resource management.
|
||||
- **PromiseQueue**: A queue system for promises, allowing for sequential execution and cancellation.
|
||||
- **MultiQueue**: Manages multiple `PromiseQueue` instances, distributing tasks to optimize concurrency.
|
||||
- **Id Generation**: Utility function for generating random IDs.
|
||||
|
||||
## Usage
|
||||
|
||||
### Lazy Initialization
|
||||
|
||||
```typescript
|
||||
import { Lazy } from "@worph/async-utils";
|
||||
|
||||
const lazyValue: Lazy<number> = async () => {
|
||||
// some asynchronous operation
|
||||
return 42;
|
||||
};
|
||||
|
||||
// Usage
|
||||
lazyValue().then(value => console.log(value));
|
||||
```
|
||||
|
||||
### Listener Cleaner
|
||||
|
||||
```typescript
|
||||
import { ListenerCleaner } from "worph/async-utils";
|
||||
|
||||
const cleaner = new ListenerCleaner();
|
||||
cleaner.add(() => console.log("Cleanup action"));
|
||||
|
||||
// Trigger all cleanup actions
|
||||
cleaner.cleanUp();
|
||||
```
|
||||
|
||||
### Promise Queue
|
||||
|
||||
```typescript
|
||||
import { PromiseQueue } from "@worph/async-utils";
|
||||
|
||||
const queue = new PromiseQueue<number>();
|
||||
|
||||
queue.add(async () => {
|
||||
// some asynchronous task
|
||||
return 1;
|
||||
});
|
||||
|
||||
// Wait for the queue to be empty
|
||||
queue.awaitQueueEmpty().then(() => console.log("Queue empty"));
|
||||
```
|
||||
|
||||
### Multi Queue
|
||||
|
||||
```typescript
|
||||
import { MultiQueue } from "@worph/async-utils";
|
||||
|
||||
const multiQueue = new MultiQueue<number>(2); // 2 concurrent tasks
|
||||
|
||||
multiQueue.add(async () => {
|
||||
// some asynchronous task
|
||||
return 1;
|
||||
});
|
||||
|
||||
// Add more tasks...
|
||||
|
||||
// Wait for all queues to be empty
|
||||
multiQueue.awaitQueueEmpty().then(() => console.log("All queues empty"));
|
||||
```
|
||||
|
||||
### Generating an ID
|
||||
|
||||
```typescript
|
||||
import { makeid } from "@worph/async-utils";
|
||||
|
||||
const id = makeid(10); // Generates a random 10 character string
|
||||
console.log(id);
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome! Please submit a pull request or open an issue for discussion.
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the MIT License - see the LICENSE file for details.
|
||||
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"name": "@worph/async-utils",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/ApteroSAS/async-utils.git"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"bugs": {
|
||||
"url": "https://github.com/ApteroSAS/async-utils/issues"
|
||||
},
|
||||
"homepage": "https://github.com/ApteroSAS/async-utils#readme",
|
||||
"type": "module",
|
||||
"main": "./dist/index.cjs",
|
||||
"module": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/index.js",
|
||||
"require": "./dist/index.cjs"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsup src/index.ts",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"dependencies": {
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^29.5.1",
|
||||
"jest": "^29.5.0",
|
||||
"tsup": "^6.7.0",
|
||||
"typescript": "^5.0.4"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
export function makeid(length) {
|
||||
let result = '';
|
||||
let characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
let charactersLength = characters.length;
|
||||
for ( let i = 0; i < length; i++ ) {
|
||||
result += characters.charAt(Math.floor(Math.random() *
|
||||
charactersLength));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
|
||||
export type Lazy<T> = () => Promise<T>;
|
||||
@@ -0,0 +1,23 @@
|
||||
export class ListenerCleaner {
|
||||
cleaners: (() => void)[] = [];
|
||||
|
||||
add(cleanerCallback: () => void) {
|
||||
this.cleaners.push(cleanerCallback);
|
||||
}
|
||||
|
||||
cleaner(): () => void {
|
||||
return () => {
|
||||
this.cleanUp();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Call all the cleaner callbacks and reset this cleaner to be reused.
|
||||
*/
|
||||
cleanUp() {
|
||||
for (const cleaner of this.cleaners) {
|
||||
cleaner();
|
||||
}
|
||||
this.cleaners = [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import {PromiseQueue} from "./PromiseQueue";
|
||||
|
||||
export class MultiQueue<T> {
|
||||
private readonly queues:PromiseQueue<T>[];
|
||||
|
||||
/**
|
||||
*
|
||||
* @param concurrentTask
|
||||
*/
|
||||
constructor(
|
||||
concurrentTask:number=1,
|
||||
) {
|
||||
if (concurrentTask<1){
|
||||
throw new Error("concurrentTask must be >=1");
|
||||
}
|
||||
this.queues = [];
|
||||
for(let i=0;i<concurrentTask;i++){
|
||||
this.queues.push(new PromiseQueue<T>());
|
||||
}
|
||||
}
|
||||
|
||||
public getQueueSize(): number {
|
||||
let ret = 0;
|
||||
for(let queue of this.queues){
|
||||
ret+=queue.getQueueSize();
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
public async awaitQueueEmpty(){
|
||||
for(let queue of this.queues){
|
||||
await queue.awaitQueueEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a task to the queue. The queue with the smallest task size is used
|
||||
* @param task
|
||||
* @param id
|
||||
**/
|
||||
public add(task: () => Promise<T>, id: string = null): { id: string, promise: Promise<T> } {
|
||||
let queue = this.queues[0];
|
||||
for(let i=1;i<this.queues.length;i++){
|
||||
if(this.queues[i].getQueueSize()<queue.getQueueSize()){
|
||||
queue = this.queues[i];
|
||||
}
|
||||
}
|
||||
return queue.add(task,id);
|
||||
}
|
||||
|
||||
public cancel(id: string) {
|
||||
for(let queue of this.queues){
|
||||
queue.cancel(id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The last task in progress is not canceled
|
||||
*/
|
||||
public async cancelAll(){
|
||||
for(let queue of this.queues){
|
||||
await queue.cancelAll();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import {makeid} from "./Id";
|
||||
|
||||
export class PromiseQueue<T> {
|
||||
private nextPromise: Promise<T> = Promise.resolve(null);
|
||||
private canceled = {};
|
||||
private queueSize = 0;
|
||||
private queueID = 0;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param onlyLast if true all task are cancel and only the last one is keep alive (a task started still finishes)
|
||||
*/
|
||||
constructor(
|
||||
private onlyLast: boolean = false
|
||||
) {
|
||||
this.cancelAll();
|
||||
}
|
||||
|
||||
getQueueSize(): number {
|
||||
return this.queueSize;
|
||||
}
|
||||
|
||||
async awaitQueueEmpty(){
|
||||
await this.nextPromise;//await the current promise
|
||||
while (this.queueSize!=0){//await all the fowllofing promise
|
||||
await this.nextPromise;
|
||||
}
|
||||
}
|
||||
|
||||
add(task: () => Promise<T>, id: string = null): { id: string, promise: Promise<T> } {
|
||||
if (!id) {
|
||||
id = "promise-" + makeid(8);
|
||||
}
|
||||
let lastPromise = this.nextPromise;
|
||||
this.queueSize++;
|
||||
let currentID = this.queueID;
|
||||
let currentPromise=null;
|
||||
currentPromise = (async () => {
|
||||
this.queueSize--;
|
||||
try {
|
||||
await lastPromise;
|
||||
}catch (e) {
|
||||
/* ignore error of previous promise*/
|
||||
}
|
||||
if(currentID!==this.queueID){
|
||||
throw new Error("canceled caused by new queue");
|
||||
}
|
||||
if (this.canceled[id]) {
|
||||
delete this.canceled[id];
|
||||
throw new Error("canceled by user");
|
||||
}
|
||||
if(this.onlyLast && currentPromise!=this.nextPromise){
|
||||
throw new Error("canceled caused by not last");
|
||||
}
|
||||
//execute task and return result
|
||||
return task();
|
||||
})();
|
||||
currentPromise.catch(e=>{
|
||||
/* we catch the exception this way to avoid an uncaught exception in the console*/
|
||||
});
|
||||
this.nextPromise = currentPromise
|
||||
return {id, promise: this.nextPromise};
|
||||
}
|
||||
|
||||
cancel(id: string) {
|
||||
this.canceled[id] = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* The last task in progress is not canceled
|
||||
*/
|
||||
async cancelAll(){
|
||||
this.queueID++;//create a new queue will cancel all previous task except the one in progress
|
||||
await this.awaitQueueEmpty();
|
||||
this.nextPromise = Promise.resolve(null);
|
||||
this.canceled = {};
|
||||
this.queueSize = 0;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export * from "./ListenerCleaner";
|
||||
export * from "./PromiseQueue";
|
||||
export * from "./MultiQueue";
|
||||
export * from "./Id";
|
||||
export * from "./Lazy";
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"experimentalDecorators": true,//needed for inversify
|
||||
"module": "ESNext",
|
||||
"target": "ESNext",
|
||||
"moduleResolution": "node",
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"esModuleInterop": true,
|
||||
"noImplicitAny": false,
|
||||
"noLib": false,
|
||||
"skipLibCheck": true,
|
||||
"removeComments": true,
|
||||
"sourceMap": true,
|
||||
"outDir": "./dist"
|
||||
},
|
||||
"include": [
|
||||
"./src"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"test/*",
|
||||
"examples/*"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { defineConfig } from 'tsup'
|
||||
|
||||
export default defineConfig({
|
||||
entry: ['src/index.ts'],
|
||||
splitting: false,
|
||||
sourcemap: true,
|
||||
clean: true,
|
||||
dts:true,
|
||||
minify:true,
|
||||
format: ['cjs', 'esm'],
|
||||
})
|
||||
Reference in New Issue
Block a user