diff --git a/keyserver/src/responders/redux-state-responders.js b/keyserver/src/responders/redux-state-responders.js --- a/keyserver/src/responders/redux-state-responders.js +++ b/keyserver/src/responders/redux-state-responders.js @@ -68,6 +68,7 @@ import { thisKeyserverID } from '../user/identity.js'; const excludedDataValidator: TInterface = tShape({ + userStore: t.maybe(t.Bool), threadStore: t.maybe(t.Bool), }); @@ -244,6 +245,12 @@ ]); return hasNotAcknowledgedPolicies ? {} : userInfos; })(); + const finalUserInfosPromise = (async () => { + if (excludedData.userStore && useDatabase) { + return {}; + } + return await userInfosPromise; + })(); const navInfoPromise = (async () => { const [ @@ -368,7 +375,7 @@ currentUserInfo: currentUserInfoPromise, entryStore: entryStorePromise, threadStore: threadStorePromise, - userInfos: userInfosPromise, + userInfos: finalUserInfosPromise, messageStore: messageStorePromise, pushApiPublicKey: pushApiPublicKeyPromise, inviteLinksStore: inviteLinksStorePromise, diff --git a/lib/types/store-ops-types.js b/lib/types/store-ops-types.js --- a/lib/types/store-ops-types.js +++ b/lib/types/store-ops-types.js @@ -55,6 +55,7 @@ ThreadStoreOperation, } from '../ops/thread-store-ops.js'; import type { + ClientDBUserStoreOperation, UserStoreOperation, ClientDBUserInfo, } from '../ops/user-store-ops.js'; @@ -77,6 +78,7 @@ +threadStoreOperations?: $ReadOnlyArray, +messageStoreOperations?: $ReadOnlyArray, +reportStoreOperations?: $ReadOnlyArray, + +userStoreOperations?: $ReadOnlyArray, +keyserverStoreOperations?: $ReadOnlyArray, +communityStoreOperations?: $ReadOnlyArray, +integrityStoreOperations?: $ReadOnlyArray, diff --git a/web/redux/action-types.js b/web/redux/action-types.js --- a/web/redux/action-types.js +++ b/web/redux/action-types.js @@ -58,9 +58,9 @@ continue; } const clientUpdatesCurrentAsOf = allUpdatesCurrentAsOf[keyserverID]; - const keyserverExcludedData: ExcludedData = { - threadStore: !!excludedData.threadStore && !!clientUpdatesCurrentAsOf, - }; + const keyserverExcludedData: ExcludedData = clientUpdatesCurrentAsOf + ? excludedData + : {}; if (keyserverID === threadKeyserverID) { requests[keyserverID] = { urlInfo, diff --git a/web/redux/initial-state-gate.js b/web/redux/initial-state-gate.js --- a/web/redux/initial-state-gate.js +++ b/web/redux/initial-state-gate.js @@ -6,12 +6,13 @@ import { setClientDBStoreActionType } from 'lib/actions/client-db-store-actions.js'; import type { ThreadStoreOperation } from 'lib/ops/thread-store-ops.js'; +import type { UserStoreOperation } from 'lib/ops/user-store-ops.js'; import { allUpdatesCurrentAsOfSelector } from 'lib/selectors/keyserver-selectors.js'; import { canUseDatabaseOnWeb } from 'lib/shared/web-database.js'; import type { RawThreadInfo } from 'lib/types/minimally-encoded-thread-permissions-types.js'; import type { LegacyRawThreadInfo } from 'lib/types/thread-types.js'; import { convertIDToNewSchema } from 'lib/utils/migration-utils.js'; -import { entries } from 'lib/utils/objects.js'; +import { entries, values } from 'lib/utils/objects.js'; import { useDispatch } from 'lib/utils/redux-utils.js'; import { infoFromURL } from 'lib/utils/url-utils.js'; @@ -26,6 +27,7 @@ getClientDBStore, processDBStoreOperations, } from '../shared-worker/utils/store.js'; +import type { InitialReduxStateActionPayload } from '../types/redux-types.js'; type Props = { +persistor: Persistor, @@ -75,6 +77,7 @@ urlInfo, excludedData: { threadStore: !!clientDBStore.threadStore, + userStore: !!clientDBStore.users, }, allUpdatesCurrentAsOf, }); @@ -89,50 +92,65 @@ return; } + let initialReduxState: InitialReduxStateActionPayload = payload; + + let threadStoreOperations: ThreadStoreOperation[] = []; if (clientDBStore.threadStore) { - const { threadStore, ...rest } = payload; - dispatch({ type: setInitialReduxState, payload: rest }); - return; + const { threadStore, ...rest } = initialReduxState; + initialReduxState = rest; + } else { + // When there is no data in the DB, it's necessary to migrate data + // from the keyserver payload to the DB + threadStoreOperations = entries(payload.threadStore.threadInfos).map( + ([id, threadInfo]: [ + string, + LegacyRawThreadInfo | RawThreadInfo, + ]) => ({ + type: 'replace', + payload: { + id, + threadInfo, + }, + }), + ); + } + + let userStoreOperations: UserStoreOperation[] = []; + if (clientDBStore.users) { + const { userInfos, ...rest } = initialReduxState; + initialReduxState = rest; + } else { + userStoreOperations = values(payload.userInfos).map(userInfo => ({ + type: 'replace_user', + payload: userInfo, + })); } - // When there is no data in the DB, it's necessary to migrate data - // from the keyserver payload to the DB - const { - threadStore: { threadInfos }, - } = payload; - - const threadStoreOperations: ThreadStoreOperation[] = entries( - threadInfos, - ).map( - ([id, threadInfo]: [ - string, - LegacyRawThreadInfo | RawThreadInfo, - ]) => ({ - type: 'replace', - payload: { - id, - threadInfo, + if ( + threadStoreOperations.length > 0 || + userStoreOperations.length > 0 + ) { + await processDBStoreOperations( + { + threadStoreOperations, + draftStoreOperations: [], + messageStoreOperations: [], + reportStoreOperations: [], + userStoreOperations, + keyserverStoreOperations: [], + communityStoreOperations: [], + integrityStoreOperations: [], + syncedMetadataStoreOperations: [], + auxUserStoreOperations: [], }, - }), - ); - - await processDBStoreOperations( - { - threadStoreOperations, - draftStoreOperations: [], - messageStoreOperations: [], - reportStoreOperations: [], - userStoreOperations: [], - keyserverStoreOperations: [], - communityStoreOperations: [], - integrityStoreOperations: [], - syncedMetadataStoreOperations: [], - auxUserStoreOperations: [], - }, - currentLoggedInUserID, - ); + currentLoggedInUserID, + ); + } - dispatch({ type: setInitialReduxState, payload }); + dispatch({ + type: setInitialReduxState, + payload: initialReduxState, + }); } catch (err) { setInitError(err); } 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 @@ -69,7 +69,7 @@ import { reduceServicesAccessToken } from './services-access-token-reducer.js'; import { getVisibility } from './visibility.js'; import { activeThreadSelector } from '../selectors/nav-selectors.js'; -import type { InitialReduxState } from '../types/redux-types.js'; +import type { InitialReduxStateActionPayload } from '../types/redux-types.js'; export type WindowDimensions = { width: number, height: number }; @@ -140,7 +140,10 @@ +type: 'UPDATE_WINDOW_ACTIVE', +payload: boolean, } - | { +type: 'SET_INITIAL_REDUX_STATE', +payload: InitialReduxState }, + | { + +type: 'SET_INITIAL_REDUX_STATE', + +payload: InitialReduxStateActionPayload, + }, }, >; @@ -195,27 +198,26 @@ }, }); } - return validateStateAndQueueOpsProcessing( - action, - oldState, - { - ...state, - ...rest, - userStore: { userInfos }, - keyserverStore: keyserverStoreOpsHandlers.processStoreOperations( - state.keyserverStore, - replaceOperations, - ), - initialStateLoaded: true, - }, - { - ...storeOperations, - keyserverStoreOperations: [ - ...storeOperations.keyserverStoreOperations, - ...replaceOperations, - ], - }, - ); + let newState = { + ...state, + ...rest, + keyserverStore: keyserverStoreOpsHandlers.processStoreOperations( + state.keyserverStore, + replaceOperations, + ), + initialStateLoaded: true, + }; + + if (userInfos) { + newState = { ...newState, userStore: { userInfos } }; + } + return validateStateAndQueueOpsProcessing(action, oldState, newState, { + ...storeOperations, + keyserverStoreOperations: [ + ...storeOperations.keyserverStoreOperations, + ...replaceOperations, + ], + }); } else if (action.type === updateWindowDimensionsActionType) { return validateStateAndQueueOpsProcessing( action, diff --git a/web/shared-worker/utils/store.js b/web/shared-worker/utils/store.js --- a/web/shared-worker/utils/store.js +++ b/web/shared-worker/utils/store.js @@ -7,6 +7,7 @@ import { reportStoreOpsHandlers } from 'lib/ops/report-store-ops.js'; import { syncedMetadataStoreOpsHandlers } from 'lib/ops/synced-metadata-store-ops.js'; import { threadStoreOpsHandlers } from 'lib/ops/thread-store-ops.js'; +import { userStoreOpsHandlers } from 'lib/ops/user-store-ops.js'; import { canUseDatabaseOnWeb } from 'lib/shared/web-database.js'; import type { ClientStore, @@ -98,6 +99,12 @@ ), }; } + if (data?.store?.users && data.store.users.length > 0) { + result = { + ...result, + users: userStoreOpsHandlers.translateClientDBData(data.store.users), + }; + } return result; } @@ -114,6 +121,7 @@ integrityStoreOperations, syncedMetadataStoreOperations, auxUserStoreOperations, + userStoreOperations, } = storeOperations; const canUseDatabase = canUseDatabaseOnWeb(userID); @@ -135,6 +143,8 @@ ); const convertedAuxUserStoreOperations = auxUserStoreOpsHandlers.convertOpsToClientDBOps(auxUserStoreOperations); + const convertedUserStoreOperations = + userStoreOpsHandlers.convertOpsToClientDBOps(userStoreOperations); if ( convertedThreadStoreOperations.length === 0 && @@ -144,7 +154,8 @@ convertedCommunityStoreOperations.length === 0 && convertedIntegrityStoreOperations.length === 0 && convertedSyncedMetadataStoreOperations.length === 0 && - convertedAuxUserStoreOperations.length === 0 + convertedAuxUserStoreOperations.length === 0 && + convertedUserStoreOperations.length === 0 ) { return; } @@ -166,6 +177,7 @@ integrityStoreOperations: convertedIntegrityStoreOperations, syncedMetadataStoreOperations: convertedSyncedMetadataStoreOperations, auxUserStoreOperations: convertedAuxUserStoreOperations, + userStoreOperations: convertedUserStoreOperations, }, }); } catch (e) { diff --git a/web/shared-worker/worker/process-operations.js b/web/shared-worker/worker/process-operations.js --- a/web/shared-worker/worker/process-operations.js +++ b/web/shared-worker/worker/process-operations.js @@ -7,6 +7,7 @@ import type { ClientDBReportStoreOperation } from 'lib/ops/report-store-ops.js'; import type { ClientDBSyncedMetadataStoreOperation } from 'lib/ops/synced-metadata-store-ops.js'; import type { ClientDBThreadStoreOperation } from 'lib/ops/thread-store-ops.js'; +import type { ClientDBUserStoreOperation } from 'lib/ops/user-store-ops.js'; import type { ClientDBDraftStoreOperation, DraftStoreOperation, @@ -255,6 +256,34 @@ } } +function processUserStoreOperations( + sqliteQueryExecutor: SQLiteQueryExecutor, + operations: $ReadOnlyArray, + module: EmscriptenModule, +) { + for (const operation of operations) { + try { + if (operation.type === 'remove_users') { + const { ids } = operation.payload; + sqliteQueryExecutor.removeUsers(ids); + } else if (operation.type === 'replace_user') { + const user = operation.payload; + sqliteQueryExecutor.replaceUser(user); + } else if (operation.type === 'remove_all_users') { + sqliteQueryExecutor.removeAllUsers(); + } else { + throw new Error('Unsupported user operation'); + } + } catch (e) { + throw new Error( + `Error while processing ${ + operation.type + } user operation: ${getProcessingStoreOpsExceptionMessage(e, module)}`, + ); + } + } +} + function processDBStoreOperations( sqliteQueryExecutor: SQLiteQueryExecutor, storeOperations: ClientDBStoreOperations, @@ -269,6 +298,7 @@ integrityStoreOperations, syncedMetadataStoreOperations, auxUserStoreOperations, + userStoreOperations, } = storeOperations; try { @@ -332,6 +362,13 @@ module, ); } + if (userStoreOperations && userStoreOperations.length > 0) { + processUserStoreOperations( + sqliteQueryExecutor, + userStoreOperations, + module, + ); + } sqliteQueryExecutor.commitTransaction(); } catch (e) { sqliteQueryExecutor.rollbackTransaction(); @@ -382,7 +419,7 @@ .map(t => webThreadToClientDBThreadInfo(t)), messageStoreThreads: [], reports: sqliteQueryExecutor.getAllReports(), - users: [], + users: sqliteQueryExecutor.getAllUsers(), keyservers: sqliteQueryExecutor.getAllKeyservers(), communities: sqliteQueryExecutor.getAllCommunities(), integrityThreadHashes: sqliteQueryExecutor.getAllIntegrityThreadHashes(), diff --git a/web/types/redux-types.js b/web/types/redux-types.js --- a/web/types/redux-types.js +++ b/web/types/redux-types.js @@ -23,7 +23,14 @@ +keyserverInfos: { +[keyserverID: string]: WebInitialKeyserverInfo }, }; +export type InitialReduxStateActionPayload = $ReadOnly<{ + ...InitialReduxState, + +threadStore?: ThreadStore, + +userInfos?: UserInfos, +}>; + export type ExcludedData = { + +userStore?: boolean, +threadStore?: boolean, };