Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGES.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
2.6.0 (September 19, 2025)
- Added `storage.wrapper` configuration option to allow the SDK to use a custom storage wrapper for the storage type `LOCALSTORAGE`. Default value is `window.localStorage`.

2.5.0 (September 10, 2025)
- Added `factory.getRolloutPlan()` method for standalone server-side SDKs, which returns the rollout plan snapshot from the storage.
- Added `initialRolloutPlan` configuration option for standalone client-side SDKs, which allows preloading the SDK storage with a snapshot of the rollout plan.
Expand Down
34 changes: 31 additions & 3 deletions src/storages/inLocalStorage/__tests__/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ jest.mock('../../inMemory/InMemoryStorageCS', () => {
import { IStorageFactoryParams } from '../../types';
import { assertStorageInterface } from '../../__tests__/testUtils';
import { fullSettings } from '../../../utils/settingsValidation/__tests__/settings.mocks';
import { createMemoryStorage } from './wrapper.mock';
import * as storageAdapter from '../storageAdapter';

const storageAdapterSpy = jest.spyOn(storageAdapter, 'storageAdapter');

// Test target
import { InLocalStorage } from '../index';
Expand All @@ -23,7 +27,7 @@ describe('IN LOCAL STORAGE', () => {
fakeInMemoryStorageFactory.mockClear();
});

test('calls InMemoryStorage factory if LocalStorage API is not available or the provided storage wrapper is invalid', () => {
test('fallback to InMemoryStorage 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', {});
Expand All @@ -40,22 +44,46 @@ describe('IN LOCAL STORAGE', () => {
expect(storage).toBe(fakeInMemoryStorage);

// Provided storage is valid
storageFactory = InLocalStorage({ prefix: 'prefix', wrapper: { getItem: () => Promise.resolve(null), setItem: () => Promise.resolve(), removeItem: () => Promise.resolve() } });
storageFactory = InLocalStorage({ prefix: 'prefix', wrapper: createMemoryStorage() });
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', () => {
test('calls InLocalStorage if LocalStorage API is available', () => {

const storageFactory = InLocalStorage({ prefix: 'prefix' });
const storage = storageFactory(internalSdkParams);

assertStorageInterface(storage); // the instance must implement the storage interface
expect(fakeInMemoryStorageFactory).not.toBeCalled(); // doesn't call InMemoryStorage factory
});

test('calls InLocalStorage if the provided storage wrapper is valid', () => {
storageAdapterSpy.mockClear();

// Web Storages should not use the storageAdapter
let storageFactory = InLocalStorage({ prefix: 'prefix', wrapper: localStorage });
let storage = storageFactory(internalSdkParams);
assertStorageInterface(storage);
expect(fakeInMemoryStorageFactory).not.toBeCalled();
expect(storageAdapterSpy).not.toBeCalled();

storageFactory = InLocalStorage({ prefix: 'prefix', wrapper: sessionStorage });
storage = storageFactory(internalSdkParams);
assertStorageInterface(storage);
expect(fakeInMemoryStorageFactory).not.toBeCalled();
expect(storageAdapterSpy).not.toBeCalled();

// Non Web Storages should use the storageAdapter
storageFactory = InLocalStorage({ prefix: 'prefix', wrapper: createMemoryStorage() });
storage = storageFactory(internalSdkParams);

assertStorageInterface(storage);
expect(fakeInMemoryStorageFactory).not.toBeCalled();
expect(storageAdapterSpy).toBeCalled();
});

});
8 changes: 6 additions & 2 deletions src/storages/inLocalStorage/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { EventsCacheInMemory } from '../inMemory/EventsCacheInMemory';
import { IStorageFactoryParams, IStorageSync, IStorageSyncFactory, StorageAdapter } from '../types';
import { validatePrefix } from '../KeyBuilder';
import { KeyBuilderCS, myLargeSegmentsKeyBuilder } from '../KeyBuilderCS';
import { isLocalStorageAvailable, isValidStorageWrapper } from '../../utils/env/isLocalStorageAvailable';
import { isLocalStorageAvailable, isValidStorageWrapper, isWebStorage } from '../../utils/env/isLocalStorageAvailable';
import { SplitsCacheInLocal } from './SplitsCacheInLocal';
import { RBSegmentsCacheInLocal } from './RBSegmentsCacheInLocal';
import { MySegmentsCacheInLocal } from './MySegmentsCacheInLocal';
Expand All @@ -21,7 +21,11 @@ import { storageAdapter } from './storageAdapter';

function validateStorage(log: ILogger, prefix: string, wrapper?: SplitIO.StorageWrapper): StorageAdapter | undefined {
if (wrapper) {
if (isValidStorageWrapper(wrapper)) return storageAdapter(log, prefix, wrapper);
if (isValidStorageWrapper(wrapper)) {
return isWebStorage(wrapper) ?
wrapper as StorageAdapter: // localStorage and sessionStorage don't need adapter
storageAdapter(log, prefix, wrapper);
}
log.warn(LOG_PREFIX + 'Invalid storage provided. Falling back to LocalStorage API');
}

Expand Down
12 changes: 12 additions & 0 deletions src/utils/env/isLocalStorageAvailable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,15 @@ export function isValidStorageWrapper(wrapper: any): boolean {
typeof wrapper.getItem === 'function' &&
typeof wrapper.removeItem === 'function';
}

export function isWebStorage(wrapper: any): boolean {
if (typeof wrapper.length === 'number') {
try {
wrapper.key(0);
return true;
} catch (e) {
return false;
}
}
return false;
}