async/iterators.js

/**
 * Asynchronous tools.
 *
 * @module async
 * @license Apache-2.0
 * @copyright Mat. 2018-present
 */

import { head } from "../array/list";
import { curry } from "../func/curry";
import { inc } from "../math/arithmetic";
import { isArray, isFunction } from "../type/check";
import { btquote } from "../utils/misc";




/**
 * Asynchronous version of standard `Array.prototype.map` function.
 *
 * - `arr` - array to operate on
 * - `f` - async or sync function with signature:
 *     - `this` - bound to `arr`
 *     - `element` - currently processed element
 *     - `index` - current index
 *
 * `f` can return `Promise.<unknown>` or `<unknown>`
 *
 * Example:
 *
 * ```
 * (async () => {
 *     let x = await async.map(
 *         array.range(10),
 *         x => async.timeout(() => 4*x, 100*x)
 *     )
 *     console.log(x)
 * })()
 * ```
 *
 * @async
 * @function map
 * @param {Array} arr
 * @param {Function} f
 * @returns {Promise.<Array>}
 */
export const map = curry((arr, f) => {
    const results = [];
    let i = 0;

    return new Promise((resolve, reject) => {
        const progress = (r) => {
            results.push(r);
            i = inc(i);
            if (i < arr.length) {
                Promise
                    .resolve(f.call(arr, arr[i], i))
                    .then(progress).catch(reject);
            } else resolve(results);
        };

        if (isArray(arr)  &&  isFunction(f)) {
            if (arr.length > 0) {
                Promise
                    .resolve(f.call(arr, head(arr), 0))
                    .then(progress).catch(reject);
            } else resolve(results);
        } else throw new TypeError(
            "async.map() expected array and function, " +
            `got ${btquote(arr)} and ${btquote(f)}`,
        );
    });
});




/**
 * Asynchronous version of standard `Array.prototype.map` function.
 *
 * *Implementation that does paralell execution*.
 *
 * - `arr` - array to operate on
 * - `f` - async or sync function with signature:
 *     - `this` - bound to `arr`
 *     - `element` - currently processed element
 *     - `index` - current index
 *
 * `f` can return `Promise.<unknown>` or `<unknown>`
 *
 * Example:
 *
 * ```
 * (async () => {
 *     let x = await async.parMap(
 *         array.range(10),
 *         x => async.timeout(() => 4*x, 100*x)
 *     )
 *     console.log(x)
 * })()
 * ```
 *
 * @async
 * @function parMap
 * @param {Array} arr
 * @param {Function} f
 * @returns {Promise.<Array>}
 */
export const parMap = curry((arr, f) =>
    Promise.all(arr.map((el, i) => Promise.resolve(f.call(arr, el, i)))),
);




/**
 * Asynchronous version of standard `Array.prototype.reduce` function.
 *
 * - `arr` - array to operate on
 * - `f` - async or sync function with signature:
 *     - `this` - bound to `arr`
 *     - `acc` - accumulates the `f`'s return values; it is
 *         the accumulated value previously returned
 *         in the last invocation of `f`, or `initAcc`, if supplied.
 *     - `element` - currently processed element
 *     - `index` - current index
 * - `initAcc` - value to use as the first argument to the first call
 *     of the `f`. If no initial value is supplied, the first element
 *     in the array will be used.
 *
 * `f` can return `Promise.<unknown>` or `<unknown>`
 *
 * Example:
 *
 * ```
 * (async () => {
 *     let x = await async.reduce(
 *         array.range(10),
 *         (acc, x) => async.timeout(() => acc+x, 100*x),
 *         0
 *     )
 *     console.log(x)
 * })()
 * ```
 *
 * @async
 * @function reduce
 * @param {Array} arr
 * @param {Function} f
 * @param {unknown} [initAcc]
 * @returns {Promise.<unknown>}
 */
export const reduce = curry((arr, f, initAcc) => {
    let i = 0;

    return new Promise((resolve, reject) => {
        const progress = r => {
            i = inc(i);
            if (i < arr.length) {
                Promise
                    .resolve(f.call(arr, r, arr[i], i))
                    .then(progress).catch(reject);
            } else resolve(r);
        };

        if (isArray(arr)  &&  isFunction(f)) {
            if (arr.length > 0) {
                Promise
                    .resolve(f.call(arr, initAcc ?? head(arr), head(arr), 0))
                    .then(progress).catch(reject);
            } else resolve(initAcc);
        } else throw new TypeError(
            "async.reduce() expected array and function, " +
            `got ${btquote(arr)} and ${btquote(f)}`,
        );
    });
});