Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
4 changes: 2 additions & 2 deletions src/client/eppo-client-with-bandits.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
IConfigurationWire,
IPrecomputedConfiguration,
IObfuscatedPrecomputedConfigurationResponse,
ConfigurationWireV1,
ConfigurationWireV1, configurationFromString,

Check failure on line 24 in src/client/eppo-client-with-bandits.spec.ts

View workflow job for this annotation

GitHub Actions / lint-test-sdk (18)

'ConfigurationWireV1' is defined but never used

Check warning on line 24 in src/client/eppo-client-with-bandits.spec.ts

View workflow job for this annotation

GitHub Actions / lint-test-sdk (18)

'ConfigurationWireV1' is defined but never used

Check warning on line 24 in src/client/eppo-client-with-bandits.spec.ts

View workflow job for this annotation

GitHub Actions / lint-test-sdk (18)

Insert `⏎·`

Check failure on line 24 in src/client/eppo-client-with-bandits.spec.ts

View workflow job for this annotation

GitHub Actions / lint-test-sdk (20)

'ConfigurationWireV1' is defined but never used

Check warning on line 24 in src/client/eppo-client-with-bandits.spec.ts

View workflow job for this annotation

GitHub Actions / lint-test-sdk (20)

'ConfigurationWireV1' is defined but never used

Check warning on line 24 in src/client/eppo-client-with-bandits.spec.ts

View workflow job for this annotation

GitHub Actions / lint-test-sdk (20)

Insert `⏎·`

Check failure on line 24 in src/client/eppo-client-with-bandits.spec.ts

View workflow job for this annotation

GitHub Actions / lint-test-sdk (22)

'ConfigurationWireV1' is defined but never used

Check warning on line 24 in src/client/eppo-client-with-bandits.spec.ts

View workflow job for this annotation

GitHub Actions / lint-test-sdk (22)

'ConfigurationWireV1' is defined but never used

Check warning on line 24 in src/client/eppo-client-with-bandits.spec.ts

View workflow job for this annotation

GitHub Actions / lint-test-sdk (22)

Insert `⏎·`

Check failure on line 24 in src/client/eppo-client-with-bandits.spec.ts

View workflow job for this annotation

GitHub Actions / lint-test-sdk (23)

'ConfigurationWireV1' is defined but never used

Check warning on line 24 in src/client/eppo-client-with-bandits.spec.ts

View workflow job for this annotation

GitHub Actions / lint-test-sdk (23)

'ConfigurationWireV1' is defined but never used

Check warning on line 24 in src/client/eppo-client-with-bandits.spec.ts

View workflow job for this annotation

GitHub Actions / lint-test-sdk (23)

Insert `⏎·`
} from '../configuration-wire/configuration-wire-types';
import { Evaluator, FlagEvaluation } from '../evaluator';
import {
Expand Down Expand Up @@ -137,7 +137,7 @@
}

