1'use strict';
2const AggregateError = require('aggregate-error');
3
4module.exports = async (
5	iterable,
6	mapper,
7	{
8		concurrency = Infinity,
9		stopOnError = true
10	} = {}
11) => {
12	return new Promise((resolve, reject) => {
13		if (typeof mapper !== 'function') {
14			throw new TypeError('Mapper function is required');
15		}
16
17		if (!((Number.isSafeInteger(concurrency) || concurrency === Infinity) && concurrency >= 1)) {
18			throw new TypeError(`Expected \`concurrency\` to be an integer from 1 and up or \`Infinity\`, got \`${concurrency}\` (${typeof concurrency})`);
19		}
20
21		const result = [];
22		const errors = [];
23		const iterator = iterable[Symbol.iterator]();
24		let isRejected = false;
25		let isIterableDone = false;
26		let resolvingCount = 0;
27		let currentIndex = 0;
28
29		const next = () => {
30			if (isRejected) {
31				return;
32			}
33
34			const nextItem = iterator.next();
35			const index = currentIndex;
36			currentIndex++;
37
38			if (nextItem.done) {
39				isIterableDone = true;
40
41				if (resolvingCount === 0) {
42					if (!stopOnError && errors.length !== 0) {
43						reject(new AggregateError(errors));
44					} else {
45						resolve(result);
46					}
47				}
48
49				return;
50			}
51
52			resolvingCount++;
53
54			(async () => {
55				try {
56					const element = await nextItem.value;
57					result[index] = await mapper(element, index);
58					resolvingCount--;
59					next();
60				} catch (error) {
61					if (stopOnError) {
62						isRejected = true;
63						reject(error);
64					} else {
65						errors.push(error);
66						resolvingCount--;
67						next();
68					}
69				}
70			})();
71		};
72
73		for (let i = 0; i < concurrency; i++) {
74			next();
75
76			if (isIterableDone) {
77				break;
78			}
79		}
80	});
81};
82