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
3 changes: 2 additions & 1 deletion CHANGES.txt
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
3.1.0 (July XX, 2026)
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.
Expand Down
28 changes: 14 additions & 14 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 numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
{
"name": "@splitsoftware/splitio-commons",
"version": "3.0.1-rc.2",
"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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
2 changes: 1 addition & 1 deletion src/sync/polling/pollingManagerCS.ts
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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
6 changes: 2 additions & 4 deletions src/sync/streaming/UpdateWorkers/DefinitionsUpdateWorker.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,7 +7,7 @@ import { IRBSegmentsCacheSync, IDefinitionsCacheSync, IStorageSync } from '../..
import { ITelemetryTracker } from '../../../trackers/types';
import { Backoff } from '../../../utils/Backoff';
import { SPLITS } from '../../../utils/constants';
import { ISegmentsSyncTask, IDefinitionsSyncTask } from '../../polling/types';
import { IDefinitionsSyncTask } from '../../polling/types';
import { InstantUpdate } from '../../polling/updaters/definitionChangesUpdater';
import { RB_SEGMENT_UPDATE } from '../constants';
import { parseFFUpdatePayload } from '../parseUtils';
Expand All@@ -18,7 +18,7 @@ import { IUpdateWorker } from './types';
/**
* DefinitionsUpdateWorker factory
*/
export function DefinitionsUpdateWorker(log: ILogger, storage: IStorageSync, definitionsSyncTask: IDefinitionsSyncTask, definitionsEventEmitter: IDefinitionsEventEmitter, telemetryTracker: ITelemetryTracker, segmentsSyncTask?: ISegmentsSyncTask): IUpdateWorker<[updateData: ISplitUpdateData]> & { killDefinition(event: ISplitKillData): void } {
export function DefinitionsUpdateWorker(log: ILogger, storage: IStorageSync, definitionsSyncTask: IDefinitionsSyncTask, definitionsEventEmitter: IDefinitionsEventEmitter, telemetryTracker: ITelemetryTracker): IUpdateWorker<[updateData: ISplitUpdateData]> & { killDefinition(event: ISplitKillData): void } {

const ff = DefinitionsUpdateWorker(storage.definitions);
const rbs = DefinitionsUpdateWorker(storage.rbSegments);
Expand All@@ -42,8 +42,6 @@ export function DefinitionsUpdateWorker(log: ILogger, storage: IStorageSync, def
__handleDefinitionUpdateCall();
} else {
if (instantUpdate) telemetryTracker.trackUpdatesFromSSE(SPLITS);
// fetch new registered segments for server-side API. Not retrying on error
if (segmentsSyncTask) segmentsSyncTask.execute(true);

const attempts = backoff.attempts + 1;

Expand Down
2 changes: 1 addition & 1 deletion src/sync/streaming/pushManager.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -56,7 +56,7 @@ export function pushManagerFactory(
// MySegmentsUpdateWorker (client-side) are initiated in `add` method
const segmentsUpdateWorker = userKey ? undefined : SegmentsUpdateWorker(log, pollingManager.segmentsSyncTask as ISegmentsSyncTask, storage.segments);
// For server-side we pass the segmentsSyncTask, used by DefinitionsUpdateWorker to fetch new segments
const definitionsUpdateWorker = DefinitionsUpdateWorker(log, storage, pollingManager.definitionsSyncTask, readiness.definitions, telemetryTracker, userKey ? undefined : pollingManager.segmentsSyncTask as ISegmentsSyncTask);
const definitionsUpdateWorker = DefinitionsUpdateWorker(log, storage, pollingManager.definitionsSyncTask, readiness.definitions, telemetryTracker);

// [Only for client-side] map of hashes to user keys, to dispatch membership update events to the corresponding MySegmentsUpdateWorker
const userKeyHashes: Record<string, string> = {};
Expand Down