0

Obviously, given a list l and an function f that returns a promise, I could do this:

Promise.all(l.map(f));

The hard part is, I need to map each element, in order. That is, the mapping of the first element must be resolved before the the next one is even started. I want to prevent any parallelism.

I have an idea how to do this, which I will give as an answer, but I am not sure it's a good answer.

Edit: some people are under the impression that since Javascript is itself single-threaded, parallelism is not possible in Javascript.

Consider the following code:

const delay = t => new Promise(resolve => setTimeout(resolve, t));
mapAsync([3000, 2000, 1000], delay).then(n => console.log('beep: ' + n));

A naïve implementation of mapAsync() would cause "beep" to be printed out once a second for three seconds -- with the numbers in ascending order -- but a correct one would space the beeps out increasingly over six seconds, with the number in descending orders.

For a more practical example, imagine a function that invoked fetch() and was called on an array of thousands of elements.

Further Edit:

Somebody didn't believe me, so here is the Fiddle.

Michael Lorton
  • 43,060
  • 26
  • 103
  • 144

3 Answers3

1
const mapAsync = (l, f) => new Promise((resolve, reject) => {
  const results = [];
  const recur = () => {
    if (results.length < l.length) {
      f(l[results.length]).then(v => {
        results.push(v);
        recur();
      }).catch(reject);
    } else {
      resolve(results);
    }
  };
  recur();
});

EDIT: Tholle's remark led me to this far more elegant and (I hope) anti-pattern-free solution:

const mapAsync = (l, f) => {
  const recur = index =>
    index < l.length
      ? f(l[index]).then(car => recur(index + 1).then(cdr => [car].concat(cdr)))
      : Promise.resolve([]);

  return recur(0);
};

FURTHER EDIT:

The appropriately named Try-catch-finally suggest an even neater implementation, using reduce. Further improvements welcome.

const mapAsync2 = (l, f) =>
  l.reduce(
    (promise, item) =>
      promise.then(results => 
        f(item).then(result => results.concat([result]))),
    Promise.resolve([])
  );
Community
  • 1
  • 1
Michael Lorton
  • 43,060
  • 26
  • 103
  • 144
  • I like your solution. I asked [**this similar question**](http://stackoverflow.com/questions/31575001/looping-with-promises) a while back. – Tholle Jan 28 '17 at 10:41
1

Rather than coding the logic yourself I'd suggest using async.js for this. Since you're dealing with promises use the promisified async-q library: https://www.npmjs.com/package/async-q (note: the documentation is much easier to read on github:https://github.com/dbushong/async-q)

What you need is mapSeries:

async.mapSeries(l,f).then(function (result) {
    // result is guaranteed to be in the correct order
});

Note that the arguments passed to f is hardcoded as f(item, index, arr). If your function accept different arguments you can always wrap it up in another function to reorder the arguments:

async.mapSeries(l,function(x,idx,l){
    return f(x); // must return a promise
}).then(function (result) {
    // result is guaranteed to be in the correct order
});

You don't need to do this if your function accepts only one argument.


You can also just use the original callback based async.js:

async.mapSeries(l,function(x,idx,l){
    function (cb) {
        f(x).then(function(result){
            cb(null, result); // pass result as second argument,
                              // first argument is error
        });
    }
},function (err, result) {
    // result is guaranteed to be in the correct order
});
slebetman
  • 109,858
  • 19
  • 140
  • 171
0

You can't use map() on its own since you should be able to handle the resolution of the previous Promise. There was a good example of using reduce() for sequencing Promises in an Google article.

reduce() allowes you to "chain" the Promise of the current item with the Promise of the previous item. To start the chain, you pass a resolved Promise as initial value to reduce().

Assume l as the input data and async() to modify the data asynchronously. It will just multiply the input data by 10.

var l = [1, 2, 3 ,4];

function async(data) {
    console.log("call with ", data);
    return new Promise((resolve, reject) => {
        setTimeout(() => { console.log("resolve", data); resolve(data * 10); }, 1000);
    });
}

This is the relevant code (its function is inline-commented)

// Reduce the inout data into a Promise chain
l.reduce(function(sequencePromise, inValue) {
    /* For the first item sequencePromise will resolve with the value of the 
     * Promise.resolve() call passed to reduce(), for all other items it's 
     * the previous promise that was returned by the handler in the next line.
     */
    return sequencePromise.then(function(responseValues) {
        /* responseValues is an array, initially it's the empty value passed to
         * reduce(), for subsequent calls it's the concat()enation result.
         *
         * Call async with the current inValue.
         */
        return async(inValue).then((outValue) => {
            /* and concat the outValue to the 
             * sequence array and return it. The next item will receive that new 
             * array as value to the resolver of sequencePromise.
             */
            return responseValues.concat([outValue]);
        });
    });
}, Promise.resolve([]) /* Start with a resolved Promise */ ).then(function(responseValues){
    console.log(responseValues);
});

The console will finally log

Array [ 10, 20, 30, 40 ]
try-catch-finally
  • 7,436
  • 6
  • 46
  • 67
  • `You can't use map()` - you actually can - https://jsfiddle.net/jaromanda/d6qwf1x4/ - in some ways less elegant, but not unpossible :p – Jaromanda X Jan 28 '17 at 12:08
  • Well yes you're right - but you can't use `map()` on its own without surrounding code. It's the same with the `forEach()` code behind that Google dev link, the code's just a bit less elegant. – try-catch-finally Jan 28 '17 at 12:25
  • I like the substance of this answer but honestly, the code almost unreadable, and the comments actually make it worse. I'm accepting it (because it's right) but I put a far briefer version in [my own answer](http://stackoverflow.com/a/41908707/238884). – Michael Lorton Jan 29 '17 at 19:05
  • First, thanks for accepting my answer! The rationale behind the comments were to explain the function of the code. While you might understand its function without the comments other users coming across this post may appreciate the explanation. – try-catch-finally Jan 29 '17 at 19:13
  • 1
    I hate to criticize somebody who did exactly what I asked, out of the goodness of his heart, but you did also commit the Original Sin of commenting, repeating the code in comments. This is one line: `return responseValues.concat([outValue]);`. Here is your comment on the line: "concat the outValue to the sequence array and return it." Please don't do that. Not here, not in your real life. Code says what; comments are for why. Secondarily, don't use "value" as part of a variable name: all variables point to values. – Michael Lorton Jan 30 '17 at 19:57