5
\$\begingroup\$

I wrote this as a convenient way to batch together multiple, repetitive AJAX calls. I was working in angular at the time, so I use the $q service, but I'm sure it could be adapted for a more general promise framework. I find it so useful, I'm interested in polishing it for general consumption.

// A utility function that can run for-like loops with asynchronous processes.
// Use just like forEach, but you can launch something like an ajax call in the
// loop, as long as you return a promise. You can choose to run the calls in
// serial (one after the other) or in parallel (launching them all at once).
// Either way, you can attach a callback when they're all done, and an array of
// the promise resolve values of each step is available.
// For example, this would call doAsyncJob() for every item in myList, one
// after the other, and run doSomethingElse() once those were all done:

// $forEachAsync(myList, function (item) {
//     return doAsyncJob(item);
// }, 'serial').then(function (arrayOfJobResults) {
//     doSomethingElse();
// });
MyAngularApp.factory('$forEachAsync', function ($q) {
    'use strict';
    return function forEachAsync(arrayOrDict, f, serialOrParallel) {
        if (serialOrParallel === 'parallel') {
            // Iterate over the data, calling f immediately for each data
            // point. Collect all the resulting promises together for return
            // so further code can be executed when all the calls are done.
            return $q.all(forEach(arrayOrDict, f));
        }
        if (serialOrParallel === 'serial') {
            // Set up a deferred we control as a zeroth link in the chain,
            // which makes writing the loop easier.
            var serialDeferred = $q.defer(),
                serialPromise = serialDeferred.promise,
                returnValues = [];
            // Do NOT make all the calls immediately, instead embed each data
            // point and chain them in a series of `then`s.
            // Note: this makes the assumption that forEach iterates over both
            // arrays and objects by providing only two arguments.
            forEach(arrayOrDict, function (a, b) {
                serialPromise = serialPromise.then(function (value) {
                    returnValues.push(value);
                    return f(a, b);
                });
            });
            // Fire off the chain.
            serialDeferred.resolve();
            // Return the whole chain so further code can extend it, making
            // sure to return the resolve values of each iteration as a list.
            return serialPromise.then(function (value) {
                // Add the final resolve value, and slice off the first, which
                // is the "zeroth link" value and is always undefined.
                returnValues.push(value);
                return returnValues.slice(1);
            });
        }
        throw new Error(
            "Must be 'serial' or 'parallel', got " + serialOrParallel
        );
    };
});
\$\endgroup\$
  • 1
    \$\begingroup\$ In the serial case, it should be possible to construct returnValues in its entirety in the forEach loop without post-processing it. \$\endgroup\$ – Roamer-1888 Jun 30 '15 at 0:05
1
\$\begingroup\$

Regarding the desire to make an implementation of the $q service, it is useful too consider that angular's $q is a stripped down version of the excellent promise library q by, kriskowal which you can leverage independently of angular. You can also achieve it through other promise implementations such as bluebird and with native ES2015 promises where available.

As for the implementation itself, I strongly recommend removing the serialOrParallel and doing one of the following.

  1. Turn you function into an object which exposes a parallel and a sequential property. Ex:

    var forEach = { 
        parallel: function () {...},
        sequential: function() {...}
    } 
    
  2. Create 2 separate functions, one that traverses sequentially and one that traverses in parallel.

The reasoning behind this is that it makes the code clearer, removes the need for an exception, and makes it easier for consumers to do the right thing.

As @Roamer-1888 noted, In the serial case, it should be possible to construct returnValues in its entirety in the forEach loop without post-processing it.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.