5.2. The RequestManager Object

The RequestManager object is the main object used for handling XHR requests. Its main job is to manage two simultaneous XHR requests, since no more than two can be sent on any client that obeys the HTTP 1.1 specification. This object handles the creation and destruction of all XHR objects used to make the requests, meaning that the developer never has to worry about creating an XHR object directly. Additionally, the RequestManager object handles the monitoring of all requests and the marshaling of results to particular event handlers.

Since requests are metered by connections that the client is making, the RequestManager is implemented using the singleton pattern (meaning that only one instance can be created per page). It wouldn't make sense to allow more than one instance to be created, since there's only ever two available requests for an entire page (for example, it wouldn't make sense to create three RequestManager objects because there are still only two requests to manage). The basic pattern used to define this object is:

var RequestManager = (function () {

    var oManager = {
        //properties/methods go here
    };

    //initialization goes here

    //return the object
    return oManager;

})();

This is one of several ways to implement a singleton pattern in JavaScript. The outermost function is anonymous and is called immediately as the code is executed, creating the object, initializing it, and returning it. In this way RequestManager becomes a globally available object with its own properties and methods without creating a prototype.

Before delving into the inner workings of this object, consider the information that is to be handled. All of the information about each request must be handled by RequestManager in order for it to be effective. However, the goal is to free developers from instantiating XHR objects directly, which is where request description objects come in.

5.2.1. Request Description Objects

Instead of creating XHR objects directly, developers can define an object describing the request to execute. Since there are no methods on this object, there's no reason to define a constructor; just use an object literal with the following format:

var oRequest = {
    priority: 1,
    type: "post",
    url: "example.htm",
    data: "post_data",
    oncancel: function () {},
    onsuccess: function () {},
    onnotmodified: function () {},
    onfailure: function () {},
    scope: oObject
}

This object literal contains all of the information used by the RequestManager object. First is the priority property, which should be a numeric value where the smaller the number, the higher the priority (priority 1 is higher than priority 2, for example); this property is required. Next come the type and url properties, which should be set to the type of request (typically "get" or "post") and the URL to request, respectively. If you are sending a POST request, then the data property should be assigned to the post data to be sent to the server; otherwise, it can be omitted.

Next come the event handlers. Each of these methods is called according to the HTTP status of the response from the server:

  • oncancel() is called when a request is canceled before a response has been received.

  • onsuccess() is called to handle a response with a status in the 200 range.

  • onnotmodified() is called to handle a response with a status of 304.

  • onfailure() is called to handle a response with all other statuses.

The scope property works with each of these methods, setting the scope in which the function should be called (this allows for methods on other objects to be called for any of the three methods). If the scope isn't specified, then all of the functions are run in the global (window) scope.

Request description objects are stored and used by RequestManager in the handling of Ajax communication. These are the only other objects that developers interact with, so they are passed around repeatedly.

5.2.2. Queuing Requests

All pending requests (represented by request description objects) in the RequestManager are stored in a priority queue. The property name _pending (a private property) is used to store the PriorityQueue object, which is created with a custom comparison function to sort the objects by priority:

var RequestManager = (function () {

    var oManager = {


        _pending: new PriorityQueue(function (oRequest1, oRequest2) {
            return oRequest1.priority – oRequest2.priority;
        }),

        //more code here
    };

    //initialization goes here

    //return the object
    return oManager;

})();

The comparison function used here simply subtracts the value of each object's priority property, which will return a negative number if oRequest1.priority is less than oRequest2.priority, a positive number if the opposite is true, and 0 if they're equal. Simply subtracting the two priorities is a much faster way of implementing this function versus creating the full if...else structure discussed previously.

With the pending request queue is set up, there needs to be a publicly accessible way for developers to add requests to the queue. The method responsible for this is called send(), which expects a request description object to be passed in:

var RequestManager = (function () {

    var oManager = {

        DEFAULT_PRIORITY: 10,

        _pending: new PriorityQueue(function (oRequest1, oRequest2) {
            return oRequest1.priority - oRequest2.priority;
        }),


        //more code here


        send : function (oRequest) {
            if(typeof oRequest.priority != "number"){
                oRequest.priority = this.DEFAULT_PRIORITY;
            }
            oRequest.active = false;
            this._pending.put(oRequest);
        }
    };

    //initialization goes here

    //return the object
    return oManager;

})();

The first step in the send() method is to check for a valid priority on the request description object. If the property isn't a number, then a default priority of 10 is defined so as not to cause an error in the priority queue (this priority is stored in the constant DEFAULT_PRIORITY). Next, the active property is set to false; this property is used to determine if the request is currently being executed. The last step is to add the object into the priority queue so that it's prioritized among other pending requests.

5.2.3. Sending Requests

Now that requests can be queued, there must be a way to send them. To accomplish this, several methods are necessary. The first, _createTransport(), is a private method that creates an XHR object appropriate for the browser being used. This code is essentially the same as the XHR creation code discussed in Chapter 2 (note that to save space, other properties and methods have been shortened to "..."):

var RequestManager = (function () {

    var oManager = {

DEFAULT_PRIORITY: 10,

        //more code here

        _pending: new PriorityQueue(function (oRequest1, oRequest2) {...}),

        _createTransport : function (){
            if (typeof XMLHttpRequest != "undefined") {
                return new XMLHttpRequest();
            } else if (typeof ActiveXObject != "undefined") {
                var oHttp = null;
                try {
                    oHttp = new ActiveXObject("MSXML2.XmlHttp.6.0");
                    return oHttp;
                } catch (oEx) {
                    try {
                        oHttp = new ActiveXObjct("MSXML2.XmlHttp.3.0");
                        return oHttp;
                    } catch (oEx2) {
                        throw Error("Cannot create XMLHttp object.");
                    }
                }
            }
        },

        send : function (oRequest) {...}
    };

    //initialization goes here

    //return the object
    return oManager;

})();

Now that an appropriate XHR object can be created, the next pending request needs to be sent. Remember, there can be two active requests at a time, so there must be a way to track this. The active property contains a simple array of request description objects whose requests are active.

5.2.3.1. Initiating Requests

It's the job of the _sendNext() method to get the next request from the queue, assign it to the active list, and send it:

var RequestManager = (function () {

    var oManager = {

        DEFAULT_PRIORITY: 10,

        _active: new Array(),

        _pending: new PriorityQueue(function (oRequest1, oRequest2) {...}),

        _createTransport : function (){...},

_sendNext : function () {
            if (this._active.length < 2) {
                var oRequest = this._pending.get();
                if (oRequest != null) {
                    this._active.push(oRequest);
                    oRequest.transport = this._createTransport();
                    oRequest.transport.open(oRequest.type, oRequest.url, true);
                    oRequest.transport.send(oRequest.data);
                    oRequest.active = true;
                }
            }

        },

        send : function (oRequest) {...}
    };

    //initialization goes here

    //return the object
    return oManager;

})();

The sendNext() method starts by checking to see if there's an available connection. If the active array has less than two items in it, that means a connection is available and the function continues, calling get() on the priority queue to retrieve the next request. Since there may be no next request, it must be checked to ensure it's not null. If it's not null, then the request is added to the active list and an XHR object is created and stored in the transport property (this makes it easier to keep track of which XHR object is executing each request). Next, the open() and send() methods are called with the information inside the request description object. The last step is to set the active property to true, indicating that the request is currently being processed.

5.2.3.2. Monitoring Requests

It may seem odd that an XHR object is used asynchronously without an onreadystatechange event handler. This decision is intentional, since binding to the onreadystatechange event handler can cause memory issues in Internet Explorer. Instead, the RequestManager polls the status of the active requests, monitoring each XHR object every 250 milliseconds (four times a second) for changes to the readyState property. When the readyState changes to 4, then a sequence of event-handling steps takes place. This takes place in the _checkActiveRequests() method which, along with _sendNext(), is called in a function that exists outside of the RequestManager object so that it can be called via setInterval():

var RequestManager = (function () {

    var oManager = {

        DEFAULT_PRIORITY: 10,

        INTERVAL : 250,

        _active: new Array(),

        _pending: new PriorityQueue(function (oRequest1, oRequest2) {...}),

_checkActiveRequests : function () {

            var oRequest = null;
            var oTransport = null;

            for (var i=this._active.length-1; i >= 0; i--) {
                oRequest = this._active[i];
                oTransport = oRequest.transport;
                if (oTransport.readyState == 4) {
                    oRequest.active = false;
                    this._active.splice(i, 1);
                    var fnCallback = null;
                    if (oTransport.status >= 200 && oTransport.status < 300) {
                        if (typeof oRequest.onsuccess == "function") {
                            fnCallback = oRequest.onsuccess;
                        }
                    } else if (oTransport.status == 304) {
                        if (typeof oRequest.onnotmodified == "function") {
                            fnCallback = oRequest.onnotmodified;
                        }
                    } else {
                        if (typeof oRequest.onfailure == "function") {
                            fnCallback = oRequest.onfailure;
                        }
                    }
                    if (fnCallback != null) {
                        setTimeout((function (fnCallback, oRequest, oTransport) {
                            return function (){
                                fnCallback.call(oRequest.scope||window, {
                                    status : oTransport.status,
                                    data : oTransport.responseText,
                                    request : oRequest});
                            }
                        })(fnCallback, oRequest, oTransport), 1);
                    }
                }
            }
        },

        _createTransport : function (){...},

        _sendNext : function () {...},

        send : function (oRequest) {...}
    };

    //initialization
    setInterval(function () {
        RequestManager._checkActiveRequests();
        RequestManager._sendNext();
    }, oManager.INTERVAL);

    //return the object
    return oManager;

})();

The checkActiveRequests() method is the longest function, but it's also the one that does most of the work. Its job is to check the status of each active request to see if readyState is equal to 4. To accomplish this, a for loop is used to loop over each request in the active array (since items will be removed, the loop goes in reverse order to avoid skipping items). For convenience, the request description object is stored in oRequest, and the XHR object is stored in oTransport. Next, the readyState property is checked; if it's equal to 4, then some processing occurs.

The first step in processing a completed request is to set the active property to false, to indicate that it has returned and is complete. Then, the request is removed from the _active array using splice(). Next comes the decision as to which callback function should be executed. A variable, fnCallback, is created to store the callback function. This variable is assigned a value based on the status of the response and the availability of the callback function. If the status is between 200 and 299, then fnCallback is assigned the value of onsuccess; for a status of 304, fnCallback is set equal to onnotmodified; all other statuses force fnCallback to be assigned to onfailure. Each of these assignments takes place only if the given callback function is available (typeof is used to ensure that the function is defined).

After the assignment of fnCallback, the variable is checked to see if it's a valid function. If so, then a timeout is created to execute it. The timeout is important because it's possible for a callback function to take longer than 250 milliseconds to execute, which creates a race condition where the first call inside the interval may not have been completed by the time the next call begins. Delaying the execution of the callback ensures that the interval function executes completely before it is executed again.

In order to ensure proper scoping, a special time of function is created to pass into the setTimeout() function. This anonymous function accepts three arguments: fnCallback, oRequest, and oTransport (the same variables necessary to execute the callback function). These arguments are passed in immediately to the anonymous function in order to create proper copies of each variable. Inside of the anonymous function, another function is returned that actually executes the callback. It is safe to execute the callback within that scope because the variables are no longer the ones used within the for loop; they are copies. This technique is a bit involved, so consider the step-by-step buildup. First, the anonymous function is defined:

function (fnCallback, oRequest, oTransport) {
...
}

Next, the anonymous function defines and returns a function in its body:

function (fnCallback, oRequest, oTransport) {

    return function () {
        ...
    };
}

The returned function is written to execute the callback function:

function (fnCallback, oRequest, oTransport) {
    return function () {
        fnCallback.call(oRequest.scope||window, {
            status : oTransport.status,
            data : oTransport.responseText,
            request : oRequest});
    };
}

Then, the outer function is called, passing in the necessary variables:

(function (fnCallback, oRequest, oTransport) {
    return function () {
        fnCallback.call(oRequest.scope||window, {
            status : oTransport.status,
            data : oTransport.responseText,
            request : oRequest});
    };
})(fnCallback, oRequest, oTransport)

This effectively creates and returns a function to execute, so the result can be passed into setTimeout():

setTimeout((function (fnCallback, oRequest, oTransport) {
    return function () {
        fnCallback.call(oRequest.scope||window, {
            status : oTransport.status,
            data : oTransport.responseText,
            request : oRequest});
    };
})(fnCallback, oRequest, oTransport), 1);

Now, the callback function will be executed with the proper variables by using call() and passing in the appropriate scope in which to run and a data object. The first argument is a logical OR of the scope property and the window object, which returns scope if it's not null; otherwise, it returns window. The second argument is an object literal with three properties: status, which is the HTTP status of the request; data, which is the response body; and request, which returns the request description object that was used to make the request. This function call takes place inside a timeout, which is delayed for 1 millisecond.

After checkActiveRequests() is called in the interval function, it's time to see if there's room to make another request. The sendNext() method is then called to initiate the next request (if another request is pending).

NOTE

As mentioned previously, this whole function is called using setInterval() every 250 milliseconds. The interval setting is stored as INTERVAL on the RequestManager object. For most uses, this rate of polling is fine, but this interval length can and should be customized according to individual needs.

5.2.4. Cancelling Requests

It's entirely possible that a request may need to be canceled before it's executed. The cancel() method handles this, accepting the request description object as an argument and ensuring that it doesn't get sent. This is accomplished by removing the object from the list of pending requests. If the request is already active (it's in the active array, not the priority queue), then the XHR request must be aborted and the request removed from the active list:

