sharedb 2.2.0 → 2.2.4

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 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
  [![NPM Version](https://img.shields.io/npm/v/sharedb.svg)](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 [check the FAQ](./docs/faq.md).
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
@@ -36,7 +36,7 @@ function Backend(options) {
36
36
  this.maxSubmitRetries = options.maxSubmitRetries || null;
37
37
  this.presenceEnabled = !!options.presence;
38
38
  this.doNotForwardSendPresenceErrorsToClient = !!options.doNotForwardSendPresenceErrorsToClient;
39
- if (!this.doNotForwardSendPresenceErrorsToClient) {
39
+ if (this.presenceEnabled && !this.doNotForwardSendPresenceErrorsToClient) {
40
40
  logger.warn(
41
41
  'Broadcasting "sendPresence" middleware errors to clients is deprecated ' +
42
42
  'and will be removed in a future release. Disable this behaviour with:\n\n' +
@@ -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.data = undefined;
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.data = (this.type && this.type.deserialize) ?
222
- this.type.deserialize(snapshot.data) :
223
- snapshot.data;
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
  };
@@ -622,7 +633,7 @@ Doc.prototype._otApply = function(op, source) {
622
633
  }
623
634
  // Apply the individual op component
624
635
  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.data = this.type.apply(this.data, op.op);
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
- this.data = (this.type.deserialize) ?
652
- (this.type.createDeserialized) ?
653
- this.type.createDeserialized(op.create.data) :
654
- this.type.deserialize(this.type.create(op.create.data)) :
655
- this.type.create(op.create.data);
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 = snapshot.data;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sharedb",
3
- "version": "2.2.0",
3
+ "version": "2.2.4",
4
4
  "description": "JSON OT database backend",
5
5
  "main": "lib/index.js",
6
6
  "dependencies": {
@@ -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
- it('applies an op to a snapshot', function() {
377
- var snapshot = {
378
- type: 'json0',
379
- data: {title: 'Wee Free Men'}
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
- var ops = [
383
- {
384
- v: 1,
385
- op: [{p: ['title', 0], si: 'The '}]
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
- var error = ot.applyOps(snapshot, ops);
390
- expect(error).to.be.undefined;
391
- expect(snapshot.data).to.eql({title: 'The Wee Free Men'});
392
- expect(snapshot.v).to.equal(2);
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
- it('applies multiple ops', function() {
396
- var snapshot = {
397
- type: 'json0',
398
- data: {title: 'Wee Free Men'}
399
- };
431
+ var ops = [{v: 1, del: true}];
400
432
 
401
- var ops = [
402
- {
403
- v: 1,
404
- op: [{p: ['title', 0], si: 'The '}]
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
- it('applies a del to a snapshot', function() {
421
- var snapshot = {
422
- type: 'json0',
423
- data: {title: 'Wee Free Men'}
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
- var ops = [{v: 1, del: true}];
451
+ ot.applyOps(snapshot, ops, {
452
+ _normalizeLegacyJson0Ops: true
453
+ });
454
+ expect(snapshot.data).to.eql({title: 'Wee Free Men'});
455
+ });
427
456
 
428
- ot.applyOps(snapshot, ops);
429
- expect(snapshot.data).to.be.undefined;
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
- it('applies a create to a snapshot', function() {
433
- var snapshot = {};
434
- var ops = [
435
- {
436
- v: 1,
437
- create: {
438
- type: 'json0',
439
- data: {title: 'Wee Free Men'}
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
- ot.applyOps(snapshot, ops);
445
- expect(snapshot.data).to.eql({title: 'Wee Free Men'});
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
- it('returns an error if the snapshot has an unknown type', function() {
449
- var snapshot = {type: 'unknown-type', data: {}};
450
- var ops = [
451
- {
494
+ var ops = [{
452
495
  v: 1,
453
- op: [{p: ['title'], oi: 'Title'}]
454
- }
455
- ];
456
- var error = ot.applyOps(snapshot, ops);
457
- expect(error.code).to.equal(ERROR_CODE.ERR_DOC_TYPE_NOT_RECOGNIZED);
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
- it('returns an error if a create op has an unknown type', function() {
461
- var snapshot = {};
462
- var ops = [
463
- {
464
- v: 1,
465
- create: {
466
- type: 'unknown-type',
467
- data: {}
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
- it('catches and returns an error thrown by type.apply', function() {
476
- var snapshot = {
477
- type: 'json0',
478
- data: {title: 'Wee Free Men'}
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
- var ops = [{
482
- v: 1,
483
- op: [{p: ['title'], li: 'not a list'}]
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
- var error = ot.applyOps(snapshot, ops);
487
- expect(error.code).to.equal(ERROR_CODE.ERR_OT_OP_NOT_APPLIED);
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
  });