Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@splitsoftware/splitio-commons",
"version": "1.0.0",
"version": "1.0.1-rc.1",
"description": "Split Javascript SDK common components",
"main": "cjs/index.js",
"module": "esm/index.js",
Expand Down
6 changes: 3 additions & 3 deletions src/logger/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,9 +91,8 @@ export const WARN_INTEGRATION_INVALID = 218;
export const WARN_SPLITS_FILTER_IGNORED = 219;
export const WARN_SPLITS_FILTER_INVALID = 220;
export const WARN_SPLITS_FILTER_EMPTY = 221;
export const WARN_STORAGE_INVALID = 222;
export const WARN_API_KEY = 223;
export const STREAMING_PARSING_MY_SEGMENTS_UPDATE_V2 = 224;
export const WARN_API_KEY = 222;
export const STREAMING_PARSING_MY_SEGMENTS_UPDATE_V2 = 223;

export const ERROR_ENGINE_COMBINER_IFELSEIF = 300;
export const ERROR_LOGLEVEL_INVALID = 301;
Expand All @@ -119,6 +118,7 @@ export const ERROR_EMPTY_ARRAY = 320;
export const ERROR_INVALID_IMPRESSIONS_MODE = 321;
export const ERROR_HTTP = 322;
export const ERROR_LOCALHOST_MODULE_REQUIRED = 323;
export const ERROR_STORAGE_INVALID = 324;

