diff --git a/Makefile b/Makefile index c23789b..d262eab 100644 --- a/Makefile +++ b/Makefile @@ -35,6 +35,7 @@ test-data: mkdir -p $(tempDir) git clone -b ${branchName} --depth 1 --single-branch ${githubRepoLink} ${gitDataDir} cp -r ${gitDataDir}ufc ${testDataDir} + cp -r ${gitDataDir}configuration-wire ${testDataDir} rm -rf ${tempDir} ## prepare @@ -49,4 +50,4 @@ prepare: test-data ## test .PHONY: test test: test test-data - yarn test:unit \ No newline at end of file + yarn test:unit diff --git a/docs/js-client-sdk.iclientconfig.eventingestionconfig.md b/docs/js-client-sdk.iclientconfig.eventingestionconfig.md index 47aa57a..f7bb096 100644 --- a/docs/js-client-sdk.iclientconfig.eventingestionconfig.md +++ b/docs/js-client-sdk.iclientconfig.eventingestionconfig.md @@ -15,5 +15,6 @@ eventIngestionConfig?: { maxRetryDelayMs?: number; maxRetries?: number; batchSize?: number; + maxQueueSize?: number; }; ``` diff --git a/docs/js-client-sdk.iclientconfig.md b/docs/js-client-sdk.iclientconfig.md index 7e4355c..367b89f 100644 --- a/docs/js-client-sdk.iclientconfig.md +++ b/docs/js-client-sdk.iclientconfig.md @@ -46,7 +46,7 @@ Description -{ deliveryIntervalMs?: number; retryIntervalMs?: number; maxRetryDelayMs?: number; maxRetries?: number; batchSize?: number; } +{ deliveryIntervalMs?: number; retryIntervalMs?: number; maxRetryDelayMs?: number; maxRetries?: number; batchSize?: number; maxQueueSize?: number; } diff --git a/docs/js-client-sdk.iprecomputedclientconfig.md b/docs/js-client-sdk.iprecomputedclientconfig.md index 7e394c3..37f20e7 100644 --- a/docs/js-client-sdk.iprecomputedclientconfig.md +++ b/docs/js-client-sdk.iprecomputedclientconfig.md @@ -38,7 +38,7 @@ Description -[subjectAttributes?](./js-client-sdk.iprecomputedclientconfig.subjectattributes.md) +[precompute](./js-client-sdk.iprecomputedclientconfig.precompute.md) @@ -46,32 +46,11 @@ Description -Record<string, AttributeType> +IPrecompute -_(Optional)_ Subject attributes to use for precomputed flag assignments. - - - - - -[subjectKey](./js-client-sdk.iprecomputedclientconfig.subjectkey.md) - - - - - - - -string - - - - -Subject key to use for precomputed flag assignments. - diff --git a/docs/js-client-sdk.iprecomputedclientconfig.subjectkey.md b/docs/js-client-sdk.iprecomputedclientconfig.precompute.md similarity index 52% rename from docs/js-client-sdk.iprecomputedclientconfig.subjectkey.md rename to docs/js-client-sdk.iprecomputedclientconfig.precompute.md index 3d7ba10..0dbe526 100644 --- a/docs/js-client-sdk.iprecomputedclientconfig.subjectkey.md +++ b/docs/js-client-sdk.iprecomputedclientconfig.precompute.md @@ -1,13 +1,11 @@ -[Home](./index.md) > [@eppo/js-client-sdk](./js-client-sdk.md) > [IPrecomputedClientConfig](./js-client-sdk.iprecomputedclientconfig.md) > [subjectKey](./js-client-sdk.iprecomputedclientconfig.subjectkey.md) +[Home](./index.md) > [@eppo/js-client-sdk](./js-client-sdk.md) > [IPrecomputedClientConfig](./js-client-sdk.iprecomputedclientconfig.md) > [precompute](./js-client-sdk.iprecomputedclientconfig.precompute.md) -## IPrecomputedClientConfig.subjectKey property - -Subject key to use for precomputed flag assignments. +## IPrecomputedClientConfig.precompute property **Signature:** ```typescript -subjectKey: string; +precompute: IPrecompute; ``` diff --git a/docs/js-client-sdk.iprecomputedclientconfig.subjectattributes.md b/docs/js-client-sdk.iprecomputedclientconfig.subjectattributes.md deleted file mode 100644 index 715fec6..0000000 --- a/docs/js-client-sdk.iprecomputedclientconfig.subjectattributes.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [@eppo/js-client-sdk](./js-client-sdk.md) > [IPrecomputedClientConfig](./js-client-sdk.iprecomputedclientconfig.md) > [subjectAttributes](./js-client-sdk.iprecomputedclientconfig.subjectattributes.md) - -## IPrecomputedClientConfig.subjectAttributes property - -Subject attributes to use for precomputed flag assignments. - -**Signature:** - -```typescript -subjectAttributes?: Record; -``` diff --git a/docs/js-client-sdk.iprecomputedclientconfigsync.assignmentlogger.md b/docs/js-client-sdk.iprecomputedclientconfigsync.assignmentlogger.md new file mode 100644 index 0000000..ba011e3 --- /dev/null +++ b/docs/js-client-sdk.iprecomputedclientconfigsync.assignmentlogger.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [@eppo/js-client-sdk](./js-client-sdk.md) > [IPrecomputedClientConfigSync](./js-client-sdk.iprecomputedclientconfigsync.md) > [assignmentLogger](./js-client-sdk.iprecomputedclientconfigsync.assignmentlogger.md) + +## IPrecomputedClientConfigSync.assignmentLogger property + +**Signature:** + +```typescript +assignmentLogger?: IAssignmentLogger; +``` diff --git a/docs/js-client-sdk.iprecomputedclientconfigsync.md b/docs/js-client-sdk.iprecomputedclientconfigsync.md new file mode 100644 index 0000000..2d334c3 --- /dev/null +++ b/docs/js-client-sdk.iprecomputedclientconfigsync.md @@ -0,0 +1,97 @@ + + +[Home](./index.md) > [@eppo/js-client-sdk](./js-client-sdk.md) > [IPrecomputedClientConfigSync](./js-client-sdk.iprecomputedclientconfigsync.md) + +## IPrecomputedClientConfigSync interface + +Configuration parameters for initializing the Eppo precomputed client. + +This interface is used for cases where precomputed assignments are available from an external process that can bootstrap the SDK client. + + precomputedConfiguration - The configuration as a string to bootstrap the client. assignmentLogger - Optional logger for assignment events. throwOnFailedInitialization - Optional flag to throw an error if initialization fails. + +**Signature:** + +```typescript +export interface IPrecomputedClientConfigSync +``` + +## Properties + + + + + +
+ +Property + + + + +Modifiers + + + + +Type + + + + +Description + + +
+ +[assignmentLogger?](./js-client-sdk.iprecomputedclientconfigsync.assignmentlogger.md) + + + + + + + +IAssignmentLogger + + + + +_(Optional)_ + + +
+ +[precomputedConfiguration](./js-client-sdk.iprecomputedclientconfigsync.precomputedconfiguration.md) + + + + + + + +string + + + + + +
+ +[throwOnFailedInitialization?](./js-client-sdk.iprecomputedclientconfigsync.throwonfailedinitialization.md) + + + + + + + +boolean + + + + +_(Optional)_ + + +
diff --git a/docs/js-client-sdk.iprecomputedclientconfigsync.precomputedconfiguration.md b/docs/js-client-sdk.iprecomputedclientconfigsync.precomputedconfiguration.md new file mode 100644 index 0000000..6f6726b --- /dev/null +++ b/docs/js-client-sdk.iprecomputedclientconfigsync.precomputedconfiguration.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [@eppo/js-client-sdk](./js-client-sdk.md) > [IPrecomputedClientConfigSync](./js-client-sdk.iprecomputedclientconfigsync.md) > [precomputedConfiguration](./js-client-sdk.iprecomputedclientconfigsync.precomputedconfiguration.md) + +## IPrecomputedClientConfigSync.precomputedConfiguration property + +**Signature:** + +```typescript +precomputedConfiguration: string; +``` diff --git a/docs/js-client-sdk.iprecomputedclientconfigsync.throwonfailedinitialization.md b/docs/js-client-sdk.iprecomputedclientconfigsync.throwonfailedinitialization.md new file mode 100644 index 0000000..e20f953 --- /dev/null +++ b/docs/js-client-sdk.iprecomputedclientconfigsync.throwonfailedinitialization.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [@eppo/js-client-sdk](./js-client-sdk.md) > [IPrecomputedClientConfigSync](./js-client-sdk.iprecomputedclientconfigsync.md) > [throwOnFailedInitialization](./js-client-sdk.iprecomputedclientconfigsync.throwonfailedinitialization.md) + +## IPrecomputedClientConfigSync.throwOnFailedInitialization property + +**Signature:** + +```typescript +throwOnFailedInitialization?: boolean; +``` diff --git a/docs/js-client-sdk.md b/docs/js-client-sdk.md index bdaed9d..38730ac 100644 --- a/docs/js-client-sdk.md +++ b/docs/js-client-sdk.md @@ -138,6 +138,21 @@ The purpose is for use-cases where the configuration is available from an extern This method should be called once on application startup. + + + +[offlinePrecomputedInit(config)](./js-client-sdk.offlineprecomputedinit.md) + + + + +Initializes the Eppo precomputed client with configuration parameters. + +The purpose is for use-cases where the precomputed assignments are available from an external process that can bootstrap the SDK. + +This method should be called once on application startup. + + @@ -197,5 +212,20 @@ Configuration interface for synchronous client initialization. Configuration for Eppo precomputed client initialization + + + +[IPrecomputedClientConfigSync](./js-client-sdk.iprecomputedclientconfigsync.md) + + + + +Configuration parameters for initializing the Eppo precomputed client. + +This interface is used for cases where precomputed assignments are available from an external process that can bootstrap the SDK client. + + precomputedConfiguration - The configuration as a string to bootstrap the client. assignmentLogger - Optional logger for assignment events. throwOnFailedInitialization - Optional flag to throw an error if initialization fails. + + diff --git a/docs/js-client-sdk.offlineprecomputedinit.md b/docs/js-client-sdk.offlineprecomputedinit.md new file mode 100644 index 0000000..61ffe3d --- /dev/null +++ b/docs/js-client-sdk.offlineprecomputedinit.md @@ -0,0 +1,59 @@ + + +[Home](./index.md) > [@eppo/js-client-sdk](./js-client-sdk.md) > [offlinePrecomputedInit](./js-client-sdk.offlineprecomputedinit.md) + +## offlinePrecomputedInit() function + +Initializes the Eppo precomputed client with configuration parameters. + +The purpose is for use-cases where the precomputed assignments are available from an external process that can bootstrap the SDK. + +This method should be called once on application startup. + +**Signature:** + +```typescript +export declare function offlinePrecomputedInit(config: IPrecomputedClientConfigSync): EppoPrecomputedClient; +``` + +## Parameters + + + +
+ +Parameter + + + + +Type + + + + +Description + + +
+ +config + + + + +[IPrecomputedClientConfigSync](./js-client-sdk.iprecomputedclientconfigsync.md) + + + + +precomputed client configuration + + +
+**Returns:** + +EppoPrecomputedClient + +a singleton precomputed client instance + diff --git a/js-client-sdk.api.md b/js-client-sdk.api.md index 670df78..a6adacc 100644 --- a/js-client-sdk.api.md +++ b/js-client-sdk.api.md @@ -122,6 +122,7 @@ export interface IClientConfig extends IBaseRequestConfig { maxRetryDelayMs?: number; maxRetries?: number; batchSize?: number; + maxQueueSize?: number; }; forceReinitialize?: boolean; maxCacheAgeSeconds?: number; @@ -149,8 +150,20 @@ export function init(config: IClientConfig): Promise; // @public export interface IPrecomputedClientConfig extends IBaseRequestConfig { - subjectAttributes?: Record; - subjectKey: string; + // Warning: (ae-forgotten-export) The symbol "IPrecompute" needs to be exported by the entry point index.d.ts + // + // (undocumented) + precompute: IPrecompute; +} + +// @public +export interface IPrecomputedClientConfigSync { + // (undocumented) + assignmentLogger?: IAssignmentLogger; + // (undocumented) + precomputedConfiguration: string; + // (undocumented) + throwOnFailedInitialization?: boolean; } export { ObfuscatedFlag } @@ -158,6 +171,9 @@ export { ObfuscatedFlag } // @public export function offlineInit(config: IClientConfigSync): EppoClient; +// @public +export function offlinePrecomputedInit(config: IPrecomputedClientConfigSync): EppoPrecomputedClient; + // @public export function precomputedInit(config: IPrecomputedClientConfig): Promise; diff --git a/package.json b/package.json index 4020c81..60b7fb9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@eppo/js-client-sdk", - "version": "3.9.1", + "version": "3.9.2-alpha.0", "description": "Eppo SDK for client-side JavaScript applications", "main": "dist/index.js", "files": [ @@ -36,6 +36,7 @@ "@microsoft/api-extractor": "^7.48.1", "@types/chrome": "^0.0.268", "@types/jest": "^29.5.11", + "@types/spark-md5": "^3.0.5", "@typescript-eslint/eslint-plugin": "^5.13.0", "@typescript-eslint/parser": "^5.13.0", "eslint": "^8.17.0", @@ -59,7 +60,7 @@ "webpack-cli": "^6.0.1" }, "dependencies": { - "@eppo/js-client-sdk-common": "^4.7.1" + "@eppo/js-client-sdk-common": "4.8.0-alpha.1" }, "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" } diff --git a/src/i-client-config.ts b/src/i-client-config.ts index d2acee0..7f67f61 100644 --- a/src/i-client-config.ts +++ b/src/i-client-config.ts @@ -63,10 +63,9 @@ interface IBaseRequestConfig { } /** - * Configuration for Eppo precomputed client initialization - * @public + * Configuration for precomputed flag assignments */ -export interface IPrecomputedClientConfig extends IBaseRequestConfig { +interface IPrecompute { /** * Subject key to use for precomputed flag assignments. */ @@ -78,6 +77,14 @@ export interface IPrecomputedClientConfig extends IBaseRequestConfig { subjectAttributes?: Record; } +/** + * Configuration for Eppo precomputed client initialization + * @public + */ +export interface IPrecomputedClientConfig extends IBaseRequestConfig { + precompute: IPrecompute; +} + /** * Configuration for regular client initialization * @public diff --git a/src/index.spec.ts b/src/index.spec.ts index 4f7f510..660780b 100644 --- a/src/index.spec.ts +++ b/src/index.spec.ts @@ -5,12 +5,14 @@ import { createHash } from 'crypto'; import { + applicationLogger, AssignmentCache, constants, EppoClient, Flag, HybridConfigurationStore, IAsyncStore, + IPrecomputedConfigurationResponse, VariationType, } from '@eppo/js-client-sdk-common'; import * as td from 'testdouble'; @@ -18,9 +20,11 @@ import * as td from 'testdouble'; import { getTestAssignments, IAssignmentTestCase, + MOCK_PRECOMPUTED_WIRE_FILE, MOCK_UFC_RESPONSE_FILE, OBFUSCATED_MOCK_UFC_RESPONSE_FILE, readAssignmentTestData, + readMockPrecomputedResponse, readMockUfcResponse, validateTestAssignments, } from '../test/testHelpers'; @@ -35,6 +39,7 @@ import { IAssignmentLogger, init, offlineInit, + offlinePrecomputedInit, precomputedInit, } from './index'; @@ -1065,59 +1070,14 @@ describe('EppoPrecomputedJSClient E2E test', () => { beforeAll(async () => { global.fetch = jest.fn(() => { + const precomputedConfiguration = readMockPrecomputedResponse(MOCK_PRECOMPUTED_WIRE_FILE); + const precomputedResponse: IPrecomputedConfigurationResponse = JSON.parse( + JSON.parse(precomputedConfiguration).precomputed.response, + ); return Promise.resolve({ ok: true, status: 200, - json: () => - Promise.resolve({ - createdAt: '2024-11-18T14:23:39.456Z', - format: 'PRECOMPUTED', - environment: { - name: 'Test', - }, - flags: { - 'string-flag': { - allocationKey: 'allocation-123', - variationKey: 'variation-123', - variationType: 'STRING', - variationValue: 'red', - extraLogging: {}, - doLog: true, - }, - 'boolean-flag': { - allocationKey: 'allocation-124', - variationKey: 'variation-124', - variationType: 'BOOLEAN', - variationValue: true, - extraLogging: {}, - doLog: true, - }, - 'numeric-flag': { - allocationKey: 'allocation-126', - variationKey: 'variation-126', - variationType: 'NUMERIC', - variationValue: 3.14, - extraLogging: {}, - doLog: true, - }, - 'integer-flag': { - allocationKey: 'allocation-125', - variationKey: 'variation-125', - variationType: 'INTEGER', - variationValue: 42, - extraLogging: {}, - doLog: true, - }, - 'json-flag': { - allocationKey: 'allocation-127', - variationKey: 'variation-127', - variationType: 'JSON', - variationValue: '{"key": "value", "number": 123}', - extraLogging: {}, - doLog: true, - }, - }, - }), + json: () => Promise.resolve(precomputedResponse), }); }) as jest.Mock; @@ -1127,8 +1087,10 @@ describe('EppoPrecomputedJSClient E2E test', () => { apiKey: 'dummy', baseUrl: 'http://127.0.0.1:4000', assignmentLogger: mockLogger, - subjectKey: 'test-subject', - subjectAttributes: { attr1: 'value1' }, + precompute: { + subjectKey: 'test-subject', + subjectAttributes: { attr1: 'value1' }, + }, }); }); @@ -1178,6 +1140,79 @@ describe('EppoPrecomputedJSClient E2E test', () => { }); }); +describe('offlinePrecomputedInit', () => { + let mockLogger: IAssignmentLogger; + let precomputedConfiguration: string; + + beforeAll(() => { + precomputedConfiguration = readMockPrecomputedResponse(MOCK_PRECOMPUTED_WIRE_FILE); + }); + + beforeEach(() => { + mockLogger = td.object(); + }); + + afterEach(() => { + td.reset(); + }); + + it('initializes with precomputed assignments', () => { + const client = offlinePrecomputedInit({ + precomputedConfiguration, + assignmentLogger: mockLogger, + }); + + expect(client.getStringAssignment('string-flag', 'default')).toBe('red'); + expect(td.explain(mockLogger.logAssignment).callCount).toBe(1); + expect(td.explain(mockLogger.logAssignment).calls[0]?.args[0]).toMatchObject({ + subject: 'test-subject-key', + featureFlag: 'string-flag', + allocation: 'allocation-123', + variation: 'variation-123', + subjectAttributes: { + buildNumber: 42, + hasPushEnabled: false, + language: 'en-US', + lastLoginDays: 3, + lifetimeValue: 543.21, + platform: 'ios', + }, + }); + }); + + it('initializes without an assignment logger', () => { + const client = offlinePrecomputedInit({ precomputedConfiguration }); + + expect(client.getStringAssignment('string-flag', 'default')).toBe('red'); + }); + + it('logs a warning on re-initialization', () => { + td.replace(applicationLogger, 'warn'); + EppoPrecomputedJSClient.initialized = false; + // First initialization there is no client to spy on, so we only test that no warning is logged + offlinePrecomputedInit({ + precomputedConfiguration, + assignmentLogger: mockLogger, + }); + td.verify( + applicationLogger.warn(td.matchers.contains('Precomputed client is being re-initialized.')), + { times: 0 }, + ); + // Replace instance with a mock and check that shutdown is called on re-initialization + const mockInstance = td.object(); + EppoPrecomputedJSClient.instance = mockInstance; + offlinePrecomputedInit({ + precomputedConfiguration, + assignmentLogger: mockLogger, + }); + td.verify(mockInstance.stopPolling(), { times: 1 }); + td.verify( + applicationLogger.warn(td.matchers.contains('Precomputed client is being re-initialized.')), + { times: 1 }, + ); + }); +}); + describe('EppoClient config', () => { it('should initialize event dispatcher with default values', async () => { global.fetch = jest.fn(() => { diff --git a/src/index.ts b/src/index.ts index aa52011..e1af51e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -18,7 +18,10 @@ import { BoundedEventQueue, validation, Event, + IConfigurationWire, + Subject, } from '@eppo/js-client-sdk-common'; +import { IObfuscatedPrecomputedConfigurationResponse } from '@eppo/js-client-sdk-common/src/configuration'; import { assignmentCacheFactory } from './cache/assignment-cache-factory'; import HybridAssignmentCache from './cache/hybrid-assignment-cache'; @@ -510,10 +513,7 @@ export function getConfigUrl(apiKey: string, baseUrl?: string): URL { * @public */ export class EppoPrecomputedJSClient extends EppoPrecomputedClient { - // Use an empty memory-only configuration store - public static instance: EppoPrecomputedJSClient = new EppoPrecomputedJSClient( - memoryOnlyPrecomputedFlagsStore, - ); + public static instance: EppoPrecomputedJSClient; public static initialized = false; public getStringAssignment(flagKey: string, defaultValue: string): string { @@ -557,14 +557,16 @@ export class EppoPrecomputedJSClient extends EppoPrecomputedClient { export async function precomputedInit( config: IPrecomputedClientConfig, ): Promise { + if (EppoPrecomputedJSClient.instance) { + return EppoPrecomputedJSClient.instance; + } + validation.validateNotBlank(config.apiKey, 'API key required'); - validation.validateNotBlank(config.subjectKey, 'Subject key required'); + validation.validateNotBlank(config.precompute.subjectKey, 'Subject key required'); - const instance = EppoPrecomputedJSClient.instance; const { apiKey, - subjectKey, - subjectAttributes = {}, + precompute: { subjectKey, subjectAttributes = {} }, baseUrl, requestTimeoutMs, numInitialRequestRetries, @@ -575,19 +577,12 @@ export async function precomputedInit( skipInitialRequest = false, } = config; - // Set up assignment logger and cache - instance.setAssignmentLogger(config.assignmentLogger); - // Set up parameters for requesting updated configurations - const precomputedFlagsRequestParameters: PrecomputedFlagsRequestParameters = { + const requestParameters: PrecomputedFlagsRequestParameters = { apiKey, sdkName, sdkVersion, baseUrl, - precompute: { - subjectKey, - subjectAttributes, - }, requestTimeoutMs, numInitialRequestRetries, numPollRequestRetries, @@ -597,14 +592,113 @@ export async function precomputedInit( throwOnFailedInitialization: true, // always use true here as underlying instance fetch is surrounded by try/catch skipInitialPoll: skipInitialRequest, }; - instance.setSubjectAndPrecomputedFlagsRequestParameters(precomputedFlagsRequestParameters); - await instance.fetchPrecomputedFlags(); + const subject: Subject = { subjectKey, subjectAttributes }; + + EppoPrecomputedJSClient.instance = new EppoPrecomputedJSClient({ + precomputedFlagStore: memoryOnlyPrecomputedFlagsStore, + requestParameters, + subject, + }); + + EppoPrecomputedJSClient.instance.setAssignmentLogger(config.assignmentLogger); + await EppoPrecomputedJSClient.instance.fetchPrecomputedFlags(); EppoPrecomputedJSClient.initialized = true; return EppoPrecomputedJSClient.instance; } +/** + * Configuration parameters for initializing the Eppo precomputed client. + * + * This interface is used for cases where precomputed assignments are available + * from an external process that can bootstrap the SDK client. + * + * @param precomputedConfiguration - The configuration as a string to bootstrap the client. + * @param assignmentLogger - Optional logger for assignment events. + * @param throwOnFailedInitialization - Optional flag to throw an error if initialization fails. + * @public + */ +export interface IPrecomputedClientConfigSync { + precomputedConfiguration: string; + assignmentLogger?: IAssignmentLogger; + throwOnFailedInitialization?: boolean; +} + +/** + * Initializes the Eppo precomputed client with configuration parameters. + * + * The purpose is for use-cases where the precomputed assignments are available from an external process + * that can bootstrap the SDK. + * + * This method should be called once on application startup. + * + * @param config - precomputed client configuration + * @returns a singleton precomputed client instance + * @public + */ +export function offlinePrecomputedInit( + config: IPrecomputedClientConfigSync, +): EppoPrecomputedClient { + const throwOnFailedInitialization = config.throwOnFailedInitialization ?? true; + + const configurationWire: IConfigurationWire = JSON.parse(config.precomputedConfiguration); + if (!configurationWire.precomputed) { + const errorMessage = 'Invalid precomputed configuration wire'; + if (throwOnFailedInitialization) { + throw new Error(errorMessage); + } else { + applicationLogger.error('[Eppo SDK] ${errorMessage}'); + return EppoPrecomputedJSClient.instance; + } + } + const { subjectKey, subjectAttributes, response } = configurationWire.precomputed; + const parsedResponse: IObfuscatedPrecomputedConfigurationResponse = JSON.parse(response); + + try { + const memoryOnlyPrecomputedStore = precomputedFlagsStorageFactory(); + memoryOnlyPrecomputedStore + .setEntries(parsedResponse.flags) + .catch((err) => + applicationLogger.warn('Error setting precomputed assignments for memory-only store', err), + ); + memoryOnlyPrecomputedStore.salt = parsedResponse.salt; + + const subject: Subject = { + subjectKey, + subjectAttributes: subjectAttributes ?? {}, + }; + + shutdownEppoPrecomputedClient(); + EppoPrecomputedJSClient.instance = new EppoPrecomputedJSClient({ + precomputedFlagStore: memoryOnlyPrecomputedStore, + subject, + }); + + if (config.assignmentLogger) { + EppoPrecomputedJSClient.instance.setAssignmentLogger(config.assignmentLogger); + } + } catch (error) { + applicationLogger.warn( + '[Eppo SDK] Encountered an error initializing precomputed client, assignment calls will return the default value and not be logged', + ); + if (throwOnFailedInitialization) { + throw error; + } + } + + EppoPrecomputedJSClient.initialized = true; + return EppoPrecomputedJSClient.instance; +} + +function shutdownEppoPrecomputedClient() { + if (EppoPrecomputedJSClient.instance && EppoPrecomputedJSClient.initialized) { + EppoPrecomputedJSClient.instance.stopPolling(); + EppoPrecomputedJSClient.initialized = false; + applicationLogger.warn('[Eppo SDK] Precomputed client is being re-initialized.'); + } +} + /** * Used to access a singleton SDK precomputed client instance. * Use the method after calling precomputedInit() to initialize the client. diff --git a/test/testHelpers.ts b/test/testHelpers.ts index 98fb31f..cb73441 100644 --- a/test/testHelpers.ts +++ b/test/testHelpers.ts @@ -1,6 +1,12 @@ import * as fs from 'fs'; -import { Flag, VariationType, AttributeType } from '@eppo/js-client-sdk-common'; +import { + Flag, + VariationType, + AttributeType, + PrecomputedFlag, + FormatEnum, +} from '@eppo/js-client-sdk-common'; export const TEST_DATA_DIR = './test/data/ufc/'; export const ASSIGNMENT_TEST_DATA_DIR = TEST_DATA_DIR + 'tests/'; @@ -8,6 +14,11 @@ const MOCK_UFC_FILENAME = 'flags-v1'; export const MOCK_UFC_RESPONSE_FILE = `${MOCK_UFC_FILENAME}.json`; export const OBFUSCATED_MOCK_UFC_RESPONSE_FILE = `${MOCK_UFC_FILENAME}-obfuscated.json`; +const TEST_CONFIGURATION_WIRE_DATA_DIR = './test/data/configuration-wire/'; +const MOCK_PRECOMPUTED_FILENAME = 'precomputed-v1'; +export const MOCK_PRECOMPUTED_WIRE_FILE = `${MOCK_PRECOMPUTED_FILENAME}.json`; +export const MOCK_DEOBFUSCATED_PRECOMPUTED_RESPONSE_FILE = `${MOCK_PRECOMPUTED_WIRE_FILE}-deobfuscated.json`; + export enum ValueTestType { BoolType = 'boolean', NumericType = 'numeric', @@ -15,6 +26,26 @@ export enum ValueTestType { JSONType = 'json', } +interface Environment { + name: string; +} + +export interface IConfigurationWire { + version: number; + precomputed: { + subjectKey: string; + subjectAttributes: Record; + fetchedAt: string; + response: { + createdAt: string; + format: FormatEnum; + obfuscated: boolean; + environment: Environment; + flags: Record; + }; + }; +} + export interface SubjectTestCase { subjectKey: string; subjectAttributes: Record; @@ -34,6 +65,10 @@ export function readMockUfcResponse(filename: string): { return JSON.parse(fs.readFileSync(TEST_DATA_DIR + filename, 'utf-8')); } +export function readMockPrecomputedResponse(filename: string): string { + return fs.readFileSync(TEST_CONFIGURATION_WIRE_DATA_DIR + filename, 'utf-8'); +} + export function readAssignmentTestData(): IAssignmentTestCase[] { const testCaseData: IAssignmentTestCase[] = []; const testCaseFiles = fs.readdirSync(ASSIGNMENT_TEST_DATA_DIR); diff --git a/yarn.lock b/yarn.lock index 0bc2cbe..93813b2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -380,10 +380,10 @@ resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.6.3.tgz#f13c7c205915eb91ae54c557f5e92bddd8be0e83" integrity sha512-4B4OijXeVNOPZlYA2oEwWOTkzyltLao+xbotHQeqN++Rv27Y6s818+n2Qkp8q+Fxhn0t/5lA5X1Mxktud8eayQ== -"@eppo/js-client-sdk-common@^4.7.1": - version "4.7.1" - resolved "https://registry.yarnpkg.com/@eppo/js-client-sdk-common/-/js-client-sdk-common-4.7.1.tgz#8a0776055604af65d0e0f8410d4756aa3117992f" - integrity sha512-8+5WbFN1EvsS5Ba/qakjDGEhp9loTxSvVHeWaQXKKLXxV+5AhFNOl+d8jSwOkLnP+Qr5D9w1eO7lfzuNDkUcWw== +"@eppo/js-client-sdk-common@4.8.0-alpha.1": + version "4.8.0-alpha.1" + resolved "https://registry.yarnpkg.com/@eppo/js-client-sdk-common/-/js-client-sdk-common-4.8.0-alpha.1.tgz#3c5c5f9fad5aa4ba582f0f9ea09b5ee60456e4cf" + integrity sha512-sB/3D/aj4TvTw44mxb2hWNGB22FrJzvHrQxQ6eGGeTBZkrBJz4wnl3WcvVjiiNPKBJpKxYGs5C0kM/++IHfH6A== dependencies: buffer "npm:@eppo/buffer@6.2.0" js-base64 "^3.7.7" @@ -1053,6 +1053,11 @@ resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.8.tgz#8268a8c57a3e4abd25c165ecd36237db7948a55e" integrity sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ== +"@types/spark-md5@^3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@types/spark-md5/-/spark-md5-3.0.5.tgz#eddec8639217e518c26e9e221ff56bf5f5f5c900" + integrity sha512-lWf05dnD42DLVKQJZrDHtWFidcLrHuip01CtnC2/S6AMhX4t9ZlEUj4iuRlAnts0PQk7KESOqKxeGE/b6sIPGg== + "@types/stack-utils@^2.0.0": version "2.0.1" resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c"