Skip to content

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

Merged
merged 17 commits into from
Apr 21, 2020
Merged

Factor out Multi-Tab code #2882

merged 17 commits into from
Apr 21, 2020

Conversation

schmidt-sebastian
Copy link
Contributor

This PR is based on the changes in #2828 (and the reason for #2828) and meant to reduce the size of the memory-only build.

It creates two new classes: MultiTabLocalStore and MultiTabSyncEngine that each inherit from their old structures. It then moves all fully-encapsulated Multi-Tab only code the new classes which are only used in the full Firestore build.

Since MultiTabLocalStore only works with IndexedDbPersistence, this change also allows us to remove a few methods from MemoryPersistence that were never used. I did this by removing the methods from the shared interfaces and moving the method comment down to the IndexedDbBimplementation.

As a third thing, it changes LocalStoreTester. LocalStoreTester needs to know the source of the QueryEngine (whether index-free or not), but that plumbing is pretty gnarly, so I opted for the pattern used in C++ and added a type: https://github.com/firebase/firebase-ios-sdk/blob/d9b7a33f0c0464e72e894aa7c207efc07dadbbbe/Firestore/core/test/firebase/firestore/local/counting_query_engine.h#L62

@google-oss-bot
Copy link
Contributor

google-oss-bot commented Apr 8, 2020

Binary Size Report

Affected SDKs

  • @firebase/firestore

    Type Base (a36b51b) Head (751324f) Diff
    browser 248 kB 248 kB +353 B (+0.1%)
    esm2017 194 kB 194 kB +17 B (+0.0%)
    main 488 kB 489 kB +752 B (+0.2%)
    module 246 kB 246 kB +333 B (+0.1%)
  • @firebase/firestore/memory

    Type Base (a36b51b) Head (751324f) Diff
    browser 195 kB 189 kB -6.36 kB (-3.3%)
    esm2017 152 kB 149 kB -3.31 kB (-2.2%)
    main 377 kB 366 kB -11.5 kB (-3.1%)
    module 194 kB 188 kB -6.15 kB (-3.2%)
  • firebase

    Type Base (a36b51b) Head (751324f) Diff
    firebase-firestore.js 290 kB 290 kB +314 B (+0.1%)
    firebase-firestore.memory.js 239 kB 232 kB -6.11 kB (-2.6%)
    firebase.js 823 kB 824 kB +316 B (+0.0%)

Test Logs

@schmidt-sebastian
Copy link
Contributor Author

Note: Some of the unit tests are failing after Master merge. I will fix them later tonight.

Copy link
Contributor

@rafikhan rafikhan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Below is my feedback for multi-tab.

Along the way I found a small issue in the Firestore class. The constructor takes a persistenceProvider parameter and I think you meant this to be named componentProvider.