var RequestManager = (function () {

    var oManager = {

DEFAULT_PRIORITY: 10,

        INTERVAL : 250,

        _active: new Array(),

        _pending: new PriorityQueue(function (oRequest1, oRequest2) {...}),

        _checkActiveRequests : function (){...},

        _createTransport : function (){...},

        _sendNext : function () {...},

        cancel : function (oRequest) {
            if (!this._pending.remove(oRequest)){

                oRequest.transport.abort();

                if (this._active[0] === oRequest) {
                    this._active.shift();
                } else if (this._active[1] === oRequest) {
                    this._active.pop();
                }

                if (typeof oRequest.oncancel == "function") {
                    oRequest.oncancel.call(oRequest.scope||window,
                        {request : oRequest});
                }

            }
        },

        send : function (oRequest) {...}
    };

    //initialization
    setInterval(function () {...}, 250);

    //return the object
    return oManager;

})();

The cancel() method begins by attempting to remove the request description object from the priority queue. Remember that the priority queue's remove() method returns true if the item was found and removed, and false if not. So, if this call to remove() returns false, it means that the request is active. When the request is active, the first step is to call abort() on the XHR object being used by the request. Since there are only two possible items in the array, it's easy to check each item in _active to see if it's the request of interest. If the request is the first item, then shift() is called to remove it; if the request is the second item, pop() is called to remove it. The last step is to execute the oncancel() callback function if it's defined.

