diff --git a/packages/appcache/appcache-server.js b/packages/appcache/appcache-server.js index c74ac5d9d4..936be30359 100644 --- a/packages/appcache/appcache-server.js +++ b/packages/appcache/appcache-server.js @@ -84,84 +84,95 @@ WebApp.connectHandlers.use(function(req, res, next) { return; } + var manifest = "CACHE MANIFEST\n\n"; + // 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 to ensure that the client updates if the auto update client - // version id changes (which defaults to a hash of the client - // resources), include the version id in the manifest. + // So to ensure that the client updates if client resources change, + // include a hash of client resources in the manifest. - AutoUpdate.withAutoUpdateVersion(function (autoUpdateVersion) { + manifest += "# " + WebApp.clientHash + "\n"; - var manifest = "CACHE MANIFEST\n\n"; - manifest += '# ' + autoUpdateVersion + "\n\n"; + // When using the autoupdate package, also include + // AUTOUPDATE_VERSION. Otherwise the client will get into an + // infinite loop of reloads when the browser doesn't fetch the new + // app HTML which contains the new version, and autoupdate will + // reload again trying to get the new code. - manifest += "CACHE:" + "\n"; - manifest += "/" + "\n"; - _.each(WebApp.clientProgram.manifest, function (resource) { - if (resource.where === 'client' && - ! RoutePolicy.classify(resource.url)) { - 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; + if (Package.autoupdate) { + var version = Package.autoupdate.AutoUpdate.autoUpdateVersion; + if (version !== WebApp.clientHash) + manifest += "# " + version + "\n"; + } - manifest += "\n"; - } - }); - manifest += "\n"; + manifest += "\n"; + + manifest += "CACHE:" + "\n"; + manifest += "/" + "\n"; + _.each(WebApp.clientProgram.manifest, function (resource) { + if (resource.where === 'client' && + ! RoutePolicy.classify(resource.url)) { + 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 += "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(WebApp.clientProgram.manifest, function (resource) { - if (resource.where === 'client' && - ! RoutePolicy.classify(resource.url) && - !resource.cacheable) { - manifest += resource.url + " " + resource.url + - "?" + resource.hash + "\n"; - } - }); - - 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( - [].concat( - RoutePolicy.urlPrefixesFor('network'), - RoutePolicy.urlPrefixesFor('static-online') - ), - 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); + 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(WebApp.clientProgram.manifest, function (resource) { + if (resource.where === 'client' && + ! RoutePolicy.classify(resource.url) && + !resource.cacheable) { + manifest += resource.url + " " + resource.url + + "?" + resource.hash + "\n"; + } + }); + + 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( + [].concat( + RoutePolicy.urlPrefixesFor('network'), + RoutePolicy.urlPrefixesFor('static-online') + ), + 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() { diff --git a/packages/appcache/package.js b/packages/appcache/package.js index 4387e13b43..24fd197964 100644 --- a/packages/appcache/package.js +++ b/packages/appcache/package.js @@ -7,7 +7,7 @@ Package.on_use(function (api) { api.use('reload', 'client'); api.use('routepolicy', 'server'); api.use('underscore', 'server'); - api.use('autoupdate', 'server'); + api.use('autoupdate', 'server', {weak: true}); api.add_files('appcache-client.js', 'client'); api.add_files('appcache-server.js', 'server'); }); diff --git a/packages/autoupdate/QA.md b/packages/autoupdate/QA.md index 32cb1c8203..cd80573647 100644 --- a/packages/autoupdate/QA.md +++ b/packages/autoupdate/QA.md @@ -23,20 +23,8 @@ client code changes) by restarting the server with a new value for `AUTOUPDATE_VERSION`. -## Test with the appcache - -Add the appcache package: - - $ meteor add appcache - -And do the above "Hot Code Push Reload" test again. - - ## No Client Reload on Server-only Change -Undo previous changes made, such as by using `git checkout .` Reload -the client, which will cause the browser to stop using the app cache. - Note that it might look like the browser is reloading because the page content in the leaderboard example will flicker when the server restarts because the example is using autopublish, but that the window @@ -50,8 +38,29 @@ and add `foo.js`. See in the browser console that `a` is still defined, indicating the browser hasn't reloaded. +## Test with the appcache + +Add the appcache package: + + $ meteor add appcache + +And do the above tests again. + +Note that if 1) AUTOUPDATE_VERSION is set so the client doesn't +automatically reload, 2) you make a client change, and 3) you manually +reload the browser page, you usually *won't* see the updated HTML the +*first* time you reload (unless the browser happened to check the app +cache manifest between steps 2 and 3). This is normal browser app +cache behavior: the browser populates the app cache in the background, +so it doesn't wait for new files to download before displaying the web +page. + + ## AutoUpdate.newClientAvailable +Undo previous changes made, such as by using `git checkout .` Reload +the client, which will cause the browser to stop using the app cache. + It's hard to see the `newClientAvailable` reactive variable when the client automatically reloads. Remove the `reload` package so you can see the variable without having the client also reload. diff --git a/packages/autoupdate/autoupdate_server.js b/packages/autoupdate/autoupdate_server.js index 79c270749a..8f2ccfa0dc 100644 --- a/packages/autoupdate/autoupdate_server.js +++ b/packages/autoupdate/autoupdate_server.js @@ -3,73 +3,37 @@ var crypto = Npm.require('crypto'); AutoUpdate = {}; -// Everything that goes into the client code and resources as -// downloaded by the browser. +// The client hash includes __meteor_runtime_config__, so wait until +// all packages have loaded and have had a chance to populate the +// runtime config before using the client hash as our default auto +// update version id. -var calculateClientHash = function () { - var hash = crypto.createHash('sha1'); - hash.update(JSON.stringify(__meteor_runtime_config__), 'utf8'); - _.each(WebApp.clientProgram.manifest, function (resource) { - if (resource.where === 'client' || resource.where === 'internal') { - hash.update(resource.hash); - } - }); - return hash.digest('hex'); -}; +AutoUpdate.autoUpdateVersion = null; +Meteor.startup(function () { + AutoUpdate.autoUpdateVersion = + process.env.AUTOUPDATE_VERSION || + process.env.SERVER_ID || + WebApp.clientHash; -// We need to calculate the autoupdate version after all packages have -// loaded and have had an opportunity to update -// `__meteor_runtime_config__`, so it's possible a subscription might -// get started before we have the version available. - -var autoUpdateVersion = null; - - -// Subscriptions waiting for the autoupdate version to become -// available. - -var callbacks = []; - - -// Calls the callback `cb` when the version is available. - -AutoUpdate.withAutoUpdateVersion = function (cb) { - if (autoUpdateVersion === null) - callbacks.push(cb); - else - cb(autoUpdateVersion); -}; + // also make the autoUpdateVersion available on the client. + __meteor_runtime_config__.autoUpdateVersion = AutoUpdate.autoUpdateVersion; +}); Meteor.publish( "meteor_autoupdate_clientVersions", function () { var self = this; - AutoUpdate.withAutoUpdateVersion(function (autoUpdateVersion) { - self.added( - "meteor_autoupdate_clientVersions", - autoUpdateVersion, - {current: true} - ); - self.ready(); - }); + // Using `autoUpdateVersion` here is safe because we can't get a + // subscription before webapp starts listening, and it doesn't do + // that until the startup hooks have run. + self.added( + "meteor_autoupdate_clientVersions", + AutoUpdate.autoUpdateVersion, + {current: true} + ); + self.ready(); }, {is_auto: true} ); - - -// Wait until all packages have loaded and have had a chance to -// populate __meteor_runtime_config__. - -Meteor.startup(function () { - autoUpdateVersion = - process.env.AUTOUPDATE_VERSION || - process.env.SERVER_ID || - calculateClientHash(); - - __meteor_runtime_config__.autoUpdateVersion = autoUpdateVersion; - - while (callbacks.length > 0) - callbacks.shift()(autoUpdateVersion); -}); diff --git a/packages/webapp/webapp_server.js b/packages/webapp/webapp_server.js index 8225fa3f2b..36f469d74d 100644 --- a/packages/webapp/webapp_server.js +++ b/packages/webapp/webapp_server.js @@ -151,6 +151,50 @@ var appUrl = function (url) { return true; }; + +// Calculate a hash of all the client resources downloaded by the +// browser, including the application HTML, runtime config, code, and +// static files. +// +// This hash *must* change if any resources seen by the browser +// change, and ideally *doesn't* change for any server-only changes +// (but the second is a performance enhancement, not a hard +// requirement). + +var calculateClientHash = function () { + var hash = crypto.createHash('sha1'); + hash.update(JSON.stringify(__meteor_runtime_config__), 'utf8'); + _.each(WebApp.clientProgram.manifest, function (resource) { + if (resource.where === 'client' || resource.where === 'internal') { + hash.update(resource.hash); + } + }); + return hash.digest('hex'); +}; + + +// We need to calculate the client hash after all packages have loaded +// to give them a chance to populate __meteor_runtime_config__. +// +// Calculating the hash during startup means that packages can only +// populate __meteor_runtime_config__ during load, not during startup. +// +// Calculating instead it at the beginning of main after all startup +// hooks had run would allow packages to also populate +// __meteor_runtime_config__ during startup, but that's too late for +// autoupdate because it needs to have the client hash at startup to +// insert the auto update version itself into +// __meteor_runtime_config__ to get it to the client. +// +// An alternative would be to give autoupdate a "post-start, +// pre-listen" hook to allow it to insert the auto update version at +// the right moment. + +Meteor.startup(function () { + WebApp.clientHash = calculateClientHash(); +}); + + var runWebAppServer = function () { var shuttingDown = false; // read the control for the client we'll be serving up