Skip to content

Commit ac8d76b

Browse files
Merge pull request #383 from splitio/cache_expiration_baseline
[Cache expiration]
2 parents 81fbc9a + 65dd894 commit ac8d76b

File tree

22 files changed

+343
-146
lines changed

22 files changed

+343
-146
lines changed

CHANGES.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
2.2.0 (March 28, 2025)
22
- Added new optional argument to the client `getTreatment` methods to allow passing additional evaluation options, such as a map of properties to append to the generated impression object sent to Split backend.
3+
- Added two new configuration options for the SDK storage in browsers when using storage type `LOCALSTORAGE`:
4+
- `storage.expirationDays` to specify the validity period of the rollout cache.
5+
- `storage.clearOnInit` to clear the rollout cache on SDK initialization.
6+
- Updated SDK_READY_FROM_CACHE event when using the `LOCALSTORAGE` storage type to be emitted alongside the SDK_READY event if it has not already been emitted.
37

48
2.1.0 (January 17, 2025)
59
- Added support for the new impressions tracking toggle available on feature flags, both respecting the setting and including the new field being returned on `SplitView` type objects. Read more in our docs.

src/readiness/__tests__/readinessManager.spec.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,14 @@ import { EventEmitter } from '../../utils/MinEvents';
33
import { IReadinessManager } from '../types';
44
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';
55
import { ISettings } from '../../types';
6+
import { STORAGE_LOCALSTORAGE } from '../../utils/constants';
67

78
const settings = {
89
startup: {
910
readyTimeout: 0,
11+
},
12+
storage: {
13+
type: STORAGE_LOCALSTORAGE
1014
}
1115
} as unknown as ISettings;
1216

