Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
I am the chief technical officer at InVision App, Inc - a prototyping and collaboration platform for designers, built by designers. I also rock out in JavaScript and ColdFusion 24x7.
Meanwhile on Twitter
Loading latest tweet...
Ben Nadel at Scotch On The Rocks (SOTR) 2011 (Edinburgh) with:

Using HTML5 Offline Application Cache Events In Javascript

By Ben Nadel on

I've played around with HTML5's Offline Application Cache and Cache Manifest features before; however, I've never looked into how the application cache activity can be monitored with Javascript. After watching Peter Lubbers' "HTML5 Offline Web Applications" presentation at the HTML5 Meetup.com group last night, I was finally inspired to dig a little deeper into the application cache lifecycle.

 
 
 
 
 
 
 
 
 
 

I've already blogged about how to set up the offline application cache manifest file and explored which pages do and do not have access to cached assets; so, I won't both explaining the basic configuration. What's important to understand in this post is that as the browser interacts with the cache manifest file and builds the local cache, it triggers a number of events to which we can bind. As this happens, the following application cache events are available:

  • checking - The browser is checking for an update, or is attempting to download the cache manifest for the first time. This is always the first event in the sequence.
  • noupdate - The cache manifest hadn't changed.
  • downloading - The browser has started to download the cache manifest, either for the first time or because changes have been detected.
  • progress - The browser had downloaded and cached an asset. This is fired once for every file that is downloaded (including the current page which is cached implicitly).
  • cached - The resources listed in the manifest have been fully downloaded, and the application is now cached locally.
  • updateready - The resources listed in the manifest have been newly redownloaded, and the script can use swapCache() to switch to the new cache.
  • obsolete - The cache manifest file could not be found, indicating that the cache is no longer needed. The application cache is being deleted.
  • error - An error occurred at some point - this could be caused by a number of things. This will always be the last event in the sequence.

To experiment with these events, I am going to cache the same ColdFusion file a number of times. Here is the cache manifest:

Manifest.cfm - Cache Manifest

  • <!---
  • Define the Cache Manifest content. I'm doing it this way since
  • the "CACHE MANIFEST" line needs to be the first line in the file
  • and storing it in a buffer allows us to TRIM later without having
  • ugly line breaks.
  • --->
  • <cfsavecontent variable="cacheManifest">
  •  
  • <!---
  • NOTE: Cache Manifest must be the very first thing in this
  • manifest file.
  • --->
  • CACHE MANIFEST
  •  
  • <!---
  • When a cache manifest is reviewed by the browser, it uses a
  • complete byte-wise comparison. As such, we can use COMMENTS
  • to invalidate a previously used cache manifest. In this way,
  • we can use a version-comment to indicate change even when the
  • file list has not changed (by the file contents have!).
  •  
  • NOTE: If ANY part of this file is different from the previous
  • cache manifest, ALL of the files are re-downloaded.
  • --->
  • # Cache Manifest Version: 1.10
  •  
  • ./index.cfm
  • ./jquery-1.4.2.js
  •  
  • # Cache our sleeper pages. Notice that these are all the same script,
  • # but are differentiated by their arbitrary IDs.
  •  
  • ./sleeper.cfm?id=1
  • ./sleeper.cfm?id=2
  • ./sleeper.cfm?id=3
  • ./sleeper.cfm?id=4
  • ./sleeper.cfm?id=5
  • ./sleeper.cfm?id=6
  • ./sleeper.cfm?id=7
  • ./sleeper.cfm?id=8
  • ./sleeper.cfm?id=9
  • ./sleeper.cfm?id=10
  •  
  •  
  • # This is where we can white-list pages that cannot be cached.
  •  
  • NETWORK:
  •  
  • ./manifest.cfm
  •  
  •  
  • # This is where we can define fall-backs.
  •  
  • FALLBACK:
  •  
  • </cfsavecontent>
  •  
  •  
  • <!--- ----------------------------------------------------- --->
  • <!--- ----------------------------------------------------- --->
  •  
  •  
  • <!---
  • Let's reset the output and set the appropriate content type.
  • It is critical that the manifest file be served up as a type
  • "text/cache-manifest" mime-type otherwise the client simply
  • will not treat this as a cache manifest.
  •  
  • NOTE: We need to be careful about the whitespace here since
  • the very first line of the file must contain the phrase,
  • "CACHE MANIFEST". As such, we must TRIM() the content.
  • --->
  • <cfcontent
  • type="text/cache-manifest"
  • variable="#toBinary( toBase64( trim( cacheManifest ) ) )#"
  • />

