Skip to content

Memory-only Firestore build #2608

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 23 commits into from
Mar 18, 2020
Merged

Memory-only Firestore build #2608

merged 23 commits into from
Mar 18, 2020

Conversation

schmidt-sebastian
Copy link
Contributor

@schmidt-sebastian schmidt-sebastian commented Feb 8, 2020

This PR exposes a Memory-only version of Firestore that is 50 KB smaller than our current build. It does so by implementing two different Firestore classes, only one of which supports enablePersistence() and clearPersistence().

The new memory-only Firestore classes are exported as:

  • @firebase/firestore/memory
  • firebase/firestore/memory
  • firebase-firestore.memory.js

This PR also changes the integration tests to run against both versions of Firestore

@schmidt-sebastian
Copy link
Contributor Author

@Feiyang1 Do you mind taking a look? If this is not moving in the direction of go/firebase-next, then we can also create Memory-only Firestore class and a PeristenceFirestore class that extends from it. That would provide more type safety.

import { FirebaseNamespace } from '@firebase/app-types';

import { configureForFirebase } from './src/platform/config';
import './src/platform_browser/browser_init';
Copy link
Member

Choose a reason for hiding this comment

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

Do you mean import './src/platform_node/node_init';?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed. Thanks for catching.

if (!canFallback(error)) {
throw error;
}
console.warn(
Copy link
Member

Choose a reason for hiding this comment

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

use log from import * as log from '../util/log';?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Firestore doesn't support the log level "Warn".

*
* @returns A promise that will successfully resolve.
*/
export function newMemoryPersistence(
Copy link
Member

Choose a reason for hiding this comment

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

Just an organizational comment - Should this function live in src/api/persistence.ts ?

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 tried to make it so the memory client doesn't import any persistence-related source (to make sure that my asserts in the rollup script work). Until we have size presubmits, I would like to keep these asserts to ensure that we don't accidentally include the wrong files.

To address your concern, I moved the factory-methods to their corresponding classes in ./local/persistence

* Patches the Firestore prototype and provides the backing implementations for
* `Firestore.enablePersistence()` and `Firestore.clearPersistence()`.
*/
export function registerFirestorePersistence(
Copy link
Member

Choose a reason for hiding this comment

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

Instead of patching prototype, a cleaner way might be passing the persistence logics as a constructor parameter to Firestore class. Something like:

class Firestore {
 constructor(
    databaseIdOrApp: FirestoreDatabase | FirebaseApp,
    authProvider: Provider<FirebaseAuthInternalName>,
    private persistence: FirestorePersistence | null
  ) { ... }

  enablePersistence() { 
       if ( this.persistence) {
         ...
       } else {
          throw new Error(PERSISTENCE_MISSING_ERROR_MSG);
       }
  }
}

It can potentially allow us to export persistence logics and let developers decide whether or not to use persistence instead of us making multiple build targets.

Copy link
Member

@Feiyang1 Feiyang1 Feb 13, 2020

Choose a reason for hiding this comment

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

we can also create Memory-only Firestore class and a PeristenceFirestore class that extends from it is also a good option that potentially allows developer to pick which one to use .

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 updated the PR to use two different Firestore classes.

@schmidt-sebastian schmidt-sebastian changed the title RFC: Memory-only Firestore build Memory-only Firestore build Feb 20, 2020
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.

Revamped the PR to use two different Firestore classes. PTAL.

* the key 'firestore'. This is used for wrapped binary that exposes Firestore
* as a goog module.
*/
export function configureForStandalone(exportObject: {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

FYI: This is part of #2646

@schmidt-sebastian
Copy link
Contributor Author

PTAL @Feiyang1 :)

Copy link
Member

@Feiyang1 Feiyang1 left a comment

Choose a reason for hiding this comment

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

LGTM, but I think the UX will be better if we make whether or not to use persistence a runtime choice instead of a compile time choice.

From my experience interacting with developers, many people don't know we provide different builds. Guides will need to be provided and they will likely live outside the reference docs, so I'm concerned many people will miss it. It also puts burden on developers that they need to dive into the build tool options to figure out how to pick the right build. As a result, we will likely see Github issues that ask why their build setup doesn't work, which can require a lot of time to debug.

I would advocate that we make the persistence/no persistence choice part of our official API, so it can be documented in reference docs and it will be very easy for developer to understand and use. Perhaps something like:

import * as firebase from 'firebase/app';
import { indexeddbPersistence } from 'firebase/firestore';

const app = firebase.initializeApp(...);
const firestore = app.firestore();

// enable persistence at runtime
firestore.enablePersistence(indexeddbPersistence, settings);

json(),
terser(terserOptions)
],
external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`))
Copy link
Member

Choose a reason for hiding this comment

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

use resolveMemoryExterns instead ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

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. Your suggestion seems like a good improvement and I will discuss with the Firestore team if we should adopt it.

I do have two questions though:

  • What should we do about the CDN builds? Should we only have a "firestore with everything build" and not provide the size savings for CDN users?
  • I believe the final API to run on persistence will look more like enablePersistence(firestore, settings) rather than firestore.enablePersistence(implementation, settings). If we roll this out via a breaking change, would it make more sense to use the firebase@next API directly?

json(),
terser(terserOptions)
],
external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`))
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

@Feiyang1
Copy link
Member

Feiyang1 commented Mar 4, 2020

  • What should we do about the CDN builds? Should we only have a "firestore with everything build" and not provide the size savings for CDN users?

We can always make a special CDN build without persistence if there is sufficient demand, but I will be fine with just a firestore with everything build in CDN. In fact, we will only have [auth/app/...] with everything build in CDN after the modularization.

  • I believe the final API to run on persistence will look more like enablePersistence(firestore, settings) rather than firestore.enablePersistence(implementation, settings). If we roll this out via a breaking change, would it make more sense to use the firebase@next API directly?

SG. IIUC, I think it makes sense to make it part of firebase@next effort, but we don't have a proposal for Firestore yet. Perhaps we can create a proposal based on the persistence work?

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.

Updated the PR to use composition. It probably makes more sense to compare against master than against the previous revision.

"test": "echo 'Automated tests temporarily disabled, run `yarn test:manual` to execute these tests'",
"test:manual": "karma start --single-run",
"test:debug": "karma start --auto-watch --browsers Chrome"
"build:dependencies": "cd ../../ ; yarn build",
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed. Note that the main entry points should be test and related rules.

"test:manual": "karma start --single-run",
"test:debug": "karma start --auto-watch --browsers Chrome"
"build:dependencies": "cd ../../ ; yarn build",
"build:persistence": "DISABLE_FIRESTORE_PERSISTENCE=false gulp compile-tests",
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 have gone through multiple versions of this. I ended up with the negative flag since it allows me to treat the absence of the flag and its default value ("false") similarly. I, however, don't feel strongly and the check for the flag value is pretty isolated. Updated to use INCLUDE_FIRESTORE_PERSISTENCE.

"build:dependencies": "cd ../../ ; yarn build",
"build:persistence": "DISABLE_FIRESTORE_PERSISTENCE=false gulp compile-tests",
"build:memory": "DISABLE_FIRESTORE_PERSISTENCE=true gulp compile-tests",
"test": "yarn build:memory; karma start --single-run; yarn build:persistence; karma start --single-run;",
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Right now, gulp uses the same output folder for both modes. This means that the builds overwrite each other. We can probably use different output folders, but this would require some extra plumbing. Let me know if this is something that you want me to do.

* limitations under the License.
*/

import '@firbease/firestore/memory';
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This supports the firebase/firestore/memory import, which I believe we don't advocate for publicly.

@Feiyang1 Can you elaborate on the need for this?

@@ -0,0 +1,6 @@
{
"name": "firebase/firestore/memory",
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is modeled after the package.json files in firebase@next, but I believe you are right. @Feiyang1 should we add license/description fields?

@@ -382,65 +378,11 @@ export class Firestore implements firestore.FirebaseFirestore, FirebaseService {
}

enablePersistence(settings?: firestore.PersistenceSettings): Promise<void> {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

We could, but we would at the very least have to publish two different typings (and go through API review). Without duplicated typing, having these method here to throw an error seems like a better experience.

@@ -2678,7 +2624,7 @@ function applyFirestoreDataConverter<T>(

// We're treating the variables as class names, so disable checking for lower
// case variable names.
export const PublicFirestore = makeConstructorPrivate(
export const PublicMemoryFirestore = makeConstructorPrivate(
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 overhauled the PR to use composition instead. enablePersistence() and clearPersistence() invoke a Persistence, and all private members are passed to the provider.

* A Firestore-implementation that provides support for `enablePersistence()`
* and `clearPersistence()`.
*/
export class PersistenceFirestore extends Firestore {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is no longer applicable.

*
* @returns A Promise that resolves with the various persistence components.
*/
export const newIndexedDbPersistence: PersistenceFactory<IndexedDbPersistenceSettings> = async (
Copy link
Contributor Author

Choose a reason for hiding this comment

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

No longer applicable, but this style allows me to omit the return type (and I probably could have omitted the argument types as well).

(firebase as _FirebaseNamespace).INTERNAL.registerComponent(
new Component(
'firestore',
container => {
const app = container.getProvider('app').getImmediate()!;
return new Firestore(app, container.getProvider('auth-internal'));
return new internalFirestore(
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 am now able to use the constructor again, which reverts much of the changes here. The only difference is that I am now passing in a PersistenceProvider.

Copy link
Contributor

@wilhuff wilhuff left a comment

Choose a reason for hiding this comment

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

Very close to LGTM. A final suggestion on configureForFirebase and some nits/optional suggestions.

"build:dependencies": "cd ../../ ; yarn build",
"build:persistence": "DISABLE_FIRESTORE_PERSISTENCE=false gulp compile-tests",
"build:memory": "DISABLE_FIRESTORE_PERSISTENCE=true gulp compile-tests",
"test": "yarn build:memory; karma start --single-run; yarn build:persistence; karma start --single-run;",
Copy link
Contributor

Choose a reason for hiding this comment

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

Ah. I see. It's certainly not something to tackle in this PR. If gulp were better about doing incremental builds this could be worthwhile, but I suspect it's not going to actually buy us much or possibly even anything.

* limitations under the License.
*/

import '../../../firestore/dist/index.memory.esm';
Copy link
Contributor

Choose a reason for hiding this comment

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

It's still the case that looking at this file I can't tell why it exists, what bits of the build it interacts with, and whether or not I should use for anything when e.g. writing Firestore tests. I think this file is establishing the public interface to Firestore as seen when a user imports @firebase/firestore/memory, right? Comments that address these concerns would be helpful.

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 short comment that explains this a little bit.

});
return deferred.promise;
async clearPersistence(): Promise<void> {
if (!this._persistenceProvider.isDurable) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Is it possible to fold the throwing of these errors into the memory-only persistence provider?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, that's a good clean up. I moved them to the MemoryPersistenceProvider.

@@ -59,14 +60,27 @@ const firestoreNamespace = {

/**
* Configures Firestore as part of the Firebase SDK by calling registerService.
*
* @param firebase The FirebaseNamespace to register Firestore with
* @param persistenceProviderFactory A factory function that returns a
Copy link
Contributor

Choose a reason for hiding this comment

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

FWIW, what I was proposing was essentially to remove the Firestore instance creation from here altogether so that all the instances could be newed up together. Something like this here:

export function configureForFirebase(
  firebase: FirebaseNamespace,
  firestoreProvider: (app: FirebaseApp, auth: Provider<FirebaseAuthInternalName>) => Firestore
): void {
  (firebase as _FirebaseNamespace).INTERNAL.registerComponent(
    new Component(
      'firestore',
      container => {
        const app = container.getProvider('app').getImmediate()!;
        const authProvider = container.getProvider('auth-internal');
        return firestoreProvider(app, authProvider);
      },
      ComponentType.PUBLIC
    ).setServiceProps(shallowCopy(firestoreNamespace))
  );
}

For example, this is what index.memory.ts would look like:

export function registerFirestore(instance: FirebaseNamespace): void {
  configureForFirebase(instance, (app, authProvider) => {
    const persistenceProvider = new MemoryPersistenceProvider();
    return new Firestore(app, authProvider, persistenceProvider);
  });
  instance.registerVersion(name, version);
}

The benefit of this arrangement is that we clearly separate how we plug into the component framework from the way we instantiate Firestore instances. If we move toward modularized initialization configureForFirebase won't have to know, and we won't have to refactor all the ways that Firestore instances are created at the same time.

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 like what you have proposed and shamelessly updated the PR.

* True if the underlying implementation supports durable storage via
* `enablePersistence()`.
*/
readonly isDurable: boolean;
Copy link
Contributor

Choose a reason for hiding this comment

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

Couldn't we have configure throw if the settings include enabling persistence but the provider doesn't actually support it?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's how I ended up implementing the new error semantics for the MemoryPersistenceProvider.

persistenceResult: Deferred<void>
): Promise<void> {
try {
await persistenceProvider.configure(
Copy link
Contributor

Choose a reason for hiding this comment

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

Since persistence provider's configure method is called during initializePersistence, consider calling this method initialize.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

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.

Addressed all feedback and also added an integration tests. This test surfaced an existing issue in canFallback: Instanceof checks don't seem to work for errors in JS (https://stackoverflow.com/questions/33870684/why-doesnt-instanceof-work-on-instances-of-error-subclasses-under-babel-node), so I had to change canFallback to rely on the error name instead.

* limitations under the License.
*/

import '../../../firestore/dist/index.memory.esm';
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 short comment that explains this a little bit.

@@ -0,0 +1,6 @@
{
"name": "firebase/firestore/memory",
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Descriptions added to this package.json and top-level package.json.

});
return deferred.promise;
async clearPersistence(): Promise<void> {
if (!this._persistenceProvider.isDurable) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, that's a good clean up. I moved them to the MemoryPersistenceProvider.

persistenceResult: Deferred<void>
): Promise<void> {
try {
await persistenceProvider.configure(
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

* True if the underlying implementation supports durable storage via
* `enablePersistence()`.
*/
readonly isDurable: boolean;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's how I ended up implementing the new error semantics for the MemoryPersistenceProvider.

@@ -59,14 +60,27 @@ const firestoreNamespace = {

/**
* Configures Firestore as part of the Firebase SDK by calling registerService.
*
* @param firebase The FirebaseNamespace to register Firestore with
* @param persistenceProviderFactory A factory function that returns a
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 like what you have proposed and shamelessly updated the PR.

@schmidt-sebastian schmidt-sebastian force-pushed the mrschmidt/memorybuild branch 2 times, most recently from 9949ce7 to cbfa9a1 Compare March 18, 2020 04:34
Copy link
Contributor

@wilhuff wilhuff left a comment

Choose a reason for hiding this comment

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

LGTM

// size.
//
// See https://g3doc.corp.google.com/firebase/jscore/g3doc/contributing/builds.md
Copy link
Contributor

Choose a reason for hiding this comment

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

FYI, you can shorten this to http://g3doc/firebase/jscore/g3doc/contributing/builds.md if you wanted.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

@schmidt-sebastian schmidt-sebastian merged commit a5d80d9 into master Mar 18, 2020
@schmidt-sebastian schmidt-sebastian deleted the mrschmidt/memorybuild branch March 18, 2020 20:35
schmidt-sebastian added a commit that referenced this pull request Mar 21, 2020
#2608 re-introduced mangling to the Node build, which wasn't mangled before. Since it uses CJS, the output is not minified very well. This reverts this change and makes sure the next release uses the same settings as the current release.
@firebase firebase locked and limited conversation to collaborators Apr 18, 2020
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