flagcraft 0.1.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.
@@ -0,0 +1,31 @@
1
+ import { FlagDefinition, Ruleset } from './types.js';
2
+ /**
3
+ * In-memory store for flag definitions and ETag.
4
+ * Replaces the entire cached ruleset atomically on updates.
5
+ */
6
+ export declare class InMemoryCache {
7
+ private ruleset;
8
+ private flagMap;
9
+ private lastUpdated;
10
+ /**
11
+ * Checks if the cache has a valid state (has been initialized and not expired).
12
+ */
13
+ isStateValid(maxAge: number): boolean;
14
+ /**
15
+ * Replaces the entire cached ruleset atomically and updates the internal lookup map.
16
+ */
17
+ setRuleset(ruleset: Ruleset): void;
18
+ /**
19
+ * Retrieves a flag definition by its key in O(1) time.
20
+ */
21
+ getFlag(key: string): FlagDefinition | undefined;
22
+ /**
23
+ * Retrieves the current ETag associated with the cached ruleset.
24
+ */
25
+ getEtag(): string | null;
26
+ /**
27
+ * Retrieves the entire cached ruleset.
28
+ */
29
+ getRuleset(): Ruleset | null;
30
+ }
31
+ //# sourceMappingURL=cache.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAErD;;;GAGG;AACH,qBAAa,aAAa;IACtB,OAAO,CAAC,OAAO,CAAwB;IACvC,OAAO,CAAC,OAAO,CAA0C;IACzD,OAAO,CAAC,WAAW,CAAqB;IAExC;;OAEG;IACH,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO;IAKrC;;OAEG;IACI,UAAU,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI;IASzC;;OAEG;IACI,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,cAAc,GAAG,SAAS;IAIvD;;OAEG;IACI,OAAO,IAAI,MAAM,GAAG,IAAI;IAI/B;;OAEG;IACI,UAAU,IAAI,OAAO,GAAG,IAAI;CAGtC"}
package/dist/cache.js ADDED
@@ -0,0 +1,47 @@
1
+ /**
2
+ * In-memory store for flag definitions and ETag.
3
+ * Replaces the entire cached ruleset atomically on updates.
4
+ */
5
+ export class InMemoryCache {
6
+ ruleset = null;
7
+ flagMap = new Map();
8
+ lastUpdated = null;
9
+ /**
10
+ * Checks if the cache has a valid state (has been initialized and not expired).
11
+ */
12
+ isStateValid(maxAge) {
13
+ if (!this.lastUpdated)
14
+ return false;
15
+ return (Date.now() - this.lastUpdated.getTime()) <= maxAge;
16
+ }
17
+ /**
18
+ * Replaces the entire cached ruleset atomically and updates the internal lookup map.
19
+ */
20
+ setRuleset(ruleset) {
21
+ this.ruleset = ruleset;
22
+ this.lastUpdated = new Date();
23
+ this.flagMap.clear();
24
+ for (const flag of ruleset.flags) {
25
+ this.flagMap.set(flag.key, flag);
26
+ }
27
+ }
28
+ /**
29
+ * Retrieves a flag definition by its key in O(1) time.
30
+ */
31
+ getFlag(key) {
32
+ return this.flagMap.get(key);
33
+ }
34
+ /**
35
+ * Retrieves the current ETag associated with the cached ruleset.
36
+ */
37
+ getEtag() {
38
+ return this.ruleset ? this.ruleset.etag : null;
39
+ }
40
+ /**
41
+ * Retrieves the entire cached ruleset.
42
+ */
43
+ getRuleset() {
44
+ return this.ruleset;
45
+ }
46
+ }
47
+ //# sourceMappingURL=cache.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache.js","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"AAEA;;;GAGG;AACH,MAAM,OAAO,aAAa;IACd,OAAO,GAAmB,IAAI,CAAC;IAC/B,OAAO,GAAgC,IAAI,GAAG,EAAE,CAAC;IACjD,WAAW,GAAgB,IAAI,CAAC;IAExC;;OAEG;IACH,YAAY,CAAC,MAAc;QACvB,IAAI,CAAC,IAAI,CAAC,WAAW;YAAE,OAAO,KAAK,CAAC;QACpC,OAAO,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,WAAW,CAAC,OAAO,EAAE,CAAC,IAAI,MAAM,CAAC;IAC/D,CAAC;IAED;;OAEG;IACI,UAAU,CAAC,OAAgB;QAC9B,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,IAAI,CAAC,WAAW,GAAG,IAAI,IAAI,EAAE,CAAC;QAC9B,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;QACrB,KAAK,MAAM,IAAI,IAAI,OAAO,CAAC,KAAK,EAAE,CAAC;YAC/B,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;QACrC,CAAC;IACL,CAAC;IAED;;OAEG;IACI,OAAO,CAAC,GAAW;QACtB,OAAO,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACjC,CAAC;IAED;;OAEG;IACI,OAAO;QACV,OAAO,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC;IACnD,CAAC;IAED;;OAEG;IACI,UAAU;QACb,OAAO,IAAI,CAAC,OAAO,CAAC;IACxB,CAAC;CACJ"}
@@ -0,0 +1,45 @@
1
+ import { EvaluationContext, FeatureFlagConfig, FlagDefinition } from './types.js';
2
+ /**
3
+ * FeatureFlagClient is the main entry point for the flagcraft package.
4
+ * It provides zero-latency local flag evaluation, conditional ETag polling, and fail-open resilience.
5
+ */
6
+ export declare class FeatureFlagClient {
7
+ private config;
8
+ private cache;
9
+ private poller;
10
+ private initialized;
11
+ private lastSuccessfulFetch;
12
+ private connectionStatus;
13
+ private consicutiveFailure;
14
+ private metricsQueue;
15
+ private flushTimer;
16
+ constructor(config: FeatureFlagConfig);
17
+ private flushMetrics;
18
+ trackMetrics(flagKey: string, isError: boolean): void;
19
+ private updateStatus;
20
+ getConnectionStatus(): 'connected' | 'degraded' | 'disconnected';
21
+ /**
22
+ * Bootstraps the SDK by performing an initial fetch of rulesets and starting the background polling loop.
23
+ * Guaranteed fail-open: never throws an error if the initial network call fails.
24
+ */
25
+ initialize(): Promise<void>;
26
+ /**
27
+ * Evaluates a boolean feature flag from local memory in < 1ms.
28
+ * If the flag is not found, checks defaultValues from config, and falls back to false.
29
+ */
30
+ isEnabled(flagKey: string, context: EvaluationContext): boolean;
31
+ /**
32
+ * Evaluates a feature flag of any type (string, number, json, boolean) from local memory.
33
+ * If the flag is not found, returns the configured default value or undefined.
34
+ */
35
+ getValue(flagKey: string, context: EvaluationContext): unknown;
36
+ /**
37
+ * Returns a map of all currently cached flag definitions.
38
+ */
39
+ getAllFlags(): Record<string, FlagDefinition>;
40
+ /**
41
+ * Stops the polling loop and clears the cache. Should be called when shutting down the host application.
42
+ */
43
+ destroy(): void;
44
+ }
45
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,cAAc,EAAW,MAAM,YAAY,CAAC;AAK3F;;;GAGG;AACH,qBAAa,iBAAiB;IAWd,OAAO,CAAC,MAAM;IAV1B,OAAO,CAAC,KAAK,CAAgB;IAC7B,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,mBAAmB,CAAqB;IAChD,OAAO,CAAC,gBAAgB,CAA6D;IACrF,OAAO,CAAC,kBAAkB,CAAK;IAC/B,OAAO,CAAC,YAAY,CAAoD;IACxE,OAAO,CAAC,UAAU,CAA+C;gBAG7C,MAAM,EAAE,iBAAiB;YAgC/B,YAAY;IA0B1B,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,IAAI;IASrD,OAAO,CAAC,YAAY;IAUb,mBAAmB,IAAI,WAAW,GAAG,UAAU,GAAG,cAAc;IAIvE;;;OAGG;IACU,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAkBxC;;;OAGG;IACI,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,iBAAiB,GAAG,OAAO;IAYtE;;;OAGG;IACI,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,iBAAiB,GAAG,OAAO;IAWrE;;OAEG;IACI,WAAW,IAAI,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC;IAWpD;;OAEG;IACI,OAAO,IAAI,IAAI;CAUzB"}
package/dist/client.js ADDED
@@ -0,0 +1,168 @@
1
+ import { InMemoryCache } from './cache.js';
2
+ import { Poller } from './poller.js';
3
+ import { evaluateFlag } from './evaluator.js';
4
+ /**
5
+ * FeatureFlagClient is the main entry point for the flagcraft package.
6
+ * It provides zero-latency local flag evaluation, conditional ETag polling, and fail-open resilience.
7
+ */
8
+ export class FeatureFlagClient {
9
+ config;
10
+ cache;
11
+ poller;
12
+ initialized = false;
13
+ lastSuccessfulFetch = null;
14
+ connectionStatus = 'disconnected';
15
+ consicutiveFailure = 0;
16
+ metricsQueue = [];
17
+ flushTimer = null;
18
+ constructor(config) {
19
+ this.config = config;
20
+ this.cache = new InMemoryCache();
21
+ const baseUrl = config.baseUrl || 'http://localhost:3001';
22
+ const interval = config.pollingInterval || 30000;
23
+ const onError = config.onError || ((err) => {
24
+ console.error('[FeatureFlagClient] Error:', err.message);
25
+ });
26
+ this.poller = new Poller({
27
+ baseUrl,
28
+ apiKey: config.apiKey,
29
+ interval,
30
+ onUpdate: (ruleset) => {
31
+ this.cache.setRuleset(ruleset);
32
+ },
33
+ onError,
34
+ onFetchSuccess: () => {
35
+ this.lastSuccessfulFetch = new Date();
36
+ this.consicutiveFailure = 0;
37
+ this.updateStatus('connected');
38
+ },
39
+ onFetchError: () => {
40
+ this.consicutiveFailure++;
41
+ this.updateStatus(this.consicutiveFailure < 3 ? 'degraded' : 'disconnected');
42
+ }
43
+ });
44
+ this.flushTimer = setInterval(() => {
45
+ this.flushMetrics();
46
+ }, 5000);
47
+ }
48
+ async flushMetrics() {
49
+ if (this.metricsQueue.length === 0)
50
+ return;
51
+ const batch = this.metricsQueue.splice(0, this.metricsQueue.length);
52
+ const baseUrl = this.config.baseUrl || 'http://localhost:3001';
53
+ try {
54
+ const url = `${baseUrl}/sdk/metrics`;
55
+ const payload = JSON.stringify({ metrics: batch });
56
+ await fetch(url, {
57
+ method: 'POST',
58
+ headers: {
59
+ 'Content-Type': 'application/json',
60
+ 'x-api-key': this.config.apiKey,
61
+ },
62
+ keepalive: true,
63
+ body: payload
64
+ });
65
+ }
66
+ catch (error) {
67
+ if (this.config.onError) {
68
+ this.config.onError(error instanceof Error ? error : new Error('Failed to flush metrics'));
69
+ }
70
+ }
71
+ }
72
+ trackMetrics(flagKey, isError) {
73
+ this.metricsQueue.push({
74
+ flagKey, isError
75
+ });
76
+ if (this.metricsQueue.length >= 100) {
77
+ this.flushMetrics();
78
+ }
79
+ }
80
+ updateStatus(newStatus) {
81
+ if (this.connectionStatus !== newStatus) {
82
+ this.connectionStatus = newStatus;
83
+ if (this.config.onConnectionStatusChange) {
84
+ this.config.onConnectionStatusChange(newStatus);
85
+ }
86
+ }
87
+ }
88
+ getConnectionStatus() {
89
+ return this.connectionStatus;
90
+ }
91
+ /**
92
+ * Bootstraps the SDK by performing an initial fetch of rulesets and starting the background polling loop.
93
+ * Guaranteed fail-open: never throws an error if the initial network call fails.
94
+ */
95
+ async initialize() {
96
+ if (this.initialized)
97
+ return;
98
+ try {
99
+ await this.poller.fetchRuleset();
100
+ }
101
+ catch (error) {
102
+ const err = error instanceof Error ? error : new Error(String(error));
103
+ if (this.config.onError) {
104
+ this.config.onError(err);
105
+ }
106
+ else {
107
+ console.error('[FeatureFlagClient] Initialization failed (fail-open mode active):', err.message);
108
+ }
109
+ }
110
+ finally {
111
+ this.initialized = true;
112
+ this.poller.start();
113
+ }
114
+ }
115
+ /**
116
+ * Evaluates a boolean feature flag from local memory in < 1ms.
117
+ * If the flag is not found, checks defaultValues from config, and falls back to false.
118
+ */
119
+ isEnabled(flagKey, context) {
120
+ const flag = this.cache.getFlag(flagKey);
121
+ if (!flag) {
122
+ const defaultVal = this.config.defaultValues?.[flagKey];
123
+ return typeof defaultVal === 'boolean' ? defaultVal : false;
124
+ }
125
+ const val = evaluateFlag(flag, context);
126
+ this.trackMetrics(flagKey, false);
127
+ return Boolean(val);
128
+ }
129
+ /**
130
+ * Evaluates a feature flag of any type (string, number, json, boolean) from local memory.
131
+ * If the flag is not found, returns the configured default value or undefined.
132
+ */
133
+ getValue(flagKey, context) {
134
+ const flag = this.cache.getFlag(flagKey);
135
+ if (!flag) {
136
+ return this.config.defaultValues?.[flagKey];
137
+ }
138
+ const val = evaluateFlag(flag, context);
139
+ this.trackMetrics(flagKey, false);
140
+ return val;
141
+ }
142
+ /**
143
+ * Returns a map of all currently cached flag definitions.
144
+ */
145
+ getAllFlags() {
146
+ const ruleset = this.cache.getRuleset();
147
+ if (!ruleset || !ruleset.flags)
148
+ return {};
149
+ const map = {};
150
+ for (const flag of ruleset.flags) {
151
+ map[flag.key] = flag;
152
+ }
153
+ return map;
154
+ }
155
+ /**
156
+ * Stops the polling loop and clears the cache. Should be called when shutting down the host application.
157
+ */
158
+ destroy() {
159
+ if (this.flushTimer) {
160
+ clearInterval(this.flushTimer);
161
+ this.flushTimer = null;
162
+ }
163
+ this.poller.stop();
164
+ this.cache.setRuleset({ flags: [], etag: '' });
165
+ this.initialized = false;
166
+ }
167
+ }
168
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,40 @@
1
+ import { EvaluationContext, FlagRule, FlagDefinition } from "./types.js";
2
+ /**
3
+ * Deterministic rollout bucketing according to claude.md §7:
4
+ * bucket = hash(flag_key + user_id) % 100 -> returns stable value in [0, 99]
5
+ */
6
+ export declare function hashBucket(flagKey: string, userId: string): number;
7
+ /**
8
+ * Evaluates whether a user falls within the rollout percentage [0, 100]
9
+ */
10
+ export declare function isUserInRollout(flagKey: string, userId: string, rolloutPct: number): boolean;
11
+ export interface TargetingConditions {
12
+ [attribute: string]: unknown;
13
+ }
14
+ export interface TargetingRuleInput {
15
+ priority: number;
16
+ conditions: TargetingConditions;
17
+ value: unknown;
18
+ }
19
+ /**
20
+ * Checks whether a user's attributes satisfy ALL conditions in a rule.
21
+ * A rule matches only if every condition key matches the user's corresponding attribute.
22
+ */
23
+ export declare function matchesConditions(conditions: TargetingConditions, attributes: Record<string, unknown>): boolean;
24
+ /**
25
+ * Evaluates targeting rules in priority order against user attributes.
26
+ * Returns the value of the first matching rule, or undefined if no rule matches.
27
+ *
28
+ * Rules must be sorted by priority ASC (lowest number = highest priority).
29
+ */
30
+ export declare function evaluateRules(rules: TargetingRuleInput[], attributes: Record<string, unknown>): unknown | undefined;
31
+ export declare function evaluateFlags(rules: FlagRule[], context: EvaluationContext): unknown;
32
+ /**
33
+ * Evaluates a complete FlagDefinition against an EvaluationContext.
34
+ * Order:
35
+ * 1. If !flag.enabled -> return flag.defaultValue
36
+ * 2. Check targeting rules in priority order. If match -> return rule.value
37
+ * 3. Check rollout bucketing: if user is in rollout -> return true (or active value); otherwise flag.defaultValue
38
+ */
39
+ export declare function evaluateFlag(flag: FlagDefinition, context: EvaluationContext): unknown;
40
+ //# sourceMappingURL=evaluator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"evaluator.d.ts","sourceRoot":"","sources":["../src/evaluator.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAgBzE;;;GAGG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAIlE;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAM5F;AAID,MAAM,WAAW,mBAAmB;IAChC,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC;CAChC;AAED,MAAM,WAAW,kBAAkB;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,mBAAmB,CAAC;IAChC,KAAK,EAAE,OAAO,CAAC;CAClB;AAiDD;;;GAGG;AACH,wBAAgB,iBAAiB,CAC7B,UAAU,EAAE,mBAAmB,EAC/B,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACpC,OAAO,CAUT;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CACzB,KAAK,EAAE,kBAAkB,EAAE,EAC3B,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACpC,OAAO,GAAG,SAAS,CAOrB;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,QAAQ,EAAE,EAAE,OAAO,EAAE,iBAAiB,WAG1E;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,cAAc,EAAE,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAmBtF"}
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Fast, pure-TypeScript FNV-1a 32-bit hash function.
3
+ * Zero external dependencies — works identically in Node.js and Browser SDKs.
4
+ */
5
+ function fnv1aHash(str) {
6
+ let hash = 0x811c9dc5; // FNV offset basis
7
+ for (let i = 0; i < str.length; i++) {
8
+ hash ^= str.charCodeAt(i);
9
+ // Multiply by FNV prime (0x01000193) using bit shifts to maintain 32-bit integer arithmetic
10
+ hash += (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24);
11
+ }
12
+ return hash >>> 0; // Ensure unsigned 32-bit integer
13
+ }
14
+ /**
15
+ * Deterministic rollout bucketing according to claude.md §7:
16
+ * bucket = hash(flag_key + user_id) % 100 -> returns stable value in [0, 99]
17
+ */
18
+ export function hashBucket(flagKey, userId) {
19
+ const combined = `${flagKey}:${userId}`;
20
+ const hashValue = fnv1aHash(combined);
21
+ return hashValue % 100;
22
+ }
23
+ /**
24
+ * Evaluates whether a user falls within the rollout percentage [0, 100]
25
+ */
26
+ export function isUserInRollout(flagKey, userId, rolloutPct) {
27
+ if (rolloutPct <= 0)
28
+ return false;
29
+ if (rolloutPct >= 100)
30
+ return true;
31
+ const bucket = hashBucket(flagKey, userId);
32
+ return bucket < rolloutPct;
33
+ }
34
+ /**
35
+ * Evaluates a single condition value against a user's attribute value.
36
+ *
37
+ * Supported formats:
38
+ * - Exact match: { "plan": "enterprise" }
39
+ * - Operators: { "age": { "$gte": 18 } }
40
+ *
41
+ * Supported operators: $eq, $ne, $gt, $gte, $lt, $lte, $in, $nin
42
+ */
43
+ function matchConditionValue(userValue, conditionValue) {
44
+ // Operator object: { "$gte": 18, "$lt": 65 }
45
+ if (conditionValue !== null && typeof conditionValue === 'object' && !Array.isArray(conditionValue)) {
46
+ const ops = conditionValue;
47
+ const keys = Object.keys(ops);
48
+ // Only treat as operator object if all keys start with $
49
+ if (keys.length > 0 && keys.every(k => k.startsWith('$'))) {
50
+ return keys.every(op => {
51
+ const operand = ops[op];
52
+ switch (op) {
53
+ case '$eq':
54
+ return userValue === operand;
55
+ case '$ne':
56
+ return userValue !== operand;
57
+ case '$gt':
58
+ return typeof userValue === 'number' && typeof operand === 'number' && userValue > operand;
59
+ case '$gte':
60
+ return typeof userValue === 'number' && typeof operand === 'number' && userValue >= operand;
61
+ case '$lt':
62
+ return typeof userValue === 'number' && typeof operand === 'number' && userValue < operand;
63
+ case '$lte':
64
+ return typeof userValue === 'number' && typeof operand === 'number' && userValue <= operand;
65
+ case '$in':
66
+ return Array.isArray(operand) && operand.includes(userValue);
67
+ case '$nin':
68
+ return Array.isArray(operand) && !operand.includes(userValue);
69
+ default:
70
+ return false; // Unknown operator = no match
71
+ }
72
+ });
73
+ }
74
+ }
75
+ // Exact match (string, number, boolean, null)
76
+ return userValue === conditionValue;
77
+ }
78
+ /**
79
+ * Checks whether a user's attributes satisfy ALL conditions in a rule.
80
+ * A rule matches only if every condition key matches the user's corresponding attribute.
81
+ */
82
+ export function matchesConditions(conditions, attributes) {
83
+ for (const [key, conditionValue] of Object.entries(conditions)) {
84
+ const userValue = attributes[key];
85
+ // If user doesn't have the attribute, the condition cannot match
86
+ if (userValue === undefined)
87
+ return false;
88
+ if (!matchConditionValue(userValue, conditionValue))
89
+ return false;
90
+ }
91
+ return true;
92
+ }
93
+ /**
94
+ * Evaluates targeting rules in priority order against user attributes.
95
+ * Returns the value of the first matching rule, or undefined if no rule matches.
96
+ *
97
+ * Rules must be sorted by priority ASC (lowest number = highest priority).
98
+ */
99
+ export function evaluateRules(rules, attributes) {
100
+ for (const rule of rules) {
101
+ if (matchesConditions(rule.conditions, attributes)) {
102
+ return rule.value;
103
+ }
104
+ }
105
+ return undefined;
106
+ }
107
+ export function evaluateFlags(rules, context) {
108
+ const sortedRules = [...rules].sort((a, b) => a.priority - b.priority);
109
+ return evaluateRules(sortedRules, context.attributes || {});
110
+ }
111
+ /**
112
+ * Evaluates a complete FlagDefinition against an EvaluationContext.
113
+ * Order:
114
+ * 1. If !flag.enabled -> return flag.defaultValue
115
+ * 2. Check targeting rules in priority order. If match -> return rule.value
116
+ * 3. Check rollout bucketing: if user is in rollout -> return true (or active value); otherwise flag.defaultValue
117
+ */
118
+ export function evaluateFlag(flag, context) {
119
+ if (!flag.enabled) {
120
+ return flag.defaultValue;
121
+ }
122
+ if (flag.rules && flag.rules.length > 0) {
123
+ const sortedRules = [...flag.rules].sort((a, b) => a.priority - b.priority);
124
+ const ruleValue = evaluateRules(sortedRules, context.attributes || {});
125
+ if (ruleValue !== undefined) {
126
+ return ruleValue;
127
+ }
128
+ }
129
+ const inRollout = isUserInRollout(flag.key, context.userId, flag.rolloutPct);
130
+ if (!inRollout) {
131
+ return flag.defaultValue;
132
+ }
133
+ return typeof flag.defaultValue === 'boolean' ? true : flag.defaultValue;
134
+ }
135
+ //# sourceMappingURL=evaluator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"evaluator.js","sourceRoot":"","sources":["../src/evaluator.ts"],"names":[],"mappings":"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"}
package/dist/hash.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ export declare function hashBucket(flagKey: string, userId: string): number;
2
+ //# sourceMappingURL=hash.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hash.d.ts","sourceRoot":"","sources":["../src/hash.ts"],"names":[],"mappings":"AAUA,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAIlE"}
package/dist/hash.js ADDED
@@ -0,0 +1,14 @@
1
+ function fnv1aHash(str) {
2
+ let hash = 0x811c9dc5;
3
+ for (let i = 0; i < str.length; i++) {
4
+ hash ^= str.charCodeAt(i);
5
+ hash += (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24);
6
+ }
7
+ return hash >>> 0;
8
+ }
9
+ export function hashBucket(flagKey, userId) {
10
+ const combined = `${flagKey}:${userId}`;
11
+ const hashValue = fnv1aHash(combined);
12
+ return hashValue % 100;
13
+ }
14
+ //# sourceMappingURL=hash.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hash.js","sourceRoot":"","sources":["../src/hash.ts"],"names":[],"mappings":"AAAA,SAAS,SAAS,CAAC,GAAW;IAC1B,IAAI,IAAI,GAAG,UAAU,CAAC;IACtB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,GAAG,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QAClC,IAAI,IAAI,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;QAC1B,IAAI,IAAI,CAAC,IAAI,IAAI,CAAC,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC,CAAC,GAAG,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC;IACjF,CAAC;IACD,OAAO,IAAI,KAAK,CAAC,CAAC;AACtB,CAAC;AAGD,MAAM,UAAU,UAAU,CAAC,OAAe,EAAE,MAAc;IACtD,MAAM,QAAQ,GAAG,GAAG,OAAO,IAAI,MAAM,EAAE,CAAC;IACxC,MAAM,SAAS,GAAG,SAAS,CAAC,QAAQ,CAAC,CAAC;IACtC,OAAO,SAAS,GAAG,GAAG,CAAC;AAC3B,CAAC"}
@@ -0,0 +1,6 @@
1
+ export { FeatureFlagClient, FeatureFlagClient as FlagCraft, FeatureFlagClient as FlagCraftSDK, FeatureFlagClient as VanguardSDK } from './client.js';
2
+ export { InMemoryCache } from './cache.js';
3
+ export { Poller } from './poller.js';
4
+ export { hashBucket, isUserInRollout, evaluateRules, matchesConditions, evaluateFlag, evaluateFlags } from './evaluator.js';
5
+ export * from './types.js';
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,IAAI,SAAS,EAAE,iBAAiB,IAAI,YAAY,EAAE,iBAAiB,IAAI,WAAW,EAAE,MAAM,aAAa,CAAC;AACrJ,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAC3C,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,aAAa,EAAE,iBAAiB,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAC5H,cAAc,YAAY,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ export { FeatureFlagClient, FeatureFlagClient as FlagCraft, FeatureFlagClient as FlagCraftSDK, FeatureFlagClient as VanguardSDK } from './client.js';
2
+ export { InMemoryCache } from './cache.js';
3
+ export { Poller } from './poller.js';
4
+ export { hashBucket, isUserInRollout, evaluateRules, matchesConditions, evaluateFlag, evaluateFlags } from './evaluator.js';
5
+ export * from './types.js';
6
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,IAAI,SAAS,EAAE,iBAAiB,IAAI,YAAY,EAAE,iBAAiB,IAAI,WAAW,EAAE,MAAM,aAAa,CAAC;AACrJ,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAC3C,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,aAAa,EAAE,iBAAiB,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAC5H,cAAc,YAAY,CAAC"}
@@ -0,0 +1,43 @@
1
+ import { Ruleset } from './types.js';
2
+ export interface PollerConfig {
3
+ baseUrl: string;
4
+ apiKey: string;
5
+ interval: number;
6
+ onUpdate: (ruleset: Ruleset) => void;
7
+ onError: (error: Error) => void;
8
+ onFetchSuccess?: () => void;
9
+ onFetchError?: () => void;
10
+ }
11
+ /**
12
+ * Conditional polling loop for fetching rulesets using ETag / 304 Not Modified.
13
+ * Uses the native `fetch` API.
14
+ */
15
+ export declare class Poller {
16
+ private config;
17
+ private timer;
18
+ private currentEtag;
19
+ private isPolling;
20
+ private failureCount;
21
+ private _lastError;
22
+ constructor(config: PollerConfig);
23
+ get lastError(): Error | null;
24
+ /**
25
+ * Begins the periodic polling loop.
26
+ */
27
+ start(): void;
28
+ /**
29
+ * Stops the polling loop.
30
+ */
31
+ stop(): void;
32
+ private scheduleNext;
33
+ /**
34
+ * Fetches the latest ruleset from the API server using conditional polling (`If-None-Match`).
35
+ * - 200: Parses ruleset, updates current ETag, and calls `onUpdate`.
36
+ * - 304: No-op.
37
+ * - Error: Catches error, calls `onError`, and never throws to ensure fail-open resilience.
38
+ */
39
+ fetchRuleset(etag?: string | null): Promise<void>;
40
+ onFetchSuccess(): void;
41
+ onFetchError(): void;
42
+ }
43
+ //# sourceMappingURL=poller.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"poller.d.ts","sourceRoot":"","sources":["../src/poller.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAErC,MAAM,WAAW,YAAY;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,CAAC;IACrC,OAAO,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;IAChC,cAAc,CAAC,EAAE,MAAM,IAAI,CAAC;IAC5B,YAAY,CAAC,EAAE,MAAM,IAAI,CAAC;CAC7B;AAED;;;GAGG;AACH,qBAAa,MAAM;IAOH,OAAO,CAAC,MAAM;IAN1B,OAAO,CAAC,KAAK,CAA8C;IAC3D,OAAO,CAAC,WAAW,CAAuB;IAC1C,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,YAAY,CAAK;IACzB,OAAO,CAAC,UAAU,CAAsB;gBAEpB,MAAM,EAAE,YAAY;IAExC,IAAW,SAAS,IAAI,KAAK,GAAG,IAAI,CAEnC;IAED;;OAEG;IACI,KAAK,IAAI,IAAI;IAMpB;;OAEG;IACI,IAAI,IAAI,IAAI;IAQnB,OAAO,CAAC,YAAY;IAYpB;;;;;OAKG;IACU,YAAY,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAsC9D,cAAc;IAKd,YAAY;CAIf"}
package/dist/poller.js ADDED
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Conditional polling loop for fetching rulesets using ETag / 304 Not Modified.
3
+ * Uses the native `fetch` API.
4
+ */
5
+ export class Poller {
6
+ config;
7
+ timer = null;
8
+ currentEtag = null;
9
+ isPolling = false;
10
+ failureCount = 0;
11
+ _lastError = null;
12
+ constructor(config) {
13
+ this.config = config;
14
+ }
15
+ get lastError() {
16
+ return this._lastError;
17
+ }
18
+ /**
19
+ * Begins the periodic polling loop.
20
+ */
21
+ start() {
22
+ if (this.isPolling)
23
+ return;
24
+ this.isPolling = true;
25
+ this.scheduleNext(this.config.interval);
26
+ }
27
+ /**
28
+ * Stops the polling loop.
29
+ */
30
+ stop() {
31
+ if (this.timer) {
32
+ clearTimeout(this.timer);
33
+ this.timer = null;
34
+ }
35
+ this.isPolling = false;
36
+ }
37
+ scheduleNext(delay) {
38
+ if (!this.isPolling)
39
+ return;
40
+ this.timer = setTimeout(async () => {
41
+ await this.fetchRuleset(this.currentEtag);
42
+ if (this.isPolling) {
43
+ const nextDelay = this.failureCount === 0 ? this.config.interval : (Math.min(this.config.interval * Math.pow(2, this.failureCount), 300000));
44
+ this.scheduleNext(nextDelay);
45
+ }
46
+ }, delay);
47
+ }
48
+ /**
49
+ * Fetches the latest ruleset from the API server using conditional polling (`If-None-Match`).
50
+ * - 200: Parses ruleset, updates current ETag, and calls `onUpdate`.
51
+ * - 304: No-op.
52
+ * - Error: Catches error, calls `onError`, and never throws to ensure fail-open resilience.
53
+ */
54
+ async fetchRuleset(etag) {
55
+ try {
56
+ const headers = {
57
+ 'X-API-Key': this.config.apiKey,
58
+ 'Accept': 'application/json'
59
+ };
60
+ const targetEtag = etag !== undefined ? etag : this.currentEtag;
61
+ if (targetEtag) {
62
+ headers['If-None-Match'] = targetEtag;
63
+ }
64
+ const url = `${this.config.baseUrl.replace(/\/$/, '')}/sdk/flags`;
65
+ const response = await fetch(url, {
66
+ method: 'GET',
67
+ headers
68
+ });
69
+ if (response.status === 304) {
70
+ this.onFetchSuccess();
71
+ return;
72
+ }
73
+ if (!response.ok) {
74
+ throw new Error(`Failed to fetch flags: HTTP ${response.status} ${response.statusText}`);
75
+ }
76
+ const data = (await response.json());
77
+ this.onFetchSuccess();
78
+ this.currentEtag = data.etag;
79
+ this.config.onUpdate(data);
80
+ }
81
+ catch (error) {
82
+ const err = error instanceof Error ? error : new Error(String(error));
83
+ this._lastError = err;
84
+ this.onFetchError();
85
+ this.config.onError(err);
86
+ }
87
+ }
88
+ onFetchSuccess() {
89
+ this.failureCount = 0;
90
+ this._lastError = null;
91
+ this.config.onFetchSuccess?.();
92
+ }
93
+ onFetchError() {
94
+ this.failureCount++;
95
+ this.config.onFetchError?.();
96
+ }
97
+ }
98
+ //# sourceMappingURL=poller.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"poller.js","sourceRoot":"","sources":["../src/poller.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,29 @@
1
+ export interface FeatureFlagConfig {
2
+ apiKey: string;
3
+ baseUrl?: string;
4
+ pollingInterval?: number;
5
+ defaultValues?: Record<string, unknown>;
6
+ onError?: (error: Error) => void;
7
+ onConnectionStatusChange?: (status: 'connected' | 'degraded' | 'disconnected') => void;
8
+ }
9
+ export interface FlagRule {
10
+ priority: number;
11
+ conditions: Record<string, unknown>;
12
+ value: unknown;
13
+ }
14
+ export interface FlagDefinition {
15
+ key: string;
16
+ enabled: boolean;
17
+ rolloutPct: number;
18
+ defaultValue: unknown;
19
+ rules: FlagRule[];
20
+ }
21
+ export interface EvaluationContext {
22
+ userId: string;
23
+ attributes?: Record<string, unknown>;
24
+ }
25
+ export interface Ruleset {
26
+ flags: FlagDefinition[];
27
+ etag: string;
28
+ }
29
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,iBAAiB;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACxC,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;IACjC,wBAAwB,CAAC,EAAE,CAAC,MAAM,EAAE,WAAW,GAAG,UAAU,GAAG,cAAc,KAAK,IAAI,CAAC;CAC1F;AAED,MAAM,WAAW,QAAQ;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACpC,KAAK,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,cAAc;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,OAAO,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,OAAO,CAAC;IACtB,KAAK,EAAE,QAAQ,EAAE,CAAC;CACrB;AAED,MAAM,WAAW,iBAAiB;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACxC;AAED,MAAM,WAAW,OAAO;IACpB,KAAK,EAAE,cAAc,EAAE,CAAC;IACxB,IAAI,EAAE,MAAM,CAAC;CAChB"}
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "flagcraft",
3
+ "version": "0.1.0",
4
+ "description": "Universal, fail-open feature flag evaluation SDK with ETag conditional caching and auto-rollback support.",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "files": [
9
+ "dist"
10
+ ],
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "import": "./dist/index.js"
15
+ },
16
+ "./*": {
17
+ "types": "./dist/*.d.ts",
18
+ "import": "./dist/*.js"
19
+ }
20
+ },
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "https://github.com/Akash-1808/feature-flag.git"
24
+ },
25
+ "keywords": [
26
+ "feature-flags",
27
+ "sdk",
28
+ "vanguard",
29
+ "ab-testing",
30
+ "rollout",
31
+ "etag-caching"
32
+ ],
33
+ "author": "Vanguard Team",
34
+ "license": "MIT",
35
+ "scripts": {
36
+ "build": "tsc",
37
+ "test": "vitest run"
38
+ }
39
+ }