Skip to content

Commit 09f15de

Browse files
Merge branch 'inlocalstorage_storageAdapter' into inlocalstorage_sessionStorage
2 parents a7edc8d + fb3b97a commit 09f15de

File tree

21 files changed

+399
-326
lines changed

21 files changed

+399
-326
lines changed

CHANGES.txt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1-
2.5.0 (August XX, 2025)
1+
2.6.0 (September 19, 2025)
22
- Added `storage.wrapper` configuration option to allow the SDK to use a custom storage wrapper for the storage type `LOCALSTORAGE`. Default value is `window.localStorage`.
33

4+
2.5.0 (September 10, 2025)
5+
- Added `factory.getRolloutPlan()` method for standalone server-side SDKs, which returns the rollout plan snapshot from the storage.
6+
- Added `initialRolloutPlan` configuration option for standalone client-side SDKs, which allows preloading the SDK storage with a snapshot of the rollout plan.
7+
48
2.4.1 (June 3, 2025)
59
- Bugfix - Improved the Proxy fallback to flag spec version 1.2 to handle cases where the Proxy does not return an end-of-stream marker in 400 status code responses.
610

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@splitsoftware/splitio-commons",
3-
"version": "2.4.2-rc.3",
3+
"version": "2.5.0",
44
"description": "Split JavaScript SDK common components",
55
"main": "cjs/index.js",
66
"module": "esm/index.js",

src/evaluator/convertions/index.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,17 @@
1+
import { IBetweenMatcherData } from '../../dtos/types';
2+
13
export function zeroSinceHH(millisSinceEpoch: number): number {
24
return new Date(millisSinceEpoch).setUTCHours(0, 0, 0, 0);
35
}
46

57
export function zeroSinceSS(millisSinceEpoch: number): number {
68
return new Date(millisSinceEpoch).setUTCSeconds(0, 0);
79
}
10+
11+
export function betweenDateTimeTransform(betweenMatcherData: IBetweenMatcherData): IBetweenMatcherData {
12+
return {
13+
dataType: betweenMatcherData.dataType,
14+
start: zeroSinceSS(betweenMatcherData.start),
15+
end: zeroSinceSS(betweenMatcherData.end)
16+
};
17+
}

src/evaluator/matchersTransform/index.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { matcherTypes, matcherTypesMapper, matcherDataTypes } from '../matchers/
33
import { segmentTransform } from './segment';
44
import { whitelistTransform } from './whitelist';
55
import { numericTransform } from './unaryNumeric';
6-
import { zeroSinceHH, zeroSinceSS } from '../convertions';
6+
import { zeroSinceHH, zeroSinceSS, betweenDateTimeTransform } from '../convertions';
77
import { IBetweenMatcherData, IInLargeSegmentMatcherData, IInSegmentMatcherData, ISplitMatcher, IUnaryNumericMatcherData } from '../../dtos/types';
88
import { IMatcherDto } from '../types';
99

