Skip to content

Commit 4679cfd

Browse files
authored
feat: IConfiguration interface for config access in EppoClient (#222)
* chore: rename configuration.ts to configuration-wire-types.ts * chore: deprecate fetchFlagConfigurations in favour of startPolling * feat: Wrap configuration store access in new Configuration object * feat: ConfigurationRequestor.getConfiguration * chore: delegate isObfuscated check to i-config * chore: delegate getFlagKeys to i-config * chore: delegate getFlagConfigurations to i-config * chore: delegate getFlag->getNormalizedFlag and evalDetails method to use i-config * fix: point default config at other config stores * chore: remove findBanditByVariation and delegate to i-config * chore: remove getBandit and delegate to i-config * feat: i-config.isInitialized * chore: remove getConfigDetails and delegate to i-config * feat: getters for other config stores and some renaming * chore: delegate banditVariations.get to i-config * move flag config expired check to config requestor * v4.11.0 * chore: drop unimplemented and add specific string types * chore: rename new config file to make a nicer diff * chore:lint * chore: take TODO * chore: don't duplicate code * comments
1 parent 717aba8 commit 4679cfd

15 files changed

+905
-110
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@eppo/js-client-sdk-common",
3-
"version": "4.10.0",
3+
"version": "4.11.0",
44
"description": "Common library for Eppo JavaScript SDKs (web, react native, and node)",
55
"main": "dist/index.js",
66
"files": [

src/client/eppo-client-with-bandits.spec.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,13 @@ import ApiEndpoints from '../api-endpoints';
1212
import { IAssignmentEvent, IAssignmentLogger } from '../assignment-logger';
1313
import { BanditEvaluation, BanditEvaluator } from '../bandit-evaluator';
1414
import { IBanditEvent, IBanditLogger } from '../bandit-logger';
15+
import ConfigurationRequestor from '../configuration-requestor';
16+
import { MemoryOnlyConfigurationStore } from '../configuration-store/memory.store';
1517
import {
1618
IConfigurationWire,
1719
IPrecomputedConfiguration,
1820
IObfuscatedPrecomputedConfigurationResponse,
19-
} from '../configuration';
20-
import ConfigurationRequestor from '../configuration-requestor';
21-
import { MemoryOnlyConfigurationStore } from '../configuration-store/memory.store';
21+
} from '../configuration-wire-types';
2222
import { Evaluator, FlagEvaluation } from '../evaluator';
2323
import {
2424
AllocationEvaluationCode,

src/client/eppo-client.spec.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,14 @@ import {
1515
} from '../../test/testHelpers';
1616
import { IAssignmentLogger } from '../assignment-logger';
1717
import { AssignmentCache } from '../cache/abstract-assignment-cache';
18+
import { IConfigurationStore } from '../configuration-store/configuration-store';
19+
import { MemoryOnlyConfigurationStore } from '../configuration-store/memory.store';
1820
import {
1921
IConfigurationWire,
2022
IObfuscatedPrecomputedConfigurationResponse,
2123
ObfuscatedPrecomputedConfigurationResponse,
22-
} from '../configuration';
23-
import { IConfigurationStore } from '../configuration-store/configuration-store';
24-
import { MemoryOnlyConfigurationStore } from '../configuration-store/memory.store';
25-
import { DEFAULT_POLL_INTERVAL_MS, MAX_EVENT_QUEUE_SIZE, POLL_JITTER_PCT } from '../constants';
24+
} from '../configuration-wire-types';
25+
import { MAX_EVENT_QUEUE_SIZE, DEFAULT_POLL_INTERVAL_MS, POLL_JITTER_PCT } from '../constants';
2626
import { decodePrecomputedFlag } from '../decoding';
2727
import { Flag, ObfuscatedFlag, VariationType, FormatEnum, Variation } from '../interfaces';
2828
import { getMD5Hash } from '../obfuscation';

src/client/eppo-client.ts

Lines changed: 61 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -14,20 +14,19 @@ import { AssignmentCache } from '../cache/abstract-assignment-cache';
1414
import { LRUInMemoryAssignmentCache } from '../cache/lru-in-memory-assignment-cache';
1515
import { NonExpiringInMemoryAssignmentCache } from '../cache/non-expiring-in-memory-cache-assignment';
1616
import { TLRUInMemoryAssignmentCache } from '../cache/tlru-in-memory-assignment-cache';
17+
import ConfigurationRequestor from '../configuration-requestor';
18+
import { IConfigurationStore, ISyncStore } from '../configuration-store/configuration-store';
1719
import {
1820
ConfigurationWireV1,
1921
IConfigurationWire,
2022
IPrecomputedConfiguration,
2123
PrecomputedConfiguration,
22-
} from '../configuration';
23-
import ConfigurationRequestor from '../configuration-requestor';
24-
import { IConfigurationStore, ISyncStore } from '../configuration-store/configuration-store';
24+
} from '../configuration-wire-types';
2525
import {
2626
DEFAULT_INITIAL_CONFIG_REQUEST_RETRIES,
2727
DEFAULT_POLL_CONFIG_REQUEST_RETRIES,
2828
DEFAULT_POLL_INTERVAL_MS,
2929
DEFAULT_REQUEST_TIMEOUT_MS,
30-
OBFUSCATED_FORMATS,
3130
} from '../constants';
3231
import { decodeFlag } from '../decoding';
3332
import { EppoValue } from '../eppo_value';
@@ -41,13 +40,12 @@ import {
4140
} from '../flag-evaluation-details-builder';
4241
import { FlagEvaluationError } from '../flag-evaluation-error';
4342
import FetchHttpClient from '../http-client';
43+
import { IConfiguration, StoreBackedConfiguration } from '../i-configuration';
4444
import {
4545
BanditModelData,
4646
BanditParameters,
4747
BanditVariation,
48-
ConfigDetails,
4948
Flag,
50-
FormatEnum,
5149
IPrecomputedBandit,
5250
ObfuscatedFlag,
5351
PrecomputedFlag,
@@ -135,6 +133,7 @@ export default class EppoClient {
135133
private configObfuscatedCache?: boolean;
136134
private requestPoller?: IPoller;
137135
private readonly evaluator = new Evaluator();
136+
private configurationRequestor?: ConfigurationRequestor;
138137

139138
constructor({
140139
eventDispatcher = new NoOpEventDispatcher(),
@@ -164,6 +163,16 @@ export default class EppoClient {
164163
this.expectObfuscated = isObfuscated;
165164
}
166165

166+
private getConfiguration(): IConfiguration {
167+
return this.configurationRequestor
168+
? this.configurationRequestor.getConfiguration()
169+
: new StoreBackedConfiguration(
170+
this.flagConfigurationStore,
171+
this.banditVariationConfigurationStore,
172+
this.banditModelConfigurationStore,
173+
);
174+
}
175+
167176
private maybeWarnAboutObfuscationMismatch(configObfuscated: boolean) {
168177
// Don't warn again if we did on the last check.
169178
if (configObfuscated !== this.expectObfuscated && !this.obfuscationMismatchWarningIssued) {
@@ -177,11 +186,14 @@ export default class EppoClient {
177186
}
178187
}
179188

180-
private isObfuscated() {
189+
/**
190+
* This method delegates to the configuration to determine whether it is obfuscated, then caches the actual
191+
* obfuscation state and issues a warning if it hasn't already.
192+
* This method can be removed with the next major update when the @deprecated setIsObfuscated is removed
193+
*/
194+
private isObfuscated(config: IConfiguration) {
181195
if (this.configObfuscatedCache === undefined) {
182-
this.configObfuscatedCache = OBFUSCATED_FORMATS.includes(
183-
this.flagConfigurationStore.getFormat() ?? FormatEnum.SERVER,
184-
);
196+
this.configObfuscatedCache = config.isObfuscated();
185197
}
186198
this.maybeWarnAboutObfuscationMismatch(this.configObfuscatedCache);
187199
return this.configObfuscatedCache;
@@ -307,7 +319,7 @@ export default class EppoClient {
307319
);
308320

309321
const pollingCallback = async () => {
310-
if (await this.flagConfigurationStore.isExpired()) {
322+
if (await configurationRequestor.isFlagConfigExpired()) {
311323
this.configObfuscatedCache = undefined;
312324
return configurationRequestor.fetchAndStoreConfigurations();
313325
}
@@ -624,13 +636,14 @@ export default class EppoClient {
624636
actions: BanditActions,
625637
defaultAction: string,
626638
): string {
639+
const config = this.getConfiguration();
627640
let result: string | null = null;
628641

629-
const flagBanditVariations = this.banditVariationConfigurationStore?.get(flagKey);
642+
const flagBanditVariations = config.getFlagBanditVariations(flagKey);
630643
const banditKey = flagBanditVariations?.at(0)?.key;
631644

632645
if (banditKey) {
633-
const banditParameters = this.getBandit(banditKey);
646+
const banditParameters = config.getBandit(banditKey);
634647
if (banditParameters) {
635648
const contextualSubjectAttributes = ensureContextualSubjectAttributes(subjectAttributes);
636649
const actionsWithContextualAttributes = ensureActionsWithContextualAttributes(actions);
@@ -653,11 +666,13 @@ export default class EppoClient {
653666
actions: BanditActions,
654667
defaultValue: string,
655668
): IAssignmentDetails<string> {
669+
const config = this.getConfiguration();
656670
let variation = defaultValue;
657671
let action: string | null = null;
658672

659673
// Initialize with a generic evaluation details. This will mutate as the function progresses.
660674
let evaluationDetails: IFlagEvaluationDetails = this.newFlagEvaluationDetailsBuilder(
675+
config,
661676
flagKey,
662677
).buildForNoneResult(
663678
'ASSIGNMENT_ERROR',
@@ -681,7 +696,7 @@ export default class EppoClient {
681696
// Check if the assigned variation is an active bandit
682697
// Note: the reason for non-bandit assignments include the subject being bucketed into a non-bandit variation or
683698
// a rollout having been done.
684-
const bandit = this.findBanditByVariation(flagKey, variation);
699+
const bandit = config.getFlagVariationBandit(flagKey, variation);
685700

686701
if (!bandit) {
687702
return { variation, action: null, evaluationDetails };
@@ -909,13 +924,14 @@ export default class EppoClient {
909924
subjectKey: string,
910925
subjectAttributes: Attributes = {},
911926
): Record<FlagKey, PrecomputedFlag> {
912-
const configDetails = this.getConfigDetails();
927+
const config = this.getConfiguration();
928+
const configDetails = config.getFlagConfigDetails();
913929
const flagKeys = this.getFlagKeys();
914930
const flags: Record<FlagKey, PrecomputedFlag> = {};
915931

916932
// Evaluate all the enabled flags for the user
917933
flagKeys.forEach((flagKey) => {
918-
const flag = this.getFlag(flagKey);
934+
const flag = this.getNormalizedFlag(config, flagKey);
919935
if (!flag) {
920936
logger.debug(`${loggerPrefix} No assigned variation. Flag does not exist.`);
921937
return;
@@ -927,7 +943,7 @@ export default class EppoClient {
927943
configDetails,
928944
subjectKey,
929945
subjectAttributes,
930-
this.isObfuscated(),
946+
this.isObfuscated(config),
931947
);
932948

933949
// allocationKey is set along with variation when there is a result. this check appeases typescript below
@@ -965,13 +981,15 @@ export default class EppoClient {
965981
banditActions: Record<FlagKey, BanditActions> = {},
966982
salt?: string,
967983
): string {
968-
const configDetails = this.getConfigDetails();
984+
const config = this.getConfiguration();
985+
const configDetails = config.getFlagConfigDetails();
969986

970987
const subjectContextualAttributes = ensureContextualSubjectAttributes(subjectAttributes);
971988
const subjectFlatAttributes = ensureNonContextualSubjectAttributes(subjectAttributes);
972989
const flags = this.getAllAssignments(subjectKey, subjectFlatAttributes);
973990

974991
const bandits = this.computeBanditsForFlags(
992+
config,
975993
subjectKey,
976994
subjectContextualAttributes,
977995
banditActions,
@@ -1011,8 +1029,9 @@ export default class EppoClient {
10111029
): FlagEvaluation {
10121030
validateNotBlank(subjectKey, 'Invalid argument: subjectKey cannot be blank');
10131031
validateNotBlank(flagKey, 'Invalid argument: flagKey cannot be blank');
1032+
const config = this.getConfiguration();
10141033

1015-
const flagEvaluationDetailsBuilder = this.newFlagEvaluationDetailsBuilder(flagKey);
1034+
const flagEvaluationDetailsBuilder = this.newFlagEvaluationDetailsBuilder(config, flagKey);
10161035
const overrideVariation = this.overrideStore?.get(flagKey);
10171036
if (overrideVariation) {
10181037
return overrideResult(
@@ -1024,8 +1043,8 @@ export default class EppoClient {
10241043
);
10251044
}
10261045

1027-
const configDetails = this.getConfigDetails();
1028-
const flag = this.getFlag(flagKey);
1046+
const configDetails = config.getFlagConfigDetails();
1047+
const flag = this.getNormalizedFlag(config, flagKey);
10291048

10301049
if (flag === null) {
10311050
logger.warn(`${loggerPrefix} No assigned variation. Flag not found: ${flagKey}`);
@@ -1077,7 +1096,7 @@ export default class EppoClient {
10771096
);
10781097
}
10791098

1080-
const isObfuscated = this.isObfuscated();
1099+
const isObfuscated = this.isObfuscated(config);
10811100
const result = this.evaluator.evaluateFlag(
10821101
flag,
10831102
configDetails,
@@ -1114,9 +1133,12 @@ export default class EppoClient {
11141133
});
11151134
}
11161135

1117-
private newFlagEvaluationDetailsBuilder(flagKey: string): FlagEvaluationDetailsBuilder {
1118-
const flag = this.getFlag(flagKey);
1119-
const configDetails = this.getConfigDetails();
1136+
private newFlagEvaluationDetailsBuilder(
1137+
config: IConfiguration,
1138+
flagKey: string,
1139+
): FlagEvaluationDetailsBuilder {
1140+
const flag = this.getNormalizedFlag(config, flagKey);
1141+
const configDetails = config.getFlagConfigDetails();
11201142
return new FlagEvaluationDetailsBuilder(
11211143
configDetails.configEnvironment.name,
11221144
flag?.allocations ?? [],
@@ -1125,35 +1147,17 @@ export default class EppoClient {
11251147
);
11261148
}
11271149

1128-
private getConfigDetails(): ConfigDetails {
1129-
return {
1130-
configFetchedAt: this.flagConfigurationStore.getConfigFetchedAt() ?? '',
1131-
configPublishedAt: this.flagConfigurationStore.getConfigPublishedAt() ?? '',
1132-
configEnvironment: this.flagConfigurationStore.getEnvironment() ?? {
1133-
name: '',
1134-
},
1135-
configFormat: this.flagConfigurationStore.getFormat() ?? '',
1136-
};
1137-
}
1138-
1139-
private getFlag(flagKey: string): Flag | null {
1140-
return this.isObfuscated()
1141-
? this.getObfuscatedFlag(flagKey)
1142-
: this.flagConfigurationStore.get(flagKey);
1150+
private getNormalizedFlag(config: IConfiguration, flagKey: string): Flag | null {
1151+
return this.isObfuscated(config)
1152+
? this.getObfuscatedFlag(config, flagKey)
1153+
: config.getFlag(flagKey);
11431154
}
11441155

1145-
private getObfuscatedFlag(flagKey: string): Flag | null {
1146-
const flag: ObfuscatedFlag | null = this.flagConfigurationStore.get(
1147-
getMD5Hash(flagKey),
1148-
) as ObfuscatedFlag;
1156+
private getObfuscatedFlag(config: IConfiguration, flagKey: string): Flag | null {
1157+
const flag: ObfuscatedFlag | null = config.getFlag(getMD5Hash(flagKey)) as ObfuscatedFlag;
11491158
return flag ? decodeFlag(flag) : null;
11501159
}
11511160

1152-
private getBandit(banditKey: string): BanditParameters | null {
1153-
// Upstreams for this SDK do not yet support obfuscating bandits, so no `isObfuscated` check here.
1154-
return this.banditModelConfigurationStore?.get(banditKey) ?? null;
1155-
}
1156-
11571161
// noinspection JSUnusedGlobalSymbols
11581162
getFlagKeys() {
11591163
/**
@@ -1162,16 +1166,11 @@ export default class EppoClient {
11621166
*
11631167
* Note that it is generally not a good idea to preload all flag configurations.
11641168
*/
1165-
return this.flagConfigurationStore.getKeys();
1169+
return this.getConfiguration().getFlagKeys();
11661170
}
11671171

11681172
isInitialized() {
1169-
return (
1170-
this.flagConfigurationStore.isInitialized() &&
1171-
(!this.banditVariationConfigurationStore ||
1172-
this.banditVariationConfigurationStore.isInitialized()) &&
1173-
(!this.banditModelConfigurationStore || this.banditModelConfigurationStore.isInitialized())
1174-
);
1173+
return this.getConfiguration().isInitialized();
11751174
}
11761175

11771176
/** @deprecated Use `setAssignmentLogger` */
@@ -1237,7 +1236,7 @@ export default class EppoClient {
12371236
}
12381237

12391238
getFlagConfigurations(): Record<string, Flag> {
1240-
return this.flagConfigurationStore.entries();
1239+
return this.getConfiguration().getFlags();
12411240
}
12421241

12431242
private flushQueuedEvents<T>(eventQueue: BoundedEventQueue<T>, logFunction?: (event: T) => void) {
@@ -1305,13 +1304,14 @@ export default class EppoClient {
13051304

13061305
private buildLoggerMetadata(): Record<string, unknown> {
13071306
return {
1308-
obfuscated: this.isObfuscated(),
1307+
obfuscated: this.isObfuscated(this.getConfiguration()),
13091308
sdkLanguage: 'javascript',
13101309
sdkLibVersion: LIB_VERSION,
13111310
};
13121311
}
13131312

13141313
private computeBanditsForFlags(
1314+
config: IConfiguration,
13151315
subjectKey: string,
13161316
subjectAttributes: ContextAttributes,
13171317
banditActions: Record<FlagKey, BanditActions>,
@@ -1325,6 +1325,7 @@ export default class EppoClient {
13251325
if (flagVariation) {
13261326
// Precompute a bandit, if there is one matching this variation.
13271327
const precomputedResult = this.getPrecomputedBandit(
1328+
config,
13281329
flagKey,
13291330
flagVariation.variationValue,
13301331
subjectKey,
@@ -1339,27 +1340,15 @@ export default class EppoClient {
13391340
return banditResults;
13401341
}
13411342

1342-
private findBanditByVariation(flagKey: string, variationValue: string): BanditParameters | null {
1343-
const banditVariations = this.banditVariationConfigurationStore?.get(flagKey);
1344-
const banditKey = banditVariations?.find(
1345-
(banditVariation) => banditVariation.variationValue === variationValue,
1346-
)?.key;
1347-
1348-
if (banditKey) {
1349-
// Retrieve the model parameters for the bandit
1350-
return this.getBandit(banditKey);
1351-
}
1352-
return null;
1353-
}
1354-
13551343
private getPrecomputedBandit(
1344+
config: IConfiguration,
13561345
flagKey: string,
13571346
variationValue: string,
13581347
subjectKey: string,
13591348
subjectAttributes: ContextAttributes,
13601349
banditActions: BanditActions,
13611350
): IPrecomputedBandit | null {
1362-
const bandit = this.findBanditByVariation(flagKey, variationValue);
1351+
const bandit = config.getFlagVariationBandit(flagKey, variationValue);
13631352
if (!bandit) {
13641353
return null;
13651354
}

0 commit comments

Comments
 (0)