Skip to content

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

Merged
merged 43 commits into from
Jul 11, 2020
Merged

Implement bundle loading. #3201

merged 43 commits into from
Jul 11, 2020

Conversation

wu-hui
Copy link
Contributor

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

No description provided.

@wu-hui wu-hui force-pushed the wuandy/BundleLoadProgress branch from d6fafba to adf1504 Compare June 11, 2020 15:09
wu-hui added 2 commits June 12, 2020 15:45
# 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
@googlebot
Copy link

CLAs look good, thanks!

ℹ️ Googlers: Go here for more info.

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.

Thanks for this!

@@ -98,3 +108,247 @@ export class BundleConverter {
return fromVersion(time);
}
}

/**
* Returns a `LoadBundleTaskProgress` representing the first progress of
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
* Returns a `LoadBundleTaskProgress` representing the first progress of
* Returns a `LoadBundleTaskProgress`, representing the initial progress of

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.

}

/* eslint-disable @typescript-eslint/no-explicit-any */
export class LoadBundleTaskImpl implements firestore.LoadBundleTask {
Copy link
Contributor

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>();
Copy link
Contributor

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) {
Copy link
Contributor

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.

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.

onFulfilled?: (a: firestore.LoadBundleTaskProgress) => any,
onRejected?: (a: Error) => any
): Promise<any> {
this.promiseFulfilled = onFulfilled;
Copy link
Contributor

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)?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We could, obviously I cannot wrap my mind around promises.

})
// Loading both bundles will not raise snapshots, because of the
// mutation is not acknowledged.
.loadBundle(bundleString1)
Copy link
Contributor

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.

Copy link
Contributor Author

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.
Copy link
Contributor

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?

Copy link
Contributor Author

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.

Comment on lines 460 to 465
const task = new LoadBundleTaskImpl();
this.queue.enqueueAndForget(() => {
return loadBundle(this.syncEngine, reader, task);
});

await task;
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
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));

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.

syncEngine: SyncEngine,
bundleReader: BundleReader,
task: LoadBundleTaskImpl
): Promise<void> {
Copy link
Contributor

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.

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 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?

Copy link
Contributor

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.

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 acknowledged mutation.
.loadBundle(bundleString2)
.expectEvents(query, {
modified: [doc('collection/a', 1001, { key: 'fromBundle' })]
Copy link
Contributor

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed.

@google-oss-bot
Copy link
Contributor

google-oss-bot commented Jun 29, 2020

Binary Size Report

Affected SDKs

  • @firebase/firestore

    Type Base (3a78d7c) Head (a6740c8) Diff
    browser 250 kB 258 kB +7.97 kB (+3.2%)
    esm2017 195 kB 201 kB +5.32 kB (+2.7%)
    main 498 kB 512 kB +14.1 kB (+2.8%)
    module 248 kB 256 kB +7.74 kB (+3.1%)
  • @firebase/firestore/exp

    Type Base (3a78d7c) Head (a6740c8) Diff
    main 277 kB 288 kB +10.9 kB (+3.9%)
  • @firebase/firestore/memory

    Type Base (3a78d7c) Head (a6740c8) Diff
    browser 188 kB 196 kB +8.10 kB (+4.3%)
    esm2017 147 kB 153 kB +5.46 kB (+3.7%)
    main 368 kB 382 kB +14.4 kB (+3.9%)
    module 187 kB 194 kB +7.86 kB (+4.2%)
  • firebase

    Type Base (3a78d7c) Head (a6740c8) Diff
    firebase-firestore.js 289 kB 296 kB +7.54 kB (+2.6%)
    firebase-firestore.memory.js 228 kB 236 kB +7.65 kB (+3.4%)
    firebase.js 822 kB 829 kB +7.56 kB (+0.9%)

Test Logs

syncEngine: SyncEngine,
bundleReader: BundleReader,
task: LoadBundleTaskImpl
): Promise<void> {
Copy link
Contributor

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.

next?: (progress: LoadBundleTaskProgress) => any,
error?: (error: Error) => any,
complete?: () => void
): Promise<void>;
Copy link
Contributor

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?

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 110 to 113
then(
onFulfilled?: (a: LoadBundleTaskProgress) => any,
onRejected?: (a: Error) => any
): Promise<any>;
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(
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>;

Copy link
Contributor

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>;
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Nice, thanks!

onRejected?: (a: Error) => any
): Promise<any>;

catch(onRejected: (a: Error) => any): Promise<any>;
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
catch(onRejected: (a: Error) => any): Promise<any>;
catch(onRejected: (a: Error) => R|PromiseLike<R>): Promise<R>;

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. Although it seems it should return a Promise<R|Progress> in this case.

this._userProgressHandler = next;
this._userProgressErrorHandler = error;
this._userProgressCompleteHandler = complete;
return this._progressResolver.promise;
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 don't need to return this promise 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.

Done.

readTime: TestSnapshotVersion;
createTime?: TestSnapshotVersion;
updateTime?: TestSnapshotVersion;
content?: api.ApiClientObjectMap<Value>;
Copy link
Contributor

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.

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.

.expectEvents(query1, { modified: [docAChanged], fromCache: true });
});

