7

In a Node app, I need to iterate through some items in a synchronous fashion, but some of the operations inside the loop are asynchronous. My code right now looks like so:

someAPIpromise().then((items) => {
   items.forEach((item) => {
      Promise.all[myPromiseA(item), myPromiseB(item)]).then(() => {
         doSomethingSynchronouslyThatTakesAWhile();
      });
    }
}

This works wonders when the items is an array of 1. But, once there's more than one item, promise.all() will just fire off instantly for every item in the array, without waiting for the operation in the loop to end.

All that to say... how can I ensure that the entire operation for each item in the array is run synchronously (even if some operations are async and return a promise)?

Thanks so much!

N

napo
  • 869
  • 9
  • 19
  • Have you tried passing `items` to `Promise.all()`, using `.reduce()` to iterate; and removing `.forEach()`? – guest271314 May 07 '16 at 14:07
  • Not sure I follow... each `item` in `items` should be its own synchronous operation. Are you suggesting resolving the promise on all items at once? – napo May 07 '16 at 14:13
  • Are elements within `items` functions? – guest271314 May 07 '16 at 14:22
  • Push values to an array; if element in `items` array is a function, call function, if element is `Promise` return `Promise`, if element if neither function or `Promise`, return value wrapped in `Promise.resolve()` – guest271314 May 07 '16 at 14:43

5 Answers5

4

You're constructing several promises, but they are all asynchronous. You construct Promise1, Promise2, Promise3, ... but once they're in the wild they are all firing simultaneously. If you want synchronous behavior you've got to chain them together so Promise1's .then() executes Promise2 and so on. In the past I've used Array.reduce for this.

someAPIpromise().then((items) => {
    items.reduce((accumulator, current) =>
        accumulator.then(() =>
             Promise.all[myPromiseA(item), myPromiseB(item)]).then(() => 
                 doSomethingSynchronouslyThatTakesAWhile();
             )
        )
    , Promise.resolve());

You can write this as a helper function if you like, which may make things clearer.

function execSequentially (arr, func) {
    return arr.reduce(
        (accumulator, current) => accumulator.then(() => func(current)), 
        Promise.resolve());
}

That function is executed as

execSequentially(items, item => console.log(item));

of course replacing console.log with what you want to do.

The helper function approach is also less invasive of a change. The helper applied to your original code:

someAPIpromise().then((items) => {
   execSequentially(items, (item) =>
      Promise.all[myPromiseA(item), myPromiseB(item)]).then(() => {
         doSomethingSynchronouslyThatTakesAWhile();
      });
   );
});
Paarth
  • 9,687
  • 4
  • 27
  • 36
  • Great explanation, @Paarth ! The use of `reduce` makes perfect sense... Alas, still no luck, and every item in the array is firing off at the same time. In fact, the way I tested it was as so: First, put a `console.log` right before the `Promise.all`, then a `console.log` after then thenable, and I see both "Hey, I'm getting started here" firing off at the same time, and all the async calls (say, array has two items ---> four total async calls) go off at once. – napo May 07 '16 at 19:02
  • @napo, are you making sure to *return* the inner promise from Promise.all? Note that expression-bodied lambdas like I've got in my sample do it implicitly if you have blocks you need to return it explicitly. Apologies for the late reply – Paarth May 10 '16 at 01:57
  • No prob! See answer below for how it was eventually resolved. Pretty close to what you had, actually. Thanks! – napo May 10 '16 at 02:33
  • @napo, did you use the code in my answer with the expression-bodied lambdas? I'm looking at what you have marked as your answer and it looks nearly exactly the same. The only difference is that you're constructing another promise and using it's resolution function, which should be equivalent to what I'm doing with just returning the `Promise.all` promise. – Paarth May 10 '16 at 03:39
  • In part, yes... but I had to return a new promise, which is what was missing from your snippet and thus causing everything to fire off async. I've given it a +1 because it was still a good base for the end result. Thanks much! – napo May 10 '16 at 13:25
  • @napo Can you show me what you tried to do with my sample before you arrived at your code? At this point I'm trying to see if I am missing something in my sample code because I've definitely used this same pattern successfully. – Paarth May 10 '16 at 13:32
1

You should be able to remove .forEach(); use Array.prototype.reduce() to return an array of Promise values to Promise.all(). If element with items is a function, call function, else wrap within Promise.resolve(), which should return results in same order as in items array

See Promise.all()

Promise.all passes an array of values from all the promises in the iterable object that it was passed. The array of values maintains the order of the original iterable object, not the order that the promises were resolved in. If something passed in the iterable array is not a promise, it's converted to one by Promise.resolve.

var arr = [1, // not asynchronous
  function j() {
    return new Promise(function(resolve) {
      setTimeout(function() {
        resolve(2)
      }, Math.floor(Math.random() * 10000))
    })
  }, // asynchronous
  3, // not asynchronous
  function j() {
    return new Promise(function(resolve) {
      setTimeout(function() {
        resolve(4)
      }, Math.floor(Math.random() * 3500))
    })
  }, // asynchronous
  5, // not asynchronous
  Promise.resolve(6), // asynchronous
  7
];

Promise.all(arr.reduce(function(p, next) {
    var curr = Promise.resolve(typeof next === "function" ? next() : next);
    return p.concat.apply(p, [curr.then(function(data) {
      console.log(data);
      return data
    })]);
  }, []))
  .then(function(data) {
    console.log("complete", data)
  })

An alternative approach would be to use Array.prototype.shift() , Promise.resolve(), .then(), recursion

function re(items, res) {
  if (items.length) {
    var curr = items.shift();
    return Promise.resolve(
      typeof curr === "function" 
      ? curr() 
      : curr
    ).then(function(data) {
      // values from `arr` elements should be logged in sequential order
      console.log(data);
      res.push(data)
    }).then(re.bind(null, items, res))
  } else {
    return ["complete", res]
  }
}

var _items = arr.slice(0);

re(_items, [])
.then(function(complete) {
  console.log(complete)
})

var arr = [1, // not asynchronous
  function j() {
    return new Promise(function(resolve) {
      setTimeout(function() {
        resolve(2)
      }, Math.floor(Math.random() * 10000))
    })
  }, // asynchronous
  3, // not asynchronous
  function j() {
    return new Promise(function(resolve) {
      setTimeout(function() {
        resolve(4)
      }, Math.floor(Math.random() * 3500))
    })
  }, // asynchronous
  5, // not asynchronous
  Promise.resolve(6), // asynchronous
  7
];

function re(items, res) {
  if (items.length) {
    var curr = items.shift();
    return Promise.resolve(
      typeof curr === "function" 
      ? curr() 
      : curr
    ).then(function(data) {
      // values from `arr` elements should be logged in sequential order
      console.log(data);
      res.push(data)
    }).then(re.bind(null, items, res))
  } else {
    return ["complete", res]
  }
}
var _items = arr.slice(0);
re(_items, [])
  .then(function(complete) {
    console.log(complete)
  })
guest271314
  • 1
  • 15
  • 104
  • 177
  • I kinda see what you're doing there, thanks for the explanation! That said... are you saying I should promisify every item in the array, then calling `promise.all` on that entire array? – napo May 07 '16 at 19:09
  • @napo Call `Promise.all()` on the array, whether values are `Promise` or not `Promise`, use `.reduce()` to either call the function or return the value. If the original value is not a `Promise` it you do not need use `Promise.resolve()` or use `new Promise()` constructor on the value. The results will still be returned in the order of the items in the original array at `.then()` following `Promise.all()`. For example, if you adjust first `setTimeout` duration to `10000`, `2` should still be returned to `.then()` following `.all()` in sequential order – guest271314 May 07 '16 at 19:14
  • @napo Are promises or values in `items` resolved in sequential order using `js` at post? – guest271314 May 07 '16 at 19:21
  • Thanks for the follow-up. Yes, items are called in sequential order. That said, I can see the same items for each of the `Promise.all()` getting executed at the same time. Case in point: One of the promisified methods downloads a handful of assets from an HTTP resource. If I look at network traffic, I see assets pertaining to both items from array getting downloaded, which shouldn't happen — I should be able to download assets in `promise.all()` for item 1, then do my synchronous stuff, then move on to item 2. Makes sense? – napo May 07 '16 at 20:45
  • _"I should be able to download assets in promise.all() for item 1, then do my synchronous stuff, then move on to item 2. Makes sense?"_ Do not believe the order in which values are returned is guarantted to be in sequential order using `Promise.all()` alone. Though the second approach should not proceed to the next item within the array until the previous item is resolved. – guest271314 May 07 '16 at 20:49
  • I'm not concerned about the order in which `Promise.all([something(item[0]), somethingElse(item[0])]).then()` promises are resolved. That's all well and good. I'm more concerned about `item[0]` being done for good before the loop moves on to `item[1]`. – napo May 07 '16 at 20:51
  • @napo Yes, then first option would not achieve this; see updated post. Though second option using `.shift()`, recursion should. Also note, to preserve original array, you can use `var _items = items.slice(0)` , call `re` with `_items` as parameter instead of `items`; `.shift()` removes element from the array. – guest271314 May 07 '16 at 20:56
  • Yes, looking at your second option now. Where would you call it from, however? I see what you're doing just fine, but not sure where you're passing the function that needs to be promisified. Are you saying you should call it right within the `forEach`, or in place of, or...? Thanks! – napo May 07 '16 at 21:02
  • @napo In place of. Are there actually two functions to call for each element within the `items` array? What does `myPromiseA(item), myPromiseB(item)` represent? Same value being called at two different functions? Do elements of `items` have two index for each element ? Or should `items` be iterated only sequentially _"I'm more concerned about item[0] being done for good before the loop moves on to item[1]"_ 0-n ? – guest271314 May 07 '16 at 21:05
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/111315/discussion-between-napo-and-guest271314). – napo May 07 '16 at 21:16
1

