@react-native-firebase/database 26.4.0 → 27.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/RNFBDatabase.podspec +8 -2
  3. package/android/build.gradle +5 -0
  4. package/android/src/main/java/io/invertase/firebase/database/RNFBDatabaseKeepSyncedRegistry.java +106 -0
  5. package/android/src/main/java/io/invertase/firebase/database/RNFBDatabaseListenerRegistry.kt +133 -0
  6. package/android/src/main/java/io/invertase/firebase/database/RNFBDatabaseQueryRegistry.kt +76 -0
  7. package/android/src/main/java/io/invertase/firebase/database/RNFBDatabaseTransactionRegistry.kt +67 -0
  8. package/android/src/main/java/io/invertase/firebase/database/UniversalFirebaseDatabaseOnDisconnectModule.java +102 -9
  9. package/android/src/reactnative/java/io/invertase/firebase/database/NativeRNFBTurboDatabaseOnDisconnect.java +6 -11
  10. package/android/src/reactnative/java/io/invertase/firebase/database/NativeRNFBTurboDatabaseQuery.java +93 -2
  11. package/android/src/reactnative/java/io/invertase/firebase/database/generated/java/com/facebook/fbreact/specs/NativeRNFBTurboDatabaseQuerySpec.java +4 -0
  12. package/android/src/reactnative/java/io/invertase/firebase/database/generated/jni/RNFBDatabaseTurboModules-generated.cpp +6 -0
  13. package/android/src/reactnative/java/io/invertase/firebase/database/generated/jni/react/renderer/components/RNFBDatabaseTurboModules/RNFBDatabaseTurboModulesJSI.h +12 -0
  14. package/android/src/test/java/io/invertase/firebase/database/RNFBDatabaseKeepSyncedRegistryTest.java +174 -0
  15. package/android/src/test/java/io/invertase/firebase/database/RNFBDatabaseListenerRegistryTest.java +31 -0
  16. package/android/src/test/java/io/invertase/firebase/database/RNFBDatabaseQueryRegistryTest.java +55 -0
  17. package/android/src/test/java/io/invertase/firebase/database/UniversalFirebaseDatabaseOnDisconnectCancelErrorTest.java +137 -0
  18. package/dist/module/DatabaseQuery.js +52 -1
  19. package/dist/module/DatabaseQuery.js.map +1 -1
  20. package/dist/module/DatabaseSyncTree.js +1 -1
  21. package/dist/module/DatabaseSyncTree.js.map +1 -1
  22. package/dist/module/index.js +1 -2
  23. package/dist/module/index.js.map +1 -1
  24. package/dist/module/modular/query.js +1 -1
  25. package/dist/module/modular/query.js.map +1 -1
  26. package/dist/module/version.js +1 -1
  27. package/dist/module/web/RNFBDatabaseModule.js +7 -0
  28. package/dist/module/web/RNFBDatabaseModule.js.map +1 -1
  29. package/dist/typescript/lib/DatabaseQuery.d.ts +25 -0
  30. package/dist/typescript/lib/DatabaseQuery.d.ts.map +1 -1
  31. package/dist/typescript/lib/index.d.ts +1 -1
  32. package/dist/typescript/lib/types/internal.d.ts +2 -0
  33. package/dist/typescript/lib/types/internal.d.ts.map +1 -1
  34. package/dist/typescript/lib/version.d.ts +1 -1
  35. package/dist/typescript/lib/web/RNFBDatabaseModule.d.ts.map +1 -1
  36. package/ios/RNFBDatabase/RNFBDatabaseListenerRegistry.m +24 -17
  37. package/ios/RNFBDatabase/RNFBDatabaseListenerRegistryStorage.swift +81 -0
  38. package/ios/RNFBDatabase/RNFBDatabaseNullSentinelDecoder.h +36 -0
  39. package/ios/RNFBDatabase/RNFBDatabaseNullSentinelDecoder.m +48 -0
  40. package/ios/RNFBDatabase/RNFBDatabaseOnDisconnectHelper.m +29 -26
  41. package/ios/RNFBDatabase/RNFBDatabaseQueryHelper.h +7 -0
  42. package/ios/RNFBDatabase/RNFBDatabaseQueryHelper.m +20 -0
  43. package/ios/RNFBDatabase/RNFBDatabaseQueryModule.mm +14 -0
  44. package/ios/RNFBDatabase/RNFBDatabaseQueryRegistry.m +25 -51
  45. package/ios/RNFBDatabase/RNFBDatabaseQueryRegistryStorage.swift +104 -0
  46. package/ios/RNFBDatabase/RNFBDatabaseReferenceHelper.m +14 -12
  47. package/ios/RNFBDatabase/RNFBDatabaseTransactionHelper.m +3 -1
  48. package/ios/RNFBDatabase.xcodeproj/project.pbxproj +12 -0
  49. package/ios/RNFBDatabaseUnitTests/RNFBDatabaseListenerRegistryStorageTests.swift +86 -0
  50. package/ios/RNFBDatabaseUnitTests/RNFBDatabaseNullSentinelDecodeTests.m +118 -0
  51. package/ios/RNFBDatabaseUnitTests/RNFBDatabaseQueryRegistryStorageTests.swift +181 -0
  52. package/ios/RNFBDatabaseUnitTests/RNFBDatabaseUnitTests.xcodeproj/project.pbxproj +42 -0
  53. package/ios/generated/RNFBDatabaseTurboModules/RNFBDatabaseTurboModules-generated.mm +7 -0
  54. package/ios/generated/RNFBDatabaseTurboModules/RNFBDatabaseTurboModules.h +6 -0
  55. package/ios/generated/RNFBDatabaseTurboModulesJSI.h +12 -0
  56. package/lib/DatabaseQuery.ts +56 -0
  57. package/lib/DatabaseSyncTree.ts +1 -1
  58. package/lib/index.ts +1 -1
  59. package/lib/modular/query.ts +1 -1
  60. package/lib/types/internal.ts +2 -0
  61. package/lib/version.ts +1 -1
  62. package/lib/web/RNFBDatabaseModule.ts +13 -0
  63. package/package.json +4 -4
  64. package/specs/NativeRNFBTurboDatabaseQuery.ts +1 -0
  65. package/android/src/main/java/io/invertase/firebase/database/RNFBDatabaseListenerRegistry.java +0 -117
  66. package/android/src/main/java/io/invertase/firebase/database/RNFBDatabaseQueryRegistry.java +0 -78
  67. package/android/src/main/java/io/invertase/firebase/database/RNFBDatabaseTransactionRegistry.java +0 -71
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Copyright (c) 2016-present Invertase Limited & Contributors
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this library except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ import XCTest
18
+
19
+ final class RNFBDatabaseListenerRegistryStorageTests: XCTestCase {
20
+ private var storage: RNFBDatabaseListenerRegistryStorage!
21
+
22
+ override func setUp() {
23
+ super.setUp()
24
+ storage = RNFBDatabaseListenerRegistryStorage()
25
+ }
26
+
27
+ func testPutIfAbsent_storesWhenFree_andIncrementsOccupancy() {
28
+ let handle = 7 as NSNumber
29
+ XCTAssertTrue(storage.putIfAbsent("k" as NSString, value: handle))
30
+ XCTAssertTrue(storage.get("k" as NSString) === handle)
31
+ XCTAssertTrue(storage.hasListeners)
32
+ XCTAssertTrue(storage.hasEventListener("k" as NSString))
33
+ }
34
+
35
+ func testPutIfAbsent_collision_keepsExistingWithoutOccupancyBump() {
36
+ let first = 1 as NSNumber
37
+ let second = 2 as NSNumber
38
+ XCTAssertTrue(storage.putIfAbsent("k" as NSString, value: first))
39
+ XCTAssertFalse(storage.putIfAbsent("k" as NSString, value: second))
40
+ XCTAssertTrue(storage.get("k" as NSString) === first)
41
+ XCTAssertTrue(storage.hasListeners)
42
+ XCTAssertEqual(storage.takeAll().count, 1)
43
+ XCTAssertFalse(storage.hasListeners)
44
+ }
45
+
46
+ func testGet_whenFree_isNil() {
47
+ XCTAssertNil(storage.get("missing" as NSString))
48
+ XCTAssertFalse(storage.hasEventListener("missing" as NSString))
49
+ XCTAssertFalse(storage.hasListeners)
50
+ }
51
+
52
+ func testTake_removesAndDecrementsOccupancy() {
53
+ let handle = 7 as NSNumber
54
+ XCTAssertTrue(storage.putIfAbsent("k" as NSString, value: handle))
55
+ XCTAssertTrue(storage.take("k" as NSString) === handle)
56
+ XCTAssertNil(storage.get("k" as NSString))
57
+ XCTAssertFalse(storage.hasListeners)
58
+ }
59
+
60
+ func testTake_whenFree_isNoOp() {
61
+ XCTAssertNil(storage.take("missing" as NSString))
62
+ XCTAssertFalse(storage.hasListeners)
63
+ }
64
+
65
+ func testTakeAll_snapshotAndLeavesEmpty() {
66
+ XCTAssertTrue(storage.putIfAbsent("a" as NSString, value: 1 as NSNumber))
67
+ XCTAssertTrue(storage.putIfAbsent("b" as NSString, value: 2 as NSNumber))
68
+ let remaining = storage.takeAll()
69
+ XCTAssertEqual(remaining.count, 2)
70
+ XCTAssertFalse(storage.hasListeners)
71
+ XCTAssertNil(storage.get("a" as NSString))
72
+ }
73
+
74
+ func testTakeAll_empty_isNoOp() {
75
+ XCTAssertEqual(storage.takeAll().count, 0)
76
+ XCTAssertFalse(storage.hasListeners)
77
+ }
78
+
79
+ func testPut_afterTake_allowsReuse() {
80
+ XCTAssertTrue(storage.putIfAbsent("k" as NSString, value: 1 as NSNumber))
81
+ XCTAssertEqual(storage.take("k" as NSString) as? NSNumber, 1)
82
+ XCTAssertTrue(storage.putIfAbsent("k" as NSString, value: 2 as NSNumber))
83
+ XCTAssertEqual(storage.get("k" as NSString) as? NSNumber, 2)
84
+ XCTAssertTrue(storage.hasListeners)
85
+ }
86
+ }
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Copyright (c) 2016-present Invertase Limited & Contributors
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this library except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ *
16
+ */
17
+
18
+ #import <XCTest/XCTest.h>
19
+
20
+ #import "RNFBDatabaseNullSentinelDecoder.h"
21
+
22
+ @interface RNFBDatabaseNullSentinelDecodeTests : XCTestCase
23
+ @end
24
+
25
+ @implementation RNFBDatabaseNullSentinelDecodeTests
26
+
27
+ - (NSDictionary *)nullSentinel {
28
+ return @{@"__rnfbNull" : @YES};
29
+ }
30
+
31
+ - (NSDictionary *)serverTimestamp {
32
+ return @{@".sv" : @"timestamp"};
33
+ }
34
+
35
+ #pragma mark - Reference helper seams
36
+
37
+ - (void)testReferenceSet_topLevelNullSentinel_becomesNSNull {
38
+ NSDictionary *props = @{@"value" : [self nullSentinel]};
39
+ XCTAssertEqualObjects([RNFBDatabaseNullSentinelDecoder decodedValueFromProps:props],
40
+ [NSNull null]);
41
+ }
42
+
43
+ - (void)testReferenceUpdate_childNullSentinel_becomesNSNull {
44
+ NSDictionary *props = @{@"values" : @{@"a" : [self nullSentinel], @"b" : @1}};
45
+ NSDictionary *decoded = [RNFBDatabaseNullSentinelDecoder decodedValuesFromProps:props];
46
+ XCTAssertEqualObjects(decoded[@"a"], [NSNull null]);
47
+ XCTAssertEqualObjects(decoded[@"b"], @1);
48
+ }
49
+
50
+ - (void)testReferenceSetWithPriority_valueAndPrioritySentinels_becomeNSNull {
51
+ NSDictionary *props = @{
52
+ @"value" : [self nullSentinel],
53
+ @"priority" : [self nullSentinel],
54
+ };
55
+ XCTAssertEqualObjects([RNFBDatabaseNullSentinelDecoder decodedValueFromProps:props],
56
+ [NSNull null]);
57
+ XCTAssertEqualObjects([RNFBDatabaseNullSentinelDecoder decodedPriorityFromProps:props],
58
+ [NSNull null]);
59
+ }
60
+
61
+ - (void)testReferenceSetPriority_sentinel_becomesNSNull {
62
+ NSDictionary *props = @{@"priority" : [self nullSentinel]};
63
+ XCTAssertEqualObjects([RNFBDatabaseNullSentinelDecoder decodedPriorityFromProps:props],
64
+ [NSNull null]);
65
+ }
66
+
67
+ - (void)testReference_serverTimestampDictionary_unchanged {
68
+ NSDictionary *props = @{@"value" : [self serverTimestamp]};
69
+ XCTAssertEqualObjects([RNFBDatabaseNullSentinelDecoder decodedValueFromProps:props],
70
+ [self serverTimestamp]);
71
+ }
72
+
73
+ #pragma mark - OnDisconnect helper seams
74
+
75
+ - (void)testOnDisconnectSet_topLevelNullSentinel_becomesNSNull {
76
+ NSDictionary *props = @{@"value" : [self nullSentinel]};
77
+ XCTAssertEqualObjects([RNFBDatabaseNullSentinelDecoder decodedValueFromProps:props],
78
+ [NSNull null]);
79
+ }
80
+
81
+ - (void)testOnDisconnectSetWithPriority_valueAndPrioritySentinels_becomeNSNull {
82
+ NSDictionary *props = @{
83
+ @"value" : [self nullSentinel],
84
+ @"priority" : [self nullSentinel],
85
+ };
86
+ XCTAssertEqualObjects([RNFBDatabaseNullSentinelDecoder decodedValueFromProps:props],
87
+ [NSNull null]);
88
+ XCTAssertEqualObjects([RNFBDatabaseNullSentinelDecoder decodedPriorityFromProps:props],
89
+ [NSNull null]);
90
+ }
91
+
92
+ - (void)testOnDisconnectUpdate_childNullSentinel_becomesNSNull {
93
+ NSDictionary *props = @{@"values" : @{@"gone" : [self nullSentinel]}};
94
+ NSDictionary *decoded = [RNFBDatabaseNullSentinelDecoder decodedValuesFromProps:props];
95
+ XCTAssertEqualObjects(decoded[@"gone"], [NSNull null]);
96
+ }
97
+
98
+ - (void)testOnDisconnect_serverTimestampDictionary_unchanged {
99
+ NSDictionary *props = @{@"value" : [self serverTimestamp]};
100
+ XCTAssertEqualObjects([RNFBDatabaseNullSentinelDecoder decodedValueFromProps:props],
101
+ [self serverTimestamp]);
102
+ }
103
+
104
+ #pragma mark - Transaction helper seam
105
+
106
+ - (void)testTransactionTryCommit_valueSentinel_becomesNSNull {
107
+ id decoded = [RNFBDatabaseNullSentinelDecoder decodedTransactionValue:[self nullSentinel]];
108
+ XCTAssertEqualObjects(decoded, [NSNull null]);
109
+ XCTAssertFalse([decoded isKindOfClass:[NSDictionary class]]);
110
+ }
111
+
112
+ - (void)testTransactionTryCommit_serverTimestamp_unchanged {
113
+ XCTAssertEqualObjects(
114
+ [RNFBDatabaseNullSentinelDecoder decodedTransactionValue:[self serverTimestamp]],
115
+ [self serverTimestamp]);
116
+ }
117
+
118
+ @end
@@ -0,0 +1,181 @@
1
+ /**
2
+ * Copyright (c) 2016-present Invertase Limited & Contributors
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this library except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ import XCTest
18
+
19
+ private final class FakeQueryHandle: NSObject {
20
+ private(set) var removeCount = 0
21
+ var listeners = false
22
+
23
+ @objc func removeAllEventListeners() {
24
+ removeCount += 1
25
+ listeners = false
26
+ }
27
+
28
+ @objc func hasListeners() -> Bool {
29
+ listeners
30
+ }
31
+ }
32
+
33
+ final class RNFBDatabaseQueryRegistryStorageTests: XCTestCase {
34
+ private var storage: RNFBDatabaseQueryRegistryStorage!
35
+
36
+ override func setUp() {
37
+ super.setUp()
38
+ storage = RNFBDatabaseQueryRegistryStorage()
39
+ }
40
+
41
+ func testPutIfAbsent_storesWhenFree() {
42
+ let handle = FakeQueryHandle()
43
+ XCTAssertTrue(storage.putIfAbsent("q1" as NSString, value: handle))
44
+ XCTAssertTrue(storage.get("q1" as NSString) === handle)
45
+ }
46
+
47
+ func testPutIfAbsent_collision_keepsExisting() {
48
+ let first = FakeQueryHandle()
49
+ let second = FakeQueryHandle()
50
+ XCTAssertTrue(storage.putIfAbsent("q1" as NSString, value: first))
51
+ XCTAssertFalse(storage.putIfAbsent("q1" as NSString, value: second))
52
+ XCTAssertTrue(storage.get("q1" as NSString) === first)
53
+ }
54
+
55
+ func testGet_whenFree_isNil() {
56
+ XCTAssertNil(storage.get("missing" as NSString))
57
+ }
58
+
59
+ func testTake_removesWithoutCallingRemove() {
60
+ let handle = FakeQueryHandle()
61
+ XCTAssertTrue(storage.putIfAbsent("q1" as NSString, value: handle))
62
+ XCTAssertTrue(storage.take("q1" as NSString) === handle)
63
+ XCTAssertNil(storage.get("q1" as NSString))
64
+ XCTAssertEqual(handle.removeCount, 0)
65
+ }
66
+
67
+ func testTake_whenFree_isNil() {
68
+ XCTAssertNil(storage.take("missing" as NSString))
69
+ }
70
+
71
+ func testTakeIfIdle_whenNoListeners_takes() {
72
+ let handle = FakeQueryHandle()
73
+ handle.listeners = false
74
+ XCTAssertTrue(storage.putIfAbsent("q" as NSString, value: handle))
75
+ XCTAssertTrue(storage.takeIfIdle("q" as NSString) === handle)
76
+ XCTAssertNil(storage.get("q" as NSString))
77
+ XCTAssertEqual(handle.removeCount, 0)
78
+ }
79
+
80
+ func testTakeIfIdle_whenHasListeners_leavesMapping() {
81
+ let handle = FakeQueryHandle()
82
+ handle.listeners = true
83
+ XCTAssertTrue(storage.putIfAbsent("q" as NSString, value: handle))
84
+ XCTAssertNil(storage.takeIfIdle("q" as NSString))
85
+ XCTAssertTrue(storage.get("q" as NSString) === handle)
86
+ }
87
+
88
+ func testTakeIfIdle_whenMissing_isNil() {
89
+ XCTAssertNil(storage.takeIfIdle("missing" as NSString))
90
+ }
91
+
92
+ func testTakeIfIdle_objectWithoutHasListeners_leavesMapping() {
93
+ let plain = NSObject()
94
+ XCTAssertTrue(storage.putIfAbsent("p" as NSString, value: plain))
95
+ XCTAssertNil(storage.takeIfIdle("p" as NSString))
96
+ XCTAssertTrue(storage.get("p" as NSString) === plain)
97
+ }
98
+
99
+ func testTakeIfIdle_putBackWhenListenersAppearAfterTake() {
100
+ let gated = PutBackGatedQueryHandle()
101
+ gated.listeners = false
102
+ gated.onSecondHasListenersEnter = {
103
+ gated.listeners = true
104
+ }
105
+ XCTAssertTrue(storage.putIfAbsent("g" as NSString, value: gated))
106
+ XCTAssertNil(storage.takeIfIdle("g" as NSString))
107
+ XCTAssertTrue(storage.get("g" as NSString) === gated)
108
+ XCTAssertEqual(gated.callCount, 2)
109
+ XCTAssertEqual(gated.removeCount, 0)
110
+ }
111
+
112
+ func testTakeIfIdle_putBackFails_clearsOrphanListeners() {
113
+ let orphan = PutBackGatedQueryHandle()
114
+ orphan.listeners = false
115
+ XCTAssertTrue(storage.putIfAbsent("q" as NSString, value: orphan))
116
+
117
+ let replacement = FakeQueryHandle()
118
+ replacement.listeners = true
119
+ orphan.onSecondHasListenersEnter = { [weak self] in
120
+ guard let self = self else { return }
121
+ XCTAssertTrue(self.storage.putIfAbsent("q" as NSString, value: replacement))
122
+ orphan.listeners = true
123
+ }
124
+
125
+ XCTAssertNil(storage.takeIfIdle("q" as NSString))
126
+ XCTAssertTrue(storage.get("q" as NSString) === replacement)
127
+ XCTAssertEqual(orphan.removeCount, 1)
128
+ XCTAssertFalse(orphan.listeners)
129
+ XCTAssertEqual(replacement.removeCount, 0)
130
+ }
131
+
132
+ func testRemoveAll_removesSnapshotAndLeavesEmpty() {
133
+ let a = FakeQueryHandle()
134
+ let b = FakeQueryHandle()
135
+ XCTAssertTrue(storage.putIfAbsent("a" as NSString, value: a))
136
+ XCTAssertTrue(storage.putIfAbsent("b" as NSString, value: b))
137
+ storage.removeAll()
138
+ XCTAssertEqual(a.removeCount, 1)
139
+ XCTAssertEqual(b.removeCount, 1)
140
+ XCTAssertNil(storage.get("a" as NSString))
141
+ XCTAssertNil(storage.get("b" as NSString))
142
+ }
143
+
144
+ func testRemoveAll_objectWithoutRemove_doesNotCrash() {
145
+ let plain = NSObject()
146
+ XCTAssertTrue(storage.putIfAbsent("p" as NSString, value: plain))
147
+ storage.removeAll()
148
+ XCTAssertNil(storage.get("p" as NSString))
149
+ }
150
+
151
+ func testPut_afterTake_allowsReuse() {
152
+ let first = FakeQueryHandle()
153
+ let second = FakeQueryHandle()
154
+ XCTAssertTrue(storage.putIfAbsent("q1" as NSString, value: first))
155
+ XCTAssertTrue(storage.take("q1" as NSString) === first)
156
+ XCTAssertTrue(storage.putIfAbsent("q1" as NSString, value: second))
157
+ XCTAssertTrue(storage.get("q1" as NSString) === second)
158
+ }
159
+ }
160
+
161
+ /// First `hasListeners` returns idle; second returns `listeners` (settable between calls).
162
+ private final class PutBackGatedQueryHandle: NSObject {
163
+ private(set) var removeCount = 0
164
+ private(set) var callCount = 0
165
+ var listeners = false
166
+ var onSecondHasListenersEnter: (() -> Void)?
167
+
168
+ @objc func removeAllEventListeners() {
169
+ removeCount += 1
170
+ listeners = false
171
+ }
172
+
173
+ @objc func hasListeners() -> Bool {
174
+ callCount += 1
175
+ if callCount == 1 {
176
+ return false
177
+ }
178
+ onSecondHasListenersEnter?()
179
+ return listeners
180
+ }
181
+ }
@@ -12,7 +12,15 @@
12
12
  D8000000000000000000000C /* RNFBDatabaseListenerRegistryTests.m in Sources */ = {isa = PBXBuildFile; fileRef = D80000000000000000000009 /* RNFBDatabaseListenerRegistryTests.m */; };
