Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: buildAndInit for isolated client instances #166

Open
wants to merge 48 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
e20ef73
detatched construction
typotter Jan 30, 2025
50e66aa
combine new and old constructors
typotter Jan 30, 2025
de9f653
use spark for md5 instead of non-exported member from common
typotter Jan 31, 2025
fa5baab
update to latest common, use exported members
typotter Jan 31, 2025
953c4e4
doc comments
typotter Jan 31, 2025
159df11
wip
typotter Jan 31, 2025
a73bf0c
skip initial config for now
typotter Jan 31, 2025
7e5abe1
exports
typotter Jan 31, 2025
92cdd1d
waitForReady
typotter Jan 31, 2025
c4b2717
clean up offline init method
typotter Jan 31, 2025
ddab6c0
no need to export
typotter Jan 31, 2025
d55d23d
DOCS
typotter Jan 31, 2025
3c18bc9
builder method
typotter Feb 4, 2025
4b8077f
polish
typotter Feb 5, 2025
621cccb
chore: Move initialization code from static to the instance
typotter Feb 11, 2025
ed3c4ce
docs
typotter Feb 11, 2025
5f96888
lint
typotter Feb 12, 2025
228e1e4
move reinit check
typotter Feb 13, 2025
4d35d4f
restore buffered init
typotter Feb 13, 2025
cf3c2ba
docs
typotter Feb 13, 2025
132c0a5
docs
typotter Feb 13, 2025
6141581
polish
typotter Feb 5, 2025
82ec342
test name
typotter Feb 5, 2025
81ff3b1
move comment
typotter Feb 11, 2025
fad43d3
move forceReinit
typotter Feb 13, 2025
febb3f8
docs
typotter Feb 13, 2025
212e26e
move members
typotter Feb 13, 2025
a943e90
refactor wip
typotter Feb 14, 2025
ba219f2
chore: Move initialization code from static to the instance
typotter Feb 11, 2025
5333574
docs
typotter Feb 11, 2025
aab1009
lint
typotter Feb 12, 2025
f93fa61
docs
typotter Feb 13, 2025
030aa33
docs
typotter Feb 14, 2025
e61ec6c
merge fixes
typotter Feb 14, 2025
a000205
fix the horrible diff and merge artifacts
typotter Feb 14, 2025
6f1a68e
docs
typotter Feb 14, 2025
654ffaa
cleamup merge
typotter Feb 14, 2025
74498b9
merge main
typotter Feb 18, 2025
9176d21
docs
typotter Feb 18, 2025
a179e32
merge main
typotter Feb 21, 2025
0734752
chore: lint
typotter Feb 21, 2025
fb76db9
undeprecate
typotter Feb 21, 2025
a384132
chore: additional comments
typotter Feb 21, 2025
3ae0ec7
chore: update docs
typotter Feb 21, 2025
98bd24a
chore: deprecate static EppoJSClient.initialized
typotter Feb 21, 2025
2d45fe5
v3.12.0
typotter Feb 21, 2025
5d3cabc
Merge branch 'main' into tp/namespace
typotter Feb 24, 2025
2712f6b
chore: nit
typotter Feb 24, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,8 @@
"webpack-cli": "^6.0.1"
},
"dependencies": {
"@eppo/js-client-sdk-common": "4.8.4"
"@eppo/js-client-sdk-common": "4.8.4",
"spark-md5": "^3.0.2"
},
"packageManager": "[email protected]+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
}
134 changes: 134 additions & 0 deletions src/client-options-converter.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import {
IConfigurationStore,
ObfuscatedFlag,
Flag,
EventDispatcher,
} from '@eppo/js-client-sdk-common';
import * as td from 'testdouble';

import { clientOptionsToParameters } from './client-options-converter';
import { IClientOptions } from './i-client-config';
import { sdkName, sdkVersion } from './sdk-data';

