@metamask-previews/assets-controllers 96.0.0-preview-766f7065 → 98.0.0-preview-cd0d0950

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.
Files changed (54) hide show
  1. package/CHANGELOG.md +43 -1
  2. package/dist/NftDetectionController.cjs +5 -3
  3. package/dist/NftDetectionController.cjs.map +1 -1
  4. package/dist/NftDetectionController.d.cts.map +1 -1
  5. package/dist/NftDetectionController.d.mts.map +1 -1
  6. package/dist/NftDetectionController.mjs +5 -3
  7. package/dist/NftDetectionController.mjs.map +1 -1
  8. package/dist/TokenListController.cjs +375 -78
  9. package/dist/TokenListController.cjs.map +1 -1
  10. package/dist/TokenListController.d.cts +22 -17
  11. package/dist/TokenListController.d.cts.map +1 -1
  12. package/dist/TokenListController.d.mts +22 -17
  13. package/dist/TokenListController.d.mts.map +1 -1
  14. package/dist/TokenListController.mjs +375 -78
  15. package/dist/TokenListController.mjs.map +1 -1
  16. package/dist/TokenRatesController.cjs +8 -1
  17. package/dist/TokenRatesController.cjs.map +1 -1
  18. package/dist/TokenRatesController.d.cts +2 -1
  19. package/dist/TokenRatesController.d.cts.map +1 -1
  20. package/dist/TokenRatesController.d.mts +2 -1
  21. package/dist/TokenRatesController.d.mts.map +1 -1
  22. package/dist/TokenRatesController.mjs +8 -1
  23. package/dist/TokenRatesController.mjs.map +1 -1
  24. package/dist/TokenSearchDiscoveryDataController/TokenSearchDiscoveryDataController.cjs +3 -53
  25. package/dist/TokenSearchDiscoveryDataController/TokenSearchDiscoveryDataController.cjs.map +1 -1
  26. package/dist/TokenSearchDiscoveryDataController/TokenSearchDiscoveryDataController.d.cts +1 -12
  27. package/dist/TokenSearchDiscoveryDataController/TokenSearchDiscoveryDataController.d.cts.map +1 -1
  28. package/dist/TokenSearchDiscoveryDataController/TokenSearchDiscoveryDataController.d.mts +1 -12
  29. package/dist/TokenSearchDiscoveryDataController/TokenSearchDiscoveryDataController.d.mts.map +1 -1
  30. package/dist/TokenSearchDiscoveryDataController/TokenSearchDiscoveryDataController.mjs +3 -53
  31. package/dist/TokenSearchDiscoveryDataController/TokenSearchDiscoveryDataController.mjs.map +1 -1
  32. package/dist/token-prices-service/abstract-token-prices-service.cjs.map +1 -1
  33. package/dist/token-prices-service/abstract-token-prices-service.d.cts +12 -1
  34. package/dist/token-prices-service/abstract-token-prices-service.d.cts.map +1 -1
  35. package/dist/token-prices-service/abstract-token-prices-service.d.mts +12 -1
  36. package/dist/token-prices-service/abstract-token-prices-service.d.mts.map +1 -1
  37. package/dist/token-prices-service/abstract-token-prices-service.mjs.map +1 -1
  38. package/dist/token-prices-service/codefi-v2.cjs +156 -16
  39. package/dist/token-prices-service/codefi-v2.cjs.map +1 -1
  40. package/dist/token-prices-service/codefi-v2.d.cts +45 -2
  41. package/dist/token-prices-service/codefi-v2.d.cts.map +1 -1
  42. package/dist/token-prices-service/codefi-v2.d.mts +45 -2
  43. package/dist/token-prices-service/codefi-v2.d.mts.map +1 -1
  44. package/dist/token-prices-service/codefi-v2.mjs +153 -16
  45. package/dist/token-prices-service/codefi-v2.mjs.map +1 -1
  46. package/dist/token-prices-service/index.cjs +4 -1
  47. package/dist/token-prices-service/index.cjs.map +1 -1
  48. package/dist/token-prices-service/index.d.cts +2 -2
  49. package/dist/token-prices-service/index.d.cts.map +1 -1
  50. package/dist/token-prices-service/index.d.mts +2 -2
  51. package/dist/token-prices-service/index.d.mts.map +1 -1
  52. package/dist/token-prices-service/index.mjs +1 -1
  53. package/dist/token-prices-service/index.mjs.map +1 -1
  54. package/package.json +5 -3
