From 2555b58c47ee8c27bc3e0fa9571d2ddec1aac5d5 Mon Sep 17 00:00:00 2001 From: Wu-Hui Date: Wed, 8 Feb 2023 13:12:42 -0500 Subject: [PATCH] New way to config Firestore SDK Cache. --- packages/firestore/src/api/cache_config.ts | 152 ++++++++++++++++++ packages/firestore/src/api/database.ts | 19 +++ packages/firestore/src/api/settings.ts | 6 + .../firestore/src/core/firestore_client.ts | 46 ++++-- packages/firestore/src/lite-api/settings.ts | 11 ++ .../test/integration/util/helpers.ts | 8 +- 6 files changed, 227 insertions(+), 15 deletions(-) create mode 100644 packages/firestore/src/api/cache_config.ts diff --git a/packages/firestore/src/api/cache_config.ts b/packages/firestore/src/api/cache_config.ts new file mode 100644 index 00000000000..7bd37f71f02 --- /dev/null +++ b/packages/firestore/src/api/cache_config.ts @@ -0,0 +1,152 @@ +/** + * @license + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file 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. + */ + +import { + IndexedDbOfflineComponentProvider, + MemoryOfflineComponentProvider, + MultiTabOfflineComponentProvider, + OfflineComponentProvider, + OnlineComponentProvider +} from '../core/component_provider'; + +export interface MemoryLocalCache { + kind: 'memory'; + _onlineComponentProvider: OnlineComponentProvider; + _offlineComponentProvider: MemoryOfflineComponentProvider; +} + +class MemoryLocalCacheImpl implements MemoryLocalCache { + kind: 'memory' = 'memory'; + _onlineComponentProvider: OnlineComponentProvider; + _offlineComponentProvider: MemoryOfflineComponentProvider; + + constructor() { + this._onlineComponentProvider = new OnlineComponentProvider(); + this._offlineComponentProvider = new MemoryOfflineComponentProvider(); + } +} + +export interface IndexedDbLocalCache { + kind: 'indexeddb'; + _onlineComponentProvider: OnlineComponentProvider; + _offlineComponentProvider: OfflineComponentProvider; +} + +class IndexedDbLocalCacheImpl implements IndexedDbLocalCache { + kind: 'indexeddb' = 'indexeddb'; + _onlineComponentProvider: OnlineComponentProvider; + _offlineComponentProvider: OfflineComponentProvider; + + constructor(settings: IndexedDbSettings | undefined) { + let tabManager: IndexedDbTabManager; + if (settings?.tabManager) { + settings.tabManager.initialize(settings); + tabManager = settings.tabManager; + } else { + tabManager = indexedDbSingleTabManager(undefined); + tabManager.initialize(settings); + } + this._onlineComponentProvider = tabManager._onlineComponentProvider!; + this._offlineComponentProvider = tabManager._offlineComponentProvider!; + } +} + +export type FirestoreLocalCache = MemoryLocalCache | IndexedDbLocalCache; + +// Factory function +export function memoryLocalCache(): MemoryLocalCache { + return new MemoryLocalCacheImpl(); +} + +export interface IndexedDbSettings { + cacheSizeBytes?: number; + // default to singleTabManager({forceOwnership: false}) + tabManager?: IndexedDbTabManager; +} + +// Factory function +export function indexedDbLocalCache( + settings?: IndexedDbSettings +): IndexedDbLocalCache { + return new IndexedDbLocalCacheImpl(settings); +} + +export interface IndexedDbSingleTabManager { + kind: 'indexedDbSingleTab'; + initialize: ( + settings: Omit | undefined + ) => void; + _onlineComponentProvider?: OnlineComponentProvider; + _offlineComponentProvider?: OfflineComponentProvider; +} + +class SingleTabManagerImpl implements IndexedDbSingleTabManager { + kind: 'indexedDbSingleTab' = 'indexedDbSingleTab'; + + _onlineComponentProvider?: OnlineComponentProvider; + _offlineComponentProvider?: OfflineComponentProvider; + + constructor(private forceOwnership?: boolean) {} + + initialize( + settings: Omit | undefined + ): void { + this._onlineComponentProvider = new OnlineComponentProvider(); + this._offlineComponentProvider = new IndexedDbOfflineComponentProvider( + this._onlineComponentProvider, + settings?.cacheSizeBytes, + this.forceOwnership + ); + } +} + +export interface IndexedDbMultipleTabManager { + kind: 'IndexedDbMultipleTab'; + initialize: (settings: Omit) => void; + _onlineComponentProvider?: OnlineComponentProvider; + _offlineComponentProvider?: OfflineComponentProvider; +} + +class MultiTabManagerImpl implements IndexedDbMultipleTabManager { + kind: 'IndexedDbMultipleTab' = 'IndexedDbMultipleTab'; + + _onlineComponentProvider?: OnlineComponentProvider; + _offlineComponentProvider?: OfflineComponentProvider; + + initialize( + settings: Omit | undefined + ): void { + this._onlineComponentProvider = new OnlineComponentProvider(); + this._offlineComponentProvider = new MultiTabOfflineComponentProvider( + this._onlineComponentProvider, + settings?.cacheSizeBytes + ); + } +} + +export type IndexedDbTabManager = + | IndexedDbSingleTabManager + | IndexedDbMultipleTabManager; + +export function indexedDbSingleTabManager( + settings: { forceOwnership?: boolean } | undefined +): IndexedDbSingleTabManager { + return new SingleTabManagerImpl(settings?.forceOwnership); +} +export function indexedDbMultipleTabManager(): IndexedDbMultipleTabManager { + return new MultiTabManagerImpl(); +} diff --git a/packages/firestore/src/api/database.ts b/packages/firestore/src/api/database.ts index 29cca7e68ca..cb2f2daf373 100644 --- a/packages/firestore/src/api/database.ts +++ b/packages/firestore/src/api/database.ts @@ -283,6 +283,15 @@ export function configureFirestore(firestore: Firestore): void { firestore._queue, databaseInfo ); + if ( + settings.cache?._offlineComponentProvider && + settings.cache?._onlineComponentProvider + ) { + firestore._firestoreClient.uninitializedComponentsProvider = { + offline: settings.cache._offlineComponentProvider, + online: settings.cache._onlineComponentProvider + }; + } } /** @@ -308,6 +317,7 @@ export function configureFirestore(firestore: Firestore): void { * persistence. * @returns A `Promise` that represents successfully enabling persistent storage. */ +// TODO(wuandy): mark obselete export function enableIndexedDbPersistence( firestore: Firestore, persistenceSettings?: PersistenceSettings @@ -316,6 +326,10 @@ export function enableIndexedDbPersistence( verifyNotInitialized(firestore); const client = ensureFirestoreConfigured(firestore); + if (client.uninitializedComponentsProvider) { + throw new FirestoreError(Code.INVALID_ARGUMENT, 'Already specified.'); + } + const settings = firestore._freezeSettings(); const onlineComponentProvider = new OnlineComponentProvider(); @@ -353,6 +367,7 @@ export function enableIndexedDbPersistence( * @returns A `Promise` that represents successfully enabling persistent * storage. */ +// TODO(wuandy): mark obselete export function enableMultiTabIndexedDbPersistence( firestore: Firestore ): Promise { @@ -360,6 +375,10 @@ export function enableMultiTabIndexedDbPersistence( verifyNotInitialized(firestore); const client = ensureFirestoreConfigured(firestore); + if (client.uninitializedComponentsProvider) { + throw new FirestoreError(Code.INVALID_ARGUMENT, 'Already specified.'); + } + const settings = firestore._freezeSettings(); const onlineComponentProvider = new OnlineComponentProvider(); diff --git a/packages/firestore/src/api/settings.ts b/packages/firestore/src/api/settings.ts index f6e92854495..e56b2aa1bd9 100644 --- a/packages/firestore/src/api/settings.ts +++ b/packages/firestore/src/api/settings.ts @@ -17,6 +17,8 @@ import { FirestoreSettings as LiteSettings } from '../lite-api/settings'; +import { FirestoreLocalCache } from './cache_config'; + export { DEFAULT_HOST } from '../lite-api/settings'; /** @@ -30,6 +32,7 @@ export interface PersistenceSettings { * Workers. Setting this to `true` will enable persistence, but cause other * tabs using persistence to fail. */ + // TODO(wuandy): Deprecate this forceOwnership?: boolean; } @@ -48,8 +51,11 @@ export interface FirestoreSettings extends LiteSettings { * The default value is 40 MB. The threshold must be set to at least 1 MB, and * can be set to `CACHE_SIZE_UNLIMITED` to disable garbage collection. */ + // TODO(wuandy): Deprecate this cacheSizeBytes?: number; + cache?: FirestoreLocalCache; + /** * Forces the SDK’s underlying network transport (WebChannel) to use * long-polling. Each response from the backend will be closed immediately diff --git a/packages/firestore/src/core/firestore_client.ts b/packages/firestore/src/core/firestore_client.ts index 2c2c0af1771..67c7245c599 100644 --- a/packages/firestore/src/core/firestore_client.ts +++ b/packages/firestore/src/core/firestore_client.ts @@ -112,6 +112,10 @@ export class FirestoreClient { appCheckToken: string, user: User ) => Promise = () => Promise.resolve(); + uninitializedComponentsProvider?: { + offline: OfflineComponentProvider; + online: OnlineComponentProvider; + }; offlineComponents?: OfflineComponentProvider; onlineComponents?: OnlineComponentProvider; @@ -120,12 +124,12 @@ export class FirestoreClient { private authCredentials: CredentialsProvider, private appCheckCredentials: CredentialsProvider, /** - * Asynchronous queue responsible for all of our internal processing. When - * we get incoming work from the user (via public API) or the network - * (incoming GRPC messages), we should always schedule onto this queue. - * This ensures all of our work is properly serialized (e.g. we don't - * start processing a new operation while the previous one is waiting for - * an async I/O to complete). + * Asynchronous queue responsible for all of our internal processing. When // + * we get incoming work from the user (via public API) or the network // + * (incoming GRPC messages), we should always schedule onto this queue. // + * This ensures all of our work is properly serialized (e.g. we don't // + * start processing a new operation while the previous one is waiting for // + * an async I/O to complete). // */ public asyncQueue: AsyncQueue, private databaseInfo: DatabaseInfo @@ -265,11 +269,19 @@ async function ensureOfflineComponents( client: FirestoreClient ): Promise { if (!client.offlineComponents) { - logDebug(LOG_TAG, 'Using default OfflineComponentProvider'); - await setOfflineComponentProvider( - client, - new MemoryOfflineComponentProvider() - ); + if (client.uninitializedComponentsProvider) { + logDebug(LOG_TAG, 'Using user provided OfflineComponentProvider'); + await setOfflineComponentProvider( + client, + client.uninitializedComponentsProvider.offline + ); + } else { + logDebug(LOG_TAG, 'Using default OfflineComponentProvider'); + await setOfflineComponentProvider( + client, + new MemoryOfflineComponentProvider() + ); + } } return client.offlineComponents!; @@ -279,8 +291,16 @@ async function ensureOnlineComponents( client: FirestoreClient ): Promise { if (!client.onlineComponents) { - logDebug(LOG_TAG, 'Using default OnlineComponentProvider'); - await setOnlineComponentProvider(client, new OnlineComponentProvider()); + if (client.uninitializedComponentsProvider) { + logDebug(LOG_TAG, 'Using user provided OnlineComponentProvider'); + await setOnlineComponentProvider( + client, + client.uninitializedComponentsProvider.online + ); + } else { + logDebug(LOG_TAG, 'Using default OnlineComponentProvider'); + await setOnlineComponentProvider(client, new OnlineComponentProvider()); + } } return client.onlineComponents!; diff --git a/packages/firestore/src/lite-api/settings.ts b/packages/firestore/src/lite-api/settings.ts index 3743cc344d0..160188c33f7 100644 --- a/packages/firestore/src/lite-api/settings.ts +++ b/packages/firestore/src/lite-api/settings.ts @@ -15,6 +15,7 @@ * limitations under the License. */ +import { FirestoreLocalCache } from '../api/cache_config'; import { CredentialsSettings } from '../api/credentials'; import { LRU_COLLECTION_DISABLED, @@ -23,6 +24,7 @@ import { import { LRU_MINIMUM_CACHE_SIZE_BYTES } from '../local/lru_garbage_collector_impl'; import { Code, FirestoreError } from '../util/error'; import { validateIsNotUsedTogether } from '../util/input_validation'; +import { logWarn } from '../util/log'; // settings() defaults: export const DEFAULT_HOST = 'firestore.googleapis.com'; @@ -60,6 +62,8 @@ export interface PrivateSettings extends FirestoreSettings { experimentalAutoDetectLongPolling?: boolean; // Used in firestore@exp useFetchStreams?: boolean; + + cache?: FirestoreLocalCache; } /** @@ -83,6 +87,7 @@ export class FirestoreSettingsImpl { readonly ignoreUndefinedProperties: boolean; readonly useFetchStreams: boolean; + readonly cache?: FirestoreLocalCache; // Can be a google-auth-library or gapi client. // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -105,6 +110,12 @@ export class FirestoreSettingsImpl { this.credentials = settings.credentials; this.ignoreUndefinedProperties = !!settings.ignoreUndefinedProperties; + logWarn( + `Setting offline cache to ${JSON.stringify( + settings + )} from PrivateSettings` + ); + this.cache = settings.cache; if (settings.cacheSizeBytes === undefined) { this.cacheSizeBytes = LRU_DEFAULT_CACHE_SIZE_BYTES; diff --git a/packages/firestore/test/integration/util/helpers.ts b/packages/firestore/test/integration/util/helpers.ts index 79dbacaafa0..2624d958fa0 100644 --- a/packages/firestore/test/integration/util/helpers.ts +++ b/packages/firestore/test/integration/util/helpers.ts @@ -17,6 +17,9 @@ import { isIndexedDBAvailable } from '@firebase/util'; +import { indexedDbLocalCache } from '../../../src/api/cache_config'; +import { logWarn } from '../../../src/util/log'; + import { collection, doc, @@ -184,10 +187,11 @@ export async function withTestDbsSettings( const dbs: Firestore[] = []; for (let i = 0; i < numDbs; i++) { - const db = newTestFirestore(newTestApp(projectId), settings); + logWarn(`set persistence from helper: ${persistence}`); if (persistence) { - await enableIndexedDbPersistence(db); + settings.cache = indexedDbLocalCache(); } + const db = newTestFirestore(newTestApp(projectId), settings); dbs.push(db); }