From 5b53cb0cc320fc5d8e4cee89242f404a7a10d4b6 Mon Sep 17 00:00:00 2001 From: Sebastian Schmidt Date: Fri, 17 Jul 2020 12:19:08 -0700 Subject: [PATCH 1/4] Remove MulitTabSyncEngine --- .../firestore/src/core/component_provider.ts | 51 +- packages/firestore/src/core/sync_engine.ts | 714 +++++++++--------- .../src/local/shared_client_state_syncer.ts | 25 + .../unit/local/persistence_test_helpers.ts | 36 +- 4 files changed, 399 insertions(+), 427 deletions(-) diff --git a/packages/firestore/src/core/component_provider.ts b/packages/firestore/src/core/component_provider.ts index 66a76b7ea68..4e8c4ef87e8 100644 --- a/packages/firestore/src/core/component_provider.ts +++ b/packages/firestore/src/core/component_provider.ts @@ -27,8 +27,11 @@ import { synchronizeLastDocumentChangeReadTime } from '../local/local_store'; import { - MultiTabSyncEngine, - newMultiTabSyncEngine, + applyActiveTargetsChange, + applyBatchState, + applyPrimaryState, + applyTargetState, + getActiveClients, newSyncEngine, SyncEngine } from './sync_engine'; @@ -45,6 +48,7 @@ import { Code, FirestoreError } from '../util/error'; import { OnlineStateSource } from './types'; import { LruParams, LruScheduler } from '../local/lru_garbage_collector'; import { IndexFreeQueryEngine } from '../local/index_free_query_engine'; +import { MemorySharedClientStateSyncer } from '../local/shared_client_state_syncer'; import { indexedDbStoragePrefix, IndexedDbPersistence, @@ -194,14 +198,19 @@ export class MemoryComponentProvider implements ComponentProvider { } createSyncEngine(cfg: ComponentConfiguration): SyncEngine { - return newSyncEngine( + const syncEngine = newSyncEngine( this.localStore, this.remoteStore, this.datastore, this.sharedClientState, cfg.initialUser, - cfg.maxConcurrentLimboResolutions + cfg.maxConcurrentLimboResolutions, + /* isPrimary= */ true ); + this.sharedClientState.syncEngine = new MemorySharedClientStateSyncer([ + cfg.clientId + ]); + return syncEngine; } clearPersistence( @@ -229,17 +238,6 @@ export class IndexedDbComponentProvider extends MemoryComponentProvider { ); } - createSyncEngine(cfg: ComponentConfiguration): SyncEngine { - return newSyncEngine( - this.localStore, - this.remoteStore, - this.datastore, - this.sharedClientState, - cfg.initialUser, - cfg.maxConcurrentLimboResolutions - ); - } - createGarbageCollectionScheduler( cfg: ComponentConfiguration ): GarbageCollectionScheduler | null { @@ -296,17 +294,13 @@ export class IndexedDbComponentProvider extends MemoryComponentProvider { * `synchronizeTabs` will be enabled. */ export class MultiTabIndexedDbComponentProvider extends IndexedDbComponentProvider { - 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 - ); + await applyPrimaryState(this.syncEngine, isPrimary); if (this.gcScheduler) { if (isPrimary && !this.gcScheduler.started) { this.gcScheduler.start(this.localStore); @@ -324,17 +318,24 @@ export class MultiTabIndexedDbComponentProvider extends IndexedDbComponentProvid } createSyncEngine(cfg: ComponentConfiguration): SyncEngine { - const syncEngine = newMultiTabSyncEngine( + const startsAsPrimary = + !cfg.persistenceSettings.durable || + !cfg.persistenceSettings.synchronizeTabs; + const syncEngine = newSyncEngine( this.localStore, this.remoteStore, this.datastore, this.sharedClientState, cfg.initialUser, - cfg.maxConcurrentLimboResolutions + cfg.maxConcurrentLimboResolutions, + startsAsPrimary ); - if (this.sharedClientState instanceof WebStorageSharedClientState) { - this.sharedClientState.syncEngine = syncEngine; - } + this.sharedClientState.syncEngine = { + applyBatchState: applyBatchState.bind(null, syncEngine), + applyTargetState: applyTargetState.bind(null, syncEngine), + applyActiveTargetsChange: applyActiveTargetsChange.bind(null, syncEngine), + getActiveClients: getActiveClients.bind(null, syncEngine) + }; return syncEngine; } diff --git a/packages/firestore/src/core/sync_engine.ts b/packages/firestore/src/core/sync_engine.ts index 0937b77d321..ba97d4c2f38 100644 --- a/packages/firestore/src/core/sync_engine.ts +++ b/packages/firestore/src/core/sync_engine.ts @@ -40,7 +40,7 @@ import { BATCHID_UNKNOWN, MutationBatchResult } from '../model/mutation_batch'; import { RemoteEvent, TargetChange } from '../remote/remote_event'; import { RemoteStore } from '../remote/remote_store'; import { RemoteSyncer } from '../remote/remote_syncer'; -import { debugAssert, fail, hardAssert } from '../util/assert'; +import { debugAssert, debugCast, fail, hardAssert } from '../util/assert'; import { Code, FirestoreError } from '../util/error'; import { logDebug } from '../util/log'; import { primitiveComparator } from '../util/misc'; @@ -49,10 +49,7 @@ import { Deferred } from '../util/promise'; import { SortedMap } from '../util/sorted_map'; import { ClientId, SharedClientState } from '../local/shared_client_state'; -import { - QueryTargetState, - SharedClientStateSyncer -} from '../local/shared_client_state_syncer'; +import { QueryTargetState } from '../local/shared_client_state_syncer'; import { SortedSet } from '../util/sorted_set'; import { ListenSequence } from './listen_sequence'; import { @@ -241,13 +238,13 @@ export interface SyncEngine extends RemoteSyncer { * functions, such that they are tree-shakeable. */ class SyncEngineImpl implements SyncEngine { - protected syncEngineListener: SyncEngineListener | null = null; + syncEngineListener: SyncEngineListener | null = null; - protected queryViewsByQuery = new ObjectMap( + queryViewsByQuery = new ObjectMap( q => canonifyQuery(q), queryEquals ); - protected queriesByTarget = new Map(); + queriesByTarget = new Map(); /** * The keys of documents that are in limbo for which we haven't yet started a * limbo resolution query. @@ -257,18 +254,15 @@ class SyncEngineImpl implements SyncEngine { * Keeps track of the target ID for each document that is in limbo with an * active target. */ - protected activeLimboTargetsByKey = new SortedMap( + 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. */ - protected activeLimboResolutionsByTarget = new Map< - TargetId, - LimboResolution - >(); - protected limboDocumentRefs = new ReferenceSet(); + activeLimboResolutionsByTarget = new Map(); + limboDocumentRefs = new ReferenceSet(); /** Stores user completion handlers, indexed by User and BatchId. */ private mutationUserCallbacks = {} as { [uidKey: string]: SortedMap>; @@ -279,18 +273,23 @@ class SyncEngineImpl implements SyncEngine { private onlineState = OnlineState.Unknown; + // 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. + _isPrimaryClient: undefined | boolean = undefined; + constructor( - protected localStore: LocalStore, - protected remoteStore: RemoteStore, + public localStore: LocalStore, + public remoteStore: RemoteStore, protected datastore: Datastore, // PORTING NOTE: Manages state synchronization in multi-tab environments. - protected sharedClientState: SharedClientState, + public sharedClientState: SharedClientState, private currentUser: User, private maxConcurrentLimboResolutions: number ) {} get isPrimaryClient(): boolean { - return true; + return this._isPrimaryClient === true; } subscribe(syncEngineListener: SyncEngineListener): void { @@ -347,7 +346,7 @@ class SyncEngineImpl implements SyncEngine { * Registers a view for a previously unknown query and computes its initial * snapshot. */ - protected async initializeViewAndComputeSnapshot( + async initializeViewAndComputeSnapshot( query: Query, targetId: TargetId, current: boolean @@ -510,21 +509,33 @@ class SyncEngineImpl implements SyncEngine { onlineState: OnlineState, source: OnlineStateSource ): void { - 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); + // 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.RemoteStore) || + (!this.isPrimaryClient && 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.isPrimaryClient) { + this.sharedClientState.setOnlineState(onlineState); } - }); - this.syncEngineListener!.onOnlineStateChange(onlineState); - this.syncEngineListener!.onWatchChange(newViewSnapshots); - this.onlineState = onlineState; + } } async rejectListen(targetId: TargetId, err: FirestoreError): Promise { @@ -698,7 +709,7 @@ class SyncEngineImpl implements SyncEngine { * Resolves or rejects the user callback for the given batch and then discards * it. */ - protected processUserCallback(batchId: BatchId, error: Error | null): void { + 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 @@ -721,10 +732,7 @@ class SyncEngineImpl implements SyncEngine { } } - protected removeAndCleanupTarget( - targetId: number, - error: Error | null = null - ): void { + removeAndCleanupTarget(targetId: number, error: Error | null = null): void { this.sharedClientState.removeLocalQueryTarget(targetId); debugAssert( @@ -769,7 +777,7 @@ class SyncEngineImpl implements SyncEngine { this.pumpEnqueuedLimboResolutions(); } - protected updateTrackedLimbos( + updateTrackedLimbos( targetId: TargetId, limboChanges: LimboDocumentChange[] ): void { @@ -846,7 +854,7 @@ class SyncEngineImpl implements SyncEngine { return this.enqueuedLimboResolutions; } - protected async emitNewSnapsAndNotifyLocalStore( + async emitNewSnapsAndNotifyLocalStore( changes: MaybeDocumentMap, remoteEvent?: RemoteEvent ): Promise { @@ -910,7 +918,7 @@ class SyncEngineImpl implements SyncEngine { await this.localStore.notifyLocalViewChanges(docChangesInAllViews); } - protected assertSubscribed(fnName: string): void { + assertSubscribed(fnName: string): void { debugAssert( this.syncEngineListener !== null, 'Trying to call ' + fnName + ' before calling subscribe().' @@ -970,9 +978,10 @@ export function newSyncEngine( // PORTING NOTE: Manages state synchronization in multi-tab environments. sharedClientState: SharedClientState, currentUser: User, - maxConcurrentLimboResolutions: number + maxConcurrentLimboResolutions: number, + isPrimary: boolean ): SyncEngine { - return new SyncEngineImpl( + const syncEngine = new SyncEngineImpl( localStore, remoteStore, datastore, @@ -980,373 +989,342 @@ export function newSyncEngine( currentUser, maxConcurrentLimboResolutions ); + if (isPrimary) { + syncEngine._isPrimaryClient = true; + } + return syncEngine; } /** - * An extension of SyncEngine that also includes SharedClientStateSyncer for - * Multi-Tab synchronization. + * Reconcile the list of synced documents in an existing view with those + * from persistence. */ -// PORTING NOTE: Web only -export interface MultiTabSyncEngine - extends SharedClientStateSyncer, - SyncEngine { - applyPrimaryState(isPrimary: boolean): Promise; +async function synchronizeViewAndComputeSnapshot( + syncEngine: SyncEngine, + queryView: QueryView +): Promise { + const syncEngineImpl = debugCast(syncEngine, SyncEngineImpl); + const queryResult = await syncEngineImpl.localStore.executeQuery( + queryView.query, + /* usePreviousResults= */ true + ); + const viewSnapshot = queryView.view.synchronizeWithPersistedState( + queryResult + ); + if (syncEngineImpl.isPrimaryClient) { + syncEngineImpl.updateTrackedLimbos( + queryView.targetId, + viewSnapshot.limboChanges + ); + } + return viewSnapshot; } -/** - * An implementation of `SyncEngineImpl` providing multi-tab synchronization on - * top of `SyncEngineImpl`. - * - * Note: some field defined in this class might have public access level, but - * the class is not exported so they are only accessible from this module. - * This is useful to implement optional features (like bundles) in free - * functions, such that they are tree-shakeable. - */ -class MultiTabSyncEngineImpl extends SyncEngineImpl { - // 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 _isPrimaryClient: undefined | boolean = undefined; +/** Applies a mutation state to an existing batch. */ +// PORTING NOTE: Multi-Tab only. +export async function applyBatchState( + syncEngine: SyncEngine, + batchId: BatchId, + batchState: MutationBatchState, + error?: FirestoreError +): Promise { + const syncEngineImpl = debugCast(syncEngine, SyncEngineImpl); + syncEngineImpl.assertSubscribed('applyBatchState()'); + const documents = await lookupMutationDocuments( + syncEngineImpl.localStore, + batchId + ); - get isPrimaryClient(): boolean { - return this._isPrimaryClient === true; + 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; } - /** - * 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._isPrimaryClient) { - this.updateTrackedLimbos(queryView.targetId, viewSnapshot.limboChanges); - } - return viewSnapshot; + 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 syncEngineImpl.remoteStore.fillWritePipeline(); + } else if (batchState === 'acknowledged' || batchState === 'rejected') { + // NOTE: Both these methods are no-ops for batches that originated from + // other clients. + syncEngineImpl.processUserCallback(batchId, error ? error : null); + removeCachedMutationBatchMetadata(syncEngineImpl.localStore, batchId); + } else { + fail(`Unknown batchState: ${batchState}`); } - 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); - } - } + await syncEngineImpl.emitNewSnapsAndNotifyLocalStore(documents); +} - async applyBatchState( - batchId: BatchId, - batchState: MutationBatchState, - error?: FirestoreError - ): Promise { - this.assertSubscribed('applyBatchState()'); - const documents = await lookupMutationDocuments(this.localStore, 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; +/** Applies a query target change from a different tab. */ +// PORTING NOTE: Multi-Tab only. +export async function applyPrimaryState( + syncEngine: SyncEngine, + isPrimary: boolean +): Promise { + const syncEngineImpl = debugCast(syncEngine, SyncEngineImpl); + if (isPrimary === true && syncEngineImpl._isPrimaryClient !== true) { + // Secondary tabs only maintain Views for their local listeners and the + // Views internal state may not be 100% populated (in particular + // secondary tabs don't track syncedDocuments, the set of documents the + // server considers to be in the target). So when a secondary becomes + // primary, we need to need to make sure that all views for all targets + // match the state on disk. + const activeTargets = syncEngineImpl.sharedClientState.getAllActiveQueryTargets(); + const activeQueries = await synchronizeQueryViewsAndRaiseSnapshots( + syncEngineImpl, + activeTargets.toArray(), + /*transitionToPrimary=*/ true + ); + syncEngineImpl._isPrimaryClient = true; + await syncEngineImpl.remoteStore.applyPrimaryState(true); + for (const targetData of activeQueries) { + syncEngineImpl.remoteStore.listen(targetData); } + } else if (isPrimary === false && syncEngineImpl._isPrimaryClient !== false) { + const activeTargets: TargetId[] = []; - 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); - removeCachedMutationBatchMetadata(this.localStore, batchId); - } else { - fail(`Unknown batchState: ${batchState}`); - } + let p = Promise.resolve(); + syncEngineImpl.queriesByTarget.forEach((_, targetId) => { + if (syncEngineImpl.sharedClientState.isLocalQueryTarget(targetId)) { + activeTargets.push(targetId); + } else { + p = p.then(() => { + syncEngineImpl.removeAndCleanupTarget(targetId); + return syncEngineImpl.localStore.releaseTarget( + targetId, + /*keepPersistedTargetData=*/ true + ); + }); + } + syncEngineImpl.remoteStore.unlisten(targetId); + }); + await p; - await this.emitNewSnapsAndNotifyLocalStore(documents); + await synchronizeQueryViewsAndRaiseSnapshots( + syncEngineImpl, + activeTargets, + /*transitionToPrimary=*/ false + ); + resetLimboDocuments(syncEngineImpl); + syncEngineImpl._isPrimaryClient = false; + await syncEngineImpl.remoteStore.applyPrimaryState(false); } +} - async applyPrimaryState(isPrimary: boolean): Promise { - if (isPrimary === true && this._isPrimaryClient !== true) { - // Secondary tabs only maintain Views for their local listeners and the - // Views internal state may not be 100% populated (in particular - // secondary tabs don't track syncedDocuments, the set of documents the - // server considers to be in the target). So when a secondary becomes - // primary, we need to need to make sure that all views for all targets - // match the state on disk. - const activeTargets = this.sharedClientState.getAllActiveQueryTargets(); - const activeQueries = await this.synchronizeQueryViewsAndRaiseSnapshots( - activeTargets.toArray(), - /*transitionToPrimary=*/ true +// PORTING NOTE: Multi-Tab only. +function resetLimboDocuments(syncEngine: SyncEngine): void { + const syncEngineImpl = debugCast(syncEngine, SyncEngineImpl); + syncEngineImpl.activeLimboResolutionsByTarget.forEach((_, targetId) => { + syncEngineImpl.remoteStore.unlisten(targetId); + }); + syncEngineImpl.limboDocumentRefs.removeAllReferences(); + syncEngineImpl.activeLimboResolutionsByTarget = new Map< + TargetId, + LimboResolution + >(); + syncEngineImpl.activeLimboTargetsByKey = new SortedMap( + DocumentKey.comparator + ); +} + +/** + * Reconcile the query views of the provided query targets with the state from + * persistence. Raises snapshots for any changes that affect the local + * client and returns the updated state of all target's query data. + * + * @param targets the list of targets with views that need to be recomputed + * @param transitionToPrimary `true` iff the tab transitions from a secondary + * tab to a primary tab + */ +// PORTING NOTE: Multi-Tab only. +async function synchronizeQueryViewsAndRaiseSnapshots( + syncEngine: SyncEngine, + targets: TargetId[], + transitionToPrimary: boolean +): Promise { + const syncEngineImpl = debugCast(syncEngine, SyncEngineImpl); + const activeQueries: TargetData[] = []; + const newViewSnapshots: ViewSnapshot[] = []; + for (const targetId of targets) { + let targetData: TargetData; + const queries = syncEngineImpl.queriesByTarget.get(targetId); + + if (queries && queries.length !== 0) { + // For queries that have a local View, we fetch their current state + // from LocalStore (as the resume token and the snapshot version + // might have changed) and reconcile their views with the persisted + // state (the list of syncedDocuments may have gotten out of sync). + targetData = await syncEngineImpl.localStore.allocateTarget( + queries[0].toTarget() ); - this._isPrimaryClient = true; - await this.remoteStore.applyPrimaryState(true); - for (const targetData of activeQueries) { - this.remoteStore.listen(targetData); - } - } else if (isPrimary === false && this._isPrimaryClient !== false) { - const activeTargets: TargetId[] = []; - let p = Promise.resolve(); - this.queriesByTarget.forEach((_, targetId) => { - if (this.sharedClientState.isLocalQueryTarget(targetId)) { - activeTargets.push(targetId); - } else { - p = p.then(() => { - this.removeAndCleanupTarget(targetId); - return this.localStore.releaseTarget( - targetId, - /*keepPersistedTargetData=*/ true - ); - }); - } - this.remoteStore.unlisten(targetId); - }); - await p; + for (const query of queries) { + const queryView = syncEngineImpl.queryViewsByQuery.get(query); + debugAssert( + !!queryView, + `No query view found for ${stringifyQuery(query)}` + ); - await this.synchronizeQueryViewsAndRaiseSnapshots( - activeTargets, - /*transitionToPrimary=*/ false + const viewChange = await synchronizeViewAndComputeSnapshot( + syncEngineImpl, + queryView + ); + if (viewChange.snapshot) { + newViewSnapshots.push(viewChange.snapshot); + } + } + } else { + debugAssert( + transitionToPrimary, + 'A secondary tab should never have an active view without an active target.' + ); + // For queries that never executed on this client, we need to + // allocate the target in LocalStore and initialize a new View. + const target = await getCachedTarget(syncEngineImpl.localStore, targetId); + debugAssert(!!target, `Target for id ${targetId} not found`); + targetData = await syncEngineImpl.localStore.allocateTarget(target); + await syncEngineImpl.initializeViewAndComputeSnapshot( + synthesizeTargetToQuery(target!), + targetId, + /*current=*/ false ); - this.resetLimboDocuments(); - this._isPrimaryClient = false; - await this.remoteStore.applyPrimaryState(false); } - } - private resetLimboDocuments(): void { - this.activeLimboResolutionsByTarget.forEach((_, targetId) => { - this.remoteStore.unlisten(targetId); - }); - this.limboDocumentRefs.removeAllReferences(); - this.activeLimboResolutionsByTarget = new Map(); - this.activeLimboTargetsByKey = new SortedMap( - DocumentKey.comparator - ); + activeQueries.push(targetData!); } - /** - * Reconcile the query views of the provided query targets with the state from - * persistence. Raises snapshots for any changes that affect the local - * client and returns the updated state of all target's query data. - * - * @param targets the list of targets with views that need to be recomputed - * @param transitionToPrimary `true` iff the tab transitions from a secondary - * tab to a primary tab - */ - private async synchronizeQueryViewsAndRaiseSnapshots( - targets: TargetId[], - transitionToPrimary: boolean - ): Promise { - const activeQueries: TargetData[] = []; - const newViewSnapshots: ViewSnapshot[] = []; - for (const targetId of targets) { - let targetData: TargetData; - const queries = this.queriesByTarget.get(targetId); + syncEngineImpl.syncEngineListener!.onWatchChange(newViewSnapshots); + return activeQueries; +} - if (queries && queries.length !== 0) { - // For queries that have a local View, we fetch their current state - // from LocalStore (as the resume token and the snapshot version - // might have changed) and reconcile their views with the persisted - // state (the list of syncedDocuments may have gotten out of sync). - targetData = await this.localStore.allocateTarget( - queries[0].toTarget() - ); +/** + * Creates a `Query` object from the specified `Target`. There is no way to + * obtain the original `Query`, so we synthesize a `Query` from the `Target` + * object. + * + * The synthesized result might be different from the original `Query`, but + * since the synthesized `Query` should return the same results as the + * original one (only the presentation of results might differ), the potential + * difference will not cause issues. + */ +// PORTING NOTE: Multi-Tab only. +function synthesizeTargetToQuery(target: Target): Query { + return new Query( + target.path, + target.collectionGroup, + target.orderBy, + target.filters, + target.limit, + LimitType.First, + target.startAt, + target.endAt + ); +} - for (const query of queries) { - const queryView = this.queryViewsByQuery.get(query); - debugAssert( - !!queryView, - `No query view found for ${stringifyQuery(query)}` - ); +/** Returns the IDs of the clients that are currently active. */ +// PORTING NOTE: Multi-Tab only. +export function getActiveClients(syncEngine: SyncEngine): Promise { + const syncEngineImpl = debugCast(syncEngine, SyncEngineImpl); + return getCurrentlyActiveClients(syncEngineImpl.localStore); +} - const viewChange = await this.synchronizeViewAndComputeSnapshot( - queryView - ); - if (viewChange.snapshot) { - newViewSnapshots.push(viewChange.snapshot); - } - } - } else { - debugAssert( - transitionToPrimary, - 'A secondary tab should never have an active view without an active target.' +/** Applies a query target change from a different tab. */ +// PORTING NOTE: Multi-Tab only. +export async function applyTargetState( + syncEngine: SyncEngine, + targetId: TargetId, + state: QueryTargetState, + error?: FirestoreError +): Promise { + const syncEngineImpl = debugCast(syncEngine, SyncEngineImpl); + if (syncEngineImpl._isPrimaryClient) { + // If we receive a target state notification via WebStorage, we are + // either already secondary or another tab has taken the primary lease. + logDebug(LOG_TAG, 'Ignoring unexpected query state notification.'); + return; + } + + if (syncEngineImpl.queriesByTarget.has(targetId)) { + switch (state) { + case 'current': + case 'not-current': { + const changes = await getNewDocumentChanges(syncEngineImpl.localStore); + const synthesizedRemoteEvent = RemoteEvent.createSynthesizedRemoteEventForCurrentChange( + targetId, + state === 'current' ); - // For queries that never executed on this client, we need to - // allocate the target in LocalStore and initialize a new View. - const target = await getCachedTarget(this.localStore, targetId); - debugAssert(!!target, `Target for id ${targetId} not found`); - targetData = await this.localStore.allocateTarget(target); - await this.initializeViewAndComputeSnapshot( - this.synthesizeTargetToQuery(target!), + await syncEngineImpl.emitNewSnapsAndNotifyLocalStore( + changes, + synthesizedRemoteEvent + ); + break; + } + case 'rejected': { + await syncEngineImpl.localStore.releaseTarget( targetId, - /*current=*/ false + /* keepPersistedTargetData */ true ); + syncEngineImpl.removeAndCleanupTarget(targetId, error); + break; } - - activeQueries.push(targetData!); + default: + fail('Unexpected target state: ' + state); } - - this.syncEngineListener!.onWatchChange(newViewSnapshots); - return activeQueries; - } - - /** - * Creates a `Query` object from the specified `Target`. There is no way to - * obtain the original `Query`, so we synthesize a `Query` from the `Target` - * object. - * - * The synthesized result might be different from the original `Query`, but - * since the synthesized `Query` should return the same results as the - * original one (only the presentation of results might differ), the potential - * difference will not cause issues. - */ - private synthesizeTargetToQuery(target: Target): Query { - return new Query( - target.path, - target.collectionGroup, - target.orderBy, - target.filters, - target.limit, - LimitType.First, - target.startAt, - target.endAt - ); } +} - getActiveClients(): Promise { - return getCurrentlyActiveClients(this.localStore); +/** Adds or removes Watch targets for queries from different tabs. */ +export async function applyActiveTargetsChange( + syncEngine: SyncEngine, + added: TargetId[], + removed: TargetId[] +): Promise { + const syncEngineImpl = debugCast(syncEngine, SyncEngineImpl); + if (!syncEngineImpl._isPrimaryClient) { + return; } - async applyTargetState( - targetId: TargetId, - state: QueryTargetState, - error?: FirestoreError - ): Promise { - if (this._isPrimaryClient) { - // If we receive a target state notification via WebStorage, we are - // either already secondary or another tab has taken the primary lease. - logDebug(LOG_TAG, 'Ignoring unexpected query state notification.'); - return; + for (const targetId of added) { + if (syncEngineImpl.queriesByTarget.has(targetId)) { + // A target might have been added in a previous attempt + logDebug(LOG_TAG, 'Adding an already active target ' + targetId); + continue; } - if (this.queriesByTarget.has(targetId)) { - switch (state) { - case 'current': - case 'not-current': { - const changes = await getNewDocumentChanges(this.localStore); - const synthesizedRemoteEvent = RemoteEvent.createSynthesizedRemoteEventForCurrentChange( - targetId, - state === 'current' - ); - await this.emitNewSnapsAndNotifyLocalStore( - changes, - synthesizedRemoteEvent - ); - break; - } - case 'rejected': { - await this.localStore.releaseTarget( - targetId, - /* keepPersistedTargetData */ true - ); - this.removeAndCleanupTarget(targetId, error); - break; - } - default: - fail('Unexpected target state: ' + state); - } - } + const target = await getCachedTarget(syncEngineImpl.localStore, targetId); + debugAssert(!!target, `Query data for active target ${targetId} not found`); + const targetData = await syncEngineImpl.localStore.allocateTarget(target); + await syncEngineImpl.initializeViewAndComputeSnapshot( + synthesizeTargetToQuery(target), + targetData.targetId, + /*current=*/ false + ); + syncEngineImpl.remoteStore.listen(targetData); } - async applyActiveTargetsChange( - added: TargetId[], - removed: TargetId[] - ): Promise { - if (!this._isPrimaryClient) { - return; + for (const targetId of removed) { + // Check that the target is still active since the target might have been + // removed if it has been rejected by the backend. + if (!syncEngineImpl.queriesByTarget.has(targetId)) { + continue; } - for (const targetId of added) { - if (this.queriesByTarget.has(targetId)) { - // A target might have been added in a previous attempt - logDebug(LOG_TAG, 'Adding an already active target ' + targetId); - continue; - } - - const target = await getCachedTarget(this.localStore, targetId); - debugAssert( - !!target, - `Query data for active target ${targetId} not found` - ); - const targetData = await this.localStore.allocateTarget(target); - await this.initializeViewAndComputeSnapshot( - this.synthesizeTargetToQuery(target), - targetData.targetId, - /*current=*/ false - ); - this.remoteStore.listen(targetData); - } - - for (const targetId of removed) { - // Check that the target is still active since the target might have been - // removed if it has been rejected by the backend. - if (!this.queriesByTarget.has(targetId)) { - continue; - } - - // Release queries that are still active. - await this.localStore - .releaseTarget(targetId, /* keepPersistedTargetData */ false) - .then(() => { - this.remoteStore.unlisten(targetId); - this.removeAndCleanupTarget(targetId); - }) - .catch(ignoreIfPrimaryLeaseLoss); - } + // Release queries that are still active. + await syncEngineImpl.localStore + .releaseTarget(targetId, /* keepPersistedTargetData */ false) + .then(() => { + syncEngineImpl.remoteStore.unlisten(targetId); + syncEngineImpl.removeAndCleanupTarget(targetId); + }) + .catch(ignoreIfPrimaryLeaseLoss); } } - -export function newMultiTabSyncEngine( - localStore: LocalStore, - remoteStore: RemoteStore, - datastore: Datastore, - sharedClientState: SharedClientState, - currentUser: User, - maxConcurrentLimboResolutions: number -): MultiTabSyncEngine { - return new MultiTabSyncEngineImpl( - localStore, - remoteStore, - datastore, - sharedClientState, - currentUser, - maxConcurrentLimboResolutions - ); -} diff --git a/packages/firestore/src/local/shared_client_state_syncer.ts b/packages/firestore/src/local/shared_client_state_syncer.ts index 29736807277..a8804938aae 100644 --- a/packages/firestore/src/local/shared_client_state_syncer.ts +++ b/packages/firestore/src/local/shared_client_state_syncer.ts @@ -50,3 +50,28 @@ export interface SharedClientStateSyncer { /** Returns the IDs of the clients that are currently active. */ getActiveClients(): Promise; } + +/** + * An implementation of SharedClientStateSyncer that provides (mostly) empty + * implementations for the SharedClientStateSyncer protocol. + */ +export class MemorySharedClientStateSyncer implements SharedClientStateSyncer { + constructor(private readonly activeClients: ClientId[]) {} + async applyBatchState( + batchId: BatchId, + state: MutationBatchState, + error?: FirestoreError + ): Promise {} + async getActiveClients(): Promise { + return this.activeClients; + } + async applyTargetState( + targetId: TargetId, + state: QueryTargetState, + error?: FirestoreError + ): Promise {} + async applyActiveTargetsChange( + added: TargetId[], + removed: TargetId[] + ): Promise {} +} diff --git a/packages/firestore/test/unit/local/persistence_test_helpers.ts b/packages/firestore/test/unit/local/persistence_test_helpers.ts index 98f84dbacd5..fdf57d266fe 100644 --- a/packages/firestore/test/unit/local/persistence_test_helpers.ts +++ b/packages/firestore/test/unit/local/persistence_test_helpers.ts @@ -20,8 +20,6 @@ import { DatabaseId } from '../../../src/core/database_info'; import { SequenceNumberSyncer } from '../../../src/core/listen_sequence'; import { BatchId, - MutationBatchState, - OnlineState, TargetId, ListenSequenceNumber } from '../../../src/core/types'; @@ -43,14 +41,10 @@ import { ClientId, WebStorageSharedClientState } from '../../../src/local/shared_client_state'; -import { - QueryTargetState, - SharedClientStateSyncer -} from '../../../src/local/shared_client_state_syncer'; +import { MemorySharedClientStateSyncer } from '../../../src/local/shared_client_state_syncer'; import { SimpleDb } from '../../../src/local/simple_db'; import { JsonProtoSerializer } from '../../../src/remote/serializer'; import { AsyncQueue } from '../../../src/util/async_queue'; -import { FirestoreError } from '../../../src/util/error'; import { AutoId } from '../../../src/util/misc'; import { WindowLike } from '../../../src/util/types'; import { getDocument, getWindow } from '../../../src/platform/dom'; @@ -143,32 +137,6 @@ export function clearTestPersistence(): Promise { return indexedDbClearPersistence(TEST_PERSISTENCE_PREFIX); } -class NoOpSharedClientStateSyncer implements SharedClientStateSyncer { - constructor(private readonly activeClients: ClientId[]) {} - async applyBatchState( - batchId: BatchId, - state: MutationBatchState, - error?: FirestoreError - ): Promise {} - async applySuccessfulWrite(batchId: BatchId): Promise {} - async rejectFailedWrite( - batchId: BatchId, - err: FirestoreError - ): Promise {} - async getActiveClients(): Promise { - return this.activeClients; - } - async applyTargetState( - targetId: TargetId, - state: QueryTargetState, - error?: FirestoreError - ): Promise {} - async applyActiveTargetsChange( - added: TargetId[], - removed: TargetId[] - ): Promise {} - applyOnlineStateChange(onlineState: OnlineState): void {} -} /** * Populates Web Storage with instance data from a pre-existing client. */ @@ -189,7 +157,7 @@ export async function populateWebStorage( user ); - secondaryClientState.syncEngine = new NoOpSharedClientStateSyncer([ + secondaryClientState.syncEngine = new MemorySharedClientStateSyncer([ existingClientId ]); secondaryClientState.onlineStateHandler = () => {}; From e1cd35b727d5ecc5accc58c407e95e5bd2f770db Mon Sep 17 00:00:00 2001 From: Sebastian Schmidt Date: Mon, 20 Jul 2020 15:27:11 -0700 Subject: [PATCH 2/4] Simplify --- .../firestore/src/core/component_provider.ts | 29 ++++++++++--------- packages/firestore/src/core/sync_engine.ts | 4 +-- packages/firestore/src/local/local_store.ts | 2 +- .../src/local/shared_client_state.ts | 3 -- .../src/local/shared_client_state_syncer.ts | 25 ---------------- .../unit/local/persistence_test_helpers.ts | 8 ----- .../web_storage_shared_client_state.test.ts | 9 +++--- 7 files changed, 22 insertions(+), 58 deletions(-) diff --git a/packages/firestore/src/core/component_provider.ts b/packages/firestore/src/core/component_provider.ts index 4e8c4ef87e8..5010be09287 100644 --- a/packages/firestore/src/core/component_provider.ts +++ b/packages/firestore/src/core/component_provider.ts @@ -48,11 +48,10 @@ import { Code, FirestoreError } from '../util/error'; import { OnlineStateSource } from './types'; import { LruParams, LruScheduler } from '../local/lru_garbage_collector'; import { IndexFreeQueryEngine } from '../local/index_free_query_engine'; -import { MemorySharedClientStateSyncer } from '../local/shared_client_state_syncer'; import { - indexedDbStoragePrefix, + indexedDbClearPersistence, IndexedDbPersistence, - indexedDbClearPersistence + indexedDbStoragePrefix } from '../local/indexeddb_persistence'; import { MemoryEagerDelegate, @@ -63,6 +62,7 @@ import { newSerializer } from '../platform/serializer'; import { getDocument, getWindow } from '../platform/dom'; import { CredentialsProvider } from '../api/credentials'; import { Connection } from '../remote/connection'; + const MEMORY_ONLY_PERSISTENCE_ERROR_MESSAGE = 'You are using the memory-only build of Firestore. Persistence support is ' + 'only available via the @firebase/firestore bundle or the ' + @@ -198,7 +198,7 @@ export class MemoryComponentProvider implements ComponentProvider { } createSyncEngine(cfg: ComponentConfiguration): SyncEngine { - const syncEngine = newSyncEngine( + return newSyncEngine( this.localStore, this.remoteStore, this.datastore, @@ -207,10 +207,6 @@ export class MemoryComponentProvider implements ComponentProvider { cfg.maxConcurrentLimboResolutions, /* isPrimary= */ true ); - this.sharedClientState.syncEngine = new MemorySharedClientStateSyncer([ - cfg.clientId - ]); - return syncEngine; } clearPersistence( @@ -330,12 +326,17 @@ export class MultiTabIndexedDbComponentProvider extends IndexedDbComponentProvid cfg.maxConcurrentLimboResolutions, startsAsPrimary ); - this.sharedClientState.syncEngine = { - applyBatchState: applyBatchState.bind(null, syncEngine), - applyTargetState: applyTargetState.bind(null, syncEngine), - applyActiveTargetsChange: applyActiveTargetsChange.bind(null, syncEngine), - getActiveClients: getActiveClients.bind(null, syncEngine) - }; + if (this.sharedClientState instanceof WebStorageSharedClientState) { + this.sharedClientState.syncEngine = { + applyBatchState: applyBatchState.bind(null, syncEngine), + applyTargetState: applyTargetState.bind(null, syncEngine), + applyActiveTargetsChange: applyActiveTargetsChange.bind( + null, + syncEngine + ), + getActiveClients: getActiveClients.bind(null, syncEngine) + }; + } return syncEngine; } diff --git a/packages/firestore/src/core/sync_engine.ts b/packages/firestore/src/core/sync_engine.ts index ba97d4c2f38..0300fa1af12 100644 --- a/packages/firestore/src/core/sync_engine.ts +++ b/packages/firestore/src/core/sync_engine.ts @@ -21,7 +21,7 @@ import { getCachedTarget, ignoreIfPrimaryLeaseLoss, LocalStore, - getCurrentlyActiveClients, + getActiveClientsFromPersistence, lookupMutationDocuments, removeCachedMutationBatchMetadata } from '../local/local_store'; @@ -1234,7 +1234,7 @@ function synthesizeTargetToQuery(target: Target): Query { // PORTING NOTE: Multi-Tab only. export function getActiveClients(syncEngine: SyncEngine): Promise { const syncEngineImpl = debugCast(syncEngine, SyncEngineImpl); - return getCurrentlyActiveClients(syncEngineImpl.localStore); + return getActiveClientsFromPersistence(syncEngineImpl.localStore); } /** Applies a query target change from a different tab. */ diff --git a/packages/firestore/src/local/local_store.ts b/packages/firestore/src/local/local_store.ts index c25c8542ef4..c6cd44943d5 100644 --- a/packages/firestore/src/local/local_store.ts +++ b/packages/firestore/src/local/local_store.ts @@ -1090,7 +1090,7 @@ export function removeCachedMutationBatchMetadata( } // PORTING NOTE: Multi-Tab only. -export function getCurrentlyActiveClients( +export function getActiveClientsFromPersistence( localStore: LocalStore ): Promise { const persistenceImpl = debugCast( diff --git a/packages/firestore/src/local/shared_client_state.ts b/packages/firestore/src/local/shared_client_state.ts index a333ff74c43..03815ce7095 100644 --- a/packages/firestore/src/local/shared_client_state.ts +++ b/packages/firestore/src/local/shared_client_state.ts @@ -75,7 +75,6 @@ export type ClientId = string; * assigned before calling `start()`. */ export interface SharedClientState { - syncEngine: SharedClientStateSyncer | null; onlineStateHandler: ((onlineState: OnlineState) => void) | null; sequenceNumberHandler: | ((sequenceNumber: ListenSequenceNumber) => void) @@ -1059,8 +1058,6 @@ function fromWebStorageSequenceNumber( export class MemorySharedClientState implements SharedClientState { private localState = new LocalClientState(); private queryState: { [targetId: number]: QueryTargetState } = {}; - - syncEngine: SharedClientStateSyncer | null = null; onlineStateHandler: ((onlineState: OnlineState) => void) | null = null; sequenceNumberHandler: | ((sequenceNumber: ListenSequenceNumber) => void) diff --git a/packages/firestore/src/local/shared_client_state_syncer.ts b/packages/firestore/src/local/shared_client_state_syncer.ts index a8804938aae..29736807277 100644 --- a/packages/firestore/src/local/shared_client_state_syncer.ts +++ b/packages/firestore/src/local/shared_client_state_syncer.ts @@ -50,28 +50,3 @@ export interface SharedClientStateSyncer { /** Returns the IDs of the clients that are currently active. */ getActiveClients(): Promise; } - -/** - * An implementation of SharedClientStateSyncer that provides (mostly) empty - * implementations for the SharedClientStateSyncer protocol. - */ -export class MemorySharedClientStateSyncer implements SharedClientStateSyncer { - constructor(private readonly activeClients: ClientId[]) {} - async applyBatchState( - batchId: BatchId, - state: MutationBatchState, - error?: FirestoreError - ): Promise {} - async getActiveClients(): Promise { - return this.activeClients; - } - async applyTargetState( - targetId: TargetId, - state: QueryTargetState, - error?: FirestoreError - ): Promise {} - async applyActiveTargetsChange( - added: TargetId[], - removed: TargetId[] - ): Promise {} -} diff --git a/packages/firestore/test/unit/local/persistence_test_helpers.ts b/packages/firestore/test/unit/local/persistence_test_helpers.ts index fdf57d266fe..a5312f9628e 100644 --- a/packages/firestore/test/unit/local/persistence_test_helpers.ts +++ b/packages/firestore/test/unit/local/persistence_test_helpers.ts @@ -41,7 +41,6 @@ import { ClientId, WebStorageSharedClientState } from '../../../src/local/shared_client_state'; -import { MemorySharedClientStateSyncer } from '../../../src/local/shared_client_state_syncer'; import { SimpleDb } from '../../../src/local/simple_db'; import { JsonProtoSerializer } from '../../../src/remote/serializer'; import { AsyncQueue } from '../../../src/util/async_queue'; @@ -156,13 +155,6 @@ export async function populateWebStorage( existingClientId, user ); - - secondaryClientState.syncEngine = new MemorySharedClientStateSyncer([ - existingClientId - ]); - secondaryClientState.onlineStateHandler = () => {}; - await secondaryClientState.start(); - for (const batchId of existingMutationBatchIds) { secondaryClientState.addPendingMutation(batchId); } diff --git a/packages/firestore/test/unit/local/web_storage_shared_client_state.test.ts b/packages/firestore/test/unit/local/web_storage_shared_client_state.test.ts index ecfa554780b..ec8ef1b3b0d 100644 --- a/packages/firestore/test/unit/local/web_storage_shared_client_state.test.ts +++ b/packages/firestore/test/unit/local/web_storage_shared_client_state.test.ts @@ -32,10 +32,6 @@ import { SharedClientState, WebStorageSharedClientState } from '../../../src/local/shared_client_state'; -import { - QueryTargetState, - SharedClientStateSyncer -} from '../../../src/local/shared_client_state_syncer'; import { targetIdSet } from '../../../src/model/collections'; import { AsyncQueue } from '../../../src/util/async_queue'; import { FirestoreError } from '../../../src/util/error'; @@ -48,6 +44,10 @@ import { } from './persistence_test_helpers'; import { testWindow } from '../../util/test_platform'; import { WindowLike } from '../../../src/util/types'; +import { + QueryTargetState, + SharedClientStateSyncer +} from '../../../src/local/shared_client_state_syncer'; /* eslint-disable no-restricted-globals */ @@ -215,7 +215,6 @@ describe('WebStorageSharedClientState', () => { AUTHENTICATED_USER ); clientSyncer = new TestSharedClientSyncer([primaryClientId]); - sharedClientState.syncEngine = clientSyncer; sharedClientState.onlineStateHandler = clientSyncer.applyOnlineStateChange.bind( clientSyncer ); From 15a76783aa5cc735bde5047d9b3af4c22c5cbb99 Mon Sep 17 00:00:00 2001 From: Sebastian Schmidt Date: Mon, 20 Jul 2020 16:04:12 -0700 Subject: [PATCH 3/4] Test fix --- .../local/web_storage_shared_client_state.test.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/firestore/test/unit/local/web_storage_shared_client_state.test.ts b/packages/firestore/test/unit/local/web_storage_shared_client_state.test.ts index ec8ef1b3b0d..98355308a80 100644 --- a/packages/firestore/test/unit/local/web_storage_shared_client_state.test.ts +++ b/packages/firestore/test/unit/local/web_storage_shared_client_state.test.ts @@ -32,6 +32,10 @@ import { SharedClientState, WebStorageSharedClientState } from '../../../src/local/shared_client_state'; +import { + QueryTargetState, + SharedClientStateSyncer +} from '../../../src/local/shared_client_state_syncer'; import { targetIdSet } from '../../../src/model/collections'; import { AsyncQueue } from '../../../src/util/async_queue'; import { FirestoreError } from '../../../src/util/error'; @@ -44,10 +48,6 @@ import { } from './persistence_test_helpers'; import { testWindow } from '../../util/test_platform'; import { WindowLike } from '../../../src/util/types'; -import { - QueryTargetState, - SharedClientStateSyncer -} from '../../../src/local/shared_client_state_syncer'; /* eslint-disable no-restricted-globals */ @@ -158,7 +158,7 @@ describe('WebStorageSharedClientState', () => { let queue: AsyncQueue; let primaryClientId: string; - let sharedClientState: SharedClientState; + let sharedClientState: WebStorageSharedClientState; let clientSyncer: TestSharedClientSyncer; let previousAddEventListener: typeof window.addEventListener; @@ -215,6 +215,7 @@ describe('WebStorageSharedClientState', () => { AUTHENTICATED_USER ); clientSyncer = new TestSharedClientSyncer([primaryClientId]); + sharedClientState.syncEngine = clientSyncer; sharedClientState.onlineStateHandler = clientSyncer.applyOnlineStateChange.bind( clientSyncer ); From a70479b4b48d5caee6fa7deb1e0732437a9305e4 Mon Sep 17 00:00:00 2001 From: Sebastian Schmidt Date: Mon, 20 Jul 2020 16:31:52 -0700 Subject: [PATCH 4/4] Update web_storage_shared_client_state.test.ts --- .../test/unit/local/web_storage_shared_client_state.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/firestore/test/unit/local/web_storage_shared_client_state.test.ts b/packages/firestore/test/unit/local/web_storage_shared_client_state.test.ts index 98355308a80..5b3578649a9 100644 --- a/packages/firestore/test/unit/local/web_storage_shared_client_state.test.ts +++ b/packages/firestore/test/unit/local/web_storage_shared_client_state.test.ts @@ -29,7 +29,6 @@ import { LocalClientState, MutationMetadata, QueryTargetMetadata, - SharedClientState, WebStorageSharedClientState } from '../../../src/local/shared_client_state'; import {