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.
@@ -585,20 +585,595 @@ 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
- // Have to catch the error on the Presence instance, because obviously
591
- // we won't find the LocalPresence without the ID
592
- presence1.on('error', function(error) {
593
- expect(error.code).to.eql('ERR_MESSAGE_BADLY_FORMED');
594
- done();
911
+ // Have to catch the error on the Presence instance, because obviously
912
+ // we won't find the LocalPresence without the ID
913
+ presence1.on('error', function(error) {
914
+ expect(error.code).to.eql('ERR_MESSAGE_BADLY_FORMED');
915
+ done();
916
+ });
917
+
918
+ var message = localPresence1._message();
919
+ message.id = null;
920
+ message.v = 1;
921
+ message.t = presenceTestType.type.uri;
922
+ connection1.send(message);
923
+ });
924
+
925
+ it('rejects a presence message for a different doc than its channel', function(done) {
926
+ var localPresence1 = presence1.create('presence-1');
927
+ localPresence1.on('error', function(error) {
928
+ expect(error.code).to.eql('ERR_MESSAGE_BADLY_FORMED');
929
+ expect(error.message).to.equal('Presence collection and doc ID do not match channel');
930
+ done();
931
+ });
932
+
933
+ var message = localPresence1._message();
934
+ message.d = 'subtle-knife';
935
+ message.v = 1;
936
+ message.t = presenceTestType.type.uri;
937
+ connection1.send(message);
938
+ });
939
+
940
+ it('rejects a presence message with a doc ID but no collection', function(done) {
941
+ var localPresence1 = presence1.create('presence-1');
942
+ localPresence1.on('error', function(error) {
943
+ expect(error.code).to.eql('ERR_MESSAGE_BADLY_FORMED');
944
+ done();
945
+ });
946
+
947
+ var message = localPresence1._message();
948
+ message.c = null;
949
+ message.v = 1;
950
+ message.t = presenceTestType.type.uri;
951
+ connection1.send(message);
952
+ });
953
+
954
+ it('rejects a presence message with a collection but no doc ID', function(done) {
955
+ var localPresence1 = presence1.create('presence-1');
956
+ localPresence1.on('error', function(error) {
957
+ expect(error.code).to.eql('ERR_MESSAGE_BADLY_FORMED');
958
+ done();
959
+ });
960
+
961
+ var message = localPresence1._message();
962
+ message.d = null;
963
+ message.v = 1;
964
+ message.t = presenceTestType.type.uri;
965
+ connection1.send(message);
966
+ });
967
+
968
+ it('rejects a presence message with an empty collection', function(done) {
969
+ var localPresence1 = connection1.getDocPresence('', 'northern-lights').create('presence-1');
970
+ localPresence1.on('error', function(error) {
971
+ expect(error.code).to.eql('ERR_MESSAGE_BADLY_FORMED');
972
+ done();
973
+ });
974
+
975
+ var message = localPresence1._message();
976
+ message.v = 1;
977
+ message.t = presenceTestType.type.uri;
978
+ connection1.send(message);
979
+ });
980
+
981
+ it('rejects a presence message with an empty doc ID', function(done) {
982
+ var localPresence1 = connection1.getDocPresence('books', '').create('presence-1');
983
+ localPresence1.on('error', function(error) {
984
+ expect(error.code).to.eql('ERR_MESSAGE_BADLY_FORMED');
985
+ done();
986
+ });
987
+
988
+ var message = localPresence1._message();
989
+ message.v = 1;
990
+ message.t = presenceTestType.type.uri;
991
+ connection1.send(message);
992
+ });
993
+
994
+ it('rejects presence on a doc the client cannot read', function(done) {
995
+ backend.use(backend.MIDDLEWARE_ACTIONS.readSnapshots, function(context, next) {
996
+ next(new Error('Forbidden'));
997
+ });
998
+
999
+ var localPresence1 = presence1.create('presence-1');
1000
+ localPresence1.submit({index: 1}, function(error) {
1001
+ expect(error.message).to.equal('Forbidden');
1002
+ done();
1003
+ });
1004
+ });
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
+
1043
+ it('ignores untyped presence on its channel', function(done) {
1044
+ var connection3 = backend.connect();
1045
+ var untypedPresence3 = connection3.getPresence('books.northern-lights');
1046
+ var localPresence1 = presence1.create('presence-1');
1047
+
1048
+ async.series([
1049
+ presence2.subscribe.bind(presence2),
1050
+ untypedPresence3.subscribe.bind(untypedPresence3),
1051
+ function(next) {
1052
+ connection2.on('receive', function(request) {
1053
+ if (request.data.src !== connection3.id) return;
1054
+ localPresence1.submit({index: 1}, errorHandler(done));
1055
+ });
1056
+ presence2.once('receive', function(id, presence) {
1057
+ expect(id).to.equal('presence-1');
1058
+ expect(presence).to.eql({index: 1});
1059
+ next();
1060
+ });
1061
+ connection3.send({
1062
+ a: 'p', ch: 'books.northern-lights', id: 'presence-1', p: {index: 9}, pv: 0, v: doc1.version
1063
+ });
1064
+ }
1065
+ ], done);
1066
+ });
1067
+
1068
+ it('does not let untyped presence with the same ID ack its local presence', function(done) {
1069
+ var connection3 = backend.connect();
1070
+ var untypedPresence3 = connection3.getPresence('books.northern-lights');
1071
+ var localPresence1 = presence1.create('presence-1');
1072
+ var hasReceivedOwnPresence = false;
1073
+
1074
+ async.series([
1075
+ presence1.subscribe.bind(presence1),
1076
+ untypedPresence3.subscribe.bind(untypedPresence3),
1077
+ function(next) {
1078
+ connection1.on('receive', function(request) {
1079
+ if (request.data.src === connection1.id) hasReceivedOwnPresence = true;
1080
+ if (request.data.src !== connection3.id) return;
1081
+ localPresence1.submit({index: 1}, function(error) {
1082
+ if (error) return done(error);
1083
+ expect(hasReceivedOwnPresence).to.be.true;
1084
+ next();
1085
+ });
1086
+ });
1087
+ connection3.send({
1088
+ a: 'p', ch: 'books.northern-lights', id: 'presence-1', p: {index: 9}, pv: 0, v: doc1.version
1089
+ });
1090
+ }
1091
+ ], done);
1092
+ });
1093
+
1094
+ it('does not let typed presence from another client with the same ID ack its local presence', function(done) {
1095
+ var connection3 = backend.connect();
1096
+ var presence3 = connection3.getDocPresence('books', 'northern-lights');
1097
+ var localPresence1 = presence1.create('presence-1');
1098
+ var hasReceivedOwnPresence = false;
1099
+
1100
+ async.series([
1101
+ presence1.subscribe.bind(presence1),
1102
+ presence3.subscribe.bind(presence3),
1103
+ function(next) {
1104
+ connection1.on('receive', function(request) {
1105
+ if (request.data.src === connection1.id) hasReceivedOwnPresence = true;
1106
+ if (request.data.src !== connection3.id) return;
1107
+ localPresence1.submit({index: 1}, function(error) {
1108
+ if (error) return done(error);
1109
+ expect(hasReceivedOwnPresence).to.be.true;
1110
+ next();
1111
+ });
1112
+ });
1113
+ connection3.send({
1114
+ a: 'p',
1115
+ ch: 'books.northern-lights',
1116
+ c: 'books',
1117
+ d: 'northern-lights',
1118
+ t: presenceTestType.type.uri,
1119
+ id: 'presence-1',
1120
+ p: {index: 9},
1121
+ pv: 0,
1122
+ v: doc1.version
1123
+ });
1124
+ }
1125
+ ], done);
1126
+ });
1127
+
1128
+ it('emits a forwarded error for a presence with the same ID on the presence, not its local presence', function(done) {
1129
+ backend.use(backend.MIDDLEWARE_ACTIONS.sendPresence, function(context, next) {
1130
+ next(new Error('uh-oh!'));
595
1131
  });
596
1132
 
597
- var message = localPresence1._message();
598
- message.id = null;
599
- message.v = 1;
600
- message.t = presenceTestType.type.uri;
601
- connection1.send(message);
1133
+ var connection3 = backend.connect();
1134
+ var doc3 = connection3.get('books', 'northern-lights');
1135
+ var localPresence3 = connection3.getDocPresence('books', 'northern-lights').create('presence-1');
1136
+ var localPresence1 = presence1.create('presence-1');
1137
+ localPresence1.on('error', done);
1138
+
1139
+ async.series([
1140
+ doc3.subscribe.bind(doc3),
1141
+ presence1.subscribe.bind(presence1),
1142
+ function(next) {
1143
+ presence1.once('error', function(error) {
1144
+ expect(error.message).to.equal('uh-oh!');
1145
+ next();
1146
+ });
1147
+ localPresence3.submit({index: 1}, errorHandler(done));
1148
+ }
1149
+ ], done);
1150
+ });
1151
+
1152
+ it('ignores presence for a doc whose collection and id join to the same channel', function(done) {
1153
+ var connection3 = backend.connect();
1154
+ var notesDoc1 = connection1.get('notes', 'private.alice');
1155
+ var notesDoc3 = connection3.get('notes', 'private.alice');
1156
+ var diaryDoc2 = connection2.get('notes.private', 'alice');
1157
+ var localNotesPresence1 = connection1.getDocPresence('notes', 'private.alice').create('notes-presence');
1158
+ var localDiaryPresence2 = connection2.getDocPresence('notes.private', 'alice').create('diary-presence');
1159
+ var notesPresence3 = connection3.getDocPresence('notes', 'private.alice');
1160
+
1161
+ async.series([
1162
+ notesDoc1.create.bind(notesDoc1, 'Hello', presenceTestType.type.name),
1163
+ diaryDoc2.create.bind(diaryDoc2, 'Dear diary', presenceTestType.type.name),
1164
+ notesDoc3.subscribe.bind(notesDoc3),
1165
+ notesPresence3.subscribe.bind(notesPresence3),
1166
+ function(next) {
1167
+ notesPresence3.once('receive', function(id) {
1168
+ expect(id).to.equal('notes-presence');
1169
+ next();
1170
+ });
1171
+ localDiaryPresence2.submit({index: 1}, function(error) {
1172
+ if (error) return done(error);
1173
+ localNotesPresence1.submit({index: 1}, errorHandler(done));
1174
+ });
1175
+ }
1176
+ ], done);
602
1177
  });
