-
Notifications
You must be signed in to change notification settings - Fork 939
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
Conversation
edae3ad
to
3f89126
Compare
@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. |
3f89126
to
860b649
Compare
import { FirebaseNamespace } from '@firebase/app-types'; | ||
|
||
import { configureForFirebase } from './src/platform/config'; | ||
import './src/platform_browser/browser_init'; |
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.
Do you mean import './src/platform_node/node_init';
?
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.
Fixed. Thanks for catching.
if (!canFallback(error)) { | ||
throw error; | ||
} | ||
console.warn( |
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.
use log from import * as log from '../util/log';
?
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.
Firestore doesn't support the log level "Warn".
* | ||
* @returns A promise that will successfully resolve. | ||
*/ | ||
export function newMemoryPersistence( |
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.
Just an organizational comment - Should this function live in src/api/persistence.ts
?
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 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( |
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.
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.
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.
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 .
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 updated the PR to use two different Firestore classes.
dc24a99
to
2080ba8
Compare
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.
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: { |
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.
FYI: This is part of #2646
PTAL @Feiyang1 :) |
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.
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);
packages/firestore/rollup.config.js
Outdated
json(), | ||
terser(terserOptions) | ||
], | ||
external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)) |
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.
use resolveMemoryExterns
instead ?
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.
Done
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.
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 thanfirestore.enablePersistence(implementation, settings)
. If we roll this out via a breaking change, would it make more sense to use thefirebase@next
API directly?
packages/firestore/rollup.config.js
Outdated
json(), | ||
terser(terserOptions) | ||
], | ||
external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)) |
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.
Done
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.
SG. IIUC, I think it makes sense to make it part of |
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.
Updated the PR to use composition. It probably makes more sense to compare against master than against the previous revision.
integration/firestore/package.json
Outdated
"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", |
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.
Fixed. Note that the main entry points should be test
and related rules.
integration/firestore/package.json
Outdated
"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", |
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 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;", |
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.
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'; |
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 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", |
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 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> { |
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.
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( |
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 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 { |
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 is no longer applicable.
* | ||
* @returns A Promise that resolves with the various persistence components. | ||
*/ | ||
export const newIndexedDbPersistence: PersistenceFactory<IndexedDbPersistenceSettings> = async ( |
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.
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( |
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 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.
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.
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;", |
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.
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'; |
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.
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.
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 added a short comment that explains this a little bit.
}); | ||
return deferred.promise; | ||
async clearPersistence(): Promise<void> { | ||
if (!this._persistenceProvider.isDurable) { |
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.
Is it possible to fold the throwing of these errors into the memory-only persistence provider?
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.
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 |
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.
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.
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 like what you have proposed and shamelessly updated the PR.
* True if the underlying implementation supports durable storage via | ||
* `enablePersistence()`. | ||
*/ | ||
readonly isDurable: boolean; |
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.
Couldn't we have configure
throw if the settings
include enabling persistence but the provider doesn't actually support it?
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.
That's how I ended up implementing the new error semantics for the MemoryPersistenceProvider.
persistenceResult: Deferred<void> | ||
): Promise<void> { | ||
try { | ||
await persistenceProvider.configure( |
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.
Since persistence provider's configure
method is called during initializePersistence
, consider calling this method initialize
.
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.
Done
4b22fbb
to
397bddc
Compare
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.
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'; |
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 added a short comment that explains this a little bit.
@@ -0,0 +1,6 @@ | |||
{ | |||
"name": "firebase/firestore/memory", |
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.
Descriptions added to this package.json and top-level package.json.
}); | ||
return deferred.promise; | ||
async clearPersistence(): Promise<void> { | ||
if (!this._persistenceProvider.isDurable) { |
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.
Yes, that's a good clean up. I moved them to the MemoryPersistenceProvider.
persistenceResult: Deferred<void> | ||
): Promise<void> { | ||
try { | ||
await persistenceProvider.configure( |
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.
Done
* True if the underlying implementation supports durable storage via | ||
* `enablePersistence()`. | ||
*/ | ||
readonly isDurable: boolean; |
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.
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 |
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 like what you have proposed and shamelessly updated the PR.
9949ce7
to
cbfa9a1
Compare
cbfa9a1
to
53a4d12
Compare
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.
LGTM
packages/firestore/rollup.config.js
Outdated
// size. | ||
// | ||
// See https://g3doc.corp.google.com/firebase/jscore/g3doc/contributing/builds.md |
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.
FYI, you can shorten this to http://g3doc/firebase/jscore/g3doc/contributing/builds.md
if you wanted.
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.
Done
#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.
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()
andclearPersistence(
).The new memory-only Firestore classes are exported as:
This PR also changes the integration tests to run against both versions of Firestore