describe('clientOptionsToParameters', () => {
const mockStore = td.object<IConfigurationStore<Flag | ObfuscatedFlag>>();

it('converts basic client options', () => {
const options: IClientOptions = {
sdkKey: 'test-key',
baseUrl: 'https://test.eppo.cloud',
assignmentLogger: { logAssignment: jest.fn() },
};

const result = clientOptionsToParameters(options, mockStore);

expect(result.isObfuscated).toBe(true);
expect(result.flagConfigurationStore).toBeDefined();
expect(result.configurationRequestParameters).toEqual({
apiKey: 'test-key',
baseUrl: 'https://test.eppo.cloud',
sdkName,
sdkVersion,
numInitialRequestRetries: undefined,
numPollRequestRetries: undefined,
pollingIntervalMs: undefined,
requestTimeoutMs: undefined,
pollAfterFailedInitialization: undefined,
pollAfterSuccessfulInitialization: undefined,
throwOnFailedInitialization: undefined,
skipInitialPoll: undefined,
});
});

it('uses provided flag configuration store', () => {
const options: IClientOptions = {
sdkKey: 'test-key',
assignmentLogger: { logAssignment: jest.fn() },
};

const result = clientOptionsToParameters(options, mockStore);

expect(result.flagConfigurationStore).toBe(mockStore);
});

it('converts client options with event ingestion config', () => {
const options: IClientOptions = {
sdkKey: 'test-key',
assignmentLogger: { logAssignment: jest.fn() },
};
const mockDispatcher: EventDispatcher = td.object<EventDispatcher>();

const result = clientOptionsToParameters(options, mockStore, mockDispatcher);

expect(result.eventDispatcher).toBeDefined();
});

it('converts client options with polling configuration', () => {
const options: IClientOptions = {
sdkKey: 'test-key',
assignmentLogger: { logAssignment: jest.fn() },
pollingIntervalMs: 30000,
pollAfterSuccessfulInitialization: true,
pollAfterFailedInitialization: true,
skipInitialRequest: true,
};

const result = clientOptionsToParameters(options, mockStore);

expect(result.configurationRequestParameters).toMatchObject({
pollingIntervalMs: 30000,
pollAfterSuccessfulInitialization: true,
pollAfterFailedInitialization: true,
skipInitialPoll: true,
});
});

it('converts client options with retry configuration', () => {
const options: IClientOptions = {
sdkKey: 'test-key',
assignmentLogger: { logAssignment: jest.fn() },
requestTimeoutMs: 5000,
numInitialRequestRetries: 3,
numPollRequestRetries: 2,
};

const result = clientOptionsToParameters(options, mockStore);

expect(result.configurationRequestParameters).toMatchObject({
requestTimeoutMs: 5000,
numInitialRequestRetries: 3,
numPollRequestRetries: 2,
});
});

it('handles undefined optional parameters', () => {
const options: IClientOptions = {
sdkKey: 'test-key',
assignmentLogger: { logAssignment: jest.fn() },
};

const result = clientOptionsToParameters(options, mockStore);

expect(result.configurationRequestParameters).toMatchObject({
baseUrl: undefined,
pollingIntervalMs: undefined,
requestTimeoutMs: undefined,
numInitialRequestRetries: undefined,
numPollRequestRetries: undefined,
});
});

it('includes sdk metadata', () => {
const options: IClientOptions = {
sdkKey: 'test-key',
assignmentLogger: { logAssignment: jest.fn() },
};

const result = clientOptionsToParameters(options, mockStore);

expect(result.configurationRequestParameters).toMatchObject({
sdkName,
sdkVersion,
});
});
});
58 changes: 58 additions & 0 deletions src/client-options-converter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import {
BanditParameters,
BanditVariation,
EventDispatcher,
Flag,
FlagConfigurationRequestParameters,
IConfigurationStore,
ObfuscatedFlag,
} from '@eppo/js-client-sdk-common';

import { IClientOptions } from './i-client-config';
import { sdkName, sdkVersion } from './sdk-data';

export type EppoClientParameters = {
// Dispatcher for arbitrary, application-level events (not to be confused with Eppo specific assignment
// or bandit events). These events are application-specific and captures by EppoClient#track API.
eventDispatcher?: EventDispatcher;
flagConfigurationStore: IConfigurationStore<Flag | ObfuscatedFlag>;
banditVariationConfigurationStore?: IConfigurationStore<BanditVariation[]>;
banditModelConfigurationStore?: IConfigurationStore<BanditParameters>;
configurationRequestParameters?: FlagConfigurationRequestParameters;
isObfuscated?: boolean;
};

