Repository: firebase/firebase-ios-sdk (language: cpp)

Issue:
Non-sendable type Query / CollectionReference cause warnings in any actor-isolated context 
### Description

Any time I try to use Firestore in an actor-isolated context (ie. within an Actor or a class on the MainActor), there are warnings that `Non-sendable type... cannot cross actor boundary`. This applies to `Query` and `CollectionReference`. Is there an approach to avoid this or can these classes become `Sendable`?

Notes: 
- These warnings appear after setting **Strict Concurrency Checking** to **Complete**.
- I wasn't sure whether to label this as a bug and I couldn't find anything in the documentation explaining how to avoid this.

### Reproducing the issue

![Screenshot 2024-03-29 at 7 40 24 PM](https://github.com/firebase/firebase-ios-sdk/assets/44950578/074e6604-b527-4b5c-8a66-852815220a13)


### Firebase SDK Version

10.23.0

### Xcode Version

15.3

### Installation Method

Swift Package Manager

### Firebase Product(s)

Firestore

### Targeted Platforms

iOS

### Relevant Log Output

_No response_

### If using Swift Package Manager, the project's Package.resolved

<!--- Look below for instructions on how to share your Package.resolved. --->

<details>
<summary>Expand <code>Package.resolved</code> snippet</summary>
<br>

```json

Replace this line with the contents of your Package.resolved.

```

</details>


### If using CocoaPods, the project's Podfile.lock

<!--- Look below for instructions on how to share your Podfile.lock. --->

<details>
<summary>Expand <code>Podfile.lock</code> snippet</summary>
<br>

```yml

Replace this line with the contents of your Podfile.lock!

```

</details>



Discussion / hints:
I couldn't figure out how to label this issue, so I've labeled it for a human to triage. Hang tight.
The firestore API was designed before swift actors existed. We do intend to improve the situation, but this needs to be carefully examined as the SDK is written in C++. I will add this to our todo list, but we also won't have capacity to address in the near future.

Thanks for the report though!
@wu-hui I understand that fully supporting Swift concurrency might be a significant task. However, I have a question about thread safety: is it safe to access `QuerySnapshot` or `DocumentSnapshot` across different threads?

For instance, we listen to Firestore on a utility queue by setting `firestoreSettings.dispatchQueue = DispatchQueue(label: "firestore", qos: .utility)` because we often handle large collections. However, the converted data sometimes needs to be consumed by the UI on the main thread. Therefore, I'd like to know if it's safe to pass the snapshot objects between threads.
One suggestion is to do the parsing while still on the background thread, then just passing a parsed (Sendable, plain value type) model back to the main thread.
Would that work for you?
Yes, that works, but it does create some difficulties. One of the main challenges is the need to frequently check metadata in DocumentSnapshot, such as isFromCache and hasPendingWrites. While it's certainly feasible to create a wrapper type and perform the parsing on a background thread, I find it challenging to locate clear documentation on the thread safety guarantees provided by the Firestore library. So it would be great if there is an official documentation/guidance on the thread safety of these classes :) 

Fix patch:
diff --git a/Firestore/CHANGELOG.md b/Firestore/CHANGELOG.md
index 6a4973c7b02..5ee300f8015 100644
--- a/Firestore/CHANGELOG.md
+++ b/Firestore/CHANGELOG.md
@@ -1,3 +1,7 @@
+# Unreleased
+- [fixed] Marked all public classes with only readonly properties as `Sendable` to address
+  Swift Concurrency Check warning. (#12666)
+
 # 11.1.0
 - [feature] Add `VectorValue` type support.
 
diff --git a/Firestore/Source/Public/FirebaseFirestore/FIRAggregateField.h b/Firestore/Source/Public/FirebaseFirestore/FIRAggregateField.h
index be7826489a3..36706d6c165 100644
--- a/Firestore/Source/Public/FirebaseFirestore/FIRAggregateField.h
+++ b/Firestore/Source/Public/FirebaseFirestore/FIRAggregateField.h
@@ -23,6 +23,7 @@ NS_ASSUME_NONNULL_BEGIN
 /**
  * Represents an aggregation that can be performed by Firestore.
  */
+NS_SWIFT_SENDABLE
 NS_SWIFT_NAME(AggregateField)
 @interface FIRAggregateField : NSObject
 
diff --git a/Firestore/Source/Public/FirebaseFirestore/FIRAggregateQuery.h b/Firestore/Source/Public/FirebaseFirestore/FIRAggregateQuery.h
index 8170d5caa53..764d395f728 100644
--- a/Firestore/Source/Public/FirebaseFirestore/FIRAggregateQuery.h
+++ b/Firestore/Source/Public/FirebaseFirestore/FIRAggregateQuery.h
@@ -26,6 +26,7 @@ NS_ASSUME_NONNULL_BEGIN
 /**
  * A query that calculates aggregations over an underlying query.
  */
+NS_SWIFT_SENDABLE
 NS_SWIFT_NAME(AggregateQuery)
 @interface FIRAggregateQuery : NSObject
 
diff --git a/Firestore/Source/Public/FirebaseFirestore/FIRAggregateQuerySnapshot.h b/Firestore/Source/Public/FirebaseFirestore/FIRAggregateQuerySnapshot.h
index 9a558bebed2..220a09cef17 100644
--- a/Firestore/Source/Public/FirebaseFirestore/FIRAggregateQuerySnapshot.h
+++ b/Firestore/Source/Public/FirebaseFirestore/FIRAggregateQuerySnapshot.h
@@ -24,6 +24,7 @@ NS_ASSUME_NONNULL_BEGIN
 /**
  * The results of executing an `AggregateQuery`.
  */
+NS_SWIFT_SENDABLE
 NS_SWIFT_NAME(AggregateQuerySnapshot)
 @interface FIRAggregateQuerySnapshot : NSObject
 
diff --git a/Firestore/Source/Public/FirebaseFirestore/FIRCollectionReference.h b/Firestore/Source/Public/FirebaseFirestore/FIRCollectionReference.h
index 40ece263952..e0ad130531b 100644
--- a/Firestore/Source/Public/FirebaseFirestore/FIRCollectionReference.h
+++ b/Firestore/Source/Public/FirebaseFirestore/FIRCollectionReference.h
@@ -26,6 +26,7 @@ NS_ASSUME_NONNULL_BEGIN
  * A `CollectionReference` object can be used for adding documents, getting document references,
  * and querying for documents (using the methods inherited from `Query`).
  */
+NS_SWIFT_SENDABLE
 NS_SWIFT_NAME(CollectionReference)
 @interface FIRCollectionReference : FIRQuery
 
diff --git a/Firestore/Source/Public/FirebaseFirestore/FIRDocumentChange.h b/Firestore/Source/Public/FirebaseFirestore/FIRDocumentChange.h
index e36f77382e4..c33166f4043 100644
--- a/Firestore/Source/Public/FirebaseFirestore/FIRDocumentChange.h
+++ b/Firestore/Source/Public/FirebaseFirestore/FIRDocumentChange.h
@@ -43,6 +43,7 @@ typedef NS_ENUM(NSInteger, FIRDocumentChangeType)
  * A `DocumentChange` represents a change to the documents matching a query. It contains the
  * document affected and the type of change that occurred (added, modified, or removed).
  */
+NS_SWIFT_SENDABLE
 NS_SWIFT_NAME(DocumentChange)
 @interface FIRDocumentChange : NSObject
 
diff --git a/Firestore/Source/Public/FirebaseFirestore/FIRDocumentReference.h b/Firestore/Source/Public/FirebaseFirestore/FIRDocumentReference.h
index b6f87450cb3..e340057db22 100644
--- a/Firestore/Source/Public/FirebaseFirestore/FIRDocumentReference.h
+++ b/Firestore/Source/Public/FirebaseFirestore/FIRDocumentReference.h
@@ -39,6 +39,7 @@ typedef void (^FIRDocumentSnapshotBlock)(FIRDocumentSnapshot *_Nullable snapshot
  * may or may not exist. A `DocumentReference` can also be used to create a `CollectionReference` to
  * a subcollection.
  */
+NS_SWIFT_SENDABLE
 NS_SWIFT_NAME(DocumentReference)
 @interface FIRDocumentReference : NSObject
 
diff --git a/Firestore/Source/Public/FirebaseFirestore/FIRDocumentSnapshot.h b/Firestore/Source/Public/FirebaseFirestore/FIRDocumentSnapshot.h
index ef6e011863e..72a699879eb 100644
--- a/Firestore/Source/Public/FirebaseFirestore/FIRDocumentSnapshot.h
+++ b/Firestore/Source/Public/FirebaseFirestore/FIRDocumentSnapshot.h
@@ -55,6 +55,7 @@ typedef NS_ENUM(NSInteger, FIRServerTimestampBehavior) {
  * For a `DocumentSnapshot` that points to a non-existing document, any data access will return
  * `nil`. You can use the `exists` property to explicitly verify a documents existence.
  */
+NS_SWIFT_SENDABLE
 NS_SWIFT_NAME(DocumentSnapshot)
 @interface FIRDocumentSnapshot : NSObject
 
diff --git a/Firestore/Source/Public/FirebaseFirestore/FIRFieldPath.h b/Firestore/Source/Public/FirebaseFirestore/FIRFieldPath.h
index 9f64fbdc99d..e64bd4e95a0 100644
--- a/Firestore/Source/Public/FirebaseFirestore/FIRFieldPath.h
+++ b/Firestore/Source/Public/FirebaseFirestore/FIRFieldPath.h
@@ -23,6 +23,7 @@ NS_ASSUME_NONNULL_BEGIN
  * (referring to a top level field in the document), or a list of field names (referring to a nested
  * field in the document).
  */
+NS_SWIFT_SENDABLE
 NS_SWIFT_NAME(FieldPath)
 @interface FIRFieldPath : NSObject <NSCopying>
 
diff --git a/Firestore/Source/Public/FirebaseFirestore/FIRFieldValue.h b/Firestore/Source/Public/FirebaseFirestore/FIRFieldValue.h
index 8add3dec1fa..9defa3a0569 100644
--- a/Firestore/Source/Public/FirebaseFirestore/FIRFieldValue.h
+++ b/Firestore/Source/Public/FirebaseFirestore/FIRFieldValue.h
@@ -22,6 +22,7 @@ NS_ASSUME_NONNULL_BEGIN
 /**
  * Sentinel values that can be used when writing document fields with `setData()` or `updateData()`.
  */
+NS_SWIFT_SENDABLE
 NS_SWIFT_NAME(FieldValue)
 @interface FIRFieldValue : NSObject
 
diff --git a/Firestore/Source/Public/FirebaseFirestore/FIRFilter.h b/Firestore/Source/Public/FirebaseFirestore/FIRFilter.h
index ef5946eda63..788ecda2554 100644
--- a/Firestore/Source/Public/FirebaseFirestore/FIRFilter.h
+++ b/Firestore/Source/Public/FirebaseFirestore/FIRFilter.h
@@ -24,6 +24,7 @@ NS_ASSUME_NONNULL_BEGIN
  * A Filter represents a restriction on one or more field values and can be used to refine
  * the results of a Query.
  */
+NS_SWIFT_SENDABLE
 NS_SWIFT_NAME(Filter)
 @interface FIRFilter : NSObject
 
diff --git a/Firestore/Source/Public/FirebaseFirestore/FIRGeoPoint.h b/Firestore/Source/Public/FirebaseFirestore/FIRGeoPoint.h
index 4454225caf8..05d864f8475 100644
--- a/Firestore/Source/Public/FirebaseFirestore/FIRGeoPoint.h
+++ b/Firestore/Source/Public/FirebaseFirestore/FIRGeoPoint.h
@@ -25,6 +25,7 @@ NS_ASSUME_NONNULL_BEGIN
  * Latitude values are in the range of [-90, 90].
  * Longitude values are in the range of [-180, 180].
  */
+NS_SWIFT_SENDABLE
 NS_SWIFT_NAME(GeoPoint)
 @interface FIRGeoPoint : NSObject <NSCopying>
 
diff --git a/Firestore/Source/Public/FirebaseFirestore/FIRLoadBundleTask.h b/Firestore/Source/Public/FirebaseFirestore/FIRLoadBundleTask.h
index ff237f3a19a..aa241895d7f 100644
--- a/Firestore/Source/Public/FirebaseFirestore/FIRLoadBundleTask.h
+++ b/Firestore/Source/Public/FirebaseFirestore/FIRLoadBundleTask.h
@@ -36,6 +36,7 @@ typedef NS_ENUM(NSInteger, FIRLoadBundleTaskState) {
 
 /** Represents a progress update or a final state from loading bundles. */
 NS_SWIFT_NAME(LoadBundleTaskProgress)
+NS_SWIFT_SENDABLE
 @interface FIRLoadBundleTaskProgress : NSObject
 
 /** How many documents have been loaded. */
diff --git a/Firestore/Source/Public/FirebaseFirestore/FIRQuery.h b/Firestore/Source/Public/FirebaseFirestore/FIRQuery.h
index c75952876a2..dad8bbd1eed 100644
--- a/Firestore/Source/Public/FirebaseFirestore/FIRQuery.h
+++ b/Firestore/Source/Public/FirebaseFirestore/FIRQuery.h
@@ -41,6 +41,7 @@ typedef void (^FIRQuerySnapshotBlock)(FIRQuerySnapshot *_Nullable snapshot,
  * A `Query` refers to a query which you can read or listen to. You can also construct
  * refined `Query` objects by adding filters and ordering.
  */
+NS_SWIFT_SENDABLE
 NS_SWIFT_NAME(Query)
 @interface FIRQuery : NSObject
 /** :nodoc: */
diff --git a/Firestore/Source/Public/FirebaseFirestore/FIRQuerySnapshot.h b/Firestore/Source/Public/FirebaseFirestore/FIRQuerySnapshot.h
index f3e80b94564..8c649755a35 100644
--- a/Firestore/Source/Public/FirebaseFirestore/FIRQuerySnapshot.h
+++ b/Firestore/Source/Public/FirebaseFirestore/FIRQuerySnapshot.h
@@ -28,6 +28,7 @@ NS_ASSUME_NONNULL_BEGIN
  * using the `documents` property and its size can be inspected with `isEmpty` and
  * `count`.
  */
+NS_SWIFT_SENDABLE
 NS_SWIFT_NAME(QuerySnapshot)
 @interface FIRQuerySnapshot : NSObject
 
diff --git a/Firestore/Source/Public/FirebaseFirestore/FIRSnapshotListenOptions.h b/Firestore/Source/Public/FirebaseFirestore/FIRSnapshotListenOptions.h
index c1ef6a3a3a1..b866a114212 100644
--- a/Firestore/Source/Public/FirebaseFirestore/FIRSnapshotListenOptions.h
+++ b/Firestore/Source/Public/FirebaseFirestore/FIRSnapshotListenOptions.h
@@ -44,6 +44,7 @@ typedef NS_ENUM(NSUInteger, FIRListenSource) {
  * of this class control settings like whether metadata-only changes trigger events and the
  * preferred data source.
  */
+NS_SWIFT_SENDABLE
 NS_SWIFT_NAME(SnapshotListenOptions)
 @interface FIRSnapshotListenOptions : NSObject
 
diff --git a/Firestore/Source/Public/FirebaseFirestore/FIRSnapshotMetadata.h b/Firestore/Source/Public/FirebaseFirestore/FIRSnapshotMetadata.h
index 04e19c1ba68..ad26df21af2 100644
--- a/Firestore/Source/Public/FirebaseFirestore/FIRSnapshotMetadata.h
+++ b/Firestore/Source/Public/FirebaseFirestore/FIRSnapshotMetadata.h
@@ -19,6 +19,7 @@
 NS_ASSUME_NONNULL_BEGIN
 
 /** Metadata about a snapshot, describing the state of the snapshot. */
+NS_SWIFT_SENDABLE
 NS_SWIFT_NAME(SnapshotMetadata)
 @interface FIRSnapshotMetadata : NSObject
 
diff --git a/Firestore/Swift/Tests/API/BasicCompileTests.swift b/Firestore/Swift/Tests/API/BasicCompileTests.swift
index 480f85be3a1..01d2678ff09 100644
--- a/Firestore/Swift/Tests/API/BasicCompileTests.swift
+++ b/Firestore/Swift/Tests/API/BasicCompileTests.swift
@@ -493,36 +493,40 @@ func terminateDb(database db: Firestore) {
   }
 }
 
-#if swift(>=5.5.2)
-  @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
-  func testAsyncAwait(database db: Firestore) async throws {
-    try await db.enableNetwork()
-    try await db.disableNetwork()
-    try await db.waitForPendingWrites()
-    try await db.clearPersistence()
-    try await db.terminate()
-    try await db.runTransaction { _, _ in
-      0
-    }
+@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
+func testAsyncAwait(database db: Firestore) async throws {
+  try await db.enableNetwork()
+  try await db.disableNetwork()
+  try await db.waitForPendingWrites()
+  try await db.clearPersistence()
+  try await db.terminate()
+  _ = try await db.runTransaction { _, _ in
+    0
+  }
 
-    let batch = db.batch()
-    try await batch.commit()
+  let batch = db.batch()
+  try await batch.commit()
 
-    _ = await db.getQuery(named: "foo")
-    _ = try await db.loadBundle(Data())
+  _ = await db.getQuery(named: "foo")
+  _ = try await db.loadBundle(Data())
 
-    let collection = db.collection("coll")
-    try await collection.getDocuments()
-    try await collection.getDocuments(source: FirestoreSource.default)
+  let collection = db.collection("coll")
+  try await collection.getDocuments()
+  try await collection.getDocuments(source: FirestoreSource.default)
 
-    let document = try await collection.addDocument(data: [:])
+  let document = try await collection.addDocument(data: [:])
 
-    try await document.setData([:])
-    try await document.setData([:], merge: true)
-    try await document.setData([:], mergeFields: [])
-    try await document.updateData([:])
-    try await document.delete()
-    try await document.getDocument()
-    try await document.getDocument(source: FirestoreSource.default)
+  try await document.setData([:])
+  try await document.setData([:], merge: true)
+  try await document.setData([:], mergeFields: [])
+  try await document.updateData([:])
+  try await document.delete()
+  try await document.getDocument()
+  try await document.getDocument(source: FirestoreSource.default)
+}
+
+actor regression12666 {
+  func getUser() async throws {
+    _ = try await Firestore.firestore().collection("users").getDocuments()
   }
-#endif
+}


Test patch: