sharedb 5.0.4 → 5.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/agent.js CHANGED
@@ -4,6 +4,7 @@ var logger = require('./logger');
4
4
  var ACTIONS = require('./message-actions').ACTIONS;
5
5
  var types = require('./types');
6
6
  var util = require('./util');
7
+ var protocol = require('./protocol');
7
8
 
8
9
  var ERROR_CODE = ShareDBError.CODES;
9
10
 
@@ -62,6 +63,8 @@ function Agent(backend, stream) {
62
63
  // active, and it is passed to each middleware call
63
64
  this.custom = Object.create(null);
64
65
 
66
+ this.protocol = Object.create(null);
67
+
65
68
  // The first message received over the connection. Stored to warn if messages
66
69
  // are being sent before the handshake.
67
70
  this._firstReceivedMessage = null;
@@ -437,6 +440,7 @@ Agent.prototype._handleMessage = function(request, callback) {
437
440
  switch (request.a) {
438
441
  case ACTIONS.handshake:
439
442
  if (request.id) this.src = request.id;
443
+ this._setProtocol(request);
440
444
  return callback(null, this._initMessage(ACTIONS.handshake));
441
445
  case ACTIONS.queryFetch:
442
446
  return this._queryFetch(request.id, request.c, request.q, getQueryOptions(request), callback);
@@ -788,8 +792,8 @@ Agent.prototype._fetchSnapshotByTimestamp = function(collection, id, timestamp,
788
792
  Agent.prototype._initMessage = function(action) {
789
793
  return {
790
794
  a: action,
791
- protocol: 1,
792
- protocolMinor: 1,
795
+ protocol: protocol.major,
796
+ protocolMinor: protocol.minor,
793
797
  id: this._src(),
794
798
  type: types.defaultType.uri
795
799
  };
@@ -973,6 +977,11 @@ Agent.prototype._checkFirstMessage = function(request) {
973
977
  }
974
978
  };
975
979
 
980
+ Agent.prototype._setProtocol = function(request) {
981
+ this.protocol.major = request.protocol;
982
+ this.protocol.minor = request.protocolMinor;
983
+ };
984
+
976
985
  function createClientOp(request, clientId) {
977
986
  // src can be provided if it is not the same as the current agent,
978
987
  // such as a resubmission after a reconnect, but it usually isn't needed
package/lib/backend.js CHANGED
@@ -43,6 +43,7 @@ function Backend(options) {
43
43
  'new Backend({doNotForwardSendPresenceErrorsToClient: true})\n\n'
44
44
  );
45
45
  }
46
+ this.doNotCommitNoOps = !!options.doNotCommitNoOps;
46
47
 
47
48
  // Map from event name to a list of middleware
48
49
  this.middleware = Object.create(null);
@@ -11,6 +11,7 @@ var types = require('../types');
11
11
  var util = require('../util');
12
12
  var logger = require('../logger');
13
13
  var DocPresenceEmitter = require('./presence/doc-presence-emitter');
14
+ var protocol = require('../protocol');
14
15
 
15
16
  var ERROR_CODE = ShareDBError.CODES;
16
17
 
@@ -728,16 +729,21 @@ Connection.prototype._handleSnapshotFetch = function(error, message) {
728
729
  };
729
730
 
730
731
  Connection.prototype._handleLegacyInit = function(message) {
731
- // If the minor protocol version has been set, we want to use the
732
+ // If the protocol is at least 1.1, we want to use the
732
733
  // new handshake protocol. Let's send a handshake initialize, because
733
734
  // we now know the server is ready. If we've already sent it, we'll
734
735
  // just ignore the response anyway.
735
- if (message.protocolMinor) return this._initializeHandshake();
736
+ if (protocol.checkAtLeast(message, '1.1')) return this._initializeHandshake();
736
737
  this._initialize(message);
737
738
  };
738
739
 
739
740
  Connection.prototype._initializeHandshake = function() {
740
- this.send({a: ACTIONS.handshake, id: this.id});
741
+ this.send({
742
+ a: ACTIONS.handshake,
743
+ id: this.id,
744
+ protocol: protocol.major,
745
+ protocolMinor: protocol.minor
746
+ });
741
747
  };
742
748
 
743
749
  Connection.prototype._handleHandshake = function(error, message) {
@@ -753,7 +759,7 @@ Connection.prototype._handlePingPong = function(error) {
753
759
  Connection.prototype._initialize = function(message) {
754
760
  if (this.state !== 'connecting') return;
755
761
 
756
- if (message.protocol !== 1) {
762
+ if (message.protocol !== protocol.major) {
757
763
  return this.emit('error', new ShareDBError(
758
764
  ERROR_CODE.ERR_PROTOCOL_VERSION_NOT_SUPPORTED,
759
765
  'Unsupported protocol version: ' + message.protocol
package/lib/client/doc.js CHANGED
@@ -328,6 +328,15 @@ Doc.prototype._handleSubscribe = function(error, snapshot) {
328
328
 
329
329
  Doc.prototype._handleOp = function(err, message) {
330
330
  if (err) {
331
+ if (err.code === ERROR_CODE.ERR_NO_OP && message.seq === this.inflightOp.seq) {
332
+ // Our op was a no-op, either because we submitted a no-op, or - more
333
+ // likely - because our op was transformed into a no-op by the server
334
+ // because of a similar remote op. In this case, the server has avoided
335
+ // committing the op to the database, and we should just clear the in-flight
336
+ // op and call the callbacks. However, let's first catch ourselves up to
337
+ // the remote, so that we're in a nice consistent state
338
+ return this.fetch(this._clearInflightOp.bind(this));
339
+ }
331
340
  if (this.inflightOp) {
332
341
  return this._rollback(err);
333
342
  }
package/lib/error.js CHANGED
@@ -35,6 +35,7 @@ ShareDBError.CODES = {
35
35
  ERR_MAX_SUBMIT_RETRIES_EXCEEDED: 'ERR_MAX_SUBMIT_RETRIES_EXCEEDED',
36
36
  ERR_MESSAGE_BADLY_FORMED: 'ERR_MESSAGE_BADLY_FORMED',
37
37
  ERR_MILESTONE_ARGUMENT_INVALID: 'ERR_MILESTONE_ARGUMENT_INVALID',
38
+ ERR_NO_OP: 'ERR_NO_OP',
38
39
  ERR_OP_ALREADY_SUBMITTED: 'ERR_OP_ALREADY_SUBMITTED',
39
40
  ERR_OP_NOT_ALLOWED_IN_PROJECTION: 'ERR_OP_NOT_ALLOWED_IN_PROJECTION',
40
41
  ERR_OP_SUBMIT_REJECTED: 'ERR_OP_SUBMIT_REJECTED',
@@ -0,0 +1,28 @@
1
+ module.exports = {
2
+ major: 1,
3
+ minor: 2,
4
+ checkAtLeast: checkAtLeast
5
+ };
6
+
7
+ function checkAtLeast(toCheck, checkAgainst) {
8
+ toCheck = normalizedProtocol(toCheck);
9
+ checkAgainst = normalizedProtocol(checkAgainst);
10
+ if (toCheck.major > checkAgainst.major) return true;
11
+ return toCheck.major === checkAgainst.major &&
12
+ toCheck.minor >= checkAgainst.minor;
13
+ }
14
+
15
+ function normalizedProtocol(protocol) {
16
+ if (typeof protocol === 'string') {
17
+ var segments = protocol.split('.');
18
+ protocol = {
19
+ major: segments[0],
20
+ minor: segments[1]
21
+ };
22
+ }
23
+
24
+ return {
25
+ major: +(protocol.protocol || protocol.major || 0),
26
+ minor: +(protocol.protocolMinor || protocol.minor || 0)
27
+ };
28
+ }
@@ -2,6 +2,7 @@ var ot = require('./ot');
2
2
  var projections = require('./projections');
3
3
  var ShareDBError = require('./error');
4
4
  var types = require('./types');
5
+ var protocol = require('./protocol');
5
6
 
6
7
  var ERROR_CODE = ShareDBError.CODES;
7
8
 
@@ -152,6 +153,18 @@ SubmitRequest.prototype.submit = function(callback) {
152
153
  err = request._transformOp(ops);
153
154
  if (err) return callback(err);
154
155
 
156
+ var skipNoOp = backend.doNotCommitNoOps &&
157
+ protocol.checkAtLeast(request.agent.protocol, '1.2') &&
158
+ request.op.op &&
159
+ request.op.op.length === 0;
160
+
161
+ if (skipNoOp) {
162
+ // The op is a no-op, either because it was submitted as such, or - more
163
+ // likely - because it was transformed into one. Let's avoid committing it
164
+ // and tell the client.
165
+ return callback(request.noOpError());
166
+ }
167
+
155
168
  if (op.v !== snapshot.v) {
156
169
  // This shouldn't happen, but is just a final sanity check to make
157
170
  // sure we have transformed the op to the current snapshot version
@@ -361,3 +374,9 @@ SubmitRequest.prototype.maxRetriesError = function() {
361
374
  'Op submit failed. Exceeded max submit retries of ' + this.maxRetries
362
375
  );
363
376
  };
377
+ SubmitRequest.prototype.noOpError = function() {
378
+ return new ShareDBError(
379
+ ERROR_CODE.ERR_NO_OP,
380
+ 'Op is a no-op. Skipping apply.'
381
+ );
382
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sharedb",
3
- "version": "5.0.4",
3
+ "version": "5.1.0",
4
4
  "description": "JSON OT database backend",
5
5
  "main": "lib/index.js",
6
6
  "dependencies": {
package/test/agent.js CHANGED
@@ -5,6 +5,7 @@ var StreamSocket = require('../lib/stream-socket');
5
5
  var expect = require('chai').expect;
6
6
  var ACTIONS = require('../lib/message-actions').ACTIONS;
7
7
  var Connection = require('../lib/client/connection');
8
+ var protocol = require('../lib/protocol');
8
9
  var LegacyConnection = require('sharedb-legacy/lib/client').Connection;
9
10
 
10
11
  describe('Agent', function() {
@@ -70,5 +71,16 @@ describe('Agent', function() {
70
71
  done();
71
72
  });
72
73
  });
74
+
75
+ it('records the client protocol on the agent', function(done) {
76
+ var connection = backend.connect();
77
+ connection.once('connected', function() {
78
+ expect(connection.agent.protocol).to.eql({
79
+ major: protocol.major,
80
+ minor: protocol.minor
81
+ });
82
+ done();
83
+ });
84
+ });
73
85
  });
74
86
  });
@@ -1290,6 +1290,119 @@ module.exports = function() {
1290
1290
  });
1291
1291
  });
1292
1292
 
1293
+ it('commits both of two identical ops submitted from different clients by default', function(done) {
1294
+ var backend = this.backend;
1295
+ backend.doNotCommitNoOps = false;
1296
+ var doc1 = backend.connect().get('dogs', id);
1297
+ var doc2 = backend.connect().get('dogs', id);
1298
+ var op = [{p: ['tricks', 0], ld: 'fetch'}];
1299
+
1300
+ async.series([
1301
+ doc1.create.bind(doc1, {tricks: ['fetch', 'sit']}),
1302
+ doc2.fetch.bind(doc2),
1303
+ async.parallel.bind(async.parallel, [
1304
+ doc1.submitOp.bind(doc1, op),
1305
+ doc2.submitOp.bind(doc2, op)
1306
+ ]),
1307
+ function(next) {
1308
+ expect(doc1.data).to.eql({tricks: ['sit']});
1309
+ expect(doc2.data).to.eql({tricks: ['sit']});
1310
+ next();
1311
+ },
1312
+ function(next) {
1313
+ backend.db.getOps('dogs', id, 0, null, {}, function(error, ops) {
1314
+ if (error) return next(error);
1315
+ // Expect:
1316
+ // v0: create
1317
+ // v1: update
1318
+ // v2: duplicate update
1319
+ expect(ops).to.have.length(3);
1320
+ next();
1321
+ });
1322
+ }
1323
+ ], done);
1324
+ });
1325
+
1326
+ it('only commits one of two identical ops submitted from different clients', function(done) {
1327
+ var backend = this.backend;
1328
+ backend.doNotCommitNoOps = true;
1329
+ var doc1 = backend.connect().get('dogs', id);
1330
+ var doc2 = backend.connect().get('dogs', id);
1331
+ var op = [{p: ['tricks', 0], ld: 'fetch'}];
1332
+
1333
+ async.series([
1334
+ doc1.create.bind(doc1, {tricks: ['fetch', 'sit']}),
1335
+ doc2.fetch.bind(doc2),
1336
+ async.parallel.bind(async.parallel, [
1337
+ doc1.submitOp.bind(doc1, op),
1338
+ doc2.submitOp.bind(doc2, op)
1339
+ ]),
1340
+ function(next) {
1341
+ expect(doc1.data).to.eql({tricks: ['sit']});
1342
+ expect(doc2.data).to.eql({tricks: ['sit']});
1343
+ next();
1344
+ },
1345
+ function(next) {
1346
+ backend.db.getOps('dogs', id, 0, null, {}, function(error, ops) {
1347
+ if (error) return next(error);
1348
+ // Expect:
1349
+ // v0: create
1350
+ // v1: update
1351
+ // no duplicate update
1352
+ expect(ops).to.have.length(2);
1353
+ next();
1354
+ });
1355
+ }
1356
+ ], done);
1357
+ });
1358
+
1359
+ it('can submit a new op after getting a no-op', function(done) {
1360
+ var backend = this.backend;
1361
+ backend.doNotCommitNoOps = true;
1362
+ var doc1 = backend.connect().get('dogs', id);
1363
+ var doc2 = backend.connect().get('dogs', id);
1364
+ var op = [{p: ['tricks', 0], ld: 'fetch'}];
1365
+
1366
+ async.series([
1367
+ doc1.create.bind(doc1, {tricks: ['fetch', 'sit']}),
1368
+ doc2.fetch.bind(doc2),
1369
+ async.parallel.bind(async.parallel, [
1370
+ doc1.submitOp.bind(doc1, op),
1371
+ doc2.submitOp.bind(doc2, op)
1372
+ ]),
1373
+ function(next) {
1374
+ expect(doc1.data).to.eql({tricks: ['sit']});
1375
+ expect(doc2.data).to.eql({tricks: ['sit']});
1376
+ next();
1377
+ },
1378
+ doc1.submitOp.bind(doc1, [{p: ['tricks', 0], li: 'play dead'}]),
1379
+ function(next) {
1380
+ expect(doc1.data.tricks).to.eql(['play dead', 'sit']);
1381
+ next();
1382
+ }
1383
+ ], done);
1384
+ });
1385
+
1386
+ it('fixes up even if an op is fixed up to become a no-op', function(done) {
1387
+ var backend = this.backend;
1388
+ backend.doNotCommitNoOps = true;
1389
+ var doc = backend.connect().get('dogs', id);
1390
+
1391
+ backend.use('apply', function(req, next) {
1392
+ req.$fixup([{p: ['fixme'], od: true}]);
1393
+ next();
1394
+ });
1395
+
1396
+ async.series([
1397
+ doc.create.bind(doc, {}),
1398
+ doc.submitOp.bind(doc, [{p: ['fixme'], oi: true}]),
1399
+ function(next) {
1400
+ expect(doc.data).to.eql({});
1401
+ next();
1402
+ }
1403
+ ], done);
1404
+ });
1405
+
1293
1406
  describe('type.deserialize', function() {
1294
1407
  it('can create a new doc', function(done) {
1295
1408
  var doc = this.backend.connect().get('dogs', id);
@@ -0,0 +1,31 @@
1
+ var protocol = require('../lib/protocol');
2
+ var expect = require('chai').expect;
3
+
4
+ describe('protocol', function() {
5
+ describe('checkAtLeast', function() {
6
+ var FIXTURES = [
7
+ ['1.0', '1.0', true],
8
+ ['1.1', '1.0', true],
9
+ ['1.0', '1.1', false],
10
+ ['1.0', '1', true],
11
+ ['1.10', '1.3', true],
12
+ ['2.0', '1.3', true],
13
+ [{major: 1, minor: 0}, {major: 1, minor: 0}, true],
14
+ [{major: 1, minor: 1}, {major: 1, minor: 0}, true],
15
+ [{major: 1, minor: 0}, {major: 1, minor: 1}, false],
16
+ [{protocol: 1, protocolMinor: 0}, {protocol: 1, protocolMinor: 0}, true],
17
+ [{protocol: 1, protocolMinor: 1}, {protocol: 1, protocolMinor: 0}, true],
18
+ [{protocol: 1, protocolMinor: 0}, {protocol: 1, protocolMinor: 1}, false],
19
+ [{}, '1.0', false],
20
+ ['', '1.0', false]
21
+ ];
22
+
23
+ FIXTURES.forEach(function(fixture) {
24
+ var is = fixture[2] ? ' is ' : ' is not ';
25
+ var name = 'checks ' + JSON.stringify(fixture[0]) + is + 'at least ' + JSON.stringify(fixture[1]);
26
+ it(name, function() {
27
+ expect(protocol.checkAtLeast(fixture[0], fixture[1])).to.equal(fixture[2]);
28
+ });
29
+ });
30
+ });
31
+ });