From 9c55aeeb973d7cd241c2a66dde76f9e0450bf999 Mon Sep 17 00:00:00 2001 From: Andrew Wilcox Date: Thu, 14 Feb 2013 20:06:19 +0000 Subject: [PATCH 01/11] appcache package This code depends on PR 680. In addition, the docs include a link to the proposed AppCache wiki page. Adds the appcache smart package and associated documentation. QA notes are in packages/appcache/QA.md (Is this a good place to put them?) --- docs/client/docs.js | 1 + docs/client/packages.html | 1 + docs/client/packages/appcache.html | 31 ++++++ packages/appcache/QA.md | 93 +++++++++++++++++ packages/appcache/appcache-client.js | 65 ++++++++++++ packages/appcache/appcache-server.js | 148 +++++++++++++++++++++++++++ packages/appcache/package.js | 10 ++ 7 files changed, 349 insertions(+) create mode 100644 docs/client/packages/appcache.html create mode 100644 packages/appcache/QA.md create mode 100644 packages/appcache/appcache-client.js create mode 100644 packages/appcache/appcache-server.js create mode 100644 packages/appcache/package.js diff --git a/docs/client/docs.js b/docs/client/docs.js index 749ac59b82..4785064814 100644 --- a/docs/client/docs.js +++ b/docs/client/docs.js @@ -287,6 +287,7 @@ var toc = [ "Packages", [ [ "accounts-ui", + "appcache", "amplify", "backbone", "bootstrap", diff --git a/docs/client/packages.html b/docs/client/packages.html index adfd08f2b8..0d2d9e892e 100644 --- a/docs/client/packages.html +++ b/docs/client/packages.html @@ -17,6 +17,7 @@ and removed with: $ meteor remove {{> pkg_accounts_ui}} +{{> pkg_appcache}} {{> pkg_amplify}} {{> pkg_backbone}} {{> pkg_bootstrap}} diff --git a/docs/client/packages/appcache.html b/docs/client/packages/appcache.html new file mode 100644 index 0000000000..137470303a --- /dev/null +++ b/docs/client/packages/appcache.html @@ -0,0 +1,31 @@ + diff --git a/packages/appcache/QA.md b/packages/appcache/QA.md new file mode 100644 index 0000000000..b30aa23d0f --- /dev/null +++ b/packages/appcache/QA.md @@ -0,0 +1,93 @@ +# QA Notes + +## Viewing the app cache + +Chrome: Navigate to chrome://appcache-internals/ + +Firefox: Open Tools / Advanced / Network. The section reading "The +following websites are allowed to store data for offline use" will +show the amount of data in the app cache ("1.2 MB"). If this number +is 0 the app is permitted to use the app cache but the app cache is +currently turned off. + + +## Setup + +Create a simple static app and add the appcache package. + +static.html: + +```` + + some static content + +```` + +If you're testing with Firefox, enable it: + +static.js: + +```` +if (Meteor.isServer) { + Meteor.AppCache.config({ + firefox: true + }); +} +```` + + +## App is cached offline + +Run Meteor, load the app in the browser, stop Meteor. Reload the page +in the browser and observe the content is still visible. + + +## Hot code reload still works + +Run Meteor, open the app in the browser. Make a change to +static.html. Observe the change appear in the web page. + +Note that it is normal when using the app cache for the page reload to +be delayed a bit while the browser fetches the changed code in the +background. + +Without app cache: (page goes blank) -> (browser fetches) -> (page renders) + +With app cache: (browser fetches) -> (page goes blank) -> (page renders) + + +## Enabling / disabling the appcache turns the app cache on / off + +Run Meteor, open the app in the browser. + +Disable your browser in the appcache config. For example, if you're +using Chrome: + +```` +Meteor.AppCache.config({ + chrome: false +}); +```` + +Observe following the hot code reload the app is no longer cached. + +Enable your browser again: + +```` +Meteor.AppCache.config({ + chrome: true +}); +```` + +Observe following the hot code reload the app is cached again. + + +## Removing the appcache package turns off app caching + +Start Meteor, open the app in the browser. + +Stop Meteor, remove the appcache package, remove or comment out the +call to Meteor.AppCache.config in static.js, start Meteor again. + +Wait for the browser to reestablish its livedata connection. Observe +following the hot code reload that the app is no longer cached. diff --git a/packages/appcache/appcache-client.js b/packages/appcache/appcache-client.js new file mode 100644 index 0000000000..55f532c4aa --- /dev/null +++ b/packages/appcache/appcache-client.js @@ -0,0 +1,65 @@ +(function() { + + if (window.applicationCache == null) + return; + + var appCacheStatuses = [ + 'uncached', + 'idle', + 'checking', + 'downloading', + 'updateready', + 'obsolete' + ]; + + var updating_appcache = false; + var reload_retry = null; + var appcache_updated = false; + + Meteor._reload.onMigrate('appcache', function(retry) { + if (appcache_updated) + return [true]; + + // An uncached application (one that does not have a manifest) cannot + // be updated. + if (window.applicationCache.status === window.applicationCache.UNCACHED) + return [true]; + + if (!updating_appcache) { + try { + window.applicationCache.update(); + } catch (e) { + Meteor._debug('applicationCache update error', e); + // There's no point in delaying the reload if we can't update the cache. + return [true]; + } + updating_appcache = true; + } + + // Delay migration until the app cache has been updated. + reload_retry = retry; + return false; + }); + + // If we're migrating and the app cache is now up to date, signal that + // we're now ready to migrate. + var cacheIsNowUpToDate = function() { + if (!updating_appcache) + return; + appcache_updated = true; + return reload_retry(); + }; + + window.applicationCache.addEventListener('updateready', cacheIsNowUpToDate, false); + window.applicationCache.addEventListener('noupdate', cacheIsNowUpToDate, false); + + // We'll get the obsolete event on a 404 fetching the app.manifest: + // we had previously been running with an app cache, but the app + // cache has now been disabled or the appcache package removed. + // Reload immediately to get the new non-cached code. + + window.applicationCache.addEventListener('obsolete', (function() { + return window.location.reload(); + }), false); + +})(); diff --git a/packages/appcache/appcache-server.js b/packages/appcache/appcache-server.js new file mode 100644 index 0000000000..0003f7f166 --- /dev/null +++ b/packages/appcache/appcache-server.js @@ -0,0 +1,148 @@ +(function() { + + var app = __meteor_bootstrap__.app; + var bundle = __meteor_bootstrap__.bundle; + var crypto = __meteor_bootstrap__.require('crypto'); + var fs = __meteor_bootstrap__.require('fs'); + var path = __meteor_bootstrap__.require('path'); + + var knownBrowsers = ['android', 'chrome', 'firefox', 'ie', 'mobileSafari', 'safari']; + + var browsersEnabledByDefault = ['android', 'chrome', 'ie', 'mobileSafari', 'safari']; + + var enabledBrowsers = {}; + _.each(browsersEnabledByDefault, function (browser) { + enabledBrowsers[browser] = true; + }); + + Meteor.AppCache = { + config: function(options) { + _.each(options, function (value, option) { + if (option === 'browsers') { + enabledBrowsers = {}; + _.each(value, function (browser) { + enabledBrowsers[browser] = true; + }); + } + else if (_.contains(knownBrowsers, option)) { + enabledBrowsers[option] = value; + } + else { + throw new Error('Invalid AppCache config option: ' + option) + } + }); + } + }; + + var browserEnabled = function(request) { + return enabledBrowsers[request.browser.name]; + }; + + __meteor_bootstrap__.htmlAttributeHooks.push(function (request) { + if (browserEnabled(request)) + return 'manifest="/app.manifest"'; + else + return null; + }); + + app.use(function(req, res, next) { + if (req.url !== '/app.manifest') { + return next(); + } + + // Browsers will get confused if we unconditionally serve the + // manifest and then disable the app cache for that browser. If + // the app cache had previously been enabled for a browser, it + // will continue to fetch the manifest as long as it's available, + // even if we now are not including the manifest attribute in the + // app HTML. (Firefox for example will continue to display "this + // website is asking to store data on your computer for offline + // use"). Returning a 404 gets the browser to really turn off the + // app cache. + + if (!browserEnabled(__meteor_bootstrap__.categorizeRequest(req))) { + res.writeHead(404); + res.end(); + return; + } + + // After the browser has downloaded the app files from the server and + // has populated the browser's application cache, the browser will + // *only* connect to the server and reload the application if the + // *contents* of the app manifest file has changed. + // + // So we have to ensure that if any static client resources change, + // something changes in the manifest file. We compute a hash of + // everything that gets delivered to the client during the initial + // web page load, and include that hash as a comment in the app + // manifest. That way if anything changes, the comment changes, and + // the browser will reload resources. + + var hash = crypto.createHash('sha1'); + hash.update(JSON.stringify(__meteor_runtime_config__), 'utf8'); + hash.update(fs.readFileSync(path.join(bundle.root, 'app.html'))); + _.each(bundle.manifest, function (resource) { + if (resource.where === 'client') { + hash.update(resource.hash); + } + }); + var digest = hash.digest('hex'); + + var manifest = "CACHE MANIFEST\n\n"; + manifest += '# ' + digest + "\n\n"; + + manifest += "CACHE:" + "\n"; + manifest += "/" + "\n"; + _.each(bundle.manifest, function (resource) { + if (resource.where === 'client') { + manifest += resource.url + "\n"; + } + }); + manifest += "\n"; + + manifest += "FALLBACK:\n"; + manifest += "/ /" + "\n"; + manifest += "\n"; + + manifest += "NETWORK:\n"; + manifest += "/app.manifest" + "\n"; + _.each(Meteor._routePolicy.urlPrefixesFor('network'), function (urlPrefix) { + manifest += urlPrefix + "\n"; + }); + manifest += "*" + "\n"; + + // content length needs to be based on bytes + var body = new Buffer(manifest); + + res.setHeader('Content-Type', 'text/cache-manifest'); + res.setHeader('Content-Length', body.length); + return res.end(body); + }); + + var sizeCheck = function() { + var totalSize = 0; + _.each(bundle.manifest, function (resource) { + if (resource.where === 'client') { + totalSize += resource.size; + } + }); + if (totalSize > 5 * 1024 * 1024) { + return Meteor._debug( + "** You are publishing " + totalSize + " bytes of assets (including\n" + + "** the contents of the public/ directory) to be stored in the\n" + + "** browser's application cache.\n" + + "**\n" + + "** Browsers differ in the amount of data they will store in the app\n" + + "** cache, and if you go over their limit they don't gracefully fallback to\n" + + "** just running the app online (going over their limit breaks the app\n" + + "** online as well as making it not cacheable for offline use).\n" + + "**\n" + + "** To avoid this problem we recommend keeping the size of your static\n" + + "** application assets under 5MB." + ); + } + }; + + sizeCheck(); + +})(); diff --git a/packages/appcache/package.js b/packages/appcache/package.js new file mode 100644 index 0000000000..eb4a012bed --- /dev/null +++ b/packages/appcache/package.js @@ -0,0 +1,10 @@ +Package.describe({ + summary: "enable the application cache in the browser" +}); + +Package.on_use(function (api) { + api.use('startup', 'client'); + api.use('routepolicy', 'server'); + api.add_files('appcache-client.js', 'client'); + api.add_files('appcache-server.js', 'server'); +}); From 6bea9656d63a926e22dd1868b92b3e822aa1ab27 Mon Sep 17 00:00:00 2001 From: Andrew Wilcox Date: Fri, 15 Feb 2013 12:46:01 +0000 Subject: [PATCH 02/11] Avoid calling readFileSync at runtime. --- app/lib/bundler.js | 11 ++++++++--- packages/appcache/appcache-server.js | 5 +++-- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/app/lib/bundler.js b/app/lib/bundler.js index 2bd7b98146..f530b1c892 100644 --- a/app/lib/bundler.js +++ b/app/lib/bundler.js @@ -12,7 +12,7 @@ // - manifest [list of resources in load order, each consists of an object]: // { // "path": relative path of file in the bundle, normalized to use forward slashes -// "where": "client" [could also be "server" in future] +// "where": "client", "internal" [could also be "server" in future] // "type": "js", "css", or "static" // "cacheable": (client) boolean, is it safe to ask the browser to cache this file // "url": (client) relative url to download the resource, includes cache @@ -613,8 +613,13 @@ _.extend(Bundle.prototype, { fs.writeFileSync(full_path, self.files.server[rel_path]); } - fs.writeFileSync(path.join(build_path, 'app.html'), - self._generate_app_html()); + var app_html = self._generate_app_html(); + fs.writeFileSync(path.join(build_path, 'app.html'), app_html); + self.manifest.push({ + path: 'app.html', + where: 'internal', + hash: self._hash(app_html) + }); dependencies_json.core.push(path.join('lib', 'app.html.in')); fs.writeFileSync(path.join(build_path, 'unsupported.html'), diff --git a/packages/appcache/appcache-server.js b/packages/appcache/appcache-server.js index 0003f7f166..eb5f2c5f05 100644 --- a/packages/appcache/appcache-server.js +++ b/packages/appcache/appcache-server.js @@ -80,9 +80,8 @@ var hash = crypto.createHash('sha1'); hash.update(JSON.stringify(__meteor_runtime_config__), 'utf8'); - hash.update(fs.readFileSync(path.join(bundle.root, 'app.html'))); _.each(bundle.manifest, function (resource) { - if (resource.where === 'client') { + if (resource.where === 'client' || resource.where === 'internal') { hash.update(resource.hash); } }); @@ -105,6 +104,8 @@ manifest += "\n"; manifest += "NETWORK:\n"; + // TODO adding the manifest file to NETWORK should be unnecessary? + // Want more testing to be sure. manifest += "/app.manifest" + "\n"; _.each(Meteor._routePolicy.urlPrefixesFor('network'), function (urlPrefix) { manifest += urlPrefix + "\n"; From 6ed6e8ce172c7a94964db7a2568906da84946d15 Mon Sep 17 00:00:00 2001 From: Andrew Wilcox Date: Fri, 15 Feb 2013 17:18:11 -0800 Subject: [PATCH 03/11] appcache code review changes --- docs/client/packages/appcache.html | 7 +++--- packages/appcache/appcache-client.js | 32 +++++++++++++++++----------- packages/appcache/appcache-server.js | 12 +++++------ packages/appcache/package.js | 3 ++- 4 files changed, 30 insertions(+), 24 deletions(-) diff --git a/docs/client/packages/appcache.html b/docs/client/packages/appcache.html index 137470303a..650bb4554a 100644 --- a/docs/client/packages/appcache.html +++ b/docs/client/packages/appcache.html @@ -3,10 +3,9 @@ ## `appcache` -The `appcache` package is an experimental package to enable the -browser's application cache, which allows the static portion of a -Meteor application (the client side Javascript, HTML, CSS, and images) -to run offline. +The `appcache` package enables the browser's application cache, which +allows the static portion of a Meteor application (the client side +Javascript, HTML, CSS, and images) to run offline. The `appcache` package by itself however does not provide a mechanism for offline *data*: if an application starts up offline, a diff --git a/packages/appcache/appcache-client.js b/packages/appcache/appcache-client.js index 55f532c4aa..599e6eb002 100644 --- a/packages/appcache/appcache-client.js +++ b/packages/appcache/appcache-client.js @@ -1,6 +1,6 @@ (function() { - if (window.applicationCache == null) + if (! window.applicationCache) return; var appCacheStatuses = [ @@ -12,12 +12,12 @@ 'obsolete' ]; - var updating_appcache = false; - var reload_retry = null; - var appcache_updated = false; + var updatingAppcache = false; + var reloadRetry = null; + var appcacheUpdated = false; Meteor._reload.onMigrate('appcache', function(retry) { - if (appcache_updated) + if (appcacheUpdated) return [true]; // An uncached application (one that does not have a manifest) cannot @@ -25,7 +25,7 @@ if (window.applicationCache.status === window.applicationCache.UNCACHED) return [true]; - if (!updating_appcache) { + if (!updatingAppcache) { try { window.applicationCache.update(); } catch (e) { @@ -33,21 +33,21 @@ // There's no point in delaying the reload if we can't update the cache. return [true]; } - updating_appcache = true; + updatingAppcache = true; } // Delay migration until the app cache has been updated. - reload_retry = retry; + reloadRetry = retry; return false; }); // If we're migrating and the app cache is now up to date, signal that // we're now ready to migrate. var cacheIsNowUpToDate = function() { - if (!updating_appcache) + if (!updatingAppcache) return; - appcache_updated = true; - return reload_retry(); + appcacheUpdated = true; + reloadRetry(); }; window.applicationCache.addEventListener('updateready', cacheIsNowUpToDate, false); @@ -56,10 +56,16 @@ // We'll get the obsolete event on a 404 fetching the app.manifest: // we had previously been running with an app cache, but the app // cache has now been disabled or the appcache package removed. - // Reload immediately to get the new non-cached code. + // Reload to get the new non-cached code. window.applicationCache.addEventListener('obsolete', (function() { - return window.location.reload(); + if (reloadRetry) { + cacheIsNowUpToDate() + } + else { + appcacheUpdated = true; + Meteor._reload.reload(); + } }), false); })(); diff --git a/packages/appcache/appcache-server.js b/packages/appcache/appcache-server.js index eb5f2c5f05..7a9400cdc1 100644 --- a/packages/appcache/appcache-server.js +++ b/packages/appcache/appcache-server.js @@ -1,10 +1,10 @@ (function() { - var app = __meteor_bootstrap__.app; - var bundle = __meteor_bootstrap__.bundle; - var crypto = __meteor_bootstrap__.require('crypto'); - var fs = __meteor_bootstrap__.require('fs'); - var path = __meteor_bootstrap__.require('path'); + var app = __meteor_bootstrap__.app; + var bundle = __meteor_bootstrap__.bundle; + var crypto = __meteor_bootstrap__.require('crypto'); + var fs = __meteor_bootstrap__.require('fs'); + var path = __meteor_bootstrap__.require('path'); var knownBrowsers = ['android', 'chrome', 'firefox', 'ie', 'mobileSafari', 'safari']; @@ -128,7 +128,7 @@ } }); if (totalSize > 5 * 1024 * 1024) { - return Meteor._debug( + Meteor._debug( "** You are publishing " + totalSize + " bytes of assets (including\n" + "** the contents of the public/ directory) to be stored in the\n" + "** browser's application cache.\n" + diff --git a/packages/appcache/package.js b/packages/appcache/package.js index eb4a012bed..53f4526877 100644 --- a/packages/appcache/package.js +++ b/packages/appcache/package.js @@ -3,8 +3,9 @@ Package.describe({ }); Package.on_use(function (api) { - api.use('startup', 'client'); + api.use('reload', 'client'); api.use('routepolicy', 'server'); + api.use('startup', 'client'); api.add_files('appcache-client.js', 'client'); api.add_files('appcache-server.js', 'server'); }); From 1bbbe2901f1897483b2779adae2eefad04aaddcb Mon Sep 17 00:00:00 2001 From: Andrew Wilcox Date: Fri, 15 Feb 2013 17:31:59 -0800 Subject: [PATCH 04/11] add missing semicolon --- packages/appcache/appcache-client.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/appcache/appcache-client.js b/packages/appcache/appcache-client.js index 599e6eb002..cae65a397e 100644 --- a/packages/appcache/appcache-client.js +++ b/packages/appcache/appcache-client.js @@ -60,7 +60,7 @@ window.applicationCache.addEventListener('obsolete', (function() { if (reloadRetry) { - cacheIsNowUpToDate() + cacheIsNowUpToDate(); } else { appcacheUpdated = true; From 1be9a4989cb28fafda03d10da4f0406e67cf830a Mon Sep 17 00:00:00 2001 From: Andrew Wilcox Date: Mon, 11 Feb 2013 18:40:16 +0000 Subject: [PATCH 05/11] Depends on PR 685 (the appcache package). Enable the app cache for docs. Move the "forkme" image to public/ so that it is available offline. --- docs/.meteor/packages | 1 + docs/client/introduction.html | 2 +- docs/public/forkme_right_red_aa0000.png | Bin 0 -> 7927 bytes 3 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 docs/public/forkme_right_red_aa0000.png diff --git a/docs/.meteor/packages b/docs/.meteor/packages index 36eb4b5aea..cfb17d208a 100644 --- a/docs/.meteor/packages +++ b/docs/.meteor/packages @@ -10,3 +10,4 @@ code-prettify jquery-waypoints less spiderable +appcache diff --git a/docs/client/introduction.html b/docs/client/introduction.html index b48f876cad..82e53b0f7b 100644 --- a/docs/client/introduction.html +++ b/docs/client/introduction.html @@ -107,7 +107,7 @@ clean, classically beautiful APIs.

Developer Resources

-Fork me on GitHub +Fork me on GitHub If anything in Meteor catches your interest, we hope you'll get involved with the project! diff --git a/docs/public/forkme_right_red_aa0000.png b/docs/public/forkme_right_red_aa0000.png new file mode 100644 index 0000000000000000000000000000000000000000..1e19c21262bccf6ae951636758b99708de331f27 GIT binary patch literal 7927 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 VW=p$pVk+5ycXG0W literal 0 HcmV?d00001 From 2500312cd5a927a449c4bb2b7b91bc07759d65f5 Mon Sep 17 00:00:00 2001 From: Andrew Wilcox Date: Wed, 20 Feb 2013 14:21:47 +0000 Subject: [PATCH 06/11] Allow static resources to be configured as online only. Add a route policy type "static-online" for files in public/ that shouldn't be cached offline. Put "static-online" files in the manifest NETWORK section instead of in the CACHE section. --- app/server/server.js | 4 ++-- packages/appcache/appcache-server.js | 25 ++++++++++++++++---- packages/routepolicy/routepolicy.js | 28 +++++++++++++++++++++-- packages/routepolicy/routepolicy_tests.js | 26 +++++++++++++++------ 4 files changed, 67 insertions(+), 16 deletions(-) diff --git a/app/server/server.js b/app/server/server.js index f58d581955..ef8322a18e 100644 --- a/app/server/server.js +++ b/app/server/server.js @@ -157,9 +157,9 @@ var appUrl = function (url) { if (url === '/app.manifest') return false; - // Avoid serving app HTML for declared network routes such as /sockjs/. + // Avoid serving app HTML for declared routes such as /sockjs/. if (__meteor_bootstrap__._routePolicy && - __meteor_bootstrap__._routePolicy.classify(url) === 'network') + __meteor_bootstrap__._routePolicy.classify(url)) return false; // we currently return app HTML on all URLs by default diff --git a/packages/appcache/appcache-server.js b/packages/appcache/appcache-server.js index 7a9400cdc1..98dc7d0a42 100644 --- a/packages/appcache/appcache-server.js +++ b/packages/appcache/appcache-server.js @@ -27,6 +27,11 @@ else if (_.contains(knownBrowsers, option)) { enabledBrowsers[option] = value; } + else if (option === 'onlineOnly') { + _.each(value, function (urlPrefix) { + Meteor._routePolicy.declare(urlPrefix, 'static-online'); + }); + } else { throw new Error('Invalid AppCache config option: ' + option) } @@ -93,7 +98,8 @@ manifest += "CACHE:" + "\n"; manifest += "/" + "\n"; _.each(bundle.manifest, function (resource) { - if (resource.where === 'client') { + if (resource.where === 'client' && + ! Meteor._routePolicy.classify(resource.url)) { manifest += resource.url + "\n"; } }); @@ -107,9 +113,15 @@ // TODO adding the manifest file to NETWORK should be unnecessary? // Want more testing to be sure. manifest += "/app.manifest" + "\n"; - _.each(Meteor._routePolicy.urlPrefixesFor('network'), function (urlPrefix) { - manifest += urlPrefix + "\n"; - }); + _.each( + [].concat( + Meteor._routePolicy.urlPrefixesFor('network'), + Meteor._routePolicy.urlPrefixesFor('static-online') + ), + function (urlPrefix) { + manifest += urlPrefix + "\n"; + } + ); manifest += "*" + "\n"; // content length needs to be based on bytes @@ -139,7 +151,10 @@ "** online as well as making it not cacheable for offline use).\n" + "**\n" + "** To avoid this problem we recommend keeping the size of your static\n" + - "** application assets under 5MB." + "** application assets under 5MB.\n" + + "**\n" + + "** If you have some larger assets that you'd like to make online only,\n" + + "** you can do that with the AppCache "onlineOnly" config option." ); } }; diff --git a/packages/routepolicy/routepolicy.js b/packages/routepolicy/routepolicy.js index ce27701568..72300fa479 100644 --- a/packages/routepolicy/routepolicy.js +++ b/packages/routepolicy/routepolicy.js @@ -1,3 +1,25 @@ +// In addition to listing specific files to be cached, the browser +// application cache manifest allows URLs to be designated as NETWORK +// (always fetched from the Internet) and FALLBACK (which we use to +// serve app HTML on arbitrary URLs). +// +// The limitation of the manifest file format is that the designations +// are by prefix only: if "/foo" is declared NETWORK then "/foobar" +// will also be treated as a network route. +// +// Meteor._routePolicy is a low-level API for declaring the route type +// of URL prefixes: +// +// "network": for network routes that should not conflict with static +// resources. (For example, if "/sockjs/" is a network route, we +// shouldn't have "/sockjs/red-sock.jpg" as a static resource). +// +// "static-online": for static resources which should not be cached in +// the app cache. This is implemented by also adding them to the +// NETWORK section (as otherwise the browser would receive app HTML +// for them because of the FALLBACK section), but static-online routes +// don't need to be checked for conflict with static resources. + (function () { // The route policy is a singleton in a running application, but we @@ -17,8 +39,8 @@ }, checkType: function (type) { - if (! _.contains(['network'], type)) - return 'the route type must be "network"'; + if (! _.contains(['network', 'static-online'], type)) + return 'the route type must be "network" or "static-online"'; return null; }, @@ -35,6 +57,8 @@ checkForConflictWithStatic: function (urlPrefix, type, _testManifest) { var self = this; + if (type === 'static-online') + return null; var manifest = _testManifest || __meteor_bootstrap__.bundle.manifest; var conflict = _.find(manifest, function (resource) { return (resource.type === 'static' && diff --git a/packages/routepolicy/routepolicy_tests.js b/packages/routepolicy/routepolicy_tests.js index 51c38eaf59..17db8f1e2f 100644 --- a/packages/routepolicy/routepolicy_tests.js +++ b/packages/routepolicy/routepolicy_tests.js @@ -2,9 +2,8 @@ Tinytest.add("routepolicy", function (test) { var policy = new Meteor.__RoutePolicyConstructor(); policy.declare('/sockjs/', 'network'); - // App routes might look like this... - // policy.declare('/posts/', 'app'); - // policy.declare('/about', 'app'); + policy.declare('/bigphoto.jpg', 'static-online'); + policy.declare('/anotherphoto.png', 'static-online'); test.equal(policy.classify('/'), null); test.equal(policy.classify('/foo'), null); @@ -13,11 +12,14 @@ Tinytest.add("routepolicy", function (test) { test.equal(policy.classify('/sockjs/'), 'network'); test.equal(policy.classify('/sockjs/foo'), 'network'); - // test.equal(policy.classify('/posts/'), 'app'); - // test.equal(policy.classify('/posts/1234'), 'app'); + test.equal(policy.classify('/bigphoto.jpg'), 'static-online'); + test.equal(policy.classify('/bigphoto.jpg.orig'), 'static-online'); test.equal(policy.urlPrefixesFor('network'), ['/sockjs/']); - // test.equal(policy.urlPrefixesFor('app'), ['/about', '/posts/']); + test.equal( + policy.urlPrefixesFor('static-online'), + ['/anotherphoto.png', '/bigphoto.jpg'] + ); }); Tinytest.add("routepolicy - static conflicts", function (test) { @@ -26,9 +28,14 @@ Tinytest.add("routepolicy - static conflicts", function (test) { "path": "static/sockjs/socks-are-comfy.jpg", "type": "static", "where": "client", - "cacheable": false, "url": "/sockjs/socks-are-comfy.jpg" }, + { + "path": "static/bigphoto.jpg", + "type": "static", + "where": "client", + "url": "/bigphoto.jpg" + } ]; var policy = new Meteor.__RoutePolicyConstructor(); @@ -36,4 +43,9 @@ Tinytest.add("routepolicy - static conflicts", function (test) { policy.checkForConflictWithStatic('/sockjs/', 'network', manifest), "static resource /sockjs/socks-are-comfy.jpg conflicts with network route /sockjs/" ); + + test.equal( + policy.checkForConflictWithStatic('/bigphoto.jpg', 'static-online', manifest), + null + ); }); From 43282254a13f2abe66931d21871465edb8c32346 Mon Sep 17 00:00:00 2001 From: Andrew Wilcox Date: Thu, 21 Feb 2013 12:53:19 +0000 Subject: [PATCH 07/11] Fix syntax error in appcache-server. Ouch. --- packages/appcache/appcache-server.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/appcache/appcache-server.js b/packages/appcache/appcache-server.js index 98dc7d0a42..560763b609 100644 --- a/packages/appcache/appcache-server.js +++ b/packages/appcache/appcache-server.js @@ -154,7 +154,7 @@ "** application assets under 5MB.\n" + "**\n" + "** If you have some larger assets that you'd like to make online only,\n" + - "** you can do that with the AppCache "onlineOnly" config option." + "** you can do that with the AppCache \"onlineOnly\" config option." ); } }; From 87eb5c708ac1cba058e9366b49bfb64d67ab4649 Mon Sep 17 00:00:00 2001 From: Andrew Wilcox Date: Thu, 21 Feb 2013 14:49:41 +0000 Subject: [PATCH 08/11] Also add static resources from packages to the manifest. Static resources (such as images) added by packages weren't getting added to the manifest. This meant that e.g. the bootstrap glyph icons weren't available offline. (Fixes https://github.com/awwx/meteor-appcache/issues/1) --- app/lib/bundler.js | 57 +++++++++++++++++++++++++++------------------- 1 file changed, 33 insertions(+), 24 deletions(-) diff --git a/app/lib/bundler.js b/app/lib/bundler.js index f530b1c892..2c172f89c6 100644 --- a/app/lib/bundler.js +++ b/app/lib/bundler.js @@ -231,11 +231,19 @@ var Bundle = function () { // list of filenames self.css = []; + // images and other static files added from packages + // map from environment, to list of filenames + self.static = {client: [], server: []}; + // Map from environment, to path name (server relative), to contents // of file as buffer. self.files = {client: {}, client_cacheable: {}, server: {}}; - // See description of manifest at the top + // See description of the manifest at the top. + // Note that in contrast to self.js etc., the manifest only includes + // files which are in the final bundler output: for example, if code + // is minified, the manifest includes the minify output file but not + // the individual input files that were combined. self.manifest = []; // list of segments of additional HTML for / @@ -315,6 +323,7 @@ var Bundle = function () { self[options.type].push(data); } else if (options.type === "static") { self.files[w][options.path] = data; + self.static[w].push(options.path); } else { throw new Error("Unknown type " + options.type); } @@ -529,6 +538,25 @@ _.extend(Bundle.prototype, { // --- Static assets --- + var addClientFileToManifest = function (filepath, contents, type, cacheable, url) { + if (! contents instanceof Buffer) + throw new Error('contents must be a Buffer'); + var normalized = filepath.split(path.sep).join('/'); + if (normalized.charAt(0) === '/') + normalized = normalized.substr(1); + self.manifest.push({ + // path is normalized to use forward slashes + path: (cacheable ? 'static_cacheable' : 'static') + '/' + normalized, + where: 'client', + type: type, + cacheable: cacheable, + url: url || '/' + normalized, + // contents is a Buffer and so correctly gives us the size in bytes + size: contents.length, + hash: self._hash(contents) + }); + }; + if (is_app) { if (fs.existsSync(path.join(project_dir, 'public'))) { var copied = @@ -537,18 +565,8 @@ _.extend(Bundle.prototype, { _.each(copied, function (fs_relative_path) { var filepath = path.join(build_path, 'static', fs_relative_path); - var normalized = fs_relative_path.split(path.sep).join('/'); - self.manifest.push({ - // path is normalized to use forward slashes, so deliberately - // not using path.sep here - path: 'static/' + normalized, - type: 'static', - where: 'client', - cacheable: false, - url: '/' + normalized, - size: fs.statSync(filepath).size, - hash: self._hash(fs.readFileSync(filepath)) - }); + var contents = fs.readFileSync(filepath); + addClientFileToManifest(fs_relative_path, contents, 'static', false); }); } dependencies_json.app.push('public'); @@ -573,17 +591,7 @@ _.extend(Bundle.prototype, { else throw new Error('unable to find file: ' + file); - self.manifest.push({ - // path is normalized to use forward slashes - path: 'static_cacheable' + file.split(path.sep).join('/'), - where: 'client', - type: type, - cacheable: true, - url: url, - // contents is a Buffer and so correctly gives us the size in bytes - size: contents.length, - hash: self._hash(contents) - }); + addClientFileToManifest(file, contents, type, true, url); }; _.each(self.js.client, function (file) { processClientCode('js', file); }); @@ -594,6 +602,7 @@ _.extend(Bundle.prototype, { var full_path = path.join(build_path, 'static', rel_path); files.mkdir_p(path.dirname(full_path), 0755); fs.writeFileSync(full_path, self.files.client[rel_path]); + addClientFileToManifest(rel_path, self.files.client[rel_path], 'static', false); } // -- Client cache forever code -- From f72f366e201afb34452d922e186446d9b2d2cbaa Mon Sep 17 00:00:00 2001 From: Andrew Wilcox Date: Fri, 22 Feb 2013 16:23:48 -0800 Subject: [PATCH 09/11] appcache docs and tweak warning message --- docs/client/packages/appcache.html | 99 ++++++++++++++++++++++------ packages/appcache/appcache-server.js | 19 ++---- 2 files changed, 86 insertions(+), 32 deletions(-) diff --git a/docs/client/packages/appcache.html b/docs/client/packages/appcache.html index 650bb4554a..00b68561fd 100644 --- a/docs/client/packages/appcache.html +++ b/docs/client/packages/appcache.html @@ -3,28 +3,89 @@ ## `appcache` -The `appcache` package enables the browser's application cache, which -allows the static portion of a Meteor application (the client side -Javascript, HTML, CSS, and images) to run offline. +The `appcache` package stores the static parts of a Meteor application +(the client side Javascript, HTML, CSS, and images) in the browser's +[application cache](https://en.wikipedia.org/wiki/AppCache). -The `appcache` package by itself however does not provide a mechanism -for offline *data*: if an application starts up offline, a -`Meteor.Collection` on the client will be empty... until such time the -browser connects to the Internet and Meteor is able to establish its -online livedata connection. +* Once a user has visited a Meteor application for the first time and + the application has been cached, on subsequent visits the web page + loads faster because the browser can load the application out of the + cache without contacting the server first. -This package should not be used on a domain if there's a chance you -might want to revert to using Meteor 0.5.4 or older on that domain. -(Older versions of Meteor do not tell the browser to turn off the app -cache, which means that users who have your application cached would -be stuck running your old code even if you aren't using the `appcache` -package any more). Meteor >= 0.5.5 does not have this problem: the -browser will revert to not using the app cache if you remove the -`appcache` package. +* Hot code pushes are loaded by the browser in the background while the + app continues to run. Once the new code has been fully loaded the + browser is able to switch over to the new code quickly. -For more information about the `AppCache` package see the -[AppCache](https://github.com/meteor/meteor/wiki/AppCache) -page in the Meteor wiki. +* The application cache allows the application to be loaded even when + the browser doesn't have an Internet connection, and so enables using + the app offline. + +(Note however that the `appcache` package by itself doesn't make +*data* available offline: in an application loaded offline, a Meteor +Collection will appear to be empty in the client until the Internet +becomes available and the browser is able to establish a livedata +connection). + +The application cache works transparently in all supported browsers +except for Firefox, which pops up a message saying "This website is +asking to store data on your computer for offline use" and asks the +user whether to allow or deny the request. The application cache is +disabled on Firefox by default; to turn it on use: + + Meteor.AppCache.config({firefox: true}); + +You can also disable the application cache for specific browsers: + + Meteor.AppCache.config({ + chrome: false, + firefox: true, + ie: false + }); + +The supported browsers that can be enabled or disabled are `android`, +`chrome`, `firefox`, `ie`, `mobileSafari` and `safari`. + +Browsers limit the amount of data they will put in the application +cache, which can vary due to factors such as how much disk space is +free. Unfortunately if your application goes over the limit rather +than disabling the application cache altogether and running the +application online, the browser will instead fail that particular +*update* of the cache, leaving your users running old code. + +Thus it's best to keep the size of the cache below 5MB. The +`appcache` package will print a warning on the Meteor server console +if the total size of the resources being cached is over 5MB. + +If you have files too large to fit in the cache you can disable +caching by URL prefix. For example, + + Meteor.AppCache.config({onlineOnly: ['/online/']}); + +causes files in your `public/online` directory to not be cached, and +so they will only be available online. You can then move your large +files into that directory and refer to them at the new URL: + + + +If you'd prefer not to move your files, you can use the file names +themselves as the URL prefix: + + Meteor.AppCache.config({ + onlineOnly: [ + '/bigimage.jpg', + '/largedata.json' + ] + }); + +though keep in mind that since the exclusion is by prefix (this is a +limitation of the application cache manifest), excluding +`/largedata.json` will also exclude such URLs as +`/largedata.json.orig` and `/largedata.json/file1`. + +For more information about how Meteor interacts with the application +cache, see the +[AppCache page](https://github.com/meteor/meteor/wiki/AppCache) +in the Meteor wiki. {{/better_markdown}} diff --git a/packages/appcache/appcache-server.js b/packages/appcache/appcache-server.js index 560763b609..5697d82610 100644 --- a/packages/appcache/appcache-server.js +++ b/packages/appcache/appcache-server.js @@ -141,20 +141,13 @@ }); if (totalSize > 5 * 1024 * 1024) { Meteor._debug( - "** You are publishing " + totalSize + " bytes of assets (including\n" + - "** the contents of the public/ directory) to be stored in the\n" + - "** browser's application cache.\n" + + "** You are using the appcache package but the total size of the\n" + + "** cached resources is " + + (totalSize / 1024 / 1024).toFixed(1) + "MB.\n" + "**\n" + - "** Browsers differ in the amount of data they will store in the app\n" + - "** cache, and if you go over their limit they don't gracefully fallback to\n" + - "** just running the app online (going over their limit breaks the app\n" + - "** online as well as making it not cacheable for offline use).\n" + - "**\n" + - "** To avoid this problem we recommend keeping the size of your static\n" + - "** application assets under 5MB.\n" + - "**\n" + - "** If you have some larger assets that you'd like to make online only,\n" + - "** you can do that with the AppCache \"onlineOnly\" config option." + "** This is over the recommended maximum of 5 MB and may break your\n" + + "** app in some browsers! See http://docs.meteor.com/#appcache\n" + + "** for more information and fixes.\n" ); } }; From b2bf406e55d3ac1f98624ad992b4a18f4c160466 Mon Sep 17 00:00:00 2001 From: Nick Martin Date: Wed, 6 Mar 2013 16:41:56 -0800 Subject: [PATCH 10/11] Only cache assets with a cache-busting parameter. Use fallback to access these when offline. --- packages/appcache/appcache-server.js | 32 ++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/packages/appcache/appcache-server.js b/packages/appcache/appcache-server.js index 5697d82610..a0b5badd4d 100644 --- a/packages/appcache/appcache-server.js +++ b/packages/appcache/appcache-server.js @@ -33,7 +33,7 @@ }); } else { - throw new Error('Invalid AppCache config option: ' + option) + throw new Error('Invalid AppCache config option: ' + option); } }); } @@ -100,13 +100,41 @@ _.each(bundle.manifest, function (resource) { if (resource.where === 'client' && ! Meteor._routePolicy.classify(resource.url)) { - manifest += resource.url + "\n"; + manifest += resource.url; + // If the resource is not already cacheable (has a query + // parameter, presumably with a hash or version of some sort), + // put a version with a hash in the cache. + // + // Avoid putting a non-cacheable asset into the cache, otherwise + // the user can't modify the asset until the cache headers + // expire. + if (!resource.cacheable) + manifest += "?" + resource.hash; + + manifest += "\n"; } }); manifest += "\n"; manifest += "FALLBACK:\n"; manifest += "/ /" + "\n"; + // Add a fallback entry for each uncacheable asset we added above. + // + // This means requests for the bare url (/image.png instead of + // /image.png?hash) will work offline. Online, however, the browser + // will send a request to the server. Users can remove this extra + // request to the server and have the asset served from cache by + // specifying the full URL with hash in their code (manually, with + // some sort of URL rewriting helper) + _.each(bundle.manifest, function (resource) { + if (resource.where === 'client' && + ! Meteor._routePolicy.classify(resource.url) && + !resource.cacheable) { + manifest += resource.url + " " + resource.url + + "?" + resource.hash + "\n"; + } + }); + manifest += "\n"; manifest += "NETWORK:\n"; From 5f7cd81eebb4cdf10d8e3f864ac5ee37464f6578 Mon Sep 17 00:00:00 2001 From: Nick Martin Date: Mon, 11 Mar 2013 22:34:05 -0700 Subject: [PATCH 11/11] Doc tweak and History.md. --- History.md | 4 ++++ docs/client/packages/appcache.html | 3 ++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/History.md b/History.md index a2e7327082..46916c6589 100644 --- a/History.md +++ b/History.md @@ -1,6 +1,10 @@ ## vNEXT +* Add new `appcache` package. Add this package to your project to speed + up page load and make hot code reload smoother using the HTML5 + AppCache API. See http://docs.meteor.com/#appcache for details. + * Publish functions may now return an array of cursors to publish. Currently, the cursors must all be from different collections. #716 diff --git a/docs/client/packages/appcache.html b/docs/client/packages/appcache.html index 00b68561fd..35e94c8902 100644 --- a/docs/client/packages/appcache.html +++ b/docs/client/packages/appcache.html @@ -5,7 +5,8 @@ The `appcache` package stores the static parts of a Meteor application (the client side Javascript, HTML, CSS, and images) in the browser's -[application cache](https://en.wikipedia.org/wiki/AppCache). +[application cache](https://en.wikipedia.org/wiki/AppCache). To enable +caching simply add the `appcache` package to your project. * Once a user has visited a Meteor application for the first time and the application has been cached, on subsequent visits the web page