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: 3 additions & 0 deletions CHANGES.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
2.8.1 (November 25, 2025)
- Updated the order of storage operations to prevent inconsistent states when using the `LOCALSTORAGE` storage type and the browser’s `localStorage` fails due to quota limits.

2.8.0 (October 30, 2025)
- Added new configuration for Fallback Treatments, which allows setting a treatment value and optional config to be returned in place of "control", either globally or by flag. Read more in our docs.
- Added `client.getStatus()` method to retrieve the client readiness status properties (`isReady`, `isReadyFromCache`, etc).
Expand Down
4 changes: 0 additions & 4 deletions src/readiness/__tests__/readinessManager.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,10 @@ import { EventEmitter } from '../../utils/MinEvents';
import { IReadinessManager } from '../types';
import { SDK_READY, SDK_UPDATE, SDK_SPLITS_ARRIVED, SDK_SEGMENTS_ARRIVED, SDK_READY_FROM_CACHE, SDK_SPLITS_CACHE_LOADED, SDK_READY_TIMED_OUT } from '../constants';
import { ISettings } from '../../types';
import { STORAGE_LOCALSTORAGE } from '../../utils/constants';

const settings = {
startup: {
readyTimeout: 0,
},
storage: {
type: STORAGE_LOCALSTORAGE
}
} as unknown as ISettings;