13
13
  D80000000000000000000020 /* RNFBDatabaseQueryRegistry.m in Sources */ = {isa = PBXBuildFile; fileRef = D80000000000000000000021 /* RNFBDatabaseQueryRegistry.m */; };
14
14
  D80000000000000000000022 /* RNFBDatabaseQueryRegistryTests.m in Sources */ = {isa = PBXBuildFile; fileRef = D80000000000000000000023 /* RNFBDatabaseQueryRegistryTests.m */; };
15
+ D80000000000000000000030 /* RNFBNullSentinelDecoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = D80000000000000000000031 /* RNFBNullSentinelDecoding.swift */; };
16
+ D80000000000000000000032 /* RNFBDatabaseNullSentinelDecoder.m in Sources */ = {isa = PBXBuildFile; fileRef = D80000000000000000000033 /* RNFBDatabaseNullSentinelDecoder.m */; };
17
+ D80000000000000000000038 /* RNFBDatabaseNullSentinelDecodeTests.m in Sources */ = {isa = PBXBuildFile; fileRef = D80000000000000000000039 /* RNFBDatabaseNullSentinelDecodeTests.m */; };
15
18
  D80000000000000000000015 /* XCTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D80000000000000000000014 /* XCTest.framework */; };
19
+ D80000000000000000000025 /* RNFBHandleMapStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = D80000000000000000000026 /* RNFBHandleMapStorage.swift */; };
20
+ D80000000000000000000028 /* RNFBDatabaseListenerRegistryStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = D80000000000000000000027 /* RNFBDatabaseListenerRegistryStorage.swift */; };
21
+ D8000000000000000000002A /* RNFBDatabaseListenerRegistryStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D80000000000000000000029 /* RNFBDatabaseListenerRegistryStorageTests.swift */; };
22
+ D8000000000000000000002C /* RNFBDatabaseQueryRegistryStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8000000000000000000002B /* RNFBDatabaseQueryRegistryStorage.swift */; };
23
+ D8000000000000000000002E /* RNFBDatabaseQueryRegistryStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8000000000000000000002D /* RNFBDatabaseQueryRegistryStorageTests.swift */; };
16
24
  /* End PBXBuildFile section */
