@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.
- package/CHANGELOG.md +21 -0
- package/RNFBDatabase.podspec +8 -2
- package/android/build.gradle +5 -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 +93 -2
- 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 +31 -0
- package/android/src/test/java/io/invertase/firebase/database/RNFBDatabaseQueryRegistryTest.java +55 -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.m +24 -17
- 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/RNFBDatabaseQueryHelper.h +7 -0
- package/ios/RNFBDatabase/RNFBDatabaseQueryHelper.m +20 -0
- package/ios/RNFBDatabase/RNFBDatabaseQueryModule.mm +14 -0
- package/ios/RNFBDatabase/RNFBDatabaseQueryRegistry.m +25 -51
- 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/RNFBDatabaseNullSentinelDecodeTests.m +118 -0
- package/ios/RNFBDatabaseUnitTests/RNFBDatabaseQueryRegistryStorageTests.swift +181 -0
- package/ios/RNFBDatabaseUnitTests/RNFBDatabaseUnitTests.xcodeproj/project.pbxproj +42 -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 +4 -4
- package/specs/NativeRNFBTurboDatabaseQuery.ts +1 -0
- package/android/src/main/java/io/invertase/firebase/database/RNFBDatabaseListenerRegistry.java +0 -117
- package/android/src/main/java/io/invertase/firebase/database/RNFBDatabaseQueryRegistry.java +0 -78
- package/android/src/main/java/io/invertase/firebase/database/RNFBDatabaseTransactionRegistry.java +0 -71
|
@@ -29,6 +29,7 @@ import com.google.firebase.database.*;
|
|
|
29
29
|
import io.invertase.firebase.common.RNFBHandleCollisionException;
|
|
30
30
|
import io.invertase.firebase.common.ReactNativeFirebaseEventEmitter;
|
|
31
31
|
import io.invertase.firebase.common.ReactNativeFirebaseModule;
|
|
32
|
+
import io.invertase.firebase.common.UniversalFirebasePreferences;
|
|
32
33
|
import java.util.Objects;
|
|
33
34
|
import javax.annotation.Nonnull;
|
|
34
35
|
import javax.annotation.Nullable;
|
|
@@ -374,6 +375,88 @@ public class NativeRNFBTurboDatabaseQuery extends NativeRNFBTurboDatabaseQuerySp
|
|
|
374
375
|
}
|
|
375
376
|
}
|
|
376
377
|
|
|
378
|
+
// Static because the SDK keeps keepSynced registrations across a JS reload.
|
|
379
|
+
private static final RNFBDatabaseKeepSyncedRegistry keepSyncedRegistry =
|
|
380
|
+
new RNFBDatabaseKeepSyncedRegistry();
|
|
381
|
+
|
|
382
|
+
// reference.toString() is the full URL, so a null dbURL and the default URL give the same key.
|
|
383
|
+
private static String locationKey(String app, DatabaseReference reference) {
|
|
384
|
+
return app + "|" + reference;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
private static final java.util.regex.Pattern PERMISSION_DENIED_REASON =
|
|
388
|
+
java.util.regex.Pattern.compile(
|
|
389
|
+
"permission.?denied", java.util.regex.Pattern.CASE_INSENSITIVE);
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Single request/response read backing modular get(). Offline, Query.get() waits for the
|
|
393
|
+
* connection, like once(). It uses once() instead when:
|
|
394
|
+
*
|
|
395
|
+
* <ul>
|
|
396
|
+
* <li>disk persistence is on: Query.get() then resolves from disk after 3s, even online;
|
|
397
|
+
* <li>RTDB debug logging is on: a get in flight when the socket drops is resent on reconnect
|
|
398
|
+
* only because PersistentConnectionImpl.sendGet's early return sits inside {@code if
|
|
399
|
+
* (logger.logsDebug())};
|
|
400
|
+
* <li>the app keeps anything at this location synced (see RNFBDatabaseKeepSyncedRegistry).
|
|
401
|
+
* </ul>
|
|
402
|
+
*/
|
|
403
|
+
@Override
|
|
404
|
+
public void get(String app, String dbURL, String path, ReadableArray modifiers, Promise promise) {
|
|
405
|
+
DatabaseReference reference = getDatabaseForApp(app, dbURL).getReference(path);
|
|
406
|
+
ReactNativeFirebaseDatabaseQuery databaseQuery = getDatabaseQueryInstance(reference, modifiers);
|
|
407
|
+
|
|
408
|
+
UniversalFirebasePreferences preferences = UniversalFirebasePreferences.getSharedInstance();
|
|
409
|
+
String location = locationKey(app, reference);
|
|
410
|
+
if (modifiers.size() > 0
|
|
411
|
+
|| preferences.getBooleanValue(UniversalDatabaseStatics.DATABASE_PERSISTENCE_ENABLED, false)
|
|
412
|
+
|| preferences.getBooleanValue(UniversalDatabaseStatics.DATABASE_LOGGING_ENABLED, false)
|
|
413
|
+
|| !keepSyncedRegistry.beginNativeGet(location)) {
|
|
414
|
+
addOnceValueEventListener(databaseQuery, promise);
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
databaseQuery
|
|
419
|
+
.query
|
|
420
|
+
.get()
|
|
421
|
+
.addOnCompleteListener(
|
|
422
|
+
getTask -> {
|
|
423
|
+
keepSyncedRegistry.endNativeGet(location);
|
|
424
|
+
if (!getTask.isSuccessful()) {
|
|
425
|
+
Exception exception = getTask.getException();
|
|
426
|
+
// Query.get() surfaces a server error as a plain Exception carrying only the
|
|
427
|
+
// reply's reason text. Map a denial to the error once() gives, so callers see the
|
|
428
|
+
// same code and DatabaseQuery._get need not re-ask the server.
|
|
429
|
+
if (exception != null
|
|
430
|
+
&& exception.getMessage() != null
|
|
431
|
+
&& PERMISSION_DENIED_REASON.matcher(exception.getMessage()).find()) {
|
|
432
|
+
rejectPromiseDatabaseException(
|
|
433
|
+
promise,
|
|
434
|
+
new UniversalDatabaseException(
|
|
435
|
+
DatabaseError.PERMISSION_DENIED, exception.getMessage(), exception));
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
ReactNativeFirebaseModule.rejectPromiseWithExceptionMap(promise, exception);
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
DataSnapshot dataSnapshot = getTask.getResult();
|
|
442
|
+
// Serialise off the main thread, as addOnceValueEventListener does.
|
|
443
|
+
try {
|
|
444
|
+
Tasks.call(turboSupport.getExecutor(), () -> snapshotToMap(dataSnapshot))
|
|
445
|
+
.addOnCompleteListener(
|
|
446
|
+
task -> {
|
|
447
|
+
if (task.isSuccessful()) {
|
|
448
|
+
promise.resolve(task.getResult());
|
|
449
|
+
} else {
|
|
450
|
+
ReactNativeFirebaseModule.rejectPromiseWithExceptionMap(
|
|
451
|
+
promise, task.getException());
|
|
452
|
+
}
|
|
453
|
+
});
|
|
454
|
+
} catch (java.util.concurrent.RejectedExecutionException e) {
|
|
455
|
+
ReactNativeFirebaseModule.rejectPromiseWithExceptionMap(promise, e);
|
|
456
|
+
}
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
|
|
377
460
|
@Override
|
|
378
461
|
public void on(String app, String dbURL, ReadableMap props) {
|
|
379
462
|
String key = props.getString("key");
|
|
@@ -414,7 +497,15 @@ public class NativeRNFBTurboDatabaseQuery extends NativeRNFBTurboDatabaseQuerySp
|
|
|
414
497
|
boolean enabled,
|
|
415
498
|
Promise promise) {
|
|
416
499
|
DatabaseReference reference = getDatabaseForApp(app, dbURL).getReference(path);
|
|
417
|
-
getDatabaseQueryInstance(key, reference, modifiers).query
|
|
418
|
-
|
|
500
|
+
Query query = getDatabaseQueryInstance(key, reference, modifiers).query;
|
|
501
|
+
// Resolve only after the SDK apply runs (now, or after endNativeGet when deferred).
|
|
502
|
+
keepSyncedRegistry.keepSynced(
|
|
503
|
+
locationKey(app, reference),
|
|
504
|
+
key,
|
|
505
|
+
enabled,
|
|
506
|
+
() -> {
|
|
507
|
+
query.keepSynced(enabled);
|
|
508
|
+
promise.resolve(null);
|
|
509
|
+
});
|
|
419
510
|
}
|
|
420
511
|
}
|
|
@@ -38,6 +38,10 @@ public abstract class NativeRNFBTurboDatabaseQuerySpec extends ReactContextBaseJ
|
|
|
38
38
|
@DoNotStrip
|
|
39
39
|
public abstract void once(String app, String dbURL, String path, ReadableArray modifiers, String eventType, Promise promise);
|
|
40
40
|
|
|
41
|
+
@ReactMethod
|
|
42
|
+
@DoNotStrip
|
|
43
|
+
public abstract void get(String app, String dbURL, String path, ReadableArray modifiers, Promise promise);
|
|
44
|
+
|
|
41
45
|
@ReactMethod
|
|
42
46
|
@DoNotStrip
|
|
43
47
|
public abstract void on(String app, String dbURL, ReadableMap props);
|
|
@@ -89,6 +89,11 @@ static facebook::jsi::Value __hostFunction_NativeRNFBTurboDatabaseQuerySpecJSI_o
|
|
|
89
89
|
return static_cast<JavaTurboModule &>(turboModule).invokeJavaMethod(rt, PromiseKind, "once", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lcom/facebook/react/bridge/ReadableArray;Ljava/lang/String;Lcom/facebook/react/bridge/Promise;)V", args, count, cachedMethodId);
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
static facebook::jsi::Value __hostFunction_NativeRNFBTurboDatabaseQuerySpecJSI_get(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) {
|
|
93
|
+
static jmethodID cachedMethodId = nullptr;
|
|
94
|
+
return static_cast<JavaTurboModule &>(turboModule).invokeJavaMethod(rt, PromiseKind, "get", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lcom/facebook/react/bridge/ReadableArray;Lcom/facebook/react/bridge/Promise;)V", args, count, cachedMethodId);
|
|
95
|
+
}
|
|
96
|
+
|
|
92
97
|
static facebook::jsi::Value __hostFunction_NativeRNFBTurboDatabaseQuerySpecJSI_on(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) {
|
|
93
98
|
static jmethodID cachedMethodId = nullptr;
|
|
94
99
|
return static_cast<JavaTurboModule &>(turboModule).invokeJavaMethod(rt, VoidKind, "on", "(Ljava/lang/String;Ljava/lang/String;Lcom/facebook/react/bridge/ReadableMap;)V", args, count, cachedMethodId);
|
|
@@ -107,6 +112,7 @@ static facebook::jsi::Value __hostFunction_NativeRNFBTurboDatabaseQuerySpecJSI_k
|
|
|
107
112
|
NativeRNFBTurboDatabaseQuerySpecJSI::NativeRNFBTurboDatabaseQuerySpecJSI(const JavaTurboModule::InitParams ¶ms)
|
|
108
113
|
: JavaTurboModule(params) {
|
|
109
114
|
methodMap_["once"] = MethodMetadata {5, __hostFunction_NativeRNFBTurboDatabaseQuerySpecJSI_once};
|
|
115
|
+
methodMap_["get"] = MethodMetadata {4, __hostFunction_NativeRNFBTurboDatabaseQuerySpecJSI_get};
|
|
110
116
|
methodMap_["on"] = MethodMetadata {3, __hostFunction_NativeRNFBTurboDatabaseQuerySpecJSI_on};
|
|
111
117
|
methodMap_["off"] = MethodMetadata {2, __hostFunction_NativeRNFBTurboDatabaseQuerySpecJSI_off};
|
|
112
118
|
methodMap_["keepSynced"] = MethodMetadata {6, __hostFunction_NativeRNFBTurboDatabaseQuerySpecJSI_keepSynced};
|
|
@@ -170,6 +170,7 @@ public:
|
|
|
170
170
|
protected:
|
|
171
171
|
NativeRNFBTurboDatabaseQueryCxxSpec(std::shared_ptr<CallInvoker> jsInvoker) : TurboModule(std::string{NativeRNFBTurboDatabaseQueryCxxSpec::kModuleName}, jsInvoker) {
|
|
172
172
|
methodMap_["once"] = MethodMetadata {.argCount = 5, .invoker = __once};
|
|
173
|
+
methodMap_["get"] = MethodMetadata {.argCount = 4, .invoker = __get};
|
|
173
174
|
methodMap_["on"] = MethodMetadata {.argCount = 3, .invoker = __on};
|
|
174
175
|
methodMap_["off"] = MethodMetadata {.argCount = 2, .invoker = __off};
|
|
175
176
|
methodMap_["keepSynced"] = MethodMetadata {.argCount = 6, .invoker = __keepSynced};
|
|
@@ -188,6 +189,17 @@ private:
|
|
|
188
189
|
count <= 4 ? throw jsi::JSError(rt, "Expected argument in position 4 to be passed") : args[4].asString(rt));
|
|
189
190
|
}
|
|
190
191
|
|
|
192
|
+
static jsi::Value __get(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
|
|
193
|
+
static_assert(
|
|
194
|
+
bridging::getParameterCount(&T::get) == 5,
|
|
195
|
+
"Expected get(...) to have 5 parameters");
|
|
196
|
+
return bridging::callFromJs<jsi::Value>(rt, &T::get, static_cast<NativeRNFBTurboDatabaseQueryCxxSpec*>(&turboModule)->jsInvoker_, static_cast<T*>(&turboModule),
|
|
197
|
+
count <= 0 ? throw jsi::JSError(rt, "Expected argument in position 0 to be passed") : args[0].asString(rt),
|
|
198
|
+
count <= 1 ? throw jsi::JSError(rt, "Expected argument in position 1 to be passed") : args[1].asString(rt),
|
|
199
|
+
count <= 2 ? throw jsi::JSError(rt, "Expected argument in position 2 to be passed") : args[2].asString(rt),
|
|
200
|
+
count <= 3 ? throw jsi::JSError(rt, "Expected argument in position 3 to be passed") : args[3].asObject(rt).asArray(rt));
|
|
201
|
+
}
|
|
202
|
+
|
|
191
203
|
static jsi::Value __on(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
|
|
192
204
|
static_assert(
|
|
193
205
|
bridging::getParameterCount(&T::on) == 4,
|
package/android/src/test/java/io/invertase/firebase/database/RNFBDatabaseKeepSyncedRegistryTest.java
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
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.assertTrue;
|
|
23
|
+
|
|
24
|
+
import java.util.ArrayList;
|
|
25
|
+
import java.util.Arrays;
|
|
26
|
+
import java.util.Collections;
|
|
27
|
+
import java.util.List;
|
|
28
|
+
import org.junit.Test;
|
|
29
|
+
|
|
30
|
+
public class RNFBDatabaseKeepSyncedRegistryTest {
|
|
31
|
+
private static final String LOCATION = "[DEFAULT]|https://db.example.com/users/1";
|
|
32
|
+
private static final String OTHER_LOCATION = "[DEFAULT]|https://db.example.com/users/2";
|
|
33
|
+
|
|
34
|
+
private final RNFBDatabaseKeepSyncedRegistry registry = new RNFBDatabaseKeepSyncedRegistry();
|
|
35
|
+
private final List<String> applied = new ArrayList<>();
|
|
36
|
+
|
|
37
|
+
private void keepSynced(String location, String queryKey, boolean enabled) {
|
|
38
|
+
registry.keepSynced(location, queryKey, enabled, () -> applied.add(queryKey + "=" + enabled));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
@Test
|
|
42
|
+
public void beginNativeGet_nothingKeptSynced_returnsTrue() {
|
|
43
|
+
assertTrue(registry.beginNativeGet(LOCATION));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
@Test
|
|
47
|
+
public void beginNativeGet_locationKeptSynced_returnsFalse() {
|
|
48
|
+
keepSynced(LOCATION, "plain", true);
|
|
49
|
+
assertFalse(registry.beginNativeGet(LOCATION));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
@Test
|
|
53
|
+
public void beginNativeGet_queryAtLocationKeptSynced_returnsFalse() {
|
|
54
|
+
keepSynced(LOCATION, "limitToLast(1)", true);
|
|
55
|
+
assertFalse(registry.beginNativeGet(LOCATION));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
@Test
|
|
59
|
+
public void beginNativeGet_otherLocationKeptSynced_returnsTrue() {
|
|
60
|
+
keepSynced(OTHER_LOCATION, "plain", true);
|
|
61
|
+
assertTrue(registry.beginNativeGet(LOCATION));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
@Test
|
|
65
|
+
public void beginNativeGet_afterKeepSyncedFalse_returnsTrue() {
|
|
66
|
+
keepSynced(LOCATION, "plain", true);
|
|
67
|
+
keepSynced(LOCATION, "plain", false);
|
|
68
|
+
assertTrue(registry.beginNativeGet(LOCATION));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
@Test
|
|
72
|
+
public void beginNativeGet_untilEveryQueryAtLocationIsCleared_returnsFalse() {
|
|
73
|
+
keepSynced(LOCATION, "plain", true);
|
|
74
|
+
keepSynced(LOCATION, "limitToLast(1)", true);
|
|
75
|
+
keepSynced(LOCATION, "plain", false);
|
|
76
|
+
assertFalse(registry.beginNativeGet(LOCATION));
|
|
77
|
+
keepSynced(LOCATION, "limitToLast(1)", false);
|
|
78
|
+
assertTrue(registry.beginNativeGet(LOCATION));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
@Test
|
|
82
|
+
public void keepSynced_noNativeGetInFlight_appliesNow() {
|
|
83
|
+
keepSynced(LOCATION, "plain", true);
|
|
84
|
+
assertEquals(Collections.singletonList("plain=true"), applied);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
@Test
|
|
88
|
+
public void keepSynced_disableWhenNothingKept_stillApplies() {
|
|
89
|
+
keepSynced(LOCATION, "plain", false);
|
|
90
|
+
assertEquals(Collections.singletonList("plain=false"), applied);
|
|
91
|
+
assertTrue(registry.beginNativeGet(LOCATION));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
@Test
|
|
95
|
+
public void keepSynced_noNativeGetInFlight_runsApplyBeforeCallerCompletion() {
|
|
96
|
+
List<String> order = new ArrayList<>();
|
|
97
|
+
registry.keepSynced(
|
|
98
|
+
LOCATION,
|
|
99
|
+
"plain",
|
|
100
|
+
true,
|
|
101
|
+
() -> {
|
|
102
|
+
order.add("sdk");
|
|
103
|
+
order.add("promise");
|
|
104
|
+
});
|
|
105
|
+
assertEquals(Arrays.asList("sdk", "promise"), order);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
@Test
|
|
109
|
+
public void keepSynced_nativeGetInFlight_waitsForItInCallOrder() {
|
|
110
|
+
registry.beginNativeGet(LOCATION);
|
|
111
|
+
keepSynced(LOCATION, "plain", true);
|
|
112
|
+
keepSynced(LOCATION, "limitToLast(1)", true);
|
|
113
|
+
keepSynced(LOCATION, "plain", false);
|
|
114
|
+
assertTrue(applied.isEmpty());
|
|
115
|
+
|
|
116
|
+
registry.endNativeGet(LOCATION);
|
|
117
|
+
assertEquals(Arrays.asList("plain=true", "limitToLast(1)=true", "plain=false"), applied);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
@Test
|
|
121
|
+
public void keepSynced_nativeGetInFlight_runsEachApplyBeforeItsCallerCompletion() {
|
|
122
|
+
List<String> order = new ArrayList<>();
|
|
123
|
+
registry.beginNativeGet(LOCATION);
|
|
124
|
+
registry.keepSynced(
|
|
125
|
+
LOCATION,
|
|
126
|
+
"plain",
|
|
127
|
+
true,
|
|
128
|
+
() -> {
|
|
129
|
+
order.add("sdk-true");
|
|
130
|
+
order.add("promise-true");
|
|
131
|
+
});
|
|
132
|
+
registry.keepSynced(
|
|
133
|
+
LOCATION,
|
|
134
|
+
"plain",
|
|
135
|
+
false,
|
|
136
|
+
() -> {
|
|
137
|
+
order.add("sdk-false");
|
|
138
|
+
order.add("promise-false");
|
|
139
|
+
});
|
|
140
|
+
assertTrue(order.isEmpty());
|
|
141
|
+
|
|
142
|
+
registry.endNativeGet(LOCATION);
|
|
143
|
+
assertEquals(Arrays.asList("sdk-true", "promise-true", "sdk-false", "promise-false"), order);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
@Test
|
|
147
|
+
public void keepSynced_nativeGetInFlight_makesLaterGetsUseOnce() {
|
|
148
|
+
registry.beginNativeGet(LOCATION);
|
|
149
|
+
keepSynced(LOCATION, "plain", true);
|
|
150
|
+
assertFalse(registry.beginNativeGet(LOCATION));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
@Test
|
|
154
|
+
public void keepSynced_nativeGetInFlightAtOtherLocation_appliesNow() {
|
|
155
|
+
registry.beginNativeGet(OTHER_LOCATION);
|
|
156
|
+
keepSynced(LOCATION, "plain", true);
|
|
157
|
+
assertEquals(Collections.singletonList("plain=true"), applied);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
@Test
|
|
161
|
+
public void endNativeGet_waitsForTheLastGetAtLocation() {
|
|
162
|
+
registry.beginNativeGet(LOCATION);
|
|
163
|
+
registry.beginNativeGet(LOCATION);
|
|
164
|
+
keepSynced(LOCATION, "plain", true);
|
|
165
|
+
|
|
166
|
+
registry.endNativeGet(LOCATION);
|
|
167
|
+
assertTrue(applied.isEmpty());
|
|
168
|
+
registry.endNativeGet(LOCATION);
|
|
169
|
+
assertEquals(Collections.singletonList("plain=true"), applied);
|
|
170
|
+
|
|
171
|
+
keepSynced(LOCATION, "plain", false);
|
|
172
|
+
assertEquals(Arrays.asList("plain=true", "plain=false"), applied);
|
|
173
|
+
}
|
|
174
|
+
}
|
package/android/src/test/java/io/invertase/firebase/database/RNFBDatabaseListenerRegistryTest.java
CHANGED
|
@@ -109,6 +109,37 @@ public class RNFBDatabaseListenerRegistryTest {
|
|
|
109
109
|
assertFalse(registry.hasListeners());
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
@Test
|
|
113
|
+
public void getValue_nullKey_returnsNull_noNpe() {
|
|
114
|
+
RNFBDatabaseListenerRegistry registry = new RNFBDatabaseListenerRegistry();
|
|
115
|
+
assertNull(registry.getValue(null));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
@Test
|
|
119
|
+
public void getChild_nullKey_returnsNull_noNpe() {
|
|
120
|
+
RNFBDatabaseListenerRegistry registry = new RNFBDatabaseListenerRegistry();
|
|
121
|
+
assertNull(registry.getChild(null));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
@Test
|
|
125
|
+
public void takeValue_nullKey_returnsNull_noNpe() {
|
|
126
|
+
RNFBDatabaseListenerRegistry registry = new RNFBDatabaseListenerRegistry();
|
|
127
|
+
assertNull(registry.takeValue(null));
|
|
128
|
+
assertFalse(registry.hasListeners());
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
@Test
|
|
132
|
+
public void takeChild_nullKey_returnsNull_noNpe() {
|
|
133
|
+
RNFBDatabaseListenerRegistry registry = new RNFBDatabaseListenerRegistry();
|
|
134
|
+
assertNull(registry.takeChild(null));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
@Test
|
|
138
|
+
public void hasEventListener_nullKey_returnsFalse_noNpe() {
|
|
139
|
+
RNFBDatabaseListenerRegistry registry = new RNFBDatabaseListenerRegistry();
|
|
140
|
+
assertFalse(registry.hasEventListener(null));
|
|
141
|
+
}
|
|
142
|
+
|
|
112
143
|
@Test
|
|
113
144
|
public void takeChild_missingKey_isNoOp() {
|
|
114
145
|
RNFBDatabaseListenerRegistry registry = new RNFBDatabaseListenerRegistry();
|
package/android/src/test/java/io/invertase/firebase/database/RNFBDatabaseQueryRegistryTest.java
CHANGED
|
@@ -209,6 +209,42 @@ public class RNFBDatabaseQueryRegistryTest {
|
|
|
209
209
|
assertEquals(2, query.calls.get());
|
|
210
210
|
}
|
|
211
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
|
+
|
|
212
248
|
/**
|
|
213
249
|
* After identity-take, listeners appear but a concurrent put claimed the slot — put-back fails;
|
|
214
250
|
* orphan must clear listeners so SDK callbacks are not left outside the registry.
|
|
@@ -431,4 +467,23 @@ public class RNFBDatabaseQueryRegistryTest {
|
|
|
431
467
|
registry.takeIfIdle("missing");
|
|
432
468
|
assertNull(registry.get("missing"));
|
|
433
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
|
+
}
|
|
434
489
|
}
|
|
@@ -0,0 +1,137 @@
|
|
|
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.assertNotNull;
|
|
22
|
+
import static org.junit.Assert.assertTrue;
|
|
23
|
+
import static org.mockito.ArgumentMatchers.any;
|
|
24
|
+
import static org.mockito.ArgumentMatchers.anyString;
|
|
25
|
+
import static org.mockito.Mockito.doAnswer;
|
|
26
|
+
import static org.mockito.Mockito.mock;
|
|
27
|
+
import static org.mockito.Mockito.mockStatic;
|
|
28
|
+
import static org.mockito.Mockito.verify;
|
|
29
|
+
import static org.mockito.Mockito.when;
|
|
30
|
+
|
|
31
|
+
import android.content.Context;
|
|
32
|
+
import com.facebook.react.bridge.Promise;
|
|
33
|
+
import com.facebook.react.bridge.ReactApplicationContext;
|
|
34
|
+
import com.facebook.react.bridge.WritableMap;
|
|
35
|
+
import com.google.android.gms.tasks.Task;
|
|
36
|
+
import com.google.firebase.database.DatabaseError;
|
|
37
|
+
import com.google.firebase.database.DatabaseReference;
|
|
38
|
+
import com.google.firebase.database.FirebaseDatabase;
|
|
39
|
+
import com.google.firebase.database.OnDisconnect;
|
|
40
|
+
import org.junit.Test;
|
|
41
|
+
import org.junit.runner.RunWith;
|
|
42
|
+
import org.mockito.ArgumentCaptor;
|
|
43
|
+
import org.mockito.MockedStatic;
|
|
44
|
+
import org.robolectric.RobolectricTestRunner;
|
|
45
|
+
import org.robolectric.shadows.ShadowLooper;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* onDisconnect.cancel() is not rejected by RTDB {@code .write: false} (e2e wire evidence in
|
|
49
|
+
* writePermissionDenied.e2e.js). Drive the CompletionListener error arm + TurboModule reject with a
|
|
50
|
+
* mocked Firebase OnDisconnect — AndroidTest-AD-1.
|
|
51
|
+
*/
|
|
52
|
+
@RunWith(RobolectricTestRunner.class)
|
|
53
|
+
public class UniversalFirebaseDatabaseOnDisconnectCancelErrorTest {
|
|
54
|
+
|
|
55
|
+
private static void stubCancelWithPermissionDenied(
|
|
56
|
+
OnDisconnect onDisconnect, DatabaseReference ref) {
|
|
57
|
+
doAnswer(
|
|
58
|
+
invocation -> {
|
|
59
|
+
DatabaseReference.CompletionListener listener = invocation.getArgument(0);
|
|
60
|
+
listener.onComplete(DatabaseError.fromCode(DatabaseError.PERMISSION_DENIED), ref);
|
|
61
|
+
return null;
|
|
62
|
+
})
|
|
63
|
+
.when(onDisconnect)
|
|
64
|
+
.cancel(any(DatabaseReference.CompletionListener.class));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
@Test
|
|
68
|
+
public void onDisconnectCancel_taskFailsWithUniversalDatabaseException() {
|
|
69
|
+
Context context = mock(Context.class);
|
|
70
|
+
FirebaseDatabase firebaseDatabase = mock(FirebaseDatabase.class);
|
|
71
|
+
DatabaseReference reference = mock(DatabaseReference.class);
|
|
72
|
+
OnDisconnect onDisconnect = mock(OnDisconnect.class);
|
|
73
|
+
|
|
74
|
+
when(firebaseDatabase.getReference("nope/foo")).thenReturn(reference);
|
|
75
|
+
when(reference.onDisconnect()).thenReturn(onDisconnect);
|
|
76
|
+
stubCancelWithPermissionDenied(onDisconnect, reference);
|
|
77
|
+
|
|
78
|
+
try (MockedStatic<UniversalFirebaseDatabaseCommon> common =
|
|
79
|
+
mockStatic(UniversalFirebaseDatabaseCommon.class)) {
|
|
80
|
+
common
|
|
81
|
+
.when(() -> UniversalFirebaseDatabaseCommon.getDatabaseForApp(anyString(), anyString()))
|
|
82
|
+
.thenReturn(firebaseDatabase);
|
|
83
|
+
|
|
84
|
+
UniversalFirebaseDatabaseOnDisconnectModule module =
|
|
85
|
+
new UniversalFirebaseDatabaseOnDisconnectModule(context, "DatabaseOnDisconnect");
|
|
86
|
+
|
|
87
|
+
Task<Void> task = module.onDisconnectCancel("app", "https://example.com", "nope/foo");
|
|
88
|
+
|
|
89
|
+
assertTrue(task.isComplete());
|
|
90
|
+
assertFalse(task.isSuccessful());
|
|
91
|
+
assertTrue(task.getException() instanceof UniversalDatabaseException);
|
|
92
|
+
UniversalDatabaseException databaseException =
|
|
93
|
+
(UniversalDatabaseException) task.getException();
|
|
94
|
+
assertTrue(databaseException.getCode().contains("permission-denied"));
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
@Test
|
|
99
|
+
public void turboOnDisconnectCancel_rejectsPromiseOnDatabaseError() {
|
|
100
|
+
ReactApplicationContext reactContext = mock(ReactApplicationContext.class);
|
|
101
|
+
when(reactContext.getApplicationContext()).thenReturn(reactContext);
|
|
102
|
+
|
|
103
|
+
FirebaseDatabase firebaseDatabase = mock(FirebaseDatabase.class);
|
|
104
|
+
DatabaseReference reference = mock(DatabaseReference.class);
|
|
105
|
+
OnDisconnect onDisconnect = mock(OnDisconnect.class);
|
|
106
|
+
|
|
107
|
+
when(firebaseDatabase.getReference("nope/foo")).thenReturn(reference);
|
|
108
|
+
when(reference.onDisconnect()).thenReturn(onDisconnect);
|
|
109
|
+
stubCancelWithPermissionDenied(onDisconnect, reference);
|
|
110
|
+
|
|
111
|
+
Promise promise = mock(Promise.class);
|
|
112
|
+
WritableMap userInfo = mock(WritableMap.class);
|
|
113
|
+
|
|
114
|
+
try (MockedStatic<UniversalFirebaseDatabaseCommon> common =
|
|
115
|
+
mockStatic(UniversalFirebaseDatabaseCommon.class);
|
|
116
|
+
MockedStatic<com.facebook.react.bridge.Arguments> arguments =
|
|
117
|
+
mockStatic(com.facebook.react.bridge.Arguments.class)) {
|
|
118
|
+
common
|
|
119
|
+
.when(() -> UniversalFirebaseDatabaseCommon.getDatabaseForApp(anyString(), anyString()))
|
|
120
|
+
.thenReturn(firebaseDatabase);
|
|
121
|
+
arguments.when(com.facebook.react.bridge.Arguments::createMap).thenReturn(userInfo);
|
|
122
|
+
|
|
123
|
+
NativeRNFBTurboDatabaseOnDisconnect turbo =
|
|
124
|
+
new NativeRNFBTurboDatabaseOnDisconnect(reactContext);
|
|
125
|
+
turbo.onDisconnectCancel("app", "https://example.com", "nope/foo", promise);
|
|
126
|
+
|
|
127
|
+
// addOnCompleteListener without an executor posts to the main looper.
|
|
128
|
+
ShadowLooper.idleMainLooper();
|
|
129
|
+
|
|
130
|
+
ArgumentCaptor<String> code = ArgumentCaptor.forClass(String.class);
|
|
131
|
+
ArgumentCaptor<String> message = ArgumentCaptor.forClass(String.class);
|
|
132
|
+
verify(promise).reject(code.capture(), message.capture(), any(WritableMap.class));
|
|
133
|
+
assertTrue(code.getValue().contains("permission-denied"));
|
|
134
|
+
assertNotNull(message.getValue());
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -17,10 +17,24 @@
|
|
|
17
17
|
*
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
-
import { isBoolean, isFunction, isNull, isNumber, isObject, isString, isUndefined, pathIsEmpty, pathToUrlEncodedString, ReferenceBase } from '@react-native-firebase/app/dist/module/common';
|
|
20
|
+
import { isBoolean, isFunction, isIOS, isNull, isNumber, isObject, isString, isUndefined, pathIsEmpty, pathToUrlEncodedString, ReferenceBase } from '@react-native-firebase/app/dist/module/common';
|
|
21
21
|
import DatabaseDataSnapshot from "./DatabaseDataSnapshot.js";
|
|
22
22
|
import DatabaseSyncTree from "./DatabaseSyncTree.js";
|
|
23
23
|
const eventTypes = ['value', 'child_added', 'child_changed', 'child_moved', 'child_removed'];
|
|
24
|
+
|
|
25
|
+
/** `.info/*` is client-local metadata that only the listener path serves. */
|
|
26
|
+
const INFO_PATH = /^\/?\.info(\/|$)/;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* True when native/web get already produced an observer-shaped `database/*` code.
|
|
30
|
+
* Android Query.get() non-denial failures use SharedUtils.getExceptionMap → `database/unknown`;
|
|
31
|
+
* those still fall through to `once('value')`. Specific codes mean once already ran (or a
|
|
32
|
+
* denial was mapped to once's code) — do not ask the server again.
|
|
33
|
+
*/
|
|
34
|
+
function isObserverShapedDatabaseError(error) {
|
|
35
|
+
const code = error?.code;
|
|
36
|
+
return typeof code === 'string' && code.startsWith('database/') && code !== 'database/unknown';
|
|
37
|
+
}
|
|
24
38
|
let DatabaseReferenceClass = null;
|
|
25
39
|
export function provideReferenceClass(databaseReference) {
|
|
26
40
|
DatabaseReferenceClass = databaseReference;
|
|
@@ -180,6 +194,43 @@ export default class DatabaseQuery extends ReferenceBase {
|
|
|
180
194
|
listeners += 1;
|
|
181
195
|
return callback;
|
|
182
196
|
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* One-shot read backing modular `get()`: on Android and web, the SDKs' single request/response
|
|
200
|
+
* read (`Query.get()`, web `get()`) instead of the listen -> data -> unlisten exchange
|
|
201
|
+
* `once('value')` performs. On iOS the native `get` is still the single-event observer; see
|
|
202
|
+
* `RNFBDatabaseQueryHelper get:`.
|
|
203
|
+
*
|
|
204
|
+
* Callers keep `once('value')` semantics, with one exception noted below:
|
|
205
|
+
* - iOS failures pass straight through (native get already is `once`).
|
|
206
|
+
* - On Android / web, an error whose code is already a specific `database/*` value (anything
|
|
207
|
+
* other than `database/unknown`) passes through: Android maps Query.get() permission denials
|
|
208
|
+
* to the same code `once` gives, and when native get already used `addOnceValueEventListener`
|
|
209
|
+
* (persistence, logging, keepSynced) its cancellations are those codes too — do not call
|
|
210
|
+
* `once('value')` again. Only a plain Query.get() failure is rejected as `database/unknown`
|
|
211
|
+
* (SharedUtils.getExceptionMap); that path still retries as `once('value')`.
|
|
212
|
+
* - Queries (any orderBy / limit / range modifier) use `once('value')`: `get()` parity for
|
|
213
|
+
* unindexed queries is unverified.
|
|
214
|
+
* - `.info/*` paths use `once('value')`: the one-shot reads go to the server tree, not the
|
|
215
|
+
* client-local `.info` tree.
|
|
216
|
+
*
|
|
217
|
+
* The exception: the one-shot read resolves with the server's reply as it was when the read was
|
|
218
|
+
* sent, where `once('value')` layered this client's pending writes over it. A local write made to
|
|
219
|
+
* an overlapping path while a `get()` is in flight is therefore not in its result, as with the
|
|
220
|
+
* web SDK's `get()`.
|
|
221
|
+
*/
|
|
222
|
+
_get() {
|
|
223
|
+
const modifiers = this._modifiers._copy().toArray();
|
|
224
|
+
if (modifiers.length > 0 || INFO_PATH.test(this.path)) {
|
|
225
|
+
return this.once('value');
|
|
226
|
+
}
|
|
227
|
+
return this._database.native.get(this.path, modifiers).then(result => new DatabaseDataSnapshot(this.ref, result), error => {
|
|
228
|
+
if (isIOS || isObserverShapedDatabaseError(error)) {
|
|
229
|
+
return Promise.reject(error);
|
|
230
|
+
}
|
|
231
|
+
return this.once('value');
|
|
232
|
+
});
|
|
233
|
+
}
|
|
183
234
|
once(eventType, successCallBack, failureCallbackOrContext, context) {
|
|
184
235
|
if (!eventTypes.includes(eventType)) {
|
|
185
236
|
throw new Error(`firebase.database().ref().once(*) 'eventType' must be one of ${eventTypes.join(', ')}.`);
|