@@ -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_persistDebounceTimer, _TokenListController_persistInFlightPromise, _TokenListController_changedChainsToPersist, _TokenListController_chainsLoadedFromStorage, _TokenListController_previousTokensChainsCache, _TokenListController_persistDebounceMs, _TokenListController_storageKeyPrefix, _TokenListController_getChainStorageKey, _TokenListController_intervalId, _TokenListController_intervalDelay, _TokenListController_cacheRefreshThreshold, _TokenListController_chainId, _TokenListController_abortController, _TokenListController_onCacheChanged, _TokenListController_debouncePersist, _TokenListController_persistChangedChains, _TokenListController_synchronizeCacheWithStorage, _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,44 @@ 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
+ * Debounce timer for persisting state changes to storage.
71
+ */
72
+ _TokenListController_persistDebounceTimer.set(this, void 0);
73
+ /**
74
+ * Promise that resolves when the current persist operation completes.
75
+ * Used to prevent race conditions between persist and clear operations.
76
+ */
77
+ _TokenListController_persistInFlightPromise.set(this, void 0);
78
+ /**
79
+ * Tracks which chains have pending changes to persist.
80
+ * Only changed chains are persisted to reduce write amplification.
81
+ */
82
+ _TokenListController_changedChainsToPersist.set(this, new Set());
83
+ /**
84
+ * Tracks chains that were just loaded from storage and should skip
85
+ * the next persistence cycle. This prevents redundant writes where
86
+ * data loaded from storage would be immediately written back.
87
+ * Chains are removed from this set after being skipped once.
88
+ */
89
+ _TokenListController_chainsLoadedFromStorage.set(this, new Set());
90
+ /**
91
+ * Previous tokensChainsCache for detecting which chains changed.
92
+ */
93
+ _TokenListController_previousTokensChainsCache.set(this, {});
94
+ _TokenListController_intervalId.set(this, void 0);
95
+ _TokenListController_intervalDelay.set(this, void 0);
96
+ _TokenListController_cacheRefreshThreshold.set(this, void 0);
97
+ _TokenListController_chainId.set(this, void 0);
98
+ _TokenListController_abortController.set(this, void 0);
99
+ __classPrivateFieldSet(this, _TokenListController_intervalDelay, interval, "f");
66
100
  this.setIntervalLength(interval);
67
- this.cacheRefreshThreshold = cacheRefreshThreshold;
68
- this.chainId = chainId;
101
+ __classPrivateFieldSet(this, _TokenListController_cacheRefreshThreshold, cacheRefreshThreshold, "f");
102
+ __classPrivateFieldSet(this, _TokenListController_chainId, chainId, "f");
69
103
  this.updatePreventPollingOnNetworkRestart(preventPollingOnNetworkRestart);
70
- this.abortController = new AbortController();
104
+ __classPrivateFieldSet(this, _TokenListController_abortController, new AbortController(), "f");
105
+ // Subscribe to state changes to automatically persist tokensChainsCache
106
+ this.messenger.subscribe('TokenListController:stateChange', (newCache) => __classPrivateFieldGet(this, _TokenListController_instances, "m", _TokenListController_onCacheChanged).call(this, newCache), (controllerState) => controllerState.tokensChainsCache);
71
107
  if (onNetworkStateChange) {
72
108
  // TODO: Either fix this lint violation or explain why it's necessary to ignore.
73
109
  // eslint-disable-next-line @typescript-eslint/no-misused-promises
@@ -84,6 +120,15 @@ class TokenListController extends (0, polling_controller_1.StaticIntervalPolling
84
120
  });
85
121
  }
86
122
  }
123
+ /**
124
+ * Initialize the controller by loading cache from storage and running migration.
125
+ * This method should be called by clients after construction.
126
+ *
127
+ * @returns A promise that resolves when initialization is complete.
128
+ */
129
+ async initialize() {
130
+ await __classPrivateFieldGet(this, _TokenListController_instances, "m", _TokenListController_synchronizeCacheWithStorage).call(this);
131
+ }
87
132
  // Eventually we want to remove start/restart/stop controls in favor of new _executePoll API
88
133
  // Maintaining these functions for now until we can safely deprecate them for backwards compatibility
89
134
  /**
@@ -93,7 +138,7 @@ class TokenListController extends (0, polling_controller_1.StaticIntervalPolling
93
138
  * Consider using the new polling approach instead
94
139
  */
95
140
  async start() {
96
- if (!(0, assetsUtil_1.isTokenListSupportedForNetwork)(this.chainId)) {
141
+ if (!(0, assetsUtil_1.isTokenListSupportedForNetwork)(__classPrivateFieldGet(this, _TokenListController_chainId, "f"))) {
97
142
  return;
98
143
  }
99
144
  await __classPrivateFieldGet(this, _TokenListController_instances, "m", _TokenListController_startDeprecatedPolling).call(this);
@@ -105,7 +150,7 @@ class TokenListController extends (0, polling_controller_1.StaticIntervalPolling
105
150
  * Consider using the new polling approach instead
106
151
  */
107
152
  async restart() {
108
- this.stopPolling();
153
+ __classPrivateFieldGet(this, _TokenListController_instances, "m", _TokenListController_stopPolling).call(this);
109
154
  await __classPrivateFieldGet(this, _TokenListController_instances, "m", _TokenListController_startDeprecatedPolling).call(this);
110
155
  }
111
156
  /**
@@ -115,7 +160,7 @@ class TokenListController extends (0, polling_controller_1.StaticIntervalPolling
115
160
  * Consider using the new polling approach instead
116
161
  */
117
162
  stop() {
118
- this.stopPolling();
163
+ __classPrivateFieldGet(this, _TokenListController_instances, "m", _TokenListController_stopPolling).call(this);
119
164
  }
120
165
  /**
121
166
  * This stops any active polling.
@@ -125,18 +170,14 @@ class TokenListController extends (0, polling_controller_1.StaticIntervalPolling
125
170
  */
126
171
  destroy() {
127
172
  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);
173
+ __classPrivateFieldGet(this, _TokenListController_instances, "m", _TokenListController_stopPolling).call(this);
174
+ // Cancel any pending debounced persistence operations
175
+ if (__classPrivateFieldGet(this, _TokenListController_persistDebounceTimer, "f")) {
176
+ clearTimeout(__classPrivateFieldGet(this, _TokenListController_persistDebounceTimer, "f"));
177
+ __classPrivateFieldSet(this, _TokenListController_persistDebounceTimer, undefined, "f");
139
178
  }
179
+ __classPrivateFieldGet(this, _TokenListController_changedChainsToPersist, "f").clear();
180
+ __classPrivateFieldGet(this, _TokenListController_chainsLoadedFromStorage, "f").clear();
140
181
  }
141
182
  /**
142
183
  * This starts a new polling loop for any given chain. Under the hood it is deduping polls
@@ -149,71 +190,146 @@ class TokenListController extends (0, polling_controller_1.StaticIntervalPolling
149
190
  return this.fetchTokenList(chainId);
150
191
  }
151
192
  /**
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)
193
+ * Fetching token list from the Token Service API. This will fetch tokens across chains.
194
+ * State changes are automatically persisted via the stateChange subscription.
153
195
  *
154
196
  * @param chainId - The chainId of the current chain triggering the fetch.
155
197
  */
156
198
  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;
199
+ if (this.isCacheValid(chainId)) {
200
+ return;
201
+ }
202
+ // Fetch fresh token list from the API
203
+ const tokensFromAPI = await (0, controller_utils_1.safelyExecute)(() => (0, token_service_1.fetchTokenListByChainId)(chainId, __classPrivateFieldGet(this, _TokenListController_abortController, "f").signal));
204
+ // Have response - process and update list
205
+ if (tokensFromAPI) {
206
+ // Format tokens from API (HTTP) and update tokenList
207
+ const tokenList = {};
208
+ for (const token of tokensFromAPI) {
209
+ tokenList[token.address] = {
210
+ ...token,
211
+ aggregators: (0, assetsUtil_1.formatAggregatorNames)(token.aggregators),
212
+ iconUrl: (0, assetsUtil_1.formatIconUrlWithProxy)({
213
+ chainId,
214
+ tokenAddress: token.address,
215
+ }),
216
+ };
186
217
  }
187
- // No response - fallback to previous state, or initialise empty
188
- if (!tokensFromAPI) {
218
+ // Update state - persistence happens automatically via subscription
219
+ const newDataCache = {
220
+ data: tokenList,
221
+ timestamp: Date.now(),
222
+ };
223
+ this.update((state) => {
224
+ state.tokensChainsCache[chainId] = newDataCache;
225
+ });
226
+ return;
227
+ }
228
+ // No response - fallback to previous state, or initialise empty.
229
+ // Only initialize with a new timestamp if there's no existing cache.
230
+ // If there's existing cache, keep it as-is without updating the timestamp
231
+ // to avoid making stale data appear "fresh" and preventing retry attempts.
232
+ if (!tokensFromAPI) {
233
+ const existingCache = this.state.tokensChainsCache[chainId];
234
+ if (!existingCache) {
235
+ // No existing cache - initialize empty (persistence happens automatically)
236
+ const newDataCache = { data: {}, timestamp: Date.now() };
189
237
  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();
238
+ state.tokensChainsCache[chainId] = newDataCache;
194
239
  });
195
240
  }
196
- }
197
- finally {
198
- releaseLock();
241
+ // If there's existing cache, keep it as-is (don't update timestamp or persist)
199
242
  }
200
243
  }
