firebase / firebase/firebase-ios-sdk

Snapshot Listener not triggering when hasPendingWrites = false

Open
#15,585 2 comments 0 reactions 1 assignee Claimed by @cherylEnkidu View on GitHub
api: firestore
Dominant language
C++
Stars
6.7k
Forks
1.8k
Avg merge
2d 18h
Merged PRs (30d)
75

Description

### Description

When you create a snapshot listener with `SnapshotListenOptions().withSource(.cache).withIncludeMetadataChanges(true)` it doesn't get triggered when a document syncs and its `hasPendingWrites` becomes `false`.

This happens under specific circumstances outlined in the reproducing section. Namely when you are not listening or fetching data from the server, but instead observing local changes only. Fetching data from the server triggers the cache listener as expected.

Why I believe this to be unexpected behaviour:

- If after you add a document, you fetch the collection with `getDocuments(source: .cache)`, it will return the correct metadata showing `hasPendingWrites=false`.
- If after you add a document, you remove the listener with `cacheListenerRegistration?.remove()` and recreate it, it will log the correct metadata.
```
Listener: 49EY8kyiqMlSwwpij28l hasPendingWrites=false isFromCache=true ["name": matt]
```
- If instead of `withSource(.cache)` you set up the listener with `withSource(.default)`. It behaves as expected. The listener is called twice.
```
Listener: Uryyuhhti6wW5JbUH624 hasPendingWrites=true isFromCache=false ["name": matt]
Listener: Uryyuhhti6wW5JbUH624 hasPendingWrites=false isFromCache=false ["name": matt]
```

### Reproducing the issue

1. Initialize a basic Firebase project with Firestore configured. Nothing fancy here, just a bare bones setup.
2. Run the Emulator. (I am running with: `firebase emulators:start --project demo-123`)
3. Run the iOS app (sample code below)
```
import UIKit
import FirebaseFirestore

class ViewController: UIViewController {

private var cacheListenerRegistration: ListenerRegistration?

override func viewDidLoad() {
super.viewDidLoad()
subscribeToCache()
}

private func subscribeToCache() {
print("subscribeToCache")
let options = SnapshotListenOptions()
.withSource(.cache)
.withIncludeMetadataChanges(true)

cacheListenerRegistration = Firestore.firestore().collection("users").order(by: "name").addSnapshotListener(options: options) { querySnapshot, error in
guard let documents = querySnapshot?.documents else {
return
}

for document in documents {
print("Listener: \(document.documentID) hasPendingWrites=\(document.metadata.hasPendingWrites) isFromCache=\(document.metadata.isFromCache) \(document.data())")
}
}
}

@IBAction func onFetchFromCache() {
print("onFetchFromCache")
Firestore.firestore().collection("users").getDocuments(source: .cache) { (querySnapshot, error) in
if let error {
print("Error getting documents: \(error)")
} else {
for document in querySnapshot!.documents {
print("Cache: \(document.documentID) hasPendingWrites=\(document.metadata.hasPendingWrites) isFromCache=\(document.metadata.isFromCache) \(document.data())")
}
}
}
}

@IBAction func onAddUser() {
print("onAddUser")
Firestore.firestore().collection("users").addDocument(data: ["name":"matt"])
}
}
```

4. Invoke `onAddUser()`. This triggers the cache listener only once when `hasPendingWrites=true`. I would expect a second call when the document syncs and `hasPendingWrites=false`.
```
Listener: Iw1kEjKrOz1KeIUo8Fsh hasPendingWrites=true isFromCache=true ["name": matt]
```

5. Invoke `onFetchFromCache()`. This returns the correct metadata, so the change has occurred but the listener was never triggered.
```
Cache: Iw1kEjKrOz1KeIUo8Fsh hasPendingWrites=false isFromCache=true ["name": matt]
```

### Firebase SDK Version

12.6.0

### Xcode Version

16.4

### Installation Method

Swift Package Manager

### Firebase Product(s)

Firestore

### Targeted Platforms

iOS

### Relevant Log Output

