Skip to content

Commit 5996dac

Browse files
authored
Fb/redis read local mode (#75)
* better defaulting for constructor/reset * jsdoc for constructor * some more jsdoc * make redisRead local mode work similar to state
1 parent 8e41965 commit 5996dac

File tree

4 files changed

+241
-74
lines changed

4 files changed

+241
-74
lines changed

src/featureToggles.js

Lines changed: 73 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,30 @@ class FeatureToggles {
119119
// START OF CONSTRUCTOR SECTION
120120
// ========================================
121121

122+
static _getDefaultUniqueName() {
123+
if (ENV_UNIQUE_NAME) {
124+
return ENV_UNIQUE_NAME;
125+
}
126+
let cfApp;
127+
try {
128+
cfApp = cfEnv.cfApp;
129+
if (cfApp.application_name) {
130+
return cfApp.application_name;
131+
}
132+
} catch (err) {
133+
throw new VError(
134+
{
135+
name: VERROR_CLUSTER_NAME,
136+
cause: err,
137+
info: {
138+
cfApp: JSON.stringify(cfApp),
139+
},
140+
},
141+
"error determining cf app name"
142+
);
143+
}
144+
}
145+
122146
_processValidations(featureKey, validations, configFilepath) {
123147
const configDir = configFilepath ? pathlib.dirname(configFilepath) : process.cwd();
124148

@@ -274,7 +298,16 @@ class FeatureToggles {
274298
);
275299
}
276300

277-
_reset({ uniqueName, redisChannel = DEFAULT_REDIS_CHANNEL, redisKey = DEFAULT_REDIS_KEY }) {
301+
/**
302+
* Implementation for {@link constructor}.
303+
*
304+
* @param {ConstructorOptions} [options]
305+
*/
306+
_reset({
307+
uniqueName = FeatureToggles._getDefaultUniqueName(),
308+
redisChannel = DEFAULT_REDIS_CHANNEL,
309+
redisKey = DEFAULT_REDIS_KEY,
310+
} = {}) {
278311
this.__redisChannel = uniqueName ? redisChannel + "-" + uniqueName : redisChannel;
279312
this.__redisKey = uniqueName ? redisKey + "-" + uniqueName : redisKey;
280313

@@ -292,9 +325,19 @@ class FeatureToggles {
292325
this.__isConfigProcessed = false;
293326
}
294327

295-
// NOTE: constructors cannot be async, so we need to split this state preparation part from the initialize part
296-
constructor({ uniqueName = undefined, redisChannel = DEFAULT_REDIS_CHANNEL, redisKey = DEFAULT_REDIS_KEY } = {}) {
297-
this._reset({ uniqueName, redisChannel, redisKey });
328+
/**
329+
* @typedef ConstructorOptions
330+
* @type object
331+
* @property {string} [uniqueName] unique name to prefix both Redis channel and key
332+
* @property {string} [redisChannel] channel for Redis pub/sub to propagate changes across servers
333+
* @property {string} [redisKey] key in Redis to save non-fallback values
334+
*/
335+
/**
336+
* NOTE: constructors cannot be async, so we need to split this state preparation part from the initialize part
337+
* @param {ConstructorOptions} [options]
338+
*/
339+
constructor(options) {
340+
this._reset(options);
298341
}
299342

300343
// ========================================
@@ -304,39 +347,14 @@ class FeatureToggles {
304347
// START OF SINGLETON SECTION
305348
// ========================================
306349

307-
static _getInstanceUniqueName() {
308-
if (ENV_UNIQUE_NAME) {
309-
return ENV_UNIQUE_NAME;
310-
}
311-
let cfApp;
312-
try {
313-
cfApp = cfEnv.cfApp;
314-
if (cfApp.application_name) {
315-
return cfApp.application_name;
316-
}
317-
} catch (err) {
318-
throw new VError(
319-
{
320-
name: VERROR_CLUSTER_NAME,
321-
cause: err,
322-
info: {
323-
cfApp: JSON.stringify(cfApp),
324-
},
325-
},
326-
"error determining cf app name"
327-
);
328-
}
329-
}
330-
331350
/**
332351
* Get singleton instance
333352
*
334353
* @returns {FeatureToggles}
335354
*/
336355
static getInstance() {
337356
if (!FeatureToggles.__instance) {
338-
const uniqueName = FeatureToggles._getInstanceUniqueName();
339-
FeatureToggles.__instance = new FeatureToggles({ uniqueName });
357+
FeatureToggles.__instance = new FeatureToggles();
340358
}
341359
return FeatureToggles.__instance;
342360
}
@@ -713,6 +731,11 @@ class FeatureToggles {
713731
);
714732
}
715733

734+
/**
735+
* Implementation for {@link initializeFeatures}.
736+
*
737+
* @param {InitializeOptions} [options]
738+
*/
716739
async _initializeFeatures({ config: configRuntime, configFile: configFilepath, configAuto } = {}) {
717740
if (this.__isInitialized) {
718741
return;
@@ -840,9 +863,23 @@ class FeatureToggles {
840863
return this;
841864
}
842865

866+
/**
867+
* TODO
868+
* @typedef Config
869+
* @type object
870+
*/
871+
/**
872+
* @typedef InitializeOptions
873+
* @type object
874+
* @property {Config} [config]
875+
* @property {string} [configFile]
876+
* @property {Config} [configAuto]
877+
*/
843878
/**
844879
* Initialize needs to run and finish before other APIs are called. It processes the configuration, sets up
845880
* related internal state, and starts communication with redis.
881+
*
882+
* @param {InitializeOptions} [options]
846883
*/
847884
async initializeFeatures(options) {
848885
if (!this.__initializePromise) {
@@ -919,11 +956,15 @@ class FeatureToggles {
919956
*/
920957
async getRemoteFeaturesInfos() {
921958
this._ensureInitialized();
959+
960+
let remoteStateScopedValues;
961+
// NOTE: for NO_REDIS mode, we show local updates
922962
if ((await redis.getIntegrationMode()) === REDIS_INTEGRATION_MODE.NO_REDIS) {
923-
return {};
963+
remoteStateScopedValues = this.__stateScopedValues ?? {};
964+
} else {
965+
remoteStateScopedValues = await redis.hashGetAllObjects(this.__redisKey);
924966
}
925967

926-
const remoteStateScopedValues = await redis.hashGetAllObjects(this.__redisKey);
927968
if (!remoteStateScopedValues) {
928969
return null;
929970
}

test/cds-service/__snapshots__/featureToggles.service.test.js.snap

Lines changed: 106 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// Jest Snapshot v1, https://goo.gl/fbAQLP
22

3-
exports[`cds-service endpoints state response 1`] = `
3+
exports[`cds-service endpoints state response no change 1`] = `
44
{
55
"test/feature_a": {
66
"config": {
@@ -99,3 +99,108 @@ exports[`cds-service endpoints state response 1`] = `
9999
},
100100
}
101101
`;
102+
103+
exports[`cds-service endpoints state response with changes 1`] = `
104+
{
105+
"test/feature_a": {
106+
"config": {
107+
"SOURCE": "RUNTIME",
108+
"TYPE": "boolean",
109+
},
110+
"fallbackValue": false,
111+
},
112+
"test/feature_aa": {
113+
"config": {
114+
"SOURCE": "RUNTIME",
115+
"TYPE": "boolean",
116+
"VALIDATIONS": [
117+
{
118+
"scopes": [
119+
"tenant",
120+
"user",
121+
],
122+
},
123+
],
124+
},
125+
"fallbackValue": false,
126+
},
127+
"test/feature_b": {
128+
"config": {
129+
"SOURCE": "RUNTIME",
130+
"TYPE": "number",
131+
},
132+
"fallbackValue": 1,
133+
"rootValue": 2,
134+
"scopedValues": {
135+
"tenant::a": 20,
136+
"tenant::b": 30,
137+
},
138+
},
139+
"test/feature_c": {
140+
"config": {
141+
"SOURCE": "RUNTIME",
142+
"TYPE": "string",
143+
},
144+
"fallbackValue": "best",
145+
},
146+
"test/feature_d": {
147+
"config": {
148+
"SOURCE": "RUNTIME",
149+
"TYPE": "boolean",
150+
"VALIDATIONS": [
151+
{
152+
"regex": "^(?:true)$",
153+
},
154+
],
155+
},
156+
"fallbackValue": true,
157+
},
158+
"test/feature_e": {
159+
"config": {
160+
"SOURCE": "RUNTIME",
161+
"TYPE": "number",
162+
"VALIDATIONS": [
163+
{
164+
"scopes": [
165+
"component",
166+
"layer",
167+
"tenant",
168+
],
169+
},
170+
{
171+
"regex": "^\\d{1}$",
172+
},
173+
],
174+
},
175+
"fallbackValue": 5,
176+
},
177+
"test/feature_f": {
178+
"config": {
179+
"SOURCE": "RUNTIME",
180+
"TYPE": "string",
181+
"VALIDATIONS": [
182+
{
183+
"regex": "^(?:best|worst)$",
184+
},
185+
],
186+
},
187+
"fallbackValue": "best",
188+
},
189+
"test/feature_g": {
190+
"config": {
191+
"ACTIVE": false,
192+
"SOURCE": "RUNTIME",
193+
"TYPE": "string",
194+
},
195+
"fallbackValue": "activeTest",
196+
},
197+
"test/feature_h": {
198+
"config": {
199+
"APP_URL": "\\.cfapps\\.sap\\.hana\\.ondemand\\.com$",
200+
"SOURCE": "RUNTIME",
201+
"TYPE": "string",
202+
},
203+
"fallbackValue": "appUrlTest",
204+
},
205+
}
206+
`;

0 commit comments

Comments
 (0)