refactor(ui): refactor persist config

Add more structure around persist configs to avoid bugs from typos and misplaced persist denylists.
This commit is contained in:
psychedelicious 2024-02-03 20:18:13 +11:00 committed by Kent Keirsey
parent 0976ddba23
commit c1300fa8b1
24 changed files with 175 additions and 189 deletions

View File

@ -3,49 +3,27 @@ import { autoBatchEnhancer, combineReducers, configureStore } from '@reduxjs/too
import { logger } from 'app/logging/logger';
import { idbKeyValDriver } from 'app/store/enhancers/reduxRemember/driver';
import { errorHandler } from 'app/store/enhancers/reduxRemember/errors';
import { canvasPersistDenylist } from 'features/canvas/store/canvasPersistDenylist';
import canvasReducer, { initialCanvasState, migrateCanvasState } from 'features/canvas/store/canvasSlice';
import canvasReducer, { canvasPersistConfig } from 'features/canvas/store/canvasSlice';
import changeBoardModalReducer from 'features/changeBoardModal/store/slice';
import { controlAdaptersPersistDenylist } from 'features/controlAdapters/store/controlAdaptersPersistDenylist';
import controlAdaptersReducer, {
initialControlAdaptersState,
migrateControlAdaptersState,
controlAdaptersPersistConfig,
} from 'features/controlAdapters/store/controlAdaptersSlice';
import deleteImageModalReducer from 'features/deleteImageModal/store/slice';
import { dynamicPromptsPersistDenylist } from 'features/dynamicPrompts/store/dynamicPromptsPersistDenylist';
import dynamicPromptsReducer, {
initialDynamicPromptsState,
migrateDynamicPromptsState,
} from 'features/dynamicPrompts/store/dynamicPromptsSlice';
import { galleryPersistDenylist } from 'features/gallery/store/galleryPersistDenylist';
import galleryReducer, { initialGalleryState, migrateGalleryState } from 'features/gallery/store/gallerySlice';
import hrfReducer, { initialHRFState, migrateHRFState } from 'features/hrf/store/hrfSlice';
import loraReducer, { initialLoraState, migrateLoRAState } from 'features/lora/store/loraSlice';
import modelmanagerReducer, {
initialModelManagerState,
migrateModelManagerState,
} from 'features/modelManager/store/modelManagerSlice';
import { nodesPersistDenylist } from 'features/nodes/store/nodesPersistDenylist';
import nodesReducer, { initialNodesState, migrateNodesState } from 'features/nodes/store/nodesSlice';
import dynamicPromptsReducer, { dynamicPromptsPersistConfig } from 'features/dynamicPrompts/store/dynamicPromptsSlice';
import galleryReducer, { galleryPersistConfig } from 'features/gallery/store/gallerySlice';
import hrfReducer, { hrfPersistConfig } from 'features/hrf/store/hrfSlice';
import loraReducer, { loraPersistConfig } from 'features/lora/store/loraSlice';
import modelmanagerReducer, { modelManagerPersistConfig } from 'features/modelManager/store/modelManagerSlice';
import nodesReducer, { nodesPersistConfig } from 'features/nodes/store/nodesSlice';
import nodeTemplatesReducer from 'features/nodes/store/nodeTemplatesSlice';
import workflowReducer, { initialWorkflowState, migrateWorkflowState } from 'features/nodes/store/workflowSlice';
import { generationPersistDenylist } from 'features/parameters/store/generationPersistDenylist';
import generationReducer, {
initialGenerationState,
migrateGenerationState,
} from 'features/parameters/store/generationSlice';
import { postprocessingPersistDenylist } from 'features/parameters/store/postprocessingPersistDenylist';
import postprocessingReducer, {
initialPostprocessingState,
migratePostprocessingState,
} from 'features/parameters/store/postprocessingSlice';
import workflowReducer, { workflowPersistConfig } from 'features/nodes/store/workflowSlice';
import generationReducer, { generationPersistConfig } from 'features/parameters/store/generationSlice';
import postprocessingReducer, { postprocessingPersistConfig } from 'features/parameters/store/postprocessingSlice';
import queueReducer from 'features/queue/store/queueSlice';
import sdxlReducer, { initialSDXLState, migrateSDXLState } from 'features/sdxl/store/sdxlSlice';
import sdxlReducer, { sdxlPersistConfig } from 'features/sdxl/store/sdxlSlice';
import configReducer from 'features/system/store/configSlice';
import { systemPersistDenylist } from 'features/system/store/systemPersistDenylist';
import systemReducer, { initialSystemState, migrateSystemState } from 'features/system/store/systemSlice';
import { uiPersistDenylist } from 'features/ui/store/uiPersistDenylist';
import uiReducer, { initialUIState, migrateUIState } from 'features/ui/store/uiSlice';
import systemReducer, { systemPersistConfig } from 'features/system/store/systemSlice';
import uiReducer, { uiPersistConfig } from 'features/ui/store/uiSlice';
import { diff } from 'jsondiffpatch';
import { defaultsDeep, keys, omit, pick } from 'lodash-es';
import dynamicMiddlewares from 'redux-dynamic-middlewares';
@ -88,70 +66,48 @@ const rootReducer = combineReducers(allReducers);
const rememberedRootReducer = rememberReducer(rootReducer);
const rememberedKeys = [
'canvas',
'gallery',
'generation',
'sdxl',
'nodes',
'workflow',
'postprocessing',
'system',
'ui',
'controlAdapters',
'dynamicPrompts',
'lora',
'modelmanager',
'hrf',
] satisfies (keyof typeof allReducers)[];
type SliceConfig = {
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
initialState: any;
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
migrate: (state: any) => any;
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
export type PersistConfig<T = any> = {
/**
* The name of the slice.
*/
name: keyof typeof allReducers;
/**
* The initial state of the slice.
*/
initialState: T;
/**
* Migrate the state to the current version during rehydration.
* @param state The rehydrated state.
* @returns A correctly-shaped state.
*/
migrate: (state: unknown) => T;
/**
* Keys to omit from the persisted state.
*/
persistDenylist: (keyof T)[];
};
const sliceConfigs: {
[key in (typeof rememberedKeys)[number]]: SliceConfig;
} = {
canvas: { initialState: initialCanvasState, migrate: migrateCanvasState },
gallery: { initialState: initialGalleryState, migrate: migrateGalleryState },
generation: {
initialState: initialGenerationState,
migrate: migrateGenerationState,
},
nodes: { initialState: initialNodesState, migrate: migrateNodesState },
postprocessing: {
initialState: initialPostprocessingState,
migrate: migratePostprocessingState,
},
system: { initialState: initialSystemState, migrate: migrateSystemState },
workflow: {
initialState: initialWorkflowState,
migrate: migrateWorkflowState,
},
ui: { initialState: initialUIState, migrate: migrateUIState },
controlAdapters: {
initialState: initialControlAdaptersState,
migrate: migrateControlAdaptersState,
},
dynamicPrompts: {
initialState: initialDynamicPromptsState,
migrate: migrateDynamicPromptsState,
},
sdxl: { initialState: initialSDXLState, migrate: migrateSDXLState },
lora: { initialState: initialLoraState, migrate: migrateLoRAState },
modelmanager: {
initialState: initialModelManagerState,
migrate: migrateModelManagerState,
},
hrf: { initialState: initialHRFState, migrate: migrateHRFState },
const persistConfigs: { [key in keyof typeof allReducers]?: PersistConfig } = {
[canvasPersistConfig.name]: canvasPersistConfig,
[galleryPersistConfig.name]: galleryPersistConfig,
[generationPersistConfig.name]: generationPersistConfig,
[nodesPersistConfig.name]: nodesPersistConfig,
[postprocessingPersistConfig.name]: postprocessingPersistConfig,
[systemPersistConfig.name]: systemPersistConfig,
[workflowPersistConfig.name]: workflowPersistConfig,
[uiPersistConfig.name]: uiPersistConfig,
[controlAdaptersPersistConfig.name]: controlAdaptersPersistConfig,
[dynamicPromptsPersistConfig.name]: dynamicPromptsPersistConfig,
[sdxlPersistConfig.name]: sdxlPersistConfig,
[loraPersistConfig.name]: loraPersistConfig,
[modelManagerPersistConfig.name]: modelManagerPersistConfig,
[hrfPersistConfig.name]: hrfPersistConfig,
};
const unserialize: UnserializeFunction = (data, key) => {
const log = logger('system');
const config = sliceConfigs[key as keyof typeof sliceConfigs];
const config = persistConfigs[key as keyof typeof persistConfigs];
if (!config) {
throw new Error(`No unserialize config for slice "${key}"`);
}
@ -180,22 +136,8 @@ const unserialize: UnserializeFunction = (data, key) => {
}
};
const serializationDenylist: {
[key in (typeof rememberedKeys)[number]]?: string[];
} = {
canvas: canvasPersistDenylist,
gallery: galleryPersistDenylist,
generation: generationPersistDenylist,
nodes: nodesPersistDenylist,
postprocessing: postprocessingPersistDenylist,
system: systemPersistDenylist,
ui: uiPersistDenylist,
controlAdapters: controlAdaptersPersistDenylist,
dynamicPrompts: dynamicPromptsPersistDenylist,
};
export const serialize: SerializeFunction = (data, key) => {
const result = omit(data, serializationDenylist[key as keyof typeof serializationDenylist] ?? []);
const result = omit(data, persistConfigs[key as keyof typeof persistConfigs]?.persistDenylist ?? []);
return JSON.stringify(result);
};
@ -215,7 +157,7 @@ export const createStore = (uniqueStoreKey?: string, persist = true) =>
const _enhancers = getDefaultEnhancers().concat(autoBatchEnhancer());
if (persist) {
_enhancers.push(
rememberEnhancer(idbKeyValDriver, rememberedKeys, {
rememberEnhancer(idbKeyValDriver, keys(persistConfigs), {
persistDebounce: 300,
serialize,
unserialize,

View File

@ -1,6 +0,0 @@
import type { CanvasState } from './canvasTypes';
/**
* Canvas slice persist denylist
*/
export const canvasPersistDenylist: (keyof CanvasState)[] = [];

View File

@ -1,6 +1,6 @@
import type { PayloadAction } from '@reduxjs/toolkit';
import { createSlice } from '@reduxjs/toolkit';
import type { RootState } from 'app/store/store';
import type { PersistConfig, RootState } from 'app/store/store';
import { roundDownToMultiple, roundToMultiple } from 'common/util/roundDownToMultiple';
import calculateCoordinates from 'features/canvas/util/calculateCoordinates';
import calculateScale from 'features/canvas/util/calculateScale';
@ -731,3 +731,10 @@ export const migrateCanvasState = (state: any): any => {
}
return state;
};
export const canvasPersistConfig: PersistConfig<CanvasState> = {
name: canvasSlice.name,
initialState: initialCanvasState,
migrate: migrateCanvasState,
persistDenylist: [],
};

View File

@ -1,6 +0,0 @@
import type { ControlAdaptersState } from './types';
/**
* ControlNet slice persist denylist
*/
export const controlAdaptersPersistDenylist: (keyof ControlAdaptersState)[] = ['pendingControlImages'];

View File

@ -1,7 +1,7 @@
import type { PayloadAction, Update } from '@reduxjs/toolkit';
import { createEntityAdapter, createSlice, isAnyOf } from '@reduxjs/toolkit';
import { getSelectorsOptions } from 'app/store/createMemoizedSelector';
import type { RootState } from 'app/store/store';
import type { PersistConfig, RootState } from 'app/store/store';
import { buildControlAdapter } from 'features/controlAdapters/util/buildControlAdapter';
import type {
ParameterControlNetModel,
@ -441,3 +441,10 @@ export const migrateControlAdaptersState = (state: any): any => {
}
return state;
};
export const controlAdaptersPersistConfig: PersistConfig<ControlAdaptersState> = {
name: controlAdaptersSlice.name,
initialState: initialControlAdaptersState,
migrate: migrateControlAdaptersState,
persistDenylist: ['pendingControlImages'],
};

View File

@ -1,3 +0,0 @@
import type { initialDynamicPromptsState } from './dynamicPromptsSlice';
export const dynamicPromptsPersistDenylist: (keyof typeof initialDynamicPromptsState)[] = ['prompts'];

View File

@ -1,6 +1,6 @@
import type { PayloadAction } from '@reduxjs/toolkit';
import { createSlice } from '@reduxjs/toolkit';
import type { RootState } from 'app/store/store';
import type { PersistConfig, RootState } from 'app/store/store';
import { z } from 'zod';
export const zSeedBehaviour = z.enum(['PER_ITERATION', 'PER_PROMPT']);
@ -85,3 +85,10 @@ export const migrateDynamicPromptsState = (state: any): any => {
}
return state;
};
export const dynamicPromptsPersistConfig: PersistConfig<DynamicPromptsState> = {
name: dynamicPromptsSlice.name,
initialState: initialDynamicPromptsState,
migrate: migrateDynamicPromptsState,
persistDenylist: ['prompts'],
};

View File

@ -1,12 +0,0 @@
import type { initialGalleryState } from './gallerySlice';
/**
* Gallery slice persist denylist
*/
export const galleryPersistDenylist: (keyof typeof initialGalleryState)[] = [
'selection',
'selectedBoardId',
'galleryView',
'offset',
'limit',
];

View File

@ -1,6 +1,6 @@
import type { PayloadAction } from '@reduxjs/toolkit';
import { createSlice, isAnyOf } from '@reduxjs/toolkit';
import type { RootState } from 'app/store/store';
import type { PersistConfig, RootState } from 'app/store/store';
import { uniqBy } from 'lodash-es';
import { boardsApi } from 'services/api/endpoints/boards';
import { imagesApi } from 'services/api/endpoints/images';
@ -125,3 +125,10 @@ export const migrateGalleryState = (state: any): any => {
}
return state;
};
export const galleryPersistConfig: PersistConfig<GalleryState> = {
name: gallerySlice.name,
initialState: initialGalleryState,
migrate: migrateGalleryState,
persistDenylist: ['selection', 'selectedBoardId', 'galleryView', 'offset', 'limit'],
};

View File

@ -1,6 +1,6 @@
import type { PayloadAction } from '@reduxjs/toolkit';
import { createSlice } from '@reduxjs/toolkit';
import type { RootState } from 'app/store/store';
import type { PersistConfig, RootState } from 'app/store/store';
import type { ParameterHRFMethod, ParameterStrength } from 'features/parameters/types/parameterSchemas';
export interface HRFState {
@ -48,3 +48,10 @@ export const migrateHRFState = (state: any): any => {
}
return state;
};
export const hrfPersistConfig: PersistConfig<HRFState> = {
name: hrfSlice.name,
initialState: initialHRFState,
migrate: migrateHRFState,
persistDenylist: [],
};

View File

@ -1,6 +1,6 @@
import type { PayloadAction } from '@reduxjs/toolkit';
import { createSlice } from '@reduxjs/toolkit';
import type { RootState } from 'app/store/store';
import type { PersistConfig, RootState } from 'app/store/store';
import type { ParameterLoRAModel } from 'features/parameters/types/parameterSchemas';
import type { LoRAModelConfigEntity } from 'services/api/endpoints/models';
@ -92,3 +92,10 @@ export const migrateLoRAState = (state: any): any => {
}
return state;
};
export const loraPersistConfig: PersistConfig<LoraState> = {
name: loraSlice.name,
initialState: initialLoraState,
migrate: migrateLoRAState,
persistDenylist: [],
};

View File

@ -1,6 +1,6 @@
import type { PayloadAction } from '@reduxjs/toolkit';
import { createSlice } from '@reduxjs/toolkit';
import type { RootState } from 'app/store/store';
import type { PersistConfig, RootState } from 'app/store/store';
type ModelManagerState = {
_version: 1;
@ -40,3 +40,10 @@ export const migrateModelManagerState = (state: any): any => {
}
return state;
};
export const modelManagerPersistConfig: PersistConfig<ModelManagerState> = {
name: modelManagerSlice.name,
initialState: initialModelManagerState,
migrate: migrateModelManagerState,
persistDenylist: [],
};

View File

@ -1,17 +0,0 @@
import type { NodesState } from './types';
/**
* Nodes slice persist denylist
*/
export const nodesPersistDenylist: (keyof NodesState)[] = [
'connectionStartParams',
'connectionStartFieldType',
'selectedNodes',
'selectedEdges',
'isReady',
'nodesToCopy',
'edgesToCopy',
'connectionMade',
'modifyingEdge',
'addNewNodePosition',
];

View File

@ -1,6 +1,6 @@
import type { PayloadAction } from '@reduxjs/toolkit';
import { createSlice, isAnyOf } from '@reduxjs/toolkit';
import type { RootState } from 'app/store/store';
import type { PersistConfig, RootState } from 'app/store/store';
import { workflowLoaded } from 'features/nodes/store/actions';
import { nodeTemplatesBuilt } from 'features/nodes/store/nodeTemplatesSlice';
import { SHARED_NODE_PROPERTIES } from 'features/nodes/types/constants';
@ -863,3 +863,21 @@ export const migrateNodesState = (state: any): any => {
}
return state;
};
export const nodesPersistConfig: PersistConfig<NodesState> = {
name: nodesSlice.name,
initialState: initialNodesState,
migrate: migrateNodesState,
persistDenylist: [
'connectionStartParams',
'connectionStartFieldType',
'selectedNodes',
'selectedEdges',
'isReady',
'nodesToCopy',
'edgesToCopy',
'connectionMade',
'modifyingEdge',
'addNewNodePosition',
],
};

View File

@ -1,6 +1,6 @@
import type { PayloadAction } from '@reduxjs/toolkit';
import { createSlice } from '@reduxjs/toolkit';
import type { RootState } from 'app/store/store';
import type { PersistConfig, RootState } from 'app/store/store';
import { workflowLoaded } from 'features/nodes/store/actions';
import { isAnyNodeOrEdgeMutation, nodeEditorReset, nodesDeleted } from 'features/nodes/store/nodesSlice';
import type { WorkflowsState as WorkflowState } from 'features/nodes/store/types';
@ -130,3 +130,10 @@ export const migrateWorkflowState = (state: any): any => {
}
return state;
};
export const workflowPersistConfig: PersistConfig<WorkflowState> = {
name: workflowSlice.name,
initialState: initialWorkflowState,
migrate: migrateWorkflowState,
persistDenylist: [],
};

View File

@ -1,6 +0,0 @@
import type { GenerationState } from './types';
/**
* Generation slice persist denylist
*/
export const generationPersistDenylist: (keyof GenerationState)[] = [];

View File

@ -1,6 +1,6 @@
import type { PayloadAction } from '@reduxjs/toolkit';
import { createSlice } from '@reduxjs/toolkit';
import type { RootState } from 'app/store/store';
import type { PersistConfig, RootState } from 'app/store/store';
import { roundToMultiple } from 'common/util/roundDownToMultiple';
import { isAnyControlAdapterAdded } from 'features/controlAdapters/store/controlAdaptersSlice';
import { calculateNewSize } from 'features/parameters/components/ImageSize/calculateNewSize';
@ -311,3 +311,10 @@ export const migrateGenerationState = (state: any): GenerationState => {
}
return state;
};
export const generationPersistConfig: PersistConfig<GenerationState> = {
name: generationSlice.name,
initialState: initialGenerationState,
migrate: migrateGenerationState,
persistDenylist: [],
};

View File

@ -1,6 +0,0 @@
import type { PostprocessingState } from './postprocessingSlice';
/**
* Postprocessing slice persist denylist
*/
export const postprocessingPersistDenylist: (keyof PostprocessingState)[] = [];

View File

@ -1,6 +1,6 @@
import type { PayloadAction } from '@reduxjs/toolkit';
import { createSlice } from '@reduxjs/toolkit';
import type { RootState } from 'app/store/store';
import type { PersistConfig, RootState } from 'app/store/store';
import { z } from 'zod';
export const zParamESRGANModelName = z.enum([
@ -46,3 +46,10 @@ export const migratePostprocessingState = (state: any): any => {
}
return state;
};
export const postprocessingPersistConfig: PersistConfig<PostprocessingState> = {
name: postprocessingSlice.name,
initialState: initialPostprocessingState,
migrate: migratePostprocessingState,
persistDenylist: [],
};

View File

@ -1,6 +1,6 @@
import type { PayloadAction } from '@reduxjs/toolkit';
import { createSlice } from '@reduxjs/toolkit';
import type { RootState } from 'app/store/store';
import type { PersistConfig, RootState } from 'app/store/store';
import type {
ParameterNegativeStylePromptSDXL,
ParameterPositiveStylePromptSDXL,
@ -97,3 +97,10 @@ export const migrateSDXLState = (state: any): any => {
}
return state;
};
export const sdxlPersistConfig: PersistConfig<SDXLState> = {
name: sdxlSlice.name,
initialState: initialSDXLState,
migrate: migrateSDXLState,
persistDenylist: [],
};

View File

@ -1,3 +0,0 @@
import type { SystemState } from './types';
export const systemPersistDenylist: (keyof SystemState)[] = ['isConnected', 'denoiseProgress', 'status'];

View File

@ -1,7 +1,7 @@
import type { UseToastOptions } from '@invoke-ai/ui-library';
import type { PayloadAction } from '@reduxjs/toolkit';
import { createSlice, isAnyOf } from '@reduxjs/toolkit';
import type { RootState } from 'app/store/store';
import type { PersistConfig, RootState } from 'app/store/store';
import { calculateStepPercentage } from 'features/system/util/calculateStepPercentage';
import { makeToast } from 'features/system/util/makeToast';
import { t } from 'i18next';
@ -207,3 +207,10 @@ export const migrateSystemState = (state: any): any => {
}
return state;
};
export const systemPersistConfig: PersistConfig<SystemState> = {
name: systemSlice.name,
initialState: initialSystemState,
migrate: migrateSystemState,
persistDenylist: ['isConnected', 'denoiseProgress', 'status'],
};

View File

@ -1,6 +0,0 @@
import type { UIState } from './uiTypes';
/**
* UI slice persist denylist
*/
export const uiPersistDenylist: (keyof UIState)[] = ['shouldShowImageDetails'];

View File

@ -1,6 +1,6 @@
import type { PayloadAction } from '@reduxjs/toolkit';
import { createSlice } from '@reduxjs/toolkit';
import type { RootState } from 'app/store/store';
import type { PersistConfig, RootState } from 'app/store/store';
import { initialImageChanged } from 'features/parameters/store/generationSlice';
import type { InvokeTabName } from './tabMap';
@ -68,3 +68,10 @@ export const migrateUIState = (state: any): any => {
}
return state;
};
export const uiPersistConfig: PersistConfig<UIState> = {
name: uiSlice.name,
initialState: initialUIState,
migrate: migrateUIState,
persistDenylist: ['shouldShowImageDetails'],
};