As you can see, we are caching the file - sleeper.cfm - ten times. I am using a URL parameter in order to differentiate the cached asset. Since the cache manifest defines URLs, not script names, the ID=N query string value is enough to turn one file into ten different cache entries. The sleeper.cfm itself will sleep for one thousand milliseconds in order to give our master page time to observe the download events as they happen.

You might notice that I have white-listed (Network) the cache manifest file itself. This is not something you would typically do; I have done this here to help minimizing cache of an asynchronous (AJAX) manifest request later on in the demo.

Sleeper.cfm - Our Cached Asset

  • <!--- Param the page ID. --->
  • <cfparam name="url.id" type="numeric" default="0" />
  •  
  • <!---
  • This page is being cached. Sleep the thread so that it gives us
  • enough time to observe the caching events.
  • --->
  • <cfset sleep( 1 * 1000 ) />
  •  
  • <!--- Return some content arbitrary test content. --->
  • <cfoutput>
  •  
  • I am sleeper page #url.id#.
  •  
  • </cfoutput>

Ok, now that we understand our offline application cache manifest file and the secondary assets that we are going to cache, let's take a look at the demo that makes use of the application cache events. In the following code, we will be binding the previously outlined events using the window.applicationCache object and jQuery's bind() method. As each event fires, we are simply going to output it to the screen.

  • <!---
  • Use the HTML5 doc type and provide a link to the Cache
  • Manifest file for this application.
  • --->
  • <!DOCTYPE html>
  • <html manifest="./manifest.cfm">
  • <head>
  • <title>Listening For Cache Events From The Application Cach</title>
  • <script type="text/javascript" src="./jquery-1.4.2.js"></script>
  • </head>
  • <body>
  •  
  • <h1>
  • Listening For Cache Events From The Application Cach
  • </h1>
  •  
  • <p>
  • Application Status: <span id="applicationStatus">Online</span>
  • -
  • <!--- Output the time. --->
  • <cfset writeOutput( timeFormat( now(), "h:mm:ss TT" ) ) />
  • </p>
  •  
  • <p>
  • <a id="manualUpdate" href="#">Check for an updated Cache</a>
  • </p>
  •  
  •  
  • <h2>
  • Application Cache Events
  • </h2>
  •  
  • <p>
  • Progress: <span id="cacheProgress">N/A</span>
  • </p>
  •  
  • <ul id="applicationEvents">
  • <!-- This will be populated dynamically. -->
  • </ul>
  •  
  •  
  •  
  • <!-- When the DOM is ready (ie. Now), run the scripts. -->
  • <script type="text/javascript">
  •  
  • // Get the DOM references we'll need to play with.
  • var appStatus = $( "#applicationStatus" );
  • var appEvents = $( "#applicationEvents" );
  • var manualUpdate = $( "#manualUpdate" );
  • var cacheProgress = $( "#cacheProgress" );
  •  
  • // Get a short-hand for our application cache object.
  • var appCache = window.applicationCache;
  •  
  • // Create a cache properties object to help us keep track of
  • // the progress of the caching.
  • var cacheProperties = {
  • filesDownloaded: 0,
  • totalFiles: 0
  • };
  •  
  •  
  • // I log an event to the event list.
  • function logEvent( event ){
  • appEvents.prepend(
  • "<li>" +
  • (event + " ... " + (new Date()).toTimeString()) +
  • "</li>"
  • );
  • }
  •  
  •  
  • // I get the total number of files in the cache manifest.
  • // I do this by manually parsing the manifest file.
  • function getTotalFiles(){
  • // First, reset the total file count and download count.
  • cacheProperties.filesDownloaded = 0;
  • cacheProperties.totalFiles = 0;
  •  
  • // Now, grab the cache manifest file.
  • $.ajax({
  • type: "get",
  • url: "./manifest.cfm",
  • dataType: "text",
  • cache: false,
  • success: function( content ){
  • // Strip out the non-cache sections.
  • // NOTE: The line break here is only to prevent
  • // wrapping in the BLOG.
  • content = content.replace(
  • new RegExp(
  • "(NETWORK|FALLBACK):" +
  • "((?!(NETWORK|FALLBACK|CACHE):)[\\w\\W]*)",
  • "gi"
  • ),
  • ""
  • );
  •  
  • // Strip out all comments.
  • content = content.replace(
  • new RegExp( "#[^\\r\\n]*(\\r\\n?|\\n)", "g" ),
  • ""
  • );
  •  
  • // Strip out the cache manifest header and
  • // trailing slashes.
  • content = content.replace(
  • new RegExp( "CACHE MANIFEST\\s*|\\s*$", "g" ),
  • ""
  • );
  •  
  • // Strip out extra line breaks and replace with
  • // a hash sign that we can break on.
  • content = content.replace(
  • new RegExp( "[\\r\\n]+", "g" ),
  • "#"
  • );
  •  
  • // Get the total number of files.
  • var totalFiles = content.split( "#" ).length;
  •  
  • // Store the total number of files. Here, we are
  • // adding one for *THIS* file, which is cached
  • // implicitly as it points to the manifest.
  • cacheProperties.totalFiles = (totalFiles + 1);
  • }
  • });
  • }
  •  
  •  
  • // I display the download progress.
  • function displayProgress(){
  • // Increment the running total.
  • cacheProperties.filesDownloaded++;
  •  
  • // Check to see if we have a total number of files.
  • if (cacheProperties.totalFiles){
  •  
  • // We have the total number of files, so output the
  • // running total as a function of the known total.
  • cacheProgress.text(
  • cacheProperties.filesDownloaded +
  • " of " +
  • cacheProperties.totalFiles +
  • " files downloaded."
  • );
  •  
  • } else {
  •  
  • // We don't yet know the total number of files, so
  • // just output the running total.
  • cacheProgress.text(
  • cacheProperties.filesDownloaded +
  • " files downloaded."
  • );
  •  
  • }
  • }
  •  
  •  
  • // Bind the manual update link.
  • manualUpdate.click(
  • function( event ){
  • // Prevent the default event.
  • event.preventDefault();
  •  
  • // Manually ask the cache to update.
  • appCache.update();
  • }
  • );
  •  
  •  
  • // Bind to online/offline events.
  • $( window ).bind(
  • "online offline",
  • function( event ){
  • // Update the online status.
  • appStatus.text( navigator.onLine ? "Online" : "Offline" );
  • }
  • );
  •  
  • // Set the initial status of the application.
  • appStatus.text( navigator.onLine ? "Online" : "Offline" );
  •  
  •  
  • // List for checking events. This gets fired when the browser
  • // is checking for an udpated manifest file or is attempting
  • // to download it for the first time.
  • $( appCache ).bind(
  • "checking",
  • function( event ){
  • logEvent( "Checking for manifest" );
  • }
  • );
  •  
  • // This gets fired if there is no update to the manifest file
  • // that has just been checked.
  • $( appCache ).bind(
  • "noupdate",
  • function( event ){
  • logEvent( "No cache updates" );
  • }
  • );
  •  
  • // This gets fired when the browser is downloading the files
  • // defined in the cache manifest.
  • $( appCache ).bind(
  • "downloading",
  • function( event ){
  • logEvent( "Downloading cache" );
  •  
  • // Get the total number of files in our manifest.
  • getTotalFiles();
  • }
  • );
  •  
  • // This gets fired for every file that is downloaded by the
  • // cache update.
  • $( appCache ).bind(
  • "progress",
  • function( event ){
  • logEvent( "File downloaded" );
  •  
  • // Show the download progress.
  • displayProgress();
  • }
  • );
  •  
  • // This gets fired when all cached files have been
  • // downloaded and are available to the application cache.
  • $( appCache ).bind(
  • "cached",
  • function( event ){
  • logEvent( "All files downloaded" );
  • }
  • );
  •  
  • // This gets fired when new cache files have been downloaded
  • // and are ready to replace the *existing* cache. The old
  • // cache will need to be swapped out.
  • $( appCache ).bind(
  • "updateready",
  • function( event ){
  • logEvent( "New cache available" );
  •  
  • // Swap out the old cache.
  • appCache.swapCache();
  • }
  • );
  •  
  • // This gets fired when the cache manifest cannot be found.
  • $( appCache ).bind(
  • "obsolete",
  • function( event ){
  • logEvent( "Manifest cannot be found" );
  • }
  • );
  •  
  • // This gets fired when an error occurs
  • $( appCache ).bind(
  • "error",
  • function( event ){
  • logEvent( "An error occurred" );
  • }
  • );
  •  
  • </script>
  •  
  • </body>
  • </html>

In addition to binding to the application cache events, we are also binding to the window object in order to listen for the "online" and "offline" events. These events will fire as the browser gains and loses its network connection respectively.

Most of the code in this demo is just basic event binding. The trickiest thing that we're doing is calculating the total number of files that get downloaded with the cache. As the events fire, we can see how many files have been downloaded; however, there is nothing in the event data that indicates how much progress we are making. As such, when the downloading kicks off, I am using an AJAX request to grab the live cache manifest file. Then, using regular expressions, I count the number of assets listed in the "Cache" portions of the manifest file in order to find the total number of files our cache construction will attempt to download.

Each browser has a strategy for making sure that the local cache is up-to-date; however, this cache synchronization is not always as timely as we would like (it is a cache after all). If we want to force the browser to check for cache updates, we can use the window.applicationCache.update() method to initiate an update check manually. When we do this, the browser will check to see if the live cache manifest file has been altered. If it has been, the browser will then start re-downloading all of the files listed in the cache manifest.

Once the manually-initiated cache has been downloaded, we need to explicitly tell the browser to start using the newly cached assets. In order to do that, we can call the window.applicationCache.swapCache() method. This will make the new cache available on the next page refresh.

NOTE: I have found that it is not always necessary to explicitly call the swapCache() method; it appears that some browsers handle this transition more implicitly that others.

