sharedb 7.0.0 → 7.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.
@@ -519,6 +519,156 @@ describe('middleware', function() {
519
519
  });
520
520
  });
521
521
 
522
+ describe('retry', function() {
523
+ var backend;
524
+ var doc;
525
+ var remoteDoc;
526
+
527
+ beforeEach(function(done) {
528
+ backend = this.backend;
529
+ doc = backend.connect().get('dogs', 'fido');
530
+ remoteDoc = backend.connect().get('dogs', 'fido');
531
+ doc.create({age: 3}, function(error) {
532
+ if (error) return done(error);
533
+ remoteDoc.fetch(done);
534
+ });
535
+ });
536
+
537
+ // Holds both ops in 'commit' until they've been applied to the same snapshot,
538
+ // then commits the remote op first, so the local op loses the race and retries
539
+ function forceRetry(callback) {
540
+ var commitDoc;
541
+ var commitRemoteDoc;
542
+ backend.use('commit', function(request, next) {
543
+ if (commitDoc && commitRemoteDoc) return next();
544
+ if (request.op.src === doc.connection.id) commitDoc = next;
545
+ else commitRemoteDoc = next;
546
+ if (commitDoc && commitRemoteDoc) commitRemoteDoc();
547
+ });
548
+
549
+ remoteDoc.submitOp([{p: ['age'], na: 7}], function(error) {
550
+ if (error) return callback(error);
551
+ commitDoc();
552
+ });
553
+ doc.submitOp([{p: ['age'], na: 2}], callback);
554
+ }
555
+
556
+ it('is triggered between a lost commit and the next attempt', function(done) {
557
+ var actions = [];
558
+ ['submit', 'apply', 'commit', 'retry', 'afterWrite'].forEach(function(action) {
559
+ backend.use(action, function(request, next) {
560
+ if (request.op.src === doc.connection.id) actions.push(action);
561
+ next();
562
+ });
563
+ });
564
+
565
+ forceRetry(function(error) {
566
+ if (error) return done(error);
567
+ expect(actions).to.eql(['submit', 'apply', 'commit', 'retry', 'apply', 'commit', 'afterWrite']);
568
+ expect(doc.data).to.eql({age: 12});
569
+ done();
570
+ });
571
+ });
572
+
573
+ it('has the number of retries so far', function(done) {
574
+ var retries = [];
575
+ backend.use('retry', function(request, next) {
576
+ retries.push(request.retries);
577
+ next();
578
+ });
579
+
580
+ forceRetry(function(error) {
581
+ if (error) return done(error);
582
+ expect(retries).to.eql([1]);
583
+ done();
584
+ });
585
+ });
586
+
587
+ it('is triggered after the failed attempt is reset', function(done) {
588
+ backend.use('apply', function(request, next) {
589
+ if (request.op.src === doc.connection.id && !request.retries) {
590
+ request.$fixup([{p: ['age'], na: 1}]);
591
+ }
592
+ next();
593
+ });
594
+ var retryRequest;
595
+ backend.use('retry', function(request, next) {
596
+ retryRequest = {
597
+ op: request.op.op,
598
+ fixup: request.op.m.fixup,
599
+ snapshot: request.snapshot,
600
+ channels: request.channels
601
+ };
602
+ next();
603
+ });
604
+
605
+ forceRetry(function(error) {
606
+ if (error) return done(error);
607
+ expect(retryRequest).to.eql({
608
+ op: [{p: ['age'], na: 2}],
609
+ fixup: undefined,
610
+ snapshot: null,
611
+ channels: null
612
+ });
613
+ done();
614
+ });
615
+ });
616
+
617
+ it('lets middleware reset state before the next attempt', function(done) {
618
+ var snapshotsBeforeApply = [];
619
+ backend.use('apply', function(request, next) {
620
+ if (request.op.src !== doc.connection.id) return next();
621
+ if (!request.snapshotBeforeApply) {
622
+ request.snapshotBeforeApply = JSON.parse(JSON.stringify(request.snapshot.data));
623
+ }
624
+ snapshotsBeforeApply.push(request.snapshotBeforeApply);
625
+ next();
626
+ });
627
+ backend.use('retry', function(request, next) {
628
+ delete request.snapshotBeforeApply;
629
+ next();
630
+ });
631
+
632
+ forceRetry(function(error) {
633
+ if (error) return done(error);
634
+ expect(snapshotsBeforeApply).to.eql([{age: 3}, {age: 10}]);
635
+ done();
636
+ });
637
+ });
638
+
639
+ it('is not triggered if the op commits first time', function(done) {
640
+ var retry = sinon.spy(function(_request, next) {
641
+ next();
642
+ });
643
+ backend.use('retry', retry);
644
+
645
+ doc.submitOp([{p: ['age'], na: 2}], function(error) {
646
+ if (error) return done(error);
647
+ expect(retry).not.to.have.been.called;
648
+ done();
649
+ });
650
+ });
651
+
652
+ it('is not triggered once the op has used up its retries', function(done) {
653
+ backend.maxSubmitRetries = 0;
654
+ var retry = sinon.spy(function(_request, next) {
655
+ next();
656
+ });
657
+ backend.use('retry', retry);
658
+
659
+ forceRetry(function(error) {
660
+ expect(error.code).to.equal(ERROR_CODE.ERR_MAX_SUBMIT_RETRIES_EXCEEDED);
661
+ expect(retry).not.to.have.been.called;
662
+ done();
663
+ });
664
+ });
665
+
666
+ it('rejects the op with an error passed to next', function(done) {
667
+ backend.use('retry', passError);
668
+ forceRetry(getErrorTest(done));
669
+ });
670
+ });
671
+
522
672
  describe('$fixup', function() {
523
673
  var connection;
524
674
  var backend;
@@ -74,6 +74,81 @@ describe('PubSub base class', function() {
74
74
  });
75
75
  });
