Skip to content

Implement bundle features in local store. #3200

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 27 commits into from
Jun 25, 2020

Conversation

wu-hui
Copy link
Contributor

@wu-hui wu-hui commented Jun 10, 2020

No description provided.

@wu-hui wu-hui changed the base branch from wuandy/Bundles to wuandy/BundlePersistence June 10, 2020 14:41
wu-hui added 2 commits June 13, 2020 22:48
# Conflicts:
#	packages/firestore/src/core/bundle.ts
#	packages/firestore/src/local/bundle_cache.ts
#	packages/firestore/src/local/indexeddb_bundle_cache.ts
#	packages/firestore/src/local/local_serializer.ts
#	packages/firestore/src/local/memory_bundle_cache.ts
#	packages/firestore/src/platform/platform.ts
#	packages/firestore/src/platform_browser/browser_platform.ts
#	packages/firestore/src/platform_node/node_platform.ts
#	packages/firestore/src/util/bundle_reader.ts
#	packages/firestore/test/unit/local/bundle_cache.test.ts
#	packages/firestore/test/unit/platform/platform.test.ts
#	packages/firestore/test/unit/specs/spec_test_components.ts
#	packages/firestore/test/unit/util/bundle.test.ts
#	packages/firestore/test/util/test_platform.ts
@wu-hui wu-hui changed the base branch from wuandy/BundlePersistence to wuandy/Bundles June 14, 2020 03:21
@wu-hui wu-hui requested a review from schmidt-sebastian June 15, 2020 15:31
Copy link
Contributor

@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.

Sorry about the slow review. In general, this looks fine and most of my comments are on style/naming.

* Represents the [metadata, document] pairs from the bundles in an array.
*/
export type BundledDocuments = Array<
[bundleProto.BundledDocumentMetadata, api.Document | undefined]
Copy link
Contributor

Choose a reason for hiding this comment

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

I would prefer if you a nested object here: Array<{metadata: ..., document?: }>. Index-based typing is very hard to parse for readers of the code.

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.

@@ -541,65 +548,31 @@ export class LocalStore {
}
});

const documentBuffer = this.remoteDocuments.newChangeBuffer({
Copy link
Contributor

Choose a reason for hiding this comment

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

it is much more efficient to keep a global change buffer, as a document that is part of multiple targets will only be written once per RemoteEvent rather than once per target. We should keep this invariant.

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, thanks for the catch.

* Returns a promise of a boolean to indicate if the given bundle has already
* been loaded and the create time is newer than the current loading bundle.
*/
isNewerBundleLoaded(
Copy link
Contributor

Choose a reason for hiding this comment

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

hasNewerBundle?

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.

/**
* Represents a document change to be applied to remote document cache.
*/
class RemoteDocumentChange {
Copy link
Contributor

Choose a reason for hiding this comment

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

This could be a simple interface, which would remove any size overhead since interfaces "disappear" when transpiled to JS. The only downside (in terms of code size) is that you will not be able to use this constructor anymore, but instead have to assign object properties directly.

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.

expected: boolean
): LocalStoreTester {
this.promiseChain = this.promiseChain.then(() => {
return this.localStore.isNewerBundleLoaded(metadata).then(actual => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Name is also inconsistent here. FWIW, toHaveNewerBundle matches my name suggestion from above.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Agree!

.toContain(doc('foo/bar1', 1, { val: 'to-delete' }))
.after(
bundledDocuments([
doc('foo/bar', 1, { sum: 1336 }),
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this be version 2?

We may also want to skip bundle document loading for documents that we already have cached at that version. The local documents seem safer than replacing them with a bundled document that could be from an arbitrary source.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not there were no foo/bar when loading the bundle. I changed the name to make it less confusing.

Also added a test to check when the existing version is the same.


it('handles PatchMutation with Transform -> BundledDocuments', () => {
// Note: This test reflects the current behavior, but it may be preferable
// to replay the mutation once we receive the first value from the backend.
Copy link
Contributor

Choose a reason for hiding this comment

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

I think what is implemented is the better behavior, since the document will likely end up looking like the local mutation. What do you think?

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 copied from the remote event tests. I am actually not sure why it was there and is it still valid.

I think we can discuss this more with the team in one of the standups.

@@ -401,6 +404,55 @@ export function docUpdateRemoteEvent(
return aggregator.createRemoteEvent(doc.version);
}

export class TestBundledDocuments {
Copy link
Contributor

Choose a reason for hiding this comment

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

Can this be inlined? bundledDocuments would just return BundledDocuments.

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 this because it hard to add code to after(op) method otherwise, suggestions welcome.

Copy link
Contributor Author

@wu-hui wu-hui left a comment

Choose a reason for hiding this comment

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

Thanks! Given the size of my recent PRs, I think you are already very quick in reviewing them!

* Represents the [metadata, document] pairs from the bundles in an array.
*/
export type BundledDocuments = Array<
[bundleProto.BundledDocumentMetadata, api.Document | undefined]
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.

@@ -209,10 +212,12 @@ export class IndexedDbComponentProvider extends MemoryComponentProvider {
}

createLocalStore(cfg: ComponentConfiguration): LocalStore {
const serializer = cfg.platform.newSerializer(cfg.databaseInfo.databaseId);
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.

return new MultiTabLocalStore(
this.persistence,
new IndexFreeQueryEngine(),
cfg.initialUser
cfg.initialUser,
new BundleConverter(serializer)
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.

'Cannot add a document with a read time of zero'
);
const doc = this.documentCache.serializer.toDbRemoteDocument(
maybeDocument,
this.readTime
documentChange.maybeDoc!,
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.


this.promiseChain = this.promiseChain
.then(() => {
return this.localStore.applyBundledDocuments(documents);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

You were right!

expected: boolean
): LocalStoreTester {
this.promiseChain = this.promiseChain.then(() => {
return this.localStore.isNewerBundleLoaded(metadata).then(actual => {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Agree!

.toContain(doc('foo/bar1', 1, { val: 'to-delete' }))
.after(
bundledDocuments([
doc('foo/bar', 1, { sum: 1336 }),
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not there were no foo/bar when loading the bundle. I changed the name to make it less confusing.

Also added a test to check when the existing version is the same.


it('handles PatchMutation with Transform -> BundledDocuments', () => {
// Note: This test reflects the current behavior, but it may be preferable
// to replay the mutation once we receive the first value from the backend.
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 copied from the remote event tests. I am actually not sure why it was there and is it still valid.

I think we can discuss this more with the team in one of the standups.

@wu-hui wu-hui assigned schmidt-sebastian and unassigned wu-hui Jun 17, 2020
Copy link
Contributor

@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.

Some feedback that may trigger some pushback :)

Comment on lines 62 to 65
export type BundledDocuments = Array<{
metadata: bundleProto.BundledDocumentMetadata;
document: api.Document | undefined;
}>;
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
export type BundledDocuments = Array<{
metadata: bundleProto.BundledDocumentMetadata;
document: api.Document | undefined;
}>;
export type BundledDocuments = Array<BundledDocument>;

It might be cleaner just to inline this type.

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, I forgot to do it earlier.

* LocalDocuments are re-calculated if there are remaining mutations in the
* queue.
*/
applyBundleDocuments(documents: BundledDocuments): Promise<MaybeDocumentMap> {
Copy link
Contributor

Choose a reason for hiding this comment

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

To make today's code review meaningfully different from Tuesday's review, I think maybe we should try to make all bundle code tree-shakeable? I threw together some pseudocode here: https://gist.github.com/schmidt-sebastian/932ffd496e1129309b9d6b4861d2c879. The benefits of this design is that it would allows to make all bundling code optional (once we do the same thing for the persistence implementation).

It might make sense to do this as a follow-up since this PR is basically ready to be submitted.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Happy to make this tree-shakeable, not sure I did everything right though.

I'll update my other PRs to make it tree-shakeable as well.

* @param remoteVersion The read time of the documents to be applied, it is
* a `SnapshotVersion` if all documents have the same read time, or a `DocumentVersionMap` if
* they have different read times.
*/
Copy link
Contributor

Choose a reason for hiding this comment

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

Do you mind adding a short description for all documents?

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.

documentBuffer: RemoteDocumentChangeBuffer,
documents: MaybeDocumentMap,
globalVersion: SnapshotVersion | undefined,
documentVersions: DocumentVersionMap | undefined
Copy link
Contributor

Choose a reason for hiding this comment

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

Looking at this more, would it be possible to use the readTime from the bundle as the globalVersion and drop documentVersions? AFAIK the only real guarantee that we have to make is that a changed document has a changed read time that is higher than the new document version.

If that is not possible, I think we should explore adding the read time to MaybeDocument (it's already part of DbRemoteDocument, so we do have the data). That might be a relatively simple way to improve the code flow here.

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 think you are right that readTime is used only to fetch newly changed docs, which means setting a higher than actual readTime for documents is fine: the SDK just need to process more documents than required.

The performance gain here might be beneficial though, given that user could load a big bundle there and a more accurate readTime will likely have a sizeable reduction to the query result.

One other reason to keep document based read time is I don't want us to design future features having to account for the fact that readTime can be higher than actual. I cannot think of a case now, so this is mostly design for the future (that might never happen).

MaybeDocument is constructed mostly from remote (for example: Serializer.fromDocument does not have a read time there) so I am not sure if we can put readTime there.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think it would be possible to add a readTime to MaybeDocument. It looks like we would need to change a lot of our plumbing, so I am ok with leaving as is. We could add a TODO if this is something that we think would clean up our code in general.

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.

bundleMetadata.createTime!
);
return this.persistence
.runTransaction('isNewerBundleLoaded', 'readonly', transaction => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
.runTransaction('isNewerBundleLoaded', 'readonly', transaction => {
.runTransaction('hasNewerBundle', 'readonly', transaction => {

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.

// The document in this change, null if it is a removal from the cache.
readonly maybeDocument: MaybeDocument | null;
// The timestamp when this change is read.
readonly readTime?: SnapshotVersion;
Copy link
Contributor

Choose a reason for hiding this comment

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

The use of "null" and "undefined" for "not set" is a bit inconsistent here.

FWIW, we could drop readTime if we add it to MaybeDocument.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated to be more consistent.

Comment on lines 173 to 175
.then(() => {
return this.localStore.applyBundleDocuments(documents);
})
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
.then(() => {
return this.localStore.applyBundleDocuments(documents);
})
.then(() => this.localStore.applyBundleDocuments(documents))

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.

Comment on lines 429 to 431
this.promiseChain = this.promiseChain.then(() => {
return this.localStore.saveBundle(metadata);
});
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
this.promiseChain = this.promiseChain.then(() => {
return this.localStore.saveBundle(metadata);
});
this.promiseChain = this.promiseChain.then(() => this.localStore.saveBundle(metadata));

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.

Comment on lines 414 to 436
const result: BundledDocuments = [];
for (const d of documents) {
if (d instanceof NoDocument) {
result.push({
metadata: {
name: JSON_SERIALIZER.toName(d.key),
readTime: JSON_SERIALIZER.toVersion(d.version),
exists: false
},
document: undefined
});
} else if (d instanceof Document) {
result.push({
metadata: {
name: JSON_SERIALIZER.toName(d.key),
readTime: JSON_SERIALIZER.toVersion(d.version),
exists: true
},
document: JSON_SERIALIZER.toDocument(d)
});
}
}

Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe:

Suggested change
const result: BundledDocuments = [];
for (const d of documents) {
if (d instanceof NoDocument) {
result.push({
metadata: {
name: JSON_SERIALIZER.toName(d.key),
readTime: JSON_SERIALIZER.toVersion(d.version),
exists: false
},
document: undefined
});
} else if (d instanceof Document) {
result.push({
metadata: {
name: JSON_SERIALIZER.toName(d.key),
readTime: JSON_SERIALIZER.toVersion(d.version),
exists: true
},
document: JSON_SERIALIZER.toDocument(d)
});
}
}
const result = documents.map(d => {
return {
metadata: {
name: JSON_SERIALIZER.toName(d.key),
readTime: JSON_SERIALIZER.toVersion(d.version),
exists: d instanceof Document
},
document: d instanceof Document ? JSON_SERIALIZER.toDocument(d) : undefined
});
});

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, thanks!

@wu-hui wu-hui assigned schmidt-sebastian and unassigned wu-hui Jun 19, 2020
@@ -89,6 +95,40 @@ export interface QueryResult {
readonly remoteKeys: DocumentKeySet;
}

/**
* Base of local store implementations. This is mainly used to define fields
Copy link
Contributor

Choose a reason for hiding this comment

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

s/local store/

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.

* that are used by tree-shakeable free functions that implement optional
* local store features.
*/
class LocalStoreBase {
Copy link
Contributor

Choose a reason for hiding this comment

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

So the idea behind the pattern that is used in Datastore/DatastoreImpl (and just as of today, also in Target/TargetImpl) is to have a main class (in this case LocalStore) that doesn't change its public API.

So you would keep LocalStore as is (and continue to export it), and then you would create LocalStoreImpl that makes fields public that you only need to be public in this module. Then you would add a newLocalStore function that creates a LocalStoreImpl, but is typed to return a LocalStore. You then end up with a module that has a bunch of public members that are not leaked externally (in our code base).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sorry I misunderstood earlier.

As discussed offline, we will need to make LocalStore and SyncEngine interfaces to achieve this because of existing MultiTab* implementations, which is best done from master.

* Note: this function will use `documentVersions` if it is defined;
* when it is not defined, resorts to `globalVersion`.
*/
prepareApplyingDocuments(
Copy link
Contributor

Choose a reason for hiding this comment

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

s/prepareApplyingDocuments/prepareToApplyDocuments/ (or back to applyDocuments)

Or rather: populateDocumentChangeBuffer?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

populateDocumentChangeBuffer sounds good.

@@ -642,6 +628,84 @@ export class LocalStore {
});
}

/**
* Prepares document change buffer resulted from documents to be applied,
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: This line needs a rewrite.

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.

txn: PersistenceTransaction,
documentBuffer: RemoteDocumentChangeBuffer,
documents: MaybeDocumentMap,
globalVersion: SnapshotVersion | undefined,
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we make this non-optional and use SnapshotVersion.min() for the bundle loader?

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.

Comment on lines 660 to 666
let docReadVersion: SnapshotVersion | null = null;
if (documentVersions) {
docReadVersion = documentVersions.get(key);
} else if (globalVersion) {
docReadVersion = globalVersion;
}
debugAssert(!!docReadVersion, 'Document read version must exist');
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
let docReadVersion: SnapshotVersion | null = null;
if (documentVersions) {
docReadVersion = documentVersions.get(key);
} else if (globalVersion) {
docReadVersion = globalVersion;
}
debugAssert(!!docReadVersion, 'Document read version must exist');
const readTime = documentVersions?.get(key) ?? globalVersion

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, thanks!

@@ -1049,11 +1114,13 @@ export class MultiTabLocalStore extends LocalStore {
}

setNetworkEnabled(networkEnabled: boolean): void {
this.persistence.setNetworkEnabled(networkEnabled);
(this.persistence as IndexedDbPersistence).setNetworkEnabled(
Copy link
Contributor

Choose a reason for hiding this comment

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

If you add a persistence: IndexedDbPersistence field and assign it manually (similar to mutationQueue), this cast will go away.

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.

localStore: LocalStore,
documents: BundledDocuments
): Promise<MaybeDocumentMap> {
const bundleConverter = new BundleConverter(localStore.serializer);
Copy link
Contributor

Choose a reason for hiding this comment

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

If you follow the Datastore pattern, you would start this off with:

const localStoreImpl = debugCast(localStore, LocalStoreImpl);

Otherwise, this functions are tree-shakeable as is. Thanks!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Will do once we have addressed inheritance issue in master.

@wu-hui wu-hui assigned schmidt-sebastian and unassigned wu-hui Jun 23, 2020
# Conflicts:
#	packages/firestore/src/core/component_provider.ts
#	packages/firestore/src/local/indexeddb_remote_document_cache.ts
#	packages/firestore/src/local/local_store.ts
#	packages/firestore/src/local/remote_document_change_buffer.ts
#	packages/firestore/test/unit/local/local_store.test.ts

/**
* Represents a bundled document, including the metadata and the document
* itself, if exists.
Copy link
Contributor

Choose a reason for hiding this comment

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

missing "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.

Done.

}

/**
* Converts a [metadata, document] pair to a MaybeDocument.
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
* Converts a [metadata, document] pair to a MaybeDocument.
* Converts a BundledDocument to a MaybeDocument.

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.

@@ -347,6 +357,7 @@ class LocalStoreImpl implements LocalStore {
this.mutationQueue,
this.persistence.getIndexManager()
);
this.bundleCache = persistence.getBundleCache();
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we have done enough iterations on this PR, but ideally this would not be a field on LocalStore (as it breaks tree-shaking). Could BundleCache take LocalStore as an argument? Please explore in a follow up.

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 see. Will do a followup PR.

let changedDocs = maybeDocumentMap();
documents.forEach((key, doc) => {
const existingDoc = existingDocs.get(key);
const docReadTime = documentVersions?.get(key) ?? globalVersion;
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: I probably suggested this, but this is functionally equivalent to documentVersions?.get(key) || globalVersion, which generates a lot less code. ?? is only needed if you only want to check for undefined or null and explicitly want to not fallback on "" (empty string), 0 or false.

if (doc?.version || 'foo')

=> 

if ((doc === null || doc === void 0 ? void 0 : doc.version) || 'foo')
 

if (doc?.version ?? 'foo')

=>

if ((_a = doc === null || doc === void 0 ? void 0 : doc.version) !== null && _a !== void 0 ? _a : 'foo')

As an example for where ?? works but || doesn't, consider:

class Settings {
   enableAmazingFeature?: boolean;
}

 // This is now always true
if (settings.enableAmazingFeature || true) {}
// This is true iff enableAmazingFeature=undefined or enableAmazingFeature=true
if (settings.enableAmazingFeature ?? true) {}

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 see, applied. Thanks!

@wu-hui wu-hui merged commit e8b9ec2 into wuandy/Bundles Jun 25, 2020
@firebase firebase locked and limited conversation to collaborators Jul 26, 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.

2 participants