Skip to content

Commit 5889fa5

Browse files
feat(ui): add migration path for client state from IndexedDB to server-backed storage
1 parent 0e71ba8 commit 5889fa5

File tree

3 files changed

+123
-54
lines changed

3 files changed

+123
-54
lines changed

invokeai/frontend/web/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@
6363
"framer-motion": "^11.10.0",
6464
"i18next": "^25.3.2",
6565
"i18next-http-backend": "^3.0.2",
66+
"idb-keyval": "6.2.1",
6667
"jsondiffpatch": "^0.7.3",
6768
"konva": "^9.3.22",
6869
"linkify-react": "^4.3.1",

invokeai/frontend/web/pnpm-lock.yaml

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

invokeai/frontend/web/src/app/store/enhancers/reduxRemember/driver.ts

Lines changed: 114 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,12 @@ import { StorageError } from 'app/store/enhancers/reduxRemember/errors';
33
import { $authToken } from 'app/store/nanostores/authToken';
44
import { $projectId } from 'app/store/nanostores/projectId';
55
import { $queueId } from 'app/store/nanostores/queueId';
6+
import type { UseStore } from 'idb-keyval';
7+
import { createStore as idbCreateStore, del as idbDel, get as idbGet } from 'idb-keyval';
68
import type { Driver } from 'redux-remember';
9+
import { serializeError } from 'serialize-error';
710
import { buildV1Url, getBaseUrl } from 'services/api';
11+
import type { JsonObject } from 'type-fest';
812

913
const log = logger('system');
1014

@@ -52,68 +56,124 @@ let persistRefCount = 0;
5256
// This logic is unknown to `redux-remember`. When an omitted field changes, it will still attempt to persist the
5357
// whole slice, even if the final, _serialized_ slice value is unchanged.
5458
//
55-
// To avoid unnecessary network requests, we keep track of the last persisted state for each key. If the value to
56-
// be persisted is the same as the last persisted value, we can skip the network request.
59+
// To avoid unnecessary network requests, we keep track of the last persisted state for each key in this map.
60+
// If the value to be persisted is the same as the last persisted value, we will skip the network request.
5761
const lastPersistedState = new Map<string, string | undefined>();
5862

