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
|
||||
|
||||
* 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
|
||||
|
||||
@@ -84,30 +84,32 @@ 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 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 client resources change,
|
||||
// include a hash of client resources 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);
|
||||
}
|
||||
});
|
||||
var digest = hash.digest('hex');
|
||||
manifest += "# " + WebApp.clientHash + "\n";
|
||||
|
||||
var manifest = "CACHE MANIFEST\n\n";
|
||||
manifest += '# ' + digest + "\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.
|
||||
|
||||
if (Package.autoupdate) {
|
||||
var version = Package.autoupdate.AutoUpdate.autoUpdateVersion;
|
||||
if (version !== WebApp.clientHash)
|
||||
manifest += "# " + version + "\n";
|
||||
}
|
||||
|
||||
manifest += "\n";
|
||||
|
||||
manifest += "CACHE:" + "\n";
|
||||
manifest += "/" + "\n";
|
||||
_.each(WebApp.clientProgram.manifest, function (resource) {
|
||||
|
||||
@@ -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', {weak: true});
|
||||
api.add_files('appcache-client.js', 'client');
|
||||
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:
|
||||
|
||||
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
|
||||
|
||||
@@ -11,8 +11,28 @@ if (Meteor.isClient) {
|
||||
if (__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 =
|
||||
DDP.connect(ddpUrl, true /* restart_on_update */);
|
||||
DDP.connect(ddpUrl, {
|
||||
onDDPVersionNegotiationFailure: onDDPVersionNegotiationFailure
|
||||
});
|
||||
|
||||
// Proxy the public methods of Meteor.connection so they can
|
||||
// be called directly on Meteor.
|
||||
|
||||
@@ -7,20 +7,18 @@ 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?
|
||||
// onDDPNegotiationVersionFailure: callback when version negotiation fails.
|
||||
var Connection = function (url, options) {
|
||||
var self = this;
|
||||
options = _.extend({
|
||||
reloadOnUpdate: false,
|
||||
// The rest of these options are only for testing.
|
||||
reloadWithOutstanding: false,
|
||||
supportedDDPVersions: SUPPORTED_DDP_VERSIONS,
|
||||
onConnectionFailure: function (reason) {
|
||||
Meteor._debug("Failed DDP connection: " + reason);
|
||||
onConnected: function () {},
|
||||
onDDPVersionNegotiationFailure: function (description) {
|
||||
Meteor._debug(description);
|
||||
},
|
||||
onConnected: function () {}
|
||||
// These options are only for testing.
|
||||
reloadWithOutstanding: false,
|
||||
supportedDDPVersions: SUPPORTED_DDP_VERSIONS
|
||||
}, options);
|
||||
|
||||
// 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.
|
||||
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?");
|
||||
@@ -183,7 +181,11 @@ var Connection = function (url, options) {
|
||||
}
|
||||
|
||||
if (msg === null || !msg.msg) {
|
||||
Meteor._debug("discarding invalid livedata message", 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);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -197,10 +199,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.onConnectionFailure(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))
|
||||
@@ -277,17 +279,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
|
||||
@@ -800,8 +791,16 @@ _.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.
|
||||
//
|
||||
// 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];
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
@@ -1392,9 +1391,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, options) {
|
||||
var ret = new Connection(url, options);
|
||||
allConnections.push(ret); // hack. see below.
|
||||
return ret;
|
||||
};
|
||||
|
||||
@@ -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,9 +1349,9 @@ 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.matches(connection.status().reason, /DDP version negotiation failed/);
|
||||
test.isFalse(connection.status().connected);
|
||||
onComplete();
|
||||
},
|
||||
|
||||
@@ -31,8 +31,9 @@ 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('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']);
|
||||
|
||||
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) {
|
||||
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])
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,7 +50,7 @@ _.extend(LivedataTest.ClientStream.prototype, {
|
||||
self.rawUrl = url;
|
||||
},
|
||||
|
||||
_connected: function (welcome_message) {
|
||||
_connected: function () {
|
||||
var self = this;
|
||||
|
||||
if (self.connectionTimer) {
|
||||
@@ -65,24 +63,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;
|
||||
@@ -171,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);
|
||||
});
|
||||
|
||||
@@ -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 () {
|
||||
@@ -74,10 +65,12 @@ 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}));
|
||||
// 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
|
||||
// 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.
|
||||
'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.js'
|
||||
], "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;
|
||||
};
|
||||
|
||||
|
||||
// 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
|
||||
|
||||
Reference in New Issue
Block a user