@splitsoftware/splitio-commons
Version:
Split JavaScript SDK common components
122 lines (103 loc) • 4.88 kB
text/typescript
import { ISettings } from '../../../types';
import { IDefinition, IRBSegment, IDefinitionChangesResponse } from '../../../dtos/types';
import { IResponse } from '../../../services/types';
import { FLAG_SPEC_VERSION } from '../../../utils/constants';
import { base } from '../../../utils/settingsValidation';
import { IDefinitionChangesFetcher } from './types';
import { LOG_PREFIX_SYNC } from '../../../logger/constants';
import { checkIfServerSide } from '../../../utils/key';
import { ISdkFactoryContextSync } from '../../../sdkFactory/types';
const PROXY_CHECK_INTERVAL_MILLIS_CS = 60 * 60 * 1000; // 1 hour in Client Side
const PROXY_CHECK_INTERVAL_MILLIS_SS = 24 * PROXY_CHECK_INTERVAL_MILLIS_CS; // 24 hours in Server Side
function sdkEndpointOverridden(settings: ISettings) {
return settings.urls.sdk !== base.urls.sdk;
}
export type ISplit = IDefinition;
/** JSON response of `/splitChanges` */
export interface ISplitChangesResponse {
ff?: {
t: number,
s?: number,
d?: ISplit[] | null,
},
rbs?: {
t: number,
s?: number,
d?: IRBSegment[] | null,
}
}
/** JSON response of `/splitChanges` for flag spec version 1.2 or below */
export interface ISplitChangesLegacyResponse {
till: number,
since?: number,
splits: ISplit[]
}
function isSplitChangesLegacyResponse(data: ISplitChangesResponse | ISplitChangesLegacyResponse): data is ISplitChangesLegacyResponse {
return (data as ISplitChangesLegacyResponse).splits != null;
}
function partitionByStatus<T extends { status?: string; name: string }>(items: T[], till: number, since?: number) {
const updated: T[] = [], removed: string[] = [];
items.forEach(item => item.status === 'ARCHIVED' ? removed.push(item.name) : updated.push(item));
return { updated, removed, till, since };
}
/**
* Factory of SplitChanges fetcher.
* SplitChanges fetcher is a wrapper around `splitChanges` API service that parses the response and handle errors.
*/
// @TODO breaking: drop support for Split Proxy below v5.10.0 and simplify the implementation
export function splitChangesFetcherFactory(params: ISdkFactoryContextSync): IDefinitionChangesFetcher {
const { serviceApi: { fetchSplitChanges }, settings, storage } = params;
const log = settings.log;
const PROXY_CHECK_INTERVAL_MILLIS = checkIfServerSide(settings) ? PROXY_CHECK_INTERVAL_MILLIS_SS : PROXY_CHECK_INTERVAL_MILLIS_CS;
let lastProxyCheckTimestamp: number | undefined;
function splitChangesFetcher(
since: number,
noCache?: boolean,
till?: number,
rbSince?: number,
// Optional decorator for `fetchSplitChanges` promise, such as timeout or time tracker
decorator?: (promise: Promise<IResponse>) => Promise<IResponse>
): Promise<IDefinitionChangesResponse> {
// Recheck proxy
if (lastProxyCheckTimestamp && (Date.now() - lastProxyCheckTimestamp) > PROXY_CHECK_INTERVAL_MILLIS) {
settings.sync.flagSpecVersion = FLAG_SPEC_VERSION;
}
let splitsPromise = fetchSplitChanges(since, noCache, till, settings.sync.flagSpecVersion === FLAG_SPEC_VERSION ? rbSince : undefined)
.catch((err) => {
// Handle proxy error with spec 1.3
if ((!err.statusCode || err.statusCode === 400) && sdkEndpointOverridden(settings) && settings.sync.flagSpecVersion === FLAG_SPEC_VERSION) {
log.error(LOG_PREFIX_SYNC + 'Proxy error detected. Retrying with spec 1.2. If you are using Split Proxy, please upgrade to latest version');
lastProxyCheckTimestamp = Date.now();
settings.sync.flagSpecVersion = '1.2'; // fallback to 1.2 spec
return fetchSplitChanges(since, noCache, till); // retry request without rbSince
}
throw err;
});
if (decorator) splitsPromise = decorator(splitsPromise);
return splitsPromise
.then(resp => resp.json())
.then((data: ISplitChangesResponse | ISplitChangesLegacyResponse) => {
// Using flag spec version 1.2 or below
if (isSplitChangesLegacyResponse(data)) {
return { d: partitionByStatus(data.splits || [], data.till, data.since) };
}
// Proxy recovery
if (lastProxyCheckTimestamp) {
log.info(LOG_PREFIX_SYNC + 'Proxy error recovered');
lastProxyCheckTimestamp = undefined;
return splitChangesFetcher(-1, undefined, undefined, -1)
.then((splitChangesResponse: IDefinitionChangesResponse) =>
Promise.all([storage.definitions.clear(), storage.rbSegments.clear()])
.then(() => splitChangesResponse)
);
}
const { ff, rbs } = data;
return {
d: ff?.d ? partitionByStatus(ff.d, ff.t, ff.s) : undefined,
rbs: rbs?.d ? partitionByStatus(rbs.d, rbs.t, rbs.s) : undefined,
};
});
}
splitChangesFetcher.type = 'feature flags' as const;
return splitChangesFetcher;
}