@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.
- package/CHANGELOG.md +27 -0
- package/RNFBDatabase.podspec +10 -2
- package/android/build.gradle +7 -0
- package/android/src/main/java/io/invertase/firebase/database/DatabaseAbortable.java +22 -0
- package/android/src/main/java/io/invertase/firebase/database/DatabaseQueryHandle.java +24 -0
- package/android/src/main/java/io/invertase/firebase/database/RNFBDatabaseKeepSyncedRegistry.java +106 -0
- package/android/src/main/java/io/invertase/firebase/database/RNFBDatabaseListenerRegistry.kt +133 -0
- package/android/src/main/java/io/invertase/firebase/database/RNFBDatabaseQueryRegistry.kt +76 -0
- package/android/src/main/java/io/invertase/firebase/database/RNFBDatabaseTransactionRegistry.kt +67 -0
- package/android/src/main/java/io/invertase/firebase/database/UniversalFirebaseDatabaseOnDisconnectModule.java +102 -9
- package/android/src/reactnative/java/io/invertase/firebase/database/NativeRNFBTurboDatabaseOnDisconnect.java +6 -11
- package/android/src/reactnative/java/io/invertase/firebase/database/NativeRNFBTurboDatabaseQuery.java +111 -24
- package/android/src/reactnative/java/io/invertase/firebase/database/NativeRNFBTurboDatabaseTransaction.java +12 -25
- package/android/src/reactnative/java/io/invertase/firebase/database/ReactNativeFirebaseDatabaseQuery.java +30 -33
- package/android/src/reactnative/java/io/invertase/firebase/database/ReactNativeFirebaseDatabaseTransactionHandler.java +3 -3
- package/android/src/reactnative/java/io/invertase/firebase/database/generated/java/com/facebook/fbreact/specs/NativeRNFBTurboDatabaseQuerySpec.java +4 -0
- package/android/src/reactnative/java/io/invertase/firebase/database/generated/jni/RNFBDatabaseTurboModules-generated.cpp +6 -0
- package/android/src/reactnative/java/io/invertase/firebase/database/generated/jni/react/renderer/components/RNFBDatabaseTurboModules/RNFBDatabaseTurboModulesJSI.h +12 -0
- package/android/src/test/java/io/invertase/firebase/database/RNFBDatabaseKeepSyncedRegistryTest.java +174 -0
- package/android/src/test/java/io/invertase/firebase/database/RNFBDatabaseListenerRegistryTest.java +191 -0
- package/android/src/test/java/io/invertase/firebase/database/RNFBDatabaseQueryRegistryTest.java +489 -0
- package/android/src/test/java/io/invertase/firebase/database/RNFBDatabaseTransactionRegistryTest.java +199 -0
- package/android/src/test/java/io/invertase/firebase/database/UniversalFirebaseDatabaseOnDisconnectCancelErrorTest.java +137 -0
- package/dist/module/DatabaseQuery.js +52 -1
- package/dist/module/DatabaseQuery.js.map +1 -1
- package/dist/module/DatabaseSyncTree.js +1 -1
- package/dist/module/DatabaseSyncTree.js.map +1 -1
- package/dist/module/index.js +1 -2
- package/dist/module/index.js.map +1 -1
- package/dist/module/modular/query.js +1 -1
- package/dist/module/modular/query.js.map +1 -1
- package/dist/module/version.js +1 -1
- package/dist/module/web/RNFBDatabaseModule.js +7 -0
- package/dist/module/web/RNFBDatabaseModule.js.map +1 -1
- package/dist/typescript/lib/DatabaseQuery.d.ts +25 -0
- package/dist/typescript/lib/DatabaseQuery.d.ts.map +1 -1
- package/dist/typescript/lib/index.d.ts +1 -1
- package/dist/typescript/lib/types/internal.d.ts +2 -0
- package/dist/typescript/lib/types/internal.d.ts.map +1 -1
- package/dist/typescript/lib/version.d.ts +1 -1
- package/dist/typescript/lib/web/RNFBDatabaseModule.d.ts.map +1 -1
- package/ios/RNFBDatabase/RNFBDatabaseListenerRegistry.h +37 -0
- package/ios/RNFBDatabase/RNFBDatabaseListenerRegistry.m +91 -0
- package/ios/RNFBDatabase/RNFBDatabaseListenerRegistryStorage.swift +81 -0
- package/ios/RNFBDatabase/RNFBDatabaseNullSentinelDecoder.h +36 -0
- package/ios/RNFBDatabase/RNFBDatabaseNullSentinelDecoder.m +48 -0
- package/ios/RNFBDatabase/RNFBDatabaseOnDisconnectHelper.m +29 -26
- package/ios/RNFBDatabase/RNFBDatabaseQuery.h +3 -2
- package/ios/RNFBDatabase/RNFBDatabaseQuery.m +14 -13
- package/ios/RNFBDatabase/RNFBDatabaseQueryHelper.h +7 -0
- package/ios/RNFBDatabase/RNFBDatabaseQueryHelper.m +44 -21
- package/ios/RNFBDatabase/RNFBDatabaseQueryModule.mm +14 -0
- package/ios/RNFBDatabase/RNFBDatabaseQueryRegistry.h +37 -0
- package/ios/RNFBDatabase/RNFBDatabaseQueryRegistry.m +79 -0
- package/ios/RNFBDatabase/RNFBDatabaseQueryRegistryStorage.swift +104 -0
- package/ios/RNFBDatabase/RNFBDatabaseReferenceHelper.m +14 -12
- package/ios/RNFBDatabase/RNFBDatabaseTransactionHelper.m +3 -1
- package/ios/RNFBDatabase.xcodeproj/project.pbxproj +12 -0
- package/ios/RNFBDatabaseUnitTests/RNFBDatabaseListenerRegistryStorageTests.swift +86 -0
- package/ios/RNFBDatabaseUnitTests/RNFBDatabaseListenerRegistryTests.m +90 -0
- package/ios/RNFBDatabaseUnitTests/RNFBDatabaseNullSentinelDecodeTests.m +118 -0
- package/ios/RNFBDatabaseUnitTests/RNFBDatabaseQueryRegistryStorageTests.swift +181 -0
- package/ios/RNFBDatabaseUnitTests/RNFBDatabaseQueryRegistryTests.m +390 -0
- package/ios/RNFBDatabaseUnitTests/RNFBDatabaseUnitTests.xcodeproj/project.pbxproj +303 -0
- package/ios/RNFBDatabaseUnitTests/RNFBDatabaseUnitTests.xcodeproj/xcshareddata/xcschemes/RNFBDatabaseUnitTests.xcscheme +69 -0
- package/ios/generated/RNFBDatabaseTurboModules/RNFBDatabaseTurboModules-generated.mm +7 -0
- package/ios/generated/RNFBDatabaseTurboModules/RNFBDatabaseTurboModules.h +6 -0
- package/ios/generated/RNFBDatabaseTurboModulesJSI.h +12 -0
- package/lib/DatabaseQuery.ts +56 -0
- package/lib/DatabaseSyncTree.ts +1 -1
- package/lib/index.ts +1 -1
- package/lib/modular/query.ts +1 -1
- package/lib/types/internal.ts +2 -0
- package/lib/version.ts +1 -1
- package/lib/web/RNFBDatabaseModule.ts +13 -0
- package/package.json +5 -4
- package/specs/NativeRNFBTurboDatabaseQuery.ts +1 -0
package/android/src/test/java/io/invertase/firebase/database/RNFBDatabaseQueryRegistryTest.java
ADDED
|
@@ -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
|
+
}
|