@contractspec/lib.feature-flags 1.57.0 → 1.58.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.
Files changed (53) hide show
  1. package/dist/browser/contracts/index.js +636 -0
  2. package/dist/browser/docs/feature-flags.docblock.js +71 -0
  3. package/dist/browser/docs/index.js +71 -0
  4. package/dist/browser/entities/index.js +306 -0
  5. package/dist/browser/evaluation/index.js +223 -0
  6. package/dist/browser/events.js +296 -0
  7. package/dist/browser/feature-flags.capability.js +28 -0
  8. package/dist/browser/feature-flags.feature.js +55 -0
  9. package/dist/browser/index.js +1583 -0
  10. package/dist/contracts/index.d.ts +944 -950
  11. package/dist/contracts/index.d.ts.map +1 -1
  12. package/dist/contracts/index.js +635 -906
  13. package/dist/docs/feature-flags.docblock.d.ts +2 -1
  14. package/dist/docs/feature-flags.docblock.d.ts.map +1 -0
  15. package/dist/docs/feature-flags.docblock.js +18 -22
  16. package/dist/docs/index.d.ts +2 -1
  17. package/dist/docs/index.d.ts.map +1 -0
  18. package/dist/docs/index.js +72 -1
  19. package/dist/entities/index.d.ts +159 -164
  20. package/dist/entities/index.d.ts.map +1 -1
  21. package/dist/entities/index.js +297 -315
  22. package/dist/evaluation/index.d.ts +119 -122
  23. package/dist/evaluation/index.d.ts.map +1 -1
  24. package/dist/evaluation/index.js +215 -212
  25. package/dist/events.d.ts +480 -486
  26. package/dist/events.d.ts.map +1 -1
  27. package/dist/events.js +272 -511
  28. package/dist/feature-flags.capability.d.ts +2 -7
  29. package/dist/feature-flags.capability.d.ts.map +1 -1
  30. package/dist/feature-flags.capability.js +29 -25
  31. package/dist/feature-flags.feature.d.ts +1 -6
  32. package/dist/feature-flags.feature.d.ts.map +1 -1
  33. package/dist/feature-flags.feature.js +54 -146
  34. package/dist/index.d.ts +7 -6
  35. package/dist/index.d.ts.map +1 -0
  36. package/dist/index.js +1584 -8
  37. package/dist/node/contracts/index.js +636 -0
  38. package/dist/node/docs/feature-flags.docblock.js +71 -0
  39. package/dist/node/docs/index.js +71 -0
  40. package/dist/node/entities/index.js +306 -0
  41. package/dist/node/evaluation/index.js +223 -0
  42. package/dist/node/events.js +296 -0
  43. package/dist/node/feature-flags.capability.js +28 -0
  44. package/dist/node/feature-flags.feature.js +55 -0
  45. package/dist/node/index.js +1583 -0
  46. package/package.json +117 -30
  47. package/dist/contracts/index.js.map +0 -1
  48. package/dist/docs/feature-flags.docblock.js.map +0 -1
  49. package/dist/entities/index.js.map +0 -1
  50. package/dist/evaluation/index.js.map +0 -1
  51. package/dist/events.js.map +0 -1
  52. package/dist/feature-flags.capability.js.map +0 -1
  53. package/dist/feature-flags.feature.js.map +0 -1
@@ -1,221 +1,224 @@
1
- //#region src/evaluation/index.ts
2
- /**
3
- * Simple hash function for consistent bucketing.
4
- * Uses a deterministic algorithm so the same input always produces the same bucket.
5
- */
1
+ // @bun
2
+ // src/evaluation/index.ts
6
3
  function hashToBucket(value, seed = "") {
7
- const input = `${seed}:${value}`;
8
- let hash = 0;
9
- for (let i = 0; i < input.length; i++) {
10
- const char = input.charCodeAt(i);
11
- hash = (hash << 5) - hash + char;
12
- hash = hash & hash;
13
- }
14
- return Math.abs(hash % 100);
4
+ const input = `${seed}:${value}`;
5
+ let hash = 0;
6
+ for (let i = 0;i < input.length; i++) {
7
+ const char = input.charCodeAt(i);
8
+ hash = (hash << 5) - hash + char;
9
+ hash = hash & hash;
10
+ }
11
+ return Math.abs(hash % 100);
15
12
  }
