Skip to content

Commit 4c8edad

Browse files
committed
refactor(database): return sync parity for connection controls
1 parent fe459fc commit 4c8edad

27 files changed

Lines changed: 155 additions & 143 deletions

docs/migrating-to-v26.mdx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ When migrating from firebase-js-sdk web examples, watch for these high-impact ru
8585
| ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
8686
| **Auth** | Browser persistence, redirects, reCAPTCHA, `revokeAccessToken`, `useDeviceLanguage`, `linkWithPhoneNumber` / `reauthenticateWithPhoneNumber` | Native iOS/Android SDKs manage persistence. Several web-only helpers **throw synchronously** on React Native (`setPersistence`, `getRedirectResult`, `revokeAccessToken`, `useDeviceLanguage`, `linkWithPhoneNumber`, `reauthenticateWithPhoneNumber`). Use native provider flows instead. |
8787
| **Analytics** | Cross-platform modular API | On-device conversion measurement helpers (`initiateOnDeviceConversionMeasurement*`) are **iOS only**; on Android and web the JS layer resolves without calling native (no-op). `logTransaction` (verified iOS in-app purchase events) is **iOS only** and **rejects** on other platforms. |
88-
| **Realtime Database** | `goOnline` / `goOffline` are synchronous; web transport toggles | RN helpers are **async** (native bridge). `getServerTime`, `setPersistenceEnabled`, and `setPersistenceCacheSizeBytes` are RN-specific. `forceLongPolling` / `forceWebSockets` **throw** — transport is native-controlled. |
88+
| **Realtime Database** | `goOnline` / `goOffline` are synchronous; web transport toggles | `goOnline` / `goOffline` are **synchronous** through TurboModules, matching firebase-js-sdk. `getServerTime`, `setPersistenceEnabled`, and `setPersistenceCacheSizeBytes` are RN-specific. `forceLongPolling` / `forceWebSockets` **throw** — transport is native-controlled. |
8989
| **Phone Number Verification** | N/A (Android SDK only) | **Android only** — all entry points throw on iOS and Web. |
9090
| **Cloud Storage** | `uploadBytes` / `uploadString` from JS blobs | `putFile` and `writeToFile` are **native-only** file-path APIs with no firebase-js-sdk equivalent. Retry-time setters are modular functions on RN, not instance properties. |
9191
| **Cloud Messaging** | Web push / service-worker surface | FCM token lifecycle, permissions, background handlers, and **APNs token APIs are iOS only**. Event delivery still uses the legacy native event proxy during the TurboModule migration; behavior matches pre-v26 releases but will change when Codegen events land in a future release. |
@@ -534,6 +534,10 @@ The namespaced API for `@react-native-firebase/database` has been **removed**. T
534534
| `database().ref()` | `ref(database, path?)` |
535535
| `database().refFromURL()` | `refFromURL(database, url)` |
536536

537+
## Signature changes
538+
539+
- `goOnline(db)` and `goOffline(db)` (and the database instance methods) now return `void` synchronously (previously `Promise<void>`), matching firebase-js-sdk. Remove any `await` on these calls.
540+
537541
# Cloud Storage
538542

539543
The namespaced API for `@react-native-firebase/storage` has been **removed**. This package is **modular-only** — use `getStorage()` and root-level helper functions.

