@@ -123,6 +123,12 @@ export class TokenListController extends StaticIntervalPollingController<TokenLi
123123> {
124124 readonly #mutex = new Mutex ( ) ;
125125
126+ /**
127+ * Promise that resolves when initialization (loading cache from storage) is complete.
128+ * Methods that access the cache should await this before proceeding.
129+ */
130+ readonly #initializationPromise: Promise < void > ;
131+
126132 // Storage key prefix for per-chain files
127133 static readonly #storageKeyPrefix = 'tokensChainsCache' ;
128134
@@ -191,18 +197,9 @@ export class TokenListController extends StaticIntervalPollingController<TokenLi
191197 this . updatePreventPollingOnNetworkRestart ( preventPollingOnNetworkRestart ) ;
192198 this . #abortController = new AbortController ( ) ;
193199
194- // Load cache from StorageService on initialization and handle migration
195- this . #loadCacheFromStorage( )
196- . then ( ( ) => {
197- // Migrate existing cache from state to StorageService if needed
198- return this . #migrateStateToStorage( ) ;
199- } )
200- . catch ( ( error ) => {
201- console . error (
202- 'TokenListController: Failed to load cache from storage:' ,
203- error ,
204- ) ;
205- } ) ;
200+ // Load cache from StorageService on initialization and handle migration.
201+ // Store the promise so other methods can await it to avoid race conditions.
202+ this . #initializationPromise = this . #initializeFromStorage( ) ;
206203
207204 if ( onNetworkStateChange ) {
208205 // TODO: Either fix this lint violation or explain why it's necessary to ignore.
@@ -222,11 +219,36 @@ export class TokenListController extends StaticIntervalPollingController<TokenLi
222219 }
223220 }
224221
222+ /**
223+ * Initialize the controller by loading cache from storage and running migration.
224+ * This method acquires the mutex to prevent race conditions with fetchTokenList.
225+ *
226+ * @returns A promise that resolves when initialization is complete.
227+ */
228+ async #initializeFromStorage( ) : Promise < void > {
229+ const releaseLock = await this . #mutex. acquire ( ) ;
230+ try {
231+ await this . #loadCacheFromStorage( ) ;
232+ await this . #migrateStateToStorage( ) ;
233+ } catch ( error ) {
234+ console . error (
235+ 'TokenListController: Failed to initialize from storage:' ,
236+ error ,
237+ ) ;
238+ } finally {
239+ releaseLock ( ) ;
240+ }
241+ }
242+
225243 /**
226244 * Load tokensChainsCache from StorageService into state.
227245 * Loads all cached chains from separate per-chain files in parallel.
228246 * Called during initialization to restore cached data.
229247 *
248+ * Note: This method merges loaded data with existing state to avoid
249+ * overwriting any fresh data that may have been fetched concurrently.
250+ * Caller must hold the mutex.
251+ *
230252 * @returns A promise that resolves when loading is complete.
231253 */
232254 async #loadCacheFromStorage( ) : Promise < void > {
@@ -278,10 +300,17 @@ export class TokenListController extends StaticIntervalPollingController<TokenLi
278300 }
279301 } ) ;
280302
281- // Load into state (all chains available for TokenDetectionController)
303+ // Merge loaded cache with existing state, preferring existing data
304+ // (which may be fresher if fetched during initialization)
282305 if ( Object . keys ( loadedCache ) . length > 0 ) {
283306 this . update ( ( state ) => {
284- state . tokensChainsCache = loadedCache ;
307+ // Only load chains that don't already exist in state
308+ // This prevents overwriting fresh API data with stale cached data
309+ for ( const [ chainId , cacheData ] of Object . entries ( loadedCache ) ) {
310+ if ( ! state . tokensChainsCache [ chainId as Hex ] ) {
311+ state . tokensChainsCache [ chainId as Hex ] = cacheData ;
312+ }
313+ }
285314 } ) ;
286315 }
287316 } catch ( error ) {
@@ -497,6 +526,10 @@ export class TokenListController extends StaticIntervalPollingController<TokenLi
497526 * @param chainId - The chainId of the current chain triggering the fetch.
498527 */
499528 async fetchTokenList ( chainId : Hex ) : Promise < void > {
529+ // Wait for initialization to complete before fetching
530+ // This ensures we have loaded any cached data from storage first
531+ await this . #initializationPromise;
532+
500533 const releaseLock = await this . #mutex. acquire ( ) ;
501534 try {
502535 if ( this . isCacheValid ( chainId ) ) {
@@ -571,6 +604,9 @@ export class TokenListController extends StaticIntervalPollingController<TokenLi
571604 * This clears both state and all per-chain files in StorageService.
572605 */
573606 async clearingTokenListData ( ) : Promise < void > {
607+ // Wait for initialization to complete before clearing
608+ await this . #initializationPromise;
609+
574610 // Clear state
575611 this . update ( ( state ) => {
576612 state . tokensChainsCache = { } ;
@@ -593,13 +629,6 @@ export class TokenListController extends StaticIntervalPollingController<TokenLi
593629 this . messenger . call ( 'StorageService:removeItem' , name , key ) ,
594630 ) ,
595631 ) ;
596-
597- // Also remove old single-file storage if it exists (cleanup)
598- await this . messenger . call (
599- 'StorageService:removeItem' ,
600- name ,
601- TokenListController . #storageKeyPrefix,
602- ) ;
603632 } catch ( error ) {
604633 console . error (
605634 'TokenListController: Failed to clear cache from storage:' ,
0 commit comments