16
- /**
17
- * Get subject identifier from context for consistent hashing.
18
- */
19
13
  function getSubjectId(context) {
20
- return context.userId || context.sessionId || context.orgId || "anonymous";
14
+ return context.userId || context.sessionId || context.orgId || "anonymous";
21
15
  }
22
- /**
23
- * Evaluate a single targeting rule condition.
24
- */
25
16
  function evaluateRuleCondition(rule, context) {
26
- const attributeValue = getAttributeValue(rule.attribute, context);
27
- switch (rule.operator) {
28
- case "EQ": return attributeValue === rule.value;
29
- case "NEQ": return attributeValue !== rule.value;
30
- case "IN":
31
- if (!Array.isArray(rule.value)) return false;
32
- return rule.value.includes(attributeValue);
33
- case "NIN":
34
- if (!Array.isArray(rule.value)) return true;
35
- return !rule.value.includes(attributeValue);
36
- case "CONTAINS":
37
- if (typeof attributeValue !== "string" || typeof rule.value !== "string") return false;
38
- return attributeValue.includes(rule.value);
39
- case "NOT_CONTAINS":
40
- if (typeof attributeValue !== "string" || typeof rule.value !== "string") return true;
41
- return !attributeValue.includes(rule.value);
42
- case "GT":
43
- if (typeof attributeValue !== "number" || typeof rule.value !== "number") return false;
44
- return attributeValue > rule.value;
45
- case "GTE":
46
- if (typeof attributeValue !== "number" || typeof rule.value !== "number") return false;
47
- return attributeValue >= rule.value;
48
- case "LT":
49
- if (typeof attributeValue !== "number" || typeof rule.value !== "number") return false;
50
- return attributeValue < rule.value;
51
- case "LTE":
52
- if (typeof attributeValue !== "number" || typeof rule.value !== "number") return false;
53
- return attributeValue <= rule.value;
54
- case "PERCENTAGE": return hashToBucket(getSubjectId(context), rule.attribute) < (typeof rule.value === "number" ? rule.value : 0);
55
- default: return false;
56
- }
17
+ const attributeValue = getAttributeValue(rule.attribute, context);
18
+ switch (rule.operator) {
19
+ case "EQ":
20
+ return attributeValue === rule.value;
21
+ case "NEQ":
22
+ return attributeValue !== rule.value;
23
+ case "IN":
24
+ if (!Array.isArray(rule.value))
25
+ return false;
26
+ return rule.value.includes(attributeValue);
27
+ case "NIN":
28
+ if (!Array.isArray(rule.value))
29
+ return true;
30
+ return !rule.value.includes(attributeValue);
31
+ case "CONTAINS":
32
+ if (typeof attributeValue !== "string" || typeof rule.value !== "string")
33
+ return false;
34
+ return attributeValue.includes(rule.value);
35
+ case "NOT_CONTAINS":
36
+ if (typeof attributeValue !== "string" || typeof rule.value !== "string")
37
+ return true;
38
+ return !attributeValue.includes(rule.value);
39
+ case "GT":
40
+ if (typeof attributeValue !== "number" || typeof rule.value !== "number")
41
+ return false;
42
+ return attributeValue > rule.value;
43
+ case "GTE":
44
+ if (typeof attributeValue !== "number" || typeof rule.value !== "number")
45
+ return false;
46
+ return attributeValue >= rule.value;
47
+ case "LT":
48
+ if (typeof attributeValue !== "number" || typeof rule.value !== "number")
49
+ return false;
50
+ return attributeValue < rule.value;
51
+ case "LTE":
52
+ if (typeof attributeValue !== "number" || typeof rule.value !== "number")
53
+ return false;
54
+ return attributeValue <= rule.value;
55
+ case "PERCENTAGE":
56
+ return hashToBucket(getSubjectId(context), rule.attribute) < (typeof rule.value === "number" ? rule.value : 0);
57
+ default:
58
+ return false;
59
+ }
57
60
  }
