-
Notifications
You must be signed in to change notification settings - Fork 934
Factor out Multi-Tab code #2882
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 9 commits
73c5947
7178633
b7766c0
2448b16
d9b0725
7835818
4eee9a1
dc09fd1
86ee9fa
fefc69c
30a8251
114be4b
a0bc0ce
ba98a20
4c3e474
7ef2d47
c4d6a84
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm unable to comment on lines of code outside of what's changed however I see a few high level things:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
As part of the Master merge, I also had to fix the spec tests to be aware of the differences between the different persistence implementations. |
||
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<void> { | ||
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, | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This review doesn't include
MultiTabSyncEngine
but I have some thoughts on how to moveSharedClientState
and related properties into it. Most of these options will make the code ugly, awkward or might not be viable but want to throw it out there anyway.In any method in
SyncEngine
that has multi-tab logic is it possible to refactor it to not be in the middle? For exampleremoveAndCleanupTarget()
only has one line at the top. Could you do that to everything else? If so, you could rely on method overloading inMultiTabSyncEngine
to do pre/post processing and call the base class method in the appropriate place. If the logic can't be split it might be worth overloading the method entirely and relying on unit tests to ensure consistency.Alternatively, in methods where you need a return value from
SharedClientState
you can modify the method to have an optional callback parameter that usually gives you a default value but inMultiTabSyncEngine
you can overload the method and call into the base with an argument for the callback that gives you a real result. You can do the same for notification only methods OR raise an event thatMultiTabSyncEngine
can subscribe to. The latter is super weird but eliminates the reference entirely without needing a stub implementation.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I looked through all the calls to SharedClientState to see if we can improve the logic and remove some of the tight coupling. Unfortunately, I think in almost all cases the current logic is simpler than what we would have if we pulled out the code:
MultiTabSyncEngine
.removeAndCleanupTarget()
) this is not actually all that bad - but it creates some inconsistencies between target allocation and target cleanup. I would rather keep all of this code in one place.I did create an overwrite for
applyOnlineStateChange
. This method has different behavior for Multi-Tab clients and has no "mirrored" behavior in SyncEngine.