I think the basic offline application cache functionality is very cool. I also like the ability to programmatically interact with the cache; however, I'm having trouble coming up with the best use-cases for such an explicit action. In any event, it's just nice to have finally had a chance to look deeper into this.




Reader Comments

@Ben,

You know what I would like to see?

  • $(appCache).bind("error",
  • function(error)
  • {
  • var str = "There are "
  • +arguments.length+" arguments.\n";
  • for (var key in event)
  • str += key+":"+event[key]+"\n";
  • logEvent(str);
  • });

Barring typos, that's intending to check for additional arguments beyond the first argument, then using introspection to build a dump of the first argument.

I think you could force an error by changing a filename of a file named in the manifest, so that it would not be found. I'm curious about just how much feedback is available in the arguments.

I would do it myself and report the results, but I'm under the gun to get some work done. Curious as to whether any useful feedback is possible in the error event. Probably varies by browser.

Anyway, good post!

Reply to this Comment

@Steve,

I can definitely check that out. I played around with the arguments and I found very little information available in them. Of course, between checking arguments, clearing offline caches, clearing regular caches, and making sure the manifest makes sense - you can definitely lose sight of what is actually going on :D

The offline cache has to be one of the most mentally trying things to test. Gone are the days of Type / Save / Refresh (at least with this topic).

I'll look into this more, get back to you.

Reply to this Comment

Awesome tutorial and code !

Helped me a lot in understanding the whole "Manifest" chapter. Very well explained.

Much appreciated Ben, keep them coming!

Reply to this Comment

For debugging the error (event) in Safari, I used:

$( appCache ).bind(
"error",
function( event ){
logEvent( console.error(event) );
}
);

not very helpful though.

Reply to this Comment

@AjaxLover,

Glad this helped you out. The offline cache capabilities are definitely thought provoking! I'm really trying to come up with a fun idea to play with for this kind of stuff. Right now, I'm just trying to get my fingers in a whole lot of pies to round out some understanding.

Reply to this Comment

I want to know what it contains index.cfm?
thanks a lot!

ps:
I have this error in chrome browse.
Application Cache Error event: Invalid manifest mime type ()

type="text/cache-manifest" is right? I've done.

Reply to this Comment

@Lee'Bin,

index.cfm was probably my main testing file. CFM is the ColdFusion file-extension and "index" is typically the directory default file.

"text/cache-manifest" should be good. If you are copy/pasting my code, you have to be running ColdFusion, otherwise none of it will work on the server-side.

Reply to this Comment

