sharedb 6.0.1 → 6.0.2

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/client/doc.js CHANGED
@@ -968,11 +968,11 @@ Doc.prototype._opAcknowledged = function(message) {
968
968
  }
969
969
 
970
970
  if (message[ACTIONS.fixup]) {
971
- for (var i = 0; i < message[ACTIONS.fixup].length; i++) {
972
- var fixupOp = message[ACTIONS.fixup][i];
971
+ for (var fixupIndex = 0; fixupIndex < message[ACTIONS.fixup].length; fixupIndex++) {
972
+ var fixupOp = message[ACTIONS.fixup][fixupIndex];
973
973
 
974
- for (var j = 0; j < this.pendingOps.length; j++) {
975
- var transformErr = transformX(this.pendingOps[i], fixupOp);
974
+ for (var pendingIndex = 0; pendingIndex < this.pendingOps.length; pendingIndex++) {
975
+ var transformErr = transformX(this.pendingOps[pendingIndex], fixupOp);
976
976
  if (transformErr) return this._hardRollback(transformErr);
977
977
  }
978
978
 
@@ -3,6 +3,7 @@ var projections = require('./projections');
3
3
  var ShareDBError = require('./error');
4
4
  var types = require('./types');
5
5
  var protocol = require('./protocol');
6
+ var util = require('./util');
6
7
 
7
8
  var ERROR_CODE = ShareDBError.CODES;
8
9
 
@@ -43,6 +44,7 @@ function SubmitRequest(backend, agent, index, id, op, options) {
43
44
  this.ops = [];
44
45
  this.channels = null;
45
46
  this._fixupOps = [];
47
+ this._opBeforeFixups = null;
46
48
  }
47
49
  module.exports = SubmitRequest;
48
50
 
@@ -70,6 +72,12 @@ SubmitRequest.prototype.$fixup = function(op) {
70
72
  );
71
73
  }
72
74
 
75
+ if (!this._opBeforeFixups) {
76
+ this._opBeforeFixups = this.op.create ?
77
+ {data: util.clone(this.op.create.data)} :
78
+ {op: util.clone(this.op.op)};
79
+ }
80
+
73
81
  if (this.op.create) this.op.create.data = type.apply(this.op.create.data, op);
74
82
  else this.op.op = type.compose(this.op.op, op);
75
83
 
@@ -186,8 +194,6 @@ SubmitRequest.prototype.apply = function(callback) {
186
194
  // Always set the channels before each attempt to apply. If the channels are
187
195
  // modified in a middleware and we retry, we want to reset to a new array
188
196
  this.channels = this.backend.getChannels(this.collection, this.id);
189
- this._fixupOps = [];
190
- delete this.op.m.fixup;
191
197
 
192
198
  var request = this;
193
199
  this.backend.trigger(this.backend.MIDDLEWARE_ACTIONS.apply, this.agent, this, function(err) {
@@ -254,10 +260,21 @@ SubmitRequest.prototype.retry = function(callback) {
254
260
  if (this.maxRetries != null && this.retries > this.maxRetries) {
255
261
  return callback(this.maxRetriesError());
256
262
  }
263
+ this._resetFixups();
257
264
  this.backend.emit('timing', 'submit.retry', Date.now() - this.start, this);
258
265
  this.submit(callback);
259
266
  };
260
267
 
268
+ // The fixups must be undone before the op is transformed forward again
269
+ SubmitRequest.prototype._resetFixups = function() {
270
+ this._fixupOps = [];
271
+ delete this.op.m.fixup;
272
+ if (!this._opBeforeFixups) return;
273
+ if (this.op.create) this.op.create.data = this._opBeforeFixups.data;
274
+ else this.op.op = this._opBeforeFixups.op;
275
+ this._opBeforeFixups = null;
276
+ };
277
+
261
278
  SubmitRequest.prototype._transformOp = function(ops) {
262
279
  var type = this.snapshot.type;
263
280
  for (var i = 0; i < ops.length; i++) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sharedb",
3
- "version": "6.0.1",
3
+ "version": "6.0.2",
4
4
  "description": "JSON OT database backend",
5
5
  "main": "lib/index.js",
6
6
  "dependencies": {
@@ -565,6 +565,68 @@ describe('middleware', function() {
565
565
  });
566
566
  });
567
567
 
568
+ it('transforms a pending op by multiple fixups', function(done) {
569
+ var applied = false;
570
+ backend.use('apply', function(request, next) {
571
+ if (applied) return next();
572
+ applied = true;
573
+ request.$fixup([{p: ['tricks', 0], li: 'sit'}]);
574
+ request.$fixup([{p: ['tricks', 0], li: 'stay'}]);
575
+ next();
576
+ });
577
+
578
+ var remoteConnection = backend.connect();
579
+ var remoteDoc = remoteConnection.get('dogs', 'fido');
580
+
581
+ remoteDoc.subscribe(function(error) {
582
+ if (error) return done(error);
583
+
584
+ expect(remoteDoc.data).to.eql({name: 'fido'});
585
+
586
+ remoteDoc.on('op batch', function() {
587
+ if (remoteDoc.version !== 3) return;
588
+ expect(remoteDoc.data.tricks).to.eql(['stay', 'sit', 'fetch', 'lie down']);
589
+ expect(remoteDoc.data).to.eql(doc.data);
590
+ done();
591
+ });
592
+
593
+ doc.preventCompose = true;
594
+ doc.submitOp([{p: ['tricks'], oi: ['fetch']}], errorHandler(done));
595
+ doc.submitOp([{p: ['tricks', 1], li: 'lie down'}], errorHandler(done));
596
+ });
597
+ });
598
+
599
+ it('transforms multiple pending ops by a fixup', function(done) {
600
+ var applied = false;
601
+ backend.use('apply', function(request, next) {
602
+ if (applied) return next();
603
+ applied = true;
604
+ request.$fixup([{p: ['tricks', 0], li: 'stay'}]);
605
+ next();
606
+ });
607
+
608
+ var remoteConnection = backend.connect();
609
+ var remoteDoc = remoteConnection.get('dogs', 'fido');
610
+
611
+ remoteDoc.subscribe(function(error) {
612
+ if (error) return done(error);
613
+
614
+ expect(remoteDoc.data).to.eql({name: 'fido'});
615
+
616
+ remoteDoc.on('op batch', function() {
617
+ if (remoteDoc.version !== 4) return;
618
+ expect(remoteDoc.data.tricks).to.eql(['stay', 'fetch', 'sit', 'roll over']);
619
+ expect(remoteDoc.data).to.eql(doc.data);
620
+ done();
621
+ });
622
+
623
+ doc.preventCompose = true;
624
+ doc.submitOp([{p: ['tricks'], oi: ['fetch']}], errorHandler(done));
625
+ doc.submitOp([{p: ['tricks', 1], li: 'sit'}], errorHandler(done));
626
+ doc.submitOp([{p: ['tricks', 2], li: 'roll over'}], errorHandler(done));
627
+ });
628
+ });
629
+
568
630
  it('applies a fixup to a creation op', function(done) {
569
631
  backend.use('apply', function(request, next) {
570
632
  request.$fixup([{p: ['goodBoy'], oi: true}]);
@@ -702,6 +764,79 @@ describe('middleware', function() {
702
764
  });
703
765
  });
704
766
 
767
+ it('applies the fixup once when the submit is retried', function(done) {
768
+ var remoteDoc = backend.connect().get('dogs', 'fido');
769
+
770
+ doc.submitOp([{p: ['fixups'], oi: 0}], function(error) {
771
+ if (error) return done(error);
772
+ remoteDoc.fetch(function(error) {
773
+ if (error) return done(error);
774
+
775
+ backend.use('apply', function(request, next) {
776
+ request.$fixup([{p: ['fixups'], na: 1}]);
777
+ next();
778
+ });
779
+
780
+ var commitDoc;
781
+ var commitRemoteDoc;
782
+ backend.use('commit', function(request, next) {
783
+ // Once we've forced the race, let the retry through untouched
784
+ if (commitDoc && commitRemoteDoc) return next();
785
+ if (request.op.src === doc.connection.id) commitDoc = next;
786
+ else commitRemoteDoc = next;
787
+ // Both ops have now been applied to the same snapshot, so
788
+ // committing one of them will force the other to retry
789
+ if (commitDoc && commitRemoteDoc) commitDoc();
790
+ });
791
+
792
+ doc.submitOp([{p: ['name', 0], si: 'a'}], function(error) {
793
+ if (error) return done(error);
794
+ commitRemoteDoc();
795
+ });
796
+
797
+ remoteDoc.submitOp([{p: ['name', 0], si: 'b'}], function(error) {
798
+ if (error) return done(error);
799
+ backend.db.getSnapshot('dogs', 'fido', null, null, function(error, snapshot) {
800
+ if (error) return done(error);
801
+ expect(snapshot.data.fixups).to.equal(2);
802
+ expect(snapshot.data).to.eql(remoteDoc.data);
803
+ expect(snapshot.v).to.equal(remoteDoc.version);
804
+ done();
805
+ });
806
+ });
807
+ });
808
+ });
809
+ });
810
+
811
+ it('applies the fixup once when a create is retried', function(done) {
812
+ backend.use('apply', function(request, next) {
813
+ request.$fixup([{p: ['fixups'], na: 1}]);
814
+ next();
815
+ });
816
+
817
+ // A create can only be retried if the database reports a failed commit
818
+ // without another op having landed, since a create that loses a genuine
819
+ // race is rejected during transform
820
+ var commit = backend.db.commit;
821
+ var lostRace = false;
822
+ sinon.stub(backend.db, 'commit').callsFake(function(collection, id, op, snapshot, options, callback) {
823
+ if (lostRace) return commit.call(this, collection, id, op, snapshot, options, callback);
824
+ lostRace = true;
825
+ process.nextTick(callback, null, false);
826
+ });
827
+
828
+ doc = connection.get('dogs', 'rover');
829
+ doc.create({name: 'rover', fixups: 0}, function(error) {
830
+ if (error) return done(error);
831
+ backend.db.getSnapshot('dogs', 'rover', null, null, function(error, snapshot) {
832
+ if (error) return done(error);
833
+ expect(snapshot.data.fixups).to.equal(1);
834
+ expect(snapshot.data).to.eql(doc.data);
835
+ done();
836
+ });
837
+ });
838
+ });
839
+
705
840
  it('applies two fixups', function(done) {
706
841
  backend.use('apply', function(request, next) {
707
842
  request.$fixup([{p: ['tricks', 0], li: 'sit'}]);