Source: loops.js

'use strict';

/**
 * Asynchronous `for` loop.
 * @function for
 * @param {function} initial Sets up initial condition for loop. ie: `() => i = 0`
 * @param {function} condition Checks whether loop body should be executed. ie: `() => i < 10`
 * @param {function} update Executes after the loop body. ie: `() => i++`
 * @param {function} thunk Loop body. Returns a Promise.
 * @returns {Promise} Resolves if successful; otherwise rejects with error.
 */
let forLoop = (initial, condition, update, thunk) => {
  initial();
  return _loop(condition, update, thunk);
};

/**
 * Asynchronous `while` loop.
 * @function while
 * @param {function} condition Checks whether loop body should be executed. ie: `() => i < 10`
 * @param {function} thunk Loop body. Returns a Promise.
 * @returns {Promise} Resolves if successful; otherwise rejects with error.
 */
let whileLoop = (condition, thunk) => {
  return _loop(condition, () => {}, thunk);
};

let _loop = (condition, update, thunk) => {
  return new Promise((resolve, reject) => {
    if(!condition()) {
      resolve();
    }
    else {
      thunk()
        .then(() => {
          update();
          return _loop(condition, update, thunk).then(resolve);
        })
        .catch((error) => {
          switch(error) {
            case BREAK_ERROR:
              resolve();
              break;
            case CONTINUE_ERROR:
              update();
              return _loop(condition, update, thunk).then(resolve);
            default:
              reject(error);
          }
        });
    }
  });
};

/**
 * Asynchronous `doWhile` loop.
 * @function doWhile
 * @param {function} condition Checks whether loop should continue or quit. ie: `() => i < 10`
 * @param {function} thunk Loop body. Returns a Promise.
 * @returns {Promise} Resolves if successful; otherwise rejects with error.
 */
let doWhileLoop = (condition, thunk) => {
  return _doLoop(condition, thunk);
};

let _doLoop = (condition, thunk) => {
  return new Promise((resolve, reject) => {
    thunk()
      .then(() => {
        if(!condition()) {
          resolve();
        }
        else {
          return _doLoop(condition, thunk).then(resolve);
        }
      })
      .catch((error) => {
        switch(error) {
          case BREAK_ERROR:
            resolve();
            break;
          case CONTINUE_ERROR:
            if(!condition()) {
              resolve();
            }
            else {
              return _doLoop(condition, thunk).then(resolve);
            }
            break;
          default:
            reject(error);
        }
      });
  });
};

/**
 * Asynchronous `map`.
 * Each iteration is passed a the following: `(items[index], index, items)`.
 * Thunk should resolve a value, which will be stored in array that is built up each iteration.
 * Index starts at 0 and is incremented each iteration.
 * NOTE: modifying `items` will impact subsequent iterations.
 * @function map
 * @param {object[]} items Collection to iterator over.
 * @param {function} thunk Loop body. Returns a Promise.
 * @returns {Promise} Resolves with array of values if successful; otherwise rejects with error.
 */
const mapLoop = (items, thunk) => {
  return _mapLoop(items, thunk, 0, items.slice());
};

const _mapLoop = (items, thunk, index, result) => {
  return new Promise((resolve, reject) => {
    if(index >= items.length) {
      resolve(result);
    }
    else {
      thunk(items[index], index, items)
        .then((v) => {
          result[index] = v;
          return _mapLoop(items, thunk, index + 1, result).then(resolve);
        })
        .catch((error) => {
          switch(error) {
            case BREAK_ERROR:
              resolve(result);
              break;
            case CONTINUE_ERROR:
              return _mapLoop(items, thunk, index + 1, result).then(resolve);
            default:
              reject(error);
          }
        });
    }
  });
};

/**
 * Asynchronous `reduce`.
 * Each iteration is passed a the following: `(accumulator, items[index], index, items)`.
 * Thunk should resolve a value, which is used as `accumulator` input for next iteration.
 * Index starts at 0 and is incremented each iteration.
 * NOTE: modifying `items` will impact subsequent iterations.
 * @function reduce
 * @param {object[]} items Collection to iterator over.
 * @param {function} thunk Loop body. Returns a Promise and accepts a value.
 * @param {object|number} [initialValue = 0] Initial value for accumulator.
 * @returns {Promise} Resolves with reduced value if successful; otherwise rejects with error.
 */
const reduceLoop = (items, thunk, initialValue) => {
  return _reduceLoop(items, thunk, 0, initialValue || 0);
};

const _reduceLoop = (items, thunk, index, accumulator) => {
  return new Promise((resolve, reject) => {
    if(index >= items.length) {
      resolve(accumulator);
    }
    else {
      thunk(accumulator, items[index], index, items)
        .then((v) => {
          return _reduceLoop(items, thunk, index + 1, v).then(resolve);
        })
        .catch((error) => {
          switch(error) {
            case BREAK_ERROR:
              resolve(accumulator);
              break;
            case CONTINUE_ERROR:
              return _reduceLoop(items, thunk, index + 1, accumulator).then(resolve);
            default:
              reject(error);
          }
        });
    }
  });
};

const BREAK_ERROR = 'ASYNC_LOOPS_BREAK';
const CONTINUE_ERROR = 'ASYNC_LOOPS_CONTINUE';

module.exports = {
  for: forLoop,
  while: whileLoop,
  doWhile: doWhileLoop,
  map: mapLoop,
  reduce: reduceLoop,
  break: BREAK_ERROR,
  continue: CONTINUE_ERROR
};