603
1178
 
604
1179
  it('only sends presence responses for the associated doc', function(done) {
@@ -793,6 +1368,21 @@ describe('DocPresence', function() {
793
1368
  ], done);
794
1369
  });
795
1370
 
1371
+ it('does not keep presence when fetching its doc version fails', function(done) {
1372
+ var localPresence1 = presence1.create('presence-1');
1373
+
1374
+ backend.use(backend.MIDDLEWARE_ACTIONS.readSnapshots, function(context, next) {
1375
+ next(new Error('denied'));
1376
+ });
1377
+
1378
+ localPresence1.submit({index: 1}, function(error) {
1379
+ expect(error.message).to.contain('denied');
1380
+ expect(connection1.agent.presenceRequests).to.be.empty;
1381
+ expect(connection1.agent._pendingPresenceBroadcasts).to.be.empty;
1382
+ done();
1383
+ });
1384
+ });
1385
+
796
1386
  it('errors when trying to submit presence on a type that does not support it', function(done) {
797
1387
  var jsonDoc = connection1.get('books', 'snuff');
798
1388
  var jsonPresence = connection1.getDocPresence('books', 'snuff');
@@ -976,12 +1566,19 @@ describe('DocPresence', function() {
976
1566
  var localPresence1 = presence1.create('presence-1');
977
1567
  var catchUpCount = 0;
978
1568
 
979
- function sendUntypedPresenceAtVersion(version) {
980
- var message = localPresence1._message();
981
- message.d = null;
982
- message.p = {index: 0};
983
- message.v = version;
984
- connection1.send(message);
1569
+ function receiveUntransformablePresenceAtVersion(version) {
1570
+ connection2.handleMessage({
1571
+ a: ACTIONS.presence,
1572
+ ch: 'books.northern-lights',
1573
+ src: connection1.id,
1574
+ id: 'presence-1',
1575
+ p: {index: 0},
1576
+ pv: version,
1577
+ c: 'books',
1578
+ d: 'northern-lights',
1579
+ v: version,
1580
+ t: null
1581
+ });
985
1582
  }
986
1583
 
987
1584
  async.series([
@@ -992,7 +1589,7 @@ describe('DocPresence', function() {
992
1589
  catchUpCount++;
993
1590
  if (catchUpCount === 1) next();
994
1591
  });
995
- sendUntypedPresenceAtVersion(0);
1592
+ receiveUntransformablePresenceAtVersion(0);
996
1593
  },
997
1594
  function(next) {
998
1595
  doc2.once('op', function() {
@@ -1006,7 +1603,7 @@ describe('DocPresence', function() {
1006
1603
  expect(error.code).to.equal('ERR_DOC_TYPE_NOT_RECOGNIZED');
1007
1604
  next();
1008
1605
  });
1009
- sendUntypedPresenceAtVersion(1);
1606
+ receiveUntransformablePresenceAtVersion(1);
1010
1607
  },
1011
1608
  function(next) {
1012
1609
  doc2.once('op', function() {
@@ -1047,6 +1644,252 @@ describe('DocPresence', function() {
1047
1644
  ], done);
1048
1645
  });
1049
1646
 
1647
+ it('releases the doc version stream of presence in flight when unsubscribing', function(done) {
1648
+ var localPresence1 = presence1.create('presence-1');
1649
+ var streamsCount;
1650
+
1651
+ backend.use(backend.MIDDLEWARE_ACTIONS.receivePresence, function(context, next) {
1652
+ if (context.presence.p === null) return next();
1653
+ presence1.unsubscribe(function(error) {
1654
+ if (error) return done(error);
1655
+ next();
1656
+ });
1657
+ });
1658
+
1659
+ async.series([
1660
+ presence2.subscribe.bind(presence2),
1661
+ function(next) {
1662
+ streamsCount = backend.pubsub.streamsCount;
1663
+ next();
1664
+ },
1665
+ presence1.subscribe.bind(presence1),
1666
+ function(next) {
1667
+ async.parallel([
1668
+ function(cb) {
1669
+ presence2.once('receive', function(id, presence) {
1670
+ expect(id).to.equal('presence-1');
1671
+ expect(presence).to.be.null;
1672
+ cb();
1673
+ });
1674
+ },
1675
+ function(cb) {
1676
+ localPresence1.submit({index: 1}, cb);
1677
+ }
1678
+ ], next);
1679
+ },
1680
+ function(next) {
1681
+ expect(backend.pubsub.streamsCount).to.equal(streamsCount);
1682
+ next();
1683
+ }
1684
+ ], done);
1685
+ });
1686
+
1687
+ it('releases the doc version stream of presence transformed across a delete when unsubscribing', function(done) {
1688
+ var localPresence1 = presence1.create('presence-1');
1689
+
1690
+ async.series([
1691
+ presence1.subscribe.bind(presence1),
1692
+ doc1.unsubscribe.bind(doc1),
1693
+ doc2.del.bind(doc2),
1694
+ localPresence1.submit.bind(localPresence1, {index: 1}),
1695
+ function(next) {
1696
+ expect(connection1.agent.presenceRequests['books.northern-lights']['presence-1'].p).to.be.null;
1697
+ expect(connection1.agent.latestDocVersionStreams).not.to.be.empty;
1698
+ presence1.unsubscribe(next);
1699
+ },
1700
+ function(next) {
1701
+ expect(connection1.agent.latestDocVersions).to.be.empty;
1702
+ expect(connection1.agent.latestDocVersionStreams).to.be.empty;
1703
+ next();
1704
+ }
1705
+ ], done);
1706
+ });
1707
+
1708
+ it('releases the doc version stream of presence that failed to transform when unsubscribing', function(done) {
1709
+ var localPresence1 = presence1.create('presence-1');
1710
+ var localPresence2 = presence1.create('presence-2');
1711
+
1712
+ async.series([
1713
+ presence1.subscribe.bind(presence1),
1714
+ presence2.subscribe.bind(presence2),
1715
+ doc1.unsubscribe.bind(doc1),
1716
+ doc2.submitOp.bind(doc2, {index: 5, value: 'ern'}),
1717
+ function(next) {
1718
+ localPresence1.submit({badProp: 'foo'}, function(error) {
1719
+ expect(error.code).to.equal('ERR_PRESENCE_TRANSFORM_FAILED');
1720
+ expect(connection1.agent.latestDocVersionStreams).not.to.be.empty;
1721
+ presence1.unsubscribe(next);
1722
+ });
1723
+ },
1724
+ function(next) {
1725
+ expect(connection1.agent.latestDocVersions).to.be.empty;
1726
+ expect(connection1.agent.latestDocVersionStreams).to.be.empty;
1727
+ localPresence2.submit({index: 2}, errorHandler(done));
1728
+ presence2.once('receive', function(id) {
1729
+ expect(id).to.equal('presence-2');
1730
+ next();
1731
+ });
1732
+ }
1733
+ ], done);
1734
+ });
1735
+
1736
+ it('does not broadcast an older presence that finishes transforming after its client unsubscribes', function(done) {
1737
+ var localPresence1 = presence1.create('presence-1');
1738
+ var localPresence2 = presence1.create('presence-2');
1739
+ var releaseFirstTransform;
1740
+
1741
+ async.series([
1742
+ presence1.subscribe.bind(presence1),
1743
+ presence2.subscribe.bind(presence2),
1744
+ doc1.unsubscribe.bind(doc1),
1745
+ doc2.submitOp.bind(doc2, {index: 5, value: 'ern'}),
1746
+ function(next) {
1747
+ backend.use(backend.MIDDLEWARE_ACTIONS.op, function(context, callback) {
1748
+ if (releaseFirstTransform || context.agent !== connection1.agent) return callback();
1749
+ releaseFirstTransform = callback;
1750
+ next();
1751
+ });
1752
+ localPresence1.submit({index: 1}, errorHandler(done));
1753
+ },
1754
+ function(next) {
1755
+ localPresence1.submit({index: 2}, errorHandler(done));
1756
+ presence2.once('receive', function(id, presence) {
1757
+ expect(presence).to.eql({index: 2});
1758
+ next();
1759
+ });
1760
+ },
1761
+ function(next) {
1762
+ presence1.unsubscribe(errorHandler(done));
1763
+ presence2.once('receive', function(id, presence) {
1764
+ expect(presence).to.be.null;
1765
+ next();
1766
+ });
1767
+ },
1768
+ function(next) {
1769
+ releaseFirstTransform();
1770
+ localPresence2.submit({index: 3}, errorHandler(done));
1771
+ presence2.once('receive', function(id) {
1772
+ expect(id).to.equal('presence-2');
1773
+ next();
1774
+ });
1775
+ },
1776
+ function(next) {
1777
+ expect(presence2.remotePresences).to.have.all.keys('presence-2');
1778
+ next();
1779
+ }
1780
+ ], done);
1781
+ });
1782
+
1783
+ it('broadcasts an older presence still transforming when a newer one fails to transform', function(done) {
1784
+ var localPresence1 = presence1.create('presence-1');
1785
+ var releaseFirstTransform;
1786
+
1787
+ async.series([
1788
+ presence2.subscribe.bind(presence2),
1789
+ doc1.unsubscribe.bind(doc1),
1790
+ doc2.submitOp.bind(doc2, {index: 5, value: 'ern'}),
1791
+ function(next) {
1792
+ backend.use(backend.MIDDLEWARE_ACTIONS.op, function(context, callback) {
1793
+ if (releaseFirstTransform || context.agent !== connection1.agent) return callback();
1794
+ releaseFirstTransform = callback;
1795
+ next();
1796
+ });
1797
+ localPresence1.submit({index: 1}, errorHandler(done));
1798
+ },
1799
+ function(next) {
1800
+ localPresence1.submit({badProp: 'foo'}, function(error) {
1801
+ expect(error.code).to.equal('ERR_PRESENCE_TRANSFORM_FAILED');
1802
+ next();
1803
+ });
1804
+ },
1805
+ function(next) {
1806
+ releaseFirstTransform();
1807
+ presence2.once('receive', function(id, presence) {
1808
+ expect(id).to.equal('presence-1');
1809
+ expect(presence).to.eql({index: 1});
1810
+ next();
1811
+ });
1812
+ }
1813
+ ], done);
1814
+ });
1815
+
1816
+ it('broadcasts a null presence still being transformed when the connection closes', function(done) {
1817
+ var localPresence1 = presence1.create('presence-1');
1818
+
1819
+ async.series([
1820
+ presence2.subscribe.bind(presence2),
1821
+ doc1.unsubscribe.bind(doc1),
1822
+ doc2.submitOp.bind(doc2, {index: 5, value: 'ern'}),
1823
+ function(next) {
1824
+ localPresence1.submit({index: 1}, errorHandler(done));
1825
+ presence2.once('receive', function() {
1826
+ next();
1827
+ });
1828
+ },
1829
+ function(next) {
1830
+ backend.use(backend.MIDDLEWARE_ACTIONS.op, function(context, callback) {
1831
+ if (context.agent !== connection1.agent) return callback();
1832
+ connection1.agent.stream.once('end', function() {
1833
+ callback();
1834
+ });
1835
+ connection1.close();
1836
+ });
1837
+ localPresence1.submit(null, errorHandler(done));
1838
+ presence2.once('receive', function(id, presence) {
1839
+ expect(id).to.equal('presence-1');
1840
+ expect(presence).to.be.null;
1841
+ next();
1842
+ });
1843
+ }
1844
+ ], done);
1845
+ });
1846
+
1847
+ it('does not broadcast presence still looking up its doc version when it is destroyed', function(done) {
1848
+ var localPresence1 = presence1.create('presence-1');
1849
+ var localPresence2 = presence1.create('presence-2');
1850
+ var releaseDocVersion;
1851
+
1852
+ async.series([
1853
+ presence2.subscribe.bind(presence2),
1854
+ function(next) {
1855
+ backend.use(backend.MIDDLEWARE_ACTIONS.readSnapshots, function(context, callback) {
1856
+ if (releaseDocVersion || context.agent !== connection1.agent) return callback();
1857
+ releaseDocVersion = callback;
1858
+ next();
1859
+ });
1860
+ localPresence1.submit({index: 1}, errorHandler(done));
1861
+ },
1862
+ function(next) {
1863
+ localPresence1.destroy(errorHandler(done));
1864
+ presence2.once('receive', function(id, presence) {
1865
+ expect(id).to.equal('presence-1');
1866
+ expect(presence).to.be.null;
1867
+ next();
1868
+ });
1869
+ },
1870
+ function(next) {
1871
+ connection1.once('receive', function(request) {
1872
+ expect(request.data).to.include({id: 'presence-1', pv: 0});
1873
+ expect(connection1.agent.latestDocVersions).to.be.empty;
1874
+ expect(connection1.agent.latestDocVersionStreams).to.be.empty;
1875
+ next();
1876
+ });
1877
+ releaseDocVersion();
1878
+ },
1879
+ function(next) {
1880
+ localPresence2.submit({index: 2}, errorHandler(done));
1881
+ presence2.once('receive', function(id) {
1882
+ expect(id).to.equal('presence-2');
1883
+ next();
1884
+ });
1885
+ },
1886
+ function(next) {
1887
+ expect(presence2.remotePresences).to.have.all.keys('presence-2');
1888
+ next();
1889
+ }
1890
+ ], done);
1891
+ });
1892
+
1050
1893
  it('does not error when destroying presence for a deleted doc', function(done) {
1051
1894
  var localPresence1 = presence1.create('presence-1');
1052
1895