@@ -210,6 +197,68 @@ export class MemoryLruComponentProvider extends MemoryComponentProvider {
* Provides all components needed for Firestore with IndexedDB persistence.
*/
export class IndexedDbComponentProvider extends MemoryComponentProvider {
Copy link
Contributor

Choose a reason for hiding this comment

The 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:

  1. MemoryComponentProvider should explicitly implicitly implement ComponentProvider. Currently it doesn't.
  2. I think these assertions provide the correct guarantee with minimal lines of code but I wonder if instead you modified the ComponentProvider interface to take generic arguments for every member with defaults and then overrode it in MemoryComponentProvider and again here it would get you compile time safety and let you eliminate these assertions. It would be a large list of generic constraints which comes with its own problems but might be worth thinking about.

Copy link
Contributor Author

@schmidt-sebastian schmidt-sebastian Apr 20, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Fixed (also renamed the constructor argument in FirestoreClient).
  2. I opted not to go down the route and make this generic, but I did overwrite some of the members and use more explicit types. This gets rid of some of the asserts. Let me know if that is enough.

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.

import { LocalStore } from '../local/local_store';
import { SyncEngine } from './sync_engine';
import { LocalStore, MultiTabLocalStore } from '../local/local_store';
import { MultiTabSyncEngine, SyncEngine } from './sync_engine';
Copy link
Contributor

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 move SharedClientState 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 example removeAndCleanupTarget() only has one line at the top. Could you do that to everything else? If so, you could rely on method overloading in MultiTabSyncEngine 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 in MultiTabSyncEngine 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 that MultiTabSyncEngine can subscribe to. The latter is super weird but eliminates the reference entirely without needing a stub implementation.

Copy link
Contributor Author

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:

  • If we overwrite the methods to extract one line, it creates an interaction model that is much harder to follow. While it is relatively straightforward to pull out the first or last line in some of these methods, it essentially hides these lines from view if the use is not aware that they are overwritten in MultiTabSyncEngine.
  • In some cases (like 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.

Copy link
Contributor Author

@schmidt-sebastian schmidt-sebastian left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the review! I applied some of your suggestions and replied to your comments when I decided not to. To truly refactor out all Mutli-Tab code, the code flow becomes even more convoluted than it is, so at the moment I prefer to only refactor methods where we don't have to drastically alter the code flow.

I did experiment with creating a stand alone MultiTabComponentProvider. This is much cleaner, but we unfortunately don't know whether the user would like to opt into Multi-Tab persistence until the client has been initialized and cannot register the specific provider at compile time.

import { LocalStore } from '../local/local_store';
import { SyncEngine } from './sync_engine';
import { LocalStore, MultiTabLocalStore } from '../local/local_store';
import { MultiTabSyncEngine, SyncEngine } from './sync_engine';
Copy link
Contributor Author

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:

  • If we overwrite the methods to extract one line, it creates an interaction model that is much harder to follow. While it is relatively straightforward to pull out the first or last line in some of these methods, it essentially hides these lines from view if the use is not aware that they are overwritten in MultiTabSyncEngine.
  • In some cases (like 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.

@@ -210,6 +197,68 @@ export class MemoryLruComponentProvider extends MemoryComponentProvider {
* Provides all components needed for Firestore with IndexedDB persistence.
*/
export class IndexedDbComponentProvider extends MemoryComponentProvider {
Copy link
Contributor Author

@schmidt-sebastian schmidt-sebastian Apr 20, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Fixed (also renamed the constructor argument in FirestoreClient).
  2. I opted not to go down the route and make this generic, but I did overwrite some of the members and use more explicit types. This gets rid of some of the asserts. Let me know if that is enough.

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.

@schmidt-sebastian
Copy link
Contributor Author

I seem to have fixed all breakages. This is again ready for review.

constructor(private readonly queryEngine: QueryEngine) {}
constructor(
private readonly queryEngine: QueryEngine,
readonly type: 'index-free' | 'simple'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to create a type definition for this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a test-only enum.

allowTabSynchronization: multiClient,
persistenceKey: TEST_PERSISTENCE_PREFIX,
const persistence = new IndexedDbPersistence(
/* allowTabSynchronization= */ multiClient,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional: Consider including a comment for sequenceNumberSyncer and persistenceKey or dropping this one. The inconsistency seems strange.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The goal of these comments is to make the argument type descriptive when just the value is not descriptive by default. In this case, one could argue that multiClient alone is descriptive enough. Dropped.

allowTabSynchronization: !!options.synchronizeTabs,
persistenceKey: TEST_PERSISTENCE_PREFIX,
const persistence = new IndexedDbPersistence(
!!options.synchronizeTabs,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is another instance of my previous comment. You should comment this consistently.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did not add the inline argument name here since I deemed synchronizeTabs more descriptive than multiTab. As stated above, both are probably descriptive enough on their own.

Copy link
Contributor

@rafikhan rafikhan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@schmidt-sebastian schmidt-sebastian merged commit 5a60243 into master Apr 21, 2020
@firebase firebase locked and limited conversation to collaborators May 22, 2020
@schmidt-sebastian schmidt-sebastian deleted the mrschmidt/nomultitabv2 branch July 9, 2020 17:45
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants