Skip to content

Commit daa5c31

Browse files
Merge pull request #423 from splitio/inlocalstorage_sessionStorage
[Custom InLocalStorage] Handle the case where the user passes the `window.localStorage` or `window.sessionStorage` object
2 parents f45dcc7 + 0c22f4d commit daa5c31

File tree

4 files changed

+52
-5
lines changed

4 files changed

+52
-5
lines changed

CHANGES.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
2.6.0 (September 19, 2025)
2+
- 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`.
3+
14
2.5.0 (September 10, 2025)
25
- Added `factory.getRolloutPlan()` method for standalone server-side SDKs, which returns the rollout plan snapshot from the storage.
36
- Added `initialRolloutPlan` configuration option for standalone client-side SDKs, which allows preloading the SDK storage with a snapshot of the rollout plan.

src/storages/inLocalStorage/__tests__/index.spec.ts

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ jest.mock('../../inMemory/InMemoryStorageCS', () => {
1010
import { IStorageFactoryParams } from '../../types';
1111
import { assertStorageInterface } from '../../__tests__/testUtils';
1212
import { fullSettings } from '../../../utils/settingsValidation/__tests__/settings.mocks';
13+
import { createMemoryStorage } from './wrapper.mock';
14+
import * as storageAdapter from '../storageAdapter';
15+
16+
const storageAdapterSpy = jest.spyOn(storageAdapter, 'storageAdapter');
1317

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

26-
test('calls InMemoryStorage factory if LocalStorage API is not available or the provided storage wrapper is invalid', () => {
30+
test('fallback to InMemoryStorage if LocalStorage API is not available or the provided storage wrapper is invalid', () => {
2731
// Delete global localStorage property
2832
const originalLocalStorage = Object.getOwnPropertyDescriptor(global, 'localStorage');
2933
Object.defineProperty(global, 'localStorage', {});
@@ -40,22 +44,46 @@ describe('IN LOCAL STORAGE', () => {
4044
expect(storage).toBe(fakeInMemoryStorage);
4145

4246
// Provided storage is valid
43-
storageFactory = InLocalStorage({ prefix: 'prefix', wrapper: { getItem: () => Promise.resolve(null), setItem: () => Promise.resolve(), removeItem: () => Promise.resolve() } });
47+
storageFactory = InLocalStorage({ prefix: 'prefix', wrapper: createMemoryStorage() });
4448
storage = storageFactory(internalSdkParams);
4549
expect(storage).not.toBe(fakeInMemoryStorage);
4650

4751
// Restore original localStorage
4852
Object.defineProperty(global, 'localStorage', originalLocalStorage as PropertyDescriptor);
4953
});
5054

51-
test('calls its own storage factory if LocalStorage API is available', () => {
55+
test('calls InLocalStorage if LocalStorage API is available', () => {
5256

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

5660
assertStorageInterface(storage); // the instance must implement the storage interface
5761
expect(fakeInMemoryStorageFactory).not.toBeCalled(); // doesn't call InMemoryStorage factory
62+
});
63+
64+
test('calls InLocalStorage if the provided storage wrapper is valid', () => {
65+
storageAdapterSpy.mockClear();
66+
67+
// Web Storages should not use the storageAdapter
68+
let storageFactory = InLocalStorage({ prefix: 'prefix', wrapper: localStorage });
69+
let storage = storageFactory(internalSdkParams);
70+
assertStorageInterface(storage);
71+
expect(fakeInMemoryStorageFactory).not.toBeCalled();
72+
expect(storageAdapterSpy).not.toBeCalled();
73+
74+
storageFactory = InLocalStorage({ prefix: 'prefix', wrapper: sessionStorage });
75+
storage = storageFactory(internalSdkParams);
76+
assertStorageInterface(storage);
77+
expect(fakeInMemoryStorageFactory).not.toBeCalled();
78+
expect(storageAdapterSpy).not.toBeCalled();
79+
80+
// Non Web Storages should use the storageAdapter
81+
storageFactory = InLocalStorage({ prefix: 'prefix', wrapper: createMemoryStorage() });
82+
storage = storageFactory(internalSdkParams);
5883

84+
assertStorageInterface(storage);
85+
expect(fakeInMemoryStorageFactory).not.toBeCalled();
86+
expect(storageAdapterSpy).toBeCalled();
5987
});
6088

6189
});

src/storages/inLocalStorage/index.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { EventsCacheInMemory } from '../inMemory/EventsCacheInMemory';
44
import { IStorageFactoryParams, IStorageSync, IStorageSyncFactory, StorageAdapter } from '../types';
55
import { validatePrefix } from '../KeyBuilder';
66
import { KeyBuilderCS, myLargeSegmentsKeyBuilder } from '../KeyBuilderCS';
7-
import { isLocalStorageAvailable, isValidStorageWrapper } from '../../utils/env/isLocalStorageAvailable';
7+
import { isLocalStorageAvailable, isValidStorageWrapper, isWebStorage } from '../../utils/env/isLocalStorageAvailable';
88
import { SplitsCacheInLocal } from './SplitsCacheInLocal';
99
import { RBSegmentsCacheInLocal } from './RBSegmentsCacheInLocal';
1010
import { MySegmentsCacheInLocal } from './MySegmentsCacheInLocal';
@@ -21,7 +21,11 @@ import { storageAdapter } from './storageAdapter';
2121

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

src/utils/env/isLocalStorageAvailable.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,15 @@ export function isValidStorageWrapper(wrapper: any): boolean {
1717
typeof wrapper.getItem === 'function' &&
1818
typeof wrapper.removeItem === 'function';
1919
}
20+
21+
export function isWebStorage(wrapper: any): boolean {
22+
if (typeof wrapper.length === 'number') {
23+
try {
24+
wrapper.key(0);
25+
return true;
26+
} catch (e) {
27+
return false;
28+
}
29+
}
30+
return false;
31+
}

0 commit comments

Comments
 (0)