Thursday, July 15, 2010

Bundling multiple Asyncronous requests

I'm working on a project which involves gathering multiple little bits of data via separate ajax calls. For example, I make one call to get a list of "groups", then another call to get details for each of those groups.

What I have been doing is loading the list first, displaying some information on the page, then filling in more information as the subsequent details calls come back.

This creates a bit of flicker and annoyance as the details on each item get filled in. I've decided that I want to try fetching all of the data at once, and waiting until it's all collected until I do anything. So I've created a function which allows me to submit a back of function references, along with their arguments. If one of the arguments is a callback, I wrap it with another function which allows me to tell if it's been called or not. After all of the callbacks have been called, I call the master callback for the bundle.

Here's the code.



$jq = jQuery;

bundledAsync = function(options){
var callbacksRemaining = 0;
var decrimentcallbacksRemaining = function(){
if(--callbacksRemaining == 0 && options.bundleCallback){
options.bundleCallback();
}
}
// Look through the args searching for functions.
// When one is found, wrap it with our own function so
// that we can keep track of which callbacks have returned
// this assumes that each callback is only called once
$jq.each(options.calls, function(index, call){
$jq.each(call.args, function(index, arg){
if(typeof arg === "function"){
callbacksRemaining++;
call.args[index] = function(){
decrimentcallbacksRemaining();
arg.apply(null, arguments);
}
}
});
});
// now actually call all the functions
$jq.each(options.calls, function(index, call){
call.fn.apply(null, call.args);
});
}



And here's how your run it:



bundledAsync({
calls:[
{
fn: settings.service.getGroupsCached,
args: [1234234234, function(resp){}]
}
],
bundleCallback: function(){}
})


///////////////////////////// EDIT ////////////////////

After giving this a try, I realized that it wasn't exactly what I needed. I needed to not only bundle async functions, but to allow the return of certain functions to spawn others. So now I have this:


bundledAsync = function(options){

var callbacksLeft = 0;

var decrimentCallbacksLeft = function(){
if(--callbacksLeft == 0 && options.bundleCallback){
options.bundleCallback();
}
}

var doCalls = function(calls){
$jq.each(calls, function(index, call){call.fn.apply(null, call.args)});
}

// // Look through the args searching for functions.
// // When one is found, wrap it with our own function.
// // This assumes that each function has exactly one
// // callback, and that each callback is called exactly once
var wrapCallbacks = function(calls){
$jq.each(calls, function(index, call){
$jq.each(call.args, function(index, arg){
if(typeof arg === "function"){
callbacksLeft++
call.args[index] = function(){
arg.apply(null, arguments); // call the original callback
if(call.calls){
// maybe we don't want to create the child calls until after
// the parent has returned. In that case, pass a function instead of an array
if(typeof call.calls === "function"){
call.calls = call.calls();
}
wrapCallbacks(call.calls);
doCalls(call.calls);
}
decrimentCallbacksLeft();
}
}
});
});
}
wrapCallbacks(options.calls);
doCalls(options.calls);
}



and an invocation that looks like this:



service.bundledAsync({
calls:[
{
fn: settings.service.getGroupsCached,
args: [
function(listsArg){
listsSummary = listsArg;
}
],
// These are child function calls of settings.service.getGroupsCached
// I want to wait until getGroupsCached returns before I create the
// child calls, b/c I won't know what the child calls are until
// getGroupsCached returns
calls: function(){return makeArray(listsSummary, function(list){
return {
fn: service.getGroupCached,
args: [list.id, function(resp){listsDetail.push(resp)}]
}
})}
},
{
fn: settings.service.getAllContactsCached,
args:[function(resp){
contacts = resp;
}]
}
],
bundleCallback: function(){
lists = listsDetail;
view = UNAB.TemplateRenderer.replaceHTMLResults(
settings.container,
"PrivateListBox",
{lists: lists}
)
}
});




The syntax could be lovelier, but I like how I can describe a tree of dependant asyncronous function calls with this. Haven't tested it too much yet, but it seems to be working.

No comments: