This commit is contained in:
psychedelicious
2025-07-23 21:19:15 +10:00
parent 61e711620d
commit 9492569a2c
7 changed files with 134 additions and 74 deletions

View File

@@ -123,6 +123,8 @@ export default [
},
],
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/consistent-type-imports': [
'error',
{

View File

@@ -5,6 +5,12 @@ 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 {
$resetClientState,
buildDriver,
buildResetClientState,
type StorageDriverApi,
} 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';
@@ -70,6 +76,7 @@ interface Props extends PropsWithChildren {
* If provided, overrides in-app navigation to the model manager
*/
onClickGoToModelManager?: () => void;
storageDriverApi?: StorageDriverApi;
}
const InvokeAIUI = ({
@@ -96,6 +103,7 @@ const InvokeAIUI = ({
loggingOverrides,
onClickGoToModelManager,
whatsNew,
storageDriverApi,
}: Props) => {
useLayoutEffect(() => {
/*
@@ -308,9 +316,18 @@ const InvokeAIUI = ({
};
}, [isDebugging]);
useEffect(() => {
$resetClientState.set(buildResetClientState(storageDriverApi));
return () => {
$resetClientState.set(() => {});
};
}, [storageDriverApi]);
const store = useMemo(() => {
return createStore(projectId);
}, [projectId]);
const driver = buildDriver(storageDriverApi);
return createStore(driver);
}, [storageDriverApi]);
useEffect(() => {
$store.set(store);

View File

@@ -3,10 +3,17 @@ import { StorageError } from 'app/store/enhancers/reduxRemember/errors';
import { $authToken } from 'app/store/nanostores/authToken';
import { $projectId } from 'app/store/nanostores/projectId';
import { $queueId } from 'app/store/nanostores/queueId';
import { atom } from 'nanostores';
import type { Driver } from 'redux-remember';
import { getBaseUrl } from 'services/api';
import { buildAppInfoUrl } from 'services/api/endpoints/appInfo';
export type StorageDriverApi = {
getItem: (key: string) => Promise<any>;
setItem: (key: string, value: any) => Promise<any>;
clear: () => Promise<void>;
};
const log = logger('system');
// Persistence happens per slice. To track when persistence is in progress, maintain a ref count, incrementing
@@ -47,77 +54,94 @@ const getHeaders = (extra?: Record<string, string>) => {
return headers;
};
export const serverBackedDriver: Driver = {
getItem: async (key) => {
try {
const url = getUrl(key);
const headers = getHeaders();
const res = await fetch(url, { headers, method: 'GET' });
if (!res.ok) {
throw new Error(`Response status: ${res.status}`);
export const buildDriver = (api?: StorageDriverApi): Driver => {
return {
getItem: async (key) => {
try {
if (api) {
log.trace(`Using provided API to get item for key "${key}"`);
return await api.getItem(key);
}
const url = getUrl(key);
const headers = getHeaders();
const res = await fetch(url, { headers, method: 'GET' });
if (!res.ok) {
throw new Error(`Response status: ${res.status}`);
}
const json = await res.json();
return json;
} catch (originalError) {
throw new StorageError({
key,
projectId: $projectId.get(),
originalError,
});
}
const json = await res.json();
return json;
} catch (originalError) {
throw new StorageError({
key,
projectId: $projectId.get(),
originalError,
});
}
},
setItem: async (key, value) => {
try {
persistRefCount++;
// Deep equality check to avoid noop persist 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 skip the network request.
if (lastPersistedState.get(key) === value) {
log.trace(`Skipping persist for key "${key}" as value is unchanged.`);
},
setItem: async (key, value) => {
try {
persistRefCount++;
if (api) {
log.trace(`Using provided API to get item for key "${key}"`);
return await api.setItem(key, value);
}
// Deep equality check to avoid noop persist 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 skip the network request.
if (lastPersistedState.get(key) === value) {
log.trace(`Skipping persist for key "${key}" as value is unchanged.`);
return value;
}
const url = getUrl(key);
const headers = getHeaders({ 'content-type': 'application/json' });
const res = await fetch(url, { headers, method: 'POST', body: JSON.stringify(value) });
if (!res.ok) {
throw new Error(`Response status: ${res.status}`);
}
lastPersistedState.set(key, value);
return value;
} catch (originalError) {
throw new StorageError({
key,
value,
projectId: $projectId.get(),
originalError,
});
} finally {
persistRefCount--;
if (persistRefCount < 0) {
log.warn('Persist ref count is negative, resetting to 0');
persistRefCount = 0;
}
}
const url = getUrl(key);
const headers = getHeaders({ 'content-type': 'application/json' });
const res = await fetch(url, { headers, method: 'POST', body: JSON.stringify(value) });
if (!res.ok) {
throw new Error(`Response status: ${res.status}`);
}
lastPersistedState.set(key, value);
return value;
} catch (originalError) {
throw new StorageError({
key,
value,
projectId: $projectId.get(),
originalError,
});
} finally {
persistRefCount--;
if (persistRefCount < 0) {
log.warn('Persist ref count is negative, resetting to 0');
persistRefCount = 0;
}
}
},
},
};
};
export const resetClientState = async () => {
export const $resetClientState = atom(() => {});
export const buildResetClientState = (api?: StorageDriverApi) => async () => {
try {
persistRefCount++;
if (api) {
log.trace('Using provided API to reset client state');
await api.clear();
return;
}
const url = getUrl();
const headers = getHeaders();
const res = await fetch(url, { headers, method: 'DELETE' });

View File

@@ -1,7 +1,6 @@
import type { ThunkDispatch, TypedStartListening, UnknownAction } from '@reduxjs/toolkit';
import { addListener, combineReducers, configureStore, createListenerMiddleware } from '@reduxjs/toolkit';
import { logger } from 'app/logging/logger';
import { serverBackedDriver } from 'app/store/enhancers/reduxRemember/driver';
import { errorHandler } from 'app/store/enhancers/reduxRemember/errors';
import { addAdHocPostProcessingRequestedListener } from 'app/store/middleware/listenerMiddleware/listeners/addAdHocPostProcessingRequestedListener';
import { addAnyEnqueuedListener } from 'app/store/middleware/listenerMiddleware/listeners/anyEnqueued';
@@ -41,7 +40,7 @@ import { systemSliceConfig } from 'features/system/store/systemSlice';
import { uiSliceConfig } from 'features/ui/store/uiSlice';
import { diff } from 'jsondiffpatch';
import dynamicMiddlewares from 'redux-dynamic-middlewares';
import type { SerializeFunction, UnserializeFunction } from 'redux-remember';
import type { Driver, SerializeFunction, UnserializeFunction } from 'redux-remember';
import { rememberEnhancer, rememberReducer } from 'redux-remember';
import undoable, { newHistory } from 'redux-undo';
import { serializeError } from 'serialize-error';
@@ -185,7 +184,7 @@ const PERSISTED_KEYS = Object.values(SLICE_CONFIGS)
.filter((sliceConfig) => !!sliceConfig.persistConfig)
.map((sliceConfig) => sliceConfig.slice.reducerPath);
export const createStore = (uniqueStoreKey?: string, persist = true) =>
export const createStore = (driver: Driver, persist = true) =>
configureStore({
reducer: rememberedRootReducer,
middleware: (getDefaultMiddleware) =>
@@ -204,7 +203,7 @@ export const createStore = (uniqueStoreKey?: string, persist = true) =>
const enhancers = getDefaultEnhancers();
if (persist) {
const res = enhancers.prepend(
rememberEnhancer(serverBackedDriver, PERSISTED_KEYS, {
rememberEnhancer(driver, PERSISTED_KEYS, {
persistThrottle: 2000,
serialize,
unserialize,

View File

@@ -1,10 +1,9 @@
import { resetClientState } from 'app/store/enhancers/reduxRemember/driver';
import { $resetClientState } from 'app/store/enhancers/reduxRemember/driver';
import { useCallback } from 'react';
export const useClearStorage = () => {
const clearStorage = useCallback(() => {
// clearIdbKeyValStore();
resetClientState();
$resetClientState.get()();
localStorage.clear();
}, []);

View File

@@ -31,6 +31,7 @@ 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';

View File

@@ -1,5 +1,23 @@
import type { StorageDriverApi } from 'app/store/enhancers/reduxRemember/driver';
import ReactDOM from 'react-dom/client';
import InvokeAIUI from './app/components/InvokeAIUI';
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(<InvokeAIUI />);
let state: Record<string, any> = {};
const storageDriverApi: StorageDriverApi = {
getItem: (key: string) => {
return Promise.resolve(state[key]);
},
setItem: (key: string, value: any) => {
state[key] = value;
return Promise.resolve(value);
},
clear: () => {
state = {};
return Promise.resolve();
},
};
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<InvokeAIUI storageDriverApi={storageDriverApi} />
);