Skip to content

Commit 358a6b7

Browse files
Place 'setChangeNumber' call as last operation inside storage 'update' methods, to signal transaction commit
1 parent db1f424 commit 358a6b7

File tree

7 files changed

+49
-41
lines changed

7 files changed

+49
-41
lines changed

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-
5452
const { added, removed } = segmentsData as MySegmentsData;
53+
let isDiff = false;
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/RBSegmentsCacheInLocal.ts

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

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

3536
private setChangeNumber(changeNumber: number) {

src/storages/inLocalStorage/__tests__/validateCache.spec.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ describe('validateCache', () => {
2929
});
3030

3131
test('if there is no cache, it should return false', async () => {
32-
expect(await validateCache({ storage: localStorage }, fullSettings, keys, splits, rbSegments, segments, largeSegments)).toBe(false);
32+
expect(await validateCache({}, localStorage, fullSettings, keys, splits, rbSegments, segments, largeSegments)).toBe(false);
3333

3434
expect(logSpy).not.toHaveBeenCalled();
3535

@@ -47,7 +47,7 @@ describe('validateCache', () => {
4747
localStorage.setItem(keys.buildSplitsTillKey(), '1');
4848
localStorage.setItem(keys.buildHashKey(), FULL_SETTINGS_HASH);
4949

50-
expect(await validateCache({ storage: localStorage }, fullSettings, keys, splits, rbSegments, segments, largeSegments)).toBe(true);
50+
expect(await validateCache({}, localStorage, fullSettings, keys, splits, rbSegments, segments, largeSegments)).toBe(true);
5151

5252
expect(logSpy).not.toHaveBeenCalled();
5353

@@ -66,7 +66,7 @@ describe('validateCache', () => {
6666
localStorage.setItem(keys.buildHashKey(), FULL_SETTINGS_HASH);
6767
localStorage.setItem(keys.buildLastUpdatedKey(), Date.now() - 1000 * 60 * 60 * 24 * 2 + ''); // 2 days ago
6868

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

7171
expect(logSpy).toHaveBeenCalledWith('storage:localstorage: Cache expired more than 1 days ago. Cleaning up cache');
7272

@@ -83,7 +83,7 @@ describe('validateCache', () => {
8383
localStorage.setItem(keys.buildSplitsTillKey(), '1');
8484
localStorage.setItem(keys.buildHashKey(), FULL_SETTINGS_HASH);
8585

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

8888
expect(logSpy).toHaveBeenCalledWith('storage:localstorage: SDK key, flags filter criteria, or flags spec version has changed. Cleaning up cache');
8989

@@ -101,7 +101,7 @@ describe('validateCache', () => {
101101
localStorage.setItem(keys.buildSplitsTillKey(), '1');
102102
localStorage.setItem(keys.buildHashKey(), FULL_SETTINGS_HASH);
103103

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

106106
expect(logSpy).toHaveBeenCalledWith('storage:localstorage: clearOnInit was set and cache was not cleared in the last 24 hours. Cleaning up cache');
107107

@@ -117,13 +117,13 @@ describe('validateCache', () => {
117117
// If cache is cleared, it should not clear again until a day has passed
118118
logSpy.mockClear();
119119
localStorage.setItem(keys.buildSplitsTillKey(), '1');
120-
expect(await validateCache({ storage: localStorage, clearOnInit: true }, fullSettings, keys, splits, rbSegments, segments, largeSegments)).toBe(true);
120+
expect(await validateCache({ clearOnInit: true }, localStorage, fullSettings, keys, splits, rbSegments, segments, largeSegments)).toBe(true);
121121
expect(logSpy).not.toHaveBeenCalled();
122122
expect(localStorage.getItem(keys.buildLastClear())).toBe(lastClear); // Last clear should not have changed
123123

124124
// If a day has passed, it should clear again
125125
localStorage.setItem(keys.buildLastClear(), (Date.now() - 1000 * 60 * 60 * 24 - 1) + '');
126-
expect(await validateCache({ storage: localStorage, clearOnInit: true }, fullSettings, keys, splits, rbSegments, segments, largeSegments)).toBe(false);
126+
expect(await validateCache({ clearOnInit: true }, localStorage, fullSettings, keys, splits, rbSegments, segments, largeSegments)).toBe(false);
127127
expect(logSpy).toHaveBeenCalledWith('storage:localstorage: clearOnInit was set and cache was not cleared in the last 24 hours. Cleaning up cache');
128128
expect(splits.clear).toHaveBeenCalledTimes(2);
129129
expect(rbSegments.clear).toHaveBeenCalledTimes(2);

src/storages/inLocalStorage/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ export function InLocalStorage(options: SplitIO.InLocalStorageOptions = {}): ISt
6363
uniqueKeys: new UniqueKeysCacheInMemoryCS(),
6464

6565
validateCache() {
66-
return validateCachePromise || (validateCachePromise = validateCache({ ...options, storage }, settings, keys, splits, rbSegments, segments, largeSegments));
66+
return validateCachePromise || (validateCachePromise = validateCache(options, storage, settings, keys, splits, rbSegments, segments, largeSegments));
6767
},
6868

6969
destroy() {

src/storages/inLocalStorage/validateCache.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,8 @@ const MILLIS_IN_A_DAY = 86400000;
1616
*
1717
* @returns `true` if cache should be cleared, `false` otherwise
1818
*/
19-
function validateExpiration(options: SplitIO.InLocalStorageOptions & { storage: SplitIO.Storage }, settings: ISettings, keys: KeyBuilderCS, currentTimestamp: number, isThereCache: boolean) {
19+
function validateExpiration(options: SplitIO.InLocalStorageOptions, storage: SplitIO.Storage, settings: ISettings, keys: KeyBuilderCS, currentTimestamp: number, isThereCache: boolean) {
2020
const { log } = settings;
21-
const { storage } = options;
2221

2322
// Check expiration
2423
const lastUpdatedTimestamp = parseInt(storage.getItem(keys.buildLastUpdatedKey()) as string, 10);
@@ -68,28 +67,29 @@ function validateExpiration(options: SplitIO.InLocalStorageOptions & { storage:
6867
*
6968
* @returns `true` if cache is ready to be used, `false` otherwise (cache was cleared or there is no cache)
7069
*/
71-
export function validateCache(options: SplitIO.InLocalStorageOptions & { storage: SplitIO.Storage }, settings: ISettings, keys: KeyBuilderCS, splits: SplitsCacheInLocal, rbSegments: RBSegmentsCacheInLocal, segments: MySegmentsCacheInLocal, largeSegments: MySegmentsCacheInLocal): Promise<boolean> {
72-
return new Promise<boolean>((resolve) => {
70+
export function validateCache(options: SplitIO.InLocalStorageOptions, storage: SplitIO.Storage, settings: ISettings, keys: KeyBuilderCS, splits: SplitsCacheInLocal, rbSegments: RBSegmentsCacheInLocal, segments: MySegmentsCacheInLocal, largeSegments: MySegmentsCacheInLocal): Promise<boolean> {
71+
72+
return Promise.resolve().then(() => {
7373
const currentTimestamp = Date.now();
7474
const isThereCache = splits.getChangeNumber() > -1;
7575

76-
if (validateExpiration(options, settings, keys, currentTimestamp, isThereCache)) {
76+
if (validateExpiration(options, storage, settings, keys, currentTimestamp, isThereCache)) {
7777
splits.clear();
7878
rbSegments.clear();
7979
segments.clear();
8080
largeSegments.clear();
8181

8282
// Update last clear timestamp
8383
try {
84-
options.storage.setItem(keys.buildLastClear(), currentTimestamp + '');
84+
storage.setItem(keys.buildLastClear(), currentTimestamp + '');
8585
} catch (e) {
8686
settings.log.error(LOG_PREFIX + e);
8787
}
8888

89-
resolve(false);
89+
return false;
9090
}
9191

9292
// Check if ready from cache
93-
resolve(isThereCache);
93+
return isThereCache;
9494
});
9595
}

types/splitio.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1012,7 +1012,7 @@ declare namespace SplitIO {
10121012
*/
10131013
clearOnInit?: boolean;
10141014
/**
1015-
* Optional storage API to use. If not provided, the SDK will use the default localStorage Web API.
1015+
* Optional storage API to persist rollout plan related data. If not provided, the SDK will use the default localStorage Web API.
10161016
*
10171017
* @defaultValue `window.localStorage`
10181018
*/

0 commit comments

Comments
 (0)