From aadcde3edd12b8b81d22d9b6cc8205bf1e460a44 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 13 Nov 2023 18:08:17 +1100 Subject: [PATCH] feat(ui): use IndexedDB for persistence IndexedDB has a much larger storage limit than LocalStorage, and is widely supported. Implemented as a custom storage driver for `redux-remember` via `idb-keyval`. `idb-keyval` is a simple wrapper for IndexedDB that allows it to be used easily as a key-value store. The logic to clear persisted storage has been updated throughout the app. --- invokeai/frontend/web/package.json | 1 + .../frontend/web/src/app/components/App.tsx | 6 ++++-- .../web/src/app/components/InvokeAIUI.tsx | 6 +++--- .../src/app/components/ThemeLocaleProvider.tsx | 2 +- .../frontend/web/src/app/store/constants.ts | 9 +-------- invokeai/frontend/web/src/app/store/store.ts | 18 ++++++++++++++---- .../web/src/common/hooks/useClearStorage.ts | 12 ++++++++++++ .../components/SettingsModal/SettingsModal.tsx | 16 +++++----------- invokeai/frontend/web/yarn.lock | 5 +++++ 9 files changed, 46 insertions(+), 29 deletions(-) create mode 100644 invokeai/frontend/web/src/common/hooks/useClearStorage.ts diff --git a/invokeai/frontend/web/package.json b/invokeai/frontend/web/package.json index 6f160bae46..6a6b79c3b7 100644 --- a/invokeai/frontend/web/package.json +++ b/invokeai/frontend/web/package.json @@ -75,6 +75,7 @@ "framer-motion": "^10.16.4", "i18next": "^23.6.0", "i18next-http-backend": "^2.3.1", + "idb-keyval": "^6.2.1", "konva": "^9.2.3", "lodash-es": "^4.17.21", "nanostores": "^0.9.4", diff --git a/invokeai/frontend/web/src/app/components/App.tsx b/invokeai/frontend/web/src/app/components/App.tsx index 63533aee0d..73bd92ffab 100644 --- a/invokeai/frontend/web/src/app/components/App.tsx +++ b/invokeai/frontend/web/src/app/components/App.tsx @@ -21,6 +21,7 @@ import GlobalHotkeys from './GlobalHotkeys'; import PreselectedImage from './PreselectedImage'; import Toaster from './Toaster'; import { useSocketIO } from 'app/hooks/useSocketIO'; +import { useClearStorage } from 'common/hooks/useClearStorage'; const DEFAULT_CONFIG = {}; @@ -36,15 +37,16 @@ const App = ({ config = DEFAULT_CONFIG, selectedImage }: Props) => { const language = useAppSelector(languageSelector); const logger = useLogger('system'); const dispatch = useAppDispatch(); + const clearStorage = useClearStorage(); // singleton! useSocketIO(); const handleReset = useCallback(() => { - localStorage.clear(); + clearStorage(); location.reload(); return false; - }, []); + }, [clearStorage]); useEffect(() => { i18n.changeLanguage(language); diff --git a/invokeai/frontend/web/src/app/components/InvokeAIUI.tsx b/invokeai/frontend/web/src/app/components/InvokeAIUI.tsx index 459ac65635..64d0d8d3ab 100644 --- a/invokeai/frontend/web/src/app/components/InvokeAIUI.tsx +++ b/invokeai/frontend/web/src/app/components/InvokeAIUI.tsx @@ -9,6 +9,9 @@ import { $projectId } from 'app/store/nanostores/projectId'; import { $queueId, DEFAULT_QUEUE_ID } from 'app/store/nanostores/queueId'; import { store } from 'app/store/store'; import { PartialAppConfig } from 'app/types/invokeai'; +import Loading from 'common/components/Loading/Loading'; +import AppDndContext from 'features/dnd/components/AppDndContext'; +import 'i18n'; import React, { PropsWithChildren, ReactNode, @@ -19,9 +22,6 @@ import React, { import { Provider } from 'react-redux'; import { addMiddleware, resetMiddlewares } from 'redux-dynamic-middlewares'; import { ManagerOptions, SocketOptions } from 'socket.io-client'; -import Loading from 'common/components/Loading/Loading'; -import AppDndContext from 'features/dnd/components/AppDndContext'; -import 'i18n'; const App = lazy(() => import('./App')); const ThemeLocaleProvider = lazy(() => import('./ThemeLocaleProvider')); diff --git a/invokeai/frontend/web/src/app/components/ThemeLocaleProvider.tsx b/invokeai/frontend/web/src/app/components/ThemeLocaleProvider.tsx index a9d56a7f16..ba0aaa5823 100644 --- a/invokeai/frontend/web/src/app/components/ThemeLocaleProvider.tsx +++ b/invokeai/frontend/web/src/app/components/ThemeLocaleProvider.tsx @@ -9,9 +9,9 @@ import { TOAST_OPTIONS, theme as invokeAITheme } from 'theme/theme'; import '@fontsource-variable/inter'; import { MantineProvider } from '@mantine/core'; +import { useMantineTheme } from 'mantine-theme/theme'; import 'overlayscrollbars/overlayscrollbars.css'; import 'theme/css/overlayscrollbars.css'; -import { useMantineTheme } from 'mantine-theme/theme'; type ThemeLocaleProviderProps = { children: ReactNode; diff --git a/invokeai/frontend/web/src/app/store/constants.ts b/invokeai/frontend/web/src/app/store/constants.ts index 6d48762bef..c2f3a5e10b 100644 --- a/invokeai/frontend/web/src/app/store/constants.ts +++ b/invokeai/frontend/web/src/app/store/constants.ts @@ -1,8 +1 @@ -export const LOCALSTORAGE_KEYS = [ - 'chakra-ui-color-mode', - 'i18nextLng', - 'ROARR_FILTER', - 'ROARR_LOG', -]; - -export const LOCALSTORAGE_PREFIX = '@@invokeai-'; +export const STORAGE_PREFIX = '@@invokeai-'; diff --git a/invokeai/frontend/web/src/app/store/store.ts b/invokeai/frontend/web/src/app/store/store.ts index d9bc7b085d..a0230c2807 100644 --- a/invokeai/frontend/web/src/app/store/store.ts +++ b/invokeai/frontend/web/src/app/store/store.ts @@ -23,9 +23,9 @@ import systemReducer from 'features/system/store/systemSlice'; import hotkeysReducer from 'features/ui/store/hotkeysSlice'; import uiReducer from 'features/ui/store/uiSlice'; import dynamicMiddlewares from 'redux-dynamic-middlewares'; -import { rememberEnhancer, rememberReducer } from 'redux-remember'; +import { Driver, rememberEnhancer, rememberReducer } from 'redux-remember'; import { api } from 'services/api'; -import { LOCALSTORAGE_PREFIX } from './constants'; +import { STORAGE_PREFIX } from './constants'; import { serialize } from './enhancers/reduxRemember/serialize'; import { unserialize } from './enhancers/reduxRemember/unserialize'; import { actionSanitizer } from './middleware/devtools/actionSanitizer'; @@ -33,6 +33,7 @@ import { actionsDenylist } from './middleware/devtools/actionsDenylist'; import { stateSanitizer } from './middleware/devtools/stateSanitizer'; import { listenerMiddleware } from './middleware/listenerMiddleware'; import { $store } from './nanostores/store'; +import { createStore as createIDBKeyValStore, get, set } from 'idb-keyval'; const allReducers = { canvas: canvasReducer, @@ -74,16 +75,25 @@ const rememberedKeys: (keyof typeof allReducers)[] = [ 'modelmanager', ]; +// Create a custom idb-keyval store (just needed to customize the name) +export const idbKeyValStore = createIDBKeyValStore('invoke', 'invoke-store'); + +// Create redux-remember driver, wrapping idb-keyval +const idbKeyValDriver: Driver = { + getItem: (key) => get(key, idbKeyValStore), + setItem: (key, value) => set(key, value, idbKeyValStore), +}; + export const store = configureStore({ reducer: rememberedRootReducer, enhancers: (existingEnhancers) => { return existingEnhancers .concat( - rememberEnhancer(window.localStorage, rememberedKeys, { + rememberEnhancer(idbKeyValDriver, rememberedKeys, { persistDebounce: 300, serialize, unserialize, - prefix: LOCALSTORAGE_PREFIX, + prefix: STORAGE_PREFIX, }) ) .concat(autoBatchEnhancer()); diff --git a/invokeai/frontend/web/src/common/hooks/useClearStorage.ts b/invokeai/frontend/web/src/common/hooks/useClearStorage.ts new file mode 100644 index 0000000000..0ab4936d72 --- /dev/null +++ b/invokeai/frontend/web/src/common/hooks/useClearStorage.ts @@ -0,0 +1,12 @@ +import { idbKeyValStore } from 'app/store/store'; +import { clear } from 'idb-keyval'; +import { useCallback } from 'react'; + +export const useClearStorage = () => { + const clearStorage = useCallback(() => { + clear(idbKeyValStore); + localStorage.clear(); + }, []); + + return clearStorage; +}; diff --git a/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsModal.tsx b/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsModal.tsx index e1eeb19df3..7841a94d3f 100644 --- a/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsModal.tsx +++ b/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsModal.tsx @@ -14,11 +14,11 @@ import { } from '@chakra-ui/react'; import { createSelector } from '@reduxjs/toolkit'; import { VALID_LOG_LEVELS } from 'app/logging/logger'; -import { LOCALSTORAGE_KEYS, LOCALSTORAGE_PREFIX } from 'app/store/constants'; import { stateSelector } from 'app/store/store'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import IAIButton from 'common/components/IAIButton'; import IAIMantineSelect from 'common/components/IAIMantineSelect'; +import { useClearStorage } from 'common/hooks/useClearStorage'; import { consoleLogLevelChanged, setEnableImageDebugging, @@ -164,20 +164,14 @@ const SettingsModal = ({ children, config }: SettingsModalProps) => { shouldEnableInformationalPopovers, } = useAppSelector(selector); + const clearStorage = useClearStorage(); + const handleClickResetWebUI = useCallback(() => { - // Only remove our keys - Object.keys(window.localStorage).forEach((key) => { - if ( - LOCALSTORAGE_KEYS.includes(key) || - key.startsWith(LOCALSTORAGE_PREFIX) - ) { - localStorage.removeItem(key); - } - }); + clearStorage(); onSettingsModalClose(); onRefreshModalOpen(); setInterval(() => setCountdown((prev) => prev - 1), 1000); - }, [onSettingsModalClose, onRefreshModalOpen]); + }, [clearStorage, onSettingsModalClose, onRefreshModalOpen]); useEffect(() => { if (countdown <= 0) { diff --git a/invokeai/frontend/web/yarn.lock b/invokeai/frontend/web/yarn.lock index e0a9db1c5e..6c661af24b 100644 --- a/invokeai/frontend/web/yarn.lock +++ b/invokeai/frontend/web/yarn.lock @@ -4158,6 +4158,11 @@ i18next@^23.6.0: dependencies: "@babel/runtime" "^7.22.5" +idb-keyval@^6.2.1: + version "6.2.1" + resolved "https://registry.yarnpkg.com/idb-keyval/-/idb-keyval-6.2.1.tgz#94516d625346d16f56f3b33855da11bfded2db33" + integrity sha512-8Sb3veuYCyrZL+VBt9LJfZjLUPWVvqn8tG28VqYNFCo43KHcKuq+b4EiXGeuaLAQWL2YmyDgMp2aSpH9JHsEQg== + ieee754@^1.1.13: version "1.2.1" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"