Skip to content

Commit

Permalink
Move validateCache logic outside SplitsCacheInLocal
Browse files Browse the repository at this point in the history
  • Loading branch information
EmilianoSanchez committed Dec 18, 2024
1 parent e13f819 commit b32e3ee
Show file tree
Hide file tree
Showing 5 changed files with 48 additions and 51 deletions.
38 changes: 0 additions & 38 deletions src/storages/inLocalStorage/SplitsCacheInLocal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,7 @@ import { KeyBuilderCS } from '../KeyBuilderCS';
import { ILogger } from '../../logger/types';
import { LOG_PREFIX } from './constants';
import { ISettings } from '../../types';
import { getStorageHash } from '../KeyBuilder';
import { setToArray } from '../../utils/lang/sets';
import { DEFAULT_CACHE_EXPIRATION_IN_MILLIS } from '../../utils/constants/browser';

/**
* ISplitsCacheSync implementation that stores split definitions in browser LocalStorage.
Expand All @@ -26,42 +24,6 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync {
this.flagSetsFilter = settings.sync.__splitFiltersValidation.groupedFilters.bySet;
}

/**
* Clean Splits cache if:
* - it has expired, i.e., its `lastUpdated` timestamp is older than the given `expirationTimestamp`
* - hash has changes, i.e., the SDK key, flags filter criteria or flags spec version was modified
*/
public validateCache(settings: ISettings) {
// _checkExpiration
const expirationTimestamp = Date.now() - DEFAULT_CACHE_EXPIRATION_IN_MILLIS;
let value: string | number | null = localStorage.getItem(this.keys.buildLastUpdatedKey());
if (value !== null) {
value = parseInt(value, 10);
if (!isNaNNumber(value) && value < expirationTimestamp) this.clear();
}

// @TODO eventually remove `_checkFilterQuery`. Cache should be cleared at the storage level, reusing same logic than PluggableStorage
// _checkFilterQuery
const storageHashKey = this.keys.buildHashKey();
const storageHash = localStorage.getItem(storageHashKey);
const currentStorageHash = getStorageHash(settings);

if (storageHash !== currentStorageHash) {
this.log.info(LOG_PREFIX + 'SDK key, flags filter criteria or flags spec version was modified. Updating cache');
try {
// if there is cache, clear it
if (this.getChangeNumber() > -1) this.clear();

localStorage.setItem(storageHashKey, currentStorageHash);
} catch (e) {
this.log.error(LOG_PREFIX + e);
}
}
// if the filter didn't change, nothing is done

return this.getChangeNumber() > -1;
}

