Skip to content

Commit ba83154

Browse files
refactor: set change number only if update operations don't fail, for storage consistency
1 parent d35663b commit ba83154

File tree

8 files changed

+91
-104
lines changed

8 files changed

+91
-104
lines changed

CHANGES.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
2.8.1 (November 21, 2025)
2+
- 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.
3+
14
2.8.0 (October 30, 2025)
25
- 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.
36
- Added `client.getStatus()` method to retrieve the client readiness status properties (`isReady`, `isReadyFromCache`, etc).

src/storages/AbstractMySegmentsCacheSync.ts

Lines changed: 26 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,10 @@ export abstract class AbstractMySegmentsCacheSync implements ISegmentsCacheSync
4949
* For client-side synchronizer: it resets or updates the cache.
5050
*/
5151
resetSegments(segmentsData: MySegmentsData | IMySegmentsResponse): boolean {
52-
this.setChangeNumber(segmentsData.cn);
53-
52+
let isDiff = false;
5453
const { added, removed } = segmentsData as MySegmentsData;
5554

5655
if (added && removed) {
57-
let isDiff = false;
5856

5957
added.forEach(segment => {
6058
isDiff = this.addSegment(segment) || isDiff;
@@ -63,32 +61,40 @@ export abstract class AbstractMySegmentsCacheSync implements ISegmentsCacheSync
6361
removed.forEach(segment => {
6462
isDiff = this.removeSegment(segment) || isDiff;
6563
});
64+
} else {
6665

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

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

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

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

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

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

83-
// Slowest path => add and/or remove segments
84-
for (let removeIndex = index; removeIndex < storedSegmentKeys.length; removeIndex++) {
85-
this.removeSegment(storedSegmentKeys[removeIndex]);
86-
}
88+
for (let addIndex = index; addIndex < names.length; addIndex++) {
89+
this.addSegment(names[addIndex]);
90+
}
8791

88-
for (let addIndex = index; addIndex < names.length; addIndex++) {
89-
this.addSegment(names[addIndex]);
92+
isDiff = true;
93+
}
94+
}
9095
}
9196

92-
return true;
97+
this.setChangeNumber(segmentsData.cn);
98+
return isDiff;
9399
}
94100
}

