From 5df861e7606a32f2a81a49992628a6200214bf12 Mon Sep 17 00:00:00 2001 From: shrouxm Date: Fri, 24 Jan 2025 10:53:11 -0800 Subject: [PATCH] refactor: change mmkv library --- dev-client/jest.integration.config.js | 3 +- dev-client/jest.unit.config.js | 6 +- dev-client/jest/integration/setup.ts | 9 --- dev-client/jest/unit/setup.ts | 9 --- dev-client/package-lock.json | 18 +++-- dev-client/package.json | 2 +- dev-client/src/config/featureFlags.test.ts | 5 +- dev-client/src/config/index.ts | 4 +- dev-client/src/hooks/useStorage.ts | 30 --------- .../navigation/navigators/RootNavigator.tsx | 4 +- dev-client/src/persistence/kvStorage.test.ts | 4 +- dev-client/src/persistence/kvStorage.ts | 65 ++++++++++++++----- dev-client/src/screens/WelcomeScreen.tsx | 4 +- dev-client/src/store/persistence.test.ts | 2 +- dev-client/src/store/persistence.ts | 4 +- 15 files changed, 71 insertions(+), 98 deletions(-) delete mode 100644 dev-client/src/hooks/useStorage.ts diff --git a/dev-client/jest.integration.config.js b/dev-client/jest.integration.config.js index 4922ff0d9..46cc7757e 100644 --- a/dev-client/jest.integration.config.js +++ b/dev-client/jest.integration.config.js @@ -22,11 +22,10 @@ module.exports = { setupFilesAfterEnv: [ '@testing-library/jest-native/extend-expect', '@rnmapbox/maps/setup-jest', - './node_modules/react-native-mmkv-storage/jest/mmkvJestSetup.js', '/jest/integration/setup.ts', ], transformIgnorePatterns: [ - 'node_modules/(?!(react-native|@react-native|react-native-cookies|uuid|react-native-mmkv-storage|react-native-autocomplete-input|expo(nent)?|@expo(nent)?/.*)|expo-constants|@rnmapbox/)', + 'node_modules/(?!(react-native|@react-native|react-native-cookies|uuid|react-native-autocomplete-input|expo(nent)?|@expo(nent)?/.*)|expo-constants|@rnmapbox/)', ], moduleNameMapper: { '\\.(jpg|ico|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': diff --git a/dev-client/jest.unit.config.js b/dev-client/jest.unit.config.js index 5342ea635..9da45df57 100644 --- a/dev-client/jest.unit.config.js +++ b/dev-client/jest.unit.config.js @@ -20,11 +20,7 @@ module.exports = { testMatch: ['**/src/**/*.test.[jt]s?(x)'], preset: 'jest-expo', testEnvironment: 'jsdom', - setupFilesAfterEnv: [ - './node_modules/react-native-mmkv-storage/jest/mmkvJestSetup.js', - '/jest/unit/setup.ts', - ], - transformIgnorePatterns: ['/!node_modules\\/react-native-mmkv-storage/'], + setupFilesAfterEnv: ['/jest/unit/setup.ts'], clearMocks: true, moduleNameMapper: { '\\.(jpg|ico|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': diff --git a/dev-client/jest/integration/setup.ts b/dev-client/jest/integration/setup.ts index 3720339e3..221afaa28 100644 --- a/dev-client/jest/integration/setup.ts +++ b/dev-client/jest/integration/setup.ts @@ -46,9 +46,6 @@ jest.mock('terraso-mobile-client/config', () => ({ APP_CONFIG: {}, })); -let mmkvMock = require('react-native-mmkv-storage/jest/dist/jest/memoryStore.js'); -mmkvMock.mock(); // Mock the storage - setAPIConfig({ terrasoAPIURL: 'http://127.0.0.1:8000', graphQLEndpoint: '/graphql/', @@ -67,9 +64,3 @@ global.console.warn = jest.fn(); // for native base animations. Would be nice to figure out how to disable this // For now, just silence global.console.error = jest.fn(); - -beforeEach(() => { - // Install the in-memory adapter - mmkvMock.unmock(); // Cleanup if already mocked - mmkvMock.mock(); // Mock the storage -}); diff --git a/dev-client/jest/unit/setup.ts b/dev-client/jest/unit/setup.ts index 0cf179f75..a017b7ee8 100644 --- a/dev-client/jest/unit/setup.ts +++ b/dev-client/jest/unit/setup.ts @@ -14,12 +14,3 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see https://www.gnu.org/licenses/. */ - -let mmkvMock = require('react-native-mmkv-storage/jest/dist/jest/memoryStore.js'); -mmkvMock.mock(); // Mock the storage - -beforeEach(() => { - // Install the in-memory adapter - mmkvMock.unmock(); // Cleanup if already mocked - mmkvMock.mock(); // Mock the storage -}); diff --git a/dev-client/package-lock.json b/dev-client/package-lock.json index 30261e401..46f627c67 100644 --- a/dev-client/package-lock.json +++ b/dev-client/package-lock.json @@ -53,7 +53,7 @@ "react-native-gesture-handler": "^2.21.2", "react-native-get-random-values": "^1.11.0", "react-native-gradle-plugin": "^0.71.19", - "react-native-mmkv-storage": "^0.11.2", + "react-native-mmkv": "^2.12.2", "react-native-pager-view": "^6.6.1", "react-native-paper": "^5.12.5", "react-native-reanimated": "^3.16.6", @@ -22068,16 +22068,14 @@ "version": "0.71.19", "license": "MIT" }, - "node_modules/react-native-mmkv-storage": { - "version": "0.11.2", - "resolved": "https://registry.npmjs.org/react-native-mmkv-storage/-/react-native-mmkv-storage-0.11.2.tgz", - "integrity": "sha512-/jbYNOUrwgVU09WyXDK6lFGXqBs+23oR9X37z3N68rwHNiXF5WDyXnT38dU2tF07ZlvmsobNHgdxgTu4kGQUKQ==", - "license": "MIT", - "bin": { - "mmkv-link": "autolink/postlink/run.js" - }, + "node_modules/react-native-mmkv": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/react-native-mmkv/-/react-native-mmkv-2.12.2.tgz", + "integrity": "sha512-6058Aq0p57chPrUutLGe9fYoiDVDNMU2PKV+lLFUJ3GhoHvUrLdsS1PDSCLr00yqzL4WJQ7TTzH+V8cpyrNcfg==", + "license": "(MIT AND BSD-3-Clause)", "peerDependencies": { - "react-native": "*" + "react": "*", + "react-native": ">=0.71.0" } }, "node_modules/react-native-pager-view": { diff --git a/dev-client/package.json b/dev-client/package.json index 2deb8ba5b..97789fc76 100644 --- a/dev-client/package.json +++ b/dev-client/package.json @@ -71,7 +71,7 @@ "react-native-gesture-handler": "^2.21.2", "react-native-get-random-values": "^1.11.0", "react-native-gradle-plugin": "^0.71.19", - "react-native-mmkv-storage": "^0.11.2", + "react-native-mmkv": "^2.12.2", "react-native-pager-view": "^6.6.1", "react-native-paper": "^5.12.5", "react-native-reanimated": "^3.16.6", diff --git a/dev-client/src/config/featureFlags.test.ts b/dev-client/src/config/featureFlags.test.ts index 12382d0b9..14c1296a4 100644 --- a/dev-client/src/config/featureFlags.test.ts +++ b/dev-client/src/config/featureFlags.test.ts @@ -94,9 +94,6 @@ test('offline feature flag starts on in dev mode and can be disabled', () => { expect(isFlagEnabled('FF_offline')).toBe(true); - // This assertion fails due to a bug in the react-native-mmkv-storage library - // It can be commented back in when this is fixed: - // https://github.com/ammarahm-ed/react-native-mmkv-storage/issues/360 - // expect(willFlagBeEnabledOnReload('FF_offline')).toBe(false); + expect(willFlagBeEnabledOnReload('FF_offline')).toBe(false); }); }); diff --git a/dev-client/src/config/index.ts b/dev-client/src/config/index.ts index b063b0d98..bf2fbb44a 100644 --- a/dev-client/src/config/index.ts +++ b/dev-client/src/config/index.ts @@ -34,10 +34,10 @@ setAPIConfig({ return value === null ? undefined : value; }, setToken: (name, value) => { - kvStorage.setStringAsync(name, value); + kvStorage.setString(name, value); }, removeToken: name => { - kvStorage.removeItem(name); + kvStorage.remove(name); }, initialToken: null, }, diff --git a/dev-client/src/hooks/useStorage.ts b/dev-client/src/hooks/useStorage.ts deleted file mode 100644 index e712cb89a..000000000 --- a/dev-client/src/hooks/useStorage.ts +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright © 2024 Technology Matters - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see https://www.gnu.org/licenses/. - */ - -import {useMMKVStorage} from 'react-native-mmkv-storage'; - -import {WELCOME_SCREEN_SEEN_KEY} from 'terraso-mobile-client/constants'; -import {kvStorage} from 'terraso-mobile-client/persistence/kvStorage'; - -if (!kvStorage.indexer.hasKey(WELCOME_SCREEN_SEEN_KEY)) { - kvStorage.setBool(WELCOME_SCREEN_SEEN_KEY, false); -} - -export const useKVStorage = (key: string, defaultValue: any) => { - const [value, setValue] = useMMKVStorage(key, kvStorage, defaultValue); - return [value, setValue]; -}; diff --git a/dev-client/src/navigation/navigators/RootNavigator.tsx b/dev-client/src/navigation/navigators/RootNavigator.tsx index ce8590094..eebc01100 100644 --- a/dev-client/src/navigation/navigators/RootNavigator.tsx +++ b/dev-client/src/navigation/navigators/RootNavigator.tsx @@ -25,13 +25,13 @@ import { } from 'terraso-client-shared/account/accountSlice'; import {WELCOME_SCREEN_SEEN_KEY} from 'terraso-mobile-client/constants'; -import {useKVStorage} from 'terraso-mobile-client/hooks/useStorage'; import {DEFAULT_STACK_NAVIGATOR_OPTIONS} from 'terraso-mobile-client/navigation/constants'; import { modalScreens, screens, } from 'terraso-mobile-client/navigation/screenDefinitions'; import {RootStack} from 'terraso-mobile-client/navigation/types'; +import {kvStorage} from 'terraso-mobile-client/persistence/kvStorage'; import {useDispatch, useSelector} from 'terraso-mobile-client/store'; const modalScreenOptions: NativeStackNavigationOptions = { @@ -47,7 +47,7 @@ export const RootNavigator = () => { state => state.account.currentUser.data !== null, ); - const [welcomeScreenAlreadySeen] = useKVStorage( + const [welcomeScreenAlreadySeen] = kvStorage.useBool( WELCOME_SCREEN_SEEN_KEY, false, ); diff --git a/dev-client/src/persistence/kvStorage.test.ts b/dev-client/src/persistence/kvStorage.test.ts index 07248e921..ab038c3fc 100644 --- a/dev-client/src/persistence/kvStorage.test.ts +++ b/dev-client/src/persistence/kvStorage.test.ts @@ -18,6 +18,6 @@ import {kvStorage} from 'terraso-mobile-client/persistence/kvStorage'; test('kvStorage mock works', () => { - kvStorage.setMap('map-key', {testKey: 'testValue'}); - expect(kvStorage.getMap('map-key')).toEqual({testKey: 'testValue'}); + kvStorage.setObject('map-key', {testKey: 'testValue'}); + expect(kvStorage.getObject('map-key')).toEqual({testKey: 'testValue'}); }); diff --git a/dev-client/src/persistence/kvStorage.ts b/dev-client/src/persistence/kvStorage.ts index 27b77b366..57ced2b20 100644 --- a/dev-client/src/persistence/kvStorage.ts +++ b/dev-client/src/persistence/kvStorage.ts @@ -15,7 +15,13 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import {MMKVLoader} from 'react-native-mmkv-storage'; +import { + MMKV, + useMMKVBoolean, + useMMKVNumber, + useMMKVObject, + useMMKVString, +} from 'react-native-mmkv'; // helpful things to know about MMKV: // @@ -24,15 +30,10 @@ import {MMKVLoader} from 'react-native-mmkv-storage'; // there's the underlying cross-platform native library: // https://github.com/Tencent/MMKV // and then there's the react native wrapper library: -// https://www.npmjs.com/package/react-native-mmkv-storage -// there is also a competing react native wrapper library: // https://www.npmjs.com/package/react-native-mmkv -// -// the library we use simplifies encryption by automatically -// generating an encryption key and storing it on the keychain, -// which we would have to do manually if we were using the other -// library. at time of writing there is not a well-documented -// migration story from one to the other. +// there is also a competing react native wrapper library, which +// we used originally but abandoned due to slow maintenance: +// https://www.npmjs.com/package/react-native-mmkv-storage // // MMKV does not necessarily write to disk after every operation, // it will keep data in memory and only flush it when necessary @@ -40,12 +41,6 @@ import {MMKVLoader} from 'react-native-mmkv-storage'; // way to manually flush to disk, there's a `clearMemoryCache` // method which presumably does this but haven't confirmed. // -// `withEncryption` only has an effect when the device has never -// initialized the MMKV store before. after first usage, the store -// gets initialized with whatever settings it previously used. -// there are additional methods available for changing the encryption -// settings later: https://rnmmkv.vercel.app/#/workingwithencryption -// // There is discussion on all of the libraries of a potential memory // use issue: // https://github.com/ammarahm-ed/react-native-mmkv-storage/issues/286 @@ -53,7 +48,43 @@ import {MMKVLoader} from 'react-native-mmkv-storage'; // https://github.com/Tencent/MMKV/issues/610 // I ran a test in our environment and was unable to reproduce (persisted // entire redux state to storage in a loop for several hours), but leaving -// here for context. There is also a `clearMemoryCache` method on storage +// here for context. There is also a `trim` method on storage // object which may help. -export const kvStorage = new MMKVLoader().withEncryption().initialize(); +const MMKV_STORAGE_ID = 'mmkv.storage'; + +const mmkvStorage = new MMKV({id: MMKV_STORAGE_ID}); + +export const kvStorage = { + setBool: (key: string, value: boolean) => mmkvStorage.set(key, value), + setString: (key: string, value: string) => mmkvStorage.set(key, value), + setNumber: (key: string, value: number) => mmkvStorage.set(key, value), + setObject: (key: string, value: object) => + mmkvStorage.set(key, JSON.stringify(value)), + getBool: (key: string) => mmkvStorage.getBoolean(key), + getString: (key: string) => mmkvStorage.getString(key), + getNumber: (key: string) => mmkvStorage.getNumber(key), + getObject: (key: string) => { + const str = mmkvStorage.getString(key); + if (str === undefined) return str; + return JSON.parse(str) as T; + }, + useBool: (key: string, defaultValue: boolean) => { + const [value, setValue] = useMMKVBoolean(key, mmkvStorage); + return [value ?? defaultValue, setValue] as const; + }, + useString: (key: string, defaultValue: string) => { + const [value, setValue] = useMMKVString(key, mmkvStorage); + return [value ?? defaultValue, setValue] as const; + }, + useNumber: (key: string, defaultValue: number) => { + const [value, setValue] = useMMKVNumber(key, mmkvStorage); + return [value ?? defaultValue, setValue] as const; + }, + useObject: (key: string, defaultValue: T) => { + const [value, setValue] = useMMKVObject(key, mmkvStorage); + return [value ?? defaultValue, setValue] as const; + }, + hasKey: mmkvStorage.contains, + remove: mmkvStorage.delete, +}; diff --git a/dev-client/src/screens/WelcomeScreen.tsx b/dev-client/src/screens/WelcomeScreen.tsx index 0051d7f1f..5c02bd5f5 100644 --- a/dev-client/src/screens/WelcomeScreen.tsx +++ b/dev-client/src/screens/WelcomeScreen.tsx @@ -27,12 +27,12 @@ import {ExternalLink} from 'terraso-mobile-client/components/links/ExternalLink' import {InternalLink} from 'terraso-mobile-client/components/links/InternalLink'; import {Box, Text} from 'terraso-mobile-client/components/NativeBaseAdapters'; import {WELCOME_SCREEN_SEEN_KEY} from 'terraso-mobile-client/constants'; -import {useKVStorage} from 'terraso-mobile-client/hooks/useStorage'; import {useNavigation} from 'terraso-mobile-client/navigation/hooks/useNavigation'; +import {kvStorage} from 'terraso-mobile-client/persistence/kvStorage'; export const WelcomeScreen = () => { const {t} = useTranslation(); - const [, setWelcomeScreenAlreadySeen] = useKVStorage( + const [, setWelcomeScreenAlreadySeen] = kvStorage.useBool( WELCOME_SCREEN_SEEN_KEY, false, ); diff --git a/dev-client/src/store/persistence.test.ts b/dev-client/src/store/persistence.test.ts index 1943d8e98..78768f496 100644 --- a/dev-client/src/store/persistence.test.ts +++ b/dev-client/src/store/persistence.test.ts @@ -74,7 +74,7 @@ describe('persistence middleware', () => { loadPersistedReduxState, } = require('terraso-mobile-client/store/persistence'); - kvStorage.setMap('persisted-redux-state', {counter: 1}); + kvStorage.setObject('persisted-redux-state', {counter: 1}); const store = configureStore({ middleware: [persistenceMiddleware], reducer, diff --git a/dev-client/src/store/persistence.ts b/dev-client/src/store/persistence.ts index 1996d4708..87af8a885 100644 --- a/dev-client/src/store/persistence.ts +++ b/dev-client/src/store/persistence.ts @@ -27,7 +27,7 @@ export const persistenceMiddleware: Middleware = store => next => action => { const result = next(action); if (isFlagEnabled('FF_offline')) { const newState = store.getState(); - kvStorage.setMap(PERSISTED_STATE_KEY, newState); + kvStorage.setObject(PERSISTED_STATE_KEY, newState); } return result; }; @@ -35,7 +35,7 @@ export const persistenceMiddleware: Middleware = store => next => action => { export const loadPersistedReduxState = () => { if (isFlagEnabled('FF_offline')) { return ( - kvStorage.getMap>(PERSISTED_STATE_KEY) ?? undefined + kvStorage.getObject>(PERSISTED_STATE_KEY) ?? undefined ); } };