diff --git a/src/adapter/basic.ts b/src/adapter/basic.ts index 2cfe01bd..7970fc86 100644 --- a/src/adapter/basic.ts +++ b/src/adapter/basic.ts @@ -84,6 +84,7 @@ export class Adapter< this.config.initialize() this.normalizeEndpointNames() + this.filterEndpoints() this.calculateRateLimitAllocations() this.shutdownNotifier = new EventEmitter() } @@ -184,6 +185,47 @@ export class Adapter< } } + /** + * Filters the adapter's endpoints based on the ENABLED_ENDPOINTS setting. + * If ENABLED_ENDPOINTS is not set, all endpoints are loaded. + * If ENABLED_ENDPOINTS is set, only the specified endpoints are loaded. + * Throws an error if none of the specified endpoints match. + */ + private filterEndpoints() { + const enabledEndpoints = this.config.settings.ENABLED_ENDPOINTS + if (!enabledEndpoints) { + return + } + + // Normalize the ENABLED_ENDPOINTS values + const enabledSet = new Set(enabledEndpoints.split(',').map((name) => name.trim().toLowerCase())) + + const allNames = this.endpoints.map((e) => e.name) + this.endpoints = this.endpoints.filter((e) => enabledSet.has(e.name)) + + const excluded = allNames.filter((name) => !enabledSet.has(name)) + if (excluded.length) { + logger.info( + `ENABLED_ENDPOINTS is set — loaded [${this.endpoints.map((e) => e.name).join(', ')}], excluded [${excluded.join(', ')}]`, + ) + } + + // Throws error if none of the specified endpoints match + if (this.endpoints.length === 0) { + throw new Error( + `ENABLED_ENDPOINTS is set to "${enabledEndpoints}" but none of the adapter endpoints match. Available: [${allNames.join(', ')}]`, + ) + } + + // Clear default endpoint if it is not included in the enabled endpoints + if (this.defaultEndpoint && !enabledSet.has(this.defaultEndpoint)) { + logger.warn( + `Default endpoint "${this.defaultEndpoint}" was excluded by ENABLED_ENDPOINTS, clearing it`, + ) + this.defaultEndpoint = undefined + } + } + /** * This function will take an adapter structure and go through each endpoint, calculating * each one's allocation of the total rate limits that are set for the adapter as a whole. diff --git a/src/config/index.ts b/src/config/index.ts index f1fa2814..00f69b78 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -399,6 +399,12 @@ export const BaseSettingsDefinition = { description: 'Whether to use enableCompositeTransport parameter in AdapterEndpoint', default: false, }, + ENABLED_ENDPOINTS: { + description: + 'Comma-separated list of endpoint names to load. If not set, all endpoints are loaded. Useful for local development to reduce log noise.', + type: 'string', + sensitive: false, + }, } as const satisfies SettingsDefinitionMap export const buildAdapterSettings = < diff --git a/test/adapter/filter-endpoints.test.ts b/test/adapter/filter-endpoints.test.ts new file mode 100644 index 00000000..151a3c36 --- /dev/null +++ b/test/adapter/filter-endpoints.test.ts @@ -0,0 +1,68 @@ +import test from 'ava' +import { Adapter, AdapterEndpoint } from '../../src/adapter' +import { LoggerFactoryProvider } from '../../src/util/logger' +import { NopTransport } from '../../src/util/testing-utils' + +test.before(() => { + LoggerFactoryProvider.set() +}) + +const makeEndpoint = (name: string) => new AdapterEndpoint({ name, transport: new NopTransport() }) + +test.afterEach(() => { + delete process.env['ENABLED_ENDPOINTS'] +}) + +test('loads all endpoints when ENABLED_ENDPOINTS is not set', (t) => { + const adapter = new Adapter({ + name: 'TEST', + endpoints: [makeEndpoint('price'), makeEndpoint('volume')], + }) + t.is(adapter.endpoints.length, 2) +}) + +test('filters to only the named endpoints', (t) => { + process.env['ENABLED_ENDPOINTS'] = 'price' + const adapter = new Adapter({ + name: 'TEST', + endpoints: [makeEndpoint('price'), makeEndpoint('volume')], + }) + t.is(adapter.endpoints.length, 1) + t.is(adapter.endpoints[0].name, 'price') +}) + +test('trims whitespace and lowercases names in ENABLED_ENDPOINTS', (t) => { + process.env['ENABLED_ENDPOINTS'] = ' Price , VOLUME ' + const adapter = new Adapter({ + name: 'TEST', + endpoints: [makeEndpoint('price'), makeEndpoint('volume')], + }) + t.is(adapter.endpoints.length, 2) +}) + +test('throws when no endpoints match ENABLED_ENDPOINTS', (t) => { + process.env['ENABLED_ENDPOINTS'] = 'nonexistent' + t.throws(() => new Adapter({ name: 'TEST', endpoints: [makeEndpoint('price')] }), { + message: /nonexistent/, + }) +}) + +test('clears defaultEndpoint when it is excluded by ENABLED_ENDPOINTS', (t) => { + process.env['ENABLED_ENDPOINTS'] = 'volume' + const adapter = new Adapter({ + name: 'TEST', + defaultEndpoint: 'price', + endpoints: [makeEndpoint('price'), makeEndpoint('volume')], + }) + t.is(adapter.defaultEndpoint, undefined) +}) + +test('keeps defaultEndpoint when it is included in ENABLED_ENDPOINTS', (t) => { + process.env['ENABLED_ENDPOINTS'] = 'price' + const adapter = new Adapter({ + name: 'TEST', + defaultEndpoint: 'price', + endpoints: [makeEndpoint('price'), makeEndpoint('volume')], + }) + t.is(adapter.defaultEndpoint, 'price') +})