@splitsoftware/splitio-commons
Version:
Split JavaScript SDK common components
521 lines (458 loc) • 22.9 kB
text/typescript
import SplitIO from '../../types/splitio';
import { MaybeThenable, ISplit, IRBSegment, IMySegmentsResponse } from '../dtos/types';
import { MySegmentsData } from '../sync/polling/types';
import { EventDataType, HttpErrors, HttpLatencies, ImpressionDataType, LastSync, Method, MethodExceptions, MethodLatencies, MultiMethodExceptions, MultiMethodLatencies, MultiConfigs, OperationType, StoredEventWithMetadata, StoredImpressionWithMetadata, StreamingEvent, UniqueKeysPayloadCs, UniqueKeysPayloadSs, TelemetryUsageStatsPayload, UpdatesFromSSEEnum } from '../sync/submitters/types';
import { ISettings } from '../types';
/**
* Interface of a pluggable storage wrapper.
*/
export interface IPluggableStorageWrapper {
/** Key-Value operations */
/**
* Get the value of given `key`.
*
* @param key - Item to retrieve
* @returns A promise that resolves with the element value associated with the specified `key`,
* or null if the key does not exist. The promise rejects if the operation fails.
*/
get: (key: string) => Promise<string | null>
/**
* Add or update an item with a specified `key` and `value`.
*
* @param key - Item to update
* @param value - Value to set
* @returns A promise that resolves if the operation success, whether the key was added or updated.
* The promise rejects if the operation fails.
*/
set: (key: string, value: string) => Promise<boolean | void>
/**
* Add or update an item with a specified `key` and `value`.
*
* @param key - Item to update
* @param value - Value to set
* @returns A promise that resolves with the previous value associated to the given `key`, or null if not set.
* The promise rejects if the operation fails.
*/
getAndSet: (key: string, value: string) => Promise<string | null>
/**
* Removes the specified item by `key`.
*
* @param key - Item to delete
* @returns A promise that resolves if the operation success, whether the key existed and was removed (resolves with true) or it didn't exist (resolves with false).
* The promise rejects if the operation fails, for example, if there is a connection error.
*/
del: (key: string) => Promise<boolean>
/**
* Returns all keys matching the given prefix.
*
* @param prefix - String prefix to match
* @returns A promise that resolves with the list of keys that match the given `prefix`.
* The promise rejects if the operation fails.
*/
getKeysByPrefix: (prefix: string) => Promise<string[]>
/**
* Returns the values of all given `keys`.
*
* @param keys - List of keys to retrieve
* @returns A promise that resolves with the list of items associated with the specified list of `keys`.
* For every key that does not hold a string value or does not exist, null is returned. The promise rejects if the operation fails.
*/
getMany: (keys: string[]) => Promise<(string | null)[]>
/** Integer operations */
/**
* Increments the number stored at `key` by `increment`, or set it to `increment` if the value doesn't exist.
*
* @param key - Key to increment
* @param increment - Value to increment by. Defaults to 1.
* @returns A promise that resolves with the value of key after the increment. The promise rejects if the operation fails,
* for example, if there is a connection error or the key contains a string that can not be represented as integer.
*/
incr: (key: string, increment?: number) => Promise<number>
/**
* Decrements the number stored at `key` by `decrement`, or set it to minus `decrement` if the value doesn't exist.
*
* @param key - Key to decrement
* @param decrement - Value to decrement by. Defaults to 1.
* @returns A promise that resolves with the value of key after the decrement. The promise rejects if the operation fails,
* for example, if there is a connection error or the key contains a string that can not be represented as integer.
*/
decr: (key: string, decrement?: number) => Promise<number>
/** Queue operations */
/**
* Inserts given items at the tail of `key` list. If `key` does not exist, an empty list is created before pushing the items.
*
* @param key - List key
* @param items - List of items to push
* @returns A promise that resolves if the operation success.
* The promise rejects if the operation fails, for example, if there is a connection error or the key holds a value that is not a list.
*/
pushItems: (key: string, items: string[]) => Promise<void>
/**
* Removes and returns the first `count` items from a list. If `key` does not exist, an empty list is items is returned.
*
* @param key - List key
* @param count - Number of items to pop
* @returns A promise that resolves with the list of removed items from the list, or an empty array when key does not exist.
* The promise rejects if the operation fails, for example, if there is a connection error or the key holds a value that is not a list.
*/
popItems: (key: string, count: number) => Promise<string[]>
/**
* Returns the count of items in a list, or 0 if `key` does not exist.
*
* @param key - List key
* @returns A promise that resolves with the number of items at the `key` list, or 0 when `key` does not exist.
* The promise rejects if the operation fails, for example, if there is a connection error or the key holds a value that is not a list.
*/
getItemsCount: (key: string) => Promise<number>
/** Set operations */
/**
* Returns if item is a member of a set.
*
* @param key - Set key
* @param item - Item value
* @returns A promise that resolves with true boolean value if `item` is a member of the set stored at `key`,
* or false if it is not a member or `key` set does not exist. The promise rejects if the operation fails, for example,
* if there is a connection error or the key holds a value that is not a set.
*/
itemContains: (key: string, item: string) => Promise<boolean>
/**
* Add the specified `items` to the set stored at `key`. Those items that are already part of the set are ignored.
* If key does not exist, an empty set is created before adding the items.
*
* @param key - Set key
* @param items - Items to add
* @returns A promise that resolves if the operation success.
* The promise rejects if the operation fails, for example, if there is a connection error or the key holds a value that is not a set.
*/
addItems: (key: string, items: string[]) => Promise<boolean | void>
/**
* Remove the specified `items` from the set stored at `key`. Those items that are not part of the set are ignored.
*
* @param key - Set key
* @param items - Items to remove
* @returns A promise that resolves if the operation success. If key does not exist, the promise also resolves.
* The promise rejects if the operation fails, for example, if there is a connection error or the key holds a value that is not a set.
*/
removeItems: (key: string, items: string[]) => Promise<boolean | void>
/**
* Returns all the items of the `key` set.
*
* @param key - Set key
* @returns A promise that resolves with the list of items. If key does not exist, the result is an empty list.
* The promise rejects if the operation fails, for example, if there is a connection error or the key holds a value that is not a set.
*/
getItems: (key: string) => Promise<string[]>
/** Control operations */
/**
* Connects to the underlying storage.
* It is meant for storages that requires to be connected to some database or server. Otherwise it can just return a resolved promise.
* Note: will be called once on SplitFactory instantiation and once per each shared client instantiation.
*
* @returns A promise that resolves when the wrapper successfully connect to the underlying storage.
* The promise rejects with the corresponding error if the wrapper fails to connect.
*/
connect: () => Promise<void>
/**
* Disconnects from the underlying storage.
* It is meant for storages that requires to be closed, in order to release resources. Otherwise it can just return a resolved promise.
* Note: will be called once on SplitFactory main client destroy.
*
* @returns A promise that resolves when the operation ends.
* The promise never rejects.
*/
disconnect: () => Promise<void>
}
/** Splits cache */
export interface ISplitsCacheBase {
update(toAdd: ISplit[], toRemove: ISplit[], changeNumber: number): MaybeThenable<boolean>,
getSplit(name: string): MaybeThenable<ISplit | null>,
getSplits(names: string[]): MaybeThenable<Record<string, ISplit | null>>, // `fetchMany` in spec
// should never reject or throw an exception. Instead return -1 by default, assuming no splits are present in the storage.
getChangeNumber(): MaybeThenable<number>,
getAll(): MaybeThenable<ISplit[]>,
getSplitNames(): MaybeThenable<string[]>,
// should never reject or throw an exception. Instead return true by default, asssuming the TT might exist.
trafficTypeExists(trafficType: string): MaybeThenable<boolean>,
// only for Client-Side. Returns true if the storage is not synchronized yet (getChangeNumber() === -1) or contains a FF using segments or large segments
usesSegments(): MaybeThenable<boolean>,
clear(): MaybeThenable<boolean | void>,
killLocally(name: string, defaultTreatment: string, changeNumber: number): MaybeThenable<boolean>,
getNamesByFlagSets(flagSets: string[]): MaybeThenable<Set<string>[]>
}
export interface ISplitsCacheSync extends ISplitsCacheBase {
update(toAdd: ISplit[], toRemove: ISplit[], changeNumber: number): boolean,
getSplit(name: string): ISplit | null,
getSplits(names: string[]): Record<string, ISplit | null>,
getChangeNumber(): number,
getAll(): ISplit[],
getSplitNames(): string[],
trafficTypeExists(trafficType: string): boolean,
usesSegments(): boolean,
clear(): void,
killLocally(name: string, defaultTreatment: string, changeNumber: number): boolean,
getNamesByFlagSets(flagSets: string[]): Set<string>[]
}
export interface ISplitsCacheAsync extends ISplitsCacheBase {
update(toAdd: ISplit[], toRemove: ISplit[], changeNumber: number): Promise<boolean>,
getSplit(name: string): Promise<ISplit | null>,
getSplits(names: string[]): Promise<Record<string, ISplit | null>>,
getChangeNumber(): Promise<number>,
getAll(): Promise<ISplit[]>,
getSplitNames(): Promise<string[]>,
trafficTypeExists(trafficType: string): Promise<boolean>,
usesSegments(): Promise<boolean>,
clear(): Promise<boolean | void>,
killLocally(name: string, defaultTreatment: string, changeNumber: number): Promise<boolean>,
getNamesByFlagSets(flagSets: string[]): Promise<Set<string>[]>
}
/** Rule-Based Segments cache */
export interface IRBSegmentsCacheBase {
update(toAdd: IRBSegment[], toRemove: IRBSegment[], changeNumber: number): MaybeThenable<boolean>,
get(name: string): MaybeThenable<IRBSegment | null>,
getChangeNumber(): MaybeThenable<number>,
clear(): MaybeThenable<boolean | void>,
contains(names: Set<string>): MaybeThenable<boolean>,
}
export interface IRBSegmentsCacheSync extends IRBSegmentsCacheBase {
update(toAdd: IRBSegment[], toRemove: IRBSegment[], changeNumber: number): boolean,
get(name: string): IRBSegment | null,
getChangeNumber(): number,
clear(): void,
contains(names: Set<string>): boolean,
// Used only for smart pausing in client-side standalone. Returns true if the storage contains a RBSegment using segments or large segments matchers
usesSegments(): boolean,
}
export interface IRBSegmentsCacheAsync extends IRBSegmentsCacheBase {
update(toAdd: IRBSegment[], toRemove: IRBSegment[], changeNumber: number): Promise<boolean>,
get(name: string): Promise<IRBSegment | null>,
getChangeNumber(): Promise<number>,
clear(): Promise<boolean | void>,
contains(names: Set<string>): Promise<boolean>,
}
/** Segments cache */
export interface ISegmentsCacheBase {
isInSegment(name: string, key?: string): MaybeThenable<boolean> // different signature on Server and Client-Side
registerSegments(names: string[]): MaybeThenable<boolean | void> // only for Server-Side
getRegisteredSegments(): MaybeThenable<string[]> // only for Server-Side
getChangeNumber(name: string): MaybeThenable<number | undefined> // only for Server-Side
update(name: string, addedKeys: string[], removedKeys: string[], changeNumber: number): MaybeThenable<boolean> // only for Server-Side
clear(): MaybeThenable<boolean | void>
}
// Same API for both variants: SegmentsCache and MySegmentsCache (client-side API)
export interface ISegmentsCacheSync extends ISegmentsCacheBase {
isInSegment(name: string, key?: string): boolean
registerSegments(names: string[]): boolean
getRegisteredSegments(): string[]
getKeysCount(): number // only used for telemetry
getChangeNumber(name?: string): number | undefined
update(name: string, addedKeys: string[], removedKeys: string[], changeNumber: number): boolean // only for Server-Side
resetSegments(segmentsData: MySegmentsData | IMySegmentsResponse): boolean // only for Sync Client-Side
clear(): void
}
export interface ISegmentsCacheAsync extends ISegmentsCacheBase {
isInSegment(name: string, key: string): Promise<boolean>
registerSegments(names: string[]): Promise<boolean | void>
getRegisteredSegments(): Promise<string[]>
getChangeNumber(name: string): Promise<number | undefined>
update(name: string, addedKeys: string[], removedKeys: string[], changeNumber: number): Promise<boolean>
clear(): Promise<boolean | void>
}
/** Recorder storages (impressions, events and telemetry) */
export interface IImpressionsCacheBase {
// Used by impressions tracker, in DEBUG and OPTIMIZED impression modes, to push impressions into the storage.
track(data: SplitIO.ImpressionDTO[]): MaybeThenable<void>
}
export interface IEventsCacheBase {
// Used by events tracker to push events into the storage.
track(data: SplitIO.EventData, size?: number): MaybeThenable<boolean>
}
export interface IImpressionCountsCacheBase {
// Used by impressions tracker, in OPTIMIZED and NONE impression modes, to count impressions.
track(featureName: string, timeFrame: number, amount: number): void
}
export interface IUniqueKeysCacheBase {
// Used by impressions tracker, in NONE impression mode, to track unique keys.
track(key: string, value: string): void
}
/** Impressions and events cache for standalone and partial consumer modes (sync methods) */
// API methods for sync recorder storages, used by submitters in standalone mode to pop data and post it to Split BE.
export interface IRecorderCacheSync<T> {
// @TODO names are inconsistent with spec
/* Checks if cache is empty. Returns true if the cache was just created or cleared */
isEmpty(): boolean
/* Clears cache data */
clear(): void
/* Pops cache data */
pop(toMerge?: T): T
}
export interface IImpressionsCacheSync extends IImpressionsCacheBase, IRecorderCacheSync<SplitIO.ImpressionDTO[]> {
track(data: SplitIO.ImpressionDTO[]): void
/* Registers callback for full queue */
setOnFullQueueCb(cb: () => void): void
}
export interface IEventsCacheSync extends IEventsCacheBase, IRecorderCacheSync<SplitIO.EventData[]> {
track(data: SplitIO.EventData, size?: number): boolean
/* Registers callback for full queue */
setOnFullQueueCb(cb: () => void): void
}
/* Named `ImpressionsCounter` in spec */
export interface IImpressionCountsCacheSync extends IImpressionCountsCacheBase, IRecorderCacheSync<Record<string, number>> { }
export interface IUniqueKeysCacheSync extends IUniqueKeysCacheBase, IRecorderCacheSync<UniqueKeysPayloadSs | UniqueKeysPayloadCs> {
setOnFullQueueCb(cb: () => void): void,
}
/** Impressions and events cache for consumer and producer modes (async methods) */
// API methods for async recorder storages, used by submitters in producer mode (synchronizer) to pop data and post it to Split BE.
export interface IRecorderCacheAsync<T> {
/* returns the number of stored items */
count(): Promise<number>
/* removes the given number of items from the store. If not provided, it deletes all items */
drop(count?: number): Promise<void>
/* pops the given number of items from the store */
popNWithMetadata(count: number): Promise<T>
}
export interface IImpressionsCacheAsync extends IImpressionsCacheBase, IRecorderCacheAsync<StoredImpressionWithMetadata[]> {
// Consumer API method, used by impressions tracker (in standalone and consumer modes) to push data into.
// The result promise can reject.
track(data: SplitIO.ImpressionDTO[]): Promise<void>
}
export interface IEventsCacheAsync extends IEventsCacheBase, IRecorderCacheAsync<StoredEventWithMetadata[]> {
// Consumer API method, used by events tracker (in standalone and consumer modes) to push data into.
// The result promise cannot reject.
track(data: SplitIO.EventData, size?: number): Promise<boolean>
}
/**
* Telemetry storage interface for standalone and partial consumer modes.
* Methods are sync because data is stored in memory.
*/
export interface ITelemetryInitConsumerSync {
getTimeUntilReady(): number | undefined;
getTimeUntilReadyFromCache(): number | undefined;
getNonReadyUsage(): number;
// 'active factories' and 'redundant factories' are not tracked in the storage. They are derived from `usedKeysMap`
}
export interface ITelemetryRuntimeConsumerSync {
getImpressionStats(type: ImpressionDataType): number;
getEventStats(type: EventDataType): number;
getLastSynchronization(): LastSync;
popHttpErrors(): HttpErrors;
popHttpLatencies(): HttpLatencies;
popAuthRejections(): number;
popTokenRefreshes(): number;
popStreamingEvents(): Array<StreamingEvent>;
popTags(): Array<string> | undefined;
getSessionLength(): number | undefined;
}
export interface ITelemetryEvaluationConsumerSync {
popExceptions(): MethodExceptions;
popLatencies(): MethodLatencies;
}
export interface ITelemetryStorageConsumerSync extends ITelemetryInitConsumerSync, ITelemetryRuntimeConsumerSync, ITelemetryEvaluationConsumerSync { }
export interface ITelemetryInitProducerSync {
recordTimeUntilReady(ms: number): void;
recordTimeUntilReadyFromCache(ms: number): void;
recordNonReadyUsage(): void;
// 'active factories' and 'redundant factories' are not tracked in the storage. They are derived from `usedKeysMap`
}
export interface ITelemetryRuntimeProducerSync {
addTag(tag: string): void;
recordImpressionStats(type: ImpressionDataType, count: number): void;
recordEventStats(type: EventDataType, count: number): void;
recordSuccessfulSync(resource: OperationType, timeMs: number): void;
recordHttpError(resource: OperationType, status: number): void;
recordHttpLatency(resource: OperationType, latencyMs: number): void;
recordAuthRejections(): void;
recordTokenRefreshes(): void;
recordStreamingEvents(streamingEvent: StreamingEvent): void;
recordSessionLength(ms: number): void;
recordUpdatesFromSSE(type: UpdatesFromSSEEnum): void
}
export interface ITelemetryEvaluationProducerSync {
recordLatency(method: Method, latencyMs: number): void;
recordException(method: Method): void;
}
export interface ITelemetryStorageProducerSync extends ITelemetryInitProducerSync, ITelemetryRuntimeProducerSync, ITelemetryEvaluationProducerSync { }
export interface ITelemetryCacheSync extends ITelemetryStorageConsumerSync, ITelemetryStorageProducerSync, IRecorderCacheSync<TelemetryUsageStatsPayload> { }
/**
* Telemetry storage interface for consumer mode.
* Methods are async because data is stored in Redis or a pluggable storage.
*/
export interface ITelemetryEvaluationConsumerAsync {
popLatencies(): Promise<MultiMethodLatencies>;
popExceptions(): Promise<MultiMethodExceptions>;
popConfigs(): Promise<MultiConfigs>;
}
export interface ITelemetryEvaluationProducerAsync {
recordLatency(method: Method, latencyMs: number): Promise<any>;
recordException(method: Method): Promise<any>;
recordConfig(): Promise<any>;
}
// ATM it only implements the producer API, used by the SDK in consumer mode.
export interface ITelemetryCacheAsync extends ITelemetryEvaluationProducerAsync, ITelemetryEvaluationConsumerAsync { }
/**
* Storages
*/
export interface IStorageBase<
TSplitsCache extends ISplitsCacheBase = ISplitsCacheBase,
TRBSegmentsCache extends IRBSegmentsCacheBase = IRBSegmentsCacheBase,
TSegmentsCache extends ISegmentsCacheBase = ISegmentsCacheBase,
TImpressionsCache extends IImpressionsCacheBase = IImpressionsCacheBase,
TImpressionsCountCache extends IImpressionCountsCacheBase = IImpressionCountsCacheBase,
TEventsCache extends IEventsCacheBase = IEventsCacheBase,
TTelemetryCache extends ITelemetryCacheSync | ITelemetryCacheAsync = ITelemetryCacheSync | ITelemetryCacheAsync,
TUniqueKeysCache extends IUniqueKeysCacheBase = IUniqueKeysCacheBase
> {
splits: TSplitsCache,
rbSegments: TRBSegmentsCache,
segments: TSegmentsCache,
largeSegments?: TSegmentsCache,
impressions: TImpressionsCache,
impressionCounts: TImpressionsCountCache,
events: TEventsCache,
telemetry?: TTelemetryCache,
uniqueKeys: TUniqueKeysCache,
destroy(): void | Promise<void>,
shared?: (matchingKey: string, onReadyCb: (error?: any) => void) => this
}
export interface IStorageSync extends IStorageBase<
ISplitsCacheSync,
IRBSegmentsCacheSync,
ISegmentsCacheSync,
IImpressionsCacheSync,
IImpressionCountsCacheSync,
IEventsCacheSync,
ITelemetryCacheSync,
IUniqueKeysCacheSync
> {
// Defined in client-side
validateCache?: () => boolean, // @TODO support async
largeSegments?: ISegmentsCacheSync,
}
export interface IStorageAsync extends IStorageBase<
ISplitsCacheAsync,
IRBSegmentsCacheAsync,
ISegmentsCacheAsync,
IImpressionsCacheAsync | IImpressionsCacheSync,
IImpressionCountsCacheBase,
IEventsCacheAsync | IEventsCacheSync,
ITelemetryCacheAsync | ITelemetryCacheSync,
IUniqueKeysCacheBase
> { }
/** StorageFactory */
export type DataLoader = (storage: IStorageSync, matchingKey: string) => void
export interface IStorageFactoryParams {
settings: ISettings,
/**
* Error-first callback invoked when the storage is ready to be used. An error means that the storage failed to connect and shouldn't be used.
* It is meant for emitting SDK_READY event in consumer mode, and waiting before using the storage in the synchronizer.
*/
onReadyCb: (error?: any) => void,
onReadyFromCacheCb: () => void,
}
export type IStorageSyncFactory = SplitIO.StorageSyncFactory & {
readonly type: SplitIO.StorageType,
(params: IStorageFactoryParams): IStorageSync
}
export type IStorageAsyncFactory = SplitIO.StorageAsyncFactory & {
readonly type: SplitIO.StorageType,
(params: IStorageFactoryParams): IStorageAsync
}