/**
* Converts IClientOptions to EppoClientParameters
* @internal
*/
export function clientOptionsToParameters(
options: IClientOptions,
flagConfigurationStore: IConfigurationStore<Flag>,
eventDispatcher?: EventDispatcher,
): EppoClientParameters {
const parameters: EppoClientParameters = {
flagConfigurationStore,
isObfuscated: true,
};

parameters.eventDispatcher = eventDispatcher;

// Always include configuration request parameters
parameters.configurationRequestParameters = {
apiKey: options.sdkKey,
sdkVersion, // dynamically picks up version.
sdkName, // Hardcoded to `js-client-sdk`
baseUrl: options.baseUrl,
requestTimeoutMs: options.requestTimeoutMs,
numInitialRequestRetries: options.numInitialRequestRetries,
numPollRequestRetries: options.numPollRequestRetries,
pollAfterSuccessfulInitialization: options.pollAfterSuccessfulInitialization,
pollAfterFailedInitialization: options.pollAfterFailedInitialization,
pollingIntervalMs: options.pollingIntervalMs,
throwOnFailedInitialization: options.throwOnFailedInitialization,
skipInitialPoll: options.skipInitialRequest,
};

return parameters;
}
144 changes: 119 additions & 25 deletions src/i-client-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
IAssignmentLogger,
IAsyncStore,
IBanditLogger,
IConfigurationStore,
} from '@eppo/js-client-sdk-common';

import { ServingStoreUpdateStrategy } from './isolatable-hybrid.store';
Expand Down Expand Up @@ -103,11 +104,55 @@ export interface IPrecomputedClientConfig extends IBaseRequestConfig {
precompute: IPrecompute;
}

/**
* Configuration for regular client initialization
* @public
*/
export interface IClientConfig extends IBaseRequestConfig {
export type IEventOptions = {
eventIngestionConfig?: {
/** Number of milliseconds to wait between each batch delivery. Defaults to 10 seconds. */
deliveryIntervalMs?: number;
/** Minimum amount of milliseconds to wait before retrying a failed delivery. Defaults to 5 seconds */
retryIntervalMs?: number;
/** Maximum amount of milliseconds to wait before retrying a failed delivery. Defaults to 30 seconds. */
maxRetryDelayMs?: number;
/** Maximum number of retry attempts before giving up on a batch delivery. Defaults to 3 retries. */
maxRetries?: number;
/** Maximum number of events to send per delivery request. Defaults to 1000 events. */
batchSize?: number;
/**
* Maximum number of events to queue in memory before starting to drop events.
* Note: This is only used if localStorage is not available.
* Defaults to 10000 events.
*/
maxQueueSize?: number;
};
};

export type IApiOptions = {
sdkKey: string;

initialConfiguration?: string;
baseUrl?: string;

/**
* Force reinitialize the SDK if it is already initialized.
*/
forceReinitialize?: boolean;

/**
* 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)
*/
Expand All @@ -133,36 +178,85 @@ export interface IClientConfig extends IBaseRequestConfig {
* - empty: only use the new configuration if the current one is both expired and uninitialized/empty
*/
updateOnFetch?: ServingStoreUpdateStrategy;
};

/**
* Handy options class for when you want to create an offline client.
*/
export class OfflineApiOptions implements IApiOptions {
constructor(
public readonly sdkKey: string,
public readonly initialConfiguration?: string,
) {}
public readonly offline = true;
}

export type IStorageOptions = {
flagConfigurationStore?: IConfigurationStore<Flag>;

/**
* 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<Flag>;
};

export type IPollingOptions = {
/**
* Force reinitialize the SDK if it is already initialized.
* Poll for new configurations even if the initial configuration request failed. (default: false)
*/
forceReinitialize?: boolean;
pollAfterFailedInitialization?: boolean;

/** Configuration settings for the event dispatcher */
eventIngestionConfig?: {
/** Number of milliseconds to wait between each batch delivery. Defaults to 10 seconds. */
deliveryIntervalMs?: number;
/** Minimum amount of milliseconds to wait before retrying a failed delivery. Defaults to 5 seconds */
retryIntervalMs?: number;
/** Maximum amount of milliseconds to wait before retrying a failed delivery. Defaults to 30 seconds. */
maxRetryDelayMs?: number;
/** Maximum number of retry attempts before giving up on a batch delivery. Defaults to 3 retries. */
maxRetries?: number;
/** Maximum number of events to send per delivery request. Defaults to 1000 events. */
batchSize?: number;
/**
* Maximum number of events to queue in memory before starting to drop events.
* Note: This is only used if localStorage is not available.
* Defaults to 10000 events.
*/
maxQueueSize?: number;
/**
* 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;
};

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;
};

/**
* Config shape for client v2.
*/
export type IClientOptions = IApiOptions &
ILoggers &
IEventOptions &
IStorageOptions &
IPollingOptions;

/**
* Configuration for regular client initialization
* @public
*/
export type IClientConfig = Omit<IClientOptions, 'sdkKey' | 'offline'> &
Pick<IBaseRequestConfig, 'apiKey'>;

export function convertClientOptionsToClientConfig(options: IClientOptions): IClientConfig {
return {
...options,
apiKey: options.sdkKey,
};
}
Loading
Loading