17
25
 
18
26
  /* Begin PBXFileReference section */
@@ -25,7 +33,16 @@
25
33
  D80000000000000000000024 /* RNFBDatabaseQueryRegistry.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = RNFBDatabaseQueryRegistry.h; path = ../RNFBDatabase/RNFBDatabaseQueryRegistry.h; sourceTree = SOURCE_ROOT; };
26
34
  D80000000000000000000021 /* RNFBDatabaseQueryRegistry.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = RNFBDatabaseQueryRegistry.m; path = ../RNFBDatabase/RNFBDatabaseQueryRegistry.m; sourceTree = SOURCE_ROOT; };
27
35
  D80000000000000000000023 /* RNFBDatabaseQueryRegistryTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RNFBDatabaseQueryRegistryTests.m; sourceTree = "<group>"; };
36
+ D80000000000000000000031 /* RNFBNullSentinelDecoding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = RNFBNullSentinelDecoding.swift; path = ../../../app/ios/RNFBApp/RNFBNullSentinelDecoding.swift; sourceTree = SOURCE_ROOT; };
37
+ D80000000000000000000033 /* RNFBDatabaseNullSentinelDecoder.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = RNFBDatabaseNullSentinelDecoder.m; path = ../RNFBDatabase/RNFBDatabaseNullSentinelDecoder.m; sourceTree = SOURCE_ROOT; };
38
+ D8000000000000000000003B /* RNFBDatabaseNullSentinelDecoder.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = RNFBDatabaseNullSentinelDecoder.h; path = ../RNFBDatabase/RNFBDatabaseNullSentinelDecoder.h; sourceTree = SOURCE_ROOT; };
39
+ D80000000000000000000039 /* RNFBDatabaseNullSentinelDecodeTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RNFBDatabaseNullSentinelDecodeTests.m; sourceTree = "<group>"; };
28
40
  D80000000000000000000014 /* XCTest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = XCTest.framework; path = Platforms/MacOSX.platform/Developer/Library/Frameworks/XCTest.framework; sourceTree = DEVELOPER_DIR; };
41
+ D80000000000000000000026 /* RNFBHandleMapStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = RNFBHandleMapStorage.swift; path = ../../../app/ios/RNFBApp/RNFBHandleMapStorage.swift; sourceTree = SOURCE_ROOT; };
42
+ D80000000000000000000027 /* RNFBDatabaseListenerRegistryStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = RNFBDatabaseListenerRegistryStorage.swift; path = ../RNFBDatabase/RNFBDatabaseListenerRegistryStorage.swift; sourceTree = SOURCE_ROOT; };
43
+ D80000000000000000000029 /* RNFBDatabaseListenerRegistryStorageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RNFBDatabaseListenerRegistryStorageTests.swift; sourceTree = "<group>"; };
44
+ D8000000000000000000002B /* RNFBDatabaseQueryRegistryStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = RNFBDatabaseQueryRegistryStorage.swift; path = ../RNFBDatabase/RNFBDatabaseQueryRegistryStorage.swift; sourceTree = SOURCE_ROOT; };
45
+ D8000000000000000000002D /* RNFBDatabaseQueryRegistryStorageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RNFBDatabaseQueryRegistryStorageTests.swift; sourceTree = "<group>"; };
29
46
  /* End PBXFileReference section */