jest.setup.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -341,8 +341,8 @@ jest.doMock('react-native', () => {
341341
),
342342
},
343343
NativeRNFBTurboDatabase: {
344-
goOnline: jest.fn(() => Promise.resolve()),
345-
goOffline: jest.fn(() => Promise.resolve()),
344+
goOnline: jest.fn(),
345+
goOffline: jest.fn(),
346346
setPersistenceEnabled: jest.fn(),
347347
setLoggingEnabled: jest.fn(),
348348
setPersistenceCacheSizeBytes: jest.fn(),

okf-bundle/new-architecture/migration-work-queue.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,7 @@ The "keep async if it does network/IO/disk" rule in the discriminator **assumes*
231231
| `needs-native-change` | `analytics` fire-and-forget setters/events; `app-check/initializeAppCheck`; `firestore/initializeFirestore`; `storage` task controls; storage retry setters | Web APIs are sync or fire-and-forget, but RNFB native paths currently wrap async work or need behavior clarification before sync conversion. |
232232
| `keep-async` | Firestore persistent cache index manager delete/enable/disable | Web returns `void` but starts real persistent-cache work; do not block JS thread. |
233233

234-
**Next slice:** `app/registerVersion` because RNFB already behaves synchronously and Phase S should only change the public type/compare-types entry. Planned subject: `refactor(app): return sync parity for registerVersion`.
234+
**Next slice:** `convert` inventory complete (2026-07-03). Remaining gap-analysis verdicts (`needs-native-change`, `keep-async`) are out of Phase S scope until native behavior is clarified or documented only.
235235

236236
---
237237

@@ -332,9 +332,9 @@ Skip steps 1–2 when spec shape is known (most Tier D packages).
332332

333333
**Label:** `phase-s-sync-conversion` (2026-07-03)
334334

335-
**Next item:** Phase **S** database sync controls (`PS-database-online` — after perf commit)
335+
**Next item:** Phase **S** `convert` inventory **complete** — deferred `needs-native-change` / `keep-async` items documented in gap-analysis table
336336

337-
**Current gates:** PS-perf-metrics all gates **closed** — committing `refactor(perf): return sync parity for metric controls`.
337+
**Current gates:** PS-database-online all gates **closed** — committing `refactor(database): return sync parity for connection controls`.
338338

339339
**Host rule:** one `:test-cover` at a time — never parallel subagents with e2e.
340340

@@ -375,6 +375,7 @@ Skip steps 1–2 when spec shape is known (most Tier D packages).
375375
| Phase S `app/registerVersion` sync parity | PS-app-registerVersion | **closed** | **closed** | **closed** | done | `area-focused` | `refactor(app): return sync parity for registerVersion` | Implemented 2026-07-03: `registerVersion(): void`, sync throw Jest assertion, removed stale `configs/app.ts` async-vs-sync entry. Green: `lerna:prepare`, `tsc:compile`, `tsc:compile:consumer`, focused app Jest 10/10, `reference:api`, `compare:types`, `lint:js`. Independent review: no findings; no e2e needed for type-only scope. |
376376
| Phase S auth parser/TOTP sync parity | PS-auth-parsers | **closed** | **closed** | **closed** | done | `area-focused` | `refactor(auth): return sync parity for auth parsers` | Committed 2026-07-03. Sync `isSignInWithEmailLink` + `TotpSecret.generateQrCodeUrl`; native sync error shape; jest.setup; tests + e2e sync calls; compare-types + v26 doc. Validation: Jest 91/91, compare:types, lint, reference:api, codegen:verify exit 0. Android area e2e 159/15/0 (`/tmp/rnfb-e2e-android-auth-phaseS-final.log`). |
377377
| Phase S perf metric controls sync parity | PS-perf-metrics | **closed** | **closed** | **closed** | done | `area-focused` | `refactor(perf): return sync parity for metric controls` | Sync trace/http/screen start/stop; Android *Sync helpers; iOS sync shells. Jest 10/10; compare:types, lint, reference:api green. Review e2e iOS/Android 60/2/0 (`/tmp/rnfb-e2e-*-perf-review.log`). macOS N/A (module skip). Remediation: perf usage docs await removed. |
378+
| Phase S database connection sync parity | PS-database-online | **closed** | **closed** | **closed** | done | `area-focused` | `refactor(database): return sync parity for connection controls` | Sync turbo + instance goOnline/goOffline; Android direct SDK calls; e2e await removed on modular calls. Jest 52/52; codegen:verify exit 0. Review e2e macOS 181/10/0, iOS 182/9/0, Android 183/8/0 (`/tmp/rnfb-e2e-*-database-review.log`). |
378379

379380
---
380381

packages/database/__tests__/database.test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,4 +229,33 @@ describe('Database', function () {
229229
expect(update).toBeDefined();
230230
});
231231
});
232+
233+
describe('synchronous connection control parity', function () {
234+
it('`goOnline()` and `goOffline()` return synchronously (not Promises)', function () {
235+
const db = getDatabase();
236+
237+
const onlineResult = goOnline(db);
238+
expect(onlineResult).toBeUndefined();
239+
expect(onlineResult).not.toBeInstanceOf(Promise);
240+
241+
const offlineResult = goOffline(db);
242+
expect(offlineResult).toBeUndefined();
243+
expect(offlineResult).not.toBeInstanceOf(Promise);
244+
});
245+
246+
it('database instance `goOnline()` / `goOffline()` return synchronously (not Promises)', function () {
247+
const db = getDatabase() as ReturnType<typeof getDatabase> & {
248+
goOnline(): void;
249+
goOffline(): void;
250+
};
251+
252+
const onlineResult = db.goOnline();
253+
expect(onlineResult).toBeUndefined();
254+
expect(onlineResult).not.toBeInstanceOf(Promise);
255+
256+
const offlineResult = db.goOffline();
257+
expect(offlineResult).toBeUndefined();
258+
expect(offlineResult).not.toBeInstanceOf(Promise);
259+
});
260+
});
232261
});