specTest('Newer deleted docs from bundles should delete cache', [], () => {
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
specTest('Newer deleted docs from bundles should delete cache', [], () => {
specTest('Newer deleted docs from bundles should delete cached docs', [], () => {

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.

});

specTest(
'Newer docs from bundles should raise snapshot only when watch catches up with acknowledged writes',
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
'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',

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 (
spec()
.withGCEnabled(false)
Copy link
Contributor

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.

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.

added: [doc('collection/a', 500, { key: 'b' })],
fromCache: true
})
.expectLimboDocs(docA.key)
Copy link
Contributor

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

@schmidt-sebastian schmidt-sebastian removed their assignment Jul 7, 2020
@wu-hui wu-hui assigned schmidt-sebastian and unassigned wu-hui Jul 9, 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.

Next round. This is 98% done!

implements
firestore.LoadBundleTask,
PromiseLike<firestore.LoadBundleTaskProgress> {
private _progressObserver?: PartialObserver<firestore.LoadBundleTaskProgress>;
Copy link
Contributor

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: {}.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

Copy link
Contributor Author

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);
}
Copy link
Contributor

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:

private notifyObserver_(observer: Observer<UploadTaskSnapshot>): void {

Basically, next doesn't get called for the Complete of Error state.

Copy link
Contributor Author

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);
Copy link
Contributor

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)?

Copy link
Contributor Author

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.

*/
_updateProgress(progress: firestore.LoadBundleTaskProgress): void {
debugAssert(
this._lastProgress.taskState !== 'Error',
Copy link
Contributor

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.

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.

readonly progress: firestore.LoadBundleTaskProgress,
readonly changedDocs?: MaybeDocumentMap
) {}
}
Copy link
Contributor

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.

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.

hasPendingWrites: true
})
.writeAcks('collection/a', 1000)
// loading bundleBeforeMutationAck will not raise snapshots, because it is before
Copy link
Contributor

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/

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.

readTime: 1001,
createTime: 250,
updateTime: 1001,
content: { key: 'fromBundle' }
Copy link
Contributor

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.

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.

hasPendingWrites: true
})
// Loading the bundle will not raise snapshots, because the
// mutation is not acknowledged.
Copy link
Contributor

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/

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.

.withGCEnabled(false)
.userListens(query)
.watchAcksFull(query, 250)
// Backend tells there is no such doc.
Copy link
Contributor

Choose a reason for hiding this comment

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

s/tells/tells us

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.asyncQueue.enqueueAndForget(async () => {
loadBundle(this.syncEngine, reader, task);
return task.catch(e => {
logWarn(`Loading bundle failed with ${e}`);
Copy link
Contributor

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

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.

@wu-hui wu-hui assigned schmidt-sebastian and unassigned wu-hui Jul 10, 2020
INTERNAL: { delete: () => Promise<void> };
}

export interface LoadBundleTask {
onProgress(
Copy link
Contributor

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?

Copy link
Contributor Author

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
Copy link
Contributor

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.

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.

@wu-hui wu-hui merged commit 0bf9c73 into wuandy/Bundles Jul 11, 2020
@firebase firebase locked and limited conversation to collaborators Aug 11, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants