From 9b8d36af77c55ead5cfafd835d25aae8a3ea4b16 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 18 Dec 2024 15:48:49 -0300 Subject: [PATCH 01/10] Clear segments and largeSegments caches --- src/storages/inLocalStorage/index.ts | 9 +++------ src/storages/inLocalStorage/validateCache.ts | 5 ++++- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/storages/inLocalStorage/index.ts b/src/storages/inLocalStorage/index.ts index cb14a235..f32cc014 100644 --- a/src/storages/inLocalStorage/index.ts +++ b/src/storages/inLocalStorage/index.ts @@ -14,15 +14,12 @@ import { shouldRecordTelemetry, TelemetryCacheInMemory } from '../inMemory/Telem import { UniqueKeysCacheInMemoryCS } from '../inMemory/UniqueKeysCacheInMemoryCS'; import { getMatching } from '../../utils/key'; import { validateCache } from './validateCache'; - -export interface InLocalStorageOptions { - prefix?: string -} +import SplitIO from '../../../types/splitio'; /** * InLocal storage factory for standalone client-side SplitFactory */ -export function InLocalStorage(options: InLocalStorageOptions = {}): IStorageSyncFactory { +export function InLocalStorage(options: SplitIO.InLocalStorageOptions = {}): IStorageSyncFactory { const prefix = validatePrefix(options.prefix); @@ -53,7 +50,7 @@ export function InLocalStorage(options: InLocalStorageOptions = {}): IStorageSyn uniqueKeys: impressionsMode === NONE ? new UniqueKeysCacheInMemoryCS() : undefined, validateCache() { - return validateCache(settings, keys, splits); + return validateCache(settings, keys, splits, segments, largeSegments); }, destroy() { }, diff --git a/src/storages/inLocalStorage/validateCache.ts b/src/storages/inLocalStorage/validateCache.ts index 331a8df9..f76b77ca 100644 --- a/src/storages/inLocalStorage/validateCache.ts +++ b/src/storages/inLocalStorage/validateCache.ts @@ -4,6 +4,7 @@ import { isNaNNumber } from '../../utils/lang'; import { getStorageHash } from '../KeyBuilder'; import { LOG_PREFIX } from './constants'; import type { SplitsCacheInLocal } from './SplitsCacheInLocal'; +import type { MySegmentsCacheInLocal } from './MySegmentsCacheInLocal'; import { KeyBuilderCS } from '../KeyBuilderCS'; function validateExpiration(settings: ISettings, keys: KeyBuilderCS) { @@ -38,10 +39,12 @@ function validateExpiration(settings: ISettings, keys: KeyBuilderCS) { * - 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 { +export function validateCache(settings: ISettings, keys: KeyBuilderCS, splits: SplitsCacheInLocal, segments: MySegmentsCacheInLocal, largeSegments: MySegmentsCacheInLocal): boolean { if (validateExpiration(settings, keys)) { splits.clear(); + segments.clear(); + largeSegments.clear(); } // Check if the cache is ready From 87fbc4f6fc77791e3868152674c80c14667975a4 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 18 Dec 2024 16:20:16 -0300 Subject: [PATCH 02/10] expirationDays configuration --- src/storages/dataLoader.ts | 4 ++- src/storages/inLocalStorage/index.ts | 2 +- src/storages/inLocalStorage/validateCache.ts | 23 +++++++++++------ src/utils/constants/browser.ts | 2 -- src/utils/lang/index.ts | 2 +- types/splitio.d.ts | 26 +++++++++++++++++++- 6 files changed, 45 insertions(+), 14 deletions(-) delete mode 100644 src/utils/constants/browser.ts diff --git a/src/storages/dataLoader.ts b/src/storages/dataLoader.ts index ce288868..55535cfd 100644 --- a/src/storages/dataLoader.ts +++ b/src/storages/dataLoader.ts @@ -1,7 +1,9 @@ import { PreloadedData } from '../types'; -import { DEFAULT_CACHE_EXPIRATION_IN_MILLIS } from '../utils/constants/browser'; import { DataLoader, ISegmentsCacheSync, ISplitsCacheSync } from './types'; +// This value might be eventually set via a config parameter +const DEFAULT_CACHE_EXPIRATION_IN_MILLIS = 864000000; // 10 days + /** * Factory of client-side storage loader * diff --git a/src/storages/inLocalStorage/index.ts b/src/storages/inLocalStorage/index.ts index f32cc014..bb01bd7d 100644 --- a/src/storages/inLocalStorage/index.ts +++ b/src/storages/inLocalStorage/index.ts @@ -50,7 +50,7 @@ export function InLocalStorage(options: SplitIO.InLocalStorageOptions = {}): ISt uniqueKeys: impressionsMode === NONE ? new UniqueKeysCacheInMemoryCS() : undefined, validateCache() { - return validateCache(settings, keys, splits, segments, largeSegments); + return validateCache(options, settings, keys, splits, segments, largeSegments); }, destroy() { }, diff --git a/src/storages/inLocalStorage/validateCache.ts b/src/storages/inLocalStorage/validateCache.ts index f76b77ca..d2da969a 100644 --- a/src/storages/inLocalStorage/validateCache.ts +++ b/src/storages/inLocalStorage/validateCache.ts @@ -1,21 +1,28 @@ import { ISettings } from '../../types'; -import { DEFAULT_CACHE_EXPIRATION_IN_MILLIS } from '../../utils/constants/browser'; -import { isNaNNumber } from '../../utils/lang'; +import { isFiniteNumber, isNaNNumber } from '../../utils/lang'; import { getStorageHash } from '../KeyBuilder'; import { LOG_PREFIX } from './constants'; import type { SplitsCacheInLocal } from './SplitsCacheInLocal'; import type { MySegmentsCacheInLocal } from './MySegmentsCacheInLocal'; import { KeyBuilderCS } from '../KeyBuilderCS'; +import SplitIO from '../../../types/splitio'; -function validateExpiration(settings: ISettings, keys: KeyBuilderCS) { +// milliseconds in a day +const DEFAULT_CACHE_EXPIRATION_IN_DAYS = 10; +const MILLIS_IN_A_DAY = 86400000; + +function validateExpiration(options: SplitIO.InLocalStorageOptions, settings: ISettings, keys: KeyBuilderCS) { const { log } = settings; // Check expiration - const expirationTimestamp = Date.now() - DEFAULT_CACHE_EXPIRATION_IN_MILLIS; + const expirationTimestamp = Date.now() - MILLIS_IN_A_DAY * (isFiniteNumber(options.expirationDays) && options.expirationDays >= 1 ? options.expirationDays : DEFAULT_CACHE_EXPIRATION_IN_DAYS); let value: string | number | null = localStorage.getItem(keys.buildLastUpdatedKey()); if (value !== null) { value = parseInt(value, 10); - if (!isNaNNumber(value) && value < expirationTimestamp) return true; + if (!isNaNNumber(value) && value < expirationTimestamp) { + log.info(LOG_PREFIX + 'Cache expired. Cleaning up cache'); + return true; + } } // Check hash @@ -24,7 +31,7 @@ function validateExpiration(settings: ISettings, keys: KeyBuilderCS) { const currentStorageHash = getStorageHash(settings); if (storageHash !== currentStorageHash) { - log.info(LOG_PREFIX + 'SDK key, flags filter criteria or flags spec version was modified. Updating cache'); + log.info(LOG_PREFIX + 'SDK key, flags filter criteria or flags spec version was modified. Cleaning up cache'); try { localStorage.setItem(storageHashKey, currentStorageHash); } catch (e) { @@ -39,9 +46,9 @@ function validateExpiration(settings: ISettings, keys: KeyBuilderCS) { * - 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, segments: MySegmentsCacheInLocal, largeSegments: MySegmentsCacheInLocal): boolean { +export function validateCache(options: SplitIO.InLocalStorageOptions, settings: ISettings, keys: KeyBuilderCS, splits: SplitsCacheInLocal, segments: MySegmentsCacheInLocal, largeSegments: MySegmentsCacheInLocal): boolean { - if (validateExpiration(settings, keys)) { + if (validateExpiration(options, settings, keys)) { splits.clear(); segments.clear(); largeSegments.clear(); diff --git a/src/utils/constants/browser.ts b/src/utils/constants/browser.ts deleted file mode 100644 index d627f780..00000000 --- a/src/utils/constants/browser.ts +++ /dev/null @@ -1,2 +0,0 @@ -// This value might be eventually set via a config parameter -export const DEFAULT_CACHE_EXPIRATION_IN_MILLIS = 864000000; // 10 days diff --git a/src/utils/lang/index.ts b/src/utils/lang/index.ts index 11b6afd0..b1a7e35a 100644 --- a/src/utils/lang/index.ts +++ b/src/utils/lang/index.ts @@ -120,7 +120,7 @@ export function isBoolean(val: any): boolean { * Unlike `Number.isFinite`, it also tests Number object instances. * Unlike global `isFinite`, it returns false if the value is not a number or Number object instance. */ -export function isFiniteNumber(val: any): boolean { +export function isFiniteNumber(val: any): val is number { if (val instanceof Number) val = val.valueOf(); return typeof val === 'number' ? Number.isFinite ? Number.isFinite(val) : isFinite(val) : diff --git a/types/splitio.d.ts b/types/splitio.d.ts index bb108c1c..4aa59db3 100644 --- a/types/splitio.d.ts +++ b/types/splitio.d.ts @@ -906,6 +906,18 @@ declare namespace SplitIO { * @defaultValue `'SPLITIO'` */ prefix?: string; + /** + * Number of days before cached data expires if it was not updated. If cache expires, it is cleared on initialization. + * + * @defaultValue `10` + */ + expirationDays?: number; + /** + * Optional settings to clear the cache. If set to `true`, the SDK clears the cached data on initialization, unless the cache was cleared within the last 24 hours. + * + * @defaultValue `false` + */ + clearOnInit?: boolean; } /** * Storage for asynchronous (consumer) SDK. @@ -1229,11 +1241,23 @@ declare namespace SplitIO { */ type?: BrowserStorage; /** - * Optional prefix to prevent any kind of data collision between SDK versions. + * Optional prefix to prevent any kind of data collision between SDK versions when using 'LOCALSTORAGE'. * * @defaultValue `'SPLITIO'` */ prefix?: string; + /** + * Optional settings for the 'LOCALSTORAGE' storage type. It specifies the number of days before cached data expires if it was not updated. If cache expires, it is cleared on initialization. + * + * @defaultValue `10` + */ + expirationDays?: number; + /** + * Optional settings for the 'LOCALSTORAGE' storage type. If set to `true`, the SDK clears the cached data on initialization, unless the cache was cleared within the last 24 hours. + * + * @defaultValue `false` + */ + clearOnInit?: boolean; }; } /** From aca35fe3fea2171e532c43b3de79c535cae4731c Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 18 Dec 2024 16:37:47 -0300 Subject: [PATCH 03/10] clearOnInit configuration --- src/storages/KeyBuilderCS.ts | 4 ++++ src/storages/inLocalStorage/validateCache.ts | 21 +++++++++++++++++++- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/storages/KeyBuilderCS.ts b/src/storages/KeyBuilderCS.ts index a59d7208..20372358 100644 --- a/src/storages/KeyBuilderCS.ts +++ b/src/storages/KeyBuilderCS.ts @@ -43,6 +43,10 @@ export class KeyBuilderCS extends KeyBuilder implements MySegmentsKeyBuilder { buildTillKey() { return `${this.prefix}.${this.matchingKey}.segments.till`; } + + buildLastClear() { + return `${this.prefix}.lastClear`; + } } export function myLargeSegmentsKeyBuilder(prefix: string, matchingKey: string): MySegmentsKeyBuilder { diff --git a/src/storages/inLocalStorage/validateCache.ts b/src/storages/inLocalStorage/validateCache.ts index d2da969a..7c1fa31d 100644 --- a/src/storages/inLocalStorage/validateCache.ts +++ b/src/storages/inLocalStorage/validateCache.ts @@ -39,6 +39,18 @@ function validateExpiration(options: SplitIO.InLocalStorageOptions, settings: IS } return true; } + + // Clear on init + if (options.clearOnInit) { + let value: string | number | null = localStorage.getItem(keys.buildLastClear()); + if (value !== null) { + value = parseInt(value, 10); + if (!isNaNNumber(value) && value < Date.now() - MILLIS_IN_A_DAY) { + log.info(LOG_PREFIX + 'Clear on init was set and cache was cleared more than a day ago. Cleaning up cache'); + return true; + } + } + } } /** @@ -52,8 +64,15 @@ export function validateCache(options: SplitIO.InLocalStorageOptions, settings: splits.clear(); segments.clear(); largeSegments.clear(); + + // Update last clear timestamp + try { + localStorage.setItem(keys.buildLastClear(), Date.now() + ''); + } catch (e) { + settings.log.error(LOG_PREFIX + e); + } } - // Check if the cache is ready + // Check if ready from cache return splits.getChangeNumber() > -1; } From 534d6ca8808662c6ad735f323694db49bfc92a1a Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 19 Dec 2024 11:47:33 -0300 Subject: [PATCH 04/10] Reuse Date.now() result --- src/storages/inLocalStorage/validateCache.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/storages/inLocalStorage/validateCache.ts b/src/storages/inLocalStorage/validateCache.ts index 7c1fa31d..d7690828 100644 --- a/src/storages/inLocalStorage/validateCache.ts +++ b/src/storages/inLocalStorage/validateCache.ts @@ -11,16 +11,17 @@ import SplitIO from '../../../types/splitio'; const DEFAULT_CACHE_EXPIRATION_IN_DAYS = 10; const MILLIS_IN_A_DAY = 86400000; -function validateExpiration(options: SplitIO.InLocalStorageOptions, settings: ISettings, keys: KeyBuilderCS) { +function validateExpiration(options: SplitIO.InLocalStorageOptions, settings: ISettings, keys: KeyBuilderCS, currentTimestamp: number) { const { log } = settings; // Check expiration - const expirationTimestamp = Date.now() - MILLIS_IN_A_DAY * (isFiniteNumber(options.expirationDays) && options.expirationDays >= 1 ? options.expirationDays : DEFAULT_CACHE_EXPIRATION_IN_DAYS); + const cacheExpirationInDays = isFiniteNumber(options.expirationDays) && options.expirationDays >= 1 ? options.expirationDays : DEFAULT_CACHE_EXPIRATION_IN_DAYS; + const expirationTimestamp = currentTimestamp - MILLIS_IN_A_DAY * cacheExpirationInDays; let value: string | number | null = localStorage.getItem(keys.buildLastUpdatedKey()); if (value !== null) { value = parseInt(value, 10); if (!isNaNNumber(value) && value < expirationTimestamp) { - log.info(LOG_PREFIX + 'Cache expired. Cleaning up cache'); + log.info(LOG_PREFIX + 'Cache expired more than ' + cacheExpirationInDays + ' days ago. Cleaning up cache'); return true; } } @@ -45,7 +46,7 @@ function validateExpiration(options: SplitIO.InLocalStorageOptions, settings: IS let value: string | number | null = localStorage.getItem(keys.buildLastClear()); if (value !== null) { value = parseInt(value, 10); - if (!isNaNNumber(value) && value < Date.now() - MILLIS_IN_A_DAY) { + if (!isNaNNumber(value) && value < currentTimestamp - MILLIS_IN_A_DAY) { log.info(LOG_PREFIX + 'Clear on init was set and cache was cleared more than a day ago. Cleaning up cache'); return true; } @@ -60,14 +61,16 @@ function validateExpiration(options: SplitIO.InLocalStorageOptions, settings: IS */ export function validateCache(options: SplitIO.InLocalStorageOptions, settings: ISettings, keys: KeyBuilderCS, splits: SplitsCacheInLocal, segments: MySegmentsCacheInLocal, largeSegments: MySegmentsCacheInLocal): boolean { - if (validateExpiration(options, settings, keys)) { + const currentTimestamp = Date.now(); + + if (validateExpiration(options, settings, keys, currentTimestamp)) { splits.clear(); segments.clear(); largeSegments.clear(); // Update last clear timestamp try { - localStorage.setItem(keys.buildLastClear(), Date.now() + ''); + localStorage.setItem(keys.buildLastClear(), currentTimestamp + ''); } catch (e) { settings.log.error(LOG_PREFIX + e); } From 6451cda87ee72fe71f506ca0224c46e8ccf5d372 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 19 Dec 2024 12:48:08 -0300 Subject: [PATCH 05/10] Handle clearOnInit case with older version of the SDK where lastClear item is not available --- src/storages/inLocalStorage/validateCache.ts | 27 ++++++++++---------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/src/storages/inLocalStorage/validateCache.ts b/src/storages/inLocalStorage/validateCache.ts index d7690828..b8676cb7 100644 --- a/src/storages/inLocalStorage/validateCache.ts +++ b/src/storages/inLocalStorage/validateCache.ts @@ -15,12 +15,11 @@ function validateExpiration(options: SplitIO.InLocalStorageOptions, settings: IS const { log } = settings; // Check expiration - const cacheExpirationInDays = isFiniteNumber(options.expirationDays) && options.expirationDays >= 1 ? options.expirationDays : DEFAULT_CACHE_EXPIRATION_IN_DAYS; - const expirationTimestamp = currentTimestamp - MILLIS_IN_A_DAY * cacheExpirationInDays; - let value: string | number | null = localStorage.getItem(keys.buildLastUpdatedKey()); - if (value !== null) { - value = parseInt(value, 10); - if (!isNaNNumber(value) && value < expirationTimestamp) { + const lastUpdatedTimestamp = parseInt(localStorage.getItem(keys.buildLastUpdatedKey()) as string, 10); + if (!isNaNNumber(lastUpdatedTimestamp)) { + const cacheExpirationInDays = isFiniteNumber(options.expirationDays) && options.expirationDays >= 1 ? options.expirationDays : DEFAULT_CACHE_EXPIRATION_IN_DAYS; + const expirationTimestamp = currentTimestamp - MILLIS_IN_A_DAY * cacheExpirationInDays; + if (lastUpdatedTimestamp < expirationTimestamp) { log.info(LOG_PREFIX + 'Cache expired more than ' + cacheExpirationInDays + ' days ago. Cleaning up cache'); return true; } @@ -32,7 +31,7 @@ function validateExpiration(options: SplitIO.InLocalStorageOptions, settings: IS const currentStorageHash = getStorageHash(settings); if (storageHash !== currentStorageHash) { - log.info(LOG_PREFIX + 'SDK key, flags filter criteria or flags spec version was modified. Cleaning up cache'); + log.info(LOG_PREFIX + 'SDK key, flags filter criteria or flags spec version has changed. Cleaning up cache'); try { localStorage.setItem(storageHashKey, currentStorageHash); } catch (e) { @@ -43,13 +42,11 @@ function validateExpiration(options: SplitIO.InLocalStorageOptions, settings: IS // Clear on init if (options.clearOnInit) { - let value: string | number | null = localStorage.getItem(keys.buildLastClear()); - if (value !== null) { - value = parseInt(value, 10); - if (!isNaNNumber(value) && value < currentTimestamp - MILLIS_IN_A_DAY) { - log.info(LOG_PREFIX + 'Clear on init was set and cache was cleared more than a day ago. Cleaning up cache'); - return true; - } + const lastClearTimestamp = parseInt(localStorage.getItem(keys.buildLastClear()) as string, 10); + + if (isNaNNumber(lastClearTimestamp) || lastClearTimestamp < currentTimestamp - MILLIS_IN_A_DAY) { + log.info(LOG_PREFIX + 'clearOnInit was set and cache was not cleared in the last 24 hours. Cleaning up cache'); + return true; } } } @@ -58,6 +55,8 @@ function validateExpiration(options: SplitIO.InLocalStorageOptions, settings: IS * 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 + * + * @returns `true` if cache is ready to be used, `false` otherwise (cache was cleared or there is no cache) */ export function validateCache(options: SplitIO.InLocalStorageOptions, settings: ISettings, keys: KeyBuilderCS, splits: SplitsCacheInLocal, segments: MySegmentsCacheInLocal, largeSegments: MySegmentsCacheInLocal): boolean { From 956c1df61ebec9f4df14dc040970026faae97884 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 19 Dec 2024 13:22:25 -0300 Subject: [PATCH 06/10] Handle no cache: cache should not be clearer --- src/storages/inLocalStorage/validateCache.ts | 24 +++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/src/storages/inLocalStorage/validateCache.ts b/src/storages/inLocalStorage/validateCache.ts index b8676cb7..7c0c8ae9 100644 --- a/src/storages/inLocalStorage/validateCache.ts +++ b/src/storages/inLocalStorage/validateCache.ts @@ -11,7 +11,12 @@ import SplitIO from '../../../types/splitio'; const DEFAULT_CACHE_EXPIRATION_IN_DAYS = 10; const MILLIS_IN_A_DAY = 86400000; -function validateExpiration(options: SplitIO.InLocalStorageOptions, settings: ISettings, keys: KeyBuilderCS, currentTimestamp: number) { +/** + * Validates if cache should be cleared and sets the cache `hash` if needed. + * + * @returns `true` if cache should be cleared, `false` otherwise + */ +function validateExpiration(options: SplitIO.InLocalStorageOptions, settings: ISettings, keys: KeyBuilderCS, currentTimestamp: number, isThereCache: boolean) { const { log } = settings; // Check expiration @@ -31,13 +36,16 @@ function validateExpiration(options: SplitIO.InLocalStorageOptions, settings: IS const currentStorageHash = getStorageHash(settings); if (storageHash !== currentStorageHash) { - log.info(LOG_PREFIX + 'SDK key, flags filter criteria or flags spec version has changed. Cleaning up cache'); try { localStorage.setItem(storageHashKey, currentStorageHash); } catch (e) { log.error(LOG_PREFIX + e); } - return true; + if (isThereCache) { + log.info(LOG_PREFIX + 'SDK key, flags filter criteria or flags spec version has changed. Cleaning up cache'); + return true; + } + return false; // No cache to clear } // Clear on init @@ -54,15 +62,17 @@ function validateExpiration(options: SplitIO.InLocalStorageOptions, settings: IS /** * 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 + * - its hash has changed, i.e., the SDK key, flags filter criteria or flags spec version was modified + * - `clearOnInit` was set and cache was not cleared in the last 24 hours * * @returns `true` if cache is ready to be used, `false` otherwise (cache was cleared or there is no cache) */ export function validateCache(options: SplitIO.InLocalStorageOptions, settings: ISettings, keys: KeyBuilderCS, splits: SplitsCacheInLocal, segments: MySegmentsCacheInLocal, largeSegments: MySegmentsCacheInLocal): boolean { const currentTimestamp = Date.now(); + const isThereCache = splits.getChangeNumber() > -1; - if (validateExpiration(options, settings, keys, currentTimestamp)) { + if (validateExpiration(options, settings, keys, currentTimestamp, isThereCache)) { splits.clear(); segments.clear(); largeSegments.clear(); @@ -73,8 +83,10 @@ export function validateCache(options: SplitIO.InLocalStorageOptions, settings: } catch (e) { settings.log.error(LOG_PREFIX + e); } + + return false; } // Check if ready from cache - return splits.getChangeNumber() > -1; + return isThereCache; } From 28b7fb994217f92c55c3819b810d50e159c1b60b Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 19 Dec 2024 17:24:30 -0300 Subject: [PATCH 07/10] Add unit test --- .../inLocalStorage/SplitsCacheInLocal.ts | 2 - .../__tests__/validateCache.spec.ts | 125 ++++++++++++++++++ 2 files changed, 125 insertions(+), 2 deletions(-) create mode 100644 src/storages/inLocalStorage/__tests__/validateCache.spec.ts diff --git a/src/storages/inLocalStorage/SplitsCacheInLocal.ts b/src/storages/inLocalStorage/SplitsCacheInLocal.ts index 14767d72..7d4c002f 100644 --- a/src/storages/inLocalStorage/SplitsCacheInLocal.ts +++ b/src/storages/inLocalStorage/SplitsCacheInLocal.ts @@ -71,8 +71,6 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync { * We cannot simply call `localStorage.clear()` since that implies removing user items from the storage. */ clear() { - this.log.info(LOG_PREFIX + 'Flushing Splits data from localStorage'); - // collect item keys const len = localStorage.length; const accum = []; diff --git a/src/storages/inLocalStorage/__tests__/validateCache.spec.ts b/src/storages/inLocalStorage/__tests__/validateCache.spec.ts new file mode 100644 index 00000000..a1f1784d --- /dev/null +++ b/src/storages/inLocalStorage/__tests__/validateCache.spec.ts @@ -0,0 +1,125 @@ +import { validateCache } from '../validateCache'; + +import { KeyBuilderCS } from '../../KeyBuilderCS'; +import { fullSettings } from '../../../utils/settingsValidation/__tests__/settings.mocks'; +import { SplitsCacheInLocal } from '../SplitsCacheInLocal'; +import { nearlyEqual } from '../../../__tests__/testUtils'; +import { MySegmentsCacheInLocal } from '../MySegmentsCacheInLocal'; + +const FULL_SETTINGS_HASH = '404832b3'; + +describe('validateCache', () => { + const keys = new KeyBuilderCS('SPLITIO', 'user'); + const logSpy = jest.spyOn(fullSettings.log, 'info'); + const segments = new MySegmentsCacheInLocal(fullSettings.log, keys); + const largeSegments = new MySegmentsCacheInLocal(fullSettings.log, keys); + const splits = new SplitsCacheInLocal(fullSettings, keys); + + jest.spyOn(splits, 'clear'); + jest.spyOn(splits, 'getChangeNumber'); + jest.spyOn(segments, 'clear'); + jest.spyOn(largeSegments, 'clear'); + + beforeEach(() => { + jest.clearAllMocks(); + localStorage.clear(); + }); + + test('if there is no cache, it should return false', () => { + expect(validateCache({}, fullSettings, keys, splits, segments, largeSegments)).toBe(false); + + expect(logSpy).not.toHaveBeenCalled(); + + expect(splits.clear).not.toHaveBeenCalled(); + expect(segments.clear).not.toHaveBeenCalled(); + expect(largeSegments.clear).not.toHaveBeenCalled(); + expect(splits.getChangeNumber).toHaveBeenCalledTimes(1); + + expect(localStorage.getItem(keys.buildHashKey())).toBe(FULL_SETTINGS_HASH); + expect(localStorage.getItem(keys.buildLastClear())).toBeNull(); + }); + + test('if there is cache and it must not be cleared, it should return true', () => { + localStorage.setItem(keys.buildSplitsTillKey(), '1'); + localStorage.setItem(keys.buildHashKey(), FULL_SETTINGS_HASH); + + expect(validateCache({}, fullSettings, keys, splits, segments, largeSegments)).toBe(true); + + expect(logSpy).not.toHaveBeenCalled(); + + expect(splits.clear).not.toHaveBeenCalled(); + expect(segments.clear).not.toHaveBeenCalled(); + expect(largeSegments.clear).not.toHaveBeenCalled(); + expect(splits.getChangeNumber).toHaveBeenCalledTimes(1); + + expect(localStorage.getItem(keys.buildHashKey())).toBe(FULL_SETTINGS_HASH); + expect(localStorage.getItem(keys.buildLastClear())).toBeNull(); + }); + + test('if there is cache and it has expired, it should clear cache and return false', () => { + localStorage.setItem(keys.buildSplitsTillKey(), '1'); + localStorage.setItem(keys.buildHashKey(), FULL_SETTINGS_HASH); + localStorage.setItem(keys.buildLastUpdatedKey(), Date.now() - 1000 * 60 * 60 * 24 * 2 + ''); // 2 days ago + + expect(validateCache({ expirationDays: 1 }, fullSettings, keys, splits, segments, largeSegments)).toBe(false); + + expect(logSpy).toHaveBeenCalledWith('storage:localstorage: Cache expired more than 1 days ago. Cleaning up cache'); + + expect(splits.clear).toHaveBeenCalledTimes(1); + expect(segments.clear).toHaveBeenCalledTimes(1); + expect(largeSegments.clear).toHaveBeenCalledTimes(1); + + expect(localStorage.getItem(keys.buildHashKey())).toBe(FULL_SETTINGS_HASH); + expect(nearlyEqual(parseInt(localStorage.getItem(keys.buildLastClear()) as string), Date.now())).toBe(true); + }); + + test('if there is cache and its hash has changed, it should clear cache and return false', () => { + localStorage.setItem(keys.buildSplitsTillKey(), '1'); + localStorage.setItem(keys.buildHashKey(), FULL_SETTINGS_HASH); + + expect(validateCache({}, { ...fullSettings, core: { ...fullSettings.core, authorizationKey: 'another' } }, keys, splits, segments, largeSegments)).toBe(false); + + expect(logSpy).toHaveBeenCalledWith('storage:localstorage: SDK key, flags filter criteria or flags spec version has changed. Cleaning up cache'); + + expect(splits.clear).toHaveBeenCalledTimes(1); + expect(segments.clear).toHaveBeenCalledTimes(1); + expect(largeSegments.clear).toHaveBeenCalledTimes(1); + + expect(localStorage.getItem(keys.buildHashKey())).toBe('aa4877c2'); + expect(nearlyEqual(parseInt(localStorage.getItem(keys.buildLastClear()) as string), Date.now())).toBe(true); + }); + + test('if there is cache and clearOnInit is true, it should clear cache and return false', () => { + // Older cache version (without last clear) + localStorage.setItem(keys.buildSplitsTillKey(), '1'); + localStorage.setItem(keys.buildHashKey(), FULL_SETTINGS_HASH); + + expect(validateCache({ clearOnInit: true }, fullSettings, keys, splits, segments, largeSegments)).toBe(false); + + expect(logSpy).toHaveBeenCalledWith('storage:localstorage: clearOnInit was set and cache was not cleared in the last 24 hours. Cleaning up cache'); + + expect(splits.clear).toHaveBeenCalledTimes(1); + expect(segments.clear).toHaveBeenCalledTimes(1); + expect(largeSegments.clear).toHaveBeenCalledTimes(1); + + expect(localStorage.getItem(keys.buildHashKey())).toBe(FULL_SETTINGS_HASH); + const lastClear = localStorage.getItem(keys.buildLastClear()); + expect(nearlyEqual(parseInt(lastClear as string), Date.now())).toBe(true); + + // If cache is cleared, it should not clear again until a day has passed + logSpy.mockClear(); + localStorage.setItem(keys.buildSplitsTillKey(), '1'); + expect(validateCache({ clearOnInit: true }, fullSettings, keys, splits, segments, largeSegments)).toBe(true); + expect(logSpy).not.toHaveBeenCalled(); + expect(localStorage.getItem(keys.buildLastClear())).toBe(lastClear); // Last clear should not have changed + + // If a day has passed, it should clear again + localStorage.setItem(keys.buildLastClear(), (Date.now() - 1000 * 60 * 60 * 24 - 1) + ''); + expect(validateCache({ clearOnInit: true }, fullSettings, keys, splits, segments, largeSegments)).toBe(false); + expect(logSpy).toHaveBeenCalledWith('storage:localstorage: clearOnInit was set and cache was not cleared in the last 24 hours. Cleaning up cache'); + expect(splits.clear).toHaveBeenCalledTimes(2); + expect(segments.clear).toHaveBeenCalledTimes(2); + expect(largeSegments.clear).toHaveBeenCalledTimes(2); + expect(nearlyEqual(parseInt(localStorage.getItem(keys.buildLastClear()) as string), Date.now())).toBe(true); + }); +}); From 6dc1e613c74cadf1aef770dac902a22c2a846e3a Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 19 Dec 2024 18:16:33 -0300 Subject: [PATCH 08/10] rc --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 971d1486..160c9c26 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@splitsoftware/splitio-commons", - "version": "2.0.2", + "version": "2.1.0-rc.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@splitsoftware/splitio-commons", - "version": "2.0.2", + "version": "2.1.0-rc.0", "license": "Apache-2.0", "dependencies": { "@types/ioredis": "^4.28.0", diff --git a/package.json b/package.json index 936c23bd..37317c20 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "2.0.2", + "version": "2.1.0-rc.0", "description": "Split JavaScript SDK common components", "main": "cjs/index.js", "module": "esm/index.js", From 5ea10ad81efaa55570df61c8f1ac6b89b18b16a3 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 19 Dec 2024 18:31:35 -0300 Subject: [PATCH 09/10] Add changelog entry --- CHANGES.txt | 5 +++++ src/storages/inLocalStorage/validateCache.ts | 1 - 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGES.txt b/CHANGES.txt index 02a7bd61..6988dc05 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,8 @@ +2.1.0 (January XX, 2025) + - Added two new configuration options for the SDK storage in browsers when using storage type `LOCALSTORAGE`: + - `storage.expirationDays` to specify the validity period of the rollout cache. + - `storage.clearOnInit` to clear the rollout cache on SDK initialization. + 2.0.2 (December 3, 2024) - Updated the factory `init` and `destroy` methods to support re-initialization after destruction. This update ensures compatibility of the React SDK with React Strict Mode, where the factory's `init` and `destroy` effects are executed an extra time to validate proper resource cleanup. - Bugfixing - Sanitize the `SplitSDKMachineName` header value to avoid exceptions on HTTP/S requests when it contains non ISO-8859-1 characters (Related to issue https://github.com/splitio/javascript-client/issues/847). diff --git a/src/storages/inLocalStorage/validateCache.ts b/src/storages/inLocalStorage/validateCache.ts index 7c0c8ae9..c11e8d90 100644 --- a/src/storages/inLocalStorage/validateCache.ts +++ b/src/storages/inLocalStorage/validateCache.ts @@ -7,7 +7,6 @@ import type { MySegmentsCacheInLocal } from './MySegmentsCacheInLocal'; import { KeyBuilderCS } from '../KeyBuilderCS'; import SplitIO from '../../../types/splitio'; -// milliseconds in a day const DEFAULT_CACHE_EXPIRATION_IN_DAYS = 10; const MILLIS_IN_A_DAY = 86400000; From 71007342f9b44180518021f2efc7a379a94f1226 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Fri, 20 Dec 2024 13:53:44 -0300 Subject: [PATCH 10/10] Fix typo --- src/storages/inLocalStorage/__tests__/validateCache.spec.ts | 2 +- src/storages/inLocalStorage/validateCache.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/storages/inLocalStorage/__tests__/validateCache.spec.ts b/src/storages/inLocalStorage/__tests__/validateCache.spec.ts index a1f1784d..27050a56 100644 --- a/src/storages/inLocalStorage/__tests__/validateCache.spec.ts +++ b/src/storages/inLocalStorage/__tests__/validateCache.spec.ts @@ -79,7 +79,7 @@ describe('validateCache', () => { expect(validateCache({}, { ...fullSettings, core: { ...fullSettings.core, authorizationKey: 'another' } }, keys, splits, segments, largeSegments)).toBe(false); - expect(logSpy).toHaveBeenCalledWith('storage:localstorage: SDK key, flags filter criteria or flags spec version has changed. Cleaning up cache'); + expect(logSpy).toHaveBeenCalledWith('storage:localstorage: SDK key, flags filter criteria, or flags spec version has changed. Cleaning up cache'); expect(splits.clear).toHaveBeenCalledTimes(1); expect(segments.clear).toHaveBeenCalledTimes(1); diff --git a/src/storages/inLocalStorage/validateCache.ts b/src/storages/inLocalStorage/validateCache.ts index c11e8d90..c9bd78d2 100644 --- a/src/storages/inLocalStorage/validateCache.ts +++ b/src/storages/inLocalStorage/validateCache.ts @@ -41,7 +41,7 @@ function validateExpiration(options: SplitIO.InLocalStorageOptions, settings: IS log.error(LOG_PREFIX + e); } if (isThereCache) { - log.info(LOG_PREFIX + 'SDK key, flags filter criteria or flags spec version has changed. Cleaning up cache'); + log.info(LOG_PREFIX + 'SDK key, flags filter criteria, or flags spec version has changed. Cleaning up cache'); return true; } return false; // No cache to clear