Skip to content
Closed
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
27 changes: 27 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,38 @@ type BaseOptions = {

Must be an integer from 1 and up or `Infinity`.

Defaults to the global default concurrency, which is `Infinity` unless changed with `setDefaultConcurrency()`.

@default Infinity
*/
readonly concurrency?: number;
};

/**
Set the global default `concurrency` used by `pMap` and `pMapIterable` when the `concurrency` option is not explicitly provided.

Must be an integer from 1 and up or `Infinity`.

@throws {TypeError} If `concurrency` is not an integer from 1 and up or `Infinity`.

@example
```
import pMap, {setDefaultConcurrency} from 'p-map';

setDefaultConcurrency(10);

await pMap(sites, mapper); //=> Runs with a concurrency of 10.
```
*/
export function setDefaultConcurrency(concurrency: number): void;

/**
Get the current global default `concurrency`.

@returns The global default concurrency. Initially `Infinity`.
*/
export function getDefaultConcurrency(): number;

export type Options = BaseOptions & {
/**
When `true`, the first mapper rejection will be rejected back to the consumer.
Expand Down
31 changes: 23 additions & 8 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,27 @@
const DEFAULT_CONCURRENCY = Number.POSITIVE_INFINITY;

let globalConcurrency = DEFAULT_CONCURRENCY;

function validateConcurrency(value) {
if (!((Number.isSafeInteger(value) && value >= 1) || value === Number.POSITIVE_INFINITY)) {
throw new TypeError(`Expected \`concurrency\` to be an integer from 1 and up or \`Infinity\`, got \`${value}\` (${typeof value})`);
}
}

export function setDefaultConcurrency(concurrency) {
validateConcurrency(concurrency);
globalConcurrency = concurrency;
}

export function getDefaultConcurrency() {
return globalConcurrency;
}

export default async function pMap(
iterable,
mapper,
{
concurrency = Number.POSITIVE_INFINITY,
concurrency = globalConcurrency,
stopOnError = true,
signal,
} = {},
Expand All @@ -16,9 +35,7 @@ export default async function pMap(
throw new TypeError('Mapper function is required');
}

if (!((Number.isSafeInteger(concurrency) && concurrency >= 1) || concurrency === Number.POSITIVE_INFINITY)) {
throw new TypeError(`Expected \`concurrency\` to be an integer from 1 and up or \`Infinity\`, got \`${concurrency}\` (${typeof concurrency})`);
}
validateConcurrency(concurrency);

const result = [];
const errors = [];
Expand Down Expand Up @@ -179,7 +196,7 @@ export function pMapIterable(
iterable,
mapper,
{
concurrency = Number.POSITIVE_INFINITY,
concurrency = globalConcurrency,
backpressure = concurrency,
} = {},
) {
Expand All @@ -191,9 +208,7 @@ export function pMapIterable(
throw new TypeError('Mapper function is required');
}

if (!((Number.isSafeInteger(concurrency) && concurrency >= 1) || concurrency === Number.POSITIVE_INFINITY)) {
throw new TypeError(`Expected \`concurrency\` to be an integer from 1 and up or \`Infinity\`, got \`${concurrency}\` (${typeof concurrency})`);
}
validateConcurrency(concurrency);

if (!((Number.isSafeInteger(backpressure) && backpressure >= concurrency) || backpressure === Number.POSITIVE_INFINITY)) {
throw new TypeError(`Expected \`backpressure\` to be an integer from \`concurrency\` (${concurrency}) and up or \`Infinity\`, got \`${backpressure}\` (${typeof backpressure})`);
Expand Down
5 changes: 4 additions & 1 deletion index.test-d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {expectType, expectAssignable} from 'tsd';
import pMap, {pMapIterable, type Options, type Mapper, pMapSkip} from './index.js';
import pMap, {pMapIterable, type Options, type Mapper, pMapSkip, setDefaultConcurrency, getDefaultConcurrency} from './index.js';

const sites = [
'https://sindresorhus.com',
Expand Down Expand Up @@ -80,3 +80,6 @@ expectType<AsyncIterable<string>>(pMapIterable(sites, asyncMapper));
expectType<AsyncIterable<string>>(pMapIterable(sitesWithPromises, asyncMapper));
expectType<AsyncIterable<string>>(pMapIterable(sitesAsyncIterable, asyncMapper));
expectType<AsyncIterable<string>>(pMapIterable(sitesAsyncIterableWithPromises, asyncMapper));

expectType<void>(setDefaultConcurrency(50));
expectType<number>(getDefaultConcurrency());
21 changes: 21 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ Minimum: `1`

Number of concurrently pending promises returned by `mapper`.

Pass `Infinity` for unbounded concurrency. The default can be changed globally with [`setDefaultConcurrency()`](#setdefaultconcurrencyconcurrency).

##### backpressure

**Only for `pMapIterable`**
Expand Down Expand Up @@ -129,6 +131,25 @@ await pMap([delay(1000), delay(1000)], mapper, {signal: abortController.signal})
// Throws AbortError (DOMException) after 500 ms.
```

### setDefaultConcurrency(concurrency)

Set the global default `concurrency` used by both `pMap` and `pMapIterable` when the `concurrency` option is not explicitly provided.

Must be an integer from 1 and up or `Infinity`. Throws a `TypeError` otherwise.

```js
import pMap, {setDefaultConcurrency} from 'p-map';

setDefaultConcurrency(10);

await pMap(sites, mapper); //=> Runs with a concurrency of 10.
await pMap(sites, mapper, {concurrency: 2}); //=> Explicit option still wins.
```

### getDefaultConcurrency()

Returns the current global default concurrency. Initially `Infinity`.

### pMapSkip

Return this value from a `mapper` function to skip including the value in the returned array.
Expand Down
82 changes: 81 additions & 1 deletion test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import delay from 'delay';
import timeSpan from 'time-span';
import randomInt from 'random-int';
import assertInRange from './assert-in-range.js';
import pMap, {pMapIterable, pMapSkip} from './index.js';
import pMap, {pMapIterable, pMapSkip, setDefaultConcurrency, getDefaultConcurrency} from './index.js';

const sharedInput = [
[async () => 10, 300],
Expand Down Expand Up @@ -678,3 +678,83 @@ test('pMapIterable - pMapSkip', async t => {
2,
], async value => value)), [1, 2]);
});

test.serial('default concurrency is Infinity', t => {
t.is(getDefaultConcurrency(), Number.POSITIVE_INFINITY);
});

test.serial('setDefaultConcurrency affects pMap', async t => {
t.teardown(() => {
setDefaultConcurrency(100);
});

setDefaultConcurrency(2);
t.is(getDefaultConcurrency(), 2);

let running = 0;
let maxRunning = 0;

await pMap(Array.from({length: 10}).fill(0), async () => {
running++;
maxRunning = Math.max(maxRunning, running);
await delay(10);
running--;
});

t.is(maxRunning, 2);
});

test.serial('setDefaultConcurrency affects pMapIterable', async t => {
t.teardown(() => {
setDefaultConcurrency(100);
});

setDefaultConcurrency(2);

let running = 0;
let maxRunning = 0;

await collectAsyncIterable(pMapIterable(Array.from({length: 10}).fill(0), async () => {
running++;
maxRunning = Math.max(maxRunning, running);
await delay(10);
running--;
}));

t.is(maxRunning, 2);
});

test.serial('explicit concurrency overrides the global default', async t => {
t.teardown(() => {
setDefaultConcurrency(100);
});

setDefaultConcurrency(1);

let running = 0;
let maxRunning = 0;

await pMap(Array.from({length: 10}).fill(0), async () => {
running++;
maxRunning = Math.max(maxRunning, running);
await delay(10);
running--;
}, {concurrency: Number.POSITIVE_INFINITY});

t.is(maxRunning, 10);
t.is(getDefaultConcurrency(), 1);
});

test.serial('setDefaultConcurrency validates its input', t => {
for (const value of [0, -1, 1.5, '5', Number.NaN, undefined, null]) {
t.throws(() => {
setDefaultConcurrency(value);
}, {instanceOf: TypeError});
}

t.is(getDefaultConcurrency(), 100);
t.notThrows(() => {
setDefaultConcurrency(Number.POSITIVE_INFINITY);
});
setDefaultConcurrency(100);
});
Loading