Skip to content

Commit

Permalink
Add .allSettled() method (#24)
Browse files Browse the repository at this point in the history
Co-authored-by: Sindre Sorhus <[email protected]>
  • Loading branch information
Richienb and sindresorhus authored Aug 1, 2021
1 parent 402ad0f commit 81c597b
Show file tree
Hide file tree
Showing 7 changed files with 330 additions and 61 deletions.
3 changes: 2 additions & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,12 @@ jobs:
fail-fast: false
matrix:
node-version:
- 16
- 14
- 12
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
- uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node-version }}
- run: npm install
Expand Down
78 changes: 78 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
type Awaited<ValueType> = ValueType extends undefined ? ValueType : ValueType extends PromiseLike<infer ResolveValueType> ? ResolveValueType : ValueType;

// https://github.com/microsoft/TypeScript/blob/582e404a1041ce95d22939b73f0b4d95be77c6ec/lib/lib.es2020.promise.d.ts#L21-L31
export type PromiseSettledResult<ResolveValueType> = {
status: 'fulfilled';
value: ResolveValueType;
} | {
status: 'rejected';
reason: unknown;
};

export interface Options {
/**
Number of concurrently pending promises. Minimum: `1`.
Expand Down Expand Up @@ -133,6 +142,75 @@ export class PProgress<ValueType> extends Promise<ValueType> {
options?: Options
): PProgress<Iterable<ReturnValue>>;

/**
Like [`Promise.allSettled`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/allSettled) but also exposes the total progress of all of the promises like `PProgress.all`.
@param promises - Array of promises or promise-returning functions, similar to [p-all](https://github.com/sindresorhus/p-all).
@example
```
import pProgress, {PProgress} from 'p-progress';
import delay from 'delay';
const progressPromise = () => pProgress(async progress => {
progress(0.14);
await delay(52);
progress(0.37);
await delay(104);
progress(0.41);
await delay(26);
progress(0.93);
await delay(55);
return 1;
});
const progressPromise2 = () => pProgress(async progress => {
progress(0.14);
await delay(52);
progress(0.37);
await delay(104);
progress(0.41);
await delay(26);
progress(0.93);
await delay(55);
throw new Error('Catch me if you can!');
});
const allProgressPromise = PProgress.allSettled([
progressPromise(),
progressPromise2()
]);
allProgressPromise.onProgress(console.log);
//=> 0.0925
//=> 0.3425
//=> 0.5925
//=> 0.6025
//=> 0.7325
//=> 0.9825
//=> 1
console.log(await allProgressPromise);
//=> [{status: 'fulfilled', value: 1}, {status: 'rejected', reason: Error: Catch me if you can!}]
```
*/
static allSettled<Promises extends Array<PromiseFactory<unknown> | PromiseLike<unknown>>>(
promises: readonly [...Promises],
options?: Options
): PProgress<{
[Promise_ in keyof Promises]: PromiseSettledResult<Promises[Promise_] extends PromiseLike<unknown>
? Awaited<Promises[Promise_]>
: (
Promises[Promise_] extends PromiseFactory<unknown>
? Awaited<ReturnType<Promises[Promise_]>>
: Promises[Promise_]
)>
}>;
static allSettled<ReturnValue>(
promises: Iterable<PromiseFactory<ReturnValue> | PromiseLike<ReturnValue>>,
options?: Options
): PProgress<Iterable<PromiseSettledResult<ReturnValue>>>;

/**
Accepts a function that gets `instance.progress` as an argument and is called for every progress event.
*/
Expand Down
81 changes: 65 additions & 16 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,14 @@ const sum = iterable => {

export class PProgress extends Promise {
static all(promises, options) {
if (
options && typeof options.concurrency === 'number' &&
!(promises.every(promise => typeof promise === 'function'))
) {
throw new TypeError('When `options.concurrency` is set, the first argument must be an Array of Promise-returning functions');
}
return pProgress(async progress => {
if (
options && typeof options.concurrency === 'number' &&
!(promises.every(promise => typeof promise === 'function'))
) {
throw new TypeError('When `options.concurrency` is set, the first argument must be an Array of Promise-returning functions');
}

return pProgress(progress => {
const progressMap = new Map();

const reportProgress = () => {
Expand Down Expand Up @@ -48,6 +48,55 @@ export class PProgress extends Promise {
});
}

static allSettled(promises, {concurrency} = {}) {
return pProgress(async progress => {
if (
typeof concurrency === 'number' &&
!(promises.every(promise => typeof promise === 'function'))
) {
throw new TypeError('When `options.concurrency` is set, the first argument must be an Array of Promise-returning functions');
}

const progressMap = new Map();

const reportProgress = () => {
progress(sum(progressMap) / promises.length);
};

const mapper = async index => {
const nextValue = promises[index];
const promise = typeof nextValue === 'function' ? nextValue() : nextValue;
progressMap.set(promise, 0);

if (promise instanceof PProgress) {
promise.onProgress(percentage => {
progressMap.set(promise, percentage);
reportProgress();
});
}

try {
return {
status: 'fulfilled',
value: await promise
};
} catch (error) {
return {
status: 'rejected',
reason: error
};
} finally {
progressMap.set(promise, 1);
reportProgress();
}
};

return pTimes(promises.length, mapper, {
concurrency
});
});
}

constructor(executor) {
const setProgress = progress => {
if (progress > 1 || progress < 0) {
Expand Down Expand Up @@ -116,12 +165,12 @@ export class PProgress extends Promise {
}
}

const pProgress = input => new PProgress(async (resolve, reject, progress) => {
try {
resolve(await input(progress));
} catch (error) {
reject(error);
}
});

export default pProgress;
export default function pProgress(input) {
return new PProgress(async (resolve, reject, progress) => {
try {
resolve(await input(progress));
} catch (error) {
reject(error);
}
});
}
10 changes: 9 additions & 1 deletion index.test-d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {expectType} from 'tsd';
import pProgress, {PProgress, ProgressNotifier} from './index.js';
import pProgress, {PProgress, ProgressNotifier, PromiseSettledResult} from './index.js';

const progressPromise = new PProgress(async (resolve, reject, progress) => {
expectType<(progress: number) => void>(progress);
Expand Down Expand Up @@ -496,3 +496,11 @@ expectType<PProgress<Iterable<string | number | boolean | symbol | string[]>>>(
{concurrency: 1}
)
);
expectType<
PProgress<[PromiseSettledResult<string>, PromiseSettledResult<number>]>
>(
PProgress.allSettled([
Promise.resolve('sindresorhus.com'),
Promise.resolve(1)
])
);
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"type": "module",
"exports": "./index.js",
"engines": {
"node": ">=12.2"
"node": ">=12"
},
"scripts": {
"test": "xo && ava && tsd"
Expand Down Expand Up @@ -44,6 +44,7 @@
"in-range": "^3.0.0",
"time-span": "^5.0.0",
"tsd": "^0.16.0",
"typescript": "^4.3.5",
"xo": "^0.40.1"
},
"xo": {
Expand Down
26 changes: 21 additions & 5 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,10 @@ await progressPromise;

Convenience method to run multiple promises and get a total progress of all of them. It counts normal promises with progress `0` when pending and progress `1` when resolved. For `PProgress` type promises, it listens to their `onProgress()` method for more fine grained progress reporting. You can mix and match normal promises and `PProgress` promises.

### PProgress.allSettled(promises, options?)

Like [`Promise.allSettled`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/allSettled) but also exposes the total progress of all of the promises like `PProgress.all`.

```js
import pProgress, {PProgress} from 'p-progress';
import delay from 'delay';
Expand All @@ -117,13 +121,24 @@ const progressPromise = () => pProgress(async progress => {
await delay(26);
progress(0.93);
await delay(55);
return 1;
});

const progressPromise2 = () => pProgress(async progress => {
progress(0.14);
await delay(52);
progress(0.37);
await delay(104);
progress(0.41);
await delay(26);
progress(0.93);
await delay(55);
throw new Error('Catch me if you can!');
});

const allProgressPromise = PProgress.all([
delay(103),
const allProgressPromise = PProgress.allSettled([
progressPromise(),
delay(55),
delay(209)
progressPromise2()
]);

allProgressPromise.onProgress(console.log);
Expand All @@ -135,7 +150,8 @@ allProgressPromise.onProgress(console.log);
//=> 0.9825
//=> 1

await allProgressPromise;
console.log(await allProgressPromise);
//=> [{status: 'fulfilled', value: 1}, {status: 'rejected', reason: Error: Catch me if you can!}]
```

#### promises
Expand Down
Loading

0 comments on commit 81c597b

Please sign in to comment.