diff --git a/packages/chomp-api-service/CHANGELOG.md b/packages/chomp-api-service/CHANGELOG.md index d5b8a0690c..b8382a4fc4 100644 --- a/packages/chomp-api-service/CHANGELOG.md +++ b/packages/chomp-api-service/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Add `ChompApiService.setBaseUrl` to update the base URL after construction. The base URL is now part of every query key, so requests are cached per endpoint and a request in flight against a previous endpoint can never be served in place of a result from the new one ([#9056](https://github.com/MetaMask/core/pull/9056)) + ### Changed - Bump `@metamask/controller-utils` from `^12.0.0` to `^12.1.0` ([#8774](https://github.com/MetaMask/core/pull/8774)) diff --git a/packages/chomp-api-service/src/chomp-api-service.test.ts b/packages/chomp-api-service/src/chomp-api-service.test.ts index 0cab2c7e02..3e3488f74b 100644 --- a/packages/chomp-api-service/src/chomp-api-service.test.ts +++ b/packages/chomp-api-service/src/chomp-api-service.test.ts @@ -618,6 +618,62 @@ describe('ChompApiService', () => { expect(attempts).toBe(DEFAULT_MAX_RETRIES + 1); }); }); + + describe('setBaseUrl', () => { + const NEW_BASE_URL = 'https://api.chomp.other.example.com'; + + it('routes subsequent requests to the new base URL', async () => { + nock(NEW_BASE_URL).get('/v1/account-upgrade/0xabc').reply(200, []); + const { service } = createService(); + + service.setBaseUrl(NEW_BASE_URL); + const result = await service.getUpgrades('0xabc'); + + expect(result).toStrictEqual([]); + }); + + it('caches per endpoint, so a read after the URL changes is not served from the previous endpoint cache', async () => { + // The base URL is part of the query key, so the entry cached against the + // original endpoint and the read issued after the switch are distinct + // queries: the second read misses the cache and hits the new host rather + // than being served the previous endpoint's data. + nock(BASE_URL).get('/v1/account-upgrade/0xabc').reply(200, []); + const { service } = createService(); + await service.getUpgrades('0xabc'); // fully cached under the original URL + + const newScope = nock(NEW_BASE_URL) + .get('/v1/account-upgrade/0xabc') + .reply(200, []); + service.setBaseUrl(NEW_BASE_URL); + await service.getUpgrades('0xabc'); + + expect(newScope.isDone()).toBe(true); + }); + + it('keeps a request in flight against the old endpoint on its own cache entry, so it cannot satisfy a read against the new one', async () => { + const oldScope = nock(BASE_URL) + .get('/v1/account-upgrade/0xabc') + .delay(20) + .reply(200, []); + const newScope = nock(NEW_BASE_URL) + .get('/v1/account-upgrade/0xabc') + .reply(200, []); + const { service } = createService(); + + // Let the first request get past the async auth-header step and actually + // hit the network against BASE_URL before switching, so it is genuinely + // in flight against the old endpoint when the new read is issued. + const inFlight = service.getUpgrades('0xabc'); + await new Promise((resolve) => setTimeout(resolve, 0)); + + service.setBaseUrl(NEW_BASE_URL); + await service.getUpgrades('0xabc'); + await inFlight; + + expect(oldScope.isDone()).toBe(true); + expect(newScope.isDone()).toBe(true); + }); + }); }); /** diff --git a/packages/chomp-api-service/src/chomp-api-service.ts b/packages/chomp-api-service/src/chomp-api-service.ts index b04c683041..ad0c9928db 100644 --- a/packages/chomp-api-service/src/chomp-api-service.ts +++ b/packages/chomp-api-service/src/chomp-api-service.ts @@ -263,7 +263,7 @@ export class ChompApiService extends BaseDataService< typeof serviceName, ChompApiServiceMessenger > { - readonly #baseUrl: string; + #baseUrl: string; /** * Constructs a new ChompApiService. @@ -301,6 +301,22 @@ export class ChompApiService extends BaseDataService< ); } + /** + * Updates the base URL used for all subsequent CHOMP API requests. + * + * This is useful when the endpoint is sourced from a remote feature flag + * that may not have hydrated by the time the service is constructed: the + * service can be created with a fallback URL and re-pointed once the flag + * lands. The base URL is part of every query key, so requests already in + * flight against the previous endpoint occupy separate cache entries and + * can never be served in place of results from the new endpoint. + * + * @param baseUrl - The new base URL of the CHOMP API. + */ + setBaseUrl(baseUrl: string): void { + this.#baseUrl = baseUrl; + } + /** * Builds the standard headers for an authenticated CHOMP API request. * @@ -329,7 +345,7 @@ export class ChompApiService extends BaseDataService< params: AssociateAddressParams, ): Promise { const jsonResponse = await this.fetchQuery({ - queryKey: [`${this.name}:associateAddress`, params], + queryKey: [`${this.name}:associateAddress`, this.#baseUrl, params], staleTime: 0, queryFn: async () => { const headers = await this.#authHeaders(); @@ -369,7 +385,7 @@ export class ChompApiService extends BaseDataService< params: CreateUpgradeParams, ): Promise { const jsonResponse = await this.fetchQuery({ - queryKey: [`${this.name}:createUpgrade`, params], + queryKey: [`${this.name}:createUpgrade`, this.#baseUrl, params], staleTime: 0, queryFn: async () => { const headers = await this.#authHeaders(); @@ -407,7 +423,7 @@ export class ChompApiService extends BaseDataService< */ async getUpgrades(address: Hex): Promise { const jsonResponse = await this.fetchQuery({ - queryKey: [`${this.name}:getUpgrades`, address], + queryKey: [`${this.name}:getUpgrades`, this.#baseUrl, address], queryFn: async () => { const headers = await this.#authHeaders(); const response = await fetch( @@ -441,7 +457,7 @@ export class ChompApiService extends BaseDataService< params: VerifyDelegationParams, ): Promise { const jsonResponse = await this.fetchQuery({ - queryKey: [`${this.name}:verifyDelegation`, params], + queryKey: [`${this.name}:verifyDelegation`, this.#baseUrl, params], staleTime: 0, queryFn: async () => { const headers = await this.#authHeaders(); @@ -480,7 +496,7 @@ export class ChompApiService extends BaseDataService< intents: SendIntentParams[], ): Promise { const jsonResponse = await this.fetchQuery({ - queryKey: [`${this.name}:createIntents`, intents], + queryKey: [`${this.name}:createIntents`, this.#baseUrl, intents], staleTime: 0, queryFn: async () => { const headers = await this.#authHeaders(); @@ -514,7 +530,7 @@ export class ChompApiService extends BaseDataService< */ async getIntentsByAddress(address: Hex): Promise { const jsonResponse = await this.fetchQuery({ - queryKey: [`${this.name}:getIntentsByAddress`, address], + queryKey: [`${this.name}:getIntentsByAddress`, this.#baseUrl, address], queryFn: async () => { const headers = await this.#authHeaders(); const response = await fetch( @@ -549,7 +565,7 @@ export class ChompApiService extends BaseDataService< params: CreateWithdrawalParams, ): Promise { const jsonResponse = await this.fetchQuery({ - queryKey: [`${this.name}:createWithdrawal`, params], + queryKey: [`${this.name}:createWithdrawal`, this.#baseUrl, params], staleTime: 0, queryFn: async () => { const headers = await this.#authHeaders(); @@ -585,7 +601,7 @@ export class ChompApiService extends BaseDataService< */ async getServiceDetails(chainIds: Hex[]): Promise { const jsonResponse = await this.fetchQuery({ - queryKey: [`${this.name}:getServiceDetails`, chainIds], + queryKey: [`${this.name}:getServiceDetails`, this.#baseUrl, chainIds], queryFn: async () => { const headers = await this.#authHeaders(); const url = new URL('/v1/chomp', this.#baseUrl);