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.
- package/lib/agent.js +197 -99
- package/lib/backend.js +40 -9
- package/lib/client/presence/doc-presence.js +6 -0
- package/lib/client/presence/presence.js +4 -1
- package/lib/pubsub/index.js +21 -9
- package/lib/query-emitter.js +6 -6
- package/lib/submit-request.js +8 -1
- package/lib/util.js +10 -0
- package/package.json +1 -1
- package/test/agent.js +415 -0
- package/test/client/presence/doc-presence.js +861 -18
- package/test/client/presence/presence.js +582 -1
- package/test/client/query-subscribe.js +25 -0
- package/test/middleware.js +150 -0
- package/test/pubsub-memory.js +75 -0
- package/test/pubsub.js +32 -0
package/test/middleware.js
CHANGED
|
@@ -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;
|
package/test/pubsub-memory.js
CHANGED
|
@@ -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) {
|