Skip to content

Commit

Permalink
✨ Add ping/pong
Browse files Browse the repository at this point in the history
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
  • Loading branch information
alecgibson committed Sep 27, 2022
1 parent 8b531be commit 5cb2dc5
Show file tree
Hide file tree
Showing 4 changed files with 54 additions and 0 deletions.
10 changes: 10 additions & 0 deletions lib/agent.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'));
}
Expand Down Expand Up @@ -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++) {
Expand Down
13 changes: 13 additions & 0 deletions lib/client/connection.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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'
Expand Down Expand Up @@ -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;

Expand Down
1 change: 1 addition & 0 deletions lib/message-actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ exports.ACTIONS = {
op: 'op',
snapshotFetch: 'nf',
snapshotFetchByTimestamp: 'nt',
pingPong: 'pp',
presence: 'p',
presenceSubscribe: 'ps',
presenceUnsubscribe: 'pu',
Expand Down
30 changes: 30 additions & 0 deletions test/client/connection.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down

0 comments on commit 5cb2dc5

Please sign in to comment.