UNPKG

@naturalcycles/js-lib

Version:

Standard library for universal (browser + Node.js) javascript

176 lines (175 loc) 5.87 kB
import { ErrorMode } from '../error/errorMode.js'; import { END, SKIP } from '../types.js'; /** * Forked from https://github.com/sindresorhus/p-map * * Improvements: * - Exported as { pMap }, so IDE auto-completion works * - Included Typescript typings (no need for @types/p-map) * - Compatible with pProps (that had typings issues) * - Preserves async stack traces (in selected cases) * * Returns a `Promise` that is fulfilled when all promises in `input` and ones returned from `mapper` are fulfilled, * or rejects if any of the promises reject. The fulfilled value is an `Array` of the fulfilled values returned * from `mapper` in `input` order. * * @param iterable - Iterated over concurrently in the `mapper` function. * @param mapper - Function which is called for every item in `input`. Expected to return a `Promise` or value. * @param opt - Options-object. * * @example * * const sites = [ * 'ava.li', * 'todomvc.com * ]; * * (async () => { * const mapper = async site => { * const {requestUrl} = await got.head(site); * return requestUrl; * }; * * const result = await pMap(sites, mapper, {concurrency: 2}); * //=> ['http://ava.li/', 'http://todomvc.com/'] * })(); */ export async function pMap(iterable, mapper, opt = {}) { const items = [...iterable]; const itemsLength = items.length; if (itemsLength === 0) return []; // short circuit const { concurrency = Infinity, errorMode = ErrorMode.THROW_IMMEDIATELY, logger = console } = opt; // Special cases that are able to preserve async stack traces // Special case: serial execution if (concurrency === 1) { return await pMap1(items, mapper, errorMode, logger); } // Special case: items.length <= concurrency (including when concurrency === Infinity) if (items.length <= concurrency) { return await pMapAll(items, mapper, errorMode, logger); } // General case: execution with throttled concurrency const ret = []; const errors = []; let isSettled = false; let resolvingCount = 0; let currentIndex = 0; return await new Promise((resolve, reject) => { const next = () => { if (isSettled) { return; } const nextItem = items[currentIndex]; const i = currentIndex++; if (currentIndex > itemsLength) { if (resolvingCount === 0) { isSettled = true; const r = ret.filter(r => r !== SKIP); if (errors.length) { reject(new AggregateError(errors, `pMap resulted in ${errors.length} error(s)`)); } else { resolve(r); } } return; } resolvingCount++; Promise.resolve(nextItem) .then(async (element) => await mapper(element, i)) .then(value => { if (value === END) { isSettled = true; return resolve(ret.filter(r => r !== SKIP)); } ret[i] = value; resolvingCount--; next(); }, (err) => { if (errorMode === ErrorMode.THROW_IMMEDIATELY) { isSettled = true; reject(err); } else { if (errorMode === ErrorMode.THROW_AGGREGATED) { errors.push(err); } else { // otherwise, suppress (but still log via logger) logger?.error(err); } resolvingCount--; next(); } }); }; for (let i = 0; i < concurrency; i++) { next(); if (isSettled) { break; } } }); } /** pMap with serial (non-concurrent) execution. */ async function pMap1(items, mapper, errorMode, logger) { let i = 0; const ret = []; const errors = []; for (const item of items) { try { const r = await mapper(item, i++); if (r === END) break; if (r !== SKIP) ret.push(r); } catch (err) { if (errorMode === ErrorMode.THROW_IMMEDIATELY) throw err; if (errorMode === ErrorMode.THROW_AGGREGATED) { errors.push(err); } else { // otherwise, suppress (but still log via logger) logger?.error(err); } } } if (errors.length) { throw new AggregateError(errors, `pMap resulted in ${errors.length} error(s)`); } return ret; } /** pMap with fully concurrent execution, like Promise.all */ async function pMapAll(items, mapper, errorMode, logger) { if (errorMode === ErrorMode.THROW_IMMEDIATELY) { return (await Promise.all(items.map((item, i) => mapper(item, i)))).filter(r => r !== SKIP && r !== END); } const ret = []; const errors = []; for (const r of await Promise.allSettled(items.map((item, i) => mapper(item, i)))) { if (r.status === 'fulfilled') { if (r.value === END) break; if (r.value !== SKIP) ret.push(r.value); } else if (errorMode === ErrorMode.THROW_AGGREGATED) { errors.push(r.reason); } else { // otherwise, suppress (but still log via logger) logger?.error(r.reason); } } if (errors.length) { throw new AggregateError(errors, `pMap resulted in ${errors.length} error(s)`); } return ret; }