Merge branch 'pr/1583' into devel

This commit is contained in:
Nick Martin
2013-11-19 02:26:36 -08:00
22 changed files with 497 additions and 158 deletions

View File

@@ -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

View File

@@ -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) {

View File

@@ -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
View File

@@ -0,0 +1 @@
.build*

114
packages/autoupdate/QA.md Normal file
View 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.

View 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();
}
});
}
}
});

View 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}
);

View 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');
});

View File

@@ -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

View File

@@ -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.

View File

@@ -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;
};

View File

@@ -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();
},

View File

@@ -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']);

View 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;
}
});

View File

@@ -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;

View File

@@ -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);

View File

@@ -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);
});

View File

@@ -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.

View File

@@ -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']);
});

View 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();

View File

@@ -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');
});

View File

@@ -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