src/storages/AbstractSplitsCacheSync.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,10 @@ export abstract class AbstractSplitsCacheSync implements ISplitsCacheSync {
1414
protected abstract setChangeNumber(changeNumber: number): boolean | void
1515

1616
update(toAdd: ISplit[], toRemove: ISplit[], changeNumber: number): boolean {
17+
let updated = toAdd.map(addedFF => this.addSplit(addedFF)).some(result => result);
18+
updated = toRemove.map(removedFF => this.removeSplit(removedFF.name)).some(result => result) || updated;
1719
this.setChangeNumber(changeNumber);
18-
const updated = toAdd.map(addedFF => this.addSplit(addedFF)).some(result => result);
19-
return toRemove.map(removedFF => this.removeSplit(removedFF.name)).some(result => result) || updated;
20+
return updated;
2021
}
2122

2223
abstract getSplit(name: string): ISplit | null

src/storages/inLocalStorage/MySegmentsCacheInLocal.ts

Lines changed: 17 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -22,27 +22,17 @@ export class MySegmentsCacheInLocal extends AbstractMySegmentsCacheSync {
2222
protected addSegment(name: string): boolean {
2323
const segmentKey = this.keys.buildSegmentNameKey(name);
2424

25-
try {
26-
if (this.storage.getItem(segmentKey) === DEFINED) return false;
27-
this.storage.setItem(segmentKey, DEFINED);
28-
return true;
29-
} catch (e) {
30-
this.log.error(LOG_PREFIX + e);
31-
return false;
32-
}
25+
if (this.storage.getItem(segmentKey) === DEFINED) return false;
26+
this.storage.setItem(segmentKey, DEFINED);
27+
return true;
3328
}
3429

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

38-
try {
39-
if (this.storage.getItem(segmentKey) !== DEFINED) return false;
40-
this.storage.removeItem(segmentKey);
41-
return true;
42-
} catch (e) {
43-
this.log.error(LOG_PREFIX + e);
44-
return false;
45-
}
33+
if (this.storage.getItem(segmentKey) !== DEFINED) return false;
34+
this.storage.removeItem(segmentKey);
35+
return true;
4636
}
4737

4838
isInSegment(name: string): boolean {
@@ -63,12 +53,8 @@ export class MySegmentsCacheInLocal extends AbstractMySegmentsCacheSync {
6353
}
6454

6555
protected setChangeNumber(changeNumber?: number) {
66-
try {
67-
if (changeNumber) this.storage.setItem(this.keys.buildTillKey(), changeNumber + '');
68-
else this.storage.removeItem(this.keys.buildTillKey());
69-
} catch (e) {
70-
this.log.error(e);
71-
}
56+
if (changeNumber) this.storage.setItem(this.keys.buildTillKey(), changeNumber + '');
57+
else this.storage.removeItem(this.keys.buildTillKey());
7258
}
7359

7460
getChangeNumber() {
@@ -84,4 +70,13 @@ export class MySegmentsCacheInLocal extends AbstractMySegmentsCacheSync {
8470
return n;
8571
}
8672

73+
registerSegments() {
74+
try {
75+
return super.registerSegments();
76+
} catch (e) {
77+
this.log.error(LOG_PREFIX + e);
78+
return false;
79+
}
80+
}
81+
8782
}

src/storages/inLocalStorage/RBSegmentsCacheInLocal.ts

Lines changed: 18 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,10 @@ export class RBSegmentsCacheInLocal implements IRBSegmentsCacheSync {
2626
}
2727

2828
update(toAdd: IRBSegment[], toRemove: IRBSegment[], changeNumber: number): boolean {
29+
let updated = toAdd.map(toAdd => this.add(toAdd)).some(result => result);
30+
updated = toRemove.map(toRemove => this.remove(toRemove.name)).some(result => result) || updated;
2931
this.setChangeNumber(changeNumber);
30-
const updated = toAdd.map(toAdd => this.add(toAdd)).some(result => result);
31-
return toRemove.map(toRemove => this.remove(toRemove.name)).some(result => result) || updated;
32+
return updated;
3233
}
3334

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

5051
private add(rbSegment: IRBSegment): boolean {
51-
try {
52-
const name = rbSegment.name;
53-
const rbSegmentKey = this.keys.buildRBSegmentKey(name);
54-
const rbSegmentFromStorage = this.storage.getItem(rbSegmentKey);
55-
const previous = rbSegmentFromStorage ? JSON.parse(rbSegmentFromStorage) : null;
52+
const name = rbSegment.name;
53+
const rbSegmentKey = this.keys.buildRBSegmentKey(name);
54+
const rbSegmentFromStorage = this.storage.getItem(rbSegmentKey);
55+
const previous = rbSegmentFromStorage ? JSON.parse(rbSegmentFromStorage) : null;
5656

57-
this.storage.setItem(rbSegmentKey, JSON.stringify(rbSegment));
57+
this.storage.setItem(rbSegmentKey, JSON.stringify(rbSegment));
5858

59-
let usesSegmentsDiff = 0;
60-
if (previous && usesSegments(previous)) usesSegmentsDiff--;
61-
if (usesSegments(rbSegment)) usesSegmentsDiff++;
62-
if (usesSegmentsDiff !== 0) this.updateSegmentCount(usesSegmentsDiff);
59+
let usesSegmentsDiff = 0;
60+
if (previous && usesSegments(previous)) usesSegmentsDiff--;
61+
if (usesSegments(rbSegment)) usesSegmentsDiff++;
62+
if (usesSegmentsDiff !== 0) this.updateSegmentCount(usesSegmentsDiff);
6363

64-
return true;
65-
} catch (e) {
66-
this.log.error(LOG_PREFIX + e);
67-
return false;
68-
}
64+
return true;
6965
}
7066

7167
private remove(name: string): boolean {
72-
try {
73-
const rbSegment = this.get(name);
74-
if (!rbSegment) return false;
68+
const rbSegment = this.get(name);
69+
if (!rbSegment) return false;
7570

76-
this.storage.removeItem(this.keys.buildRBSegmentKey(name));
71+
this.storage.removeItem(this.keys.buildRBSegmentKey(name));
7772

78-
if (usesSegments(rbSegment)) this.updateSegmentCount(-1);
73+
if (usesSegments(rbSegment)) this.updateSegmentCount(-1);
7974

80-
return true;
81-
} catch (e) {
82-
this.log.error(LOG_PREFIX + e);
83-
return false;
84-
}
75+
return true;
8576
}
8677

8778
private getNames(): string[] {

src/storages/inLocalStorage/SplitsCacheInLocal.ts

Lines changed: 19 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -80,44 +80,34 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync {
8080
}
8181

8282
addSplit(split: ISplit) {
83-
try {
84-
const name = split.name;
85-
const splitKey = this.keys.buildSplitKey(name);
86-
const splitFromStorage = this.storage.getItem(splitKey);
87-
const previousSplit = splitFromStorage ? JSON.parse(splitFromStorage) : null;
88-
89-
if (previousSplit) {
90-
this._decrementCounts(previousSplit);
91-
this.removeFromFlagSets(previousSplit.name, previousSplit.sets);
92-
}
83+
const name = split.name;
84+
const splitKey = this.keys.buildSplitKey(name);
85+
const splitFromStorage = this.storage.getItem(splitKey);
86+
const previousSplit = splitFromStorage ? JSON.parse(splitFromStorage) : null;
87+
88+
if (previousSplit) {
89+
this._decrementCounts(previousSplit);
90+
this.removeFromFlagSets(previousSplit.name, previousSplit.sets);
91+
}
9392

94-
this.storage.setItem(splitKey, JSON.stringify(split));
93+
this.storage.setItem(splitKey, JSON.stringify(split));
9594

96-
this._incrementCounts(split);
97-
this.addToFlagSets(split);
95+
this._incrementCounts(split);
96+
this.addToFlagSets(split);
9897

99-
return true;
100-
} catch (e) {
101-
this.log.error(LOG_PREFIX + e);
102-
return false;
103-
}
98+
return true;
10499
}
105100

106101
removeSplit(name: string): boolean {
107-
try {
108-
const split = this.getSplit(name);
109-
if (!split) return false;
102+
const split = this.getSplit(name);
103+
if (!split) return false;
110104

111-
this.storage.removeItem(this.keys.buildSplitKey(name));
105+
this.storage.removeItem(this.keys.buildSplitKey(name));
112106

113-
this._decrementCounts(split);
114-
this.removeFromFlagSets(split.name, split.sets);
107+
this._decrementCounts(split);
108+
this.removeFromFlagSets(split.name, split.sets);
115109

116-
return true;
117-
} catch (e) {
118-
this.log.error(LOG_PREFIX + e);
119-
return false;
120-
}
110+
return true;
121111
}
122112

123113
getSplit(name: string): ISplit | null {

src/storages/inMemory/RBSegmentsCacheInMemory.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,10 @@ export class RBSegmentsCacheInMemory implements IRBSegmentsCacheSync {
1616
}
1717

1818
update(toAdd: IRBSegment[], toRemove: IRBSegment[], changeNumber: number): boolean {
19+
let updated = toAdd.map(toAdd => this.add(toAdd)).some(result => result);
20+
updated = toRemove.map(toRemove => this.remove(toRemove.name)).some(result => result) || updated;
1921
this.changeNumber = changeNumber;
20-
const updated = toAdd.map(toAdd => this.add(toAdd)).some(result => result);
21-
return toRemove.map(toRemove => this.remove(toRemove.name)).some(result => result) || updated;
22+
return updated;
2223
}
2324

2425
private add(rbSegment: IRBSegment): boolean {

src/sync/polling/updaters/splitChangesUpdater.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -163,8 +163,6 @@ export function splitChangesUpdaterFactory(
163163
splitChangesFetcher(since, noCache, till, rbSince, _promiseDecorator)
164164
)
165165
.then((splitChanges: ISplitChangesResponse) => {
166-
startingUp = false;
167-
168166
const usedSegments = new Set<string>();
169167

170168
let ffUpdate: MaybeThenable<boolean> = false;
@@ -187,6 +185,8 @@ export function splitChangesUpdaterFactory(
187185
]).then(([ffChanged, rbsChanged]) => {
188186
if (storage.save) storage.save();
189187

188+
startingUp = false;
189+
190190
if (splitsEventEmitter) {
191191
// To emit SDK_SPLITS_ARRIVED for server-side SDK, we must check that all registered segments have been fetched
192192
return Promise.resolve(!splitsEventEmitter.splitsArrived || ((ffChanged || rbsChanged) && (isClientSide || checkAllSegmentsExist(segments))))

0 commit comments

Comments
 (0)