30
47
 
31
48
  /* Begin PBXFrameworksBuildPhase section */
@@ -61,12 +78,21 @@
61
78
  children = (
62
79
  D80000000000000000000007 /* RNFBHandleMap.h */,
63
80
  D80000000000000000000008 /* RNFBHandleMap.m */,
81
+ D80000000000000000000026 /* RNFBHandleMapStorage.swift */,
64
82
  D80000000000000000000016 /* RNFBDatabaseListenerRegistry.h */,
65
83
  D80000000000000000000018 /* RNFBDatabaseListenerRegistry.m */,
84
+ D80000000000000000000027 /* RNFBDatabaseListenerRegistryStorage.swift */,
66
85
  D80000000000000000000024 /* RNFBDatabaseQueryRegistry.h */,
67
86
  D80000000000000000000021 /* RNFBDatabaseQueryRegistry.m */,
87
+ D80000000000000000000031 /* RNFBNullSentinelDecoding.swift */,
88
+ D8000000000000000000003B /* RNFBDatabaseNullSentinelDecoder.h */,
89
+ D80000000000000000000033 /* RNFBDatabaseNullSentinelDecoder.m */,
90
+ D8000000000000000000002B /* RNFBDatabaseQueryRegistryStorage.swift */,
68
91
  D80000000000000000000009 /* RNFBDatabaseListenerRegistryTests.m */,
92
+ D80000000000000000000029 /* RNFBDatabaseListenerRegistryStorageTests.swift */,
69
93
  D80000000000000000000023 /* RNFBDatabaseQueryRegistryTests.m */,
94
+ D80000000000000000000039 /* RNFBDatabaseNullSentinelDecodeTests.m */,
95
+ D8000000000000000000002D /* RNFBDatabaseQueryRegistryStorageTests.swift */,
70
96
  );
