Skip to content

Commit edb8995

Browse files
Merge pull request #379 from splitio/cache_expiration_refactor_validateCache_method
[Cache expiration] Add `validateCache` function
2 parents 49c7f52 + 6605bfc commit edb8995

File tree

5 files changed

+69
-73
lines changed

5 files changed

+69
-73
lines changed

src/storages/inLocalStorage/SplitsCacheInLocal.ts

Lines changed: 1 addition & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import { KeyBuilderCS } from '../KeyBuilderCS';
55
import { ILogger } from '../../logger/types';
66
import { LOG_PREFIX } from './constants';
77
import { ISettings } from '../../types';
8-
import { getStorageHash } from '../KeyBuilder';
98
import { setToArray } from '../../utils/lang/sets';
109

1110
/**
@@ -15,21 +14,14 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync {
1514

1615
private readonly keys: KeyBuilderCS;
1716
private readonly log: ILogger;
18-
private readonly storageHash: string;
1917
private readonly flagSetsFilter: string[];
2018
private hasSync?: boolean;
21-
private updateNewFilter?: boolean;
2219

23-
constructor(settings: ISettings, keys: KeyBuilderCS, expirationTimestamp?: number) {
20+
constructor(settings: ISettings, keys: KeyBuilderCS) {
2421
super();
2522
this.keys = keys;
2623
this.log = settings.log;
27-
this.storageHash = getStorageHash(settings);
2824
this.flagSetsFilter = settings.sync.__splitFiltersValidation.groupedFilters.bySet;
29-
30-
this._checkExpiration(expirationTimestamp);
31-
32-
this._checkFilterQuery();
3325
}
3426

3527
private _decrementCount(key: string) {
@@ -138,19 +130,6 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync {
138130
}
139131

140132
setChangeNumber(changeNumber: number): boolean {
141-
142-
// when using a new split query, we must update it at the store
143-
if (this.updateNewFilter) {
144-
this.log.info(LOG_PREFIX + 'SDK key, flags filter criteria or flags spec version was modified. Updating cache');
145-
const storageHashKey = this.keys.buildHashKey();
146-
try {
147-
localStorage.setItem(storageHashKey, this.storageHash);
148-
} catch (e) {
149-
this.log.error(LOG_PREFIX + e);
150-
}
151-
this.updateNewFilter = false;
152-
}
153-
154133
try {
155134
localStorage.setItem(this.keys.buildSplitsTillKey(), changeNumber + '');
156135
// update "last updated" timestamp with current time
@@ -212,39 +191,6 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync {
212191
}
213192
}
214193

215-
/**
216-
* Clean Splits cache if its `lastUpdated` timestamp is older than the given `expirationTimestamp`,
217-
*
218-
* @param expirationTimestamp - if the value is not a number, data will not be cleaned
219-
*/
220-
private _checkExpiration(expirationTimestamp?: number) {
221-
let value: string | number | null = localStorage.getItem(this.keys.buildLastUpdatedKey());
222-
if (value !== null) {
223-
value = parseInt(value, 10);
224-
if (!isNaNNumber(value) && expirationTimestamp && value < expirationTimestamp) this.clear();
225-
}
226-
}
227-
228-
// @TODO eventually remove `_checkFilterQuery`. Cache should be cleared at the storage level, reusing same logic than PluggableStorage
229-
private _checkFilterQuery() {
230-
const storageHashKey = this.keys.buildHashKey();
231-
const storageHash = localStorage.getItem(storageHashKey);
232-
233-
if (storageHash !== this.storageHash) {
234-
try {
235-
// mark cache to update the new query filter on first successful splits fetch
236-
this.updateNewFilter = true;
237-
238-
// if there is cache, clear it
239-
if (this.getChangeNumber() > -1) this.clear();
240-
241-
} catch (e) {
242-
this.log.error(LOG_PREFIX + e);
243-
}
244-
}
245-
// if the filter didn't change, nothing is done
246-
}
247-
248194
getNamesByFlagSets(flagSets: string[]): Set<string>[] {
249195
return flagSets.map(flagSet => {
250196
const flagSetKey = this.keys.buildFlagSetKey(flagSet);

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

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ test('SPLIT CACHE / LocalStorage / trafficTypeExists and ttcache tests', () => {
104104

105105
test('SPLIT CACHE / LocalStorage / killLocally', () => {
106106
const cache = new SplitsCacheInLocal(fullSettings, new KeyBuilderCS('SPLITIO', 'user'));
107+
107108
cache.addSplit('lol1', something);
108109
cache.addSplit('lol2', somethingElse);
109110
const initialChangeNumber = cache.getChangeNumber();
@@ -167,6 +168,7 @@ test('SPLIT CACHE / LocalStorage / flag set cache tests', () => {
167168
}
168169
}
169170
}, new KeyBuilderCS('SPLITIO', 'user'));
171+
170172
const emptySet = new Set([]);
171173

172174
cache.addSplits([
@@ -206,25 +208,25 @@ test('SPLIT CACHE / LocalStorage / flag set cache tests', () => {
206208

207209
// if FlagSets are not defined, it should store all FlagSets in memory.
208210
test('SPLIT CACHE / LocalStorage / flag set cache tests without filters', () => {
209-
const cacheWithoutFilters = new SplitsCacheInLocal(fullSettings, new KeyBuilderCS('SPLITIO', 'user'));
211+
const cache = new SplitsCacheInLocal(fullSettings, new KeyBuilderCS('SPLITIO', 'user'));
212+
210213
const emptySet = new Set([]);
211214

212-
cacheWithoutFilters.addSplits([
215+
cache.addSplits([
213216
[featureFlagOne.name, featureFlagOne],
214217
[featureFlagTwo.name, featureFlagTwo],
215218
[featureFlagThree.name, featureFlagThree],
216219
]);
217-
cacheWithoutFilters.addSplit(featureFlagWithEmptyFS.name, featureFlagWithEmptyFS);
220+
cache.addSplit(featureFlagWithEmptyFS.name, featureFlagWithEmptyFS);
218221

219-
expect(cacheWithoutFilters.getNamesByFlagSets(['o'])).toEqual([new Set(['ff_one', 'ff_two'])]);
220-
expect(cacheWithoutFilters.getNamesByFlagSets(['n'])).toEqual([new Set(['ff_one'])]);
221-
expect(cacheWithoutFilters.getNamesByFlagSets(['e'])).toEqual([new Set(['ff_one', 'ff_three'])]);
222-
expect(cacheWithoutFilters.getNamesByFlagSets(['t'])).toEqual([new Set(['ff_two', 'ff_three'])]);
223-
expect(cacheWithoutFilters.getNamesByFlagSets(['y'])).toEqual([emptySet]);
224-
expect(cacheWithoutFilters.getNamesByFlagSets(['o', 'n', 'e'])).toEqual([new Set(['ff_one', 'ff_two']), new Set(['ff_one']), new Set(['ff_one', 'ff_three'])]);
222+
expect(cache.getNamesByFlagSets(['o'])).toEqual([new Set(['ff_one', 'ff_two'])]);
223+
expect(cache.getNamesByFlagSets(['n'])).toEqual([new Set(['ff_one'])]);
224+
expect(cache.getNamesByFlagSets(['e'])).toEqual([new Set(['ff_one', 'ff_three'])]);
225+
expect(cache.getNamesByFlagSets(['t'])).toEqual([new Set(['ff_two', 'ff_three'])]);
226+
expect(cache.getNamesByFlagSets(['y'])).toEqual([emptySet]);
227+
expect(cache.getNamesByFlagSets(['o', 'n', 'e'])).toEqual([new Set(['ff_one', 'ff_two']), new Set(['ff_one']), new Set(['ff_one', 'ff_three'])]);
225228

226229
// Validate that the feature flag cache is cleared when calling `clear` method
227-
cacheWithoutFilters.clear();
228-
expect(localStorage.length).toBe(1); // only 'SPLITIO.hash' should remain in localStorage
229-
expect(localStorage.key(0)).toBe('SPLITIO.hash');
230+
cache.clear();
231+
expect(localStorage.length).toBe(0);
230232
});

src/storages/inLocalStorage/index.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@ import { KeyBuilderCS, myLargeSegmentsKeyBuilder } from '../KeyBuilderCS';
77
import { isLocalStorageAvailable } from '../../utils/env/isLocalStorageAvailable';
88
import { SplitsCacheInLocal } from './SplitsCacheInLocal';
99
import { MySegmentsCacheInLocal } from './MySegmentsCacheInLocal';
10-
import { DEFAULT_CACHE_EXPIRATION_IN_MILLIS } from '../../utils/constants/browser';
1110
import { InMemoryStorageCSFactory } from '../inMemory/InMemoryStorageCS';
1211
import { LOG_PREFIX } from './constants';
1312
import { DEBUG, NONE, STORAGE_LOCALSTORAGE } from '../../utils/constants';
1413
import { shouldRecordTelemetry, TelemetryCacheInMemory } from '../inMemory/TelemetryCacheInMemory';
1514
import { UniqueKeysCacheInMemoryCS } from '../inMemory/UniqueKeysCacheInMemoryCS';
1615
import { getMatching } from '../../utils/key';
16+
import { validateCache } from './validateCache';
1717

1818
export interface InLocalStorageOptions {
1919
prefix?: string
@@ -37,9 +37,8 @@ export function InLocalStorage(options: InLocalStorageOptions = {}): IStorageSyn
3737
const { settings, settings: { log, scheduler: { impressionsQueueSize, eventsQueueSize, }, sync: { impressionsMode } } } = params;
3838
const matchingKey = getMatching(settings.core.key);
3939
const keys = new KeyBuilderCS(prefix, matchingKey);
40-
const expirationTimestamp = Date.now() - DEFAULT_CACHE_EXPIRATION_IN_MILLIS;
4140

42-
const splits = new SplitsCacheInLocal(settings, keys, expirationTimestamp);
41+
const splits = new SplitsCacheInLocal(settings, keys);
4342
const segments = new MySegmentsCacheInLocal(log, keys);
4443
const largeSegments = new MySegmentsCacheInLocal(log, myLargeSegmentsKeyBuilder(prefix, matchingKey));
4544

@@ -53,9 +52,8 @@ export function InLocalStorage(options: InLocalStorageOptions = {}): IStorageSyn
5352
telemetry: shouldRecordTelemetry(params) ? new TelemetryCacheInMemory(splits, segments) : undefined,
5453
uniqueKeys: impressionsMode === NONE ? new UniqueKeysCacheInMemoryCS() : undefined,
5554

56-
// @TODO implement
5755
validateCache() {
58-
return splits.getChangeNumber() > -1;
56+
return validateCache(settings, keys, splits);
5957
},
6058

6159
destroy() { },
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { ISettings } from '../../types';
2+
import { DEFAULT_CACHE_EXPIRATION_IN_MILLIS } from '../../utils/constants/browser';
3+
import { isNaNNumber } from '../../utils/lang';
4+
import { getStorageHash } from '../KeyBuilder';
5+
import { LOG_PREFIX } from './constants';
6+
import type { SplitsCacheInLocal } from './SplitsCacheInLocal';
7+
import { KeyBuilderCS } from '../KeyBuilderCS';
8+
9+
function validateExpiration(settings: ISettings, keys: KeyBuilderCS) {
10+
const { log } = settings;
11+
12+
// Check expiration
13+
const expirationTimestamp = Date.now() - DEFAULT_CACHE_EXPIRATION_IN_MILLIS;
14+
let value: string | number | null = localStorage.getItem(keys.buildLastUpdatedKey());
15+
if (value !== null) {
16+
value = parseInt(value, 10);
17+
if (!isNaNNumber(value) && value < expirationTimestamp) return true;
18+
}
19+
20+
// Check hash
21+
const storageHashKey = keys.buildHashKey();
22+
const storageHash = localStorage.getItem(storageHashKey);
23+
const currentStorageHash = getStorageHash(settings);
24+
25+
if (storageHash !== currentStorageHash) {
26+
log.info(LOG_PREFIX + 'SDK key, flags filter criteria or flags spec version was modified. Updating cache');
27+
try {
28+
localStorage.setItem(storageHashKey, currentStorageHash);
29+
} catch (e) {
30+
log.error(LOG_PREFIX + e);
31+
}
32+
return true;
33+
}
34+
}
35+
36+
/**
37+
* Clean cache if:
38+
* - it has expired, i.e., its `lastUpdated` timestamp is older than the given `expirationTimestamp`
39+
* - hash has changed, i.e., the SDK key, flags filter criteria or flags spec version was modified
40+
*/
41+
export function validateCache(settings: ISettings, keys: KeyBuilderCS, splits: SplitsCacheInLocal): boolean {
42+
43+
if (validateExpiration(settings, keys)) {
44+
splits.clear();
45+
}
46+
47+
// Check if the cache is ready
48+
return splits.getChangeNumber() > -1;
49+
}

src/storages/pluggable/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,8 @@ export function PluggableStorage(options: PluggableStorageOptions): IStorageAsyn
9292
// Connects to wrapper and emits SDK_READY event on main client
9393
const connectPromise = wrapper.connect().then(() => {
9494
if (isSyncronizer) {
95-
// In standalone or producer mode, clear storage if SDK key or feature flag filter has changed
95+
// @TODO reuse InLocalStorage::validateCache logic
96+
// In standalone or producer mode, clear storage if SDK key, flags filter criteria or flags spec version was modified
9697
return wrapper.get(keys.buildHashKey()).then((hash) => {
9798
const currentHash = getStorageHash(settings);
9899
if (hash !== currentHash) {

0 commit comments

Comments
 (0)