Expand Down
46 changes: 26 additions & 20 deletions src/storages/AbstractMySegmentsCacheSync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,10 @@ export abstract class AbstractMySegmentsCacheSync implements ISegmentsCacheSync
* For client-side synchronizer: it resets or updates the cache.
*/
resetSegments(segmentsData: MySegmentsData | IMySegmentsResponse): boolean {
this.setChangeNumber(segmentsData.cn);

let isDiff = false;
const { added, removed } = segmentsData as MySegmentsData;

if (added && removed) {
let isDiff = false;

added.forEach(segment => {
isDiff = this.addSegment(segment) || isDiff;
Expand All @@ -63,32 +61,40 @@ export abstract class AbstractMySegmentsCacheSync implements ISegmentsCacheSync
removed.forEach(segment => {
isDiff = this.removeSegment(segment) || isDiff;
});
} else {

return isDiff;
}
const names = ((segmentsData as IMySegmentsResponse).k || []).map(s => s.n).sort();
const storedSegmentKeys = this.getRegisteredSegments().sort();

const names = ((segmentsData as IMySegmentsResponse).k || []).map(s => s.n).sort();
const storedSegmentKeys = this.getRegisteredSegments().sort();
// Extreme fast => everything is empty
if (!names.length && !storedSegmentKeys.length) {
isDiff = false;
} else {

// Extreme fast => everything is empty
if (!names.length && !storedSegmentKeys.length) return false;
let index = 0;

let index = 0;
while (index < names.length && index < storedSegmentKeys.length && names[index] === storedSegmentKeys[index]) index++;

while (index < names.length && index < storedSegmentKeys.length && names[index] === storedSegmentKeys[index]) index++;
// Quick path => no changes
if (index === names.length && index === storedSegmentKeys.length) {
isDiff = false;
} else {

// Quick path => no changes
if (index === names.length && index === storedSegmentKeys.length) return false;
// Slowest path => add and/or remove segments
for (let removeIndex = index; removeIndex < storedSegmentKeys.length; removeIndex++) {
this.removeSegment(storedSegmentKeys[removeIndex]);
}

// Slowest path => add and/or remove segments
for (let removeIndex = index; removeIndex < storedSegmentKeys.length; removeIndex++) {
this.removeSegment(storedSegmentKeys[removeIndex]);
}
for (let addIndex = index; addIndex < names.length; addIndex++) {
this.addSegment(names[addIndex]);
}

for (let addIndex = index; addIndex < names.length; addIndex++) {
this.addSegment(names[addIndex]);
isDiff = true;
}
}
}

return true;
this.setChangeNumber(segmentsData.cn);
return isDiff;
}
}
5 changes: 3 additions & 2 deletions src/storages/AbstractSplitsCacheSync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,10 @@ export abstract class AbstractSplitsCacheSync implements ISplitsCacheSync {
protected abstract setChangeNumber(changeNumber: number): boolean | void

update(toAdd: ISplit[], toRemove: ISplit[], changeNumber: number): boolean {
let updated = toAdd.map(addedFF => this.addSplit(addedFF)).some(result => result);
updated = toRemove.map(removedFF => this.removeSplit(removedFF.name)).some(result => result) || updated;
this.setChangeNumber(changeNumber);
const updated = toAdd.map(addedFF => this.addSplit(addedFF)).some(result => result);
return toRemove.map(removedFF => this.removeSplit(removedFF.name)).some(result => result) || updated;
return updated;
}

abstract getSplit(name: string): ISplit | null
Expand Down
33 changes: 9 additions & 24 deletions src/storages/inLocalStorage/MySegmentsCacheInLocal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { ILogger } from '../../logger/types';
import { isNaNNumber } from '../../utils/lang';
import { AbstractMySegmentsCacheSync } from '../AbstractMySegmentsCacheSync';
import type { MySegmentsKeyBuilder } from '../KeyBuilderCS';
import { LOG_PREFIX, DEFINED } from './constants';
import { DEFINED } from './constants';
import { StorageAdapter } from '../types';

export class MySegmentsCacheInLocal extends AbstractMySegmentsCacheSync {
Expand All @@ -16,33 +16,22 @@ export class MySegmentsCacheInLocal extends AbstractMySegmentsCacheSync {
this.log = log;
this.keys = keys;
this.storage = storage;
// There is not need to flush segments cache like splits cache, since resetSegments receives the up-to-date list of active segments
}

protected addSegment(name: string): boolean {
const segmentKey = this.keys.buildSegmentNameKey(name);

try {
if (this.storage.getItem(segmentKey) === DEFINED) return false;
this.storage.setItem(segmentKey, DEFINED);
return true;
} catch (e) {
this.log.error(LOG_PREFIX + e);
return false;
}
if (this.storage.getItem(segmentKey) === DEFINED) return false;
this.storage.setItem(segmentKey, DEFINED);
return true;
}

protected removeSegment(name: string): boolean {
const segmentKey = this.keys.buildSegmentNameKey(name);

try {
if (this.storage.getItem(segmentKey) !== DEFINED) return false;
this.storage.removeItem(segmentKey);
return true;
} catch (e) {
this.log.error(LOG_PREFIX + e);
return false;
}
if (this.storage.getItem(segmentKey) !== DEFINED) return false;
this.storage.removeItem(segmentKey);
return true;
}

isInSegment(name: string): boolean {
Expand All @@ -63,12 +52,8 @@ export class MySegmentsCacheInLocal extends AbstractMySegmentsCacheSync {
}

protected setChangeNumber(changeNumber?: number) {
try {
if (changeNumber) this.storage.setItem(this.keys.buildTillKey(), changeNumber + '');
else this.storage.removeItem(this.keys.buildTillKey());
} catch (e) {
this.log.error(e);
}
if (changeNumber) this.storage.setItem(this.keys.buildTillKey(), changeNumber + '');
else this.storage.removeItem(this.keys.buildTillKey());
}

getChangeNumber() {
Expand Down
45 changes: 18 additions & 27 deletions src/storages/inLocalStorage/RBSegmentsCacheInLocal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,10 @@ export class RBSegmentsCacheInLocal implements IRBSegmentsCacheSync {
}

update(toAdd: IRBSegment[], toRemove: IRBSegment[], changeNumber: number): boolean {
let updated = toAdd.map(toAdd => this.add(toAdd)).some(result => result);
updated = toRemove.map(toRemove => this.remove(toRemove.name)).some(result => result) || updated;
this.setChangeNumber(changeNumber);
const updated = toAdd.map(toAdd => this.add(toAdd)).some(result => result);
return toRemove.map(toRemove => this.remove(toRemove.name)).some(result => result) || updated;
return updated;
}

private setChangeNumber(changeNumber: number) {
Expand All @@ -48,40 +49,30 @@ export class RBSegmentsCacheInLocal implements IRBSegmentsCacheSync {
}

private add(rbSegment: IRBSegment): boolean {
try {
const name = rbSegment.name;
const rbSegmentKey = this.keys.buildRBSegmentKey(name);
const rbSegmentFromStorage = this.storage.getItem(rbSegmentKey);
const previous = rbSegmentFromStorage ? JSON.parse(rbSegmentFromStorage) : null;
const name = rbSegment.name;
const rbSegmentKey = this.keys.buildRBSegmentKey(name);
const rbSegmentFromStorage = this.storage.getItem(rbSegmentKey);
const previous = rbSegmentFromStorage ? JSON.parse(rbSegmentFromStorage) : null;

this.storage.setItem(rbSegmentKey, JSON.stringify(rbSegment));
this.storage.setItem(rbSegmentKey, JSON.stringify(rbSegment));

let usesSegmentsDiff = 0;
if (previous && usesSegments(previous)) usesSegmentsDiff--;
if (usesSegments(rbSegment)) usesSegmentsDiff++;
if (usesSegmentsDiff !== 0) this.updateSegmentCount(usesSegmentsDiff);
let usesSegmentsDiff = 0;
if (previous && usesSegments(previous)) usesSegmentsDiff--;
if (usesSegments(rbSegment)) usesSegmentsDiff++;
if (usesSegmentsDiff !== 0) this.updateSegmentCount(usesSegmentsDiff);

return true;
} catch (e) {
this.log.error(LOG_PREFIX + e);
return false;
}
return true;
}

private remove(name: string): boolean {
try {
const rbSegment = this.get(name);
if (!rbSegment) return false;
const rbSegment = this.get(name);
if (!rbSegment) return false;

this.storage.removeItem(this.keys.buildRBSegmentKey(name));
this.storage.removeItem(this.keys.buildRBSegmentKey(name));

if (usesSegments(rbSegment)) this.updateSegmentCount(-1);
if (usesSegments(rbSegment)) this.updateSegmentCount(-1);

return true;
} catch (e) {
this.log.error(LOG_PREFIX + e);
return false;
}
return true;
}

private getNames(): string[] {
Expand Down
51 changes: 22 additions & 29 deletions src/storages/inLocalStorage/SplitsCacheInLocal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,44 +80,34 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync {
}

addSplit(split: ISplit) {
try {
const name = split.name;
const splitKey = this.keys.buildSplitKey(name);
const splitFromStorage = this.storage.getItem(splitKey);
const previousSplit = splitFromStorage ? JSON.parse(splitFromStorage) : null;

if (previousSplit) {
this._decrementCounts(previousSplit);
this.removeFromFlagSets(previousSplit.name, previousSplit.sets);
}
const name = split.name;
const splitKey = this.keys.buildSplitKey(name);
const splitFromStorage = this.storage.getItem(splitKey);
const previousSplit = splitFromStorage ? JSON.parse(splitFromStorage) : null;

if (previousSplit) {
this._decrementCounts(previousSplit);
this.removeFromFlagSets(previousSplit.name, previousSplit.sets);
}

this.storage.setItem(splitKey, JSON.stringify(split));
this.storage.setItem(splitKey, JSON.stringify(split));

this._incrementCounts(split);
this.addToFlagSets(split);
this._incrementCounts(split);
this.addToFlagSets(split);

return true;
} catch (e) {
this.log.error(LOG_PREFIX + e);
return false;
}
return true;
}

removeSplit(name: string): boolean {
try {
const split = this.getSplit(name);
if (!split) return false;
const split = this.getSplit(name);
if (!split) return false;

this.storage.removeItem(this.keys.buildSplitKey(name));
this.storage.removeItem(this.keys.buildSplitKey(name));

this._decrementCounts(split);
this.removeFromFlagSets(split.name, split.sets);
this._decrementCounts(split);
this.removeFromFlagSets(split.name, split.sets);

return true;
} catch (e) {
this.log.error(LOG_PREFIX + e);
return false;
}
return true;
}

getSplit(name: string): ISplit | null {
Expand Down Expand Up @@ -206,6 +196,9 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync {
const flagSetFromStorage = this.storage.getItem(flagSetKey);

const flagSetCache = new Set(flagSetFromStorage ? JSON.parse(flagSetFromStorage) : []);

if (flagSetCache.has(featureFlag.name)) return;

flagSetCache.add(featureFlag.name);

this.storage.setItem(flagSetKey, JSON.stringify(setToArray(flagSetCache)));
Expand Down
5 changes: 5 additions & 0 deletions src/storages/inLocalStorage/__tests__/validateCache.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ describe.each(storages)('validateCache', (storage) => {
test('if there is cache and it must not be cleared, it should return true', async () => {
storage.setItem(keys.buildSplitsTillKey(), '1');
storage.setItem(keys.buildHashKey(), FULL_SETTINGS_HASH);
await storage.save && storage.save();

expect(await validateCache({}, storage, fullSettings, keys, splits, rbSegments, segments, largeSegments)).toBe(true);

Expand All @@ -66,6 +67,7 @@ describe.each(storages)('validateCache', (storage) => {
storage.setItem(keys.buildSplitsTillKey(), '1');
storage.setItem(keys.buildHashKey(), FULL_SETTINGS_HASH);
storage.setItem(keys.buildLastUpdatedKey(), Date.now() - 1000 * 60 * 60 * 24 * 2 + ''); // 2 days ago
await storage.save && storage.save();

expect(await validateCache({ expirationDays: 1 }, storage, fullSettings, keys, splits, rbSegments, segments, largeSegments)).toBe(false);

Expand All @@ -83,6 +85,7 @@ describe.each(storages)('validateCache', (storage) => {
test('if there is cache and its hash has changed, it should clear cache and return false', async () => {
storage.setItem(keys.buildSplitsTillKey(), '1');
storage.setItem(keys.buildHashKey(), FULL_SETTINGS_HASH);
await storage.save && storage.save();

expect(await validateCache({}, storage, { ...fullSettings, core: { ...fullSettings.core, authorizationKey: 'another-sdk-key' } }, keys, splits, rbSegments, segments, largeSegments)).toBe(false);

Expand All @@ -99,8 +102,10 @@ describe.each(storages)('validateCache', (storage) => {

test('if there is cache and clearOnInit is true, it should clear cache and return false', async () => {
// Older cache version (without last clear)
storage.removeItem(keys.buildLastClear());
storage.setItem(keys.buildSplitsTillKey(), '1');
storage.setItem(keys.buildHashKey(), FULL_SETTINGS_HASH);
await storage.save && storage.save();

expect(await validateCache({ clearOnInit: true }, storage, fullSettings, keys, splits, rbSegments, segments, largeSegments)).toBe(false);

Expand Down
5 changes: 3 additions & 2 deletions src/storages/inMemory/RBSegmentsCacheInMemory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@ export class RBSegmentsCacheInMemory implements IRBSegmentsCacheSync {
}

update(toAdd: IRBSegment[], toRemove: IRBSegment[], changeNumber: number): boolean {
let updated = toAdd.map(toAdd => this.add(toAdd)).some(result => result);
updated = toRemove.map(toRemove => this.remove(toRemove.name)).some(result => result) || updated;
this.changeNumber = changeNumber;
const updated = toAdd.map(toAdd => this.add(toAdd)).some(result => result);
return toRemove.map(toRemove => this.remove(toRemove.name)).some(result => result) || updated;
return updated;
}

private add(rbSegment: IRBSegment): boolean {
Expand Down
2 changes: 1 addition & 1 deletion src/storages/inRedis/SegmentsCacheInRedis.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ILogger } from '../../logger/types';
import { isNaNNumber } from '../../utils/lang';
import { LOG_PREFIX } from '../inLocalStorage/constants';
import { LOG_PREFIX } from './constants';
import { KeyBuilderSS } from '../KeyBuilderSS';
import { ISegmentsCacheAsync } from '../types';
import type { RedisAdapter } from './RedisAdapter';
Expand Down
6 changes: 3 additions & 3 deletions src/sync/polling/updaters/mySegmentsUpdater.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,10 @@ export function mySegmentsUpdaterFactory(
new Promise((res) => { updateSegments(segmentsData); res(true); }) :
// If not provided, fetch mySegments
mySegmentsFetcher(matchingKey, noCache, till, _promiseDecorator).then(segments => {
// Only when we have downloaded segments completely, we should not keep retrying anymore
startingUp = false;

updateSegments(segments);

// Only when we have downloaded and stored segments completely, we should not keep retrying anymore
startingUp = false;
return true;
});

Expand Down
Loading