diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c2128f2b..dec7b190 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,10 +19,10 @@ jobs: - 6379:6379 steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Set up nodejs - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: # @TODO: rollback to 'lts/*' node-version: '22' diff --git a/.github/workflows/update-license-year.yml b/.github/workflows/update-license-year.yml index 7e0a945f..bfd31318 100644 --- a/.github/workflows/update-license-year.yml +++ b/.github/workflows/update-license-year.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: fetch-depth: 0 diff --git a/CHANGES.txt b/CHANGES.txt index 4c71ee9f..f3e8b8c9 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,7 @@ +2.9.0 (November 26, 2025) + - Updated the SDK’s initial synchronization in Node.js (server-side) to use the `startup.requestTimeoutBeforeReady` and `startup.retriesOnFailureBeforeReady` options to control the timeout and retry behavior of segment requests. + - Updated the order of storage operations to prevent inconsistent states when using the `LOCALSTORAGE` storage type and the browser’s `localStorage` fails due to quota limits. + 2.8.0 (October 30, 2025) - Added new configuration for Fallback Treatments, which allows setting a treatment value and optional config to be returned in place of "control", either globally or by flag. Read more in our docs. - Added `client.getStatus()` method to retrieve the client readiness status properties (`isReady`, `isReadyFromCache`, etc). diff --git a/package-lock.json b/package-lock.json index a6093d8d..7bf8ffce 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@splitsoftware/splitio-commons", - "version": "2.8.0", + "version": "2.9.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@splitsoftware/splitio-commons", - "version": "2.8.0", + "version": "2.9.0", "license": "Apache-2.0", "dependencies": { "@types/ioredis": "^4.28.0", @@ -617,10 +617,11 @@ } }, "node_modules/@eslint/eslintrc/node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, + "license": "MIT", "dependencies": { "argparse": "^2.0.1" }, @@ -3381,10 +3382,11 @@ } }, "node_modules/eslint/node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, + "license": "MIT", "dependencies": { "argparse": "^2.0.1" }, @@ -6302,10 +6304,11 @@ "dev": true }, "node_modules/js-yaml": { - "version": "3.14.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.0.tgz", - "integrity": "sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==", + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", "dev": true, + "license": "MIT", "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" @@ -8596,9 +8599,9 @@ } }, "js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, "requires": { "argparse": "^2.0.1" @@ -10424,9 +10427,9 @@ "dev": true }, "js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, "requires": { "argparse": "^2.0.1" @@ -12814,9 +12817,9 @@ "dev": true }, "js-yaml": { - "version": "3.14.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.0.tgz", - "integrity": "sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==", + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", "dev": true, "requires": { "argparse": "^1.0.7", diff --git a/package.json b/package.json index ab0a1d2f..d68c8e55 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "2.8.0", + "version": "2.9.0", "description": "Split JavaScript SDK common components", "main": "cjs/index.js", "module": "esm/index.js", diff --git a/src/readiness/__tests__/readinessManager.spec.ts b/src/readiness/__tests__/readinessManager.spec.ts index 174f1373..34eaf9a3 100644 --- a/src/readiness/__tests__/readinessManager.spec.ts +++ b/src/readiness/__tests__/readinessManager.spec.ts @@ -3,14 +3,10 @@ import { EventEmitter } from '../../utils/MinEvents'; import { IReadinessManager } from '../types'; import { SDK_READY, SDK_UPDATE, SDK_SPLITS_ARRIVED, SDK_SEGMENTS_ARRIVED, SDK_READY_FROM_CACHE, SDK_SPLITS_CACHE_LOADED, SDK_READY_TIMED_OUT } from '../constants'; import { ISettings } from '../../types'; -import { STORAGE_LOCALSTORAGE } from '../../utils/constants'; const settings = { startup: { readyTimeout: 0, - }, - storage: { - type: STORAGE_LOCALSTORAGE } } as unknown as ISettings; diff --git a/src/storages/AbstractMySegmentsCacheSync.ts b/src/storages/AbstractMySegmentsCacheSync.ts index 7d3dc304..289fdc0d 100644 --- a/src/storages/AbstractMySegmentsCacheSync.ts +++ b/src/storages/AbstractMySegmentsCacheSync.ts @@ -49,12 +49,10 @@ export abstract class AbstractMySegmentsCacheSync implements ISegmentsCacheSync * For client-side synchronizer: it resets or updates the cache. */ resetSegments(segmentsData: MySegmentsData | IMySegmentsResponse): boolean { - this.setChangeNumber(segmentsData.cn); - + let isDiff = false; const { added, removed } = segmentsData as MySegmentsData; if (added && removed) { - let isDiff = false; added.forEach(segment => { isDiff = this.addSegment(segment) || isDiff; @@ -63,32 +61,40 @@ export abstract class AbstractMySegmentsCacheSync implements ISegmentsCacheSync removed.forEach(segment => { isDiff = this.removeSegment(segment) || isDiff; }); + } else { - return isDiff; - } + const names = ((segmentsData as IMySegmentsResponse).k || []).map(s => s.n).sort(); + const storedSegmentKeys = this.getRegisteredSegments().sort(); - const names = ((segmentsData as IMySegmentsResponse).k || []).map(s => s.n).sort(); - const storedSegmentKeys = this.getRegisteredSegments().sort(); + // Extreme fast => everything is empty + if (!names.length && !storedSegmentKeys.length) { + isDiff = false; + } else { - // Extreme fast => everything is empty - if (!names.length && !storedSegmentKeys.length) return false; + let index = 0; - let index = 0; + while (index < names.length && index < storedSegmentKeys.length && names[index] === storedSegmentKeys[index]) index++; - while (index < names.length && index < storedSegmentKeys.length && names[index] === storedSegmentKeys[index]) index++; + // Quick path => no changes + if (index === names.length && index === storedSegmentKeys.length) { + isDiff = false; + } else { - // Quick path => no changes - if (index === names.length && index === storedSegmentKeys.length) return false; + // Slowest path => add and/or remove segments + for (let removeIndex = index; removeIndex < storedSegmentKeys.length; removeIndex++) { + this.removeSegment(storedSegmentKeys[removeIndex]); + } - // Slowest path => add and/or remove segments - for (let removeIndex = index; removeIndex < storedSegmentKeys.length; removeIndex++) { - this.removeSegment(storedSegmentKeys[removeIndex]); - } + for (let addIndex = index; addIndex < names.length; addIndex++) { + this.addSegment(names[addIndex]); + } - for (let addIndex = index; addIndex < names.length; addIndex++) { - this.addSegment(names[addIndex]); + isDiff = true; + } + } } - return true; + this.setChangeNumber(segmentsData.cn); + return isDiff; } } diff --git a/src/storages/AbstractSplitsCacheSync.ts b/src/storages/AbstractSplitsCacheSync.ts index 2a4b9b78..64194561 100644 --- a/src/storages/AbstractSplitsCacheSync.ts +++ b/src/storages/AbstractSplitsCacheSync.ts @@ -14,9 +14,10 @@ export abstract class AbstractSplitsCacheSync implements ISplitsCacheSync { protected abstract setChangeNumber(changeNumber: number): boolean | void update(toAdd: ISplit[], toRemove: ISplit[], changeNumber: number): boolean { + let updated = toAdd.map(addedFF => this.addSplit(addedFF)).some(result => result); + updated = toRemove.map(removedFF => this.removeSplit(removedFF.name)).some(result => result) || updated; this.setChangeNumber(changeNumber); - const updated = toAdd.map(addedFF => this.addSplit(addedFF)).some(result => result); - return toRemove.map(removedFF => this.removeSplit(removedFF.name)).some(result => result) || updated; + return updated; } abstract getSplit(name: string): ISplit | null diff --git a/src/storages/inLocalStorage/MySegmentsCacheInLocal.ts b/src/storages/inLocalStorage/MySegmentsCacheInLocal.ts index fd038a07..0a1cc258 100644 --- a/src/storages/inLocalStorage/MySegmentsCacheInLocal.ts +++ b/src/storages/inLocalStorage/MySegmentsCacheInLocal.ts @@ -2,7 +2,7 @@ import { ILogger } from '../../logger/types'; import { isNaNNumber } from '../../utils/lang'; import { AbstractMySegmentsCacheSync } from '../AbstractMySegmentsCacheSync'; import type { MySegmentsKeyBuilder } from '../KeyBuilderCS'; -import { LOG_PREFIX, DEFINED } from './constants'; +import { DEFINED } from './constants'; import { StorageAdapter } from '../types'; export class MySegmentsCacheInLocal extends AbstractMySegmentsCacheSync { @@ -16,33 +16,22 @@ export class MySegmentsCacheInLocal extends AbstractMySegmentsCacheSync { this.log = log; this.keys = keys; this.storage = storage; - // There is not need to flush segments cache like splits cache, since resetSegments receives the up-to-date list of active segments } protected addSegment(name: string): boolean { const segmentKey = this.keys.buildSegmentNameKey(name); - try { - if (this.storage.getItem(segmentKey) === DEFINED) return false; - this.storage.setItem(segmentKey, DEFINED); - return true; - } catch (e) { - this.log.error(LOG_PREFIX + e); - return false; - } + if (this.storage.getItem(segmentKey) === DEFINED) return false; + this.storage.setItem(segmentKey, DEFINED); + return true; } protected removeSegment(name: string): boolean { const segmentKey = this.keys.buildSegmentNameKey(name); - try { - if (this.storage.getItem(segmentKey) !== DEFINED) return false; - this.storage.removeItem(segmentKey); - return true; - } catch (e) { - this.log.error(LOG_PREFIX + e); - return false; - } + if (this.storage.getItem(segmentKey) !== DEFINED) return false; + this.storage.removeItem(segmentKey); + return true; } isInSegment(name: string): boolean { @@ -63,12 +52,8 @@ export class MySegmentsCacheInLocal extends AbstractMySegmentsCacheSync { } protected setChangeNumber(changeNumber?: number) { - try { - if (changeNumber) this.storage.setItem(this.keys.buildTillKey(), changeNumber + ''); - else this.storage.removeItem(this.keys.buildTillKey()); - } catch (e) { - this.log.error(e); - } + if (changeNumber) this.storage.setItem(this.keys.buildTillKey(), changeNumber + ''); + else this.storage.removeItem(this.keys.buildTillKey()); } getChangeNumber() { diff --git a/src/storages/inLocalStorage/RBSegmentsCacheInLocal.ts b/src/storages/inLocalStorage/RBSegmentsCacheInLocal.ts index 56e8ed41..61bf13fa 100644 --- a/src/storages/inLocalStorage/RBSegmentsCacheInLocal.ts +++ b/src/storages/inLocalStorage/RBSegmentsCacheInLocal.ts @@ -26,9 +26,10 @@ export class RBSegmentsCacheInLocal implements IRBSegmentsCacheSync { } update(toAdd: IRBSegment[], toRemove: IRBSegment[], changeNumber: number): boolean { + let updated = toAdd.map(toAdd => this.add(toAdd)).some(result => result); + updated = toRemove.map(toRemove => this.remove(toRemove.name)).some(result => result) || updated; this.setChangeNumber(changeNumber); - const updated = toAdd.map(toAdd => this.add(toAdd)).some(result => result); - return toRemove.map(toRemove => this.remove(toRemove.name)).some(result => result) || updated; + return updated; } private setChangeNumber(changeNumber: number) { @@ -48,40 +49,30 @@ export class RBSegmentsCacheInLocal implements IRBSegmentsCacheSync { } private add(rbSegment: IRBSegment): boolean { - try { - const name = rbSegment.name; - const rbSegmentKey = this.keys.buildRBSegmentKey(name); - const rbSegmentFromStorage = this.storage.getItem(rbSegmentKey); - const previous = rbSegmentFromStorage ? JSON.parse(rbSegmentFromStorage) : null; + const name = rbSegment.name; + const rbSegmentKey = this.keys.buildRBSegmentKey(name); + const rbSegmentFromStorage = this.storage.getItem(rbSegmentKey); + const previous = rbSegmentFromStorage ? JSON.parse(rbSegmentFromStorage) : null; - this.storage.setItem(rbSegmentKey, JSON.stringify(rbSegment)); + this.storage.setItem(rbSegmentKey, JSON.stringify(rbSegment)); - let usesSegmentsDiff = 0; - if (previous && usesSegments(previous)) usesSegmentsDiff--; - if (usesSegments(rbSegment)) usesSegmentsDiff++; - if (usesSegmentsDiff !== 0) this.updateSegmentCount(usesSegmentsDiff); + let usesSegmentsDiff = 0; + if (previous && usesSegments(previous)) usesSegmentsDiff--; + if (usesSegments(rbSegment)) usesSegmentsDiff++; + if (usesSegmentsDiff !== 0) this.updateSegmentCount(usesSegmentsDiff); - return true; - } catch (e) { - this.log.error(LOG_PREFIX + e); - return false; - } + return true; } private remove(name: string): boolean { - try { - const rbSegment = this.get(name); - if (!rbSegment) return false; + const rbSegment = this.get(name); + if (!rbSegment) return false; - this.storage.removeItem(this.keys.buildRBSegmentKey(name)); + this.storage.removeItem(this.keys.buildRBSegmentKey(name)); - if (usesSegments(rbSegment)) this.updateSegmentCount(-1); + if (usesSegments(rbSegment)) this.updateSegmentCount(-1); - return true; - } catch (e) { - this.log.error(LOG_PREFIX + e); - return false; - } + return true; } private getNames(): string[] { diff --git a/src/storages/inLocalStorage/SplitsCacheInLocal.ts b/src/storages/inLocalStorage/SplitsCacheInLocal.ts index 3aa08452..30945684 100644 --- a/src/storages/inLocalStorage/SplitsCacheInLocal.ts +++ b/src/storages/inLocalStorage/SplitsCacheInLocal.ts @@ -80,44 +80,34 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync { } addSplit(split: ISplit) { - try { - const name = split.name; - const splitKey = this.keys.buildSplitKey(name); - const splitFromStorage = this.storage.getItem(splitKey); - const previousSplit = splitFromStorage ? JSON.parse(splitFromStorage) : null; - - if (previousSplit) { - this._decrementCounts(previousSplit); - this.removeFromFlagSets(previousSplit.name, previousSplit.sets); - } + const name = split.name; + const splitKey = this.keys.buildSplitKey(name); + const splitFromStorage = this.storage.getItem(splitKey); + const previousSplit = splitFromStorage ? JSON.parse(splitFromStorage) : null; + + if (previousSplit) { + this._decrementCounts(previousSplit); + this.removeFromFlagSets(previousSplit.name, previousSplit.sets); + } - this.storage.setItem(splitKey, JSON.stringify(split)); + this.storage.setItem(splitKey, JSON.stringify(split)); - this._incrementCounts(split); - this.addToFlagSets(split); + this._incrementCounts(split); + this.addToFlagSets(split); - return true; - } catch (e) { - this.log.error(LOG_PREFIX + e); - return false; - } + return true; } removeSplit(name: string): boolean { - try { - const split = this.getSplit(name); - if (!split) return false; + const split = this.getSplit(name); + if (!split) return false; - this.storage.removeItem(this.keys.buildSplitKey(name)); + this.storage.removeItem(this.keys.buildSplitKey(name)); - this._decrementCounts(split); - this.removeFromFlagSets(split.name, split.sets); + this._decrementCounts(split); + this.removeFromFlagSets(split.name, split.sets); - return true; - } catch (e) { - this.log.error(LOG_PREFIX + e); - return false; - } + return true; } getSplit(name: string): ISplit | null { @@ -206,6 +196,9 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync { const flagSetFromStorage = this.storage.getItem(flagSetKey); const flagSetCache = new Set(flagSetFromStorage ? JSON.parse(flagSetFromStorage) : []); + + if (flagSetCache.has(featureFlag.name)) return; + flagSetCache.add(featureFlag.name); this.storage.setItem(flagSetKey, JSON.stringify(setToArray(flagSetCache))); diff --git a/src/storages/inLocalStorage/__tests__/validateCache.spec.ts b/src/storages/inLocalStorage/__tests__/validateCache.spec.ts index 102444ba..7feea541 100644 --- a/src/storages/inLocalStorage/__tests__/validateCache.spec.ts +++ b/src/storages/inLocalStorage/__tests__/validateCache.spec.ts @@ -47,6 +47,7 @@ describe.each(storages)('validateCache', (storage) => { test('if there is cache and it must not be cleared, it should return true', async () => { storage.setItem(keys.buildSplitsTillKey(), '1'); storage.setItem(keys.buildHashKey(), FULL_SETTINGS_HASH); + await storage.save && storage.save(); expect(await validateCache({}, storage, fullSettings, keys, splits, rbSegments, segments, largeSegments)).toBe(true); @@ -66,6 +67,7 @@ describe.each(storages)('validateCache', (storage) => { storage.setItem(keys.buildSplitsTillKey(), '1'); storage.setItem(keys.buildHashKey(), FULL_SETTINGS_HASH); storage.setItem(keys.buildLastUpdatedKey(), Date.now() - 1000 * 60 * 60 * 24 * 2 + ''); // 2 days ago + await storage.save && storage.save(); expect(await validateCache({ expirationDays: 1 }, storage, fullSettings, keys, splits, rbSegments, segments, largeSegments)).toBe(false); @@ -83,6 +85,7 @@ describe.each(storages)('validateCache', (storage) => { test('if there is cache and its hash has changed, it should clear cache and return false', async () => { storage.setItem(keys.buildSplitsTillKey(), '1'); storage.setItem(keys.buildHashKey(), FULL_SETTINGS_HASH); + await storage.save && storage.save(); expect(await validateCache({}, storage, { ...fullSettings, core: { ...fullSettings.core, authorizationKey: 'another-sdk-key' } }, keys, splits, rbSegments, segments, largeSegments)).toBe(false); @@ -99,8 +102,10 @@ describe.each(storages)('validateCache', (storage) => { test('if there is cache and clearOnInit is true, it should clear cache and return false', async () => { // Older cache version (without last clear) + storage.removeItem(keys.buildLastClear()); storage.setItem(keys.buildSplitsTillKey(), '1'); storage.setItem(keys.buildHashKey(), FULL_SETTINGS_HASH); + await storage.save && storage.save(); expect(await validateCache({ clearOnInit: true }, storage, fullSettings, keys, splits, rbSegments, segments, largeSegments)).toBe(false); diff --git a/src/storages/inMemory/RBSegmentsCacheInMemory.ts b/src/storages/inMemory/RBSegmentsCacheInMemory.ts index 2b876202..cd98dc62 100644 --- a/src/storages/inMemory/RBSegmentsCacheInMemory.ts +++ b/src/storages/inMemory/RBSegmentsCacheInMemory.ts @@ -16,9 +16,10 @@ export class RBSegmentsCacheInMemory implements IRBSegmentsCacheSync { } update(toAdd: IRBSegment[], toRemove: IRBSegment[], changeNumber: number): boolean { + let updated = toAdd.map(toAdd => this.add(toAdd)).some(result => result); + updated = toRemove.map(toRemove => this.remove(toRemove.name)).some(result => result) || updated; this.changeNumber = changeNumber; - const updated = toAdd.map(toAdd => this.add(toAdd)).some(result => result); - return toRemove.map(toRemove => this.remove(toRemove.name)).some(result => result) || updated; + return updated; } private add(rbSegment: IRBSegment): boolean { diff --git a/src/storages/inRedis/SegmentsCacheInRedis.ts b/src/storages/inRedis/SegmentsCacheInRedis.ts index d645495f..5a81dede 100644 --- a/src/storages/inRedis/SegmentsCacheInRedis.ts +++ b/src/storages/inRedis/SegmentsCacheInRedis.ts @@ -1,6 +1,6 @@ import { ILogger } from '../../logger/types'; import { isNaNNumber } from '../../utils/lang'; -import { LOG_PREFIX } from '../inLocalStorage/constants'; +import { LOG_PREFIX } from './constants'; import { KeyBuilderSS } from '../KeyBuilderSS'; import { ISegmentsCacheAsync } from '../types'; import type { RedisAdapter } from './RedisAdapter'; diff --git a/src/sync/polling/syncTasks/segmentsSyncTask.ts b/src/sync/polling/syncTasks/segmentsSyncTask.ts index f5a93711..b69d7087 100644 --- a/src/sync/polling/syncTasks/segmentsSyncTask.ts +++ b/src/sync/polling/syncTasks/segmentsSyncTask.ts @@ -23,6 +23,8 @@ export function segmentsSyncTaskFactory( segmentChangesFetcherFactory(fetchSegmentChanges), storage.segments, readiness, + settings.startup.requestTimeoutBeforeReady, + settings.startup.retriesOnFailureBeforeReady, ), settings.scheduler.segmentsRefreshRate, 'segmentChangesUpdater' diff --git a/src/sync/polling/updaters/mySegmentsUpdater.ts b/src/sync/polling/updaters/mySegmentsUpdater.ts index 4b6038c5..5421d3f9 100644 --- a/src/sync/polling/updaters/mySegmentsUpdater.ts +++ b/src/sync/polling/updaters/mySegmentsUpdater.ts @@ -66,10 +66,10 @@ export function mySegmentsUpdaterFactory( new Promise((res) => { updateSegments(segmentsData); res(true); }) : // If not provided, fetch mySegments mySegmentsFetcher(matchingKey, noCache, till, _promiseDecorator).then(segments => { - // Only when we have downloaded segments completely, we should not keep retrying anymore - startingUp = false; - updateSegments(segments); + + // Only when we have downloaded and stored segments completely, we should not keep retrying anymore + startingUp = false; return true; }); diff --git a/src/sync/polling/updaters/segmentChangesUpdater.ts b/src/sync/polling/updaters/segmentChangesUpdater.ts index ab951b24..7fe5b7b7 100644 --- a/src/sync/polling/updaters/segmentChangesUpdater.ts +++ b/src/sync/polling/updaters/segmentChangesUpdater.ts @@ -4,6 +4,7 @@ import { IReadinessManager } from '../../../readiness/types'; import { SDK_SEGMENTS_ARRIVED } from '../../../readiness/constants'; import { ILogger } from '../../../logger/types'; import { LOG_PREFIX_INSTANTIATION, LOG_PREFIX_SYNC_SEGMENTS } from '../../../logger/constants'; +import { timeout } from '../../../utils/promise/timeout'; type ISegmentChangesUpdater = (fetchOnlyNew?: boolean, segmentName?: string, noCache?: boolean, till?: number) => Promise @@ -23,11 +24,18 @@ export function segmentChangesUpdaterFactory( segmentChangesFetcher: ISegmentChangesFetcher, segments: ISegmentsCacheBase, readiness?: IReadinessManager, + requestTimeoutBeforeReady?: number, + retriesOnFailureBeforeReady?: number, ): ISegmentChangesUpdater { let readyOnAlreadyExistentState = true; - function updateSegment(segmentName: string, noCache?: boolean, till?: number, fetchOnlyNew?: boolean): Promise { + function _promiseDecorator(promise: Promise) { + if (readyOnAlreadyExistentState && requestTimeoutBeforeReady) promise = timeout(requestTimeoutBeforeReady, promise); + return promise; + } + + function updateSegment(segmentName: string, noCache?: boolean, till?: number, fetchOnlyNew?: boolean, retries?: number): Promise { log.debug(`${LOG_PREFIX_SYNC_SEGMENTS}Processing segment ${segmentName}`); let sincePromise = Promise.resolve(segments.getChangeNumber(segmentName)); @@ -35,13 +43,19 @@ export function segmentChangesUpdaterFactory( // if fetchOnlyNew flag, avoid processing already fetched segments return fetchOnlyNew && since !== undefined ? false : - segmentChangesFetcher(since || -1, segmentName, noCache, till).then((changes) => { + segmentChangesFetcher(since || -1, segmentName, noCache, till, _promiseDecorator).then((changes) => { return Promise.all(changes.map(x => { log.debug(`${LOG_PREFIX_SYNC_SEGMENTS}Processing ${segmentName} with till = ${x.till}. Added: ${x.added.length}. Removed: ${x.removed.length}`); return segments.update(segmentName, x.added, x.removed, x.till); })).then((updates) => { return updates.some(update => update); }); + }).catch(error => { + if (retries) { + log.warn(`${LOG_PREFIX_SYNC_SEGMENTS}Retrying fetch of segment ${segmentName} (attempt #${retries}). Reason: ${error}`); + return updateSegment(segmentName, noCache, till, fetchOnlyNew, retries - 1); + } + throw error; }); }); } @@ -63,8 +77,7 @@ export function segmentChangesUpdaterFactory( let segmentsPromise = Promise.resolve(segmentName ? [segmentName] : segments.getRegisteredSegments()); return segmentsPromise.then(segmentNames => { - // Async fetchers - const updaters = segmentNames.map(segmentName => updateSegment(segmentName, noCache, till, fetchOnlyNew)); + const updaters = segmentNames.map(segmentName => updateSegment(segmentName, noCache, till, fetchOnlyNew, readyOnAlreadyExistentState ? retriesOnFailureBeforeReady : 0)); return Promise.all(updaters).then(shouldUpdateFlags => { // if at least one segment fetch succeeded, mark segments ready diff --git a/src/sync/polling/updaters/splitChangesUpdater.ts b/src/sync/polling/updaters/splitChangesUpdater.ts index 6c6371e3..3a1fc5a7 100644 --- a/src/sync/polling/updaters/splitChangesUpdater.ts +++ b/src/sync/polling/updaters/splitChangesUpdater.ts @@ -120,8 +120,8 @@ export function splitChangesUpdaterFactory( storage: Pick, splitFiltersValidation: ISplitFiltersValidation, splitsEventEmitter?: ISplitsEventEmitter, - requestTimeoutBeforeReady: number = 0, - retriesOnFailureBeforeReady: number = 0, + requestTimeoutBeforeReady = 0, + retriesOnFailureBeforeReady = 0, isClientSide?: boolean ): SplitChangesUpdater { const { splits, rbSegments, segments } = storage; @@ -163,8 +163,6 @@ export function splitChangesUpdaterFactory( splitChangesFetcher(since, noCache, till, rbSince, _promiseDecorator) ) .then((splitChanges: ISplitChangesResponse) => { - startingUp = false; - const usedSegments = new Set(); let ffUpdate: MaybeThenable = false; @@ -187,6 +185,8 @@ export function splitChangesUpdaterFactory( ]).then(([ffChanged, rbsChanged]) => { if (storage.save) storage.save(); + startingUp = false; + if (splitsEventEmitter) { // To emit SDK_SPLITS_ARRIVED for server-side SDK, we must check that all registered segments have been fetched return Promise.resolve(!splitsEventEmitter.splitsArrived || ((ffChanged || rbsChanged) && (isClientSide || checkAllSegmentsExist(segments)))) @@ -201,14 +201,13 @@ export function splitChangesUpdaterFactory( }); }) .catch(error => { - log.warn(SYNC_SPLITS_FETCH_FAILS, [error]); - if (startingUp && retriesOnFailureBeforeReady > retry) { retry += 1; - log.info(SYNC_SPLITS_FETCH_RETRY, [retry, error]); + log.warn(SYNC_SPLITS_FETCH_RETRY, [retry, error]); return _splitChangesUpdater(sinces, retry); } else { startingUp = false; + log.warn(SYNC_SPLITS_FETCH_FAILS, [error]); } return false; }); diff --git a/types/splitio.d.ts b/types/splitio.d.ts index bd7da67c..ba2be58b 100644 --- a/types/splitio.d.ts +++ b/types/splitio.d.ts @@ -1335,8 +1335,6 @@ declare namespace SplitIO { /** * Defines the factory function to instantiate the storage. If not provided, the default in-memory storage is used. * - * NOTE: Currently, there is no persistent storage option available for the React Native SDK; only `InLocalStorage` for the Browser SDK. - * * Example: * ``` * SplitFactory({