Vachan/promiseMap.js

// taken from: https://github.com/sindresorhus/p-map
import _ from 'lodash';

/**
 * 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.
 *
 * @memberof Vachan
 * @param {array|object|Map<any,any>|Set<*>} iterable collection to iterate over
 * @param {function} mapper The function invoked per iteration, should return a promise
 * 	mapper is invoked with (value, index|key, iterable)
 * @param {object} options object of {concurrency}
 * 	concurrency: Number of maximum concurrently running promises, default is Infinity
 * @return {Promise<Array<any>>} a promise that resolves to an array of results
 */
function promiseMap(iterable, mapper, options = {}) {
	return new Promise((resolve, reject) => {
		const concurrency = options.concurrency || options.concurrancy || Infinity;

		if (typeof mapper !== 'function') {
			throw new TypeError('Mapper function is required');
		}
		if (!(typeof concurrency === 'number' && concurrency >= 1)) {
			throw new TypeError(`Expected \`concurrency\` to be a number from 1 and up, got \`${concurrency}\` (${typeof concurrency})`);
		}

		let iterator;
		const ret = [];
		let isRejected = false;
		let iterableDone = false;
		let resolvingCount = 0;
		let currentIdx = 0;
		let isMap = iterable instanceof Map;

		if (iterable[Symbol.iterator]) {
			iterator = iterable[Symbol.iterator]();
		}
		else if (_.isPlainObject(iterable)) {
			const entries = Object.entries(iterable);
			iterator = entries[Symbol.iterator]();
			isMap = true;
		}
		else {
			throw new Error('Expected iterable to be an iterable object');
		}

		const next = () => {
			if (isRejected) {
				return;
			}

			const nextItem = iterator.next();
			const i = currentIdx;
			currentIdx++;

			if (nextItem.done) {
				iterableDone = true;

				if (resolvingCount === 0) {
					resolve(ret);
				}

				return;
			}

			resolvingCount++;

			Promise.resolve(nextItem.value)
				.then((el) => {
					if (isMap) {
						return mapper(el[1], el[0], iterable);
					}
					return mapper(el, i, iterable);
				})
				.then((val) => {
					ret[i] = val;
					resolvingCount--;
					next();
				}, (err) => {
					isRejected = true;
					reject(err);
				});
		};

		for (let i = 0; i < concurrency; i++) {
			next();

			if (iterableDone) {
				break;
			}
		}
	});
}

/**
 * Like promiseMap but for keys
 * @memberof Vachan
 * @param {array|object|Map<any,any>|Set<*>} iterable
 * @param {function} mapper
 * @param {object} options
 * @return {Promise<Array<any>>} a promise that resolves to an array of results
 */
function promiseMapKeys(iterable, mapper, options = {}) {
	const result = {};
	return promiseMap(
		iterable,
		() => {	// eslint-disable-line
			return (value, key, iter) => mapper(value, key, iter).then((res) => {
				result[res] = value;
				return res;
			});
		},
		options
	).then(() => result);
}

/**
 * Like promiseMap but for values
 * @memberof Vachan
 * @param {array|object|Map<any,any>|Set<*>} iterable
 * @param {function} mapper
 * @param {object} options
 * @return {Promise<Array<any>>} a promise that resolves to an array of results
 */
function promiseMapValues(iterable, mapper, options = {}) {
	const result = {};
	return promiseMap(
		iterable,
		() => {	// eslint-disable-line
			return (value, key, iter) => mapper(value, key, iter).then((res) => {
				result[key] = res;
				return res;
			});
		},
		options
	).then(() => result);
}

export {
	promiseMap,
	promiseMapKeys,
	promiseMapValues,
};