feat(firestore): support clearPersistence() & terminate() APIs (#3591)

Co-authored-by: Mike Diarmid <mike.diarmid@gmail.com>

[publish]
This commit is contained in:
Russell Wheatley
2020-06-22 14:46:46 +01:00
committed by GitHub
parent ca34cef160
commit 57ff9003b6
15 changed files with 6423 additions and 5274 deletions

11453
docs/typedoc.json vendored

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -76,6 +76,10 @@ public class UniversalFirebasePreferences {
getPreferences().edit().clear().apply();
}
public SharedPreferences.Editor remove(String key){
return getPreferences().edit().remove(key);
}
private SharedPreferences getPreferences() {
if (preferences == null) {
preferences = ReactNativeFirebaseApp

View File

@@ -37,6 +37,8 @@
- (void)clearAll;
- (void)remove:(NSString *)key;
+ (RNFBPreferences *)shared;
@end

View File

@@ -83,6 +83,10 @@ static RNFBPreferences *sharedInstance;
[_userDefaults removePersistentDomainForName:RNFBDomainIdentifier];
}
- (void)remove:(NSString *)key {
[_userDefaults removeObjectForKey:key];
}
+ (RNFBPreferences *)shared {
return sharedInstance;
}

View File

@@ -20,50 +20,63 @@ package io.invertase.firebase.firestore;
import com.google.firebase.FirebaseApp;
import com.google.firebase.firestore.DocumentReference;
import com.google.firebase.firestore.FirebaseFirestore;
import com.google.firebase.firestore.FirebaseFirestoreException;
import com.google.firebase.firestore.FirebaseFirestoreSettings;
import com.google.firebase.firestore.Query;
import java.util.HashMap;
import java.util.WeakHashMap;
import java.lang.ref.WeakReference;
import io.invertase.firebase.common.UniversalFirebasePreferences;
public class UniversalFirebaseFirestoreCommon {
private static HashMap<String, Boolean> settingsLock = new HashMap<>();
static WeakHashMap<String, WeakReference<FirebaseFirestore>> instanceCache = new WeakHashMap<>();
static FirebaseFirestore getFirestoreForApp(String appName) {
WeakReference<FirebaseFirestore> cachedInstance = instanceCache.get(appName);
if(cachedInstance != null){
return cachedInstance.get();
}
FirebaseApp firebaseApp = FirebaseApp.getInstance(appName);
FirebaseFirestore instance = FirebaseFirestore.getInstance(firebaseApp);
setFirestoreSettings(instance, appName);
instanceCache.put(appName, new WeakReference<FirebaseFirestore>(instance));
return instance;
}
private static void setFirestoreSettings(FirebaseFirestore firebaseFirestore, String appName) {
// Ensure not already been set
if (settingsLock.containsKey(appName)) return;
UniversalFirebasePreferences preferences = UniversalFirebasePreferences.getSharedInstance();
FirebaseFirestoreSettings.Builder firestoreSettings = new FirebaseFirestoreSettings.Builder();
String cacheSizeKey = UniversalFirebaseFirestoreStatics.FIRESTORE_CACHE_SIZE + "_" + appName;
String hostKey = UniversalFirebaseFirestoreStatics.FIRESTORE_HOST + "_" + appName;
String persistenceKey = UniversalFirebaseFirestoreStatics.FIRESTORE_PERSISTENCE + "_" + appName;
String sslKey = UniversalFirebaseFirestoreStatics.FIRESTORE_SSL + "_" + appName;
int cacheSizeBytes = preferences.getIntValue(
UniversalFirebaseFirestoreStatics.FIRESTORE_CACHE_SIZE + "_" + appName,
cacheSizeKey,
(int) firebaseFirestore.getFirestoreSettings().getCacheSizeBytes()
);
String host = preferences.getStringValue(
UniversalFirebaseFirestoreStatics.FIRESTORE_HOST + "_" + appName,
hostKey,
firebaseFirestore.getFirestoreSettings().getHost()
);
boolean persistence = preferences.getBooleanValue(
UniversalFirebaseFirestoreStatics.FIRESTORE_PERSISTENCE + "_" + appName,
persistenceKey,
firebaseFirestore.getFirestoreSettings().isPersistenceEnabled()
);
boolean ssl = preferences.getBooleanValue(
UniversalFirebaseFirestoreStatics.FIRESTORE_SSL + "_" + appName,
sslKey,
firebaseFirestore.getFirestoreSettings().isSslEnabled()
);
@@ -79,7 +92,8 @@ public class UniversalFirebaseFirestoreCommon {
firebaseFirestore.setFirestoreSettings(firestoreSettings.build());
settingsLock.put(appName, true);
preferences.remove(cacheSizeKey).remove(hostKey).remove(persistenceKey).remove(sslKey).apply();
}
static Query getQueryForFirestore(

View File

@@ -30,7 +30,7 @@ public class UniversalFirebaseFirestoreException extends Exception {
UniversalFirebaseFirestoreException(FirebaseFirestoreException nativeException, Throwable cause) {
super(nativeException != null ? nativeException.getMessage() : "", cause);
String code = "unknown";
String code = null;
String message = "An unknown error occurred";
if (cause != null && cause.getMessage() != null && cause.getMessage().contains(":")) {

View File

@@ -18,10 +18,10 @@ package io.invertase.firebase.firestore;
*/
import android.content.Context;
import com.google.android.gms.tasks.Task;
import com.google.android.gms.tasks.Tasks;
import com.google.firebase.firestore.FirebaseFirestore;
import com.google.firebase.firestore.FirebaseFirestoreSettings;
import io.invertase.firebase.common.UniversalFirebaseModule;
import io.invertase.firebase.common.UniversalFirebasePreferences;
@@ -29,6 +29,7 @@ import java.util.Map;
import java.util.Objects;
import static io.invertase.firebase.firestore.UniversalFirebaseFirestoreCommon.getFirestoreForApp;
import static io.invertase.firebase.firestore.UniversalFirebaseFirestoreCommon.instanceCache;
public class UniversalFirebaseFirestoreModule extends UniversalFirebaseModule {
@@ -52,35 +53,44 @@ public class UniversalFirebaseFirestoreModule extends UniversalFirebaseModule {
UniversalFirebasePreferences.getSharedInstance().setIntValue(
UniversalFirebaseFirestoreStatics.FIRESTORE_CACHE_SIZE + "_" + appName,
Objects.requireNonNull(cacheSizeBytesDouble).intValue()
);
Objects.requireNonNull(cacheSizeBytesDouble).intValue());
}
// settings.host
if (settings.containsKey("host")) {
UniversalFirebasePreferences.getSharedInstance().setStringValue(
UniversalFirebaseFirestoreStatics.FIRESTORE_HOST + "_" + appName,
(String) settings.get("host")
);
UniversalFirebaseFirestoreStatics.FIRESTORE_HOST + "_" + appName, (String) settings.get("host"));
}
// settings.persistence
if (settings.containsKey("persistence")) {
UniversalFirebasePreferences.getSharedInstance().setBooleanValue(
UniversalFirebaseFirestoreStatics.FIRESTORE_PERSISTENCE + "_" + appName,
(boolean) settings.get("persistence")
);
(boolean) settings.get("persistence"));
}
// settings.ssl
if (settings.containsKey("ssl")) {
UniversalFirebasePreferences.getSharedInstance().setBooleanValue(
UniversalFirebaseFirestoreStatics.FIRESTORE_SSL + "_" + appName,
(boolean) settings.get("ssl")
);
UniversalFirebaseFirestoreStatics.FIRESTORE_SSL + "_" + appName, (boolean) settings.get("ssl"));
}
return null;
});
}
Task<Void> clearPersistence(String appName) {
return getFirestoreForApp(appName).clearPersistence();
}
Task<Void> terminate(String appName) {
FirebaseFirestore firebaseFirestore = getFirestoreForApp(appName);
if (instanceCache.get(appName) != null) {
instanceCache.get(appName).clear();
instanceCache.remove(appName);
}
return firebaseFirestore.terminate();
}
}

View File

@@ -45,6 +45,17 @@ public class ReactNativeFirebaseFirestoreModule extends ReactNativeFirebaseModul
}
}
@ReactMethod
public void clearPersistence(String appName, Promise promise) {
module.clearPersistence(appName).addOnCompleteListener(task -> {
if (task.isSuccessful()) {
promise.resolve(null);
} else {
rejectPromiseFirestoreException(promise, task.getException());
}
});
}
@ReactMethod
public void disableNetwork(String appName, Promise promise) {
module.disableNetwork(appName).addOnCompleteListener(task -> {
@@ -77,4 +88,15 @@ public class ReactNativeFirebaseFirestoreModule extends ReactNativeFirebaseModul
}
});
}
@ReactMethod
public void terminate(String appName, Promise promise) {
module.terminate(appName).addOnCompleteListener(task -> {
if (task.isSuccessful()) {
promise.resolve(null);
} else {
rejectPromiseFirestoreException(promise, task.getException());
}
});
}
}

View File

@@ -318,4 +318,35 @@ describe('firestore()', () => {
}
});
});
describe('Clear cached data persistence', () => {
it('should clear any cached data', async () => {
const db = firebase.firestore();
const id = 'foobar';
const ref = db.doc(`v6/${id}`);
await ref.set({ foo: 'bar' });
try {
await db.clearPersistence();
return Promise.reject(new Error('Did not throw an Error.'));
} catch (error) {
error.code.should.equal('firestore/failed-precondition');
}
const doc = await ref.get({ source: 'cache' });
should(doc.id).equal(id);
await db.terminate();
await db.clearPersistence();
try {
await ref.get({ source: 'cache' });
return Promise.reject(new Error('Did not throw an Error.'));
} catch (error) {
error.code.should.equal('firestore/unavailable');
return Promise.resolve();
}
});
});
});

View File

@@ -41,3 +41,4 @@ extern NSString *const FIRESTORE_CACHE_SIZE;
extern NSString *const FIRESTORE_HOST;
extern NSString *const FIRESTORE_PERSISTENCE;
extern NSString *const FIRESTORE_SSL;
extern NSMutableDictionary * instanceCache;

View File

@@ -24,14 +24,27 @@ NSString *const FIRESTORE_HOST = @"firebase_firestore_host";
NSString *const FIRESTORE_PERSISTENCE = @"firebase_firestore_persistence";
NSString *const FIRESTORE_SSL = @"firebase_firestore_ssl";
__strong NSMutableDictionary *settingsLock;
NSMutableDictionary * instanceCache;
@implementation RNFBFirestoreCommon
+ (FIRFirestore *)getFirestoreForApp:(FIRApp *)app {
FIRFirestore *instance = [FIRFirestore firestoreForApp:app];
[self setFirestoreSettings:instance appName:[RNFBSharedUtils getAppJavaScriptName:app.name]];
return instance;
if(instanceCache == nil){
instanceCache = [[NSMutableDictionary alloc] init];
}
FIRFirestore * cachedInstance = instanceCache[[app name]];
if(cachedInstance){
return cachedInstance;
}
FIRFirestore *instance = [FIRFirestore firestoreForApp:app];
[self setFirestoreSettings:instance appName:[RNFBSharedUtils getAppJavaScriptName:app.name]];
instanceCache[[app name]] = instance;
return instance;
}
+ (dispatch_queue_t)getFirestoreQueue {
@@ -44,16 +57,6 @@ __strong NSMutableDictionary *settingsLock;
}
+ (void)setFirestoreSettings:(FIRFirestore *)firestore appName:(NSString *)appName {
@synchronized(settingsLock) {
if (settingsLock == nil) {
settingsLock = [[NSMutableDictionary alloc] init];
}
// Prevent setting if already set
if (settingsLock[appName]) {
return;
}
FIRFirestoreSettings *firestoreSettings = [[FIRFirestoreSettings alloc] init];
RNFBPreferences *preferences = [RNFBPreferences shared];
@@ -79,9 +82,12 @@ __strong NSMutableDictionary *settingsLock;
NSString *sslKey = [NSString stringWithFormat:@"%@_%@", FIRESTORE_SSL, appName];
firestoreSettings.sslEnabled = (BOOL) [preferences getBooleanValue:sslKey defaultValue:firestore.settings.sslEnabled];
settingsLock[appName] = @(YES);
firestore.settings = firestoreSettings;
}
[preferences remove:cacheKey];
[preferences remove:hostKey];
[preferences remove:persistenceKey];
[preferences remove:sslKey];
}
+ (FIRDocumentReference *)getDocumentForFirestore:(FIRFirestore *)firestore path:(NSString *)path; {

View File

@@ -103,4 +103,36 @@ RCT_EXPORT_METHOD(settings:
resolve([NSNull null]);
}
RCT_EXPORT_METHOD(clearPersistence:
(FIRApp *) firebaseApp
: (RCTPromiseResolveBlock) resolve
: (RCTPromiseRejectBlock)reject
) {
[[RNFBFirestoreCommon getFirestoreForApp:firebaseApp] clearPersistenceWithCompletion:^(NSError *error) {
if (error) {
[RNFBFirestoreCommon promiseRejectFirestoreException:reject error:error];
} else {
resolve(nil);
}
}];
}
RCT_EXPORT_METHOD(terminate:
(FIRApp *) firebaseApp
: (RCTPromiseResolveBlock) resolve
: (RCTPromiseRejectBlock)reject
) {
FIRFirestore *instance = [RNFBFirestoreCommon getFirestoreForApp:firebaseApp];
[instance terminateWithCompletion:^(NSError *error) {
if (error) {
[RNFBFirestoreCommon promiseRejectFirestoreException:reject error:error];
} else {
[instanceCache removeObjectForKey: [firebaseApp name]];
resolve(nil);
}
}];
}
@end

View File

@@ -1958,6 +1958,28 @@ export namespace FirebaseFirestoreTypes {
* @param settings A `Settings` object.
*/
settings(settings: Settings): Promise<void>;
/**
* Aimed primarily at clearing up any data cached from running tests. Needs to be executed before any database calls
* are made.
*
* #### Example
*
*```js
* await firebase.firestore().clearPersistence();
* ```
*/
clearPersistence(): Promise<void>;
/**
* Typically called to ensure a new Firestore instance is initialized before calling
* `firebase.firestore().clearPersistence()`.
*
* #### Example
*
*```js
* await firebase.firestore().terminate();
* ```
*/
terminate(): Promise<void>;
}
}

View File

@@ -80,6 +80,14 @@ class FirebaseFirestoreModule extends FirebaseModule {
return new FirestoreWriteBatch(this);
}
async clearPersistence() {
await this.native.clearPersistence();
}
async terminate() {
await this.native.terminate();
}
collection(collectionPath) {
if (!isString(collectionPath)) {
throw new Error(
@@ -130,8 +138,8 @@ class FirebaseFirestoreModule extends FirebaseModule {
);
}
disableNetwork() {
return this.native.disableNetwork();
async disableNetwork() {
await this.native.disableNetwork();
}
doc(documentPath) {
@@ -152,8 +160,8 @@ class FirebaseFirestoreModule extends FirebaseModule {
return new FirestoreDocumentReference(this, path);
}
enableNetwork() {
return this.native.enableNetwork();
async enableNetwork() {
await this.native.enableNetwork();
}
runTransaction(updateFunction) {