sharedb 2.2.1 → 2.2.5
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/README.md +3 -4
- package/lib/backend.js +19 -4
- package/lib/client/doc.js +28 -13
- package/lib/client/presence/local-doc-presence.js +15 -0
- package/lib/ot.js +13 -1
- package/package.json +1 -1
- package/test/client/doc.js +30 -0
- package/test/client/presence/doc-presence.js +46 -0
- package/test/client/snapshot-timestamp-request.js +48 -0
- package/test/client/snapshot-version-request.js +100 -0
- package/test/ot.js +225 -93
package/README.md
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
_This README is for `sharedb@1.x`. For `sharedb@1.x-beta`, see [the 1.x-beta branch](https://github.com/share/sharedb/tree/1.x-beta). To upgrade, see [the upgrade guide](https://github.com/share/sharedb/wiki/Upgrading-to-sharedb@1.0.0-from-1.0.0-beta)._
|
|
2
|
-
|
|
3
1
|
# ShareDB
|
|
4
2
|
|
|
5
3
|
[](https://npmjs.org/package/sharedb)
|
|
@@ -11,8 +9,9 @@ ShareDB is a realtime database backend based on [Operational Transformation
|
|
|
11
9
|
documents. It is the realtime backend for the [DerbyJS web application
|
|
12
10
|
framework](http://derbyjs.com/).
|
|
13
11
|
|
|
14
|
-
For questions, discussion and announcements, join the [ShareJS mailing
|
|
15
|
-
list](https://groups.google.com/forum/?fromgroups#!forum/sharejs) or [
|
|
12
|
+
For help, questions, discussion and announcements, join the [ShareJS mailing
|
|
13
|
+
list](https://groups.google.com/forum/?fromgroups#!forum/sharejs) or [read the documentation](https://share.github.io/sharedb/
|
|
14
|
+
).
|
|
16
15
|
|
|
17
16
|
Please report any bugs you find to the [issue
|
|
18
17
|
tracker](https://github.com/share/sharedb/issues).
|
package/lib/backend.js
CHANGED
|
@@ -772,6 +772,16 @@ Backend.prototype._fetchSnapshot = function(collection, id, version, callback) {
|
|
|
772
772
|
var db = this.db;
|
|
773
773
|
var backend = this;
|
|
774
774
|
|
|
775
|
+
var shouldGetLatestSnapshot = version === null;
|
|
776
|
+
if (shouldGetLatestSnapshot) {
|
|
777
|
+
return backend.db.getSnapshot(collection, id, null, null, function(error, snapshot) {
|
|
778
|
+
if (error) return callback(error);
|
|
779
|
+
|
|
780
|
+
callback(null, snapshot);
|
|
781
|
+
});
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
|
|
775
785
|
this.milestoneDb.getMilestoneSnapshot(collection, id, version, function(error, milestoneSnapshot) {
|
|
776
786
|
if (error) return callback(error);
|
|
777
787
|
|
|
@@ -833,6 +843,15 @@ Backend.prototype._fetchSnapshotByTimestamp = function(collection, id, timestamp
|
|
|
833
843
|
var from = 0;
|
|
834
844
|
var to = null;
|
|
835
845
|
|
|
846
|
+
var shouldGetLatestSnapshot = timestamp === null;
|
|
847
|
+
if (shouldGetLatestSnapshot) {
|
|
848
|
+
return backend.db.getSnapshot(collection, id, null, null, function(error, snapshot) {
|
|
849
|
+
if (error) return callback(error);
|
|
850
|
+
|
|
851
|
+
callback(null, snapshot);
|
|
852
|
+
});
|
|
853
|
+
}
|
|
854
|
+
|
|
836
855
|
milestoneDb.getMilestoneSnapshotAtOrBeforeTime(collection, id, timestamp, function(error, snapshot) {
|
|
837
856
|
if (error) return callback(error);
|
|
838
857
|
milestoneSnapshot = snapshot;
|
|
@@ -883,10 +902,6 @@ function pluckIds(snapshots) {
|
|
|
883
902
|
}
|
|
884
903
|
|
|
885
904
|
function filterOpsInPlaceBeforeTimestamp(ops, timestamp) {
|
|
886
|
-
if (timestamp === null) {
|
|
887
|
-
return;
|
|
888
|
-
}
|
|
889
|
-
|
|
890
905
|
for (var i = 0; i < ops.length; i++) {
|
|
891
906
|
var op = ops[i];
|
|
892
907
|
var opTimestamp = op.m && op.m.ts;
|
package/lib/client/doc.js
CHANGED
|
@@ -114,6 +114,10 @@ function Doc(connection, collection, id) {
|
|
|
114
114
|
// ops are still received. Should be toggled through the pause() and
|
|
115
115
|
// resume() methods to correctly flush on resume.
|
|
116
116
|
this.paused = false;
|
|
117
|
+
|
|
118
|
+
// Internal counter that gets incremented every time doc.data is updated.
|
|
119
|
+
// Used as a cheap way to check if doc.data has changed.
|
|
120
|
+
this._dataStateVersion = 0;
|
|
117
121
|
}
|
|
118
122
|
emitter.mixin(Doc);
|
|
119
123
|
|
|
@@ -156,13 +160,18 @@ Doc.prototype._setType = function(newType) {
|
|
|
156
160
|
} else if (newType === null) {
|
|
157
161
|
this.type = newType;
|
|
158
162
|
// If we removed the type from the object, also remove its data
|
|
159
|
-
this.
|
|
163
|
+
this._setData(undefined);
|
|
160
164
|
} else {
|
|
161
165
|
var err = new ShareDBError(ERROR_CODE.ERR_DOC_TYPE_NOT_RECOGNIZED, 'Missing type ' + newType);
|
|
162
166
|
return this.emit('error', err);
|
|
163
167
|
}
|
|
164
168
|
};
|
|
165
169
|
|
|
170
|
+
Doc.prototype._setData = function(data) {
|
|
171
|
+
this.data = data;
|
|
172
|
+
this._dataStateVersion++;
|
|
173
|
+
};
|
|
174
|
+
|
|
166
175
|
// Ingest snapshot data. This data must include a version, snapshot and type.
|
|
167
176
|
// This is used both to ingest data that was exported with a webpage and data
|
|
168
177
|
// that was received from the server during a fetch.
|
|
@@ -218,9 +227,11 @@ Doc.prototype.ingestSnapshot = function(snapshot, callback) {
|
|
|
218
227
|
this.version = snapshot.v;
|
|
219
228
|
var type = (snapshot.type === undefined) ? types.defaultType : snapshot.type;
|
|
220
229
|
this._setType(type);
|
|
221
|
-
this.
|
|
222
|
-
this.type.deserialize
|
|
223
|
-
|
|
230
|
+
this._setData(
|
|
231
|
+
(this.type && this.type.deserialize) ?
|
|
232
|
+
this.type.deserialize(snapshot.data) :
|
|
233
|
+
snapshot.data
|
|
234
|
+
);
|
|
224
235
|
this.emit('load');
|
|
225
236
|
callback && callback();
|
|
226
237
|
};
|
|
@@ -613,6 +624,8 @@ Doc.prototype._otApply = function(op, source) {
|
|
|
613
624
|
for (var i = 0; i < op.op.length; i++) {
|
|
614
625
|
var component = op.op[i];
|
|
615
626
|
var componentOp = {op: [component]};
|
|
627
|
+
// Apply the individual op component
|
|
628
|
+
this.emit('before op', componentOp.op, source, op.src);
|
|
616
629
|
// Transform componentOp against any ops that have been submitted
|
|
617
630
|
// sychronously inside of an op event handler since we began apply of
|
|
618
631
|
// our operation
|
|
@@ -620,9 +633,7 @@ Doc.prototype._otApply = function(op, source) {
|
|
|
620
633
|
var transformErr = transformX(this.applyStack[j], componentOp);
|
|
621
634
|
if (transformErr) return this._hardRollback(transformErr);
|
|
622
635
|
}
|
|
623
|
-
|
|
624
|
-
this.emit('before op', componentOp.op, source, op.src);
|
|
625
|
-
this.data = this.type.apply(this.data, componentOp.op);
|
|
636
|
+
this._setData(this.type.apply(this.data, componentOp.op));
|
|
626
637
|
this.emit('op', componentOp.op, source, op.src);
|
|
627
638
|
}
|
|
628
639
|
this.emit('op batch', op.op, source);
|
|
@@ -635,7 +646,7 @@ Doc.prototype._otApply = function(op, source) {
|
|
|
635
646
|
// the snapshot before it gets changed
|
|
636
647
|
this.emit('before op', op.op, source, op.src);
|
|
637
648
|
// Apply the operation to the local data, mutating it in place
|
|
638
|
-
this.
|
|
649
|
+
this._setData(this.type.apply(this.data, op.op));
|
|
639
650
|
// Emit an 'op' event once the local data includes the changes from the
|
|
640
651
|
// op. For locally submitted ops, this will be synchronously with
|
|
641
652
|
// submission and before the server or other clients have received the op.
|
|
@@ -648,11 +659,15 @@ Doc.prototype._otApply = function(op, source) {
|
|
|
648
659
|
|
|
649
660
|
if (op.create) {
|
|
650
661
|
this._setType(op.create.type);
|
|
651
|
-
|
|
652
|
-
(this.type.createDeserialized)
|
|
653
|
-
this.type.createDeserialized(op.create.data)
|
|
654
|
-
|
|
655
|
-
|
|
662
|
+
if (this.type.deserialize) {
|
|
663
|
+
if (this.type.createDeserialized) {
|
|
664
|
+
this._setData(this.type.createDeserialized(op.create.data));
|
|
665
|
+
} else {
|
|
666
|
+
this._setData(this.type.deserialize(this.type.create(op.create.data)));
|
|
667
|
+
}
|
|
668
|
+
} else {
|
|
669
|
+
this._setData(this.type.create(op.create.data));
|
|
670
|
+
}
|
|
656
671
|
this.emit('create', source);
|
|
657
672
|
return;
|
|
658
673
|
}
|
|
@@ -12,6 +12,7 @@ function LocalDocPresence(presence, presenceId) {
|
|
|
12
12
|
|
|
13
13
|
this._doc = this.connection.get(this.collection, this.id);
|
|
14
14
|
this._isSending = false;
|
|
15
|
+
this._docDataVersionByPresenceVersion = {};
|
|
15
16
|
|
|
16
17
|
this._opHandler = this._transformAgainstOp.bind(this);
|
|
17
18
|
this._createOrDelHandler = this._handleCreateOrDel.bind(this);
|
|
@@ -35,6 +36,9 @@ LocalDocPresence.prototype.submit = function(value, callback) {
|
|
|
35
36
|
return this._callbackOrEmit(error, callback);
|
|
36
37
|
};
|
|
37
38
|
|
|
39
|
+
// Record the current data state version to check if we need to transform
|
|
40
|
+
// the presence later
|
|
41
|
+
this._docDataVersionByPresenceVersion[this.presenceVersion] = this._doc._dataStateVersion;
|
|
38
42
|
LocalPresence.prototype.submit.call(this, value, callback);
|
|
39
43
|
};
|
|
40
44
|
|
|
@@ -63,6 +67,7 @@ LocalDocPresence.prototype._sendPending = function() {
|
|
|
63
67
|
});
|
|
64
68
|
|
|
65
69
|
presence._pendingMessages = [];
|
|
70
|
+
presence._docDataVersionByPresenceVersion = {};
|
|
66
71
|
});
|
|
67
72
|
};
|
|
68
73
|
|
|
@@ -76,9 +81,18 @@ LocalDocPresence.prototype._registerWithDoc = function() {
|
|
|
76
81
|
|
|
77
82
|
LocalDocPresence.prototype._transformAgainstOp = function(op, source) {
|
|
78
83
|
var presence = this;
|
|
84
|
+
var docDataVersion = this._doc._dataStateVersion;
|
|
85
|
+
|
|
79
86
|
this._pendingMessages.forEach(function(message) {
|
|
87
|
+
// Check if the presence needs transforming against the op - this is to check against
|
|
88
|
+
// edge cases where presence is submitted from an 'op' event
|
|
89
|
+
var messageDocDataVersion = presence._docDataVersionByPresenceVersion[message.pv];
|
|
90
|
+
if (messageDocDataVersion >= docDataVersion) return;
|
|
80
91
|
try {
|
|
81
92
|
message.p = presence._transformPresence(message.p, op, source);
|
|
93
|
+
// Ensure the presence's data version is kept consistent to deal with "deep" op
|
|
94
|
+
// submissions
|
|
95
|
+
presence._docDataVersionByPresenceVersion[message.pv] = docDataVersion;
|
|
82
96
|
} catch (error) {
|
|
83
97
|
var callback = presence._getCallback(message.pv);
|
|
84
98
|
presence._callbackOrEmit(error, callback);
|
|
@@ -103,6 +117,7 @@ LocalDocPresence.prototype._handleCreateOrDel = function() {
|
|
|
103
117
|
LocalDocPresence.prototype._handleLoad = function() {
|
|
104
118
|
this.value = null;
|
|
105
119
|
this._pendingMessages = [];
|
|
120
|
+
this._docDataVersionByPresenceVersion = {};
|
|
106
121
|
};
|
|
107
122
|
|
|
108
123
|
LocalDocPresence.prototype._message = function() {
|
package/lib/ot.js
CHANGED
|
@@ -221,11 +221,18 @@ function normalizeLegacyJson0Ops(snapshot, json0Op) {
|
|
|
221
221
|
if (snapshot.type !== types.defaultType.uri) return;
|
|
222
222
|
var components = json0Op.op;
|
|
223
223
|
if (!components) return;
|
|
224
|
+
var data = snapshot.data;
|
|
225
|
+
|
|
226
|
+
// type.apply() makes no guarantees about mutating the original data, so
|
|
227
|
+
// we need to clone. However, we only need to apply() if we have multiple
|
|
228
|
+
// components, so avoid cloning if we don't have to.
|
|
229
|
+
if (components.length > 1) data = util.clone(data);
|
|
230
|
+
|
|
224
231
|
for (var i = 0; i < components.length; i++) {
|
|
225
232
|
var component = components[i];
|
|
226
233
|
if (typeof component.lm === 'string') component.lm = +component.lm;
|
|
227
234
|
var path = component.p;
|
|
228
|
-
var element =
|
|
235
|
+
var element = data;
|
|
229
236
|
for (var j = 0; j < path.length; j++) {
|
|
230
237
|
var key = path[j];
|
|
231
238
|
// https://github.com/ottypes/json0/blob/73db17e86adc5d801951d1a69453b01382e66c7d/lib/json0.js#L21
|
|
@@ -234,5 +241,10 @@ function normalizeLegacyJson0Ops(snapshot, json0Op) {
|
|
|
234
241
|
else if (element.constructor === Object) path[j] = key.toString();
|
|
235
242
|
element = element[key];
|
|
236
243
|
}
|
|
244
|
+
|
|
245
|
+
// Apply to update the snapshot, so we can correctly check the path for
|
|
246
|
+
// the next component. We don't need to do this on the final iteration,
|
|
247
|
+
// since there's no more ops.
|
|
248
|
+
if (i < components.length - 1) data = types.defaultType.apply(data, [component]);
|
|
237
249
|
}
|
|
238
250
|
}
|
package/package.json
CHANGED
package/test/client/doc.js
CHANGED
|
@@ -214,6 +214,36 @@ describe('Doc', function() {
|
|
|
214
214
|
});
|
|
215
215
|
});
|
|
216
216
|
|
|
217
|
+
it('remote ops are transformed by ops submitted in `before op` event handlers', function(done) {
|
|
218
|
+
var doc = this.doc;
|
|
219
|
+
var doc2 = this.doc2;
|
|
220
|
+
var doc3 = this.doc3;
|
|
221
|
+
function beforeOpHandler(op, source) {
|
|
222
|
+
if (source) {
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
doc.off('before op', beforeOpHandler);
|
|
226
|
+
doc.submitOp({p: ['list', 0], li: 2}, {source: true});
|
|
227
|
+
}
|
|
228
|
+
function opHandler(op, source) {
|
|
229
|
+
if (source) {
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
doc.off('op', opHandler);
|
|
233
|
+
doc.submitOp({p: ['list', 0], li: 3}, {source: true});
|
|
234
|
+
}
|
|
235
|
+
doc2.submitOp({p: ['list'], oi: []}, function() {
|
|
236
|
+
doc.fetch(function() {
|
|
237
|
+
doc.on('before op', beforeOpHandler);
|
|
238
|
+
doc.on('op', opHandler);
|
|
239
|
+
doc2.submitOp([{p: ['list', 0], li: 1}, {p: ['list', 1], li: 42}], function() {
|
|
240
|
+
doc.fetch();
|
|
241
|
+
verifyConsistency(doc, doc2, doc3, [], done);
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
|
|
217
247
|
it('remote multi component ops are transformed by ops submitted in `op` event handlers', function(done) {
|
|
218
248
|
var doc = this.doc;
|
|
219
249
|
var doc2 = this.doc2;
|
|
@@ -979,4 +979,50 @@ describe('DocPresence', function() {
|
|
|
979
979
|
}
|
|
980
980
|
], done);
|
|
981
981
|
});
|
|
982
|
+
|
|
983
|
+
it('does not transform presence submitted in an op event when the presence was created late', function(done) {
|
|
984
|
+
var localPresence1;
|
|
985
|
+
|
|
986
|
+
doc1.on('op', function() {
|
|
987
|
+
localPresence1.submit({index: 7});
|
|
988
|
+
});
|
|
989
|
+
|
|
990
|
+
localPresence1 = presence1.create('presence-1');
|
|
991
|
+
|
|
992
|
+
async.series([
|
|
993
|
+
presence2.subscribe.bind(presence2),
|
|
994
|
+
function(next) {
|
|
995
|
+
doc1.submitOp({index: 5, value: 'ern'});
|
|
996
|
+
presence2.on('receive', function(id, value) {
|
|
997
|
+
expect(value).to.eql({index: 7});
|
|
998
|
+
next();
|
|
999
|
+
});
|
|
1000
|
+
}
|
|
1001
|
+
], done);
|
|
1002
|
+
});
|
|
1003
|
+
|
|
1004
|
+
it('does transform late-created presence submitted in an op event by a deep second op', function(done) {
|
|
1005
|
+
var localPresence1;
|
|
1006
|
+
|
|
1007
|
+
var submitted = false;
|
|
1008
|
+
doc1.on('op', function() {
|
|
1009
|
+
if (submitted) return;
|
|
1010
|
+
submitted = true;
|
|
1011
|
+
localPresence1.submit({index: 7});
|
|
1012
|
+
doc1.submitOp({index: 5, value: 'akg'});
|
|
1013
|
+
});
|
|
1014
|
+
|
|
1015
|
+
localPresence1 = presence1.create('presence-1');
|
|
1016
|
+
|
|
1017
|
+
async.series([
|
|
1018
|
+
presence2.subscribe.bind(presence2),
|
|
1019
|
+
function(next) {
|
|
1020
|
+
doc1.submitOp({index: 5, value: 'ern'});
|
|
1021
|
+
presence2.on('receive', function(id, value) {
|
|
1022
|
+
expect(value).to.eql({index: 10});
|
|
1023
|
+
next();
|
|
1024
|
+
});
|
|
1025
|
+
}
|
|
1026
|
+
], done);
|
|
1027
|
+
});
|
|
982
1028
|
});
|
|
@@ -490,6 +490,54 @@ describe('SnapshotTimestampRequest', function() {
|
|
|
490
490
|
done();
|
|
491
491
|
});
|
|
492
492
|
});
|
|
493
|
+
|
|
494
|
+
describe('when timestamp is null', function() {
|
|
495
|
+
it('fetches latest snapshot', function(done) {
|
|
496
|
+
sinon.spy(milestoneDb, 'getMilestoneSnapshotAtOrBeforeTime');
|
|
497
|
+
sinon.spy(milestoneDb, 'getMilestoneSnapshotAtOrAfterTime');
|
|
498
|
+
sinon.spy(db, 'getOps');
|
|
499
|
+
|
|
500
|
+
backendWithMilestones.connect()
|
|
501
|
+
.fetchSnapshotByTimestamp('books', 'mocking-bird', null, function(error, snapshot) {
|
|
502
|
+
if (error) return done(error);
|
|
503
|
+
|
|
504
|
+
expect(milestoneDb.getMilestoneSnapshotAtOrBeforeTime.called).to.be.false;
|
|
505
|
+
expect(milestoneDb.getMilestoneSnapshotAtOrAfterTime.called).to.be.false;
|
|
506
|
+
expect(db.getOps.called).to.be.false;
|
|
507
|
+
|
|
508
|
+
expect(snapshot.v).to.equal(5);
|
|
509
|
+
expect(snapshot.data).to.eql({
|
|
510
|
+
title: 'To Kill a Mocking Bird',
|
|
511
|
+
author: 'Harper Lee',
|
|
512
|
+
year: 1960
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
done();
|
|
516
|
+
});
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
it('returns error if getSnapshot fails', function(done) {
|
|
520
|
+
sinon.spy(milestoneDb, 'getMilestoneSnapshotAtOrBeforeTime');
|
|
521
|
+
sinon.spy(milestoneDb, 'getMilestoneSnapshotAtOrAfterTime');
|
|
522
|
+
sinon.spy(db, 'getOps');
|
|
523
|
+
|
|
524
|
+
sinon.stub(db, 'getSnapshot').callsFake(function(_collection, _id, _fields, _options, callback) {
|
|
525
|
+
callback(new Error('TEST_ERROR'));
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
backendWithMilestones.connect()
|
|
529
|
+
.fetchSnapshotByTimestamp('books', 'mocking-bird', null, function(error) {
|
|
530
|
+
expect(error.message).to.be.equal('TEST_ERROR');
|
|
531
|
+
|
|
532
|
+
expect(milestoneDb.getMilestoneSnapshotAtOrBeforeTime.called).to.be.false;
|
|
533
|
+
expect(milestoneDb.getMilestoneSnapshotAtOrAfterTime.called).to.be.false;
|
|
534
|
+
expect(db.getOps.called).to.be.false;
|
|
535
|
+
expect(db.getSnapshot.called).to.be.true;
|
|
536
|
+
|
|
537
|
+
done();
|
|
538
|
+
});
|
|
539
|
+
});
|
|
540
|
+
});
|
|
493
541
|
});
|
|
494
542
|
});
|
|
495
543
|
});
|
|
@@ -439,6 +439,60 @@ describe('SnapshotVersionRequest', function() {
|
|
|
439
439
|
}
|
|
440
440
|
], done);
|
|
441
441
|
});
|
|
442
|
+
|
|
443
|
+
describe('with version null', function() {
|
|
444
|
+
it('fetches a latest version', function(done) {
|
|
445
|
+
var doc = backendWithMilestones.connect().get('books', 'mocking-bird');
|
|
446
|
+
|
|
447
|
+
async.waterfall([
|
|
448
|
+
doc.create.bind(doc, {title: 'To Kill a Mocking Bird'}),
|
|
449
|
+
doc.submitOp.bind(doc, {p: ['author'], oi: 'Harper Lea'}),
|
|
450
|
+
doc.submitOp.bind(doc, {p: ['author'], od: 'Harper Lea', oi: 'Harper Lee'}),
|
|
451
|
+
function(next) {
|
|
452
|
+
sinon.spy(milestoneDb, 'getMilestoneSnapshot');
|
|
453
|
+
sinon.spy(db, 'getOps');
|
|
454
|
+
backendWithMilestones.connect().fetchSnapshot('books', 'mocking-bird', null, next);
|
|
455
|
+
},
|
|
456
|
+
function(snapshot, next) {
|
|
457
|
+
expect(milestoneDb.getMilestoneSnapshot.called).to.be.false;
|
|
458
|
+
expect(db.getOps.called).to.be.false;
|
|
459
|
+
expect(snapshot.v).to.equal(3);
|
|
460
|
+
expect(snapshot.data).to.eql({
|
|
461
|
+
title: 'To Kill a Mocking Bird',
|
|
462
|
+
author: 'Harper Lee'
|
|
463
|
+
});
|
|
464
|
+
next();
|
|
465
|
+
}
|
|
466
|
+
], done);
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
it('return error if getSnapshot fails', function(done) {
|
|
470
|
+
var doc = backendWithMilestones.connect().get('books', 'mocking-bird');
|
|
471
|
+
|
|
472
|
+
async.waterfall([
|
|
473
|
+
doc.create.bind(doc, {title: 'To Kill a Mocking Bird'}),
|
|
474
|
+
doc.submitOp.bind(doc, {p: ['author'], oi: 'Harper Lea'}),
|
|
475
|
+
doc.submitOp.bind(doc, {p: ['author'], od: 'Harper Lea', oi: 'Harper Lee'}),
|
|
476
|
+
function(next) {
|
|
477
|
+
sinon.spy(milestoneDb, 'getMilestoneSnapshot');
|
|
478
|
+
sinon.spy(db, 'getOps');
|
|
479
|
+
sinon.stub(db, 'getSnapshot').callsFake(function(_collection, _id, _fields, _options, callback) {
|
|
480
|
+
callback(new Error('TEST_ERROR'));
|
|
481
|
+
});
|
|
482
|
+
backendWithMilestones.connect().fetchSnapshot('books', 'mocking-bird', null, function(error) {
|
|
483
|
+
expect(error.message).to.be.equal('TEST_ERROR');
|
|
484
|
+
next(null, null);
|
|
485
|
+
});
|
|
486
|
+
},
|
|
487
|
+
function(snapshot, next) {
|
|
488
|
+
expect(milestoneDb.getMilestoneSnapshot.called).to.be.false;
|
|
489
|
+
expect(db.getOps.called).to.be.false;
|
|
490
|
+
expect(db.getSnapshot.called).to.be.true;
|
|
491
|
+
next();
|
|
492
|
+
}
|
|
493
|
+
], done);
|
|
494
|
+
});
|
|
495
|
+
});
|
|
442
496
|
});
|
|
443
497
|
|
|
444
498
|
describe('invalid json0v2 path', function() {
|
|
@@ -496,4 +550,50 @@ describe('SnapshotVersionRequest', function() {
|
|
|
496
550
|
});
|
|
497
551
|
});
|
|
498
552
|
});
|
|
553
|
+
|
|
554
|
+
describe('invalid json0v2 path with multiple components', function() {
|
|
555
|
+
beforeEach(function(done) {
|
|
556
|
+
var doc = backend.connect().get('series', 'his-dark-materials');
|
|
557
|
+
async.series([
|
|
558
|
+
doc.create.bind(doc, [{}]),
|
|
559
|
+
doc.submitOp.bind(doc, [
|
|
560
|
+
{p: ['0', 'title'], oi: ''},
|
|
561
|
+
{p: ['0', 'title', 0], si: 'Northern Lights'}
|
|
562
|
+
])
|
|
563
|
+
], done);
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
describe('json0v1', function() {
|
|
567
|
+
it('fetches v2 with json0v1', function(done) {
|
|
568
|
+
backend.connect().fetchSnapshot('series', 'his-dark-materials', 2, function(error, snapshot) {
|
|
569
|
+
if (error) return done(error);
|
|
570
|
+
expect(snapshot.data).to.eql([{title: 'Northern Lights'}]);
|
|
571
|
+
done();
|
|
572
|
+
});
|
|
573
|
+
});
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
describe('json0v2', function() {
|
|
577
|
+
var defaultType;
|
|
578
|
+
|
|
579
|
+
beforeEach(function() {
|
|
580
|
+
defaultType = types.defaultType;
|
|
581
|
+
types.defaultType = json0v2;
|
|
582
|
+
types.register(json0v2);
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
afterEach(function() {
|
|
586
|
+
types.defaultType = defaultType;
|
|
587
|
+
types.register(defaultType);
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
it('fetches v2 with json0v2', function(done) {
|
|
591
|
+
backend.connect().fetchSnapshot('series', 'his-dark-materials', 2, function(error, snapshot) {
|
|
592
|
+
if (error) return done(error);
|
|
593
|
+
expect(snapshot.data).to.eql([{title: 'Northern Lights'}]);
|
|
594
|
+
done();
|
|
595
|
+
});
|
|
596
|
+
});
|
|
597
|
+
});
|
|
598
|
+
});
|
|
499
599
|
});
|
package/test/ot.js
CHANGED
|
@@ -373,118 +373,250 @@ describe('ot', function() {
|
|
|
373
373
|
});
|
|
374
374
|
|
|
375
375
|
describe('applyOps', function() {
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
376
|
+
describe('with normalization turned on', function() {
|
|
377
|
+
it('applies an op to a snapshot', function() {
|
|
378
|
+
var snapshot = {
|
|
379
|
+
type: 'http://sharejs.org/types/JSONv0',
|
|
380
|
+
data: {title: 'Wee Free Men'}
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
var ops = [
|
|
384
|
+
{
|
|
385
|
+
v: 1,
|
|
386
|
+
op: [{p: ['title', 0], si: 'The '}]
|
|
387
|
+
}
|
|
388
|
+
];
|
|
389
|
+
|
|
390
|
+
var error = ot.applyOps(snapshot, ops, {
|
|
391
|
+
_normalizeLegacyJson0Ops: true
|
|
392
|
+
});
|
|
393
|
+
expect(error).to.be.undefined;
|
|
394
|
+
expect(snapshot.data).to.eql({title: 'The Wee Free Men'});
|
|
395
|
+
expect(snapshot.v).to.equal(2);
|
|
396
|
+
});
|
|
381
397
|
|
|
382
|
-
|
|
383
|
-
{
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
}
|
|
387
|
-
|
|
398
|
+
it('applies multiple ops', function() {
|
|
399
|
+
var snapshot = {
|
|
400
|
+
type: 'http://sharejs.org/types/JSONv0',
|
|
401
|
+
data: {title: 'Wee Free Men'}
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
var ops = [
|
|
405
|
+
{
|
|
406
|
+
v: 1,
|
|
407
|
+
op: [{p: ['title', 0], si: 'The '}]
|
|
408
|
+
},
|
|
409
|
+
{
|
|
410
|
+
v: 2,
|
|
411
|
+
op: [{p: ['author'], oi: 'Terry Pratchett'}]
|
|
412
|
+
}
|
|
413
|
+
];
|
|
414
|
+
|
|
415
|
+
ot.applyOps(snapshot, ops, {
|
|
416
|
+
_normalizeLegacyJson0Ops: true
|
|
417
|
+
});
|
|
418
|
+
expect(snapshot.data).to.eql({
|
|
419
|
+
author: 'Terry Pratchett',
|
|
420
|
+
title: 'The Wee Free Men'
|
|
421
|
+
});
|
|
422
|
+
expect(snapshot.v).to.equal(3);
|
|
423
|
+
});
|
|
388
424
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
425
|
+
it('applies a del to a snapshot', function() {
|
|
426
|
+
var snapshot = {
|
|
427
|
+
type: 'http://sharejs.org/types/JSONv0',
|
|
428
|
+
data: {title: 'Wee Free Men'}
|
|
429
|
+
};
|
|
394
430
|
|
|
395
|
-
|
|
396
|
-
var snapshot = {
|
|
397
|
-
type: 'json0',
|
|
398
|
-
data: {title: 'Wee Free Men'}
|
|
399
|
-
};
|
|
431
|
+
var ops = [{v: 1, del: true}];
|
|
400
432
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
},
|
|
406
|
-
{
|
|
407
|
-
v: 2,
|
|
408
|
-
op: [{p: ['author'], oi: 'Terry Pratchett'}]
|
|
409
|
-
}
|
|
410
|
-
];
|
|
411
|
-
|
|
412
|
-
ot.applyOps(snapshot, ops);
|
|
413
|
-
expect(snapshot.data).to.eql({
|
|
414
|
-
author: 'Terry Pratchett',
|
|
415
|
-
title: 'The Wee Free Men'
|
|
433
|
+
ot.applyOps(snapshot, ops, {
|
|
434
|
+
_normalizeLegacyJson0Ops: true
|
|
435
|
+
});
|
|
436
|
+
expect(snapshot.data).to.be.undefined;
|
|
416
437
|
});
|
|
417
|
-
expect(snapshot.v).to.equal(3);
|
|
418
|
-
});
|
|
419
438
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
439
|
+
it('applies a create to a snapshot', function() {
|
|
440
|
+
var snapshot = {};
|
|
441
|
+
var ops = [
|
|
442
|
+
{
|
|
443
|
+
v: 1,
|
|
444
|
+
create: {
|
|
445
|
+
type: 'http://sharejs.org/types/JSONv0',
|
|
446
|
+
data: {title: 'Wee Free Men'}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
];
|
|
425
450
|
|
|
426
|
-
|
|
451
|
+
ot.applyOps(snapshot, ops, {
|
|
452
|
+
_normalizeLegacyJson0Ops: true
|
|
453
|
+
});
|
|
454
|
+
expect(snapshot.data).to.eql({title: 'Wee Free Men'});
|
|
455
|
+
});
|
|
427
456
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
457
|
+
it('returns an error if the snapshot has an unknown type', function() {
|
|
458
|
+
var snapshot = {type: 'unknown-type', data: {}};
|
|
459
|
+
var ops = [
|
|
460
|
+
{
|
|
461
|
+
v: 1,
|
|
462
|
+
op: [{p: ['title'], oi: 'Title'}]
|
|
463
|
+
}
|
|
464
|
+
];
|
|
465
|
+
var error = ot.applyOps(snapshot, ops, {
|
|
466
|
+
_normalizeLegacyJson0Ops: true
|
|
467
|
+
});
|
|
468
|
+
expect(error.code).to.equal(ERROR_CODE.ERR_DOC_TYPE_NOT_RECOGNIZED);
|
|
469
|
+
});
|
|
431
470
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
471
|
+
it('returns an error if a create op has an unknown type', function() {
|
|
472
|
+
var snapshot = {};
|
|
473
|
+
var ops = [
|
|
474
|
+
{
|
|
475
|
+
v: 1,
|
|
476
|
+
create: {
|
|
477
|
+
type: 'unknown-type',
|
|
478
|
+
data: {}
|
|
479
|
+
}
|
|
440
480
|
}
|
|
441
|
-
|
|
442
|
-
|
|
481
|
+
];
|
|
482
|
+
var error = ot.applyOps(snapshot, ops, {
|
|
483
|
+
_normalizeLegacyJson0Ops: true
|
|
484
|
+
});
|
|
485
|
+
expect(error.code).to.equal(ERROR_CODE.ERR_DOC_TYPE_NOT_RECOGNIZED);
|
|
486
|
+
});
|
|
443
487
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
488
|
+
it('catches and returns an error thrown by type.apply', function() {
|
|
489
|
+
var snapshot = {
|
|
490
|
+
type: 'http://sharejs.org/types/JSONv0',
|
|
491
|
+
data: {title: 'Wee Free Men'}
|
|
492
|
+
};
|
|
447
493
|
|
|
448
|
-
|
|
449
|
-
var snapshot = {type: 'unknown-type', data: {}};
|
|
450
|
-
var ops = [
|
|
451
|
-
{
|
|
494
|
+
var ops = [{
|
|
452
495
|
v: 1,
|
|
453
|
-
op: [{p: ['title'],
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
496
|
+
op: [{p: ['title'], li: 'not a list'}]
|
|
497
|
+
}];
|
|
498
|
+
|
|
499
|
+
var error = ot.applyOps(snapshot, ops, {
|
|
500
|
+
_normalizeLegacyJson0Ops: true
|
|
501
|
+
});
|
|
502
|
+
expect(error.code).to.equal(ERROR_CODE.ERR_OT_OP_NOT_APPLIED);
|
|
503
|
+
});
|
|
458
504
|
});
|
|
459
505
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
506
|
+
describe('with normalization turned off', function() {
|
|
507
|
+
it('applies an op to a snapshot', function() {
|
|
508
|
+
var snapshot = {
|
|
509
|
+
type: 'json0',
|
|
510
|
+
data: {title: 'Wee Free Men'}
|
|
511
|
+
};
|
|
512
|
+
|
|
513
|
+
var ops = [
|
|
514
|
+
{
|
|
515
|
+
v: 1,
|
|
516
|
+
op: [{p: ['title', 0], si: 'The '}]
|
|
468
517
|
}
|
|
469
|
-
|
|
470
|
-
];
|
|
471
|
-
var error = ot.applyOps(snapshot, ops);
|
|
472
|
-
expect(error.code).to.equal(ERROR_CODE.ERR_DOC_TYPE_NOT_RECOGNIZED);
|
|
473
|
-
});
|
|
518
|
+
];
|
|
474
519
|
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
};
|
|
520
|
+
var error = ot.applyOps(snapshot, ops);
|
|
521
|
+
expect(error).to.be.undefined;
|
|
522
|
+
expect(snapshot.data).to.eql({title: 'The Wee Free Men'});
|
|
523
|
+
expect(snapshot.v).to.equal(2);
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
it('applies multiple ops', function() {
|
|
527
|
+
var snapshot = {
|
|
528
|
+
type: 'json0',
|
|
529
|
+
data: {title: 'Wee Free Men'}
|
|
530
|
+
};
|
|
531
|
+
|
|
532
|
+
var ops = [
|
|
533
|
+
{
|
|
534
|
+
v: 1,
|
|
535
|
+
op: [{p: ['title', 0], si: 'The '}]
|
|
536
|
+
},
|
|
537
|
+
{
|
|
538
|
+
v: 2,
|
|
539
|
+
op: [{p: ['author'], oi: 'Terry Pratchett'}]
|
|
540
|
+
}
|
|
541
|
+
];
|
|
542
|
+
|
|
543
|
+
ot.applyOps(snapshot, ops);
|
|
544
|
+
expect(snapshot.data).to.eql({
|
|
545
|
+
author: 'Terry Pratchett',
|
|
546
|
+
title: 'The Wee Free Men'
|
|
547
|
+
});
|
|
548
|
+
expect(snapshot.v).to.equal(3);
|
|
549
|
+
});
|
|
480
550
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
551
|
+
it('applies a del to a snapshot', function() {
|
|
552
|
+
var snapshot = {
|
|
553
|
+
type: 'json0',
|
|
554
|
+
data: {title: 'Wee Free Men'}
|
|
555
|
+
};
|
|
485
556
|
|
|
486
|
-
|
|
487
|
-
|
|
557
|
+
var ops = [{v: 1, del: true}];
|
|
558
|
+
|
|
559
|
+
ot.applyOps(snapshot, ops);
|
|
560
|
+
expect(snapshot.data).to.be.undefined;
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
it('applies a create to a snapshot', function() {
|
|
564
|
+
var snapshot = {};
|
|
565
|
+
var ops = [
|
|
566
|
+
{
|
|
567
|
+
v: 1,
|
|
568
|
+
create: {
|
|
569
|
+
type: 'json0',
|
|
570
|
+
data: {title: 'Wee Free Men'}
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
];
|
|
574
|
+
|
|
575
|
+
ot.applyOps(snapshot, ops);
|
|
576
|
+
expect(snapshot.data).to.eql({title: 'Wee Free Men'});
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
it('returns an error if the snapshot has an unknown type', function() {
|
|
580
|
+
var snapshot = {type: 'unknown-type', data: {}};
|
|
581
|
+
var ops = [
|
|
582
|
+
{
|
|
583
|
+
v: 1,
|
|
584
|
+
op: [{p: ['title'], oi: 'Title'}]
|
|
585
|
+
}
|
|
586
|
+
];
|
|
587
|
+
var error = ot.applyOps(snapshot, ops);
|
|
588
|
+
expect(error.code).to.equal(ERROR_CODE.ERR_DOC_TYPE_NOT_RECOGNIZED);
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
it('returns an error if a create op has an unknown type', function() {
|
|
592
|
+
var snapshot = {};
|
|
593
|
+
var ops = [
|
|
594
|
+
{
|
|
595
|
+
v: 1,
|
|
596
|
+
create: {
|
|
597
|
+
type: 'unknown-type',
|
|
598
|
+
data: {}
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
];
|
|
602
|
+
var error = ot.applyOps(snapshot, ops);
|
|
603
|
+
expect(error.code).to.equal(ERROR_CODE.ERR_DOC_TYPE_NOT_RECOGNIZED);
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
it('catches and returns an error thrown by type.apply', function() {
|
|
607
|
+
var snapshot = {
|
|
608
|
+
type: 'json0',
|
|
609
|
+
data: {title: 'Wee Free Men'}
|
|
610
|
+
};
|
|
611
|
+
|
|
612
|
+
var ops = [{
|
|
613
|
+
v: 1,
|
|
614
|
+
op: [{p: ['title'], li: 'not a list'}]
|
|
615
|
+
}];
|
|
616
|
+
|
|
617
|
+
var error = ot.applyOps(snapshot, ops);
|
|
618
|
+
expect(error.code).to.equal(ERROR_CODE.ERR_OT_OP_NOT_APPLIED);
|
|
619
|
+
});
|
|
488
620
|
});
|
|
489
621
|
});
|
|
490
622
|
});
|