cherylEnkidu před 10 měsíci
rodič
revize
8c65663ccf

+ 13 - 5
Firestore/Example/Firestore.xcodeproj/project.pbxproj

@@ -748,6 +748,9 @@
 		61D35E0DE04E70D3BC243A65 /* FIRGeoPointTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E048202154AA00B64F25 /* FIRGeoPointTests.mm */; };
 		61ECC7CE18700CBD73D0D810 /* leveldb_migrations_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = EF83ACD5E1E9F25845A9ACED /* leveldb_migrations_test.cc */; };
 		61F72C5620BC48FD001A68CB /* serializer_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 61F72C5520BC48FD001A68CB /* serializer_test.cc */; };
+		62180C272DF20F6500B370CD /* SnapshotStreamListenerSourceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62180C262DF20F4B00B370CD /* SnapshotStreamListenerSourceTests.swift */; };
+		62180C282DF20F6500B370CD /* SnapshotStreamListenerSourceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62180C262DF20F4B00B370CD /* SnapshotStreamListenerSourceTests.swift */; };
+		62180C292DF20F6500B370CD /* SnapshotStreamListenerSourceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62180C262DF20F4B00B370CD /* SnapshotStreamListenerSourceTests.swift */; };
 		621D620A28F9CE7400D2FA26 /* QueryIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621D620928F9CE7400D2FA26 /* QueryIntegrationTests.swift */; };
 		621D620B28F9CE7400D2FA26 /* QueryIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621D620928F9CE7400D2FA26 /* QueryIntegrationTests.swift */; };
 		621D620C28F9CE7400D2FA26 /* QueryIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621D620928F9CE7400D2FA26 /* QueryIntegrationTests.swift */; };
@@ -1777,7 +1780,7 @@
 		4334F87873015E3763954578 /* status_testing.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = status_testing.h; sourceTree = "<group>"; };
 		4375BDCDBCA9938C7F086730 /* Validation_BloomFilterTest_MD5_5000_1_bloom_filter_proto.json */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.json; name = Validation_BloomFilterTest_MD5_5000_1_bloom_filter_proto.json; path = bloom_filter_golden_test_data/Validation_BloomFilterTest_MD5_5000_1_bloom_filter_proto.json; sourceTree = "<group>"; };
 		444B7AB3F5A2929070CB1363 /* hard_assert_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = hard_assert_test.cc; sourceTree = "<group>"; };
-		4564AD9C55EC39C080EB9476 /* globals_cache_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; path = globals_cache_test.cc; sourceTree = "<group>"; };
+		4564AD9C55EC39C080EB9476 /* globals_cache_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = globals_cache_test.cc; sourceTree = "<group>"; };
 		478DC75A0DCA6249A616DD30 /* Validation_BloomFilterTest_MD5_500_0001_membership_test_result.json */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.json; name = Validation_BloomFilterTest_MD5_500_0001_membership_test_result.json; path = bloom_filter_golden_test_data/Validation_BloomFilterTest_MD5_500_0001_membership_test_result.json; sourceTree = "<group>"; };
 		48D0915834C3D234E5A875A9 /* grpc_stream_tester.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = grpc_stream_tester.h; sourceTree = "<group>"; };
 		4B3E4A77493524333133C5DC /* Validation_BloomFilterTest_MD5_50000_1_bloom_filter_proto.json */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.json; name = Validation_BloomFilterTest_MD5_50000_1_bloom_filter_proto.json; path = bloom_filter_golden_test_data/Validation_BloomFilterTest_MD5_50000_1_bloom_filter_proto.json; sourceTree = "<group>"; };
