diff --git a/invokeai/frontend/web/.storybook/preview.tsx b/invokeai/frontend/web/.storybook/preview.tsx index af2d10e27a..2525bbe276 100644 --- a/invokeai/frontend/web/.storybook/preview.tsx +++ b/invokeai/frontend/web/.storybook/preview.tsx @@ -26,7 +26,7 @@ i18n.use(initReactI18next).init({ returnNull: false, }); -const store = createStore({ getItem: () => {}, setItem: () => {} }, false); +const store = createStore({ driver: { getItem: () => {}, setItem: () => {} }, persistThrottle: 2000 }); $store.set(store); $baseUrl.set('http://localhost:9090'); diff --git a/invokeai/frontend/web/src/app/components/InvokeAIUI.tsx b/invokeai/frontend/web/src/app/components/InvokeAIUI.tsx index b361b87bba..7e420190fc 100644 --- a/invokeai/frontend/web/src/app/components/InvokeAIUI.tsx +++ b/invokeai/frontend/web/src/app/components/InvokeAIUI.tsx @@ -6,7 +6,7 @@ import type { StudioInitAction } from 'app/hooks/useStudioInitAction'; import { $didStudioInit } from 'app/hooks/useStudioInitAction'; import type { LoggingOverrides } from 'app/logging/logger'; import { $loggingOverrides, configureLogging } from 'app/logging/logger'; -import { buildStorageApi, type StorageDriverApi } from 'app/store/enhancers/reduxRemember/driver'; +import { buildStorageApi } from 'app/store/enhancers/reduxRemember/driver'; import { $accountSettingsLink } from 'app/store/nanostores/accountSettingsLink'; import { $authToken } from 'app/store/nanostores/authToken'; import { $baseUrl } from 'app/store/nanostores/baseUrl'; @@ -72,7 +72,14 @@ interface Props extends PropsWithChildren { * If provided, overrides in-app navigation to the model manager */ onClickGoToModelManager?: () => void; - storageDriverApi?: StorageDriverApi; + storageConfig?: { + /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ + getItem: (key: string) => Promise; + /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ + setItem: (key: string, value: any) => Promise; + clear: () => Promise; + persistThrottle: number; + }; } const InvokeAIUI = ({ @@ -99,7 +106,7 @@ const InvokeAIUI = ({ loggingOverrides, onClickGoToModelManager, whatsNew, - storageDriverApi, + storageConfig, }: Props) => { useLayoutEffect(() => { /* @@ -312,7 +319,7 @@ const InvokeAIUI = ({ }; }, [isDebugging]); - const storage = useMemo(() => buildStorageApi(storageDriverApi), [storageDriverApi]); + const storage = useMemo(() => buildStorageApi(storageConfig), [storageConfig]); useEffect(() => { const storageCleanup = storage.registerListeners(); @@ -322,8 +329,11 @@ const InvokeAIUI = ({ }, [storage]); const store = useMemo(() => { - return createStore(storage.reduxRememberDriver); - }, [storage.reduxRememberDriver]); + return createStore({ + driver: storage.reduxRememberDriver, + persistThrottle: storageConfig?.persistThrottle ?? 2000, + }); + }, [storage.reduxRememberDriver, storageConfig?.persistThrottle]); useEffect(() => { $store.set(store); diff --git a/invokeai/frontend/web/src/app/store/enhancers/reduxRemember/driver.ts b/invokeai/frontend/web/src/app/store/enhancers/reduxRemember/driver.ts index 3f31a8d9e8..f13c0569ef 100644 --- a/invokeai/frontend/web/src/app/store/enhancers/reduxRemember/driver.ts +++ b/invokeai/frontend/web/src/app/store/enhancers/reduxRemember/driver.ts @@ -7,35 +7,6 @@ import type { Driver as ReduxRememberDriver } from 'redux-remember'; import { getBaseUrl } from 'services/api'; import { buildAppInfoUrl } from 'services/api/endpoints/appInfo'; -// Persistence happens per slice. To track when persistence is in progress, maintain a ref count, incrementing -// it when a slice is being persisted and decrementing it when the persistence is done. -let persistRefCount = 0; - -// Keep track of the last persisted state for each key to avoid unnecessary network requests. -// -// `redux-remember` persists individual slices of state, so we can implicity denylist a slice by not giving it a -// persist config. -// -// However, we may need to avoid persisting individual _fields_ of a slice. `redux-remember` does not provide a -// way to do this directly. -// -// To accomplish this, we add a layer of logic on top of the `redux-remember`. In the state serializer function -// provided to `redux-remember`, we can omit certain fields from the state that we do not want to persist. See -// the implementation in `store.ts` for this logic. -// -// This logic is unknown to `redux-remember`. When an omitted field changes, it will still attempt to persist the -// whole slice, even if the final, _serialized_ slice value is unchanged. -// -// To avoid unnecessary network requests, we keep track of the last persisted state for each key. If the value to -// be persisted is the same as the last persisted value, we can skip the network request. -const lastPersistedState = new Map(); - -export type StorageDriverApi = { - getItem: (key: string) => Promise; - setItem: (key: string, value: any) => Promise; - clear: () => Promise; -}; - const log = logger('system'); const buildOSSServerBackedDriver = (): { @@ -43,6 +14,29 @@ const buildOSSServerBackedDriver = (): { clearStorage: () => Promise; registerListeners: () => () => void; } => { + // Persistence happens per slice. To track when persistence is in progress, maintain a ref count, incrementing + // it when a slice is being persisted and decrementing it when the persistence is done. + let persistRefCount = 0; + + // Keep track of the last persisted state for each key to avoid unnecessary network requests. + // + // `redux-remember` persists individual slices of state, so we can implicity denylist a slice by not giving it a + // persist config. + // + // However, we may need to avoid persisting individual _fields_ of a slice. `redux-remember` does not provide a + // way to do this directly. + // + // To accomplish this, we add a layer of logic on top of the `redux-remember`. In the state serializer function + // provided to `redux-remember`, we can omit certain fields from the state that we do not want to persist. See + // the implementation in `store.ts` for this logic. + // + // This logic is unknown to `redux-remember`. When an omitted field changes, it will still attempt to persist the + // whole slice, even if the final, _serialized_ slice value is unchanged. + // + // To avoid unnecessary network requests, we keep track of the last persisted state for each key. If the value to + // be persisted is the same as the last persisted value, we can skip the network request. + const lastPersistedState = new Map(); + const getUrl = (key?: string) => { const baseUrl = getBaseUrl(); const query: Record = {}; @@ -80,10 +74,15 @@ const buildOSSServerBackedDriver = (): { return value; } const url = getUrl(key); - const res = await fetch(url, { method: 'POST', body: value }); + const headers = new Headers({ + 'Content-Type': 'application/json', + }); + const res = await fetch(url, { method: 'POST', headers, body: value }); if (!res.ok) { throw new Error(`Response status: ${res.status}`); } + lastPersistedState.set(key, value); + return value; } catch (originalError) { throw new StorageError({ key, @@ -137,13 +136,21 @@ const buildOSSServerBackedDriver = (): { return { reduxRememberDriver, clearStorage, registerListeners }; }; -const buildCustomDriver = ( - api: StorageDriverApi -): { +const buildCustomDriver = (api: { + getItem: (key: string) => Promise; + setItem: (key: string, value: any) => Promise; + clear: () => Promise; +}): { reduxRememberDriver: ReduxRememberDriver; clearStorage: () => Promise; registerListeners: () => () => void; } => { + // See the comment in `buildOSSServerBackedDriver` for an explanation of this variable. + let persistRefCount = 0; + + // See the comment in `buildOSSServerBackedDriver` for an explanation of this variable. + const lastPersistedState = new Map(); + const reduxRememberDriver: ReduxRememberDriver = { getItem: async (key) => { try { @@ -219,9 +226,13 @@ const buildCustomDriver = ( return { reduxRememberDriver, clearStorage, registerListeners }; }; -export const buildStorageApi = (driverApi?: StorageDriverApi) => { - if (driverApi) { - return buildCustomDriver(driverApi); +export const buildStorageApi = (api?: { + getItem: (key: string) => Promise; + setItem: (key: string, value: any) => Promise; + clear: () => Promise; +}) => { + if (api) { + return buildCustomDriver(api); } else { return buildOSSServerBackedDriver(); } diff --git a/invokeai/frontend/web/src/app/store/store.ts b/invokeai/frontend/web/src/app/store/store.ts index da610cdec2..96781cc934 100644 --- a/invokeai/frontend/web/src/app/store/store.ts +++ b/invokeai/frontend/web/src/app/store/store.ts @@ -184,7 +184,7 @@ const PERSISTED_KEYS = Object.values(SLICE_CONFIGS) .filter((sliceConfig) => !!sliceConfig.persistConfig) .map((sliceConfig) => sliceConfig.slice.reducerPath); -export const createStore = (driver: Driver, persist = true) => +export const createStore = (reduxRememberOptions: { driver: Driver; persistThrottle: number }) => configureStore({ reducer: rememberedRootReducer, middleware: (getDefaultMiddleware) => @@ -201,20 +201,15 @@ export const createStore = (driver: Driver, persist = true) => .prepend(listenerMiddleware.middleware), enhancers: (getDefaultEnhancers) => { const enhancers = getDefaultEnhancers(); - if (persist) { - const res = enhancers.prepend( - rememberEnhancer(driver, PERSISTED_KEYS, { - persistThrottle: 2000, - serialize, - unserialize, - prefix: '', - errorHandler, - }) - ); - return res; - } else { - return enhancers; - } + return enhancers.prepend( + rememberEnhancer(reduxRememberOptions.driver, PERSISTED_KEYS, { + persistThrottle: reduxRememberOptions.persistThrottle, + serialize, + unserialize, + prefix: '', + errorHandler, + }) + ); }, devTools: { actionSanitizer, diff --git a/invokeai/frontend/web/src/index.ts b/invokeai/frontend/web/src/index.ts index c69ef7ea92..fd6ab14f9b 100644 --- a/invokeai/frontend/web/src/index.ts +++ b/invokeai/frontend/web/src/index.ts @@ -31,7 +31,6 @@ import { export { default as InvokeAIUI } from './app/components/InvokeAIUI'; export type { StudioInitAction } from './app/hooks/useStudioInitAction'; export type { LoggingOverrides } from './app/logging/logger'; -export type { StorageDriverApi } from './app/store/enhancers/reduxRemember/driver'; export type { PartialAppConfig } from './app/types/invokeai'; export { default as HotkeysModal } from './features/system/components/HotkeysModal/HotkeysModal'; export { default as InvokeAiLogoComponent } from './features/system/components/InvokeAILogoComponent';