DailyJS

Let's Make a Framework: Promises Part 2

Alex R. Young

Subscribe

@dailyjs

Facebook

Google+

tutorials frameworks lmaf documentation promises

Let's Make a Framework: Promises Part 2

Posted by Alex R. Young on .
Featured

tutorials frameworks lmaf documentation promises

Let's Make a Framework: Promises Part 2

Posted by Alex R. Young on .
*Let's Make a Framework* is an ongoing series about building a JavaScript framework from the ground up. These articles are tagged with [lmaf](http://dailyjs.com/tags.html#lmaf). The project we're creating is called [Turing](http://github.com/alexyoung/turing.js). Documentation is available at [turingjs.com](http://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.