When developing integrated mobile applications, the UI may expose components that are best suited to mediation. This is very true when for example invoking an asynchronous service. By nature all web service calls are asynchronous, so this makes the best sense. While most UI actions directly process on a main UI thread, that does not shelter us from the scheduling of asynchronous processing. Some of the things that can come out of this side effect:

  • Excessively chatty applications
  • Unresponsive UI processing the UI components
  • Out of sync invocations that have undesired results

To bring this home, lets consider the following application, which has the ability to set the configuration of 3 properties. Upon clicking each row, you toggle the configuration and that value is submitted to the server (the entire code for this project is available on https://github.com/ehirelabs/async_techniques).

This type of implicit save is a preferred UI construct, but if we do not properly guard against this then the application could become unstable. Consider the situation where a user clicks “subscribe to emails” 4 times quickly and then toggles the “agree to terms”. Using a simple heuristic, we could assert that we only want to send the configuration change if the value has had a net different change over a specified period (say 2.5 seconds). It would also be nice if we dealt with the configuration changes one at a time so that we don’t have any changes being saved in parallel. Thus after a specified time in the 5 click example above we would send only one request to our service to save the “agree to terms” change.

There are ways that we can help shelter our application from the user and do some preventive programming for these scenarios. I’d like to introduce you to Async.js which is an open source asynchronous library built for Node.js, but usable in the browser and especially well in Titanium.

There are over 20 functions that async provides which can be broken down into the following categories:

  • Collections – a set of functions for dealing with collections and processing tasks against them in a asynchronous way
  • Control flow – functions which control the way that asynchronous tasks are processed – queues, chaining, etc
  • Utils – memoization, logging etc

In this example we implement the tableview in app.js. When our row value changes, we simply make a call tomyservice.save()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var myservice = require('myservice').myservice;
 
var createRow = function(title, value, callback) {
var row = Ti.UI.createTableViewRow(_style.row, {
   hasCheck: value
});
...
row.addEventListener('click', function() {
   row.hasCheck = !row.hasCheck;
      callback(row.hasCheck);
   });
   return row;
};
 
data.push(createRow("subscribe to emails", myservice.get('subcribe_emails'),function(value) {
   myservice.save('subcribe_emails', value);
}));

Pretty straight forward stuff here so far. Now lets get to the fun stuff, the definition of the myservice (in myservice.js). The first thing that we need to do is create a queue. This will help us with processing the items one at a time. It is the responsibility of the myservice to deal with the processing of the save in a safe way. Our myservice example is essentially a mock. It does not actually call a webservice, but it could easily if we modified the _saveConfig() method. Lets focus first on how we implement the queuing:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var async = require('async');
exports.myservice = {
   ...
   save: function(name, value, callback) {
      Ti.API.debug('saveConfig ' + name + '=' + value);
      this.config[name] = value;
      this.queue.push({
         name: name,
         value: value
      }, callback);
   },
   ...
   _initialize: function() {
      Ti.API.info('initializing');
      var self = this;
      this.config = {};
      this.queue = async.queue(function(task, callback) {
         ...
      }, 1);
      return this;
   }
}._initialize();

As we can see, our _initialize() function sets up the queue with the async.queue() on line 17 which takes as a parameter the worker function for the task (definition is omitted above) for the queue and the number of threads to process the queue (1).

Meanwhile, our save method on line 4 calls queue.push() which takes as a parameter the “task” object that is passed into the worker function and optionally the callback function for when the queued task is completed.

This is great, now in our worker function we’d like to execute a set of actions including:

  • Delaying 2.5 seconds (asynchronous)
  • Ensuring the session to our backend is still active (asynchronous)
  • Determining if we have already synched the value (blocking)
  • Invoking the save (asynchronous)

To do this, we make use of the waterfall() function from Async.js. It allows us to specify a chain of functions to be executed as an array with a completion callback. If you have ever had “too deep recursion” jslint errors in your code or you wanted to change the execution order of the actions above or insert other actions then you’d appreciate the value of waterfall. This and of course, its just much cleaner code to read as well. Here is the initialize method in full now with the waterfall definition:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
_initialize: function() {
   Ti.API.info('initializing');
   var self = this;
   this.config = {};
   this.queue = async.queue(function(task, callback) {
      async.waterfall([
         function(callback) {
            task.push = self;
            self._delay(task, callback);
         },
         self._ensureSession,
         self._synced,
         self._saveConfig
      ], function(err) {
         if (err) {
            Ti.API.error('error in worker ' + task.name + ' ' + err);
            callback(err);
         } else {
            Ti.API.info('completed worker ' + task.name + ' is ' +Ti.App.Properties.getBool(task.name));
            callback();
         }
      });
   }, 1);
   Ti.API.info('initialized complete');
   return this;
}

Now lets do the scenario that we identified above where we click the “subscribe to emails” 4 times very quickly inside of 2.5 seconds and then toggle the “agree to terms” to checked. Our logs show the following:

1
2
3
4
5
[ERROR] error in worker subcribe_emails worker already synced subcribe_emails
[ERROR] error in worker subcribe_emails worker already synced subcribe_emails
[ERROR] error in worker subcribe_emails worker already synced subcribe_emails
[ERROR] error in worker subcribe_emails worker already synced subcribe_emails
[INFO] completed worker terms is true

As we can see, myservice errors out on all 4 of the “subscribe to emails” as we would have desired as escaping out of the waterfall is done by invoking the callback with an error, which cascades out to our worker callback from the chain. Meanwhile we can see that the terms is set to true and is successfully processed last.

There are some things to keep in mind with this implementation, which just might change how you construct your callbacks in other places in your code.

  • When you define your callback function, the first parameter is the error and the rest of the arguments can be multivalued. this way you just check if the first parameter is null or not to preserve the definition for handling both scenarios
  • When you invoke a callback, likewise make the first argument null or populate it for an error
  • When you implement an action/worker function the very last argument is the callback. the other parameters mirror what is in the previous callback
  • Consider placing all your state and scope into the task object that is passed around in the action functions and callbacks

The example in this was used for Titanium, but the same concepts can also be applied in the nodejs container and in web browsers as well. From the point at which I originally wrote this blog post and now, a new book has come out from the Pragmatic Bookshelf – Async JavaScript: Build More Responsive Apps with Less Code. In the book they go into more detail and would recommend it for continued reading.