All righty... the way we were able to get it to work: array.reduce() with the help of Promises. The end result:

myAsyncAPIcall.then(items => {
    items.reduce((current, nextItem) => {
        return current.then(() => {
          return new Promise(res => {
             Promise.all([myPromiseA(nextItem), myPromiseB(nextItem]).then(() => {
               someSynchronousCallThatTakesAWhile(nextItem);
               res();
             }).catch(err => {
                   console.log(err);
             });
          });
        });
    }, Promise.resolve())
})

The way it works is, by wrapping each item of the array in its own Promise(resolve, reject), we can ensure that each iteration is run synchronously, as the completion of one iteration will trigger the need to resolve the next Promise, and so on and so forth. Within each promise resolving, calls can get kicked off asynchronously as much as you want, with the knowledge that they will only be scoped to the parent promise until it finishes.

I hope this helps folks!

napo
  • 869
  • 9
  • 19
  • instead of returning a `new Promise(res => ...)` if you remove that and just `return Promise.all(...)` does that work for you? – Paarth May 10 '16 at 13:38
0

How about keeping the forEach...

var stopAllProcessingOnServerLowValue= false;

function someAPIpromise(){
    var arr = [
        {id:123, urlVal:null},
        {id:456, urlVal:null},
        {id:789, urlVal:null},
        {id:101112, urlVal:null}
    ];

    return new Promise(function(resolve){
        setTimeout(function(){
            resolve(arr)
        }, 3000);
    })
}

function extractSomeValueRemotely(url){
    return new Promise(function(resolve, reject){
        console.log("simulate an async connection @ %s to request a value", url);
        setTimeout(function(){
            var someRandom = Math.round(Math.random()*7) + 1;
            console.log("%s responded with %s", url, someRandom);
            if(someRandom > 4){
                resolve(someRandom);
            }
            else{
                var issue = "Urls result is too low ("+someRandom+" <= 4).";
                console.warn(issue+".It will be set to -1");
                if(stopAllProcessingOnServerLowValue){
                    reject(issue+".Operation rejected because one or mole server results are too low ["+someRandom+"].");
                }
                else{
                    resolve(-1);
                }
            }
        }, 1500*Math.round(Math.random()*7) + 1);
    });
}

function addAnotherExtraParamToItem(_item){
    return new Promise(function(resolve, reject){
        setTimeout(function(){
            console.log("setting extra2 on %s", _item.id);
            _item['extra'] = "additional_processing_"+_item.id;
            resolve(_item);
        }, 1500*Math.round(Math.random()*5) + 1);
    });
}

function addOrderIndexToItem(_item, _order){
    return new Promise(function(resolve, reject){
        setTimeout(function(){
            console.log(">> setting order %s on %s",_order,  _item.id);
            _item['order'] = _order;
            resolve(_item);
        }, 1500*Math.round(Math.random()*3) + 1);
    });
}

someAPIpromise().then(function(items){

    var perItemPromises = [];
    items.forEach(function(item, idx){

        perItemPromises.push(

            new Promise(function(pulseItemResolve, pulseItemReject){
                var itemStepsPromises =  [];
                itemStepsPromises.push(addAnotherExtraParamToItem(item));

                itemStepsPromises.push(extractSomeValueRemotely("http://someservice:777/serve-me")
                    .catch(
                        function(reason){
                            //the entire item will be rejected id
                            pulseItemReject(reason);
                        })
                );

                itemStepsPromises.push(addOrderIndexToItem(item, idx));

                //promise that ensure order of execution on all previous async methods
                Promise.all(itemStepsPromises).then(function(values){
                    //0 - first is result from addAnotherExtraParamToItem
                    var theItem = values[0]; //it returns the item itself
                    //urlVal has not been set yet

                    // 1 - second promise return the url result
                    var serverResult = values[1];

                    //2 - third promise add the order index but we do not care to inspect it because theItem reference in value[0] has been already updated.
                    // console.info(values[2]);

                    //sets the url result in the item
                    theItem.urlVal = serverResult;
                    console.log("urlVal set to:", theItem.urlVal);

                    //resolve the prepared item
                    pulseItemResolve(theItem);

                });
            })
                .catch(function(reason){
                    //escalate error
                    throw new Error(reason);
                })
        )

    });

    Promise.all(perItemPromises).then(function(resultsInAllItems){
        console.info("Final results:");
        console.info(resultsInAllItems);
    }).catch(function(finalReject){
        console.error("Critical error:",finalReject);
    })


});
MFAL
  • 1,090
  • 13
  • 19
0

After much research, the definitive answer for me was here...

I've read bunches of solutions for having a useful pure JavaScript (no addons) -Promise Iterator- that could be easily used (one line) all over my projects, and finally I've found this solution by Salketer:

function one_by_one(objects_array, iterator, callback) {
    var start_promise = objects_array.reduce(function (prom, object) {
        return prom.then(function () {
            return iterator(object);
        });
    }, Promise.resolve()); // initial
    if(callback){
        start_promise.then(callback);
    }else{
        return start_promise;
    }
}

For details and example of usage visit the link.

It also allows to handle a callback directly.

It was simply the most logical and reusable method I've found after many days of struggling with Promise iterations and testing MULTIPLE solutions from many questions, blogs and official sites.

If you're also struggling for a definitive answer, give it a try.

Community
  • 1
  • 1
DavidTaubmann
  • 3,223
  • 2
  • 34
  • 43