Skip to content

Commit

Permalink
Fb/redis read local mode (#75)
Browse files Browse the repository at this point in the history
* better defaulting for constructor/reset

* jsdoc for constructor

* some more jsdoc

* make redisRead local mode work similar to state
  • Loading branch information
rlindner81 authored Sep 30, 2024
1 parent 8e41965 commit 5996dac
Show file tree
Hide file tree
Showing 4 changed files with 241 additions and 74 deletions.
105 changes: 73 additions & 32 deletions src/featureToggles.js
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,30 @@ class FeatureToggles {
// START OF CONSTRUCTOR SECTION
// ========================================

static _getDefaultUniqueName() {
if (ENV_UNIQUE_NAME) {
return ENV_UNIQUE_NAME;
}
let cfApp;
try {
cfApp = cfEnv.cfApp;
if (cfApp.application_name) {
return cfApp.application_name;
}
} catch (err) {
throw new VError(
{
name: VERROR_CLUSTER_NAME,
cause: err,
info: {
cfApp: JSON.stringify(cfApp),
},
},
"error determining cf app name"
);
}
}

_processValidations(featureKey, validations, configFilepath) {
const configDir = configFilepath ? pathlib.dirname(configFilepath) : process.cwd();

Expand Down Expand Up @@ -274,7 +298,16 @@ class FeatureToggles {
);
}

_reset({ uniqueName, redisChannel = DEFAULT_REDIS_CHANNEL, redisKey = DEFAULT_REDIS_KEY }) {
/**
* Implementation for {@link constructor}.
*
* @param {ConstructorOptions} [options]
*/
_reset({
uniqueName = FeatureToggles._getDefaultUniqueName(),
redisChannel = DEFAULT_REDIS_CHANNEL,
redisKey = DEFAULT_REDIS_KEY,
} = {}) {
this.__redisChannel = uniqueName ? redisChannel + "-" + uniqueName : redisChannel;
this.__redisKey = uniqueName ? redisKey + "-" + uniqueName : redisKey;

Expand All @@ -292,9 +325,19 @@ class FeatureToggles {
this.__isConfigProcessed = false;
}

// NOTE: constructors cannot be async, so we need to split this state preparation part from the initialize part
constructor({ uniqueName = undefined, redisChannel = DEFAULT_REDIS_CHANNEL, redisKey = DEFAULT_REDIS_KEY } = {}) {
this._reset({ uniqueName, redisChannel, redisKey });
/**
* @typedef ConstructorOptions
* @type object
* @property {string} [uniqueName] unique name to prefix both Redis channel and key
* @property {string} [redisChannel] channel for Redis pub/sub to propagate changes across servers
* @property {string} [redisKey] key in Redis to save non-fallback values
*/
/**
* NOTE: constructors cannot be async, so we need to split this state preparation part from the initialize part
* @param {ConstructorOptions} [options]
*/
constructor(options) {
this._reset(options);
}

// ========================================
Expand All @@ -304,39 +347,14 @@ class FeatureToggles {
// START OF SINGLETON SECTION
// ========================================

static _getInstanceUniqueName() {
if (ENV_UNIQUE_NAME) {
return ENV_UNIQUE_NAME;
}
let cfApp;
try {
cfApp = cfEnv.cfApp;
if (cfApp.application_name) {
return cfApp.application_name;
}
} catch (err) {
throw new VError(
{
name: VERROR_CLUSTER_NAME,
cause: err,
info: {
cfApp: JSON.stringify(cfApp),
},
},
"error determining cf app name"
);
}
}

/**
* Get singleton instance
*
* @returns {FeatureToggles}
*/
static getInstance() {
if (!FeatureToggles.__instance) {
const uniqueName = FeatureToggles._getInstanceUniqueName();
FeatureToggles.__instance = new FeatureToggles({ uniqueName });
FeatureToggles.__instance = new FeatureToggles();
}
return FeatureToggles.__instance;
}
Expand Down Expand Up @@ -713,6 +731,11 @@ class FeatureToggles {
);
}

/**
* Implementation for {@link initializeFeatures}.
*
* @param {InitializeOptions} [options]
*/
async _initializeFeatures({ config: configRuntime, configFile: configFilepath, configAuto } = {}) {
if (this.__isInitialized) {
return;
Expand Down Expand Up @@ -840,9 +863,23 @@ class FeatureToggles {
return this;
}

/**
* TODO
* @typedef Config
* @type object
*/
/**
* @typedef InitializeOptions
* @type object
* @property {Config} [config]
* @property {string} [configFile]
* @property {Config} [configAuto]
*/
/**
* Initialize needs to run and finish before other APIs are called. It processes the configuration, sets up
* related internal state, and starts communication with redis.
*
* @param {InitializeOptions} [options]
*/
async initializeFeatures(options) {
if (!this.__initializePromise) {
Expand Down Expand Up @@ -919,11 +956,15 @@ class FeatureToggles {
*/
async getRemoteFeaturesInfos() {
this._ensureInitialized();

let remoteStateScopedValues;
// NOTE: for NO_REDIS mode, we show local updates
if ((await redis.getIntegrationMode()) === REDIS_INTEGRATION_MODE.NO_REDIS) {
return {};
remoteStateScopedValues = this.__stateScopedValues ?? {};
} else {
remoteStateScopedValues = await redis.hashGetAllObjects(this.__redisKey);
}

const remoteStateScopedValues = await redis.hashGetAllObjects(this.__redisKey);
if (!remoteStateScopedValues) {
return null;
}
Expand Down
107 changes: 106 additions & 1 deletion test/cds-service/__snapshots__/featureToggles.service.test.js.snap
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`cds-service endpoints state response 1`] = `
exports[`cds-service endpoints state response no change 1`] = `
{
"test/feature_a": {
"config": {
Expand Down Expand Up @@ -99,3 +99,108 @@ exports[`cds-service endpoints state response 1`] = `
},
}
`;

exports[`cds-service endpoints state response with changes 1`] = `
{
"test/feature_a": {
"config": {
"SOURCE": "RUNTIME",
"TYPE": "boolean",
},
"fallbackValue": false,
},
"test/feature_aa": {
"config": {
"SOURCE": "RUNTIME",
"TYPE": "boolean",
"VALIDATIONS": [
{
"scopes": [
"tenant",
"user",
],
},
],
},
"fallbackValue": false,
},
"test/feature_b": {
"config": {
"SOURCE": "RUNTIME",
"TYPE": "number",
},
"fallbackValue": 1,
"rootValue": 2,
"scopedValues": {
"tenant::a": 20,
"tenant::b": 30,
},
},
"test/feature_c": {
"config": {
"SOURCE": "RUNTIME",
"TYPE": "string",
},
"fallbackValue": "best",
},
"test/feature_d": {
"config": {
"SOURCE": "RUNTIME",
"TYPE": "boolean",
"VALIDATIONS": [
{
"regex": "^(?:true)$",
},
],
},
"fallbackValue": true,
},
"test/feature_e": {
"config": {
"SOURCE": "RUNTIME",
"TYPE": "number",
"VALIDATIONS": [
{
"scopes": [
"component",
"layer",
"tenant",
],
},
{
"regex": "^\\d{1}$",
},
],
},
"fallbackValue": 5,
},
"test/feature_f": {
"config": {
"SOURCE": "RUNTIME",
"TYPE": "string",
"VALIDATIONS": [
{
"regex": "^(?:best|worst)$",
},
],
},
"fallbackValue": "best",
},
"test/feature_g": {
"config": {
"ACTIVE": false,
"SOURCE": "RUNTIME",
"TYPE": "string",
},
"fallbackValue": "activeTest",
},
"test/feature_h": {
"config": {
"APP_URL": "\\.cfapps\\.sap\\.hana\\.ondemand\\.com$",
"SOURCE": "RUNTIME",
"TYPE": "string",
},
"fallbackValue": "appUrlTest",
},
}
`;
Loading

0 comments on commit 5996dac

Please sign in to comment.