msgbartop
The SynaTree Development Weblog
msgbarbottom

08 Nov 08 Piling up data before firing an event

In our increasingly Ajax-centered world, where things happen in an unpredictable order, it is often very important to make sure some list of things has occurred prior to taking an action. This can be especially painful, for example, when multiple XHR events are pending and a final action relies on them all being completed in no particular order.

Presenting Aggregator, a deceptively simple little widget with a lot of firepower. The concept is as follows:

  • Destiny Function: The final destiny of the Aggregator, the function that will fire after all prerequisites have been satisfied.
  • Latch Function: [optional]  The internal mechanism to determine if the requirements have been satisfied.  The default latch simply checks to see if each stage has been completed.  More complex latches might have “either/or” logic or other arbitrary requirements.
  • Stages: An array of simple string names, each of which represents a stage.

The Aggregator is called like this:

Collector = Aggregator( {stages: ['first', 'second']}, alert.pass('all done'));

Collector is a closure that provides the aggregating functionality.  You use Collector like this:

Collector( 'first' );

This toggles the first stage switch and runs the latch.  If the latch returns true, the destiny function is called.  In this case, since we didn’t specify a latch, the default is being used.  Therefore, the destiny function will not yet call because ’second’ has not yet been satisfied.

Asset.javascript( 'somescript.js', {onload: Collector.pass('second') });

You can cheat and run the second step below.

Collector('second');

When the javascript file is loaded, the Collector function will be called with ’second’ as it’s argument.  When that happens, the latch will be satisfied and the destiny function alert(’all done’) will be called.

The Aggregator can also collect data.  A good example is when using Google Maps API.  Let’s say you want to wait until an address has been geocoded, an image has been loaded, some data has been loaded, and a few other operations are done before calling an “addMarker” function:

var gc = new API.ClientGeocoder;

var markerBuilder = Aggregator({
    stages: ['placemarks', 'markeropts', 'markerdata', 'nodata', 'image'],
    latch: function(s){
        with (s)
        	return placemarks && markeropts && image && (markerdata || nodata);
    }
}, addMarker);

/* Step 1: Start Geocode */
gc.getLatLng(address, markerBuilder.curry('placemarks'));

/* Step 2: Fetch Marker */
var markerOpts = {
    clickable: true,
    draggable: (opts ? (opts.canmove ? true : false) : false),
    minZoom: (opts ? (opts.weight ? opts.weight : 1) : 1)
};
markerBuilder('markeropts', markerOpts);

/* Step 3: Fetch Category Data */
if (categoryId) {
 	// hypothetical data loader "DataAdapter"
    var MarkerData = new DataAdapter('Marker', categoryId);
    MarkerData.get(markerBuilder.curry('markerdata'));
}
else {
    markerBuilder('nodata');
}

/* Step 4: Image */
var Image = new Asset.image('/someimage.jpg', {onload: markerBuilder.curry('image',Image)})

The appeal of this approach is immediately obvious to anyone who regularly needs to dispatch multiple concurrent operations using XHR or similar asynchronous processes.

The full code for Aggregator is very small, as you can see:

Aggregator = function(config, finalfn)
{
var latch = null; var priv = {}; var state = $H({});
	// first, assign the latching semantics.
	// either use the configured latch function or a simple one.
	// the simple latch below examines each member of 'state' to see if it
	// is defined.  If not, it quits. If the latch gets all the way through,
	// it succeeds.

	latch = config.latch ? config.latch : ( function(state,priv){
	// release when all values are defined.
	return ! (state.contains(false));
	});

	// next, set up the private space for stages and the latch state for each.
	$each(config.stages || config, function(v){
		state[v] = false;
		priv[v] = '';
	});

	// next, define the closure.
	var r = function(stage, data){
        	if (stage && !state[stage]) {
		priv[stage] = data;
		state[stage] = true;
		}
		if(latch(state,priv))
			finalfn(priv);
		}
	return r;
}

Note that if you want to pass data values to an Aggregator lambda-function, you also need my Function.curry definition, which allows you to build functions with a built-in argument list.  Curry is posted on the Nibbles and Bits blog as well.

Tags: , , , ,