Explorar el Código

Snapshot listener source from cache (#12370)

Mila hace 2 años
padre
commit
07b7669175
Se han modificado 28 ficheros con 7138 adiciones y 37 borrados
  1. 3 0
      Firestore/CHANGELOG.md
  2. 22 0
      Firestore/Example/Firestore.xcodeproj/project.pbxproj
  3. 18 1
      Firestore/Example/Tests/SpecTests/FSTSpecTests.mm
  4. 2 1
      Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.h
  5. 1 3
      Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.mm
  6. 5895 0
      Firestore/Example/Tests/SpecTests/json/listen_source_spec_test.json
  7. 5 0
      Firestore/Example/Tests/Util/FSTIntegrationTestCase.h
  8. 16 0
      Firestore/Example/Tests/Util/FSTIntegrationTestCase.mm
  9. 9 0
      Firestore/Source/API/FIRDocumentReference.mm
  10. 9 0
      Firestore/Source/API/FIRQuery.mm
  11. 63 0
      Firestore/Source/API/FIRSnapshotListenOptions.mm
  12. 4 0
      Firestore/Source/API/converters.h
  13. 12 0
      Firestore/Source/API/converters.mm
  14. 17 0
      Firestore/Source/Public/FirebaseFirestore/FIRDocumentReference.h
  15. 16 0
      Firestore/Source/Public/FirebaseFirestore/FIRQuery.h
  16. 83 0
      Firestore/Source/Public/FirebaseFirestore/FIRSnapshotListenOptions.h
  17. 1 0
      Firestore/Source/Public/FirebaseFirestore/FirebaseFirestore.h
  18. 1 0
      Firestore/Swift/Tests/BridgingHeader.h
  19. 684 0
      Firestore/Swift/Tests/Integration/SnapshotListenerSourceTests.swift
  20. 37 0
      Firestore/core/src/api/listen_source.h
  21. 58 7
      Firestore/core/src/core/event_manager.cc
  22. 24 1
      Firestore/core/src/core/event_manager.h
  23. 47 4
      Firestore/core/src/core/listen_options.h
  24. 5 0
      Firestore/core/src/core/query_listener.cc
  25. 4 0
      Firestore/core/src/core/query_listener.h
  26. 34 6
      Firestore/core/src/core/sync_engine.cc
  27. 31 7
      Firestore/core/src/core/sync_engine.h
  28. 37 7
      Firestore/core/test/unit/core/event_manager_test.cc

+ 3 - 0
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)
 

+ 22 - 0
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 = "<group>"; };
 		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 = "<group>"; };
 		4C73C0CC6F62A90D8573F383 /* string_apple_benchmark.mm */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.objcpp; path = string_apple_benchmark.mm; sourceTree = "<group>"; };
+		4D65F6E69993611D47DC8E7C /* SnapshotListenerSourceTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SnapshotListenerSourceTests.swift; sourceTree = "<group>"; };
+		4D9E51DA7A275D8B1CAEAEB2 /* listen_source_spec_test.json */ = {isa = PBXFileReference; includeInIndex = 1; path = listen_source_spec_test.json; sourceTree = "<group>"; };
 		4F5B96F3ABCD2CA901DB1CD4 /* bundle_builder.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = bundle_builder.cc; sourceTree = "<group>"; };
 		526D755F65AC676234F57125 /* target_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = target_test.cc; sourceTree = "<group>"; };
 		52756B7624904C36FBB56000 /* fake_target_metadata_provider.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = fake_target_metadata_provider.h; sourceTree = "<group>"; };
@@ -2221,6 +2232,7 @@
 				3355BE9391CC4857AF0BDAE3 /* DatabaseTests.swift */,
 				62E54B832A9E910A003347C8 /* IndexingTests.swift */,
 				621D620928F9CE7400D2FA26 /* QueryIntegrationTests.swift */,
+				4D65F6E69993611D47DC8E7C /* SnapshotListenerSourceTests.swift */,
 			);
 			path = Integration;
 			sourceTree = "<group>";
@@ -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 */,

+ 18 - 1
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 @@ namespace objc = firebase::firestore::objc;
 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 @@ NSString *ToTargetIdListString(const ActiveTargetMap &map) {
   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");

+ 2 - 1
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

+ 1 - 3
Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.mm

@@ -476,10 +476,8 @@ NS_ASSUME_NONNULL_BEGIN
   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<ViewSnapshot> &maybe_snapshot) {
         FSTQueryEvent *event = [[FSTQueryEvent alloc] init];

+ 5895 - 0
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
+      }
+    ]
+  }
+}

+ 5 - 0
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<NSString *, id> *)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<NSString *, id> *)data;
 
 - (void)mergeDocumentRef:(FIRDocumentReference *)ref

+ 16 - 0
Firestore/Example/Tests/Util/FSTIntegrationTestCase.mm

@@ -569,6 +569,22 @@ class FakeAuthCredentialsProvider : public EmptyAuthCredentialsProvider {
   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<NSString *, id> *)data {
   XCTestExpectation *expectation = [self expectationWithDescription:@"setDataWithMerge"];
   [ref setData:data merge:YES completion:[self completionForExpectation:expectation]];

+ 9 - 0
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::DocumentSnapshot;
 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 @@ NS_ASSUME_NONNULL_BEGIN
   return [self addSnapshotListenerInternalWithOptions:options listener:listener];
 }
 
