Skip to main content
Ben Nadel at NCDevCon 2011 (Raleigh, NC) with: Shannon Cross
Ben Nadel at NCDevCon 2011 (Raleigh, NC) with: Shannon Cross

Using HTML5 Offline Application Cache Events In Javascript

By
Published in , Comments (25)

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.

Want to use code from this post? Check out the license.

Reader Comments

81 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!

15,841 Comments

@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.

4 Comments

Awesome tutorial and code !

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

Much appreciated Ben, keep them coming!

4 Comments

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

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

not very helpful though.

15,841 Comments

@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.

1 Comments

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.

15,841 Comments

@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.

1 Comments

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!

15,841 Comments

@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?

1 Comments

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!!

1 Comments

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?

15,841 Comments

@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.

2 Comments

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);
	}
15,841 Comments

@Brian,

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

2 Comments

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....

1 Comments

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).

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

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?

1 Comments

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

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel