-
-
Notifications
You must be signed in to change notification settings - Fork 669
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
redux: Discard messages/narrows on REALM_INIT, not DEAD_QUEUE. #3822
redux: Discard messages/narrows on REALM_INIT, not DEAD_QUEUE. #3822
Conversation
This is WIP because, while my manual testing hasn't revealed any unintended side effects, this touches some central logic, so should be tested more thoroughly, and I have a few lingering questions about redux-persist (to be posted shortly). |
Specifically, from reading the code, it seems like redux-persist is used to rehydrate the Redux store with data from AsyncStorage, right? Are I'm not sure what the order is of the REHYDRATE action and our other actions, including the ones in doInitialFetch. 42adfb3 describes createActionBuffer as a way to postpone other actions so REHYDRATE always happens first, but I'm not sure that's exactly what it does. From the doc:
Our breaker condition is the REHYDRATE action, but from that description, it doesn't seem like there's any reordering of the queue (e.g., for our case, to put REHYDRATE at the front), only that the queue doesn't start emptying until the breaker condition is met (for us, when REHYDRATE is dispatched -- potentially with other actions already ahead of it in the queue). Does this seem right, and if so, might it matter (either in general or for this PR)? |
0f14ddb
to
894d1a4
Compare
(Just made some edits to the commit message.) |
We discussed these questions yesterday in person, and I think I answered them then. I've also just sent #3907 which adds a bunch of jsdoc and other comments to |
Ah, we were writing comments at the same time! 🙂 I'll post mine anyway, just to make sure I've understood correctly.
This makes more sense, after discussing in person. The REHYDRATE action marks the completion of retrieving the state from ZulipAsyncStorage, for the whitelisted fields specified in storeKeys and cacheKeys. It's clear from reading the source code that all of the actions that would have preceded REHYDRATE are entered into a queue, which is released after REHYDRATE (not before). In fact, I may have been a little too harsh on the docs yesterday — it turns out that the "breaker condition" can be anything, not just a Redux action. The fact that it happens to be an action for us made me think, ok, here's a library intended to manage a homogeneous queue of Redux actions (including REHYDRATE), but it just likes to break the definition of a queue, to suit some purpose, by reordering REHYDRATE to the front. In reality, the buffered actions are managed properly in a queue; REHYDRATE was just never considered part of the queue in the first place. |
Thanks @chrisbobbe! I just reread this PR, in light of #3900. (Which, for self-containedness of this thread, was this:
) The main question in my mind was: after this PR, when you open the app with an expired event queue, what's the sequence of changes that would affect whether we show "No messages"? And should we expect that there will still be a stage at which we do show that? I think the answer is: this change narrows that window by a lot, but doesn't quite close it; and there's an additional tweak we can do that will then close it. Specifically, this is what now decides when that's shown (in const isFetching = fetching.older || fetching.newer;
const showMessagePlaceholders = haveNoMessages && isFetching;
const sayNoMessages = haveNoMessages && !isFetching; where The issue this PR is aimed at is basically that we're making The PR works by delaying the point when export const doInitialFetch = () => async (dispatch: Dispatch, getState: GetState) => {
// ...
dispatch(initialFetchStart());
const auth = getAuth(getState());
// ...
initData = await tryUntilSuccessful(() =>
api.registerForEvents(auth, {
fetch_event_types: config.serverDataOnStartup,
apply_markdown: true,
include_subscribers: false,
client_gravatar: true,
client_capabilities: {
notification_settings_null: true,
},
}),
);
dispatch(realmInit(initData));
dispatch(fetchTopMostNarrow());
dispatch(initialFetchComplete()); it moves to the Just after the Hmm, though perhaps it's not actually possible for Still, even if that turns out to be the case, the whole chain of reasoning feels a bit fragile -- dependent on fine details of the sequence of calls in I think a good fix would be to augment the definition of |
A separate (simpler 🙂 ) question I looked at this to answer: is this acting together on all the data that's closely related and we want to act together on? So I went and looked at * These `Message` objects lack the `flags` property; we store that
* information separately, as `FlagsState`.
*
* See also `NarrowsState`, which is an index on this data that identifies
* messages belonging to a given narrow.
*/
export type MessagesState = {|
// ...
* See also:
* * `MessagesState`, which stores the message data indexed by ID;
* * `CaughtUpState` for information about where this data *is*
* complete for a given narrow;
* * `FetchingState` for information about which narrows we're actively
* fetching more messages from.
*/
export type NarrowsState = { So I think at least And then also As for |
894d1a4
to
4e7cc12
Compare
All good points, thanks for explaining all of that! I've made changes to address these things. |
When we have no messages to show (i.e. `haveNoMessages` is true) but we're in the middle of loading a fresh copy of the overall app state from the server (i.e. `state.session.loading` true), it's more informative to emphasize the "we're loading messages" rather than "we have no messages." Do that. See further discussion here: zulip#3822 (comment) The window that this closes will be much smaller following the next commit, but it's still worth closing. And while this will hide *some* stale data that users might want to see while waiting for a refresh, it won't hide much. In particular, showMessagePlaceholders won't be true unless haveNoMessages is also true, and it's rare to be looking at a MessageList that has no messages.
…D_QUEUE. When returning to the app after the event queue has expired, there's a moment after stale data is purged when the app incorrectly believes that an empty message list is the up-to-date state, and "No messages! Why not start the conversation?" displays until fresh data arrives. Currently, state.messages, state.narrows, state.flags, and state.caughtUp are emptied on the DEAD_QUEUE action, which is dispatched before the /register request. Instead, preserve the stale data until /register completes, clearing it in response to REALM_INIT. In doInitialFetch: fetchTopMostNarrow, fetchPrivateMessages, and (sometimes) fetchMessagesInNarrow are also dispatched after /register to ensure the stale data is refreshed. All of these already handle their loading states correctly, on a per-narrow basis in state.fetching. Clearing the data in MESSAGE_FETCH_START instead of REALM_INIT was considered but rejected because MESSAGE_FETCH_START is dispatched before the /register request. Fixes: zulip#3802
state.fetching should not be cleared on either REALM_INIT or DEAD_QUEUE because neither one halts an ongoing fetch. For flags, we noticed that messages, narrows, fetching, and caughtUp are all cleared on LOGIN_SUCCESS, and there isn't a clear reason why flags shouldn't be either.
4e7cc12
to
96f89d8
Compare
And merged -- thanks! |
Fixes: #3802.
When returning to the app after the event queue has expired, there's
a moment after stale data is purged when the app incorrectly
believes that an empty message list is the up-to-date state, and "No
messages! Why not start the conversation?" displays until fresh data
arrives.
Currently, state.messages and state.narrows are emptied on the
DEAD_QUEUE action, which is dispatched before the /register request.
Instead, preserve the stale data until /register completes, clearing
it in response to REALM_INIT.
In doInitialFetch, fetchTopMostNarrow, fetchPrivateMessages, and
(sometimes) fetchMessagesInNarrow are also dispatched after
/register to ensure the stale data is refreshed. All of these
already handle their loading states correctly, on a per-narrow basis
in state.fetching.
Clearing the messages in MESSAGE_FETCH_START instead of REALM_INIT
was considered but rejected because MESSAGE_FETCH_START is
dispatched before the /register request.