From 78c5f03722039f322ccc4682075a8ce367704eaf Mon Sep 17 00:00:00 2001 From: Alec Gibson <12036746+alecgibson@users.noreply.github.com> Date: Mon, 7 Oct 2024 12:44:58 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Store=20client=20protocol=20on=20`A?= =?UTF-8?q?gent`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- lib/agent.js | 13 +++++++++++-- lib/client/connection.js | 14 ++++++++++---- lib/protocol.js | 27 +++++++++++++++++++++++++++ test/agent.js | 12 ++++++++++++ test/protocol.js | 28 ++++++++++++++++++++++++++++ 5 files changed, 88 insertions(+), 6 deletions(-) create mode 100644 lib/protocol.js create mode 100644 test/protocol.js diff --git a/lib/agent.js b/lib/agent.js index a3ee100d6..1352d6600 100644 --- a/lib/agent.js +++ b/lib/agent.js @@ -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; @@ -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; @@ -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); @@ -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 }; @@ -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 diff --git a/lib/client/connection.js b/lib/client/connection.js index 134931c40..da89dfef5 100644 --- a/lib/client/connection.js +++ b/lib/client/connection.js @@ -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; @@ -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) { @@ -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 diff --git a/lib/protocol.js b/lib/protocol.js new file mode 100644 index 000000000..208af5b6b --- /dev/null +++ b/lib/protocol.js @@ -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) + }; +} diff --git a/test/agent.js b/test/agent.js index b67761487..531f04a28 100644 --- a/test/agent.js +++ b/test/agent.js @@ -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() { @@ -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(); + }); + }); }); }); diff --git a/test/protocol.js b/test/protocol.js new file mode 100644 index 000000000..71d14aa22 --- /dev/null +++ b/test/protocol.js @@ -0,0 +1,28 @@ +var protocol = require('../lib/protocol'); +var expect = require('chai').expect; + +describe('protocol', function() { + describe('checkAtLeast', function() { + var FIXTURES = [ + ['1.0', '1.0', true], + ['1.1', '1.0', true], + ['1.0', '1.1', false], + ['1.0', '1', true], + ['1.10', '1.3', 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] + ]; + + FIXTURES.forEach(function(fixture) { + var is = fixture[2] ? ' is ' : ' is not '; + var 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]); + }); + }); + }); +});