From acdd9fae4d059756e8af4df2b153f880297ef0dd Mon Sep 17 00:00:00 2001 From: Elliot Winkler Date: Fri, 24 Jan 2025 07:52:15 -0700 Subject: [PATCH] Refactor services to align with eventual RpcService (#5109) In a future commit, we are adding an RpcService class to `network-controller` to handle automatic failovers. This class is different from the existing service classes in a couple of ways: - RpcService will make use of a new `createServicePolicy` function which encapsulates retry and circuit breaker logic. This will relieve any module that consumes RpcService from needing to test this logic. - RpcService will have `onBreak` and `onDegraded` methods instead of taking `onBreak` and `onDegraded` callbacks as constructor options. This is reflected not only in the class but also in the abstract RPC service interface. To these ends, this commit makes the following changes to `CodefiTokenPricesServiceV2` in `assets-controllers` and `ClientConfigApiService` in `remote-feature-flag-controller`: - Refactor service class to use `createServicePolicy` - Update the abstract service class interface to extend `ServicePolicy` from `controller-utils` - Deprecate the `onBreak` and `onDegraded` constructor options, and add methods instead Note that we are not including `TokenSearchApiService` in this commit, even though it is a class, because it does not use the retry or circuit breaker policy. --- eslint-warning-thresholds.json | 2 +- packages/assets-controllers/CHANGELOG.md | 11 + packages/assets-controllers/package.json | 1 - .../src/TokenRatesController.test.ts | 16 +- .../src/TokenRatesController.ts | 2 +- .../abstract-token-prices-service.ts | 3 +- .../token-prices-service/codefi-v2.test.ts | 1164 ++++------------- .../src/token-prices-service/codefi-v2.ts | 147 ++- .../CHANGELOG.md | 11 + .../package.json | 3 +- .../abstract-client-config-api-service.ts | 19 +- .../client-config-api-service.test.ts | 157 +-- .../client-config-api-service.ts | 163 ++- .../src/remote-feature-flag-controller.ts | 6 +- yarn.lock | 2 - 15 files changed, 543 insertions(+), 1164 deletions(-) diff --git a/eslint-warning-thresholds.json b/eslint-warning-thresholds.json index 9d3a9d433a5..e1358ad4b77 100644 --- a/eslint-warning-thresholds.json +++ b/eslint-warning-thresholds.json @@ -5,7 +5,7 @@ "@typescript-eslint/no-unsafe-enum-comparison": 34, "@typescript-eslint/no-unused-vars": 41, "@typescript-eslint/prefer-promise-reject-errors": 33, - "@typescript-eslint/prefer-readonly": 147, + "@typescript-eslint/prefer-readonly": 143, "import-x/namespace": 189, "import-x/no-named-as-default": 1, "import-x/no-named-as-default-member": 8, diff --git a/packages/assets-controllers/CHANGELOG.md b/packages/assets-controllers/CHANGELOG.md index 4a62573da5b..ada9f939fa5 100644 --- a/packages/assets-controllers/CHANGELOG.md +++ b/packages/assets-controllers/CHANGELOG.md @@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Add `onBreak` and `onDegraded` methods to `CodefiTokenPricesServiceV2` ([#5109](https://github.com/MetaMask/core/pull/5109)) + - These serve the same purpose as the `onBreak` and `onDegraded` constructor options, but align more closely with the Cockatiel policy API. + +### Changed + +- Deprecate `ClientConfigApiService` constructor options `onBreak` and `onDegraded` in favor of methods ([#5109](https://github.com/MetaMask/core/pull/5109)) +- Add `@metamask/controller-utils@^11.4.5` as a dependency ([#5109](https://github.com/MetaMask/core/pull/5109)) + - `cockatiel` should still be in the dependency tree because it's now a dependency of `@metamask/controller-utils` + ## [46.0.1] ### Changed diff --git a/packages/assets-controllers/package.json b/packages/assets-controllers/package.json index acde5f8d16a..0a70808053a 100644 --- a/packages/assets-controllers/package.json +++ b/packages/assets-controllers/package.json @@ -68,7 +68,6 @@ "async-mutex": "^0.5.0", "bitcoin-address-validation": "^2.2.3", "bn.js": "^5.2.1", - "cockatiel": "^3.1.2", "immer": "^9.0.6", "lodash": "^4.17.21", "multiformats": "^13.1.0", diff --git a/packages/assets-controllers/src/TokenRatesController.test.ts b/packages/assets-controllers/src/TokenRatesController.test.ts index db41deddd41..ee01c088e3d 100644 --- a/packages/assets-controllers/src/TokenRatesController.test.ts +++ b/packages/assets-controllers/src/TokenRatesController.test.ts @@ -2093,11 +2093,9 @@ describe('TokenRatesController', () => { price: 0.002, }, }), - validateCurrencySupported: jest.fn().mockReturnValue( - false, - // Cast used because this method has an assertion in the return - // value that I don't know how to type properly with Jest's mock. - ) as unknown as AbstractTokenPricesService['validateCurrencySupported'], + validateCurrencySupported(_currency: unknown): _currency is string { + return false; + }, }); nock('https://min-api.cryptocompare.com') .get('/data/price') @@ -2288,11 +2286,9 @@ describe('TokenRatesController', () => { value: 0.002, }, }), - validateChainIdSupported: jest.fn().mockReturnValue( - false, - // Cast used because this method has an assertion in the return - // value that I don't know how to type properly with Jest's mock. - ) as unknown as AbstractTokenPricesService['validateChainIdSupported'], + validateChainIdSupported(_chainId: unknown): _chainId is Hex { + return false; + }, }); await withController( { diff --git a/packages/assets-controllers/src/TokenRatesController.ts b/packages/assets-controllers/src/TokenRatesController.ts index f25702bbf82..61f569ee963 100644 --- a/packages/assets-controllers/src/TokenRatesController.ts +++ b/packages/assets-controllers/src/TokenRatesController.ts @@ -238,7 +238,7 @@ export class TokenRatesController extends StaticIntervalPollingController> = {}; diff --git a/packages/assets-controllers/src/token-prices-service/abstract-token-prices-service.ts b/packages/assets-controllers/src/token-prices-service/abstract-token-prices-service.ts index a3cd09a0586..7e705a33ab6 100644 --- a/packages/assets-controllers/src/token-prices-service/abstract-token-prices-service.ts +++ b/packages/assets-controllers/src/token-prices-service/abstract-token-prices-service.ts @@ -1,3 +1,4 @@ +import type { ServicePolicy } from '@metamask/controller-utils'; import type { Hex } from '@metamask/utils'; /** @@ -53,7 +54,7 @@ export type AbstractTokenPricesService< ChainId extends Hex = Hex, TokenAddress extends Hex = Hex, Currency extends string = string, -> = { +> = Partial> & { /** * Retrieves prices in the given currency for the tokens identified by the * given addresses which are expected to live on the given chain. diff --git a/packages/assets-controllers/src/token-prices-service/codefi-v2.test.ts b/packages/assets-controllers/src/token-prices-service/codefi-v2.test.ts index e1efe858ec2..8f644e7e02e 100644 --- a/packages/assets-controllers/src/token-prices-service/codefi-v2.test.ts +++ b/packages/assets-controllers/src/token-prices-service/codefi-v2.test.ts @@ -14,6 +14,232 @@ import { const defaultMaxRetryDelay = 30_000; describe('CodefiTokenPricesServiceV2', () => { + describe('onBreak', () => { + let clock: sinon.SinonFakeTimers; + + beforeEach(() => { + clock = useFakeTimers({ now: Date.now() }); + }); + + afterEach(() => { + clock.restore(); + }); + + it('registers a listener that is called upon break', async () => { + const retries = 3; + // Max consencutive failures is set to match number of calls in three update attempts (including retries) + const maximumConsecutiveFailures = (1 + retries) * 3; + // Initial interceptor for failing requests + nock('https://price.api.cx.metamask.io') + .get('/v2/chains/1/spot-prices') + .query({ + tokenAddresses: + '0x0000000000000000000000000000000000000000,0xAAA,0xBBB,0xCCC', + vsCurrency: 'ETH', + includeMarketData: 'true', + }) + .times(maximumConsecutiveFailures) + .replyWithError('Failed to fetch'); + // This interceptor should not be used + nock('https://price.api.cx.metamask.io') + .get('/v2/chains/1/spot-prices') + .query({ + tokenAddresses: + '0x0000000000000000000000000000000000000000,0xAAA,0xBBB,0xCCC', + vsCurrency: 'ETH', + includeMarketData: 'true', + }) + .reply(200, { + '0x0000000000000000000000000000000000000000': { + price: 14, + currency: 'ETH', + pricePercentChange1d: 1, + priceChange1d: 1, + marketCap: 117219.99428314982, + allTimeHigh: 0.00060467892389492, + allTimeLow: 0.00002303954000865728, + totalVolume: 5155.094053542448, + high1d: 0.00008020715848194385, + low1d: 0.00007792083564549064, + circulatingSupply: 1494269733.9526057, + dilutedMarketCap: 117669.5125951733, + marketCapPercentChange1d: 0.76671, + pricePercentChange1h: -1.0736342953259423, + pricePercentChange7d: -7.351582573655089, + pricePercentChange14d: -1.0799098946709822, + pricePercentChange30d: -25.776321124365992, + pricePercentChange200d: 46.091571238599165, + pricePercentChange1y: -2.2992517267242754, + }, + '0xaaa': { + price: 148.17205755299946, + currency: 'ETH', + pricePercentChange1d: 1, + priceChange1d: 1, + marketCap: 117219.99428314982, + allTimeHigh: 0.00060467892389492, + allTimeLow: 0.00002303954000865728, + totalVolume: 5155.094053542448, + high1d: 0.00008020715848194385, + low1d: 0.00007792083564549064, + circulatingSupply: 1494269733.9526057, + dilutedMarketCap: 117669.5125951733, + marketCapPercentChange1d: 0.76671, + pricePercentChange1h: -1.0736342953259423, + pricePercentChange7d: -7.351582573655089, + pricePercentChange14d: -1.0799098946709822, + pricePercentChange30d: -25.776321124365992, + pricePercentChange200d: 46.091571238599165, + pricePercentChange1y: -2.2992517267242754, + }, + '0xbbb': { + price: 33689.98134554716, + currency: 'ETH', + pricePercentChange1d: 1, + priceChange1d: 1, + marketCap: 117219.99428314982, + allTimeHigh: 0.00060467892389492, + allTimeLow: 0.00002303954000865728, + totalVolume: 5155.094053542448, + high1d: 0.00008020715848194385, + low1d: 0.00007792083564549064, + circulatingSupply: 1494269733.9526057, + dilutedMarketCap: 117669.5125951733, + marketCapPercentChange1d: 0.76671, + pricePercentChange1h: -1.0736342953259423, + pricePercentChange7d: -7.351582573655089, + pricePercentChange14d: -1.0799098946709822, + pricePercentChange30d: -25.776321124365992, + pricePercentChange200d: 46.091571238599165, + pricePercentChange1y: -2.2992517267242754, + }, + '0xccc': { + price: 148.1344197578456, + currency: 'ETH', + pricePercentChange1d: 1, + priceChange1d: 1, + marketCap: 117219.99428314982, + allTimeHigh: 0.00060467892389492, + allTimeLow: 0.00002303954000865728, + totalVolume: 5155.094053542448, + high1d: 0.00008020715848194385, + low1d: 0.00007792083564549064, + circulatingSupply: 1494269733.9526057, + dilutedMarketCap: 117669.5125951733, + marketCapPercentChange1d: 0.76671, + pricePercentChange1h: -1.0736342953259423, + pricePercentChange7d: -7.351582573655089, + pricePercentChange14d: -1.0799098946709822, + pricePercentChange30d: -25.776321124365992, + pricePercentChange200d: 46.091571238599165, + pricePercentChange1y: -2.2992517267242754, + }, + }); + const onBreakHandler = jest.fn(); + const service = new CodefiTokenPricesServiceV2({ + retries, + maximumConsecutiveFailures, + // Ensure break duration is well over the max delay for a single request, so that the + // break doesn't end during a retry attempt + circuitBreakDuration: defaultMaxRetryDelay * 10, + }); + service.onBreak(onBreakHandler); + const fetchTokenPrices = () => + service.fetchTokenPrices({ + chainId: '0x1', + tokenAddresses: ['0xAAA', '0xBBB', '0xCCC'], + currency: 'ETH', + }); + expect(onBreakHandler).not.toHaveBeenCalled(); + + // Initial three calls to exhaust maximum allowed failures + // eslint-disable-next-line @typescript-eslint/no-unused-vars + for (const _retryAttempt of Array(retries).keys()) { + // eslint-disable-next-line no-loop-func + await expect(() => + fetchTokenPricesWithFakeTimers({ + clock, + fetchTokenPrices, + retries, + }), + ).rejects.toThrow('Failed to fetch'); + } + + expect(onBreakHandler).toHaveBeenCalledTimes(1); + }); + }); + + describe('onDegraded', () => { + let clock: sinon.SinonFakeTimers; + + beforeEach(() => { + clock = useFakeTimers({ now: Date.now() }); + }); + + afterEach(() => { + clock.restore(); + }); + + it('calls onDegraded when request is slower than threshold', async () => { + const degradedThreshold = 1000; + const retries = 0; + nock('https://price.api.cx.metamask.io') + .get('/v2/chains/1/spot-prices') + .query({ + tokenAddresses: + '0x0000000000000000000000000000000000000000,0xAAA,0xBBB,0xCCC', + vsCurrency: 'ETH', + includeMarketData: 'true', + }) + .delay(degradedThreshold * 2) + .reply(200, { + '0x0000000000000000000000000000000000000000': { + price: 14, + currency: 'ETH', + pricePercentChange1d: 1, + priceChange1d: 1, + }, + '0xaaa': { + price: 148.17205755299946, + currency: 'ETH', + pricePercentChange1d: 1, + priceChange1d: 1, + }, + '0xbbb': { + price: 33689.98134554716, + currency: 'ETH', + pricePercentChange1d: 1, + priceChange1d: 1, + }, + '0xccc': { + price: 148.1344197578456, + currency: 'ETH', + pricePercentChange1d: 1, + priceChange1d: 1, + }, + }); + const onDegradedHandler = jest.fn(); + const service = new CodefiTokenPricesServiceV2({ + degradedThreshold, + retries, + }); + service.onDegraded(onDegradedHandler); + + await fetchTokenPricesWithFakeTimers({ + clock, + fetchTokenPrices: () => + service.fetchTokenPrices({ + chainId: '0x1', + tokenAddresses: ['0xAAA', '0xBBB', '0xCCC'], + currency: 'ETH', + }), + retries, + }); + + expect(onDegradedHandler).toHaveBeenCalledTimes(1); + }); + }); + describe('fetchTokenPrices', () => { it('uses the /spot-prices endpoint of the Codefi Price API to gather prices for the given tokens', async () => { nock('https://price.api.cx.metamask.io') @@ -829,8 +1055,9 @@ describe('CodefiTokenPricesServiceV2', () => { clock.restore(); }); - it('does not call onDegraded when requests succeeds faster than threshold', async () => { + it('calls onDegraded when request is slower than threshold', async () => { const degradedThreshold = 1000; + const retries = 0; nock('https://price.api.cx.metamask.io') .get('/v2/chains/1/spot-prices') .query({ @@ -839,326 +1066,38 @@ describe('CodefiTokenPricesServiceV2', () => { vsCurrency: 'ETH', includeMarketData: 'true', }) - .delay(degradedThreshold / 2) + .delay(degradedThreshold * 2) .reply(200, { '0x0000000000000000000000000000000000000000': { price: 14, currency: 'ETH', pricePercentChange1d: 1, priceChange1d: 1, - marketCap: 117219.99428314982, - allTimeHigh: 0.00060467892389492, - allTimeLow: 0.00002303954000865728, - totalVolume: 5155.094053542448, - high1d: 0.00008020715848194385, - low1d: 0.00007792083564549064, - circulatingSupply: 1494269733.9526057, - dilutedMarketCap: 117669.5125951733, - marketCapPercentChange1d: 0.76671, - pricePercentChange1h: -1.0736342953259423, - pricePercentChange7d: -7.351582573655089, - pricePercentChange14d: -1.0799098946709822, - pricePercentChange30d: -25.776321124365992, - pricePercentChange200d: 46.091571238599165, - pricePercentChange1y: -2.2992517267242754, }, '0xaaa': { price: 148.17205755299946, currency: 'ETH', pricePercentChange1d: 1, priceChange1d: 1, - marketCap: 117219.99428314982, - allTimeHigh: 0.00060467892389492, - allTimeLow: 0.00002303954000865728, - totalVolume: 5155.094053542448, - high1d: 0.00008020715848194385, - low1d: 0.00007792083564549064, - circulatingSupply: 1494269733.9526057, - dilutedMarketCap: 117669.5125951733, - marketCapPercentChange1d: 0.76671, - pricePercentChange1h: -1.0736342953259423, - pricePercentChange7d: -7.351582573655089, - pricePercentChange14d: -1.0799098946709822, - pricePercentChange30d: -25.776321124365992, - pricePercentChange200d: 46.091571238599165, - pricePercentChange1y: -2.2992517267242754, }, '0xbbb': { price: 33689.98134554716, currency: 'ETH', pricePercentChange1d: 1, priceChange1d: 1, - marketCap: 117219.99428314982, - allTimeHigh: 0.00060467892389492, - allTimeLow: 0.00002303954000865728, - totalVolume: 5155.094053542448, - high1d: 0.00008020715848194385, - low1d: 0.00007792083564549064, - circulatingSupply: 1494269733.9526057, - dilutedMarketCap: 117669.5125951733, - marketCapPercentChange1d: 0.76671, - pricePercentChange1h: -1.0736342953259423, - pricePercentChange7d: -7.351582573655089, - pricePercentChange14d: -1.0799098946709822, - pricePercentChange30d: -25.776321124365992, - pricePercentChange200d: 46.091571238599165, - pricePercentChange1y: -2.2992517267242754, }, '0xccc': { price: 148.1344197578456, currency: 'ETH', pricePercentChange1d: 1, priceChange1d: 1, - marketCap: 117219.99428314982, - allTimeHigh: 0.00060467892389492, - allTimeLow: 0.00002303954000865728, - totalVolume: 5155.094053542448, - high1d: 0.00008020715848194385, - low1d: 0.00007792083564549064, - circulatingSupply: 1494269733.9526057, - dilutedMarketCap: 117669.5125951733, - marketCapPercentChange1d: 0.76671, - pricePercentChange1h: -1.0736342953259423, - pricePercentChange7d: -7.351582573655089, - pricePercentChange14d: -1.0799098946709822, - pricePercentChange30d: -25.776321124365992, - pricePercentChange200d: 46.091571238599165, - pricePercentChange1y: -2.2992517267242754, }, }); const onDegradedHandler = jest.fn(); const service = new CodefiTokenPricesServiceV2({ degradedThreshold, onDegraded: onDegradedHandler, - }); - - await service.fetchTokenPrices({ - chainId: '0x1', - tokenAddresses: ['0xAAA', '0xBBB', '0xCCC'], - currency: 'ETH', - }); - - expect(onDegradedHandler).not.toHaveBeenCalled(); - }); - - it('does not call onDegraded when requests succeeds on retry faster than threshold', async () => { - // Set threshold above max retry delay to ensure the time is always under the threshold, - // even with random jitter - const degradedThreshold = defaultMaxRetryDelay + 1000; - const retries = 1; - // Initial interceptor for failing request - nock('https://price.api.cx.metamask.io') - .get('/v2/chains/1/spot-prices') - .query({ - tokenAddresses: - '0x0000000000000000000000000000000000000000,0xAAA,0xBBB,0xCCC', - vsCurrency: 'ETH', - includeMarketData: 'true', - }) - .replyWithError('Failed to fetch'); - // Second interceptor for successful response - nock('https://price.api.cx.metamask.io') - .get('/v2/chains/1/spot-prices') - .query({ - tokenAddresses: - '0x0000000000000000000000000000000000000000,0xAAA,0xBBB,0xCCC', - vsCurrency: 'ETH', - includeMarketData: 'true', - }) - .delay(500) - .reply(200, { - '0x0000000000000000000000000000000000000000': { - price: 14, - currency: 'ETH', - pricePercentChange1d: 1, - priceChange1d: 1, - }, - '0xaaa': { - price: 148.17205755299946, - currency: 'ETH', - pricePercentChange1d: 1, - priceChange1d: 1, - }, - '0xbbb': { - price: 33689.98134554716, - currency: 'ETH', - pricePercentChange1d: 1, - priceChange1d: 1, - }, - '0xccc': { - price: 148.1344197578456, - currency: 'ETH', - pricePercentChange1d: 1, - priceChange1d: 1, - }, - }); - const onDegradedHandler = jest.fn(); - const service = new CodefiTokenPricesServiceV2({ - degradedThreshold, - onDegraded: onDegradedHandler, - retries, - }); - - await fetchTokenPricesWithFakeTimers({ - clock, - fetchTokenPrices: () => - service.fetchTokenPrices({ - chainId: '0x1', - tokenAddresses: ['0xAAA', '0xBBB', '0xCCC'], - currency: 'ETH', - }), - retries, - }); - - expect(onDegradedHandler).not.toHaveBeenCalled(); - }); - - it('calls onDegraded when request fails', async () => { - const retries = 0; - nock('https://price.api.cx.metamask.io') - .get('/v2/chains/1/spot-prices') - .query({ - tokenAddresses: - '0x0000000000000000000000000000000000000000,0xAAA,0xBBB,0xCCC', - vsCurrency: 'ETH', - includeMarketData: 'true', - }) - .replyWithError('Failed to fetch'); - const onDegradedHandler = jest.fn(); - const service = new CodefiTokenPricesServiceV2({ - onDegraded: onDegradedHandler, - retries, - }); - - await expect(() => - fetchTokenPricesWithFakeTimers({ - clock, - fetchTokenPrices: () => - service.fetchTokenPrices({ - chainId: '0x1', - tokenAddresses: ['0xAAA', '0xBBB', '0xCCC'], - currency: 'ETH', - }), - retries, - }), - ).rejects.toThrow('Failed to fetch'); - - expect(onDegradedHandler).toHaveBeenCalledTimes(1); - }); - - it('calls onDegraded when request is slower than threshold', async () => { - const degradedThreshold = 1000; - const retries = 0; - nock('https://price.api.cx.metamask.io') - .get('/v2/chains/1/spot-prices') - .query({ - tokenAddresses: - '0x0000000000000000000000000000000000000000,0xAAA,0xBBB,0xCCC', - vsCurrency: 'ETH', - includeMarketData: 'true', - }) - .delay(degradedThreshold * 2) - .reply(200, { - '0x0000000000000000000000000000000000000000': { - price: 14, - currency: 'ETH', - pricePercentChange1d: 1, - priceChange1d: 1, - }, - '0xaaa': { - price: 148.17205755299946, - currency: 'ETH', - pricePercentChange1d: 1, - priceChange1d: 1, - }, - '0xbbb': { - price: 33689.98134554716, - currency: 'ETH', - pricePercentChange1d: 1, - priceChange1d: 1, - }, - '0xccc': { - price: 148.1344197578456, - currency: 'ETH', - pricePercentChange1d: 1, - priceChange1d: 1, - }, - }); - const onDegradedHandler = jest.fn(); - const service = new CodefiTokenPricesServiceV2({ - degradedThreshold, - onDegraded: onDegradedHandler, - retries, - }); - - await fetchTokenPricesWithFakeTimers({ - clock, - fetchTokenPrices: () => - service.fetchTokenPrices({ - chainId: '0x1', - tokenAddresses: ['0xAAA', '0xBBB', '0xCCC'], - currency: 'ETH', - }), - retries, - }); - - expect(onDegradedHandler).toHaveBeenCalledTimes(1); - }); - - it('calls onDegraded when request is slower than threshold after retry', async () => { - const degradedThreshold = 1000; - const retries = 1; - // Initial interceptor for failing request - nock('https://price.api.cx.metamask.io') - .get('/v2/chains/1/spot-prices') - .query({ - tokenAddresses: - '0x0000000000000000000000000000000000000000,0xAAA,0xBBB,0xCCC', - vsCurrency: 'ETH', - includeMarketData: 'true', - }) - .replyWithError('Failed to fetch'); - // Second interceptor for successful response - nock('https://price.api.cx.metamask.io') - .get('/v2/chains/1/spot-prices') - .query({ - tokenAddresses: - '0x0000000000000000000000000000000000000000,0xAAA,0xBBB,0xCCC', - vsCurrency: 'ETH', - includeMarketData: 'true', - }) - .delay(degradedThreshold * 2) - .reply(200, { - '0x0000000000000000000000000000000000000000': { - price: 14, - currency: 'ETH', - pricePercentChange1d: 1, - priceChange1d: 1, - }, - '0xaaa': { - price: 148.17205755299946, - currency: 'ETH', - pricePercentChange1d: 1, - priceChange1d: 1, - }, - '0xbbb': { - price: 33689.98134554716, - currency: 'ETH', - pricePercentChange1d: 1, - priceChange1d: 1, - }, - '0xccc': { - price: 148.1344197578456, - currency: 'ETH', - pricePercentChange1d: 1, - priceChange1d: 1, - }, - }); - const onDegradedHandler = jest.fn(); - const service = new CodefiTokenPricesServiceV2({ - degradedThreshold, - onDegraded: onDegradedHandler, - retries, + retries, }); await fetchTokenPricesWithFakeTimers({ @@ -1187,7 +1126,7 @@ describe('CodefiTokenPricesServiceV2', () => { clock.restore(); }); - it('stops making fetch requests after too many consecutive failures', async () => { + it('calls onBreak handler upon break', async () => { const retries = 3; // Max consencutive failures is set to match number of calls in three update attempts (including retries) const maximumConsecutiveFailures = (1 + retries) * 3; @@ -1203,7 +1142,7 @@ describe('CodefiTokenPricesServiceV2', () => { .times(maximumConsecutiveFailures) .replyWithError('Failed to fetch'); // This interceptor should not be used - const successfullCallScope = nock('https://price.api.cx.metamask.io') + nock('https://price.api.cx.metamask.io') .get('/v2/chains/1/spot-prices') .query({ tokenAddresses: @@ -1297,9 +1236,11 @@ describe('CodefiTokenPricesServiceV2', () => { pricePercentChange1y: -2.2992517267242754, }, }); + const onBreakHandler = jest.fn(); const service = new CodefiTokenPricesServiceV2({ retries, maximumConsecutiveFailures, + onBreak: onBreakHandler, // Ensure break duration is well over the max delay for a single request, so that the // break doesn't end during a retry attempt circuitBreakDuration: defaultMaxRetryDelay * 10, @@ -1310,6 +1251,8 @@ describe('CodefiTokenPricesServiceV2', () => { tokenAddresses: ['0xAAA', '0xBBB', '0xCCC'], currency: 'ETH', }); + expect(onBreakHandler).not.toHaveBeenCalled(); + // Initial three calls to exhaust maximum allowed failures // eslint-disable-next-line @typescript-eslint/no-unused-vars for (const _retryAttempt of Array(retries).keys()) { @@ -1323,642 +1266,7 @@ describe('CodefiTokenPricesServiceV2', () => { ).rejects.toThrow('Failed to fetch'); } - await expect(() => - fetchTokenPricesWithFakeTimers({ - clock, - fetchTokenPrices, - retries, - }), - ).rejects.toThrow( - 'Execution prevented because the circuit breaker is open', - ); - expect(successfullCallScope.isDone()).toBe(false); - }); - - it('calls onBreak handler upon break', async () => { - const retries = 3; - // Max consencutive failures is set to match number of calls in three update attempts (including retries) - const maximumConsecutiveFailures = (1 + retries) * 3; - // Initial interceptor for failing requests - nock('https://price.api.cx.metamask.io') - .get('/v2/chains/1/spot-prices') - .query({ - tokenAddresses: - '0x0000000000000000000000000000000000000000,0xAAA,0xBBB,0xCCC', - vsCurrency: 'ETH', - includeMarketData: 'true', - }) - .times(maximumConsecutiveFailures) - .replyWithError('Failed to fetch'); - // This interceptor should not be used - nock('https://price.api.cx.metamask.io') - .get('/v2/chains/1/spot-prices') - .query({ - tokenAddresses: - '0x0000000000000000000000000000000000000000,0xAAA,0xBBB,0xCCC', - vsCurrency: 'ETH', - includeMarketData: 'true', - }) - .reply(200, { - '0x0000000000000000000000000000000000000000': { - price: 14, - currency: 'ETH', - pricePercentChange1d: 1, - priceChange1d: 1, - marketCap: 117219.99428314982, - allTimeHigh: 0.00060467892389492, - allTimeLow: 0.00002303954000865728, - totalVolume: 5155.094053542448, - high1d: 0.00008020715848194385, - low1d: 0.00007792083564549064, - circulatingSupply: 1494269733.9526057, - dilutedMarketCap: 117669.5125951733, - marketCapPercentChange1d: 0.76671, - pricePercentChange1h: -1.0736342953259423, - pricePercentChange7d: -7.351582573655089, - pricePercentChange14d: -1.0799098946709822, - pricePercentChange30d: -25.776321124365992, - pricePercentChange200d: 46.091571238599165, - pricePercentChange1y: -2.2992517267242754, - }, - '0xaaa': { - price: 148.17205755299946, - currency: 'ETH', - pricePercentChange1d: 1, - priceChange1d: 1, - marketCap: 117219.99428314982, - allTimeHigh: 0.00060467892389492, - allTimeLow: 0.00002303954000865728, - totalVolume: 5155.094053542448, - high1d: 0.00008020715848194385, - low1d: 0.00007792083564549064, - circulatingSupply: 1494269733.9526057, - dilutedMarketCap: 117669.5125951733, - marketCapPercentChange1d: 0.76671, - pricePercentChange1h: -1.0736342953259423, - pricePercentChange7d: -7.351582573655089, - pricePercentChange14d: -1.0799098946709822, - pricePercentChange30d: -25.776321124365992, - pricePercentChange200d: 46.091571238599165, - pricePercentChange1y: -2.2992517267242754, - }, - '0xbbb': { - price: 33689.98134554716, - currency: 'ETH', - pricePercentChange1d: 1, - priceChange1d: 1, - marketCap: 117219.99428314982, - allTimeHigh: 0.00060467892389492, - allTimeLow: 0.00002303954000865728, - totalVolume: 5155.094053542448, - high1d: 0.00008020715848194385, - low1d: 0.00007792083564549064, - circulatingSupply: 1494269733.9526057, - dilutedMarketCap: 117669.5125951733, - marketCapPercentChange1d: 0.76671, - pricePercentChange1h: -1.0736342953259423, - pricePercentChange7d: -7.351582573655089, - pricePercentChange14d: -1.0799098946709822, - pricePercentChange30d: -25.776321124365992, - pricePercentChange200d: 46.091571238599165, - pricePercentChange1y: -2.2992517267242754, - }, - '0xccc': { - price: 148.1344197578456, - currency: 'ETH', - pricePercentChange1d: 1, - priceChange1d: 1, - marketCap: 117219.99428314982, - allTimeHigh: 0.00060467892389492, - allTimeLow: 0.00002303954000865728, - totalVolume: 5155.094053542448, - high1d: 0.00008020715848194385, - low1d: 0.00007792083564549064, - circulatingSupply: 1494269733.9526057, - dilutedMarketCap: 117669.5125951733, - marketCapPercentChange1d: 0.76671, - pricePercentChange1h: -1.0736342953259423, - pricePercentChange7d: -7.351582573655089, - pricePercentChange14d: -1.0799098946709822, - pricePercentChange30d: -25.776321124365992, - pricePercentChange200d: 46.091571238599165, - pricePercentChange1y: -2.2992517267242754, - }, - }); - const onBreakHandler = jest.fn(); - const service = new CodefiTokenPricesServiceV2({ - retries, - maximumConsecutiveFailures, - // Ensure break duration is well over the max delay for a single request, so that the - // break doesn't end during a retry attempt - onBreak: onBreakHandler, - circuitBreakDuration: defaultMaxRetryDelay * 10, - }); - const fetchTokenPrices = () => - service.fetchTokenPrices({ - chainId: '0x1', - tokenAddresses: ['0xAAA', '0xBBB', '0xCCC'], - currency: 'ETH', - }); - expect(onBreakHandler).not.toHaveBeenCalled(); - - // Initial three calls to exhaust maximum allowed failures - // eslint-disable-next-line @typescript-eslint/no-unused-vars - for (const _retryAttempt of Array(retries).keys()) { - // eslint-disable-next-line no-loop-func - await expect(() => - fetchTokenPricesWithFakeTimers({ - clock, - fetchTokenPrices, - retries, - }), - ).rejects.toThrow('Failed to fetch'); - } - - expect(onBreakHandler).toHaveBeenCalledTimes(1); - }); - - it('stops calling onDegraded after circuit break', async () => { - const retries = 3; - // Max consencutive failures is set to match number of calls in three update attempts (including retries) - const maximumConsecutiveFailures = (1 + retries) * 3; - // Initial interceptor for failing requests - nock('https://price.api.cx.metamask.io') - .get('/v2/chains/1/spot-prices') - .query({ - tokenAddresses: - '0x0000000000000000000000000000000000000000,0xAAA,0xBBB,0xCCC', - vsCurrency: 'ETH', - includeMarketData: 'true', - }) - .times(maximumConsecutiveFailures) - .replyWithError('Failed to fetch'); - const onBreakHandler = jest.fn(); - const onDegradedHandler = jest.fn(); - const service = new CodefiTokenPricesServiceV2({ - retries, - maximumConsecutiveFailures, - // Ensure break duration is well over the max delay for a single request, so that the - // break doesn't end during a retry attempt - onBreak: onBreakHandler, - onDegraded: onDegradedHandler, - circuitBreakDuration: defaultMaxRetryDelay * 10, - }); - const fetchTokenPrices = () => - service.fetchTokenPrices({ - chainId: '0x1', - tokenAddresses: ['0xAAA', '0xBBB', '0xCCC'], - currency: 'ETH', - }); - expect(onBreakHandler).not.toHaveBeenCalled(); - // Initial three calls to exhaust maximum allowed failures - // eslint-disable-next-line @typescript-eslint/no-unused-vars - for (const _retryAttempt of Array(retries).keys()) { - // eslint-disable-next-line no-loop-func - await expect(() => - fetchTokenPricesWithFakeTimers({ - clock, - fetchTokenPrices, - retries, - }), - ).rejects.toThrow('Failed to fetch'); - } - // Confirm that circuit is broken expect(onBreakHandler).toHaveBeenCalledTimes(1); - // Should be called twice by now, once per update attempt prior to break - expect(onDegradedHandler).toHaveBeenCalledTimes(2); - - await expect(() => - fetchTokenPricesWithFakeTimers({ - clock, - fetchTokenPrices, - retries, - }), - ).rejects.toThrow( - 'Execution prevented because the circuit breaker is open', - ); - - expect(onDegradedHandler).toHaveBeenCalledTimes(2); - }); - - it('keeps circuit closed if first request fails when half-open', async () => { - const retries = 3; - // Max consencutive failures is set to match number of calls in three update attempts (including retries) - const maximumConsecutiveFailures = (1 + retries) * 3; - // Ensure break duration is well over the max delay for a single request, so that the - // break doesn't end during a retry attempt - const circuitBreakDuration = defaultMaxRetryDelay * 10; - // Initial interceptor for failing requests - nock('https://price.api.cx.metamask.io') - .get('/v2/chains/1/spot-prices') - .query({ - tokenAddresses: - '0x0000000000000000000000000000000000000000,0xAAA,0xBBB,0xCCC', - vsCurrency: 'ETH', - includeMarketData: 'true', - }) - // The +1 is for the additional request when the circuit is half-open - .times(maximumConsecutiveFailures + 1) - .replyWithError('Failed to fetch'); - // This interceptor should not be used - const successfullCallScope = nock('https://price.api.cx.metamask.io') - .get('/v2/chains/1/spot-prices') - .query({ - tokenAddresses: '0xAAA,0xBBB,0xCCC', - vsCurrency: 'ETH', - }) - .reply(200, { - '0x0000000000000000000000000000000000000000': { - price: 14, - currency: 'ETH', - pricePercentChange1d: 1, - priceChange1d: 1, - marketCap: 117219.99428314982, - allTimeHigh: 0.00060467892389492, - allTimeLow: 0.00002303954000865728, - totalVolume: 5155.094053542448, - high1d: 0.00008020715848194385, - low1d: 0.00007792083564549064, - circulatingSupply: 1494269733.9526057, - dilutedMarketCap: 117669.5125951733, - marketCapPercentChange1d: 0.76671, - pricePercentChange1h: -1.0736342953259423, - pricePercentChange7d: -7.351582573655089, - pricePercentChange14d: -1.0799098946709822, - pricePercentChange30d: -25.776321124365992, - pricePercentChange200d: 46.091571238599165, - pricePercentChange1y: -2.2992517267242754, - }, - '0xaaa': { - price: 148.17205755299946, - currency: 'ETH', - pricePercentChange1d: 1, - priceChange1d: 1, - marketCap: 117219.99428314982, - allTimeHigh: 0.00060467892389492, - allTimeLow: 0.00002303954000865728, - totalVolume: 5155.094053542448, - high1d: 0.00008020715848194385, - low1d: 0.00007792083564549064, - circulatingSupply: 1494269733.9526057, - dilutedMarketCap: 117669.5125951733, - marketCapPercentChange1d: 0.76671, - pricePercentChange1h: -1.0736342953259423, - pricePercentChange7d: -7.351582573655089, - pricePercentChange14d: -1.0799098946709822, - pricePercentChange30d: -25.776321124365992, - pricePercentChange200d: 46.091571238599165, - pricePercentChange1y: -2.2992517267242754, - }, - '0xbbb': { - price: 33689.98134554716, - currency: 'ETH', - pricePercentChange1d: 1, - priceChange1d: 1, - marketCap: 117219.99428314982, - allTimeHigh: 0.00060467892389492, - allTimeLow: 0.00002303954000865728, - totalVolume: 5155.094053542448, - high1d: 0.00008020715848194385, - low1d: 0.00007792083564549064, - circulatingSupply: 1494269733.9526057, - dilutedMarketCap: 117669.5125951733, - marketCapPercentChange1d: 0.76671, - pricePercentChange1h: -1.0736342953259423, - pricePercentChange7d: -7.351582573655089, - pricePercentChange14d: -1.0799098946709822, - pricePercentChange30d: -25.776321124365992, - pricePercentChange200d: 46.091571238599165, - pricePercentChange1y: -2.2992517267242754, - }, - '0xccc': { - price: 148.1344197578456, - currency: 'ETH', - pricePercentChange1d: 1, - priceChange1d: 1, - marketCap: 117219.99428314982, - allTimeHigh: 0.00060467892389492, - allTimeLow: 0.00002303954000865728, - totalVolume: 5155.094053542448, - high1d: 0.00008020715848194385, - low1d: 0.00007792083564549064, - circulatingSupply: 1494269733.9526057, - dilutedMarketCap: 117669.5125951733, - marketCapPercentChange1d: 0.76671, - pricePercentChange1h: -1.0736342953259423, - pricePercentChange7d: -7.351582573655089, - pricePercentChange14d: -1.0799098946709822, - pricePercentChange30d: -25.776321124365992, - pricePercentChange200d: 46.091571238599165, - pricePercentChange1y: -2.2992517267242754, - }, - }); - const service = new CodefiTokenPricesServiceV2({ - retries, - maximumConsecutiveFailures, - circuitBreakDuration, - }); - const fetchTokenPrices = () => - service.fetchTokenPrices({ - chainId: '0x1', - tokenAddresses: ['0xAAA', '0xBBB', '0xCCC'], - currency: 'ETH', - }); - // Initial three calls to exhaust maximum allowed failures - // eslint-disable-next-line @typescript-eslint/no-unused-vars - for (const _retryAttempt of Array(retries).keys()) { - // eslint-disable-next-line no-loop-func - await expect(() => - fetchTokenPricesWithFakeTimers({ - clock, - fetchTokenPrices, - retries, - }), - ).rejects.toThrow('Failed to fetch'); - } - // Confirm that circuit has broken - await expect(() => - fetchTokenPricesWithFakeTimers({ - clock, - fetchTokenPrices, - retries, - }), - ).rejects.toThrow( - 'Execution prevented because the circuit breaker is open', - ); - // Wait for circuit to move to half-open - await clock.tickAsync(circuitBreakDuration); - - // The circuit should remain open after the first request fails - // The fetch error is replaced by the circuit break error due to the retries - await expect(() => - fetchTokenPricesWithFakeTimers({ - clock, - fetchTokenPrices, - retries, - }), - ).rejects.toThrow( - 'Execution prevented because the circuit breaker is open', - ); - - // Confirm that the circuit is still open - await expect(() => - fetchTokenPricesWithFakeTimers({ - clock, - fetchTokenPrices, - retries, - }), - ).rejects.toThrow( - 'Execution prevented because the circuit breaker is open', - ); - expect(successfullCallScope.isDone()).toBe(false); - }); - - it('recovers after circuit break', async () => { - const retries = 3; - // Max consencutive failures is set to match number of calls in three update attempts (including retries) - const maximumConsecutiveFailures = (1 + retries) * 3; - // Ensure break duration is well over the max delay for a single request, so that the - // break doesn't end during a retry attempt - const circuitBreakDuration = defaultMaxRetryDelay * 10; - // Initial interceptor for failing requests - nock('https://price.api.cx.metamask.io') - .get('/v2/chains/1/spot-prices') - .query({ - tokenAddresses: - '0x0000000000000000000000000000000000000000,0xAAA,0xBBB,0xCCC', - vsCurrency: 'ETH', - includeMarketData: 'true', - }) - .times(maximumConsecutiveFailures) - .replyWithError('Failed to fetch'); - // Later interceptor for successfull request after recovery - nock('https://price.api.cx.metamask.io') - .get('/v2/chains/1/spot-prices') - .query({ - tokenAddresses: - '0x0000000000000000000000000000000000000000,0xAAA,0xBBB,0xCCC', - vsCurrency: 'ETH', - includeMarketData: 'true', - }) - .reply(200, { - '0x0000000000000000000000000000000000000000': { - price: 14, - currency: 'ETH', - pricePercentChange1d: 1, - priceChange1d: 1, - marketCap: 117219.99428314982, - allTimeHigh: 0.00060467892389492, - allTimeLow: 0.00002303954000865728, - totalVolume: 5155.094053542448, - high1d: 0.00008020715848194385, - low1d: 0.00007792083564549064, - circulatingSupply: 1494269733.9526057, - dilutedMarketCap: 117669.5125951733, - marketCapPercentChange1d: 0.76671, - pricePercentChange1h: -1.0736342953259423, - pricePercentChange7d: -7.351582573655089, - pricePercentChange14d: -1.0799098946709822, - pricePercentChange30d: -25.776321124365992, - pricePercentChange200d: 46.091571238599165, - pricePercentChange1y: -2.2992517267242754, - }, - '0xaaa': { - price: 148.17205755299946, - currency: 'ETH', - pricePercentChange1d: 1, - priceChange1d: 1, - marketCap: 117219.99428314982, - allTimeHigh: 0.00060467892389492, - allTimeLow: 0.00002303954000865728, - totalVolume: 5155.094053542448, - high1d: 0.00008020715848194385, - low1d: 0.00007792083564549064, - circulatingSupply: 1494269733.9526057, - dilutedMarketCap: 117669.5125951733, - marketCapPercentChange1d: 0.76671, - pricePercentChange1h: -1.0736342953259423, - pricePercentChange7d: -7.351582573655089, - pricePercentChange14d: -1.0799098946709822, - pricePercentChange30d: -25.776321124365992, - pricePercentChange200d: 46.091571238599165, - pricePercentChange1y: -2.2992517267242754, - }, - '0xbbb': { - price: 33689.98134554716, - currency: 'ETH', - pricePercentChange1d: 1, - priceChange1d: 1, - marketCap: 117219.99428314982, - allTimeHigh: 0.00060467892389492, - allTimeLow: 0.00002303954000865728, - totalVolume: 5155.094053542448, - high1d: 0.00008020715848194385, - low1d: 0.00007792083564549064, - circulatingSupply: 1494269733.9526057, - dilutedMarketCap: 117669.5125951733, - marketCapPercentChange1d: 0.76671, - pricePercentChange1h: -1.0736342953259423, - pricePercentChange7d: -7.351582573655089, - pricePercentChange14d: -1.0799098946709822, - pricePercentChange30d: -25.776321124365992, - pricePercentChange200d: 46.091571238599165, - pricePercentChange1y: -2.2992517267242754, - }, - '0xccc': { - price: 148.1344197578456, - currency: 'ETH', - pricePercentChange1d: 1, - priceChange1d: 1, - marketCap: 117219.99428314982, - allTimeHigh: 0.00060467892389492, - allTimeLow: 0.00002303954000865728, - totalVolume: 5155.094053542448, - high1d: 0.00008020715848194385, - low1d: 0.00007792083564549064, - circulatingSupply: 1494269733.9526057, - dilutedMarketCap: 117669.5125951733, - marketCapPercentChange1d: 0.76671, - pricePercentChange1h: -1.0736342953259423, - pricePercentChange7d: -7.351582573655089, - pricePercentChange14d: -1.0799098946709822, - pricePercentChange30d: -25.776321124365992, - pricePercentChange200d: 46.091571238599165, - pricePercentChange1y: -2.2992517267242754, - }, - }); - const service = new CodefiTokenPricesServiceV2({ - retries, - maximumConsecutiveFailures, - circuitBreakDuration, - }); - const fetchTokenPrices = () => - service.fetchTokenPrices({ - chainId: '0x1', - tokenAddresses: ['0xAAA', '0xBBB', '0xCCC'], - currency: 'ETH', - }); - // Initial three calls to exhaust maximum allowed failures - // eslint-disable-next-line @typescript-eslint/no-unused-vars - for (const _retryAttempt of Array(retries).keys()) { - // eslint-disable-next-line no-loop-func - await expect(() => - fetchTokenPricesWithFakeTimers({ - clock, - fetchTokenPrices, - retries, - }), - ).rejects.toThrow('Failed to fetch'); - } - // Confirm that circuit has broken - await expect(() => - fetchTokenPricesWithFakeTimers({ - clock, - fetchTokenPrices, - retries, - }), - ).rejects.toThrow( - 'Execution prevented because the circuit breaker is open', - ); - // Wait for circuit to move to half-open - await clock.tickAsync(circuitBreakDuration); - - const marketDataTokensByAddress = await fetchTokenPricesWithFakeTimers({ - clock, - fetchTokenPrices, - retries, - }); - - expect(marketDataTokensByAddress).toStrictEqual({ - '0x0000000000000000000000000000000000000000': { - tokenAddress: '0x0000000000000000000000000000000000000000', - currency: 'ETH', - pricePercentChange1d: 1, - priceChange1d: 1, - marketCap: 117219.99428314982, - allTimeHigh: 0.00060467892389492, - allTimeLow: 0.00002303954000865728, - totalVolume: 5155.094053542448, - high1d: 0.00008020715848194385, - low1d: 0.00007792083564549064, - price: 14, - circulatingSupply: 1494269733.9526057, - dilutedMarketCap: 117669.5125951733, - marketCapPercentChange1d: 0.76671, - pricePercentChange1h: -1.0736342953259423, - pricePercentChange7d: -7.351582573655089, - pricePercentChange14d: -1.0799098946709822, - pricePercentChange30d: -25.776321124365992, - pricePercentChange200d: 46.091571238599165, - pricePercentChange1y: -2.2992517267242754, - }, - '0xAAA': { - tokenAddress: '0xAAA', - currency: 'ETH', - pricePercentChange1d: 1, - priceChange1d: 1, - marketCap: 117219.99428314982, - allTimeHigh: 0.00060467892389492, - allTimeLow: 0.00002303954000865728, - totalVolume: 5155.094053542448, - high1d: 0.00008020715848194385, - low1d: 0.00007792083564549064, - price: 148.17205755299946, - circulatingSupply: 1494269733.9526057, - dilutedMarketCap: 117669.5125951733, - marketCapPercentChange1d: 0.76671, - pricePercentChange1h: -1.0736342953259423, - pricePercentChange7d: -7.351582573655089, - pricePercentChange14d: -1.0799098946709822, - pricePercentChange30d: -25.776321124365992, - pricePercentChange200d: 46.091571238599165, - pricePercentChange1y: -2.2992517267242754, - }, - '0xBBB': { - tokenAddress: '0xBBB', - currency: 'ETH', - pricePercentChange1d: 1, - priceChange1d: 1, - marketCap: 117219.99428314982, - allTimeHigh: 0.00060467892389492, - allTimeLow: 0.00002303954000865728, - totalVolume: 5155.094053542448, - high1d: 0.00008020715848194385, - low1d: 0.00007792083564549064, - price: 33689.98134554716, - circulatingSupply: 1494269733.9526057, - dilutedMarketCap: 117669.5125951733, - marketCapPercentChange1d: 0.76671, - pricePercentChange1h: -1.0736342953259423, - pricePercentChange7d: -7.351582573655089, - pricePercentChange14d: -1.0799098946709822, - pricePercentChange30d: -25.776321124365992, - pricePercentChange200d: 46.091571238599165, - pricePercentChange1y: -2.2992517267242754, - }, - '0xCCC': { - tokenAddress: '0xCCC', - currency: 'ETH', - pricePercentChange1d: 1, - priceChange1d: 1, - marketCap: 117219.99428314982, - allTimeHigh: 0.00060467892389492, - allTimeLow: 0.00002303954000865728, - totalVolume: 5155.094053542448, - high1d: 0.00008020715848194385, - low1d: 0.00007792083564549064, - price: 148.1344197578456, - circulatingSupply: 1494269733.9526057, - dilutedMarketCap: 117669.5125951733, - marketCapPercentChange1d: 0.76671, - pricePercentChange1h: -1.0736342953259423, - pricePercentChange7d: -7.351582573655089, - pricePercentChange14d: -1.0799098946709822, - pricePercentChange30d: -25.776321124365992, - pricePercentChange200d: 46.091571238599165, - pricePercentChange1y: -2.2992517267242754, - }, - }); }); }); }); diff --git a/packages/assets-controllers/src/token-prices-service/codefi-v2.ts b/packages/assets-controllers/src/token-prices-service/codefi-v2.ts index 4f163203e4d..901d18f7245 100644 --- a/packages/assets-controllers/src/token-prices-service/codefi-v2.ts +++ b/packages/assets-controllers/src/token-prices-service/codefi-v2.ts @@ -1,16 +1,14 @@ -import { handleFetch } from '@metamask/controller-utils'; +import { + createServicePolicy, + DEFAULT_CIRCUIT_BREAK_DURATION, + DEFAULT_DEGRADED_THRESHOLD, + DEFAULT_MAX_CONSECUTIVE_FAILURES, + DEFAULT_MAX_RETRIES, + handleFetch, +} from '@metamask/controller-utils'; +import type { ServicePolicy } from '@metamask/controller-utils'; import type { Hex } from '@metamask/utils'; import { hexToNumber } from '@metamask/utils'; -import { - circuitBreaker, - ConsecutiveBreaker, - ExponentialBackoff, - handleAll, - type IPolicy, - retry, - wrap, - CircuitState, -} from 'cockatiel'; import type { AbstractTokenPricesService, @@ -271,13 +269,6 @@ type SupportedChainId = (typeof SUPPORTED_CHAIN_IDS)[number]; */ const BASE_URL = 'https://price.api.cx.metamask.io/v2'; -const DEFAULT_TOKEN_PRICE_RETRIES = 3; -// Each update attempt will result (1 + retries) calls if the server is down -const DEFAULT_TOKEN_PRICE_MAX_CONSECUTIVE_FAILURES = - (1 + DEFAULT_TOKEN_PRICE_RETRIES) * 3; - -const DEFAULT_DEGRADED_THRESHOLD = 5_000; - /** * The shape of the data that the /spot-prices endpoint returns. */ @@ -365,31 +356,64 @@ export class CodefiTokenPricesServiceV2 implements AbstractTokenPricesService { - #tokenPricePolicy: IPolicy; + readonly #policy: ServicePolicy; /** * Construct a Codefi Token Price Service. * - * @param options - Constructor options - * @param options.degradedThreshold - The threshold between "normal" and "degrated" service, - * in milliseconds. - * @param options.retries - Number of retry attempts for each token price update. - * @param options.maximumConsecutiveFailures - The maximum number of consecutive failures - * allowed before breaking the circuit and pausing further updates. - * @param options.onBreak - An event handler for when the circuit breaks, useful for capturing - * metrics about network failures. - * @param options.onDegraded - An event handler for when the circuit remains closed, but requests - * are failing or resolving too slowly (i.e. resolving more slowly than the `degradedThreshold). - * @param options.circuitBreakDuration - The amount of time to wait when the circuit breaks - * from too many consecutive failures. + * @param args - The arguments. + * @param args.degradedThreshold - The length of time (in milliseconds) + * that governs when the service is regarded as degraded (affecting when + * `onDegraded` is called). Defaults to 5 seconds. + * @param args.retries - Number of retry attempts for each fetch request. + * @param args.maximumConsecutiveFailures - The maximum number of consecutive + * failures allowed before breaking the circuit and pausing further updates. + * @param args.circuitBreakDuration - The amount of time to wait when the + * circuit breaks from too many consecutive failures. */ + constructor(args?: { + degradedThreshold?: number; + retries?: number; + maximumConsecutiveFailures?: number; + circuitBreakDuration?: number; + }); + + /** + * Construct a Codefi Token Price Service. + * + * @deprecated This signature is deprecated; please use the `onBreak` and + * `onDegraded` methods instead. + * @param args - The arguments. + * @param args.degradedThreshold - The length of time (in milliseconds) + * that governs when the service is regarded as degraded (affecting when + * `onDegraded` is called). Defaults to 5 seconds. + * @param args.retries - Number of retry attempts for each fetch request. + * @param args.maximumConsecutiveFailures - The maximum number of consecutive + * failures allowed before breaking the circuit and pausing further updates. + * @param args.onBreak - Callback for when the circuit breaks, useful + * for capturing metrics about network failures. + * @param args.onDegraded - Callback for when the API responds successfully + * but takes too long to respond (5 seconds or more). + * @param args.circuitBreakDuration - The amount of time to wait when the + * circuit breaks from too many consecutive failures. + */ + // eslint-disable-next-line @typescript-eslint/unified-signatures + constructor(args?: { + degradedThreshold?: number; + retries?: number; + maximumConsecutiveFailures?: number; + onBreak?: () => void; + onDegraded?: () => void; + circuitBreakDuration?: number; + }); + constructor({ degradedThreshold = DEFAULT_DEGRADED_THRESHOLD, - retries = DEFAULT_TOKEN_PRICE_RETRIES, - maximumConsecutiveFailures = DEFAULT_TOKEN_PRICE_MAX_CONSECUTIVE_FAILURES, + retries = DEFAULT_MAX_RETRIES, + maximumConsecutiveFailures = DEFAULT_MAX_CONSECUTIVE_FAILURES, onBreak, onDegraded, - circuitBreakDuration = 30 * 60 * 1000, + circuitBreakDuration = DEFAULT_CIRCUIT_BREAK_DURATION, }: { degradedThreshold?: number; retries?: number; @@ -398,35 +422,40 @@ export class CodefiTokenPricesServiceV2 onDegraded?: () => void; circuitBreakDuration?: number; } = {}) { - // Construct a policy that will retry each update, and halt further updates - // for a certain period after too many consecutive failures. - const retryPolicy = retry(handleAll, { - maxAttempts: retries, - backoff: new ExponentialBackoff(), - }); - const circuitBreakerPolicy = circuitBreaker(handleAll, { - halfOpenAfter: circuitBreakDuration, - breaker: new ConsecutiveBreaker(maximumConsecutiveFailures), + this.#policy = createServicePolicy({ + maxRetries: retries, + maxConsecutiveFailures: maximumConsecutiveFailures, + circuitBreakDuration, + degradedThreshold, }); if (onBreak) { - circuitBreakerPolicy.onBreak(onBreak); + this.#policy.onBreak(onBreak); } if (onDegraded) { - retryPolicy.onGiveUp(() => { - if (circuitBreakerPolicy.state === CircuitState.Closed) { - onDegraded(); - } - }); - retryPolicy.onSuccess(({ duration }) => { - if ( - circuitBreakerPolicy.state === CircuitState.Closed && - duration > degradedThreshold - ) { - onDegraded(); - } - }); + this.#policy.onDegraded(onDegraded); } - this.#tokenPricePolicy = wrap(retryPolicy, circuitBreakerPolicy); + } + + /** + * Listens for when the request to the API fails too many times in a row. + * + * @param args - The same arguments that {@link ServicePolicy.onBreak} + * takes. + * @returns What {@link ServicePolicy.onBreak} returns. + */ + onBreak(...args: Parameters) { + return this.#policy.onBreak(...args); + } + + /** + * Listens for when the API is degraded. + * + * @param args - The same arguments that {@link ServicePolicy.onDegraded} + * takes. + * @returns What {@link ServicePolicy.onDegraded} returns. + */ + onDegraded(...args: Parameters) { + return this.#policy.onDegraded(...args); } /** @@ -459,7 +488,7 @@ export class CodefiTokenPricesServiceV2 url.searchParams.append('includeMarketData', 'true'); const addressCryptoDataMap: MarketDataByTokenAddress = - await this.#tokenPricePolicy.execute(() => + await this.#policy.execute(() => handleFetch(url, { headers: { 'Cache-Control': 'no-cache' } }), ); diff --git a/packages/remote-feature-flag-controller/CHANGELOG.md b/packages/remote-feature-flag-controller/CHANGELOG.md index 20df3a0a4ca..b688177db93 100644 --- a/packages/remote-feature-flag-controller/CHANGELOG.md +++ b/packages/remote-feature-flag-controller/CHANGELOG.md @@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Add `onBreak` and `onDegraded` methods to `ClientConfigApiService` ([#5109](https://github.com/MetaMask/core/pull/5109)) + - These serve the same purpose as the `onBreak` and `onDegraded` constructor options, but align more closely with the Cockatiel policy API. + +### Changed + +- Deprecate `ClientConfigApiService` constructor options `onBreak` and `onDegraded` in favor of methods ([#5109](https://github.com/MetaMask/core/pull/5109)) +- Add `@metamask/controller-utils@^11.4.5` as a dependency ([#5109](https://github.com/MetaMask/core/pull/5109)) + - `cockatiel` should still be in the dependency tree because it's now a dependency of `@metamask/controller-utils` + ## [1.3.0] ### Changed diff --git a/packages/remote-feature-flag-controller/package.json b/packages/remote-feature-flag-controller/package.json index 38128e1b2df..7184fd4a618 100644 --- a/packages/remote-feature-flag-controller/package.json +++ b/packages/remote-feature-flag-controller/package.json @@ -48,14 +48,13 @@ }, "dependencies": { "@metamask/base-controller": "^7.1.1", + "@metamask/controller-utils": "^11.4.5", "@metamask/utils": "^11.0.1", - "cockatiel": "^3.1.2", "uuid": "^8.3.2" }, "devDependencies": { "@lavamoat/allow-scripts": "^3.0.4", "@metamask/auto-changelog": "^3.4.4", - "@metamask/controller-utils": "^11.4.5", "@types/jest": "^27.4.1", "deepmerge": "^4.2.2", "jest": "^27.5.1", diff --git a/packages/remote-feature-flag-controller/src/client-config-api-service/abstract-client-config-api-service.ts b/packages/remote-feature-flag-controller/src/client-config-api-service/abstract-client-config-api-service.ts index a7def3bef56..7a07ed2db9b 100644 --- a/packages/remote-feature-flag-controller/src/client-config-api-service/abstract-client-config-api-service.ts +++ b/packages/remote-feature-flag-controller/src/client-config-api-service/abstract-client-config-api-service.ts @@ -1,9 +1,20 @@ -import type { PublicInterface } from '@metamask/utils'; +import type { ServicePolicy } from '@metamask/controller-utils'; -import type { ClientConfigApiService } from './client-config-api-service'; +import type { ServiceResponse } from '../remote-feature-flag-controller-types'; /** * A service object responsible for fetching feature flags. */ -export type AbstractClientConfigApiService = - PublicInterface; +export type AbstractClientConfigApiService = Partial< + Pick +> & { + /** + * Fetches feature flags from the API with specific client, distribution, and + * environment parameters. Provides structured error handling, including + * fallback to cached data if available. + * + * @returns An object of feature flags and their boolean values or a + * structured error object. + */ + fetchRemoteFeatureFlags(): Promise; +}; diff --git a/packages/remote-feature-flag-controller/src/client-config-api-service/client-config-api-service.test.ts b/packages/remote-feature-flag-controller/src/client-config-api-service/client-config-api-service.test.ts index 1baaaf72bf4..0cd8756d47c 100644 --- a/packages/remote-feature-flag-controller/src/client-config-api-service/client-config-api-service.test.ts +++ b/packages/remote-feature-flag-controller/src/client-config-api-service/client-config-api-service.test.ts @@ -20,9 +20,68 @@ const mockFeatureFlags: FeatureFlags = { feature2: { chrome: '<109' }, }; +jest.setTimeout(8000); + describe('ClientConfigApiService', () => { const networkError = new Error('Network error'); + describe('onBreak', () => { + it('should register a listener that is called when the circuit opens', async () => { + const onBreak = jest.fn(); + const mockFetch = createMockFetch({ error: networkError }); + + const clientConfigApiService = new ClientConfigApiService({ + fetch: mockFetch, + maximumConsecutiveFailures: 1, + config: { + client: ClientType.Extension, + distribution: DistributionType.Main, + environment: EnvironmentType.Production, + }, + }); + clientConfigApiService.onBreak(onBreak); + + await expect( + clientConfigApiService.fetchRemoteFeatureFlags(), + ).rejects.toThrow( + 'Execution prevented because the circuit breaker is open', + ); + + expect(onBreak).toHaveBeenCalled(); + }); + }); + + describe('onDegraded', () => { + it('should register a listener that is called when the request is slow', async () => { + const onDegraded = jest.fn(); + const slowFetchTime = 5500; // Exceed the DEFAULT_DEGRADED_THRESHOLD (5000ms) + // Mock fetch to take a long time + const mockSlowFetch = createMockFetch({ + response: { + ok: true, + status: 200, + json: async () => mockServerFeatureFlagsResponse, + }, + delay: slowFetchTime, + }); + + const clientConfigApiService = new ClientConfigApiService({ + fetch: mockSlowFetch, + config: { + client: ClientType.Extension, + distribution: DistributionType.Main, + environment: EnvironmentType.Production, + }, + }); + clientConfigApiService.onDegraded(onDegraded); + + await clientConfigApiService.fetchRemoteFeatureFlags(); + + // Verify the degraded callback was called + expect(onDegraded).toHaveBeenCalled(); + }, 7000); + }); + describe('fetchRemoteFeatureFlags', () => { it('fetches successfully and returns feature flags', async () => { const mockFetch = createMockFetch({ @@ -132,43 +191,8 @@ describe('ClientConfigApiService', () => { // Check that fetch was retried the correct number of times expect(mockFetch).toHaveBeenCalledTimes(maxRetries + 1); // Initial + retries }); - }); - - describe('circuit breaker', () => { - it('opens the circuit breaker after consecutive failures', async () => { - const mockFetch = createMockFetch({ error: networkError }); - const maxFailures = 3; - const clientConfigApiService = new ClientConfigApiService({ - fetch: mockFetch, - maximumConsecutiveFailures: maxFailures, - config: { - client: ClientType.Extension, - distribution: DistributionType.Main, - environment: EnvironmentType.Production, - }, - }); - - // Attempt requests until circuit breaker opens - for (let i = 0; i < maxFailures; i++) { - await expect( - clientConfigApiService.fetchRemoteFeatureFlags(), - ).rejects.toThrow( - /Network error|Execution prevented because the circuit breaker is open/u, - ); - } - - // Verify the circuit breaker is now open - await expect( - clientConfigApiService.fetchRemoteFeatureFlags(), - ).rejects.toThrow( - 'Execution prevented because the circuit breaker is open', - ); - - // Verify fetch was called the expected number of times - expect(mockFetch).toHaveBeenCalledTimes(maxFailures); - }); - it('should call onBreak when circuit breaker opens', async () => { + it('should call the onBreak callback when the circuit opens', async () => { const onBreak = jest.fn(); const mockFetch = createMockFetch({ error: networkError }); @@ -192,8 +216,7 @@ describe('ClientConfigApiService', () => { expect(onBreak).toHaveBeenCalled(); }); - it('should call the onDegraded callback when requests are slow', async () => { - jest.setTimeout(7000); + it('should call the onDegraded callback when the request is slow', async () => { const onDegraded = jest.fn(); const slowFetchTime = 5500; // Exceed the DEFAULT_DEGRADED_THRESHOLD (5000ms) // Mock fetch to take a long time @@ -221,64 +244,6 @@ describe('ClientConfigApiService', () => { // Verify the degraded callback was called expect(onDegraded).toHaveBeenCalled(); }, 7000); - - it('should succeed on a subsequent fetch attempt after retries', async () => { - const maxRetries = 2; - // Mock fetch to fail initially, then succeed - const mockFetch = jest - .fn() - .mockRejectedValueOnce(networkError) - .mockRejectedValueOnce(networkError) - .mockResolvedValueOnce({ - ok: true, - status: 200, - statusText: 'OK', - json: async () => mockServerFeatureFlagsResponse, - }); - - const clientConfigApiService = new ClientConfigApiService({ - fetch: mockFetch, - retries: maxRetries, - config: { - client: ClientType.Extension, - distribution: DistributionType.Main, - environment: EnvironmentType.Production, - }, - }); - - const result = await clientConfigApiService.fetchRemoteFeatureFlags(); - - expect(result).toStrictEqual({ - remoteFeatureFlags: mockFeatureFlags, - cacheTimestamp: expect.any(Number), - }); - - expect(mockFetch).toHaveBeenCalledTimes(maxRetries + 1); - }); - - it('calls onDegraded when retries are exhausted and circuit is closed', async () => { - const onDegraded = jest.fn(); - const mockFetch = jest.fn(); - mockFetch.mockRejectedValue(new Error('Network error')); - - const service = new ClientConfigApiService({ - fetch: mockFetch, - retries: 2, - onDegraded, - config: { - client: ClientType.Extension, - distribution: DistributionType.Main, - environment: EnvironmentType.Production, - }, - }); - - await expect(service.fetchRemoteFeatureFlags()).rejects.toThrow( - 'Network error', - ); - - // Should be called once after all retries are exhausted - expect(onDegraded).toHaveBeenCalledTimes(1); - }); }); }); diff --git a/packages/remote-feature-flag-controller/src/client-config-api-service/client-config-api-service.ts b/packages/remote-feature-flag-controller/src/client-config-api-service/client-config-api-service.ts index 7cd9177e0b9..73e642526d8 100644 --- a/packages/remote-feature-flag-controller/src/client-config-api-service/client-config-api-service.ts +++ b/packages/remote-feature-flag-controller/src/client-config-api-service/client-config-api-service.ts @@ -1,14 +1,12 @@ import { - circuitBreaker, - ConsecutiveBreaker, - ExponentialBackoff, - handleAll, - type IPolicy, - retry, - wrap, - CircuitState, -} from 'cockatiel'; - + createServicePolicy, + DEFAULT_CIRCUIT_BREAK_DURATION, + DEFAULT_MAX_CONSECUTIVE_FAILURES, + DEFAULT_MAX_RETRIES, +} from '@metamask/controller-utils'; +import type { ServicePolicy } from '@metamask/controller-utils'; + +import type { AbstractClientConfigApiService } from './abstract-client-config-api-service'; import { BASE_URL } from '../constants'; import type { FeatureFlags, @@ -19,19 +17,13 @@ import type { ApiDataResponse, } from '../remote-feature-flag-controller-types'; -const DEFAULT_FETCH_RETRIES = 3; -// Each update attempt will result (1 + retries) calls if the server is down -const DEFAULT_MAX_CONSECUTIVE_FAILURES = (1 + DEFAULT_FETCH_RETRIES) * 3; - -export const DEFAULT_DEGRADED_THRESHOLD = 5000; - /** * This service is responsible for fetching feature flags from the ClientConfig API. */ -export class ClientConfigApiService { +export class ClientConfigApiService implements AbstractClientConfigApiService { #fetch: typeof fetch; - #policy: IPolicy; + readonly #policy: ServicePolicy; #client: ClientType; @@ -44,23 +36,83 @@ export class ClientConfigApiService { * * @param args - The arguments. * @param args.fetch - A function that can be used to make an HTTP request. + * If your JavaScript environment supports `fetch` natively, you'll probably + * want to pass that; otherwise you can pass an equivalent (such as `fetch` + * via `node-fetch`). * @param args.retries - Number of retry attempts for each fetch request. - * @param args.maximumConsecutiveFailures - The maximum number of consecutive failures - * allowed before breaking the circuit and pausing further fetch attempts. - * @param args.circuitBreakDuration - The duration for which the circuit remains open after - * too many consecutive failures. - * @param args.onBreak - Callback invoked when the circuit breaks. - * @param args.onDegraded - Callback invoked when the service is degraded (requests resolving too slowly). - * @param args.config - The configuration object, includes client, distribution, and environment. + * @param args.maximumConsecutiveFailures - The maximum number of consecutive + * failures allowed before breaking the circuit and pausing further fetch + * attempts. + * @param args.circuitBreakDuration - The amount of time to wait when the + * circuit breaks from too many consecutive failures. + * @param args.config - The configuration object, includes client, + * distribution, and environment. * @param args.config.client - The client type (e.g., 'extension', 'mobile'). - * @param args.config.distribution - The distribution type (e.g., 'main', 'flask'). - * @param args.config.environment - The environment type (e.g., 'prod', 'rc', 'dev'). + * @param args.config.distribution - The distribution type (e.g., 'main', + * 'flask'). + * @param args.config.environment - The environment type (e.g., 'prod', 'rc', + * 'dev'). */ + constructor(args: { + fetch: typeof fetch; + retries?: number; + maximumConsecutiveFailures?: number; + circuitBreakDuration?: number; + config: { + client: ClientType; + distribution: DistributionType; + environment: EnvironmentType; + }; + }); + + /** + * Constructs a new ClientConfigApiService object. + * + * @deprecated This signature is deprecated; please use the `onBreak` and + * `onDegraded` methods instead. + * @param args - The arguments. + * @param args.fetch - A function that can be used to make an HTTP request. + * If your JavaScript environment supports `fetch` natively, you'll probably + * want to pass that; otherwise you can pass an equivalent (such as `fetch` + * via `node-fetch`). + * @param args.retries - Number of retry attempts for each fetch request. + * @param args.maximumConsecutiveFailures - The maximum number of consecutive + * failures allowed before breaking the circuit and pausing further fetch + * attempts. + * @param args.circuitBreakDuration - The amount of time to wait when the + * circuit breaks from too many consecutive failures. + * @param args.onBreak - Callback for when the circuit breaks, useful + * for capturing metrics about network failures. + * @param args.onDegraded - Callback for when the API responds successfully + * but takes too long to respond (5 seconds or more). + * @param args.config - The configuration object, includes client, + * distribution, and environment. + * @param args.config.client - The client type (e.g., 'extension', 'mobile'). + * @param args.config.distribution - The distribution type (e.g., 'main', + * 'flask'). + * @param args.config.environment - The environment type (e.g., 'prod', 'rc', + * 'dev'). + */ + // eslint-disable-next-line @typescript-eslint/unified-signatures + constructor(args: { + fetch: typeof fetch; + retries?: number; + maximumConsecutiveFailures?: number; + circuitBreakDuration?: number; + onBreak?: () => void; + onDegraded?: () => void; + config: { + client: ClientType; + distribution: DistributionType; + environment: EnvironmentType; + }; + }); + constructor({ fetch: fetchFunction, - retries = DEFAULT_FETCH_RETRIES, + retries = DEFAULT_MAX_RETRIES, maximumConsecutiveFailures = DEFAULT_MAX_CONSECUTIVE_FAILURES, - circuitBreakDuration = 30 * 60 * 1000, + circuitBreakDuration = DEFAULT_CIRCUIT_BREAK_DURATION, onBreak, onDegraded, config, @@ -82,38 +134,39 @@ export class ClientConfigApiService { this.#distribution = config.distribution; this.#environment = config.environment; - const retryPolicy = retry(handleAll, { - maxAttempts: retries, - backoff: new ExponentialBackoff(), - }); - - const circuitBreakerPolicy = circuitBreaker(handleAll, { - halfOpenAfter: circuitBreakDuration, - breaker: new ConsecutiveBreaker(maximumConsecutiveFailures), + this.#policy = createServicePolicy({ + maxRetries: retries, + maxConsecutiveFailures: maximumConsecutiveFailures, + circuitBreakDuration, }); - if (onBreak) { - circuitBreakerPolicy.onBreak(onBreak); + this.#policy.onBreak(onBreak); } - if (onDegraded) { - retryPolicy.onGiveUp(() => { - if (circuitBreakerPolicy.state === CircuitState.Closed) { - onDegraded(); - } - }); - - retryPolicy.onSuccess(({ duration }) => { - if ( - circuitBreakerPolicy.state === CircuitState.Closed && - duration > DEFAULT_DEGRADED_THRESHOLD // Default degraded threshold - ) { - onDegraded(); - } - }); + this.#policy.onDegraded(onDegraded); } + } - this.#policy = wrap(retryPolicy, circuitBreakerPolicy); + /** + * Listens for when the request to the API fails too many times in a row. + * + * @param args - The same arguments that {@link ServicePolicy.onBreak} + * takes. + * @returns What {@link ServicePolicy.onBreak} returns. + */ + onBreak(...args: Parameters) { + return this.#policy.onBreak(...args); + } + + /** + * Listens for when the API is degraded. + * + * @param args - The same arguments that {@link ServicePolicy.onDegraded} + * takes. + * @returns What {@link ServicePolicy.onDegraded} returns. + */ + onDegraded(...args: Parameters) { + return this.#policy.onDegraded(...args); } /** diff --git a/packages/remote-feature-flag-controller/src/remote-feature-flag-controller.ts b/packages/remote-feature-flag-controller/src/remote-feature-flag-controller.ts index 9e370998baa..da206fc1219 100644 --- a/packages/remote-feature-flag-controller/src/remote-feature-flag-controller.ts +++ b/packages/remote-feature-flag-controller/src/remote-feature-flag-controller.ts @@ -98,7 +98,7 @@ export class RemoteFeatureFlagController extends BaseController< #disabled: boolean; - #clientConfigApiService: AbstractClientConfigApiService; + readonly #clientConfigApiService: AbstractClientConfigApiService; #inProgressFlagUpdate?: Promise; @@ -193,9 +193,7 @@ export class RemoteFeatureFlagController extends BaseController< * @private */ async #updateCache(remoteFeatureFlags: FeatureFlags) { - const processedRemoteFeatureFlags = await this.#processRemoteFeatureFlags( - remoteFeatureFlags, - ); + const processedRemoteFeatureFlags = await this.#processRemoteFeatureFlags(remoteFeatureFlags); this.update(() => { return { remoteFeatureFlags: processedRemoteFeatureFlags, diff --git a/yarn.lock b/yarn.lock index c48d3f85a7d..bf510aad60a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2447,7 +2447,6 @@ __metadata: async-mutex: "npm:^0.5.0" bitcoin-address-validation: "npm:^2.2.3" bn.js: "npm:^5.2.1" - cockatiel: "npm:^3.1.2" deepmerge: "npm:^4.2.2" immer: "npm:^9.0.6" jest: "npm:^27.5.1" @@ -3799,7 +3798,6 @@ __metadata: "@metamask/controller-utils": "npm:^11.4.5" "@metamask/utils": "npm:^11.0.1" "@types/jest": "npm:^27.4.1" - cockatiel: "npm:^3.1.2" deepmerge: "npm:^4.2.2" jest: "npm:^27.5.1" nock: "npm:^13.3.1"