@featureflip/js 1.0.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.
- package/README.md +118 -0
- package/dist/browser.cjs +1 -0
- package/dist/browser.d.ts +117 -0
- package/dist/browser.mjs +112 -0
- package/dist/client-Bf3tS17L.cjs +1 -0
- package/dist/client-ChXPtrh5.js +406 -0
- package/dist/node.cjs +1 -0
- package/dist/node.d.ts +117 -0
- package/dist/node.mjs +37 -0
- package/package.json +48 -0
package/README.md
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# @featureflip/sdk
|
|
2
|
+
|
|
3
|
+
Server-side JavaScript/TypeScript SDK for [Featureflip](https://github.com/featureflip/featureflip) - evaluate feature flags locally with near-zero latency.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @featureflip/sdk
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { FeatureflipClient } from '@featureflip/sdk';
|
|
15
|
+
|
|
16
|
+
const client = new FeatureflipClient({
|
|
17
|
+
sdkKey: 'your-sdk-key',
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
await client.waitForInitialization();
|
|
21
|
+
|
|
22
|
+
const enabled = client.boolVariation('my-feature', { user_id: '123' }, false);
|
|
23
|
+
|
|
24
|
+
if (enabled) {
|
|
25
|
+
console.log('Feature is enabled!');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
await client.close();
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Configuration
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
const client = new FeatureflipClient({
|
|
35
|
+
sdkKey: 'your-sdk-key',
|
|
36
|
+
baseUrl: 'https://eval.featureflip.io', // Evaluation API URL (default)
|
|
37
|
+
streaming: true, // Use SSE for real-time updates (default)
|
|
38
|
+
pollInterval: 30000, // Polling interval in ms if streaming=false
|
|
39
|
+
flushInterval: 30000, // Event flush interval in ms
|
|
40
|
+
flushBatchSize: 100, // Events per batch
|
|
41
|
+
initTimeout: 10000, // Max ms to wait for initialization
|
|
42
|
+
maxStreamRetries: 5, // SSE retries before falling back to polling
|
|
43
|
+
});
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
The SDK key can also be set via the `FEATUREFLIP_SDK_KEY` environment variable.
|
|
47
|
+
|
|
48
|
+
## Evaluation
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
const context = { user_id: '123', email: 'user@example.com' };
|
|
52
|
+
|
|
53
|
+
// Boolean flag
|
|
54
|
+
const enabled = client.boolVariation('feature-key', context, false);
|
|
55
|
+
|
|
56
|
+
// String flag
|
|
57
|
+
const tier = client.stringVariation('pricing-tier', context, 'free');
|
|
58
|
+
|
|
59
|
+
// Number flag
|
|
60
|
+
const limit = client.numberVariation('rate-limit', context, 100);
|
|
61
|
+
|
|
62
|
+
// JSON flag
|
|
63
|
+
const config = client.jsonVariation('ui-config', context, { theme: 'light' });
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Detailed Evaluation
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
const detail = client.variationDetail('feature-key', { user_id: '123' }, false);
|
|
70
|
+
|
|
71
|
+
console.log(detail.value); // The evaluated value
|
|
72
|
+
console.log(detail.reason); // "RuleMatch", "Fallthrough", "FlagDisabled", etc.
|
|
73
|
+
console.log(detail.ruleId); // Rule ID if reason is "RuleMatch"
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Event Tracking
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
// Track custom events
|
|
80
|
+
client.track('checkout-completed', { user_id: '123' }, { total: 99.99 });
|
|
81
|
+
|
|
82
|
+
// Identify users for segment building
|
|
83
|
+
client.identify({ user_id: '123', email: 'user@example.com', plan: 'pro' });
|
|
84
|
+
|
|
85
|
+
// Force flush pending events
|
|
86
|
+
await client.flush();
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Testing
|
|
90
|
+
|
|
91
|
+
Use `forTesting()` to create a client with predetermined flag values -- no network calls.
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
const client = FeatureflipClient.forTesting({
|
|
95
|
+
'my-feature': true,
|
|
96
|
+
'pricing-tier': 'pro',
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
client.boolVariation('my-feature', {}, false); // true
|
|
100
|
+
client.stringVariation('pricing-tier', {}, 'free'); // 'pro'
|
|
101
|
+
client.boolVariation('unknown', {}, false); // false (default)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Features
|
|
105
|
+
|
|
106
|
+
- **Local evaluation** - Near-zero latency after initialization
|
|
107
|
+
- **Real-time updates** - SSE streaming with automatic polling fallback
|
|
108
|
+
- **Event tracking** - Automatic batching and background flushing
|
|
109
|
+
- **Test support** - `forTesting()` factory for deterministic unit tests
|
|
110
|
+
- **TypeScript** - Full type definitions included
|
|
111
|
+
|
|
112
|
+
## Requirements
|
|
113
|
+
|
|
114
|
+
- Node.js 18+
|
|
115
|
+
|
|
116
|
+
## License
|
|
117
|
+
|
|
118
|
+
MIT
|
package/dist/browser.cjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require(`./client-Bf3tS17L.cjs`);function t(){return{md5(e){return i(e)},createEventSource(e,t){let n=new EventSource(e);return{addEventListener:(e,t)=>{n.addEventListener(e,e=>t(e))},close:()=>n.close(),get readyState(){return n.readyState}}},async fetch(e,t){return globalThis.fetch(e,t)}}}var n=[7,12,17,22,7,12,17,22,7,12,17,22,7,12,17,22,5,9,14,20,5,9,14,20,5,9,14,20,5,9,14,20,4,11,16,23,4,11,16,23,4,11,16,23,4,11,16,23,6,10,15,21,6,10,15,21,6,10,15,21,6,10,15,21],r=new Uint32Array(64);for(let e=0;e<64;e++)r[e]=Math.floor(2**32*Math.abs(Math.sin(e+1)))>>>0;function i(e){let t=new TextEncoder().encode(e),i=t.length*8,a=(56-(t.length+1)%64+64)%64,o=new Uint8Array(t.length+1+a+8);o.set(t),o[t.length]=128;let s=new DataView(o.buffer);s.setUint32(o.length-8,i>>>0,!0),s.setUint32(o.length-4,0,!0);let c=1732584193,l=4023233417,u=2562383102,d=271733878;for(let e=0;e<o.length;e+=64){let t=new Uint32Array(16);for(let n=0;n<16;n++)t[n]=s.getUint32(e+n*4,!0);let i=c,a=l,o=u,f=d;for(let e=0;e<64;e++){let s,c;e<16?(s=a&o|~a&f,c=e):e<32?(s=f&a|~f&o,c=(5*e+1)%16):e<48?(s=a^o^f,c=(3*e+5)%16):(s=o^(a|~f),c=7*e%16),s=s+i+r[e]+t[c]>>>0,i=f,f=o,o=a,a=a+(s<<n[e]|s>>>32-n[e])>>>0}c=c+i>>>0,l=l+a>>>0,u=u+o>>>0,d=d+f>>>0}let f=new Uint8Array(16),p=new DataView(f.buffer);return p.setUint32(0,c,!0),p.setUint32(4,l,!0),p.setUint32(8,u,!0),p.setUint32(12,d,!0),f}exports.FeatureflipClient=e.t,exports.createBrowserPlatform=t;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
export declare function createBrowserPlatform(): Platform;
|
|
2
|
+
|
|
3
|
+
export declare type EvaluationContext = Record<string, unknown>;
|
|
4
|
+
|
|
5
|
+
export declare interface EvaluationDetail<T = unknown> {
|
|
6
|
+
value: T;
|
|
7
|
+
variationKey?: string;
|
|
8
|
+
reason: EvaluationReason;
|
|
9
|
+
ruleId?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export declare type EvaluationReason = 'RuleMatch' | 'Fallthrough' | 'FlagDisabled' | 'FlagNotFound' | 'Error';
|
|
13
|
+
|
|
14
|
+
declare interface EventSourceLike {
|
|
15
|
+
addEventListener(type: string, listener: (event: {
|
|
16
|
+
data: string;
|
|
17
|
+
}) => void): void;
|
|
18
|
+
close(): void;
|
|
19
|
+
readonly readyState: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export declare class FeatureflipClient {
|
|
23
|
+
private readonly config;
|
|
24
|
+
private readonly store;
|
|
25
|
+
private readonly events;
|
|
26
|
+
private readonly platform;
|
|
27
|
+
private initialized;
|
|
28
|
+
private initPromise;
|
|
29
|
+
private eventSource;
|
|
30
|
+
private pollTimer;
|
|
31
|
+
private closed;
|
|
32
|
+
private streamRetryCount;
|
|
33
|
+
private streamRetryTimer;
|
|
34
|
+
constructor(config: FeatureflipConfig, platform: Platform);
|
|
35
|
+
/** Whether the client has successfully loaded initial flag data. */
|
|
36
|
+
get isInitialized(): boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Wait for the client to finish initialization.
|
|
39
|
+
* Rejects after initTimeout if initial flag fetch fails.
|
|
40
|
+
*/
|
|
41
|
+
waitForInitialization(): Promise<void>;
|
|
42
|
+
/**
|
|
43
|
+
* Evaluate a boolean flag.
|
|
44
|
+
* Returns defaultValue if flag not found or evaluation fails.
|
|
45
|
+
*/
|
|
46
|
+
boolVariation(key: string, context: EvaluationContext, defaultValue: boolean): boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Evaluate a string flag.
|
|
49
|
+
*/
|
|
50
|
+
stringVariation(key: string, context: EvaluationContext, defaultValue: string): string;
|
|
51
|
+
/**
|
|
52
|
+
* Evaluate a number flag.
|
|
53
|
+
*/
|
|
54
|
+
numberVariation(key: string, context: EvaluationContext, defaultValue: number): number;
|
|
55
|
+
/**
|
|
56
|
+
* Evaluate a JSON flag.
|
|
57
|
+
*/
|
|
58
|
+
jsonVariation<T>(key: string, context: EvaluationContext, defaultValue: T): T;
|
|
59
|
+
/**
|
|
60
|
+
* Evaluate a flag and return the full detail including reason.
|
|
61
|
+
*/
|
|
62
|
+
variationDetail<T>(key: string, context: EvaluationContext, defaultValue: T): EvaluationDetail<T>;
|
|
63
|
+
/**
|
|
64
|
+
* Track a custom event.
|
|
65
|
+
*/
|
|
66
|
+
track(eventKey: string, context: EvaluationContext, metadata?: Record<string, unknown>): void;
|
|
67
|
+
/**
|
|
68
|
+
* Send an identify event for the given context.
|
|
69
|
+
*/
|
|
70
|
+
identify(context: EvaluationContext): void;
|
|
71
|
+
/**
|
|
72
|
+
* Flush any pending events immediately.
|
|
73
|
+
*/
|
|
74
|
+
flush(): Promise<void>;
|
|
75
|
+
/**
|
|
76
|
+
* Close the client, flushing pending events and stopping all connections.
|
|
77
|
+
*/
|
|
78
|
+
close(): Promise<void>;
|
|
79
|
+
/**
|
|
80
|
+
* Create a test client with hardcoded flag values. No network calls.
|
|
81
|
+
*/
|
|
82
|
+
static forTesting(flags: Record<string, unknown>): FeatureflipClient;
|
|
83
|
+
private evaluateFlag;
|
|
84
|
+
private initialize;
|
|
85
|
+
private fetchFlags;
|
|
86
|
+
private startDataSource;
|
|
87
|
+
private startStreaming;
|
|
88
|
+
private startPolling;
|
|
89
|
+
private fetchSingleFlag;
|
|
90
|
+
private recordEvaluation;
|
|
91
|
+
private headers;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export declare interface FeatureflipConfig {
|
|
95
|
+
sdkKey: string;
|
|
96
|
+
baseUrl: string;
|
|
97
|
+
streaming?: boolean;
|
|
98
|
+
pollInterval?: number;
|
|
99
|
+
flushInterval?: number;
|
|
100
|
+
flushBatchSize?: number;
|
|
101
|
+
initTimeout?: number;
|
|
102
|
+
maxStreamRetries?: number;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export declare type FlagType = 'Boolean' | 'String' | 'Number' | 'Json';
|
|
106
|
+
|
|
107
|
+
export declare interface Platform {
|
|
108
|
+
md5(input: string): Uint8Array;
|
|
109
|
+
createEventSource(url: string, headers: Record<string, string>): EventSourceLike;
|
|
110
|
+
fetch(url: string, init?: RequestInit): Promise<Response>;
|
|
111
|
+
/** Extra headers the platform can inject (e.g. User-Agent on Node). */
|
|
112
|
+
readonly extraHeaders?: Record<string, string>;
|
|
113
|
+
/** Whether the platform's EventSource implementation supports custom headers. */
|
|
114
|
+
readonly sseSupportsHeaders?: boolean;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export { }
|
package/dist/browser.mjs
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { t as e } from "./client-ChXPtrh5.js";
|
|
2
|
+
//#region src/platform/browser.ts
|
|
3
|
+
function t() {
|
|
4
|
+
return {
|
|
5
|
+
md5(e) {
|
|
6
|
+
return i(e);
|
|
7
|
+
},
|
|
8
|
+
createEventSource(e, t) {
|
|
9
|
+
let n = new EventSource(e);
|
|
10
|
+
return {
|
|
11
|
+
addEventListener: (e, t) => {
|
|
12
|
+
n.addEventListener(e, (e) => t(e));
|
|
13
|
+
},
|
|
14
|
+
close: () => n.close(),
|
|
15
|
+
get readyState() {
|
|
16
|
+
return n.readyState;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
},
|
|
20
|
+
async fetch(e, t) {
|
|
21
|
+
return globalThis.fetch(e, t);
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
var n = [
|
|
26
|
+
7,
|
|
27
|
+
12,
|
|
28
|
+
17,
|
|
29
|
+
22,
|
|
30
|
+
7,
|
|
31
|
+
12,
|
|
32
|
+
17,
|
|
33
|
+
22,
|
|
34
|
+
7,
|
|
35
|
+
12,
|
|
36
|
+
17,
|
|
37
|
+
22,
|
|
38
|
+
7,
|
|
39
|
+
12,
|
|
40
|
+
17,
|
|
41
|
+
22,
|
|
42
|
+
5,
|
|
43
|
+
9,
|
|
44
|
+
14,
|
|
45
|
+
20,
|
|
46
|
+
5,
|
|
47
|
+
9,
|
|
48
|
+
14,
|
|
49
|
+
20,
|
|
50
|
+
5,
|
|
51
|
+
9,
|
|
52
|
+
14,
|
|
53
|
+
20,
|
|
54
|
+
5,
|
|
55
|
+
9,
|
|
56
|
+
14,
|
|
57
|
+
20,
|
|
58
|
+
4,
|
|
59
|
+
11,
|
|
60
|
+
16,
|
|
61
|
+
23,
|
|
62
|
+
4,
|
|
63
|
+
11,
|
|
64
|
+
16,
|
|
65
|
+
23,
|
|
66
|
+
4,
|
|
67
|
+
11,
|
|
68
|
+
16,
|
|
69
|
+
23,
|
|
70
|
+
4,
|
|
71
|
+
11,
|
|
72
|
+
16,
|
|
73
|
+
23,
|
|
74
|
+
6,
|
|
75
|
+
10,
|
|
76
|
+
15,
|
|
77
|
+
21,
|
|
78
|
+
6,
|
|
79
|
+
10,
|
|
80
|
+
15,
|
|
81
|
+
21,
|
|
82
|
+
6,
|
|
83
|
+
10,
|
|
84
|
+
15,
|
|
85
|
+
21,
|
|
86
|
+
6,
|
|
87
|
+
10,
|
|
88
|
+
15,
|
|
89
|
+
21
|
|
90
|
+
], r = new Uint32Array(64);
|
|
91
|
+
for (let e = 0; e < 64; e++) r[e] = Math.floor(2 ** 32 * Math.abs(Math.sin(e + 1))) >>> 0;
|
|
92
|
+
function i(e) {
|
|
93
|
+
let t = new TextEncoder().encode(e), i = t.length * 8, a = (56 - (t.length + 1) % 64 + 64) % 64, o = new Uint8Array(t.length + 1 + a + 8);
|
|
94
|
+
o.set(t), o[t.length] = 128;
|
|
95
|
+
let s = new DataView(o.buffer);
|
|
96
|
+
s.setUint32(o.length - 8, i >>> 0, !0), s.setUint32(o.length - 4, 0, !0);
|
|
97
|
+
let c = 1732584193, l = 4023233417, u = 2562383102, d = 271733878;
|
|
98
|
+
for (let e = 0; e < o.length; e += 64) {
|
|
99
|
+
let t = new Uint32Array(16);
|
|
100
|
+
for (let n = 0; n < 16; n++) t[n] = s.getUint32(e + n * 4, !0);
|
|
101
|
+
let i = c, a = l, o = u, f = d;
|
|
102
|
+
for (let e = 0; e < 64; e++) {
|
|
103
|
+
let s, c;
|
|
104
|
+
e < 16 ? (s = a & o | ~a & f, c = e) : e < 32 ? (s = f & a | ~f & o, c = (5 * e + 1) % 16) : e < 48 ? (s = a ^ o ^ f, c = (3 * e + 5) % 16) : (s = o ^ (a | ~f), c = 7 * e % 16), s = s + i + r[e] + t[c] >>> 0, i = f, f = o, o = a, a = a + (s << n[e] | s >>> 32 - n[e]) >>> 0;
|
|
105
|
+
}
|
|
106
|
+
c = c + i >>> 0, l = l + a >>> 0, u = u + o >>> 0, d = d + f >>> 0;
|
|
107
|
+
}
|
|
108
|
+
let f = new Uint8Array(16), p = new DataView(f.buffer);
|
|
109
|
+
return p.setUint32(0, c, !0), p.setUint32(4, l, !0), p.setUint32(8, u, !0), p.setUint32(12, d, !0), f;
|
|
110
|
+
}
|
|
111
|
+
//#endregion
|
|
112
|
+
export { e as FeatureflipClient, t as createBrowserPlatform };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var e={streaming:!0,pollInterval:3e4,flushInterval:3e4,flushBatchSize:100,initTimeout:1e4,maxStreamRetries:5};function t(t){if(!t.sdkKey)throw Error(`sdkKey is required`);if(!t.baseUrl)throw Error(`baseUrl is required`);let n=t.baseUrl.replace(/\/+$/,``);return{sdkKey:t.sdkKey,baseUrl:n,streaming:t.streaming??e.streaming,pollInterval:t.pollInterval??e.pollInterval,flushInterval:t.flushInterval??e.flushInterval,flushBatchSize:t.flushBatchSize??e.flushBatchSize,initTimeout:t.initTimeout??e.initTimeout,maxStreamRetries:t.maxStreamRetries??e.maxStreamRetries}}var n=class{flags=new Map;segments=new Map;listeners=[];version=0;getFlag(e){return this.flags.get(e)}getSegment(e){return this.segments.get(e)}getAllFlags(){return Array.from(this.flags.values())}getVersion(){return this.version}init(e,t,n){this.flags.clear(),this.segments.clear();for(let t of e)this.flags.set(t.key,t);for(let e of t)this.segments.set(e.key,e);this.version=n;for(let t of e)this.notifyListeners(t.key)}upsert(e){let t=this.flags.get(e.key);t&&t.version>=e.version||(this.flags.set(e.key,e),this.notifyListeners(e.key))}delete(e){this.flags.delete(e)&&this.notifyListeners(e)}onChange(e){return this.listeners.push(e),()=>{let t=this.listeners.indexOf(e);t>=0&&this.listeners.splice(t,1)}}notifyListeners(e){for(let t of this.listeners)try{t(e)}catch{}}};function r(e,t,n){let r=n(`${e}:${t}`);return((r[0]|r[1]<<8|r[2]<<16|r[3]<<24>>>0)>>>0)%100}function i(e,t){let n=e[t];if(n!==void 0)return n;if(t===`userId`)return e.user_id;if(t===`user_id`)return e.userId}function a(e,t,n){switch(e){case`Equals`:return n.some(e=>t===e);case`NotEquals`:return n.every(e=>t!==e);case`Contains`:return n.some(e=>t.includes(e));case`NotContains`:return n.every(e=>!t.includes(e));case`StartsWith`:return n.some(e=>t.startsWith(e));case`EndsWith`:return n.some(e=>t.endsWith(e));case`In`:return n.includes(t);case`NotIn`:return!n.includes(t);case`MatchesRegex`:return n.some(e=>{try{return new RegExp(e,`i`).test(t)}catch{return!1}});case`GreaterThan`:return o(t,n[0],`>`);case`GreaterThanOrEqual`:return o(t,n[0],`>=`);case`LessThan`:return o(t,n[0],`<`);case`LessThanOrEqual`:return o(t,n[0],`<=`);case`Before`:return t<n[0];case`After`:return t>n[0];default:return!1}}function o(e,t,n){let r=parseFloat(e),i=parseFloat(t);if(isNaN(r)||isNaN(i))return!1;switch(n){case`>`:return r>i;case`<`:return r<i;case`>=`:return r>=i;case`<=`:return r<=i}}function s(e,t){let n=i(t,e.attribute);if(n==null)return e.negate;let r=String(n).toLowerCase(),o=e.values.map(e=>e.toLowerCase()),s=a(e.operator,r,o);return e.negate?!s:s}function c(e,t,n){return e.length===0?!0:t===`And`?e.every(e=>s(e,n)):e.some(e=>s(e,n))}function l(e,t){return e.length===0?!0:e.every(e=>c(e.conditions,e.operator,t))}function u(e,t,n){if(e.type===`Fixed`)return e.variation??``;let a=i(t,e.bucketBy??`userId`),o=a==null?``:String(a),s=r(e.salt??``,o,n),c=0;for(let t of e.variations??[])if(c+=t.weight,s<c)return t.key;let l=e.variations??[];return l.length>0?l[l.length-1].key:``}function d(e,t,n){if(!e.enabled)return{value:e.variations.find(t=>t.key===e.offVariation)?.value??null,variationKey:e.offVariation,reason:`FlagDisabled`};let r=[...e.rules].sort((e,t)=>e.priority-t.priority);for(let i of r){let r;if(i.segmentKey&&n.getSegment){let e=n.getSegment(i.segmentKey);r=e?c(e.conditions,e.conditionLogic,t):!1}else r=l(i.conditionGroups,t);if(r){let r=u(i.serve,t,n.md5);return{value:e.variations.find(e=>e.key===r)?.value??null,variationKey:r,reason:`RuleMatch`,ruleId:i.id}}}let i=u(e.fallthrough,t,n.md5);return{value:e.variations.find(e=>e.key===i)?.value??null,variationKey:i,reason:`Fallthrough`}}var f=class{queue=[];flushTimer=null;closed=!1;flushPromise=null;constructor(e,t,n){this.sender=e,this.flushInterval=t,this.flushBatchSize=n}start(){this.flushTimer||=setInterval(()=>{this.flush()},this.flushInterval)}enqueue(e){this.closed||(this.queue.push(e),this.queue.length>=this.flushBatchSize&&this.flush())}async flush(){if(this.queue.length!==0)return this.flushPromise||=(async()=>{for(;this.queue.length>0;){let e=this.queue.splice(0,this.flushBatchSize);try{await this.sender.sendEvents({events:e})}catch{}}})().finally(()=>{this.flushPromise=null}),this.flushPromise}async close(){for(this.closed=!0,this.flushTimer&&=(clearInterval(this.flushTimer),null);this.queue.length>0;)await this.flush()}},p=class e{config;store;events;platform;initialized=!1;initPromise=null;eventSource=null;pollTimer=null;closed=!1;streamRetryCount=0;streamRetryTimer=null;constructor(e,r){this.config=t(e),this.store=new n,this.platform=r,this.events=new f({sendEvents:async e=>{await this.platform.fetch(`${this.config.baseUrl}/v1/sdk/events`,{method:`POST`,headers:this.headers(),body:JSON.stringify(e)})}},this.config.flushInterval,this.config.flushBatchSize)}get isInitialized(){return this.initialized}async waitForInitialization(){if(!this.initialized)return this.initPromise||=this.initialize(),this.initPromise}boolVariation(e,t,n){return this.evaluateFlag(e,t,n)}stringVariation(e,t,n){return this.evaluateFlag(e,t,n)}numberVariation(e,t,n){return this.evaluateFlag(e,t,n)}jsonVariation(e,t,n){return this.evaluateFlag(e,t,n)}variationDetail(e,t,n){let r=this.store.getFlag(e);if(!r)return this.recordEvaluation(e,t,void 0),{value:n,reason:`FlagNotFound`};try{let i=d(r,t,{md5:e=>this.platform.md5(e),getSegment:e=>this.store.getSegment(e)}),a=i.value!==void 0&&i.value!==null?i.value:n;return this.recordEvaluation(e,t,i.variationKey),{value:a,reason:i.reason,ruleId:i.ruleId}}catch{return this.recordEvaluation(e,t,void 0),{value:n,reason:`Error`}}}track(e,t,n){let r=t.user_id==null?void 0:String(t.user_id);this.events.enqueue({type:`Custom`,flagKey:e,userId:r,timestamp:new Date().toISOString(),metadata:n})}identify(e){let t=e.user_id==null?void 0:String(e.user_id),{user_id:n,...r}=e;this.events.enqueue({type:`Identify`,flagKey:`$identify`,userId:t,timestamp:new Date().toISOString(),metadata:Object.keys(r).length>0?r:void 0})}async flush(){await this.events.flush()}async close(){this.closed=!0,this.eventSource?.close(),this.eventSource=null,this.streamRetryTimer&&=(clearTimeout(this.streamRetryTimer),null),this.pollTimer&&=(clearInterval(this.pollTimer),null),await this.events.close()}static forTesting(t){let n=Object.entries(t).map(([e,t])=>({key:e,version:1,type:typeof t==`boolean`?`Boolean`:typeof t==`number`?`Number`:typeof t==`string`?`String`:`Json`,enabled:!0,variations:[{key:`default`,value:t}],rules:[],fallthrough:{type:`Fixed`,variation:`default`},offVariation:`default`})),r=new e({sdkKey:`test-key`,baseUrl:`http://localhost`},{md5:()=>new Uint8Array(16),createEventSource:()=>({addEventListener:()=>{},close:()=>{},readyState:2}),fetch:async()=>new Response});return r.store.init(n,[],1),r.initialized=!0,r}evaluateFlag(e,t,n){return this.variationDetail(e,t,n).value}async initialize(){let e,t=new Promise((t,n)=>{e=setTimeout(()=>n(Error(`Initialization timed out`)),this.config.initTimeout)}),n=(async()=>{await this.fetchFlags(),this.initialized=!0,this.events.start(),this.startDataSource()})();try{await Promise.race([n,t])}finally{clearTimeout(e)}}async fetchFlags(){let e=await this.platform.fetch(`${this.config.baseUrl}/v1/sdk/flags`,{headers:this.headers()});if(!e.ok)throw Error(`Failed to fetch flags: ${e.status}`);let t=await e.json();this.store.init(t.flags,t.segments,t.version)}startDataSource(){this.closed||(this.config.streaming?this.startStreaming():this.startPolling())}startStreaming(){if(this.closed)return;let e=this.platform.sseSupportsHeaders?`${this.config.baseUrl}/v1/sdk/stream`:`${this.config.baseUrl}/v1/sdk/stream?authorization=${encodeURIComponent(this.config.sdkKey)}`;this.eventSource=this.platform.createEventSource(e,this.headers());for(let e of[`flag.created`,`flag.updated`])this.eventSource.addEventListener(e,e=>{try{let t=JSON.parse(e.data);t.key&&this.fetchSingleFlag(t.key)}catch{}});this.eventSource.addEventListener(`flag.deleted`,e=>{try{let t=JSON.parse(e.data);t.key&&this.store.delete(t.key)}catch{}}),this.eventSource.addEventListener(`segment.updated`,()=>{this.fetchFlags().catch(()=>{})}),this.eventSource.addEventListener(`open`,()=>{this.streamRetryCount=0}),this.eventSource.addEventListener(`error`,()=>{if(this.eventSource?.close(),this.eventSource=null,this.closed)return;if(this.streamRetryCount>=this.config.maxStreamRetries){console.warn(`[featureflip] SSE connection failed after ${this.config.maxStreamRetries} retries, falling back to polling`),this.startPolling();return}let e=Math.min(1e3*2**this.streamRetryCount,3e4);this.streamRetryCount++,this.streamRetryTimer=setTimeout(()=>{this.streamRetryTimer=null,this.startStreaming()},e)})}startPolling(){this.pollTimer=setInterval(()=>{this.fetchFlags().catch(()=>{})},this.config.pollInterval)}async fetchSingleFlag(e){try{let t=await this.platform.fetch(`${this.config.baseUrl}/v1/sdk/flags/${encodeURIComponent(e)}`,{headers:this.headers()});if(t.ok){let e=await t.json();this.store.upsert(e)}}catch{}}recordEvaluation(e,t,n){let r=t.user_id==null?void 0:String(t.user_id);this.events.enqueue({type:`Evaluation`,flagKey:e,userId:r,variation:n,timestamp:new Date().toISOString()})}headers(){return{Authorization:this.config.sdkKey,"Content-Type":`application/json`,...this.platform.extraHeaders}}};Object.defineProperty(exports,`t`,{enumerable:!0,get:function(){return p}});
|
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
//#region src/config.ts
|
|
2
|
+
var e = {
|
|
3
|
+
streaming: !0,
|
|
4
|
+
pollInterval: 3e4,
|
|
5
|
+
flushInterval: 3e4,
|
|
6
|
+
flushBatchSize: 100,
|
|
7
|
+
initTimeout: 1e4,
|
|
8
|
+
maxStreamRetries: 5
|
|
9
|
+
};
|
|
10
|
+
function t(t) {
|
|
11
|
+
if (!t.sdkKey) throw Error("sdkKey is required");
|
|
12
|
+
if (!t.baseUrl) throw Error("baseUrl is required");
|
|
13
|
+
let n = t.baseUrl.replace(/\/+$/, "");
|
|
14
|
+
return {
|
|
15
|
+
sdkKey: t.sdkKey,
|
|
16
|
+
baseUrl: n,
|
|
17
|
+
streaming: t.streaming ?? e.streaming,
|
|
18
|
+
pollInterval: t.pollInterval ?? e.pollInterval,
|
|
19
|
+
flushInterval: t.flushInterval ?? e.flushInterval,
|
|
20
|
+
flushBatchSize: t.flushBatchSize ?? e.flushBatchSize,
|
|
21
|
+
initTimeout: t.initTimeout ?? e.initTimeout,
|
|
22
|
+
maxStreamRetries: t.maxStreamRetries ?? e.maxStreamRetries
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
//#endregion
|
|
26
|
+
//#region src/core/store.ts
|
|
27
|
+
var n = class {
|
|
28
|
+
flags = /* @__PURE__ */ new Map();
|
|
29
|
+
segments = /* @__PURE__ */ new Map();
|
|
30
|
+
listeners = [];
|
|
31
|
+
version = 0;
|
|
32
|
+
getFlag(e) {
|
|
33
|
+
return this.flags.get(e);
|
|
34
|
+
}
|
|
35
|
+
getSegment(e) {
|
|
36
|
+
return this.segments.get(e);
|
|
37
|
+
}
|
|
38
|
+
getAllFlags() {
|
|
39
|
+
return Array.from(this.flags.values());
|
|
40
|
+
}
|
|
41
|
+
getVersion() {
|
|
42
|
+
return this.version;
|
|
43
|
+
}
|
|
44
|
+
init(e, t, n) {
|
|
45
|
+
this.flags.clear(), this.segments.clear();
|
|
46
|
+
for (let t of e) this.flags.set(t.key, t);
|
|
47
|
+
for (let e of t) this.segments.set(e.key, e);
|
|
48
|
+
this.version = n;
|
|
49
|
+
for (let t of e) this.notifyListeners(t.key);
|
|
50
|
+
}
|
|
51
|
+
upsert(e) {
|
|
52
|
+
let t = this.flags.get(e.key);
|
|
53
|
+
t && t.version >= e.version || (this.flags.set(e.key, e), this.notifyListeners(e.key));
|
|
54
|
+
}
|
|
55
|
+
delete(e) {
|
|
56
|
+
this.flags.delete(e) && this.notifyListeners(e);
|
|
57
|
+
}
|
|
58
|
+
onChange(e) {
|
|
59
|
+
return this.listeners.push(e), () => {
|
|
60
|
+
let t = this.listeners.indexOf(e);
|
|
61
|
+
t >= 0 && this.listeners.splice(t, 1);
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
notifyListeners(e) {
|
|
65
|
+
for (let t of this.listeners) try {
|
|
66
|
+
t(e);
|
|
67
|
+
} catch {}
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
//#endregion
|
|
71
|
+
//#region src/core/evaluator.ts
|
|
72
|
+
function r(e, t, n) {
|
|
73
|
+
let r = n(`${e}:${t}`);
|
|
74
|
+
return ((r[0] | r[1] << 8 | r[2] << 16 | r[3] << 24 >>> 0) >>> 0) % 100;
|
|
75
|
+
}
|
|
76
|
+
function i(e, t) {
|
|
77
|
+
let n = e[t];
|
|
78
|
+
if (n !== void 0) return n;
|
|
79
|
+
if (t === "userId") return e.user_id;
|
|
80
|
+
if (t === "user_id") return e.userId;
|
|
81
|
+
}
|
|
82
|
+
function a(e, t, n) {
|
|
83
|
+
switch (e) {
|
|
84
|
+
case "Equals": return n.some((e) => t === e);
|
|
85
|
+
case "NotEquals": return n.every((e) => t !== e);
|
|
86
|
+
case "Contains": return n.some((e) => t.includes(e));
|
|
87
|
+
case "NotContains": return n.every((e) => !t.includes(e));
|
|
88
|
+
case "StartsWith": return n.some((e) => t.startsWith(e));
|
|
89
|
+
case "EndsWith": return n.some((e) => t.endsWith(e));
|
|
90
|
+
case "In": return n.includes(t);
|
|
91
|
+
case "NotIn": return !n.includes(t);
|
|
92
|
+
case "MatchesRegex": return n.some((e) => {
|
|
93
|
+
try {
|
|
94
|
+
return new RegExp(e, "i").test(t);
|
|
95
|
+
} catch {
|
|
96
|
+
return !1;
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
case "GreaterThan": return o(t, n[0], ">");
|
|
100
|
+
case "GreaterThanOrEqual": return o(t, n[0], ">=");
|
|
101
|
+
case "LessThan": return o(t, n[0], "<");
|
|
102
|
+
case "LessThanOrEqual": return o(t, n[0], "<=");
|
|
103
|
+
case "Before": return t < n[0];
|
|
104
|
+
case "After": return t > n[0];
|
|
105
|
+
default: return !1;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function o(e, t, n) {
|
|
109
|
+
let r = parseFloat(e), i = parseFloat(t);
|
|
110
|
+
if (isNaN(r) || isNaN(i)) return !1;
|
|
111
|
+
switch (n) {
|
|
112
|
+
case ">": return r > i;
|
|
113
|
+
case "<": return r < i;
|
|
114
|
+
case ">=": return r >= i;
|
|
115
|
+
case "<=": return r <= i;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function s(e, t) {
|
|
119
|
+
let n = i(t, e.attribute);
|
|
120
|
+
if (n == null) return e.negate;
|
|
121
|
+
let r = String(n).toLowerCase(), o = e.values.map((e) => e.toLowerCase()), s = a(e.operator, r, o);
|
|
122
|
+
return e.negate ? !s : s;
|
|
123
|
+
}
|
|
124
|
+
function c(e, t, n) {
|
|
125
|
+
return e.length === 0 ? !0 : t === "And" ? e.every((e) => s(e, n)) : e.some((e) => s(e, n));
|
|
126
|
+
}
|
|
127
|
+
function l(e, t) {
|
|
128
|
+
return e.length === 0 ? !0 : e.every((e) => c(e.conditions, e.operator, t));
|
|
129
|
+
}
|
|
130
|
+
function u(e, t, n) {
|
|
131
|
+
if (e.type === "Fixed") return e.variation ?? "";
|
|
132
|
+
let a = i(t, e.bucketBy ?? "userId"), o = a == null ? "" : String(a), s = r(e.salt ?? "", o, n), c = 0;
|
|
133
|
+
for (let t of e.variations ?? []) if (c += t.weight, s < c) return t.key;
|
|
134
|
+
let l = e.variations ?? [];
|
|
135
|
+
return l.length > 0 ? l[l.length - 1].key : "";
|
|
136
|
+
}
|
|
137
|
+
function d(e, t, n) {
|
|
138
|
+
if (!e.enabled) return {
|
|
139
|
+
value: e.variations.find((t) => t.key === e.offVariation)?.value ?? null,
|
|
140
|
+
variationKey: e.offVariation,
|
|
141
|
+
reason: "FlagDisabled"
|
|
142
|
+
};
|
|
143
|
+
let r = [...e.rules].sort((e, t) => e.priority - t.priority);
|
|
144
|
+
for (let i of r) {
|
|
145
|
+
let r;
|
|
146
|
+
if (i.segmentKey && n.getSegment) {
|
|
147
|
+
let e = n.getSegment(i.segmentKey);
|
|
148
|
+
r = e ? c(e.conditions, e.conditionLogic, t) : !1;
|
|
149
|
+
} else r = l(i.conditionGroups, t);
|
|
150
|
+
if (r) {
|
|
151
|
+
let r = u(i.serve, t, n.md5);
|
|
152
|
+
return {
|
|
153
|
+
value: e.variations.find((e) => e.key === r)?.value ?? null,
|
|
154
|
+
variationKey: r,
|
|
155
|
+
reason: "RuleMatch",
|
|
156
|
+
ruleId: i.id
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
let i = u(e.fallthrough, t, n.md5);
|
|
161
|
+
return {
|
|
162
|
+
value: e.variations.find((e) => e.key === i)?.value ?? null,
|
|
163
|
+
variationKey: i,
|
|
164
|
+
reason: "Fallthrough"
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
//#endregion
|
|
168
|
+
//#region src/core/events.ts
|
|
169
|
+
var f = class {
|
|
170
|
+
queue = [];
|
|
171
|
+
flushTimer = null;
|
|
172
|
+
closed = !1;
|
|
173
|
+
flushPromise = null;
|
|
174
|
+
constructor(e, t, n) {
|
|
175
|
+
this.sender = e, this.flushInterval = t, this.flushBatchSize = n;
|
|
176
|
+
}
|
|
177
|
+
start() {
|
|
178
|
+
this.flushTimer ||= setInterval(() => {
|
|
179
|
+
this.flush();
|
|
180
|
+
}, this.flushInterval);
|
|
181
|
+
}
|
|
182
|
+
enqueue(e) {
|
|
183
|
+
this.closed || (this.queue.push(e), this.queue.length >= this.flushBatchSize && this.flush());
|
|
184
|
+
}
|
|
185
|
+
async flush() {
|
|
186
|
+
if (this.queue.length !== 0) return this.flushPromise ||= (async () => {
|
|
187
|
+
for (; this.queue.length > 0;) {
|
|
188
|
+
let e = this.queue.splice(0, this.flushBatchSize);
|
|
189
|
+
try {
|
|
190
|
+
await this.sender.sendEvents({ events: e });
|
|
191
|
+
} catch {}
|
|
192
|
+
}
|
|
193
|
+
})().finally(() => {
|
|
194
|
+
this.flushPromise = null;
|
|
195
|
+
}), this.flushPromise;
|
|
196
|
+
}
|
|
197
|
+
async close() {
|
|
198
|
+
for (this.closed = !0, this.flushTimer &&= (clearInterval(this.flushTimer), null); this.queue.length > 0;) await this.flush();
|
|
199
|
+
}
|
|
200
|
+
}, p = class e {
|
|
201
|
+
config;
|
|
202
|
+
store;
|
|
203
|
+
events;
|
|
204
|
+
platform;
|
|
205
|
+
initialized = !1;
|
|
206
|
+
initPromise = null;
|
|
207
|
+
eventSource = null;
|
|
208
|
+
pollTimer = null;
|
|
209
|
+
closed = !1;
|
|
210
|
+
streamRetryCount = 0;
|
|
211
|
+
streamRetryTimer = null;
|
|
212
|
+
constructor(e, r) {
|
|
213
|
+
this.config = t(e), this.store = new n(), this.platform = r, this.events = new f({ sendEvents: async (e) => {
|
|
214
|
+
await this.platform.fetch(`${this.config.baseUrl}/v1/sdk/events`, {
|
|
215
|
+
method: "POST",
|
|
216
|
+
headers: this.headers(),
|
|
217
|
+
body: JSON.stringify(e)
|
|
218
|
+
});
|
|
219
|
+
} }, this.config.flushInterval, this.config.flushBatchSize);
|
|
220
|
+
}
|
|
221
|
+
get isInitialized() {
|
|
222
|
+
return this.initialized;
|
|
223
|
+
}
|
|
224
|
+
async waitForInitialization() {
|
|
225
|
+
if (!this.initialized) return this.initPromise ||= this.initialize(), this.initPromise;
|
|
226
|
+
}
|
|
227
|
+
boolVariation(e, t, n) {
|
|
228
|
+
return this.evaluateFlag(e, t, n);
|
|
229
|
+
}
|
|
230
|
+
stringVariation(e, t, n) {
|
|
231
|
+
return this.evaluateFlag(e, t, n);
|
|
232
|
+
}
|
|
233
|
+
numberVariation(e, t, n) {
|
|
234
|
+
return this.evaluateFlag(e, t, n);
|
|
235
|
+
}
|
|
236
|
+
jsonVariation(e, t, n) {
|
|
237
|
+
return this.evaluateFlag(e, t, n);
|
|
238
|
+
}
|
|
239
|
+
variationDetail(e, t, n) {
|
|
240
|
+
let r = this.store.getFlag(e);
|
|
241
|
+
if (!r) return this.recordEvaluation(e, t, void 0), {
|
|
242
|
+
value: n,
|
|
243
|
+
reason: "FlagNotFound"
|
|
244
|
+
};
|
|
245
|
+
try {
|
|
246
|
+
let i = d(r, t, {
|
|
247
|
+
md5: (e) => this.platform.md5(e),
|
|
248
|
+
getSegment: (e) => this.store.getSegment(e)
|
|
249
|
+
}), a = i.value !== void 0 && i.value !== null ? i.value : n;
|
|
250
|
+
return this.recordEvaluation(e, t, i.variationKey), {
|
|
251
|
+
value: a,
|
|
252
|
+
reason: i.reason,
|
|
253
|
+
ruleId: i.ruleId
|
|
254
|
+
};
|
|
255
|
+
} catch {
|
|
256
|
+
return this.recordEvaluation(e, t, void 0), {
|
|
257
|
+
value: n,
|
|
258
|
+
reason: "Error"
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
track(e, t, n) {
|
|
263
|
+
let r = t.user_id == null ? void 0 : String(t.user_id);
|
|
264
|
+
this.events.enqueue({
|
|
265
|
+
type: "Custom",
|
|
266
|
+
flagKey: e,
|
|
267
|
+
userId: r,
|
|
268
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
269
|
+
metadata: n
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
identify(e) {
|
|
273
|
+
let t = e.user_id == null ? void 0 : String(e.user_id), { user_id: n, ...r } = e;
|
|
274
|
+
this.events.enqueue({
|
|
275
|
+
type: "Identify",
|
|
276
|
+
flagKey: "$identify",
|
|
277
|
+
userId: t,
|
|
278
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
279
|
+
metadata: Object.keys(r).length > 0 ? r : void 0
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
async flush() {
|
|
283
|
+
await this.events.flush();
|
|
284
|
+
}
|
|
285
|
+
async close() {
|
|
286
|
+
this.closed = !0, this.eventSource?.close(), this.eventSource = null, this.streamRetryTimer &&= (clearTimeout(this.streamRetryTimer), null), this.pollTimer &&= (clearInterval(this.pollTimer), null), await this.events.close();
|
|
287
|
+
}
|
|
288
|
+
static forTesting(t) {
|
|
289
|
+
let n = Object.entries(t).map(([e, t]) => ({
|
|
290
|
+
key: e,
|
|
291
|
+
version: 1,
|
|
292
|
+
type: typeof t == "boolean" ? "Boolean" : typeof t == "number" ? "Number" : typeof t == "string" ? "String" : "Json",
|
|
293
|
+
enabled: !0,
|
|
294
|
+
variations: [{
|
|
295
|
+
key: "default",
|
|
296
|
+
value: t
|
|
297
|
+
}],
|
|
298
|
+
rules: [],
|
|
299
|
+
fallthrough: {
|
|
300
|
+
type: "Fixed",
|
|
301
|
+
variation: "default"
|
|
302
|
+
},
|
|
303
|
+
offVariation: "default"
|
|
304
|
+
})), r = new e({
|
|
305
|
+
sdkKey: "test-key",
|
|
306
|
+
baseUrl: "http://localhost"
|
|
307
|
+
}, {
|
|
308
|
+
md5: () => new Uint8Array(16),
|
|
309
|
+
createEventSource: () => ({
|
|
310
|
+
addEventListener: () => {},
|
|
311
|
+
close: () => {},
|
|
312
|
+
readyState: 2
|
|
313
|
+
}),
|
|
314
|
+
fetch: async () => new Response()
|
|
315
|
+
});
|
|
316
|
+
return r.store.init(n, [], 1), r.initialized = !0, r;
|
|
317
|
+
}
|
|
318
|
+
evaluateFlag(e, t, n) {
|
|
319
|
+
return this.variationDetail(e, t, n).value;
|
|
320
|
+
}
|
|
321
|
+
async initialize() {
|
|
322
|
+
let e, t = new Promise((t, n) => {
|
|
323
|
+
e = setTimeout(() => n(/* @__PURE__ */ Error("Initialization timed out")), this.config.initTimeout);
|
|
324
|
+
}), n = (async () => {
|
|
325
|
+
await this.fetchFlags(), this.initialized = !0, this.events.start(), this.startDataSource();
|
|
326
|
+
})();
|
|
327
|
+
try {
|
|
328
|
+
await Promise.race([n, t]);
|
|
329
|
+
} finally {
|
|
330
|
+
clearTimeout(e);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
async fetchFlags() {
|
|
334
|
+
let e = await this.platform.fetch(`${this.config.baseUrl}/v1/sdk/flags`, { headers: this.headers() });
|
|
335
|
+
if (!e.ok) throw Error(`Failed to fetch flags: ${e.status}`);
|
|
336
|
+
let t = await e.json();
|
|
337
|
+
this.store.init(t.flags, t.segments, t.version);
|
|
338
|
+
}
|
|
339
|
+
startDataSource() {
|
|
340
|
+
this.closed || (this.config.streaming ? this.startStreaming() : this.startPolling());
|
|
341
|
+
}
|
|
342
|
+
startStreaming() {
|
|
343
|
+
if (this.closed) return;
|
|
344
|
+
let e = this.platform.sseSupportsHeaders ? `${this.config.baseUrl}/v1/sdk/stream` : `${this.config.baseUrl}/v1/sdk/stream?authorization=${encodeURIComponent(this.config.sdkKey)}`;
|
|
345
|
+
this.eventSource = this.platform.createEventSource(e, this.headers());
|
|
346
|
+
for (let e of ["flag.created", "flag.updated"]) this.eventSource.addEventListener(e, (e) => {
|
|
347
|
+
try {
|
|
348
|
+
let t = JSON.parse(e.data);
|
|
349
|
+
t.key && this.fetchSingleFlag(t.key);
|
|
350
|
+
} catch {}
|
|
351
|
+
});
|
|
352
|
+
this.eventSource.addEventListener("flag.deleted", (e) => {
|
|
353
|
+
try {
|
|
354
|
+
let t = JSON.parse(e.data);
|
|
355
|
+
t.key && this.store.delete(t.key);
|
|
356
|
+
} catch {}
|
|
357
|
+
}), this.eventSource.addEventListener("segment.updated", () => {
|
|
358
|
+
this.fetchFlags().catch(() => {});
|
|
359
|
+
}), this.eventSource.addEventListener("open", () => {
|
|
360
|
+
this.streamRetryCount = 0;
|
|
361
|
+
}), this.eventSource.addEventListener("error", () => {
|
|
362
|
+
if (this.eventSource?.close(), this.eventSource = null, this.closed) return;
|
|
363
|
+
if (this.streamRetryCount >= this.config.maxStreamRetries) {
|
|
364
|
+
console.warn(`[featureflip] SSE connection failed after ${this.config.maxStreamRetries} retries, falling back to polling`), this.startPolling();
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
let e = Math.min(1e3 * 2 ** this.streamRetryCount, 3e4);
|
|
368
|
+
this.streamRetryCount++, this.streamRetryTimer = setTimeout(() => {
|
|
369
|
+
this.streamRetryTimer = null, this.startStreaming();
|
|
370
|
+
}, e);
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
startPolling() {
|
|
374
|
+
this.pollTimer = setInterval(() => {
|
|
375
|
+
this.fetchFlags().catch(() => {});
|
|
376
|
+
}, this.config.pollInterval);
|
|
377
|
+
}
|
|
378
|
+
async fetchSingleFlag(e) {
|
|
379
|
+
try {
|
|
380
|
+
let t = await this.platform.fetch(`${this.config.baseUrl}/v1/sdk/flags/${encodeURIComponent(e)}`, { headers: this.headers() });
|
|
381
|
+
if (t.ok) {
|
|
382
|
+
let e = await t.json();
|
|
383
|
+
this.store.upsert(e);
|
|
384
|
+
}
|
|
385
|
+
} catch {}
|
|
386
|
+
}
|
|
387
|
+
recordEvaluation(e, t, n) {
|
|
388
|
+
let r = t.user_id == null ? void 0 : String(t.user_id);
|
|
389
|
+
this.events.enqueue({
|
|
390
|
+
type: "Evaluation",
|
|
391
|
+
flagKey: e,
|
|
392
|
+
userId: r,
|
|
393
|
+
variation: n,
|
|
394
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
headers() {
|
|
398
|
+
return {
|
|
399
|
+
Authorization: this.config.sdkKey,
|
|
400
|
+
"Content-Type": "application/json",
|
|
401
|
+
...this.platform.extraHeaders
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
};
|
|
405
|
+
//#endregion
|
|
406
|
+
export { p as t };
|
package/dist/node.cjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require(`./client-Bf3tS17L.cjs`);let t=require(`crypto`);var n=(0,require(`module`).createRequire)({}.url);function r(){return{md5(e){return(0,t.createHash)(`md5`).update(e,`utf8`).digest()},createEventSource(e,t){let{EventSource:r}=n(`eventsource`),i=new r(e,{fetch:(e,n)=>globalThis.fetch(e,{...n,headers:{...n?.headers,...t}})});return{addEventListener:(e,t)=>{i.addEventListener(e,t)},close:()=>i.close(),get readyState(){return i.readyState}}},async fetch(e,t){return globalThis.fetch(e,t)},extraHeaders:{"User-Agent":`featureflip-js/0.1.0`},sseSupportsHeaders:!0}}exports.FeatureflipClient=e.t,exports.createNodePlatform=r;
|
package/dist/node.d.ts
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
export declare function createNodePlatform(): Platform;
|
|
2
|
+
|
|
3
|
+
export declare type EvaluationContext = Record<string, unknown>;
|
|
4
|
+
|
|
5
|
+
export declare interface EvaluationDetail<T = unknown> {
|
|
6
|
+
value: T;
|
|
7
|
+
variationKey?: string;
|
|
8
|
+
reason: EvaluationReason;
|
|
9
|
+
ruleId?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export declare type EvaluationReason = 'RuleMatch' | 'Fallthrough' | 'FlagDisabled' | 'FlagNotFound' | 'Error';
|
|
13
|
+
|
|
14
|
+
declare interface EventSourceLike {
|
|
15
|
+
addEventListener(type: string, listener: (event: {
|
|
16
|
+
data: string;
|
|
17
|
+
}) => void): void;
|
|
18
|
+
close(): void;
|
|
19
|
+
readonly readyState: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export declare class FeatureflipClient {
|
|
23
|
+
private readonly config;
|
|
24
|
+
private readonly store;
|
|
25
|
+
private readonly events;
|
|
26
|
+
private readonly platform;
|
|
27
|
+
private initialized;
|
|
28
|
+
private initPromise;
|
|
29
|
+
private eventSource;
|
|
30
|
+
private pollTimer;
|
|
31
|
+
private closed;
|
|
32
|
+
private streamRetryCount;
|
|
33
|
+
private streamRetryTimer;
|
|
34
|
+
constructor(config: FeatureflipConfig, platform: Platform);
|
|
35
|
+
/** Whether the client has successfully loaded initial flag data. */
|
|
36
|
+
get isInitialized(): boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Wait for the client to finish initialization.
|
|
39
|
+
* Rejects after initTimeout if initial flag fetch fails.
|
|
40
|
+
*/
|
|
41
|
+
waitForInitialization(): Promise<void>;
|
|
42
|
+
/**
|
|
43
|
+
* Evaluate a boolean flag.
|
|
44
|
+
* Returns defaultValue if flag not found or evaluation fails.
|
|
45
|
+
*/
|
|
46
|
+
boolVariation(key: string, context: EvaluationContext, defaultValue: boolean): boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Evaluate a string flag.
|
|
49
|
+
*/
|
|
50
|
+
stringVariation(key: string, context: EvaluationContext, defaultValue: string): string;
|
|
51
|
+
/**
|
|
52
|
+
* Evaluate a number flag.
|
|
53
|
+
*/
|
|
54
|
+
numberVariation(key: string, context: EvaluationContext, defaultValue: number): number;
|
|
55
|
+
/**
|
|
56
|
+
* Evaluate a JSON flag.
|
|
57
|
+
*/
|
|
58
|
+
jsonVariation<T>(key: string, context: EvaluationContext, defaultValue: T): T;
|
|
59
|
+
/**
|
|
60
|
+
* Evaluate a flag and return the full detail including reason.
|
|
61
|
+
*/
|
|
62
|
+
variationDetail<T>(key: string, context: EvaluationContext, defaultValue: T): EvaluationDetail<T>;
|
|
63
|
+
/**
|
|
64
|
+
* Track a custom event.
|
|
65
|
+
*/
|
|
66
|
+
track(eventKey: string, context: EvaluationContext, metadata?: Record<string, unknown>): void;
|
|
67
|
+
/**
|
|
68
|
+
* Send an identify event for the given context.
|
|
69
|
+
*/
|
|
70
|
+
identify(context: EvaluationContext): void;
|
|
71
|
+
/**
|
|
72
|
+
* Flush any pending events immediately.
|
|
73
|
+
*/
|
|
74
|
+
flush(): Promise<void>;
|
|
75
|
+
/**
|
|
76
|
+
* Close the client, flushing pending events and stopping all connections.
|
|
77
|
+
*/
|
|
78
|
+
close(): Promise<void>;
|
|
79
|
+
/**
|
|
80
|
+
* Create a test client with hardcoded flag values. No network calls.
|
|
81
|
+
*/
|
|
82
|
+
static forTesting(flags: Record<string, unknown>): FeatureflipClient;
|
|
83
|
+
private evaluateFlag;
|
|
84
|
+
private initialize;
|
|
85
|
+
private fetchFlags;
|
|
86
|
+
private startDataSource;
|
|
87
|
+
private startStreaming;
|
|
88
|
+
private startPolling;
|
|
89
|
+
private fetchSingleFlag;
|
|
90
|
+
private recordEvaluation;
|
|
91
|
+
private headers;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export declare interface FeatureflipConfig {
|
|
95
|
+
sdkKey: string;
|
|
96
|
+
baseUrl: string;
|
|
97
|
+
streaming?: boolean;
|
|
98
|
+
pollInterval?: number;
|
|
99
|
+
flushInterval?: number;
|
|
100
|
+
flushBatchSize?: number;
|
|
101
|
+
initTimeout?: number;
|
|
102
|
+
maxStreamRetries?: number;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export declare type FlagType = 'Boolean' | 'String' | 'Number' | 'Json';
|
|
106
|
+
|
|
107
|
+
export declare interface Platform {
|
|
108
|
+
md5(input: string): Uint8Array;
|
|
109
|
+
createEventSource(url: string, headers: Record<string, string>): EventSourceLike;
|
|
110
|
+
fetch(url: string, init?: RequestInit): Promise<Response>;
|
|
111
|
+
/** Extra headers the platform can inject (e.g. User-Agent on Node). */
|
|
112
|
+
readonly extraHeaders?: Record<string, string>;
|
|
113
|
+
/** Whether the platform's EventSource implementation supports custom headers. */
|
|
114
|
+
readonly sseSupportsHeaders?: boolean;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export { }
|
package/dist/node.mjs
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { t as e } from "./client-ChXPtrh5.js";
|
|
2
|
+
import { createHash as t } from "crypto";
|
|
3
|
+
import { createRequire as n } from "module";
|
|
4
|
+
//#region src/platform/node.ts
|
|
5
|
+
var r = n(import.meta.url);
|
|
6
|
+
function i() {
|
|
7
|
+
return {
|
|
8
|
+
md5(e) {
|
|
9
|
+
return t("md5").update(e, "utf8").digest();
|
|
10
|
+
},
|
|
11
|
+
createEventSource(e, t) {
|
|
12
|
+
let { EventSource: n } = r("eventsource"), i = new n(e, { fetch: (e, n) => globalThis.fetch(e, {
|
|
13
|
+
...n,
|
|
14
|
+
headers: {
|
|
15
|
+
...n?.headers,
|
|
16
|
+
...t
|
|
17
|
+
}
|
|
18
|
+
}) });
|
|
19
|
+
return {
|
|
20
|
+
addEventListener: (e, t) => {
|
|
21
|
+
i.addEventListener(e, t);
|
|
22
|
+
},
|
|
23
|
+
close: () => i.close(),
|
|
24
|
+
get readyState() {
|
|
25
|
+
return i.readyState;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
},
|
|
29
|
+
async fetch(e, t) {
|
|
30
|
+
return globalThis.fetch(e, t);
|
|
31
|
+
},
|
|
32
|
+
extraHeaders: { "User-Agent": "featureflip-js/0.1.0" },
|
|
33
|
+
sseSupportsHeaders: !0
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
//#endregion
|
|
37
|
+
export { e as FeatureflipClient, i as createNodePlatform };
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@featureflip/js",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "JavaScript/TypeScript SDK for FeatureFlip",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"homepage": "https://featureflip.io/docs",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/node.d.ts",
|
|
11
|
+
"node": {
|
|
12
|
+
"import": "./dist/node.mjs",
|
|
13
|
+
"require": "./dist/node.cjs"
|
|
14
|
+
},
|
|
15
|
+
"browser": {
|
|
16
|
+
"import": "./dist/browser.mjs",
|
|
17
|
+
"require": "./dist/browser.cjs"
|
|
18
|
+
},
|
|
19
|
+
"default": "./dist/browser.mjs"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"types": "./dist/node.d.ts",
|
|
23
|
+
"files": [
|
|
24
|
+
"dist"
|
|
25
|
+
],
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "vite build",
|
|
28
|
+
"test": "vitest run",
|
|
29
|
+
"test:watch": "vitest",
|
|
30
|
+
"test:coverage": "vitest run --coverage"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"eventsource": "^4.0.0"
|
|
34
|
+
},
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=20.19.0"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@types/node": "^25.5.0",
|
|
40
|
+
"typescript": "^6.0.2",
|
|
41
|
+
"vite": "^8.0.3",
|
|
42
|
+
"vite-plugin-dts": "^4.0.0",
|
|
43
|
+
"vitest": "^4.1.2"
|
|
44
|
+
},
|
|
45
|
+
"publishConfig": {
|
|
46
|
+
"access": "public"
|
|
47
|
+
}
|
|
48
|
+
}
|