+- (id<FIRListenerRegistration>)addSnapshotListenerWithOptions:(FIRSnapshotListenOptions *)options
+                                                     listener:(FIRDocumentSnapshotBlock)listener {
+  ListenOptions listenOptions =
+      ListenOptions::FromOptions(options.includeMetadataChanges, MakeListenSource(options.source));
+  return [self addSnapshotListenerInternalWithOptions:listenOptions listener:listener];
+}
+
 - (id<FIRListenerRegistration>)addSnapshotListenerInternalWithOptions:(ListenOptions)internalOptions
                                                              listener:(FIRDocumentSnapshotBlock)
                                                                           listener {

+ 9 - 0
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_ArrayValue;
 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 @@ int32_t SaturatedLimitValue(NSInteger limit) {
   return [self addSnapshotListenerInternalWithOptions:options listener:listener];
 }
 
+- (id<FIRListenerRegistration>)addSnapshotListenerWithOptions:(FIRSnapshotListenOptions *)options
+                                                     listener:(FIRQuerySnapshotBlock)listener {
+  ListenOptions listenOptions =
+      ListenOptions::FromOptions(options.includeMetadataChanges, MakeListenSource(options.source));
+  return [self addSnapshotListenerInternalWithOptions:listenOptions listener:listener];
+}
+
 - (id<FIRListenerRegistration>)addSnapshotListenerInternalWithOptions:(ListenOptions)internalOptions
                                                              listener:
                                                                  (FIRQuerySnapshotBlock)listener {

+ 63 - 0
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 <Foundation/Foundation.h>
+
+#include <cstdint>
+#include <string>
+
+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

+ 4 - 0
Firestore/Source/API/converters.h

@@ -24,6 +24,8 @@
 #import <Foundation/Foundation.h>
 
 #include <memory>
+#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> firestore);
 
+ListenSource MakeListenSource(const FIRListenSource& source);
+
 }  // namespace api
 }  // namespace firestore
 }  // namespace firebase

+ 12 - 0
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 @@ FIRDocumentReference* MakeFIRDocumentReference(const model::DocumentKey& key,
   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

+ 17 - 0
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<FIRListenerRegistration>)
+    addSnapshotListenerWithOptions:(FIRSnapshotListenOptions *)options
+                          listener:(void (^)(FIRDocumentSnapshot *_Nullable snapshot,
+                                             NSError *_Nullable error))listener
+    NS_SWIFT_NAME(addSnapshotListener(options:listener:));
+
 @end
 
 NS_ASSUME_NONNULL_END

+ 16 - 0
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<FIRListenerRegistration>)
+    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.

+ 83 - 0
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 <Foundation/Foundation.h>
+
+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

+ 1 - 0
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"

+ 1 - 0
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"
 

+ 684 - 0
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<QuerySnapshot>.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<QuerySnapshot>
+      .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<QuerySnapshot>.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<QuerySnapshot>
+      .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<QuerySnapshot>
+      .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<QuerySnapshot>.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<QuerySnapshot>.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<QuerySnapshot>.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<QuerySnapshot>.init(forTest: self)
+    let defaultRegistration = query.addSnapshotListener(defaultAccumulator.valueEventHandler)
+    defaultAccumulator.awaitEvent(withName: "snapshot")
+    let cacheAccumulator = FSTEventAccumulator<QuerySnapshot>
+      .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<QuerySnapshot>.init(forTest: self)
+    let defaultRegistration = query.addSnapshotListener(defaultAccumulator.valueEventHandler)
+    defaultAccumulator.awaitEvent(withName: "snapshot")
+    let cacheAccumulator = FSTEventAccumulator<QuerySnapshot>
+      .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<QuerySnapshot>.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<QuerySnapshot>
+      .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<QuerySnapshot>.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<QuerySnapshot>
+      .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()
+  }
+}

+ 37 - 0
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_

+ 58 - 7
Firestore/core/src/core/event_manager.cc

@@ -37,11 +37,27 @@ EventManager::EventManager(QueryEventSource* query_event_source)
 model::TargetId EventManager::AddQueryListener(
     std::shared_ptr<core::QueryListener> 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<core::QueryListener> 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;
   }
 }
 

+ 24 - 1
Firestore/core/src/core/event_manager.h

@@ -23,6 +23,7 @@
 #include <vector>
 
 #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<ViewSnapshot> 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<core::Query, QueryListenersInfo> queries_;

+ 47 - 4
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 <utility>
+#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

+ 5 - 0
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;

+ 4 - 0
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<ViewSnapshot>& snapshot() const {
     return snapshot_;

+ 34 - 6
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());
   }
 }

+ 31 - 7
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);
 

+ 37 - 7
Firestore/core/test/unit/core/event_manager_test.cc

@@ -57,11 +57,21 @@ std::shared_ptr<QueryListener> NoopQueryListener(core::Query query) {
                                NoopViewSnapshotHandler());
 }
 
+std::shared_ptr<QueryListener> 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<MockEventSource> 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);