@@ -1895,7 +1898,7 @@
 		5B5414D28802BC76FDADABD6 /* stream_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = stream_test.cc; sourceTree = "<group>"; };
 		5B96CC29E9946508F022859C /* Validation_BloomFilterTest_MD5_50000_0001_membership_test_result.json */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.json; name = Validation_BloomFilterTest_MD5_50000_0001_membership_test_result.json; path = bloom_filter_golden_test_data/Validation_BloomFilterTest_MD5_50000_0001_membership_test_result.json; sourceTree = "<group>"; };
 		5C68EE4CB94C0DD6E333F546 /* Validation_BloomFilterTest_MD5_1_01_membership_test_result.json */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.json; name = Validation_BloomFilterTest_MD5_1_01_membership_test_result.json; path = bloom_filter_golden_test_data/Validation_BloomFilterTest_MD5_1_01_membership_test_result.json; sourceTree = "<group>"; };
-		5C6DEA63FBDE19D841291723 /* memory_globals_cache_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; path = memory_globals_cache_test.cc; sourceTree = "<group>"; };
+		5C6DEA63FBDE19D841291723 /* memory_globals_cache_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = memory_globals_cache_test.cc; sourceTree = "<group>"; };
 		5C7942B6244F4C416B11B86C /* leveldb_mutation_queue_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = leveldb_mutation_queue_test.cc; sourceTree = "<group>"; };
 		5CAE131920FFFED600BE9A4A /* Firestore_Benchmarks_iOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Firestore_Benchmarks_iOS.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
 		5CAE131D20FFFED600BE9A4A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
@@ -1935,6 +1938,7 @@
 		618BBE9A20B89AAC00B5BCE7 /* status.pb.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = status.pb.h; sourceTree = "<group>"; };
 		61F72C5520BC48FD001A68CB /* serializer_test.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = serializer_test.cc; sourceTree = "<group>"; };
 		620C1427763BA5D3CCFB5A1F /* BridgingHeader.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = BridgingHeader.h; sourceTree = "<group>"; };
+		62180C262DF20F4B00B370CD /* SnapshotStreamListenerSourceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SnapshotStreamListenerSourceTests.swift; sourceTree = "<group>"; };
 		621D620928F9CE7400D2FA26 /* QueryIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QueryIntegrationTests.swift; sourceTree = "<group>"; };
 		62E103B28B48A81D682A0DE9 /* Pods_Firestore_Example_tvOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Firestore_Example_tvOS.framework; sourceTree = BUILT_PRODUCTS_DIR; };
 		62E54B832A9E910A003347C8 /* IndexingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IndexingTests.swift; sourceTree = "<group>"; };
@@ -1945,7 +1949,7 @@
 		69E6C311558EC77729A16CF1 /* Pods-Firestore_Example_iOS-Firestore_SwiftTests_iOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_Example_iOS-Firestore_SwiftTests_iOS.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Firestore_Example_iOS-Firestore_SwiftTests_iOS/Pods-Firestore_Example_iOS-Firestore_SwiftTests_iOS.debug.xcconfig"; sourceTree = "<group>"; };
 		6A7A30A2DB3367E08939E789 /* bloom_filter.pb.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = bloom_filter.pb.h; sourceTree = "<group>"; };
 		6AE927CDFC7A72BF825BE4CB /* Pods-Firestore_Tests_tvOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_Tests_tvOS.release.xcconfig"; path = "Pods/Target Support Files/Pods-Firestore_Tests_tvOS/Pods-Firestore_Tests_tvOS.release.xcconfig"; sourceTree = "<group>"; };
-		6E42FA109D363EA7F3387AAE /* thread_safe_memoizer_testing.cc */ = {isa = PBXFileReference; includeInIndex = 1; path = thread_safe_memoizer_testing.cc; sourceTree = "<group>"; };
+		6E42FA109D363EA7F3387AAE /* thread_safe_memoizer_testing.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = thread_safe_memoizer_testing.cc; sourceTree = "<group>"; };
 		6E8302DE210222ED003E1EA3 /* FSTFuzzTestFieldPath.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FSTFuzzTestFieldPath.h; sourceTree = "<group>"; };
 		6E8302DF21022309003E1EA3 /* FSTFuzzTestFieldPath.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FSTFuzzTestFieldPath.mm; sourceTree = "<group>"; };
 		6EA39FDD20FE820E008D461F /* FSTFuzzTestSerializer.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = FSTFuzzTestSerializer.mm; sourceTree = "<group>"; };
@@ -2123,7 +2127,7 @@
 		E42355285B9EF55ABD785792 /* Pods_Firestore_Example_macOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Firestore_Example_macOS.framework; sourceTree = BUILT_PRODUCTS_DIR; };
 		E592181BFD7C53C305123739 /* Pods-Firestore_Tests_iOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_Tests_iOS.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Firestore_Tests_iOS/Pods-Firestore_Tests_iOS.debug.xcconfig"; sourceTree = "<group>"; };
 		E76F0CDF28E5FA62D21DE648 /* leveldb_target_cache_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = leveldb_target_cache_test.cc; sourceTree = "<group>"; };
-		EA10515F99A42D71DA2D2841 /* thread_safe_memoizer_testing_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; path = thread_safe_memoizer_testing_test.cc; sourceTree = "<group>"; };
+		EA10515F99A42D71DA2D2841 /* thread_safe_memoizer_testing_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = thread_safe_memoizer_testing_test.cc; sourceTree = "<group>"; };
 		ECEBABC7E7B693BE808A1052 /* Pods_Firestore_IntegrationTests_iOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Firestore_IntegrationTests_iOS.framework; sourceTree = BUILT_PRODUCTS_DIR; };
 		EF3A65472C66B9560041EE69 /* FIRVectorValueTests.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = FIRVectorValueTests.mm; sourceTree = "<group>"; };
 		EF6C285029E462A200A7D4F1 /* FIRAggregateTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FIRAggregateTests.mm; sourceTree = "<group>"; };
@@ -2141,7 +2145,7 @@
 		F848C41C03A25C42AD5A4BC2 /* target_cache_test.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = target_cache_test.h; sourceTree = "<group>"; };
 		F869D85E900E5AF6CD02E2FC /* firebase_auth_credentials_provider_test.mm */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.objcpp; name = firebase_auth_credentials_provider_test.mm; path = credentials/firebase_auth_credentials_provider_test.mm; sourceTree = "<group>"; };
 		FA2E9952BA2B299C1156C43C /* Pods-Firestore_Benchmarks_iOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_Benchmarks_iOS.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Firestore_Benchmarks_iOS/Pods-Firestore_Benchmarks_iOS.debug.xcconfig"; sourceTree = "<group>"; };
-		FC44D934D4A52C790659C8D6 /* leveldb_globals_cache_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; path = leveldb_globals_cache_test.cc; sourceTree = "<group>"; };
+		FC44D934D4A52C790659C8D6 /* leveldb_globals_cache_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = leveldb_globals_cache_test.cc; sourceTree = "<group>"; };
 		FC738525340E594EBFAB121E /* Pods-Firestore_Example_tvOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_Example_tvOS.release.xcconfig"; path = "Pods/Target Support Files/Pods-Firestore_Example_tvOS/Pods-Firestore_Example_tvOS.release.xcconfig"; sourceTree = "<group>"; };
 		FF73B39D04D1760190E6B84A /* FIRQueryUnitTests.mm */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.objcpp; path = FIRQueryUnitTests.mm; sourceTree = "<group>"; };
 		FFCA39825D9678A03D1845D0 /* document_overlay_cache_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = document_overlay_cache_test.cc; sourceTree = "<group>"; };
@@ -2267,6 +2271,7 @@
 		124C932A22C1635300CA8C2D /* Integration */ = {
 			isa = PBXGroup;
 			children = (
+				62180C262DF20F4B00B370CD /* SnapshotStreamListenerSourceTests.swift */,
 				EF6C286C29E6D22200A7D4F1 /* AggregationIntegrationTests.swift */,
 				062072B62773A055001655D7 /* AsyncAwaitIntegrationTests.swift */,
 				124C932B22C1642C00CA8C2D /* CodableIntegrationTests.swift */,
@@ -4655,6 +4660,7 @@
 				4D42E5C756229C08560DD731 /* XCTestCase+Await.mm in Sources */,
 				09BE8C01EC33D1FD82262D5D /* aggregate_query_test.cc in Sources */,
 				0EC3921AE220410F7394729B /* aggregation_result.pb.cc in Sources */,
+				62180C272DF20F6500B370CD /* SnapshotStreamListenerSourceTests.swift in Sources */,
 				276A563D546698B6AAC20164 /* annotations.pb.cc in Sources */,
 				7B8D7BAC1A075DB773230505 /* app_testing.mm in Sources */,
 				DC1C711290E12F8EF3601151 /* array_sorted_map_test.cc in Sources */,
@@ -4902,6 +4908,7 @@
 				736C4E82689F1CA1859C4A3F /* XCTestCase+Await.mm in Sources */,
 				412BE974741729A6683C386F /* aggregate_query_test.cc in Sources */,
 				DF983A9C1FBF758AF3AF110D /* aggregation_result.pb.cc in Sources */,
+				62180C292DF20F6500B370CD /* SnapshotStreamListenerSourceTests.swift in Sources */,
 				EA46611779C3EEF12822508C /* annotations.pb.cc in Sources */,
 				8F4F40E9BC7ED588F67734D5 /* app_testing.mm in Sources */,
 				A6E236CE8B3A47BE32254436 /* array_sorted_map_test.cc in Sources */,
@@ -5401,6 +5408,7 @@
 				5492E0442021457E00B64F25 /* XCTestCase+Await.mm in Sources */,
 				B04E4FE20930384DF3A402F9 /* aggregate_query_test.cc in Sources */,
 				1A3D8028303B45FCBB21CAD3 /* aggregation_result.pb.cc in Sources */,
+				62180C282DF20F6500B370CD /* SnapshotStreamListenerSourceTests.swift in Sources */,
 				02EB33CC2590E1484D462912 /* annotations.pb.cc in Sources */,
 				EBFC611B1BF195D0EC710AF4 /* app_testing.mm in Sources */,
 				FCA48FB54FC50BFDFDA672CD /* array_sorted_map_test.cc in Sources */,

+ 2 - 17
Firestore/Swift/Source/AsyncAwait/Query+AsyncAwait.swift → Firestore/Swift/Source/AsyncAwait/Query+AsyncThrowingStream.swift

@@ -23,23 +23,8 @@ import Foundation
 
 @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
 public extension Query {
-  func asyncThrowingStream() -> AsyncThrowingStream<QuerySnapshot, Error> {
-    AsyncThrowingStream { continuation in
-      let listener = self.addSnapshotListener { snapshot, error in
-        if let snapshot = snapshot {
-          continuation.yield(snapshot)
-        } else if let error = error {
-          continuation.finish(throwing: error)
-        }
-      }
-
-      continuation.onTermination = { _ in
-        listener.remove()
-      }
-    }
-  }
-
-  func asyncThrowingStream(options: SnapshotListenOptions) -> AsyncThrowingStream<QuerySnapshot, Error> {
+  func snapshotStream(options: SnapshotListenOptions = SnapshotListenOptions())
+    -> AsyncThrowingStream<QuerySnapshot, Error> {
     AsyncThrowingStream { continuation in
       let listener = self.addSnapshotListener(options: options) { snapshot, error in
         if let snapshot = snapshot {

+ 0 - 43
Firestore/Swift/Tests/Integration/AsyncAwaitIntegrationTests.swift

@@ -155,48 +155,5 @@ let emptyBundle = """
       }
       XCTAssertThrowsError(try deleteAllIndexes(), "The client has already been terminated.")
     }
-
-    func testCanListenToDefaultSourceFirstAndThenCacheAsyncStream() async 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")
-
-      // 1. Create a signal stream. The test will wait on this stream.
-      //    The Task will write to it after receiving the first snapshot.
-      let (signalStream, signalContinuation) = AsyncStream.makeStream(of: Void.self)
-
-      let stream = query.asyncThrowingStream()
-      var iterator = stream.makeAsyncIterator()
-
-      let task = Task {
-        // This task will now run and eventually signal its progress.
-        let firstSnapshot = try await iterator.next()
-
-        // Assertions for the first snapshot
-        XCTAssertNotNil(firstSnapshot, "Expected an initial snapshot.")
-        try assertQuerySnapshotDataEquals(firstSnapshot!, [["k": "b", "sort": 1]])
-        XCTAssertEqual(firstSnapshot!.metadata.isFromCache, false)
-
-        // 2. Send the signal to the test function now that we have the first snapshot.
-        signalContinuation.yield(())
-        signalContinuation.finish() // We only need to signal once.
-
-        // This next await will be suspended until it's cancelled.
-        let second = try await iterator.next()
-
-        // After cancellation, the iterator should terminate and return nil.
-        XCTAssertNil(second, "iterator.next() should have returned nil after cancellation.")
-      }
-
-      // 3. Instead of sleeping, await the signal from the Task.
-      //    This line will pause execution until `signalContinuation.yield()` is called.
-      await signalStream.first { _ in true }
-
-      // 4. As soon as we receive the signal, we know it's safe to cancel.
-      task.cancel()
-    }
   }
 #endif

+ 211 - 0
Firestore/Swift/Tests/Integration/SnapshotStreamListenerSourceTests.swift

@@ -0,0 +1,211 @@
+/*
+ * Copyright 2025 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.
+ */
+
+@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
+class SnapshotStreamListenerSourceTests: 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 testSnapshotStreamReturnsNilAfterCancellation() async throws {
+    // 1. Set up the collection.
+    let collRef = collectionRef(withDocuments: ["a": ["k": "a"]])
+    readDocumentSet(forRef: collRef) // populate the cache.
+
+    // 2. Create the signal stream to coordinate cancellation timing.
+    let (signalStream, signalContinuation) = AsyncStream.makeStream(of: Void.self)
+
+    // 3. Wrap the asynchronous work in a Task.
+    let task = Task {
+      // Use a standard stream that stays open for new events.
+      var iterator = collRef.snapshotStream().makeAsyncIterator()
+
+      // Await the first snapshot to confirm the listener is active.
+      let firstDefault = try await iterator.next()
+
+      XCTAssertNotNil(firstDefault, "Expected an initial snapshot.")
+      try assertQuerySnapshotDataEquals(firstDefault!, [["k": "a"]])
+      XCTAssertEqual(firstDefault!.metadata.isFromCache, true)
+
+      // 4. Send the signal that the first snapshot has been received.
+      signalContinuation.yield(())
+      signalContinuation.finish()
+
+      // This await will be suspended until the task is cancelled.
+      let secondDefault = try await iterator.next()
+
+      // 5. Assert that the iterator returned nil as requested, because the
+      //    task was cancelled while it was awaiting this event.
+      XCTAssertNil(secondDefault, "iterator.next() should have returned nil after cancellation.")
+    }
+
+    // 6. Wait for the signal, ensuring we don't cancel prematurely.
+    await signalStream.first { _ in true }
+
+    // 7. Now that we know the first snapshot has been processed, cancel the task.
+    task.cancel()
+  }
+
+  func testCanListenToDefaultSourceFirstAndThenCacheAsyncStream() async 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")
+
+    // 1. Create a signal stream. The test will wait on this stream.
+    //    The Task will write to it after receiving the first snapshot.
+    let (signalStreamDefault, signalContinuationDefault) = AsyncStream.makeStream(of: Void.self)
+
+    let streamDefault = query.snapshotStream()
+    var iteratorDefault = streamDefault.makeAsyncIterator()
+
+    let task = Task {
+      // This task will now run and eventually signal its progress.
+      let firstSnapshotDefault = try await iteratorDefault.next()
+
+      // Assertions for the first snapshot
+      XCTAssertNotNil(firstSnapshotDefault, "Expected an initial snapshot.")
+      try assertQuerySnapshotDataEquals(firstSnapshotDefault!, [["k": "b", "sort": 1]])
+      XCTAssertEqual(firstSnapshotDefault!.metadata.isFromCache, false)
+
+      let streamCache = query.snapshotStream(
+        options: SnapshotListenOptions().withSource(ListenSource.cache)
+      )
+      var iteratorCache = streamCache.makeAsyncIterator()
+      // This task will now run and eventually signal its progress.
+      let firstSnapshotCache = try await iteratorCache.next()
+      // Assertions for the first snapshot
+      XCTAssertNotNil(firstSnapshotCache, "Expected an initial snapshot.")
+      try assertQuerySnapshotDataEquals(firstSnapshotCache!, [["k": "b", "sort": 1]])
+      XCTAssertEqual(firstSnapshotCache!.metadata.isFromCache, false)
+
+      // 2. Send the signal to the test function now that we have the first snapshot.
+      signalContinuationDefault.yield(())
+      signalContinuationDefault.finish() // We only need to signal once.
+
+      // This next await will be suspended until it's cancelled.
+      let secondDefault = try await iteratorDefault.next()
+
+      // This next await will be suspended until it's cancelled.
+      let secondCache = try await iteratorCache.next()
+
+      // After cancellation, the iterator should terminate and return nil.
+      XCTAssertNil(secondDefault, "iterator.next() should have returned nil after cancellation.")
+      // After cancellation, the iterator should terminate and return nil.
+      XCTAssertNil(secondCache, "iterator.next() should have returned nil after cancellation.")
+    }
+
+    // 3. Instead of sleeping, await the signal from the Task.
+    //    This line will pause execution until `signalContinuation.yield()` is called.
+    await signalStreamDefault.first { _ in true }
+
+    // 4. As soon as we receive the signal, we know it's safe to cancel.
+    task.cancel()
+  }
+
+  func testCanListenToDefaultSourceFirstAndThenCacheAsync2() async 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")
+
+    // 1. Create iterators for both the default (server) and cache sources.
+    var serverIterator = query.snapshotStream().makeAsyncIterator()
+
+    // 2. Await the server snapshot first. This populates the cache.
+    let serverSnapshot = try await serverIterator.next()
+    XCTAssertNotNil(serverSnapshot)
+    try assertQuerySnapshotDataEquals(serverSnapshot!, [["k": "b", "sort": 1]])
+    XCTAssertFalse(
+      serverSnapshot!.metadata.isFromCache,
+      "The first snapshot should come from the server."
+    )
+
+    // 3. Now, await the snapshot from the cache iterator. It will immediately
+    //    return the data that the server listener just populated.
+    var cacheIterator = query
+      .snapshotStream(options: SnapshotListenOptions().withSource(ListenSource.cache))
+      .makeAsyncIterator()
+    let cacheSnapshot = try await cacheIterator.next()
+
+    XCTAssertNotNil(cacheSnapshot)
+    try assertQuerySnapshotDataEquals(cacheSnapshot!, [["k": "b", "sort": 1]])
+
+    // Because the server listener is active, the cache data is fresh,
+    // so isFromCache will be false.
+    XCTAssertFalse(
+      cacheSnapshot!.metadata.isFromCache,
+      "Cache snapshot metadata should be synced by the active server listener."
+    )
+
+    // Cleanup is handled automatically when the iterators go out of scope.
+  }
+
+  func testCanRaiseSnapshotFromCacheForQueryAsync() async throws {
+    // 1. Set up the collection and populate the cache, same as the original.
+    let collRef = collectionRef(withDocuments: ["a": ["k": "a"]])
+    readDocumentSet(forRef: collRef) // populate the cache.
+
+    // Create an async stream iterator that only listens to the cache.
+    var iterator = collRef
+      .snapshotStream(options: SnapshotListenOptions().withSource(ListenSource.cache))
+      .makeAsyncIterator()
+
+    // Await the snapshot from the iterator.
+    guard let querySnap = try await iterator.next() else {
+      XCTFail("Expected a snapshot from the cache but received nil.")
+      return
+    }
+
+    // 4. Perform the same assertions as the original test.
+    try assertQuerySnapshotDataEquals(querySnap, [["k": "a"]])
+    XCTAssertTrue(querySnap.metadata.isFromCache, "Snapshot should have come from the cache.")
+  }
+}