58
- /**
59
- * Get attribute value from context.
60
- */
61
61
  function getAttributeValue(attribute, context) {
62
- switch (attribute) {
63
- case "userId": return context.userId;
64
- case "orgId": return context.orgId;
65
- case "plan": return context.plan;
66
- case "segment": return context.segment;
67
- case "sessionId": return context.sessionId;
68
- default: return context.attributes?.[attribute];
69
- }
62
+ switch (attribute) {
63
+ case "userId":
64
+ return context.userId;
65
+ case "orgId":
66
+ return context.orgId;
67
+ case "plan":
68
+ return context.plan;
69
+ case "segment":
70
+ return context.segment;
71
+ case "sessionId":
72
+ return context.sessionId;
73
+ default:
74
+ return context.attributes?.[attribute];
75
+ }
76
+ }
77
+
78
+ class FlagEvaluator {
79
+ repository;
80
+ logger;
81
+ logEvaluations;
82
+ constructor(options) {
83
+ this.repository = options.repository;
84
+ this.logger = options.logger;
85
+ this.logEvaluations = options.logEvaluations ?? false;
86
+ }
87
+ async evaluate(key, context) {
88
+ const orgId = context.orgId;
89
+ const flag = await this.repository.getFlag(key, orgId);
90
+ if (!flag) {
91
+ return this.makeResult(false, "FLAG_NOT_FOUND");
92
+ }
93
+ if (flag.status === "OFF") {
94
+ return this.logAndReturn(flag, context, this.makeResult(false, "FLAG_OFF"));
95
+ }
96
+ if (flag.status === "ON") {
97
+ return this.logAndReturn(flag, context, this.makeResult(true, "FLAG_ON"));
98
+ }
99
+ const rules = await this.repository.getRules(flag.id);
100
+ const sortedRules = [...rules].filter((r) => r.enabled).sort((a, b) => a.priority - b.priority);
101
+ for (const rule of sortedRules) {
102
+ if (evaluateRuleCondition(rule, context)) {
103
+ if (rule.rolloutPercentage !== undefined && rule.rolloutPercentage !== null) {
104
+ const bucket = hashToBucket(getSubjectId(context), flag.key);
105
+ if (bucket >= rule.rolloutPercentage) {
106
+ continue;
107
+ }
108
+ }
109
+ const enabled = rule.serveValue ?? true;
110
+ return this.logAndReturn(flag, context, this.makeResult(enabled, "RULE_MATCH", rule.serveVariant, rule.id));
111
+ }
112
+ }
113
+ const experiment = await this.repository.getActiveExperiment(flag.id);
114
+ if (experiment && experiment.status === "RUNNING") {
115
+ const result = await this.evaluateExperiment(experiment, context);
116
+ if (result) {
117
+ return this.logAndReturn(flag, context, result);
118
+ }
119
+ }
120
+ return this.logAndReturn(flag, context, this.makeResult(flag.defaultValue, "DEFAULT_VALUE"));
121
+ }
122
+ async evaluateExperiment(experiment, context) {
123
+ const subjectId = getSubjectId(context);
124
+ const subjectType = context.userId ? "user" : context.orgId ? "org" : "session";
125
+ const audienceBucket = hashToBucket(subjectId, `${experiment.key}:audience`);
126
+ if (audienceBucket >= experiment.audiencePercentage) {
127
+ return null;
128
+ }
129
+ let variant = await this.repository.getExperimentAssignment(experiment.id, subjectType, subjectId);
130
+ if (!variant) {
131
+ const variantBucket = hashToBucket(subjectId, experiment.key);
132
+ variant = this.assignVariant(experiment.variants, variantBucket);
133
+ await this.repository.saveExperimentAssignment(experiment.id, subjectType, subjectId, variant, variantBucket);
134
+ }
135
+ const enabled = variant !== "control";
136
+ return this.makeResult(enabled, "EXPERIMENT_VARIANT", variant, undefined, experiment.id);
137
+ }
138
+ assignVariant(variants, bucket) {
139
+ let cumulative = 0;
140
+ for (const variant of variants) {
141
+ cumulative += variant.percentage;
142
+ if (bucket < cumulative) {
143
+ return variant.key;
144
+ }
145
+ }
146
+ return variants[variants.length - 1]?.key ?? "control";
147
+ }
148
+ makeResult(enabled, reason, variant, ruleId, experimentId) {
149
+ return {
150
+ enabled,
151
+ variant,
152
+ reason,
153
+ ruleId,
154
+ experimentId
155
+ };
156
+ }
157
+ logAndReturn(flag, context, result) {
158
+ if (this.logEvaluations && this.logger) {
159
+ const subjectId = getSubjectId(context);
160
+ const subjectType = context.userId ? "user" : context.orgId ? "org" : "session";
161
+ this.logger.log({
162
+ flagId: flag.id,
163
+ flagKey: flag.key,
164
+ subjectType,
165
+ subjectId,
166
+ result: result.enabled,
167
+ variant: result.variant,
168
+ reason: result.reason,
169
+ ruleId: result.ruleId,
170
+ experimentId: result.experimentId,
171
+ context
172
+ });
173
+ }
174
+ return result;
175
+ }
70
176
  }
