@tehw0lf/yaft 0.0.17 → 0.0.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -129,6 +129,16 @@ rules can be applied directly to a feature without going through a provider.
129
129
  A missing feature is off. Unset, `null` or unparseable dates are ignored rather
130
130
  than treated as an error, and never throw.
131
131
 
132
+ ### API providers and a failing backend
133
+
134
+ `ApiServiceFeatureProvider` and `ApiServiceBooleanProvider` keep their data
135
+ when a refresh fails — and, **since 0.0.18**, also when `/features` answers
136
+ `200` with something that is not a toggle group (`null`, an array, a proxy's
137
+ error page). Before, that replaced the data with nothing and switched every
138
+ feature off. A failed fetch is now also retried on the next
139
+ `getCollectionHash`; before, one failure stopped refreshing until the backend
140
+ changed again. `normaliseGroup` is exported for providers of your own.
141
+
132
142
  ### Boolean shape
133
143
 
134
144
  The boolean providers hold `{ "myToggle": true }` and have no time logic. Only
@@ -7,5 +7,7 @@ export declare class ApiServiceBooleanProvider implements FeatureProvider<boolea
7
7
  constructor(apiUrl: string, baseUUID: string);
8
8
  getCollectionHash(configPathOrUrl: string): Promise<void>;
9
9
  getConfig(configPathOrUrl: string): Promise<void>;
10
+ /** Loads the group and reports whether it did; on failure the data stays. */
11
+ private loadGroup;
10
12
  isEnabled(key: string): boolean;
11
13
  }