describe('bootstrapped client', () => {
const banditFlagsConfig = ConfigurationWireV1.fromString(
const banditFlagsConfig = configurationFromString(
readMockConfigurationWireResponse(SHARED_BOOTSTRAP_BANDIT_FLAGS_FILE),
);

Expand Down
8 changes: 3 additions & 5 deletions src/client/eppo-client.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { AssignmentCache } from '../cache/abstract-assignment-cache';
import { IConfigurationStore } from '../configuration-store/configuration-store';
import { MemoryOnlyConfigurationStore } from '../configuration-store/memory.store';
import {
ConfigurationWireV1,
configurationFromString,
IConfigurationWire,
IObfuscatedPrecomputedConfigurationResponse,
ObfuscatedPrecomputedConfigurationResponse,
Expand Down Expand Up @@ -321,8 +321,6 @@ describe('EppoClient E2E test', () => {
});
});

const testCases = testCasesByFileName<IAssignmentTestCase>(ASSIGNMENT_TEST_DATA_DIR);

function testCasesAgainstClient(client: EppoClient, testCase: IAssignmentTestCase) {
const { flag, variationType, defaultValue, subjects } = testCase;

Expand Down Expand Up @@ -358,10 +356,10 @@ describe('EppoClient E2E test', () => {
const testCases = testCasesByFileName<IAssignmentTestCase>(ASSIGNMENT_TEST_DATA_DIR);

describe('boostrapped client', () => {
const bootstrapFlagsConfig = ConfigurationWireV1.fromString(
const bootstrapFlagsConfig = configurationFromString(
readMockConfigurationWireResponse(SHARED_BOOTSTRAP_FLAGS_FILE),
);
const bootstrapFlagsObfuscatedConfig = ConfigurationWireV1.fromString(
const bootstrapFlagsObfuscatedConfig = configurationFromString(
readMockConfigurationWireResponse(SHARED_BOOTSTRAP_FLAGS_OBFUSCATED_FILE),
);

Expand Down
6 changes: 3 additions & 3 deletions src/client/eppo-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@ import { MemoryOnlyConfigurationStore } from '../configuration-store/memory.stor
import {
ConfigurationWireV1,
IConfigurationWire,
inflateResponse,
IPrecomputedConfiguration,
PrecomputedConfiguration,
} from '../configuration-wire/configuration-wire-types';
import { inflateJsonObject } from '../configuration-wire/json-util';
import {
DEFAULT_INITIAL_CONFIG_REQUEST_RETRIES,
DEFAULT_POLL_CONFIG_REQUEST_RETRIES,
Expand Down Expand Up @@ -348,11 +348,11 @@ export default class EppoClient {
if (!configuration.config) {
throw new Error('Flag configuration not provided');
}
const flagConfigResponse: IUniversalFlagConfigResponse = inflateResponse(
const flagConfigResponse: IUniversalFlagConfigResponse = inflateJsonObject(
configuration.config.response,
);
const banditParamResponse: IBanditParametersResponse | undefined = configuration.bandits
? inflateResponse(configuration.bandits.response)
? inflateJsonObject(configuration.bandits.response)
: undefined;

// We need to run this method sync, but, because the configuration stores potentially have an async write at the end
Expand Down
21 changes: 11 additions & 10 deletions src/configuration-wire/configuration-wire-types.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import {
import { IUniversalFlagConfigResponse, IBanditParametersResponse } from '../http-client';
import { FormatEnum } from '../interfaces';

import { ConfigurationWireV1, deflateResponse, inflateResponse } from './configuration-wire-types';
import { ConfigurationWireV1 } from './configuration-wire-types';
import { deflateJsonObject, inflateJsonObject } from './json-util';

describe('Response String Type Safety', () => {
const mockFlagConfig: IUniversalFlagConfigResponse = readMockUFCResponse(
Expand All @@ -18,22 +19,22 @@ describe('Response String Type Safety', () => {

describe('deflateResponse and inflateResponse', () => {
it('should correctly serialize and deserialize flag config', () => {
const serialized = deflateResponse(mockFlagConfig);
const deserialized = inflateResponse(serialized);
const serialized = deflateJsonObject(mockFlagConfig);
const deserialized = inflateJsonObject(serialized);

expect(deserialized).toEqual(mockFlagConfig);
});

it('should correctly serialize and deserialize bandit config', () => {
const serialized = deflateResponse(mockBanditConfig);
const deserialized = inflateResponse(serialized);
const serialized = deflateJsonObject(mockBanditConfig);
const deserialized = inflateJsonObject(serialized);

expect(deserialized).toEqual(mockBanditConfig);
});

it('should maintain type information through serialization', () => {
const serialized = deflateResponse(mockFlagConfig);
const deserialized = inflateResponse(serialized);
const serialized = deflateJsonObject(mockFlagConfig);
const deserialized = inflateJsonObject(serialized);

// TypeScript compilation check: these should work
expect(deserialized.format).toBe(FormatEnum.SERVER);
Expand All @@ -54,7 +55,7 @@ describe('Response String Type Safety', () => {
if (!wirePacket.config) {
fail('Flag config not present in ConfigurationWire');
}
const deserializedConfig = inflateResponse(wirePacket.config.response);
const deserializedConfig = inflateJsonObject(wirePacket.config.response);
expect(deserializedConfig).toEqual(mockFlagConfig);
});

Expand All @@ -80,8 +81,8 @@ describe('Response String Type Safety', () => {
expect(wirePacket.bandits.etag).toBe('bandit-etag');

// Verify we can deserialize both responses
const deserializedConfig = inflateResponse(wirePacket.config.response);
const deserializedBandits = inflateResponse(wirePacket.bandits.response);
const deserializedConfig = inflateJsonObject(wirePacket.config.response);
const deserializedBandits = inflateJsonObject(wirePacket.bandits.response);

expect(deserializedConfig).toEqual(mockFlagConfig);
expect(deserializedBandits).toEqual(mockBanditConfig);
Expand Down
39 changes: 19 additions & 20 deletions src/configuration-wire/configuration-wire-types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { IUniversalFlagConfigResponse, IBanditParametersResponse } from '../http-client';
import { IBanditParametersResponse, IUniversalFlagConfigResponse } from '../http-client';
import {
Environment,
FormatEnum,
Expand All @@ -9,6 +9,20 @@ import {
import { obfuscatePrecomputedBanditMap, obfuscatePrecomputedFlags } from '../obfuscation';
import { ContextAttributes, FlagKey, HashedFlagKey } from '../types';

import { deflateJsonObject, inflateJsonObject, JsonString } from './json-util';

/**
* Builds an `IConfigurationWire` instance from the payload string.
* To generate the payload string, see `ConfigurationWireHelper.fetchBootstrapConfiguration`.
*
* @param payloadString
*/
export function configurationFromString(
payloadString: string | JsonString<IConfigurationWire>,
): IConfigurationWire {
return inflateJsonObject(payloadString as JsonString<IConfigurationWire>);
}

// Base interface for all configuration responses
interface IBasePrecomputedConfigurationResponse {
readonly format: FormatEnum.PRECOMPUTED;
Expand Down Expand Up @@ -169,28 +183,17 @@ export interface IConfigurationWire {
type UfcResponseType = IUniversalFlagConfigResponse | IBanditParametersResponse;

// The UFC responses are JSON-encoded strings so we can treat them as opaque blobs, but we also want to enforce type safety.
type ResponseString<T extends UfcResponseType> = string & {
readonly __brand: unique symbol;
readonly __type: T;
};
type UFCResponseString<T extends UfcResponseType> = JsonString<T>;

/**
* A wrapper around a server response that includes the response, etag, and fetchedAt timestamp.
*/
interface IConfigResponse<T extends UfcResponseType> {
readonly response: ResponseString<T>; // JSON-encoded server response
readonly response: UFCResponseString<T>; // JSON-encoded server response
readonly etag?: string; // Entity Tag - denotes a snapshot or version of the config.
readonly fetchedAt?: string; // ISO timestamp for when this config was fetched
}

export function inflateResponse<T extends UfcResponseType>(response: ResponseString<T>): T {
return JSON.parse(response) as T;
}

export function deflateResponse<T extends UfcResponseType>(value: T): ResponseString<T> {
return JSON.stringify(value) as ResponseString<T>;
}

export class ConfigurationWireV1 implements IConfigurationWire {
public readonly version = 1;

Expand All @@ -200,10 +203,6 @@ export class ConfigurationWireV1 implements IConfigurationWire {
readonly bandits?: IConfigResponse<IBanditParametersResponse>,
) {}

public static fromString(stringifiedPayload: string): IConfigurationWire {
return JSON.parse(stringifiedPayload) as IConfigurationWire;
}

public static fromResponses(
flagConfig: IUniversalFlagConfigResponse,
banditConfig?: IBanditParametersResponse,
Expand All @@ -213,13 +212,13 @@ export class ConfigurationWireV1 implements IConfigurationWire {
return new ConfigurationWireV1(
undefined,
{
response: deflateResponse(flagConfig),
response: deflateJsonObject(flagConfig),
fetchedAt: new Date().toISOString(),
etag: flagConfigEtag,
},
banditConfig
? {
response: deflateResponse(banditConfig),
response: deflateJsonObject(banditConfig),
fetchedAt: new Date().toISOString(),
etag: banditConfigEtag,
}
Expand Down
12 changes: 12 additions & 0 deletions src/configuration-wire/json-util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export type JsonString<T> = string & {
readonly __brand: unique symbol;
readonly __type: T;
};

export function inflateJsonObject<T>(response: JsonString<T>): T {
return JSON.parse(response) as T;
}

export function deflateJsonObject<T>(value: T): JsonString<T> {
return JSON.stringify(value) as JsonString<T>;
}
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import { ConfigurationStoreBundle } from './configuration-store/i-configuration-
import { MemoryStore, MemoryOnlyConfigurationStore } from './configuration-store/memory.store';
import { ConfigurationWireHelper } from './configuration-wire/configuration-wire-helper';
import {
configurationFromString,
IConfigurationWire,
IObfuscatedPrecomputedConfigurationResponse,
IPrecomputedConfigurationResponse,
Expand Down Expand Up @@ -159,6 +160,7 @@ export {
PrecomputedFlag,
FlagKey,
ConfigurationWireHelper,
configurationFromString,

// Test helpers
decodePrecomputedFlag,
Expand Down
Loading