5.2.5. Age-Based Promotion

With priority queues, there's a danger that a low-priority item will remain at the back indefinitely. This means that, in the case of the RequestManager, there may be low priority requests that are never executed. Obviously, this is an undesirable occurrence, since even the lowest-priority requests should be executed eventually. Age-based promotion seeks to resolve this issue by automatically bumping up the priority of requests that have remained in the queue for a longer-than-normal time.

The actual time considered to be "longer than normal" is directly related to the functionality that your application requires. In this case, assume that the time limit is 1 minute (60,000 milliseconds). Any request that hasn't been executed for 1 minute will receive an automatic priority promotion. Doing this ensures that a request will only be in the queue for a maximum of 1 minute times its initial priority (a request with a priority of 4 will be in the queue for a maximum of 4 minutes).

To accomplish age-based promotion, the RequestManager needs to add an additional property to each request description object. The age property tracks how long the request has been at a given priority. When age reaches the maximum, the priority property is automatically decremented (remember, the lower the number, the higher the priority), and age is reset back to 0. This functionality takes place in the _agePromote() method:

var RequestManager = (function () {

    var oManager = {

        AGE_LIMIT : 60000,

        DEFAULT_PRIORITY: 10,

        INTERVAL : 250,

        _active: new Array(),

        _pending: new PriorityQueue(function (oRequest1, oRequest2) {...}),

        _agePromote : function() {
            for (var i=0; i < this._pending.size(); i++) {
                var oRequest = this._pending.item(i);
                oRequest.age += this.INTERVAL;
                if (oRequest.age >= this.AGE_LIMIT){
                    oRequest.age = 0;
                    oRequest.priority--;
                }
            }
            this._pending.prioritize();
        },

        _checkActiveRequests : function (){...},

        _createTransport : function (){...},

        _sendNext : function () {...},

        cancel : function (oRequest) {...},

        send : function (oRequest) {

if(typeof oRequest.priority != "number"){
                oRequest.priority = this.DEFAULT_PROPERTY;
            }
            oRequest.active = false;

            oRequest.age = 0;
            this._pending.put(oRequest);
        }
    };

    //initialization
    setInterval(function () {
        RequestManager._checkActiveRequests();
        RequestManager._sendNext();

        RequestManager._agePromote();
    }, oManager.INTERVAL);

    //return the object
    return oManager;

})();

