sharedb 3.2.4 → 3.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,6 +4,7 @@ var sinon = require('sinon');
4
4
  var types = require('../../lib/types');
5
5
  var deserializedType = require('./deserialized-type');
6
6
  var numberType = require('./number-type');
7
+ var errorHandler = require('../util').errorHandler;
7
8
  types.register(deserializedType.type);
8
9
  types.register(deserializedType.type2);
9
10
  types.register(numberType.type);
@@ -1210,5 +1211,52 @@ module.exports = function() {
1210
1211
  });
1211
1212
  });
1212
1213
  });
1214
+
1215
+ describe('submitting when behind the server', function() {
1216
+ var doc;
1217
+ var remoteDoc;
1218
+
1219
+ beforeEach(function(done) {
1220
+ var connection = this.backend.connect();
1221
+ doc = connection.get('dogs', 'fido');
1222
+ var remoteConnection = this.backend.connect();
1223
+ remoteDoc = remoteConnection.get('dogs', 'fido');
1224
+
1225
+ async.series([
1226
+ doc.create.bind(doc, {name: 'fido'}),
1227
+ remoteDoc.fetch.bind(remoteDoc),
1228
+ remoteDoc.submitOp.bind(remoteDoc, [{p: ['tricks'], oi: ['fetch']}]),
1229
+ function(next) {
1230
+ expect(doc.data).to.eql({name: 'fido'});
1231
+ expect(remoteDoc.data).to.eql({name: 'fido', tricks: ['fetch']});
1232
+ next();
1233
+ }
1234
+ ], done);
1235
+ });
1236
+
1237
+ it('is sent ops it has missed when submitting, without calling fetch', function(done) {
1238
+ sinon.spy(doc, 'fetch');
1239
+
1240
+ doc.submitOp([{p: ['age'], oi: 2}], errorHandler(done));
1241
+
1242
+ doc.once('op', function() {
1243
+ expect(doc.data).to.eql({name: 'fido', tricks: ['fetch'], age: 2});
1244
+ expect(doc.fetch.called).to.be.false;
1245
+ done();
1246
+ });
1247
+ });
1248
+
1249
+ it('does not expose op metadata in the middleware when sending missing ops', function(done) {
1250
+ this.backend.use('apply', function(request) {
1251
+ expect(request.ops).to.have.length(1);
1252
+ var op = request.ops[0];
1253
+ expect(op.op).to.eql([{p: ['tricks'], oi: ['fetch']}]);
1254
+ expect(op.m).to.be.undefined;
1255
+ done();
1256
+ });
1257
+
1258
+ doc.submitOp([{p: ['age'], oi: 2}], errorHandler(done));
1259
+ });
1260
+ });
1213
1261
  });
1214
1262
  };
package/test/logger.js CHANGED
@@ -8,10 +8,6 @@ describe('Logger', function() {
8
8
  sinon.stub(console, 'warn');
9
9
  });
10
10
 
