From f45b39ca2728a64eb5854a07f0d4650c22750724 Mon Sep 17 00:00:00 2001 From: Ty Potter Date: Tue, 18 Feb 2025 21:57:34 -0700 Subject: [PATCH 01/24] chore: rename configuration.ts to configuration-wire-types.ts --- src/client/eppo-client-with-bandits.spec.ts | 6 +++--- src/client/eppo-client.spec.ts | 6 +++--- src/client/eppo-client.ts | 6 +++--- src/client/eppo-precomputed-client.spec.ts | 2 +- src/{configuration.ts => configuration-wire-types.ts} | 0 src/http-client.ts | 2 +- src/index.ts | 10 +++++----- 7 files changed, 16 insertions(+), 16 deletions(-) rename src/{configuration.ts => configuration-wire-types.ts} (100%) diff --git a/src/client/eppo-client-with-bandits.spec.ts b/src/client/eppo-client-with-bandits.spec.ts index 440459b..18a1b40 100644 --- a/src/client/eppo-client-with-bandits.spec.ts +++ b/src/client/eppo-client-with-bandits.spec.ts @@ -12,13 +12,13 @@ import ApiEndpoints from '../api-endpoints'; import { IAssignmentEvent, IAssignmentLogger } from '../assignment-logger'; import { BanditEvaluation, BanditEvaluator } from '../bandit-evaluator'; import { IBanditEvent, IBanditLogger } from '../bandit-logger'; +import ConfigurationRequestor from '../configuration-requestor'; +import { MemoryOnlyConfigurationStore } from '../configuration-store/memory.store'; import { IConfigurationWire, IPrecomputedConfiguration, IObfuscatedPrecomputedConfigurationResponse, -} from '../configuration'; -import ConfigurationRequestor from '../configuration-requestor'; -import { MemoryOnlyConfigurationStore } from '../configuration-store/memory.store'; +} from '../configuration-wire-types'; import { Evaluator, FlagEvaluation } from '../evaluator'; import { AllocationEvaluationCode, diff --git a/src/client/eppo-client.spec.ts b/src/client/eppo-client.spec.ts index 707d567..553767c 100644 --- a/src/client/eppo-client.spec.ts +++ b/src/client/eppo-client.spec.ts @@ -14,13 +14,13 @@ import { validateTestAssignments, } from '../../test/testHelpers'; import { IAssignmentLogger } from '../assignment-logger'; +import { IConfigurationStore } from '../configuration-store/configuration-store'; +import { MemoryOnlyConfigurationStore } from '../configuration-store/memory.store'; import { IConfigurationWire, IObfuscatedPrecomputedConfigurationResponse, ObfuscatedPrecomputedConfigurationResponse, -} from '../configuration'; -import { IConfigurationStore } from '../configuration-store/configuration-store'; -import { MemoryOnlyConfigurationStore } from '../configuration-store/memory.store'; +} from '../configuration-wire-types'; import { MAX_EVENT_QUEUE_SIZE, DEFAULT_POLL_INTERVAL_MS, POLL_JITTER_PCT } from '../constants'; import { decodePrecomputedFlag } from '../decoding'; import { Flag, ObfuscatedFlag, VariationType } from '../interfaces'; diff --git a/src/client/eppo-client.ts b/src/client/eppo-client.ts index 20e8861..a15e9d0 100644 --- a/src/client/eppo-client.ts +++ b/src/client/eppo-client.ts @@ -14,14 +14,14 @@ import { AssignmentCache } from '../cache/abstract-assignment-cache'; import { LRUInMemoryAssignmentCache } from '../cache/lru-in-memory-assignment-cache'; import { NonExpiringInMemoryAssignmentCache } from '../cache/non-expiring-in-memory-cache-assignment'; import { TLRUInMemoryAssignmentCache } from '../cache/tlru-in-memory-assignment-cache'; +import ConfigurationRequestor from '../configuration-requestor'; +import { IConfigurationStore } from '../configuration-store/configuration-store'; import { IConfigurationWire, ConfigurationWireV1, IPrecomputedConfiguration, PrecomputedConfiguration, -} from '../configuration'; -import ConfigurationRequestor from '../configuration-requestor'; -import { IConfigurationStore } from '../configuration-store/configuration-store'; +} from '../configuration-wire-types'; import { DEFAULT_INITIAL_CONFIG_REQUEST_RETRIES, DEFAULT_POLL_CONFIG_REQUEST_RETRIES, diff --git a/src/client/eppo-precomputed-client.spec.ts b/src/client/eppo-precomputed-client.spec.ts index 02f265d..029ad35 100644 --- a/src/client/eppo-precomputed-client.spec.ts +++ b/src/client/eppo-precomputed-client.spec.ts @@ -11,9 +11,9 @@ import { ensureContextualSubjectAttributes, ensureNonContextualSubjectAttributes, } from '../attributes'; -import { IPrecomputedConfigurationResponse } from '../configuration'; import { IConfigurationStore } from '../configuration-store/configuration-store'; import { MemoryOnlyConfigurationStore } from '../configuration-store/memory.store'; +import { IPrecomputedConfigurationResponse } from '../configuration-wire-types'; import { DEFAULT_POLL_INTERVAL_MS, MAX_EVENT_QUEUE_SIZE, POLL_JITTER_PCT } from '../constants'; import FetchHttpClient from '../http-client'; import { diff --git a/src/configuration.ts b/src/configuration-wire-types.ts similarity index 100% rename from src/configuration.ts rename to src/configuration-wire-types.ts diff --git a/src/http-client.ts b/src/http-client.ts index 76f2f5b..61cace5 100644 --- a/src/http-client.ts +++ b/src/http-client.ts @@ -1,5 +1,5 @@ import ApiEndpoints from './api-endpoints'; -import { IObfuscatedPrecomputedConfigurationResponse } from './configuration'; +import { IObfuscatedPrecomputedConfigurationResponse } from './configuration-wire-types'; import { BanditParameters, BanditReference, diff --git a/src/index.ts b/src/index.ts index 85489a5..df1770b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -25,11 +25,6 @@ import EppoPrecomputedClient, { PrecomputedFlagsRequestParameters, Subject, } from './client/eppo-precomputed-client'; -import { - IConfigurationWire, - IObfuscatedPrecomputedConfigurationResponse, - IPrecomputedConfigurationResponse, -} from './configuration'; import FlagConfigRequestor from './configuration-requestor'; import { IConfigurationStore, @@ -38,6 +33,11 @@ import { } from './configuration-store/configuration-store'; import { HybridConfigurationStore } from './configuration-store/hybrid.store'; import { MemoryStore, MemoryOnlyConfigurationStore } from './configuration-store/memory.store'; +import { + IConfigurationWire, + IObfuscatedPrecomputedConfigurationResponse, + IPrecomputedConfigurationResponse, +} from './configuration-wire-types'; import * as constants from './constants'; import { decodePrecomputedFlag } from './decoding'; import BatchEventProcessor from './events/batch-event-processor'; From 704d2e5d779a28a5cef28b4217b868b2d8c18668 Mon Sep 17 00:00:00 2001 From: Ty Potter Date: Tue, 18 Feb 2025 22:35:11 -0700 Subject: [PATCH 02/24] chore: deprecate fetchFlagConfigurations in favour of startPolling --- src/client/eppo-client.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/client/eppo-client.ts b/src/client/eppo-client.ts index a15e9d0..b593478 100644 --- a/src/client/eppo-client.ts +++ b/src/client/eppo-client.ts @@ -192,6 +192,18 @@ export default class EppoClient { this.isObfuscated = isObfuscated; } + /** + * Attempts to fetch Eppo configuration and start polling, depending on the `FlagConfigurationRequestParameters` + * provided. + * @return a promise that resolves when the poller has completed the first fetch and/or begun to periodically poll. + */ + async startPolling() { + return this.fetchFlagConfigurations(); + } + + /** + * @deprecated use startPolling instead. + */ async fetchFlagConfigurations() { if (!this.configurationRequestParameters) { throw new Error( From 0e9c2f5d9c73fd07fbd3b61df80d72577e284c16 Mon Sep 17 00:00:00 2001 From: Ty Potter Date: Wed, 19 Feb 2025 09:46:02 -0700 Subject: [PATCH 03/24] feat: Wrap configuration store access in new Configuration object --- src/configuration.spec.ts | 283 ++++++++++++++++++++++++++++++++++++++ src/configuration.ts | 150 ++++++++++++++++++++ src/constants.ts | 3 + 3 files changed, 436 insertions(+) create mode 100644 src/configuration.spec.ts create mode 100644 src/configuration.ts diff --git a/src/configuration.spec.ts b/src/configuration.spec.ts new file mode 100644 index 0000000..b2c757f --- /dev/null +++ b/src/configuration.spec.ts @@ -0,0 +1,283 @@ +import { StoreBackedConfiguration } from './configuration'; +import { IConfigurationStore } from './configuration-store/configuration-store'; +import { BanditParameters, BanditVariation, Environment, Flag, ObfuscatedFlag } from './interfaces'; + +describe('StoreBackedConfiguration', () => { + let mockFlagStore: jest.Mocked>; + let mockBanditVariationStore: jest.Mocked>; + let mockBanditModelStore: jest.Mocked>; + + beforeEach(() => { + mockFlagStore = { + get: jest.fn(), + getKeys: jest.fn(), + entries: jest.fn(), + setEntries: jest.fn(), + setEnvironment: jest.fn(), + setConfigFetchedAt: jest.fn(), + setConfigPublishedAt: jest.fn(), + setFormat: jest.fn(), + getConfigFetchedAt: jest.fn(), + getConfigPublishedAt: jest.fn(), + getEnvironment: jest.fn(), + getFormat: jest.fn(), + salt: undefined, + init: jest.fn(), + isInitialized: jest.fn(), + isExpired: jest.fn(), + }; + + mockBanditVariationStore = { + get: jest.fn(), + getKeys: jest.fn(), + entries: jest.fn(), + setEntries: jest.fn(), + setEnvironment: jest.fn(), + setConfigFetchedAt: jest.fn(), + setConfigPublishedAt: jest.fn(), + setFormat: jest.fn(), + getConfigFetchedAt: jest.fn(), + getConfigPublishedAt: jest.fn(), + getEnvironment: jest.fn(), + getFormat: jest.fn(), + salt: undefined, + init: jest.fn(), + isInitialized: jest.fn(), + isExpired: jest.fn(), + }; + + mockBanditModelStore = { + get: jest.fn(), + getKeys: jest.fn(), + entries: jest.fn(), + setEntries: jest.fn(), + setEnvironment: jest.fn(), + setConfigFetchedAt: jest.fn(), + setConfigPublishedAt: jest.fn(), + setFormat: jest.fn(), + getConfigFetchedAt: jest.fn(), + getConfigPublishedAt: jest.fn(), + getEnvironment: jest.fn(), + getFormat: jest.fn(), + salt: undefined, + init: jest.fn(), + isInitialized: jest.fn(), + isExpired: jest.fn(), + }; + }); + + describe('hydrateConfigurationStores', () => { + it('should hydrate flag store and return true if updates occurred', async () => { + const config = new StoreBackedConfiguration( + mockFlagStore, + mockBanditVariationStore, + mockBanditModelStore, + ); + + mockFlagStore.setEntries.mockResolvedValue(true); + mockBanditVariationStore.setEntries.mockResolvedValue(true); + mockBanditModelStore.setEntries.mockResolvedValue(true); + + const result = await config.hydrateConfigurationStores( + { + entries: { testFlag: { key: 'test' } as Flag }, + environment: { name: 'test' }, + createdAt: '2024-01-01', + format: 'SERVER', + }, + { + entries: { testVar: [] }, + environment: { name: 'test' }, + createdAt: '2024-01-01', + format: 'SERVER', + }, + { + entries: { testBandit: {} as BanditParameters }, + environment: { name: 'test' }, + createdAt: '2024-01-01', + format: 'SERVER', + }, + ); + + expect(result).toBe(true); + expect(mockFlagStore.setEntries).toHaveBeenCalled(); + expect(mockBanditVariationStore.setEntries).toHaveBeenCalled(); + expect(mockBanditModelStore.setEntries).toHaveBeenCalled(); + }); + }); + + describe('getFlag', () => { + it('should return flag when it exists', () => { + const config = new StoreBackedConfiguration(mockFlagStore); + const mockFlag: Flag = { key: 'test-flag' } as Flag; + mockFlagStore.get.mockReturnValue(mockFlag); + + const result = config.getFlag('test-flag'); + expect(result).toEqual(mockFlag); + }); + + it('should return null when flag does not exist', () => { + const config = new StoreBackedConfiguration(mockFlagStore); + mockFlagStore.get.mockReturnValue(null); + + const result = config.getFlag('non-existent'); + expect(result).toBeNull(); + }); + }); + + describe('getFlagVariationBandit', () => { + it('should return bandit parameters when variation exists', () => { + const config = new StoreBackedConfiguration( + mockFlagStore, + mockBanditVariationStore, + mockBanditModelStore, + ); + + const mockVariations: BanditVariation[] = [ + { + key: 'bandit-1', + variationValue: 'var-1', + flagKey: 'test-flag', + variationKey: 'test-variation', + }, + ]; + const mockBanditParams: BanditParameters = {} as BanditParameters; + + mockBanditVariationStore.get.mockReturnValue(mockVariations); + mockBanditModelStore.get.mockReturnValue(mockBanditParams); + + const result = config.getFlagVariationBandit('test-flag', 'var-1'); + expect(result).toEqual(mockBanditParams); + }); + + it('should return null when variation does not exist', () => { + const config = new StoreBackedConfiguration( + mockFlagStore, + mockBanditVariationStore, + mockBanditModelStore, + ); + + mockBanditVariationStore.get.mockReturnValue([]); + + const result = config.getFlagVariationBandit('test-flag', 'non-existent'); + expect(result).toBeNull(); + }); + }); + + describe('getFlagConfigDetails', () => { + it('should return config details with default values when store returns null', () => { + const config = new StoreBackedConfiguration(mockFlagStore); + mockFlagStore.getConfigFetchedAt.mockReturnValue(null); + mockFlagStore.getConfigPublishedAt.mockReturnValue(null); + mockFlagStore.getEnvironment.mockReturnValue(null); + mockFlagStore.getFormat.mockReturnValue(null); + + const result = config.getFlagConfigDetails(); + expect(result).toEqual({ + configFetchedAt: '', + configPublishedAt: '', + configEnvironment: { name: '' }, + configFormat: '', + }); + }); + + it('should return actual config details when available', () => { + const config = new StoreBackedConfiguration(mockFlagStore); + const mockEnvironment: Environment = { name: 'test' }; + + mockFlagStore.getConfigFetchedAt.mockReturnValue('2024-01-01T00:00:00Z'); + mockFlagStore.getConfigPublishedAt.mockReturnValue('2024-01-01T00:00:00Z'); + mockFlagStore.getEnvironment.mockReturnValue(mockEnvironment); + mockFlagStore.getFormat.mockReturnValue('SERVER'); + + const result = config.getFlagConfigDetails(); + expect(result).toEqual({ + configFetchedAt: '2024-01-01T00:00:00Z', + configPublishedAt: '2024-01-01T00:00:00Z', + configEnvironment: mockEnvironment, + configFormat: 'SERVER', + }); + }); + }); + + describe('getBanditVariations', () => { + it('should return variations when they exist', () => { + const config = new StoreBackedConfiguration(mockFlagStore, mockBanditVariationStore); + const mockVariations: BanditVariation[] = [ + { + key: 'bandit-1', + variationValue: 'var-1', + flagKey: 'test-flag', + variationKey: 'test-variation', + }, + ]; + mockBanditVariationStore.get.mockReturnValue(mockVariations); + + const result = config.getBanditVariations('test-flag'); + expect(result).toEqual(mockVariations); + }); + + it('should return empty array when variations do not exist', () => { + const config = new StoreBackedConfiguration(mockFlagStore, mockBanditVariationStore); + mockBanditVariationStore.get.mockReturnValue(null); + + const result = config.getBanditVariations('test-flag'); + expect(result).toEqual([]); + }); + }); + + describe('getFlagKeys', () => { + it('should return flag keys from store', () => { + const config = new StoreBackedConfiguration(mockFlagStore); + const mockKeys = ['flag-1', 'flag-2']; + mockFlagStore.getKeys.mockReturnValue(mockKeys); + + const result = config.getFlagKeys(); + expect(result).toEqual(mockKeys); + }); + }); + + describe('getFlags', () => { + it('should return all flags from store', () => { + const config = new StoreBackedConfiguration(mockFlagStore); + const mockFlags: Record = { + 'flag-1': { key: 'flag-1' } as Flag, + 'flag-2': { key: 'flag-2' } as Flag, + }; + mockFlagStore.entries.mockReturnValue(mockFlags); + + const result = config.getFlags(); + expect(result).toEqual(mockFlags); + }); + }); + + describe('isObfuscated', () => { + it('should return true for CLIENT format', () => { + const config = new StoreBackedConfiguration(mockFlagStore); + mockFlagStore.getFormat.mockReturnValue('CLIENT'); + + expect(config.isObfuscated()).toBe(true); + }); + + it('should return true for PRECOMPUTED format', () => { + const config = new StoreBackedConfiguration(mockFlagStore); + mockFlagStore.getFormat.mockReturnValue('PRECOMPUTED'); + + expect(config.isObfuscated()).toBe(true); + }); + + it('should return false for SERVER format', () => { + const config = new StoreBackedConfiguration(mockFlagStore); + mockFlagStore.getFormat.mockReturnValue('SERVER'); + + expect(config.isObfuscated()).toBe(false); + }); + + it('should return false when format is undefined', () => { + const config = new StoreBackedConfiguration(mockFlagStore); + mockFlagStore.getFormat.mockReturnValue(null); + + expect(config.isObfuscated()).toBe(false); + }); + }); +}); diff --git a/src/configuration.ts b/src/configuration.ts new file mode 100644 index 0000000..e99117c --- /dev/null +++ b/src/configuration.ts @@ -0,0 +1,150 @@ +import { IConfigurationStore } from './configuration-store/configuration-store'; +import { OBFUSCATED_FORMATS } from './constants'; +import { + BanditParameters, + BanditVariation, + ConfigDetails, + Environment, + Flag, + IObfuscatedPrecomputedBandit, + ObfuscatedFlag, + PrecomputedFlag, +} from './interfaces'; + +export interface IConfiguration { + getFlag(key: string): Flag | ObfuscatedFlag | null; + getFlags(): Record; + getFlagVariationBandit(flagKey: string, variationValue: string): BanditParameters | null; + getBandit(key: string): BanditParameters | null; + getFlagConfigDetails(): ConfigDetails; + getBanditVariations(flagKey: string): BanditVariation[]; + getFlagKeys(): string[]; + isObfuscated(): boolean; +} + +type Entry = + | Flag + | BanditVariation[] + | BanditParameters + | PrecomputedFlag + | IObfuscatedPrecomputedBandit; + +export type ConfigStoreHydrationPacket = { + entries: Record; + environment: Environment; + createdAt: string; + format: string; + salt?: string; +}; + +export class StoreBackedConfiguration implements IConfiguration { + constructor( + private readonly flagConfigurationStore: IConfigurationStore, + private readonly banditVariationConfigurationStore?: IConfigurationStore< + BanditVariation[] + > | null, + private readonly banditModelConfigurationStore?: IConfigurationStore | null, + ) {} + + public async hydrateConfigurationStores( + flagConfig: ConfigStoreHydrationPacket, + banditVariationConfig?: ConfigStoreHydrationPacket, + banditModelConfig?: ConfigStoreHydrationPacket, + ) { + const didUpdateFlags = await StoreBackedConfiguration.hydrateConfigurationStore( + this.flagConfigurationStore, + flagConfig, + ); + const promises: Promise[] = []; + if (this.banditVariationConfigurationStore && banditVariationConfig) { + promises.push( + StoreBackedConfiguration.hydrateConfigurationStore( + this.banditVariationConfigurationStore, + banditVariationConfig, + ), + ); + } + if (this.banditModelConfigurationStore && banditModelConfig) { + promises.push( + StoreBackedConfiguration.hydrateConfigurationStore( + this.banditModelConfigurationStore, + banditModelConfig, + ), + ); + } + await Promise.all(promises); + return didUpdateFlags; + } + + private static async hydrateConfigurationStore( + configurationStore: IConfigurationStore | null, + response: { + entries: Record; + environment: Environment; + createdAt: string; + format: string; + salt?: string; + }, + ): Promise { + if (configurationStore) { + const didUpdate = await configurationStore.setEntries(response.entries); + if (didUpdate) { + configurationStore.setEnvironment(response.environment); + configurationStore.setConfigFetchedAt(new Date().toISOString()); + configurationStore.setConfigPublishedAt(response.createdAt); + configurationStore.setFormat(response.format); + configurationStore.salt = response.salt; + } + return didUpdate; + } + return false; + } + + getBandit(key: string): BanditParameters | null { + return this.banditModelConfigurationStore?.get(key) ?? null; + } + + getFlagVariationBandit(flagKey: string, variationValue: string): BanditParameters | null { + const banditVariations = this.banditVariationConfigurationStore?.get(flagKey); + const banditKey = banditVariations?.find( + (banditVariation) => banditVariation.variationValue === variationValue, + )?.key; + + if (banditKey) { + // Retrieve the model parameters for the bandit + return this.getBandit(banditKey); + } + return null; + } + + getFlag(key: string): Flag | ObfuscatedFlag | null { + return this.flagConfigurationStore.get(key) ?? null; + } + + getFlagConfigDetails(): ConfigDetails { + return { + configFetchedAt: this.flagConfigurationStore.getConfigFetchedAt() ?? '', + configPublishedAt: this.flagConfigurationStore.getConfigPublishedAt() ?? '', + configEnvironment: this.flagConfigurationStore.getEnvironment() ?? { + name: '', + }, + configFormat: this.flagConfigurationStore.getFormat() ?? '', + }; + } + + getBanditVariations(flagKey: string): BanditVariation[] { + return this.banditVariationConfigurationStore?.get(flagKey) ?? []; + } + + getFlagKeys(): string[] { + return this.flagConfigurationStore.getKeys(); + } + + getFlags(): Record { + return this.flagConfigurationStore.entries(); + } + + isObfuscated(): boolean { + return OBFUSCATED_FORMATS.includes(this.getFlagConfigDetails().configFormat ?? 'SERVER'); + } +} diff --git a/src/constants.ts b/src/constants.ts index cac895f..7af2ee4 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,3 +1,5 @@ +import { FormatEnum } from './interfaces'; + export const DEFAULT_REQUEST_TIMEOUT_MS = 5000; export const REQUEST_TIMEOUT_MILLIS = DEFAULT_REQUEST_TIMEOUT_MS; // for backwards compatibility export const DEFAULT_POLL_INTERVAL_MS = 30000; @@ -15,3 +17,4 @@ export const NULL_SENTINEL = 'EPPO_NULL'; export const MAX_EVENT_QUEUE_SIZE = 100; export const BANDIT_ASSIGNMENT_SHARDS = 10000; export const DEFAULT_TLRU_TTL_MS = 600_000; +export const OBFUSCATED_FORMATS: string[] = [FormatEnum.CLIENT, FormatEnum.PRECOMPUTED]; From 8b11bb4593f18c0594382f475925bf466e4b3323 Mon Sep 17 00:00:00 2001 From: Ty Potter Date: Wed, 19 Feb 2025 11:40:56 -0700 Subject: [PATCH 04/24] feat: ConfigurationRequestor.getConfiguration --- src/configuration-requestor.spec.ts | 216 ++++++++++++++++++++++++++++ src/configuration-requestor.ts | 58 +++++--- 2 files changed, 254 insertions(+), 20 deletions(-) diff --git a/src/configuration-requestor.spec.ts b/src/configuration-requestor.spec.ts index 31d7f76..f324265 100644 --- a/src/configuration-requestor.spec.ts +++ b/src/configuration-requestor.spec.ts @@ -6,6 +6,7 @@ import { } from '../test/testHelpers'; import ApiEndpoints from './api-endpoints'; +import { StoreBackedConfiguration } from './configuration'; import ConfigurationRequestor from './configuration-requestor'; import { IConfigurationStore } from './configuration-store/configuration-store'; import { MemoryOnlyConfigurationStore } from './configuration-store/memory.store'; @@ -426,4 +427,219 @@ describe('ConfigurationRequestor', () => { }); }); }); + + describe('with mocked response', () => { + const response = { + environment: { + name: 'Test', + }, + createdAt: '2024-01-01', + format: 'SERVER', + flags: { + test_flag: { + key: 'test_flag', + enabled: true, + variationType: 'STRING', + variations: { + bandit: { + key: 'bandit', + value: 'bandit', + }, + }, + }, + }, + banditReferences: { + bandit: { + modelVersion: '123', + flagVariations: [ + { + key: 'bandit', + flagKey: 'test_flag', + allocationKey: 'analysis', + variationKey: 'bandit', + variationValue: 'bandit', + }, + ], + }, + }, + }; + const banditResponse = { + updatedAt: '2023-09-13T04:52:06.462Z', + environment: { + name: 'Test', + }, + bandits: { + bandit: { + banditKey: 'bandit', + modelName: 'falcon', + updatedAt: '2023-09-13T04:52:06.462Z', + modelVersion: '123', + }, + }, + }; + let fetchSpy: jest.Mock; + beforeAll(() => { + fetchSpy = jest.fn((req) => { + return Promise.resolve({ + ok: true, + status: 200, + json: () => + Promise.resolve(req.includes('flag-config/v1/bandits') ? banditResponse : response), + }); + }) as jest.Mock; + global.fetch = fetchSpy; + }); + afterEach(() => { + jest.clearAllMocks(); + }); + afterAll(() => { + jest.restoreAllMocks(); + }); + + describe('getConfiguration', () => { + it('should return an empty configuration instance before a config has been loaded', async () => { + const requestor = new ConfigurationRequestor( + httpClient, + flagStore, + banditVariationStore, + banditModelStore, + ); + + const config = requestor.getConfiguration(); + expect(config).toBeInstanceOf(StoreBackedConfiguration); + expect(config.getFlagKeys()).toEqual([]); + }); + + it('should return a populated configuration instance', async () => { + const requestor = new ConfigurationRequestor( + httpClient, + flagStore, + banditVariationStore, + banditModelStore, + ); + + await requestor.fetchAndStoreConfigurations(); + + const config = requestor.getConfiguration(); + expect(config).toBeInstanceOf(StoreBackedConfiguration); + expect(config.getFlagKeys()).toEqual(['test_flag']); + }); + }); + + describe('fetchAndStoreConfigurations', () => { + it('should update configuration with flag data', async () => { + const requestor = new ConfigurationRequestor( + httpClient, + flagStore, + banditVariationStore, + banditModelStore, + ); + const config = requestor.getConfiguration(); + + await requestor.fetchAndStoreConfigurations(); + + expect(config.getFlagKeys()).toEqual(['test_flag']); + expect(config.getFlagConfigDetails()).toEqual({ + configEnvironment: { name: 'Test' }, + configFetchedAt: expect.any(String), + configFormat: 'SERVER', + configPublishedAt: '2024-01-01', + }); + }); + + it('should update configuration with bandit data when present', async () => { + const requestor = new ConfigurationRequestor( + httpClient, + flagStore, + banditVariationStore, + banditModelStore, + ); + const config = requestor.getConfiguration(); + + await requestor.fetchAndStoreConfigurations(); + + // Verify flag configuration + expect(config.getFlagKeys()).toEqual(['test_flag']); + + // Verify bandit variation configuration + // expect(banditVariationDetails.entries).toEqual({ + // 'test_flag': [ + // { + // flagKey: 'test_flag', + // variationId: 'variation-1', + // // Add other expected properties based on your mock data + // } + // ] + // }); + // expect(banditVariationDetails.environment).toBe('test-env'); + // expect(banditVariationDetails.configFormat).toBe('SERVER'); + + // Verify bandit model configuration + const banditVariations = config.getBanditVariations('test_flag'); + expect(banditVariations).toEqual([ + { + allocationKey: 'analysis', + flagKey: 'test_flag', + key: 'bandit', + variationKey: 'bandit', + variationValue: 'bandit', + }, + ]); + + const banditKey = banditVariations.at(0)?.key; + + expect(banditKey).toEqual('bandit'); + if (!banditKey) { + fail('bandit Key null, appeasing typescript'); + } + const banditModelDetails = config.getBandit(banditKey); + expect(banditModelDetails).toEqual({ + banditKey: 'bandit', + modelName: 'falcon', + modelVersion: '123', + updatedAt: '2023-09-13T04:52:06.462Z', + // Add other expected properties based on your mock data + }); + }); + + it('should not fetch bandit parameters if model versions are already loaded', async () => { + const requestor = new ConfigurationRequestor( + httpClient, + flagStore, + banditVariationStore, + banditModelStore, + ); + + const ufcResponse = { + flags: { test_flag: { key: 'test_flag', value: true } }, + banditReferences: { + bandit: { + modelVersion: 'v1', + flagVariations: [{ flagKey: 'test_flag', variationId: '1' }], + }, + }, + environment: 'test', + createdAt: '2024-01-01', + format: 'SERVER', + }; + + await requestor.fetchAndStoreConfigurations(); + // const initialFetchCount = fetchSpy.mock.calls.length; + + // Second call with same model version + // fetchSpy.mockImplementationOnce(() => + // Promise.resolve({ + // ok: true, + // status: 200, + // json: () => Promise.resolve(ufcResponse) + // }) + // ); + + await requestor.fetchAndStoreConfigurations(); + + // Should only have one additional fetch (the UFC) and not the bandit parameters + // expect(fetchSpy.mock.calls.length).toBe(initialFetchCount + 1); + }); + }); + }); }); diff --git a/src/configuration-requestor.ts b/src/configuration-requestor.ts index 14ead0b..b2cc600 100644 --- a/src/configuration-requestor.ts +++ b/src/configuration-requestor.ts @@ -1,11 +1,16 @@ +import { + ConfigStoreHydrationPacket, + IConfiguration, + StoreBackedConfiguration, +} from './configuration'; import { IConfigurationStore } from './configuration-store/configuration-store'; -import { hydrateConfigurationStore } from './configuration-store/configuration-store-utils'; import { IHttpClient } from './http-client'; import { BanditVariation, BanditParameters, Flag, BanditReference } from './interfaces'; // Requests AND stores flag configurations export default class ConfigurationRequestor { private banditModelVersions: string[] = []; + private readonly configuration: StoreBackedConfiguration; constructor( private readonly httpClient: IHttpClient, @@ -14,7 +19,17 @@ export default class ConfigurationRequestor { BanditVariation[] > | null, private readonly banditModelConfigurationStore: IConfigurationStore | null, - ) {} + ) { + this.configuration = new StoreBackedConfiguration( + this.flagConfigurationStore, + this.banditVariationConfigurationStore, + this.banditModelConfigurationStore, + ); + } + + public getConfiguration(): IConfiguration { + return this.configuration; + } async fetchAndStoreConfigurations(): Promise { const configResponse = await this.httpClient.getUniversalFlagConfiguration(); @@ -22,13 +37,15 @@ export default class ConfigurationRequestor { return; } - await hydrateConfigurationStore(this.flagConfigurationStore, { + const flagResponsePacket: ConfigStoreHydrationPacket = { entries: configResponse.flags, environment: configResponse.environment, createdAt: configResponse.createdAt, format: configResponse.format, - }); + }; + let banditVariationPacket: ConfigStoreHydrationPacket | undefined; + let banditModelPacket: ConfigStoreHydrationPacket | undefined; const flagsHaveBandits = Object.keys(configResponse.banditReferences ?? {}).length > 0; const banditStoresProvided = Boolean( this.banditVariationConfigurationStore && this.banditModelConfigurationStore, @@ -37,12 +54,12 @@ export default class ConfigurationRequestor { // Map bandit flag associations by flag key for quick lookup (instead of bandit key as provided by the UFC) const banditVariations = this.indexBanditVariationsByFlagKey(configResponse.banditReferences); - await hydrateConfigurationStore(this.banditVariationConfigurationStore, { + banditVariationPacket = { entries: banditVariations, environment: configResponse.environment, createdAt: configResponse.createdAt, format: configResponse.format, - }); + }; if ( this.requiresBanditModelConfigurationStoreUpdate( @@ -52,30 +69,31 @@ export default class ConfigurationRequestor { ) { const banditResponse = await this.httpClient.getBanditParameters(); if (banditResponse?.bandits) { - await hydrateConfigurationStore(this.banditModelConfigurationStore, { + banditModelPacket = { entries: banditResponse.bandits, environment: configResponse.environment, createdAt: configResponse.createdAt, format: configResponse.format, - }); + }; - this.banditModelVersions = this.getLoadedBanditModelVersionsFromStore( - this.banditModelConfigurationStore, - ); + this.banditModelVersions = this.getLoadedBanditModelVersions(banditResponse.bandits); } } } - } - private getLoadedBanditModelVersionsFromStore( - banditModelConfigurationStore: IConfigurationStore | null, - ): string[] { - if (banditModelConfigurationStore === null) { - return []; + if ( + await this.configuration.hydrateConfigurationStores( + flagResponsePacket, + banditVariationPacket, + banditModelPacket, + ) + ) { + // Notify that config updated. } - return Object.values(banditModelConfigurationStore.entries()).map( - (banditParam: BanditParameters) => banditParam.modelVersion, - ); + } + + private getLoadedBanditModelVersions(entries: Record): string[] { + return Object.values(entries).map((banditParam: BanditParameters) => banditParam.modelVersion); } private requiresBanditModelConfigurationStoreUpdate( From 688b2007b7f7601e2e128343fe38e70fc32a3a53 Mon Sep 17 00:00:00 2001 From: Ty Potter Date: Wed, 19 Feb 2025 12:15:12 -0700 Subject: [PATCH 05/24] chore: delegate isObfuscated check to i-config --- src/client/eppo-client.ts | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/src/client/eppo-client.ts b/src/client/eppo-client.ts index a7fa841..fd16a7a 100644 --- a/src/client/eppo-client.ts +++ b/src/client/eppo-client.ts @@ -14,6 +14,7 @@ import { AssignmentCache } from '../cache/abstract-assignment-cache'; import { LRUInMemoryAssignmentCache } from '../cache/lru-in-memory-assignment-cache'; import { NonExpiringInMemoryAssignmentCache } from '../cache/non-expiring-in-memory-cache-assignment'; import { TLRUInMemoryAssignmentCache } from '../cache/tlru-in-memory-assignment-cache'; +import { IConfiguration, StoreBackedConfiguration } from '../configuration'; import ConfigurationRequestor from '../configuration-requestor'; import { IConfigurationStore, ISyncStore } from '../configuration-store/configuration-store'; import { @@ -27,7 +28,6 @@ import { DEFAULT_POLL_CONFIG_REQUEST_RETRIES, DEFAULT_POLL_INTERVAL_MS, DEFAULT_REQUEST_TIMEOUT_MS, - OBFUSCATED_FORMATS, } from '../constants'; import { decodeFlag } from '../decoding'; import { EppoValue } from '../eppo_value'; @@ -47,7 +47,6 @@ import { BanditVariation, ConfigDetails, Flag, - FormatEnum, IPrecomputedBandit, ObfuscatedFlag, PrecomputedFlag, @@ -135,6 +134,7 @@ export default class EppoClient { private configObfuscatedCache?: boolean; private requestPoller?: IPoller; private readonly evaluator = new Evaluator(); + private configurationRequestor?: ConfigurationRequestor; constructor({ eventDispatcher = new NoOpEventDispatcher(), @@ -164,6 +164,12 @@ export default class EppoClient { this.expectObfuscated = isObfuscated; } + private getConfiguration(): IConfiguration { + return this.configurationRequestor + ? this.configurationRequestor.getConfiguration() + : new StoreBackedConfiguration(this.flagConfigurationStore); + } + private maybeWarnAboutObfuscationMismatch(configObfuscated: boolean) { // Don't warn again if we did on the last check. if (configObfuscated !== this.expectObfuscated && !this.obfuscationMismatchWarningIssued) { @@ -177,11 +183,9 @@ export default class EppoClient { } } - private isObfuscated() { + private isObfuscated(config: IConfiguration) { if (this.configObfuscatedCache === undefined) { - this.configObfuscatedCache = OBFUSCATED_FORMATS.includes( - this.flagConfigurationStore.getFormat() ?? FormatEnum.SERVER, - ); + this.configObfuscatedCache = config.isObfuscated(); } this.maybeWarnAboutObfuscationMismatch(this.configObfuscatedCache); return this.configObfuscatedCache; @@ -921,6 +925,7 @@ export default class EppoClient { subjectKey: string, subjectAttributes: Attributes = {}, ): Record { + const config = this.getConfiguration(); const configDetails = this.getConfigDetails(); const flagKeys = this.getFlagKeys(); const flags: Record = {}; @@ -939,7 +944,7 @@ export default class EppoClient { configDetails, subjectKey, subjectAttributes, - this.isObfuscated(), + this.isObfuscated(config), ); // allocationKey is set along with variation when there is a result. this check appeases typescript below @@ -1023,6 +1028,7 @@ export default class EppoClient { ): FlagEvaluation { validateNotBlank(subjectKey, 'Invalid argument: subjectKey cannot be blank'); validateNotBlank(flagKey, 'Invalid argument: flagKey cannot be blank'); + const config = this.getConfiguration(); const flagEvaluationDetailsBuilder = this.newFlagEvaluationDetailsBuilder(flagKey); const overrideVariation = this.overrideStore?.get(flagKey); @@ -1089,7 +1095,7 @@ export default class EppoClient { ); } - const isObfuscated = this.isObfuscated(); + const isObfuscated = this.isObfuscated(config); const result = this.evaluator.evaluateFlag( flag, configDetails, @@ -1149,7 +1155,7 @@ export default class EppoClient { } private getFlag(flagKey: string): Flag | null { - return this.isObfuscated() + return this.isObfuscated(this.getConfiguration()) ? this.getObfuscatedFlag(flagKey) : this.flagConfigurationStore.get(flagKey); } @@ -1317,7 +1323,7 @@ export default class EppoClient { private buildLoggerMetadata(): Record { return { - obfuscated: this.isObfuscated(), + obfuscated: this.isObfuscated(this.getConfiguration()), sdkLanguage: 'javascript', sdkLibVersion: LIB_VERSION, }; From 40a953d43f50c474d7da8cddd04c7593a0802d72 Mon Sep 17 00:00:00 2001 From: Ty Potter Date: Wed, 19 Feb 2025 12:15:58 -0700 Subject: [PATCH 06/24] chore: delegate getFlagKeys to i-config --- src/client/eppo-client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/eppo-client.ts b/src/client/eppo-client.ts index fd16a7a..b21ed8b 100644 --- a/src/client/eppo-client.ts +++ b/src/client/eppo-client.ts @@ -1180,7 +1180,7 @@ export default class EppoClient { * * Note that it is generally not a good idea to preload all flag configurations. */ - return this.flagConfigurationStore.getKeys(); + return this.getConfiguration().getFlagKeys(); } isInitialized() { From 3786b78d022a4c1710f77e5eff394b5130d15219 Mon Sep 17 00:00:00 2001 From: Ty Potter Date: Wed, 19 Feb 2025 12:17:04 -0700 Subject: [PATCH 07/24] chore: delegate getFlagConfigurations to i-config --- src/client/eppo-client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/eppo-client.ts b/src/client/eppo-client.ts index b21ed8b..7ef4069 100644 --- a/src/client/eppo-client.ts +++ b/src/client/eppo-client.ts @@ -1255,7 +1255,7 @@ export default class EppoClient { } getFlagConfigurations(): Record { - return this.flagConfigurationStore.entries(); + return this.getConfiguration().getFlags(); } private flushQueuedEvents(eventQueue: BoundedEventQueue, logFunction?: (event: T) => void) { From 2ab3ddac899258dd2bd4422d6a7a139d454c568f Mon Sep 17 00:00:00 2001 From: Ty Potter Date: Wed, 19 Feb 2025 12:24:51 -0700 Subject: [PATCH 08/24] chore: delegate getFlag->getNormalizedFlag and evalDetails method to use i-config --- src/client/eppo-client.ts | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/src/client/eppo-client.ts b/src/client/eppo-client.ts index 7ef4069..445b476 100644 --- a/src/client/eppo-client.ts +++ b/src/client/eppo-client.ts @@ -669,11 +669,13 @@ export default class EppoClient { actions: BanditActions, defaultValue: string, ): IAssignmentDetails { + const config = this.getConfiguration(); let variation = defaultValue; let action: string | null = null; // Initialize with a generic evaluation details. This will mutate as the function progresses. let evaluationDetails: IFlagEvaluationDetails = this.newFlagEvaluationDetailsBuilder( + config, flagKey, ).buildForNoneResult( 'ASSIGNMENT_ERROR', @@ -932,7 +934,7 @@ export default class EppoClient { // Evaluate all the enabled flags for the user flagKeys.forEach((flagKey) => { - const flag = this.getFlag(flagKey); + const flag = this.getNormalizedFlag(config, flagKey); if (!flag) { logger.debug(`${loggerPrefix} No assigned variation. Flag does not exist.`); return; @@ -1030,7 +1032,7 @@ export default class EppoClient { validateNotBlank(flagKey, 'Invalid argument: flagKey cannot be blank'); const config = this.getConfiguration(); - const flagEvaluationDetailsBuilder = this.newFlagEvaluationDetailsBuilder(flagKey); + const flagEvaluationDetailsBuilder = this.newFlagEvaluationDetailsBuilder(config, flagKey); const overrideVariation = this.overrideStore?.get(flagKey); if (overrideVariation) { return overrideResult( @@ -1043,7 +1045,7 @@ export default class EppoClient { } const configDetails = this.getConfigDetails(); - const flag = this.getFlag(flagKey); + const flag = this.getNormalizedFlag(config, flagKey); if (flag === null) { logger.warn(`${loggerPrefix} No assigned variation. Flag not found: ${flagKey}`); @@ -1132,9 +1134,12 @@ export default class EppoClient { }); } - private newFlagEvaluationDetailsBuilder(flagKey: string): FlagEvaluationDetailsBuilder { - const flag = this.getFlag(flagKey); - const configDetails = this.getConfigDetails(); + private newFlagEvaluationDetailsBuilder( + config: IConfiguration, + flagKey: string, + ): FlagEvaluationDetailsBuilder { + const flag = this.getNormalizedFlag(config, flagKey); + const configDetails = config.getFlagConfigDetails(); return new FlagEvaluationDetailsBuilder( configDetails.configEnvironment.name, flag?.allocations ?? [], @@ -1154,16 +1159,14 @@ export default class EppoClient { }; } - private getFlag(flagKey: string): Flag | null { - return this.isObfuscated(this.getConfiguration()) - ? this.getObfuscatedFlag(flagKey) - : this.flagConfigurationStore.get(flagKey); + private getNormalizedFlag(config: IConfiguration, flagKey: string): Flag | null { + return this.isObfuscated(config) + ? this.getObfuscatedFlag(config, flagKey) + : config.getFlag(flagKey); } - private getObfuscatedFlag(flagKey: string): Flag | null { - const flag: ObfuscatedFlag | null = this.flagConfigurationStore.get( - getMD5Hash(flagKey), - ) as ObfuscatedFlag; + private getObfuscatedFlag(config: IConfiguration, flagKey: string): Flag | null { + const flag: ObfuscatedFlag | null = config.getFlag(getMD5Hash(flagKey)) as ObfuscatedFlag; return flag ? decodeFlag(flag) : null; } From 06fc3e518f59368db1fbfda9147bab8385eb86a8 Mon Sep 17 00:00:00 2001 From: Ty Potter Date: Wed, 19 Feb 2025 12:43:49 -0700 Subject: [PATCH 09/24] fix: point default config at other config stores --- src/client/eppo-client.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/client/eppo-client.ts b/src/client/eppo-client.ts index 445b476..0c138a3 100644 --- a/src/client/eppo-client.ts +++ b/src/client/eppo-client.ts @@ -167,7 +167,11 @@ export default class EppoClient { private getConfiguration(): IConfiguration { return this.configurationRequestor ? this.configurationRequestor.getConfiguration() - : new StoreBackedConfiguration(this.flagConfigurationStore); + : new StoreBackedConfiguration( + this.flagConfigurationStore, + this.banditVariationConfigurationStore, + this.banditModelConfigurationStore, + ); } private maybeWarnAboutObfuscationMismatch(configObfuscated: boolean) { From 1cc8cea8e1d63a8cc72e813b311b3f00de0ed019 Mon Sep 17 00:00:00 2001 From: Ty Potter Date: Wed, 19 Feb 2025 12:46:53 -0700 Subject: [PATCH 10/24] chore: remove findBanditByVariation and delegate to i-config --- src/client/eppo-client.ts | 24 ++++++++---------------- 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/src/client/eppo-client.ts b/src/client/eppo-client.ts index 0c138a3..2e58a16 100644 --- a/src/client/eppo-client.ts +++ b/src/client/eppo-client.ts @@ -703,7 +703,7 @@ export default class EppoClient { // Check if the assigned variation is an active bandit // Note: the reason for non-bandit assignments include the subject being bucketed into a non-bandit variation or // a rollout having been done. - const bandit = this.findBanditByVariation(flagKey, variation); + const bandit = config.getFlagVariationBandit(flagKey, variation); if (!bandit) { return { variation, action: null, evaluationDetails }; @@ -988,13 +988,15 @@ export default class EppoClient { banditActions: Record = {}, salt?: string, ): string { - const configDetails = this.getConfigDetails(); + const config = this.getConfiguration(); + const configDetails = config.getFlagConfigDetails(); const subjectContextualAttributes = ensureContextualSubjectAttributes(subjectAttributes); const subjectFlatAttributes = ensureNonContextualSubjectAttributes(subjectAttributes); const flags = this.getAllAssignments(subjectKey, subjectFlatAttributes); const bandits = this.computeBanditsForFlags( + config, subjectKey, subjectContextualAttributes, banditActions, @@ -1337,6 +1339,7 @@ export default class EppoClient { } private computeBanditsForFlags( + config: IConfiguration, subjectKey: string, subjectAttributes: ContextAttributes, banditActions: Record, @@ -1350,6 +1353,7 @@ export default class EppoClient { if (flagVariation) { // Precompute a bandit, if there is one matching this variation. const precomputedResult = this.getPrecomputedBandit( + config, flagKey, flagVariation.variationValue, subjectKey, @@ -1364,27 +1368,15 @@ export default class EppoClient { return banditResults; } - private findBanditByVariation(flagKey: string, variationValue: string): BanditParameters | null { - const banditVariations = this.banditVariationConfigurationStore?.get(flagKey); - const banditKey = banditVariations?.find( - (banditVariation) => banditVariation.variationValue === variationValue, - )?.key; - - if (banditKey) { - // Retrieve the model parameters for the bandit - return this.getBandit(banditKey); - } - return null; - } - private getPrecomputedBandit( + config: IConfiguration, flagKey: string, variationValue: string, subjectKey: string, subjectAttributes: ContextAttributes, banditActions: BanditActions, ): IPrecomputedBandit | null { - const bandit = this.findBanditByVariation(flagKey, variationValue); + const bandit = config.getFlagVariationBandit(flagKey, variationValue); if (!bandit) { return null; } From f7e539ebc3150d6c650f1467568afeebb31a379f Mon Sep 17 00:00:00 2001 From: Ty Potter Date: Wed, 19 Feb 2025 12:51:01 -0700 Subject: [PATCH 11/24] chore: remove getBandit and delegate to i-config --- src/client/eppo-client.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/client/eppo-client.ts b/src/client/eppo-client.ts index 2e58a16..e487a53 100644 --- a/src/client/eppo-client.ts +++ b/src/client/eppo-client.ts @@ -644,13 +644,14 @@ export default class EppoClient { actions: BanditActions, defaultAction: string, ): string { + const config = this.getConfiguration(); let result: string | null = null; const flagBanditVariations = this.banditVariationConfigurationStore?.get(flagKey); const banditKey = flagBanditVariations?.at(0)?.key; if (banditKey) { - const banditParameters = this.getBandit(banditKey); + const banditParameters = config.getBandit(banditKey); if (banditParameters) { const contextualSubjectAttributes = ensureContextualSubjectAttributes(subjectAttributes); const actionsWithContextualAttributes = ensureActionsWithContextualAttributes(actions); @@ -1176,11 +1177,6 @@ export default class EppoClient { return flag ? decodeFlag(flag) : null; } - private getBandit(banditKey: string): BanditParameters | null { - // Upstreams for this SDK do not yet support obfuscating bandits, so no `isObfuscated` check here. - return this.banditModelConfigurationStore?.get(banditKey) ?? null; - } - // noinspection JSUnusedGlobalSymbols getFlagKeys() { /** From 39aed59b5ce2b3aaadf0c3a588c2959d290207b7 Mon Sep 17 00:00:00 2001 From: Ty Potter Date: Wed, 19 Feb 2025 12:55:54 -0700 Subject: [PATCH 12/24] feat: i-config.isInitialized --- src/client/eppo-client.ts | 7 +---- src/configuration.spec.ts | 66 +++++++++++++++++++++++++++++++++++++++ src/configuration.ts | 10 ++++++ 3 files changed, 77 insertions(+), 6 deletions(-) diff --git a/src/client/eppo-client.ts b/src/client/eppo-client.ts index e487a53..89c5ba8 100644 --- a/src/client/eppo-client.ts +++ b/src/client/eppo-client.ts @@ -1189,12 +1189,7 @@ export default class EppoClient { } isInitialized() { - return ( - this.flagConfigurationStore.isInitialized() && - (!this.banditVariationConfigurationStore || - this.banditVariationConfigurationStore.isInitialized()) && - (!this.banditModelConfigurationStore || this.banditModelConfigurationStore.isInitialized()) - ); + return this.getConfiguration().isInitialized(); } /** @deprecated Use `setAssignmentLogger` */ diff --git a/src/configuration.spec.ts b/src/configuration.spec.ts index b2c757f..24076d1 100644 --- a/src/configuration.spec.ts +++ b/src/configuration.spec.ts @@ -280,4 +280,70 @@ describe('StoreBackedConfiguration', () => { expect(config.isObfuscated()).toBe(false); }); }); + + describe('isInitialized', () => { + it('should return false when no stores are initialized', () => { + mockFlagStore.isInitialized.mockReturnValue(false); + mockBanditVariationStore.isInitialized.mockReturnValue(false); + mockBanditModelStore.isInitialized.mockReturnValue(false); + + const config = new StoreBackedConfiguration( + mockFlagStore, + mockBanditVariationStore, + mockBanditModelStore, + ); + + expect(config.isInitialized()).toBe(false); + }); + + it('should return true when all stores are initialized', () => { + mockFlagStore.isInitialized.mockReturnValue(true); + mockBanditVariationStore.isInitialized.mockReturnValue(true); + mockBanditModelStore.isInitialized.mockReturnValue(true); + + const config = new StoreBackedConfiguration( + mockFlagStore, + mockBanditVariationStore, + mockBanditModelStore, + ); + + expect(config.isInitialized()).toBe(true); + }); + + it('should return true when flag store is initialized and no bandit stores are provided', () => { + mockFlagStore.isInitialized.mockReturnValue(true); + + const config = new StoreBackedConfiguration(mockFlagStore); + + expect(config.isInitialized()).toBe(true); + }); + + it('should return false if flag store is uninitialized even if bandit stores are initialized', () => { + mockFlagStore.isInitialized.mockReturnValue(false); + mockBanditVariationStore.isInitialized.mockReturnValue(true); + mockBanditModelStore.isInitialized.mockReturnValue(true); + + const config = new StoreBackedConfiguration( + mockFlagStore, + mockBanditVariationStore, + mockBanditModelStore, + ); + + expect(config.isInitialized()).toBe(false); + }); + + it('should return false if any bandit store is uninitialized', () => { + mockFlagStore.isInitialized.mockReturnValue(true); + mockBanditVariationStore.isInitialized.mockReturnValue(true); + mockBanditModelStore.isInitialized.mockReturnValue(false); + + const config = new StoreBackedConfiguration( + mockFlagStore, + mockBanditVariationStore, + mockBanditModelStore, + ); + + expect(config.isInitialized()).toBe(false); + }); + }); }); diff --git a/src/configuration.ts b/src/configuration.ts index e99117c..c7e3f75 100644 --- a/src/configuration.ts +++ b/src/configuration.ts @@ -20,6 +20,7 @@ export interface IConfiguration { getBanditVariations(flagKey: string): BanditVariation[]; getFlagKeys(): string[]; isObfuscated(): boolean; + isInitialized(): boolean; } type Entry = @@ -147,4 +148,13 @@ export class StoreBackedConfiguration implements IConfiguration { isObfuscated(): boolean { return OBFUSCATED_FORMATS.includes(this.getFlagConfigDetails().configFormat ?? 'SERVER'); } + + isInitialized() { + return ( + this.flagConfigurationStore.isInitialized() && + (!this.banditVariationConfigurationStore || + this.banditVariationConfigurationStore.isInitialized()) && + (!this.banditModelConfigurationStore || this.banditModelConfigurationStore.isInitialized()) + ); + } } From 421470d8cdb29b8d3ba707d46342a09bf5e5e15f Mon Sep 17 00:00:00 2001 From: Ty Potter Date: Wed, 19 Feb 2025 12:57:02 -0700 Subject: [PATCH 13/24] remove getConfigDetails and delegate to i-config --- src/client/eppo-client.ts | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/src/client/eppo-client.ts b/src/client/eppo-client.ts index 89c5ba8..6d93ef0 100644 --- a/src/client/eppo-client.ts +++ b/src/client/eppo-client.ts @@ -45,7 +45,6 @@ import { BanditModelData, BanditParameters, BanditVariation, - ConfigDetails, Flag, IPrecomputedBandit, ObfuscatedFlag, @@ -933,7 +932,7 @@ export default class EppoClient { subjectAttributes: Attributes = {}, ): Record { const config = this.getConfiguration(); - const configDetails = this.getConfigDetails(); + const configDetails = config.getFlagConfigDetails(); const flagKeys = this.getFlagKeys(); const flags: Record = {}; @@ -1051,7 +1050,7 @@ export default class EppoClient { ); } - const configDetails = this.getConfigDetails(); + const configDetails = config.getFlagConfigDetails(); const flag = this.getNormalizedFlag(config, flagKey); if (flag === null) { @@ -1155,17 +1154,6 @@ export default class EppoClient { ); } - private getConfigDetails(): ConfigDetails { - return { - configFetchedAt: this.flagConfigurationStore.getConfigFetchedAt() ?? '', - configPublishedAt: this.flagConfigurationStore.getConfigPublishedAt() ?? '', - configEnvironment: this.flagConfigurationStore.getEnvironment() ?? { - name: '', - }, - configFormat: this.flagConfigurationStore.getFormat() ?? '', - }; - } - private getNormalizedFlag(config: IConfiguration, flagKey: string): Flag | null { return this.isObfuscated(config) ? this.getObfuscatedFlag(config, flagKey) From 5b34fc718d302e79afed89ef92de626bd417f158 Mon Sep 17 00:00:00 2001 From: Ty Potter Date: Wed, 19 Feb 2025 13:17:29 -0700 Subject: [PATCH 14/24] feat: getters for other config stores and some renaming --- src/configuration-requestor.spec.ts | 2 +- src/configuration.spec.ts | 88 ++++++++++++++++++++++++++++- src/configuration.ts | 18 +++++- src/types.ts | 1 + 4 files changed, 103 insertions(+), 6 deletions(-) diff --git a/src/configuration-requestor.spec.ts b/src/configuration-requestor.spec.ts index f324265..63ad378 100644 --- a/src/configuration-requestor.spec.ts +++ b/src/configuration-requestor.spec.ts @@ -575,7 +575,7 @@ describe('ConfigurationRequestor', () => { // expect(banditVariationDetails.configFormat).toBe('SERVER'); // Verify bandit model configuration - const banditVariations = config.getBanditVariations('test_flag'); + const banditVariations = config.getFlagBanditVariations('test_flag'); expect(banditVariations).toEqual([ { allocationKey: 'analysis', diff --git a/src/configuration.spec.ts b/src/configuration.spec.ts index 24076d1..0b092fb 100644 --- a/src/configuration.spec.ts +++ b/src/configuration.spec.ts @@ -1,6 +1,7 @@ import { StoreBackedConfiguration } from './configuration'; import { IConfigurationStore } from './configuration-store/configuration-store'; import { BanditParameters, BanditVariation, Environment, Flag, ObfuscatedFlag } from './interfaces'; +import { BanditKey, FlagKey } from './types'; describe('StoreBackedConfiguration', () => { let mockFlagStore: jest.Mocked>; @@ -213,7 +214,7 @@ describe('StoreBackedConfiguration', () => { ]; mockBanditVariationStore.get.mockReturnValue(mockVariations); - const result = config.getBanditVariations('test-flag'); + const result = config.getFlagBanditVariations('test-flag'); expect(result).toEqual(mockVariations); }); @@ -221,7 +222,7 @@ describe('StoreBackedConfiguration', () => { const config = new StoreBackedConfiguration(mockFlagStore, mockBanditVariationStore); mockBanditVariationStore.get.mockReturnValue(null); - const result = config.getBanditVariations('test-flag'); + const result = config.getFlagBanditVariations('test-flag'); expect(result).toEqual([]); }); }); @@ -240,7 +241,7 @@ describe('StoreBackedConfiguration', () => { describe('getFlags', () => { it('should return all flags from store', () => { const config = new StoreBackedConfiguration(mockFlagStore); - const mockFlags: Record = { + const mockFlags: Record = { 'flag-1': { key: 'flag-1' } as Flag, 'flag-2': { key: 'flag-2' } as Flag, }; @@ -346,4 +347,85 @@ describe('StoreBackedConfiguration', () => { expect(config.isInitialized()).toBe(false); }); }); + + describe('getBandits', () => { + it('should return empty object when bandit store is null', () => { + const config = new StoreBackedConfiguration(mockFlagStore); + expect(config.getBandits()).toEqual({}); + }); + + it('should return bandits from store', () => { + const mockBandits: Record = { + 'bandit-1': { + banditKey: 'bandit-1', + modelName: 'falcon', + modelVersion: '123', + modelData: { + gamma: 0, + defaultActionScore: 0, + actionProbabilityFloor: 0, + coefficients: {}, + }, + }, + 'bandit-2': { + banditKey: 'bandit-2', + modelName: 'falcon', + modelVersion: '123', + modelData: { + gamma: 0, + defaultActionScore: 0, + actionProbabilityFloor: 0, + coefficients: {}, + }, + }, + }; + + mockBanditModelStore.entries.mockReturnValue(mockBandits); + + const config = new StoreBackedConfiguration(mockFlagStore, null, mockBanditModelStore); + + expect(config.getBandits()).toEqual(mockBandits); + }); + }); + + describe('getBanditVariations', () => { + it('should return empty variations when bandit variation store is null', () => { + const config = new StoreBackedConfiguration(mockFlagStore); + expect(config.getBanditVariations()).toEqual({}); + }); + + it('should return flag variations from store', () => { + const mockVariations: Record = { + 'bandit-1': [ + { + key: 'bandit-1', + variationValue: 'true', + flagKey: 'flag_with_bandit', + variationKey: 'bandit-1', + }, + ], + 'bandit-2': [ + { + key: 'bandit-2', + variationValue: 'true', + flagKey: 'flag_with_bandit2', + variationKey: 'bandit-2', + }, + ], + }; + + mockBanditVariationStore.entries.mockReturnValue(mockVariations); + + const config = new StoreBackedConfiguration(mockFlagStore, mockBanditVariationStore); + + expect(config.getBanditVariations()['bandit-1']).toEqual([ + { + key: 'bandit-1', + variationValue: 'true', + flagKey: 'flag_with_bandit', + variationKey: 'bandit-1', + }, + ]); + }); + }); }); diff --git a/src/configuration.ts b/src/configuration.ts index c7e3f75..84c9458 100644 --- a/src/configuration.ts +++ b/src/configuration.ts @@ -14,10 +14,12 @@ import { export interface IConfiguration { getFlag(key: string): Flag | ObfuscatedFlag | null; getFlags(): Record; + getBandits(): Record; + getBanditVariations(): Record; + getFlagBanditVariations(flagKey: string): BanditVariation[]; getFlagVariationBandit(flagKey: string, variationValue: string): BanditParameters | null; getBandit(key: string): BanditParameters | null; getFlagConfigDetails(): ConfigDetails; - getBanditVariations(flagKey: string): BanditVariation[]; getFlagKeys(): string[]; isObfuscated(): boolean; isInitialized(): boolean; @@ -133,7 +135,7 @@ export class StoreBackedConfiguration implements IConfiguration { }; } - getBanditVariations(flagKey: string): BanditVariation[] { + getFlagBanditVariations(flagKey: string): BanditVariation[] { return this.banditVariationConfigurationStore?.get(flagKey) ?? []; } @@ -157,4 +159,16 @@ export class StoreBackedConfiguration implements IConfiguration { (!this.banditModelConfigurationStore || this.banditModelConfigurationStore.isInitialized()) ); } + + getBandits(): Record { + return this.banditModelConfigurationStore?.entries() ?? {}; + } + + getBanditVariations(): Record { + return this.banditVariationConfigurationStore?.entries() ?? {}; + } } + +// export class ReadOnlyConfiguration implements IConfiguration { +// public static from(other: IConfiguration): ReadOnlyConfiguration {} +// } diff --git a/src/types.ts b/src/types.ts index 8390427..abc4e57 100644 --- a/src/types.ts +++ b/src/types.ts @@ -14,4 +14,5 @@ export type BanditActions = export type Base64String = string; export type MD5String = string; export type FlagKey = string; +export type BanditKey = string; export type HashedFlagKey = FlagKey; From 15e3262ede492dda226d96f4c9838ef9e3c7f103 Mon Sep 17 00:00:00 2001 From: Ty Potter Date: Wed, 19 Feb 2025 13:46:13 -0700 Subject: [PATCH 15/24] chore: delegate banditVariations.get to i-config --- src/client/eppo-client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/eppo-client.ts b/src/client/eppo-client.ts index 6d93ef0..f77d71e 100644 --- a/src/client/eppo-client.ts +++ b/src/client/eppo-client.ts @@ -646,7 +646,7 @@ export default class EppoClient { const config = this.getConfiguration(); let result: string | null = null; - const flagBanditVariations = this.banditVariationConfigurationStore?.get(flagKey); + const flagBanditVariations = config.getFlagBanditVariations(flagKey); const banditKey = flagBanditVariations?.at(0)?.key; if (banditKey) { From bf412f8b58d79c973c97f658bef8ea942c876832 Mon Sep 17 00:00:00 2001 From: Ty Potter Date: Wed, 19 Feb 2025 13:47:59 -0700 Subject: [PATCH 16/24] move flag config expired check to config requestor --- src/client/eppo-client.ts | 2 +- src/configuration-requestor.ts | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/client/eppo-client.ts b/src/client/eppo-client.ts index f77d71e..2cc91da 100644 --- a/src/client/eppo-client.ts +++ b/src/client/eppo-client.ts @@ -326,7 +326,7 @@ export default class EppoClient { ); const pollingCallback = async () => { - if (await this.flagConfigurationStore.isExpired()) { + if (await configurationRequestor.isFlagConfigExpired()) { this.configObfuscatedCache = undefined; return configurationRequestor.fetchAndStoreConfigurations(); } diff --git a/src/configuration-requestor.ts b/src/configuration-requestor.ts index b2cc600..4d12ac5 100644 --- a/src/configuration-requestor.ts +++ b/src/configuration-requestor.ts @@ -27,6 +27,9 @@ export default class ConfigurationRequestor { ); } + public isFlagConfigExpired(): Promise { + return this.flagConfigurationStore.isExpired(); + } public getConfiguration(): IConfiguration { return this.configuration; } From 1ff4f35551ce7062776e8699c337dc2011561e9b Mon Sep 17 00:00:00 2001 From: Ty Potter Date: Wed, 19 Feb 2025 13:51:00 -0700 Subject: [PATCH 17/24] v4.11.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 26acec6..a825eab 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@eppo/js-client-sdk-common", - "version": "4.10.0", + "version": "4.11.0", "description": "Common library for Eppo JavaScript SDKs (web, react native, and node)", "main": "dist/index.js", "files": [ From 5fc7ef381a8e8883348cda3db17806338e3658cc Mon Sep 17 00:00:00 2001 From: Ty Potter Date: Wed, 19 Feb 2025 14:02:43 -0700 Subject: [PATCH 18/24] chore: drop unimplemented and add specific string types --- src/configuration.ts | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/src/configuration.ts b/src/configuration.ts index 84c9458..cc8c870 100644 --- a/src/configuration.ts +++ b/src/configuration.ts @@ -10,17 +10,18 @@ import { ObfuscatedFlag, PrecomputedFlag, } from './interfaces'; +import { BanditKey, FlagKey, HashedFlagKey } from './types'; export interface IConfiguration { - getFlag(key: string): Flag | ObfuscatedFlag | null; - getFlags(): Record; - getBandits(): Record; - getBanditVariations(): Record; - getFlagBanditVariations(flagKey: string): BanditVariation[]; - getFlagVariationBandit(flagKey: string, variationValue: string): BanditParameters | null; - getBandit(key: string): BanditParameters | null; + getFlag(key: FlagKey | HashedFlagKey): Flag | ObfuscatedFlag | null; + getFlags(): Record; + getBandits(): Record; + getBanditVariations(): Record; + getFlagBanditVariations(flagKey: FlagKey | HashedFlagKey): BanditVariation[]; + getFlagVariationBandit(flagKey: FlagKey | HashedFlagKey, variationValue: string): BanditParameters | null; + getBandit(key: BanditKey): BanditParameters | null; getFlagConfigDetails(): ConfigDetails; - getFlagKeys(): string[]; + getFlagKeys(): FlagKey[] | HashedFlagKey[]; isObfuscated(): boolean; isInitialized(): boolean; } @@ -168,7 +169,3 @@ export class StoreBackedConfiguration implements IConfiguration { return this.banditVariationConfigurationStore?.entries() ?? {}; } } - -// export class ReadOnlyConfiguration implements IConfiguration { -// public static from(other: IConfiguration): ReadOnlyConfiguration {} -// } From 3ab4301b6c24864464b2bc46254bccdf3958e86c Mon Sep 17 00:00:00 2001 From: Ty Potter Date: Wed, 19 Feb 2025 14:06:58 -0700 Subject: [PATCH 19/24] chore: rename new config file to make a nicer diff --- src/client/eppo-client.ts | 2 +- src/configuration-requestor.spec.ts | 2 +- src/configuration-requestor.ts | 6 +++--- src/{configuration.spec.ts => i-configuration.spec.ts} | 2 +- src/{configuration.ts => i-configuration.ts} | 5 ++++- 5 files changed, 10 insertions(+), 7 deletions(-) rename src/{configuration.spec.ts => i-configuration.spec.ts} (99%) rename src/{configuration.ts => i-configuration.ts} (97%) diff --git a/src/client/eppo-client.ts b/src/client/eppo-client.ts index 2cc91da..827de42 100644 --- a/src/client/eppo-client.ts +++ b/src/client/eppo-client.ts @@ -14,7 +14,6 @@ import { AssignmentCache } from '../cache/abstract-assignment-cache'; import { LRUInMemoryAssignmentCache } from '../cache/lru-in-memory-assignment-cache'; import { NonExpiringInMemoryAssignmentCache } from '../cache/non-expiring-in-memory-cache-assignment'; import { TLRUInMemoryAssignmentCache } from '../cache/tlru-in-memory-assignment-cache'; -import { IConfiguration, StoreBackedConfiguration } from '../configuration'; import ConfigurationRequestor from '../configuration-requestor'; import { IConfigurationStore, ISyncStore } from '../configuration-store/configuration-store'; import { @@ -41,6 +40,7 @@ import { } from '../flag-evaluation-details-builder'; import { FlagEvaluationError } from '../flag-evaluation-error'; import FetchHttpClient from '../http-client'; +import { IConfiguration, StoreBackedConfiguration } from '../i-configuration'; import { BanditModelData, BanditParameters, diff --git a/src/configuration-requestor.spec.ts b/src/configuration-requestor.spec.ts index 63ad378..2bfe61d 100644 --- a/src/configuration-requestor.spec.ts +++ b/src/configuration-requestor.spec.ts @@ -6,7 +6,6 @@ import { } from '../test/testHelpers'; import ApiEndpoints from './api-endpoints'; -import { StoreBackedConfiguration } from './configuration'; import ConfigurationRequestor from './configuration-requestor'; import { IConfigurationStore } from './configuration-store/configuration-store'; import { MemoryOnlyConfigurationStore } from './configuration-store/memory.store'; @@ -15,6 +14,7 @@ import FetchHttpClient, { IHttpClient, IUniversalFlagConfigResponse, } from './http-client'; +import { StoreBackedConfiguration } from './i-configuration'; import { BanditParameters, BanditVariation, Flag } from './interfaces'; describe('ConfigurationRequestor', () => { diff --git a/src/configuration-requestor.ts b/src/configuration-requestor.ts index 4d12ac5..024e9db 100644 --- a/src/configuration-requestor.ts +++ b/src/configuration-requestor.ts @@ -1,10 +1,10 @@ +import { IConfigurationStore } from './configuration-store/configuration-store'; +import { IHttpClient } from './http-client'; import { ConfigStoreHydrationPacket, IConfiguration, StoreBackedConfiguration, -} from './configuration'; -import { IConfigurationStore } from './configuration-store/configuration-store'; -import { IHttpClient } from './http-client'; +} from './i-configuration'; import { BanditVariation, BanditParameters, Flag, BanditReference } from './interfaces'; // Requests AND stores flag configurations diff --git a/src/configuration.spec.ts b/src/i-configuration.spec.ts similarity index 99% rename from src/configuration.spec.ts rename to src/i-configuration.spec.ts index 0b092fb..e368579 100644 --- a/src/configuration.spec.ts +++ b/src/i-configuration.spec.ts @@ -1,5 +1,5 @@ -import { StoreBackedConfiguration } from './configuration'; import { IConfigurationStore } from './configuration-store/configuration-store'; +import { StoreBackedConfiguration } from './i-configuration'; import { BanditParameters, BanditVariation, Environment, Flag, ObfuscatedFlag } from './interfaces'; import { BanditKey, FlagKey } from './types'; diff --git a/src/configuration.ts b/src/i-configuration.ts similarity index 97% rename from src/configuration.ts rename to src/i-configuration.ts index cc8c870..ef5c1b0 100644 --- a/src/configuration.ts +++ b/src/i-configuration.ts @@ -18,7 +18,10 @@ export interface IConfiguration { getBandits(): Record; getBanditVariations(): Record; getFlagBanditVariations(flagKey: FlagKey | HashedFlagKey): BanditVariation[]; - getFlagVariationBandit(flagKey: FlagKey | HashedFlagKey, variationValue: string): BanditParameters | null; + getFlagVariationBandit( + flagKey: FlagKey | HashedFlagKey, + variationValue: string, + ): BanditParameters | null; getBandit(key: BanditKey): BanditParameters | null; getFlagConfigDetails(): ConfigDetails; getFlagKeys(): FlagKey[] | HashedFlagKey[]; From 3533fa54f65d0b4acd41b94f8e6f6b17d68967de Mon Sep 17 00:00:00 2001 From: Ty Potter Date: Wed, 19 Feb 2025 14:08:30 -0700 Subject: [PATCH 20/24] chore: save startPolling for later --- src/client/eppo-client.ts | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/client/eppo-client.ts b/src/client/eppo-client.ts index 827de42..8c3aea4 100644 --- a/src/client/eppo-client.ts +++ b/src/client/eppo-client.ts @@ -271,18 +271,6 @@ export default class EppoClient { ); } - /** - * Attempts to fetch Eppo configuration and start polling, depending on the `FlagConfigurationRequestParameters` - * provided. - * @return a promise that resolves when the poller has completed the first fetch and/or begun to periodically poll. - */ - async startPolling() { - return this.fetchFlagConfigurations(); - } - - /** - * @deprecated use startPolling instead. - */ async fetchFlagConfigurations() { if (!this.configurationRequestParameters) { throw new Error( From b4b44b9ff30623980417deb1d07c888c19a9d184 Mon Sep 17 00:00:00 2001 From: Ty Potter Date: Wed, 19 Feb 2025 14:11:00 -0700 Subject: [PATCH 21/24] chore:lint --- src/configuration-requestor.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/configuration-requestor.ts b/src/configuration-requestor.ts index 024e9db..c0a3e10 100644 --- a/src/configuration-requestor.ts +++ b/src/configuration-requestor.ts @@ -30,6 +30,7 @@ export default class ConfigurationRequestor { public isFlagConfigExpired(): Promise { return this.flagConfigurationStore.isExpired(); } + public getConfiguration(): IConfiguration { return this.configuration; } From 4a1231bddaba61e77b3224f83a3b82bac56a5e72 Mon Sep 17 00:00:00 2001 From: Ty Potter Date: Wed, 19 Feb 2025 14:11:48 -0700 Subject: [PATCH 22/24] chore: take TODO --- src/configuration-requestor.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/configuration-requestor.ts b/src/configuration-requestor.ts index c0a3e10..1f48e09 100644 --- a/src/configuration-requestor.ts +++ b/src/configuration-requestor.ts @@ -92,7 +92,7 @@ export default class ConfigurationRequestor { banditModelPacket, ) ) { - // Notify that config updated. + // TODO: Notify that config updated. } } From 0f1874edb0fb7870b65b428f242714d0997f5599 Mon Sep 17 00:00:00 2001 From: Ty Potter Date: Wed, 19 Feb 2025 14:16:55 -0700 Subject: [PATCH 23/24] chore: don't duplicate code --- .../configuration-store-utils.ts | 6 ++- src/i-configuration.ts | 49 ++----------------- src/precomputed-requestor.ts | 3 +- 3 files changed, 10 insertions(+), 48 deletions(-) diff --git a/src/configuration-store/configuration-store-utils.ts b/src/configuration-store/configuration-store-utils.ts index ebd7bd6..6375de2 100644 --- a/src/configuration-store/configuration-store-utils.ts +++ b/src/configuration-store/configuration-store-utils.ts @@ -9,7 +9,7 @@ import { import { IConfigurationStore } from './configuration-store'; -type Entry = +export type Entry = | Flag | BanditVariation[] | BanditParameters @@ -25,7 +25,7 @@ export async function hydrateConfigurationStore( format: string; salt?: string; }, -): Promise { +): Promise { if (configurationStore) { const didUpdate = await configurationStore.setEntries(response.entries); if (didUpdate) { @@ -35,5 +35,7 @@ export async function hydrateConfigurationStore( configurationStore.setFormat(response.format); configurationStore.salt = response.salt; } + return didUpdate; } + return false; } diff --git a/src/i-configuration.ts b/src/i-configuration.ts index ef5c1b0..f8d3b0d 100644 --- a/src/i-configuration.ts +++ b/src/i-configuration.ts @@ -1,4 +1,5 @@ import { IConfigurationStore } from './configuration-store/configuration-store'; +import { Entry, hydrateConfigurationStore } from './configuration-store/configuration-store-utils'; import { OBFUSCATED_FORMATS } from './constants'; import { BanditParameters, @@ -6,9 +7,7 @@ import { ConfigDetails, Environment, Flag, - IObfuscatedPrecomputedBandit, ObfuscatedFlag, - PrecomputedFlag, } from './interfaces'; import { BanditKey, FlagKey, HashedFlagKey } from './types'; @@ -29,13 +28,6 @@ export interface IConfiguration { isInitialized(): boolean; } -type Entry = - | Flag - | BanditVariation[] - | BanditParameters - | PrecomputedFlag - | IObfuscatedPrecomputedBandit; - export type ConfigStoreHydrationPacket = { entries: Record; environment: Environment; @@ -58,55 +50,22 @@ export class StoreBackedConfiguration implements IConfiguration { banditVariationConfig?: ConfigStoreHydrationPacket, banditModelConfig?: ConfigStoreHydrationPacket, ) { - const didUpdateFlags = await StoreBackedConfiguration.hydrateConfigurationStore( - this.flagConfigurationStore, - flagConfig, - ); + const didUpdateFlags = await hydrateConfigurationStore(this.flagConfigurationStore, flagConfig); const promises: Promise[] = []; if (this.banditVariationConfigurationStore && banditVariationConfig) { promises.push( - StoreBackedConfiguration.hydrateConfigurationStore( - this.banditVariationConfigurationStore, - banditVariationConfig, - ), + hydrateConfigurationStore(this.banditVariationConfigurationStore, banditVariationConfig), ); } if (this.banditModelConfigurationStore && banditModelConfig) { promises.push( - StoreBackedConfiguration.hydrateConfigurationStore( - this.banditModelConfigurationStore, - banditModelConfig, - ), + hydrateConfigurationStore(this.banditModelConfigurationStore, banditModelConfig), ); } await Promise.all(promises); return didUpdateFlags; } - private static async hydrateConfigurationStore( - configurationStore: IConfigurationStore | null, - response: { - entries: Record; - environment: Environment; - createdAt: string; - format: string; - salt?: string; - }, - ): Promise { - if (configurationStore) { - const didUpdate = await configurationStore.setEntries(response.entries); - if (didUpdate) { - configurationStore.setEnvironment(response.environment); - configurationStore.setConfigFetchedAt(new Date().toISOString()); - configurationStore.setConfigPublishedAt(response.createdAt); - configurationStore.setFormat(response.format); - configurationStore.salt = response.salt; - } - return didUpdate; - } - return false; - } - getBandit(key: string): BanditParameters | null { return this.banditModelConfigurationStore?.get(key) ?? null; } diff --git a/src/precomputed-requestor.ts b/src/precomputed-requestor.ts index ec128ad..ef8e0fc 100644 --- a/src/precomputed-requestor.ts +++ b/src/precomputed-requestor.ts @@ -30,7 +30,7 @@ export default class PrecomputedFlagRequestor { return; } - const promises: Promise[] = []; + const promises: Promise[] = []; promises.push( hydrateConfigurationStore(this.precomputedFlagStore, { entries: precomputedResponse.flags, @@ -51,5 +51,6 @@ export default class PrecomputedFlagRequestor { }), ); } + await Promise.all(promises); } } From 94e7f3ee728aeee79d72bb5da05081ae1463a563 Mon Sep 17 00:00:00 2001 From: Ty Potter Date: Thu, 20 Feb 2025 22:36:13 -0700 Subject: [PATCH 24/24] comments --- src/client/eppo-client.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/client/eppo-client.ts b/src/client/eppo-client.ts index 8c3aea4..4541d6a 100644 --- a/src/client/eppo-client.ts +++ b/src/client/eppo-client.ts @@ -186,6 +186,11 @@ export default class EppoClient { } } + /** + * This method delegates to the configuration to determine whether it is obfuscated, then caches the actual + * obfuscation state and issues a warning if it hasn't already. + * This method can be removed with the next major update when the @deprecated setIsObfuscated is removed + */ private isObfuscated(config: IConfiguration) { if (this.configObfuscatedCache === undefined) { this.configObfuscatedCache = config.isObfuscated();