
Asynchronous, Follow, Monitor, Parallel, Tracking, XMLHttpRequest
Dave is writing an Ajax chat client, and he's concerned the more vocal users will hog server resources, so he decides to enforce a policy of no more than three pending server calls at any time. Thus, the browser script pushes messages into a queue, and tracks the progress of each “XMLHttpRequest Call” containing those commands. He can enforce his maximum call count policy by tracking the state of each call.
How can you control parallel “XMLHttpRequest Call”s?
Fast users and busy applications often stretch networks and servers to their limits.
Asynchronous interaction is the only practical approach for Ajax applications. You don't want to block all interaction just because the network happens to be slow.
The number of simultaneous requests must be controlled, because browsers can only handle a few at a time, and also to reduce the overall load.
Track XMLHttpRequest calls as they proceed from browser to server and back again. XMLHttpRequest is a fairly basic component, and needs to be augmented or wrapped for better control over asynchronous dialogue. Furthermore, it is useful to keep all these wrappers in a collection. Note that this pattern is fairly low-level, and the details should generally be encapsulated in a wrapper library.
The standard mechanism for Call Tracking requires an XMLHttpRequest wrapper. Consider the implementation of the Ajax Client Engine (ACE) library. A Requester abstraction creates an XMLHttpRequest object upon construction:
function Requester()
{
var requester;
if (window.XMLHttpRequest)
{
requester = new window.XMLHttpRequest();
...
}
...
}
Requester's response handler doesn't go directly back to the caller's registered event handler, but instead to a method of Requester. This internal callback can then perform logging and other tasks before passing control back to the caller's handler. There's also a caching mechanism, so the Requester will add and remove itself from the cache as the call progresses.
The most important reason to track calls is to tame the asynchronous nature of XMLHttpRequest. There's a potential bug in which the programmer treats XMLHttpRequest as a Singleton (Gamma et al., 1995). That is, there's only a single, global, XMLHttpRequest instance, but parallel calls are made. An initial call will be pending, when suddenly a new call steps in. What happens here is unclear as it's the sort of unanticipated behaviour that will vary across browser implementations, but it certainly won't be a happyr result. The first call will possibly be cancelled, and it's unlikely its event handler will be notified. The simplest way to resolve this problem is to use a closure, i.e.:
xhReq.onreadystatechange = function() {
if (xhReq.readyState != 4) { return; }
var serverResponse = xhReq.responseText;
...
};
We can consider this a limited form of Call Tracking, since we've at least ensured each pending call will have its own associated XMLHttpRequest. Still, we can achieve a lot more by wrapping requests and placing the wrappers in a collection, as demonstrated by ACE and other libraries:
A collection of wrappers can be used as a cache. When each object has delivered its response, it's returned to the available pool (and its state reset in case of an error). This reduces object creation and destruction.
Using a fixed-size pool (previous point), you have the option of limiting the number of simultaneous requests to reduce network load and prevents problems in the browser; most browsers will only allow a handful of pending requests at any time.
As explained in “XMLHttpRequest Call”, the framework can start a timer when the request is issued and notify the caller if a timeout occurs.
With all requests co-ordinated by a central piece of logic, it's possible to influence the sequence of calls going in and out of the server. While it's best to design for calls to be completely parallel, sometimes you might have constraints, e.g. you might want the server to process calls in the same order as the user initiated them, hence fire a request only when the previous response has been received.
Related to the previous point, you often need to ensure the browser will handle one response completely before moving onto the next. One scenario is appending a bunch of response information - if the responses are dealt with in parallel, you'll end up with interleaved messages. One style of Call Tracking is to create Javascript Command (Gamma et al.) objects from each response, push them onto a queue, and have a separate thread execute them one at a time.
Sometimes, a service's response is minimal and doesn't include any detail about the original call. For example, "false" as opposed to "<spellcheck term="misspeld">false</spellcheck>. Indeed, sometimes there's no response at all and the framework will need to inform the response handler of a timeout. In these cases, it's useful to provide some context about the original call. By tracking the call, a framework can remember a "call context" which the caller sets when the request is issued. When the framework eventually transmits the response to the caller's response handler, it also passes in the call context. See the Code Example in “Predictive Fetch” to see how a calling context can be used.
A wrapper can register itself as the wrapped object's response handler, then log any changes. It can also poll for any new content (since there's no guarantee the event handler will be called when new content is added).
Li Shen's Ajax Client Engine (ACE) uses Call Tracking to ease development and harden the application in production. Among its many features are several related to Call Tracking:
Long calls are timed out.
Changes to XMLHttpRequest's response state are logged.
A service can be periodically polled.
The caller can declare exactly when the callback method should be invoked.
The AjaxCaller library, used throughout the AjaxPatterns demos for ???, uses Call Tracking to pool Calls, which wrap XMLHttpRequest objects.
libXmlRequest is another XMLHttpRequest wrapper. It keeps XMLHttpRequest objects in a pool so they can be reused, and tracks the response status in order to manage the pool.
In this example, we'll look at ACE's handling of call timeouts. The library consists of Requester objects which wrap XMLHttpRequest objects. As explained in the Solution above, the wrapped XMLHttpRequest objects are created upon construction. When the wrapper is invoked to make a call, it creates a timer to cancel the request if it takes too long: The timer Id is held as an attribute of the wrapper.
timeoutId = window.setTimeout(endRequest, request.callbackTimeout * 1000);
To track the call,the wrapper registers itself as a request handler:
function beginRequest()
{
...
requester.onreadystatechange = readystatechangeHandler;
...
}
Its handler is therefore called upon each change, and in the case of a complete call, cancels the timer.
function readystatechangeHandler()
...
if (requester.readyState == Ace.ReadyState.Complete)
{
...
if (requester.status == Ace.Status.OK)
{
...
if (timeoutId)
{
window.clearTimeout(timeoutId);
timeoutId = undefined;
}
...
}
...
}
Some calls need no tracking because they are low-priority uploads of information to the server. For example, a chat app might reasonably ignore the results of uploading the user's messages because a separate thread is continuously polling for all recent messages. Another example would be uploading information to support “Predictive Fetch”, where the worst case is simply the lost opportunity of a performance optimisation. Ajaxian.com featured an interesting article on optimising for this kind of request.
You may be able to get away with a single, global, XMLHttpRequest under certain conditions and with care. For example, you can use a lock to ensure there's only one pending call at each time (which is really a special case of Call Tracking). However, you risk forcing the user to endure long waiting periods.
Call Tracking is like tagging self-addressed envelopes before you send them away, so you can track them as they return.
This pattern was originally inspired by Richard Schwartz's caution against the familiar anti-pattern of working with a global XMLHttpRequest.