diff --git a/lib/shared/create-async-migrate.js b/lib/shared/create-async-migrate.js --- a/lib/shared/create-async-migrate.js +++ b/lib/shared/create-async-migrate.js @@ -1,33 +1,47 @@ // @flow import { DEFAULT_VERSION } from 'redux-persist/es/constants.js'; -import type { PersistState } from 'redux-persist/es/types.js'; -type MigrationManifest = { - +[number | string]: (PersistedState) => Promise, +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 = { - +_persist: PersistState, - ... -} | void; +type PersistedState> = T | void; type ConfigType = { +debug: boolean, }; -export type StorageMigrationFunction = ( +export type StorageMigrationFunction> = ( debug: boolean, -) => Promise; +) => Promise>; + +type MigrationManifest> = { + +[number | string]: (PersistedState) => Promise<{ + +state: T, + +ops: StoreOperations, + }>, +}; -function createAsyncMigrate( - migrations: MigrationManifest, +function createAsyncMigrate>( + legacyMigrations: LegacyMigrationManifest, config: ConfigType, - storageMigration: ?StorageMigrationFunction, -): (state: PersistedState, currentVersion: number) => Promise { + 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, + state: ?PersistedState, currentVersion: number, - ): Promise { + ): Promise> { if (!state && storageMigration) { state = await storageMigration(debug); } @@ -53,16 +67,19 @@ return state; } - const newMigrationKeys = Object.keys(migrations) + const migrationKeys = [ + ...Object.keys(legacyMigrations), + ...Object.keys(migrations), + ] .map(ver => parseInt(ver)) .filter(key => currentVersion >= key && key > inboundVersion); - const sortedMigrationKeys = newMigrationKeys.sort((a, b) => a - b); + const sortedMigrationKeys = migrationKeys.sort((a, b) => a - b); if (debug) { console.log('redux-persist: migrationKeys', sortedMigrationKeys); } - let migratedState: PersistedState = state; + let migratedState = state; for (const versionKey of sortedMigrationKeys) { if (debug) { console.log( @@ -70,8 +87,36 @@ versionKey, ); } - if (versionKey) { - migratedState = await migrations[versionKey](migratedState); + + 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); + } } } 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 @@ -1,5 +1,7 @@ // @flow +import type { PersistState } from 'redux-persist/es/types'; + import type { LogOutResult, KeyserverLogOutResult, @@ -174,6 +176,7 @@ +dbOpsStore: DBOpsStore, +syncedMetadataStore: SyncedMetadataStore, +auxUserStore: AuxUserStore, + +_persist: ?PersistState, ... }; diff --git a/native/redux/persist.js b/native/redux/persist.js --- a/native/redux/persist.js +++ b/native/redux/persist.js @@ -137,7 +137,7 @@ import { isTaskCancelledError } from '../utils/error-handling.js'; import { defaultURLPrefix } from '../utils/url-utils.js'; -const migrations = { +const legacyMigrations = { [1]: (state: AppState) => ({ ...state, notifPermissionAlertInfo: defaultAlertInfo, @@ -1305,18 +1305,37 @@ { 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: 74, + version: 75, transforms: [ messageStoreMessagesBlocklistTransform, reportStoreTransform, keyserverStoreTransform, ], - migrate: (createAsyncMigrate(migrations, { debug: __DEV__ }): any), + 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), }; diff --git a/web/redux/persist.js b/web/redux/persist.js --- a/web/redux/persist.js +++ b/web/redux/persist.js @@ -24,6 +24,7 @@ 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'; @@ -57,7 +58,7 @@ declare var keyserverURL: string; -const migrations = { +const legacyMigrations = { [1]: async (state: any) => { const { primaryIdentityPublicKey, @@ -472,7 +473,10 @@ unshimClientDB(state, [messageTypes.UPDATE_RELATIONSHIP]), }; -const migrateStorageToSQLite: StorageMigrationFunction = async debug => { +const migrateStorageToSQLite: StorageMigrationFunction< + WebNavInfo, + AppState, +> = async debug => { const sharedWorker = await getCommSharedWorker(); const isSupported = await sharedWorker.isSupported(); if (!isSupported) { @@ -503,6 +507,15 @@ 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, @@ -511,11 +524,13 @@ ? persistWhitelist : [...persistWhitelist, 'draftStore'], migrate: (createAsyncMigrate( - migrations, + legacyMigrations, { debug: isDev }, + migrations, + (error: Error, state: AppState) => handleReduxMigrationFailure(state), migrateStorageToSQLite, ): any), - version: 17, + version: 75, transforms: [messageStoreMessagesBlocklistTransform, keyserverStoreTransform], };