Introduction
Months ago, a friend of mine who is also a consultant and trainer, told me that one of his customers met with a problem. Internet Explorer will be stuck when too many connections have been set up in the page at the same time. The problem is becoming more and more popular since AJAX technology has been widely used these days. When an AJAX application is composed of smaller ones - that we call "mash up" - the problem will be likely to occur.
It's a bug in Internet Explorer. When you make a lot of AJAX calls, the browser keeps all the requests in a queue and executes two at a time. So, if you click on something to try to navigate to another page, the browser has to wait for running calls to complete before it can take another one. The bug is quite serious in Internet Explorer 6 and unfortunately, it still exists in Internet Explorer 7.
Manage the Requests Programmatically
The solution is simple. We should maintain the queue ourselves and send requests to the browser's queue from our queue one by one. Thus I wrote a queue to manage the requests. It's really a piece of cake:
if (!window.Global)
{
window.Global = new Object();
}
Global._ConnectionManager = function()
{
this._requestDelegateQueue = new Array();
this._requestInProgress = 0;
this._maxConcurrentRequest = 2;
}
Global._ConnectionManager.prototype =
{
enqueueRequestDelegate : function(requestDelegate)
{
this._requestDelegateQueue.push(requestDelegate);
this._request();
},
next : function()
{
this._requestInProgress --;
this._request();
},
_request : function()
{
if (this._requestDelegateQueue.length <= 0) return;
if (this._requestInProgress >= this._maxConcurrentRequest) return;
this._requestInProgress ++;
var requestDelegate = this._requestDelegateQueue.shift();
requestDelegate.call(null);
}
}
Global.ConnectionManager = new Global._ConnectionManager();
I build the component names ConnectionManager
using pure JavaScript code without any dependence on any AJAX/JavaScript framework/library. If users want to use this component to manage the request, they should use enqueueRequestDelegate
method to put a delegate into the queue. The delegate will be executed when there's none or only one request is running in the browser. And after receiving the response from the server, the user must call the next
method to notify the ConnectionManager
, and then the ConnectionManager
will execute the next pending request delegate if the queue is not empty.
For example, if we are using Prototype framework to make ten AJAX calls continuously:
function requestWithoutQueue()
{
for (var i = 0; i < 10; i++)
{
new Ajax.Request(
url,
{
method: 'post',
onComplete: callback
});
}
}
function callback(xmlHttpRequest)
{
}
We'll use the ConnectionManager
to queue the requests as follows:
function requestWithQueue()
{
for (var i = 0; i < 10; i++)
{
var requestDelegate = function()
{
new Ajax.Request(
url,
{
method: 'post',
onComplete: callback,
onFailure: Global.ConnectionManager.next,
onException: Global.ConnectionManager.next
});
}
Global.ConnectionManager.enqueueRequestDelegate(requestDelegate);
}
}
function callback(xmlHttpRequest)
{
Global.ConnectionManager.next();
}
Please note that we assign the next
method to both the onFailure
and onException
callback handlers to guarantee that it will be called after receiving the response from the server, since the rest delegate in the queue will fail to execute and the system cannot raise a new call anymore if the next
method hasn't been executed.
I send the file to my friend and several days later he told me that his customer said the component is hard to use. I agreed. It's really verbose and error prone. Apparently the ConnectionManager
is not so convenient to be integrated into the existing codes. The devs must make sure that all the requests should be queued in ConnectionManager
and the next
method must be executed in any case when the request finishes. But it's far from enough yet. More and more AJAX applications will execute scripts created by the server. Perhaps the dynamically created file cannot be loaded successfully if the internet connection of client side is not stable enough. At that time, the scripts execution throws exceptions and the next
method which should be executed by design will probably be missed.
Build a Fake XMLHttpRequest Type
I got an idea after days of thinking. It will be perfect if we can use another component to replace the native XMLHttpRequest
object and provide the built-in request queue. If so, devs can solve the problem by putting the script file in the page without changing a single line of code.
The solution is much easier than I thought before and now I'm going to show you how to build it.
The first thing we should do is to keep the native XHR
type. Please note that the following code has solved the compatibility problem of XHR
in different browsers:
window._progIDs = [ 'Msxml2.XMLHTTP', 'Microsoft.XMLHTTP' ];
if (!window.XMLHttpRequest)
{
window.XMLHttpRequest = function()
{
for (var i = 0; i < window._progIDs.length; i++)
{
try
{
var xmlHttp = new _originaActiveXObject(window._progIDs[i]);
return xmlHttp;
}
catch (ex) {}
}
return null;
}
}
if (window.ActiveXObject)
{
window._originalActiveXObject = window.ActiveXObject;
window.ActiveXObject = function(id)
{
id = id.toUpperCase();
for (var i = 0; i < window._progIDs.length; i++)
{
if (id === window._progIDs[i].toUpperCase())
{
return new XMLHttpRequest();
}
}
return new _originaActiveXObject(id);
}
}
window._originalXMLHttpRequest = window.XMLHttpRequest;
And then, we should create a new class to replace the native XHR
type. Most of the methods are just delegated to the corresponding one in the native object.
window.XMLHttpRequest = function()
{
this._xmlHttpRequest = new _originalXMLHttpRequest();
this.readyState = this._xmlHttpRequest.readyState;
this._xmlHttpRequest.onreadystatechange =
this._createDelegate(this, this._internalOnReadyStateChange);
}
window.XMLHttpRequest.prototype =
{
open : function(method, url, async)
{
this._xmlHttpRequest.open(method, url, async);
this.readyState = this._xmlHttpRequest.readyState;
},
setRequestHeader : function(header, value)
{
this._xmlHttpRequest.setRequestHeader(header, value);
},
getResponseHeader : function(header)
{
return this._xmlHttpRequest.getResponseHeader(header);
},
getAllResponseHeaders : function()
{
return this._xmlHttpRequest.getAllResponseHeaders();
},
abort : function()
{
this._xmlHttpRequest.abort();
},
_createDelegate : function(instance, method)
{
return function()
{
return method.apply(instance, arguments);
}
},
_internalOnReadyStateChange : function()
{
},
send : function(body)
{
}
}
The key points are the implementations of the send
method and _internalOnReadyStateChange
method. The send
method will put a delegate of the native XHR
type's method into the queue. The delegate will be executed by the ConnectionManager
at a proper time.
send : function(body)
{
var requestDelegate = this._createDelegate(
this,
function()
{
this._xmlHttpRequest.send(body);
this.readyState = this._xmlHttpRequest.readyState;
});
Global.ConnectionManager.enqueueRequestDelegate(requestDelegate);
},
We assign the _internalOnReadyStateChange
method as the onreadystatechange
callback handler of the native XHR
object in the constructor. When the callback function raises, we'll keep all the native properties into our object and execute our onreadystatechange
handler. Please note that our new component takes the responsibility of executing the next
method of ConnectionManager
when the readyState
equals to 4
, which means the current request is "completed", so that the next
method can be executed automatically from the devs' point of view.
_internalOnReadyStateChange : function()
{
var xmlHttpRequest = this._xmlHttpRequest;
try
{
this.readyState = xmlHttpRequest.readyState;
this.responseText = xmlHttpRequest.responseText;
this.responseXML = xmlHttpRequest.responseXML;
this.statusText = xmlHttpRequest.statusText;
this.status = xmlHttpRequest.status;
}
catch(e){}
if (4 == this.readyState)
{
Global.ConnectionManager.next();
}
if (this.onreadystatechange)
{
this.onreadystatechange.call(null);
}
}
We have tried our best to let the new component behave the same as the native XHR
type. But it still exists. It is a little thing but we can't do it. When we access the status
property in the native XHR
object, an error would be thrown if the object cannot receive the headers from server side. But in Internet Explorer, we can't define the object's property as a method like using __setter__
keyword in FireFox. It's the only difference between the native XHR
type and our new one when using the two components.
How to Use
And now, we can easily reference the JS file in the page to solve the problem when the user browses the page using Internet Explorer.
<!--[if IE]>
<script type="text/javascript" src="ConnectionManager.js"></script>
<script type="text/javascript" src="MyXMLHttpRequest.js"></script>
<![endif]-->
I sent the script file to my friend. It seems that his customer is quite pleased with this solution.
History
- 21st June, 2007: Initial post