Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions CHANGES.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
3.1.0 (July 23, 2026)
- Added support for rule-based segments in /api/v1/configs endpoint.
- Updated polling flow to fetch new referenced segments immediately.

3.0.0 (June 26, 2026)
- Extracted SDK lifecycle methods (`init`, `flush`, and `destroy`) into a reusable `sdkLifecycle` module.
- Extracted the `track` method into a standalone `trackMethod` factory for reuse across SDKs.
Expand Down
35 changes: 18 additions & 17 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@splitsoftware/splitio-commons",
"version": "3.0.0",
"version": "3.1.0",
"description": "Split JavaScript SDK common components",
"main": "cjs/index.js",
"module": "esm/index.js",
Expand Down
2 changes: 1 addition & 1 deletion src/dtos/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ export interface IDefinitionCondition {
matchers: IDefinitionMatcher[]
}
partitions?: IDefinitionPartition[]
label?: string
label?: string | null
conditionType?: 'ROLLOUT' | 'WHITELIST'
}

Expand Down
6 changes: 3 additions & 3 deletions src/evaluator/condition/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@ import SplitIO from '../../../types/splitio';
import { ILogger } from '../../logger/types';

// Build Evaluation object if and only if matchingResult is true
function match(log: ILogger, matchingResult: boolean, bucketingKey: string | undefined, seed?: number, treatments?: { getTreatmentFor: (x: number) => string }, label?: string): IEvaluation | boolean | undefined {
function match(log: ILogger, matchingResult: boolean, bucketingKey: string | undefined, seed?: number, treatments?: { getTreatmentFor: (x: number) => string }, label?: string | null): IEvaluation | boolean | undefined {
if (matchingResult) {
return treatments ? // Feature flag
{
treatment: getTreatment(log, bucketingKey as string, seed, treatments),
label: label!
label: label ?? ''
} : // Rule-based segment
true;
}
Expand All @@ -22,7 +22,7 @@ function match(log: ILogger, matchingResult: boolean, bucketingKey: string | und
}

// Condition factory
export function conditionContext(log: ILogger, matcherEvaluator: (key: SplitIO.SplitKeyObject, attributes?: SplitIO.Attributes, splitEvaluator?: IDefinitionEvaluator) => MaybeThenable<boolean>, treatments?: { getTreatmentFor: (x: number) => string }, label?: string, conditionType?: 'ROLLOUT' | 'WHITELIST'): IEvaluator {
export function conditionContext(log: ILogger, matcherEvaluator: (key: SplitIO.SplitKeyObject, attributes?: SplitIO.Attributes, splitEvaluator?: IDefinitionEvaluator) => MaybeThenable<boolean>, treatments?: { getTreatmentFor: (x: number) => string }, label?: string | null, conditionType?: 'ROLLOUT' | 'WHITELIST'): IEvaluator {

return function conditionEvaluator(key: SplitIO.SplitKeyObject, seed?: number, trafficAllocation?: number, trafficAllocationSeed?: number, attributes?: SplitIO.Attributes, splitEvaluator?: IDefinitionEvaluator) {

Expand Down
3 changes: 2 additions & 1 deletion src/sdkClient/sdkLifecycle.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { releaseApiKey, validateAndTrackApiKey } from '../utils/inputValidation/apiKey';
import { ISdkFactoryContext } from '../sdkFactory/types';
import { LOCALHOST_MODE } from '../utils/constants';

const COOLDOWN_TIME_IN_MILLIS = 1000;

Expand All @@ -12,7 +13,7 @@ export function sdkLifecycleFactory(params: ISdkFactoryContext, isSharedClient?:
let hasInit = false;
let lastActionTime = 0;

const signalListener = platform.SignalListener && new platform.SignalListener(params);
const signalListener = platform.SignalListener && settings.mode !== LOCALHOST_MODE ? new platform.SignalListener(params) : undefined;

function __cooldown(func: Function, time: number) {
const now = Date.now();
Expand Down
6 changes: 1 addition & 5 deletions src/sdkFactory/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,11 +105,7 @@ export interface ISdkFactoryParams {

// Sdk client method factory.
// It Allows to distinguish SDK clients with the client-side API (`IBrowserSDK` and `IBrowserAsyncSDK`) or server-side API (`ISDK` and `IAsyncSDK`).
sdkClientMethodFactory: (params: ISdkFactoryContext) => (
{ (): SplitIO.IBrowserClient & { init(): void }; (key: SplitIO.SplitKey): SplitIO.IBrowserClient & { init(): void }; } |
(() => SplitIO.IClient & { init(): void }) |
(() => SplitIO.IAsyncClient & { init(): void })
)
sdkClientMethodFactory: (params: ISdkFactoryContext) => ({ (): SplitIO.IBrowserClient; (key: SplitIO.SplitKey): SplitIO.IBrowserClient; } | (() => SplitIO.IClient) | (() => SplitIO.IAsyncClient))

// Impression observer factory.
impressionsObserverFactory: () => IImpressionObserver
Expand Down
8 changes: 4 additions & 4 deletions src/services/__tests__/splitApi.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,10 @@ describe('serviceApi', () => {
assertHeaders(settings, headers);
expect(url).toBe(expectedFlagsUrl(-1, 100, settings.validateFilters || false, settings, -1));

serviceApi.fetchConfigs(-1, false, 100);
serviceApi.fetchConfigs(-1, false, 100, -1);
[url, { headers }] = fetchMock.mock.calls[4];
assertHeaders(settings, headers);
expect(url).toBe(expectedConfigsUrl(-1, 100, settings.validateFilters || false, settings));
expect(url).toBe(expectedConfigsUrl(-1, 100, settings.validateFilters || false, settings, -1));

serviceApi.fetchConfigsSegmentChanges(-1, 'segmentName', false, 100);
[url, { headers }] = fetchMock.mock.calls[5];
Expand Down Expand Up @@ -81,9 +81,9 @@ describe('serviceApi', () => {
return `sdk/splitChanges?s=1.1&since=${since}${rbSince ? '&rbSince=' + rbSince : ''}${usesFilter ? filterQueryString : ''}${till ? '&till=' + till : ''}`;
}

function expectedConfigsUrl(since: number, till: number, usesFilter: boolean, settings: ISettings) {
function expectedConfigsUrl(since: number, till: number, usesFilter: boolean, settings: ISettings, rbSince?: number) {
const filterQueryString = settings.sync.__splitFiltersValidation && settings.sync.__splitFiltersValidation.queryString;
return `configs/v1/configs?since=${since}${usesFilter ? filterQueryString : ''}${till ? '&till=' + till : ''}`;
return `configs/v1/configs?since=${since}${rbSince ? '&rbSince=' + rbSince : ''}${usesFilter ? filterQueryString : ''}${till ? '&till=' + till : ''}`;
}
});

Expand Down
4 changes: 2 additions & 2 deletions src/services/serviceApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,8 @@ export function serviceApiFactory(
},

// @TODO support filterQueryString and handle ERROR_TOO_MANY_SETS error
fetchConfigs(since: number, noCache?: boolean, till?: number) {
const url = `${urls.configs}/api/v1/configs?since=${since}${filterQueryString || ''}${till ? '&till=' + till : ''}`;
fetchConfigs(since: number, noCache?: boolean, till?: number, rbSince?: number) {
const url = `${urls.configs}/api/v1/configs?since=${since}${rbSince ? '&rbSince=' + rbSince : ''}${filterQueryString || ''}${till ? '&till=' + till : ''}`;
return secureSplitHttpClient(url, noCache ? noCacheHeaderOptions : undefined);
},

Expand Down
4 changes: 2 additions & 2 deletions src/services/splitApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,8 +149,8 @@ export function splitApiFactory(

// Not used. Just here to satisfy the IServiceApi interface.

fetchConfigs(since: number, noCache?: boolean, till?: number) {
const url = `${urls.configs}/v1/configs?since=${since}${filterQueryString || ''}${till ? '&till=' + till : ''}`;
fetchConfigs(since: number, noCache?: boolean, till?: number, rbSince?: number) {
const url = `${urls.configs}/v1/configs?since=${since}${rbSince ? '&rbSince=' + rbSince : ''}${filterQueryString || ''}${till ? '&till=' + till : ''}`;
return splitHttpClient(url, noCache ? noCacheHeaderOptions : undefined);
},

Expand Down
4 changes: 1 addition & 3 deletions src/storages/inLocalStorage/MySegmentsCacheInLocal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,10 @@ import { StorageAdapter } from '../types';
export class MySegmentsCacheInLocal extends AbstractMySegmentsCacheSync {

private readonly keys: MySegmentsKeyBuilder;
private readonly log: ILogger;
private readonly storage: StorageAdapter;

constructor(log: ILogger, keys: MySegmentsKeyBuilder, storage: StorageAdapter) {
constructor(_log: ILogger, keys: MySegmentsKeyBuilder, storage: StorageAdapter) {
super();
this.log = log;
this.keys = keys;
this.storage = storage;
}
Expand Down
2 changes: 1 addition & 1 deletion src/sync/polling/pollingManagerCS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export function pollingManagerCSFactory(
const { serviceApi, storage, readiness, settings } = params;
const log = settings.log;

const definitionsSyncTask = definitionsSyncTaskFactory(definitionChangesFetcher, storage, readiness, settings, true);
const definitionsSyncTask = definitionsSyncTaskFactory(definitionChangesFetcher, storage, readiness, settings);

// Map of matching keys to their corresponding MySegmentsSyncTask.
const mySegmentsSyncTasks: Record<string, IMySegmentsSyncTask> = {};
Expand Down
2 changes: 1 addition & 1 deletion src/sync/polling/pollingManagerSS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ export function pollingManagerSSFactory(
const { storage, readiness, settings } = params;
const log = settings.log;

const definitionsSyncTask: IDefinitionsSyncTask = definitionsSyncTaskFactory(definitionChangesFetcher, storage, readiness, settings);
const segmentsSyncTask: ISegmentsSyncTask = segmentsSyncTaskFactory(segmentChangesFetcher, storage, readiness, settings);
const definitionsSyncTask: IDefinitionsSyncTask = definitionsSyncTaskFactory(definitionChangesFetcher, storage, readiness, settings, segmentsSyncTask);

return {
definitionsSyncTask,
Expand Down
6 changes: 3 additions & 3 deletions src/sync/polling/syncTasks/definitionsSyncTask.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { IStorageSync } from '../../../storages/types';
import { IReadinessManager } from '../../../readiness/types';
import { syncTaskFactory } from '../../syncTask';
import { IDefinitionsSyncTask } from '../types';
import { IDefinitionsSyncTask, ISegmentsSyncTask } from '../types';
import { ISettings } from '../../../types';
import { definitionChangesUpdaterFactory } from '../updaters/definitionChangesUpdater';
import { IDefinitionChangesFetcher } from '../fetchers/types';
Expand All @@ -14,7 +14,7 @@ export function definitionsSyncTaskFactory(
storage: IStorageSync,
readiness: IReadinessManager,
settings: ISettings,
isClientSide?: boolean
segmentsSyncTask?: ISegmentsSyncTask // undefined for client-side
): IDefinitionsSyncTask {
return syncTaskFactory(
settings.log,
Expand All @@ -26,7 +26,7 @@ export function definitionsSyncTaskFactory(
readiness.definitions,
settings.startup.requestTimeoutBeforeReady,
settings.startup.retriesOnFailureBeforeReady,
isClientSide
segmentsSyncTask
),
settings.scheduler.featuresRefreshRate,
'definitionChangesUpdater',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ describe('definitionChangesUpdater', () => {
{ sets: ['set_a'], shouldEmit: true }, /* should emit if flag is back in configured sets */
];

definitionChangesUpdater = definitionChangesUpdaterFactory(loggerMock, splitChangesFetcher, storage, splitFiltersValidation, readinessManager.definitions, 1000, 1, true);
definitionChangesUpdater = definitionChangesUpdaterFactory(loggerMock, splitChangesFetcher, storage, splitFiltersValidation, readinessManager.definitions, 1000, 1);

let index = 0;
let calls = 0;
Expand All @@ -305,7 +305,7 @@ describe('definitionChangesUpdater', () => {
// @ts-ignore
splitFiltersValidation = { queryString: null, groupedFilters: { bySet: ['set_a'], byName: [], byPrefix: [] }, validFilters: [] };
storage.definitions.clear();
definitionChangesUpdater = definitionChangesUpdaterFactory(loggerMock, splitChangesFetcher, storage, splitFiltersValidation, readinessManager.definitions, 1000, 1, true);
definitionChangesUpdater = definitionChangesUpdaterFactory(loggerMock, splitChangesFetcher, storage, splitFiltersValidation, readinessManager.definitions, 1000, 1);
splitsEmitSpy.mockReset();
index = 0;
for (const setMock of setMocks) {
Expand Down Expand Up @@ -424,8 +424,8 @@ describe('definitionChangesUpdater', () => {
readinessManager.definitions.definitionsArrived = false;
readinessManager.segments.segmentsArrived = false; // Segments not ready - client-side should still emit

// Create client-side updater (isClientSide = true)
const clientSideUpdater = definitionChangesUpdaterFactory(loggerMock, splitChangesFetcher, storage, splitFiltersValidation, readinessManager.definitions, 1000, 1, true);
// Create client-side updater (segmentsSyncTask = undefined)
const clientSideUpdater = definitionChangesUpdaterFactory(loggerMock, splitChangesFetcher, storage, splitFiltersValidation, readinessManager.definitions, 1000, 1);

const flag1 = { name: 'client-flag', status: 'ACTIVE', changeNumber: 300, conditions: [] } as unknown as IDefinition;
fetchMock.once('*', { status: 200, body: { ff: { d: [flag1], t: 300 } } });
Expand Down
20 changes: 5 additions & 15 deletions src/sync/polling/updaters/definitionChangesUpdater.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ISegmentsCacheBase, IStorageBase } from '../../../storages/types';
import { IStorageBase } from '../../../storages/types';
import { IDefinitionChangesFetcher } from '../fetchers/types';
import { IRBSegment, IDefinition, IDefinitionChangesResponse, ISplitFiltersValidation, MaybeThenable } from '../../../dtos/types';
import { IDefinitionsEventEmitter } from '../../../readiness/types';
Expand All @@ -12,21 +12,11 @@ import { setToArray } from '../../../utils/lang/sets';
import { SPLIT_UPDATE } from '../../streaming/constants';
import { SdkUpdateMetadata } from '../../../../types/splitio';
import { ISplit } from '../fetchers/splitChangesFetcher';
import { ISegmentsSyncTask } from '../types';

export type InstantUpdate = { payload: ISplit | IRBSegment, changeNumber: number, type: string };
type DefinitionChangesUpdater = (noCache?: boolean, till?: number, instantUpdate?: InstantUpdate) => Promise<boolean>

// Checks that all registered segments have been fetched (changeNumber !== -1 for every segment).
// Returns a promise that could be rejected.
// @TODO review together with Segments and MySegments storage APIs
function checkAllSegmentsExist(segments: ISegmentsCacheBase): Promise<boolean> {
let registeredSegments = Promise.resolve(segments.getRegisteredSegments());
return registeredSegments.then(segmentNames => {
return Promise.all(segmentNames.map(segmentName => segments.getChangeNumber(segmentName)))
.then(changeNumbers => changeNumbers.every(changeNumber => changeNumber !== undefined));
});
}

/**
* Collect segments from a raw FF or RBS definition.
* Exported for testing purposes.
Expand Down Expand Up @@ -133,7 +123,7 @@ export function definitionChangesUpdaterFactory(
definitionsEventEmitter?: IDefinitionsEventEmitter,
requestTimeoutBeforeReady = 0,
retriesOnFailureBeforeReady = 0,
isClientSide?: boolean
segmentsSyncTask?: ISegmentsSyncTask // undefined for client-side
): DefinitionChangesUpdater {
const { definitions, rbSegments, segments } = storage;

Expand Down Expand Up @@ -202,8 +192,8 @@ export function definitionChangesUpdaterFactory(
startingUp = false;

if (definitionsEventEmitter) {
// To emit SDK_DEFINITIONS_ARRIVED for server-side SDK, we must check that all registered segments have been fetched
return Promise.resolve(!definitionsEventEmitter.definitionsArrived || ((ffChanged || rbsChanged) && (isClientSide || checkAllSegmentsExist(segments))))
// To emit SDK_DEFINITIONS_ARRIVED for server-side SDK, we must wait for all registered segments to be fetched
return Promise.resolve(!definitionsEventEmitter.definitionsArrived || ((ffChanged || rbsChanged) && (!segmentsSyncTask || segmentsSyncTask.execute(true))))
.catch(() => false /** noop. just to handle a possible `checkAllSegmentsExist` rejection, before emitting SDK event */)
.then(emitSplitsArrivedEvent => {
// emit SDK events
Expand Down
4 changes: 2 additions & 2 deletions src/sync/polling/updaters/segmentChangesUpdater.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ export function segmentChangesUpdaterFactory(
log.debug(`${LOG_PREFIX_SYNC}Started segments update`);

// If not a segment name provided, read list of available segments names to be updated.
let segmentsPromise = Promise.resolve(segmentName ? [segmentName] : segments.getRegisteredSegments());
const segmentsPromise = Promise.resolve(segmentName ? [segmentName] : segments.getRegisteredSegments());

return segmentsPromise.then(segmentNames => {
const updaters = segmentNames.map(segmentName => updateSegment(segmentName, noCache, till, fetchOnlyNew, readyOnAlreadyExistentState ? retriesOnFailureBeforeReady : 0));
Expand All @@ -85,7 +85,7 @@ export function segmentChangesUpdaterFactory(
// if at least one segment fetch succeeded, mark segments ready
if (shouldUpdateFlags.some(update => update) || readyOnAlreadyExistentState) {
readyOnAlreadyExistentState = false;
if (readiness) {
if (readiness && !fetchOnlyNew) {
const metadata: SdkUpdateMetadata = {
type: SEGMENTS_UPDATE,
names: []
Expand Down
Loading
Loading