JS : fluent async methods using deferred piping

Image

I’ve never written a code post on this blog before, but wish to write a short post about a little pattern that can make any code that uses extensive async methods slightly more concise and easy to read.  I applied this method to an ORM I’m building for the INDX Personal Data Store, which I will talk about later.

Fluent Interfaces are all the rage; even if you don’t know it by name, if you are a Web developer you use them all the time; they’re the kind of API design where methods return a reference to the original object so that consecutive calls can be chained — e.g., jQuery(‘p.bold’).css(‘color’, ‘green’).attr(‘data-awesome’,true) … which only works because the css() method returns a reference to the original selection object, which defined an attr() method, and so on. jQuery and d3 are among the popular JS toolkits that heavily use the fluent interface idiom.

Today I realised that an API that I am designing is quite verbose. I frequently do this:

db.getObj('some-object-identifier')
  .then(function(obj) { 
    obj.set({prop1:'a',prop2:'b'});
    obj.save().then(continue.resolve);
  })

The getObj() method performs a remote method call and thus necessitates an async return.  set() executes immediately, but save() also performs a remote call subsequent to the set(), and so forth.  Wouldn’t it be nicer if this same code could be written:

db.getObj('some-obj-id')
  .set({prop1:'a', prop2:'b'})
  .save().then(continue.resolve);

The point here is that set() is not the same above in that it is merely promising to set the values when they have finally arrived; same for save, and ultimately the then.

A key challenge is to make sure that the order of chained methods is respected, even if asynchronous and synchronous methods are mixed in the chain.  For example, in the above example, we would expect set() to be called before save(), and save() needs to complete before the final then().

Cutting to the solution, I create a new kind of deferred called an ObjProxy which gets returned by getObj() and re-defines many of the original object’s methods to support fluent chaining. Here’s the entire listing

var ObjProxy = function(get_deferred) {
 var this_ = this;
 this.d = get_deferred.pipe(function(x) { this_.obj = x; return u.dresolve(); });
};

ObjProxy.prototype = {
 // weird case: wrap a synchronous call
 set:function(obj) {
    var a = _.toArray(arguments), this_ = this;
    this.d = this.d.pipe(function() {
      // call original set - sync 
      this_.obj.set.apply(obj, a); 
      // return a resolving deferred 
      // so the pipe continues
      return $.when(); 
    });
  // return ourselves!
  return this;
},
// simpler case: wrap an existing async method
save:function() {
  var a = _.toArray(arguments), this_ = this;
  this.d = this.d.pipe(function() { 
    return this_.obj.save(); // async
  });
  return this;
 } ...

Then, in my original getObj() method, I simply return one of these proxies, passing it the deferred that I would have originally returned:

getObj:function(objid) {
 // call original getObj(), now _getObj()
 var dfd = this._getObj(objid);
 return new ObjProxy(dfd);
}

Explanation

The key insight is to re-use the Deferred.pipe() method with each chain extension, and with new calls to re-define our pipe to be the chain resulting from extending the pipe, so that the subsequent call will extend this resulting pipe.

In the above example, each chain stage receives a copy of obj, regardless of what the actual chain stage returned.  If you want values to flow between chain stages, such as often used by d3, the code becomes simpler;  values merely appear in the function() passed to the pipe() method in each chain.

Hope this helps. Let me know if it’s useful or confusing by leaving a comment below.  Happy async hacking!

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s