@react-native-firebase/database 26.3.2 → 26.4.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 (31) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/RNFBDatabase.podspec +3 -1
  3. package/android/build.gradle +2 -0
  4. package/android/src/main/java/io/invertase/firebase/database/DatabaseAbortable.java +22 -0
  5. package/android/src/main/java/io/invertase/firebase/database/DatabaseQueryHandle.java +24 -0
  6. package/android/src/main/java/io/invertase/firebase/database/RNFBDatabaseListenerRegistry.java +117 -0
  7. package/android/src/main/java/io/invertase/firebase/database/RNFBDatabaseQueryRegistry.java +78 -0
  8. package/android/src/main/java/io/invertase/firebase/database/RNFBDatabaseTransactionRegistry.java +71 -0
  9. package/android/src/reactnative/java/io/invertase/firebase/database/NativeRNFBTurboDatabaseQuery.java +18 -22
  10. package/android/src/reactnative/java/io/invertase/firebase/database/NativeRNFBTurboDatabaseTransaction.java +12 -25
  11. package/android/src/reactnative/java/io/invertase/firebase/database/ReactNativeFirebaseDatabaseQuery.java +30 -33
  12. package/android/src/reactnative/java/io/invertase/firebase/database/ReactNativeFirebaseDatabaseTransactionHandler.java +3 -3
  13. package/android/src/test/java/io/invertase/firebase/database/RNFBDatabaseListenerRegistryTest.java +160 -0
  14. package/android/src/test/java/io/invertase/firebase/database/RNFBDatabaseQueryRegistryTest.java +434 -0
  15. package/android/src/test/java/io/invertase/firebase/database/RNFBDatabaseTransactionRegistryTest.java +199 -0
  16. package/dist/module/version.js +1 -1
  17. package/dist/typescript/lib/index.d.ts +1 -1
  18. package/dist/typescript/lib/version.d.ts +1 -1
  19. package/ios/RNFBDatabase/RNFBDatabaseListenerRegistry.h +37 -0
  20. package/ios/RNFBDatabase/RNFBDatabaseListenerRegistry.m +84 -0
  21. package/ios/RNFBDatabase/RNFBDatabaseQuery.h +3 -2
  22. package/ios/RNFBDatabase/RNFBDatabaseQuery.m +14 -13
  23. package/ios/RNFBDatabase/RNFBDatabaseQueryHelper.m +24 -21
  24. package/ios/RNFBDatabase/RNFBDatabaseQueryRegistry.h +37 -0
  25. package/ios/RNFBDatabase/RNFBDatabaseQueryRegistry.m +105 -0
  26. package/ios/RNFBDatabaseUnitTests/RNFBDatabaseListenerRegistryTests.m +90 -0
  27. package/ios/RNFBDatabaseUnitTests/RNFBDatabaseQueryRegistryTests.m +390 -0
  28. package/ios/RNFBDatabaseUnitTests/RNFBDatabaseUnitTests.xcodeproj/project.pbxproj +261 -0
  29. package/ios/RNFBDatabaseUnitTests/RNFBDatabaseUnitTests.xcodeproj/xcshareddata/xcschemes/RNFBDatabaseUnitTests.xcscheme +69 -0
  30. package/lib/version.ts +1 -1
  31. package/package.json +5 -4
@@ -0,0 +1,199 @@
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 static org.junit.Assert.assertFalse;
21
+ import static org.junit.Assert.assertNull;
22
+ import static org.junit.Assert.assertSame;
23
+ import static org.junit.Assert.assertTrue;
24
+ import static org.junit.Assert.fail;
25
+
26
+ import io.invertase.firebase.common.RNFBHandleCollisionException;
27
+ import java.util.concurrent.CountDownLatch;
28
+ import java.util.concurrent.TimeUnit;
29
+ import org.junit.Test;
30
+
31
+ public class RNFBDatabaseTransactionRegistryTest {
32
+
33
+ private static final class FakeAbortable implements DatabaseAbortable {
34
+ int abortCount;
35
+
36
+ @Override
37
+ public void abort() {
38
+ abortCount++;
39
+ }
40
+ }
41
+
42
+ @Test
43
+ public void putGetTake_happyPath() throws Exception {
44
+ RNFBDatabaseTransactionRegistry registry = new RNFBDatabaseTransactionRegistry();
45
+ FakeAbortable handler = new FakeAbortable();
46
+ registry.put(1, handler);
47
+ assertSame(handler, registry.get(1));
48
+ assertSame(handler, registry.take(1));
49
+ assertNull(registry.get(1));
50
+ assertTrue(handler.abortCount == 0);
51
+ }
52
+
53
+ @Test
54
+ public void put_occupiedId_abortsIncomingAndLeavesExisting() throws Exception {
55
+ RNFBDatabaseTransactionRegistry registry = new RNFBDatabaseTransactionRegistry();
56
+ FakeAbortable first = new FakeAbortable();
57
+ FakeAbortable duplicate = new FakeAbortable();
58
+ registry.put(1, first);
59
+ try {
60
+ registry.put(1, duplicate);
61
+ fail("expected RNFBHandleCollisionException");
62
+ } catch (RNFBHandleCollisionException e) {
63
+ assertTrue(e.getMessage().contains("1"));
64
+ assertSame(first, registry.get(1));
65
+ assertTrue(first.abortCount == 0);
66
+ assertTrue(duplicate.abortCount == 1);
67
+ }
68
+ }
69
+
70
+ @Test
71
+ public void put_occupiedId_nullIncoming_doesNotAbort() throws Exception {
72
+ RNFBDatabaseTransactionRegistry registry = new RNFBDatabaseTransactionRegistry();
73
+ FakeAbortable first = new FakeAbortable();
74
+ registry.put(2, first);
75
+ try {
76
+ registry.put(2, null);
77
+ fail("expected RNFBHandleCollisionException");
78
+ } catch (RNFBHandleCollisionException e) {
79
+ assertSame(first, registry.get(2));
80
+ assertTrue(first.abortCount == 0);
81
+ }
82
+ }
83
+
84
+ @Test
85
+ public void registerReplacing_replacesLastWins() throws Exception {
86
+ RNFBDatabaseTransactionRegistry registry = new RNFBDatabaseTransactionRegistry();
87
+ FakeAbortable first = new FakeAbortable();
88
+ FakeAbortable second = new FakeAbortable();
89
+ registry.put(3, first);
90
+ registry.registerReplacing(3, second);
91
+ assertSame(second, registry.get(3));
92
+ assertTrue(first.abortCount == 0);
93
+ assertTrue(second.abortCount == 0);
94
+ }
95
+
96
+ @Test
97
+ public void registerReplacing_sameIdFromTwoThreads_lastWins() throws Exception {
98
+ RNFBDatabaseTransactionRegistry registry = new RNFBDatabaseTransactionRegistry();
99
+ FakeAbortable first = new FakeAbortable();
100
+ registry.put(5, first);
101
+
102
+ CountDownLatch start = new CountDownLatch(1);
103
+ CountDownLatch done = new CountDownLatch(2);
104
+ FakeAbortable second = new FakeAbortable();
105
+ FakeAbortable third = new FakeAbortable();
106
+
107
+ Thread t1 =
108
+ new Thread(
109
+ () -> {
110
+ try {
111
+ start.await();
112
+ registry.registerReplacing(5, second);
113
+ } catch (InterruptedException e) {
114
+ Thread.currentThread().interrupt();
115
+ } finally {
116
+ done.countDown();
117
+ }
118
+ });
119
+ Thread t2 =
120
+ new Thread(
121
+ () -> {
122
+ try {
123
+ start.await();
124
+ registry.registerReplacing(5, third);
125
+ } catch (InterruptedException e) {
126
+ Thread.currentThread().interrupt();
127
+ } finally {
128
+ done.countDown();
129
+ }
130
+ });
131
+
132
+ t1.start();
133
+ t2.start();
134
+ start.countDown();
135
+ assertTrue(done.await(5, TimeUnit.SECONDS));
136
+
137
+ DatabaseAbortable stored = registry.get(5);
138
+ assertTrue(stored == second || stored == third);
139
+ assertTrue(first.abortCount == 0);
140
+ }
141
+
142
+ @Test
143
+ public void takeAndAbort_abortsAfterTake() throws Exception {
144
+ RNFBDatabaseTransactionRegistry registry = new RNFBDatabaseTransactionRegistry();
145
+ FakeAbortable handler = new FakeAbortable();
146
+ registry.put(3, handler);
147
+ registry.takeAndAbort(3);
148
+ assertTrue(handler.abortCount == 1);
149
+ assertNull(registry.get(3));
150
+ }
151
+
152
+ @Test
153
+ public void takeAndAbort_missingKey_isNoOp() {
154
+ RNFBDatabaseTransactionRegistry registry = new RNFBDatabaseTransactionRegistry();
155
+ registry.takeAndAbort(99);
156
+ assertNull(registry.get(99));
157
+ }
158
+
159
+ @Test
160
+ public void takeAllAndAbort_abortsSnapshotAndLeavesEmpty() throws Exception {
161
+ RNFBDatabaseTransactionRegistry registry = new RNFBDatabaseTransactionRegistry();
162
+ FakeAbortable a = new FakeAbortable();
163
+ FakeAbortable b = new FakeAbortable();
164
+ registry.put(1, a);
165
+ registry.put(2, b);
166
+ registry.takeAllAndAbort();
167
+ assertTrue(a.abortCount == 1);
168
+ assertTrue(b.abortCount == 1);
169
+ assertNull(registry.get(1));
170
+ assertNull(registry.get(2));
171
+ }
172
+
173
+ @Test
174
+ public void takeAllAndAbort_empty_isNoOp() {
175
+ RNFBDatabaseTransactionRegistry registry = new RNFBDatabaseTransactionRegistry();
176
+ registry.takeAllAndAbort();
177
+ assertNull(registry.get(1));
178
+ }
179
+
180
+ @Test
181
+ public void takeAllAndAbort_nullHandler_isNoOp() throws Exception {
182
+ RNFBDatabaseTransactionRegistry registry = new RNFBDatabaseTransactionRegistry();
183
+ registry.put(4, null);
184
+ registry.takeAllAndAbort();
185
+ assertNull(registry.get(4));
186
+ }
187
+
188
+ @Test
189
+ public void put_afterTake_allowsReuse() throws Exception {
190
+ RNFBDatabaseTransactionRegistry registry = new RNFBDatabaseTransactionRegistry();
191
+ FakeAbortable first = new FakeAbortable();
192
+ FakeAbortable second = new FakeAbortable();
193
+ registry.put(1, first);
194
+ assertSame(first, registry.take(1));
195
+ registry.put(1, second);
196
+ assertSame(second, registry.get(1));
197
+ assertFalse(first.abortCount != 0);
198
+ }
199
+ }
@@ -1,5 +1,5 @@
1
1
  "use strict";
