diff --git a/docs/js-client-sdk.buildstoragekeysuffix.md b/docs/js-client-sdk.eppojsclient.buildandinit.md similarity index 50% rename from docs/js-client-sdk.buildstoragekeysuffix.md rename to docs/js-client-sdk.eppojsclient.buildandinit.md index 160bb3c..51a91ed 100644 --- a/docs/js-client-sdk.buildstoragekeysuffix.md +++ b/docs/js-client-sdk.eppojsclient.buildandinit.md @@ -1,15 +1,13 @@ -[Home](./index.md) > [@eppo/js-client-sdk](./js-client-sdk.md) > [buildStorageKeySuffix](./js-client-sdk.buildstoragekeysuffix.md) +[Home](./index.md) > [@eppo/js-client-sdk](./js-client-sdk.md) > [EppoJSClient](./js-client-sdk.eppojsclient.md) > [buildAndInit](./js-client-sdk.eppojsclient.buildandinit.md) -## buildStorageKeySuffix() function - -Builds a storage key suffix from an API key. +## EppoJSClient.buildAndInit() method **Signature:** ```typescript -export declare function buildStorageKeySuffix(apiKey: string): string; +static buildAndInit(config: IClientConfig): EppoJSClient; ``` ## Parameters @@ -32,24 +30,20 @@ Description -apiKey +config -string +[IClientConfig](./js-client-sdk.iclientconfig.md) -The API key to build the suffix from - **Returns:** -string - -A string suffix for storage keys +[EppoJSClient](./js-client-sdk.eppojsclient.md) diff --git a/docs/js-client-sdk.eppojsclient.initialized.md b/docs/js-client-sdk.eppojsclient.initialized.md index 74ed212..1fb3518 100644 --- a/docs/js-client-sdk.eppojsclient.initialized.md +++ b/docs/js-client-sdk.eppojsclient.initialized.md @@ -4,6 +4,11 @@ ## EppoJSClient.initialized property +> Warning: This API is now obsolete. +> +> use `instance.isInitialized()` instead. +> + **Signature:** ```typescript diff --git a/docs/js-client-sdk.eppojsclient.instance.md b/docs/js-client-sdk.eppojsclient.instance.md index 2c9196e..fd4a92c 100644 --- a/docs/js-client-sdk.eppojsclient.instance.md +++ b/docs/js-client-sdk.eppojsclient.instance.md @@ -4,7 +4,10 @@ ## EppoJSClient.instance property -@deprecated. use `getInstance()` instead. +> Warning: This API is now obsolete. +> +> use `getInstance()` instead. +> **Signature:** diff --git a/docs/js-client-sdk.eppojsclient.md b/docs/js-client-sdk.eppojsclient.md index 704c9c6..a0e3f21 100644 --- a/docs/js-client-sdk.eppojsclient.md +++ b/docs/js-client-sdk.eppojsclient.md @@ -72,8 +72,6 @@ boolean -@deprecated. use `getInstance()` instead. - @@ -98,6 +96,20 @@ Description +[buildAndInit(config)](./js-client-sdk.eppojsclient.buildandinit.md) + + + + +`static` + + + + + + + + [getBanditAction(flagKey, subjectKey, subjectAttributes, actions, defaultValue)](./js-client-sdk.eppojsclient.getbanditaction.md) @@ -263,5 +275,19 @@ Description + + + +[waitForConfiguration()](./js-client-sdk.eppojsclient.waitforconfiguration.md) + + + + + + + +Resolves when the EppoClient has completed its initialization. + + diff --git a/docs/js-client-sdk.eppojsclient.waitforconfiguration.md b/docs/js-client-sdk.eppojsclient.waitforconfiguration.md new file mode 100644 index 0000000..eff4247 --- /dev/null +++ b/docs/js-client-sdk.eppojsclient.waitforconfiguration.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [@eppo/js-client-sdk](./js-client-sdk.md) > [EppoJSClient](./js-client-sdk.eppojsclient.md) > [waitForConfiguration](./js-client-sdk.eppojsclient.waitforconfiguration.md) + +## EppoJSClient.waitForConfiguration() method + +Resolves when the EppoClient has completed its initialization. + +**Signature:** + +```typescript +waitForConfiguration(): Promise; +``` +**Returns:** + +Promise<void> + diff --git a/docs/js-client-sdk.iclientconfig.enableoverrides.md b/docs/js-client-sdk.iclientconfig.enableoverrides.md deleted file mode 100644 index f83b330..0000000 --- a/docs/js-client-sdk.iclientconfig.enableoverrides.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [@eppo/js-client-sdk](./js-client-sdk.md) > [IClientConfig](./js-client-sdk.iclientconfig.md) > [enableOverrides](./js-client-sdk.iclientconfig.enableoverrides.md) - -## IClientConfig.enableOverrides property - -Enable the Overrides Store for local flag overrides. (default: false) - -**Signature:** - -```typescript -enableOverrides?: boolean; -``` diff --git a/docs/js-client-sdk.iclientconfig.eventingestionconfig.md b/docs/js-client-sdk.iclientconfig.eventingestionconfig.md deleted file mode 100644 index f7bb096..0000000 --- a/docs/js-client-sdk.iclientconfig.eventingestionconfig.md +++ /dev/null @@ -1,20 +0,0 @@ - - -[Home](./index.md) > [@eppo/js-client-sdk](./js-client-sdk.md) > [IClientConfig](./js-client-sdk.iclientconfig.md) > [eventIngestionConfig](./js-client-sdk.iclientconfig.eventingestionconfig.md) - -## IClientConfig.eventIngestionConfig property - -Configuration settings for the event dispatcher - -**Signature:** - -```typescript -eventIngestionConfig?: { - deliveryIntervalMs?: number; - retryIntervalMs?: number; - maxRetryDelayMs?: number; - maxRetries?: number; - batchSize?: number; - maxQueueSize?: number; - }; -``` diff --git a/docs/js-client-sdk.iclientconfig.forcereinitialize.md b/docs/js-client-sdk.iclientconfig.forcereinitialize.md deleted file mode 100644 index 837c284..0000000 --- a/docs/js-client-sdk.iclientconfig.forcereinitialize.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [@eppo/js-client-sdk](./js-client-sdk.md) > [IClientConfig](./js-client-sdk.iclientconfig.md) > [forceReinitialize](./js-client-sdk.iclientconfig.forcereinitialize.md) - -## IClientConfig.forceReinitialize property - -Force reinitialize the SDK if it is already initialized. - -**Signature:** - -```typescript -forceReinitialize?: boolean; -``` diff --git a/docs/js-client-sdk.iclientconfig.maxcacheageseconds.md b/docs/js-client-sdk.iclientconfig.maxcacheageseconds.md deleted file mode 100644 index b942b01..0000000 --- a/docs/js-client-sdk.iclientconfig.maxcacheageseconds.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [@eppo/js-client-sdk](./js-client-sdk.md) > [IClientConfig](./js-client-sdk.iclientconfig.md) > [maxCacheAgeSeconds](./js-client-sdk.iclientconfig.maxcacheageseconds.md) - -## IClientConfig.maxCacheAgeSeconds property - -Maximum age, in seconds, previously cached values are considered valid until new values will be fetched (default: 0) - -**Signature:** - -```typescript -maxCacheAgeSeconds?: number; -``` diff --git a/docs/js-client-sdk.iclientconfig.md b/docs/js-client-sdk.iclientconfig.md index 3ddcf8f..a53f939 100644 --- a/docs/js-client-sdk.iclientconfig.md +++ b/docs/js-client-sdk.iclientconfig.md @@ -2,209 +2,20 @@ [Home](./index.md) > [@eppo/js-client-sdk](./js-client-sdk.md) > [IClientConfig](./js-client-sdk.iclientconfig.md) -## IClientConfig interface +## IClientConfig type -Configuration for regular client initialization - -**Signature:** - -```typescript -export interface IClientConfig extends IBaseRequestConfig -``` -**Extends:** IBaseRequestConfig - -## Properties - - - - - - - - - - - -
- -Property - - - - -Modifiers - - - - -Type - - - - -Description - - -
- -[enableOverrides?](./js-client-sdk.iclientconfig.enableoverrides.md) - - - - - - - -boolean - - - - -_(Optional)_ Enable the Overrides Store for local flag overrides. (default: false) - - -
- -[eventIngestionConfig?](./js-client-sdk.iclientconfig.eventingestionconfig.md) - - - - - - - -{ deliveryIntervalMs?: number; retryIntervalMs?: number; maxRetryDelayMs?: number; maxRetries?: number; batchSize?: number; maxQueueSize?: number; } - - - - -_(Optional)_ Configuration settings for the event dispatcher - - -
- -[forceReinitialize?](./js-client-sdk.iclientconfig.forcereinitialize.md) - - - - - - - -boolean - - - - -_(Optional)_ Force reinitialize the SDK if it is already initialized. - - -
- -[maxCacheAgeSeconds?](./js-client-sdk.iclientconfig.maxcacheageseconds.md) - - - - - - - -number +Configuration for regular client initialization Create your initialization options object as one large object: +const options { apiKey = 'MY SDK KEY', assignmentLogger, maxCacheAgeSeconds = 30, } - +OR, build separate objects for your config and destructure them at call to `init`. -_(Optional)_ Maximum age, in seconds, previously cached values are considered valid until new values will be fetched (default: 0) +const apiOptions: IApiOptions = { apiKey = 'MY SDK KEY'}; const loggerOptions: ILoggerOptions = {assignmentLogger, banditLogger}; const eventOptions: IEventOptions = { ... }; +const eppoClient = init({...apiOptions, ...loggerOptions, ...eventOptions}); -
- -[overridesStorageKey?](./js-client-sdk.iclientconfig.overridesstoragekey.md) - - - - - - - -string - - - - -_(Optional)_ The key to use for the overrides store. - - -
- -[persistentStore?](./js-client-sdk.iclientconfig.persistentstore.md) - - - - - - - -IAsyncStore<Flag> - - - - -_(Optional)_ A custom class to use for storing flag configurations. This is useful for cases where you want to use a different storage mechanism than the default storage provided by the SDK. - - -
- -[throwOnFailedInitialization?](./js-client-sdk.iclientconfig.throwonfailedinitialization.md) - - - - - - - -boolean - - - - -_(Optional)_ Throw an error if unable to fetch an initial configuration during initialization. (default: true) - - -
- -[updateOnFetch?](./js-client-sdk.iclientconfig.updateonfetch.md) - - - - - - - -ServingStoreUpdateStrategy - - - - -_(Optional)_ Sets how the configuration is updated after a successful fetch - always: immediately start using the new configuration - expired: immediately start using the new configuration only if the current one has expired - empty: only use the new configuration if the current one is both expired and uninitialized/empty - - -
- -[useExpiredCache?](./js-client-sdk.iclientconfig.useexpiredcache.md) - - - - - - - -boolean - - - - -_(Optional)_ Whether initialization will be considered successfully complete if expired cache values are loaded. If false, initialization will always wait for a fetch if cached values are expired. (default: false) - +**Signature:** -
+```typescript +export declare type IClientConfig = IApiOptions & ILoggers & IEventOptions & IStorageOptions & IPollingOptions & OverridesConfig; +``` diff --git a/docs/js-client-sdk.iclientconfig.overridesstoragekey.md b/docs/js-client-sdk.iclientconfig.overridesstoragekey.md deleted file mode 100644 index e2954b3..0000000 --- a/docs/js-client-sdk.iclientconfig.overridesstoragekey.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [@eppo/js-client-sdk](./js-client-sdk.md) > [IClientConfig](./js-client-sdk.iclientconfig.md) > [overridesStorageKey](./js-client-sdk.iclientconfig.overridesstoragekey.md) - -## IClientConfig.overridesStorageKey property - -The key to use for the overrides store. - -**Signature:** - -```typescript -overridesStorageKey?: string; -``` diff --git a/docs/js-client-sdk.iclientconfig.persistentstore.md b/docs/js-client-sdk.iclientconfig.persistentstore.md deleted file mode 100644 index 08a89a2..0000000 --- a/docs/js-client-sdk.iclientconfig.persistentstore.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [@eppo/js-client-sdk](./js-client-sdk.md) > [IClientConfig](./js-client-sdk.iclientconfig.md) > [persistentStore](./js-client-sdk.iclientconfig.persistentstore.md) - -## IClientConfig.persistentStore property - -A custom class to use for storing flag configurations. This is useful for cases where you want to use a different storage mechanism than the default storage provided by the SDK. - -**Signature:** - -```typescript -persistentStore?: IAsyncStore; -``` diff --git a/docs/js-client-sdk.iclientconfig.throwonfailedinitialization.md b/docs/js-client-sdk.iclientconfig.throwonfailedinitialization.md deleted file mode 100644 index 35dfe23..0000000 --- a/docs/js-client-sdk.iclientconfig.throwonfailedinitialization.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [@eppo/js-client-sdk](./js-client-sdk.md) > [IClientConfig](./js-client-sdk.iclientconfig.md) > [throwOnFailedInitialization](./js-client-sdk.iclientconfig.throwonfailedinitialization.md) - -## IClientConfig.throwOnFailedInitialization property - -Throw an error if unable to fetch an initial configuration during initialization. (default: true) - -**Signature:** - -```typescript -throwOnFailedInitialization?: boolean; -``` diff --git a/docs/js-client-sdk.iclientconfig.updateonfetch.md b/docs/js-client-sdk.iclientconfig.updateonfetch.md deleted file mode 100644 index dfb03a9..0000000 --- a/docs/js-client-sdk.iclientconfig.updateonfetch.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [@eppo/js-client-sdk](./js-client-sdk.md) > [IClientConfig](./js-client-sdk.iclientconfig.md) > [updateOnFetch](./js-client-sdk.iclientconfig.updateonfetch.md) - -## IClientConfig.updateOnFetch property - -Sets how the configuration is updated after a successful fetch - always: immediately start using the new configuration - expired: immediately start using the new configuration only if the current one has expired - empty: only use the new configuration if the current one is both expired and uninitialized/empty - -**Signature:** - -```typescript -updateOnFetch?: ServingStoreUpdateStrategy; -``` diff --git a/docs/js-client-sdk.iclientconfig.useexpiredcache.md b/docs/js-client-sdk.iclientconfig.useexpiredcache.md deleted file mode 100644 index 2183b4e..0000000 --- a/docs/js-client-sdk.iclientconfig.useexpiredcache.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [@eppo/js-client-sdk](./js-client-sdk.md) > [IClientConfig](./js-client-sdk.iclientconfig.md) > [useExpiredCache](./js-client-sdk.iclientconfig.useexpiredcache.md) - -## IClientConfig.useExpiredCache property - -Whether initialization will be considered successfully complete if expired cache values are loaded. If false, initialization will always wait for a fetch if cached values are expired. (default: false) - -**Signature:** - -```typescript -useExpiredCache?: boolean; -``` diff --git a/docs/js-client-sdk.init.md b/docs/js-client-sdk.init.md index 7a0de16..37b9a01 100644 --- a/docs/js-client-sdk.init.md +++ b/docs/js-client-sdk.init.md @@ -9,7 +9,7 @@ Initializes the Eppo client with configuration parameters. This method should be **Signature:** ```typescript -export declare function init(config: IClientConfig): Promise; +export declare function init(config: IClientConfig & ICompatibilityOptions): Promise; ``` ## Parameters @@ -37,7 +37,7 @@ config -[IClientConfig](./js-client-sdk.iclientconfig.md) +[IClientConfig](./js-client-sdk.iclientconfig.md) & ICompatibilityOptions diff --git a/docs/js-client-sdk.md b/docs/js-client-sdk.md index 58dc4e6..16c7b1c 100644 --- a/docs/js-client-sdk.md +++ b/docs/js-client-sdk.md @@ -71,17 +71,6 @@ Description -[buildStorageKeySuffix(apiKey)](./js-client-sdk.buildstoragekeysuffix.md) - - - - -Builds a storage key suffix from an API key. - - - - - [getConfigUrl(apiKey, baseUrl)](./js-client-sdk.getconfigurl.md) @@ -182,47 +171,70 @@ Description -[IClientConfig](./js-client-sdk.iclientconfig.md) +[IClientConfigSync](./js-client-sdk.iclientconfigsync.md) -Configuration for regular client initialization +Configuration interface for synchronous client initialization. -[IClientConfigSync](./js-client-sdk.iclientconfigsync.md) +[IPrecomputedClientConfig](./js-client-sdk.iprecomputedclientconfig.md) -Configuration interface for synchronous client initialization. +Configuration for Eppo precomputed client initialization -[IPrecomputedClientConfig](./js-client-sdk.iprecomputedclientconfig.md) +[IPrecomputedClientConfigSync](./js-client-sdk.iprecomputedclientconfigsync.md) -Configuration for Eppo precomputed client initialization +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. - + -[IPrecomputedClientConfigSync](./js-client-sdk.iprecomputedclientconfigsync.md) +## Type Aliases + + + diff --git a/js-client-sdk.api.md b/js-client-sdk.api.md index 5945c80..7c1b9bb 100644 --- a/js-client-sdk.api.md +++ b/js-client-sdk.api.md @@ -22,6 +22,7 @@ import { IAssignmentLogger } from '@eppo/js-client-sdk-common'; import { IAsyncStore } from '@eppo/js-client-sdk-common'; import { IBanditEvent } from '@eppo/js-client-sdk-common'; import { IBanditLogger } from '@eppo/js-client-sdk-common'; +import { IConfigurationStore } from '@eppo/js-client-sdk-common'; import { IContainerExperiment } from '@eppo/js-client-sdk-common'; import { ObfuscatedFlag } from '@eppo/js-client-sdk-common'; @@ -33,9 +34,6 @@ export { BanditActions } export { BanditSubjectAttributes } -// @public -export function buildStorageKeySuffix(apiKey: string): string; - // Warning: (ae-forgotten-export) The symbol "IStringStorageEngine" needs to be exported by the entry point index.d.ts // // @public @@ -56,6 +54,8 @@ export { ContextAttributes } // @public export class EppoJSClient extends EppoClient { + // (undocumented) + static buildAndInit(config: IClientConfig): EppoJSClient; // (undocumented) getBanditAction(flagKey: string, subjectKey: string, subjectAttributes: BanditSubjectAttributes, actions: BanditActions, defaultValue: string): Omit, 'evaluationDetails'>; // (undocumented) @@ -86,11 +86,13 @@ export class EppoJSClient extends EppoClient { getStringAssignmentDetails(flagKey: string, subjectKey: string, subjectAttributes: Record, defaultValue: string): IAssignmentDetails; // @internal (undocumented) init(config: Omit): Promise; - // (undocumented) + // @deprecated (undocumented) static initialized: boolean; + // @deprecated (undocumented) static instance: EppoJSClient; // @internal (undocumented) offlineInit(config: IClientConfigSync): void; + waitForConfiguration(): Promise; } // @public @@ -136,28 +138,15 @@ export { IBanditEvent } export { IBanditLogger } -// Warning: (ae-forgotten-export) The symbol "IBaseRequestConfig" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "IApiOptions" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "ILoggers" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "IEventOptions" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "IStorageOptions" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "IPollingOptions" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "OverridesConfig" needs to be exported by the entry point index.d.ts // // @public -export interface IClientConfig extends IBaseRequestConfig { - enableOverrides?: boolean; - eventIngestionConfig?: { - deliveryIntervalMs?: number; - retryIntervalMs?: number; - maxRetryDelayMs?: number; - maxRetries?: number; - batchSize?: number; - maxQueueSize?: number; - }; - forceReinitialize?: boolean; - maxCacheAgeSeconds?: number; - overridesStorageKey?: string; - persistentStore?: IAsyncStore; - throwOnFailedInitialization?: boolean; - // Warning: (ae-forgotten-export) The symbol "ServingStoreUpdateStrategy" needs to be exported by the entry point index.d.ts - updateOnFetch?: ServingStoreUpdateStrategy; - useExpiredCache?: boolean; -} +export type IClientConfig = IApiOptions & ILoggers & IEventOptions & IStorageOptions & IPollingOptions & OverridesConfig; // @public export interface IClientConfigSync { @@ -177,9 +166,13 @@ export interface IClientConfigSync { throwOnFailedInitialization?: boolean; } +// Warning: (ae-forgotten-export) The symbol "ICompatibilityOptions" needs to be exported by the entry point index.d.ts +// // @public -export function init(config: IClientConfig): Promise; +export function init(config: IClientConfig & ICompatibilityOptions): Promise; +// Warning: (ae-forgotten-export) The symbol "IBaseRequestConfig" needs to be exported by the entry point index.d.ts +// // @public export interface IPrecomputedClientConfig extends IBaseRequestConfig { enableOverrides?: boolean; diff --git a/package.json b/package.json index 5566c20..51ec463 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@eppo/js-client-sdk", - "version": "3.11.0", + "version": "3.12.0", "description": "Eppo SDK for client-side JavaScript applications", "main": "dist/index.js", "files": [ diff --git a/src/i-client-config.ts b/src/i-client-config.ts index 327b397..5999778 100644 --- a/src/i-client-config.ts +++ b/src/i-client-config.ts @@ -6,6 +6,7 @@ import { IAssignmentLogger, IAsyncStore, IBanditLogger, + IConfigurationStore, } from '@eppo/js-client-sdk-common'; import { ServingStoreUpdateStrategy } from './isolatable-hybrid.store'; @@ -115,10 +116,40 @@ export interface IPrecomputedClientConfig extends IBaseRequestConfig { } /** - * Configuration for regular client initialization - * @public + * Base options for the EppoClient SDK */ -export interface IClientConfig extends IBaseRequestConfig { +export type IApiOptions = { + /** + * Your key for accessing Eppo through the Eppo SDK. + * + * Some persistent storage mechanisms use this key (hashed) to index saved Eppo configuration data. + * It is not advisable to create multiple EppoClient instances with the same API key as they will each make network + * call(s) (depending on the other request options in `IApiOptions`) while sharing the same persistent storage. + */ + apiKey: string; + + /** + * Override the endpoint the SDK uses to load configuration. + */ + baseUrl?: string; + + /** + * Timeout in milliseconds for the HTTPS request for the experiment configuration. (Default: 5000) + */ + requestTimeoutMs?: number; + + /** + * Number of additional times the initial configuration request will be attempted if it fails. + * This is the request typically synchronously waited (via await) for completion. A small wait will be + * done between requests. (Default: 1) + */ + numInitialRequestRetries?: number; + + /** + * Skip the request for new configurations during initialization. (default: false) + */ + skipInitialRequest?: boolean; + /** * Throw an error if unable to fetch an initial configuration during initialization. (default: true) */ @@ -144,19 +175,12 @@ export interface IClientConfig extends IBaseRequestConfig { * - empty: only use the new configuration if the current one is both expired and uninitialized/empty */ updateOnFetch?: ServingStoreUpdateStrategy; +}; - /** - * A custom class to use for storing flag configurations. - * This is useful for cases where you want to use a different storage mechanism - * than the default storage provided by the SDK. - */ - persistentStore?: IAsyncStore; - - /** - * Force reinitialize the SDK if it is already initialized. - */ - forceReinitialize?: boolean; - +/** + * Wrapper for configuration settings for the event dispatcher + */ +export type IEventOptions = { /** Configuration settings for the event dispatcher */ eventIngestionConfig?: { /** Number of milliseconds to wait between each batch delivery. Defaults to 10 seconds. */ @@ -176,7 +200,79 @@ export interface IClientConfig extends IBaseRequestConfig { */ maxQueueSize?: number; }; +}; + +/** + * Custom storage instances. + */ +export type IStorageOptions = { + /** + * Custom implementation of the flag configuration store for advanced use-cases. + */ + flagConfigurationStore?: IConfigurationStore; + + /** + * A custom class to use for storing flag configurations. + * This is useful for cases where you want to use a different storage mechanism + * than the default storage provided by the SDK. + */ + persistentStore?: IAsyncStore; +}; + +/** + * Configure periodic loading of the Eppo configration from the API server. + */ +export type IPollingOptions = { + /** + * Poll for new configurations even if the initial configuration request failed. (default: false) + */ + pollAfterFailedInitialization?: boolean; + + /** + * Poll for new configurations (every `pollingIntervalMs`) after successfully requesting the initial configuration. (default: false) + */ + pollAfterSuccessfulInitialization?: boolean; + /** + * Amount of time to wait between API calls to refresh configuration data. Default of 30_000 (30 seconds). + */ + pollingIntervalMs?: number; + + /** + * Number of additional times polling for updated configurations will be attempted before giving up. + * Polling is done after a successful initial request. Subsequent attempts are done using an exponential + * backoff. (Default: 7) + */ + numPollRequestRetries?: number; +}; + +/** + * Loggers used by the Eppo Client when assignment are made (and bandit actions are selected). + */ +export type ILoggers = { + /** + * Pass a logging implementation to send variation assignments to your data warehouse. + */ + assignmentLogger: IAssignmentLogger; + + /** + * Pass a logging implementation to send bandit assignments to your data warehouse. + */ + banditLogger?: IBanditLogger; +}; + +/** + * Options for backwards compatibility. + */ +export type ICompatibilityOptions = { + /** + * Force reinitialize the SDK if it is already initialized. + * @deprecated use `buildAndInit` to create a fresh client. + */ + forceReinitialize?: boolean; +}; + +export type OverridesConfig = { /** * Enable the Overrides Store for local flag overrides. * (default: false) @@ -187,4 +283,30 @@ export interface IClientConfig extends IBaseRequestConfig { * The key to use for the overrides store. */ overridesStorageKey?: string; -} +}; + +/** + * Configuration for regular client initialization + * Create your initialization options object as one large object: + * + * const options { + * apiKey = 'MY SDK KEY', + * assignmentLogger, + * maxCacheAgeSeconds = 30, + * } + * + * OR, build separate objects for your config and destructure them at call to `init`. + * + * const apiOptions: IApiOptions = { apiKey = 'MY SDK KEY'}; + * const loggerOptions: ILoggerOptions = {assignmentLogger, banditLogger}; + * const eventOptions: IEventOptions = { ... }; + * + * const eppoClient = init({...apiOptions, ...loggerOptions, ...eventOptions}); + * @public + */ +export type IClientConfig = IApiOptions & + ILoggers & + IEventOptions & + IStorageOptions & + IPollingOptions & + OverridesConfig; diff --git a/src/index.spec.ts b/src/index.spec.ts index 63e0bcb..f8aa266 100644 --- a/src/index.spec.ts +++ b/src/index.spec.ts @@ -29,10 +29,11 @@ import { validateTestAssignments, } from '../test/testHelpers'; -import { IClientConfig } from './i-client-config'; +import { IApiOptions, IClientConfig, ICompatibilityOptions } from './i-client-config'; import { ServingStoreUpdateStrategy } from './isolatable-hybrid.store'; import { + EppoJSClient, EppoPrecomputedJSClient, getConfigUrl, getInstance, @@ -138,6 +139,10 @@ const mockObfuscatedUfcFlagConfig: Flag = { key: base64Encode('variant-2'), value: base64Encode('variant-2'), }, + [base64Encode('variant-3')]: { + key: base64Encode('variant-3'), + value: base64Encode('variant-3'), + }, }, allocations: [ { @@ -382,6 +387,166 @@ describe('EppoJSClient E2E test', () => { }); }); +describe('decoupled initialization', () => { + let mockLogger: IAssignmentLogger; + // eslint-disable-next-line @typescript-eslint/ban-types + let init: (config: IClientConfig) => Promise; + // eslint-disable-next-line @typescript-eslint/ban-types + let getInstance: () => EppoJSClient; + + beforeEach(async () => { + jest.isolateModules(() => { + // Isolate and re-require so that the static instance is reset to its default state + // eslint-disable-next-line @typescript-eslint/no-var-requires + const reloadedModule = require('./index'); + init = reloadedModule.init; + getInstance = reloadedModule.getInstance; + }); + }); + + describe('isolated from the singleton', () => { + beforeEach(() => { + mockLogger = td.object(); + + global.fetch = jest.fn(() => { + const ufc = { flags: { [obfuscatedFlagKey]: mockObfuscatedUfcFlagConfig } }; + + return Promise.resolve({ + ok: true, + status: 200, + json: () => Promise.resolve(ufc), + }); + }) as jest.Mock; + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('should be independent of the singleton', async () => { + const apiOptions: IApiOptions = { apiKey: '' }; + const options: IClientConfig = { ...apiOptions, assignmentLogger: mockLogger }; + const isolatedClient = EppoJSClient.buildAndInit(options); + + expect(isolatedClient).not.toEqual(getInstance()); + await isolatedClient.waitForConfiguration(); + + expect(isolatedClient.isInitialized()).toBe(true); + expect(getInstance().isInitialized()).toBe(false); + + expect(getInstance().getStringAssignment(flagKey, 'subject-10', {}, 'default-value')).toEqual( + 'default-value', + ); + expect( + isolatedClient.getStringAssignment(flagKey, 'subject-10', {}, 'default-value'), + ).toEqual('variant-1'); + }); + it('initializes on instantiation and notifies when ready', async () => { + const apiOptions: IApiOptions = { apiKey: '', baseUrl }; + const options: IClientConfig = { ...apiOptions, assignmentLogger: mockLogger }; + const client = EppoJSClient.buildAndInit(options); + + expect(client.getStringAssignment(flagKey, 'subject-10', {}, 'default-value')).toEqual( + 'default-value', + ); + + await client.waitForConfiguration(); + + const assignment = client.getStringAssignment(flagKey, 'subject-10', {}, 'default-value'); + expect(assignment).toEqual('variant-1'); + }); + }); + + describe('multiple client instances', () => { + const API_KEY_1 = 'my-api-key-1'; + const API_KEY_2 = 'my-api-key-2'; + const API_KEY_3 = 'my-api-key-3'; + + const commonOptions: Omit = { + baseUrl, + assignmentLogger: mockLogger, + }; + + let callCount = 0; + + beforeAll(() => { + global.fetch = jest.fn((url: string) => { + callCount++; + + const urlParams = new URLSearchParams(url.split('?')[1]); + + // Get the value of the apiKey parameter and serve a specific variant. + const apiKey = urlParams.get('apiKey'); + + // differentiate between the SDK keys by changing the variant that `flagKey` assigns. + let variant = 'variant-1'; + if (apiKey === API_KEY_2) { + variant = 'variant-2'; + } else if (apiKey === API_KEY_3) { + variant = 'variant-3'; + } + + const encodedVariant = base64Encode(variant); + + // deep copy the mock data since we're going to inject a change below. + const flagConfig: Flag = JSON.parse(JSON.stringify(mockObfuscatedUfcFlagConfig)); + // Inject the encoded variant as a single split for the flag's only allocation. + flagConfig.allocations[0].splits = [ + { + variationKey: encodedVariant, + shards: [], + }, + ]; + + const ufc = { flags: { [obfuscatedFlagKey]: flagConfig } }; + + return Promise.resolve({ + ok: true, + status: 200, + json: () => Promise.resolve(ufc), + }); + }) as jest.Mock; + }); + + afterAll(() => { + jest.restoreAllMocks(); + }); + + it('should evaluate separate UFCs for each SDK key', async () => { + const singleton = await init({ ...commonOptions, apiKey: API_KEY_1 }); + expect(singleton.getStringAssignment(flagKey, 'subject-10', {}, 'default-value')).toEqual( + 'variant-1', + ); + expect(callCount).toBe(1); + + const myClient2 = EppoJSClient.buildAndInit({ ...commonOptions, apiKey: API_KEY_2 }); + await myClient2.waitForConfiguration(); + expect(callCount).toBe(2); + + expect(singleton.getStringAssignment(flagKey, 'subject-10', {}, 'default-value')).toEqual( + 'variant-1', + ); + expect(myClient2.getStringAssignment(flagKey, 'subject-10', {}, 'default-value')).toEqual( + 'variant-2', + ); + + const myClient3 = EppoJSClient.buildAndInit({ ...commonOptions, apiKey: API_KEY_3 }); + await myClient3.waitForConfiguration(); + + expect(singleton.getStringAssignment(flagKey, 'subject-10', {}, 'default-value')).toEqual( + 'variant-1', + ); + expect(myClient2.getStringAssignment(flagKey, 'subject-10', {}, 'default-value')).toEqual( + 'variant-2', + ); + + expect(myClient3.getStringAssignment(flagKey, 'subject-10', {}, 'default-value')).toEqual( + 'variant-3', + ); + }); + }); +}); + describe('sync init', () => { it('initializes with flags in obfuscated mode', () => { const client = offlineInit({ @@ -422,9 +587,9 @@ describe('initialization options', () => { } as unknown as Record<'flags', Record>; // eslint-disable-next-line @typescript-eslint/ban-types - let init: (config: IClientConfig) => Promise; + let init: (config: IClientConfig & ICompatibilityOptions) => Promise; // eslint-disable-next-line @typescript-eslint/ban-types - let getInstance: () => EppoClient; + let getInstance: () => EppoJSClient; beforeEach(async () => { jest.isolateModules(() => { @@ -1165,7 +1330,7 @@ describe('initialization options', () => { async entries() { return entriesPromise.promise; }, - async setEntries(entries) { + async setEntries() { // pass }, }; diff --git a/src/index.ts b/src/index.ts index cc5777e..b6f3b0b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -22,6 +22,8 @@ import { Subject, IBanditLogger, IObfuscatedPrecomputedConfigurationResponse, + EppoClientParameters, + buildStorageKeySuffix, } from '@eppo/js-client-sdk-common'; import { assignmentCacheFactory } from './cache/assignment-cache-factory'; @@ -44,7 +46,7 @@ import { } from './configuration-factory'; import BrowserNetworkStatusListener from './events/browser-network-status-listener'; import LocalStorageBackedNamedEventQueue from './events/local-storage-backed-named-event-queue'; -import { IClientConfig, IPrecomputedClientConfig } from './i-client-config'; +import { IClientConfig, ICompatibilityOptions, IPrecomputedClientConfig } from './i-client-config'; import { sdkName, sdkVersion } from './sdk-data'; /** @@ -106,18 +108,64 @@ export class EppoJSClient extends EppoClient { // Ensure that the client is instantiated during class loading. // Use an empty memory-only configuration store until the `init` method is called, // to avoid serving stale data to the user. + /** - * @deprecated. use `getInstance()` instead. + * @deprecated use `getInstance()` instead. */ public static instance = new EppoJSClient({ flagConfigurationStore, isObfuscated: true, }); + /** + * @deprecated use `instance.isInitialized()` instead. + */ public static initialized = false; private initialized = false; + public static buildAndInit(config: IClientConfig): EppoJSClient { + const flagConfigurationStore = + config.flagConfigurationStore ?? + configurationStorageFactory({ + forceMemoryOnly: true, + }); + const client = new EppoJSClient({ flagConfigurationStore }); + + // init will resolve the promise that client.waitForConfiguration returns. + client.init(config); + return client; + } + + /** + * Resolved when the client is initialized + */ + private readonly initPromise: Promise; + + /** + * Resolves the `initPromise` when initialization is complete + * + * Initialization happens outside the constructor, so we can't assign `initPromise` to the result + * of initialization. Instead, we call the resolver when `init` is complete. + */ + private initPromiseResolver: () => void = () => null; + + private constructor(options: EppoClientParameters) { + super(options); + + // Create a promise that will be resolved when initialization is complete. + this.initPromise = new Promise((resolve) => { + this.initPromiseResolver = resolve; + }); + } + + /** + * Resolves when the EppoClient has completed its initialization. + */ + public waitForConfiguration(): Promise { + return this.initPromise; + } + public getStringAssignment( flagKey: string, subjectKey: string, @@ -478,8 +526,8 @@ export class EppoJSClient extends EppoClient { initializationError = initFromFetchError ? initFromFetchError : initFromConfigStoreError - ? initFromConfigStoreError - : new Error('Eppo SDK: No configuration source produced a valid configuration'); + ? initFromConfigStoreError + : new Error('Eppo SDK: No configuration source produced a valid configuration'); } applicationLogger.debug('Initialization source', initializationSource); } catch (error: unknown) { @@ -499,6 +547,7 @@ export class EppoJSClient extends EppoClient { } this.initialized = true; + this.initPromiseResolver(); return this; } @@ -575,17 +624,6 @@ export class EppoJSClient extends EppoClient { } } -/** - * Builds a storage key suffix from an API key. - * @param apiKey - The API key to build the suffix from - * @returns A string suffix for storage keys - * @public - */ -export function buildStorageKeySuffix(apiKey: string): string { - // Note that we use the first 8 characters of the API key to create per-API key persistent storages and caches - return apiKey.replace(/\W/g, '').substring(0, 8); -} - /** * Initializes the Eppo client with configuration parameters. * @@ -619,7 +657,7 @@ let initializationPromise: Promise | null = null; * @param config - client configuration * @public */ -export async function init(config: IClientConfig): Promise { +export async function init(config: IClientConfig & ICompatibilityOptions): Promise { validation.validateNotBlank(config.apiKey, 'API key required'); const instance = getInstance();
+ +Type Alias + + + + +Description + + +
+ +[IClientConfig](./js-client-sdk.iclientconfig.md) -Configuration parameters for initializing the Eppo precomputed client. +Configuration for regular client initialization Create your initialization options object as one large object: -This interface is used for cases where precomputed assignments are available from an external process that can bootstrap the SDK client. +const options { apiKey = 'MY SDK KEY', assignmentLogger, maxCacheAgeSeconds = 30, } + +OR, build separate objects for your config and destructure them at call to `init`. + +const apiOptions: IApiOptions = { apiKey = 'MY SDK KEY'}; const loggerOptions: ILoggerOptions = {assignmentLogger, banditLogger}; const eventOptions: IEventOptions = { ... }; + +const eppoClient = init({...apiOptions, ...loggerOptions, ...eventOptions});