private _decrementCount(key: string) {
const count = toNumber(localStorage.getItem(key)) - 1;
// @ts-expect-error
Expand Down
11 changes: 1 addition & 10 deletions src/storages/inLocalStorage/__tests__/SplitsCacheInLocal.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import { fullSettings } from '../../../utils/settingsValidation/__tests__/settin

test('SPLIT CACHE / LocalStorage', () => {
const cache = new SplitsCacheInLocal(fullSettings, new KeyBuilderCS('SPLITIO', 'user'));
cache.validateCache(fullSettings);

cache.clear();

Expand Down Expand Up @@ -41,7 +40,6 @@ test('SPLIT CACHE / LocalStorage', () => {

test('SPLIT CACHE / LocalStorage / Get Keys', () => {
const cache = new SplitsCacheInLocal(fullSettings, new KeyBuilderCS('SPLITIO', 'user'));
cache.validateCache(fullSettings);

cache.addSplit('lol1', something);
cache.addSplit('lol2', somethingElse);
Expand All @@ -54,7 +52,6 @@ test('SPLIT CACHE / LocalStorage / Get Keys', () => {

test('SPLIT CACHE / LocalStorage / Add Splits', () => {
const cache = new SplitsCacheInLocal(fullSettings, new KeyBuilderCS('SPLITIO', 'user'));
cache.validateCache(fullSettings);

cache.addSplits([
['lol1', something],
Expand All @@ -69,7 +66,6 @@ test('SPLIT CACHE / LocalStorage / Add Splits', () => {

test('SPLIT CACHE / LocalStorage / trafficTypeExists and ttcache tests', () => {
const cache = new SplitsCacheInLocal(fullSettings, new KeyBuilderCS('SPLITIO', 'user'));
cache.validateCache(fullSettings);

cache.addSplits([ // loop of addSplit
['split1', splitWithUserTT],
Expand Down Expand Up @@ -108,7 +104,6 @@ test('SPLIT CACHE / LocalStorage / trafficTypeExists and ttcache tests', () => {

test('SPLIT CACHE / LocalStorage / killLocally', () => {
const cache = new SplitsCacheInLocal(fullSettings, new KeyBuilderCS('SPLITIO', 'user'));
cache.validateCache(fullSettings);

cache.addSplit('lol1', something);
cache.addSplit('lol2', somethingElse);
Expand Down Expand Up @@ -142,7 +137,6 @@ test('SPLIT CACHE / LocalStorage / killLocally', () => {

test('SPLIT CACHE / LocalStorage / usesSegments', () => {
const cache = new SplitsCacheInLocal(fullSettings, new KeyBuilderCS('SPLITIO', 'user'));
cache.validateCache(fullSettings);

expect(cache.usesSegments()).toBe(true); // true initially, until data is synchronized
cache.setChangeNumber(1); // to indicate that data has been synced.
Expand Down Expand Up @@ -174,7 +168,6 @@ test('SPLIT CACHE / LocalStorage / flag set cache tests', () => {
}
}
}, new KeyBuilderCS('SPLITIO', 'user'));
cache.validateCache(fullSettings);

const emptySet = new Set([]);

Expand Down Expand Up @@ -216,7 +209,6 @@ test('SPLIT CACHE / LocalStorage / flag set cache tests', () => {
// if FlagSets are not defined, it should store all FlagSets in memory.
test('SPLIT CACHE / LocalStorage / flag set cache tests without filters', () => {
const cache = new SplitsCacheInLocal(fullSettings, new KeyBuilderCS('SPLITIO', 'user'));
cache.validateCache(fullSettings);

const emptySet = new Set([]);

Expand All @@ -236,6 +228,5 @@ test('SPLIT CACHE / LocalStorage / flag set cache tests without filters', () =>

// Validate that the feature flag cache is cleared when calling `clear` method
cache.clear();
expect(localStorage.length).toBe(1); // only 'SPLITIO.hash' should remain in localStorage
expect(localStorage.key(0)).toBe('SPLITIO.hash');
expect(localStorage.length).toBe(0);
});
4 changes: 2 additions & 2 deletions src/storages/inLocalStorage/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { DEBUG, NONE, STORAGE_LOCALSTORAGE } from '../../utils/constants';
import { shouldRecordTelemetry, TelemetryCacheInMemory } from '../inMemory/TelemetryCacheInMemory';
import { UniqueKeysCacheInMemoryCS } from '../inMemory/UniqueKeysCacheInMemoryCS';
import { getMatching } from '../../utils/key';
import { validateCache } from './validateCache';

export interface InLocalStorageOptions {
prefix?: string
Expand Down Expand Up @@ -51,9 +52,8 @@ export function InLocalStorage(options: InLocalStorageOptions = {}): IStorageSyn
telemetry: shouldRecordTelemetry(params) ? new TelemetryCacheInMemory(splits, segments) : undefined,
uniqueKeys: impressionsMode === NONE ? new UniqueKeysCacheInMemoryCS() : undefined,

// @TODO implement
validateCache() {
return splits.validateCache(settings);
return validateCache(settings, keys, splits);
},

destroy() { },
Expand Down
43 changes: 43 additions & 0 deletions src/storages/inLocalStorage/validateCache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { ISettings } from '../../types';
import { DEFAULT_CACHE_EXPIRATION_IN_MILLIS } from '../../utils/constants/browser';
import { isNaNNumber } from '../../utils/lang';
import { getStorageHash } from '../KeyBuilder';
import { LOG_PREFIX } from './constants';
import type { SplitsCacheInLocal } from './SplitsCacheInLocal';
import { KeyBuilderCS } from '../KeyBuilderCS';

/**
* Clean cache if:
* - it has expired, i.e., its `lastUpdated` timestamp is older than the given `expirationTimestamp`
* - hash has changed, i.e., the SDK key, flags filter criteria or flags spec version was modified
*/
export function validateCache(settings: ISettings, keys: KeyBuilderCS, splits: SplitsCacheInLocal): boolean {
const { log } = settings;

// Check expiration and clear cache if needed
const expirationTimestamp = Date.now() - DEFAULT_CACHE_EXPIRATION_IN_MILLIS;
let value: string | number | null = localStorage.getItem(keys.buildLastUpdatedKey());
if (value !== null) {
value = parseInt(value, 10);
if (!isNaNNumber(value) && value < expirationTimestamp) splits.clear();
}

// Check hash and clear cache if needed
const storageHashKey = keys.buildHashKey();
const storageHash = localStorage.getItem(storageHashKey);
const currentStorageHash = getStorageHash(settings);

if (storageHash !== currentStorageHash) {
log.info(LOG_PREFIX + 'SDK key, flags filter criteria or flags spec version was modified. Updating cache');
try {
if (splits.getChangeNumber() > -1) splits.clear();

localStorage.setItem(storageHashKey, currentStorageHash);
} catch (e) {
log.error(LOG_PREFIX + e);
}
}

// Check if the cache is ready
return splits.getChangeNumber() > -1;
}
3 changes: 2 additions & 1 deletion src/storages/pluggable/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,8 @@ export function PluggableStorage(options: PluggableStorageOptions): IStorageAsyn
// Connects to wrapper and emits SDK_READY event on main client
const connectPromise = wrapper.connect().then(() => {
if (isSyncronizer) {
// In standalone or producer mode, clear storage if SDK key or feature flag filter has changed
// @TODO reuse InLocalStorage::validateCache logic
// In standalone or producer mode, clear storage if SDK key, flags filter criteria or flags spec version was modified
return wrapper.get(keys.buildHashKey()).then((hash) => {
const currentHash = getStorageHash(settings);
if (hash !== currentHash) {
Expand Down

0 comments on commit b32e3ee

Please sign in to comment.