diff --git a/packages/firestore/src/api/persistent_cache_index_manager.ts b/packages/firestore/src/api/persistent_cache_index_manager.ts index 0e7f4d17614..94f47d72539 100644 --- a/packages/firestore/src/api/persistent_cache_index_manager.ts +++ b/packages/firestore/src/api/persistent_cache_index_manager.ts @@ -16,9 +16,10 @@ */ import { + FirestoreClient, firestoreClientDeleteAllFieldIndexes, - firestoreClientSetPersistentCacheIndexAutoCreationEnabled, - FirestoreClient + firestoreClientDisablePersistentCacheIndexAutoCreation, + firestoreClientEnablePersistentCacheIndexAutoCreation } from '../core/firestore_client'; import { cast } from '../util/input_validation'; import { logDebug, logWarn } from '../util/log'; @@ -76,7 +77,12 @@ export function getPersistentCacheIndexManager( export function enablePersistentCacheIndexAutoCreation( indexManager: PersistentCacheIndexManager ): void { - setPersistentCacheIndexAutoCreationEnabled(indexManager, true); + indexManager._client.verifyNotTerminated(); + firestoreClientEnablePersistentCacheIndexAutoCreation(indexManager._client) + .then(_ => logDebug('enablePersistentCacheIndexAutoCreation() succeeded.')) + .catch(error => + logWarn('enablePersistentCacheIndexAutoCreation() failed', error) + ); } /** @@ -87,7 +93,12 @@ export function enablePersistentCacheIndexAutoCreation( export function disablePersistentCacheIndexAutoCreation( indexManager: PersistentCacheIndexManager ): void { - setPersistentCacheIndexAutoCreationEnabled(indexManager, false); + indexManager._client.verifyNotTerminated(); + firestoreClientDisablePersistentCacheIndexAutoCreation(indexManager._client) + .then(_ => logDebug('disablePersistentCacheIndexAutoCreation() succeeded.')) + .catch(error => + logWarn('disablePersistentCacheIndexAutoCreation() failed', error) + ); } /** @@ -100,41 +111,9 @@ export function deleteAllPersistentCacheIndexes( indexManager: PersistentCacheIndexManager ): void { indexManager._client.verifyNotTerminated(); - - const promise = firestoreClientDeleteAllFieldIndexes(indexManager._client); - - promise - .then(_ => logDebug('deleting all persistent cache indexes succeeded')) - .catch(error => - logWarn('deleting all persistent cache indexes failed', error) - ); -} - -function setPersistentCacheIndexAutoCreationEnabled( - indexManager: PersistentCacheIndexManager, - isEnabled: boolean -): void { - indexManager._client.verifyNotTerminated(); - - const promise = firestoreClientSetPersistentCacheIndexAutoCreationEnabled( - indexManager._client, - isEnabled - ); - - promise - .then(_ => - logDebug( - `setting persistent cache index auto creation ` + - `isEnabled=${isEnabled} succeeded` - ) - ) - .catch(error => - logWarn( - `setting persistent cache index auto creation ` + - `isEnabled=${isEnabled} failed`, - error - ) - ); + firestoreClientDeleteAllFieldIndexes(indexManager._client) + .then(_ => logDebug('deleteAllPersistentCacheIndexes() succeeded.')) + .catch(error => logWarn('deleteAllPersistentCacheIndexes() failed', error)); } /** diff --git a/packages/firestore/src/core/component_provider.ts b/packages/firestore/src/core/component_provider.ts index 618653b9237..ae53a3def13 100644 --- a/packages/firestore/src/core/component_provider.ts +++ b/packages/firestore/src/core/component_provider.ts @@ -22,8 +22,8 @@ import { IndexBackfillerScheduler } from '../local/index_backfiller'; import { - indexedDbStoragePrefix, - IndexedDbPersistence + IndexedDbPersistence, + indexedDbStoragePrefix } from '../local/indexeddb_persistence'; import { LocalStore } from '../local/local_store'; import { newLocalStore } from '../local/local_store_impl'; @@ -34,7 +34,7 @@ import { MemoryLruDelegate, MemoryPersistence } from '../local/memory_persistence'; -import { Scheduler, Persistence } from '../local/persistence'; +import { Persistence, Scheduler } from '../local/persistence'; import { QueryEngine } from '../local/query_engine'; import { ClientId, @@ -57,6 +57,7 @@ import { JsonProtoSerializer } from '../remote/serializer'; import { hardAssert } from '../util/assert'; import { AsyncQueue } from '../util/async_queue'; import { Code, FirestoreError } from '../util/error'; +import { logDebug } from '../util/log'; import { DatabaseInfo } from './database_info'; import { EventManager, newEventManager } from './event_manager'; @@ -90,6 +91,7 @@ export interface ComponentConfiguration { * cache. Implementations override `initialize()` to provide all components. */ export interface OfflineComponentProvider { + asyncQueue: AsyncQueue; persistence: Persistence; sharedClientState: SharedClientState; localStore: LocalStore; @@ -109,16 +111,23 @@ export interface OfflineComponentProvider { export class MemoryOfflineComponentProvider implements OfflineComponentProvider { + asyncQueue!: AsyncQueue; persistence!: Persistence; sharedClientState!: SharedClientState; localStore!: LocalStore; gcScheduler!: Scheduler | null; - indexBackfillerScheduler!: Scheduler | null; + indexBackfillerScheduler: Scheduler | null = null; synchronizeTabs = false; serializer!: JsonProtoSerializer; + get schedulers(): Scheduler[] { + const schedulers = [this.gcScheduler, this.indexBackfillerScheduler]; + return schedulers.filter(scheduler => !!scheduler) as Scheduler[]; + } + async initialize(cfg: ComponentConfiguration): Promise { + this.asyncQueue = cfg.asyncQueue; this.serializer = newSerializer(cfg.databaseInfo.databaseId); this.sharedClientState = this.createSharedClientState(cfg); this.persistence = this.createPersistence(cfg); @@ -128,10 +137,6 @@ export class MemoryOfflineComponentProvider cfg, this.localStore ); - this.indexBackfillerScheduler = this.createIndexBackfillerScheduler( - cfg, - this.localStore - ); } createGarbageCollectionScheduler( @@ -141,13 +146,6 @@ export class MemoryOfflineComponentProvider return null; } - createIndexBackfillerScheduler( - cfg: ComponentConfiguration, - localStore: LocalStore - ): Scheduler | null { - return null; - } - createLocalStore(cfg: ComponentConfiguration): LocalStore { return newLocalStore( this.persistence, @@ -166,8 +164,9 @@ export class MemoryOfflineComponentProvider } async terminate(): Promise { - this.gcScheduler?.stop(); - this.indexBackfillerScheduler?.stop(); + for (const scheduler of this.schedulers) { + scheduler.stop(); + } this.sharedClientState.shutdown(); await this.persistence.shutdown(); } @@ -215,6 +214,8 @@ export class IndexedDbOfflineComponentProvider extends MemoryOfflineComponentPro indexBackfillerScheduler!: Scheduler | null; synchronizeTabs = false; + private primaryStateListenerNotified = false; + constructor( protected readonly onlineComponentProvider: OnlineComponentProvider, protected readonly cacheSizeBytes: number | undefined, @@ -237,19 +238,30 @@ export class IndexedDbOfflineComponentProvider extends MemoryOfflineComponentPro // NOTE: This will immediately call the listener, so we make sure to // set it after localStore / remoteStore are started. await this.persistence.setPrimaryStateListener(() => { - if (this.gcScheduler && !this.gcScheduler.started) { - this.gcScheduler.start(); - } - if ( - this.indexBackfillerScheduler && - !this.indexBackfillerScheduler.started - ) { - this.indexBackfillerScheduler.start(); - } + this.primaryStateListenerNotified = true; + this.startSchedulers(); return Promise.resolve(); }); } + private startSchedulers(): void { + if (!this.primaryStateListenerNotified) { + return; + } + + for (const scheduler of this.schedulers) { + if (!scheduler.started) { + scheduler.start(); + } + } + } + + installIndexBackfillerScheduler(scheduler: IndexBackfillerScheduler): void { + hardAssert(!this.indexBackfillerScheduler); + this.indexBackfillerScheduler = scheduler; + this.startSchedulers(); + } + createLocalStore(cfg: ComponentConfiguration): LocalStore { return newLocalStore( this.persistence, @@ -268,14 +280,6 @@ export class IndexedDbOfflineComponentProvider extends MemoryOfflineComponentPro return new LruScheduler(garbageCollector, cfg.asyncQueue, localStore); } - createIndexBackfillerScheduler( - cfg: ComponentConfiguration, - localStore: LocalStore - ): Scheduler | null { - const indexBackfiller = new IndexBackfiller(localStore, this.persistence); - return new IndexBackfillerScheduler(cfg.asyncQueue, indexBackfiller); - } - createPersistence(cfg: ComponentConfiguration): IndexedDbPersistence { const persistenceKey = indexedDbStoragePrefix( cfg.databaseInfo.databaseId, @@ -305,6 +309,30 @@ export class IndexedDbOfflineComponentProvider extends MemoryOfflineComponentPro } } +export function indexedDbOfflineComponentProviderInstallFieldIndexPlugin( + componentProvider: IndexedDbOfflineComponentProvider +): void { + if (componentProvider.indexBackfillerScheduler) { + return; + } + + logDebug( + 'Installing IndexBackfillerScheduler into OfflineComponentProvider ' + + 'to support persistent cache indexing.' + ); + + const indexBackfiller = new IndexBackfiller( + componentProvider.localStore, + componentProvider.persistence + ); + const scheduler = new IndexBackfillerScheduler( + componentProvider.asyncQueue, + indexBackfiller + ); + + componentProvider.installIndexBackfillerScheduler(scheduler); +} + /** * Provides all components needed for Firestore with multi-tab IndexedDB * persistence. @@ -316,6 +344,8 @@ export class IndexedDbOfflineComponentProvider extends MemoryOfflineComponentPro export class MultiTabOfflineComponentProvider extends IndexedDbOfflineComponentProvider { synchronizeTabs = true; + private isPrimary: boolean | null = null; + constructor( protected readonly onlineComponentProvider: OnlineComponentProvider, protected readonly cacheSizeBytes: number | undefined @@ -346,27 +376,33 @@ export class MultiTabOfflineComponentProvider extends IndexedDbOfflineComponentP // NOTE: This will immediately call the listener, so we make sure to // set it after localStore / remoteStore are started. await this.persistence.setPrimaryStateListener(async isPrimary => { + this.isPrimary = isPrimary; + await syncEngineApplyPrimaryState( this.onlineComponentProvider.syncEngine, isPrimary ); - if (this.gcScheduler) { - if (isPrimary && !this.gcScheduler.started) { - this.gcScheduler.start(); - } else if (!isPrimary) { - this.gcScheduler.stop(); - } - } - if (this.indexBackfillerScheduler) { - if (isPrimary && !this.indexBackfillerScheduler.started) { - this.indexBackfillerScheduler.start(); - } else if (!isPrimary) { - this.indexBackfillerScheduler.stop(); - } - } + + this.startOrStopSchedulers(); }); } + private startOrStopSchedulers(): void { + for (const scheduler of this.schedulers) { + if (this.isPrimary === true && !scheduler.started) { + scheduler.start(); + } else if (this.isPrimary === false) { + scheduler.stop(); + } + } + } + + installIndexBackfillerScheduler(scheduler: IndexBackfillerScheduler): void { + hardAssert(!this.indexBackfillerScheduler); + this.indexBackfillerScheduler = scheduler; + this.startOrStopSchedulers(); + } + createSharedClientState(cfg: ComponentConfiguration): SharedClientState { const window = getWindow(); if (!WebStorageSharedClientState.isAvailable(window)) { diff --git a/packages/firestore/src/core/firestore_client.ts b/packages/firestore/src/core/firestore_client.ts index d1b66d86e2f..c563a4c7f77 100644 --- a/packages/firestore/src/core/firestore_client.ts +++ b/packages/firestore/src/core/firestore_client.ts @@ -27,11 +27,12 @@ import { LocalStore } from '../local/local_store'; import { localStoreConfigureFieldIndexes, localStoreDeleteAllFieldIndexes, + localStoreDisableIndexAutoCreation, + localStoreEnableIndexAutoCreation, localStoreExecuteQuery, localStoreGetNamedQuery, localStoreHandleUserChange, - localStoreReadDocument, - localStoreSetIndexAutoCreationEnabled + localStoreReadDocument } from '../local/local_store_impl'; import { Persistence } from '../local/persistence'; import { Document } from '../model/document'; @@ -50,7 +51,7 @@ import { remoteStoreHandleCredentialChange } from '../remote/remote_store'; import { JsonProtoSerializer } from '../remote/serializer'; -import { debugAssert } from '../util/assert'; +import { debugAssert, hardAssert } from '../util/assert'; import { AsyncObserver } from '../util/async_observer'; import { AsyncQueue, wrapInUserErrorIfRecoverable } from '../util/async_queue'; import { BundleReader } from '../util/bundle_reader'; @@ -64,6 +65,8 @@ import { Aggregate } from './aggregate'; import { NamedQuery } from './bundle'; import { ComponentConfiguration, + IndexedDbOfflineComponentProvider, + indexedDbOfflineComponentProviderInstallFieldIndexPlugin, MemoryOfflineComponentProvider, OfflineComponentProvider, OnlineComponentProvider @@ -817,27 +820,46 @@ function createBundleReader( return newBundleReader(toByteStreamReader(content), serializer); } +function firestoreClientInstallFieldIndexPlugins( + client: FirestoreClient +): void { + hardAssert( + client._offlineComponents instanceof IndexedDbOfflineComponentProvider + ); + indexedDbOfflineComponentProviderInstallFieldIndexPlugin( + client._offlineComponents + ); +} + export function firestoreClientSetIndexConfiguration( client: FirestoreClient, indexes: FieldIndex[] ): Promise { - return client.asyncQueue.enqueue(async () => { - return localStoreConfigureFieldIndexes( - await getLocalStore(client), - indexes - ); - }); + return client.asyncQueue + .enqueue(async () => { + return localStoreConfigureFieldIndexes( + await getLocalStore(client), + indexes + ); + }) + .then(() => firestoreClientInstallFieldIndexPlugins(client)); } -export function firestoreClientSetPersistentCacheIndexAutoCreationEnabled( - client: FirestoreClient, - isEnabled: boolean +export function firestoreClientEnablePersistentCacheIndexAutoCreation( + client: FirestoreClient +): Promise { + return client.asyncQueue + .enqueue(async () => { + return localStoreEnableIndexAutoCreation(await getLocalStore(client)); + }) + .then(() => firestoreClientInstallFieldIndexPlugins(client)); +} + +export function firestoreClientDisablePersistentCacheIndexAutoCreation( + client: FirestoreClient ): Promise { return client.asyncQueue.enqueue(async () => { - return localStoreSetIndexAutoCreationEnabled( - await getLocalStore(client), - isEnabled - ); + return localStoreDisableIndexAutoCreation(await getLocalStore(client)); }); } diff --git a/packages/firestore/src/local/index_backfiller.ts b/packages/firestore/src/local/index_backfiller.ts index 1b3b16e288f..fb5a50386f9 100644 --- a/packages/firestore/src/local/index_backfiller.ts +++ b/packages/firestore/src/local/index_backfiller.ts @@ -133,13 +133,19 @@ export class IndexBackfiller { transation: PersistenceTransaction, maxDocumentsToProcess: number ): PersistencePromise { + const fieldIndexPlugin = this.localStore.indexManager.fieldIndexPlugin; + debugAssert( + !!fieldIndexPlugin, + 'localStore.indexManager.fieldIndexPlugin must not be null' + ); + const processedCollectionGroups = new Set(); let documentsRemaining = maxDocumentsToProcess; let continueLoop = true; return PersistencePromise.doWhile( () => continueLoop === true && documentsRemaining > 0, () => { - return this.localStore.indexManager + return fieldIndexPlugin .getNextCollectionGroupToUpdate(transation) .next((collectionGroup: string | null) => { if ( @@ -171,8 +177,14 @@ export class IndexBackfiller { collectionGroup: string, documentsRemainingUnderCap: number ): PersistencePromise { + const fieldIndexPlugin = this.localStore.indexManager.fieldIndexPlugin; + debugAssert( + !!fieldIndexPlugin, + 'localStore.indexManager.fieldIndexPlugin must not be null' + ); + // Use the earliest offset of all field indexes to query the local cache. - return this.localStore.indexManager + return fieldIndexPlugin .getMinOffsetFromCollectionGroup(transaction, collectionGroup) .next(existingOffset => this.localStore.localDocuments @@ -184,12 +196,12 @@ export class IndexBackfiller { ) .next(nextBatch => { const docs: DocumentMap = nextBatch.changes; - return this.localStore.indexManager + return fieldIndexPlugin .updateIndexEntries(transaction, docs) .next(() => this.getNewOffset(existingOffset, nextBatch)) .next(newOffset => { logDebug(LOG_TAG, `Updating offset: ${newOffset}`); - return this.localStore.indexManager.updateCollectionGroup( + return fieldIndexPlugin.updateCollectionGroup( transaction, collectionGroup, newOffset diff --git a/packages/firestore/src/local/index_manager.ts b/packages/firestore/src/local/index_manager.ts index d81693acc60..019282a4fb4 100644 --- a/packages/firestore/src/local/index_manager.ts +++ b/packages/firestore/src/local/index_manager.ts @@ -85,6 +85,15 @@ export interface IndexManager { collectionId: string ): PersistencePromise; + /** + * The plugin that implements the logic for field indexes; may be null if + * it has not been installed into this object, or if the implementation does + * not support the plugin. + */ + readonly fieldIndexPlugin: IndexManagerFieldIndexPlugin | null; +} + +export interface IndexManagerFieldIndexPlugin { /** * Adds a field path index. * @@ -105,11 +114,6 @@ export interface IndexManager { index: FieldIndex ): PersistencePromise; - /** Removes all field indexes and deletes all index values. */ - deleteAllFieldIndexes( - transaction: PersistenceTransaction - ): PersistencePromise; - /** Creates a full matched field index which serves the given target. */ createTargetIndexes( transaction: PersistenceTransaction, diff --git a/packages/firestore/src/local/indexeddb_index_manager.ts b/packages/firestore/src/local/indexeddb_index_manager.ts index 04a380601b3..3d4a1a0001a 100644 --- a/packages/firestore/src/local/indexeddb_index_manager.ts +++ b/packages/firestore/src/local/indexeddb_index_manager.ts @@ -69,7 +69,11 @@ import { decodeResourcePath, encodeResourcePath } from './encoded_resource_path'; -import { IndexManager, IndexType } from './index_manager'; +import { + IndexManager, + IndexManagerFieldIndexPlugin, + IndexType +} from './index_manager'; import { DbCollectionParent, DbIndexConfiguration, @@ -122,19 +126,24 @@ export class IndexedDbIndexManager implements IndexManager { private readonly uid: string; - /** - * Maps from a target to its equivalent list of sub-targets. Each sub-target - * contains only one term from the target's disjunctive normal form (DNF). - */ - private targetToDnfSubTargets = new ObjectMap( - t => canonifyTarget(t), - (l, r) => targetEquals(l, r) - ); - constructor(user: User, private readonly databaseId: DatabaseId) { this.uid = user.uid || ''; } + private _fieldIndexPlugin: IndexedDbIndexManagerFieldIndexPlugin | null = + null; + + get fieldIndexPlugin(): IndexedDbIndexManagerFieldIndexPlugin | null { + return this._fieldIndexPlugin; + } + + installFieldIndexPlugin( + factory: IndexedDbIndexManagerFieldIndexPluginConstructor + ): void { + hardAssert(!this._fieldIndexPlugin); + this._fieldIndexPlugin = new factory(this.uid, this.databaseId); + } + /** * Adds a new entry to the collection parent index. * @@ -193,6 +202,42 @@ export class IndexedDbIndexManager implements IndexManager { return parentPaths; }); } +} + +export function indexedDbIndexManagerInstallFieldIndexPlugin( + instance: IndexedDbIndexManager +): void { + if (instance.fieldIndexPlugin) { + return; + } + + logDebug( + 'Installing IndexedDbIndexManagerFieldIndexPlugin into ' + + 'IndexedDbIndexManager to support persistent cache indexing.' + ); + instance.installFieldIndexPlugin(IndexedDbIndexManagerFieldIndexPlugin); +} + +interface IndexedDbIndexManagerFieldIndexPluginConstructor { + new ( + uid: string, + databaseId: DatabaseId + ): IndexedDbIndexManagerFieldIndexPlugin; +} + +export class IndexedDbIndexManagerFieldIndexPlugin + implements IndexManagerFieldIndexPlugin +{ + constructor(readonly uid: string, readonly databaseId: DatabaseId) {} + + /** + * Maps from a target to its equivalent list of sub-targets. Each sub-target + * contains only one term from the target's disjunctive normal form (DNF). + */ + private targetToDnfSubTargets = new ObjectMap( + t => canonifyTarget(t), + (l, r) => targetEquals(l, r) + ); addFieldIndex( transaction: PersistenceTransaction, @@ -252,19 +297,6 @@ export class IndexedDbIndexManager implements IndexManager { ); } - deleteAllFieldIndexes( - transaction: PersistenceTransaction - ): PersistencePromise { - const indexes = indexConfigurationStore(transaction); - const entries = indexEntriesStore(transaction); - const states = indexStateStore(transaction); - - return indexes - .deleteAll() - .next(() => entries.deleteAll()) - .next(() => states.deleteAll()); - } - createTargetIndexes( transaction: PersistenceTransaction, target: Target @@ -1134,3 +1166,21 @@ function getMinOffsetFromFieldIndexes(fieldIndexes: FieldIndex[]): IndexOffset { } return new IndexOffset(minOffset.readTime, minOffset.documentKey, maxBatchId); } + +// Make deleteAllFieldIndexes() a free function instead of a member function of +// IndexedDbIndexManagerFieldIndexPlugin so that JavaScript bundlers can +// tree-shake away the rest of IndexedDbIndexManagerFieldIndexPlugin if this +// function is the only one that is used. +/** Removes all field indexes and deletes all index values. */ +export function deleteAllFieldIndexes( + transaction: PersistenceTransaction +): PersistencePromise { + const indexes = indexConfigurationStore(transaction); + const entries = indexEntriesStore(transaction); + const states = indexStateStore(transaction); + + return indexes + .deleteAll() + .next(() => entries.deleteAll()) + .next(() => states.deleteAll()); +} diff --git a/packages/firestore/src/local/local_store_impl.ts b/packages/firestore/src/local/local_store_impl.ts index 56f2b96f8d1..b14817038e3 100644 --- a/packages/firestore/src/local/local_store_impl.ts +++ b/packages/firestore/src/local/local_store_impl.ts @@ -45,8 +45,8 @@ import { newIndexOffsetSuccessorFromReadTime } from '../model/field_index'; import { - mutationExtractBaseValue, Mutation, + mutationExtractBaseValue, PatchMutation, Precondition } from '../model/mutation'; @@ -71,6 +71,11 @@ import { BATCHID_UNKNOWN } from '../util/types'; import { BundleCache } from './bundle_cache'; import { DocumentOverlayCache } from './document_overlay_cache'; import { IndexManager } from './index_manager'; +import { + deleteAllFieldIndexes, + IndexedDbIndexManager, + indexedDbIndexManagerInstallFieldIndexPlugin +} from './indexeddb_index_manager'; import { IndexedDbMutationQueue } from './indexeddb_mutation_queue'; import { IndexedDbPersistence } from './indexeddb_persistence'; import { IndexedDbTargetCache } from './indexeddb_target_cache'; @@ -83,7 +88,11 @@ import { MutationQueue } from './mutation_queue'; import { Persistence } from './persistence'; import { PersistencePromise } from './persistence_promise'; import { PersistenceTransaction } from './persistence_transaction'; -import { QueryEngine } from './query_engine'; +import { + QueryEngine, + QueryEngineFieldIndexPlugin, + queryEngineInstallFieldIndexPlugin +} from './query_engine'; import { RemoteDocumentCache } from './remote_document_cache'; import { RemoteDocumentChangeBuffer } from './remote_document_change_buffer'; import { ClientId } from './shared_client_state'; @@ -1493,18 +1502,33 @@ export async function localStoreSaveNamedQuery( ); } +export function localStoreInstallFieldIndexPlugins( + localStore: LocalStore +): QueryEngineFieldIndexPlugin { + const localStoreImpl = debugCast(localStore, LocalStoreImpl); + + const indexManager = localStoreImpl.indexManager; + hardAssert(indexManager instanceof IndexedDbIndexManager); + indexedDbIndexManagerInstallFieldIndexPlugin(indexManager); + + return queryEngineInstallFieldIndexPlugin(localStoreImpl.queryEngine); +} + export async function localStoreConfigureFieldIndexes( localStore: LocalStore, newFieldIndexes: FieldIndex[] ): Promise { + localStoreInstallFieldIndexPlugins(localStore); const localStoreImpl = debugCast(localStore, LocalStoreImpl); - const indexManager = localStoreImpl.indexManager; + const fieldIndexPlugin = localStoreImpl.indexManager.fieldIndexPlugin; + hardAssert(!!fieldIndexPlugin); + const promises: Array> = []; return localStoreImpl.persistence.runTransaction( 'Configure indexes', 'readwrite', transaction => - indexManager + fieldIndexPlugin .getFieldIndexes(transaction) .next(oldFieldIndexes => diffArrays( @@ -1513,12 +1537,12 @@ export async function localStoreConfigureFieldIndexes( fieldIndexSemanticComparator, fieldIndex => { promises.push( - indexManager.addFieldIndex(transaction, fieldIndex) + fieldIndexPlugin.addFieldIndex(transaction, fieldIndex) ); }, fieldIndex => { promises.push( - indexManager.deleteFieldIndex(transaction, fieldIndex) + fieldIndexPlugin.deleteFieldIndex(transaction, fieldIndex) ); } ) @@ -1527,49 +1551,33 @@ export async function localStoreConfigureFieldIndexes( ); } -export function localStoreSetIndexAutoCreationEnabled( - localStore: LocalStore, - isEnabled: boolean +export function localStoreEnableIndexAutoCreation( + localStore: LocalStore +): void { + localStoreInstallFieldIndexPlugins(localStore); + const localStoreImpl = debugCast(localStore, LocalStoreImpl); + const fieldIndexPlugin = localStoreImpl.queryEngine.fieldIndexPlugin; + hardAssert(!!fieldIndexPlugin); + fieldIndexPlugin.indexAutoCreationEnabled = true; +} + +export function localStoreDisableIndexAutoCreation( + localStore: LocalStore ): void { const localStoreImpl = debugCast(localStore, LocalStoreImpl); - localStoreImpl.queryEngine.indexAutoCreationEnabled = isEnabled; + const fieldIndexPlugin = localStoreImpl.queryEngine.fieldIndexPlugin; + if (fieldIndexPlugin) { + fieldIndexPlugin.indexAutoCreationEnabled = false; + } } export function localStoreDeleteAllFieldIndexes( localStore: LocalStore ): Promise { const localStoreImpl = debugCast(localStore, LocalStoreImpl); - const indexManager = localStoreImpl.indexManager; return localStoreImpl.persistence.runTransaction( 'Delete All Indexes', 'readwrite', - transaction => indexManager.deleteAllFieldIndexes(transaction) + transaction => deleteAllFieldIndexes(transaction) ); } - -/** - * Test-only hooks into the SDK for use exclusively by tests. - */ -export class TestingHooks { - private constructor() { - throw new Error('creating instances is not supported'); - } - - static setIndexAutoCreationSettings( - localStore: LocalStore, - settings: { - indexAutoCreationMinCollectionSize?: number; - relativeIndexReadCostPerDocument?: number; - } - ): void { - const localStoreImpl = debugCast(localStore, LocalStoreImpl); - if (settings.indexAutoCreationMinCollectionSize !== undefined) { - localStoreImpl.queryEngine.indexAutoCreationMinCollectionSize = - settings.indexAutoCreationMinCollectionSize; - } - if (settings.relativeIndexReadCostPerDocument !== undefined) { - localStoreImpl.queryEngine.relativeIndexReadCostPerDocument = - settings.relativeIndexReadCostPerDocument; - } - } -} diff --git a/packages/firestore/src/local/memory_index_manager.ts b/packages/firestore/src/local/memory_index_manager.ts index 2153cd197b7..2d4878ed6a6 100644 --- a/packages/firestore/src/local/memory_index_manager.ts +++ b/packages/firestore/src/local/memory_index_manager.ts @@ -15,15 +15,11 @@ * limitations under the License. */ -import { Target } from '../core/target'; -import { DocumentMap } from '../model/collections'; -import { DocumentKey } from '../model/document_key'; -import { FieldIndex, IndexOffset } from '../model/field_index'; import { ResourcePath } from '../model/path'; import { debugAssert } from '../util/assert'; import { SortedSet } from '../util/sorted_set'; -import { IndexManager, IndexType } from './index_manager'; +import { IndexManager } from './index_manager'; import { PersistencePromise } from './persistence_promise'; import { PersistenceTransaction } from './persistence_transaction'; @@ -50,98 +46,7 @@ export class MemoryIndexManager implements IndexManager { ); } - addFieldIndex( - transaction: PersistenceTransaction, - index: FieldIndex - ): PersistencePromise { - // Field indices are not supported with memory persistence. - return PersistencePromise.resolve(); - } - - deleteFieldIndex( - transaction: PersistenceTransaction, - index: FieldIndex - ): PersistencePromise { - // Field indices are not supported with memory persistence. - return PersistencePromise.resolve(); - } - - deleteAllFieldIndexes( - transaction: PersistenceTransaction - ): PersistencePromise { - // Field indices are not supported with memory persistence. - return PersistencePromise.resolve(); - } - - createTargetIndexes( - transaction: PersistenceTransaction, - target: Target - ): PersistencePromise { - // Field indices are not supported with memory persistence. - return PersistencePromise.resolve(); - } - - getDocumentsMatchingTarget( - transaction: PersistenceTransaction, - target: Target - ): PersistencePromise { - // Field indices are not supported with memory persistence. - return PersistencePromise.resolve(null); - } - - getIndexType( - transaction: PersistenceTransaction, - target: Target - ): PersistencePromise { - // Field indices are not supported with memory persistence. - return PersistencePromise.resolve(IndexType.NONE); - } - - getFieldIndexes( - transaction: PersistenceTransaction, - collectionGroup?: string - ): PersistencePromise { - // Field indices are not supported with memory persistence. - return PersistencePromise.resolve([]); - } - - getNextCollectionGroupToUpdate( - transaction: PersistenceTransaction - ): PersistencePromise { - // Field indices are not supported with memory persistence. - return PersistencePromise.resolve(null); - } - - getMinOffset( - transaction: PersistenceTransaction, - target: Target - ): PersistencePromise { - return PersistencePromise.resolve(IndexOffset.min()); - } - - getMinOffsetFromCollectionGroup( - transaction: PersistenceTransaction, - collectionGroup: string - ): PersistencePromise { - return PersistencePromise.resolve(IndexOffset.min()); - } - - updateCollectionGroup( - transaction: PersistenceTransaction, - collectionGroup: string, - offset: IndexOffset - ): PersistencePromise { - // Field indices are not supported with memory persistence. - return PersistencePromise.resolve(); - } - - updateIndexEntries( - transaction: PersistenceTransaction, - documents: DocumentMap - ): PersistencePromise { - // Field indices are not supported with memory persistence. - return PersistencePromise.resolve(); - } + readonly fieldIndexPlugin = null; } /** diff --git a/packages/firestore/src/local/query_engine.ts b/packages/firestore/src/local/query_engine.ts index 60fb056d1c3..3b25d0bc3a6 100644 --- a/packages/firestore/src/local/query_engine.ts +++ b/packages/firestore/src/local/query_engine.ts @@ -37,12 +37,16 @@ import { INITIAL_LARGEST_BATCH_ID, newIndexOffsetSuccessorFromReadTime } from '../model/field_index'; -import { debugAssert } from '../util/assert'; +import { debugAssert, hardAssert } from '../util/assert'; import { getLogLevel, logDebug, LogLevel } from '../util/log'; import { Iterable } from '../util/misc'; import { SortedSet } from '../util/sorted_set'; -import { IndexManager, IndexType } from './index_manager'; +import { + IndexManager, + IndexManagerFieldIndexPlugin, + IndexType +} from './index_manager'; import { LocalDocumentsView } from './local_documents_view'; import { PersistencePromise } from './persistence_promise'; import { PersistenceTransaction } from './persistence_transaction'; @@ -103,18 +107,6 @@ export class QueryEngine { private indexManager!: IndexManager; private initialized = false; - indexAutoCreationEnabled = false; - - /** - * SDK only decides whether it should create index when collection size is - * larger than this. - */ - indexAutoCreationMinCollectionSize = - DEFAULT_INDEX_AUTO_CREATION_MIN_COLLECTION_SIZE; - - relativeIndexReadCostPerDocument = - DEFAULT_RELATIVE_INDEX_READ_COST_PER_DOCUMENT; - /** Sets the document view to query against. */ initialize( localDocuments: LocalDocumentsView, @@ -125,6 +117,29 @@ export class QueryEngine { this.initialized = true; } + private get indexAutoCreationEnabled(): boolean { + return this._fieldIndexPlugin + ? this._fieldIndexPlugin.indexAutoCreationEnabled + : false; + } + + private _fieldIndexPlugin: QueryEngineFieldIndexPlugin | null = null; + + get fieldIndexPlugin(): QueryEngineFieldIndexPlugin | null { + return this._fieldIndexPlugin; + } + + installFieldIndexPlugin( + factory: QueryEngineFieldIndexPluginConstructor + ): QueryEngineFieldIndexPlugin { + hardAssert(!this._fieldIndexPlugin); + this._fieldIndexPlugin = new factory( + this.indexManager, + this.localDocumentsView + ); + return this._fieldIndexPlugin; + } + /** Returns all local documents matching the specified query. */ getDocumentsMatchingQuery( transaction: PersistenceTransaction, @@ -178,69 +193,178 @@ export class QueryEngine { .next(() => queryResult.result!); } - createCacheIndexes( + private createCacheIndexes( transaction: PersistenceTransaction, query: Query, context: QueryContext, resultSize: number ): PersistencePromise { - if (context.documentReadCount < this.indexAutoCreationMinCollectionSize) { - if (getLogLevel() <= LogLevel.DEBUG) { - logDebug( - 'QueryEngine', - 'SDK will not create cache indexes for query:', - stringifyQuery(query), - 'since it only creates cache indexes for collection contains', - 'more than or equal to', - this.indexAutoCreationMinCollectionSize, - 'documents' - ); - } - return PersistencePromise.resolve(); + return ( + this.fieldIndexPlugin?.createCacheIndexes( + transaction, + query, + context, + resultSize + ) ?? PersistencePromise.resolve() + ); + } + + private performQueryUsingIndex( + transaction: PersistenceTransaction, + query: Query + ): PersistencePromise { + return ( + this.fieldIndexPlugin?.performQueryUsingIndex(transaction, query) ?? + PersistencePromise.resolve(null) + ); + } + + /** + * Performs a query based on the target's persisted query mapping. Returns + * `null` if the mapping is not available or cannot be used. + */ + private performQueryUsingRemoteKeys( + transaction: PersistenceTransaction, + query: Query, + remoteKeys: DocumentKeySet, + lastLimboFreeSnapshotVersion: SnapshotVersion + ): PersistencePromise { + if (queryMatchesAllDocuments(query)) { + // Queries that match all documents don't benefit from using + // key-based lookups. It is more efficient to scan all documents in a + // collection, rather than to perform individual lookups. + return PersistencePromise.resolve(null); + } + + // Queries that have never seen a snapshot without limbo free documents + // should also be run as a full collection scan. + if (lastLimboFreeSnapshotVersion.isEqual(SnapshotVersion.min())) { + return PersistencePromise.resolve(null); } + return this.localDocumentsView!.getDocuments(transaction, remoteKeys).next( + documents => { + const previousResults = applyQuery(query, documents); + + if ( + needsRefill( + query, + previousResults, + remoteKeys, + lastLimboFreeSnapshotVersion + ) + ) { + return PersistencePromise.resolve(null); + } + + if (getLogLevel() <= LogLevel.DEBUG) { + logDebug( + 'QueryEngine', + 'Re-using previous result from %s to execute query: %s', + lastLimboFreeSnapshotVersion.toString(), + stringifyQuery(query) + ); + } + + // Retrieve all results for documents that were updated since the last + // limbo-document free remote snapshot. + return appendRemainingResults( + transaction, + this.localDocumentsView, + previousResults, + query, + newIndexOffsetSuccessorFromReadTime( + lastLimboFreeSnapshotVersion, + INITIAL_LARGEST_BATCH_ID + ) + ).next(results => results); + } + ); + } + + private executeFullCollectionScan( + transaction: PersistenceTransaction, + query: Query, + context: QueryContext + ): PersistencePromise { if (getLogLevel() <= LogLevel.DEBUG) { logDebug( 'QueryEngine', - 'Query:', - stringifyQuery(query), - 'scans', - context.documentReadCount, - 'local documents and returns', - resultSize, - 'documents as results.' + 'Using full collection scan to execute query:', + stringifyQuery(query) ); } - if ( - context.documentReadCount > - this.relativeIndexReadCostPerDocument * resultSize - ) { - if (getLogLevel() <= LogLevel.DEBUG) { - logDebug( - 'QueryEngine', - 'The SDK decides to create cache indexes for query:', - stringifyQuery(query), - 'as using cache indexes may help improve performance.' - ); - } - return this.indexManager.createTargetIndexes( - transaction, - queryToTarget(query) - ); - } + return this.localDocumentsView!.getDocumentsMatchingQuery( + transaction, + query, + IndexOffset.min(), + context + ); + } +} - return PersistencePromise.resolve(); +export function queryEngineInstallFieldIndexPlugin( + instance: QueryEngine +): QueryEngineFieldIndexPlugin { + if (instance.fieldIndexPlugin) { + return instance.fieldIndexPlugin; } + logDebug( + 'Installing QueryEngineFieldIndexPlugin into ' + + 'QueryEngine to support persistent cache indexing.' + ); + + return instance.installFieldIndexPlugin(QueryEngineFieldIndexPlugin); +} + +interface QueryEngineFieldIndexPluginConstructor { + new ( + indexManager: IndexManager, + localDocumentsView: LocalDocumentsView + ): QueryEngineFieldIndexPlugin; +} + +export class QueryEngineFieldIndexPlugin { + indexAutoCreationEnabled = false; + + /** + * SDK only decides whether it should create index when collection size is + * larger than this. + */ + indexAutoCreationMinCollectionSize = + DEFAULT_INDEX_AUTO_CREATION_MIN_COLLECTION_SIZE; + + relativeIndexReadCostPerDocument = + DEFAULT_RELATIVE_INDEX_READ_COST_PER_DOCUMENT; + + get indexManagerFieldIndexPlugin(): IndexManagerFieldIndexPlugin { + const plugin = this.indexManager.fieldIndexPlugin; + hardAssert( + !!plugin, + 'The IndexManager specified to QueryEngineFieldIndexPlugin ' + + 'must have a non-null field index plugin.' + ); + return plugin; + } + + constructor( + private readonly indexManager: IndexManager, + private readonly localDocumentsView: LocalDocumentsView + ) {} + /** * Performs an indexed query that evaluates the query based on a collection's * persisted index values. Returns `null` if an index is not available. */ - private performQueryUsingIndex( + performQueryUsingIndex( transaction: PersistenceTransaction, query: Query ): PersistencePromise { + hardAssert(!!this.indexManager); + hardAssert(!!this.localDocumentsView); + if (queryMatchesAllDocuments(query)) { // Queries that match all documents don't benefit from using // key-based lookups. It is more efficient to scan all documents in a @@ -249,7 +373,7 @@ export class QueryEngine { } let target = queryToTarget(query); - return this.indexManager + return this.indexManagerFieldIndexPlugin .getIndexType(transaction, target) .next(indexType => { if (indexType === IndexType.NONE) { @@ -269,7 +393,7 @@ export class QueryEngine { target = queryToTarget(query); } - return this.indexManager + return this.indexManagerFieldIndexPlugin .getDocumentsMatchingTarget(transaction, target) .next(keys => { debugAssert( @@ -280,16 +404,13 @@ export class QueryEngine { return this.localDocumentsView .getDocuments(transaction, sortedKeys) .next(indexedDocuments => { - return this.indexManager + return this.indexManagerFieldIndexPlugin .getMinOffset(transaction, target) .next(offset => { - const previousResults = this.applyQuery( - query, - indexedDocuments - ); + const previousResults = applyQuery(query, indexedDocuments); if ( - this.needsRefill( + needsRefill( query, previousResults, sortedKeys, @@ -308,8 +429,9 @@ export class QueryEngine { ); } - return this.appendRemainingResults( + return appendRemainingResults( transaction, + this.localDocumentsView, previousResults, query, offset @@ -320,175 +442,148 @@ export class QueryEngine { }); } - /** - * Performs a query based on the target's persisted query mapping. Returns - * `null` if the mapping is not available or cannot be used. - */ - private performQueryUsingRemoteKeys( + createCacheIndexes( transaction: PersistenceTransaction, query: Query, - remoteKeys: DocumentKeySet, - lastLimboFreeSnapshotVersion: SnapshotVersion - ): PersistencePromise { - if (queryMatchesAllDocuments(query)) { - // Queries that match all documents don't benefit from using - // key-based lookups. It is more efficient to scan all documents in a - // collection, rather than to perform individual lookups. - return PersistencePromise.resolve(null); - } + context: QueryContext, + resultSize: number + ): PersistencePromise { + hardAssert(!!this.indexManager); + hardAssert(!!this.localDocumentsView); - // Queries that have never seen a snapshot without limbo free documents - // should also be run as a full collection scan. - if (lastLimboFreeSnapshotVersion.isEqual(SnapshotVersion.min())) { - return PersistencePromise.resolve(null); + if (context.documentReadCount < this.indexAutoCreationMinCollectionSize) { + if (getLogLevel() <= LogLevel.DEBUG) { + logDebug( + 'QueryEngine', + 'SDK will not create cache indexes for query:', + stringifyQuery(query), + 'since it only creates cache indexes for collection contains', + 'more than or equal to', + this.indexAutoCreationMinCollectionSize, + 'documents' + ); + } + return PersistencePromise.resolve(); } - return this.localDocumentsView!.getDocuments(transaction, remoteKeys).next( - documents => { - const previousResults = this.applyQuery(query, documents); - - if ( - this.needsRefill( - query, - previousResults, - remoteKeys, - lastLimboFreeSnapshotVersion - ) - ) { - return PersistencePromise.resolve(null); - } - - if (getLogLevel() <= LogLevel.DEBUG) { - logDebug( - 'QueryEngine', - 'Re-using previous result from %s to execute query: %s', - lastLimboFreeSnapshotVersion.toString(), - stringifyQuery(query) - ); - } + if (getLogLevel() <= LogLevel.DEBUG) { + logDebug( + 'QueryEngine', + 'Query:', + stringifyQuery(query), + 'scans', + context.documentReadCount, + 'local documents and returns', + resultSize, + 'documents as results.' + ); + } - // Retrieve all results for documents that were updated since the last - // limbo-document free remote snapshot. - return this.appendRemainingResults( - transaction, - previousResults, - query, - newIndexOffsetSuccessorFromReadTime( - lastLimboFreeSnapshotVersion, - INITIAL_LARGEST_BATCH_ID - ) - ).next(results => results); + if ( + context.documentReadCount > + this.relativeIndexReadCostPerDocument * resultSize + ) { + if (getLogLevel() <= LogLevel.DEBUG) { + logDebug( + 'QueryEngine', + 'The SDK decides to create cache indexes for query:', + stringifyQuery(query), + 'as using cache indexes may help improve performance.' + ); } - ); - } + return this.indexManagerFieldIndexPlugin.createTargetIndexes( + transaction, + queryToTarget(query) + ); + } - /** Applies the query filter and sorting to the provided documents. */ - private applyQuery( - query: Query, - documents: DocumentMap - ): SortedSet { - // Sort the documents and re-apply the query filter since previously - // matching documents do not necessarily still match the query. - let queryResults = new SortedSet(newQueryComparator(query)); - documents.forEach((_, maybeDoc) => { - if (queryMatches(query, maybeDoc)) { - queryResults = queryResults.add(maybeDoc); - } - }); - return queryResults; + return PersistencePromise.resolve(); } +} - /** - * Determines if a limit query needs to be refilled from cache, making it - * ineligible for index-free execution. - * - * @param query - The query. - * @param sortedPreviousResults - The documents that matched the query when it - * was last synchronized, sorted by the query's comparator. - * @param remoteKeys - The document keys that matched the query at the last - * snapshot. - * @param limboFreeSnapshotVersion - The version of the snapshot when the - * query was last synchronized. - */ - private needsRefill( - query: Query, - sortedPreviousResults: SortedSet, - remoteKeys: DocumentKeySet, - limboFreeSnapshotVersion: SnapshotVersion - ): boolean { - if (query.limit === null) { - // Queries without limits do not need to be refilled. - return false; - } - - if (remoteKeys.size !== sortedPreviousResults.size) { - // The query needs to be refilled if a previously matching document no - // longer matches. - return true; +/** Applies the query filter and sorting to the provided documents. */ +function applyQuery(query: Query, documents: DocumentMap): SortedSet { + // Sort the documents and re-apply the query filter since previously + // matching documents do not necessarily still match the query. + let queryResults = new SortedSet(newQueryComparator(query)); + documents.forEach((_, maybeDoc) => { + if (queryMatches(query, maybeDoc)) { + queryResults = queryResults.add(maybeDoc); } + }); + return queryResults; +} - // Limit queries are not eligible for index-free query execution if there is - // a potential that an older document from cache now sorts before a document - // that was previously part of the limit. This, however, can only happen if - // the document at the edge of the limit goes out of limit. - // If a document that is not the limit boundary sorts differently, - // the boundary of the limit itself did not change and documents from cache - // will continue to be "rejected" by this boundary. Therefore, we can ignore - // any modifications that don't affect the last document. - const docAtLimitEdge = - query.limitType === LimitType.First - ? sortedPreviousResults.last() - : sortedPreviousResults.first(); - if (!docAtLimitEdge) { - // We don't need to refill the query if there were already no documents. - return false; - } - return ( - docAtLimitEdge.hasPendingWrites || - docAtLimitEdge.version.compareTo(limboFreeSnapshotVersion) > 0 - ); +/** + * Determines if a limit query needs to be refilled from cache, making it + * ineligible for index-free execution. + * + * @param query - The query. + * @param sortedPreviousResults - The documents that matched the query when it + * was last synchronized, sorted by the query's comparator. + * @param remoteKeys - The document keys that matched the query at the last + * snapshot. + * @param limboFreeSnapshotVersion - The version of the snapshot when the + * query was last synchronized. + */ +function needsRefill( + query: Query, + sortedPreviousResults: SortedSet, + remoteKeys: DocumentKeySet, + limboFreeSnapshotVersion: SnapshotVersion +): boolean { + if (query.limit === null) { + // Queries without limits do not need to be refilled. + return false; } - private executeFullCollectionScan( - transaction: PersistenceTransaction, - query: Query, - context: QueryContext - ): PersistencePromise { - if (getLogLevel() <= LogLevel.DEBUG) { - logDebug( - 'QueryEngine', - 'Using full collection scan to execute query:', - stringifyQuery(query) - ); - } + if (remoteKeys.size !== sortedPreviousResults.size) { + // The query needs to be refilled if a previously matching document no + // longer matches. + return true; + } - return this.localDocumentsView!.getDocumentsMatchingQuery( - transaction, - query, - IndexOffset.min(), - context - ); + // Limit queries are not eligible for index-free query execution if there is + // a potential that an older document from cache now sorts before a document + // that was previously part of the limit. This, however, can only happen if + // the document at the edge of the limit goes out of limit. + // If a document that is not the limit boundary sorts differently, + // the boundary of the limit itself did not change and documents from cache + // will continue to be "rejected" by this boundary. Therefore, we can ignore + // any modifications that don't affect the last document. + const docAtLimitEdge = + query.limitType === LimitType.First + ? sortedPreviousResults.last() + : sortedPreviousResults.first(); + if (!docAtLimitEdge) { + // We don't need to refill the query if there were already no documents. + return false; } + return ( + docAtLimitEdge.hasPendingWrites || + docAtLimitEdge.version.compareTo(limboFreeSnapshotVersion) > 0 + ); +} - /** - * Combines the results from an indexed execution with the remaining documents - * that have not yet been indexed. - */ - private appendRemainingResults( - transaction: PersistenceTransaction, - indexedResults: Iterable, - query: Query, - offset: IndexOffset - ): PersistencePromise { - // Retrieve all results for documents that were updated since the offset. - return this.localDocumentsView - .getDocumentsMatchingQuery(transaction, query, offset) - .next(remainingResults => { - // Merge with existing results - indexedResults.forEach(d => { - remainingResults = remainingResults.insert(d.key, d); - }); - return remainingResults; +/** + * Combines the results from an indexed execution with the remaining documents + * that have not yet been indexed. + */ +function appendRemainingResults( + transaction: PersistenceTransaction, + localDocumentsView: LocalDocumentsView, + indexedResults: Iterable, + query: Query, + offset: IndexOffset +): PersistencePromise { + // Retrieve all results for documents that were updated since the offset. + return localDocumentsView + .getDocumentsMatchingQuery(transaction, query, offset) + .next(remainingResults => { + // Merge with existing results + indexedResults.forEach(d => { + remainingResults = remainingResults.insert(d.key, d); }); - } + return remainingResults; + }); } diff --git a/packages/firestore/test/unit/local/index_backfiller.test.ts b/packages/firestore/test/unit/local/index_backfiller.test.ts index 6a92099bfd8..cd4fa2b0a00 100644 --- a/packages/firestore/test/unit/local/index_backfiller.test.ts +++ b/packages/firestore/test/unit/local/index_backfiller.test.ts @@ -22,7 +22,10 @@ import { Query, queryToTarget } from '../../../src/core/query'; import { IndexBackfiller } from '../../../src/local/index_backfiller'; import { IndexedDbPersistence } from '../../../src/local/indexeddb_persistence'; import { LocalStore } from '../../../src/local/local_store'; -import { newLocalStore } from '../../../src/local/local_store_impl'; +import { + localStoreInstallFieldIndexPlugins, + newLocalStore +} from '../../../src/local/local_store_impl'; import { Persistence } from '../../../src/local/persistence'; import { PersistencePromise } from '../../../src/local/persistence_promise'; import { PersistenceTransaction } from '../../../src/local/persistence_transaction'; @@ -38,12 +41,12 @@ import { import { Mutation } from '../../../src/model/mutation'; import { AsyncQueue } from '../../../src/util/async_queue'; import { newAsyncQueue } from '../../../src/util/async_queue_impl'; -import { key, version } from '../../util/helpers'; import * as Helpers from '../../util/helpers'; +import { key, version } from '../../util/helpers'; import { CountingQueryEngine } from './counting_query_engine'; -import { JSON_SERIALIZER } from './persistence_test_helpers'; import * as PersistenceTestHelpers from './persistence_test_helpers'; +import { JSON_SERIALIZER } from './persistence_test_helpers'; import { TestDocumentOverlayCache } from './test_document_overlay_cache'; import { TestIndexManager } from './test_index_manager'; @@ -70,9 +73,7 @@ function genericIndexBackfillerTests( } }); persistence = await newPersistence(queue); - const indexManager = persistence.getIndexManager(User.UNAUTHENTICATED); - remoteDocumentCache = persistence.getRemoteDocumentCache(); - remoteDocumentCache.setIndexManager(indexManager); + const queryEngine = new CountingQueryEngine(); const localStore: LocalStore = newLocalStore( persistence, @@ -80,9 +81,16 @@ function genericIndexBackfillerTests( User.UNAUTHENTICATED, JSON_SERIALIZER ); + localStoreInstallFieldIndexPlugins(localStore); + + remoteDocumentCache = persistence.getRemoteDocumentCache(); + remoteDocumentCache.setIndexManager(localStore.indexManager); backfiller = new IndexBackfiller(localStore, persistence); - testIndexManager = new TestIndexManager(persistence, indexManager); + testIndexManager = new TestIndexManager( + persistence, + localStore.indexManager + ); overlayCache = new TestDocumentOverlayCache( persistence, persistence.getDocumentOverlayCache(User.UNAUTHENTICATED) diff --git a/packages/firestore/test/unit/local/index_manager.test.ts b/packages/firestore/test/unit/local/index_manager.test.ts index 0788dcabbde..81b23b17efd 100644 --- a/packages/firestore/test/unit/local/index_manager.test.ts +++ b/packages/firestore/test/unit/local/index_manager.test.ts @@ -34,6 +34,10 @@ import { displayNameForIndexType, IndexType } from '../../../src/local/index_manager'; +import { + IndexedDbIndexManager, + indexedDbIndexManagerInstallFieldIndexPlugin +} from '../../../src/local/indexeddb_index_manager'; import { IndexedDbPersistence } from '../../../src/local/indexeddb_persistence'; import { Persistence } from '../../../src/local/persistence'; import { documentMap } from '../../../src/model/collections'; @@ -103,7 +107,15 @@ describe('IndexedDbIndexManager', async () => { user = User.UNAUTHENTICATED ): Promise { const persistence = await persistencePromise; - return new TestIndexManager(persistence, persistence.getIndexManager(user)); + const indexManager = persistence.getIndexManager(user); + if (!(indexManager instanceof IndexedDbIndexManager)) { + throw new Error( + 'persistence.getIndexManager() should have returned ' + + 'an instance of IndexedDbIndexManager' + ); + } + indexedDbIndexManagerInstallFieldIndexPlugin(indexManager); + return new TestIndexManager(persistence, indexManager); } genericIndexManagerTests(() => persistencePromise); diff --git a/packages/firestore/test/unit/local/indexeddb_persistence.test.ts b/packages/firestore/test/unit/local/indexeddb_persistence.test.ts index 991b149cfc7..63c2983577e 100644 --- a/packages/firestore/test/unit/local/indexeddb_persistence.test.ts +++ b/packages/firestore/test/unit/local/indexeddb_persistence.test.ts @@ -264,6 +264,17 @@ describe('IndexedDbSchema: createOrUpgradeDb', () => { return; } + if ( + typeof process !== 'undefined' && + process.env.MOCK_PERSISTENCE_MODE === 'memory' + ) { + console.warn( + 'IndexedDB is configured to use an in-memory database. ' + + 'Skipping createOrUpgradeDb() tests.' + ); + return; + } + beforeEach(() => SimpleDb.delete(INDEXEDDB_TEST_DATABASE_NAME)); after(() => SimpleDb.delete(INDEXEDDB_TEST_DATABASE_NAME)); diff --git a/packages/firestore/test/unit/local/local_store.test.ts b/packages/firestore/test/unit/local/local_store.test.ts index b8fe6878d9f..ddacb594a9d 100644 --- a/packages/firestore/test/unit/local/local_store.test.ts +++ b/packages/firestore/test/unit/local/local_store.test.ts @@ -40,18 +40,17 @@ import { localStoreApplyRemoteEventToLocalCache, localStoreExecuteQuery, localStoreGetHighestUnacknowledgedBatchId, - localStoreGetTargetData, localStoreGetNamedQuery, - localStoreSetIndexAutoCreationEnabled, + localStoreGetTargetData, localStoreHasNewerBundle, - localStoreWriteLocally, - LocalWriteResult, localStoreNotifyLocalViewChanges, localStoreReadDocument, localStoreRejectBatch, localStoreReleaseTarget, localStoreSaveBundle, localStoreSaveNamedQuery, + localStoreWriteLocally, + LocalWriteResult, newLocalStore } from '../../../src/local/local_store_impl'; import { LocalViewChanges } from '../../../src/local/local_view_changes'; @@ -659,17 +658,6 @@ function genericLocalStoreTests( ); } - it('localStoreSetIndexAutoCreationEnabled()', () => { - localStoreSetIndexAutoCreationEnabled(localStore, true); - expect(queryEngine.indexAutoCreationEnabled).to.be.true; - localStoreSetIndexAutoCreationEnabled(localStore, false); - expect(queryEngine.indexAutoCreationEnabled).to.be.false; - localStoreSetIndexAutoCreationEnabled(localStore, true); - expect(queryEngine.indexAutoCreationEnabled).to.be.true; - localStoreSetIndexAutoCreationEnabled(localStore, false); - expect(queryEngine.indexAutoCreationEnabled).to.be.false; - }); - it('handles SetMutation', () => { return expectLocalStore() .after(setMutation('foo/bar', { foo: 'bar' })) diff --git a/packages/firestore/test/unit/local/local_store_indexeddb.test.ts b/packages/firestore/test/unit/local/local_store_indexeddb.test.ts index e88b8d6a978..793d6d0c760 100644 --- a/packages/firestore/test/unit/local/local_store_indexeddb.test.ts +++ b/packages/firestore/test/unit/local/local_store_indexeddb.test.ts @@ -36,11 +36,12 @@ import { localStoreApplyRemoteEventToLocalCache, localStoreConfigureFieldIndexes, localStoreDeleteAllFieldIndexes, + localStoreDisableIndexAutoCreation, + localStoreEnableIndexAutoCreation, localStoreExecuteQuery, - localStoreSetIndexAutoCreationEnabled, + localStoreInstallFieldIndexPlugins, localStoreWriteLocally, - newLocalStore, - TestingHooks as LocalStoreTestingHooks + newLocalStore } from '../../../src/local/local_store_impl'; import { Persistence } from '../../../src/local/persistence'; import { DocumentMap } from '../../../src/model/collections'; @@ -84,7 +85,7 @@ class AsyncLocalStoreTester { constructor( public localStore: LocalStore, private readonly persistence: Persistence, - private readonly queryEngine: CountingQueryEngine, + public readonly queryEngine: CountingQueryEngine, readonly gcIsEager: boolean ) { this.bundleConverter = new BundleConverterImpl(JSON_SERIALIZER); @@ -143,13 +144,24 @@ class AsyncLocalStoreTester { }): void { this.prepareNextStep(); - if (config.isEnabled !== undefined) { - localStoreSetIndexAutoCreationEnabled(this.localStore, config.isEnabled); + if (config.isEnabled === true) { + localStoreEnableIndexAutoCreation(this.localStore); + } else if (config.isEnabled === false) { + localStoreDisableIndexAutoCreation(this.localStore); + } + + if (config.indexAutoCreationMinCollectionSize !== undefined) { + localStoreInstallFieldIndexPlugins( + this.localStore + ).indexAutoCreationMinCollectionSize = + config.indexAutoCreationMinCollectionSize; + } + if (config.relativeIndexReadCostPerDocument !== undefined) { + localStoreInstallFieldIndexPlugins( + this.localStore + ).relativeIndexReadCostPerDocument = + config.relativeIndexReadCostPerDocument; } - LocalStoreTestingHooks.setIndexAutoCreationSettings( - this.localStore, - config - ); } deleteAllFieldIndexes(): Promise { @@ -171,7 +183,10 @@ class AsyncLocalStoreTester { const fieldIndexes: FieldIndex[] = await this.persistence.runTransaction( 'getFieldIndexes ', 'readonly', - transaction => this.localStore.indexManager.getFieldIndexes(transaction) + transaction => + this.localStore.indexManager.fieldIndexPlugin!.getFieldIndexes( + transaction + ) ); expect(fieldIndexes).to.have.deep.members(indexes); } @@ -248,7 +263,9 @@ describe('LocalStore w/ IndexedDB Persistence (Non generic)', () => { }); afterEach(async () => { - await persistence.shutdown(); + if (persistence) { + await persistence.shutdown(); + } await persistenceHelpers.clearTestPersistence(); }); @@ -937,4 +954,38 @@ describe('LocalStore w/ IndexedDB Persistence (Non generic)', () => { test.assertRemoteDocumentsRead(0, 2); test.assertQueryReturned('coll/a', 'coll/e'); }); + + it('localStoreEnableIndexAutoCreation()', () => { + const localStore = test.localStore; + const queryEngine = test.queryEngine; + + localStoreEnableIndexAutoCreation(localStore); + expect(queryEngine.fieldIndexPlugin?.indexAutoCreationEnabled).to.be.true; + + localStoreDisableIndexAutoCreation(localStore); + expect(queryEngine.fieldIndexPlugin?.indexAutoCreationEnabled).to.be.false; + + localStoreEnableIndexAutoCreation(localStore); + expect(queryEngine.fieldIndexPlugin?.indexAutoCreationEnabled).to.be.true; + + localStoreEnableIndexAutoCreation(localStore); + expect(queryEngine.fieldIndexPlugin?.indexAutoCreationEnabled).to.be.true; + }); + + it('localStoreDisableIndexAutoCreation()', () => { + const localStore = test.localStore; + const queryEngine = test.queryEngine; + + localStoreDisableIndexAutoCreation(localStore); + expect(queryEngine.fieldIndexPlugin).to.be.null; + + localStoreEnableIndexAutoCreation(localStore); + expect(queryEngine.fieldIndexPlugin?.indexAutoCreationEnabled).to.be.true; + + localStoreDisableIndexAutoCreation(localStore); + expect(queryEngine.fieldIndexPlugin?.indexAutoCreationEnabled).to.be.false; + + localStoreDisableIndexAutoCreation(localStore); + expect(queryEngine.fieldIndexPlugin?.indexAutoCreationEnabled).to.be.false; + }); }); diff --git a/packages/firestore/test/unit/local/query_engine.test.ts b/packages/firestore/test/unit/local/query_engine.test.ts index d65626acf53..106e97ad28f 100644 --- a/packages/firestore/test/unit/local/query_engine.test.ts +++ b/packages/firestore/test/unit/local/query_engine.test.ts @@ -32,8 +32,13 @@ import { View } from '../../../src/core/view'; import { DocumentOverlayCache } from '../../../src/local/document_overlay_cache'; import { displayNameForIndexType, + IndexManager, IndexType } from '../../../src/local/index_manager'; +import { + IndexedDbIndexManager, + indexedDbIndexManagerInstallFieldIndexPlugin +} from '../../../src/local/indexeddb_index_manager'; import { IndexedDbPersistence } from '../../../src/local/indexeddb_persistence'; import { LocalDocumentsView } from '../../../src/local/local_documents_view'; import { MutationQueue } from '../../../src/local/mutation_queue'; @@ -41,7 +46,10 @@ import { Persistence } from '../../../src/local/persistence'; import { PersistencePromise } from '../../../src/local/persistence_promise'; import { PersistenceTransaction } from '../../../src/local/persistence_transaction'; import { QueryContext } from '../../../src/local/query_context'; -import { QueryEngine } from '../../../src/local/query_engine'; +import { + QueryEngine, + queryEngineInstallFieldIndexPlugin +} from '../../../src/local/query_engine'; import { RemoteDocumentCache } from '../../../src/local/remote_document_cache'; import { TargetCache } from '../../../src/local/target_cache'; import { @@ -159,6 +167,7 @@ function genericQueryEngineTest( let targetCache!: TargetCache; let queryEngine!: QueryEngine; let indexManager!: TestIndexManager; + let underlyingIndexManager!: IndexManager; let mutationQueue!: MutationQueue; let localDocuments!: TestLocalDocumentsView; @@ -265,9 +274,17 @@ function genericQueryEngineTest( targetCache = persistence.getTargetCache(); queryEngine = new QueryEngine(); - const underlyingIndexManager = persistence.getIndexManager( - User.UNAUTHENTICATED - ); + underlyingIndexManager = persistence.getIndexManager(User.UNAUTHENTICATED); + if (configureCsi) { + if (!(underlyingIndexManager instanceof IndexedDbIndexManager)) { + throw new Error( + 'persistence.getIndexManager() should have ' + + 'returned an instance of IndexedDbIndexManager' + ); + } + indexedDbIndexManagerInstallFieldIndexPlugin(underlyingIndexManager); + } + remoteDocumentCache = persistence.getRemoteDocumentCache(); remoteDocumentCache.setIndexManager(underlyingIndexManager); mutationQueue = persistence.getMutationQueue( @@ -284,6 +301,9 @@ function genericQueryEngineTest( underlyingIndexManager ); queryEngine.initialize(localDocuments, underlyingIndexManager); + if (configureCsi) { + queryEngineInstallFieldIndexPlugin(queryEngine); + } indexManager = new TestIndexManager(persistence, underlyingIndexManager); }); @@ -841,7 +861,7 @@ function genericQueryEngineTest( // A generic test for index auto-creation. // This function can be called with explicit parameters from it() methods. const testIndexAutoCreation = async (config: { - indexAutoCreationEnabled: boolean; + indexAutoCreationEnabled?: boolean; indexAutoCreationMinCollectionSize?: number; relativeIndexReadCostPerDocument?: number; matchingDocumentCount?: number; @@ -850,6 +870,9 @@ function genericQueryEngineTest( }): Promise => { debugAssert(configureCsi, 'Test requires durable persistence'); + const queryEngineFieldIndexPlugin = + queryEngineInstallFieldIndexPlugin(queryEngine); + const matchingDocuments: MutableDocument[] = []; for (let i = 0; i < (config.matchingDocumentCount ?? 3); i++) { const matchingDocument = doc(`coll/A${i}`, 1, { 'foo': 'match' }); @@ -864,14 +887,16 @@ function genericQueryEngineTest( } await addDocument(...nonmatchingDocuments); - queryEngine.indexAutoCreationEnabled = config.indexAutoCreationEnabled; - + if (config.indexAutoCreationEnabled !== undefined) { + queryEngineFieldIndexPlugin.indexAutoCreationEnabled = + config.indexAutoCreationEnabled; + } if (config.indexAutoCreationMinCollectionSize !== undefined) { - queryEngine.indexAutoCreationMinCollectionSize = + queryEngineFieldIndexPlugin.indexAutoCreationMinCollectionSize = config.indexAutoCreationMinCollectionSize; } if (config.relativeIndexReadCostPerDocument !== undefined) { - queryEngine.relativeIndexReadCostPerDocument = + queryEngineFieldIndexPlugin.relativeIndexReadCostPerDocument = config.relativeIndexReadCostPerDocument; } @@ -921,6 +946,11 @@ function genericQueryEngineTest( expectedPostQueryExecutionIndexType: IndexType.NONE })); + it('does not create indexes when no QueryEngineFieldIndexPlugin is installed', () => + testIndexAutoCreation({ + expectedPostQueryExecutionIndexType: IndexType.NONE + })); + it( 'creates indexes when ' + 'min collection size is met exactly ' + diff --git a/packages/firestore/test/unit/local/test_index_manager.ts b/packages/firestore/test/unit/local/test_index_manager.ts index c3b6c092652..1c404eabb21 100644 --- a/packages/firestore/test/unit/local/test_index_manager.ts +++ b/packages/firestore/test/unit/local/test_index_manager.ts @@ -17,6 +17,7 @@ import { Target } from '../../../src/core/target'; import { IndexManager, IndexType } from '../../../src/local/index_manager'; +import { deleteAllFieldIndexes } from '../../../src/local/indexeddb_index_manager'; import { Persistence } from '../../../src/local/persistence'; import { DocumentMap } from '../../../src/model/collections'; import { DocumentKey } from '../../../src/model/document_key'; @@ -32,7 +33,6 @@ export class TestIndexManager { public persistence: Persistence, public indexManager: IndexManager ) {} - addToCollectionParentIndex(collectionPath: ResourcePath): Promise { return this.persistence.runTransaction( 'addToCollectionParentIndex', @@ -51,7 +51,7 @@ export class TestIndexManager { addFieldIndex(index: FieldIndex): Promise { return this.persistence.runTransaction('addFieldIndex', 'readwrite', txn => - this.indexManager.addFieldIndex(txn, index) + this.indexManager.fieldIndexPlugin!.addFieldIndex(txn, index) ); } @@ -59,7 +59,7 @@ export class TestIndexManager { return this.persistence.runTransaction( 'deleteFieldIndex', 'readwrite', - txn => this.indexManager.deleteFieldIndex(txn, index) + txn => this.indexManager.fieldIndexPlugin!.deleteFieldIndex(txn, index) ); } @@ -67,7 +67,8 @@ export class TestIndexManager { return this.persistence.runTransaction( 'createTargetIndexes', 'readwrite', - txn => this.indexManager.createTargetIndexes(txn, target) + txn => + this.indexManager.fieldIndexPlugin!.createTargetIndexes(txn, target) ); } @@ -75,21 +76,24 @@ export class TestIndexManager { return this.persistence.runTransaction( 'deleteAllFieldIndexes', 'readwrite', - txn => this.indexManager.deleteAllFieldIndexes(txn) + txn => deleteAllFieldIndexes(txn) ); } getFieldIndexes(collectionGroup?: string): Promise { return this.persistence.runTransaction('getFieldIndexes', 'readonly', txn => collectionGroup - ? this.indexManager.getFieldIndexes(txn, collectionGroup) - : this.indexManager.getFieldIndexes(txn) + ? this.indexManager.fieldIndexPlugin!.getFieldIndexes( + txn, + collectionGroup + ) + : this.indexManager.fieldIndexPlugin!.getFieldIndexes(txn) ); } getIndexType(target: Target): Promise { return this.persistence.runTransaction('getIndexType', 'readonly', txn => - this.indexManager.getIndexType(txn, target) + this.indexManager.fieldIndexPlugin!.getIndexType(txn, target) ); } @@ -97,7 +101,11 @@ export class TestIndexManager { return this.persistence.runTransaction( 'getDocumentsMatchingTarget', 'readonly', - txn => this.indexManager.getDocumentsMatchingTarget(txn, target) + txn => + this.indexManager.fieldIndexPlugin!.getDocumentsMatchingTarget( + txn, + target + ) ); } @@ -105,7 +113,8 @@ export class TestIndexManager { return this.persistence.runTransaction( 'getNextCollectionGroupToUpdate', 'readonly', - txn => this.indexManager.getNextCollectionGroupToUpdate(txn) + txn => + this.indexManager.fieldIndexPlugin!.getNextCollectionGroupToUpdate(txn) ); } @@ -117,7 +126,11 @@ export class TestIndexManager { 'updateCollectionGroup', 'readwrite-primary', txn => - this.indexManager.updateCollectionGroup(txn, collectionGroup, offset) + this.indexManager.fieldIndexPlugin!.updateCollectionGroup( + txn, + collectionGroup, + offset + ) ); } @@ -125,7 +138,8 @@ export class TestIndexManager { return this.persistence.runTransaction( 'updateIndexEntries', 'readwrite-primary', - txn => this.indexManager.updateIndexEntries(txn, documents) + txn => + this.indexManager.fieldIndexPlugin!.updateIndexEntries(txn, documents) ); } } diff --git a/packages/firestore/test/unit/specs/spec_test_runner.ts b/packages/firestore/test/unit/specs/spec_test_runner.ts index 1245a1c0231..2b864a67ce9 100644 --- a/packages/firestore/test/unit/specs/spec_test_runner.ts +++ b/packages/firestore/test/unit/specs/spec_test_runner.ts @@ -31,13 +31,13 @@ import { User } from '../../../src/auth/user'; import { ComponentConfiguration } from '../../../src/core/component_provider'; import { DatabaseInfo } from '../../../src/core/database_info'; import { + addSnapshotsInSyncListener, EventManager, eventManagerListen, eventManagerUnlisten, Observer, QueryListener, - removeSnapshotsInSyncListener, - addSnapshotsInSyncListener + removeSnapshotsInSyncListener } from '../../../src/core/event_manager'; import { canonifyQuery, @@ -55,9 +55,9 @@ import { SyncEngine } from '../../../src/core/sync_engine'; import { syncEngineGetActiveLimboDocumentResolutions, syncEngineGetEnqueuedLimboDocumentResolutions, - syncEngineRegisterPendingWritesCallback, syncEngineListen, syncEngineLoadBundle, + syncEngineRegisterPendingWritesCallback, syncEngineUnlisten, syncEngineWrite } from '../../../src/core/sync_engine_impl'; @@ -66,6 +66,10 @@ import { ChangeType, DocumentViewChange } from '../../../src/core/view_snapshot'; +import { + IndexedDbIndexManager, + indexedDbIndexManagerInstallFieldIndexPlugin +} from '../../../src/local/indexeddb_index_manager'; import { IndexedDbLruDelegateImpl } from '../../../src/local/indexeddb_lru_delegate_impl'; import { IndexedDbPersistence } from '../../../src/local/indexeddb_persistence'; import { @@ -97,13 +101,13 @@ import { newTextEncoder } from '../../../src/platform/text_serializer'; import * as api from '../../../src/protos/firestore_proto_api'; import { ExistenceFilter } from '../../../src/remote/existence_filter'; import { - RemoteStore, fillWritePipeline, + outstandingWrites, + RemoteStore, remoteStoreDisableNetwork, - remoteStoreShutdown, remoteStoreEnableNetwork, remoteStoreHandleCredentialChange, - outstandingWrites + remoteStoreShutdown } from '../../../src/remote/remote_store'; import { mapCodeFromRpcCode } from '../../../src/remote/rpc_error'; import { @@ -991,12 +995,24 @@ abstract class TestRunner { expect(this.started).to.equal(!expectedState.isShutdown); } if ('indexes' in expectedState) { + if (!(this.localStore.indexManager instanceof IndexedDbIndexManager)) { + throw new Error( + 'localStore.indexManager should be ' + + 'an instance of IndexedDbIndexManager' + ); + } + indexedDbIndexManagerInstallFieldIndexPlugin( + this.localStore.indexManager + ); + const fieldIndexes: FieldIndex[] = await this.persistence.runTransaction( 'getFieldIndexes ', 'readonly', transaction => - this.localStore.indexManager.getFieldIndexes(transaction) + this.localStore.indexManager.fieldIndexPlugin!.getFieldIndexes( + transaction + ) ); assert.deepEqualExcluding( diff --git a/packages/firestore/test/util/node_persistence.ts b/packages/firestore/test/util/node_persistence.ts index 54d34be994d..61d514422f3 100644 --- a/packages/firestore/test/util/node_persistence.ts +++ b/packages/firestore/test/util/node_persistence.ts @@ -37,11 +37,25 @@ const globalAny = global as any; const dbDir = fs.mkdtempSync(os.tmpdir() + '/firestore_tests'); if (process.env.USE_MOCK_PERSISTENCE === 'YES') { - registerIndexedDBShim(null, { - checkOrigin: false, - databaseBasePath: dbDir, - deleteDatabaseFiles: true - }); + const indexedDbShimOptions: Record = { + checkOrigin: false + }; + + // Mocking persistence using an in-memory database reduces test execution time + // by orders of magnitude compared to using a temporary database on disk; + // however, some tests have erratic behavior when using an in-memory database + // because they rely on the database persisting between opens (e.g. tests for + // upgrading the database schema). Therefore, setting the + // `MOCK_PERSISTENCE_MODE` environment variable to `memory` is only + // recommended during local build/test development cycles. + if (process.env.MOCK_PERSISTENCE_MODE === 'memory') { + indexedDbShimOptions.memoryDatabase = ':memory:'; + } else { + indexedDbShimOptions.databaseBasePath = dbDir; + indexedDbShimOptions.deleteDatabaseFiles = true; + } + + registerIndexedDBShim(null, indexedDbShimOptions); // 'indexeddbshim' installs IndexedDB onto `globalAny`, which means we don't // have to register it ourselves.