```shell
12.6.0 - [FirebaseCore][I-COR000001] Configuring the default app.
subscribeToCache
12.6.0 - [FirebaseFirestore][I-FST000001] Initializing. Current user:
12.6.0 - [FirebaseFirestore][I-FST000001] Using full collection scan to execute query: Query(canonical_id=users|f:|ob:nameasc__name__asc)
12.6.0 - [FirebaseFirestore][I-FST000001] RemoteStore 313063653138663630 restarting streams as connectivity changed
12.6.0 - [FirebaseFirestore][I-FST000001] WatchStream (313063653138633238) stop
12.6.0 - [FirebaseFirestore][I-FST000001] WriteStream (313063653230346638) stop
12.6.0 - [FirebaseCore][I-COR000033] Data Collection flag is not set.
onAddUser
12.6.0 - [FirebaseFirestore][I-FST000001] WriteStream (313063653230346638) start
12.6.0 - [FirebaseFirestore][I-FST000001] Creating Firestore stub.
Listener: CmZzFmjFcteZoNTcOt1y hasPendingWrites=true isFromCache=true ["name": matt]
12.6.0 - [FirebaseFirestore][I-FST000001] WriteStream (313063653230346638) initial request: : {
database: "projects/demo-123/databases/(default)"
}
12.6.0 - [FirebaseFirestore][I-FST000001] WriteStream (313063653230346638) headers (allowlisted):
12.6.0 - [FirebaseFirestore][I-FST000001] WriteStream (313063653230346638) response: : {
stream_id: "85"
stream_token: "0"
}
12.6.0 - [FirebaseFirestore][I-FST000001] WriteStream (313063653230346638) write request: : {
writes {
update {
name: "projects/demo-123/databases/(default)/documents/users/CmZzFmjFcteZoNTcOt1y"
fields {
key: "name"
value {
string_value: "matt"
}
}
}
}
stream_token: "0"
}
12.6.0 - [FirebaseFirestore][I-FST000001] WriteStream (313063653230346638) headers (allowlisted):
12.6.0 - [FirebaseFirestore][I-FST000001] WriteStream (313063653230346638) response: : {
stream_token: "1"
write_results {
update_time {
seconds: 1765337219
nanos: 377020000
}
}
commit_time {
seconds: 1765337219
nanos: 377020000
}
}
onFetchFromCache
12.6.0 - [FirebaseFirestore][I-FST000001] Using full collection scan to execute query: Query(canonical_id=users|f:|ob:__name__asc)
Cache: CmZzFmjFcteZoNTcOt1y hasPendingWrites=false isFromCache=true ["name": matt]
```

### If using Swift Package Manager, the project's Package.resolved

Expand Package.resolved snippet

```json

{
"originHash" : "c63c63846d9c539229e96de38d6af51417e28c0ee9a0bc48bd0f0f19d923c329",
"pins" : [
{
"identity" : "abseil-cpp-binary",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/abseil-cpp-binary.git",
"state" : {
"revision" : "bbe8b69694d7873315fd3a4ad41efe043e1c07c5",
"version" : "1.2024072200.0"
}
},
{
"identity" : "app-check",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/app-check.git",
"state" : {
"revision" : "61b85103a1aeed8218f17c794687781505fbbef5",
"version" : "11.2.0"
}
},
{
"identity" : "firebase-ios-sdk",
"kind" : "remoteSourceControl",
"location" : "https://github.com/firebase/firebase-ios-sdk",
"state" : {
"revision" : "087bb95235f676c1a37e928769a5b6645dcbd325",
"version" : "12.6.0"
}
},
{
"identity" : "google-ads-on-device-conversion-ios-sdk",
"kind" : "remoteSourceControl",
"location" : "https://github.com/googleads/google-ads-on-device-conversion-ios-sdk",
"state" : {
"revision" : "35b601a60fbbea2de3ea461f604deaaa4d8bbd0c",
"version" : "3.2.0"
}
},
{
"identity" : "googleappmeasurement",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/GoogleAppMeasurement.git",
"state" : {
"revision" : "c2d59acf17a8ba7ed80a763593c67c9c7c006ad1",
"version" : "12.5.0"
}
},
{
"identity" : "googledatatransport",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/GoogleDataTransport.git",
"state" : {
"revision" : "617af071af9aa1d6a091d59a202910ac482128f9",
"version" : "10.1.0"
}
},
{
"identity" : "googleutilities",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/GoogleUtilities.git",
"state" : {
"revision" : "60da361632d0de02786f709bdc0c4df340f7613e",
"version" : "8.1.0"
}
},
{
"identity" : "grpc-binary",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/grpc-binary.git",
"state" : {
"revision" : "75b31c842f664a0f46a2e590a570e370249fd8f6",
"version" : "1.69.1"
}
},
{
"identity" : "gtm-session-fetcher",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/gtm-session-fetcher.git",
"state" : {
"revision" : "c756a29784521063b6a1202907e2cc47f41b667c",
"version" : "4.5.0"
}
},
{
"identity" : "interop-ios-for-google-sdks",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/interop-ios-for-google-sdks.git",
"state" : {
"revision" : "040d087ac2267d2ddd4cca36c757d1c6a05fdbfe",
"version" : "101.0.0"
}
},
{
"identity" : "leveldb",
"kind" : "remoteSourceControl",
"location" : "https://github.com/firebase/leveldb.git",
"state" : {
"revision" : "a0bc79961d7be727d258d33d5a6b2f1023270ba1",
"version" : "1.22.5"
}
},
{
"identity" : "nanopb",
"kind" : "remoteSourceControl",
"location" : "https://github.com/firebase/nanopb.git",
"state" : {
"revision" : "b7e1104502eca3a213b46303391ca4d3bc8ddec1",
"version" : "2.30910.0"
}
},
{
"identity" : "promises",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/promises.git",
"state" : {
"revision" : "540318ecedd63d883069ae7f1ed811a2df00b6ac",
"version" : "2.4.0"
}
},
{
"identity" : "swift-protobuf",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-protobuf.git",
"state" : {
"revision" : "c169a5744230951031770e27e475ff6eefe51f9d",
"version" : "1.33.3"
}
}
],
"version" : 3
}

```

### If using CocoaPods, the project's Podfile.lock

_No response_

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.