@react-native-firebase/database 26.3.3 → 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 (77) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/RNFBDatabase.podspec +10 -2
  3. package/android/build.gradle +7 -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/RNFBDatabaseKeepSyncedRegistry.java +106 -0
  7. package/android/src/main/java/io/invertase/firebase/database/RNFBDatabaseListenerRegistry.kt +133 -0
  8. package/android/src/main/java/io/invertase/firebase/database/RNFBDatabaseQueryRegistry.kt +76 -0
  9. package/android/src/main/java/io/invertase/firebase/database/RNFBDatabaseTransactionRegistry.kt +67 -0
  10. package/android/src/main/java/io/invertase/firebase/database/UniversalFirebaseDatabaseOnDisconnectModule.java +102 -9
  11. package/android/src/reactnative/java/io/invertase/firebase/database/NativeRNFBTurboDatabaseOnDisconnect.java +6 -11
  12. package/android/src/reactnative/java/io/invertase/firebase/database/NativeRNFBTurboDatabaseQuery.java +111 -24
  13. package/android/src/reactnative/java/io/invertase/firebase/database/NativeRNFBTurboDatabaseTransaction.java +12 -25
  14. package/android/src/reactnative/java/io/invertase/firebase/database/ReactNativeFirebaseDatabaseQuery.java +30 -33
  15. package/android/src/reactnative/java/io/invertase/firebase/database/ReactNativeFirebaseDatabaseTransactionHandler.java +3 -3
  16. package/android/src/reactnative/java/io/invertase/firebase/database/generated/java/com/facebook/fbreact/specs/NativeRNFBTurboDatabaseQuerySpec.java +4 -0
  17. package/android/src/reactnative/java/io/invertase/firebase/database/generated/jni/RNFBDatabaseTurboModules-generated.cpp +6 -0
  18. package/android/src/reactnative/java/io/invertase/firebase/database/generated/jni/react/renderer/components/RNFBDatabaseTurboModules/RNFBDatabaseTurboModulesJSI.h +12 -0
  19. package/android/src/test/java/io/invertase/firebase/database/RNFBDatabaseKeepSyncedRegistryTest.java +174 -0
  20. package/android/src/test/java/io/invertase/firebase/database/RNFBDatabaseListenerRegistryTest.java +191 -0
  21. package/android/src/test/java/io/invertase/firebase/database/RNFBDatabaseQueryRegistryTest.java +489 -0
  22. package/android/src/test/java/io/invertase/firebase/database/RNFBDatabaseTransactionRegistryTest.java +199 -0
  23. package/android/src/test/java/io/invertase/firebase/database/UniversalFirebaseDatabaseOnDisconnectCancelErrorTest.java +137 -0
  24. package/dist/module/DatabaseQuery.js +52 -1
  25. package/dist/module/DatabaseQuery.js.map +1 -1
  26. package/dist/module/DatabaseSyncTree.js +1 -1
  27. package/dist/module/DatabaseSyncTree.js.map +1 -1
  28. package/dist/module/index.js +1 -2
  29. package/dist/module/index.js.map +1 -1
  30. package/dist/module/modular/query.js +1 -1
  31. package/dist/module/modular/query.js.map +1 -1
  32. package/dist/module/version.js +1 -1
  33. package/dist/module/web/RNFBDatabaseModule.js +7 -0
  34. package/dist/module/web/RNFBDatabaseModule.js.map +1 -1
  35. package/dist/typescript/lib/DatabaseQuery.d.ts +25 -0
  36. package/dist/typescript/lib/DatabaseQuery.d.ts.map +1 -1
  37. package/dist/typescript/lib/index.d.ts +1 -1
  38. package/dist/typescript/lib/types/internal.d.ts +2 -0
  39. package/dist/typescript/lib/types/internal.d.ts.map +1 -1
  40. package/dist/typescript/lib/version.d.ts +1 -1
  41. package/dist/typescript/lib/web/RNFBDatabaseModule.d.ts.map +1 -1
  42. package/ios/RNFBDatabase/RNFBDatabaseListenerRegistry.h +37 -0
  43. package/ios/RNFBDatabase/RNFBDatabaseListenerRegistry.m +91 -0
  44. package/ios/RNFBDatabase/RNFBDatabaseListenerRegistryStorage.swift +81 -0
  45. package/ios/RNFBDatabase/RNFBDatabaseNullSentinelDecoder.h +36 -0
  46. package/ios/RNFBDatabase/RNFBDatabaseNullSentinelDecoder.m +48 -0
  47. package/ios/RNFBDatabase/RNFBDatabaseOnDisconnectHelper.m +29 -26
  48. package/ios/RNFBDatabase/RNFBDatabaseQuery.h +3 -2
  49. package/ios/RNFBDatabase/RNFBDatabaseQuery.m +14 -13
  50. package/ios/RNFBDatabase/RNFBDatabaseQueryHelper.h +7 -0
  51. package/ios/RNFBDatabase/RNFBDatabaseQueryHelper.m +44 -21
  52. package/ios/RNFBDatabase/RNFBDatabaseQueryModule.mm +14 -0
  53. package/ios/RNFBDatabase/RNFBDatabaseQueryRegistry.h +37 -0
  54. package/ios/RNFBDatabase/RNFBDatabaseQueryRegistry.m +79 -0
  55. package/ios/RNFBDatabase/RNFBDatabaseQueryRegistryStorage.swift +104 -0
  56. package/ios/RNFBDatabase/RNFBDatabaseReferenceHelper.m +14 -12
  57. package/ios/RNFBDatabase/RNFBDatabaseTransactionHelper.m +3 -1
  58. package/ios/RNFBDatabase.xcodeproj/project.pbxproj +12 -0
  59. package/ios/RNFBDatabaseUnitTests/RNFBDatabaseListenerRegistryStorageTests.swift +86 -0
  60. package/ios/RNFBDatabaseUnitTests/RNFBDatabaseListenerRegistryTests.m +90 -0
  61. package/ios/RNFBDatabaseUnitTests/RNFBDatabaseNullSentinelDecodeTests.m +118 -0
  62. package/ios/RNFBDatabaseUnitTests/RNFBDatabaseQueryRegistryStorageTests.swift +181 -0
  63. package/ios/RNFBDatabaseUnitTests/RNFBDatabaseQueryRegistryTests.m +390 -0
  64. package/ios/RNFBDatabaseUnitTests/RNFBDatabaseUnitTests.xcodeproj/project.pbxproj +303 -0
  65. package/ios/RNFBDatabaseUnitTests/RNFBDatabaseUnitTests.xcodeproj/xcshareddata/xcschemes/RNFBDatabaseUnitTests.xcscheme +69 -0
  66. package/ios/generated/RNFBDatabaseTurboModules/RNFBDatabaseTurboModules-generated.mm +7 -0
  67. package/ios/generated/RNFBDatabaseTurboModules/RNFBDatabaseTurboModules.h +6 -0
  68. package/ios/generated/RNFBDatabaseTurboModulesJSI.h +12 -0
  69. package/lib/DatabaseQuery.ts +56 -0
  70. package/lib/DatabaseSyncTree.ts +1 -1
  71. package/lib/index.ts +1 -1
  72. package/lib/modular/query.ts +1 -1
  73. package/lib/types/internal.ts +2 -0
  74. package/lib/version.ts +1 -1
  75. package/lib/web/RNFBDatabaseModule.ts +13 -0
  76. package/package.json +5 -4
  77. package/specs/NativeRNFBTurboDatabaseQuery.ts +1 -0
