Sunday, January 8, 2012

defactor your deferreds and kill your callbacks

Defactor is a project that began after submitting a pull request for myQuery, then deciding to try my had at a selector engine I began to create my own called rufus (which isn't online so don't bother looking for it). Early I decided the syntax should work like the following:

rufus( 'div' )
    .on('click')  // begin tying events to the click event
        .animate({}) // animate the clicked element
            .exec(fn)  // execute fn once .animate is complete
            .end()       // return to previous context
        .ajax({})  // make an ajax call, returns a new deferred
        .success(fn)  // if successful
            .fail(fn)     // if error
            .complete(fn) // when call completes

I figured the core of a fully chainable asynchronous API would begin with a library similar to jQuery's Deferred object. I was first introduced to this concept during an interview at Mozilla, and haven't been able to leave them alone since. To begin explaining the concept, here's line taken directly from jQuery's API documentation:

"[Deferred] is a chainable utility object that can register multiple callbacks into callback queues, invoke callback queues, and relay the success or failure state of any synchronous or asynchronous function"

After using jQuery.Deferred in a couple small projects I began to want more than simple pass/fail. But how to build a Deferred object that would understand complex events? It didn't take very many attempts to realize it wasn't possible. Then I wondered "why not let the developer decide on their own level of complexity?" The concept for Defactor was born.

Defactor allows a developer to create a many-to-many relationship of, what are now called, queues and triggers. Defactor first instantiates a new object of itself, then self-populates with names for the different queue/trigger relations that were added using the add(trigger, queue) command. To finish the process the create() method is invoked to generate the new Deferred object.

Queues are registered against triggers, then the functions are passed to a queue to be executed later. Different queues will fire depending on which trigger is called. Here is an example (the names for the triggers and queues are similar to those used in jQuery.Deferred):

var Defer = defactor()
    .add('resolve', 'done always')
    .add('reject', 'fail always')
    .create();

var ss = Defer()
    .done(function(arg) {
        console.log('done');
    })
    .fail(function(args) {
        console.log('fail');
    })
    .always(function() {
        console.log('always');
    });

ss.resolve([context,] args);
// LOG: done
// LOG: always

While the above example has been simplified to a pass/fail modal, I hope you can see how more complex Deferred objects can be generated.

In the v0.2.0 release the API will change by allowing an optional final argument to [cci]add()[/cci]. This function will be called on an arguments set that will be passed to the queue. Code will help me explain:

var Defer = defactor()
    .add('resolve', 'debug', function(opts) {
        // debug opts
        return this;
    })
    .add('resolve', 'done');

var ss = new Defer()
    .done(function() {
        //
    })
    .debug({
        // opts object that are passed to the function
    });

ss.resolve();

This library is still very early, and while I don't see many drastic deviations from the API that is currently there, backwards compatibility isn't guaranteed until the release of v1.0. Which will hopefully be within the next few months. Grab the source.