76
76
 
77
+ it('stays subscribed when the last stream is destroyed just after a subscribe', function(done) {
78
+ var pubsub = new PubSub();
79
+ pubsub._subscribe = sinon.spy(function(channel, callback) {
80
+ callback();
81
+ });
82
+ pubsub._unsubscribe = sinon.spy(function(channel, callback) {
83
+ callback();
84
+ });
85
+ pubsub.subscribe('x', function(err, lastStream) {
86
+ if (err) return done(err);
87
+ pubsub.subscribe('x', function(err) {
88
+ if (err) return done(err);
89
+ expect(pubsub._unsubscribe).not.to.have.been.called;
90
+ expect(pubsub._subscribe).to.have.been.calledOnce;
91
+ expect(pubsub.subscribed.x).to.equal(true);
92
+ expect(pubsub.streamsCount).to.equal(1);
93
+ done();
94
+ });
95
+ lastStream.destroy();
96
+ });
97
+ });
98
+
99
+ it('waits for an in-flight _unsubscribe before calling _subscribe', function(done) {
100
+ var pubsub = new PubSub();
101
+ var finishUnsubscribe;
102
+ pubsub._subscribe = sinon.spy(function(channel, callback) {
103
+ callback();
104
+ });
105
+ pubsub._unsubscribe = function(channel, callback) {
106
+ finishUnsubscribe = callback;
107
+ };
108
+ pubsub.subscribe('x', function(err, stream) {
109
+ if (err) return done(err);
110
+ stream.destroy();
111
+ var resubscribed = 0;
112
+ function onResubscribe(err) {
113
+ if (err) return done(err);
114
+ if (++resubscribed < 2) return;
115
+ expect(pubsub._subscribe).to.have.been.calledTwice;
116
+ expect(pubsub.streamsCount).to.equal(2);
117
+ done();
118
+ }
119
+ pubsub.subscribe('x', onResubscribe);
120
+ pubsub.subscribe('x', onResubscribe);
121
+ expect(pubsub._subscribe).to.have.been.calledOnce;
122
+ finishUnsubscribe();
123
+ });
124
+ });
125
+
126
+ it('subscribes before throwing an unhandled in-flight _unsubscribe error', function(done) {
127
+ var pubsub = new PubSub();
128
+ var finishUnsubscribe;
129
+ pubsub._subscribe = function(channel, callback) {
130
+ callback();
131
+ };
132
+ pubsub._unsubscribe = function(channel, callback) {
133
+ finishUnsubscribe = callback;
134
+ };
135
+ pubsub.subscribe('x', function(err, stream) {
136
+ if (err) return done(err);
137
+ stream.destroy();
138
+ var resubscribed = false;
139
+ pubsub.subscribe('x', function(err) {
140
+ if (err) return done(err);
141
+ resubscribed = true;
142
+ });
143
+ expect(function() {
144
+ finishUnsubscribe(new Error('unsubscribe failed'));
145
+ }).to.throw('unsubscribe failed');
146
+ expect(resubscribed).to.equal(true);
147
+ expect(pubsub.subscribed.x).to.equal(true);
148
+ done();
149
+ });
150
+ });
151
+
77
152
  it('emits an error if _unsubscribe is unimplemented', function(done) {
78
153
  var pubsub = new PubSub();
79
154
  pubsub._subscribe = function(channel, callback) {
package/test/pubsub.js CHANGED
@@ -105,6 +105,38 @@ module.exports = function(create) {
105
105
  });
106
106
  });
107
107
 
108
+ it('can resubscribe to a channel straight after destroying its last stream', function(done) {
109
+ var pubsub = this.pubsub;
110
+ pubsub.subscribe('x', function(err, stream) {
111
+ if (err) return done(err);
112
+ stream.destroy();
113
+ pubsub.subscribe('x', function(err, stream) {
114
+ if (err) return done(err);
115
+ stream.on('data', function(data) {
116
+ expect(data).eql({test: true});
117
+ done();
118
+ });
119
+ pubsub.publish(['x'], {test: true});
120
+ });
121
+ });
122
+ });
123
+
124
+ it('can subscribe to a channel just before its last stream is destroyed', function(done) {
125
+ var pubsub = this.pubsub;
126
+ pubsub.subscribe('x', function(err, lastStream) {
127
+ if (err) return done(err);
128
+ pubsub.subscribe('x', function(err, stream) {
129
+ if (err) return done(err);
130
+ stream.on('data', function(data) {
131
+ expect(data).eql({test: true});
132
+ done();
133
+ });
134
+ pubsub.publish(['x'], {test: true});
135
+ });
136
+ lastStream.destroy();
137
+ });
138
+ });
139
+
108
140
  it('stream.destroy() unsubscribes from a channel', function(done) {
109
141
  var pubsub = this.pubsub;
110
142
  pubsub.subscribe('x', function(err, stream) {