diff --git a/src/storages/inLocalStorage/__tests__/MySegmentsCacheInLocal.spec.ts b/src/storages/inLocalStorage/__tests__/MySegmentsCacheInLocal.spec.ts index 25c44637..aa218423 100644 --- a/src/storages/inLocalStorage/__tests__/MySegmentsCacheInLocal.spec.ts +++ b/src/storages/inLocalStorage/__tests__/MySegmentsCacheInLocal.spec.ts @@ -1,12 +1,13 @@ import { MySegmentsCacheInLocal } from '../MySegmentsCacheInLocal'; import { KeyBuilderCS, myLargeSegmentsKeyBuilder } from '../../KeyBuilderCS'; import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; +import { storages, PREFIX } from './wrapper.mock'; import { IMySegmentsResponse } from '../../../dtos/types'; -test('SEGMENT CACHE / in LocalStorage', () => { +test.each(storages)('SEGMENT CACHE / in LocalStorage', (storage) => { const caches = [ - new MySegmentsCacheInLocal(loggerMock, new KeyBuilderCS('SPLITIO', 'user'), localStorage), - new MySegmentsCacheInLocal(loggerMock, myLargeSegmentsKeyBuilder('SPLITIO', 'user'), localStorage) + new MySegmentsCacheInLocal(loggerMock, new KeyBuilderCS(PREFIX, 'user'), storage), + new MySegmentsCacheInLocal(loggerMock, myLargeSegmentsKeyBuilder(PREFIX, 'user'), storage) ]; caches.forEach(cache => { @@ -33,8 +34,8 @@ test('SEGMENT CACHE / in LocalStorage', () => { expect(cache.getKeysCount()).toBe(1); }); - expect(localStorage.getItem('SPLITIO.user.segment.mocked-segment-2')).toBe('1'); - expect(localStorage.getItem('SPLITIO.user.segment.mocked-segment')).toBe(null); - expect(localStorage.getItem('SPLITIO.user.largeSegment.mocked-segment-2')).toBe('1'); - expect(localStorage.getItem('SPLITIO.user.largeSegment.mocked-segment')).toBe(null); + expect(storage.getItem(PREFIX + '.user.segment.mocked-segment-2')).toBe('1'); + expect(storage.getItem(PREFIX + '.user.segment.mocked-segment')).toBe(null); + expect(storage.getItem(PREFIX + '.user.largeSegment.mocked-segment-2')).toBe('1'); + expect(storage.getItem(PREFIX + '.user.largeSegment.mocked-segment')).toBe(null); }); diff --git a/src/storages/inLocalStorage/__tests__/SplitsCacheInLocal.spec.ts b/src/storages/inLocalStorage/__tests__/SplitsCacheInLocal.spec.ts index 90dda35f..976baa72 100644 --- a/src/storages/inLocalStorage/__tests__/SplitsCacheInLocal.spec.ts +++ b/src/storages/inLocalStorage/__tests__/SplitsCacheInLocal.spec.ts @@ -3,228 +3,231 @@ import { KeyBuilderCS } from '../../KeyBuilderCS'; import { splitWithUserTT, splitWithAccountTT, splitWithAccountTTAndUsesSegments, something, somethingElse, featureFlagOne, featureFlagTwo, featureFlagThree, featureFlagWithEmptyFS, featureFlagWithoutFS } from '../../__tests__/testUtils'; import { ISplit } from '../../../dtos/types'; import { fullSettings } from '../../../utils/settingsValidation/__tests__/settings.mocks'; +import { storages, PREFIX } from './wrapper.mock'; -test('SPLITS CACHE / LocalStorage', () => { - const cache = new SplitsCacheInLocal(fullSettings, new KeyBuilderCS('SPLITIO', 'user'), localStorage); +describe.each(storages)('SPLITS CACHE', (storage) => { + test('LocalStorage', () => { + const cache = new SplitsCacheInLocal(fullSettings, new KeyBuilderCS(PREFIX, 'user'), storage); - cache.clear(); + cache.clear(); - cache.update([something, somethingElse], [], -1); + cache.update([something, somethingElse], [], -1); - let values = cache.getAll(); + let values = cache.getAll(); - expect(values).toEqual([something, somethingElse]); + expect(values).toEqual([something, somethingElse]); - cache.removeSplit(something.name); + cache.removeSplit(something.name); - const splits = cache.getSplits([something.name, somethingElse.name]); - expect(splits[something.name]).toEqual(null); - expect(splits[somethingElse.name]).toEqual(somethingElse); + const splits = cache.getSplits([something.name, somethingElse.name]); + expect(splits[something.name]).toEqual(null); + expect(splits[somethingElse.name]).toEqual(somethingElse); - values = cache.getAll(); + values = cache.getAll(); - expect(values).toEqual([somethingElse]); + expect(values).toEqual([somethingElse]); - expect(cache.getSplit(something.name)).toEqual(null); - expect(cache.getSplit(somethingElse.name)).toEqual(somethingElse); + expect(cache.getSplit(something.name)).toEqual(null); + expect(cache.getSplit(somethingElse.name)).toEqual(somethingElse); - expect(cache.getChangeNumber()).toBe(-1); + expect(cache.getChangeNumber()).toBe(-1); - cache.setChangeNumber(123); + cache.setChangeNumber(123); - expect(cache.getChangeNumber()).toBe(123); -}); + expect(cache.getChangeNumber()).toBe(123); + }); -test('SPLITS CACHE / LocalStorage / Get Keys', () => { - const cache = new SplitsCacheInLocal(fullSettings, new KeyBuilderCS('SPLITIO', 'user'), localStorage); + test('LocalStorage / Get Keys', () => { + const cache = new SplitsCacheInLocal(fullSettings, new KeyBuilderCS(PREFIX, 'user'), storage); - cache.update([something, somethingElse], [], 1); + cache.update([something, somethingElse], [], 1); - const keys = cache.getSplitNames(); + const keys = cache.getSplitNames(); - expect(keys.indexOf(something.name) !== -1).toBe(true); - expect(keys.indexOf(somethingElse.name) !== -1).toBe(true); -}); + expect(keys.indexOf(something.name) !== -1).toBe(true); + expect(keys.indexOf(somethingElse.name) !== -1).toBe(true); + }); -test('SPLITS CACHE / LocalStorage / Update Splits', () => { - const cache = new SplitsCacheInLocal(fullSettings, new KeyBuilderCS('SPLITIO', 'user'), localStorage); + test('LocalStorage / Update Splits', () => { + const cache = new SplitsCacheInLocal(fullSettings, new KeyBuilderCS(PREFIX, 'user'), storage); - cache.update([something, somethingElse], [], 1); + cache.update([something, somethingElse], [], 1); - cache.update([], [something, somethingElse], 1); + cache.update([], [something, somethingElse], 1); - expect(cache.getSplit(something.name)).toBe(null); - expect(cache.getSplit(somethingElse.name)).toBe(null); -}); + expect(cache.getSplit(something.name)).toBe(null); + expect(cache.getSplit(somethingElse.name)).toBe(null); + }); -test('SPLITS CACHE / LocalStorage / trafficTypeExists and ttcache tests', () => { - const cache = new SplitsCacheInLocal(fullSettings, new KeyBuilderCS('SPLITIO', 'user'), localStorage); + test('LocalStorage / trafficTypeExists and ttcache tests', () => { + const cache = new SplitsCacheInLocal(fullSettings, new KeyBuilderCS(PREFIX, 'user'), storage); - cache.update([ - { ...splitWithUserTT, name: 'split1' }, - { ...splitWithAccountTT, name: 'split2' }, - { ...splitWithUserTT, name: 'split3' }, - ], [], 1); - cache.addSplit({ ...splitWithUserTT, name: 'split4' }); + cache.update([ + { ...splitWithUserTT, name: 'split1' }, + { ...splitWithAccountTT, name: 'split2' }, + { ...splitWithUserTT, name: 'split3' }, + ], [], 1); + cache.addSplit({ ...splitWithUserTT, name: 'split4' }); - expect(cache.trafficTypeExists('user_tt')).toBe(true); - expect(cache.trafficTypeExists('account_tt')).toBe(true); - expect(cache.trafficTypeExists('not_existent_tt')).toBe(false); + expect(cache.trafficTypeExists('user_tt')).toBe(true); + expect(cache.trafficTypeExists('account_tt')).toBe(true); + expect(cache.trafficTypeExists('not_existent_tt')).toBe(false); - cache.removeSplit('split4'); + cache.removeSplit('split4'); - expect(cache.trafficTypeExists('user_tt')).toBe(true); - expect(cache.trafficTypeExists('account_tt')).toBe(true); + expect(cache.trafficTypeExists('user_tt')).toBe(true); + expect(cache.trafficTypeExists('account_tt')).toBe(true); - cache.removeSplit('split3'); - cache.removeSplit('split2'); + cache.removeSplit('split3'); + cache.removeSplit('split2'); - expect(cache.trafficTypeExists('user_tt')).toBe(true); - expect(cache.trafficTypeExists('account_tt')).toBe(false); + expect(cache.trafficTypeExists('user_tt')).toBe(true); + expect(cache.trafficTypeExists('account_tt')).toBe(false); - cache.removeSplit('split1'); + cache.removeSplit('split1'); - expect(cache.trafficTypeExists('user_tt')).toBe(false); - expect(cache.trafficTypeExists('account_tt')).toBe(false); + expect(cache.trafficTypeExists('user_tt')).toBe(false); + expect(cache.trafficTypeExists('account_tt')).toBe(false); - cache.addSplit({ ...splitWithUserTT, name: 'split1' }); - expect(cache.trafficTypeExists('user_tt')).toBe(true); + cache.addSplit({ ...splitWithUserTT, name: 'split1' }); + expect(cache.trafficTypeExists('user_tt')).toBe(true); - cache.addSplit({ ...splitWithAccountTT, name: 'split1' }); - expect(cache.trafficTypeExists('account_tt')).toBe(true); - expect(cache.trafficTypeExists('user_tt')).toBe(false); + cache.addSplit({ ...splitWithAccountTT, name: 'split1' }); + expect(cache.trafficTypeExists('account_tt')).toBe(true); + expect(cache.trafficTypeExists('user_tt')).toBe(false); -}); + }); -test('SPLITS CACHE / LocalStorage / killLocally', () => { - const cache = new SplitsCacheInLocal(fullSettings, new KeyBuilderCS('SPLITIO', 'user'), localStorage); + test('LocalStorage / killLocally', () => { + const cache = new SplitsCacheInLocal(fullSettings, new KeyBuilderCS(PREFIX, 'user'), storage); - cache.addSplit(something); - cache.addSplit(somethingElse); - const initialChangeNumber = cache.getChangeNumber(); + cache.addSplit(something); + cache.addSplit(somethingElse); + const initialChangeNumber = cache.getChangeNumber(); - // kill an non-existent split - let updated = cache.killLocally('nonexistent_split', 'other_treatment', 101); - const nonexistentSplit = cache.getSplit('nonexistent_split'); + // kill an non-existent split + let updated = cache.killLocally('nonexistent_split', 'other_treatment', 101); + const nonexistentSplit = cache.getSplit('nonexistent_split'); - expect(updated).toBe(false); // killLocally resolves without update if split doesn't exist - expect(nonexistentSplit).toBe(null); // non-existent split keeps being non-existent + expect(updated).toBe(false); // killLocally resolves without update if split doesn't exist + expect(nonexistentSplit).toBe(null); // non-existent split keeps being non-existent - // kill an existent split - updated = cache.killLocally(something.name, 'some_treatment', 100); - let lol1Split = cache.getSplit(something.name) as ISplit; + // kill an existent split + updated = cache.killLocally(something.name, 'some_treatment', 100); + let lol1Split = cache.getSplit(something.name) as ISplit; - expect(updated).toBe(true); // killLocally resolves with update if split is changed - expect(lol1Split.killed).toBe(true); // existing split must be killed - expect(lol1Split.defaultTreatment).toBe('some_treatment'); // existing split must have new default treatment - expect(lol1Split.changeNumber).toBe(100); // existing split must have the given change number - expect(cache.getChangeNumber()).toBe(initialChangeNumber); // cache changeNumber is not changed + expect(updated).toBe(true); // killLocally resolves with update if split is changed + expect(lol1Split.killed).toBe(true); // existing split must be killed + expect(lol1Split.defaultTreatment).toBe('some_treatment'); // existing split must have new default treatment + expect(lol1Split.changeNumber).toBe(100); // existing split must have the given change number + expect(cache.getChangeNumber()).toBe(initialChangeNumber); // cache changeNumber is not changed - // not update if changeNumber is old - updated = cache.killLocally(something.name, 'some_treatment_2', 90); - lol1Split = cache.getSplit(something.name) as ISplit; + // not update if changeNumber is old + updated = cache.killLocally(something.name, 'some_treatment_2', 90); + lol1Split = cache.getSplit(something.name) as ISplit; - expect(updated).toBe(false); // killLocally resolves without update if changeNumber is old - expect(lol1Split.defaultTreatment).not.toBe('some_treatment_2'); // existing split is not updated if given changeNumber is older + expect(updated).toBe(false); // killLocally resolves without update if changeNumber is old + expect(lol1Split.defaultTreatment).not.toBe('some_treatment_2'); // existing split is not updated if given changeNumber is older -}); + }); -test('SPLITS CACHE / LocalStorage / usesSegments', () => { - const cache = new SplitsCacheInLocal(fullSettings, new KeyBuilderCS('SPLITIO', 'user'), localStorage); + test('LocalStorage / usesSegments', () => { + const cache = new SplitsCacheInLocal(fullSettings, new KeyBuilderCS(PREFIX, 'user'), storage); - expect(cache.usesSegments()).toBe(true); // true initially, until data is synchronized - cache.setChangeNumber(1); // to indicate that data has been synced. + expect(cache.usesSegments()).toBe(true); // true initially, until data is synchronized + cache.setChangeNumber(1); // to indicate that data has been synced. - cache.update([splitWithUserTT, splitWithAccountTT], [], 1); - expect(cache.usesSegments()).toBe(false); // 0 splits using segments + cache.update([splitWithUserTT, splitWithAccountTT], [], 1); + expect(cache.usesSegments()).toBe(false); // 0 splits using segments - cache.addSplit({ ...splitWithAccountTTAndUsesSegments, name: 'split3' }); - expect(cache.usesSegments()).toBe(true); // 1 split using segments + cache.addSplit({ ...splitWithAccountTTAndUsesSegments, name: 'split3' }); + expect(cache.usesSegments()).toBe(true); // 1 split using segments - cache.addSplit({ ...splitWithAccountTTAndUsesSegments, name: 'split4' }); - expect(cache.usesSegments()).toBe(true); // 2 splits using segments + cache.addSplit({ ...splitWithAccountTTAndUsesSegments, name: 'split4' }); + expect(cache.usesSegments()).toBe(true); // 2 splits using segments - cache.removeSplit('split3'); - expect(cache.usesSegments()).toBe(true); // 1 split using segments + cache.removeSplit('split3'); + expect(cache.usesSegments()).toBe(true); // 1 split using segments - cache.removeSplit('split4'); - expect(cache.usesSegments()).toBe(false); // 0 splits using segments -}); + cache.removeSplit('split4'); + expect(cache.usesSegments()).toBe(false); // 0 splits using segments + }); -test('SPLITS CACHE / LocalStorage / flag set cache tests', () => { - // @ts-ignore - const cache = new SplitsCacheInLocal({ - ...fullSettings, - sync: { // @ts-expect-error - __splitFiltersValidation: { - groupedFilters: { bySet: ['o', 'n', 'e', 'x'], byName: [], byPrefix: [] }, - queryString: '&sets=e,n,o,x', + test('LocalStorage / flag set cache tests', () => { + // @ts-ignore + const cache = new SplitsCacheInLocal({ + ...fullSettings, + sync: { // @ts-expect-error + __splitFiltersValidation: { + groupedFilters: { bySet: ['o', 'n', 'e', 'x'], byName: [], byPrefix: [] }, + queryString: '&sets=e,n,o,x', + } } - } - }, new KeyBuilderCS('SPLITIO', 'user'), localStorage); + }, new KeyBuilderCS(PREFIX, 'user'), storage); - const emptySet = new Set([]); + const emptySet = new Set([]); - cache.update([ - featureFlagOne, - featureFlagTwo, - featureFlagThree, - ], [], -1); - cache.addSplit(featureFlagWithEmptyFS); + cache.update([ + featureFlagOne, + featureFlagTwo, + featureFlagThree, + ], [], -1); + cache.addSplit(featureFlagWithEmptyFS); - // Adding an existing FF should not affect the cache - cache.update([featureFlagTwo], [], -1); + // Adding an existing FF should not affect the cache + cache.update([featureFlagTwo], [], -1); - expect(cache.getNamesByFlagSets(['o'])).toEqual([new Set(['ff_one', 'ff_two'])]); - expect(cache.getNamesByFlagSets(['n'])).toEqual([new Set(['ff_one'])]); - expect(cache.getNamesByFlagSets(['e'])).toEqual([new Set(['ff_one', 'ff_three'])]); - expect(cache.getNamesByFlagSets(['t'])).toEqual([emptySet]); // 't' not in filter - expect(cache.getNamesByFlagSets(['o', 'n', 'e'])).toEqual([new Set(['ff_one', 'ff_two']), new Set(['ff_one']), new Set(['ff_one', 'ff_three'])]); + expect(cache.getNamesByFlagSets(['o'])).toEqual([new Set(['ff_one', 'ff_two'])]); + expect(cache.getNamesByFlagSets(['n'])).toEqual([new Set(['ff_one'])]); + expect(cache.getNamesByFlagSets(['e'])).toEqual([new Set(['ff_one', 'ff_three'])]); + expect(cache.getNamesByFlagSets(['t'])).toEqual([emptySet]); // 't' not in filter + expect(cache.getNamesByFlagSets(['o', 'n', 'e'])).toEqual([new Set(['ff_one', 'ff_two']), new Set(['ff_one']), new Set(['ff_one', 'ff_three'])]); - cache.addSplit({ ...featureFlagOne, sets: ['1'] }); + cache.addSplit({ ...featureFlagOne, sets: ['1'] }); - expect(cache.getNamesByFlagSets(['1'])).toEqual([emptySet]); // '1' not in filter - expect(cache.getNamesByFlagSets(['o'])).toEqual([new Set(['ff_two'])]); - expect(cache.getNamesByFlagSets(['n'])).toEqual([emptySet]); + expect(cache.getNamesByFlagSets(['1'])).toEqual([emptySet]); // '1' not in filter + expect(cache.getNamesByFlagSets(['o'])).toEqual([new Set(['ff_two'])]); + expect(cache.getNamesByFlagSets(['n'])).toEqual([emptySet]); - cache.addSplit({ ...featureFlagOne, sets: ['x'] }); - expect(cache.getNamesByFlagSets(['x'])).toEqual([new Set(['ff_one'])]); - expect(cache.getNamesByFlagSets(['o', 'e', 'x'])).toEqual([new Set(['ff_two']), new Set(['ff_three']), new Set(['ff_one'])]); + cache.addSplit({ ...featureFlagOne, sets: ['x'] }); + expect(cache.getNamesByFlagSets(['x'])).toEqual([new Set(['ff_one'])]); + expect(cache.getNamesByFlagSets(['o', 'e', 'x'])).toEqual([new Set(['ff_two']), new Set(['ff_three']), new Set(['ff_one'])]); - cache.removeSplit(featureFlagOne.name); - expect(cache.getNamesByFlagSets(['x'])).toEqual([emptySet]); + cache.removeSplit(featureFlagOne.name); + expect(cache.getNamesByFlagSets(['x'])).toEqual([emptySet]); - cache.removeSplit(featureFlagOne.name); - expect(cache.getNamesByFlagSets(['y'])).toEqual([emptySet]); // 'y' not in filter - expect(cache.getNamesByFlagSets([])).toEqual([]); + cache.removeSplit(featureFlagOne.name); + expect(cache.getNamesByFlagSets(['y'])).toEqual([emptySet]); // 'y' not in filter + expect(cache.getNamesByFlagSets([])).toEqual([]); - cache.addSplit(featureFlagWithoutFS); - expect(cache.getNamesByFlagSets([])).toEqual([]); -}); + cache.addSplit(featureFlagWithoutFS); + expect(cache.getNamesByFlagSets([])).toEqual([]); + }); + + // if FlagSets are not defined, it should store all FlagSets in memory. + test('LocalStorage / flag set cache tests without filters', () => { + const cache = new SplitsCacheInLocal(fullSettings, new KeyBuilderCS(PREFIX, 'user'), storage); + + const emptySet = new Set([]); + + cache.update([ + featureFlagOne, + featureFlagTwo, + featureFlagThree, + ], [], -1); + cache.addSplit(featureFlagWithEmptyFS); + + expect(cache.getNamesByFlagSets(['o'])).toEqual([new Set(['ff_one', 'ff_two'])]); + expect(cache.getNamesByFlagSets(['n'])).toEqual([new Set(['ff_one'])]); + expect(cache.getNamesByFlagSets(['e'])).toEqual([new Set(['ff_one', 'ff_three'])]); + expect(cache.getNamesByFlagSets(['t'])).toEqual([new Set(['ff_two', 'ff_three'])]); + expect(cache.getNamesByFlagSets(['y'])).toEqual([emptySet]); + expect(cache.getNamesByFlagSets(['o', 'n', 'e'])).toEqual([new Set(['ff_one', 'ff_two']), new Set(['ff_one']), new Set(['ff_one', 'ff_three'])]); -// 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'), localStorage); - - const emptySet = new Set([]); - - cache.update([ - featureFlagOne, - featureFlagTwo, - featureFlagThree, - ], [], -1); - cache.addSplit(featureFlagWithEmptyFS); - - expect(cache.getNamesByFlagSets(['o'])).toEqual([new Set(['ff_one', 'ff_two'])]); - expect(cache.getNamesByFlagSets(['n'])).toEqual([new Set(['ff_one'])]); - expect(cache.getNamesByFlagSets(['e'])).toEqual([new Set(['ff_one', 'ff_three'])]); - expect(cache.getNamesByFlagSets(['t'])).toEqual([new Set(['ff_two', 'ff_three'])]); - expect(cache.getNamesByFlagSets(['y'])).toEqual([emptySet]); - expect(cache.getNamesByFlagSets(['o', 'n', 'e'])).toEqual([new Set(['ff_one', 'ff_two']), new Set(['ff_one']), new Set(['ff_one', 'ff_three'])]); - - // Validate that the feature flag cache is cleared when calling `clear` method - cache.clear(); - expect(localStorage.length).toBe(0); + // Validate that the feature flag cache is cleared when calling `clear` method + cache.clear(); + expect(storage.length).toBe(0); + }); }); diff --git a/src/storages/inLocalStorage/__tests__/index.spec.ts b/src/storages/inLocalStorage/__tests__/index.spec.ts index b9d0fc1d..545c532f 100644 --- a/src/storages/inLocalStorage/__tests__/index.spec.ts +++ b/src/storages/inLocalStorage/__tests__/index.spec.ts @@ -23,19 +23,29 @@ describe('IN LOCAL STORAGE', () => { fakeInMemoryStorageFactory.mockClear(); }); - test('calls InMemoryStorage factory if LocalStorage API is not available', () => { - + test('calls InMemoryStorage factory if LocalStorage API is not available or the provided storage wrapper is invalid', () => { + // Delete global localStorage property const originalLocalStorage = Object.getOwnPropertyDescriptor(global, 'localStorage'); - Object.defineProperty(global, 'localStorage', {}); // delete global localStorage property - - const storageFactory = InLocalStorage({ prefix: 'prefix' }); - const storage = storageFactory(internalSdkParams); + Object.defineProperty(global, 'localStorage', {}); + // LocalStorage API is not available + let storageFactory = InLocalStorage({ prefix: 'prefix' }); + let storage = storageFactory(internalSdkParams); expect(fakeInMemoryStorageFactory).toBeCalledWith(internalSdkParams); // calls InMemoryStorage factory expect(storage).toBe(fakeInMemoryStorage); - Object.defineProperty(global, 'localStorage', originalLocalStorage as PropertyDescriptor); // restore original localStorage + // @ts-expect-error Provided storage is invalid + storageFactory = InLocalStorage({ prefix: 'prefix', wrapper: {} }); + storage = storageFactory(internalSdkParams); + expect(storage).toBe(fakeInMemoryStorage); + + // Provided storage is valid + storageFactory = InLocalStorage({ prefix: 'prefix', wrapper: { getItem: () => Promise.resolve(null), setItem: () => Promise.resolve(), removeItem: () => Promise.resolve() } }); + storage = storageFactory(internalSdkParams); + expect(storage).not.toBe(fakeInMemoryStorage); + // Restore original localStorage + Object.defineProperty(global, 'localStorage', originalLocalStorage as PropertyDescriptor); }); test('calls its own storage factory if LocalStorage API is available', () => { diff --git a/src/storages/inLocalStorage/__tests__/storageAdapter.spec.ts b/src/storages/inLocalStorage/__tests__/storageAdapter.spec.ts new file mode 100644 index 00000000..84be924a --- /dev/null +++ b/src/storages/inLocalStorage/__tests__/storageAdapter.spec.ts @@ -0,0 +1,62 @@ +import { storageAdapter } from '../storageAdapter'; +import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; + + +const syncWrapper = { + getItem: jest.fn(() => JSON.stringify({ key1: 'value1' })), + setItem: jest.fn(), + removeItem: jest.fn(), +}; + +const asyncWrapper = { + getItem: jest.fn(() => Promise.resolve(JSON.stringify({ key1: 'value1' }))), + setItem: jest.fn(() => Promise.resolve()), + removeItem: jest.fn(() => Promise.resolve()), +}; + +test.each([ + [syncWrapper], + [asyncWrapper], +])('storageAdapter', async (wrapper) => { + + const storage = storageAdapter(loggerMock, 'prefix', wrapper); + + expect(storage.length).toBe(0); + + // Load cache from storage wrapper + await storage.load(); + + expect(wrapper.getItem).toHaveBeenCalledWith('prefix'); + expect(storage.length).toBe(1); + expect(storage.key(0)).toBe('key1'); + expect(storage.getItem('key1')).toBe('value1'); + + // Set item + storage.setItem('key2', 'value2'); + expect(storage.getItem('key2')).toBe('value2'); + expect(storage.length).toBe(2); + + // Remove item + storage.removeItem('key1'); + expect(storage.getItem('key1')).toBe(null); + expect(storage.length).toBe(1); + + // Until `save` is called, changes should not be saved/persisted + await storage.whenSaved(); + expect(wrapper.setItem).not.toHaveBeenCalled(); + + storage.setItem('.till', '1'); + expect(storage.length).toBe(2); + expect(storage.key(0)).toBe('key2'); + expect(storage.key(1)).toBe('.till'); + + // When `save` is called, changes should be saved/persisted immediately + storage.save(); + await storage.whenSaved(); + expect(wrapper.setItem).toHaveBeenCalledWith('prefix', JSON.stringify({ key2: 'value2', '.till': '1' })); + + expect(wrapper.setItem).toHaveBeenCalledTimes(1); + + await storage.whenSaved(); + expect(wrapper.setItem).toHaveBeenCalledTimes(1); +}); diff --git a/src/storages/inLocalStorage/__tests__/validateCache.spec.ts b/src/storages/inLocalStorage/__tests__/validateCache.spec.ts index 2699d184..102444ba 100644 --- a/src/storages/inLocalStorage/__tests__/validateCache.spec.ts +++ b/src/storages/inLocalStorage/__tests__/validateCache.spec.ts @@ -6,16 +6,17 @@ import { SplitsCacheInLocal } from '../SplitsCacheInLocal'; import { nearlyEqual } from '../../../__tests__/testUtils'; import { MySegmentsCacheInLocal } from '../MySegmentsCacheInLocal'; import { RBSegmentsCacheInLocal } from '../RBSegmentsCacheInLocal'; +import { storages, PREFIX } from './wrapper.mock'; const FULL_SETTINGS_HASH = 'dc1f9817'; -describe('validateCache', () => { - const keys = new KeyBuilderCS('SPLITIO', 'user'); +describe.each(storages)('validateCache', (storage) => { + const keys = new KeyBuilderCS(PREFIX, 'user'); const logSpy = jest.spyOn(fullSettings.log, 'info'); - 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); + const segments = new MySegmentsCacheInLocal(fullSettings.log, keys, storage); + const largeSegments = new MySegmentsCacheInLocal(fullSettings.log, keys, storage); + const splits = new SplitsCacheInLocal(fullSettings, keys, storage); + const rbSegments = new RBSegmentsCacheInLocal(fullSettings, keys, storage); jest.spyOn(splits, 'getChangeNumber'); jest.spyOn(splits, 'clear'); @@ -25,11 +26,11 @@ describe('validateCache', () => { beforeEach(() => { jest.clearAllMocks(); - localStorage.clear(); + for (let i = 0; i < storage.length; i++) storage.removeItem(storage.key(i) as string); }); test('if there is no cache, it should return false', async () => { - expect(await validateCache({}, localStorage, fullSettings, keys, splits, rbSegments, segments, largeSegments)).toBe(false); + expect(await validateCache({}, storage, fullSettings, keys, splits, rbSegments, segments, largeSegments)).toBe(false); expect(logSpy).not.toHaveBeenCalled(); @@ -39,15 +40,15 @@ describe('validateCache', () => { 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(); + expect(storage.getItem(keys.buildHashKey())).toBe(FULL_SETTINGS_HASH); + expect(storage.getItem(keys.buildLastClear())).toBeNull(); }); test('if there is cache and it must not be cleared, it should return true', async () => { - localStorage.setItem(keys.buildSplitsTillKey(), '1'); - localStorage.setItem(keys.buildHashKey(), FULL_SETTINGS_HASH); + storage.setItem(keys.buildSplitsTillKey(), '1'); + storage.setItem(keys.buildHashKey(), FULL_SETTINGS_HASH); - expect(await validateCache({}, localStorage, fullSettings, keys, splits, rbSegments, segments, largeSegments)).toBe(true); + expect(await validateCache({}, storage, fullSettings, keys, splits, rbSegments, segments, largeSegments)).toBe(true); expect(logSpy).not.toHaveBeenCalled(); @@ -57,16 +58,16 @@ describe('validateCache', () => { 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(); + expect(storage.getItem(keys.buildHashKey())).toBe(FULL_SETTINGS_HASH); + expect(storage.getItem(keys.buildLastClear())).toBeNull(); }); test('if there is cache and it has expired, it should clear cache and return false', async () => { - 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 + storage.setItem(keys.buildSplitsTillKey(), '1'); + storage.setItem(keys.buildHashKey(), FULL_SETTINGS_HASH); + storage.setItem(keys.buildLastUpdatedKey(), Date.now() - 1000 * 60 * 60 * 24 * 2 + ''); // 2 days ago - expect(await validateCache({ expirationDays: 1 }, localStorage, fullSettings, keys, splits, rbSegments, segments, largeSegments)).toBe(false); + expect(await validateCache({ expirationDays: 1 }, storage, fullSettings, keys, splits, rbSegments, segments, largeSegments)).toBe(false); expect(logSpy).toHaveBeenCalledWith('storage:localstorage: Cache expired more than 1 days ago. Cleaning up cache'); @@ -75,15 +76,15 @@ describe('validateCache', () => { 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); + expect(storage.getItem(keys.buildHashKey())).toBe(FULL_SETTINGS_HASH); + expect(nearlyEqual(parseInt(storage.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', async () => { - localStorage.setItem(keys.buildSplitsTillKey(), '1'); - localStorage.setItem(keys.buildHashKey(), FULL_SETTINGS_HASH); + storage.setItem(keys.buildSplitsTillKey(), '1'); + storage.setItem(keys.buildHashKey(), FULL_SETTINGS_HASH); - expect(await validateCache({}, localStorage, { ...fullSettings, core: { ...fullSettings.core, authorizationKey: 'another-sdk-key' } }, keys, splits, rbSegments, segments, largeSegments)).toBe(false); + expect(await validateCache({}, storage, { ...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'); @@ -92,16 +93,16 @@ describe('validateCache', () => { expect(segments.clear).toHaveBeenCalledTimes(1); expect(largeSegments.clear).toHaveBeenCalledTimes(1); - expect(localStorage.getItem(keys.buildHashKey())).toBe('45c6ba5d'); - expect(nearlyEqual(parseInt(localStorage.getItem(keys.buildLastClear()) as string), Date.now())).toBe(true); + expect(storage.getItem(keys.buildHashKey())).toBe('45c6ba5d'); + expect(nearlyEqual(parseInt(storage.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', async () => { // Older cache version (without last clear) - localStorage.setItem(keys.buildSplitsTillKey(), '1'); - localStorage.setItem(keys.buildHashKey(), FULL_SETTINGS_HASH); + storage.setItem(keys.buildSplitsTillKey(), '1'); + storage.setItem(keys.buildHashKey(), FULL_SETTINGS_HASH); - expect(await validateCache({ clearOnInit: true }, localStorage, fullSettings, keys, splits, rbSegments, segments, largeSegments)).toBe(false); + expect(await validateCache({ clearOnInit: true }, storage, 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'); @@ -110,25 +111,25 @@ describe('validateCache', () => { 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(storage.getItem(keys.buildHashKey())).toBe(FULL_SETTINGS_HASH); + const lastClear = storage.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(await validateCache({ clearOnInit: true }, localStorage, fullSettings, keys, splits, rbSegments, segments, largeSegments)).toBe(true); + storage.setItem(keys.buildSplitsTillKey(), '1'); + expect(await validateCache({ clearOnInit: true }, storage, 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 + expect(storage.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 }, localStorage, fullSettings, keys, splits, rbSegments, segments, largeSegments)).toBe(false); + storage.setItem(keys.buildLastClear(), (Date.now() - 1000 * 60 * 60 * 24 - 1) + ''); + expect(await validateCache({ clearOnInit: true }, storage, 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); expect(segments.clear).toHaveBeenCalledTimes(2); expect(largeSegments.clear).toHaveBeenCalledTimes(2); - expect(nearlyEqual(parseInt(localStorage.getItem(keys.buildLastClear()) as string), Date.now())).toBe(true); + expect(nearlyEqual(parseInt(storage.getItem(keys.buildLastClear()) as string), Date.now())).toBe(true); }); }); diff --git a/src/storages/inLocalStorage/__tests__/wrapper.mock.ts b/src/storages/inLocalStorage/__tests__/wrapper.mock.ts new file mode 100644 index 00000000..897c13a0 --- /dev/null +++ b/src/storages/inLocalStorage/__tests__/wrapper.mock.ts @@ -0,0 +1,27 @@ +import { storageAdapter } from '../storageAdapter'; +import SplitIO from '../../../../types/splitio'; +import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; + +export const PREFIX = 'SPLITIO'; + +export function createMemoryStorage(): SplitIO.AsyncStorageWrapper { + let cache: Record = {}; + return { + getItem(key: string) { + return Promise.resolve(cache[key] || null); + }, + setItem(key: string, value: string) { + cache[key] = value; + return Promise.resolve(); + }, + removeItem(key: string) { + delete cache[key]; + return Promise.resolve(); + } + }; +} + +export const storages = [ + localStorage, + storageAdapter(loggerMock, PREFIX, createMemoryStorage()) +]; diff --git a/src/storages/inLocalStorage/index.ts b/src/storages/inLocalStorage/index.ts index 736d1f7b..ae77bb41 100644 --- a/src/storages/inLocalStorage/index.ts +++ b/src/storages/inLocalStorage/index.ts @@ -1,10 +1,10 @@ import { ImpressionsCacheInMemory } from '../inMemory/ImpressionsCacheInMemory'; import { ImpressionCountsCacheInMemory } from '../inMemory/ImpressionCountsCacheInMemory'; import { EventsCacheInMemory } from '../inMemory/EventsCacheInMemory'; -import { IStorageFactoryParams, IStorageSync, IStorageSyncFactory } from '../types'; +import { IStorageFactoryParams, IStorageSync, IStorageSyncFactory, StorageAdapter } from '../types'; import { validatePrefix } from '../KeyBuilder'; import { KeyBuilderCS, myLargeSegmentsKeyBuilder } from '../KeyBuilderCS'; -import { isLocalStorageAvailable } from '../../utils/env/isLocalStorageAvailable'; +import { isLocalStorageAvailable, isValidStorageWrapper } from '../../utils/env/isLocalStorageAvailable'; import { SplitsCacheInLocal } from './SplitsCacheInLocal'; import { RBSegmentsCacheInLocal } from './RBSegmentsCacheInLocal'; import { MySegmentsCacheInLocal } from './MySegmentsCacheInLocal'; @@ -17,8 +17,14 @@ import { getMatching } from '../../utils/key'; import { validateCache } from './validateCache'; import { ILogger } from '../../logger/types'; import SplitIO from '../../../types/splitio'; +import { storageAdapter } from './storageAdapter'; + +function validateStorage(log: ILogger, prefix: string, wrapper?: SplitIO.SyncStorageWrapper | SplitIO.AsyncStorageWrapper): StorageAdapter | undefined { + if (wrapper) { + if (isValidStorageWrapper(wrapper)) return storageAdapter(log, prefix, wrapper); + log.warn(LOG_PREFIX + 'Invalid storage provided. Falling back to LocalStorage API'); + } -function validateStorage(log: ILogger) { if (isLocalStorageAvailable()) return localStorage; log.warn(LOG_PREFIX + 'LocalStorage API is unavailable. Falling back to default MEMORY storage'); @@ -34,7 +40,7 @@ export function InLocalStorage(options: SplitIO.InLocalStorageOptions = {}): ISt function InLocalStorageCSFactory(params: IStorageFactoryParams): IStorageSync { const { settings, settings: { log, scheduler: { impressionsQueueSize, eventsQueueSize } } } = params; - const storage = validateStorage(log); + const storage = validateStorage(log, prefix, options.wrapper); if (!storage) return InMemoryStorageCSFactory(params); const matchingKey = getMatching(settings.core.key); @@ -61,8 +67,12 @@ export function InLocalStorage(options: SplitIO.InLocalStorageOptions = {}): ISt return validateCachePromise || (validateCachePromise = validateCache(options, storage, settings, keys, splits, rbSegments, segments, largeSegments)); }, + save() { + return storage.save && storage.save(); + }, + destroy() { - return Promise.resolve(); + return storage.whenSaved && storage.whenSaved(); }, // When using shared instantiation with MEMORY we reuse everything but segments (they are customer per key). diff --git a/src/storages/inLocalStorage/storageAdapter.ts b/src/storages/inLocalStorage/storageAdapter.ts new file mode 100644 index 00000000..af92df22 --- /dev/null +++ b/src/storages/inLocalStorage/storageAdapter.ts @@ -0,0 +1,62 @@ +import { ILogger } from '../../logger/types'; +import SplitIO from '../../../types/splitio'; +import { LOG_PREFIX } from './constants'; +import { StorageAdapter } from '../types'; + + +export function storageAdapter(log: ILogger, prefix: string, wrapper: SplitIO.SyncStorageWrapper | SplitIO.AsyncStorageWrapper): Required { + let keys: string[] = []; + let cache: Record = {}; + + let loadPromise: Promise | undefined; + let savePromise = Promise.resolve(); + + return { + load() { + return loadPromise || (loadPromise = Promise.resolve().then(() => { + return wrapper.getItem(prefix); + }).then((storedCache) => { + cache = JSON.parse(storedCache || '{}'); + keys = Object.keys(cache); + }).catch((e) => { + log.error(LOG_PREFIX + 'Rejected promise calling wrapper `getItem` method, with error: ' + e); + })); + }, + + save() { + return savePromise = savePromise.then(() => { + return Promise.resolve(wrapper.setItem(prefix, JSON.stringify(cache))); + }).catch((e) => { + log.error(LOG_PREFIX + 'Rejected promise calling wrapper `setItem` method, with error: ' + e); + }); + }, + + whenSaved() { + return savePromise; + }, + + get length() { + return keys.length; + }, + + getItem(key: string) { + return cache[key] || null; + }, + + key(index: number) { + return keys[index] || null; + }, + + removeItem(key: string) { + const index = keys.indexOf(key); + if (index === -1) return; + keys.splice(index, 1); + delete cache[key]; + }, + + setItem(key: string, value: string) { + if (keys.indexOf(key) === -1) keys.push(key); + cache[key] = value; + } + }; +} diff --git a/src/storages/inLocalStorage/validateCache.ts b/src/storages/inLocalStorage/validateCache.ts index c5adf199..38df9899 100644 --- a/src/storages/inLocalStorage/validateCache.ts +++ b/src/storages/inLocalStorage/validateCache.ts @@ -70,7 +70,7 @@ function validateExpiration(options: SplitIO.InLocalStorageOptions, storage: Sto */ 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(() => { + return Promise.resolve(storage.load && storage.load()).then(() => { const currentTimestamp = Date.now(); const isThereCache = splits.getChangeNumber() > -1; @@ -87,6 +87,9 @@ export function validateCache(options: SplitIO.InLocalStorageOptions, storage: S settings.log.error(LOG_PREFIX + e); } + // Persist clear + if (storage.save) storage.save(); + return false; } diff --git a/src/storages/types.ts b/src/storages/types.ts index 8de14402..307fee3e 100644 --- a/src/storages/types.ts +++ b/src/storages/types.ts @@ -9,6 +9,11 @@ import { ISettings } from '../types'; * (https://developer.mozilla.org/en-US/docs/Web/API/Storage) used by the SDK */ export interface StorageAdapter { + // Methods to support async storages + load?: () => Promise; + save?: () => Promise; + whenSaved?: () => Promise; + // Methods based on https://developer.mozilla.org/en-US/docs/Web/API/Storage readonly length: number; key(index: number): string | null; getItem(key: string): string | null; @@ -478,6 +483,7 @@ export interface IStorageBase< uniqueKeys: TUniqueKeysCache, destroy(): void | Promise, shared?: (matchingKey: string, onReadyCb: (error?: any) => void) => this + save?: () => void | Promise, } export interface IStorageSync extends IStorageBase< diff --git a/src/sync/polling/updaters/mySegmentsUpdater.ts b/src/sync/polling/updaters/mySegmentsUpdater.ts index 5de512fa..4b6038c5 100644 --- a/src/sync/polling/updaters/mySegmentsUpdater.ts +++ b/src/sync/polling/updaters/mySegmentsUpdater.ts @@ -51,6 +51,8 @@ export function mySegmentsUpdaterFactory( shouldNotifyUpdate = largeSegments!.resetSegments((segmentsData as IMembershipsResponse).ls || {}) || shouldNotifyUpdate; } + if (storage.save) storage.save(); + // Notify update if required if (usesSegmentsSync(storage) && (shouldNotifyUpdate || readyOnAlreadyExistentState)) { readyOnAlreadyExistentState = false; diff --git a/src/sync/polling/updaters/splitChangesUpdater.ts b/src/sync/polling/updaters/splitChangesUpdater.ts index 0331bc43..6c6371e3 100644 --- a/src/sync/polling/updaters/splitChangesUpdater.ts +++ b/src/sync/polling/updaters/splitChangesUpdater.ts @@ -117,7 +117,7 @@ export function computeMutation(rules: Array, export function splitChangesUpdaterFactory( log: ILogger, splitChangesFetcher: ISplitChangesFetcher, - storage: Pick, + storage: Pick, splitFiltersValidation: ISplitFiltersValidation, splitsEventEmitter?: ISplitsEventEmitter, requestTimeoutBeforeReady: number = 0, @@ -185,6 +185,8 @@ export function splitChangesUpdaterFactory( // @TODO if at least 1 segment fetch fails due to 404 and other segments are updated in the storage, SDK_UPDATE is not emitted segments.registerSegments(setToArray(usedSegments)) ]).then(([ffChanged, rbsChanged]) => { + if (storage.save) storage.save(); + if (splitsEventEmitter) { // To emit SDK_SPLITS_ARRIVED for server-side SDK, we must check that all registered segments have been fetched return Promise.resolve(!splitsEventEmitter.splitsArrived || ((ffChanged || rbsChanged) && (isClientSide || checkAllSegmentsExist(segments)))) diff --git a/src/utils/env/isLocalStorageAvailable.ts b/src/utils/env/isLocalStorageAvailable.ts index e062b57d..d1bdc8e2 100644 --- a/src/utils/env/isLocalStorageAvailable.ts +++ b/src/utils/env/isLocalStorageAvailable.ts @@ -1,9 +1,18 @@ -/* eslint-disable no-undef */ export function isLocalStorageAvailable(): boolean { + try { + // eslint-disable-next-line no-undef + return isValidStorageWrapper(localStorage); + } catch (e) { + return false; + } +} + +export function isValidStorageWrapper(wrapper: any): boolean { var mod = '__SPLITSOFTWARE__'; try { - localStorage.setItem(mod, mod); - localStorage.removeItem(mod); + wrapper.setItem(mod, mod); + wrapper.getItem(mod); + wrapper.removeItem(mod); return true; } catch (e) { return false; diff --git a/types/splitio.d.ts b/types/splitio.d.ts index e85ab01b..93635db4 100644 --- a/types/splitio.d.ts +++ b/types/splitio.d.ts @@ -458,6 +458,36 @@ interface IClientSideSyncSharedSettings extends IClientSideSharedSettings, ISync */ declare namespace SplitIO { + interface SyncStorageWrapper { + /** + * Returns the value associated with the given key, or null if the key does not exist. + */ + getItem(key: string): string | null; + /** + * Sets the value for the given key, creating a new key/value pair if key does not exist. + */ + setItem(key: string, value: string): void; + /** + * Removes the key/value pair for the given key, if the key exists. + */ + removeItem(key: string): void; + } + + interface AsyncStorageWrapper { + /** + * Returns a promise that resolves to the value associated with the given key, or null if the key does not exist. + */ + getItem(key: string): Promise; + /** + * Returns a promise that resolves when the value of the pair identified by key is set to value, creating a new key/value pair if key does not exist. + */ + setItem(key: string, value: string): Promise; + /** + * Returns a promise that resolves when the key/value pair for the given key is removed, if the key exists. + */ + removeItem(key: string): Promise; + } + /** * EventEmitter interface based on a subset of the Node.js EventEmitter methods. */ @@ -972,6 +1002,12 @@ declare namespace SplitIO { * @defaultValue `false` */ clearOnInit?: boolean; + /** + * Optional storage wrapper to persist rollout plan related data. If not provided, the SDK will use the default localStorage Web API. + * + * @defaultValue `window.localStorage` + */ + wrapper?: SyncStorageWrapper | AsyncStorageWrapper; } /** * Storage for asynchronous (consumer) SDK. @@ -1312,6 +1348,12 @@ declare namespace SplitIO { * @defaultValue `false` */ clearOnInit?: boolean; + /** + * Optional storage wrapper to persist rollout plan related data. If not provided, the SDK will use the default localStorage Web API. + * + * @defaultValue `window.localStorage` + */ + wrapper?: SyncStorageWrapper | AsyncStorageWrapper; }; } /**