@zudojs/feature-flags 0.1.0 → 1.1.0
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/LICENSE +21 -0
- package/README.md +171 -13
- package/dist/evaluator/evaluator.core.d.ts +15 -1
- package/dist/evaluator/evaluator.core.js +48 -10
- package/dist/evaluator/evaluatorAttribute.d.ts +5 -0
- package/dist/evaluator/evaluatorAttribute.js +62 -4
- package/dist/evaluator/evaluatorRule.core.js +32 -7
- package/dist/featureFlagTypes/featureFlagProvider.d.ts +10 -0
- package/dist/featureFlagTypes/index.d.ts +1 -1
- package/dist/featureFlags/featureFlags.core.d.ts +28 -8
- package/dist/featureFlags/featureFlags.core.js +101 -34
- package/dist/featureFlags/featureFlags.resolve.d.ts +4 -3
- package/dist/featureFlags/featureFlags.resolve.js +29 -12
- package/dist/index.d.ts +4 -1
- package/dist/index.js +1 -0
- package/dist/provider/providerCached.core.d.ts +3 -3
- package/dist/provider/providerCached.core.js +22 -3
- package/dist/provider/providerComposite.core.d.ts +2 -2
- package/dist/provider/providerComposite.core.js +62 -14
- package/dist/provider/providerEnvironment.core.d.ts +3 -3
- package/dist/provider/providerEnvironment.core.js +9 -4
- package/dist/provider/providerMemory.core.d.ts +18 -2
- package/dist/provider/providerMemory.core.js +38 -4
- package/package.json +26 -14
- package/dist/evaluator/evaluator.core.d.ts.map +0 -1
- package/dist/evaluator/evaluator.core.js.map +0 -1
- package/dist/evaluator/evaluatorAttribute.d.ts.map +0 -1
- package/dist/evaluator/evaluatorAttribute.js.map +0 -1
- package/dist/evaluator/evaluatorRule.core.d.ts.map +0 -1
- package/dist/evaluator/evaluatorRule.core.js.map +0 -1
- package/dist/evaluator/index.d.ts.map +0 -1
- package/dist/evaluator/index.js.map +0 -1
- package/dist/featureFlagErrors/featureFlagError.base.d.ts.map +0 -1
- package/dist/featureFlagErrors/featureFlagError.base.js.map +0 -1
- package/dist/featureFlagErrors/featureFlagError.types.d.ts.map +0 -1
- package/dist/featureFlagErrors/featureFlagError.types.js.map +0 -1
- package/dist/featureFlagErrors/index.d.ts.map +0 -1
- package/dist/featureFlagErrors/index.js.map +0 -1
- package/dist/featureFlagTypes/featureFlag.interface.d.ts.map +0 -1
- package/dist/featureFlagTypes/featureFlag.interface.js.map +0 -1
- package/dist/featureFlagTypes/featureFlagContext.d.ts.map +0 -1
- package/dist/featureFlagTypes/featureFlagContext.js.map +0 -1
- package/dist/featureFlagTypes/featureFlagEvaluation.d.ts.map +0 -1
- package/dist/featureFlagTypes/featureFlagEvaluation.js.map +0 -1
- package/dist/featureFlagTypes/featureFlagProvider.d.ts.map +0 -1
- package/dist/featureFlagTypes/featureFlagProvider.js.map +0 -1
- package/dist/featureFlagTypes/featureFlagRule/featureFlagRule.type.d.ts.map +0 -1
- package/dist/featureFlagTypes/featureFlagRule/featureFlagRule.type.js.map +0 -1
- package/dist/featureFlagTypes/featureFlagRule/featureFlagValue.type.d.ts.map +0 -1
- package/dist/featureFlagTypes/featureFlagRule/featureFlagValue.type.js.map +0 -1
- package/dist/featureFlagTypes/featureFlagRule/index.d.ts.map +0 -1
- package/dist/featureFlagTypes/featureFlagRule/index.js.map +0 -1
- package/dist/featureFlagTypes/index.d.ts.map +0 -1
- package/dist/featureFlagTypes/index.js.map +0 -1
- package/dist/featureFlags/featureFlags.core.d.ts.map +0 -1
- package/dist/featureFlags/featureFlags.core.js.map +0 -1
- package/dist/featureFlags/featureFlags.resolve.d.ts.map +0 -1
- package/dist/featureFlags/featureFlags.resolve.js.map +0 -1
- package/dist/featureFlags/index.d.ts.map +0 -1
- package/dist/featureFlags/index.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/provider/index.d.ts.map +0 -1
- package/dist/provider/index.js.map +0 -1
- package/dist/provider/providerCached.core.d.ts.map +0 -1
- package/dist/provider/providerCached.core.js.map +0 -1
- package/dist/provider/providerComposite.core.d.ts.map +0 -1
- package/dist/provider/providerComposite.core.js.map +0 -1
- package/dist/provider/providerEnvironment.core.d.ts.map +0 -1
- package/dist/provider/providerEnvironment.core.js.map +0 -1
- package/dist/provider/providerMemory.core.d.ts.map +0 -1
- package/dist/provider/providerMemory.core.js.map +0 -1
- package/dist/registry/index.d.ts.map +0 -1
- package/dist/registry/index.js.map +0 -1
- package/dist/registry/registry.core.d.ts.map +0 -1
- package/dist/registry/registry.core.js.map +0 -1
- package/dist/rollout/index.d.ts.map +0 -1
- package/dist/rollout/index.js.map +0 -1
- package/dist/rollout/rolloutBucketing.d.ts.map +0 -1
- package/dist/rollout/rolloutBucketing.js.map +0 -1
- package/dist/rollout/rolloutHashing.d.ts.map +0 -1
- package/dist/rollout/rolloutHashing.js.map +0 -1
- package/dist/utils/index.d.ts.map +0 -1
- package/dist/utils/index.js.map +0 -1
- package/dist/utils/utils.helper.d.ts.map +0 -1
- package/dist/utils/utils.helper.js.map +0 -1
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { createFeatureFlagRegistry } from "../registry/registry.core.js";
|
|
9
9
|
import { evaluateFlag } from "../evaluator/evaluator.core.js";
|
|
10
|
-
import { FeatureFlagNotFoundError } from "../featureFlagErrors/featureFlagError.types.js";
|
|
10
|
+
import { FeatureFlagNotFoundError, FeatureFlagProviderError, } from "../featureFlagErrors/featureFlagError.types.js";
|
|
11
11
|
import { resolveDependencies, mergeContext } from "./featureFlags.resolve.js";
|
|
12
12
|
/**
|
|
13
13
|
* Create a FeatureFlags instance.
|
|
@@ -16,45 +16,103 @@ import { resolveDependencies, mergeContext } from "./featureFlags.resolve.js";
|
|
|
16
16
|
* @returns A FeatureFlags API object.
|
|
17
17
|
*/
|
|
18
18
|
export function createFeatureFlags(options) {
|
|
19
|
-
const { provider, defaultContext = {}, throwOnMissing = false } = options;
|
|
19
|
+
const { provider, defaultContext = {}, throwOnMissing = false, onError, throwOnProviderError = false, } = options;
|
|
20
20
|
let registry = createFeatureFlagRegistry();
|
|
21
|
+
let loaded = false;
|
|
22
|
+
/**
|
|
23
|
+
* Report a provider failure, or rethrow it when the caller asked to own it.
|
|
24
|
+
*
|
|
25
|
+
* @returns `false` when the failure was contained.
|
|
26
|
+
*/
|
|
27
|
+
function handleProviderError(error, source) {
|
|
28
|
+
if (throwOnProviderError)
|
|
29
|
+
throw error;
|
|
30
|
+
onError?.(error instanceof Error
|
|
31
|
+
? error
|
|
32
|
+
: new FeatureFlagProviderError(`Provider ${source} failed.`, {
|
|
33
|
+
cause: error,
|
|
34
|
+
}), source);
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
21
37
|
async function load() {
|
|
22
|
-
|
|
23
|
-
|
|
38
|
+
try {
|
|
39
|
+
const flags = await provider.getAll();
|
|
40
|
+
registry = createFeatureFlagRegistry(flags);
|
|
41
|
+
loaded = true;
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
return handleProviderError(error, "FeatureFlagProvider.getAll");
|
|
46
|
+
}
|
|
24
47
|
}
|
|
25
48
|
async function ensureLoaded() {
|
|
26
|
-
|
|
27
|
-
|
|
49
|
+
// Keyed on whether a load has ever succeeded, not on `registry.size`: a
|
|
50
|
+
// provider that legitimately holds no flags used to be re-queried on
|
|
51
|
+
// every single evaluation.
|
|
52
|
+
if (loaded)
|
|
53
|
+
return true;
|
|
54
|
+
return load();
|
|
28
55
|
}
|
|
29
56
|
async function resolveFlag(key) {
|
|
30
|
-
|
|
31
|
-
if (
|
|
32
|
-
flag
|
|
57
|
+
const known = registry.get(key);
|
|
58
|
+
if (known)
|
|
59
|
+
return { flag: known, reachable: true };
|
|
60
|
+
try {
|
|
61
|
+
const flag = await provider.get(key);
|
|
33
62
|
if (flag)
|
|
34
63
|
registry.set(flag);
|
|
64
|
+
return { flag, reachable: true };
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
handleProviderError(error, "FeatureFlagProvider.get");
|
|
68
|
+
// A `get()` that failed is not a `get()` that found nothing: reporting
|
|
69
|
+
// it as `not_found` (or throwing FeatureFlagNotFoundError under
|
|
70
|
+
// `throwOnMissing`) told the caller the flag does not exist when the
|
|
71
|
+
// truth is that the store could not be asked.
|
|
72
|
+
return { flag: undefined, reachable: false };
|
|
35
73
|
}
|
|
36
|
-
return flag;
|
|
37
74
|
}
|
|
38
|
-
|
|
75
|
+
// A provider that can announce changes is asked to: without this, a flag
|
|
76
|
+
// flipped at the source never reached an already-loaded instance until
|
|
77
|
+
// somebody called refresh() by hand.
|
|
78
|
+
let unsubscribe = provider.subscribe?.((flags) => {
|
|
79
|
+
registry = createFeatureFlagRegistry(flags);
|
|
80
|
+
loaded = true;
|
|
81
|
+
});
|
|
82
|
+
const api = {
|
|
39
83
|
async isEnabled(key, context) {
|
|
40
|
-
const result = await
|
|
41
|
-
|
|
84
|
+
const result = await api.evaluate(key, context);
|
|
85
|
+
// Strictly `true`. Returning the raw value meant a flag holding a
|
|
86
|
+
// string or a number — "off", 0.5, a config object — came back truthy
|
|
87
|
+
// from a method whose whole contract is a boolean.
|
|
88
|
+
return result.value === true;
|
|
42
89
|
},
|
|
43
90
|
async get(key, context) {
|
|
44
|
-
const result = await
|
|
91
|
+
const result = await api.evaluate(key, context);
|
|
45
92
|
return result.value;
|
|
46
93
|
},
|
|
47
94
|
async getBoolean(key, defaultValue, context) {
|
|
48
|
-
const result = await
|
|
49
|
-
if (result.reason === "not_found")
|
|
95
|
+
const result = await api.evaluate(key, context);
|
|
96
|
+
if (result.reason === "not_found" || result.reason === "error") {
|
|
50
97
|
return defaultValue;
|
|
51
|
-
|
|
98
|
+
}
|
|
99
|
+
return typeof result.value === "boolean" ? result.value : defaultValue;
|
|
52
100
|
},
|
|
53
101
|
async evaluate(key, context) {
|
|
54
|
-
await ensureLoaded();
|
|
102
|
+
const available = await ensureLoaded();
|
|
55
103
|
const mergedCtx = mergeContext(defaultContext, context);
|
|
56
|
-
const flag = await resolveFlag(key);
|
|
104
|
+
const { flag, reachable } = await resolveFlag(key);
|
|
57
105
|
if (!flag) {
|
|
106
|
+
if (!available || !reachable) {
|
|
107
|
+
// The store could not be reached and nothing is known about this
|
|
108
|
+
// flag. Report it as an error, not as a decision.
|
|
109
|
+
return {
|
|
110
|
+
key,
|
|
111
|
+
value: undefined,
|
|
112
|
+
reason: "error",
|
|
113
|
+
defaulted: true,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
58
116
|
if (throwOnMissing)
|
|
59
117
|
throw new FeatureFlagNotFoundError(key);
|
|
60
118
|
return {
|
|
@@ -64,17 +122,10 @@ export function createFeatureFlags(options) {
|
|
|
64
122
|
defaulted: true,
|
|
65
123
|
};
|
|
66
124
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
value: flag.defaultValue,
|
|
72
|
-
reason: "dependency_disabled",
|
|
73
|
-
defaulted: true,
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
return evaluateFlag(flag, mergedCtx);
|
|
125
|
+
const dependenciesSatisfied = !flag.dependencies ||
|
|
126
|
+
flag.dependencies.length === 0 ||
|
|
127
|
+
resolveDependencies(key, registry);
|
|
128
|
+
return evaluateFlag(flag, mergedCtx, { dependenciesSatisfied });
|
|
78
129
|
},
|
|
79
130
|
async snapshot(context) {
|
|
80
131
|
await ensureLoaded();
|
|
@@ -82,20 +133,36 @@ export function createFeatureFlags(options) {
|
|
|
82
133
|
const flags = registry.getAll();
|
|
83
134
|
const results = new Map();
|
|
84
135
|
for (const flag of flags) {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
136
|
+
// Only flags explicitly marked client-visible. A flag that declares
|
|
137
|
+
// no visibility stays server-side: a snapshot is shipped to a
|
|
138
|
+
// browser, so the safe default is to withhold.
|
|
139
|
+
if (flag.visibility !== "client")
|
|
140
|
+
continue;
|
|
141
|
+
const dependenciesSatisfied = !flag.dependencies ||
|
|
142
|
+
flag.dependencies.length === 0 ||
|
|
143
|
+
resolveDependencies(flag.key, registry);
|
|
144
|
+
results.set(flag.key, evaluateFlag(flag, mergedCtx, { dependenciesSatisfied }));
|
|
88
145
|
}
|
|
89
146
|
return results;
|
|
90
147
|
},
|
|
91
148
|
async refresh() {
|
|
92
|
-
|
|
149
|
+
try {
|
|
150
|
+
await provider.refresh?.();
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
handleProviderError(error, "FeatureFlagProvider.refresh");
|
|
154
|
+
}
|
|
93
155
|
await load();
|
|
94
156
|
},
|
|
95
157
|
async getAll() {
|
|
96
158
|
await ensureLoaded();
|
|
97
159
|
return registry.getAll();
|
|
98
160
|
},
|
|
161
|
+
close() {
|
|
162
|
+
unsubscribe?.();
|
|
163
|
+
unsubscribe = undefined;
|
|
164
|
+
},
|
|
99
165
|
};
|
|
166
|
+
return api;
|
|
100
167
|
}
|
|
101
168
|
//# sourceMappingURL=featureFlags.core.js.map
|
|
@@ -10,10 +10,11 @@ import type { FeatureFlagRegistry } from "../registry/registry.core.js";
|
|
|
10
10
|
*
|
|
11
11
|
* @param key - The flag key to resolve.
|
|
12
12
|
* @param registry - The flag registry.
|
|
13
|
-
* @param
|
|
14
|
-
* @
|
|
13
|
+
* @param chain - Keys on the current resolution path, for cycle detection.
|
|
14
|
+
* @param satisfied - Keys already proved satisfied, to avoid re-walking them.
|
|
15
|
+
* @returns True if the flag and all its transitive dependencies are enabled.
|
|
15
16
|
*/
|
|
16
|
-
export declare function resolveDependencies(key: string, registry: FeatureFlagRegistry,
|
|
17
|
+
export declare function resolveDependencies(key: string, registry: FeatureFlagRegistry, chain?: Set<string>, satisfied?: Set<string>): boolean;
|
|
17
18
|
/**
|
|
18
19
|
* Merge default context with provided context.
|
|
19
20
|
*/
|
|
@@ -8,23 +8,40 @@
|
|
|
8
8
|
*
|
|
9
9
|
* @param key - The flag key to resolve.
|
|
10
10
|
* @param registry - The flag registry.
|
|
11
|
-
* @param
|
|
12
|
-
* @
|
|
11
|
+
* @param chain - Keys on the current resolution path, for cycle detection.
|
|
12
|
+
* @param satisfied - Keys already proved satisfied, to avoid re-walking them.
|
|
13
|
+
* @returns True if the flag and all its transitive dependencies are enabled.
|
|
13
14
|
*/
|
|
14
|
-
export function resolveDependencies(key, registry,
|
|
15
|
-
|
|
15
|
+
export function resolveDependencies(key, registry, chain = new Set(), satisfied = new Set()) {
|
|
16
|
+
// A key already proved good on another branch is good here too. Reusing one
|
|
17
|
+
// "visited" set for both jobs meant a diamond — A depends on B and C, both
|
|
18
|
+
// of which depend on D — reported D as a cycle the second time it was
|
|
19
|
+
// reached and disabled A.
|
|
20
|
+
if (satisfied.has(key))
|
|
21
|
+
return true;
|
|
22
|
+
if (chain.has(key))
|
|
16
23
|
return false;
|
|
17
|
-
|
|
24
|
+
chain.add(key);
|
|
18
25
|
const flag = registry.get(key);
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
if (!flag.dependencies || flag.dependencies.length === 0)
|
|
22
|
-
return true;
|
|
23
|
-
for (const dep of flag.dependencies) {
|
|
24
|
-
if (!resolveDependencies(dep, registry, visited))
|
|
26
|
+
try {
|
|
27
|
+
if (!flag || !flag.enabled)
|
|
25
28
|
return false;
|
|
29
|
+
if (!flag.dependencies || flag.dependencies.length === 0) {
|
|
30
|
+
satisfied.add(key);
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
for (const dep of flag.dependencies) {
|
|
34
|
+
if (!resolveDependencies(dep, registry, chain, satisfied))
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
satisfied.add(key);
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
finally {
|
|
41
|
+
// Leaving the key in the chain would make a sibling branch see a cycle
|
|
42
|
+
// that is not there.
|
|
43
|
+
chain.delete(key);
|
|
26
44
|
}
|
|
27
|
-
return true;
|
|
28
45
|
}
|
|
29
46
|
/**
|
|
30
47
|
* Merge default context with provided context.
|
package/dist/index.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ export { FeatureFlagNotFoundError, FeatureFlagProviderError, FeatureFlagEvaluati
|
|
|
12
12
|
export { hashString } from "./rollout/rolloutHashing.js";
|
|
13
13
|
export { getBucket, isInRollout } from "./rollout/rolloutBucketing.js";
|
|
14
14
|
export { createMemoryProvider } from "./provider/providerMemory.core.js";
|
|
15
|
+
export type { MemoryFeatureFlagProvider } from "./provider/providerMemory.core.js";
|
|
15
16
|
export { createEnvironmentProvider } from "./provider/providerEnvironment.core.js";
|
|
16
17
|
export type { EnvironmentProviderOptions } from "./provider/providerEnvironment.core.js";
|
|
17
18
|
export { createCompositeProvider } from "./provider/providerComposite.core.js";
|
|
@@ -21,9 +22,11 @@ export { resolvePath, matchAttribute } from "./evaluator/evaluatorAttribute.js";
|
|
|
21
22
|
export { evaluateRule } from "./evaluator/evaluatorRule.core.js";
|
|
22
23
|
export type { RuleEvaluationResult } from "./evaluator/evaluatorRule.core.js";
|
|
23
24
|
export { evaluateFlag } from "./evaluator/evaluator.core.js";
|
|
25
|
+
export type { EvaluateFlagOptions } from "./evaluator/evaluator.core.js";
|
|
24
26
|
export { createFeatureFlagRegistry } from "./registry/registry.core.js";
|
|
25
27
|
export type { FeatureFlagRegistry } from "./registry/registry.core.js";
|
|
26
28
|
export { createFeatureFlags } from "./featureFlags/featureFlags.core.js";
|
|
27
|
-
export type { FeatureFlagsOptions } from "./featureFlags/featureFlags.core.js";
|
|
29
|
+
export type { FeatureFlags, FeatureFlagsOptions, } from "./featureFlags/featureFlags.core.js";
|
|
30
|
+
export { resolveDependencies, mergeContext, } from "./featureFlags/featureFlags.resolve.js";
|
|
28
31
|
export { isPlainObject, valuesEqual } from "./utils/utils.helper.js";
|
|
29
32
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -24,6 +24,7 @@ export { evaluateFlag } from "./evaluator/evaluator.core.js";
|
|
|
24
24
|
export { createFeatureFlagRegistry } from "./registry/registry.core.js";
|
|
25
25
|
// ─── FeatureFlags ─────────────────────────────────────────────────────────
|
|
26
26
|
export { createFeatureFlags } from "./featureFlags/featureFlags.core.js";
|
|
27
|
+
export { resolveDependencies, mergeContext, } from "./featureFlags/featureFlags.resolve.js";
|
|
27
28
|
// ─── Utils ────────────────────────────────────────────────────────────────
|
|
28
29
|
export { isPlainObject, valuesEqual } from "./utils/utils.helper.js";
|
|
29
30
|
//# sourceMappingURL=index.js.map
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*
|
|
6
6
|
* @module provider/providerCached
|
|
7
7
|
*/
|
|
8
|
-
import type { FeatureFlagProvider } from "../featureFlagTypes/featureFlagProvider.js";
|
|
8
|
+
import type { FeatureFlagProvider, RefreshableFeatureFlagProvider } from "../featureFlagTypes/featureFlagProvider.js";
|
|
9
9
|
/** Options for the cached provider. */
|
|
10
10
|
export interface CachedProviderOptions {
|
|
11
11
|
/** Time-to-live in milliseconds (default: 30,000). */
|
|
@@ -16,7 +16,7 @@ export interface CachedProviderOptions {
|
|
|
16
16
|
*
|
|
17
17
|
* @param inner - The upstream provider to cache.
|
|
18
18
|
* @param options - Caching options.
|
|
19
|
-
* @returns A
|
|
19
|
+
* @returns A provider with in-memory TTL caching.
|
|
20
20
|
*/
|
|
21
|
-
export declare function createCachedProvider(inner: FeatureFlagProvider, options?: CachedProviderOptions):
|
|
21
|
+
export declare function createCachedProvider(inner: FeatureFlagProvider, options?: CachedProviderOptions): RefreshableFeatureFlagProvider;
|
|
22
22
|
//# sourceMappingURL=providerCached.core.d.ts.map
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*
|
|
11
11
|
* @param inner - The upstream provider to cache.
|
|
12
12
|
* @param options - Caching options.
|
|
13
|
-
* @returns A
|
|
13
|
+
* @returns A provider with in-memory TTL caching.
|
|
14
14
|
*/
|
|
15
15
|
export function createCachedProvider(inner, options = {}) {
|
|
16
16
|
const ttl = options.ttl ?? 30_000;
|
|
@@ -19,7 +19,27 @@ export function createCachedProvider(inner, options = {}) {
|
|
|
19
19
|
function isExpired(entry) {
|
|
20
20
|
return Date.now() > entry.expiresAt;
|
|
21
21
|
}
|
|
22
|
+
function clear() {
|
|
23
|
+
flagCache.clear();
|
|
24
|
+
listCache = undefined;
|
|
25
|
+
}
|
|
26
|
+
// A change announced by the upstream provider is forwarded, and the cache
|
|
27
|
+
// is dropped first so a `get()` made by the listener sees the new state.
|
|
28
|
+
// Without this the wrapper had no `subscribe`, so `createFeatureFlags` on
|
|
29
|
+
// a cached provider never heard about a flag flipped at the source and
|
|
30
|
+
// served the stale copy until the TTL ran out.
|
|
31
|
+
const subscribe = inner.subscribe
|
|
32
|
+
? {
|
|
33
|
+
subscribe(listener) {
|
|
34
|
+
return inner.subscribe((flags) => {
|
|
35
|
+
clear();
|
|
36
|
+
listener(flags);
|
|
37
|
+
});
|
|
38
|
+
},
|
|
39
|
+
}
|
|
40
|
+
: {};
|
|
22
41
|
return {
|
|
42
|
+
...subscribe,
|
|
23
43
|
async get(key) {
|
|
24
44
|
const cached = flagCache.get(key);
|
|
25
45
|
if (cached && !isExpired(cached))
|
|
@@ -36,8 +56,7 @@ export function createCachedProvider(inner, options = {}) {
|
|
|
36
56
|
return flags;
|
|
37
57
|
},
|
|
38
58
|
async refresh() {
|
|
39
|
-
|
|
40
|
-
listCache = undefined;
|
|
59
|
+
clear();
|
|
41
60
|
await inner.refresh?.();
|
|
42
61
|
},
|
|
43
62
|
};
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*
|
|
6
6
|
* @module provider/providerComposite
|
|
7
7
|
*/
|
|
8
|
-
import type { FeatureFlagProvider } from "../featureFlagTypes/featureFlagProvider.js";
|
|
8
|
+
import type { FeatureFlagProvider, RefreshableFeatureFlagProvider } from "../featureFlagTypes/featureFlagProvider.js";
|
|
9
9
|
/**
|
|
10
10
|
* Create a composite provider that queries providers in order.
|
|
11
11
|
*
|
|
@@ -15,5 +15,5 @@ import type { FeatureFlagProvider } from "../featureFlagTypes/featureFlagProvide
|
|
|
15
15
|
* @param providers - Providers to chain, in priority order.
|
|
16
16
|
* @returns A composite FeatureFlagProvider.
|
|
17
17
|
*/
|
|
18
|
-
export declare function createCompositeProvider(providers: readonly FeatureFlagProvider[]):
|
|
18
|
+
export declare function createCompositeProvider(providers: readonly FeatureFlagProvider[]): RefreshableFeatureFlagProvider;
|
|
19
19
|
//# sourceMappingURL=providerComposite.core.d.ts.map
|
|
@@ -15,7 +15,68 @@
|
|
|
15
15
|
* @returns A composite FeatureFlagProvider.
|
|
16
16
|
*/
|
|
17
17
|
export function createCompositeProvider(providers) {
|
|
18
|
+
/**
|
|
19
|
+
* The last flag list each member produced — from its own `getAll()` or
|
|
20
|
+
* from a change it announced. Lets a change be re-announced synchronously
|
|
21
|
+
* as the merged view, without a round trip to every other member.
|
|
22
|
+
*/
|
|
23
|
+
const lastSeen = new Map();
|
|
24
|
+
function merge(lists) {
|
|
25
|
+
const seen = new Set();
|
|
26
|
+
const result = [];
|
|
27
|
+
for (const flags of lists) {
|
|
28
|
+
for (const flag of flags) {
|
|
29
|
+
if (!seen.has(flag.key)) {
|
|
30
|
+
seen.add(flag.key);
|
|
31
|
+
result.push(flag);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return result;
|
|
36
|
+
}
|
|
37
|
+
async function getAll() {
|
|
38
|
+
const lists = [];
|
|
39
|
+
for (const provider of providers) {
|
|
40
|
+
const flags = await provider.getAll();
|
|
41
|
+
lastSeen.set(provider, flags);
|
|
42
|
+
lists.push(flags);
|
|
43
|
+
}
|
|
44
|
+
return merge(lists);
|
|
45
|
+
}
|
|
46
|
+
// Changes announced by any member are re-announced as the merged view, so
|
|
47
|
+
// a consumer subscribed to the composite sees the same precedence
|
|
48
|
+
// `getAll()` applies. The composite used to have no `subscribe` at all,
|
|
49
|
+
// which silently cut change propagation for every provider behind it.
|
|
50
|
+
const subscribable = providers.filter((provider) => provider.subscribe);
|
|
51
|
+
const subscribe = subscribable.length > 0
|
|
52
|
+
? {
|
|
53
|
+
subscribe(listener) {
|
|
54
|
+
// A merged snapshot fetched asynchronously must not overtake a
|
|
55
|
+
// later change.
|
|
56
|
+
let version = 0;
|
|
57
|
+
const unsubscribes = subscribable.map((provider) => provider.subscribe((flags) => {
|
|
58
|
+
lastSeen.set(provider, flags);
|
|
59
|
+
const current = ++version;
|
|
60
|
+
if (providers.every((member) => lastSeen.has(member))) {
|
|
61
|
+
listener(merge(providers.map((member) => lastSeen.get(member))));
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
void getAll().then((merged) => {
|
|
65
|
+
if (current === version)
|
|
66
|
+
listener(merged);
|
|
67
|
+
}, () => {
|
|
68
|
+
/* a member that cannot be read keeps the last view */
|
|
69
|
+
});
|
|
70
|
+
}));
|
|
71
|
+
return () => {
|
|
72
|
+
for (const unsubscribe of unsubscribes)
|
|
73
|
+
unsubscribe();
|
|
74
|
+
};
|
|
75
|
+
},
|
|
76
|
+
}
|
|
77
|
+
: {};
|
|
18
78
|
return {
|
|
79
|
+
...subscribe,
|
|
19
80
|
async get(key) {
|
|
20
81
|
for (const provider of providers) {
|
|
21
82
|
const flag = await provider.get(key);
|
|
@@ -24,20 +85,7 @@ export function createCompositeProvider(providers) {
|
|
|
24
85
|
}
|
|
25
86
|
return undefined;
|
|
26
87
|
},
|
|
27
|
-
|
|
28
|
-
const seen = new Set();
|
|
29
|
-
const result = [];
|
|
30
|
-
for (const provider of providers) {
|
|
31
|
-
const flags = await provider.getAll();
|
|
32
|
-
for (const flag of flags) {
|
|
33
|
-
if (!seen.has(flag.key)) {
|
|
34
|
-
seen.add(flag.key);
|
|
35
|
-
result.push(flag);
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
return result;
|
|
40
|
-
},
|
|
88
|
+
getAll,
|
|
41
89
|
async refresh() {
|
|
42
90
|
for (const provider of providers) {
|
|
43
91
|
await provider.refresh?.();
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* @module provider/providerEnvironment
|
|
9
9
|
*/
|
|
10
|
-
import type {
|
|
10
|
+
import type { RefreshableFeatureFlagProvider } from "../featureFlagTypes/featureFlagProvider.js";
|
|
11
11
|
/** Options for the environment provider. */
|
|
12
12
|
export interface EnvironmentProviderOptions {
|
|
13
13
|
/** Prefix for environment variable names (default: "FEATURE_"). */
|
|
@@ -19,7 +19,7 @@ export interface EnvironmentProviderOptions {
|
|
|
19
19
|
* Create an environment-variable feature flag provider.
|
|
20
20
|
*
|
|
21
21
|
* @param options - Configuration options.
|
|
22
|
-
* @returns A
|
|
22
|
+
* @returns A provider that reads from environment variables.
|
|
23
23
|
*/
|
|
24
|
-
export declare function createEnvironmentProvider(options?: EnvironmentProviderOptions):
|
|
24
|
+
export declare function createEnvironmentProvider(options?: EnvironmentProviderOptions): RefreshableFeatureFlagProvider;
|
|
25
25
|
//# sourceMappingURL=providerEnvironment.core.d.ts.map
|
|
@@ -15,16 +15,21 @@ function parseEnvValue(raw) {
|
|
|
15
15
|
return true;
|
|
16
16
|
if (raw === "false")
|
|
17
17
|
return false;
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
// `Number("")` and `Number(" ")` are both `0`, so an empty
|
|
19
|
+
// `FEATURE_X=` used to become the number zero. Only a value that is not
|
|
20
|
+
// blank is a candidate for a number; blank stays the string it is.
|
|
21
|
+
if (raw.trim() !== "") {
|
|
22
|
+
const num = Number(raw);
|
|
23
|
+
if (!Number.isNaN(num))
|
|
24
|
+
return num;
|
|
25
|
+
}
|
|
21
26
|
return raw;
|
|
22
27
|
}
|
|
23
28
|
/**
|
|
24
29
|
* Create an environment-variable feature flag provider.
|
|
25
30
|
*
|
|
26
31
|
* @param options - Configuration options.
|
|
27
|
-
* @returns A
|
|
32
|
+
* @returns A provider that reads from environment variables.
|
|
28
33
|
*/
|
|
29
34
|
export function createEnvironmentProvider(options = {}) {
|
|
30
35
|
const prefix = options.prefix ?? "FEATURE_";
|
|
@@ -6,12 +6,28 @@
|
|
|
6
6
|
* @module provider/providerMemory
|
|
7
7
|
*/
|
|
8
8
|
import type { FeatureFlag } from "../featureFlagTypes/featureFlag.interface.js";
|
|
9
|
-
import type { FeatureFlagProvider } from "../featureFlagTypes/featureFlagProvider.js";
|
|
9
|
+
import type { FeatureFlagChangeListener, FeatureFlagProvider, Unsubscribe } from "../featureFlagTypes/featureFlagProvider.js";
|
|
10
|
+
/**
|
|
11
|
+
* An in-memory provider, with the mutation methods it actually has.
|
|
12
|
+
*
|
|
13
|
+
* `createMemoryProvider` used to return `FeatureFlagProvider` through a cast,
|
|
14
|
+
* so `set` and `delete` existed at runtime and were invisible to the type
|
|
15
|
+
* system — callers had to cast to reach them.
|
|
16
|
+
*/
|
|
17
|
+
export interface MemoryFeatureFlagProvider extends FeatureFlagProvider {
|
|
18
|
+
/** Add or update a flag at runtime, notifying subscribers. */
|
|
19
|
+
set(flag: FeatureFlag): void;
|
|
20
|
+
/** Remove a flag by key, notifying subscribers. Returns whether it existed. */
|
|
21
|
+
delete(key: string): boolean;
|
|
22
|
+
/** Replace every flag at once, notifying subscribers. */
|
|
23
|
+
setAll(flags: readonly FeatureFlag[]): void;
|
|
24
|
+
subscribe(listener: FeatureFlagChangeListener): Unsubscribe;
|
|
25
|
+
}
|
|
10
26
|
/**
|
|
11
27
|
* Create an in-memory feature flag provider.
|
|
12
28
|
*
|
|
13
29
|
* @param flags - Initial flag definitions.
|
|
14
30
|
* @returns A FeatureFlagProvider backed by a Map.
|
|
15
31
|
*/
|
|
16
|
-
export declare function createMemoryProvider(flags?: readonly FeatureFlag[]):
|
|
32
|
+
export declare function createMemoryProvider(flags?: readonly FeatureFlag[]): MemoryFeatureFlagProvider;
|
|
17
33
|
//# sourceMappingURL=providerMemory.core.d.ts.map
|
|
@@ -13,23 +13,57 @@
|
|
|
13
13
|
*/
|
|
14
14
|
export function createMemoryProvider(flags = []) {
|
|
15
15
|
const store = new Map();
|
|
16
|
+
const listeners = new Set();
|
|
16
17
|
for (const flag of flags) {
|
|
17
18
|
store.set(flag.key, Object.freeze({ ...flag }));
|
|
18
19
|
}
|
|
20
|
+
function snapshot() {
|
|
21
|
+
return [...store.values()];
|
|
22
|
+
}
|
|
23
|
+
/** Announce a change so an already-loaded consumer picks it up. */
|
|
24
|
+
function notify() {
|
|
25
|
+
if (listeners.size === 0)
|
|
26
|
+
return;
|
|
27
|
+
const current = snapshot();
|
|
28
|
+
for (const listener of listeners) {
|
|
29
|
+
// A broken listener must not stop the others, or the mutation itself.
|
|
30
|
+
try {
|
|
31
|
+
listener(current);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
/* listener failures are the listener's problem */
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
19
38
|
return {
|
|
20
39
|
async get(key) {
|
|
21
40
|
return store.get(key);
|
|
22
41
|
},
|
|
23
42
|
async getAll() {
|
|
24
|
-
return
|
|
43
|
+
return snapshot();
|
|
25
44
|
},
|
|
26
|
-
/** Add or update a flag at runtime. */
|
|
27
45
|
set(flag) {
|
|
28
46
|
store.set(flag.key, Object.freeze({ ...flag }));
|
|
47
|
+
notify();
|
|
29
48
|
},
|
|
30
|
-
/** Remove a flag by key. */
|
|
31
49
|
delete(key) {
|
|
32
|
-
|
|
50
|
+
const existed = store.delete(key);
|
|
51
|
+
if (existed)
|
|
52
|
+
notify();
|
|
53
|
+
return existed;
|
|
54
|
+
},
|
|
55
|
+
setAll(next) {
|
|
56
|
+
store.clear();
|
|
57
|
+
for (const flag of next) {
|
|
58
|
+
store.set(flag.key, Object.freeze({ ...flag }));
|
|
59
|
+
}
|
|
60
|
+
notify();
|
|
61
|
+
},
|
|
62
|
+
subscribe(listener) {
|
|
63
|
+
listeners.add(listener);
|
|
64
|
+
return () => {
|
|
65
|
+
listeners.delete(listener);
|
|
66
|
+
};
|
|
33
67
|
},
|
|
34
68
|
};
|
|
35
69
|
}
|