Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
name: CI

on:
push:
branches: [master, main]
pull_request:

jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [16.x, 18.x, 20.x, 22.x]
steps:
- uses: actions/checkout@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- run: npm install
- run: npm test
3 changes: 1 addition & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
node_modules/
dist/
test/test.js
dist/
57 changes: 36 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,48 +1,63 @@
# await-parallel-limit

```javascript
Run an array of async functions with a bounded number running concurrently.
Zero dependencies, first-class TypeScript types.

```bash
npm install await-parallel-limit --save
```

Processes an array of async functions with concurrency limit (default limit is 5).
## Behaviour

- Runs at most `concurrency` jobs at a time (default `5`).
- Results are returned in **input order**, not completion order.
- If any job rejects, the returned promise rejects with the first error
(same semantics as `Promise.all`). Jobs already in flight still run to
completion, but their results are discarded.
- A `concurrency` value that is not a positive integer (e.g. `0`, `-1`, `2.5`)
falls back to the default of `5`.

## API

```typescript
parallel(
Array jobs,
Integer concurrency,
)
parallel<T>(
jobs: Array<() => Promise<T>>,
concurrency?: number, // default: 5
): Promise<T[]>
```

## Typescript
## TypeScript

```typescript
import parallel from 'await-parallel-limit'
// named import also available: import { parallel } from 'await-parallel-limit'

const jobs = [
async () { return await true },
async () { return await 2 },
async () => true,
async () => 2,
] as const

// results array is typed based on return types of async functions
// it is an ordered tuple of types

// const results: [ boolean, number ]
// When `jobs` is a readonly tuple (`as const`), the result is an ordered tuple
// typed from each job's return type:
// const results: [boolean, number]
const results = await parallel(jobs, 2)
```

## Javascript
## JavaScript

```javascript
const parallel = require('await-parallel-limit').default

var jobs = [
async () { ... },
async () { ... },
async () { ... },
async () { ... }
const jobs = [
async () => { /* ... */ },
async () => { /* ... */ },
async () => { /* ... */ },
async () => { /* ... */ },
]

var results = await parallel(jobs, 2)
```
const results = await parallel(jobs, 2)
```

## License

MIT
33 changes: 33 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

33 changes: 26 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,25 +1,44 @@
{
"name": "await-parallel-limit",
"version": "2.1.0",
"version": "2.2.0",
"description": "Await an array of async functions with parallel limit.",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": [
"dist"
],
"sideEffects": false,
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
"build": "tsc",
"clean": "rm -rf dist",
"prepare": "tsc",
"test": "tsc && node test/index.test.cjs"
},
"prepublish": "tsc",
"keywords": [
"async",
"await",
"array",
"parallel",
"limit"
"concurrency",
"limit",
"promise",
"pool"
],
"author": "Ondrej Machek, acrocz@gmail.com",
"license": "ISC",
"license": "MIT",
"engines": {
"node": ">=12"
},
"repository": {
"type": "git",
"url": "git://github.com/Acro/await-parallel-limit"
"url": "git+https://github.com/Acro/await-parallel-limit.git"
},
"bugs": {
"url": "https://github.com/Acro/await-parallel-limit/issues"
},
"dependencies": {}
"homepage": "https://github.com/Acro/await-parallel-limit#readme",
"dependencies": {},
"devDependencies": {
"typescript": "^5.4.0"
}
}
71 changes: 49 additions & 22 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,61 @@
type Unpacked<T> =
T extends (...args: any[]) => infer U ? U :
T extends Promise<infer U> ? U :
T;
T extends (...args: any[]) => infer U ? U :
T extends Promise<infer U> ? U :
T

const parallel = async <T>(
jobs: { [K in keyof T]: (() => Promise<T[K]>) },
limit:number,
) => {
type Data = typeof jobs
type MapToResult<X extends Data> = { [K in keyof X]: Unpacked<Unpacked<X[K]>> }
type MapToResult<T> = { [K in keyof T]: Unpacked<Unpacked<T[K]>> }

export const DEFAULT_CONCURRENCY = 5

/**
* Run an array of async, zero-argument job functions with a bounded number of
* jobs in flight at any one time.
*
* Results are returned in the same order as `jobs` (not completion order). When
* `jobs` is a `readonly` tuple (e.g. declared `as const`), the result type is an
* ordered tuple matching each job's resolved value.
*
* If any job rejects, the returned promise rejects with the first such error
* (matching `Promise.all` semantics); jobs already in flight still run to
* completion but their results are discarded.
*
* @param jobs Array of functions, each returning a promise.
* @param limit Maximum number of jobs to run concurrently. Values that are not
* positive integers fall back to {@link DEFAULT_CONCURRENCY} (5).
*/
const parallel = async <T>(
jobs: { [K in keyof T]: () => Promise<T[K]> },
limit: number = DEFAULT_CONCURRENCY,
): Promise<MapToResult<typeof jobs>> => {
if (!Array.isArray(jobs)) {
throw new Error('First argument is not an array.')
throw new Error('First argument is not an array.')
}

var n = Math.min(limit || 5, jobs.length)
var ret:any = []
var index = 0
// Normalise the concurrency limit. Non-integer / non-positive values fall back
// to the default — this preserves the historical behaviour where a falsy limit
// (e.g. `0` or `undefined`) meant "use the default of 5".
const concurrency = Number.isInteger(limit) && limit > 0 ? limit : DEFAULT_CONCURRENCY

var next = async () => {
var i = index++
var job = jobs[i]
ret[i] = await job()
if (index < jobs.length) await next()
const results: any = new Array(jobs.length)
let index = 0

const worker = async (): Promise<void> => {
while (true) {
const i = index++
if (i >= jobs.length) return
results[i] = await jobs[i]()
}
}

var next_arr = Array(n).fill(next)
await Promise.all(next_arr.map((f) => { return f() }))
const workerCount = Math.min(concurrency, jobs.length)
const workers: Promise<void>[] = []
for (let w = 0; w < workerCount; w++) {
workers.push(worker())
}
await Promise.all(workers)

return <MapToResult<Data>>ret
return results as MapToResult<typeof jobs>
}

export default parallel
export default parallel
export { parallel }
Loading
Loading