-
Notifications
You must be signed in to change notification settings - Fork 310
store: Have GlobalStore manage one PerAccountStore per account #27
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
This is how it's really functioning anyway. This arrangement will be helpful as we split PerAccountStore into a base class and an implementation with more details, as the latter can then have a constructor that cleanly delegates most of the work to the former's constructor. This way also reduces the amount of boilerplate required to add another piece of data to the model.
This lets a test subclass GlobalStore to provide a different way to load a PerAccountStore. (Widgets still hardcode the "live" GlobalStore, so we'll need to make some further changes when we go to write tests for those widgets. But this is already enough to write tests for any logic we put on GlobalStore.)
4e95a46
to
9fef8cf
Compare
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, LGTM! Please merge at will after seeing the small thing below.
lib/model/store.dart
Outdated
/// The store's per-account data for the given account, if already loaded. | ||
/// | ||
/// When not null, this is the same [PerAccountStore] that would be returned | ||
/// by the asynchronous [ofAccount]. | ||
PerAccountStore? ofAccountSync(int accountId) => _perAccountStores[accountId]; | ||
|
||
/// The store's per-account data for the given account. | ||
/// | ||
/// If the data for this account is not already loaded, this will ensure a | ||
/// request is made to load it, and the returned future will complete when | ||
/// the data is ready. | ||
/// | ||
/// The [GlobalStore] will avoid making redundant requests for the same data, | ||
/// even if this method is called many times. The futures returned from each | ||
/// call for the same account will all complete once the data is ready. | ||
/// | ||
/// Consider checking [ofAccountSync] before calling this function, so that if | ||
/// the data is already available it can be used immediately (e.g., in the | ||
/// current frame.) | ||
/// | ||
/// See also: | ||
/// * [PerAccountStoreWidget.of], for getting the relevant [PerAccountStore] | ||
/// from UI code. | ||
Future<PerAccountStore> ofAccount(int accountId) async { | ||
// First, see if we have the store already. | ||
PerAccountStore? store = _perAccountStores[accountId]; | ||
if (store != null) { | ||
return store; | ||
} | ||
|
||
// Next, see if another call has already started loading one. | ||
Future<PerAccountStore>? future = _perAccountStoresLoading[accountId]; | ||
if (future != null) { | ||
return future; | ||
} | ||
|
||
// It's up to us. Start loading. | ||
final account = getAccount(accountId); | ||
assert(account != null, 'Account not found on global store'); | ||
future = loadPerAccount(account!); | ||
_perAccountStoresLoading[accountId] = future; | ||
store = await future; | ||
_perAccountStores[accountId] = store; | ||
_perAccountStoresLoading.remove(accountId); | ||
return 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.
A small thing we discussed in the office: perhaps there's a better name than ofAccount
/ ofAccountSync
? It's easy to misunderstand globalStore.ofAccount(2)
as something like "get the global store of account 2", when really the store you get is a per-account store (PerAccountStore
), not a global one (GlobalStore
).
Not a blocker though, I 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.
Sure — renaming to perAccount
and perAccountSync
.
And make use of the testability refactors we've made in the preceding series of commits, in order to test this code because it needs to deal with concurrency and so is a bit subtle.
9fef8cf
to
2896360
Compare
Thanks for the review! Merging, with that rename. |
This is stacked atop #26.
As part of #21, we'll want to be able to freely instantiate a
PerAccountStoreWidget
whenever we want to mark some subtree as being for a particular account, and have that cheaply just refer to the already-loaded data for that account. This PR does that by having aGlobalStore
maintain aPerAccountStore
for each account, whichPerAccountStoreWidget
asks it for when it's first mounted in the element tree.Because the logic in
GlobalStore
to maintain that, and in particular to avoid redundant requests if severalPerAccountStoreWidget
s ask about the same account at the beginning, is a bit complex, we want tests for it. To make those possible, we split bothGlobalStore
andPerAccountStore
into a base class (which keeps the name) and a subclass that adds the using-real-data part which a unit test will want to leave out.We also make sure to accommodate this fun trick (in
_PerAccountStoreWidgetState
):Fortunately the nature of that trick means that other code generally won't have to think about it; just use
PerAccountWidget
.(We might eventually want to play the same kind of trick in a small number of other places, where we depend on data that's loaded outside of the initial fetch (and so isn't guaranteed by simply having a
PerAccountState
) but that we'll sometimes have already loaded. The message list is the main example that comes to mind.)