@@ -146,6 +146,8 @@ export class TokenRatesController extends PollingControllerV1<
146
146
147
147
#tokenPricesService: AbstractTokenPricesService ;
148
148
149
+ #inProcessExchangeRateUpdates: Record < `${Hex } :${string } `, Promise < void > > = { } ;
150
+
149
151
/**
150
152
* Name of this controller used during composition
151
153
*/
@@ -360,36 +362,60 @@ export class TokenRatesController extends PollingControllerV1<
360
362
return ;
361
363
}
362
364
363
- const newContractExchangeRates = await this . #fetchAndMapExchangeRates( {
364
- tokenContractAddresses,
365
- chainId,
366
- nativeCurrency,
367
- } ) ;
365
+ const updateKey : `${Hex } :${string } ` = `${ chainId } :${ nativeCurrency } ` ;
366
+ if ( updateKey in this . #inProcessExchangeRateUpdates) {
367
+ // This prevents redundant updates
368
+ // This promise is resolved after the in-progress update has finished,
369
+ // and state has been updated.
370
+ await this . #inProcessExchangeRateUpdates[ updateKey ] ;
371
+ return ;
372
+ }
373
+
374
+ const {
375
+ promise : inProgressUpdate ,
376
+ resolve : updateSucceeded ,
377
+ reject : updateFailed ,
378
+ } = createDeferredPromise ( { suppressUnhandledRejection : true } ) ;
379
+ this . #inProcessExchangeRateUpdates[ updateKey ] = inProgressUpdate ;
380
+
381
+ try {
382
+ const newContractExchangeRates = await this . #fetchAndMapExchangeRates( {
383
+ tokenContractAddresses,
384
+ chainId,
385
+ nativeCurrency,
386
+ } ) ;
368
387
369
- const existingContractExchangeRates = this . state . contractExchangeRates ;
370
- const updatedContractExchangeRates =
371
- chainId === this . config . chainId &&
372
- nativeCurrency === this . config . nativeCurrency
373
- ? newContractExchangeRates
374
- : existingContractExchangeRates ;
375
-
376
- const existingContractExchangeRatesForChainId =
377
- this . state . contractExchangeRatesByChainId [ chainId ] ?? { } ;
378
- const updatedContractExchangeRatesForChainId = {
379
- ...this . state . contractExchangeRatesByChainId ,
380
- [ chainId ] : {
381
- ...existingContractExchangeRatesForChainId ,
382
- [ nativeCurrency ] : {
383
- ...existingContractExchangeRatesForChainId [ nativeCurrency ] ,
384
- ...newContractExchangeRates ,
388
+ const existingContractExchangeRates = this . state . contractExchangeRates ;
389
+ const updatedContractExchangeRates =
390
+ chainId === this . config . chainId &&
391
+ nativeCurrency === this . config . nativeCurrency
392
+ ? newContractExchangeRates
393
+ : existingContractExchangeRates ;
394
+
395
+ const existingContractExchangeRatesForChainId =
396
+ this . state . contractExchangeRatesByChainId [ chainId ] ?? { } ;
397
+ const updatedContractExchangeRatesForChainId = {
398
+ ...this . state . contractExchangeRatesByChainId ,
399
+ [ chainId ] : {
400
+ ...existingContractExchangeRatesForChainId ,
401
+ [ nativeCurrency ] : {
402
+ ...existingContractExchangeRatesForChainId [ nativeCurrency ] ,
403
+ ...newContractExchangeRates ,
404
+ } ,
385
405
} ,
386
- } ,
387
- } ;
406
+ } ;
388
407
389
- this . update ( {
390
- contractExchangeRates : updatedContractExchangeRates ,
391
- contractExchangeRatesByChainId : updatedContractExchangeRatesForChainId ,
392
- } ) ;
408
+ this . update ( {
409
+ contractExchangeRates : updatedContractExchangeRates ,
410
+ contractExchangeRatesByChainId : updatedContractExchangeRatesForChainId ,
411
+ } ) ;
412
+ updateSucceeded ( ) ;
413
+ } catch ( error : unknown ) {
414
+ updateFailed ( error ) ;
415
+ throw error ;
416
+ } finally {
417
+ delete this . #inProcessExchangeRateUpdates[ updateKey ] ;
418
+ }
393
419
}
394
420
395
421
/**
@@ -548,4 +574,60 @@ export class TokenRatesController extends PollingControllerV1<
548
574
}
549
575
}
550
576
577
+ /**
578
+ * A deferred Promise.
579
+ *
580
+ * A deferred Promise is one that can be resolved or rejected independently of
581
+ * the Promise construction.
582
+ */
583
+ type DeferredPromise = {
584
+ /**
585
+ * The Promise that has been deferred.
586
+ */
587
+ promise : Promise < void > ;
588
+ /**
589
+ * A function that resolves the Promise.
590
+ */
591
+ resolve : ( ) => void ;
592
+ /**
593
+ * A function that rejects the Promise.
594
+ */
595
+ reject : ( error : unknown ) => void ;
596
+ } ;
597
+
598
+ /**
599
+ * Create a defered Promise.
600
+ *
601
+ * TODO: Migrate this to utils
602
+ *
603
+ * @param args - The arguments.
604
+ * @param args.suppressUnhandledRejection - This option adds an empty error handler
605
+ * to the Promise to suppress the UnhandledPromiseRejection error. This can be
606
+ * useful if the deferred Promise is sometimes intentionally not used.
607
+ * @returns A deferred Promise.
608
+ */
609
+ function createDeferredPromise ( {
610
+ suppressUnhandledRejection = false ,
611
+ } : {
612
+ suppressUnhandledRejection : boolean ;
613
+ } ) : DeferredPromise {
614
+ let resolve : DeferredPromise [ 'resolve' ] ;
615
+ let reject : DeferredPromise [ 'reject' ] ;
616
+ const promise = new Promise < void > (
617
+ ( innerResolve : ( ) => void , innerReject : ( ) => void ) => {
618
+ resolve = innerResolve ;
619
+ reject = innerReject ;
620
+ } ,
621
+ ) ;
622
+
623
+ if ( suppressUnhandledRejection ) {
624
+ promise . catch ( ( _error ) => {
625
+ // This handler is used to suppress the UnhandledPromiseRejection error
626
+ } ) ;
627
+ }
628
+
629
+ // @ts -expect-error We know that these are assigned, but TypeScript doesn't
630
+ return { promise, resolve, reject } ;
631
+ }
632
+
551
633
export default TokenRatesController ;
0 commit comments