71
97
  name = Sources;
72
98
  sourceTree = "<group>";
@@ -129,10 +155,18 @@
129
155
  buildActionMask = 2147483647;
130
156
  files = (
131
157
  D8000000000000000000000A /* RNFBHandleMap.m in Sources */,
158
+ D80000000000000000000025 /* RNFBHandleMapStorage.swift in Sources */,
159
+ D80000000000000000000028 /* RNFBDatabaseListenerRegistryStorage.swift in Sources */,
132
160
  D8000000000000000000000B /* RNFBDatabaseListenerRegistry.m in Sources */,
161
+ D8000000000000000000002C /* RNFBDatabaseQueryRegistryStorage.swift in Sources */,
133
162
  D80000000000000000000020 /* RNFBDatabaseQueryRegistry.m in Sources */,
163
+ D80000000000000000000030 /* RNFBNullSentinelDecoding.swift in Sources */,
164
+ D80000000000000000000032 /* RNFBDatabaseNullSentinelDecoder.m in Sources */,
134
165
  D8000000000000000000000C /* RNFBDatabaseListenerRegistryTests.m in Sources */,
166
+ D8000000000000000000002A /* RNFBDatabaseListenerRegistryStorageTests.swift in Sources */,
135
167
  D80000000000000000000022 /* RNFBDatabaseQueryRegistryTests.m in Sources */,
168
+ D80000000000000000000038 /* RNFBDatabaseNullSentinelDecodeTests.m in Sources */,
169
+ D8000000000000000000002E /* RNFBDatabaseQueryRegistryStorageTests.swift in Sources */,
136
170
  );
