Skip to content

Commit 626dd0c

Browse files
committed
feat(database)!: migrate database to TurboModules
1 parent 9d0cfd8 commit 626dd0c

55 files changed

Lines changed: 3656 additions & 464 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

jest.setup.ts

Lines changed: 39 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -252,59 +252,6 @@ jest.doMock('react-native', () => {
252252
sendUnsentReports: jest.fn(),
253253
setCrashlyticsCollectionEnabled: jest.fn(),
254254
},
255-
RNFBDatabaseModule: {
256-
constants: {
257-
isDatabaseCollectionEnabled: true,
258-
url: 'https://test.firebaseio.com',
259-
ref: 'ref()',
260-
},
261-
on: jest.fn(),
262-
off: jest.fn(),
263-
once: jest.fn(
264-
(_appName: any, _customUrl: any, path: any, _modifiers: any, eventType: any) => {
265-
// Database native methods receive (appName, customUrlOrRegion, ...actualArgs)
266-
let key = 'test';
267-
if (path && typeof path === 'string') {
268-
const parts = path.split('/').filter(p => p);
269-
key = parts[parts.length - 1] || 'test';
270-
}
271-
272-
const snapshotData = {
273-
key,
274-
value: null,
275-
exists: false,
276-
childKeys: [],
277-
priority: null,
278-
};
279-
280-
if (eventType === 'value') {
281-
return Promise.resolve(snapshotData);
282-
}
283-
284-
return Promise.resolve({
285-
snapshot: snapshotData,
286-
previousChildName: null,
287-
});
288-
},
289-
),
290-
useEmulator: jest.fn((_appName: any, _customUrl: any) => Promise.resolve()),
291-
set: jest.fn((_appName: any, _customUrl: any) => Promise.resolve()),
292-
update: jest.fn((_appName: any, _customUrl: any) => Promise.resolve()),
293-
setWithPriority: jest.fn((_appName: any, _customUrl: any) => Promise.resolve()),
294-
remove: jest.fn((_appName: any, _customUrl: any) => Promise.resolve()),
295-
setPriority: jest.fn((_appName: any, _customUrl: any) => Promise.resolve()),
296-
keepSynced: jest.fn((_appName: any, _customUrl: any) => Promise.resolve()),
297-
transactionStart: jest.fn((_appName: any, _customUrl: any) => Promise.resolve()),
298-
transactionTryCommit: jest.fn((_appName: any, _customUrl: any) => Promise.resolve()),
299-
goOnline: jest.fn((_appName: any, _customUrl: any) => Promise.resolve()),
300-
goOffline: jest.fn((_appName: any, _customUrl: any) => Promise.resolve()),
301-
setPersistenceEnabled: jest.fn((_appName: any, _customUrl: any) => Promise.resolve()),
302-
setLoggingEnabled: jest.fn((_appName: any, _customUrl: any) => Promise.resolve()),
303-
setPersistenceCacheSizeBytes: jest.fn((_appName: any, _customUrl: any) =>
304-
Promise.resolve(),
305-
),
306-
getServerTime: jest.fn((_appName: any, _customUrl: any) => Promise.resolve(Date.now())),
307-
},
308255
NativeRNFBTurboFirestore: {
309256
setLogLevel: jest.fn(),
310257
loadBundle: jest.fn(() =>
@@ -385,6 +332,45 @@ jest.doMock('react-native', () => {
385332
}),
386333
),
387334
},
335+
NativeRNFBTurboDatabase: {
336+
goOnline: jest.fn(() => Promise.resolve()),
337+
goOffline: jest.fn(() => Promise.resolve()),
338+
setPersistenceEnabled: jest.fn(),
339+
setLoggingEnabled: jest.fn(),
340+
setPersistenceCacheSizeBytes: jest.fn(),
341+
useEmulator: jest.fn(),
342+
},
343+
NativeRNFBTurboDatabaseReference: {
344+
set: jest.fn(() => Promise.resolve()),
345+
update: jest.fn(() => Promise.resolve()),
346+
setWithPriority: jest.fn(() => Promise.resolve()),
347+
remove: jest.fn(() => Promise.resolve()),
348+
setPriority: jest.fn(() => Promise.resolve()),
349+
},
350+
NativeRNFBTurboDatabaseQuery: {
351+
once: jest.fn(() =>
352+
Promise.resolve({
353+
value: null,
354+
key: null,
355+
exists: false,
356+
childKeys: [],
357+
}),
358+
),
359+
on: jest.fn(),
360+
off: jest.fn(),
361+
keepSynced: jest.fn(() => Promise.resolve()),
362+
},
363+
NativeRNFBTurboDatabaseOnDisconnect: {
364+
onDisconnectCancel: jest.fn(() => Promise.resolve()),
365+
onDisconnectRemove: jest.fn(() => Promise.resolve()),
366+
onDisconnectSet: jest.fn(() => Promise.resolve()),
367+
onDisconnectSetWithPriority: jest.fn(() => Promise.resolve()),
368+
onDisconnectUpdate: jest.fn(() => Promise.resolve()),
369+
},
370+
NativeRNFBTurboDatabaseTransaction: {
371+
transactionStart: jest.fn(),
372+
transactionTryCommit: jest.fn(),
373+
},
388374
NativeRNFBTurboFiam: {
389375
getConstants: () => ({
390376
isMessagesDisplaySuppressed: false,

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

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ timestamp: 2026-06-26T00:00:00Z
88

99
# TurboModule migration — work queue
1010

11-
> **IN PROGRESS (2026-07-02):** Phase **4** **next pickup:** `database` + remaining Tier A/B. P4m committed; P4a events deferred to Phase C ([ADR-4](architecture-decisions.md#newarch-ad-4--events-deferred-to-phase-c--accepted)).
11+
> **IN PROGRESS (2026-07-02):** Phase **4** remaining complex — P4d `database` committed; next pickup from phase table (Tier A/B packages not yet queued).
1212
> **Goal/order:** app foundation → hard probe → easy wins → remaining complex → sync conversion → coordinated break → cleanup (events, shared-state encapsulation). Decisions: [architecture-decisions.md](architecture-decisions.md). Links: [implementation workflow](turbomodule-implementation-workflow.md), [change authoring](../testing/change-authoring-workflow.md), [functions reference](../../../packages/functions/) ([PR #8603](https://github.com/invertase/react-native-firebase/pull/8603)).
1313
1414
Ephemeral tracker; see [OKF policy](../documentation-policy.md).
@@ -322,11 +322,11 @@ Skip steps 1–2 when spec shape is known (most Tier D packages).
322322

323323
## Current snapshot
324324

325-
**Label:** `phase-4-messaging` (2026-07-02); **harness:** area-focused (`messaging`)
325+
**Label:** `phase-4-database` (2026-07-02); **harness:** area-focused (`database`)
326326

327-
**Next item:** Phase **4** `messaging` TurboModule migration
327+
**Next item:** Phase **4** remaining complex (pick next Tier A/B package — no arbiter row yet)
328328

329-
**Current gates:** P4m committed. **Next:** Phase 4 `database` (or next Tier A/B item).
329+
**Current gates:** P4d all gates **closed** — committed `feat(database)!: migrate database to TurboModules` 2026-07-02. Delta review: macOS 213 / iOS 219 / Android 220 passing.
330330

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

@@ -357,6 +357,7 @@ Skip steps 1–2 when spec shape is known (most Tier D packages).
357357
| Phase 3.5 guardrails | P3.5 | **closed** | **closed** | **closed** | done | `full` | `fix: add codegen verify and spec-native parity tests` | Committed `64f99c53d` 2026-07-02. |
358358
| Phase 4a messaging event decision | P4a | n/a | n/a | n/a | done | `none` | none | **Defer** events to Phase C — [NewArch-AD-4 § messaging](architecture-decisions.md#messaging--defer-eventemitter-cutover-migrate-shell-only-in-phase-4). |
359359
| Phase 4 `messaging` TurboModules | P4m | **closed** | **closed** | **closed** | done | `area-focused` | `feat(messaging)!: migrate messaging to TurboModules` | Turbo shell only (AD-4 event path preserved). Jest 38/38 + parity 32/32; codegen:verify incl messaging; legacy Java removed. iOS/Android area e2e green on method-call + listener-registration scope. Foreground `onMessage` delivery test **left `xit`** — flaky FCM harness; re-enable in [Phase C](#deferred-cleanup-phase-eventemitter). |
360+
| Phase 4 `database` TurboModules | P4d | **closed** | **closed** | **closed** | done | `area-focused` | `feat(database)!: migrate database to TurboModules` | 5 Turbo specs/shells + committed codegen; Jest 49 + parity 38; area e2e macOS 213 / iOS 219 / Android 220 (delta logs `/tmp/rnfb-e2e-*-database-delta-review.log`). Remediation: invalidate/teardown parity (Reference+Transaction), onComplete guard, contract test 22 methods, jest mock cleanup. Orchestration: macOS/Android `:8090` pre-flight in `firebase.test.js` + OKF. **Deferred minor:** void spec vs Promise JS API; silent Query `RejectedExecutionException` post-invalidate. |
360361
| Phase Docs — NA reqs + consolidation | PDoc | open | open | open | documentation | `none` | `docs: new-architecture requirements + migration consolidation` | Opportunistic; does not gate 4. |
361362
| Phase PD — platform divergence | PPD | open | open | open | documentation | `none` | `docs(<pkg>): …` per package | Opportunistic; gap-analysis first. |
362363
| Phase R — remove `NativeModules` fallback | PR-fallback | open | open | open | pre-merge-validation | `full` | (Phase R) | Decision B. Jest + `app` e2e "unknown module throws". |

okf-bundle/testing/running-e2e.md

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -97,10 +97,11 @@ macOS: `yarn tests:macos:test-cover` only — same `:8090` transport, no Detox.
9797

9898
**Launch gate (orchestration race fix)**`firebase.test.js` starts the test runner with `RNFB_JET_DEFER_RUN=1`. It listens on 8090 and **defers** `server.run()` until the host signals launch success:
9999

100-
1. Host waits for TCP **8090**, then Metro (debug) if needed, then `launchAppWithRetry`.
101-
2. Host `POST`s **`/orchestrate-state`** (`{ "phase": "launch-pending" | "launch-ok" | … }`) to the control port (best-effort diagnostics).
102-
3. After `launchApp` succeeds, host `POST`s **`/launch-ready`** → test runner calls `server.run()` and the app may receive the mocha-remote `run` action.
103-
4. Mocha tests must not start during a stuck or retried `launchApp`; on inner launch retry the host may kill and respawn the test runner before `terminateApp`/simulator reboot.
100+
1. On Android, host force-stops both test packages and clears any stray **8090** listener before spawning Jet.
101+
2. Host waits for TCP **8090**, then Metro (debug) if needed, then `launchAppWithRetry`.
102+
3. Host `POST`s **`/orchestrate-state`** (`{ "phase": "launch-pending" | "launch-ok" | … }`) to the control port (best-effort diagnostics).
103+
4. After `launchApp` succeeds, host `POST`s **`/launch-ready`** → test runner calls `server.run()` and the app may receive the mocha-remote `run` action.
104+
5. Mocha tests must not start during a stuck or retried `launchApp`; on inner launch retry the host may kill and respawn the test runner before `terminateApp`/simulator reboot.
104105

105106
**Log markers**`[rnfb-e2e] orchestrate-state=…`, `[jet-control] deferring server.run until POST /launch-ready`, `[jet-control] launch-ready received`, `[jet-control] listening on http://…:8091`, `[jet-coverage] …`, `Jet client connected`.
106107

@@ -165,15 +166,34 @@ No in-flight test run on the target platform:
165166

166167
| Platform | Clear when |
167168
|----------|------------|
168-
| **Android** | [Host-clear probes](#host-clear-probes) pass (no instrumentation PID) |
169+
| **Android** | [Android app reset](#android-app-reset-blocking) + [host-clear probes](#host-clear-probes) pass |
169170
| **iOS** | [Host-clear probes](#host-clear-probes) pass — **zero booted simulators** and no stray listener on `:8090`. Detox boots `iPhone 17` from `tests/.detoxrc.js`; do not pre-boot or leave simulators running. |
170171
| **macOS** | [Host-clear probes](#host-clear-probes) pass (no `io.invertase.testing` process) |
171172

172173
Also wait for any visible unfinished `yarn tests:*:test-cover`.
173174

175+
<a id="android-app-reset-blocking"></a>
176+
177+
**Android app reset (blocking)** — run **before every** Android `:test-cover`, not only after a failed run:
178+
179+
```bash
180+
adb -s emulator-5554 shell am force-stop com.invertase.testing
181+
adb -s emulator-5554 shell am force-stop com.invertase.testing.test
182+
```
183+
184+
The main app (`com.invertase.testing`) can sit on **“waiting for jet to start tests…”** while the [host-clear probe](#host-clear-probes) still passes (it only checks `com.invertase.testing.test`). A stale main app then connects as a **second Jet client** after Detox launches a fresh run → `Received a message from the client, but server wasn't running` and no Mocha summary. Force-stop **both** packages, then re-run the probe.
185+
186+
On **darwin** hosts, also clear the macOS test app before Android `:test-cover` — macOS and Android share Jet **`:8090`**. Alternating macOS and Android runs without killing `io.invertase.testing` leaves a stale macOS Jet client that connects when Android starts → duplicate clients and `server wasn't running`.
187+
188+
```bash
189+
! pgrep -x io.invertase.testing >/dev/null 2>&1
190+
```
191+
192+
(`firebase.test.js` runs this automatically via `ensureAndroidJetHostClear` before spawning Android Jet; use the probe manually when prepping from the runbook.)
193+
174194
<a id="host-clear-probes"></a>
175195

176-
**Host-clear probes** — run the block for your platform; **exit 0 = clear** (chain with `&&`):
196+
**Host-clear probes** — run after [Android app reset](#android-app-reset-blocking) (Android) or directly (iOS/macOS); **exit 0 = clear** (chain with `&&`):
177197

178198
```bash
179199
# iOS — booted-device count must be 0
@@ -189,12 +209,13 @@ test -z "$(lsof -nP -iTCP:8090 -sTCP:LISTEN -t 2>/dev/null || true)"
189209

190210
<a id="pre-flight-recovery"></a>
191211

192-
**Pre-flight recovery** — when probes fail after abort, kill, or `EADDRINUSE` on `:8090`. Then re-run [host-clear probes](#host-clear-probes).
212+
**Pre-flight recovery** — when probes fail **after** [Android app reset](#android-app-reset-blocking), abort, kill, or `EADDRINUSE` on `:8090`. Before any new `:test-cover`, kill the stray **8090** listener, re-run force-stop (Android) or the iOS/macOS steps below, then [host-clear probes](#host-clear-probes).
193213

194214
```bash
195-
# Android
215+
# Android — force-stop both apps, then clear the Jet WS listener
196216
adb -s emulator-5554 shell am force-stop com.invertase.testing
197217
adb -s emulator-5554 shell am force-stop com.invertase.testing.test
218+
lsof -nP -iTCP:8090 -sTCP:LISTEN -t | xargs kill 2>/dev/null || true
198219

199220
# iOS — Detox re-boots iPhone 17 after shutdown booted
200221
lsof -nP -iTCP:8090 -sTCP:LISTEN -t | xargs kill 2>/dev/null || true
@@ -203,6 +224,8 @@ pkill -f 'jet.js --target=ios' 2>/dev/null || true
203224
xcrun simctl shutdown booted
204225
```
205226

227+
After Android recovery, verify `pidof com.invertase.testing.test` is empty and `:8090` is closed before rerunning `:test-cover`.
228+
206229
Do **not** use `boot-simulator.sh` or `simctl shutdown all` as routine prep ([what not to do](#what-not-to-do)).
207230

208231
#### 2. Services ready
@@ -332,7 +355,7 @@ Each run owns its blocking `:test-cover` and returns summaries only.
332355

333356
### Interrupted run (abort, killed terminal, EADDRINUSE on :8090)
334357

335-
Run [pre-flight recovery](#pre-flight-recovery), confirm [host-clear probes](#host-clear-probes) pass, then rerun from repo root: `yarn tests:<platform>:build && yarn tests:<platform>:test-cover` (foreground; tee if logging).
358+
Run [pre-flight recovery](#pre-flight-recovery), confirm [host-clear probes](#host-clear-probes) pass, then rerun from repo root: `yarn tests:<platform>:build && yarn tests:<platform>:test-cover` (foreground; tee if logging). Keep one `:test-cover` active at a time on a host.
336359

337360
### What not to do
338361

packages/app/__tests__/specNativeParityHelper.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export const MIGRATED_TURBO_PACKAGES = [
2222
'storage',
2323
'functions',
2424
'messaging',
25+
'database',
2526
] as const;
2627

2728
export type MigratedTurboPackage = (typeof MIGRATED_TURBO_PACKAGES)[number];

packages/database/RNFBDatabase.podspec

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,27 @@ Pod::Spec.new do |s|
2828
s.ios.deployment_target = firebase_ios_target
2929
s.macos.deployment_target = firebase_macos_target
3030
s.tvos.deployment_target = firebase_tvos_target
31-
s.source_files = 'ios/**/*.{h,m}'
31+
s.source_files = 'ios/**/*.{h,m,mm,cpp}'
32+
s.private_header_files = [
33+
'ios/RNFBDatabase/RNFBDatabaseModule.h',
34+
'ios/RNFBDatabase/RNFBDatabaseReferenceModule.h',
35+
'ios/RNFBDatabase/RNFBDatabaseQueryModule.h',
36+
'ios/RNFBDatabase/RNFBDatabaseOnDisconnectModule.h',
37+
'ios/RNFBDatabase/RNFBDatabaseTransactionModule.h',
38+
'ios/generated/**/*.h',
39+
]
40+
s.exclude_files = 'ios/generated/RCTThirdPartyComponentsProvider.*', 'ios/generated/RCTAppDependencyProvider.*', 'ios/generated/RCTModuleProviders.*', 'ios/generated/RCTModulesConformingToProtocolsProvider.*', 'ios/generated/RCTUnstableModulesRequiringMainQueueSetupProvider.*'
41+
42+
s.pod_target_xcconfig = {
43+
"HEADER_SEARCH_PATHS" => "\"$(PODS_TARGET_SRCROOT)/ios/generated/RNFBDatabaseTurboModules\" \"$(PODS_TARGET_SRCROOT)/ios/generated\"",
44+
}
3245

3346
s.dependency 'RNFBApp'
3447

35-
# React Native dependencies
36-
if defined?(install_modules_dependencies()) != nil
37-
install_modules_dependencies(s);
38-
else
39-
s.dependency "React-Core"
48+
install_modules_dependencies(s);
49+
50+
if defined?(ENV["RCT_NEW_ARCH_ENABLED"]) != nil && (ENV["RCT_NEW_ARCH_ENABLED"] == '0')
51+
raise "#{s.name} requires New Architecture. Enable New Architecture to use this module"
4052
end
4153

4254
if defined?($FirebaseSDKVersion)

0 commit comments

Comments
 (0)