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"