diff --git a/lib/reducers/integrity-reducer.js b/lib/reducers/integrity-reducer.js index ddb3e6906..b277c176c 100644 --- a/lib/reducers/integrity-reducer.js +++ b/lib/reducers/integrity-reducer.js @@ -1,204 +1,185 @@ // @flow import { setClientDBStoreActionType } from '../actions/client-db-store-actions.js'; import { updateIntegrityStoreActionType } from '../actions/integrity-actions.js'; import { legacySiweAuthActionTypes } from '../actions/siwe-actions.js'; import { fetchPendingUpdatesActionTypes } from '../actions/update-actions.js'; import { keyserverAuthActionTypes, legacyLogInActionTypes, legacyKeyserverRegisterActionTypes, } from '../actions/user-actions.js'; import { extractKeyserverIDFromID } from '../keyserver-conn/keyserver-call-utils.js'; import { integrityStoreOpsHandlers, type IntegrityStoreOperation, } from '../ops/integrity-store-ops.js'; import type { ThreadStoreOperation } from '../ops/thread-store-ops'; import type { IntegrityStore, ThreadHashes } from '../types/integrity-types'; import type { RawThreadInfo } from '../types/minimally-encoded-thread-permissions-types.js'; import type { BaseAction } from '../types/redux-types.js'; import { fullStateSyncActionType, stateSyncPayloadTypes, } from '../types/socket-types.js'; -import { getMessageForException } from '../utils/errors.js'; -import { assertObjectsAreEqual, hash } from '../utils/objects.js'; +import { hash } from '../utils/objects.js'; const { processStoreOperations: processStoreOps } = integrityStoreOpsHandlers; -function assertIntegrityStoresAreEqual( - processedIntegrityStore: ThreadHashes, - expectedIntegrityStore: ThreadHashes, - location: string, - onStateDifference?: (message: string) => mixed, -) { - try { - assertObjectsAreEqual( - processedIntegrityStore, - expectedIntegrityStore, - `ThreadHashes - ${location}`, - ); - } catch (e) { - console.log( - 'Error processing IntegrityStore ops', - processedIntegrityStore, - expectedIntegrityStore, - ); - const message = `Error processing IntegrityStore ops ${ - getMessageForException(e) ?? '{no exception message}' - }`; - onStateDifference?.(message); - } -} - function reduceIntegrityStore( state: IntegrityStore, action: BaseAction, - onStateDifference?: (message: string) => mixed, threadInfos: { +[string]: RawThreadInfo, }, threadStoreOperations: $ReadOnlyArray, ): { +integrityStore: IntegrityStore, +integrityStoreOperations: $ReadOnlyArray, } { if ( action.type === fullStateSyncActionType || (action.type === fetchPendingUpdatesActionTypes.success && action.payload.type === stateSyncPayloadTypes.FULL) ) { const removeAllOperation = { type: 'remove_all_integrity_thread_hashes' }; const threadHashesArray = Object.entries(state.threadHashes).filter( ([key]) => extractKeyserverIDFromID(key) !== action.payload.keyserverID, ); const replaceOperation = { type: 'replace_integrity_thread_hashes', payload: { threadHashes: Object.fromEntries(threadHashesArray) }, }; return { integrityStore: { threadHashes: processStoreOps(state, [ removeAllOperation, replaceOperation, ]).threadHashes, threadHashingStatus: 'starting', }, integrityStoreOperations: [removeAllOperation, replaceOperation], }; } else if ( action.type === legacyLogInActionTypes.success || action.type === legacySiweAuthActionTypes.success || action.type === legacyKeyserverRegisterActionTypes.success || (action.type === setClientDBStoreActionType && !!action.payload.threadStore && state.threadHashingStatus !== 'completed') ) { const removeAllOperation = { type: 'remove_all_integrity_thread_hashes' }; return { integrityStore: { threadHashes: processStoreOps(state, [removeAllOperation]).threadHashes, threadHashingStatus: 'starting', }, integrityStoreOperations: [removeAllOperation], }; } else if (action.type === keyserverAuthActionTypes.success) { return { integrityStore: { threadHashes: processStoreOps(state, []).threadHashes, threadHashingStatus: 'starting', }, integrityStoreOperations: [], }; } else if (action.type === setClientDBStoreActionType) { - assertIntegrityStoresAreEqual( - action.payload.threadHashes ?? {}, - state.threadHashes, - action.type, - onStateDifference, - ); + const newThreadHashes = action.payload.threadHashes; + + if (!newThreadHashes) { + return { + integrityStore: state, + integrityStoreOperations: [], + }; + } + + const newIntegrityStore: IntegrityStore = { + ...state, + threadHashes: newThreadHashes, + }; + return { - integrityStore: state, + integrityStore: newIntegrityStore, integrityStoreOperations: [], }; } let newState = state; const integrityOperations: IntegrityStoreOperation[] = []; if (action.type === updateIntegrityStoreActionType) { if (action.payload.threadIDsToHash) { const newThreadHashes = Object.fromEntries( action.payload.threadIDsToHash .map(id => [id, threadInfos[id]]) .filter(([, info]) => !!info) .map(([id, info]) => [id, hash(info)]), ); const replaceOperation = { type: 'replace_integrity_thread_hashes', payload: { threadHashes: newThreadHashes }, }; newState = processStoreOps(state, [replaceOperation]); integrityOperations.push(replaceOperation); } if (action.payload.threadHashingStatus) { newState = { ...newState, threadHashingStatus: action.payload.threadHashingStatus, }; } } if (threadStoreOperations.length === 0) { return { integrityStore: newState, integrityStoreOperations: integrityOperations, }; } let groupedReplaceThreadHashes: ThreadHashes = {}; let threadHashingStatus = newState.threadHashingStatus; for (const operation of threadStoreOperations) { if ( operation.type !== 'replace' && Object.keys(groupedReplaceThreadHashes).length > 0 ) { integrityOperations.push({ type: 'replace_integrity_thread_hashes', payload: { threadHashes: groupedReplaceThreadHashes }, }); groupedReplaceThreadHashes = {}; } if (operation.type === 'replace') { const newIntegrityThreadHash = hash(operation.payload.threadInfo); groupedReplaceThreadHashes = { ...groupedReplaceThreadHashes, [operation.payload.id]: newIntegrityThreadHash, }; } else if (operation.type === 'remove') { integrityOperations.push({ type: 'remove_integrity_thread_hashes', payload: { ids: operation.payload.ids }, }); } else if (operation.type === 'remove_all') { integrityOperations.push({ type: 'remove_all_integrity_thread_hashes' }); threadHashingStatus = 'completed'; } } if (Object.keys(groupedReplaceThreadHashes).length > 0) { integrityOperations.push({ type: 'replace_integrity_thread_hashes', payload: { threadHashes: groupedReplaceThreadHashes }, }); } return { integrityStore: { threadHashes: processStoreOps(newState, integrityOperations).threadHashes, threadHashingStatus, }, integrityStoreOperations: integrityOperations, }; } export { reduceIntegrityStore }; diff --git a/lib/reducers/integrity-reducer.test.js b/lib/reducers/integrity-reducer.test.js index cc995ec5b..d8e61a29f 100644 --- a/lib/reducers/integrity-reducer.test.js +++ b/lib/reducers/integrity-reducer.test.js @@ -1,228 +1,224 @@ // @flow import { reduceIntegrityStore } from './integrity-reducer.js'; import { updateIntegrityStoreActionType } from '../actions/integrity-actions.js'; import type { ThreadStoreOperation } from '../ops/thread-store-ops'; import { type ThreadHashes } from '../types/integrity-types.js'; import type { RawThreadInfo } from '../types/minimally-encoded-thread-permissions-types.js'; import { hash } from '../utils/objects.js'; jest.mock('../utils/config.js'); const currentThreadHashes: ThreadHashes = { '256|2204191': 1029852, '256|2205980': 3119392, '256|2208693': 4157082, '256|2212631': 8951764, }; const threadIDToUpdate = '256|2210486'; const threadIDsToUpdateList = [threadIDToUpdate]; type ThreadInfos = { +[string]: RawThreadInfo, }; const threadInfos: ThreadInfos = { [threadIDToUpdate]: { currentUser: { role: '256|83795', permissions: '3', minimallyEncoded: true, unread: true, subscription: { pushNotifs: true, home: true, }, }, id: threadIDToUpdate, type: 12, name: 'GENESIS', description: '', color: '648caa', creationTime: 1689091732528, parentThreadID: null, repliesCount: 0, containingThreadID: null, community: null, pinnedCount: 0, minimallyEncoded: true, members: [ { id: '256', role: '256|83796', isSender: true, minimallyEncoded: true, }, { id: '83810', role: '256|83795', isSender: false, minimallyEncoded: true, }, ], roles: { '256|83795': { id: '256|83795', name: 'Members', permissions: ['000', '010', '005', '015', '0a7'], minimallyEncoded: true, }, '256|83796': { id: '256|83796', name: 'Admins', permissions: ['000', '010', '005', '015', '0a7'], minimallyEncoded: true, }, }, }, }; const threadHashToUpdate = hash(threadInfos[threadIDToUpdate]); describe('reduceIntegrityStore', () => { it('should update integrity store with new thread hash', () => { const oldIntegrityStore = { threadHashes: currentThreadHashes, threadHashingStatus: 'completed', }; const updateThreadHashesAction = { type: updateIntegrityStoreActionType, payload: { threadIDsToHash: threadIDsToUpdateList }, }; expect( reduceIntegrityStore( oldIntegrityStore, updateThreadHashesAction, - () => null, threadInfos, [], ).integrityStore, ).toEqual({ threadHashes: { ...currentThreadHashes, [threadIDToUpdate]: threadHashToUpdate, }, threadHashingStatus: 'completed', }); }); it('should update integrity store with new thread hash', () => { const oldIntegrityStore = { threadHashes: currentThreadHashes, threadHashingStatus: 'completed', }; const updateThreadHashesAction = { type: updateIntegrityStoreActionType, payload: { threadHashingStatus: 'completed' }, }; const threadStoreOperations: Array = [ { type: 'replace', payload: { id: threadIDToUpdate, threadInfo: threadInfos[threadIDToUpdate], }, }, ]; expect( reduceIntegrityStore( oldIntegrityStore, updateThreadHashesAction, - () => null, threadInfos, threadStoreOperations, ).integrityStore, ).toEqual({ threadHashes: { ...currentThreadHashes, [threadIDToUpdate]: threadHashToUpdate, }, threadHashingStatus: 'completed', }); }); it('should remove two thread hashes', () => { const oldIntegrityStore = { threadHashes: currentThreadHashes, threadHashingStatus: 'completed', }; const thread_ids_remove = ['256|2204191', '256|2205980']; const thread_hashes_after_removal: ThreadHashes = { '256|2208693': 4157082, '256|2212631': 8951764, }; const updateThreadHashesAction = { type: updateIntegrityStoreActionType, payload: { threadHashingStatus: 'completed' }, }; const threadStoreOperations: Array = [ { type: 'remove', payload: { ids: thread_ids_remove, }, }, ]; expect( reduceIntegrityStore( oldIntegrityStore, updateThreadHashesAction, - () => null, threadInfos, threadStoreOperations, ).integrityStore, ).toEqual({ threadHashes: thread_hashes_after_removal, threadHashingStatus: 'completed', }); }); it('should clear thread hashes and update with a single thread hash', () => { const oldIntegrityStore = { threadHashes: currentThreadHashes, threadHashingStatus: 'completed', }; const updateThreadHashesAction = { type: updateIntegrityStoreActionType, payload: { threadHashingStatus: 'completed' }, }; const threadStoreOperations: Array = [ { type: 'remove_all', }, { type: 'replace', payload: { id: threadIDToUpdate, threadInfo: threadInfos[threadIDToUpdate], }, }, ]; expect( reduceIntegrityStore( oldIntegrityStore, updateThreadHashesAction, - () => null, threadInfos, threadStoreOperations, ).integrityStore, ).toEqual({ threadHashes: { [threadIDToUpdate]: threadHashToUpdate }, threadHashingStatus: 'completed', }); }); }); diff --git a/lib/reducers/master-reducer.js b/lib/reducers/master-reducer.js index ad8b3456f..24cf83afb 100644 --- a/lib/reducers/master-reducer.js +++ b/lib/reducers/master-reducer.js @@ -1,246 +1,245 @@ // @flow import { reduceAlertStore } from './alert-reducer.js'; import { reduceAuxUserStore } from './aux-user-reducer.js'; import reduceCalendarFilters from './calendar-filters-reducer.js'; import { reduceCommunityStore } from './community-reducer.js'; import reduceCustomerServer from './custom-server-reducer.js'; import reduceDataLoaded from './data-loaded-reducer.js'; import { reduceDBOpsStore } from './db-ops-reducer.js'; import { reduceDraftStore } from './draft-reducer.js'; import reduceEnabledApps from './enabled-apps-reducer.js'; import { reduceEntryInfos } from './entry-reducer.js'; import { reduceIntegrityStore } from './integrity-reducer.js'; import reduceInviteLinks from './invite-links-reducer.js'; import reduceKeyserverStore from './keyserver-reducer.js'; import reduceLifecycleState from './lifecycle-state-reducer.js'; import { reduceLoadingStatuses } from './loading-reducer.js'; import { reduceMessageStore } from './message-reducer.js'; import reduceBaseNavInfo from './nav-reducer.js'; import policiesReducer from './policies-reducer.js'; import reduceReportStore from './report-store-reducer.js'; import { reduceSyncedMetadataStore } from './synced-metadata-reducer.js'; import reduceGlobalThemeInfo from './theme-reducer.js'; import { reduceThreadActivity } from './thread-activity-reducer.js'; import { reduceThreadInfos } from './thread-reducer.js'; import reduceTunnelbrokerDeviceToken from './tunnelbroker-device-token-reducer.js'; import { reduceCurrentUserInfo, reduceUserInfos } from './user-reducer.js'; import { addKeyserverActionType } from '../actions/keyserver-actions.js'; import { legacySiweAuthActionTypes } from '../actions/siwe-actions.js'; import { fetchPendingUpdatesActionTypes } from '../actions/update-actions.js'; import { legacyKeyserverRegisterActionTypes, legacyLogInActionTypes, keyserverAuthActionTypes, } from '../actions/user-actions.js'; import { keyserverStoreOpsHandlers, type ReplaceKeyserverOperation, } from '../ops/keyserver-store-ops.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 { fullStateSyncActionType, incrementalStateSyncActionType, } from '../types/socket-types.js'; import type { StoreOperations } from '../types/store-ops-types.js'; import { isDev } from '../utils/dev-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); const { threadInfos } = threadStore; const { entryStore, reportCreationRequests: newEntryInconsistencies, entryStoreOperations, } = reduceEntryInfos(state.entryStore, action, threadInfos); const onStateDifferenceForStaff = (message: string) => { const isCurrentUserStaff = state.currentUserInfo?.id ? isStaff(state.currentUserInfo.id) : false; if (isCurrentUserStaff || isDev) { onStateDifference(message); } }; const [userStore, newUserInconsistencies, userStoreOperations] = reduceUserInfos(state.userStore, action); const newInconsistencies = [ ...newEntryInconsistencies, ...newThreadInconsistencies, ...newUserInconsistencies, ]; // Only allow checkpoints to increase if we are connected // or if the action is a STATE_SYNC const { messageStoreOperations, messageStore: reducedMessageStore } = reduceMessageStore( state.messageStore, action, threadInfos, onStateDifferenceForStaff, ); let messageStore = reducedMessageStore; let { keyserverStore, keyserverStoreOperations } = reduceKeyserverStore( state.keyserverStore, action, onStateDifferenceForStaff, ); if ( action.type !== incrementalStateSyncActionType && action.type !== fullStateSyncActionType && action.type !== fetchPendingUpdatesActionTypes.success && action.type !== legacyKeyserverRegisterActionTypes.success && action.type !== legacyLogInActionTypes.success && action.type !== legacySiweAuthActionTypes.success && action.type !== keyserverAuthActionTypes.success && action.type !== addKeyserverActionType ) { const replaceOperations: ReplaceKeyserverOperation[] = []; for (const keyserverID in keyserverStore.keyserverInfos) { if ( keyserverStore.keyserverInfos[keyserverID].connection.status === 'connected' ) { continue; } if ( messageStore.currentAsOf[keyserverID] !== state.messageStore.currentAsOf[keyserverID] ) { messageStore = { ...messageStore, currentAsOf: { ...messageStore.currentAsOf, [keyserverID]: state.messageStore.currentAsOf[keyserverID], }, }; } if ( state.keyserverStore.keyserverInfos[keyserverID] && keyserverStore.keyserverInfos[keyserverID].updatesCurrentAsOf !== state.keyserverStore.keyserverInfos[keyserverID].updatesCurrentAsOf ) { replaceOperations.push({ type: 'replace_keyserver', payload: { id: keyserverID, keyserverInfo: { ...keyserverStore.keyserverInfos[keyserverID], updatesCurrentAsOf: state.keyserverStore.keyserverInfos[keyserverID] .updatesCurrentAsOf, }, }, }); } } keyserverStore = keyserverStoreOpsHandlers.processStoreOperations( keyserverStore, replaceOperations, ); keyserverStoreOperations = [ ...keyserverStoreOperations, ...replaceOperations, ]; } const { draftStore, draftStoreOperations } = reduceDraftStore( state.draftStore, action, ); const { reportStore, reportStoreOperations } = reduceReportStore( state.reportStore, action, newInconsistencies, ); const { communityStore, communityStoreOperations } = reduceCommunityStore( state.communityStore, action, ); const { integrityStore, integrityStoreOperations } = reduceIntegrityStore( state.integrityStore, action, - onStateDifferenceForStaff, threadInfos, threadStoreOperations, ); const { syncedMetadataStore, syncedMetadataStoreOperations } = reduceSyncedMetadataStore(state.syncedMetadataStore, action); const { auxUserStore, auxUserStoreOperations } = reduceAuxUserStore( state.auxUserStore, action, ); const { threadActivityStore, threadActivityStoreOperations } = reduceThreadActivity(state.threadActivityStore, action); return { state: { ...state, navInfo: reduceBaseNavInfo(state.navInfo, action), draftStore, entryStore, loadingStatuses: reduceLoadingStatuses(state.loadingStatuses, action), currentUserInfo: reduceCurrentUserInfo(state.currentUserInfo, action), threadStore, userStore, messageStore, calendarFilters: reduceCalendarFilters( state.calendarFilters, action, threadStore, ), alertStore: reduceAlertStore(state.alertStore, action), lifecycleState: reduceLifecycleState(state.lifecycleState, action), enabledApps: reduceEnabledApps(state.enabledApps, action), reportStore, dataLoaded: reduceDataLoaded(state.dataLoaded, action), userPolicies: policiesReducer(state.userPolicies, action), inviteLinksStore: reduceInviteLinks(state.inviteLinksStore, action), keyserverStore, integrityStore, globalThemeInfo: reduceGlobalThemeInfo(state.globalThemeInfo, action), customServer: reduceCustomerServer(state.customServer, action), communityStore, dbOpsStore: reduceDBOpsStore(state.dbOpsStore, action), syncedMetadataStore, auxUserStore, threadActivityStore, tunnelbrokerDeviceToken: reduceTunnelbrokerDeviceToken( state.tunnelbrokerDeviceToken, action, ), }, storeOperations: { draftStoreOperations, threadStoreOperations, messageStoreOperations, reportStoreOperations, userStoreOperations, keyserverStoreOperations, communityStoreOperations, integrityStoreOperations, syncedMetadataStoreOperations, auxUserStoreOperations, threadActivityStoreOperations, entryStoreOperations, }, }; } diff --git a/native/redux/handle-redux-migration-failure.js b/native/redux/handle-redux-migration-failure.js index acf49d189..4ab11df2a 100644 --- a/native/redux/handle-redux-migration-failure.js +++ b/native/redux/handle-redux-migration-failure.js @@ -1,42 +1,43 @@ // @flow import { wipeKeyserverStore } from 'lib/utils/keyserver-store-utils.js'; import { resetUserSpecificState } from 'lib/utils/reducers-utils.js'; import { defaultState } from './default-state.js'; import { nonUserSpecificFieldsNative, type AppState } from './state-types.js'; const persistBlacklist = [ 'loadingStatuses', 'lifecycleState', 'dimensions', 'draftStore', 'connectivity', 'deviceOrientation', 'frozen', 'threadStore', 'storeLoaded', 'dbOpsStore', 'syncedMetadataStore', 'userStore', 'auxUserStore', 'commServicesAccessToken', 'inviteLinksStore', + 'integrityStore', ]; function handleReduxMigrationFailure(oldState: AppState): AppState { const persistedNonUserSpecificFields = nonUserSpecificFieldsNative.filter( field => !persistBlacklist.includes(field) || field === '_persist', ); const stateAfterReset = resetUserSpecificState( oldState, defaultState, persistedNonUserSpecificFields, ); return { ...stateAfterReset, keyserverStore: wipeKeyserverStore(stateAfterReset.keyserverStore), }; } export { persistBlacklist, handleReduxMigrationFailure };