diff --git a/src/storages/__tests__/RBSegmentsCacheSync.spec.ts b/src/storages/__tests__/RBSegmentsCacheSync.spec.ts index 03579351..0946bacf 100644 --- a/src/storages/__tests__/RBSegmentsCacheSync.spec.ts +++ b/src/storages/__tests__/RBSegmentsCacheSync.spec.ts @@ -6,7 +6,8 @@ import { IRBSegmentsCacheSync } from '../types'; import { fullSettings } from '../../utils/settingsValidation/__tests__/settings.mocks'; const cacheInMemory = new RBSegmentsCacheInMemory(); -const cacheInLocal = new RBSegmentsCacheInLocal(fullSettings, new KeyBuilderCS('SPLITIO', 'user')); +// eslint-disable-next-line no-undef +const cacheInLocal = new RBSegmentsCacheInLocal(fullSettings, new KeyBuilderCS('SPLITIO', 'user'), localStorage); describe.each([cacheInMemory, cacheInLocal])('Rule-based segments cache sync (Memory & LocalStorage)', (cache: IRBSegmentsCacheSync) => { diff --git a/src/storages/inLocalStorage/MySegmentsCacheInLocal.ts b/src/storages/inLocalStorage/MySegmentsCacheInLocal.ts index e3b250b5..fd038a07 100644 --- a/src/storages/inLocalStorage/MySegmentsCacheInLocal.ts +++ b/src/storages/inLocalStorage/MySegmentsCacheInLocal.ts @@ -3,16 +3,19 @@ import { isNaNNumber } from '../../utils/lang'; import { AbstractMySegmentsCacheSync } from '../AbstractMySegmentsCacheSync'; import type { MySegmentsKeyBuilder } from '../KeyBuilderCS'; import { LOG_PREFIX, DEFINED } from './constants'; +import { StorageAdapter } from '../types'; export class MySegmentsCacheInLocal extends AbstractMySegmentsCacheSync { private readonly keys: MySegmentsKeyBuilder; private readonly log: ILogger; + private readonly storage: StorageAdapter; - constructor(log: ILogger, keys: MySegmentsKeyBuilder) { + constructor(log: ILogger, keys: MySegmentsKeyBuilder, storage: StorageAdapter) { super(); this.log = log; this.keys = keys; + this.storage = storage; // There is not need to flush segments cache like splits cache, since resetSegments receives the up-to-date list of active segments } @@ -20,8 +23,8 @@ export class MySegmentsCacheInLocal extends AbstractMySegmentsCacheSync { const segmentKey = this.keys.buildSegmentNameKey(name); try { - if (localStorage.getItem(segmentKey) === DEFINED) return false; - localStorage.setItem(segmentKey, DEFINED); + if (this.storage.getItem(segmentKey) === DEFINED) return false; + this.storage.setItem(segmentKey, DEFINED); return true; } catch (e) { this.log.error(LOG_PREFIX + e); @@ -33,8 +36,8 @@ export class MySegmentsCacheInLocal extends AbstractMySegmentsCacheSync { const segmentKey = this.keys.buildSegmentNameKey(name); try { - if (localStorage.getItem(segmentKey) !== DEFINED) return false; - localStorage.removeItem(segmentKey); + if (this.storage.getItem(segmentKey) !== DEFINED) return false; + this.storage.removeItem(segmentKey); return true; } catch (e) { this.log.error(LOG_PREFIX + e); @@ -43,18 +46,16 @@ export class MySegmentsCacheInLocal extends AbstractMySegmentsCacheSync { } isInSegment(name: string): boolean { - return localStorage.getItem(this.keys.buildSegmentNameKey(name)) === DEFINED; + return this.storage.getItem(this.keys.buildSegmentNameKey(name)) === DEFINED; } getRegisteredSegments(): string[] { - // Scan current values from localStorage - return Object.keys(localStorage).reduce((accum, key) => { - let segmentName = this.keys.extractSegmentName(key); - - if (segmentName) accum.push(segmentName); - - return accum; - }, [] as string[]); + const registeredSegments: string[] = []; + for (let i = 0, len = this.storage.length; i < len; i++) { + const segmentName = this.keys.extractSegmentName(this.storage.key(i)!); + if (segmentName) registeredSegments.push(segmentName); + } + return registeredSegments; } getKeysCount() { @@ -63,8 +64,8 @@ export class MySegmentsCacheInLocal extends AbstractMySegmentsCacheSync { protected setChangeNumber(changeNumber?: number) { try { - if (changeNumber) localStorage.setItem(this.keys.buildTillKey(), changeNumber + ''); - else localStorage.removeItem(this.keys.buildTillKey()); + if (changeNumber) this.storage.setItem(this.keys.buildTillKey(), changeNumber + ''); + else this.storage.removeItem(this.keys.buildTillKey()); } catch (e) { this.log.error(e); } @@ -72,7 +73,7 @@ export class MySegmentsCacheInLocal extends AbstractMySegmentsCacheSync { getChangeNumber() { const n = -1; - let value: string | number | null = localStorage.getItem(this.keys.buildTillKey()); + let value: string | number | null = this.storage.getItem(this.keys.buildTillKey()); if (value !== null) { value = parseInt(value, 10); diff --git a/src/storages/inLocalStorage/RBSegmentsCacheInLocal.ts b/src/storages/inLocalStorage/RBSegmentsCacheInLocal.ts index 37f6ad8e..312787bc 100644 --- a/src/storages/inLocalStorage/RBSegmentsCacheInLocal.ts +++ b/src/storages/inLocalStorage/RBSegmentsCacheInLocal.ts @@ -5,22 +5,24 @@ import { isFiniteNumber, isNaNNumber, toNumber } from '../../utils/lang'; import { setToArray } from '../../utils/lang/sets'; import { usesSegments } from '../AbstractSplitsCacheSync'; import { KeyBuilderCS } from '../KeyBuilderCS'; -import { IRBSegmentsCacheSync } from '../types'; +import { IRBSegmentsCacheSync, StorageAdapter } from '../types'; import { LOG_PREFIX } from './constants'; export class RBSegmentsCacheInLocal implements IRBSegmentsCacheSync { private readonly keys: KeyBuilderCS; private readonly log: ILogger; + private readonly storage: StorageAdapter; - constructor(settings: ISettings, keys: KeyBuilderCS) { + constructor(settings: ISettings, keys: KeyBuilderCS, storage: StorageAdapter) { this.keys = keys; this.log = settings.log; + this.storage = storage; } clear() { this.getNames().forEach(name => this.remove(name)); - localStorage.removeItem(this.keys.buildRBSegmentsTillKey()); + this.storage.removeItem(this.keys.buildRBSegmentsTillKey()); } update(toAdd: IRBSegment[], toRemove: IRBSegment[], changeNumber: number): boolean { @@ -31,8 +33,8 @@ export class RBSegmentsCacheInLocal implements IRBSegmentsCacheSync { private setChangeNumber(changeNumber: number) { try { - localStorage.setItem(this.keys.buildRBSegmentsTillKey(), changeNumber + ''); - localStorage.setItem(this.keys.buildLastUpdatedKey(), Date.now() + ''); + this.storage.setItem(this.keys.buildRBSegmentsTillKey(), changeNumber + ''); + this.storage.setItem(this.keys.buildLastUpdatedKey(), Date.now() + ''); } catch (e) { this.log.error(LOG_PREFIX + e); } @@ -40,20 +42,19 @@ export class RBSegmentsCacheInLocal implements IRBSegmentsCacheSync { private updateSegmentCount(diff: number) { const segmentsCountKey = this.keys.buildSplitsWithSegmentCountKey(); - const count = toNumber(localStorage.getItem(segmentsCountKey)) + diff; - // @ts-expect-error - if (count > 0) localStorage.setItem(segmentsCountKey, count); - else localStorage.removeItem(segmentsCountKey); + const count = toNumber(this.storage.getItem(segmentsCountKey)) + diff; + if (count > 0) this.storage.setItem(segmentsCountKey, count + ''); + else this.storage.removeItem(segmentsCountKey); } private add(rbSegment: IRBSegment): boolean { try { const name = rbSegment.name; const rbSegmentKey = this.keys.buildRBSegmentKey(name); - const rbSegmentFromLocalStorage = localStorage.getItem(rbSegmentKey); - const previous = rbSegmentFromLocalStorage ? JSON.parse(rbSegmentFromLocalStorage) : null; + const rbSegmentFromStorage = this.storage.getItem(rbSegmentKey); + const previous = rbSegmentFromStorage ? JSON.parse(rbSegmentFromStorage) : null; - localStorage.setItem(rbSegmentKey, JSON.stringify(rbSegment)); + this.storage.setItem(rbSegmentKey, JSON.stringify(rbSegment)); let usesSegmentsDiff = 0; if (previous && usesSegments(previous)) usesSegmentsDiff--; @@ -72,7 +73,7 @@ export class RBSegmentsCacheInLocal implements IRBSegmentsCacheSync { const rbSegment = this.get(name); if (!rbSegment) return false; - localStorage.removeItem(this.keys.buildRBSegmentKey(name)); + this.storage.removeItem(this.keys.buildRBSegmentKey(name)); if (usesSegments(rbSegment)) this.updateSegmentCount(-1); @@ -84,13 +85,13 @@ export class RBSegmentsCacheInLocal implements IRBSegmentsCacheSync { } private getNames(): string[] { - const len = localStorage.length; + const len = this.storage.length; const accum = []; let cur = 0; while (cur < len) { - const key = localStorage.key(cur); + const key = this.storage.key(cur); if (key != null && this.keys.isRBSegmentKey(key)) accum.push(this.keys.extractKey(key)); @@ -101,7 +102,7 @@ export class RBSegmentsCacheInLocal implements IRBSegmentsCacheSync { } get(name: string): IRBSegment | null { - const item = localStorage.getItem(this.keys.buildRBSegmentKey(name)); + const item = this.storage.getItem(this.keys.buildRBSegmentKey(name)); return item && JSON.parse(item); } @@ -113,7 +114,7 @@ export class RBSegmentsCacheInLocal implements IRBSegmentsCacheSync { getChangeNumber(): number { const n = -1; - let value: string | number | null = localStorage.getItem(this.keys.buildRBSegmentsTillKey()); + let value: string | number | null = this.storage.getItem(this.keys.buildRBSegmentsTillKey()); if (value !== null) { value = parseInt(value, 10); @@ -125,7 +126,7 @@ export class RBSegmentsCacheInLocal implements IRBSegmentsCacheSync { } usesSegments(): boolean { - const storedCount = localStorage.getItem(this.keys.buildSplitsWithSegmentCountKey()); + const storedCount = this.storage.getItem(this.keys.buildSplitsWithSegmentCountKey()); const splitsWithSegmentsCount = storedCount === null ? 0 : toNumber(storedCount); return isFiniteNumber(splitsWithSegmentsCount) ? diff --git a/src/storages/inLocalStorage/SplitsCacheInLocal.ts b/src/storages/inLocalStorage/SplitsCacheInLocal.ts index 2fb6183c..3aa08452 100644 --- a/src/storages/inLocalStorage/SplitsCacheInLocal.ts +++ b/src/storages/inLocalStorage/SplitsCacheInLocal.ts @@ -6,29 +6,28 @@ import { ILogger } from '../../logger/types'; import { LOG_PREFIX } from './constants'; import { ISettings } from '../../types'; import { setToArray } from '../../utils/lang/sets'; +import { StorageAdapter } from '../types'; -/** - * ISplitsCacheSync implementation that stores split definitions in browser LocalStorage. - */ export class SplitsCacheInLocal extends AbstractSplitsCacheSync { private readonly keys: KeyBuilderCS; private readonly log: ILogger; private readonly flagSetsFilter: string[]; private hasSync?: boolean; + private readonly storage: StorageAdapter; - constructor(settings: ISettings, keys: KeyBuilderCS) { + constructor(settings: ISettings, keys: KeyBuilderCS, storage: StorageAdapter) { super(); this.keys = keys; this.log = settings.log; this.flagSetsFilter = settings.sync.__splitFiltersValidation.groupedFilters.bySet; + this.storage = storage; } private _decrementCount(key: string) { - const count = toNumber(localStorage.getItem(key)) - 1; - // @ts-expect-error - if (count > 0) localStorage.setItem(key, count); - else localStorage.removeItem(key); + const count = toNumber(this.storage.getItem(key)) - 1; + if (count > 0) this.storage.setItem(key, count + ''); + else this.storage.removeItem(key); } private _decrementCounts(split: ISplit) { @@ -48,13 +47,11 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync { private _incrementCounts(split: ISplit) { try { const ttKey = this.keys.buildTrafficTypeKey(split.trafficTypeName); - // @ts-expect-error - localStorage.setItem(ttKey, toNumber(localStorage.getItem(ttKey)) + 1); + this.storage.setItem(ttKey, (toNumber(this.storage.getItem(ttKey)) + 1) + ''); if (usesSegments(split)) { const segmentsCountKey = this.keys.buildSplitsWithSegmentCountKey(); - // @ts-expect-error - localStorage.setItem(segmentsCountKey, toNumber(localStorage.getItem(segmentsCountKey)) + 1); + this.storage.setItem(segmentsCountKey, (toNumber(this.storage.getItem(segmentsCountKey)) + 1) + ''); } } catch (e) { this.log.error(LOG_PREFIX + e); @@ -68,15 +65,15 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync { */ clear() { // collect item keys - const len = localStorage.length; + const len = this.storage.length; const accum = []; for (let cur = 0; cur < len; cur++) { - const key = localStorage.key(cur); + const key = this.storage.key(cur); if (key != null && this.keys.isSplitsCacheKey(key)) accum.push(key); } // remove items accum.forEach(key => { - localStorage.removeItem(key); + this.storage.removeItem(key); }); this.hasSync = false; @@ -86,15 +83,15 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync { try { const name = split.name; const splitKey = this.keys.buildSplitKey(name); - const splitFromLocalStorage = localStorage.getItem(splitKey); - const previousSplit = splitFromLocalStorage ? JSON.parse(splitFromLocalStorage) : null; + const splitFromStorage = this.storage.getItem(splitKey); + const previousSplit = splitFromStorage ? JSON.parse(splitFromStorage) : null; if (previousSplit) { this._decrementCounts(previousSplit); this.removeFromFlagSets(previousSplit.name, previousSplit.sets); } - localStorage.setItem(splitKey, JSON.stringify(split)); + this.storage.setItem(splitKey, JSON.stringify(split)); this._incrementCounts(split); this.addToFlagSets(split); @@ -111,7 +108,7 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync { const split = this.getSplit(name); if (!split) return false; - localStorage.removeItem(this.keys.buildSplitKey(name)); + this.storage.removeItem(this.keys.buildSplitKey(name)); this._decrementCounts(split); this.removeFromFlagSets(split.name, split.sets); @@ -124,15 +121,15 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync { } getSplit(name: string): ISplit | null { - const item = localStorage.getItem(this.keys.buildSplitKey(name)); + const item = this.storage.getItem(this.keys.buildSplitKey(name)); return item && JSON.parse(item); } setChangeNumber(changeNumber: number): boolean { try { - localStorage.setItem(this.keys.buildSplitsTillKey(), changeNumber + ''); + this.storage.setItem(this.keys.buildSplitsTillKey(), changeNumber + ''); // update "last updated" timestamp with current time - localStorage.setItem(this.keys.buildLastUpdatedKey(), Date.now() + ''); + this.storage.setItem(this.keys.buildLastUpdatedKey(), Date.now() + ''); this.hasSync = true; return true; } catch (e) { @@ -143,7 +140,7 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync { getChangeNumber(): number { const n = -1; - let value: string | number | null = localStorage.getItem(this.keys.buildSplitsTillKey()); + let value: string | number | null = this.storage.getItem(this.keys.buildSplitsTillKey()); if (value !== null) { value = parseInt(value, 10); @@ -155,13 +152,13 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync { } getSplitNames(): string[] { - const len = localStorage.length; + const len = this.storage.length; const accum = []; let cur = 0; while (cur < len) { - const key = localStorage.key(cur); + const key = this.storage.key(cur); if (key != null && this.keys.isSplitKey(key)) accum.push(this.keys.extractKey(key)); @@ -172,7 +169,7 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync { } trafficTypeExists(trafficType: string): boolean { - const ttCount = toNumber(localStorage.getItem(this.keys.buildTrafficTypeKey(trafficType))); + const ttCount = toNumber(this.storage.getItem(this.keys.buildTrafficTypeKey(trafficType))); return isFiniteNumber(ttCount) && ttCount > 0; } @@ -180,7 +177,7 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync { // If cache hasn't been synchronized with the cloud, assume we need them. if (!this.hasSync) return true; - const storedCount = localStorage.getItem(this.keys.buildSplitsWithSegmentCountKey()); + const storedCount = this.storage.getItem(this.keys.buildSplitsWithSegmentCountKey()); const splitsWithSegmentsCount = storedCount === null ? 0 : toNumber(storedCount); return isFiniteNumber(splitsWithSegmentsCount) ? @@ -191,9 +188,9 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync { getNamesByFlagSets(flagSets: string[]): Set[] { return flagSets.map(flagSet => { const flagSetKey = this.keys.buildFlagSetKey(flagSet); - const flagSetFromLocalStorage = localStorage.getItem(flagSetKey); + const flagSetFromStorage = this.storage.getItem(flagSetKey); - return new Set(flagSetFromLocalStorage ? JSON.parse(flagSetFromLocalStorage) : []); + return new Set(flagSetFromStorage ? JSON.parse(flagSetFromStorage) : []); }); } @@ -206,12 +203,12 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync { const flagSetKey = this.keys.buildFlagSetKey(featureFlagSet); - const flagSetFromLocalStorage = localStorage.getItem(flagSetKey); + const flagSetFromStorage = this.storage.getItem(flagSetKey); - const flagSetCache = new Set(flagSetFromLocalStorage ? JSON.parse(flagSetFromLocalStorage) : []); + const flagSetCache = new Set(flagSetFromStorage ? JSON.parse(flagSetFromStorage) : []); flagSetCache.add(featureFlag.name); - localStorage.setItem(flagSetKey, JSON.stringify(setToArray(flagSetCache))); + this.storage.setItem(flagSetKey, JSON.stringify(setToArray(flagSetCache))); }); } @@ -226,19 +223,19 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync { private removeNames(flagSetName: string, featureFlagName: string) { const flagSetKey = this.keys.buildFlagSetKey(flagSetName); - const flagSetFromLocalStorage = localStorage.getItem(flagSetKey); + const flagSetFromStorage = this.storage.getItem(flagSetKey); - if (!flagSetFromLocalStorage) return; + if (!flagSetFromStorage) return; - const flagSetCache = new Set(JSON.parse(flagSetFromLocalStorage)); + const flagSetCache = new Set(JSON.parse(flagSetFromStorage)); flagSetCache.delete(featureFlagName); if (flagSetCache.size === 0) { - localStorage.removeItem(flagSetKey); + this.storage.removeItem(flagSetKey); return; } - localStorage.setItem(flagSetKey, JSON.stringify(setToArray(flagSetCache))); + this.storage.setItem(flagSetKey, JSON.stringify(setToArray(flagSetCache))); } } diff --git a/src/storages/inLocalStorage/__tests__/MySegmentsCacheInLocal.spec.ts b/src/storages/inLocalStorage/__tests__/MySegmentsCacheInLocal.spec.ts index bb38fe10..25c44637 100644 --- a/src/storages/inLocalStorage/__tests__/MySegmentsCacheInLocal.spec.ts +++ b/src/storages/inLocalStorage/__tests__/MySegmentsCacheInLocal.spec.ts @@ -1,11 +1,12 @@ import { MySegmentsCacheInLocal } from '../MySegmentsCacheInLocal'; import { KeyBuilderCS, myLargeSegmentsKeyBuilder } from '../../KeyBuilderCS'; import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; +import { IMySegmentsResponse } from '../../../dtos/types'; test('SEGMENT CACHE / in LocalStorage', () => { const caches = [ - new MySegmentsCacheInLocal(loggerMock, new KeyBuilderCS('SPLITIO', 'user')), - new MySegmentsCacheInLocal(loggerMock, myLargeSegmentsKeyBuilder('SPLITIO', 'user')) + new MySegmentsCacheInLocal(loggerMock, new KeyBuilderCS('SPLITIO', 'user'), localStorage), + new MySegmentsCacheInLocal(loggerMock, myLargeSegmentsKeyBuilder('SPLITIO', 'user'), localStorage) ]; caches.forEach(cache => { @@ -22,11 +23,10 @@ test('SEGMENT CACHE / in LocalStorage', () => { }); caches.forEach(cache => { - // @ts-expect-error cache.resetSegments({ added: [], removed: ['mocked-segment'] - }); + } as IMySegmentsResponse); expect(cache.isInSegment('mocked-segment')).toBe(false); expect(cache.getRegisteredSegments()).toEqual(['mocked-segment-2']); diff --git a/src/storages/inLocalStorage/__tests__/SplitsCacheInLocal.spec.ts b/src/storages/inLocalStorage/__tests__/SplitsCacheInLocal.spec.ts index 13ab1b32..90dda35f 100644 --- a/src/storages/inLocalStorage/__tests__/SplitsCacheInLocal.spec.ts +++ b/src/storages/inLocalStorage/__tests__/SplitsCacheInLocal.spec.ts @@ -6,7 +6,7 @@ import { fullSettings } from '../../../utils/settingsValidation/__tests__/settin test('SPLITS CACHE / LocalStorage', () => { - const cache = new SplitsCacheInLocal(fullSettings, new KeyBuilderCS('SPLITIO', 'user')); + const cache = new SplitsCacheInLocal(fullSettings, new KeyBuilderCS('SPLITIO', 'user'), localStorage); cache.clear(); @@ -37,7 +37,7 @@ test('SPLITS CACHE / LocalStorage', () => { }); test('SPLITS CACHE / LocalStorage / Get Keys', () => { - const cache = new SplitsCacheInLocal(fullSettings, new KeyBuilderCS('SPLITIO', 'user')); + const cache = new SplitsCacheInLocal(fullSettings, new KeyBuilderCS('SPLITIO', 'user'), localStorage); cache.update([something, somethingElse], [], 1); @@ -48,7 +48,7 @@ test('SPLITS CACHE / LocalStorage / Get Keys', () => { }); test('SPLITS CACHE / LocalStorage / Update Splits', () => { - const cache = new SplitsCacheInLocal(fullSettings, new KeyBuilderCS('SPLITIO', 'user')); + const cache = new SplitsCacheInLocal(fullSettings, new KeyBuilderCS('SPLITIO', 'user'), localStorage); cache.update([something, somethingElse], [], 1); @@ -59,7 +59,7 @@ test('SPLITS CACHE / LocalStorage / Update Splits', () => { }); test('SPLITS CACHE / LocalStorage / trafficTypeExists and ttcache tests', () => { - const cache = new SplitsCacheInLocal(fullSettings, new KeyBuilderCS('SPLITIO', 'user')); + const cache = new SplitsCacheInLocal(fullSettings, new KeyBuilderCS('SPLITIO', 'user'), localStorage); cache.update([ { ...splitWithUserTT, name: 'split1' }, @@ -98,7 +98,7 @@ test('SPLITS CACHE / LocalStorage / trafficTypeExists and ttcache tests', () => }); test('SPLITS CACHE / LocalStorage / killLocally', () => { - const cache = new SplitsCacheInLocal(fullSettings, new KeyBuilderCS('SPLITIO', 'user')); + const cache = new SplitsCacheInLocal(fullSettings, new KeyBuilderCS('SPLITIO', 'user'), localStorage); cache.addSplit(something); cache.addSplit(somethingElse); @@ -131,7 +131,7 @@ test('SPLITS CACHE / LocalStorage / killLocally', () => { }); test('SPLITS CACHE / LocalStorage / usesSegments', () => { - const cache = new SplitsCacheInLocal(fullSettings, new KeyBuilderCS('SPLITIO', 'user')); + const cache = new SplitsCacheInLocal(fullSettings, new KeyBuilderCS('SPLITIO', 'user'), localStorage); expect(cache.usesSegments()).toBe(true); // true initially, until data is synchronized cache.setChangeNumber(1); // to indicate that data has been synced. @@ -162,7 +162,7 @@ test('SPLITS CACHE / LocalStorage / flag set cache tests', () => { queryString: '&sets=e,n,o,x', } } - }, new KeyBuilderCS('SPLITIO', 'user')); + }, new KeyBuilderCS('SPLITIO', 'user'), localStorage); const emptySet = new Set([]); @@ -206,7 +206,7 @@ test('SPLITS 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')); + const cache = new SplitsCacheInLocal(fullSettings, new KeyBuilderCS('SPLITIO', 'user'), localStorage); const emptySet = new Set([]); diff --git a/src/storages/inLocalStorage/__tests__/validateCache.spec.ts b/src/storages/inLocalStorage/__tests__/validateCache.spec.ts index 1d502ad1..2699d184 100644 --- a/src/storages/inLocalStorage/__tests__/validateCache.spec.ts +++ b/src/storages/inLocalStorage/__tests__/validateCache.spec.ts @@ -12,10 +12,10 @@ const FULL_SETTINGS_HASH = 'dc1f9817'; 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); - const rbSegments = new RBSegmentsCacheInLocal(fullSettings, keys); + const segments = new MySegmentsCacheInLocal(fullSettings.log, keys, localStorage); + const largeSegments = new MySegmentsCacheInLocal(fullSettings.log, keys, localStorage); + const splits = new SplitsCacheInLocal(fullSettings, keys, localStorage); + const rbSegments = new RBSegmentsCacheInLocal(fullSettings, keys, localStorage); jest.spyOn(splits, 'getChangeNumber'); jest.spyOn(splits, 'clear'); @@ -29,7 +29,7 @@ describe('validateCache', () => { }); test('if there is no cache, it should return false', async () => { - expect(await validateCache({}, fullSettings, keys, splits, rbSegments, segments, largeSegments)).toBe(false); + expect(await validateCache({}, localStorage, fullSettings, keys, splits, rbSegments, segments, largeSegments)).toBe(false); expect(logSpy).not.toHaveBeenCalled(); @@ -47,7 +47,7 @@ describe('validateCache', () => { localStorage.setItem(keys.buildSplitsTillKey(), '1'); localStorage.setItem(keys.buildHashKey(), FULL_SETTINGS_HASH); - expect(await validateCache({}, fullSettings, keys, splits, rbSegments, segments, largeSegments)).toBe(true); + expect(await validateCache({}, localStorage, fullSettings, keys, splits, rbSegments, segments, largeSegments)).toBe(true); expect(logSpy).not.toHaveBeenCalled(); @@ -66,7 +66,7 @@ describe('validateCache', () => { localStorage.setItem(keys.buildHashKey(), FULL_SETTINGS_HASH); localStorage.setItem(keys.buildLastUpdatedKey(), Date.now() - 1000 * 60 * 60 * 24 * 2 + ''); // 2 days ago - expect(await validateCache({ expirationDays: 1 }, fullSettings, keys, splits, rbSegments, segments, largeSegments)).toBe(false); + expect(await validateCache({ expirationDays: 1 }, localStorage, fullSettings, keys, splits, rbSegments, segments, largeSegments)).toBe(false); expect(logSpy).toHaveBeenCalledWith('storage:localstorage: Cache expired more than 1 days ago. Cleaning up cache'); @@ -83,7 +83,7 @@ describe('validateCache', () => { localStorage.setItem(keys.buildSplitsTillKey(), '1'); localStorage.setItem(keys.buildHashKey(), FULL_SETTINGS_HASH); - expect(await validateCache({}, { ...fullSettings, core: { ...fullSettings.core, authorizationKey: 'another-sdk-key' } }, keys, splits, rbSegments, segments, largeSegments)).toBe(false); + expect(await validateCache({}, localStorage, { ...fullSettings, core: { ...fullSettings.core, authorizationKey: 'another-sdk-key' } }, keys, splits, rbSegments, segments, largeSegments)).toBe(false); expect(logSpy).toHaveBeenCalledWith('storage:localstorage: SDK key, flags filter criteria, or flags spec version has changed. Cleaning up cache'); @@ -101,7 +101,7 @@ describe('validateCache', () => { localStorage.setItem(keys.buildSplitsTillKey(), '1'); localStorage.setItem(keys.buildHashKey(), FULL_SETTINGS_HASH); - expect(await validateCache({ clearOnInit: true }, fullSettings, keys, splits, rbSegments, segments, largeSegments)).toBe(false); + expect(await validateCache({ clearOnInit: true }, localStorage, fullSettings, keys, splits, rbSegments, 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'); @@ -117,13 +117,13 @@ describe('validateCache', () => { // If cache is cleared, it should not clear again until a day has passed logSpy.mockClear(); localStorage.setItem(keys.buildSplitsTillKey(), '1'); - expect(await validateCache({ clearOnInit: true }, fullSettings, keys, splits, rbSegments, segments, largeSegments)).toBe(true); + expect(await validateCache({ clearOnInit: true }, localStorage, fullSettings, keys, splits, rbSegments, 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(await validateCache({ clearOnInit: true }, fullSettings, keys, splits, rbSegments, segments, largeSegments)).toBe(false); + expect(await validateCache({ clearOnInit: true }, localStorage, fullSettings, keys, splits, rbSegments, 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(rbSegments.clear).toHaveBeenCalledTimes(2); diff --git a/src/storages/inLocalStorage/index.ts b/src/storages/inLocalStorage/index.ts index c86b1008..736d1f7b 100644 --- a/src/storages/inLocalStorage/index.ts +++ b/src/storages/inLocalStorage/index.ts @@ -15,8 +15,15 @@ import { shouldRecordTelemetry, TelemetryCacheInMemory } from '../inMemory/Telem import { UniqueKeysCacheInMemoryCS } from '../inMemory/UniqueKeysCacheInMemoryCS'; import { getMatching } from '../../utils/key'; import { validateCache } from './validateCache'; +import { ILogger } from '../../logger/types'; import SplitIO from '../../../types/splitio'; +function validateStorage(log: ILogger) { + if (isLocalStorageAvailable()) return localStorage; + + log.warn(LOG_PREFIX + 'LocalStorage API is unavailable. Falling back to default MEMORY storage'); +} + /** * InLocal storage factory for standalone client-side SplitFactory */ @@ -25,21 +32,18 @@ export function InLocalStorage(options: SplitIO.InLocalStorageOptions = {}): ISt const prefix = validatePrefix(options.prefix); function InLocalStorageCSFactory(params: IStorageFactoryParams): IStorageSync { + const { settings, settings: { log, scheduler: { impressionsQueueSize, eventsQueueSize } } } = params; - // Fallback to InMemoryStorage if LocalStorage API is not available - if (!isLocalStorageAvailable()) { - params.settings.log.warn(LOG_PREFIX + 'LocalStorage API is unavailable. Falling back to default MEMORY storage'); - return InMemoryStorageCSFactory(params); - } + const storage = validateStorage(log); + if (!storage) return InMemoryStorageCSFactory(params); - const { settings, settings: { log, scheduler: { impressionsQueueSize, eventsQueueSize } } } = params; const matchingKey = getMatching(settings.core.key); const keys = new KeyBuilderCS(prefix, matchingKey); - const splits = new SplitsCacheInLocal(settings, keys); - const rbSegments = new RBSegmentsCacheInLocal(settings, keys); - const segments = new MySegmentsCacheInLocal(log, keys); - const largeSegments = new MySegmentsCacheInLocal(log, myLargeSegmentsKeyBuilder(prefix, matchingKey)); + const splits = new SplitsCacheInLocal(settings, keys, storage); + const rbSegments = new RBSegmentsCacheInLocal(settings, keys, storage); + const segments = new MySegmentsCacheInLocal(log, keys, storage); + const largeSegments = new MySegmentsCacheInLocal(log, myLargeSegmentsKeyBuilder(prefix, matchingKey), storage); let validateCachePromise: Promise | undefined; return { @@ -54,7 +58,7 @@ export function InLocalStorage(options: SplitIO.InLocalStorageOptions = {}): ISt uniqueKeys: new UniqueKeysCacheInMemoryCS(), validateCache() { - return validateCachePromise || (validateCachePromise = validateCache(options, settings, keys, splits, rbSegments, segments, largeSegments)); + return validateCachePromise || (validateCachePromise = validateCache(options, storage, settings, keys, splits, rbSegments, segments, largeSegments)); }, destroy() { @@ -67,8 +71,8 @@ export function InLocalStorage(options: SplitIO.InLocalStorageOptions = {}): ISt return { splits: this.splits, rbSegments: this.rbSegments, - segments: new MySegmentsCacheInLocal(log, new KeyBuilderCS(prefix, matchingKey)), - largeSegments: new MySegmentsCacheInLocal(log, myLargeSegmentsKeyBuilder(prefix, matchingKey)), + segments: new MySegmentsCacheInLocal(log, new KeyBuilderCS(prefix, matchingKey), storage), + largeSegments: new MySegmentsCacheInLocal(log, myLargeSegmentsKeyBuilder(prefix, matchingKey), storage), impressions: this.impressions, impressionCounts: this.impressionCounts, events: this.events, diff --git a/src/storages/inLocalStorage/validateCache.ts b/src/storages/inLocalStorage/validateCache.ts index 07d87c79..c5adf199 100644 --- a/src/storages/inLocalStorage/validateCache.ts +++ b/src/storages/inLocalStorage/validateCache.ts @@ -7,6 +7,7 @@ import type { RBSegmentsCacheInLocal } from './RBSegmentsCacheInLocal'; import type { MySegmentsCacheInLocal } from './MySegmentsCacheInLocal'; import { KeyBuilderCS } from '../KeyBuilderCS'; import SplitIO from '../../../types/splitio'; +import { StorageAdapter } from '../types'; const DEFAULT_CACHE_EXPIRATION_IN_DAYS = 10; const MILLIS_IN_A_DAY = 86400000; @@ -16,11 +17,11 @@ const MILLIS_IN_A_DAY = 86400000; * * @returns `true` if cache should be cleared, `false` otherwise */ -function validateExpiration(options: SplitIO.InLocalStorageOptions, settings: ISettings, keys: KeyBuilderCS, currentTimestamp: number, isThereCache: boolean) { +function validateExpiration(options: SplitIO.InLocalStorageOptions, storage: StorageAdapter, settings: ISettings, keys: KeyBuilderCS, currentTimestamp: number, isThereCache: boolean) { const { log } = settings; // Check expiration - const lastUpdatedTimestamp = parseInt(localStorage.getItem(keys.buildLastUpdatedKey()) as string, 10); + const lastUpdatedTimestamp = parseInt(storage.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; @@ -32,12 +33,12 @@ function validateExpiration(options: SplitIO.InLocalStorageOptions, settings: IS // Check hash const storageHashKey = keys.buildHashKey(); - const storageHash = localStorage.getItem(storageHashKey); + const storageHash = storage.getItem(storageHashKey); const currentStorageHash = getStorageHash(settings); if (storageHash !== currentStorageHash) { try { - localStorage.setItem(storageHashKey, currentStorageHash); + storage.setItem(storageHashKey, currentStorageHash); } catch (e) { log.error(LOG_PREFIX + e); } @@ -50,7 +51,7 @@ function validateExpiration(options: SplitIO.InLocalStorageOptions, settings: IS // Clear on init if (options.clearOnInit) { - const lastClearTimestamp = parseInt(localStorage.getItem(keys.buildLastClear()) as string, 10); + const lastClearTimestamp = parseInt(storage.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'); @@ -67,13 +68,13 @@ function validateExpiration(options: SplitIO.InLocalStorageOptions, settings: IS * * @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, rbSegments: RBSegmentsCacheInLocal, segments: MySegmentsCacheInLocal, largeSegments: MySegmentsCacheInLocal): Promise { +export function validateCache(options: SplitIO.InLocalStorageOptions, storage: StorageAdapter, settings: ISettings, keys: KeyBuilderCS, splits: SplitsCacheInLocal, rbSegments: RBSegmentsCacheInLocal, segments: MySegmentsCacheInLocal, largeSegments: MySegmentsCacheInLocal): Promise { return Promise.resolve().then(() => { const currentTimestamp = Date.now(); const isThereCache = splits.getChangeNumber() > -1; - if (validateExpiration(options, settings, keys, currentTimestamp, isThereCache)) { + if (validateExpiration(options, storage, settings, keys, currentTimestamp, isThereCache)) { splits.clear(); rbSegments.clear(); segments.clear(); @@ -81,7 +82,7 @@ export function validateCache(options: SplitIO.InLocalStorageOptions, settings: // Update last clear timestamp try { - localStorage.setItem(keys.buildLastClear(), currentTimestamp + ''); + storage.setItem(keys.buildLastClear(), currentTimestamp + ''); } catch (e) { settings.log.error(LOG_PREFIX + e); } diff --git a/src/storages/types.ts b/src/storages/types.ts index 0e9c3140..8de14402 100644 --- a/src/storages/types.ts +++ b/src/storages/types.ts @@ -4,6 +4,18 @@ import { MySegmentsData } from '../sync/polling/types'; import { EventDataType, HttpErrors, HttpLatencies, ImpressionDataType, LastSync, Method, MethodExceptions, MethodLatencies, MultiMethodExceptions, MultiMethodLatencies, MultiConfigs, OperationType, StoredEventWithMetadata, StoredImpressionWithMetadata, StreamingEvent, UniqueKeysPayloadCs, UniqueKeysPayloadSs, TelemetryUsageStatsPayload, UpdatesFromSSEEnum } from '../sync/submitters/types'; import { ISettings } from '../types'; +/** + * Internal interface based on a subset of the Web Storage API interface + * (https://developer.mozilla.org/en-US/docs/Web/API/Storage) used by the SDK + */ +export interface StorageAdapter { + readonly length: number; + key(index: number): string | null; + getItem(key: string): string | null; + removeItem(key: string): void; + setItem(key: string, value: string): void; +} + /** * Interface of a pluggable storage wrapper. */