Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
6241b0f
Extract SDK lifecycle methods (init, flush, destroy) into separate mo…
EmilianoSanchez Mar 9, 2026
e4289de
Merge branch 'refactor-impressions-tracker' into configs-sdk-client
EmilianoSanchez Mar 10, 2026
008756a
Merge branch 'refactor-impressions-tracker' into configs-sdk-client
EmilianoSanchez Mar 11, 2026
767324d
Add IConfig DTO and Configs SDK client wrapper
EmilianoSanchez Mar 12, 2026
6a6febd
Merge branch 'refactor-evaluator-to-support-no-target' into configs-s…
EmilianoSanchez Mar 13, 2026
c396555
Consolidate impression logging into single message when queueing
EmilianoSanchez Mar 13, 2026
74ac2e6
Merge branch 'refactor-evaluator-to-support-no-target' into configs-s…
EmilianoSanchez Mar 13, 2026
ee756a0
Merge branch 'refactor-evaluator-to-support-no-target' into configs-s…
EmilianoSanchez Mar 13, 2026
021086f
Merge branch 'refactor-evaluator-to-support-no-target' into configs-s…
EmilianoSanchez Mar 13, 2026
c3d9730
Rename SPLIT_NOT_FOUND to DEFINITION_NOT_FOUND and update related ref…
EmilianoSanchez Mar 13, 2026
30d102f
Remove feature flag name from SDK not ready warning message and simpl…
EmilianoSanchez Mar 13, 2026
858bb96
Polishing
EmilianoSanchez Mar 13, 2026
7004f30
Merge branch 'refactor-evaluator-to-support-no-target' into configs-s…
EmilianoSanchez Mar 18, 2026
f75580e
Merge branch 'sdk-configs-handle-configs-dto' into configs-sdk-client
EmilianoSanchez Mar 18, 2026
38a9f3a
Merge branch 'refactor-fallback-calculator' into configs-sdk-client
EmilianoSanchez Mar 18, 2026
51724d3
Merge branch 'sdk-configs-rename-split-to-definition' into configs-sd…
EmilianoSanchez Mar 19, 2026
db9e597
Merge branch 'sdk-configs-rename-split-to-definition' into configs-sd…
EmilianoSanchez Mar 19, 2026
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
2 changes: 1 addition & 1 deletion src/evaluator/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export interface IEvaluation {
treatment?: string,
label: string,
changeNumber?: number,
config?: string | null
config?: string | object | null
}

export type IEvaluationResult = IEvaluation & { treatment: string; impressionsDisabled?: boolean }
Expand Down
8 changes: 4 additions & 4 deletions src/sdkClient/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { validateDefinitionExistence } from '../utils/inputValidation/definition
import { validateTrafficTypeExistence } from '../utils/inputValidation/trafficTypeExistence';
import { SDK_NOT_READY } from '../utils/labels';
import { CONTROL, TREATMENT, TREATMENTS, TREATMENT_WITH_CONFIG, TREATMENTS_WITH_CONFIG, TRACK, TREATMENTS_WITH_CONFIG_BY_FLAGSETS, TREATMENTS_BY_FLAGSETS, TREATMENTS_BY_FLAGSET, TREATMENTS_WITH_CONFIG_BY_FLAGSET, GET_TREATMENTS_WITH_CONFIG, GET_TREATMENTS_BY_FLAG_SETS, GET_TREATMENTS_WITH_CONFIG_BY_FLAG_SETS, GET_TREATMENTS_BY_FLAG_SET, GET_TREATMENTS_WITH_CONFIG_BY_FLAG_SET, GET_TREATMENT_WITH_CONFIG, GET_TREATMENT, GET_TREATMENTS, TRACK_FN_LABEL } from '../utils/constants';
import { IEvaluationResult } from '../evaluator/types';
import { IEvaluation, IEvaluationResult } from '../evaluator/types';
import SplitIO from '../../types/splitio';
import { IMPRESSION_QUEUEING } from '../logger/constants';
import { ISdkFactoryContext } from '../sdkFactory/types';
Expand Down Expand Up @@ -72,7 +72,7 @@ export function clientFactory(params: ISdkFactoryContext): SplitIO.IClient | Spl
const treatments: SplitIO.Treatments | SplitIO.TreatmentsWithConfig = {};
const properties = stringify(options);
Object.keys(evaluationResults).forEach(featureFlagName => {
treatments[featureFlagName] = processEvaluation(evaluationResults[featureFlagName], featureFlagName, key, properties, withConfig, methodName, queue);
treatments[featureFlagName] = processEvaluation(evaluationResults[featureFlagName], featureFlagName, key, properties, withConfig, methodName, queue) as SplitIO.Treatment | SplitIO.TreatmentWithConfig;
});
impressionsTracker.track(queue, attributes);

