diff --git a/lib/shared/create-async-migrate.js b/lib/shared/create-async-migrate.js deleted file mode 100644 index a0fb93362..000000000 --- a/lib/shared/create-async-migrate.js +++ /dev/null @@ -1,127 +0,0 @@ -// @flow - -import { DEFAULT_VERSION } from 'redux-persist/es/constants.js'; - -import type { BaseNavInfo } from '../types/nav-types.js'; -import type { BaseAppState } from '../types/redux-types.js'; -import type { StoreOperations } from '../types/store-ops-types.js'; -import { syncedMetadataNames } from '../types/synced-metadata-types.js'; -import { getConfig } from '../utils/config.js'; - -type LegacyMigrationManifest> = { - +[number | string]: (T) => Promise, -}; -type PersistedState> = T | void; -type ConfigType = { - +debug: boolean, -}; - -export type StorageMigrationFunction> = ( - debug: boolean, -) => Promise>; - -type MigrationManifest> = { - +[number | string]: (PersistedState) => Promise<{ - +state: T, - +ops: StoreOperations, - }>, -}; - -function createAsyncMigrate>( - legacyMigrations: LegacyMigrationManifest, - config: ConfigType, - migrations: MigrationManifest, - handleException: (error: Error, state: T) => T, - storageMigration: ?StorageMigrationFunction, -): ( - state: PersistedState, - currentVersion: number, -) => Promise> { - const debug = process.env.NODE_ENV !== 'production' && !!config?.debug; - return async function ( - state: ?PersistedState, - currentVersion: number, - ): Promise> { - if (!state && storageMigration) { - state = await storageMigration(debug); - } - if (!state) { - if (debug) { - console.log('redux-persist: no inbound state, skipping migration'); - } - return undefined; - } - - const inboundVersion: number = state?._persist?.version ?? DEFAULT_VERSION; - - if (inboundVersion === currentVersion) { - if (debug) { - console.log('redux-persist: versions match, noop migration'); - } - return state; - } - if (inboundVersion > currentVersion) { - if (debug) { - console.error('redux-persist: downgrading version is not supported'); - } - return state; - } - - const migrationKeys = [ - ...Object.keys(legacyMigrations), - ...Object.keys(migrations), - ] - .map(ver => parseInt(ver)) - .filter(key => currentVersion >= key && key > inboundVersion); - const sortedMigrationKeys = migrationKeys.sort((a, b) => a - b); - - if (debug) { - console.log('redux-persist: migrationKeys', sortedMigrationKeys); - } - - let migratedState = state; - for (const versionKey of sortedMigrationKeys) { - if (debug) { - console.log( - 'redux-persist: running migration for versionKey', - versionKey, - ); - } - - if (!versionKey) { - continue; - } - - if (legacyMigrations[versionKey]) { - migratedState = await legacyMigrations[versionKey](migratedState); - } else { - const { state: newState, ops } = - await migrations[versionKey](migratedState); - migratedState = newState; - const versionUpdateOp = { - type: 'replace_synced_metadata_entry', - payload: { - name: syncedMetadataNames.DB_VERSION, - data: versionKey.toString(), - }, - }; - const dbOps = { - ...ops, - syncedMetadataStoreOperations: [ - ...(ops.syncedMetadataStoreOperations ?? []), - versionUpdateOp, - ], - }; - try { - await getConfig().sqliteAPI.processDBStoreOperations(dbOps); - } catch (exception) { - return handleException(exception, state); - } - } - } - - return migratedState; - }; -} - -export { createAsyncMigrate }; diff --git a/lib/utils/migration-utils.js b/lib/utils/migration-utils.js index b5766f7ac..b6c044f35 100644 --- a/lib/utils/migration-utils.js +++ b/lib/utils/migration-utils.js @@ -1,200 +1,322 @@ // @flow import invariant from 'invariant'; import { authoritativeKeyserverID } from './authoritative-keyserver.js'; +import { getConfig } from './config.js'; import type { TranslatedThreadMessageInfos } from './message-ops-utils.js'; import { entries } from './objects.js'; import { convertRawMessageInfoToNewIDSchema, convertRawThreadInfoToNewIDSchema, } from '../_generated/migration-utils.js'; import { parsePendingThreadID, getPendingThreadID, draftKeySuffix, } from '../shared/thread-utils.js'; import type { ClientDBDraftInfo, ClientDBDraftStoreOperation, DraftStore, } from '../types/draft-types'; import type { RawMessageInfo } from '../types/message-types.js'; +import type { BaseNavInfo } from '../types/nav-types.js'; +import type { BaseAppState } from '../types/redux-types.js'; +import type { StoreOperations } from '../types/store-ops-types.js'; +import { syncedMetadataNames } from '../types/synced-metadata-types.js'; import { threadPermissions, threadPermissionPropagationPrefixes, threadPermissionFilterPrefixes, } from '../types/thread-permission-types.js'; import type { MixedRawThreadInfos } from '../types/thread-types.js'; function convertDraftKeyToNewIDSchema(key: string): string { const threadID = key.slice(0, -draftKeySuffix.length); const convertedThreadID = convertIDToNewSchema( threadID, authoritativeKeyserverID(), ); return `${convertedThreadID}${draftKeySuffix}`; } function convertDraftStoreToNewIDSchema(store: DraftStore): DraftStore { return { drafts: Object.fromEntries( entries(store.drafts).map(([key, value]) => [ convertDraftKeyToNewIDSchema(key), value, ]), ), }; } function generateIDSchemaMigrationOpsForDrafts( drafts: $ReadOnlyArray, ): $ReadOnlyArray { const operations = drafts.map(draft => ({ type: 'update', payload: { key: convertDraftKeyToNewIDSchema(draft.key), text: draft.text, }, })); return [{ type: 'remove_all' }, ...operations]; } function convertMessageStoreThreadsToNewIDSchema( messageStoreThreads: TranslatedThreadMessageInfos, ): TranslatedThreadMessageInfos { return Object.fromEntries( entries(messageStoreThreads).map(([id, translatedThreadMessageInfo]) => [ `${authoritativeKeyserverID()}|` + id, translatedThreadMessageInfo, ]), ); } function convertThreadStoreThreadInfosToNewIDSchema( threadStoreThreadInfos: MixedRawThreadInfos, ): MixedRawThreadInfos { return Object.fromEntries( entries(threadStoreThreadInfos).map(([id, threadInfo]) => { invariant( !threadInfo.minimallyEncoded, `threadInfo during ID schema migration shouldn't be minimallyEncoded`, ); return [ `${authoritativeKeyserverID()}|` + id, convertRawThreadInfoToNewIDSchema(threadInfo), ]; }), ); } function convertIDToNewSchema(threadID: string, idPrefix: string): string { const pendingIDContents = parsePendingThreadID(threadID); if (!pendingIDContents) { return convertNonPendingIDToNewSchema(threadID, idPrefix); } const { threadType, sourceMessageID, memberIDs } = pendingIDContents; if (!sourceMessageID) { return threadID; } return getPendingThreadID( threadType, memberIDs, convertNonPendingIDToNewSchema(sourceMessageID, idPrefix), ); } function convertNonPendingIDToNewSchema( threadID: string, idPrefix: string, ): string { if (threadID.indexOf('|') === -1) { return `${idPrefix}|${threadID}`; } return threadID; } function convertNotificationMessageInfoToNewIDSchema( messageInfosString: ?string, ): ?$ReadOnlyArray { let messageInfos: ?$ReadOnlyArray = null; if (messageInfosString) { messageInfos = JSON.parse(messageInfosString); } if (messageInfos?.some(message => message.threadID.indexOf('|') === -1)) { messageInfos = messageInfos?.map(convertRawMessageInfoToNewIDSchema); } return messageInfos; } // This is an array of all permissions that need to be removed // in an upcoming migration for roles. Once the migrations are landed, // no changes to this array should be made to prevent future migrations // from having unexpected behavior. // See context in https://linear.app/comm/issue/ENG-5622/#comment-2d98a2cd const permissionsToRemoveInMigration: $ReadOnlyArray = [ threadPermissionPropagationPrefixes.DESCENDANT + threadPermissionFilterPrefixes.OPEN + threadPermissions.VOICED, threadPermissions.JOIN_THREAD, threadPermissionPropagationPrefixes.DESCENDANT + threadPermissions.EDIT_ENTRIES, threadPermissionPropagationPrefixes.DESCENDANT + threadPermissions.EDIT_THREAD_NAME, threadPermissionPropagationPrefixes.DESCENDANT + threadPermissions.EDIT_THREAD_DESCRIPTION, threadPermissionPropagationPrefixes.DESCENDANT + threadPermissions.EDIT_THREAD_COLOR, threadPermissionPropagationPrefixes.DESCENDANT + threadPermissionFilterPrefixes.TOP_LEVEL + threadPermissions.CREATE_SUBCHANNELS, threadPermissionPropagationPrefixes.DESCENDANT + threadPermissions.EDIT_THREAD_AVATAR, threadPermissionPropagationPrefixes.DESCENDANT + threadPermissionFilterPrefixes.TOP_LEVEL + threadPermissions.CREATE_SIDEBARS, threadPermissionPropagationPrefixes.DESCENDANT + threadPermissions.ADD_MEMBERS, threadPermissionPropagationPrefixes.DESCENDANT + threadPermissions.REMOVE_MEMBERS, threadPermissionPropagationPrefixes.DESCENDANT + threadPermissions.CHANGE_ROLE, threadPermissionPropagationPrefixes.DESCENDANT + threadPermissions.EDIT_PERMISSIONS, threadPermissionPropagationPrefixes.DESCENDANT + threadPermissions.MANAGE_PINS, threadPermissionPropagationPrefixes.DESCENDANT + threadPermissions.REACT_TO_MESSAGE, threadPermissionPropagationPrefixes.DESCENDANT + threadPermissions.EDIT_MESSAGE, threadPermissionPropagationPrefixes.DESCENDANT + threadPermissions.MANAGE_INVITE_LINKS, ]; +type LegacyMigrationManifest> = { + +[number | string]: (T) => Promise, +}; +type PersistedState> = T | void; +type ConfigType = { + +debug: boolean, +}; + +export type StorageMigrationFunction> = ( + debug: boolean, +) => Promise>; + +type MigrationManifest> = { + +[number | string]: (PersistedState) => Promise<{ + +state: T, + +ops: StoreOperations, + }>, +}; + +function createAsyncMigrate>( + legacyMigrations: LegacyMigrationManifest, + config: ConfigType, + migrations: MigrationManifest, + handleException: (error: Error, state: T) => T, + storageMigration: ?StorageMigrationFunction, +): ( + state: PersistedState, + currentVersion: number, +) => Promise> { + const debug = process.env.NODE_ENV !== 'production' && !!config?.debug; + return async function ( + state: ?PersistedState, + currentVersion: number, + ): Promise> { + if (!state && storageMigration) { + state = await storageMigration(debug); + } + if (!state) { + if (debug) { + console.log('redux-persist: no inbound state, skipping migration'); + } + return undefined; + } + + const inboundVersion: number = state?._persist?.version ?? -1; + + if (inboundVersion === currentVersion) { + if (debug) { + console.log('redux-persist: versions match, noop migration'); + } + return state; + } + if (inboundVersion > currentVersion) { + if (debug) { + console.error('redux-persist: downgrading version is not supported'); + } + return state; + } + + const migrationKeys = [ + ...Object.keys(legacyMigrations), + ...Object.keys(migrations), + ] + .map(ver => parseInt(ver)) + .filter(key => currentVersion >= key && key > inboundVersion); + const sortedMigrationKeys = migrationKeys.sort((a, b) => a - b); + + if (debug) { + console.log('redux-persist: migrationKeys', sortedMigrationKeys); + } + + let migratedState = state; + for (const versionKey of sortedMigrationKeys) { + if (debug) { + console.log( + 'redux-persist: running migration for versionKey', + versionKey, + ); + } + + if (!versionKey) { + continue; + } + + if (legacyMigrations[versionKey]) { + migratedState = await legacyMigrations[versionKey](migratedState); + } else { + const { state: newState, ops } = + await migrations[versionKey](migratedState); + migratedState = newState; + const versionUpdateOp = { + type: 'replace_synced_metadata_entry', + payload: { + name: syncedMetadataNames.DB_VERSION, + data: versionKey.toString(), + }, + }; + const dbOps = { + ...ops, + syncedMetadataStoreOperations: [ + ...(ops.syncedMetadataStoreOperations ?? []), + versionUpdateOp, + ], + }; + try { + await getConfig().sqliteAPI.processDBStoreOperations(dbOps); + } catch (exception) { + return handleException(exception, state); + } + } + } + + return migratedState; + }; +} + export { convertDraftKeyToNewIDSchema, convertDraftStoreToNewIDSchema, generateIDSchemaMigrationOpsForDrafts, convertMessageStoreThreadsToNewIDSchema, convertThreadStoreThreadInfosToNewIDSchema, convertNonPendingIDToNewSchema, convertIDToNewSchema, convertNotificationMessageInfoToNewIDSchema, permissionsToRemoveInMigration, + createAsyncMigrate, }; diff --git a/native/redux/persist.js b/native/redux/persist.js index 9cac7ff47..8a26086eb 100644 --- a/native/redux/persist.js +++ b/native/redux/persist.js @@ -1,1390 +1,1390 @@ // @flow import AsyncStorage from '@react-native-async-storage/async-storage'; import invariant from 'invariant'; import { Platform } from 'react-native'; import Orientation from 'react-native-orientation-locker'; import { createTransform } from 'redux-persist'; import type { Transform, Persistor } from 'redux-persist/es/types.js'; import { convertEntryStoreToNewIDSchema, convertInviteLinksStoreToNewIDSchema, convertMessageStoreToNewIDSchema, convertRawMessageInfoToNewIDSchema, convertCalendarFilterToNewIDSchema, convertConnectionInfoToNewIDSchema, } from 'lib/_generated/migration-utils.js'; import { extractKeyserverIDFromID } from 'lib/keyserver-conn/keyserver-call-utils.js'; import type { ClientDBEntryStoreOperation, ReplaceEntryOperation, } from 'lib/ops/entries-store-ops'; import { entryStoreOpsHandlers } from 'lib/ops/entries-store-ops.js'; import { type ClientDBIntegrityStoreOperation, integrityStoreOpsHandlers, type ReplaceIntegrityThreadHashesOperation, } from 'lib/ops/integrity-store-ops.js'; import { type ClientDBKeyserverStoreOperation, keyserverStoreOpsHandlers, type ReplaceKeyserverOperation, } from 'lib/ops/keyserver-store-ops.js'; import { type ClientDBMessageStoreOperation, messageStoreOpsHandlers, } from 'lib/ops/message-store-ops.js'; import { type ReportStoreOperation, type ClientDBReportStoreOperation, convertReportsToReplaceReportOps, reportStoreOpsHandlers, } from 'lib/ops/report-store-ops.js'; import { type ClientDBThreadActivityStoreOperation, threadActivityStoreOpsHandlers, type ReplaceThreadActivityEntryOperation, } from 'lib/ops/thread-activity-store-ops.js'; import type { ClientDBThreadStoreOperation } from 'lib/ops/thread-store-ops.js'; import { threadStoreOpsHandlers } from 'lib/ops/thread-store-ops.js'; import { type ClientDBUserStoreOperation, type UserStoreOperation, convertUserInfosToReplaceUserOps, userStoreOpsHandlers, } from 'lib/ops/user-store-ops.js'; import { patchRawThreadInfosWithSpecialRole } from 'lib/permissions/special-roles.js'; import { filterThreadIDsInFilterList } from 'lib/reducers/calendar-filters-reducer.js'; import { highestLocalIDSelector } from 'lib/selectors/local-id-selectors.js'; -import { createAsyncMigrate } from 'lib/shared/create-async-migrate.js'; import { inconsistencyResponsesToReports } from 'lib/shared/report-utils.js'; import { getContainingThreadID, getCommunity, assertAllThreadInfosAreLegacy, } from 'lib/shared/thread-utils.js'; import { keyserverStoreTransform } from 'lib/shared/transforms/keyserver-store-transform.js'; import { messageStoreMessagesBlocklistTransform } from 'lib/shared/transforms/message-store-transform.js'; import { DEPRECATED_unshimMessageStore, unshimFunc, } from 'lib/shared/unshim-utils.js'; import { defaultAlertInfo, defaultAlertInfos, alertTypes, } from 'lib/types/alert-types.js'; import { defaultEnabledApps } from 'lib/types/enabled-apps.js'; import { defaultCalendarQuery } from 'lib/types/entry-types.js'; import { defaultCalendarFilters } from 'lib/types/filter-types.js'; import type { KeyserverInfo } from 'lib/types/keyserver-types.js'; import { messageTypes, type MessageType, } from 'lib/types/message-types-enum.js'; import { type MessageStoreThreads, type RawMessageInfo, } from 'lib/types/message-types.js'; import { minimallyEncodeRawThreadInfo } from 'lib/types/minimally-encoded-thread-permissions-types.js'; import type { RawThreadInfo } from 'lib/types/minimally-encoded-thread-permissions-types.js'; import type { ReportStore, ClientReportCreationRequest, } from 'lib/types/report-types.js'; import { defaultConnectionInfo } from 'lib/types/socket-types.js'; import { defaultGlobalThemeInfo } from 'lib/types/theme-types.js'; import type { ClientDBThreadInfo, LegacyRawThreadInfo, MixedRawThreadInfos, } from 'lib/types/thread-types.js'; import { translateClientDBMessageInfoToRawMessageInfo, translateRawMessageInfoToClientDBMessageInfo, } from 'lib/utils/message-ops-utils.js'; import { generateIDSchemaMigrationOpsForDrafts, convertMessageStoreThreadsToNewIDSchema, convertThreadStoreThreadInfosToNewIDSchema, + createAsyncMigrate, } from 'lib/utils/migration-utils.js'; import { entries } from 'lib/utils/objects.js'; import { deprecatedConvertClientDBThreadInfoToRawThreadInfo, convertRawThreadInfoToClientDBThreadInfo, } from 'lib/utils/thread-ops-utils.js'; import { getUUID } from 'lib/utils/uuid.js'; import { createUpdateDBOpsForMessageStoreMessages, createUpdateDBOpsForMessageStoreThreads, updateClientDBThreadStoreThreadInfos, } from './client-db-utils.js'; import { defaultState } from './default-state.js'; import { deprecatedCreateUpdateDBOpsForThreadStoreThreadInfos, deprecatedUpdateClientDBThreadStoreThreadInfos, } from './deprecated-client-db-utils.js'; import { migrateThreadStoreForEditThreadPermissions } from './edit-thread-permission-migration.js'; import { handleReduxMigrationFailure, persistBlacklist, } from './handle-redux-migration-failure.js'; import { legacyUpdateRolesAndPermissions } from './legacy-update-roles-and-permissions.js'; import { persistMigrationForManagePinsThreadPermission } from './manage-pins-permission-migration.js'; import { persistMigrationToRemoveSelectRolePermissions } from './remove-select-role-permissions.js'; import type { AppState } from './state-types.js'; import { unshimClientDB } from './unshim-utils.js'; import { authoritativeKeyserverID } from '../authoritative-keyserver.js'; import { commCoreModule } from '../native-modules.js'; import { defaultDeviceCameraInfo } from '../types/camera.js'; import { isTaskCancelledError } from '../utils/error-handling.js'; import { defaultURLPrefix } from '../utils/url-utils.js'; const legacyMigrations = { [1]: (state: AppState) => ({ ...state, notifPermissionAlertInfo: defaultAlertInfo, }), [2]: (state: AppState) => ({ ...state, messageSentFromRoute: [], }), [3]: (state: any) => ({ currentUserInfo: state.currentUserInfo, entryStore: state.entryStore, threadInfos: state.threadInfos, userInfos: state.userInfos, messageStore: { ...state.messageStore, currentAsOf: state.currentAsOf, }, updatesCurrentAsOf: state.currentAsOf, cookie: state.cookie, deviceToken: state.deviceToken, urlPrefix: state.urlPrefix, customServer: state.customServer, notifPermissionAlertInfo: state.notifPermissionAlertInfo, messageSentFromRoute: state.messageSentFromRoute, _persist: state._persist, }), [4]: (state: AppState) => ({ ...state, pingTimestamps: undefined, activeServerRequests: undefined, }), [5]: (state: AppState) => ({ ...state, calendarFilters: defaultCalendarFilters, }), [6]: (state: any) => ({ ...state, threadInfos: undefined, threadStore: { threadInfos: state.threadInfos, inconsistencyResponses: [], }, }), [7]: (state: AppState) => ({ ...state, lastUserInteraction: undefined, sessionID: undefined, entryStore: { ...state.entryStore, inconsistencyResponses: [], }, }), [8]: (state: AppState) => ({ ...state, pingTimestamps: undefined, activeServerRequests: undefined, connection: { ...defaultConnectionInfo, actualizedCalendarQuery: defaultCalendarQuery(Platform.OS), }, watchedThreadIDs: [], entryStore: { ...state.entryStore, actualizedCalendarQuery: undefined, }, }), [9]: (state: any) => ({ ...state, connection: { ...state.connection, lateResponses: [], }, }), [10]: (state: any) => ({ ...state, nextLocalID: highestLocalIDSelector(state) + 1, connection: { ...state.connection, showDisconnectedBar: false, }, messageStore: { ...state.messageStore, local: {}, }, }), [11]: (state: AppState) => ({ ...state, messageStore: DEPRECATED_unshimMessageStore(state.messageStore, [ messageTypes.IMAGES, ]), }), [12]: (state: AppState) => ({ ...state, globalThemeInfo: defaultGlobalThemeInfo, }), [13]: (state: AppState) => ({ ...state, deviceCameraInfo: defaultDeviceCameraInfo, deviceOrientation: Orientation.getInitialOrientation(), }), [14]: (state: AppState) => state, [15]: (state: any) => ({ ...state, threadStore: { ...state.threadStore, inconsistencyReports: inconsistencyResponsesToReports( state.threadStore.inconsistencyResponses, ), inconsistencyResponses: undefined, }, entryStore: { ...state.entryStore, inconsistencyReports: inconsistencyResponsesToReports( state.entryStore.inconsistencyResponses, ), inconsistencyResponses: undefined, }, queuedReports: [], }), [16]: (state: any) => { const result = { ...state, messageSentFromRoute: undefined, dataLoaded: !!state.currentUserInfo && !state.currentUserInfo.anonymous, }; if (state.navInfo) { result.navInfo = { ...state.navInfo, navigationState: undefined, }; } return result; }, [17]: (state: any) => ({ ...state, userInfos: undefined, userStore: { userInfos: state.userInfos, inconsistencyResponses: [], }, }), [18]: (state: AppState) => ({ ...state, userStore: { userInfos: state.userStore.userInfos, inconsistencyReports: [], }, }), [19]: (state: any) => { const threadInfos: { [string]: LegacyRawThreadInfo } = {}; for (const threadID in state.threadStore.threadInfos) { const threadInfo = state.threadStore.threadInfos[threadID]; const { visibilityRules, ...rest } = threadInfo; threadInfos[threadID] = rest; } return { ...state, threadStore: { ...state.threadStore, threadInfos, }, }; }, [20]: (state: AppState) => ({ ...state, messageStore: DEPRECATED_unshimMessageStore(state.messageStore, [ messageTypes.LEGACY_UPDATE_RELATIONSHIP, ]), }), [21]: (state: AppState) => ({ ...state, messageStore: DEPRECATED_unshimMessageStore(state.messageStore, [ messageTypes.CREATE_SIDEBAR, messageTypes.SIDEBAR_SOURCE, ]), }), [22]: (state: any) => { for (const key in state.drafts) { const value = state.drafts[key]; try { void commCoreModule.updateDraft(key, value); } catch (e) { if (!isTaskCancelledError(e)) { throw e; } } } return { ...state, drafts: undefined, }; }, [23]: (state: AppState) => ({ ...state, globalThemeInfo: defaultGlobalThemeInfo, }), [24]: (state: AppState) => ({ ...state, enabledApps: defaultEnabledApps, }), [25]: (state: AppState) => ({ ...state, crashReportsEnabled: __DEV__, }), [26]: (state: any) => { const { currentUserInfo } = state; if (currentUserInfo.anonymous) { return state; } return { ...state, crashReportsEnabled: undefined, currentUserInfo: { id: currentUserInfo.id, username: currentUserInfo.username, }, enabledReports: { crashReports: __DEV__, inconsistencyReports: __DEV__, mediaReports: __DEV__, }, }; }, [27]: (state: any) => ({ ...state, queuedReports: undefined, enabledReports: undefined, threadStore: { ...state.threadStore, inconsistencyReports: undefined, }, entryStore: { ...state.entryStore, inconsistencyReports: undefined, }, reportStore: { enabledReports: { crashReports: __DEV__, inconsistencyReports: __DEV__, mediaReports: __DEV__, }, queuedReports: [ ...state.entryStore.inconsistencyReports, ...state.threadStore.inconsistencyReports, ...state.queuedReports, ], }, }), [28]: (state: AppState) => { const threadParentToChildren: { [string]: string[] } = {}; for (const threadID in state.threadStore.threadInfos) { const threadInfo = state.threadStore.threadInfos[threadID]; const parentThreadInfo = threadInfo.parentThreadID ? state.threadStore.threadInfos[threadInfo.parentThreadID] : null; const parentIndex = parentThreadInfo ? parentThreadInfo.id : '-1'; if (!threadParentToChildren[parentIndex]) { threadParentToChildren[parentIndex] = []; } threadParentToChildren[parentIndex].push(threadID); } const rootIDs = threadParentToChildren['-1']; if (!rootIDs) { // This should never happen, but if it somehow does we'll let the state // check mechanism resolve it... return state; } const threadInfos: { [string]: LegacyRawThreadInfo | RawThreadInfo, } = {}; const stack = [...rootIDs]; while (stack.length > 0) { const threadID = stack.shift(); const threadInfo = state.threadStore.threadInfos[threadID]; const parentThreadInfo = threadInfo.parentThreadID ? threadInfos[threadInfo.parentThreadID] : null; threadInfos[threadID] = { ...threadInfo, containingThreadID: getContainingThreadID( parentThreadInfo, threadInfo.type, ), community: getCommunity(parentThreadInfo), }; const children = threadParentToChildren[threadID]; if (children) { stack.push(...children); } } return { ...state, threadStore: { ...state.threadStore, threadInfos } }; }, [29]: (state: AppState) => { const legacyRawThreadInfos: { +[id: string]: LegacyRawThreadInfo, } = assertAllThreadInfosAreLegacy(state.threadStore.threadInfos); const updatedThreadInfos = migrateThreadStoreForEditThreadPermissions(legacyRawThreadInfos); return { ...state, threadStore: { ...state.threadStore, threadInfos: updatedThreadInfos, }, }; }, [30]: (state: AppState) => { const threadInfos = state.threadStore.threadInfos; const operations = [ { type: 'remove_all', }, ...Object.keys(threadInfos).map((id: string) => ({ type: 'replace', payload: { id, threadInfo: threadInfos[id] }, })), ]; try { commCoreModule.processThreadStoreOperationsSync( threadStoreOpsHandlers.convertOpsToClientDBOps(operations), ); } catch (exception) { console.log(exception); if (isTaskCancelledError(exception)) { return state; } return { ...state, cookie: null }; } return state; }, [31]: (state: AppState) => { const messages = state.messageStore.messages; const operations: $ReadOnlyArray = [ { type: 'remove_all', }, ...Object.keys(messages).map((id: string) => ({ type: 'replace', payload: translateRawMessageInfoToClientDBMessageInfo(messages[id]), })), ]; try { commCoreModule.processMessageStoreOperationsSync(operations); } catch (exception) { console.log(exception); if (isTaskCancelledError(exception)) { return state; } return { ...state, cookie: null }; } return state; }, [32]: (state: AppState) => unshimClientDB(state, [messageTypes.MULTIMEDIA]), [33]: (state: AppState) => unshimClientDB(state, [messageTypes.REACTION]), [34]: (state: any) => { const { threadIDsToNotifIDs, ...stateSansThreadIDsToNotifIDs } = state; return stateSansThreadIDsToNotifIDs; }, [35]: (state: AppState) => unshimClientDB(state, [messageTypes.MULTIMEDIA]), [36]: (state: AppState) => { // 1. Get threads and messages from SQLite `threads` and `messages` tables. const clientDBThreadInfos = commCoreModule.getAllThreadsSync(); const clientDBMessageInfos = commCoreModule.getAllMessagesSync(); // 2. Translate `ClientDBThreadInfo`s to `RawThreadInfo`s and // `ClientDBMessageInfo`s to `RawMessageInfo`s. const rawThreadInfos = clientDBThreadInfos.map( deprecatedConvertClientDBThreadInfoToRawThreadInfo, ); const rawMessageInfos = clientDBMessageInfos.map( translateClientDBMessageInfoToRawMessageInfo, ); // 3. Unshim translated `RawMessageInfos` to get the TOGGLE_PIN messages const unshimmedRawMessageInfos = rawMessageInfos.map(messageInfo => unshimFunc(messageInfo, new Set([messageTypes.TOGGLE_PIN])), ); // 4. Filter out non-TOGGLE_PIN messages const filteredRawMessageInfos = unshimmedRawMessageInfos .map(messageInfo => messageInfo.type === messageTypes.TOGGLE_PIN ? messageInfo : null, ) .filter(Boolean); // 5. We want only the last TOGGLE_PIN message for each message ID, // so 'pin', 'unpin', 'pin' don't count as 3 pins, but only 1. const lastMessageIDToRawMessageInfoMap = new Map(); for (const messageInfo of filteredRawMessageInfos) { const { targetMessageID } = messageInfo; lastMessageIDToRawMessageInfoMap.set(targetMessageID, messageInfo); } const lastMessageIDToRawMessageInfos = Array.from( lastMessageIDToRawMessageInfoMap.values(), ); // 6. Create a Map of threadIDs to pinnedCount const threadIDsToPinnedCount = new Map(); for (const messageInfo of lastMessageIDToRawMessageInfos) { const { threadID, type } = messageInfo; if (type === messageTypes.TOGGLE_PIN) { const pinnedCount = threadIDsToPinnedCount.get(threadID) || 0; threadIDsToPinnedCount.set(threadID, pinnedCount + 1); } } // 7. Include a pinnedCount for each rawThreadInfo const rawThreadInfosWithPinnedCount = rawThreadInfos.map(threadInfo => ({ ...threadInfo, pinnedCount: threadIDsToPinnedCount.get(threadInfo.id) || 0, })); // 8. Convert rawThreadInfos to a map of threadID to threadInfo const threadIDToThreadInfo = rawThreadInfosWithPinnedCount.reduce( ( acc: { [string]: LegacyRawThreadInfo }, threadInfo: LegacyRawThreadInfo, ) => { acc[threadInfo.id] = threadInfo; return acc; }, {}, ); // 9. Add threadPermission to each threadInfo const rawThreadInfosWithThreadPermission = persistMigrationForManagePinsThreadPermission(threadIDToThreadInfo); // 10. Convert the new threadInfos back into an array const rawThreadInfosWithCountAndPermission = Object.keys( rawThreadInfosWithThreadPermission, ).map(id => rawThreadInfosWithThreadPermission[id]); // 11. Translate `RawThreadInfo`s to `ClientDBThreadInfo`s. const convertedClientDBThreadInfos = rawThreadInfosWithCountAndPermission.map( convertRawThreadInfoToClientDBThreadInfo, ); // 12. Construct `ClientDBThreadStoreOperation`s to clear SQLite `threads` // table and repopulate with `ClientDBThreadInfo`s. const operations: $ReadOnlyArray = [ { type: 'remove_all', }, ...convertedClientDBThreadInfos.map((thread: ClientDBThreadInfo) => ({ type: 'replace', payload: thread, })), ]; // 13. Try processing `ClientDBThreadStoreOperation`s and log out if // `processThreadStoreOperationsSync(...)` throws an exception. try { commCoreModule.processThreadStoreOperationsSync(operations); } catch (exception) { console.log(exception); return { ...state, cookie: null }; } return state; }, [37]: (state: AppState) => { const operations = messageStoreOpsHandlers.convertOpsToClientDBOps([ { type: 'remove_all_threads', }, { type: 'replace_threads', payload: { threads: state.messageStore.threads }, }, ]); try { commCoreModule.processMessageStoreOperationsSync(operations); } catch (exception) { console.error(exception); if (isTaskCancelledError(exception)) { return state; } return { ...state, cookie: null }; } return state; }, [38]: (state: AppState) => deprecatedUpdateClientDBThreadStoreThreadInfos( state, legacyUpdateRolesAndPermissions, ), [39]: (state: AppState) => unshimClientDB(state, [messageTypes.EDIT_MESSAGE]), [40]: (state: AppState) => deprecatedUpdateClientDBThreadStoreThreadInfos( state, legacyUpdateRolesAndPermissions, ), [41]: (state: AppState) => { const queuedReports = state.reportStore.queuedReports.map(report => ({ ...report, id: getUUID(), })); return { ...state, reportStore: { ...state.reportStore, queuedReports }, }; }, [42]: (state: AppState) => { const reportStoreOperations: $ReadOnlyArray = [ { type: 'remove_all_reports' }, ...convertReportsToReplaceReportOps(state.reportStore.queuedReports), ]; const dbOperations: $ReadOnlyArray = reportStoreOpsHandlers.convertOpsToClientDBOps(reportStoreOperations); try { commCoreModule.processReportStoreOperationsSync(dbOperations); } catch (exception) { if (isTaskCancelledError(exception)) { return state; } return { ...state, cookie: null }; } return state; }, [43]: async (state: any) => { const { messages, drafts, threads, messageStoreThreads } = await commCoreModule.getClientDBStore(); const messageStoreThreadsOperations = createUpdateDBOpsForMessageStoreThreads( messageStoreThreads, convertMessageStoreThreadsToNewIDSchema, ); const messageStoreMessagesOperations = createUpdateDBOpsForMessageStoreMessages(messages, messageInfos => messageInfos.map(convertRawMessageInfoToNewIDSchema), ); const threadOperations = deprecatedCreateUpdateDBOpsForThreadStoreThreadInfos( threads, convertThreadStoreThreadInfosToNewIDSchema, ); const draftOperations = generateIDSchemaMigrationOpsForDrafts(drafts); try { await commCoreModule.processDBStoreOperations({ messageStoreOperations: [ ...messageStoreMessagesOperations, ...messageStoreThreadsOperations, ], threadStoreOperations: threadOperations, draftStoreOperations: draftOperations, }); } catch (exception) { console.log(exception); return { ...state, cookie: null }; } const inviteLinksStore = state.inviteLinksStore ?? defaultState.inviteLinksStore; return { ...state, entryStore: convertEntryStoreToNewIDSchema(state.entryStore), messageStore: convertMessageStoreToNewIDSchema(state.messageStore), calendarFilters: state.calendarFilters.map( convertCalendarFilterToNewIDSchema, ), connection: convertConnectionInfoToNewIDSchema(state.connection), watchedThreadIDs: state.watchedThreadIDs.map( id => `${authoritativeKeyserverID}|${id}`, ), inviteLinksStore: convertInviteLinksStoreToNewIDSchema(inviteLinksStore), }; }, [44]: async (state: any) => { const { cookie, ...rest } = state; return { ...rest, keyserverStore: { keyserverInfos: { [authoritativeKeyserverID]: { cookie } }, }, }; }, [45]: async (state: any) => { const { updatesCurrentAsOf, keyserverStore, ...rest } = state; return { ...rest, keyserverStore: { ...keyserverStore, keyserverInfos: { ...keyserverStore.keyserverInfos, [authoritativeKeyserverID]: { ...keyserverStore.keyserverInfos[authoritativeKeyserverID], updatesCurrentAsOf, }, }, }, }; }, [46]: async (state: AppState) => { const { currentAsOf } = state.messageStore; return { ...state, messageStore: { ...state.messageStore, currentAsOf: { [authoritativeKeyserverID]: currentAsOf }, }, }; }, [47]: async (state: any) => { const { urlPrefix, keyserverStore, ...rest } = state; return { ...rest, keyserverStore: { ...keyserverStore, keyserverInfos: { ...keyserverStore.keyserverInfos, [authoritativeKeyserverID]: { ...keyserverStore.keyserverInfos[authoritativeKeyserverID], urlPrefix, }, }, }, }; }, [48]: async (state: any) => { const { connection, keyserverStore, ...rest } = state; return { ...rest, keyserverStore: { ...keyserverStore, keyserverInfos: { ...keyserverStore.keyserverInfos, [authoritativeKeyserverID]: { ...keyserverStore.keyserverInfos[authoritativeKeyserverID], connection, }, }, }, }; }, [49]: async (state: AppState) => { const { keyserverStore, ...rest } = state; const { connection, ...keyserverRest } = keyserverStore.keyserverInfos[authoritativeKeyserverID]; return { ...rest, keyserverStore: { ...keyserverStore, keyserverInfos: { ...keyserverStore.keyserverInfos, [authoritativeKeyserverID]: { ...keyserverRest, }, }, }, connection, }; }, [50]: async (state: any) => { const { connection, ...rest } = state; const { actualizedCalendarQuery, ...connectionRest } = connection; return { ...rest, connection: connectionRest, actualizedCalendarQuery, }; }, [51]: async (state: any) => { const { lastCommunicatedPlatformDetails, keyserverStore, ...rest } = state; return { ...rest, keyserverStore: { ...keyserverStore, keyserverInfos: { ...keyserverStore.keyserverInfos, [authoritativeKeyserverID]: { ...keyserverStore.keyserverInfos[authoritativeKeyserverID], lastCommunicatedPlatformDetails, }, }, }, }; }, [52]: async (state: AppState) => ({ ...state, integrityStore: { threadHashes: {}, threadHashingStatus: 'data_not_loaded', }, }), [53]: (state: any) => { if (!state.userStore.inconsistencyReports) { return state; } const reportStoreOperations = convertReportsToReplaceReportOps( state.userStore.inconsistencyReports, ); const dbOperations: $ReadOnlyArray = reportStoreOpsHandlers.convertOpsToClientDBOps(reportStoreOperations); try { commCoreModule.processReportStoreOperationsSync(dbOperations); } catch (exception) { if (isTaskCancelledError(exception)) { return state; } return handleReduxMigrationFailure(state); } const { inconsistencyReports, ...newUserStore } = state.userStore; const queuedReports = reportStoreOpsHandlers.processStoreOperations( state.reportStore.queuedReports, reportStoreOperations, ); return { ...state, userStore: newUserStore, reportStore: { ...state.reportStore, queuedReports, }, }; }, [54]: (state: any) => { let updatedMessageStoreThreads: MessageStoreThreads = {}; for (const threadID: string in state.messageStore.threads) { const { lastNavigatedTo, lastPruned, ...rest } = state.messageStore.threads[threadID]; updatedMessageStoreThreads = { ...updatedMessageStoreThreads, [threadID]: rest, }; } return { ...state, messageStore: { ...state.messageStore, threads: updatedMessageStoreThreads, }, }; }, [55]: async (state: AppState) => __DEV__ ? { ...state, keyserverStore: { ...state.keyserverStore, keyserverInfos: { ...state.keyserverStore.keyserverInfos, [authoritativeKeyserverID]: { ...state.keyserverStore.keyserverInfos[ authoritativeKeyserverID ], urlPrefix: defaultURLPrefix, }, }, }, } : state, [56]: (state: any) => { const { deviceToken, keyserverStore, ...rest } = state; return { ...rest, keyserverStore: { ...keyserverStore, keyserverInfos: { ...keyserverStore.keyserverInfos, [authoritativeKeyserverID]: { ...keyserverStore.keyserverInfos[authoritativeKeyserverID], deviceToken, }, }, }, }; }, [57]: async (state: any) => { const { connection, keyserverStore: { keyserverInfos }, ...rest } = state; const newKeyserverInfos: { [string]: KeyserverInfo } = {}; for (const key in keyserverInfos) { newKeyserverInfos[key] = { ...keyserverInfos[key], connection: { ...defaultConnectionInfo }, }; } return { ...rest, keyserverStore: { ...state.keyserverStore, keyserverInfos: newKeyserverInfos, }, }; }, [58]: async (state: AppState) => { const userStoreOperations: $ReadOnlyArray = [ { type: 'remove_all_users' }, ...convertUserInfosToReplaceUserOps(state.userStore.userInfos), ]; const dbOperations: $ReadOnlyArray = userStoreOpsHandlers.convertOpsToClientDBOps(userStoreOperations); try { await commCoreModule.processDBStoreOperations({ userStoreOperations: dbOperations, }); } catch (exception) { if (isTaskCancelledError(exception)) { return state; } return handleReduxMigrationFailure(state); } return state; }, [59]: (state: AppState) => { const clientDBThreadInfos = commCoreModule.getAllThreadsSync(); const rawThreadInfos = clientDBThreadInfos.map( deprecatedConvertClientDBThreadInfoToRawThreadInfo, ); const rawThreadInfosObject = rawThreadInfos.reduce( ( acc: { [string]: LegacyRawThreadInfo }, threadInfo: LegacyRawThreadInfo, ) => { acc[threadInfo.id] = threadInfo; return acc; }, {}, ); const migratedRawThreadInfos = persistMigrationToRemoveSelectRolePermissions(rawThreadInfosObject); const migratedThreadInfosArray = Object.keys(migratedRawThreadInfos).map( id => migratedRawThreadInfos[id], ); const convertedClientDBThreadInfos = migratedThreadInfosArray.map( convertRawThreadInfoToClientDBThreadInfo, ); const operations: $ReadOnlyArray = [ { type: 'remove_all', }, ...convertedClientDBThreadInfos.map((thread: ClientDBThreadInfo) => ({ type: 'replace', payload: thread, })), ]; try { commCoreModule.processThreadStoreOperationsSync(operations); } catch (exception) { console.log(exception); return handleReduxMigrationFailure(state); } return state; }, [60]: (state: AppState) => deprecatedUpdateClientDBThreadStoreThreadInfos( state, legacyUpdateRolesAndPermissions, handleReduxMigrationFailure, ), [61]: (state: AppState) => { const minimallyEncodeThreadInfosFunc = ( threadStoreInfos: MixedRawThreadInfos, ): MixedRawThreadInfos => Object.keys(threadStoreInfos).reduce( ( acc: { [string]: LegacyRawThreadInfo | RawThreadInfo, }, key: string, ) => { const threadInfo = threadStoreInfos[key]; acc[threadInfo.id] = threadInfo.minimallyEncoded ? threadInfo : minimallyEncodeRawThreadInfo(threadInfo); return acc; }, {}, ); return deprecatedUpdateClientDBThreadStoreThreadInfos( state, minimallyEncodeThreadInfosFunc, handleReduxMigrationFailure, ); }, [62]: async (state: AppState) => { const replaceOps: $ReadOnlyArray = entries( state.keyserverStore.keyserverInfos, ).map(([id, keyserverInfo]) => ({ type: 'replace_keyserver', payload: { id, keyserverInfo, }, })); const dbOperations: $ReadOnlyArray = keyserverStoreOpsHandlers.convertOpsToClientDBOps([ { type: 'remove_all_keyservers' }, ...replaceOps, ]); try { await commCoreModule.processDBStoreOperations({ keyserverStoreOperations: dbOperations, }); } catch (exception) { if (isTaskCancelledError(exception)) { return state; } return handleReduxMigrationFailure(state); } return state; }, [63]: async (state: any) => { const { actualizedCalendarQuery, ...rest } = state; const operations: $ReadOnlyArray = entries( state.keyserverStore.keyserverInfos, ).map(([id, keyserverInfo]) => ({ type: 'replace_keyserver', payload: { id, keyserverInfo: { ...keyserverInfo, actualizedCalendarQuery: { ...actualizedCalendarQuery, filters: filterThreadIDsInFilterList( actualizedCalendarQuery.filters, (threadID: string) => extractKeyserverIDFromID(threadID) === id, ), }, }, }, })); const dbOperations: $ReadOnlyArray = keyserverStoreOpsHandlers.convertOpsToClientDBOps(operations); const newState = { ...rest, keyserverStore: keyserverStoreOpsHandlers.processStoreOperations( rest.keyserverStore, operations, ), }; try { await commCoreModule.processDBStoreOperations({ keyserverStoreOperations: dbOperations, }); } catch (exception) { if (isTaskCancelledError(exception)) { return newState; } return handleReduxMigrationFailure(newState); } return newState; }, // Migration 64 is a noop to unblock a `native` release since the previous // contents are not ready to be deployed to prod and we don't want to // decrement migration 65. [64]: (state: AppState) => state, [65]: async (state: AppState) => { const replaceOp: ReplaceIntegrityThreadHashesOperation = { type: 'replace_integrity_thread_hashes', payload: { threadHashes: state.integrityStore.threadHashes, }, }; const dbOperations: $ReadOnlyArray = integrityStoreOpsHandlers.convertOpsToClientDBOps([ { type: 'remove_all_integrity_thread_hashes' }, replaceOp, ]); try { await commCoreModule.processDBStoreOperations({ integrityStoreOperations: dbOperations, }); } catch (exception) { if (isTaskCancelledError(exception)) { return state; } return handleReduxMigrationFailure(state); } return state; }, [66]: async (state: AppState) => { const stores = await commCoreModule.getClientDBStore(); const keyserversDBInfo = stores.keyservers; const { translateClientDBData } = keyserverStoreOpsHandlers; const keyservers = translateClientDBData(keyserversDBInfo); // There is no modification of the keyserver data, but the ops handling // should correctly split the data between synced and non-synced tables const replaceOps: $ReadOnlyArray = entries( keyservers, ).map(([id, keyserverInfo]) => ({ type: 'replace_keyserver', payload: { id, keyserverInfo, }, })); const keyserverStoreOperations: $ReadOnlyArray = keyserverStoreOpsHandlers.convertOpsToClientDBOps([ { type: 'remove_all_keyservers' }, ...replaceOps, ]); try { await commCoreModule.processDBStoreOperations({ keyserverStoreOperations, }); } catch (exception) { if (isTaskCancelledError(exception)) { return state; } return handleReduxMigrationFailure(state); } return state; }, [67]: (state: any) => { const { nextLocalID, ...rest } = state; return rest; }, [68]: async (state: AppState) => { const { userStore, ...rest } = state; return rest; }, [69]: (state: any) => { const { notifPermissionAlertInfo, ...rest } = state; const newState = { ...rest, alertStore: { alertInfos: defaultAlertInfos, }, }; return newState; }, [70]: (state: any) => { const clientDBMessageInfos = commCoreModule.getAllMessagesSync(); const unsupportedMessageIDsToRemove = clientDBMessageInfos .filter( message => parseInt(message.type) === messageTypes.UNSUPPORTED && parseInt(message.future_type) === messageTypes.UPDATE_RELATIONSHIP, ) .map(message => message.id); const messageStoreOperations: $ReadOnlyArray = [ { type: 'remove', payload: { ids: unsupportedMessageIDsToRemove }, }, ]; try { commCoreModule.processMessageStoreOperationsSync(messageStoreOperations); } catch (exception) { console.log(exception); if (isTaskCancelledError(exception)) { return state; } return { ...state, cookie: null }; } return state; }, [71]: async (state: AppState) => { const replaceOps: $ReadOnlyArray = entries(state.threadActivityStore).map(([threadID, entry]) => ({ type: 'replace_thread_activity_entry', payload: { id: threadID, threadActivityStoreEntry: entry, }, })); const dbOperations: $ReadOnlyArray = threadActivityStoreOpsHandlers.convertOpsToClientDBOps([ { type: 'remove_all_thread_activity_entries' }, ...replaceOps, ]); try { await commCoreModule.processDBStoreOperations({ threadActivityStoreOperations: dbOperations, }); } catch (exception) { if (isTaskCancelledError(exception)) { return state; } return handleReduxMigrationFailure(state); } return state; }, [72]: (state: AppState) => updateClientDBThreadStoreThreadInfos( state, patchRawThreadInfosWithSpecialRole, handleReduxMigrationFailure, ), [73]: (state: AppState) => { return { ...state, alertStore: { ...state.alertStore, alertInfos: { ...state.alertStore.alertInfos, [alertTypes.SIWE_BACKUP_MESSAGE]: defaultAlertInfo, }, }, }; }, [74]: (state: AppState) => unshimClientDB( state, [messageTypes.UPDATE_RELATIONSHIP], handleReduxMigrationFailure, ), [75]: async (state: AppState) => { const replaceOps: $ReadOnlyArray = entries( state.entryStore.entryInfos, ).map(([id, entry]) => ({ type: 'replace_entry', payload: { id, entry, }, })); const dbOperations: $ReadOnlyArray = entryStoreOpsHandlers.convertOpsToClientDBOps([ { type: 'remove_all_entries' }, ...replaceOps, ]); try { await commCoreModule.processDBStoreOperations({ entryStoreOperations: dbOperations, }); } catch (exception) { if (isTaskCancelledError(exception)) { return state; } return handleReduxMigrationFailure(state); } return state; }, }; type PersistedReportStore = $Diff< ReportStore, { +queuedReports: $ReadOnlyArray }, >; const reportStoreTransform: Transform = createTransform( (state: ReportStore): PersistedReportStore => { return { enabledReports: state.enabledReports }; }, (state: PersistedReportStore): ReportStore => { return { ...state, queuedReports: [] }; }, { whitelist: ['reportStore'] }, ); const migrations = { // This migration doesn't change the store but sets a persisted version // in the DB [75]: (state: AppState) => ({ state, ops: [], }), }; const persistConfig = { key: 'root', storage: AsyncStorage, blacklist: persistBlacklist, debug: __DEV__, version: 75, transforms: [ messageStoreMessagesBlocklistTransform, reportStoreTransform, keyserverStoreTransform, ], migrate: (createAsyncMigrate( legacyMigrations, { debug: __DEV__ }, migrations, (error: Error, state: AppState) => { if (isTaskCancelledError(error)) { return state; } return handleReduxMigrationFailure(state); }, ): any), timeout: ((__DEV__ ? 0 : undefined): number | void), }; const codeVersion: number = commCoreModule.getCodeVersion(); // This local exists to avoid a circular dependency where redux-setup needs to // import all the navigation and screen stuff, but some of those screens want to // access the persistor to purge its state. let storedPersistor = null; function setPersistor(persistor: *) { storedPersistor = persistor; } function getPersistor(): Persistor { invariant(storedPersistor, 'should be set'); return storedPersistor; } export { persistConfig, codeVersion, setPersistor, getPersistor }; diff --git a/web/redux/persist.js b/web/redux/persist.js index 2a2fb12b1..7825f3747 100644 --- a/web/redux/persist.js +++ b/web/redux/persist.js @@ -1,537 +1,535 @@ // @flow import invariant from 'invariant'; import { getStoredState, purgeStoredState } from 'redux-persist'; import storage from 'redux-persist/es/storage/index.js'; import type { PersistConfig } from 'redux-persist/src/types.js'; import { type ClientDBKeyserverStoreOperation, keyserverStoreOpsHandlers, type ReplaceKeyserverOperation, } from 'lib/ops/keyserver-store-ops.js'; import type { ClientDBMessageStoreOperation } from 'lib/ops/message-store-ops.js'; import type { ClientDBThreadStoreOperation } from 'lib/ops/thread-store-ops.js'; import { patchRawThreadInfoWithSpecialRole } from 'lib/permissions/special-roles.js'; -import { - createAsyncMigrate, - type StorageMigrationFunction, -} from 'lib/shared/create-async-migrate.js'; import { keyserverStoreTransform } from 'lib/shared/transforms/keyserver-store-transform.js'; import { messageStoreMessagesBlocklistTransform } from 'lib/shared/transforms/message-store-transform.js'; import { defaultAlertInfos } from 'lib/types/alert-types.js'; import { defaultCalendarQuery } from 'lib/types/entry-types.js'; import type { KeyserverInfo } from 'lib/types/keyserver-types.js'; import { messageTypes } from 'lib/types/message-types-enum.js'; import type { ClientDBMessageInfo } from 'lib/types/message-types.js'; import type { WebNavInfo } from 'lib/types/nav-types.js'; import { cookieTypes } from 'lib/types/session-types.js'; import { defaultConnectionInfo } from 'lib/types/socket-types.js'; import { defaultGlobalThemeInfo } from 'lib/types/theme-types.js'; import type { ClientDBThreadInfo } from 'lib/types/thread-types.js'; import { getConfig } from 'lib/utils/config.js'; import { parseCookies } from 'lib/utils/cookie-utils.js'; import { isDev } from 'lib/utils/dev-utils.js'; import { generateIDSchemaMigrationOpsForDrafts, convertDraftStoreToNewIDSchema, + createAsyncMigrate, + type StorageMigrationFunction, } from 'lib/utils/migration-utils.js'; import { entries } from 'lib/utils/objects.js'; import { convertClientDBThreadInfoToRawThreadInfo, convertRawThreadInfoToClientDBThreadInfo, } from 'lib/utils/thread-ops-utils.js'; import commReduxStorageEngine from './comm-redux-storage-engine.js'; import { handleReduxMigrationFailure, persistWhitelist, } from './handle-redux-migration-failure.js'; import { rootKey, rootKeyPrefix, storeVersion } from './persist-constants.js'; import type { AppState } from './redux-setup.js'; import { unshimClientDB } from './unshim-utils.js'; import { authoritativeKeyserverID } from '../authoritative-keyserver.js'; import { getCommSharedWorker } from '../shared-worker/shared-worker-provider.js'; import { getOlmWasmPath } from '../shared-worker/utils/constants.js'; import { isSQLiteSupported } from '../shared-worker/utils/db-utils.js'; import { workerRequestMessageTypes } from '../types/worker-types.js'; declare var keyserverURL: string; const legacyMigrations = { [1]: async (state: any) => { const { primaryIdentityPublicKey, ...stateWithoutPrimaryIdentityPublicKey } = state; return { ...stateWithoutPrimaryIdentityPublicKey, cryptoStore: { primaryAccount: null, primaryIdentityKeys: null, notificationAccount: null, notificationIdentityKeys: null, }, }; }, [2]: async (state: AppState) => { return state; }, [3]: async (state: AppState) => { let newState = state; if (state.draftStore) { newState = { ...newState, draftStore: convertDraftStoreToNewIDSchema(state.draftStore), }; } const sharedWorker = await getCommSharedWorker(); const isSupported = await sharedWorker.isSupported(); if (!isSupported) { return newState; } const stores = await sharedWorker.schedule({ type: workerRequestMessageTypes.GET_CLIENT_STORE, }); invariant(stores?.store, 'Stores should exist'); await sharedWorker.schedule({ type: workerRequestMessageTypes.PROCESS_STORE_OPERATIONS, storeOperations: { draftStoreOperations: generateIDSchemaMigrationOpsForDrafts( stores.store.drafts, ), }, }); return newState; }, [4]: async (state: any) => { const { lastCommunicatedPlatformDetails, keyserverStore, ...rest } = state; return { ...rest, keyserverStore: { ...keyserverStore, keyserverInfos: { ...keyserverStore.keyserverInfos, [authoritativeKeyserverID]: { ...keyserverStore.keyserverInfos[authoritativeKeyserverID], lastCommunicatedPlatformDetails, }, }, }, }; }, [5]: async (state: any) => { const sharedWorker = await getCommSharedWorker(); const isSupported = await sharedWorker.isSupported(); if (!isSupported) { return state; } if (!state.draftStore) { return state; } const { drafts } = state.draftStore; const draftStoreOperations = []; for (const key in drafts) { const text = drafts[key]; draftStoreOperations.push({ type: 'update', payload: { key, text }, }); } await sharedWorker.schedule({ type: workerRequestMessageTypes.PROCESS_STORE_OPERATIONS, storeOperations: { draftStoreOperations }, }); return state; }, [6]: async (state: AppState) => ({ ...state, integrityStore: { threadHashes: {}, threadHashingStatus: 'starting' }, }), [7]: async (state: AppState): Promise => { if (!document.cookie) { return state; } const params = parseCookies(document.cookie); let cookie = null; if (params[cookieTypes.USER]) { cookie = `${cookieTypes.USER}=${params[cookieTypes.USER]}`; } else if (params[cookieTypes.ANONYMOUS]) { cookie = `${cookieTypes.ANONYMOUS}=${params[cookieTypes.ANONYMOUS]}`; } return { ...state, keyserverStore: { ...state.keyserverStore, keyserverInfos: { ...state.keyserverStore.keyserverInfos, [authoritativeKeyserverID]: { ...state.keyserverStore.keyserverInfos[authoritativeKeyserverID], cookie, }, }, }, }; }, [8]: async (state: AppState) => ({ ...state, globalThemeInfo: defaultGlobalThemeInfo, }), [9]: async (state: AppState) => ({ ...state, keyserverStore: { ...state.keyserverStore, keyserverInfos: { ...state.keyserverStore.keyserverInfos, [authoritativeKeyserverID]: { ...state.keyserverStore.keyserverInfos[authoritativeKeyserverID], urlPrefix: keyserverURL, }, }, }, }), [10]: async (state: AppState) => { const { keyserverInfos } = state.keyserverStore; const newKeyserverInfos: { [string]: KeyserverInfo } = {}; for (const key in keyserverInfos) { newKeyserverInfos[key] = { ...keyserverInfos[key], connection: { ...defaultConnectionInfo }, updatesCurrentAsOf: 0, sessionID: null, }; } return { ...state, keyserverStore: { ...state.keyserverStore, keyserverInfos: newKeyserverInfos, }, }; }, [11]: async (state: AppState) => { const sharedWorker = await getCommSharedWorker(); const isSupported = await sharedWorker.isSupported(); if (!isSupported) { return state; } const replaceOps: $ReadOnlyArray = entries( state.keyserverStore.keyserverInfos, ).map(([id, keyserverInfo]) => ({ type: 'replace_keyserver', payload: { id, keyserverInfo, }, })); const keyserverStoreOperations: $ReadOnlyArray = keyserverStoreOpsHandlers.convertOpsToClientDBOps([ { type: 'remove_all_keyservers' }, ...replaceOps, ]); try { await sharedWorker.schedule({ type: workerRequestMessageTypes.PROCESS_STORE_OPERATIONS, storeOperations: { keyserverStoreOperations }, }); return state; } catch (e) { console.log(e); return handleReduxMigrationFailure(state); } }, [12]: async (state: AppState) => { const sharedWorker = await getCommSharedWorker(); const isSupported = await sharedWorker.isSupported(); if (!isSupported) { return state; } const replaceOps: $ReadOnlyArray = entries( state.keyserverStore.keyserverInfos, ) .filter(([, keyserverInfo]) => !keyserverInfo.actualizedCalendarQuery) .map(([id, keyserverInfo]) => ({ type: 'replace_keyserver', payload: { id, keyserverInfo: { ...keyserverInfo, actualizedCalendarQuery: defaultCalendarQuery( getConfig().platformDetails.platform, ), }, }, })); if (replaceOps.length === 0) { return state; } const newState = { ...state, keyserverStore: keyserverStoreOpsHandlers.processStoreOperations( state.keyserverStore, replaceOps, ), }; const keyserverStoreOperations = keyserverStoreOpsHandlers.convertOpsToClientDBOps(replaceOps); try { await sharedWorker.schedule({ type: workerRequestMessageTypes.PROCESS_STORE_OPERATIONS, storeOperations: { keyserverStoreOperations }, }); return newState; } catch (e) { console.log(e); return handleReduxMigrationFailure(newState); } }, [13]: async (state: any) => { const { cryptoStore, ...rest } = state; const sharedWorker = await getCommSharedWorker(); await sharedWorker.schedule({ type: workerRequestMessageTypes.INITIALIZE_CRYPTO_ACCOUNT, olmWasmPath: getOlmWasmPath(), initialCryptoStore: cryptoStore, }); return rest; }, [14]: async (state: AppState) => { const sharedWorker = await getCommSharedWorker(); const isSupported = await sharedWorker.isSupported(); if (!isSupported) { return state; } const stores = await sharedWorker.schedule({ type: workerRequestMessageTypes.GET_CLIENT_STORE, }); const keyserversDBInfo = stores?.store?.keyservers; if (!keyserversDBInfo) { return state; } const { translateClientDBData } = keyserverStoreOpsHandlers; const keyservers = translateClientDBData(keyserversDBInfo); // There is no modification of the keyserver data, but the ops handling // should correctly split the data between synced and non-synced tables const replaceOps: $ReadOnlyArray = entries( keyservers, ).map(([id, keyserverInfo]) => ({ type: 'replace_keyserver', payload: { id, keyserverInfo, }, })); const keyserverStoreOperations: $ReadOnlyArray = keyserverStoreOpsHandlers.convertOpsToClientDBOps([ { type: 'remove_all_keyservers' }, ...replaceOps, ]); try { await sharedWorker.schedule({ type: workerRequestMessageTypes.PROCESS_STORE_OPERATIONS, storeOperations: { keyserverStoreOperations }, }); return state; } catch (e) { console.log(e); return handleReduxMigrationFailure(state); } }, [15]: (state: any) => { const { notifPermissionAlertInfo, ...rest } = state; const newState = { ...rest, alertStore: { alertInfos: defaultAlertInfos, }, }; return newState; }, [16]: async (state: AppState) => { // 1. Check if `databaseModule` is supported and early-exit if not. const sharedWorker = await getCommSharedWorker(); const isDatabaseSupported = await sharedWorker.isSupported(); if (!isDatabaseSupported) { return state; } // 2. Get existing `stores` from SQLite. const stores = await sharedWorker.schedule({ type: workerRequestMessageTypes.GET_CLIENT_STORE, }); const messages: ?$ReadOnlyArray = stores?.store?.messages; if (messages === null || messages === undefined || messages.length === 0) { return state; } // 3. Filter out `UNSUPPORTED.UPDATE_RELATIONSHIP` `ClientDBMessageInfo`s. const unsupportedMessageIDsToRemove = messages .filter((message: ClientDBMessageInfo) => { if (parseInt(message.type) !== messageTypes.UPDATE_RELATIONSHIP) { return false; } if (message.content === null || message.content === undefined) { return false; } const { operation } = JSON.parse(message.content); return operation === 'farcaster_mutual'; }) .map(message => message.id); // 4. Construct `ClientDBMessageStoreOperation`s const messageStoreOperations: $ReadOnlyArray = [ { type: 'remove', payload: { ids: unsupportedMessageIDsToRemove }, }, ]; // 5. Process the constructed `messageStoreOperations`. await sharedWorker.schedule({ type: workerRequestMessageTypes.PROCESS_STORE_OPERATIONS, storeOperations: { messageStoreOperations }, }); return state; }, [17]: async (state: AppState) => { // 1. Check if `databaseModule` is supported and early-exit if not. const sharedWorker = await getCommSharedWorker(); const isDatabaseSupported = await sharedWorker.isSupported(); if (!isDatabaseSupported) { return state; } // 2. Get existing `stores` from SQLite. const stores = await sharedWorker.schedule({ type: workerRequestMessageTypes.GET_CLIENT_STORE, }); const threads: ?$ReadOnlyArray = stores?.store?.threads; if (threads === null || threads === undefined || threads.length === 0) { return state; } // 3. Convert to `RawThreadInfo`, patch in `specialRole`, and convert back. const patchedClientDBThreadInfos: $ReadOnlyArray = threads .map(convertClientDBThreadInfoToRawThreadInfo) .map(patchRawThreadInfoWithSpecialRole) .map(convertRawThreadInfoToClientDBThreadInfo); // 4. Construct operations to remove existing threads and replace them // with threads that have the `specialRole` field patched in. const threadStoreOperations: ClientDBThreadStoreOperation[] = []; threadStoreOperations.push({ type: 'remove_all' }); for (const clientDBThreadInfo: ClientDBThreadInfo of patchedClientDBThreadInfos) { threadStoreOperations.push({ type: 'replace', payload: clientDBThreadInfo, }); } // 5. Process the constructed `threadStoreOperations`. await sharedWorker.schedule({ type: workerRequestMessageTypes.PROCESS_STORE_OPERATIONS, storeOperations: { threadStoreOperations }, }); return state; }, [18]: (state: AppState) => unshimClientDB(state, [messageTypes.UPDATE_RELATIONSHIP]), }; const migrateStorageToSQLite: StorageMigrationFunction< WebNavInfo, AppState, > = async debug => { const sharedWorker = await getCommSharedWorker(); const isSupported = await sharedWorker.isSupported(); if (!isSupported) { return undefined; } const oldStorage = await getStoredState({ storage, key: rootKey }); if (!oldStorage) { return undefined; } purgeStoredState({ storage, key: rootKey }); if (debug) { console.log('redux-persist: migrating state to SQLite storage'); } const allKeys = Object.keys(oldStorage); const transforms = persistConfig.transforms ?? []; const newStorage = { ...oldStorage }; for (const transform of transforms) { for (const key of allKeys) { const transformedStore = transform.out(newStorage[key], key, newStorage); newStorage[key] = transformedStore; } } return newStorage; }; const migrations = { // This migration doesn't change the store but sets a persisted version // in the DB [75]: (state: AppState) => ({ state, ops: [], }), }; const persistConfig: PersistConfig = { keyPrefix: rootKeyPrefix, key: rootKey, storage: commReduxStorageEngine, whitelist: isSQLiteSupported() ? persistWhitelist : [...persistWhitelist, 'draftStore'], migrate: (createAsyncMigrate( legacyMigrations, { debug: isDev }, migrations, (error: Error, state: AppState) => handleReduxMigrationFailure(state), migrateStorageToSQLite, ): any), version: storeVersion, transforms: [messageStoreMessagesBlocklistTransform, keyserverStoreTransform], }; export { persistConfig };