Skip to content Skip to sidebar Skip to footer

How Can A Map An Asynchronous Function Over A List?

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 m

Solution 1:

constmapAsync = (l, f) => newPromise((resolve, reject) => {
  const results = [];
  constrecur = () => {
    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:

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

  returnrecur(0);
};

FURTHER EDIT:

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

constmapAsync2 = (l, f) =>
  l.reduce(
    (promise, item) =>
      promise.then(results =>f(item).then(result => results.concat([result]))),
    Promise.resolve([])
  );

Solution 2:

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){
    returnf(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
});

Solution 3:

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];

functionasync(data) {
    console.log("call with ", data);
    returnnewPromise((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.
         */returnasync(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 ]

Post a Comment for "How Can A Map An Asynchronous Function Over A List?"