From 7d25bd1f87f2156899ff54695d0863bce40d1bb8 Mon Sep 17 00:00:00 2001 From: Andrew Wilcox Date: Thu, 7 Nov 2013 14:55:00 -0500 Subject: [PATCH 01/19] Move auto reload functionality out of livedata inside of DDP to new autoupdate package implemented on top of DDP. --- packages/autoupdate/.gitignore | 1 + packages/autoupdate/autoupdate_client.js | 19 +++++++ packages/autoupdate/autoupdate_server.js | 69 +++++++++++++++++++++++ packages/autoupdate/package.js | 13 +++++ packages/livedata/livedata_connection.js | 23 ++------ packages/livedata/package.js | 2 +- packages/livedata/stream_client_common.js | 2 +- packages/livedata/stream_client_sockjs.js | 21 +------ packages/livedata/stream_server.js | 15 +---- packages/standard-app-packages/package.js | 9 +++ 10 files changed, 121 insertions(+), 53 deletions(-) create mode 100644 packages/autoupdate/.gitignore create mode 100644 packages/autoupdate/autoupdate_client.js create mode 100644 packages/autoupdate/autoupdate_server.js create mode 100644 packages/autoupdate/package.js diff --git a/packages/autoupdate/.gitignore b/packages/autoupdate/.gitignore new file mode 100644 index 0000000000..677a6fc263 --- /dev/null +++ b/packages/autoupdate/.gitignore @@ -0,0 +1 @@ +.build* diff --git a/packages/autoupdate/autoupdate_client.js b/packages/autoupdate/autoupdate_client.js new file mode 100644 index 0000000000..d88bd641b9 --- /dev/null +++ b/packages/autoupdate/autoupdate_client.js @@ -0,0 +1,19 @@ +var autoUpdateVersion = __meteor_runtime_config__.autoUpdateVersion; + +var ClientVersions = new Meteor.Collection("meteor_autoupdate_clientVersions"); + +Meteor.subscribe("meteor_autoupdate_clientVersions", { + onError: function (error) { + Meteor._debug("autoupdate subscription failed:", error); + }, + onReady: function () { + if (Package.reload) { + Meteor.autorun(function (computation) { + if (! ClientVersions.findOne({_id: autoUpdateVersion})) { + computation.stop(); + Package.reload.Reload._reload(); + } + }); + } + } +}); diff --git a/packages/autoupdate/autoupdate_server.js b/packages/autoupdate/autoupdate_server.js new file mode 100644 index 0000000000..625e56f723 --- /dev/null +++ b/packages/autoupdate/autoupdate_server.js @@ -0,0 +1,69 @@ +var crypto = Npm.require('crypto'); + + +// Everything that goes into the client code and resources as +// downloaded by the browser. + +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 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 when the version is available. + +var withAutoUpdateVersion = function (cb) { + if (autoUpdateVersion === null) + callbacks.push(cb); + else + cb(autoUpdateVersion); +}; + + +Meteor.publish("meteor_autoupdate_clientVersions", function () { + var self = this; + withAutoUpdateVersion(function (autoUpdateVersion) { + self.added( + "meteor_autoupdate_clientVersions", + autoUpdateVersion, + {current: true} + ); + self.ready(); + }); +}); + + +// 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/autoupdate/package.js b/packages/autoupdate/package.js new file mode 100644 index 0000000000..c44a1c65f8 --- /dev/null +++ b/packages/autoupdate/package.js @@ -0,0 +1,13 @@ +Package.describe({ + summary: "Update the client when new client code is available", + internal: true +}); + +Package.on_use(function (api) { + api.use('webapp', 'server'); + api.use(['livedata', 'mongo-livedata'], ['client', 'server']); + api.use('reload', 'client', {weak: true}); + + api.add_files('autoupdate_server.js', 'server'); + api.add_files('autoupdate_client.js', 'client'); +}); diff --git a/packages/livedata/livedata_connection.js b/packages/livedata/livedata_connection.js index 04fb380f90..f71db523bc 100644 --- a/packages/livedata/livedata_connection.js +++ b/packages/livedata/livedata_connection.js @@ -7,14 +7,11 @@ if (Meteor.isServer) { // @param url {String|Object} URL to Meteor app, // or an object as a test hook (see code) // Options: -// reloadOnUpdate: should we try to reload when the server says -// there's new code available? // reloadWithOutstanding: is it OK to reload if there are outstanding methods? var Connection = function (url, options) { var self = this; options = _.extend({ - reloadOnUpdate: false, - // The rest of these options are only for testing. + // These options are only for testing. reloadWithOutstanding: false, supportedDDPVersions: SUPPORTED_DDP_VERSIONS, onConnectionFailure: function (reason) { @@ -162,7 +159,7 @@ var Connection = function (url, options) { // Block auto-reload while we're waiting for method responses. if (Meteor.isClient && Package.reload && !options.reloadWithOutstanding) { - Reload._onMigrate(function (retry) { + Package.reload.Reload._onMigrate(function (retry) { if (!self._readyToMigrate()) { if (self._retryMigrate) throw new Error("Two migrations in progress?"); @@ -277,17 +274,6 @@ var Connection = function (url, options) { self._stream.on('message', onMessage); self._stream.on('reset', onReset); } - - - if (Meteor.isClient && Package.reload && options.reloadOnUpdate) { - self._stream.on('update_available', function () { - // Start trying to migrate to a new version. Until all packages - // signal that they're ready for a migration, the app will - // continue running normally. - Reload._reload(); - }); - } - }; // A MethodInvoker manages sending a method to the server and calling the user's @@ -1392,9 +1378,8 @@ LivedataTest.Connection = Connection; // "/", // "ddp+sockjs://ddp--****-foo.meteor.com/sockjs" // -DDP.connect = function (url, _reloadOnUpdate) { - var ret = new Connection( - url, {reloadOnUpdate: _reloadOnUpdate}); +DDP.connect = function (url) { + var ret = new Connection(url); allConnections.push(ret); // hack. see below. return ret; }; diff --git a/packages/livedata/package.js b/packages/livedata/package.js index 250c116f22..f0f7a7e5b2 100644 --- a/packages/livedata/package.js +++ b/packages/livedata/package.js @@ -31,7 +31,7 @@ Package.on_use(function (api) { api.export('LivedataTest', {testOnly: true}); // Transport - api.use('reload', 'client'); + api.use('reload', 'client', {weak: true}); api.add_files('common.js'); api.add_files(['sockjs-0.3.4.js', 'stream_client_sockjs.js'], 'client'); api.add_files('stream_client_nodejs.js', 'server'); diff --git a/packages/livedata/stream_client_common.js b/packages/livedata/stream_client_common.js index 817eb211d3..270e4ee48f 100644 --- a/packages/livedata/stream_client_common.js +++ b/packages/livedata/stream_client_common.js @@ -77,7 +77,7 @@ _.extend(LivedataTest.ClientStream.prototype, { on: function (name, callback) { var self = this; - if (name !== 'message' && name !== 'reset' && name !== 'update_available') + if (name !== 'message' && name !== 'reset') throw new Error("unknown event type: " + name); if (!self.eventCallbacks[name]) diff --git a/packages/livedata/stream_client_sockjs.js b/packages/livedata/stream_client_sockjs.js index 8605e6275c..06ed1403cd 100644 --- a/packages/livedata/stream_client_sockjs.js +++ b/packages/livedata/stream_client_sockjs.js @@ -20,8 +20,6 @@ LivedataTest.ClientStream = function (url) { self.rawUrl = url; self.socket = null; - self.sent_update_available = false; - self.heartbeatTimer = null; // Listen to global 'online' event if we are running in a browser. @@ -52,6 +50,7 @@ _.extend(LivedataTest.ClientStream.prototype, { self.rawUrl = url; }, + // The welcome_message is deprecated and is ignored. _connected: function (welcome_message) { var self = this; @@ -65,24 +64,6 @@ _.extend(LivedataTest.ClientStream.prototype, { return; } - // inspect the welcome data and decide if we have to reload - try { - var welcome_data = JSON.parse(welcome_message); - } catch (err) { - Meteor._debug("DEBUG: malformed welcome packet", welcome_message); - } - - if (welcome_data && welcome_data.server_id) { - if (__meteor_runtime_config__.serverId && - __meteor_runtime_config__.serverId !== welcome_data.server_id && - !self.sent_update_available) { - self.sent_update_available = true; - _.each(self.eventCallbacks.update_available, - function (callback) { callback(); }); - } - } else - Meteor._debug("DEBUG: invalid welcome packet", welcome_data); - // update status self.currentStatus.status = "connected"; self.currentStatus.connected = true; diff --git a/packages/livedata/stream_server.js b/packages/livedata/stream_server.js index 566dc0574b..32b1eed551 100644 --- a/packages/livedata/stream_server.js +++ b/packages/livedata/stream_server.js @@ -1,12 +1,3 @@ -// unique id for this instantiation of the server. If this changes -// between client reconnects, the client will reload. You can set the -// environment variable "SERVER_ID" to control this. For example, if -// you want to only force a reload on major changes, you can use a -// custom serverId which you only change when something worth pushing -// to clients immediately happens. -__meteor_runtime_config__.serverId = - process.env.SERVER_ID ? process.env.SERVER_ID : Random.id(); - var pathPrefix = __meteor_runtime_config__.ROOT_URL_PATH_PREFIX || ""; StreamServer = function () { @@ -75,9 +66,9 @@ StreamServer = function () { self.open_sockets.push(socket); - // Send a welcome message with the serverId. Client uses this to - // reload if needed. - socket.send(JSON.stringify({server_id: __meteor_runtime_config__.serverId})); + // Send the old style welcome message, which will force old + // clients to reload. + socket.send(JSON.stringify({server_id: "0"})); // call all our callbacks when we get a new socket. they will do the // work of setting up handlers and such for specific messages. diff --git a/packages/standard-app-packages/package.js b/packages/standard-app-packages/package.js index 147674e8e5..5510982ad2 100644 --- a/packages/standard-app-packages/package.js +++ b/packages/standard-app-packages/package.js @@ -42,4 +42,13 @@ Package.on_use(function(api) { // People like being able to clone objects. 'ejson' ]); + + // These are useful too! But you don't have to see their exports + // unless you want to. + api.use([ + // We can reload the client without messing up methods in flight. + 'reload', + // And update automatically when new client code is available! + 'autoupdate' + ], ['client', 'server']); }); From e48fa460bc0aca50b3af7fc4d1b24123c2f08f90 Mon Sep 17 00:00:00 2001 From: Andrew Wilcox Date: Thu, 7 Nov 2013 17:05:07 -0500 Subject: [PATCH 02/19] Implement `AutoUpdate.newClientAvailable` reactive data source. Have the appcache use the auto update client version id. Fix autopublish warning. --- packages/appcache/appcache-server.js | 151 +++++++++++------------ packages/appcache/package.js | 1 + packages/autoupdate/autoupdate_client.js | 13 ++ packages/autoupdate/autoupdate_server.js | 32 +++-- packages/autoupdate/package.js | 1 + 5 files changed, 105 insertions(+), 93 deletions(-) diff --git a/packages/appcache/appcache-server.js b/packages/appcache/appcache-server.js index acd7006012..c74ac5d9d4 100644 --- a/packages/appcache/appcache-server.js +++ b/packages/appcache/appcache-server.js @@ -89,88 +89,79 @@ WebApp.connectHandlers.use(function(req, res, next) { // *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. + // 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. - 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); - } + AutoUpdate.withAutoUpdateVersion(function (autoUpdateVersion) { + + var manifest = "CACHE MANIFEST\n\n"; + manifest += '# ' + autoUpdateVersion + "\n\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 += "\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 digest = hash.digest('hex'); - - var manifest = "CACHE MANIFEST\n\n"; - manifest += '# ' + digest + "\n\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 += "\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 9edc1c5bb8..4387e13b43 100644 --- a/packages/appcache/package.js +++ b/packages/appcache/package.js @@ -7,6 +7,7 @@ Package.on_use(function (api) { api.use('reload', 'client'); api.use('routepolicy', 'server'); api.use('underscore', 'server'); + api.use('autoupdate', 'server'); api.add_files('appcache-client.js', 'client'); api.add_files('appcache-server.js', 'server'); }); diff --git a/packages/autoupdate/autoupdate_client.js b/packages/autoupdate/autoupdate_client.js index d88bd641b9..cf4fe4f41b 100644 --- a/packages/autoupdate/autoupdate_client.js +++ b/packages/autoupdate/autoupdate_client.js @@ -2,6 +2,19 @@ var autoUpdateVersion = __meteor_runtime_config__.autoUpdateVersion; var ClientVersions = new Meteor.Collection("meteor_autoupdate_clientVersions"); + +AutoUpdate = {}; + +AutoUpdate.newClientAvailable = function () { + return !! ClientVersions.findOne( + {$and: [ + {current: true}, + {_id: {$ne: autoUpdateVersion}} + ]} + ); +}; + + Meteor.subscribe("meteor_autoupdate_clientVersions", { onError: function (error) { Meteor._debug("autoupdate subscription failed:", error); diff --git a/packages/autoupdate/autoupdate_server.js b/packages/autoupdate/autoupdate_server.js index 625e56f723..79c270749a 100644 --- a/packages/autoupdate/autoupdate_server.js +++ b/packages/autoupdate/autoupdate_server.js @@ -1,5 +1,7 @@ var crypto = Npm.require('crypto'); +AutoUpdate = {}; + // Everything that goes into the client code and resources as // downloaded by the browser. @@ -30,9 +32,9 @@ var autoUpdateVersion = null; var callbacks = []; -// Calls the callback when the version is available. +// Calls the callback `cb` when the version is available. -var withAutoUpdateVersion = function (cb) { +AutoUpdate.withAutoUpdateVersion = function (cb) { if (autoUpdateVersion === null) callbacks.push(cb); else @@ -40,17 +42,21 @@ var withAutoUpdateVersion = function (cb) { }; -Meteor.publish("meteor_autoupdate_clientVersions", function () { - var self = this; - withAutoUpdateVersion(function (autoUpdateVersion) { - self.added( - "meteor_autoupdate_clientVersions", - autoUpdateVersion, - {current: true} - ); - self.ready(); - }); -}); +Meteor.publish( + "meteor_autoupdate_clientVersions", + function () { + var self = this; + AutoUpdate.withAutoUpdateVersion(function (autoUpdateVersion) { + self.added( + "meteor_autoupdate_clientVersions", + autoUpdateVersion, + {current: true} + ); + self.ready(); + }); + }, + {is_auto: true} +); // Wait until all packages have loaded and have had a chance to diff --git a/packages/autoupdate/package.js b/packages/autoupdate/package.js index c44a1c65f8..a24a312c26 100644 --- a/packages/autoupdate/package.js +++ b/packages/autoupdate/package.js @@ -8,6 +8,7 @@ Package.on_use(function (api) { api.use(['livedata', 'mongo-livedata'], ['client', 'server']); api.use('reload', 'client', {weak: true}); + api.export('AutoUpdate'); api.add_files('autoupdate_server.js', 'server'); api.add_files('autoupdate_client.js', 'client'); }); From a741726dfcd0bda1cbd80b337af6624683a08cac Mon Sep 17 00:00:00 2001 From: Andrew Wilcox Date: Thu, 7 Nov 2013 17:18:17 -0500 Subject: [PATCH 03/19] autoupdate package doesn't need to be internal --- packages/autoupdate/package.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/autoupdate/package.js b/packages/autoupdate/package.js index a24a312c26..c7f04a279c 100644 --- a/packages/autoupdate/package.js +++ b/packages/autoupdate/package.js @@ -1,6 +1,5 @@ Package.describe({ - summary: "Update the client when new client code is available", - internal: true + summary: "Update the client when new client code is available" }); Package.on_use(function (api) { From cbc28e1c7ff70db19a73c162ead567629608f73a Mon Sep 17 00:00:00 2001 From: Andrew Wilcox Date: Fri, 8 Nov 2013 12:55:01 -0500 Subject: [PATCH 04/19] Reload the client on DDP version negotiation failure on the default connection, on the assumption that new client code will be able to negotiate successfully. Uses an exponential backoff if after reload the DDP version negotiation fails again. --- packages/livedata/client_convenience.js | 32 +++++++++++++++++++++++- packages/livedata/livedata_connection.js | 17 +++++++------ packages/livedata/package.js | 2 ++ 3 files changed, 42 insertions(+), 9 deletions(-) diff --git a/packages/livedata/client_convenience.js b/packages/livedata/client_convenience.js index b6992d1ac7..5838ab1e2d 100644 --- a/packages/livedata/client_convenience.js +++ b/packages/livedata/client_convenience.js @@ -11,8 +11,38 @@ if (Meteor.isClient) { if (__meteor_runtime_config__.DDP_DEFAULT_CONNECTION_URL) ddpUrl = __meteor_runtime_config__.DDP_DEFAULT_CONNECTION_URL; } + + var negotiationFailuresKey = "Meteor.DDPVersionNegotiationFailures"; + + var onConnected = function () { + Meteor._localStorage.removeItem(negotiationFailuresKey); + }; + + var exponentialBackoff = function (failures) { + return Math.pow(failures, 1.5) * 1000; + }; + + var onDDPVersionNegotiationFailure = function (serverRequestedVersion) { + if (Package.reload) { + var failures = parseInt(Meteor._localStorage.getItem(negotiationFailuresKey) || "0") + 1; + Meteor._localStorage.setItem(negotiationFailuresKey, "" + failures); + Meteor.setTimeout( + function () { + Package.reload.Reload._reload(); + }, + exponentialBackoff(failures) + ); + } + else { + Meteor._debug("DDP version negotiation failed; server requested version " + serverRequestedVersion); + } + }; + Meteor.connection = - DDP.connect(ddpUrl, true /* restart_on_update */); + DDP.connect(ddpUrl, { + onConnected: onConnected, + onDDPVersionNegotiationFailure: onDDPVersionNegotiationFailure + }); // Proxy the public methods of Meteor.connection so they can // be called directly on Meteor. diff --git a/packages/livedata/livedata_connection.js b/packages/livedata/livedata_connection.js index f71db523bc..fd62c5c5a1 100644 --- a/packages/livedata/livedata_connection.js +++ b/packages/livedata/livedata_connection.js @@ -8,16 +8,17 @@ if (Meteor.isServer) { // or an object as a test hook (see code) // Options: // reloadWithOutstanding: is it OK to reload if there are outstanding methods? +// onDDPNegotiationVersionFailure: callback with the server requested version. var Connection = function (url, options) { var self = this; options = _.extend({ + onConnected: function () {}, + onDDPVersionNegotiationFailure: function (serverRequestedVersion, error) { + Meteor._debug("Failed DDP connection: " + error); + }, // These options are only for testing. reloadWithOutstanding: false, - supportedDDPVersions: SUPPORTED_DDP_VERSIONS, - onConnectionFailure: function (reason) { - Meteor._debug("Failed DDP connection: " + reason); - }, - onConnected: function () {} + supportedDDPVersions: SUPPORTED_DDP_VERSIONS }, options); // If set, called when we reconnect, queuing method calls _before_ the @@ -197,7 +198,7 @@ var Connection = function (url, options) { var error = "Version negotiation failed; server requested version " + msg.version; self._stream.disconnect({_permanent: true, _error: error}); - options.onConnectionFailure(error); + options.onDDPVersionNegotiationFailure(msg.version, error); } } else if (_.include(['added', 'changed', 'removed', 'ready', 'updated'], msg.msg)) @@ -1378,8 +1379,8 @@ LivedataTest.Connection = Connection; // "/", // "ddp+sockjs://ddp--****-foo.meteor.com/sockjs" // -DDP.connect = function (url) { - var ret = new Connection(url); +DDP.connect = function (url, options) { + var ret = new Connection(url, options); allConnections.push(ret); // hack. see below. return ret; }; diff --git a/packages/livedata/package.js b/packages/livedata/package.js index f0f7a7e5b2..d8c01f2cab 100644 --- a/packages/livedata/package.js +++ b/packages/livedata/package.js @@ -25,6 +25,8 @@ Package.on_use(function (api) { // If the facts package is loaded, publish some statistics. api.use('facts', 'server', {weak: true}); + api.use('localstorage', 'client'); + api.export('DDP'); api.export('DDPServer', 'server'); From d8935bdbcb00f1b403293a3087d6af8648ffecd4 Mon Sep 17 00:00:00 2001 From: Andrew Wilcox Date: Fri, 8 Nov 2013 13:47:36 -0500 Subject: [PATCH 05/19] Update description of deprecated DDP welcome message and include QA notes. --- packages/autoupdate/QA.md | 85 +++++++++++++++++++++++++++++++++++++++ packages/livedata/DDP.md | 10 ++--- 2 files changed, 90 insertions(+), 5 deletions(-) create mode 100644 packages/autoupdate/QA.md diff --git a/packages/autoupdate/QA.md b/packages/autoupdate/QA.md new file mode 100644 index 0000000000..73a4043327 --- /dev/null +++ b/packages/autoupdate/QA.md @@ -0,0 +1,85 @@ +# QA Notes + +## Hot Code Push Reload + +Run the leaderboard example, and click on one of the names. Make a +change to the leaderboard.html file, see the client reload, and see +that the name is still selected. + + +## 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 +won't actually be reloading. + +In the browser console, assign a variable such as `a = true` so that +you can easily verify that the client hasn't reloaded. + +In the leaderboard example directory, create the `server` directory +and add `foo.js`. See in the browser console that `a` is still +defined, indicating the browser hasn't reloaded. + + +## AutoUpdate.newClientAvailable + +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. + + $ meteor remove standard-app-packages + $ meteor add meteor webapp logging deps session livedata + $ meteor add mongo-livedata templating handlebars check underscore + $ meteor add jquery random ejson autoupdate + +Add to leaderboard.js: + + Template.leaderboard.available = AutoUpdate.newClientAvailable; + +And add {{available}} to the leaderboard template in eaderboard.html. + +Initially you'll see `false`, and then when you make a change to the +leaderboard HTML you'll see the variable change to `true`. (You won't +see the new HTML on the client because you disabled reload). + +Amusingly, you can undo the addition you made to the HTML and the "new +client available" variable will go back to `false` (you now don't now +have client code on the server different than what's running in the +browser), because by default the client version is based on a hash of +the client files. + + +## DDP Version Negotiation Failure + +A quick way to test DDP version negotiation failure is to force the +client to use the wrong DDP version. At the top of +livedata_connection.js: + + var Connection = function (url, options) { + var self = this; + + options.supportedDDPVersions = ['abc']; + +You will see the client reload (in the hope that new client code will +be available that can successfully negotiation the DDP version). Each +reload takes longer than the one before, using an exponential backoff. + +If you remove the `options.supportedDDPVersions` line and allow the +client to connect (or manually reload the browser page so you don't +have to wait), this will reset the exponential backoff counter. + +You can verify the counter was reset by adding the line back in a +second time, and you'll see the reload cycle start over again with +first reloading quickly, and then again taking longer between tries. diff --git a/packages/livedata/DDP.md b/packages/livedata/DDP.md index 8f88889a5d..655f1ca105 100644 --- a/packages/livedata/DDP.md +++ b/packages/livedata/DDP.md @@ -37,11 +37,11 @@ depending on message type. ### Procedure: -The server may send an initial message which is a JSON object lacking a `msg` -key. If so, the client should ignore it. The client does not have to wait for -this message. (This message is used to help implement hot code reload over our -SockJS transport. It is currently sent over websockets as well, but probably -should not be.) +The server may send an initial message which is a JSON object lacking +a `msg` key. If so, the client should ignore it. The client does not +have to wait for this message. (The message was once used to help +implement Meteor's hot code reload feature; it is now only included to +force old clients to update). * The client sends a `connect` message. * If the server is willing to speak the `version` of the protocol specified in From 572fc333c7251f0ff1be2668f9ba80c31abe8936 Mon Sep 17 00:00:00 2001 From: Andrew Wilcox Date: Fri, 8 Nov 2013 15:46:21 -0500 Subject: [PATCH 06/19] Avoid having test-in-browser shutdown the autoupdate subscription, so that developers still get hot code pushes when writing tests. Fix livedata tests. Add a sanity check to deciding when to reload: only reload if there is an available client version marked "current". (Otherwise if the server fails to publish any valid client versions, perhaps as a bug in a forked autopublish package, the client will go into an infinite loop of reloads). Update QA notes. --- packages/autoupdate/QA.md | 27 +++++++++++++++---- packages/autoupdate/autoupdate_client.js | 3 ++- packages/livedata/livedata_connection.js | 8 ++++-- .../livedata/livedata_connection_tests.js | 4 +-- 4 files changed, 32 insertions(+), 10 deletions(-) diff --git a/packages/autoupdate/QA.md b/packages/autoupdate/QA.md index 73a4043327..32cb1c8203 100644 --- a/packages/autoupdate/QA.md +++ b/packages/autoupdate/QA.md @@ -7,6 +7,22 @@ change to the leaderboard.html file, see the client reload, and see that the name is still selected. +## AUTOUPDATE_VERSION + +Set the `AUTOUPDATE_VERSION` environment variable when running the +application: + + $ AUTOUPDATE_VERSION=abc meteor + +Now when you make an HTML change, it won't appear in the client +automatically. (Note the leader list flickers when the server +subscription restarts, but that's not a window reload). + +Conversely, you can force a client reload (even without making any +client code changes) by restarting the server with a new value for +`AUTOUPDATE_VERSION`. + + ## Test with the appcache Add the appcache package: @@ -49,17 +65,18 @@ Add to leaderboard.js: Template.leaderboard.available = AutoUpdate.newClientAvailable; -And add {{available}} to the leaderboard template in eaderboard.html. +And add `{{available}}` to the leaderboard template in +leaderboard.html. Initially you'll see `false`, and then when you make a change to the leaderboard HTML you'll see the variable change to `true`. (You won't see the new HTML on the client because you disabled reload). Amusingly, you can undo the addition you made to the HTML and the "new -client available" variable will go back to `false` (you now don't now -have client code on the server different than what's running in the -browser), because by default the client version is based on a hash of -the client files. +client available" variable will go back to `false` (you now don't have +client code available on the server different than what's running in +the browser), because by default the client version is based on a hash +of the client files. ## DDP Version Negotiation Failure diff --git a/packages/autoupdate/autoupdate_client.js b/packages/autoupdate/autoupdate_client.js index cf4fe4f41b..f079689e60 100644 --- a/packages/autoupdate/autoupdate_client.js +++ b/packages/autoupdate/autoupdate_client.js @@ -22,7 +22,8 @@ Meteor.subscribe("meteor_autoupdate_clientVersions", { onReady: function () { if (Package.reload) { Meteor.autorun(function (computation) { - if (! ClientVersions.findOne({_id: autoUpdateVersion})) { + if (ClientVersions.findOne({current: true}) && + (! ClientVersions.findOne({_id: autoUpdateVersion}))) { computation.stop(); Package.reload.Reload._reload(); } diff --git a/packages/livedata/livedata_connection.js b/packages/livedata/livedata_connection.js index fd62c5c5a1..0016355b76 100644 --- a/packages/livedata/livedata_connection.js +++ b/packages/livedata/livedata_connection.js @@ -787,8 +787,12 @@ _.extend(Connection.prototype, { _unsubscribeAll: function () { var self = this; _.each(_.clone(self._subscriptions), function (sub, id) { - self._send({msg: 'unsub', id: id}); - delete self._subscriptions[id]; + // Avoid killing the autoupdate subscription so that developers + // still get hot code pushes when writing tests. + if (sub.name !== 'meteor_autoupdate_clientVersions') { + self._send({msg: 'unsub', id: id}); + delete self._subscriptions[id]; + } }); }, diff --git a/packages/livedata/livedata_connection_tests.js b/packages/livedata/livedata_connection_tests.js index 119a78792d..c977360802 100644 --- a/packages/livedata/livedata_connection_tests.js +++ b/packages/livedata/livedata_connection_tests.js @@ -1335,7 +1335,7 @@ Tinytest.addAsync("livedata connection - version negotiation requires renegotiat var connection = new LivedataTest.Connection(getSelfConnectionUrl(), { reloadWithOutstanding: true, supportedDDPVersions: ["garbled", LivedataTest.SUPPORTED_DDP_VERSIONS[0]], - onConnectionFailure: function () { test.fail(); onComplete(); }, + onDDPVersionNegotiationFailure: function () { test.fail(); onComplete(); }, onConnected: function () { test.equal(connection._version, LivedataTest.SUPPORTED_DDP_VERSIONS[0]); connection._stream.disconnect({_permanent: true}); @@ -1349,7 +1349,7 @@ Tinytest.addAsync("livedata connection - version negotiation error", var connection = new LivedataTest.Connection(getSelfConnectionUrl(), { reloadWithOutstanding: true, supportedDDPVersions: ["garbled", "more garbled"], - onConnectionFailure: function () { + onDDPVersionNegotiationFailure: function () { test.equal(connection.status().status, "failed"); test.matches(connection.status().reason, /Version negotiation failed/); test.isFalse(connection.status().connected); From e27e2d8c828257a2dbe39b4b8bcd8a8e32eaf2bd Mon Sep 17 00:00:00 2001 From: Andrew Wilcox Date: Mon, 11 Nov 2013 16:50:59 -0500 Subject: [PATCH 07/19] Calculate the client hash in webapp so that autoupdate doesn't have to be a strong dependency of appcache. Delaying publishing the current client version document until the auto update version is available isn't necessary because the publish callback won't be called before webapp starts listening, and that happens after startup. In the app cache manifest include both the client hash and the AUTOUPDATE_VERSION, which both allows the browser to load new client code even when the auto update version is explicitly set by the developer, and also ensures that if the developer changes the auto update version this is propagated to the client in the app HTML so that autoupdate doesn't get into an infinite loop of reloads. --- packages/appcache/appcache-server.js | 145 ++++++++++++----------- packages/appcache/package.js | 2 +- packages/autoupdate/QA.md | 33 ++++-- packages/autoupdate/autoupdate_server.js | 80 ++++--------- packages/webapp/webapp_server.js | 44 +++++++ 5 files changed, 166 insertions(+), 138 deletions(-) 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 From 456e6916be799c004590ef710748b6125f9ebcb5 Mon Sep 17 00:00:00 2001 From: Andrew Wilcox Date: Mon, 11 Nov 2013 18:03:55 -0500 Subject: [PATCH 08/19] Use reload migration API instead of localstorage. Remove the unused `serverRequestedVersion` argument to `onDDPVersionNegotiationFailure`. And let's call the remaining argument `description` instead of `error` since it isn't an Error object. --- packages/livedata/client_convenience.js | 21 +++++++------------ packages/livedata/livedata_connection.js | 14 ++++++------- .../livedata/livedata_connection_tests.js | 2 +- 3 files changed, 16 insertions(+), 21 deletions(-) diff --git a/packages/livedata/client_convenience.js b/packages/livedata/client_convenience.js index 5838ab1e2d..9847618fbe 100644 --- a/packages/livedata/client_convenience.js +++ b/packages/livedata/client_convenience.js @@ -12,20 +12,19 @@ if (Meteor.isClient) { ddpUrl = __meteor_runtime_config__.DDP_DEFAULT_CONNECTION_URL; } - var negotiationFailuresKey = "Meteor.DDPVersionNegotiationFailures"; - - var onConnected = function () { - Meteor._localStorage.removeItem(negotiationFailuresKey); - }; - var exponentialBackoff = function (failures) { return Math.pow(failures, 1.5) * 1000; }; - var onDDPVersionNegotiationFailure = function (serverRequestedVersion) { + var onDDPVersionNegotiationFailure = function (description) { + Meteor._debug(description); if (Package.reload) { - var failures = parseInt(Meteor._localStorage.getItem(negotiationFailuresKey) || "0") + 1; - Meteor._localStorage.setItem(negotiationFailuresKey, "" + failures); + var migrationData = Package.reload.Reload._migrationData('livedata') || {}; + var failures = migrationData.DDPVersionNegotiationFailures || 0; + ++failures; + Package.reload.Reload._onMigrate('livedata', function () { + return [true, {DDPVersionNegotiationFailures: failures}]; + }); Meteor.setTimeout( function () { Package.reload.Reload._reload(); @@ -33,14 +32,10 @@ if (Meteor.isClient) { exponentialBackoff(failures) ); } - else { - Meteor._debug("DDP version negotiation failed; server requested version " + serverRequestedVersion); - } }; Meteor.connection = DDP.connect(ddpUrl, { - onConnected: onConnected, onDDPVersionNegotiationFailure: onDDPVersionNegotiationFailure }); diff --git a/packages/livedata/livedata_connection.js b/packages/livedata/livedata_connection.js index 0016355b76..f63b05ee9f 100644 --- a/packages/livedata/livedata_connection.js +++ b/packages/livedata/livedata_connection.js @@ -8,13 +8,13 @@ if (Meteor.isServer) { // or an object as a test hook (see code) // Options: // reloadWithOutstanding: is it OK to reload if there are outstanding methods? -// onDDPNegotiationVersionFailure: callback with the server requested version. +// onDDPNegotiationVersionFailure: callback when version negotiation fails. var Connection = function (url, options) { var self = this; options = _.extend({ onConnected: function () {}, - onDDPVersionNegotiationFailure: function (serverRequestedVersion, error) { - Meteor._debug("Failed DDP connection: " + error); + onDDPVersionNegotiationFailure: function (description) { + Meteor._debug(description); }, // These options are only for testing. reloadWithOutstanding: false, @@ -195,10 +195,10 @@ var Connection = function (url, options) { self._versionSuggestion = msg.version; self._stream.reconnect({_force: true}); } else { - var error = - "Version negotiation failed; server requested version " + msg.version; - self._stream.disconnect({_permanent: true, _error: error}); - options.onDDPVersionNegotiationFailure(msg.version, error); + var description = + "DDP version negotiation failed; server requested version " + msg.version; + self._stream.disconnect({_permanent: true, _error: description}); + options.onDDPVersionNegotiationFailure(description); } } else if (_.include(['added', 'changed', 'removed', 'ready', 'updated'], msg.msg)) diff --git a/packages/livedata/livedata_connection_tests.js b/packages/livedata/livedata_connection_tests.js index c977360802..b37e3d97a7 100644 --- a/packages/livedata/livedata_connection_tests.js +++ b/packages/livedata/livedata_connection_tests.js @@ -1351,7 +1351,7 @@ Tinytest.addAsync("livedata connection - version negotiation error", supportedDDPVersions: ["garbled", "more garbled"], onDDPVersionNegotiationFailure: function () { test.equal(connection.status().status, "failed"); - test.matches(connection.status().reason, /Version negotiation failed/); + test.matches(connection.status().reason, /DDP version negotiation failed/); test.isFalse(connection.status().connected); onComplete(); }, From 6239d388efa3dc700093df631f3dbea9dd583894 Mon Sep 17 00:00:00 2001 From: Andrew Wilcox Date: Tue, 12 Nov 2013 10:59:37 -0500 Subject: [PATCH 09/19] Reuse stream reconnect exponential backoff code for DDP version negotiation failure retries. --- packages/livedata/client_convenience.js | 13 ++--- packages/livedata/package.js | 1 + packages/livedata/retry.js | 65 +++++++++++++++++++++++ packages/livedata/stream_client_common.js | 56 +++---------------- 4 files changed, 77 insertions(+), 58 deletions(-) create mode 100644 packages/livedata/retry.js diff --git a/packages/livedata/client_convenience.js b/packages/livedata/client_convenience.js index 9847618fbe..a95fbc17dd 100644 --- a/packages/livedata/client_convenience.js +++ b/packages/livedata/client_convenience.js @@ -12,9 +12,7 @@ if (Meteor.isClient) { ddpUrl = __meteor_runtime_config__.DDP_DEFAULT_CONNECTION_URL; } - var exponentialBackoff = function (failures) { - return Math.pow(failures, 1.5) * 1000; - }; + var retry = new Retry(); var onDDPVersionNegotiationFailure = function (description) { Meteor._debug(description); @@ -25,12 +23,9 @@ if (Meteor.isClient) { Package.reload.Reload._onMigrate('livedata', function () { return [true, {DDPVersionNegotiationFailures: failures}]; }); - Meteor.setTimeout( - function () { - Package.reload.Reload._reload(); - }, - exponentialBackoff(failures) - ); + retry.retryLater(failures, function () { + Package.reload.Reload._reload(); + }); } }; diff --git a/packages/livedata/package.js b/packages/livedata/package.js index d8c01f2cab..cec74864bb 100644 --- a/packages/livedata/package.js +++ b/packages/livedata/package.js @@ -35,6 +35,7 @@ Package.on_use(function (api) { // Transport api.use('reload', 'client', {weak: true}); api.add_files('common.js'); + api.add_files('retry.js', 'client'); api.add_files(['sockjs-0.3.4.js', 'stream_client_sockjs.js'], 'client'); api.add_files('stream_client_nodejs.js', 'server'); api.add_files('stream_client_common.js', ['client', 'server']); diff --git a/packages/livedata/retry.js b/packages/livedata/retry.js new file mode 100644 index 0000000000..ac155c19e0 --- /dev/null +++ b/packages/livedata/retry.js @@ -0,0 +1,65 @@ +// Retry logic with an exponential backoff. + +Retry = function (options) { + var self = this; + _.extend(self, _.defaults(_.clone(options || {}), { + // time for initial reconnect attempt. + baseTimeout: 1000, + // exponential factor to increase timeout each attempt. + exponent: 2.2, + // maximum time between reconnects. keep this intentionally + // high-ish to ensure a server can recover from a failure caused + // by load + maxTimeout: 5 * 60000, // 5 minutes + // time to wait for the first 2 retries. this helps page reload + // speed during dev mode restarts, but doesn't hurt prod too + // much (due to CONNECT_TIMEOUT) + minTimeout: 10, + // how many times to try to reconnect 'instantly' + minCount: 2, + // fuzz factor to randomize reconnect times by. avoid reconnect + // storms. + fuzz: 0.5 // +- 25% + })); + self.retryTimer = null; +}; + +_.extend(Retry.prototype, { + + // Reset a pending retry, if any. + clear: function () { + var self = this; + if (self.retryTimer) + clearTimeout(self.retryTimer); + self.retryTimer = null; + }, + + // Calculate how long to wait in milliseconds to retry, based on the + // `count` of which retry this is. + _timeout: function (count) { + var self = this; + + if (count < self.minCount) + return self.minTimeout; + + var timeout = Math.min( + self.maxTimeout, + self.baseTimeout * Math.pow(self.exponent, count)); + // fuzz the timeout randomly, to avoid reconnect storms when a + // server goes down. + timeout = timeout * ((Random.fraction() * self.fuzz) + + (1 - self.fuzz/2)); + return timeout; + }, + + // Call `fn` after a delay, based on the `count` of which retry this is. + retryLater: function (count, fn) { + var self = this; + var timeout = self._timeout(count); + if (self.retryTimer) + clearTimeout(self.retryTimer); + self.retryTimer = setTimeout(fn, timeout); + return timeout; + } + +}); diff --git a/packages/livedata/stream_client_common.js b/packages/livedata/stream_client_common.js index 270e4ee48f..47f003cc62 100644 --- a/packages/livedata/stream_client_common.js +++ b/packages/livedata/stream_client_common.js @@ -94,27 +94,6 @@ _.extend(LivedataTest.ClientStream.prototype, { // failed. self.CONNECT_TIMEOUT = 10000; - - // time for initial reconnect attempt. - self.RETRY_BASE_TIMEOUT = 1000; - // exponential factor to increase timeout each attempt. - self.RETRY_EXPONENT = 2.2; - // maximum time between reconnects. keep this intentionally - // high-ish to ensure a server can recover from a failure caused - // by load - self.RETRY_MAX_TIMEOUT = 5 * 60000; // 5 minutes - // time to wait for the first 2 retries. this helps page reload - // speed during dev mode restarts, but doesn't hurt prod too - // much (due to CONNECT_TIMEOUT) - self.RETRY_MIN_TIMEOUT = 10; - // how many times to try to reconnect 'instantly' - self.RETRY_MIN_COUNT = 2; - // fuzz factor to randomize reconnect times by. avoid reconnect - // storms. - self.RETRY_FUZZ = 0.5; // +- 25% - - - self.eventCallbacks = {}; // name -> [callback] self._forcedToDisconnect = false; @@ -134,7 +113,7 @@ _.extend(LivedataTest.ClientStream.prototype, { }; //// Retry logic - self.retryTimer = null; + self._retry = new Retry; self.connectionTimer = null; }, @@ -161,9 +140,7 @@ _.extend(LivedataTest.ClientStream.prototype, { self._lostConnection(); } - if (self.retryTimer) - clearTimeout(self.retryTimer); - self.retryTimer = null; + self._retry.clear(); self.currentStatus.retryCount -= 1; // don't count manual retries self._retryNow(); }, @@ -186,10 +163,7 @@ _.extend(LivedataTest.ClientStream.prototype, { } self._cleanup(); - if (self.retryTimer) { - clearTimeout(self.retryTimer); - self.retryTimer = null; - } + self._retry.clear(); self.currentStatus = { status: (options._permanent ? "failed" : "offline"), @@ -210,22 +184,6 @@ _.extend(LivedataTest.ClientStream.prototype, { self._retryLater(); // sets status. no need to do it here. }, - _retryTimeout: function (count) { - var self = this; - - if (count < self.RETRY_MIN_COUNT) - return self.RETRY_MIN_TIMEOUT; - - var timeout = Math.min( - self.RETRY_MAX_TIMEOUT, - self.RETRY_BASE_TIMEOUT * Math.pow(self.RETRY_EXPONENT, count)); - // fuzz the timeout randomly, to avoid reconnect storms when a - // server goes down. - timeout = timeout * ((Random.fraction() * self.RETRY_FUZZ) + - (1 - self.RETRY_FUZZ/2)); - return timeout; - }, - // fired when we detect that we've gone online. try to reconnect // immediately. _online: function () { @@ -237,10 +195,10 @@ _.extend(LivedataTest.ClientStream.prototype, { _retryLater: function () { var self = this; - var timeout = self._retryTimeout(self.currentStatus.retryCount); - if (self.retryTimer) - clearTimeout(self.retryTimer); - self.retryTimer = setTimeout(_.bind(self._retryNow, self), timeout); + var timeout = self._retry.retryLater( + self.currentStatus.retryCount, + _.bind(self._retryNow, self) + ); self.currentStatus.status = "waiting"; self.currentStatus.connected = false; From 78b9a47fcb75a7662b1012524b62de950a8052bc Mon Sep 17 00:00:00 2001 From: Andrew Wilcox Date: Tue, 12 Nov 2013 11:28:14 -0500 Subject: [PATCH 10/19] Fix livedata tests. --- packages/livedata/package.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/livedata/package.js b/packages/livedata/package.js index cec74864bb..3953edc3f8 100644 --- a/packages/livedata/package.js +++ b/packages/livedata/package.js @@ -35,7 +35,7 @@ Package.on_use(function (api) { // Transport api.use('reload', 'client', {weak: true}); api.add_files('common.js'); - api.add_files('retry.js', 'client'); + api.add_files('retry.js', ['client', 'server']); api.add_files(['sockjs-0.3.4.js', 'stream_client_sockjs.js'], 'client'); api.add_files('stream_client_nodejs.js', 'server'); api.add_files('stream_client_common.js', ['client', 'server']); From 8c0e02635b7be2119e098360d20ac5bf474d92ec Mon Sep 17 00:00:00 2001 From: Andrew Wilcox Date: Tue, 12 Nov 2013 15:15:09 -0500 Subject: [PATCH 11/19] Add an overview of autoupdate and the code push process. --- packages/autoupdate/autoupdate_client.js | 28 ++++++++++++++++++ packages/autoupdate/autoupdate_server.js | 36 +++++++++++++++++++++++- 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/packages/autoupdate/autoupdate_client.js b/packages/autoupdate/autoupdate_client.js index f079689e60..d81ac91d30 100644 --- a/packages/autoupdate/autoupdate_client.js +++ b/packages/autoupdate/autoupdate_client.js @@ -1,5 +1,33 @@ +// Subscribe to the `meteor_autoupdate_clientVersions` collection, +// which contains the set of acceptable client versions. +// +// A "hard code push" occurs when the current client version is not in +// the set of acceptable client versions (or the server updates the +// collection, and the current client version is no longer in the +// set). +// +// When the `reload` package is loaded, a hard code push causes +// the browser to reload, so that it will load the latest client +// version from the server. +// +// A "soft code push" represents the situation when the current client +// version is in the set of acceptable versions, but there is a newer +// version available on the server. +// +// `AutoUpdate.newClientAvailable` is a reactive data source which +// becomes `true` if there is a new version of the client is available on +// the server. +// +// This package doesn't implement a soft code reload process itself, +// but `newClientAvailable` could be used for example to display a +// "click to reload" link to the user. + +// The client version of the client code currently running in the +// browser. var autoUpdateVersion = __meteor_runtime_config__.autoUpdateVersion; + +// The collection of acceptable client versions. var ClientVersions = new Meteor.Collection("meteor_autoupdate_clientVersions"); diff --git a/packages/autoupdate/autoupdate_server.js b/packages/autoupdate/autoupdate_server.js index 8f2ccfa0dc..2359cf28cb 100644 --- a/packages/autoupdate/autoupdate_server.js +++ b/packages/autoupdate/autoupdate_server.js @@ -1,8 +1,41 @@ +// Publish the current client version to the client. When a client +// sees the subscription change and that there is a new version of the +// client available on the server, it can reload. +// +// By default the current client version is identified by a hash of +// the client resources seen by the browser (the HTML, CSS, code, and +// static files in the `public` directory). +// +// If the environment variable `AUTOUPDATE_VERSION` is set it will be +// used as the client id instead. You can use this to control when +// the client reloads. For example, if you want to only force a +// reload on major changes, you can use a custom AUTOUPDATE_VERSION +// which you only change when something worth pushing to clients +// immediately happens. +// +// The server publishes a `meteor_autoupdate_clientVersions` +// collection. The contract of this collection is that each document +// in the collection represents an acceptable client version, with the +// `_id` field of the document set to the client id. +// +// An "unacceptable" client version, for example, might be a version +// of the client code which has a severe UI bug, or is incompatible +// with the server. An "acceptable" client version could be one that +// is older than the latest client code available on the server but +// still works. +// +// One of the published documents in the collection will have its +// `current` field set to `true`. This is the version of the client +// code that the browser will receive from the server if it reloads. +// +// In this implementation only one such document is published, the +// current client version. Developers can easily experiment with +// different versioning and updating models by forking this package. + var crypto = Npm.require('crypto'); AutoUpdate = {}; - // 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 @@ -13,6 +46,7 @@ AutoUpdate.autoUpdateVersion = null; Meteor.startup(function () { AutoUpdate.autoUpdateVersion = process.env.AUTOUPDATE_VERSION || + // also accept SERVER_ID for backwards compatibility. process.env.SERVER_ID || WebApp.clientHash; From 8d3929e450c686fe176074491a588935913c1d91 Mon Sep 17 00:00:00 2001 From: Andrew Wilcox Date: Tue, 12 Nov 2013 15:23:40 -0500 Subject: [PATCH 12/19] Fix typo. --- packages/autoupdate/autoupdate_server.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/autoupdate/autoupdate_server.js b/packages/autoupdate/autoupdate_server.js index 2359cf28cb..9cc3522c1a 100644 --- a/packages/autoupdate/autoupdate_server.js +++ b/packages/autoupdate/autoupdate_server.js @@ -28,9 +28,9 @@ // `current` field set to `true`. This is the version of the client // code that the browser will receive from the server if it reloads. // -// In this implementation only one such document is published, the -// current client version. Developers can easily experiment with -// different versioning and updating models by forking this package. +// In this implementation only one document is published, the current +// client version. Developers can easily experiment with different +// versioning and updating models by forking this package. var crypto = Npm.require('crypto'); From 15f5ca3d7e17510db991547cdc02b6bd8881e7d8 Mon Sep 17 00:00:00 2001 From: Andrew Wilcox Date: Wed, 13 Nov 2013 15:57:30 -0500 Subject: [PATCH 13/19] Have the client send the "connect" message on stream open instead of waiting for the welcome message. --- packages/livedata/livedata_connection.js | 4 +++- packages/livedata/stream_client_nodejs.js | 10 ---------- packages/livedata/stream_client_sockjs.js | 13 +++++-------- 3 files changed, 8 insertions(+), 19 deletions(-) diff --git a/packages/livedata/livedata_connection.js b/packages/livedata/livedata_connection.js index f63b05ee9f..4a7b20859e 100644 --- a/packages/livedata/livedata_connection.js +++ b/packages/livedata/livedata_connection.js @@ -181,7 +181,9 @@ var Connection = function (url, options) { } if (msg === null || !msg.msg) { - Meteor._debug("discarding invalid livedata message", msg); + // ignore the old welcome message + if (! (msg && msg.server_id)) + Meteor._debug("discarding invalid livedata message", msg); return; } diff --git a/packages/livedata/stream_client_nodejs.js b/packages/livedata/stream_client_nodejs.js index 0b79754aa3..1a833c0e34 100644 --- a/packages/livedata/stream_client_nodejs.js +++ b/packages/livedata/stream_client_nodejs.js @@ -42,7 +42,6 @@ LivedataTest.ClientStream = function (endpoint) { self._initCommon(); - self.expectingWelcome = false; //// Kickoff! self._launchConnection(); }; @@ -106,19 +105,10 @@ _.extend(LivedataTest.ClientStream.prototype, { self._lostConnection(); }); - self.expectingWelcome = true; connection.on('message', function (message) { if (self.currentConnection !== this) return; // old connection still emitting messages - if (self.expectingWelcome) { - // Discard the first message that comes across the - // connection. It is the hot code push version identifier and - // is not actually part of DDP. - self.expectingWelcome = false; - return; - } - if (message.type === "utf8") // ignore binary frames _.each(self.eventCallbacks.message, function (callback) { callback(message.utf8Data); diff --git a/packages/livedata/stream_client_sockjs.js b/packages/livedata/stream_client_sockjs.js index 06ed1403cd..1edf9ea7d3 100644 --- a/packages/livedata/stream_client_sockjs.js +++ b/packages/livedata/stream_client_sockjs.js @@ -50,8 +50,7 @@ _.extend(LivedataTest.ClientStream.prototype, { self.rawUrl = url; }, - // The welcome_message is deprecated and is ignored. - _connected: function (welcome_message) { + _connected: function () { var self = this; if (self.connectionTimer) { @@ -152,15 +151,13 @@ _.extend(LivedataTest.ClientStream.prototype, { toSockjsUrl(self.rawUrl), undefined, { debug: false, protocols_whitelist: self._sockjsProtocolsWhitelist() }); + self.socket.onopen = function (data) { + self._connected(); + }; self.socket.onmessage = function (data) { self._heartbeat_received(); - // first message we get when we're connecting goes to _connected, - // which connects us. All subsequent messages (while connected) go to - // the callback. - if (self.currentStatus.status === "connecting") - self._connected(data.data); - else if (self.currentStatus.connected) + if (self.currentStatus.connected) _.each(self.eventCallbacks.message, function (callback) { callback(data.data); }); From a2c4a787430617387c83e87f13a3498acef87b45 Mon Sep 17 00:00:00 2001 From: Andrew Wilcox Date: Wed, 13 Nov 2013 16:32:51 -0500 Subject: [PATCH 14/19] Have using test-in-browser cause autoupdate to reload on server changes, which is convenient when running unit tests in the browser. --- packages/autoupdate/QA.md | 3 +++ packages/autoupdate/autoupdate_server.js | 15 +++++++++------ packages/livedata/stream_server.js | 1 - packages/test-in-browser/autoupdate.js | 7 +++++++ packages/test-in-browser/package.js | 4 ++++ 5 files changed, 23 insertions(+), 7 deletions(-) create mode 100644 packages/test-in-browser/autoupdate.js diff --git a/packages/autoupdate/QA.md b/packages/autoupdate/QA.md index cd80573647..189353ec32 100644 --- a/packages/autoupdate/QA.md +++ b/packages/autoupdate/QA.md @@ -25,6 +25,9 @@ client code changes) by restarting the server with a new value for ## No Client Reload on Server-only Change +Revert previous changes and run the example without setting +AUTOUPDATE_VERSION. + 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 diff --git a/packages/autoupdate/autoupdate_server.js b/packages/autoupdate/autoupdate_server.js index 9cc3522c1a..bd337fb615 100644 --- a/packages/autoupdate/autoupdate_server.js +++ b/packages/autoupdate/autoupdate_server.js @@ -13,6 +13,9 @@ // which you only change when something worth pushing to clients // immediately happens. // +// For backwards compatibility, SERVER_ID can be used instead of +// AUTOUPDATE_VERSION. +// // The server publishes a `meteor_autoupdate_clientVersions` // collection. The contract of this collection is that each document // in the collection represents an acceptable client version, with the @@ -44,13 +47,13 @@ AutoUpdate = {}; AutoUpdate.autoUpdateVersion = null; Meteor.startup(function () { - AutoUpdate.autoUpdateVersion = - process.env.AUTOUPDATE_VERSION || - // also accept SERVER_ID for backwards compatibility. - process.env.SERVER_ID || - WebApp.clientHash; + if (AutoUpdate.autoUpdateVersion === null) + AutoUpdate.autoUpdateVersion = + process.env.AUTOUPDATE_VERSION || + process.env.SERVER_ID || + WebApp.clientHash; - // also make the autoUpdateVersion available on the client. + // Make autoUpdateVersion available on the client. __meteor_runtime_config__.autoUpdateVersion = AutoUpdate.autoUpdateVersion; }); diff --git a/packages/livedata/stream_server.js b/packages/livedata/stream_server.js index 32b1eed551..8315342ca8 100644 --- a/packages/livedata/stream_server.js +++ b/packages/livedata/stream_server.js @@ -65,7 +65,6 @@ StreamServer = function () { }); self.open_sockets.push(socket); - // Send the old style welcome message, which will force old // clients to reload. socket.send(JSON.stringify({server_id: "0"})); diff --git a/packages/test-in-browser/autoupdate.js b/packages/test-in-browser/autoupdate.js new file mode 100644 index 0000000000..b97d27fd4d --- /dev/null +++ b/packages/test-in-browser/autoupdate.js @@ -0,0 +1,7 @@ +// autoupdate normally won't reload on server-only changes, but when +// running tests in the browser it's nice to have server changes cause +// the tests to reload. Setting the auto update version to a +// different value when the server restarts accomplishes this. + +if (Package.autoupdate) + Package.autoupdate.AutoUpdate.autoUpdateVersion = Random.id(); diff --git a/packages/test-in-browser/package.js b/packages/test-in-browser/package.js index 61eaf8980b..cfe84ca858 100644 --- a/packages/test-in-browser/package.js +++ b/packages/test-in-browser/package.js @@ -21,4 +21,8 @@ Package.on_use(function (api) { 'driver.html', 'driver.js' ], "client"); + + api.use('autoupdate', 'server', {weak: true}); + api.use('random', 'server'); + api.add_files('autoupdate.js', 'server'); }); From 005e4a2e85e9725a697e8c3321acd16535a46c69 Mon Sep 17 00:00:00 2001 From: Andrew Wilcox Date: Wed, 13 Nov 2013 16:42:33 -0500 Subject: [PATCH 15/19] livedata doesn't need to use localstorage because it's now using reload migration instead. --- packages/livedata/package.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/livedata/package.js b/packages/livedata/package.js index 3953edc3f8..1de163f06d 100644 --- a/packages/livedata/package.js +++ b/packages/livedata/package.js @@ -25,8 +25,6 @@ Package.on_use(function (api) { // If the facts package is loaded, publish some statistics. api.use('facts', 'server', {weak: true}); - api.use('localstorage', 'client'); - api.export('DDP'); api.export('DDPServer', 'server'); From 22f9183c6b5e7a827e9a266ebafb2fbb2128f4ce Mon Sep 17 00:00:00 2001 From: Andrew Wilcox Date: Thu, 14 Nov 2013 14:45:18 -0500 Subject: [PATCH 16/19] use Deps.autorun, remove unused crypto --- packages/autoupdate/autoupdate_client.js | 2 +- packages/autoupdate/autoupdate_server.js | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/autoupdate/autoupdate_client.js b/packages/autoupdate/autoupdate_client.js index d81ac91d30..2216e80c3f 100644 --- a/packages/autoupdate/autoupdate_client.js +++ b/packages/autoupdate/autoupdate_client.js @@ -49,7 +49,7 @@ Meteor.subscribe("meteor_autoupdate_clientVersions", { }, onReady: function () { if (Package.reload) { - Meteor.autorun(function (computation) { + Deps.autorun(function (computation) { if (ClientVersions.findOne({current: true}) && (! ClientVersions.findOne({_id: autoUpdateVersion}))) { computation.stop(); diff --git a/packages/autoupdate/autoupdate_server.js b/packages/autoupdate/autoupdate_server.js index bd337fb615..1d0031e76d 100644 --- a/packages/autoupdate/autoupdate_server.js +++ b/packages/autoupdate/autoupdate_server.js @@ -35,8 +35,6 @@ // client version. Developers can easily experiment with different // versioning and updating models by forking this package. -var crypto = Npm.require('crypto'); - AutoUpdate = {}; // The client hash includes __meteor_runtime_config__, so wait until From 3245f0d9d679f78b8b65a5066266e29ed7658c8c Mon Sep 17 00:00:00 2001 From: Andrew Wilcox Date: Thu, 14 Nov 2013 15:02:06 -0500 Subject: [PATCH 17/19] Comment on hack encoding knowledge of autoupdate in livedata. --- packages/livedata/livedata_connection.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/livedata/livedata_connection.js b/packages/livedata/livedata_connection.js index 4a7b20859e..aa7c9a9ebe 100644 --- a/packages/livedata/livedata_connection.js +++ b/packages/livedata/livedata_connection.js @@ -791,6 +791,10 @@ _.extend(Connection.prototype, { _.each(_.clone(self._subscriptions), function (sub, id) { // Avoid killing the autoupdate subscription so that developers // still get hot code pushes when writing tests. + // + // XXX it's a hack to encode knowledge about autoupdate here, + // but it doesn't seem worth it yet to have a special API for + // subscriptions to preserve after unit tests. if (sub.name !== 'meteor_autoupdate_clientVersions') { self._send({msg: 'unsub', id: id}); delete self._subscriptions[id]; From 4951520d55c8ac3d32978b680492f8f49e766658 Mon Sep 17 00:00:00 2001 From: Nick Martin Date: Tue, 19 Nov 2013 02:16:48 -0800 Subject: [PATCH 18/19] Add history for autoupdate change. --- History.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/History.md b/History.md index c9cdbb3fb1..be3919064f 100644 --- a/History.md +++ b/History.md @@ -1,5 +1,11 @@ ## vNEXT +* Rework hot code push. The new `autoupdate` package drives automatic + reloads on update using standard DDP messages instead of a hardcoded + message at DDP startup. Now the hot code push only triggers when + client code changes; server only code changes will not cause the page + to reload. + * Bundler failures cause non-zero exit code in `meteor run`. #1515 * Fix `meteor run` with settings files containing non-ASCII characters. #1497 @@ -20,7 +26,7 @@ * Upgraded dependencies: * SockJS server from 0.3.7 to 0.3.8 -Patches contributed by GitHub users mcbain, rzymek. +Patches contributed by GitHub users awwx, mcbain, rzymek. ## v0.6.6.3 From 32000d4c5eef81a3bd12ecb661cc6d1285fd3a40 Mon Sep 17 00:00:00 2001 From: Nick Martin Date: Tue, 19 Nov 2013 02:20:47 -0800 Subject: [PATCH 19/19] Comments. --- packages/livedata/livedata_connection.js | 4 +++- packages/livedata/stream_server.js | 7 +++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/livedata/livedata_connection.js b/packages/livedata/livedata_connection.js index aa7c9a9ebe..15b37d1b96 100644 --- a/packages/livedata/livedata_connection.js +++ b/packages/livedata/livedata_connection.js @@ -181,7 +181,9 @@ var Connection = function (url, options) { } if (msg === null || !msg.msg) { - // ignore the old welcome message + // DEPRECATED. ignore the old welcome message for back compat. + // Remove this 'if' once the server stops sending welcome messages + // (stream_server.js). if (! (msg && msg.server_id)) Meteor._debug("discarding invalid livedata message", msg); return; diff --git a/packages/livedata/stream_server.js b/packages/livedata/stream_server.js index 8315342ca8..160d11e9e8 100644 --- a/packages/livedata/stream_server.js +++ b/packages/livedata/stream_server.js @@ -65,8 +65,11 @@ StreamServer = function () { }); self.open_sockets.push(socket); - // Send the old style welcome message, which will force old - // clients to reload. + // DEPRECATED. Send the old style welcome message, which will force + // old clients to reload. Remove this once we're not concerned about + // people upgrading from a pre-0.6.7 release. Also, remove the + // clause in the client that ignores the welcome message + // (livedata_connection.js) socket.send(JSON.stringify({server_id: "0"})); // call all our callbacks when we get a new socket. they will do the