From 5cb2dc584001e6667d4214fd15ccacc5158c5b47 Mon Sep 17 00:00:00 2001 From: Alec Gibson <12036746+alecgibson@users.noreply.github.com> Date: Tue, 27 Sep 2022 12:08:41 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Add=20ping/pong?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This change adds the ability for a client to send a "ping" to the server and receive a "pong" response. The motivation for this is that sometimes the socket underlying the `Connection` may not reliably report its connection state (for example if using a wrapper around a "vanilla" websocket that handles reconnection). The most bullet-proof way for a client to determine its connection state is to actually make a request to the server and assert that it receives a timely response. The implementation of ping/pong is arguably a websocket concern, especially since it's already [defined by the spec][1]. However, the browser JavaScript [`WebSocket`][2] does not expose this functionality, so we have to add our own ping/pong layer on top anyway. It's also worth noting that consumers of this library can't easily send their own ping messages down the socket, since ShareDB will [error][3] for unknown messages. Note that this change only adds the ability for a client to ping the server, and not the other way around. This is because: - the `Agent` is not an `Emitter`, and emitting a `pong` on the `Backend` is pretty meaningless - server-side implementations of WebSockets, such as `ws`, _do_ [expose a `ping` API][4] [1]: https://www.rfc-editor.org/rfc/rfc6455#section-5.5.2 [2]: https://developer.mozilla.org/en-US/docs/Web/API/WebSocket [3]: https://github.com/share/sharedb/blob/8b531bedf19860ffe0b37a3e1c8da7a32b5005bd/lib/agent.js#L451 [4]: https://github.com/websockets/ws/blob/966f9d47cd0ff5aa9db0b2aa262f9819d3f4d414/lib/websocket.js#L351 --- lib/agent.js | 10 ++++++++++ lib/client/connection.js | 13 +++++++++++++ lib/message-actions.js | 1 + test/client/connection.js | 30 ++++++++++++++++++++++++++++++ 4 files changed, 54 insertions(+) diff --git a/lib/agent.js b/lib/agent.js index ce2e43a46..2ad4d383d 100644 --- a/lib/agent.js +++ b/lib/agent.js @@ -447,6 +447,8 @@ Agent.prototype._handleMessage = function(request, callback) { return this._subscribePresence(request.ch, request.seq, callback); case ACTIONS.presenceUnsubscribe: return this._unsubscribePresence(request.ch, request.seq, callback); + case ACTIONS.pingPong: + return this._pingPong(callback); default: callback(new ShareDBError(ERROR_CODE.ERR_MESSAGE_BADLY_FORMED, 'Invalid or unknown message')); } @@ -540,6 +542,14 @@ Agent.prototype._querySubscribe = function(queryId, collection, query, options, }); }; +Agent.prototype._pingPong = function(callback) { + var error = null; + var message = { + a: ACTIONS.pingPong + }; + callback(error, message); +}; + function getResultsData(results) { var items = []; for (var i = 0; i < results.length; i++) { diff --git a/lib/client/connection.js b/lib/client/connection.js index 8e3209d2e..2ed3fb32f 100644 --- a/lib/client/connection.js +++ b/lib/client/connection.js @@ -253,6 +253,8 @@ Connection.prototype.handleMessage = function(message) { return this._handlePresenceUnsubscribe(err, message); case ACTIONS.presenceRequest: return this._handlePresenceRequest(err, message); + case ACTIONS.pingPong: + return this._handlePingPong(err); default: logger.warn('Ignoring unrecognized message', message); @@ -476,6 +478,12 @@ Connection.prototype.send = function(message) { this.socket.send(JSON.stringify(message)); }; +Connection.prototype.ping = function() { + var message = { + a: ACTIONS.pingPong + }; + this.send(message); +}; /** * Closes the socket and emits 'closed' @@ -725,6 +733,11 @@ Connection.prototype._handleHandshake = function(error, message) { this._initialize(message); }; +Connection.prototype._handlePingPong = function(error) { + if (error) return this.emit('error', error); + this.emit('pong'); +}; + Connection.prototype._initialize = function(message) { if (this.state !== 'connecting') return; diff --git a/lib/message-actions.js b/lib/message-actions.js index ad2b7536b..baa242b2c 100644 --- a/lib/message-actions.js +++ b/lib/message-actions.js @@ -14,6 +14,7 @@ exports.ACTIONS = { op: 'op', snapshotFetch: 'nf', snapshotFetchByTimestamp: 'nt', + pingPong: 'pp', presence: 'p', presenceSubscribe: 'ps', presenceUnsubscribe: 'pu', diff --git a/test/client/connection.js b/test/client/connection.js index abf2cb970..698aa9b34 100644 --- a/test/client/connection.js +++ b/test/client/connection.js @@ -124,6 +124,36 @@ describe('client connection', function() { }); }); + describe('ping/pong', function() { + it('pings the backend', function(done) { + var connection = this.backend.connect(); + + connection.on('pong', function() { + done(); + }); + + connection.on('connected', function() { + connection.ping(); + }); + }); + + it('handles errors', function(done) { + this.backend.use('receive', function(request, next) { + if (request.data.a !== 'pp') return; + next(new Error('bad')); + }); + + var connection = this.backend.connect(); + + connection.on('error', function(error) { + expect(error.message).to.equal('bad'); + done(); + }); + + connection.ping(); + }); + }); + describe('backend.agentsCount', function() { it('updates after connect and connection.close()', function(done) { var backend = this.backend;