Expand Down Expand Up @@ -101,7 +101,7 @@ export function clientFactory(params: ISdkFactoryContext): SplitIO.IClient | Spl
const treatments: SplitIO.Treatments | SplitIO.TreatmentsWithConfig = {};
const properties = stringify(options);
Object.keys(evaluationResults).forEach(featureFlagName => {
treatments[featureFlagName] = processEvaluation(evaluationResults[featureFlagName], featureFlagName, key, properties, withConfig, methodName, queue);
treatments[featureFlagName] = processEvaluation(evaluationResults[featureFlagName], featureFlagName, key, properties, withConfig, methodName, queue) as SplitIO.Treatment | SplitIO.TreatmentWithConfig;
});
impressionsTracker.track(queue, attributes);

Expand Down Expand Up @@ -139,7 +139,7 @@ export function clientFactory(params: ISdkFactoryContext): SplitIO.IClient | Spl
withConfig: boolean,
invokingMethodName: string,
queue: ImpressionDecorated[]
): SplitIO.Treatment | SplitIO.TreatmentWithConfig {
): SplitIO.Treatment | Pick<IEvaluation, 'treatment' | 'config'> {
const { changeNumber, impressionsDisabled } = evaluation;
let { treatment, label, config = null } = evaluation;

Expand Down
70 changes: 3 additions & 67 deletions src/sdkClient/sdkClient.ts
Original file line number Diff line number Diff line change
@@ -1,40 +1,16 @@
import { objectAssign } from '../utils/lang/objectAssign';
import SplitIO from '../../types/splitio';
import { releaseApiKey, validateAndTrackApiKey } from '../utils/inputValidation/apiKey';
import { clientFactory } from './client';
import { clientInputValidationDecorator } from './clientInputValidation';
import { ISdkFactoryContext } from '../sdkFactory/types';

const COOLDOWN_TIME_IN_MILLIS = 1000;
import { sdkLifecycleFactory } from './sdkLifecycle';

/**
* Creates an Sdk client, i.e., a base client with status, init, flush and destroy interface
*/
export function sdkClientFactory(params: ISdkFactoryContext, isSharedClient?: boolean): SplitIO.IClient | SplitIO.IAsyncClient {
const { sdkReadinessManager, syncManager, storage, signalListener, settings, telemetryTracker, impressionsTracker } = params;

let hasInit = false;
let lastActionTime = 0;

function __cooldown(func: Function, time: number) {
const now = Date.now();
//get the actual time elapsed in ms
const timeElapsed = now - lastActionTime;
//check if the time elapsed is less than desired cooldown
if (timeElapsed < time) {
//if yes, return message with remaining time in seconds
settings.log.warn(`Flush cooldown, remaining time ${(time - timeElapsed) / 1000} seconds`);
return Promise.resolve();
} else {
//Do the requested action and re-assign the lastActionTime
lastActionTime = now;
return func();
}
}
const { sdkReadinessManager, settings } = params;

function __flush() {
return syncManager ? syncManager.flush() : Promise.resolve();
}

return objectAssign(
// Proto-linkage of the readiness Event Emitter
Expand All @@ -48,46 +24,6 @@ export function sdkClientFactory(params: ISdkFactoryContext, isSharedClient?: bo
params.fallbackTreatmentsCalculator
),

{
init() {
if (hasInit) return;
hasInit = true;

if (!isSharedClient) {
validateAndTrackApiKey(settings.log, settings.core.authorizationKey);
sdkReadinessManager.readinessManager.init();
impressionsTracker.start();
syncManager && syncManager.start();
signalListener && signalListener.start();
}
},

flush() {
// @TODO define cooldown time
return __cooldown(__flush, COOLDOWN_TIME_IN_MILLIS);
},

destroy() {
hasInit = false;
// Mark the SDK as destroyed immediately
sdkReadinessManager.readinessManager.destroy();

// For main client, cleanup the SDK Key, listeners and scheduled jobs, and record stat before flushing data
if (!isSharedClient) {
releaseApiKey(settings.core.authorizationKey);
telemetryTracker.sessionLength();
signalListener && signalListener.stop();
impressionsTracker.stop();
}

// Stop background jobs
syncManager && syncManager.stop();

return __flush().then(() => {
// Cleanup storage
return storage.destroy();
});
}
}
sdkLifecycleFactory(params, isSharedClient)
);
}
77 changes: 77 additions & 0 deletions src/sdkClient/sdkLifecycle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import SplitIO from '../../types/splitio';
import { releaseApiKey, validateAndTrackApiKey } from '../utils/inputValidation/apiKey';
import { ISdkFactoryContext } from '../sdkFactory/types';

const COOLDOWN_TIME_IN_MILLIS = 1000;

/**
* Creates an Sdk client, i.e., a base client with status, init, flush and destroy interface
*/
export function sdkLifecycleFactory(params: ISdkFactoryContext, isSharedClient?: boolean): Pick<SplitIO.ConfigsClient, 'init' | 'flush' | 'destroy'> {
const { sdkReadinessManager, syncManager, storage, signalListener, settings, telemetryTracker, impressionsTracker } = params;

let hasInit = false;
let lastActionTime = 0;

function __cooldown(func: Function, time: number) {
const now = Date.now();
//get the actual time elapsed in ms
const timeElapsed = now - lastActionTime;
//check if the time elapsed is less than desired cooldown
if (timeElapsed < time) {
//if yes, return message with remaining time in seconds
settings.log.warn(`Flush cooldown, remaining time ${(time - timeElapsed) / 1000} seconds`);
return Promise.resolve();
} else {
//Do the requested action and re-assign the lastActionTime
lastActionTime = now;
return func();
}
}

function __flush() {
return syncManager ? syncManager.flush() : Promise.resolve();
}

return {
init() {
if (hasInit) return;
hasInit = true;

if (!isSharedClient) {
validateAndTrackApiKey(settings.log, settings.core.authorizationKey);
sdkReadinessManager.readinessManager.init();
impressionsTracker.start();
syncManager && syncManager.start();
signalListener && signalListener.start();
}
},

flush() {
// @TODO define cooldown time
return __cooldown(__flush, COOLDOWN_TIME_IN_MILLIS);
},

destroy() {
hasInit = false;
// Mark the SDK as destroyed immediately
sdkReadinessManager.readinessManager.destroy();

// For main client, cleanup the SDK Key, listeners and scheduled jobs, and record stat before flushing data
if (!isSharedClient) {
releaseApiKey(settings.core.authorizationKey);
telemetryTracker.sessionLength();
signalListener && signalListener.stop();
impressionsTracker.stop();
}

// Stop background jobs
syncManager && syncManager.stop();

return __flush().then(() => {
// Cleanup storage
return storage.destroy();
});
}
};
}
68 changes: 68 additions & 0 deletions src/sdkConfig/configObject.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import SplitIO from '../../types/splitio';
import { isString } from '../utils/lang';

function createConfigObject(value: any): SplitIO.Config {
return {
value,
getString(propertyName: string, propertyDefaultValue?: string): string {
const val = value != null ? value[propertyName] : undefined;
if (typeof val === 'string') return val;
return propertyDefaultValue !== undefined ? propertyDefaultValue : '';
},
getNumber(propertyName: string, propertyDefaultValue?: number): number {
const val = value != null ? value[propertyName] : undefined;
if (typeof val === 'number') return val;
return propertyDefaultValue !== undefined ? propertyDefaultValue : 0;
},
getBoolean(propertyName: string, propertyDefaultValue?: boolean): boolean {
const val = value != null ? value[propertyName] : undefined;
if (typeof val === 'boolean') return val;
return propertyDefaultValue !== undefined ? propertyDefaultValue : false;
},
getArray(propertyName: string): SplitIO.ConfigArray {
const val = value != null ? value[propertyName] : undefined;
return createConfigArrayObject(Array.isArray(val) ? val : []);
},
getObject(propertyName: string): SplitIO.Config {
const val = value != null ? value[propertyName] : undefined;
return createConfigObject(val != null && typeof val === 'object' && !Array.isArray(val) ? val : null);
}
};
}

function createConfigArrayObject(arr: any[]): SplitIO.ConfigArray {
return {
value: arr,
getString(index: number, propertyDefaultValue?: string): string {
const val = arr[index];
if (typeof val === 'string') return val;
return propertyDefaultValue !== undefined ? propertyDefaultValue : '';
},
getNumber(index: number, propertyDefaultValue?: number): number {
const val = arr[index];
if (typeof val === 'number') return val;
return propertyDefaultValue !== undefined ? propertyDefaultValue : 0;
},
getBoolean(index: number, propertyDefaultValue?: boolean): boolean {
const val = arr[index];
if (typeof val === 'boolean') return val;
return propertyDefaultValue !== undefined ? propertyDefaultValue : false;
},
getArray(index: number): SplitIO.ConfigArray {
const val = arr[index];
return createConfigArrayObject(Array.isArray(val) ? val : []);
},
getObject(index: number): SplitIO.Config {
const val = arr[index];
return createConfigObject(val != null && typeof val === 'object' && !Array.isArray(val) ? val : null);
}
};
}

export function parseConfig(config?: string | object | null): SplitIO.Config {
try {
return createConfigObject(isString(config) ? JSON.parse(config) : config);
} catch {
return createConfigObject(null);
}
}
68 changes: 68 additions & 0 deletions src/sdkConfig/index-ff-wrapper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { ISdkFactoryParams } from '../sdkFactory/types';
import { sdkFactory } from '../sdkFactory/index';
import SplitIO from '../../types/splitio';
import { objectAssign } from '../utils/lang/objectAssign';
import { parseConfig } from './configObject';
import { validateTarget } from '../utils/inputValidation/target';
import { GET_CONFIG } from '../utils/constants';
import { ISettings } from '../types';

/**
* Configs SDK Client factory implemented as a wrapper over the FF SDK.
* Exposes getConfig and track at the root level instead of requiring a client() call.
* getConfig delegates to getTreatmentWithConfig and wraps the parsed JSON config in a Config object.
*/
export function configsClientFactory(params: ISdkFactoryParams): SplitIO.ConfigsClient {
const ffSdk = sdkFactory({ ...params, lazyInit: true }) as (SplitIO.ISDK | SplitIO.IAsyncSDK) & { init(): void };
const ffClient = ffSdk.client() as SplitIO.IClient & { init(): void; flush(): Promise<void> };
const ffManager = ffSdk.manager();
const log = (ffSdk.settings as ISettings).log;

return objectAssign(
// Inherit status interface (EventEmitter, Event, getStatus, ready, whenReady, whenReadyFromCache) from ffClient
Object.create(ffClient) as SplitIO.IStatusInterface,
{
settings: ffSdk.settings,
Logger: ffSdk.Logger,

init() {
ffSdk.init();
},

flush(): Promise<void> {
return ffClient.flush();
},

destroy(): Promise<void> {
return ffSdk.destroy();
},

getConfig(name: string, target?: SplitIO.Target): SplitIO.Config {
if (target) {
// Serve config with target
if (validateTarget(log, target, GET_CONFIG)) {
const result = ffClient.getTreatmentWithConfig(target.key, name, target.attributes) as SplitIO.TreatmentWithConfig;
return parseConfig(result.config);
} else {
log.error('Invalid target for getConfig.');
}
}

// Serve config without target
const config = ffManager.split(name) as SplitIO.SplitView;
if (!config) {
log.error('Provided config name does not exist. Serving empty config object.');
return parseConfig({});
}

log.info('Serving default config variant, ' + config.defaultTreatment + ' for config ' + name);
const defaultConfigVariant = config.configs[config.defaultTreatment];
return parseConfig(defaultConfigVariant);
},

track(key: SplitIO.SplitKey, trafficType: string, eventType: string, value?: number, properties?: SplitIO.Properties): boolean {
return ffClient.track(key, trafficType, eventType, value, properties) as boolean;
}
}
);
}
Loading