Skip to content

Commit e2a12af

Browse files
authored
Fb/custom change handler no null value (#66)
* some tests for future behavior * some tests for future behavior 2 * trigger change handlers after internal state update * change handlers get the actual new value in case of deletion/resets * update tests * slightly more docs for change handlers
1 parent 3ab22dc commit e2a12af

File tree

3 files changed

+88
-9
lines changed

3 files changed

+88
-9
lines changed

docs/usage/index.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,14 @@ toggles.registerFeatureValueChangeHandler("/srv/util/logger/logLevel", async (ne
266266
});
267267
```
268268

269+
Some important caveats for change handlers:
270+
271+
- **scopes**: The handler will always receive the new value that is relevant for the scopeMap that is passed along.
272+
Meaning it will be called with new values that can be highly restricted and `getFeatureValue` calls with the same
273+
feature key can return different values for other scopeMaps or no scopeMap.
274+
- **deletion**: If values are deleted, by setting them to `null`, the change handler will receive the new actual
275+
value for the relevant scopeMap and not the change value of `null`.
276+
269277
{: .info }
270278
Registering any callback will not require that the feature toggles are initialized, so this can happen on top-level.
271279

src/featureToggles.js

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1296,14 +1296,26 @@ class FeatureToggles {
12961296
return;
12971297
}
12981298

1299-
await this._triggerChangeHandlers(featureKey, oldValue, newValue, scopeMap, options);
13001299
FeatureToggles._updateStateScopedValuesOneScopeInPlace(
13011300
this.__stateScopedValues,
13021301
featureKey,
13031302
newValue,
13041303
scopeKey,
13051304
options
13061305
);
1306+
1307+
// NOTE: the change handler expects the actual value.
1308+
const newActualValue =
1309+
newValue !== null
1310+
? newValue
1311+
: FeatureToggles._getFeatureValueForScopeAndStateAndFallback(
1312+
this.__superScopeCache,
1313+
this.__stateScopedValues,
1314+
this.__fallbackValues,
1315+
featureKey,
1316+
scopeMap
1317+
);
1318+
await this._triggerChangeHandlers(featureKey, oldValue, newActualValue, scopeMap, options);
13071319
} catch (err) {
13081320
logger.error(
13091321
new VError(
@@ -1344,14 +1356,24 @@ class FeatureToggles {
13441356
featureKey,
13451357
scopeMap
13461358
);
1347-
await this._triggerChangeHandlers(featureKey, oldValue, newValue, scopeMap, options);
13481359
FeatureToggles._updateStateScopedValuesOneScopeInPlace(
13491360
this.__stateScopedValues,
13501361
featureKey,
13511362
newValue,
13521363
scopeKey,
13531364
options
13541365
);
1366+
const newActualValue =
1367+
newValue !== null
1368+
? newValue
1369+
: FeatureToggles._getFeatureValueForScopeAndStateAndFallback(
1370+
this.__superScopeCache,
1371+
this.__stateScopedValues,
1372+
this.__fallbackValues,
1373+
featureKey,
1374+
scopeMap
1375+
);
1376+
await this._triggerChangeHandlers(featureKey, oldValue, newActualValue, scopeMap, options);
13551377
return;
13561378
}
13571379

@@ -1419,10 +1441,10 @@ class FeatureToggles {
14191441
*
14201442
* The change handler gets the new value as well as the old value, immediately after the update is propagated.
14211443
*
1422-
* @param {boolean | number | string | null} newValue
1423-
* @param {boolean | number | string} oldValue
1424-
* @param {Object} [scopeMap] optional scope restrictions
1425-
* @param {ChangeOptions} [options] optional switch to clear all sub scopes
1444+
* @param {boolean | number | string} newValue
1445+
* @param {boolean | number | string} oldValue
1446+
* @param {Object} [scopeMap] optional scope restrictions
1447+
* @param {ChangeOptions} [options] optional switch to clear all sub scopes
14261448
*/
14271449
/**
14281450
* Register given handler to receive changes of given feature value key.

test/featureToggles.test.js

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,6 @@ describe("feature toggles test", () => {
5353
redisWrapperMock._reset();
5454
envMock._reset();
5555
featureToggles = new FeatureToggles({ redisKey, redisChannel });
56-
});
57-
58-
afterEach(() => {
5956
jest.clearAllMocks();
6057
});
6158

@@ -1223,4 +1220,56 @@ describe("feature toggles test", () => {
12231220
expect(featureToggles.getFeatureValue(FEATURE.E, scopeMap)).toMatchInlineSnapshot(`9`);
12241221
});
12251222
});
1223+
1224+
describe("changeHandler details", () => {
1225+
const handler = jest.fn();
1226+
const tenant = "t1";
1227+
const user = "u1";
1228+
const fallbackValue = mockFallbackValues[FEATURE.C];
1229+
const rootValue = "root";
1230+
const tenantValue = "t1";
1231+
const tenantUserValue = "t1u1";
1232+
1233+
beforeEach(async () => {
1234+
await featureToggles.initializeFeatures({ config: mockConfig });
1235+
featureToggles.registerFeatureValueChangeHandler(FEATURE.C, handler);
1236+
await featureToggles.changeFeatureValue(FEATURE.C, rootValue);
1237+
await featureToggles.changeFeatureValue(FEATURE.C, tenantValue, { tenant });
1238+
await featureToggles.changeFeatureValue(FEATURE.C, tenantUserValue, { tenant, user });
1239+
jest.clearAllMocks();
1240+
});
1241+
1242+
test("reset tenantUser should fall back to tenant", async () => {
1243+
await featureToggles.changeFeatureValue(FEATURE.C, null, { tenant, user });
1244+
expect(handler).toHaveBeenCalledTimes(1);
1245+
expect(handler).toHaveBeenCalledWith(tenantValue, tenantUserValue, { tenant, user }, undefined);
1246+
});
1247+
test("reset tenant should fall back to root", async () => {
1248+
await featureToggles.changeFeatureValue(FEATURE.C, null, { tenant });
1249+
expect(handler).toHaveBeenCalledTimes(1);
1250+
expect(handler).toHaveBeenCalledWith(rootValue, tenantValue, { tenant }, undefined);
1251+
});
1252+
test("reset root should fall back to fallback", async () => {
1253+
await featureToggles.changeFeatureValue(FEATURE.C, null);
1254+
expect(handler).toHaveBeenCalledTimes(1);
1255+
expect(handler).toHaveBeenCalledWith(fallbackValue, rootValue, undefined, undefined);
1256+
});
1257+
test("reset with clearSubScopes root should fall back to fallback", async () => {
1258+
await featureToggles.changeFeatureValue(FEATURE.C, null, undefined, { clearSubScopes: true });
1259+
expect(handler).toHaveBeenCalledTimes(1);
1260+
expect(handler).toHaveBeenCalledWith(fallbackValue, rootValue, undefined, { clearSubScopes: true });
1261+
});
1262+
1263+
test("get in changeHandler should give new value", async () => {
1264+
let newValueCheck;
1265+
const newTenantUserValue = "super";
1266+
handler.mockImplementationOnce(() => {
1267+
newValueCheck = featureToggles.getFeatureValue(FEATURE.C, { tenant, user });
1268+
});
1269+
await featureToggles.changeFeatureValue(FEATURE.C, newTenantUserValue, { tenant, user });
1270+
expect(newValueCheck).toBe(newTenantUserValue);
1271+
expect(handler).toHaveBeenCalledTimes(1);
1272+
expect(handler).toHaveBeenCalledWith(newTenantUserValue, tenantUserValue, { tenant, user }, undefined);
1273+
});
1274+
});
12261275
});

0 commit comments

Comments
 (0)