201
244
  isCacheValid(chainId) {
202
245
  const { tokensChainsCache } = this.state;
203
246
  const timestamp = tokensChainsCache[chainId]?.timestamp;
204
247
  const now = Date.now();
205
- return (timestamp !== undefined && now - timestamp < this.cacheRefreshThreshold);
248
+ return (timestamp !== undefined && now - timestamp < __classPrivateFieldGet(this, _TokenListController_cacheRefreshThreshold, "f"));
206
249
  }
207
250
  /**
208
251
  * Clearing tokenList and tokensChainsCache explicitly.
252
+ * This clears both state and all per-chain files in StorageService.
253
+ *
254
+ * Uses Promise.allSettled to handle partial failures gracefully.
255
+ * After all removal attempts complete, state is updated to match storage:
256
+ * - Successfully removed chains are cleared from state
257
+ * - Failed removals are kept in state to maintain consistency with storage
258
+ *
259
+ * Note: This method explicitly deletes from storage rather than relying on the
260
+ * stateChange subscription, since the subscription handles saves, not deletes.
209
261
  */
210
- clearingTokenListData() {
211
- this.update(() => {
212
- return {
213
- ...this.state,
214
- tokensChainsCache: {},
215
- };
216
- });
262
+ async clearingTokenListData() {
263
+ if (__classPrivateFieldGet(this, _TokenListController_persistDebounceTimer, "f")) {
264
+ clearTimeout(__classPrivateFieldGet(this, _TokenListController_persistDebounceTimer, "f"));
265
+ __classPrivateFieldSet(this, _TokenListController_persistDebounceTimer, undefined, "f");
266
+ }
267
+ __classPrivateFieldGet(this, _TokenListController_changedChainsToPersist, "f").clear();
268
+ __classPrivateFieldGet(this, _TokenListController_chainsLoadedFromStorage, "f").clear();
269
+ // Wait for any in-flight persist operation to complete before clearing storage.
270
+ // This prevents race conditions where persist setItem calls interleave with
271
+ // our removeItem calls, potentially re-saving data after we remove it.
272
+ if (__classPrivateFieldGet(this, _TokenListController_persistInFlightPromise, "f")) {
273
+ try {
274
+ await __classPrivateFieldGet(this, _TokenListController_persistInFlightPromise, "f");
275
+ }
276
+ catch {
277
+ // Ignore
278
+ }
279
+ }
280
+ try {
281
+ const allKeys = await this.messenger.call('StorageService:getAllKeys', name);
282
+ // Filter and remove all tokensChainsCache keys
283
+ const cacheKeys = allKeys.filter((key) => key.startsWith(`${__classPrivateFieldGet(_a, _a, "f", _TokenListController_storageKeyPrefix)}:`));
284
+ if (cacheKeys.length === 0) {
285
+ // No storage keys to remove, just clear state
286
+ this.update((state) => {
287
+ state.tokensChainsCache = {};
288
+ });
289
+ // Reset previous cache after state is cleared to prevent false "new chain" detections
290
+ __classPrivateFieldSet(this, _TokenListController_previousTokensChainsCache, {}, "f");
291
+ return;
292
+ }
293
+ // Use Promise.allSettled to handle partial failures gracefully.
294
+ // This ensures all removals are attempted and we can track which succeeded.
295
+ const results = await Promise.allSettled(cacheKeys.map((key) => this.messenger.call('StorageService:removeItem', name, key)));
296
+ // Identify which chains failed to be removed from storage
297
+ const failedChainIds = new Set();
298
+ results.forEach((result, index) => {
299
+ if (result.status === 'rejected') {
300
+ const key = cacheKeys[index];
301
+ const chainId = key.split(':')[1];
302
+ failedChainIds.add(chainId);
303
+ console.error(`TokenListController: Failed to remove cache for chain ${chainId}:`, result.reason);
304
+ }
305
+ });
306
+ // Update state to match storage: keep only chains that failed to be removed
307
+ this.update((state) => {
308
+ if (failedChainIds.size === 0) {
309
+ state.tokensChainsCache = {};
310
+ }
311
+ else {
312
+ // Keep only chains that failed to be removed from storage
313
+ const preservedCache = {};
314
+ for (const chainId of failedChainIds) {
315
+ if (state.tokensChainsCache[chainId]) {
316
+ preservedCache[chainId] = state.tokensChainsCache[chainId];
317
+ }
318
+ }
319
+ state.tokensChainsCache = preservedCache;
320
+ }
321
+ });
322
+ // Reset previous cache after state is cleared to prevent false "new chain" detections
323
+ __classPrivateFieldSet(this, _TokenListController_previousTokensChainsCache, { ...this.state.tokensChainsCache }, "f");
324
+ }
325
+ catch (error) {
326
+ console.error('TokenListController: Failed to clear cache from storage:', error);
327
+ // Still clear state even if storage access fails
328
+ this.update((state) => {
329
+ state.tokensChainsCache = {};
330
+ });
331
+ __classPrivateFieldSet(this, _TokenListController_previousTokensChainsCache, {}, "f");
332
+ }
217
333
  }
218
334
  /**
219
335
  * Updates preventPollingOnNetworkRestart from extension.
@@ -230,7 +346,177 @@ class TokenListController extends (0, polling_controller_1.StaticIntervalPolling
230
346
  }
231
347
  }
232
348
  exports.TokenListController = TokenListController;
233
- _TokenListController_instances = new WeakSet(), _TokenListController_onNetworkControllerStateChange =
349
+ _a = TokenListController, _TokenListController_persistDebounceTimer = new WeakMap(), _TokenListController_persistInFlightPromise = new WeakMap(), _TokenListController_changedChainsToPersist = new WeakMap(), _TokenListController_chainsLoadedFromStorage = 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) {
350
+ return `${__classPrivateFieldGet(_a, _a, "f", _TokenListController_storageKeyPrefix)}:${chainId}`;
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
+ // Skip persistence for chains that were just loaded from storage
359
+ // (they don't need to be written back immediately)
360
+ if (__classPrivateFieldGet(this, _TokenListController_chainsLoadedFromStorage, "f").has(chainId)) {
361
+ __classPrivateFieldGet(this, _TokenListController_chainsLoadedFromStorage, "f").delete(chainId); // Clean up - future updates should persist
362
+ }
363
+ else {
364
+ __classPrivateFieldGet(this, _TokenListController_changedChainsToPersist, "f").add(chainId);
365
+ }
366
+ }
367
+ }
368
+ // Update previous cache reference
369
+ __classPrivateFieldSet(this, _TokenListController_previousTokensChainsCache, { ...newCache }, "f");
370
+ // Schedule persistence if there are changes
371
+ if (__classPrivateFieldGet(this, _TokenListController_changedChainsToPersist, "f").size > 0) {
372
+ __classPrivateFieldGet(this, _TokenListController_instances, "m", _TokenListController_debouncePersist).call(this);
373
+ }
374
+ }, _TokenListController_debouncePersist = function _TokenListController_debouncePersist() {
375
+ if (__classPrivateFieldGet(this, _TokenListController_persistDebounceTimer, "f")) {
376
+ clearTimeout(__classPrivateFieldGet(this, _TokenListController_persistDebounceTimer, "f"));
377
+ }
378
+ __classPrivateFieldSet(this, _TokenListController_persistDebounceTimer, setTimeout(() => {
379
+ // Note: #persistChangedChains handles errors internally via #saveChainCacheToStorage,
380
+ // so this promise will not reject.
381
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
382
+ __classPrivateFieldGet(this, _TokenListController_instances, "m", _TokenListController_persistChangedChains).call(this);
383
+ }, __classPrivateFieldGet(_a, _a, "f", _TokenListController_persistDebounceMs)), "f");
384
+ }, _TokenListController_persistChangedChains =
385
+ /**
386
+ * Persist only the chains that have changed to storage.
387
+ * Reduces write amplification by skipping unchanged chains.
388
+ *
389
+ * Tracks the in-flight operation via #persistInFlightPromise so that
390
+ * clearingTokenListData() can wait for it to complete before removing
391
+ * items from storage, preventing race conditions.
392
+ *
393
+ * If a persist operation is already in-flight, this method returns early
394
+ * and reschedules the debounce to ensure accumulated changes are retried
395
+ * after the current operation completes.
396
+ *
397
+ * @returns A promise that resolves when changed chains are persisted.
398
+ */
399
+ async function _TokenListController_persistChangedChains() {
400
+ if (__classPrivateFieldGet(this, _TokenListController_persistInFlightPromise, "f")) {
401
+ // Reschedule debounce to retry accumulated changes after in-flight persist completes
402
+ if (__classPrivateFieldGet(this, _TokenListController_changedChainsToPersist, "f").size > 0) {
403
+ __classPrivateFieldGet(this, _TokenListController_instances, "m", _TokenListController_debouncePersist).call(this);
404
+ }
405
+ return;
406
+ }
407
+ const chainsToPersist = [...__classPrivateFieldGet(this, _TokenListController_changedChainsToPersist, "f")];
408
+ __classPrivateFieldGet(this, _TokenListController_changedChainsToPersist, "f").clear();
409
+ if (chainsToPersist.length === 0) {
410
+ return;
411
+ }
412
+ __classPrivateFieldSet(this, _TokenListController_persistInFlightPromise, Promise.all(chainsToPersist.map((chainId) => __classPrivateFieldGet(this, _TokenListController_instances, "m", _TokenListController_saveChainCacheToStorage).call(this, chainId))).then(() => undefined), "f");
413
+ try {
414
+ await __classPrivateFieldGet(this, _TokenListController_persistInFlightPromise, "f");
415
+ }
416
+ finally {
417
+ __classPrivateFieldSet(this, _TokenListController_persistInFlightPromise, undefined, "f");
418
+ }
419
+ }, _TokenListController_synchronizeCacheWithStorage =
420
+ /**
421
+ * Synchronize tokensChainsCache between state and storage bidirectionally.
422
+ *
423
+ * This method:
424
+ * 1. Loads cached chains from storage (per-chain files) in parallel
425
+ * 2. Merges loaded data into state (preferring existing state to avoid overwriting fresh data)
426
+ * 3. Persists any chains that exist in state but not in storage
427
+ *
428
+ * Called during initialization to ensure state and storage are consistent.
429
+ *
430
+ * @returns A promise that resolves when synchronization is complete.
431
+ */
432
+ async function _TokenListController_synchronizeCacheWithStorage() {
433
+ try {
434
+ const allKeys = await this.messenger.call('StorageService:getAllKeys', name);
435
+ // Filter keys that belong to tokensChainsCache (per-chain files)
436
+ const cacheKeys = allKeys.filter((key) => key.startsWith(`${__classPrivateFieldGet(_a, _a, "f", _TokenListController_storageKeyPrefix)}:`));
437
+ // Load all chains in parallel
438
+ const chainCaches = await Promise.all(cacheKeys.map(async (key) => {
439
+ // Extract chainId from key: 'tokensChainsCache:0x1' → '0x1'
440
+ const chainId = key.split(':')[1];
441
+ const { result, error } = await this.messenger.call('StorageService:getItem', name, key);
442
+ if (error) {
443
+ console.error(`TokenListController: Error loading cache for ${chainId}:`, error);
444
+ return null;
445
+ }
446
+ return result ? { chainId, data: result } : null;
447
+ }));
448
+ // Build complete cache from loaded chains
449
+ const loadedCache = {};
450
+ chainCaches.forEach((chainCache) => {
451
+ if (chainCache) {
452
+ loadedCache[chainCache.chainId] = chainCache.data;
453
+ }
454
+ });
455
+ // Merge loaded cache with existing state, preferring existing data
456
+ // (which may be fresher if fetched during initialization)
457
+ if (Object.keys(loadedCache).length > 0) {
458
+ // Track which chains we're actually loading from storage
459
+ // These will be skipped in the next #onCacheChanged to avoid redundant writes
460
+ for (const chainId of Object.keys(loadedCache)) {
461
+ if (!this.state.tokensChainsCache[chainId]) {
462
+ __classPrivateFieldGet(this, _TokenListController_chainsLoadedFromStorage, "f").add(chainId);
463
+ }
464
+ }
465
+ this.update((state) => {
466
+ // Only load chains that don't already exist in state
467
+ // This prevents overwriting fresh API data with stale cached data
468
+ for (const [chainId, cacheData] of Object.entries(loadedCache)) {
469
+ if (!state.tokensChainsCache[chainId]) {
470
+ state.tokensChainsCache[chainId] = cacheData;
471
+ }
472
+ }
473
+ });
474
+ // Note: The update() call above only triggers #onCacheChanged if chains
475
+ // were actually added to state. If initial state already contains chains
476
+ // from storage, the update() is a no-op and #onCacheChanged is never called.
477
+ }
478
+ // Persist chains that exist in state but were not loaded from storage.
479
+ // This handles the case where initial state contains chains that don't exist
480
+ // in storage yet (e.g., fresh data from API). Without this, those chains
481
+ // would be lost on the next app restart.
482
+ const loadedChainIds = new Set(Object.keys(loadedCache));
483
+ const chainsInState = new Set(Object.keys(this.state.tokensChainsCache));
484
+ for (const chainId of chainsInState) {
485
+ if (!loadedChainIds.has(chainId)) {
486
+ __classPrivateFieldGet(this, _TokenListController_changedChainsToPersist, "f").add(chainId);
487
+ }
488
+ }
489
+ // Persist any chains that need to be saved
490
+ if (__classPrivateFieldGet(this, _TokenListController_changedChainsToPersist, "f").size > 0) {
491
+ __classPrivateFieldGet(this, _TokenListController_instances, "m", _TokenListController_debouncePersist).call(this);
492
+ }
493
+ __classPrivateFieldSet(this, _TokenListController_previousTokensChainsCache, { ...this.state.tokensChainsCache }, "f");
494
+ }
495
+ catch (error) {
496
+ console.error('TokenListController: Failed to load cache from storage:', error);
497
+ }
498
+ }, _TokenListController_saveChainCacheToStorage =
499
+ /**
500
+ * Save a specific chain's cache to StorageService.
501
+ * This persists only the updated chain's data, reducing write amplification.
502
+ *
503
+ * @param chainId - The chain ID to save.
504
+ * @returns A promise that resolves when saving is complete.
505
+ */
506
+ async function _TokenListController_saveChainCacheToStorage(chainId) {
507
+ try {
508
+ const chainData = this.state.tokensChainsCache[chainId];
509
+ if (!chainData) {
510
+ console.warn(`TokenListController: No cache data for chain ${chainId}`);
511
+ return;
512
+ }
513
+ const storageKey = __classPrivateFieldGet(_a, _a, "m", _TokenListController_getChainStorageKey).call(_a, chainId);
514
+ await this.messenger.call('StorageService:setItem', name, storageKey, chainData);
515
+ }
516
+ catch (error) {
517
+ console.error(`TokenListController: Failed to save cache for ${chainId}:`, error);
518
+ }
519
+ }, _TokenListController_onNetworkControllerStateChange =
234
520
  /**
235
521
  * Updates state and restarts polling on changes to the network controller
236
522
  * state.
@@ -240,14 +526,19 @@ _TokenListController_instances = new WeakSet(), _TokenListController_onNetworkCo
240
526
  async function _TokenListController_onNetworkControllerStateChange(networkControllerState) {
241
527
  const selectedNetworkClient = this.messenger.call('NetworkController:getNetworkClientById', networkControllerState.selectedNetworkClientId);
242
528
  const { chainId } = selectedNetworkClient.configuration;
243
- if (this.chainId !== chainId) {
244
- this.abortController.abort();
245
- this.abortController = new AbortController();
246
- this.chainId = chainId;
529
+ if (__classPrivateFieldGet(this, _TokenListController_chainId, "f") !== chainId) {
530
+ __classPrivateFieldGet(this, _TokenListController_abortController, "f").abort();
531
+ __classPrivateFieldSet(this, _TokenListController_abortController, new AbortController(), "f");
532
+ __classPrivateFieldSet(this, _TokenListController_chainId, chainId, "f");
247
533
  if (this.state.preventPollingOnNetworkRestart) {
534
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
248
535
  this.clearingTokenListData();
249
536
  }
250
537
  }
538
+ }, _TokenListController_stopPolling = function _TokenListController_stopPolling() {
539
+ if (__classPrivateFieldGet(this, _TokenListController_intervalId, "f")) {
540
+ clearInterval(__classPrivateFieldGet(this, _TokenListController_intervalId, "f"));
541
+ }
251
542
  }, _TokenListController_startDeprecatedPolling =
252
543
  /**
253
544
  * Starts a new polling interval for a given chainId (this should be deprecated in favor of _executePoll)
@@ -257,12 +548,18 @@ async function _TokenListController_onNetworkControllerStateChange(networkContro
257
548
  */
258
549
  async function _TokenListController_startDeprecatedPolling() {
259
550
  // renaming this to avoid collision with base class
260
- await (0, controller_utils_1.safelyExecute)(() => this.fetchTokenList(this.chainId));
551
+ await (0, controller_utils_1.safelyExecute)(() => this.fetchTokenList(__classPrivateFieldGet(this, _TokenListController_chainId, "f")));
261
552
  // TODO: Either fix this lint violation or explain why it's necessary to ignore.
262
553
  // 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);
554
+ __classPrivateFieldSet(this, _TokenListController_intervalId, setInterval(async () => {
555
+ await (0, controller_utils_1.safelyExecute)(() => this.fetchTokenList(__classPrivateFieldGet(this, _TokenListController_chainId, "f")));
556
+ }, __classPrivateFieldGet(this, _TokenListController_intervalDelay, "f")), "f");
266
557
  };
558
+ /**
559
+ * Debounce delay for persisting state changes (in milliseconds).
560
+ */
561
+ _TokenListController_persistDebounceMs = { value: 500 };
562
+ // Storage key prefix for per-chain files
563
+ _TokenListController_storageKeyPrefix = { value: 'tokensChainsCache' };
267
564
  exports.default = TokenListController;
268
565
  //# sourceMappingURL=TokenListController.cjs.map