@rolloutctrl/evaluator 0.0.1 → 0.0.2
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/dist/index.d.mts +4 -3
- package/dist/index.d.ts +4 -3
- package/dist/index.js +51 -21
- package/dist/index.mjs +51 -21
- package/package.json +2 -1
package/dist/index.d.mts
CHANGED
|
@@ -17,13 +17,14 @@ interface Segment {
|
|
|
17
17
|
}
|
|
18
18
|
interface Strategy {
|
|
19
19
|
id: string;
|
|
20
|
+
flagKey: string;
|
|
20
21
|
enabled: boolean;
|
|
21
22
|
priority: number;
|
|
22
23
|
matchType: MatchType;
|
|
23
24
|
rolloutPercentage: number | null;
|
|
24
25
|
rolloutStickinessField: string | null;
|
|
25
|
-
startsAt: Date | null;
|
|
26
|
-
endsAt: Date | null;
|
|
26
|
+
startsAt: Date | string | null;
|
|
27
|
+
endsAt: Date | string | null;
|
|
27
28
|
segment: Segment | null;
|
|
28
29
|
rules: Rule[];
|
|
29
30
|
}
|
|
@@ -64,7 +65,7 @@ declare function evaluateRules(rules: Rule[], matchType: MatchType, context: Eva
|
|
|
64
65
|
|
|
65
66
|
declare function evaluateSegment(segment: Segment, context: EvaluationContext): boolean;
|
|
66
67
|
|
|
67
|
-
declare function evaluateRollout(
|
|
68
|
+
declare function evaluateRollout(flagKey: string, percentage: number, stickinessField: string | null, context: EvaluationContext): boolean;
|
|
68
69
|
|
|
69
70
|
declare function evaluateStrategy(strategy: Strategy, context: EvaluationContext): boolean;
|
|
70
71
|
declare function evaluateActionStrategy(strategy: ActionStrategy, context: EvaluationContext): ActionEffect | null;
|
package/dist/index.d.ts
CHANGED
|
@@ -17,13 +17,14 @@ interface Segment {
|
|
|
17
17
|
}
|
|
18
18
|
interface Strategy {
|
|
19
19
|
id: string;
|
|
20
|
+
flagKey: string;
|
|
20
21
|
enabled: boolean;
|
|
21
22
|
priority: number;
|
|
22
23
|
matchType: MatchType;
|
|
23
24
|
rolloutPercentage: number | null;
|
|
24
25
|
rolloutStickinessField: string | null;
|
|
25
|
-
startsAt: Date | null;
|
|
26
|
-
endsAt: Date | null;
|
|
26
|
+
startsAt: Date | string | null;
|
|
27
|
+
endsAt: Date | string | null;
|
|
27
28
|
segment: Segment | null;
|
|
28
29
|
rules: Rule[];
|
|
29
30
|
}
|
|
@@ -64,7 +65,7 @@ declare function evaluateRules(rules: Rule[], matchType: MatchType, context: Eva
|
|
|
64
65
|
|
|
65
66
|
declare function evaluateSegment(segment: Segment, context: EvaluationContext): boolean;
|
|
66
67
|
|
|
67
|
-
declare function evaluateRollout(
|
|
68
|
+
declare function evaluateRollout(flagKey: string, percentage: number, stickinessField: string | null, context: EvaluationContext): boolean;
|
|
68
69
|
|
|
69
70
|
declare function evaluateStrategy(strategy: Strategy, context: EvaluationContext): boolean;
|
|
70
71
|
declare function evaluateActionStrategy(strategy: ActionStrategy, context: EvaluationContext): ActionEffect | null;
|
package/dist/index.js
CHANGED
|
@@ -44,48 +44,61 @@ function getFieldValue(context, field) {
|
|
|
44
44
|
}
|
|
45
45
|
return current;
|
|
46
46
|
}
|
|
47
|
+
function normalizeRuleValue(value) {
|
|
48
|
+
if (typeof value === "string") {
|
|
49
|
+
const trimmed = value.trim();
|
|
50
|
+
if (trimmed.startsWith("[") || trimmed.startsWith("{")) {
|
|
51
|
+
try {
|
|
52
|
+
return JSON.parse(trimmed);
|
|
53
|
+
} catch {
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return value;
|
|
58
|
+
}
|
|
47
59
|
function toNumber(value) {
|
|
48
60
|
const n = Number(value);
|
|
49
61
|
return isNaN(n) ? null : n;
|
|
50
62
|
}
|
|
51
63
|
function applyOperator(operator, contextValue, ruleValue) {
|
|
64
|
+
const normalizedValue = normalizeRuleValue(ruleValue);
|
|
52
65
|
switch (operator) {
|
|
53
66
|
case "EQUALS":
|
|
54
|
-
return String(contextValue) === String(
|
|
67
|
+
return String(contextValue) === String(normalizedValue);
|
|
55
68
|
case "IN": {
|
|
56
|
-
const list = Array.isArray(
|
|
57
|
-
return list.
|
|
69
|
+
const list = Array.isArray(normalizedValue) ? normalizedValue : [normalizedValue];
|
|
70
|
+
return list.includes(contextValue);
|
|
58
71
|
}
|
|
59
72
|
case "INCLUDES": {
|
|
60
73
|
if (!Array.isArray(contextValue)) return false;
|
|
61
|
-
return contextValue.
|
|
74
|
+
return contextValue.includes(normalizedValue);
|
|
62
75
|
}
|
|
63
76
|
case "GT": {
|
|
64
77
|
const a = toNumber(contextValue);
|
|
65
|
-
const b = toNumber(
|
|
78
|
+
const b = toNumber(normalizedValue);
|
|
66
79
|
return a !== null && b !== null && a > b;
|
|
67
80
|
}
|
|
68
81
|
case "LT": {
|
|
69
82
|
const a = toNumber(contextValue);
|
|
70
|
-
const b = toNumber(
|
|
83
|
+
const b = toNumber(normalizedValue);
|
|
71
84
|
return a !== null && b !== null && a < b;
|
|
72
85
|
}
|
|
73
86
|
case "GTE": {
|
|
74
87
|
const a = toNumber(contextValue);
|
|
75
|
-
const b = toNumber(
|
|
88
|
+
const b = toNumber(normalizedValue);
|
|
76
89
|
return a !== null && b !== null && a >= b;
|
|
77
90
|
}
|
|
78
91
|
case "LTE": {
|
|
79
92
|
const a = toNumber(contextValue);
|
|
80
|
-
const b = toNumber(
|
|
93
|
+
const b = toNumber(normalizedValue);
|
|
81
94
|
return a !== null && b !== null && a <= b;
|
|
82
95
|
}
|
|
83
96
|
case "CONTAINS":
|
|
84
|
-
return typeof contextValue === "string" && contextValue.includes(
|
|
97
|
+
return typeof contextValue === "string" && typeof normalizedValue === "string" && contextValue.includes(normalizedValue);
|
|
85
98
|
case "STARTS_WITH":
|
|
86
|
-
return typeof contextValue === "string" && contextValue.startsWith(
|
|
99
|
+
return typeof contextValue === "string" && typeof normalizedValue === "string" && contextValue.startsWith(normalizedValue);
|
|
87
100
|
case "ENDS_WITH":
|
|
88
|
-
return typeof contextValue === "string" && contextValue.endsWith(
|
|
101
|
+
return typeof contextValue === "string" && typeof normalizedValue === "string" && contextValue.endsWith(normalizedValue);
|
|
89
102
|
default:
|
|
90
103
|
return false;
|
|
91
104
|
}
|
|
@@ -98,7 +111,13 @@ function evaluateOperator(operator, not, field, ruleValue, context) {
|
|
|
98
111
|
|
|
99
112
|
// src/rule.ts
|
|
100
113
|
function evaluateRule(rule, context) {
|
|
101
|
-
return evaluateOperator(
|
|
114
|
+
return evaluateOperator(
|
|
115
|
+
rule.operator,
|
|
116
|
+
rule.not,
|
|
117
|
+
rule.field,
|
|
118
|
+
rule.value,
|
|
119
|
+
context
|
|
120
|
+
);
|
|
102
121
|
}
|
|
103
122
|
function evaluateRules(rules, matchType, context) {
|
|
104
123
|
if (rules.length === 0) return true;
|
|
@@ -116,6 +135,9 @@ function evaluateSegment(segment, context) {
|
|
|
116
135
|
}
|
|
117
136
|
|
|
118
137
|
// src/rollout.ts
|
|
138
|
+
function getNestedValue(input, path) {
|
|
139
|
+
return path.split(".").reduce((obj, key) => obj?.[key], input);
|
|
140
|
+
}
|
|
119
141
|
function hashString(input) {
|
|
120
142
|
let hash = 5381;
|
|
121
143
|
for (let i = 0; i < input.length; i++) {
|
|
@@ -124,11 +146,12 @@ function hashString(input) {
|
|
|
124
146
|
}
|
|
125
147
|
return hash;
|
|
126
148
|
}
|
|
127
|
-
function evaluateRollout(
|
|
149
|
+
function evaluateRollout(flagKey, percentage, stickinessField, context) {
|
|
128
150
|
if (percentage <= 0) return false;
|
|
129
151
|
if (percentage >= 100) return true;
|
|
130
|
-
const
|
|
131
|
-
|
|
152
|
+
const stickinessValue = stickinessField ? getNestedValue(context, stickinessField) : context["userId"];
|
|
153
|
+
if (stickinessValue === void 0 || stickinessValue === null) return false;
|
|
154
|
+
const seed = `${flagKey}:${String(stickinessValue)}`;
|
|
132
155
|
const hash = hashString(seed);
|
|
133
156
|
const bucket = hash % 100;
|
|
134
157
|
return bucket < percentage;
|
|
@@ -137,8 +160,8 @@ function evaluateRollout(strategyId, percentage, stickinessField, context) {
|
|
|
137
160
|
// src/strategy.ts
|
|
138
161
|
function isWithinTimeWindow(startsAt, endsAt) {
|
|
139
162
|
const now = /* @__PURE__ */ new Date();
|
|
140
|
-
if (startsAt
|
|
141
|
-
if (endsAt
|
|
163
|
+
if (startsAt && new Date(startsAt) > now) return false;
|
|
164
|
+
if (endsAt && new Date(endsAt) < now) return false;
|
|
142
165
|
return true;
|
|
143
166
|
}
|
|
144
167
|
function evaluateStrategy(strategy, context) {
|
|
@@ -147,11 +170,12 @@ function evaluateStrategy(strategy, context) {
|
|
|
147
170
|
if (strategy.segment !== null) {
|
|
148
171
|
if (!evaluateSegment(strategy.segment, context)) return false;
|
|
149
172
|
} else if (strategy.rules.length > 0) {
|
|
150
|
-
if (!evaluateRules(strategy.rules, strategy.matchType, context))
|
|
173
|
+
if (!evaluateRules(strategy.rules, strategy.matchType, context))
|
|
174
|
+
return false;
|
|
151
175
|
}
|
|
152
176
|
if (strategy.rolloutPercentage !== null) {
|
|
153
177
|
return evaluateRollout(
|
|
154
|
-
strategy.
|
|
178
|
+
strategy.flagKey,
|
|
155
179
|
strategy.rolloutPercentage,
|
|
156
180
|
strategy.rolloutStickinessField,
|
|
157
181
|
context
|
|
@@ -164,7 +188,8 @@ function evaluateActionStrategy(strategy, context) {
|
|
|
164
188
|
if (strategy.segment !== null) {
|
|
165
189
|
if (!evaluateSegment(strategy.segment, context)) return null;
|
|
166
190
|
} else if (strategy.rules.length > 0) {
|
|
167
|
-
if (!evaluateRules(strategy.rules, strategy.matchType, context))
|
|
191
|
+
if (!evaluateRules(strategy.rules, strategy.matchType, context))
|
|
192
|
+
return null;
|
|
168
193
|
}
|
|
169
194
|
return strategy.effect;
|
|
170
195
|
}
|
|
@@ -174,7 +199,12 @@ function evaluateFeatureFlag(flagEnv, context) {
|
|
|
174
199
|
if (!flagEnv.enabled) {
|
|
175
200
|
return { enabled: false, strategyId: null };
|
|
176
201
|
}
|
|
177
|
-
|
|
202
|
+
if (!flagEnv.strategies || flagEnv.strategies.length === 0) {
|
|
203
|
+
return { enabled: true, strategyId: null };
|
|
204
|
+
}
|
|
205
|
+
const sorted = [...flagEnv.strategies].sort(
|
|
206
|
+
(a, b) => b.priority - a.priority
|
|
207
|
+
);
|
|
178
208
|
for (const strategy of sorted) {
|
|
179
209
|
if (evaluateStrategy(strategy, context)) {
|
|
180
210
|
return { enabled: true, strategyId: strategy.id };
|
package/dist/index.mjs
CHANGED
|
@@ -10,48 +10,61 @@ function getFieldValue(context, field) {
|
|
|
10
10
|
}
|
|
11
11
|
return current;
|
|
12
12
|
}
|
|
13
|
+
function normalizeRuleValue(value) {
|
|
14
|
+
if (typeof value === "string") {
|
|
15
|
+
const trimmed = value.trim();
|
|
16
|
+
if (trimmed.startsWith("[") || trimmed.startsWith("{")) {
|
|
17
|
+
try {
|
|
18
|
+
return JSON.parse(trimmed);
|
|
19
|
+
} catch {
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return value;
|
|
24
|
+
}
|
|
13
25
|
function toNumber(value) {
|
|
14
26
|
const n = Number(value);
|
|
15
27
|
return isNaN(n) ? null : n;
|
|
16
28
|
}
|
|
17
29
|
function applyOperator(operator, contextValue, ruleValue) {
|
|
30
|
+
const normalizedValue = normalizeRuleValue(ruleValue);
|
|
18
31
|
switch (operator) {
|
|
19
32
|
case "EQUALS":
|
|
20
|
-
return String(contextValue) === String(
|
|
33
|
+
return String(contextValue) === String(normalizedValue);
|
|
21
34
|
case "IN": {
|
|
22
|
-
const list = Array.isArray(
|
|
23
|
-
return list.
|
|
35
|
+
const list = Array.isArray(normalizedValue) ? normalizedValue : [normalizedValue];
|
|
36
|
+
return list.includes(contextValue);
|
|
24
37
|
}
|
|
25
38
|
case "INCLUDES": {
|
|
26
39
|
if (!Array.isArray(contextValue)) return false;
|
|
27
|
-
return contextValue.
|
|
40
|
+
return contextValue.includes(normalizedValue);
|
|
28
41
|
}
|
|
29
42
|
case "GT": {
|
|
30
43
|
const a = toNumber(contextValue);
|
|
31
|
-
const b = toNumber(
|
|
44
|
+
const b = toNumber(normalizedValue);
|
|
32
45
|
return a !== null && b !== null && a > b;
|
|
33
46
|
}
|
|
34
47
|
case "LT": {
|
|
35
48
|
const a = toNumber(contextValue);
|
|
36
|
-
const b = toNumber(
|
|
49
|
+
const b = toNumber(normalizedValue);
|
|
37
50
|
return a !== null && b !== null && a < b;
|
|
38
51
|
}
|
|
39
52
|
case "GTE": {
|
|
40
53
|
const a = toNumber(contextValue);
|
|
41
|
-
const b = toNumber(
|
|
54
|
+
const b = toNumber(normalizedValue);
|
|
42
55
|
return a !== null && b !== null && a >= b;
|
|
43
56
|
}
|
|
44
57
|
case "LTE": {
|
|
45
58
|
const a = toNumber(contextValue);
|
|
46
|
-
const b = toNumber(
|
|
59
|
+
const b = toNumber(normalizedValue);
|
|
47
60
|
return a !== null && b !== null && a <= b;
|
|
48
61
|
}
|
|
49
62
|
case "CONTAINS":
|
|
50
|
-
return typeof contextValue === "string" && contextValue.includes(
|
|
63
|
+
return typeof contextValue === "string" && typeof normalizedValue === "string" && contextValue.includes(normalizedValue);
|
|
51
64
|
case "STARTS_WITH":
|
|
52
|
-
return typeof contextValue === "string" && contextValue.startsWith(
|
|
65
|
+
return typeof contextValue === "string" && typeof normalizedValue === "string" && contextValue.startsWith(normalizedValue);
|
|
53
66
|
case "ENDS_WITH":
|
|
54
|
-
return typeof contextValue === "string" && contextValue.endsWith(
|
|
67
|
+
return typeof contextValue === "string" && typeof normalizedValue === "string" && contextValue.endsWith(normalizedValue);
|
|
55
68
|
default:
|
|
56
69
|
return false;
|
|
57
70
|
}
|
|
@@ -64,7 +77,13 @@ function evaluateOperator(operator, not, field, ruleValue, context) {
|
|
|
64
77
|
|
|
65
78
|
// src/rule.ts
|
|
66
79
|
function evaluateRule(rule, context) {
|
|
67
|
-
return evaluateOperator(
|
|
80
|
+
return evaluateOperator(
|
|
81
|
+
rule.operator,
|
|
82
|
+
rule.not,
|
|
83
|
+
rule.field,
|
|
84
|
+
rule.value,
|
|
85
|
+
context
|
|
86
|
+
);
|
|
68
87
|
}
|
|
69
88
|
function evaluateRules(rules, matchType, context) {
|
|
70
89
|
if (rules.length === 0) return true;
|
|
@@ -82,6 +101,9 @@ function evaluateSegment(segment, context) {
|
|
|
82
101
|
}
|
|
83
102
|
|
|
84
103
|
// src/rollout.ts
|
|
104
|
+
function getNestedValue(input, path) {
|
|
105
|
+
return path.split(".").reduce((obj, key) => obj?.[key], input);
|
|
106
|
+
}
|
|
85
107
|
function hashString(input) {
|
|
86
108
|
let hash = 5381;
|
|
87
109
|
for (let i = 0; i < input.length; i++) {
|
|
@@ -90,11 +112,12 @@ function hashString(input) {
|
|
|
90
112
|
}
|
|
91
113
|
return hash;
|
|
92
114
|
}
|
|
93
|
-
function evaluateRollout(
|
|
115
|
+
function evaluateRollout(flagKey, percentage, stickinessField, context) {
|
|
94
116
|
if (percentage <= 0) return false;
|
|
95
117
|
if (percentage >= 100) return true;
|
|
96
|
-
const
|
|
97
|
-
|
|
118
|
+
const stickinessValue = stickinessField ? getNestedValue(context, stickinessField) : context["userId"];
|
|
119
|
+
if (stickinessValue === void 0 || stickinessValue === null) return false;
|
|
120
|
+
const seed = `${flagKey}:${String(stickinessValue)}`;
|
|
98
121
|
const hash = hashString(seed);
|
|
99
122
|
const bucket = hash % 100;
|
|
100
123
|
return bucket < percentage;
|
|
@@ -103,8 +126,8 @@ function evaluateRollout(strategyId, percentage, stickinessField, context) {
|
|
|
103
126
|
// src/strategy.ts
|
|
104
127
|
function isWithinTimeWindow(startsAt, endsAt) {
|
|
105
128
|
const now = /* @__PURE__ */ new Date();
|
|
106
|
-
if (startsAt
|
|
107
|
-
if (endsAt
|
|
129
|
+
if (startsAt && new Date(startsAt) > now) return false;
|
|
130
|
+
if (endsAt && new Date(endsAt) < now) return false;
|
|
108
131
|
return true;
|
|
109
132
|
}
|
|
110
133
|
function evaluateStrategy(strategy, context) {
|
|
@@ -113,11 +136,12 @@ function evaluateStrategy(strategy, context) {
|
|
|
113
136
|
if (strategy.segment !== null) {
|
|
114
137
|
if (!evaluateSegment(strategy.segment, context)) return false;
|
|
115
138
|
} else if (strategy.rules.length > 0) {
|
|
116
|
-
if (!evaluateRules(strategy.rules, strategy.matchType, context))
|
|
139
|
+
if (!evaluateRules(strategy.rules, strategy.matchType, context))
|
|
140
|
+
return false;
|
|
117
141
|
}
|
|
118
142
|
if (strategy.rolloutPercentage !== null) {
|
|
119
143
|
return evaluateRollout(
|
|
120
|
-
strategy.
|
|
144
|
+
strategy.flagKey,
|
|
121
145
|
strategy.rolloutPercentage,
|
|
122
146
|
strategy.rolloutStickinessField,
|
|
123
147
|
context
|
|
@@ -130,7 +154,8 @@ function evaluateActionStrategy(strategy, context) {
|
|
|
130
154
|
if (strategy.segment !== null) {
|
|
131
155
|
if (!evaluateSegment(strategy.segment, context)) return null;
|
|
132
156
|
} else if (strategy.rules.length > 0) {
|
|
133
|
-
if (!evaluateRules(strategy.rules, strategy.matchType, context))
|
|
157
|
+
if (!evaluateRules(strategy.rules, strategy.matchType, context))
|
|
158
|
+
return null;
|
|
134
159
|
}
|
|
135
160
|
return strategy.effect;
|
|
136
161
|
}
|
|
@@ -140,7 +165,12 @@ function evaluateFeatureFlag(flagEnv, context) {
|
|
|
140
165
|
if (!flagEnv.enabled) {
|
|
141
166
|
return { enabled: false, strategyId: null };
|
|
142
167
|
}
|
|
143
|
-
|
|
168
|
+
if (!flagEnv.strategies || flagEnv.strategies.length === 0) {
|
|
169
|
+
return { enabled: true, strategyId: null };
|
|
170
|
+
}
|
|
171
|
+
const sorted = [...flagEnv.strategies].sort(
|
|
172
|
+
(a, b) => b.priority - a.priority
|
|
173
|
+
);
|
|
144
174
|
for (const strategy of sorted) {
|
|
145
175
|
if (evaluateStrategy(strategy, context)) {
|
|
146
176
|
return { enabled: true, strategyId: strategy.id };
|
package/package.json
CHANGED