diff --git a/lib/package.json b/lib/package.json index 9ddc49fbc..4f83021e3 100644 --- a/lib/package.json +++ b/lib/package.json @@ -1,71 +1,72 @@ { "name": "lib", "version": "0.0.1", "type": "module", "private": true, "license": "BSD-3-Clause", "scripts": { "clean": "rm -rf node_modules/", "test": "jest" }, "devDependencies": { "buffer": "^6.0.3", "@babel/core": "^7.13.14", "@babel/plugin-proposal-class-properties": "^7.13.0", "@babel/plugin-proposal-nullish-coalescing-operator": "^7.13.8", "@babel/plugin-proposal-object-rest-spread": "^7.13.8", "@babel/plugin-proposal-optional-chaining": "^7.13.12", "@babel/plugin-transform-runtime": "^7.13.10", "@babel/preset-env": "^7.13.12", "@babel/preset-flow": "^7.13.13", "@babel/preset-react": "^7.13.13", "@pmmmwh/react-refresh-webpack-plugin": "^0.5.10", "babel-jest": "^26.6.3", "babel-loader": "^9.1.2", "clean-webpack-plugin": "^4.0.0", "css-loader": "^6.7.3", "css-minimizer-webpack-plugin": "^4.2.2", "flow-bin": "^0.182.0", "flow-typed": "^3.2.1", "mini-css-extract-plugin": "^2.7.2", "react-refresh": "^0.14.0", "style-loader": "^3.3.1", "terser-webpack-plugin": "^5.3.6", "webpack": "^5.76.0" }, "dependencies": { "@rainbow-me/rainbowkit": "^0.12.15", "dateformat": "^3.0.3", "emoji-regex": "^10.2.1", "eth-ens-namehash": "^2.0.8", "ethers": "^5.7.2", "fast-json-stable-stringify": "^2.0.0", "file-type": "^12.3.0", "focus-trap-react": "^10.1.4", "invariant": "^2.2.4", "just-clone": "^3.2.1", "lodash": "^4.17.21", "react": "18.1.0", "react-icomoon": "^2.5.7", "react-redux": "^7.1.1", + "redux-persist": "^6.0.0", "reselect": "^4.0.0", "reselect-map": "^1.0.5", "simple-markdown": "^0.7.2", "string-hash": "^1.1.3", "tcomb": "^3.2.29", "siwe": "^1.1.6", "tinycolor2": "^1.4.1", "tokenize-text": "^1.1.3", "util-inspect": "^0.1.8", "utils-copy-error": "^1.0.1", "wagmi": "^0.12.16" }, "jest": { "transform": { "\\.js$": "babel-jest" }, "transformIgnorePatterns": [ "/node_modules/(?!@babel/runtime)" ] } } diff --git a/web/redux/create-async-migrate.js b/lib/shared/create-async-migrate.js similarity index 71% rename from web/redux/create-async-migrate.js rename to lib/shared/create-async-migrate.js index 65ac24bc2..d6cd598c7 100644 --- a/web/redux/create-async-migrate.js +++ b/lib/shared/create-async-migrate.js @@ -1,92 +1,82 @@ // @flow -import { getStoredState, purgeStoredState } from 'redux-persist'; import { DEFAULT_VERSION } from 'redux-persist/es/constants.js'; -import storage from 'redux-persist/es/storage/index.js'; import type { PersistState } from 'redux-persist/es/types.js'; -import { databaseModule } from '../database/database-module-provider.js'; - type MigrationManifest = { +[number | string]: (PersistedState) => Promise, }; type PersistedState = { +_persist: PersistState, ... } | void; type ConfigType = { +debug: boolean, }; +export type StorageMigrationFunction = ( + debug: boolean, +) => Promise; + function createAsyncMigrate( migrations: MigrationManifest, config: ConfigType, + 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 { + if (!state && storageMigration) { + state = await storageMigration(debug); + } if (!state) { - const isSupported = await databaseModule.isDatabaseSupported(); - if (!isSupported) { - if (debug) { - console.log('redux-persist: no inbound state, skipping migration'); - } - return undefined; - } - - const oldStorage = await getStoredState({ storage, key: 'root' }); - if (!oldStorage) { - return undefined; - } - - state = oldStorage; - purgeStoredState({ storage, key: 'root' }); if (debug) { - console.log('redux-persist: migrating state to SQLite storage'); + 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 newMigrationKeys = Object.keys(migrations) .map(ver => parseInt(ver)) .filter(key => currentVersion >= key && key > inboundVersion); const sortedMigrationKeys = newMigrationKeys.sort((a, b) => a - b); if (debug) { console.log('redux-persist: migrationKeys', sortedMigrationKeys); } let migratedState: PersistedState = state; for (const versionKey of sortedMigrationKeys) { if (debug) { console.log( 'redux-persist: running migration for versionKey', versionKey, ); } if (versionKey) { migratedState = await migrations[versionKey](migratedState); } } return migratedState; }; } export { createAsyncMigrate }; diff --git a/web/redux/persist.js b/web/redux/persist.js index d6564e596..95de8268f 100644 --- a/web/redux/persist.js +++ b/web/redux/persist.js @@ -1,80 +1,110 @@ // @flow +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 { + createAsyncMigrate, + type StorageMigrationFunction, +} from 'lib/shared/create-async-migrate.js'; import { isDev } from 'lib/utils/dev-utils.js'; import commReduxStorageEngine from './comm-redux-storage-engine.js'; -import { createAsyncMigrate } from './create-async-migrate.js'; import type { AppState } from './redux-setup.js'; import { databaseModule } from '../database/database-module-provider.js'; import { isSQLiteSupported } from '../database/utils/db-utils.js'; import { workerRequestMessageTypes } from '../types/worker-types.js'; declare var preloadedState: AppState; const initiallyLoggedInUserID = preloadedState.currentUserInfo?.anonymous ? undefined : preloadedState.currentUserInfo?.id; const isDatabaseSupported = isSQLiteSupported(initiallyLoggedInUserID); const migrations = { [1]: async state => { const { primaryIdentityPublicKey, ...stateWithoutPrimaryIdentityPublicKey } = state; return { ...stateWithoutPrimaryIdentityPublicKey, cryptoStore: { primaryAccount: null, primaryIdentityKeys: null, notificationAccount: null, notificationIdentityKeys: null, }, }; }, [2]: async state => { if (!isDatabaseSupported) { 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 databaseModule.schedule({ type: workerRequestMessageTypes.PROCESS_STORE_OPERATIONS, storeOperations: { draftStoreOperations }, }); return state; }, }; const persistWhitelist = [ 'enabledApps', 'deviceID', 'cryptoStore', 'notifPermissionAlertInfo', 'commServicesAccessToken', 'lastCommunicatedPlatformDetails', ]; +const rootKey = 'root'; + +const migrateStorageToSQLite: StorageMigrationFunction = async debug => { + const isSupported = await databaseModule.isDatabaseSupported(); + 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'); + } + + return oldStorage; +}; + const persistConfig: PersistConfig = { - key: 'root', + key: rootKey, storage: commReduxStorageEngine, whitelist: isDatabaseSupported ? persistWhitelist : [...persistWhitelist, 'draftStore'], - migrate: (createAsyncMigrate(migrations, { debug: isDev }): any), + migrate: (createAsyncMigrate( + migrations, + { debug: isDev }, + migrateStorageToSQLite, + ): any), version: 2, }; export { persistConfig };