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
211 changes: 211 additions & 0 deletions src/sdkClient/__tests__/clientAttributesDecoration.spec.ts

Large diffs are not rendered by default.

104 changes: 102 additions & 2 deletions src/sdkClient/__tests__/sdkClientMethodCS.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,20 @@ const params = {
settings: settingsWithKey
};

const invalidAttributes = [
new Date(),
{ some: 'object' },
Infinity
];

const validAttributes = [
25, // number
Date.now(), // number
'string', // string
['string', 'list'], // list
false // boolean
];

/** End mocks */

describe('sdkClientMethodCSFactory', () => {
Expand Down Expand Up @@ -227,8 +241,94 @@ describe('sdkClientMethodCSFactory', () => {
assertClientApi(client, params.sdkReadinessManager.sdkStatus);

// but with false as binded key and TT
if (ignoresTT) expect(clientCSDecoratorSpy).toHaveBeenCalledWith(expect.anything(), false);
else expect(clientCSDecoratorSpy).toHaveBeenCalledWith(expect.anything(), false, false);
if (ignoresTT) expect(clientCSDecoratorSpy).toHaveBeenCalledWith(expect.anything(), expect.anything(), false);
else expect(clientCSDecoratorSpy).toHaveBeenCalledWith(expect.anything(), expect.anything(), false, false);
});

test.each(testTargets)('attributes binding - main client', (sdkClientMethodCSFactory) => {
// @ts-expect-error
const sdkClientMethod = sdkClientMethodCSFactory(params);

// should return a function
expect(typeof sdkClientMethod).toBe('function');

// calling the function should return a client instance
const client = sdkClientMethod();
assertClientApi(client, params.sdkReadinessManager.sdkStatus);

expect(client.setAttribute('attributeName1', 'attributeValue1')).toEqual(true);
expect(client.setAttribute('attributeName2', 'attributeValue2')).toEqual(true);

expect(client.setAttribute('', 'empty')).toEqual(false); // Attribute name should not be an empty string
expect(client.setAttribute({ '': 'empty' })).toEqual(false); // Attribute name should not be an empty string

invalidAttributes.forEach(invalidValue => {
expect(client.setAttribute('attributeName', invalidValue)).toEqual(false);
expect(client.setAttributes({ attributeName: invalidValue })).toEqual(false);
});

expect(client.getAttributes()).toEqual({ attributeName1: 'attributeValue1', attributeName2: 'attributeValue2' });

validAttributes.forEach(validValue => {
expect(client.setAttribute('attributeName', validValue)).toEqual(true);
});

validAttributes.forEach(validValue => {
expect(client.setAttributes({ attributeName: validValue })).toEqual(true);
});

expect(client.getAttributes()).toEqual({ attributeName: false, attributeName1: 'attributeValue1', attributeName2: 'attributeValue2' });

expect(client.removeAttribute('attributeName1')).toEqual(true);

expect(client.getAttribute('attributeName1')).toEqual(undefined);
expect(client.getAttribute('attributeName2')).toEqual('attributeValue2');

expect(client.setAttributes({
'attributeName3': 'attributeValue3',
'attributeName4': 'attributeValue4'
})).toEqual(true);

expect(client.getAttribute('attributeName2')).toEqual('attributeValue2');
expect(client.getAttribute('attributeName3')).toEqual('attributeValue3');
expect(client.getAttribute('attributeName4')).toEqual('attributeValue4');

expect(client.clearAttributes()).toEqual(true);

expect(client.getAttributes()).toEqual({});
});


test.each(testTargets)('attributes binding - shared clients', (sdkClientMethodCSFactory) => {
// @ts-expect-error
const sdkClientMethod = sdkClientMethodCSFactory(params);

// should return a function
expect(typeof sdkClientMethod).toBe('function');

// calling the function should return a client instance
const emmanuelClient = sdkClientMethod('emmanuel@split.io');
const emilianoClient = sdkClientMethod('emiliano@split.io');
assertClientApi(emmanuelClient);
assertClientApi(emilianoClient);

expect(emmanuelClient.setAttribute('name', 'Emmanuel')).toEqual(true);
expect(emilianoClient.setAttribute('name', 'Emiliano')).toEqual(true);

expect(emmanuelClient.getAttribute('name')).toEqual('Emmanuel');
expect(emilianoClient.getAttribute('name')).toEqual('Emiliano');

expect(emmanuelClient.setAttributes({ email: 'emmanuel@split.io' })).toEqual(true);
expect(emilianoClient.setAttributes({ email: 'emiliano@split.io' })).toEqual(true);

expect(emmanuelClient.getAttributes()).toEqual({ name: 'Emmanuel', email: 'emmanuel@split.io' });
expect(emilianoClient.getAttributes()).toEqual({ name: 'Emiliano', email: 'emiliano@split.io' });

expect(emmanuelClient.clearAttributes()).toEqual(true);

expect(emmanuelClient.getAttributes()).toEqual({});
expect(emilianoClient.getAttributes()).toEqual({ name: 'Emiliano', email: 'emiliano@split.io' });

});

});
122 changes: 122 additions & 0 deletions src/sdkClient/clientAttributesDecoration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { AttributesCacheInMemory } from '../storages/inMemory/AttributesCacheInMemory';
import { validateAttributesDeep } from '../utils/inputValidation/attributes';
import { SplitIO } from '../types';
import { ILogger } from '../logger/types';
import { objectAssign } from '../utils/lang/objectAssign';

/**
* Add in memory attributes storage methods and combine them with any attribute received from the getTreatment/s call
*/
export function clientAttributesDecoration<TClient extends SplitIO.IClient | SplitIO.IAsyncClient>(log: ILogger, client: TClient) {

const attributeStorage = new AttributesCacheInMemory();

// Keep a reference to the original methods
const clientGetTreatment = client.getTreatment;
const clientGetTreatmentWithConfig = client.getTreatmentWithConfig;
const clientGetTreatments = client.getTreatments;
const clientGetTreatmentsWithConfig = client.getTreatmentsWithConfig;
const clientTrack = client.track;

function getTreatment(maybeKey: SplitIO.SplitKey, maybeSplit: string, maybeAttributes?: SplitIO.Attributes) {
return clientGetTreatment(maybeKey, maybeSplit, combineAttributes(maybeAttributes));
}

function getTreatmentWithConfig(maybeKey: SplitIO.SplitKey, maybeSplit: string, maybeAttributes?: SplitIO.Attributes) {
return clientGetTreatmentWithConfig(maybeKey, maybeSplit, combineAttributes(maybeAttributes));
}

function getTreatments(maybeKey: SplitIO.SplitKey, maybeSplits: string[], maybeAttributes?: SplitIO.Attributes) {
return clientGetTreatments(maybeKey, maybeSplits, combineAttributes(maybeAttributes));
}

function getTreatmentsWithConfig(maybeKey: SplitIO.SplitKey, maybeSplits: string[], maybeAttributes?: SplitIO.Attributes) {
return clientGetTreatmentsWithConfig(maybeKey, maybeSplits, combineAttributes(maybeAttributes));
}

function track(maybeKey: SplitIO.SplitKey, maybeTT: string, maybeEvent: string, maybeEventValue?: number, maybeProperties?: SplitIO.Properties) {
return clientTrack(maybeKey, maybeTT, maybeEvent, maybeEventValue, maybeProperties);
}

function combineAttributes(maybeAttributes: SplitIO.Attributes | undefined): SplitIO.Attributes | undefined{
const storedAttributes = attributeStorage.getAll();
if (Object.keys(storedAttributes).length > 0) {
return objectAssign({}, storedAttributes, maybeAttributes);
}
return maybeAttributes;
}

return objectAssign(client, {
getTreatment: getTreatment,
getTreatmentWithConfig: getTreatmentWithConfig,
getTreatments: getTreatments,
getTreatmentsWithConfig: getTreatmentsWithConfig,
track: track,

/**
* Add an attribute to client's in memory attributes storage
*
* @param {string} attributeName Attrinute name
* @param {string, number, boolean, list} attributeValue Attribute value
* @returns {boolean} true if the attribute was stored and false otherways
*/
setAttribute(attributeName: string, attributeValue: Object) {
const attribute: Record<string, Object> = {};
attribute[attributeName] = attributeValue;
if (!validateAttributesDeep(log, attribute, 'setAttribute')) return false;
log.debug(`stored ${attributeValue} for attribute ${attributeName}`);
return attributeStorage.setAttribute(attributeName, attributeValue);
},

/**
* Returns the attribute with the given key
*
* @param {string} attributeName Attribute name
* @returns {Object} Attribute with the given key
*/
getAttribute(attributeName: string) {
log.debug(`retrieved attribute ${attributeName}`);
return attributeStorage.getAttribute(attributeName + '');
},

/**
* Add to client's in memory attributes storage the attributes in 'attributes'
*
* @param {Object} attributes Object with attributes to store
* @returns true if attributes were stored an false otherways
*/
setAttributes(attributes: Record<string, Object>) {
if (!validateAttributesDeep(log, attributes, 'setAttributes')) return false;
return attributeStorage.setAttributes(attributes);
},

/**
* Return all the attributes stored in client's in memory attributes storage
*
* @returns {Object} returns all the stored attributes
*/
getAttributes(): Record<string, Object> {
return attributeStorage.getAll();
},

/**
* Removes from client's in memory attributes storage the attribute with the given key
*
* @param {string} attributeName
* @returns {boolean} true if attribute was removed and false otherways
*/
removeAttribute(attributeName: string) {
log.debug(`removed attribute ${attributeName}`);
return attributeStorage.removeAttribute(attributeName + '');
},

/**
* Remove all the stored attributes in the client's in memory attribute storage
*/
clearAttributes() {
return attributeStorage.clear();
}

});

}
19 changes: 12 additions & 7 deletions src/sdkClient/clientCS.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { objectAssign } from '../utils/lang/objectAssign';
import { ILogger } from '../logger/types';
import { SplitIO } from '../types';
import { clientAttributesDecoration } from './clientAttributesDecoration';


