Node.js design patterns:
Asynchronous serial iteration over a collection using callbacks.

This is an example of a very common Node.js design pattern.
Serial asynchronous iteration over collections (arrays) using callbacks.

If you want to learn more about common (and not-so-common) Node.js design patterns, check this out.
Book Node.js design patterns by Mario Casciaro and Luciano Mammino modified by L3n4r0x support for typescript. source

Features

  • iterate without callback (auto detect callback is called or not)
  • support asynchronous or synchronous callback function
  • typescript generic type supported

Javascript Asynchronous serial iteration over an array collection

"use strict";
/**
 * A callback based function that allows to iterate over a collection
 * and perform asynchronous actions on every element. The processing is
 * done is series, so an element is started only when the previous one
 * has been completed. In case of error the whole processing is interrupted
 * early and the error is propagated to the finalCallback to let the caller
 * decide how to handle it.
 *
 *
 * @param collection - The generic array of elements to iterate over
 * @param iteratorCallback - The callback based function that will
 *        be used to process the current element.
 *        It receives the current element as first paramenter and a callback
 *        as second parameter. The callback needs to be invoked to indicate
 *        the end of the asynchronous processing. It can be called passing an
 *        error as first parameter to propagate an error)
 * @param finalCallback - A function that is called when all the
 *        items have been processed or when an error occurred. If an error
 *        occurred the function will be invoked with a single argument
 *        representing the error.
 */
const iterateSeries = (collection, iteratorCallback, finalCallback) => {
    const stoppingPoint = collection.length;
    function iterate(index) {
        // console.log('it', index);
        if (index === stoppingPoint) {
            // console.log('final');
            return finalCallback();
        }
        else {
            const current = collection[index];
            let called = false;
            const done = function (err) {
                called = true;
                if (err) {
                    return finalCallback(err);
                }
                return iterate(index + 1);
            };
            if (iteratorCallback instanceof Promise) {
                iteratorCallback(current, done).then(done);
            }
            else {
                iteratorCallback(current, done);
            }
            if (!called)
                done();
        }
    }
    iterate(0);
};

Typescript Asynchronous serial iteration over an array collection

/**
 * A callback based function that allows to iterate over a collection
 * and perform asynchronous actions on every element. The processing is
 * done is series, so an element is started only when the previous one
 * has been completed. In case of error the whole processing is interrupted
 * early and the error is propagated to the finalCallback to let the caller
 * decide how to handle it.
 *
 *
 * @param collection - The generic array of elements to iterate over
 * @param iteratorCallback - The callback based function that will
 *        be used to process the current element.
 *        It receives the current element as first paramenter and a callback
 *        as second parameter. The callback needs to be invoked to indicate
 *        the end of the asynchronous processing. It can be called passing an
 *        error as first parameter to propagate an error)
 * @param finalCallback - A function that is called when all the
 *        items have been processed or when an error occurred. If an error
 *        occurred the function will be invoked with a single argument
 *        representing the error.
 */
const iterateSeries = <T extends any[]>(
  collection: T,
  iteratorCallback:
    | ((currentElement: T[number], done: (...args: any[]) => any) => any)
    | ((currentElement: T[number], done: (...args: any[]) => any) => Promise<any>),
  finalCallback: (err?: Error) => any | Promise<any>
) => {
  const stoppingPoint = collection.length;

  function iterate(index: number) {
    // console.log('it', index);
    if (index === stoppingPoint) {
      // console.log('final');
      return finalCallback();
    } else {
      const current = collection[index];
      let called = false;
      const done = function (err?: Error | undefined) {
        called = true;
        if (err) {
          return finalCallback(err);
        }

        return iterate(index + 1);
      };
      if (iteratorCallback instanceof Promise) {
        iteratorCallback(current, done).then(done);
      } else {
        iteratorCallback(current, done);
      }
      if (!called) done();
    }
  }

  iterate(0);
};

Usage Asynchronous serial iteration over an array collection

const deployInfo = [
  {
    name: 'root',
    dest: path.join(__dirname, '.deploy_git'),
    url: 'https://github.com/dimaslanjaka/dimaslanjaka.github.io',
    ref: 'master'
  },
  {
    name: 'docs',
    dest: path.join(__dirname, '.deploy_git/docs'),
    url: 'https://github.com/dimaslanjaka/docs',
    ref: 'master'
  },
  {
    name: 'chimeraland',
    dest: path.join(__dirname, '.deploy_git/chimeraland'),
    url: 'https://github.com/dimaslanjaka/chimeraland',
    ref: 'gh-pages'
  },
  {
    name: 'page',
    dest: path.join(__dirname, '.deploy_git/page'),
    url: 'https://github.com/dimaslanjaka/page',
    ref: 'gh-pages'
  }
];

iterateSeries(
  deployInfo,
  function (info) {
    console.log(`writing ${info.dest}`);
  },
  function (err) {
    if (err) {
      console.error(err);
      process.exit(1);
    }
  }
);

Live Playground

const path_1 = require("path") /** * A callback based function that allows to iterate over a collection * and perform asynchronous actions on every element. The processing is * done is series, so an element is started only when the previous one * has been completed. In case of error the whole processing is interrupted * early and the error is propagated to the finalCallback to let the caller * decide how to handle it. * * * @param collection - The generic array of elements to iterate over * @param iteratorCallback - The callback based function that will * be used to process the current element. * It receives the current element as first paramenter and a callback * as second parameter. The callback needs to be invoked to indicate * the end of the asynchronous processing. It can be called passing an * error as first parameter to propagate an error) * @param finalCallback - A function that is called when all the * items have been processed or when an error occurred. If an error * occurred the function will be invoked with a single argument * representing the error. */ const iterateSeries = (collection, iteratorCallback, finalCallback) => { const stoppingPoint = collection.length; function iterate(index) { // console.log('it', index); if (index === stoppingPoint) { // console.log('final'); return finalCallback(); } else { const current = collection[index]; let called = false; const done = function (err) { called = true; if (err) { return finalCallback(err); } return iterate(index + 1); }; if (iteratorCallback instanceof Promise) { iteratorCallback(current, done).then(done); } else { iteratorCallback(current, done); } if (!called) done(); } } iterate(0); }; // usage samples const deployInfo = [ { name: 'root', dest: path_1.join(__dirname, '.deploy_git'), url: 'https://github.com/dimaslanjaka/dimaslanjaka.github.io', ref: 'master' }, { name: 'docs', dest: path_1.join(__dirname, '.deploy_git/docs'), url: 'https://github.com/dimaslanjaka/docs', ref: 'master' }, { name: 'chimeraland', dest: path_1.join(__dirname, '.deploy_git/chimeraland'), url: 'https://github.com/dimaslanjaka/chimeraland', ref: 'gh-pages' }, { name: 'page', dest: path_1.join(__dirname, '.deploy_git/page'), url: 'https://github.com/dimaslanjaka/page', ref: 'gh-pages' } ]; iterateSeries(deployInfo, function (info) { console.log(`writing ${info.dest}`); }, function (err) { if (err) { console.error(err); process.exit(1); } });