diff --git a/lib/shared/state-sync/current-user-state-sync-spec.js b/lib/shared/state-sync/current-user-state-sync-spec.js index d1d3129a8..11fdb94df 100644 --- a/lib/shared/state-sync/current-user-state-sync-spec.js +++ b/lib/shared/state-sync/current-user-state-sync-spec.js @@ -1,27 +1,40 @@ // @flow +import { createSelector } from 'reselect'; + import type { StateSyncSpec } from './state-sync-spec.js'; +import type { AppState } from '../../types/redux-types'; import { type CurrentUserInfo, currentUserInfoValidator, } from '../../types/user-types.js'; import { convertClientIDsToServerIDs } from '../../utils/conversion-utils.js'; +import { hash } from '../../utils/objects.js'; import { ashoatKeyserverID } from '../../utils/validation-utils.js'; export const currentUserStateSyncSpec: StateSyncSpec< CurrentUserInfo, CurrentUserInfo, void, > = Object.freeze({ hashKey: 'currentUserInfo', convertClientToServerInfos(info: CurrentUserInfo) { return convertClientIDsToServerIDs( ashoatKeyserverID, currentUserInfoValidator, info, ); }, findStoreInconsistencies() { return undefined; }, + selector: createSelector( + (state: AppState) => state.currentUserInfo, + currentUserInfo => ({ + ...currentUserStateSyncSpec, + getInfoHash: () => hash(currentUserInfo), + getAllInfosHash: () => hash(currentUserInfo), + getIDs: () => [], + }), + ), }); diff --git a/lib/shared/state-sync/entries-state-sync-spec.js b/lib/shared/state-sync/entries-state-sync-spec.js index 7c2d1e975..c61a7f3a5 100644 --- a/lib/shared/state-sync/entries-state-sync-spec.js +++ b/lib/shared/state-sync/entries-state-sync-spec.js @@ -1,92 +1,125 @@ // @flow import _isEqual from 'lodash/fp/isEqual.js'; +import { createSelector } from 'reselect'; import t from 'tcomb'; import type { StateSyncSpec } from './state-sync-spec.js'; import { type CalendarQuery, type RawEntryInfos, rawEntryInfoValidator, type RawEntryInfo, } from '../../types/entry-types.js'; +import type { AppState } from '../../types/redux-types.js'; import { reportTypes, type ClientEntryInconsistencyReportCreationRequest, } from '../../types/report-types.js'; import type { ProcessServerRequestAction } from '../../types/request-types.js'; import { actionLogger } from '../../utils/action-logger.js'; import { getConfig } from '../../utils/config.js'; import { convertClientIDsToServerIDs } from '../../utils/conversion-utils.js'; -import { values } from '../../utils/objects.js'; +import { values, combineUnorderedHashes, hash } from '../../utils/objects.js'; import { generateReportID } from '../../utils/report-utils.js'; import { sanitizeActionSecrets } from '../../utils/sanitization.js'; import { ashoatKeyserverID, tID } from '../../utils/validation-utils.js'; import { filterRawEntryInfosByCalendarQuery, serverEntryInfosObject, } from '../entry-utils.js'; export const entriesStateSyncSpec: StateSyncSpec< RawEntryInfos, RawEntryInfo, $ReadOnlyArray, > = Object.freeze({ hashKey: 'entryInfos', innerHashSpec: { hashKey: 'entryInfo', deleteKey: 'deleteEntryIDs', rawInfosKey: 'rawEntryInfos', }, convertClientToServerInfos( infos: RawEntryInfos, calendarQuery: CalendarQuery, ) { const filteredEntryInfos = filterRawEntryInfosByCalendarQuery( serverEntryInfosObject(values(infos)), calendarQuery, ); return convertClientIDsToServerIDs( ashoatKeyserverID, t.dict(tID, rawEntryInfoValidator), filteredEntryInfos, ); }, findStoreInconsistencies( action: ProcessServerRequestAction, beforeStateCheck: RawEntryInfos, afterStateCheck: RawEntryInfos, ) { const calendarQuery = action.payload.calendarQuery; // We don't want to bother reporting an inconsistency if it's just because // of extraneous EntryInfos (not within the current calendarQuery) on either // side const filteredBeforeResult = filterRawEntryInfosByCalendarQuery( serverEntryInfosObject(values(beforeStateCheck)), calendarQuery, ); const filteredAfterResult = filterRawEntryInfosByCalendarQuery( serverEntryInfosObject(values(afterStateCheck)), calendarQuery, ); if (_isEqual(filteredBeforeResult)(filteredAfterResult)) { return emptyArray; } return [ { type: reportTypes.ENTRY_INCONSISTENCY, platformDetails: getConfig().platformDetails, beforeAction: beforeStateCheck, action: sanitizeActionSecrets(action), calendarQuery, pushResult: afterStateCheck, lastActions: actionLogger.interestingActionSummaries, time: Date.now(), id: generateReportID(), }, ]; }, + selector: createSelector( + (state: AppState) => state.entryStore.entryInfos, + entryInfos => ({ + ...entriesStateSyncSpec, + getInfoHash: id => hash(entryInfos[`${ashoatKeyserverID}|${id}`]), + getAllInfosHash: calendarQuery => + getEntryInfosHash(entryInfos, calendarQuery), + getIDs: calendarQuery => getEntryIDs(entryInfos, calendarQuery), + }), + ), }); const emptyArray = []; + +function getEntryInfosHash( + entryInfos: RawEntryInfos, + calendarQuery: CalendarQuery, +) { + const filteredEntryInfos = filterRawEntryInfosByCalendarQuery( + serverEntryInfosObject(values(entryInfos)), + calendarQuery, + ); + + return combineUnorderedHashes(Object.values(filteredEntryInfos).map(hash)); +} + +function getEntryIDs(entryInfos: RawEntryInfos, calendarQuery: CalendarQuery) { + const filteredEntryInfos = filterRawEntryInfosByCalendarQuery( + serverEntryInfosObject(values(entryInfos)), + calendarQuery, + ); + + return Object.keys(filteredEntryInfos).map(id => id.split('|')[1]); +} diff --git a/lib/shared/state-sync/state-sync-spec.js b/lib/shared/state-sync/state-sync-spec.js index dab9175a1..2cf1d4ddd 100644 --- a/lib/shared/state-sync/state-sync-spec.js +++ b/lib/shared/state-sync/state-sync-spec.js @@ -1,23 +1,39 @@ // @flow import type { CalendarQuery } from '../../types/entry-types.js'; +import type { AppState } from '../../types/redux-types.js'; import type { ProcessServerRequestAction } from '../../types/request-types.js'; export type StateSyncSpec = { +hashKey: string, +innerHashSpec?: { +hashKey: string, +deleteKey: string, +rawInfosKey: string, +additionalDeleteCondition?: Info => boolean, }, +convertClientToServerInfos: ( infos: Infos, calendarQuery: CalendarQuery, ) => Infos, +findStoreInconsistencies: ( action: ProcessServerRequestAction, beforeStateCheck: Infos, afterStateCheck: Infos, ) => Inconsistencies, + +selector: ( + state: AppState, + ) => BoundStateSyncSpec, +}; + +// All ids specified here (getInfoHash and getIDs) are server ids. +// E.g. in the case of threadStore or entryStore the keyserver prefix +// needs to be handled additionaly +export type BoundStateSyncSpec = { + // If these function depend on background hashing that is still not complete + // they should return null, to indicate that the hashes aren't available yet + +getInfoHash: (id: string) => ?number, + +getAllInfosHash: (query: CalendarQuery) => ?number, + +getIDs: (query: CalendarQuery) => ?Array, + ...StateSyncSpec, }; diff --git a/lib/shared/state-sync/threads-state-sync-spec.js b/lib/shared/state-sync/threads-state-sync-spec.js index 1e90a7b71..36ce61cac 100644 --- a/lib/shared/state-sync/threads-state-sync-spec.js +++ b/lib/shared/state-sync/threads-state-sync-spec.js @@ -1,65 +1,83 @@ // @flow import _isEqual from 'lodash/fp/isEqual.js'; +import { createSelector } from 'reselect'; import t from 'tcomb'; import type { StateSyncSpec } from './state-sync-spec.js'; +import type { AppState } from '../../types/redux-types.js'; import { reportTypes, type ClientThreadInconsistencyReportCreationRequest, } from '../../types/report-types.js'; import type { ProcessServerRequestAction } from '../../types/request-types.js'; import { type RawThreadInfos, type RawThreadInfo, rawThreadInfoValidator, } from '../../types/thread-types.js'; import { actionLogger } from '../../utils/action-logger.js'; import { getConfig } from '../../utils/config.js'; import { convertClientIDsToServerIDs } from '../../utils/conversion-utils.js'; +import { combineUnorderedHashes, values } from '../../utils/objects.js'; import { generateReportID } from '../../utils/report-utils.js'; import { sanitizeActionSecrets } from '../../utils/sanitization.js'; import { ashoatKeyserverID, tID } from '../../utils/validation-utils.js'; export const threadsStateSyncSpec: StateSyncSpec< RawThreadInfos, RawThreadInfo, $ReadOnlyArray, > = Object.freeze({ hashKey: 'threadInfos', innerHashSpec: { hashKey: 'threadInfo', deleteKey: 'deleteThreadIDs', rawInfosKey: 'rawThreadInfos', }, convertClientToServerInfos(infos: RawThreadInfos) { return convertClientIDsToServerIDs( ashoatKeyserverID, t.dict(tID, rawThreadInfoValidator), infos, ); }, findStoreInconsistencies( action: ProcessServerRequestAction, beforeStateCheck: RawThreadInfos, afterStateCheck: RawThreadInfos, ) { if (_isEqual(beforeStateCheck)(afterStateCheck)) { return emptyArray; } return [ { type: reportTypes.THREAD_INCONSISTENCY, platformDetails: getConfig().platformDetails, beforeAction: beforeStateCheck, action: sanitizeActionSecrets(action), pushResult: afterStateCheck, lastActions: actionLogger.interestingActionSummaries, time: Date.now(), id: generateReportID(), }, ]; }, + selector: createSelector( + (state: AppState) => state.integrityStore.threadHashes, + (state: AppState) => + state.integrityStore.threadHashingStatus === 'completed', + (threadHashes, threadHashingComplete) => ({ + ...threadsStateSyncSpec, + getInfoHash: id => threadHashes[`${ashoatKeyserverID}|${id}`], + getAllInfosHash: threadHashingComplete + ? () => combineUnorderedHashes(values(threadHashes)) + : () => null, + getIDs: threadHashingComplete + ? () => Object.keys(threadHashes).map(id => id.split('|')[1]) + : () => null, + }), + ), }); const emptyArray = []; diff --git a/lib/shared/state-sync/users-state-sync-spec.js b/lib/shared/state-sync/users-state-sync-spec.js index 26d049da9..5e13df419 100644 --- a/lib/shared/state-sync/users-state-sync-spec.js +++ b/lib/shared/state-sync/users-state-sync-spec.js @@ -1,67 +1,80 @@ // @flow import _isEqual from 'lodash/fp/isEqual.js'; +import { createSelector } from 'reselect'; import type { StateSyncSpec } from './state-sync-spec.js'; +import type { AppState } from '../../types/redux-types'; import { reportTypes, type UserInconsistencyReportCreationRequest, } from '../../types/report-types.js'; import type { ProcessServerRequestAction } from '../../types/request-types.js'; import { type UserInfo, type UserInfos, userInfosValidator, } from '../../types/user-types.js'; import { actionLogger } from '../../utils/action-logger.js'; import { getConfig } from '../../utils/config.js'; import { convertClientIDsToServerIDs } from '../../utils/conversion-utils.js'; +import { combineUnorderedHashes, hash } from '../../utils/objects.js'; import { generateReportID } from '../../utils/report-utils.js'; import { sanitizeActionSecrets } from '../../utils/sanitization.js'; import { ashoatKeyserverID } from '../../utils/validation-utils.js'; export const usersStateSyncSpec: StateSyncSpec< UserInfos, UserInfo, $ReadOnlyArray, > = Object.freeze({ hashKey: 'userInfos', innerHashSpec: { hashKey: 'userInfo', deleteKey: 'deleteUserInfoIDs', rawInfosKey: 'userInfos', additionalDeleteCondition(user: UserInfo) { return !user.username; }, }, convertClientToServerInfos(infos: UserInfos) { return convertClientIDsToServerIDs( ashoatKeyserverID, userInfosValidator, infos, ); }, findStoreInconsistencies( action: ProcessServerRequestAction, beforeStateCheck: UserInfos, afterStateCheck: UserInfos, ) { if (_isEqual(beforeStateCheck)(afterStateCheck)) { return emptyArray; } return [ { type: reportTypes.USER_INCONSISTENCY, platformDetails: getConfig().platformDetails, action: sanitizeActionSecrets(action), beforeStateCheck, afterStateCheck, lastActions: actionLogger.interestingActionSummaries, time: Date.now(), id: generateReportID(), }, ]; }, + selector: createSelector( + (state: AppState) => state.userStore.userInfos, + userInfos => ({ + ...usersStateSyncSpec, + getInfoHash: id => hash(userInfos[id]), + getAllInfosHash: () => + combineUnorderedHashes(Object.values(userInfos).map(hash)), + getIDs: () => Object.keys(userInfos), + }), + ), }); const emptyArray = [];