diff --git a/docs/client/api.html b/docs/client/api.html index a28563bf19..90cd244316 100644 --- a/docs/client/api.html +++ b/docs/client/api.html @@ -359,8 +359,9 @@ the server. The return value is an object with the following fields: Describes the current reconnection status. The possible values are `connected` (the connection is up and running), `connecting` (disconnected and trying to open a - new connection), and `waiting` (failed to connect and - waiting to try to reconnect). + new connection), `failed` (permanently failed to connect; e.g., the client + and server support different versions of DDP) and `waiting` (failed + to connect and waiting to try to reconnect). {{/dtdd}} {{#dtdd name="retryCount" type="Number"}} @@ -374,6 +375,10 @@ the server. The return value is an object with the following fields: `retryTime - (new Date()).getTime()`. This key will be set only when `status` is `waiting`. {{/dtdd}} + +{{#dtdd name="reason" type="String or undefined"}} + If `status` is `failed`, a description of why the connection failed. +{{/dtdd}} Instead of using callbacks to notify you on changes, this is diff --git a/packages/livedata/livedata_connection.js b/packages/livedata/livedata_connection.js index f0203542aa..6dc9191f4c 100644 --- a/packages/livedata/livedata_connection.js +++ b/packages/livedata/livedata_connection.js @@ -197,10 +197,10 @@ Meteor._LivedataConnection = function (url, options) { self._versionSuggestion = msg.version; self._stream.reconnect({_force: true}); } else { - // XXX We should probably plumb this through to Meteor.connect and add - // an error callback there. - options.onConnectionFailure("Version negotiation failed; server requested version " + msg.version); - self._stream.forceDisconnect(true); + var error = + "Version negotiation failed; server requested version " + msg.version; + self._stream.forceDisconnect(error); + options.onConnectionFailure(error); } } else if (_.include(['added', 'changed', 'removed', 'complete', 'updated'], msg.msg)) diff --git a/packages/livedata/livedata_connection_tests.js b/packages/livedata/livedata_connection_tests.js index 1a2e2f38aa..23ce32d80f 100644 --- a/packages/livedata/livedata_connection_tests.js +++ b/packages/livedata/livedata_connection_tests.js @@ -1214,7 +1214,7 @@ Tinytest.addAsync("livedata connection - version negotiation requires renegotiat onConnectionFailure: function () { test.fail(); onComplete(); }, onConnected: function () { test.equal(connection._version, Meteor._SUPPORTED_DDP_VERSIONS[0]); - connection._stream.forceDisconnect(true); + connection._stream.forceDisconnect(); onComplete(); } }); @@ -1225,7 +1225,12 @@ Tinytest.addAsync("livedata connection - version negotiation fails", var connection = new Meteor._LivedataConnection("/", { reloadWithOutstanding: true, supportedDDPVersions: ["garbled", "more garbled"], - onConnectionFailure: function () { onComplete(); }, + onConnectionFailure: function () { + test.equal(connection.status().status, "failed"); + test.matches(connection.status().reason, /Version negotiation failed/); + test.isFalse(connection.status().connected); + onComplete(); + }, onConnected: function () { test.fail(); onComplete(); diff --git a/packages/stream/stream_client.js b/packages/stream/stream_client.js index 5edcbaaea0..c7b5c6d5c7 100644 --- a/packages/stream/stream_client.js +++ b/packages/stream/stream_client.js @@ -9,7 +9,7 @@ Meteor._Stream = function (url) { self.event_callbacks = {}; // name -> [callback] self.server_id = null; self.sent_update_available = false; - self.force_fail = false; // for debugging. + self._forcedToDisconnect = false; //// Constants @@ -151,14 +151,14 @@ _.extend(Meteor._Stream.prototype, { if (self.current_status.connected) { if (options && options._force) { // force reconnect. - self._disconnected(); + self._lostConnection(); } // else, noop. return; } // if we're mid-connection, stop it. if (self.current_status.status === "connecting") { - self._fake_connect_failed(); + self._lostConnection(); } if (self.retry_timer) @@ -170,14 +170,6 @@ _.extend(Meteor._Stream.prototype, { self._retry_now(); }, - // Permanently disconnect a stream. - forceDisconnect: function (flag) { - var self = this; - self.force_fail = flag; - if (flag && self.socket) - self.socket.close(); - }, - _connected: function (welcome_message) { var self = this; @@ -227,23 +219,20 @@ _.extend(Meteor._Stream.prototype, { }, - _cleanup_socket: function () { + _cleanupSocket: function () { var self = this; + self._clearConnectionAndHeartbeatTimers(); if (self.socket) { self.socket.onmessage = self.socket.onclose = self.socket.onerror = function () {}; self.socket.close(); - - var old_socket = self.socket; self.socket = null; - } }, - _disconnected: function () { + _clearConnectionAndHeartbeatTimers: function () { var self = this; - if (self.connection_timer) { clearTimeout(self.connection_timer); self.connection_timer = null; @@ -252,20 +241,40 @@ _.extend(Meteor._Stream.prototype, { clearTimeout(self.heartbeat_timer); self.heartbeat_timer = null; } - self._cleanup_socket(); - self._retry_later(); // sets status. no need to do it here. }, - _fake_connect_failed: function () { + // Permanently disconnect a stream. + forceDisconnect: function (optionalErrorMessage) { var self = this; - self._cleanup_socket(); - self._disconnected(); + self._forcedToDisconnect = true; + self._cleanupSocket(); + if (self.retry_timer) { + clearTimeout(self.retry_timer); + self.retry_timer = null; + } + self.current_status = { + status: "failed", + connected: false, + retryCount: 0, + // XXX Backwards compatibility only. Remove this before 1.0. + retry_count: 0 + }; + if (optionalErrorMessage) + self.current_status.reason = optionalErrorMessage; + self.status_changed(); + }, + + _lostConnection: function () { + var self = this; + + self._cleanupSocket(); + self._retry_later(); // sets status. no need to do it here. }, _heartbeat_timeout: function () { var self = this; Meteor._debug("Connection timeout. No heartbeat received."); - self._fake_connect_failed(); + self._lostConnection(); }, _heartbeat_received: function () { @@ -312,7 +321,7 @@ _.extend(Meteor._Stream.prototype, { _retry_now: function () { var self = this; - if (self.force_fail) + if (self._forcedToDisconnect) return; self.current_status.retryCount += 1; @@ -330,7 +339,7 @@ _.extend(Meteor._Stream.prototype, { _launch_connection: function () { var self = this; - self._cleanup_socket(); // cleanup the old socket, if there was one. + self._cleanupSocket(); // cleanup the old socket, if there was one. // Convert raw URL to SockJS URL each time we open a connection, so that we // can connect to random hostnames and get around browser per-host @@ -358,7 +367,7 @@ _.extend(Meteor._Stream.prototype, { }; self.socket.onclose = function () { // Meteor._debug("stream disconnect", _.toArray(arguments), (new Date()).toDateString()); - self._disconnected(); + self._lostConnection(); }; self.socket.onerror = function () { // XXX is this ever called? @@ -372,7 +381,7 @@ _.extend(Meteor._Stream.prototype, { if (self.connection_timer) clearTimeout(self.connection_timer); self.connection_timer = setTimeout( - _.bind(self._fake_connect_failed, self), + _.bind(self._lostConnection, self), self.CONNECT_TIMEOUT); } });