import Promise, { Thenable } from '../promise'; import * as array from '../array'; import { ArrayLike } from '../array'; /** * Processes all items and then applies the callback to each item and eventually returns an object containing the * processed values and callback results * @param items a list of synchronous/asynchronous values to process * @param callback a callback that maps values to synchronous/asynchronous results * @return a list of objects holding the synchronous values and synchronous results. */ function processValuesAndCallback(items: (T | Promise)[], callback: Mapper): Promise<{ values: T[]; results: U[] }> { return Promise.all(items) .then(function (results) { let pass: (U | Promise)[] = Array.prototype.map.call(results, callback); return Promise.all(pass) .then<{ values: T[]; results: U[] }>(function (pass) { return { values: results, results: pass }; }); }); } /** * Finds the index of the next value in a sparse array-like object * @param list the sparse array-like object * @param offset the starting offset * @return {number} the offset of the next index with a value; or -1 if not found */ function findNextValueIndex(list: ArrayLike, offset: number = -1): number { offset++; for (let length = list.length; offset < length; offset++) { if (offset in list) { return offset; } } return -1; } function findLastValueIndex(list: ArrayLike, offset?: number): number { offset = (offset === undefined ? list.length : offset) - 1; for (; offset >= 0; offset--) { if (offset in list) { return offset; } } return -1; } function generalReduce(findNextIndex: (list: ArrayLike, offset?: number) => number, items: (T | Promise)[], callback: Reducer, initialValue?: U): Promise { const hasInitialValue = arguments.length > 3; return Promise.all(items) .then(function (results) { return new Promise(function (resolve, reject) { let i: number; function next(currentValue: U): void { i = findNextIndex(items, i); if (i >= 0) { const result = callback(currentValue, results[i], i, results); if ( (> result).then) { (> result).then(next, reject); } else { next( result); } } else { resolve(currentValue); } }; let value: U; if (hasInitialValue) { value = initialValue; } else { i = findNextIndex(items); if (i < 0) { throw new Error('reduce array with no initial value'); } value = results[i]; } next(value); }); }); } function testAndHaltOnCondition(condition: boolean, items: (T | Promise)[], callback: Filterer): Promise { return Promise.all(items).then(function (results) { return new Promise(function(resolve) { let result: (boolean | Thenable); let pendingCount = 0; for (let i = 0; i < results.length; i++) { result = callback(results[i], i, results); if (result === condition) { return resolve(result); } else if ((> result).then) { pendingCount++; (> result).then(function (result) { if (result === condition) { resolve(result); } pendingCount--; if (pendingCount === 0) { resolve(!condition); } }); } } if (pendingCount === 0) { resolve(!condition); } }); }); } /** * Test whether all elements in the array pass the provided callback * @param items a collection of synchronous/asynchronous values * @param callback a synchronous/asynchronous test * @return eventually returns true if all values pass; otherwise false */ export function every(items: (T | Promise)[], callback: Filterer): Promise { return testAndHaltOnCondition(false, items, callback); } /** * Returns an array of elements which pass the provided callback * @param items a collection of synchronous/asynchronous values * @param callback a synchronous/asynchronous test * @return eventually returns a new array with only values that have passed */ export function filter(items: (T | Promise)[], callback: Filterer): Promise { return processValuesAndCallback(items, callback).then(function ({ results, values }) { let arr: T[] = []; for (let i = 0; i < results.length; i++) { results[i] && arr.push(values[i]); } return arr; }); } /** * Find the first value matching a filter function * @param items a collection of synchronous/asynchronous values * @param callback a synchronous/asynchronous test * @return a promise eventually containing the item or undefined if a match is not found */ export function find(items: (T | Promise)[], callback: Filterer): Promise { return findIndex(items, callback).then(function (i) { return i >= 0 ? items[i] : undefined; }); } /** * Find the first index with a value matching the filter function * @param items a collection of synchronous/asynchronous values * @param callback a synchronous/asynchronous test * @return a promise eventually containing the index of the matching item or -1 if a match is not found */ export function findIndex(items: (T | Promise)[], callback: Filterer): Promise { // TODO we can improve this by returning immediately return processValuesAndCallback(items, callback).then(function ({ results }) { for (let i = 0; i < results.length; i++) { if (results[i]) { return i; } } return -1; }); } /** * transform a list of items using a mapper function * @param items a collection of synchronous/asynchronous values * @param callback a synchronous/asynchronous transform function * @return a promise eventually containing a collection of each transformed value */ export function map(items: (T | Promise)[], callback: Mapper): Promise { return processValuesAndCallback(items, callback) .then(function ({ results }) { return results; }); } /** * reduce a list of items down to a single value * @param items a collection of synchronous/asynchronous values * @param callback a synchronous/asynchronous reducer function * @param [initialValue] the first value to pass to the callback * @return a promise eventually containing a value that is the result of the reduction */ export function reduce(items: (T | Promise)[], callback: Reducer, initialValue?: U): Promise { let args: any[] = array.from(arguments); args.unshift(findNextValueIndex); return generalReduce.apply(this, args); } export function reduceRight(items: (T | Promise)[], callback: Reducer, initialValue?: U): Promise { let args: any[] = array.from(arguments); args.unshift(findLastValueIndex); return generalReduce.apply(this, args); } export function series(items: (T | Promise)[], operation: Mapper): Promise { return generalReduce(findNextValueIndex, items, function (previousValue, currentValue, index, array) { const result = operation(currentValue, index, array); if ((> result).then) { return (> result).then(function (value) { previousValue.push(value); return previousValue; }); } previousValue.push(result); return previousValue; }, []); } export function some(items: Array>, callback: Filterer): Promise { return testAndHaltOnCondition(true, items, callback); } export interface Filterer extends Mapper {} export interface Mapper { (value: T, index: number, array: T[]): (U | Thenable); } export interface Reducer { (previousValue: U, currentValue: T, index: number, array: T[]): (U | Thenable); }