diff --git a/CHANGES.txt b/CHANGES.txt index 3dc954a..4dbca39 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,9 +1,16 @@ -0.7.1 (May XXX, 2025) - - Updated some transitive dependencies for vulnerability fixes. +1.0.0 (May 28, 2025) + - Added support for synchronizing feature flags with rule-based segments. These segments determine membership at runtime by evaluating their configured rules against the user attributes provided to the SDK. + - Added support for synchronizing feature flags with prerequisites. This allows customers to define dependency conditions between flags, which are evaluated before any allowlists or targeting rules. + - Added support for synchronizing SDK impressions with properties. + - Added `sync.requestOptions.getHeaderOverrides` configuration option to enhance Synchronizer HTTP request Headers for Authorization Frameworks. + - Updated @splitsoftware/splitio-commons package to version 2.3.0 and some transitive dependencies for vulnerability fixes and other improvements. + - BREAKING CHANGES: + - Dropped support for Node.js v8. The SDK now requires Node.js v14 or above. + - Removed internal ponyfills for the `Map` and `Set` global objects. The SDK now requires the runtime environment to support these features natively or provide a polyfill. 0.7.0 (August 5, 2024) - Added `sync.requestOptions.agent` option to allow passing a custom Node.js HTTP(S) Agent with specific configurations for the Synchronizer requests, like custom TLS settings or a network proxy (See https://help.split.io/hc/en-us/articles/4421513571469-Split-JavaScript-synchronizer-tools#proxy). - - Updated some transitive dependencies for vulnerability fixes. + - Updated @splitsoftware/splitio-commons package to version 1.16.0 and some transitive dependencies for vulnerability fixes. 0.6.0 (May 13, 2024) - Added a new configuration option `sync.flagSpecVersion` to specify the flags spec version of feature flag definitions to be fetched and stored. diff --git a/README.md b/README.md index e1f05a5..017a304 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ This package includes a set of JavaScript synchronization tools built based on t [![Twitter Follow](https://img.shields.io/twitter/follow/splitsoftware.svg?style=social&label=Follow&maxAge=1529000)](https://twitter.com/intent/follow?screen_name=splitsoftware) ## Compatibility -Split sync tools supports Node.js version 8 or higher. To run the tools in other JavaScript environments, the target environment must support ES6 (ECMAScript 2015) syntax, and provide built-in support or a global polyfill for Promises and Web Fetch API. +Split sync tools supports Node.js version 14 or higher. To run the tools in other JavaScript environments, the target environment must support ES6 (ECMAScript 2015) syntax, and provide built-in support or a global polyfill for Promises, Web Fetch API, Map and Set. ## Getting started Below is a simple example that describes the execution of the JavaScript Synchronizer: diff --git a/e2e/synchronizer.test.ts b/e2e/synchronizer.test.ts index 4bab15a..37e3bc9 100644 --- a/e2e/synchronizer.test.ts +++ b/e2e/synchronizer.test.ts @@ -66,7 +66,7 @@ describe('Synchronizer e2e tests', () => { describe('Runs Synchronizer for the [FIRST] time, and', () => { beforeAll(async () => { - fetchMock.getOnce(SERVER_MOCK_URL + '/splitChanges?s=1.1&since=-1', { status: 200, body: responseMocks.splitChanges[0] }); + fetchMock.getOnce(SERVER_MOCK_URL + '/splitChanges?s=1.3&since=-1&rbSince=-1', { status: 200, body: responseMocks.splitChanges[0] }); fetchMock.getOnce(SERVER_MOCK_URL + '/segmentChanges/test_maldo?since=-1', { status: 200, body: responseMocks.segmentChanges[0] }); fetchMock.getOnce(SERVER_MOCK_URL + '/segmentChanges/ENDIOS_PEREZ?since=-1', { status: 200, body: responseMocks.segmentChanges[1] }); fetchMock.getOnce(SERVER_MOCK_URL + '/segmentChanges/Lucas_Segments_Tests?since=-1', { status: 200, body: responseMocks.segmentChanges[2] }); @@ -116,6 +116,14 @@ describe('Synchronizer e2e tests', () => { expect(itemsSetB.sort()).toEqual(['TEST_DOC', 'TEST_MATIAS']); expect(itemsInexistentSet).toEqual([]); }); + + test('saves 1 rule-based segment', async () => { + const ruleBasedSegments = await _redisWrapper.getKeysByPrefix(`${REDIS_PREFIX}.rbsegment.*`); + expect(ruleBasedSegments).toHaveLength(1); + + expect(await _redisWrapper.get(`${REDIS_PREFIX}.rbsegments.till`)).toBe('100'); + }); + }); describe('Runs SDK Consumer with DEBUG impressions mode, and', () => { @@ -144,7 +152,7 @@ describe('Synchronizer e2e tests', () => { describe('Runs Synchronizer a [SECOND] time and', () => { beforeAll(async () => { - fetchMock.getOnce(SERVER_MOCK_URL + '/splitChanges?s=1.1&since=1619720346271', { status: 200, body: responseMocks.splitChanges[2] }); + fetchMock.getOnce(SERVER_MOCK_URL + '/splitChanges?s=1.3&since=1619720346271&rbSince=100', { status: 200, body: responseMocks.splitChanges[2] }); fetchMock.getOnce(SERVER_MOCK_URL + '/segmentChanges/test_maldo?since=1589906133231', { status: 200, body: responseMocks.segmentChanges[3] }); fetchMock.getOnce(SERVER_MOCK_URL + '/segmentChanges/Lucas_Segments_Tests?since=1617053238061', { status: 200, body: responseMocks.segmentChanges[6] }); @@ -224,7 +232,7 @@ describe('Synchronizer e2e tests', () => { }); test('Run Synchronizer and check that data was popped from Redis and sent to Split BE', async () => { - fetchMock.getOnce(SERVER_MOCK_URL + '/splitChanges?s=1.1&since=1619720346272', { status: 200, body: responseMocks.splitChanges[3] }); + fetchMock.getOnce(SERVER_MOCK_URL + '/splitChanges?s=1.3&since=1619720346272&rbSince=100', { status: 200, body: responseMocks.splitChanges[3] }); fetchMock.getOnce(SERVER_MOCK_URL + '/segmentChanges/test_maldo?since=1589906133231', { status: 200, body: responseMocks.segmentChanges[3] }); fetchMock.getOnce(SERVER_MOCK_URL + '/segmentChanges/Lucas_Segments_Tests?since=1617053238061', { status: 200, body: responseMocks.segmentChanges[6] }); @@ -272,7 +280,7 @@ describe('Synchronizer e2e tests', () => { }); test('Run Synchronizer and check that data was popped from Redis and sent to Split BE', async () => { - fetchMock.getOnce(SERVER_MOCK_URL + '/splitChanges?s=1.1&since=1619720346272', { status: 200, body: responseMocks.splitChanges[3] }); + fetchMock.getOnce(SERVER_MOCK_URL + '/splitChanges?s=1.3&since=1619720346272&rbSince=100', { status: 200, body: responseMocks.splitChanges[3] }); fetchMock.getOnce(SERVER_MOCK_URL + '/segmentChanges/test_maldo?since=1589906133231', { status: 200, body: responseMocks.segmentChanges[3] }); fetchMock.getOnce(SERVER_MOCK_URL + '/segmentChanges/Lucas_Segments_Tests?since=1617053238061', { status: 200, body: responseMocks.segmentChanges[6] }); @@ -347,7 +355,7 @@ describe('Synchronizer e2e tests - OPTIMIZED impressions mode & Flag Sets filter describe('Synchronizer runs the first time', () => { beforeAll(async () => { - fetchMock.getOnce(SERVER_MOCK_URL + '/splitChanges?s=1.1&since=-1&sets=set_b', { status: 200, body: responseMocks.splitChanges[0] }); + fetchMock.getOnce(SERVER_MOCK_URL + '/splitChanges?s=1.3&since=-1&rbSince=-1&sets=set_b', { status: 200, body: responseMocks.splitChanges[0] }); fetchMock.getOnce(SERVER_MOCK_URL + '/segmentChanges/test_maldo?since=-1', { status: 200, body: responseMocks.segmentChanges[0] }); fetchMock.getOnce(SERVER_MOCK_URL + '/segmentChanges/test_maldo?since=1589906133231', { status: 200, body: responseMocks.segmentChanges[3] }); @@ -402,7 +410,7 @@ describe('Synchronizer e2e tests - OPTIMIZED impressions mode & Flag Sets filter describe('Synchronizer runs a second time, and', () => { beforeAll(async () => { - fetchMock.getOnce(SERVER_MOCK_URL + '/splitChanges?s=1.1&since=1619720346271&sets=set_b', { status: 200, body: responseMocks.splitChanges[2] }); + fetchMock.getOnce(SERVER_MOCK_URL + '/splitChanges?s=1.3&since=1619720346271&rbSince=100&sets=set_b', { status: 200, body: responseMocks.splitChanges[2] }); fetchMock.getOnce(SERVER_MOCK_URL + '/segmentChanges/test_maldo?since=1589906133231', { status: 200, body: responseMocks.segmentChanges[3] }); await _synchronizer.execute(); @@ -460,7 +468,7 @@ describe('Synchronizer e2e tests - OPTIMIZED impressions mode & Flag Sets filter }, }); - fetchMock.getOnce(SERVER_MOCK_URL + '/splitChanges?s=1.1&since=1619720346272&sets=set_b', { status: 500 }); + fetchMock.getOnce(SERVER_MOCK_URL + '/splitChanges?s=1.3&since=1619720346272&rbSince=100&sets=set_b', { status: 500 }); fetchMock.getOnce(SERVER_MOCK_URL + '/segmentChanges/test_maldo?since=1589906133231', { status: 200, body: responseMocks.segmentChanges[3] }); expect(await synchronizer.execute()).toBe(false); @@ -477,7 +485,7 @@ describe('Synchronizer e2e tests - OPTIMIZED impressions mode & Flag Sets filter }, }); - fetchMock.getOnce(SERVER_MOCK_URL + '/splitChanges?s=1.1&since=-1&sets=set_b', { status: 500 }); + fetchMock.getOnce(SERVER_MOCK_URL + '/splitChanges?s=1.3&since=-1&rbSince=-1&sets=set_b', { status: 500 }); expect(await synchronizer.execute()).toBe(false); expect(keys.length).toBeGreaterThan(0); @@ -491,7 +499,7 @@ describe('Synchronizer - only Splits & Segments mode', () => { let executeImpressionsAndEventsCallSpy: jest.SpyInstance; beforeAll(async () => { - fetchMock.getOnce(SERVER_MOCK_URL + '/splitChanges?s=1.1&since=-1', { status: 200, body: responseMocks.splitChanges[0] }); + fetchMock.getOnce(SERVER_MOCK_URL + '/splitChanges?s=1.3&since=-1&rbSince=-1', { status: 200, body: responseMocks.splitChanges[0] }); fetchMock.getOnce(SERVER_MOCK_URL + '/segmentChanges/test_maldo?since=-1', { status: 200, body: responseMocks.segmentChanges[0] }); fetchMock.getOnce(SERVER_MOCK_URL + '/segmentChanges/Lucas_Segments_Tests?since=-1', { status: 200, body: responseMocks.segmentChanges[2] }); fetchMock.getOnce(SERVER_MOCK_URL + '/segmentChanges/test_maldo?since=1589906133231', { status: 200, body: responseMocks.segmentChanges[3] }); diff --git a/e2e/utils/SDKConsumerMode.ts b/e2e/utils/SDKConsumerMode.ts index 9214d1e..fad70aa 100644 --- a/e2e/utils/SDKConsumerMode.ts +++ b/e2e/utils/SDKConsumerMode.ts @@ -23,8 +23,7 @@ const config = { * Function to run an example SDK in Consumer mode, in order to generate Events and Impressions * to be then processed by the Synchronizer. * - * @param {ImpressionsMode} impressionsMode Impressions mode. - * @returns {Promise} + * @param impressionsMode - Impressions mode. */ export default function runSDKConsumer(impressionsMode: ImpressionsMode) { const factory = SplitFactory({ diff --git a/e2e/utils/redisAdapterWrapper.ts b/e2e/utils/redisAdapterWrapper.ts index 7416dd2..c1b4f7f 100644 --- a/e2e/utils/redisAdapterWrapper.ts +++ b/e2e/utils/redisAdapterWrapper.ts @@ -7,8 +7,8 @@ import { noopLogger } from '../../src/submitters/__tests__/commonUtils'; * Creates a storage wrapper that uses our RedisAdapter. * Operations fail until `connect` is resolved once the Redis 'ready' event is emitted. * - * @param {Object} redisOptions Redis options with the format expected at `settings.storage.options`. - * @returns {IPluggableStorageWrapper} Storage wrapper instance. + * @param redisOptions - Redis options with the format expected at `settings.storage.options`. + * @returns Storage wrapper instance. */ export default function redisAdapterWrapper(redisOptions: Record): IPluggableStorageWrapper { diff --git a/e2e/utils/responseMocks.json b/e2e/utils/responseMocks.json index f7e354d..33d51f7 100644 --- a/e2e/utils/responseMocks.json +++ b/e2e/utils/responseMocks.json @@ -1,692 +1,754 @@ { "splitChanges": [ { - "splits": [ - { - "trafficTypeName": "testTT", - "name": "MATIAS_TEST", - "trafficAllocation": 92, - "trafficAllocationSeed": 59120715, - "seed": -2094556730, - "status": "ACTIVE", - "killed": false, - "defaultTreatment": "off", - "changeNumber": 1619720346270, - "algo": 2, - "configurations": {}, - "sets": ["set_a"], - "conditions": [ - { - "conditionType": "ROLLOUT", - "matcherGroup": { - "combiner": "AND", - "matchers": [ - { - "keySelector": { - "trafficType": "account", - "attribute": "test" - }, - "matcherType": "MATCHES_STRING", - "negate": false, - "userDefinedSegmentMatcherData": null, - "whitelistMatcherData": null, - "unaryNumericMatcherData": null, - "betweenMatcherData": null, - "booleanMatcherData": null, - "dependencyMatcherData": null, - "stringMatcherData": "/matias/i" - } - ] - }, - "partitions": [ - { - "treatment": "on", - "size": 100 + "ff": { + "d": [ + { + "trafficTypeName": "testTT", + "name": "MATIAS_TEST", + "trafficAllocation": 92, + "trafficAllocationSeed": 59120715, + "seed": -2094556730, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "changeNumber": 1619720346270, + "algo": 2, + "configurations": {}, + "sets": [ + "set_a" + ], + "conditions": [ + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "account", + "attribute": "test" + }, + "matcherType": "MATCHES_STRING", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": "/matias/i" + } + ] }, - { - "treatment": "off", - "size": 0 - } - ], - "label": "test matches /matias/i" - }, - { - "conditionType": "ROLLOUT", - "matcherGroup": { - "combiner": "AND", - "matchers": [ - { - "keySelector": { - "trafficType": "account", - "attribute": null - }, - "matcherType": "ALL_KEYS", - "negate": false, - "userDefinedSegmentMatcherData": null, - "whitelistMatcherData": null, - "unaryNumericMatcherData": null, - "betweenMatcherData": null, - "booleanMatcherData": null, - "dependencyMatcherData": null, - "stringMatcherData": null + "partitions": [ + { + "treatment": "on", + "size": 100 + }, + { + "treatment": "off", + "size": 0 } - ] + ], + "label": "test matches /matias/i" }, - "partitions": [ - { - "treatment": "on", - "size": 0 + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "account", + "attribute": null + }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] }, - { - "treatment": "off", - "size": 100 - } - ], - "label": "default rule" - } - ] - }, - { - "trafficTypeName": "user", - "name": "TEST_MATIAS", - "trafficAllocation": 44, - "trafficAllocationSeed": -1207740278, - "seed": 203792729, - "status": "ACTIVE", - "killed": false, - "defaultTreatment": "off", - "changeNumber": 1619205925116, - "algo": 2, - "configurations": {}, - "sets": ["set_b"], - "conditions": [ - { - "conditionType": "WHITELIST", - "matcherGroup": { - "combiner": "AND", - "matchers": [ - { - "keySelector": null, - "matcherType": "IN_SEGMENT", - "negate": false, - "userDefinedSegmentMatcherData": { - "segmentName": "test_maldo" - }, - "whitelistMatcherData": null, - "unaryNumericMatcherData": null, - "betweenMatcherData": null, - "booleanMatcherData": null, - "dependencyMatcherData": null, - "stringMatcherData": null + "partitions": [ + { + "treatment": "on", + "size": 0 + }, + { + "treatment": "off", + "size": 100 } - ] - }, - "partitions": [ - { - "treatment": "on", - "size": 100 - } - ], - "label": "whitelisted segment" - }, - { - "conditionType": "ROLLOUT", - "matcherGroup": { - "combiner": "AND", - "matchers": [ - { - "keySelector": { - "trafficType": "user", - "attribute": null - }, - "matcherType": "ALL_KEYS", - "negate": false, - "userDefinedSegmentMatcherData": null, - "whitelistMatcherData": null, - "unaryNumericMatcherData": null, - "betweenMatcherData": null, - "booleanMatcherData": null, - "dependencyMatcherData": null, - "stringMatcherData": null + ], + "label": "default rule" + } + ] + }, + { + "trafficTypeName": "user", + "name": "TEST_MATIAS", + "trafficAllocation": 44, + "trafficAllocationSeed": -1207740278, + "seed": 203792729, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "changeNumber": 1619205925116, + "algo": 2, + "configurations": {}, + "sets": [ + "set_b" + ], + "conditions": [ + { + "conditionType": "WHITELIST", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": null, + "matcherType": "IN_SEGMENT", + "negate": false, + "userDefinedSegmentMatcherData": { + "segmentName": "test_maldo" + }, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 100 } - ] + ], + "label": "whitelisted segment" }, - "partitions": [ - { - "treatment": "on", - "size": 100 - }, - { - "treatment": "off", - "size": 0 - }, - { - "treatment": "PITY_MARTINEZ", - "size": 0 + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": null + }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] }, - { - "treatment": "JUANFER_QUINTERO", - "size": 0 - }, - { - "treatment": "LUQUITAS_PRATTO", - "size": 0 - }, - { - "treatment": "ENZO_PEREZ", - "size": 0 - }, - { - "treatment": "REDO", - "size": 0 - } - ], - "label": "default rule" - } - ] - }, - { - "trafficTypeName": "user", - "name": "TEST_DOC", - "trafficAllocation": 100, - "trafficAllocationSeed": -1845986406, - "seed": 255141922, - "status": "ACTIVE", - "killed": false, - "defaultTreatment": "off", - "changeNumber": 1555536480284, - "algo": 2, - "configurations": { - "on": "{\"ojoijoii\":\"oijoijioj\",\"\":\"\"}" + "partitions": [ + { + "treatment": "on", + "size": 100 + }, + { + "treatment": "off", + "size": 0 + }, + { + "treatment": "PITY_MARTINEZ", + "size": 0 + }, + { + "treatment": "JUANFER_QUINTERO", + "size": 0 + }, + { + "treatment": "LUQUITAS_PRATTO", + "size": 0 + }, + { + "treatment": "ENZO_PEREZ", + "size": 0 + }, + { + "treatment": "REDO", + "size": 0 + } + ], + "label": "default rule" + } + ] }, - "sets": ["set_a", "set_b"], - "conditions": [ - { - "conditionType": "ROLLOUT", - "matcherGroup": { - "combiner": "AND", - "matchers": [ - { - "keySelector": { - "trafficType": "desded", - "attribute": null - }, - "matcherType": "ALL_KEYS", - "negate": false, - "userDefinedSegmentMatcherData": null, - "whitelistMatcherData": null, - "unaryNumericMatcherData": null, - "betweenMatcherData": null, - "booleanMatcherData": null, - "dependencyMatcherData": null, - "stringMatcherData": null + { + "trafficTypeName": "user", + "name": "TEST_DOC", + "trafficAllocation": 100, + "trafficAllocationSeed": -1845986406, + "seed": 255141922, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "changeNumber": 1555536480284, + "algo": 2, + "configurations": { + "on": "{\"ojoijoii\":\"oijoijioj\",\"\":\"\"}" + }, + "sets": [ + "set_a", + "set_b" + ], + "conditions": [ + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "desded", + "attribute": null + }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 0 + }, + { + "treatment": "off", + "size": 100 } - ] - }, - "partitions": [ - { - "treatment": "on", - "size": 0 + ], + "label": "default rule" + } + ] + }, + { + "trafficTypeName": "user", + "name": "Lucas_Split", + "trafficAllocation": 32, + "trafficAllocationSeed": 2048379668, + "seed": 871802730, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "v1", + "changeNumber": 1619205566698, + "algo": 2, + "configurations": {}, + "sets": [], + "conditions": [ + { + "conditionType": "WHITELIST", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": null, + "matcherType": "IN_SEGMENT", + "negate": false, + "userDefinedSegmentMatcherData": { + "segmentName": "Lucas_Segments_Tests" + }, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] }, - { - "treatment": "off", - "size": 100 - } - ], - "label": "default rule" - } - ] - }, - { - "trafficTypeName": "user", - "name": "Lucas_Split", - "trafficAllocation": 32, - "trafficAllocationSeed": 2048379668, - "seed": 871802730, - "status": "ACTIVE", - "killed": false, - "defaultTreatment": "v1", - "changeNumber": 1619205566698, - "algo": 2, - "configurations": {}, - "sets": [], - "conditions": [ - { - "conditionType": "WHITELIST", - "matcherGroup": { - "combiner": "AND", - "matchers": [ - { - "keySelector": null, - "matcherType": "IN_SEGMENT", - "negate": false, - "userDefinedSegmentMatcherData": { - "segmentName": "Lucas_Segments_Tests" - }, - "whitelistMatcherData": null, - "unaryNumericMatcherData": null, - "betweenMatcherData": null, - "booleanMatcherData": null, - "dependencyMatcherData": null, - "stringMatcherData": null + "partitions": [ + { + "treatment": "on", + "size": 100 } - ] + ], + "label": "whitelisted segment" }, - "partitions": [ - { - "treatment": "on", - "size": 100 - } - ], - "label": "whitelisted segment" - }, - { - "conditionType": "ROLLOUT", - "matcherGroup": { - "combiner": "AND", - "matchers": [ - { - "keySelector": { - "trafficType": "user", - "attribute": null - }, - "matcherType": "IN_SEGMENT", - "negate": false, - "userDefinedSegmentMatcherData": { - "segmentName": "Lucas_Segments_Tests" - }, - "whitelistMatcherData": null, - "unaryNumericMatcherData": null, - "betweenMatcherData": null, - "booleanMatcherData": null, - "dependencyMatcherData": null, - "stringMatcherData": null + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": null + }, + "matcherType": "IN_SEGMENT", + "negate": false, + "userDefinedSegmentMatcherData": { + "segmentName": "Lucas_Segments_Tests" + }, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 0 + }, + { + "treatment": "off", + "size": 0 + }, + { + "treatment": "v1", + "size": 100 } - ] + ], + "label": "in segment Lucas_Segments_Tests" }, - "partitions": [ - { - "treatment": "on", - "size": 0 - }, - { - "treatment": "off", - "size": 0 + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": null + }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] }, + "partitions": [ + { + "treatment": "on", + "size": 0 + }, + { + "treatment": "off", + "size": 100 + }, + { + "treatment": "v1", + "size": 0 + } + ], + "label": "default rule" + } + ] + } + ], + "s": -1, + "t": 1619720346271 + }, + "rbs": { + "s": -1, + "t": 100, + "d": [ + { + "changeNumber": 5, + "name": "test_rule_based_segment", + "status": "ACTIVE", + "trafficTypeName": "user", + "excluded": { + "keys": [ + "mauro@split.io", + "gaston@split.io" + ], + "segments": [ { - "treatment": "v1", - "size": 100 + "type": "standard", + "name": "test_maldo" } - ], - "label": "in segment Lucas_Segments_Tests" + ] }, - { - "conditionType": "ROLLOUT", - "matcherGroup": { - "combiner": "AND", - "matchers": [ - { - "keySelector": { - "trafficType": "user", - "attribute": null - }, - "matcherType": "ALL_KEYS", - "negate": false, - "userDefinedSegmentMatcherData": null, - "whitelistMatcherData": null, - "unaryNumericMatcherData": null, - "betweenMatcherData": null, - "booleanMatcherData": null, - "dependencyMatcherData": null, - "stringMatcherData": null - } - ] - }, - "partitions": [ - { - "treatment": "on", - "size": 0 - }, - { - "treatment": "off", - "size": 100 - }, - { - "treatment": "v1", - "size": 0 + "conditions": [ + { + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user" + }, + "matcherType": "ENDS_WITH", + "negate": false, + "whitelistMatcherData": { + "whitelist": [ + "@split.io" + ] + } + } + ] } - ], - "label": "default rule" - } - ] - } - ], - "since": -1, - "till": 1619720346271 + } + ] + } + ] + } }, { - "splits": [], - "since": 1619720346271, - "till": 1619720346271 + "ff": { + "d": [], + "s": 1619720346271, + "t": 1619720346271 + } }, { - "splits": [ - { - "trafficTypeName": "account", - "name": "TEST_RULO", - "trafficAllocation": 100, - "trafficAllocationSeed": -1845986406, - "seed": 255141922, - "status": "ACTIVE", - "killed": false, - "defaultTreatment": "off", - "changeNumber": 1555536480284, - "algo": 2, - "configurations": { - "on": "{\"ojoijoii\":\"oijoijioj\",\"\":\"\"}" - }, - "conditions": [ - { - "conditionType": "ROLLOUT", - "matcherGroup": { - "combiner": "AND", - "matchers": [ - { - "keySelector": { - "trafficType": "desded", - "attribute": null - }, - "matcherType": "ALL_KEYS", - "negate": false, - "userDefinedSegmentMatcherData": null, - "whitelistMatcherData": null, - "unaryNumericMatcherData": null, - "betweenMatcherData": null, - "booleanMatcherData": null, - "dependencyMatcherData": null, - "stringMatcherData": null - } - ] - }, - "partitions": [ - { - "treatment": "on", - "size": 0 + "ff": { + "d": [ + { + "trafficTypeName": "account", + "name": "TEST_RULO", + "trafficAllocation": 100, + "trafficAllocationSeed": -1845986406, + "seed": 255141922, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "changeNumber": 1555536480284, + "algo": 2, + "configurations": { + "on": "{\"ojoijoii\":\"oijoijioj\",\"\":\"\"}" + }, + "conditions": [ + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "desded", + "attribute": null + }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] }, - { - "treatment": "off", - "size": 100 - } - ], - "label": "default rule" - } - ] - }, - { - "trafficTypeName": "account", - "name": "MATIAS_TEST", - "trafficAllocation": 92, - "trafficAllocationSeed": 59120715, - "seed": -2094556730, - "status": "ACTIVE", - "killed": false, - "defaultTreatment": "off", - "changeNumber": 1619720346272, - "algo": 2, - "configurations": {}, - "sets": ["set_c"], - "conditions": [ - { - "conditionType": "ROLLOUT", - "matcherGroup": { - "combiner": "AND", - "matchers": [ - { - "keySelector": { - "trafficType": "account", - "attribute": "test" - }, - "matcherType": "MATCHES_STRING", - "negate": false, - "userDefinedSegmentMatcherData": null, - "whitelistMatcherData": null, - "unaryNumericMatcherData": null, - "betweenMatcherData": null, - "booleanMatcherData": null, - "dependencyMatcherData": null, - "stringMatcherData": "/matias/i" + "partitions": [ + { + "treatment": "on", + "size": 0 + }, + { + "treatment": "off", + "size": 100 } - ] - }, - "partitions": [ - { - "treatment": "on", - "size": 100 + ], + "label": "default rule" + } + ] + }, + { + "trafficTypeName": "account", + "name": "MATIAS_TEST", + "trafficAllocation": 92, + "trafficAllocationSeed": 59120715, + "seed": -2094556730, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "changeNumber": 1619720346272, + "algo": 2, + "configurations": {}, + "sets": [ + "set_c" + ], + "conditions": [ + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "account", + "attribute": "test" + }, + "matcherType": "MATCHES_STRING", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": "/matias/i" + } + ] }, - { - "treatment": "off", - "size": 0 - } - ], - "label": "test matches /matias/i" - }, - { - "conditionType": "ROLLOUT", - "matcherGroup": { - "combiner": "AND", - "matchers": [ - { - "keySelector": { - "trafficType": "account", - "attribute": null - }, - "matcherType": "ALL_KEYS", - "negate": false, - "userDefinedSegmentMatcherData": null, - "whitelistMatcherData": null, - "unaryNumericMatcherData": null, - "betweenMatcherData": null, - "booleanMatcherData": null, - "dependencyMatcherData": null, - "stringMatcherData": null + "partitions": [ + { + "treatment": "on", + "size": 100 + }, + { + "treatment": "off", + "size": 0 } - ] + ], + "label": "test matches /matias/i" }, - "partitions": [ - { - "treatment": "on", - "size": 0 + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "account", + "attribute": null + }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] }, - { - "treatment": "off", - "size": 100 - } - ], - "label": "default rule" - } - ] - }, - { - "trafficTypeName": "user", - "name": "TEST_DOC", - "trafficAllocation": 100, - "trafficAllocationSeed": -1845986406, - "seed": 255141922, - "status": "ACTIVE", - "killed": false, - "defaultTreatment": "on", - "changeNumber": 1619720346272, - "algo": 2, - "configurations": { - "on": "{\"ojoijoii\":\"oijoijioj\",\"\":\"\"}" - }, - "conditions": [ - { - "conditionType": "ROLLOUT", - "matcherGroup": { - "combiner": "AND", - "matchers": [ - { - "keySelector": { - "trafficType": "desded", - "attribute": null - }, - "matcherType": "ALL_KEYS", - "negate": false, - "userDefinedSegmentMatcherData": null, - "whitelistMatcherData": null, - "unaryNumericMatcherData": null, - "betweenMatcherData": null, - "booleanMatcherData": null, - "dependencyMatcherData": null, - "stringMatcherData": null + "partitions": [ + { + "treatment": "on", + "size": 0 + }, + { + "treatment": "off", + "size": 100 } - ] - }, - "partitions": [ - { - "treatment": "on", - "size": 0 + ], + "label": "default rule" + } + ] + }, + { + "trafficTypeName": "user", + "name": "TEST_DOC", + "trafficAllocation": 100, + "trafficAllocationSeed": -1845986406, + "seed": 255141922, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "on", + "changeNumber": 1619720346272, + "algo": 2, + "configurations": { + "on": "{\"ojoijoii\":\"oijoijioj\",\"\":\"\"}" + }, + "conditions": [ + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "desded", + "attribute": null + }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] }, - { - "treatment": "off", - "size": 100 - } - ], - "label": "default rule" - } - ] - }, - { - "trafficTypeName": "user", - "name": "Lucas_Split", - "trafficAllocation": 32, - "trafficAllocationSeed": 2048379668, - "seed": 871802730, - "status": "ARCHIVED", - "killed": false, - "defaultTreatment": "v1", - "changeNumber": 1619720346272, - "algo": 2, - "configurations": {}, - "conditions": [ - { - "conditionType": "WHITELIST", - "matcherGroup": { - "combiner": "AND", - "matchers": [ - { - "keySelector": null, - "matcherType": "IN_SEGMENT", - "negate": false, - "userDefinedSegmentMatcherData": { - "segmentName": "Lucas_Segments_Tests" - }, - "whitelistMatcherData": null, - "unaryNumericMatcherData": null, - "betweenMatcherData": null, - "booleanMatcherData": null, - "dependencyMatcherData": null, - "stringMatcherData": null + "partitions": [ + { + "treatment": "on", + "size": 0 + }, + { + "treatment": "off", + "size": 100 } - ] - }, - "partitions": [ - { - "treatment": "on", - "size": 100 - } - ], - "label": "whitelisted segment" - }, - { - "conditionType": "ROLLOUT", - "matcherGroup": { - "combiner": "AND", - "matchers": [ - { - "keySelector": { - "trafficType": "user", - "attribute": null - }, - "matcherType": "IN_SEGMENT", - "negate": false, - "userDefinedSegmentMatcherData": { - "segmentName": "Lucas_Segments_Tests" - }, - "whitelistMatcherData": null, - "unaryNumericMatcherData": null, - "betweenMatcherData": null, - "booleanMatcherData": null, - "dependencyMatcherData": null, - "stringMatcherData": null + ], + "label": "default rule" + } + ] + }, + { + "trafficTypeName": "user", + "name": "Lucas_Split", + "trafficAllocation": 32, + "trafficAllocationSeed": 2048379668, + "seed": 871802730, + "status": "ARCHIVED", + "killed": false, + "defaultTreatment": "v1", + "changeNumber": 1619720346272, + "algo": 2, + "configurations": {}, + "conditions": [ + { + "conditionType": "WHITELIST", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": null, + "matcherType": "IN_SEGMENT", + "negate": false, + "userDefinedSegmentMatcherData": { + "segmentName": "Lucas_Segments_Tests" + }, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 100 } - ] + ], + "label": "whitelisted segment" }, - "partitions": [ - { - "treatment": "on", - "size": 0 - }, - { - "treatment": "off", - "size": 0 + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": null + }, + "matcherType": "IN_SEGMENT", + "negate": false, + "userDefinedSegmentMatcherData": { + "segmentName": "Lucas_Segments_Tests" + }, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] }, - { - "treatment": "v1", - "size": 100 - } - ], - "label": "in segment Lucas_Segments_Tests" - }, - { - "conditionType": "ROLLOUT", - "matcherGroup": { - "combiner": "AND", - "matchers": [ - { - "keySelector": { - "trafficType": "user", - "attribute": null - }, - "matcherType": "ALL_KEYS", - "negate": false, - "userDefinedSegmentMatcherData": null, - "whitelistMatcherData": null, - "unaryNumericMatcherData": null, - "betweenMatcherData": null, - "booleanMatcherData": null, - "dependencyMatcherData": null, - "stringMatcherData": null + "partitions": [ + { + "treatment": "on", + "size": 0 + }, + { + "treatment": "off", + "size": 0 + }, + { + "treatment": "v1", + "size": 100 } - ] + ], + "label": "in segment Lucas_Segments_Tests" }, - "partitions": [ - { - "treatment": "on", - "size": 0 - }, - { - "treatment": "off", - "size": 100 + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": null + }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] }, - { - "treatment": "v1", - "size": 0 - } - ], - "label": "default rule" - } - ] - } - ], - "since": 1619720346271, - "till": 1619720346272 + "partitions": [ + { + "treatment": "on", + "size": 0 + }, + { + "treatment": "off", + "size": 100 + }, + { + "treatment": "v1", + "size": 0 + } + ], + "label": "default rule" + } + ] + } + ], + "s": 1619720346271, + "t": 1619720346272 + } }, { - "splits": [], - "since": 1619720346272, - "till": 1619720346272 + "ff": { + "d": [], + "s": 1619720346272, + "t": 1619720346272 + } } ], "segmentChanges": [ diff --git a/package-lock.json b/package-lock.json index 4e1637a..056a286 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,15 @@ { "name": "@splitsoftware/splitio-sync-tools", - "version": "0.7.0", + "version": "1.0.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@splitsoftware/splitio-sync-tools", - "version": "0.7.0", + "version": "1.0.0", "license": "Apache-2.0", "dependencies": { - "@splitsoftware/splitio-commons": "1.16.0", + "@splitsoftware/splitio-commons": "2.4.0", "dotenv": "^9.0.1", "node-fetch": "^2.7.0", "yargs": "^17.0.1" @@ -1693,10 +1693,12 @@ } }, "node_modules/@splitsoftware/splitio-commons": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/@splitsoftware/splitio-commons/-/splitio-commons-1.16.0.tgz", - "integrity": "sha512-k16cCWJOWut/NB5W1d9hQEYPxFrZXO66manp+8d6RjZYH4r+Q6lu82NYjDcfh5E93H9v+TVKcQLAmpVofbjcvg==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@splitsoftware/splitio-commons/-/splitio-commons-2.4.0.tgz", + "integrity": "sha512-VjrzXe7zDM5oi+VWfNNAu1DtcsZl1he8c/MeC4O2SiNRid+Nurzs0ROziHEcBt/4nnCI7vZMNdM4FCcnZHMccA==", + "license": "Apache-2.0", "dependencies": { + "@types/ioredis": "^4.28.0", "tslib": "^2.3.1" }, "peerDependencies": { @@ -1800,7 +1802,6 @@ "version": "4.28.10", "resolved": "https://registry.npmjs.org/@types/ioredis/-/ioredis-4.28.10.tgz", "integrity": "sha512-69LyhUgrXdgcNDv7ogs1qXZomnfOEnSmrmMFqKgt1XMJxmoOSG/u3wYy13yACIfKuMJ8IhKgHafDO3sx19zVQQ==", - "dev": true, "dependencies": { "@types/node": "*" } @@ -1855,8 +1856,7 @@ "node_modules/@types/node": { "version": "15.14.9", "resolved": "https://registry.npmjs.org/@types/node/-/node-15.14.9.tgz", - "integrity": "sha512-qjd88DrCxupx/kJD5yQgZdcYKZKSIGBVDIBE1/LTGcNm3d2Np/jxojkdePDdfnBHJc5W7vSMpbJ1aB7p/Py69A==", - "dev": true + "integrity": "sha512-qjd88DrCxupx/kJD5yQgZdcYKZKSIGBVDIBE1/LTGcNm3d2Np/jxojkdePDdfnBHJc5W7vSMpbJ1aB7p/Py69A==" }, "node_modules/@types/node-fetch": { "version": "2.6.12", @@ -9208,10 +9208,11 @@ } }, "@splitsoftware/splitio-commons": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/@splitsoftware/splitio-commons/-/splitio-commons-1.16.0.tgz", - "integrity": "sha512-k16cCWJOWut/NB5W1d9hQEYPxFrZXO66manp+8d6RjZYH4r+Q6lu82NYjDcfh5E93H9v+TVKcQLAmpVofbjcvg==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@splitsoftware/splitio-commons/-/splitio-commons-2.4.0.tgz", + "integrity": "sha512-VjrzXe7zDM5oi+VWfNNAu1DtcsZl1he8c/MeC4O2SiNRid+Nurzs0ROziHEcBt/4nnCI7vZMNdM4FCcnZHMccA==", "requires": { + "@types/ioredis": "^4.28.0", "tslib": "^2.3.1" } }, @@ -9287,7 +9288,6 @@ "version": "4.28.10", "resolved": "https://registry.npmjs.org/@types/ioredis/-/ioredis-4.28.10.tgz", "integrity": "sha512-69LyhUgrXdgcNDv7ogs1qXZomnfOEnSmrmMFqKgt1XMJxmoOSG/u3wYy13yACIfKuMJ8IhKgHafDO3sx19zVQQ==", - "dev": true, "requires": { "@types/node": "*" } @@ -9341,8 +9341,7 @@ "@types/node": { "version": "15.14.9", "resolved": "https://registry.npmjs.org/@types/node/-/node-15.14.9.tgz", - "integrity": "sha512-qjd88DrCxupx/kJD5yQgZdcYKZKSIGBVDIBE1/LTGcNm3d2Np/jxojkdePDdfnBHJc5W7vSMpbJ1aB7p/Py69A==", - "dev": true + "integrity": "sha512-qjd88DrCxupx/kJD5yQgZdcYKZKSIGBVDIBE1/LTGcNm3d2Np/jxojkdePDdfnBHJc5W7vSMpbJ1aB7p/Py69A==" }, "@types/node-fetch": { "version": "2.6.12", diff --git a/package.json b/package.json index 61a23cf..740ef25 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-sync-tools", - "version": "0.7.0", + "version": "1.0.0", "description": "Split JavaScript Sync Tools", "main": "lib/cjs/index.js", "module": "lib/esm/index.js", @@ -36,7 +36,7 @@ "license": "Apache-2.0", "scripts": { "check": "npm run check:lint && npm run check:types", - "check:lint": "eslint src --ext .js,.ts", + "check:lint": "eslint src types e2e --ext .js,.ts", "check:types": "tsc --noEmit", "build": "npm run build:esm && npm run build:cjs", "build:esm": "rimraf lib/esm && tsc -m es2015 --outDir lib/esm --importHelpers && scripts/build_esm_replace_imports.sh && replace @VERSION@ $npm_package_version lib/esm/settings/defaults.js", @@ -50,7 +50,7 @@ "prepublishOnly": "npm run check && npm run test && npm run build" }, "dependencies": { - "@splitsoftware/splitio-commons": "1.16.0", + "@splitsoftware/splitio-commons": "2.4.0", "dotenv": "^9.0.1", "node-fetch": "^2.7.0", "yargs": "^17.0.1" diff --git a/src/Synchronizer.ts b/src/Synchronizer.ts index 9e9d69e..268003c 100644 --- a/src/Synchronizer.ts +++ b/src/Synchronizer.ts @@ -2,8 +2,10 @@ import { splitApiFactory } from '@splitsoftware/splitio-commons/src/services/spl import { ISplitApi } from '@splitsoftware/splitio-commons/src/services/types'; import { IStorageAsync, ITelemetryCacheAsync } from '@splitsoftware/splitio-commons/src/storages/types'; import { ISettings } from '@splitsoftware/splitio-commons/src/types'; -import { SegmentsSynchronizer } from './synchronizers/SegmentsSynchronizer'; -import { SplitsSynchronizer } from './synchronizers/SplitsSynchronizer'; +import { segmentChangesFetcherFactory } from '@splitsoftware/splitio-commons/src/sync/polling/fetchers/segmentChangesFetcher'; +import { segmentChangesUpdaterFactory } from '@splitsoftware/splitio-commons/src/sync/polling/updaters/segmentChangesUpdater'; +import { splitChangesFetcherFactory } from '@splitsoftware/splitio-commons/src/sync/polling/fetchers/splitChangesFetcher'; +import { splitChangesUpdaterFactory } from '@splitsoftware/splitio-commons/src/sync/polling/updaters/splitChangesUpdater'; import { synchronizerStorageFactory } from './storages/synchronizerStorage'; import { eventsSubmitterFactory } from './submitters/eventsSubmitter'; import { impressionsSubmitterFactory } from './submitters/impressionsSubmitter'; @@ -15,7 +17,6 @@ import { impressionCountsSubmitterFactory } from './submitters/impressionCountsS import { synchronizerSettingsValidator } from './settings'; import { validateApiKey } from '@splitsoftware/splitio-commons/src/utils/inputValidation'; import { ISynchronizerSettings } from '../types'; -import { InMemoryStorageFactory } from '@splitsoftware/splitio-commons/src/storages/inMemory/InMemoryStorage'; import { IEventsCacheAsync } from '@splitsoftware/splitio-commons/src/storages/types'; import { IImpressionsCacheAsync } from '@splitsoftware/splitio-commons/src/storages/types'; import { telemetrySubmitterFactory } from './submitters/telemetrySubmitter'; @@ -37,13 +38,13 @@ export class Synchronizer { */ private _splitApi: ISplitApi; /** - * The local reference to the SegmentsUpdater instance from `@splitio/javascript-commons`. + * The local reference to the segmentChangesUpdater instance from `@splitio/javascript-commons`. */ - private _segmentsSynchronizer!: SegmentsSynchronizer; + private _segmentChangesUpdater!: ReturnType; /** - * The local reference to the SplitUpdater instance from `@splitio/javascript-commons`. + * The local reference to the splitChangesUpdater instance from `@splitio/javascript-commons`. */ - private _splitsSynchronizer!: SplitsSynchronizer; + private _splitChangesUpdater!: ReturnType; /** * The local reference to the EventsSynchronizer class. */ @@ -90,11 +91,11 @@ export class Synchronizer { * The Split's HTTPclient, required to make the requests to the API. */ this._splitApi = splitApiFactory( - this.settings, // @ts-expect-error + this.settings, { getFetch, getOptions(settings: ISettings) { - // @ts-expect-error + // User provided options take precedence if (settings.sync.requestOptions) return settings.sync.requestOptions; }, }, @@ -138,20 +139,16 @@ export class Synchronizer { new ImpressionCountsCacheInMemory() : undefined; - this._segmentsSynchronizer = new SegmentsSynchronizer( - this._splitApi.fetchSegmentChanges, - this.settings, + this._segmentChangesUpdater = segmentChangesUpdaterFactory( + this.settings.log, + segmentChangesFetcherFactory(this._splitApi.fetchSegmentChanges), this._storage.segments, ); - this._splitsSynchronizer = new SplitsSynchronizer( - this._splitApi.fetchSplitChanges, - this.settings, - this._storage.splits, - this._storage.segments, - // @ts-ignore - InMemoryStorageFactory({ settings: this.settings }), - // @ts-ignore - InMemoryStorageFactory({ settings: this.settings }) + this._splitChangesUpdater = splitChangesUpdaterFactory( + this.settings.log, + splitChangesFetcherFactory(this._splitApi.fetchSplitChanges, this.settings, this._storage), + this._storage, + this.settings.sync.__splitFiltersValidation ); this._eventsSubmitter = eventsSubmitterFactory( this.settings.log, @@ -197,7 +194,7 @@ export class Synchronizer { private async preExecute(): Promise { const log = this.settings.log; if (!getFetch()) throw new Error('Global Fetch API is not available'); - log.info('Synchronizer: Execute'); + log.info(`Synchronizer: Execute. Version: ${this.settings.version}`); const areAPIsReady = await this._checkEndpointHealth(); if (!areAPIsReady) throw new Error('Health check of Split API endpoints failed'); @@ -266,11 +263,10 @@ export class Synchronizer { private async executeSplitsAndSegments(standalone = true) { if (standalone) await this.preExecute(); - // @TODO optimize SplitChangesUpdater to reduce storage operations ("inMemoryOperation" mode) - const isSplitsSyncSuccessful = await this._splitsSynchronizer.getSplitChanges(); + const isSplitsSyncSuccessful = await this._splitChangesUpdater(); this.settings.log.debug(`Feature flags Synchronizer task: ${isSplitsSyncSuccessful ? 'Successful' : 'Unsuccessful'}`); - const isSegmentsSyncSuccessful = await this._segmentsSynchronizer.getSegmentsChanges(); + const isSegmentsSyncSuccessful = await this._segmentChangesUpdater(); this.settings.log.debug(`Segments Synchronizer task: ${isSegmentsSyncSuccessful ? 'Successful' : 'Unsuccessful'}`); if (standalone) await this.postExecute(); diff --git a/src/settings/__tests__/index.spec.ts b/src/settings/__tests__/index.spec.ts index 12a1ac4..19d46c2 100644 --- a/src/settings/__tests__/index.spec.ts +++ b/src/settings/__tests__/index.spec.ts @@ -26,7 +26,7 @@ describe('synchronizerSettingsValidator', () => { expect(settings.scheduler.eventsPerPost).toBe(defaults.scheduler.eventsPerPost); expect(settings.scheduler.impressionsPerPost).toBe(defaults.scheduler.impressionsPerPost); expect(settings.scheduler.maxRetries).toBe(config.scheduler!.maxRetries); - expect(settings.sync.flagSpecVersion).toBe('1.1'); + expect(settings.sync.flagSpecVersion).toBe('1.3'); expect(settings.sync.requestOptions).toBe(config.sync!.requestOptions); }); diff --git a/src/settings/index.ts b/src/settings/index.ts index cd0021b..e590daa 100644 --- a/src/settings/index.ts +++ b/src/settings/index.ts @@ -7,7 +7,7 @@ import { validateLogger } from '@splitsoftware/splitio-commons/src/utils/setting import { ISynchronizerSettings } from '../../types'; import { defaults } from './defaults'; -const FLAG_SPEC_VERSIONS = ['1.0', FLAG_SPEC_VERSION]; +const FLAG_SPEC_VERSIONS = ['1.0', '1.1', '1.2', FLAG_SPEC_VERSION]; /** * Object with some default values to instantiate the application and fullfil internal @@ -53,7 +53,7 @@ export function synchronizerSettingsValidator( const { scheduler, log } = settings; // @TODO validate synchronizerMode eventually - // @TODO: validate minimum and maximum value for config params. + // @TODO validate minimum and maximum value for config params. scheduler.eventsPerPost = validatePositiveInteger(log, 'eventsPerPost', scheduler.eventsPerPost, defaults.scheduler.eventsPerPost); scheduler.impressionsPerPost = validatePositiveInteger(log, 'impressionsPerPost', scheduler.impressionsPerPost, defaults.scheduler.impressionsPerPost); scheduler.maxRetries = validatePositiveInteger(log, 'maxRetries', scheduler.maxRetries, defaults.scheduler.maxRetries); diff --git a/src/storages/synchronizerStorage.ts b/src/storages/synchronizerStorage.ts index 754f561..248e636 100644 --- a/src/storages/synchronizerStorage.ts +++ b/src/storages/synchronizerStorage.ts @@ -14,6 +14,7 @@ export function synchronizerStorageFactory(settings: ISettings, onReadyCb: IStor const { storage } = settings; // @TODO support both storage param types?: config object (JS SDK) and storage function (Browser and RN SDK) + // @ts-expect-error const storageFactory = typeof storage === 'function' ? storage : PluggableStorage(storage); // Ignoring metadata parameter since it's use by the Consumer API (like Events.track) // and the Synchronizer doesn't need to perform such actions. diff --git a/src/submitters/__tests__/impressionsMockUtils.ts b/src/submitters/__tests__/impressionsMockUtils.ts index 6b52584..7f70272 100644 --- a/src/submitters/__tests__/impressionsMockUtils.ts +++ b/src/submitters/__tests__/impressionsMockUtils.ts @@ -1,11 +1,11 @@ +import type SplitIO from '@splitsoftware/splitio-commons/types/splitio'; import { StoredImpressionWithMetadata } from '@splitsoftware/splitio-commons/src/sync/submitters/types'; -import { ImpressionDTO } from '@splitsoftware/splitio-commons/src/types'; import { _getRandomString } from './commonUtils'; /** * An Impression example. */ -const impressionFullNameNoMetadata: ImpressionDTO = { +const impressionFullNameNoMetadata: SplitIO.ImpressionDTO = { keyName: 'marcio@split.io', bucketingKey: 'impr_bucketing_2', feature: 'qc_team', @@ -13,6 +13,7 @@ const impressionFullNameNoMetadata: ImpressionDTO = { label: 'default rule', changeNumber: 828282828282, time: Date.now(), + properties: '{"key":"value"}', }; /** * An Impression with Metadata example. @@ -31,6 +32,7 @@ const impressionWithMetadata: StoredImpressionWithMetadata = { b: 'impr_bucketing_2', r: 'default rule', c: 828282828282, + properties: '{"key":"value"}', }, }; /** @@ -49,17 +51,17 @@ function getRandomizeMetadata(): StoredImpressionWithMetadata { * and then return that Impression. */ function getRandomizeImpression(): StoredImpressionWithMetadata { - const { k, t, m, b, r, c } = impressionWithMetadata.i; + const { k, t, m, b, r, c, properties } = impressionWithMetadata.i; return Object.assign( {}, impressionWithMetadata, - { i: { k, t, m, b, r, c, f: _getRandomString(12) } } + { i: { k, t, m, b, r, c, properties, f: _getRandomString(12) } } ); } /** * Function to return an Impression with its full Key's name and no metadata. */ -export function getImpressionSampleWithNoMetadata(): ImpressionDTO { +export function getImpressionSampleWithNoMetadata(): SplitIO.ImpressionDTO { return impressionFullNameNoMetadata; } /** diff --git a/src/submitters/__tests__/impressionsSubmitter.spec.ts b/src/submitters/__tests__/impressionsSubmitter.spec.ts index 0b182eb..f47d0a5 100644 --- a/src/submitters/__tests__/impressionsSubmitter.spec.ts +++ b/src/submitters/__tests__/impressionsSubmitter.spec.ts @@ -242,6 +242,7 @@ describe('Impressions Submitter for Lightweight Synchronizer', () => { c: expect.any(Number), m: expect.any(Number), pt: expect.any(Number), + properties: expect.any(String), }) ); // @ts-ignore diff --git a/src/submitters/impressionCountsSubmitter.ts b/src/submitters/impressionCountsSubmitter.ts index 11df007..4b20e04 100644 --- a/src/submitters/impressionCountsSubmitter.ts +++ b/src/submitters/impressionCountsSubmitter.ts @@ -6,11 +6,10 @@ import { ILogger } from '@splitsoftware/splitio-commons/src/logger/types'; import { ImpressionCountsCachePluggable } from '@splitsoftware/splitio-commons/src/storages/pluggable/ImpressionCountsCachePluggable'; import { submitterFactory } from './submitter'; import { ImpressionCountsPayload } from '@splitsoftware/splitio-commons/src/sync/submitters/types'; -import { _Map } from '@splitsoftware/splitio-commons/src/utils/lang/maps'; // Merge impressions counts objects function merge(counts1: ImpressionCountsPayload, counts2: ImpressionCountsPayload): ImpressionCountsPayload { - const merged = new _Map(counts1.pf.map((count) => [count.f + count.m, count])); + const merged = new Map(counts1.pf.map((count) => [count.f + count.m, count])); counts2.pf.forEach((count) => { const key = count.f + count.m; if (merged.has(key)) merged.get(key)!.rc += count.rc; diff --git a/src/submitters/impressionsSubmitter.ts b/src/submitters/impressionsSubmitter.ts index 1c0c428..08ad638 100644 --- a/src/submitters/impressionsSubmitter.ts +++ b/src/submitters/impressionsSubmitter.ts @@ -1,19 +1,18 @@ +import type SplitIO from '@splitsoftware/splitio-commons/types/splitio'; import { IPostTestImpressionsBulk } from '@splitsoftware/splitio-commons/src/services/types'; import { IImpressionCountsCacheBase, IImpressionsCacheAsync } from '@splitsoftware/splitio-commons/src/storages/types'; import { StoredImpressionWithMetadata } from '@splitsoftware/splitio-commons/src/sync/submitters/types'; import { truncateTimeFrame } from '@splitsoftware/splitio-commons/src/utils/time'; import { ImpressionObserver } from '@splitsoftware/splitio-commons/src/trackers/impressionObserver/ImpressionObserver'; import { groupBy, metadataToHeaders } from './utils'; -import { SplitIO } from '@splitsoftware/splitio-commons/src/types'; import { ILogger } from '@splitsoftware/splitio-commons/src/logger/types'; import { IMetadata } from '@splitsoftware/splitio-commons/src/dtos/types'; -import { ImpressionDTO } from '@splitsoftware/splitio-commons/src/types'; import { ImpressionsPayload } from '@splitsoftware/splitio-commons/src/sync/submitters/types'; import { submitterWithMetadataFactory } from './submitter'; export type ImpressionsDTOWithMetadata = { metadata: IMetadata; - impression: ImpressionDTO; + impression: SplitIO.ImpressionDTO; } /** @@ -49,7 +48,8 @@ export const impressionWithMetadataToImpressionDTO = (storedImpression: StoredIm changeNumber: i.c, time: i.m, pt: i.pt, - } as ImpressionDTO, + properties: i.properties, + } as SplitIO.ImpressionDTO, }; }; @@ -131,6 +131,7 @@ export function impressionsSubmitterFactory( r: entry.label, // Rule label c: entry.changeNumber, // ChangeNumber pt: entry.pt, // Previous time + properties: entry.properties, }; }), }); diff --git a/src/submitters/telemetrySubmitter.ts b/src/submitters/telemetrySubmitter.ts index 7f236a4..3ae0e11 100644 --- a/src/submitters/telemetrySubmitter.ts +++ b/src/submitters/telemetrySubmitter.ts @@ -3,7 +3,6 @@ import { TelemetryUsageStats } from '@splitsoftware/splitio-commons/src/sync/sub import { ITelemetryCacheAsync } from '@splitsoftware/splitio-commons/src/storages/types'; import { metadataToHeaders } from './utils'; import { ISplitApi } from '@splitsoftware/splitio-commons/src/services/types'; -import { _Map, IMap } from '@splitsoftware/splitio-commons/src/utils/lang/maps'; /** @@ -22,11 +21,11 @@ export function telemetrySubmitterFactory( telemetryCache: ITelemetryCacheAsync, ): () => Promise { - async function buildUsageStats(): Promise> { + async function buildUsageStats(): Promise> { const latencies = await telemetryCache.popLatencies(); const exceptions = await telemetryCache.popExceptions(); - const result = new _Map(); + const result = new Map(); latencies.forEach((latency, metadata) => { result.set(metadata, { mL: latency }); diff --git a/src/submitters/uniqueKeysSubmitter.ts b/src/submitters/uniqueKeysSubmitter.ts index 216f263..30cd718 100644 --- a/src/submitters/uniqueKeysSubmitter.ts +++ b/src/submitters/uniqueKeysSubmitter.ts @@ -3,7 +3,6 @@ import { ILogger } from '@splitsoftware/splitio-commons/src/logger/types'; import { IPostUniqueKeysBulkSs } from '@splitsoftware/splitio-commons/src/services/types'; import { fromUniqueKeysCollector } from '@splitsoftware/splitio-commons/src/storages/inMemory/UniqueKeysCacheInMemory'; import { UniqueKeysCachePluggable } from '@splitsoftware/splitio-commons/src/storages/pluggable/UniqueKeysCachePluggable'; -import { ISet, _Set } from '@splitsoftware/splitio-commons/src/utils/lang/sets'; import { submitterFactory } from './submitter'; export function uniqueKeysSubmitterFactory( @@ -19,12 +18,12 @@ export function uniqueKeysSubmitterFactory( if (!uniqueKeyItems.length) return undefined; - const mergedUniqueKeys = uniqueKeyItems.reduce<{ [featureName: string]: ISet }>((accUniqueKeys, uniqueKeyItem) => { + const mergedUniqueKeys = uniqueKeyItems.reduce<{ [featureName: string]: Set }>((accUniqueKeys, uniqueKeyItem) => { const featureNameKeys = accUniqueKeys[uniqueKeyItem.f]; if (featureNameKeys) { uniqueKeyItem.ks.forEach(key => featureNameKeys.add(key)); } else { - accUniqueKeys[uniqueKeyItem.f] = new _Set(uniqueKeyItem.ks); + accUniqueKeys[uniqueKeyItem.f] = new Set(uniqueKeyItem.ks); } return accUniqueKeys; }, {}); diff --git a/src/synchronizers/SplitsSynchronizer.ts b/src/synchronizers/SplitsSynchronizer.ts index b5f1297..798f1e5 100644 --- a/src/synchronizers/SplitsSynchronizer.ts +++ b/src/synchronizers/SplitsSynchronizer.ts @@ -1,9 +1,10 @@ import { IFetchSplitChanges } from '@splitsoftware/splitio-commons/src/services/types'; +import { InMemoryStorageFactory } from '@splitsoftware/splitio-commons/src/storages/inMemory/InMemoryStorage'; import { splitChangesFetcherFactory } from '@splitsoftware/splitio-commons/src/sync/polling/fetchers/splitChangesFetcher'; import { splitChangesUpdaterFactory } from '@splitsoftware/splitio-commons/src/sync/polling/updaters/splitChangesUpdater'; import { ISettings } from '@splitsoftware/splitio-commons/src/types'; import { ISplit } from '@splitsoftware/splitio-commons/src/dtos/types'; -import { ISegmentsCacheAsync, ISplitsCacheAsync, IStorageSync } from '@splitsoftware/splitio-commons/src/storages/types'; +import { IStorageAsync, IStorageSync } from '@splitsoftware/splitio-commons/src/storages/types'; type ISplitChangesUpdater = (noCache?: boolean) => Promise; @@ -12,13 +13,9 @@ type ISplitChangesUpdater = (noCache?: boolean) => Promise; */ export class SplitsSynchronizer { /** - * The local reference to the Synchronizer's Split Storage. + * The local reference to the Synchronizer's Storage. */ - private _segmentsStorage; - /** - * The local reference to the Synchronizer's Segments Storage. - */ - private _splitsStorage; + private _storage; /** * The local reference to the Fetch implementation required to perform requests. */ @@ -48,26 +45,19 @@ export class SplitsSynchronizer { /** * @param splitFetcher - The SplitChanges fetcher from Split API. * @param settings - The Synchronizer's settings. - * @param splitsStorage - The reference to the current Split Storage. - * @param segmentsStorage - The reference to the current Cache Storage. - * @param inMemoryStorage - The reference to the current Cache Storage. - * @param inMemoryStorageSnapshot - The reference to the current Cache Storage. + * @param storage - The reference to the current Storage. */ constructor( splitFetcher: IFetchSplitChanges, settings: ISettings, - splitsStorage: ISplitsCacheAsync, - segmentsStorage: ISegmentsCacheAsync, - inMemoryStorage: IStorageSync, - inMemoryStorageSnapshot: IStorageSync, + storage: IStorageAsync, ) { - this._splitsStorage = splitsStorage; - this._segmentsStorage = segmentsStorage; + this._storage = storage; this._settings = settings; - this._fetcher = splitChangesFetcherFactory(splitFetcher); - this._splitUpdater = undefined; - this._inMemoryStorage = inMemoryStorage; - this._inMemoryStorageSnapshot = inMemoryStorageSnapshot; + this._fetcher = splitChangesFetcherFactory(splitFetcher, settings, storage); + this._splitUpdater = undefined; // @ts-ignore + this._inMemoryStorage = InMemoryStorageFactory({ settings }); // @ts-ignore + this._inMemoryStorageSnapshot = InMemoryStorageFactory({ settings }); } /** @@ -79,8 +69,7 @@ export class SplitsSynchronizer { this._splitUpdater = splitChangesUpdaterFactory( this._settings.log, this._fetcher, - this._splitsStorage, - this._segmentsStorage, + this._storage, this._settings.sync.__splitFiltersValidation, ); return this._splitUpdater(); @@ -91,28 +80,19 @@ export class SplitsSynchronizer { * @param splitCacheInMemory - Reference to the local InMemoryCache. */ async getDataFromStorage() { - const _splitsList: [string, ISplit][] = []; try { - const splits = await this._splitsStorage.getAll(); - - splits.forEach((split) => { - _splitsList.push([split.name, split]); - }); + const splits = await this._storage.splits.getAll(); - this._inMemoryStorage.splits.addSplits(_splitsList); + const changeNumber = await this._storage.splits.getChangeNumber(); + this._inMemoryStorage.splits.update(splits, [], changeNumber); - this._inMemoryStorageSnapshot.splits.addSplits(_splitsList); + this._inMemoryStorageSnapshot.splits.update(splits, [], changeNumber); - const registeredSegments = await this._segmentsStorage.getRegisteredSegments(); + const registeredSegments = await this._storage.segments.getRegisteredSegments(); if (registeredSegments.length > 0) { this._inMemoryStorage.segments.registerSegments(registeredSegments); this._inMemoryStorageSnapshot.segments.registerSegments(registeredSegments); } - const changeNumber = await this._splitsStorage.getChangeNumber(); - - this._inMemoryStorage.splits.setChangeNumber(changeNumber); - this._inMemoryStorageSnapshot.splits.setChangeNumber(changeNumber); - } catch (error) { this._settings.log.error( `Feature flags InMemory synchronization: Error when retrieving data from external Storage. Error: ${error}` @@ -137,7 +117,7 @@ export class SplitsSynchronizer { if (splits.length > 0) { - const splitsToStore: [string, ISplit][] = []; + const splitsToStore: ISplit[] = []; for (let i = 0; i < splits.length; i++) { const split = splits[i]; const { name, changeNumber } = split; @@ -146,26 +126,25 @@ export class SplitsSynchronizer { if (split) { // If the feature flag doesn't exists. if (!oldSplitDefinition) { - splitsToStore.push([name, split]); + splitsToStore.push(split); continue; } // If the feature flag exists and needs to be updated. if (oldSplitDefinition.changeNumber !== changeNumber) { - splitsToStore.push([name, split]); + splitsToStore.push(split); continue; } } } - await this._splitsStorage.addSplits(splitsToStore); + await this._storage.splits.update(splitsToStore, [], changeNumber); } - await this._splitsStorage.setChangeNumber(changeNumber); const registeredSegments = this._inMemoryStorage.segments.getRegisteredSegments(); - // @todo: Update segment definitions and change number + // @todo: Update rule-based segments, segment definitions and change number if (registeredSegments.length > 0) - await this._segmentsStorage.registerSegments(registeredSegments); + await this._storage.segments.registerSegments(registeredSegments); } catch (error) { this._settings.log.error( `Feature flags InMemory synchronization: Error when storing data to external Storage. Error: ${error}` @@ -183,8 +162,7 @@ export class SplitsSynchronizer { this._splitUpdater = splitChangesUpdaterFactory( this._settings.log, this._fetcher, - this._inMemoryStorage.splits, - this._inMemoryStorage.segments, + this._inMemoryStorage, this._settings.sync.__splitFiltersValidation, ); try { @@ -223,7 +201,8 @@ export class SplitsSynchronizer { } }); - await this._splitsStorage.removeSplits(splitKeysToRemove); + // @ts-expect-error + await this._storage.splits.update([], splitKeysToRemove.map((k) => ({ name: k })), this._storage.splits.getChangeNumber()); return deletedAmount; } diff --git a/src/synchronizers/__tests__/SplitsSynchronizer.spec.ts b/src/synchronizers/__tests__/SplitsSynchronizer.spec.ts index d3b8e93..2422c8f 100644 --- a/src/synchronizers/__tests__/SplitsSynchronizer.spec.ts +++ b/src/synchronizers/__tests__/SplitsSynchronizer.spec.ts @@ -1,4 +1,5 @@ // @ts-nocheck +import { ISplit } from '@splitsoftware/splitio-commons/src/dtos/types'; import { SplitsSynchronizer } from '../SplitsSynchronizer'; describe('Splits Synchronizer', () => { @@ -7,59 +8,29 @@ describe('Splits Synchronizer', () => { status: 200, json: Promise.resolve, })); - const _settingsMock = { log: { error: () => {} } }; - - const _splitsInMemoryCacheFactory = () => { - const _splitsCache = []; - let _changeNumber; - - return { - addSplits: jest.fn((splitList: [string, string][]) => { - splitList.forEach((keyValueTuple) => { - _splitsCache.push(keyValueTuple[1]); - }); - }), - getSplitNames: jest.fn(() => _splitsCache.map(s => JSON.parse(s).name)), - getSplit: jest.fn((name) => { - const res = _splitsCache.find(i => i[0] === name); - return res ? res[1] : null; - }), - setChangeNumber: jest.fn((number) => _changeNumber = number), - getChangeNumber: jest.fn(() => _changeNumber), - getAll: jest.fn(() => _splitsCache), - removeSplits: jest.fn((names) => { - for (let i = 0; i < names.length; i++) { - const index = _splitsCache.indexOf(names[i]); - if (index) _splitsCache.splice(index, 1); - } - }), - }; - }; - - const _segmentsInMemoryCacheFactory = () => { - let _segmentsCache = []; - - return { - registerSegments: jest.fn((segments) => segments.map(s => _segmentsCache.push(s))), - getRegisteredSegments: jest.fn(() => _segmentsCache), - }; + const _settingsMock = { + log: { error: () => { } }, + scheduler: { impressionsQueueSize: 100 }, + sync: { __splitFiltersValidation: false }, + core: { key: undefined }, }; const _splitStorageMock = (() => { const _splitsStored = [ - JSON.stringify({ name: 'pepito1', changeNumber: 0 }), - JSON.stringify({ name: 'pepito2', changeNumber: 0 }), + { name: 'pepito1', changeNumber: 0 }, + { name: 'pepito2', changeNumber: 0 }, ]; let _changeNumber = 1; return { - addSplits: jest.fn((tupleList: [string, string][]) => tupleList.map(tuple => _splitsStored.push(tuple[1]))), - getSplitNames: jest.fn(() => Promise.resolve(_splitsStored.map(s => JSON.parse(s).name))), + update: jest.fn((toAdd: ISplit[], toRemove, n) => { + toAdd.map(s => _splitsStored.push(s)); + _changeNumber = n; + }), + getSplitNames: jest.fn(() => Promise.resolve(_splitsStored.map(s => s.name))), getAll: jest.fn(() => _splitsStored), - getSplit: jest.fn((name) => Promise.resolve(_splitsStored[name])), - setChangeNumber: jest.fn((n) => _changeNumber = n), + getSplit: jest.fn((name) => Promise.resolve(_splitsStored.find(s => s.name === name))), getChangeNumber: jest.fn(() => _changeNumber), - removeSplits: jest.fn(() => Promise.resolve), }; })(); @@ -68,29 +39,22 @@ describe('Splits Synchronizer', () => { return { registerSegments: jest.fn((segments) => { segments.map(s => _registeredSegments.push(s)); - _registeredSegments = Array.from(new Set(_registeredSegments)); + _registeredSegments = Array.from(new Set(_registeredSegments)); }), getRegisteredSegments: jest.fn(() => _registeredSegments), }; })(); - const _inMemoryStorageMockFactory = () => { - return { - splits: _splitsInMemoryCacheFactory(), - segments: _segmentsInMemoryCacheFactory(), - }; - }; - let _splitsSynchronizer; const createNewSynchronizer = () => { _splitsSynchronizer = new SplitsSynchronizer( _splitFetcherMock, _settingsMock, - _splitStorageMock, - _segmentsStorageMock, - _inMemoryStorageMockFactory(), - _inMemoryStorageMockFactory() + { + splits: _splitStorageMock, + segments: _segmentsStorageMock, + } ); }; @@ -104,7 +68,7 @@ describe('Splits Synchronizer', () => { test('retrieves [SPLITS] stored from Storage into InMemory cache', () => { const splitsNames = _splitsSynchronizer._inMemoryStorage.splits .getAll() - .map((split) => JSON.parse(split).name); + .map((split) => split.name); expect(splitsNames).toEqual(['pepito1', 'pepito2']); expect(_splitsSynchronizer._inMemoryStorage.splits.getChangeNumber()).toBe(1); @@ -118,18 +82,10 @@ describe('Splits Synchronizer', () => { describe('Process differences between data previous to Sync and new data.', () => { beforeAll(() => { createNewSynchronizer(); - _splitsSynchronizer._inMemoryStorageSnapshot.splits.addSplits([ - ['pepito1', '{"name":"pepito1","changeNumber":4}'], - ]); - _splitsSynchronizer._inMemoryStorageSnapshot.splits.addSplits([ - ['pepito2', '{"name":"pepito2","changeNumber":4}'], - ]); - _splitsSynchronizer._inMemoryStorage.splits.addSplits([ - ['pepito2', '{"name":"pepito2","changeNumber":5}'], - ]); - _splitsSynchronizer._inMemoryStorage.splits.addSplits([ - ['pepito3', '{"name":"pepito3","changeNumber":3}'], - ]); + _splitsSynchronizer._inMemoryStorageSnapshot.splits.update([{ name: 'pepito1', changeNumber: 4 }], [], 4); + _splitsSynchronizer._inMemoryStorageSnapshot.splits.update([{ name: 'pepito2', changeNumber: 4 }], [], 4); + _splitsSynchronizer._inMemoryStorage.splits.update([{ name: 'pepito2', changeNumber: 5 }], [], 5); + _splitsSynchronizer._inMemoryStorage.splits.update([{ name: 'pepito3', changeNumber: 3 }], [], 3); }); test('compares the updated InMemory cache with previous snapshot and removes [1] unused split', async () => { @@ -143,11 +99,8 @@ describe('Splits Synchronizer', () => { beforeAll(async () => { createNewSynchronizer(); // adding some data simulating synchronization execution') - _splitsSynchronizer._inMemoryStorage.splits.addSplits([ - ['pepito3', JSON.stringify({ name: 'pepito3', changeNumber: 6 })], - ]); + _splitsSynchronizer._inMemoryStorage.splits.update([{ name: 'pepito3', changeNumber: 6 }], [], 12); - _splitsSynchronizer._inMemoryStorage.splits.setChangeNumber(12); _splitsSynchronizer._inMemoryStorage.segments.registerSegments(['anotherSegment']); await _splitsSynchronizer.putDataToStorage(); diff --git a/types/index.d.ts b/types/index.d.ts index 6a5275e..cc349e5 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -1,6 +1,7 @@ // Type definitions for Split JavaScript Sync Tools // Project: https://www.split.io/ // Definitions by: Emiliano Sanchez + import { RequestOptions } from 'http'; export = JsSyncTools; @@ -14,14 +15,15 @@ declare module JsSyncTools { export class Synchronizer { /** * Creates a new Synchronizer instance - * @param config The synchronizer config object + * + * @param config - The synchronizer config object */ constructor(config: ISynchronizerSettings); /** * Execute synchronization - * @param cb Optional error-first callback to be invoked when the synchronization ends. The callback will be invoked with an error as first argument if the synchronization fails. - * @return {Promise} A promise that resolves when the operation ends, - * with a boolean indicating if operation succeeded or not. The promise never rejects. + * + * @param cb - Optional error-first callback to be invoked when the synchronization ends. The callback will be invoked with an error as first argument if the synchronization fails. + * @returns A promise that resolves when the operation ends, with a boolean indicating if operation succeeded or not. The promise never rejects. */ execute(cb?: (err?: Error) => void): Promise; // @TODO expose settings eventually @@ -30,8 +32,6 @@ declare module JsSyncTools { /** * Log levels. - * - * @typedef {string} LogLevel */ type LogLevel = 'DEBUG' | 'INFO' | 'WARN' | 'ERROR' | 'NONE'; /** @@ -41,30 +41,25 @@ declare module JsSyncTools { /** * String property to override the base URL where the synchronizer will get feature flagging related data like a feature flag rollout plan or segments information. * - * @property {string} sdk - * @default 'https://sdk.split.io/api' + * @defaultValue `'https://sdk.split.io/api'` */ sdk?: string /** * String property to override the base URL where the synchronizer will post impressions and events. * - * @property {string} events - * @default 'https://events.split.io/api' + * @defaultValue `'https://events.split.io/api'` */ events?: string, /** * String property to override the base URL where the synchronizer will post telemetry data. * - * @property {string} telemetry - * @default 'https://telemetry.split.io/api' + * @defaultValue `'https://telemetry.split.io/api'` */ telemetry?: string }; /** * SplitFilter type. - * - * @typedef {string} SplitFilterType */ type SplitFilterType = 'byName' | 'bySet'; /** @@ -73,132 +68,137 @@ declare module JsSyncTools { interface SplitFilter { /** * Type of the filter. - * - * @property {SplitFilterType} type */ type: SplitFilterType, /** * List of values: feature flag names for 'byName' filter type, and feature flag name prefixes for 'byPrefix' type. - * - * @property {string[]} values */ values: string[], } /** * ImpressionsMode type. - * - * @typedef {string} ImpressionsMode */ type ImpressionsMode = 'OPTIMIZED' | 'DEBUG'; /** * Settings interface for Synchronizer instances. * - * @interface ISynchronizerSettings * @see {@link https://help.split.io/hc/en-us/articles/4421513571469-Split-JavaScript-synchronizer-tools#configuration} */ interface ISynchronizerSettings { /** * Core settings. - * - * @property {Object} core */ core: { /** - * Your SDK key. More information: @see {@link https://help.split.io/hc/en-us/articles/360019916211-API-keys} + * Your SDK key. * - * @property {string} authorizationKey + * @see {@link https://help.split.io/hc/en-us/articles/360019916211-API-keys} */ authorizationKey: string } /** * Defines which kind of storage we should instantiate. - * - * @property {Object} storage */ storage: { /** - * Storage type. The only possible value is "PLUGGABLE", which is the default. - * @property {'PLUGGABLE'} type + * Storage type. The only possible value is `'PLUGGABLE'`, which is the default. */ type?: 'PLUGGABLE', /** * A valid storage instance. - * - * @property {Object} wrapper */ wrapper: Object /** * Optional prefix added to the storage keys to prevent any kind of data collision between SDK versions. * - * @property {string} prefix - * @default 'SPLITIO' + * @defaultValue `'SPLITIO'` */ prefix?: string } /** * List of URLs that the Synchronizer will use as base for it's synchronization functionalities. * Do not change these settings unless you're working an advanced use case, like connecting to a proxy. - * - * @property {Object} urls */ urls?: UrlSettings /** * Boolean value to indicate whether the logger should be enabled or disabled by default, or a log level string. * * Examples: - * ```typescript + * ``` * config.debug = true * config.debug = 'WARN' * ``` - * @property {boolean | LogLevel | ILogger} debug - * @default false + * + * @defaultValue `false` */ debug?: boolean | LogLevel /** * Synchronization settings. - * @property {Object} sync */ sync?: { /** * List of feature flag filters. These filters are used to fetch a subset of the feature flag definitions in your environment. * * Example: - * `splitFilter: [ - * { type: 'byName', values: ['my_feature_flag_1', 'my_feature_flag_2'] }, // will fetch feature flags named 'my_feature_flag_1' and 'my_feature_flag_2' - * ]` - * @property {SplitFilter[]} splitFilters + * ``` + * splitFilter: [ + * { type: 'byName', values: ['my_feature_flag_1', 'my_feature_flag_2'] }, // will fetch feature flags named 'my_feature_flag_1' and 'my_feature_flag_2' + * ] + * ``` */ splitFilters?: SplitFilter[] /** * Feature Flag Spec version. Option to determine which version of the feature flag definitions are fetched and stored. - * Possible values are '1.0' and '1.1'. + * Possible values are `'1.0'`, `'1.1'`, `'1.2'`, and `'1.3'`. * - * @default '1.1' + * @defaultValue `'1.3'` */ - flagSpecVersion?: '1.0' | '1.1' + flagSpecVersion?: '1.0' | '1.1' | '1.2' | '1.3' /** * Impressions Collection Mode. Option to determine how impressions are going to be sent to Split Servers. * - * Possible values are 'DEBUG' and 'OPTIMIZED'. + * Possible values are `'DEBUG'` and `'OPTIMIZED'`. * - DEBUG: will send all the impressions generated (recommended only for debugging purposes). * - OPTIMIZED: will send unique impressions to Split Servers avoiding a considerable amount of traffic that duplicated impressions could generate. * - * @property {String} impressionsMode - * @default 'OPTIMIZED' + * @defaultValue `'OPTIMIZED'` */ impressionsMode?: ImpressionsMode /** * Custom options object for HTTP(S) requests in Node.js. * If provided, this object is merged with the options object passed for Node-Fetch calls. + * * @see {@link https://www.npmjs.com/package/node-fetch#options} */ requestOptions?: { + /** + * Custom function called before each request, allowing you to add or update headers in Synchronizer HTTP requests. + * Some headers, such as `SplitSDKVersion`, are required by the Synchronizer and cannot be overridden. + * To pass multiple headers with the same name, combine their values into a single line, separated by commas. Example: `{ 'Authorization': 'value1, value2' }` + * Or provide keys with different cases since headers are case-insensitive. Example: `{ 'authorization': 'value1', 'Authorization': 'value2' }` + * + * @defaultValue `undefined` + * + * @param context - The context for the request, which contains the `headers` property object representing the current headers in the request. + * @returns An object representing a set of headers to be merged with the current headers. + * + * @example + * ``` + * const getHeaderOverrides = (context) => { + * return { + * 'Authorization': context.headers['Authorization'] + ', other-value', + * 'custom-header': 'custom-value' + * }; + * }; + * ``` + */ + getHeaderOverrides?: (context: { headers: Record }) => Record; /** * Custom Node.js HTTP(S) Agent used for HTTP(S) requests. * * You can use it, for example, for certificate pinning or setting a network proxy: * - * ```javascript + * ``` * const { HttpsProxyAgent } = require('https-proxy-agent'); * * const proxyAgent = new HttpsProxyAgent(process.env.HTTPS_PROXY || 'http://10.10.1.10:1080'); @@ -215,33 +215,31 @@ declare module JsSyncTools { * * @see {@link https://nodejs.org/api/https.html#class-httpsagent} * - * @property {http.Agent | https.Agent} agent - * @default undefined + * @defaultValue `undefined` */ agent?: RequestOptions['agent'] }, } /** * Scheduler settings. - * @property {Object} scheduler */ scheduler?: { /** * Maximum number of impressions to send per POST request. - * @property {number} impressionsPerPost - * @default 1000 + * + * @defaultValue `1000` */ impressionsPerPost?: number /** * Maximum number of events to send per POST request. - * @property {number} eventsPerPost - * @default 1000 + * + * @defaultValue `1000` */ eventsPerPost?: number /** * Maximum number of retry attempts for posting impressions and events. - * @property {number} maxRetries - * @default 3 + * + * @defaultValue `3` */ maxRetries?: number }