From 0b0de45513cd3aab9f7037fd8468a63cf96aa62c Mon Sep 17 00:00:00 2001 From: Russell Wheatley Date: Tue, 13 Aug 2024 10:58:46 +0100 Subject: [PATCH] feat(firestore): support for `PersistentCacheIndexManager` (#7910) --- .../firestore/__tests__/firestore.test.ts | 46 +++++ .../ReactNativeFirebaseFirestoreModule.java | 29 +++ packages/firestore/e2e/firestore.e2e.js | 188 +++++++++++++++++- .../ios/RNFBFirestore/RNFBFirestoreModule.m | 33 +++ .../FirestorePersistentCacheIndexManager.js | 34 ++++ packages/firestore/lib/index.d.ts | 31 +++ packages/firestore/lib/index.js | 14 ++ packages/firestore/lib/modular/index.d.ts | 40 ++++ packages/firestore/lib/modular/index.js | 44 ++++ .../firestore/lib/web/RNFBFirestoreModule.js | 4 + 10 files changed, 462 insertions(+), 1 deletion(-) create mode 100644 packages/firestore/lib/FirestorePersistentCacheIndexManager.js diff --git a/packages/firestore/__tests__/firestore.test.ts b/packages/firestore/__tests__/firestore.test.ts index 423dad6faa..28e232c53f 100644 --- a/packages/firestore/__tests__/firestore.test.ts +++ b/packages/firestore/__tests__/firestore.test.ts @@ -51,6 +51,10 @@ import firestore, { deleteDoc, onSnapshot, Timestamp, + getPersistentCacheIndexManager, + deleteAllPersistentCacheIndexes, + disablePersistentCacheIndexAutoCreation, + enablePersistentCacheIndexAutoCreation, } from '../lib'; const COLLECTION = 'firestore'; @@ -629,5 +633,47 @@ describe('Firestore', function () { it('`Timestamp` is properly exposed to end user', function () { expect(Timestamp).toBeDefined(); }); + + it('`getPersistentCacheIndexManager` is properly exposed to end user', function () { + expect(getPersistentCacheIndexManager).toBeDefined(); + const indexManager = getPersistentCacheIndexManager(firebase.firestore()); + expect(indexManager!.constructor.name).toEqual('FirestorePersistentCacheIndexManager'); + }); + + it('`deleteAllPersistentCacheIndexes` is properly exposed to end user', function () { + expect(deleteAllPersistentCacheIndexes).toBeDefined(); + }); + + it('`disablePersistentCacheIndexAutoCreation` is properly exposed to end user', function () { + expect(disablePersistentCacheIndexAutoCreation).toBeDefined(); + }); + + it('`enablePersistentCacheIndexAutoCreation` is properly exposed to end user', function () { + expect(enablePersistentCacheIndexAutoCreation).toBeDefined(); + }); + }); + + describe('FirestorePersistentCacheIndexManager', function () { + it('is exposed to end user', function () { + const firestore1 = firebase.firestore(); + firestore1.settings({ persistence: true }); + const indexManager = firestore1.persistentCacheIndexManager(); + expect(indexManager).toBeDefined(); + expect(indexManager.constructor.name).toEqual('FirestorePersistentCacheIndexManager'); + + expect(indexManager.enableIndexAutoCreation).toBeInstanceOf(Function); + expect(indexManager.disableIndexAutoCreation).toBeInstanceOf(Function); + expect(indexManager.deleteAllIndexes).toBeInstanceOf(Function); + + const firestore2 = firebase.firestore(); + firestore2.settings({ persistence: false }); + + const nullIndexManager = firestore2.persistentCacheIndexManager(); + + expect(nullIndexManager).toBeNull(); + + const nullIndexManagerModular = getPersistentCacheIndexManager(firestore2); + expect(nullIndexManagerModular).toBeNull(); + }); }); }); diff --git a/packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestoreModule.java b/packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestoreModule.java index d3405533ef..b8f5ceaab4 100644 --- a/packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestoreModule.java +++ b/packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestoreModule.java @@ -20,6 +20,7 @@ import static io.invertase.firebase.common.RCTConvertFirebase.toHashMap; import static io.invertase.firebase.firestore.ReactNativeFirebaseFirestoreCommon.rejectPromiseFirestoreException; import static io.invertase.firebase.firestore.UniversalFirebaseFirestoreCommon.createFirestoreKey; +import static io.invertase.firebase.firestore.UniversalFirebaseFirestoreCommon.getFirestoreForApp; import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.Promise; @@ -29,6 +30,7 @@ import com.facebook.react.bridge.WritableMap; import com.google.firebase.firestore.FirebaseFirestore; import com.google.firebase.firestore.LoadBundleTaskProgress; +import com.google.firebase.firestore.PersistentCacheIndexManager; import io.invertase.firebase.common.ReactNativeFirebaseModule; public class ReactNativeFirebaseFirestoreModule extends ReactNativeFirebaseModule { @@ -164,6 +166,33 @@ public void terminate(String appName, String databaseId, Promise promise) { }); } + @ReactMethod + public void persistenceCacheIndexManager( + String appName, String databaseId, int requestType, Promise promise) { + PersistentCacheIndexManager indexManager = + getFirestoreForApp(appName, databaseId).getPersistentCacheIndexManager(); + if (indexManager != null) { + switch (requestType) { + case 0: + indexManager.enableIndexAutoCreation(); + break; + case 1: + indexManager.disableIndexAutoCreation(); + break; + case 2: + indexManager.deleteAllIndexes(); + break; + } + } else { + promise.reject( + "firestore/index-manager-null", + "`PersistentCacheIndexManager` is not available, persistence has not been enabled for" + + " Firestore"); + return; + } + promise.resolve(null); + } + private WritableMap taskProgressToWritableMap(LoadBundleTaskProgress progress) { WritableMap writableMap = Arguments.createMap(); writableMap.putDouble("bytesLoaded", progress.getBytesLoaded()); diff --git a/packages/firestore/e2e/firestore.e2e.js b/packages/firestore/e2e/firestore.e2e.js index 6d711fa205..da0f08ae08 100644 --- a/packages/firestore/e2e/firestore.e2e.js +++ b/packages/firestore/e2e/firestore.e2e.js @@ -114,7 +114,8 @@ describe('firestore()', function () { describe('disableNetwork() & enableNetwork()', function () { it('disables and enables with no errors', async function () { - if (Platform.other) { + if (!(Platform.android || Platform.ios)) { + // Not supported on web lite sdk return; } @@ -211,6 +212,7 @@ describe('firestore()', function () { it("handles 'estimate'", async function () { // TODO(ehesp): Figure out how to call settings on other. if (Platform.other) { + // Not supported on web lite sdk return; } @@ -237,6 +239,7 @@ describe('firestore()', function () { it("handles 'previous'", async function () { // TODO(ehesp): Figure out how to call settings on other. if (Platform.other) { + // Not supported on web lite sdk return; } @@ -331,6 +334,88 @@ describe('firestore()', function () { }); }); }); + + describe('FirestorePersistentCacheIndexManager', function () { + describe('if persistence is enabled', function () { + it('should enableIndexAutoCreation()', async function () { + if (Platform.other) { + // Not supported on web lite sdk + return; + } + + const db = firebase.firestore(); + const indexManager = db.persistentCacheIndexManager(); + await indexManager.enableIndexAutoCreation(); + }); + + it('should disableIndexAutoCreation()', async function () { + if (Platform.other) { + // Not supported on web lite sdk + return; + } + const db = firebase.firestore(); + const indexManager = db.persistentCacheIndexManager(); + await indexManager.disableIndexAutoCreation(); + }); + + it('should deleteAllIndexes()', async function () { + if (Platform.other) { + // Not supported on web lite sdk + return; + } + const db = firebase.firestore(); + const indexManager = db.persistentCacheIndexManager(); + await indexManager.deleteAllIndexes(); + }); + }); + + describe('if persistence is disabled', function () { + it('should return `null` when calling `persistentCacheIndexManager()`', async function () { + if (Platform.other) { + // Not supported on web lite sdk + return; + } + const secondFirestore = firebase.app('secondaryFromNative').firestore(); + secondFirestore.settings({ persistence: false }); + const indexManager = secondFirestore.persistentCacheIndexManager(); + should.equal(indexManager, null); + }); + }); + + describe('macOS should throw exception when calling `persistentCacheIndexManager()`', function () { + it('should throw an exception when calling PersistentCacheIndexManager API', async function () { + if (!Platform.other) { + return; + } + const db = firebase.firestore(); + const indexManager = db.persistentCacheIndexManager(); + + try { + await indexManager.enableIndexAutoCreation(); + + throw new Error('Did not throw an Error.'); + } catch (e) { + e.message.should.containEql('Not supported in the lite SDK'); + } + + try { + await indexManager.deleteAllIndexes(); + + throw new Error('Did not throw an Error.'); + } catch (e) { + e.message.should.containEql('Not supported in the lite SDK'); + } + + try { + await indexManager.disableIndexAutoCreation(); + + throw new Error('Did not throw an Error.'); + } catch (e) { + e.message.should.containEql('Not supported in the lite SDK'); + } + }); + }); + }); }); describe('modular', function () { @@ -683,5 +768,106 @@ describe('firestore()', function () { }); }); }); + + describe('FirestorePersistentCacheIndexManager', function () { + describe('if persistence is enabled', function () { + it('should enableIndexAutoCreation()', async function () { + if (Platform.other) { + // Not supported on web lite sdk + return; + } + const { + getFirestore, + getPersistentCacheIndexManager, + enablePersistentCacheIndexAutoCreation, + } = firestoreModular; + const db = getFirestore(); + const indexManager = getPersistentCacheIndexManager(db); + await enablePersistentCacheIndexAutoCreation(indexManager); + }); + + it('should disableIndexAutoCreation()', async function () { + if (Platform.other) { + // Not supported on web lite sdk + return; + } + const { + getFirestore, + getPersistentCacheIndexManager, + disablePersistentCacheIndexAutoCreation, + } = firestoreModular; + const db = getFirestore(); + const indexManager = getPersistentCacheIndexManager(db); + await disablePersistentCacheIndexAutoCreation(indexManager); + }); + + it('should deleteAllIndexes()', async function () { + if (Platform.other) { + // Not supported on web lite sdk + return; + } + const { getFirestore, getPersistentCacheIndexManager, deleteAllPersistentCacheIndexes } = + firestoreModular; + const db = getFirestore(); + const indexManager = getPersistentCacheIndexManager(db); + await deleteAllPersistentCacheIndexes(indexManager); + }); + }); + + describe('if persistence is disabled', function () { + it('should return `null` when calling `persistentCacheIndexManager()`', async function () { + if (Platform.other) { + // Not supported on web lite sdk + return; + } + const { initializeFirestore, getPersistentCacheIndexManager } = firestoreModular; + const { getApp } = modular; + const app = getApp('secondaryFromNative'); + const db = await initializeFirestore(app, { persistence: false }); + + const indexManager = getPersistentCacheIndexManager(db); + should.equal(indexManager, null); + }); + }); + + describe('macOS should throw exception when calling `persistentCacheIndexManager()`', function () { + it('should throw an exception when calling PersistentCacheIndexManager API', async function () { + if (Platform.android || Platform.ios) { + return; + } + const { + getFirestore, + getPersistentCacheIndexManager, + enablePersistentCacheIndexAutoCreation, + disablePersistentCacheIndexAutoCreation, + deleteAllPersistentCacheIndexes, + } = firestoreModular; + + const db = getFirestore(); + const indexManager = getPersistentCacheIndexManager(db); + + try { + await enablePersistentCacheIndexAutoCreation(indexManager); + throw new Error('Did not throw an Error.'); + } catch (e) { + e.message.should.containEql('Not supported in the lite SDK'); + } + + try { + await disablePersistentCacheIndexAutoCreation(indexManager); + throw new Error('Did not throw an Error.'); + } catch (e) { + e.message.should.containEql('Not supported in the lite SDK'); + } + + try { + await deleteAllPersistentCacheIndexes(indexManager); + throw new Error('Did not throw an Error.'); + } catch (e) { + e.message.should.containEql('Not supported in the lite SDK'); + } + }); + }); + }); }); }); diff --git a/packages/firestore/ios/RNFBFirestore/RNFBFirestoreModule.m b/packages/firestore/ios/RNFBFirestore/RNFBFirestoreModule.m index 2563487aeb..bdb1a1de48 100644 --- a/packages/firestore/ios/RNFBFirestore/RNFBFirestoreModule.m +++ b/packages/firestore/ios/RNFBFirestore/RNFBFirestoreModule.m @@ -17,6 +17,7 @@ #import "RNFBFirestoreModule.h" #import +#import "FirebaseFirestoreInternal/FIRPersistentCacheIndexManager.h" #import "RNFBFirestoreCommon.h" #import "RNFBPreferences.h" @@ -207,6 +208,38 @@ + (BOOL)requiresMainQueueSetup { }]; } +RCT_EXPORT_METHOD(persistenceCacheIndexManager + : (FIRApp *)firebaseApp + : (NSString *)databaseId + : (NSInteger)requestType + : (RCTPromiseResolveBlock)resolve + : (RCTPromiseRejectBlock)reject) { + FIRPersistentCacheIndexManager *persistentCacheIndexManager = + [RNFBFirestoreCommon getFirestoreForApp:firebaseApp databaseId:databaseId] + .persistentCacheIndexManager; + + if (persistentCacheIndexManager) { + switch (requestType) { + case 0: + [persistentCacheIndexManager enableIndexAutoCreation]; + break; + case 1: + [persistentCacheIndexManager disableIndexAutoCreation]; + break; + case 2: + [persistentCacheIndexManager deleteAllIndexes]; + break; + } + } else { + reject(@"firestore/index-manager-null", + @"`PersistentCacheIndexManager` is not available, persistence has not been enabled for " + @"Firestore", + nil); + return; + } + resolve(nil); +} + - (NSMutableDictionary *)taskProgressToDictionary:(FIRLoadBundleTaskProgress *)progress { NSMutableDictionary *progressMap = [[NSMutableDictionary alloc] init]; progressMap[@"bytesLoaded"] = @(progress.bytesLoaded); diff --git a/packages/firestore/lib/FirestorePersistentCacheIndexManager.js b/packages/firestore/lib/FirestorePersistentCacheIndexManager.js new file mode 100644 index 0000000000..13ccade8dd --- /dev/null +++ b/packages/firestore/lib/FirestorePersistentCacheIndexManager.js @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2016-present Invertase Limited & Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this library except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +export default class FirestorePersistentCacheIndexManager { + constructor(firestore) { + this._firestore = firestore; + } + + async enableIndexAutoCreation() { + await this._firestore.native.persistenceCacheIndexManager(0); + } + + async disableIndexAutoCreation() { + await this._firestore.native.persistenceCacheIndexManager(1); + } + + async deleteAllIndexes() { + await this._firestore.native.persistenceCacheIndexManager(2); + } +} diff --git a/packages/firestore/lib/index.d.ts b/packages/firestore/lib/index.d.ts index ed816e2cfe..261250473c 100644 --- a/packages/firestore/lib/index.d.ts +++ b/packages/firestore/lib/index.d.ts @@ -2005,6 +2005,29 @@ export namespace FirebaseFirestoreTypes { ): WriteBatch; } + /** + * Returns the PersistentCache Index Manager used by the given Firestore object. + * The PersistentCacheIndexManager instance, or null if local persistent storage is not in use. + */ + export interface PersistentCacheIndexManager { + /** + * Enables the SDK to create persistent cache indexes automatically for local query + * execution when the SDK believes cache indexes can help improves performance. + * This feature is disabled by default. + */ + enableIndexAutoCreation(): Promise; + /** + * Stops creating persistent cache indexes automatically for local query execution. + * The indexes which have been created by calling `enableIndexAutoCreation()` still take effect. + */ + disableIndexAutoCreation(): Promise; + /** + * Removes all persistent cache indexes. Note this function also deletes indexes + * generated by `setIndexConfiguration()`, which is deprecated. + */ + deleteAllIndexes(): Promise; + } + /** * Represents the state of bundle loading tasks. * @@ -2321,6 +2344,14 @@ export namespace FirebaseFirestoreTypes { * @param port: emulator port (eg, 8080) */ useEmulator(host: string, port: number): void; + + /** + * Gets the `PersistentCacheIndexManager` instance used by this Cloud Firestore instance. + * This is not the same as Cloud Firestore Indexes. + * Persistent cache indexes are optional indexes that only exist within the SDK to assist in local query execution. + * Returns `null` if local persistent storage is not in use. + */ + persistentCacheIndexManager(): PersistentCacheIndexManager | null; } /** diff --git a/packages/firestore/lib/index.js b/packages/firestore/lib/index.js index 16cd0d2ad9..08a5cfafea 100644 --- a/packages/firestore/lib/index.js +++ b/packages/firestore/lib/index.js @@ -40,6 +40,7 @@ import FirestoreTransactionHandler from './FirestoreTransactionHandler'; import FirestoreWriteBatch from './FirestoreWriteBatch'; import version from './version'; import fallBackModule from './web/RNFBFirestoreModule'; +import FirestorePersistentCacheIndexManager from './FirestorePersistentCacheIndexManager'; const namespace = 'firestore'; @@ -84,6 +85,7 @@ class FirebaseFirestoreModule extends FirebaseModule { this._settings = { ignoreUndefinedProperties: false, + persistence: true, }; } // We override the FirebaseModule's `eventNameForApp()` method to include the customUrlOrRegion @@ -363,8 +365,20 @@ class FirebaseFirestoreModule extends FirebaseModule { delete settings.ignoreUndefinedProperties; } + if (settings.persistence === false) { + // Required for persistentCacheIndexManager(), if this setting is `false`, it returns `null` + this._settings.persistence = false; + } + return this.native.settings(settings); } + + persistentCacheIndexManager() { + if (this._settings.persistence === false) { + return null; + } + return new FirestorePersistentCacheIndexManager(this); + } } // import { SDK_VERSION } from '@react-native-firebase/firestore'; diff --git a/packages/firestore/lib/modular/index.d.ts b/packages/firestore/lib/modular/index.d.ts index c1f8ef8007..179bf65a80 100644 --- a/packages/firestore/lib/modular/index.d.ts +++ b/packages/firestore/lib/modular/index.d.ts @@ -9,6 +9,7 @@ import DocumentData = FirebaseFirestoreTypes.DocumentData; import Query = FirebaseFirestoreTypes.Query; import FieldValue = FirebaseFirestoreTypes.FieldValue; import FieldPath = FirebaseFirestoreTypes.FieldPath; +import PersistentCacheIndexManager = FirebaseFirestoreTypes.PersistentCacheIndexManager; /** Primitive types. */ export type Primitive = string | number | boolean | undefined | null; @@ -545,6 +546,45 @@ export function namedQuery(firestore: Firestore, name: string): Query} - A promise that resolves when the operation is complete. + */ +export function enablePersistentCacheIndexAutoCreation( + indexManager: PersistentCacheIndexManager, +): Promise; +/** + * Stops creating persistent cache indexes automatically for local query execution. + * The indexes which have been created by calling `enableIndexAutoCreation()` still take effect. + * @param {PersistentCacheIndexManager} - The `PersistentCacheIndexManager` instance. + * @return {Promise} - A promise that resolves when the operation is complete. + */ +export function disablePersistentCacheIndexAutoCreation( + indexManager: PersistentCacheIndexManager, +): Promise; +/** + * Removes all persistent cache indexes. Note this function also deletes indexes + * generated by `setIndexConfiguration()`, which is deprecated. + * @param {PersistentCacheIndexManager} - The `PersistentCacheIndexManager` instance. + * @return {Promise} - A promise that resolves when the operation is complete. + */ +export function deleteAllPersistentCacheIndexes( + indexManager: PersistentCacheIndexManager, +): Promise; + export * from './query'; export * from './snapshot'; export * from './Bytes'; diff --git a/packages/firestore/lib/modular/index.js b/packages/firestore/lib/modular/index.js index ee6552fc5c..46eb2d8c4c 100644 --- a/packages/firestore/lib/modular/index.js +++ b/packages/firestore/lib/modular/index.js @@ -8,6 +8,7 @@ * @typedef {import('..').FirebaseFirestoreTypes.Query} Query * @typedef {import('..').FirebaseFirestoreTypes.SetOptions} SetOptions * @typedef {import('..').FirebaseFirestoreTypes.Settings} FirestoreSettings + * @typedef {import('..').FirebaseFirestoreTypes.PersistentCacheIndexManager} PersistentCacheIndexManager * @typedef {import('@firebase/app').FirebaseApp} FirebaseApp */ @@ -217,6 +218,49 @@ export function writeBatch(firestore) { return firestore.batch(); } +/** + * Gets the `PersistentCacheIndexManager` instance used by this Cloud Firestore instance. + * This is not the same as Cloud Firestore Indexes. + * Persistent cache indexes are optional indexes that only exist within the SDK to assist in local query execution. + * Returns `null` if local persistent storage is not in use. + * @param {Firestore} firestore + * @returns {PersistentCacheIndexManager | null} + */ +export function getPersistentCacheIndexManager(firestore) { + return firestore.persistentCacheIndexManager(); +} + +/** + * Enables the SDK to create persistent cache indexes automatically for local query + * execution when the SDK believes cache indexes can help improves performance. + * This feature is disabled by default. + * @param {PersistentCacheIndexManager} indexManager + * @returns {Promise