packages/database/__tests__/nativeModuleContract.test.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,4 +165,35 @@ describe('TurboModule wrapper contract (NewArch-AD-17.1)', function () {
165165

166166
expect(Object.keys(wrapped).sort()).toEqual([...ALL_SPEC_METHODS].sort());
167167
});
168+
it('returns synchronously from goOnline and goOffline on the main host (Phase S)', function () {
169+
const goOnlineMock = jest.fn();
170+
const goOfflineMock = jest.fn();
171+
172+
jest
173+
.mocked(TurboModuleRegistry.get)
174+
.mockReturnValueOnce(
175+
createTurboModuleFixture({ goOnline: goOnlineMock, goOffline: goOfflineMock }),
176+
)
177+
.mockReturnValueOnce(createTurboModuleFixture({}))
178+
.mockReturnValueOnce(createTurboModuleFixture({}))
179+
.mockReturnValueOnce(createTurboModuleFixture({}))
180+
.mockReturnValueOnce(createTurboModuleFixture({}));
181+
182+
const wrapped = getNativeModule(
183+
createMergeModule('databaseSyncConnection'),
184+
) as WrappedNativeModule & {
185+
goOnline: () => unknown;
186+
goOffline: () => unknown;
187+
};
188+
189+
const onlineResult = wrapped.goOnline();
190+
expect(onlineResult).toBeUndefined();
191+
expect(onlineResult).not.toBeInstanceOf(Promise);
192+
expect(goOnlineMock).toHaveBeenCalledTimes(1);
193+
194+
const offlineResult = wrapped.goOffline();
195+
expect(offlineResult).toBeUndefined();
196+
expect(offlineResult).not.toBeInstanceOf(Promise);
197+
expect(goOfflineMock).toHaveBeenCalledTimes(1);
198+
});
168199
});

packages/database/android/src/main/java/io/invertase/firebase/database/UniversalFirebaseDatabaseModule.java

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,6 @@
2020
import static io.invertase.firebase.database.UniversalFirebaseDatabaseCommon.getDatabaseForApp;
2121

2222
import android.content.Context;
23-
import com.google.android.gms.tasks.Task;
24-
import com.google.android.gms.tasks.Tasks;
2523
import io.invertase.firebase.common.UniversalFirebaseModule;
2624

