From e76ce72fe08472c93a7b225a75142bc640631599 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 10 Aug 2020 21:14:43 -0300 Subject: [PATCH 01/15] POC implementation --- src/storage/DataLoader.js | 55 +++++++++++++++++++++++++++ src/storage/browser.js | 11 +++++- src/utils/settings/storage/browser.js | 9 +++-- types/splitio.d.ts | 9 ++++- 4 files changed, 78 insertions(+), 6 deletions(-) create mode 100644 src/storage/DataLoader.js diff --git a/src/storage/DataLoader.js b/src/storage/DataLoader.js new file mode 100644 index 000000000..f439c88b9 --- /dev/null +++ b/src/storage/DataLoader.js @@ -0,0 +1,55 @@ +// @TODO implement +export function validateData(data) { + return data && data.serializedData ? true : false; +} + +/** + * Factory of data builders + * + * @param {Object} data validated serializedData and userId following the format proposed in https://github.com/godaddy/split-javascript-data-loader + * and extended with a `mySegmentsData` property. + */ +export function dataLoaderFactory({ serializedData = {}, userId = '' }) { + + /** + * Storage-agnostic adaptation of `loadDataIntoLocalStorage` function + * (https://github.com/godaddy/split-javascript-data-loader/blob/master/src/load-data.js) + * + * @param {Object} storage storage for client-side + */ + return function loadData(storage) { + // Do not load data if current serializedData is empty + if (Object.keys(serializedData).length === 0) { + return; + } + + const { segmentsData = {}, since = 0, splitsData = {} } = serializedData; + let { mySegmentsData } = serializedData; + + const currentSince = storage.splits.getChangeNumber(); + + // Do not load data if current localStorage data is more recent + if (since <= currentSince) { + return; + } + // Split.IO recommends cleaning up the localStorage data + if (currentSince === -1) storage.splits.flush(); + 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 + Object.keys(splitsData).forEach(splitName => { + storage.splits.addSplit(splitName, splitsData[splitName]); + }); + + // add mySegments data + 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 added = JSON.parse(segmentsData[segmentName]).added; + return added.includes(userId); + }); + } + storage.segments.resetSegments(mySegmentsData); + }; + +} \ No newline at end of file diff --git a/src/storage/browser.js b/src/storage/browser.js index 603a809f3..4242b70b1 100644 --- a/src/storage/browser.js +++ b/src/storage/browser.js @@ -16,12 +16,13 @@ export const DEFAULT_CACHE_EXPIRATION_IN_MILLIS = 864000000; // 10 days const BrowserStorageFactory = context => { const settings = context.get(context.constants.SETTINGS); const { storage } = settings; + let result; switch (storage.type) { case STORAGE_MEMORY: { const keys = new KeyBuilder(settings); - return { + result = { splits: new SplitCacheInMemory, segments: new SegmentCacheInMemory(keys), impressions: new ImpressionsCacheInMemory, @@ -57,13 +58,14 @@ const BrowserStorageFactory = context => { this.events.clear(); } }; + break; } case STORAGE_LOCALSTORAGE: { const keys = new KeyBuilderLocalStorage(settings); const expirationTimestamp = Date.now() - DEFAULT_CACHE_EXPIRATION_IN_MILLIS; - return { + result = { splits: new SplitCacheInLocalStorage(keys, expirationTimestamp, settings.sync.__splitFiltersValidation), segments: new SegmentCacheInLocalStorage(keys), impressions: new ImpressionsCacheInMemory, @@ -99,12 +101,17 @@ const BrowserStorageFactory = context => { this.events.clear(); } }; + break; } default: throw new Error('Unsupported storage type'); } + // load precached data into storage + if (storage.dataLoader) storage.dataLoader(result); + + return result; }; export default BrowserStorageFactory; \ No newline at end of file diff --git a/src/utils/settings/storage/browser.js b/src/utils/settings/storage/browser.js index 2618e784b..cdc49c2ea 100644 --- a/src/utils/settings/storage/browser.js +++ b/src/utils/settings/storage/browser.js @@ -22,6 +22,7 @@ import { STORAGE_MEMORY, STORAGE_LOCALSTORAGE } from '../../../utils/constants'; +import { validateData, dataLoaderFactory } from '../../../storage/DataLoader'; const ParseStorageSettings = settings => { let { @@ -29,7 +30,8 @@ const ParseStorageSettings = settings => { storage: { type = STORAGE_MEMORY, options = {}, - prefix + prefix, + data }, } = settings; @@ -47,7 +49,7 @@ const ParseStorageSettings = settings => { // If an invalid storage type is provided OR we want to use LOCALSTORAGE and // it's not available, fallback into MEMORY if (type !== STORAGE_MEMORY && type !== STORAGE_LOCALSTORAGE || - type === STORAGE_LOCALSTORAGE && !isLocalStorageAvailable()) { + type === STORAGE_LOCALSTORAGE && !isLocalStorageAvailable()) { type = STORAGE_MEMORY; log.warn('Invalid or unavailable storage. Fallbacking into MEMORY storage'); } @@ -55,7 +57,8 @@ const ParseStorageSettings = settings => { return { type, options, - prefix + prefix, + dataLoader: validateData(data) ? dataLoaderFactory(data) : undefined }; }; diff --git a/types/splitio.d.ts b/types/splitio.d.ts index aa9430d22..a6d23b421 100644 --- a/types/splitio.d.ts +++ b/types/splitio.d.ts @@ -902,7 +902,14 @@ declare namespace SplitIO { * @property {string} prefix * @default SPLITIO */ - prefix?: string + prefix?: string, + /** + * @TODO document + */ + data?: { + serializedData: any, + userId?: string + } } /** * SDK integration settings for the Browser. From b9bafb8580e51e181bde063f3005fbe17c2d1a8f Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 10 Aug 2020 21:44:32 -0300 Subject: [PATCH 02/15] simplified storage interface and added SDK_READY_FROM_CACHE to memory storage --- src/storage/DataLoader.js | 10 ++++++---- src/storage/SplitCache/InMemory.js | 4 ++-- src/utils/settings/storage/browser.js | 5 +++-- types/splitio.d.ts | 7 ++----- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/storage/DataLoader.js b/src/storage/DataLoader.js index f439c88b9..1116b8ac9 100644 --- a/src/storage/DataLoader.js +++ b/src/storage/DataLoader.js @@ -1,6 +1,8 @@ +import { isObject } from '../utils/lang'; + // @TODO implement -export function validateData(data) { - return data && data.serializedData ? true : false; +export function validateData(serializedData) { + return isObject(serializedData) ? true : false; } /** @@ -9,7 +11,7 @@ export function validateData(data) { * @param {Object} data validated serializedData and userId following the format proposed in https://github.com/godaddy/split-javascript-data-loader * and extended with a `mySegmentsData` property. */ -export function dataLoaderFactory({ serializedData = {}, userId = '' }) { +export function dataLoaderFactory(serializedData = {}, userId) { /** * Storage-agnostic adaptation of `loadDataIntoLocalStorage` function @@ -46,7 +48,7 @@ export function dataLoaderFactory({ serializedData = {}, userId = '' }) { // 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 added = JSON.parse(segmentsData[segmentName]).added; - return added.includes(userId); + return added.indexOf(userId) > -1; }); } storage.segments.resetSegments(mySegmentsData); diff --git a/src/storage/SplitCache/InMemory.js b/src/storage/SplitCache/InMemory.js index b060815bc..d4f2c2bed 100644 --- a/src/storage/SplitCache/InMemory.js +++ b/src/storage/SplitCache/InMemory.js @@ -133,10 +133,10 @@ class SplitCacheInMemory { } /** - * Check if the splits information is already stored in cache. In memory there is no cache to check. + * Check if the splits information is already stored in cache. The data can be preloaded and passed via the config. */ checkCache() { - return false; + return this.getChangeNumber() > -1; } } diff --git a/src/utils/settings/storage/browser.js b/src/utils/settings/storage/browser.js index cdc49c2ea..01d18a5c8 100644 --- a/src/utils/settings/storage/browser.js +++ b/src/utils/settings/storage/browser.js @@ -26,12 +26,13 @@ import { validateData, dataLoaderFactory } from '../../../storage/DataLoader'; const ParseStorageSettings = settings => { let { + core: { key }, mode, storage: { type = STORAGE_MEMORY, options = {}, prefix, - data + serializedData }, } = settings; @@ -58,7 +59,7 @@ const ParseStorageSettings = settings => { type, options, prefix, - dataLoader: validateData(data) ? dataLoaderFactory(data) : undefined + dataLoader: validateData(serializedData) ? dataLoaderFactory(serializedData, key) : undefined }; }; diff --git a/types/splitio.d.ts b/types/splitio.d.ts index a6d23b421..bd0064173 100644 --- a/types/splitio.d.ts +++ b/types/splitio.d.ts @@ -904,12 +904,9 @@ declare namespace SplitIO { */ prefix?: string, /** - * @TODO document + * @TODO document and define type */ - data?: { - serializedData: any, - userId?: string - } + serializedData?: any, } /** * SDK integration settings for the Browser. From 82f8f150b9ea846871abec94d889881d14adb365 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 10 Aug 2020 22:35:25 -0300 Subject: [PATCH 03/15] refactored user key param --- src/storage/DataLoader.js | 15 ++++++++------- src/storage/browser.js | 5 ++++- src/utils/settings/storage/browser.js | 3 +-- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/storage/DataLoader.js b/src/storage/DataLoader.js index 1116b8ac9..fa5a3342c 100644 --- a/src/storage/DataLoader.js +++ b/src/storage/DataLoader.js @@ -8,25 +8,25 @@ export function validateData(serializedData) { /** * Factory of data builders * - * @param {Object} data validated serializedData and userId following the format proposed in https://github.com/godaddy/split-javascript-data-loader + * @param {Object} serializedData validated data following the format proposed in https://github.com/godaddy/split-javascript-data-loader * and extended with a `mySegmentsData` property. */ -export function dataLoaderFactory(serializedData = {}, userId) { +export function dataLoaderFactory(serializedData = {}) { /** * Storage-agnostic adaptation of `loadDataIntoLocalStorage` function * (https://github.com/godaddy/split-javascript-data-loader/blob/master/src/load-data.js) * * @param {Object} storage storage for client-side + * @param {Object} userId main user key defined at the SDK config */ - return function loadData(storage) { + return function loadData(storage, userId) { // Do not load data if current serializedData is empty if (Object.keys(serializedData).length === 0) { return; } const { segmentsData = {}, since = 0, splitsData = {} } = serializedData; - let { mySegmentsData } = serializedData; const currentSince = storage.splits.getChangeNumber(); @@ -44,14 +44,15 @@ export function dataLoaderFactory(serializedData = {}, userId) { }); // add mySegments data - if (!mySegmentsData) { + let userIdMySegmentsData = serializedData.mySegmentsData && serializedData.mySegmentsData[userId]; + if (!userIdMySegmentsData) { // 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 => { + userIdMySegmentsData = Object.keys(segmentsData).filter(segmentName => { const added = JSON.parse(segmentsData[segmentName]).added; return added.indexOf(userId) > -1; }); } - storage.segments.resetSegments(mySegmentsData); + storage.segments.resetSegments(userIdMySegmentsData); }; } \ No newline at end of file diff --git a/src/storage/browser.js b/src/storage/browser.js index 4242b70b1..3511009ce 100644 --- a/src/storage/browser.js +++ b/src/storage/browser.js @@ -109,7 +109,10 @@ const BrowserStorageFactory = context => { } // load precached data into storage - if (storage.dataLoader) storage.dataLoader(result); + if (storage.dataLoader) { + const key = settings.core.key; + storage.dataLoader(result, key); + } return result; }; diff --git a/src/utils/settings/storage/browser.js b/src/utils/settings/storage/browser.js index 01d18a5c8..2af953662 100644 --- a/src/utils/settings/storage/browser.js +++ b/src/utils/settings/storage/browser.js @@ -26,7 +26,6 @@ import { validateData, dataLoaderFactory } from '../../../storage/DataLoader'; const ParseStorageSettings = settings => { let { - core: { key }, mode, storage: { type = STORAGE_MEMORY, @@ -59,7 +58,7 @@ const ParseStorageSettings = settings => { type, options, prefix, - dataLoader: validateData(serializedData) ? dataLoaderFactory(serializedData, key) : undefined + dataLoader: validateData(serializedData) ? dataLoaderFactory(serializedData) : undefined }; }; From 1eebd4766a5db2c247fc07de1d17f56a6272215a Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Fri, 14 Aug 2020 17:26:01 -0300 Subject: [PATCH 04/15] added validation. bugfixing lodder options parameter --- package-lock.json | 2 +- package.json | 2 +- src/storage/DataLoader.js | 9 +--- src/utils/inputValidation/serializedData.js | 56 +++++++++++++++++++++ src/utils/logger/index.js | 2 +- src/utils/settings/storage/browser.js | 5 +- types/splitio.d.ts | 31 +++++++++++- 7 files changed, 93 insertions(+), 14 deletions(-) create mode 100644 src/utils/inputValidation/serializedData.js diff --git a/package-lock.json b/package-lock.json index c1f4b1203..fbe863584 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio", - "version": "10.14.1", + "version": "10.14.2-canary.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index b7bd272a7..435b8166f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio", - "version": "10.14.1", + "version": "10.14.2-canary.0", "description": "Split SDK", "files": [ "README.md", diff --git a/src/storage/DataLoader.js b/src/storage/DataLoader.js index fa5a3342c..170ab2cf1 100644 --- a/src/storage/DataLoader.js +++ b/src/storage/DataLoader.js @@ -1,12 +1,5 @@ -import { isObject } from '../utils/lang'; - -// @TODO implement -export function validateData(serializedData) { - return isObject(serializedData) ? true : false; -} - /** - * Factory of data builders + * Factory of data loaders * * @param {Object} serializedData validated data following the format proposed in https://github.com/godaddy/split-javascript-data-loader * and extended with a `mySegmentsData` property. diff --git a/src/utils/inputValidation/serializedData.js b/src/utils/inputValidation/serializedData.js new file mode 100644 index 000000000..da1881426 --- /dev/null +++ b/src/utils/inputValidation/serializedData.js @@ -0,0 +1,56 @@ +import { isObject, isString } from '../lang'; +import logFactory from '../logger'; +const log = logFactory('', { + displayAllErrors: true +}); + +function validateSinceData(maybeSince, method) { + if (maybeSince > -1) return true; + log.error(`${method}: serializedData.since must be a positive number.`); + return false; +} + +function validateSplitsData(maybeSplitsData, method) { + if (isObject(maybeSplitsData)) { + const splitNames = Object.keys(maybeSplitsData); + if (splitNames.length > 0 && splitNames.every(splitName => isString(maybeSplitsData[splitName]))) return true; + } + log.error(`${method}: serializedData.splitData must be a map of split names to their serialized definitions.`); + return false; +} + +function validateMySegmentsData(maybeMySegmentsData, method) { + if (isObject(maybeMySegmentsData)) { + const userKeys = Object.keys(maybeMySegmentsData); + if (userKeys.length > 0 && userKeys.every(userKey => { + const segmentNames = maybeMySegmentsData[userKey]; + // an empty list is valid + return Array.isArray(segmentNames) && segmentNames.every(segmentName => isString(segmentName)); + })) return true; + } + log.error(`${method}: serializedData.mySegmentsData must be a map of user keys to their list of segment names.`); + return false; +} + +function validateSegmentsData(maybeSegmentsData, method) { + if (isObject(maybeSegmentsData)) { + const segmentNames = Object.keys(maybeSegmentsData); + if (segmentNames.length > 0 && segmentNames.every(segmentName => isString(maybeSegmentsData[segmentName]))) return true; + } + log.error(`${method}: serializedData.segmentsData must be a map of segment names to their serialized definitions.`); + return false; +} + +export function validateSerializedData(maybeSerializedData, method) { + if (!isObject(maybeSerializedData)) { + log.error(`${method}: serializedData must be an object.`); + } else if ( + validateSinceData(maybeSerializedData.since, method) && + validateSplitsData(maybeSerializedData.splitData, method) && + (!maybeSerializedData.mySegmentsData || validateMySegmentsData(maybeSerializedData.mySegmentsData, method)) && + (!maybeSerializedData.segmentsData || validateSegmentsData(maybeSerializedData.segmentsData, method)) + ) { + return true; + } + return false; +} \ No newline at end of file diff --git a/src/utils/logger/index.js b/src/utils/logger/index.js index f6a39cab7..aadc6e39e 100644 --- a/src/utils/logger/index.js +++ b/src/utils/logger/index.js @@ -41,7 +41,7 @@ const initialState = String( localStorage.getItem(LS_KEY) : '' ); -const createLog = (namespace, options = {}) => new Logger(namespace, merge(options, defaultOptions)); +const createLog = (namespace, options = {}) => new Logger(namespace, merge(defaultOptions, options)); const ownLog = createLog('splitio-utils:logger'); diff --git a/src/utils/settings/storage/browser.js b/src/utils/settings/storage/browser.js index 2af953662..340513a75 100644 --- a/src/utils/settings/storage/browser.js +++ b/src/utils/settings/storage/browser.js @@ -22,7 +22,8 @@ import { STORAGE_MEMORY, STORAGE_LOCALSTORAGE } from '../../../utils/constants'; -import { validateData, dataLoaderFactory } from '../../../storage/DataLoader'; +import { validateSerializedData } from '../../inputValidation/serializedData'; +import { dataLoaderFactory } from '../../../storage/DataLoader'; const ParseStorageSettings = settings => { let { @@ -58,7 +59,7 @@ const ParseStorageSettings = settings => { type, options, prefix, - dataLoader: validateData(serializedData) ? dataLoaderFactory(serializedData) : undefined + dataLoader: validateSerializedData(serializedData, 'Factory instantiation - storage preload') ? dataLoaderFactory(serializedData) : undefined }; }; diff --git a/types/splitio.d.ts b/types/splitio.d.ts index bd0064173..305911534 100644 --- a/types/splitio.d.ts +++ b/types/splitio.d.ts @@ -750,6 +750,35 @@ declare namespace SplitIO { */ values: string[], } + /** + * Defines the format of Split data to preload on Split factory storage (cache). + */ + interface SerializedData { + /** + * Change number of the serialized 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 splits to their serialized definitions. + */ + splitsData: { + [splitName: string]: string + }, + /** + * Optional map of user keys to their list of segments. + */ + mySegmentsData?: { + [key: string]: string[] + }, + /** + * Optional map of segments to their serialized definitions. + * This property is ignored if `mySegmentsData` was provided. + */ + segmentsData?: { + [segmentName: string]: string + }, + } /** * Settings interface for SDK instances created on the browser * @interface IBrowserSettings @@ -906,7 +935,7 @@ declare namespace SplitIO { /** * @TODO document and define type */ - serializedData?: any, + serializedData?: SerializedData, } /** * SDK integration settings for the Browser. From d9b78abfd1dd1a353870a607c6ae08d5720c8b49 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 17 Aug 2020 17:31:51 -0300 Subject: [PATCH 05/15] renamed: serialized to preloadedData --- src/storage/DataLoader.js | 13 ++++++----- .../{serializedData.js => preloadedData.js} | 22 +++++++++---------- src/utils/settings/storage/browser.js | 6 ++--- types/splitio.d.ts | 13 ++++++----- 4 files changed, 29 insertions(+), 25 deletions(-) rename src/utils/inputValidation/{serializedData.js => preloadedData.js} (58%) diff --git a/src/storage/DataLoader.js b/src/storage/DataLoader.js index 170ab2cf1..a63e18afd 100644 --- a/src/storage/DataLoader.js +++ b/src/storage/DataLoader.js @@ -1,10 +1,11 @@ /** * Factory of data loaders * - * @param {Object} serializedData validated data following the format proposed in https://github.com/godaddy/split-javascript-data-loader + * @TODO update comment + * @param {Object} preloadedData validated data following the format proposed in https://github.com/godaddy/split-javascript-data-loader * and extended with a `mySegmentsData` property. */ -export function dataLoaderFactory(serializedData = {}) { +export function dataLoaderFactory(preloadedData = {}) { /** * Storage-agnostic adaptation of `loadDataIntoLocalStorage` function @@ -14,12 +15,12 @@ export function dataLoaderFactory(serializedData = {}) { * @param {Object} userId main user key defined at the SDK config */ return function loadData(storage, userId) { - // Do not load data if current serializedData is empty - if (Object.keys(serializedData).length === 0) { + // Do not load data if current preloadedData is empty + if (Object.keys(preloadedData).length === 0) { return; } - const { segmentsData = {}, since = 0, splitsData = {} } = serializedData; + const { segmentsData = {}, since = 0, splitsData = {} } = preloadedData; const currentSince = storage.splits.getChangeNumber(); @@ -37,7 +38,7 @@ export function dataLoaderFactory(serializedData = {}) { }); // add mySegments data - let userIdMySegmentsData = serializedData.mySegmentsData && serializedData.mySegmentsData[userId]; + let userIdMySegmentsData = preloadedData.mySegmentsData && preloadedData.mySegmentsData[userId]; if (!userIdMySegmentsData) { // 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 userIdMySegmentsData = Object.keys(segmentsData).filter(segmentName => { diff --git a/src/utils/inputValidation/serializedData.js b/src/utils/inputValidation/preloadedData.js similarity index 58% rename from src/utils/inputValidation/serializedData.js rename to src/utils/inputValidation/preloadedData.js index da1881426..d339bba72 100644 --- a/src/utils/inputValidation/serializedData.js +++ b/src/utils/inputValidation/preloadedData.js @@ -6,7 +6,7 @@ const log = logFactory('', { function validateSinceData(maybeSince, method) { if (maybeSince > -1) return true; - log.error(`${method}: serializedData.since must be a positive number.`); + log.error(`${method}: preloadedData.since must be a positive number.`); return false; } @@ -15,7 +15,7 @@ function validateSplitsData(maybeSplitsData, method) { const splitNames = Object.keys(maybeSplitsData); if (splitNames.length > 0 && splitNames.every(splitName => isString(maybeSplitsData[splitName]))) return true; } - log.error(`${method}: serializedData.splitData must be a map of split names to their serialized definitions.`); + log.error(`${method}: preloadedData.splitData must be a map of split names to their serialized definitions.`); return false; } @@ -28,7 +28,7 @@ function validateMySegmentsData(maybeMySegmentsData, method) { return Array.isArray(segmentNames) && segmentNames.every(segmentName => isString(segmentName)); })) return true; } - log.error(`${method}: serializedData.mySegmentsData must be a map of user keys to their list of segment names.`); + log.error(`${method}: preloadedData.mySegmentsData must be a map of user keys to their list of segment names.`); return false; } @@ -37,18 +37,18 @@ function validateSegmentsData(maybeSegmentsData, method) { const segmentNames = Object.keys(maybeSegmentsData); if (segmentNames.length > 0 && segmentNames.every(segmentName => isString(maybeSegmentsData[segmentName]))) return true; } - log.error(`${method}: serializedData.segmentsData must be a map of segment names to their serialized definitions.`); + log.error(`${method}: preloadedData.segmentsData must be a map of segment names to their serialized definitions.`); return false; } -export function validateSerializedData(maybeSerializedData, method) { - if (!isObject(maybeSerializedData)) { - log.error(`${method}: serializedData must be an object.`); +export function validatePreloadedData(maybePreloadedData, method) { + if (!isObject(maybePreloadedData)) { + log.error(`${method}: preloadedData must be an object.`); } else if ( - validateSinceData(maybeSerializedData.since, method) && - validateSplitsData(maybeSerializedData.splitData, method) && - (!maybeSerializedData.mySegmentsData || validateMySegmentsData(maybeSerializedData.mySegmentsData, method)) && - (!maybeSerializedData.segmentsData || validateSegmentsData(maybeSerializedData.segmentsData, method)) + validateSinceData(maybePreloadedData.since, method) && + validateSplitsData(maybePreloadedData.splitData, method) && + (!maybePreloadedData.mySegmentsData || validateMySegmentsData(maybePreloadedData.mySegmentsData, method)) && + (!maybePreloadedData.segmentsData || validateSegmentsData(maybePreloadedData.segmentsData, method)) ) { return true; } diff --git a/src/utils/settings/storage/browser.js b/src/utils/settings/storage/browser.js index 340513a75..b9e5a1aef 100644 --- a/src/utils/settings/storage/browser.js +++ b/src/utils/settings/storage/browser.js @@ -22,7 +22,7 @@ import { STORAGE_MEMORY, STORAGE_LOCALSTORAGE } from '../../../utils/constants'; -import { validateSerializedData } from '../../inputValidation/serializedData'; +import { validatePreloadedData } from '../../inputValidation/preloadedData'; import { dataLoaderFactory } from '../../../storage/DataLoader'; const ParseStorageSettings = settings => { @@ -32,7 +32,7 @@ const ParseStorageSettings = settings => { type = STORAGE_MEMORY, options = {}, prefix, - serializedData + preloadedData }, } = settings; @@ -59,7 +59,7 @@ const ParseStorageSettings = settings => { type, options, prefix, - dataLoader: validateSerializedData(serializedData, 'Factory instantiation - storage preload') ? dataLoaderFactory(serializedData) : undefined + dataLoader: validatePreloadedData(preloadedData, 'Factory instantiation - storage preload') ? dataLoaderFactory(preloadedData) : undefined }; }; diff --git a/types/splitio.d.ts b/types/splitio.d.ts index 305911534..abb30f5cc 100644 --- a/types/splitio.d.ts +++ b/types/splitio.d.ts @@ -751,11 +751,11 @@ declare namespace SplitIO { values: string[], } /** - * Defines the format of Split data to preload on Split factory storage (cache). + * Defines the format of Split data to preload on the factory storage (cache). */ - interface SerializedData { + interface PreloadedData { /** - * Change number of the serialized data. + * 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, @@ -933,9 +933,12 @@ declare namespace SplitIO { */ prefix?: string, /** - * @TODO document and define type + * Split data to preload the storage. You may optionally specify it to quickly initialice and use the SDK with cached data. + * If the data is valid, the SDK emits an SDK_READY_FROM_CACHE event once it is ready to be used. + * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#preloaded-data} + * @property {PreloadedData} preloadedData */ - serializedData?: SerializedData, + preloadedData?: PreloadedData, } /** * SDK integration settings for the Browser. From f8ad479071e9980b98be34076946897b6b551ce6 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 19 Aug 2020 13:06:30 -0300 Subject: [PATCH 06/15] TODO comment --- src/storage/SplitCache/InMemory.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/storage/SplitCache/InMemory.js b/src/storage/SplitCache/InMemory.js index d4f2c2bed..a11f22a3e 100644 --- a/src/storage/SplitCache/InMemory.js +++ b/src/storage/SplitCache/InMemory.js @@ -136,6 +136,7 @@ class SplitCacheInMemory { * Check if the splits information is already stored in cache. The data can be preloaded and passed via the config. */ checkCache() { + // @TODO rollback if we decide not to emit SDK_READY_FROM_CACHE using InMemory storage return this.getChangeNumber() > -1; } } From 3074bed918e68fa71229292844fe38afefa6843d Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 19 Aug 2020 14:19:02 -0300 Subject: [PATCH 07/15] updated TS tests --- ts-tests/index.ts | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/ts-tests/index.ts b/ts-tests/index.ts index b6eebf111..2ec4119a4 100644 --- a/ts-tests/index.ts +++ b/ts-tests/index.ts @@ -387,6 +387,21 @@ impressionListener.logImpression(impressionData); // Split filters let splitFilters: SplitIO.SplitFilter[] = [{ type: 'byName', values: ['my_split_1', 'my_split_1'] }, { type: 'byPrefix', values: ['my_split', 'test_split_'] }] +// storage preloaded data +let preloadedData: SplitIO.PreloadedData = { + since: 10000, + splitsData: { + split_1: 'SPLIT_1_DEFINITION', split_2: 'SPLIT_2_DEFINITION' + }, + mySegmentsData: { + user_id_1: [], + user_id_2: ['segment_1', 'segment_2'] + }, + segmentsData: { + segment_1: 'SEGMENT_1_DEFINITION', segment_2: 'SEGMENT_2_DEFINITION' + } +} + // Browser integrations let fieldsObjectSample: UniversalAnalytics.FieldsObject = { hitType: 'event', eventAction: 'action' }; let eventDataSample: SplitIO.EventData = { eventTypeId: 'someEventTypeId', value: 10, properties: {} } @@ -442,7 +457,8 @@ let fullBrowserSettings: SplitIO.IBrowserSettings = { features: mockedFeaturesMap, storage: { type: 'LOCALSTORAGE', - prefix: 'PREFIX' + prefix: 'PREFIX', + preloadedData: preloadedData }, impressionListener: impressionListener, debug: true, From f892c4c70071a3a2d9d8994a9b43a8455e991173 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 19 Aug 2020 16:44:32 -0300 Subject: [PATCH 08/15] kick off E2E tests --- .../browserSuites/ready-from-cache.spec.js | 144 ++++++++++++------ src/__tests__/mocks/preloadedData.js | 44 ++++++ src/utils/inputValidation/preloadedData.js | 4 +- 3 files changed, 143 insertions(+), 49 deletions(-) create mode 100644 src/__tests__/mocks/preloadedData.js diff --git a/src/__tests__/browserSuites/ready-from-cache.spec.js b/src/__tests__/browserSuites/ready-from-cache.spec.js index 683bbe77b..324352963 100644 --- a/src/__tests__/browserSuites/ready-from-cache.spec.js +++ b/src/__tests__/browserSuites/ready-from-cache.spec.js @@ -3,6 +3,7 @@ import { SplitFactory } from '../../'; import splitChangesMock1 from '../mocks/splitchanges.since.-1.json'; import splitChangesMock2 from '../mocks/splitchanges.since.1457552620999.json'; import mySegmentsNicolas from '../mocks/mysegments.nicolas@split.io.json'; +import { splitDefinitions, preloadedDataWithSegments } from '../mocks/preloadedData'; import { DEFAULT_CACHE_EXPIRATION_IN_MILLIS } from '../../storage/browser'; import { nearlyEqual } from '../testUtils'; @@ -45,24 +46,6 @@ const alwaysOnSplitInverted = JSON.stringify({ ] }); -const splitDeclarations = { - p1__split: { - 'name': 'p1__split', - 'status': 'ACTIVE', - 'conditions': [] - }, - p2__split: { - 'name': 'p2__split', - 'status': 'ACTIVE', - 'conditions': [] - }, - p3__split: { - 'name': 'p3__split', - 'status': 'ACTIVE', - 'conditions': [] - }, -}; - const baseConfig = { core: { authorizationKey: '', @@ -468,14 +451,14 @@ export default function (fetchMock, assert) { localStorage.clear(); t.plan(7); - fetchMock.getOnce(testUrls.sdk + '/splitChanges?since=-1&names=p1__split,p2__split', { status: 200, body: { splits: [splitDeclarations.p1__split, splitDeclarations.p2__split], since: -1, till: 1457552620999 } }, { delay: 10 }); // short delay to let emit SDK_READY_FROM_CACHE + fetchMock.getOnce(testUrls.sdk + '/splitChanges?since=-1&names=p1__split,p2__split', { status: 200, body: { splits: [splitDefinitions.p1__split, splitDefinitions.p2__split], since: -1, till: 1457552620999 } }, { delay: 10 }); // short delay to let emit SDK_READY_FROM_CACHE // fetchMock.getOnce(testUrls.sdk + '/splitChanges?since=1457552620999&names=p1__split', { status: 200, body: { splits: [], since: 1457552620999, till: 1457552620999 } }); fetchMock.getOnce(testUrls.sdk + '/mySegments/nicolas%40split.io', { status: 200, body: { mySegments: [] } }); localStorage.setItem('some_user_item', 'user_item'); localStorage.setItem('readyFromCache_5.SPLITIO.splits.till', 25); - localStorage.setItem('readyFromCache_5.SPLITIO.split.p2__split', JSON.stringify(splitDeclarations.p2__split)); - localStorage.setItem('readyFromCache_5.SPLITIO.split.p3__split', JSON.stringify(splitDeclarations.p3__split)); + localStorage.setItem('readyFromCache_5.SPLITIO.split.p2__split', JSON.stringify(splitDefinitions.p2__split)); + localStorage.setItem('readyFromCache_5.SPLITIO.split.p3__split', JSON.stringify(splitDefinitions.p3__split)); const splitio = SplitFactory({ ...baseConfig, @@ -502,8 +485,8 @@ export default function (fetchMock, assert) { client.destroy().then(() => { t.equal(localStorage.getItem('some_user_item'), 'user_item', 'user items at localStorage must not be changed'); t.equal(localStorage.getItem('readyFromCache_5.SPLITIO.splits.till'), '1457552620999', 'splits.till must correspond to the till of the last successfully fetched Splits'); - t.equal(localStorage.getItem('readyFromCache_5.SPLITIO.split.p1__split'), JSON.stringify(splitDeclarations.p1__split), 'split declarations must be cached'); - t.equal(localStorage.getItem('readyFromCache_5.SPLITIO.split.p2__split'), JSON.stringify(splitDeclarations.p2__split), 'split declarations must be cached'); + t.equal(localStorage.getItem('readyFromCache_5.SPLITIO.split.p1__split'), JSON.stringify(splitDefinitions.p1__split), 'split declarations must be cached'); + t.equal(localStorage.getItem('readyFromCache_5.SPLITIO.split.p2__split'), JSON.stringify(splitDefinitions.p2__split), 'split declarations must be cached'); t.equal(localStorage.getItem('readyFromCache_5.SPLITIO.splits.filterQuery'), '&names=p1__split,p2__split', 'splits.filterQuery must correspond to the split filter query'); t.end(); }); @@ -518,7 +501,7 @@ export default function (fetchMock, assert) { localStorage.clear(); t.plan(5); - fetchMock.getOnce(testUrls.sdk + '/splitChanges?since=-1&names=p1__split,p2__split', { status: 200, body: { splits: [splitDeclarations.p1__split, splitDeclarations.p2__split], since: -1, till: 1457552620999 } }, { delay: 10 }); // short delay to let emit SDK_READY_FROM_CACHE + fetchMock.getOnce(testUrls.sdk + '/splitChanges?since=-1&names=p1__split,p2__split', { status: 200, body: { splits: [splitDefinitions.p1__split, splitDefinitions.p2__split], since: -1, till: 1457552620999 } }, { delay: 10 }); // short delay to let emit SDK_READY_FROM_CACHE fetchMock.getOnce(testUrls.sdk + '/mySegments/nicolas%40split.io', { status: 200, body: { mySegments: [] } }); const splitio = SplitFactory({ @@ -546,8 +529,8 @@ export default function (fetchMock, assert) { client.destroy().then(() => { t.equal(localStorage.getItem('readyFromCache_5B.SPLITIO.splits.till'), '1457552620999', 'splits.till must correspond to the till of the last successfully fetched Splits'); - t.equal(localStorage.getItem('readyFromCache_5B.SPLITIO.split.p1__split'), JSON.stringify(splitDeclarations.p1__split), 'split declarations must be cached'); - t.equal(localStorage.getItem('readyFromCache_5B.SPLITIO.split.p2__split'), JSON.stringify(splitDeclarations.p2__split), 'split declarations must be cached'); + t.equal(localStorage.getItem('readyFromCache_5B.SPLITIO.split.p1__split'), JSON.stringify(splitDefinitions.p1__split), 'split declarations must be cached'); + t.equal(localStorage.getItem('readyFromCache_5B.SPLITIO.split.p2__split'), JSON.stringify(splitDefinitions.p2__split), 'split declarations must be cached'); t.equal(localStorage.getItem('readyFromCache_5B.SPLITIO.splits.filterQuery'), '&names=p1__split,p2__split', 'splits.filterQuery must correspond to the split filter query'); t.end(); }); @@ -562,13 +545,13 @@ export default function (fetchMock, assert) { localStorage.clear(); t.plan(7); - fetchMock.getOnce(testUrls.sdk + '/splitChanges?since=25&names=p2__split&prefixes=p1', { status: 200, body: { splits: [splitDeclarations.p1__split, splitDeclarations.p2__split], since: 25, till: 1457552620999 } }, { delay: 10 }); // short delay to let emit SDK_READY_FROM_CACHE + fetchMock.getOnce(testUrls.sdk + '/splitChanges?since=25&names=p2__split&prefixes=p1', { status: 200, body: { splits: [splitDefinitions.p1__split, splitDefinitions.p2__split], since: 25, till: 1457552620999 } }, { delay: 10 }); // short delay to let emit SDK_READY_FROM_CACHE fetchMock.getOnce(testUrls.sdk + '/mySegments/nicolas%40split.io', { status: 200, body: { mySegments: [] } }); localStorage.setItem('some_user_item', 'user_item'); localStorage.setItem('readyFromCache_6.SPLITIO.splits.till', 25); - localStorage.setItem('readyFromCache_6.SPLITIO.split.p1__split', JSON.stringify(splitDeclarations.p1__split)); - localStorage.setItem('readyFromCache_6.SPLITIO.split.p2__split', JSON.stringify(splitDeclarations.p2__split)); + localStorage.setItem('readyFromCache_6.SPLITIO.split.p1__split', JSON.stringify(splitDefinitions.p1__split)); + localStorage.setItem('readyFromCache_6.SPLITIO.split.p2__split', JSON.stringify(splitDefinitions.p2__split)); localStorage.setItem('readyFromCache_6.SPLITIO.splits.filterQuery', '&names=p2__split&prefixes=p1'); const splitio = SplitFactory({ @@ -596,8 +579,8 @@ export default function (fetchMock, assert) { client.destroy().then(() => { t.equal(localStorage.getItem('some_user_item'), 'user_item', 'user items at localStorage must not be changed'); t.equal(localStorage.getItem('readyFromCache_6.SPLITIO.splits.till'), '1457552620999', 'splits.till must correspond to the till of the last successfully fetched Splits'); - t.equal(localStorage.getItem('readyFromCache_6.SPLITIO.split.p1__split'), JSON.stringify(splitDeclarations.p1__split), 'split declarations must be cached'); - t.equal(localStorage.getItem('readyFromCache_6.SPLITIO.split.p2__split'), JSON.stringify(splitDeclarations.p2__split), 'split declarations must be cached'); + t.equal(localStorage.getItem('readyFromCache_6.SPLITIO.split.p1__split'), JSON.stringify(splitDefinitions.p1__split), 'split declarations must be cached'); + t.equal(localStorage.getItem('readyFromCache_6.SPLITIO.split.p2__split'), JSON.stringify(splitDefinitions.p2__split), 'split declarations must be cached'); t.equal(localStorage.getItem('readyFromCache_6.SPLITIO.splits.filterQuery'), '&names=p2__split&prefixes=p1', 'splits.filterQuery must correspond to the split filter query'); t.end(); }); @@ -612,13 +595,13 @@ export default function (fetchMock, assert) { localStorage.clear(); t.plan(6); - fetchMock.getOnce(testUrls.sdk + '/splitChanges?since=-1&prefixes=p1,p2', { status: 200, body: { splits: [splitDeclarations.p1__split, splitDeclarations.p2__split], since: -1, till: 1457552620999 } }, { delay: 10 }); // short delay to let emit SDK_READY_FROM_CACHE + fetchMock.getOnce(testUrls.sdk + '/splitChanges?since=-1&prefixes=p1,p2', { status: 200, body: { splits: [splitDefinitions.p1__split, splitDefinitions.p2__split], since: -1, till: 1457552620999 } }, { delay: 10 }); // short delay to let emit SDK_READY_FROM_CACHE fetchMock.getOnce(testUrls.sdk + '/mySegments/nicolas%40split.io', { status: 200, body: { mySegments: [] } }); localStorage.setItem('some_user_item', 'user_item'); localStorage.setItem('readyFromCache_7.SPLITIO.splits.till', 25); - localStorage.setItem('readyFromCache_7.SPLITIO.split.p1__split', JSON.stringify(splitDeclarations.p1__split)); - localStorage.setItem('readyFromCache_7.SPLITIO.split.p2__split', JSON.stringify(splitDeclarations.p2__split)); + localStorage.setItem('readyFromCache_7.SPLITIO.split.p1__split', JSON.stringify(splitDefinitions.p1__split)); + localStorage.setItem('readyFromCache_7.SPLITIO.split.p2__split', JSON.stringify(splitDefinitions.p2__split)); localStorage.setItem('readyFromCache_7.SPLITIO.splits.filterQuery', '&prefixes=p1,p2'); localStorage.setItem('readyFromCache_7.SPLITIO.splits.lastUpdated', Date.now() - DEFAULT_CACHE_EXPIRATION_IN_MILLIS - 1); // -1 to ensure having an expired lastUpdated item @@ -648,8 +631,8 @@ export default function (fetchMock, assert) { client.destroy().then(() => { t.equal(localStorage.getItem('some_user_item'), 'user_item', 'user items at localStorage must not be changed'); t.equal(localStorage.getItem('readyFromCache_7.SPLITIO.splits.till'), '1457552620999', 'splits.till must correspond to the till of the last successfully fetched Splits'); - t.equal(localStorage.getItem('readyFromCache_7.SPLITIO.split.p1__split'), JSON.stringify(splitDeclarations.p1__split), 'split declarations must be cached'); - t.equal(localStorage.getItem('readyFromCache_7.SPLITIO.split.p2__split'), JSON.stringify(splitDeclarations.p2__split), 'split declarations must be cached'); + t.equal(localStorage.getItem('readyFromCache_7.SPLITIO.split.p1__split'), JSON.stringify(splitDefinitions.p1__split), 'split declarations must be cached'); + t.equal(localStorage.getItem('readyFromCache_7.SPLITIO.split.p2__split'), JSON.stringify(splitDefinitions.p2__split), 'split declarations must be cached'); t.equal(localStorage.getItem('readyFromCache_7.SPLITIO.splits.filterQuery'), '&prefixes=p1,p2', 'splits.filterQuery must correspond to the split filter query'); t.end(); }); @@ -677,13 +660,13 @@ export default function (fetchMock, assert) { localStorage.clear(); t.plan(7); - fetchMock.getOnce(testUrls.sdk + '/splitChanges?since=-1', { status: 200, body: { splits: [splitDeclarations.p1__split, splitDeclarations.p2__split, splitDeclarations.p3__split], since: -1, till: 1457552620999 } }, { delay: 10 }); // short delay to let emit SDK_READY_FROM_CACHE + fetchMock.getOnce(testUrls.sdk + '/splitChanges?since=-1', { status: 200, body: { splits: [splitDefinitions.p1__split, splitDefinitions.p2__split, splitDefinitions.p3__split], since: -1, till: 1457552620999 } }, { delay: 10 }); // short delay to let emit SDK_READY_FROM_CACHE fetchMock.getOnce(testUrls.sdk + '/mySegments/nicolas%40split.io', { status: 200, body: { mySegments: [] } }); localStorage.setItem('some_user_item', 'user_item'); localStorage.setItem('readyFromCache_8.SPLITIO.splits.till', 25); - localStorage.setItem('readyFromCache_8.SPLITIO.split.p1__split', JSON.stringify(splitDeclarations.p1__split)); - localStorage.setItem('readyFromCache_8.SPLITIO.split.p2__split', JSON.stringify(splitDeclarations.p2__split)); + localStorage.setItem('readyFromCache_8.SPLITIO.split.p1__split', JSON.stringify(splitDefinitions.p1__split)); + localStorage.setItem('readyFromCache_8.SPLITIO.split.p2__split', JSON.stringify(splitDefinitions.p2__split)); localStorage.setItem('readyFromCache_8.SPLITIO.split.deleted__split', 'deleted_split'); localStorage.setItem('readyFromCache_8.SPLITIO.splits.filterQuery', '&names=p2__split&prefixes=p1'); @@ -711,9 +694,9 @@ export default function (fetchMock, assert) { client.destroy().then(() => { t.equal(localStorage.getItem('some_user_item'), 'user_item', 'user items at localStorage must not be changed'); t.equal(localStorage.getItem('readyFromCache_8.SPLITIO.splits.till'), '1457552620999', 'splits.till must correspond to the till of the last successfully fetched Splits'); - t.equal(localStorage.getItem('readyFromCache_8.SPLITIO.split.p1__split'), JSON.stringify(splitDeclarations.p1__split), 'split declarations must be cached'); - t.equal(localStorage.getItem('readyFromCache_8.SPLITIO.split.p2__split'), JSON.stringify(splitDeclarations.p2__split), 'split declarations must be cached'); - t.equal(localStorage.getItem('readyFromCache_8.SPLITIO.split.p3__split'), JSON.stringify(splitDeclarations.p3__split), 'split declarations must be cached'); + t.equal(localStorage.getItem('readyFromCache_8.SPLITIO.split.p1__split'), JSON.stringify(splitDefinitions.p1__split), 'split declarations must be cached'); + t.equal(localStorage.getItem('readyFromCache_8.SPLITIO.split.p2__split'), JSON.stringify(splitDefinitions.p2__split), 'split declarations must be cached'); + t.equal(localStorage.getItem('readyFromCache_8.SPLITIO.split.p3__split'), JSON.stringify(splitDefinitions.p3__split), 'split declarations must be cached'); t.equal(localStorage.getItem('readyFromCache_8.SPLITIO.splits.filterQuery'), null, 'splits.filterQuery must correspond to the split filter query'); t.end(); }); @@ -731,13 +714,13 @@ export default function (fetchMock, assert) { localStorage.clear(); t.plan(7); - fetchMock.getOnce(testUrls.sdk + '/splitChanges?since=-1&names=no%20exist%20trim,no_exist,p3__split&prefixes=no%20exist%20trim,p2', { status: 200, body: { splits: [splitDeclarations.p2__split, splitDeclarations.p3__split], since: -1, till: 1457552620999 } }, { delay: 10 }); // short delay to let emit SDK_READY_FROM_CACHE + fetchMock.getOnce(testUrls.sdk + '/splitChanges?since=-1&names=no%20exist%20trim,no_exist,p3__split&prefixes=no%20exist%20trim,p2', { status: 200, body: { splits: [splitDefinitions.p2__split, splitDefinitions.p3__split], since: -1, till: 1457552620999 } }, { delay: 10 }); // short delay to let emit SDK_READY_FROM_CACHE fetchMock.getOnce(testUrls.sdk + '/mySegments/nicolas%40split.io', { status: 200, body: { mySegments: [] } }); localStorage.setItem('some_user_item', 'user_item'); localStorage.setItem('readyFromCache_9.SPLITIO.splits.till', 25); - localStorage.setItem('readyFromCache_9.SPLITIO.split.p1__split', JSON.stringify(splitDeclarations.p1__split)); - localStorage.setItem('readyFromCache_9.SPLITIO.split.p2__split', JSON.stringify(splitDeclarations.p2__split)); + localStorage.setItem('readyFromCache_9.SPLITIO.split.p1__split', JSON.stringify(splitDefinitions.p1__split)); + localStorage.setItem('readyFromCache_9.SPLITIO.split.p2__split', JSON.stringify(splitDefinitions.p2__split)); localStorage.setItem('readyFromCache_9.SPLITIO.splits.filterQuery', '&names=p2__split&prefixes=p1'); const splitio = SplitFactory({ @@ -765,12 +748,79 @@ export default function (fetchMock, assert) { client.destroy().then(() => { t.equal(localStorage.getItem('some_user_item'), 'user_item', 'user items at localStorage must not be changed'); t.equal(localStorage.getItem('readyFromCache_9.SPLITIO.splits.till'), '1457552620999', 'splits.till must correspond to the till of the last successfully fetched Splits'); - t.equal(localStorage.getItem('readyFromCache_9.SPLITIO.split.p2__split'), JSON.stringify(splitDeclarations.p2__split), 'split declarations must be cached'); - t.equal(localStorage.getItem('readyFromCache_9.SPLITIO.split.p3__split'), JSON.stringify(splitDeclarations.p3__split), 'split declarations must be cached'); + t.equal(localStorage.getItem('readyFromCache_9.SPLITIO.split.p2__split'), JSON.stringify(splitDefinitions.p2__split), 'split declarations must be cached'); + t.equal(localStorage.getItem('readyFromCache_9.SPLITIO.split.p3__split'), JSON.stringify(splitDefinitions.p3__split), 'split declarations must be cached'); t.equal(localStorage.getItem('readyFromCache_9.SPLITIO.splits.filterQuery'), '&names=no%20exist%20trim,no_exist,p3__split&prefixes=no%20exist%20trim,p2', 'splits.filterQuery must correspond to the split filter query'); t.end(); }); }); }); + /** Preloaded data in InLocalStorage storage */ + + // Testing when we start localstorage from scrach, and with preloaded data (with segmentsData) -> emit SDK_READY_FROM_CACHE, and update storage and shared mySegments storages + assert.test(t => { + const prefix = 'readyFromCache_preloadedData1'; + const testUrls = { + sdk: 'https://sdk.baseurl/' + prefix, + events: 'https://events.baseurl/' + prefix + }; + localStorage.clear(); + t.plan(4 + Object.keys(preloadedDataWithSegments.splitsData).length); + + fetchMock.getOnce(testUrls.sdk + '/splitChanges?since=1457552620999', { status: 200, body: { splits: [], since: 1457552620999, till: 1457552620999 } }, { delay: 10 }); // short delay to let emit SDK_READY_FROM_CACHE + fetchMock.getOnce(testUrls.sdk + '/mySegments/nicolas%40split.io', { status: 200, body: { mySegments: [] } }); + + const splitio = SplitFactory({ + ...baseConfig, + storage: { + type: 'LOCALSTORAGE', + prefix: prefix, + preloadedData: preloadedDataWithSegments + }, + urls: testUrls, + debug: true + }); + const client = splitio.client(); + const manager = splitio.manager(); + + client.once(client.Event.SDK_READY_FROM_CACHE, () => { + t.deepEqual(manager.names(), Object.keys(preloadedDataWithSegments.splitsData), 'splits should be the ones in preloadedData'); + }); + + client.once(client.Event.SDK_READY, () => { + t.deepEqual(manager.names(), Object.keys(preloadedDataWithSegments.splitsData), 'splits should be the ones in preloadedData'); + + client.destroy().then(() => { + t.equal(localStorage.getItem(prefix + '.SPLITIO.splits.till'), '1457552620999', 'splits.till must correspond to the till of the last successfully fetched Splits'); + t.equal(localStorage.getItem(baseConfig.core.key + '.' + prefix + '.SPLITIO.segment.segment_1'), '1', 'user segments should be in cache'); + Object.keys(preloadedDataWithSegments.splitsData).forEach(splitName => { + t.equal(localStorage.getItem(prefix + '.SPLITIO.split.' + splitName), preloadedDataWithSegments.splitsData[splitName], 'split declarations must be cached'); + }); + t.end(); + }); + }); + }, 'readyFromCache_preloadedData1'); + + // Testing when we start localstorage from scrach, and with preloaded data but expired -> ??? + + // Testing when we start localstorage with cached data, and with newer preloaded data (with mySegmentsData) -> emit SDK_READY_FROM_CACHE, and update storage and only shared mySegments storages with existing user id + + // Testing when we start localstorage with cached data, and with newer preloaded data but invalid -> emit SDK_READY_FROM_CACHE, and don't update storages + + // Testing when we start localstorage with cached data, and with older preloaded data than storage changenumber -> emit SDK_READY_FROM_CACHE, and don't update storages + + // Testing when we start localstorage with cached data but expired, and with newer preloaded data but also expired -> ??? + + + /** Preloaded data in InMemory storage */ + + // Testing when we start inmemory, and with preloaded data (with segmentsData) -> emit SDK_READY_FROM_CACHE, and update storage and shared mySegments storages + + // Testing when we start inmemory, and with preloaded data (with mySegmentsData) -> emit SDK_READY_FROM_CACHE, and update storage and only shared mySegments storages with existing user id + + // Testing when we start inmemory, and with preloaded data but invalid -> don't emit SDK_READY_FROM_CACHE, and don't update storages + + // Testing when we start inmemory, and with preloaded data but expired -> ??? + } diff --git a/src/__tests__/mocks/preloadedData.js b/src/__tests__/mocks/preloadedData.js new file mode 100644 index 000000000..95cb041a4 --- /dev/null +++ b/src/__tests__/mocks/preloadedData.js @@ -0,0 +1,44 @@ +export const splitDefinitions = { + p1__split: { + 'name': 'p1__split', + 'status': 'ACTIVE', + 'conditions': [] + }, + p2__split: { + 'name': 'p2__split', + 'status': 'ACTIVE', + 'conditions': [] + }, + p3__split: { + 'name': 'p3__split', + 'status': 'ACTIVE', + 'conditions': [] + }, +}; + +const splitSerializedDefinitions = (function () { + return Object.keys(splitDefinitions).reduce((acum, splitName) => { + acum[splitName] = JSON.stringify(splitDefinitions[splitName]); + return acum; + }, {}); +}()); + +export const segmentsDefinitions = { + segment_1: { + 'name': 'segment_1', + 'added': ['nicolas@split.io'], + }, +}; + +const segmentsSerializedDefinitions = (function () { + return Object.keys(segmentsDefinitions).reduce((acum, segmentName) => { + acum[segmentName] = JSON.stringify(segmentsDefinitions[segmentName]); + return acum; + }, {}); +}()); + +export const preloadedDataWithSegments = { + since: 1457552620999, + splitsData: splitSerializedDefinitions, + segmentsData: segmentsSerializedDefinitions +}; diff --git a/src/utils/inputValidation/preloadedData.js b/src/utils/inputValidation/preloadedData.js index d339bba72..6e7a62fce 100644 --- a/src/utils/inputValidation/preloadedData.js +++ b/src/utils/inputValidation/preloadedData.js @@ -15,7 +15,7 @@ function validateSplitsData(maybeSplitsData, method) { const splitNames = Object.keys(maybeSplitsData); if (splitNames.length > 0 && splitNames.every(splitName => isString(maybeSplitsData[splitName]))) return true; } - log.error(`${method}: preloadedData.splitData must be a map of split names to their serialized definitions.`); + log.error(`${method}: preloadedData.splitsData must be a map of split names to their serialized definitions.`); return false; } @@ -46,7 +46,7 @@ export function validatePreloadedData(maybePreloadedData, method) { log.error(`${method}: preloadedData must be an object.`); } else if ( validateSinceData(maybePreloadedData.since, method) && - validateSplitsData(maybePreloadedData.splitData, method) && + validateSplitsData(maybePreloadedData.splitsData, method) && (!maybePreloadedData.mySegmentsData || validateMySegmentsData(maybePreloadedData.mySegmentsData, method)) && (!maybePreloadedData.segmentsData || validateSegmentsData(maybePreloadedData.segmentsData, method)) ) { From 3d74837d7fb2d9c4f4cb64b040844ac57d2d6200 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 19 Aug 2020 17:39:11 -0300 Subject: [PATCH 09/15] more E2E tests --- .../browserSuites/ready-from-cache.spec.js | 124 ++++++++++++++++-- src/__tests__/mocks/preloadedData.js | 4 +- src/storage/DataLoader.js | 4 +- 3 files changed, 117 insertions(+), 15 deletions(-) diff --git a/src/__tests__/browserSuites/ready-from-cache.spec.js b/src/__tests__/browserSuites/ready-from-cache.spec.js index 324352963..4106f9b80 100644 --- a/src/__tests__/browserSuites/ready-from-cache.spec.js +++ b/src/__tests__/browserSuites/ready-from-cache.spec.js @@ -3,7 +3,7 @@ import { SplitFactory } from '../../'; import splitChangesMock1 from '../mocks/splitchanges.since.-1.json'; import splitChangesMock2 from '../mocks/splitchanges.since.1457552620999.json'; import mySegmentsNicolas from '../mocks/mysegments.nicolas@split.io.json'; -import { splitDefinitions, preloadedDataWithSegments } from '../mocks/preloadedData'; +import { splitDefinitions, splitSerializedDefinitions, preloadedDataWithSegments } from '../mocks/preloadedData'; import { DEFAULT_CACHE_EXPIRATION_IN_MILLIS } from '../../storage/browser'; import { nearlyEqual } from '../testUtils'; @@ -802,25 +802,127 @@ export default function (fetchMock, assert) { }); }, 'readyFromCache_preloadedData1'); - // Testing when we start localstorage from scrach, and with preloaded data but expired -> ??? - // Testing when we start localstorage with cached data, and with newer preloaded data (with mySegmentsData) -> emit SDK_READY_FROM_CACHE, and update storage and only shared mySegments storages with existing user id + assert.test(t => { + const prefix = 'readyFromCache_preloadedData2'; + const testUrls = { + sdk: 'https://sdk.baseurl/' + prefix, + events: 'https://events.baseurl/' + prefix + }; + localStorage.clear(); + localStorage.setItem(prefix + '.SPLITIO.splits.till', 25); + // cached split to keep + localStorage.setItem(prefix + '.SPLITIO.split.p1__split', splitSerializedDefinitions.p1__split); + // cached split to remove + localStorage.setItem(prefix + '.SPLITIO.split.split_to_remove', 'INVALID SPLIT DEFINITION BUT WILL BE REMOVED ANYWAY'); + // cached segment to remove + localStorage.setItem(baseConfig.core.key + '.' + prefix + '.SPLITIO.segment.segment_2', '1'); + // this item makes the test to fail, but it only can occurre if it is set on purpose + // localStorage.setItem(baseConfig.core.key + '.' + prefix + '.SPLITIO.segment.segment_1', '0'); + + t.plan(5 + Object.keys(preloadedDataWithSegments.splitsData).length); - // Testing when we start localstorage with cached data, and with newer preloaded data but invalid -> emit SDK_READY_FROM_CACHE, and don't update storages + fetchMock.getOnce(testUrls.sdk + '/splitChanges?since=1457552620999', { status: 200, body: { splits: [], since: 1457552620999, till: 1457552620999 } }, { delay: 10 }); // short delay to let emit SDK_READY_FROM_CACHE + fetchMock.getOnce(testUrls.sdk + '/mySegments/nicolas%40split.io', { status: 200, body: { mySegments: [] } }); - // Testing when we start localstorage with cached data, and with older preloaded data than storage changenumber -> emit SDK_READY_FROM_CACHE, and don't update storages + const splitio = SplitFactory({ + ...baseConfig, + storage: { + type: 'LOCALSTORAGE', + prefix: prefix, + preloadedData: preloadedDataWithSegments + }, + urls: testUrls, + debug: true + }); + const client = splitio.client(); + const manager = splitio.manager(); - // Testing when we start localstorage with cached data but expired, and with newer preloaded data but also expired -> ??? + client.once(client.Event.SDK_READY_FROM_CACHE, () => { + t.deepEqual(Array.sort(manager.names()), Object.keys(preloadedDataWithSegments.splitsData), 'splits should be the ones in preloadedData'); + }); + client.once(client.Event.SDK_READY, () => { + t.deepEqual(Array.sort(manager.names()), Object.keys(preloadedDataWithSegments.splitsData), 'splits should be the ones in preloadedData'); - /** Preloaded data in InMemory storage */ + client.destroy().then(() => { + t.equal(localStorage.getItem(prefix + '.SPLITIO.splits.till'), '1457552620999', 'splits.till must correspond to the till of the last successfully fetched Splits'); + t.equal(localStorage.getItem(baseConfig.core.key + '.' + prefix + '.SPLITIO.segment.segment_1'), '1', 'added user segments should be in cache'); + t.equal(localStorage.getItem(baseConfig.core.key + '.' + prefix + '.SPLITIO.segment.segment_2'), null, 'removed user segments should not be in cache'); + Object.keys(preloadedDataWithSegments.splitsData).forEach(splitName => { + t.equal(localStorage.getItem(prefix + '.SPLITIO.split.' + splitName), preloadedDataWithSegments.splitsData[splitName], 'split declarations must be cached'); + }); + t.end(); + }); + }); + }, 'readyFromCache_preloadedData2'); + + // Testing when we start localstorage with cached data, and with invalid preloaded data (invalid format or older than storage changenumber) -> emit SDK_READY_FROM_CACHE, and don't update storages + // @TODO Testing when we start localstorage with cached data but expired, and with newer preloaded data but also expired -> ??? + // @TODO Testing when we start localstorage from scrach, and with preloaded data but expired -> ??? + assert.test(assert => { + const invalidPreloadedData = [ + // invalid format + 'INVALID PRECACHED DATA', + // older data than storage changenumber + { ...preloadedDataWithSegments, since: 10 } + ]; + + invalidPreloadedData.forEach(prealoadedData => { - // Testing when we start inmemory, and with preloaded data (with segmentsData) -> emit SDK_READY_FROM_CACHE, and update storage and shared mySegments storages + assert.test(t => { + const prefix = 'readyFromCache_preloadedData3'; + const testUrls = { + sdk: 'https://sdk.baseurl/' + prefix, + events: 'https://events.baseurl/' + prefix + }; + localStorage.clear(); + localStorage.setItem(prefix + '.SPLITIO.splits.till', 25); + localStorage.setItem(prefix + '.SPLITIO.split.p1__split', splitSerializedDefinitions.p1__split); + + t.plan(3); + + fetchMock.getOnce(testUrls.sdk + '/splitChanges?since=25', { status: 200, body: { splits: [splitDefinitions.p1__split, splitDefinitions.p2__split], since: 25, till: 1457552620999 } }, { delay: 10 }); // short delay to let emit SDK_READY_FROM_CACHE + fetchMock.getOnce(testUrls.sdk + '/mySegments/nicolas%40split.io', { status: 200, body: { mySegments: [] } }); + + const splitio = SplitFactory({ + ...baseConfig, + storage: { + type: 'LOCALSTORAGE', + prefix: prefix, + preloadedData: prealoadedData + }, + urls: testUrls, + debug: true + }); + const client = splitio.client(); + const manager = splitio.manager(); + + client.once(client.Event.SDK_READY_FROM_CACHE, () => { + t.deepEqual(manager.names(), ['p1__split'], 'splits should be the ones in preloadedData'); + }); + + client.once(client.Event.SDK_READY, () => { + t.deepEqual(manager.names(), ['p1__split', 'p2__split'], 'splits should be the ones in preloadedData'); + + client.destroy().then(() => { + t.equal(localStorage.getItem(prefix + '.SPLITIO.splits.till'), '1457552620999', 'splits.till must correspond to the till of the last successfully fetched Splits'); + t.end(); + }); + }); + }); + + }); + }, 'readyFromCache_preloadedData3'); + + + /** Preloaded data in InMemory storage */ - // Testing when we start inmemory, and with preloaded data (with mySegmentsData) -> emit SDK_READY_FROM_CACHE, and update storage and only shared mySegments storages with existing user id + // @TODO Testing when we start inmemory, and with preloaded data (with segmentsData) -> emit SDK_READY_FROM_CACHE, and update storage and shared mySegments storages - // Testing when we start inmemory, and with preloaded data but invalid -> don't emit SDK_READY_FROM_CACHE, and don't update storages + // @TODO Testing when we start inmemory, and with preloaded data (with mySegmentsData) -> emit SDK_READY_FROM_CACHE, and update storage and only shared mySegments storages with existing user id - // Testing when we start inmemory, and with preloaded data but expired -> ??? + // @TODO Testing when we start inmemory, and with invalid preloaded data (invalid format) -> don't emit SDK_READY_FROM_CACHE, and don't update storages + // @TODO Testing when we start inmemory, and with invalid preloaded data (expired) -> ??? } diff --git a/src/__tests__/mocks/preloadedData.js b/src/__tests__/mocks/preloadedData.js index 95cb041a4..0a86d27bf 100644 --- a/src/__tests__/mocks/preloadedData.js +++ b/src/__tests__/mocks/preloadedData.js @@ -16,7 +16,7 @@ export const splitDefinitions = { }, }; -const splitSerializedDefinitions = (function () { +export const splitSerializedDefinitions = (function () { return Object.keys(splitDefinitions).reduce((acum, splitName) => { acum[splitName] = JSON.stringify(splitDefinitions[splitName]); return acum; @@ -30,7 +30,7 @@ export const segmentsDefinitions = { }, }; -const segmentsSerializedDefinitions = (function () { +export const segmentsSerializedDefinitions = (function () { return Object.keys(segmentsDefinitions).reduce((acum, segmentName) => { acum[segmentName] = JSON.stringify(segmentsDefinitions[segmentName]); return acum; diff --git a/src/storage/DataLoader.js b/src/storage/DataLoader.js index a63e18afd..94f8b7767 100644 --- a/src/storage/DataLoader.js +++ b/src/storage/DataLoader.js @@ -28,8 +28,8 @@ export function dataLoaderFactory(preloadedData = {}) { if (since <= currentSince) { return; } - // Split.IO recommends cleaning up the localStorage data - if (currentSince === -1) storage.splits.flush(); + // cleaning up the localStorage data, since some cached splits might need be part of the preloaded data + storage.splits.flush(); 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 From 08d252e6c6b6b9939488a2bf40243e8e48f4f647 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 20 Aug 2020 12:20:19 -0300 Subject: [PATCH 10/15] feedback --- package-lock.json | 2 +- package.json | 2 +- .../browserSuites/ready-from-cache.spec.js | 4 ++- src/storage/DataLoader.js | 25 ++++++++----------- src/storage/browser.js | 10 ++++---- src/utils/inputValidation/preloadedData.js | 13 ++++++---- src/utils/settings/storage/browser.js | 2 +- 7 files changed, 30 insertions(+), 28 deletions(-) diff --git a/package-lock.json b/package-lock.json index fbe863584..375ac80e6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio", - "version": "10.14.2-canary.0", + "version": "10.14.2-canary.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 435b8166f..9c5179fae 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio", - "version": "10.14.2-canary.0", + "version": "10.14.2-canary.1", "description": "Split SDK", "files": [ "README.md", diff --git a/src/__tests__/browserSuites/ready-from-cache.spec.js b/src/__tests__/browserSuites/ready-from-cache.spec.js index 4106f9b80..fd6b7449f 100644 --- a/src/__tests__/browserSuites/ready-from-cache.spec.js +++ b/src/__tests__/browserSuites/ready-from-cache.spec.js @@ -759,6 +759,7 @@ export default function (fetchMock, assert) { /** Preloaded data in InLocalStorage storage */ // Testing when we start localstorage from scrach, and with preloaded data (with segmentsData) -> emit SDK_READY_FROM_CACHE, and update storage and shared mySegments storages + // @TODO test shared storage ? assert.test(t => { const prefix = 'readyFromCache_preloadedData1'; const testUrls = { @@ -802,7 +803,8 @@ export default function (fetchMock, assert) { }); }, 'readyFromCache_preloadedData1'); - // Testing when we start localstorage with cached data, and with newer preloaded data (with mySegmentsData) -> emit SDK_READY_FROM_CACHE, and update storage and only shared mySegments storages with existing user id + // Testing when we start localstorage with cached data, and with newer preloaded data (with segmentsData) -> emit SDK_READY_FROM_CACHE, and update storage and only shared mySegments storages with existing user id + // @TODO use preloaded data with mySegmentsData instead, and test shared storage ? assert.test(t => { const prefix = 'readyFromCache_preloadedData2'; const testUrls = { diff --git a/src/storage/DataLoader.js b/src/storage/DataLoader.js index 94f8b7767..dd1b59295 100644 --- a/src/storage/DataLoader.js +++ b/src/storage/DataLoader.js @@ -16,18 +16,15 @@ export function dataLoaderFactory(preloadedData = {}) { */ return function loadData(storage, userId) { // Do not load data if current preloadedData is empty - if (Object.keys(preloadedData).length === 0) { - return; - } + if (Object.keys(preloadedData).length === 0) return; - const { segmentsData = {}, since = 0, splitsData = {} } = preloadedData; + const { segmentsData = {}, since = -1, splitsData = {} } = preloadedData; - const currentSince = storage.splits.getChangeNumber(); + const storedSince = storage.splits.getChangeNumber(); // Do not load data if current localStorage data is more recent - if (since <= currentSince) { - return; - } + if (storedSince > since) return; + // cleaning up the localStorage data, since some cached splits might need be part of the preloaded data storage.splits.flush(); storage.splits.setChangeNumber(since); @@ -38,15 +35,15 @@ export function dataLoaderFactory(preloadedData = {}) { }); // add mySegments data - let userIdMySegmentsData = preloadedData.mySegmentsData && preloadedData.mySegmentsData[userId]; - if (!userIdMySegmentsData) { + let mySegmentsData = preloadedData.mySegmentsData && preloadedData.mySegmentsData[userId]; + 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 - userIdMySegmentsData = Object.keys(segmentsData).filter(segmentName => { - const added = JSON.parse(segmentsData[segmentName]).added; - return added.indexOf(userId) > -1; + mySegmentsData = Object.keys(segmentsData).filter(segmentName => { + const userIds = JSON.parse(segmentsData[segmentName]).added; + return Array.isArray(userIds) && userIds.indexOf(userId) > -1; }); } - storage.segments.resetSegments(userIdMySegmentsData); + storage.segments.resetSegments(mySegmentsData); }; } \ No newline at end of file diff --git a/src/storage/browser.js b/src/storage/browser.js index 3511009ce..f9108c0ab 100644 --- a/src/storage/browser.js +++ b/src/storage/browser.js @@ -16,13 +16,13 @@ export const DEFAULT_CACHE_EXPIRATION_IN_MILLIS = 864000000; // 10 days const BrowserStorageFactory = context => { const settings = context.get(context.constants.SETTINGS); const { storage } = settings; - let result; + let instance; switch (storage.type) { case STORAGE_MEMORY: { const keys = new KeyBuilder(settings); - result = { + instance = { splits: new SplitCacheInMemory, segments: new SegmentCacheInMemory(keys), impressions: new ImpressionsCacheInMemory, @@ -65,7 +65,7 @@ const BrowserStorageFactory = context => { const keys = new KeyBuilderLocalStorage(settings); const expirationTimestamp = Date.now() - DEFAULT_CACHE_EXPIRATION_IN_MILLIS; - result = { + instance = { splits: new SplitCacheInLocalStorage(keys, expirationTimestamp, settings.sync.__splitFiltersValidation), segments: new SegmentCacheInLocalStorage(keys), impressions: new ImpressionsCacheInMemory, @@ -111,10 +111,10 @@ const BrowserStorageFactory = context => { // load precached data into storage if (storage.dataLoader) { const key = settings.core.key; - storage.dataLoader(result, key); + storage.dataLoader(instance, key); } - return result; + return instance; }; export default BrowserStorageFactory; \ No newline at end of file diff --git a/src/utils/inputValidation/preloadedData.js b/src/utils/inputValidation/preloadedData.js index 6e7a62fce..578cc8c47 100644 --- a/src/utils/inputValidation/preloadedData.js +++ b/src/utils/inputValidation/preloadedData.js @@ -1,11 +1,12 @@ -import { isObject, isString } from '../lang'; +import { isObject, isString, numberIsFinite } from '../lang'; +import { validateSplit } from '../inputValidation'; import logFactory from '../logger'; const log = logFactory('', { displayAllErrors: true }); function validateSinceData(maybeSince, method) { - if (maybeSince > -1) return true; + if (numberIsFinite(maybeSince) && maybeSince > -1) return true; log.error(`${method}: preloadedData.since must be a positive number.`); return false; } @@ -13,7 +14,9 @@ function validateSinceData(maybeSince, method) { function validateSplitsData(maybeSplitsData, method) { if (isObject(maybeSplitsData)) { const splitNames = Object.keys(maybeSplitsData); - if (splitNames.length > 0 && splitNames.every(splitName => isString(maybeSplitsData[splitName]))) return true; + if (splitNames.length === 0) log.warn(`${method}: preloadedData.splitsData doesn't contain split definitions.`); + // @TODO in the future, consider handling the possibility of having parsed definitions of splits + if (splitNames.every(splitName => validateSplit(splitName, method) && isString(maybeSplitsData[splitName]))) return true; } log.error(`${method}: preloadedData.splitsData must be a map of split names to their serialized definitions.`); return false; @@ -22,7 +25,7 @@ function validateSplitsData(maybeSplitsData, method) { function validateMySegmentsData(maybeMySegmentsData, method) { if (isObject(maybeMySegmentsData)) { const userKeys = Object.keys(maybeMySegmentsData); - if (userKeys.length > 0 && userKeys.every(userKey => { + if (userKeys.every(userKey => { const segmentNames = maybeMySegmentsData[userKey]; // an empty list is valid return Array.isArray(segmentNames) && segmentNames.every(segmentName => isString(segmentName)); @@ -35,7 +38,7 @@ function validateMySegmentsData(maybeMySegmentsData, method) { function validateSegmentsData(maybeSegmentsData, method) { if (isObject(maybeSegmentsData)) { const segmentNames = Object.keys(maybeSegmentsData); - if (segmentNames.length > 0 && segmentNames.every(segmentName => isString(maybeSegmentsData[segmentName]))) return true; + if (segmentNames.every(segmentName => isString(maybeSegmentsData[segmentName]))) return true; } log.error(`${method}: preloadedData.segmentsData must be a map of segment names to their serialized definitions.`); return false; diff --git a/src/utils/settings/storage/browser.js b/src/utils/settings/storage/browser.js index b9e5a1aef..25a8ccd61 100644 --- a/src/utils/settings/storage/browser.js +++ b/src/utils/settings/storage/browser.js @@ -59,7 +59,7 @@ const ParseStorageSettings = settings => { type, options, prefix, - dataLoader: validatePreloadedData(preloadedData, 'Factory instantiation - storage preload') ? dataLoaderFactory(preloadedData) : undefined + dataLoader: preloadedData && validatePreloadedData(preloadedData, 'Factory instantiation - storage preload') ? dataLoaderFactory(preloadedData) : undefined }; }; From 71db86e787e35b0e287d799920b81dcdd5bce476 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 20 Aug 2020 14:04:17 -0300 Subject: [PATCH 11/15] feedback on logger --- src/utils/inputValidation/apiKey.js | 1 + src/utils/inputValidation/preloadedData.js | 4 +--- src/utils/logger/index.js | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/utils/inputValidation/apiKey.js b/src/utils/inputValidation/apiKey.js index f50335c93..95bfe4eb8 100644 --- a/src/utils/inputValidation/apiKey.js +++ b/src/utils/inputValidation/apiKey.js @@ -1,6 +1,7 @@ import { isString } from '../lang'; import logFactory from '../logger'; const log = logFactory('', { + // Errors on API key validation are important enough so that one day we might force logging them or throw an exception on startup. displayAllErrors: true }); diff --git a/src/utils/inputValidation/preloadedData.js b/src/utils/inputValidation/preloadedData.js index 578cc8c47..582338fb1 100644 --- a/src/utils/inputValidation/preloadedData.js +++ b/src/utils/inputValidation/preloadedData.js @@ -1,9 +1,7 @@ import { isObject, isString, numberIsFinite } from '../lang'; import { validateSplit } from '../inputValidation'; import logFactory from '../logger'; -const log = logFactory('', { - displayAllErrors: true -}); +const log = logFactory(''); function validateSinceData(maybeSince, method) { if (numberIsFinite(maybeSince) && maybeSince > -1) return true; diff --git a/src/utils/logger/index.js b/src/utils/logger/index.js index aadc6e39e..f6a39cab7 100644 --- a/src/utils/logger/index.js +++ b/src/utils/logger/index.js @@ -41,7 +41,7 @@ const initialState = String( localStorage.getItem(LS_KEY) : '' ); -const createLog = (namespace, options = {}) => new Logger(namespace, merge(defaultOptions, options)); +const createLog = (namespace, options = {}) => new Logger(namespace, merge(options, defaultOptions)); const ownLog = createLog('splitio-utils:logger'); From 71f8662378f62203ce80f5f65bf29a7648b576dd Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Fri, 21 Aug 2020 14:42:25 -0300 Subject: [PATCH 12/15] validation UT --- .../inputValidation/preloadedData.spec.js | 162 ++++++++++++++++++ 1 file changed, 162 insertions(+) create mode 100644 src/utils/__tests__/inputValidation/preloadedData.spec.js diff --git a/src/utils/__tests__/inputValidation/preloadedData.spec.js b/src/utils/__tests__/inputValidation/preloadedData.spec.js new file mode 100644 index 000000000..8bb827ca2 --- /dev/null +++ b/src/utils/__tests__/inputValidation/preloadedData.spec.js @@ -0,0 +1,162 @@ +import tape from 'tape'; +import sinon from 'sinon'; +import proxyquire from 'proxyquire'; +const proxyquireStrict = proxyquire.noCallThru(); + +const loggerMock = { + warn: sinon.stub(), + error: sinon.stub(), +}; + +function LogFactoryMock() { + return loggerMock; +} + +// Import the module mocking the logger. +const validatePreloadedData = proxyquireStrict('../../inputValidation/preloadedData', { + '../logger': LogFactoryMock +}).validatePreloadedData; + +const method = 'some_method'; +const testCases = [ + // valid inputs + { + input: { since: 10, splitsData: {} }, + output: true, + warn: `${method}: preloadedData.splitsData doesn't contain split definitions.` + }, + { + input: { since: 10, splitsData: { 'some_split': 'SPLIT DEFINITION' } }, + output: true + }, + { + input: { since: 10, splitsData: { 'some_split': 'SPLIT DEFINITION' }, mySegmentsData: { 'some_key': [] } }, + output: true + }, + { + input: { since: 10, splitsData: { 'some_split': 'SPLIT DEFINITION' }, mySegmentsData: { 'some_key': [] } }, + output: true + }, + { + input: { since: 10, splitsData: { 'some_split': 'SPLIT DEFINITION' }, mySegmentsData: {} }, + output: true + }, + { + input: { since: 10, splitsData: { 'some_split': 'SPLIT DEFINITION' }, mySegmentsData: { some_key: [] } }, + output: true + }, + { + input: { since: 10, splitsData: { 'some_split': 'SPLIT DEFINITION' }, mySegmentsData: { some_key: ['some_segment'] } }, + output: true + }, + { + input: { since: 10, splitsData: { 'some_split': 'SPLIT DEFINITION' }, segmentsData: {} }, + output: true + }, + { + input: { since: 10, splitsData: { 'some_split': 'SPLIT DEFINITION' }, segmentsData: { some_segment: 'SEGMENT DEFINITION' } }, + output: true + }, + { + input: { since: 10, splitsData: { 'some_split': 'SPLIT DEFINITION' }, mySegmentsData: { some_key: ['some_segment'], some_other_key: ['some_segment'] }, segmentsData: { some_segment: 'SEGMENT DEFINITION', some_other_segment: 'SEGMENT DEFINITION' } }, + output: true + }, + { + msg: 'should be true, even using objects for strings and numbers or having extra properties', + input: { ignoredProperty: 'IGNORED', since: new Number(10), splitsData: { 'some_split': new String('SPLIT DEFINITION') }, mySegmentsData: { some_key: [new String('some_segment')] }, segmentsData: { some_segment: new String('SEGMENT DEFINITION') } }, + output: true + }, + + // invalid inputs + { + msg: 'should be false if preloadedData is not an object', + input: undefined, + output: false, + error: `${method}: preloadedData must be an object.` + }, + { + msg: 'should be false if preloadedData is not an object', + input: [], + output: false, + error: `${method}: preloadedData must be an object.` + }, + { + msg: 'should be false if since property is invalid', + input: { since: undefined, splitsData: {} }, + output: false, + error: `${method}: preloadedData.since must be a positive number.` + }, + { + msg: 'should be false if since property is invalid', + input: { since: -1, splitsData: {} }, + output: false, + error: `${method}: preloadedData.since must be a positive number.` + }, + { + msg: 'should be false if splitsData property is invalid', + input: { since: 10, splitsData: undefined }, + output: false, + error: `${method}: preloadedData.splitsData must be a map of split names to their serialized definitions.` + }, + { + msg: 'should be false if splitsData property is invalid', + input: { since: 10, splitsData: ['DEFINITION'] }, + output: false, + error: `${method}: preloadedData.splitsData must be a map of split names to their serialized definitions.` + }, + { + msg: 'should be false if splitsData property is invalid', + input: { since: 10, splitsData: { some_split: undefined } }, + output: false, + error: `${method}: preloadedData.splitsData must be a map of split names to their serialized definitions.` + }, + { + msg: 'should be false if mySegmentsData property is invalid', + input: { since: 10, splitsData: { some_split: 'DEFINITION' }, mySegmentsData: ['DEFINITION'] }, + output: false, + error: `${method}: preloadedData.mySegmentsData must be a map of user keys to their list of segment names.` + }, + { + msg: 'should be false if mySegmentsData property is invalid', + input: { since: 10, splitsData: { some_split: 'DEFINITION' }, mySegmentsData: { some_key: undefined } }, + output: false, + error: `${method}: preloadedData.mySegmentsData must be a map of user keys to their list of segment names.` + }, + { + msg: 'should be false if segmentsData property is invalid', + input: { since: 10, splitsData: { some_split: 'DEFINITION' }, segmentsData: ['DEFINITION'] }, + output: false, + error: `${method}: preloadedData.segmentsData must be a map of segment names to their serialized definitions.` + }, + { + msg: 'should be false if segmentsData property is invalid', + input: { since: 10, splitsData: { some_split: 'DEFINITION' }, segmentsData: { some_segment: undefined } }, + output: false, + error: `${method}: preloadedData.segmentsData must be a map of segment names to their serialized definitions.` + } +]; + +tape('INPUT VALIDATION for preloadedData', assert => { + + for (let i = 0; i < testCases.length; i++) { + const testCase = testCases[i]; + assert.equal(validatePreloadedData(testCase.input, method), testCase.output, testCase.msg); + + if (testCase.error) { + assert.ok(loggerMock.error.calledWithExactly(testCase.error), 'Should log the error for the invalid preloadedData.'); + loggerMock.warn.resetHistory(); + } else { + assert.true(loggerMock.error.notCalled, 'Should not log any error.'); + } + + if (testCase.warn) { + assert.ok(loggerMock.warn.calledWithExactly(testCase.warn), 'Should log the warning for the given preloadedData.'); + loggerMock.warn.resetHistory(); + } else { + assert.true(loggerMock.warn.notCalled, 'Should not log any warning.'); + } + } + + assert.end(); + +}); From edece8911f54b26329f8227ce907646b0ea9a873 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 24 Aug 2020 11:01:19 -0300 Subject: [PATCH 13/15] data expired --- src/storage/DataLoader.js | 10 ++-- .../inputValidation/preloadedData.spec.js | 52 ++++++++++++------- src/utils/inputValidation/preloadedData.js | 9 ++-- types/splitio.d.ts | 5 ++ 4 files changed, 49 insertions(+), 27 deletions(-) diff --git a/src/storage/DataLoader.js b/src/storage/DataLoader.js index dd1b59295..527f2c2f9 100644 --- a/src/storage/DataLoader.js +++ b/src/storage/DataLoader.js @@ -1,3 +1,5 @@ +import { DEFAULT_CACHE_EXPIRATION_IN_MILLIS } from './browser'; + /** * Factory of data loaders * @@ -18,12 +20,14 @@ export function dataLoaderFactory(preloadedData = {}) { // Do not load data if current preloadedData is empty if (Object.keys(preloadedData).length === 0) return; - const { segmentsData = {}, since = -1, splitsData = {} } = preloadedData; + const { lastUpdated = -1, segmentsData = {}, since = -1, splitsData = {} } = preloadedData; 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 - if (storedSince > since) return; + // 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; // cleaning up the localStorage data, since some cached splits might need be part of the preloaded data storage.splits.flush(); diff --git a/src/utils/__tests__/inputValidation/preloadedData.spec.js b/src/utils/__tests__/inputValidation/preloadedData.spec.js index 8bb827ca2..f36721919 100644 --- a/src/utils/__tests__/inputValidation/preloadedData.spec.js +++ b/src/utils/__tests__/inputValidation/preloadedData.spec.js @@ -21,49 +21,49 @@ const method = 'some_method'; const testCases = [ // valid inputs { - input: { since: 10, splitsData: {} }, + input: { lastUpdated: 10, since: 10, splitsData: {} }, output: true, warn: `${method}: preloadedData.splitsData doesn't contain split definitions.` }, { - input: { since: 10, splitsData: { 'some_split': 'SPLIT DEFINITION' } }, + input: { lastUpdated: 10, since: 10, splitsData: { 'some_split': 'SPLIT DEFINITION' } }, output: true }, { - input: { since: 10, splitsData: { 'some_split': 'SPLIT DEFINITION' }, mySegmentsData: { 'some_key': [] } }, + input: { lastUpdated: 10, since: 10, splitsData: { 'some_split': 'SPLIT DEFINITION' }, mySegmentsData: { 'some_key': [] } }, output: true }, { - input: { since: 10, splitsData: { 'some_split': 'SPLIT DEFINITION' }, mySegmentsData: { 'some_key': [] } }, + input: { lastUpdated: 10, since: 10, splitsData: { 'some_split': 'SPLIT DEFINITION' }, mySegmentsData: { 'some_key': [] } }, output: true }, { - input: { since: 10, splitsData: { 'some_split': 'SPLIT DEFINITION' }, mySegmentsData: {} }, + input: { lastUpdated: 10, since: 10, splitsData: { 'some_split': 'SPLIT DEFINITION' }, mySegmentsData: {} }, output: true }, { - input: { since: 10, splitsData: { 'some_split': 'SPLIT DEFINITION' }, mySegmentsData: { some_key: [] } }, + input: { lastUpdated: 10, since: 10, splitsData: { 'some_split': 'SPLIT DEFINITION' }, mySegmentsData: { some_key: [] } }, output: true }, { - input: { since: 10, splitsData: { 'some_split': 'SPLIT DEFINITION' }, mySegmentsData: { some_key: ['some_segment'] } }, + input: { lastUpdated: 10, since: 10, splitsData: { 'some_split': 'SPLIT DEFINITION' }, mySegmentsData: { some_key: ['some_segment'] } }, output: true }, { - input: { since: 10, splitsData: { 'some_split': 'SPLIT DEFINITION' }, segmentsData: {} }, + input: { lastUpdated: 10, since: 10, splitsData: { 'some_split': 'SPLIT DEFINITION' }, segmentsData: {} }, output: true }, { - input: { since: 10, splitsData: { 'some_split': 'SPLIT DEFINITION' }, segmentsData: { some_segment: 'SEGMENT DEFINITION' } }, + input: { lastUpdated: 10, since: 10, splitsData: { 'some_split': 'SPLIT DEFINITION' }, segmentsData: { some_segment: 'SEGMENT DEFINITION' } }, output: true }, { - input: { since: 10, splitsData: { 'some_split': 'SPLIT DEFINITION' }, mySegmentsData: { some_key: ['some_segment'], some_other_key: ['some_segment'] }, segmentsData: { some_segment: 'SEGMENT DEFINITION', some_other_segment: 'SEGMENT DEFINITION' } }, + input: { lastUpdated: 10, since: 10, splitsData: { 'some_split': 'SPLIT DEFINITION' }, mySegmentsData: { some_key: ['some_segment'], some_other_key: ['some_segment'] }, segmentsData: { some_segment: 'SEGMENT DEFINITION', some_other_segment: 'SEGMENT DEFINITION' } }, output: true }, { msg: 'should be true, even using objects for strings and numbers or having extra properties', - input: { ignoredProperty: 'IGNORED', since: new Number(10), splitsData: { 'some_split': new String('SPLIT DEFINITION') }, mySegmentsData: { some_key: [new String('some_segment')] }, segmentsData: { some_segment: new String('SEGMENT DEFINITION') } }, + input: { ignoredProperty: 'IGNORED', lastUpdated: new Number(10), since: new Number(10), splitsData: { 'some_split': new String('SPLIT DEFINITION') }, mySegmentsData: { some_key: [new String('some_segment')] }, segmentsData: { some_segment: new String('SEGMENT DEFINITION') } }, output: true }, @@ -80,57 +80,69 @@ const testCases = [ output: false, error: `${method}: preloadedData must be an object.` }, + { + msg: 'should be false if lastUpdated property is invalid', + input: { lastUpdated: undefined, since: 10, splitsData: {} }, + output: false, + error: `${method}: preloadedData.lastUpdated must be a positive number.` + }, + { + msg: 'should be false if lastUpdated property is invalid', + input: { lastUpdated: -1, since: 10, splitsData: {} }, + output: false, + error: `${method}: preloadedData.lastUpdated must be a positive number.` + }, { msg: 'should be false if since property is invalid', - input: { since: undefined, splitsData: {} }, + input: { lastUpdated: 10, since: undefined, splitsData: {} }, output: false, error: `${method}: preloadedData.since must be a positive number.` }, { msg: 'should be false if since property is invalid', - input: { since: -1, splitsData: {} }, + input: { lastUpdated: 10, since: -1, splitsData: {} }, output: false, error: `${method}: preloadedData.since must be a positive number.` }, { msg: 'should be false if splitsData property is invalid', - input: { since: 10, splitsData: undefined }, + input: { lastUpdated: 10, since: 10, splitsData: undefined }, output: false, error: `${method}: preloadedData.splitsData must be a map of split names to their serialized definitions.` }, { msg: 'should be false if splitsData property is invalid', - input: { since: 10, splitsData: ['DEFINITION'] }, + input: { lastUpdated: 10, since: 10, splitsData: ['DEFINITION'] }, output: false, error: `${method}: preloadedData.splitsData must be a map of split names to their serialized definitions.` }, { msg: 'should be false if splitsData property is invalid', - input: { since: 10, splitsData: { some_split: undefined } }, + input: { lastUpdated: 10, since: 10, splitsData: { some_split: undefined } }, output: false, error: `${method}: preloadedData.splitsData must be a map of split names to their serialized definitions.` }, { msg: 'should be false if mySegmentsData property is invalid', - input: { since: 10, splitsData: { some_split: 'DEFINITION' }, mySegmentsData: ['DEFINITION'] }, + input: { lastUpdated: 10, since: 10, splitsData: { some_split: 'DEFINITION' }, mySegmentsData: ['DEFINITION'] }, output: false, error: `${method}: preloadedData.mySegmentsData must be a map of user keys to their list of segment names.` }, { msg: 'should be false if mySegmentsData property is invalid', - input: { since: 10, splitsData: { some_split: 'DEFINITION' }, mySegmentsData: { some_key: undefined } }, + input: { lastUpdated: 10, since: 10, splitsData: { some_split: 'DEFINITION' }, mySegmentsData: { some_key: undefined } }, output: false, error: `${method}: preloadedData.mySegmentsData must be a map of user keys to their list of segment names.` }, { msg: 'should be false if segmentsData property is invalid', - input: { since: 10, splitsData: { some_split: 'DEFINITION' }, segmentsData: ['DEFINITION'] }, + input: { lastUpdated: 10, since: 10, splitsData: { some_split: 'DEFINITION' }, segmentsData: ['DEFINITION'] }, output: false, error: `${method}: preloadedData.segmentsData must be a map of segment names to their serialized definitions.` }, { msg: 'should be false if segmentsData property is invalid', - input: { since: 10, splitsData: { some_split: 'DEFINITION' }, segmentsData: { some_segment: undefined } }, + input: { lastUpdated: 10, since: 10, splitsData: { some_split: 'DEFINITION' }, segmentsData: { some_segment: undefined } }, output: false, error: `${method}: preloadedData.segmentsData must be a map of segment names to their serialized definitions.` } diff --git a/src/utils/inputValidation/preloadedData.js b/src/utils/inputValidation/preloadedData.js index 582338fb1..16a1ad9e2 100644 --- a/src/utils/inputValidation/preloadedData.js +++ b/src/utils/inputValidation/preloadedData.js @@ -3,9 +3,9 @@ import { validateSplit } from '../inputValidation'; import logFactory from '../logger'; const log = logFactory(''); -function validateSinceData(maybeSince, method) { - if (numberIsFinite(maybeSince) && maybeSince > -1) return true; - log.error(`${method}: preloadedData.since must be a positive number.`); +function validateTimestampData(maybeTimestamp, method, item) { + if (numberIsFinite(maybeTimestamp) && maybeTimestamp > -1) return true; + log.error(`${method}: preloadedData.${item} must be a positive number.`); return false; } @@ -46,7 +46,8 @@ export function validatePreloadedData(maybePreloadedData, method) { if (!isObject(maybePreloadedData)) { log.error(`${method}: preloadedData must be an object.`); } else if ( - validateSinceData(maybePreloadedData.since, method) && + validateTimestampData(maybePreloadedData.lastUpdated, method, 'lastUpdated') && + validateTimestampData(maybePreloadedData.since, method, 'since') && validateSplitsData(maybePreloadedData.splitsData, method) && (!maybePreloadedData.mySegmentsData || validateMySegmentsData(maybePreloadedData.mySegmentsData, method)) && (!maybePreloadedData.segmentsData || validateSegmentsData(maybePreloadedData.segmentsData, method)) diff --git a/types/splitio.d.ts b/types/splitio.d.ts index abb30f5cc..aeb0900e1 100644 --- a/types/splitio.d.ts +++ b/types/splitio.d.ts @@ -754,6 +754,11 @@ declare namespace SplitIO { * Defines the format of Split data to preload on the factory storage (cache). */ interface PreloadedData { + /** + * Timestamp of the last moment the data was synchronized with Split servers. + * If this value is older than 10 days ago (expiration time policy), the data is not used to update the storage content. + */ + 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. From 2758f2234db25b4cfd66115f5114ffb0072b7568 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 24 Aug 2020 11:15:20 -0300 Subject: [PATCH 14/15] added comments --- .../browserSuites/ready-from-cache.spec.js | 18 +++++++++--------- src/__tests__/mocks/preloadedData.js | 1 + src/storage/DataLoader.js | 2 ++ types/splitio.d.ts | 3 +++ 4 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/__tests__/browserSuites/ready-from-cache.spec.js b/src/__tests__/browserSuites/ready-from-cache.spec.js index fd6b7449f..cb35670a5 100644 --- a/src/__tests__/browserSuites/ready-from-cache.spec.js +++ b/src/__tests__/browserSuites/ready-from-cache.spec.js @@ -756,10 +756,11 @@ export default function (fetchMock, assert) { }); }); + /** Preloaded data in InLocalStorage storage */ // Testing when we start localstorage from scrach, and with preloaded data (with segmentsData) -> emit SDK_READY_FROM_CACHE, and update storage and shared mySegments storages - // @TODO test shared storage ? + // @TODO test shared storage, SDK_READY_FROM_CACHE with and without segments, and use preloaded data with mySegmentsData instead (update only shared mySegments storages with existing user id)? assert.test(t => { const prefix = 'readyFromCache_preloadedData1'; const testUrls = { @@ -804,7 +805,7 @@ export default function (fetchMock, assert) { }, 'readyFromCache_preloadedData1'); // Testing when we start localstorage with cached data, and with newer preloaded data (with segmentsData) -> emit SDK_READY_FROM_CACHE, and update storage and only shared mySegments storages with existing user id - // @TODO use preloaded data with mySegmentsData instead, and test shared storage ? + // @TODO test shared storage, SDK_READY_FROM_CACHE with and without segments, and use preloaded data with mySegmentsData instead (update only shared mySegments storages with existing user id)? assert.test(t => { const prefix = 'readyFromCache_preloadedData2'; const testUrls = { @@ -859,15 +860,16 @@ export default function (fetchMock, assert) { }); }, 'readyFromCache_preloadedData2'); - // Testing when we start localstorage with cached data, and with invalid preloaded data (invalid format or older than storage changenumber) -> emit SDK_READY_FROM_CACHE, and don't update storages + // Testing when we start localstorage with cached data, and with invalid preloaded data (invalid format, older date (i.e. changenumber older than storage changenumber), data expired (i.e. last update older than expiration time)) -> emit SDK_READY_FROM_CACHE, and don't update storages // @TODO Testing when we start localstorage with cached data but expired, and with newer preloaded data but also expired -> ??? - // @TODO Testing when we start localstorage from scrach, and with preloaded data but expired -> ??? assert.test(assert => { const invalidPreloadedData = [ // invalid format 'INVALID PRECACHED DATA', // older data than storage changenumber - { ...preloadedDataWithSegments, since: 10 } + { ...preloadedDataWithSegments, since: 10 }, + // expired data according to expiration policy + { ...preloadedDataWithSegments, lastUpdated: Date.now() - DEFAULT_CACHE_EXPIRATION_IN_MILLIS -1 }, // -1 to ensure having an expired lastUpdated item ]; invalidPreloadedData.forEach(prealoadedData => { @@ -921,10 +923,8 @@ export default function (fetchMock, assert) { /** Preloaded data in InMemory storage */ // @TODO Testing when we start inmemory, and with preloaded data (with segmentsData) -> emit SDK_READY_FROM_CACHE, and update storage and shared mySegments storages + // @TODO test shared storage, SDK_READY_FROM_CACHE with and without segments, and use preloaded data with mySegmentsData instead (update only shared mySegments storages with existing user id)? - // @TODO Testing when we start inmemory, and with preloaded data (with mySegmentsData) -> emit SDK_READY_FROM_CACHE, and update storage and only shared mySegments storages with existing user id - - // @TODO Testing when we start inmemory, and with invalid preloaded data (invalid format) -> don't emit SDK_READY_FROM_CACHE, and don't update storages - // @TODO Testing when we start inmemory, and with invalid preloaded data (expired) -> ??? + // @TODO Testing when we start inmemory, and with invalid preloaded data (invalid format, data expired (i.e. last update older than expiration time)) -> don't emit SDK_READY_FROM_CACHE, and don't update storages } diff --git a/src/__tests__/mocks/preloadedData.js b/src/__tests__/mocks/preloadedData.js index 0a86d27bf..c9d29260b 100644 --- a/src/__tests__/mocks/preloadedData.js +++ b/src/__tests__/mocks/preloadedData.js @@ -38,6 +38,7 @@ export const segmentsSerializedDefinitions = (function () { }()); export const preloadedDataWithSegments = { + lastUpdated: Date.now(), since: 1457552620999, splitsData: splitSerializedDefinitions, segmentsData: segmentsSerializedDefinitions diff --git a/src/storage/DataLoader.js b/src/storage/DataLoader.js index 527f2c2f9..af76db181 100644 --- a/src/storage/DataLoader.js +++ b/src/storage/DataLoader.js @@ -15,6 +15,8 @@ export function dataLoaderFactory(preloadedData = {}) { * * @param {Object} storage storage for client-side * @param {Object} userId main user key defined at the SDK config + * + * @TODO extend this function 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, userId) { // Do not load data if current preloadedData is empty diff --git a/types/splitio.d.ts b/types/splitio.d.ts index aeb0900e1..a62851034 100644 --- a/types/splitio.d.ts +++ b/types/splitio.d.ts @@ -757,6 +757,7 @@ declare namespace SplitIO { /** * Timestamp of the last moment the data was synchronized with Split servers. * 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, /** @@ -772,6 +773,7 @@ declare namespace SplitIO { }, /** * Optional map of user keys to their list of segments. + * @TODO remove when releasing first version */ mySegmentsData?: { [key: string]: string[] @@ -940,6 +942,7 @@ declare namespace SplitIO { /** * Split data to preload the storage. You may optionally specify it to quickly initialice and use the SDK with cached data. * If the data is valid, the SDK emits an SDK_READY_FROM_CACHE event once it is ready to be used. + * @TODO update the following link * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#preloaded-data} * @property {PreloadedData} preloadedData */ From b600457e33cb366de0413a66f3ffe95e65f4fadc Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 24 Aug 2020 12:02:00 -0300 Subject: [PATCH 15/15] fixed ts test --- ts-tests/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/ts-tests/index.ts b/ts-tests/index.ts index 2ec4119a4..437acdeb6 100644 --- a/ts-tests/index.ts +++ b/ts-tests/index.ts @@ -389,6 +389,7 @@ let splitFilters: SplitIO.SplitFilter[] = [{ type: 'byName', values: ['my_split_ // storage preloaded data let preloadedData: SplitIO.PreloadedData = { + lastUpdated: 10000, since: 10000, splitsData: { split_1: 'SPLIT_1_DEFINITION', split_2: 'SPLIT_2_DEFINITION'