Open dereksdev opened 10 months ago
Hi @dereksdev ,
The features
property is used for testing purposes as described here, and is only available for the default mode (a.k.a, standalone or InMemory mode).
So you cannot use it with modes consumer
or consumer_partial
, and it is enforced via the TypeScript interface IBrowserAsyncSettings
.
In summary:
consumer
or consumer_partial
the config follows the IBrowserAsyncSettings interface. This interface doesn't allow the features
property, and requires PluggableStorage
as storage.standalone
, the config follows the IBrowserSettings interface that support the features property, and doesn't require an storage (it keeps feature flag definitions in memory).Thank you @EmilianoSanchez. This helps me understand the intent better.
The features
property does actually still function with the mode set to consumer
or consumer_partial
which led to my confusion.
So in order to test in consumer mode, do we need to create a mock PluggableStorage
object? Any guidance here would be appreciated.
Hi @dereksdev ,
The features property does actually still function with the mode set to consumer or consumer_partial which led to my confusion.
If you are using just JavaScript, yes, you can pass the features
property, and it will not complain, but it will be ignored in both consumer modes.
So in order to test in consumer mode, do we need to create a mock PluggableStorage object? Any guidance here would be appreciated.
We are going to take a look to it during January. At the moment there is not official support for localhost mode in consumer modes, but I can share with you a code snippet you can try on your side, based on the implementation of a storage wrapper in memory.
However, keep in mind that this might not be the final solution for sure.
// index.js
import { SplitFactory, PluggableStorage } from '@splitsoftware/splitio-browserjs';
import { inMemoryWrapperFactory } from './inMemoryWrapper.js';
const inMemoryWrapper = inMemoryWrapperFactory({
// `features` format as explained here: https://help.split.io/hc/en-us/articles/360058730852-Browser-SDK#localhost-mode
features: {
'feature_1': 'on',
}
});
const factory = SplitFactory({
core: {
authorizationKey: 'anything-except-localhost',
key: 'user_x'
},
mode: 'consumer',
storage: PluggableStorage({
wrapper: inMemoryWrapper
})
});
const client = factory.client();
client.ready().then(async () => {
console.log(await client.getTreatment('feature_1')); // on
console.log(inMemoryWrapper._cache);
});
// inMemoryWrapper.js
/**
* Creates a IPluggableStorageWrapper implementation that stores items in memory.
* The `_cache` property is the object were items are stored.
* Intended for testing purposes.
*/
export function inMemoryWrapperFactory({ features, prefix } = {}) {
let _cache = {};
const wrapper = {
_cache,
get(key) {
return Promise.resolve(key in _cache ? _cache[key] : null);
},
set(key, value) {
const result = key in _cache;
_cache[key] = value;
return Promise.resolve(result);
},
getAndSet(key, value) {
const result = key in _cache ? _cache[key] : null;
_cache[key] = value;
return Promise.resolve(result);
},
del(key) {
const result = key in _cache;
delete _cache[key];
return Promise.resolve(result);
},
getKeysByPrefix(prefix) {
return Promise.resolve(Object.keys(_cache).filter(key => key.startsWith(prefix)));
},
incr(key, increment = 1) {
if (key in _cache) {
const count = parseInt(_cache[key]) + increment;
if (isNaN(count)) return Promise.reject('Given key is not a number');
_cache[key] = count + '';
return Promise.resolve(count);
} else {
_cache[key] = '' + increment;
return Promise.resolve(1);
}
},
decr(key, decrement = 1) {
if (key in _cache) {
const count = parseInt(_cache[key]) - decrement;
if (isNaN(count)) return Promise.reject('Given key is not a number');
_cache[key] = count + '';
return Promise.resolve(count);
} else {
_cache[key] = '-' + decrement;
return Promise.resolve(-1);
}
},
getMany(keys) {
return Promise.resolve(keys.map(key => _cache[key] ? _cache[key] : null));
},
pushItems(key, items) {
if (!(key in _cache)) _cache[key] = [];
const list = _cache[key];
if (Array.isArray(list)) {
list.push(...items);
return Promise.resolve();
}
return Promise.reject('key is not a list');
},
popItems(key, count) {
const list = _cache[key];
return Promise.resolve(Array.isArray(list) ? list.splice(0, count) : []);
},
getItemsCount(key) {
const list = _cache[key];
return Promise.resolve(Array.isArray(list) ? list.length : 0);
},
itemContains(key, item) {
const set = _cache[key];
if (!set) return Promise.resolve(false);
if (set instanceof Set) return Promise.resolve(set.has(item));
return Promise.reject('key is not a set');
},
addItems(key, items) {
if (!(key in _cache)) _cache[key] = new Set();
const set = _cache[key];
if (set instanceof Set) {
items.forEach(item => set.add(item));
return Promise.resolve();
}
return Promise.reject('key is not a set');
},
removeItems(key, items) {
if (!(key in _cache)) _cache[key] = new Set();
const set = _cache[key];
if (set instanceof Set) {
items.forEach(item => set.delete(item));
return Promise.resolve();
}
return Promise.reject('key is not a set');
},
getItems(key) {
const set = _cache[key];
if (!set) return Promise.resolve([]);
if (set instanceof Set) return Promise.resolve(setToArray(set));
return Promise.reject('key is not a set');
},
// always connects and disconnects
connect() { return Promise.resolve(); },
disconnect() { return Promise.resolve(); },
};
// Add features to wrapper
if (features) {
function parseCondition(data) {
const treatment = data.treatment;
if (data.keys) {
return {
conditionType: 'WHITELIST',
matcherGroup: {
combiner: 'AND',
matchers: [
{
keySelector: null,
matcherType: 'WHITELIST',
negate: false,
whitelistMatcherData: {
whitelist: typeof data.keys === 'string' ? [data.keys] : data.keys
}
}
]
},
partitions: [
{
treatment: treatment,
size: 100
}
],
label: `whitelisted ${treatment}`
};
} else {
return {
conditionType: 'ROLLOUT',
matcherGroup: {
combiner: 'AND',
matchers: [
{
keySelector: null,
matcherType: 'ALL_KEYS',
negate: false
}
]
},
partitions: [
{
treatment: treatment,
size: 100
}
],
label: 'default rule'
};
}
}
function splitsParserFromFeatures(features) {
const splitObjects = {};
Object.entries(features).forEach(([splitName, data]) => {
let treatment = data;
let config = null;
if (data !== null && typeof data === 'object') {
treatment = data.treatment;
config = data.config || config;
}
const configurations = {};
if (config !== null) configurations[treatment] = config;
splitObjects[splitName] = {
name: splitName,
trafficTypeName: 'localhost',
conditions: [parseCondition({ treatment: treatment })],
configurations,
status: 'ACTIVE',
killed: false,
trafficAllocation: 100,
defaultTreatment: 'control',
};
});
return splitObjects;
};
const featureFlags = splitsParserFromFeatures(features);
prefix = prefix ? prefix + '.SPLITIO' : 'SPLITIO';
Object.entries(featureFlags).forEach(([featureFlagName, featureFlag]) => {
const featureFlagKey = `${prefix}.split.${featureFlagName}`;
wrapper.set(featureFlagKey, JSON.stringify(featureFlag))
});
}
return wrapper;
}
Using the features property in "consumer" or "consumer_partial" mode produces the following type error.
Example: