@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
package/CHANGELOG.md CHANGED
@@ -3,6 +3,27 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ ## [27.0.0](https://github.com/invertase/react-native-firebase/compare/v26.4.0...v27.0.0) (2026-10-19)
7
+
8
+ ### ⚠ BREAKING CHANGES
9
+
10
+ - **app, sdk:** bump Firebase Android 35, Apple 13, and JS 13
11
+
12
+ ### Features
13
+
14
+ - **app, sdk:** bump Firebase Android 35, Apple 13, and JS 13 ([0ca03d1](https://github.com/invertase/react-native-firebase/commit/0ca03d11b0c271fb37cfbee8a0c3074a21969f0b))
15
+ - **database:** get() uses the native one-shot read on Android and web ([65cf63c](https://github.com/invertase/react-native-firebase/commit/65cf63c963b3914a4aa0e450caefae7a621d383b)), closes [firebase-ios-sdk#12168](https://github.com/invertase/firebase-ios-sdk/issues/12168)
16
+
17
+ ### Bug Fixes
18
+
19
+ - **app,database:** resolve platform native module when bundlers apply package exports ([959d91f](https://github.com/invertase/react-native-firebase/commit/959d91fc79d7155d1d9a08e1f836789e6e6d6156))
20
+ - **database, android:** settle keepSynced promise after SDK apply ([61100f2](https://github.com/invertase/react-native-firebase/commit/61100f27700eda0b5f69ec21ebd991ca97ce316d))
21
+ - **database, android:** surface onDisconnect errors; cover deny reject arms ([1a4a4d6](https://github.com/invertase/react-native-firebase/commit/1a4a4d63e5f41042d9ddba55375b6a758f9829d5))
22
+ - **database:** decode iOS null sentinels before writes ([501bec8](https://github.com/invertase/react-native-firebase/commit/501bec812fab11614cae0622a93b6d16687819dd))
23
+ - **database:** keep get() from cancelling any keepSynced at the location on Android ([befb08c](https://github.com/invertase/react-native-firebase/commit/befb08c2f225ea65031ffa71ad173a68cf411dfd)), closes [firebase-android-sdk#8433](https://github.com/invertase/firebase-android-sdk/issues/8433)
24
+ - **database:** only fall back to once() for locations kept synced ([6a7eddb](https://github.com/invertase/react-native-firebase/commit/6a7eddb837f6c003eaa6f6da8697e4b402da4861))
25
+ - **database:** skip once retry after observer-shaped get errors ([6e18ddf](https://github.com/invertase/react-native-firebase/commit/6e18ddffa2f74445b843dc0d992b41232254ccb3))
26
+
6
27
  ## [26.4.0](https://github.com/invertase/react-native-firebase/compare/v26.3.3...v26.4.0) (2026-09-05)
7
28
 
8
29
  ### Bug Fixes
@@ -29,7 +29,8 @@ Pod::Spec.new do |s|
29
29
  s.ios.deployment_target = firebase_ios_target
30
30
  s.macos.deployment_target = firebase_macos_target
31
31
  s.tvos.deployment_target = firebase_tvos_target
32
- s.source_files = 'ios/**/*.{h,m,mm,cpp}'
32
+ s.swift_version = '5.10'
33
+ s.source_files = 'ios/**/*.{h,m,mm,cpp,swift}'
33
34
  s.private_header_files = [
34
35
  'ios/RNFBDatabase/RNFBDatabaseModule.h',
35
36
  'ios/RNFBDatabase/RNFBDatabaseReferenceModule.h',
@@ -38,11 +39,16 @@ Pod::Spec.new do |s|
38
39
  'ios/RNFBDatabase/RNFBDatabaseTransactionModule.h',
39
40
  'ios/RNFBDatabase/RNFBDatabaseQueryRegistry.h',
40
41
  'ios/RNFBDatabase/RNFBDatabaseListenerRegistry.h',
42
+ 'ios/RNFBDatabase/RNFBDatabaseNullSentinelDecoder.h',
41
43
  'ios/generated/**/*.h',
42
44
  ]
43
- s.exclude_files = 'ios/generated/RCTThirdPartyComponentsProvider.*', 'ios/generated/RCTAppDependencyProvider.*', 'ios/generated/RCTModuleProviders.*', 'ios/generated/RCTModulesConformingToProtocolsProvider.*', 'ios/generated/RCTUnstableModulesRequiringMainQueueSetupProvider.*', 'ios/*UnitTests/**'
45
+ # CocoaPods FileList uses FNM_PATHNAME: `ios/*UnitTests/**` does not match nested
46
+ # paths like `ios/RNFBDatabaseUnitTests/HostStubs/**` (those would compile into the pod).
47
+ s.exclude_files = 'ios/generated/RCTThirdPartyComponentsProvider.*', 'ios/generated/RCTAppDependencyProvider.*', 'ios/generated/RCTModuleProviders.*', 'ios/generated/RCTModulesConformingToProtocolsProvider.*', 'ios/generated/RCTUnstableModulesRequiringMainQueueSetupProvider.*', 'ios/*UnitTests/**/*'
44
48
 
49
+ # DEFINES_MODULE is required for the generated RNFBDatabase-Swift.h interface.
45
50
  s.pod_target_xcconfig = {
51
+ "DEFINES_MODULE" => "YES",
46
52
  "HEADER_SEARCH_PATHS" => "\"$(PODS_TARGET_SRCROOT)/ios/generated/RNFBDatabaseTurboModules\" \"$(PODS_TARGET_SRCROOT)/ios/generated\"",
47
53
  "CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES" => "YES",
48
54
  }
@@ -13,6 +13,7 @@ buildscript {
13
13
 
14
14
  dependencies {
15
15
  classpath("com.android.tools.build:gradle:8.4.0")
16
+ classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${project.findProperty('kotlinVersion') ?: '2.4.0'}")
16
17
  }
17
18
  }
18
19
  }
@@ -21,6 +22,8 @@ plugins {
21
22
  id "io.invertase.gradle.build" version "1.5"
22
23
  }
23
24
 
25
+ apply plugin: "org.jetbrains.kotlin.android"
26
+
24
27
  def appProject
25
28
  if (findProject(':@react-native-firebase_app')) {
26
29
  appProject = project(':@react-native-firebase_app')
@@ -95,6 +98,8 @@ dependencies {
95
98
  implementation "com.google.firebase:firebase-database"
96
99
 
97
100
  testImplementation "junit:junit:4.13.2"
101
+ testImplementation "org.robolectric:robolectric:4.14.1"
102
+ testImplementation "org.mockito:mockito-core:5.14.2"
98
103
  }
99
104
 
100
105
  ReactNative.shared.applyPackageVersion()
@@ -0,0 +1,106 @@
1
+ package io.invertase.firebase.database;
2
+
3
+ /*
4
+ * Copyright (c) 2016-present Invertase Limited & Contributors
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this library except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ *
18
+ */
19
+
20
+ import java.util.ArrayList;
21
+ import java.util.HashMap;
22
+ import java.util.HashSet;
23
+ import java.util.List;
24
+ import java.util.Map;
25
+ import java.util.Set;
26
+
27
+ /**
28
+ * Keeps the native one-shot get away from the app's keepSynced, by location.
29
+ *
30
+ * <p>When Query.get() misses the cache, it turns keepSynced on and then off for the location's
31
+ * default spec. Turning it off removes every keepSynced registration at the location, whatever its
32
+ * query, without unlistening, which cancels the app's keepSynced there (firebase-android-sdk#8433).
33
+ * So a location with anything kept synced is read with once(), and a keepSynced call on a location
34
+ * waits for the native gets in flight there.
35
+ */
36
+ final class RNFBDatabaseKeepSyncedRegistry {
37
+ // Keys of the queries kept synced, by location.
38
+ private final Map<String, Set<String>> keptSynced = new HashMap<>();
39
+ // Native gets in flight, by location.
40
+ private final Map<String, Integer> nativeGets = new HashMap<>();
41
+ // keepSynced calls waiting for those gets, in call order, by location.
42
+ private final Map<String, List<Runnable>> deferred = new HashMap<>();
43
+
44
+ /**
45
+ * Records keepSynced for the query with this key at the location, and runs {@code apply} now, or
46
+ * after the location's native gets complete (call order preserved). {@code apply} is the SDK
47
+ * {@code Query.keepSynced} call and any caller completion that must wait for it (for example
48
+ * settling a TurboModule Promise). A native get only starts while nothing at the location is kept
49
+ * synced, so the SDK has no keepSynced there in the meantime.
50
+ */
51
+ synchronized void keepSynced(String location, String queryKey, boolean enabled, Runnable apply) {
52
+ Set<String> queryKeys = keptSynced.get(location);
53
+ if (enabled) {
54
+ if (queryKeys == null) {
55
+ queryKeys = new HashSet<>();
56
+ keptSynced.put(location, queryKeys);
57
+ }
58
+ queryKeys.add(queryKey);
59
+ } else if (queryKeys != null) {
60
+ queryKeys.remove(queryKey);
61
+ if (queryKeys.isEmpty()) {
62
+ keptSynced.remove(location);
63
+ }
64
+ }
65
+
66
+ if (!nativeGets.containsKey(location)) {
67
+ apply.run();
68
+ return;
69
+ }
70
+ List<Runnable> waiting = deferred.get(location);
71
+ if (waiting == null) {
72
+ waiting = new ArrayList<>();
73
+ deferred.put(location, waiting);
74
+ }
75
+ waiting.add(apply);
76
+ }
77
+
78
+ /** Counts a native get on the location, or returns false if anything there is kept synced. */
79
+ synchronized boolean beginNativeGet(String location) {
80
+ if (keptSynced.containsKey(location)) {
81
+ return false;
82
+ }
83
+ Integer inFlight = nativeGets.get(location);
84
+ nativeGets.put(location, inFlight == null ? 1 : inFlight + 1);
85
+ return true;
86
+ }
87
+
88
+ /**
89
+ * Ends a native get. After the location's last one, runs the keepSynced calls that waited for it,
90
+ * in call order.
91
+ */
92
+ synchronized void endNativeGet(String location) {
93
+ int inFlight = nativeGets.get(location) - 1;
94
+ if (inFlight > 0) {
95
+ nativeGets.put(location, inFlight);
96
+ return;
97
+ }
98
+ nativeGets.remove(location);
99
+ List<Runnable> waiting = deferred.remove(location);
100
+ if (waiting != null) {
101
+ for (Runnable apply : waiting) {
102
+ apply.run();
103
+ }
104
+ }
105
+ }
106
+ }
@@ -0,0 +1,133 @@
1
+ package io.invertase.firebase.database
2
+
3
+ /*
4
+ * Copyright (c) 2016-present Invertase Limited & Contributors
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this library except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ *
18
+ */
19
+
20
+ import io.invertase.firebase.common.RNFBHandleCollisionException
21
+ import io.invertase.firebase.common.RNFBHandleMap
22
+
23
+ /**
24
+ * Per-query value/child listener maps. Unique [putValue]/[putChild]; callers [takeValue]/
25
+ * [takeChild] or [takeAll] then remove the SDK listener outside the HandleMap lock. Null values are
26
+ * not stored so [hasListeners] can use occupancy.
27
+ *
28
+ * Null-key note: [getValue]/[getChild]/[takeValue]/[takeChild]/[hasEventListener] accept
29
+ * `String?` and return null/false for a null key (Java `HashMap` interop). TurboModule/`ReadableMap`
30
+ * paths can pass a null `eventRegistrationKey` into remove/`hasEventListener`. Put keys stay
31
+ * non-null `String` (null-key put from Java NPEs via Kotlin null-check; swallowed at add call sites).
32
+ */
33
+ internal class RNFBDatabaseListenerRegistry {
34
+ private val occupancyLock = Any()
35
+ private val valueListeners = RNFBHandleMap<String, Any>()
36
+ private val childListeners = RNFBHandleMap<String, Any>()
37
+ private var occupancy = 0
38
+
39
+ @Throws(RNFBHandleCollisionException::class)
40
+ fun putValue(
41
+ eventRegistrationKey: String,
42
+ listener: Any?,
43
+ ) {
44
+ if (listener == null) {
45
+ throw NullPointerException("listener")
46
+ }
47
+ synchronized(occupancyLock) {
48
+ valueListeners.put(eventRegistrationKey, listener)
49
+ occupancy++
50
+ }
51
+ }
52
+
53
+ @Throws(RNFBHandleCollisionException::class)
54
+ fun putChild(
55
+ eventRegistrationKey: String,
56
+ listener: Any?,
57
+ ) {
58
+ if (listener == null) {
59
+ throw NullPointerException("listener")
60
+ }
61
+ synchronized(occupancyLock) {
62
+ childListeners.put(eventRegistrationKey, listener)
63
+ occupancy++
64
+ }
65
+ }
66
+
67
+ fun getValue(eventRegistrationKey: String?): Any? = if (eventRegistrationKey == null) null else valueListeners.get(eventRegistrationKey)
68
+
69
+ fun getChild(eventRegistrationKey: String?): Any? = if (eventRegistrationKey == null) null else childListeners.get(eventRegistrationKey)
70
+
71
+ fun takeValue(eventRegistrationKey: String?): Any? {
72
+ synchronized(occupancyLock) {
73
+ if (eventRegistrationKey == null) {
74
+ return null
75
+ }
76
+ val listener = valueListeners.take(eventRegistrationKey)
77
+ if (listener != null) {
78
+ occupancy--
79
+ }
80
+ return listener
81
+ }
82
+ }
83
+
84
+ fun takeChild(eventRegistrationKey: String?): Any? {
85
+ synchronized(occupancyLock) {
86
+ if (eventRegistrationKey == null) {
87
+ return null
88
+ }
89
+ val listener = childListeners.take(eventRegistrationKey)
90
+ if (listener != null) {
91
+ occupancy--
92
+ }
93
+ return listener
94
+ }
95
+ }
96
+
97
+ fun takeAllValues(): List<Any> {
98
+ synchronized(occupancyLock) {
99
+ val remaining = valueListeners.takeAll()
100
+ occupancy -= remaining.size
101
+ return remaining
102
+ }
103
+ }
104
+
105
+ fun takeAllChildren(): List<Any> {
106
+ synchronized(occupancyLock) {
107
+ val remaining = childListeners.takeAll()
108
+ occupancy -= remaining.size
109
+ return remaining
110
+ }
111
+ }
112
+
113
+ fun takeAll(): List<Any> {
114
+ val remaining = ArrayList<Any>()
115
+ remaining.addAll(takeAllValues())
116
+ remaining.addAll(takeAllChildren())
117
+ return remaining
118
+ }
119
+
120
+ fun hasEventListener(eventRegistrationKey: String?): Boolean {
121
+ if (eventRegistrationKey == null) {
122
+ return false
123
+ }
124
+ return valueListeners.get(eventRegistrationKey) != null ||
125
+ childListeners.get(eventRegistrationKey) != null
126
+ }
127
+
128
+ fun hasListeners(): Boolean {
129
+ synchronized(occupancyLock) {
130
+ return occupancy > 0
131
+ }
132
+ }
133
+ }
@@ -0,0 +1,76 @@
1
+ package io.invertase.firebase.database
2
+
3
+ /*
4
+ * Copyright (c) 2016-present Invertase Limited & Contributors
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this library except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ *
18
+ */
19
+
20
+ import io.invertase.firebase.common.RNFBHandleCollisionException
21
+ import io.invertase.firebase.common.RNFBHandleMap
22
+
23
+ /**
24
+ * Cached query map. Unique [put]; callers [take], [takeIfIdle], or [takeAllAndRemove] then
25
+ * `removeAllEventListeners()` outside the HandleMap lock.
26
+ *
27
+ * Null-key note: [get]/[take]/[takeIfIdle] accept `String?` and return null / no-op for a null key
28
+ * (Java `HashMap` interop). TurboModule `off(queryKey, …)` can pass a null `queryKey`. Put keys stay
29
+ * non-null `String`.
30
+ */
31
+ internal class RNFBDatabaseQueryRegistry {
32
+ private val map = RNFBHandleMap<String, DatabaseQueryHandle?>()
33
+
34
+ @Throws(RNFBHandleCollisionException::class)
35
+ fun put(
36
+ queryKey: String,
37
+ query: DatabaseQueryHandle?,
38
+ ) {
39
+ map.put(queryKey, query)
40
+ }
41
+
42
+ fun get(queryKey: String?): DatabaseQueryHandle? = if (queryKey == null) null else map.get(queryKey)
43
+
44
+ fun take(queryKey: String?): DatabaseQueryHandle? = if (queryKey == null) null else map.take(queryKey)
45
+
46
+ /**
47
+ * Removes the mapping when the query has no listeners. Reads [DatabaseQueryHandle.hasListeners]
48
+ * outside the HandleMap lock (avoids nesting `occupancyLock`), then identity-takes and put-backs if
49
+ * listeners appeared.
50
+ */
51
+ fun takeIfIdle(queryKey: String?) {
52
+ if (queryKey == null) {
53
+ return
54
+ }
55
+ val query = map.get(queryKey) ?: return
56
+ if (query.hasListeners() == true) {
57
+ return
58
+ }
59
+ val taken = map.takeIf(queryKey) { q -> q === query } ?: return
60
+ if (taken.hasListeners() == true) {
61
+ // Prefer put-back so an active query stays registered. If a concurrent put claimed the
62
+ // slot, putIfAbsent fails and this taken query would be an orphan with listeners — clear
63
+ // them so SDK callbacks are not left attached outside the registry.
64
+ if (!map.putIfAbsent(queryKey, taken)) {
65
+ taken.removeAllEventListeners()
66
+ }
67
+ }
68
+ }
69
+
70
+ fun takeAllAndRemove() {
71
+ val remaining = map.takeAll()
72
+ for (query in remaining) {
73
+ query?.removeAllEventListeners()
74
+ }
75
+ }
76
+ }
@@ -0,0 +1,67 @@
1
+ package io.invertase.firebase.database
2
+
3
+ /*
4
+ * Copyright (c) 2016-present Invertase Limited & Contributors
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this library except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ *
18
+ */
19
+
20
+ import io.invertase.firebase.common.RNFBHandleCollisionException
21
+ import io.invertase.firebase.common.RNFBHandleMap
22
+
23
+ /**
24
+ * Transaction handler map. Unique [put] (occupied id aborts the incoming handler, then throws);
25
+ * callers [take] or [takeAllAndAbort] then `abort()` outside the HandleMap lock. Replace is
26
+ * [registerReplacing] (atomic last wins).
27
+ */
28
+ internal class RNFBDatabaseTransactionRegistry {
29
+ private val map = RNFBHandleMap<Int, DatabaseAbortable?>()
30
+
31
+ @Throws(RNFBHandleCollisionException::class)
32
+ fun put(
33
+ transactionId: Int,
34
+ handler: DatabaseAbortable?,
35
+ ) {
36
+ try {
37
+ map.put(transactionId, handler)
38
+ } catch (collision: RNFBHandleCollisionException) {
39
+ handler?.abort()
40
+ throw collision
41
+ }
42
+ }
43
+
44
+ /** Atomic replace (last wins). The replaced handler is not aborted (Firebase retry). */
45
+ fun registerReplacing(
46
+ transactionId: Int,
47
+ handler: DatabaseAbortable?,
48
+ ) {
49
+ map.putReplacing(transactionId, handler)
50
+ }
51
+
52
+ fun get(transactionId: Int): DatabaseAbortable? = map.get(transactionId)
53
+
54
+ fun take(transactionId: Int): DatabaseAbortable? = map.take(transactionId)
55
+
56
+ fun takeAndAbort(transactionId: Int) {
57
+ val handler = map.take(transactionId)
58
+ handler?.abort()
59
+ }
60
+
61
+ fun takeAllAndAbort() {
62
+ val remaining = map.takeAll()
63
+ for (handler in remaining) {
64
+ handler?.abort()
65
+ }
66
+ }
67
+ }
@@ -21,6 +21,7 @@ import static io.invertase.firebase.database.UniversalFirebaseDatabaseCommon.get
21
21
 
22
22
  import android.content.Context;
23
23
  import com.google.android.gms.tasks.Task;
24
+ import com.google.android.gms.tasks.TaskCompletionSource;
24
25
  import com.google.firebase.database.OnDisconnect;
25
26
  import io.invertase.firebase.common.UniversalFirebaseModule;
26
27
  import java.util.Map;
@@ -32,33 +33,125 @@ public class UniversalFirebaseDatabaseOnDisconnectModule extends UniversalFireba
32
33
  }
33
34
 
34
35
  Task<Void> onDisconnectCancel(String appName, String dbURL, String path) {
35
- return getDatabaseForApp(appName, dbURL).getReference(path).onDisconnect().cancel();
36
+ TaskCompletionSource<Void> taskCompletionSource = new TaskCompletionSource<>();
37
+ OnDisconnect onDisconnect = getDatabaseForApp(appName, dbURL).getReference(path).onDisconnect();
38
+
39
+ onDisconnect.cancel(
40
+ (databaseError, databaseReference) -> {
41
+ if (databaseError != null) {
42
+ taskCompletionSource.setException(
43
+ new UniversalDatabaseException(
44
+ databaseError.getCode(),
45
+ databaseError.getMessage(),
46
+ databaseError.toException()));
47
+ } else {
48
+ taskCompletionSource.setResult(null);
49
+ }
50
+ });
51
+
52
+ return taskCompletionSource.getTask();
36
53
  }
37
54
 
38
55
  Task<Void> onDisconnectRemove(String appName, String dbURL, String path) {
39
- return getDatabaseForApp(appName, dbURL).getReference(path).onDisconnect().removeValue();
56
+ TaskCompletionSource<Void> taskCompletionSource = new TaskCompletionSource<>();
57
+ OnDisconnect onDisconnect = getDatabaseForApp(appName, dbURL).getReference(path).onDisconnect();
58
+
59
+ onDisconnect.removeValue(
60
+ (databaseError, databaseReference) -> {
61
+ if (databaseError != null) {
62
+ taskCompletionSource.setException(
63
+ new UniversalDatabaseException(
64
+ databaseError.getCode(),
65
+ databaseError.getMessage(),
66
+ databaseError.toException()));
67
+ } else {
68
+ taskCompletionSource.setResult(null);
69
+ }
70
+ });
71
+
72
+ return taskCompletionSource.getTask();
40
73
  }
41
74
 
42
75
  Task<Void> onDisconnectSet(String appName, String dbURL, String path, Object value) {
43
- return getDatabaseForApp(appName, dbURL).getReference(path).onDisconnect().setValue(value);
76
+ TaskCompletionSource<Void> taskCompletionSource = new TaskCompletionSource<>();
77
+ OnDisconnect onDisconnect = getDatabaseForApp(appName, dbURL).getReference(path).onDisconnect();
78
+
79
+ onDisconnect.setValue(
80
+ value,
81
+ (databaseError, databaseReference) -> {
82
+ if (databaseError != null) {
83
+ taskCompletionSource.setException(
84
+ new UniversalDatabaseException(
85
+ databaseError.getCode(),
86
+ databaseError.getMessage(),
87
+ databaseError.toException()));
88
+ } else {
89
+ taskCompletionSource.setResult(null);
90
+ }
91
+ });
92
+
93
+ return taskCompletionSource.getTask();
44
94
  }
45
95
 
46
96
  Task<Void> onDisconnectSetWithPriority(
47
97
  String appName, String dbURL, String path, Object value, Object priority) {
98
+ TaskCompletionSource<Void> taskCompletionSource = new TaskCompletionSource<>();
48
99
  OnDisconnect onDisconnect = getDatabaseForApp(appName, dbURL).getReference(path).onDisconnect();
49
100
 
50
101
  if (priority instanceof String) {
51
- return onDisconnect.setValue(value, (String) priority);
102
+ onDisconnect.setValue(
103
+ value,
104
+ (String) priority,
105
+ (databaseError, databaseReference) -> {
106
+ if (databaseError != null) {
107
+ taskCompletionSource.setException(
108
+ new UniversalDatabaseException(
109
+ databaseError.getCode(),
110
+ databaseError.getMessage(),
111
+ databaseError.toException()));
112
+ } else {
113
+ taskCompletionSource.setResult(null);
114
+ }
115
+ });
52
116
  } else {
53
- return onDisconnect.setValue(value, (double) priority);
117
+ onDisconnect.setValue(
118
+ value,
119
+ (double) priority,
120
+ (databaseError, databaseReference) -> {
121
+ if (databaseError != null) {
122
+ taskCompletionSource.setException(
123
+ new UniversalDatabaseException(
124
+ databaseError.getCode(),
125
+ databaseError.getMessage(),
126
+ databaseError.toException()));
127
+ } else {
128
+ taskCompletionSource.setResult(null);
129
+ }
130
+ });
54
131
  }
132
+
133
+ return taskCompletionSource.getTask();
55
134
  }
56
135
 
57
136
  Task<Void> onDisconnectUpdate(
58
137
  String appName, String dbURL, String path, Map<String, Object> values) {
59
- return getDatabaseForApp(appName, dbURL)
60
- .getReference(path)
61
- .onDisconnect()
62
- .updateChildren(values);
138
+ TaskCompletionSource<Void> taskCompletionSource = new TaskCompletionSource<>();
139
+ OnDisconnect onDisconnect = getDatabaseForApp(appName, dbURL).getReference(path).onDisconnect();
140
+
141
+ onDisconnect.updateChildren(
142
+ values,
143
+ (databaseError, databaseReference) -> {
144
+ if (databaseError != null) {
145
+ taskCompletionSource.setException(
146
+ new UniversalDatabaseException(
147
+ databaseError.getCode(),
148
+ databaseError.getMessage(),
149
+ databaseError.toException()));
150
+ } else {
151
+ taskCompletionSource.setResult(null);
152
+ }
153
+ });
154
+
155
+ return taskCompletionSource.getTask();
63
156
  }
64
157
  }
@@ -18,12 +18,12 @@ package io.invertase.firebase.database;
18
18
  */
19
19
 
20
20
  import static io.invertase.firebase.common.RCTConvertFirebase.toHashMap;
21
+ import static io.invertase.firebase.database.ReactNativeFirebaseDatabaseCommon.rejectPromiseDatabaseException;
21
22
 
22
23
  import com.facebook.fbreact.specs.NativeRNFBTurboDatabaseOnDisconnectSpec;
23
24
  import com.facebook.react.bridge.Promise;
24
25
  import com.facebook.react.bridge.ReactApplicationContext;
25
26
  import com.facebook.react.bridge.ReadableMap;
26
- import io.invertase.firebase.common.ReactNativeFirebaseModule;
27
27
  import java.util.Map;
28
28
 
29
29
  public class NativeRNFBTurboDatabaseOnDisconnect extends NativeRNFBTurboDatabaseOnDisconnectSpec {
@@ -44,8 +44,7 @@ public class NativeRNFBTurboDatabaseOnDisconnect extends NativeRNFBTurboDatabase
44
44
  if (task.isSuccessful()) {
45
45
  promise.resolve(task.getResult());
46
46
  } else {
47
- ReactNativeFirebaseModule.rejectPromiseWithExceptionMap(
48
- promise, task.getException());
47
+ rejectPromiseDatabaseException(promise, task.getException());
49
48
  }
50
49
  });
51
50
  }
@@ -59,8 +58,7 @@ public class NativeRNFBTurboDatabaseOnDisconnect extends NativeRNFBTurboDatabase
59
58
  if (task.isSuccessful()) {
60
59
  promise.resolve(task.getResult());
61
60
  } else {
62
- ReactNativeFirebaseModule.rejectPromiseWithExceptionMap(
63
- promise, task.getException());
61
+ rejectPromiseDatabaseException(promise, task.getException());
64
62
  }
65
63
  });
66
64
  }
@@ -75,8 +73,7 @@ public class NativeRNFBTurboDatabaseOnDisconnect extends NativeRNFBTurboDatabase
75
73
  if (task.isSuccessful()) {
76
74
  promise.resolve(task.getResult());
77
75
  } else {
78
- ReactNativeFirebaseModule.rejectPromiseWithExceptionMap(
79
- promise, task.getException());
76
+ rejectPromiseDatabaseException(promise, task.getException());
80
77
  }
81
78
  });
82
79
  }
@@ -92,8 +89,7 @@ public class NativeRNFBTurboDatabaseOnDisconnect extends NativeRNFBTurboDatabase
92
89
  if (task.isSuccessful()) {
93
90
  promise.resolve(task.getResult());
94
91
  } else {
95
- ReactNativeFirebaseModule.rejectPromiseWithExceptionMap(
96
- promise, task.getException());
92
+ rejectPromiseDatabaseException(promise, task.getException());
97
93
  }
98
94
  });
99
95
  }
@@ -110,8 +106,7 @@ public class NativeRNFBTurboDatabaseOnDisconnect extends NativeRNFBTurboDatabase
110
106
  if (task.isSuccessful()) {
111
107
  promise.resolve(task.getResult());
112
108
  } else {
113
- ReactNativeFirebaseModule.rejectPromiseWithExceptionMap(
114
- promise, task.getException());
109
+ rejectPromiseDatabaseException(promise, task.getException());
115
110
  }
116
111
  });
117
112
  }