Smarter queuing of files using jQuery deferred and when

I had a somewhat simple need …a single web page style application using Backbone.JS had multiple template files it needed to download, if and only if the files hadn’t already been downloaded before.

While I suppose I could have relied on browser caching, I wanted to manage the requests in JavaScript. The trick was that at any given time, a request might include one or more template files, and some of the request might have already been made, or in progress.

Here’s the queuing function:

(function() {
    var allFiles = {};
    var allResults = {};

    var queueRequest = function (path, files, options) {
        options = options || {};
        files = $.isArray(files) ? files : [files];
        
        // this will contain all of the deferreds that will be 
        // wrapped with a `when` deferred
        var when = [];
        // might want to override some defaults...
        var ajaxOpt = $.extend({
            dataType: 'json',
            type: 'GET'
        }, options.ajax || {});

        // go thru each file
        $.each(files, function (i, file) {
            var xhr;
            // if we've not seen it before
            if (typeof allFiles[file] === 'undefined') {
                // kick it off
                xhr = $.ajax(path + file, ajaxOpt).done(function (data) {
                    // store the file results in the hash index
                    allResults[file] = data;
                });
                // keep the deferred for later use
                allFiles[file] = xhr;
                // and keey this for later
                when.push(xhr);
            } else {
                // already seen this, so just pack it on
                when.push(allFiles[file]);
            }
        });
        // return all of the built up deferreds as a 
        // single when. this will then wait until 
        // all are satisfied
        return $.when.apply(this, when).done(function () {
            if ($.isFunction(options.done)) {
                options.done.call(this, allResults);
            }
        }).fail(options.error);
    };

    window.Queue = queueRequest;
})();

Queuing is easy enough:

Queue(app_url("api/template/"), templates, {
    done: function (allFiles) {
        _.each(templates, function (n) {
            // compile only if needed
            if (typeof allTemplates[n] === 'undefined') {
                allTemplates[n] = _.template(allFiles[n]);
            }
        });
        if (typeof callback === 'function') {
            callback.call(context, allTemplates);
        }
    }
});

allTemplates is a list of compiled underscore templates stored elsewhere.

The first parameter to the Queue function is the full path to the templates. I’ve got a simple MVC controller that responds with a template given a key, so that’s the path that is provided in the example above. The list of templates (as an array) is passed.

The Queue code returns a jQuery deferred when. The function returns both active and completed ajax requests … which means that the jQuery when function may immediately fire … or not. When simply waits for all of the async operations to complete, and then calls the callback passed to done.

When complete, the code passes all results back to the requesting function. It’s not filtered as I trusted the caller (no reason to unnecessarily clone the array and waste CPU time). :)