diff --git a/Firestore/CHANGELOG.md b/Firestore/CHANGELOG.md index 60d99030d03..ceded5649ef 100644 --- a/Firestore/CHANGELOG.md +++ b/Firestore/CHANGELOG.md @@ -1,3 +1,6 @@ +# Unreleased +- [feature] Enable snapshot listener option to retrieve data from local cache only. (#12370) + # 10.22.0 - [fixed] Fix the flaky offline behaviour when using `arrayRemove` on `Map` object. (#12378) diff --git a/Firestore/Example/Firestore.xcodeproj/project.pbxproj b/Firestore/Example/Firestore.xcodeproj/project.pbxproj index b0aa3693dd8..99f967e3ccd 100644 --- a/Firestore/Example/Firestore.xcodeproj/project.pbxproj +++ b/Firestore/Example/Firestore.xcodeproj/project.pbxproj @@ -205,6 +205,7 @@ 1CC56DCA513B98CE39A6ED45 /* memory_local_store_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = F6CA0C5638AB6627CB5B4CF4 /* memory_local_store_test.cc */; }; 1CC9BABDD52B2A1E37E2698D /* mutation_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = C8522DE226C467C54E6788D8 /* mutation_test.cc */; }; 1CEEB0E7FBBB974224BBA557 /* bloom_filter_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = A2E6F09AD1EE0A6A452E9A08 /* bloom_filter_test.cc */; }; + 1CFBD4563960D8A20C4679A3 /* SnapshotListenerSourceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D65F6E69993611D47DC8E7C /* SnapshotListenerSourceTests.swift */; }; 1D618761796DE311A1707AA2 /* database_id_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = AB71064B201FA60300344F18 /* database_id_test.cc */; }; 1D71CA6BBA1E3433F243188E /* common.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 544129D221C2DDC800EFB9CC /* common.pb.cc */; }; 1D76DDBE57A4D66C64C00B65 /* FIRFieldValueTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E04A202154AA00B64F25 /* FIRFieldValueTests.mm */; }; @@ -238,6 +239,7 @@ 21E588CF29C72813D8A7A0A1 /* FSTExceptionCatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = B8BFD9B37D1029D238BDD71E /* FSTExceptionCatcher.m */; }; 21E66B6A4A00786C3E934EB1 /* query_engine_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B8A853940305237AFDA8050B /* query_engine_test.cc */; }; 224496E752E42E220F809FAC /* resource.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 1C3F7302BF4AE6CBC00ECDD0 /* resource.pb.cc */; }; + 2252357505C92A067DAC38B0 /* listen_source_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 4D9E51DA7A275D8B1CAEAEB2 /* listen_source_spec_test.json */; }; 226574601C3F6D14DF14C16B /* recovery_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 9C1AFCC9E616EC33D6E169CF /* recovery_spec_test.json */; }; 227CFA0B2A01884C277E4F1D /* hashing_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 54511E8D209805F8005BD28F /* hashing_test.cc */; }; 229D1A9381F698D71F229471 /* string_win_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 79507DF8378D3C42F5B36268 /* string_win_test.cc */; }; @@ -508,6 +510,7 @@ 5150E9F256E6E82D6F3CB3F1 /* bundle_cache_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = F7FC06E0A47D393DE1759AE1 /* bundle_cache_test.cc */; }; 518BF03D57FBAD7C632D18F8 /* FIRQueryUnitTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = FF73B39D04D1760190E6B84A /* FIRQueryUnitTests.mm */; }; 51A483DE202CC3E9FCD8FF6E /* Validation_BloomFilterTest_MD5_5000_01_membership_test_result.json in Resources */ = {isa = PBXBuildFile; fileRef = B0520A41251254B3C24024A3 /* Validation_BloomFilterTest_MD5_5000_01_membership_test_result.json */; }; + 5250AE69A391E7A3310E013B /* listen_source_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 4D9E51DA7A275D8B1CAEAEB2 /* listen_source_spec_test.json */; }; 52967C3DD7896BFA48840488 /* byte_string_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 5342CDDB137B4E93E2E85CCA /* byte_string_test.cc */; }; 529AB59F636060FEA21BD4FF /* garbage_collection_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = AAED89D7690E194EF3BA1132 /* garbage_collection_spec_test.json */; }; 5360D52DCAD1069B1E4B0B9D /* testing_hooks_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = A002425BC4FC4E805F4175B6 /* testing_hooks_test.cc */; }; @@ -816,6 +819,7 @@ 6F67601562343B63B8996F7A /* FSTTestingHooks.mm in Sources */ = {isa = PBXBuildFile; fileRef = D85AC18C55650ED230A71B82 /* FSTTestingHooks.mm */; }; 6F914209F46E6552B5A79570 /* async_queue_std_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B6FB4681208EA0BE00554BA2 /* async_queue_std_test.cc */; }; 6FAC16B7FBD3B40D11A6A816 /* target.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 618BBE7D20B89AAC00B5BCE7 /* target.pb.cc */; }; + 6FB40B88ACB4CFB34917319C /* listen_source_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 4D9E51DA7A275D8B1CAEAEB2 /* listen_source_spec_test.json */; }; 6FC85C48CF8235BA1845E1C8 /* FSTUserDataReaderTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 8D9892F204959C50613F16C8 /* FSTUserDataReaderTests.mm */; }; 6FCC64A1937E286E76C294D0 /* logic_utils_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 28B45B2104E2DAFBBF86DBB7 /* logic_utils_test.cc */; }; 6FD2369F24E884A9D767DD80 /* FIRDocumentSnapshotTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E04B202154AA00B64F25 /* FIRDocumentSnapshotTests.mm */; }; @@ -1050,6 +1054,7 @@ 9C86EEDEA131BFD50255EEF1 /* comparison_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 548DB928200D59F600E00ABC /* comparison_test.cc */; }; 9CC32ACF397022BB7DF11B52 /* Validation_BloomFilterTest_MD5_500_0001_bloom_filter_proto.json in Resources */ = {isa = PBXBuildFile; fileRef = D22D4C211AC32E4F8B4883DA /* Validation_BloomFilterTest_MD5_500_0001_bloom_filter_proto.json */; }; 9CE07BAAD3D3BC5F069D38FE /* grpc_streaming_reader_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B6D964922154AB8F00EB9CFB /* grpc_streaming_reader_test.cc */; }; + 9CFF379C7404F7CE6B26AF29 /* listen_source_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 4D9E51DA7A275D8B1CAEAEB2 /* listen_source_spec_test.json */; }; 9D71628E38D9F64C965DF29E /* FSTAPIHelpers.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E04E202154AA00B64F25 /* FSTAPIHelpers.mm */; }; 9E1997789F19BF2E9029012E /* FIRCompositeIndexQueryTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 65AF0AB593C3AD81A1F1A57E /* FIRCompositeIndexQueryTests.mm */; }; 9E656F4FE92E8BFB7F625283 /* to_string_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B696858D2214B53900271095 /* to_string_test.cc */; }; @@ -1059,6 +1064,7 @@ 9F9244225BE2EC88AA0CE4EF /* sorted_set_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 549CCA4C20A36DBB00BCEB75 /* sorted_set_test.cc */; }; A05BC6BDA2ABE405009211A9 /* target_id_generator_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = AB380CF82019382300D97691 /* target_id_generator_test.cc */; }; A06FBB7367CDD496887B86F8 /* leveldb_opener_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 75860CD13AF47EB1EA39EC2F /* leveldb_opener_test.cc */; }; + A0BC30D482B0ABD1A3A24CDC /* SnapshotListenerSourceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D65F6E69993611D47DC8E7C /* SnapshotListenerSourceTests.swift */; }; A0C6C658DFEE58314586907B /* offline_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 54DA12A11F315EE100DD57A1 /* offline_spec_test.json */; }; A0D61250F959BC52CEFF9467 /* Validation_BloomFilterTest_MD5_50000_01_membership_test_result.json in Resources */ = {isa = PBXBuildFile; fileRef = C8FB22BCB9F454DA44BA80C8 /* Validation_BloomFilterTest_MD5_50000_01_membership_test_result.json */; }; A0E1C7F5C7093A498F65C5CF /* memory_bundle_cache_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = AB4AB1388538CD3CB19EB028 /* memory_bundle_cache_test.cc */; }; @@ -1068,6 +1074,7 @@ A17DBC8F24127DA8A381F865 /* testutil.cc in Sources */ = {isa = PBXBuildFile; fileRef = 54A0352820A3B3BD003E0143 /* testutil.cc */; }; A186FECD0257B92FDB0E83B8 /* Validation_BloomFilterTest_MD5_50000_0001_membership_test_result.json in Resources */ = {isa = PBXBuildFile; fileRef = 5B96CC29E9946508F022859C /* Validation_BloomFilterTest_MD5_50000_0001_membership_test_result.json */; }; A192648233110B7B8BD65528 /* field_transform_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 7515B47C92ABEEC66864B55C /* field_transform_test.cc */; }; + A1A466F55A1ED0AC5EE449BF /* listen_source_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 4D9E51DA7A275D8B1CAEAEB2 /* listen_source_spec_test.json */; }; A1F57CC739211F64F2E9232D /* hard_assert_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 444B7AB3F5A2929070CB1363 /* hard_assert_test.cc */; }; A215078DBFBB5A4F4DADE8A9 /* leveldb_index_manager_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 166CE73C03AB4366AAC5201C /* leveldb_index_manager_test.cc */; }; A21819C437C3C80450D7EEEE /* writer_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = BC3C788D290A935C353CEAA1 /* writer_test.cc */; }; @@ -1156,11 +1163,13 @@ AFCA3C24AA751B5B2D3E6FEF /* Validation_BloomFilterTest_MD5_1_01_bloom_filter_proto.json in Resources */ = {isa = PBXBuildFile; fileRef = 0D964D4936953635AC7E0834 /* Validation_BloomFilterTest_MD5_1_01_bloom_filter_proto.json */; }; AFE84E7B0C356CD2A113E56E /* status_testing.cc in Sources */ = {isa = PBXBuildFile; fileRef = 3CAA33F964042646FDDAF9F9 /* status_testing.cc */; }; AFF7D2CF35B51656E4744164 /* bloom_filter_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = A2E6F09AD1EE0A6A452E9A08 /* bloom_filter_test.cc */; }; + B00F8D1819EE20C45B660940 /* SnapshotListenerSourceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D65F6E69993611D47DC8E7C /* SnapshotListenerSourceTests.swift */; }; B03F286F3AEC3781C386C646 /* FIRNumericTransformTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = D5B25E7E7D6873CBA4571841 /* FIRNumericTransformTests.mm */; }; B04E4FE20930384DF3A402F9 /* aggregate_query_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = AF924C79F49F793992A84879 /* aggregate_query_test.cc */; }; B0B779769926304268200015 /* query_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 731541602214AFFA0037F4DC /* query_spec_test.json */; }; B0D10C3451EDFB016A6EAF03 /* writer_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = BC3C788D290A935C353CEAA1 /* writer_test.cc */; }; B0E745EAC5F37CA61F868F38 /* Validation_BloomFilterTest_MD5_50000_1_bloom_filter_proto.json in Resources */ = {isa = PBXBuildFile; fileRef = 4B3E4A77493524333133C5DC /* Validation_BloomFilterTest_MD5_50000_1_bloom_filter_proto.json */; }; + B104B69726EF6A5B41DAFB17 /* listen_source_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 4D9E51DA7A275D8B1CAEAEB2 /* listen_source_spec_test.json */; }; B15D17049414E2F5AE72C9C6 /* memory_local_store_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = F6CA0C5638AB6627CB5B4CF4 /* memory_local_store_test.cc */; }; B188D7EC9A100F365DB02490 /* Validation_BloomFilterTest_MD5_500_01_membership_test_result.json in Resources */ = {isa = PBXBuildFile; fileRef = DD990FD89C165F4064B4F608 /* Validation_BloomFilterTest_MD5_500_01_membership_test_result.json */; }; B192F30DECA8C28007F9B1D0 /* array_sorted_map_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 54EB764C202277B30088B8F3 /* array_sorted_map_test.cc */; }; @@ -1740,6 +1749,8 @@ 4B59C0A7B2A4548496ED4E7D /* Validation_BloomFilterTest_MD5_1_0001_bloom_filter_proto.json */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.json; name = Validation_BloomFilterTest_MD5_1_0001_bloom_filter_proto.json; path = bloom_filter_golden_test_data/Validation_BloomFilterTest_MD5_1_0001_bloom_filter_proto.json; sourceTree = ""; }; 4BD051DBE754950FEAC7A446 /* Validation_BloomFilterTest_MD5_500_01_bloom_filter_proto.json */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.json; name = Validation_BloomFilterTest_MD5_500_01_bloom_filter_proto.json; path = bloom_filter_golden_test_data/Validation_BloomFilterTest_MD5_500_01_bloom_filter_proto.json; sourceTree = ""; }; 4C73C0CC6F62A90D8573F383 /* string_apple_benchmark.mm */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.objcpp; path = string_apple_benchmark.mm; sourceTree = ""; }; + 4D65F6E69993611D47DC8E7C /* SnapshotListenerSourceTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SnapshotListenerSourceTests.swift; sourceTree = ""; }; + 4D9E51DA7A275D8B1CAEAEB2 /* listen_source_spec_test.json */ = {isa = PBXFileReference; includeInIndex = 1; path = listen_source_spec_test.json; sourceTree = ""; }; 4F5B96F3ABCD2CA901DB1CD4 /* bundle_builder.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = bundle_builder.cc; sourceTree = ""; }; 526D755F65AC676234F57125 /* target_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = target_test.cc; sourceTree = ""; }; 52756B7624904C36FBB56000 /* fake_target_metadata_provider.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = fake_target_metadata_provider.h; sourceTree = ""; }; @@ -2221,6 +2232,7 @@ 3355BE9391CC4857AF0BDAE3 /* DatabaseTests.swift */, 62E54B832A9E910A003347C8 /* IndexingTests.swift */, 621D620928F9CE7400D2FA26 /* QueryIntegrationTests.swift */, + 4D65F6E69993611D47DC8E7C /* SnapshotListenerSourceTests.swift */, ); path = Integration; sourceTree = ""; @@ -2968,6 +2980,7 @@ 8C7278B604B8799F074F4E8C /* index_spec_test.json */, 54DA129E1F315EE100DD57A1 /* limbo_spec_test.json */, 54DA129F1F315EE100DD57A1 /* limit_spec_test.json */, + 4D9E51DA7A275D8B1CAEAEB2 /* listen_source_spec_test.json */, 54DA12A01F315EE100DD57A1 /* listen_spec_test.json */, 54DA12A11F315EE100DD57A1 /* offline_spec_test.json */, 54DA12A21F315EE100DD57A1 /* orderby_spec_test.json */, @@ -3378,6 +3391,7 @@ BFBE4732E93E38317B110778 /* index_spec_test.json in Resources */, 546877D72248206A005E3DE0 /* limbo_spec_test.json in Resources */, 546877D82248206A005E3DE0 /* limit_spec_test.json in Resources */, + 6FB40B88ACB4CFB34917319C /* listen_source_spec_test.json in Resources */, 546877D92248206A005E3DE0 /* listen_spec_test.json in Resources */, 546877DA2248206A005E3DE0 /* offline_spec_test.json in Resources */, 546877DB2248206A005E3DE0 /* orderby_spec_test.json in Resources */, @@ -3436,6 +3450,7 @@ 604B75044D6BEC2B7515EA1B /* index_spec_test.json in Resources */, 54ACB6CB224C11F400172E69 /* limbo_spec_test.json in Resources */, 54ACB6CC224C11F400172E69 /* limit_spec_test.json in Resources */, + 2252357505C92A067DAC38B0 /* listen_source_spec_test.json in Resources */, 54ACB6CD224C11F400172E69 /* listen_spec_test.json in Resources */, 54ACB6CE224C11F400172E69 /* offline_spec_test.json in Resources */, 54ACB6CF224C11F400172E69 /* orderby_spec_test.json in Resources */, @@ -3484,6 +3499,7 @@ 77C36312F8025EC73991D7DA /* index_spec_test.json in Resources */, F08DA55D31E44CB5B9170CCE /* limbo_spec_test.json in Resources */, 15A5F95DA733FD89A1E4147D /* limit_spec_test.json in Resources */, + 5250AE69A391E7A3310E013B /* listen_source_spec_test.json in Resources */, D73BBA4AB42940AB187169E3 /* listen_spec_test.json in Resources */, C15F5F1E7427738F20C2D789 /* offline_spec_test.json in Resources */, 4781186C01D33E67E07F0D0D /* orderby_spec_test.json in Resources */, @@ -3532,6 +3548,7 @@ 6156C6A837D78D49ED8B8812 /* index_spec_test.json in Resources */, 85BC2AB572A400114BF59255 /* limbo_spec_test.json in Resources */, 9F41D724D9947A89201495AD /* limit_spec_test.json in Resources */, + B104B69726EF6A5B41DAFB17 /* listen_source_spec_test.json in Resources */, 3CFFA6F016231446367E3A69 /* listen_spec_test.json in Resources */, A0C6C658DFEE58314586907B /* offline_spec_test.json in Resources */, D98430EA4FAA357D855FA50F /* orderby_spec_test.json in Resources */, @@ -3599,6 +3616,7 @@ 3783E25DFF9E5C0896D34FEF /* index_spec_test.json in Resources */, 54DA12A81F315EE100DD57A1 /* limbo_spec_test.json in Resources */, 54DA12A91F315EE100DD57A1 /* limit_spec_test.json in Resources */, + 9CFF379C7404F7CE6B26AF29 /* listen_source_spec_test.json in Resources */, 54DA12AA1F315EE100DD57A1 /* listen_spec_test.json in Resources */, 54DA12AB1F315EE100DD57A1 /* offline_spec_test.json in Resources */, 54DA12AC1F315EE100DD57A1 /* orderby_spec_test.json in Resources */, @@ -3665,6 +3683,7 @@ E04607A1E2964684184E8AEA /* index_spec_test.json in Resources */, 2AD8EE91928AE68DF268BEDA /* limbo_spec_test.json in Resources */, BC5AC8890974E0821431267E /* limit_spec_test.json in Resources */, + A1A466F55A1ED0AC5EE449BF /* listen_source_spec_test.json in Resources */, 5B89B1BA0AD400D9BF581420 /* listen_spec_test.json in Resources */, F660788F69B4336AC6CD2720 /* offline_spec_test.json in Resources */, 4F5714D37B6D119CB07ED8AE /* orderby_spec_test.json in Resources */, @@ -4572,6 +4591,7 @@ 3B1E27D951407FD237E64D07 /* FirestoreEncoderTests.swift in Sources */, 62E54B862A9E910B003347C8 /* IndexingTests.swift in Sources */, 621D620C28F9CE7400D2FA26 /* QueryIntegrationTests.swift in Sources */, + 1CFBD4563960D8A20C4679A3 /* SnapshotListenerSourceTests.swift in Sources */, 4D42E5C756229C08560DD731 /* XCTestCase+Await.mm in Sources */, 09BE8C01EC33D1FD82262D5D /* aggregate_query_test.cc in Sources */, 0EC3921AE220410F7394729B /* aggregation_result.pb.cc in Sources */, @@ -4812,6 +4832,7 @@ 5E89B1A5A5430713C79C4854 /* FirestoreEncoderTests.swift in Sources */, 62E54B852A9E910B003347C8 /* IndexingTests.swift in Sources */, 621D620B28F9CE7400D2FA26 /* QueryIntegrationTests.swift in Sources */, + A0BC30D482B0ABD1A3A24CDC /* SnapshotListenerSourceTests.swift in Sources */, 736C4E82689F1CA1859C4A3F /* XCTestCase+Await.mm in Sources */, 412BE974741729A6683C386F /* aggregate_query_test.cc in Sources */, DF983A9C1FBF758AF3AF110D /* aggregation_result.pb.cc in Sources */, @@ -5299,6 +5320,7 @@ 6F45846C159D3C063DBD3CBE /* FirestoreEncoderTests.swift in Sources */, 62E54B842A9E910B003347C8 /* IndexingTests.swift in Sources */, 621D620A28F9CE7400D2FA26 /* QueryIntegrationTests.swift in Sources */, + B00F8D1819EE20C45B660940 /* SnapshotListenerSourceTests.swift in Sources */, 5492E0442021457E00B64F25 /* XCTestCase+Await.mm in Sources */, B04E4FE20930384DF3A402F9 /* aggregate_query_test.cc in Sources */, 1A3D8028303B45FCBB21CAD3 /* aggregation_result.pb.cc in Sources */, diff --git a/Firestore/Example/Tests/SpecTests/FSTSpecTests.mm b/Firestore/Example/Tests/SpecTests/FSTSpecTests.mm index 622b3b527d7..f2b8ca2e4be 100644 --- a/Firestore/Example/Tests/SpecTests/FSTSpecTests.mm +++ b/Firestore/Example/Tests/SpecTests/FSTSpecTests.mm @@ -41,6 +41,7 @@ #include "Firestore/core/src/bundle/bundle_reader.h" #include "Firestore/core/src/bundle/bundle_serializer.h" #include "Firestore/core/src/core/field_filter.h" +#import "Firestore/core/src/core/listen_options.h" #include "Firestore/core/src/credentials/user.h" #include "Firestore/core/src/local/persistence.h" #include "Firestore/core/src/local/target_data.h" @@ -79,10 +80,12 @@ using firebase::firestore::Error; using firebase::firestore::google_firestore_v1_ArrayValue; using firebase::firestore::google_firestore_v1_Value; +using firebase::firestore::api::ListenSource; using firebase::firestore::api::LoadBundleTask; using firebase::firestore::bundle::BundleReader; using firebase::firestore::bundle::BundleSerializer; using firebase::firestore::core::DocumentViewChange; +using firebase::firestore::core::ListenOptions; using firebase::firestore::core::Query; using firebase::firestore::credentials::User; using firebase::firestore::local::Persistence; @@ -385,11 +388,25 @@ - (DocumentViewChange)parseChange:(NSDictionary *)jsonDoc ofType:(DocumentViewCh return DocumentViewChange{std::move(doc), type}; } +- (ListenOptions)parseOptions:(NSDictionary *)optionsSpec { + ListenOptions options = ListenOptions::FromIncludeMetadataChanges(true); + + if (optionsSpec != nil) { + ListenSource source = + [optionsSpec[@"source"] isEqual:@"cache"] ? ListenSource::Cache : ListenSource::Default; + // include_metadata_changes are default to true in spec tests + options = ListenOptions::FromOptions(true, source); + } + + return options; +} + #pragma mark - Methods for doing the steps of the spec test. - (void)doListen:(NSDictionary *)listenSpec { Query query = [self parseQuery:listenSpec[@"query"]]; - TargetId actualID = [self.driver addUserListenerWithQuery:std::move(query)]; + ListenOptions options = [self parseOptions:listenSpec[@"options"]]; + TargetId actualID = [self.driver addUserListenerWithQuery:std::move(query) options:options]; TargetId expectedID = [listenSpec[@"targetId"] intValue]; XCTAssertEqual(actualID, expectedID, @"targetID assigned to listen"); diff --git a/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.h b/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.h index c99836f8406..978ae28a4e5 100644 --- a/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.h +++ b/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.h @@ -146,9 +146,10 @@ typedef std:: * Resulting events are captured and made available via the capturedEventsSinceLastCall method. * * @param query A valid query to execute against the backend. + * @param options A listen option to configure snapshot listener. * @return The target ID assigned by the system to track the query. */ -- (model::TargetId)addUserListenerWithQuery:(core::Query)query; +- (model::TargetId)addUserListenerWithQuery:(core::Query)query options:(core::ListenOptions)options; /** * Removes a listener from the FSTSyncEngine as if the user had removed a listener corresponding diff --git a/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.mm b/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.mm index 41fe884c70f..7fe5dc5d91e 100644 --- a/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.mm +++ b/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.mm @@ -476,10 +476,8 @@ - (FSTOutstandingWrite *)receiveWriteError:(int)errorCode return result; } -- (TargetId)addUserListenerWithQuery:(Query)query { - // TODO(dimond): Allow customizing listen options in spec tests +- (TargetId)addUserListenerWithQuery:(Query)query options:(ListenOptions)options { // TODO(dimond): Change spec tests to verify isFromCache on snapshots - ListenOptions options = ListenOptions::FromIncludeMetadataChanges(true); auto listener = QueryListener::Create( query, options, [self, query](const StatusOr &maybe_snapshot) { FSTQueryEvent *event = [[FSTQueryEvent alloc] init]; diff --git a/Firestore/Example/Tests/SpecTests/json/listen_source_spec_test.json b/Firestore/Example/Tests/SpecTests/json/listen_source_spec_test.json new file mode 100644 index 00000000000..1912afc320f --- /dev/null +++ b/Firestore/Example/Tests/SpecTests/json/listen_source_spec_test.json @@ -0,0 +1,5895 @@ +{ + "Clients can have multiple listeners with different sources": { + "describeName": "Listens source options:", + "itName": "Clients can have multiple listeners with different sources", + "tags": [ + "multi-client" + ], + "config": { + "numClients": 2, + "useEagerGCForMemory": false + }, + "steps": [ + { + "clientIndex": 0, + "drainQueue": true + }, + { + "applyClientState": { + "visibility": "visible" + }, + "clientIndex": 0 + }, + { + "clientIndex": 0, + "userListen": { + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + }, + "targetId": 2 + }, + "expectedState": { + "activeTargets": { + "2": { + "queries": [ + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "resumeToken": "" + } + } + } + }, + { + "clientIndex": 0, + "watchAck": [ + 2 + ] + }, + { + "clientIndex": 0, + "watchEntity": { + "docs": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "key": "a" + }, + "version": 1000 + } + ], + "targets": [ + 2 + ] + } + }, + { + "clientIndex": 0, + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ] + }, + { + "clientIndex": 0, + "watchSnapshot": { + "targetIds": [ + ], + "version": 1000 + }, + "expectedSnapshotEvents": [ + { + "added": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "key": "a" + }, + "version": 1000 + } + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ] + }, + { + "clientIndex": 0, + "userListen": { + "options": { + "source": "cache" + }, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + }, + "targetId": 2 + }, + "expectedSnapshotEvents": [ + { + "added": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "key": "a" + }, + "version": 1000 + } + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ], + "expectedState": { + "activeTargets": { + "2": { + "queries": [ + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "resumeToken": "" + } + } + } + }, + { + "clientIndex": 1, + "drainQueue": true + }, + { + "clientIndex": 1, + "userListen": { + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + }, + "targetId": 2 + }, + "expectedSnapshotEvents": [ + { + "added": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "key": "a" + }, + "version": 1000 + } + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ], + "expectedState": { + "activeTargets": { + "2": { + "queries": [ + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "resumeToken": "" + } + } + } + }, + { + "clientIndex": 1, + "userListen": { + "options": { + "source": "cache" + }, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + }, + "targetId": 2 + }, + "expectedSnapshotEvents": [ + { + "added": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "key": "a" + }, + "version": 1000 + } + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ], + "expectedState": { + "activeTargets": { + "2": { + "queries": [ + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "resumeToken": "" + } + } + } + }, + { + "clientIndex": 0, + "drainQueue": true + }, + { + "clientIndex": 0, + "watchEntity": { + "docs": [ + { + "createTime": 0, + "key": "collection/b", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "key": "a" + }, + "version": 2000 + } + ], + "targets": [ + 2 + ] + } + }, + { + "clientIndex": 0, + "watchSnapshot": { + "targetIds": [ + ], + "version": 2000 + }, + "expectedSnapshotEvents": [ + { + "added": [ + { + "createTime": 0, + "key": "collection/b", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "key": "a" + }, + "version": 2000 + } + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + }, + { + "added": [ + { + "createTime": 0, + "key": "collection/b", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "key": "a" + }, + "version": 2000 + } + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ] + }, + { + "clientIndex": 1, + "drainQueue": true, + "expectedSnapshotEvents": [ + { + "added": [ + { + "createTime": 0, + "key": "collection/b", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "key": "a" + }, + "version": 2000 + } + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + }, + { + "added": [ + { + "createTime": 0, + "key": "collection/b", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "key": "a" + }, + "version": 2000 + } + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ] + } + ] + }, + "Contents of query are cleared when listen is removed.": { + "comment": "Explicitly tests eager GC behavior", + "describeName": "Listens source options:", + "itName": "Contents of query are cleared when listen is removed.", + "tags": [ + "eager-gc" + ], + "config": { + "numClients": 1, + "useEagerGCForMemory": true + }, + "steps": [ + { + "userSet": [ + "collection/a", + { + "key": "a" + } + ] + }, + { + "userListen": { + "options": { + "source": "cache" + }, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + }, + "targetId": 2 + }, + "expectedSnapshotEvents": [ + { + "added": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": true + }, + "value": { + "key": "a" + }, + "version": 0 + } + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ], + "expectedState": { + "activeTargets": { + } + } + }, + { + "userUnlisten": [ + 2, + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "expectedState": { + "activeTargets": { + } + } + }, + { + "writeAck": { + "version": 1000 + }, + "expectedState": { + "userCallbacks": { + "acknowledgedDocs": [ + "collection/a" + ], + "rejectedDocs": [ + ] + } + } + }, + { + "userListen": { + "options": { + "source": "cache" + }, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + }, + "targetId": 4 + }, + "expectedSnapshotEvents": [ + { + "added": [ + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ], + "expectedState": { + "activeTargets": { + } + } + } + ] + }, + "Documents are cleared when listen is removed.": { + "describeName": "Listens source options:", + "itName": "Documents are cleared when listen is removed.", + "tags": [ + "eager-gc" + ], + "config": { + "numClients": 1, + "useEagerGCForMemory": true + }, + "steps": [ + { + "userSet": [ + "collection/a", + { + "matches": true + } + ] + }, + { + "userSet": [ + "collection/b", + { + "matches": true + } + ] + }, + { + "userListen": { + "options": { + "source": "cache" + }, + "query": { + "filters": [ + [ + "matches", + "==", + true + ] + ], + "orderBys": [ + ], + "path": "collection" + }, + "targetId": 2 + }, + "expectedSnapshotEvents": [ + { + "added": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": true + }, + "value": { + "matches": true + }, + "version": 0 + }, + { + "createTime": 0, + "key": "collection/b", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": true + }, + "value": { + "matches": true + }, + "version": 0 + } + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true, + "query": { + "filters": [ + [ + "matches", + "==", + true + ] + ], + "orderBys": [ + ], + "path": "collection" + } + } + ], + "expectedState": { + "activeTargets": { + } + } + }, + { + "writeAck": { + "version": 1000 + }, + "expectedState": { + "userCallbacks": { + "acknowledgedDocs": [ + "collection/a" + ], + "rejectedDocs": [ + ] + } + } + }, + { + "writeAck": { + "version": 2000 + }, + "expectedState": { + "userCallbacks": { + "acknowledgedDocs": [ + "collection/b" + ], + "rejectedDocs": [ + ] + } + } + }, + { + "userSet": [ + "collection/b", + { + "matches": false + } + ], + "expectedSnapshotEvents": [ + { + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true, + "query": { + "filters": [ + [ + "matches", + "==", + true + ] + ], + "orderBys": [ + ], + "path": "collection" + }, + "removed": [ + { + "createTime": 0, + "key": "collection/b", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": true + }, + "value": { + "matches": true + }, + "version": 0 + } + ] + } + ] + }, + { + "userUnlisten": [ + 2, + { + "filters": [ + [ + "matches", + "==", + true + ] + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "expectedState": { + "activeTargets": { + } + } + }, + { + "writeAck": { + "version": 3000 + }, + "expectedState": { + "userCallbacks": { + "acknowledgedDocs": [ + "collection/b" + ], + "rejectedDocs": [ + ] + } + } + }, + { + "userListen": { + "options": { + "source": "cache" + }, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + }, + "targetId": 4 + }, + "expectedSnapshotEvents": [ + { + "added": [ + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ], + "expectedState": { + "activeTargets": { + } + } + }, + { + "userUnlisten": [ + 4, + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "expectedState": { + "activeTargets": { + } + } + } + ] + }, + "Doesn't include unknown documents in cached result": { + "describeName": "Listens source options:", + "itName": "Doesn't include unknown documents in cached result", + "tags": [ + ], + "config": { + "numClients": 1, + "useEagerGCForMemory": true + }, + "steps": [ + { + "userSet": [ + "collection/exists", + { + "key": "a" + } + ] + }, + { + "userPatch": [ + "collection/unknown", + { + "key": "b" + } + ] + }, + { + "userListen": { + "options": { + "source": "cache" + }, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + }, + "targetId": 2 + }, + "expectedSnapshotEvents": [ + { + "added": [ + { + "createTime": 0, + "key": "collection/exists", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": true + }, + "value": { + "key": "a" + }, + "version": 0 + } + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ], + "expectedState": { + "activeTargets": { + } + } + } + ] + }, + "Doesn't raise 'hasPendingWrites' for deletes": { + "describeName": "Listens source options:", + "itName": "Doesn't raise 'hasPendingWrites' for deletes", + "tags": [ + ], + "config": { + "numClients": 1, + "useEagerGCForMemory": false + }, + "steps": [ + { + "userListen": { + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + }, + "targetId": 2 + }, + "expectedState": { + "activeTargets": { + "2": { + "queries": [ + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "key": "a" + }, + "version": 1000 + } + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ] + }, + { + "watchSnapshot": { + "targetIds": [ + ], + "version": 1000 + }, + "expectedSnapshotEvents": [ + { + "added": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "key": "a" + }, + "version": 1000 + } + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ] + }, + { + "userUnlisten": [ + 2, + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "expectedState": { + "activeTargets": { + } + } + }, + { + "watchRemove": { + "targetIds": [ + 2 + ] + } + }, + { + "userListen": { + "options": { + "source": "cache" + }, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + }, + "targetId": 2 + }, + "expectedSnapshotEvents": [ + { + "added": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "key": "a" + }, + "version": 1000 + } + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ], + "expectedState": { + "activeTargets": { + } + } + }, + { + "userDelete": "collection/a", + "expectedSnapshotEvents": [ + { + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + }, + "removed": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "key": "a" + }, + "version": 1000 + } + ] + } + ] + }, + { + "writeAck": { + "version": 2000 + }, + "expectedState": { + "userCallbacks": { + "acknowledgedDocs": [ + "collection/a" + ], + "rejectedDocs": [ + ] + } + } + } + ] + }, + "Empty initial snapshot is raised from cache": { + "describeName": "Listens source options:", + "itName": "Empty initial snapshot is raised from cache", + "tags": [ + ], + "config": { + "numClients": 1, + "useEagerGCForMemory": false + }, + "steps": [ + { + "userListen": { + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + }, + "targetId": 2 + }, + "expectedState": { + "activeTargets": { + "2": { + "queries": [ + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ] + }, + { + "watchSnapshot": { + "targetIds": [ + ], + "version": 1000 + }, + "expectedSnapshotEvents": [ + { + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ] + }, + { + "userUnlisten": [ + 2, + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "expectedState": { + "activeTargets": { + } + } + }, + { + "watchRemove": { + "targetIds": [ + 2 + ] + } + }, + { + "userListen": { + "options": { + "source": "cache" + }, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + }, + "targetId": 2 + }, + "expectedSnapshotEvents": [ + { + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ], + "expectedState": { + "activeTargets": { + } + } + } + ] + }, + "Empty-due-to-delete initial snapshot is raised from cache": { + "describeName": "Listens source options:", + "itName": "Empty-due-to-delete initial snapshot is raised from cache", + "tags": [ + ], + "config": { + "numClients": 1, + "useEagerGCForMemory": false + }, + "steps": [ + { + "userListen": { + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + }, + "targetId": 2 + }, + "expectedState": { + "activeTargets": { + "2": { + "queries": [ + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "v": 1 + }, + "version": 1000 + } + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ] + }, + { + "watchSnapshot": { + "targetIds": [ + ], + "version": 1000 + }, + "expectedSnapshotEvents": [ + { + "added": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "v": 1 + }, + "version": 1000 + } + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ] + }, + { + "userUnlisten": [ + 2, + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "expectedState": { + "activeTargets": { + } + } + }, + { + "watchRemove": { + "targetIds": [ + 2 + ] + } + }, + { + "userDelete": "collection/a" + }, + { + "userListen": { + "options": { + "source": "cache" + }, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + }, + "targetId": 2 + }, + "expectedSnapshotEvents": [ + { + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ], + "expectedState": { + "activeTargets": { + } + } + } + ] + }, + "Listeners with different source shares watch changes between primary and secondary clients": { + "describeName": "Listens source options:", + "itName": "Listeners with different source shares watch changes between primary and secondary clients", + "tags": [ + "multi-client" + ], + "config": { + "numClients": 3, + "useEagerGCForMemory": false + }, + "steps": [ + { + "clientIndex": 0, + "drainQueue": true + }, + { + "applyClientState": { + "visibility": "visible" + }, + "clientIndex": 0 + }, + { + "clientIndex": 0, + "userListen": { + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + }, + "targetId": 2 + }, + "expectedState": { + "activeTargets": { + "2": { + "queries": [ + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "resumeToken": "" + } + } + } + }, + { + "clientIndex": 0, + "watchAck": [ + 2 + ] + }, + { + "clientIndex": 0, + "watchEntity": { + "docs": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "key": "a" + }, + "version": 1000 + } + ], + "targets": [ + 2 + ] + } + }, + { + "clientIndex": 0, + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ] + }, + { + "clientIndex": 0, + "watchSnapshot": { + "targetIds": [ + ], + "version": 1000 + }, + "expectedSnapshotEvents": [ + { + "added": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "key": "a" + }, + "version": 1000 + } + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ] + }, + { + "clientIndex": 1, + "drainQueue": true + }, + { + "clientIndex": 1, + "userListen": { + "options": { + "source": "cache" + }, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + }, + "targetId": 2 + }, + "expectedSnapshotEvents": [ + { + "added": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "key": "a" + }, + "version": 1000 + } + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ], + "expectedState": { + "activeTargets": { + } + } + }, + { + "clientIndex": 2, + "drainQueue": true + }, + { + "clientIndex": 2, + "userListen": { + "options": { + "source": "cache" + }, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + }, + "targetId": 2 + }, + "expectedSnapshotEvents": [ + { + "added": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "key": "a" + }, + "version": 1000 + } + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ], + "expectedState": { + "activeTargets": { + } + } + }, + { + "clientIndex": 0, + "drainQueue": true + }, + { + "clientIndex": 0, + "watchEntity": { + "docs": [ + { + "createTime": 0, + "key": "collection/b", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "key": "a" + }, + "version": 2000 + } + ], + "targets": [ + 2 + ] + } + }, + { + "clientIndex": 0, + "watchSnapshot": { + "targetIds": [ + ], + "version": 2000 + }, + "expectedSnapshotEvents": [ + { + "added": [ + { + "createTime": 0, + "key": "collection/b", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "key": "a" + }, + "version": 2000 + } + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ] + }, + { + "clientIndex": 1, + "drainQueue": true, + "expectedSnapshotEvents": [ + { + "added": [ + { + "createTime": 0, + "key": "collection/b", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "key": "a" + }, + "version": 2000 + } + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ] + }, + { + "clientIndex": 2, + "drainQueue": true, + "expectedSnapshotEvents": [ + { + "added": [ + { + "createTime": 0, + "key": "collection/b", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "key": "a" + }, + "version": 2000 + } + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ] + }, + { + "clientIndex": 0, + "drainQueue": true + }, + { + "clientIndex": 0, + "userUnlisten": [ + 2, + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "expectedState": { + "activeTargets": { + } + } + } + ] + }, + "Local mutations notifies listeners sourced from cache in all tabs": { + "describeName": "Listens source options:", + "itName": "Local mutations notifies listeners sourced from cache in all tabs", + "tags": [ + "multi-client" + ], + "config": { + "numClients": 2, + "useEagerGCForMemory": false + }, + "steps": [ + { + "clientIndex": 0, + "drainQueue": true + }, + { + "applyClientState": { + "visibility": "visible" + }, + "clientIndex": 0 + }, + { + "clientIndex": 0, + "userListen": { + "options": { + "source": "cache" + }, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + }, + "targetId": 2 + }, + "expectedSnapshotEvents": [ + { + "added": [ + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ], + "expectedState": { + "activeTargets": { + } + } + }, + { + "clientIndex": 1, + "drainQueue": true + }, + { + "clientIndex": 1, + "userListen": { + "options": { + "source": "cache" + }, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + }, + "targetId": 2 + }, + "expectedSnapshotEvents": [ + { + "added": [ + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ], + "expectedState": { + "activeTargets": { + } + } + }, + { + "clientIndex": 0, + "drainQueue": true + }, + { + "clientIndex": 0, + "userSet": [ + "collection/a", + { + "key": "a" + } + ], + "expectedSnapshotEvents": [ + { + "added": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": true + }, + "value": { + "key": "a" + }, + "version": 0 + } + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ] + }, + { + "clientIndex": 1, + "drainQueue": true, + "expectedSnapshotEvents": [ + { + "added": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": true + }, + "value": { + "key": "a" + }, + "version": 0 + } + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ] + } + ] + }, + "Mirror queries being listened from different sources while listening to server in primary tab": { + "describeName": "Listens source options:", + "itName": "Mirror queries being listened from different sources while listening to server in primary tab", + "tags": [ + "multi-client" + ], + "config": { + "numClients": 2, + "useEagerGCForMemory": false + }, + "steps": [ + { + "clientIndex": 0, + "drainQueue": true + }, + { + "applyClientState": { + "visibility": "visible" + }, + "clientIndex": 0 + }, + { + "clientIndex": 0, + "userListen": { + "query": { + "filters": [ + ], + "limit": 2, + "limitType": "LimitToFirst", + "orderBys": [ + [ + "sort", + "asc" + ] + ], + "path": "collection" + }, + "targetId": 2 + }, + "expectedState": { + "activeTargets": { + "2": { + "queries": [ + { + "filters": [ + ], + "limit": 2, + "limitType": "LimitToFirst", + "orderBys": [ + [ + "sort", + "asc" + ] + ], + "path": "collection" + } + ], + "resumeToken": "" + } + } + } + }, + { + "clientIndex": 0, + "watchAck": [ + 2 + ] + }, + { + "clientIndex": 0, + "watchEntity": { + "docs": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "sort": 0 + }, + "version": 1000 + }, + { + "createTime": 0, + "key": "collection/b", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "sort": 1 + }, + "version": 1000 + } + ], + "targets": [ + 2 + ] + } + }, + { + "clientIndex": 0, + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ] + }, + { + "clientIndex": 0, + "watchSnapshot": { + "targetIds": [ + ], + "version": 1000 + }, + "expectedSnapshotEvents": [ + { + "added": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "sort": 0 + }, + "version": 1000 + }, + { + "createTime": 0, + "key": "collection/b", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "sort": 1 + }, + "version": 1000 + } + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "limit": 2, + "limitType": "LimitToFirst", + "orderBys": [ + [ + "sort", + "asc" + ] + ], + "path": "collection" + } + } + ] + }, + { + "clientIndex": 1, + "drainQueue": true + }, + { + "clientIndex": 1, + "userListen": { + "options": { + "source": "cache" + }, + "query": { + "filters": [ + ], + "limit": 2, + "limitType": "LimitToLast", + "orderBys": [ + [ + "sort", + "desc" + ] + ], + "path": "collection" + }, + "targetId": 2 + }, + "expectedSnapshotEvents": [ + { + "added": [ + { + "createTime": 0, + "key": "collection/b", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "sort": 1 + }, + "version": 1000 + }, + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "sort": 0 + }, + "version": 1000 + } + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "limit": 2, + "limitType": "LimitToLast", + "orderBys": [ + [ + "sort", + "desc" + ] + ], + "path": "collection" + } + } + ], + "expectedState": { + "activeTargets": { + } + } + }, + { + "clientIndex": 0, + "drainQueue": true + }, + { + "clientIndex": 0, + "watchEntity": { + "docs": [ + { + "createTime": 0, + "key": "collection/c", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "sort": -1 + }, + "version": 2000 + } + ], + "targets": [ + 2 + ] + } + }, + { + "clientIndex": 0, + "watchSnapshot": { + "targetIds": [ + ], + "version": 2000 + }, + "expectedSnapshotEvents": [ + { + "added": [ + { + "createTime": 0, + "key": "collection/c", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "sort": -1 + }, + "version": 2000 + } + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "limit": 2, + "limitType": "LimitToFirst", + "orderBys": [ + [ + "sort", + "asc" + ] + ], + "path": "collection" + }, + "removed": [ + { + "createTime": 0, + "key": "collection/b", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "sort": 1 + }, + "version": 1000 + } + ] + } + ] + }, + { + "clientIndex": 1, + "drainQueue": true, + "expectedSnapshotEvents": [ + { + "added": [ + { + "createTime": 0, + "key": "collection/c", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "sort": -1 + }, + "version": 2000 + } + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "limit": 2, + "limitType": "LimitToLast", + "orderBys": [ + [ + "sort", + "desc" + ] + ], + "path": "collection" + }, + "removed": [ + { + "createTime": 0, + "key": "collection/b", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "sort": 1 + }, + "version": 1000 + } + ] + } + ] + } + ] + }, + "Mirror queries being listened in different clients sourced from cache ": { + "describeName": "Listens source options:", + "itName": "Mirror queries being listened in different clients sourced from cache ", + "tags": [ + "multi-client" + ], + "config": { + "numClients": 2, + "useEagerGCForMemory": false + }, + "steps": [ + { + "clientIndex": 0, + "drainQueue": true + }, + { + "applyClientState": { + "visibility": "visible" + }, + "clientIndex": 0 + }, + { + "clientIndex": 0, + "userListen": { + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + }, + "targetId": 2 + }, + "expectedState": { + "activeTargets": { + "2": { + "queries": [ + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "resumeToken": "" + } + } + } + }, + { + "clientIndex": 0, + "watchAck": [ + 2 + ] + }, + { + "clientIndex": 0, + "watchEntity": { + "docs": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "sort": 0 + }, + "version": 1000 + }, + { + "createTime": 0, + "key": "collection/b", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "sort": 1 + }, + "version": 1000 + }, + { + "createTime": 0, + "key": "collection/c", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "sort": 1 + }, + "version": 1000 + } + ], + "targets": [ + 2 + ] + } + }, + { + "clientIndex": 0, + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ] + }, + { + "clientIndex": 0, + "watchSnapshot": { + "targetIds": [ + ], + "version": 1000 + }, + "expectedSnapshotEvents": [ + { + "added": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "sort": 0 + }, + "version": 1000 + }, + { + "createTime": 0, + "key": "collection/b", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "sort": 1 + }, + "version": 1000 + }, + { + "createTime": 0, + "key": "collection/c", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "sort": 1 + }, + "version": 1000 + } + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ] + }, + { + "clientIndex": 0, + "userUnlisten": [ + 2, + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "expectedState": { + "activeTargets": { + } + } + }, + { + "clientIndex": 0, + "watchRemove": { + "targetIds": [ + 2 + ] + } + }, + { + "clientIndex": 0, + "userListen": { + "options": { + "source": "cache" + }, + "query": { + "filters": [ + ], + "limit": 2, + "limitType": "LimitToFirst", + "orderBys": [ + [ + "sort", + "asc" + ] + ], + "path": "collection" + }, + "targetId": 4 + }, + "expectedSnapshotEvents": [ + { + "added": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "sort": 0 + }, + "version": 1000 + }, + { + "createTime": 0, + "key": "collection/b", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "sort": 1 + }, + "version": 1000 + } + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "limit": 2, + "limitType": "LimitToFirst", + "orderBys": [ + [ + "sort", + "asc" + ] + ], + "path": "collection" + } + } + ], + "expectedState": { + "activeTargets": { + } + } + }, + { + "clientIndex": 1, + "drainQueue": true + }, + { + "clientIndex": 1, + "userListen": { + "options": { + "source": "cache" + }, + "query": { + "filters": [ + ], + "limit": 2, + "limitType": "LimitToLast", + "orderBys": [ + [ + "sort", + "desc" + ] + ], + "path": "collection" + }, + "targetId": 4 + }, + "expectedSnapshotEvents": [ + { + "added": [ + { + "createTime": 0, + "key": "collection/b", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "sort": 1 + }, + "version": 1000 + }, + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "sort": 0 + }, + "version": 1000 + } + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "limit": 2, + "limitType": "LimitToLast", + "orderBys": [ + [ + "sort", + "desc" + ] + ], + "path": "collection" + } + } + ], + "expectedState": { + "activeTargets": { + } + } + }, + { + "clientIndex": 0, + "drainQueue": true + }, + { + "clientIndex": 0, + "userUnlisten": [ + 4, + { + "filters": [ + ], + "limit": 2, + "limitType": "LimitToFirst", + "orderBys": [ + [ + "sort", + "asc" + ] + ], + "path": "collection" + } + ], + "expectedState": { + "activeTargets": { + } + } + }, + { + "clientIndex": 0, + "userSet": [ + "collection/c", + { + "sort": -1 + } + ] + }, + { + "clientIndex": 1, + "drainQueue": true, + "expectedSnapshotEvents": [ + { + "added": [ + { + "createTime": 0, + "key": "collection/c", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": true + }, + "value": { + "sort": -1 + }, + "version": 0 + } + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true, + "query": { + "filters": [ + ], + "limit": 2, + "limitType": "LimitToLast", + "orderBys": [ + [ + "sort", + "desc" + ] + ], + "path": "collection" + }, + "removed": [ + { + "createTime": 0, + "key": "collection/b", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "sort": 1 + }, + "version": 1000 + } + ] + } + ] + } + ] + }, + "Mirror queries being listened in the same secondary client sourced from cache": { + "describeName": "Listens source options:", + "itName": "Mirror queries being listened in the same secondary client sourced from cache", + "tags": [ + "multi-client" + ], + "config": { + "numClients": 2, + "useEagerGCForMemory": false + }, + "steps": [ + { + "clientIndex": 0, + "drainQueue": true + }, + { + "applyClientState": { + "visibility": "visible" + }, + "clientIndex": 0 + }, + { + "clientIndex": 0, + "userListen": { + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + }, + "targetId": 2 + }, + "expectedState": { + "activeTargets": { + "2": { + "queries": [ + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "resumeToken": "" + } + } + } + }, + { + "clientIndex": 0, + "watchAck": [ + 2 + ] + }, + { + "clientIndex": 0, + "watchEntity": { + "docs": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "sort": 0 + }, + "version": 1000 + }, + { + "createTime": 0, + "key": "collection/b", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "sort": 1 + }, + "version": 1000 + }, + { + "createTime": 0, + "key": "collection/c", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "sort": 1 + }, + "version": 1000 + } + ], + "targets": [ + 2 + ] + } + }, + { + "clientIndex": 0, + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ] + }, + { + "clientIndex": 0, + "watchSnapshot": { + "targetIds": [ + ], + "version": 1000 + }, + "expectedSnapshotEvents": [ + { + "added": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "sort": 0 + }, + "version": 1000 + }, + { + "createTime": 0, + "key": "collection/b", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "sort": 1 + }, + "version": 1000 + }, + { + "createTime": 0, + "key": "collection/c", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "sort": 1 + }, + "version": 1000 + } + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ] + }, + { + "clientIndex": 0, + "userUnlisten": [ + 2, + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "expectedState": { + "activeTargets": { + } + } + }, + { + "clientIndex": 0, + "watchRemove": { + "targetIds": [ + 2 + ] + } + }, + { + "clientIndex": 1, + "drainQueue": true + }, + { + "clientIndex": 1, + "userListen": { + "options": { + "source": "cache" + }, + "query": { + "filters": [ + ], + "limit": 2, + "limitType": "LimitToFirst", + "orderBys": [ + [ + "sort", + "asc" + ] + ], + "path": "collection" + }, + "targetId": 4 + }, + "expectedSnapshotEvents": [ + { + "added": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "sort": 0 + }, + "version": 1000 + }, + { + "createTime": 0, + "key": "collection/b", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "sort": 1 + }, + "version": 1000 + } + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "limit": 2, + "limitType": "LimitToFirst", + "orderBys": [ + [ + "sort", + "asc" + ] + ], + "path": "collection" + } + } + ], + "expectedState": { + "activeTargets": { + } + } + }, + { + "clientIndex": 1, + "userListen": { + "options": { + "source": "cache" + }, + "query": { + "filters": [ + ], + "limit": 2, + "limitType": "LimitToLast", + "orderBys": [ + [ + "sort", + "desc" + ] + ], + "path": "collection" + }, + "targetId": 4 + }, + "expectedSnapshotEvents": [ + { + "added": [ + { + "createTime": 0, + "key": "collection/b", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "sort": 1 + }, + "version": 1000 + }, + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "sort": 0 + }, + "version": 1000 + } + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "limit": 2, + "limitType": "LimitToLast", + "orderBys": [ + [ + "sort", + "desc" + ] + ], + "path": "collection" + } + } + ], + "expectedState": { + "activeTargets": { + } + } + }, + { + "clientIndex": 1, + "userUnlisten": [ + 4, + { + "filters": [ + ], + "limit": 2, + "limitType": "LimitToFirst", + "orderBys": [ + [ + "sort", + "asc" + ] + ], + "path": "collection" + } + ], + "expectedState": { + "activeTargets": { + } + } + }, + { + "clientIndex": 1, + "userSet": [ + "collection/c", + { + "sort": -1 + } + ], + "expectedSnapshotEvents": [ + { + "added": [ + { + "createTime": 0, + "key": "collection/c", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": true + }, + "value": { + "sort": -1 + }, + "version": 0 + } + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true, + "query": { + "filters": [ + ], + "limit": 2, + "limitType": "LimitToLast", + "orderBys": [ + [ + "sort", + "desc" + ] + ], + "path": "collection" + }, + "removed": [ + { + "createTime": 0, + "key": "collection/b", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "sort": 1 + }, + "version": 1000 + } + ] + } + ] + } + ] + }, + "Mirror queries from different sources while listening to server in secondary tab": { + "describeName": "Listens source options:", + "itName": "Mirror queries from different sources while listening to server in secondary tab", + "tags": [ + "multi-client" + ], + "config": { + "numClients": 2, + "useEagerGCForMemory": false + }, + "steps": [ + { + "clientIndex": 0, + "drainQueue": true + }, + { + "applyClientState": { + "visibility": "visible" + }, + "clientIndex": 0 + }, + { + "clientIndex": 1, + "drainQueue": true + }, + { + "clientIndex": 1, + "userListen": { + "query": { + "filters": [ + ], + "limit": 2, + "limitType": "LimitToFirst", + "orderBys": [ + [ + "sort", + "asc" + ] + ], + "path": "collection" + }, + "targetId": 2 + }, + "expectedState": { + "activeTargets": { + "2": { + "queries": [ + { + "filters": [ + ], + "limit": 2, + "limitType": "LimitToFirst", + "orderBys": [ + [ + "sort", + "asc" + ] + ], + "path": "collection" + } + ], + "resumeToken": "" + } + } + } + }, + { + "clientIndex": 0, + "drainQueue": true, + "expectedState": { + "activeTargets": { + "2": { + "queries": [ + { + "filters": [ + ], + "limit": 2, + "limitType": "LimitToFirst", + "orderBys": [ + [ + "sort", + "asc" + ] + ], + "path": "collection" + } + ], + "resumeToken": "" + } + } + } + }, + { + "clientIndex": 0, + "watchAck": [ + 2 + ] + }, + { + "clientIndex": 0, + "watchEntity": { + "docs": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "sort": 0 + }, + "version": 1000 + }, + { + "createTime": 0, + "key": "collection/b", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "sort": 1 + }, + "version": 1000 + } + ], + "targets": [ + 2 + ] + } + }, + { + "clientIndex": 0, + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ] + }, + { + "clientIndex": 0, + "watchSnapshot": { + "targetIds": [ + ], + "version": 1000 + } + }, + { + "clientIndex": 1, + "drainQueue": true, + "expectedSnapshotEvents": [ + { + "added": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "sort": 0 + }, + "version": 1000 + }, + { + "createTime": 0, + "key": "collection/b", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "sort": 1 + }, + "version": 1000 + } + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "limit": 2, + "limitType": "LimitToFirst", + "orderBys": [ + [ + "sort", + "asc" + ] + ], + "path": "collection" + } + } + ] + }, + { + "clientIndex": 0, + "drainQueue": true + }, + { + "clientIndex": 0, + "userListen": { + "options": { + "source": "cache" + }, + "query": { + "filters": [ + ], + "limit": 2, + "limitType": "LimitToLast", + "orderBys": [ + [ + "sort", + "desc" + ] + ], + "path": "collection" + }, + "targetId": 2 + }, + "expectedSnapshotEvents": [ + { + "added": [ + { + "createTime": 0, + "key": "collection/b", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "sort": 1 + }, + "version": 1000 + }, + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "sort": 0 + }, + "version": 1000 + } + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "limit": 2, + "limitType": "LimitToLast", + "orderBys": [ + [ + "sort", + "desc" + ] + ], + "path": "collection" + } + } + ], + "expectedState": { + "activeTargets": { + "2": { + "queries": [ + { + "filters": [ + ], + "limit": 2, + "limitType": "LimitToFirst", + "orderBys": [ + [ + "sort", + "asc" + ] + ], + "path": "collection" + } + ], + "resumeToken": "" + } + } + } + }, + { + "clientIndex": 0, + "watchEntity": { + "docs": [ + { + "createTime": 0, + "key": "collection/c", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "sort": -1 + }, + "version": 2000 + } + ], + "targets": [ + 2 + ] + } + }, + { + "clientIndex": 0, + "watchSnapshot": { + "targetIds": [ + ], + "version": 2000 + }, + "expectedSnapshotEvents": [ + { + "added": [ + { + "createTime": 0, + "key": "collection/c", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "sort": -1 + }, + "version": 2000 + } + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "limit": 2, + "limitType": "LimitToLast", + "orderBys": [ + [ + "sort", + "desc" + ] + ], + "path": "collection" + }, + "removed": [ + { + "createTime": 0, + "key": "collection/b", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "sort": 1 + }, + "version": 1000 + } + ] + } + ] + }, + { + "clientIndex": 1, + "drainQueue": true, + "expectedSnapshotEvents": [ + { + "added": [ + { + "createTime": 0, + "key": "collection/c", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "sort": -1 + }, + "version": 2000 + } + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "limit": 2, + "limitType": "LimitToFirst", + "orderBys": [ + [ + "sort", + "asc" + ] + ], + "path": "collection" + }, + "removed": [ + { + "createTime": 0, + "key": "collection/b", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "sort": 1 + }, + "version": 1000 + } + ] + } + ] + } + ] + }, + "Newer deleted docs from bundles should delete cached docs": { + "describeName": "Listens source options:", + "itName": "Newer deleted docs from bundles should delete cached docs", + "tags": [ + ], + "config": { + "numClients": 1, + "useEagerGCForMemory": false + }, + "steps": [ + { + "userListen": { + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + }, + "targetId": 2 + }, + "expectedState": { + "activeTargets": { + "2": { + "queries": [ + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "value": "a" + }, + "version": 1000 + } + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ] + }, + { + "watchSnapshot": { + "targetIds": [ + ], + "version": 1000 + }, + "expectedSnapshotEvents": [ + { + "added": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "value": "a" + }, + "version": 1000 + } + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ] + }, + { + "userUnlisten": [ + 2, + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "expectedState": { + "activeTargets": { + } + } + }, + { + "watchRemove": { + "targetIds": [ + 2 + ] + } + }, + { + "userListen": { + "options": { + "source": "cache" + }, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + }, + "targetId": 2 + }, + "expectedSnapshotEvents": [ + { + "added": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "value": "a" + }, + "version": 1000 + } + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ], + "expectedState": { + "activeTargets": { + } + } + }, + { + "loadBundle": "127{\"metadata\":{\"id\":\"test-bundle\",\"createTime\":\"1970-01-01T00:00:00.003000000Z\",\"version\":1,\"totalDocuments\":1,\"totalBytes\":158}}155{\"documentMetadata\":{\"name\":\"projects/test-project/databases/(default)/documents/collection/a\",\"readTime\":\"1970-01-01T00:00:00.003000000Z\",\"exists\":false}}", + "expectedSnapshotEvents": [ + { + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + }, + "removed": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "value": "a" + }, + "version": 1000 + } + ] + } + ] + } + ] + }, + "Newer docs from bundles should keep not raise snapshot if there are unacknowledged writes": { + "describeName": "Listens source options:", + "itName": "Newer docs from bundles should keep not raise snapshot if there are unacknowledged writes", + "tags": [ + ], + "config": { + "numClients": 1, + "useEagerGCForMemory": false + }, + "steps": [ + { + "userListen": { + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + }, + "targetId": 2 + }, + "expectedState": { + "activeTargets": { + "2": { + "queries": [ + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "value": "a" + }, + "version": 250 + } + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-250" + ] + }, + { + "watchSnapshot": { + "targetIds": [ + ], + "version": 250 + }, + "expectedSnapshotEvents": [ + { + "added": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "value": "a" + }, + "version": 250 + } + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ] + }, + { + "userUnlisten": [ + 2, + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "expectedState": { + "activeTargets": { + } + } + }, + { + "watchRemove": { + "targetIds": [ + 2 + ] + } + }, + { + "userListen": { + "options": { + "source": "cache" + }, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + }, + "targetId": 2 + }, + "expectedSnapshotEvents": [ + { + "added": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "value": "a" + }, + "version": 250 + } + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ], + "expectedState": { + "activeTargets": { + } + } + }, + { + "userPatch": [ + "collection/a", + { + "value": "patched" + } + ], + "expectedSnapshotEvents": [ + { + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true, + "modified": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": true + }, + "value": { + "value": "patched" + }, + "version": 0 + } + ], + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ] + }, + { + "loadBundle": "127{\"metadata\":{\"id\":\"test-bundle\",\"createTime\":\"1970-01-01T00:00:00.001001000Z\",\"version\":1,\"totalDocuments\":1,\"totalBytes\":388}}154{\"documentMetadata\":{\"name\":\"projects/test-project/databases/(default)/documents/collection/a\",\"readTime\":\"1970-01-01T00:00:00.001001000Z\",\"exists\":true}}228{\"document\":{\"name\":\"projects/test-project/databases/(default)/documents/collection/a\",\"createTime\":\"1970-01-01T00:00:00.000250000Z\",\"updateTime\":\"1970-01-01T00:00:00.001001000Z\",\"fields\":{\"value\":{\"stringValue\":\"fromBundle\"}}}}" + } + ] + }, + "Newer docs from bundles should overwrite cache": { + "describeName": "Listens source options:", + "itName": "Newer docs from bundles should overwrite cache", + "tags": [ + ], + "config": { + "numClients": 1, + "useEagerGCForMemory": false + }, + "steps": [ + { + "userListen": { + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + }, + "targetId": 2 + }, + "expectedState": { + "activeTargets": { + "2": { + "queries": [ + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "value": "a" + }, + "version": 1000 + } + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ] + }, + { + "watchSnapshot": { + "targetIds": [ + ], + "version": 1000 + }, + "expectedSnapshotEvents": [ + { + "added": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "value": "a" + }, + "version": 1000 + } + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ] + }, + { + "userUnlisten": [ + 2, + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "expectedState": { + "activeTargets": { + } + } + }, + { + "watchRemove": { + "targetIds": [ + 2 + ] + } + }, + { + "userListen": { + "options": { + "source": "cache" + }, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + }, + "targetId": 2 + }, + "expectedSnapshotEvents": [ + { + "added": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "value": "a" + }, + "version": 1000 + } + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ], + "expectedState": { + "activeTargets": { + } + } + }, + { + "loadBundle": "127{\"metadata\":{\"id\":\"test-bundle\",\"createTime\":\"1970-01-01T00:00:00.003000000Z\",\"version\":1,\"totalDocuments\":1,\"totalBytes\":379}}154{\"documentMetadata\":{\"name\":\"projects/test-project/databases/(default)/documents/collection/a\",\"readTime\":\"1970-01-01T00:00:00.003000000Z\",\"exists\":true}}219{\"document\":{\"name\":\"projects/test-project/databases/(default)/documents/collection/a\",\"createTime\":\"1970-01-01T00:00:00.001999000Z\",\"updateTime\":\"1970-01-01T00:00:00.002999000Z\",\"fields\":{\"value\":{\"stringValue\":\"b\"}}}}", + "expectedSnapshotEvents": [ + { + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false, + "modified": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "value": "b" + }, + "version": 2999 + } + ], + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ] + } + ] + }, + "Newer docs from bundles should raise snapshot only when Watch catches up with acknowledged writes": { + "describeName": "Listens source options:", + "itName": "Newer docs from bundles should raise snapshot only when Watch catches up with acknowledged writes", + "tags": [ + ], + "config": { + "numClients": 1, + "useEagerGCForMemory": false + }, + "steps": [ + { + "userListen": { + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + }, + "targetId": 2 + }, + "expectedState": { + "activeTargets": { + "2": { + "queries": [ + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "value": "a" + }, + "version": 250 + } + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-250" + ] + }, + { + "watchSnapshot": { + "targetIds": [ + ], + "version": 250 + }, + "expectedSnapshotEvents": [ + { + "added": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "value": "a" + }, + "version": 250 + } + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ] + }, + { + "userUnlisten": [ + 2, + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "expectedState": { + "activeTargets": { + } + } + }, + { + "watchRemove": { + "targetIds": [ + 2 + ] + } + }, + { + "userListen": { + "options": { + "source": "cache" + }, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + }, + "targetId": 2 + }, + "expectedSnapshotEvents": [ + { + "added": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "value": "a" + }, + "version": 250 + } + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ], + "expectedState": { + "activeTargets": { + } + } + }, + { + "userPatch": [ + "collection/a", + { + "value": "patched" + } + ], + "expectedSnapshotEvents": [ + { + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true, + "modified": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": true + }, + "value": { + "value": "patched" + }, + "version": 0 + } + ], + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ] + }, + { + "writeAck": { + "version": 1000 + }, + "expectedState": { + "userCallbacks": { + "acknowledgedDocs": [ + "collection/a" + ], + "rejectedDocs": [ + ] + } + } + }, + { + "loadBundle": "127{\"metadata\":{\"id\":\"test-bundle\",\"createTime\":\"1970-01-01T00:00:00.000500000Z\",\"version\":1,\"totalDocuments\":1,\"totalBytes\":379}}154{\"documentMetadata\":{\"name\":\"projects/test-project/databases/(default)/documents/collection/a\",\"readTime\":\"1970-01-01T00:00:00.000500000Z\",\"exists\":true}}219{\"document\":{\"name\":\"projects/test-project/databases/(default)/documents/collection/a\",\"createTime\":\"1970-01-01T00:00:00.000250000Z\",\"updateTime\":\"1970-01-01T00:00:00.000500000Z\",\"fields\":{\"value\":{\"stringValue\":\"b\"}}}}" + }, + { + "loadBundle": "127{\"metadata\":{\"id\":\"test-bundle\",\"createTime\":\"1970-01-01T00:00:00.001001000Z\",\"version\":1,\"totalDocuments\":1,\"totalBytes\":388}}154{\"documentMetadata\":{\"name\":\"projects/test-project/databases/(default)/documents/collection/a\",\"readTime\":\"1970-01-01T00:00:00.001001000Z\",\"exists\":true}}228{\"document\":{\"name\":\"projects/test-project/databases/(default)/documents/collection/a\",\"createTime\":\"1970-01-01T00:00:00.000250000Z\",\"updateTime\":\"1970-01-01T00:00:00.001001000Z\",\"fields\":{\"value\":{\"stringValue\":\"fromBundle\"}}}}", + "expectedSnapshotEvents": [ + { + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false, + "modified": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "value": "fromBundle" + }, + "version": 1001 + } + ], + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ] + } + ] + }, + "Older deleted docs from bundles should do nothing": { + "describeName": "Listens source options:", + "itName": "Older deleted docs from bundles should do nothing", + "tags": [ + ], + "config": { + "numClients": 1, + "useEagerGCForMemory": false + }, + "steps": [ + { + "userListen": { + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + }, + "targetId": 2 + }, + "expectedState": { + "activeTargets": { + "2": { + "queries": [ + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "value": "a" + }, + "version": 1000 + } + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ] + }, + { + "watchSnapshot": { + "targetIds": [ + ], + "version": 1000 + }, + "expectedSnapshotEvents": [ + { + "added": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "value": "a" + }, + "version": 1000 + } + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ] + }, + { + "userUnlisten": [ + 2, + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "expectedState": { + "activeTargets": { + } + } + }, + { + "watchRemove": { + "targetIds": [ + 2 + ] + } + }, + { + "userListen": { + "options": { + "source": "cache" + }, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + }, + "targetId": 2 + }, + "expectedSnapshotEvents": [ + { + "added": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "value": "a" + }, + "version": 1000 + } + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ], + "expectedState": { + "activeTargets": { + } + } + }, + { + "loadBundle": "127{\"metadata\":{\"id\":\"test-bundle\",\"createTime\":\"1970-01-01T00:00:00.000999000Z\",\"version\":1,\"totalDocuments\":1,\"totalBytes\":158}}155{\"documentMetadata\":{\"name\":\"projects/test-project/databases/(default)/documents/collection/a\",\"readTime\":\"1970-01-01T00:00:00.000999000Z\",\"exists\":false}}" + } + ] + }, + "Primary client should not invoke watch request while all clients are listening to cache": { + "describeName": "Listens source options:", + "itName": "Primary client should not invoke watch request while all clients are listening to cache", + "tags": [ + "multi-client" + ], + "config": { + "numClients": 2, + "useEagerGCForMemory": false + }, + "steps": [ + { + "clientIndex": 0, + "drainQueue": true + }, + { + "applyClientState": { + "visibility": "visible" + }, + "clientIndex": 0 + }, + { + "clientIndex": 1, + "drainQueue": true + }, + { + "clientIndex": 1, + "userListen": { + "options": { + "source": "cache" + }, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + }, + "targetId": 2 + }, + "expectedSnapshotEvents": [ + { + "added": [ + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ], + "expectedState": { + "activeTargets": { + } + } + }, + { + "clientIndex": 0, + "drainQueue": true, + "expectedState": { + "activeTargets": { + } + } + }, + { + "clientIndex": 1, + "drainQueue": true + }, + { + "clientIndex": 1, + "userUnlisten": [ + 2, + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "expectedState": { + "activeTargets": { + } + } + }, + { + "clientIndex": 0, + "drainQueue": true, + "expectedState": { + "activeTargets": { + } + } + } + ] + }, + "Query is executed by primary client even if primary client only has listeners sourced from cache": { + "describeName": "Listens source options:", + "itName": "Query is executed by primary client even if primary client only has listeners sourced from cache", + "tags": [ + "multi-client" + ], + "config": { + "numClients": 2, + "useEagerGCForMemory": false + }, + "steps": [ + { + "clientIndex": 0, + "drainQueue": true + }, + { + "applyClientState": { + "visibility": "visible" + }, + "clientIndex": 0 + }, + { + "clientIndex": 0, + "userListen": { + "options": { + "source": "cache" + }, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + }, + "targetId": 2 + }, + "expectedSnapshotEvents": [ + { + "added": [ + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ], + "expectedState": { + "activeTargets": { + } + } + }, + { + "clientIndex": 1, + "drainQueue": true + }, + { + "clientIndex": 1, + "userListen": { + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + }, + "targetId": 2 + }, + "expectedState": { + "activeTargets": { + "2": { + "queries": [ + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "resumeToken": "" + } + } + } + }, + { + "clientIndex": 0, + "drainQueue": true, + "expectedState": { + "activeTargets": { + "2": { + "queries": [ + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "resumeToken": "" + } + } + } + }, + { + "clientIndex": 0, + "watchAck": [ + 2 + ] + }, + { + "clientIndex": 0, + "watchEntity": { + "docs": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "key": "a" + }, + "version": 1000 + } + ], + "targets": [ + 2 + ] + } + }, + { + "clientIndex": 0, + "watchSnapshot": { + "targetIds": [ + ], + "version": 1000 + }, + "expectedSnapshotEvents": [ + { + "added": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "key": "a" + }, + "version": 1000 + } + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ] + }, + { + "clientIndex": 1, + "drainQueue": true, + "expectedSnapshotEvents": [ + { + "added": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "key": "a" + }, + "version": 1000 + } + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ] + }, + { + "clientIndex": 0, + "drainQueue": true + }, + { + "clientIndex": 0, + "watchCurrent": [ + [ + 2 + ], + "resume-token-2000" + ] + }, + { + "clientIndex": 0, + "watchSnapshot": { + "targetIds": [ + ], + "version": 2000 + }, + "expectedSnapshotEvents": [ + { + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ] + }, + { + "clientIndex": 1, + "drainQueue": true, + "expectedSnapshotEvents": [ + { + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ] + } + ] + }, + "Query only raises events in participating clients": { + "describeName": "Listens source options:", + "itName": "Query only raises events in participating clients", + "tags": [ + "multi-client" + ], + "config": { + "numClients": 4, + "useEagerGCForMemory": false + }, + "steps": [ + { + "clientIndex": 0, + "drainQueue": true + }, + { + "applyClientState": { + "visibility": "visible" + }, + "clientIndex": 0 + }, + { + "clientIndex": 1, + "drainQueue": true + }, + { + "clientIndex": 2, + "drainQueue": true + }, + { + "clientIndex": 2, + "userListen": { + "options": { + "source": "cache" + }, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + }, + "targetId": 2 + }, + "expectedSnapshotEvents": [ + { + "added": [ + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ], + "expectedState": { + "activeTargets": { + } + } + }, + { + "clientIndex": 3, + "drainQueue": true + }, + { + "clientIndex": 3, + "userListen": { + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + }, + "targetId": 2 + }, + "expectedState": { + "activeTargets": { + "2": { + "queries": [ + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "resumeToken": "" + } + } + } + }, + { + "clientIndex": 0, + "drainQueue": true, + "expectedState": { + "activeTargets": { + "2": { + "queries": [ + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "resumeToken": "" + } + } + } + }, + { + "clientIndex": 0, + "watchAck": [ + 2 + ] + }, + { + "clientIndex": 0, + "watchEntity": { + "docs": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "key": "a" + }, + "version": 1000 + } + ], + "targets": [ + 2 + ] + } + }, + { + "clientIndex": 0, + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ] + }, + { + "clientIndex": 0, + "watchSnapshot": { + "targetIds": [ + ], + "version": 1000 + } + }, + { + "clientIndex": 1, + "drainQueue": true + }, + { + "clientIndex": 2, + "drainQueue": true, + "expectedSnapshotEvents": [ + { + "added": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "key": "a" + }, + "version": 1000 + } + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ] + }, + { + "clientIndex": 3, + "drainQueue": true, + "expectedSnapshotEvents": [ + { + "added": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "key": "a" + }, + "version": 1000 + } + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ] + } + ] + }, + "Un-listen to listeners from different source": { + "describeName": "Listens source options:", + "itName": "Un-listen to listeners from different source", + "tags": [ + "multi-client" + ], + "config": { + "numClients": 2, + "useEagerGCForMemory": false + }, + "steps": [ + { + "clientIndex": 0, + "drainQueue": true + }, + { + "applyClientState": { + "visibility": "visible" + }, + "clientIndex": 0 + }, + { + "clientIndex": 0, + "userListen": { + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + }, + "targetId": 2 + }, + "expectedState": { + "activeTargets": { + "2": { + "queries": [ + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "resumeToken": "" + } + } + } + }, + { + "clientIndex": 0, + "watchAck": [ + 2 + ] + }, + { + "clientIndex": 0, + "watchEntity": { + "docs": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "key": "a" + }, + "version": 1000 + } + ], + "targets": [ + 2 + ] + } + }, + { + "clientIndex": 0, + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ] + }, + { + "clientIndex": 0, + "watchSnapshot": { + "targetIds": [ + ], + "version": 1000 + }, + "expectedSnapshotEvents": [ + { + "added": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "key": "a" + }, + "version": 1000 + } + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ] + }, + { + "clientIndex": 1, + "drainQueue": true + }, + { + "clientIndex": 1, + "userListen": { + "options": { + "source": "cache" + }, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + }, + "targetId": 2 + }, + "expectedSnapshotEvents": [ + { + "added": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "key": "a" + }, + "version": 1000 + } + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ], + "expectedState": { + "activeTargets": { + } + } + }, + { + "clientIndex": 0, + "drainQueue": true + }, + { + "clientIndex": 0, + "userUnlisten": [ + 2, + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "expectedState": { + "activeTargets": { + } + } + }, + { + "clientIndex": 0, + "userSet": [ + "collection/b", + { + "key": "b" + } + ] + }, + { + "clientIndex": 1, + "drainQueue": true, + "expectedSnapshotEvents": [ + { + "added": [ + { + "createTime": 0, + "key": "collection/b", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": true + }, + "value": { + "key": "b" + }, + "version": 0 + } + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ] + }, + { + "clientIndex": 1, + "userUnlisten": [ + 2, + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "expectedState": { + "activeTargets": { + } + } + } + ] + }, + "onSnapshotsInSync fires for multiple listeners": { + "describeName": "Listens source options:", + "itName": "onSnapshotsInSync fires for multiple listeners", + "tags": [ + ], + "config": { + "numClients": 1, + "useEagerGCForMemory": false + }, + "steps": [ + { + "userListen": { + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + }, + "targetId": 2 + }, + "expectedState": { + "activeTargets": { + "2": { + "queries": [ + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "v": 1 + }, + "version": 1000 + } + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ] + }, + { + "watchSnapshot": { + "targetIds": [ + ], + "version": 1000 + }, + "expectedSnapshotEvents": [ + { + "added": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "v": 1 + }, + "version": 1000 + } + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ] + }, + { + "userUnlisten": [ + 2, + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "expectedState": { + "activeTargets": { + } + } + }, + { + "watchRemove": { + "targetIds": [ + 2 + ] + } + }, + { + "userListen": { + "options": { + "source": "cache" + }, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + }, + "targetId": 2 + }, + "expectedSnapshotEvents": [ + { + "added": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "v": 1 + }, + "version": 1000 + } + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ], + "expectedState": { + "activeTargets": { + } + } + }, + { + "addSnapshotsInSyncListener": true, + "expectedSnapshotsInSyncEvents": 1 + }, + { + "userSet": [ + "collection/a", + { + "v": 2 + } + ], + "expectedSnapshotEvents": [ + { + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true, + "modified": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": true + }, + "value": { + "v": 2 + }, + "version": 0 + } + ], + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ], + "expectedSnapshotsInSyncEvents": 1 + }, + { + "addSnapshotsInSyncListener": true, + "expectedSnapshotsInSyncEvents": 1 + }, + { + "addSnapshotsInSyncListener": true, + "expectedSnapshotsInSyncEvents": 1 + }, + { + "userSet": [ + "collection/a", + { + "v": 3 + } + ], + "expectedSnapshotEvents": [ + { + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true, + "modified": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": true + }, + "value": { + "v": 3 + }, + "version": 0 + } + ], + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ], + "expectedSnapshotsInSyncEvents": 3 + }, + { + "removeSnapshotsInSyncListener": true + }, + { + "userSet": [ + "collection/a", + { + "v": 4 + } + ], + "expectedSnapshotEvents": [ + { + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true, + "modified": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": true + }, + "value": { + "v": 4 + }, + "version": 0 + } + ], + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ], + "expectedSnapshotsInSyncEvents": 2 + } + ] + } +} diff --git a/Firestore/Example/Tests/Util/FSTIntegrationTestCase.h b/Firestore/Example/Tests/Util/FSTIntegrationTestCase.h index abd965beee3..a63fd66cd1b 100644 --- a/Firestore/Example/Tests/Util/FSTIntegrationTestCase.h +++ b/Firestore/Example/Tests/Util/FSTIntegrationTestCase.h @@ -35,6 +35,7 @@ @class FIRQuery; @class FIRWriteBatch; @class FSTEventAccumulator; +@class FIRTransaction; NS_ASSUME_NONNULL_BEGIN @@ -113,6 +114,10 @@ extern "C" { - (FIRDocumentReference *)addDocumentRef:(FIRCollectionReference *)ref data:(NSDictionary *)data; +- (void)runTransaction:(FIRFirestore *)db + block:(id _Nullable (^)(FIRTransaction *, NSError **error))block + completion:(nullable void (^)(id _Nullable result, NSError *_Nullable error))completion; + - (void)mergeDocumentRef:(FIRDocumentReference *)ref data:(NSDictionary *)data; - (void)mergeDocumentRef:(FIRDocumentReference *)ref diff --git a/Firestore/Example/Tests/Util/FSTIntegrationTestCase.mm b/Firestore/Example/Tests/Util/FSTIntegrationTestCase.mm index 6633ed03748..f42d10bcb01 100644 --- a/Firestore/Example/Tests/Util/FSTIntegrationTestCase.mm +++ b/Firestore/Example/Tests/Util/FSTIntegrationTestCase.mm @@ -569,6 +569,22 @@ - (FIRDocumentReference *)addDocumentRef:(FIRCollectionReference *)ref return doc; } +- (void)runTransaction:(FIRFirestore *)db + block:(id _Nullable (^)(FIRTransaction *, NSError **error))block + completion: + (nullable void (^)(id _Nullable result, NSError *_Nullable error))completion { + XCTestExpectation *expectation = [self expectationWithDescription:@"runTransaction"]; + [db runTransactionWithOptions:nil + block:block + completion:^(id _Nullable result, NSError *_Nullable error) { + if (completion) { + completion(result, error); + } + [expectation fulfill]; + }]; + [self awaitExpectation:expectation]; +} + - (void)mergeDocumentRef:(FIRDocumentReference *)ref data:(NSDictionary *)data { XCTestExpectation *expectation = [self expectationWithDescription:@"setDataWithMerge"]; [ref setData:data merge:YES completion:[self completionForExpectation:expectation]]; diff --git a/Firestore/Source/API/FIRDocumentReference.mm b/Firestore/Source/API/FIRDocumentReference.mm index b6f326ee8d9..a50a2c7df71 100644 --- a/Firestore/Source/API/FIRDocumentReference.mm +++ b/Firestore/Source/API/FIRDocumentReference.mm @@ -27,6 +27,7 @@ #import "Firestore/Source/API/FIRFirestoreSource+Internal.h" #import "Firestore/Source/API/FIRListenerRegistration+Internal.h" #import "Firestore/Source/API/FSTUserDataReader.h" +#import "Firestore/Source/API/converters.h" #include "Firestore/core/src/api/collection_reference.h" #include "Firestore/core/src/api/document_reference.h" @@ -50,6 +51,7 @@ using firebase::firestore::api::DocumentSnapshotListener; using firebase::firestore::api::Firestore; using firebase::firestore::api::ListenerRegistration; +using firebase::firestore::api::MakeListenSource; using firebase::firestore::api::MakeSource; using firebase::firestore::api::Source; using firebase::firestore::core::EventListener; @@ -212,6 +214,13 @@ - (void)getDocumentWithSource:(FIRFirestoreSource)source return [self addSnapshotListenerInternalWithOptions:options listener:listener]; } +- (id)addSnapshotListenerWithOptions:(FIRSnapshotListenOptions *)options + listener:(FIRDocumentSnapshotBlock)listener { + ListenOptions listenOptions = + ListenOptions::FromOptions(options.includeMetadataChanges, MakeListenSource(options.source)); + return [self addSnapshotListenerInternalWithOptions:listenOptions listener:listener]; +} + - (id)addSnapshotListenerInternalWithOptions:(ListenOptions)internalOptions listener:(FIRDocumentSnapshotBlock) listener { diff --git a/Firestore/Source/API/FIRQuery.mm b/Firestore/Source/API/FIRQuery.mm index 98518183049..d4185488341 100644 --- a/Firestore/Source/API/FIRQuery.mm +++ b/Firestore/Source/API/FIRQuery.mm @@ -37,6 +37,7 @@ #import "Firestore/Source/API/FIRQuerySnapshot+Internal.h" #import "Firestore/Source/API/FIRSnapshotMetadata+Internal.h" #import "Firestore/Source/API/FSTUserDataReader.h" +#import "Firestore/Source/API/converters.h" #include "Firestore/core/src/api/query_core.h" #include "Firestore/core/src/api/query_listener_registration.h" @@ -69,6 +70,7 @@ using firebase::firestore::google_firestore_v1_Value; using firebase::firestore::google_firestore_v1_Value_fields; using firebase::firestore::api::Firestore; +using firebase::firestore::api::MakeListenSource; using firebase::firestore::api::Query; using firebase::firestore::api::QueryListenerRegistration; using firebase::firestore::api::QuerySnapshot; @@ -191,6 +193,13 @@ - (void)getDocumentsWithSource:(FIRFirestoreSource)publicSource return [self addSnapshotListenerInternalWithOptions:options listener:listener]; } +- (id)addSnapshotListenerWithOptions:(FIRSnapshotListenOptions *)options + listener:(FIRQuerySnapshotBlock)listener { + ListenOptions listenOptions = + ListenOptions::FromOptions(options.includeMetadataChanges, MakeListenSource(options.source)); + return [self addSnapshotListenerInternalWithOptions:listenOptions listener:listener]; +} + - (id)addSnapshotListenerInternalWithOptions:(ListenOptions)internalOptions listener: (FIRQuerySnapshotBlock)listener { diff --git a/Firestore/Source/API/FIRSnapshotListenOptions.mm b/Firestore/Source/API/FIRSnapshotListenOptions.mm new file mode 100644 index 00000000000..9e22d686428 --- /dev/null +++ b/Firestore/Source/API/FIRSnapshotListenOptions.mm @@ -0,0 +1,63 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "FIRSnapshotListenOptions.h" + +#import + +#include +#include + +NS_ASSUME_NONNULL_BEGIN + +@implementation FIRSnapshotListenOptions + +- (instancetype)initPrivate:(FIRListenSource)source + includeMetadataChanges:(BOOL)includeMetadataChanges { + self = [self init]; + if (self) { + _source = source; + _includeMetadataChanges = includeMetadataChanges; + } + return self; +} + +- (instancetype)init { + self = [super init]; + if (self) { + _source = FIRListenSourceDefault; + _includeMetadataChanges = NO; + } + return self; +} + +- (FIRSnapshotListenOptions *)optionsWithIncludeMetadataChanges:(BOOL)includeMetadataChanges { + FIRSnapshotListenOptions *newOptions = + [[FIRSnapshotListenOptions alloc] initPrivate:self.source + includeMetadataChanges:includeMetadataChanges]; + return newOptions; +} + +- (FIRSnapshotListenOptions *)optionsWithSource:(FIRListenSource)source { + FIRSnapshotListenOptions *newOptions = + [[FIRSnapshotListenOptions alloc] initPrivate:source + includeMetadataChanges:self.includeMetadataChanges]; + return newOptions; +} + +@end + +NS_ASSUME_NONNULL_END \ No newline at end of file diff --git a/Firestore/Source/API/converters.h b/Firestore/Source/API/converters.h index f5d0e4545b8..e777ba81a2c 100644 --- a/Firestore/Source/API/converters.h +++ b/Firestore/Source/API/converters.h @@ -24,6 +24,8 @@ #import #include +#import "FIRSnapshotListenOptions.h" +#import "Firestore/core/src/api/listen_source.h" @class FIRGeoPoint; @class FIRTimestamp; @@ -62,6 +64,8 @@ FIRTimestamp* MakeFIRTimestamp(const Timestamp& timestamp); FIRDocumentReference* MakeFIRDocumentReference(const model::DocumentKey& document_key, std::shared_ptr firestore); +ListenSource MakeListenSource(const FIRListenSource& source); + } // namespace api } // namespace firestore } // namespace firebase diff --git a/Firestore/Source/API/converters.mm b/Firestore/Source/API/converters.mm index 251d3e578bf..8bfd5a07090 100644 --- a/Firestore/Source/API/converters.mm +++ b/Firestore/Source/API/converters.mm @@ -25,6 +25,7 @@ #include "Firestore/core/include/firebase/firestore/geo_point.h" #include "Firestore/core/include/firebase/firestore/timestamp.h" #include "Firestore/core/src/api/firestore.h" +#import "Firestore/core/src/api/listen_source.h" #include "Firestore/core/src/model/document_key.h" NS_ASSUME_NONNULL_BEGIN @@ -61,6 +62,17 @@ Timestamp MakeTimestamp(NSDate* date) { return [[FIRDocumentReference alloc] initWithKey:key firestore:std::move(firestore)]; } +ListenSource MakeListenSource(const FIRListenSource& source) { + switch (source) { + case FIRListenSourceDefault: + return ListenSource::Default; + case FIRListenSourceCache: + return ListenSource::Cache; + default: + return ListenSource::Default; + } +} + } // namespace api } // namespace firestore } // namespace firebase diff --git a/Firestore/Source/Public/FirebaseFirestore/FIRDocumentReference.h b/Firestore/Source/Public/FirebaseFirestore/FIRDocumentReference.h index 70dbd0d02ad..b6f87450cb3 100644 --- a/Firestore/Source/Public/FirebaseFirestore/FIRDocumentReference.h +++ b/Firestore/Source/Public/FirebaseFirestore/FIRDocumentReference.h @@ -18,6 +18,7 @@ #import "FIRFirestoreSource.h" #import "FIRListenerRegistration.h" +#import "FIRSnapshotListenOptions.h" @class FIRCollectionReference; @class FIRDocumentSnapshot; @@ -270,6 +271,22 @@ addSnapshotListenerWithIncludeMetadataChanges:(BOOL)includeMetadataChanges NS_SWIFT_NAME(addSnapshotListener(includeMetadataChanges:listener:)); // clang-format on +/** + * Attaches a listener for `DocumentSnapshot` events. + * + * @param options Sets snapshot listener options, including whether metadata-only changes should + * trigger snapshot events, the source to listen to, the executor to use to call the + * listener, or the activity to scope the listener to. + * @param listener The listener to attach. + * + * @return A `ListenerRegistration` that can be used to remove this listener. + */ +- (id) + addSnapshotListenerWithOptions:(FIRSnapshotListenOptions *)options + listener:(void (^)(FIRDocumentSnapshot *_Nullable snapshot, + NSError *_Nullable error))listener + NS_SWIFT_NAME(addSnapshotListener(options:listener:)); + @end NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Public/FirebaseFirestore/FIRQuery.h b/Firestore/Source/Public/FirebaseFirestore/FIRQuery.h index bde102c212a..c75952876a2 100644 --- a/Firestore/Source/Public/FirebaseFirestore/FIRQuery.h +++ b/Firestore/Source/Public/FirebaseFirestore/FIRQuery.h @@ -18,6 +18,7 @@ #import "FIRFirestoreSource.h" #import "FIRListenerRegistration.h" +#import "FIRSnapshotListenOptions.h" @class FIRAggregateQuery; @class FIRAggregateField; @@ -104,6 +105,21 @@ NS_SWIFT_NAME(Query) NSError *_Nullable error))listener NS_SWIFT_NAME(addSnapshotListener(includeMetadataChanges:listener:)); +/** + * Attaches a listener for `QuerySnapshot` events. + * @param options Sets snapshot listener options, including whether metadata-only changes should + * trigger snapshot events, the source to listen to, the executor to use to call the + * listener, or the activity to scope the listener to. + * @param listener The listener to attach. + * + * @return A `ListenerRegistration` that can be used to remove this listener. + */ +- (id) + addSnapshotListenerWithOptions:(FIRSnapshotListenOptions *)options + listener:(void (^)(FIRQuerySnapshot *_Nullable snapshot, + NSError *_Nullable error))listener + NS_SWIFT_NAME(addSnapshotListener(options:listener:)); + #pragma mark - Filtering Data /** * Creates and returns a new Query with the additional filter. diff --git a/Firestore/Source/Public/FirebaseFirestore/FIRSnapshotListenOptions.h b/Firestore/Source/Public/FirebaseFirestore/FIRSnapshotListenOptions.h new file mode 100644 index 00000000000..13d9903abef --- /dev/null +++ b/Firestore/Source/Public/FirebaseFirestore/FIRSnapshotListenOptions.h @@ -0,0 +1,83 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + * The source the snapshot listener retrieves data from. + */ +typedef NS_ENUM(NSUInteger, FIRListenSource) { + /** + * The default behavior. The listener attempts to return initial snapshot from cache and retrieve + * up-to-date snapshots from the Firestore server. Snapshot events will be triggered on local + * mutations and server-side updates. + */ + FIRListenSourceDefault, + /** + * The listener retrieves data and listens to updates from the local Firestore cache without + * attempting to send the query to the server. If some documents gets updated as a result from + * other queries, they will be picked up by listeners using the cache. + * + * Note that the data might be stale if the cache hasn't synchronized with recent server-side + * changes. + */ + FIRListenSourceCache +} NS_SWIFT_NAME(ListenSource); + +/** + * Options to configure the behavior of `Firestore.addSnapshotListenerWithOptions()`. Instances + * of this class control settings like whether metadata-only changes trigger events and the + * preferred data source. + */ +NS_SWIFT_NAME(SnapshotListenOptions) +@interface FIRSnapshotListenOptions : NSObject + +/** The source the snapshot listener retrieves data from. */ +@property(nonatomic, readonly) FIRListenSource source; +/** Indicates whether metadata-only changes should trigger snapshot events. */ +@property(nonatomic, readonly) BOOL includeMetadataChanges; + +/** + * Creates and returns a new `SnapshotListenOptions` object with all properties initialized to their + * default values. + * + * @return The created `SnapshotListenOptions` object. + */ +- (instancetype)init NS_DESIGNATED_INITIALIZER; + +/** + * Creates and returns a new `SnapshotListenOptions` object with with all properties of the current + * `SnapshotListenOptions` object plus the new property specifying whether metadata-only changes + * should trigger snapshot events + * + * @return The created `SnapshotListenOptions` object. + */ +- (FIRSnapshotListenOptions *)optionsWithIncludeMetadataChanges:(BOOL)includeMetadataChanges; + +/** + * Creates and returns a new `SnapshotListenOptions` object with with all properties of the current + * `SnapshotListenOptions` object plus the new property specifying the source that the snapshot + * listener listens to. + * + * @return The created `SnapshotListenOptions` object. + */ +- (FIRSnapshotListenOptions *)optionsWithSource:(FIRListenSource)source; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Public/FirebaseFirestore/FirebaseFirestore.h b/Firestore/Source/Public/FirebaseFirestore/FirebaseFirestore.h index 6746aa489e8..9a0574a8207 100644 --- a/Firestore/Source/Public/FirebaseFirestore/FirebaseFirestore.h +++ b/Firestore/Source/Public/FirebaseFirestore/FirebaseFirestore.h @@ -34,6 +34,7 @@ #import "FIRLocalCacheSettings.h" #import "FIRQuery.h" #import "FIRQuerySnapshot.h" +#import "FIRSnapshotListenOptions.h" #import "FIRSnapshotMetadata.h" #import "FIRTimestamp.h" #import "FIRTransaction.h" diff --git a/Firestore/Swift/Tests/BridgingHeader.h b/Firestore/Swift/Tests/BridgingHeader.h index 5bfed83adf4..58165c82944 100644 --- a/Firestore/Swift/Tests/BridgingHeader.h +++ b/Firestore/Swift/Tests/BridgingHeader.h @@ -18,6 +18,7 @@ #define FIRESTORE_SWIFT_TESTS_BRIDGINGHEADER_H_ #import "Firestore/Example/Tests/API/FSTAPIHelpers.h" +#import "Firestore/Example/Tests/Util/FSTEventAccumulator.h" #import "Firestore/Example/Tests/Util/FSTExceptionCatcher.h" #import "Firestore/Example/Tests/Util/FSTIntegrationTestCase.h" diff --git a/Firestore/Swift/Tests/Integration/SnapshotListenerSourceTests.swift b/Firestore/Swift/Tests/Integration/SnapshotListenerSourceTests.swift new file mode 100644 index 00000000000..22c69679224 --- /dev/null +++ b/Firestore/Swift/Tests/Integration/SnapshotListenerSourceTests.swift @@ -0,0 +1,684 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import FirebaseFirestore +import FirebaseFirestoreSwift +import Foundation + +@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) +class SnapshotListenerSourceTests: FSTIntegrationTestCase { + func assertQuerySnapshotDataEquals(_ snapshot: Any, + _ expectedData: [[String: Any]]) throws { + let extractedData = FIRQuerySnapshotGetData(snapshot as! QuerySnapshot) + guard extractedData.count == expectedData.count else { + XCTFail( + "Result count mismatch: Expected \(expectedData.count), got \(extractedData.count)" + ) + return + } + for index in 0 ..< extractedData.count { + XCTAssertTrue(areDictionariesEqual(extractedData[index], expectedData[index])) + } + } + + // TODO(swift testing): update the function to be able to check other value types as well. + func areDictionariesEqual(_ dict1: [String: Any], _ dict2: [String: Any]) -> Bool { + guard dict1.count == dict2.count + else { return false } // Check if the number of elements matches + + for (key, value1) in dict1 { + guard let value2 = dict2[key] else { return false } + + // Value Checks (Assuming consistent types after the type check) + if let str1 = value1 as? String, let str2 = value2 as? String { + if str1 != str2 { return false } + } else if let int1 = value1 as? Int, let int2 = value2 as? Int { + if int1 != int2 { return false } + } else { + // Handle other potential types or return false for mismatch + return false + } + } + return true + } + + func testCanRaiseSnapshotFromCacheForQuery() throws { + let collRef = collectionRef(withDocuments: ["a": ["k": "a"]]) + readDocumentSet(forRef: collRef) // populate the cache. + + let options = SnapshotListenOptions().withSource(ListenSource.cache) + let registration = collRef.addSnapshotListener( + options: options, + listener: eventAccumulator.valueEventHandler + ) + + let querySnap = eventAccumulator.awaitEvent(withName: "snapshot") + try assertQuerySnapshotDataEquals(querySnap, [["k": "a"]]) + XCTAssertEqual(querySnap.metadata.isFromCache, true) + + eventAccumulator.assertNoAdditionalEvents() + registration.remove() + } + + func testCanRaiseSnapshotFromCacheForDocumentReference() throws { + let docRef = documentRef() + docRef.setData(["k": "a"]) + readDocument(forRef: docRef) // populate the cache. + + let options = SnapshotListenOptions().withSource(ListenSource.cache) + let registration = docRef.addSnapshotListener( + options: options, + listener: eventAccumulator.valueEventHandler + ) + + let docSnap = eventAccumulator.awaitEvent(withName: "snapshot") as! DocumentSnapshot + XCTAssertEqual(docSnap.data() as! [String: String], ["k": "a"]) + XCTAssertEqual(docSnap.metadata.isFromCache, true) + + eventAccumulator.assertNoAdditionalEvents() + registration.remove() + } + + func testListenToCacheShouldNotBeAffectedByOnlineStatusChange() throws { + let collRef = collectionRef(withDocuments: ["a": ["k": "a"]]) + readDocumentSet(forRef: collRef) // populate the cache. + + let options = SnapshotListenOptions().withSource(ListenSource.cache) + .withIncludeMetadataChanges(true) + let registration = collRef.addSnapshotListener( + options: options, + listener: eventAccumulator.valueEventHandler + ) + + let querySnap = eventAccumulator.awaitEvent(withName: "snapshot") + try assertQuerySnapshotDataEquals(querySnap, [["k": "a"]]) + XCTAssertEqual(querySnap.metadata.isFromCache, true) + + disableNetwork() + enableNetwork() + + eventAccumulator.assertNoAdditionalEvents() + registration.remove() + } + + func testMultipleListenersSourcedFromCacheCanWorkIndependently() throws { + let collRef = collectionRef(withDocuments: [ + "a": ["k": "a", "sort": 0], + "b": ["k": "b", "sort": 1], + ]) + readDocumentSet(forRef: collRef) // populate the cache. + + let query = collRef.whereField("sort", isGreaterThan: 0).order(by: "sort") + + let options = SnapshotListenOptions().withSource(ListenSource.cache) + let registration1 = query.addSnapshotListener( + options: options, + listener: eventAccumulator.valueEventHandler + ) + let registration2 = query.addSnapshotListener( + options: options, + listener: eventAccumulator.valueEventHandler + ) + + var expected = [["k": "b", "sort": 1]] + var querySnap = eventAccumulator.awaitEvent(withName: "snapshot") + try assertQuerySnapshotDataEquals(querySnap, expected) + XCTAssertEqual(querySnap.metadata.isFromCache, true) + querySnap = eventAccumulator.awaitEvent(withName: "snapshot") + try assertQuerySnapshotDataEquals(querySnap, expected) + XCTAssertEqual(querySnap.metadata.isFromCache, true) + + // Do a local mutation + addDocumentRef(collRef, data: ["k": "c", "sort": 2]) + + expected = [["k": "b", "sort": 1], ["k": "c", "sort": 2]] + querySnap = eventAccumulator.awaitEvent(withName: "snapshot") + try assertQuerySnapshotDataEquals(querySnap, expected) + XCTAssertEqual(querySnap.metadata.isFromCache, true) + querySnap = eventAccumulator.awaitEvent(withName: "snapshot") + try assertQuerySnapshotDataEquals(querySnap, expected) + XCTAssertEqual(querySnap.metadata.isFromCache, true) + + // Detach one listener, and do a local mutation. The other listener + // should not be affected. + registration1.remove() + addDocumentRef(collRef, data: ["k": "d", "sort": 3]) + + expected = [["k": "b", "sort": 1], ["k": "c", "sort": 2], ["k": "d", "sort": 3]] + querySnap = eventAccumulator.awaitEvent(withName: "snapshot") + try assertQuerySnapshotDataEquals(querySnap, expected) + XCTAssertEqual(querySnap.metadata.isFromCache, true) + + eventAccumulator.assertNoAdditionalEvents() + registration2.remove() + } + + // Two queries that mapped to the same target ID are referred to as + // "mirror queries". An example for a mirror query is a limitToLast() + // query and a limit() query that share the same backend Target ID. + // Since limitToLast() queries are sent to the backend with a modified + // orderBy() clause, they can map to the same target representation as + // limit() query, even if both queries appear separate to the user. + func testListenUnlistenRelistenToMirrorQueriesFromCache() throws { + let collRef = collectionRef(withDocuments: [ + "a": ["k": "a", "sort": 0], + "b": ["k": "b", "sort": 1], + "c": ["k": "c", "sort": 1], + ]) + readDocumentSet(forRef: collRef) // populate the cache. + let options = SnapshotListenOptions().withSource(ListenSource.cache) + + // Setup a `limit` query. + let limit = collRef.order(by: "sort", descending: false).limit(to: 2) + let limitAccumulator = FSTEventAccumulator.init(forTest: self) + var limitRegistration = limit.addSnapshotListener( + options: options, + listener: limitAccumulator.valueEventHandler + ) + // Setup a mirroring `limitToLast` query. + let limitToLast = collRef.order(by: "sort", descending: true).limit(toLast: 2) + let limitToLastAccumulator = FSTEventAccumulator + .init(forTest: self) + var limitToLastRegistration = limitToLast.addSnapshotListener( + options: options, + listener: limitToLastAccumulator.valueEventHandler + ) + + // Verify both queries get expected result. + var querySnap = limitAccumulator.awaitEvent(withName: "snapshot") + try assertQuerySnapshotDataEquals(querySnap, [["k": "a", "sort": 0], ["k": "b", "sort": 1]]) + XCTAssertEqual(querySnap.metadata.isFromCache, true) + + querySnap = limitToLastAccumulator.awaitEvent(withName: "snapshot") + try assertQuerySnapshotDataEquals(querySnap, [["k": "b", "sort": 1], ["k": "a", "sort": 0]]) + XCTAssertEqual(querySnap.metadata.isFromCache, true) + + // Un-listen then re-listen to the limit query. + limitRegistration.remove() + limitRegistration = limit.addSnapshotListener( + options: options, + listener: limitAccumulator.valueEventHandler + ) + querySnap = limitAccumulator.awaitEvent(withName: "snapshot") + try assertQuerySnapshotDataEquals( + querySnap, + [["k": "a", "sort": 0], ["k": "b", "sort": 1]] + ) + XCTAssertEqual(querySnap.metadata.isFromCache, true) + + // Add a document that would change the result set. + addDocumentRef(collRef, data: ["k": "d", "sort": -1]) + + // Verify both queries get expected result. + querySnap = limitAccumulator.awaitEvent(withName: "snapshot") + try assertQuerySnapshotDataEquals(querySnap, [["k": "d", "sort": -1], ["k": "a", "sort": 0]]) + XCTAssertEqual(querySnap.metadata.isFromCache, true) + XCTAssertEqual(querySnap.metadata.hasPendingWrites, true) + querySnap = limitToLastAccumulator.awaitEvent(withName: "snapshot") + try assertQuerySnapshotDataEquals(querySnap, [["k": "a", "sort": 0], ["k": "d", "sort": -1]]) + XCTAssertEqual(querySnap.metadata.isFromCache, true) + XCTAssertEqual(querySnap.metadata.hasPendingWrites, true) + + // Un-listen to limitToLast, update a doc, then re-listen to limitToLast + limitToLastRegistration.remove() + updateDocumentRef(collRef.document("a"), data: ["k": "a", "sort": -2]) + limitToLastRegistration = limitToLast.addSnapshotListener( + options: options, + listener: limitToLastAccumulator.valueEventHandler + ) + + // Verify both queries get expected result. + querySnap = limitAccumulator.awaitEvent(withName: "snapshot") + try assertQuerySnapshotDataEquals(querySnap, [["k": "a", "sort": -2], ["k": "d", "sort": -1]]) + XCTAssertEqual(querySnap.metadata.isFromCache, true) + XCTAssertEqual(querySnap.metadata.hasPendingWrites, true) + querySnap = limitToLastAccumulator.awaitEvent(withName: "snapshot") + try assertQuerySnapshotDataEquals(querySnap, [["k": "d", "sort": -1], ["k": "a", "sort": -2]]) + XCTAssertEqual(querySnap.metadata.isFromCache, true) + // We listened to LimitToLast query after the doc update. + XCTAssertEqual(querySnap.metadata.hasPendingWrites, false) + } + + func testCanListenToDefaultSourceFirstAndThenCache() throws { + let collRef = collectionRef(withDocuments: [ + "a": ["k": "a", "sort": 0], + "b": ["k": "b", "sort": 1], + ]) + let query = collRef.whereField("sort", isGreaterThanOrEqualTo: 1).order(by: "sort") + + // Listen to the query with default options, which will also populates the cache + let defaultAccumulator = FSTEventAccumulator.init(forTest: self) + let defaultRegistration = query.addSnapshotListener(defaultAccumulator.valueEventHandler) + + var querySnap = defaultAccumulator.awaitEvent(withName: "snapshot") + try assertQuerySnapshotDataEquals(querySnap, [["k": "b", "sort": 1]]) + XCTAssertEqual(querySnap.metadata.isFromCache, false) + + // Listen to the same query from cache + let cacheAccumulator = FSTEventAccumulator + .init(forTest: self) + let options = SnapshotListenOptions().withSource(ListenSource.cache) + let cacheRegistration = query.addSnapshotListener( + options: options, + listener: cacheAccumulator.valueEventHandler + ) + querySnap = cacheAccumulator.awaitEvent(withName: "snapshot") + try assertQuerySnapshotDataEquals(querySnap, [["k": "b", "sort": 1]]) + // The metadata is sync with server due to the default listener + XCTAssertEqual(querySnap.metadata.isFromCache, false) + + defaultAccumulator.assertNoAdditionalEvents() + cacheAccumulator.assertNoAdditionalEvents() + defaultRegistration.remove() + cacheRegistration.remove() + } + + func testCanListenToCacheSourceFirstAndThenDefault() throws { + let collRef = collectionRef(withDocuments: [ + "a": ["k": "a", "sort": 0], + "b": ["k": "b", "sort": 1], + ]) + let query = collRef.whereField("sort", isNotEqualTo: 0).order(by: "sort") + + // Listen to the cache + let cacheAccumulator = FSTEventAccumulator + .init(forTest: self) + let options = SnapshotListenOptions().withSource(ListenSource.cache) + let cacheRegistration = query.addSnapshotListener( + options: options, + listener: cacheAccumulator.valueEventHandler + ) + var querySnap = cacheAccumulator.awaitEvent(withName: "snapshot") + // Cache is empty + try assertQuerySnapshotDataEquals(querySnap, []) + XCTAssertEqual(querySnap.metadata.isFromCache, true) + + // Listen to the same query from server + let defaultAccumulator = FSTEventAccumulator.init(forTest: self) + let defaultRegistration = query.addSnapshotListener(defaultAccumulator.valueEventHandler) + querySnap = defaultAccumulator.awaitEvent(withName: "snapshot") + try assertQuerySnapshotDataEquals(querySnap, [["k": "b", "sort": 1]]) + XCTAssertEqual(querySnap.metadata.isFromCache, false) + + // Default listener updates the cache, whish triggers cache listener to raise snapshot. + querySnap = cacheAccumulator.awaitEvent(withName: "snapshot") + try assertQuerySnapshotDataEquals(querySnap, [["k": "b", "sort": 1]]) + // The metadata is sync with server due to the default listener + XCTAssertEqual(querySnap.metadata.isFromCache, false) + + defaultAccumulator.assertNoAdditionalEvents() + cacheAccumulator.assertNoAdditionalEvents() + defaultRegistration.remove() + cacheRegistration.remove() + } + + func testWillNotGetMetadataOnlyUpdatesIfListeningToCacheOnly() throws { + let collRef = collectionRef(withDocuments: [ + "a": ["k": "a", "sort": 0], + "b": ["k": "b", "sort": 1], + ]) + readDocumentSet(forRef: collRef) // populate the cache. + + let query = collRef.whereField("sort", isNotEqualTo: 0).order(by: "sort") + let options = SnapshotListenOptions().withSource(ListenSource.cache) + + let registration = query.addSnapshotListener( + options: options, + listener: eventAccumulator.valueEventHandler + ) + + var querySnap = eventAccumulator.awaitEvent(withName: "snapshot") + try assertQuerySnapshotDataEquals(querySnap, [["k": "b", "sort": 1]]) + XCTAssertEqual(querySnap.metadata.isFromCache, true) + + // Do a local mutation + addDocumentRef(collRef, data: ["k": "c", "sort": 2]) + + querySnap = eventAccumulator.awaitEvent(withName: "snapshot") + try assertQuerySnapshotDataEquals(querySnap, [["k": "b", "sort": 1], ["k": "c", "sort": 2]]) + XCTAssertEqual(querySnap.metadata.isFromCache, true) + XCTAssertEqual(querySnap.metadata?.hasPendingWrites, true) + + // As we are not listening to server, the listener will not get notified + // when local mutation is acknowledged by server. + eventAccumulator.assertNoAdditionalEvents() + registration.remove() + } + + func testWillHaveSynceMetadataUpdatesWhenListeningToBothCacheAndDefaultSource() throws { + let collRef = collectionRef(withDocuments: [ + "a": ["k": "a", "sort": 0], + "b": ["k": "b", "sort": 1], + ]) + readDocumentSet(forRef: collRef) // populate the cache. + let query = collRef.whereField("sort", isNotEqualTo: 0).order(by: "sort") + + // Listen to the cache + let cacheAccumulator = FSTEventAccumulator.init(forTest: self) + let options = SnapshotListenOptions().withSource(ListenSource.cache) + .withIncludeMetadataChanges(true) + let cacheRegistration = query.addSnapshotListener( + options: options, + listener: cacheAccumulator.valueEventHandler + ) + var querySnap = cacheAccumulator.awaitEvent(withName: "snapshot") + var expected = [["k": "b", "sort": 1]] + try assertQuerySnapshotDataEquals(querySnap, expected) + XCTAssertEqual(querySnap.metadata.isFromCache, true) + + // Listen to the same query from server + let defaultAccumulator = FSTEventAccumulator.init(forTest: self) + let defaultRegistration = query.addSnapshotListener( + includeMetadataChanges: true, + listener: defaultAccumulator.valueEventHandler + ) + + querySnap = defaultAccumulator.awaitEvent(withName: "snapshot") + try assertQuerySnapshotDataEquals(querySnap, expected) + // First snapshot will be raised from cache. + XCTAssertEqual(querySnap.metadata.isFromCache, true) + querySnap = defaultAccumulator.awaitEvent(withName: "snapshot") + // Second snapshot will be raised from server result + XCTAssertEqual(querySnap.metadata.isFromCache, false) + + // As listening to metadata changes, the cache listener also gets triggered and synced + // with default listener. + querySnap = cacheAccumulator.awaitEvent(withName: "snapshot") + // The metadata is sync with server due to the default listener + XCTAssertEqual(querySnap.metadata.isFromCache, false) + + // Do a local mutation + addDocumentRef(collRef, data: ["k": "c", "sort": 2]) + + // snapshot gets triggered by local mutation + expected = [["k": "b", "sort": 1], ["k": "c", "sort": 2]] + querySnap = defaultAccumulator.awaitEvent(withName: "snapshot") + try assertQuerySnapshotDataEquals(querySnap, expected) + XCTAssertEqual(querySnap.metadata.hasPendingWrites, true) + XCTAssertEqual(querySnap.metadata.isFromCache, false) + querySnap = cacheAccumulator.awaitEvent(withName: "snapshot") + try assertQuerySnapshotDataEquals(querySnap, expected) + XCTAssertEqual(querySnap.metadata.hasPendingWrites, true) + XCTAssertEqual(querySnap.metadata.isFromCache, false) + + // Local mutation gets acknowledged by the server + querySnap = defaultAccumulator.awaitEvent(withName: "snapshot") + XCTAssertEqual(querySnap.metadata.hasPendingWrites, false) + XCTAssertEqual(querySnap.metadata.isFromCache, false) + querySnap = cacheAccumulator.awaitEvent(withName: "snapshot") + XCTAssertEqual(querySnap.metadata.hasPendingWrites, false) + XCTAssertEqual(querySnap.metadata.isFromCache, false) + + defaultAccumulator.assertNoAdditionalEvents() + cacheAccumulator.assertNoAdditionalEvents() + defaultRegistration.remove() + cacheRegistration.remove() + } + + func testCanUnlistenToDefaultSourceWhileStillListeningToCache() throws { + let collRef = collectionRef(withDocuments: [ + "a": ["k": "a", "sort": 0], + "b": ["k": "b", "sort": 1], + ]) + let query = collRef.whereField("sort", isNotEqualTo: 0).order(by: "sort") + + // Listen to the query with both source options + let defaultAccumulator = FSTEventAccumulator.init(forTest: self) + let defaultRegistration = query.addSnapshotListener(defaultAccumulator.valueEventHandler) + defaultAccumulator.awaitEvent(withName: "snapshot") + let cacheAccumulator = FSTEventAccumulator + .init(forTest: self) + let options = SnapshotListenOptions().withSource(ListenSource.cache) + let cacheRegistration = query.addSnapshotListener( + options: options, + listener: cacheAccumulator.valueEventHandler + ) + cacheAccumulator.awaitEvent(withName: "snapshot") + + // Un-listen to the default listener. + defaultRegistration.remove() + + // Add a document and verify listener to cache works as expected + addDocumentRef(collRef, data: ["k": "c", "sort": -1]) + defaultAccumulator.assertNoAdditionalEvents() + + let querySnap = cacheAccumulator.awaitEvent(withName: "snapshot") + try assertQuerySnapshotDataEquals( + querySnap, + [["k": "c", "sort": -1], ["k": "b", "sort": 1]] + ) + + cacheAccumulator.assertNoAdditionalEvents() + cacheRegistration.remove() + } + + func testCanUnlistenToCacheSourceWhileStillListeningToServer() throws { + let collRef = collectionRef(withDocuments: [ + "a": ["k": "a", "sort": 0], + "b": ["k": "b", "sort": 1], + ]) + let query = collRef.whereField("sort", isNotEqualTo: 0).order(by: "sort") + + // Listen to the query with both source options + let defaultAccumulator = FSTEventAccumulator.init(forTest: self) + let defaultRegistration = query.addSnapshotListener(defaultAccumulator.valueEventHandler) + defaultAccumulator.awaitEvent(withName: "snapshot") + let cacheAccumulator = FSTEventAccumulator + .init(forTest: self) + let options = SnapshotListenOptions().withSource(ListenSource.cache) + let cacheRegistration = query.addSnapshotListener( + options: options, + listener: cacheAccumulator.valueEventHandler + ) + cacheAccumulator.awaitEvent(withName: "snapshot") + + // Un-listen to cache. + cacheRegistration.remove() + + // Add a document and verify listener to server works as expected. + addDocumentRef(collRef, data: ["k": "c", "sort": -1]) + cacheAccumulator.assertNoAdditionalEvents() + + let querySnap = defaultAccumulator.awaitEvent(withName: "snapshot") + try assertQuerySnapshotDataEquals( + querySnap, + [["k": "c", "sort": -1], ["k": "b", "sort": 1]] + ) + + defaultAccumulator.assertNoAdditionalEvents() + defaultRegistration.remove() + } + + func testCanListenUnlistenRelistenToSameQueryWithDifferentSourceOptions() throws { + let collRef = collectionRef(withDocuments: [ + "a": ["k": "a", "sort": 0], + "b": ["k": "b", "sort": 1], + ]) + let query = collRef.whereField("sort", isGreaterThan: 0).order(by: "sort") + + // Listen to the query with default options, which will also populates the cache + let defaultAccumulator = FSTEventAccumulator.init(forTest: self) + var defaultRegistration = query.addSnapshotListener(defaultAccumulator.valueEventHandler) + var querySnap = defaultAccumulator.awaitEvent(withName: "snapshot") + var expected = [["k": "b", "sort": 1]] + try assertQuerySnapshotDataEquals(querySnap, expected) + + // Listen to the same query from cache + let cacheAccumulator = FSTEventAccumulator + .init(forTest: self) + let options = SnapshotListenOptions().withSource(ListenSource.cache) + var cacheRegistration = query.addSnapshotListener( + options: options, + listener: cacheAccumulator.valueEventHandler + ) + querySnap = cacheAccumulator.awaitEvent(withName: "snapshot") + try assertQuerySnapshotDataEquals(querySnap, expected) + + // Un-listen to the default listener, add a doc and re-listen. + defaultRegistration.remove() + addDocumentRef(collRef, data: ["k": "c", "sort": 2]) + + expected = [["k": "b", "sort": 1], ["k": "c", "sort": 2]] + querySnap = cacheAccumulator.awaitEvent(withName: "snapshot") + try assertQuerySnapshotDataEquals(querySnap, expected) + + defaultRegistration = query.addSnapshotListener(defaultAccumulator.valueEventHandler) + querySnap = defaultAccumulator.awaitEvent(withName: "snapshot") + try assertQuerySnapshotDataEquals(querySnap, expected) + + // Un-listen to cache, update a doc, then re-listen to cache. + cacheRegistration.remove() + updateDocumentRef(collRef.document("b"), data: ["k": "b", "sort": 3]) + + expected = [["k": "c", "sort": 2], ["k": "b", "sort": 3]] + querySnap = defaultAccumulator.awaitEvent(withName: "snapshot") + try assertQuerySnapshotDataEquals( + querySnap, expected + ) + + cacheRegistration = query.addSnapshotListener( + options: options, + listener: cacheAccumulator.valueEventHandler + ) + querySnap = cacheAccumulator.awaitEvent(withName: "snapshot") + try assertQuerySnapshotDataEquals( + querySnap, expected + ) + + defaultAccumulator.assertNoAdditionalEvents() + cacheAccumulator.assertNoAdditionalEvents() + defaultRegistration.remove() + cacheRegistration.remove() + } + + func testCanListenToCompositeIndexQueriesFromCache() throws { + let collRef = collectionRef(withDocuments: [ + "a": ["k": "a", "sort": 0], + "b": ["k": "b", "sort": 1], + ]) + readDocumentSet(forRef: collRef) // populate the cache. + + let query = collRef.whereField("k", isLessThanOrEqualTo: "a") + .whereField("sort", isGreaterThanOrEqualTo: 0) + + let options = SnapshotListenOptions().withSource(ListenSource.cache) + let registration = query.addSnapshotListener( + options: options, + listener: eventAccumulator.valueEventHandler + ) + + let querySnap = eventAccumulator.awaitEvent(withName: "snapshot") + try assertQuerySnapshotDataEquals(querySnap, [["k": "a", "sort": 0]]) + + eventAccumulator.assertNoAdditionalEvents() + registration.remove() + } + + func testCanRaiseInitialSnapshotFromCachedEmptyResults() throws { + let collRef = collectionRef() + + // Populate the cache with empty query result. + var querySnap = readDocumentSet(forRef: collRef) + try assertQuerySnapshotDataEquals(querySnap, []) + + // Add a snapshot listener whose first event should be raised from cache. + let options = SnapshotListenOptions().withSource(ListenSource.cache) + let registration = collRef.addSnapshotListener( + options: options, + listener: eventAccumulator.valueEventHandler + ) + + querySnap = eventAccumulator.awaitEvent(withName: "initial event") as! QuerySnapshot + try assertQuerySnapshotDataEquals(querySnap, []) + XCTAssertEqual(querySnap.metadata.isFromCache, true) + + eventAccumulator.assertNoAdditionalEvents() + registration.remove() + } + + func testWillNotBeTriggeredByTransactionsWhileListeningToCache() throws { + let collRef = collectionRef() + + // Add a snapshot listener whose first event should be raised from cache. + let options = SnapshotListenOptions().withSource(ListenSource.cache) + let registration = collRef.addSnapshotListener( + options: options, + listener: eventAccumulator.valueEventHandler + ) + let querySnap = eventAccumulator.awaitEvent(withName: "initial event") + try assertQuerySnapshotDataEquals(querySnap, []) + + let docRef = documentRef() + // Use a transaction to perform a write without triggering any local events. + runTransaction(docRef.firestore, block: { transaction, errorPointer -> Any? in + transaction.updateData(["K": "a"], forDocument: docRef) + return nil + }) + + // There should be no events raised + eventAccumulator.assertNoAdditionalEvents() + registration.remove() + } + + func testSharesServerSideUpdatesWhenListeningToBothCacheAndDefault() throws { + let collRef = collectionRef(withDocuments: [ + "a": ["k": "a", "sort": 0], + "b": ["k": "b", "sort": 1], + ]) + let query = collRef.whereField("sort", isGreaterThan: 0).order(by: "sort") + + // Listen to the query with default options, which will also populates the cache + let defaultAccumulator = FSTEventAccumulator.init(forTest: self) + let defaultRegistration = query.addSnapshotListener(defaultAccumulator.valueEventHandler) + var querySnap = defaultAccumulator.awaitEvent(withName: "snapshot") + var expected = [["k": "b", "sort": 1]] + try assertQuerySnapshotDataEquals(querySnap, expected) + + // Listen to the same query from cache + let cacheAccumulator = FSTEventAccumulator + .init(forTest: self) + let options = SnapshotListenOptions().withSource(ListenSource.cache) + let cacheRegistration = query.addSnapshotListener( + options: options, + listener: cacheAccumulator.valueEventHandler + ) + querySnap = cacheAccumulator.awaitEvent(withName: "snapshot") + try assertQuerySnapshotDataEquals(querySnap, expected) + + // Use a transaction to mock server side updates + let docRef = collRef.document() + runTransaction(docRef.firestore, block: { transaction, errorPointer -> Any? in + transaction.setData(["k": "c", "sort": 2], forDocument: docRef) + return nil + }) + + // Default listener receives the server update + querySnap = defaultAccumulator.awaitEvent(withName: "snapshot") + expected = [["k": "b", "sort": 1], ["k": "c", "sort": 2]] + try assertQuerySnapshotDataEquals(querySnap, expected) + XCTAssertEqual(querySnap.metadata.isFromCache, false) + + // Cache listener raises snapshot as well + querySnap = cacheAccumulator.awaitEvent(withName: "snapshot") + try assertQuerySnapshotDataEquals(querySnap, expected) + XCTAssertEqual(querySnap.metadata.isFromCache, false) + + defaultAccumulator.assertNoAdditionalEvents() + cacheAccumulator.assertNoAdditionalEvents() + defaultRegistration.remove() + cacheRegistration.remove() + } +} diff --git a/Firestore/core/src/api/listen_source.h b/Firestore/core/src/api/listen_source.h new file mode 100644 index 00000000000..2053a92df39 --- /dev/null +++ b/Firestore/core/src/api/listen_source.h @@ -0,0 +1,37 @@ +/* + * Copyright 2024 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef FIRESTORE_CORE_SRC_API_LISTEN_SOURCE_H_ +#define FIRESTORE_CORE_SRC_API_LISTEN_SOURCE_H_ + +namespace firebase { +namespace firestore { +namespace api { + +/** + * An enum that configures the snapshot listener data source. Using this enum, + * specify whether snapshot events are triggered by local cache changes + * only, or from both local cache and watch changes(which is the default). + * + * See `FIRFirestoreListenSource` for more details. + */ +enum class ListenSource { Default, Cache }; + +} // namespace api +} // namespace firestore +} // namespace firebase + +#endif // FIRESTORE_CORE_SRC_API_LISTEN_SOURCE_H_ diff --git a/Firestore/core/src/core/event_manager.cc b/Firestore/core/src/core/event_manager.cc index 283a8341076..d5c3f3542b9 100644 --- a/Firestore/core/src/core/event_manager.cc +++ b/Firestore/core/src/core/event_manager.cc @@ -37,11 +37,27 @@ EventManager::EventManager(QueryEventSource* query_event_source) model::TargetId EventManager::AddQueryListener( std::shared_ptr listener) { const Query& query = listener->query(); + ListenerSetupAction listener_action = + ListenerSetupAction::NoSetupActionRequired; auto inserted = queries_.emplace(query, QueryListenersInfo{}); + // If successfully inserted, it means we haven't listened to this query + // before. bool first_listen = inserted.second; QueryListenersInfo& query_info = inserted.first->second; + if (first_listen) { + listener_action = listener->listens_to_remote_store() + ? ListenerSetupAction:: + InitializeLocalListenAndRequireWatchConnection + : ListenerSetupAction::InitializeLocalListenOnly; + } else if (!query_info.has_remote_listeners() && + listener->listens_to_remote_store()) { + // Query has been listening to local cache, and tries to add a new listener + // sourced from watch. + listener_action = ListenerSetupAction::RequireWatchConnectionOnly; + } + query_info.listeners.push_back(listener); bool raised_event = listener->OnOnlineStateChanged(online_state_); @@ -56,8 +72,20 @@ model::TargetId EventManager::AddQueryListener( } } - if (first_listen) { - query_info.target_id = query_event_source_->Listen(query); + switch (listener_action) { + case ListenerSetupAction::InitializeLocalListenAndRequireWatchConnection: + query_info.target_id = query_event_source_->Listen( + query, /** should_listen_to_remote= */ true); + break; + case ListenerSetupAction::InitializeLocalListenOnly: + query_info.target_id = query_event_source_->Listen( + query, /** should_listen_to_remote= */ false); + break; + case ListenerSetupAction::RequireWatchConnectionOnly: + query_event_source_->ListenToRemoteStore(query); + break; + default: + break; } return query_info.target_id; } @@ -65,18 +93,41 @@ model::TargetId EventManager::AddQueryListener( void EventManager::RemoveQueryListener( std::shared_ptr listener) { const Query& query = listener->query(); - bool last_listen = false; + ListenerRemovalAction listener_action = + ListenerRemovalAction::NoRemovalActionRequired; auto found_iter = queries_.find(query); if (found_iter != queries_.end()) { QueryListenersInfo& query_info = found_iter->second; query_info.Erase(listener); - last_listen = query_info.listeners.empty(); + + if (query_info.listeners.empty()) { + listener_action = + listener->listens_to_remote_store() + ? ListenerRemovalAction:: + TerminateLocalListenAndRequireWatchDisconnection + : ListenerRemovalAction::TerminateLocalListenOnly; + } else if (!query_info.has_remote_listeners() && + listener->listens_to_remote_store()) { + // The removed listener is the last one that sourced from watch. + listener_action = ListenerRemovalAction::RequireWatchDisconnectionOnly; + } } - if (last_listen) { - queries_.erase(found_iter); - query_event_source_->StopListening(query); + switch (listener_action) { + case ListenerRemovalAction:: + TerminateLocalListenAndRequireWatchDisconnection: + queries_.erase(found_iter); + return query_event_source_->StopListening( + query, /** should_stop_remote_listening= */ true); + case ListenerRemovalAction::TerminateLocalListenOnly: + queries_.erase(found_iter); + return query_event_source_->StopListening( + query, /** should_stop_remote_listening= */ false); + case ListenerRemovalAction::RequireWatchDisconnectionOnly: + return query_event_source_->StopListeningToRemoteStoreOnly(query); + default: + return; } } diff --git a/Firestore/core/src/core/event_manager.h b/Firestore/core/src/core/event_manager.h index ba4f2ee56e9..9ee783a85bd 100644 --- a/Firestore/core/src/core/event_manager.h +++ b/Firestore/core/src/core/event_manager.h @@ -23,6 +23,7 @@ #include #include "Firestore/core/src/core/query.h" +#include "Firestore/core/src/core/query_listener.h" #include "Firestore/core/src/core/sync_engine_callback.h" #include "Firestore/core/src/core/view_snapshot.h" #include "Firestore/core/src/model/model_fwd.h" @@ -35,7 +36,6 @@ namespace firestore { namespace core { class QueryEventSource; -class QueryListener; /** * EventManager is responsible for mapping queries to query event listeners. @@ -97,12 +97,35 @@ class EventManager : public SyncEngineCallback { snapshot_ = snapshot; } + bool has_remote_listeners() { + for (const auto& listener : listeners) { + if (listener->listens_to_remote_store()) { + return true; + } + } + return false; + } + private: // Other members are public in this struct, ensure that any reads are // copies by requiring reads to go through a const getter. absl::optional snapshot_; }; + enum ListenerSetupAction { + InitializeLocalListenAndRequireWatchConnection = 0, + InitializeLocalListenOnly = 1, + RequireWatchConnectionOnly = 2, + NoSetupActionRequired = 3 + }; + + enum ListenerRemovalAction { + TerminateLocalListenAndRequireWatchDisconnection = 0, + TerminateLocalListenOnly = 1, + RequireWatchDisconnectionOnly = 2, + NoRemovalActionRequired = 3 + }; + QueryEventSource* query_event_source_ = nullptr; model::OnlineState online_state_ = model::OnlineState::Unknown; std::unordered_map queries_; diff --git a/Firestore/core/src/core/listen_options.h b/Firestore/core/src/core/listen_options.h index cf76d872878..2499b75e224 100644 --- a/Firestore/core/src/core/listen_options.h +++ b/Firestore/core/src/core/listen_options.h @@ -17,10 +17,14 @@ #ifndef FIRESTORE_CORE_SRC_CORE_LISTEN_OPTIONS_H_ #define FIRESTORE_CORE_SRC_CORE_LISTEN_OPTIONS_H_ +#include +#include "Firestore/core/src/api/listen_source.h" namespace firebase { namespace firestore { namespace core { +using api::ListenSource; + class ListenOptions { public: ListenOptions() = default; @@ -44,14 +48,36 @@ class ListenOptions { } /** - * Creates a default ListenOptions, with metadata changes and - * wait_for_sync_when_online disabled. + * Creates a new ListenOptions. + * + * @param include_query_metadata_changes Raise events when only metadata of + * the query changes. + * @param include_document_metadata_changes Raise events when only metadata of + * documents changes. + * @param wait_for_sync_when_online Wait for a sync with the server when + * online, but still raise events while offline. + * @param source sets the source a snapshot listener listens to. + */ + ListenOptions(bool include_query_metadata_changes, + bool include_document_metadata_changes, + bool wait_for_sync_when_online, + ListenSource source) + : include_query_metadata_changes_(include_query_metadata_changes), + include_document_metadata_changes_(include_document_metadata_changes), + wait_for_sync_when_online_(wait_for_sync_when_online), + source_(std::move(source)) { + } + + /** + * Creates a default ListenOptions, with metadata changes, + * wait_for_sync_when_online disabled, and listen source set to default. */ static ListenOptions DefaultOptions() { return ListenOptions( /*include_query_metadata_changes=*/false, /*include_document_metadata_changes=*/false, - /*wait_for_sync_when_online=*/false); + /*wait_for_sync_when_online=*/false, + /*source=*/ListenSource::Default); } /** @@ -63,7 +89,19 @@ class ListenOptions { return ListenOptions( /*include_query_metadata_changes=*/include_metadata_changes, /*include_document_metadata_changes=*/include_metadata_changes, - /*wait_for_sync_when_online=*/false); + /*wait_for_sync_when_online=*/false, + /*source=*/ListenSource::Default); + } + + /** + * Creates a ListenOptions which sets the source snapshot listener listens to. + */ + static ListenOptions FromOptions(bool include_metadata_changes, + ListenSource source) { + return ListenOptions( + /*include_query_metadata_changes=*/include_metadata_changes, + /*include_document_metadata_changes=*/include_metadata_changes, + /*wait_for_sync_when_online=*/false, std::move(source)); } bool include_query_metadata_changes() const { @@ -78,10 +116,15 @@ class ListenOptions { return wait_for_sync_when_online_; } + ListenSource source() const { + return source_; + } + private: bool include_query_metadata_changes_ = false; bool include_document_metadata_changes_ = false; bool wait_for_sync_when_online_ = false; + ListenSource source_ = ListenSource::Default; }; } // namespace core diff --git a/Firestore/core/src/core/query_listener.cc b/Firestore/core/src/core/query_listener.cc index 3e0cbb2cceb..c0ec38e752a 100644 --- a/Firestore/core/src/core/query_listener.cc +++ b/Firestore/core/src/core/query_listener.cc @@ -136,6 +136,11 @@ bool QueryListener::ShouldRaiseInitialEvent(const ViewSnapshot& snapshot, return true; } + // Always raise first event if listening to cache + if (!listens_to_remote_store()) { + return true; + } + // NOTE: We consider OnlineState::Unknown as online (it should become Offline // or Online if we wait long enough). bool maybe_online = online_state != OnlineState::Offline; diff --git a/Firestore/core/src/core/query_listener.h b/Firestore/core/src/core/query_listener.h index eec5ada73b8..6b934a0de59 100644 --- a/Firestore/core/src/core/query_listener.h +++ b/Firestore/core/src/core/query_listener.h @@ -63,6 +63,10 @@ class QueryListener { return query_; } + bool listens_to_remote_store() const { + return options_.source() != ListenSource::Cache; + } + /** The last received view snapshot. */ const absl::optional& snapshot() const { return snapshot_; diff --git a/Firestore/core/src/core/sync_engine.cc b/Firestore/core/src/core/sync_engine.cc index 24a4c3f7803..77223cb1fed 100644 --- a/Firestore/core/src/core/sync_engine.cc +++ b/Firestore/core/src/core/sync_engine.cc @@ -104,7 +104,7 @@ void SyncEngine::AssertCallbackExists(absl::string_view source) { "Tried to call '%s' before callback was registered.", source); } -TargetId SyncEngine::Listen(Query query) { +TargetId SyncEngine::Listen(Query query, bool should_listen_to_remote) { AssertCallbackExists("Listen"); HARD_ASSERT(query_views_by_query_.find(query) == query_views_by_query_.end(), @@ -121,7 +121,9 @@ TargetId SyncEngine::Listen(Query query) { snapshots.push_back(std::move(view_snapshot)); sync_engine_callback_->OnViewSnapshots(std::move(snapshots)); - remote_store_->Listen(std::move(target_data)); + if (should_listen_to_remote) { + remote_store_->Listen(std::move(target_data)); + } return target_id; } @@ -161,22 +163,48 @@ ViewSnapshot SyncEngine::InitializeViewAndComputeSnapshot( return view_change.snapshot().value(); } -void SyncEngine::StopListening(const Query& query) { +void SyncEngine::ListenToRemoteStore(Query query) { + AssertCallbackExists("ListenToRemoteStore"); + TargetData target_data = local_store_->AllocateTarget(query.ToTarget()); + remote_store_->Listen(std::move(target_data)); +} + +void SyncEngine::StopListening(const Query& query, + bool should_stop_remote_listening) { AssertCallbackExists("StopListening"); + StopListeningAndReleaseTarget(query, /** last_listen= */ true, + should_stop_remote_listening); +} + +void SyncEngine::StopListeningToRemoteStoreOnly(const Query& query) { + AssertCallbackExists("StopListeningToRemoteStoreOnly"); + StopListeningAndReleaseTarget(query, /** last_listen= */ false, + /** should_stop_remote_listening= */ true); +} +void SyncEngine::StopListeningAndReleaseTarget( + const Query& query, bool last_listen, bool should_stop_remote_listening) { auto query_view = query_views_by_query_[query]; HARD_ASSERT(query_view, "Trying to stop listening to a query not found"); - query_views_by_query_.erase(query); + if (last_listen) { + query_views_by_query_.erase(query); + } + // One target could have multiple queries mapped to it. TargetId target_id = query_view->target_id(); auto& queries = queries_by_target_[target_id]; queries.erase(std::remove(queries.begin(), queries.end(), query), queries.end()); - if (queries.empty()) { - local_store_->ReleaseTarget(target_id); + if (!queries.empty()) return; + + if (should_stop_remote_listening) { remote_store_->StopListening(target_id); + } + + if (last_listen) { + local_store_->ReleaseTarget(target_id); RemoveAndCleanupTarget(target_id, Status::OK()); } } diff --git a/Firestore/core/src/core/sync_engine.h b/Firestore/core/src/core/sync_engine.h index e8bfc028d02..bcf930fdd0c 100644 --- a/Firestore/core/src/core/sync_engine.h +++ b/Firestore/core/src/core/sync_engine.h @@ -70,16 +70,33 @@ class QueryEventSource { /** * Initiates a new listen. The LocalStore will be queried for initial data - * and the listen will be sent to the `RemoteStore` to get remote data. The - * registered SyncEngineCallback will be notified of resulting view + * and the listen will be sent to the RemoteStore if the query is listening to + * watch. The registered SyncEngineCallback will be notified of resulting view * snapshots and/or listen errors. * * @return the target ID assigned to the query. */ - virtual model::TargetId Listen(Query query) = 0; + virtual model::TargetId Listen(Query query, bool should_listen_to_remote) = 0; - /** Stops listening to a query previously listened to via `Listen`. */ - virtual void StopListening(const Query& query) = 0; + /** + * Sends the listen to the RemoteStore to get remote data. Invoked when a + * Query starts listening to the remote store, while already listening to the + * cache. + */ + virtual void ListenToRemoteStore(Query query) = 0; + + /** + * Stops listening to a query previously listened to via `Listen`. Un-listen + * to remote store if there is a watch connection established and stayed open. + */ + virtual void StopListening(const Query& query, + bool should_stop_remote_listening) = 0; + + /** + * Stops listening to a query from watch. Invoked when a Query stops listening + * to the remote store, while still listening to the cache. + */ + virtual void StopListeningToRemoteStoreOnly(const Query& query) = 0; }; /** @@ -107,8 +124,12 @@ class SyncEngine : public remote::RemoteStoreCallback, public QueryEventSource { void SetCallback(SyncEngineCallback* callback) override { sync_engine_callback_ = callback; } - model::TargetId Listen(Query query) override; - void StopListening(const Query& query) override; + model::TargetId Listen(Query query, + bool should_listen_to_remote = true) override; + void ListenToRemoteStore(Query query) override; + void StopListening(const Query& query, + bool should_stop_remote_listening = true) override; + void StopListeningToRemoteStoreOnly(const Query& query) override; /** * Initiates the write of local mutation batch which involves adding the @@ -244,6 +265,9 @@ class SyncEngine : public remote::RemoteStoreCallback, public QueryEventSource { nanopb::ByteString resume_token); void RemoveAndCleanupTarget(model::TargetId target_id, util::Status status); + void StopListeningAndReleaseTarget(const Query& query, + bool should_stop_remote_listening, + bool last_listen); void RemoveLimboTarget(const model::DocumentKey& key); diff --git a/Firestore/core/test/unit/core/event_manager_test.cc b/Firestore/core/test/unit/core/event_manager_test.cc index 9229dae919e..5d48a9aae0c 100644 --- a/Firestore/core/test/unit/core/event_manager_test.cc +++ b/Firestore/core/test/unit/core/event_manager_test.cc @@ -57,11 +57,21 @@ std::shared_ptr NoopQueryListener(core::Query query) { NoopViewSnapshotHandler()); } +std::shared_ptr NoopQueryCacheListener(core::Query query) { + return QueryListener::Create( + std::move(query), + ListenOptions::FromOptions(/** include_metadata_changes= */ false, + ListenSource::Cache), + NoopViewSnapshotHandler()); +} + class MockEventSource : public core::QueryEventSource { public: MOCK_METHOD1(SetCallback, void(core::SyncEngineCallback*)); - MOCK_METHOD1(Listen, model::TargetId(core::Query)); - MOCK_METHOD1(StopListening, void(const core::Query&)); + MOCK_METHOD2(Listen, model::TargetId(core::Query, bool)); + MOCK_METHOD1(ListenToRemoteStore, void(core::Query)); + MOCK_METHOD2(StopListening, void(const core::Query&, bool)); + MOCK_METHOD1(StopListeningToRemoteStoreOnly, void(const core::Query&)); }; TEST(EventManagerTest, HandlesManyListnersPerQuery) { @@ -73,14 +83,34 @@ TEST(EventManagerTest, HandlesManyListnersPerQuery) { EXPECT_CALL(mock_event_source, SetCallback(_)); EventManager event_manager(&mock_event_source); - EXPECT_CALL(mock_event_source, Listen(query)); + EXPECT_CALL(mock_event_source, Listen(query, true)); + event_manager.AddQueryListener(listener1); + + // Expecting no activity from mock_event_source. + event_manager.AddQueryListener(listener2); + event_manager.RemoveQueryListener(listener2); + + EXPECT_CALL(mock_event_source, StopListening(query, true)); + event_manager.RemoveQueryListener(listener1); +} + +TEST(EventManagerTest, HandlesManyCacheListnersPerQuery) { + core::Query query = Query("foo/bar"); + auto listener1 = NoopQueryCacheListener(query); + auto listener2 = NoopQueryCacheListener(query); + + StrictMock mock_event_source; + EXPECT_CALL(mock_event_source, SetCallback(_)); + EventManager event_manager(&mock_event_source); + + EXPECT_CALL(mock_event_source, Listen(query, false)); event_manager.AddQueryListener(listener1); // Expecting no activity from mock_event_source. event_manager.AddQueryListener(listener2); event_manager.RemoveQueryListener(listener2); - EXPECT_CALL(mock_event_source, StopListening(query)); + EXPECT_CALL(mock_event_source, StopListening(query, false)); event_manager.RemoveQueryListener(listener1); } @@ -91,7 +121,7 @@ TEST(EventManagerTest, HandlesUnlistenOnUnknownListenerGracefully) { MockEventSource mock_event_source; EventManager event_manager(&mock_event_source); - EXPECT_CALL(mock_event_source, StopListening(_)).Times(0); + EXPECT_CALL(mock_event_source, StopListening(_, true)).Times(0); event_manager.RemoveQueryListener(listener); } @@ -128,10 +158,10 @@ TEST(EventManagerTest, NotifiesListenersInTheRightOrder) { MockEventSource mock_event_source; EventManager event_manager(&mock_event_source); - EXPECT_CALL(mock_event_source, Listen(query1)); + EXPECT_CALL(mock_event_source, Listen(query1, true)); event_manager.AddQueryListener(listener1); - EXPECT_CALL(mock_event_source, Listen(query2)); + EXPECT_CALL(mock_event_source, Listen(query2, true)); event_manager.AddQueryListener(listener2); event_manager.AddQueryListener(listener3);