Let's Make a Framework: Promises Part 2

09 Jun 2011 | By Alex Young | Tags frameworks tutorials lmaf documentation promises

Let’s Make a Framework is an ongoing series about building a JavaScript framework from the ground up.

These articles are tagged with lmaf. The project we’re creating is called Turing. Documentation is available at turingjs.com.

Last week’s post introduced the concept of promises, which are defined by the CommonJS Promises/A specification.

Implementation Concepts

It’s important to realise that implementing promises requires some coupling or wrapping between the promise library and code that will use it. For example, in jQuery, deferred.js contains the promise API implementation. That doesn’t mean Ajax requests can now simply be manipulated to work with a promise, like this:

// This won't work:
promiseLibrary($.get('/example')).then(function() {
  // Ajax complete!
});

There’s no way for promiseLibrary to be signalled by a given method in a generic way.

This next example is from q for Node. It wraps promises around a file system call:

var FS = require('fs');

function list(path) {
  path = String(path);
  var result = Q.defer();
  FS.readdir(path, function(error, list) {
    if (error)
      return result.reject(error);
    else
      result.resolve(list);
  });
  return result.promise;
}

list('/path/name');

Here Q is used inside the callback usually called by the fs module. This new list() method will return a promise, allowing subsequent use of q’s when primitive to build potentially more expressive file system queries.

What all of this means is jQuery’s ajax.js is coupled to the promise implementation. This allows jQuery to offer an alternative API based around promises:

$.get('test').then(
  function() { alert('$.get succeeded'); },
  function() { alert('$.get failed!'); }
);

Despite this coupling between the Promise and Ajax libraries, injecting jQuery’s Deferred objects into the Ajax library isn’t particularly taxing. Here’s a core piece of this implementation:

// Success/Error
if ( isSuccess ) {
  deferred.resolveWith( callbackContext, [ success, statusText, jqXHR ] );
} else {
  deferred.rejectWith( callbackContext, [ jqXHR, statusText, error ] );
}

jQuery’s public APIs place a veneer between jQuery’s other functionality and the Deferred object’s API. Using an Ajax call looks a lot like CommonJS Promises/A’s suggestion, but in reality methods like resolveWith and rejectWith are being used underneath to provide the logic required to maintain asynchronous states.

An Example

I’m going to introduce promises gently, because it’s the kind of thing that can easily end up a confusing mess. The example in this article is intentionally limited so anyone should be able to follow it.

Developing a promise API needs a straightforward example to get the basic features nailed down. We need to be able to create a delay that can trigger the promise API in a predictable way. When experimenting with anything asynchronous, I always find good old setTimeout to be a big help, so let’s use that:

function delay(ms) {
  var d = new Promise();
  setTimeout(d.resolve, ms);
  return d;
}

This is based on q’s examples, and it just wraps setTimeout with a promise. Ideally, running the following in an interpreter should take a second, then display the message:

delay(1000).then(function() {
  console.log('Delay complete');
});

However… once a promise has fired, it shouldn’t be able to get triggered again. Imagine if someone had implemented a timeout system like this:

function timeout(duration, limit) {
  var d = new Promise();
  setTimeout(d.resolve, duration);

  // If the duration was over the limit then ideally the promise should fail
  setTimeout(d.reject, limit);
  return d;
}

Should both callbacks fire? Should an exception be thrown? Well, jQuery is designed to simply clear out the then callbacks as it rattles through them:

// resolve with given context and args
resolveWith: function( context, args ) {
  if ( !cancelled && !fired && !firing ) {
    // make sure args are available (#8421)
    args = args || [];
    firing = 1;
    try {
      while( callbacks[ 0 ] ) {
        callbacks.shift().apply( context, args );
      }
      // ...

See that use of shift() there? That’s effectively looping through each callback, running it, then removing it.

A Promise Object

Of course, our promise API isn’t just a stack of callbacks. We also need to store state, and allow callbacks to be added. To me this implies a class with a simple API:

function Promise() {
  // Pending callbacks
  this.pending = [];
}

Promise.prototype = {
  // Called when something finished successfully
  resolve: function(result) {
    // This is where we'll loop through callbacks
  },

  // Called when something broke
  reject: function(result) {
  },

  // I think you'll agree that implementing then is pretty easy
  // This uses a handy object that follows along with our API nicely
  then: function(success, failure) {
    this.pending.push({ resolve: success, reject: failure });
    return this;
  }
};

That’s looking good, but I want to use setTimeout(promise.resolve, 100) and that will cause resolve to have the wrong this. These methods could easily be moved to the constructor to make the resolve and reject methods behave more naturally:

function Promise() {
  var self = this;
  this.pending = [];

  this.resolve = function(result) {
    self.complete('resolve', result);
  },

  this.reject = function(result) {
    self.complete('reject', result);
  }
}

Promise.prototype = {
  then: function(success, failure) {
    this.pending.push({ resolve: success, reject: failure });
    return this;
  },

  complete: function(type, result) {
    while (this.pending[0]) {
      this.pending.shift()[type](result);
    }
  }
};

I also started writing some tests for this:

function delay(ms) {
  var p = new Promise();
  setTimeout(p.resolve, ms);
  return p;
}

function timeout(duration, limit) {
  var p = new Promise();
  setTimeout(p.resolve, duration);
  setTimeout(p.reject, limit);
  return p;
}

delay(1000).then(function() {
  console.log('Delay complete');
  assert.ok('Delay completed');
});

timeout(10, 100).then(
  function() {
    console.log('Timeout 1 OK');
    assert.ok('10ms is under 100ms');
  },
  function() {
    assert.fail();
  }
);

timeout(100, 10).then(
  function() {
    assert.fail();
  },
  function() {
    console.log('Timeout 2 OK');
    assert.ok('100ms is over 10ms');
  }
);

Conclusion

This is a very naive implementation of what CommonJS Promises/A describes. Ultimately, I’d like to have something that can be used to provide an elegant API for Ajax callbacks and animations, which are both areas in client-side JavaScript where asynchronous code is important.


blog comments powered by Disqus