@@ -3,8 +3,12 @@ import { StorageError } from 'app/store/enhancers/reduxRemember/errors';
33import { $authToken } from 'app/store/nanostores/authToken' ;
44import { $projectId } from 'app/store/nanostores/projectId' ;
55import { $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' ;
68import type { Driver } from 'redux-remember' ;
9+ import { serializeError } from 'serialize-error' ;
710import { buildV1Url , getBaseUrl } from 'services/api' ;
11+ import type { JsonObject } from 'type-fest' ;
812
913const 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.
5761const 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+
117177export const clearStorage = async ( ) => {
118178 try {
119179 persistRefCount ++ ;
0 commit comments