From a6268840a18c61c3b4f011312eadcc3789f8a5a5 Mon Sep 17 00:00:00 2001 From: Md Junaed Hossain <169046794+junaed-optimizely@users.noreply.github.com> Date: Mon, 7 Sep 2026 23:02:48 +0600 Subject: [PATCH] [FSSDK-13110] cache update --- .../event_processor_factory.browser.ts | 5 +-- .../event_processor_factory.react_native.ts | 3 +- lib/event_processor/event_store.spec.ts | 44 ++++++++++--------- lib/event_processor/event_store.ts | 8 ++-- .../polling_datafile_manager.spec.ts | 14 +++--- .../polling_datafile_manager.ts | 2 +- .../async_storage_cache.react_native.spec.ts | 37 +++++----------- .../cache/async_storage_cache.react_native.ts | 14 +++--- .../cache/local_storage_cache.browser.spec.ts | 35 +++++---------- .../cache/local_storage_cache.browser.ts | 13 +++--- lib/vuid/vuid_manager_factory.browser.ts | 2 +- lib/vuid/vuid_manager_factory.react_native.ts | 2 +- 12 files changed, 74 insertions(+), 105 deletions(-) diff --git a/lib/event_processor/event_processor_factory.browser.ts b/lib/event_processor/event_processor_factory.browser.ts index a4d782c7a..e55436236 100644 --- a/lib/event_processor/event_processor_factory.browser.ts +++ b/lib/event_processor/event_processor_factory.browser.ts @@ -15,8 +15,7 @@ */ import { EventDispatcher } from './event_dispatcher/event_dispatcher'; import { EventProcessor } from './event_processor'; -import { EventWithId } from './batch_event_processor'; -import { +import { getOpaqueBatchEventProcessor, BatchEventProcessorOptions, OpaqueEventProcessor, @@ -44,7 +43,7 @@ export const createBatchEventProcessor = ( options: BatchEventProcessorOptions = {} ): OpaqueEventProcessor => { const eventStore = options.eventStore ? getPrefixEventStore(options.eventStore) : new EventStore({ - store: new LocalStorageCache(), + store: new LocalStorageCache(), maxSize: options.batchSize ? Math.max(options.batchSize * 2, DEFAULT_MAX_EVENTS_IN_STORE) : DEFAULT_MAX_EVENTS_IN_STORE, ttl: options.storeTtl, diff --git a/lib/event_processor/event_processor_factory.react_native.ts b/lib/event_processor/event_processor_factory.react_native.ts index 1b4f58dc9..950bddd75 100644 --- a/lib/event_processor/event_processor_factory.react_native.ts +++ b/lib/event_processor/event_processor_factory.react_native.ts @@ -24,7 +24,6 @@ import { getForwardingEventProcessor, } from './event_processor_factory'; import { FAILED_EVENT_RETRY_INTERVAL } from './event_processor_factory'; -import { EventWithId } from './batch_event_processor'; import { AsyncStorageCache } from '../utils/cache/async_storage_cache.react_native'; import { ReactNativeNetInfoEventProcessor } from './batch_event_processor.react_native'; import { DEFAULT_MAX_EVENTS_IN_STORE, EventStore } from './event_store'; @@ -43,7 +42,7 @@ export const createBatchEventProcessor = ( options: BatchEventProcessorOptions = {} ): OpaqueEventProcessor => { const eventStore = options.eventStore ? getPrefixEventStore(options.eventStore) : new EventStore({ - store: new AsyncStorageCache(), + store: new AsyncStorageCache(), maxSize: options.batchSize ? Math.max(options.batchSize * 2, DEFAULT_MAX_EVENTS_IN_STORE) : DEFAULT_MAX_EVENTS_IN_STORE, ttl: options.storeTtl, diff --git a/lib/event_processor/event_store.spec.ts b/lib/event_processor/event_store.spec.ts index 785eea183..c45eea214 100644 --- a/lib/event_processor/event_store.spec.ts +++ b/lib/event_processor/event_store.spec.ts @@ -30,7 +30,7 @@ type TestStoreConfig = { } const getEventStore = (config: TestStoreConfig = {}) => { - const mockStore = getMockAsyncCache(); + const mockStore = getMockAsyncCache(); const store = new EventStore({...config, store: mockStore }); return { mockStore, store } } @@ -213,17 +213,18 @@ describe('EventStore', () => { const originalSet = mockStore.set.bind(mockStore); let call = 0; - const setSpy = vi.spyOn(mockStore, 'set').mockImplementation(async (key: string, value: StoredEvent) => { + const setSpy = vi.spyOn(mockStore, 'set').mockImplementation(async (key: string, value: string) => { if (call++ > 0) { return originalSet(key, value); } // Simulate old stored event without time info + const stored: StoredEvent = JSON.parse(value); const eventWithoutTime: StoredEvent = { - id: value.id, - event: value.event, + id: stored.id, + event: stored.event, }; - return originalSet(key, eventWithoutTime); + return originalSet(key, JSON.stringify(eventWithoutTime)); }); await store.set('test', event); @@ -235,12 +236,12 @@ describe('EventStore', () => { await exhaustMicrotasks(); expect(setSpy).toHaveBeenCalledTimes(2); - const secondCall = setSpy.mock.calls[1]; + const resavedEvent: StoredEvent = JSON.parse(setSpy.mock.calls[1][1]); - expect(secondCall[1]._time).toBeDefined(); - expect(secondCall[1]._time?.storedAt).toBeLessThanOrEqual(Date.now()); - expect(secondCall[1]._time?.storedAt).toBeGreaterThanOrEqual(Date.now() - 10); - expect(secondCall[1]._time?.ttl).toBe(ttl); + expect(resavedEvent._time).toBeDefined(); + expect(resavedEvent._time?.storedAt).toBeLessThanOrEqual(Date.now()); + expect(resavedEvent._time?.storedAt).toBeGreaterThanOrEqual(Date.now() - 10); + expect(resavedEvent._time?.ttl).toBe(ttl); }); it('should store event when key expires after store being full', async () => { @@ -327,24 +328,25 @@ describe('EventStore', () => { const originalSet = mockStore.set.bind(mockStore); let call = 0; - const setSpy = vi.spyOn(mockStore, 'set').mockImplementation(async (key: string, value: StoredEvent) => { + const setSpy = vi.spyOn(mockStore, 'set').mockImplementation(async (key: string, value: string) => { if (call++ > 0) { return originalSet(key, value); } // Simulate old stored event without time information + const stored: StoredEvent = JSON.parse(value); const eventWithoutTime: StoredEvent = { - id: value.id, - event: value.event, + id: stored.id, + event: stored.event, }; - return originalSet(key, eventWithoutTime); + return originalSet(key, JSON.stringify(eventWithoutTime)); }); await store.set('key-1', event); await store.set('key-2', event); const results = await store.getBatched(['key-1', 'key-2']); - + expect(results).toHaveLength(2); expect(results[0]).toEqual(expect.objectContaining(event)); expect(results[1]).toEqual(expect.objectContaining(event)); @@ -352,12 +354,12 @@ describe('EventStore', () => { await exhaustMicrotasks(); expect(setSpy).toHaveBeenCalledTimes(3); - const secondCall = setSpy.mock.calls[1]; + const resavedEvent: StoredEvent = JSON.parse(setSpy.mock.calls[1][1]); - expect(secondCall[1]._time).toBeDefined(); - expect(secondCall[1]._time?.storedAt).toBeLessThanOrEqual(Date.now()); - expect(secondCall[1]._time?.storedAt).toBeGreaterThanOrEqual(Date.now() - 10); - expect(secondCall[1]._time?.ttl).toBe(ttl); + expect(resavedEvent._time).toBeDefined(); + expect(resavedEvent._time?.storedAt).toBeLessThanOrEqual(Date.now()); + expect(resavedEvent._time?.storedAt).toBeGreaterThanOrEqual(Date.now() - 10); + expect(resavedEvent._time?.ttl).toBe(ttl); }); it('should store event when keys expire during getBatched after store being full', async () => { @@ -401,7 +403,7 @@ describe('EventStore', () => { const originalSet = mockStore.set.bind(mockStore); let call = 0; - vi.spyOn(mockStore, 'set').mockImplementation(async (key: string, value: StoredEvent) => { + vi.spyOn(mockStore, 'set').mockImplementation(async (key: string, value: string) => { // only the seconde set call should fail if (call++ != 1) return originalSet(key, value); return Promise.reject(new Error('Simulated set failure')); diff --git a/lib/event_processor/event_store.ts b/lib/event_processor/event_store.ts index 90ccf9ff6..627848494 100644 --- a/lib/event_processor/event_store.ts +++ b/lib/event_processor/event_store.ts @@ -20,8 +20,6 @@ export type StoredEvent = EventWithId & { }; }; -const identity = (v: T): T => v; - const LOGGER_NAME = 'EventStore'; export const DEFAULT_MAX_EVENTS_IN_STORE = 500; export const DEFAULT_STORE_TTL = 10 * 24 * 60 * 60 * 1000; // 10 days @@ -31,7 +29,7 @@ export const EVENT_STORE_PREFIX = 'optly_event:'; export type EventStoreConfig = { maxSize?: number; ttl?: number, - store: Store, + store: Store, logger?: LoggerFacade, }; @@ -56,9 +54,9 @@ export class EventStore extends AsyncStoreWithBatchedGet implements } = config; if (store.operation === 'sync') { - this.store = new SyncPrefixStore(store, EVENT_STORE_PREFIX, identity, identity); + this.store = new SyncPrefixStore(store, EVENT_STORE_PREFIX, JSON.parse, JSON.stringify); } else { - this.store = new AsyncPrefixStore(store, EVENT_STORE_PREFIX, identity, identity); + this.store = new AsyncPrefixStore(store, EVENT_STORE_PREFIX, JSON.parse, JSON.stringify); } if (logger) { diff --git a/lib/project_config/polling_datafile_manager.spec.ts b/lib/project_config/polling_datafile_manager.spec.ts index b87416186..d39de95ed 100644 --- a/lib/project_config/polling_datafile_manager.spec.ts +++ b/lib/project_config/polling_datafile_manager.spec.ts @@ -106,7 +106,7 @@ describe('PollingDatafileManager', () => { const repeater = getMockRepeater(); const requestHandler = getMockRequestHandler(); // response promise is pending const cache = getMockAsyncCache(); - await cache.set('opt-datafile-keyThatExists', JSON.stringify({ name: 'keyThatExists' })); + await cache.set('opt-datafile-v6-keyThatExists', JSON.stringify({ name: 'keyThatExists' })); const manager = new PollingDatafileManager({ repeater, @@ -131,7 +131,7 @@ describe('PollingDatafileManager', () => { requestHandler.makeRequest.mockReturnValueOnce(mockResponse); const cache = getMockAsyncCache(); - await cache.set('opt-datafile-keyThatExists', JSON.stringify({ name: 'keyThatExists' })); + await cache.set('opt-datafile-v6-keyThatExists', JSON.stringify({ name: 'keyThatExists' })); const manager = new PollingDatafileManager({ repeater, @@ -155,7 +155,7 @@ describe('PollingDatafileManager', () => { const repeater = getMockRepeater(); const requestHandler = getMockRequestHandler(); const cache = getMockAsyncCache(); - await cache.set('opt-datafile-keyThatExists', JSON.stringify({ name: 'keyThatExists' })); + await cache.set('opt-datafile-v6-keyThatExists', JSON.stringify({ name: 'keyThatExists' })); const mockResponse = getMockAbortableRequest(); requestHandler.makeRequest.mockReturnValueOnce(mockResponse); @@ -564,7 +564,7 @@ describe('PollingDatafileManager', () => { repeater.execute(0); await expect(manager.onRunning()).resolves.not.toThrow(); - expect(spy).toHaveBeenCalledWith('opt-datafile-keyThatDoesNotExists', '{"foo": "bar"}'); + expect(spy).toHaveBeenCalledWith('opt-datafile-v6-keyThatDoesNotExists', '{"foo": "bar"}'); }); }); @@ -635,9 +635,9 @@ describe('PollingDatafileManager', () => { } await expect(manager.onRunning()).resolves.not.toThrow(); - expect(spy).toHaveBeenNthCalledWith(1, 'opt-datafile-keyThatDoesNotExists', '{"foo": "bar"}'); - expect(spy).toHaveBeenNthCalledWith(2, 'opt-datafile-keyThatDoesNotExists', '{"foo2": "bar2"}'); - expect(spy).toHaveBeenNthCalledWith(3, 'opt-datafile-keyThatDoesNotExists', '{"foo3": "bar3"}'); + expect(spy).toHaveBeenNthCalledWith(1, 'opt-datafile-v6-keyThatDoesNotExists', '{"foo": "bar"}'); + expect(spy).toHaveBeenNthCalledWith(2, 'opt-datafile-v6-keyThatDoesNotExists', '{"foo2": "bar2"}'); + expect(spy).toHaveBeenNthCalledWith(3, 'opt-datafile-v6-keyThatDoesNotExists', '{"foo3": "bar3"}'); }); it('logs an error if fetch request fails and does not call onUpdate handler', async () => { diff --git a/lib/project_config/polling_datafile_manager.ts b/lib/project_config/polling_datafile_manager.ts index d4f02e37e..9d592adc3 100644 --- a/lib/project_config/polling_datafile_manager.ts +++ b/lib/project_config/polling_datafile_manager.ts @@ -73,7 +73,7 @@ export class PollingDatafileManager extends BaseService implements DatafileManag logger, } = config; this.cache = cache; - this.cacheKey = 'opt-datafile-' + sdkKey; + this.cacheKey = 'opt-datafile-v6-' + sdkKey; this.sdkKey = sdkKey; this.datafileAccessToken = datafileAccessToken; this.customHeaders = customHeaders; diff --git a/lib/utils/cache/async_storage_cache.react_native.spec.ts b/lib/utils/cache/async_storage_cache.react_native.spec.ts index f67fca7bf..1ada3e80b 100644 --- a/lib/utils/cache/async_storage_cache.react_native.spec.ts +++ b/lib/utils/cache/async_storage_cache.react_native.spec.ts @@ -20,48 +20,33 @@ import { getDefaultAsyncStorage } from '../import.react_native/@react-native-asy vi.mock('@react-native-async-storage/async-storage'); -type TestData = { - a: number; - b: string; - d: { e: boolean }; -}; - describe('AsyncStorageCache', () => { const asyncStorage = getDefaultAsyncStorage(); - it('should store a stringified value in async storage', async () => { - const cache = new AsyncStorageCache(); + it('should store the value as-is in async storage without serialization', async () => { + const cache = new AsyncStorageCache(); - const data = { a: 1, b: '2', d: { e: true } }; - await cache.set('key', data); + await cache.set('key', 'value'); - expect(await asyncStorage.getItem('key')).toBe(JSON.stringify(data)); - expect(await cache.get('key')).toEqual(data); + expect(await asyncStorage.getItem('key')).toBe('value'); + expect(await cache.get('key')).toBe('value'); }); it('should return undefined if get is called for a nonexistent key', async () => { - const cache = new AsyncStorageCache(); + const cache = new AsyncStorageCache(); expect(await cache.get('nonexistent')).toBeUndefined(); }); it('should return the value if get is called for an existing key', async () => { - const cache = new AsyncStorageCache(); + const cache = new AsyncStorageCache(); await cache.set('key', 'value'); expect(await cache.get('key')).toBe('value'); }); - it('should return the value after json parsing if get is called for an existing key', async () => { - const cache = new AsyncStorageCache(); - const data = { a: 1, b: '2', d: { e: true } }; - await cache.set('key', data); - - expect(await cache.get('key')).toEqual(data); - }); - it('should remove the key from async storage when remove is called', async () => { - const cache = new AsyncStorageCache(); + const cache = new AsyncStorageCache(); await cache.set('key', 'value'); await cache.remove('key'); @@ -69,7 +54,7 @@ describe('AsyncStorageCache', () => { }); it('should remove all keys from async storage when clear is called', async () => { - const cache = new AsyncStorageCache(); + const cache = new AsyncStorageCache(); await cache.set('key1', 'value1'); await cache.set('key2', 'value2'); @@ -79,7 +64,7 @@ describe('AsyncStorageCache', () => { }); it('should return all keys when getKeys is called', async () => { - const cache = new AsyncStorageCache(); + const cache = new AsyncStorageCache(); await cache.set('key1', 'value1'); await cache.set('key2', 'value2'); @@ -87,7 +72,7 @@ describe('AsyncStorageCache', () => { }); it('should return an array of values for an array of keys when getBatched is called', async () => { - const cache = new AsyncStorageCache(); + const cache = new AsyncStorageCache(); await cache.set('key1', 'value1'); await cache.set('key2', 'value2'); diff --git a/lib/utils/cache/async_storage_cache.react_native.ts b/lib/utils/cache/async_storage_cache.react_native.ts index 049633fd6..b568edc0b 100644 --- a/lib/utils/cache/async_storage_cache.react_native.ts +++ b/lib/utils/cache/async_storage_cache.react_native.ts @@ -19,21 +19,21 @@ import { AsyncStore } from "./store"; import { getDefaultAsyncStorage } from "../import.react_native/@react-native-async-storage/async-storage"; import { Platform } from '../../platform_support'; -export class AsyncStorageCache implements AsyncStore { +export class AsyncStorageCache implements AsyncStore { public readonly operation = 'async'; private asyncStorage = getDefaultAsyncStorage(); - async get(key: string): Promise { + async get(key: string): Promise { const value = await this.asyncStorage.getItem(key); - return value ? JSON.parse(value) : undefined; + return value ?? undefined; } async remove(key: string): Promise { return this.asyncStorage.removeItem(key); } - async set(key: string, val: V): Promise { - return this.asyncStorage.setItem(key, JSON.stringify(val)); + async set(key: string, val: string): Promise { + return this.asyncStorage.setItem(key, val); } async clear(): Promise { @@ -44,9 +44,9 @@ export class AsyncStorageCache implements AsyncStore { return [... await this.asyncStorage.getAllKeys()]; } - async getBatched(keys: string[]): Promise[]> { + async getBatched(keys: string[]): Promise[]> { const items = await this.asyncStorage.multiGet(keys); - return items.map(([key, value]) => value ? JSON.parse(value) : undefined); + return items.map(([key, value]) => value ?? undefined); } } diff --git a/lib/utils/cache/local_storage_cache.browser.spec.ts b/lib/utils/cache/local_storage_cache.browser.spec.ts index 1556c8966..8e190462c 100644 --- a/lib/utils/cache/local_storage_cache.browser.spec.ts +++ b/lib/utils/cache/local_storage_cache.browser.spec.ts @@ -17,51 +17,38 @@ import { describe, it, expect, beforeEach } from 'vitest'; import { LocalStorageCache } from './local_storage_cache.browser'; -type TestData = { - a: number; - b: string; - d: { e: boolean }; -} - describe('LocalStorageCache', () => { beforeEach(() => { localStorage.clear(); }); - it('should store a stringified value in local storage', () => { - const cache = new LocalStorageCache(); - const data = { a: 1, b: '2', d: { e: true } }; - cache.set('key', data); - expect(localStorage.getItem('key')).toBe(JSON.stringify(data)); + it('should store the value as-is in local storage without serialization', () => { + const cache = new LocalStorageCache(); + cache.set('key', 'value'); + expect(localStorage.getItem('key')).toBe('value'); + expect(cache.get('key')).toBe('value'); }); it('should return undefined if get is called for a nonexistent key', () => { - const cache = new LocalStorageCache(); + const cache = new LocalStorageCache(); expect(cache.get('nonexistent')).toBeUndefined(); }); it('should return the value if get is called for an existing key', () => { - const cache = new LocalStorageCache(); + const cache = new LocalStorageCache(); cache.set('key', 'value'); expect(cache.get('key')).toBe('value'); }); - it('should return the value after json parsing if get is called for an existing key', () => { - const cache = new LocalStorageCache(); - const data = { a: 1, b: '2', d: { e: true } }; - cache.set('key', data); - expect(cache.get('key')).toEqual(data); - }); - it('should remove the key from local storage when remove is called', () => { - const cache = new LocalStorageCache(); + const cache = new LocalStorageCache(); cache.set('key', 'value'); cache.remove('key'); expect(localStorage.getItem('key')).toBeNull(); }); it('should remove all keys from local storage when clear is called', () => { - const cache = new LocalStorageCache(); + const cache = new LocalStorageCache(); cache.set('key1', 'value1'); cache.set('key2', 'value2'); expect(localStorage.length).toBe(2); @@ -70,14 +57,14 @@ describe('LocalStorageCache', () => { }); it('should return all keys when getKeys is called', () => { - const cache = new LocalStorageCache(); + const cache = new LocalStorageCache(); cache.set('key1', 'value1'); cache.set('key2', 'value2'); expect(cache.getKeys().sort()).toEqual(['key1', 'key2']); }); it('should return an array of values for an array of keys when getBatched is called', () => { - const cache = new LocalStorageCache(); + const cache = new LocalStorageCache(); cache.set('key1', 'value1'); cache.set('key2', 'value2'); expect(cache.getBatched(['key1', 'key2'])).toEqual(['value1', 'value2']); diff --git a/lib/utils/cache/local_storage_cache.browser.ts b/lib/utils/cache/local_storage_cache.browser.ts index 3e5ede910..8b9b4124d 100644 --- a/lib/utils/cache/local_storage_cache.browser.ts +++ b/lib/utils/cache/local_storage_cache.browser.ts @@ -18,16 +18,15 @@ import { Maybe } from "../type"; import { SyncStore } from "./store"; import { Platform } from '../../platform_support'; -export class LocalStorageCache implements SyncStore { +export class LocalStorageCache implements SyncStore { public readonly operation = 'sync'; - public set(key: string, value: V): void { - localStorage.setItem(key, JSON.stringify(value)); + public set(key: string, value: string): void { + localStorage.setItem(key, value); } - public get(key: string): Maybe { - const value = localStorage.getItem(key); - return value ? JSON.parse(value) : undefined; + public get(key: string): Maybe { + return localStorage.getItem(key) ?? undefined; } public remove(key: string): void { @@ -49,7 +48,7 @@ export class LocalStorageCache implements SyncStore { return keys; } - getBatched(keys: string[]): Maybe[] { + getBatched(keys: string[]): Maybe[] { return keys.map((k) => this.get(k)); } } diff --git a/lib/vuid/vuid_manager_factory.browser.ts b/lib/vuid/vuid_manager_factory.browser.ts index eace086b3..9fde2c706 100644 --- a/lib/vuid/vuid_manager_factory.browser.ts +++ b/lib/vuid/vuid_manager_factory.browser.ts @@ -23,7 +23,7 @@ export const vuidCacheManager = new VuidCacheManager(); export const createVuidManager = (options: VuidManagerOptions = {}): OpaqueVuidManager => { return wrapVuidManager(new DefaultVuidManager({ vuidCacheManager, - vuidCache: options.vuidCache || new LocalStorageCache(), + vuidCache: options.vuidCache || new LocalStorageCache(), enableVuid: options.enableVuid })); }; diff --git a/lib/vuid/vuid_manager_factory.react_native.ts b/lib/vuid/vuid_manager_factory.react_native.ts index 57b2e9d3a..ca0bbef55 100644 --- a/lib/vuid/vuid_manager_factory.react_native.ts +++ b/lib/vuid/vuid_manager_factory.react_native.ts @@ -23,7 +23,7 @@ export const vuidCacheManager = new VuidCacheManager(); export const createVuidManager = (options: VuidManagerOptions = {}): OpaqueVuidManager => { return wrapVuidManager(new DefaultVuidManager({ vuidCacheManager, - vuidCache: options.vuidCache || new AsyncStorageCache(), + vuidCache: options.vuidCache || new AsyncStorageCache(), enableVuid: options.enableVuid })); };