Saturday, March 3, 2012

Loading and Refreshing the Application Cache of Multiple Sites

THE PROBLEM:

We have multiple web applications that are offline-capable using the HTML5 Application Cache feature. We want to give users the ability to go to one location to load / refresh / update / synchronize all applications and avoid the need to go to each and every application individually. Details of interest: we are only targeting the iPad 2 (or higher) and each application is not guaranteed to be on the same domain or sub-domain.

THE SOLUTION:

The HTML5 Cross-Document Messaging feature + an iframe + jQuery. Certainly feels little goofy, but it works.

THE DETAILS:

At least at this stage, there doesn't appear to be a way to determine whether the Application Cache of a specific site is up-to-date without loading that site. Said another way, we don't know if a site's Application Cache contains stale data until we load that site, and as soon as that site is loaded, it will begin updating, if necessary. The user must then wait until the browser is done fetching and storing the resources for the cache to actually be ready for offline use.

Therefore, if we could come up with a way to visit each site for the user, we could guarantee the newest manifest file would be checked and the cache updated.

There are several pieces to this puzzle.

1) One site, which we'll call "the loader," with the list of URLs for the target sites in a JavaScript array. I injected that list into the page server side, but they don't need to be. These are the individual web applications using the Application Cache so they can be available offline.

2) An iframe on the loader, which can be visible or hidden.

3) The individual web applications will each need a function to listen for requests.
function crossDocumentListener(event) {
//NOTE: There's no security in place here! Educate yourself before using!
if (event) {
var response = "origin:" + event.origin + ";request:" + event.data + ";location:" + window.location + ";response:" + window.applicationCache.status;
event.source.postMessage(response, '*');
}
}
window.addEventListener("message", crossDocumentListener, false);

I'm returning the origin, the original request data, the site's location, and the application cache status. The latter is what I want most.

4) A listener on the loader to get the responses. It is set to message the site loaded in the iframe every 2 seconds.

function responseListener(event) {
var response = event.data;
displayResponseVisible(event);

// 0 = uncached, 2 = checking, 3 = downloading
if(response.indexOf('response:0') > -1 || response.indexOf('response:2') > -1 || response.indexOf('response:3') > -1) {
if(!intervalId) { // We only set it once.
intervalId = setInterval(
function() {
getStatus();
}
, 2000);
}
}
// 1 = idle, 4 = updateready, 5 = obsolete
else {
if(intervalId) {
clearInterval(intervalId);
intervalId = undefined;
}

if(nextUrlIndex == -1) {
// The array is empty. We're done.
$('#message').prepend(completeMessage);
}
else{
// There are more URLs in the array to process so load the next one.
$.when(loadUrl(nextUrlIndex)).then(function() { getStatus(); });
}
}
}
window.addEventListener('message', responseListener, false);

5) More code to glue it altogether. Here are the key bits.

function getStatus() {
document.getElementById("app").contentWindow.postMessage('status', '*');
}

function loadUrl(arrayIndex) {
// Set the URL on the iframe, triggering it to load.
var iframe = $('#app');
$(iframe).attr('src', urls[arrayIndex]);

// Set the nextUrlIndex appropriately.
if(arrayIndex + 1 == urls.length) nextUrlIndex = -1;
else nextUrlIndex = arrayIndex + 1;

// http://www.elijahmanor.com/2011/02/jquerydeferred-to-tell-when-certain.html
var deferred = $.Deferred();
iframe.load(deferred.resolve);

return deferred.promise();
}

$(document).ready(function() {
$('#loader').click(function(event) {
$.when(loadUrl(nextUrlIndex)).then(function() { getStatus(); });
event.preventDefault();
});
});

I'm not crazy about the design of this, but with the glue code in place the details can be refined. As I refine it I'll try to update this post. If you have another way to approach this that you believe is somehow an improvement, I'd love to hear it.

Resources: