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.
This commit is contained in:
psychedelicious 2023-11-13 18:08:17 +11:00
parent 984e609c61
commit aadcde3edd
9 changed files with 46 additions and 29 deletions

View File

@ -75,6 +75,7 @@
"framer-motion": "^10.16.4", "framer-motion": "^10.16.4",
"i18next": "^23.6.0", "i18next": "^23.6.0",
"i18next-http-backend": "^2.3.1", "i18next-http-backend": "^2.3.1",
"idb-keyval": "^6.2.1",
"konva": "^9.2.3", "konva": "^9.2.3",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"nanostores": "^0.9.4", "nanostores": "^0.9.4",

View File

@ -21,6 +21,7 @@ import GlobalHotkeys from './GlobalHotkeys';
import PreselectedImage from './PreselectedImage'; import PreselectedImage from './PreselectedImage';
import Toaster from './Toaster'; import Toaster from './Toaster';
import { useSocketIO } from 'app/hooks/useSocketIO'; import { useSocketIO } from 'app/hooks/useSocketIO';
import { useClearStorage } from 'common/hooks/useClearStorage';
const DEFAULT_CONFIG = {}; const DEFAULT_CONFIG = {};
@ -36,15 +37,16 @@ const App = ({ config = DEFAULT_CONFIG, selectedImage }: Props) => {
const language = useAppSelector(languageSelector); const language = useAppSelector(languageSelector);
const logger = useLogger('system'); const logger = useLogger('system');
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const clearStorage = useClearStorage();
// singleton! // singleton!
useSocketIO(); useSocketIO();
const handleReset = useCallback(() => { const handleReset = useCallback(() => {
localStorage.clear(); clearStorage();
location.reload(); location.reload();
return false; return false;
}, []); }, [clearStorage]);
useEffect(() => { useEffect(() => {
i18n.changeLanguage(language); i18n.changeLanguage(language);

View File

@ -9,6 +9,9 @@ import { $projectId } from 'app/store/nanostores/projectId';
import { $queueId, DEFAULT_QUEUE_ID } from 'app/store/nanostores/queueId'; import { $queueId, DEFAULT_QUEUE_ID } from 'app/store/nanostores/queueId';
import { store } from 'app/store/store'; import { store } from 'app/store/store';
import { PartialAppConfig } from 'app/types/invokeai'; import { PartialAppConfig } from 'app/types/invokeai';
import Loading from 'common/components/Loading/Loading';
import AppDndContext from 'features/dnd/components/AppDndContext';
import 'i18n';
import React, { import React, {
PropsWithChildren, PropsWithChildren,
ReactNode, ReactNode,
@ -19,9 +22,6 @@ import React, {
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import { addMiddleware, resetMiddlewares } from 'redux-dynamic-middlewares'; import { addMiddleware, resetMiddlewares } from 'redux-dynamic-middlewares';
import { ManagerOptions, SocketOptions } from 'socket.io-client'; 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 App = lazy(() => import('./App'));
const ThemeLocaleProvider = lazy(() => import('./ThemeLocaleProvider')); const ThemeLocaleProvider = lazy(() => import('./ThemeLocaleProvider'));

View File

@ -9,9 +9,9 @@ import { TOAST_OPTIONS, theme as invokeAITheme } from 'theme/theme';
import '@fontsource-variable/inter'; import '@fontsource-variable/inter';
import { MantineProvider } from '@mantine/core'; import { MantineProvider } from '@mantine/core';
import { useMantineTheme } from 'mantine-theme/theme';
import 'overlayscrollbars/overlayscrollbars.css'; import 'overlayscrollbars/overlayscrollbars.css';
import 'theme/css/overlayscrollbars.css'; import 'theme/css/overlayscrollbars.css';
import { useMantineTheme } from 'mantine-theme/theme';
type ThemeLocaleProviderProps = { type ThemeLocaleProviderProps = {
children: ReactNode; children: ReactNode;

View File

@ -1,8 +1 @@
export const LOCALSTORAGE_KEYS = [ export const STORAGE_PREFIX = '@@invokeai-';
'chakra-ui-color-mode',
'i18nextLng',
'ROARR_FILTER',
'ROARR_LOG',
];
export const LOCALSTORAGE_PREFIX = '@@invokeai-';

View File

@ -23,9 +23,9 @@ import systemReducer from 'features/system/store/systemSlice';
import hotkeysReducer from 'features/ui/store/hotkeysSlice'; import hotkeysReducer from 'features/ui/store/hotkeysSlice';
import uiReducer from 'features/ui/store/uiSlice'; import uiReducer from 'features/ui/store/uiSlice';
import dynamicMiddlewares from 'redux-dynamic-middlewares'; 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 { api } from 'services/api';
import { LOCALSTORAGE_PREFIX } from './constants'; import { STORAGE_PREFIX } from './constants';
import { serialize } from './enhancers/reduxRemember/serialize'; import { serialize } from './enhancers/reduxRemember/serialize';
import { unserialize } from './enhancers/reduxRemember/unserialize'; import { unserialize } from './enhancers/reduxRemember/unserialize';
import { actionSanitizer } from './middleware/devtools/actionSanitizer'; import { actionSanitizer } from './middleware/devtools/actionSanitizer';
@ -33,6 +33,7 @@ import { actionsDenylist } from './middleware/devtools/actionsDenylist';
import { stateSanitizer } from './middleware/devtools/stateSanitizer'; import { stateSanitizer } from './middleware/devtools/stateSanitizer';
import { listenerMiddleware } from './middleware/listenerMiddleware'; import { listenerMiddleware } from './middleware/listenerMiddleware';
import { $store } from './nanostores/store'; import { $store } from './nanostores/store';
import { createStore as createIDBKeyValStore, get, set } from 'idb-keyval';
const allReducers = { const allReducers = {
canvas: canvasReducer, canvas: canvasReducer,
@ -74,16 +75,25 @@ const rememberedKeys: (keyof typeof allReducers)[] = [
'modelmanager', '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({ export const store = configureStore({
reducer: rememberedRootReducer, reducer: rememberedRootReducer,
enhancers: (existingEnhancers) => { enhancers: (existingEnhancers) => {
return existingEnhancers return existingEnhancers
.concat( .concat(
rememberEnhancer(window.localStorage, rememberedKeys, { rememberEnhancer(idbKeyValDriver, rememberedKeys, {
persistDebounce: 300, persistDebounce: 300,
serialize, serialize,
unserialize, unserialize,
prefix: LOCALSTORAGE_PREFIX, prefix: STORAGE_PREFIX,
}) })
) )
.concat(autoBatchEnhancer()); .concat(autoBatchEnhancer());

View File

@ -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;
};

View File

@ -14,11 +14,11 @@ import {
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit'; import { createSelector } from '@reduxjs/toolkit';
import { VALID_LOG_LEVELS } from 'app/logging/logger'; import { VALID_LOG_LEVELS } from 'app/logging/logger';
import { LOCALSTORAGE_KEYS, LOCALSTORAGE_PREFIX } from 'app/store/constants';
import { stateSelector } from 'app/store/store'; import { stateSelector } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAIButton from 'common/components/IAIButton'; import IAIButton from 'common/components/IAIButton';
import IAIMantineSelect from 'common/components/IAIMantineSelect'; import IAIMantineSelect from 'common/components/IAIMantineSelect';
import { useClearStorage } from 'common/hooks/useClearStorage';
import { import {
consoleLogLevelChanged, consoleLogLevelChanged,
setEnableImageDebugging, setEnableImageDebugging,
@ -164,20 +164,14 @@ const SettingsModal = ({ children, config }: SettingsModalProps) => {
shouldEnableInformationalPopovers, shouldEnableInformationalPopovers,
} = useAppSelector(selector); } = useAppSelector(selector);
const clearStorage = useClearStorage();
const handleClickResetWebUI = useCallback(() => { const handleClickResetWebUI = useCallback(() => {
// Only remove our keys clearStorage();
Object.keys(window.localStorage).forEach((key) => {
if (
LOCALSTORAGE_KEYS.includes(key) ||
key.startsWith(LOCALSTORAGE_PREFIX)
) {
localStorage.removeItem(key);
}
});
onSettingsModalClose(); onSettingsModalClose();
onRefreshModalOpen(); onRefreshModalOpen();
setInterval(() => setCountdown((prev) => prev - 1), 1000); setInterval(() => setCountdown((prev) => prev - 1), 1000);
}, [onSettingsModalClose, onRefreshModalOpen]); }, [clearStorage, onSettingsModalClose, onRefreshModalOpen]);
useEffect(() => { useEffect(() => {
if (countdown <= 0) { if (countdown <= 0) {

View File

@ -4158,6 +4158,11 @@ i18next@^23.6.0:
dependencies: dependencies:
"@babel/runtime" "^7.22.5" "@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: ieee754@^1.1.13:
version "1.2.1" version "1.2.1"
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"