-
-
Notifications
You must be signed in to change notification settings - Fork 654
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
Multi-org-compatible schemas for data in Redux and persistent store #5006
Comments
In the status quo, our Redux state tree looks like:
Then the schema of our persistent storage is simple to describe in terms of the Redux schema:
|
Here's thoughts on a new design. First, considering what we'd like the schema in Redux to look like, because that's the surface area that the bulk of our app's code interacts with:
A UI side note: the behavior where we keep accounts in LRU order is kind of silly. The normal thing would be that they're in a stable order; new accounts you add go at the bottom; and there's some UI for the user to explicitly reorder them, by dragging them around. It'd be straightforward to proceed with this data-schema change without affecting the LRU behavior. That's even probably the right way to organize the changes, in order to separate PRs making big infrastructural changes from PRs making significant UI changes, which call for different modes of review. But once we've made this schema change, so that the concept of "which account is the active account" is an explicit value rather than being implicitly bound up with the order of the |
And here's thoughts on what we might want the schema to look like for the data we store persistently on the device. We're not going to want to continue to have each whole top-level subtree (that we persist at all) correspond to a single key in the persistent store. That would put almost all the app's data in a single key, corresponding to
As things stand, we hardcode that one-key-per-top-level-subtree behavior. The upstream But that behavior remains very much under our control. It's expressed entirely by this bit of code in // Atomically collect the subtrees that need to be written out.
const updatedSubstates = [];
for (const key of Object.keys(state)) {
if (!passWhitelistBlacklist(key)) {
continue;
}
if (state[key] === lastWrittenState[key]) {
continue;
}
updatedSubstates.push([key, state[key]]);
} together with some code in So we'll want to write some new code that does those jobs, but with all the specific wrinkles we want, like that For efficiency, one difference I think we'll want from the old For the actual shape of the keys, I imagine we'll go for something like |
Another question is how to do migration. This was actually the reason we forked/vendored redux-persist in the first place: upstream had a new major version with a new design for migrations, which insisted on doing migrations locally in each subtree and was not compatible with changes that seriously rearranged the subtrees themselves, as we need to do here. I think the existing migration logic will mostly give us what we need. But this is something I'll need to look at more closely. One thing that I know is missing is that when we stop using an old key -- for example |
Here's a different aspect that requires some more design and experimentation: what will we want the app code to look like? One part of this, about reducers, is discussed above:
But there's also our UI code, and our selector code (particularly for complex selectors, like those in
In a future with multiple accounts' data #5005, this solution will break down, at least in some areas:
I think what this may point toward is that we're going to want, for each account, a separate object that looks a lot like a Redux store. (Not sure yet if they should each be actual Redux stores, or some sort of proxy for a single global actual Redux store.) That is, we'll want an object that:
I say "a method like One thing that might point us toward having a single actual Redux store, and the per-account versions as proxy views on that global store, is if we have much code that with separate stores would need to look at several different stores at once. But there's very little code that acts on several accounts' data at once; the main example that comes to mind is AccountPickScreen, but even there it's mostly a matter of having several Another thing that might point toward a single actual store is that separate stores might require more complex management for persistence: if we simply have separate Redux stores each running redux-persist, then they're each separately rehydrating on startup, each separately doing migrations, and each separately making transactional writes. This might be dispositive that we want to keep a single global actual store, at least initially -- I'd rather not have to worry about added complexity there while we're making a bunch of other complex changes. If we do have a single actual store, and per-account proxies for it, then TBD what interface exactly those proxies provide, and how they're implemented -- this will require some more thinking and experimentation. |
I've done some more thinking about what this internal API for the app code to look like, and then I spent yesterday drafting up a branch to move toward that API. I'll start sending PRs from the work in that branch. At present it's 120 commits long, though some of those changes are experiments we won't need and some will get squashed together in cleaning it up. In addition to branch cleanups, it needs more jsdoc and comments and probably some tests. The basic thrust of what's in the branch is to separate which parts of our code will want to be dealing with one of these store-like objects that's bound to a specific account's data, and which really will want to be dealing with the global store. In the branch, we don't yet change any of the actual data structures… but we distinguish per-account vs. global types, even though secretly we're passing the same values around under both types. That lets us use the type-checker to trace how the data flows around to ensure that we're consistent in which code gets which kind of type, and to identify exactly which spots of code rely on assumptions that won't stay true in the future where these objects are actually distinct. In particular:
|
This fixes a regression introduced in 3b811d6 that made it impossible to log into a new account, by crashing just after completing authentication with the message "zulipVersion must be non-null". Oops: getHaveServerData was not an OK place to call getServerVersion. It has the following invariant: // This invariant will hold as long as we only call this function in a // context where we have server data. // // TODO(zulip#5006): […] invariant(zulipVersion, 'zulipVersion must be non-null'); So, abandon that selector and access zulipVersion safely. While I was at it, I double-checked `Account['zulipVersion']` and `Account['zulipFeatureLevel']` …and found that their jsdocs weren't right about when those fields were null. Fix that, copying some text from the jsdoc of `Account['userId']`.
We'd like to store server data for all the accounts the user is logged in as, #5005, instead of just a single "active account".
As part of that, we'll need to arrange the data in a way that makes room for data from multiple accounts, both in Redux and in the store that persists between runs of the app.
This issue is for developing that new pair of schemas and migrating to it, without yet making any changes to what data we actually store.
I'll add in a comment some specific design thoughts we have so far on what that might look like.
The text was updated successfully, but these errors were encountered: