-
Notifications
You must be signed in to change notification settings - Fork 584
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
RJS-2636: Add progress notifications and tests #6743
Conversation
@@ -49,6 +50,7 @@ const PersonForSyncSchema: Realm.ObjectSchema = { | |||
}, | |||
}; | |||
|
|||
// TODO: Fix this. |
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 unrelated to this PR, I can create a follow-up issue but waitForConnectionState seems to be a complete noop right now, may mean some tests either don't need it or are meaningless.
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 progressCallback = spy(); | ||
await Realm.open(config).progress(progressCallback); | ||
|
||
expect(progressCallback.called).is.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.
Not going to spend much time refactoring the other tests since they related to the deprecated method, just thought this was a nice opportunity for sinon (also just timeout).
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.
Also, there's a sinon-chai integration and I will switch to that soon, maybe in a different PR if there's not enough time for now
945ca7a
to
1908727
Compare
I am still battling some CI issues but I figure it may be worth requesting some reviews for now. |
integration-tests/tests/package.json
Outdated
@@ -84,6 +84,8 @@ | |||
"@types/chai-as-promised": "^7.1.5", | |||
"@types/jsrsasign": "^10.5.4", | |||
"@types/mocha": "^10.0.0", | |||
"@types/sinon": "^9.0.5", | |||
"assert": "^2.1.0", |
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 polyfill seems necessary for electron...
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'd like more context on this change.
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 seemed necessary for sinon but I had since then changed the version to something which has more compatibility so it may not be needed anymore
@@ -167,18 +175,31 @@ export class ProgressRealmPromise implements Promise<Realm> { | |||
*/ | |||
progress(callback: ProgressNotificationCallback): this { | |||
this.listeners.add(callback); | |||
if (callback.length === 1) { | |||
const estimateCallback = callback as DynamicProgressNotificationCallback; | |||
estimateCallback(1.0); |
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 behavior is sort of expected from previous implementation, I figured this was best equivalent.
Okay, I am 99% certain that last failing tests is a result of GHA caching, and I tried to delete the cache manually many times for this branch/PR but seems to keep using it, @kraenhansen may have a better idea of what is going on here... but will have to leave it at that since this now 62 commits deep into making it fully green 😄 At least it does not seem flaky and is somewhat reasonable in its underlying implementation. Will leave it at this point since there's no chance to have a proper reviews but feel free to continue from here to get it merged. I'd say it is pretty complete and although tests are far from ideal, there is also no clear behavior expectations yet and much of it is just up to core. Should be good to merge beyond any review comments you can have. Feel free to get in touch if needed in next few days. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
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.
Comments from a first pass here, looks really nice 🙂
/** @internal */ | ||
private token = 0; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- Is
0
being used as an "unset" indicator here? It could possibly make it clearer if we used e.g.-1
(as the token in Core uint) ornull
.- We should also have the number type be
binding.Int64
rather thannumber
. (See e.g. this)
- We should also have the number type be
- Could we add a short sentence to the internal docs here mentioning that it's used for unregistering the notifier?
// TODO: Consider storing the token returned here to unregister when the task gets cancelled, | ||
// if for some reason, that doesn't happen internally | ||
this.task.registerDownloadProgressNotifier(this.emitProgress); | ||
if (this.listeners.size > 0) { |
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.
Isn't it possible for a listener to be added (via progress()) after this code has been executed? If so, then I think this guard should probably be removed. (Invoking the listeners is still guarded by it's size here.)
if (this.token !== 0) { | ||
this.task?.unregisterDownloadProgressNotifier(this.token); | ||
} |
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 we reset the token
after the Core call here as well?
@@ -153,6 +160,7 @@ export class ProgressRealmPromise implements Promise<Realm> { | |||
cancel(): void { | |||
this.cancelAndResetTask(); | |||
this.timeoutPromise?.cancel(); | |||
this.task?.unregisterDownloadProgressNotifier(this.token); |
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 guard for the "unset" token value here.
@@ -167,18 +175,31 @@ export class ProgressRealmPromise implements Promise<Realm> { | |||
*/ | |||
progress(callback: ProgressNotificationCallback): this { |
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 API docs for this function needs an update 🙂
@@ -93,6 +109,21 @@ function toBindingDirection(direction: ProgressDirection) { | |||
} | |||
} | |||
|
|||
function isEstimateProgressNotification(cb: ProgressNotificationCallback): cb is DynamicProgressNotificationCallback { |
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 believe a
Callback
postfix is missing from the name. - Could we use the fully spelled out name
callback
for increased consistency?
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 with the callback, these bits of code are a mix of mine and largely @kneth so he may want to weigh in as well
function toBindingProgressNotificationCallback(cb: ProgressNotificationCallback) { | ||
if (isEstimateProgressNotification(cb)) { | ||
return (transferredBytes: binding.Int64, transferrableBytes: binding.Int64, progressEstimate: number) => | ||
cb(progressEstimate); | ||
} else { | ||
// eslint-disable-next-line @typescript-eslint/no-unused-vars | ||
return (transferredBytes: binding.Int64, transferrableBytes: binding.Int64, _: number) => | ||
cb(transferrableBytes, transferrableBytes); | ||
} | ||
} |
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: Argument name
- The 2nd callback was accidentally called without
transferredBytes
.
function toBindingProgressNotificationCallback(cb: ProgressNotificationCallback) { | |
if (isEstimateProgressNotification(cb)) { | |
return (transferredBytes: binding.Int64, transferrableBytes: binding.Int64, progressEstimate: number) => | |
cb(progressEstimate); | |
} else { | |
// eslint-disable-next-line @typescript-eslint/no-unused-vars | |
return (transferredBytes: binding.Int64, transferrableBytes: binding.Int64, _: number) => | |
cb(transferrableBytes, transferrableBytes); | |
} | |
} | |
function toBindingProgressNotificationCallback(callback: ProgressNotificationCallback) { | |
if (isEstimateProgressNotification(callback)) { | |
return (_: binding.Int64, __: binding.Int64, progressEstimate: number) => callback(progressEstimate); | |
} else { | |
return (transferredBytes: binding.Int64, transferrableBytes: binding.Int64, _: number) => | |
callback(transferredBytes, transferrableBytes); | |
} | |
} |
(transferred, transferable) => callback(Number(transferred), Number(transferable)), | ||
toBindingProgressNotificationCallback(callback), |
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.
💪
* @param callback - Called with the following arguments: | ||
* 1. `transferred`: The current number of bytes already transferred | ||
* 2. `transferable`: The total number of transferable bytes (the number of bytes already transferred plus the number of bytes pending transfer) | ||
* @param callback - see {@link DynamicProgressNotificationCallback} and {@link PartitionBasedSyncProgressNotificationCallback} |
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.
Perhaps something similar to this could suffice (mainly because the type itself exists by the parameter; so similarly to e.g. direction
, we don't refer to the ProgressDirections
s available in the doc description here 👍 ). (Although it looks like we do for mode
so I see that it's not really consistent 😛 )
* @param callback - see {@link DynamicProgressNotificationCallback} and {@link PartitionBasedSyncProgressNotificationCallback} | |
* @param callback - The function to call when the progress is updated. |
if (this.config.flexible) { | ||
assert( | ||
isEstimateProgressNotification(callback), | ||
`For flexible sync the callback can only take one argument - got ${callback.length} arguments`, | ||
); | ||
} |
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 this may be a breaking change, since it has still been possible to call the API, and we haven't documented that it should have thrown before. Hmm, could we warn here instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree - this feels more breaking than a deprecation (which might be okay? but should be explicit and released accordingly) if it actually throws instead of simply warning.
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 update it to e.g.:
if (this.config.flexible && !isEstimateProgressNotificationCallback(callback)) {
/* eslint-disable-next-line no-console */
console.warn("Deprecated progress notifier used. Please use 'EstimateProgressNotificationCallback' which accepts the estimate as the only argument.");
}
But I think we could even skip the warning, since the warning is part of the deprecation in the docs. (We don't usually warn for other deprecations.)
For now I've updated this to just assert that it's a function.
@@ -53,13 +53,29 @@ export enum ProgressMode { | |||
ForCurrentlyOutstandingWork = "forCurrentlyOutstandingWork", | |||
} | |||
|
|||
export type ProgressNotificationCallback = | |||
/** @deprecated */ | |||
export type PartitionBasedSyncProgressNotificationCallback = |
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.
before(async function (this: UserContext) { | ||
// TODO: Investigate this. | ||
// Use a new user for this test suite as not doing so causes some session issues in other tests | ||
this.user = await this.app.logIn(Credentials.anonymous(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.
Are you aware of the authenticateUserBefore
hook? If its important to avoid reusing, you could patch the function to take a boolean argument:
this.user = this.app.currentUser || (await this.app.logIn(Credentials.anonymous())); |
beforeEach(async function (this: RealmContext & UserContext) { | ||
// @ts-expect-error Using an internal API | ||
this.realm = new Realm({ | ||
schema: [Person, Dog], | ||
sync: { | ||
flexible: true, | ||
user: this.user, | ||
_sessionStopPolicy: SessionStopPolicy.Immediately, | ||
}, | ||
}); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a reason you're not using openRealmBeforeEach
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.
Hm, I was moving back and forth between using that or my own as I had other cleanup hooks to check different things but now it seems like this can be fully covered byopenRealmBeforeEach
, feel free to change it.
it("only estimate callback is allowed", async function (this: RealmContext) { | ||
// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function | ||
const callback = (estimate: number) => {}; | ||
this.realm.syncSession?.addProgressNotification( | ||
Realm.ProgressDirection.Download, | ||
Realm.ProgressMode.ForCurrentlyOutstandingWork, | ||
callback, | ||
); | ||
}); |
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.
What is this testing specifically? Passing a single argument arrow function as callback?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, this has to do with deprecated API vs non-deprecated. Specifically, with sync 2 argument arrow function will error with flexible sync
Just tried to reply to some of the comments that may have been hard to guess without my input. Beyond that, going to be away so feel free to go ahead with what seems best 😄 |
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.
Great to see many tests here 🙂 Left some suggestions and thoughts
); | ||
}); | ||
|
||
it("old callback style is not allowed", async function (this: RealmContext) { |
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 should be updated (to be allowed) once the assertion from addProgressNotification()
is removed in order to not break the API 👍
await this.realm.syncSession?.uploadAllLocalChanges(); | ||
await this.realm.syncSession?.downloadAllServerChanges(); |
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 this combo is used frequently, we could create a small helper.
// TODO: This callback should not be called at this stage but seems flakey | ||
// and gets called with 1.0 at times, likely because of a race condition. | ||
expect(callback.notCalled || callback.calledWith(1.0)).is.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.
Wondering if we should still remove the callback.calledWith(1.0)
though.
// TODO: This callback should not be called at this stage but seems flakey | |
// and gets called with 1.0 at times, likely because of a race condition. | |
expect(callback.notCalled || callback.calledWith(1.0)).is.true; | |
expect(callback.notCalled || callback.calledWith(1.0)).to.be.true; |
|
||
// TODO: Find a way to fix | ||
// There should be at least one point where the progress is not yet finished. | ||
// expect(callback.args.find(([estimate]) => estimate < 1)).to.not.be.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.
// expect(callback.args.find(([estimate]) => estimate < 1)).to.not.be.undefined; | |
// expect(callback.args.some(([estimate]) => estimate < 1)).to.be.true; |
I'm unsure why your code there didn't work?
There seem to be another way with sinon as well:
expect(callback.calledWith(sinon.match(estimate => estimate < 1))).to.be.true;
A question regarding the comment though, is it true that we should always expect a number < 1? Or is it possible that for small uploads to sometimes only report the 1?
// There should be at least one point where the progress is not yet finished. | ||
// expect(callback.args.find(([estimate]) => estimate < 1)).to.not.be.undefined; | ||
|
||
expect(callback.withArgs(1.0)).called; |
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.
Unsure if withArgs()
is the correct usage here since that seems to create a new spy (according to their API docs). If using that, you probably want to put the called
onto the spy instead (similar to the above where callback.notCalled
is used):
expect(callback.withArgs(1.0).called).to.be.true
However, there are also these APIs:
callback.calledWith(..)
callback.calledWithExactly(..)
import { buildAppConfig } from "../../utils/build-app-config"; | ||
import { spy } from "sinon"; |
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: Can move up to the 3rd party imports 🙂
expect(progressCalled).to.be.true; | ||
|
||
const progressCallback = spy(); | ||
await Realm.open(config).progress(progressCallback); |
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.
👍
CHANGELOG.md
Outdated
* The callback for `realm.syncSession.addProgressNotification` will only take a single argument in the future: `estimate` (which supports both partition based and flexible sync). The old callback functionality is deprecated and will be removed. The estimate is roughly equivalent to an estimated value of `transferred / transferable` in the deprecated partition-based sync callback. ([#6256](/~https://github.com/realm/realm-js/issues/6256)) | ||
```ts | ||
/** New callback which supports both flexible and partition-based sync */ | ||
realm.syncSession.addProgressNotification( | ||
Realm.ProgressDirection.Upload, | ||
ProgressMode.ReportIndefinitely, | ||
(estimate) => console.log(`progress estimate: ${estimate}/1.0`), | ||
); | ||
/** @deprecated */ | ||
realm.syncSession.addProgressNotification( | ||
Realm.ProgressDirection.Upload, | ||
ProgressMode.ReportIndefinitely, | ||
(transferred, transferable) => console.log(`progress: ${(transferred / transferable)}/1.0`), | ||
); | ||
``` |
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 info 🙂 I'm wondering if the info about the new API is better served along with the Enhancement entry and refer to that one instead? We could still mention deprecated API in the current section.
CHANGELOG.md
Outdated
|
||
### Enhancements | ||
* None | ||
* Added progress notifications support for flexible sync using a new callback argument. ([#6256](/~https://github.com/realm/realm-js/issues/6256)) | ||
```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.
```ts | |
```typescript |
CHANGELOG.md
Outdated
* None | ||
* Added progress notifications support for flexible sync using a new callback argument. ([#6256](/~https://github.com/realm/realm-js/issues/6256)) | ||
```ts | ||
realm.syncSession.addProgressNotification( |
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.
realm.syncSession.addProgressNotification( | |
realm.syncSession?.addProgressNotification( |
Note on Android tests: Still experiencing signature mismatch. |
Using link to the PR rather than the issue as we'll add more info there.
Needed on iOS.
3714a5d
to
6f3811d
Compare
What, How & Why?
Adds progress notifications for flexible sync and tests for it.
This closes #6256
@realm/devdocs
☑️ ToDos
Compatibility
label is updated or copied from previous entryCOMPATIBILITY.md
📦 Updated internal package version in consumingpackage.json
s (if updating internal packages)📱 Check the React Native/other sample apps work if necessary💥Breaking
label has been applied or is not necessary