Skip to content

Commit

Permalink
✨ Store client protocol on Agent
Browse files Browse the repository at this point in the history
When developing new features, it will be useful to be able to compare
server and client protocol versions.

For example, if a server is ahead of an old client, the server could
gracefully degrade performance for clients that don't support the new
functionality.

This change stores the client's protocol on the `Agent` so we can
perform these checks.

We also add a `checkAtLeast()` protocol helper function, and store the
current protocol in a constant to avoid magic numbers.
  • Loading branch information
alecgibson committed Oct 7, 2024
1 parent 945dc50 commit cc58b8a
Show file tree
Hide file tree
Showing 5 changed files with 87 additions and 6 deletions.
13 changes: 11 additions & 2 deletions lib/agent.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ var logger = require('./logger');
var ACTIONS = require('./message-actions').ACTIONS;
var types = require('./types');
var util = require('./util');
var protocol = require('./protocol');

var ERROR_CODE = ShareDBError.CODES;

Expand Down Expand Up @@ -62,6 +63,8 @@ function Agent(backend, stream) {
// active, and it is passed to each middleware call
this.custom = Object.create(null);

this.protocol = Object.create(null);

// The first message received over the connection. Stored to warn if messages
// are being sent before the handshake.
this._firstReceivedMessage = null;
Expand Down Expand Up @@ -437,6 +440,7 @@ Agent.prototype._handleMessage = function(request, callback) {
switch (request.a) {
case ACTIONS.handshake:
if (request.id) this.src = request.id;
this._setProtocol(request);
return callback(null, this._initMessage(ACTIONS.handshake));
case ACTIONS.queryFetch:
return this._queryFetch(request.id, request.c, request.q, getQueryOptions(request), callback);
Expand Down Expand Up @@ -788,8 +792,8 @@ Agent.prototype._fetchSnapshotByTimestamp = function(collection, id, timestamp,
Agent.prototype._initMessage = function(action) {
return {
a: action,
protocol: 1,
protocolMinor: 1,
protocol: protocol.major,
protocolMinor: protocol.minor,
id: this._src(),
type: types.defaultType.uri
};
Expand Down Expand Up @@ -973,6 +977,11 @@ Agent.prototype._checkFirstMessage = function(request) {
}
};

Agent.prototype._setProtocol = function(request) {
this.protocol.major = request.protocol;
this.protocol.minor = request.protocolMinor;
};

function createClientOp(request, clientId) {
// src can be provided if it is not the same as the current agent,
// such as a resubmission after a reconnect, but it usually isn't needed
Expand Down
14 changes: 10 additions & 4 deletions lib/client/connection.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ var types = require('../types');
var util = require('../util');
var logger = require('../logger');
var DocPresenceEmitter = require('./presence/doc-presence-emitter');
var protocol = require('../protocol');

var ERROR_CODE = ShareDBError.CODES;

Expand Down Expand Up @@ -728,16 +729,21 @@ Connection.prototype._handleSnapshotFetch = function(error, message) {
};

Connection.prototype._handleLegacyInit = function(message) {
// If the minor protocol version has been set, we want to use the
// If the protocol is at least 1.1, we want to use the
// new handshake protocol. Let's send a handshake initialize, because
// we now know the server is ready. If we've already sent it, we'll
// just ignore the response anyway.
if (message.protocolMinor) return this._initializeHandshake();
if (protocol.checkAtLeast(message, '1.1')) return this._initializeHandshake();
this._initialize(message);
};

Connection.prototype._initializeHandshake = function() {
this.send({a: ACTIONS.handshake, id: this.id});
this.send({
a: ACTIONS.handshake,
id: this.id,
protocol: protocol.major,
protocolMinor: protocol.minor,
});
};

Connection.prototype._handleHandshake = function(error, message) {
Expand All @@ -753,7 +759,7 @@ Connection.prototype._handlePingPong = function(error) {
Connection.prototype._initialize = function(message) {
if (this.state !== 'connecting') return;

if (message.protocol !== 1) {
if (message.protocol !== protocol.major) {
return this.emit('error', new ShareDBError(
ERROR_CODE.ERR_PROTOCOL_VERSION_NOT_SUPPORTED,
'Unsupported protocol version: ' + message.protocol
Expand Down
27 changes: 27 additions & 0 deletions lib/protocol.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
module.exports = {
major: 1,
minor: 1,
checkAtLeast: checkAtLeast
};

function checkAtLeast(toCheck, checkAgainst) {
toCheck = normalizedProtocol(toCheck);
checkAgainst = normalizedProtocol(checkAgainst);
return toCheck.major >= checkAgainst.major &&
toCheck.minor >= checkAgainst.minor;
}

function normalizedProtocol(protocol) {
if (typeof protocol === 'string') {
var segments = protocol.split('.');
protocol = {
major: segments[0],
minor: segments[1]
};
}

return {
major: protocol.protocol || protocol.major || 0,
minor: protocol.protocolMinor || protocol.minor || 0
};
}
12 changes: 12 additions & 0 deletions test/agent.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ var StreamSocket = require('../lib/stream-socket');
var expect = require('chai').expect;
var ACTIONS = require('../lib/message-actions').ACTIONS;
var Connection = require('../lib/client/connection');
var protocol = require('../lib/protocol');
var LegacyConnection = require('sharedb-legacy/lib/client').Connection;

describe('Agent', function() {
Expand Down Expand Up @@ -70,5 +71,16 @@ describe('Agent', function() {
done();
});
});

it('records the client protocol on the agent', function(done) {
var connection = backend.connect();
connection.once('connected', function() {
expect(connection.agent.protocol).to.eql({
major: protocol.major,
minor: protocol.minor,
});
done();
});
});
});
});
27 changes: 27 additions & 0 deletions test/protocol.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
var protocol = require('../lib/protocol');
var expect = require('chai').expect;

describe('protocol', function() {
describe('checkAtLeast', function() {
const FIXTURES = [
['1.0', '1.0', true],
['1.1', '1.0', true],
['1.0', '1.1', false],
['1.0', '1', true],
[{major: 1, minor: 0}, {major: 1, minor: 0}, true],
[{major: 1, minor: 1}, {major: 1, minor: 0}, true],
[{major: 1, minor: 0}, {major: 1, minor: 1}, false],
[{protocol: 1, protocolMinor: 0}, {protocol: 1, protocolMinor: 0}, true],
[{protocol: 1, protocolMinor: 1}, {protocol: 1, protocolMinor: 0}, true],
[{protocol: 1, protocolMinor: 0}, {protocol: 1, protocolMinor: 1}, false],
];

for (const fixture of FIXTURES) {
const is = fixture[2] ? ' is ' : ' is not ';
const name = 'checks ' + JSON.stringify(fixture[0]) + is + 'at least ' + JSON.stringify(fixture[1]);
it(name, function() {
expect(protocol.checkAtLeast(fixture[0], fixture[1])).to.equal(fixture[2]);
});
}
});
});

0 comments on commit cc58b8a

Please sign in to comment.