@featureflip/browser 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 +107 -0
- package/dist/index.cjs +1 -0
- package/dist/index.d.ts +52 -0
- package/dist/index.js +220 -0
- package/package.json +39 -0
package/README.md
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# @featureflip/browser-sdk
|
|
2
|
+
|
|
3
|
+
Framework-agnostic browser SDK for evaluating Featureflip feature flags.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @featureflip/browser-sdk
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { FeatureflipClient } from '@featureflip/browser-sdk';
|
|
15
|
+
|
|
16
|
+
const client = new FeatureflipClient({
|
|
17
|
+
clientKey: 'your-client-sdk-key',
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
await client.initialize();
|
|
21
|
+
|
|
22
|
+
const showBanner = client.boolVariation('show-banner', false);
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## API Reference
|
|
26
|
+
|
|
27
|
+
### Constructor
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
new FeatureflipClient(config: FeatureflipClientConfig)
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Configuration Options
|
|
34
|
+
|
|
35
|
+
| Option | Type | Default | Description |
|
|
36
|
+
|---|---|---|---|
|
|
37
|
+
| `clientKey` | `string` | **(required)** | Client SDK key from your project settings |
|
|
38
|
+
| `baseUrl` | `string` | `https://eval.featureflip.io` | Evaluation API base URL |
|
|
39
|
+
| `context` | `Record<string, unknown>` | `{}` | Initial evaluation context (user attributes) |
|
|
40
|
+
| `streaming` | `boolean` | `true` | Enable SSE streaming for real-time updates |
|
|
41
|
+
| `initTimeout` | `number` | `10000` | Timeout in ms for the initial evaluate request |
|
|
42
|
+
|
|
43
|
+
### Methods
|
|
44
|
+
|
|
45
|
+
#### `initialize(): Promise<void>`
|
|
46
|
+
|
|
47
|
+
Fetches all flag values from the server. Must be called before reading variations. Opens an SSE streaming connection if `streaming` is enabled.
|
|
48
|
+
|
|
49
|
+
#### `boolVariation(key: string, defaultValue: boolean): boolean`
|
|
50
|
+
|
|
51
|
+
Returns a boolean flag value, or `defaultValue` if the flag is missing or not a boolean.
|
|
52
|
+
|
|
53
|
+
#### `stringVariation(key: string, defaultValue: string): string`
|
|
54
|
+
|
|
55
|
+
Returns a string flag value, or `defaultValue` if the flag is missing or not a string.
|
|
56
|
+
|
|
57
|
+
#### `numberVariation(key: string, defaultValue: number): number`
|
|
58
|
+
|
|
59
|
+
Returns a number flag value, or `defaultValue` if the flag is missing or not a number.
|
|
60
|
+
|
|
61
|
+
#### `jsonVariation<T>(key: string, defaultValue: T): T`
|
|
62
|
+
|
|
63
|
+
Returns a flag value cast to `T`, or `defaultValue` if the flag is missing.
|
|
64
|
+
|
|
65
|
+
#### `identify(context: Record<string, unknown>): Promise<void>`
|
|
66
|
+
|
|
67
|
+
Re-evaluates all flags with a new context (e.g., after login). Emits `change` events for any flags whose values changed.
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
await client.identify({ user_id: '123', plan: 'pro' });
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
#### `on(event: EventType, handler: EventHandler): void`
|
|
74
|
+
|
|
75
|
+
Subscribe to events.
|
|
76
|
+
|
|
77
|
+
- `'ready'` -- fired after `initialize()` completes
|
|
78
|
+
- `'change'` -- fired when flag values change (receives a `FlagChanges` object)
|
|
79
|
+
- `'error'` -- fired on streaming or network errors
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
client.on('change', (changes) => {
|
|
83
|
+
console.log('Flags changed:', changes);
|
|
84
|
+
});
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
#### `off(event: EventType, handler: EventHandler): void`
|
|
88
|
+
|
|
89
|
+
Unsubscribe from events.
|
|
90
|
+
|
|
91
|
+
#### `close(): void`
|
|
92
|
+
|
|
93
|
+
Closes the SSE streaming connection and cleans up resources.
|
|
94
|
+
|
|
95
|
+
### Testing
|
|
96
|
+
|
|
97
|
+
Use `FeatureflipClient.forTesting()` to create a client with predetermined flag values -- no network calls.
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
const client = FeatureflipClient.forTesting({
|
|
101
|
+
'show-banner': true,
|
|
102
|
+
'button-color': 'blue',
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
client.boolVariation('show-banner', false); // true
|
|
106
|
+
client.stringVariation('button-color', 'red'); // 'blue'
|
|
107
|
+
```
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});var e=class{handlers=new Map;on(e,t){this.handlers.has(e)||this.handlers.set(e,new Set),this.handlers.get(e).add(t)}off(e,t){this.handlers.get(e)?.delete(t)}emit(e,...t){this.handlers.get(e)?.forEach(e=>{try{e(...t)}catch{}})}};async function t(e,t,n,r){let i=await fetch(`${e}/v1/client/evaluate`,{method:`POST`,headers:{"Content-Type":`application/json`,Authorization:t},body:JSON.stringify({context:n}),signal:r});if(!i.ok)throw Error(`Evaluate request failed with status ${i.status}`);return i.json()}async function n(e,t,n,r){let i={"Content-Type":`application/json`,Authorization:t};r&&(i[`X-Connection-Id`]=r);let a=await fetch(`${e}/v1/client/identify`,{method:`POST`,headers:i,body:JSON.stringify({context:n})});if(!a.ok)throw Error(`Identify request failed with status ${a.status}`);return a.json()}var r=3e4,i=1e3,a=class{eventSource=null;options;backoffMs=i;reconnectTimer=null;closed=!1;_connectionId=null;get connectionId(){return this._connectionId}constructor(e){this.options=e,this.connect()}connect(){if(this.closed)return;this._connectionId=null;let{baseUrl:e,clientKey:t,context:n}=this.options,r=new TextEncoder().encode(JSON.stringify(n)),a=``;for(let e=0;e<r.length;e++)a+=String.fromCharCode(r[e]);let o=btoa(a),s=`${e}/v1/client/stream?authorization=${encodeURIComponent(t)}&context=${encodeURIComponent(o)}`;this.eventSource=new EventSource(s),this.eventSource.addEventListener(`flags-updated`,e=>{try{let t=JSON.parse(e.data);this.backoffMs=i,this.options.onChange(t.flags??t)}catch{}}),this.eventSource.addEventListener(`connection-ready`,e=>{try{let t=JSON.parse(e.data);t.connectionId&&(this._connectionId=t.connectionId)}catch{}}),this.eventSource.onerror=()=>{this.eventSource?.close(),this.eventSource=null,this.closed||(this.options.onError?.(Error(`SSE connection error`)),this.scheduleReconnect())}}scheduleReconnect(){this.closed||this.reconnectTimer!==null||(this.reconnectTimer=setTimeout(()=>{this.reconnectTimer=null,this.connect()},this.backoffMs),this.backoffMs=Math.min(this.backoffMs*2,r))}close(){this.closed=!0,this.eventSource?.close(),this.eventSource=null,this.reconnectTimer!==null&&(clearTimeout(this.reconnectTimer),this.reconnectTimer=null)}},o=class r{config;flags=new Map;emitter=new e;stream=null;initialized=!1;initPromise=null;closed=!1;constructor(e){if(!e.clientKey)throw Error(`clientKey is required`);this.config={clientKey:e.clientKey,baseUrl:e.baseUrl??`https://eval.featureflip.io`,context:e.context??{},streaming:e.streaming??!0,initTimeout:e.initTimeout??1e4}}initialize(){return this.initialized?Promise.resolve():(this.initPromise||=this.doInitialize(),this.initPromise)}async doInitialize(){let e=new AbortController,n=setTimeout(()=>e.abort(),this.config.initTimeout),r;try{r=await t(this.config.baseUrl,this.config.clientKey,this.config.context,e.signal)}finally{clearTimeout(n)}this.closed||(this.setFlags(r.flags),this.initialized=!0,this.config.streaming&&(this.stream=this.createStream()),this.emitter.emit(`ready`))}boolVariation(e,t){let n=this.flags.get(e);return n===void 0?t:typeof n.value==`boolean`?n.value:t}stringVariation(e,t){let n=this.flags.get(e);return n===void 0?t:typeof n.value==`string`?n.value:t}numberVariation(e,t){let n=this.flags.get(e);return n===void 0?t:typeof n.value==`number`?n.value:t}jsonVariation(e,t){let n=this.flags.get(e);return n===void 0?t:n.value}async identify(e){let t=this.config.context;this.config.context=e,this.stream&&=(this.stream.close(),null);let r;try{r=await n(this.config.baseUrl,this.config.clientKey,e)}catch(e){throw this.config.context=t,this.emitter.emit(`error`,e instanceof Error?e:Error(String(e))),this.config.streaming&&(this.stream=this.createStream()),e}let i=this.computeChanges(r.flags);this.setFlags(r.flags),Object.keys(i).length>0&&this.emitter.emit(`change`,i),this.config.streaming&&(this.stream=this.createStream())}on(e,t){this.emitter.on(e,t)}off(e,t){this.emitter.off(e,t)}close(){this.closed=!0,this.stream?.close(),this.stream=null}static forTesting(t){let n=Object.create(r.prototype);n.config={clientKey:`test-key`,baseUrl:`http://localhost`,context:{},streaming:!1,initTimeout:1e4},n.flags=new Map,n.emitter=new e,n.stream=null,n.initialized=!0,n.closed=!1;for(let[e,r]of Object.entries(t))n.flags.set(e,{value:r,variation:`test`,reason:`test`});return n}createStream(){return new a({baseUrl:this.config.baseUrl,clientKey:this.config.clientKey,context:this.config.context,onChange:e=>this.handleFlagUpdate(e),onError:e=>this.emitter.emit(`error`,e)})}setFlags(e){this.flags.clear();for(let[t,n]of Object.entries(e))this.flags.set(t,n)}computeChanges(e){let t={};for(let[n,r]of Object.entries(e)){let e=this.flags.get(n)?.value;e!==r.value&&(t[n]={oldValue:e,newValue:r.value})}for(let[n]of this.flags)n in e||(t[n]={oldValue:this.flags.get(n).value,newValue:void 0});return t}handleFlagUpdate(e){let t={};for(let[n,r]of Object.entries(e))if(r.reason===`FLAG_REMOVED`&&r.value===null){let e=this.flags.get(n);e!==void 0&&(t[n]={oldValue:e.value,newValue:void 0},this.flags.delete(n))}else{let e=this.flags.get(n);(e===void 0||e.value!==r.value)&&(t[n]={oldValue:e?.value,newValue:r.value}),this.flags.set(n,r)}Object.keys(t).length>0&&this.emitter.emit(`change`,t)}};exports.FeatureflipClient=o;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export declare type EventHandler = (...args: unknown[]) => void;
|
|
2
|
+
|
|
3
|
+
export declare type EventType = 'ready' | 'change' | 'error';
|
|
4
|
+
|
|
5
|
+
export declare class FeatureflipClient {
|
|
6
|
+
private config;
|
|
7
|
+
private flags;
|
|
8
|
+
private emitter;
|
|
9
|
+
private stream;
|
|
10
|
+
private initialized;
|
|
11
|
+
private initPromise;
|
|
12
|
+
private closed;
|
|
13
|
+
constructor(config: FeatureflipClientConfig);
|
|
14
|
+
initialize(): Promise<void>;
|
|
15
|
+
private doInitialize;
|
|
16
|
+
boolVariation(key: string, defaultValue: boolean): boolean;
|
|
17
|
+
stringVariation(key: string, defaultValue: string): string;
|
|
18
|
+
numberVariation(key: string, defaultValue: number): number;
|
|
19
|
+
jsonVariation<T>(key: string, defaultValue: T): T;
|
|
20
|
+
identify(context: Record<string, unknown>): Promise<void>;
|
|
21
|
+
on(event: EventType, handler: EventHandler): void;
|
|
22
|
+
off(event: EventType, handler: EventHandler): void;
|
|
23
|
+
close(): void;
|
|
24
|
+
static forTesting(flags: Record<string, unknown>): FeatureflipClient;
|
|
25
|
+
private createStream;
|
|
26
|
+
private setFlags;
|
|
27
|
+
private computeChanges;
|
|
28
|
+
private handleFlagUpdate;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export declare interface FeatureflipClientConfig {
|
|
32
|
+
clientKey: string;
|
|
33
|
+
baseUrl?: string;
|
|
34
|
+
context?: Record<string, unknown>;
|
|
35
|
+
streaming?: boolean;
|
|
36
|
+
initTimeout?: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export declare interface FlagChanges {
|
|
40
|
+
[flagKey: string]: {
|
|
41
|
+
oldValue: unknown;
|
|
42
|
+
newValue: unknown;
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export declare interface FlagValue {
|
|
47
|
+
value: unknown;
|
|
48
|
+
variation: string;
|
|
49
|
+
reason: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export { }
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
//#region src/events.ts
|
|
2
|
+
var e = class {
|
|
3
|
+
handlers = /* @__PURE__ */ new Map();
|
|
4
|
+
on(e, t) {
|
|
5
|
+
this.handlers.has(e) || this.handlers.set(e, /* @__PURE__ */ new Set()), this.handlers.get(e).add(t);
|
|
6
|
+
}
|
|
7
|
+
off(e, t) {
|
|
8
|
+
this.handlers.get(e)?.delete(t);
|
|
9
|
+
}
|
|
10
|
+
emit(e, ...t) {
|
|
11
|
+
this.handlers.get(e)?.forEach((e) => {
|
|
12
|
+
try {
|
|
13
|
+
e(...t);
|
|
14
|
+
} catch {}
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
//#endregion
|
|
19
|
+
//#region src/http.ts
|
|
20
|
+
async function t(e, t, n, r) {
|
|
21
|
+
let i = await fetch(`${e}/v1/client/evaluate`, {
|
|
22
|
+
method: "POST",
|
|
23
|
+
headers: {
|
|
24
|
+
"Content-Type": "application/json",
|
|
25
|
+
Authorization: t
|
|
26
|
+
},
|
|
27
|
+
body: JSON.stringify({ context: n }),
|
|
28
|
+
signal: r
|
|
29
|
+
});
|
|
30
|
+
if (!i.ok) throw Error(`Evaluate request failed with status ${i.status}`);
|
|
31
|
+
return i.json();
|
|
32
|
+
}
|
|
33
|
+
async function n(e, t, n, r) {
|
|
34
|
+
let i = {
|
|
35
|
+
"Content-Type": "application/json",
|
|
36
|
+
Authorization: t
|
|
37
|
+
};
|
|
38
|
+
r && (i["X-Connection-Id"] = r);
|
|
39
|
+
let a = await fetch(`${e}/v1/client/identify`, {
|
|
40
|
+
method: "POST",
|
|
41
|
+
headers: i,
|
|
42
|
+
body: JSON.stringify({ context: n })
|
|
43
|
+
});
|
|
44
|
+
if (!a.ok) throw Error(`Identify request failed with status ${a.status}`);
|
|
45
|
+
return a.json();
|
|
46
|
+
}
|
|
47
|
+
//#endregion
|
|
48
|
+
//#region src/streaming.ts
|
|
49
|
+
var r = 3e4, i = 1e3, a = class {
|
|
50
|
+
eventSource = null;
|
|
51
|
+
options;
|
|
52
|
+
backoffMs = i;
|
|
53
|
+
reconnectTimer = null;
|
|
54
|
+
closed = !1;
|
|
55
|
+
_connectionId = null;
|
|
56
|
+
get connectionId() {
|
|
57
|
+
return this._connectionId;
|
|
58
|
+
}
|
|
59
|
+
constructor(e) {
|
|
60
|
+
this.options = e, this.connect();
|
|
61
|
+
}
|
|
62
|
+
connect() {
|
|
63
|
+
if (this.closed) return;
|
|
64
|
+
this._connectionId = null;
|
|
65
|
+
let { baseUrl: e, clientKey: t, context: n } = this.options, r = new TextEncoder().encode(JSON.stringify(n)), a = "";
|
|
66
|
+
for (let e = 0; e < r.length; e++) a += String.fromCharCode(r[e]);
|
|
67
|
+
let o = btoa(a), s = `${e}/v1/client/stream?authorization=${encodeURIComponent(t)}&context=${encodeURIComponent(o)}`;
|
|
68
|
+
this.eventSource = new EventSource(s), this.eventSource.addEventListener("flags-updated", (e) => {
|
|
69
|
+
try {
|
|
70
|
+
let t = JSON.parse(e.data);
|
|
71
|
+
this.backoffMs = i, this.options.onChange(t.flags ?? t);
|
|
72
|
+
} catch {}
|
|
73
|
+
}), this.eventSource.addEventListener("connection-ready", (e) => {
|
|
74
|
+
try {
|
|
75
|
+
let t = JSON.parse(e.data);
|
|
76
|
+
t.connectionId && (this._connectionId = t.connectionId);
|
|
77
|
+
} catch {}
|
|
78
|
+
}), this.eventSource.onerror = () => {
|
|
79
|
+
this.eventSource?.close(), this.eventSource = null, this.closed || (this.options.onError?.(/* @__PURE__ */ Error("SSE connection error")), this.scheduleReconnect());
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
scheduleReconnect() {
|
|
83
|
+
this.closed || this.reconnectTimer !== null || (this.reconnectTimer = setTimeout(() => {
|
|
84
|
+
this.reconnectTimer = null, this.connect();
|
|
85
|
+
}, this.backoffMs), this.backoffMs = Math.min(this.backoffMs * 2, r));
|
|
86
|
+
}
|
|
87
|
+
close() {
|
|
88
|
+
this.closed = !0, this.eventSource?.close(), this.eventSource = null, this.reconnectTimer !== null && (clearTimeout(this.reconnectTimer), this.reconnectTimer = null);
|
|
89
|
+
}
|
|
90
|
+
}, o = class r {
|
|
91
|
+
config;
|
|
92
|
+
flags = /* @__PURE__ */ new Map();
|
|
93
|
+
emitter = new e();
|
|
94
|
+
stream = null;
|
|
95
|
+
initialized = !1;
|
|
96
|
+
initPromise = null;
|
|
97
|
+
closed = !1;
|
|
98
|
+
constructor(e) {
|
|
99
|
+
if (!e.clientKey) throw Error("clientKey is required");
|
|
100
|
+
this.config = {
|
|
101
|
+
clientKey: e.clientKey,
|
|
102
|
+
baseUrl: e.baseUrl ?? "https://eval.featureflip.io",
|
|
103
|
+
context: e.context ?? {},
|
|
104
|
+
streaming: e.streaming ?? !0,
|
|
105
|
+
initTimeout: e.initTimeout ?? 1e4
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
initialize() {
|
|
109
|
+
return this.initialized ? Promise.resolve() : (this.initPromise ||= this.doInitialize(), this.initPromise);
|
|
110
|
+
}
|
|
111
|
+
async doInitialize() {
|
|
112
|
+
let e = new AbortController(), n = setTimeout(() => e.abort(), this.config.initTimeout), r;
|
|
113
|
+
try {
|
|
114
|
+
r = await t(this.config.baseUrl, this.config.clientKey, this.config.context, e.signal);
|
|
115
|
+
} finally {
|
|
116
|
+
clearTimeout(n);
|
|
117
|
+
}
|
|
118
|
+
this.closed || (this.setFlags(r.flags), this.initialized = !0, this.config.streaming && (this.stream = this.createStream()), this.emitter.emit("ready"));
|
|
119
|
+
}
|
|
120
|
+
boolVariation(e, t) {
|
|
121
|
+
let n = this.flags.get(e);
|
|
122
|
+
return n === void 0 ? t : typeof n.value == "boolean" ? n.value : t;
|
|
123
|
+
}
|
|
124
|
+
stringVariation(e, t) {
|
|
125
|
+
let n = this.flags.get(e);
|
|
126
|
+
return n === void 0 ? t : typeof n.value == "string" ? n.value : t;
|
|
127
|
+
}
|
|
128
|
+
numberVariation(e, t) {
|
|
129
|
+
let n = this.flags.get(e);
|
|
130
|
+
return n === void 0 ? t : typeof n.value == "number" ? n.value : t;
|
|
131
|
+
}
|
|
132
|
+
jsonVariation(e, t) {
|
|
133
|
+
let n = this.flags.get(e);
|
|
134
|
+
return n === void 0 ? t : n.value;
|
|
135
|
+
}
|
|
136
|
+
async identify(e) {
|
|
137
|
+
let t = this.config.context;
|
|
138
|
+
this.config.context = e, this.stream &&= (this.stream.close(), null);
|
|
139
|
+
let r;
|
|
140
|
+
try {
|
|
141
|
+
r = await n(this.config.baseUrl, this.config.clientKey, e);
|
|
142
|
+
} catch (e) {
|
|
143
|
+
throw this.config.context = t, this.emitter.emit("error", e instanceof Error ? e : Error(String(e))), this.config.streaming && (this.stream = this.createStream()), e;
|
|
144
|
+
}
|
|
145
|
+
let i = this.computeChanges(r.flags);
|
|
146
|
+
this.setFlags(r.flags), Object.keys(i).length > 0 && this.emitter.emit("change", i), this.config.streaming && (this.stream = this.createStream());
|
|
147
|
+
}
|
|
148
|
+
on(e, t) {
|
|
149
|
+
this.emitter.on(e, t);
|
|
150
|
+
}
|
|
151
|
+
off(e, t) {
|
|
152
|
+
this.emitter.off(e, t);
|
|
153
|
+
}
|
|
154
|
+
close() {
|
|
155
|
+
this.closed = !0, this.stream?.close(), this.stream = null;
|
|
156
|
+
}
|
|
157
|
+
static forTesting(t) {
|
|
158
|
+
let n = Object.create(r.prototype);
|
|
159
|
+
n.config = {
|
|
160
|
+
clientKey: "test-key",
|
|
161
|
+
baseUrl: "http://localhost",
|
|
162
|
+
context: {},
|
|
163
|
+
streaming: !1,
|
|
164
|
+
initTimeout: 1e4
|
|
165
|
+
}, n.flags = /* @__PURE__ */ new Map(), n.emitter = new e(), n.stream = null, n.initialized = !0, n.closed = !1;
|
|
166
|
+
for (let [e, r] of Object.entries(t)) n.flags.set(e, {
|
|
167
|
+
value: r,
|
|
168
|
+
variation: "test",
|
|
169
|
+
reason: "test"
|
|
170
|
+
});
|
|
171
|
+
return n;
|
|
172
|
+
}
|
|
173
|
+
createStream() {
|
|
174
|
+
return new a({
|
|
175
|
+
baseUrl: this.config.baseUrl,
|
|
176
|
+
clientKey: this.config.clientKey,
|
|
177
|
+
context: this.config.context,
|
|
178
|
+
onChange: (e) => this.handleFlagUpdate(e),
|
|
179
|
+
onError: (e) => this.emitter.emit("error", e)
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
setFlags(e) {
|
|
183
|
+
this.flags.clear();
|
|
184
|
+
for (let [t, n] of Object.entries(e)) this.flags.set(t, n);
|
|
185
|
+
}
|
|
186
|
+
computeChanges(e) {
|
|
187
|
+
let t = {};
|
|
188
|
+
for (let [n, r] of Object.entries(e)) {
|
|
189
|
+
let e = this.flags.get(n)?.value;
|
|
190
|
+
e !== r.value && (t[n] = {
|
|
191
|
+
oldValue: e,
|
|
192
|
+
newValue: r.value
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
for (let [n] of this.flags) n in e || (t[n] = {
|
|
196
|
+
oldValue: this.flags.get(n).value,
|
|
197
|
+
newValue: void 0
|
|
198
|
+
});
|
|
199
|
+
return t;
|
|
200
|
+
}
|
|
201
|
+
handleFlagUpdate(e) {
|
|
202
|
+
let t = {};
|
|
203
|
+
for (let [n, r] of Object.entries(e)) if (r.reason === "FLAG_REMOVED" && r.value === null) {
|
|
204
|
+
let e = this.flags.get(n);
|
|
205
|
+
e !== void 0 && (t[n] = {
|
|
206
|
+
oldValue: e.value,
|
|
207
|
+
newValue: void 0
|
|
208
|
+
}, this.flags.delete(n));
|
|
209
|
+
} else {
|
|
210
|
+
let e = this.flags.get(n);
|
|
211
|
+
(e === void 0 || e.value !== r.value) && (t[n] = {
|
|
212
|
+
oldValue: e?.value,
|
|
213
|
+
newValue: r.value
|
|
214
|
+
}), this.flags.set(n, r);
|
|
215
|
+
}
|
|
216
|
+
Object.keys(t).length > 0 && this.emitter.emit("change", t);
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
//#endregion
|
|
220
|
+
export { o as FeatureflipClient };
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@featureflip/browser",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Browser SDK for Featureflip - framework-agnostic feature flag client",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.cjs",
|
|
7
|
+
"module": "dist/index.js",
|
|
8
|
+
"types": "dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js",
|
|
13
|
+
"require": "./dist/index.cjs"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "vite build",
|
|
21
|
+
"test": "vitest run",
|
|
22
|
+
"test:watch": "vitest"
|
|
23
|
+
},
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=20.19.0"
|
|
26
|
+
},
|
|
27
|
+
"license": "Apache-2.0",
|
|
28
|
+
"homepage": "https://featureflip.io/docs",
|
|
29
|
+
"publishConfig": {
|
|
30
|
+
"access": "public"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"jsdom": "^29.0.1",
|
|
34
|
+
"typescript": "^6.0.2",
|
|
35
|
+
"vite": "^8.0.3",
|
|
36
|
+
"vite-plugin-dts": "^4.0.0",
|
|
37
|
+
"vitest": "^4.1.2"
|
|
38
|
+
}
|
|
39
|
+
}
|