-
Notifications
You must be signed in to change notification settings - Fork 938
Implement bundle loading. #3201
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
Two TODO: 1. load from secondary tab. 2. don't fail async queue.
d6fafba
to
adf1504
Compare
# Conflicts: # integration/messaging/test/utils/openNewTab.js # packages/firestore/src/core/bundle.ts # packages/firestore/src/core/component_provider.ts # packages/firestore/src/core/firestore_client.ts # packages/firestore/src/core/sync_engine.ts # packages/firestore/src/local/bundle_cache.ts # packages/firestore/src/local/indexeddb_bundle_cache.ts # packages/firestore/src/local/indexeddb_persistence.ts # packages/firestore/src/local/indexeddb_remote_document_cache.ts # packages/firestore/src/local/local_serializer.ts # packages/firestore/src/local/local_store.ts # packages/firestore/src/local/memory_bundle_cache.ts # packages/firestore/src/local/memory_remote_document_cache.ts # packages/firestore/src/local/remote_document_change_buffer.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/remote/serializer.ts # packages/firestore/src/util/bundle_reader.ts # packages/firestore/test/unit/local/bundle_cache.test.ts # packages/firestore/test/unit/local/local_store.test.ts # packages/firestore/test/unit/local/persistence_test_helpers.ts # packages/firestore/test/unit/local/test_bundle_cache.ts # packages/firestore/test/unit/specs/spec_test_components.ts # packages/firestore/test/unit/specs/spec_test_runner.ts # packages/firestore/test/unit/util/bundle.test.ts # packages/firestore/test/util/helpers.ts # packages/firestore/test/util/test_platform.ts
CLAs look good, thanks! ℹ️ Googlers: Go here for more info. |
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 this!
@@ -98,3 +108,247 @@ export class BundleConverter { | |||
return fromVersion(time); | |||
} | |||
} | |||
|
|||
/** | |||
* Returns a `LoadBundleTaskProgress` representing the first progress 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.
* Returns a `LoadBundleTaskProgress` representing the first progress of | |
* Returns a `LoadBundleTaskProgress`, representing the initial progress 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.
Done.
} | ||
|
||
/* eslint-disable @typescript-eslint/no-explicit-any */ | ||
export class LoadBundleTaskImpl implements firestore.LoadBundleTask { |
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.
Couple things:
This could probably just be a LoadBundleTask
? Our other API classes don't use the "Impl" prefix.
This should also be move to src/api/
All members that are not part of the public API should be prefixed with an underscore.
If possible, all instances of any
should be replaced with unknown
.
progress?: firestore.LoadBundleTaskProgress | ||
) => any; | ||
|
||
private promiseResolver = new Deferred<any>(); |
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 a very generic name. Is this the "completionResolver"?
*/ | ||
completeWith(progress: firestore.LoadBundleTaskProgress): void { | ||
let result; | ||
if (this.progressComplete) { |
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 sounds like a boolean state check, but it is far from it. We probably should rename the members that contain the user functions.
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.
onFulfilled?: (a: firestore.LoadBundleTaskProgress) => any, | ||
onRejected?: (a: Error) => any | ||
): Promise<any> { | ||
this.promiseFulfilled = onFulfilled; |
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 we need these members? Could we just call return this.promiseResolver.promise.then(onFullfilled, onRejected)
?
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, obviously I cannot wrap my mind around promises.
}) | ||
// Loading both bundles will not raise snapshots, because of the | ||
// mutation is not acknowledged. | ||
.loadBundle(bundleString1) |
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 bundle is ignored (as tested above), so we probably don't need to test it here. We could also add a small separate test that just verifies this code path.
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.
Deleted it.
added: [docA] | ||
}) | ||
.client(1) | ||
// Bundle tells otherwise, leads to limbo resolution. |
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.
Please be more descriptive. What is the conflicting message in the bundle?
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.
My bad, this is copied from above test, and should be deleted.
const task = new LoadBundleTaskImpl(); | ||
this.queue.enqueueAndForget(() => { | ||
return loadBundle(this.syncEngine, reader, task); | ||
}); | ||
|
||
await task; |
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.
const task = new LoadBundleTaskImpl(); | |
this.queue.enqueueAndForget(() => { | |
return loadBundle(this.syncEngine, reader, task); | |
}); | |
await task; | |
const task = new LoadBundleTaskImpl(); | |
return this.queue.enqueue(() => loadBundle(this.syncEngine, reader, task)); |
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.
syncEngine: SyncEngine, | ||
bundleReader: BundleReader, | ||
task: LoadBundleTaskImpl | ||
): 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.
Did you consider returning the LoadBundleTask instead of having it passed in? You are now mixing two async patterns in one callsite.
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 agree it's awkward, the need for passing task as an argument comes from how firestore client needs to return the task itself, and loadBundle
has to be called on async queue, i am not sure if returning a function from async queue would work there.
The need to return a promise here is for async queue to wait for loading to complete.
Do you have a more concrete sketch?
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.
How about something like this: https://gist.github.com/schmidt-sebastian/22566fb61bfef994825bd24b45bcda02
Note that the diff is mostly whitespace, the change is actually pretty small. Basically, instead of returning the Promise here you can await the Task in the callsite.
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 acknowledged mutation. | ||
.loadBundle(bundleString2) | ||
.expectEvents(query, { | ||
modified: [doc('collection/a', 1001, { key: 'fromBundle' })] |
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.
Ideally, this should also be fromCache
.
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.
Binary Size ReportAffected SDKs
Test Logs |
syncEngine: SyncEngine, | ||
bundleReader: BundleReader, | ||
task: LoadBundleTaskImpl | ||
): 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.
How about something like this: https://gist.github.com/schmidt-sebastian/22566fb61bfef994825bd24b45bcda02
Note that the diff is mostly whitespace, the change is actually pretty small. Basically, instead of returning the Promise here you can await the Task in the callsite.
packages/firestore-types/index.d.ts
Outdated
next?: (progress: LoadBundleTaskProgress) => any, | ||
error?: (error: Error) => any, | ||
complete?: () => void | ||
): 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.
Shouldn't this be 'void'? What does it mean for the onProgess callback to return a Promise?
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.
packages/firestore-types/index.d.ts
Outdated
then( | ||
onFulfilled?: (a: LoadBundleTaskProgress) => any, | ||
onRejected?: (a: Error) => any | ||
): Promise<any>; |
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( | |
onFulfilled?: (a: LoadBundleTaskProgress) => any, | |
onRejected?: (a: Error) => any | |
): Promise<any>; | |
then<T,R>( | |
onFulfilled?: (a: LoadBundleTaskProgress) => T|PromiseLike<T>, | |
onRejected?: (a: Error) => R|PromiseLike<R> | |
): Promise<T|R>; |
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 ES5 definition for these types is:
interface Promise<T> {
then<TResult1 = T, TResult2 = never>(onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | undefined | null, onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | undefined | null): Promise<TResult1 | TResult2>;
catch<TResult = never>(onrejected?: ((reason: any) => TResult | PromiseLike<TResult>) | undefined | null): Promise<T | TResult>;
}
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.
Nice, thanks!
packages/firestore-types/index.d.ts
Outdated
onRejected?: (a: Error) => any | ||
): Promise<any>; | ||
|
||
catch(onRejected: (a: Error) => any): Promise<any>; |
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.
catch(onRejected: (a: Error) => any): Promise<any>; | |
catch(onRejected: (a: Error) => R|PromiseLike<R>): Promise<R>; |
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. Although it seems it should return a Promise<R|Progress>
in this case.
packages/firestore/src/api/bundle.ts
Outdated
this._userProgressHandler = next; | ||
this._userProgressErrorHandler = error; | ||
this._userProgressCompleteHandler = complete; | ||
return this._progressResolver.promise; |
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 don't need to return this promise 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.
Done.
readTime: TestSnapshotVersion; | ||
createTime?: TestSnapshotVersion; | ||
updateTime?: TestSnapshotVersion; | ||
content?: api.ApiClientObjectMap<Value>; |
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 existing spec tests use the "user facing types" rather than the Value proto to encode the types. This means that we would store { foo: 1 }
rather than { foo: { integerValue: 1 }}
. I think that you will find that following this pattern will greatly simplify replaying these tests on Android and iOS.
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.
.expectEvents(query1, { modified: [docAChanged], fromCache: true }); | ||
}); | ||
|
||
specTest('Newer deleted docs from bundles should delete cache', [], () => { |
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.
specTest('Newer deleted docs from bundles should delete cache', [], () => { | |
specTest('Newer deleted docs from bundles should delete cached docs', [], () => { |
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.
}); | ||
|
||
specTest( | ||
'Newer docs from bundles should raise snapshot only when watch catches up with acknowledged writes', |
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.
'Newer docs from bundles should raise snapshot only when watch catches up with acknowledged writes', | |
'Newer docs from bundles should raise snapshot only when Watch catches up with acknowledged writes', |
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 ( | ||
spec() | ||
.withGCEnabled(false) |
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 you file a bug to define the behavior bundles when withGCEnabled
is not set to false? Note that this flag is spec test only and that this not actually a mode that users can enable with memory persistence.
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.
added: [doc('collection/a', 500, { key: 'b' })], | ||
fromCache: true | ||
}) | ||
.expectLimboDocs(docA.key) |
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 you expand this test: We should verify that when the Limbo doc is resolved we go back to fromCache: false
.
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.
Next round. This is 98% done!
packages/firestore/src/api/bundle.ts
Outdated
implements | ||
firestore.LoadBundleTask, | ||
PromiseLike<firestore.LoadBundleTaskProgress> { | ||
private _progressObserver?: PartialObserver<firestore.LoadBundleTaskProgress>; |
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.
Consider initializing this to an empty observer: {}
.
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.
Done.
|
||
if (this._progressObserver && this._progressObserver.next) { | ||
this._progressObserver.next(this._lastProgress); | ||
} |
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.
Please try to match this behavior:
firebase-js-sdk/packages/storage/src/task.ts
Line 626 in 0131e1f
private notifyObserver_(observer: Observer<UploadTaskSnapshot>): void { |
Basically, next
doesn't get called for the Complete
of Error
state.
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.
As discussed, we would keep current behaviour because these progress updates can be considered part of the progress update series.
* `LoadBundleTaskProgress` object. | ||
*/ | ||
_completeWith(progress: firestore.LoadBundleTaskProgress): void { | ||
this._updateProgress(progress); |
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 we not need to set the taskState here (like you did below)?
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 call sites set this up. I added an assert instead.
packages/firestore/src/api/bundle.ts
Outdated
*/ | ||
_updateProgress(progress: firestore.LoadBundleTaskProgress): void { | ||
debugAssert( | ||
this._lastProgress.taskState !== 'Error', |
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 may want to add 'complete' to this check as well.
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.
readonly progress: firestore.LoadBundleTaskProgress, | ||
readonly changedDocs?: 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.
Since this is an exported class, it should have a less generic name.
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.
hasPendingWrites: true | ||
}) | ||
.writeAcks('collection/a', 1000) | ||
// loading bundleBeforeMutationAck will not raise snapshots, because it is before |
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/it is before/its snapshot version is older than/
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.
readTime: 1001, | ||
createTime: 250, | ||
updateTime: 1001, | ||
content: { key: 'fromBundle' } |
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/key/value since the key
is 'a'. You need to update all document contents.
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.
hasPendingWrites: true | ||
}) | ||
// Loading the bundle will not raise snapshots, because the | ||
// mutation is not acknowledged. |
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/is not/has not been/
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.
.withGCEnabled(false) | ||
.userListens(query) | ||
.watchAcksFull(query, 250) | ||
// Backend tells there is no such doc. |
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/tells/tells us
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.asyncQueue.enqueueAndForget(async () => { | ||
loadBundle(this.syncEngine, reader, task); | ||
return task.catch(e => { | ||
logWarn(`Loading bundle failed with ${e}`); |
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 should add a LOG_TAG (see most other log lines).
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.
INTERNAL: { delete: () => Promise<void> }; | ||
} | ||
|
||
export interface LoadBundleTask { | ||
onProgress( |
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.
BTW, should we include an overload that takes an Observer?
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.
Yeah, it's actually in the API doc. I will add it in a followup PR.
export class BundleLoadResult { | ||
constructor( | ||
readonly progress: firestore.LoadBundleTaskProgress, | ||
readonly changedDocs?: 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.
This doesn't need to be optional anymore, which I think leads to one more simplification.
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.
No description provided.