feat(database): get() uses the native one-shot read on Android and web - #9345
nickcernera wants to merge 5 commits into
Conversation
Modular get() now performs a single request/response read on Android
(Query.get()) and web (get()) instead of once('value')'s listen/data/unlisten.
Queries, .info paths and any native failure use once('value'), and Android
also falls back when persistence, debug logging or keepSynced is on, so
callers keep once()'s semantics. iOS's native get stays the single-event
observer until FirebaseDatabase fixes getData (firebase-ios-sdk#12168 and
two related bugs).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LAdPSWtZj2CzYxG4gSb8Ws
russellwheatley
left a comment
There was a problem hiding this comment.
One change on the keepSynced fallback.
|
|
||
| // Set by keepSynced(true). Query.get() toggles keepSynced(spec) on its success path, which | ||
| // would cancel an app's own keepSynced on the same spec, so get() uses once() after any. | ||
| private static volatile boolean keepSyncedUsed = false; |
There was a problem hiding this comment.
keepSyncedUsed is wider than the bug.
Query.get() does cancel an app's own keepSynced, but only for the spec it just read. On success Repo.getValue calls keepSynced(spec, true, skipDedup=true) and then keepSynced(spec, false, skipDedup=true). SyncTree.keepSynced removes that spec from keepSyncedQueries. A get on a different path leaves the registration alone, and keepSynced(false) already drops it.
This flag is set on the first keepSynced(true) and never cleared. After that, every get() in the process uses once(), including other paths, other database URLs, and locations that have since been set back to keepSynced(false).
Could you track the specs that are currently keep-synced and fall back to once() only for those? Clear one when keepSynced(false) is called.
There was a problem hiding this comment.
Good catch, thanks. It now tracks the locations that are currently keep-synced. keepSynced(true) adds one, keepSynced(false) removes it, and get() only falls back to once() for those.
One thing I found while checking: orderByPriority() on its own builds the same QuerySpec as the plain ref, since the default params already order by priority. So that counts as the plain location too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EHZB3Mvnbec65q15ckiHoe
Signed a minute ago, just not seeing this update yet. |
… this.timeout in get() e2e Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A6vSSCHebYnRobVkUkZzP2
|
@russellwheatley Apologies for the churn here. The E2E failures are from two bugs in the tests I added, not in |
|
Green now. Two bugs in
E2E Android, Other and iOS all pass on my fork against this commit (Android, Other, iOS). The runs here need your approval whenever you get a chance. |
russellwheatley
left a comment
There was a problem hiding this comment.
Thanks for narrowing this down. One more thing on the test side, see the inline comment.
| boolean enabled, | ||
| Promise promise) { | ||
| DatabaseReference reference = getDatabaseForApp(app, dbURL).getReference(path); | ||
| if (isPlainSpec(modifiers)) { |
There was a problem hiding this comment.
This tracking is the fix for the keepSynced issue, but nothing exercises it. The Android side is only covered by e2e, so a regression here would go unnoticed. Do you mind adding an e2e case for it?
I think something like this would catch it: keepSynced(true) on a ref, get() that same ref, goOffline(), then read it again. If get() cancelled the app's keepSynced, the data wouldn't be cached any more and the offline read would hang. Might also be worth a case where get() on a different path still takes the native route while another location is kept synced, and one where keepSynced(false) clears it again.
If you think that's not observable from JS, let me know and we can work out another way.
There was a problem hiding this comment.
Thanks. Added in 0fbb691 and b2a8932, and writing the tests turned up two more gaps, so there's a fix too.
Two gaps. When Query.get() turns keepSynced off for the spec it read, SyncTree treats it as a removal on the default spec. That walks every view at the location, and KeepSyncedEventRegistration.isSameListener matches any keepSynced registration. So it also cancels:
- a keepSynced on a query at that location, e.g.
keepSynced(query(ref, limitToLast(1)), true)and thenget(ref); - a
keepSynced(true)that lands while a native get of the location is in flight.
The tracking now lives in a small RNFBDatabaseKeepSyncedRegistry, keyed by location for any query. get() uses once() while anything at the location is kept synced. A keepSynced call made during a native get there is replayed, in order, when the get completes.
On the e2e case. I built your case first, but it can't fail in the e2e harness. goOffline() doesn't keep the connection down there: a write made right after it is acked within a second. That's presumably also why .info/connected's offline case is xit. So the read after goOffline() succeeds whether or not keepSynced survived, and I couldn't find another way to see from JS whether keepSynced is still active. Instead:
RNFBDatabaseKeepSyncedRegistryTest(JVM, runs intests:android:unit) covers the tracking, including your other two cases: a get on another location still goes native while one is kept synced, andkeepSynced(false)clears it. It also covers queries, the in-flight replay and its order, and several gets on one location.get.e2e.jskeeps three Android cases that runget()next to keepSynced through each path of the wiring: a kept location, keepSynced turned on mid-read, and a query kept synced. They exercise the code end to end but can't tell a cancelled keepSynced from a live one.- For the same reason, I removed my "waits for the connection while offline" case, since it couldn't fail either.
Checked locally (Android emulator against the Firebase emulators): the database e2e passes (189 passing, 0 failing). Each JVM case below fails against a registry build with that piece removed.
| Removed | Fails |
|---|---|
once() for a location with anything kept synced |
beginNativeGet_locationKeptSynced_returnsFalse, …queryAtLocationKeptSynced…, and 2 more |
| replaying keepSynced made during a native get | keepSynced_nativeGetInFlight_waitsForItInCallOrder, endNativeGet_waitsForTheLastGetAtLocation |
clearing on keepSynced(false) |
beginNativeGet_afterKeepSyncedFalse_returnsTrue, …untilEveryQueryAtLocationIsCleared… |
keying by location (any kept location forces once()) |
beginNativeGet_otherLocationKeptSynced_returnsTrue |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #9345 +/- ##
============================================
- Coverage 69.82% 69.82% -0.00%
- Complexity 2131 2137 +6
============================================
Files 439 439
Lines 25518 25594 +76
Branches 4257 4275 +18
============================================
+ Hits 17816 17869 +53
- Misses 6359 6375 +16
- Partials 1343 1350 +7
Flags with carried forward coverage won't be shown. Click here to find out more. 🚀 New features to boost your workflow:
|
…ion on Android When Query.get() misses the cache, it turns keepSynced on and then off for the location's default spec. Turning it off removes every keepSynced registration at the location, whatever its query (firebase-android-sdk#8433). Tracking only the plain spec missed a keepSynced on a query there, and one made while a native get was in flight. RNFBDatabaseKeepSyncedRegistry now tracks keepSynced by location for any query. get() reads such a location with once(), and a keepSynced call made during a native get is replayed when the get completes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018UP5Ca3Em7tqDZ3Pn63ngu
Also drops the offline case: goOffline() does not hold the connection in the e2e harness (a write made right after it is acked within a second), so that case could not fail. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018UP5Ca3Em7tqDZ3Pn63ngu
Description
Modular
get()wasonce('value'): a listen → data → unlisten exchange of about three server responses and two round trips. On Android and web it now uses the SDKs' single request/response read (Query.get(), webget()). In our measurement that is about 129 B less billed per read and one round trip fewer (numbers in #9344).get(app, dbURL, path, modifiers)onNativeRNFBTurboDatabaseQuery.Query.get(), with the snapshot serialised throughTasks.callexactly asoncedoes. It usesonceinstead when disk persistence is on (Query.get()then resolves from disk after 3 s, even online), when RTDB debug logging is on (a get in flight at disconnect is then never resent), and for a location where the app keeps anything synced. AkeepSyncedcall made while a native get of the location is in flight waits for the get to finish. (On a cache miss,Query.get()turnskeepSyncedoff for the location's default spec. That removes every keepSynced registration there, whatever its query, which cancels the app's keepSynced: Realtime Database Android: keepSynced(true) followed by get() causes AssertionError "listen() called twice for same QuerySpec" firebase/firebase-android-sdk#8433.) The bookkeeping is inRNFBDatabaseKeepSyncedRegistry.get().getDatahas three open bugs (RTDB: callingget()on a path where a parent path is already subscribed to, will return the entire cached parent data rather than the data for the requested path firebase/firebase-ios-sdk#12168, RTDB: getData never completes if the connection drops after the get is sent firebase/firebase-ios-sdk#16717, RTDB: getData crashes with NSInvalidArgumentException on an error reply without a reason firebase/firebase-ios-sdk#16718), and the comment atRNFBDatabaseQueryHelper get:names them. Switching iOS togetDatais a one-line follow-up once they ship.DatabaseQuery._get()backs modularget():.info/*paths useonce('value');once's code and message, so a denied read is not asked twice;once('value'), so the rejection carries the SDK's error code, as it does today.once()is unchanged. Callers keeponce('value')semantics with one exception. The one-shot read resolves with the server's reply as it was when it was sent, so a local write made to an overlapping path while aget()is in flight is not in its result. (oncelayered pending writes over the reply; the web SDK'sget()behaves like this change.) The comment on_getdocuments it.Related issues
Fixes #9344
Release Summary
get()in@react-native-firebase/databasenow performs a single request/response read on Android and web, which bills fewer responses and saves a round trip. iOS is unchanged until FirebaseDatabase fixesgetData.Checklist
AndroidiOSOther(macOS, web)e2etests added or updated inpackages/\*\*/e2ejesttests added or updated inpackages/\*\*/__tests__Test Plan
Run locally on this branch:
yarn codegen(generated specs updated, additions only),yarn codegen:verify,yarn lint(eslint, depcruise, clang-format, google-java-format),yarn tsc:compile,yarn tsc:compile:consumer, andyarn jest packages/database(68 passed). For the keepSynced changes, the Android database e2e (Jet, against the Firebase emulators) and the database JVM unit tests were also run locally. The JVM tests were checked against builds with each part of the tracking removed.__tests__/get.test.ts, per platform:once, and surfaceonce's error;.info/*paths skip the native get.__tests__/nativeModuleContract.test.ts:getis added to the query host (23 spec methods).e2e/query/get.e2e.js, new cases:once;.info/serverTimeOffsetreturns a number;get()of a location that is kept synced, of one whose keepSynced is turned on while the read is in flight, and of one with a query there kept synced.RNFBDatabaseKeepSyncedRegistryTest(JVM,tests:android:unit): a location with any query kept synced reads withonce, while other locations, and a location afterkeepSynced(false), read natively;keepSyncedcalls made during native gets wait for the location's last get and run in call order.patch-packagepatch, is in device QA in our app, together with a FirebaseDatabase source patch carrying the three iOS fixes.🔥