diff --git a/CHANGES.txt b/CHANGES.txt index 03ada664..7447fcdb 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,6 +1,6 @@ 2.11.0 (January XX, 2026) - Added metadata to SDK_UPDATE events to indicate the type of update (FLAGS_UPDATE or SEGMENTS_UPDATE) and the names of updated flags or segments. - - Added metadata to SDK_READY and SDK_READY_FROM_CACHE events, including `initialCacheLoad` (boolean: `true` for fresh install/first app launch, `false` for warm cache/second app launch) and `lastUpdateTimestamp` (Int64 milliseconds since epoch). + - Added metadata to SDK_READY and SDK_READY_FROM_CACHE events, including `initialCacheLoad` (boolean: `true` for fresh install/first app launch, `false` for warm cache/second app launch) and `lastUpdateTimestamp` (milliseconds since epoch). 2.10.1 (December 18, 2025) - Bugfix - Handle `null` prerequisites properly. diff --git a/src/readiness/__tests__/readinessManager.spec.ts b/src/readiness/__tests__/readinessManager.spec.ts index 10abe124..2ece8380 100644 --- a/src/readiness/__tests__/readinessManager.spec.ts +++ b/src/readiness/__tests__/readinessManager.spec.ts @@ -100,13 +100,13 @@ test('READINESS MANAGER / Ready from cache event should be fired once', (done) = counter++; }); - readinessManager.splits.emit(SDK_SPLITS_CACHE_LOADED, { isCacheValid: true, lastUpdateTimestamp: null }); - readinessManager.splits.emit(SDK_SPLITS_CACHE_LOADED, { isCacheValid: true, lastUpdateTimestamp: null }); + readinessManager.splits.emit(SDK_SPLITS_CACHE_LOADED, { initialCacheLoad: false, lastUpdateTimestamp: undefined }); + readinessManager.splits.emit(SDK_SPLITS_CACHE_LOADED, { initialCacheLoad: false, lastUpdateTimestamp: undefined }); setTimeout(() => { - readinessManager.splits.emit(SDK_SPLITS_CACHE_LOADED, { isCacheValid: true, lastUpdateTimestamp: null }); + readinessManager.splits.emit(SDK_SPLITS_CACHE_LOADED, { initialCacheLoad: false, lastUpdateTimestamp: undefined }); }, 0); - readinessManager.splits.emit(SDK_SPLITS_CACHE_LOADED, { isCacheValid: true, lastUpdateTimestamp: null }); - readinessManager.splits.emit(SDK_SPLITS_CACHE_LOADED, { isCacheValid: true, lastUpdateTimestamp: null }); + readinessManager.splits.emit(SDK_SPLITS_CACHE_LOADED, { initialCacheLoad: false, lastUpdateTimestamp: undefined }); + readinessManager.splits.emit(SDK_SPLITS_CACHE_LOADED, { initialCacheLoad: false, lastUpdateTimestamp: undefined }); setTimeout(() => { expect(counter).toBe(1); // should be called only once @@ -372,7 +372,7 @@ test('READINESS MANAGER / SDK_READY_FROM_CACHE should emit with metadata when ca // Emit cache loaded event with timestamp readinessManager.splits.emit(SDK_SPLITS_CACHE_LOADED, { - isCacheValid: true, + initialCacheLoad: false, lastUpdateTimestamp: cacheTimestamp }); @@ -395,7 +395,7 @@ test('READINESS MANAGER / SDK_READY_FROM_CACHE should emit with metadata when SD expect(receivedMetadata).toBeDefined(); expect(receivedMetadata!.initialCacheLoad).toBe(true); - expect(receivedMetadata!.lastUpdateTimestamp).toBeNull(); + expect(receivedMetadata!.lastUpdateTimestamp).toBeUndefined(); }); test('READINESS MANAGER / SDK_READY should emit with metadata when ready from cache', () => { @@ -403,7 +403,7 @@ test('READINESS MANAGER / SDK_READY should emit with metadata when ready from ca const cacheTimestamp = Date.now() - 1000 * 60 * 60; // 1 hour ago // First emit cache loaded with timestamp - readinessManager.splits.emit(SDK_SPLITS_CACHE_LOADED, { isCacheValid: true, lastUpdateTimestamp: cacheTimestamp }); + readinessManager.splits.emit(SDK_SPLITS_CACHE_LOADED, { initialCacheLoad: false, lastUpdateTimestamp: cacheTimestamp }); let receivedMetadata: SdkReadyMetadata | undefined; readinessManager.gate.on(SDK_READY, (meta: SdkReadyMetadata) => { @@ -433,5 +433,5 @@ test('READINESS MANAGER / SDK_READY should emit with metadata when ready without expect(receivedMetadata).toBeDefined(); expect(receivedMetadata!.initialCacheLoad).toBe(true); // Was not ready from cache - expect(receivedMetadata!.lastUpdateTimestamp).toBeNull(); // No cache timestamp when fresh install + expect(receivedMetadata!.lastUpdateTimestamp).toBeUndefined(); // No cache timestamp when fresh install }); diff --git a/src/readiness/__tests__/sdkReadinessManager.spec.ts b/src/readiness/__tests__/sdkReadinessManager.spec.ts index 996d6359..63174eea 100644 --- a/src/readiness/__tests__/sdkReadinessManager.spec.ts +++ b/src/readiness/__tests__/sdkReadinessManager.spec.ts @@ -219,7 +219,7 @@ describe('SDK Readiness Manager - Promises', () => { const sdkReadinessManager = sdkReadinessManagerFactory(EventEmitter, fullSettings); // make the SDK ready from cache - sdkReadinessManager.readinessManager.splits.emit(SDK_SPLITS_CACHE_LOADED, { isCacheValid: true, lastUpdateTimestamp: null }); + sdkReadinessManager.readinessManager.splits.emit(SDK_SPLITS_CACHE_LOADED, { initialCacheLoad: false, lastUpdateTimestamp: null }); expect(await sdkReadinessManager.sdkStatus.whenReadyFromCache()).toBe(false); // validate error log for SDK_READY_FROM_CACHE diff --git a/src/readiness/readinessManager.ts b/src/readiness/readinessManager.ts index 7d4d9847..d2f501b2 100644 --- a/src/readiness/readinessManager.ts +++ b/src/readiness/readinessManager.ts @@ -1,9 +1,8 @@ import { objectAssign } from '../utils/lang/objectAssign'; import { ISettings } from '../types'; -import SplitIO from '../../types/splitio'; +import SplitIO, { SdkReadyMetadata } from '../../types/splitio'; import { SDK_SPLITS_ARRIVED, SDK_SPLITS_CACHE_LOADED, SDK_SEGMENTS_ARRIVED, SDK_READY_TIMED_OUT, SDK_READY_FROM_CACHE, SDK_UPDATE, SDK_READY } from './constants'; import { IReadinessEventEmitter, IReadinessManager, ISegmentsEventEmitter, ISplitsEventEmitter } from './types'; -import { CacheValidationMetadata } from '../storages/inLocalStorage/validateCache'; function splitsEventEmitterFactory(EventEmitter: new () => SplitIO.IEventEmitter): ISplitsEventEmitter { const splitsEventEmitter = objectAssign(new EventEmitter(), { @@ -56,7 +55,7 @@ export function readinessManagerFactory( // emit SDK_READY_FROM_CACHE let isReadyFromCache = false; - let cacheLastUpdateTimestamp: number | null = null; + let cacheLastUpdateTimestamp: number | undefined = undefined; if (splits.splitsCacheLoaded) isReadyFromCache = true; // ready from cache, but doesn't emit SDK_READY_FROM_CACHE else splits.once(SDK_SPLITS_CACHE_LOADED, checkIsReadyFromCache); @@ -86,17 +85,14 @@ export function readinessManagerFactory( splits.initCallbacks.push(__init); if (splits.hasInit) __init(); - function checkIsReadyFromCache(cacheMetadata: CacheValidationMetadata) { - isReadyFromCache = true; + function checkIsReadyFromCache(cacheMetadata: SdkReadyMetadata) { cacheLastUpdateTimestamp = cacheMetadata.lastUpdateTimestamp; + isReadyFromCache = true; // Don't emit SDK_READY_FROM_CACHE if SDK_READY has been emitted if (!isReady && !isDestroyed) { try { syncLastUpdate(); - gate.emit(SDK_READY_FROM_CACHE, { - initialCacheLoad: !cacheMetadata.isCacheValid, - lastUpdateTimestamp: cacheLastUpdateTimestamp - }); + gate.emit(SDK_READY_FROM_CACHE, cacheMetadata); } catch (e) { // throws user callback exceptions in next tick setTimeout(() => { throw e; }, 0); @@ -124,14 +120,14 @@ export function readinessManagerFactory( if (!isReadyFromCache) { isReadyFromCache = true; const metadataReadyFromCache: SplitIO.SdkReadyMetadata = { - initialCacheLoad: true, - lastUpdateTimestamp: null // No cache timestamp when fresh install + initialCacheLoad: true, // Fresh install, no cache existed + lastUpdateTimestamp: undefined // No cache timestamp when fresh install }; gate.emit(SDK_READY_FROM_CACHE, metadataReadyFromCache); } const metadataReady: SplitIO.SdkReadyMetadata = { - initialCacheLoad: !wasReadyFromCache, - lastUpdateTimestamp: wasReadyFromCache ? cacheLastUpdateTimestamp : null + initialCacheLoad: !wasReadyFromCache, // true if not ready from cache (initial load), false if ready from cache + lastUpdateTimestamp: wasReadyFromCache ? cacheLastUpdateTimestamp : undefined }; gate.emit(SDK_READY, metadataReady); } catch (e) { diff --git a/src/sdkFactory/index.ts b/src/sdkFactory/index.ts index ee9314b9..300fccc0 100644 --- a/src/sdkFactory/index.ts +++ b/src/sdkFactory/index.ts @@ -54,10 +54,10 @@ export function sdkFactory(params: ISdkFactoryParams): SplitIO.ISDK | SplitIO.IA return; } readiness.splits.emit(SDK_SPLITS_ARRIVED); - readiness.segments.emit(SDK_SEGMENTS_ARRIVED, { isCacheValid: true, lastUpdateTimestamp: null }); + readiness.segments.emit(SDK_SEGMENTS_ARRIVED, { initialCacheLoad: false /* Not an initial load, cache exists */ }); }, onReadyFromCacheCb() { - readiness.splits.emit(SDK_SPLITS_CACHE_LOADED, { isCacheValid: true, lastUpdateTimestamp: null }); + readiness.splits.emit(SDK_SPLITS_CACHE_LOADED, { initialCacheLoad: false /* Not an initial load, cache exists */ }); } }); @@ -65,7 +65,7 @@ export function sdkFactory(params: ISdkFactoryParams): SplitIO.ISDK | SplitIO.IA if (initialRolloutPlan) { setRolloutPlan(log, initialRolloutPlan, storage as IStorageSync, key && getMatching(key)); - if ((storage as IStorageSync).splits.getChangeNumber() > -1) readiness.splits.emit(SDK_SPLITS_CACHE_LOADED, { isCacheValid: true, lastUpdateTimestamp: null }); + if ((storage as IStorageSync).splits.getChangeNumber() > -1) readiness.splits.emit(SDK_SPLITS_CACHE_LOADED, { initialCacheLoad: false /* Not an initial load, cache exists */ }); } const clients: Record = {}; diff --git a/src/storages/inLocalStorage/__tests__/validateCache.spec.ts b/src/storages/inLocalStorage/__tests__/validateCache.spec.ts index f2f76417..6ddfa134 100644 --- a/src/storages/inLocalStorage/__tests__/validateCache.spec.ts +++ b/src/storages/inLocalStorage/__tests__/validateCache.spec.ts @@ -29,10 +29,10 @@ describe.each(storages)('validateCache', (storage) => { 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 () => { + test('if there is no cache, it should return initialCacheLoad: true', async () => { const result = await validateCache({}, storage, fullSettings, keys, splits, rbSegments, segments, largeSegments); - expect(result.isCacheValid).toBe(false); - expect(result.lastUpdateTimestamp).toBeNull(); + expect(result.initialCacheLoad).toBe(true); + expect(result.lastUpdateTimestamp).toBeUndefined(); expect(logSpy).not.toHaveBeenCalled(); @@ -46,7 +46,7 @@ describe.each(storages)('validateCache', (storage) => { expect(storage.getItem(keys.buildLastClear())).toBeNull(); }); - test('if there is cache and it must not be cleared, it should return true', async () => { + test('if there is cache and it must not be cleared, it should return initialCacheLoad: false', async () => { const lastUpdateTimestamp = Date.now() - 1000 * 60 * 60; // 1 hour ago storage.setItem(keys.buildSplitsTillKey(), '1'); storage.setItem(keys.buildHashKey(), FULL_SETTINGS_HASH); @@ -54,7 +54,7 @@ describe.each(storages)('validateCache', (storage) => { await storage.save && storage.save(); const result = await validateCache({}, storage, fullSettings, keys, splits, rbSegments, segments, largeSegments); - expect(result.isCacheValid).toBe(true); + expect(result.initialCacheLoad).toBe(false); expect(result.lastUpdateTimestamp).toBe(lastUpdateTimestamp); expect(logSpy).not.toHaveBeenCalled(); @@ -69,15 +69,15 @@ describe.each(storages)('validateCache', (storage) => { expect(storage.getItem(keys.buildLastClear())).toBeNull(); }); - test('if there is cache and it has expired, it should clear cache and return false', async () => { + test('if there is cache and it has expired, it should clear cache and return initialCacheLoad: true', async () => { 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 await storage.save && storage.save(); const result = await validateCache({ expirationDays: 1 }, storage, fullSettings, keys, splits, rbSegments, segments, largeSegments); - expect(result.isCacheValid).toBe(false); - expect(result.lastUpdateTimestamp).toBeNull(); + expect(result.initialCacheLoad).toBe(true); + expect(result.lastUpdateTimestamp).toBeUndefined(); expect(logSpy).toHaveBeenCalledWith('storage:localstorage: Cache expired more than 1 days ago. Cleaning up cache'); @@ -90,14 +90,14 @@ describe.each(storages)('validateCache', (storage) => { 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 () => { + test('if there is cache and its hash has changed, it should clear cache and return initialCacheLoad: true', async () => { storage.setItem(keys.buildSplitsTillKey(), '1'); storage.setItem(keys.buildHashKey(), FULL_SETTINGS_HASH); await storage.save && storage.save(); const result = await validateCache({}, storage, { ...fullSettings, core: { ...fullSettings.core, authorizationKey: 'another-sdk-key' } }, keys, splits, rbSegments, segments, largeSegments); - expect(result.isCacheValid).toBe(false); - expect(result.lastUpdateTimestamp).toBeNull(); + expect(result.initialCacheLoad).toBe(true); + expect(result.lastUpdateTimestamp).toBeUndefined(); expect(logSpy).toHaveBeenCalledWith('storage:localstorage: SDK key, flags filter criteria, or flags spec version has changed. Cleaning up cache'); @@ -110,7 +110,7 @@ describe.each(storages)('validateCache', (storage) => { 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 () => { + test('if there is cache and clearOnInit is true, it should clear cache and return initialCacheLoad: true', async () => { // Older cache version (without last clear) storage.removeItem(keys.buildLastClear()); storage.setItem(keys.buildSplitsTillKey(), '1'); @@ -118,8 +118,8 @@ describe.each(storages)('validateCache', (storage) => { await storage.save && storage.save(); const result = await validateCache({ clearOnInit: true }, storage, fullSettings, keys, splits, rbSegments, segments, largeSegments); - expect(result.isCacheValid).toBe(false); - expect(result.lastUpdateTimestamp).toBeNull(); + expect(result.initialCacheLoad).toBe(true); + expect(result.lastUpdateTimestamp).toBeUndefined(); expect(logSpy).toHaveBeenCalledWith('storage:localstorage: clearOnInit was set and cache was not cleared in the last 24 hours. Cleaning up cache'); @@ -138,7 +138,7 @@ describe.each(storages)('validateCache', (storage) => { storage.setItem(keys.buildSplitsTillKey(), '1'); storage.setItem(keys.buildLastUpdatedKey(), lastUpdateTimestamp + ''); const result2 = await validateCache({ clearOnInit: true }, storage, fullSettings, keys, splits, rbSegments, segments, largeSegments); - expect(result2.isCacheValid).toBe(true); + expect(result2.initialCacheLoad).toBe(false); expect(result2.lastUpdateTimestamp).toBe(lastUpdateTimestamp); expect(logSpy).not.toHaveBeenCalled(); expect(storage.getItem(keys.buildLastClear())).toBe(lastClear); // Last clear should not have changed @@ -146,8 +146,8 @@ describe.each(storages)('validateCache', (storage) => { // If a day has passed, it should clear again storage.setItem(keys.buildLastClear(), (Date.now() - 1000 * 60 * 60 * 24 - 1) + ''); const result3 = await validateCache({ clearOnInit: true }, storage, fullSettings, keys, splits, rbSegments, segments, largeSegments); - expect(result3.isCacheValid).toBe(false); - expect(result3.lastUpdateTimestamp).toBeNull(); + expect(result3.initialCacheLoad).toBe(true); + expect(result3.lastUpdateTimestamp).toBeUndefined(); 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); diff --git a/src/storages/inLocalStorage/index.ts b/src/storages/inLocalStorage/index.ts index ab54d43f..cc5d38f4 100644 --- a/src/storages/inLocalStorage/index.ts +++ b/src/storages/inLocalStorage/index.ts @@ -14,7 +14,7 @@ import { STORAGE_LOCALSTORAGE } from '../../utils/constants'; import { shouldRecordTelemetry, TelemetryCacheInMemory } from '../inMemory/TelemetryCacheInMemory'; import { UniqueKeysCacheInMemoryCS } from '../inMemory/UniqueKeysCacheInMemoryCS'; import { getMatching } from '../../utils/key'; -import { validateCache, CacheValidationMetadata } from './validateCache'; +import { validateCache } from './validateCache'; import { ILogger } from '../../logger/types'; import SplitIO from '../../../types/splitio'; import { storageAdapter } from './storageAdapter'; @@ -54,7 +54,7 @@ export function InLocalStorage(options: SplitIO.InLocalStorageOptions = {}): ISt const rbSegments = new RBSegmentsCacheInLocal(settings, keys, storage); const segments = new MySegmentsCacheInLocal(log, keys, storage); const largeSegments = new MySegmentsCacheInLocal(log, myLargeSegmentsKeyBuilder(prefix, matchingKey), storage); - let validateCachePromise: Promise | undefined; + let validateCachePromise: Promise | undefined; return { splits, diff --git a/src/storages/inLocalStorage/validateCache.ts b/src/storages/inLocalStorage/validateCache.ts index 74ed1241..d9fa8de0 100644 --- a/src/storages/inLocalStorage/validateCache.ts +++ b/src/storages/inLocalStorage/validateCache.ts @@ -12,11 +12,6 @@ import { StorageAdapter } from '../types'; const DEFAULT_CACHE_EXPIRATION_IN_DAYS = 10; const MILLIS_IN_A_DAY = 86400000; -export interface CacheValidationMetadata { - isCacheValid: boolean; - lastUpdateTimestamp: number | null; -} - /** * Validates if cache should be cleared and sets the cache `hash` if needed. * @@ -71,9 +66,9 @@ function validateExpiration(options: SplitIO.InLocalStorageOptions, storage: Sto * - its hash has changed, i.e., the SDK key, flags filter criteria or flags spec version was modified * - `clearOnInit` was set and cache was not cleared in the last 24 hours * - * @returns Metadata object with `isCacheValid` (true if cache is ready to be used, false otherwise) and `lastUpdateTimestamp` (timestamp of last cache update or null) + * @returns Metadata object with `initialCacheLoad` (true if is fresh install, false if is ready from cache) and `lastUpdateTimestamp` (timestamp of last cache update or undefined) */ -export function validateCache(options: SplitIO.InLocalStorageOptions, storage: StorageAdapter, settings: ISettings, keys: KeyBuilderCS, splits: SplitsCacheInLocal, rbSegments: RBSegmentsCacheInLocal, segments: MySegmentsCacheInLocal, largeSegments: MySegmentsCacheInLocal): Promise { +export function validateCache(options: SplitIO.InLocalStorageOptions, storage: StorageAdapter, settings: ISettings, keys: KeyBuilderCS, splits: SplitsCacheInLocal, rbSegments: RBSegmentsCacheInLocal, segments: MySegmentsCacheInLocal, largeSegments: MySegmentsCacheInLocal): Promise { return Promise.resolve(storage.load && storage.load()).then(() => { const currentTimestamp = Date.now(); @@ -81,8 +76,8 @@ export function validateCache(options: SplitIO.InLocalStorageOptions, storage: S // Get lastUpdateTimestamp from storage const lastUpdatedTimestampStr = storage.getItem(keys.buildLastUpdatedKey()); - const lastUpdatedTimestamp = lastUpdatedTimestampStr ? parseInt(lastUpdatedTimestampStr, 10) : null; - const lastUpdateTimestamp = (!isNaNNumber(lastUpdatedTimestamp) && lastUpdatedTimestamp !== null) ? lastUpdatedTimestamp : null; + const lastUpdatedTimestamp = lastUpdatedTimestampStr ? parseInt(lastUpdatedTimestampStr, 10) : undefined; + const lastUpdateTimestamp = (!isNaNNumber(lastUpdatedTimestamp) && lastUpdatedTimestamp !== undefined) ? lastUpdatedTimestamp : undefined; if (validateExpiration(options, storage, settings, keys, currentTimestamp, isThereCache)) { splits.clear(); @@ -101,14 +96,14 @@ export function validateCache(options: SplitIO.InLocalStorageOptions, storage: S if (storage.save) storage.save(); return { - isCacheValid: false, - lastUpdateTimestamp: null + initialCacheLoad: true, // Cache was cleared, so this is an initial load (no cache existed) + lastUpdateTimestamp: undefined }; } // Check if ready from cache return { - isCacheValid: isThereCache, + initialCacheLoad: !isThereCache, // true if no cache exists (initial load), false if cache exists (ready from cache) lastUpdateTimestamp }; }); diff --git a/src/storages/types.ts b/src/storages/types.ts index 988b0441..fea0cc2b 100644 --- a/src/storages/types.ts +++ b/src/storages/types.ts @@ -499,7 +499,7 @@ export interface IStorageSync extends IStorageBase< IUniqueKeysCacheSync > { // Defined in client-side - validateCache?: () => Promise<{ isCacheValid: boolean; lastUpdateTimestamp: number | null }>, + validateCache?: () => Promise, largeSegments?: ISegmentsCacheSync, } diff --git a/src/sync/__tests__/syncManagerOnline.spec.ts b/src/sync/__tests__/syncManagerOnline.spec.ts index 4c0bb227..c78b9215 100644 --- a/src/sync/__tests__/syncManagerOnline.spec.ts +++ b/src/sync/__tests__/syncManagerOnline.spec.ts @@ -100,7 +100,7 @@ test('syncManagerOnline should syncAll a single time when sync is disabled', asy // Test pushManager for main client const syncManager = syncManagerOnlineFactory(() => pollingManagerMock, pushManagerFactoryMock)({ settings, // @ts-ignore - storage: { validateCache: () => false }, + storage: { validateCache: () => { return Promise.resolve({ initialCacheLoad: true, lastUpdateTimestamp: undefined }); } }, }); expect(pushManagerFactoryMock).not.toBeCalled(); @@ -169,7 +169,7 @@ test('syncManagerOnline should syncAll a single time when sync is disabled', asy // pushManager instantiation control test const testSyncManager = syncManagerOnlineFactory(() => pollingManagerMock, pushManagerFactoryMock)({ settings, // @ts-ignore - storage: { validateCache: () => false }, + storage: { validateCache: () => Promise.resolve({ initialCacheLoad: true, lastUpdateTimestamp: undefined }) }, }); expect(pushManagerFactoryMock).toBeCalled(); @@ -183,18 +183,18 @@ test('syncManagerOnline should syncAll a single time when sync is disabled', asy }); -test('syncManagerOnline should emit SDK_SPLITS_CACHE_LOADED if validateCache returns true', async () => { +test('syncManagerOnline should emit SDK_SPLITS_CACHE_LOADED if validateCache returns false', async () => { const lastUpdateTimestamp = Date.now() - 1000 * 60 * 60; // 1 hour ago const params = { settings: fullSettings, - storage: { validateCache: () => Promise.resolve({ isCacheValid: true, lastUpdateTimestamp }) }, + storage: { validateCache: () => Promise.resolve({ initialCacheLoad: false, lastUpdateTimestamp }) }, readiness: { splits: { emit: jest.fn() } } }; // @ts-ignore const syncManager = syncManagerOnlineFactory()(params); await syncManager.start(); - expect(params.readiness.splits.emit).toBeCalledWith(SDK_SPLITS_CACHE_LOADED, { isCacheValid: true, lastUpdateTimestamp }); + expect(params.readiness.splits.emit).toBeCalledWith(SDK_SPLITS_CACHE_LOADED, { initialCacheLoad: false, lastUpdateTimestamp }); syncManager.stop(); }); diff --git a/src/sync/offline/syncTasks/fromObjectSyncTask.ts b/src/sync/offline/syncTasks/fromObjectSyncTask.ts index 59879e5b..cc2ffcb7 100644 --- a/src/sync/offline/syncTasks/fromObjectSyncTask.ts +++ b/src/sync/offline/syncTasks/fromObjectSyncTask.ts @@ -59,9 +59,9 @@ export function fromObjectUpdaterFactory( if (startingUp) { startingUp = false; - Promise.resolve(storage.validateCache ? storage.validateCache() : { isCacheValid: false, lastUpdateTimestamp: null }).then((cacheMetadata) => { + Promise.resolve(storage.validateCache ? storage.validateCache() : { initialCacheLoad: true /* Fallback: assume initial load when validateCache doesn't exist */ }).then((cacheMetadata) => { // Emits SDK_READY_FROM_CACHE - if (cacheMetadata.isCacheValid) { + if (!cacheMetadata.initialCacheLoad) { readiness.splits.emit(SDK_SPLITS_CACHE_LOADED, cacheMetadata); } // Emits SDK_READY diff --git a/src/sync/syncManagerOnline.ts b/src/sync/syncManagerOnline.ts index 24664122..df9ff152 100644 --- a/src/sync/syncManagerOnline.ts +++ b/src/sync/syncManagerOnline.ts @@ -92,12 +92,12 @@ export function syncManagerOnlineFactory( // @TODO once event, impression and telemetry storages support persistence, call when `validateCache` promise is resolved submitterManager.start(!isConsentGranted(settings)); - return Promise.resolve(storage.validateCache ? storage.validateCache() : { isCacheValid: false, lastUpdateTimestamp: null }).then((cacheMetadata) => { + return Promise.resolve(storage.validateCache ? storage.validateCache() : { initialCacheLoad: true /* Fallback: assume initial load when validateCache doesn't exist */ }).then((cacheMetadata) => { if (!running) return; if (startFirstTime) { // Emits SDK_READY_FROM_CACHE - if (cacheMetadata.isCacheValid) { + if (!cacheMetadata.initialCacheLoad) { readiness.splits.emit(SDK_SPLITS_CACHE_LOADED, cacheMetadata); } diff --git a/src/utils/settingsValidation/storage/storageCS.ts b/src/utils/settingsValidation/storage/storageCS.ts index e38bdaf3..04705253 100644 --- a/src/utils/settingsValidation/storage/storageCS.ts +++ b/src/utils/settingsValidation/storage/storageCS.ts @@ -8,7 +8,7 @@ import { IStorageFactoryParams, IStorageSync } from '../../../storages/types'; export function __InLocalStorageMockFactory(params: IStorageFactoryParams): IStorageSync { const result = InMemoryStorageCSFactory(params); - result.validateCache = () => Promise.resolve({ isCacheValid: true, lastUpdateTimestamp: null }); // to emit SDK_READY_FROM_CACHE + result.validateCache = () => Promise.resolve({ initialCacheLoad: false /* Not an initial load, cache exists - to emit SDK_READY_FROM_CACHE */ }); return result; } __InLocalStorageMockFactory.type = STORAGE_MEMORY; diff --git a/types/splitio.d.ts b/types/splitio.d.ts index 0e02612f..387b49bd 100644 --- a/types/splitio.d.ts +++ b/types/splitio.d.ts @@ -528,9 +528,9 @@ declare namespace SplitIO { */ initialCacheLoad: boolean /** - * Timestamp in milliseconds since epoch when the event was emitted. + * Timestamp in milliseconds since epoch when the event was emitted. Undefined if `initialCacheLoad` is `true`. */ - lastUpdateTimestamp: number | null + lastUpdateTimestamp?: number } /**