This new code adds a constant, AGE_LIMIT, to define how long a request should remain at the given priority. The constant is used inside of _agePromote() to determine when a request should be promoted. Before a request can be checked, its age property must be initialized; this takes place in the send() method with one additional line. The _agePromote() method is called inside of the interval function, just after _sendNext() to ensure that all of the pending requests are in the correct order for the next interval. Inside of _agePromote(), each item in the _pending queue has its age updated by adding the INTERVAL value to its current age. If age is greater than or equal to the limit, age is reset to 0 and the priority is decremented. The last step is to call prioritize() on the queue, since this method effectively changes the priority of an item already in the queue.

5.2.6. Handling Ajax Patterns

Having a prioritized list of requests is very helpful in managing traffic between the client and server, but it does require the developer to determine the relative priority of each request. In some cases, this is quite simple. For example, if a user action (a mouse click, a key press, etc.) initiated a request, clearly it is very important because the user is waiting for a response, so a priority of 0 would be most appropriate. In other cases, however, it's not always clear what the priority should be. To remedy this situation, it may be necessary to provide methods that decide priorities according to the Ajax pattern being used. Recall the patterns discussed earlier in the book:

  • Predictive Fetch: Guesses what the user will do next and preloads the necessary information from the server.

  • Submission Throttling: Incrementally sends data to the server.

  • Periodic Refresh: Also known as polling, periodically polls the server for updated information.

  • Multi-Stage Download: Downloads only the most important information first and then sends subsequent requests for additional information.

Consider the relative priorities among these four patterns. None of them is as important as a user action, so that means a priority of 1 or lower.

While helpful, Predictive Fetch is far from high priority. Its intent is to improve the user experience, not to control it. In an Ajax application that is making requests using various patterns, chances are that Predictive Fetch requests are a fairly low priority. Assuming that priorities are assigned from 0 to 10, Predictive Fetch may accurately be described as a priority of 5.

Submission Throttling is more important than Predictive Fetch because it is sending user information to the server as opposed to retrieving information from the server. Once again, this is not as important as a user action, so it falls somewhere between a priority of 0 and 5, probably landing at 2.

Periodic Refresh is very similar to Predictive Fetch, though the fact that it's sent on a recurring basis indicates that it's more important. More than likely, Periodic Refresh is waiting to indicate some new information to the user as soon as it's available. Because it's receiving data from the server, however, it would be a lower priority than Submission Throttling, which sends data to the server. So, Periodic Refresh is a priority of 3.

The last pattern is Multi-Stage Download, which is actually just another form of Predictive Fetch. The only difference between the two is when the request(s) take place. For Multi-Stage Download, the requests typically take place at the initial page load, while Predictive Fetch can occur at any time, depending on user action. Really, the two patterns are too close to consider one a higher priority than the other, so Multi-Stage Download can also be considered a 5.

Now that the priorities are clear among these patterns, what can be done to make this easier for developers? The best approach is to add a method for each pattern, along with one for a user action, so that developers don't need to remember these priorities on their own. Also, by encapsulating this functionality and auto-assigning priorities, this frees you to easily change priorities later without changing code in multiple places.

Each of these methods works the same way: accept a request description object as an argument, assign the given priority, and then pass the modified object to the send() method to be queued. Since the names of the Ajax patterns are rather verbose, the method names have been shortened:

  • poll(): Use for Periodic Refresh.

  • prefetch(): Use for Predictive Fetch and Multi-Stage Download.

  • submit(): Use for a user action.

  • submitPart(): Use for Submission Throttling.

The code for each is as follows:

var RequestManager = (function () {

    var oManager = {

        AGE_LIMIT : 60000,

        DEFAULT_PRIORITY: 10,

        INTERVAL : 250,

        _active: new Array(),

_pending: new PriorityQueue(function (oRequest1, oRequest2) {...}),

        _agePromote : function() {...},

        _checkActiveRequests : function (){...},

        _createTransport : function (){...},

        _sendNext : function () {...},

        cancel : function (oRequest) {...},

        poll : function (oRequest) {
            oRequest.priority = 3;
            this.send(oRequest);
        },

        prefetch : function (oRequest) {
            oRequest.priority = 5;
            this.send(oRequest);
        },

        send : function (oRequest) {...},

        submit : function (oRequest) {
            oRequest.priority = 0;
            this.send(oRequest);
        },

        submitPart : function (oRequest) {
            oRequest.priority = 2;
            this.send(oRequest);
        }

    };

    //initialization
    setInterval(function () {...}, oManager.INTERVAL);

    //return the object
    return oManager;

})();

These methods can be used in place of send(), such as:

RequestManager.poll({
    type: "get",
    url: "example.htm",
    data: "post_data",
    onsuccess: function () {},
});

RequestManager.submitPart({
    type: "post",

url: "handler.php",
    data: "name=Nicholas",
    onsuccess: function () {},
});

Note that a priority property isn't assigned in these request description objects, as it is not needed. If a priority property were assigned, however, it would be overridden by the method being called.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset