From e0ef9cd0ba1821317cca7c5aa7f1a2bfd6e6128f Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 30 Jul 2025 17:51:43 -0300 Subject: [PATCH 1/4] feat: add WebStorage type check and optimize storage adapter usage --- src/storages/inLocalStorage/index.ts | 8 ++++++-- src/utils/env/isLocalStorageAvailable.ts | 12 ++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/storages/inLocalStorage/index.ts b/src/storages/inLocalStorage/index.ts index d6b0691f..c83abc59 100644 --- a/src/storages/inLocalStorage/index.ts +++ b/src/storages/inLocalStorage/index.ts @@ -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'; @@ -66,7 +66,11 @@ export function storageAdapter(log: ILogger, prefix: string, wrapper: SplitIO.St 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'); } diff --git a/src/utils/env/isLocalStorageAvailable.ts b/src/utils/env/isLocalStorageAvailable.ts index d1bdc8e2..10a7dba8 100644 --- a/src/utils/env/isLocalStorageAvailable.ts +++ b/src/utils/env/isLocalStorageAvailable.ts @@ -18,3 +18,15 @@ export function isValidStorageWrapper(wrapper: any): boolean { return false; } } + +export function isWebStorage(wrapper: any): boolean { + if (typeof wrapper.length === 'number') { + try { + wrapper.key(0); + return true; + } catch (e) { + return false; + } + } + return false; +} From 1cae832d95dc6c15ae1798a6501f23e8fe1b7d61 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 31 Jul 2025 12:11:50 -0300 Subject: [PATCH 2/4] unit tests --- .../inLocalStorage/__tests__/index.spec.ts | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/src/storages/inLocalStorage/__tests__/index.spec.ts b/src/storages/inLocalStorage/__tests__/index.spec.ts index 545c532f..cc45c99c 100644 --- a/src/storages/inLocalStorage/__tests__/index.spec.ts +++ b/src/storages/inLocalStorage/__tests__/index.spec.ts @@ -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'; @@ -40,7 +44,7 @@ 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); @@ -55,7 +59,31 @@ describe('IN LOCAL STORAGE', () => { assertStorageInterface(storage); // the instance must implement the storage interface expect(fakeInMemoryStorageFactory).not.toBeCalled(); // doesn't call InMemoryStorage factory + }); + + test('calls its own storage factory 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(); }); }); From f1f5bf5b5ab69074bc27f2cc910ec360123994a4 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 31 Jul 2025 12:19:21 -0300 Subject: [PATCH 3/4] rc --- CHANGES.txt | 3 +++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 4a18088b..cacc2904 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,6 @@ +2.5.0 (August XX, 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.4.1 (June 3, 2025) - Bugfix - Improved the Proxy fallback to flag spec version 1.2 to handle cases where the Proxy does not return an end-of-stream marker in 400 status code responses. diff --git a/package-lock.json b/package-lock.json index aa7cf6d8..661ab4c6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@splitsoftware/splitio-commons", - "version": "2.4.1", + "version": "2.4.2-rc.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@splitsoftware/splitio-commons", - "version": "2.4.1", + "version": "2.4.2-rc.2", "license": "Apache-2.0", "dependencies": { "@types/ioredis": "^4.28.0", diff --git a/package.json b/package.json index 27b15da2..d46d7735 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "2.4.1", + "version": "2.4.2-rc.2", "description": "Split JavaScript SDK common components", "main": "cjs/index.js", "module": "esm/index.js", From a7edc8dd68b3adecae16a56c28faee3c5144e034 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 12 Aug 2025 17:29:57 -0300 Subject: [PATCH 4/4] rc --- package-lock.json | 4 ++-- package.json | 2 +- src/storages/inLocalStorage/__tests__/index.spec.ts | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 661ab4c6..c9900fcb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@splitsoftware/splitio-commons", - "version": "2.4.2-rc.2", + "version": "2.4.2-rc.3", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@splitsoftware/splitio-commons", - "version": "2.4.2-rc.2", + "version": "2.4.2-rc.3", "license": "Apache-2.0", "dependencies": { "@types/ioredis": "^4.28.0", diff --git a/package.json b/package.json index d46d7735..027c1597 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "2.4.2-rc.2", + "version": "2.4.2-rc.3", "description": "Split JavaScript SDK common components", "main": "cjs/index.js", "module": "esm/index.js", diff --git a/src/storages/inLocalStorage/__tests__/index.spec.ts b/src/storages/inLocalStorage/__tests__/index.spec.ts index cc45c99c..3341871d 100644 --- a/src/storages/inLocalStorage/__tests__/index.spec.ts +++ b/src/storages/inLocalStorage/__tests__/index.spec.ts @@ -27,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', {}); @@ -52,7 +52,7 @@ describe('IN LOCAL STORAGE', () => { 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); @@ -61,7 +61,7 @@ describe('IN LOCAL STORAGE', () => { expect(fakeInMemoryStorageFactory).not.toBeCalled(); // doesn't call InMemoryStorage factory }); - test('calls its own storage factory if the provided storage wrapper is valid', () => { + test('calls InLocalStorage if the provided storage wrapper is valid', () => { storageAdapterSpy.mockClear(); // Web Storages should not use the storageAdapter