@dungarees/feature-flipper 0.11.4
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/identity.d.ts +5 -0
- package/identity.js +23 -0
- package/identity.test.d.ts +1 -0
- package/identity.test.js +78 -0
- package/package.json +450 -0
- package/service.d.ts +7 -0
- package/service.js +43 -0
- package/service.test.d.ts +1 -0
- package/service.test.js +123 -0
- package/type.d.ts +37 -0
- package/type.js +1 -0
package/identity.d.ts
ADDED
package/identity.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const PERCENTAGE_BUCKETS = 100;
|
|
2
|
+
// Hashed rather than sampled, so the same identity always lands in the same bucket and a feature
|
|
3
|
+
// stays on for whoever it turned on for.
|
|
4
|
+
const hash2number = (input) => Math.abs(input.split('').reduce((hash, character) => (hash << 5) - hash + character.charCodeAt(0), 0));
|
|
5
|
+
const isIdentityInPercentage = (identityValue, percentage) => hash2number(identityValue) % PERCENTAGE_BUCKETS < percentage;
|
|
6
|
+
const MATCHERS = {
|
|
7
|
+
regex: ({ value }, identityValue) => value.test(identityValue),
|
|
8
|
+
percentage: ({ value }, identityValue) => isIdentityInPercentage(identityValue, value),
|
|
9
|
+
};
|
|
10
|
+
// Indexing MATCHERS with a union key trips TypeScript's correlated-union limitation, so the key is
|
|
11
|
+
// a single type parameter here and each handler sees only its own variant.
|
|
12
|
+
const isMatch = (matcher, identityValue) => MATCHERS[matcher.type](matcher, identityValue);
|
|
13
|
+
export const getIdentityValue = ({ feature, identity, }) => {
|
|
14
|
+
const rules = feature.identity;
|
|
15
|
+
if (rules === undefined) {
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
// A rule whose key the caller did not supply cannot be judged, so it does not stand in the way.
|
|
19
|
+
return Object.entries(rules).every(([key, matcher]) => {
|
|
20
|
+
const identityValue = identity[key];
|
|
21
|
+
return identityValue === undefined || isMatch(matcher, identityValue);
|
|
22
|
+
});
|
|
23
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/identity.test.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { getIdentityValue } from './identity.js';
|
|
2
|
+
import { expect, test } from 'vitest';
|
|
3
|
+
const EMAIL_MATCHER = { type: 'regex', value: /.*@example\.com/ };
|
|
4
|
+
test('a feature with no identity rules has no identity verdict', () => {
|
|
5
|
+
const feature = { name: 'feature', value: true };
|
|
6
|
+
expect(getIdentityValue({ feature, identity: { id1: 'someone' } })).toBe(undefined);
|
|
7
|
+
});
|
|
8
|
+
test('an identity that satisfies every rule is enabled', () => {
|
|
9
|
+
const feature = {
|
|
10
|
+
name: 'feature',
|
|
11
|
+
value: false,
|
|
12
|
+
identity: { id1: EMAIL_MATCHER, id2: EMAIL_MATCHER },
|
|
13
|
+
};
|
|
14
|
+
expect(getIdentityValue({
|
|
15
|
+
feature,
|
|
16
|
+
identity: { id1: 'a@example.com', id2: 'b@example.com' },
|
|
17
|
+
})).toBe(true);
|
|
18
|
+
});
|
|
19
|
+
test('an identity that fails one rule is disabled', () => {
|
|
20
|
+
const feature = {
|
|
21
|
+
name: 'feature',
|
|
22
|
+
value: true,
|
|
23
|
+
identity: { id1: EMAIL_MATCHER, id2: EMAIL_MATCHER },
|
|
24
|
+
};
|
|
25
|
+
expect(getIdentityValue({
|
|
26
|
+
feature,
|
|
27
|
+
identity: { id1: 'a@example.com', id2: 'b@elsewhere.com' },
|
|
28
|
+
})).toBe(false);
|
|
29
|
+
});
|
|
30
|
+
test('a rule whose identity key was not supplied does not count against it', () => {
|
|
31
|
+
const feature = {
|
|
32
|
+
name: 'feature',
|
|
33
|
+
value: false,
|
|
34
|
+
identity: { id1: EMAIL_MATCHER, id2: EMAIL_MATCHER },
|
|
35
|
+
};
|
|
36
|
+
expect(getIdentityValue({ feature, identity: { id1: 'a@example.com' } })).toBe(true);
|
|
37
|
+
});
|
|
38
|
+
test('an identity supplying none of the keys satisfies the feature by default', () => {
|
|
39
|
+
const feature = {
|
|
40
|
+
name: 'feature',
|
|
41
|
+
value: false,
|
|
42
|
+
identity: { id1: EMAIL_MATCHER },
|
|
43
|
+
};
|
|
44
|
+
expect(getIdentityValue({ feature, identity: {} })).toBe(true);
|
|
45
|
+
});
|
|
46
|
+
test('a percentage rule includes an identity that hashes below the threshold', () => {
|
|
47
|
+
const feature = {
|
|
48
|
+
name: 'feature',
|
|
49
|
+
value: false,
|
|
50
|
+
identity: { id1: { type: 'percentage', value: 100 } },
|
|
51
|
+
};
|
|
52
|
+
expect(getIdentityValue({ feature, identity: { id1: 'anyone' } })).toBe(true);
|
|
53
|
+
});
|
|
54
|
+
test('a percentage rule of zero excludes every identity', () => {
|
|
55
|
+
const feature = {
|
|
56
|
+
name: 'feature',
|
|
57
|
+
value: true,
|
|
58
|
+
identity: { id1: { type: 'percentage', value: 0 } },
|
|
59
|
+
};
|
|
60
|
+
expect(getIdentityValue({ feature, identity: { id1: 'anyone' } })).toBe(false);
|
|
61
|
+
});
|
|
62
|
+
test('a percentage rule gives the same identity the same verdict every time', () => {
|
|
63
|
+
const feature = {
|
|
64
|
+
name: 'feature',
|
|
65
|
+
value: false,
|
|
66
|
+
identity: { id1: { type: 'percentage', value: 50 } },
|
|
67
|
+
};
|
|
68
|
+
const verdicts = Array.from({ length: 5 }, () => getIdentityValue({ feature, identity: { id1: 'stable' } }));
|
|
69
|
+
expect(new Set(verdicts).size).toBe(1);
|
|
70
|
+
});
|
|
71
|
+
test('a regex and a percentage rule on the same feature must both hold', () => {
|
|
72
|
+
const feature = {
|
|
73
|
+
name: 'feature',
|
|
74
|
+
value: false,
|
|
75
|
+
identity: { id1: EMAIL_MATCHER, id2: { type: 'percentage', value: 0 } },
|
|
76
|
+
};
|
|
77
|
+
expect(getIdentityValue({ feature, identity: { id1: 'a@example.com', id2: 'anyone' } })).toBe(false);
|
|
78
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dungarees/feature-flipper",
|
|
3
|
+
"engines": {
|
|
4
|
+
"node": ">=25.0.0"
|
|
5
|
+
},
|
|
6
|
+
"scripts": {
|
|
7
|
+
"type-check": "tsc --noEmit"
|
|
8
|
+
},
|
|
9
|
+
"type": "module",
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"@dungarees/key-value-stores": "*"
|
|
12
|
+
},
|
|
13
|
+
"devDependencies": {
|
|
14
|
+
"typescript": "^5.9.3",
|
|
15
|
+
"vitest": "^3.0.2"
|
|
16
|
+
},
|
|
17
|
+
"author": "info@productkind.com",
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"version": "0.11.4",
|
|
20
|
+
"exports": {
|
|
21
|
+
"./identity.test.ts": {
|
|
22
|
+
"import": "./identity.test.js",
|
|
23
|
+
"types": "./identity.test.d.ts"
|
|
24
|
+
},
|
|
25
|
+
"./identity.ts": {
|
|
26
|
+
"import": "./identity.js",
|
|
27
|
+
"types": "./identity.d.ts"
|
|
28
|
+
},
|
|
29
|
+
"./node_modules/typescript/lib/lib.d.ts": {
|
|
30
|
+
"import": "./node_modules/typescript/lib/lib.d.js",
|
|
31
|
+
"types": "./node_modules/typescript/lib/lib.d.d.ts"
|
|
32
|
+
},
|
|
33
|
+
"./node_modules/typescript/lib/lib.decorators.d.ts": {
|
|
34
|
+
"import": "./node_modules/typescript/lib/lib.decorators.d.js",
|
|
35
|
+
"types": "./node_modules/typescript/lib/lib.decorators.d.d.ts"
|
|
36
|
+
},
|
|
37
|
+
"./node_modules/typescript/lib/lib.decorators.legacy.d.ts": {
|
|
38
|
+
"import": "./node_modules/typescript/lib/lib.decorators.legacy.d.js",
|
|
39
|
+
"types": "./node_modules/typescript/lib/lib.decorators.legacy.d.d.ts"
|
|
40
|
+
},
|
|
41
|
+
"./node_modules/typescript/lib/lib.dom.asynciterable.d.ts": {
|
|
42
|
+
"import": "./node_modules/typescript/lib/lib.dom.asynciterable.d.js",
|
|
43
|
+
"types": "./node_modules/typescript/lib/lib.dom.asynciterable.d.d.ts"
|
|
44
|
+
},
|
|
45
|
+
"./node_modules/typescript/lib/lib.dom.d.ts": {
|
|
46
|
+
"import": "./node_modules/typescript/lib/lib.dom.d.js",
|
|
47
|
+
"types": "./node_modules/typescript/lib/lib.dom.d.d.ts"
|
|
48
|
+
},
|
|
49
|
+
"./node_modules/typescript/lib/lib.dom.iterable.d.ts": {
|
|
50
|
+
"import": "./node_modules/typescript/lib/lib.dom.iterable.d.js",
|
|
51
|
+
"types": "./node_modules/typescript/lib/lib.dom.iterable.d.d.ts"
|
|
52
|
+
},
|
|
53
|
+
"./node_modules/typescript/lib/lib.es2015.collection.d.ts": {
|
|
54
|
+
"import": "./node_modules/typescript/lib/lib.es2015.collection.d.js",
|
|
55
|
+
"types": "./node_modules/typescript/lib/lib.es2015.collection.d.d.ts"
|
|
56
|
+
},
|
|
57
|
+
"./node_modules/typescript/lib/lib.es2015.core.d.ts": {
|
|
58
|
+
"import": "./node_modules/typescript/lib/lib.es2015.core.d.js",
|
|
59
|
+
"types": "./node_modules/typescript/lib/lib.es2015.core.d.d.ts"
|
|
60
|
+
},
|
|
61
|
+
"./node_modules/typescript/lib/lib.es2015.d.ts": {
|
|
62
|
+
"import": "./node_modules/typescript/lib/lib.es2015.d.js",
|
|
63
|
+
"types": "./node_modules/typescript/lib/lib.es2015.d.d.ts"
|
|
64
|
+
},
|
|
65
|
+
"./node_modules/typescript/lib/lib.es2015.generator.d.ts": {
|
|
66
|
+
"import": "./node_modules/typescript/lib/lib.es2015.generator.d.js",
|
|
67
|
+
"types": "./node_modules/typescript/lib/lib.es2015.generator.d.d.ts"
|
|
68
|
+
},
|
|
69
|
+
"./node_modules/typescript/lib/lib.es2015.iterable.d.ts": {
|
|
70
|
+
"import": "./node_modules/typescript/lib/lib.es2015.iterable.d.js",
|
|
71
|
+
"types": "./node_modules/typescript/lib/lib.es2015.iterable.d.d.ts"
|
|
72
|
+
},
|
|
73
|
+
"./node_modules/typescript/lib/lib.es2015.promise.d.ts": {
|
|
74
|
+
"import": "./node_modules/typescript/lib/lib.es2015.promise.d.js",
|
|
75
|
+
"types": "./node_modules/typescript/lib/lib.es2015.promise.d.d.ts"
|
|
76
|
+
},
|
|
77
|
+
"./node_modules/typescript/lib/lib.es2015.proxy.d.ts": {
|
|
78
|
+
"import": "./node_modules/typescript/lib/lib.es2015.proxy.d.js",
|
|
79
|
+
"types": "./node_modules/typescript/lib/lib.es2015.proxy.d.d.ts"
|
|
80
|
+
},
|
|
81
|
+
"./node_modules/typescript/lib/lib.es2015.reflect.d.ts": {
|
|
82
|
+
"import": "./node_modules/typescript/lib/lib.es2015.reflect.d.js",
|
|
83
|
+
"types": "./node_modules/typescript/lib/lib.es2015.reflect.d.d.ts"
|
|
84
|
+
},
|
|
85
|
+
"./node_modules/typescript/lib/lib.es2015.symbol.d.ts": {
|
|
86
|
+
"import": "./node_modules/typescript/lib/lib.es2015.symbol.d.js",
|
|
87
|
+
"types": "./node_modules/typescript/lib/lib.es2015.symbol.d.d.ts"
|
|
88
|
+
},
|
|
89
|
+
"./node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.ts": {
|
|
90
|
+
"import": "./node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.js",
|
|
91
|
+
"types": "./node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.d.ts"
|
|
92
|
+
},
|
|
93
|
+
"./node_modules/typescript/lib/lib.es2016.array.include.d.ts": {
|
|
94
|
+
"import": "./node_modules/typescript/lib/lib.es2016.array.include.d.js",
|
|
95
|
+
"types": "./node_modules/typescript/lib/lib.es2016.array.include.d.d.ts"
|
|
96
|
+
},
|
|
97
|
+
"./node_modules/typescript/lib/lib.es2016.d.ts": {
|
|
98
|
+
"import": "./node_modules/typescript/lib/lib.es2016.d.js",
|
|
99
|
+
"types": "./node_modules/typescript/lib/lib.es2016.d.d.ts"
|
|
100
|
+
},
|
|
101
|
+
"./node_modules/typescript/lib/lib.es2016.full.d.ts": {
|
|
102
|
+
"import": "./node_modules/typescript/lib/lib.es2016.full.d.js",
|
|
103
|
+
"types": "./node_modules/typescript/lib/lib.es2016.full.d.d.ts"
|
|
104
|
+
},
|
|
105
|
+
"./node_modules/typescript/lib/lib.es2016.intl.d.ts": {
|
|
106
|
+
"import": "./node_modules/typescript/lib/lib.es2016.intl.d.js",
|
|
107
|
+
"types": "./node_modules/typescript/lib/lib.es2016.intl.d.d.ts"
|
|
108
|
+
},
|
|
109
|
+
"./node_modules/typescript/lib/lib.es2017.arraybuffer.d.ts": {
|
|
110
|
+
"import": "./node_modules/typescript/lib/lib.es2017.arraybuffer.d.js",
|
|
111
|
+
"types": "./node_modules/typescript/lib/lib.es2017.arraybuffer.d.d.ts"
|
|
112
|
+
},
|
|
113
|
+
"./node_modules/typescript/lib/lib.es2017.d.ts": {
|
|
114
|
+
"import": "./node_modules/typescript/lib/lib.es2017.d.js",
|
|
115
|
+
"types": "./node_modules/typescript/lib/lib.es2017.d.d.ts"
|
|
116
|
+
},
|
|
117
|
+
"./node_modules/typescript/lib/lib.es2017.date.d.ts": {
|
|
118
|
+
"import": "./node_modules/typescript/lib/lib.es2017.date.d.js",
|
|
119
|
+
"types": "./node_modules/typescript/lib/lib.es2017.date.d.d.ts"
|
|
120
|
+
},
|
|
121
|
+
"./node_modules/typescript/lib/lib.es2017.full.d.ts": {
|
|
122
|
+
"import": "./node_modules/typescript/lib/lib.es2017.full.d.js",
|
|
123
|
+
"types": "./node_modules/typescript/lib/lib.es2017.full.d.d.ts"
|
|
124
|
+
},
|
|
125
|
+
"./node_modules/typescript/lib/lib.es2017.intl.d.ts": {
|
|
126
|
+
"import": "./node_modules/typescript/lib/lib.es2017.intl.d.js",
|
|
127
|
+
"types": "./node_modules/typescript/lib/lib.es2017.intl.d.d.ts"
|
|
128
|
+
},
|
|
129
|
+
"./node_modules/typescript/lib/lib.es2017.object.d.ts": {
|
|
130
|
+
"import": "./node_modules/typescript/lib/lib.es2017.object.d.js",
|
|
131
|
+
"types": "./node_modules/typescript/lib/lib.es2017.object.d.d.ts"
|
|
132
|
+
},
|
|
133
|
+
"./node_modules/typescript/lib/lib.es2017.sharedmemory.d.ts": {
|
|
134
|
+
"import": "./node_modules/typescript/lib/lib.es2017.sharedmemory.d.js",
|
|
135
|
+
"types": "./node_modules/typescript/lib/lib.es2017.sharedmemory.d.d.ts"
|
|
136
|
+
},
|
|
137
|
+
"./node_modules/typescript/lib/lib.es2017.string.d.ts": {
|
|
138
|
+
"import": "./node_modules/typescript/lib/lib.es2017.string.d.js",
|
|
139
|
+
"types": "./node_modules/typescript/lib/lib.es2017.string.d.d.ts"
|
|
140
|
+
},
|
|
141
|
+
"./node_modules/typescript/lib/lib.es2017.typedarrays.d.ts": {
|
|
142
|
+
"import": "./node_modules/typescript/lib/lib.es2017.typedarrays.d.js",
|
|
143
|
+
"types": "./node_modules/typescript/lib/lib.es2017.typedarrays.d.d.ts"
|
|
144
|
+
},
|
|
145
|
+
"./node_modules/typescript/lib/lib.es2018.asyncgenerator.d.ts": {
|
|
146
|
+
"import": "./node_modules/typescript/lib/lib.es2018.asyncgenerator.d.js",
|
|
147
|
+
"types": "./node_modules/typescript/lib/lib.es2018.asyncgenerator.d.d.ts"
|
|
148
|
+
},
|
|
149
|
+
"./node_modules/typescript/lib/lib.es2018.asynciterable.d.ts": {
|
|
150
|
+
"import": "./node_modules/typescript/lib/lib.es2018.asynciterable.d.js",
|
|
151
|
+
"types": "./node_modules/typescript/lib/lib.es2018.asynciterable.d.d.ts"
|
|
152
|
+
},
|
|
153
|
+
"./node_modules/typescript/lib/lib.es2018.d.ts": {
|
|
154
|
+
"import": "./node_modules/typescript/lib/lib.es2018.d.js",
|
|
155
|
+
"types": "./node_modules/typescript/lib/lib.es2018.d.d.ts"
|
|
156
|
+
},
|
|
157
|
+
"./node_modules/typescript/lib/lib.es2018.full.d.ts": {
|
|
158
|
+
"import": "./node_modules/typescript/lib/lib.es2018.full.d.js",
|
|
159
|
+
"types": "./node_modules/typescript/lib/lib.es2018.full.d.d.ts"
|
|
160
|
+
},
|
|
161
|
+
"./node_modules/typescript/lib/lib.es2018.intl.d.ts": {
|
|
162
|
+
"import": "./node_modules/typescript/lib/lib.es2018.intl.d.js",
|
|
163
|
+
"types": "./node_modules/typescript/lib/lib.es2018.intl.d.d.ts"
|
|
164
|
+
},
|
|
165
|
+
"./node_modules/typescript/lib/lib.es2018.promise.d.ts": {
|
|
166
|
+
"import": "./node_modules/typescript/lib/lib.es2018.promise.d.js",
|
|
167
|
+
"types": "./node_modules/typescript/lib/lib.es2018.promise.d.d.ts"
|
|
168
|
+
},
|
|
169
|
+
"./node_modules/typescript/lib/lib.es2018.regexp.d.ts": {
|
|
170
|
+
"import": "./node_modules/typescript/lib/lib.es2018.regexp.d.js",
|
|
171
|
+
"types": "./node_modules/typescript/lib/lib.es2018.regexp.d.d.ts"
|
|
172
|
+
},
|
|
173
|
+
"./node_modules/typescript/lib/lib.es2019.array.d.ts": {
|
|
174
|
+
"import": "./node_modules/typescript/lib/lib.es2019.array.d.js",
|
|
175
|
+
"types": "./node_modules/typescript/lib/lib.es2019.array.d.d.ts"
|
|
176
|
+
},
|
|
177
|
+
"./node_modules/typescript/lib/lib.es2019.d.ts": {
|
|
178
|
+
"import": "./node_modules/typescript/lib/lib.es2019.d.js",
|
|
179
|
+
"types": "./node_modules/typescript/lib/lib.es2019.d.d.ts"
|
|
180
|
+
},
|
|
181
|
+
"./node_modules/typescript/lib/lib.es2019.full.d.ts": {
|
|
182
|
+
"import": "./node_modules/typescript/lib/lib.es2019.full.d.js",
|
|
183
|
+
"types": "./node_modules/typescript/lib/lib.es2019.full.d.d.ts"
|
|
184
|
+
},
|
|
185
|
+
"./node_modules/typescript/lib/lib.es2019.intl.d.ts": {
|
|
186
|
+
"import": "./node_modules/typescript/lib/lib.es2019.intl.d.js",
|
|
187
|
+
"types": "./node_modules/typescript/lib/lib.es2019.intl.d.d.ts"
|
|
188
|
+
},
|
|
189
|
+
"./node_modules/typescript/lib/lib.es2019.object.d.ts": {
|
|
190
|
+
"import": "./node_modules/typescript/lib/lib.es2019.object.d.js",
|
|
191
|
+
"types": "./node_modules/typescript/lib/lib.es2019.object.d.d.ts"
|
|
192
|
+
},
|
|
193
|
+
"./node_modules/typescript/lib/lib.es2019.string.d.ts": {
|
|
194
|
+
"import": "./node_modules/typescript/lib/lib.es2019.string.d.js",
|
|
195
|
+
"types": "./node_modules/typescript/lib/lib.es2019.string.d.d.ts"
|
|
196
|
+
},
|
|
197
|
+
"./node_modules/typescript/lib/lib.es2019.symbol.d.ts": {
|
|
198
|
+
"import": "./node_modules/typescript/lib/lib.es2019.symbol.d.js",
|
|
199
|
+
"types": "./node_modules/typescript/lib/lib.es2019.symbol.d.d.ts"
|
|
200
|
+
},
|
|
201
|
+
"./node_modules/typescript/lib/lib.es2020.bigint.d.ts": {
|
|
202
|
+
"import": "./node_modules/typescript/lib/lib.es2020.bigint.d.js",
|
|
203
|
+
"types": "./node_modules/typescript/lib/lib.es2020.bigint.d.d.ts"
|
|
204
|
+
},
|
|
205
|
+
"./node_modules/typescript/lib/lib.es2020.d.ts": {
|
|
206
|
+
"import": "./node_modules/typescript/lib/lib.es2020.d.js",
|
|
207
|
+
"types": "./node_modules/typescript/lib/lib.es2020.d.d.ts"
|
|
208
|
+
},
|
|
209
|
+
"./node_modules/typescript/lib/lib.es2020.date.d.ts": {
|
|
210
|
+
"import": "./node_modules/typescript/lib/lib.es2020.date.d.js",
|
|
211
|
+
"types": "./node_modules/typescript/lib/lib.es2020.date.d.d.ts"
|
|
212
|
+
},
|
|
213
|
+
"./node_modules/typescript/lib/lib.es2020.full.d.ts": {
|
|
214
|
+
"import": "./node_modules/typescript/lib/lib.es2020.full.d.js",
|
|
215
|
+
"types": "./node_modules/typescript/lib/lib.es2020.full.d.d.ts"
|
|
216
|
+
},
|
|
217
|
+
"./node_modules/typescript/lib/lib.es2020.intl.d.ts": {
|
|
218
|
+
"import": "./node_modules/typescript/lib/lib.es2020.intl.d.js",
|
|
219
|
+
"types": "./node_modules/typescript/lib/lib.es2020.intl.d.d.ts"
|
|
220
|
+
},
|
|
221
|
+
"./node_modules/typescript/lib/lib.es2020.number.d.ts": {
|
|
222
|
+
"import": "./node_modules/typescript/lib/lib.es2020.number.d.js",
|
|
223
|
+
"types": "./node_modules/typescript/lib/lib.es2020.number.d.d.ts"
|
|
224
|
+
},
|
|
225
|
+
"./node_modules/typescript/lib/lib.es2020.promise.d.ts": {
|
|
226
|
+
"import": "./node_modules/typescript/lib/lib.es2020.promise.d.js",
|
|
227
|
+
"types": "./node_modules/typescript/lib/lib.es2020.promise.d.d.ts"
|
|
228
|
+
},
|
|
229
|
+
"./node_modules/typescript/lib/lib.es2020.sharedmemory.d.ts": {
|
|
230
|
+
"import": "./node_modules/typescript/lib/lib.es2020.sharedmemory.d.js",
|
|
231
|
+
"types": "./node_modules/typescript/lib/lib.es2020.sharedmemory.d.d.ts"
|
|
232
|
+
},
|
|
233
|
+
"./node_modules/typescript/lib/lib.es2020.string.d.ts": {
|
|
234
|
+
"import": "./node_modules/typescript/lib/lib.es2020.string.d.js",
|
|
235
|
+
"types": "./node_modules/typescript/lib/lib.es2020.string.d.d.ts"
|
|
236
|
+
},
|
|
237
|
+
"./node_modules/typescript/lib/lib.es2020.symbol.wellknown.d.ts": {
|
|
238
|
+
"import": "./node_modules/typescript/lib/lib.es2020.symbol.wellknown.d.js",
|
|
239
|
+
"types": "./node_modules/typescript/lib/lib.es2020.symbol.wellknown.d.d.ts"
|
|
240
|
+
},
|
|
241
|
+
"./node_modules/typescript/lib/lib.es2021.d.ts": {
|
|
242
|
+
"import": "./node_modules/typescript/lib/lib.es2021.d.js",
|
|
243
|
+
"types": "./node_modules/typescript/lib/lib.es2021.d.d.ts"
|
|
244
|
+
},
|
|
245
|
+
"./node_modules/typescript/lib/lib.es2021.full.d.ts": {
|
|
246
|
+
"import": "./node_modules/typescript/lib/lib.es2021.full.d.js",
|
|
247
|
+
"types": "./node_modules/typescript/lib/lib.es2021.full.d.d.ts"
|
|
248
|
+
},
|
|
249
|
+
"./node_modules/typescript/lib/lib.es2021.intl.d.ts": {
|
|
250
|
+
"import": "./node_modules/typescript/lib/lib.es2021.intl.d.js",
|
|
251
|
+
"types": "./node_modules/typescript/lib/lib.es2021.intl.d.d.ts"
|
|
252
|
+
},
|
|
253
|
+
"./node_modules/typescript/lib/lib.es2021.promise.d.ts": {
|
|
254
|
+
"import": "./node_modules/typescript/lib/lib.es2021.promise.d.js",
|
|
255
|
+
"types": "./node_modules/typescript/lib/lib.es2021.promise.d.d.ts"
|
|
256
|
+
},
|
|
257
|
+
"./node_modules/typescript/lib/lib.es2021.string.d.ts": {
|
|
258
|
+
"import": "./node_modules/typescript/lib/lib.es2021.string.d.js",
|
|
259
|
+
"types": "./node_modules/typescript/lib/lib.es2021.string.d.d.ts"
|
|
260
|
+
},
|
|
261
|
+
"./node_modules/typescript/lib/lib.es2021.weakref.d.ts": {
|
|
262
|
+
"import": "./node_modules/typescript/lib/lib.es2021.weakref.d.js",
|
|
263
|
+
"types": "./node_modules/typescript/lib/lib.es2021.weakref.d.d.ts"
|
|
264
|
+
},
|
|
265
|
+
"./node_modules/typescript/lib/lib.es2022.array.d.ts": {
|
|
266
|
+
"import": "./node_modules/typescript/lib/lib.es2022.array.d.js",
|
|
267
|
+
"types": "./node_modules/typescript/lib/lib.es2022.array.d.d.ts"
|
|
268
|
+
},
|
|
269
|
+
"./node_modules/typescript/lib/lib.es2022.d.ts": {
|
|
270
|
+
"import": "./node_modules/typescript/lib/lib.es2022.d.js",
|
|
271
|
+
"types": "./node_modules/typescript/lib/lib.es2022.d.d.ts"
|
|
272
|
+
},
|
|
273
|
+
"./node_modules/typescript/lib/lib.es2022.error.d.ts": {
|
|
274
|
+
"import": "./node_modules/typescript/lib/lib.es2022.error.d.js",
|
|
275
|
+
"types": "./node_modules/typescript/lib/lib.es2022.error.d.d.ts"
|
|
276
|
+
},
|
|
277
|
+
"./node_modules/typescript/lib/lib.es2022.full.d.ts": {
|
|
278
|
+
"import": "./node_modules/typescript/lib/lib.es2022.full.d.js",
|
|
279
|
+
"types": "./node_modules/typescript/lib/lib.es2022.full.d.d.ts"
|
|
280
|
+
},
|
|
281
|
+
"./node_modules/typescript/lib/lib.es2022.intl.d.ts": {
|
|
282
|
+
"import": "./node_modules/typescript/lib/lib.es2022.intl.d.js",
|
|
283
|
+
"types": "./node_modules/typescript/lib/lib.es2022.intl.d.d.ts"
|
|
284
|
+
},
|
|
285
|
+
"./node_modules/typescript/lib/lib.es2022.object.d.ts": {
|
|
286
|
+
"import": "./node_modules/typescript/lib/lib.es2022.object.d.js",
|
|
287
|
+
"types": "./node_modules/typescript/lib/lib.es2022.object.d.d.ts"
|
|
288
|
+
},
|
|
289
|
+
"./node_modules/typescript/lib/lib.es2022.regexp.d.ts": {
|
|
290
|
+
"import": "./node_modules/typescript/lib/lib.es2022.regexp.d.js",
|
|
291
|
+
"types": "./node_modules/typescript/lib/lib.es2022.regexp.d.d.ts"
|
|
292
|
+
},
|
|
293
|
+
"./node_modules/typescript/lib/lib.es2022.string.d.ts": {
|
|
294
|
+
"import": "./node_modules/typescript/lib/lib.es2022.string.d.js",
|
|
295
|
+
"types": "./node_modules/typescript/lib/lib.es2022.string.d.d.ts"
|
|
296
|
+
},
|
|
297
|
+
"./node_modules/typescript/lib/lib.es2023.array.d.ts": {
|
|
298
|
+
"import": "./node_modules/typescript/lib/lib.es2023.array.d.js",
|
|
299
|
+
"types": "./node_modules/typescript/lib/lib.es2023.array.d.d.ts"
|
|
300
|
+
},
|
|
301
|
+
"./node_modules/typescript/lib/lib.es2023.collection.d.ts": {
|
|
302
|
+
"import": "./node_modules/typescript/lib/lib.es2023.collection.d.js",
|
|
303
|
+
"types": "./node_modules/typescript/lib/lib.es2023.collection.d.d.ts"
|
|
304
|
+
},
|
|
305
|
+
"./node_modules/typescript/lib/lib.es2023.d.ts": {
|
|
306
|
+
"import": "./node_modules/typescript/lib/lib.es2023.d.js",
|
|
307
|
+
"types": "./node_modules/typescript/lib/lib.es2023.d.d.ts"
|
|
308
|
+
},
|
|
309
|
+
"./node_modules/typescript/lib/lib.es2023.full.d.ts": {
|
|
310
|
+
"import": "./node_modules/typescript/lib/lib.es2023.full.d.js",
|
|
311
|
+
"types": "./node_modules/typescript/lib/lib.es2023.full.d.d.ts"
|
|
312
|
+
},
|
|
313
|
+
"./node_modules/typescript/lib/lib.es2023.intl.d.ts": {
|
|
314
|
+
"import": "./node_modules/typescript/lib/lib.es2023.intl.d.js",
|
|
315
|
+
"types": "./node_modules/typescript/lib/lib.es2023.intl.d.d.ts"
|
|
316
|
+
},
|
|
317
|
+
"./node_modules/typescript/lib/lib.es2024.arraybuffer.d.ts": {
|
|
318
|
+
"import": "./node_modules/typescript/lib/lib.es2024.arraybuffer.d.js",
|
|
319
|
+
"types": "./node_modules/typescript/lib/lib.es2024.arraybuffer.d.d.ts"
|
|
320
|
+
},
|
|
321
|
+
"./node_modules/typescript/lib/lib.es2024.collection.d.ts": {
|
|
322
|
+
"import": "./node_modules/typescript/lib/lib.es2024.collection.d.js",
|
|
323
|
+
"types": "./node_modules/typescript/lib/lib.es2024.collection.d.d.ts"
|
|
324
|
+
},
|
|
325
|
+
"./node_modules/typescript/lib/lib.es2024.d.ts": {
|
|
326
|
+
"import": "./node_modules/typescript/lib/lib.es2024.d.js",
|
|
327
|
+
"types": "./node_modules/typescript/lib/lib.es2024.d.d.ts"
|
|
328
|
+
},
|
|
329
|
+
"./node_modules/typescript/lib/lib.es2024.full.d.ts": {
|
|
330
|
+
"import": "./node_modules/typescript/lib/lib.es2024.full.d.js",
|
|
331
|
+
"types": "./node_modules/typescript/lib/lib.es2024.full.d.d.ts"
|
|
332
|
+
},
|
|
333
|
+
"./node_modules/typescript/lib/lib.es2024.object.d.ts": {
|
|
334
|
+
"import": "./node_modules/typescript/lib/lib.es2024.object.d.js",
|
|
335
|
+
"types": "./node_modules/typescript/lib/lib.es2024.object.d.d.ts"
|
|
336
|
+
},
|
|
337
|
+
"./node_modules/typescript/lib/lib.es2024.promise.d.ts": {
|
|
338
|
+
"import": "./node_modules/typescript/lib/lib.es2024.promise.d.js",
|
|
339
|
+
"types": "./node_modules/typescript/lib/lib.es2024.promise.d.d.ts"
|
|
340
|
+
},
|
|
341
|
+
"./node_modules/typescript/lib/lib.es2024.regexp.d.ts": {
|
|
342
|
+
"import": "./node_modules/typescript/lib/lib.es2024.regexp.d.js",
|
|
343
|
+
"types": "./node_modules/typescript/lib/lib.es2024.regexp.d.d.ts"
|
|
344
|
+
},
|
|
345
|
+
"./node_modules/typescript/lib/lib.es2024.sharedmemory.d.ts": {
|
|
346
|
+
"import": "./node_modules/typescript/lib/lib.es2024.sharedmemory.d.js",
|
|
347
|
+
"types": "./node_modules/typescript/lib/lib.es2024.sharedmemory.d.d.ts"
|
|
348
|
+
},
|
|
349
|
+
"./node_modules/typescript/lib/lib.es2024.string.d.ts": {
|
|
350
|
+
"import": "./node_modules/typescript/lib/lib.es2024.string.d.js",
|
|
351
|
+
"types": "./node_modules/typescript/lib/lib.es2024.string.d.d.ts"
|
|
352
|
+
},
|
|
353
|
+
"./node_modules/typescript/lib/lib.es5.d.ts": {
|
|
354
|
+
"import": "./node_modules/typescript/lib/lib.es5.d.js",
|
|
355
|
+
"types": "./node_modules/typescript/lib/lib.es5.d.d.ts"
|
|
356
|
+
},
|
|
357
|
+
"./node_modules/typescript/lib/lib.es6.d.ts": {
|
|
358
|
+
"import": "./node_modules/typescript/lib/lib.es6.d.js",
|
|
359
|
+
"types": "./node_modules/typescript/lib/lib.es6.d.d.ts"
|
|
360
|
+
},
|
|
361
|
+
"./node_modules/typescript/lib/lib.esnext.array.d.ts": {
|
|
362
|
+
"import": "./node_modules/typescript/lib/lib.esnext.array.d.js",
|
|
363
|
+
"types": "./node_modules/typescript/lib/lib.esnext.array.d.d.ts"
|
|
364
|
+
},
|
|
365
|
+
"./node_modules/typescript/lib/lib.esnext.collection.d.ts": {
|
|
366
|
+
"import": "./node_modules/typescript/lib/lib.esnext.collection.d.js",
|
|
367
|
+
"types": "./node_modules/typescript/lib/lib.esnext.collection.d.d.ts"
|
|
368
|
+
},
|
|
369
|
+
"./node_modules/typescript/lib/lib.esnext.d.ts": {
|
|
370
|
+
"import": "./node_modules/typescript/lib/lib.esnext.d.js",
|
|
371
|
+
"types": "./node_modules/typescript/lib/lib.esnext.d.d.ts"
|
|
372
|
+
},
|
|
373
|
+
"./node_modules/typescript/lib/lib.esnext.decorators.d.ts": {
|
|
374
|
+
"import": "./node_modules/typescript/lib/lib.esnext.decorators.d.js",
|
|
375
|
+
"types": "./node_modules/typescript/lib/lib.esnext.decorators.d.d.ts"
|
|
376
|
+
},
|
|
377
|
+
"./node_modules/typescript/lib/lib.esnext.disposable.d.ts": {
|
|
378
|
+
"import": "./node_modules/typescript/lib/lib.esnext.disposable.d.js",
|
|
379
|
+
"types": "./node_modules/typescript/lib/lib.esnext.disposable.d.d.ts"
|
|
380
|
+
},
|
|
381
|
+
"./node_modules/typescript/lib/lib.esnext.error.d.ts": {
|
|
382
|
+
"import": "./node_modules/typescript/lib/lib.esnext.error.d.js",
|
|
383
|
+
"types": "./node_modules/typescript/lib/lib.esnext.error.d.d.ts"
|
|
384
|
+
},
|
|
385
|
+
"./node_modules/typescript/lib/lib.esnext.float16.d.ts": {
|
|
386
|
+
"import": "./node_modules/typescript/lib/lib.esnext.float16.d.js",
|
|
387
|
+
"types": "./node_modules/typescript/lib/lib.esnext.float16.d.d.ts"
|
|
388
|
+
},
|
|
389
|
+
"./node_modules/typescript/lib/lib.esnext.full.d.ts": {
|
|
390
|
+
"import": "./node_modules/typescript/lib/lib.esnext.full.d.js",
|
|
391
|
+
"types": "./node_modules/typescript/lib/lib.esnext.full.d.d.ts"
|
|
392
|
+
},
|
|
393
|
+
"./node_modules/typescript/lib/lib.esnext.intl.d.ts": {
|
|
394
|
+
"import": "./node_modules/typescript/lib/lib.esnext.intl.d.js",
|
|
395
|
+
"types": "./node_modules/typescript/lib/lib.esnext.intl.d.d.ts"
|
|
396
|
+
},
|
|
397
|
+
"./node_modules/typescript/lib/lib.esnext.iterator.d.ts": {
|
|
398
|
+
"import": "./node_modules/typescript/lib/lib.esnext.iterator.d.js",
|
|
399
|
+
"types": "./node_modules/typescript/lib/lib.esnext.iterator.d.d.ts"
|
|
400
|
+
},
|
|
401
|
+
"./node_modules/typescript/lib/lib.esnext.promise.d.ts": {
|
|
402
|
+
"import": "./node_modules/typescript/lib/lib.esnext.promise.d.js",
|
|
403
|
+
"types": "./node_modules/typescript/lib/lib.esnext.promise.d.d.ts"
|
|
404
|
+
},
|
|
405
|
+
"./node_modules/typescript/lib/lib.esnext.sharedmemory.d.ts": {
|
|
406
|
+
"import": "./node_modules/typescript/lib/lib.esnext.sharedmemory.d.js",
|
|
407
|
+
"types": "./node_modules/typescript/lib/lib.esnext.sharedmemory.d.d.ts"
|
|
408
|
+
},
|
|
409
|
+
"./node_modules/typescript/lib/lib.scripthost.d.ts": {
|
|
410
|
+
"import": "./node_modules/typescript/lib/lib.scripthost.d.js",
|
|
411
|
+
"types": "./node_modules/typescript/lib/lib.scripthost.d.d.ts"
|
|
412
|
+
},
|
|
413
|
+
"./node_modules/typescript/lib/lib.webworker.asynciterable.d.ts": {
|
|
414
|
+
"import": "./node_modules/typescript/lib/lib.webworker.asynciterable.d.js",
|
|
415
|
+
"types": "./node_modules/typescript/lib/lib.webworker.asynciterable.d.d.ts"
|
|
416
|
+
},
|
|
417
|
+
"./node_modules/typescript/lib/lib.webworker.d.ts": {
|
|
418
|
+
"import": "./node_modules/typescript/lib/lib.webworker.d.js",
|
|
419
|
+
"types": "./node_modules/typescript/lib/lib.webworker.d.d.ts"
|
|
420
|
+
},
|
|
421
|
+
"./node_modules/typescript/lib/lib.webworker.importscripts.d.ts": {
|
|
422
|
+
"import": "./node_modules/typescript/lib/lib.webworker.importscripts.d.js",
|
|
423
|
+
"types": "./node_modules/typescript/lib/lib.webworker.importscripts.d.d.ts"
|
|
424
|
+
},
|
|
425
|
+
"./node_modules/typescript/lib/lib.webworker.iterable.d.ts": {
|
|
426
|
+
"import": "./node_modules/typescript/lib/lib.webworker.iterable.d.js",
|
|
427
|
+
"types": "./node_modules/typescript/lib/lib.webworker.iterable.d.d.ts"
|
|
428
|
+
},
|
|
429
|
+
"./node_modules/typescript/lib/tsserverlibrary.d.ts": {
|
|
430
|
+
"import": "./node_modules/typescript/lib/tsserverlibrary.d.js",
|
|
431
|
+
"types": "./node_modules/typescript/lib/tsserverlibrary.d.d.ts"
|
|
432
|
+
},
|
|
433
|
+
"./node_modules/typescript/lib/typescript.d.ts": {
|
|
434
|
+
"import": "./node_modules/typescript/lib/typescript.d.js",
|
|
435
|
+
"types": "./node_modules/typescript/lib/typescript.d.d.ts"
|
|
436
|
+
},
|
|
437
|
+
"./service.test.ts": {
|
|
438
|
+
"import": "./service.test.js",
|
|
439
|
+
"types": "./service.test.d.ts"
|
|
440
|
+
},
|
|
441
|
+
"./service.ts": {
|
|
442
|
+
"import": "./service.js",
|
|
443
|
+
"types": "./service.d.ts"
|
|
444
|
+
},
|
|
445
|
+
"./type.ts": {
|
|
446
|
+
"import": "./type.js",
|
|
447
|
+
"types": "./type.d.ts"
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
package/service.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { FeatureFlipperConfiguration, FeatureFlipperService } from './type.ts';
|
|
2
|
+
import type { ReadableRawKeyValueStore } from '@dungarees/key-value-stores/type.ts';
|
|
3
|
+
export declare const URL_OVERRIDE_FEATURE_NAME = "url_override";
|
|
4
|
+
export declare const createFeatureFlipperService: <const CONFIG extends readonly FeatureFlipperConfiguration[]>({ featureFlippers, envVarStore, }: {
|
|
5
|
+
featureFlippers: CONFIG;
|
|
6
|
+
envVarStore: ReadableRawKeyValueStore<string | undefined>;
|
|
7
|
+
}) => FeatureFlipperService<CONFIG>;
|
package/service.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { getIdentityValue } from './identity.js';
|
|
2
|
+
import { createMemoryRawKeyValueStore } from '@dungarees/key-value-stores/raw-stores/memory.ts';
|
|
3
|
+
import { createTransformedStore } from '@dungarees/key-value-stores/service-transform.ts';
|
|
4
|
+
// The feature that has to be on before any override is honoured, so a url cannot turn features on
|
|
5
|
+
// in an environment that did not opt into that.
|
|
6
|
+
export const URL_OVERRIDE_FEATURE_NAME = 'url_override';
|
|
7
|
+
const PROCESS_ENV_PREFIX = 'FF_';
|
|
8
|
+
// Vite only exposes variables to the browser under its public prefix, so a flag that has to reach
|
|
9
|
+
// the client is named this way instead.
|
|
10
|
+
const VITE_ENV_PREFIX = 'PUBLIC_FF_';
|
|
11
|
+
export const createFeatureFlipperService = ({ featureFlippers, envVarStore, }) => {
|
|
12
|
+
const readEnvVar = (name, store) => {
|
|
13
|
+
const upperCasedName = name.toUpperCase();
|
|
14
|
+
const value = store.get(`${PROCESS_ENV_PREFIX}${upperCasedName}`) ??
|
|
15
|
+
store.get(`${VITE_ENV_PREFIX}${upperCasedName}`);
|
|
16
|
+
return value !== undefined ? value === 'true' : undefined;
|
|
17
|
+
};
|
|
18
|
+
const transformedEnvVarStore = createTransformedStore(envVarStore, readEnvVar);
|
|
19
|
+
const overrideStore = createMemoryRawKeyValueStore();
|
|
20
|
+
const identityStore = createMemoryRawKeyValueStore();
|
|
21
|
+
// Highest precedence first: an identity verdict beats an override, which beats the environment.
|
|
22
|
+
const stores = [identityStore, overrideStore, transformedEnvVarStore];
|
|
23
|
+
const getConfiguration = (name) => featureFlippers.find(({ name: configuredName }) => configuredName === name);
|
|
24
|
+
const isEnabledByName = (name, identity) => {
|
|
25
|
+
const feature = getConfiguration(name);
|
|
26
|
+
// Recorded rather than merely read, so a later call without an identity keeps the verdict the
|
|
27
|
+
// identity already earned instead of falling back to the configured value.
|
|
28
|
+
if (feature !== undefined && identity !== undefined) {
|
|
29
|
+
identityStore.set(name, getIdentityValue({ feature, identity }));
|
|
30
|
+
}
|
|
31
|
+
const decided = stores.map((store) => store.get(name)).find((value) => value !== undefined);
|
|
32
|
+
return decided ?? feature?.value ?? false;
|
|
33
|
+
};
|
|
34
|
+
return {
|
|
35
|
+
isEnabled: ({ name, identity }) => isEnabledByName(name, identity),
|
|
36
|
+
setOverride: ({ name, value }) => {
|
|
37
|
+
if (isEnabledByName(URL_OVERRIDE_FEATURE_NAME)) {
|
|
38
|
+
overrideStore.set(name, value);
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
isFeatureFlipperName: (name) => getConfiguration(name) !== undefined,
|
|
42
|
+
};
|
|
43
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/service.test.js
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { createFeatureFlipperService } from './service.js';
|
|
2
|
+
import { createMemoryRawKeyValueStore } from '@dungarees/key-value-stores/raw-stores/memory.ts';
|
|
3
|
+
import { expect, expectTypeOf, test } from 'vitest';
|
|
4
|
+
const CONFIG = [
|
|
5
|
+
{
|
|
6
|
+
name: 'feature_1',
|
|
7
|
+
value: false,
|
|
8
|
+
identity: {
|
|
9
|
+
id1: { type: 'regex', value: /.*@example\.com/ },
|
|
10
|
+
id2: { type: 'regex', value: /.*@example\.com/ },
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
{ name: 'feature_2', value: true },
|
|
14
|
+
{ name: 'url_override', value: true },
|
|
15
|
+
{
|
|
16
|
+
name: 'feature_3',
|
|
17
|
+
value: true,
|
|
18
|
+
identity: {
|
|
19
|
+
id1: { type: 'percentage', value: 20 },
|
|
20
|
+
id2: { type: 'percentage', value: 50 },
|
|
21
|
+
id3: { type: 'percentage', value: 90 },
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
];
|
|
25
|
+
const createFfService = () => {
|
|
26
|
+
const envVarStore = createMemoryRawKeyValueStore();
|
|
27
|
+
return {
|
|
28
|
+
ffService: createFeatureFlipperService({ featureFlippers: CONFIG, envVarStore }),
|
|
29
|
+
envVarStore,
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
test('a feature falls back to the value its configuration declares', () => {
|
|
33
|
+
const { ffService } = createFfService();
|
|
34
|
+
expect(ffService.isEnabled({ name: 'feature_1' })).toBe(false);
|
|
35
|
+
expect(ffService.isEnabled({ name: 'feature_2' })).toBe(true);
|
|
36
|
+
});
|
|
37
|
+
test('an FF_ prefixed environment variable overrides the configured value', () => {
|
|
38
|
+
const { ffService, envVarStore } = createFfService();
|
|
39
|
+
envVarStore.set('FF_FEATURE_1', 'true');
|
|
40
|
+
expect(ffService.isEnabled({ name: 'feature_1' })).toBe(true);
|
|
41
|
+
});
|
|
42
|
+
test('a PUBLIC_FF_ prefixed environment variable overrides the configured value', () => {
|
|
43
|
+
const { ffService, envVarStore } = createFfService();
|
|
44
|
+
envVarStore.set('PUBLIC_FF_FEATURE_1', 'true');
|
|
45
|
+
expect(ffService.isEnabled({ name: 'feature_1' })).toBe(true);
|
|
46
|
+
});
|
|
47
|
+
test('an environment variable holding anything but true reads as false', () => {
|
|
48
|
+
const { ffService, envVarStore } = createFfService();
|
|
49
|
+
envVarStore.set('FF_FEATURE_2', 'false');
|
|
50
|
+
expect(ffService.isEnabled({ name: 'feature_2' })).toBe(false);
|
|
51
|
+
});
|
|
52
|
+
test('an override applies while url_override is enabled', () => {
|
|
53
|
+
const { ffService } = createFfService();
|
|
54
|
+
ffService.setOverride({ name: 'feature_2', value: false });
|
|
55
|
+
expect(ffService.isEnabled({ name: 'feature_2' })).toBe(false);
|
|
56
|
+
});
|
|
57
|
+
test('an override is ignored while url_override is disabled', () => {
|
|
58
|
+
const { ffService, envVarStore } = createFfService();
|
|
59
|
+
envVarStore.set('FF_URL_OVERRIDE', 'false');
|
|
60
|
+
ffService.setOverride({ name: 'feature_2', value: false });
|
|
61
|
+
expect(ffService.isEnabled({ name: 'feature_2' })).toBe(true);
|
|
62
|
+
});
|
|
63
|
+
test('an override beats an environment variable', () => {
|
|
64
|
+
const { ffService, envVarStore } = createFfService();
|
|
65
|
+
envVarStore.set('PUBLIC_FF_FEATURE_2', 'true');
|
|
66
|
+
ffService.setOverride({ name: 'feature_2', value: false });
|
|
67
|
+
expect(ffService.isEnabled({ name: 'feature_2' })).toBe(false);
|
|
68
|
+
});
|
|
69
|
+
test('an identity failing one rule disables the feature', () => {
|
|
70
|
+
const { ffService } = createFfService();
|
|
71
|
+
expect(ffService.isEnabled({
|
|
72
|
+
name: 'feature_1',
|
|
73
|
+
identity: { id1: 'example', id2: 'yy@example.com' },
|
|
74
|
+
})).toBe(false);
|
|
75
|
+
});
|
|
76
|
+
test('an identity satisfying every rule enables the feature', () => {
|
|
77
|
+
const { ffService } = createFfService();
|
|
78
|
+
expect(ffService.isEnabled({
|
|
79
|
+
name: 'feature_1',
|
|
80
|
+
identity: { id1: 'xx@example.com', id2: 'yy@example.com' },
|
|
81
|
+
})).toBe(true);
|
|
82
|
+
});
|
|
83
|
+
test('an identity verdict beats an environment variable', () => {
|
|
84
|
+
const { ffService, envVarStore } = createFfService();
|
|
85
|
+
envVarStore.set('FF_FEATURE_1', 'true');
|
|
86
|
+
expect(ffService.isEnabled({ name: 'feature_1', identity: { id1: 'example' } })).toBe(false);
|
|
87
|
+
});
|
|
88
|
+
test('a percentage rule enables roughly the share of identities it names', () => {
|
|
89
|
+
const { ffService } = createFfService();
|
|
90
|
+
const identities = Array.from({ length: 65535 }, (_, index) => String.fromCharCode(index));
|
|
91
|
+
const shareEnabledFor = (key) => {
|
|
92
|
+
const enabled = identities.filter((identity) => ffService.isEnabled({ name: 'feature_3', identity: { [key]: identity } })).length;
|
|
93
|
+
return (enabled / identities.length) * 100;
|
|
94
|
+
};
|
|
95
|
+
expect(shareEnabledFor('id1')).toBeCloseTo(20, 1);
|
|
96
|
+
expect(shareEnabledFor('id2')).toBeCloseTo(50, 1);
|
|
97
|
+
expect(shareEnabledFor('id3')).toBeCloseTo(90, 1);
|
|
98
|
+
});
|
|
99
|
+
test('a feature the configuration does not declare reads as disabled', () => {
|
|
100
|
+
const { ffService } = createFfService();
|
|
101
|
+
// @ts-expect-error 'unknown_feature' is not one of the configured names
|
|
102
|
+
expect(ffService.isEnabled({ name: 'unknown_feature' })).toBe(false);
|
|
103
|
+
});
|
|
104
|
+
test('isFeatureFlipperName recognises a configured name', () => {
|
|
105
|
+
const { ffService } = createFfService();
|
|
106
|
+
expect(ffService.isFeatureFlipperName('feature_1')).toBe(true);
|
|
107
|
+
});
|
|
108
|
+
test('isFeatureFlipperName rejects a name that is not configured', () => {
|
|
109
|
+
const { ffService } = createFfService();
|
|
110
|
+
expect(ffService.isFeatureFlipperName('unknown_feature')).toBe(false);
|
|
111
|
+
});
|
|
112
|
+
test('isFeatureFlipperName narrows the string it was given', () => {
|
|
113
|
+
const { ffService } = createFfService();
|
|
114
|
+
const name = 'feature_1';
|
|
115
|
+
if (ffService.isFeatureFlipperName(name)) {
|
|
116
|
+
expectTypeOf(name).toEqualTypeOf();
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
test('isEnabled only accepts the identity keys the feature declares', () => {
|
|
120
|
+
const { ffService } = createFfService();
|
|
121
|
+
// @ts-expect-error feature_1 declares id1 and id2, not id9
|
|
122
|
+
ffService.isEnabled({ name: 'feature_1', identity: { id9: 'someone' } });
|
|
123
|
+
});
|
package/type.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export type IdentityMatcher = {
|
|
2
|
+
type: 'regex';
|
|
3
|
+
value: RegExp;
|
|
4
|
+
} | {
|
|
5
|
+
type: 'percentage';
|
|
6
|
+
value: number;
|
|
7
|
+
};
|
|
8
|
+
export type FeatureFlipperConfiguration = {
|
|
9
|
+
name: string;
|
|
10
|
+
value: boolean;
|
|
11
|
+
identity?: Record<string, IdentityMatcher>;
|
|
12
|
+
};
|
|
13
|
+
export type FeatureFlipperName<CONFIG extends readonly FeatureFlipperConfiguration[]> = CONFIG[number]['name'];
|
|
14
|
+
type GetIdentityRules<CONFIG extends FeatureFlipperConfiguration, NAME extends string> = CONFIG extends {
|
|
15
|
+
name: NAME;
|
|
16
|
+
identity: infer RULES;
|
|
17
|
+
} ? RULES : never;
|
|
18
|
+
export type Identity<CONFIG extends readonly FeatureFlipperConfiguration[], NAME extends FeatureFlipperName<CONFIG>> = GetIdentityRules<CONFIG[number], NAME> extends undefined ? never : Partial<Record<keyof GetIdentityRules<CONFIG[number], NAME>, string>>;
|
|
19
|
+
export type IsEnabledArgs<CONFIG extends readonly FeatureFlipperConfiguration[], NAME extends FeatureFlipperName<CONFIG>> = {
|
|
20
|
+
name: NAME;
|
|
21
|
+
identity?: Identity<CONFIG, NAME>;
|
|
22
|
+
};
|
|
23
|
+
export type FeatureFlipperService<CONFIG extends readonly FeatureFlipperConfiguration[]> = {
|
|
24
|
+
isEnabled: <NAME extends FeatureFlipperName<CONFIG>>(args: IsEnabledArgs<CONFIG, NAME>) => boolean;
|
|
25
|
+
setOverride: (args: {
|
|
26
|
+
name: FeatureFlipperName<CONFIG>;
|
|
27
|
+
value: boolean;
|
|
28
|
+
}) => void;
|
|
29
|
+
isFeatureFlipperName: (name: string) => name is FeatureFlipperName<CONFIG>;
|
|
30
|
+
};
|
|
31
|
+
export type FeatureFlipperServiceByNames<NAMES extends string[]> = Omit<FeatureFlipperService<{
|
|
32
|
+
[KEY in keyof NAMES]: NAMES[KEY] extends string ? {
|
|
33
|
+
name: NAMES[KEY];
|
|
34
|
+
value: boolean;
|
|
35
|
+
} : never;
|
|
36
|
+
}>, 'isFeatureFlipperName'>;
|
|
37
|
+
export {};
|
package/type.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|