137
171
  runOnlyForDeploymentPostprocessing = 0;
138
172
  };
@@ -181,8 +215,10 @@
181
215
  CODE_SIGNING_REQUIRED = NO;
182
216
  CODE_SIGN_IDENTITY = "-";
183
217
  COMBINE_HIDPI_IMAGES = YES;
218
+ DEFINES_MODULE = YES;
184
219
  GENERATE_INFOPLIST_FILE = YES;
185
220
  HEADER_SEARCH_PATHS = (
221
+ "$(SRCROOT)",
186
222
  "$(SRCROOT)/../../../app/ios",
187
223
  "$(SRCROOT)/../../../app/ios/RNFBApp",
188
224
  "$(SRCROOT)/../RNFBDatabase",
@@ -204,6 +240,8 @@
204
240
  PRODUCT_NAME = "$(TARGET_NAME)";
205
241
  SDKROOT = macosx;
206
242
  SUPPORTED_PLATFORMS = macosx;
243
+ SWIFT_OBJC_INTERFACE_HEADER_NAME = "RNFBHandleMapStorage-Swift.inc";
244
+ SWIFT_VERSION = 5.10;
207
245
  };
208
246
  name = Debug;
209
247
  };
@@ -215,8 +253,10 @@
215
253
  CODE_SIGNING_REQUIRED = NO;
