Skip to content
Merged
3 changes: 2 additions & 1 deletion CHANGES.txt
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
2 changes: 2 additions & 0 deletions src/sync/polling/syncTasks/segmentsSyncTask.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ export function segmentsSyncTaskFactory(
segmentChangesFetcherFactory(fetchSegmentChanges),
storage.segments,
readiness,
settings.startup.requestTimeoutBeforeReady,
settings.startup.retriesOnFailureBeforeReady,
),
settings.scheduler.segmentsRefreshRate,
'segmentChangesUpdater'
Expand Down
21 changes: 17 additions & 4 deletions src/sync/polling/updaters/segmentChangesUpdater.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<boolean>

Expand All @@ -23,25 +24,38 @@ 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<boolean> {
function _promiseDecorator<T>(promise: Promise<T>) {
if (readyOnAlreadyExistentState && requestTimeoutBeforeReady) promise = timeout(requestTimeoutBeforeReady, promise);
return promise;
}

function updateSegment(segmentName: string, noCache?: boolean, till?: number, fetchOnlyNew?: boolean, retries?: number): Promise<boolean> {
log.debug(`${LOG_PREFIX_SYNC_SEGMENTS}Processing segment ${segmentName}`);
let sincePromise = Promise.resolve(segments.getChangeNumber(segmentName));

return sincePromise.then(since => {
// 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;
});
});
}
Expand All @@ -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
Expand Down
9 changes: 4 additions & 5 deletions src/sync/polling/updaters/splitChangesUpdater.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,8 @@ export function splitChangesUpdaterFactory(
storage: Pick<IStorageBase, 'splits' | 'rbSegments' | 'segments' | 'save'>,
splitFiltersValidation: ISplitFiltersValidation,
splitsEventEmitter?: ISplitsEventEmitter,
requestTimeoutBeforeReady: number = 0,
retriesOnFailureBeforeReady: number = 0,
requestTimeoutBeforeReady = 0,
retriesOnFailureBeforeReady = 0,
isClientSide?: boolean
): SplitChangesUpdater {
const { splits, rbSegments, segments } = storage;
Expand Down Expand Up @@ -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;
});
Expand Down