diff --git a/lib/reducers/master-reducer.js b/lib/reducers/master-reducer.js --- a/lib/reducers/master-reducer.js +++ b/lib/reducers/master-reducer.js @@ -27,6 +27,7 @@ registerActionTypes, logInActionTypes, } from '../actions/user-actions.js'; +import { isStaff } from '../shared/staff-utils.js'; import type { BaseNavInfo } from '../types/nav-types.js'; import type { BaseAppState, BaseAction } from '../types/redux-types.js'; import { @@ -34,11 +35,13 @@ incrementalStateSyncActionType, } from '../types/socket-types.js'; import type { StoreOperations } from '../types/store-ops-types.js'; +import { isDev } from '../utils/dev-utils.js'; import { ashoatKeyserverID } from '../utils/validation-utils.js'; export default function baseReducer>( state: T, action: BaseAction, + onStateDifference: (message: string) => mixed, ): { state: T, storeOperations: StoreOperations } { const { threadStore, newThreadInconsistencies, threadStoreOperations } = reduceThreadInfos(state.threadStore, action); @@ -50,9 +53,19 @@ threadInfos, ); + const onStateDifferenceForStaff = (message: string) => { + const isCurrentUserStaff = state.currentUserInfo?.id + ? isStaff(state.currentUserInfo.id) + : false; + if (isCurrentUserStaff || isDev) { + onStateDifference(message); + } + }; + const [userStore, newUserInconsistencies] = reduceUserInfos( state.userStore, action, + onStateDifferenceForStaff, ); const newInconsistencies = [ diff --git a/lib/reducers/user-reducer.js b/lib/reducers/user-reducer.js --- a/lib/reducers/user-reducer.js +++ b/lib/reducers/user-reducer.js @@ -33,6 +33,8 @@ import { processUpdatesActionType } from '../types/update-types.js'; import type { CurrentUserInfo, UserStore } from '../types/user-types.js'; import { setNewSessionActionType } from '../utils/action-utils.js'; +import { getMessageForException } from '../utils/errors.js'; +import { assertObjectsAreEqual } from '../utils/objects.js'; function reduceCurrentUserInfo( state: ?CurrentUserInfo, @@ -120,9 +122,32 @@ return state; } +// eslint-disable-next-line no-unused-vars +function assertUserStoresAreEqual( + processedUserStore: UserStore, + expectedUserStore: UserStore, + location: string, + onStateDifference: (message: string) => mixed, +) { + try { + assertObjectsAreEqual( + processedUserStore, + expectedUserStore, + `UserStore - ${location}`, + ); + } catch (e) { + const message = `Error processing UserStore ops ${ + getMessageForException(e) ?? '{no exception message}' + }`; + onStateDifference(message); + } +} + function reduceUserInfos( state: UserStore, action: BaseAction, + // eslint-disable-next-line no-unused-vars + onStateDifference: (message: string) => mixed, ): [UserStore, $ReadOnlyArray] { if ( action.type === joinThreadActionTypes.success || diff --git a/lib/utils/objects.js b/lib/utils/objects.js --- a/lib/utils/objects.js +++ b/lib/utils/objects.js @@ -119,8 +119,7 @@ const dataProcessedButNotExpected = deepDiff(processedObject, expectedObject); const dataExpectedButNotProcessed = deepDiff(expectedObject, processedObject); - invariant( - false, + throw new Error( `${message}: Objects should be equal.` + ` Data processed but not expected:` + ` ${JSON.stringify(dataProcessedButNotExpected)}` + diff --git a/native/redux/redux-debug-utils.js b/native/redux/redux-debug-utils.js new file mode 100644 --- /dev/null +++ b/native/redux/redux-debug-utils.js @@ -0,0 +1,9 @@ +// @flow + +import { Alert } from 'react-native'; + +function onStateDifference(message: string) { + Alert.alert('State difference found', message); +} + +export { onStateDifference }; diff --git a/native/redux/redux-setup.js b/native/redux/redux-setup.js --- a/native/redux/redux-setup.js +++ b/native/redux/redux-setup.js @@ -40,6 +40,7 @@ import { defaultState } from './default-state.js'; import { remoteReduxDevServerConfig } from './dev-tools.js'; import { persistConfig, setPersistor } from './persist.js'; +import { onStateDifference } from './redux-debug-utils.js'; import { processDBStoreOperations } from './redux-utils.js'; import type { AppState } from './state-types.js'; import { getGlobalNavContext } from '../navigation/icky-global.js'; @@ -220,7 +221,11 @@ } } - const baseReducerResult = baseReducer(state, (action: BaseAction)); + const baseReducerResult = baseReducer( + state, + (action: BaseAction), + onStateDifference, + ); state = baseReducerResult.state; const { storeOperations } = baseReducerResult; diff --git a/web/redux/redux-debug-utils.js b/web/redux/redux-debug-utils.js new file mode 100644 --- /dev/null +++ b/web/redux/redux-debug-utils.js @@ -0,0 +1,7 @@ +// @flow + +function onStateDifference(message: string) { + window.alert(message); +} + +export { onStateDifference }; diff --git a/web/redux/redux-setup.js b/web/redux/redux-setup.js --- a/web/redux/redux-setup.js +++ b/web/redux/redux-setup.js @@ -54,6 +54,7 @@ setPickledPrimaryAccount, } from './crypto-store-reducer.js'; import reduceNavInfo from './nav-reducer.js'; +import { onStateDifference } from './redux-debug-utils.js'; import { getVisibility } from './visibility.js'; import { getDatabaseModule } from '../database/database-module-provider.js'; import { activeThreadSelector } from '../selectors/nav-selectors.js'; @@ -200,7 +201,7 @@ action.type !== setPickledPrimaryAccount && action.type !== setPickledNotificationAccount ) { - const baseReducerResult = baseReducer(state, action); + const baseReducerResult = baseReducer(state, action, onStateDifference); state = baseReducerResult.state; const {