2
2
 
3
3
  // Generated by genversion.
4
- export const version = '26.3.2';
4
+ export const version = '26.4.0';
5
5
  //# sourceMappingURL=version.js.map
@@ -1,7 +1,7 @@
1
1
  import type { FirebaseApp } from '@react-native-firebase/app';
2
2
  import './types/internal';
3
3
  import type { Database, DatabaseReference, EmulatorMockTokenOptions } from './types/database';
4
- export declare const SDK_VERSION = "26.3.2";
4
+ export declare const SDK_VERSION = "26.4.0";
5
5
  export declare function getDatabase(app?: FirebaseApp, url?: string): Database;
6
6
  export declare function connectDatabaseEmulator(db: Database, host: string, port: number, options?: {
7
7
  mockUserToken?: EmulatorMockTokenOptions | string;
@@ -1,2 +1,2 @@
1
- export declare const version = "26.3.2";
1
+ export declare const version = "26.4.0";
2
2
  //# sourceMappingURL=version.d.ts.map
@@ -0,0 +1,37 @@
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 <Foundation/Foundation.h>
19
+
20
+ NS_ASSUME_NONNULL_BEGIN
21
+
22
+ /**
23
+ * Per-query listener handle map. Unique `put`; callers `take` or `takeAll` then remove the SDK
24
+ * observer outside the HandleMap lock. Values are typically NSNumber observer handles.
25
+ */
26
+ @interface RNFBDatabaseListenerRegistry : NSObject
27
+
28
+ - (BOOL)put:(id)key value:(id)value error:(NSError *_Nullable *_Nullable)error;
29
+ - (nullable id)get:(id)key;
30
+ - (nullable id)take:(id)key;
31
+ - (NSArray *)takeAll;
32
+ - (BOOL)hasEventListener:(id)key;
33
+ - (BOOL)hasListeners;
34
+
35
+ @end
36
+
37
+ NS_ASSUME_NONNULL_END
@@ -0,0 +1,84 @@
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 "RNFBDatabaseListenerRegistry.h"
19
+
20
+ #if __has_include("RNFBHandleMap.h")
21
+ #import "RNFBHandleMap.h"
22
+ #else
23
+ #import "RNFBApp/RNFBHandleMap.h"
24
+ #endif
25
+
26
+ @interface RNFBDatabaseListenerRegistry ()
27
+ @property(nonatomic, strong) RNFBHandleMap *map;
28
+ @property(nonatomic, assign) NSInteger occupancy;
29
+ @end
30
+
31
+ @implementation RNFBDatabaseListenerRegistry
32
+
33
+ - (instancetype)init {
34
+ self = [super init];
35
+ if (self) {
36
+ _map = [[RNFBHandleMap alloc] init];
37
+ _occupancy = 0;
38
+ }
39
+ return self;
40
+ }
41
+
42
+ - (BOOL)put:(id)key value:(id)value error:(NSError **)error {
43
+ @synchronized(self) {
44
+ if ([self.map put:key value:value error:error]) {
45
+ _occupancy += 1;
46
+ return YES;
47
+ }
48
+ return NO;
49
+ }
50
+ }
51
+
52
+ - (id)get:(id)key {
53
+ return [self.map get:key];
54
+ }
55
+
56
+ - (id)take:(id)key {
57
+ @synchronized(self) {
58
+ id value = [self.map take:key];
59
+ if (value != nil) {
60
+ _occupancy -= 1;
61
+ }
62
+ return value;
63
+ }
64
+ }
65
+
66
+ - (NSArray *)takeAll {
67
+ @synchronized(self) {
68
+ NSArray *remaining = [self.map takeAll];
69
+ _occupancy -= (NSInteger)remaining.count;
70
+ return remaining;
71
+ }
72
+ }
73
+
74
+ - (BOOL)hasEventListener:(id)key {
75
+ return [self.map get:key] != nil;
76
+ }
77
+
78
+ - (BOOL)hasListeners {
79
+ @synchronized(self) {
80
+ return _occupancy > 0;
81
+ }
82
+ }
83
+
84
+ @end
@@ -28,10 +28,11 @@
28
28
  @import FirebaseDatabaseInternal;
29
29
  #endif
30
30
  #import <React/RCTBridgeModule.h>
31
+ #import "RNFBDatabaseListenerRegistry.h"
31
32
 
32
33
  @interface RNFBDatabaseQuery : NSObject
33
34
  @property FIRDatabaseQuery *query;
34
- @property NSMutableDictionary *listeners;
35
+ @property(nonatomic, strong, readonly) RNFBDatabaseListenerRegistry *listeners;
35
36
 
36
37
  - (RNFBDatabaseQuery *)initWithReferenceAndModifiers:(FIRDatabaseReference *)reference
37
38
  modifiers:(NSArray *)modifiers;
@@ -40,7 +41,7 @@
40
41
 
41
42
  - (BOOL)hasListeners;
42
43
 
43
- - (void)addEventListener:(NSString *)eventRegistrationKey handle:(FIRDatabaseHandle)handle;
44
+ - (BOOL)addEventListener:(NSString *)eventRegistrationKey handle:(FIRDatabaseHandle)handle;
44
45
 
45
46
  - (void)removeEventListener:(NSString *)eventRegistrationKey;
46
47
 
@@ -17,6 +17,7 @@
17
17
  */
18
18
 
19
19
  #import "RNFBDatabaseQuery.h"
20
+ #import "RNFBDatabaseListenerRegistry.h"
20
21
 
21
22
  @implementation RNFBDatabaseQuery
22
23
 
@@ -26,7 +27,7 @@
26
27
 
27
28
  if (self) {
28
29
  _query = [self buildQueryWithModifiers:reference modifiers:modifiers];
29
- _listeners = [NSMutableDictionary dictionary];
30
+ _listeners = [[RNFBDatabaseListenerRegistry alloc] init];
30
31
  }
31
32
 
32
33
  return self;
@@ -112,32 +113,32 @@
112
113
  }
113
114
  }
