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 3838af79..f3e8b8c9 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,4 +1,5 @@ -2.8.1 (November 25, 2025) +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) diff --git a/package-lock.json b/package-lock.json index f2fdbade..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", @@ -621,6 +621,7 @@ "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" }, @@ -3385,6 +3386,7 @@ "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" }, @@ -6306,6 +6308,7 @@ "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" 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/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/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 b372fa46..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; @@ -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; });