71
- /**
72
- * Feature flag evaluator.
73
- *
74
- * Evaluates flags based on:
75
- * 1. Flag status (OFF/ON/GRADUAL)
76
- * 2. Targeting rules (in priority order)
77
- * 3. Experiments (if running)
78
- * 4. Default value (fallback)
79
- */
80
- var FlagEvaluator = class {
81
- repository;
82
- logger;
83
- logEvaluations;
84
- constructor(options) {
85
- this.repository = options.repository;
86
- this.logger = options.logger;
87
- this.logEvaluations = options.logEvaluations ?? false;
88
- }
89
- /**
90
- * Evaluate a feature flag.
91
- */
92
- async evaluate(key, context) {
93
- const orgId = context.orgId;
94
- const flag = await this.repository.getFlag(key, orgId);
95
- if (!flag) return this.makeResult(false, "FLAG_NOT_FOUND");
96
- if (flag.status === "OFF") return this.logAndReturn(flag, context, this.makeResult(false, "FLAG_OFF"));
97
- if (flag.status === "ON") return this.logAndReturn(flag, context, this.makeResult(true, "FLAG_ON"));
98
- const sortedRules = [...await this.repository.getRules(flag.id)].filter((r) => r.enabled).sort((a, b) => a.priority - b.priority);
99
- for (const rule of sortedRules) if (evaluateRuleCondition(rule, context)) {
100
- if (rule.rolloutPercentage !== void 0 && rule.rolloutPercentage !== null) {
101
- if (hashToBucket(getSubjectId(context), flag.key) >= rule.rolloutPercentage) continue;
102
- }
103
- const enabled = rule.serveValue ?? true;
104
- return this.logAndReturn(flag, context, this.makeResult(enabled, "RULE_MATCH", rule.serveVariant, rule.id));
105
- }
106
- const experiment = await this.repository.getActiveExperiment(flag.id);
107
- if (experiment && experiment.status === "RUNNING") {
108
- const result = await this.evaluateExperiment(experiment, context);
109
- if (result) return this.logAndReturn(flag, context, result);
110
- }
111
- return this.logAndReturn(flag, context, this.makeResult(flag.defaultValue, "DEFAULT_VALUE"));
112
- }
113
- /**
114
- * Evaluate experiment and assign variant.
115
- */
116
- async evaluateExperiment(experiment, context) {
117
- const subjectId = getSubjectId(context);
118
- const subjectType = context.userId ? "user" : context.orgId ? "org" : "session";
119
- if (hashToBucket(subjectId, `${experiment.key}:audience`) >= experiment.audiencePercentage) return null;
120
- let variant = await this.repository.getExperimentAssignment(experiment.id, subjectType, subjectId);
121
- if (!variant) {
122
- const variantBucket = hashToBucket(subjectId, experiment.key);
123
- variant = this.assignVariant(experiment.variants, variantBucket);
124
- await this.repository.saveExperimentAssignment(experiment.id, subjectType, subjectId, variant, variantBucket);
125
- }
126
- const enabled = variant !== "control";
127
- return this.makeResult(enabled, "EXPERIMENT_VARIANT", variant, void 0, experiment.id);
128
- }
129
- /**
130
- * Assign a variant based on bucket and variant percentages.
131
- */
132
- assignVariant(variants, bucket) {
133
- let cumulative = 0;
134
- for (const variant of variants) {
135
- cumulative += variant.percentage;
136
- if (bucket < cumulative) return variant.key;
137
- }
138
- return variants[variants.length - 1]?.key ?? "control";
139
- }
140
- /**
141
- * Create evaluation result.
142
- */
143
- makeResult(enabled, reason, variant, ruleId, experimentId) {
144
- return {
145
- enabled,
146
- variant,
147
- reason,
148
- ruleId,
149
- experimentId
150
- };
151
- }
152
- /**
153
- * Log evaluation and return result.
154
- */
155
- logAndReturn(flag, context, result) {
156
- if (this.logEvaluations && this.logger) {
157
- const subjectId = getSubjectId(context);
158
- const subjectType = context.userId ? "user" : context.orgId ? "org" : "session";
159
- this.logger.log({
160
- flagId: flag.id,
161
- flagKey: flag.key,
162
- subjectType,
163
- subjectId,
164
- result: result.enabled,
165
- variant: result.variant,
166
- reason: result.reason,
167
- ruleId: result.ruleId,
168
- experimentId: result.experimentId,
169
- context
170
- });
171
- }
172
- return result;
173
- }
174
- };
175
- /**
176
- * In-memory flag repository for testing and development.
177
- */
178
- var InMemoryFlagRepository = class {
179
- flags = /* @__PURE__ */ new Map();
180
- rules = /* @__PURE__ */ new Map();
181
- experiments = /* @__PURE__ */ new Map();
182
- assignments = /* @__PURE__ */ new Map();
183
- addFlag(flag) {
184
- this.flags.set(flag.key, flag);
185
- }
186
- addRule(flagId, rule) {
187
- const existing = this.rules.get(flagId) || [];
188
- existing.push(rule);
189
- this.rules.set(flagId, existing);
190
- }
191
- addExperiment(experiment, flagId) {
192
- this.experiments.set(flagId, experiment);
193
- }
194
- async getFlag(key) {
195
- return this.flags.get(key) || null;
196
- }
197
- async getRules(flagId) {
198
- return this.rules.get(flagId) || [];
199
- }
200
- async getActiveExperiment(flagId) {
201
- return this.experiments.get(flagId) || null;
202
- }
203
- async getExperimentAssignment(experimentId, subjectType, subjectId) {
204
- const key = `${experimentId}:${subjectType}:${subjectId}`;
205
- return this.assignments.get(key) || null;
206
- }
207
- async saveExperimentAssignment(experimentId, subjectType, subjectId, variant) {
208
- const key = `${experimentId}:${subjectType}:${subjectId}`;
209
- this.assignments.set(key, variant);
210
- }
211
- clear() {
212
- this.flags.clear();
213
- this.rules.clear();
214
- this.experiments.clear();
215
- this.assignments.clear();
216
- }
217
- };
218
177
 
