Traffic Cop

{Cross-posted on Fresh Brewed Code}

Recently I’ve been working on a project called ‘infuser‘ – it’s a JavaScript library for retrieving resources (i.e. – views/templates) asynchronously, and providing what is hopefully a nice API around rendering (it supports data-driven templates from multiple template engines, as well as static content), and attaching the completed content to the DOM.  I ran into a situation where multiple requests could be made for the same external resource simultaneously, and I wanted to prevent the unnecessary duplicate round trips.  Thus, “Traffic Cop” was born.  It’s roughly 30 lines of code that wraps the standard jQuery $.ajax() call with a custom $.trafficCop() function:


/*
TrafficCop
Author: Jim Cowart
License: Dual licensed MIT (http://www.opensource.org/licenses/mit-license) & GPL (http://www.opensource.org/licenses/gpl-license)
Version 0.1.0
*/
(function($, undefined) {

var inProgress = {};

$.trafficCop = function(url, options) {
    var reqOptions = url, key;
    if(arguments.length === 2) {
        reqOptions = $.extend(true, options, { url: url });
    }
    key = JSON.stringify(reqOptions);
    if(inProgress[key]) {
        inProgress[key].successCallbacks.push(reqOptions.success);
        inProgress[key].errorCallbacks.push(reqOptions.error);
        return;
    }

    var remove = function() {
            delete inProgress[key];
        },
        traffic = {
            successCallbacks: [reqOptions.success],
            errorCallbacks: [reqOptions.error],
            success: function() {
                var args = arguments;
                $.each($(inProgress[key].successCallbacks), function(idx,item){ item.apply(null, args); });
                remove();
            },
            error: function() {
                var args = arguments;
                $.each($(inProgress[key].errorCallbacks), function(idx,item){ item.apply(null, args); });
                remove();
            }
        };
    inProgress[key] = $.extend(true, {}, reqOptions, traffic);
    return $.ajax(inProgress[key]);
};

})(jQuery);

Breaking it Down:

  • Line 11 – This is not your typical jQuery plugin.  We’re adding to the jQuery object itself, as it is not intended to be used on DOM elements, and is instead an alternative to $.ajax() (it supports the same function signature as $.ajax()).
  • Line 16 – by the time we get here we have a full options hash for an ajax request, and we’ve stringified it to get a “poor man’s hash” key, since the metadata in the reqOptions object that can be serialized to JSON is also what uniquely identifies the request.
  • Line 17 – if this key already exists in our “inProgress” object, then we append the success/error callbacks for this reqOptions object to an existing array of success and error callbacks, and then return.
  • Lines 23-38 – this is where the real work happens.  If we’ve reached this point, this request is the first of its kind out of all requests currently processing.
    • We set aside a reference to function that will remove this key from the inProgress object.
    • Then we create a “traffic” object.  This is basically an $.ajax() options hash that has an array of callbacks for success (successCallbacks), and an array of callbacks for error (errorCallbacks).  We take the original success and error callbacks and init these arrays with each as the starting member (respectively).
    • Next, we create a new success callback that will iterate over the successCallbacks array and invoke each one, passing in any relevant response data, followed by invoking our “remove()” function to remove this traffic object from the “inProgress” object.  We do the same for the error callback.
    • Then we extend the traffic object (which contains our modified callback approach) onto the reqOptions object, then extend the combined traffic/reqOptions object onto a new object.  Taking this approach means that we capture all the data provided to us in the reqOptions objects, while substituting the original success and error callbacks with the modified versions from the traffic object.  We add the resulting new reference to our inProgress object, using the “poor man’s hash” we created earlier as the key/member name.
    • Finally, we invoke jQuery’s $.ajax() function, passing in the modified “traffic/reqOptions” hash as the options for the call.  From this point, jQuery will process it like a normal request, invoking the success or error callback, based on the status of the response.

If any other calling code invokes $.trafficCop with a request identical to one already running, it will simply take the duplicate request’s success and error callbacks and push them into the successCallbacks and errorCallbacks array(s) of the currently running request.  This prevents a duplicate request, while still notifying the caller of success/error when the original request completes. When the request completes, it is removed from the inProgress object, so any subsequent requests with the same metadata will start the cycle over again. Obviously, subsequent requests to the same endpoint may or may not result in an actual request being made, since the response may be in the browser’s cache.

Thoughts & Future Improvements

It’s worth noting that while my main use of TrafficCop has been in the context of “infuser” (retrieving remote template/static content asynchronously), it can be used for any request that you would invoke via $.ajax().

I’m not aware of any JavaScript implementations that would cause the “trafficCop” function to yield (before returning) to a completed AJAX request, and thus introducing a ‘race condition’ where the original request’s iterative “success” or “error” callbacks get invoked before a duplicate request’s success/error callbacks were pushed into the successCallbacks and errorCallbacks array(s).  However – if one was to insist on using web workers, then I assume that it’s possible for such a condition to exist.  That being the case, one area of improvement would be for me to add some double-check logic that prevents removal of an original request (and the firing of the success/error callbacks) if a duplicate request is currently being grandfathered in.

Or I could just recommend that you avoid web workers. :-)

 

Tags: , , , ,