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 README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ Split has built and maintains SDKs for:
* iOS [Github](https://github.com/splitio/ios-client) [Docs](https://help.split.io/hc/en-us/articles/360020401491-iOS-SDK)
* Java [Github](https://github.com/splitio/java-client) [Docs](https://help.split.io/hc/en-us/articles/360020405151-Java-SDK)
* Javascript [Github](https://github.com/splitio/javascript-client) [Docs](https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK)
* Node [Github](https://github.com/splitio/javascript-client) [Docs](https://help.split.io/hc/en-us/articles/360020564931-Node-js-SDK)
* Javascript for Browser [Github](https://github.com/splitio/javascript-browser-client) [Docs](https://help.split.io/hc/en-us/articles/360058730852-Browser-SDK)
* Node [Github](https://github.com/splitio/javascript-client) [Docs](https://help.split.io/hc/en-us/articles/360020564931-Node-js-SDK)
* PHP [Github](https://github.com/splitio/php-client) [Docs](https://help.split.io/hc/en-us/articles/360020350372-PHP-SDK)
* Python [Github](https://github.com/splitio/python-client) [Docs](https://help.split.io/hc/en-us/articles/360020359652-Python-SDK)
* React [Github](https://github.com/splitio/react-client) [Docs](https://help.split.io/hc/en-us/articles/360038825091-React-SDK)
Expand Down
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
35 changes: 31 additions & 4 deletions src/listeners/__tests__/browser.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,8 @@ function triggerUnloadEvent() {

/* Mocks end */

test('Browser JS listener / Impressions optimized mode', () => {

test('Browser JS listener / consumer mode', () => {
// No SyncManager ==> consumer mode
const listener = new BrowserSignalListener(undefined, fullSettings, fakeStorageOptimized as IStorageSync, fakeSplitApi);

listener.start();
Expand All @@ -116,6 +116,33 @@ test('Browser JS listener / Impressions optimized mode', () => {

triggerUnloadEvent();

// Unload event was triggered, but sendBeacon and post services should not be called
expect(global.window.navigator.sendBeacon).toBeCalledTimes(0);
expect(fakeSplitApi.postTestImpressionsBulk).not.toBeCalled();
expect(fakeSplitApi.postEventsBulk).not.toBeCalled();
expect(fakeSplitApi.postTestImpressionsCount).not.toBeCalled();

// pre-check and call stop
expect(global.window.removeEventListener).not.toBeCalled();
listener.stop();

// removed correct listener from correct signal on stop.
expect((global.window.removeEventListener as jest.Mock).mock.calls).toEqual([[UNLOAD_DOM_EVENT, listener.flushData]]);
});

test('Browser JS listener / standalone mode / Impressions optimized mode', () => {
const syncManagerMock = {};

// @ts-expect-error
const listener = new BrowserSignalListener(syncManagerMock, fullSettings, fakeStorageOptimized as IStorageSync, fakeSplitApi);

listener.start();

// Assigned right function to right signal.
expect((global.window.addEventListener as jest.Mock).mock.calls).toEqual([[UNLOAD_DOM_EVENT, listener.flushData]]);

triggerUnloadEvent();

// Unload event was triggered. Thus sendBeacon method should have been called three times.
expect(global.window.navigator.sendBeacon).toBeCalledTimes(3);

Expand All @@ -132,7 +159,7 @@ test('Browser JS listener / Impressions optimized mode', () => {
expect((global.window.removeEventListener as jest.Mock).mock.calls).toEqual([[UNLOAD_DOM_EVENT, listener.flushData]]);
});

test('Browser JS listener / Impressions debug mode', () => {
test('Browser JS listener / standalone mode / Impressions debug mode', () => {
const syncManagerMockWithPushManager = { pushManager: { stop: jest.fn() } };

// @ts-expect-error
Expand Down Expand Up @@ -166,7 +193,7 @@ test('Browser JS listener / Impressions debug mode', () => {
expect((global.window.removeEventListener as jest.Mock).mock.calls).toEqual([[UNLOAD_DOM_EVENT, listener.flushData]]);
});

test('Browser JS listener / Impressions debug mode without sendBeacon API', () => {
test('Browser JS listener / standalone mode / Impressions debug mode without sendBeacon API', () => {
// remove sendBeacon API
const sendBeacon = global.navigator.sendBeacon; // @ts-expect-error
global.navigator.sendBeacon = undefined;
Expand Down
4 changes: 3 additions & 1 deletion src/listeners/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ export default class BrowserSignalListener implements ISignalListener {
* using beacon API if possible, or falling back to regular post transport.
*/
flushData() {
if (!this.syncManager) return; // In consumer mode there is not sync manager and data to flush

const eventsUrl = this.settings.urls.events;
const extraMetadata = {
// sim stands for Sync/Split Impressions Mode
Expand All @@ -74,7 +76,7 @@ export default class BrowserSignalListener implements ISignalListener {
if (this.storage.impressionCounts) this._flushData(eventsUrl + '/testImpressions/count/beacon', this.storage.impressionCounts, this.serviceApi.postTestImpressionsCount, fromImpressionCountsCollector);

// Close streaming connection
if (this.syncManager && this.syncManager.pushManager) this.syncManager.pushManager.stop();
if (this.syncManager.pushManager) this.syncManager.pushManager.stop();
}

private _flushData<TState>(url: string, cache: IRecorderCacheProducerSync<TState>, postService: (body: string) => Promise<IResponse>, fromCacheToPayload?: (cacheData: TState) => any, extraMetadata?: {}) {
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
12 changes: 6 additions & 6 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 All @@ -37,11 +37,11 @@ export function sdkClientFactory(params: ISdkClientFactoryParams): SplitIO.IClie
sdkReadinessManager.readinessManager.destroy();
signalListener && signalListener.stop();

// Cleanup storage
storage.destroy();

// Release the API Key if it is the main client
if (!sharedClient) releaseApiKey(settings.core.authorizationKey);

// Cleanup storage
return storage.destroy();
});
}
}
Expand Down
18 changes: 14 additions & 4 deletions src/sdkClient/sdkClientMethodCS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import { SplitIO } from '../types';
import { validateKey } from '../utils/inputValidation/key';
import { getMatching, keyParser } from '../utils/key';
import { sdkClientFactory } from './sdkClient';
import { IStorageSyncCS } from '../storages/types';
import { ISyncManagerCS } from '../sync/types';
import objectAssign from 'object-assign';
import { RETRIEVE_CLIENT_DEFAULT, NEW_SHARED_CLIENT, RETRIEVE_CLIENT_EXISTING } from '../logger/constants';
import { SDK_SEGMENTS_ARRIVED } from '../readiness/constants';

function buildInstanceId(key: SplitIO.SplitKey) {
// @ts-ignore
Expand Down Expand Up @@ -58,15 +58,25 @@ export function sdkClientMethodCSFactory(params: ISdkClientFactoryParams): (key?
const matchingKey = getMatching(validKey);

const sharedSdkReadiness = sdkReadinessManager.shared(readyTimeout);
const sharedStorage = (storage as IStorageSyncCS).shared(matchingKey);
const sharedSyncManager = syncManager && (syncManager as ISyncManagerCS).shared(matchingKey, sharedSdkReadiness.readinessManager, sharedStorage);
const sharedStorage = storage.shared && storage.shared(matchingKey, (err) => {
if (err) return;
// 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);

// As shared clients reuse all the storage information, we don't need to check here if we
// will use offline or online mode. We should stick with the original decision.
clientInstances[instanceId] = clientCSDecorator(
sdkClientFactory(objectAssign({}, params, {
sdkReadinessManager: sharedSdkReadiness,
storage: sharedStorage,
storage: sharedStorage || storage,
syncManager: sharedSyncManager,
signalListener: undefined, // only the main client "destroy" method stops the signal listener
sharedClient: true
Expand Down
20 changes: 15 additions & 5 deletions src/sdkClient/sdkClientMethodCSWithTT.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ import { validateKey } from '../utils/inputValidation/key';
import { validateTrafficType } from '../utils/inputValidation/trafficType';
import { getMatching, keyParser } from '../utils/key';
import { sdkClientFactory } from './sdkClient';
import { IStorageSyncCS } from '../storages/types';
import { ISyncManagerCS } from '../sync/types';
import objectAssign from 'object-assign';
import { RETRIEVE_CLIENT_DEFAULT, NEW_SHARED_CLIENT, RETRIEVE_CLIENT_EXISTING } from '../logger/constants';
import { SDK_SEGMENTS_ARRIVED } from '../readiness/constants';

function buildInstanceId(key: SplitIO.SplitKey, trafficType?: string) {
// @ts-ignore
Expand Down Expand Up @@ -72,15 +72,25 @@ export function sdkClientMethodCSFactory(params: ISdkClientFactoryParams): (key?
const matchingKey = getMatching(validKey);

const sharedSdkReadiness = sdkReadinessManager.shared(readyTimeout);
const sharedStorage = (storage as IStorageSyncCS).shared(matchingKey);
const sharedSyncManager = (syncManager as ISyncManagerCS).shared(matchingKey, sharedSdkReadiness.readinessManager, sharedStorage);
const sharedStorage = storage.shared && storage.shared(matchingKey, (err) => {
if (err) return;
// 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);

// As shared clients reuse all the storage information, we don't need to check here if we
// will use offline or online mode. We should stick with the original decision.
clientInstances[instanceId] = clientCSDecorator(
sdkClientFactory(objectAssign({}, params, {
sdkReadinessManager: sharedSdkReadiness,
storage: sharedStorage,
storage: sharedStorage || storage,
syncManager: sharedSyncManager,
signalListener: undefined, // only the main client "destroy" method stops the signal listener
sharedClient: true
Expand All @@ -89,7 +99,7 @@ export function sdkClientMethodCSFactory(params: ISdkClientFactoryParams): (key?
validTrafficType
);

sharedSyncManager.start();
sharedSyncManager && sharedSyncManager.start();

log.info(NEW_SHARED_CLIENT);
} else {
Expand Down
10 changes: 7 additions & 3 deletions src/sdkFactory/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,16 @@ export function sdkFactory(params: ISdkFactoryParams): SplitIO.ICsSDK | SplitIO.
matchingKey: getMatching(settings.core.key),
splitFiltersValidation: settings.sync.__splitFiltersValidation,

// Callback used in consumer mode (`syncManagerFactory` is undefined) to emit SDK_READY
onReadyCb: !syncManagerFactory ? (error) => {
// 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) => {
if (error) return; // don't emit SDK_READY if storage failed to connect.
readinessManager.splits.emit(SDK_SPLITS_ARRIVED);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this one will never be a shared instance right? and when we implement new flavors like node "client side", etc this will still be true?

Otherwise we should check about emitting a duplicated SPLITS_ARRIVED which might cause an SDK_UPDATE

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When instantiating shared clients, a new onReadyCb is passed to the storage which only emits SDK_SEGMENTS_ARRIVED for that shared client. Therefore, no SDK_UPDATE event will be duplicated for the main client.

readinessManager.segments.emit(SDK_SEGMENTS_ARRIVED);
} : undefined,
},
metadata: metadataBuilder(settings),
log
};
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
Loading