@@ -0,0 +1,489 @@
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.assertEquals;
21
+ import static org.junit.Assert.assertFalse;
22
+ import static org.junit.Assert.assertNull;
23
+ import static org.junit.Assert.assertSame;
24
+ import static org.junit.Assert.assertTrue;
25
+ import static org.junit.Assert.fail;
26
+
27
+ import io.invertase.firebase.common.RNFBHandleCollisionException;
28
+ import java.util.concurrent.CountDownLatch;
29
+ import java.util.concurrent.TimeUnit;
30
+ import java.util.concurrent.atomic.AtomicInteger;
31
+ import org.junit.Test;
32
+
33
+ public class RNFBDatabaseQueryRegistryTest {
34
+
35
+ private static final class FakeQuery implements DatabaseQueryHandle {
36
+ int removeCount;
37
+ volatile boolean listeners;
38
+
39
+ @Override
40
+ public void removeAllEventListeners() {
41
+ removeCount++;
42
+ listeners = false;
43
+ }
44
+
45
+ @Override
46
+ public Boolean hasListeners() {
47
+ return listeners;
48
+ }
49
+ }
50
+
51
+ private static final class GatedFakeQuery implements DatabaseQueryHandle {
52
+ volatile boolean listeners;
53
+ final CountDownLatch hasListenersEntered = new CountDownLatch(1);
54
+ final CountDownLatch allowHasListenersReturn = new CountDownLatch(1);
55
+
56
+ @Override
57
+ public void removeAllEventListeners() {
58
+ listeners = false;
59
+ }
60
+
61
+ @Override
62
+ public Boolean hasListeners() {
63
+ hasListenersEntered.countDown();
64
+ try {
65
+ assertTrue(allowHasListenersReturn.await(5, TimeUnit.SECONDS));
66
+ } catch (InterruptedException e) {
67
+ Thread.currentThread().interrupt();
68
+ throw new RuntimeException(e);
69
+ }
70
+ return listeners;
71
+ }
72
+ }
73
+
74
+ private static final class PutBackGatedQuery implements DatabaseQueryHandle {
75
+ volatile boolean listeners;
76
+ int removeCount;
77
+ final CountDownLatch firstEntered = new CountDownLatch(1);
78
+ final CountDownLatch allowFirstReturn = new CountDownLatch(1);
79
+ final CountDownLatch secondEntered = new CountDownLatch(1);
80
+ final CountDownLatch allowSecondReturn = new CountDownLatch(1);
81
+ final AtomicInteger calls = new AtomicInteger();
82
+
83
+ @Override
84
+ public void removeAllEventListeners() {
85
+ removeCount++;
86
+ listeners = false;
87
+ }
88
+
89
+ @Override
90
+ public Boolean hasListeners() {
91
+ int n = calls.incrementAndGet();
92
+ if (n == 1) {
93
+ firstEntered.countDown();
94
+ try {
95
+ assertTrue(allowFirstReturn.await(5, TimeUnit.SECONDS));
96
+ } catch (InterruptedException e) {
97
+ Thread.currentThread().interrupt();
98
+ throw new RuntimeException(e);
99
+ }
100
+ return false;
101
+ }
102
+ secondEntered.countDown();
103
+ try {
104
+ assertTrue(allowSecondReturn.await(5, TimeUnit.SECONDS));
105
+ } catch (InterruptedException e) {
106
+ Thread.currentThread().interrupt();
107
+ throw new RuntimeException(e);
108
+ }
109
+ return listeners;
110
+ }
111
+ }
112
+
113
+ @Test
114
+ public void takeIfIdle_concurrentOffOnRace_retainsMappingWhenListenersAddedDuringCheck()
115
+ throws Exception {
116
+ RNFBDatabaseQueryRegistry registry = new RNFBDatabaseQueryRegistry();
117
+ GatedFakeQuery query = new GatedFakeQuery();
118
+ query.listeners = false;
119
+ registry.put("q", query);
120
+
121
+ CountDownLatch offDone = new CountDownLatch(1);
122
+ Thread offThread =
123
+ new Thread(
124
+ () -> {
125
+ try {
126
+ registry.takeIfIdle("q");
127
+ } finally {
128
+ offDone.countDown();
129
+ }
130
+ });
131
+
132
+ offThread.start();
133
+ assertTrue(query.hasListenersEntered.await(5, TimeUnit.SECONDS));
134
+ query.listeners = true;
135
+ query.allowHasListenersReturn.countDown();
136
+ assertTrue(offDone.await(5, TimeUnit.SECONDS));
137
+
138
+ assertSame(query, registry.get("q"));
139
+ }
140
+
141
+ /**
142
+ * Proves hasListeners runs outside the HandleMap lock: while hasListeners is blocked, a
143
+ * concurrent put on another key must complete (would deadlock if takeIfIdle held the map lock).
144
+ */
145
+ @Test
146
+ public void takeIfIdle_doesNotHoldMapLockDuringHasListeners() throws Exception {
147
+ RNFBDatabaseQueryRegistry registry = new RNFBDatabaseQueryRegistry();
148
+ GatedFakeQuery query = new GatedFakeQuery();
149
+ query.listeners = false;
150
+ registry.put("q", query);
151
+
152
+ CountDownLatch putDone = new CountDownLatch(1);
153
+ Thread takeThread = new Thread(() -> registry.takeIfIdle("q"));
154
+ takeThread.start();
155
+ assertTrue(query.hasListenersEntered.await(5, TimeUnit.SECONDS));
156
+
157
+ Thread putThread =
158
+ new Thread(
159
+ () -> {
160
+ try {
161
+ registry.put("other", new FakeQuery());
162
+ putDone.countDown();
163
+ } catch (RNFBHandleCollisionException e) {
164
+ throw new RuntimeException(e);
165
+ }
166
+ });
167
+ putThread.start();
168
+ assertTrue(
169
+ "HandleMap lock must not be held while hasListeners runs",
170
+ putDone.await(2, TimeUnit.SECONDS));
171
+
172
+ // Keep the mapping after unlock; this test only asserts lock nesting.
173
+ query.listeners = true;
174
+ query.allowHasListenersReturn.countDown();
175
+ takeThread.join(5000);
176
+ putThread.join(5000);
177
+ assertSame(query, registry.get("q"));
178
+ assertTrue(registry.get("other") instanceof FakeQuery);
179
+ }
180
+
181
+ /** Outside check saw idle; after identity-take, listeners appeared — put-back retains mapping. */
182
+ @Test
183
+ public void takeIfIdle_putBackWhenListenersAppearAfterOutsideIdleCheck() throws Exception {
184
+ RNFBDatabaseQueryRegistry registry = new RNFBDatabaseQueryRegistry();
185
+ PutBackGatedQuery query = new PutBackGatedQuery();
186
+ query.listeners = false;
187
+ registry.put("q", query);
188
+
189
+ CountDownLatch takeDone = new CountDownLatch(1);
190
+ Thread takeThread =
191
+ new Thread(
192
+ () -> {
193
+ try {
194
+ registry.takeIfIdle("q");
195
+ } finally {
196
+ takeDone.countDown();
197
+ }
198
+ });
199
+ takeThread.start();
200
+
201
+ assertTrue(query.firstEntered.await(5, TimeUnit.SECONDS));
202
+ query.allowFirstReturn.countDown();
203
+ assertTrue(query.secondEntered.await(5, TimeUnit.SECONDS));
204
+ query.listeners = true;
205
+ query.allowSecondReturn.countDown();
206
+ assertTrue(takeDone.await(5, TimeUnit.SECONDS));
207
+
208
+ assertSame(query, registry.get("q"));
209
+ assertEquals(2, query.calls.get());
210
+ }
211
+
212
+ /**
213
+ * Outside idle check saw the original query; before identity-take, a concurrent take+put replaced
214
+ * the mapping — takeIf predicate fails and the replacement stays registered.
215
+ */
216
+ @Test
217
+ public void takeIfIdle_identityMismatch_leavesReplacement() throws Exception {
218
+ RNFBDatabaseQueryRegistry registry = new RNFBDatabaseQueryRegistry();
219
+ GatedFakeQuery original = new GatedFakeQuery();
220
+ original.listeners = false;
221
+ registry.put("q", original);
222
+
223
+ FakeQuery replacement = new FakeQuery();
224
+ replacement.listeners = true;
225
+
226
+ CountDownLatch takeDone = new CountDownLatch(1);
227
+ Thread takeThread =
228
+ new Thread(
229
+ () -> {
230
+ try {
231
+ registry.takeIfIdle("q");
232
+ } finally {
233
+ takeDone.countDown();
234
+ }
235
+ });
236
+ takeThread.start();
237
+
238
+ assertTrue(original.hasListenersEntered.await(5, TimeUnit.SECONDS));
239
+ assertSame(original, registry.take("q"));
240
+ registry.put("q", replacement);
241
+ original.allowHasListenersReturn.countDown();
242
+ assertTrue(takeDone.await(5, TimeUnit.SECONDS));
243
+
244
+ assertSame(replacement, registry.get("q"));
245
+ assertEquals(0, replacement.removeCount);
246
+ }
247
+
248
+ /**
249
+ * After identity-take, listeners appear but a concurrent put claimed the slot — put-back fails;
250
+ * orphan must clear listeners so SDK callbacks are not left outside the registry.
251
+ */
252
+ @Test
253
+ public void takeIfIdle_putBackFails_clearsOrphanListeners() throws Exception {
254
+ RNFBDatabaseQueryRegistry registry = new RNFBDatabaseQueryRegistry();
255
+ PutBackGatedQuery orphan = new PutBackGatedQuery();
256
+ orphan.listeners = false;
257
+ registry.put("q", orphan);
258
+
259
+ FakeQuery replacement = new FakeQuery();
260
+ replacement.listeners = true;
261
+
262
+ CountDownLatch takeDone = new CountDownLatch(1);
263
+ Thread takeThread =
264
+ new Thread(
265
+ () -> {
266
+ try {
267
+ registry.takeIfIdle("q");
268
+ } finally {
269
+ takeDone.countDown();
270
+ }
271
+ });
272
+ takeThread.start();
273
+
274
+ assertTrue(orphan.firstEntered.await(5, TimeUnit.SECONDS));
275
+ orphan.allowFirstReturn.countDown();
276
+ assertTrue(orphan.secondEntered.await(5, TimeUnit.SECONDS));
277
+ registry.put("q", replacement);
278
+ orphan.listeners = true;
279
+ orphan.allowSecondReturn.countDown();
280
+ assertTrue(takeDone.await(5, TimeUnit.SECONDS));
281
+
282
+ assertSame(replacement, registry.get("q"));
283
+ assertEquals(1, orphan.removeCount);
284
+ assertFalse(orphan.listeners);
285
+ assertEquals(0, replacement.removeCount);
286
+ }
287
+
288
+ @Test
289
+ public void takeIfIdle_concurrentOffOnRace_stressRetainsWhenListenersAlreadyActive()
290
+ throws Exception {
291
+ for (int attempt = 0; attempt < 200; attempt++) {
292
+ RNFBDatabaseQueryRegistry registry = new RNFBDatabaseQueryRegistry();
293
+ FakeQuery query = new FakeQuery();
294
+ query.listeners = true;
295
+ registry.put("q", query);
296
+
297
+ CountDownLatch start = new CountDownLatch(1);
298
+ CountDownLatch done = new CountDownLatch(2);
299
+
300
+ Thread offThread =
301
+ new Thread(
302
+ () -> {
303
+ try {
304
+ start.await();
305
+ registry.takeIfIdle("q");
306
+ } catch (InterruptedException e) {
307
+ Thread.currentThread().interrupt();
308
+ } finally {
309
+ done.countDown();
310
+ }
311
+ });
312
+ Thread onThread =
313
+ new Thread(
314
+ () -> {
315
+ try {
316
+ start.await();
317
+ query.listeners = true;
318
+ } catch (InterruptedException e) {
319
+ Thread.currentThread().interrupt();
320
+ } finally {
321
+ done.countDown();
322
+ }
323
+ });
324
+
325
+ offThread.start();
326
+ onThread.start();
327
+ start.countDown();
328
+ assertTrue(done.await(5, TimeUnit.SECONDS));
329
+ assertSame(query, registry.get("q"));
330
+ }
331
+ }
332
+
333
+ @Test
334
+ public void takeIfIdle_concurrentTakeIfIdle_onIdleQuery_leavesMapEmpty() throws Exception {
335
+ RNFBDatabaseQueryRegistry registry = new RNFBDatabaseQueryRegistry();
336
+ FakeQuery query = new FakeQuery();
337
+ query.listeners = false;
338
+ registry.put("q", query);
339
+
340
+ CountDownLatch start = new CountDownLatch(1);
341
+ CountDownLatch done = new CountDownLatch(2);
342
+
343
+ Thread t1 =
344
+ new Thread(
345
+ () -> {
346
+ try {
347
+ start.await();
348
+ registry.takeIfIdle("q");
349
+ } catch (InterruptedException e) {
350
+ Thread.currentThread().interrupt();
351
+ } finally {
352
+ done.countDown();
353
+ }
354
+ });
355
+ Thread t2 =
356
+ new Thread(
357
+ () -> {
358
+ try {
359
+ start.await();
360
+ registry.takeIfIdle("q");
361
+ } catch (InterruptedException e) {
362
+ Thread.currentThread().interrupt();
363
+ } finally {
364
+ done.countDown();
365
+ }
366
+ });
367
+
368
+ t1.start();
369
+ t2.start();
370
+ start.countDown();
371
+ assertTrue(done.await(5, TimeUnit.SECONDS));
372
+
373
+ assertNull(registry.get("q"));
374
+ assertEquals(0, query.removeCount);
375
+ }
376
+
377
+ @Test
378
+ public void putGetTake_happyPath() throws Exception {
379
+ RNFBDatabaseQueryRegistry registry = new RNFBDatabaseQueryRegistry();
380
+ FakeQuery query = new FakeQuery();
381
+ registry.put("q1", query);
382
+ assertSame(query, registry.get("q1"));
383
+ assertSame(query, registry.take("q1"));
384
+ assertNull(registry.get("q1"));
385
+ assertEquals(0, query.removeCount);
386
+ }
387
+
388
+ @Test
389
+ public void put_occupiedId_throwsCollision() throws Exception {
390
+ RNFBDatabaseQueryRegistry registry = new RNFBDatabaseQueryRegistry();
391
+ FakeQuery first = new FakeQuery();
392
+ registry.put("q1", first);
393
+ try {
394
+ registry.put("q1", new FakeQuery());
395
+ fail("expected RNFBHandleCollisionException");
396
+ } catch (RNFBHandleCollisionException e) {
397
+ assertTrue(e.getMessage().contains("q1"));
398
+ assertSame(first, registry.get("q1"));
399
+ }
400
+ }
401
+
402
+ @Test
403
+ public void takeAllAndRemove_removesSnapshotAndLeavesEmpty() throws Exception {
404
+ RNFBDatabaseQueryRegistry registry = new RNFBDatabaseQueryRegistry();
405
+ FakeQuery a = new FakeQuery();
406
+ FakeQuery b = new FakeQuery();
407
+ registry.put("a", a);
408
+ registry.put("b", b);
409
+ registry.takeAllAndRemove();
410
+ assertEquals(1, a.removeCount);
411
+ assertEquals(1, b.removeCount);
412
+ assertNull(registry.get("a"));
413
+ assertNull(registry.get("b"));
414
+ }
415
+
416
+ @Test
417
+ public void takeAllAndRemove_empty_isNoOp() {
418
+ RNFBDatabaseQueryRegistry registry = new RNFBDatabaseQueryRegistry();
419
+ registry.takeAllAndRemove();
420
+ assertNull(registry.get("a"));
421
+ }
422
+
423
+ @Test
424
+ public void takeAllAndRemove_nullQuery_isNoOp() throws Exception {
425
+ RNFBDatabaseQueryRegistry registry = new RNFBDatabaseQueryRegistry();
426
+ registry.put("n", null);
427
+ registry.takeAllAndRemove();
428
+ assertNull(registry.get("n"));
429
+ }
430
+
431
+ @Test
432
+ public void put_afterTake_allowsReuse() throws Exception {
433
+ RNFBDatabaseQueryRegistry registry = new RNFBDatabaseQueryRegistry();
434
+ FakeQuery first = new FakeQuery();
435
+ FakeQuery second = new FakeQuery();
436
+ registry.put("q1", first);
437
+ assertSame(first, registry.take("q1"));
438
+ registry.put("q1", second);
439
+ assertSame(second, registry.get("q1"));
440
+ assertFalse(first.removeCount != 0);
441
+ }
442
+
443
+ @Test
444
+ public void takeIfIdle_whenNoListeners_takes() throws Exception {
445
+ RNFBDatabaseQueryRegistry registry = new RNFBDatabaseQueryRegistry();
446
+ FakeQuery query = new FakeQuery();
447
+ query.listeners = false;
448
+ registry.put("q", query);
449
+ registry.takeIfIdle("q");
450
+ assertNull(registry.get("q"));
451
+ assertEquals(0, query.removeCount);
452
+ }
453
+
454
+ @Test
455
+ public void takeIfIdle_whenHasListeners_leavesMapping() throws Exception {
456
+ RNFBDatabaseQueryRegistry registry = new RNFBDatabaseQueryRegistry();
457
+ FakeQuery query = new FakeQuery();
458
+ query.listeners = true;
459
+ registry.put("q", query);
460
+ registry.takeIfIdle("q");
461
+ assertSame(query, registry.get("q"));
462
+ }
463
+
464
+ @Test
465
+ public void takeIfIdle_whenMissing_isNoOp() {
466
+ RNFBDatabaseQueryRegistry registry = new RNFBDatabaseQueryRegistry();
467
+ registry.takeIfIdle("missing");
468
+ assertNull(registry.get("missing"));
469
+ }
470
+
471
+ @Test
472
+ public void get_nullKey_returnsNull_noNpe() {
473
+ RNFBDatabaseQueryRegistry registry = new RNFBDatabaseQueryRegistry();
474
+ assertNull(registry.get(null));
475
+ }
476
+
477
+ @Test
478
+ public void take_nullKey_returnsNull_noNpe() {
479
+ RNFBDatabaseQueryRegistry registry = new RNFBDatabaseQueryRegistry();
480
+ assertNull(registry.take(null));
481
+ }
482
+
483
+ @Test
484
+ public void takeIfIdle_nullKey_isNoOp() {
485
+ RNFBDatabaseQueryRegistry registry = new RNFBDatabaseQueryRegistry();
486
+ registry.takeIfIdle(null);
487
+ assertNull(registry.get(null));
488
+ }
489
+ }
@@ -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
+ }