sharedb 5.0.4 → 5.1.1
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 +12 -3
- package/lib/backend.js +1 -0
- package/lib/client/connection.js +10 -4
- package/lib/client/doc.js +9 -0
- package/lib/error.js +1 -0
- package/lib/protocol.js +28 -0
- package/lib/submit-request.js +19 -0
- package/package.json +1 -1
- package/test/agent.js +12 -0
- package/test/client/query-subscribe.js +23 -0
- package/test/client/submit.js +113 -0
- package/test/protocol.js +31 -0
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);
|
|
@@ -565,7 +569,7 @@ Agent.prototype._querySubscribe = function(queryId, collection, query, options,
|
|
|
565
569
|
}
|
|
566
570
|
this.backend.querySubscribe(this, collection, query, options, function(err, emitter, results, extra) {
|
|
567
571
|
if (err) return finish(err);
|
|
568
|
-
if (
|
|
572
|
+
if (agent.closed) return emitter.destroy();
|
|
569
573
|
|
|
570
574
|
agent._subscribeToQuery(emitter, queryId, collection, query);
|
|
571
575
|
// No results are returned when ids are passed in as an option. Instead,
|
|
@@ -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:
|
|
792
|
-
protocolMinor:
|
|
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
package/lib/client/connection.js
CHANGED
|
@@ -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
|
|
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.
|
|
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({
|
|
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 !==
|
|
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',
|
package/lib/protocol.js
ADDED
|
@@ -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
|
+
}
|
package/lib/submit-request.js
CHANGED
|
@@ -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
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
|
});
|
|
@@ -729,6 +729,29 @@ function commonTests(options) {
|
|
|
729
729
|
connection.get('dogs', 'fido').on('error', done).create({age: 3});
|
|
730
730
|
});
|
|
731
731
|
|
|
732
|
+
it('does not reply if the agent is closed before the query returns', function(done) {
|
|
733
|
+
var backend = this.backend;
|
|
734
|
+
var connection = backend.connect();
|
|
735
|
+
var agent = connection.agent;
|
|
736
|
+
|
|
737
|
+
backend.use('query', function(request, next) {
|
|
738
|
+
backend.use('reply', function() {
|
|
739
|
+
done(new Error('unexpected reply'));
|
|
740
|
+
});
|
|
741
|
+
|
|
742
|
+
expect(agent.closed).to.be.false;
|
|
743
|
+
agent.stream.on('close', function() {
|
|
744
|
+
expect(agent.closed).to.be.true;
|
|
745
|
+
next();
|
|
746
|
+
done();
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
agent.close();
|
|
750
|
+
});
|
|
751
|
+
|
|
752
|
+
connection.createSubscribeQuery('dogs', {});
|
|
753
|
+
});
|
|
754
|
+
|
|
732
755
|
describe('passing agent.custom to the DB adapter', function() {
|
|
733
756
|
var connection;
|
|
734
757
|
var expectedArg = {
|
package/test/client/submit.js
CHANGED
|
@@ -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);
|
package/test/protocol.js
ADDED
|
@@ -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
|
+
});
|