One of the issues we found if we weren't creating the app using one single page using AJAX. We found a pretty cool workaround we found to stop links in the offline app from breaking out into mobile Safari. The solution was on another CF forum (http://bit.ly/d21MxD) and basically you have to surround all your links with this JS code:

< href="javascript:window.location.href='URL.html'">URL</a>

This was a lifesaver! Not the most elegant solution, but it works!

Reply to this Comment

It seems as if the webpage loads but none of the events fire if the browser is without a connection. Thoughts?

Dustin

Reply to this Comment

@Tim,

I'm only just getting into the mobile space a bit; thanks for the tip.

@Dustin,

I can't remember if I ever tested that (events in offline mode). If they don't fire, I assume it's because the browser knows that it won't lead to anything? Not sure. Do you need them to fire in offline mode?

Reply to this Comment

Ben,
i've got a web servlet with 5 jsp pages; the method for call my servlet is POST. There is a lot AJAX calls between pages and servlet; it's possible to build n offline application with this scenario? I have to rewrite the calls inside jsp with GET method ? And about AJAX?

Thanks a lot!!

Reply to this Comment

Hi,

Im trying to cache pdf that are called from a server when on-line, to view in offline mode. I potentially can have upto 9 pdf's that I need to be also able to view offline? Any idea's?

Reply to this Comment

@Bruce,

That's an excellent question. I believe that in HTTP, a POST request is typically never cached (since it is designed to augment the state of the server). I *assume* that this concept would be carried over into the HTML5 Cache Manifest API; but, I have not tested this myself.

@Lee,

You should be able to cache PDFs, so long as they are in the cache manifest file. I haven't tested this; but, if they can cache images, I assume PDFs work the same way.

Reply to this Comment

Awesome tut! Thanks for sharing Ben! I'm using your concept, but I've tried a simpler approach to getting a count on the cache files. All my files are relative to root, so I just count the lines that begin with "/"...in your example, you'd obviously look for "./". Here's my code...

  • $.ajax({
  • type: "get",
  • url: "cache.manifest",
  • dataType: "text",
  • cache: false,
  • success: function( content ){
  • var totalFiles = content.match( /(^|\r*\n)\//g ).length;
  • cacheProperties.totalFiles = (totalFiles + 1);
  • }

Reply to this Comment

@Brian,

Ah, very nice. I think that works quite nicely. Way to simplify the problem by thinking about it in a slightly different way.

Reply to this Comment

Thank @Ben, I have a question that maybe you (or someone else) can help me with. The app cache is working great in Safari and Chrome. However, in Firefox the manifest triggers a new download on every page visit...as through the manifest has been updated. Has anyone seen this behavior? Any fixes? I've tried Mozilla's support forums and I'm not getting any answers unfortunately....

Reply to this Comment

hi can you give us an example without coldfusion? or what do i have to do to get this work on pure html and js?

Reply to this Comment

Hi Ben Thanks a lot for such an informative tut.

I have just started with HTML5, and i have some question about Cache Manifest that i am not been able find.

1.What's the difference between including the manifest file in the html tag and explicitly naming that resource in Manifest file(in case of multipage app)

2.Is it possible to only download those files which have been actually modified,right now all the files listed in the Manifest gets downloaded(it is just waste of bandwith).

Reply to this Comment

  • var totalFiles;
  • appCache.addEventListener('progress', function(e){
  • if( typeof totalFiles === 'undefined' ){
  • totalFiles = e.total; // tested on opera, chrome, ff, safari
  • console.log(totalFiles);
  • }
  • }, false);

Reply to this Comment

Is there any way to prevent a 404 on the appcache file from rendering the current cache as obsolete and removing the cache? I'd like to keep the appcache on the client when the server is down. Make sense?

Reply to this Comment

Wonderful information, I had come to know about your site from my friend nandu , hyderabad,i have read atleast 7 posts of yours by now, and let me tell you, your website gives the best and the most interesting information. This is just the kind of information that i had been looking for, i'm already your rss reader now and i would regularly watch out for the new posts, once again hats off to you! Thanks a ton once again, Regards, eventmanagementcompaniesinraipur.blogspot.in

Reply to this Comment

Their is a simple way to show the progress :
http://www.jefclaes.be/2012/04/visualizing-offline-application-cache.html

No need to use ajax !

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.