diff --git a/src/account/accountMisc.js b/src/account/accountMisc.js index 5f2252906ff..a246033bd43 100644 --- a/src/account/accountMisc.js +++ b/src/account/accountMisc.js @@ -10,7 +10,20 @@ export const identityOfAccount: Account => Identity = identitySlice; /** A string corresponding uniquely to an identity, for use in `Map`s. */ export const keyOfIdentity = ({ realm, email }: Identity): string => `${realm}\0${email}`; +const keyOfAuth = (auth: Auth): string => keyOfIdentity(identityOfAuth(auth)); + export const authOfAccount = (account: Account): Auth => { const { realm, email, apiKey } = account; return { realm, email, apiKey }; }; + +/** + * Takes two Auth objects and confirms that they are equal, either by + * 1.) strict equality + * 2.) equality of their identity key (realm + email) and API key + */ +export const authEquivalent = (auth1: Auth | void, auth2: Auth | void): boolean => + auth1 === auth2 + || (auth1 !== undefined + && auth2 !== undefined + && (auth1.apiKey === auth2.apiKey && keyOfAuth(auth1) === keyOfAuth(auth2))); diff --git a/src/events/eventActions.js b/src/events/eventActions.js index 09b4f49c345..a82200f61de 100644 --- a/src/events/eventActions.js +++ b/src/events/eventActions.js @@ -1,7 +1,7 @@ /* @flow strict-local */ import { batchActions } from 'redux-batched-actions'; -import type { Action, Dispatch, GeneralEvent, GetState, GlobalState } from '../types'; +import type { Auth, Action, Dispatch, GeneralEvent, GetState, GlobalState } from '../types'; import * as api from '../api'; import { logout } from '../account/accountActions'; import { deadQueue } from '../session/sessionActions'; @@ -11,6 +11,7 @@ import { tryGetAuth } from '../selectors'; import actionCreator from '../actionCreator'; import { BackoffMachine } from '../utils/async'; import { ApiError } from '../api/apiErrors'; +import { authEquivalent } from '../account/accountMisc'; /** Convert an `/events` response into a sequence of our Redux actions. */ export const responseToActions = ( @@ -44,12 +45,13 @@ export const dispatchOrBatch = (dispatch: Dispatch, actions: $ReadOnlyArray async ( +export const startEventPolling = (auth: Auth, queueId: number, eventId: number) => async ( dispatch: Dispatch, getState: GetState, ) => { @@ -59,17 +61,19 @@ export const startEventPolling = (queueId: number, eventId: number) => async ( // eslint-disable-next-line no-constant-condition while (true) { - const auth = tryGetAuth(getState()); - if (!auth) { - // User switched accounts or logged out + if (!authEquivalent(auth, tryGetAuth(getState()))) { + // The user logged out or switched accounts during progressiveTimeout + // called from see catch block, below. (If tryGetAuth returns undefined, + // the user is not logged in.) break; } try { const { events } = await api.pollForEvents(auth, queueId, lastEventId); - // User switched accounts or logged out - if (queueId !== getState().session.eventQueueId) { + if (!authEquivalent(auth, tryGetAuth(getState()))) { + // The user logged out or switched accounts while api.pollForEvents was in progress. + // (If tryGetAuth returns undefined, the user is not logged in.) break; } @@ -86,14 +90,14 @@ export const startEventPolling = (queueId: number, eventId: number) => async ( break; } - // protection from inadvertent DDOS - await backoffMachine.wait(); - if (e instanceof ApiError && e.code === 'BAD_EVENT_QUEUE_ID') { // The event queue is too old or has been garbage collected. dispatch(deadQueue()); break; } + + // protection from inadvertent DOS + await backoffMachine.wait(); } } }; diff --git a/src/message/fetchActions.js b/src/message/fetchActions.js index d59c339a095..9f180b745d9 100644 --- a/src/message/fetchActions.js +++ b/src/message/fetchActions.js @@ -12,6 +12,7 @@ import type { InitialData } from '../api/initialDataTypes'; import * as api from '../api'; import { getAuth, + tryGetAuth, getSession, getFirstMessageId, getLastMessageId, @@ -252,7 +253,19 @@ export const doInitialFetch = () => async (dispatch: Dispatch, getState: GetStat dispatch(realmInit(initData, serverSettings.zulip_version)); dispatch(fetchTopMostNarrow()); dispatch(initialFetchComplete()); - dispatch(startEventPolling(initData.queue_id, initData.last_event_id)); + + const authNow = tryGetAuth(getState()); + if (authNow !== undefined) { + // The user may have logged out while api.registerForEvents was in progress. + // If so, don't start polling events. + + // TODO: If the user did log out during that time, other actions that are dispatched + // here (in doInitialFetch, after api.registerForEvents) will throw an error, + // which may be a cause of #3706! These include the calls to fetchTopMostNarrow, + // fetchPrivateMessages, and fetchMessagesInNarrow, because they all dispatch + // fetchMessages, which calls getAuth, which throws with 'Active account not logged in'. + dispatch(startEventPolling(authNow, initData.queue_id, initData.last_event_id)); + } dispatch(fetchPrivateMessages());