How to make this code a promise?


#1

I have this code:

child.get('authors').forEach(author => author.get('books').forEach(book => {

  let time = (Math.floor(Math.random() * 10) + 1) * 1000;

  Ember.run.later(() => {

    //Some operations and then every book in the forEach is added to my favoriteList
    favoriteList.pushObject(book);

  }, time);

}));

.then(() => {
  console.log('Yoo! Finally!')
})

I need that message (and other actions) to execute finally, after all forEach. But now that message is consolelogged before that forEach get finished. Why?

I know I have to use: RSVP, Promises, EmberPromiseMixin and these stuff but it’s a mess!

Would you suggest the code and something beautiful site or book or something to finally learn once for all this stuff?


#2

Hi @johnunclesam, Promises can definitely be a little confusing to start off with, but once you get used to the basic syntax and ideas they are pretty awesome and very powerful. I’ll try and break down a few of the basic ideas and give you a little code. Unfortunately I’m not aware of any really great resources for learning them, you may just have to read a lot of code and google around. Promises aren’t really an Ember concept, Ember just happens to use them pretty heavily. Ember uses RSVP.js for promises so I would start there. I will say I find just the README for RSVP.js a pretty good primer. Your mileage may vary.

So for starters, an Ember.run.later has nothing to do with promises. It doesn’t return a promise or anything, it just waits a while to run the function. Same with forEach, no promises involved by default. However you can write a promise that will resolve when the run.later happens, thus giving you the behavior that you desire. Basically a promise is just function that executes and then either resolves on success or rejects on error. This can be anything you want. The most common use case is for AJAX requests. In your case though you can write a promise that resolves when your run.later happens and all of the operations in it successfully complete.

So I think what you’re looking for is not just one promise, but a bunch of promises that all must be resolved before the .then function gets executed. RSVP has you covered as it provides an “all” method. So to break down what you want to do a little bit:

  • first you want to loop over your authors and then your books, and do some operation on each one, and return a promise
  • then you want to use RSVP.all to make a “super promise” out of all your little promises.
  • then you want to use .then on your “super promise” to do something after ALL of your promises resolve

So your code might look something like this:

let promises = [];
child.get('authors').forEach(author => author.get('books').forEach(book => {
  // this is how to create a promise
  var promise = new RSVP.Promise(function(resolve, reject){
    let time = (Math.floor(Math.random() * 10) + 1) * 1000;
    Ember.run.later(() => {
      //Some operations and then ...
      favoriteList.pushObject(book);
      // this is how to resolve the promise, you could pass a value or not
      // if you wanted to resolve to the book for example, you could say resolve(book);
      resolve(); 
    }, time);
    if(something went wrong) {
      // this is how to reject the promise, you may not need to do this in your case
      reject(error message);
    }
  });
  promises.push(promise)
}));

RSVP.all(promises).then(function(/* arguments can be passed here if you have any */) {
  // contains an array of results for the given promises, for example if you passed 'book' into resolve
  // like suggested above you'd get an array of books here, or you could just not worry about results
  // and just do some operations here like printing this:
  console.log('Yoo! Finally!')
}).catch(function(error message){
  // if any of the promises fails. This catch function is only necessary if you want to handle errors
});

#3

I stumbled upon an interesting post about promises recently that shows how you might solve this a little differently than what @dknutsen shows above. Instead of firing off all and resolving each in parallel, the reduce below allows you to resolve one after another (with chaining). Not necessarily better than what you see above with RSVP.all(promises) just a different look at the problem that I hope is useful in some way (now or in the future) :slight_smile:

urls.reduce(
    (p, url) =>
        p.then(() => fetch(url).then(handleResponse)),
    Promise.resolve());

#4

Basically with this method that you suggest I take advantage of the blocking attitude of forEach, right?

I used “Ember.run.later” only to simulate an eventual server delay for all “gets”.

So I do not need a new promise for every forEach.

So I do not need the array promises [].

Thanks for the suggestion though.


#5

Sorry @dknutsen, if I use this modified version of tour code it’s the same?

 child.get('authors').forEach(author => author.get('books').forEach(book => {
  favoriteList.pushObject(book);
}));

console.log('Yoo! Finally!')

What are the differences?

Except for the Ember.run.later() which I use to simulate a server delay for all “gets”.

There are differences?


#6

@johnunclesam I’m not 100% sure what you mean when you say “server delay for all ‘gets’” but if you mean that the authors and books are nested async relationships via Ember data (so when you do child.get(‘authors’) it fetches the authors from the server, and when you do author.get(‘books’) it fetches the books from the server) then I don’t think that either your code or my code would do what you want.

Let’s break down your most recent version (again this is all based on the assumption that authors and books are async relationships):

child.get('authors').forEach(author => author.get('books').forEach(book => {
  favoriteList.pushObject(book);
}));
  1. First you’re saying child.get('authors'). Now, assuming child is some ember data model and ‘authors’ is an async relationship on that model, then this call is going to return a promise which will be resolved when the data from the server has been fetched.
  2. However forEach just loops over whatever it is given, it doesn’t (to my knowledge least) have any idea what a promise is, so my guess is that it’s NOT going to wait for the promise to resolve before executing, basically meaning it won’t execute at all.
  3. your second .get will behave the same way, in that it will not wait for the server fetch before executing the second forEach.

So I think what you want is more like this:

child.get('authors').then(authors => {
  authors.forEach(author => {
    author.get('books').then(books => {
      books.forEach(book => {
        favoriteList.pushObject(book);
      });
    });
  });
});

This will get all author relationships asynchronously, then loop over them and for each author get all related books asynchronously, then for each book it will push it into favoriteList. Now of course there is still the issue of wanting to know when all of this is done. So this is where you may want to employ something like RSVP.all or something more like @toranb posted above. Basically, each .get that you do returns a promise so you want to collect those promises and then wait until all of them have resolve. And actually there’s probably a better way to write it anyway, let’s try it:

child.get('authors').then(authors => {
  var bookPromises = [];
  authors.forEach(author => {
    bookPromises.push(author.get('books'));
  });
  RSVP.all(bookPromises).then(booksLists => {
    // bookLists is an array of author->books lists, so it's an array of arrays of books
    bookLists.forEach(bookList => {
      bookList.forEach(book => { favoriteList.pushObject(book); });
    });
    console.log('Yoo! Finally!')
  });
});