Let's Make a Framework: Promises Part 4

23 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.

Over the last three weeks I’ve been looking at how web frameworks implement promises, and I’ve built a Promise class along the way.

Ajax Integration

After researching promises, jQuery’s API stood out as a solid example of using them to do common tasks. In particular, jQuery.ajax is Deferred-compatible, which means it transparently provides promise-related methods:

$.get('/example').then(
  function() { alert('Success'); },
  function() { alert('Failure'); }
);

We should be able to recreate this with our turing.Promise class, even though it’s extremely simple. There are a few things to keep in mind:

  • When implementing functions that support turing.Promise, they should return an object with a method that has the same signature as then(success, failure)
  • I designed Turing to be modular, so turing.Promise may not be available
  • The callee will return a promise that can be set with success/failure functions that are subsequently run later inside the original callee. It’s not as mind-bending as it sounds!

Tests

I wrote a little Express app to test turing.net. I’ve updated it to work with Express 2.×. This should provide a useful test for promises:

'test promises': function() {
  $t.get('/get-test').then(
    function(r) { assert.equal('{"key":"value"}', r.responseText); },
    function(r) { assert.ok(false); }
  );
}

The value in the assertion, '{"key":"value"}', is just something I set the Express app to return. These tests can be run by changing directory to test/functional then running node ajax.js and visiting http://localhost:3000.

Implementation

The beginning of the net module’s internal ajax function needs to be changed to instantiate a Promise if it’s available:

function ajax(url, options) {
  var request = xhr(),
      promise;
      
  if (turing.Promise) {
    promise = new turing.Promise();
  }

Now we’ve got one, what do we do with it? Well, we need to make it accessible to the outside by changing whatever ajax() returns to include a then method. This is the bare minimum.

This function returns an XMLHttpRequest object (or IE’s various equivalents). I just added a then method to this, but it would probably be better to return something else and keep the request object around internally.

request.then = function() {
  if (promise) promise.then.apply(promise, arguments);
};

I made then just defer to the current promise object, and used apply to call it with arguments. If any beginners find this puzzling, give arguments and apply a read on MDN. These language features are extremely useful for making flexible APIs.

The final thing to do is actually call the success or failure functions set up by then. They just need to go where the original callbacks get executed. Recall that our Promise class uses resolve(value) and reject(value) to handle the resolution of a promise:

  function respondToReadyState(readyState) {
    if (request.readyState == 4) {
      if (request.getResponseHeader('content-type') === 'application/json')
        request.responseJSON = net.parseJSON(request.responseText);

      if (successfulRequest(request)) {
        if (options.success) options.success(request);

        // HERE:
        if (promise) promise.resolve(request);
      } else {
        if (options.error) options.error(request);

        // HERE:
        if (promise) promise.reject(request);
      }
    }
  }

Conclusion

That wasn’t too painful, was it? I think there may be a good argument for making Promise part of Turing’s core module, particularly as it’s so short and useful. By following this example, promises could be used throughout the framework to enhance the API.

The latest code update was commit 0d0bfac.


blog comments powered by Disqus