27 December 2013

The bright future of promises in ES6 may change my programming style. While I appreciate the blog articles extolling the benefits of ES6 Promises, I desperately need a sandbox to play in before I can learn a new technique or technology.

###A little history

My first attempt at an iPhone app, using Phonegap, introduced me to callback hell. I used the SQLite database to persist app state and store rules. I recall some of my processes requiring as many as seven nested callbacks. In my second attempt, a refactoring of the first, I created a single-actor queue to manage the steps of my procedures so I could more easily code them. Limited by one queue, it could never reach true efficiency.

When I worked with a team of experienced programmers, I learned to manage processes with events. Events don’t block other processes like my queue solution. I like event-driven models, but they often need to grow to support more features and lack a consistent set of rules to govern that growth.

Then I learned about promises with jQuery 1.7. I flip-flopped between events and promises until I saw the promises/A+ spec. Under the spec, promises held the power to solve my programming challenges in an interchangeable manner–except jQuery failed to meet the spec.

Enter the next phase with the announcement of ES6 Promises. The ES6 proposal follows the spec and could soon offer a native solution. I can’t wait for native ES6 Promises so I created a REPL to help me understand how my code may evolve in the near future.

###Scenarios

I grouped the uses for promises I’m most familiar with and created brief examples. Each example is included in the REPL along with some similar examples using popular libraries.

####Pooling

On a previous project, we used many server-side rules to control the app experience. While building the UI, the app needed to make a few calls and perform some calculations. To prevent the app from drawing parts of the interface too early, we used a promise pool (using jQuery’s $.when). Under ES6, we can use the static method Promise.all().

Promise.all( [
asyncEvent( 1 ),
asyncEvent( 2 ),
asyncEvent( 3 )
] ).then( done, fail );
//runs fail if any promise is rejected
//runs done when all promises resolve

####Racing

Some pools don’t need all of the asynchronous events to finish, just one. We call these races and while jQuery didn’t recognize this pattern, other libraries did. In ES6 we’ll have Promise.race():

Promise.race( [
asyncEvent( 1 ),
asyncEvent( 2 ),
asyncEvent( 3 )
] ).then( done, fail );
//runs fail if any promise is rejected
//runs done when any promises resolves

####Sequencing

Another common task for our project was using the lookup tables in a back-end system to take another action. In this case we can’t fire all of our events off at the same time, we need parameters from one call to feed the next one. You can always tell a sequence from a pool by the lack of invocations.

Promises naturally chain (using .then()) but some developers prefer helper functions like pipeline or chain. In ES6, we can use .then() or we can write a quick “glue” function and use [].reduce():

function pipe(chain, next) {
return Promise.cast( chain ).then( next );
}
[
asyncEvent,
asyncEvent,
asyncEvent
].reduce( pipe, 1 )
.then( done, fail );
//runs fail if any promise is rejected
//runs done when all promises resolve
//each event starts when the previous one resolves

####Throttling

When you have several calls going to the same server or you need to maintain data integrity between calls, you can use a specialized sequence to throttle events. It works like a sequence except the data doesn’t need to pass from one event to the next and the order doesn’t usually matter. In this regard, it acts like a queue (FIFO). In the following example, the key parameter allows for multiple queues.

var gateway = (function () {
var gate = {};
return function (fn, key) {
var lock = key || "default",
unlock = () => gate[lock] = null;
gate[lock] = !gate[lock] ? Promise.cast( fn() ) : gate[lock].then( fn, fn );
return gate[lock].then( unlock, unlock );
};
}());

gateway( asyncEvent1 ).then( something1 );
gateway( asyncEvent2 ).then( something2 );
//asyncEvent2 will not start until asyncEvent1 is complete

####Continuing

By using promises within generators, you can achieve something similar to CPS (continuation-passing style). This, again, works much like a sequence only you can pause between async events. Inspired by HTML5Rocks example, this sequencing example allows you to either pass the previous value/reason as a parameter or inject a value. This pattern could be very helpful for testing or mocking.

function *serial(list) {
var n,
chain = Promise.resolve(),
next;
while ( list.length ) {
n = yield n;
if ( typeof n === "undefined" ) {
next = list.shift();
} else {
next = list.shift().bind( null, n );
}
chain = chain.then( next, next );
}
}
var chain = serial( [asyncEvent, asyncEvent, asyncEvent] );
chain.next();
chain.next( 1 );
chain.next(); //uses the value/reason of the previous promise
chain.next( 2 );


Discussion:

blog comments powered by Disqus