This post is part of a series on event-driven programming. The complete series is:
- Events: they’re not just for the DOM, you know
- Observable objects
- Deferrable values
- Asynchronous methods
- First-leg round-up and final remarks
- Object lifecycle
- Asynchronous pipelines
- Testing event-driven apps
The Deferrable pattern is a specialisation of observable objects that mixes state into the event dispatch process. The goal is to encapsulate a long-running computation as an object that can have callbacks attached to it; each callback is called just once, as soon as the computation’s result is known. The classic client-side JavaScript example is Ajax web services: we need a piece of data from an API but we might have to go over the wire to get it. To hide these details, we may use a JavaScript API that returns a deferrable object. Multiple clients can call the API and get such an object, and all will be notified when the data is ready.
Let’s take an example. Suppose we have an API that exposes data about pictures on a photo-sharing website as JSON. A JavaScript object wraps the HTTP API and memoizes the responses. Again I’m using JS.Class and Ojay for my examples.
PhotoService = new JS.Class({
initialize: function(host) {
this._host = host;
this._photos = {};
},
getPhoto: function(id) {
if (this._photos[id]) return this._photos[id];
var photo = new Photo();
Ojay.HTTP.GET(this._host + '/photos', {id: id}, function(data) {
photo.succeed(data);
});
this._photos[id] = photo;
return photo;
}
});
Our API returns a Photo
object, but one whose content is initially not known.
(Such an object is also known as a ‘future’ or a ‘promise’ in some
contexts.) If we’ve already made a Photo
with the required id
we return it
so as to avoid unnecessary network calls. Otherwise we make a new one, and begin
an asynchronous network call to get its contents. It is not known how long the
call will take but it is certainly enough to stop the method from returning
anything useful immediately. So, the Photo
we return is really a placeholder
for a value we might know at some point in the future. A client using this API
would behave like so:
var service = new PhotoService('http://example.com');
var photo = service.getPhoto('f80046');
photo.callback(function(photoData) {
var username = photoData.user.username,
photoUrl = photoData.url;
// further processing...
});
What must the Photo
look like? It must be able to have event listeners
attached using callback()
that wait for its value, and it must have a way of
being populated with data using succeed()
so that clients get the data they’re
waiting for. It turns out that these are fairly generic operations and we can
implement them as a module to mix into our Photo
class.
Deferrable = {
callback: function(block, scope) {
if (this._deferredValue !== undefined)
return block.call(scope, this._deferredValue);
this._callbacks = this._callbacks || [];
this._callbacks.push([block, scope]);
},
succeed: function(value) {
this._deferredValue = value;
if (!this._callbacks) return;
this._callbacks.forEach(function(callback) {
callback[0].call(callback[1], value);
});
this._callbacks = [];
}
};
Photo = new JS.Class({
include: Deferrable
});
This code bears some similarity to our observable object examples in that
it’s just storing and calling callback functions, but there are some important
differences. The callback()
method checks to see if we already know the
object’s value, and if we do then it calls the callback immediately and promptly
forgets about it. Otherwise the callback is stored in a queue until the value is
known. succeed()
is what we use to give the object a value: the value is
cached on the object and if any callbacks have been registered they are fired
then removed from the object. This makes sure each piece of code waiting on the
object’s value executes just once.
When should you use a deferrable object? The above example, though a little contrived, is the typical client-side use case but really you can use them in any situation where you need to wait for some background/parallel processing to finish. This could be an Ajax call, an animation, a complex calculation happening in another thread, etc. One case I’ve been using it for lately is where an object needs to make sure some condition is true before executing some code.
The particular example I have in mind is the Client
class in Faye, a
publish-subscribe messaging library for web clients. You’ll notice several of
the methods, for example publish()
, have their internals wrapped in a
callback passed to the connect()
method. This is saying, “Making sure the
client is connected, run this code.” I’ve used the Deferrable pattern to
implement this but it’s probably obfuscated a little by the rest of the logic,
so I’ll make it more explicit here.
In Faye, each Client
must connect to the server and get a clientId
before
it’s allowed to subscribe and publish messages. A client can therefore exist in
one of three states:
UNCONNECTED
. The client has noclientId
and is not currently attempting to connect.CONNECTING
. The client is currently making a request for aclientId
.CONNECTED
. The client has aclientId
and may publish messages.
Therefore our connect()
method must handle these three states. In the first
state, we register a callback and initiate a request, and when the request
returns we use succeed()
to fire callbacks waiting for the Client
to be
ready. In the second state, we just register a callback since the request should
already be in progress. In the third, we can execute the callback immediately
since the Client
is ready.
Client = new JS.Class({
include: Deferrable,
UNCONNECTED: 1,
CONNECTING: 2,
CONNECTED: 3,
initialize: function() {
this._state = this.UNCONNECTED;
},
connect: function(block, scope) {
switch (this._state) {
case this.UNCONNECTED:
this.callback(block, scope);
this._state = this.CONNECTING;
Ajax.getClientIdSomehow(function(clientId) {
this._clientId = clientId;
this._state = this.CONNECTED;
this.succeed(true);
}, this);
break;
case this.CONNECTING:
this.callback(block, scope);
break;
case this.CONNECTED:
block.call(scope);
break;
}
},
publish: function(channel, message) {
this.connect(function() {
// publishing logic
}, this);
}
});
Most uses of deferrables fall into this three-state model: first we’re not ready, then we’re doing some background work to get a value, then we’re ready to use the value. They can be very useful as long as you’re careful to manage state correctly: make sure you don’t leave callbacks hanging around after they should have been fired and cleaned up, and make sure you do the right thing in each of the three states. In particular, be careful not to use a deferrable when what you really want are stateless multi-fire events (as in the previous article) or an asynchronous method – I’ll be covering those next.