Skip to content

Commit 239f37c

Browse files
authored
Fb/better interface (#40)
* start of main export re-write * more small re-write stuff * polish some ide warnings * remove unused interface * start with tests * more re-work and polish * tests work again * remove dynamic controller * new api in example cap server * re-write docs for new interface * some consistency fixes * use shorter toggles config naming in docs * slightly better text in jsdocs * fix jsdocs for function with spread arg in signature * use constructor, when you need the class * remove unused code * more cleanup * again re-naming the default config filepath * remove tests alongside code for expiring lazy cache * remove old static class field * one more change because you cannot call the static method readConfigFromFile on an instance * remove unused imports * go back to named import over spreading and constructor access
1 parent 58febd0 commit 239f37c

26 files changed

+258
-762
lines changed

cds-plugin.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
const cds = require("@sap/cds");
55
const cdsPackage = require("@sap/cds/package.json");
6-
const { initializeFeatures, getFeaturesKeys, getFeatureValue } = require("./src/singleton");
6+
const toggles = require("./src/");
77
const { closeMainClient, closeSubscriberClient } = require("./src/redisWrapper");
88

99
const FEATURE_KEY_REGEX = /\/fts\/([^\s/]+)$/;
@@ -23,7 +23,7 @@ const _registerFeatureProvider = () => {
2323
if (!cds.env.requires?.toggles) {
2424
return;
2525
}
26-
const cdsFeatures = getFeaturesKeys().reduce((result, key) => {
26+
const cdsFeatures = toggles.getFeaturesKeys().reduce((result, key) => {
2727
const match = FEATURE_KEY_REGEX.exec(key);
2828
if (match) {
2929
const feature = match[1];
@@ -42,7 +42,7 @@ const _registerFeatureProvider = () => {
4242
req.headers.features ||
4343
cds.context?.user?.features ||
4444
cdsFeatures.reduce((result, [key, feature]) => {
45-
if (getFeatureValue(key, { user, tenant })) {
45+
if (toggles.getFeatureValue(key, { user, tenant })) {
4646
result.push(feature);
4747
}
4848
return result;
@@ -68,7 +68,7 @@ const activate = async () => {
6868
_registerClientCloseOnShutdown();
6969

7070
// TODO for the "cds build" use case, this initialize makes no sense
71-
await initializeFeatures({
71+
await toggles.initializeFeatures({
7272
config: envFeatureToggles.config,
7373
configFile: envFeatureToggles.configFile,
7474
});

docs/usage/index.md

Lines changed: 31 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,12 @@ const { FeatureToggles } = require("@cap-js-community/feature-toggle-library");
2525
const instance = new FeatureToggles({ uniqueName: "snowflake" });
2626
```
2727

28-
The library prepares a convenient `singleton` instance of FeatureToggles for out-of-the-box usage, where the Cloud
29-
Foundry _app name_ is used as unique name. This should be sufficient for most use-cases.
28+
The library prepares a convenient singleton instance of FeatureToggles for out-of-the-box usage, where the Cloud
29+
Foundry _app name_ is used as unique name. This should be sufficient for most use-cases and is the default export of
30+
the library.
3031

3132
```javascript
32-
const { singleton } = require("@cap-js-community/feature-toggle-library");
33+
const toggles = require("@cap-js-community/feature-toggle-library");
3334
```
3435

3536
{: .warn }
@@ -134,8 +135,8 @@ initialization settings are in `package.json`. For example:
134135
```
135136

136137
In this example, the path `./srv/feature/feature.yaml` points to the previously discussed configuration file. With
137-
these settings in place, the `singleton` instance of the library will be initialized and is ready for usage at and
138-
after the [bootstrap](https://cap.cloud.sap/cap/docs/node.js/cds-server#bootstrap) event.
138+
these settings in place, the singleton instance of the library will be initialized and is ready for usage at and after
139+
the [bootstrap](https://cap.cloud.sap/cap/docs/node.js/cds-server#bootstrap) event.
139140

140141
{: .info }
141142
Using the feature toggles in CAP projects also enables a [REST service]({{ site.baseurl }}/plugin/), where toggles can
@@ -147,24 +148,20 @@ Other projects will need to use the corresponding filepath, in order to initiali
147148

148149
```javascript
149150
const pathlib = require("path");
150-
const {
151-
singleton: { initializeFeatures },
152-
} = require("@cap-js-community/feature-toggle-library");
153-
const FEATURES_FILEPATH = pathlib.join(__dirname, "featureTogglesConfig.yml");
151+
const toggles = require("@cap-js-community/feature-toggle-library");
152+
const FEATURES_FILEPATH = pathlib.join(__dirname, ".toggles.yml");
154153
155154
// ... during application bootstrap
156-
await initializeFeatures({ configFile: FEATURES_FILEPATH });
155+
await toggles.initializeFeatures({ configFile: FEATURES_FILEPATH });
157156
```
158157

159158
Alternatively, a runtime configuration object is also supported.
160159

161160
```javascript
162-
const {
163-
singleton: { initializeFeatures },
164-
} = require("@cap-js-community/feature-toggle-library");
161+
const toggles = require("@cap-js-community/feature-toggle-library");
165162
166163
// ... during application bootstrap
167-
await initializeFeatures({
164+
await toggles.initializeFeatures({
168165
config: {
169166
runNewCode: {
170167
type: "boolean",
@@ -183,16 +180,14 @@ feature toggles.
183180

184181
```javascript
185182
const pathlib = require("path");
186-
const {
187-
singleton: { initializeFeatures },
188-
readConfigFromFile,
189-
} = require("@cap-js-community/feature-toggle-library");
190-
const FEATURES_FILEPATH = pathlib.join(__dirname, "featureTogglesConfig.yml");
183+
const toggles = require("@cap-js-community/feature-toggle-library");
184+
const FEATURES_FILEPATH = pathlib.join(__dirname, ".toggles.yml");
185+
const { FeatureToggles } = toggles;
191186
192187
// ... during application bootstrap
193-
const config = await readConfigFromFile(FEATURES_FILEPATH);
188+
const config = await FeatureToggles.readConfigFromFile(FEATURES_FILEPATH);
194189
// ... manipulate
195-
await initializeFeatures({ config });
190+
await toggles.initializeFeatures({ config });
196191
```
197192

198193
## Integration Mode
@@ -232,41 +227,36 @@ a feature toggle with the key `/srv/util/logger/logLevel`, similar to the one de
232227
You can read the current in memory state of any feature toggle:
233228

234229
```javascript
235-
const {
236-
singleton: { getFeatureValue },
237-
} = require("@cap-js-community/feature-toggle-library");
230+
const toggles = require("@cap-js-community/feature-toggle-library");
238231
239232
// ... in some function
240-
const logLevel = getFeatureValue("/srv/util/logger/logLevel");
233+
const logLevel = toggles.getFeatureValue("/srv/util/logger/logLevel");
241234
242235
// ... with runtime scope information
243-
const logLevel = getFeatureValue("/srv/util/logger/logLevel", {
236+
const logLevel = toggles.getFeatureValue("/srv/util/logger/logLevel", {
244237
tenant: cds.context.tenant,
245238
user: cds.context.user.id,
246239
});
247240
```
248241

249242
{: .warn }
250243
While `getFeatureValue` is synchronous, and could happen on the top-level of a module. The function will throw, if it
251-
is called _before_ `initializeFeatures`, which is asynchronous. So, it's never sensible to have this on
252-
top-level.
244+
is called _before_ `initializeFeatures`, which is asynchronous. So, it's never sensible to have this on top-level.
253245

254246
### Observing Feature Value Changes
255247

256248
You can register a callback for all updates to a feature toggle:
257249

258250
```javascript
259-
const {
260-
singleton: { registerFeatureValueChangeHandler },
261-
} = require("@cap-js-community/feature-toggle-library");
251+
const toggles = require("@cap-js-community/feature-toggle-library");
262252
263-
registerFeatureValueChangeHandler("/srv/util/logger/logLevel", (newValue, oldValue, scopeMap) => {
253+
toggles.registerFeatureValueChangeHandler("/srv/util/logger/logLevel", (newValue, oldValue, scopeMap) => {
264254
console.log("changing log level from %s to %s (scope %j)", oldValue, newValue, scopeMap);
265255
updateLogLevel(newValue);
266256
});
267257
268258
// ... or for async APIs
269-
registerFeatureValueChangeHandler("/srv/util/logger/logLevel", async (newValue, oldValue, scopeMap) => {
259+
toggles.registerFeatureValueChangeHandler("/srv/util/logger/logLevel", async (newValue, oldValue, scopeMap) => {
270260
await updateLogLevel(newValue);
271261
});
272262
```
@@ -279,13 +269,11 @@ Registering any callback will not require that the feature toggles are initializ
279269
Finally, updating the feature toggle value:
280270

281271
```javascript
282-
const {
283-
singleton: { changeFeatureValue },
284-
} = require("@cap-js-community/feature-toggle-library");
272+
const toggles = require("@cap-js-community/feature-toggle-library");
285273
286274
// optionally pass in a scopeMap, which describes the least specific scope where the change should happen
287275
async function changeIt(newValue, scopeMap) {
288-
const validationErrors = await changeFeatureValue("/srv/util/logger/logLevel", newValue, scopeMap);
276+
const validationErrors = await toggles.changeFeatureValue("/srv/util/logger/logLevel", newValue, scopeMap);
289277
if (Array.isArray(validationErrors) && validationErrors.length > 0) {
290278
for (const { errorMessage, errorMessageValues } of validationErrors) {
291279
// show errors to the user, the change did not happen
@@ -306,7 +294,7 @@ scope-combination, which overrides that value, then you can use the option `{ cl
306294
argument. For example
307295

308296
```javascript
309-
await changeFeatureValue("/srv/util/logger/logLevel", "error", {}, { clearSubScopes: true });
297+
await toggles.changeFeatureValue("/srv/util/logger/logLevel", "error", {}, { clearSubScopes: true });
310298
```
311299

312300
will set the root-scope value to `"error"` and remove all sub-scopes. See
@@ -318,15 +306,13 @@ There is a convenience reset API just to reset a feature toggle and remove all a
318306
the feature toggle afterward will only yield the fallback value until new changes are made.
319307

320308
```javascript
321-
const {
322-
singleton: { resetFeatureValue, changeFeatureValue },
323-
} = require("@cap-js-community/feature-toggle-library");
309+
const toggles = require("@cap-js-community/feature-toggle-library");
324310
325311
// ... in some function
326-
await resetFeatureValue("/srv/util/logger/logLevel");
312+
await toggles.resetFeatureValue("/srv/util/logger/logLevel");
327313
328314
// this is functionally equivalent to
329-
await changeFeatureValue("/srv/util/logger/logLevel", null, {}, { clearSubScopes: true });
315+
await toggles.changeFeatureValue("/srv/util/logger/logLevel", null, {}, { clearSubScopes: true });
330316
```
331317

332318
### External Validation
@@ -335,11 +321,9 @@ The `string`-type feature toggles can theoretically encode very complex data str
335321
inputs in-depth before allowing changes to be published and propagated.
336322

337323
```javascript
338-
const {
339-
singleton: { registerFeatureValueValidation },
340-
} = require("@cap-js-community/feature-toggle-library");
324+
const toggles = require("@cap-js-community/feature-toggle-library");
341325
342-
registerFeatureValueValidation("/srv/util/logger/logLevel", (newValue) => {
326+
toggles.registerFeatureValueValidation("/srv/util/logger/logLevel", (newValue) => {
343327
if (isBad(newValue)) {
344328
return { errorMessage: "got bad value" };
345329
}

example-cap-server/srv/memoryStatistics.js

Lines changed: 15 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,11 @@
33
const { format } = require("util");
44

55
const cds = require("@sap/cds");
6-
const {
7-
singleton: { registerFeatureValueChangeHandler, getFeatureValue },
8-
DynamicIntervalController,
9-
} = require("@cap-js-community/feature-toggle-library");
6+
const toggles = require("@cap-js-community/feature-toggle-library");
107
const { MEM_STAT_LOG_INTERVAL } = require("./feature");
118

129
const logger = cds.log("memoryStatistics");
10+
let intervalId;
1311

1412
const _logStatistics = () => {
1513
const memoryUsage = process.memoryUsage?.();
@@ -18,20 +16,20 @@ const _logStatistics = () => {
1816
}
1917
};
2018

19+
const _updateValue = (newValue) => {
20+
if (intervalId) {
21+
clearInterval(intervalId);
22+
}
23+
if (newValue > 0) {
24+
intervalId = setInterval(_logStatistics, newValue);
25+
}
26+
};
27+
2128
const initializeMemoryStatistics = () => {
22-
const value = getFeatureValue(MEM_STAT_LOG_INTERVAL);
23-
const logIntervalController = new DynamicIntervalController(_logStatistics, value > 0, value);
24-
25-
registerFeatureValueChangeHandler(MEM_STAT_LOG_INTERVAL, (newValue) => {
26-
if (newValue <= 0) {
27-
logIntervalController.setActive(false);
28-
} else {
29-
logIntervalController.setWaitInterval(newValue);
30-
logIntervalController.setActive(true);
31-
}
32-
});
33-
34-
return logIntervalController;
29+
const value = toggles.getFeatureValue(MEM_STAT_LOG_INTERVAL);
30+
_updateValue(value);
31+
32+
toggles.registerFeatureValueChangeHandler(MEM_STAT_LOG_INTERVAL, _updateValue);
3533
};
3634

3735
module.exports = {

example-cap-server/srv/service/check-service.js

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
"use strict";
22

3-
const {
4-
singleton: { getFeatureValue },
5-
} = require("@cap-js-community/feature-toggle-library");
3+
const toggles = require("@cap-js-community/feature-toggle-library");
64

75
const { CHECK_API_PRIORITY } = require("../feature");
86

@@ -17,7 +15,7 @@ const HIGH_BOUNDARY = 100;
1715
const priorityHandler = async (context) => {
1816
const { "CheckService.priority": priority } = context.model.definitions;
1917
const isToggled = Boolean(priority["@marked"]);
20-
const value = getFeatureValue(CHECK_API_PRIORITY, { user: context.user.id, tenant: context.tenant });
18+
const value = toggles.getFeatureValue(CHECK_API_PRIORITY, { user: context.user.id, tenant: context.tenant });
2119
const messages =
2220
value >= HIGH_BOUNDARY
2321
? HIGH_VALUE_RESPONSES

src/env.js

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,7 @@
33
const { ENV } = require("./shared/static");
44

55
class CfEnv {
6-
isOnCf;
7-
cfApp;
8-
cfServices;
9-
cfInstanceGuid;
10-
cfInstanceIp;
11-
cfInstanceIndex;
6+
static __instance;
127

138
static parseEnvVar(env, envVar) {
149
try {
@@ -35,9 +30,9 @@ class CfEnv {
3530
}
3631

3732
/**
38-
* @return CfEnv
33+
* @returns {CfEnv}
3934
*/
40-
static get instance() {
35+
static getInstance() {
4136
if (!CfEnv.__instance) {
4237
CfEnv.__instance = new CfEnv();
4338
}
@@ -57,7 +52,4 @@ class CfEnv {
5752
}
5853
}
5954

60-
module.exports = {
61-
CfEnv,
62-
cfEnv: CfEnv.instance,
63-
};
55+
module.exports = CfEnv.getInstance();

0 commit comments

Comments
 (0)