-
-
Notifications
You must be signed in to change notification settings - Fork 4.5k
WebUI: Store persistent settings in client data API #23191
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
base: master
Are you sure you want to change the base?
Conversation
bbffd53 to
4b1bf7b
Compare
4b1bf7b to
1bd7ae3
Compare
| let clientDataPromise; | ||
| const setup = () => { | ||
| // fetch various data and store it in memory | ||
| clientDataPromise = window.qBittorrent.ClientData.get([ |
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 have a "MigrationVersion" field. This functionality will be required eventually. I don't insist it to be done in this PR but you should add it in a later PR.
Look at LocalPreferences.upgrade() for an example.
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 will likely eventually be needed. Since this API accepts arbitrary values, I think we can wait to add it until we actually need to use it for the first time.
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 can wait to add it until we actually need to use it for the first time.
The actually upgrade() function can be added later but for now you should at least set the key MigrationVersion=1 so we don't have to handle it being missing later.
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.
There's no guarantee it'll be set later - our code will still need a null check. Going with YAGNI on this one. We can add it later when it's needed since there's absolutely zero downside to waiting.
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.
There's no guarantee it'll be set later - our code will still need a null check.
You don't need to check anything. Just set it to 1 unconditionally so that the actual migration code that needs it can just use 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.
I'm referring to the code that fetches and checks for the current migration version. There's no guarantee that a migration version will have been set/will exist, so defensive code will looking something like clientData.get("migrationVersion") ?? 1.
011ac5a to
30807d0
Compare
77c79d1 to
dcec1fd
Compare
| let clientDataPromise; | ||
| const setup = () => { | ||
| // fetch various data and store it in memory | ||
| clientDataPromise = window.qBittorrent.ClientData.get([ |
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.
There's no guarantee it'll be set later - our code will still need a null check.
You don't need to check anything. Just set it to 1 unconditionally so that the actual migration code that needs it can just use it.
|
@qbittorrent/bug-handlers can I get a review and get this merged? This will make a big difference for WebUI users and belongs in v5.2. |
dcec1fd to
83978ea
Compare
83978ea to
7c539f3
Compare
|
@qbittorrent/bug-handlers is anyone interested in moving this PR forward? @Chocobo1 seems disinterested because we disagree on some points, which means this PR is stuck. |
|
@qbittorrent/bug-handlers hello? @glassez anyone? |
|
Since this PR is currently stuck on (IMO) highly subjective opinions that don't affect the correctness of the code, I thought I'd share an excerpt from this excellent article1 that's currently trending on Hacker News:
Footnotes |
You do know that I can't provide a comprehensive review of WebUI PRs (only discuss some common issues), as I am not competent in this area. |
Maybe you can help to mediate the discussion? Or just review the remaining open comments above. Or is there another person in @qbittorrent/web-developers that can approve this? |
This comment was marked as off-topic.
This comment was marked as off-topic.
This comment was marked as off-topic.
This comment was marked as off-topic.
This comment was marked as off-topic.
This comment was marked as off-topic.
This comment was marked as off-topic.
This comment was marked as off-topic.
This comment was marked as off-topic.
This comment was marked as off-topic.
This comment was marked as off-topic.
This comment was marked as off-topic.
|
@Piccirello |
The only way to make the cache transparent, which I agree would be preferable, would be to propagate async in a bunch of places that are currently sync. So the current approach seems like the right balance. Unfortunately, no one is approving this PR. |
Apologies, I'm not able to understand the balance you're referring to (between what and what).
Personally, I still can't figure out exactly what's going on here. But I'm still neutral about WebUI PRs, so my opinion can't be considered as blocking this PR. |
I still don't understand the essence of the problem. Shouldn't some value be retrieved from the server at least once in order to be cached (regardless of how caching is implemented)? Therefore, it should have an asynchronous handler, right? Or is it not the case? Additionally, how do "synchronous" callers know that the data is already present in the cache and can be retrieved? In short, I am unclear about the intended execution flow and where in it these reads/writes occur. |
The
All necessary data is cached on client load, so synchronous calls can later be made to the cache without issue. It's the same pattern that we use for the |
OK, (I hope) I got it. This became the case after the last change made at the suggestion of @Chocobo1 so now
I hope this is guaranteed by the design that it will be executed before the dependent code starts executing, so we can assert on caches are populated. The remaining problem is that the user (of |
The question is, is it possible to assign retrieved values directly to target variables instead of populating the cache, so that we don't need this cache at all? |
That's true. I feel comfortable this would be caught in testing, since the
The cache is accessed from many different places and classes, so the target variables would need to be global. At that point, I don't see what the benefit would be over having the cache class. |
If that's the case, then I agree. |
But
An alternative would be to store all the data of the same client under a single key. The disadvantage is that you also won't be able to update the values one at a time, but only all together. |
That's right. What I mean is that testing would show that the saved value is never tried. For example, change a setting, reload, notice the setting isn't retained.
I'm going to leave the current code as-is unless you feel strongly that the key list should only appear once. If you do, I'll remove it and have |
|
@qbittorrent/lead-developers any chance of getting this approved this weekend? This feature is fully functional without any performance issues and all feedback has been addressed. |
| window.parent.qBittorrent.Client.closeWindow(document.getElementById("preferencesPage")); | ||
| }); | ||
| // Send data to qBT | ||
| const results = await Promise.allSettled([ |
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.
Use .then() and not await for further handling. And therefore this function would not be tainted by async.
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 find async/await easier to understand and reason about. There's a reason it's the default in modern async JS.
And therefore this function would not be tainted by
async.
I don't know what this means, or why using async would be an issue.
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.
or why using async would be an issue.
Because it turns a sync function into an async one. And generally async should only be declared when necessary. I don't see why applyPreferences would need to be an async operation.
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.
Because it turns a sync function into an async one.
This is a correct statement, but I still don't see the issue. This code is easier to read with async/await than it is with promise chaining. They functionally work the same, but await allows you to limit nesting. I'm not sure why you're so opposed to async/await but, as I stated previously, it is preferred in modern JS.
I don't see why
applyPreferenceswould need to be an async operation.
It produces code that is easier to read, easier to reason about, and easier to understand than promise chaining.
And therefore this function would not be tainted by
async.
You view async as taining the code; I view then() as tainting the code. We don't have a style rule about this, which means this is up to the stylistic preference of the PR author. Both work the same and produce the same output.
| const initializeClientData = async () => { | ||
| try { | ||
| await clientDataPromise; | ||
| } | ||
| catch (error) { | ||
| console.error(`Failed to initialize client data. Reason: "${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.
This function looks redundant. The setup() is guaranteed to run to completion before DOMContentLoaded event.
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.
Hmm, how can we guarantee this? I'm happy to remove this function if it's not needed.
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'm happy to remove this function if it's not needed.
Yes, I would remove that and inline clientDataPromise as such:
cacheAllSettled = Promise.allSettled([
window.qBittorrent.Cache.buildInfo.init(),
window.qBittorrent.Cache.preferences.init(),
window.qBittorrent.Cache.qbtVersion.init(),
window.qBittorrent.ClientData.fetch([
"add_torrent_default_category",
"color_scheme",
"dblclick_complete",
...])
]);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, I would remove that and inline clientDataPromise as such:
And the statement await window.qBittorrent.Client.initializeCaches(); should be moved from load event to DOMContentLoaded event.
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 didn't answer my question - how can we guarantee that setup() is complete before DOMContentLoaded executes?
And the statement
await window.qBittorrent.Client.initializeCaches();should be moved fromloadevent toDOMContentLoadedevent.
This is existing behavior that I'm not going to modify in this PR. You literally just told me not to do this here.
|
A bit offtopic and sorry if someone has already mentioned this. |
13e22ef to
8d55aaa
Compare
Good point. I've updated the naming to "persistent". |
| #addKeyPrefix(data) { | ||
| return Object.fromEntries(Object.entries(data).map(([key, value]) => ([`${this.#keyPrefix}${key}`, value]))); | ||
| } | ||
|
|
||
| #removeKeyPrefix(data) { |
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 don't insist and I think it would still be helpful to include type comments for these two internal functions.
| const initializeClientData = async () => { | ||
| try { | ||
| await clientDataPromise; | ||
| } | ||
| catch (error) { | ||
| console.error(`Failed to initialize client data. Reason: "${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.
I'm happy to remove this function if it's not needed.
Yes, I would remove that and inline clientDataPromise as such:
cacheAllSettled = Promise.allSettled([
window.qBittorrent.Cache.buildInfo.init(),
window.qBittorrent.Cache.preferences.init(),
window.qBittorrent.Cache.qbtVersion.init(),
window.qBittorrent.ClientData.fetch([
"add_torrent_default_category",
"color_scheme",
"dblclick_complete",
...])
]);8d55aaa to
00a1e06
Compare
This PR moves persistent WebUI settings from browser local storage to the new client data API. This allows these settings to be shared across multiple WebUI instances. This does not include settings that are specific to the local client, like window sizes, selected filters, selected tabs, and sorted columns. I also don't want to get hung up on a debate over whether these should be included - they can always be added later.
Depends on #23088. Closes #12100