FuturesJS is a JavaScript library which (when used as directed) simplifies handling Callbacks, Errbacks, Promises, Subscriptions, Joins, Synchronization of asynchronous data, and Eventually Consistent data. It is akin to this well documented this MSDN library, but with a liberal MIT license.
Development Version (0.5.2) 15kb, Uncompressed with Comments
Just open up your favorite JavaScript console and play
Updated July 31th 2010.A promise (as per Douglas Crockford) is (essentially) a chainable callback object with the following methods:
when() - pass in a function which accepts a data parameterfulfill() - pass in a single data parameter which will be sent to all wheners oncefail() - pass in a function which accepts an error parametersmash() - pass in a single error parameter which will be sent to all failers onceChainable means that the object returns itself so these methods can be called in sequence. A normal callback function might look like this:
var xhr = getAsyncData(arg1, arg2, callback, {onError : errback, setTimeout : 5000});
To use a promise with such a function you would do something like this:
var p = Futures.promise(),
xhr = getAsyncData(arg1, arg2, p.fulfill, {onError : p.fail, setTimeout : 5000});
p.when(callback1)
.when(callback2)
.when(callback3)
.fail(callback4);
Immediates (guarantees) are promises which are guaranteed to be fulfilled because they are immediately fulfilled. FuturesJS provides a shortcut method for just this case.
var p = promise('pass in the data');
Or if you prefer the "long" way:
var p = promise().fulfill('pass in the data').passable();
For simple cases you can just use "promisify":
// this is the quick'n'dirty convenience method
var myFunc = function (url, data, callback, errback) {
// 0, 1, 2, 3
// let promisify know the index
};
myFunc = Futures.promisify(myFunc, { "when": 2, "fail": 3 });
myFunc(url, data) // now promisified
.when(callback)
.fail(errback);
TODO allow a nested map of attributes as well something like
["true", "optional", "when", {"timeout":"fail", "error":"fail"}]
But if you're a little more do-it-yourself-y, you can promisify a function in a number of ways:
A) return an object with the when() and fail() methods
function getAsyncData(param1, param2) {
var p = Futures.promise(),
result = oldGetAsyncData(arg1, arg2, p.fulfill, {onError : p.smash, setTimeout : 5000});
// Implements a synchronous callback to hand back the original data
p.withResult = function(func) {
func(result); // XMLHTTPRequest object is the result in this case
}
return p;
}
var xhr;
getAsyncData(param1, param2)
.withResult(function (r) {
xhr = r;
})
.when(doStuff)
.when(doMoreStuff)
.fail(undoStuff);
if (i_change_my_mind) {
xhr.abort();
}
B) allow a promise to be passed instead of a callback or returned with a synchronous callback.
function getAsyncData(param1, param2, p) {
return oldGetAsyncData(arg1, arg2, p.fulfill, {onError : p.smash, setTimeout : 5000});
}
var promise = Futures.promise(),
xhr;
xhr = getAsyncData(param1, param2, promise);
promise
.when(doStuff)
.when(doMoreStuff)
.fail(undoStuff);
if (i_change_my_mind) {
xhr.abort();
}
C) pass back a promise as a synchronous callback
function getAsyncData(param1, param2, promiseback) {
var p = Futures.promise();
return oldGetAsyncData(arg1, arg2, p.fulfill, {onError : p.smash, setTimeout : 5000});
}
var promise,
xhr;
xhr = getAsyncData(param1, param2, function (p) {
promise = p;
});
promise
.when(doStuff)
.when(doMoreStuff)
.fail(undoStuff);
if (i_change_my_mind) {
xhr.abort();
}
Where many functions allow only one callback, a promise allows you to enlist multiple callbacks before and after the target function has been called. FuturesJS provides convenience functions for you to wrap existing functions via promisify() (method A) and noConflict() (method C).
A join is a special type of promise which allows you to get the results of multiple promises once all of them have completed or failed.
// Futures.join(p1, p2, p3, ..., pN)
Futures.join(p1, p2, p3) // There is no limit to how many promises you pass in
.when(function (p_arr) {
// p_arr[0] is the result of p1
// p_arr[N-1] is the result of pN
})
.fail(function (p_arr) {
// at least one of the promises was smashed
// you'll have to discover which on your own
});
TODO: allow a join to also accept a subscription In writing jaysyncunit I realized that this is useful to do
I have two main use cases:
If you're lucky I'll write some psuedo-code for these examples in Part 2.
Hang around and I'll explain more about subscriptions and other fun stuff.
Updated July 5th 2010.Last time I explained promises and hopefully you saw how they can be instrumental for creating more dynamic ajax-ical magic while avoiding some of the lexical ugliness that often occurs with callback chaining.
Whereas promises may only be fulfill()ed once, subscriptions may be deliver()ed any number of times. FuturesJS implements the following methods for a subscription:
Futures.subscription() - returns a new subscription
subscribe(func) - accepts a callback which will be executed on each delivery, returns unsubscribe. Not chainabledeliver(data) - accepts an issue of the data to pass to all subscribersmiss(func) - accepts an errback to be called at each holdhold(data) - pass in the error data to be sent to all missersunsubscribe() - returned by subscribe(func), call this function to cancel the subscriptionSubscriptions are very similar to promises; here's a quick look:
var myFuturific;
(function () {
var s = Futures.subscription();
myFuturific = function (arg1, arg2) {
var p = Futures.promise();
var xhr = getAsyncData(arg1, arg2, s.deliver, {onError : s.hold, setTimeout : 5000});
s.subscribe(p.fulfill);
s.hold(p.smash);
return p.passable();
};
myFuturific.subscribe = s.subscribe;
}());
// the same subscription is used for all calls of the function
var unsubscribe = myFuturific.subscribe(callback1)
myFuturific.hold(errback1);
// a new promise belongs to each instance
myFuturific()
.when(callback2)
.fail(errback2);
Notice that the same subsciption will fire each time the function is called and that and the promises are only for specific instance:
Words like trigger, fire, and notify are being thrown all over the place these days. If you're not familiar with those terms... I don't know why you're reading this blog... but just in case you're a little behind the times; JavaScript is event-driven and that means that instead of polling (yuck!) to know what's going on in your application you can listen.
For the DOM, jQuery's delegate() is my favorite way handle events (I would also recommend looking into JavaScriptMVC's controller module).
The subscriptions in FuturesJS work decently as a poor-man's event trigger system - just deliver() without actually passing any data and she-bam, you've got an event trigger.
// Anonymous triggers - Ready, Aim... Fire!
// TODO - commit this change to the repo on github
Futures.trigger = function(ignore) {
var s = subscription();
return {
listen: function(func) {
return s.subscribe(func); // returns `unsubscribe()`
}
fire: function(ignore) {
s.deliver();
return this;
},
}
}
var t = Futures.trigger();
t.listen(func1).listen(func2);
t.fire();
// TODO why not throw in named triggers with messages too?
"There's a func for that"(TM).
Futures.subscribify(func, directive) is a hybrid of Futures.promisify(func, directive) and the example given at the top of this page.
var subscribable = Futures.subscribify(myFunc, { "when": 2, "fail": 3 });
NOTE: It isn't necessary to specify the directive if myFunc has the methods .when() and .fail()
// Pass in a duck-typed promisable
var subscribable = Futures.subscribify(myFunc);
Of course, you can still work your own fancy black magic too. For that I'll refer you to the example above and Part 1 (where I talk about remember good ol' methods A, B, and C).
You can can transparently subscribify a function which accepts the same parameters and returns the same results by calling noConflict(syncback) on the result.
Let's consider $.getJSON() from jQuery as an example:
var subscription;
$.getJSON = Futures.subscribify($.getJSON, {"when":2}).noConflict(function (s) {
subscription = s; // This is a synchronous callback
});
var unsubscribe = subscription.subscribe(func1);
subscription..when(one_time_func);
var xhr = $.getJSON(url, data);
IN PROGRESS: I'm working on an interceptor that can handle hash maps, such as $.ajax
Whereas joins fire only once, synchronizations trigger each time the subscribees make a full round of deliveries. If one subscription delivers multiple times before the others deliver once, only the most recent delivery will be used.
var z,
unsubscribe;
z = Futures.synchronize(s1, s2, s3);
unsubscribe = z.subscribe(func);
Updated July 8th 2010.TODO: only the most recent successful delivery should be used
TODO: allow a join to also accept a subscription
TODO: handle misses better
cache, callback, right promise at the right time, eventual consitency