/**
Expand All @@ -9,15 +11,18 @@ import { SplitIO } from '../types';
* @param key validated split key
* @param trafficType validated traffic type
*/
export function clientCSDecorator(client: SplitIO.IClient, key: SplitIO.SplitKey, trafficType?: string): SplitIO.ICsClient {
return objectAssign(client, {
export function clientCSDecorator(log: ILogger, client: SplitIO.IClient, key: SplitIO.SplitKey, trafficType?: string): SplitIO.ICsClient {

let clientCS = clientAttributesDecoration(log, client);

return objectAssign(clientCS, {
// In the client-side API, we bind a key to the client `getTreatment*` methods
getTreatment: client.getTreatment.bind(client, key),
getTreatmentWithConfig: client.getTreatmentWithConfig.bind(client, key),
getTreatments: client.getTreatments.bind(client, key),
getTreatmentsWithConfig: client.getTreatmentsWithConfig.bind(client, key),
getTreatment: clientCS.getTreatment.bind(clientCS, key),
getTreatmentWithConfig: clientCS.getTreatmentWithConfig.bind(clientCS, key),
getTreatments: clientCS.getTreatments.bind(clientCS, key),
getTreatmentsWithConfig: clientCS.getTreatmentsWithConfig.bind(clientCS, key),

// Key is bound to the `track` method. Same thing happens with trafficType but only if provided
track: trafficType ? client.track.bind(client, key, trafficType) : client.track.bind(client, key)
track: trafficType ? clientCS.track.bind(clientCS, key, trafficType) : clientCS.track.bind(clientCS, key)
}) as SplitIO.ICsClient;
}
2 changes: 2 additions & 0 deletions src/sdkClient/sdkClientMethodCS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export function sdkClientMethodCSFactory(params: ISdkClientFactoryParams): (key?
const validKey = validateKey(log, key, method);

const mainClientInstance = clientCSDecorator(
log,
sdkClientFactory(params) as SplitIO.IClient, // @ts-ignore
validKey
);
Expand Down Expand Up @@ -74,6 +75,7 @@ export function sdkClientMethodCSFactory(params: ISdkClientFactoryParams): (key?
// 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(
log,
sdkClientFactory(objectAssign({}, params, {
sdkReadinessManager: sharedSdkReadiness,
storage: sharedStorage || storage,
Expand Down
2 changes: 2 additions & 0 deletions src/sdkClient/sdkClientMethodCSWithTT.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export function sdkClientMethodCSFactory(params: ISdkClientFactoryParams): (key?
}

const mainClientInstance = clientCSDecorator(
log,
sdkClientFactory(params) as SplitIO.IClient, // @ts-ignore
validKey,
validTrafficType
Expand Down Expand Up @@ -88,6 +89,7 @@ export function sdkClientMethodCSFactory(params: ISdkClientFactoryParams): (key?
// 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(
log,
sdkClientFactory(objectAssign({}, params, {
sdkReadinessManager: sharedSdkReadiness,
storage: sharedStorage || storage,
Expand Down
73 changes: 73 additions & 0 deletions src/storages/inMemory/AttributesCacheInMemory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { objectAssign } from '../../utils/lang/objectAssign';

export class AttributesCacheInMemory {

private attributesCache: Record<string, Object> = {};


/**
* Create or update the value for the given attribute
*
* @param {string} attributeName attribute name
* @param {Object} attributeValue attribute value
* @returns {boolean} the attribute was stored
*/
setAttribute(attributeName: string, attributeValue: Object): boolean {
this.attributesCache[attributeName] = attributeValue;
return true;
}

/**
* Retrieves the value of a given attribute
*
* @param {string} attributeName attribute name
* @returns {Object?} stored attribute value
*/
getAttribute(attributeName: string): Object {
return this.attributesCache[attributeName];
}

/**
* Create or update all the given attributes
*
* @param {[string, Object]} attributes attributes to create or update
* @returns {boolean} attributes were stored
*/
setAttributes(attributes: Record<string, Object>): boolean {
this.attributesCache = objectAssign(this.attributesCache, attributes);
return true;
}

/**
* Retrieve the full attributes map
*
* @returns {Map<string, Object>} stored attributes
*/
getAll(): Record<string, Object> {
return this.attributesCache;
}

/**
* Removes a given attribute from the map
*
* @param {string} attributeName attribute to remove
* @returns {boolean} attribute removed
*/
removeAttribute(attributeName: string): boolean {
if (Object.keys(this.attributesCache).indexOf(attributeName) >= 0) {
delete this.attributesCache[attributeName];
return true;
}
return false;
}

/**
* Clears all attributes stored in the SDK
*
*/
clear() {
this.attributesCache = {};
return true;
}

}
Loading