219
- //#endregion
220
- export { FlagEvaluator, InMemoryFlagRepository, evaluateRuleCondition, getSubjectId, hashToBucket };
221
- //# sourceMappingURL=index.js.map
178
+ class InMemoryFlagRepository {
179
+ flags = new Map;
180
+ rules = new Map;
181
+ experiments = new Map;
182
+ assignments = new Map;
183
+ addFlag(flag) {
184
+ this.flags.set(flag.key, flag);
185
+ }
186
+ addRule(flagId, rule) {
187
+ const existing = this.rules.get(flagId) || [];
188
+ existing.push(rule);
189
+ this.rules.set(flagId, existing);
190
+ }
191
+ addExperiment(experiment, flagId) {
192
+ this.experiments.set(flagId, experiment);
193
+ }
194
+ async getFlag(key) {
195
+ return this.flags.get(key) || null;
196
+ }
197
+ async getRules(flagId) {
198
+ return this.rules.get(flagId) || [];
199
+ }
200
+ async getActiveExperiment(flagId) {
201
+ return this.experiments.get(flagId) || null;
202
+ }
203
+ async getExperimentAssignment(experimentId, subjectType, subjectId) {
204
+ const key = `${experimentId}:${subjectType}:${subjectId}`;
205
+ return this.assignments.get(key) || null;
206
+ }
207
+ async saveExperimentAssignment(experimentId, subjectType, subjectId, variant) {
208
+ const key = `${experimentId}:${subjectType}:${subjectId}`;
209
+ this.assignments.set(key, variant);
210
+ }
211
+ clear() {
212
+ this.flags.clear();
213
+ this.rules.clear();
214
+ this.experiments.clear();
215
+ this.assignments.clear();
216
+ }
217
+ }
218
+ export {
219
+ hashToBucket,
220
+ getSubjectId,
221
+ evaluateRuleCondition,
222
+ InMemoryFlagRepository,
223
+ FlagEvaluator
224
+ };