Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@eppo/js-client-sdk-common",
"version": "4.3.1-alpha.1",
"version": "4.3.1-alpha.2",
"description": "Eppo SDK for client-side JavaScript applications (base for both web and react native)",
"main": "dist/index.js",
"files": [
Expand Down
4 changes: 3 additions & 1 deletion src/client/eppo-precomputed-client.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,9 @@ describe('EppoPrecomputedClient E2E test', () => {
},
});
const httpClient = new FetchHttpClient(apiEndpoints, 1000);
const precomputedFlagRequestor = new PrecomputedRequestor(httpClient, storage);
const precomputedFlagRequestor = new PrecomputedRequestor(httpClient, storage, 'subject-key', {
'attribute-key': 'attribute-value',
});
await precomputedFlagRequestor.fetchAndStorePrecomputedFlags();
});

Expand Down
9 changes: 7 additions & 2 deletions src/client/eppo-precomputed-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,10 +102,15 @@ export default class EppoPrecomputedClient {
// todo: Inject the chain of dependencies below
const apiEndpoints = new ApiEndpoints({
baseUrl: baseUrl ?? PRECOMPUTED_BASE_URL,
queryParams: { apiKey, sdkName, sdkVersion, subjectKey, subjectAttributes },
queryParams: { apiKey, sdkName, sdkVersion },
});
const httpClient = new FetchHttpClient(apiEndpoints, requestTimeoutMs);
const precomputedRequestor = new PrecomputedRequestor(httpClient, this.precomputedFlagStore);
const precomputedRequestor = new PrecomputedRequestor(
httpClient,
this.precomputedFlagStore,
subjectKey,
subjectAttributes,
);

const pollingCallback = async () => {
if (await this.precomputedFlagStore.isExpired()) {
Expand Down
48 changes: 44 additions & 4 deletions src/http-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
Flag,
FormatEnum,
PrecomputedFlag,
PrecomputedFlagsPayload,
} from './interfaces';
import { Attributes } from './types';

export interface IQueryParams {
apiKey: string;
Expand All @@ -16,7 +18,7 @@

export interface IQueryParamsWithSubject extends IQueryParams {
subjectKey: string;
subjectAttributes: Record<string, any>;
subjectAttributes: Attributes;
}

export class HttpRequestError extends Error {
Expand Down Expand Up @@ -50,8 +52,11 @@
export interface IHttpClient {
getUniversalFlagConfiguration(): Promise<IUniversalFlagConfigResponse | undefined>;
getBanditParameters(): Promise<IBanditParametersResponse | undefined>;
getPrecomputedFlags(): Promise<IPrecomputedFlagsResponse | undefined>;
getPrecomputedFlags(
payload: PrecomputedFlagsPayload,
): Promise<IPrecomputedFlagsResponse | undefined>;
rawGet<T>(url: URL): Promise<T | undefined>;
rawPost<T, P>(url: URL, payload: P): Promise<T | undefined>;
}

export default class FetchHttpClient implements IHttpClient {
Expand All @@ -67,9 +72,11 @@
return await this.rawGet<IBanditParametersResponse>(url);
}

async getPrecomputedFlags(): Promise<IPrecomputedFlagsResponse | undefined> {
async getPrecomputedFlags(
payload: PrecomputedFlagsPayload,
): Promise<IPrecomputedFlagsResponse | undefined> {
const url = this.apiEndpoints.precomputedFlagsEndpoint();
return await this.rawGet<IPrecomputedFlagsResponse>(url);
return await this.rawPost<IPrecomputedFlagsResponse, PrecomputedFlagsPayload>(url, payload);
}

async rawGet<T>(url: URL): Promise<T | undefined> {
Expand All @@ -87,7 +94,7 @@
throw new HttpRequestError('Failed to fetch data', response?.status);
}
return await response.json();
} catch (error: any) {

Check warning on line 97 in src/http-client.ts

View workflow job for this annotation

GitHub Actions / lint-test-sdk (23)

Unexpected any. Specify a different type

Check warning on line 97 in src/http-client.ts

View workflow job for this annotation

GitHub Actions / lint-test-sdk (18)

Unexpected any. Specify a different type

Check warning on line 97 in src/http-client.ts

View workflow job for this annotation

GitHub Actions / lint-test-sdk (20)

Unexpected any. Specify a different type

Check warning on line 97 in src/http-client.ts

View workflow job for this annotation

GitHub Actions / lint-test-sdk (22)

Unexpected any. Specify a different type
if (error.name === 'AbortError') {
throw new HttpRequestError('Request timed out', 408, error);
} else if (error instanceof HttpRequestError) {
Expand All @@ -97,4 +104,37 @@
throw new HttpRequestError('Network error', 0, error);
}
}

async rawPost<T, P>(url: URL, payload: P): Promise<T | undefined> {
try {
const controller = new AbortController();
const signal = controller.signal;
const timeoutId = setTimeout(() => controller.abort(), this.timeout);

const response = await fetch(url.toString(), {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
signal,
});

clearTimeout(timeoutId);

if (!response?.ok) {
const errorBody = await response.text();
throw new HttpRequestError(errorBody || 'Failed to post data', response?.status);
}
return await response.json();
} catch (error: any) {

Check warning on line 130 in src/http-client.ts

View workflow job for this annotation

GitHub Actions / lint-test-sdk (23)

Unexpected any. Specify a different type

Check warning on line 130 in src/http-client.ts

View workflow job for this annotation

GitHub Actions / lint-test-sdk (18)

Unexpected any. Specify a different type

Check warning on line 130 in src/http-client.ts

View workflow job for this annotation

GitHub Actions / lint-test-sdk (20)

Unexpected any. Specify a different type

Check warning on line 130 in src/http-client.ts

View workflow job for this annotation

GitHub Actions / lint-test-sdk (22)

Unexpected any. Specify a different type
if (error.name === 'AbortError') {
throw new HttpRequestError('Request timed out', 408, error);
} else if (error instanceof HttpRequestError) {
throw error;
}

throw new HttpRequestError('Network error', 0, error);
}
}
}
6 changes: 6 additions & 0 deletions src/interfaces.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Rule } from './rules';
import { Attributes } from './types';

export enum VariationType {
STRING = 'STRING',
Expand Down Expand Up @@ -155,3 +156,8 @@ export interface PrecomputedFlagsDetails {
precomputedFlagsPublishedAt: string;
precomputedFlagsEnvironment: Environment;
}

export interface PrecomputedFlagsPayload {
subject_key: string;
subject_attributes: Attributes;
}
17 changes: 12 additions & 5 deletions src/precomputed-requestor.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { IConfigurationStore } from './configuration-store/configuration-store';
import { MemoryOnlyConfigurationStore } from './configuration-store/memory.store';
import FetchHttpClient, { IHttpClient } from './http-client';
import { PrecomputedFlag } from './interfaces';
import ConfigurationRequestor from './precomputed-requestor';
import PrecomputedFlagRequestor from './precomputed-requestor';

const MOCK_PRECOMPUTED_RESPONSE = {
flags: {
Expand Down Expand Up @@ -34,7 +34,7 @@ const MOCK_PRECOMPUTED_RESPONSE = {
describe('PrecomputedRequestor', () => {
let precomputedFlagStore: IConfigurationStore<PrecomputedFlag>;
let httpClient: IHttpClient;
let configurationRequestor: ConfigurationRequestor;
let precomputedFlagRequestor: PrecomputedFlagRequestor;
let fetchSpy: jest.Mock;

beforeEach(() => {
Expand All @@ -48,7 +48,14 @@ describe('PrecomputedRequestor', () => {
});
httpClient = new FetchHttpClient(apiEndpoints, 1000);
precomputedFlagStore = new MemoryOnlyConfigurationStore<PrecomputedFlag>();
configurationRequestor = new ConfigurationRequestor(httpClient, precomputedFlagStore);
precomputedFlagRequestor = new PrecomputedFlagRequestor(
httpClient,
precomputedFlagStore,
'subject-key',
{
'attribute-key': 'attribute-value',
},
);

fetchSpy = jest.fn(() => {
return Promise.resolve({
Expand All @@ -70,7 +77,7 @@ describe('PrecomputedRequestor', () => {

describe('Precomputed flags', () => {
it('Fetches and stores precomputed flag configuration', async () => {
await configurationRequestor.fetchAndStorePrecomputedFlags();
await precomputedFlagRequestor.fetchAndStorePrecomputedFlags();

expect(fetchSpy).toHaveBeenCalledTimes(1);

Expand Down Expand Up @@ -110,7 +117,7 @@ describe('PrecomputedRequestor', () => {
}),
);

await configurationRequestor.fetchAndStorePrecomputedFlags();
await precomputedFlagRequestor.fetchAndStorePrecomputedFlags();

expect(fetchSpy).toHaveBeenCalledTimes(1);
expect(precomputedFlagStore.getKeys().length).toBe(0);
Expand Down
9 changes: 8 additions & 1 deletion src/precomputed-requestor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,23 @@ import { IConfigurationStore } from './configuration-store/configuration-store';
import { hydrateConfigurationStore } from './configuration-store/configuration-store-utils';
import { IHttpClient } from './http-client';
import { PrecomputedFlag } from './interfaces';
import { Attributes } from './types';

// Requests AND stores precomputed flags, reuses the configuration store
export default class PrecomputedFlagRequestor {
constructor(
private readonly httpClient: IHttpClient,
private readonly precomputedFlagStore: IConfigurationStore<PrecomputedFlag>,
private readonly subjectKey: string,
private readonly subjectAttributes: Attributes,
) {}

async fetchAndStorePrecomputedFlags(): Promise<void> {
const precomputedResponse = await this.httpClient.getPrecomputedFlags();
const precomputedResponse = await this.httpClient.getPrecomputedFlags({
subject_key: this.subjectKey,
subject_attributes: this.subjectAttributes,
});

if (!precomputedResponse?.flags) {
return;
}
Expand Down
Loading