sharedb 7.0.1 → 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 +12 -11
- package/lib/backend.js +38 -9
- package/package.json +1 -1
- package/test/agent.js +58 -0
- package/test/client/presence/doc-presence.js +358 -0
package/lib/agent.js
CHANGED
|
@@ -188,7 +188,9 @@ Agent.prototype._subscribeToPresenceStream = function(channel, stream) {
|
|
|
188
188
|
stream.on('data', function(data) {
|
|
189
189
|
if (data.error) {
|
|
190
190
|
logger.error('Presence subscription stream error', channel, data.error);
|
|
191
|
+
return;
|
|
191
192
|
}
|
|
193
|
+
if (!isPresenceOrRequestFor(channel, data)) return;
|
|
192
194
|
agent._handlePresenceData(data);
|
|
193
195
|
});
|
|
194
196
|
|
|
@@ -199,6 +201,12 @@ Agent.prototype._subscribeToPresenceStream = function(channel, stream) {
|
|
|
199
201
|
});
|
|
200
202
|
};
|
|
201
203
|
|
|
204
|
+
function isPresenceOrRequestFor(channel, data) {
|
|
205
|
+
var isPresence = data.a === ACTIONS.presence;
|
|
206
|
+
var isPresenceRequest = !!data.r;
|
|
207
|
+
return (isPresence || isPresenceRequest) && data.ch === channel;
|
|
208
|
+
}
|
|
209
|
+
|
|
202
210
|
Agent.prototype._retractPresences = function(channel) {
|
|
203
211
|
var pendingBroadcasts = this._pendingPresenceBroadcasts;
|
|
204
212
|
for (var i = 0; i < pendingBroadcasts.length; i++) {
|
|
@@ -486,6 +494,7 @@ Agent.prototype._checkRequest = function(request) {
|
|
|
486
494
|
if (request.a === ACTIONS.presence) {
|
|
487
495
|
if (isInvalidName(request.id)) return 'Invalid presence ID';
|
|
488
496
|
if (isInvalidVersion(request.pv)) return 'Invalid presence version';
|
|
497
|
+
if (isDocPresence(request) && !util.isInteger(request.v)) return 'Invalid version';
|
|
489
498
|
}
|
|
490
499
|
} else if (
|
|
491
500
|
request.a === ACTIONS.bulkFetch ||
|
|
@@ -945,17 +954,8 @@ Agent.prototype._broadcastPresence = function(presence, supersededBroadcasts, ca
|
|
|
945
954
|
requests[presence.id] = presence;
|
|
946
955
|
}
|
|
947
956
|
|
|
948
|
-
var
|
|
949
|
-
|
|
950
|
-
};
|
|
951
|
-
|
|
952
|
-
var latestDocVersion = util.dig(agent.latestDocVersions, presence.c, presence.d);
|
|
953
|
-
var presenceIsUpToDate = presence.v === latestDocVersion;
|
|
954
|
-
if (!presenceIsUpToDate) {
|
|
955
|
-
transformer = backend.transformPresenceToLatestVersion.bind(backend);
|
|
956
|
-
}
|
|
957
|
-
|
|
958
|
-
transformer(agent, presence, function(error, transformedPresence) {
|
|
957
|
+
var trackedVersion = util.dig(agent.latestDocVersions, presence.c, presence.d);
|
|
958
|
+
backend._transformPresenceToLatestVersion(agent, presence, trackedVersion, function(error, transformedPresence) {
|
|
959
959
|
if (error) {
|
|
960
960
|
agent._withdrawPresenceRequest(requests, presence, previousRequest);
|
|
961
961
|
return callback(error);
|
|
@@ -1016,6 +1016,7 @@ Agent.prototype._subscribeDocVersion = function(collection, id, callback) {
|
|
|
1016
1016
|
versions[id] = snapshot.v;
|
|
1017
1017
|
|
|
1018
1018
|
agent._subscribeMapToStream(agent.latestDocVersionStreams, collection, id, stream, function(op) {
|
|
1019
|
+
if (agent._isOpFromCollidingChannel(collection, id, op)) return;
|
|
1019
1020
|
// op.v behind snapshot.v by 1
|
|
1020
1021
|
latestDocVersions[collection][id] = op.v + 1;
|
|
1021
1022
|
});
|
package/lib/backend.js
CHANGED
|
@@ -938,18 +938,47 @@ Backend.prototype._buildSnapshotFromOps = function(id, startingSnapshot, ops, ca
|
|
|
938
938
|
};
|
|
939
939
|
|
|
940
940
|
Backend.prototype.transformPresenceToLatestVersion = function(agent, presence, callback) {
|
|
941
|
+
this._transformPresenceToLatestVersion(agent, presence, null, callback);
|
|
942
|
+
};
|
|
943
|
+
|
|
944
|
+
Backend.prototype._transformPresenceToLatestVersion = function(agent, presence, trackedVersion, callback) {
|
|
941
945
|
if (!presence.c || !presence.d) return callback(null, presence);
|
|
942
|
-
|
|
946
|
+
var backend = this;
|
|
947
|
+
this._getDocVersionForPresence(agent, presence, trackedVersion, function(error, docVersion) {
|
|
943
948
|
if (error) return callback(error);
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
949
|
+
var leaving = presence.p === null;
|
|
950
|
+
if (leaving) presence.v = docVersion;
|
|
951
|
+
if (presence.v > docVersion) {
|
|
952
|
+
return callback(new ShareDBError(
|
|
953
|
+
ERROR_CODE.ERR_OP_VERSION_NEWER_THAN_CURRENT_SNAPSHOT,
|
|
954
|
+
'Presence version newer than current snapshot'
|
|
955
|
+
));
|
|
951
956
|
}
|
|
952
|
-
callback(null, presence);
|
|
957
|
+
if (presence.v === docVersion) return callback(null, presence);
|
|
958
|
+
backend.getOps(agent, presence.c, presence.d, presence.v, null, function(error, ops) {
|
|
959
|
+
if (error) return callback(error);
|
|
960
|
+
for (var i = 0; i < ops.length; i++) {
|
|
961
|
+
var op = ops[i];
|
|
962
|
+
var isOwnOp = op.src === presence.src;
|
|
963
|
+
var transformError = ot.transformPresence(presence, op, isOwnOp);
|
|
964
|
+
if (transformError) {
|
|
965
|
+
return callback(transformError);
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
callback(null, presence);
|
|
969
|
+
});
|
|
970
|
+
});
|
|
971
|
+
};
|
|
972
|
+
|
|
973
|
+
Backend.prototype._getDocVersionForPresence = function(agent, presence, trackedVersion, callback) {
|
|
974
|
+
var leaving = presence.p === null;
|
|
975
|
+
var presenceIsWithinTrackedVersion = trackedVersion != null && presence.v <= trackedVersion;
|
|
976
|
+
if (!leaving && presenceIsWithinTrackedVersion) return callback(null, trackedVersion);
|
|
977
|
+
var projection = this.projections[presence.c];
|
|
978
|
+
var collection = (projection) ? projection.target : presence.c;
|
|
979
|
+
this.db.getSnapshot(collection, presence.d, {}, {agentCustom: agent.custom}, function(error, snapshot) {
|
|
980
|
+
if (error) return callback(error);
|
|
981
|
+
callback(null, snapshot.v);
|
|
953
982
|
});
|
|
954
983
|
};
|
|
955
984
|
|
package/package.json
CHANGED
package/test/agent.js
CHANGED
|
@@ -341,6 +341,64 @@ describe('Agent', function() {
|
|
|
341
341
|
});
|
|
342
342
|
});
|
|
343
343
|
});
|
|
344
|
+
|
|
345
|
+
describe('with presence', function() {
|
|
346
|
+
beforeEach(function(done) {
|
|
347
|
+
backend.close(function(error) {
|
|
348
|
+
if (error) return done(error);
|
|
349
|
+
backend = new Backend({presence: true});
|
|
350
|
+
writer = backend.connect();
|
|
351
|
+
subscriber = backend.connect();
|
|
352
|
+
done();
|
|
353
|
+
});
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
function expectOnlyPresence(presence, callback) {
|
|
357
|
+
var messages = [];
|
|
358
|
+
subscriber.on('receive', function(request) {
|
|
359
|
+
messages.push(request.data);
|
|
360
|
+
});
|
|
361
|
+
presence.once('receive', function() {
|
|
362
|
+
expect(messages).to.have.length(1);
|
|
363
|
+
expect(messages[0].a).to.equal(ACTIONS.presence);
|
|
364
|
+
callback();
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
it('does not deliver ops from a collection whose channel matches the presence channel', function(done) {
|
|
369
|
+
var presence = subscriber.getPresence('secrets');
|
|
370
|
+
presence.subscribe(function(error) {
|
|
371
|
+
if (error) return done(error);
|
|
372
|
+
expectOnlyPresence(presence, done);
|
|
373
|
+
writer.get('$presence.secrets', 'alice').create({pin: '1234'}, function(error) {
|
|
374
|
+
if (error) return done(error);
|
|
375
|
+
writer.getPresence('secrets').create('writer').submit({name: 'Writer'}, function(error) {
|
|
376
|
+
if (error) done(error);
|
|
377
|
+
});
|
|
378
|
+
});
|
|
379
|
+
});
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
it('does not deliver ops from a doc whose channel matches the doc presence channel', function(done) {
|
|
383
|
+
var writerDoc = writer.get('notes', 'alice');
|
|
384
|
+
var subscriberDoc = subscriber.get('notes', 'alice');
|
|
385
|
+
var presence = subscriber.getDocPresence('notes', 'alice');
|
|
386
|
+
async.series([
|
|
387
|
+
writerDoc.create.bind(writerDoc, 'Hello', presenceTestType.type.name),
|
|
388
|
+
subscriberDoc.fetch.bind(subscriberDoc),
|
|
389
|
+
presence.subscribe.bind(presence),
|
|
390
|
+
function(next) {
|
|
391
|
+
expectOnlyPresence(presence, next);
|
|
392
|
+
writer.get('$presence', 'notes.alice').create({pin: '1234'}, function(error) {
|
|
393
|
+
if (error) return next(error);
|
|
394
|
+
writer.getDocPresence('notes', 'alice').create('writer').submit({index: 1}, function(error) {
|
|
395
|
+
if (error) next(error);
|
|
396
|
+
});
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
], done);
|
|
400
|
+
});
|
|
401
|
+
});
|
|
344
402
|
});
|
|
345
403
|
|
|
346
404
|
describe('cleanup', function() {
|
|
@@ -585,6 +585,327 @@ describe('DocPresence', function() {
|
|
|
585
585
|
connection1.send(message);
|
|
586
586
|
});
|
|
587
587
|
|
|
588
|
+
it('rejects a presence message with a non-integer version', function(done) {
|
|
589
|
+
var localPresence1 = presence1.create('presence-1');
|
|
590
|
+
localPresence1.on('error', function(error) {
|
|
591
|
+
expect(error.code).to.eql('ERR_MESSAGE_BADLY_FORMED');
|
|
592
|
+
done();
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
var message = localPresence1._message();
|
|
596
|
+
message.v = 1.5;
|
|
597
|
+
message.t = presenceTestType.type.uri;
|
|
598
|
+
connection1.send(message);
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
it('rejects a presence message without a version', function(done) {
|
|
602
|
+
var localPresence1 = presence1.create('presence-1');
|
|
603
|
+
localPresence1.on('error', function(error) {
|
|
604
|
+
expect(error.code).to.eql('ERR_MESSAGE_BADLY_FORMED');
|
|
605
|
+
done();
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
var message = localPresence1._message();
|
|
609
|
+
message.t = presenceTestType.type.uri;
|
|
610
|
+
connection1.send(message);
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
[
|
|
614
|
+
{description: 'without doc coordinates', c: undefined, d: undefined},
|
|
615
|
+
{description: 'for another doc', c: 'books', d: 'subtle-knife'}
|
|
616
|
+
].forEach(function(test) {
|
|
617
|
+
it('ignores remote presence on its channel ' + test.description, function(done) {
|
|
618
|
+
sinon.spy(doc2, 'fetch');
|
|
619
|
+
|
|
620
|
+
[doc2.version, doc2.version + 1].forEach(function(version, index) {
|
|
621
|
+
var message = presence1.create('presence-' + index)._message();
|
|
622
|
+
message.c = test.c;
|
|
623
|
+
message.d = test.d;
|
|
624
|
+
message.p = {index: 1};
|
|
625
|
+
message.v = version;
|
|
626
|
+
message.t = presenceTestType.type.uri;
|
|
627
|
+
connection2.handleMessage(message);
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
doc2.whenNothingPending(function() {
|
|
631
|
+
expect(presence2.remotePresences).to.be.empty;
|
|
632
|
+
expect(presence2._remotePresenceInstances).to.be.empty;
|
|
633
|
+
expect(doc2.fetch).not.to.have.been.called;
|
|
634
|
+
done();
|
|
635
|
+
});
|
|
636
|
+
});
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
it('rejects presence whose version is ahead of the doc', function(done) {
|
|
640
|
+
var localPresence1 = presence1.create('presence-1');
|
|
641
|
+
presence2.on('receive', function() {
|
|
642
|
+
done(new Error('Should not broadcast presence from the future'));
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
async.series([
|
|
646
|
+
presence2.subscribe.bind(presence2),
|
|
647
|
+
function(next) {
|
|
648
|
+
localPresence1.on('error', function(error) {
|
|
649
|
+
expect(error.code).to.equal('ERR_OP_VERSION_NEWER_THAN_CURRENT_SNAPSHOT');
|
|
650
|
+
expect(connection1.agent.presenceRequests[presence1.channel]['presence-1'].p).to.be.null;
|
|
651
|
+
next();
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
var message = localPresence1._message();
|
|
655
|
+
message.p = {index: 1};
|
|
656
|
+
message.v = doc1.version + 1;
|
|
657
|
+
message.t = presenceTestType.type.uri;
|
|
658
|
+
connection1.send(message);
|
|
659
|
+
}
|
|
660
|
+
], done);
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
it('rejects presence ahead of a tracked doc', function(done) {
|
|
664
|
+
var localPresence1 = presence1.create('presence-1');
|
|
665
|
+
|
|
666
|
+
async.series([
|
|
667
|
+
localPresence1.submit.bind(localPresence1, {index: 1}),
|
|
668
|
+
function(next) {
|
|
669
|
+
localPresence1.on('error', function(error) {
|
|
670
|
+
expect(error.code).to.equal('ERR_OP_VERSION_NEWER_THAN_CURRENT_SNAPSHOT');
|
|
671
|
+
next();
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
var message = localPresence1._message();
|
|
675
|
+
message.p = {index: 1};
|
|
676
|
+
message.v = doc1.version + 1;
|
|
677
|
+
message.t = presenceTestType.type.uri;
|
|
678
|
+
connection1.send(message);
|
|
679
|
+
}
|
|
680
|
+
], done);
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
it('rejects presence ahead of a tracked doc after ops to a doc on the same channel', function(done) {
|
|
684
|
+
var notesDoc1 = connection1.get('notes', 'private.alice');
|
|
685
|
+
var diaryDoc1 = connection1.get('notes.private', 'alice');
|
|
686
|
+
var diaryDoc2 = connection2.get('notes.private', 'alice');
|
|
687
|
+
var localNotesPresence1 = connection1.getDocPresence('notes', 'private.alice').create('notes-presence');
|
|
688
|
+
|
|
689
|
+
async.series([
|
|
690
|
+
notesDoc1.create.bind(notesDoc1, 'Hello', presenceTestType.type.name),
|
|
691
|
+
localNotesPresence1.submit.bind(localNotesPresence1, {index: 1}),
|
|
692
|
+
diaryDoc2.create.bind(diaryDoc2, 'Dear diary', presenceTestType.type.name),
|
|
693
|
+
diaryDoc1.subscribe.bind(diaryDoc1),
|
|
694
|
+
function(next) {
|
|
695
|
+
diaryDoc1.once('op', function() {
|
|
696
|
+
next();
|
|
697
|
+
});
|
|
698
|
+
diaryDoc2.submitOp({index: 0, value: 'Oh, '}, errorHandler(done));
|
|
699
|
+
},
|
|
700
|
+
function(next) {
|
|
701
|
+
localNotesPresence1.on('error', function(error) {
|
|
702
|
+
expect(error.code).to.equal('ERR_OP_VERSION_NEWER_THAN_CURRENT_SNAPSHOT');
|
|
703
|
+
next();
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
expect(diaryDoc2.version).to.be.above(notesDoc1.version);
|
|
707
|
+
var message = localNotesPresence1._message();
|
|
708
|
+
message.p = {index: 1};
|
|
709
|
+
message.v = diaryDoc2.version;
|
|
710
|
+
message.t = presenceTestType.type.uri;
|
|
711
|
+
connection1.send(message);
|
|
712
|
+
}
|
|
713
|
+
], done);
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
it('does not read the snapshot for presence after its own client\'s op', function(done) {
|
|
717
|
+
var localPresence1 = presence1.create('presence-1');
|
|
718
|
+
|
|
719
|
+
async.series([
|
|
720
|
+
presence2.subscribe.bind(presence2),
|
|
721
|
+
function(next) {
|
|
722
|
+
localPresence1.submit({index: 1}, errorHandler(done));
|
|
723
|
+
presence2.once('receive', function() {
|
|
724
|
+
next();
|
|
725
|
+
});
|
|
726
|
+
},
|
|
727
|
+
function(next) {
|
|
728
|
+
doc2.once('op', function() {
|
|
729
|
+
next();
|
|
730
|
+
});
|
|
731
|
+
doc1.submitOp({index: 0, value: 'The '}, errorHandler(done));
|
|
732
|
+
},
|
|
733
|
+
function(next) {
|
|
734
|
+
sinon.spy(backend.db, 'getSnapshot');
|
|
735
|
+
localPresence1.submit({index: 6}, errorHandler(done));
|
|
736
|
+
presence2.once('receive', function(id, presence) {
|
|
737
|
+
expect(presence).to.eql({index: 6});
|
|
738
|
+
expect(backend.db.getSnapshot).not.to.have.been.called;
|
|
739
|
+
next();
|
|
740
|
+
});
|
|
741
|
+
}
|
|
742
|
+
], done);
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
it('does not read the snapshot for presence at the tracked doc version', function(done) {
|
|
746
|
+
var localPresence1 = presence1.create('presence-1');
|
|
747
|
+
|
|
748
|
+
async.series([
|
|
749
|
+
presence2.subscribe.bind(presence2),
|
|
750
|
+
function(next) {
|
|
751
|
+
localPresence1.submit({index: 1}, errorHandler(done));
|
|
752
|
+
presence2.once('receive', function() {
|
|
753
|
+
next();
|
|
754
|
+
});
|
|
755
|
+
},
|
|
756
|
+
function(next) {
|
|
757
|
+
sinon.spy(backend.db, 'getSnapshot');
|
|
758
|
+
localPresence1.submit({index: 2}, errorHandler(done));
|
|
759
|
+
presence2.once('receive', function(id, presence) {
|
|
760
|
+
expect(presence).to.eql({index: 2});
|
|
761
|
+
expect(backend.db.getSnapshot).not.to.have.been.called;
|
|
762
|
+
next();
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
], done);
|
|
766
|
+
});
|
|
767
|
+
|
|
768
|
+
it('clamps a leave ahead of the doc to the doc version', function(done) {
|
|
769
|
+
var localPresence1 = presence1.create('presence-1');
|
|
770
|
+
localPresence1.on('error', done);
|
|
771
|
+
sinon.spy(doc2, 'fetch');
|
|
772
|
+
|
|
773
|
+
async.series([
|
|
774
|
+
presence2.subscribe.bind(presence2),
|
|
775
|
+
function(next) {
|
|
776
|
+
presence2.once('receive', function(id, presence) {
|
|
777
|
+
expect(id).to.equal('presence-1');
|
|
778
|
+
expect(presence).to.be.null;
|
|
779
|
+
expect(connection1.agent.presenceRequests[presence1.channel]['presence-1'].v).to.equal(doc1.version);
|
|
780
|
+
expect(doc2.fetch).not.to.have.been.called;
|
|
781
|
+
next();
|
|
782
|
+
});
|
|
783
|
+
|
|
784
|
+
var leave = localPresence1._message();
|
|
785
|
+
leave.p = null;
|
|
786
|
+
leave.v = doc1.version + 1;
|
|
787
|
+
leave.t = presenceTestType.type.uri;
|
|
788
|
+
connection1.send(leave);
|
|
789
|
+
}
|
|
790
|
+
], done);
|
|
791
|
+
});
|
|
792
|
+
|
|
793
|
+
it('broadcasts null presence on disconnect after rejecting presence ahead of the doc', function(done) {
|
|
794
|
+
var localPresence1 = presence1.create('presence-1');
|
|
795
|
+
|
|
796
|
+
async.series([
|
|
797
|
+
presence1.subscribe.bind(presence1),
|
|
798
|
+
presence2.subscribe.bind(presence2),
|
|
799
|
+
function(next) {
|
|
800
|
+
localPresence1.submit({index: 1}, errorHandler(done));
|
|
801
|
+
presence2.once('receive', function() {
|
|
802
|
+
next();
|
|
803
|
+
});
|
|
804
|
+
},
|
|
805
|
+
function(next) {
|
|
806
|
+
localPresence1.once('error', function(error) {
|
|
807
|
+
expect(error.code).to.equal('ERR_OP_VERSION_NEWER_THAN_CURRENT_SNAPSHOT');
|
|
808
|
+
next();
|
|
809
|
+
});
|
|
810
|
+
|
|
811
|
+
var message = localPresence1._message();
|
|
812
|
+
message.v = doc1.version + 1;
|
|
813
|
+
message.t = presenceTestType.type.uri;
|
|
814
|
+
connection1.send(message);
|
|
815
|
+
},
|
|
816
|
+
function(next) {
|
|
817
|
+
presence2.once('receive', function(id, presence) {
|
|
818
|
+
expect(id).to.equal('presence-1');
|
|
819
|
+
expect(presence).to.be.null;
|
|
820
|
+
next();
|
|
821
|
+
});
|
|
822
|
+
connection1.close();
|
|
823
|
+
}
|
|
824
|
+
], done);
|
|
825
|
+
});
|
|
826
|
+
|
|
827
|
+
it('accepts up-to-date presence when the agent\'s tracked doc version lags behind', function(done) {
|
|
828
|
+
var localPresence1 = presence1.create('presence-1');
|
|
829
|
+
|
|
830
|
+
async.series([
|
|
831
|
+
presence2.subscribe.bind(presence2),
|
|
832
|
+
function(next) {
|
|
833
|
+
localPresence1.submit({index: 1}, errorHandler(done));
|
|
834
|
+
presence2.once('receive', function() {
|
|
835
|
+
next();
|
|
836
|
+
});
|
|
837
|
+
},
|
|
838
|
+
function(next) {
|
|
839
|
+
doc1.submitOp({index: 5, value: 'ern'}, errorHandler(done));
|
|
840
|
+
doc2.once('op', function() {
|
|
841
|
+
next();
|
|
842
|
+
});
|
|
843
|
+
},
|
|
844
|
+
function(next) {
|
|
845
|
+
connection1.agent.latestDocVersions.books['northern-lights'] = 1;
|
|
846
|
+
localPresence1.submit({index: 12}, errorHandler(done));
|
|
847
|
+
presence2.once('receive', function(id, presence) {
|
|
848
|
+
expect(doc2.version).to.eql(2);
|
|
849
|
+
expect(presence).to.eql({index: 12});
|
|
850
|
+
next();
|
|
851
|
+
});
|
|
852
|
+
}
|
|
853
|
+
], done);
|
|
854
|
+
});
|
|
855
|
+
|
|
856
|
+
it('transforms presence ahead of the agent\'s lagging tracked doc version by later ops', function(done) {
|
|
857
|
+
var localPresence1 = presence1.create('presence-1');
|
|
858
|
+
|
|
859
|
+
async.series([
|
|
860
|
+
presence2.subscribe.bind(presence2),
|
|
861
|
+
function(next) {
|
|
862
|
+
localPresence1.submit({index: 1}, errorHandler(done));
|
|
863
|
+
presence2.once('receive', function() {
|
|
864
|
+
next();
|
|
865
|
+
});
|
|
866
|
+
},
|
|
867
|
+
function(next) {
|
|
868
|
+
doc1.submitOp({index: 5, value: 'ern'}, errorHandler(done));
|
|
869
|
+
doc2.once('op', function() {
|
|
870
|
+
next();
|
|
871
|
+
});
|
|
872
|
+
},
|
|
873
|
+
function(next) {
|
|
874
|
+
doc2.submitOp({index: 0, value: 'The '}, errorHandler(done));
|
|
875
|
+
doc1.once('op', function() {
|
|
876
|
+
next();
|
|
877
|
+
});
|
|
878
|
+
},
|
|
879
|
+
function(next) {
|
|
880
|
+
connection1.agent.latestDocVersions.books['northern-lights'] = 1;
|
|
881
|
+
presence2.once('receive', function(id, presence) {
|
|
882
|
+
expect(presence).to.eql({index: 16});
|
|
883
|
+
next();
|
|
884
|
+
});
|
|
885
|
+
|
|
886
|
+
var message = localPresence1._message();
|
|
887
|
+
message.p = {index: 12};
|
|
888
|
+
message.v = 2;
|
|
889
|
+
message.t = presenceTestType.type.uri;
|
|
890
|
+
connection1.send(message);
|
|
891
|
+
}
|
|
892
|
+
], done);
|
|
893
|
+
});
|
|
894
|
+
|
|
895
|
+
it('rejects presence ahead of the doc when transformed without a tracked version', function(done) {
|
|
896
|
+
var presence = {
|
|
897
|
+
c: 'books',
|
|
898
|
+
d: 'northern-lights',
|
|
899
|
+
p: {index: 1},
|
|
900
|
+
v: doc1.version + 1,
|
|
901
|
+
t: presenceTestType.type.uri
|
|
902
|
+
};
|
|
903
|
+
backend.transformPresenceToLatestVersion(connection1.agent, presence, function(error) {
|
|
904
|
+
expect(error.code).to.equal('ERR_OP_VERSION_NEWER_THAN_CURRENT_SNAPSHOT');
|
|
905
|
+
done();
|
|
906
|
+
});
|
|
907
|
+
});
|
|
908
|
+
|
|
588
909
|
it('rejects a presence message without an ID', function(done) {
|
|
589
910
|
var localPresence1 = presence1.create('presence-1');
|
|
590
911
|
// Have to catch the error on the Presence instance, because obviously
|
|
@@ -682,6 +1003,43 @@ describe('DocPresence', function() {
|
|
|
682
1003
|
});
|
|
683
1004
|
});
|
|
684
1005
|
|
|
1006
|
+
it('clears presence when a client that has lost read access disconnects', function(done) {
|
|
1007
|
+
var agent1 = connection1.agent;
|
|
1008
|
+
var canRead = true;
|
|
1009
|
+
[backend.MIDDLEWARE_ACTIONS.readSnapshots, backend.MIDDLEWARE_ACTIONS.op].forEach(function(action) {
|
|
1010
|
+
backend.use(action, function(context, next) {
|
|
1011
|
+
if (canRead || context.agent !== agent1) return next();
|
|
1012
|
+
next(new Error('Forbidden'));
|
|
1013
|
+
});
|
|
1014
|
+
});
|
|
1015
|
+
var localPresence1 = presence1.create('presence-1');
|
|
1016
|
+
|
|
1017
|
+
async.series([
|
|
1018
|
+
presence2.subscribe.bind(presence2),
|
|
1019
|
+
function(next) {
|
|
1020
|
+
localPresence1.submit({index: 1}, errorHandler(done));
|
|
1021
|
+
presence2.once('receive', function() {
|
|
1022
|
+
next();
|
|
1023
|
+
});
|
|
1024
|
+
},
|
|
1025
|
+
function(next) {
|
|
1026
|
+
doc2.submitOp({index: 5, value: 'ern'}, errorHandler(done));
|
|
1027
|
+
doc1.once('op', function() {
|
|
1028
|
+
next();
|
|
1029
|
+
});
|
|
1030
|
+
},
|
|
1031
|
+
function(next) {
|
|
1032
|
+
canRead = false;
|
|
1033
|
+
presence2.once('receive', function(id, presence) {
|
|
1034
|
+
expect(id).to.equal('presence-1');
|
|
1035
|
+
expect(presence).to.be.null;
|
|
1036
|
+
next();
|
|
1037
|
+
});
|
|
1038
|
+
connection1.close();
|
|
1039
|
+
}
|
|
1040
|
+
], done);
|
|
1041
|
+
});
|
|
1042
|
+
|
|
685
1043
|
it('ignores untyped presence on its channel', function(done) {
|
|
686
1044
|
var connection3 = backend.connect();
|
|
687
1045
|
var untypedPresence3 = connection3.getPresence('books.northern-lights');
|