From fe0f353feb6e5f8e01aca65c44b008e65eed9ca7 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Fri, 30 May 2025 10:54:40 -0300 Subject: [PATCH 1/7] Add usesSegmentsSync utility function to reuse code --- src/storages/AbstractSplitsCacheSync.ts | 6 +++++- src/sync/polling/pollingManagerCS.ts | 9 +++++---- src/sync/polling/updaters/mySegmentsUpdater.ts | 5 +++-- src/sync/syncManagerOnline.ts | 5 +++-- 4 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/storages/AbstractSplitsCacheSync.ts b/src/storages/AbstractSplitsCacheSync.ts index 761c5cb9..2a4b9b78 100644 --- a/src/storages/AbstractSplitsCacheSync.ts +++ b/src/storages/AbstractSplitsCacheSync.ts @@ -1,4 +1,4 @@ -import { ISplitsCacheSync } from './types'; +import { ISplitsCacheSync, IStorageSync } from './types'; import { IRBSegment, ISplit } from '../dtos/types'; import { objectAssign } from '../utils/lang/objectAssign'; import { IN_SEGMENT, IN_LARGE_SEGMENT } from '../utils/constants'; @@ -88,3 +88,7 @@ export function usesSegments(ruleEntity: ISplit | IRBSegment) { return false; } + +export function usesSegmentsSync(storage: Pick) { + return storage.splits.usesSegments() || storage.rbSegments.usesSegments(); +} diff --git a/src/sync/polling/pollingManagerCS.ts b/src/sync/polling/pollingManagerCS.ts index 6a5ba679..5e197e62 100644 --- a/src/sync/polling/pollingManagerCS.ts +++ b/src/sync/polling/pollingManagerCS.ts @@ -8,6 +8,7 @@ import { getMatching } from '../../utils/key'; import { SDK_SPLITS_ARRIVED, SDK_SEGMENTS_ARRIVED } from '../../readiness/constants'; import { POLLING_SMART_PAUSING, POLLING_START, POLLING_STOP } from '../../logger/constants'; import { ISdkFactoryContextSync } from '../../sdkFactory/types'; +import { usesSegmentsSync } from '../../storages/AbstractSplitsCacheSync'; /** * Expose start / stop mechanism for polling data from services. @@ -43,7 +44,7 @@ export function pollingManagerCSFactory( // smart pausing readiness.splits.on(SDK_SPLITS_ARRIVED, () => { if (!splitsSyncTask.isRunning()) return; // noop if not doing polling - const usingSegments = storage.splits.usesSegments() || storage.rbSegments.usesSegments(); + const usingSegments = usesSegmentsSync(storage); if (usingSegments !== mySegmentsSyncTask.isRunning()) { log.info(POLLING_SMART_PAUSING, [usingSegments ? 'ON' : 'OFF']); if (usingSegments) { @@ -59,9 +60,9 @@ export function pollingManagerCSFactory( // smart ready function smartReady() { - if (!readiness.isReady() && !storage.splits.usesSegments() && !storage.rbSegments.usesSegments()) readiness.segments.emit(SDK_SEGMENTS_ARRIVED); + if (!readiness.isReady() && !usesSegmentsSync(storage)) readiness.segments.emit(SDK_SEGMENTS_ARRIVED); } - if (!storage.splits.usesSegments() && !storage.rbSegments.usesSegments()) setTimeout(smartReady, 0); + if (!usesSegmentsSync(storage)) setTimeout(smartReady, 0); else readiness.splits.once(SDK_SPLITS_ARRIVED, smartReady); mySegmentsSyncTasks[matchingKey] = mySegmentsSyncTask; @@ -77,7 +78,7 @@ export function pollingManagerCSFactory( log.info(POLLING_START); splitsSyncTask.start(); - if (storage.splits.usesSegments() || storage.rbSegments.usesSegments()) startMySegmentsSyncTasks(); + if (usesSegmentsSync(storage)) startMySegmentsSyncTasks(); }, // Stop periodic fetching (polling) diff --git a/src/sync/polling/updaters/mySegmentsUpdater.ts b/src/sync/polling/updaters/mySegmentsUpdater.ts index 501e3b7a..5de512fa 100644 --- a/src/sync/polling/updaters/mySegmentsUpdater.ts +++ b/src/sync/polling/updaters/mySegmentsUpdater.ts @@ -8,6 +8,7 @@ import { SYNC_MYSEGMENTS_FETCH_RETRY } from '../../../logger/constants'; import { MySegmentsData } from '../types'; import { IMembershipsResponse } from '../../../dtos/types'; import { MEMBERSHIPS_LS_UPDATE } from '../../streaming/constants'; +import { usesSegmentsSync } from '../../../storages/AbstractSplitsCacheSync'; type IMySegmentsUpdater = (segmentsData?: MySegmentsData, noCache?: boolean, till?: number) => Promise @@ -27,7 +28,7 @@ export function mySegmentsUpdaterFactory( matchingKey: string ): IMySegmentsUpdater { - const { splits, rbSegments, segments, largeSegments } = storage; + const { segments, largeSegments } = storage; let readyOnAlreadyExistentState = true; let startingUp = true; @@ -51,7 +52,7 @@ export function mySegmentsUpdaterFactory( } // Notify update if required - if ((splits.usesSegments() || rbSegments.usesSegments()) && (shouldNotifyUpdate || readyOnAlreadyExistentState)) { + if (usesSegmentsSync(storage) && (shouldNotifyUpdate || readyOnAlreadyExistentState)) { readyOnAlreadyExistentState = false; segmentsEventEmitter.emit(SDK_SEGMENTS_ARRIVED); } diff --git a/src/sync/syncManagerOnline.ts b/src/sync/syncManagerOnline.ts index 21bf81e7..aac6f7e4 100644 --- a/src/sync/syncManagerOnline.ts +++ b/src/sync/syncManagerOnline.ts @@ -10,6 +10,7 @@ import { isConsentGranted } from '../consent'; import { POLLING, STREAMING, SYNC_MODE_UPDATE } from '../utils/constants'; import { ISdkFactoryContextSync } from '../sdkFactory/types'; import { SDK_SPLITS_CACHE_LOADED } from '../readiness/constants'; +import { usesSegmentsSync } from '../storages/AbstractSplitsCacheSync'; /** * Online SyncManager factory. @@ -155,14 +156,14 @@ export function syncManagerOnlineFactory( if (pushManager) { if (pollingManager.isRunning()) { // if doing polling, we must start the periodic fetch of data - if (storage.splits.usesSegments() || storage.rbSegments.usesSegments()) mySegmentsSyncTask.start(); + if (usesSegmentsSync(storage)) mySegmentsSyncTask.start(); } else { // if not polling, we must execute the sync task for the initial fetch // of segments since `syncAll` was already executed when starting the main client mySegmentsSyncTask.execute(); } } else { - if (storage.splits.usesSegments() || storage.rbSegments.usesSegments()) mySegmentsSyncTask.start(); + if (usesSegmentsSync(storage)) mySegmentsSyncTask.start(); } } else { if (!readinessManager.isReady()) mySegmentsSyncTask.execute(); From 334288291b1525bec2219fa5ccc70dad75186850 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Sun, 1 Jun 2025 19:01:45 -0300 Subject: [PATCH 2/7] Test assertion --- .../inLocalStorage/__tests__/SplitsCacheInLocal.spec.ts | 3 +++ src/storages/inMemory/__tests__/SplitsCacheInMemory.spec.ts | 3 +++ 2 files changed, 6 insertions(+) diff --git a/src/storages/inLocalStorage/__tests__/SplitsCacheInLocal.spec.ts b/src/storages/inLocalStorage/__tests__/SplitsCacheInLocal.spec.ts index 913d6a3b..13ab1b32 100644 --- a/src/storages/inLocalStorage/__tests__/SplitsCacheInLocal.spec.ts +++ b/src/storages/inLocalStorage/__tests__/SplitsCacheInLocal.spec.ts @@ -173,6 +173,9 @@ test('SPLITS CACHE / LocalStorage / flag set cache tests', () => { ], [], -1); cache.addSplit(featureFlagWithEmptyFS); + // Adding an existing FF should not affect the cache + cache.update([featureFlagTwo], [], -1); + expect(cache.getNamesByFlagSets(['o'])).toEqual([new Set(['ff_one', 'ff_two'])]); expect(cache.getNamesByFlagSets(['n'])).toEqual([new Set(['ff_one'])]); expect(cache.getNamesByFlagSets(['e'])).toEqual([new Set(['ff_one', 'ff_three'])]); diff --git a/src/storages/inMemory/__tests__/SplitsCacheInMemory.spec.ts b/src/storages/inMemory/__tests__/SplitsCacheInMemory.spec.ts index 56ca1300..2ed4478b 100644 --- a/src/storages/inMemory/__tests__/SplitsCacheInMemory.spec.ts +++ b/src/storages/inMemory/__tests__/SplitsCacheInMemory.spec.ts @@ -135,6 +135,9 @@ test('SPLITS CACHE / In Memory / flag set cache tests', () => { ], [], -1); cache.addSplit(featureFlagWithEmptyFS); + // Adding an existing FF should not affect the cache + cache.update([featureFlagTwo], [], -1); + expect(cache.getNamesByFlagSets(['o'])).toEqual([new Set(['ff_one', 'ff_two'])]); expect(cache.getNamesByFlagSets(['n'])).toEqual([new Set(['ff_one'])]); expect(cache.getNamesByFlagSets(['e'])).toEqual([new Set(['ff_one', 'ff_three'])]); From cd54074d6393c61257b07a4636856ef0ffe79514 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 11 Jun 2025 15:44:26 -0300 Subject: [PATCH 3/7] Fix comments in type definitions --- types/splitio.d.ts | 38 ++++++++++++++++++++++++-------------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/types/splitio.d.ts b/types/splitio.d.ts index ad8644b2..377d3234 100644 --- a/types/splitio.d.ts +++ b/types/splitio.d.ts @@ -66,12 +66,17 @@ interface ISharedSettings { * * @example * ``` - * const getHeaderOverrides = (context) => { - * return { - * 'Authorization': context.headers['Authorization'] + ', other-value', - * 'custom-header': 'custom-value' - * }; - * }; + * const factory = SplitFactory({ + * ... + * sync: { + * getHeaderOverrides(context) { + * return { + * 'Authorization': context.headers['Authorization'] + ', other-value', + * 'custom-header': 'custom-value' + * }; + * } + * } + * }); * ``` */ getHeaderOverrides?: (context: { headers: Record }) => Record; @@ -952,7 +957,7 @@ declare namespace SplitIO { */ prefix?: string; /** - * Number of days before cached data expires if it was not updated. If cache expires, it is cleared on initialization. + * Number of days before cached data expires if it was not successfully synchronized (i.e., last SDK_READY or SDK_UPDATE event emitted). If cache expires, it is cleared on initialization. * * @defaultValue `10` */ @@ -1292,7 +1297,7 @@ declare namespace SplitIO { */ prefix?: string; /** - * Optional settings for the 'LOCALSTORAGE' storage type. It specifies the number of days before cached data expires if it was not updated. If cache expires, it is cleared on initialization. + * Optional settings for the 'LOCALSTORAGE' storage type. It specifies the number of days before cached data expires if it was not successfully synchronized (i.e., last SDK_READY or SDK_UPDATE event emitted). If cache expires, it is cleared on initialization. * * @defaultValue `10` */ @@ -1350,12 +1355,17 @@ declare namespace SplitIO { * * @example * ``` - * const getHeaderOverrides = (context) => { - * return { - * 'Authorization': context.headers['Authorization'] + ', other-value', - * 'custom-header': 'custom-value' - * }; - * }; + * const factory = SplitFactory({ + * ... + * sync: { + * getHeaderOverrides(context) { + * return { + * 'Authorization': context.headers['Authorization'] + ', other-value', + * 'custom-header': 'custom-value' + * }; + * } + * } + * }); * ``` */ getHeaderOverrides?: (context: { headers: Record }) => Record; From 939013e64922b945419c04e941c84d686a887b2c Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 25 Jun 2025 15:45:03 -0300 Subject: [PATCH 4/7] Vulnerability fixes --- package-lock.json | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/package-lock.json b/package-lock.json index afd5917c..c9b99914 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1780,10 +1780,11 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } @@ -2218,10 +2219,11 @@ "dev": true }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -9378,9 +9380,9 @@ }, "dependencies": { "brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "requires": { "balanced-match": "^1.0.0" @@ -9700,9 +9702,9 @@ "dev": true }, "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "requires": { "balanced-match": "^1.0.0", From 8cc3531c70dd296bc61ac11ed38a1d8040fe9229 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 9 Jul 2025 11:11:10 -0300 Subject: [PATCH 5/7] Improve HTTP error message formatting --- src/logger/messages/error.ts | 2 +- src/services/splitHttpClient.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/logger/messages/error.ts b/src/logger/messages/error.ts index 123f8eee..70a87eb0 100644 --- a/src/logger/messages/error.ts +++ b/src/logger/messages/error.ts @@ -14,7 +14,7 @@ export const codesError: [number, string][] = [ [c.ERROR_SYNC_OFFLINE_LOADING, c.LOG_PREFIX_SYNC_OFFLINE + 'There was an issue loading the mock feature flags data. No changes will be applied to the current cache. %s'], [c.ERROR_STREAMING_SSE, c.LOG_PREFIX_SYNC_STREAMING + 'Failed to connect or error on streaming connection, with error message: %s'], [c.ERROR_STREAMING_AUTH, c.LOG_PREFIX_SYNC_STREAMING + 'Failed to authenticate for streaming. Error: %s.'], - [c.ERROR_HTTP, 'Response status is not OK. Status: %s. URL: %s. Message: %s'], + [c.ERROR_HTTP, 'HTTP request failed with %s. URL: %s. Message: %s'], // client status [c.ERROR_CLIENT_LISTENER, 'A listener was added for %s on the SDK, which has already fired and won\'t be emitted again. The callback won\'t be executed.'], [c.ERROR_CLIENT_DESTROYED, '%s: Client has already been destroyed - no calls possible.'], diff --git a/src/services/splitHttpClient.ts b/src/services/splitHttpClient.ts index dcb841c8..89e12533 100644 --- a/src/services/splitHttpClient.ts +++ b/src/services/splitHttpClient.ts @@ -70,7 +70,7 @@ export function splitHttpClientFactory(settings: ISettings, { getOptions, getFet } if (!resp || resp.status !== 403) { // 403's log we'll be handled somewhere else. - log[logErrorsAsInfo ? 'info' : 'error'](ERROR_HTTP, [resp ? resp.status : 'NO_STATUS', url, msg]); + log[logErrorsAsInfo ? 'info' : 'error'](ERROR_HTTP, [resp ? 'status code ' + resp.status : 'no status code', url, msg]); } const networkError: NetworkError = new Error(msg); From 39028c1748467fa2188c2874b5f60d0310bd506b Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Sun, 20 Jul 2025 11:27:55 -0300 Subject: [PATCH 6/7] enhance split filters documentation --- src/sync/polling/updaters/splitChangesUpdater.ts | 2 +- src/utils/settingsValidation/splitFilters.ts | 6 ------ types/splitio.d.ts | 12 ++++++++---- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/src/sync/polling/updaters/splitChangesUpdater.ts b/src/sync/polling/updaters/splitChangesUpdater.ts index ea5e5e44..0331bc43 100644 --- a/src/sync/polling/updaters/splitChangesUpdater.ts +++ b/src/sync/polling/updaters/splitChangesUpdater.ts @@ -59,7 +59,7 @@ interface ISplitMutations { /** * If there are defined filters and one feature flag doesn't match with them, its status is changed to 'ARCHIVE' to avoid storing it - * If there are set filter defined, names filter is ignored + * If there is `bySet` filter, `byName` and `byPrefix` filters are ignored * * @param featureFlag - feature flag to be evaluated * @param filters - splitFiltersValidation bySet | byName diff --git a/src/utils/settingsValidation/splitFilters.ts b/src/utils/settingsValidation/splitFilters.ts index cea3117f..455d3ee1 100644 --- a/src/utils/settingsValidation/splitFilters.ts +++ b/src/utils/settingsValidation/splitFilters.ts @@ -69,12 +69,6 @@ function validateSplitFilter(log: ILogger, type: SplitIO.SplitFilterType, values /** * Returns a string representing the URL encoded query component of /splitChanges URL. * - * The possible formats of the query string are: - * - null: if all filters are empty - * - '&names=': if only `byPrefix` filter is undefined - * - '&prefixes=': if only `byName` filter is undefined - * - '&names=&prefixes=': if no one is undefined - * * @param groupedFilters - object of filters. Each filter must be a list of valid, unique and ordered string values. * @returns null or string with the `split filter query` component of the URL. */ diff --git a/types/splitio.d.ts b/types/splitio.d.ts index 377d3234..e85ab01b 100644 --- a/types/splitio.d.ts +++ b/types/splitio.d.ts @@ -24,7 +24,11 @@ interface ISharedSettings { sync?: { /** * List of feature flag filters. These filters are used to fetch a subset of the feature flag definitions in your environment, in order to reduce the delay of the SDK to be ready. - * This configuration is only meaningful when the SDK is working in "standalone" mode. + * + * NOTES: + * - This configuration is only meaningful when the SDK is working in `"standalone"` mode. + * - If `bySet` filter is provided, `byName` and `byPrefix` filters are ignored. + * - If both `byName` and `byPrefix` filters are provided, the intersection of the two groups of feature flags is fetched. * * Example: * ``` @@ -69,7 +73,7 @@ interface ISharedSettings { * const factory = SplitFactory({ * ... * sync: { - * getHeaderOverrides(context) { + * getHeaderOverrides: (context) => { * return { * 'Authorization': context.headers['Authorization'] + ', other-value', * 'custom-header': 'custom-value' @@ -1135,7 +1139,7 @@ declare namespace SplitIO { */ type: SplitFilterType; /** - * List of values: feature flag names for 'byName' filter type, and feature flag name prefixes for 'byPrefix' type. + * List of values: flag set names for 'bySet' filter type, feature flag names for 'byName' filter type, and feature flag name prefixes for 'byPrefix' type. */ values: string[]; } @@ -1358,7 +1362,7 @@ declare namespace SplitIO { * const factory = SplitFactory({ * ... * sync: { - * getHeaderOverrides(context) { + * getHeaderOverrides: (context) => { * return { * 'Authorization': context.headers['Authorization'] + ', other-value', * 'custom-header': 'custom-value' From cc3ea62b129db8163de89c7cd918c5005676f4b9 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 24 Jul 2025 12:27:37 -0300 Subject: [PATCH 7/7] docs: clarify SDK sync pause during HTTP request timeouts --- CHANGES.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.txt b/CHANGES.txt index 499e296f..ab2f42c2 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -6,7 +6,7 @@ - Updated the Redis storage to: - Avoid lazy require of the `ioredis` dependency when the SDK is initialized, and - Flag the SDK as ready from cache immediately to allow queueing feature flag evaluations before SDK_READY event is emitted (Reverted in v1.7.0). - - Bugfix - Enhanced HTTP client module to implement timeouts for failing requests that might otherwise remain pending indefinitely on some Fetch API implementations. + - Bugfix - Enhanced HTTP client module to implement timeouts for failing requests that might otherwise remain pending indefinitely on some Fetch API implementations, pausing the SDK synchronization process. 2.2.0 (March 28, 2025) - Added a new optional argument to the client `getTreatment` methods to allow passing additional evaluation options, such as a map of properties to append to the generated impressions sent to Split backend. Read more in our docs.