Skip to content

Commit 4d3b534

Browse files
committed
refactor: make requestor work with new ConfigurationStore
1 parent 6df8767 commit 4d3b534

File tree

5 files changed

+101
-154
lines changed

5 files changed

+101
-154
lines changed

src/client/eppo-client.ts

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -287,18 +287,11 @@ export default class EppoClient {
287287
queryParams: { apiKey, sdkName, sdkVersion },
288288
});
289289
const httpClient = new FetchHttpClient(apiEndpoints, requestTimeoutMs);
290-
const configurationRequestor = new ConfigurationRequestor(
291-
httpClient,
292-
this.flagConfigurationStore,
293-
this.banditVariationConfigurationStore ?? null,
294-
this.banditModelConfigurationStore ?? null,
295-
);
290+
const configurationRequestor = new ConfigurationRequestor(httpClient, this.configurationStore);
296291
this.configurationRequestor = configurationRequestor;
297292

298293
const pollingCallback = async () => {
299-
if (await configurationRequestor.isFlagConfigExpired()) {
300-
return configurationRequestor.fetchAndStoreConfigurations();
301-
}
294+
return configurationRequestor.fetchAndStoreConfigurations();
302295
};
303296

304297
this.requestPoller = initPoller(pollingIntervalMs, pollingCallback, {

src/configuration-requestor.ts

Lines changed: 29 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -1,132 +1,48 @@
1-
import { IConfigurationStore } from './configuration-store/configuration-store';
1+
import { Configuration } from './configuration';
2+
import { ConfigurationStore } from './configuration-store';
23
import { IHttpClient } from './http-client';
3-
import {
4-
ConfigStoreHydrationPacket,
5-
IConfiguration,
6-
StoreBackedConfiguration,
7-
} from './i-configuration';
8-
import { BanditVariation, BanditParameters, Flag, BanditReference } from './interfaces';
4+
5+
export type ConfigurationRequestorOptions = {
6+
wantsBandits?: boolean;
7+
};
98

109
// Requests AND stores flag configurations
1110
export default class ConfigurationRequestor {
12-
private banditModelVersions: string[] = [];
13-
private readonly configuration: StoreBackedConfiguration;
11+
private readonly options: ConfigurationRequestorOptions;
1412

1513
constructor(
1614
private readonly httpClient: IHttpClient,
17-
private readonly flagConfigurationStore: IConfigurationStore<Flag>,
18-
private readonly banditVariationConfigurationStore: IConfigurationStore<
19-
BanditVariation[]
20-
> | null,
21-
private readonly banditModelConfigurationStore: IConfigurationStore<BanditParameters> | null,
15+
private readonly configurationStore: ConfigurationStore,
16+
options: Partial<ConfigurationRequestorOptions> = {},
2217
) {
23-
this.configuration = new StoreBackedConfiguration(
24-
this.flagConfigurationStore,
25-
this.banditVariationConfigurationStore,
26-
this.banditModelConfigurationStore,
27-
);
28-
}
29-
30-
public isFlagConfigExpired(): Promise<boolean> {
31-
return this.flagConfigurationStore.isExpired();
32-
}
33-
34-
public getConfiguration(): IConfiguration {
35-
return this.configuration;
18+
this.options = {
19+
wantsBandits: true,
20+
...options,
21+
};
3622
}
3723

38-
async fetchAndStoreConfigurations(): Promise<void> {
24+
async fetchConfiguration(): Promise<Configuration | null> {
3925
const configResponse = await this.httpClient.getUniversalFlagConfiguration();
40-
if (!configResponse?.flags) {
41-
return;
42-
}
43-
44-
const flagResponsePacket: ConfigStoreHydrationPacket<Flag> = {
45-
entries: configResponse.flags,
46-
environment: configResponse.environment,
47-
createdAt: configResponse.createdAt,
48-
format: configResponse.format,
49-
};
50-
51-
let banditVariationPacket: ConfigStoreHydrationPacket<BanditVariation[]> | undefined;
52-
let banditModelPacket: ConfigStoreHydrationPacket<BanditParameters> | undefined;
53-
const flagsHaveBandits = Object.keys(configResponse.banditReferences ?? {}).length > 0;
54-
const banditStoresProvided = Boolean(
55-
this.banditVariationConfigurationStore && this.banditModelConfigurationStore,
56-
);
57-
if (flagsHaveBandits && banditStoresProvided) {
58-
// Map bandit flag associations by flag key for quick lookup (instead of bandit key as provided by the UFC)
59-
const banditVariations = this.indexBanditVariationsByFlagKey(configResponse.banditReferences);
60-
61-
banditVariationPacket = {
62-
entries: banditVariations,
63-
environment: configResponse.environment,
64-
createdAt: configResponse.createdAt,
65-
format: configResponse.format,
66-
};
67-
68-
if (
69-
this.requiresBanditModelConfigurationStoreUpdate(
70-
this.banditModelVersions,
71-
configResponse.banditReferences,
72-
)
73-
) {
74-
const banditResponse = await this.httpClient.getBanditParameters();
75-
if (banditResponse?.bandits) {
76-
banditModelPacket = {
77-
entries: banditResponse.bandits,
78-
environment: configResponse.environment,
79-
createdAt: configResponse.createdAt,
80-
format: configResponse.format,
81-
};
82-
83-
this.banditModelVersions = this.getLoadedBanditModelVersions(banditResponse.bandits);
84-
}
85-
}
26+
if (!configResponse?.response.flags) {
27+
return null;
8628
}
8729

88-
if (
89-
await this.configuration.hydrateConfigurationStores(
90-
flagResponsePacket,
91-
banditVariationPacket,
92-
banditModelPacket,
93-
)
94-
) {
95-
// TODO: Notify that config updated.
96-
}
97-
}
30+
const needsBandits =
31+
this.options.wantsBandits &&
32+
Object.keys(configResponse.response.banditReferences ?? {}).length > 0;
9833

99-
private getLoadedBanditModelVersions(entries: Record<string, BanditParameters>): string[] {
100-
return Object.values(entries).map((banditParam: BanditParameters) => banditParam.modelVersion);
101-
}
34+
const banditsConfig = needsBandits ? await this.httpClient.getBanditParameters() : undefined;
10235

103-
private requiresBanditModelConfigurationStoreUpdate(
104-
currentBanditModelVersions: string[],
105-
banditReferences: Record<string, BanditReference>,
106-
): boolean {
107-
const referencedModelVersions = Object.values(banditReferences).map(
108-
(banditReference: BanditReference) => banditReference.modelVersion,
109-
);
110-
111-
return !referencedModelVersions.every((modelVersion) =>
112-
currentBanditModelVersions.includes(modelVersion),
113-
);
36+
return Configuration.fromResponses({
37+
flags: configResponse,
38+
bandits: banditsConfig,
39+
});
11440
}
11541

116-
private indexBanditVariationsByFlagKey(
117-
banditVariationsByBanditKey: Record<string, BanditReference>,
118-
): Record<string, BanditVariation[]> {
119-
const banditVariationsByFlagKey: Record<string, BanditVariation[]> = {};
120-
Object.values(banditVariationsByBanditKey).forEach((banditReference) => {
121-
banditReference.flagVariations.forEach((banditVariation) => {
122-
let banditVariations = banditVariationsByFlagKey[banditVariation.flagKey];
123-
if (!banditVariations) {
124-
banditVariations = [];
125-
banditVariationsByFlagKey[banditVariation.flagKey] = banditVariations;
126-
}
127-
banditVariations.push(banditVariation);
128-
});
129-
});
130-
return banditVariationsByFlagKey;
42+
async fetchAndStoreConfigurations(): Promise<void> {
43+
const configuration = await this.fetchConfiguration();
44+
if (configuration) {
45+
this.configurationStore.setConfiguration(configuration);
46+
}
13147
}
13248
}

src/configuration-wire/configuration-wire-helper.ts

Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -49,27 +49,4 @@ export class ConfigurationWireHelper {
4949

5050
this.httpClient = new FetchHttpClient(apiEndpoints, 5000);
5151
}
52-
53-
/**
54-
* Fetches configuration data from the API and build a Bootstrap Configuration (aka an `IConfigurationWire` object).
55-
* The IConfigurationWire instance can be used to bootstrap some SDKs.
56-
*/
57-
public async fetchBootstrapConfiguration(): Promise<IConfigurationWire> {
58-
// Get the configs
59-
let banditResponse: IBanditParametersResponse | undefined;
60-
const configResponse: IUniversalFlagConfigResponse | undefined =
61-
await this.httpClient.getUniversalFlagConfiguration();
62-
63-
if (!configResponse?.flags) {
64-
console.warn('Unable to fetch configuration, returning empty configuration');
65-
return Promise.resolve(ConfigurationWireV1.empty());
66-
}
67-
68-
const flagsHaveBandits = Object.keys(configResponse.banditReferences ?? {}).length > 0;
69-
if (flagsHaveBandits) {
70-
banditResponse = await this.httpClient.getBanditParameters();
71-
}
72-
73-
return ConfigurationWireV1.fromResponses(configResponse, banditResponse);
74-
}
7552
}

src/configuration.ts

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { decodeFlag } from './decoding';
22
import { IBanditParametersResponse, IUniversalFlagConfigResponse } from './http-client';
33
import { BanditParameters, BanditVariation, Flag, FormatEnum, ObfuscatedFlag } from './interfaces';
44
import { getMD5Hash } from './obfuscation';
5-
import { FlagKey, HashedFlagKey } from './types';
5+
import { ContextAttributes, FlagKey, HashedFlagKey } from './types';
66

77
/** @internal for SDK use only */
88
export type FlagsConfig = {
@@ -52,9 +52,28 @@ export class Configuration {
5252
return new Configuration(flags, bandits);
5353
}
5454

55-
// TODO(v5)
55+
// TODO:
5656
// public static fromString(configurationWire: string): Configuration {}
57-
// public toString(): string {}
57+
58+
/** Serializes configuration to "configuration wire" format. */
59+
public toString(): string {
60+
const wire: ConfigurationWire = {
61+
version: 1,
62+
};
63+
if (this.flags) {
64+
wire.config = {
65+
...this.flags,
66+
response: JSON.stringify(this.flags.response),
67+
};
68+
}
69+
if (this.bandits) {
70+
wire.bandits = {
71+
...this.bandits,
72+
response: JSON.stringify(this.bandits.response),
73+
};
74+
}
75+
return JSON.stringify(wire);
76+
}
5877

5978
public getFlagKeys(): FlagKey[] | HashedFlagKey[] {
6079
if (!this.flags) {
@@ -126,3 +145,30 @@ function indexBanditVariationsByFlagKey(
126145
});
127146
return banditVariationsByFlagKey;
128147
}
148+
149+
/** @internal */
150+
type ConfigurationWire = {
151+
/**
152+
* Version field should be incremented for breaking format changes.
153+
* For example, removing required fields or changing field type/meaning.
154+
*/
155+
version: 1;
156+
157+
config?: {
158+
response: string;
159+
etag?: string;
160+
fetchedAt?: string;
161+
};
162+
163+
bandits?: {
164+
response: string;
165+
etag?: string;
166+
fetchedAt?: string;
167+
};
168+
169+
precomputed?: {
170+
response: string;
171+
subjectKey: string;
172+
subjectAttributes?: ContextAttributes;
173+
};
174+
};

src/http-client.ts

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import ApiEndpoints from './api-endpoints';
2+
import { BanditsConfig, FlagsConfig } from './configuration';
23
import { IObfuscatedPrecomputedConfigurationResponse } from './configuration-wire/configuration-wire-types';
34
import {
45
BanditParameters,
@@ -47,8 +48,8 @@ export interface IBanditParametersResponse {
4748
}
4849

4950
export interface IHttpClient {
50-
getUniversalFlagConfiguration(): Promise<IUniversalFlagConfigResponse | undefined>;
51-
getBanditParameters(): Promise<IBanditParametersResponse | undefined>;
51+
getUniversalFlagConfiguration(): Promise<FlagsConfig | undefined>;
52+
getBanditParameters(): Promise<BanditsConfig | undefined>;
5253
getPrecomputedFlags(
5354
payload: PrecomputedFlagsPayload,
5455
): Promise<IObfuscatedPrecomputedConfigurationResponse | undefined>;
@@ -62,14 +63,28 @@ export default class FetchHttpClient implements IHttpClient {
6263
private readonly timeout: number,
6364
) {}
6465

65-
async getUniversalFlagConfiguration(): Promise<IUniversalFlagConfigResponse | undefined> {
66+
async getUniversalFlagConfiguration(): Promise<FlagsConfig | undefined> {
6667
const url = this.apiEndpoints.ufcEndpoint();
67-
return await this.rawGet<IUniversalFlagConfigResponse>(url);
68+
const response = await this.rawGet<IUniversalFlagConfigResponse>(url);
69+
if (!response) {
70+
return undefined;
71+
}
72+
return {
73+
response,
74+
fetchedAt: new Date().toISOString(),
75+
};
6876
}
6977

70-
async getBanditParameters(): Promise<IBanditParametersResponse | undefined> {
78+
async getBanditParameters(): Promise<BanditsConfig | undefined> {
7179
const url = this.apiEndpoints.banditParametersEndpoint();
72-
return await this.rawGet<IBanditParametersResponse>(url);
80+
const response = await this.rawGet<IBanditParametersResponse>(url);
81+
if (!response) {
82+
return undefined;
83+
}
84+
return {
85+
response,
86+
fetchedAt: new Date().toISOString(),
87+
};
7388
}
7489

7590
async getPrecomputedFlags(

0 commit comments

Comments
 (0)