@@ -20,9 +20,9 @@ class ApiServiceBooleanProvider {
20
20
  try {
21
21
  const response = await axios_1.default.get(configPathOrUrl);
22
22
  const newHash = response.data.collectionHash || response.data.value;
23
- if (this.collectionHash !== newHash) {
23
+ // Recorded only once the group has loaded, so a failed fetch is retried.
24
+ if (this.collectionHash !== newHash && (await this.loadGroup(`${this.apiUrl}/features/${this.baseUUID}`))) {
24
25
  this.collectionHash = newHash;
25
- await this.getConfig(`${this.apiUrl}/features/${this.baseUUID}`);
26
26
  }
27
27
  }
28
28
  catch (error) {
@@ -30,6 +30,10 @@ class ApiServiceBooleanProvider {
30
30
  }
31
31
  }
32
32
  async getConfig(configPathOrUrl) {
33
+ await this.loadGroup(configPathOrUrl);
34
+ }
35
+ /** Loads the group and reports whether it did; on failure the data stays. */
36
+ async loadGroup(configPathOrUrl) {
33
37
  try {
34
38
  const response = await axios_1.default.get(configPathOrUrl);
35
39
  // A keyed boolean object is already in this provider's shape and is
@@ -38,7 +42,7 @@ class ApiServiceBooleanProvider {
38
42
  // about what a response means.
39
43
  if (isKeyedBooleans(response.data)) {
40
44
  this.data = (0, mapping_1.normaliseBooleans)(response.data);
41
- return;
45
+ return true;
42
46
  }
43
47
  // Only the value matters here. The boolean shape has no time logic by
44
48
  // design (R21), so a feature collapses to whether its value is exactly
@@ -48,14 +52,20 @@ class ApiServiceBooleanProvider {
48
52
  // schedules toggles: the window is then enforced only by the backend's
49
53
  // cron job, which lags by up to a minute, instead of being evaluated
50
54
  // locally. Use ApiServiceFeatureProvider when the toggles carry dates.
51
- const features = (0, mapping_1.normaliseCollection)(response.data);
55
+ const features = (0, mapping_1.normaliseGroup)(response.data);
56
+ if (features === undefined) {
57
+ console.error("Ignoring a response that is not a toggle group:", response.data);
58
+ return false;
59
+ }
52
60
  this.data = {};
53
61
  for (const [key, feature] of Object.entries(features)) {
54
62
  this.data[key] = feature.value === 'true';
55
63
  }
64
+ return true;
56
65
  }
57
66
  catch (error) {
58
67
  console.error("Failed to fetch feature toggle from API:", error);
68
+ return false;
59
69
  }
60
70
  }
61
71
  isEnabled(key) {
@@ -13,5 +13,7 @@ export declare class ApiServiceFeatureProvider implements FeatureProvider<Featur
13
13
  constructor(apiUrl: string, baseUUID: string, clock?: Clock);
14
14
  getCollectionHash(configPathOrUrl: string): Promise<void>;
15
15
  getConfig(configPathOrUrl: string): Promise<void>;
16
+ /** Loads the group and reports whether it did; on failure the data stays. */
17
+ private loadGroup;
16
18
  isEnabled(key: string): boolean;
17
19
  }
@@ -27,9 +27,11 @@ class ApiServiceFeatureProvider {
27
27
  try {
28
28
  const response = await axios_1.default.get(configPathOrUrl);
29
29
  const newHash = response.data.collectionHash || response.data.value;
30
- if (this.collectionHash !== newHash) {
30
+ // The hash is recorded only once the group has loaded. Recording it
31
+ // first meant one failed fetch stopped every later refresh until the
32
+ // backend changed again.
33
+ if (this.collectionHash !== newHash && (await this.loadGroup(`${this.apiUrl}/features/${this.baseUUID}`))) {
31
34
  this.collectionHash = newHash;
32
- await this.getConfig(`${this.apiUrl}/features/${this.baseUUID}`);
33
35
  }
34
36
  }
35
37
  catch (error) {
@@ -37,14 +39,25 @@ class ApiServiceFeatureProvider {
37
39
  }
38
40
  }
39
41
  async getConfig(configPathOrUrl) {
42
+ await this.loadGroup(configPathOrUrl);
43
+ }
44
+ /** Loads the group and reports whether it did; on failure the data stays. */
45
+ async loadGroup(configPathOrUrl) {
40
46
  try {
41
47
  const response = await axios_1.default.get(configPathOrUrl);
42
48
  // The mapping rules live in the core, next to the evaluation rules,
43
49
  // rather than being reimplemented per provider.
44
- this.data = (0, mapping_1.normaliseCollection)(response.data);
50
+ const group = (0, mapping_1.normaliseGroup)(response.data);
51
+ if (group === undefined) {
52
+ console.error("Ignoring a response that is not a toggle group:", response.data);
53
+ return false;
54
+ }
55
+ this.data = group;
56
+ return true;
45
57
  }
46
58
  catch (error) {
47
59
  console.error("Failed to fetch feature toggle from API:", error);
60
+ return false;
48
61
  }
49
62
  }
50
63
  isEnabled(key) {
package/index.d.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  export { Feature, FeatureToggle, FeatureToggleBase, FeatureProvider, } from "./FeatureToggle";
2
2
  export { Clock, evaluate, parseTimestamp, systemClock } from "./evaluate";
3
- export { normaliseBooleans, normaliseCollection, normaliseFeature } from "./mapping";
3
+ export { normaliseBooleans, normaliseCollection, normaliseFeature, normaliseGroup } from "./mapping";
package/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.normaliseFeature = exports.normaliseCollection = exports.normaliseBooleans = exports.systemClock = exports.parseTimestamp = exports.evaluate = exports.FeatureToggleBase = exports.FeatureToggle = void 0;
3
+ exports.normaliseGroup = exports.normaliseFeature = exports.normaliseCollection = exports.normaliseBooleans = exports.systemClock = exports.parseTimestamp = exports.evaluate = exports.FeatureToggleBase = exports.FeatureToggle = void 0;
4
4
  var FeatureToggle_1 = require("./FeatureToggle");
5
5
  Object.defineProperty(exports, "FeatureToggle", { enumerable: true, get: function () { return FeatureToggle_1.FeatureToggle; } });
6
6
  Object.defineProperty(exports, "FeatureToggleBase", { enumerable: true, get: function () { return FeatureToggle_1.FeatureToggleBase; } });
@@ -12,3 +12,4 @@ var mapping_1 = require("./mapping");
12
12
  Object.defineProperty(exports, "normaliseBooleans", { enumerable: true, get: function () { return mapping_1.normaliseBooleans; } });
13
13
  Object.defineProperty(exports, "normaliseCollection", { enumerable: true, get: function () { return mapping_1.normaliseCollection; } });
14
14
  Object.defineProperty(exports, "normaliseFeature", { enumerable: true, get: function () { return mapping_1.normaliseFeature; } });
15
+ Object.defineProperty(exports, "normaliseGroup", { enumerable: true, get: function () { return mapping_1.normaliseGroup; } });
package/mapping.d.ts CHANGED
@@ -24,6 +24,17 @@ export declare function normaliseFeature(raw: RawFeature): Feature;
24
24
  * where `isEnabled("")` could reach it.
25
25
  */
26
26
  export declare function normaliseCollection(response: unknown): Record<string, Feature>;
27
+ /**
28
+ * Normalises a /features response, but only if it is recognisably a toggle
29
+ * group: a collection envelope -- an empty one is a valid empty group -- or a
30
+ * single toggle. Returns `undefined` for anything else.
31
+ *
32
+ * `normaliseCollection` turns null, an array, a proxy's error object or a
33
+ * collection whose entries are all unusable into `{}`. A provider storing that
34
+ * would switch every feature off without an error; this is what it should
35
+ * check instead. Unusable entries next to good ones are still skipped (R25).
36
+ */
37
+ export declare function normaliseGroup(response: unknown): Record<string, Feature> | undefined;
27
38
  /**
28
39
  * Normalises a boolean-shape payload, `{ "myToggle": true }`.
29
40
  *
package/mapping.js CHANGED
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.normaliseFeature = normaliseFeature;
4
4
  exports.normaliseCollection = normaliseCollection;
5
+ exports.normaliseGroup = normaliseGroup;
5
6
  exports.normaliseBooleans = normaliseBooleans;
6
7
  /**
7
8
  * Reads a field by presence, not by truthiness.
@@ -78,6 +79,31 @@ function normaliseCollection(response) {
78
79
  }
79
80
  return data;
80
81
  }
82
+ /**
83
+ * Normalises a /features response, but only if it is recognisably a toggle
84
+ * group: a collection envelope -- an empty one is a valid empty group -- or a
85
+ * single toggle. Returns `undefined` for anything else.
86
+ *
87
+ * `normaliseCollection` turns null, an array, a proxy's error object or a
88
+ * collection whose entries are all unusable into `{}`. A provider storing that
89
+ * would switch every feature off without an error; this is what it should
90
+ * check instead. Unusable entries next to good ones are still skipped (R25).
91
+ */
92
+ function normaliseGroup(response) {
93
+ if (response === null || typeof response !== 'object' || Array.isArray(response)) {
94
+ return undefined;
95
+ }
96
+ const data = normaliseCollection(response);
97
+ if (Object.keys(data).length > 0)
98
+ return data;
99
+ const body = response;
100
+ const collection = Array.isArray(body['toggles'])
101
+ ? body['toggles']
102
+ : Array.isArray(body['value'])
103
+ ? body['value']
104
+ : undefined;
105
+ return collection !== undefined && collection.length === 0 ? data : undefined;
106
+ }
81
107
  /**
82
108
  * Normalises a boolean-shape payload, `{ "myToggle": true }`.
83
109
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tehw0lf/yaft",
3
- "version": "0.0.17",
3
+ "version": "0.0.18",
4
4
  "description": "YaFT - Feature Toggles using Go&PostgreSQL or any source!",
5
5
  "type": "commonjs",
6
6
  "main": "index.js",