sharedb 5.0.3 → 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 +97 -25
- 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/presence/doc-presence.js +18 -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
|
|
|
@@ -48,6 +49,10 @@ function Agent(backend, stream) {
|
|
|
48
49
|
// request if the client disconnects ungracefully. This is a
|
|
49
50
|
// map of channel -> id -> request
|
|
50
51
|
this.presenceRequests = Object.create(null);
|
|
52
|
+
// Keep track of the latest known Doc version, so that we can avoid fetching
|
|
53
|
+
// ops to transform presence if not needed
|
|
54
|
+
this.latestDocVersionStreams = Object.create(null);
|
|
55
|
+
this.latestDocVersions = Object.create(null);
|
|
51
56
|
|
|
52
57
|
// We need to track this manually to make sure we don't reply to messages
|
|
53
58
|
// after the stream was closed.
|
|
@@ -58,6 +63,8 @@ function Agent(backend, stream) {
|
|
|
58
63
|
// active, and it is passed to each middleware call
|
|
59
64
|
this.custom = Object.create(null);
|
|
60
65
|
|
|
66
|
+
this.protocol = Object.create(null);
|
|
67
|
+
|
|
61
68
|
// The first message received over the connection. Stored to warn if messages
|
|
62
69
|
// are being sent before the handshake.
|
|
63
70
|
this._firstReceivedMessage = null;
|
|
@@ -108,6 +115,12 @@ Agent.prototype._cleanup = function() {
|
|
|
108
115
|
emitter.destroy();
|
|
109
116
|
}
|
|
110
117
|
this.subscribedQueries = Object.create(null);
|
|
118
|
+
|
|
119
|
+
for (var collection in this.latestDocVersionStreams) {
|
|
120
|
+
var streams = this.latestDocVersionStreams[collection];
|
|
121
|
+
for (var id in streams) streams[id].destroy();
|
|
122
|
+
}
|
|
123
|
+
this.latestDocVersionStreams = Object.create(null);
|
|
111
124
|
};
|
|
112
125
|
|
|
113
126
|
/**
|
|
@@ -115,17 +128,8 @@ Agent.prototype._cleanup = function() {
|
|
|
115
128
|
* _sendOp()
|
|
116
129
|
*/
|
|
117
130
|
Agent.prototype._subscribeToStream = function(collection, id, stream) {
|
|
118
|
-
if (this.closed) return stream.destroy();
|
|
119
|
-
|
|
120
|
-
var streams = this.subscribedDocs[collection] || (this.subscribedDocs[collection] = Object.create(null));
|
|
121
|
-
|
|
122
|
-
// If already subscribed to this document, destroy the previously subscribed stream
|
|
123
|
-
var previous = streams[id];
|
|
124
|
-
if (previous) previous.destroy();
|
|
125
|
-
streams[id] = stream;
|
|
126
|
-
|
|
127
131
|
var agent = this;
|
|
128
|
-
|
|
132
|
+
this._subscribeMapToStream(this.subscribedDocs, collection, id, stream, function(data) {
|
|
129
133
|
if (data.error) {
|
|
130
134
|
// Log then silently ignore errors in a subscription stream, since these
|
|
131
135
|
// may not be the client's fault, and they were not the result of a
|
|
@@ -135,13 +139,26 @@ Agent.prototype._subscribeToStream = function(collection, id, stream) {
|
|
|
135
139
|
}
|
|
136
140
|
agent._onOp(collection, id, data);
|
|
137
141
|
});
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
Agent.prototype._subscribeMapToStream = function(map, collection, id, stream, dataHandler) {
|
|
145
|
+
if (this.closed) return stream.destroy();
|
|
146
|
+
|
|
147
|
+
var streams = map[collection] || (map[collection] = Object.create(null));
|
|
148
|
+
|
|
149
|
+
// If already subscribed to this document, destroy the previously subscribed stream
|
|
150
|
+
var previous = streams[id];
|
|
151
|
+
if (previous) previous.destroy();
|
|
152
|
+
streams[id] = stream;
|
|
153
|
+
|
|
154
|
+
stream.on('data', dataHandler);
|
|
138
155
|
stream.on('end', function() {
|
|
139
156
|
// The op stream is done sending, so release its reference
|
|
140
|
-
var streams =
|
|
157
|
+
var streams = map[collection];
|
|
141
158
|
if (!streams || streams[id] !== stream) return;
|
|
142
159
|
delete streams[id];
|
|
143
160
|
if (util.hasKeys(streams)) return;
|
|
144
|
-
delete
|
|
161
|
+
delete map[collection];
|
|
145
162
|
});
|
|
146
163
|
};
|
|
147
164
|
|
|
@@ -423,6 +440,7 @@ Agent.prototype._handleMessage = function(request, callback) {
|
|
|
423
440
|
switch (request.a) {
|
|
424
441
|
case ACTIONS.handshake:
|
|
425
442
|
if (request.id) this.src = request.id;
|
|
443
|
+
this._setProtocol(request);
|
|
426
444
|
return callback(null, this._initMessage(ACTIONS.handshake));
|
|
427
445
|
case ACTIONS.queryFetch:
|
|
428
446
|
return this._queryFetch(request.id, request.c, request.q, getQueryOptions(request), callback);
|
|
@@ -774,8 +792,8 @@ Agent.prototype._fetchSnapshotByTimestamp = function(collection, id, timestamp,
|
|
|
774
792
|
Agent.prototype._initMessage = function(action) {
|
|
775
793
|
return {
|
|
776
794
|
a: action,
|
|
777
|
-
protocol:
|
|
778
|
-
protocolMinor:
|
|
795
|
+
protocol: protocol.major,
|
|
796
|
+
protocolMinor: protocol.minor,
|
|
779
797
|
id: this._src(),
|
|
780
798
|
type: types.defaultType.uri
|
|
781
799
|
};
|
|
@@ -794,25 +812,74 @@ Agent.prototype._broadcastPresence = function(presence, callback) {
|
|
|
794
812
|
collection: presence.c
|
|
795
813
|
};
|
|
796
814
|
var start = Date.now();
|
|
797
|
-
|
|
815
|
+
|
|
816
|
+
var subscriptionUpdater = presence.p === null ?
|
|
817
|
+
this._unsubscribeDocVersion.bind(this) :
|
|
818
|
+
this._subscribeDocVersion.bind(this);
|
|
819
|
+
|
|
820
|
+
subscriptionUpdater(presence.c, presence.d, function(error) {
|
|
798
821
|
if (error) return callback(error);
|
|
799
|
-
|
|
800
|
-
var previousRequest = requests[presence.id];
|
|
801
|
-
if (!previousRequest || previousRequest.pv < presence.pv) {
|
|
802
|
-
presenceRequests[presence.ch][presence.id] = presence;
|
|
803
|
-
}
|
|
804
|
-
backend.transformPresenceToLatestVersion(agent, presence, function(error, presence) {
|
|
822
|
+
backend.trigger(backend.MIDDLEWARE_ACTIONS.receivePresence, agent, context, function(error) {
|
|
805
823
|
if (error) return callback(error);
|
|
806
|
-
var
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
824
|
+
var requests = presenceRequests[presence.ch] || (presenceRequests[presence.ch] = Object.create(null));
|
|
825
|
+
var previousRequest = requests[presence.id];
|
|
826
|
+
if (!previousRequest || previousRequest.pv < presence.pv) {
|
|
827
|
+
presenceRequests[presence.ch][presence.id] = presence;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
var transformer = function(agent, presence, callback) {
|
|
810
831
|
callback(null, presence);
|
|
832
|
+
};
|
|
833
|
+
|
|
834
|
+
var latestDocVersion = util.dig(agent.latestDocVersions, presence.c, presence.d);
|
|
835
|
+
var presenceIsUpToDate = presence.v === latestDocVersion;
|
|
836
|
+
if (!presenceIsUpToDate) {
|
|
837
|
+
transformer = backend.transformPresenceToLatestVersion.bind(backend);
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
transformer(agent, presence, function(error, presence) {
|
|
841
|
+
if (error) return callback(error);
|
|
842
|
+
var channel = agent._getPresenceChannel(presence.ch);
|
|
843
|
+
agent.backend.pubsub.publish([channel], presence, function(error) {
|
|
844
|
+
if (error) return callback(error);
|
|
845
|
+
backend.emit('timing', 'presence.broadcast', Date.now() - start, context);
|
|
846
|
+
callback(null, presence);
|
|
847
|
+
});
|
|
811
848
|
});
|
|
812
849
|
});
|
|
813
850
|
});
|
|
814
851
|
};
|
|
815
852
|
|
|
853
|
+
Agent.prototype._subscribeDocVersion = function(collection, id, callback) {
|
|
854
|
+
if (!collection || !id) return callback();
|
|
855
|
+
|
|
856
|
+
var latestDocVersions = this.latestDocVersions;
|
|
857
|
+
var isSubscribed = util.dig(latestDocVersions, collection, id) !== undefined;
|
|
858
|
+
if (isSubscribed) return callback();
|
|
859
|
+
|
|
860
|
+
var agent = this;
|
|
861
|
+
this.backend.subscribe(this, collection, id, null, function(error, stream, snapshot) {
|
|
862
|
+
if (error) return callback(error);
|
|
863
|
+
|
|
864
|
+
var versions = latestDocVersions[collection] || (latestDocVersions[collection] = Object.create(null));
|
|
865
|
+
versions[id] = snapshot.v;
|
|
866
|
+
|
|
867
|
+
agent._subscribeMapToStream(agent.latestDocVersionStreams, collection, id, stream, function(op) {
|
|
868
|
+
// op.v behind snapshot.v by 1
|
|
869
|
+
latestDocVersions[collection][id] = op.v + 1;
|
|
870
|
+
});
|
|
871
|
+
|
|
872
|
+
callback();
|
|
873
|
+
});
|
|
874
|
+
};
|
|
875
|
+
|
|
876
|
+
Agent.prototype._unsubscribeDocVersion = function(collection, id, callback) {
|
|
877
|
+
var stream = util.dig(this.latestDocVersionStreams, collection, id);
|
|
878
|
+
if (stream) stream.destroy();
|
|
879
|
+
util.digAndRemove(this.latestDocVersions, collection, id);
|
|
880
|
+
util.nextTick(callback);
|
|
881
|
+
};
|
|
882
|
+
|
|
816
883
|
Agent.prototype._createPresence = function(request) {
|
|
817
884
|
return {
|
|
818
885
|
a: ACTIONS.presence,
|
|
@@ -910,6 +977,11 @@ Agent.prototype._checkFirstMessage = function(request) {
|
|
|
910
977
|
}
|
|
911
978
|
};
|
|
912
979
|
|
|
980
|
+
Agent.prototype._setProtocol = function(request) {
|
|
981
|
+
this.protocol.major = request.protocol;
|
|
982
|
+
this.protocol.minor = request.protocolMinor;
|
|
983
|
+
};
|
|
984
|
+
|
|
913
985
|
function createClientOp(request, clientId) {
|
|
914
986
|
// src can be provided if it is not the same as the current agent,
|
|
915
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
|
});
|
|
@@ -5,6 +5,7 @@ var types = require('../../../lib/types');
|
|
|
5
5
|
var presenceTestType = require('./presence-test-type');
|
|
6
6
|
var errorHandler = require('../../util').errorHandler;
|
|
7
7
|
var PresencePauser = require('./presence-pauser');
|
|
8
|
+
var sinon = require('sinon');
|
|
8
9
|
types.register(presenceTestType.type);
|
|
9
10
|
|
|
10
11
|
describe('DocPresence', function() {
|
|
@@ -297,6 +298,23 @@ describe('DocPresence', function() {
|
|
|
297
298
|
], done);
|
|
298
299
|
});
|
|
299
300
|
|
|
301
|
+
it('does not call getOps() when presence is already up-to-date', function(done) {
|
|
302
|
+
var localPresence1 = presence1.create('presence-1');
|
|
303
|
+
|
|
304
|
+
async.series([
|
|
305
|
+
doc1.fetch.bind(doc1), // Ensure up-to-date
|
|
306
|
+
function(next) {
|
|
307
|
+
sinon.spy(Backend.prototype, 'getOps');
|
|
308
|
+
next();
|
|
309
|
+
},
|
|
310
|
+
localPresence1.submit.bind(localPresence1, {index: 1}),
|
|
311
|
+
function(next) {
|
|
312
|
+
expect(Backend.prototype.getOps).not.to.have.been.called;
|
|
313
|
+
next();
|
|
314
|
+
}
|
|
315
|
+
], done);
|
|
316
|
+
});
|
|
317
|
+
|
|
300
318
|
// This test case attempts to force us into a tight race condition corner case:
|
|
301
319
|
// 1. doc1 sends presence, as well as submits an op
|
|
302
320
|
// 2. doc2 receives the op first, followed by the presence, which is now out-of-date
|
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
|
+
});
|