@@ -67,7 +71,14 @@ test('READINESS MANAGER / Ready event should be fired once', () => {
6771
const readinessManager = readinessManagerFactory(EventEmitter, settings);
6872
let counter = 0;
6973

74+
readinessManager.gate.on(SDK_READY_FROM_CACHE, () => {
75+
expect(readinessManager.isReadyFromCache()).toBe(true);
76+
expect(readinessManager.isReady()).toBe(true);
77+
counter++;
78+
});
79+
7080
readinessManager.gate.on(SDK_READY, () => {
81+
expect(readinessManager.isReadyFromCache()).toBe(true);
7182
expect(readinessManager.isReady()).toBe(true);
7283
counter++;
7384
});
@@ -79,7 +90,7 @@ test('READINESS MANAGER / Ready event should be fired once', () => {
7990
readinessManager.splits.emit(SDK_SPLITS_ARRIVED);
8091
readinessManager.segments.emit(SDK_SEGMENTS_ARRIVED);
8192

82-
expect(counter).toBe(1); // should be called once
93+
expect(counter).toBe(2); // should be called once
8394
});
8495

8596
test('READINESS MANAGER / Ready from cache event should be fired once', (done) => {
@@ -88,6 +99,7 @@ test('READINESS MANAGER / Ready from cache event should be fired once', (done) =
8899

89100
readinessManager.gate.on(SDK_READY_FROM_CACHE, () => {
90101
expect(readinessManager.isReadyFromCache()).toBe(true);
102+
expect(readinessManager.isReady()).toBe(false);
91103
counter++;
92104
});
93105

src/readiness/readinessManager.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { ISettings } from '../types';
33
import SplitIO from '../../types/splitio';
44
import { SDK_SPLITS_ARRIVED, SDK_SPLITS_CACHE_LOADED, SDK_SEGMENTS_ARRIVED, SDK_READY_TIMED_OUT, SDK_READY_FROM_CACHE, SDK_UPDATE, SDK_READY } from './constants';
55
import { IReadinessEventEmitter, IReadinessManager, ISegmentsEventEmitter, ISplitsEventEmitter } from './types';
6+
import { STORAGE_LOCALSTORAGE } from '../utils/constants';
67

78
function splitsEventEmitterFactory(EventEmitter: new () => SplitIO.IEventEmitter): ISplitsEventEmitter {
89
const splitsEventEmitter = objectAssign(new EventEmitter(), {
@@ -114,6 +115,10 @@ export function readinessManagerFactory(
114115
isReady = true;
115116
try {
116117
syncLastUpdate();
118+
if (!isReadyFromCache && settings.storage?.type === STORAGE_LOCALSTORAGE) {
119+
isReadyFromCache = true;
120+
gate.emit(SDK_READY_FROM_CACHE);
121+
}
117122
gate.emit(SDK_READY);
118123
} catch (e) {
119124
// throws user callback exceptions in next tick

src/storages/AbstractSplitsCacheAsync.ts

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,6 @@ export abstract class AbstractSplitsCacheAsync implements ISplitsCacheAsync {
3737
return Promise.resolve(true);
3838
}
3939

40-
/**
41-
* Check if the splits information is already stored in cache.
42-
* Noop, just keeping the interface. This is used by client-side implementations only.
43-
*/
44-
checkCache(): Promise<boolean> {
45-
return Promise.resolve(false);
46-
}
47-
4840
/**
4941
* Kill `name` split and set `defaultTreatment` and `changeNumber`.
5042
* Used for SPLIT_KILL push notifications.

src/storages/AbstractSplitsCacheSync.ts

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -43,14 +43,6 @@ export abstract class AbstractSplitsCacheSync implements ISplitsCacheSync {
4343

4444
abstract clear(): void
4545

46-
/**
47-
* Check if the splits information is already stored in cache. This data can be preloaded.
48-
* It is used as condition to emit SDK_SPLITS_CACHE_LOADED, and then SDK_READY_FROM_CACHE.
49-
*/
50-
checkCache(): boolean {
51-
return false;
52-
}
53-
5446
/**
5547
* Kill `name` split and set `defaultTreatment` and `changeNumber`.
5648
* Used for SPLIT_KILL push notifications.

src/storages/KeyBuilderCS.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,10 @@ export class KeyBuilderCS extends KeyBuilder implements MySegmentsKeyBuilder {
5050
buildSplitsWithSegmentCountKey() {
5151
return `${this.prefix}.splits.usingSegments`;
5252
}
53+
54+
buildLastClear() {
55+
return `${this.prefix}.lastClear`;
56+
}
5357
}
5458

5559
export function myLargeSegmentsKeyBuilder(prefix: string, matchingKey: string): MySegmentsKeyBuilder {

src/storages/dataLoader.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { PreloadedData } from '../types';
2-
import { DEFAULT_CACHE_EXPIRATION_IN_MILLIS } from '../utils/constants/browser';
32
import { DataLoader, ISegmentsCacheSync, ISplitsCacheSync } from './types';
43

4+
// This value might be eventually set via a config parameter
5+
const DEFAULT_CACHE_EXPIRATION_IN_MILLIS = 864000000; // 10 days
6+
57
/**
68
* Factory of client-side storage loader
79
*

src/storages/inLocalStorage/SplitsCacheInLocal.ts

Lines changed: 1 addition & 66 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) {
@@ -79,8 +71,6 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync {
7971
* We cannot simply call `localStorage.clear()` since that implies removing user items from the storage.
8072
*/
8173
clear() {
82-
this.log.info(LOG_PREFIX + 'Flushing Splits data from localStorage');
83-
8474
// collect item keys
8575
const len = localStorage.length;
8676
const accum = [];
@@ -141,19 +131,6 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync {
141131
}
142132

143133
setChangeNumber(changeNumber: number): boolean {
144-
145-
// when using a new split query, we must update it at the store
146-
if (this.updateNewFilter) {
147-
this.log.info(LOG_PREFIX + 'SDK key, flags filter criteria or flags spec version was modified. Updating cache');
148-
const storageHashKey = this.keys.buildHashKey();
149-
try {
150-
localStorage.setItem(storageHashKey, this.storageHash);
151-
} catch (e) {
152-
this.log.error(LOG_PREFIX + e);
153-
}
154-
this.updateNewFilter = false;
155-
}
156-
157134
try {
158135
localStorage.setItem(this.keys.buildSplitsTillKey(), changeNumber + '');
159136
// update "last updated" timestamp with current time
@@ -215,48 +192,6 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync {
215192
}
216193
}
217194

218-
/**
219-
* Check if the splits information is already stored in browser LocalStorage.
220-
* In this function we could add more code to check if the data is valid.
221-
* @override
222-
*/
223-
checkCache(): boolean {
224-
return this.getChangeNumber() > -1;
225-
}
226-
227-
/**
228-
* Clean Splits cache if its `lastUpdated` timestamp is older than the given `expirationTimestamp`,
229-
*
230-
* @param expirationTimestamp - if the value is not a number, data will not be cleaned
231-
*/
232-
private _checkExpiration(expirationTimestamp?: number) {
233-
let value: string | number | null = localStorage.getItem(this.keys.buildLastUpdatedKey());
234-
if (value !== null) {
235-
value = parseInt(value, 10);
236-
if (!isNaNNumber(value) && expirationTimestamp && value < expirationTimestamp) this.clear();
237-
}
238-
}
239-
240-
// @TODO eventually remove `_checkFilterQuery`. Cache should be cleared at the storage level, reusing same logic than PluggableStorage
241-
private _checkFilterQuery() {
242-
const storageHashKey = this.keys.buildHashKey();
243-
const storageHash = localStorage.getItem(storageHashKey);
244-
245-
if (storageHash !== this.storageHash) {
246-
try {
247-
// mark cache to update the new query filter on first successful splits fetch
248-
this.updateNewFilter = true;
249-
250-
// if there is cache, clear it
251-
if (this.checkCache()) this.clear();
252-
253-
} catch (e) {
254-
this.log.error(LOG_PREFIX + e);
255-
}
256-
}
257-
// if the filter didn't change, nothing is done
258-
}
259-
260195
getNamesByFlagSets(flagSets: string[]): Set<string>[] {
261196
return flagSets.map(flagSet => {
262197
const flagSetKey = this.keys.buildFlagSetKey(flagSet);

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

Lines changed: 14 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -29,18 +29,11 @@ test('SPLITS CACHE / LocalStorage', () => {
2929
expect(cache.getSplit(something.name)).toEqual(null);
3030
expect(cache.getSplit(somethingElse.name)).toEqual(somethingElse);
3131

32-
expect(cache.checkCache()).toBe(false); // checkCache should return false until localstorage has data.
33-
3432
expect(cache.getChangeNumber()).toBe(-1);
3533

36-
expect(cache.checkCache()).toBe(false); // checkCache should return false until localstorage has data.
37-
3834
cache.setChangeNumber(123);
3935

40-
expect(cache.checkCache()).toBe(true); // checkCache should return true once localstorage has data.
41-
4236
expect(cache.getChangeNumber()).toBe(123);
43-
4437
});
4538

4639
test('SPLITS CACHE / LocalStorage / Get Keys', () => {
@@ -106,6 +99,7 @@ test('SPLITS CACHE / LocalStorage / trafficTypeExists and ttcache tests', () =>
10699

107100
test('SPLITS CACHE / LocalStorage / killLocally', () => {
108101
const cache = new SplitsCacheInLocal(fullSettings, new KeyBuilderCS('SPLITIO', 'user'));
102+
109103
cache.addSplit(something);
110104
cache.addSplit(somethingElse);
111105
const initialChangeNumber = cache.getChangeNumber();
@@ -169,6 +163,7 @@ test('SPLITS CACHE / LocalStorage / flag set cache tests', () => {
169163
}
170164
}
171165
}, new KeyBuilderCS('SPLITIO', 'user'));
166+
172167
const emptySet = new Set([]);
173168

174169
cache.update([
@@ -208,25 +203,25 @@ test('SPLITS CACHE / LocalStorage / flag set cache tests', () => {
208203

209204
// if FlagSets are not defined, it should store all FlagSets in memory.
210205
test('SPLIT CACHE / LocalStorage / flag set cache tests without filters', () => {
211-
const cacheWithoutFilters = new SplitsCacheInLocal(fullSettings, new KeyBuilderCS('SPLITIO', 'user'));
206+
const cache = new SplitsCacheInLocal(fullSettings, new KeyBuilderCS('SPLITIO', 'user'));
207+
212208
const emptySet = new Set([]);
213209

214-
cacheWithoutFilters.update([
210+
cache.update([
215211
featureFlagOne,
216212
featureFlagTwo,
217213
featureFlagThree,
218214
], [], -1);
219-
cacheWithoutFilters.addSplit(featureFlagWithEmptyFS);
215+
cache.addSplit(featureFlagWithEmptyFS);
220216

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

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

0 commit comments

Comments
 (0)