216
254
  CODE_SIGN_IDENTITY = "-";
217
255
  COMBINE_HIDPI_IMAGES = YES;
256
+ DEFINES_MODULE = YES;
218
257
  GENERATE_INFOPLIST_FILE = YES;
219
258
  HEADER_SEARCH_PATHS = (
259
+ "$(SRCROOT)",
220
260
  "$(SRCROOT)/../../../app/ios",
221
261
  "$(SRCROOT)/../../../app/ios/RNFBApp",
222
262
  "$(SRCROOT)/../RNFBDatabase",
@@ -231,6 +271,8 @@
231
271
  PRODUCT_NAME = "$(TARGET_NAME)";
232
272
  SDKROOT = macosx;
233
273
  SUPPORTED_PLATFORMS = macosx;
274
+ SWIFT_OBJC_INTERFACE_HEADER_NAME = "RNFBHandleMapStorage-Swift.inc";
275
+ SWIFT_VERSION = 5.10;
234
276
  };
235
277
  name = Release;
236
278
  };
@@ -141,6 +141,10 @@ namespace facebook::react {
141
141
  return static_cast<ObjCTurboModule&>(turboModule).invokeObjCMethod(rt, PromiseKind, "once", @selector(once:dbURL:path:modifiers:eventType:resolve:reject:), args, count);
142
142
  }
143
143
 
144
+ static facebook::jsi::Value __hostFunction_NativeRNFBTurboDatabaseQuerySpecJSI_get(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) {
145
+ return static_cast<ObjCTurboModule&>(turboModule).invokeObjCMethod(rt, PromiseKind, "get", @selector(get:dbURL:path:modifiers:resolve:reject:), args, count);
146
+ }
147
+
144
148
  static facebook::jsi::Value __hostFunction_NativeRNFBTurboDatabaseQuerySpecJSI_on(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) {
145
149
  return static_cast<ObjCTurboModule&>(turboModule).invokeObjCMethod(rt, VoidKind, "on", @selector(on:dbURL:props:), args, count);
146
150
  }
