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:

Sunday, February 5, 2012

iOS, Private Browsing, and the HTML5 Application Cache

This problem makes sense, but it caught me off-guard during debugging.

If you have a web application that uses the HTML5 Application Cache feature, the cache will not function properly with iOS Safari's Private Browsing setting on. The various JavaScript snippets that help with debugging will catch the most general error, which doesn't tell you much.

So if you have an app that seems to be caching properly in your desktop browser but not your mobile device, be sure to check your privacy settings!

Monday, December 26, 2011

Crabmeat Cream Sauce

We made this with lobster ravioli for a special occasion. The original recipe was for about one pound of pasta and in parentheses I've added my changes 3 pounds.
  • 2 ounces of unsalted butter (6 ounces for 3 lbs.)
  • 1 tablespoon of chopped shallots (1 large bulb for 3 lbs.)
  • 4 ounces of chunk crabmeat (8 ounces for 3 lbs.)
  • 2 ounces of Cognac (I substituted Grand Marnier because I had it on hand and used only 2 ounces for 3 lbs. because of its strong flavor)
  • 5 ounces of tomato sauce (15 ounces for 3 lbs.)
  • 10 ounces of heavy cream (30 ounces for 3 lbs.)
  • Salt to taste
In a pan or pot large enough to contain all the liquid, melt the butter and then sauté the shallots on medium heat until they are translucent.

Add the crabmeat and sauté for 2-3 minutes more.

Remove from the heat, add the Cognac, then put it back on the burner. PLEASE BE CAREFUL! The Cognac might ignite and result in a large flame. Don't burn your face off. Sauté for 2-3 minutes more.

Add the tomato sauce, cream, and salt. Cook until it reduces about halfway and becomes thicker.

Add the sauce to the cooked ravioli and let them sit for 1-2 minutes to absorb some of the sauce. Serve immediately.

Peach Cobbler in a Cast Iron Skillet

This is insanely good, especially if you like cast-iron cooking.
  • 6 tablespoons of unsalted butter
  • 1 cup of sugar
  • 1 cup of flour
  • 2 teaspoons of baking powder
  • 1/4 teaspoon of salt
  • 1 cup + 1 tablespoon of whole milk
  • 1 can of sliced peaches with their liquid (I have been substituting 1 jar of Trader Joe's peach halves, minus 2 halves, with about half the liquid in the jar)
Pre-heat the oven to 350 with a 10" or 12" cast iron skillet inside. After the oven is pre-heated, let it continue to heat for another 8-10 minutes.

Mix sugar, flour, baking powder, salt, and milk together into a batter.

Take the skillet out of the oven and melt the butter in it. I find this easier if I cut the butter into 1-tablespoon blocks. Pour the batter into the skillet. Spread the peaches on top of the batter. Pour the liquid on top of that. If you're using a 12" skillet, don't worry if the batter doesn't spread all the way out to the edges--it will rise to about 3/4 of an inch.

Bake at 350 for 30-40 minutes.

Monday, November 7, 2011

Testing Geolocation Locally with Chrome and URIs with file://

If you need to test geolocation in Chrome using local files -- where your URL is going to start with file:// -- you need to throw a switch when you start Chrome.

--allow-file-access-from-files

If you're a Launchy user like me, you can do this by starting to type Chrome, hitting Tab, then pasting in that switch.

This issue is documented and tracked here: http://code.google.com/p/chromium-os/issues/detail?id=13009