diff --git a/packages/firestore/src/api/database.ts b/packages/firestore/src/api/database.ts index e4d0dcad479..6844571322b 100644 --- a/packages/firestore/src/api/database.ts +++ b/packages/firestore/src/api/database.ts @@ -288,7 +288,7 @@ export class Firestore implements firestore.FirebaseFirestore, FirebaseService { constructor( databaseIdOrApp: FirestoreDatabase | FirebaseApp, authProvider: Provider, - persistenceProvider: ComponentProvider = new MemoryComponentProvider() + componentProvider: ComponentProvider = new MemoryComponentProvider() ) { if (typeof (databaseIdOrApp as FirebaseApp).options === 'object') { // This is very likely a Firebase app object @@ -313,7 +313,7 @@ export class Firestore implements firestore.FirebaseFirestore, FirebaseService { this._credentials = new EmptyCredentialsProvider(); } - this._componentProvider = persistenceProvider; + this._componentProvider = componentProvider; this._settings = new FirestoreSettings({}); this._dataReader = this.createDataReader(this._databaseId); } diff --git a/packages/firestore/src/core/component_provider.ts b/packages/firestore/src/core/component_provider.ts index 98973baed22..2c34723ecef 100644 --- a/packages/firestore/src/core/component_provider.ts +++ b/packages/firestore/src/core/component_provider.ts @@ -21,8 +21,8 @@ import { SharedClientState, WebStorageSharedClientState } from '../local/shared_client_state'; -import { LocalStore } from '../local/local_store'; -import { SyncEngine } from './sync_engine'; +import { LocalStore, MultiTabLocalStore } from '../local/local_store'; +import { MultiTabSyncEngine, SyncEngine } from './sync_engine'; import { RemoteStore } from '../remote/remote_store'; import { EventManager } from './event_manager'; import { AsyncQueue } from '../util/async_queue'; @@ -81,7 +81,7 @@ export interface ComponentProvider { * Provides all components needed for Firestore with in-memory persistence. * Uses EagerGC garbage collection. */ -export class MemoryComponentProvider { +export class MemoryComponentProvider implements ComponentProvider { persistence!: Persistence; sharedClientState!: SharedClientState; localStore!: LocalStore; @@ -106,24 +106,12 @@ export class MemoryComponentProvider { OnlineStateSource.SharedClientState ); this.remoteStore.syncEngine = this.syncEngine; - this.sharedClientState.syncEngine = this.syncEngine; + await this.localStore.start(); await this.sharedClientState.start(); await this.remoteStore.start(); - await this.localStore.start(); - // 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 => { - await this.syncEngine.applyPrimaryState(isPrimary); - if (this.gcScheduler) { - if (isPrimary && !this.gcScheduler.started) { - this.gcScheduler.start(this.localStore); - } else if (!isPrimary) { - this.gcScheduler.stop(); - } - } - }); + await this.remoteStore.applyPrimaryState(this.syncEngine.isPrimaryClient); } createEventManager(cfg: ComponentConfiguration): EventManager { @@ -149,7 +137,7 @@ export class MemoryComponentProvider { !cfg.persistenceSettings.durable, 'Can only start memory persistence' ); - return new MemoryPersistence(cfg.clientId, MemoryEagerDelegate.factory); + return new MemoryPersistence(MemoryEagerDelegate.factory); } createRemoteStore(cfg: ComponentConfiguration): RemoteStore { @@ -192,13 +180,58 @@ export class MemoryComponentProvider { * Provides all components needed for Firestore with IndexedDB persistence. */ export class IndexedDbComponentProvider extends MemoryComponentProvider { + persistence!: IndexedDbPersistence; + + // TODO(tree-shaking): Create an IndexedDbComponentProvider and a + // MultiTabComponentProvider. The IndexedDbComponentProvider should depend + // on LocalStore and SyncEngine. + localStore!: MultiTabLocalStore; + syncEngine!: MultiTabSyncEngine; + + async initialize(cfg: ComponentConfiguration): Promise { + await super.initialize(cfg); + + // 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 => { + await (this.syncEngine as MultiTabSyncEngine).applyPrimaryState( + isPrimary + ); + if (this.gcScheduler) { + if (isPrimary && !this.gcScheduler.started) { + this.gcScheduler.start(this.localStore); + } else if (!isPrimary) { + this.gcScheduler.stop(); + } + } + }); + } + + createLocalStore(cfg: ComponentConfiguration): LocalStore { + return new MultiTabLocalStore( + this.persistence, + new IndexFreeQueryEngine(), + cfg.initialUser + ); + } + + createSyncEngine(cfg: ComponentConfiguration): SyncEngine { + const syncEngine = new MultiTabSyncEngine( + this.localStore, + this.remoteStore, + this.sharedClientState, + cfg.initialUser, + cfg.maxConcurrentLimboResolutions + ); + if (this.sharedClientState instanceof WebStorageSharedClientState) { + this.sharedClientState.syncEngine = syncEngine; + } + return syncEngine; + } + createGarbageCollectionScheduler( cfg: ComponentConfiguration ): GarbageCollectionScheduler | null { - debugAssert( - this.persistence instanceof IndexedDbPersistence, - 'IndexedDbComponentProvider should provide IndexedDBPersistence' - ); const garbageCollector = this.persistence.referenceDelegate .garbageCollector; return new LruScheduler(garbageCollector, cfg.asyncQueue); @@ -214,27 +247,23 @@ export class IndexedDbComponentProvider extends MemoryComponentProvider { cfg.databaseInfo ); const serializer = cfg.platform.newSerializer(cfg.databaseInfo.databaseId); - return IndexedDbPersistence.createIndexedDbPersistence({ - allowTabSynchronization: cfg.persistenceSettings.synchronizeTabs, + return new IndexedDbPersistence( + cfg.persistenceSettings.synchronizeTabs, persistenceKey, - clientId: cfg.clientId, - platform: cfg.platform, - queue: cfg.asyncQueue, + cfg.clientId, + cfg.platform, + LruParams.withCacheSize(cfg.persistenceSettings.cacheSizeBytes), + cfg.asyncQueue, serializer, - lruParams: LruParams.withCacheSize( - cfg.persistenceSettings.cacheSizeBytes - ), - sequenceNumberSyncer: this.sharedClientState - }); + this.sharedClientState + ); } createSharedClientState(cfg: ComponentConfiguration): SharedClientState { - debugAssert( - cfg.persistenceSettings.durable, - 'Can only start durable persistence' - ); - - if (cfg.persistenceSettings.synchronizeTabs) { + if ( + cfg.persistenceSettings.durable && + cfg.persistenceSettings.synchronizeTabs + ) { if (!WebStorageSharedClientState.isAvailable(cfg.platform)) { throw new FirestoreError( Code.UNIMPLEMENTED, diff --git a/packages/firestore/src/core/sync_engine.ts b/packages/firestore/src/core/sync_engine.ts index 727c89eee5a..50ac6a3eec5 100644 --- a/packages/firestore/src/core/sync_engine.ts +++ b/packages/firestore/src/core/sync_engine.ts @@ -19,7 +19,8 @@ import { User } from '../auth/user'; import { ignoreIfPrimaryLeaseLoss, LocalStore, - LocalWriteResult + LocalWriteResult, + MultiTabLocalStore } from '../local/local_store'; import { LocalViewChanges } from '../local/local_view_changes'; import { ReferenceSet } from '../local/reference_set'; @@ -144,13 +145,13 @@ export interface SyncEngineListener { * The SyncEngine’s methods should only ever be called by methods running in the * global async queue. */ -export class SyncEngine implements RemoteSyncer, SharedClientStateSyncer { - private syncEngineListener: SyncEngineListener | null = null; +export class SyncEngine implements RemoteSyncer { + protected syncEngineListener: SyncEngineListener | null = null; - private queryViewsByQuery = new ObjectMap(q => + protected queryViewsByQuery = new ObjectMap(q => q.canonicalId() ); - private queriesByTarget = new Map(); + protected queriesByTarget = new Map(); /** * The keys of documents that are in limbo for which we haven't yet started a * limbo resolution query. @@ -160,15 +161,18 @@ export class SyncEngine implements RemoteSyncer, SharedClientStateSyncer { * Keeps track of the target ID for each document that is in limbo with an * active target. */ - private activeLimboTargetsByKey = new SortedMap( + protected activeLimboTargetsByKey = new SortedMap( DocumentKey.comparator ); /** * Keeps track of the information about an active limbo resolution for each * active target ID that was started for the purpose of limbo resolution. */ - private activeLimboResolutionsByTarget = new Map(); - private limboDocumentRefs = new ReferenceSet(); + protected activeLimboResolutionsByTarget = new Map< + TargetId, + LimboResolution + >(); + protected limboDocumentRefs = new ReferenceSet(); /** Stores user completion handlers, indexed by User and BatchId. */ private mutationUserCallbacks = {} as { [uidKey: string]: SortedMap>; @@ -177,24 +181,19 @@ export class SyncEngine implements RemoteSyncer, SharedClientStateSyncer { private pendingWritesCallbacks = new Map>>(); private limboTargetIdGenerator = TargetIdGenerator.forSyncEngine(); - // The primary state is set to `true` or `false` immediately after Firestore - // startup. In the interim, a client should only be considered primary if - // `isPrimary` is true. - private isPrimary: undefined | boolean = undefined; private onlineState = OnlineState.Unknown; constructor( - private localStore: LocalStore, - private remoteStore: RemoteStore, + protected localStore: LocalStore, + protected remoteStore: RemoteStore, // PORTING NOTE: Manages state synchronization in multi-tab environments. - private sharedClientState: SharedClientState, + protected sharedClientState: SharedClientState, private currentUser: User, private maxConcurrentLimboResolutions: number ) {} - // Only used for testing. get isPrimaryClient(): boolean { - return this.isPrimary === true; + return true; } /** Subscribes to SyncEngine notifications. Has to be called exactly once. */ @@ -245,7 +244,7 @@ export class SyncEngine implements RemoteSyncer, SharedClientStateSyncer { targetId, status === 'current' ); - if (this.isPrimary) { + if (this.isPrimaryClient) { this.remoteStore.listen(targetData); } } @@ -258,7 +257,7 @@ export class SyncEngine implements RemoteSyncer, SharedClientStateSyncer { * Registers a view for a previously unknown query and computes its initial * snapshot. */ - private async initializeViewAndComputeSnapshot( + protected async initializeViewAndComputeSnapshot( query: Query, targetId: TargetId, current: boolean @@ -275,7 +274,7 @@ export class SyncEngine implements RemoteSyncer, SharedClientStateSyncer { ); const viewChange = view.applyChanges( viewDocChanges, - /* updateLimboDocuments= */ this.isPrimary === true, + /* updateLimboDocuments= */ this.isPrimaryClient, synthesizedTargetChange ); this.updateTrackedLimbos(targetId, viewChange.limboChanges); @@ -295,27 +294,6 @@ export class SyncEngine implements RemoteSyncer, SharedClientStateSyncer { return viewChange.snapshot!; } - /** - * Reconcile the list of synced documents in an existing view with those - * from persistence. - */ - // PORTING NOTE: Multi-tab only. - private async synchronizeViewAndComputeSnapshot( - queryView: QueryView - ): Promise { - const queryResult = await this.localStore.executeQuery( - queryView.query, - /* usePreviousResults= */ true - ); - const viewSnapshot = queryView.view.synchronizeWithPersistedState( - queryResult - ); - if (this.isPrimary) { - this.updateTrackedLimbos(queryView.targetId, viewSnapshot.limboChanges); - } - return viewSnapshot; - } - /** Stops listening to the query. */ async unlisten(query: Query): Promise { this.assertSubscribed('unlisten()'); @@ -336,7 +314,7 @@ export class SyncEngine implements RemoteSyncer, SharedClientStateSyncer { } // No other queries are mapped to the target, clean up the query and the target. - if (this.isPrimary) { + if (this.isPrimaryClient) { // We need to remove the local query target first to allow us to verify // whether any other client is still interested in this target. this.sharedClientState.removeLocalQueryTarget(queryView.targetId); @@ -480,34 +458,21 @@ export class SyncEngine implements RemoteSyncer, SharedClientStateSyncer { onlineState: OnlineState, source: OnlineStateSource ): void { - // If we are the secondary client, we explicitly ignore the remote store's - // online state (the local client may go offline, even though the primary - // tab remains online) and only apply the primary tab's online state from - // SharedClientState. - if ( - (this.isPrimary && source === OnlineStateSource.RemoteStore) || - (!this.isPrimary && source === OnlineStateSource.SharedClientState) - ) { - this.assertSubscribed('applyOnlineStateChange()'); - const newViewSnapshots = [] as ViewSnapshot[]; - this.queryViewsByQuery.forEach((query, queryView) => { - const viewChange = queryView.view.applyOnlineStateChange(onlineState); - debugAssert( - viewChange.limboChanges.length === 0, - 'OnlineState should not affect limbo documents.' - ); - if (viewChange.snapshot) { - newViewSnapshots.push(viewChange.snapshot); - } - }); - this.syncEngineListener!.onOnlineStateChange(onlineState); - this.syncEngineListener!.onWatchChange(newViewSnapshots); - - this.onlineState = onlineState; - if (this.isPrimary) { - this.sharedClientState.setOnlineState(onlineState); + this.assertSubscribed('applyOnlineStateChange()'); + const newViewSnapshots = [] as ViewSnapshot[]; + this.queryViewsByQuery.forEach((query, queryView) => { + const viewChange = queryView.view.applyOnlineStateChange(onlineState); + debugAssert( + viewChange.limboChanges.length === 0, + 'OnlineState should not affect limbo documents.' + ); + if (viewChange.snapshot) { + newViewSnapshots.push(viewChange.snapshot); } - } + }); + this.syncEngineListener!.onOnlineStateChange(onlineState); + this.syncEngineListener!.onWatchChange(newViewSnapshots); + this.onlineState = onlineState; } async rejectListen(targetId: TargetId, err: FirestoreError): Promise { @@ -558,44 +523,6 @@ export class SyncEngine implements RemoteSyncer, SharedClientStateSyncer { } } - // PORTING NOTE: Multi-tab only - async applyBatchState( - batchId: BatchId, - batchState: MutationBatchState, - error?: FirestoreError - ): Promise { - this.assertSubscribed('applyBatchState()'); - const documents = await this.localStore.lookupMutationDocuments(batchId); - - if (documents === null) { - // A throttled tab may not have seen the mutation before it was completed - // and removed from the mutation queue, in which case we won't have cached - // the affected documents. In this case we can safely ignore the update - // since that means we didn't apply the mutation locally at all (if we - // had, we would have cached the affected documents), and so we will just - // see any resulting document changes via normal remote document updates - // as applicable. - logDebug(LOG_TAG, 'Cannot apply mutation batch with id: ' + batchId); - return; - } - - if (batchState === 'pending') { - // If we are the primary client, we need to send this write to the - // backend. Secondary clients will ignore these writes since their remote - // connection is disabled. - await this.remoteStore.fillWritePipeline(); - } else if (batchState === 'acknowledged' || batchState === 'rejected') { - // NOTE: Both these methods are no-ops for batches that originated from - // other clients. - this.processUserCallback(batchId, error ? error : null); - this.localStore.removeCachedMutationBatchMetadata(batchId); - } else { - fail(`Unknown batchState: ${batchState}`); - } - - await this.emitNewSnapsAndNotifyLocalStore(documents); - } - async applySuccessfulWrite( mutationBatchResult: MutationBatchResult ): Promise { @@ -711,7 +638,7 @@ export class SyncEngine implements RemoteSyncer, SharedClientStateSyncer { * Resolves or rejects the user callback for the given batch and then discards * it. */ - private processUserCallback(batchId: BatchId, error: Error | null): void { + protected processUserCallback(batchId: BatchId, error: Error | null): void { let newCallbacks = this.mutationUserCallbacks[this.currentUser.toKey()]; // NOTE: Mutations restored from persistence won't have callbacks, so it's @@ -734,7 +661,7 @@ export class SyncEngine implements RemoteSyncer, SharedClientStateSyncer { } } - private removeAndCleanupTarget( + protected removeAndCleanupTarget( targetId: number, error: Error | null = null ): void { @@ -755,7 +682,7 @@ export class SyncEngine implements RemoteSyncer, SharedClientStateSyncer { this.queriesByTarget.delete(targetId); - if (this.isPrimary) { + if (this.isPrimaryClient) { const limboKeys = this.limboDocumentRefs.referencesForId(targetId); this.limboDocumentRefs.removeReferencesForId(targetId); limboKeys.forEach(limboKey => { @@ -783,7 +710,7 @@ export class SyncEngine implements RemoteSyncer, SharedClientStateSyncer { this.pumpEnqueuedLimboResolutions(); } - private updateTrackedLimbos( + protected updateTrackedLimbos( targetId: TargetId, limboChanges: LimboDocumentChange[] ): void { @@ -860,7 +787,7 @@ export class SyncEngine implements RemoteSyncer, SharedClientStateSyncer { return this.enqueuedLimboResolutions; } - private async emitNewSnapsAndNotifyLocalStore( + protected async emitNewSnapsAndNotifyLocalStore( changes: MaybeDocumentMap, remoteEvent?: RemoteEvent ): Promise { @@ -893,7 +820,7 @@ export class SyncEngine implements RemoteSyncer, SharedClientStateSyncer { remoteEvent && remoteEvent.targetChanges.get(queryView.targetId); const viewChange = queryView.view.applyChanges( viewDocChanges, - /* updateLimboDocuments= */ this.isPrimary === true, + /* updateLimboDocuments= */ this.isPrimaryClient, targetChange ); this.updateTrackedLimbos( @@ -901,7 +828,7 @@ export class SyncEngine implements RemoteSyncer, SharedClientStateSyncer { viewChange.limboChanges ); if (viewChange.snapshot) { - if (this.isPrimary) { + if (this.isPrimaryClient) { this.sharedClientState.updateQueryState( queryView.targetId, viewChange.snapshot.fromCache ? 'not-current' : 'current' @@ -924,7 +851,7 @@ export class SyncEngine implements RemoteSyncer, SharedClientStateSyncer { await this.localStore.notifyLocalViewChanges(docChangesInAllViews); } - private assertSubscribed(fnName: string): void { + protected assertSubscribed(fnName: string): void { debugAssert( this.syncEngineListener !== null, 'Trying to call ' + fnName + ' before calling subscribe().' @@ -954,7 +881,156 @@ export class SyncEngine implements RemoteSyncer, SharedClientStateSyncer { await this.remoteStore.handleCredentialChange(); } - // PORTING NOTE: Multi-tab only + enableNetwork(): Promise { + return this.remoteStore.enableNetwork(); + } + + disableNetwork(): Promise { + return this.remoteStore.disableNetwork(); + } + + getRemoteKeysForTarget(targetId: TargetId): DocumentKeySet { + const limboResolution = this.activeLimboResolutionsByTarget.get(targetId); + if (limboResolution && limboResolution.receivedDocument) { + return documentKeySet().add(limboResolution.key); + } else { + let keySet = documentKeySet(); + const queries = this.queriesByTarget.get(targetId); + if (!queries) { + return keySet; + } + for (const query of queries) { + const queryView = this.queryViewsByQuery.get(query); + debugAssert(!!queryView, `No query view found for ${query}`); + keySet = keySet.unionWith(queryView.view.syncedDocuments); + } + return keySet; + } + } +} + +/** + * An impplementation of SyncEngine that implement SharedClientStateSyncer for + * Multi-Tab synchronization. + */ +// PORTING NOTE: Web only +export class MultiTabSyncEngine extends SyncEngine + implements SharedClientStateSyncer { + // The primary state is set to `true` or `false` immediately after Firestore + // startup. In the interim, a client should only be considered primary if + // `isPrimary` is true. + private isPrimary: undefined | boolean = undefined; + + constructor( + protected localStore: MultiTabLocalStore, + remoteStore: RemoteStore, + sharedClientState: SharedClientState, + currentUser: User, + maxConcurrentLimboResolutions: number + ) { + super( + localStore, + remoteStore, + sharedClientState, + currentUser, + maxConcurrentLimboResolutions + ); + } + + get isPrimaryClient(): boolean { + return this.isPrimary === true; + } + + enableNetwork(): Promise { + this.localStore.setNetworkEnabled(true); + return super.enableNetwork(); + } + + disableNetwork(): Promise { + this.localStore.setNetworkEnabled(false); + return super.disableNetwork(); + } + + /** + * Reconcile the list of synced documents in an existing view with those + * from persistence. + */ + private async synchronizeViewAndComputeSnapshot( + queryView: QueryView + ): Promise { + const queryResult = await this.localStore.executeQuery( + queryView.query, + /* usePreviousResults= */ true + ); + const viewSnapshot = queryView.view.synchronizeWithPersistedState( + queryResult + ); + if (this.isPrimary) { + this.updateTrackedLimbos(queryView.targetId, viewSnapshot.limboChanges); + } + return viewSnapshot; + } + + applyOnlineStateChange( + onlineState: OnlineState, + source: OnlineStateSource + ): void { + // If we are the primary client, the online state of all clients only + // depends on the online state of the local RemoteStore. + if (this.isPrimaryClient && source === OnlineStateSource.RemoteStore) { + super.applyOnlineStateChange(onlineState, source); + this.sharedClientState.setOnlineState(onlineState); + } + + // If we are the secondary client, we explicitly ignore the remote store's + // online state (the local client may go offline, even though the primary + // tab remains online) and only apply the primary tab's online state from + // SharedClientState. + if ( + !this.isPrimaryClient && + source === OnlineStateSource.SharedClientState + ) { + super.applyOnlineStateChange(onlineState, source); + } + } + + async applyBatchState( + batchId: BatchId, + batchState: MutationBatchState, + error?: FirestoreError + ): Promise { + this.assertSubscribed('applyBatchState()'); + const documents = await this.localStore.lookupMutationDocuments(batchId); + + if (documents === null) { + // A throttled tab may not have seen the mutation before it was completed + // and removed from the mutation queue, in which case we won't have cached + // the affected documents. In this case we can safely ignore the update + // since that means we didn't apply the mutation locally at all (if we + // had, we would have cached the affected documents), and so we will just + // see any resulting document changes via normal remote document updates + // as applicable. + logDebug(LOG_TAG, 'Cannot apply mutation batch with id: ' + batchId); + return; + } + + if (batchState === 'pending') { + // If we are the primary client, we need to send this write to the + // backend. Secondary clients will ignore these writes since their remote + // connection is disabled. + await this.remoteStore.fillWritePipeline(); + } else if (batchState === 'acknowledged' || batchState === 'rejected') { + // NOTE: Both these methods are no-ops for batches that originated from + // other clients. + this.processUserCallback(batchId, error ? error : null); + this.localStore.removeCachedMutationBatchMetadata(batchId); + } else { + fail(`Unknown batchState: ${batchState}`); + } + + await this.emitNewSnapsAndNotifyLocalStore(documents); + } + async applyPrimaryState(isPrimary: boolean): Promise { if (isPrimary === true && this.isPrimary !== true) { this.isPrimary = true; @@ -1001,7 +1077,6 @@ export class SyncEngine implements RemoteSyncer, SharedClientStateSyncer { } } - // PORTING NOTE: Multi-tab only. private resetLimboDocuments(): void { this.activeLimboResolutionsByTarget.forEach((_, targetId) => { this.remoteStore.unlisten(targetId); @@ -1018,7 +1093,6 @@ export class SyncEngine implements RemoteSyncer, SharedClientStateSyncer { * persistence. Raises snapshots for any changes that affect the local * client and returns the updated state of all target's query data. */ - // PORTING NOTE: Multi-tab only. private async synchronizeQueryViewsAndRaiseSnapshots( targets: TargetId[] ): Promise { @@ -1086,7 +1160,6 @@ export class SyncEngine implements RemoteSyncer, SharedClientStateSyncer { * original one (only the presentation of results might differ), the potential * difference will not cause issues. */ - // PORTING NOTE: Multi-tab only private synthesizeTargetToQuery(target: Target): Query { return new Query( target.path, @@ -1100,12 +1173,10 @@ export class SyncEngine implements RemoteSyncer, SharedClientStateSyncer { ); } - // PORTING NOTE: Multi-tab only getActiveClients(): Promise { return this.localStore.getActiveClients(); } - // PORTING NOTE: Multi-tab only async applyTargetState( targetId: TargetId, state: QueryTargetState, @@ -1147,7 +1218,6 @@ export class SyncEngine implements RemoteSyncer, SharedClientStateSyncer { } } - // PORTING NOTE: Multi-tab only async applyActiveTargetsChange( added: TargetId[], removed: TargetId[] @@ -1194,37 +1264,4 @@ export class SyncEngine implements RemoteSyncer, SharedClientStateSyncer { .catch(ignoreIfPrimaryLeaseLoss); } } - - // PORTING NOTE: Multi-tab only. In other clients, LocalStore is unaware of - // the online state. - enableNetwork(): Promise { - this.localStore.setNetworkEnabled(true); - return this.remoteStore.enableNetwork(); - } - - // PORTING NOTE: Multi-tab only. In other clients, LocalStore is unaware of - // the online state. - disableNetwork(): Promise { - this.localStore.setNetworkEnabled(false); - return this.remoteStore.disableNetwork(); - } - - getRemoteKeysForTarget(targetId: TargetId): DocumentKeySet { - const limboResolution = this.activeLimboResolutionsByTarget.get(targetId); - if (limboResolution && limboResolution.receivedDocument) { - return documentKeySet().add(limboResolution.key); - } else { - let keySet = documentKeySet(); - const queries = this.queriesByTarget.get(targetId); - if (!queries) { - return keySet; - } - for (const query of queries) { - const queryView = this.queryViewsByQuery.get(query); - debugAssert(!!queryView, `No query view found for ${query}`); - keySet = keySet.unionWith(queryView.view.syncedDocuments); - } - return keySet; - } - } } diff --git a/packages/firestore/src/local/indexeddb_mutation_queue.ts b/packages/firestore/src/local/indexeddb_mutation_queue.ts index 01afda5c601..860323f77c1 100644 --- a/packages/firestore/src/local/indexeddb_mutation_queue.ts +++ b/packages/firestore/src/local/indexeddb_mutation_queue.ts @@ -234,6 +234,13 @@ export class IndexedDbMutationQueue implements MutationQueue { }); } + /** + * Returns the document keys for the mutation batch with the given batchId. + * For primary clients, this method returns `null` after + * `removeMutationBatches()` has been called. Secondary clients return a + * cached result until `removeCachedMutationKeys()` is invoked. + */ + // PORTING NOTE: Multi-tab only. lookupMutationKeys( transaction: PersistenceTransaction, batchId: BatchId @@ -521,6 +528,15 @@ export class IndexedDbMutationQueue implements MutationQueue { }); } + /** + * Clears the cached keys for a mutation batch. This method should be + * called by secondary clients after they process mutation updates. + * + * Note that this method does not have to be called from primary clients as + * the corresponding cache entries are cleared when an acknowledged or + * rejected batch is removed from the mutation queue. + */ + // PORTING NOTE: Multi-tab only removeCachedMutationKeys(batchId: BatchId): void { delete this.documentKeysByBatchId[batchId]; } diff --git a/packages/firestore/src/local/indexeddb_persistence.ts b/packages/firestore/src/local/indexeddb_persistence.ts index f1e683a9bbe..dabc3435025 100644 --- a/packages/firestore/src/local/indexeddb_persistence.ts +++ b/packages/firestore/src/local/indexeddb_persistence.ts @@ -29,8 +29,8 @@ import { logDebug, logError } from '../util/log'; import { CancelablePromise } from '../util/promise'; import { decodeResourcePath, - encodeResourcePath, - EncodedResourcePath + EncodedResourcePath, + encodeResourcePath } from './encoded_resource_path'; import { IndexedDbIndexManager } from './indexeddb_index_manager'; import { @@ -61,7 +61,6 @@ import { LruGarbageCollector, LruParams } from './lru_garbage_collector'; -import { MutationQueue } from './mutation_queue'; import { Persistence, PersistenceTransaction, @@ -189,36 +188,6 @@ export class IndexedDbPersistence implements Persistence { */ static MAIN_DATABASE = 'main'; - static createIndexedDbPersistence(options: { - allowTabSynchronization: boolean; - persistenceKey: string; - clientId: ClientId; - platform: Platform; - lruParams: LruParams; - queue: AsyncQueue; - serializer: JsonProtoSerializer; - sequenceNumberSyncer: SequenceNumberSyncer; - }): IndexedDbPersistence { - if (!IndexedDbPersistence.isAvailable()) { - throw new FirestoreError( - Code.UNIMPLEMENTED, - UNSUPPORTED_PLATFORM_ERROR_MSG - ); - } - - const persistence = new IndexedDbPersistence( - options.allowTabSynchronization, - options.persistenceKey, - options.clientId, - options.platform, - options.lruParams, - options.queue, - options.serializer, - options.sequenceNumberSyncer - ); - return persistence; - } - private readonly document: Document | null; private readonly window: Window; @@ -257,7 +226,7 @@ export class IndexedDbPersistence implements Persistence { private readonly webStorage: Storage; readonly referenceDelegate: IndexedDbLruDelegate; - private constructor( + constructor( private readonly allowTabSynchronization: boolean, private readonly persistenceKey: string, private readonly clientId: ClientId, @@ -267,6 +236,13 @@ export class IndexedDbPersistence implements Persistence { serializer: JsonProtoSerializer, private readonly sequenceNumberSyncer: SequenceNumberSyncer ) { + if (!IndexedDbPersistence.isAvailable()) { + throw new FirestoreError( + Code.UNIMPLEMENTED, + UNSUPPORTED_PLATFORM_ERROR_MSG + ); + } + this.referenceDelegate = new IndexedDbLruDelegate(this, lruParams); this.dbName = persistenceKey + IndexedDbPersistence.MAIN_DATABASE; this.serializer = new LocalSerializer(serializer); @@ -338,6 +314,13 @@ export class IndexedDbPersistence implements Persistence { }); } + /** + * Registers a listener that gets called when the primary state of the + * instance changes. Upon registering, this listener is invoked immediately + * with the current primary state. + * + * PORTING NOTE: This is only used for Web multi-tab. + */ setPrimaryStateListener( primaryStateListener: PrimaryStateListener ): Promise { @@ -349,6 +332,12 @@ export class IndexedDbPersistence implements Persistence { return primaryStateListener(this.isPrimary); } + /** + * Registers a listener that gets called when the database receives a + * version change event indicating that it has deleted. + * + * PORTING NOTE: This is only used for Web multi-tab. + */ setDatabaseDeletedListener( databaseDeletedListener: () => Promise ): void { @@ -360,6 +349,12 @@ export class IndexedDbPersistence implements Persistence { }); } + /** + * Adjusts the current network state in the client's metadata, potentially + * affecting the primary lease. + * + * PORTING NOTE: This is only used for Web multi-tab. + */ setNetworkEnabled(networkEnabled: boolean): void { if (this.networkEnabled !== networkEnabled) { this.networkEnabled = networkEnabled; @@ -681,6 +676,13 @@ export class IndexedDbPersistence implements Persistence { ); } + /** + * Returns the IDs of the clients that are currently active. If multi-tab + * is not supported, returns an array that only contains the local client's + * ID. + * + * PORTING NOTE: This is only used for Web multi-tab. + */ getActiveClients(): Promise { return this.simpleDb.runTransaction( 'readonly', @@ -709,7 +711,7 @@ export class IndexedDbPersistence implements Persistence { return this._started; } - getMutationQueue(user: User): MutationQueue { + getMutationQueue(user: User): IndexedDbMutationQueue { debugAssert( this.started, 'Cannot initialize MutationQueue before persistence is started.' diff --git a/packages/firestore/src/local/indexeddb_remote_document_cache.ts b/packages/firestore/src/local/indexeddb_remote_document_cache.ts index 9adb4116509..06d42ef0dce 100644 --- a/packages/firestore/src/local/indexeddb_remote_document_cache.ts +++ b/packages/firestore/src/local/indexeddb_remote_document_cache.ts @@ -291,6 +291,11 @@ export class IndexedDbRemoteDocumentCache implements RemoteDocumentCache { .next(() => results); } + /** + * Returns the set of documents that have changed since the specified read + * time. + */ + // PORTING NOTE: This is only used for multi-tab synchronization. getNewDocumentChanges( transaction: PersistenceTransaction, sinceReadTime: SnapshotVersion @@ -323,6 +328,11 @@ export class IndexedDbRemoteDocumentCache implements RemoteDocumentCache { }); } + /** + * Returns the read time of the most recently read document in the cache, or + * SnapshotVersion.MIN if not available. + */ + // PORTING NOTE: This is only used for multi-tab synchronization. getLastReadTime( transaction: PersistenceTransaction ): PersistencePromise { diff --git a/packages/firestore/src/local/indexeddb_target_cache.ts b/packages/firestore/src/local/indexeddb_target_cache.ts index 974fd006b1b..3f5fea5ae48 100644 --- a/packages/firestore/src/local/indexeddb_target_cache.ts +++ b/packages/firestore/src/local/indexeddb_target_cache.ts @@ -374,6 +374,14 @@ export class IndexedDbTargetCache implements TargetCache { .next(() => count > 0); } + /** + * Looks up a TargetData entry by target ID. + * + * @param targetId The target ID of the TargetData entry to look up. + * @return The cached TargetData entry, or null if the cache has no entry for + * the target. + */ + // PORTING NOTE: Multi-tab only. getTargetDataForTarget( transaction: PersistenceTransaction, targetId: TargetId diff --git a/packages/firestore/src/local/local_store.ts b/packages/firestore/src/local/local_store.ts index eb85db1153e..d86a2a5a97d 100644 --- a/packages/firestore/src/local/local_store.ts +++ b/packages/firestore/src/local/local_store.ts @@ -62,6 +62,10 @@ import { RemoteDocumentChangeBuffer } from './remote_document_change_buffer'; import { ClientId } from './shared_client_state'; import { TargetData, TargetPurpose } from './target_data'; import { ByteString } from '../util/byte_string'; +import { IndexedDbPersistence } from './indexeddb_persistence'; +import { IndexedDbMutationQueue } from './indexeddb_mutation_queue'; +import { IndexedDbRemoteDocumentCache } from './indexeddb_remote_document_cache'; +import { IndexedDbTargetCache } from './indexeddb_target_cache'; const LOG_TAG = 'LocalStore'; @@ -149,16 +153,16 @@ export class LocalStore { * The set of all mutations that have been sent but not yet been applied to * the backend. */ - private mutationQueue: MutationQueue; + protected mutationQueue: MutationQueue; /** The set of all cached remote documents. */ - private remoteDocuments: RemoteDocumentCache; + protected remoteDocuments: RemoteDocumentCache; /** * The "local" view of all documents (layering mutationQueue on top of * remoteDocumentCache). */ - private localDocuments: LocalDocumentsView; + protected localDocuments: LocalDocumentsView; /** * The set of document references maintained by any local views. @@ -166,7 +170,7 @@ export class LocalStore { private localViewReferences = new ReferenceSet(); /** Maps a target to its `TargetData`. */ - private targetCache: TargetCache; + protected targetCache: TargetCache; /** * Maps a targetID to data about its target. @@ -174,7 +178,7 @@ export class LocalStore { * PORTING NOTE: We are using an immutable data structure on Web to make re-runs * of `applyRemoteEvent()` idempotent. */ - private targetDataByTarget = new SortedMap( + protected targetDataByTarget = new SortedMap( primitiveComparator ); @@ -189,11 +193,11 @@ export class LocalStore { * * PORTING NOTE: This is only used for multi-tab synchronization. */ - private lastDocumentChangeReadTime = SnapshotVersion.MIN; + protected lastDocumentChangeReadTime = SnapshotVersion.MIN; constructor( /** Manages our in-memory or durable persistence. */ - private persistence: Persistence, + protected persistence: Persistence, private queryEngine: QueryEngine, initialUser: User ) { @@ -217,7 +221,7 @@ export class LocalStore { /** Starts the LocalStore. */ start(): Promise { - return this.synchronizeLastDocumentChangeReadTime(); + return Promise.resolve(); } /** @@ -356,29 +360,6 @@ export class LocalStore { }); } - /** Returns the local view of the documents affected by a mutation batch. */ - // PORTING NOTE: Multi-tab only. - lookupMutationDocuments(batchId: BatchId): Promise { - return this.persistence.runTransaction( - 'Lookup mutation documents', - 'readonly', - txn => { - return this.mutationQueue - .lookupMutationKeys(txn, batchId) - .next(keys => { - if (keys) { - return this.localDocuments.getDocuments( - txn, - keys - ) as PersistencePromise; - } else { - return PersistencePromise.resolve(null); - } - }); - } - ); - } - /** * Acknowledge the given batch. * @@ -975,21 +956,6 @@ export class LocalStore { ); } - // PORTING NOTE: Multi-tab only. - getActiveClients(): Promise { - return this.persistence.getActiveClients(); - } - - // PORTING NOTE: Multi-tab only. - removeCachedMutationBatchMetadata(batchId: BatchId): void { - this.mutationQueue.removeCachedMutationKeys(batchId); - } - - // PORTING NOTE: Multi-tab only. - setNetworkEnabled(networkEnabled: boolean): void { - this.persistence.setNetworkEnabled(networkEnabled); - } - private applyWriteToRemoteDocuments( txn: PersistenceTransaction, batchResult: MutationBatchResult, @@ -1042,8 +1008,69 @@ export class LocalStore { txn => garbageCollector.collect(txn, this.targetDataByTarget) ); } +} + +/** + * An implementation of LocalStore that provides additional functionality + * for MultiTabSyncEngine. + */ +// PORTING NOTE: Web only. +export class MultiTabLocalStore extends LocalStore { + protected mutationQueue: IndexedDbMutationQueue; + protected remoteDocuments: IndexedDbRemoteDocumentCache; + protected targetCache: IndexedDbTargetCache; + + constructor( + protected persistence: IndexedDbPersistence, + queryEngine: QueryEngine, + initialUser: User + ) { + super(persistence, queryEngine, initialUser); + + this.mutationQueue = persistence.getMutationQueue(initialUser); + this.remoteDocuments = persistence.getRemoteDocumentCache(); + this.targetCache = persistence.getTargetCache(); + } + + /** Starts the LocalStore. */ + start(): Promise { + return this.synchronizeLastDocumentChangeReadTime(); + } + + /** Returns the local view of the documents affected by a mutation batch. */ + lookupMutationDocuments(batchId: BatchId): Promise { + return this.persistence.runTransaction( + 'Lookup mutation documents', + 'readonly', + txn => { + return this.mutationQueue + .lookupMutationKeys(txn, batchId) + .next(keys => { + if (keys) { + return this.localDocuments.getDocuments( + txn, + keys + ) as PersistencePromise; + } else { + return PersistencePromise.resolve(null); + } + }); + } + ); + } + + removeCachedMutationBatchMetadata(batchId: BatchId): void { + this.mutationQueue.removeCachedMutationKeys(batchId); + } + + setNetworkEnabled(networkEnabled: boolean): void { + this.persistence.setNetworkEnabled(networkEnabled); + } + + getActiveClients(): Promise { + return this.persistence.getActiveClients(); + } - // PORTING NOTE: Multi-tab only. getTarget(targetId: TargetId): Promise { const cachedTargetData = this.targetDataByTarget.get(targetId); @@ -1068,7 +1095,6 @@ export class LocalStore { * initialization. Further invocations will return document changes since * the point of rejection. */ - // PORTING NOTE: Multi-tab only. getNewDocumentChanges(): Promise { return this.persistence .runTransaction('Get new document changes', 'readonly', txn => @@ -1088,7 +1114,6 @@ export class LocalStore { * synchronization marker so that calls to `getNewDocumentChanges()` * only return changes that happened after client initialization. */ - // PORTING NOTE: Multi-tab only. async synchronizeLastDocumentChangeReadTime(): Promise { this.lastDocumentChangeReadTime = await this.persistence.runTransaction( 'Synchronize last document change read time', diff --git a/packages/firestore/src/local/memory_mutation_queue.ts b/packages/firestore/src/local/memory_mutation_queue.ts index abad09cd2a8..63de7419e5f 100644 --- a/packages/firestore/src/local/memory_mutation_queue.ts +++ b/packages/firestore/src/local/memory_mutation_queue.ts @@ -18,11 +18,10 @@ import { Timestamp } from '../api/timestamp'; import { Query } from '../core/query'; import { BatchId } from '../core/types'; -import { DocumentKeySet } from '../model/collections'; import { DocumentKey } from '../model/document_key'; import { Mutation } from '../model/mutation'; import { MutationBatch, BATCHID_UNKNOWN } from '../model/mutation_batch'; -import { hardAssert, debugAssert } from '../util/assert'; +import { debugAssert, hardAssert } from '../util/assert'; import { primitiveComparator } from '../util/misc'; import { ByteString } from '../util/byte_string'; import { SortedMap } from '../util/sorted_map'; @@ -151,17 +150,6 @@ export class MemoryMutationQueue implements MutationQueue { return PersistencePromise.resolve(this.findMutationBatch(batchId)); } - lookupMutationKeys( - transaction: PersistenceTransaction, - batchId: BatchId - ): PersistencePromise { - const mutationBatch = this.findMutationBatch(batchId); - debugAssert(mutationBatch != null, 'Failed to find local mutation batch.'); - return PersistencePromise.resolve( - mutationBatch.keys() - ); - } - getNextMutationBatchAfterBatchId( transaction: PersistenceTransaction, batchId: BatchId diff --git a/packages/firestore/src/local/memory_persistence.ts b/packages/firestore/src/local/memory_persistence.ts index 553705279fc..1ded2738f3a 100644 --- a/packages/firestore/src/local/memory_persistence.ts +++ b/packages/firestore/src/local/memory_persistence.ts @@ -40,12 +40,10 @@ import { Persistence, PersistenceTransaction, PersistenceTransactionMode, - PrimaryStateListener, ReferenceDelegate } from './persistence'; import { PersistencePromise } from './persistence_promise'; import { ReferenceSet } from './reference_set'; -import { ClientId } from './shared_client_state'; import { TargetData } from './target_data'; const LOG_TAG = 'MemoryPersistence'; @@ -78,7 +76,6 @@ export class MemoryPersistence implements Persistence { * checked or asserted on every access. */ constructor( - private readonly clientId: ClientId, referenceDelegateFactory: (p: MemoryPersistence) => MemoryReferenceDelegate ) { this._started = true; @@ -107,25 +104,10 @@ export class MemoryPersistence implements Persistence { return this._started; } - async getActiveClients(): Promise { - return [this.clientId]; - } - - setPrimaryStateListener( - primaryStateListener: PrimaryStateListener - ): Promise { - // All clients using memory persistence act as primary. - return primaryStateListener(true); - } - setDatabaseDeletedListener(): void { // No op. } - setNetworkEnabled(networkEnabled: boolean): void { - // No op. - } - getIndexManager(): MemoryIndexManager { return this.indexManager; } diff --git a/packages/firestore/src/local/memory_remote_document_cache.ts b/packages/firestore/src/local/memory_remote_document_cache.ts index b9c11eca097..4c53ed9551b 100644 --- a/packages/firestore/src/local/memory_remote_document_cache.ts +++ b/packages/firestore/src/local/memory_remote_document_cache.ts @@ -21,7 +21,6 @@ import { DocumentMap, documentMap, DocumentSizeEntry, - MaybeDocumentMap, NullableMaybeDocumentMap, nullableMaybeDocumentMap } from '../model/collections'; @@ -176,24 +175,6 @@ export class MemoryRemoteDocumentCache implements RemoteDocumentCache { return PersistencePromise.forEach(this.docs, (key: DocumentKey) => f(key)); } - getNewDocumentChanges( - transaction: PersistenceTransaction, - sinceReadTime: SnapshotVersion - ): PersistencePromise<{ - changedDocs: MaybeDocumentMap; - readTime: SnapshotVersion; - }> { - throw new Error( - 'getNewDocumentChanges() is not supported with MemoryPersistence' - ); - } - - getLastReadTime( - transaction: PersistenceTransaction - ): PersistencePromise { - return PersistencePromise.resolve(SnapshotVersion.MIN); - } - newChangeBuffer(options?: { trackRemovals: boolean; }): RemoteDocumentChangeBuffer { diff --git a/packages/firestore/src/local/memory_target_cache.ts b/packages/firestore/src/local/memory_target_cache.ts index b69883447b8..d33cfbe41f2 100644 --- a/packages/firestore/src/local/memory_target_cache.ts +++ b/packages/firestore/src/local/memory_target_cache.ts @@ -20,7 +20,7 @@ import { TargetIdGenerator } from '../core/target_id_generator'; import { ListenSequenceNumber, TargetId } from '../core/types'; import { DocumentKeySet } from '../model/collections'; import { DocumentKey } from '../model/document_key'; -import { debugAssert, fail } from '../util/assert'; +import { debugAssert } from '../util/assert'; import { ObjectMap } from '../util/obj_map'; import { ActiveTargets } from './lru_garbage_collector'; @@ -185,15 +185,6 @@ export class MemoryTargetCache implements TargetCache { return PersistencePromise.resolve(targetData); } - getTargetDataForTarget( - transaction: PersistenceTransaction, - targetId: TargetId - ): never { - // This method is only needed for multi-tab and we can't implement it - // efficiently without additional data structures. - return fail('Not yet implemented.'); - } - addMatchingKeys( txn: PersistenceTransaction, keys: DocumentKeySet, diff --git a/packages/firestore/src/local/mutation_queue.ts b/packages/firestore/src/local/mutation_queue.ts index 6cfb00675bb..fc5d8551915 100644 --- a/packages/firestore/src/local/mutation_queue.ts +++ b/packages/firestore/src/local/mutation_queue.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright 2017 Google Inc. + * Copyright 2017 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,7 +18,6 @@ import { Timestamp } from '../api/timestamp'; import { Query } from '../core/query'; import { BatchId } from '../core/types'; -import { DocumentKeySet } from '../model/collections'; import { DocumentKey } from '../model/document_key'; import { Mutation } from '../model/mutation'; import { MutationBatch } from '../model/mutation_batch'; @@ -78,18 +77,6 @@ export interface MutationQueue { batchId: BatchId ): PersistencePromise; - /** - * Returns the document keys for the mutation batch with the given batchId. - * For primary clients, this method returns `null` after - * `removeMutationBatches()` has been called. Secondary clients return a - * cached result until `removeCachedMutationKeys()` is invoked. - */ - // PORTING NOTE: Multi-tab only. - lookupMutationKeys( - transaction: PersistenceTransaction, - batchId: BatchId - ): PersistencePromise; - /** * Gets the first unacknowledged mutation batch after the passed in batchId * in the mutation queue or null if empty. @@ -193,17 +180,6 @@ export interface MutationQueue { batch: MutationBatch ): PersistencePromise; - /** - * Clears the cached keys for a mutation batch. This method should be - * called by secondary clients after they process mutation updates. - * - * Note that this method does not have to be called from primary clients as - * the corresponding cache entries are cleared when an acknowledged or - * rejected batch is removed from the mutation queue. - */ - // PORTING NOTE: Multi-tab only - removeCachedMutationKeys(batchId: BatchId): void; - /** * Performs a consistency check, examining the mutation queue for any * leaks, if possible. diff --git a/packages/firestore/src/local/persistence.ts b/packages/firestore/src/local/persistence.ts index f5e11f5604c..43a2dfcc5fe 100644 --- a/packages/firestore/src/local/persistence.ts +++ b/packages/firestore/src/local/persistence.ts @@ -26,7 +26,6 @@ import { TargetCache } from './target_cache'; import { ReferenceSet } from './reference_set'; import { RemoteDocumentCache } from './remote_document_cache'; import { TargetData } from './target_data'; -import { ClientId } from './shared_client_state'; export const PRIMARY_LEASE_LOST_ERROR_MSG = 'The current tab is not in the required state to perform this operation. ' + @@ -178,17 +177,6 @@ export interface Persistence { */ shutdown(): Promise; - /** - * Registers a listener that gets called when the primary state of the - * instance changes. Upon registering, this listener is invoked immediately - * with the current primary state. - * - * PORTING NOTE: This is only used for Web multi-tab. - */ - setPrimaryStateListener( - primaryStateListener: PrimaryStateListener - ): Promise; - /** * Registers a listener that gets called when the database receives a * version change event indicating that it has deleted. @@ -199,23 +187,6 @@ export interface Persistence { databaseDeletedListener: () => Promise ): void; - /** - * Adjusts the current network state in the client's metadata, potentially - * affecting the primary lease. - * - * PORTING NOTE: This is only used for Web multi-tab. - */ - setNetworkEnabled(networkEnabled: boolean): void; - - /** - * Returns the IDs of the clients that are currently active. If multi-tab - * is not supported, returns an array that only contains the local client's - * ID. - * - * PORTING NOTE: This is only used for Web multi-tab. - */ - getActiveClients(): Promise; - /** * Returns a MutationQueue representing the persisted mutations for the * given user. diff --git a/packages/firestore/src/local/remote_document_cache.ts b/packages/firestore/src/local/remote_document_cache.ts index bd91530bdaa..a538c28e8b6 100644 --- a/packages/firestore/src/local/remote_document_cache.ts +++ b/packages/firestore/src/local/remote_document_cache.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright 2017 Google Inc. + * Copyright 2017 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,7 +19,6 @@ import { Query } from '../core/query'; import { DocumentKeySet, DocumentMap, - MaybeDocumentMap, NullableMaybeDocumentMap } from '../model/collections'; import { MaybeDocument } from '../model/document'; @@ -82,28 +81,6 @@ export interface RemoteDocumentCache { sinceReadTime: SnapshotVersion ): PersistencePromise; - /** - * Returns the set of documents that have changed since the specified read - * time. - */ - // PORTING NOTE: This is only used for multi-tab synchronization. - getNewDocumentChanges( - transaction: PersistenceTransaction, - sinceReadTime: SnapshotVersion - ): PersistencePromise<{ - changedDocs: MaybeDocumentMap; - readTime: SnapshotVersion; - }>; - - /** - * Returns the read time of the most recently read document in the cache, or - * SnapshotVersion.MIN if not available. - */ - // PORTING NOTE: This is only used for multi-tab synchronization. - getLastReadTime( - transaction: PersistenceTransaction - ): PersistencePromise; - /** * Provides access to add or update the contents of the cache. The buffer * handles proper size accounting for the change. diff --git a/packages/firestore/src/local/target_cache.ts b/packages/firestore/src/local/target_cache.ts index e576e6eabb4..657c4eecfaa 100644 --- a/packages/firestore/src/local/target_cache.ts +++ b/packages/firestore/src/local/target_cache.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright 2017 Google Inc. + * Copyright 2017 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -133,19 +133,6 @@ export interface TargetCache { target: Target ): PersistencePromise; - /** - * Looks up a TargetData entry by target ID. - * - * @param targetId The target ID of the TargetData entry to look up. - * @return The cached TargetData entry, or null if the cache has no entry for - * the target. - */ - // PORTING NOTE: Multi-tab only. - getTargetDataForTarget( - txn: PersistenceTransaction, - targetId: TargetId - ): PersistencePromise; - /** * Adds the given document keys to cached query results of the given target * ID. diff --git a/packages/firestore/test/unit/local/counting_query_engine.ts b/packages/firestore/test/unit/local/counting_query_engine.ts index 7a4e2625b8c..b94f502f95d 100644 --- a/packages/firestore/test/unit/local/counting_query_engine.ts +++ b/packages/firestore/test/unit/local/counting_query_engine.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright 2019 Google Inc. + * Copyright 2019 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,6 +25,11 @@ import { RemoteDocumentCache } from '../../../src/local/remote_document_cache'; import { MutationQueue } from '../../../src/local/mutation_queue'; import { DocumentKeySet, DocumentMap } from '../../../src/model/collections'; +export enum QueryEngineType { + IndexFree, + Simple +} + /** * A test-only query engine that forwards all API calls and exposes the number * of documents and mutations read. @@ -57,7 +62,10 @@ export class CountingQueryEngine implements QueryEngine { */ documentsReadByKey = 0; - constructor(private readonly queryEngine: QueryEngine) {} + constructor( + private readonly queryEngine: QueryEngine, + readonly type: QueryEngineType + ) {} resetCounts(): void { this.mutationsReadByQuery = 0; @@ -114,8 +122,6 @@ export class CountingQueryEngine implements QueryEngine { return result; }); }, - getNewDocumentChanges: subject.getNewDocumentChanges, - getLastReadTime: subject.getLastReadTime, getSize: subject.getSize, newChangeBuffer: subject.newChangeBuffer }; @@ -164,9 +170,7 @@ export class CountingQueryEngine implements QueryEngine { getNextMutationBatchAfterBatchId: subject.getNextMutationBatchAfterBatchId, lookupMutationBatch: subject.lookupMutationBatch, - lookupMutationKeys: subject.lookupMutationKeys, performConsistencyCheck: subject.performConsistencyCheck, - removeCachedMutationKeys: subject.removeCachedMutationKeys, removeMutationBatch: subject.removeMutationBatch, setLastStreamToken: subject.setLastStreamToken }; diff --git a/packages/firestore/test/unit/local/indexeddb_persistence.test.ts b/packages/firestore/test/unit/local/indexeddb_persistence.test.ts index 234a7a0ab3a..92d5b6f31a2 100644 --- a/packages/firestore/test/unit/local/indexeddb_persistence.test.ts +++ b/packages/firestore/test/unit/local/indexeddb_persistence.test.ts @@ -126,16 +126,16 @@ async function withCustomPersistence( PlatformSupport.getPlatform(), new SharedFakeWebStorage() ); - const persistence = await IndexedDbPersistence.createIndexedDbPersistence({ - allowTabSynchronization: multiClient, - persistenceKey: TEST_PERSISTENCE_PREFIX, + const persistence = new IndexedDbPersistence( + multiClient, + TEST_PERSISTENCE_PREFIX, clientId, platform, + LruParams.DEFAULT, queue, serializer, - lruParams: LruParams.DEFAULT, - sequenceNumberSyncer: MOCK_SEQUENCE_NUMBER_SYNCER - }); + MOCK_SEQUENCE_NUMBER_SYNCER + ); await persistence.start(); await fn(persistence, platform, queue); diff --git a/packages/firestore/test/unit/local/local_store.test.ts b/packages/firestore/test/unit/local/local_store.test.ts index 9e0bbfc0113..e919d12ff69 100644 --- a/packages/firestore/test/unit/local/local_store.test.ts +++ b/packages/firestore/test/unit/local/local_store.test.ts @@ -23,14 +23,17 @@ import { Timestamp } from '../../../src/api/timestamp'; import { User } from '../../../src/auth/user'; import { Query } from '../../../src/core/query'; import { Target } from '../../../src/core/target'; -import { TargetId, BatchId } from '../../../src/core/types'; +import { BatchId, TargetId } from '../../../src/core/types'; import { SnapshotVersion } from '../../../src/core/snapshot_version'; import { IndexFreeQueryEngine } from '../../../src/local/index_free_query_engine'; import { IndexedDbPersistence } from '../../../src/local/indexeddb_persistence'; -import { LocalStore, LocalWriteResult } from '../../../src/local/local_store'; +import { + LocalStore, + LocalWriteResult, + MultiTabLocalStore +} from '../../../src/local/local_store'; import { LocalViewChanges } from '../../../src/local/local_view_changes'; import { Persistence } from '../../../src/local/persistence'; -import { QueryEngine } from '../../../src/local/query_engine'; import { SimpleQueryEngine } from '../../../src/local/simple_query_engine'; import { documentKeySet, @@ -43,9 +46,9 @@ import { Precondition } from '../../../src/model/mutation'; import { + BATCHID_UNKNOWN, MutationBatch, - MutationBatchResult, - BATCHID_UNKNOWN + MutationBatchResult } from '../../../src/model/mutation_batch'; import { RemoteEvent } from '../../../src/remote/remote_event'; import { @@ -56,6 +59,7 @@ import { import { debugAssert } from '../../../src/util/assert'; import { addEqualityMatcher } from '../../util/equality_matcher'; import { + byteStringFromString, deletedDoc, deleteMutation, doc, @@ -73,14 +77,19 @@ import { TestSnapshotVersion, transformMutation, unknownDoc, - version, - byteStringFromString + version } from '../../util/helpers'; -import { CountingQueryEngine } from './counting_query_engine'; +import { CountingQueryEngine, QueryEngineType } from './counting_query_engine'; import * as persistenceHelpers from './persistence_test_helpers'; import { ByteString } from '../../../src/util/byte_string'; +export interface LocalStoreComponents { + queryEngine: CountingQueryEngine; + persistence: Persistence; + localStore: LocalStore; +} + class LocalStoreTester { private promiseChain: Promise = Promise.resolve(); private lastChanges: MaybeDocumentMap | null = null; @@ -385,20 +394,41 @@ class LocalStoreTester { describe('LocalStore w/ Memory Persistence (SimpleQueryEngine)', () => { addEqualityMatcher(); - genericLocalStoreTests( - persistenceHelpers.testMemoryEagerPersistence, - new SimpleQueryEngine(), - /* gcIsEager= */ true - ); + + async function initialize(): Promise { + const queryEngine = new CountingQueryEngine( + new SimpleQueryEngine(), + QueryEngineType.Simple + ); + const persistence = await persistenceHelpers.testMemoryEagerPersistence(); + const localStore = new LocalStore( + persistence, + queryEngine, + User.UNAUTHENTICATED + ); + return { queryEngine, persistence, localStore }; + } + + genericLocalStoreTests(initialize, /* gcIsEager= */ true); }); describe('LocalStore w/ Memory Persistence (IndexFreeQueryEngine)', () => { + async function initialize(): Promise { + const queryEngine = new CountingQueryEngine( + new IndexFreeQueryEngine(), + QueryEngineType.IndexFree + ); + const persistence = await persistenceHelpers.testMemoryEagerPersistence(); + const localStore = new LocalStore( + persistence, + queryEngine, + User.UNAUTHENTICATED + ); + return { queryEngine, persistence, localStore }; + } + addEqualityMatcher(); - genericLocalStoreTests( - persistenceHelpers.testMemoryEagerPersistence, - new IndexFreeQueryEngine(), - /* gcIsEager= */ true - ); + genericLocalStoreTests(initialize, /* gcIsEager= */ true); }); describe('LocalStore w/ IndexedDB Persistence (SimpleQueryEngine)', () => { @@ -409,12 +439,23 @@ describe('LocalStore w/ IndexedDB Persistence (SimpleQueryEngine)', () => { return; } + async function initialize(): Promise { + const queryEngine = new CountingQueryEngine( + new SimpleQueryEngine(), + QueryEngineType.Simple + ); + const persistence = await persistenceHelpers.testIndexedDbPersistence(); + const localStore = new MultiTabLocalStore( + persistence, + queryEngine, + User.UNAUTHENTICATED + ); + await localStore.start(); + return { queryEngine, persistence, localStore }; + } + addEqualityMatcher(); - genericLocalStoreTests( - persistenceHelpers.testIndexedDbPersistence, - new SimpleQueryEngine(), - /* gcIsEager= */ false - ); + genericLocalStoreTests(initialize, /* gcIsEager= */ false); }); describe('LocalStore w/ IndexedDB Persistence (IndexFreeQueryEngine)', () => { @@ -425,32 +466,38 @@ describe('LocalStore w/ IndexedDB Persistence (IndexFreeQueryEngine)', () => { return; } + async function initialize(): Promise { + const queryEngine = new CountingQueryEngine( + new IndexFreeQueryEngine(), + QueryEngineType.IndexFree + ); + const persistence = await persistenceHelpers.testIndexedDbPersistence(); + const localStore = new MultiTabLocalStore( + persistence, + queryEngine, + User.UNAUTHENTICATED + ); + await localStore.start(); + return { queryEngine, persistence, localStore }; + } + addEqualityMatcher(); - genericLocalStoreTests( - persistenceHelpers.testIndexedDbPersistence, - new IndexFreeQueryEngine(), - /* gcIsEager= */ false - ); + genericLocalStoreTests(initialize, /* gcIsEager= */ false); }); function genericLocalStoreTests( - getPersistence: () => Promise, - queryEngine: QueryEngine, + getComponents: () => Promise, gcIsEager: boolean ): void { let persistence: Persistence; let localStore: LocalStore; - let countingQueryEngine: CountingQueryEngine; + let queryEngine: CountingQueryEngine; beforeEach(async () => { - persistence = await getPersistence(); - countingQueryEngine = new CountingQueryEngine(queryEngine); - localStore = new LocalStore( - persistence, - countingQueryEngine, - User.UNAUTHENTICATED - ); - await localStore.start(); + const components = await getComponents(); + persistence = components.persistence; + localStore = components.localStore; + queryEngine = components.queryEngine; }); afterEach(async () => { @@ -459,7 +506,7 @@ function genericLocalStoreTests( }); function expectLocalStore(): LocalStoreTester { - return new LocalStoreTester(localStore, countingQueryEngine, gcIsEager); + return new LocalStoreTester(localStore, queryEngine, gcIsEager); } it('handles SetMutation', () => { @@ -1493,59 +1540,59 @@ function genericLocalStoreTests( ); }); - // eslint-disable-next-line no-restricted-properties - (queryEngine instanceof IndexFreeQueryEngine && !gcIsEager ? it : it.skip)( - 'uses target mapping to execute queries', - () => { - // This test verifies that once a target mapping has been written, only - // documents that match the query are read from the RemoteDocumentCache. + it('uses target mapping to execute queries', () => { + if (queryEngine.type !== QueryEngineType.IndexFree || gcIsEager) { + return; + } - const query = Query.atPath(path('foo')).addFilter( - filter('matches', '==', true) - ); - return ( - expectLocalStore() - .afterAllocatingQuery(query) - .toReturnTargetId(2) - .after(setMutation('foo/a', { matches: true })) - .after(setMutation('foo/b', { matches: true })) - .after(setMutation('foo/ignored', { matches: false })) - .afterAcknowledgingMutation({ documentVersion: 10 }) - .afterAcknowledgingMutation({ documentVersion: 10 }) - .afterAcknowledgingMutation({ documentVersion: 10 }) - .afterExecutingQuery(query) - // Execute the query, but note that we read all existing documents - // from the RemoteDocumentCache since we do not yet have target - // mapping. - .toHaveRead({ documentsByQuery: 2 }) - .after( - docAddedRemoteEvent( - [ - doc('foo/a', 10, { matches: true }), - doc('foo/b', 10, { matches: true }) - ], - [2], - [] - ) - ) - .after( - noChangeEvent( - /* targetId= */ 2, - /* snapshotVersion= */ 10, - /* resumeToken= */ byteStringFromString('foo') - ) + // This test verifies that once a target mapping has been written, only + // documents that match the query are read from the RemoteDocumentCache. + + const query = Query.atPath(path('foo')).addFilter( + filter('matches', '==', true) + ); + return ( + expectLocalStore() + .afterAllocatingQuery(query) + .toReturnTargetId(2) + .after(setMutation('foo/a', { matches: true })) + .after(setMutation('foo/b', { matches: true })) + .after(setMutation('foo/ignored', { matches: false })) + .afterAcknowledgingMutation({ documentVersion: 10 }) + .afterAcknowledgingMutation({ documentVersion: 10 }) + .afterAcknowledgingMutation({ documentVersion: 10 }) + .afterExecutingQuery(query) + // Execute the query, but note that we read all existing documents + // from the RemoteDocumentCache since we do not yet have target + // mapping. + .toHaveRead({ documentsByQuery: 2 }) + .after( + docAddedRemoteEvent( + [ + doc('foo/a', 10, { matches: true }), + doc('foo/b', 10, { matches: true }) + ], + [2], + [] ) - .after(localViewChanges(2, /* fromCache= */ false, {})) - .afterExecutingQuery(query) - .toHaveRead({ documentsByKey: 2, documentsByQuery: 0 }) - .toReturnChanged( - doc('foo/a', 10, { matches: true }), - doc('foo/b', 10, { matches: true }) + ) + .after( + noChangeEvent( + /* targetId= */ 2, + /* snapshotVersion= */ 10, + /* resumeToken= */ byteStringFromString('foo') ) - .finish() - ); - } - ); + ) + .after(localViewChanges(2, /* fromCache= */ false, {})) + .afterExecutingQuery(query) + .toHaveRead({ documentsByKey: 2, documentsByQuery: 0 }) + .toReturnChanged( + doc('foo/a', 10, { matches: true }), + doc('foo/b', 10, { matches: true }) + ) + .finish() + ); + }); it('last limbo free snapshot is advanced during view processing', async () => { // This test verifies that the `lastLimboFreeSnapshot` version for TargetData diff --git a/packages/firestore/test/unit/local/persistence_test_helpers.ts b/packages/firestore/test/unit/local/persistence_test_helpers.ts index 7a4f33c81c0..b60b32d4264 100644 --- a/packages/firestore/test/unit/local/persistence_test_helpers.ts +++ b/packages/firestore/test/unit/local/persistence_test_helpers.ts @@ -115,32 +115,29 @@ export async function testIndexedDbPersistence( await SimpleDb.delete(prefix + IndexedDbPersistence.MAIN_DATABASE); } const platform = PlatformSupport.getPlatform(); - const persistence = IndexedDbPersistence.createIndexedDbPersistence({ - allowTabSynchronization: !!options.synchronizeTabs, - persistenceKey: TEST_PERSISTENCE_PREFIX, + const persistence = new IndexedDbPersistence( + !!options.synchronizeTabs, + TEST_PERSISTENCE_PREFIX, clientId, platform, - queue, - serializer: JSON_SERIALIZER, lruParams, - sequenceNumberSyncer: MOCK_SEQUENCE_NUMBER_SYNCER - }); + queue, + JSON_SERIALIZER, + MOCK_SEQUENCE_NUMBER_SYNCER + ); await persistence.start(); return persistence; } /** Creates and starts a MemoryPersistence instance for testing. */ export async function testMemoryEagerPersistence(): Promise { - return new MemoryPersistence(AutoId.newId(), MemoryEagerDelegate.factory); + return new MemoryPersistence(MemoryEagerDelegate.factory); } export async function testMemoryLruPersistence( params: LruParams = LruParams.DEFAULT ): Promise { - return new MemoryPersistence( - AutoId.newId(), - p => new MemoryLruDelegate(p, params) - ); + return new MemoryPersistence(p => new MemoryLruDelegate(p, params)); } /** Clears the persistence in tests */ diff --git a/packages/firestore/test/unit/local/test_remote_document_cache.ts b/packages/firestore/test/unit/local/test_remote_document_cache.ts index d0554e43ed7..cc0433bb79a 100644 --- a/packages/firestore/test/unit/local/test_remote_document_cache.ts +++ b/packages/firestore/test/unit/local/test_remote_document_cache.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright 2017 Google Inc. + * Copyright 2017 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,6 +20,7 @@ import { SnapshotVersion } from '../../../src/core/snapshot_version'; import { Persistence } from '../../../src/local/persistence'; import { PersistencePromise } from '../../../src/local/persistence_promise'; import { RemoteDocumentCache } from '../../../src/local/remote_document_cache'; +import { IndexedDbRemoteDocumentCache } from '../../../src/local/indexeddb_remote_document_cache'; import { RemoteDocumentChangeBuffer } from '../../../src/local/remote_document_change_buffer'; import { DocumentKeySet, @@ -29,6 +30,7 @@ import { } from '../../../src/model/collections'; import { MaybeDocument } from '../../../src/model/document'; import { DocumentKey } from '../../../src/model/document_key'; +import { debugAssert } from '../../../src/util/assert'; /** * A wrapper around a RemoteDocumentCache that automatically creates a @@ -130,6 +132,10 @@ export class TestRemoteDocumentCache { 'getNewDocumentChanges', 'readonly', txn => { + debugAssert( + this.cache instanceof IndexedDbRemoteDocumentCache, + 'getNewDocumentChanges() requires IndexedDB' + ); return this.cache.getNewDocumentChanges(txn, sinceReadTime); } ); diff --git a/packages/firestore/test/unit/specs/spec_test_components.ts b/packages/firestore/test/unit/specs/spec_test_components.ts index fa27ef9d06a..ac809ce715d 100644 --- a/packages/firestore/test/unit/specs/spec_test_components.ts +++ b/packages/firestore/test/unit/specs/spec_test_components.ts @@ -24,16 +24,9 @@ import { GarbageCollectionScheduler, Persistence, PersistenceTransaction, - PersistenceTransactionMode, - PrimaryStateListener, - ReferenceDelegate + PersistenceTransactionMode } from '../../../src/local/persistence'; -import { ClientId } from '../../../src/local/shared_client_state'; -import { User } from '../../../src/auth/user'; -import { MutationQueue } from '../../../src/local/mutation_queue'; -import { TargetCache } from '../../../src/local/target_cache'; -import { RemoteDocumentCache } from '../../../src/local/remote_document_cache'; -import { IndexManager } from '../../../src/local/index_manager'; +import { IndexedDbPersistence } from '../../../src/local/indexeddb_persistence'; import { PersistencePromise } from '../../../src/local/persistence_promise'; import { IndexedDbTransactionError } from '../../../src/local/simple_db'; import { debugAssert } from '../../../src/util/assert'; @@ -45,66 +38,35 @@ import { import { LruParams } from '../../../src/local/lru_garbage_collector'; /** - * A test-only persistence implementation that delegates all calls to the - * underlying IndexedDB or Memory-based persistence implementations and is able - * to inject transaction failures. + * A test-only MemoryPersistence implementation that is able to inject + * transaction failures. */ -export class MockPersistence implements Persistence { +export class MockMemoryPersistence extends MemoryPersistence { injectFailures = false; - constructor(private readonly delegate: Persistence) {} - - start(): Promise { - return this.delegate.start(); - } - - get started(): boolean { - return this.delegate.started; - } - - get referenceDelegate(): ReferenceDelegate { - return this.delegate.referenceDelegate; - } - - shutdown(): Promise { - return this.delegate.shutdown(); - } - - setPrimaryStateListener( - primaryStateListener: PrimaryStateListener - ): Promise { - return this.delegate.setPrimaryStateListener(primaryStateListener); - } - - setDatabaseDeletedListener( - databaseDeletedListener: () => Promise - ): void { - this.delegate.setDatabaseDeletedListener(databaseDeletedListener); - } - - setNetworkEnabled(networkEnabled: boolean): void { - this.delegate.setNetworkEnabled(networkEnabled); - } - - getActiveClients(): Promise { - return this.delegate.getActiveClients(); - } - - getMutationQueue(user: User): MutationQueue { - return this.delegate.getMutationQueue(user); - } - - getTargetCache(): TargetCache { - return this.delegate.getTargetCache(); - } - - getRemoteDocumentCache(): RemoteDocumentCache { - return this.delegate.getRemoteDocumentCache(); + runTransaction( + action: string, + mode: PersistenceTransactionMode, + transactionOperation: ( + transaction: PersistenceTransaction + ) => PersistencePromise + ): Promise { + if (this.injectFailures) { + return Promise.reject( + new IndexedDbTransactionError(new Error('Simulated retryable error')) + ); + } else { + return super.runTransaction(action, mode, transactionOperation); + } } +} - getIndexManager(): IndexManager { - return this.delegate.getIndexManager(); - } +/** + * A test-only IndexedDbPersistence implementation that is able to inject + * transaction failures. + */ +export class MockIndexedDbPersistence extends IndexedDbPersistence { + injectFailures = false; runTransaction( action: string, @@ -118,13 +80,13 @@ export class MockPersistence implements Persistence { new IndexedDbTransactionError(new Error('Simulated retryable error')) ); } else { - return this.delegate.runTransaction(action, mode, transactionOperation); + return super.runTransaction(action, mode, transactionOperation); } } } export class MockIndexedDbComponentProvider extends IndexedDbComponentProvider { - persistence!: MockPersistence; + persistence!: MockIndexedDbPersistence; createGarbageCollectionScheduler( cfg: ComponentConfiguration @@ -132,13 +94,32 @@ export class MockIndexedDbComponentProvider extends IndexedDbComponentProvider { return null; } - createPersistence(cfg: ComponentConfiguration): Persistence { - return new MockPersistence(super.createPersistence(cfg)); + createPersistence(cfg: ComponentConfiguration): MockIndexedDbPersistence { + debugAssert( + cfg.persistenceSettings.durable, + 'Can only start durable persistence' + ); + + const persistenceKey = IndexedDbPersistence.buildStoragePrefix( + cfg.databaseInfo + ); + const serializer = cfg.platform.newSerializer(cfg.databaseInfo.databaseId); + + return new MockIndexedDbPersistence( + /* allowTabSynchronization= */ true, + persistenceKey, + cfg.clientId, + cfg.platform, + LruParams.withCacheSize(cfg.persistenceSettings.cacheSizeBytes), + cfg.asyncQueue, + serializer, + this.sharedClientState + ); } } export class MockMemoryComponentProvider extends MemoryComponentProvider { - persistence!: MockPersistence; + persistence!: MockMemoryPersistence; constructor(private readonly gcEnabled: boolean) { super(); @@ -155,12 +136,10 @@ export class MockMemoryComponentProvider extends MemoryComponentProvider { !cfg.persistenceSettings.durable, 'Can only start memory persistence' ); - const persistence = new MemoryPersistence( - cfg.clientId, + return new MockMemoryPersistence( this.gcEnabled ? MemoryEagerDelegate.factory : p => new MemoryLruDelegate(p, LruParams.DEFAULT) ); - return new MockPersistence(persistence); } } diff --git a/packages/firestore/test/unit/specs/spec_test_runner.ts b/packages/firestore/test/unit/specs/spec_test_runner.ts index 4a530aed0a0..0509fa59770 100644 --- a/packages/firestore/test/unit/specs/spec_test_runner.ts +++ b/packages/firestore/test/unit/specs/spec_test_runner.ts @@ -108,9 +108,11 @@ import { LruParams } from '../../../src/local/lru_garbage_collector'; import { PersistenceSettings } from '../../../src/core/firestore_client'; import { MockIndexedDbComponentProvider, + MockIndexedDbPersistence, MockMemoryComponentProvider, - MockPersistence + MockMemoryPersistence } from './spec_test_components'; +import { IndexedDbPersistence } from '../../../src/local/indexeddb_persistence'; const ARBITRARY_SEQUENCE_NUMBER = 2; @@ -415,7 +417,7 @@ abstract class TestRunner { private datastore!: Datastore; private localStore!: LocalStore; private remoteStore!: RemoteStore; - private persistence!: MockPersistence; + private persistence!: MockMemoryPersistence | MockIndexedDbPersistence; protected sharedClientState!: SharedClientState; private useGarbageCollection: boolean; @@ -1006,6 +1008,10 @@ abstract class TestRunner { ); } if ('numActiveClients' in expectedState) { + debugAssert( + this.persistence instanceof IndexedDbPersistence, + 'numActiveClients() requires IndexedDbPersistence' + ); const activeClients = await this.persistence.getActiveClients(); expect(activeClients.length).to.equal(expectedState.numActiveClients); }