59-
export const reduxRememberDriver: Driver = {
60-
getItem: async (key: string) => {
61-
try {
62-
const url = getUrl('get_by_key', key);
63-
const headers = getHeaders();
64-
const res = await fetch(url, { method: 'GET', headers });
65-
if (!res.ok) {
66-
throw new Error(`Response status: ${res.status}`);
67-
}
68-
const value = await res.json();
69-
lastPersistedState.set(key, value);
70-
log.trace({ key, last: lastPersistedState.get(key), next: value }, `Getting state for ${key}`);
71-
return value;
72-
} catch (originalError) {
73-
throw new StorageError({
74-
key,
75-
projectId: $projectId.get(),
76-
originalError,
77-
});
63+
// As of v6.3.0, we use server-backed storage for client state. This replaces the previous IndexedDB-based storage,
64+
// which was implemented using `idb-keyval`.
65+
//
66+
// To facilitate a smooth transition, we implement a migration strategy that attempts to retrieve values from IndexedDB
67+
// and persist them to the new server-backed storage. This is done on a best-effort basis.
68+
69+
// These constants were used in the previous IndexedDB-based storage implementation.
70+
const IDB_DB_NAME = 'invoke';
71+
const IDB_STORE_NAME = 'invoke-store';
72+
const IDB_STORAGE_PREFIX = '@@invokeai-';
73+
74+
// Lazy store creation
75+
let _idbKeyValStore: UseStore | null = null;
76+
const getIdbKeyValStore = () => {
77+
if (_idbKeyValStore === null) {
78+
_idbKeyValStore = idbCreateStore(IDB_DB_NAME, IDB_STORE_NAME);
79+
}
80+
return _idbKeyValStore;
81+
};
82+
83+
const getIdbKey = (key: string) => {
84+
return `${IDB_STORAGE_PREFIX}${key}`;
85+
};
86+
87+
const getItem = async (key: string) => {
88+
try {
89+
const url = getUrl('get_by_key', key);
90+
const headers = getHeaders();
91+
const res = await fetch(url, { method: 'GET', headers });
92+
if (!res.ok) {
93+
throw new Error(`Response status: ${res.status}`);
7894
}
79-
},
80-
setItem: async (key: string, value: string) => {
81-
try {
82-
persistRefCount++;
83-
if (lastPersistedState.get(key) === value) {
84-
log.trace(
85-
{ key, last: lastPersistedState.get(key), next: value },
86-
`Skipping persist for ${key} as value is unchanged`
95+
const value = await res.json();
96+
97+
// Best-effort migration from IndexedDB to the new storage system
98+
log.trace({ key, value }, 'Server-backed storage value retrieved');
99+
100+
if (!value) {
101+
const idbKey = getIdbKey(key);
102+
try {
103+
// It's a bit tricky to query IndexedDB directly to check if value exists, so we use `idb-keyval` to do it.
104+
// Thing is, `idb-keyval` requires you to create a store to query it. End result - we are creating a store
105+
// even if we don't use it for anything besides checking if the key is present.
106+
const idbKeyValStore = getIdbKeyValStore();
107+
const idbValue = await idbGet(idbKey, idbKeyValStore);
108+
if (idbValue) {
109+
log.debug(
110+
{ key, idbKey, idbValue },
111+
'No value in server-backed storage, but found value in IndexedDB - attempting migration'
112+
);
113+
await idbDel(idbKey, idbKeyValStore);
114+
await setItem(key, idbValue);
115+
log.debug({ key, idbKey, idbValue }, 'Migration successful');
116+
return idbValue;
117+
}
118+
} catch (error) {
119+
// Just log if IndexedDB retrieval fails - this is a best-effort migration.
120+
log.debug(
121+
{ key, idbKey, error: serializeError(error) } as JsonObject,
122+
'Error checking for or migrating from IndexedDB'
87123
);
88-
return value;
89-
}
90-
log.trace({ key, last: lastPersistedState.get(key), next: value }, `Persisting state for ${key}`);
91-
const url = getUrl('set_by_key', key);
92-
const headers = getHeaders();
93-
const res = await fetch(url, { method: 'POST', headers, body: value });
94-
if (!res.ok) {
95-
throw new Error(`Response status: ${res.status}`);
96-
}
97-
const resultValue = await res.json();
98-
lastPersistedState.set(key, resultValue);
99-
return resultValue;
100-
} catch (originalError) {
101-
throw new StorageError({
102-
key,
103-
value,
104-
projectId: $projectId.get(),
105-
originalError,
106-
});
107-
} finally {
108-
persistRefCount--;
109-
if (persistRefCount < 0) {
110-
log.trace('Persist ref count is negative, resetting to 0');
111-
persistRefCount = 0;
112124
}
113125
}
114-
},
126+
127+
lastPersistedState.set(key, value);
128+
log.trace({ key, last: lastPersistedState.get(key), next: value }, `Getting state for ${key}`);
129+
return value;
130+
} catch (originalError) {
131+
throw new StorageError({
132+
key,
133+
projectId: $projectId.get(),
134+
originalError,
135+
});
136+
}
137+
};
138+
139+
const setItem = async (key: string, value: string) => {
140+
try {
141+
persistRefCount++;
142+
if (lastPersistedState.get(key) === value) {
143+
log.trace(
144+
{ key, last: lastPersistedState.get(key), next: value },
145+
`Skipping persist for ${key} as value is unchanged`
146+
);
147+
return value;
148+
}
149+
log.trace({ key, last: lastPersistedState.get(key), next: value }, `Persisting state for ${key}`);
150+
const url = getUrl('set_by_key', key);
151+
const headers = getHeaders();
152+
const res = await fetch(url, { method: 'POST', headers, body: value });
153+
if (!res.ok) {
154+
throw new Error(`Response status: ${res.status}`);
155+
}
156+
const resultValue = await res.json();
157+
lastPersistedState.set(key, resultValue);
158+
return resultValue;
159+
} catch (originalError) {
160+
throw new StorageError({
161+
key,
162+
value,
163+
projectId: $projectId.get(),
164+
originalError,
165+
});
166+
} finally {
167+
persistRefCount--;
168+
if (persistRefCount < 0) {
169+
log.trace('Persist ref count is negative, resetting to 0');
170+
persistRefCount = 0;
171+
}
172+
}
115173
};
116174

175+
export const reduxRememberDriver: Driver = { getItem, setItem };
176+
117177
export const clearStorage = async () => {
118178
try {
119179
persistRefCount++;

0 commit comments

Comments
 (0)