11
- afterEach(function() {
12
- sinon.restore();
13
- });
14
-
15
11
  it('logs to console by default', function() {
16
12
  var logger = new Logger();
17
13
  logger.warn('warning');
@@ -3,6 +3,11 @@ var expect = require('chai').expect;
3
3
  var util = require('./util');
4
4
  var types = require('../lib/types');
5
5
  var errorHandler = util.errorHandler;
6
+ var ShareDBError = require('../lib/error');
7
+ var sinon = require('sinon');
8
+ var ACTIONS = require('../lib/message-actions').ACTIONS;
9
+
10
+ var ERROR_CODE = ShareDBError.CODES;
6
11
 
7
12
  describe('middleware', function() {
8
13
  beforeEach(function() {
@@ -417,4 +422,355 @@ describe('middleware', function() {
417
422
  doc.submitOp([{p: ['tricks', 1], li: 'stay'}], {source: 'c'}, errorHandler(done));
418
423
  });
419
424
  });
425
+
426
+ describe('$fixup', function() {
427
+ var connection;
428
+ var backend;
429
+ var doc;
430
+
431
+ beforeEach(function(done) {
432
+ backend = this.backend;
433
+ connection = backend.connect();
434
+ doc = connection.get('dogs', 'fido');
435
+
436
+ doc.create({name: 'fido'}, done);
437
+ });
438
+
439
+ it('applies a fixup op to the client that submitted it', function(done) {
440
+ backend.use('apply', function(request, next) {
441
+ request.$fixup([{p: ['tricks', 1], li: 'stay'}]);
442
+ next();
443
+ });
444
+
445
+ doc.submitOp([{p: ['tricks'], oi: ['fetch']}], function(error) {
446
+ if (error) return done(error);
447
+ expect(doc.data.tricks).to.eql(['fetch', 'stay']);
448
+ expect(doc.version).to.equal(2);
449
+ done();
450
+ });
451
+ });
452
+
453
+ it('emits an op event for the fixup', function(done) {
454
+ backend.use('apply', function(request, next) {
455
+ request.$fixup([{p: ['tricks', 1], li: 'stay'}]);
456
+ next();
457
+ });
458
+
459
+ doc.submitOp([{p: ['tricks'], oi: ['fetch']}], errorHandler(done));
460
+
461
+ doc.on('op', function() {
462
+ expect(doc.data.tricks).to.eql(['fetch', 'stay']);
463
+ done();
464
+ });
465
+ });
466
+
467
+ it('passes the fixed up op to future middleware', function(done) {
468
+ backend.use('apply', function(request, next) {
469
+ request.$fixup([{p: ['tricks', 1], li: 'stay'}]);
470
+ next();
471
+ });
472
+
473
+ backend.use('apply', function(request, next) {
474
+ expect(request.op.op).to.eql([
475
+ {p: ['tricks'], oi: ['fetch']},
476
+ {p: ['tricks', 1], li: 'stay'}
477
+ ]);
478
+ next();
479
+ });
480
+
481
+ doc.submitOp([{p: ['tricks'], oi: ['fetch']}], done);
482
+ });
483
+
484
+ it('applies the composed op to a remote client', function(done) {
485
+ backend.use('apply', function(request, next) {
486
+ request.$fixup([{p: ['tricks', 1], li: 'stay'}]);
487
+ next();
488
+ });
489
+
490
+ var remoteConnection = backend.connect();
491
+ var remoteDoc = remoteConnection.get('dogs', 'fido');
492
+
493
+ remoteDoc.subscribe(function(error) {
494
+ if (error) return done(error);
495
+
496
+ expect(remoteDoc.data).to.eql({name: 'fido'});
497
+
498
+ remoteDoc.on('op batch', function() {
499
+ expect(remoteDoc.data.tricks).to.eql(['fetch', 'stay']);
500
+ expect(doc.version).to.equal(remoteDoc.version);
501
+ done();
502
+ });
503
+
504
+ doc.submitOp([{p: ['tricks'], oi: ['fetch']}], errorHandler(done));
505
+ });
506
+ });
507
+
508
+ it('transforms pending ops by the fixup for remote clients', function(done) {
509
+ var applied = false;
510
+ backend.use('apply', function(request, next) {
511
+ if (applied) return next();
512
+ applied = true;
513
+ request.$fixup([{p: ['tricks', 0], li: 'stay'}]);
514
+ next();
515
+ });
516
+
517
+ var remoteConnection = backend.connect();
518
+ var remoteDoc = remoteConnection.get('dogs', 'fido');
519
+
520
+ remoteDoc.subscribe(function(error) {
521
+ if (error) return done(error);
522
+
523
+ expect(remoteDoc.data).to.eql({name: 'fido'});
524
+
525
+ remoteDoc.on('op batch', function() {
526
+ if (remoteDoc.version !== 3) return;
527
+ expect(remoteDoc.data.tricks).to.eql(['stay', 'fetch', 'sit']);
528
+ expect(remoteDoc.data).to.eql(doc.data);
529
+ done();
530
+ });
531
+
532
+ doc.preventCompose = true;
533
+ doc.submitOp([{p: ['tricks'], oi: ['fetch']}], errorHandler(done));
534
+ doc.submitOp([{p: ['tricks', 1], li: 'sit'}], errorHandler(done));
535
+ });
536
+ });
537
+
538
+ it('transforms pending ops by the fixup for the local doc', function(done) {
539
+ var applied = false;
540
+ backend.use('apply', function(request, next) {
541
+ if (applied) return next();
542
+ applied = true;
543
+ request.$fixup([{p: ['tricks', 0, 0], si: 'go '}]);
544
+ next();
545
+ });
546
+
547
+ var remoteConnection = backend.connect();
548
+ var remoteDoc = remoteConnection.get('dogs', 'fido');
549
+
550
+ remoteDoc.subscribe(function(error) {
551
+ if (error) return done(error);
552
+
553
+ expect(remoteDoc.data).to.eql({name: 'fido'});
554
+
555
+ remoteDoc.on('op batch', function() {
556
+ if (remoteDoc.version !== 3) return;
557
+ expect(remoteDoc.data.tricks).to.eql(['stay', 'go fetch']);
558
+ expect(remoteDoc.data).to.eql(doc.data);
559
+ done();
560
+ });
561
+
562
+ doc.preventCompose = true;
563
+ doc.submitOp([{p: ['tricks'], oi: ['fetch', 'stay']}], errorHandler(done));
564
+ doc.submitOp([{p: ['tricks', 0], lm: 1}], errorHandler(done));
565
+ });
566
+ });
567
+
568
+ it('applies a fixup to a creation op', function(done) {
569
+ backend.use('apply', function(request, next) {
570
+ request.$fixup([{p: ['goodBoy'], oi: true}]);
571
+ next();
572
+ });
573
+
574
+ doc = connection.get('dogs', 'rover');
575
+ doc.create({name: 'rover'}, function(error) {
576
+ if (error) return done(error);
577
+ expect(doc.data.goodBoy).to.be.true;
578
+ done();
579
+ });
580
+ });
581
+
582
+ it('throws an error if trying to fixup a deletion', function(done) {
583
+ backend.use('apply', function(request, next) {
584
+ var error;
585
+ try {
586
+ request.$fixup([{p: ['tricks', 0], oi: ['stay']}]);
587
+ } catch (e) {
588
+ error = e;
589
+ }
590
+ next(error);
591
+ });
592
+
593
+ doc.del(function(error) {
594
+ expect(error.code).to.equal(ERROR_CODE.ERR_CANNOT_FIXUP_DELETION);
595
+ done();
596
+ });
597
+ });
598
+
599
+ it('throws an error if trying to fixup in commit middleware', function(done) {
600
+ backend.use('commit', function(request, next) {
601
+ var error;
602
+ try {
603
+ request.$fixup([{p: ['tricks', 0], oi: ['stay']}]);
604
+ } catch (e) {
605
+ error = e;
606
+ }
607
+ next(error);
608
+ });
609
+
610
+ doc.submitOp([{p: ['goodBoy'], oi: true}], function(error) {
611
+ expect(error.code).to.equal(ERROR_CODE.ERR_FIXUP_IS_ONLY_VALID_ON_APPLY);
612
+ done();
613
+ });
614
+ });
615
+
616
+ it('retry fixup', function(done) {
617
+ var flush;
618
+ backend.use('apply', function(request, next) {
619
+ expect(request.op.m.fixup).to.be.undefined;
620
+ if (flush) return next();
621
+ flush = function() {
622
+ request.$fixup([{p: ['name', 0], si: 'fixup'}]);
623
+ next();
624
+ };
625
+ });
626
+
627
+ doc.subscribe(function(error) {
628
+ if (error) return done(error);
629
+
630
+ var remoteConnection = backend.connect();
631
+ var remoteDoc = remoteConnection.get('dogs', 'fido');
632
+
633
+ doc.submitOp([{p: ['name', 0], si: 'foo'}], function(error) {
634
+ if (error) return done(error);
635
+ expect(doc.data).to.eql({});
636
+ done();
637
+ });
638
+
639
+ remoteDoc.subscribe(function(error) {
640
+ if (error) return done(error);
641
+ remoteDoc.submitOp([{p: ['name'], od: 'fido'}], errorHandler(done));
642
+ });
643
+
644
+ doc.once('op', function(op, source) {
645
+ if (source) return;
646
+ expect(doc.data).to.eql({});
647
+ flush();
648
+ });
649
+ });
650
+ });
651
+
652
+ it('fixup that ignores no-op', function(done) {
653
+ var flush;
654
+ backend.use('apply', function(request, next) {
655
+ if (request.op.src !== connection.id) return next();
656
+ if (flush) {
657
+ request.$fixup([{p: ['name'], oi: 'fixup'}]);
658
+ return next();
659
+ }
660
+ flush = function() {
661
+ request.$fixup([{p: ['name', 0], si: 'fixup'}]);
662
+ next();
663
+ };
664
+ });
665
+
666
+ doc.subscribe(function(error) {
667
+ if (error) return done(error);
668
+
669
+ var remoteConnection = backend.connect();
670
+ var remoteDoc = remoteConnection.get('dogs', 'fido');
671
+
672
+ var count = 0;
673
+ var callback = function() {
674
+ count++;
675
+ if (count !== 2) return;
676
+ expect(doc.data).to.eql({name: 'fixup'});
677
+ expect(remoteDoc.data).to.eql(doc.data);
678
+ expect(doc.version).to.equal(remoteDoc.version);
679
+ done();
680
+ };
681
+
682
+ doc.submitOp([{p: ['name', 0], si: 'foo'}], function(error) {
683
+ if (error) return done(error);
684
+ callback();
685
+ });
686
+
687
+ remoteDoc.on('op', function(op, source) {
688
+ if (source) return;
689
+ callback();
690
+ });
691
+
692
+ remoteDoc.subscribe(function(error) {
693
+ if (error) return done(error);
694
+ remoteDoc.submitOp([{p: ['name'], od: 'fido'}], errorHandler(done));
695
+ });
696
+
697
+ doc.once('op', function(op, source) {
698
+ if (source) return;
699
+ expect(doc.data).to.eql({});
700
+ flush();
701
+ });
702
+ });
703
+ });
704
+
705
+ it('applies two fixups', function(done) {
706
+ backend.use('apply', function(request, next) {
707
+ request.$fixup([{p: ['tricks', 0], li: 'sit'}]);
708
+ next();
709
+ });
710
+
711
+ backend.use('apply', function(request, next) {
712
+ request.$fixup([{p: ['tricks', 0], li: 'stay'}]);
713
+ next();
714
+ });
715
+
716
+ doc.submitOp([{p: ['tricks'], oi: ['fetch']}], function(error) {
717
+ if (error) return done(error);
718
+ expect(doc.data.tricks).to.eql(['stay', 'sit', 'fetch']);
719
+ done();
720
+ });
721
+ });
722
+
723
+ it('rolls the doc back if the fixup cannot be applied', function(done) {
724
+ backend.use('apply', function(request, next) {
725
+ request.$fixup([{p: ['stay'], oi: true}]);
726
+ next();
727
+ });
728
+
729
+ backend.use('reply', function(request, next) {
730
+ if (request.reply[ACTIONS.fixup]) {
731
+ // Deliberately overwrite our fixup op to trigger a client rollback
732
+ request.reply[ACTIONS.fixup][0].op = [{p: ['fetch'], ld: 'bad'}];
733
+ }
734
+ next();
735
+ });
736
+
737
+ sinon.spy(doc, 'fetch');
738
+
739
+ doc.submitOp([{p: ['fetch'], oi: true}], function(error) {
740
+ expect(error).to.be.ok;
741
+ expect(doc.fetch.calledOnce).to.be.true;
742
+ done();
743
+ });
744
+ });
745
+
746
+ describe('no compose', function() {
747
+ var originalCompose;
748
+
749
+ beforeEach(function() {
750
+ originalCompose = types.defaultType.compose;
751
+ delete types.defaultType.compose;
752
+ });
753
+
754
+ afterEach(function() {
755
+ types.defaultType.compose = originalCompose.bind(types.defaultType);
756
+ });
757
+
758
+ it('throws an error if trying to compose on a type that does not support it', function(done) {
759
+ backend.use('apply', function(request, next) {
760
+ var error;
761
+ try {
762
+ request.$fixup([{p: ['tricks', 0], oi: ['stay']}]);
763
+ } catch (e) {
764
+ error = e;
765
+ }
766
+ next(error);
767
+ });
768
+
769
+ doc.submitOp([{p: ['goodBoy'], oi: true}], function(error) {
770
+ expect(error.code).to.equal(ERROR_CODE.ERR_TYPE_DOES_NOT_SUPPORT_COMPOSE);
771
+ done();
772
+ });
773
+ });
774
+ });
775
+ });
420
776
  });
@@ -576,8 +576,10 @@ module.exports = function(options) {
576
576
  });
577
577
 
578
578
  it('only stores even-numbered versions', function(done) {
579
- db.on('save', function(collection, snapshot) {
580
- if (snapshot.v !== 4) return;
579
+ var snapshotCount = 0;
580
+ db.on('save', function() {
581
+ snapshotCount++;
582
+ if (snapshotCount < 2) return;
581
583
 
582
584
  async.waterfall([
583
585
  db.getMilestoneSnapshot.bind(db, 'books', 'catcher-in-the-rye', 1),
@@ -619,8 +621,10 @@ module.exports = function(options) {
619
621
  callback();
620
622
  });
621
623
 
622
- db.on('save', function(collection, snapshot) {
623
- if (snapshot.v !== 4) return;
624
+ var snapshotCount = 0;
625
+ db.on('save', function() {
626
+ snapshotCount++;
627
+ if (snapshotCount < 2) return;
624
628
 
625
629
  async.waterfall([
626
630
  db.getMilestoneSnapshot.bind(db, 'books', 'catcher-in-the-rye', 1),
package/test/setup.js CHANGED
@@ -1,4 +1,5 @@
1
1
  var logger = require('../lib/logger');
2
+ var sinon = require('sinon');
2
3
 
3
4
  if (process.env.LOGGING !== 'true') {
4
5
  // Silence the logger for tests by setting all its methods to no-ops
@@ -8,3 +9,7 @@ if (process.env.LOGGING !== 'true') {
8
9
  error: function() {}
9
10
  });
10
11
  }
12
+
13
+ afterEach(function() {
14
+ sinon.restore();
15
+ });