diff --git a/src/services/splitApi.ts b/src/services/splitApi.ts index d5c2b827..29fb298a 100644 --- a/src/services/splitApi.ts +++ b/src/services/splitApi.ts @@ -5,6 +5,8 @@ import { splitHttpClientFactory } from './splitHttpClient'; import { ISplitApi } from './types'; import { ISettingsInternal } from '../utils/settingsValidation/types'; +const noCacheHeaderOptions = { headers: { 'Cache-Control': 'no-cache' } }; + function userKeyToQueryParam(userKey: string) { return 'users=' + encodeURIComponent(userKey); // no need to check availability of `encodeURIComponent`, since it is a global highly supported. } @@ -34,17 +36,17 @@ export function splitApiFactory(settings: ISettings, platform: IPlatform): ISpli return splitHttpClient(url); }, - fetchSplitChanges(since: number) { + fetchSplitChanges(since: number, noCache?: boolean) { const url = `${urls.sdk}/splitChanges?since=${since}${filterQueryString || ''}`; - return splitHttpClient(url); + return splitHttpClient(url, noCache ? noCacheHeaderOptions : undefined); }, - fetchSegmentChanges(since: number, segmentName: string) { + fetchSegmentChanges(since: number, segmentName: string, noCache?: boolean) { const url = `${urls.sdk}/segmentChanges/${segmentName}?since=${since}`; - return splitHttpClient(url); + return splitHttpClient(url, noCache ? noCacheHeaderOptions : undefined); }, - fetchMySegments(userMatchingKey: string) { + fetchMySegments(userMatchingKey: string, noCache?: boolean) { /** * URI encoding of user keys in order to: * - avoid 400 responses (due to URI malformed). E.g.: '/api/mySegments/%' @@ -52,35 +54,35 @@ export function splitApiFactory(settings: ISettings, platform: IPlatform): ISpli * - match user keys with special characters. E.g.: 'foo%bar', 'foo/bar' */ const url = `${urls.sdk}/mySegments/${encodeURIComponent(userMatchingKey)}`; - return splitHttpClient(url); + return splitHttpClient(url, noCache ? noCacheHeaderOptions : undefined); }, postEventsBulk(body: string) { const url = `${urls.events}/events/bulk`; - return splitHttpClient(url, 'POST', body); + return splitHttpClient(url, { method: 'POST', body }); }, postTestImpressionsBulk(body: string) { const url = `${urls.events}/testImpressions/bulk`; - return splitHttpClient(url, 'POST', body, false, { + return splitHttpClient(url, { // Adding extra headers to send impressions in OPTIMIZED or DEBUG modes. - SplitSDKImpressionsMode + method: 'POST', body, headers: { SplitSDKImpressionsMode } }); }, postTestImpressionsCount(body: string) { const url = `${urls.events}/testImpressions/count`; - return splitHttpClient(url, 'POST', body); + return splitHttpClient(url, { method: 'POST', body }); }, postMetricsCounters(body: string) { const url = `${urls.events}/metrics/counters`; - return splitHttpClient(url, 'POST', body, true); + return splitHttpClient(url, { method: 'POST', body }, true); }, postMetricsTimes(body: string) { const url = `${urls.events}/metrics/times`; - return splitHttpClient(url, 'POST', body, true); + return splitHttpClient(url, { method: 'POST', body }, true); } }; } diff --git a/src/services/splitHttpClient.ts b/src/services/splitHttpClient.ts index 116e5323..224dcf99 100644 --- a/src/services/splitHttpClient.ts +++ b/src/services/splitHttpClient.ts @@ -1,4 +1,4 @@ -import { IFetch, ISplitHttpClient } from './types'; +import { IFetch, IRequestOptions, ISplitHttpClient } from './types'; import { SplitError, SplitNetworkError } from '../utils/lang/errors'; import objectAssign from 'object-assign'; import { logFactory } from '../logger/sdkLogger'; @@ -33,9 +33,13 @@ export function splitHttpClientFactory(apikey: string, metadata: IMetadata, getF if (metadata.ip) headers['SplitSDKMachineIP'] = metadata.ip; if (metadata.hostname) headers['SplitSDKMachineName'] = metadata.hostname; - return function httpClient(url: string, method: string = 'GET', body?: string, logErrorsAsInfo: boolean = false, extraHeaders?: Record): Promise { - const rHeaders = extraHeaders ? objectAssign({}, headers, extraHeaders) : headers; - const request = objectAssign({ headers: rHeaders, method, body }, options); + return function httpClient(url: string, reqOpts: IRequestOptions = {}, logErrorsAsInfo: boolean = false): Promise { + + const request = objectAssign({ + headers: reqOpts.headers ? objectAssign({}, headers, reqOpts.headers) : headers, + method: reqOpts.method || 'GET', + body: reqOpts.body + }, options); // using `fetch(url, options)` signature to work with unfetch, a lightweight ponyfill of fetch API. return fetch ? fetch(url, request) diff --git a/src/services/types.ts b/src/services/types.ts index ba134f5d..7e45c9df 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -1,22 +1,21 @@ -export type IFetch = ( - url: string, - options?: { - method?: string, - headers?: Record, - credentials?: 'include' | 'omit', - body?: string - } -) => Promise - -export type ISplitHttpClient = (url: string, method?: string, body?: string, logErrorsAsInfo?: boolean, extraHeaders?: Record) => Promise +export type IRequestOptions = { + method?: string, + headers?: Record, + body?: string +}; + +// Reduced version of Fetch API +export type IFetch = (url: string, options?: IRequestOptions) => Promise + +export type ISplitHttpClient = (url: string, options?: IRequestOptions, logErrorsAsInfo?: boolean) => Promise export type IFetchAuth = (userKeys?: string[]) => Promise -export type IFetchSplitChanges = (since: number) => Promise +export type IFetchSplitChanges = (since: number, noCache?: boolean) => Promise -export type IFetchSegmentChanges = (since: number, segmentName: string) => Promise +export type IFetchSegmentChanges = (since: number, segmentName: string, noCache?: boolean) => Promise -export type IFetchMySegments = (userMatchingKey: string) => Promise +export type IFetchMySegments = (userMatchingKey: string, noCache?: boolean) => Promise export type IPostEventsBulk = (body: string) => Promise diff --git a/src/sync/polling/fetchers/mySegmentsFetcher.ts b/src/sync/polling/fetchers/mySegmentsFetcher.ts index 3ead9bef..2ba6b7b5 100644 --- a/src/sync/polling/fetchers/mySegmentsFetcher.ts +++ b/src/sync/polling/fetchers/mySegmentsFetcher.ts @@ -10,11 +10,12 @@ import { IMySegmentsFetcher } from './types'; export default function mySegmentsFetcherFactory(fetchMySegments: IFetchMySegments, userMatchingKey: string): IMySegmentsFetcher { return function mySegmentsFetcher( + noCache?: boolean, // Optional decorator for `fetchMySegments` promise, such as timeout or time tracker decorator?: (promise: Promise) => Promise ): Promise { - let mySegmentsPromise = fetchMySegments(userMatchingKey); + let mySegmentsPromise = fetchMySegments(userMatchingKey, noCache); if (decorator) mySegmentsPromise = decorator(mySegmentsPromise); // Extract segment names diff --git a/src/sync/polling/fetchers/segmentChangesFetcher.ts b/src/sync/polling/fetchers/segmentChangesFetcher.ts index bf98d881..db7d6c38 100644 --- a/src/sync/polling/fetchers/segmentChangesFetcher.ts +++ b/src/sync/polling/fetchers/segmentChangesFetcher.ts @@ -2,8 +2,8 @@ import { IFetchSegmentChanges } from '../../../services/types'; import { ISegmentChangesResponse } from '../../../dtos/types'; import { ISegmentChangesFetcher } from './types'; -function greedyFetch(fetchSegmentChanges: IFetchSegmentChanges, since: number, segmentName: string): Promise { - return fetchSegmentChanges(since, segmentName) +function greedyFetch(fetchSegmentChanges: IFetchSegmentChanges, since: number, segmentName: string, noCache?: boolean): Promise { + return fetchSegmentChanges(since, segmentName, noCache) // no need to handle json parsing errors as SplitError, since errors are handled differently for segments .then(resp => resp.json()) .then((json: ISegmentChangesResponse) => { @@ -11,7 +11,7 @@ function greedyFetch(fetchSegmentChanges: IFetchSegmentChanges, since: number, s if (since === till) { return [json]; } else { - return Promise.all([json, greedyFetch(fetchSegmentChanges, till, segmentName)]).then(flatMe => { + return Promise.all([json, greedyFetch(fetchSegmentChanges, till, segmentName, noCache)]).then(flatMe => { return [flatMe[0], ...flatMe[1]]; }); } @@ -34,11 +34,12 @@ export default function segmentChangesFetcherFactory(fetchSegmentChanges: IFetch return function segmentChangesFetcher( since: number, segmentName: string, + noCache?: boolean, // Optional decorator for `fetchMySegments` promise, such as timeout or time tracker decorator?: (promise: Promise) => Promise ): Promise { - let segmentsPromise = greedyFetch(fetchSegmentChanges, since, segmentName); + let segmentsPromise = greedyFetch(fetchSegmentChanges, since, segmentName, noCache); if (decorator) segmentsPromise = decorator(segmentsPromise); return segmentsPromise; diff --git a/src/sync/polling/fetchers/splitChangesFetcher.ts b/src/sync/polling/fetchers/splitChangesFetcher.ts index 82cf1a85..d8679c31 100644 --- a/src/sync/polling/fetchers/splitChangesFetcher.ts +++ b/src/sync/polling/fetchers/splitChangesFetcher.ts @@ -10,11 +10,12 @@ export default function splitChangesFetcherFactory(fetchSplitChanges: IFetchSpli return function splitChangesFetcher( since: number, + noCache?: boolean, // Optional decorator for `fetchSplitChanges` promise, such as timeout or time tracker decorator?: (promise: Promise) => Promise ) { - let splitsPromise = fetchSplitChanges(since); + let splitsPromise = fetchSplitChanges(since, noCache); if (decorator) splitsPromise = decorator(splitsPromise); return splitsPromise diff --git a/src/sync/polling/fetchers/types.ts b/src/sync/polling/fetchers/types.ts index a0dad6a6..e39cb85a 100644 --- a/src/sync/polling/fetchers/types.ts +++ b/src/sync/polling/fetchers/types.ts @@ -2,12 +2,18 @@ import { ISplitChangesResponse, ISegmentChangesResponse } from '../../../dtos/ty export type ISplitChangesFetcher = ( since: number, - decorator?: (promise: Promise) => Promise) => Promise + noCache?: boolean, + decorator?: (promise: Promise) => Promise +) => Promise export type ISegmentChangesFetcher = ( since: number, segmentName: string, - decorator?: (promise: Promise) => Promise) => Promise + noCache?: boolean, + decorator?: (promise: Promise) => Promise +) => Promise export type IMySegmentsFetcher = ( - decorator?: (promise: Promise) => Promise) => Promise + noCache?: boolean, + decorator?: (promise: Promise) => Promise +) => Promise diff --git a/src/sync/polling/syncTasks/__tests__/splitChangesUpdater.spec.ts b/src/sync/polling/syncTasks/__tests__/splitChangesUpdater.spec.ts index 57033e78..3f6ebeb1 100644 --- a/src/sync/polling/syncTasks/__tests__/splitChangesUpdater.spec.ts +++ b/src/sync/polling/syncTasks/__tests__/splitChangesUpdater.spec.ts @@ -69,7 +69,7 @@ test('splitChangesUpdater / factory', (done) => { const readinessManager = readinessManagerFactory(EventEmitter); const splitsEmitSpy = jest.spyOn(readinessManager.splits, 'emit'); - const splitChangesUpdater = splitChangesUpdaterFactory(splitChangesFetcher, splitsCache, segmentsCache, readinessManager.splits); + const splitChangesUpdater = splitChangesUpdaterFactory(splitChangesFetcher, splitsCache, segmentsCache, readinessManager.splits, 1000, 1); splitChangesUpdater().then((result) => { expect(setChangeNumber.mock.calls.length).toBe(1); diff --git a/src/sync/polling/syncTasks/mySegmentsSyncTask.ts b/src/sync/polling/syncTasks/mySegmentsSyncTask.ts index 7ff84ff5..be805204 100644 --- a/src/sync/polling/syncTasks/mySegmentsSyncTask.ts +++ b/src/sync/polling/syncTasks/mySegmentsSyncTask.ts @@ -10,9 +10,10 @@ import { logFactory } from '../../../logger/sdkLogger'; import { IFetchMySegments } from '../../../services/types'; import mySegmentsFetcherFactory from '../fetchers/mySegmentsFetcher'; import { ISettings } from '../../../types'; +import { SDK_SEGMENTS_ARRIVED } from '../../../readiness/constants'; const log = logFactory('splitio-sync:my-segments'); -type IMySegmentsUpdater = (segmentList?: string[]) => Promise +type IMySegmentsUpdater = (segmentList?: string[], noCache?: boolean) => Promise /** * factory of MySegments updater (a.k.a, SegmentsSyncTask), a task that: @@ -50,16 +51,16 @@ function mySegmentsUpdaterFactory( // Notify update if required if (splitsCache.usesSegments() && (shouldNotifyUpdate || readyOnAlreadyExistentState)) { readyOnAlreadyExistentState = false; - segmentsEventEmitter.emit('SDK_SEGMENTS_ARRIVED'); + segmentsEventEmitter.emit(SDK_SEGMENTS_ARRIVED); } } - function _mySegmentsUpdater(retry: number, segmentList?: string[]): Promise { + function _mySegmentsUpdater(retry: number, segmentList?: string[], noCache?: boolean): Promise { const updaterPromise: Promise = segmentList ? // If segmentList is provided, there is no need to fetch mySegments new Promise((res) => { updateSegments(segmentList); res(true); }) : // If not provided, fetch mySegments - mySegmentsFetcher(_promiseDecorator).then(segments => { + mySegmentsFetcher(noCache, _promiseDecorator).then(segments => { // Only when we have downloaded segments completely, we should not keep retrying anymore startingUp = false; @@ -74,7 +75,7 @@ function mySegmentsUpdaterFactory( if (startingUp && retriesOnFailureBeforeReady > retry) { retry += 1; log.warn(`Retrying download of segments #${retry}. Reason: ${error}`); - return _mySegmentsUpdater(retry); + return _mySegmentsUpdater(retry); // no need to forward `segmentList` and `noCache` params } else { startingUp = false; } @@ -87,9 +88,10 @@ function mySegmentsUpdaterFactory( * MySegments updater returns a promise that resolves with a `false` boolean value if it fails to fetch mySegments or synchronize them with the storage. * * @param {string[] | undefined} segmentList list of mySegments names to sync in the storage. If the list is `undefined`, it fetches them before syncing in the storage. + * @param {boolean | undefined} noCache true to revalidate data to fetch */ - return function mySegmentsUpdater(segmentList?: string[]) { - return _mySegmentsUpdater(0, segmentList); + return function mySegmentsUpdater(segmentList?: string[], noCache?: boolean) { + return _mySegmentsUpdater(0, segmentList, noCache); }; } diff --git a/src/sync/polling/syncTasks/segmentsSyncTask.ts b/src/sync/polling/syncTasks/segmentsSyncTask.ts index e0bb00f5..39a91f44 100644 --- a/src/sync/polling/syncTasks/segmentsSyncTask.ts +++ b/src/sync/polling/syncTasks/segmentsSyncTask.ts @@ -10,10 +10,11 @@ import { logFactory } from '../../../logger/sdkLogger'; import segmentChangesFetcherFactory from '../fetchers/segmentChangesFetcher'; import { IFetchSegmentChanges } from '../../../services/types'; import { ISettings } from '../../../types'; +import { SDK_SEGMENTS_ARRIVED } from '../../../readiness/constants'; const log = logFactory('splitio-sync:segment-changes'); const inputValidationLog = logFactory('', { displayAllErrors: true }); -type ISegmentChangesUpdater = (segmentNames?: string[]) => Promise +type ISegmentChangesUpdater = (segmentNames?: string[], noCache?: boolean, fetchOnlyNew?: boolean) => Promise /** * factory of SegmentChanges updater (a.k.a, SegmentsSyncTask), a task that: @@ -42,23 +43,27 @@ function segmentChangesUpdaterFactory( * Thus, a false result doesn't imply that SDK_SEGMENTS_ARRIVED was not emitted. * * @param {string[] | undefined} segmentNames list of segment names to fetch. By passing `undefined` it fetches the list of segments registered at the storage + * @param {boolean | undefined} noCache true to revalidate data to fetch on a SEGMENT_UPDATE notifications. + * @param {boolean | undefined} fetchOnlyNew if true, only fetch the segments that not exists, i.e., which `changeNumber` is equal to -1. + * This param is used by SplitUpdateWorker on server-side SDK, to fetch new registered segments on SPLIT_UPDATE notifications. */ - return function segmentChangesUpdater(segmentNames?: string[]) { + return function segmentChangesUpdater(segmentNames?: string[], noCache?: boolean, fetchOnlyNew?: boolean) { log.debug('Started segments update'); // If not a segment name provided, read list of available segments names to be updated. - if (!segmentNames) segmentNames = segmentsCache.getRegisteredSegments(); + let segments = segmentNames ? segmentNames : segmentsCache.getRegisteredSegments(); + if (fetchOnlyNew) segments = segments.filter(segmentName => segmentsCache.getChangeNumber(segmentName) === -1); // Async fetchers are collected here. const updaters: Promise[] = []; - for (let index = 0; index < segmentNames.length; index++) { - const segmentName = segmentNames[index]; + for (let index = 0; index < segments.length; index++) { + const segmentName = segments[index]; const since = segmentsCache.getChangeNumber(segmentName); log.debug(`Processing segment ${segmentName}`); - updaters.push(segmentChangesFetcher(since, segmentName, _promiseDecorator).then(function (changes) { + updaters.push(segmentChangesFetcher(since, segmentName, noCache, _promiseDecorator).then(function (changes) { let changeNumber = -1; changes.forEach(x => { if (x.added.length > 0) segmentsCache.addToSegment(segmentName, x.added); @@ -80,7 +85,7 @@ function segmentChangesUpdaterFactory( // if at least one segment fetch successes, mark segments ready if (findIndex(shouldUpdateFlags, v => v !== -1) !== -1 || readyOnAlreadyExistentState) { readyOnAlreadyExistentState = false; - readiness.segments.emit('SDK_SEGMENTS_ARRIVED'); + readiness.segments.emit(SDK_SEGMENTS_ARRIVED); } // if at least one segment fetch fails, return false to indicate that there was some error (e.g., invalid json, HTTP error, etc) if (shouldUpdateFlags.indexOf(-1) !== -1) return false; diff --git a/src/sync/polling/syncTasks/splitsSyncTask.ts b/src/sync/polling/syncTasks/splitsSyncTask.ts index 79e58c42..f8df3913 100644 --- a/src/sync/polling/syncTasks/splitsSyncTask.ts +++ b/src/sync/polling/syncTasks/splitsSyncTask.ts @@ -12,9 +12,16 @@ import splitChangesFetcherFactory from '../fetchers/splitChangesFetcher'; import { IFetchSplitChanges } from '../../../services/types'; import thenable from '../../../utils/promise/thenable'; import { ISettings } from '../../../types'; +import { SDK_SPLITS_ARRIVED, SDK_SPLITS_CACHE_LOADED } from '../../../readiness/constants'; const log = logFactory('splitio-sync:split-changes'); -type ISplitChangesUpdater = () => Promise +type ISplitChangesUpdater = (noCache?: boolean) => Promise + +// Checks that all registered segments have been fetched (changeNumber !== -1). +// @TODO review together with Segments and MySegments storage APIs +function checkAllSegmentsExist(segmentsStorage: ISegmentsCacheSync) { + return segmentsStorage.getRegisteredSegments().every(segmentName => segmentsStorage.getChangeNumber(segmentName) !== -1); +} /** * Collect segments from a raw split definition. @@ -79,8 +86,8 @@ export function splitChangesUpdaterFactory( splitsCache: ISplitsCacheSync, segmentsCache: ISegmentsCacheSync, splitsEventEmitter: ISplitsEventEmitter, - requestTimeoutBeforeReady?: number, - retriesOnFailureBeforeReady?: number + requestTimeoutBeforeReady: number, + retriesOnFailureBeforeReady: number ): ISplitChangesUpdater { let startingUp = true; @@ -97,67 +104,71 @@ export function splitChangesUpdaterFactory( } /** - * @param {number} since current changeNumber at splitsCache - * @param {number} retry current number of retry attemps + * SplitChanges updater returns a promise that resolves with a `false` boolean value if it fails to fetch splits or synchronize them with the storage. + * + * @param {boolean | undefined} noCache true to revalidate data to fetch */ - function _splitChangesUpdater(since: number, retry = 0): Promise { - log.debug(`Spin up split update using since = ${since}`); - - const fetcherPromise = splitChangesFetcher(since, _promiseDecorator) - .then((splitChanges: ISplitChangesResponse) => { - startingUp = false; - - const mutation = computeSplitsMutation(splitChanges.splits); - - log.debug(`New splits ${mutation.added.length}`); - log.debug(`Removed splits ${mutation.removed.length}`); - log.debug(`Segment names collected ${mutation.segments.length}`); - - // Write into storage - // @TODO if allowing custom storages, wrap errors as SplitErrors to distinguish from user callback errors - return Promise.all([ - // calling first `setChangenumber` method, to perform cache flush if split filter queryString changed - splitsCache.setChangeNumber(splitChanges.till), - splitsCache.addSplits(mutation.added), - splitsCache.removeSplits(mutation.removed), - segmentsCache.registerSegments(mutation.segments) - ]).then(() => { - if (since !== splitChanges.till || readyOnAlreadyExistentState) { - readyOnAlreadyExistentState = false; - splitsEventEmitter.emit('SDK_SPLITS_ARRIVED'); + return function splitChangesUpdater(noCache?: boolean) { + + /** + * @param {number} since current changeNumber at splitsCache + * @param {number} retry current number of retry attemps + */ + function _splitChangesUpdater(since: number, retry = 0): Promise { + log.debug(`Spin up split update using since = ${since}`); + + const fetcherPromise = splitChangesFetcher(since, noCache, _promiseDecorator) + .then((splitChanges: ISplitChangesResponse) => { + startingUp = false; + + const mutation = computeSplitsMutation(splitChanges.splits); + + log.debug(`New splits ${mutation.added.length}`); + log.debug(`Removed splits ${mutation.removed.length}`); + log.debug(`Segment names collected ${mutation.segments.length}`); + + // Write into storage + // @TODO if allowing custom storages, wrap errors as SplitErrors to distinguish from user callback errors + return Promise.all([ + // calling first `setChangenumber` method, to perform cache flush if split filter queryString changed + splitsCache.setChangeNumber(splitChanges.till), + splitsCache.addSplits(mutation.added), + splitsCache.removeSplits(mutation.removed), + segmentsCache.registerSegments(mutation.segments) + ]).then(() => { + // For server-side SDK, we must check that all registered segments have been fetched + if (readyOnAlreadyExistentState || (since !== splitChanges.till && checkAllSegmentsExist(segmentsCache))) { + readyOnAlreadyExistentState = false; + splitsEventEmitter.emit(SDK_SPLITS_ARRIVED); + } + return true; + }); + }) + .catch(error => { + // handle user callback errors + if (!(error instanceof SplitError)) { + setTimeout(() => { throw error; }, 0); + startingUp = false; // Stop retrying. } - return true; + + log.warn(`Error while doing fetch of Splits. ${error}`); + + if (startingUp && retriesOnFailureBeforeReady > retry) { + retry += 1; + log.info(`Retrying download of splits #${retry}. Reason: ${error}`); + return _splitChangesUpdater(since, retry); + } else { + startingUp = false; + } + return false; }); - }) - .catch(error => { - // handle user callback errors - if (!(error instanceof SplitError)) { - setTimeout(() => { throw error; }, 0); - startingUp = false; // Stop retrying. - } - - log.warn(`Error while doing fetch of Splits. ${error}`); - - if (startingUp && retriesOnFailureBeforeReady && retriesOnFailureBeforeReady > retry) { - retry += 1; - log.info(`Retrying download of splits #${retry}. Reason: ${error}`); - return _splitChangesUpdater(since, retry); - } else { - startingUp = false; - } - return false; - }); - // After triggering the requests, if we have cached splits information let's notify that. - if (startingUp && splitsCache.checkCache()) splitsEventEmitter.emit('SDK_SPLITS_CACHE_LOADED'); + // After triggering the requests, if we have cached splits information let's notify that. + if (startingUp && splitsCache.checkCache()) splitsEventEmitter.emit(SDK_SPLITS_CACHE_LOADED); - return fetcherPromise; - } + return fetcherPromise; + } - /** - * SplitChanges updater returns a promise that resolves with a `false` boolean value if it fails to fetch splits or synchronize them with the storage. - */ - return function splitChangesUpdater() { const since = splitsCache.getChangeNumber(); // Adding an extra promise to keep the fetch call asynchronous const sincePromise: Promise = thenable(since) ? since : Promise.resolve(since);