@contractspec/lib.feature-flags 1.57.0 → 1.59.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
@@ -0,0 +1,71 @@
1
+ // src/docs/feature-flags.docblock.ts
2
+ import { registerDocBlocks } from "@contractspec/lib.contracts/docs";
3
+ var featureFlagsDocBlocks = [
4
+ {
5
+ id: "docs.feature-flags.overview",
6
+ title: "Feature Flags & Experiments",
7
+ summary: "Reusable, spec-first feature flag and experiment module with targeting, gradual rollout, multivariate variants, and evaluation logging.",
8
+ kind: "reference",
9
+ visibility: "public",
10
+ route: "/docs/feature-flags/overview",
11
+ tags: ["feature-flags", "experiments", "progressive-delivery"],
12
+ body: `## What this module provides
13
+
14
+ - **Entities**: FeatureFlag, FlagTargetingRule, Experiment, ExperimentAssignment, FlagEvaluation.
15
+ - **Contracts**: create/update/delete/toggle/list/get flags; create/delete rules; evaluate flags; create/start/stop/get experiments.
16
+ - **Events**: flag.created/updated/deleted/toggled, rule.created/deleted, experiment.created/started/stopped, flag.evaluated, experiment.variant_assigned.
17
+ - **Evaluation Engine**: Deterministic evaluator with gradual rollout, rule priority, audience filters, and experiment bucketing.
18
+
19
+ ## How to use
20
+
21
+ 1) Compose schema
22
+ - Add \`featureFlagsSchemaContribution\` to your module composition.
23
+
24
+ 2) Register contracts/events
25
+ - Import exports from \`@contractspec/lib.feature-flags\` into your spec registry.
26
+
27
+ 3) Evaluate at runtime
28
+ - Instantiate \`FlagEvaluator\` with a repository implementation and optional logger.
29
+ - Evaluate with context attributes (userId, orgId, plan, segment, sessionId, attributes).
30
+
31
+ 4) Wire observability
32
+ - Emit audit trail on config changes; emit \`flag.evaluated\` for analytics.
33
+
34
+ ## Usage example
35
+
36
+ ${"```"}ts
37
+ import {
38
+ FlagEvaluator,
39
+ InMemoryFlagRepository,
40
+ } from '@contractspec/lib.feature-flags';
41
+
42
+ const repo = new InMemoryFlagRepository();
43
+ repo.addFlag({
44
+ id: 'flag-1',
45
+ key: 'new_dashboard',
46
+ status: 'GRADUAL',
47
+ defaultValue: false,
48
+ });
49
+
50
+ const evaluator = new FlagEvaluator({ repository: repo });
51
+ const result = await evaluator.evaluate('new_dashboard', {
52
+ userId: 'user-123',
53
+ orgId: 'org-456',
54
+ plan: 'pro',
55
+ });
56
+
57
+ if (result.enabled) {
58
+ // serve the new dashboard
59
+ }
60
+ ${"```"},
61
+
62
+ ## Guardrails
63
+
64
+ - Keep flag keys stable and human-readable; avoid PII in context.
65
+ - Ensure experiments’ variant percentages sum to 100; default flag status to OFF.
66
+ - Use org-scoped flags for multi-tenant isolation.
67
+ - Log evaluations only when needed to control volume; prefer sampling for noisy paths.
68
+ `
69
+ }
70
+ ];
71
+ registerDocBlocks(featureFlagsDocBlocks);
@@ -0,0 +1,306 @@
1
+ // src/entities/index.ts
2
+ import {
3
+ defineEntity,
4
+ defineEntityEnum,
5
+ field,
6
+ index
7
+ } from "@contractspec/lib.schema";
8
+ var FlagStatusEnum = defineEntityEnum({
9
+ name: "FlagStatus",
10
+ values: ["OFF", "ON", "GRADUAL"],
11
+ schema: "lssm_feature_flags",
12
+ description: "Status of a feature flag."
13
+ });
14
+ var RuleOperatorEnum = defineEntityEnum({
15
+ name: "RuleOperator",
16
+ values: [
17
+ "EQ",
18
+ "NEQ",
19
+ "IN",
20
+ "NIN",
21
+ "CONTAINS",
22
+ "NOT_CONTAINS",
23
+ "GT",
24
+ "GTE",
25
+ "LT",
26
+ "LTE",
27
+ "PERCENTAGE"
28
+ ],
29
+ schema: "lssm_feature_flags",
30
+ description: "Operator for targeting rule conditions."
31
+ });
32
+ var ExperimentStatusEnum = defineEntityEnum({
33
+ name: "ExperimentStatus",
34
+ values: ["DRAFT", "RUNNING", "PAUSED", "COMPLETED", "CANCELLED"],
35
+ schema: "lssm_feature_flags",
36
+ description: "Status of an experiment."
37
+ });
38
+ var FeatureFlagEntity = defineEntity({
39
+ name: "FeatureFlag",
40
+ description: "A feature flag for controlling feature availability.",
41
+ schema: "lssm_feature_flags",
42
+ map: "feature_flag",
43
+ fields: {
44
+ id: field.id({ description: "Unique flag identifier" }),
45
+ key: field.string({
46
+ isUnique: true,
47
+ description: "Flag key (e.g., new_dashboard)"
48
+ }),
49
+ name: field.string({ description: "Human-readable name" }),
50
+ description: field.string({
51
+ isOptional: true,
52
+ description: "Description of the flag"
53
+ }),
54
+ status: field.enum("FlagStatus", {
55
+ default: "OFF",
56
+ description: "Flag status"
57
+ }),
58
+ defaultValue: field.boolean({
59
+ default: false,
60
+ description: "Default value when no rules match"
61
+ }),
62
+ variants: field.json({
63
+ isOptional: true,
64
+ description: "Variant definitions for multivariate flags"
65
+ }),
66
+ orgId: field.string({
67
+ isOptional: true,
68
+ description: "Organization scope (null = global)"
69
+ }),
70
+ tags: field.json({
71
+ isOptional: true,
72
+ description: "Tags for categorization"
73
+ }),
74
+ metadata: field.json({
75
+ isOptional: true,
76
+ description: "Additional metadata"
77
+ }),
78
+ createdAt: field.createdAt(),
79
+ updatedAt: field.updatedAt(),
80
+ targetingRules: field.hasMany("FlagTargetingRule"),
81
+ experiments: field.hasMany("Experiment"),
82
+ evaluations: field.hasMany("FlagEvaluation")
83
+ },
84
+ indexes: [index.on(["orgId", "key"]), index.on(["status"])],
85
+ enums: [FlagStatusEnum]
86
+ });
87
+ var FlagTargetingRuleEntity = defineEntity({
88
+ name: "FlagTargetingRule",
89
+ description: "A targeting rule for conditional flag evaluation.",
90
+ schema: "lssm_feature_flags",
91
+ map: "flag_targeting_rule",
92
+ fields: {
93
+ id: field.id({ description: "Unique rule identifier" }),
94
+ flagId: field.foreignKey({ description: "Parent feature flag" }),
95
+ name: field.string({
96
+ isOptional: true,
97
+ description: "Rule name for debugging"
98
+ }),
99
+ priority: field.int({
100
+ default: 0,
101
+ description: "Rule priority (lower = higher priority)"
102
+ }),
103
+ enabled: field.boolean({
104
+ default: true,
105
+ description: "Whether rule is active"
106
+ }),
107
+ attribute: field.string({
108
+ description: "Target attribute (userId, orgId, plan, segment, etc.)"
109
+ }),
110
+ operator: field.enum("RuleOperator", {
111
+ description: "Comparison operator"
112
+ }),
113
+ value: field.json({ description: "Target value(s)" }),
114
+ rolloutPercentage: field.int({
115
+ isOptional: true,
116
+ description: "Percentage for gradual rollout (0-100)"
117
+ }),
118
+ serveValue: field.boolean({
119
+ isOptional: true,
120
+ description: "Boolean value to serve"
121
+ }),
122
+ serveVariant: field.string({
123
+ isOptional: true,
124
+ description: "Variant key to serve (for multivariate)"
125
+ }),
126
+ createdAt: field.createdAt(),
127
+ updatedAt: field.updatedAt(),
128
+ flag: field.belongsTo("FeatureFlag", ["flagId"], ["id"], {
129
+ onDelete: "Cascade"
130
+ })
131
+ },
132
+ indexes: [index.on(["flagId", "priority"]), index.on(["attribute"])],
133
+ enums: [RuleOperatorEnum]
134
+ });
135
+ var ExperimentEntity = defineEntity({
136
+ name: "Experiment",
137
+ description: "An A/B test experiment.",
138
+ schema: "lssm_feature_flags",
139
+ map: "experiment",
140
+ fields: {
141
+ id: field.id({ description: "Unique experiment identifier" }),
142
+ key: field.string({ isUnique: true, description: "Experiment key" }),
143
+ name: field.string({ description: "Human-readable name" }),
144
+ description: field.string({
145
+ isOptional: true,
146
+ description: "Experiment description"
147
+ }),
148
+ hypothesis: field.string({
149
+ isOptional: true,
150
+ description: "Experiment hypothesis"
151
+ }),
152
+ flagId: field.foreignKey({ description: "Associated feature flag" }),
153
+ status: field.enum("ExperimentStatus", {
154
+ default: "DRAFT",
155
+ description: "Experiment status"
156
+ }),
157
+ variants: field.json({
158
+ description: "Variant definitions with split ratios"
159
+ }),
160
+ metrics: field.json({ isOptional: true, description: "Metrics to track" }),
161
+ audiencePercentage: field.int({
162
+ default: 100,
163
+ description: "Percentage of audience to include"
164
+ }),
165
+ audienceFilter: field.json({
166
+ isOptional: true,
167
+ description: "Audience filter criteria"
168
+ }),
169
+ scheduledStartAt: field.dateTime({
170
+ isOptional: true,
171
+ description: "Scheduled start time"
172
+ }),
173
+ scheduledEndAt: field.dateTime({
174
+ isOptional: true,
175
+ description: "Scheduled end time"
176
+ }),
177
+ startedAt: field.dateTime({
178
+ isOptional: true,
179
+ description: "Actual start time"
180
+ }),
181
+ endedAt: field.dateTime({
182
+ isOptional: true,
183
+ description: "Actual end time"
184
+ }),
185
+ winningVariant: field.string({
186
+ isOptional: true,
187
+ description: "Declared winning variant"
188
+ }),
189
+ results: field.json({
190
+ isOptional: true,
191
+ description: "Experiment results summary"
192
+ }),
193
+ orgId: field.string({
194
+ isOptional: true,
195
+ description: "Organization scope"
196
+ }),
197
+ createdAt: field.createdAt(),
198
+ updatedAt: field.updatedAt(),
199
+ flag: field.belongsTo("FeatureFlag", ["flagId"], ["id"], {
200
+ onDelete: "Cascade"
201
+ }),
202
+ assignments: field.hasMany("ExperimentAssignment")
203
+ },
204
+ indexes: [
205
+ index.on(["status"]),
206
+ index.on(["orgId", "status"]),
207
+ index.on(["flagId"])
208
+ ],
209
+ enums: [ExperimentStatusEnum]
210
+ });
211
+ var ExperimentAssignmentEntity = defineEntity({
212
+ name: "ExperimentAssignment",
213
+ description: "Tracks experiment variant assignments.",
214
+ schema: "lssm_feature_flags",
215
+ map: "experiment_assignment",
216
+ fields: {
217
+ id: field.id({ description: "Unique assignment identifier" }),
218
+ experimentId: field.foreignKey({ description: "Parent experiment" }),
219
+ subjectType: field.string({
220
+ description: "Subject type (user, org, session)"
221
+ }),
222
+ subjectId: field.string({ description: "Subject identifier" }),
223
+ variant: field.string({ description: "Assigned variant key" }),
224
+ bucket: field.int({ description: "Hash bucket (0-99)" }),
225
+ context: field.json({
226
+ isOptional: true,
227
+ description: "Context at assignment time"
228
+ }),
229
+ assignedAt: field.dateTime({ description: "Assignment timestamp" }),
230
+ experiment: field.belongsTo("Experiment", ["experimentId"], ["id"], {
231
+ onDelete: "Cascade"
232
+ })
233
+ },
234
+ indexes: [
235
+ index.unique(["experimentId", "subjectType", "subjectId"], {
236
+ name: "experiment_assignment_unique"
237
+ }),
238
+ index.on(["subjectType", "subjectId"])
239
+ ]
240
+ });
241
+ var FlagEvaluationEntity = defineEntity({
242
+ name: "FlagEvaluation",
243
+ description: "Log of flag evaluations for debugging and analytics.",
244
+ schema: "lssm_feature_flags",
245
+ map: "flag_evaluation",
246
+ fields: {
247
+ id: field.id({ description: "Unique evaluation identifier" }),
248
+ flagId: field.foreignKey({ description: "Evaluated flag" }),
249
+ flagKey: field.string({
250
+ description: "Flag key (denormalized for queries)"
251
+ }),
252
+ subjectType: field.string({
253
+ description: "Subject type (user, org, anonymous)"
254
+ }),
255
+ subjectId: field.string({ description: "Subject identifier" }),
256
+ result: field.boolean({ description: "Evaluation result" }),
257
+ variant: field.string({
258
+ isOptional: true,
259
+ description: "Served variant (for multivariate)"
260
+ }),
261
+ matchedRuleId: field.string({
262
+ isOptional: true,
263
+ description: "Rule that matched (if any)"
264
+ }),
265
+ reason: field.string({
266
+ description: "Evaluation reason (default, rule, experiment, etc.)"
267
+ }),
268
+ context: field.json({
269
+ isOptional: true,
270
+ description: "Evaluation context"
271
+ }),
272
+ evaluatedAt: field.dateTime({ description: "Evaluation timestamp" }),
273
+ flag: field.belongsTo("FeatureFlag", ["flagId"], ["id"], {
274
+ onDelete: "Cascade"
275
+ })
276
+ },
277
+ indexes: [
278
+ index.on(["flagKey", "evaluatedAt"]),
279
+ index.on(["subjectType", "subjectId", "evaluatedAt"]),
280
+ index.on(["flagId", "evaluatedAt"])
281
+ ]
282
+ });
283
+ var featureFlagEntities = [
284
+ FeatureFlagEntity,
285
+ FlagTargetingRuleEntity,
286
+ ExperimentEntity,
287
+ ExperimentAssignmentEntity,
288
+ FlagEvaluationEntity
289
+ ];
290
+ var featureFlagsSchemaContribution = {
291
+ moduleId: "@contractspec/lib.feature-flags",
292
+ entities: featureFlagEntities,
293
+ enums: [FlagStatusEnum, RuleOperatorEnum, ExperimentStatusEnum]
294
+ };
295
+ export {
296
+ featureFlagsSchemaContribution,
297
+ featureFlagEntities,
298
+ RuleOperatorEnum,
299
+ FlagTargetingRuleEntity,
300
+ FlagStatusEnum,
301
+ FlagEvaluationEntity,
302
+ FeatureFlagEntity,
303
+ ExperimentStatusEnum,
304
+ ExperimentEntity,
305
+ ExperimentAssignmentEntity
306
+ };
@@ -0,0 +1,223 @@
1
+ // src/evaluation/index.ts
2
+ function hashToBucket(value, seed = "") {
3
+ const input = `${seed}:${value}`;
4
+ let hash = 0;
5
+ for (let i = 0;i < input.length; i++) {
6
+ const char = input.charCodeAt(i);
7
+ hash = (hash << 5) - hash + char;
8
+ hash = hash & hash;
9
+ }
10
+ return Math.abs(hash % 100);
11
+ }
12
+ function getSubjectId(context) {
13
+ return context.userId || context.sessionId || context.orgId || "anonymous";
14
+ }
15
+ function evaluateRuleCondition(rule, context) {
16
+ const attributeValue = getAttributeValue(rule.attribute, context);
17
+ switch (rule.operator) {
18
+ case "EQ":
19
+ return attributeValue === rule.value;
20
+ case "NEQ":
21
+ return attributeValue !== rule.value;
22
+ case "IN":
23
+ if (!Array.isArray(rule.value))
24
+ return false;
25
+ return rule.value.includes(attributeValue);
26
+ case "NIN":
27
+ if (!Array.isArray(rule.value))
28
+ return true;
29
+ return !rule.value.includes(attributeValue);
30
+ case "CONTAINS":
31
+ if (typeof attributeValue !== "string" || typeof rule.value !== "string")
32
+ return false;
33
+ return attributeValue.includes(rule.value);
34
+ case "NOT_CONTAINS":
35
+ if (typeof attributeValue !== "string" || typeof rule.value !== "string")
36
+ return true;
37
+ return !attributeValue.includes(rule.value);
38
+ case "GT":
39
+ if (typeof attributeValue !== "number" || typeof rule.value !== "number")
40
+ return false;
41
+ return attributeValue > rule.value;
42
+ case "GTE":
43
+ if (typeof attributeValue !== "number" || typeof rule.value !== "number")
44
+ return false;
45
+ return attributeValue >= rule.value;
46
+ case "LT":
47
+ if (typeof attributeValue !== "number" || typeof rule.value !== "number")
48
+ return false;
49
+ return attributeValue < rule.value;
50
+ case "LTE":
51
+ if (typeof attributeValue !== "number" || typeof rule.value !== "number")
52
+ return false;
53
+ return attributeValue <= rule.value;
54
+ case "PERCENTAGE":
55
+ return hashToBucket(getSubjectId(context), rule.attribute) < (typeof rule.value === "number" ? rule.value : 0);
56
+ default:
57
+ return false;
58
+ }
59
+ }
60
+ function getAttributeValue(attribute, context) {
61
+ switch (attribute) {
62
+ case "userId":
63
+ return context.userId;
64
+ case "orgId":
65
+ return context.orgId;
66
+ case "plan":
67
+ return context.plan;
68
+ case "segment":
69
+ return context.segment;
70
+ case "sessionId":
71
+ return context.sessionId;
72
+ default:
73
+ return context.attributes?.[attribute];
74
+ }
75
+ }
76
+
77
+ class FlagEvaluator {
78
+ repository;
79
+ logger;
80
+ logEvaluations;
81
+ constructor(options) {
82
+ this.repository = options.repository;
83
+ this.logger = options.logger;
84
+ this.logEvaluations = options.logEvaluations ?? false;
85
+ }
86
+ async evaluate(key, context) {
87
+ const orgId = context.orgId;
88
+ const flag = await this.repository.getFlag(key, orgId);
89
+ if (!flag) {
90
+ return this.makeResult(false, "FLAG_NOT_FOUND");
91
+ }
92
+ if (flag.status === "OFF") {
93
+ return this.logAndReturn(flag, context, this.makeResult(false, "FLAG_OFF"));
94
+ }
95
+ if (flag.status === "ON") {
96
+ return this.logAndReturn(flag, context, this.makeResult(true, "FLAG_ON"));
97
+ }
98
+ const rules = await this.repository.getRules(flag.id);
99
+ const sortedRules = [...rules].filter((r) => r.enabled).sort((a, b) => a.priority - b.priority);
100
+ for (const rule of sortedRules) {
101
+ if (evaluateRuleCondition(rule, context)) {
102
+ if (rule.rolloutPercentage !== undefined && rule.rolloutPercentage !== null) {
103
+ const bucket = hashToBucket(getSubjectId(context), flag.key);
104
+ if (bucket >= rule.rolloutPercentage) {
105
+ continue;
106
+ }
107
+ }
108
+ const enabled = rule.serveValue ?? true;
109
+ return this.logAndReturn(flag, context, this.makeResult(enabled, "RULE_MATCH", rule.serveVariant, rule.id));
110
+ }
111
+ }
112
+ const experiment = await this.repository.getActiveExperiment(flag.id);
113
+ if (experiment && experiment.status === "RUNNING") {
114
+ const result = await this.evaluateExperiment(experiment, context);
115
+ if (result) {
116
+ return this.logAndReturn(flag, context, result);
117
+ }
118
+ }
119
+ return this.logAndReturn(flag, context, this.makeResult(flag.defaultValue, "DEFAULT_VALUE"));
120
+ }
121
+ async evaluateExperiment(experiment, context) {
122
+ const subjectId = getSubjectId(context);
123
+ const subjectType = context.userId ? "user" : context.orgId ? "org" : "session";
124
+ const audienceBucket = hashToBucket(subjectId, `${experiment.key}:audience`);
125
+ if (audienceBucket >= experiment.audiencePercentage) {
126
+ return null;
127
+ }
128
+ let variant = await this.repository.getExperimentAssignment(experiment.id, subjectType, subjectId);
129
+ if (!variant) {
130
+ const variantBucket = hashToBucket(subjectId, experiment.key);
131
+ variant = this.assignVariant(experiment.variants, variantBucket);
132
+ await this.repository.saveExperimentAssignment(experiment.id, subjectType, subjectId, variant, variantBucket);
133
+ }
134
+ const enabled = variant !== "control";
135
+ return this.makeResult(enabled, "EXPERIMENT_VARIANT", variant, undefined, experiment.id);
136
+ }
137
+ assignVariant(variants, bucket) {
138
+ let cumulative = 0;
139
+ for (const variant of variants) {
140
+ cumulative += variant.percentage;
141
+ if (bucket < cumulative) {
142
+ return variant.key;
143
+ }
144
+ }
145
+ return variants[variants.length - 1]?.key ?? "control";
146
+ }
147
+ makeResult(enabled, reason, variant, ruleId, experimentId) {
148
+ return {
149
+ enabled,
150
+ variant,
151
+ reason,
152
+ ruleId,
153
+ experimentId
154
+ };
155
+ }
156
+ logAndReturn(flag, context, result) {
157
+ if (this.logEvaluations && this.logger) {
158
+ const subjectId = getSubjectId(context);
159
+ const subjectType = context.userId ? "user" : context.orgId ? "org" : "session";
160
+ this.logger.log({
161
+ flagId: flag.id,
162
+ flagKey: flag.key,
163
+ subjectType,
164
+ subjectId,
165
+ result: result.enabled,
166
+ variant: result.variant,
167
+ reason: result.reason,
168
+ ruleId: result.ruleId,
169
+ experimentId: result.experimentId,
170
+ context
171
+ });
172
+ }
173
+ return result;
174
+ }
175
+ }
176
+
177
+ class InMemoryFlagRepository {
178
+ flags = new Map;
179
+ rules = new Map;
180
+ experiments = new Map;
181
+ assignments = new Map;
182
+ addFlag(flag) {
183
+ this.flags.set(flag.key, flag);
184
+ }
185
+ addRule(flagId, rule) {
186
+ const existing = this.rules.get(flagId) || [];
187
+ existing.push(rule);
188
+ this.rules.set(flagId, existing);
189
+ }
190
+ addExperiment(experiment, flagId) {
191
+ this.experiments.set(flagId, experiment);
192
+ }
193
+ async getFlag(key) {
194
+ return this.flags.get(key) || null;
195
+ }
196
+ async getRules(flagId) {
197
+ return this.rules.get(flagId) || [];
198
+ }
199
+ async getActiveExperiment(flagId) {
200
+ return this.experiments.get(flagId) || null;
201
+ }
202
+ async getExperimentAssignment(experimentId, subjectType, subjectId) {
203
+ const key = `${experimentId}:${subjectType}:${subjectId}`;
204
+ return this.assignments.get(key) || null;
205
+ }
206
+ async saveExperimentAssignment(experimentId, subjectType, subjectId, variant) {
207
+ const key = `${experimentId}:${subjectType}:${subjectId}`;
208
+ this.assignments.set(key, variant);
209
+ }
210
+ clear() {
211
+ this.flags.clear();
212
+ this.rules.clear();
213
+ this.experiments.clear();
214
+ this.assignments.clear();
215
+ }
216
+ }
217
+ export {
218
+ hashToBucket,
219
+ getSubjectId,
220
+ evaluateRuleCondition,
221
+ InMemoryFlagRepository,
222
+ FlagEvaluator
223
+ };