diff --git a/package-lock.json b/package-lock.json index 1783cccc9..897057371 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio", - "version": "10.15.0", + "version": "10.15.1-canary.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index b42e281b5..1a0163448 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio", - "version": "10.15.0", + "version": "10.15.1-canary.0", "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 8f556e170..125014623 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, splitSerializedDefinitions, 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: '', @@ -471,14 +454,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, @@ -505,8 +488,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(); }); @@ -521,7 +504,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({ @@ -549,8 +532,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(); }); @@ -565,13 +548,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({ @@ -599,8 +582,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(); }); @@ -615,13 +598,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 @@ -651,8 +634,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(); }); @@ -680,13 +663,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'); @@ -714,9 +697,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(); }); @@ -734,13 +717,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({ @@ -768,12 +751,183 @@ 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 + // @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 = { + 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 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 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 = { + 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); + + 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(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'); + + 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, 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 -> ??? + assert.test(assert => { + const invalidPreloadedData = [ + // invalid format + 'INVALID PRECACHED DATA', + // older data than storage changenumber + { ...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 => { + + 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 */ + + // @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 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 new file mode 100644 index 000000000..c9d29260b --- /dev/null +++ b/src/__tests__/mocks/preloadedData.js @@ -0,0 +1,45 @@ +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': [] + }, +}; + +export 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'], + }, +}; + +export const segmentsSerializedDefinitions = (function () { + return Object.keys(segmentsDefinitions).reduce((acum, segmentName) => { + acum[segmentName] = JSON.stringify(segmentsDefinitions[segmentName]); + return acum; + }, {}); +}()); + +export const preloadedDataWithSegments = { + lastUpdated: Date.now(), + since: 1457552620999, + splitsData: splitSerializedDefinitions, + segmentsData: segmentsSerializedDefinitions +}; diff --git a/src/storage/DataLoader.js b/src/storage/DataLoader.js new file mode 100644 index 000000000..af76db181 --- /dev/null +++ b/src/storage/DataLoader.js @@ -0,0 +1,55 @@ +import { DEFAULT_CACHE_EXPIRATION_IN_MILLIS } from './browser'; + +/** + * Factory of data loaders + * + * @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(preloadedData = {}) { + + /** + * 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 + * + * @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 + if (Object.keys(preloadedData).length === 0) return; + + 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, + // 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(); + 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 + 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 + mySegmentsData = Object.keys(segmentsData).filter(segmentName => { + const userIds = JSON.parse(segmentsData[segmentName]).added; + return Array.isArray(userIds) && userIds.indexOf(userId) > -1; + }); + } + storage.segments.resetSegments(mySegmentsData); + }; + +} \ No newline at end of file diff --git a/src/storage/SplitCache/InMemory.js b/src/storage/SplitCache/InMemory.js index b060815bc..a11f22a3e 100644 --- a/src/storage/SplitCache/InMemory.js +++ b/src/storage/SplitCache/InMemory.js @@ -133,10 +133,11 @@ 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; + // @TODO rollback if we decide not to emit SDK_READY_FROM_CACHE using InMemory storage + return this.getChangeNumber() > -1; } } diff --git a/src/storage/browser.js b/src/storage/browser.js index 603a809f3..f9108c0ab 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 instance; switch (storage.type) { case STORAGE_MEMORY: { const keys = new KeyBuilder(settings); - return { + instance = { 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 { + instance = { splits: new SplitCacheInLocalStorage(keys, expirationTimestamp, settings.sync.__splitFiltersValidation), segments: new SegmentCacheInLocalStorage(keys), impressions: new ImpressionsCacheInMemory, @@ -99,12 +101,20 @@ const BrowserStorageFactory = context => { this.events.clear(); } }; + break; } default: throw new Error('Unsupported storage type'); } + // load precached data into storage + if (storage.dataLoader) { + const key = settings.core.key; + storage.dataLoader(instance, key); + } + + return instance; }; export default BrowserStorageFactory; \ No newline at end of file diff --git a/src/utils/__tests__/inputValidation/preloadedData.spec.js b/src/utils/__tests__/inputValidation/preloadedData.spec.js new file mode 100644 index 000000000..f36721919 --- /dev/null +++ b/src/utils/__tests__/inputValidation/preloadedData.spec.js @@ -0,0 +1,174 @@ +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: { lastUpdated: 10, since: 10, splitsData: {} }, + output: true, + warn: `${method}: preloadedData.splitsData doesn't contain split definitions.` + }, + { + input: { lastUpdated: 10, since: 10, splitsData: { 'some_split': 'SPLIT DEFINITION' } }, + output: true + }, + { + input: { lastUpdated: 10, since: 10, splitsData: { 'some_split': 'SPLIT DEFINITION' }, mySegmentsData: { 'some_key': [] } }, + output: true + }, + { + input: { lastUpdated: 10, since: 10, splitsData: { 'some_split': 'SPLIT DEFINITION' }, mySegmentsData: { 'some_key': [] } }, + output: true + }, + { + input: { lastUpdated: 10, since: 10, splitsData: { 'some_split': 'SPLIT DEFINITION' }, mySegmentsData: {} }, + output: true + }, + { + input: { lastUpdated: 10, since: 10, splitsData: { 'some_split': 'SPLIT DEFINITION' }, mySegmentsData: { some_key: [] } }, + output: true + }, + { + input: { lastUpdated: 10, since: 10, splitsData: { 'some_split': 'SPLIT DEFINITION' }, mySegmentsData: { some_key: ['some_segment'] } }, + output: true + }, + { + input: { lastUpdated: 10, since: 10, splitsData: { 'some_split': 'SPLIT DEFINITION' }, segmentsData: {} }, + output: true + }, + { + input: { lastUpdated: 10, since: 10, splitsData: { 'some_split': 'SPLIT DEFINITION' }, segmentsData: { some_segment: 'SEGMENT DEFINITION' } }, + output: true + }, + { + 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', 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 + }, + + // 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 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: { 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: { 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: { 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: { 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: { 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: { 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: { 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: { 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: { 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.` + } +]; + +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(); + +}); 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 new file mode 100644 index 000000000..16a1ad9e2 --- /dev/null +++ b/src/utils/inputValidation/preloadedData.js @@ -0,0 +1,58 @@ +import { isObject, isString, numberIsFinite } from '../lang'; +import { validateSplit } from '../inputValidation'; +import logFactory from '../logger'; +const log = logFactory(''); + +function validateTimestampData(maybeTimestamp, method, item) { + if (numberIsFinite(maybeTimestamp) && maybeTimestamp > -1) return true; + log.error(`${method}: preloadedData.${item} must be a positive number.`); + return false; +} + +function validateSplitsData(maybeSplitsData, method) { + if (isObject(maybeSplitsData)) { + const splitNames = Object.keys(maybeSplitsData); + 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; +} + +function validateMySegmentsData(maybeMySegmentsData, method) { + if (isObject(maybeMySegmentsData)) { + const userKeys = Object.keys(maybeMySegmentsData); + if (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}: preloadedData.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.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; +} + +export function validatePreloadedData(maybePreloadedData, method) { + if (!isObject(maybePreloadedData)) { + log.error(`${method}: preloadedData must be an object.`); + } else if ( + 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)) + ) { + return true; + } + return false; +} \ No newline at end of file diff --git a/src/utils/settings/storage/browser.js b/src/utils/settings/storage/browser.js index 2618e784b..25a8ccd61 100644 --- a/src/utils/settings/storage/browser.js +++ b/src/utils/settings/storage/browser.js @@ -22,6 +22,8 @@ import { STORAGE_MEMORY, STORAGE_LOCALSTORAGE } from '../../../utils/constants'; +import { validatePreloadedData } from '../../inputValidation/preloadedData'; +import { dataLoaderFactory } from '../../../storage/DataLoader'; const ParseStorageSettings = settings => { let { @@ -29,7 +31,8 @@ const ParseStorageSettings = settings => { storage: { type = STORAGE_MEMORY, options = {}, - prefix + prefix, + preloadedData }, } = settings; @@ -47,7 +50,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 +58,8 @@ const ParseStorageSettings = settings => { return { type, options, - prefix + prefix, + dataLoader: preloadedData && validatePreloadedData(preloadedData, 'Factory instantiation - storage preload') ? dataLoaderFactory(preloadedData) : undefined }; }; diff --git a/ts-tests/index.ts b/ts-tests/index.ts index 45eac38c3..9e4db15d6 100644 --- a/ts-tests/index.ts +++ b/ts-tests/index.ts @@ -387,6 +387,22 @@ 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 = { + lastUpdated: 10000, + 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 +458,8 @@ let fullBrowserSettings: SplitIO.IBrowserSettings = { features: mockedFeaturesMap, storage: { type: 'LOCALSTORAGE', - prefix: 'PREFIX' + prefix: 'PREFIX', + preloadedData: preloadedData }, impressionListener: impressionListener, debug: true, diff --git a/types/splitio.d.ts b/types/splitio.d.ts index 18b4c6c7a..470581f36 100644 --- a/types/splitio.d.ts +++ b/types/splitio.d.ts @@ -766,6 +766,42 @@ declare namespace SplitIO { * @typedef {string} ImpressionsMode */ type ImpressionsMode = 'OPTIMIZED' | 'DEBUG'; + /** + * 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. + * @TODO configurable expiration time policy? + */ + lastUpdated: number, + /** + * Change number of the preloaded data. + * If this value is older than the current changeNumber at the storage, the data is not used to update the storage content. + */ + since: number, + /** + * Map of splits to their serialized definitions. + */ + splitsData: { + [splitName: string]: string + }, + /** + * Optional map of user keys to their list of segments. + * @TODO remove when releasing first version + */ + 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 @@ -918,7 +954,15 @@ declare namespace SplitIO { * @property {string} prefix * @default SPLITIO */ - prefix?: string + prefix?: string, + /** + * 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 + */ + preloadedData?: PreloadedData, } /** * SDK integration settings for the Browser.