Pregunta

I'm trying to learn a little about Node and asynchronous programming. I read about Promises and have made an attempt at using them in a small project that copies posts for a user from Service A to Service B. I am having some trouble understanding how best to pass state between Promises

The project is written for NodeJS using the Promise library

A simple definition of my current problem is:

  • Copy posts for a user from Service A to Service B if the posts don't already exist in Service B.
  • Both services offer http APIs that requires a non-memorable user id to look up posts for that user so user id must be looked up from the user name.
  • All of the http calls are asynchronous.

This is some pseudo code that illustrates how I have chained the Promises together.

Promise.from('service_A_username')
  .then(getServiceAUserIdForUsername)
  .then(getServiceAPostsForUserId)
  .then(function(serviceAPosts) {
    // but what? store globally for access later?
    doSomethingWith(serviceAPosts);
    return Promise.from('service_B_username');
  })
  .then(getServiceBUserIdForUsername)
  .then(getServiceBPostsForUserId)
  .done(function(serviceBPosts) {
    // how do we interact with Service A posts?
    doSomethingThatInvolvesServiceAPostsWith(serviceBPosts); 
  });

There are a couple of things that I have thought about doing:

  1. Bring the getIdForUsername call inside the getPostsForUserId function. However, I wanted to keep each unit of functionality as simple as possible along the principle of 'do one thing and do it well'.
  2. Create a 'context' object and pass it through the whole chain, reading and storing state in this object. However this approach makes each function very bespoke to a chain and therefore difficult to use in isolation.

Are there any other options, and what approach is recommended?

¿Fue útil?

Solución 2

I would use Promise.all, like this

Promise.all([Promise.from('usernameA'), Promise.from('usernameB')])
    .then(function(result) {
        return Promise.all([getUsername(result[0]),getUsername(result[1])])
    })
    .then(function(result) {
        return Promise.all([getPosts(result[0]),getPosts(result[1])]);
    })
    .then(function(result) {
        var postsA = result[0], postsB = result[1];
        // Work with both the posts here
    });

Otros consejos

First of all good question. This is something we (at least I) deal with with promises often. It's also a place where promises really shine over callbacks in my opinion.

What's going on here basically is that you really want two things that your library doesn't have:

  1. .spread that takes a promise that returns an array and changes it from an array parameter to parameters. This allows cutting things like .then(result) { var postsA = result[0], postsB = result[1]; into .spread(postsA,postsB.

  2. .map that takes an array of promises and maps each promise in an array to another promise - it's like .then but for each value of an array.

There are two options, either use an implementation that already uses them like Bluebird which I recommend since it is vastly superior to the alternatives right now (faster, better stack traces, better support, stronger feature set) OR you can implement them.

Since this is an answer and not a library recommendation, let's do that:

Let's start with spreading, this is relatively easy - all it means is calling Function#apply which spreads an array into varargs. Here is a sample implementation I stole from myself:

if (!Promise.prototype.spread) {
    Promise.prototype.spread = function (fn) {
        return this.then(function (args) {
         //this is always undefined in A+ complaint, but just in case
            return fn.apply(this, args); 
        });

    };
}

Next, let's do mapping. .map on promises is basically just Array mapping with a then:

if(!Promise.prototype.map){
    Promise.prototype.map = function (mapper) {
        return this.then(function(arr){
             mapping = arr.map(mapper); // map each value
             return Promise.all(mapping); // wait for all mappings to complete
        });
    }
}

For convenience, we can introduce a static counterpart of .map to start chains:

Promise.map = function(arr,mapping){
     return Promise.resolve(arr).map(mapping);
};

Now, we can write your code like we actually want to:

var names = ["usernameA","usernameB"]; // can scale to arbitrarily long.
Promise.map(names, getUsername).map(getPosts).spread(function(postsA,postsB){
     // work with postsA,postsB and whatever
});

Which is the syntax we really wanted all along. No code repetition, it's DRY, concise and clear, the beauty of promises.

Note that this doesn't scratch the surface of what Bluebird does - for example, Bluebird will detect it's a map chain and will 'push' functions on to the second request without the first one even finishing, so the getUsername for the first user won't wait to the second user but will actually call getPosts if that's quicker, so in this case it's as fast as your own gist version while clearer imo.

However, it is working, and is nice.

Barebones A+ implementations are more for interoperability between promise libraries and are supposed to be a 'base line'. They're useful when designing specific platform small APIs - IMO almost never. A solid library like Bluebird could significantly reduce your code. The Promise library you're using, even says in their documentation:

It is designed to get the basics spot on correct, so that you can build extended promise implementations on top of it.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top