import SplitIO from '../../types/splitio'; import { MaybeThenable, ISplit, IRBSegment, IMySegmentsResponse, IMembershipsResponse, ISegmentChangesResponse, ISplitChangesResponse } 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'; /** * Internal interface based on a subset of the Web Storage API interface * (https://developer.mozilla.org/en-US/docs/Web/API/Storage) used by the SDK */ export interface StorageAdapter { // Methods to support async storages load?: () => Promise; save?: () => Promise; whenSaved?: () => Promise; // Methods based on https://developer.mozilla.org/en-US/docs/Web/API/Storage readonly length: number; key(index: number): string | null; getItem(key: string): string | null; removeItem(key: string): void; setItem(key: string, value: string): void; } /** * 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 /** * 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 /** * 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 /** * 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 /** * 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 /** * 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 /** * 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 /** 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 /** * 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 /** * 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 /** 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 /** * 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 /** * 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 /** * 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 /** 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 /** * 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 } /** Splits cache */ export interface ISplitsCacheBase { update(toAdd: ISplit[], toRemove: ISplit[], changeNumber: number): MaybeThenable, getSplit(name: string): MaybeThenable, getSplits(names: string[]): MaybeThenable>, // `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, getAll(): MaybeThenable, getSplitNames(): MaybeThenable, // should never reject or throw an exception. Instead return true by default, asssuming the TT might exist. trafficTypeExists(trafficType: string): MaybeThenable, // 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, clear(): MaybeThenable, killLocally(name: string, defaultTreatment: string, changeNumber: number): MaybeThenable, getNamesByFlagSets(flagSets: string[]): MaybeThenable[]> } export interface ISplitsCacheSync extends ISplitsCacheBase { update(toAdd: ISplit[], toRemove: ISplit[], changeNumber: number): boolean, getSplit(name: string): ISplit | null, getSplits(names: string[]): Record, 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[] } export interface ISplitsCacheAsync extends ISplitsCacheBase { update(toAdd: ISplit[], toRemove: ISplit[], changeNumber: number): Promise, getSplit(name: string): Promise, getSplits(names: string[]): Promise>, getChangeNumber(): Promise, getAll(): Promise, getSplitNames(): Promise, trafficTypeExists(trafficType: string): Promise, usesSegments(): Promise, clear(): Promise, killLocally(name: string, defaultTreatment: string, changeNumber: number): Promise, getNamesByFlagSets(flagSets: string[]): Promise[]> } /** Rule-Based Segments cache */ export interface IRBSegmentsCacheBase { update(toAdd: IRBSegment[], toRemove: IRBSegment[], changeNumber: number): MaybeThenable, get(name: string): MaybeThenable, getChangeNumber(): MaybeThenable, clear(): MaybeThenable, contains(names: Set): MaybeThenable, } export interface IRBSegmentsCacheSync extends IRBSegmentsCacheBase { update(toAdd: IRBSegment[], toRemove: IRBSegment[], changeNumber: number): boolean, get(name: string): IRBSegment | null, getChangeNumber(): number, getAll(): IRBSegment[], clear(): void, contains(names: Set): 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, get(name: string): Promise, getChangeNumber(): Promise, clear(): Promise, contains(names: Set): Promise, } /** Segments cache */ export interface ISegmentsCacheBase { isInSegment(name: string, key?: string): MaybeThenable // different signature on Server and Client-Side registerSegments(names: string[]): MaybeThenable // only for Server-Side getRegisteredSegments(): MaybeThenable // only for Server-Side getChangeNumber(name: string): MaybeThenable // only for Server-Side update(name: string, addedKeys: string[], removedKeys: string[], changeNumber: number): MaybeThenable // only for Server-Side clear(): MaybeThenable } // 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 registerSegments(names: string[]): Promise getRegisteredSegments(): Promise getChangeNumber(name: string): Promise update(name: string, addedKeys: string[], removedKeys: string[], changeNumber: number): Promise clear(): Promise } /** 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 } export interface IEventsCacheBase { // Used by events tracker to push events into the storage. track(data: SplitIO.EventData, size?: number): MaybeThenable } 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 { name: string, // @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 { track(data: SplitIO.ImpressionDTO[]): void /* Registers callback for full queue */ setOnFullQueueCb(cb: () => void): void } export interface IEventsCacheSync extends IEventsCacheBase, IRecorderCacheSync { 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> { } export interface IUniqueKeysCacheSync extends IUniqueKeysCacheBase, IRecorderCacheSync { 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 { /* returns the number of stored items */ count(): Promise /* removes the given number of items from the store. If not provided, it deletes all items */ drop(count?: number): Promise /* pops the given number of items from the store */ popNWithMetadata(count: number): Promise } export interface IImpressionsCacheAsync extends IImpressionsCacheBase, IRecorderCacheAsync { // 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 } export interface IEventsCacheAsync extends IEventsCacheBase, IRecorderCacheAsync { // 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 } /** * 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; popTags(): Array | 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 { } /** * Telemetry storage interface for consumer mode. * Methods are async because data is stored in Redis or a pluggable storage. */ export interface ITelemetryEvaluationConsumerAsync { popLatencies(): Promise; popExceptions(): Promise; popConfigs(): Promise; } export interface ITelemetryEvaluationProducerAsync { recordLatency(method: Method, latencyMs: number): Promise; recordException(method: Method): Promise; recordConfig(): Promise; } // 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, shared?: (matchingKey: string, onReadyCb?: (error?: any) => void) => this save?: () => void | Promise, } export interface IStorageSync extends IStorageBase< ISplitsCacheSync, IRBSegmentsCacheSync, ISegmentsCacheSync, IImpressionsCacheSync, IImpressionCountsCacheSync, IEventsCacheSync, ITelemetryCacheSync, IUniqueKeysCacheSync > { // Defined in client-side validateCache?: () => Promise, largeSegments?: ISegmentsCacheSync, } export interface IStorageAsync extends IStorageBase< ISplitsCacheAsync, IRBSegmentsCacheAsync, ISegmentsCacheAsync, IImpressionsCacheAsync | IImpressionsCacheSync, IImpressionCountsCacheBase, IEventsCacheAsync | IEventsCacheSync, ITelemetryCacheAsync | ITelemetryCacheSync, IUniqueKeysCacheBase > { } /** StorageFactory */ 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, /** * For emitting SDK_READY_FROM_CACHE event in consumer mode with Redis to allow immediate evaluations */ 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 } export type RolloutPlan = { /** * Feature flags and rule-based segments. */ splitChanges: ISplitChangesResponse; /** * Optional map of matching keys to their memberships. */ memberships?: { [matchingKey: string]: IMembershipsResponse; }; /** * Optional list of standard segments. * This property is ignored if `memberships` is provided. */ segmentChanges?: ISegmentChangesResponse[]; };