Skip to content

Commit

Permalink
Draft implementation of loadData and getSnapshot methods
Browse files Browse the repository at this point in the history
  • Loading branch information
EmilianoSanchez committed Oct 1, 2024
1 parent cf822b9 commit 5f865f3
Show file tree
Hide file tree
Showing 6 changed files with 124 additions and 44 deletions.
2 changes: 1 addition & 1 deletion src/sdkFactory/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export function sdkFactory(params: ISdkFactoryParams): SplitIO.ICsSDK | SplitIO.
readiness.splits.emit(SDK_SPLITS_CACHE_LOADED);
}
});
// @TODO add support for dataloader: `if (params.dataLoader) params.dataLoader(storage);`

const clients: Record<string, IBasicClient> = {};
const telemetryTracker = telemetryTrackerFactory(storage.telemetry, platform.now);
const integrationsManager = integrationsManagerFactory && integrationsManagerFactory({ settings, storage, telemetryTracker });
Expand Down
33 changes: 33 additions & 0 deletions src/storages/__tests__/dataLoader.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { InMemoryStorageFactory } from '../inMemory/InMemoryStorage';
import { InMemoryStorageCSFactory } from '../inMemory/InMemoryStorageCS';
import { fullSettings } from '../../utils/settingsValidation/__tests__/settings.mocks';

import * as dataLoader from '../dataLoader';

test('loadData & getSnapshot', () => {
jest.spyOn(dataLoader, 'loadData');
const onReadyFromCacheCb = jest.fn();
// @ts-expect-error
const serverStorage = InMemoryStorageFactory({ settings: fullSettings });
serverStorage.splits.setChangeNumber(123); // @ts-expect-error
serverStorage.splits.addSplits([['split1', { name: 'split1' }]]);
serverStorage.segments.addToSegment('segment1', [fullSettings.core.key as string]);

const preloadedData = dataLoader.getSnapshot(serverStorage, [fullSettings.core.key as string]);

// @ts-expect-error
const clientStorage = InMemoryStorageCSFactory({ settings: { ...fullSettings, preloadedData }, onReadyFromCacheCb });

// Assert
expect(dataLoader.loadData).toBeCalledTimes(1);
expect(onReadyFromCacheCb).toBeCalledTimes(1);
expect(dataLoader.getSnapshot(clientStorage, [fullSettings.core.key as string])).toEqual(preloadedData);
expect(preloadedData).toEqual({
since: 123,
splitsData: {
split1: { name: 'split1' }
},
mySegmentsData: { [fullSettings.core.key as string]: ['segment1'] },
segmentsData: undefined
});
});
95 changes: 63 additions & 32 deletions src/storages/dataLoader.ts
Original file line number Diff line number Diff line change
@@ -1,55 +1,86 @@
import { SplitIO } from '../types';
import { DEFAULT_CACHE_EXPIRATION_IN_MILLIS } from '../utils/constants/browser';
import { DataLoader, ISegmentsCacheSync, ISplitsCacheSync } from './types';
import { ISegmentsCacheSync, ISplitsCacheSync, IStorageSync } from './types';
import { setToArray, ISet } from '../utils/lang/sets';

