-
Notifications
You must be signed in to change notification settings - Fork 938
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
Conversation
# Conflicts: # packages/firestore/src/util/bundle_reader.ts # packages/firestore/test/unit/util/bundle.test.ts
# Conflicts: # packages/firestore/src/util/bundle_reader.ts # packages/firestore/test/unit/util/bundle.test.ts
Fix change buffer bug
# 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
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.
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] |
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 would prefer if you a nested object here: Array<{metadata: ..., document?: }>
. Index-based typing is very hard to parse for readers of the code.
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.
@@ -541,65 +548,31 @@ export class LocalStore { | |||
} | |||
}); | |||
|
|||
const documentBuffer = this.remoteDocuments.newChangeBuffer({ |
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 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.
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, 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( |
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.
hasNewerBundle?
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.
/** | ||
* Represents a document change to be applied to remote document cache. | ||
*/ | ||
class RemoteDocumentChange { |
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 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.
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.
expected: boolean | ||
): LocalStoreTester { | ||
this.promiseChain = this.promiseChain.then(() => { | ||
return this.localStore.isNewerBundleLoaded(metadata).then(actual => { |
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.
Name is also inconsistent here. FWIW, toHaveNewerBundle
matches my name suggestion from above.
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.
Agree!
.toContain(doc('foo/bar1', 1, { val: 'to-delete' })) | ||
.after( | ||
bundledDocuments([ | ||
doc('foo/bar', 1, { sum: 1336 }), |
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.
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.
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.
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. |
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 think what is implemented is the better behavior, since the document will likely end up looking like the local mutation. What do you think?
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 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 { |
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.
Can this be inlined? bundledDocuments
would just return BundledDocuments
.
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 this because it hard to add code to after(op)
method otherwise, suggestions welcome.
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! 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] |
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.
@@ -209,10 +212,12 @@ export class IndexedDbComponentProvider extends MemoryComponentProvider { | |||
} | |||
|
|||
createLocalStore(cfg: ComponentConfiguration): LocalStore { | |||
const serializer = cfg.platform.newSerializer(cfg.databaseInfo.databaseId); |
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.
return new MultiTabLocalStore( | ||
this.persistence, | ||
new IndexFreeQueryEngine(), | ||
cfg.initialUser | ||
cfg.initialUser, | ||
new BundleConverter(serializer) |
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.
'Cannot add a document with a read time of zero' | ||
); | ||
const doc = this.documentCache.serializer.toDbRemoteDocument( | ||
maybeDocument, | ||
this.readTime | ||
documentChange.maybeDoc!, |
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.
|
||
this.promiseChain = this.promiseChain | ||
.then(() => { | ||
return this.localStore.applyBundledDocuments(documents); |
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.
You were right!
expected: boolean | ||
): LocalStoreTester { | ||
this.promiseChain = this.promiseChain.then(() => { | ||
return this.localStore.isNewerBundleLoaded(metadata).then(actual => { |
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.
Agree!
.toContain(doc('foo/bar1', 1, { val: 'to-delete' })) | ||
.after( | ||
bundledDocuments([ | ||
doc('foo/bar', 1, { sum: 1336 }), |
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.
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. |
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 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.
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.
Some feedback that may trigger some pushback :)
export type BundledDocuments = Array<{ | ||
metadata: bundleProto.BundledDocumentMetadata; | ||
document: api.Document | undefined; | ||
}>; |
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.
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.
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, I forgot to do it earlier.
* LocalDocuments are re-calculated if there are remaining mutations in the | ||
* queue. | ||
*/ | ||
applyBundleDocuments(documents: BundledDocuments): Promise<MaybeDocumentMap> { |
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.
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.
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.
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. | ||
*/ |
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 mind adding a short description for all documents?
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.
documentBuffer: RemoteDocumentChangeBuffer, | ||
documents: MaybeDocumentMap, | ||
globalVersion: SnapshotVersion | undefined, | ||
documentVersions: DocumentVersionMap | undefined |
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.
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.
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 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.
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 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.
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.
bundleMetadata.createTime! | ||
); | ||
return this.persistence | ||
.runTransaction('isNewerBundleLoaded', 'readonly', transaction => { |
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.
.runTransaction('isNewerBundleLoaded', 'readonly', transaction => { | |
.runTransaction('hasNewerBundle', 'readonly', transaction => { |
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.
// 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; |
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.
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.
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 to be more consistent.
.then(() => { | ||
return this.localStore.applyBundleDocuments(documents); | ||
}) |
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.
.then(() => { | |
return this.localStore.applyBundleDocuments(documents); | |
}) | |
.then(() => this.localStore.applyBundleDocuments(documents)) |
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.
this.promiseChain = this.promiseChain.then(() => { | ||
return this.localStore.saveBundle(metadata); | ||
}); |
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.promiseChain = this.promiseChain.then(() => { | |
return this.localStore.saveBundle(metadata); | |
}); | |
this.promiseChain = this.promiseChain.then(() => this.localStore.saveBundle(metadata)); |
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.
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) | ||
}); | ||
} | ||
} | ||
|
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.
Maybe:
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 | |
}); | |
}); | |
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, thanks!
@@ -89,6 +95,40 @@ export interface QueryResult { | |||
readonly remoteKeys: DocumentKeySet; | |||
} | |||
|
|||
/** | |||
* Base of local store implementations. This is mainly used to define fields |
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.
s/local store/
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.
* that are used by tree-shakeable free functions that implement optional | ||
* local store features. | ||
*/ | ||
class LocalStoreBase { |
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.
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).
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.
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( |
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.
s/prepareApplyingDocuments/prepareToApplyDocuments/ (or back to applyDocuments
)
Or rather: populateDocumentChangeBuffer
?
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.
populateDocumentChangeBuffer
sounds good.
@@ -642,6 +628,84 @@ export class LocalStore { | |||
}); | |||
} | |||
|
|||
/** | |||
* Prepares document change buffer resulted from documents to be applied, |
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.
Nit: This line needs a rewrite.
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.
txn: PersistenceTransaction, | ||
documentBuffer: RemoteDocumentChangeBuffer, | ||
documents: MaybeDocumentMap, | ||
globalVersion: SnapshotVersion | undefined, |
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.
Can we make this non-optional and use SnapshotVersion.min() for the bundle loader?
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.
let docReadVersion: SnapshotVersion | null = null; | ||
if (documentVersions) { | ||
docReadVersion = documentVersions.get(key); | ||
} else if (globalVersion) { | ||
docReadVersion = globalVersion; | ||
} | ||
debugAssert(!!docReadVersion, 'Document read version must exist'); |
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.
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 |
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, thanks!
@@ -1049,11 +1114,13 @@ export class MultiTabLocalStore extends LocalStore { | |||
} | |||
|
|||
setNetworkEnabled(networkEnabled: boolean): void { | |||
this.persistence.setNetworkEnabled(networkEnabled); | |||
(this.persistence as IndexedDbPersistence).setNetworkEnabled( |
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.
If you add a persistence: IndexedDbPersistence
field and assign it manually (similar to mutationQueue), this cast will go away.
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.
localStore: LocalStore, | ||
documents: BundledDocuments | ||
): Promise<MaybeDocumentMap> { | ||
const bundleConverter = new BundleConverter(localStore.serializer); |
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.
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!
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.
Will do once we have addressed inheritance issue in master
.
# 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. |
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.
missing "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.
Done.
} | ||
|
||
/** | ||
* Converts a [metadata, document] pair to a MaybeDocument. |
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.
* Converts a [metadata, document] pair to a MaybeDocument. | |
* Converts a BundledDocument to a MaybeDocument. |
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.
@@ -347,6 +357,7 @@ class LocalStoreImpl implements LocalStore { | |||
this.mutationQueue, | |||
this.persistence.getIndexManager() | |||
); | |||
this.bundleCache = persistence.getBundleCache(); |
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 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.
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 see. Will do a followup PR.
let changedDocs = maybeDocumentMap(); | ||
documents.forEach((key, doc) => { | ||
const existingDoc = existingDocs.get(key); | ||
const docReadTime = documentVersions?.get(key) ?? globalVersion; |
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.
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) {}
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 see, applied. Thanks!
No description provided.