I am not going to illustrate the implementation of the HTTP validation cache just yet. First, I will show you the HTML code used by the client so that you will understand where the respon- sibilities lie. I don’t want to show the HTTP validation cache code because doing so would confuse you and make you wonder, “Okay, so why is this code doing this?”
From the perspective of the HTML code, the cache would operate transparently, just like what happens when using a browser. When implementing a predictive cache, the HTML code should need to provide only a function that is used for prefetching URLs. The following HTML code illustrates an ideal implementation:
<html> <head>
<title>Cached Content</title>
<script language="JavaScript" src="../lib/factory.js"></script> <script language="JavaScript" src="../lib/asynchronous.js"></script> <script language="JavaScript" src="../lib/cache.js"></script> <script language="JavaScript" type="text/javascript">
CacheController.prefetch = function(url) { if( url == "../chap03/chunkhtml01.html") {
CacheController.getCachedURL( "../chap03/chunkimage02.html"); }
}
var cache = new CacheProxy();
cache.complete = function(status, statusText, responseText, responseXML) { document.getElementById("insertplace").innerHTML = responseText; document.getElementById("status").innerHTML = status; document.getElementById("statustext").innerHTML = statusText; } function clearit() { document.getElementById("insertplace").innerHTML = "empty"; document.getElementById("status").innerHTML = "empty"; document.getElementById("statustext").innerHTML = "empty"; } </script> </head> <body>
<button onclick="cache.get('../chap03/chunkhtml01.html')">Get Content 1</button> <button onclick="cache.get('../chap03/chunkimage02.html')">Get Content 2</button>
<button onclick="clearit()">Clear Fields</button> <table> <tr><td id="insertplace">Nothing</td></tr> <tr><td id="status">Nothing</td></tr> <tr><td id="statustext">Nothing</td></tr> </table> </body> </html>
The example uses four script tags, and the third tag references the cache script code file cache.js. In the implementation of the cache.js file is an instantiation of the variable
CacheController. Because a cache must operate on all requests made by the browser, there is a single variable instance containing all cached content. Because Asynchronous is a type that can be instantiated for the Cache Controller pattern to properly implement the Proxy pattern, the type CacheProxy is defined.
The method CacheController.prefetch is used by the predictive cache code to prefetch other HTTP content. What happens in the implementation of the cache code is that when a request for content is made, the prefetch function is called with the URL that is being fetched. The prefetching implementation can then preload a single piece or multiple pieces of HTTP content based on the URL currently being retrieved. How much content is preloaded depends entirely on the prefetch function implementation.
Let’s step back for a moment and think about prefetch. The HTML page defines a prefetch
function, which contains the logic of what to get and when. The exact logic contained within the prefetch implementation reflects the possible operators associated with the data to prefetch. In the context of a mapping application, that means the prefetch logic must incorporate the resources that can be loaded using the zooming and panning functionality. Where the prefetch
logic becomes complicated is if there are two areas on the HTML page where content can be preloaded. Then, as in the example, it is important to figure out what the URL is requesting and to preload the required resources.
In a nutshell, when writing prefetch implementations, the URLs should be fairly logical and easy to deduce. Using the mapping example, if the URL is http://mydomain.com/0/0, panning up would reference the element http://mydomain/0/1. The numbers in the URL repre- sent latitude and longitude, and moving up means shifting from longitude 0 to longitude 1. The URL numbers don’t include the zooming factor, but that can be calculated. As a rule of thumb, if in your prefetch implementation you cannot logically deduce what the associated resources are based on the URL being requested, then most likely you have a passive cache only.
Getting back to the example HTML page, the variable cache is an instance of CacheProxy, which acts as a proxy for the Asynchronous class. This means whatever methods and properties exist for Asynchronous also exist for CacheProxy. As with Asynchronous, to process a response the cache.complete function is assigned. Note that when the content is prefetched by the predictive cache, the complete method is not called. This is because data that is fetched by the predictive cache is considered in a raw state and not a processed state. Only when the client makes a request for prefetched content will complete be called. As with Asynchronous, multiple
CacheProxy instances can be created; however, there is only a single instance of CacheController. The function clearit is used to clear the results so that when testing the HTML code it is possible to reset the Dynamic HTML fields insertplace, status, and statustext. To retrieve the code that is inserted into the Dynamic HTML fields, the buttons Get Content 1 and Get Content 2 are clicked. The method that is called is CacheController.getURL, which requires two
parameters. The first parameter is the URL that is downloaded, and the second URL is the complete function that receives the results.
To illustrate how the HTML content is cached, it is not helpful to show the pages after they have been downloaded. Showing images after the fact illustrates that content is available and presented but does not illustrate where the content came from—whether it was from the cache or from an HTTP request. A better way to show that there is a cache with content is to set a breakpoint in the code and illustrate the contents of the cache with a debugger. Figure 4-4 shows the variable CacheController and the cache that it contains.
In Figure 4-4, the this variable in the middle-left window is the CacheController instance. The property _cache is an Array instance that contains the cached objects. Notice that stored in the cache are the objects chunkhtml01.html and chunkimage02.html, which happen to be the HTTP content retrieved by the buttons.
Implementing CacheController and CacheProxy
Implementing CacheController requires implementing a script-defined passive cache. It would seem that writing a passive cache is a bad idea because the browser already does this. The script- defined passive cache is necessary because of the browser-defined passive cache inconsistencies. The script-defined passive cache does not implement any sophisticated cache algorithm. However, if you wanted to extend the functionality of the cache, you could. The variable
CacheController implements the client side of the HTTP Validation model. Implementing the HTTP Validation model on the client side requires the client to receive, store, and send entity tags when sending requests and receiving responses. Then based on the HTTP return codes, the cache controller receives, stores, and returns new content, or returns old content to the consuming script of the passive cache.
The following is the implementation of CacheController (the details of the getURL function are missing because that function is fairly complicated and will be explained in pieces later in the chapter):
var CacheController = { cache : new Array(),
prefetch : function( url) { },
didNotFindETagError : function(url) { } getCachedURL : function(url) {
var func = function(status, statusText, responseText, responseXML) { } CacheController.getURL(url, func, true);
},
getURL : function(url, inpCP, calledFromCache) { }
}
At first glance, the cache seems to expose one property and three functions. The reality is that CacheController is making extensive use of JavaScript anonymous functions, making the implementation contain more functions than illustrated. The effectiveness of CacheController
depends completely on the HTTP server; if the server does not use entity tags, no caching will occur and CacheController will pass all requests directly to the user’s complete function implementation.
The property _cache is an Array instance that contains a series of objects representing the cache. Each entry in the cache is associated with and found by using a URL. HTTP content is added to the array when the CacheController internally-defined complete method’s parameter
status has a value of 200, indicating a successful downloading of HTTP content. The
CacheController internally-defined complete method is an anonymous function assigned to
asynchronous.complete.
The property prefetch is a function that is assigned by the HTML code to preload HTML content pieces into the cache. If the default prefetch function implementation is used, the predictive cache becomes passive because the prefetch function does nothing.
The function getCachedURL retrieves HTTP content from a server, and is called by the
prefetch function defined by HTML code. The implementation of getCachedURL passes three parameters to getURL. The first parameter is the URL that is being retrieved. The second parameter is the complete function implementation, which for the prefetch implementation is neither required nor desired and hence is an empty function declaration. The third parameter, and the only one required to be passed by the getCachedURL function, stops the prefetch function from being called again. Otherwise, a recursive loop of calling getURL that calls prefetch that calls
getURL and so on could be initiated.
The method getURL retrieves HTTP content that is added to the cache. The method is called by the still undefined CacheProxy. Following is an implementation of getURL without the implementation of the anonymous functions:
getURL : function(url, inpCP, calledFromCache) { var asynchronous = new Asynchronous(); var cacheProxy = inpCP;
asynchronous.openCallback = function(xmlhttp) { }
asynchronous.complete = function(status, statusText, responseText, responseXML) { }
asynchronous.get(url);
if( calledFromCache != true) { CacheController.prefetch(url); }
}
Within the implementation of getURL, the anonymous function has been used several times. The anonymous function solves the problem of associating variables with object instances, as explained in Chapter 2. In the abbreviated implementation, each time getURL is called, an instance of Asynchronous is created. This was done on purpose so that multiple HTTP requests could retrieve data concurrently. Remember that CacheController is a single instance. The function
openCallback is new and is used to perform a callback operation after the XMLHttpRequest.open
method has been called. The implementation of openCallback is called by Asynchronous after the XMLHttpRequest.open method has been called. The function openCallback is required because it is not possible to call the method XMLHttpRequest.setRequestHeader until the
XMLHttpRequest.open method has been called.
The anonymous function assignment of asynchronous.complete is needed so that when a URL has been retrieved, the data can be processed. In the implementation of asynchronous. complete are the details of the Cache Controller pattern implementation. The method
asynchronous.get calls XMLHttpRquest and the server. After the call has been made and the
getURL method is not called from a prefetch implementation (calledFromCache != true), the
prefetch implementation is called.
In this implementation of CacheManager, the prefetch function is called before the request has a chance to return with the data. The prefetch function is called before a response can be generated because it is assumed that the prefetch logic can deduce from a URL what an asso- ciated URL is. However, in some situations a URL cannot be deduced because the associated URLs are defined in the response of the request. This happens when the Decoupled Navigation pattern is implemented. In that case, the CacheManager prefetch-calling functionality has to be
called in the anonymous function implementation assigned to asynchronous.complete. The modification of the prefetch functionality is beyond the scope of this book, but is mentioned as a potential extension that you may need to implement.
Focusing now on the incomplete anonymous functions, and specifically the anonymous
openCallback function:
asynchronous.openCallback = function(xmlhttp) { var obj = CacheController._cache[url]; if(obj != null) {
xmlhttp.setRequestHeader("If-None-Match", obj.ETag); }
cacheProxy.openCallback(xmlhttp); }
In the implementation of openCallback, the _cache array that contains all of the object instances is referenced, and the element associated with the variable url is retrieved and assigned to obj. If the URL exists, obj will not be equal to null and will have an associated ETag
identifier. The associated ETag is assigned to the request by using the method setRequestHeader, with the HTTP header identifier If-None-Match. This process of retrieving the URL object instance and assigning the ETag identifier is the essence of HTTP validation.
Let’s focus on the complete anonymous function implementation:
asynchronous.complete = function(status, statusText, responseText, responseXML) { if(status == 200) {
try {
var foundetag = this._xmlhttp.getResponseHeader("ETag"); if(foundetag != null) { CacheController._cache[url] = { ETag : foundetag, Status : status, StatusText : statusText, ResponseText : responseText, ResponseXML : responseXML }; } else { CacheController.didNotFindETagError(url); } } catch(exception) { CacheController.didNotFindETagError(url); } if(calledFromCache != true) {
cacheProxy.complete(status, statusText, responseText, responseXML); }
else if(status == 304) {
var obj = CacheController._cache[ url]; if(obj != null) {
cacheProxy.complete(obj.Status, obj.StatusText, obj.ResponseText, obj.ResponseXML);
} else {
throw new Error("Server indicated that this ➥
data is in the cache, but it does not seem to be"); }
} else {
if(calledFromCache != true) {
cacheProxy.complete(status, statusText, responseText, responseXML); }
} }
In the implementation, three possible actions can be carried out: HTTP return code value 200 for success, HTTP return code value 304 for a state that has not changed, and the default.
If the server returns an HTTP 200 value, the ETag is searched for in the returned HTTP headers. This condition results either when the request has never been issued before or if the already issued cached content has changed. Regardless of whether the local cache instance exists, if there is an entity tag value, then the variable CacheController._cache[ url] is assigned with the new object that has five properties: ETag, Status, StatusText, ResponseText, and
ResponseXML.
The entire ETag retrieval and assignment is wrapped in a trycatch exception block and if
statement to ensure that only objects that have an associated ETag identifier are added to the cache. If there is no ETag, the method CacheController.didNotFindETagError is called with the URL. The purpose of the method is to get the user to stop using the prefetch function. Remember that if there is no ETag, there is no caching, and hence doing a prefetch is silly. There is a default method implementation for the method didNotFindETagError, but the HTML page can imple- ment its own.
Still focusing on the complete anonymous function implementation, if the status is 304 (indicating unchanged content that the client already has), the cache is queried using the URL, and the associated content is sent to the client. There will always be content in the cache because if an ETag was generated, there is a value in the _cache variable. The retrieved content is assigned to the variable obj, and then the userComplete method is called with cached content. If in the function implementation a status code other than 200 or 304 is generated, it is passed directly to the client for further processing.
The class CacheProxy is the Proxy pattern implementation, and its implementation determines whether the call is to Asynchronous or CacheController. The following is a partial implementa- tion of CacheProxy, with the redundant pieces removed:
function CacheProxy() { }
function CacheProxy_get(url) {
CacheController.getURL(url, this, false); }
function CacheProxy_post(url, mimetype, datalength, data) { var thisreference = this;
asynchronous = new Asynchronous();
asynchronous.openCallback = function(xmlhttp) { thisreference.openCallback(xmlhttp); }
asynchronous.complete = function(status, statusText, ➥
responseText, responseXML) {
thisreference.complete(status, statusText, ➥
responseText, responseXML); }
asynchronous.post(url, mimetype, datalength, data); } CacheProxy.prototype.openCallback = CacheProxy_openCallback; CacheProxy.prototype.complete = CacheProxy_complete; CacheProxy.prototype.get = CacheProxy_get; CacheProxy.prototype.put = CacheProxy_put; CacheProxy.prototype.del = CacheProxy_delete; CacheProxy.prototype.post = CacheProxy_post;
To act as a full proxy for Asynchronous, CacheProxy needs to expose the methods that
Asynchronous exposes. This means CacheProxy needs to implement the get, put, del, and other such methods. Implementing a proxy means delegating functionality. In the case of the client calling an HTTP GET, it means the function CacheProxy_get needs to delegate to CacheController. For all of the other functions (for example, CacheProxy_post), CacheProxy delegates to Asynchronous. Doing a full proxy implementation of Asynchronous also requires implementing openCallback
and callback, which then delegates to the method CacheProxy.openCallback or CacheProxy. complete that will call the user-defined implementations, if the user defined an implementation.