@metamask-previews/assets-controllers 95.1.0-preview-f075c1f → 95.1.0-preview-821afcb8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -14,6 +14,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
14
14
 
15
15
  ### Changed
16
16
 
17
+ - **BREAKING:** `TokenListController` now persists `tokensChainsCache` via `StorageService` and requires clients to call `initialize()` after construction ([#7413](https://github.com/MetaMask/core/pull/7413))
18
+ - Each chain's token cache is stored in a separate file, reducing write amplification
19
+ - All chains are loaded in parallel at startup to maintain compatibility with TokenDetectionController
20
+ - `tokensChainsCache` state metadata now has `persist: false` to prevent duplicate persistence
21
+ - Clients must call `await controller.initialize()` before using the controller
22
+ - State changes are automatically persisted via debounced subscription
23
+ - Bump `@metamask/transaction-controller` from `^62.8.0` to `^62.9.0` ([#7602](https://github.com/MetaMask/core/pull/7602))
17
24
  - Bump `@metamask/transaction-controller` from `^62.8.0` to `^62.9.1` ([#7602](https://github.com/MetaMask/core/pull/7602), [#7604](https://github.com/MetaMask/core/pull/7604))
18
25
  - Bump `@metamask/network-controller` from `^27.2.0` to `^28.0.0` ([#7604](https://github.com/MetaMask/core/pull/7604))
19
26
  - Bump `@metamask/accounts-controller` from `^35.0.0` to `^35.0.1` ([#7604](https://github.com/MetaMask/core/pull/7604))
@@ -4,12 +4,17 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
4
4
  if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
5
5
  return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
6
6
  };
7
- var _TokenListController_instances, _TokenListController_onNetworkControllerStateChange, _TokenListController_startDeprecatedPolling;
7
+ var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
8
+ if (kind === "m") throw new TypeError("Private method is not writable");
9
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
10
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
11
+ return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
12
+ };
13
+ var _TokenListController_instances, _a, _TokenListController_initializationPromise, _TokenListController_persistDebounceTimer, _TokenListController_persistInFlightPromise, _TokenListController_changedChainsToPersist, _TokenListController_previousTokensChainsCache, _TokenListController_persistDebounceMs, _TokenListController_storageKeyPrefix, _TokenListController_getChainStorageKey, _TokenListController_intervalId, _TokenListController_intervalDelay, _TokenListController_cacheRefreshThreshold, _TokenListController_chainId, _TokenListController_abortController, _TokenListController_initializeFromStorage, _TokenListController_onCacheChanged, _TokenListController_debouncePersist, _TokenListController_persistChangedChains, _TokenListController_loadCacheFromStorage, _TokenListController_saveChainCacheToStorage, _TokenListController_onNetworkControllerStateChange, _TokenListController_stopPolling, _TokenListController_startDeprecatedPolling;
8
14
  Object.defineProperty(exports, "__esModule", { value: true });
9
15
  exports.TokenListController = exports.getDefaultTokenListState = void 0;
10
16
  const controller_utils_1 = require("@metamask/controller-utils");
11
17
  const polling_controller_1 = require("@metamask/polling-controller");
12
- const async_mutex_1 = require("async-mutex");
13
18
  const assetsUtil_1 = require("./assetsUtil.cjs");
14
19
  const token_service_1 = require("./token-service.cjs");
15
20
  // 4 Hour Interval Cache Refresh Threshold
@@ -19,7 +24,7 @@ const name = 'TokenListController';
19
24
  const metadata = {
20
25
  tokensChainsCache: {
21
26
  includeInStateLogs: false,
22
- persist: true,
27
+ persist: false, // Persisted separately via StorageService
23
28
  includeInDebugSnapshot: true,
24
29
  usedInUi: true,
25
30
  },
@@ -61,13 +66,41 @@ class TokenListController extends (0, polling_controller_1.StaticIntervalPolling
61
66
  state: { ...(0, exports.getDefaultTokenListState)(), ...state },
62
67
  });
63
68
  _TokenListController_instances.add(this);
64
- this.mutex = new async_mutex_1.Mutex();
65
- this.intervalDelay = interval;
69
+ /**
70
+ * Promise that resolves when initialization (loading cache from storage) is complete.
71
+ */
72
+ _TokenListController_initializationPromise.set(this, Promise.resolve());
73
+ /**
74
+ * Debounce timer for persisting state changes to storage.
75
+ */
76
+ _TokenListController_persistDebounceTimer.set(this, void 0);
77
+ /**
78
+ * Promise that resolves when the current persist operation completes.
79
+ * Used to prevent race conditions between persist and clear operations.
80
+ */
81
+ _TokenListController_persistInFlightPromise.set(this, void 0);
82
+ /**
83
+ * Tracks which chains have pending changes to persist.
84
+ * Only changed chains are persisted to reduce write amplification.
85
+ */
86
+ _TokenListController_changedChainsToPersist.set(this, new Set());
87
+ /**
88
+ * Previous tokensChainsCache for detecting which chains changed.
89
+ */
90
+ _TokenListController_previousTokensChainsCache.set(this, {});
91
+ _TokenListController_intervalId.set(this, void 0);
92
+ _TokenListController_intervalDelay.set(this, void 0);
93
+ _TokenListController_cacheRefreshThreshold.set(this, void 0);
94
+ _TokenListController_chainId.set(this, void 0);
95
+ _TokenListController_abortController.set(this, void 0);
96
+ __classPrivateFieldSet(this, _TokenListController_intervalDelay, interval, "f");
66
97
  this.setIntervalLength(interval);
67
- this.cacheRefreshThreshold = cacheRefreshThreshold;
68
- this.chainId = chainId;
98
+ __classPrivateFieldSet(this, _TokenListController_cacheRefreshThreshold, cacheRefreshThreshold, "f");
99
+ __classPrivateFieldSet(this, _TokenListController_chainId, chainId, "f");
69
100
  this.updatePreventPollingOnNetworkRestart(preventPollingOnNetworkRestart);
70
- this.abortController = new AbortController();
101
+ __classPrivateFieldSet(this, _TokenListController_abortController, new AbortController(), "f");
102
+ // Subscribe to state changes to automatically persist tokensChainsCache
103
+ this.messenger.subscribe('TokenListController:stateChange', (newCache) => __classPrivateFieldGet(this, _TokenListController_instances, "m", _TokenListController_onCacheChanged).call(this, newCache), (controllerState) => controllerState.tokensChainsCache);
71
104
  if (onNetworkStateChange) {
72
105
  // TODO: Either fix this lint violation or explain why it's necessary to ignore.
73
106
  // eslint-disable-next-line @typescript-eslint/no-misused-promises
@@ -84,6 +117,16 @@ class TokenListController extends (0, polling_controller_1.StaticIntervalPolling
84
117
  });
85
118
  }
86
119
  }
120
+ /**
121
+ * Initialize the controller by loading cache from storage and running migration.
122
+ * This method should be called by clients after construction.
123
+ *
124
+ * @returns A promise that resolves when initialization is complete.
125
+ */
126
+ async initialize() {
127
+ __classPrivateFieldSet(this, _TokenListController_initializationPromise, __classPrivateFieldGet(this, _TokenListController_instances, "m", _TokenListController_initializeFromStorage).call(this), "f");
128
+ await __classPrivateFieldGet(this, _TokenListController_initializationPromise, "f");
129
+ }
87
130
  // Eventually we want to remove start/restart/stop controls in favor of new _executePoll API
88
131
  // Maintaining these functions for now until we can safely deprecate them for backwards compatibility
89
132
  /**
@@ -93,7 +136,7 @@ class TokenListController extends (0, polling_controller_1.StaticIntervalPolling
93
136
  * Consider using the new polling approach instead
94
137
  */
95
138
  async start() {
96
- if (!(0, assetsUtil_1.isTokenListSupportedForNetwork)(this.chainId)) {
139
+ if (!(0, assetsUtil_1.isTokenListSupportedForNetwork)(__classPrivateFieldGet(this, _TokenListController_chainId, "f"))) {
97
140
  return;
98
141
  }
99
142
  await __classPrivateFieldGet(this, _TokenListController_instances, "m", _TokenListController_startDeprecatedPolling).call(this);
@@ -105,7 +148,7 @@ class TokenListController extends (0, polling_controller_1.StaticIntervalPolling
105
148
  * Consider using the new polling approach instead
106
149
  */
107
150
  async restart() {
108
- this.stopPolling();
151
+ __classPrivateFieldGet(this, _TokenListController_instances, "m", _TokenListController_stopPolling).call(this);
109
152
  await __classPrivateFieldGet(this, _TokenListController_instances, "m", _TokenListController_startDeprecatedPolling).call(this);
110
153
  }
111
154
  /**
@@ -115,7 +158,7 @@ class TokenListController extends (0, polling_controller_1.StaticIntervalPolling
115
158
  * Consider using the new polling approach instead
116
159
  */
117
160
  stop() {
118
- this.stopPolling();
161
+ __classPrivateFieldGet(this, _TokenListController_instances, "m", _TokenListController_stopPolling).call(this);
119
162
  }
120
163
  /**
121
164
  * This stops any active polling.
@@ -125,18 +168,13 @@ class TokenListController extends (0, polling_controller_1.StaticIntervalPolling
125
168
  */
126
169
  destroy() {
127
170
  super.destroy();
128
- this.stopPolling();
129
- }
130
- /**
131
- * This stops any active polling intervals.
132
- *
133
- * @deprecated This method is deprecated and will be removed in the future.
134
- * Consider using the new polling approach instead
135
- */
136
- stopPolling() {
137
- if (this.intervalId) {
138
- clearInterval(this.intervalId);
171
+ __classPrivateFieldGet(this, _TokenListController_instances, "m", _TokenListController_stopPolling).call(this);
172
+ // Cancel any pending debounced persistence operations
173
+ if (__classPrivateFieldGet(this, _TokenListController_persistDebounceTimer, "f")) {
174
+ clearTimeout(__classPrivateFieldGet(this, _TokenListController_persistDebounceTimer, "f"));
175
+ __classPrivateFieldSet(this, _TokenListController_persistDebounceTimer, undefined, "f");
139
176
  }
177
+ __classPrivateFieldGet(this, _TokenListController_changedChainsToPersist, "f").clear();
140
178
  }
141
179
  /**
142
180
  * This starts a new polling loop for any given chain. Under the hood it is deduping polls
@@ -149,71 +187,141 @@ class TokenListController extends (0, polling_controller_1.StaticIntervalPolling
149
187
  return this.fetchTokenList(chainId);
150
188
  }
151
189
  /**
152
- * Fetching token list from the Token Service API. This will fetch tokens across chains. It will update tokensChainsCache (scoped across chains), and also the tokenList (scoped for the selected chain)
190
+ * Fetching token list from the Token Service API. This will fetch tokens across chains.
191
+ * State changes are automatically persisted via the stateChange subscription.
153
192
  *
154
193
  * @param chainId - The chainId of the current chain triggering the fetch.
155
194
  */
156
195
  async fetchTokenList(chainId) {
157
- const releaseLock = await this.mutex.acquire();
158
- try {
159
- if (this.isCacheValid(chainId)) {
160
- return;
161
- }
162
- // Fetch fresh token list from the API
163
- const tokensFromAPI = await (0, controller_utils_1.safelyExecute)(() => (0, token_service_1.fetchTokenListByChainId)(chainId, this.abortController.signal));
164
- // Have response - process and update list
165
- if (tokensFromAPI) {
166
- // Format tokens from API (HTTP) and update tokenList
167
- const tokenList = {};
168
- for (const token of tokensFromAPI) {
169
- tokenList[token.address] = {
170
- ...token,
171
- aggregators: (0, assetsUtil_1.formatAggregatorNames)(token.aggregators),
172
- iconUrl: (0, assetsUtil_1.formatIconUrlWithProxy)({
173
- chainId,
174
- tokenAddress: token.address,
175
- }),
176
- };
177
- }
178
- this.update((state) => {
179
- var _a;
180
- const newDataCache = { data: {}, timestamp: Date.now() };
181
- (_a = state.tokensChainsCache)[chainId] ?? (_a[chainId] = newDataCache);
182
- state.tokensChainsCache[chainId].data = tokenList;
183
- state.tokensChainsCache[chainId].timestamp = Date.now();
184
- });
185
- return;
196
+ if (this.isCacheValid(chainId)) {
197
+ return;
198
+ }
199
+ // Fetch fresh token list from the API
200
+ const tokensFromAPI = await (0, controller_utils_1.safelyExecute)(() => (0, token_service_1.fetchTokenListByChainId)(chainId, __classPrivateFieldGet(this, _TokenListController_abortController, "f").signal));
201
+ // Have response - process and update list
202
+ if (tokensFromAPI) {
203
+ // Format tokens from API (HTTP) and update tokenList
204
+ const tokenList = {};
205
+ for (const token of tokensFromAPI) {
206
+ tokenList[token.address] = {
207
+ ...token,
208
+ aggregators: (0, assetsUtil_1.formatAggregatorNames)(token.aggregators),
209
+ iconUrl: (0, assetsUtil_1.formatIconUrlWithProxy)({
210
+ chainId,
211
+ tokenAddress: token.address,
212
+ }),
213
+ };
186
214
  }
187
- // No response - fallback to previous state, or initialise empty
188
- if (!tokensFromAPI) {
215
+ // Update state - persistence happens automatically via subscription
216
+ const newDataCache = {
217
+ data: tokenList,
218
+ timestamp: Date.now(),
219
+ };
220
+ this.update((state) => {
221
+ state.tokensChainsCache[chainId] = newDataCache;
222
+ });
223
+ return;
224
+ }
225
+ // No response - fallback to previous state, or initialise empty.
226
+ // Only initialize with a new timestamp if there's no existing cache.
227
+ // If there's existing cache, keep it as-is without updating the timestamp
228
+ // to avoid making stale data appear "fresh" and preventing retry attempts.
229
+ if (!tokensFromAPI) {
230
+ const existingCache = this.state.tokensChainsCache[chainId];
231
+ if (!existingCache) {
232
+ // No existing cache - initialize empty (persistence happens automatically)
233
+ const newDataCache = { data: {}, timestamp: Date.now() };
189
234
  this.update((state) => {
190
- var _a;
191
- const newDataCache = { data: {}, timestamp: Date.now() };
192
- (_a = state.tokensChainsCache)[chainId] ?? (_a[chainId] = newDataCache);
193
- state.tokensChainsCache[chainId].timestamp = Date.now();
235
+ state.tokensChainsCache[chainId] = newDataCache;
194
236
  });
195
237
  }
196
- }
197
- finally {
198
- releaseLock();
238
+ // If there's existing cache, keep it as-is (don't update timestamp or persist)
199
239
  }
200
240
  }
201
241
  isCacheValid(chainId) {
202
242
  const { tokensChainsCache } = this.state;
203
243
  const timestamp = tokensChainsCache[chainId]?.timestamp;
204
244
  const now = Date.now();
205
- return (timestamp !== undefined && now - timestamp < this.cacheRefreshThreshold);
245
+ return (timestamp !== undefined && now - timestamp < __classPrivateFieldGet(this, _TokenListController_cacheRefreshThreshold, "f"));
206
246
  }
207
247
  /**
208
248
  * Clearing tokenList and tokensChainsCache explicitly.
249
+ * This clears both state and all per-chain files in StorageService.
250
+ *
251
+ * Uses Promise.allSettled to handle partial failures gracefully.
252
+ * After all removal attempts complete, state is updated to match storage:
253
+ * - Successfully removed chains are cleared from state
254
+ * - Failed removals are kept in state to maintain consistency with storage
255
+ *
256
+ * Note: This method explicitly deletes from storage rather than relying on the
257
+ * stateChange subscription, since the subscription handles saves, not deletes.
209
258
  */
210
- clearingTokenListData() {
211
- this.update(() => {
212
- return {
213
- ...this.state,
214
- tokensChainsCache: {},
215
- };
216
- });
259
+ async clearingTokenListData() {
260
+ if (__classPrivateFieldGet(this, _TokenListController_persistDebounceTimer, "f")) {
261
+ clearTimeout(__classPrivateFieldGet(this, _TokenListController_persistDebounceTimer, "f"));
262
+ __classPrivateFieldSet(this, _TokenListController_persistDebounceTimer, undefined, "f");
263
+ }
264
+ __classPrivateFieldGet(this, _TokenListController_changedChainsToPersist, "f").clear();
265
+ __classPrivateFieldSet(this, _TokenListController_previousTokensChainsCache, {}, "f");
266
+ // Wait for any in-flight persist operation to complete before clearing storage.
267
+ // This prevents race conditions where persist setItem calls interleave with
268
+ // our removeItem calls, potentially re-saving data after we remove it.
269
+ if (__classPrivateFieldGet(this, _TokenListController_persistInFlightPromise, "f")) {
270
+ try {
271
+ await __classPrivateFieldGet(this, _TokenListController_persistInFlightPromise, "f");
272
+ }
273
+ catch {
274
+ // Ignore
275
+ }
276
+ }
277
+ try {
278
+ const allKeys = await this.messenger.call('StorageService:getAllKeys', name);
279
+ // Filter and remove all tokensChainsCache keys
280
+ const cacheKeys = allKeys.filter((key) => key.startsWith(`${__classPrivateFieldGet(_a, _a, "f", _TokenListController_storageKeyPrefix)}:`));
281
+ if (cacheKeys.length === 0) {
282
+ // No storage keys to remove, just clear state
283
+ this.update((state) => {
284
+ state.tokensChainsCache = {};
285
+ });
286
+ return;
287
+ }
288
+ // Use Promise.allSettled to handle partial failures gracefully.
289
+ // This ensures all removals are attempted and we can track which succeeded.
290
+ const results = await Promise.allSettled(cacheKeys.map((key) => this.messenger.call('StorageService:removeItem', name, key)));
291
+ // Identify which chains failed to be removed from storage
292
+ const failedChainIds = new Set();
293
+ results.forEach((result, index) => {
294
+ if (result.status === 'rejected') {
295
+ const key = cacheKeys[index];
296
+ const chainId = key.split(':')[1];
297
+ failedChainIds.add(chainId);
298
+ console.error(`TokenListController: Failed to remove cache for chain ${chainId}:`, result.reason);
299
+ }
300
+ });
301
+ // Update state to match storage: keep only chains that failed to be removed
302
+ this.update((state) => {
303
+ if (failedChainIds.size === 0) {
304
+ state.tokensChainsCache = {};
305
+ }
306
+ else {
307
+ // Keep only chains that failed to be removed from storage
308
+ const preservedCache = {};
309
+ for (const chainId of failedChainIds) {
310
+ if (state.tokensChainsCache[chainId]) {
311
+ preservedCache[chainId] = state.tokensChainsCache[chainId];
312
+ }
313
+ }
314
+ state.tokensChainsCache = preservedCache;
315
+ }
316
+ });
317
+ }
318
+ catch (error) {
319
+ console.error('TokenListController: Failed to clear cache from storage:', error);
320
+ // Still clear state even if storage access fails
321
+ this.update((state) => {
322
+ state.tokensChainsCache = {};
323
+ });
324
+ }
217
325
  }
218
326
  /**
219
327
  * Updates preventPollingOnNetworkRestart from extension.
@@ -230,7 +338,163 @@ class TokenListController extends (0, polling_controller_1.StaticIntervalPolling
230
338
  }
231
339
  }
232
340
  exports.TokenListController = TokenListController;
233
- _TokenListController_instances = new WeakSet(), _TokenListController_onNetworkControllerStateChange =
341
+ _a = TokenListController, _TokenListController_initializationPromise = new WeakMap(), _TokenListController_persistDebounceTimer = new WeakMap(), _TokenListController_persistInFlightPromise = new WeakMap(), _TokenListController_changedChainsToPersist = new WeakMap(), _TokenListController_previousTokensChainsCache = new WeakMap(), _TokenListController_intervalId = new WeakMap(), _TokenListController_intervalDelay = new WeakMap(), _TokenListController_cacheRefreshThreshold = new WeakMap(), _TokenListController_chainId = new WeakMap(), _TokenListController_abortController = new WeakMap(), _TokenListController_instances = new WeakSet(), _TokenListController_getChainStorageKey = function _TokenListController_getChainStorageKey(chainId) {
342
+ return `${__classPrivateFieldGet(_a, _a, "f", _TokenListController_storageKeyPrefix)}:${chainId}`;
343
+ }, _TokenListController_initializeFromStorage =
344
+ /**
345
+ * Internal method to load cache from storage and run migration.
346
+ *
347
+ * @returns A promise that resolves when initialization is complete.
348
+ */
349
+ async function _TokenListController_initializeFromStorage() {
350
+ await __classPrivateFieldGet(this, _TokenListController_instances, "m", _TokenListController_loadCacheFromStorage).call(this);
351
+ }, _TokenListController_onCacheChanged = function _TokenListController_onCacheChanged(newCache) {
352
+ // Detect which chains changed by comparing with previous cache
353
+ for (const chainId of Object.keys(newCache)) {
354
+ const newData = newCache[chainId];
355
+ const prevData = __classPrivateFieldGet(this, _TokenListController_previousTokensChainsCache, "f")[chainId];
356
+ // Chain is new or timestamp changed (indicating data update)
357
+ if (!prevData || prevData.timestamp !== newData.timestamp) {
358
+ __classPrivateFieldGet(this, _TokenListController_changedChainsToPersist, "f").add(chainId);
359
+ }
360
+ }
361
+ // Update previous cache reference
362
+ __classPrivateFieldSet(this, _TokenListController_previousTokensChainsCache, { ...newCache }, "f");
363
+ // Schedule persistence if there are changes
364
+ if (__classPrivateFieldGet(this, _TokenListController_changedChainsToPersist, "f").size > 0) {
365
+ __classPrivateFieldGet(this, _TokenListController_instances, "m", _TokenListController_debouncePersist).call(this);
366
+ }
367
+ }, _TokenListController_debouncePersist = function _TokenListController_debouncePersist() {
368
+ if (__classPrivateFieldGet(this, _TokenListController_persistDebounceTimer, "f")) {
369
+ clearTimeout(__classPrivateFieldGet(this, _TokenListController_persistDebounceTimer, "f"));
370
+ }
371
+ __classPrivateFieldSet(this, _TokenListController_persistDebounceTimer, setTimeout(() => {
372
+ // Note: #persistChangedChains handles errors internally via #saveChainCacheToStorage,
373
+ // so this promise will not reject.
374
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
375
+ __classPrivateFieldGet(this, _TokenListController_instances, "m", _TokenListController_persistChangedChains).call(this);
376
+ }, __classPrivateFieldGet(_a, _a, "f", _TokenListController_persistDebounceMs)), "f");
377
+ }, _TokenListController_persistChangedChains =
378
+ /**
379
+ * Persist only the chains that have changed to storage.
380
+ * Reduces write amplification by skipping unchanged chains.
381
+ *
382
+ * Tracks the in-flight operation via #persistInFlightPromise so that
383
+ * clearingTokenListData() can wait for it to complete before removing
384
+ * items from storage, preventing race conditions.
385
+ *
386
+ * @returns A promise that resolves when changed chains are persisted.
387
+ */
388
+ async function _TokenListController_persistChangedChains() {
389
+ const chainsToPersist = [...__classPrivateFieldGet(this, _TokenListController_changedChainsToPersist, "f")];
390
+ __classPrivateFieldGet(this, _TokenListController_changedChainsToPersist, "f").clear();
391
+ if (chainsToPersist.length === 0) {
392
+ return;
393
+ }
394
+ __classPrivateFieldSet(this, _TokenListController_persistInFlightPromise, Promise.all(chainsToPersist.map((chainId) => __classPrivateFieldGet(this, _TokenListController_instances, "m", _TokenListController_saveChainCacheToStorage).call(this, chainId))).then(() => undefined), "f"); // Convert Promise<void[]> to Promise<void>
395
+ try {
396
+ await __classPrivateFieldGet(this, _TokenListController_persistInFlightPromise, "f");
397
+ }
398
+ finally {
399
+ __classPrivateFieldSet(this, _TokenListController_persistInFlightPromise, undefined, "f");
400
+ }
401
+ }, _TokenListController_loadCacheFromStorage =
402
+ /**
403
+ * Load tokensChainsCache from StorageService into state.
404
+ * Loads all cached chains from separate per-chain files in parallel.
405
+ * Called during initialization to restore cached data.
406
+ *
407
+ * Note: This method merges loaded data with existing state to avoid
408
+ * overwriting any fresh data that may have been fetched concurrently.
409
+ * Caller must hold the mutex.
410
+ *
411
+ * @returns A promise that resolves when loading is complete.
412
+ */
413
+ async function _TokenListController_loadCacheFromStorage() {
414
+ try {
415
+ const allKeys = await this.messenger.call('StorageService:getAllKeys', name);
416
+ // Filter keys that belong to tokensChainsCache (per-chain files)
417
+ const cacheKeys = allKeys.filter((key) => key.startsWith(`${__classPrivateFieldGet(_a, _a, "f", _TokenListController_storageKeyPrefix)}:`));
418
+ if (cacheKeys.length === 0) {
419
+ return;
420
+ }
421
+ // Load all chains in parallel
422
+ const chainCaches = await Promise.all(cacheKeys.map(async (key) => {
423
+ // Extract chainId from key: 'tokensChainsCache:0x1' → '0x1'
424
+ const chainId = key.split(':')[1];
425
+ const { result, error } = await this.messenger.call('StorageService:getItem', name, key);
426
+ if (error) {
427
+ console.error(`TokenListController: Error loading cache for ${chainId}:`, error);
428
+ return null;
429
+ }
430
+ return result ? { chainId, data: result } : null;
431
+ }));
432
+ // Build complete cache from loaded chains
433
+ const loadedCache = {};
434
+ chainCaches.forEach((chainCache) => {
435
+ if (chainCache) {
436
+ loadedCache[chainCache.chainId] = chainCache.data;
437
+ }
438
+ });
439
+ // Merge loaded cache with existing state, preferring existing data
440
+ // (which may be fresher if fetched during initialization)
441
+ if (Object.keys(loadedCache).length > 0) {
442
+ this.update((state) => {
443
+ // Only load chains that don't already exist in state
444
+ // This prevents overwriting fresh API data with stale cached data
445
+ for (const [chainId, cacheData] of Object.entries(loadedCache)) {
446
+ if (!state.tokensChainsCache[chainId]) {
447
+ state.tokensChainsCache[chainId] = cacheData;
448
+ }
449
+ }
450
+ });
451
+ // Clear persistence for chains loaded from storage only.
452
+ // Data loaded from storage doesn't need to be re-persisted.
453
+ // The update() call above triggers #onCacheChanged which detects all
454
+ // loaded chains as "new" (since #previousTokensChainsCache is empty)
455
+ // and schedules them for persistence. We must clear these to avoid
456
+ // redundant storage writes on every initialization.
457
+ // However, we must NOT clear chains from initial state that were never
458
+ // persisted to storage - those still need their first persist.
459
+ if (__classPrivateFieldGet(this, _TokenListController_persistDebounceTimer, "f")) {
460
+ clearTimeout(__classPrivateFieldGet(this, _TokenListController_persistDebounceTimer, "f"));
461
+ __classPrivateFieldSet(this, _TokenListController_persistDebounceTimer, undefined, "f");
462
+ }
463
+ // Only remove loaded chains, not chains from initial state that need first persist
464
+ for (const chainId of Object.keys(loadedCache)) {
465
+ __classPrivateFieldGet(this, _TokenListController_changedChainsToPersist, "f").delete(chainId);
466
+ }
467
+ // Re-schedule persistence if there are remaining chains to persist
468
+ if (__classPrivateFieldGet(this, _TokenListController_changedChainsToPersist, "f").size > 0) {
469
+ __classPrivateFieldGet(this, _TokenListController_instances, "m", _TokenListController_debouncePersist).call(this);
470
+ }
471
+ }
472
+ }
473
+ catch (error) {
474
+ console.error('TokenListController: Failed to load cache from storage:', error);
475
+ }
476
+ }, _TokenListController_saveChainCacheToStorage =
477
+ /**
478
+ * Save a specific chain's cache to StorageService.
479
+ * This persists only the updated chain's data, reducing write amplification.
480
+ *
481
+ * @param chainId - The chain ID to save.
482
+ * @returns A promise that resolves when saving is complete.
483
+ */
484
+ async function _TokenListController_saveChainCacheToStorage(chainId) {
485
+ try {
486
+ const chainData = this.state.tokensChainsCache[chainId];
487
+ if (!chainData) {
488
+ console.warn(`TokenListController: No cache data for chain ${chainId}`);
489
+ return;
490
+ }
491
+ const storageKey = __classPrivateFieldGet(_a, _a, "m", _TokenListController_getChainStorageKey).call(_a, chainId);
492
+ await this.messenger.call('StorageService:setItem', name, storageKey, chainData);
493
+ }
494
+ catch (error) {
495
+ console.error(`TokenListController: Failed to save cache for ${chainId}:`, error);
496
+ }
497
+ }, _TokenListController_onNetworkControllerStateChange =
234
498
  /**
235
499
  * Updates state and restarts polling on changes to the network controller
236
500
  * state.
@@ -240,14 +504,19 @@ _TokenListController_instances = new WeakSet(), _TokenListController_onNetworkCo
240
504
  async function _TokenListController_onNetworkControllerStateChange(networkControllerState) {
241
505
  const selectedNetworkClient = this.messenger.call('NetworkController:getNetworkClientById', networkControllerState.selectedNetworkClientId);
242
506
  const { chainId } = selectedNetworkClient.configuration;
243
- if (this.chainId !== chainId) {
244
- this.abortController.abort();
245
- this.abortController = new AbortController();
246
- this.chainId = chainId;
507
+ if (__classPrivateFieldGet(this, _TokenListController_chainId, "f") !== chainId) {
508
+ __classPrivateFieldGet(this, _TokenListController_abortController, "f").abort();
509
+ __classPrivateFieldSet(this, _TokenListController_abortController, new AbortController(), "f");
510
+ __classPrivateFieldSet(this, _TokenListController_chainId, chainId, "f");
247
511
  if (this.state.preventPollingOnNetworkRestart) {
512
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
248
513
  this.clearingTokenListData();
249
514
  }
250
515
  }
516
+ }, _TokenListController_stopPolling = function _TokenListController_stopPolling() {
517
+ if (__classPrivateFieldGet(this, _TokenListController_intervalId, "f")) {
518
+ clearInterval(__classPrivateFieldGet(this, _TokenListController_intervalId, "f"));
519
+ }
251
520
  }, _TokenListController_startDeprecatedPolling =
252
521
  /**
253
522
  * Starts a new polling interval for a given chainId (this should be deprecated in favor of _executePoll)
@@ -257,12 +526,18 @@ async function _TokenListController_onNetworkControllerStateChange(networkContro
257
526
  */
258
527
  async function _TokenListController_startDeprecatedPolling() {
259
528
  // renaming this to avoid collision with base class
260
- await (0, controller_utils_1.safelyExecute)(() => this.fetchTokenList(this.chainId));
529
+ await (0, controller_utils_1.safelyExecute)(() => this.fetchTokenList(__classPrivateFieldGet(this, _TokenListController_chainId, "f")));
261
530
  // TODO: Either fix this lint violation or explain why it's necessary to ignore.
262
531
  // eslint-disable-next-line @typescript-eslint/no-misused-promises
263
- this.intervalId = setInterval(async () => {
264
- await (0, controller_utils_1.safelyExecute)(() => this.fetchTokenList(this.chainId));
265
- }, this.intervalDelay);
532
+ __classPrivateFieldSet(this, _TokenListController_intervalId, setInterval(async () => {
533
+ await (0, controller_utils_1.safelyExecute)(() => this.fetchTokenList(__classPrivateFieldGet(this, _TokenListController_chainId, "f")));
534
+ }, __classPrivateFieldGet(this, _TokenListController_intervalDelay, "f")), "f");
266
535
  };
536
+ /**
537
+ * Debounce delay for persisting state changes (in milliseconds).
538
+ */
539
+ _TokenListController_persistDebounceMs = { value: 500 };
540
+ // Storage key prefix for per-chain files
541
+ _TokenListController_storageKeyPrefix = { value: 'tokensChainsCache' };
267
542
  exports.default = TokenListController;
268
543
  //# sourceMappingURL=TokenListController.cjs.map