@@ -159,6 +163,9 @@ namespace facebook::react {
159
163
  methodMap_["once"] = MethodMetadata {5, __hostFunction_NativeRNFBTurboDatabaseQuerySpecJSI_once};
160
164
 
161
165
 
166
+ methodMap_["get"] = MethodMetadata {4, __hostFunction_NativeRNFBTurboDatabaseQuerySpecJSI_get};
167
+
168
+
162
169
  methodMap_["on"] = MethodMetadata {3, __hostFunction_NativeRNFBTurboDatabaseQuerySpecJSI_on};
163
170
 
164
171
 
@@ -135,6 +135,12 @@ namespace facebook::react {
135
135
  eventType:(NSString *)eventType
136
136
  resolve:(RCTPromiseResolveBlock)resolve
137
137
  reject:(RCTPromiseRejectBlock)reject;
138
+ - (void)get:(NSString *)app
139
+ dbURL:(NSString *)dbURL
140
+ path:(NSString *)path
141
+ modifiers:(NSArray *)modifiers
142
+ resolve:(RCTPromiseResolveBlock)resolve
143
+ reject:(RCTPromiseRejectBlock)reject;
138
144
  - (void)on:(NSString *)app
139
145
  dbURL:(NSString *)dbURL
140
146
  props:(NSDictionary *)props;
@@ -170,6 +170,7 @@ public:
170
170
  protected:
171
171
  NativeRNFBTurboDatabaseQueryCxxSpec(std::shared_ptr<CallInvoker> jsInvoker) : TurboModule(std::string{NativeRNFBTurboDatabaseQueryCxxSpec::kModuleName}, jsInvoker) {
172
172
  methodMap_["once"] = MethodMetadata {.argCount = 5, .invoker = __once};
173
+ methodMap_["get"] = MethodMetadata {.argCount = 4, .invoker = __get};
173
174
  methodMap_["on"] = MethodMetadata {.argCount = 3, .invoker = __on};
174
175
  methodMap_["off"] = MethodMetadata {.argCount = 2, .invoker = __off};
175
176
  methodMap_["keepSynced"] = MethodMetadata {.argCount = 6, .invoker = __keepSynced};
@@ -188,6 +189,17 @@ private:
188
189
  count <= 4 ? throw jsi::JSError(rt, "Expected argument in position 4 to be passed") : args[4].asString(rt));
189
190
  }
190
191
 
192
+ static jsi::Value __get(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
193
+ static_assert(
194
+ bridging::getParameterCount(&T::get) == 5,
195
+ "Expected get(...) to have 5 parameters");
196
+ return bridging::callFromJs<jsi::Value>(rt, &T::get, static_cast<NativeRNFBTurboDatabaseQueryCxxSpec*>(&turboModule)->jsInvoker_, static_cast<T*>(&turboModule),
197
+ count <= 0 ? throw jsi::JSError(rt, "Expected argument in position 0 to be passed") : args[0].asString(rt),
198
+ count <= 1 ? throw jsi::JSError(rt, "Expected argument in position 1 to be passed") : args[1].asString(rt),
199
+ count <= 2 ? throw jsi::JSError(rt, "Expected argument in position 2 to be passed") : args[2].asString(rt),
200
+ count <= 3 ? throw jsi::JSError(rt, "Expected argument in position 3 to be passed") : args[3].asObject(rt).asArray(rt));
201
+ }
202
+
191
203
  static jsi::Value __on(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
192
204
  static_assert(
193
205
  bridging::getParameterCount(&T::on) == 4,
@@ -18,6 +18,7 @@
18
18
  import {
19
19
  isBoolean,
20
20
  isFunction,
21
+ isIOS,
21
22
  isNull,
22
23
  isNumber,
23
24
  isObject,
@@ -45,6 +46,20 @@ const eventTypes = [
45
46
  'child_removed',
46
47
  ] as const;
47
48
 
49
+ /** `.info/*` is client-local metadata that only the listener path serves. */
50
+ const INFO_PATH = /^\/?\.info(\/|$)/;
51
+
52
+ /**
53
+ * True when native/web get already produced an observer-shaped `database/*` code.
54
+ * Android Query.get() non-denial failures use SharedUtils.getExceptionMap → `database/unknown`;
55
+ * those still fall through to `once('value')`. Specific codes mean once already ran (or a
56
+ * denial was mapped to once's code) — do not ask the server again.
57
+ */
58
+ function isObserverShapedDatabaseError(error: unknown): boolean {
59
+ const code = (error as { code?: string } | null | undefined)?.code;
60
+ return typeof code === 'string' && code.startsWith('database/') && code !== 'database/unknown';
61
+ }
62
+
48
63
  type DatabaseReferenceConstructor = new (
49
64
  database: DatabaseInternal,
50
65
  path: string,
@@ -316,6 +331,47 @@ export default class DatabaseQuery extends ReferenceBase implements Query {
316
331
  return callback as (a: DataSnapshot | null, b?: string | null) => void;
317
332
  }
318
333
 
334
+ /**
335
+ * One-shot read backing modular `get()`: on Android and web, the SDKs' single request/response
336
+ * read (`Query.get()`, web `get()`) instead of the listen -> data -> unlisten exchange
337
+ * `once('value')` performs. On iOS the native `get` is still the single-event observer; see
338
+ * `RNFBDatabaseQueryHelper get:`.
339
+ *
340
+ * Callers keep `once('value')` semantics, with one exception noted below:
341
+ * - iOS failures pass straight through (native get already is `once`).
342
+ * - On Android / web, an error whose code is already a specific `database/*` value (anything
343
+ * other than `database/unknown`) passes through: Android maps Query.get() permission denials
344
+ * to the same code `once` gives, and when native get already used `addOnceValueEventListener`
345
+ * (persistence, logging, keepSynced) its cancellations are those codes too — do not call
346
+ * `once('value')` again. Only a plain Query.get() failure is rejected as `database/unknown`
347
+ * (SharedUtils.getExceptionMap); that path still retries as `once('value')`.
348
+ * - Queries (any orderBy / limit / range modifier) use `once('value')`: `get()` parity for
349
+ * unindexed queries is unverified.
350
+ * - `.info/*` paths use `once('value')`: the one-shot reads go to the server tree, not the
351
+ * client-local `.info` tree.
352
+ *
353
+ * The exception: the one-shot read resolves with the server's reply as it was when the read was
354
+ * sent, where `once('value')` layered this client's pending writes over it. A local write made to
355
+ * an overlapping path while a `get()` is in flight is therefore not in its result, as with the
356
+ * web SDK's `get()`.
357
+ */
358
+ _get(): Promise<DataSnapshot> {
359
+ const modifiers = this._modifiers._copy().toArray();
360
+ if (modifiers.length > 0 || INFO_PATH.test(this.path)) {
361
+ return this.once('value');
362
+ }
363
+
364
+ return this._database.native.get(this.path, modifiers).then(
365
+ result => new DatabaseDataSnapshot(this.ref, result),
366
+ error => {
367
+ if (isIOS || isObserverShapedDatabaseError(error)) {
368
+ return Promise.reject(error);
369
+ }
370
+ return this.once('value');
371
+ },
372
+ );
373
+ }
374
+
319
375
  once(
320
376
  eventType: EventType,
321
377
  successCallBack?: (a: DataSnapshot, b?: string | null) => any,
@@ -18,7 +18,7 @@
18
18
  import { isString } from '@react-native-firebase/app/dist/module/common';
19
19
  import NativeError from '@react-native-firebase/app/dist/module/internal/NativeFirebaseError';
20
20
  import SharedEventEmitter from '@react-native-firebase/app/dist/module/internal/SharedEventEmitter';
21
- import { getReactNativeModule } from '@react-native-firebase/app/dist/module/internal/nativeModule';
21
+ import { getReactNativeModule } from '@react-native-firebase/app/dist/module/internal';
22
22
  import DatabaseDataSnapshot from './DatabaseDataSnapshot';
23
23
  import type {
24
24
  DatabaseChildSnapshotResultInternal,
package/lib/index.ts CHANGED
@@ -25,9 +25,9 @@ import type { FirebaseApp } from '@react-native-firebase/app';
25
25
  import {
26
26
  FirebaseModule,
27
27
  getOrCreateModularInstance,
28
+ setReactNativeModule,
28
29
  } from '@react-native-firebase/app/dist/module/internal';
29
30
  import type { ModuleConfig } from '@react-native-firebase/app/dist/module/internal';
30
- import { setReactNativeModule } from '@react-native-firebase/app/dist/module/internal/nativeModule';
31
31
  import type { ReactNativeFirebase } from '@react-native-firebase/app';
32
32
  import type {
33
33
  DatabaseInternal,
@@ -318,7 +318,7 @@ export function setWithPriority(
318
318
  }
319
319
 
320
320
  export function get(queryRef: Query): Promise<DataSnapshot> {
321
- return (queryRef as QueryWithSubscriptionMethodsInternal).once('value');
321
+ return (queryRef as QueryWithSubscriptionMethodsInternal)._get();
322
322
  }
323
323
 
324
324
  export function off(