114
115
 
115
- - (void)addEventListener:(NSString *)eventRegistrationKey handle:(FIRDatabaseHandle)handle {
116
- _listeners[eventRegistrationKey] = @(handle);
116
+ - (BOOL)addEventListener:(NSString *)eventRegistrationKey handle:(FIRDatabaseHandle)handle {
117
+ NSError *error = nil;
118
+ return [_listeners put:eventRegistrationKey value:@(handle) error:&error];
117
119
  }
118
120
 
119
121
  - (void)removeEventListener:(NSString *)eventRegistrationKey {
120
- FIRDatabaseHandle handle = (FIRDatabaseHandle)[_listeners[eventRegistrationKey] integerValue];
121
- if (handle) {
122
- [_query removeObserverWithHandle:handle];
123
- [_listeners removeObjectForKey:eventRegistrationKey];
122
+ NSNumber *handleNumber = [_listeners take:eventRegistrationKey];
123
+ if (handleNumber != nil) {
124
+ [_query removeObserverWithHandle:(FIRDatabaseHandle)[handleNumber integerValue]];
124
125
  }
125
126
  }
126
127
 
127
128
  - (void)removeAllEventListeners {
128
- NSArray *eventRegistrationKeys = [_listeners allKeys];
129
-
130
- for (NSString *eventRegistrationKey in eventRegistrationKeys) {
131
- [self removeEventListener:eventRegistrationKey];
129
+ NSArray *handles = [_listeners takeAll];
130
+ for (NSNumber *handleNumber in handles) {
131
+ FIRDatabaseHandle handle = (FIRDatabaseHandle)[handleNumber integerValue];
132
+ [_query removeObserverWithHandle:handle];
132
133
  }
133
134
  }
134
135
 
135
136
  - (BOOL)hasEventListener:(NSString *)eventRegistrationKey {
136
- return _listeners[eventRegistrationKey] != nil;
137
+ return [_listeners hasEventListener:eventRegistrationKey];
137
138
  }
138
139
 
139
140
  - (BOOL)hasListeners {
140
- return [[_listeners allKeys] count] > 0;
141
+ return [_listeners hasListeners];
141
142
  }
142
143
 
143
144
  @end
@@ -26,29 +26,24 @@
26
26
  #import "RNFBDatabaseCommon.h"
27
27
  #import "RNFBDatabaseQuery.h"
28
28
  #import "RNFBDatabaseQueryHelper.h"
29
+ #import "RNFBDatabaseQueryRegistry.h"
29
30
  #import "RNFBRCTEventEmitter.h"
30
31
 
31
- static __strong NSMutableDictionary *queryDictionary;
32
+ static RNFBDatabaseQueryRegistry *queryRegistry;
32
33
  static NSString *const RNFB_DATABASE_SYNC = @"database_sync_event";
33
34
 
34
35
  @implementation RNFBDatabaseQueryHelper
35
36
 
36
- + (NSMutableDictionary *)queryDictionary {
37
+ + (RNFBDatabaseQueryRegistry *)queryRegistry {
37
38
  static dispatch_once_t onceToken;
38
39
  dispatch_once(&onceToken, ^{
39
- queryDictionary = [[NSMutableDictionary alloc] init];
40
+ queryRegistry = [[RNFBDatabaseQueryRegistry alloc] init];
40
41
  });
41
- return queryDictionary;
42
+ return queryRegistry;
42
43
  }
43
44
 
44
45
  + (void)invalidate {
45
- NSMutableDictionary *queries = [self queryDictionary];
46
- NSArray *queryKeys = [queries allKeys];
47
- for (NSString *key in queryKeys) {
48
- RNFBDatabaseQuery *query = queries[key];
49
- [query removeAllEventListeners];
50
- [queries removeObjectForKey:key];
51
- }
46
+ [[self queryRegistry] removeAll];
52
47
  }
53
48
 
54
49
  + (RNFBDatabaseQuery *)getDatabaseQueryInstance:(FIRDatabaseReference *)reference
@@ -59,17 +54,23 @@ static NSString *const RNFB_DATABASE_SYNC = @"database_sync_event";
59
54
  + (RNFBDatabaseQuery *)getDatabaseQueryInstance:(NSString *)key
60
55
  reference:(FIRDatabaseReference *)reference
61
56
  modifiers:(NSArray *)modifiers {
62
- NSMutableDictionary *queries = [self queryDictionary];
63
- RNFBDatabaseQuery *cachedQuery = queries[key];
57
+ RNFBDatabaseQueryRegistry *queries = [self queryRegistry];
58
+ id cachedQuery = [queries get:key];
64
59
 
65
- if (cachedQuery != nil) {
60
+ if ([cachedQuery isKindOfClass:[RNFBDatabaseQuery class]]) {
66
61
  return cachedQuery;
67
62
  }
68
63
 
69
64
  RNFBDatabaseQuery *query = [[RNFBDatabaseQuery alloc] initWithReferenceAndModifiers:reference
70
65
  modifiers:modifiers];
71
66
 
72
- queries[key] = query;
67
+ NSError *error = nil;
68
+ if (![queries put:key value:query error:&error]) {
69
+ id winner = [queries get:key];
70
+ if ([winner isKindOfClass:[RNFBDatabaseQuery class]]) {
71
+ return winner;
72
+ }
73
+ }
73
74
  return query;
74
75
  }
75
76
 
@@ -167,7 +168,9 @@ static NSString *const RNFB_DATABASE_SYNC = @"database_sync_event";
167
168
  FIRDatabaseHandle handle = [databaseQuery.query observeEventType:firDataEventType
168
169
  andPreviousSiblingKeyWithBlock:andPreviousSiblingKeyWithBlock
169
170
  withCancelBlock:errorBlock];
170
- [databaseQuery addEventListener:eventRegistrationKey handle:handle];
171
+ if (![databaseQuery addEventListener:eventRegistrationKey handle:handle]) {
172
+ [databaseQuery.query removeObserverWithHandle:handle];
173
+ }
171
174
  }
172
175
  }
173
176
 
@@ -206,14 +209,14 @@ static NSString *const RNFB_DATABASE_SYNC = @"database_sync_event";
206
209
  }
207
210
 
208
211
  + (void)off:(NSString *)queryKey eventRegistrationKey:(NSString *)eventRegistrationKey {
209
- NSMutableDictionary *queries = [self queryDictionary];
210
- RNFBDatabaseQuery *databaseQuery = queries[queryKey];
212
+ RNFBDatabaseQueryRegistry *queries = [self queryRegistry];
213
+ id cached = [queries get:queryKey];
211
214
 
212
- if (databaseQuery != nil) {
215
+ if ([cached isKindOfClass:[RNFBDatabaseQuery class]]) {
216
+ RNFBDatabaseQuery *databaseQuery = cached;
213
217
  [databaseQuery removeEventListener:eventRegistrationKey];
214
218
 
215
- if (![databaseQuery hasListeners]) {
216
- [queries removeObjectForKey:queryKey];
219
+ if ([queries takeIfIdle:queryKey] != nil) {
217
220
  [RNFBDatabaseCommon removeReferenceByKey:queryKey];
218
221
  }
219
222
  }
@@ -0,0 +1,37 @@
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 <Foundation/Foundation.h>
19
+
20
+ NS_ASSUME_NONNULL_BEGIN
21
+
22
+ /**
23
+ * Cached query map. Unique `put`; callers `take`, `takeIfIdle`, or `removeAll` then
24
+ * `removeAllEventListeners` outside the HandleMap lock. Stored values are expected to respond to
25
+ * `removeAllEventListeners` and `hasListeners`.
26
+ */
27
+ @interface RNFBDatabaseQueryRegistry : NSObject
28
+
29
+ - (BOOL)put:(id)key value:(id)value error:(NSError *_Nullable *_Nullable)error;
30
+ - (nullable id)get:(id)key;
31
+ - (nullable id)take:(id)key;
32
+ - (nullable id)takeIfIdle:(id)key;
33
+ - (void)removeAll;
34
+
35
+ @end
36
+
37
+ NS_ASSUME_NONNULL_END
@@ -0,0 +1,105 @@
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 "RNFBDatabaseQueryRegistry.h"
19
+
20
+ #if __has_include("RNFBHandleMap.h")
21
+ #import "RNFBHandleMap.h"
22
+ #else
23
+ #import "RNFBApp/RNFBHandleMap.h"
24
+ #endif
25
+
26
+ @protocol RNFBDatabaseHasListeners
27
+ - (BOOL)hasListeners;
28
+ @end
29
+
30
+ @interface RNFBDatabaseQueryRegistry ()
31
+ @property(nonatomic, strong) RNFBHandleMap *map;
32
+ @end
33
+
34
+ @implementation RNFBDatabaseQueryRegistry
35
+
36
+ - (instancetype)init {
37
+ self = [super init];
38
+ if (self) {
39
+ _map = [[RNFBHandleMap alloc] init];
40
+ }
41
+ return self;
42
+ }
43
+
44
+ - (void)rnfb_removeAllListeners:(id)query {
45
+ if (query && [query respondsToSelector:@selector(removeAllEventListeners)]) {
46
+ #pragma clang diagnostic push
47
+ #pragma clang diagnostic ignored "-Warc-performSelector-leaks"
48
+ [query performSelector:@selector(removeAllEventListeners)];
49
+ #pragma clang diagnostic pop
50
+ }
51
+ }
52
+
53
+ - (BOOL)put:(id)key value:(id)value error:(NSError **)error {
54
+ return [self.map put:key value:value error:error];
55
+ }
56
+
57
+ - (id)get:(id)key {
58
+ return [self.map get:key];
59
+ }
60
+
61
+ - (id)take:(id)key {
62
+ return [self.map take:key];
63
+ }
64
+
65
+ - (id)takeIfIdle:(id)key {
66
+ // Read hasListeners outside the HandleMap lock (avoids nesting listener occupancy), then
67
+ // identity-take and put-back if listeners appeared.
68
+ id query = [self.map get:key];
69
+ if (query == nil) {
70
+ return nil;
71
+ }
72
+ if (![query respondsToSelector:@selector(hasListeners)]) {
73
+ return nil;
74
+ }
75
+ if ([(id<RNFBDatabaseHasListeners>)query hasListeners]) {
76
+ return nil;
77
+ }
78
+ id taken = [self.map takeIf:key
79
+ when:^BOOL(id value) {
80
+ return value == query;
81
+ }];
82
+ if (taken == nil) {
83
+ return nil;
84
+ }
85
+ if ([taken respondsToSelector:@selector(hasListeners)] &&
86
+ [(id<RNFBDatabaseHasListeners>)taken hasListeners]) {
87
+ // Prefer put-back so an active query stays registered. If a concurrent put claimed the
88
+ // slot, putIfAbsent fails and this taken query would be an orphan with listeners — clear
89
+ // them so SDK callbacks are not left attached outside the registry.
90
+ if (![self.map putIfAbsent:key value:taken]) {
91
+ [self rnfb_removeAllListeners:taken];
92
+ }
93
+ return nil;
94
+ }
95
+ return taken;
96
+ }
97
+
98
+ - (void)removeAll {
99
+ NSArray *queries = [self.map takeAll];
100
+ for (id query in queries) {
101
+ [self rnfb_removeAllListeners:query];
102
+ }
103
+ }
104
+
105
+ @end