mirror of
https://github.com/meteor/meteor.git
synced 2026-05-02 03:01:46 -04:00
Merge branch 'pr/1583' into devel
This commit is contained in:
@@ -1,5 +1,11 @@
|
|||||||
## vNEXT
|
## 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
|
* Bundler failures cause non-zero exit code in `meteor run`. #1515
|
||||||
|
|
||||||
* Fix `meteor run` with settings files containing non-ASCII characters. #1497
|
* Fix `meteor run` with settings files containing non-ASCII characters. #1497
|
||||||
@@ -20,7 +26,7 @@
|
|||||||
* Upgraded dependencies:
|
* Upgraded dependencies:
|
||||||
* SockJS server from 0.3.7 to 0.3.8
|
* 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
|
## v0.6.6.3
|
||||||
|
|||||||
@@ -84,29 +84,31 @@ WebApp.connectHandlers.use(function(req, res, next) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var manifest = "CACHE MANIFEST\n\n";
|
||||||
|
|
||||||
// After the browser has downloaded the app files from the server and
|
// After the browser has downloaded the app files from the server and
|
||||||
// has populated the browser's application cache, the browser will
|
// has populated the browser's application cache, the browser will
|
||||||
// *only* connect to the server and reload the application if the
|
// *only* connect to the server and reload the application if the
|
||||||
// *contents* of the app manifest file has changed.
|
// *contents* of the app manifest file has changed.
|
||||||
//
|
//
|
||||||
// So we have to ensure that if any static client resources change,
|
// So to ensure that the client updates if client resources change,
|
||||||
// something changes in the manifest file. We compute a hash of
|
// include a hash of client resources in the manifest.
|
||||||
// everything that gets delivered to the client during the initial
|
|
||||||
// web page load, and include that hash as a comment in the app
|
|
||||||
// manifest. That way if anything changes, the comment changes, and
|
|
||||||
// the browser will reload resources.
|
|
||||||
|
|
||||||
var hash = crypto.createHash('sha1');
|
manifest += "# " + WebApp.clientHash + "\n";
|
||||||
hash.update(JSON.stringify(__meteor_runtime_config__), 'utf8');
|
|
||||||
_.each(WebApp.clientProgram.manifest, function (resource) {
|
// When using the autoupdate package, also include
|
||||||
if (resource.where === 'client' || resource.where === 'internal') {
|
// AUTOUPDATE_VERSION. Otherwise the client will get into an
|
||||||
hash.update(resource.hash);
|
// 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.
|
||||||
|
|
||||||
|
if (Package.autoupdate) {
|
||||||
|
var version = Package.autoupdate.AutoUpdate.autoUpdateVersion;
|
||||||
|
if (version !== WebApp.clientHash)
|
||||||
|
manifest += "# " + version + "\n";
|
||||||
}
|
}
|
||||||
});
|
|
||||||
var digest = hash.digest('hex');
|
|
||||||
|
|
||||||
var manifest = "CACHE MANIFEST\n\n";
|
manifest += "\n";
|
||||||
manifest += '# ' + digest + "\n\n";
|
|
||||||
|
|
||||||
manifest += "CACHE:" + "\n";
|
manifest += "CACHE:" + "\n";
|
||||||
manifest += "/" + "\n";
|
manifest += "/" + "\n";
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ Package.on_use(function (api) {
|
|||||||
api.use('reload', 'client');
|
api.use('reload', 'client');
|
||||||
api.use('routepolicy', 'server');
|
api.use('routepolicy', 'server');
|
||||||
api.use('underscore', 'server');
|
api.use('underscore', 'server');
|
||||||
|
api.use('autoupdate', 'server', {weak: true});
|
||||||
api.add_files('appcache-client.js', 'client');
|
api.add_files('appcache-client.js', 'client');
|
||||||
api.add_files('appcache-server.js', 'server');
|
api.add_files('appcache-server.js', 'server');
|
||||||
});
|
});
|
||||||
|
|||||||
1
packages/autoupdate/.gitignore
vendored
Normal file
1
packages/autoupdate/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.build*
|
||||||
114
packages/autoupdate/QA.md
Normal file
114
packages/autoupdate/QA.md
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
|
||||||
|
## 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`.
|
||||||
|
|
||||||
|
|
||||||
|
## 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
|
||||||
|
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.
|
||||||
|
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
$ 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
|
||||||
|
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 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
|
||||||
|
|
||||||
|
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.
|
||||||
61
packages/autoupdate/autoupdate_client.js
Normal file
61
packages/autoupdate/autoupdate_client.js
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
// 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");
|
||||||
|
|
||||||
|
|
||||||
|
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);
|
||||||
|
},
|
||||||
|
onReady: function () {
|
||||||
|
if (Package.reload) {
|
||||||
|
Deps.autorun(function (computation) {
|
||||||
|
if (ClientVersions.findOne({current: true}) &&
|
||||||
|
(! ClientVersions.findOne({_id: autoUpdateVersion}))) {
|
||||||
|
computation.stop();
|
||||||
|
Package.reload.Reload._reload();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
74
packages/autoupdate/autoupdate_server.js
Normal file
74
packages/autoupdate/autoupdate_server.js
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
// 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.
|
||||||
|
//
|
||||||
|
// 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
|
||||||
|
// `_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 document is published, the current
|
||||||
|
// client version. Developers can easily experiment with different
|
||||||
|
// versioning and updating models by forking this package.
|
||||||
|
|
||||||
|
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
|
||||||
|
// update version id.
|
||||||
|
|
||||||
|
AutoUpdate.autoUpdateVersion = null;
|
||||||
|
|
||||||
|
Meteor.startup(function () {
|
||||||
|
if (AutoUpdate.autoUpdateVersion === null)
|
||||||
|
AutoUpdate.autoUpdateVersion =
|
||||||
|
process.env.AUTOUPDATE_VERSION ||
|
||||||
|
process.env.SERVER_ID ||
|
||||||
|
WebApp.clientHash;
|
||||||
|
|
||||||
|
// Make autoUpdateVersion available on the client.
|
||||||
|
__meteor_runtime_config__.autoUpdateVersion = AutoUpdate.autoUpdateVersion;
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
Meteor.publish(
|
||||||
|
"meteor_autoupdate_clientVersions",
|
||||||
|
function () {
|
||||||
|
var self = this;
|
||||||
|
// 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}
|
||||||
|
);
|
||||||
13
packages/autoupdate/package.js
Normal file
13
packages/autoupdate/package.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
Package.describe({
|
||||||
|
summary: "Update the client when new client code is available"
|
||||||
|
});
|
||||||
|
|
||||||
|
Package.on_use(function (api) {
|
||||||
|
api.use('webapp', 'server');
|
||||||
|
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');
|
||||||
|
});
|
||||||
@@ -37,11 +37,11 @@ depending on message type.
|
|||||||
|
|
||||||
### Procedure:
|
### Procedure:
|
||||||
|
|
||||||
The server may send an initial message which is a JSON object lacking a `msg`
|
The server may send an initial message which is a JSON object lacking
|
||||||
key. If so, the client should ignore it. The client does not have to wait for
|
a `msg` key. If so, the client should ignore it. The client does not
|
||||||
this message. (This message is used to help implement hot code reload over our
|
have to wait for this message. (The message was once used to help
|
||||||
SockJS transport. It is currently sent over websockets as well, but probably
|
implement Meteor's hot code reload feature; it is now only included to
|
||||||
should not be.)
|
force old clients to update).
|
||||||
|
|
||||||
* The client sends a `connect` message.
|
* The client sends a `connect` message.
|
||||||
* If the server is willing to speak the `version` of the protocol specified in
|
* If the server is willing to speak the `version` of the protocol specified in
|
||||||
|
|||||||
@@ -11,8 +11,28 @@ if (Meteor.isClient) {
|
|||||||
if (__meteor_runtime_config__.DDP_DEFAULT_CONNECTION_URL)
|
if (__meteor_runtime_config__.DDP_DEFAULT_CONNECTION_URL)
|
||||||
ddpUrl = __meteor_runtime_config__.DDP_DEFAULT_CONNECTION_URL;
|
ddpUrl = __meteor_runtime_config__.DDP_DEFAULT_CONNECTION_URL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var retry = new Retry();
|
||||||
|
|
||||||
|
var onDDPVersionNegotiationFailure = function (description) {
|
||||||
|
Meteor._debug(description);
|
||||||
|
if (Package.reload) {
|
||||||
|
var migrationData = Package.reload.Reload._migrationData('livedata') || {};
|
||||||
|
var failures = migrationData.DDPVersionNegotiationFailures || 0;
|
||||||
|
++failures;
|
||||||
|
Package.reload.Reload._onMigrate('livedata', function () {
|
||||||
|
return [true, {DDPVersionNegotiationFailures: failures}];
|
||||||
|
});
|
||||||
|
retry.retryLater(failures, function () {
|
||||||
|
Package.reload.Reload._reload();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
Meteor.connection =
|
Meteor.connection =
|
||||||
DDP.connect(ddpUrl, true /* restart_on_update */);
|
DDP.connect(ddpUrl, {
|
||||||
|
onDDPVersionNegotiationFailure: onDDPVersionNegotiationFailure
|
||||||
|
});
|
||||||
|
|
||||||
// Proxy the public methods of Meteor.connection so they can
|
// Proxy the public methods of Meteor.connection so they can
|
||||||
// be called directly on Meteor.
|
// be called directly on Meteor.
|
||||||
|
|||||||
@@ -7,20 +7,18 @@ if (Meteor.isServer) {
|
|||||||
// @param url {String|Object} URL to Meteor app,
|
// @param url {String|Object} URL to Meteor app,
|
||||||
// or an object as a test hook (see code)
|
// or an object as a test hook (see code)
|
||||||
// Options:
|
// 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?
|
// reloadWithOutstanding: is it OK to reload if there are outstanding methods?
|
||||||
|
// onDDPNegotiationVersionFailure: callback when version negotiation fails.
|
||||||
var Connection = function (url, options) {
|
var Connection = function (url, options) {
|
||||||
var self = this;
|
var self = this;
|
||||||
options = _.extend({
|
options = _.extend({
|
||||||
reloadOnUpdate: false,
|
onConnected: function () {},
|
||||||
// The rest of these options are only for testing.
|
onDDPVersionNegotiationFailure: function (description) {
|
||||||
reloadWithOutstanding: false,
|
Meteor._debug(description);
|
||||||
supportedDDPVersions: SUPPORTED_DDP_VERSIONS,
|
|
||||||
onConnectionFailure: function (reason) {
|
|
||||||
Meteor._debug("Failed DDP connection: " + reason);
|
|
||||||
},
|
},
|
||||||
onConnected: function () {}
|
// These options are only for testing.
|
||||||
|
reloadWithOutstanding: false,
|
||||||
|
supportedDDPVersions: SUPPORTED_DDP_VERSIONS
|
||||||
}, options);
|
}, options);
|
||||||
|
|
||||||
// If set, called when we reconnect, queuing method calls _before_ the
|
// If set, called when we reconnect, queuing method calls _before_ the
|
||||||
@@ -162,7 +160,7 @@ var Connection = function (url, options) {
|
|||||||
|
|
||||||
// Block auto-reload while we're waiting for method responses.
|
// Block auto-reload while we're waiting for method responses.
|
||||||
if (Meteor.isClient && Package.reload && !options.reloadWithOutstanding) {
|
if (Meteor.isClient && Package.reload && !options.reloadWithOutstanding) {
|
||||||
Reload._onMigrate(function (retry) {
|
Package.reload.Reload._onMigrate(function (retry) {
|
||||||
if (!self._readyToMigrate()) {
|
if (!self._readyToMigrate()) {
|
||||||
if (self._retryMigrate)
|
if (self._retryMigrate)
|
||||||
throw new Error("Two migrations in progress?");
|
throw new Error("Two migrations in progress?");
|
||||||
@@ -183,6 +181,10 @@ var Connection = function (url, options) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (msg === null || !msg.msg) {
|
if (msg === null || !msg.msg) {
|
||||||
|
// 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);
|
Meteor._debug("discarding invalid livedata message", msg);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -197,10 +199,10 @@ var Connection = function (url, options) {
|
|||||||
self._versionSuggestion = msg.version;
|
self._versionSuggestion = msg.version;
|
||||||
self._stream.reconnect({_force: true});
|
self._stream.reconnect({_force: true});
|
||||||
} else {
|
} else {
|
||||||
var error =
|
var description =
|
||||||
"Version negotiation failed; server requested version " + msg.version;
|
"DDP version negotiation failed; server requested version " + msg.version;
|
||||||
self._stream.disconnect({_permanent: true, _error: error});
|
self._stream.disconnect({_permanent: true, _error: description});
|
||||||
options.onConnectionFailure(error);
|
options.onDDPVersionNegotiationFailure(description);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (_.include(['added', 'changed', 'removed', 'ready', 'updated'], msg.msg))
|
else if (_.include(['added', 'changed', 'removed', 'ready', 'updated'], msg.msg))
|
||||||
@@ -277,17 +279,6 @@ var Connection = function (url, options) {
|
|||||||
self._stream.on('message', onMessage);
|
self._stream.on('message', onMessage);
|
||||||
self._stream.on('reset', onReset);
|
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
|
// A MethodInvoker manages sending a method to the server and calling the user's
|
||||||
@@ -800,8 +791,16 @@ _.extend(Connection.prototype, {
|
|||||||
_unsubscribeAll: function () {
|
_unsubscribeAll: function () {
|
||||||
var self = this;
|
var self = this;
|
||||||
_.each(_.clone(self._subscriptions), function (sub, id) {
|
_.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});
|
self._send({msg: 'unsub', id: id});
|
||||||
delete self._subscriptions[id];
|
delete self._subscriptions[id];
|
||||||
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -1392,9 +1391,8 @@ LivedataTest.Connection = Connection;
|
|||||||
// "/",
|
// "/",
|
||||||
// "ddp+sockjs://ddp--****-foo.meteor.com/sockjs"
|
// "ddp+sockjs://ddp--****-foo.meteor.com/sockjs"
|
||||||
//
|
//
|
||||||
DDP.connect = function (url, _reloadOnUpdate) {
|
DDP.connect = function (url, options) {
|
||||||
var ret = new Connection(
|
var ret = new Connection(url, options);
|
||||||
url, {reloadOnUpdate: _reloadOnUpdate});
|
|
||||||
allConnections.push(ret); // hack. see below.
|
allConnections.push(ret); // hack. see below.
|
||||||
return ret;
|
return ret;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1335,7 +1335,7 @@ Tinytest.addAsync("livedata connection - version negotiation requires renegotiat
|
|||||||
var connection = new LivedataTest.Connection(getSelfConnectionUrl(), {
|
var connection = new LivedataTest.Connection(getSelfConnectionUrl(), {
|
||||||
reloadWithOutstanding: true,
|
reloadWithOutstanding: true,
|
||||||
supportedDDPVersions: ["garbled", LivedataTest.SUPPORTED_DDP_VERSIONS[0]],
|
supportedDDPVersions: ["garbled", LivedataTest.SUPPORTED_DDP_VERSIONS[0]],
|
||||||
onConnectionFailure: function () { test.fail(); onComplete(); },
|
onDDPVersionNegotiationFailure: function () { test.fail(); onComplete(); },
|
||||||
onConnected: function () {
|
onConnected: function () {
|
||||||
test.equal(connection._version, LivedataTest.SUPPORTED_DDP_VERSIONS[0]);
|
test.equal(connection._version, LivedataTest.SUPPORTED_DDP_VERSIONS[0]);
|
||||||
connection._stream.disconnect({_permanent: true});
|
connection._stream.disconnect({_permanent: true});
|
||||||
@@ -1349,9 +1349,9 @@ Tinytest.addAsync("livedata connection - version negotiation error",
|
|||||||
var connection = new LivedataTest.Connection(getSelfConnectionUrl(), {
|
var connection = new LivedataTest.Connection(getSelfConnectionUrl(), {
|
||||||
reloadWithOutstanding: true,
|
reloadWithOutstanding: true,
|
||||||
supportedDDPVersions: ["garbled", "more garbled"],
|
supportedDDPVersions: ["garbled", "more garbled"],
|
||||||
onConnectionFailure: function () {
|
onDDPVersionNegotiationFailure: function () {
|
||||||
test.equal(connection.status().status, "failed");
|
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);
|
test.isFalse(connection.status().connected);
|
||||||
onComplete();
|
onComplete();
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -31,8 +31,9 @@ Package.on_use(function (api) {
|
|||||||
api.export('LivedataTest', {testOnly: true});
|
api.export('LivedataTest', {testOnly: true});
|
||||||
|
|
||||||
// Transport
|
// Transport
|
||||||
api.use('reload', 'client');
|
api.use('reload', 'client', {weak: true});
|
||||||
api.add_files('common.js');
|
api.add_files('common.js');
|
||||||
|
api.add_files('retry.js', ['client', 'server']);
|
||||||
api.add_files(['sockjs-0.3.4.js', 'stream_client_sockjs.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_nodejs.js', 'server');
|
||||||
api.add_files('stream_client_common.js', ['client', 'server']);
|
api.add_files('stream_client_common.js', ['client', 'server']);
|
||||||
|
|||||||
65
packages/livedata/retry.js
Normal file
65
packages/livedata/retry.js
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
@@ -77,7 +77,7 @@ _.extend(LivedataTest.ClientStream.prototype, {
|
|||||||
on: function (name, callback) {
|
on: function (name, callback) {
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
if (name !== 'message' && name !== 'reset' && name !== 'update_available')
|
if (name !== 'message' && name !== 'reset')
|
||||||
throw new Error("unknown event type: " + name);
|
throw new Error("unknown event type: " + name);
|
||||||
|
|
||||||
if (!self.eventCallbacks[name])
|
if (!self.eventCallbacks[name])
|
||||||
@@ -94,27 +94,6 @@ _.extend(LivedataTest.ClientStream.prototype, {
|
|||||||
// failed.
|
// failed.
|
||||||
self.CONNECT_TIMEOUT = 10000;
|
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.eventCallbacks = {}; // name -> [callback]
|
||||||
|
|
||||||
self._forcedToDisconnect = false;
|
self._forcedToDisconnect = false;
|
||||||
@@ -134,7 +113,7 @@ _.extend(LivedataTest.ClientStream.prototype, {
|
|||||||
};
|
};
|
||||||
|
|
||||||
//// Retry logic
|
//// Retry logic
|
||||||
self.retryTimer = null;
|
self._retry = new Retry;
|
||||||
self.connectionTimer = null;
|
self.connectionTimer = null;
|
||||||
|
|
||||||
},
|
},
|
||||||
@@ -161,9 +140,7 @@ _.extend(LivedataTest.ClientStream.prototype, {
|
|||||||
self._lostConnection();
|
self._lostConnection();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (self.retryTimer)
|
self._retry.clear();
|
||||||
clearTimeout(self.retryTimer);
|
|
||||||
self.retryTimer = null;
|
|
||||||
self.currentStatus.retryCount -= 1; // don't count manual retries
|
self.currentStatus.retryCount -= 1; // don't count manual retries
|
||||||
self._retryNow();
|
self._retryNow();
|
||||||
},
|
},
|
||||||
@@ -186,10 +163,7 @@ _.extend(LivedataTest.ClientStream.prototype, {
|
|||||||
}
|
}
|
||||||
|
|
||||||
self._cleanup();
|
self._cleanup();
|
||||||
if (self.retryTimer) {
|
self._retry.clear();
|
||||||
clearTimeout(self.retryTimer);
|
|
||||||
self.retryTimer = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.currentStatus = {
|
self.currentStatus = {
|
||||||
status: (options._permanent ? "failed" : "offline"),
|
status: (options._permanent ? "failed" : "offline"),
|
||||||
@@ -210,22 +184,6 @@ _.extend(LivedataTest.ClientStream.prototype, {
|
|||||||
self._retryLater(); // sets status. no need to do it here.
|
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
|
// fired when we detect that we've gone online. try to reconnect
|
||||||
// immediately.
|
// immediately.
|
||||||
_online: function () {
|
_online: function () {
|
||||||
@@ -237,10 +195,10 @@ _.extend(LivedataTest.ClientStream.prototype, {
|
|||||||
_retryLater: function () {
|
_retryLater: function () {
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
var timeout = self._retryTimeout(self.currentStatus.retryCount);
|
var timeout = self._retry.retryLater(
|
||||||
if (self.retryTimer)
|
self.currentStatus.retryCount,
|
||||||
clearTimeout(self.retryTimer);
|
_.bind(self._retryNow, self)
|
||||||
self.retryTimer = setTimeout(_.bind(self._retryNow, self), timeout);
|
);
|
||||||
|
|
||||||
self.currentStatus.status = "waiting";
|
self.currentStatus.status = "waiting";
|
||||||
self.currentStatus.connected = false;
|
self.currentStatus.connected = false;
|
||||||
|
|||||||
@@ -42,7 +42,6 @@ LivedataTest.ClientStream = function (endpoint) {
|
|||||||
|
|
||||||
self._initCommon();
|
self._initCommon();
|
||||||
|
|
||||||
self.expectingWelcome = false;
|
|
||||||
//// Kickoff!
|
//// Kickoff!
|
||||||
self._launchConnection();
|
self._launchConnection();
|
||||||
};
|
};
|
||||||
@@ -106,19 +105,10 @@ _.extend(LivedataTest.ClientStream.prototype, {
|
|||||||
self._lostConnection();
|
self._lostConnection();
|
||||||
});
|
});
|
||||||
|
|
||||||
self.expectingWelcome = true;
|
|
||||||
connection.on('message', function (message) {
|
connection.on('message', function (message) {
|
||||||
if (self.currentConnection !== this)
|
if (self.currentConnection !== this)
|
||||||
return; // old connection still emitting messages
|
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
|
if (message.type === "utf8") // ignore binary frames
|
||||||
_.each(self.eventCallbacks.message, function (callback) {
|
_.each(self.eventCallbacks.message, function (callback) {
|
||||||
callback(message.utf8Data);
|
callback(message.utf8Data);
|
||||||
|
|||||||
@@ -20,8 +20,6 @@ LivedataTest.ClientStream = function (url) {
|
|||||||
self.rawUrl = url;
|
self.rawUrl = url;
|
||||||
self.socket = null;
|
self.socket = null;
|
||||||
|
|
||||||
self.sent_update_available = false;
|
|
||||||
|
|
||||||
self.heartbeatTimer = null;
|
self.heartbeatTimer = null;
|
||||||
|
|
||||||
// Listen to global 'online' event if we are running in a browser.
|
// Listen to global 'online' event if we are running in a browser.
|
||||||
@@ -52,7 +50,7 @@ _.extend(LivedataTest.ClientStream.prototype, {
|
|||||||
self.rawUrl = url;
|
self.rawUrl = url;
|
||||||
},
|
},
|
||||||
|
|
||||||
_connected: function (welcome_message) {
|
_connected: function () {
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
if (self.connectionTimer) {
|
if (self.connectionTimer) {
|
||||||
@@ -65,24 +63,6 @@ _.extend(LivedataTest.ClientStream.prototype, {
|
|||||||
return;
|
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
|
// update status
|
||||||
self.currentStatus.status = "connected";
|
self.currentStatus.status = "connected";
|
||||||
self.currentStatus.connected = true;
|
self.currentStatus.connected = true;
|
||||||
@@ -171,15 +151,13 @@ _.extend(LivedataTest.ClientStream.prototype, {
|
|||||||
toSockjsUrl(self.rawUrl), undefined, {
|
toSockjsUrl(self.rawUrl), undefined, {
|
||||||
debug: false, protocols_whitelist: self._sockjsProtocolsWhitelist()
|
debug: false, protocols_whitelist: self._sockjsProtocolsWhitelist()
|
||||||
});
|
});
|
||||||
|
self.socket.onopen = function (data) {
|
||||||
|
self._connected();
|
||||||
|
};
|
||||||
self.socket.onmessage = function (data) {
|
self.socket.onmessage = function (data) {
|
||||||
self._heartbeat_received();
|
self._heartbeat_received();
|
||||||
|
|
||||||
// first message we get when we're connecting goes to _connected,
|
if (self.currentStatus.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)
|
|
||||||
_.each(self.eventCallbacks.message, function (callback) {
|
_.each(self.eventCallbacks.message, function (callback) {
|
||||||
callback(data.data);
|
callback(data.data);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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 || "";
|
var pathPrefix = __meteor_runtime_config__.ROOT_URL_PATH_PREFIX || "";
|
||||||
|
|
||||||
StreamServer = function () {
|
StreamServer = function () {
|
||||||
@@ -74,10 +65,12 @@ StreamServer = function () {
|
|||||||
});
|
});
|
||||||
self.open_sockets.push(socket);
|
self.open_sockets.push(socket);
|
||||||
|
|
||||||
|
// DEPRECATED. Send the old style welcome message, which will force
|
||||||
// Send a welcome message with the serverId. Client uses this to
|
// old clients to reload. Remove this once we're not concerned about
|
||||||
// reload if needed.
|
// people upgrading from a pre-0.6.7 release. Also, remove the
|
||||||
socket.send(JSON.stringify({server_id: __meteor_runtime_config__.serverId}));
|
// 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
|
// call all our callbacks when we get a new socket. they will do the
|
||||||
// work of setting up handlers and such for specific messages.
|
// work of setting up handlers and such for specific messages.
|
||||||
|
|||||||
@@ -42,4 +42,13 @@ Package.on_use(function(api) {
|
|||||||
// People like being able to clone objects.
|
// People like being able to clone objects.
|
||||||
'ejson'
|
'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']);
|
||||||
});
|
});
|
||||||
|
|||||||
7
packages/test-in-browser/autoupdate.js
Normal file
7
packages/test-in-browser/autoupdate.js
Normal file
@@ -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();
|
||||||
@@ -21,4 +21,8 @@ Package.on_use(function (api) {
|
|||||||
'driver.html',
|
'driver.html',
|
||||||
'driver.js'
|
'driver.js'
|
||||||
], "client");
|
], "client");
|
||||||
|
|
||||||
|
api.use('autoupdate', 'server', {weak: true});
|
||||||
|
api.use('random', 'server');
|
||||||
|
api.add_files('autoupdate.js', 'server');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -151,6 +151,50 @@ var appUrl = function (url) {
|
|||||||
return true;
|
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 runWebAppServer = function () {
|
||||||
var shuttingDown = false;
|
var shuttingDown = false;
|
||||||
// read the control for the client we'll be serving up
|
// read the control for the client we'll be serving up
|
||||||
|
|||||||
Reference in New Issue
Block a user