Tags

, , , , , ,

In the land of jQuery and web services, callbacks are used heavily to handle the response of asynchronous requests. Since the return value of the function is discarded, it can be a challenge to coordinate behavior in a meaningful way. If data needs to be shared over multiple callbacks, there are a few options. Probably the worst, yet simplest approach is to use a global variable. Callbacks with discarded return values is reminiscent of the continuation passing style, where a function is passed as an argument and is called as opposed to returning a value. While true continuations have call stack optimizations, we can leverage this functional approach to sequence behaviors.

This scenario arose in a project I’m working on to enhance rekon. One feature I’m adding is the ability to execute map/reduce jobs from within rekon. To do this, I load up existing phase specs saved in riak, and then add them to a RiakMapper job. Once all the phases are loaded, the job should execute and then render output to the page. To load a phase, an asynchronous GET request is made via jQuery. To do something with the response, you need to provide a callback that operates on the data. I need to collect all of the phase specs stored in riak and then submit them as a map/reduce job. The simple solution is to set a global variable and append it. But this is a poor solution. A better solution is to take advantage of some of the functional features of Javascript. As mentioned above, it’s possible to think of the callback as a continuation. But this doesn’t solve the data coordination. To do that, you can use a closure as the continuation and store the data within the closure. This way, as each aysnc request calls its callback, the closure updates its internal list tracking all of the phases. Adding a finalizer callback allows us to render the page once the last request completes.

The snippet below defines the closure cum continuation. Some details have been removed to make it clearer.

function phaseContinuation(count, options, finalizer) {
  // Holds a list of the phase specs
  phases = [ ];
  return function(phase_data) {
    count--;
    phases[phases.length] = phase_data;
    if (count == 0) { finalizer(phases, options); }
  }
}

The finalizer is called when the last phase is loaded and is responsible for executing the job and rendering the output. The project is using Sammy.js for the page rendering. Again, some details were removed to focus on the point of the post.

<pre>function runPhases(phases, options) {
  bucket = options['bucket']
  context = options['context']
  var mapper = new RiakMapper(Rekon.client, bucket);
  phases.map(function(p) { mapper.map(p) });

  mapper.run(null, function(status, list, xmlrequest) {
    keyRows = list.map(function(val) { return {value:val} });
    context.renderEach('key-mr.html.template', keyRows)
      .replace('#keys tbody')
      .then(function(){ searchable('#bucket table tbody tr'); })
  });
}</pre>

The actual execution then is as simple as calling a map on a list. This triggers all the GET requests, and everything else is  handled by the closure. Pretty cool!

<pre>options = {bucket:name,context:context};
continuation = phaseContinuation(raw_phases.length, options, runPhases);
raw_phases.map(function(p) {
  phase_url = Rekon.riakUrl('rekon.jobs/'+p);
  jQuery.get(phase_url, continuation);
});</pre>

The lesson here is that functional paradigms have tangible benefits. Not only is the structure of the code very simple, but all variables are appropriately scoped within the application.

N.B. The example doesn’t handle ordering of the phases, but this is accomplished easily enough.