2

How do you execute a function only after all the promises are resolved where you have to wait for async calls in loops within loops?

The code is simplified to the minimum

$scope.book = function(input) {

  //Get the cart items from DB
  ref.child('/cart/' + userId).once('value').then(function(cartSnap) {

    //Loop through the cart items
    cartSnap.forEach(function(cartItemSnap) {

      var itemId = cartItemSnap.key();

      //Get the inventory items from DB that are in the cart
      ref.child('/inventory/' + itemId).once('value').then(function(inventoryItem) {

        //Loop through booking IDs of inventoryItem
        inventoryItem.child('rentals').forEach(function(rentalSnap) {

          var rentalId = rentalSnap.key();

          //Get bookings from rental/active
          ref.child('/rental/'+ rentalId).once('value').then(function(activeRentals) {

            checkIfBookingIsAllowed();

          });
        });
      });
    });

    //Once everything was checked
    bookRental();
  });
};

To improve speed all the requests can be made in parallel but the final function bookRental() can only be called when everything is resolved.

Thanks for your help.

EDITED: Another try that failed. The Promise.all('collector') gets fired before all the promises are resolved. So 'done' shows up before all the 'checks' in the console.

$scope.book = function(input) {

  //Get the cart items from DB
  ref.child('/cart/' + userId).once('value').then(function(cartSnap) {

    //Promise collector
    var collector = [];

    //Loop through the cart items
    cartSnap.forEach(function(cartItemSnap) {

      var itemId = cartItemSnap.key();

      //Get the inventory items from DB that are in the cart
      var promise1 = ref.child('/inventory/' + itemId).once('value').then(function(inventoryItem) {

        //Loop through booking IDs of inventoryItem
        inventoryItem.child('rentals').forEach(function(rentalSnap) {

          var rentalId = rentalSnap.key();

          //Get bookings from rental/active
          var promise2 = ref.child('/rental/'+ rentalId).once('value').then(function(activeRentals) {

            console.log('check');
            collector.push(promise2);

          });
        });

        collector.push(promise1);
      });
    });

    //Once everything was checked
    Promise.all(collector).then(function() {
      console.log('Done');
    });
  });
};
stefan
  • 59
  • 6
  • what does `checkIfBookingIsAllowed` do? could you please add this code? – adolfosrs Aug 25 '16 at 18:16
  • It checks the date overlap and the quantity of an item. It's not really relevant for my problem. You could simply exchange this line with a console.log('check'); – stefan Aug 25 '16 at 19:24
  • 1
    Use `Promise.all()`: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all – Frank van Puffelen Aug 25 '16 at 19:32
  • @FrankvanPuffelen Thanks for the help. I tried, check my new code. What am I missing? – stefan Aug 25 '16 at 19:39
  • You're loading data asynchronously, so need to ensure that your promises "bubble up". It's a bit tricky to write an answer without testing. Any chance you could reproduce the problem in a jsbin, so that I can make that work? – Frank van Puffelen Aug 25 '16 at 19:47
  • here's a [codepen](http://codepen.io/anon/pen/pbrdrV?editors=1111). – stefan Aug 28 '16 at 09:07

1 Answers1

2

You're collecting the promises too late. Collect them now, not later. Code inside .thens runs later.

Here's what runs immediately:

  1. Both nested for-loops run to completion, minus all code inside .thens.
  2. Promise.all(collector).

At this point collector is still empty, so Promise.all() completes real fast.

Move the collector.push(promise) calls outside of the .thens.

A more functional approach:

With the above fix, your code should work, but a cleaner approach is to return all promises. While forEach doesn't allow return values, map does, so this can be re-written as (simplifying):

Promise.all(cartSnap.map(snap => ref.child(itemUrl).once('value').then(item =>
  Promise.all(item.child('rentals').map(snap => ref.child(url).once('value'))))))
.then(allActiveRentals => console.log('Done'));
Community
  • 1
  • 1
jib
  • 40,579
  • 17
  • 100
  • 158
  • thank jib for this approach. firebase doesn't return an array but an object which doesn't include the map function. is there another way? – stefan Aug 27 '16 at 08:53
  • @stefan Whatever you're doing `forEach` on is an array, so that's where you can use `map` instead. Use `map` on an object like this: `Object.keys(obj).map(key => foo(obj[key]))`. – jib Aug 27 '16 at 13:59
  • Ok this works for Objects. Now I'm simply struggling with the logic. I made a [codepen](http://codepen.io/anon/pen/pbrdrV?editors=1111) but the first loop still finishes before the nested ones. I know why but can't figure out how to do it right. – stefan Aug 28 '16 at 09:05
  • You're still not [returning all the promises](http://stackoverflow.com/a/37084467/918910), specifically, you're missing `return Promise.all` (note that with arrow functions, the return is implicit when brackets are not used, i.e. `x => 2*x` has an implicit return, like `x => { return 2*x; }`. Also still not sure what kind of thing `cartSnap` is, you're not calling `.key()` anymore whatever that is, so is the `itemId` correct? – jib Aug 28 '16 at 13:36
  • Ok, after I returned all the promises it seems to work! thanks. Yes itemId is already the key. I didn't try it with the arrow function. but here's the updated version: [codepen](http://codepen.io/anon/pen/jAgKAj?editors=1111) – stefan Aug 28 '16 at 20:42