@@ -32,7 +32,7 @@ export function matchersTransform(matchers: ISplitMatcher[]): IMatcherDto[] {
3232
let type = matcherTypesMapper(matcherType);
3333
// As default input data type we use string (even for ALL_KEYS)
3434
let dataType = matcherDataTypes.STRING;
35-
let value = undefined;
35+
let value;
3636

3737
if (type === matcherTypes.IN_SEGMENT) {
3838
value = segmentTransform(userDefinedSegmentMatcherData as IInSegmentMatcherData);
@@ -60,8 +60,7 @@ export function matchersTransform(matchers: ISplitMatcher[]): IMatcherDto[] {
6060
dataType = matcherDataTypes.NUMBER;
6161

6262
if (value.dataType === 'DATETIME') {
63-
value.start = zeroSinceSS(value.start);
64-
value.end = zeroSinceSS(value.end);
63+
value = betweenDateTimeTransform(value);
6564
dataType = matcherDataTypes.DATETIME;
6665
}
6766
} else if (type === matcherTypes.BETWEEN_SEMVER) {

src/sdkClient/sdkClientMethodCS.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,15 @@ import { RETRIEVE_CLIENT_DEFAULT, NEW_SHARED_CLIENT, RETRIEVE_CLIENT_EXISTING, L
99
import { SDK_SEGMENTS_ARRIVED } from '../readiness/constants';
1010
import { ISdkFactoryContext } from '../sdkFactory/types';
1111
import { buildInstanceId } from './identity';
12+
import { setRolloutPlan } from '../storages/setRolloutPlan';
13+
import { ISegmentsCacheSync } from '../storages/types';
1214

1315
/**
1416
* Factory of client method for the client-side API variant where TT is ignored.
1517
* Therefore, clients don't have a bound TT for the track method.
1618
*/
1719
export function sdkClientMethodCSFactory(params: ISdkFactoryContext): (key?: SplitIO.SplitKey) => SplitIO.IBrowserClient {
18-
const { clients, storage, syncManager, sdkReadinessManager, settings: { core: { key }, log } } = params;
20+
const { clients, storage, syncManager, sdkReadinessManager, settings: { core: { key }, log, initialRolloutPlan } } = params;
1921

2022
const mainClientInstance = clientCSDecorator(
2123
log,
@@ -56,6 +58,10 @@ export function sdkClientMethodCSFactory(params: ISdkFactoryContext): (key?: Spl
5658
sharedSdkReadiness.readinessManager.segments.emit(SDK_SEGMENTS_ARRIVED);
5759
});
5860

61+
if (sharedStorage && initialRolloutPlan) {
62+
setRolloutPlan(log, initialRolloutPlan, { segments: sharedStorage.segments as ISegmentsCacheSync, largeSegments: sharedStorage.largeSegments as ISegmentsCacheSync }, matchingKey);
63+
}
64+
5965
// 3 possibilities:
6066
// - Standalone mode: both syncManager and sharedSyncManager are defined
6167
// - Consumer mode: both syncManager and sharedSyncManager are undefined

src/sdkFactory/index.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ import { strategyOptimizedFactory } from '../trackers/strategy/strategyOptimized
1414
import { strategyNoneFactory } from '../trackers/strategy/strategyNone';
1515
import { uniqueKeysTrackerFactory } from '../trackers/uniqueKeysTracker';
1616
import { DEBUG, OPTIMIZED } from '../utils/constants';
17+
import { setRolloutPlan } from '../storages/setRolloutPlan';
18+
import { IStorageSync } from '../storages/types';
19+
import { getMatching } from '../utils/key';
1720

1821
/**
1922
* Modular SDK factory
@@ -24,7 +27,7 @@ export function sdkFactory(params: ISdkFactoryParams): SplitIO.ISDK | SplitIO.IA
2427
syncManagerFactory, SignalListener, impressionsObserverFactory,
2528
integrationsManagerFactory, sdkManagerFactory, sdkClientMethodFactory,
2629
filterAdapterFactory, lazyInit } = params;
27-
const { log, sync: { impressionsMode } } = settings;
30+
const { log, sync: { impressionsMode }, initialRolloutPlan, core: { key } } = settings;
2831

2932
// @TODO handle non-recoverable errors, such as, global `fetch` not available, invalid SDK Key, etc.
3033
// On non-recoverable errors, we should mark the SDK as destroyed and not start synchronization.
@@ -43,7 +46,7 @@ export function sdkFactory(params: ISdkFactoryParams): SplitIO.ISDK | SplitIO.IA
4346

4447
const storage = storageFactory({
4548
settings,
46-
onReadyCb: (error) => {
49+
onReadyCb(error) {
4750
if (error) {
4851
// If storage fails to connect, SDK_READY_TIMED_OUT event is emitted immediately. Review when timeout and non-recoverable errors are reworked
4952
readiness.timeout();
@@ -52,11 +55,16 @@ export function sdkFactory(params: ISdkFactoryParams): SplitIO.ISDK | SplitIO.IA
5255
readiness.splits.emit(SDK_SPLITS_ARRIVED);
5356
readiness.segments.emit(SDK_SEGMENTS_ARRIVED);
5457
},
55-
onReadyFromCacheCb: () => {
58+
onReadyFromCacheCb() {
5659
readiness.splits.emit(SDK_SPLITS_CACHE_LOADED);
5760
}
5861
});
59-
// @TODO add support for dataloader: `if (params.dataLoader) params.dataLoader(storage);`
62+
63+
if (initialRolloutPlan) {
64+
setRolloutPlan(log, initialRolloutPlan, storage as IStorageSync, key && getMatching(key));
65+
if ((storage as IStorageSync).splits.getChangeNumber() > -1) readiness.splits.emit(SDK_SPLITS_CACHE_LOADED);
66+
}
67+
6068
const clients: Record<string, SplitIO.IBasicClient> = {};
6169
const telemetryTracker = telemetryTrackerFactory(storage.telemetry, platform.now);
6270
const integrationsManager = integrationsManagerFactory && integrationsManagerFactory({ settings, storage, telemetryTracker });
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import { InMemoryStorageFactory } from '../inMemory/InMemoryStorage';
2+
import { InMemoryStorageCSFactory } from '../inMemory/InMemoryStorageCS';
3+
import { fullSettings } from '../../utils/settingsValidation/__tests__/settings.mocks';
4+
import { loggerMock } from '../../logger/__tests__/sdkLogger.mock';
5+
import { IRBSegment, ISplit } from '../../dtos/types';
6+
7+
import { validateRolloutPlan, setRolloutPlan } from '../setRolloutPlan';
8+
import { getRolloutPlan } from '../getRolloutPlan';
9+
10+
const otherKey = 'otherKey';
11+
const expectedRolloutPlan = {
12+
splitChanges: {
13+
ff: { d: [{ name: 'split1' }], t: 123, s: -1 },
14+
rbs: { d: [{ name: 'rbs1' }], t: 321, s: -1 }
15+
},
16+
memberships: {
17+
[fullSettings.core.key as string]: { ms: { k: [{ n: 'segment1' }] }, ls: { k: [] } },
18+
[otherKey]: { ms: { k: [{ n: 'segment1' }] }, ls: { k: [] } }
19+
},
20+
segmentChanges: [{
21+
name: 'segment1',
22+
added: [fullSettings.core.key as string, otherKey],
23+
removed: [],
24+
since: -1,
25+
till: 123
26+
}]
27+
};
28+
29+
describe('validateRolloutPlan', () => {
30+
afterEach(() => {
31+
loggerMock.mockClear();
32+
});
33+
34+
test('valid rollout plan and mode', () => {
35+
expect(validateRolloutPlan(loggerMock, { mode: 'standalone', initialRolloutPlan: expectedRolloutPlan } as any)).toEqual(expectedRolloutPlan);
36+
expect(loggerMock.error).not.toHaveBeenCalled();
37+
});
38+
39+
test('invalid rollout plan', () => {
40+
expect(validateRolloutPlan(loggerMock, { mode: 'standalone', initialRolloutPlan: {} } as any)).toBeUndefined();
41+
expect(loggerMock.error).toHaveBeenCalledWith('storage: invalid rollout plan provided');
42+
});
43+
44+
test('invalid mode', () => {
45+
expect(validateRolloutPlan(loggerMock, { mode: 'consumer', initialRolloutPlan: expectedRolloutPlan } as any)).toBeUndefined();
46+
expect(loggerMock.warn).toHaveBeenCalledWith('storage: initial rollout plan is ignored in consumer mode');
47+
});
48+
});
49+
50+
describe('getRolloutPlan & setRolloutPlan (client-side)', () => {
51+
// @ts-expect-error Load server-side storage
52+
const serverStorage = InMemoryStorageFactory({ settings: fullSettings });
53+
serverStorage.splits.update([{ name: 'split1' } as ISplit], [], 123);
54+
serverStorage.rbSegments.update([{ name: 'rbs1' } as IRBSegment], [], 321);
55+
serverStorage.segments.update('segment1', [fullSettings.core.key as string, otherKey], [], 123);
56+
57+
afterEach(() => {
58+
jest.clearAllMocks();
59+
});
60+
61+
test('using preloaded data (no memberships, no segments)', () => {
62+
const rolloutPlan = getRolloutPlan(loggerMock, serverStorage);
63+
64+
// @ts-expect-error Load client-side storage with preloaded data
65+
const clientStorage = InMemoryStorageCSFactory({ settings: fullSettings });
66+
setRolloutPlan(loggerMock, rolloutPlan, clientStorage, fullSettings.core.key as string);
67+
68+
// Shared client storage
69+
const sharedClientStorage = clientStorage.shared!(otherKey);
70+
setRolloutPlan(loggerMock, rolloutPlan, { segments: sharedClientStorage.segments, largeSegments: sharedClientStorage.largeSegments }, otherKey);
71+
72+
expect(clientStorage.segments.getRegisteredSegments()).toEqual([]);
73+
expect(sharedClientStorage.segments.getRegisteredSegments()).toEqual([]);
74+
75+
// Get preloaded data from client-side storage
76+
expect(getRolloutPlan(loggerMock, clientStorage)).toEqual(rolloutPlan);
77+
expect(rolloutPlan).toEqual({ ...expectedRolloutPlan, memberships: undefined, segmentChanges: undefined });
78+
});
79+
80+
test('using preloaded data with memberships', () => {
81+
const rolloutPlan = getRolloutPlan(loggerMock, serverStorage, { keys: [fullSettings.core.key as string, otherKey] });
82+
83+
// @ts-expect-error Load client-side storage with preloaded data
84+
const clientStorage = InMemoryStorageCSFactory({ settings: fullSettings });
85+
setRolloutPlan(loggerMock, rolloutPlan, clientStorage, fullSettings.core.key as string);
86+
87+
// Shared client storage
88+
const sharedClientStorage = clientStorage.shared!(otherKey);
89+
setRolloutPlan(loggerMock, rolloutPlan, { segments: sharedClientStorage.segments, largeSegments: sharedClientStorage.largeSegments }, otherKey);
90+
91+
expect(clientStorage.segments.getRegisteredSegments()).toEqual(['segment1']);
92+
expect(sharedClientStorage.segments.getRegisteredSegments()).toEqual(['segment1']);
93+
94+
// @TODO requires internal storage cache for `shared` storages
95+
// // Get preloaded data from client-side storage
96+
// expect(getRolloutPlan(loggerMock, clientStorage, { keys: [fullSettings.core.key as string, otherKey] })).toEqual(rolloutPlan);
97+
// expect(rolloutPlan).toEqual({ ...expectedRolloutPlan, segmentChanges: undefined });
98+
});
99+
100+
test('using preloaded data with segments', () => {
101+
const rolloutPlan = getRolloutPlan(loggerMock, serverStorage, { exposeSegments: true });
102+
103+
// @ts-expect-error Load client-side storage with preloaded data
104+
const clientStorage = InMemoryStorageCSFactory({ settings: fullSettings });
105+
setRolloutPlan(loggerMock, rolloutPlan, clientStorage, fullSettings.core.key as string);
106+
107+
// Shared client storage
108+
const sharedClientStorage = clientStorage.shared!(otherKey);
109+
setRolloutPlan(loggerMock, rolloutPlan, { segments: sharedClientStorage.segments, largeSegments: sharedClientStorage.largeSegments }, otherKey);
110+
111+
expect(clientStorage.segments.getRegisteredSegments()).toEqual(['segment1']);
112+
expect(sharedClientStorage.segments.getRegisteredSegments()).toEqual(['segment1']);
113+
114+
expect(rolloutPlan).toEqual({ ...expectedRolloutPlan, memberships: undefined });
115+
});
116+
117+
test('using preloaded data with memberships and segments', () => {
118+
const rolloutPlan = getRolloutPlan(loggerMock, serverStorage, { keys: [fullSettings.core.key as string], exposeSegments: true });
119+
120+
// @ts-expect-error Load client-side storage with preloaded data
121+
const clientStorage = InMemoryStorageCSFactory({ settings: fullSettings });
122+
setRolloutPlan(loggerMock, rolloutPlan, clientStorage, fullSettings.core.key as string);
123+
124+
// Shared client storage
125+
const sharedClientStorage = clientStorage.shared!(otherKey);
126+
setRolloutPlan(loggerMock, rolloutPlan, { segments: sharedClientStorage.segments, largeSegments: sharedClientStorage.largeSegments }, otherKey);
127+
128+
expect(clientStorage.segments.getRegisteredSegments()).toEqual(['segment1']); // main client membership is set via the rollout plan `memberships` field
129+
expect(sharedClientStorage.segments.getRegisteredSegments()).toEqual(['segment1']); // shared client membership is set via the rollout plan `segmentChanges` field
130+
131+
expect(rolloutPlan).toEqual({ ...expectedRolloutPlan, memberships: { [fullSettings.core.key as string]: expectedRolloutPlan.memberships![fullSettings.core.key as string] } });
132+
});
133+
});

src/storages/dataLoader.ts

Lines changed: 0 additions & 55 deletions
This file was deleted.

0 commit comments

Comments
 (0)