// Log prefixes (a.k.a. tags or categories)
export const LOG_PREFIX_SETTINGS = 'settings';
Expand Down
3 changes: 2 additions & 1 deletion src/logger/messages/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,6 @@ export const codesError: [number, string][] = [
[c.ERROR_EMPTY_ARRAY, '%s: %s must be a non-empty array.'],
// initialization / settings validation
[c.ERROR_INVALID_IMPRESSIONS_MODE, c.LOG_PREFIX_SETTINGS + ': you passed an invalid "impressionsMode". It should be one of the following values: %s. Defaulting to "%s" mode.'],
[c.ERROR_LOCALHOST_MODULE_REQUIRED, c.LOG_PREFIX_SETTINGS + ': an invalid value was received for "sync.localhostMode" config. A valid entity should be provided for localhost mode.']
[c.ERROR_LOCALHOST_MODULE_REQUIRED, c.LOG_PREFIX_SETTINGS + ': an invalid value was received for "sync.localhostMode" config. A valid entity should be provided for localhost mode.'],
[c.ERROR_STORAGE_INVALID, c.LOG_PREFIX_SETTINGS+': The provided storage is invalid.%s Fallbacking into default MEMORY storage'],
];
1 change: 0 additions & 1 deletion src/logger/messages/warn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ export const codesWarn: [number, string][] = codesError.concat([
[c.WARN_SPLITS_FILTER_IGNORED, c.LOG_PREFIX_SETTINGS+': split filters have been configured but will have no effect if mode is not "%s", since synchronization is being deferred to an external tool.'],
[c.WARN_SPLITS_FILTER_INVALID, c.LOG_PREFIX_SETTINGS+': split filter at position %s is invalid. It must be an object with a valid filter type ("byName" or "byPrefix") and a list of "values".'],
[c.WARN_SPLITS_FILTER_EMPTY, c.LOG_PREFIX_SETTINGS+': splitFilters configuration must be a non-empty array of filter objects.'],
[c.WARN_STORAGE_INVALID, c.LOG_PREFIX_SETTINGS+': The provided storage is invalid. Fallbacking into default MEMORY storage'],
[c.WARN_API_KEY, c.LOG_PREFIX_SETTINGS+': You already have %s. We recommend keeping only one instance of the factory at all times (Singleton pattern) and reusing it throughout your application'],

[c.STREAMING_PARSING_MY_SEGMENTS_UPDATE_V2, c.LOG_PREFIX_SYNC_STREAMING + 'Fetching MySegments due to an error processing %s notification: %s'],
Expand Down
12 changes: 7 additions & 5 deletions src/sdkClient/__tests__/sdkClientMethod.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,23 @@ const errorMessage = 'Shared Client not supported by the storage mechanism. Crea
const paramMocks = [
// No SyncManager (i.e., Async SDK) and No signal listener
{
storage: { destroy: jest.fn() },
storage: { destroy: jest.fn(() => Promise.resolve()) },
syncManager: undefined,
sdkReadinessManager: { sdkStatus: jest.fn(), readinessManager: { destroy: jest.fn() } },
signalListener: undefined,
settings: { mode: CONSUMER_MODE, log: loggerMock }
settings: { mode: CONSUMER_MODE, log: loggerMock, core: { authorizationKey: 'api key '} }
},
// SyncManager (i.e., Sync SDK) and Signal listener
{
storage: { destroy: jest.fn() },
syncManager: { stop: jest.fn(), flush: jest.fn(() => Promise.resolve()) },
sdkReadinessManager: { sdkStatus: jest.fn(), readinessManager: { destroy: jest.fn() } },
signalListener: { stop: jest.fn() },
settings: { mode: STANDALONE_MODE, log: loggerMock }
settings: { mode: STANDALONE_MODE, log: loggerMock, core: { authorizationKey: 'api key '} }
}
];

test.each(paramMocks)('sdkClientMethodFactory', (params) => {
test.each(paramMocks)('sdkClientMethodFactory', (params, done: any) => {
// @ts-expect-error
const sdkClientMethod = sdkClientMethodFactory(params);

Expand All @@ -38,7 +38,7 @@ test.each(paramMocks)('sdkClientMethodFactory', (params) => {
// multiple calls should return the same instance
expect(sdkClientMethod()).toBe(client);

// `client.destroy` method should stop internal components (other client methods where validated in `client.spec.ts`)
// `client.destroy` method should stop internal components (other client methods are validated in `client.spec.ts`)
client.destroy().then(() => {
expect(params.sdkReadinessManager.readinessManager.destroy).toBeCalledTimes(1);
expect(params.storage.destroy).toBeCalledTimes(1);
Expand All @@ -48,6 +48,8 @@ test.each(paramMocks)('sdkClientMethodFactory', (params) => {
expect(params.syncManager.flush).toBeCalledTimes(1);
}
if (params.signalListener) expect(params.signalListener.stop).toBeCalledTimes(1);

done();
});

// calling the function with parameters should throw an error
Expand Down
6 changes: 3 additions & 3 deletions src/sdkClient/sdkClient.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import objectAssign from 'object-assign';
import { IStatusInterface, SplitIO } from '../types';
import { CONSUMER_MODE } from '../utils/constants';
import { CONSUMER_MODE, CONSUMER_PARTIAL_MODE } from '../utils/constants';
import { releaseApiKey } from '../utils/inputValidation/apiKey';
import clientFactory from './client';
import clientInputValidationDecorator from './clientInputValidation';
Expand All @@ -21,8 +21,8 @@ export function sdkClientFactory(params: ISdkClientFactoryParams): SplitIO.IClie
settings.log,
clientFactory(params),
sdkReadinessManager.readinessManager,
// @TODO isStorageSync could be extracted from the storage itself (e.g. `storage.isSync`) to simplify interfaces
settings.mode === CONSUMER_MODE ? false : true,
// storage is async if and only if mode is consumer or partial consumer
[CONSUMER_MODE, CONSUMER_PARTIAL_MODE].indexOf(settings.mode) === -1 ? true : false,
),

// Sdk destroy
Expand Down
5 changes: 5 additions & 0 deletions src/sdkClient/sdkClientMethodCS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,11 @@ export function sdkClientMethodCSFactory(params: ISdkClientFactoryParams): (key?
// Emit SDK_READY in consumer mode for shared clients
sharedSdkReadiness.readinessManager.segments.emit(SDK_SEGMENTS_ARRIVED);
});

// 3 possibilities:
// - Standalone mode: both syncManager and sharedSyncManager are defined
// - Consumer mode: both syncManager and sharedSyncManager are undefined
// - Consumer partial mode: syncManager is defined (only for submitters) but sharedSyncManager is undefined
// @ts-ignore
const sharedSyncManager = syncManager && sharedStorage && (syncManager as ISyncManagerCS).shared(matchingKey, sharedSdkReadiness.readinessManager, sharedStorage);

Expand Down
5 changes: 5 additions & 0 deletions src/sdkClient/sdkClientMethodCSWithTT.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,11 @@ export function sdkClientMethodCSFactory(params: ISdkClientFactoryParams): (key?
// Emit SDK_READY in consumer mode for shared clients
sharedSdkReadiness.readinessManager.segments.emit(SDK_SEGMENTS_ARRIVED);
});

// 3 possibilities:
// - Standalone mode: both syncManager and sharedSyncManager are defined
// - Consumer mode: both syncManager and sharedSyncManager are undefined
// - Consumer partial mode: syncManager is defined (only for submitters) but sharedSyncManager is undefined
// @ts-ignore
const sharedSyncManager = syncManager && sharedStorage && (syncManager as ISyncManagerCS).shared(matchingKey, sharedSdkReadiness.readinessManager, sharedStorage);

Expand Down
3 changes: 3 additions & 0 deletions src/sdkFactory/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ export function sdkFactory(params: ISdkFactoryParams): SplitIO.ICsSDK | SplitIO.
matchingKey: getMatching(settings.core.key),
splitFiltersValidation: settings.sync.__splitFiltersValidation,

// ATM, only used by CustomStorage
mode: settings.mode,

// Callback used to emit SDK_READY in consumer mode, where `syncManagerFactory` is undefined
// or only instantiates submitters, and therefore it is not able to emit readiness events.
onReadyCb: (error) => {
Expand Down
5 changes: 4 additions & 1 deletion src/services/splitApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ function userKeyToQueryParam(userKey: string) {
* @param settings validated settings object
* @param platform object containing environment-specific `getFetch` and `getOptions` dependencies
*/
export function splitApiFactory(settings: ISettings, platform: Pick<IPlatform, 'getFetch' | 'getOptions'>): ISplitApi {
export function splitApiFactory(
settings: Pick<ISettings, 'urls' | 'sync' | 'log' | 'version' | 'runtime' | 'core'>,
platform: Pick<IPlatform, 'getFetch' | 'getOptions'>
): ISplitApi {

const urls = settings.urls;
const filterQueryString = settings.sync.__splitFiltersValidation && settings.sync.__splitFiltersValidation.queryString;
Expand Down
9 changes: 8 additions & 1 deletion src/storages/__tests__/testUtils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { IStorageSync, IStorageAsync } from '../types';
import { IStorageSync, IStorageAsync, IImpressionsCacheSync, IEventsCacheSync } from '../types';

// Assert that instances created by storage factories have the expected interface
export function assertStorageInterface(storage: IStorageSync | IStorageAsync) {
Expand All @@ -12,6 +12,13 @@ export function assertStorageInterface(storage: IStorageSync | IStorageAsync) {
expect(!storage.impressionCounts || typeof storage.impressionCounts === 'object').toBeTruthy;
}

export function assertSyncRecorderCacheInterface(cache: IEventsCacheSync | IImpressionsCacheSync) {
expect(typeof cache.isEmpty).toBe('function');
expect(typeof cache.clear).toBe('function');
expect(typeof cache.state).toBe('function');
expect(typeof cache.track).toBe('function');
}

// Split mocks

export const splitWithUserTT = '{ "trafficTypeName": "user_tt", "conditions": [] }';
Expand Down
2 changes: 1 addition & 1 deletion src/storages/inRedis/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export function InRedisStorage(options: InRedisStorageOptions = {}): IStorageAsy

// subscription to Redis connect event in order to emit SDK_READY event on consumer mode
redisClient.on('connect', () => {
if (onReadyCb) onReadyCb();
onReadyCb();
});

return {
Expand Down
27 changes: 24 additions & 3 deletions src/storages/pluggable/__tests__/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ const prefix = 'some_prefix';

// Test target
import { PluggableStorage } from '../index';
import { assertStorageInterface } from '../../__tests__/testUtils';
import { assertStorageInterface, assertSyncRecorderCacheInterface } from '../../__tests__/testUtils';
import { CONSUMER_PARTIAL_MODE } from '../../../utils/constants';

describe('PLUGGABLE STORAGE', () => {

Expand Down Expand Up @@ -45,7 +46,7 @@ describe('PLUGGABLE STORAGE', () => {

await storage.destroy();
await sharedStorage.destroy();
expect(wrapperMock.close).toBeCalledTimes(1); // wrapper close method should be called once when storage is destroyed
expect(wrapperMock.disconnect).toBeCalledTimes(1); // wrapper disconnect method should be called once when storage is destroyed

expect(internalSdkParams.onReadyCb).toBeCalledTimes(1); // onReady callback should be called when the wrapper connect resolved with true
expect(sharedOnReadyCb).toBeCalledTimes(1);
Expand All @@ -64,10 +65,30 @@ describe('PLUGGABLE STORAGE', () => {
// Throws exception if the given object is not a valid wrapper, informing which methods are missing
const invalidWrapper = wrapperMockFactory();
invalidWrapper.connect = undefined;
invalidWrapper.close = 'invalid function';
invalidWrapper.disconnect = 'invalid function';
const errorNoValidWrapperInterface = 'The provided wrapper instance doesn’t follow the expected interface. Check our docs.';
expect(() => PluggableStorage({ wrapper: invalidWrapper })).toThrow(errorNoValidWrapperInterface);
expect(() => PluggableStorage({ wrapper: {} })).toThrow(errorNoValidWrapperInterface);
});

test('creates a storage instance for partial consumer mode (events and impressions cache in memory)', async () => {
const storageFactory = PluggableStorage({ prefix, wrapper: wrapperMock });
const storage = storageFactory({ ...internalSdkParams, mode: CONSUMER_PARTIAL_MODE, optimize: true });

assertStorageInterface(storage);
expect(wrapperMock.connect).toBeCalledTimes(1);

// Sync cache
assertSyncRecorderCacheInterface(storage.events);
assertSyncRecorderCacheInterface(storage.impressions);
assertSyncRecorderCacheInterface(storage.impressionCounts);

// But event track is async
const eventResult = storage.events.track('some data');
expect(typeof eventResult.then === 'function').toBeTruthy();
expect(await eventResult).toBe(true);

const impResult = storage.impressions.track(['some data']);
expect(impResult).toBe(undefined);
});
});
6 changes: 3 additions & 3 deletions src/storages/pluggable/__tests__/wrapper.mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,9 +103,9 @@ export function wrapperMockFactory() {
return Promise.reject('key is not a set');
}),

// always connects and close
// always connects and disconnects
connect: jest.fn(() => Promise.resolve()),
close: jest.fn(() => Promise.resolve()),
disconnect: jest.fn(() => Promise.resolve()),

mockClear() {
this._cache = {};
Expand All @@ -117,7 +117,7 @@ export function wrapperMockFactory() {
this.decr.mockClear();
this.getMany.mockClear();
this.connect.mockClear();
this.close.mockClear();
this.disconnect.mockClear();
this.getAndSet.mockClear();
this.pushItems.mockClear();
this.popItems.mockClear();
Expand Down
4 changes: 2 additions & 2 deletions src/storages/pluggable/__tests__/wrapperAdapter.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export const wrapperWithIssues = {
decr: 'invalid value',
getMany: throwsException,
connect: rejectedPromise,
close: invalidThenable,
disconnect: invalidThenable,
getAndSet: 'invalid value',
pushItems: rejectedPromise,
popItems: invalidThenable,
Expand All @@ -48,7 +48,7 @@ const VALID_METHOD_CALLS = {
'decr': ['some_key'],
'getMany': [['some_key_1', 'some_key_2']],
'connect': [],
'close': [],
'disconnect': [],
'getAndSet': ['some_key', 'some_value'],
'pushItems': ['some_key_list', ['item1', 'item2']],
'popItems': ['some_key_list'],
Expand Down
4 changes: 2 additions & 2 deletions src/storages/pluggable/inMemoryWrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,15 +112,15 @@ export function inMemoryWrapperFactory(connDelay?: number): ICustomStorageWrappe
return Promise.reject('key is not a set');
},

// always connects and close
// always connects and disconnects
connect() {
if (typeof _connDelay === 'number') {
return new Promise(res => setTimeout(res, _connDelay));
} else {
return Promise.resolve();
}
},
close() { return Promise.resolve(); },
disconnect() { return Promise.resolve(); },

// for testing
_setConnDelay(connDelay: number) {
Expand Down
31 changes: 23 additions & 8 deletions src/storages/pluggable/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ import { EventsCachePluggable } from './EventsCachePluggable';
import { wrapperAdapter, METHODS_TO_PROMISE_WRAP } from './wrapperAdapter';
import { isObject } from '../../utils/lang';
import { validatePrefix } from '../KeyBuilder';
import { STORAGE_CUSTOM } from '../../utils/constants';
import { CONSUMER_PARTIAL_MODE, STORAGE_CUSTOM } from '../../utils/constants';
import ImpressionsCacheInMemory from '../inMemory/ImpressionsCacheInMemory';
import EventsCacheInMemory from '../inMemory/EventsCacheInMemory';
import ImpressionCountsCacheInMemory from '../inMemory/ImpressionCountsCacheInMemory';

const NO_VALID_WRAPPER = 'Expecting custom storage `wrapper` in options, but no valid wrapper instance was provided.';
const NO_VALID_WRAPPER_INTERFACE = 'The provided wrapper instance doesn’t follow the expected interface. Check our docs.';
Expand Down Expand Up @@ -41,6 +44,16 @@ function wrapperConnect(wrapper: ICustomStorageWrapper, onReadyCb: (error?: any)
});
}

// Async return type in `client.track` method on consumer partial mode
// No need to promisify impressions cache
function promisifyEventsTrack(events: any) {
const origTrack = events.track;
events.track = function () {
return Promise.resolve(origTrack.apply(this, arguments));
};
return events;
}

/**
* Pluggable storage factory for consumer server-side & client-side SplitFactory.
*/
Expand All @@ -50,31 +63,33 @@ export function PluggableStorage(options: PluggableStorageOptions): IStorageAsyn

const prefix = validatePrefix(options.prefix);

function PluggableStorageFactory({ log, metadata, onReadyCb }: IStorageFactoryParams): IStorageAsync {
function PluggableStorageFactory({ log, metadata, onReadyCb, mode, eventsQueueSize, optimize }: IStorageFactoryParams): IStorageAsync {
const keys = new KeyBuilderSS(prefix, metadata);
const wrapper = wrapperAdapter(log, options.wrapper);
const isPartialConsumer = mode === CONSUMER_PARTIAL_MODE;

// emit SDK_READY event on main client
// Connects to wrapper and emits SDK_READY event on main client
wrapperConnect(wrapper, onReadyCb);

return {
splits: new SplitsCachePluggable(log, keys, wrapper),
segments: new SegmentsCachePluggable(log, keys, wrapper),
impressions: new ImpressionsCachePluggable(log, keys.buildImpressionsKey(), wrapper, metadata),
events: new EventsCachePluggable(log, keys.buildEventsKey(), wrapper, metadata),
impressions: isPartialConsumer ? new ImpressionsCacheInMemory() : new ImpressionsCachePluggable(log, keys.buildImpressionsKey(), wrapper, metadata),
impressionCounts: optimize ? new ImpressionCountsCacheInMemory() : undefined,
events: isPartialConsumer ? promisifyEventsTrack(new EventsCacheInMemory(eventsQueueSize)) : new EventsCachePluggable(log, keys.buildEventsKey(), wrapper, metadata),
// @TODO add telemetry cache when required

// Disconnect the underlying storage, to release its resources (such as open files, database connections, etc).
// Disconnect the underlying storage
destroy() {
return wrapper.close();
return wrapper.disconnect();
},

// emits SDK_READY event on shared clients and returns a reference to the storage
shared(_, onReadyCb) {
wrapperConnect(wrapper, onReadyCb);
return {
...this,
// no-op destroy, to close the wrapper only when the main client is destroyed
// no-op destroy, to disconnect the wrapper only when the main client is destroyed
destroy() { }
};
}
Expand Down
2 changes: 1 addition & 1 deletion src/storages/pluggable/wrapperAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export const METHODS_TO_PROMISE_WRAP: string[] = [
'removeItems',
'getItems',
'connect',
'close'
'disconnect'
];

/**
Expand Down
Loading