feat(ui): iterate on storage api

This commit is contained in:
psychedelicious
2025-07-24 14:55:43 +10:00
parent e7c67da2c2
commit 7e59d040aa
5 changed files with 74 additions and 59 deletions

View File

@@ -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');

View File

@@ -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<any>;
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
setItem: (key: string, value: any) => Promise<any>;
clear: () => Promise<void>;
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);

View File

@@ -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<string, unknown>();
export type StorageDriverApi = {
getItem: (key: string) => Promise<any>;
setItem: (key: string, value: any) => Promise<any>;
clear: () => Promise<void>;
};
const log = logger('system');
const buildOSSServerBackedDriver = (): {
@@ -43,6 +14,29 @@ const buildOSSServerBackedDriver = (): {
clearStorage: () => Promise<void>;
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<string, unknown>();
const getUrl = (key?: string) => {
const baseUrl = getBaseUrl();
const query: Record<string, string> = {};
@@ -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<any>;
setItem: (key: string, value: any) => Promise<any>;
clear: () => Promise<void>;
}): {
reduxRememberDriver: ReduxRememberDriver;
clearStorage: () => Promise<void>;
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<string, unknown>();
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<any>;
setItem: (key: string, value: any) => Promise<any>;
clear: () => Promise<void>;
}) => {
if (api) {
return buildCustomDriver(api);
} else {
return buildOSSServerBackedDriver();
}

View File

@@ -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,

View File

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