Friday, July 16, 2010

Ajax and Aspect-Oriented Programming

I'm working on an ajax application which contains a number of different widgets which can manipulate data. In many cases, multiple widgets depend on the same data. I've been thinking of ways to notify all relevant widgets when a piece of data changes, to let them know that they need to fetch new data and refresh themselves.

Related to this is the question of caching. If widgetA fetches data from the server, and widgetB depends on the same data, widgetB should be able to access the data that widgetA just fetched.

So what I'm moving toward is this:

A single "service" object which contains ajax accessor methods.

A "cache" wrapper around the service object which intercepts calls, decides if cached data can be used, and also decides when listeners need to be notified of new data.

Here's my Cache object so far.


Cache = function(options){
var self = {};
var dataSource = options.dataSource;
var log = UNAB.Debug.NamedLogger("UNAB.Cache");
var aopCallbacks = {};

var aopCallbackCheck = function(name){
log.debug("aopCallbackCheck called for: " + name);
if(aopCallbacks[name]){
aopCallbacks[name]();
}
}

$jq.each(options.dataSource, function(key){
self[key] = function(){
// wrap callbacks with a function which will invoke an aop handler
var processedArguments = UNAB.Util.makeArray(arguments, function(arg){
if(typeof arg === "function"){
return function(){
arg.apply(null, arguments);
aopCallbackCheck(key);
}
}else{
return arg;
}
})
dataSource[key].apply(null, processedArguments);
};
// add a "Cached" method to the object, which will just pass values straight through,
// possibly use the cache, and not invoke any aop stuff
self[key + "Cached"] = dataSource[key];
});

self.addAopCallback = function(options){
aopCallbacks[options.name] = options.fn;
}

return self;
}


How it works is this:

You pass your data accessor object to the cache in the constructor:

wrappedDataServiceObject = Cache({dataSource: dataServiceObject})


then you can call all of the methods that were available on dataServiceObject on wrappedDataServiceObject. But wrappedDataServiceObject has some addional stuff. For every method xxx, wrappedDataServiceObject adds a method xxxCached. I haven't worked out just what I'm going to do with this. For now method xxx does the same thing as method xxxCached.

Additionally, you can register listeners which get run whenever a non-cached method's callback gets called. The callback is wrapped with a new function which calls the callback, but also looks for any aopCallbacks you've registered. This way, you can broadcast to all listeners that new data has been checked. Note -- this happens only on method xxx, not on method xxxCached.


wrappedDataServiceObject.addAopCallback({
name:"deleteContact",
fn: function(){
$jq(".contactsChangedListener").trigger("contactsChanged");
}
})


Things in my app were beginning to look a little spaghetti-ish, and I was getting weird endless loop problems. Hopefully organizing stuff this way will make things easier to manage.

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.