/**
* Factory of client-side storage loader
* Storage-agnostic adaptation of `loadDataIntoLocalStorage` function
* (https://github.com/godaddy/split-javascript-data-loader/blob/master/src/load-data.js)
*
* @param preloadedData validated data following the format proposed in https://github.com/godaddy/split-javascript-data-loader
* and extended with a `mySegmentsData` property.
* @returns function to preload the storage
* @param preloadedData validated data following the format proposed in https://github.com/godaddy/split-javascript-data-loader and extended with a `mySegmentsData` property.
* @param storage object containing `splits` and `segments` cache (client-side variant)
* @param userKey user key (matching key) of the provided MySegmentsCache
*
* @TODO extend to load largeSegments
* @TODO extend to load data on shared mySegments storages. Be specific when emitting SDK_READY_FROM_CACHE on shared clients. Maybe the serializer should provide the `useSegments` flag.
* @TODO add logs, and input validation in this module, in favor of size reduction.
* @TODO unit tests
*/
export function dataLoaderFactory(preloadedData: SplitIO.PreloadedData): DataLoader {

/**
* Storage-agnostic adaptation of `loadDataIntoLocalStorage` function
* (https://github.com/godaddy/split-javascript-data-loader/blob/master/src/load-data.js)
*
* @param storage object containing `splits` and `segments` cache (client-side variant)
* @param userId user key string of the provided MySegmentsCache
*
* @TODO extend to support SegmentsCache (server-side variant) by making `userId` optional and adding the corresponding logic.
* @TODO extend to load data on shared mySegments storages. Be specific when emitting SDK_READY_FROM_CACHE on shared clients. Maybe the serializer should provide the `useSegments` flag.
*/
return function loadData(storage: { splits: ISplitsCacheSync, segments: ISegmentsCacheSync }, userId: string) {
// Do not load data if current preloadedData is empty
if (Object.keys(preloadedData).length === 0) return;

const { lastUpdated = -1, segmentsData = {}, since = -1, splitsData = {} } = preloadedData;
export function loadData(preloadedData: SplitIO.PreloadedData, storage: { splits?: ISplitsCacheSync, segments: ISegmentsCacheSync, largeSegments?: ISegmentsCacheSync }, userKey?: string) {
// Do not load data if current preloadedData is empty
if (Object.keys(preloadedData).length === 0) return;

const { segmentsData = {}, since = -1, splitsData = {} } = preloadedData;

if (storage.splits) {
const storedSince = storage.splits.getChangeNumber();
const expirationTimestamp = Date.now() - DEFAULT_CACHE_EXPIRATION_IN_MILLIS;

// Do not load data if current localStorage data is more recent,
// or if its `lastUpdated` timestamp is older than the given `expirationTimestamp`,
if (storedSince > since || lastUpdated < expirationTimestamp) return;
// Do not load data if current data is more recent
if (storedSince > since) return;

// cleaning up the localStorage data, since some cached splits might need be part of the preloaded data
storage.splits.clear();
storage.splits.setChangeNumber(since);

// splitsData in an object where the property is the split name and the pertaining value is a stringified json of its data
storage.splits.addSplits(Object.keys(splitsData).map(splitName => JSON.parse(splitsData[splitName])));
storage.splits.addSplits(Object.keys(splitsData).map(splitName => ([splitName, splitsData[splitName]])));
}

// add mySegments data
let mySegmentsData = preloadedData.mySegmentsData && preloadedData.mySegmentsData[userId];
if (userKey) { // add mySegments data (client-side)
let mySegmentsData = preloadedData.mySegmentsData && preloadedData.mySegmentsData[userKey];
if (!mySegmentsData) {
// segmentsData in an object where the property is the segment name and the pertaining value is a stringified object that contains the `added` array of userIds
mySegmentsData = Object.keys(segmentsData).filter(segmentName => {
const userIds = JSON.parse(segmentsData[segmentName]).added;
return Array.isArray(userIds) && userIds.indexOf(userId) > -1;
const userKeys = segmentsData[segmentName];
return userKeys.indexOf(userKey) > -1;
});
}
storage.segments.resetSegments({ k: mySegmentsData.map(s => ({ n: s })) });
} else { // add segments data (server-side)
Object.keys(segmentsData).filter(segmentName => {
const userKeys = segmentsData[segmentName];
storage.segments.addToSegment(segmentName, userKeys);
});
}
}

export function getSnapshot(storage: IStorageSync, userKeys?: string[]): SplitIO.PreloadedData {
return {
// lastUpdated: Date.now(),
// @ts-ignore accessing private prop
since: storage.splits.changeNumber, // @ts-ignore accessing private prop
splitsData: storage.splits.splitsCache,
segmentsData: userKeys ?
undefined : // @ts-ignore accessing private prop
Object.keys(storage.segments.segmentCache).reduce((prev, cur) => { // @ts-ignore accessing private prop
prev[cur] = setToArray(storage.segments.segmentCache[cur] as ISet<string>);
return prev;
}, {}),
mySegmentsData: userKeys ?
userKeys.reduce((prev, userKey) => {
// @ts-ignore accessing private prop
prev[userKey] = storage.shared ?
// Client-side segments
// @ts-ignore accessing private prop
Object.keys(storage.shared(userKey).segments.segmentCache) :
// Server-side segments
// @ts-ignore accessing private prop
Object.keys(storage.segments.segmentCache).reduce<string[]>((prev, segmentName) => { // @ts-ignore accessing private prop
return storage.segments.segmentCache[segmentName].has(userKey) ?
prev.concat(segmentName) :
prev;
}, []);
return prev;
}, {}) :
undefined
};
}
23 changes: 19 additions & 4 deletions src/storages/inMemory/InMemoryStorageCS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,16 @@ import { ImpressionCountsCacheInMemory } from './ImpressionCountsCacheInMemory';
import { DEBUG, LOCALHOST_MODE, NONE, STORAGE_MEMORY } from '../../utils/constants';
import { shouldRecordTelemetry, TelemetryCacheInMemory } from './TelemetryCacheInMemory';
import { UniqueKeysCacheInMemoryCS } from './UniqueKeysCacheInMemoryCS';
import { getMatching } from '../../utils/key';
import { loadData } from '../dataLoader';

/**
* InMemory storage factory for standalone client-side SplitFactory
*
* @param params parameters required by EventsCacheSync
*/
export function InMemoryStorageCSFactory(params: IStorageFactoryParams): IStorageSync {
const { settings: { scheduler: { impressionsQueueSize, eventsQueueSize, }, sync: { impressionsMode, __splitFiltersValidation } } } = params;
const { settings: { scheduler: { impressionsQueueSize, eventsQueueSize, }, sync: { impressionsMode, __splitFiltersValidation }, preloadedData }, onReadyFromCacheCb } = params;

const splits = new SplitsCacheInMemory(__splitFiltersValidation);
const segments = new MySegmentsCacheInMemory();
Expand Down Expand Up @@ -42,11 +44,18 @@ export function InMemoryStorageCSFactory(params: IStorageFactoryParams): IStorag
},

// When using shared instanciation with MEMORY we reuse everything but segments (they are unique per key)
shared() {
shared(matchingKey: string) {
const segments = new MySegmentsCacheInMemory();
const largeSegments = new MySegmentsCacheInMemory();

if (preloadedData) {
loadData(preloadedData, { segments, largeSegments }, matchingKey);
}

return {
splits: this.splits,
segments: new MySegmentsCacheInMemory(),
largeSegments: new MySegmentsCacheInMemory(),
segments,
largeSegments,
impressions: this.impressions,
impressionCounts: this.impressionCounts,
events: this.events,
Expand All @@ -72,6 +81,12 @@ export function InMemoryStorageCSFactory(params: IStorageFactoryParams): IStorag
if (storage.uniqueKeys) storage.uniqueKeys.track = noopTrack;
}


if (preloadedData) {
loadData(preloadedData, storage, getMatching(params.settings.core.key));
if (splits.getChangeNumber() > -1) onReadyFromCacheCb();
}

return storage;
}

Expand Down
2 changes: 0 additions & 2 deletions src/storages/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -491,8 +491,6 @@ export interface IStorageAsync extends IStorageBase<

/** StorageFactory */

export type DataLoader = (storage: IStorageSync, matchingKey: string) => void

export interface IStorageFactoryParams {
settings: ISettings,
/**
Expand Down
13 changes: 8 additions & 5 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ISplitFiltersValidation } from './dtos/types';
import { ISplit, ISplitFiltersValidation } from './dtos/types';
import { IIntegration, IIntegrationFactoryParams } from './integrations/types';
import { ILogger } from './logger/types';
import { ISdkFactoryContext } from './sdkFactory/types';
Expand Down Expand Up @@ -98,6 +98,7 @@ export interface ISettings {
eventsFirstPushWindow: number
},
readonly storage: IStorageSyncFactory | IStorageAsyncFactory,
readonly preloadedData?: SplitIO.PreloadedData,
readonly integrations: Array<{
readonly type: string,
(params: IIntegrationFactoryParams): IIntegration | void
Expand Down Expand Up @@ -771,31 +772,33 @@ export namespace SplitIO {
* If this value is older than 10 days ago (expiration time policy), the data is not used to update the storage content.
* @TODO configurable expiration time policy?
*/
lastUpdated: number,
// lastUpdated: number,
/**
* Change number of the preloaded data.
* If this value is older than the current changeNumber at the storage, the data is not used to update the storage content.
*/
since: number,
/**
* Map of feature flags to their stringified definitions.
* @TODO rename to flags
*/
splitsData: {
[splitName: string]: string
[splitName: string]: ISplit
},
/**
* Optional map of user keys to their list of segments.
* @TODO remove when releasing first version
* @TODO rename to memberships
*/
mySegmentsData?: {
[key: string]: string[]
},
/**
* Optional map of segments to their stringified definitions.
* This property is ignored if `mySegmentsData` was provided.
* @TODO rename to segments
*/
segmentsData?: {
[segmentName: string]: string
[segmentName: string]: string[]
},
}
/**
Expand Down

0 comments on commit 5f865f3

Please sign in to comment.