Skip to content

Commit d0d4783

Browse files
refactor(ui): alternate approach to slice configs
1 parent 53a9f47 commit d0d4783

File tree

22 files changed

+115
-156
lines changed

22 files changed

+115
-156
lines changed
Lines changed: 25 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,25 @@
11
import { StorageError } from 'app/store/enhancers/reduxRemember/errors';
22
import { $authToken } from 'app/store/nanostores/authToken';
33
import { $projectId } from 'app/store/nanostores/projectId';
4-
import type { UseStore } from 'idb-keyval';
5-
import { clear, createStore as createIDBKeyValStore, get, set } from 'idb-keyval';
6-
import { atom } from 'nanostores';
4+
import { $queueId } from 'app/store/nanostores/queueId';
5+
import { $isPendingPersist } from 'app/store/store';
76
import type { Driver } from 'redux-remember';
87
import { getBaseUrl } from 'services/api';
98
import { buildAppInfoUrl } from 'services/api/endpoints/appInfo';
109

11-
// Create a custom idb-keyval store (just needed to customize the name)
12-
const $idbKeyValStore = atom<UseStore>(createIDBKeyValStore('invoke', 'invoke-store'));
13-
14-
export const clearIdbKeyValStore = () => {
15-
clear($idbKeyValStore.get());
16-
};
17-
18-
// Create redux-remember driver, wrapping idb-keyval
19-
export const idbKeyValDriver: Driver = {
20-
getItem: (key) => {
21-
try {
22-
return get(key, $idbKeyValStore.get());
23-
} catch (originalError) {
24-
throw new StorageError({
25-
key,
26-
projectId: $projectId.get(),
27-
originalError,
28-
});
29-
}
30-
},
31-
setItem: (key, value) => {
32-
try {
33-
return set(key, value, $idbKeyValStore.get());
34-
} catch (originalError) {
35-
throw new StorageError({
36-
key,
37-
value,
38-
projectId: $projectId.get(),
39-
originalError,
40-
});
41-
}
42-
},
10+
const getUrl = (key?: string) => {
11+
const baseUrl = getBaseUrl();
12+
const query: Record<string, string> = {};
13+
if (key) {
14+
query['key'] = key;
15+
}
16+
const queueId = $queueId.get();
17+
if (queueId) {
18+
query['queueId'] = queueId;
19+
}
20+
const path = buildAppInfoUrl('client_state', query);
21+
const url = `${baseUrl}/${path}`;
22+
return url;
4323
};
4424

4525
const getHeaders = (extra?: Record<string, string>) => {
@@ -61,9 +41,7 @@ const getHeaders = (extra?: Record<string, string>) => {
6141
export const serverBackedDriver: Driver = {
6242
getItem: async (key) => {
6343
try {
64-
const baseUrl = getBaseUrl();
65-
const path = buildAppInfoUrl('client_state', { key });
66-
const url = `${baseUrl}/${path}`;
44+
const url = getUrl(key);
6745
const headers = getHeaders();
6846
const res = await fetch(url, { headers, method: 'GET' });
6947
if (!res.ok) {
@@ -81,11 +59,9 @@ export const serverBackedDriver: Driver = {
8159
},
8260
setItem: async (key, value) => {
8361
try {
84-
const baseUrl = getBaseUrl();
85-
const path = buildAppInfoUrl('client_state');
86-
const url = `${baseUrl}/${path}`;
62+
const url = getUrl(key);
8763
const headers = getHeaders({ 'content-type': 'application/json' });
88-
const res = await fetch(url, { headers, method: 'POST', body: JSON.stringify({ key, value }) });
64+
const res = await fetch(url, { headers, method: 'POST', body: JSON.stringify(value) });
8965
if (!res.ok) {
9066
throw new Error(`Response status: ${res.status}`);
9167
}
@@ -102,12 +78,16 @@ export const serverBackedDriver: Driver = {
10278
};
10379

10480
export const resetClientState = async () => {
105-
const baseUrl = getBaseUrl();
106-
const path = buildAppInfoUrl('client_state');
107-
const url = `${baseUrl}/${path}`;
81+
const url = getUrl();
10882
const headers = getHeaders();
10983
const res = await fetch(url, { headers, method: 'DELETE' });
11084
if (!res.ok) {
11185
throw new Error(`Response status: ${res.status}`);
11286
}
11387
};
88+
89+
window.addEventListener('beforeunload', (e) => {
90+
if ($isPendingPersist.get()) {
91+
e.preventDefault();
92+
}
93+
});

invokeai/frontend/web/src/app/store/store.ts

Lines changed: 61 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { ThunkDispatch, TypedStartListening, UnknownAction } from '@reduxjs/toolkit';
2-
import { addListener, combineReducers, configureStore, createAction, createListenerMiddleware } from '@reduxjs/toolkit';
2+
import { addListener, combineReducers, configureStore, createListenerMiddleware } from '@reduxjs/toolkit';
33
import { logger } from 'app/logging/logger';
44
import { serverBackedDriver } from 'app/store/enhancers/reduxRemember/driver';
55
import { errorHandler } from 'app/store/enhancers/reduxRemember/errors';
@@ -43,14 +43,13 @@ import { diff } from 'jsondiffpatch';
4343
import { atom } from 'nanostores';
4444
import dynamicMiddlewares from 'redux-dynamic-middlewares';
4545
import type { SerializeFunction, UnserializeFunction } from 'redux-remember';
46-
import { REMEMBER_PERSISTED, rememberEnhancer, rememberReducer } from 'redux-remember';
47-
import { newHistory } from 'redux-undo';
46+
import { rememberEnhancer, rememberReducer } from 'redux-remember';
47+
import undoable, { newHistory } from 'redux-undo';
4848
import { serializeError } from 'serialize-error';
4949
import { api } from 'services/api';
5050
import { authToastMiddleware } from 'services/api/authToastMiddleware';
5151
import type { JsonObject } from 'type-fest';
5252

53-
import { getDebugLoggerMiddleware } from './middleware/debugLoggerMiddleware';
5453
import { actionSanitizer } from './middleware/devtools/actionSanitizer';
5554
import { actionsDenylist } from './middleware/devtools/actionsDenylist';
5655
import { stateSanitizer } from './middleware/devtools/stateSanitizer';
@@ -61,49 +60,60 @@ export const listenerMiddleware = createListenerMiddleware();
6160

6261
const log = logger('system');
6362

63+
// When adding a slice, add the config to the SLICE_CONFIGS object below, then add the reducer to ALL_REDUCERS.
64+
// Remember to wrap undoable slices in `undoable()`.
65+
6466
const SLICE_CONFIGS = {
65-
[canvasSessionSliceConfig.slice.name]: canvasSessionSliceConfig,
66-
[canvasSettingsSliceConfig.slice.name]: canvasSettingsSliceConfig,
67-
[canvasSliceConfig.slice.name]: canvasSliceConfig,
68-
[changeBoardModalSliceConfig.slice.name]: changeBoardModalSliceConfig,
69-
[configSliceConfig.slice.name]: configSliceConfig,
70-
[dynamicPromptsSliceConfig.slice.name]: dynamicPromptsSliceConfig,
71-
[gallerySliceConfig.slice.name]: gallerySliceConfig,
72-
[lorasSliceConfig.slice.name]: lorasSliceConfig,
73-
[modelManagerSliceConfig.slice.name]: modelManagerSliceConfig,
74-
[nodesSliceConfig.slice.name]: nodesSliceConfig,
75-
[paramsSliceConfig.slice.name]: paramsSliceConfig,
76-
[queueSliceConfig.slice.name]: queueSliceConfig,
77-
[refImagesSliceConfig.slice.name]: refImagesSliceConfig,
78-
[stylePresetSliceConfig.slice.name]: stylePresetSliceConfig,
79-
[systemSliceConfig.slice.name]: systemSliceConfig,
80-
[uiSliceConfig.slice.name]: uiSliceConfig,
81-
[upscaleSliceConfig.slice.name]: upscaleSliceConfig,
82-
[workflowLibrarySliceConfig.slice.name]: workflowLibrarySliceConfig,
83-
[workflowSettingsSliceConfig.slice.name]: workflowSettingsSliceConfig,
67+
[canvasSessionSliceConfig.slice.reducerPath]: canvasSessionSliceConfig,
68+
[canvasSettingsSliceConfig.slice.reducerPath]: canvasSettingsSliceConfig,
69+
[canvasSliceConfig.slice.reducerPath]: canvasSliceConfig,
70+
[changeBoardModalSliceConfig.slice.reducerPath]: changeBoardModalSliceConfig,
71+
[configSliceConfig.slice.reducerPath]: configSliceConfig,
72+
[dynamicPromptsSliceConfig.slice.reducerPath]: dynamicPromptsSliceConfig,
73+
[gallerySliceConfig.slice.reducerPath]: gallerySliceConfig,
74+
[lorasSliceConfig.slice.reducerPath]: lorasSliceConfig,
75+
[modelManagerSliceConfig.slice.reducerPath]: modelManagerSliceConfig,
76+
[nodesSliceConfig.slice.reducerPath]: nodesSliceConfig,
77+
[paramsSliceConfig.slice.reducerPath]: paramsSliceConfig,
78+
[queueSliceConfig.slice.reducerPath]: queueSliceConfig,
79+
[refImagesSliceConfig.slice.reducerPath]: refImagesSliceConfig,
80+
[stylePresetSliceConfig.slice.reducerPath]: stylePresetSliceConfig,
81+
[systemSliceConfig.slice.reducerPath]: systemSliceConfig,
82+
[uiSliceConfig.slice.reducerPath]: uiSliceConfig,
83+
[upscaleSliceConfig.slice.reducerPath]: upscaleSliceConfig,
84+
[workflowLibrarySliceConfig.slice.reducerPath]: workflowLibrarySliceConfig,
85+
[workflowSettingsSliceConfig.slice.reducerPath]: workflowSettingsSliceConfig,
8486
};
8587

8688
const ALL_REDUCERS = {
8789
[api.reducerPath]: api.reducer,
88-
[canvasSessionSliceConfig.slice.name]: canvasSessionSliceConfig.slice.reducer,
89-
[canvasSettingsSliceConfig.slice.name]: canvasSettingsSliceConfig.slice.reducer,
90-
[canvasSliceConfig.slice.name]: canvasSliceConfig.slice.reducer,
91-
[changeBoardModalSliceConfig.slice.name]: changeBoardModalSliceConfig.slice.reducer,
92-
[configSliceConfig.slice.name]: configSliceConfig.slice.reducer,
93-
[dynamicPromptsSliceConfig.slice.name]: dynamicPromptsSliceConfig.slice.reducer,
94-
[gallerySliceConfig.slice.name]: gallerySliceConfig.slice.reducer,
95-
[lorasSliceConfig.slice.name]: lorasSliceConfig.slice.reducer,
96-
[modelManagerSliceConfig.slice.name]: modelManagerSliceConfig.slice.reducer,
97-
[nodesSliceConfig.slice.name]: nodesSliceConfig.slice.reducer,
98-
[paramsSliceConfig.slice.name]: paramsSliceConfig.slice.reducer,
99-
[queueSliceConfig.slice.name]: queueSliceConfig.slice.reducer,
100-
[refImagesSliceConfig.slice.name]: refImagesSliceConfig.slice.reducer,
101-
[stylePresetSliceConfig.slice.name]: stylePresetSliceConfig.slice.reducer,
102-
[systemSliceConfig.slice.name]: systemSliceConfig.slice.reducer,
103-
[uiSliceConfig.slice.name]: uiSliceConfig.slice.reducer,
104-
[upscaleSliceConfig.slice.name]: upscaleSliceConfig.slice.reducer,
105-
[workflowLibrarySliceConfig.slice.name]: workflowLibrarySliceConfig.slice.reducer,
106-
[workflowSettingsSliceConfig.slice.name]: workflowSettingsSliceConfig.slice.reducer,
90+
[canvasSessionSliceConfig.slice.reducerPath]: canvasSessionSliceConfig.slice.reducer,
91+
[canvasSettingsSliceConfig.slice.reducerPath]: canvasSettingsSliceConfig.slice.reducer,
92+
// Undoable!
93+
[canvasSliceConfig.slice.reducerPath]: undoable(
94+
canvasSliceConfig.slice.reducer,
95+
canvasSliceConfig.undoableConfig?.reduxUndoOptions
96+
),
97+
[changeBoardModalSliceConfig.slice.reducerPath]: changeBoardModalSliceConfig.slice.reducer,
98+
[configSliceConfig.slice.reducerPath]: configSliceConfig.slice.reducer,
99+
[dynamicPromptsSliceConfig.slice.reducerPath]: dynamicPromptsSliceConfig.slice.reducer,
100+
[gallerySliceConfig.slice.reducerPath]: gallerySliceConfig.slice.reducer,
101+
[lorasSliceConfig.slice.reducerPath]: lorasSliceConfig.slice.reducer,
102+
[modelManagerSliceConfig.slice.reducerPath]: modelManagerSliceConfig.slice.reducer,
103+
// Undoable!
104+
[nodesSliceConfig.slice.reducerPath]: undoable(
105+
nodesSliceConfig.slice.reducer,
106+
nodesSliceConfig.undoableConfig?.reduxUndoOptions
107+
),
108+
[paramsSliceConfig.slice.reducerPath]: paramsSliceConfig.slice.reducer,
109+
[queueSliceConfig.slice.reducerPath]: queueSliceConfig.slice.reducer,
110+
[refImagesSliceConfig.slice.reducerPath]: refImagesSliceConfig.slice.reducer,
111+
[stylePresetSliceConfig.slice.reducerPath]: stylePresetSliceConfig.slice.reducer,
112+
[systemSliceConfig.slice.reducerPath]: systemSliceConfig.slice.reducer,
113+
[uiSliceConfig.slice.reducerPath]: uiSliceConfig.slice.reducer,
114+
[upscaleSliceConfig.slice.reducerPath]: upscaleSliceConfig.slice.reducer,
115+
[workflowLibrarySliceConfig.slice.reducerPath]: workflowLibrarySliceConfig.slice.reducer,
116+
[workflowSettingsSliceConfig.slice.reducerPath]: workflowSettingsSliceConfig.slice.reducer,
107117
};
108118

109119
const rootReducer = combineReducers(ALL_REDUCERS);
@@ -112,6 +122,10 @@ const rememberedRootReducer = rememberReducer(rootReducer);
112122

113123
export const $isPendingPersist = atom(false);
114124

125+
$isPendingPersist.listen((isPendingPersist) => {
126+
console.log({ isPendingPersist });
127+
});
128+
115129
const unserialize: UnserializeFunction = (data, key) => {
116130
const sliceConfig = SLICE_CONFIGS[key as keyof typeof SLICE_CONFIGS];
117131
if (!sliceConfig?.persistConfig) {
@@ -164,10 +178,11 @@ const serialize: SerializeFunction = (data, key) => {
164178
if (!sliceConfig?.persistConfig) {
165179
throw new Error(`No persist config for slice "${key}"`);
166180
}
167-
// Heuristic to determine if the slice is undoable - could just hardcode it in the persistConfig
168-
const isUndoable = 'present' in data && 'past' in data && 'future' in data && '_latestUnfiltered' in data;
169181

170-
const result = omit(isUndoable ? data.present : data, sliceConfig.persistConfig.persistDenylist ?? []);
182+
const result = omit(
183+
sliceConfig.undoableConfig ? data.present : data,
184+
sliceConfig.persistConfig.persistDenylist ?? []
185+
);
171186
return JSON.stringify(result);
172187
};
173188

@@ -187,7 +202,7 @@ export const createStore = (uniqueStoreKey?: string, persist = true) =>
187202
.concat(api.middleware)
188203
.concat(dynamicMiddlewares)
189204
.concat(authToastMiddleware)
190-
.concat(getDebugLoggerMiddleware())
205+
// .concat(getDebugLoggerMiddleware())
191206
.prepend(listenerMiddleware.middleware),
192207
enhancers: (getDefaultEnhancers) => {
193208
const enhancers = getDefaultEnhancers();
@@ -266,40 +281,3 @@ addAppConfigReceivedListener(startAppListening);
266281
addAdHocPostProcessingRequestedListener(startAppListening);
267282

268283
addSetDefaultSettingsListener(startAppListening);
269-
270-
const addPersistenceListener = (startAppListening: AppStartListening) => {
271-
startAppListening({
272-
predicate: (action, currentRootState, originalRootState) => {
273-
for (const { slice, persistConfig } of Object.values(PERSISTED_SLICE_CONFIGS)) {
274-
if (!persistConfig) {
275-
// shouldn't get here, we filtered out slices without persistConfig
276-
return false;
277-
}
278-
const persistDenylist: string[] = persistConfig.persistDenylist ?? [];
279-
const originalState = originalRootState[slice.name];
280-
const currentState = currentRootState[slice.name];
281-
for (const [k, v] of Object.entries(currentState)) {
282-
if (persistDenylist.includes(k)) {
283-
continue;
284-
}
285-
286-
if (v !== originalState[k as keyof typeof originalState]) {
287-
return true;
288-
}
289-
}
290-
}
291-
return false;
292-
},
293-
effect: () => {
294-
$isPendingPersist.set(true);
295-
},
296-
});
297-
298-
startAppListening({
299-
matcher: createAction(REMEMBER_PERSISTED).match,
300-
effect: () => {
301-
$isPendingPersist.set(false);
302-
},
303-
});
304-
};
305-
addPersistenceListener(startAppListening);
Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
1-
import type { Slice, UnknownAction } from '@reduxjs/toolkit';
1+
import type { Slice } from '@reduxjs/toolkit';
22
import type { UndoableOptions } from 'redux-undo';
33

4-
export type SliceConfig<T> = {
5-
slice: Slice<T>;
4+
type StateFromSlice<T extends Slice> = T extends Slice<infer U> ? U : never;
5+
6+
export type SliceConfig<T extends Slice = Slice> = {
7+
slice: T;
68
/**
79
* A function that returns the initial state of the slice.
810
*/
9-
getInitialState: () => T;
11+
getInitialState: () => StateFromSlice<T>;
1012
/**
1113
* The optional persist configuration for this slice. If omitted, the slice will not be persisted.
1214
*/
@@ -16,11 +18,11 @@ export type SliceConfig<T> = {
1618
* @param state The rehydrated state.
1719
* @returns A correctly-shaped state.
1820
*/
19-
migrate: (state: unknown) => T;
21+
migrate: (state: unknown) => StateFromSlice<T>;
2022
/**
2123
* Keys to omit from the persisted state.
2224
*/
23-
persistDenylist?: (keyof T)[];
25+
persistDenylist?: (keyof StateFromSlice<T>)[];
2426
};
2527
/**
2628
* The optional undoable configuration for this slice. If omitted, the slice will not be undoable.
@@ -29,6 +31,6 @@ export type SliceConfig<T> = {
2931
/**
3032
* The options to be passed into redux-undo.
3133
*/
32-
reduxUndoOptions: UndoableOptions<T, UnknownAction>;
34+
reduxUndoOptions: UndoableOptions<StateFromSlice<T>>;
3335
};
3436
};

invokeai/frontend/web/src/features/changeBoardModal/store/slice.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import type { SliceConfig } from 'app/store/types';
55
import { deepClone } from 'common/util/deepClone';
66

77
import { initialState } from './initialState';
8-
import type { ChangeBoardModalState } from './types';
98

109
const getInitialState = () => deepClone(initialState);
1110

@@ -30,7 +29,7 @@ export const { isModalOpenChanged, imagesToChangeSelected, changeBoardReset } =
3029

3130
export const selectChangeBoardModalSlice = (state: RootState) => state.changeBoardModal;
3231

33-
export const changeBoardModalSliceConfig: SliceConfig<ChangeBoardModalState> = {
32+
export const changeBoardModalSliceConfig: SliceConfig<typeof slice> = {
3433
slice,
3534
getInitialState,
3635
};

invokeai/frontend/web/src/features/controlLayers/store/canvasSettingsSlice.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ const migrate = (state: any): any => {
192192
return state;
193193
};
194194

195-
export const canvasSettingsSliceConfig: SliceConfig<CanvasSettingsState> = {
195+
export const canvasSettingsSliceConfig: SliceConfig<typeof slice> = {
196196
slice,
197197
getInitialState,
198198
persistConfig: {

invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1721,7 +1721,7 @@ export const canvasUndoableConfig: UndoableOptions<CanvasState, UnknownAction> =
17211721
// debug: import.meta.env.MODE === 'development',
17221722
};
17231723

1724-
export const canvasSliceConfig: SliceConfig<CanvasState> = {
1724+
export const canvasSliceConfig: SliceConfig<typeof slice> = {
17251725
slice,
17261726
getInitialState: getInitialCanvasState,
17271727
persistConfig: {

invokeai/frontend/web/src/features/controlLayers/store/canvasStagingAreaSlice.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ const migrate = (state: any): any => {
6161
return state;
6262
};
6363

64-
export const canvasSessionSliceConfig: SliceConfig<CanvasStagingAreaState> = {
64+
export const canvasSessionSliceConfig: SliceConfig<typeof slice> = {
6565
slice,
6666
getInitialState,
6767
persistConfig: { migrate },

invokeai/frontend/web/src/features/controlLayers/store/lorasSlice.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ const migrate = (state: any): any => {
7979
return state;
8080
};
8181

82-
export const lorasSliceConfig: SliceConfig<LoRAsState> = {
82+
export const lorasSliceConfig: SliceConfig<typeof slice> = {
8383
slice,
8484
getInitialState,
8585
persistConfig: {

0 commit comments

Comments
 (0)