Skip to content

Commit 7a4874e

Browse files
committed
feat(firestore): support TransactionOptions maxAttempts in runTransaction
1 parent 987953f commit 7a4874e

25 files changed

Lines changed: 360 additions & 193 deletions

.github/scripts/compare-types/configs/firestore.ts

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,12 @@ const config: PackageConfig = {
5454
{
5555
name: 'maximum',
5656
reason:
57-
'Aggregate field helper added in firebase-js-sdk; not yet implemented in RN Firebase.',
57+
'FieldValue set/update sentinel (`maximum(n: number)`) exported by firebase-js-sdk ' +
58+
'12.15.0. Firebase Firestore iOS SDK 12.15.0 does not expose FIRFieldValue ' +
59+
'maximum/minimum factories (see Firebase iOS FIRFieldValue reference). Firebase ' +
60+
'Firestore Android SDK 34.15.0 (BOM) exposes FieldValue.maximum/minimum but RN ' +
61+
'Firebase native serialization does not wire them; cross-platform parity deferred ' +
62+
'until iOS SDK adds support. User-accepted gap (C1.2b).',
5863
},
5964
{
6065
name: 'memoryEagerGarbageCollector',
@@ -78,7 +83,12 @@ const config: PackageConfig = {
7883
{
7984
name: 'minimum',
8085
reason:
81-
'Aggregate field helper added in firebase-js-sdk; not yet implemented in RN Firebase.',
86+
'FieldValue set/update sentinel (`minimum(n: number)`) exported by firebase-js-sdk ' +
87+
'12.15.0. Firebase Firestore iOS SDK 12.15.0 does not expose FIRFieldValue ' +
88+
'maximum/minimum factories (see Firebase iOS FIRFieldValue reference). Firebase ' +
89+
'Firestore Android SDK 34.15.0 (BOM) exposes FieldValue.maximum/minimum but RN ' +
90+
'Firebase native serialization does not wire them; cross-platform parity deferred ' +
91+
'until iOS SDK adds support. User-accepted gap (C1.2b).',
8292
},
8393
{
8494
name: 'onSnapshotResume',
@@ -204,12 +214,6 @@ const config: PackageConfig = {
204214
'Base interface for persistent tab manager strategies. The local cache ' +
205215
'configuration API is not yet implemented in RN Firebase.',
206216
},
207-
{
208-
name: 'TransactionOptions',
209-
reason:
210-
'Options object for runTransaction (e.g. maxAttempts). Not yet supported ' +
211-
'by the RN Firebase native transaction implementation.',
212-
},
213217
],
214218

215219
// ---------------------------------------------------------------------------
@@ -281,13 +285,6 @@ const config: PackageConfig = {
281285
'Returns `Promise<void>` in RN Firebase vs `void` in the firebase-js-sdk. ' +
282286
'Phase S hint: **keep-async: deferred persistent-cache IO** (see PS-S2-gap).',
283287
},
284-
{
285-
name: 'runTransaction',
286-
reason:
287-
'The firebase-js-sdk accepts an optional `TransactionOptions` parameter ' +
288-
'(e.g. maxAttempts) which is not yet supported by the RN Firebase native ' +
289-
'transaction implementation.',
290-
},
291288
{
292289
name: 'FirestoreSettings',
293290
reason:

okf-bundle/testing/compare-types-work-queue.md

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ timestamp: 2026-07-03T00:00:00Z
88

99
# Compare-types parity — work queue
1010

11-
> **IN PROGRESS:** **C1.1**firestore `TransactionOptions` + `runTransaction` `maxAttempts`; implementer dispatched.
11+
> **IN PROGRESS:** **C1.2**`aggregateQuerySnapshotEqual`; implementer dispatched.
1212
> **Stack:** `main``new-architecture` ([#9080](https://github.com/invertase/react-native-firebase/pull/9080)) → `pipeline-continue-workqueue` ([#9086](https://github.com/invertase/react-native-firebase/pull/9086)) → **`compare-types-work-queue`** (frontier).
1313
> **Goal:** shrink `.github/scripts/compare-types/configs/*.ts` by fixing real drift or tightening intractable documentation — not blanket parity for native-only surfaces. Machinery: [compare-types README](../../../.github/scripts/compare-types/README.md). Term ids: [iteration vocabulary](iteration-vocabulary.md). Policy: [documentation policy](../documentation-policy.md).
1414
@@ -53,19 +53,19 @@ Before any item's `implementation`:
5353
| Phase | Focus | Status | Outcome |
5454
| ----- | ----- | ------ | ------- |
5555
| **A** | Tier 1 — types/docs only | **partial** | A1–A5, A7–A8, A10 committed; A6/A9 deferred |
56-
| **B** | Tier 2 — moderate / Phase S | **partial** | B1–B3 committed; B4–B9 committed |
57-
| **C** | Tier 3 — hard / structural | **in progress** | C1.1 implementation dispatched |
56+
| **B** | Tier 2 — moderate / Phase S | **complete** | B1–B9 committed (B3 ✅ 2026-07-05) |
57+
| **C** | Tier 3 — hard / structural | **in progress** | C1.1 ✅ committed; C1.2 implementation dispatched |
5858
| **D** | Tier 4 — document intractable | **queued** ||
5959

6060
---
6161

6262
## Current snapshot
6363

64-
**Label:** `c1.1-implementation-2026-07-05`
64+
**Label:** `c1.2-implementation-2026-07-05`
6565

66-
**Next item:** **C1.1**`implementation` (firestore `TransactionOptions` + `runTransaction` `maxAttempts`).
66+
**Next item:** **C1.2**`implementation` (`aggregateQuerySnapshotEqual`).
6767

68-
**Current gates:** B3 all gates **closed**. C1.1 `implementation_gate` **open** — implementer dispatched.
68+
**Current gates:** C1.1 all gates **closed**. C1.2b documented (user-accepted). C1.2 `implementation_gate` **open** — implementer dispatched.
6969

7070
---
7171

@@ -88,17 +88,16 @@ Before any item's `implementation`:
8888
| **B1** | firestore-pipelines | `refactor(firestore/pipelines): add constant preferIntegers option` | closed | closed | closed || `area-focused` | Re-review green 2026-07-04: parser integerLiteral iOS/Android, preferIntegers e2e, sdk-compat; macOS 147 / iOS 152 / Android 152 |
8989
| **B2** | firestore-pipelines | `feat(firestore/pipelines): expose search stage and pipeline expressions` | closed | closed | closed || `area-focused` |`missingInRN` cleared; macOS 150/150; iOS/Android 155/155; firebase-tools 15.22.4; search index verify cycle. Follow-up: dropped RN-only `Type` export → pipelines 0 documented diffs |
9090
| **B3** | storage | `refactor!(storage): sync UploadTask pause resume cancel booleans` | closed | closed | closed || `area-focused` | Committed 2026-07-05: sync `setTaskStatus` boolean; mid-transfer e2e; iOS upload-cancel skip — user-accepted + [firebase-ios-sdk#16353](https://github.com/firebase/firebase-ios-sdk/issues/16353) |
91-
| **C1.1** | firestore | | open | open | open | `implementation` | `area-focused` | Wire `TransactionOptions` + `runTransaction` `maxAttempts` through transaction bridge; implementer dispatched 2026-07-05 |
91+
| **C1.1** | firestore | `feat(firestore): support TransactionOptions maxAttempts in runTransaction` | closed | closed | closed | | `area-focused` | Committed 2026-07-05: TransactionOptions + runTransaction maxAttempts; e2e Transaction 20/20 iOS+Android |
9292
| **B4** | analytics | `refactor!(types): align modular APIs with firebase-js-sdk sync signatures` | closed | closed | closed || `unit-focused` | logEvent sync void |
9393
| **B5** | app-check | `refactor!(types): align modular APIs with firebase-js-sdk sync signatures` | closed | closed | closed || `area-focused` | initializeAppCheck sync AppCheck |
9494
| **B6** | firestore | `refactor!(types): align modular APIs with firebase-js-sdk sync signatures` | closed | closed | closed || `area-focused` | initializeFirestore sync Firestore |
9595
| **B7** | remote-config | `refactor!(types): align modular APIs with firebase-js-sdk sync signatures` | closed | closed | closed || `unit-focused` | FetchStatus native literals documented |
9696
| **B8** | remote-config | `refactor!(types): align modular APIs with firebase-js-sdk sync signatures` | closed | closed | closed || `unit-focused` | RemoteConfigOptions on getRemoteConfig |
9797
| **B9** | firestore | `refactor!(types): align modular APIs with firebase-js-sdk sync signatures` | closed | closed | closed || `unit-focused` | aggregateFieldEqual exported |
9898
| **C0** | Phase C scope || open | open | open | `gap-analysis` | `none` | [Grilling](#phase-c--tier-3-hard--structural) |
99-
| **C1.1** | firestore || open | open | open | `implementation` | `area-focused` | **ex-B10**`TransactionOptions` + `runTransaction` `maxAttempts`; native Android/iOS SDKs support; wire through transaction bridge |
100-
| **C1.2** | firestore || open | open | open | `implementation` | `unit-focused` | `aggregateQuerySnapshotEqual` (pure JS, mirror B9 `aggregateFieldEqual`); `maximum`/`minimum`**C1.2b** blocked-on-native-sdk |
101-
| **C1.2b** | firestore || open | open | open || `none` | `maximum`/`minimum`**document-only** (Phase D): native SDK limited; harden config reason |
99+
| **C1.2** | firestore || open | open | open | `implementation` | `unit-focused` | `aggregateQuerySnapshotEqual` (pure JS); implementer dispatched 2026-07-05 |
100+
| **C1.2b** | firestore || closed | closed | closed || `none` | User-accepted: FieldValue `maximum`/`minimum` — iOS Firebase Firestore 12.15.0 lacks APIs; config hardened 2026-07-05 |
102101
| **C1.3** | firestore || open | open | open || `none` | Local-cache config (15 exports) — **document-only** (Phase D): web/N/A on native |
103102
| **C1.4** | firestore || open | open | open || `none` | Snapshot serialization (3 exports) — **document-only** (Phase D): web-only |
104103
| **C1.5** | firestore || open | open | open || `none` | Index config (4 exports) — **document-only** (Phase D): deprecated in firebase-js-sdk; RNFB will not implement |
@@ -223,7 +222,7 @@ Before any item's `implementation`:
223222
| ---- | --------------- | ------------ |
224223
| **B1**| firestore-pipelines `constant` `preferIntegers` | Add optional param + wire lowering |
225224
| **B2**| firestore-pipelines 6× `missingInRN` | Implement pipeline expressions/types per [pipeline workflow](../packages/firestore/pipeline-implementation-workflow.md) |
226-
| **B3** | storage `UploadTask` sync methods | Native sync TurboModule: spec `setTaskStatus``boolean`, codegen regen, iOS resolve fix; breaking-change queue |
225+
| **B3** | storage `UploadTask` sync methods | Native sync TurboModule: spec `setTaskStatus``boolean`, codegen regen, iOS resolve fix; breaking-change queue |
227226
| **B4**| analytics `logEvent` | Phase S sync-void+queue |
228227
| **B5**| app-check `initializeAppCheck` | Phase S sync-void+gate |
229228
| **B6**| firestore `initializeFirestore` | Phase S sync return when settings in-memory |
@@ -282,9 +281,9 @@ Gap-analysis 2026-07-05: 30 `missingInRN` entries + `differentShape: runTransact
282281

283282
| Item | Exports | Verdict | Proposed fix |
284283
| ---- | ------- | ------- | ------------ |
285-
| **C1.1** | `TransactionOptions`, `runTransaction` | **implement** | Wire `maxAttempts` through JS → `transactionBegin` → native `runTransactionWithOptions` / `TransactionOptions` |
284+
| **C1.1** | `TransactionOptions`, `runTransaction` | **implement** | Wire `maxAttempts` through JS → `transactionBegin` → native `runTransactionWithOptions` / `TransactionOptions` |
286285
| **C1.2** | `aggregateQuerySnapshotEqual` | **implement** | Pure JS equality helper (mirror B9 `aggregateFieldEqual`) |
287-
| **C1.2b** | `maximum`, `minimum` | **document-only** | Native aggregate SDK: count/sum/avg onlyaccept gap; harden config reason (Phase D) |
286+
| **C1.2b** | `maximum`, `minimum` | **document-only** | FieldValue set/update sentinelsuser-accepted gap; iOS SDK lacks APIs; see Phase C Notes |
288287
| **C1.3**| Local-cache factories + 9 types (15 exports) | **document-only** | Web/local-cache API; RN uses native `persistence`/`cacheSizeBytes` — accept gap (Phase D) |
289288
| **C1.4**| `documentSnapshotFromJSON`, `querySnapshotFromJSON`, `onSnapshotResume` | **document-only** | Web-only serialization API — accept gap (Phase D) |
290289
| **C1.5**| `setIndexConfiguration`, `Index`, `IndexConfiguration`, `IndexField` | **document-only** | **Deprecated in firebase-js-sdk; RNFB will not implement** — harden config reason (Phase D) |
@@ -321,7 +320,9 @@ Gap-analysis 2026-07-05: replace Phase B compare-types façades (B4/B5/B6) and r
321320

322321
### Phase C Notes
323322

324-
**C1 gap-analysis (2026-07-05):** … Bulk of C1 is **accepted platform drift****document-only in Phase D** (C1.2b native-SDK-limited; C1.3–C1.6 web/deprecated; C1.7 D3 error-branding). Deprecated-in-SDK exports (C1.5, C1.6) use the batch rule: *"Deprecated in firebase-js-sdk; React Native Firebase will not implement."*
323+
**C1.2b analysis (2026-07-05, user-accepted):** Compare-types `maximum`/`minimum` are **FieldValue set/update sentinels** (`maximum(n: number): FieldValue` in firebase-js-sdk **12.15.0**), not aggregate-query helpers. **iOS Firebase Firestore 12.15.0**`FIRFieldValue` public API has no maximum/minimum factories ([iOS reference](https://firebase.google.com/docs/reference/ios/firebasefirestore/api/reference/Classes/FIRFieldValue)). **Android Firebase Firestore BOM 34.15.0**`FieldValue.maximum`/`minimum` exist for set/update ([Android reference](https://firebase.google.com/docs/reference/android/com/google/firebase/firestore/FieldValue)) but RNFB serialization (`RNFBFirestoreSerialize.m` / `ReactNativeFirebaseFirestoreSerialize.java`) does not wire them. Cross-platform parity deferred until iOS SDK exposes the APIs.
324+
325+
**C1 gap-analysis (2026-07-05):** … Bulk of C1 is **accepted platform drift****document-only in Phase D** (C1.2b iOS-native-limited; C1.3–C1.6 web/deprecated; C1.7 D3 error-branding). Deprecated-in-SDK exports (C1.5, C1.6) use the batch rule: *"Deprecated in firebase-js-sdk; React Native Firebase will not implement."*
325326

326327
**C10 gap-analysis (2026-07-05):** B4/B5/B6 closed compare-types with `void native.X()` façades while native remains async. Three shipped façades + seven still-`Promise` exports (analytics setters, storage retry setters) need native follow-up. B5/B6 config rows already removed — C10.3/C10.4 completion = behavioral correctness, not config row removal. C10.2 clears 5 analytics setter `differentShape` rows.
327328

packages/firestore/__tests__/nativeModuleContract.test.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,4 +122,44 @@ describe('TurboModule wrapper contract (NewArch-AD-17.1)', function () {
122122
'transactionBegin',
123123
]);
124124
});
125+
126+
it('forwards maxAttempts through transactionBegin', function () {
127+
const transactionBegin = jest.fn();
128+
const hostTransaction = createTurboModuleFixture({ transactionBegin });
129+
130+
jest
131+
.mocked(TurboModuleRegistry.get)
132+
.mockReturnValueOnce(createTurboModuleFixture({}))
133+
.mockReturnValueOnce(createTurboModuleFixture({}))
134+
.mockReturnValueOnce(createTurboModuleFixture({}))
135+
.mockReturnValueOnce(hostTransaction);
136+
137+
const config: ModuleConfig = {
138+
namespace: 'firestoreContractMaxAttempts',
139+
nativeModuleName: [
140+
'NativeRNFBTurboFirestore',
141+
'NativeRNFBTurboFirestoreCollection',
142+
'NativeRNFBTurboFirestoreDocument',
143+
'NativeRNFBTurboFirestoreTransaction',
144+
],
145+
nativeEvents: false,
146+
hasMultiAppSupport: true,
147+
hasCustomUrlOrRegionSupport: true,
148+
turboModule: true,
149+
};
150+
151+
class MergeModule extends FirebaseModule<any> {
152+
constructor() {
153+
super({ name: '[DEFAULT]' } as any, config);
154+
}
155+
}
156+
157+
const wrapped = getNativeModule(new MergeModule()) as WrappedNativeModule & {
158+
transactionBegin: (transactionId: number, maxAttempts: number) => void;
159+
};
160+
161+
wrapped.transactionBegin(7, 5);
162+
163+
expect(transactionBegin).toHaveBeenCalledWith('[DEFAULT]', null, 7, 5);
164+
});
125165
});
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { describe, expect, it, jest } from '@jest/globals';
2+
3+
import FirestoreTransactionHandler from '../lib/FirestoreTransactionHandler';
4+
import type { FirestoreInternal } from '../lib/types/internal';
5+
6+
function createHandler(transactionBegin: jest.Mock) {
7+
const firestore = {
8+
native: { transactionBegin, transactionDispose: jest.fn(), transactionApplyBuffer: jest.fn() },
9+
emitter: { addListener: jest.fn() },
10+
eventNameForApp: jest.fn((name: string) => name),
11+
} as unknown as FirestoreInternal;
12+
13+
return new FirestoreTransactionHandler(firestore);
14+
}
15+
16+
describe('runTransaction options passthrough', function () {
17+
it('passes maxAttempts to native transactionBegin when provided', function () {
18+
const transactionBegin = jest.fn();
19+
const handler = createHandler(transactionBegin);
20+
21+
void handler._add(async () => 'ok', { maxAttempts: 5 });
22+
23+
expect(transactionBegin).toHaveBeenCalledWith(0, 5);
24+
});
25+
26+
it('passes zero maxAttempts when options are omitted', function () {
27+
const transactionBegin = jest.fn();
28+
const handler = createHandler(transactionBegin);
29+
30+
void handler._add(async () => 'ok');
31+
32+
expect(transactionBegin).toHaveBeenCalledWith(expect.any(Number), 0);
33+
});
34+
});

0 commit comments

Comments
 (0)