2725
public class UniversalFirebaseDatabaseModule extends UniversalFirebaseModule {
@@ -30,19 +28,11 @@ public class UniversalFirebaseDatabaseModule extends UniversalFirebaseModule {
3028
super(context, serviceName);
3129
}
3230

33-
Task<Void> goOnline(String appName, String dbURL) {
34-
return Tasks.call(
35-
() -> {
36-
getDatabaseForApp(appName, dbURL).goOnline();
37-
return null;
38-
});
31+
void goOnline(String appName, String dbURL) {
32+
getDatabaseForApp(appName, dbURL).goOnline();
3933
}
4034

41-
Task<Void> goOffline(String appName, String dbURL) {
42-
return Tasks.call(
43-
() -> {
44-
getDatabaseForApp(appName, dbURL).goOffline();
45-
return null;
46-
});
35+
void goOffline(String appName, String dbURL) {
36+
getDatabaseForApp(appName, dbURL).goOffline();
4737
}
4838
}

packages/database/android/src/reactnative/java/io/invertase/firebase/database/NativeRNFBTurboDatabase.java

Lines changed: 4 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,7 @@
2020
import static io.invertase.firebase.database.UniversalFirebaseDatabaseCommon.addEmulatorConfig;
2121

2222
import com.facebook.fbreact.specs.NativeRNFBTurboDatabaseSpec;
23-
import com.facebook.react.bridge.Promise;
2423
import com.facebook.react.bridge.ReactApplicationContext;
25-
import io.invertase.firebase.common.ReactNativeFirebaseModule;
2624
import io.invertase.firebase.common.UniversalFirebasePreferences;
2725

2826
public class NativeRNFBTurboDatabase extends NativeRNFBTurboDatabaseSpec {
@@ -35,33 +33,13 @@ public NativeRNFBTurboDatabase(ReactApplicationContext reactContext) {
3533
}
3634

3735
@Override
38-
public void goOnline(String app, String dbURL, Promise promise) {
39-
module
40-
.goOnline(app, dbURL)
41-
.addOnCompleteListener(
42-
task -> {
43-
if (task.isSuccessful()) {
44-
promise.resolve(task.getResult());
45-
} else {
46-
ReactNativeFirebaseModule.rejectPromiseWithExceptionMap(
47-
promise, task.getException());
48-
}
49-
});
36+
public void goOnline(String app, String dbURL) {
37+
module.goOnline(app, dbURL);
5038
}
5139

5240
@Override
53-
public void goOffline(String app, String dbURL, Promise promise) {
54-
module
55-
.goOffline(app, dbURL)
56-
.addOnCompleteListener(
57-
task -> {
58-
if (task.isSuccessful()) {
59-
promise.resolve(task.getResult());
60-
} else {
61-
ReactNativeFirebaseModule.rejectPromiseWithExceptionMap(
62-
promise, task.getException());
63-
}
64-
});
41+
public void goOffline(String app, String dbURL) {
42+
module.goOffline(app, dbURL);
6543
}
6644

6745
@Override

packages/database/android/src/reactnative/java/io/invertase/firebase/database/generated/java/com/facebook/fbreact/specs/NativeRNFBTurboDatabaseSpec.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
package com.facebook.fbreact.specs;
1414

1515
import com.facebook.proguard.annotations.DoNotStrip;
16-
import com.facebook.react.bridge.Promise;
1716
import com.facebook.react.bridge.ReactApplicationContext;
1817
import com.facebook.react.bridge.ReactContextBaseJavaModule;
1918
import com.facebook.react.bridge.ReactMethod;
@@ -34,11 +33,11 @@ public NativeRNFBTurboDatabaseSpec(ReactApplicationContext reactContext) {
3433

3534
@ReactMethod
3635
@DoNotStrip
37-
public abstract void goOnline(String app, String dbURL, Promise promise);
36+
public abstract void goOnline(String app, String dbURL);
3837

3938
@ReactMethod
4039
@DoNotStrip
41-
public abstract void goOffline(String app, String dbURL, Promise promise);
40+
public abstract void goOffline(String app, String dbURL);
4241

4342
@ReactMethod
4443
@DoNotStrip

packages/database/android/src/reactnative/java/io/invertase/firebase/database/generated/jni/RNFBDatabaseTurboModules-generated.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,12 @@ namespace facebook::react {
1414

1515
static facebook::jsi::Value __hostFunction_NativeRNFBTurboDatabaseSpecJSI_goOnline(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) {
1616
static jmethodID cachedMethodId = nullptr;
17-
return static_cast<JavaTurboModule &>(turboModule).invokeJavaMethod(rt, PromiseKind, "goOnline", "(Ljava/lang/String;Ljava/lang/String;Lcom/facebook/react/bridge/Promise;)V", args, count, cachedMethodId);
17+
return static_cast<JavaTurboModule &>(turboModule).invokeJavaMethod(rt, VoidKind, "goOnline", "(Ljava/lang/String;Ljava/lang/String;)V", args, count, cachedMethodId);
1818
}
1919

2020
static facebook::jsi::Value __hostFunction_NativeRNFBTurboDatabaseSpecJSI_goOffline(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) {
2121
static jmethodID cachedMethodId = nullptr;
22-
return static_cast<JavaTurboModule &>(turboModule).invokeJavaMethod(rt, PromiseKind, "goOffline", "(Ljava/lang/String;Ljava/lang/String;Lcom/facebook/react/bridge/Promise;)V", args, count, cachedMethodId);
22+
return static_cast<JavaTurboModule &>(turboModule).invokeJavaMethod(rt, VoidKind, "goOffline", "(Ljava/lang/String;Ljava/lang/String;)V", args, count, cachedMethodId);
2323
}
2424

2525
static facebook::jsi::Value __hostFunction_NativeRNFBTurboDatabaseSpecJSI_setPersistenceEnabled(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) {

packages/database/android/src/reactnative/java/io/invertase/firebase/database/generated/jni/react/renderer/components/RNFBDatabaseTurboModules/RNFBDatabaseTurboModulesJSI-generated.cpp

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,20 @@
1212
namespace facebook::react {
1313

1414
static jsi::Value __hostFunction_NativeRNFBTurboDatabaseCxxSpecJSI_goOnline(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
15-
return static_cast<NativeRNFBTurboDatabaseCxxSpecJSI *>(&turboModule)->goOnline(
15+
static_cast<NativeRNFBTurboDatabaseCxxSpecJSI *>(&turboModule)->goOnline(
1616
rt,
1717
count <= 0 ? throw jsi::JSError(rt, "Expected argument in position 0 to be passed") : args[0].asString(rt),
1818
count <= 1 ? throw jsi::JSError(rt, "Expected argument in position 1 to be passed") : args[1].asString(rt)
1919
);
20+
return jsi::Value::undefined();
2021
}
2122
static jsi::Value __hostFunction_NativeRNFBTurboDatabaseCxxSpecJSI_goOffline(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
22-
return static_cast<NativeRNFBTurboDatabaseCxxSpecJSI *>(&turboModule)->goOffline(
23+
static_cast<NativeRNFBTurboDatabaseCxxSpecJSI *>(&turboModule)->goOffline(
2324
rt,
2425
count <= 0 ? throw jsi::JSError(rt, "Expected argument in position 0 to be passed") : args[0].asString(rt),
2526
count <= 1 ? throw jsi::JSError(rt, "Expected argument in position 1 to be passed") : args[1].asString(rt)
2627
);
28+
return jsi::Value::undefined();
2729
}
2830
static jsi::Value __hostFunction_NativeRNFBTurboDatabaseCxxSpecJSI_setPersistenceEnabled(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
2931
static_cast<NativeRNFBTurboDatabaseCxxSpecJSI *>(&turboModule)->setPersistenceEnabled(

0 commit comments

Comments
 (0)