diff --git a/packages/autoupdate/autoupdate_client.js b/packages/autoupdate/autoupdate_client.js index 0465e3bd1e..5fb75aba3d 100644 --- a/packages/autoupdate/autoupdate_client.js +++ b/packages/autoupdate/autoupdate_client.js @@ -24,28 +24,46 @@ // The client version of the client code currently running in the // browser. -var autoupdateVersion = __meteor_runtime_config__.autoupdateVersion || "unknown"; -var autoupdateVersionRefreshable = - __meteor_runtime_config__.autoupdateVersionRefreshable || "unknown"; -// The collection of acceptable client versions. -ClientVersions = new Mongo.Collection("meteor_autoupdate_clientVersions"); +const clientArch = Meteor.isCordova ? "web.cordova" : + Meteor.isModern ? "web.browser" : "web.browser.legacy"; + +const autoupdateVersions = + __meteor_runtime_config__.autoupdate.versions[clientArch] || { + version: "unknown", + versionRefreshable: "unknown", + versionNonRefreshable: "unknown", + assets: [], + }; Autoupdate = {}; +// The collection of acceptable client versions. +ClientVersions = + Autoupdate._ClientVersions = // Used by a self-test. + new Mongo.Collection("meteor_autoupdate_clientVersions"); + Autoupdate.newClientAvailable = function () { - return !! ClientVersions.findOne({ - _id: "version", - version: {$ne: autoupdateVersion} }) || - !! ClientVersions.findOne({ - _id: "version-refreshable", - version: {$ne: autoupdateVersionRefreshable} }); + return !! ( + ClientVersions.findOne({ + _id: clientArch, + versionNonRefreshable: { + $ne: autoupdateVersions.versionNonRefreshable, + } + }) || + ClientVersions.findOne({ + _id: clientArch, + versionRefreshable: { + $ne: autoupdateVersions.versionRefreshable, + } + }) + ); }; -Autoupdate._ClientVersions = ClientVersions; // Used by a self-test -var knownToSupportCssOnLoad = false; +// Set to true if the link.onload callback ever fires for any node. +let knownToSupportCssOnLoad = false; -var retry = new Retry({ +const retry = new Retry({ // Unlike the stream reconnect use of Retry, which we want to be instant // in normal operation, this is a wacky failure. We don't want to retry // right away, we can start slowly. @@ -57,19 +75,12 @@ var retry = new Retry({ minCount: 0, // don't do any immediate retries baseTimeout: 30*1000 // start with 30s }); -var failures = 0; -function after(times, func) { - return function() { - if (--times < 1) { - return func.apply(this, arguments); - } - }; -}; +let failures = 0; -Autoupdate._retrySubscription = function () { +Autoupdate._retrySubscription = () => { Meteor.subscribe("meteor_autoupdate_clientVersions", { - onError: function (error) { + onError(error) { Meteor._debug("autoupdate subscription failed", error); failures++; retry.retryLater(failures, function () { @@ -83,94 +94,104 @@ Autoupdate._retrySubscription = function () { Autoupdate._retrySubscription(); }); }, - onReady: function () { - if (Package.reload) { - var checkNewVersionDocument = function (doc) { - var self = this; - if (doc._id === 'version-refreshable' && - doc.version !== autoupdateVersionRefreshable) { - autoupdateVersionRefreshable = doc.version; - // Switch out old css links for the new css links. Inspired by: - // https://github.com/guard/guard-livereload/blob/master/js/livereload.js#L710 - var newCss = (doc.assets && doc.assets.allCss) || []; - var oldLinks = []; - Array.prototype.forEach.call( - document.getElementsByTagName('link'), - function (link) { - if (link.className === '__meteor-css__') { - oldLinks.push(link); - } - } - ); + onReady() { + const handle = ClientVersions.find().observe({ + added: checkNewVersionDocument, + changed: checkNewVersionDocument + }); - function waitUntilCssLoads(link, callback) { - var called; - function executeCallback(...args) { - if (! called) { - called = true; - return callback(...args); - } - } + function checkNewVersionDocument(doc) { + if (doc._id !== clientArch) { + return; + } - link.onload = function () { - knownToSupportCssOnLoad = true; - executeCallback(); - }; + if (doc.versionNonRefreshable !== + autoupdateVersions.versionNonRefreshable) { + // Non-refreshable assets have changed, so we have to reload the + // whole page rather than just replacing tags. + if (handle) handle.stop(); + if (Package.reload) { + // The reload package should be provided by ddp-client, which + // is provided by the ddp package that autoupdate depends on. + Package.reload.Reload._reload(); + } + return; + } - if (! knownToSupportCssOnLoad) { - var id = Meteor.setInterval(function () { - if (link.sheet) { - executeCallback(); - Meteor.clearInterval(id); - } - }, 50); + if (doc.versionRefreshable !== autoupdateVersions.versionRefreshable) { + autoupdateVersions.versionRefreshable = doc.versionRefreshable; + + // Switch out old css links for the new css links. Inspired by: + // https://github.com/guard/guard-livereload/blob/master/js/livereload.js#L710 + var newCss = doc.assets || []; + var oldLinks = []; + + Array.prototype.forEach.call( + document.getElementsByTagName('link'), + function (link) { + if (link.className === '__meteor-css__') { + oldLinks.push(link); } } + ); - var removeOldLinks = after(newCss.length, function () { - oldLinks.forEach(function (link) { + function waitUntilCssLoads(link, callback) { + var called; + + link.onload = function () { + knownToSupportCssOnLoad = true; + if (! called) { + called = true; + callback(); + } + }; + + if (! knownToSupportCssOnLoad) { + var id = Meteor.setInterval(function () { + if (link.sheet) { + if (! called) { + called = true; + callback(); + } + Meteor.clearInterval(id); + } + }, 50); + } + } + + let newLinksLeftToLoad = newCss.length; + function removeOldLinks() { + if (oldLinks.length > 0 && + --newLinksLeftToLoad < 1) { + oldLinks.splice(0).forEach(link => { link.parentNode.removeChild(link); }); - }); + } + } - var attachStylesheetLink = function (newLink) { - document.getElementsByTagName("head").item(0).appendChild(newLink); + if (newCss.length > 0) { + newCss.forEach(css => { + const newLink = document.createElement("link"); + newLink.setAttribute("rel", "stylesheet"); + newLink.setAttribute("type", "text/css"); + newLink.setAttribute("class", "__meteor-css__"); + newLink.setAttribute("href", css.url); waitUntilCssLoads(newLink, function () { Meteor.setTimeout(removeOldLinks, 200); }); - }; - - if (newCss.length !== 0) { - newCss.forEach(function (css) { - var newLink = document.createElement("link"); - newLink.setAttribute("rel", "stylesheet"); - newLink.setAttribute("type", "text/css"); - newLink.setAttribute("class", "__meteor-css__"); - newLink.setAttribute("href", css.url); - attachStylesheetLink(newLink); - }); - } else { - removeOldLinks(); - } + const head = document.getElementsByTagName("head").item(0); + head.appendChild(newLink); + }); + } else { + removeOldLinks(); } - else if (doc._id === 'version' && doc.version !== autoupdateVersion) { - handle && handle.stop(); - - if (Package.reload) { - Package.reload.Reload._reload(); - } - } - }; - - var handle = ClientVersions.find().observe({ - added: checkNewVersionDocument, - changed: checkNewVersionDocument - }); + } } } }); }; + Autoupdate._retrySubscription(); diff --git a/packages/autoupdate/autoupdate_cordova.js b/packages/autoupdate/autoupdate_cordova.js index fd047b26e5..af62a537f1 100644 --- a/packages/autoupdate/autoupdate_cordova.js +++ b/packages/autoupdate/autoupdate_cordova.js @@ -1,16 +1,20 @@ -var autoupdateVersionCordova = __meteor_runtime_config__.autoupdateVersionCordova || "unknown"; +var autoupdateVersionsCordova = + __meteor_runtime_config__.autoupdate.versions["web.cordova"] || { + version: "unknown" + }; // The collection of acceptable client versions. ClientVersions = new Mongo.Collection("meteor_autoupdate_clientVersions"); Autoupdate = {}; -Autoupdate.newClientAvailable = function() { - return !! ClientVersions.findOne({ - _id: 'version-cordova', - version: {$ne: autoupdateVersionCordova} +Autoupdate.newClientAvailable = + () => !! ClientVersions.findOne({ + _id: "web.cordova", + version: { + $ne: autoupdateVersionsCordova.version + } }); -}; var retry = new Retry({ // Unlike the stream reconnect use of Retry, which we want to be instant @@ -24,12 +28,14 @@ var retry = new Retry({ minCount: 0, // don't do any immediate retries baseTimeout: 30*1000 // start with 30s }); -var failures = 0; -Autoupdate._retrySubscription = function() { - var appId = __meteor_runtime_config__.appId; +let failures = 0; + +Autoupdate._retrySubscription = () => { + const { appId } = __meteor_runtime_config__; + Meteor.subscribe("meteor_autoupdate_clientVersions", appId, { - onError: function(error) { + onError(error) { console.log("autoupdate subscription failed:", error); failures++; retry.retryLater(failures, function() { @@ -43,16 +49,18 @@ Autoupdate._retrySubscription = function() { Autoupdate._retrySubscription(); }); }, - onReady: function() { + + onReady() { if (Package.reload) { - var checkNewVersionDocument = function(doc) { - var self = this; - if (doc.version !== autoupdateVersionCordova) { + function checkNewVersionDocument(doc) { + if (doc.version !== autoupdateVersionsCordova.version) { newVersionAvailable(); } - }; + } - var handle = ClientVersions.find({_id: 'version-cordova'}).observe({ + ClientVersions.find({ + _id: "web.cordova" + }).observe({ added: checkNewVersionDocument, changed: checkNewVersionDocument }); @@ -61,8 +69,8 @@ Autoupdate._retrySubscription = function() { }); }; -Meteor.startup(function() { - WebAppLocalServer.onNewVersionReady(function() { +Meteor.startup(() => { + WebAppLocalServer.onNewVersionReady(() => { if (Package.reload) { Package.reload.Reload._reload(); } @@ -71,6 +79,6 @@ Meteor.startup(function() { Autoupdate._retrySubscription(); }); -var newVersionAvailable = function() { +function newVersionAvailable() { WebAppLocalServer.checkForUpdates(); } diff --git a/packages/autoupdate/autoupdate_server.js b/packages/autoupdate/autoupdate_server.js index e8cc178579..8083824c1c 100644 --- a/packages/autoupdate/autoupdate_server.js +++ b/packages/autoupdate/autoupdate_server.js @@ -1,39 +1,38 @@ -// Publish the current client versions 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. +// Publish the current client versions for each client architecture +// (web.browser, web.browser.legacy, web.cordova). When a client observes +// a change in the versions associated with its client architecture, +// it will refresh itself, either by swapping out CSS assets or by +// reloading the page. // -// By default there are two current client versions. The refreshable client -// version is identified by a hash of the client resources seen by the browser -// that are refreshable, such as CSS, while the non refreshable client version -// is identified by a hash of the rest of the client assets -// (the HTML, code, and static files in the `public` directory). +// There are three versions for any given client architecture: `version`, +// `versionRefreshable`, and `versionNonRefreshable`. The refreshable +// version is a hash of just the client resources that are refreshable, +// such as CSS, while the non-refreshable version is a hash of the rest of +// the client assets, excluding the refreshable ones: HTML, JS, and static +// files in the `public` directory. The `version` version is a combined +// hash of everything. // -// 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. +// If the environment variable `AUTOUPDATE_VERSION` is set, it will be +// used in place of all client versions. You can use this variable to +// control when the client reloads. For example, if you want to force a +// reload only after major changes, use a custom AUTOUPDATE_VERSION and +// change it only when something worth pushing to clients happens. // -// The server publishes a `meteor_autoupdate_clientVersions` -// collection. There are two documents in this collection, a document -// with _id 'version' which represents the non refreshable client assets, -// and a document with _id 'version-refreshable' which represents the -// refreshable client assets. Each document has a 'version' field -// which is equivalent to the hash of the relevant assets. The refreshable -// document also contains a list of the refreshable assets, so that the client -// can swap in the new assets without forcing a page refresh. Clients can -// observe changes on these documents to detect when there is a new -// version available. -// -// In this implementation only two documents are present in the collection -// the current refreshable client version and the current nonRefreshable client -// version. Developers can easily experiment with different versioning and -// updating models by forking this package. +// The server publishes a `meteor_autoupdate_clientVersions` collection. +// The ID of each document is the client architecture, and the fields of +// the document are the versions described above. var Future = Npm.require("fibers/future"); -Autoupdate = {}; +Autoupdate = __meteor_runtime_config__.autoupdate = { + // Map from client architectures (web.browser, web.browser.legacy, + // web.cordova) to version fields { version, versionRefreshable, + // versionNonRefreshable, refreshable } that will be stored in + // ClientVersions documents (whose IDs are client architectures). This + // data gets serialized into the boilerplate because it's stored in + // __meteor_runtime_config__.autoupdate.versions. + versions: {} +}; // The collection of acceptable client versions. ClientVersions = new Mongo.Collection("meteor_autoupdate_clientVersions", @@ -53,91 +52,47 @@ Autoupdate.appId = __meteor_runtime_config__.appId = process.env.APP_ID; var syncQueue = new Meteor._SynchronousQueue(); -// updateVersions can only be called after the server has fully loaded. -var updateVersions = function (shouldReloadClientProgram) { - // Step 1: load the current client program on the server and update the - // hash values in __meteor_runtime_config__. +function updateVersions(shouldReloadClientProgram) { + // Step 1: load the current client program on the server if (shouldReloadClientProgram) { WebAppInternals.reloadClientPrograms(); } - // If we just re-read the client program, or if we don't have an autoupdate - // version, calculate it. - if (shouldReloadClientProgram || Autoupdate.autoupdateVersion === null) { - Autoupdate.autoupdateVersion = - process.env.AUTOUPDATE_VERSION || - WebApp.calculateClientHashNonRefreshable(); - } - // If we just recalculated it OR if it was set by (eg) test-in-browser, - // ensure it ends up in __meteor_runtime_config__. - __meteor_runtime_config__.autoupdateVersion = - Autoupdate.autoupdateVersion; + // Step 2: update __meteor_runtime_config__.autoupdate.versions. + const clientArchs = Object.keys(WebApp.clientPrograms); + clientArchs.forEach(arch => { + Autoupdate.versions[arch] = { + version: WebApp.calculateClientHashNonRefreshable(arch), + versionRefreshable: WebApp.calculateClientHashRefreshable(arch), + versionNonRefreshable: + WebApp.calculateClientHashNonRefreshable(arch), + }; + }); - Autoupdate.autoupdateVersionRefreshable = - __meteor_runtime_config__.autoupdateVersionRefreshable = - process.env.AUTOUPDATE_VERSION || - WebApp.calculateClientHashRefreshable(); - - Autoupdate.autoupdateVersionCordova = - __meteor_runtime_config__.autoupdateVersionCordova = - process.env.AUTOUPDATE_VERSION || - WebApp.calculateClientHashCordova(); - - // Step 2: form the new client boilerplate which contains the updated + // Step 3: form the new client boilerplate which contains the updated // assets and __meteor_runtime_config__. if (shouldReloadClientProgram) { WebAppInternals.generateBoilerplate(); } - // XXX COMPAT WITH 0.8.3 - if (! ClientVersions.findOne({current: true})) { - // To ensure apps with version of Meteor prior to 0.9.0 (in - // which the structure of documents in `ClientVersions` was - // different) also reload. - ClientVersions.insert({current: true}); - } - - if (! ClientVersions.findOne({_id: "version"})) { - ClientVersions.insert({ - _id: "version", - version: Autoupdate.autoupdateVersion - }); - } else { - ClientVersions.update("version", { $set: { - version: Autoupdate.autoupdateVersion - }}); - } - - if (! ClientVersions.findOne({_id: "version-cordova"})) { - ClientVersions.insert({ - _id: "version-cordova", - version: Autoupdate.autoupdateVersionCordova, - refreshable: false - }); - } else { - ClientVersions.update("version-cordova", { $set: { - version: Autoupdate.autoupdateVersionCordova - }}); - } - - // Use `onListening` here because we need to use - // `WebAppInternals.refreshableAssets`, which is only set after + // Step 4: update the ClientVersions collection. + // We use `onListening` here because we need to use + // `WebApp.getRefreshableAssets`, which is only set after // `WebApp.generateBoilerplate` is called by `main` in webapp. - WebApp.onListening(function () { - if (! ClientVersions.findOne({_id: "version-refreshable"})) { - ClientVersions.insert({ - _id: "version-refreshable", - version: Autoupdate.autoupdateVersionRefreshable, - assets: WebAppInternals.refreshableAssets - }); - } else { - ClientVersions.update("version-refreshable", { $set: { - version: Autoupdate.autoupdateVersionRefreshable, - assets: WebAppInternals.refreshableAssets - }}); - } + WebApp.onListening(() => { + clientArchs.forEach(arch => { + const payload = { + ...Autoupdate.versions[arch], + assets: WebApp.getRefreshableAssets(arch), + }; + if (! ClientVersions.findOne({ _id: arch })) { + ClientVersions.insert({ _id: arch, ...payload }); + } else { + ClientVersions.update(arch, { $set: payload }); + } + }); }); -}; +} Meteor.publish( "meteor_autoupdate_clientVersions", diff --git a/packages/autoupdate/package.js b/packages/autoupdate/package.js index 576cb295e6..536eeae5f7 100644 --- a/packages/autoupdate/package.js +++ b/packages/autoupdate/package.js @@ -1,6 +1,6 @@ Package.describe({ summary: "Update the client when new client code is available", - version: '1.4.1' + version: '1.5.0' }); Package.onUse(function (api) { diff --git a/packages/webapp/webapp_server.js b/packages/webapp/webapp_server.js index 14827483fd..63c99ed84d 100644 --- a/packages/webapp/webapp_server.js +++ b/packages/webapp/webapp_server.js @@ -206,12 +206,6 @@ Meteor.startup(function () { ].versionNonRefreshable; }; - WebApp.calculateClientHashCordova = function () { - return (WebApp.clientPrograms["web.cordova"] || { - version: "none" - }).version; - }; - WebApp.getRefreshableAssets = function (arch) { return WebApp.clientPrograms[ arch || WebApp.defaultArch