diff --git a/lib/actions/update-actions.js b/lib/actions/update-actions.js new file mode 100644 --- /dev/null +++ b/lib/actions/update-actions.js @@ -0,0 +1,40 @@ +// @flow + +import { useKeyserverCall } from '../keyserver-conn/keyserver-call.js'; +import type { CallKeyserverEndpoint } from '../keyserver-conn/keyserver-conn-types.js'; +import { permissionsAndAuthRelatedRequestTimeout } from '../shared/timeouts.js'; +import type { FetchPendingUpdatesInput } from '../types/session-types.js'; +import type { ClientStateSyncSocketResult } from '../types/socket-types.js'; + +const fetchPendingUpdatesActionTypes = Object.freeze({ + started: 'FETCH_PENDING_UPDATES_STARTED', + success: 'FETCH_PENDING_UPDATES_SUCCESS', + failed: 'FETCH_PENDING_UPDATES_FAILED', +}); +const fetchPendingUpdatesCallKeyserverEndpointOptions = { + timeout: permissionsAndAuthRelatedRequestTimeout, +}; +const fetchPendingUpdates = + ( + callKeyserverEndpoint: CallKeyserverEndpoint, + ): (( + input: FetchPendingUpdatesInput, + ) => Promise) => + async input => { + const { keyserverID, ...sessionState } = input; + const requests = { [keyserverID]: sessionState }; + const responses = await callKeyserverEndpoint( + 'fetch_pending_updates', + requests, + fetchPendingUpdatesCallKeyserverEndpointOptions, + ); + return { keyserverID, ...responses[keyserverID] }; + }; + +function useFetchPendingUpdates(): ( + input: FetchPendingUpdatesInput, +) => Promise { + return useKeyserverCall(fetchPendingUpdates); +} + +export { fetchPendingUpdatesActionTypes, useFetchPendingUpdates }; diff --git a/lib/reducers/calendar-filters-reducer.js b/lib/reducers/calendar-filters-reducer.js --- a/lib/reducers/calendar-filters-reducer.js +++ b/lib/reducers/calendar-filters-reducer.js @@ -11,6 +11,7 @@ leaveThreadActionTypes, deleteThreadActionTypes, } from '../actions/thread-actions.js'; +import { fetchPendingUpdatesActionTypes } from '../actions/update-actions.js'; import { keyserverAuthActionTypes, deleteKeyserverAccountActionTypes, @@ -38,6 +39,7 @@ import { fullStateSyncActionType, incrementalStateSyncActionType, + stateSyncPayloadTypes, } from '../types/socket-types.js'; import type { RawThreadInfos, ThreadStore } from '../types/thread-types.js'; import { @@ -114,11 +116,27 @@ state, action.payload.updatesResult.newUpdates, ); + } else if ( + action.type === fetchPendingUpdatesActionTypes.success && + action.payload.type === stateSyncPayloadTypes.INCREMENTAL + ) { + return updateFilterListFromUpdateInfos( + state, + action.payload.updatesResult.newUpdates, + ); } else if (action.type === fullStateSyncActionType) { return removeDeletedThreadIDsFromFilterList( state, action.payload.threadInfos, ); + } else if ( + action.type === fetchPendingUpdatesActionTypes.success && + action.payload.type === stateSyncPayloadTypes.FULL + ) { + return removeDeletedThreadIDsFromFilterList( + state, + action.payload.threadInfos, + ); } else if (action.type === updateCalendarCommunityFilter) { const nonThreadFilters = nonThreadCalendarFilters(state); diff --git a/lib/reducers/entry-reducer.js b/lib/reducers/entry-reducer.js --- a/lib/reducers/entry-reducer.js +++ b/lib/reducers/entry-reducer.js @@ -33,6 +33,7 @@ changeThreadMemberRolesActionTypes, newThreadActionTypes, } from '../actions/thread-actions.js'; +import { fetchPendingUpdatesActionTypes } from '../actions/update-actions.js'; import { keyserverAuthActionTypes, deleteKeyserverAccountActionTypes, @@ -56,6 +57,9 @@ import { fullStateSyncActionType, incrementalStateSyncActionType, + stateSyncPayloadTypes, + type ClientStateSyncIncrementalSocketResult, + type StateSyncIncrementalActionPayload, } from '../types/socket-types.js'; import type { RawThreadInfos } from '../types/thread-types.js'; import { @@ -187,16 +191,49 @@ return ops; } +type ReduceEntryInfosResult = { + +entryStore: EntryStore, + +entryStoreOperations: $ReadOnlyArray, + +reportCreationRequests: $ReadOnlyArray, +}; + +function handleIncrementalStateSync( + entryStore: EntryStore, + newThreadInfos: RawThreadInfos, + payload: + | ClientStateSyncIncrementalSocketResult + | StateSyncIncrementalActionPayload, +): ReduceEntryInfosResult { + const { entryInfos, daysToEntries } = entryStore; + const { deletedEntryIDs, deltaEntryInfos, updatesResult } = payload; + const mergeEntriesOps = mergeNewEntryInfosOps( + entryInfos, + daysToEntries, + mergeUpdateEntryInfos(deltaEntryInfos, updatesResult.newUpdates), + newThreadInfos, + ); + const updatedEntryInfos = entryStoreOpsHandlers.processStoreOperations( + entryStore, + mergeEntriesOps, + ).entryInfos; + const markAsDeletedOps = markDeletedEntries( + updatedEntryInfos, + deletedEntryIDs, + ); + const ops = [...mergeEntriesOps, ...markAsDeletedOps]; + return { + entryStore: entryStoreOpsHandlers.processStoreOperations(entryStore, ops), + entryStoreOperations: ops, + reportCreationRequests: [], + }; +} + function reduceEntryInfos( entryStore: EntryStore, action: BaseAction, newThreadInfos: RawThreadInfos, onStateDifference?: (message: string) => mixed, -): { - +entryStore: EntryStore, - +entryStoreOperations: $ReadOnlyArray, - +reportCreationRequests: $ReadOnlyArray, -} { +): ReduceEntryInfosResult { const { entryInfos, daysToEntries, lastUserInteractionCalendar } = entryStore; if ( action.type === deleteKeyserverAccountActionTypes.success || @@ -570,29 +607,31 @@ }; } } else if (action.type === incrementalStateSyncActionType) { - const mergeEntriesOps = mergeNewEntryInfosOps( - entryInfos, - daysToEntries, - mergeUpdateEntryInfos( - action.payload.deltaEntryInfos, - action.payload.updatesResult.newUpdates, - ), - newThreadInfos, - ); - const updatedEntryInfos = entryStoreOpsHandlers.processStoreOperations( + return handleIncrementalStateSync( entryStore, - mergeEntriesOps, - ).entryInfos; - const markAsDeletedOps = markDeletedEntries( - updatedEntryInfos, - action.payload.deletedEntryIDs, + newThreadInfos, + action.payload, ); - const ops = [...mergeEntriesOps, ...markAsDeletedOps]; - return { - entryStore: entryStoreOpsHandlers.processStoreOperations(entryStore, ops), - entryStoreOperations: ops, - reportCreationRequests: [], - }; + } else if (action.type === fetchPendingUpdatesActionTypes.success) { + const { payload } = action; + if (payload.type === stateSyncPayloadTypes.INCREMENTAL) { + return handleIncrementalStateSync(entryStore, newThreadInfos, payload); + } else { + const ops = mergeNewEntryInfosOps( + entryInfos, + daysToEntries, + payload.rawEntryInfos, + newThreadInfos, + ); + return { + entryStore: entryStoreOpsHandlers.processStoreOperations( + entryStore, + ops, + ), + entryStoreOperations: ops, + reportCreationRequests: [], + }; + } } else if ( action.type === processUpdatesActionType || action.type === joinThreadActionTypes.success || diff --git a/lib/reducers/integrity-reducer.js b/lib/reducers/integrity-reducer.js --- a/lib/reducers/integrity-reducer.js +++ b/lib/reducers/integrity-reducer.js @@ -3,6 +3,7 @@ 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, @@ -17,7 +18,10 @@ 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 } from '../types/socket-types.js'; +import { + fullStateSyncActionType, + stateSyncPayloadTypes, +} from '../types/socket-types.js'; import { getMessageForException } from '../utils/errors.js'; import { assertObjectsAreEqual, hash } from '../utils/objects.js'; @@ -60,7 +64,11 @@ +integrityStore: IntegrityStore, +integrityStoreOperations: $ReadOnlyArray, } { - if (action.type === fullStateSyncActionType) { + 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, diff --git a/lib/reducers/keyserver-reducer.js b/lib/reducers/keyserver-reducer.js --- a/lib/reducers/keyserver-reducer.js +++ b/lib/reducers/keyserver-reducer.js @@ -14,6 +14,7 @@ removeKeyserverActionType, } from '../actions/keyserver-actions.js'; import { legacySiweAuthActionTypes } from '../actions/siwe-actions.js'; +import { fetchPendingUpdatesActionTypes } from '../actions/update-actions.js'; import { identityLogInActionTypes, identityRegisterActionTypes, @@ -50,6 +51,9 @@ import { fullStateSyncActionType, incrementalStateSyncActionType, + stateSyncPayloadTypes, + type ClientStateSyncIncrementalSocketResult, + type StateSyncIncrementalActionPayload, } from '../types/socket-types.js'; import { updateTypes } from '../types/update-types-enum.js'; import { processUpdatesActionType } from '../types/update-types.js'; @@ -84,6 +88,24 @@ } } +function shouldClearDeviceToken( + state: KeyserverStore, + payload: + | ClientStateSyncIncrementalSocketResult + | StateSyncIncrementalActionPayload, +): boolean { + const { keyserverID, updatesResult } = payload; + for (const update of updatesResult.newUpdates) { + if ( + update.type === updateTypes.BAD_DEVICE_TOKEN && + update.deviceToken === state.keyserverInfos[keyserverID].deviceToken + ) { + return true; + } + } + return false; +} + const { processStoreOperations: processStoreOps } = keyserverStoreOpsHandlers; export default function reduceKeyserverStore( @@ -245,31 +267,69 @@ keyserverStore: processStoreOps(state, [operation]), keyserverStoreOperations: [operation], }; - } else if (action.type === incrementalStateSyncActionType) { + } else if ( + action.type === fetchPendingUpdatesActionTypes.success && + action.payload.type === stateSyncPayloadTypes.FULL + ) { const { keyserverID } = action.payload; - let { deviceToken } = state.keyserverInfos[keyserverID]; - for (const update of action.payload.updatesResult.newUpdates) { - if ( - update.type === updateTypes.BAD_DEVICE_TOKEN && - update.deviceToken === state.keyserverInfos[keyserverID].deviceToken - ) { - deviceToken = null; - break; - } - } const operation: ReplaceKeyserverOperation = { type: 'replace_keyserver', payload: { id: keyserverID, keyserverInfo: { ...state.keyserverInfos[keyserverID], - actualizedCalendarQuery: action.payload.calendarQuery, - updatesCurrentAsOf: action.payload.updatesResult.currentAsOf, + updatesCurrentAsOf: action.payload.updatesCurrentAsOf, + }, + }, + }; + + return { + keyserverStore: processStoreOps(state, [operation]), + keyserverStoreOperations: [operation], + }; + } else if (action.type === incrementalStateSyncActionType) { + const { payload } = action; + const { keyserverID } = payload; + const deviceToken = shouldClearDeviceToken(state, payload) + ? null + : state.keyserverInfos[keyserverID].deviceToken; + const operation: ReplaceKeyserverOperation = { + type: 'replace_keyserver', + payload: { + id: keyserverID, + keyserverInfo: { + ...state.keyserverInfos[keyserverID], + actualizedCalendarQuery: payload.calendarQuery, + updatesCurrentAsOf: payload.updatesResult.currentAsOf, deviceToken, }, }, }; + return { + keyserverStore: processStoreOps(state, [operation]), + keyserverStoreOperations: [operation], + }; + } else if ( + action.type === fetchPendingUpdatesActionTypes.success && + action.payload.type === stateSyncPayloadTypes.INCREMENTAL + ) { + const { payload } = action; + const { keyserverID } = payload; + const deviceToken = shouldClearDeviceToken(state, payload) + ? null + : state.keyserverInfos[keyserverID].deviceToken; + const operation: ReplaceKeyserverOperation = { + type: 'replace_keyserver', + payload: { + id: keyserverID, + keyserverInfo: { + ...state.keyserverInfos[keyserverID], + updatesCurrentAsOf: payload.updatesResult.currentAsOf, + deviceToken, + }, + }, + }; return { keyserverStore: processStoreOps(state, [operation]), keyserverStoreOperations: [operation], 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 @@ -26,6 +26,7 @@ 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, @@ -97,6 +98,7 @@ if ( action.type !== incrementalStateSyncActionType && action.type !== fullStateSyncActionType && + action.type !== fetchPendingUpdatesActionTypes.success && action.type !== legacyKeyserverRegisterActionTypes.success && action.type !== legacyLogInActionTypes.success && action.type !== legacySiweAuthActionTypes.success && diff --git a/lib/reducers/message-reducer.js b/lib/reducers/message-reducer.js --- a/lib/reducers/message-reducer.js +++ b/lib/reducers/message-reducer.js @@ -45,6 +45,7 @@ changeThreadMemberRolesActionTypes, joinThreadActionTypes, } from '../actions/thread-actions.js'; +import { fetchPendingUpdatesActionTypes } from '../actions/update-actions.js'; import { updateMultimediaMessageMediaActionType } from '../actions/upload-actions.js'; import { keyserverAuthActionTypes, @@ -96,6 +97,7 @@ import { fullStateSyncActionType, incrementalStateSyncActionType, + stateSyncPayloadTypes, } from '../types/socket-types.js'; import { threadPermissions } from '../types/thread-permission-types.js'; import type { @@ -778,23 +780,48 @@ newThreadInfos, ); } else if (action.type === incrementalStateSyncActionType) { + const { messagesResult, updatesResult } = action.payload; if ( - action.payload.messagesResult.rawMessageInfos.length === 0 && - action.payload.updatesResult.newUpdates.length === 0 + messagesResult.rawMessageInfos.length === 0 && + updatesResult.newUpdates.length === 0 ) { return { messageStoreOperations: [], messageStore }; } - const messagesResult = mergeUpdatesWithMessageInfos( - action.payload.messagesResult.rawMessageInfos, - action.payload.updatesResult.newUpdates, - action.payload.messagesResult.truncationStatuses, + const newMessagesResult = mergeUpdatesWithMessageInfos( + messagesResult.rawMessageInfos, + updatesResult.newUpdates, + messagesResult.truncationStatuses, ); return mergeNewMessages( messageStore, + newMessagesResult.rawMessageInfos, + newMessagesResult.truncationStatuses, + newThreadInfos, + ); + } else if ( + action.type === fetchPendingUpdatesActionTypes.success && + action.payload.type === stateSyncPayloadTypes.INCREMENTAL + ) { + const { messagesResult, updatesResult } = action.payload; + if ( + messagesResult.rawMessageInfos.length === 0 && + updatesResult.newUpdates.length === 0 + ) { + return { messageStoreOperations: [], messageStore }; + } + + const newMessagesResult = mergeUpdatesWithMessageInfos( messagesResult.rawMessageInfos, + updatesResult.newUpdates, messagesResult.truncationStatuses, + ); + + return mergeNewMessages( + messageStore, + newMessagesResult.rawMessageInfos, + newMessagesResult.truncationStatuses, newThreadInfos, ); } else if (action.type === processUpdatesActionType) { @@ -825,7 +852,9 @@ }; } else if ( action.type === fullStateSyncActionType || - action.type === processMessagesActionType + action.type === processMessagesActionType || + (action.type === fetchPendingUpdatesActionTypes.success && + action.payload.type === stateSyncPayloadTypes.FULL) ) { const { messagesResult } = action.payload; return mergeNewMessages( diff --git a/lib/reducers/thread-activity-reducer.js b/lib/reducers/thread-activity-reducer.js --- a/lib/reducers/thread-activity-reducer.js +++ b/lib/reducers/thread-activity-reducer.js @@ -15,6 +15,7 @@ newThreadActionTypes, removeUsersFromThreadActionTypes, } from '../actions/thread-actions.js'; +import { fetchPendingUpdatesActionTypes } from '../actions/update-actions.js'; import { deleteKeyserverAccountActionTypes } from '../actions/user-actions.js'; import { extractKeyserverIDFromID } from '../keyserver-conn/keyserver-call-utils.js'; import { setNewSessionActionType } from '../keyserver-conn/keyserver-conn-types.js'; @@ -24,7 +25,10 @@ } from '../ops/thread-activity-store-ops.js'; import { isWebPlatform } from '../types/device-types.js'; import type { BaseAction } from '../types/redux-types.js'; -import { incrementalStateSyncActionType } from '../types/socket-types.js'; +import { + incrementalStateSyncActionType, + stateSyncPayloadTypes, +} from '../types/socket-types.js'; import type { ThreadActivityStore } from '../types/thread-activity-types.js'; import { updateThreadLastNavigatedActionType } from '../types/thread-activity-types.js'; import { updateTypes } from '../types/update-types-enum.js'; @@ -59,6 +63,52 @@ } } +type ReduceThreadActivityResult = { + +threadActivityStore: ThreadActivityStore, + +threadActivityStoreOperations: $ReadOnlyArray, +}; + +function handleThreadDeletionUpdates( + state: ThreadActivityStore, + newUpdates: $ReadOnlyArray, +): ReduceThreadActivityResult { + if (newUpdates.length === 0) { + return { + threadActivityStore: state, + threadActivityStoreOperations: [], + }; + } + + const deleteThreadUpdates = newUpdates.filter( + (update: ClientUpdateInfo) => update.type === updateTypes.DELETE_THREAD, + ); + if (deleteThreadUpdates.length === 0) { + return { + threadActivityStore: state, + threadActivityStoreOperations: [], + }; + } + + const threadIDsToRemove = []; + for (const update: ClientUpdateInfo of deleteThreadUpdates) { + invariant( + update.type === updateTypes.DELETE_THREAD, + 'update must be of type DELETE_THREAD', + ); + threadIDsToRemove.push(update.threadID); + } + const removeOperation = { + type: 'remove_thread_activity_entries', + payload: { + ids: threadIDsToRemove, + }, + }; + return { + threadActivityStore: processStoreOps(state, [removeOperation]), + threadActivityStoreOperations: [removeOperation], + }; +} + const { processStoreOperations: processStoreOps } = threadActivityStoreOpsHandlers; @@ -66,10 +116,7 @@ state: ThreadActivityStore, action: BaseAction, onStateDifference?: (message: string) => mixed, -): { - +threadActivityStore: ThreadActivityStore, - +threadActivityStoreOperations: $ReadOnlyArray, -} { +): ReduceThreadActivityResult { if (action.type === updateThreadLastNavigatedActionType) { const { threadID, time } = action.payload; const replaceOperation = { @@ -119,42 +166,18 @@ action.type === modifyCommunityRoleActionTypes.success || action.type === deleteCommunityRoleActionTypes.success ) { - const { newUpdates } = action.payload.updatesResult; - if (newUpdates.length === 0) { - return { - threadActivityStore: state, - threadActivityStoreOperations: [], - }; - } - - const deleteThreadUpdates = newUpdates.filter( - (update: ClientUpdateInfo) => update.type === updateTypes.DELETE_THREAD, + return handleThreadDeletionUpdates( + state, + action.payload.updatesResult.newUpdates, + ); + } else if ( + action.type === fetchPendingUpdatesActionTypes.success && + action.payload.type === stateSyncPayloadTypes.INCREMENTAL + ) { + return handleThreadDeletionUpdates( + state, + action.payload.updatesResult.newUpdates, ); - if (deleteThreadUpdates.length === 0) { - return { - threadActivityStore: state, - threadActivityStoreOperations: [], - }; - } - - const threadIDsToRemove = []; - for (const update: ClientUpdateInfo of deleteThreadUpdates) { - invariant( - update.type === updateTypes.DELETE_THREAD, - 'update must be of type DELETE_THREAD', - ); - threadIDsToRemove.push(update.threadID); - } - const removeOperation = { - type: 'remove_thread_activity_entries', - payload: { - ids: threadIDsToRemove, - }, - }; - return { - threadActivityStore: processStoreOps(state, [removeOperation]), - threadActivityStoreOperations: [removeOperation], - }; } else if (action.type === deleteKeyserverAccountActionTypes.success) { const threadIDsToRemove = []; const keyserverIDsSet = new Set(action.payload.keyserverIDs); diff --git a/lib/reducers/thread-reducer.js b/lib/reducers/thread-reducer.js --- a/lib/reducers/thread-reducer.js +++ b/lib/reducers/thread-reducer.js @@ -18,6 +18,7 @@ modifyCommunityRoleActionTypes, deleteCommunityRoleActionTypes, } from '../actions/thread-actions.js'; +import { fetchPendingUpdatesActionTypes } from '../actions/update-actions.js'; import { keyserverAuthActionTypes, deleteKeyserverAccountActionTypes, @@ -46,6 +47,7 @@ import { fullStateSyncActionType, incrementalStateSyncActionType, + stateSyncPayloadTypes, } from '../types/socket-types.js'; import type { RawThreadInfos, ThreadStore } from '../types/thread-types.js'; import { @@ -71,38 +73,60 @@ .flat(); } -function reduceThreadInfos( - state: ThreadStore, - action: BaseAction, -): { +type ReduceThreadInfosResult = { threadStore: ThreadStore, newThreadInconsistencies: $ReadOnlyArray, threadStoreOperations: $ReadOnlyArray, -} { +}; + +function handleFullStateSync( + state: ThreadStore, + keyserverID: string, + newThreadInfos: RawThreadInfos, +): ReduceThreadInfosResult { + const threadsToRemove = Object.keys(state.threadInfos).filter( + key => extractKeyserverIDFromID(key) === keyserverID, + ); + const threadStoreOperations = [ + { + type: 'remove', + payload: { ids: threadsToRemove }, + }, + ...Object.keys(newThreadInfos).map((id: string) => ({ + type: 'replace', + payload: { id, threadInfo: newThreadInfos[id] }, + })), + ]; + const updatedThreadStore = processThreadStoreOperations( + state, + threadStoreOperations, + ); + return { + threadStore: updatedThreadStore, + newThreadInconsistencies: [], + threadStoreOperations, + }; +} + +function reduceThreadInfos( + state: ThreadStore, + action: BaseAction, +): ReduceThreadInfosResult { if (action.type === fullStateSyncActionType) { - const threadsToRemove = Object.keys(state.threadInfos).filter( - key => extractKeyserverIDFromID(key) === action.payload.keyserverID, + return handleFullStateSync( + state, + action.payload.keyserverID, + action.payload.threadInfos, ); - const newThreadInfos = action.payload.threadInfos; - const threadStoreOperations = [ - { - type: 'remove', - payload: { ids: threadsToRemove }, - }, - ...Object.keys(newThreadInfos).map((id: string) => ({ - type: 'replace', - payload: { id, threadInfo: newThreadInfos[id] }, - })), - ]; - const updatedThreadStore = processThreadStoreOperations( + } else if ( + action.type === fetchPendingUpdatesActionTypes.success && + action.payload.type === stateSyncPayloadTypes.FULL + ) { + return handleFullStateSync( state, - threadStoreOperations, + action.payload.keyserverID, + action.payload.threadInfos, ); - return { - threadStore: updatedThreadStore, - newThreadInconsistencies: [], - threadStoreOperations, - }; } else if ( action.type === legacyLogInActionTypes.success || action.type === legacySiweAuthActionTypes.success || @@ -249,6 +273,31 @@ newThreadInconsistencies: [], threadStoreOperations, }; + } else if ( + action.type === fetchPendingUpdatesActionTypes.success && + action.payload.type === stateSyncPayloadTypes.INCREMENTAL + ) { + const { newUpdates } = action.payload.updatesResult; + if (newUpdates.length === 0) { + return { + threadStore: state, + newThreadInconsistencies: [], + threadStoreOperations: [], + }; + } + const threadStoreOperations = generateOpsForThreadUpdates( + state.threadInfos, + newUpdates, + ); + const updatedThreadStore = processThreadStoreOperations( + state, + threadStoreOperations, + ); + return { + threadStore: updatedThreadStore, + newThreadInconsistencies: [], + threadStoreOperations, + }; } else if (action.type === updateSubscriptionActionTypes.success) { const { threadID, subscription } = action.payload; const threadInfo = state.threadInfos[threadID]; 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 @@ -9,6 +9,7 @@ joinThreadActionTypes, newThreadActionTypes, } from '../actions/thread-actions.js'; +import { fetchPendingUpdatesActionTypes } from '../actions/update-actions.js'; import { findUserIdentitiesActionTypes, processNewUserIDsActionType, @@ -39,10 +40,16 @@ import { fullStateSyncActionType, incrementalStateSyncActionType, + stateSyncPayloadTypes, + type ClientStateSyncIncrementalSocketResult, + type StateSyncIncrementalActionPayload, } from '../types/socket-types.js'; import { updateTypes } from '../types/update-types-enum.js'; -import { processUpdatesActionType } from '../types/update-types.js'; -import type { ClientUpdateInfo } from '../types/update-types.js'; +import { + processUpdatesActionType, + type ClientUpdateInfo, + type ClientUpdatesResultWithUserInfos, +} from '../types/update-types.js'; import type { CurrentUserInfo, UserInfos, @@ -54,6 +61,18 @@ usingCommServicesAccessToken, } from '../utils/services-utils.js'; +function handleCurrentUserUpdates( + state: ?CurrentUserInfo, + newUpdates: $ReadOnlyArray, +): ?CurrentUserInfo { + return newUpdates.reduce((reducedState, update) => { + const { reduceCurrentUser } = updateSpecs[update.type]; + return reduceCurrentUser + ? reduceCurrentUser(reducedState, update) + : reducedState; + }, state); +} + function reduceCurrentUserInfo( state: ?CurrentUserInfo, action: BaseAction, @@ -107,7 +126,9 @@ } } } else if ( - action.type === fullStateSyncActionType && + (action.type === fullStateSyncActionType || + (action.type === fetchPendingUpdatesActionTypes.success && + action.payload.type === stateSyncPayloadTypes.FULL)) && relyingOnAuthoritativeKeyserver ) { if (action.payload.keyserverID !== authoritativeKeyserverID()) { @@ -125,15 +146,23 @@ if (action.payload.keyserverID !== authoritativeKeyserverID()) { return state; } - return action.payload.updatesResult.newUpdates.reduce( - (reducedState, update) => { - const { reduceCurrentUser } = updateSpecs[update.type]; - return reduceCurrentUser - ? reduceCurrentUser(reducedState, update) - : reducedState; - }, + return handleCurrentUserUpdates( state, + action.payload.updatesResult.newUpdates, ); + } else if (action.type === fetchPendingUpdatesActionTypes.success) { + if (!relyingOnAuthoritativeKeyserver) { + return state; + } + const { payload } = action; + if (payload.type !== stateSyncPayloadTypes.INCREMENTAL) { + return state; + } + const { newUpdates } = payload.updatesResult; + if (action.payload.keyserverID !== authoritativeKeyserverID()) { + return state; + } + return handleCurrentUserUpdates(state, newUpdates); } else if ( action.type === processServerRequestsActionType && relyingOnAuthoritativeKeyserver @@ -194,14 +223,51 @@ .flat(); } -function reduceUserInfos( - state: UserStore, - action: BaseAction, -): [ +type ReduceUserInfosResult = [ UserStore, $ReadOnlyArray, $ReadOnlyArray, -] { +]; + +function handleUserInfoUpdates( + state: UserStore, + payload: + | ClientStateSyncIncrementalSocketResult + | StateSyncIncrementalActionPayload + | ClientUpdatesResultWithUserInfos, +): ReduceUserInfosResult { + if (payload.keyserverID !== authoritativeKeyserverID()) { + return [state, [], []]; + } + const newUserInfos = _keyBy(userInfo => userInfo.id)(payload.userInfos); + const userStoreOps: $ReadOnlyArray = [ + ...convertUserInfosToReplaceUserOps(newUserInfos), + ...generateOpsForUserUpdates(payload), + ]; + + const processedUserInfos: UserInfos = processUserStoreOps( + state.userInfos, + userStoreOps, + ); + + if (_isEqual(state.userInfos)(processedUserInfos)) { + return [state, [], []]; + } + + return [ + { + ...state, + userInfos: processedUserInfos, + }, + [], + userStoreOps, + ]; +} + +function reduceUserInfos( + state: UserStore, + action: BaseAction, +): ReduceUserInfosResult { if (action.type === processNewUserIDsActionType) { const filteredUserIDs = action.payload.userIDs.filter( id => !state.userInfos[id], @@ -280,7 +346,9 @@ userStoreOps, ]; } else if ( - action.type === fullStateSyncActionType && + (action.type === fullStateSyncActionType || + (action.type === fetchPendingUpdatesActionTypes.success && + action.payload.type === stateSyncPayloadTypes.FULL)) && relyingOnAuthoritativeKeyserver ) { if (action.payload.keyserverID !== authoritativeKeyserverID()) { @@ -358,31 +426,14 @@ action.type === processUpdatesActionType) && relyingOnAuthoritativeKeyserver ) { - if (action.payload.keyserverID !== authoritativeKeyserverID()) { + return handleUserInfoUpdates(state, action.payload); + } else if (action.type === fetchPendingUpdatesActionTypes.success) { + if (!relyingOnAuthoritativeKeyserver) { return [state, [], []]; } - const newUserInfos = _keyBy(userInfo => userInfo.id)( - action.payload.userInfos, - ); - const userStoreOps: $ReadOnlyArray = [ - ...convertUserInfosToReplaceUserOps(newUserInfos), - ...generateOpsForUserUpdates(action.payload), - ]; - - const processedUserInfos: UserInfos = processUserStoreOps( - state.userInfos, - userStoreOps, - ); - - if (!_isEqual(state.userInfos)(processedUserInfos)) { - return [ - { - ...state, - userInfos: processedUserInfos, - }, - [], - userStoreOps, - ]; + const { payload } = action; + if (payload.type === stateSyncPayloadTypes.INCREMENTAL) { + return handleUserInfoUpdates(state, payload); } } else if ( action.type === processServerRequestsActionType && diff --git a/lib/types/redux-types.js b/lib/types/redux-types.js --- a/lib/types/redux-types.js +++ b/lib/types/redux-types.js @@ -120,6 +120,7 @@ StateSyncFullActionPayload, StateSyncIncrementalActionPayload, SetActiveSessionRecoveryPayload, + ClientStateSyncSocketResult, } from './socket-types.js'; import { type ClientStore } from './store-ops-types.js'; import type { SubscriptionUpdateResult } from './subscription-types.js'; @@ -1488,6 +1489,22 @@ +error: true, +payload: Error, +loadingInfo: LoadingInfo, + } + | { + +type: 'FETCH_PENDING_UPDATES_STARTED', + +loadingInfo?: LoadingInfo, + +payload?: void, + } + | { + +type: 'FETCH_PENDING_UPDATES_SUCCESS', + +payload: ClientStateSyncSocketResult, + +loadingInfo: LoadingInfo, + } + | { + +type: 'FETCH_PENDING_UPDATES_FAILED', + +error: true, + +payload: Error, + +loadingInfo: LoadingInfo, }, }>; diff --git a/lib/types/session-types.js b/lib/types/session-types.js --- a/lib/types/session-types.js +++ b/lib/types/session-types.js @@ -115,3 +115,8 @@ cookie: ?string, sessionID: ?string, }>; + +export type FetchPendingUpdatesInput = $ReadOnly<{ + ...SessionState, + +keyserverID: string, +}>; diff --git a/lib/types/socket-types.js b/lib/types/socket-types.js --- a/lib/types/socket-types.js +++ b/lib/types/socket-types.js @@ -300,6 +300,18 @@ serverStateSyncIncrementalSocketPayloadValidator, ]); +export type ClientStateSyncFullSocketResult = $ReadOnly<{ + ...ClientStateSyncFullSocketPayload, + +keyserverID: string, +}>; +export type ClientStateSyncIncrementalSocketResult = $ReadOnly<{ + ...ClientStateSyncIncrementalSocketPayload, + +keyserverID: string, +}>; +export type ClientStateSyncSocketResult = + | ClientStateSyncFullSocketResult + | ClientStateSyncIncrementalSocketResult; + export type ServerStateSyncServerSocketMessage = { +type: 0, +responseTo: number,