@rolloutctrl/evaluator 0.0.1
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 +75 -0
- package/dist/index.d.ts +75 -0
- package/dist/index.js +209 -0
- package/dist/index.mjs +174 -0
- package/package.json +37 -0
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
type Operator = 'EQUALS' | 'IN' | 'INCLUDES' | 'GT' | 'LT' | 'GTE' | 'LTE' | 'CONTAINS' | 'STARTS_WITH' | 'ENDS_WITH';
|
|
2
|
+
type MatchType = 'ALL' | 'ANY';
|
|
3
|
+
type ActionEffect = 'ALLOW' | 'DENY';
|
|
4
|
+
interface Rule {
|
|
5
|
+
field: string;
|
|
6
|
+
operator: Operator;
|
|
7
|
+
not: boolean;
|
|
8
|
+
value: unknown;
|
|
9
|
+
}
|
|
10
|
+
interface SegmentRule extends Rule {
|
|
11
|
+
priority: number;
|
|
12
|
+
}
|
|
13
|
+
interface Segment {
|
|
14
|
+
id: string;
|
|
15
|
+
key: string;
|
|
16
|
+
rules: SegmentRule[];
|
|
17
|
+
}
|
|
18
|
+
interface Strategy {
|
|
19
|
+
id: string;
|
|
20
|
+
enabled: boolean;
|
|
21
|
+
priority: number;
|
|
22
|
+
matchType: MatchType;
|
|
23
|
+
rolloutPercentage: number | null;
|
|
24
|
+
rolloutStickinessField: string | null;
|
|
25
|
+
startsAt: Date | null;
|
|
26
|
+
endsAt: Date | null;
|
|
27
|
+
segment: Segment | null;
|
|
28
|
+
rules: Rule[];
|
|
29
|
+
}
|
|
30
|
+
interface FeatureFlagEnvironment {
|
|
31
|
+
enabled: boolean;
|
|
32
|
+
strategies: Strategy[];
|
|
33
|
+
}
|
|
34
|
+
interface ActionStrategy {
|
|
35
|
+
id: string;
|
|
36
|
+
enabled: boolean;
|
|
37
|
+
priority: number;
|
|
38
|
+
effect: ActionEffect;
|
|
39
|
+
matchType: MatchType;
|
|
40
|
+
segment: Segment | null;
|
|
41
|
+
rules: Rule[];
|
|
42
|
+
}
|
|
43
|
+
interface Action {
|
|
44
|
+
id: string;
|
|
45
|
+
key: string;
|
|
46
|
+
enabled: boolean;
|
|
47
|
+
defaultEffect: ActionEffect;
|
|
48
|
+
strategies: ActionStrategy[];
|
|
49
|
+
}
|
|
50
|
+
type EvaluationContext = Record<string, unknown>;
|
|
51
|
+
interface FeatureFlagResult {
|
|
52
|
+
enabled: boolean;
|
|
53
|
+
strategyId: string | null;
|
|
54
|
+
}
|
|
55
|
+
interface ActionResult {
|
|
56
|
+
effect: ActionEffect;
|
|
57
|
+
strategyId: string | null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
declare function evaluateOperator(operator: Operator, not: boolean, field: string, ruleValue: unknown, context: Record<string, unknown>): boolean;
|
|
61
|
+
|
|
62
|
+
declare function evaluateRule(rule: Rule, context: EvaluationContext): boolean;
|
|
63
|
+
declare function evaluateRules(rules: Rule[], matchType: MatchType, context: EvaluationContext): boolean;
|
|
64
|
+
|
|
65
|
+
declare function evaluateSegment(segment: Segment, context: EvaluationContext): boolean;
|
|
66
|
+
|
|
67
|
+
declare function evaluateRollout(strategyId: string, percentage: number, stickinessField: string | null, context: EvaluationContext): boolean;
|
|
68
|
+
|
|
69
|
+
declare function evaluateStrategy(strategy: Strategy, context: EvaluationContext): boolean;
|
|
70
|
+
declare function evaluateActionStrategy(strategy: ActionStrategy, context: EvaluationContext): ActionEffect | null;
|
|
71
|
+
|
|
72
|
+
declare function evaluateFeatureFlag(flagEnv: FeatureFlagEnvironment, context: EvaluationContext): FeatureFlagResult;
|
|
73
|
+
declare function evaluateAction(action: Action, context: EvaluationContext): ActionResult;
|
|
74
|
+
|
|
75
|
+
export { type Action, type ActionEffect, type ActionResult, type ActionStrategy, type EvaluationContext, type FeatureFlagEnvironment, type FeatureFlagResult, type MatchType, type Operator, type Rule, type Segment, type SegmentRule, type Strategy, evaluateAction, evaluateActionStrategy, evaluateFeatureFlag, evaluateOperator, evaluateRollout, evaluateRule, evaluateRules, evaluateSegment, evaluateStrategy };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
type Operator = 'EQUALS' | 'IN' | 'INCLUDES' | 'GT' | 'LT' | 'GTE' | 'LTE' | 'CONTAINS' | 'STARTS_WITH' | 'ENDS_WITH';
|
|
2
|
+
type MatchType = 'ALL' | 'ANY';
|
|
3
|
+
type ActionEffect = 'ALLOW' | 'DENY';
|
|
4
|
+
interface Rule {
|
|
5
|
+
field: string;
|
|
6
|
+
operator: Operator;
|
|
7
|
+
not: boolean;
|
|
8
|
+
value: unknown;
|
|
9
|
+
}
|
|
10
|
+
interface SegmentRule extends Rule {
|
|
11
|
+
priority: number;
|
|
12
|
+
}
|
|
13
|
+
interface Segment {
|
|
14
|
+
id: string;
|
|
15
|
+
key: string;
|
|
16
|
+
rules: SegmentRule[];
|
|
17
|
+
}
|
|
18
|
+
interface Strategy {
|
|
19
|
+
id: string;
|
|
20
|
+
enabled: boolean;
|
|
21
|
+
priority: number;
|
|
22
|
+
matchType: MatchType;
|
|
23
|
+
rolloutPercentage: number | null;
|
|
24
|
+
rolloutStickinessField: string | null;
|
|
25
|
+
startsAt: Date | null;
|
|
26
|
+
endsAt: Date | null;
|
|
27
|
+
segment: Segment | null;
|
|
28
|
+
rules: Rule[];
|
|
29
|
+
}
|
|
30
|
+
interface FeatureFlagEnvironment {
|
|
31
|
+
enabled: boolean;
|
|
32
|
+
strategies: Strategy[];
|
|
33
|
+
}
|
|
34
|
+
interface ActionStrategy {
|
|
35
|
+
id: string;
|
|
36
|
+
enabled: boolean;
|
|
37
|
+
priority: number;
|
|
38
|
+
effect: ActionEffect;
|
|
39
|
+
matchType: MatchType;
|
|
40
|
+
segment: Segment | null;
|
|
41
|
+
rules: Rule[];
|
|
42
|
+
}
|
|
43
|
+
interface Action {
|
|
44
|
+
id: string;
|
|
45
|
+
key: string;
|
|
46
|
+
enabled: boolean;
|
|
47
|
+
defaultEffect: ActionEffect;
|
|
48
|
+
strategies: ActionStrategy[];
|
|
49
|
+
}
|
|
50
|
+
type EvaluationContext = Record<string, unknown>;
|
|
51
|
+
interface FeatureFlagResult {
|
|
52
|
+
enabled: boolean;
|
|
53
|
+
strategyId: string | null;
|
|
54
|
+
}
|
|
55
|
+
interface ActionResult {
|
|
56
|
+
effect: ActionEffect;
|
|
57
|
+
strategyId: string | null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
declare function evaluateOperator(operator: Operator, not: boolean, field: string, ruleValue: unknown, context: Record<string, unknown>): boolean;
|
|
61
|
+
|
|
62
|
+
declare function evaluateRule(rule: Rule, context: EvaluationContext): boolean;
|
|
63
|
+
declare function evaluateRules(rules: Rule[], matchType: MatchType, context: EvaluationContext): boolean;
|
|
64
|
+
|
|
65
|
+
declare function evaluateSegment(segment: Segment, context: EvaluationContext): boolean;
|
|
66
|
+
|
|
67
|
+
declare function evaluateRollout(strategyId: string, percentage: number, stickinessField: string | null, context: EvaluationContext): boolean;
|
|
68
|
+
|
|
69
|
+
declare function evaluateStrategy(strategy: Strategy, context: EvaluationContext): boolean;
|
|
70
|
+
declare function evaluateActionStrategy(strategy: ActionStrategy, context: EvaluationContext): ActionEffect | null;
|
|
71
|
+
|
|
72
|
+
declare function evaluateFeatureFlag(flagEnv: FeatureFlagEnvironment, context: EvaluationContext): FeatureFlagResult;
|
|
73
|
+
declare function evaluateAction(action: Action, context: EvaluationContext): ActionResult;
|
|
74
|
+
|
|
75
|
+
export { type Action, type ActionEffect, type ActionResult, type ActionStrategy, type EvaluationContext, type FeatureFlagEnvironment, type FeatureFlagResult, type MatchType, type Operator, type Rule, type Segment, type SegmentRule, type Strategy, evaluateAction, evaluateActionStrategy, evaluateFeatureFlag, evaluateOperator, evaluateRollout, evaluateRule, evaluateRules, evaluateSegment, evaluateStrategy };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
evaluateAction: () => evaluateAction,
|
|
24
|
+
evaluateActionStrategy: () => evaluateActionStrategy,
|
|
25
|
+
evaluateFeatureFlag: () => evaluateFeatureFlag,
|
|
26
|
+
evaluateOperator: () => evaluateOperator,
|
|
27
|
+
evaluateRollout: () => evaluateRollout,
|
|
28
|
+
evaluateRule: () => evaluateRule,
|
|
29
|
+
evaluateRules: () => evaluateRules,
|
|
30
|
+
evaluateSegment: () => evaluateSegment,
|
|
31
|
+
evaluateStrategy: () => evaluateStrategy
|
|
32
|
+
});
|
|
33
|
+
module.exports = __toCommonJS(index_exports);
|
|
34
|
+
|
|
35
|
+
// src/operators.ts
|
|
36
|
+
function getFieldValue(context, field) {
|
|
37
|
+
const parts = field.split(".");
|
|
38
|
+
let current = context;
|
|
39
|
+
for (const part of parts) {
|
|
40
|
+
if (current === null || current === void 0 || typeof current !== "object") {
|
|
41
|
+
return void 0;
|
|
42
|
+
}
|
|
43
|
+
current = current[part];
|
|
44
|
+
}
|
|
45
|
+
return current;
|
|
46
|
+
}
|
|
47
|
+
function toNumber(value) {
|
|
48
|
+
const n = Number(value);
|
|
49
|
+
return isNaN(n) ? null : n;
|
|
50
|
+
}
|
|
51
|
+
function applyOperator(operator, contextValue, ruleValue) {
|
|
52
|
+
switch (operator) {
|
|
53
|
+
case "EQUALS":
|
|
54
|
+
return String(contextValue) === String(ruleValue);
|
|
55
|
+
case "IN": {
|
|
56
|
+
const list = Array.isArray(ruleValue) ? ruleValue : [ruleValue];
|
|
57
|
+
return list.map(String).includes(String(contextValue));
|
|
58
|
+
}
|
|
59
|
+
case "INCLUDES": {
|
|
60
|
+
if (!Array.isArray(contextValue)) return false;
|
|
61
|
+
return contextValue.map(String).includes(String(ruleValue));
|
|
62
|
+
}
|
|
63
|
+
case "GT": {
|
|
64
|
+
const a = toNumber(contextValue);
|
|
65
|
+
const b = toNumber(ruleValue);
|
|
66
|
+
return a !== null && b !== null && a > b;
|
|
67
|
+
}
|
|
68
|
+
case "LT": {
|
|
69
|
+
const a = toNumber(contextValue);
|
|
70
|
+
const b = toNumber(ruleValue);
|
|
71
|
+
return a !== null && b !== null && a < b;
|
|
72
|
+
}
|
|
73
|
+
case "GTE": {
|
|
74
|
+
const a = toNumber(contextValue);
|
|
75
|
+
const b = toNumber(ruleValue);
|
|
76
|
+
return a !== null && b !== null && a >= b;
|
|
77
|
+
}
|
|
78
|
+
case "LTE": {
|
|
79
|
+
const a = toNumber(contextValue);
|
|
80
|
+
const b = toNumber(ruleValue);
|
|
81
|
+
return a !== null && b !== null && a <= b;
|
|
82
|
+
}
|
|
83
|
+
case "CONTAINS":
|
|
84
|
+
return typeof contextValue === "string" && contextValue.includes(String(ruleValue));
|
|
85
|
+
case "STARTS_WITH":
|
|
86
|
+
return typeof contextValue === "string" && contextValue.startsWith(String(ruleValue));
|
|
87
|
+
case "ENDS_WITH":
|
|
88
|
+
return typeof contextValue === "string" && contextValue.endsWith(String(ruleValue));
|
|
89
|
+
default:
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function evaluateOperator(operator, not, field, ruleValue, context) {
|
|
94
|
+
const contextValue = getFieldValue(context, field);
|
|
95
|
+
const result = applyOperator(operator, contextValue, ruleValue);
|
|
96
|
+
return not ? !result : result;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// src/rule.ts
|
|
100
|
+
function evaluateRule(rule, context) {
|
|
101
|
+
return evaluateOperator(rule.operator, rule.not, rule.field, rule.value, context);
|
|
102
|
+
}
|
|
103
|
+
function evaluateRules(rules, matchType, context) {
|
|
104
|
+
if (rules.length === 0) return true;
|
|
105
|
+
if (matchType === "ALL") {
|
|
106
|
+
return rules.every((rule) => evaluateRule(rule, context));
|
|
107
|
+
}
|
|
108
|
+
return rules.some((rule) => evaluateRule(rule, context));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// src/segment.ts
|
|
112
|
+
function evaluateSegment(segment, context) {
|
|
113
|
+
if (segment.rules.length === 0) return true;
|
|
114
|
+
const sorted = [...segment.rules].sort((a, b) => b.priority - a.priority);
|
|
115
|
+
return sorted.every((rule) => evaluateRule(rule, context));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// src/rollout.ts
|
|
119
|
+
function hashString(input) {
|
|
120
|
+
let hash = 5381;
|
|
121
|
+
for (let i = 0; i < input.length; i++) {
|
|
122
|
+
hash = (hash << 5) + hash ^ input.charCodeAt(i);
|
|
123
|
+
hash = hash >>> 0;
|
|
124
|
+
}
|
|
125
|
+
return hash;
|
|
126
|
+
}
|
|
127
|
+
function evaluateRollout(strategyId, percentage, stickinessField, context) {
|
|
128
|
+
if (percentage <= 0) return false;
|
|
129
|
+
if (percentage >= 100) return true;
|
|
130
|
+
const stickyValue = stickinessField !== null ? String(context[stickinessField] ?? "") : "";
|
|
131
|
+
const seed = `${strategyId}:${stickyValue}`;
|
|
132
|
+
const hash = hashString(seed);
|
|
133
|
+
const bucket = hash % 100;
|
|
134
|
+
return bucket < percentage;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// src/strategy.ts
|
|
138
|
+
function isWithinTimeWindow(startsAt, endsAt) {
|
|
139
|
+
const now = /* @__PURE__ */ new Date();
|
|
140
|
+
if (startsAt !== null && now < startsAt) return false;
|
|
141
|
+
if (endsAt !== null && now > endsAt) return false;
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
function evaluateStrategy(strategy, context) {
|
|
145
|
+
if (!strategy.enabled) return false;
|
|
146
|
+
if (!isWithinTimeWindow(strategy.startsAt, strategy.endsAt)) return false;
|
|
147
|
+
if (strategy.segment !== null) {
|
|
148
|
+
if (!evaluateSegment(strategy.segment, context)) return false;
|
|
149
|
+
} else if (strategy.rules.length > 0) {
|
|
150
|
+
if (!evaluateRules(strategy.rules, strategy.matchType, context)) return false;
|
|
151
|
+
}
|
|
152
|
+
if (strategy.rolloutPercentage !== null) {
|
|
153
|
+
return evaluateRollout(
|
|
154
|
+
strategy.id,
|
|
155
|
+
strategy.rolloutPercentage,
|
|
156
|
+
strategy.rolloutStickinessField,
|
|
157
|
+
context
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
function evaluateActionStrategy(strategy, context) {
|
|
163
|
+
if (!strategy.enabled) return null;
|
|
164
|
+
if (strategy.segment !== null) {
|
|
165
|
+
if (!evaluateSegment(strategy.segment, context)) return null;
|
|
166
|
+
} else if (strategy.rules.length > 0) {
|
|
167
|
+
if (!evaluateRules(strategy.rules, strategy.matchType, context)) return null;
|
|
168
|
+
}
|
|
169
|
+
return strategy.effect;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// src/evaluate.ts
|
|
173
|
+
function evaluateFeatureFlag(flagEnv, context) {
|
|
174
|
+
if (!flagEnv.enabled) {
|
|
175
|
+
return { enabled: false, strategyId: null };
|
|
176
|
+
}
|
|
177
|
+
const sorted = [...flagEnv.strategies].sort((a, b) => b.priority - a.priority);
|
|
178
|
+
for (const strategy of sorted) {
|
|
179
|
+
if (evaluateStrategy(strategy, context)) {
|
|
180
|
+
return { enabled: true, strategyId: strategy.id };
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return { enabled: false, strategyId: null };
|
|
184
|
+
}
|
|
185
|
+
function evaluateAction(action, context) {
|
|
186
|
+
if (!action.enabled) {
|
|
187
|
+
return { effect: action.defaultEffect, strategyId: null };
|
|
188
|
+
}
|
|
189
|
+
const sorted = [...action.strategies].sort((a, b) => b.priority - a.priority);
|
|
190
|
+
for (const strategy of sorted) {
|
|
191
|
+
const effect = evaluateActionStrategy(strategy, context);
|
|
192
|
+
if (effect !== null) {
|
|
193
|
+
return { effect, strategyId: strategy.id };
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return { effect: action.defaultEffect, strategyId: null };
|
|
197
|
+
}
|
|
198
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
199
|
+
0 && (module.exports = {
|
|
200
|
+
evaluateAction,
|
|
201
|
+
evaluateActionStrategy,
|
|
202
|
+
evaluateFeatureFlag,
|
|
203
|
+
evaluateOperator,
|
|
204
|
+
evaluateRollout,
|
|
205
|
+
evaluateRule,
|
|
206
|
+
evaluateRules,
|
|
207
|
+
evaluateSegment,
|
|
208
|
+
evaluateStrategy
|
|
209
|
+
});
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
// src/operators.ts
|
|
2
|
+
function getFieldValue(context, field) {
|
|
3
|
+
const parts = field.split(".");
|
|
4
|
+
let current = context;
|
|
5
|
+
for (const part of parts) {
|
|
6
|
+
if (current === null || current === void 0 || typeof current !== "object") {
|
|
7
|
+
return void 0;
|
|
8
|
+
}
|
|
9
|
+
current = current[part];
|
|
10
|
+
}
|
|
11
|
+
return current;
|
|
12
|
+
}
|
|
13
|
+
function toNumber(value) {
|
|
14
|
+
const n = Number(value);
|
|
15
|
+
return isNaN(n) ? null : n;
|
|
16
|
+
}
|
|
17
|
+
function applyOperator(operator, contextValue, ruleValue) {
|
|
18
|
+
switch (operator) {
|
|
19
|
+
case "EQUALS":
|
|
20
|
+
return String(contextValue) === String(ruleValue);
|
|
21
|
+
case "IN": {
|
|
22
|
+
const list = Array.isArray(ruleValue) ? ruleValue : [ruleValue];
|
|
23
|
+
return list.map(String).includes(String(contextValue));
|
|
24
|
+
}
|
|
25
|
+
case "INCLUDES": {
|
|
26
|
+
if (!Array.isArray(contextValue)) return false;
|
|
27
|
+
return contextValue.map(String).includes(String(ruleValue));
|
|
28
|
+
}
|
|
29
|
+
case "GT": {
|
|
30
|
+
const a = toNumber(contextValue);
|
|
31
|
+
const b = toNumber(ruleValue);
|
|
32
|
+
return a !== null && b !== null && a > b;
|
|
33
|
+
}
|
|
34
|
+
case "LT": {
|
|
35
|
+
const a = toNumber(contextValue);
|
|
36
|
+
const b = toNumber(ruleValue);
|
|
37
|
+
return a !== null && b !== null && a < b;
|
|
38
|
+
}
|
|
39
|
+
case "GTE": {
|
|
40
|
+
const a = toNumber(contextValue);
|
|
41
|
+
const b = toNumber(ruleValue);
|
|
42
|
+
return a !== null && b !== null && a >= b;
|
|
43
|
+
}
|
|
44
|
+
case "LTE": {
|
|
45
|
+
const a = toNumber(contextValue);
|
|
46
|
+
const b = toNumber(ruleValue);
|
|
47
|
+
return a !== null && b !== null && a <= b;
|
|
48
|
+
}
|
|
49
|
+
case "CONTAINS":
|
|
50
|
+
return typeof contextValue === "string" && contextValue.includes(String(ruleValue));
|
|
51
|
+
case "STARTS_WITH":
|
|
52
|
+
return typeof contextValue === "string" && contextValue.startsWith(String(ruleValue));
|
|
53
|
+
case "ENDS_WITH":
|
|
54
|
+
return typeof contextValue === "string" && contextValue.endsWith(String(ruleValue));
|
|
55
|
+
default:
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function evaluateOperator(operator, not, field, ruleValue, context) {
|
|
60
|
+
const contextValue = getFieldValue(context, field);
|
|
61
|
+
const result = applyOperator(operator, contextValue, ruleValue);
|
|
62
|
+
return not ? !result : result;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// src/rule.ts
|
|
66
|
+
function evaluateRule(rule, context) {
|
|
67
|
+
return evaluateOperator(rule.operator, rule.not, rule.field, rule.value, context);
|
|
68
|
+
}
|
|
69
|
+
function evaluateRules(rules, matchType, context) {
|
|
70
|
+
if (rules.length === 0) return true;
|
|
71
|
+
if (matchType === "ALL") {
|
|
72
|
+
return rules.every((rule) => evaluateRule(rule, context));
|
|
73
|
+
}
|
|
74
|
+
return rules.some((rule) => evaluateRule(rule, context));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// src/segment.ts
|
|
78
|
+
function evaluateSegment(segment, context) {
|
|
79
|
+
if (segment.rules.length === 0) return true;
|
|
80
|
+
const sorted = [...segment.rules].sort((a, b) => b.priority - a.priority);
|
|
81
|
+
return sorted.every((rule) => evaluateRule(rule, context));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// src/rollout.ts
|
|
85
|
+
function hashString(input) {
|
|
86
|
+
let hash = 5381;
|
|
87
|
+
for (let i = 0; i < input.length; i++) {
|
|
88
|
+
hash = (hash << 5) + hash ^ input.charCodeAt(i);
|
|
89
|
+
hash = hash >>> 0;
|
|
90
|
+
}
|
|
91
|
+
return hash;
|
|
92
|
+
}
|
|
93
|
+
function evaluateRollout(strategyId, percentage, stickinessField, context) {
|
|
94
|
+
if (percentage <= 0) return false;
|
|
95
|
+
if (percentage >= 100) return true;
|
|
96
|
+
const stickyValue = stickinessField !== null ? String(context[stickinessField] ?? "") : "";
|
|
97
|
+
const seed = `${strategyId}:${stickyValue}`;
|
|
98
|
+
const hash = hashString(seed);
|
|
99
|
+
const bucket = hash % 100;
|
|
100
|
+
return bucket < percentage;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// src/strategy.ts
|
|
104
|
+
function isWithinTimeWindow(startsAt, endsAt) {
|
|
105
|
+
const now = /* @__PURE__ */ new Date();
|
|
106
|
+
if (startsAt !== null && now < startsAt) return false;
|
|
107
|
+
if (endsAt !== null && now > endsAt) return false;
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
function evaluateStrategy(strategy, context) {
|
|
111
|
+
if (!strategy.enabled) return false;
|
|
112
|
+
if (!isWithinTimeWindow(strategy.startsAt, strategy.endsAt)) return false;
|
|
113
|
+
if (strategy.segment !== null) {
|
|
114
|
+
if (!evaluateSegment(strategy.segment, context)) return false;
|
|
115
|
+
} else if (strategy.rules.length > 0) {
|
|
116
|
+
if (!evaluateRules(strategy.rules, strategy.matchType, context)) return false;
|
|
117
|
+
}
|
|
118
|
+
if (strategy.rolloutPercentage !== null) {
|
|
119
|
+
return evaluateRollout(
|
|
120
|
+
strategy.id,
|
|
121
|
+
strategy.rolloutPercentage,
|
|
122
|
+
strategy.rolloutStickinessField,
|
|
123
|
+
context
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
function evaluateActionStrategy(strategy, context) {
|
|
129
|
+
if (!strategy.enabled) return null;
|
|
130
|
+
if (strategy.segment !== null) {
|
|
131
|
+
if (!evaluateSegment(strategy.segment, context)) return null;
|
|
132
|
+
} else if (strategy.rules.length > 0) {
|
|
133
|
+
if (!evaluateRules(strategy.rules, strategy.matchType, context)) return null;
|
|
134
|
+
}
|
|
135
|
+
return strategy.effect;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// src/evaluate.ts
|
|
139
|
+
function evaluateFeatureFlag(flagEnv, context) {
|
|
140
|
+
if (!flagEnv.enabled) {
|
|
141
|
+
return { enabled: false, strategyId: null };
|
|
142
|
+
}
|
|
143
|
+
const sorted = [...flagEnv.strategies].sort((a, b) => b.priority - a.priority);
|
|
144
|
+
for (const strategy of sorted) {
|
|
145
|
+
if (evaluateStrategy(strategy, context)) {
|
|
146
|
+
return { enabled: true, strategyId: strategy.id };
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return { enabled: false, strategyId: null };
|
|
150
|
+
}
|
|
151
|
+
function evaluateAction(action, context) {
|
|
152
|
+
if (!action.enabled) {
|
|
153
|
+
return { effect: action.defaultEffect, strategyId: null };
|
|
154
|
+
}
|
|
155
|
+
const sorted = [...action.strategies].sort((a, b) => b.priority - a.priority);
|
|
156
|
+
for (const strategy of sorted) {
|
|
157
|
+
const effect = evaluateActionStrategy(strategy, context);
|
|
158
|
+
if (effect !== null) {
|
|
159
|
+
return { effect, strategyId: strategy.id };
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return { effect: action.defaultEffect, strategyId: null };
|
|
163
|
+
}
|
|
164
|
+
export {
|
|
165
|
+
evaluateAction,
|
|
166
|
+
evaluateActionStrategy,
|
|
167
|
+
evaluateFeatureFlag,
|
|
168
|
+
evaluateOperator,
|
|
169
|
+
evaluateRollout,
|
|
170
|
+
evaluateRule,
|
|
171
|
+
evaluateRules,
|
|
172
|
+
evaluateSegment,
|
|
173
|
+
evaluateStrategy
|
|
174
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@rolloutctrl/evaluator",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "RolloutCtrl feature flag & action evaluator",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"module": "./dist/index.mjs",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.mjs",
|
|
12
|
+
"require": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsup src/index.ts --format cjs,esm --dts --clean",
|
|
20
|
+
"dev": "tsup src/index.ts --format cjs,esm --dts --watch",
|
|
21
|
+
"lint": "tsc --noEmit",
|
|
22
|
+
"test": "jest",
|
|
23
|
+
"test:watch": "jest --watch"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@jest/types": "^30.4.1",
|
|
27
|
+
"@types/jest": "^29.5.0",
|
|
28
|
+
"jest": "^29.7.0",
|
|
29
|
+
"jest-util": "^30.4.1",
|
|
30
|
+
"ts-jest": "^29.1.0",
|
|
31
|
+
"tsup": "^8.0.0",
|
|
32
|
+
"typescript": "^5.4.0"
|
|
33
|
+
},
|
|
34
|
+
"publishConfig": {
|
|
35
|
+
"access": "public"
|
|
36
|
+
}
|
|
37
|
+
}
|