@barricador/react-client 0.2.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 +69 -0
- package/dist/BarricadorErrorBoundary.d.ts +22 -0
- package/dist/BarricadorErrorBoundary.js +21 -0
- package/dist/BarricadorProvider.d.ts +20 -0
- package/dist/BarricadorProvider.js +42 -0
- package/dist/context.d.ts +3 -0
- package/dist/context.js +3 -0
- package/dist/hooks.d.ts +11 -0
- package/dist/hooks.js +32 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +4 -0
- package/dist/store.d.ts +46 -0
- package/dist/store.js +167 -0
- package/dist/types.d.ts +34 -0
- package/dist/types.js +2 -0
- package/package.json +53 -0
package/README.md
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# barricador-react-client
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@barricador/react-client)
|
|
4
|
+
|
|
5
|
+
Client-side **React SDK** for Barricador. TypeScript, React 18+.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @barricador/react-client
|
|
11
|
+
# peer dependency: react >= 18
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Security model
|
|
15
|
+
|
|
16
|
+
Browsers are untrusted, so the SDK never downloads rulesets. It POSTs the `UserContext` to the
|
|
17
|
+
backend `/api/v1/flags/eval` with a low-privilege **client key**; the server evaluates targeting
|
|
18
|
+
internally and returns a flattened `key → value` map. An SSE connection signals environment changes,
|
|
19
|
+
prompting a re-fetch. Telemetry is buffered and flushed asynchronously.
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
```tsx
|
|
24
|
+
import {
|
|
25
|
+
BarricadorProvider,
|
|
26
|
+
BarricadorErrorBoundary,
|
|
27
|
+
useFeatureFlag,
|
|
28
|
+
useFeatureFlagEnabled,
|
|
29
|
+
} from "@barricador/react-client";
|
|
30
|
+
|
|
31
|
+
function App() {
|
|
32
|
+
return (
|
|
33
|
+
<BarricadorProvider
|
|
34
|
+
clientKey="sdk-cli-..."
|
|
35
|
+
user={{ key: "user-123", email: "user@enterprise.com", custom: { plan: "pro" } }}
|
|
36
|
+
baseUrl="https://app.barricador.com"
|
|
37
|
+
>
|
|
38
|
+
<BarricadorErrorBoundary fallback={<Classic />}>
|
|
39
|
+
<Home />
|
|
40
|
+
</BarricadorErrorBoundary>
|
|
41
|
+
</BarricadorProvider>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function Home() {
|
|
46
|
+
const newCheckout = useFeatureFlagEnabled("new-checkout", false);
|
|
47
|
+
const theme = useFeatureFlag<string>("homepage-theme", "control");
|
|
48
|
+
return newCheckout ? <NewCheckout theme={theme} /> : <Classic />;
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Re-render discipline
|
|
53
|
+
|
|
54
|
+
`useFeatureFlag` uses `useSyncExternalStore` with a **per-key subscription**, so a component
|
|
55
|
+
re-renders only when *its* flag value changes — not on every flag update.
|
|
56
|
+
|
|
57
|
+
## Resilience
|
|
58
|
+
|
|
59
|
+
Network/eval failures never throw: the SDK keeps the last values and unknown flags return the
|
|
60
|
+
provided fallback. `BarricadorErrorBoundary` guarantees the UI still renders a safe default if
|
|
61
|
+
flag-driven rendering throws. `useBarricadorStatus()` exposes `initializing | ready | offline`.
|
|
62
|
+
|
|
63
|
+
## Build
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
npm install
|
|
67
|
+
npm run build # emits dist/ (.js + .d.ts)
|
|
68
|
+
npm run typecheck
|
|
69
|
+
```
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Component, type ErrorInfo, type ReactNode } from "react";
|
|
2
|
+
export interface BarricadorErrorBoundaryProps {
|
|
3
|
+
/** Rendered if a descendant throws during render. */
|
|
4
|
+
fallback: ReactNode;
|
|
5
|
+
onError?: (error: Error, info: ErrorInfo) => void;
|
|
6
|
+
children: ReactNode;
|
|
7
|
+
}
|
|
8
|
+
interface State {
|
|
9
|
+
hasError: boolean;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Guarantees the UI degrades gracefully: if flag-driven rendering throws, the boundary shows the
|
|
13
|
+
* provided fallback instead of crashing the tree. Combined with `useFeatureFlag`'s fallback values
|
|
14
|
+
* (returned on any network/eval failure), the app always has a safe default to render.
|
|
15
|
+
*/
|
|
16
|
+
export declare class BarricadorErrorBoundary extends Component<BarricadorErrorBoundaryProps, State> {
|
|
17
|
+
constructor(props: BarricadorErrorBoundaryProps);
|
|
18
|
+
static getDerivedStateFromError(): State;
|
|
19
|
+
componentDidCatch(error: Error, info: ErrorInfo): void;
|
|
20
|
+
render(): ReactNode;
|
|
21
|
+
}
|
|
22
|
+
export {};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Component } from "react";
|
|
2
|
+
/**
|
|
3
|
+
* Guarantees the UI degrades gracefully: if flag-driven rendering throws, the boundary shows the
|
|
4
|
+
* provided fallback instead of crashing the tree. Combined with `useFeatureFlag`'s fallback values
|
|
5
|
+
* (returned on any network/eval failure), the app always has a safe default to render.
|
|
6
|
+
*/
|
|
7
|
+
export class BarricadorErrorBoundary extends Component {
|
|
8
|
+
constructor(props) {
|
|
9
|
+
super(props);
|
|
10
|
+
this.state = { hasError: false };
|
|
11
|
+
}
|
|
12
|
+
static getDerivedStateFromError() {
|
|
13
|
+
return { hasError: true };
|
|
14
|
+
}
|
|
15
|
+
componentDidCatch(error, info) {
|
|
16
|
+
this.props.onError?.(error, info);
|
|
17
|
+
}
|
|
18
|
+
render() {
|
|
19
|
+
return this.state.hasError ? this.props.fallback : this.props.children;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import type { UserContext } from "./types";
|
|
3
|
+
export interface BarricadorProviderProps {
|
|
4
|
+
clientKey: string;
|
|
5
|
+
user: UserContext;
|
|
6
|
+
baseUrl?: string;
|
|
7
|
+
streaming?: boolean;
|
|
8
|
+
telemetry?: boolean;
|
|
9
|
+
flushIntervalMs?: number;
|
|
10
|
+
/** Optionally gate children until the first evaluation completes. */
|
|
11
|
+
fallback?: ReactNode;
|
|
12
|
+
children: ReactNode;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Wraps the app, owns the {@link BarricadorStore} lifecycle, and re-evaluates when the user identity
|
|
16
|
+
* changes. Establishes the SSE connection and telemetry flushing. Never blocks rendering: children
|
|
17
|
+
* render immediately and flags resolve to their fallbacks until the first eval lands (unless a
|
|
18
|
+
* `fallback` node is provided to gate on readiness).
|
|
19
|
+
*/
|
|
20
|
+
export declare function BarricadorProvider({ clientKey, user, baseUrl, streaming, telemetry, flushIntervalMs, fallback, children, }: BarricadorProviderProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef, useState } from "react";
|
|
3
|
+
import { BarricadorContext } from "./context";
|
|
4
|
+
import { BarricadorStore } from "./store";
|
|
5
|
+
/**
|
|
6
|
+
* Wraps the app, owns the {@link BarricadorStore} lifecycle, and re-evaluates when the user identity
|
|
7
|
+
* changes. Establishes the SSE connection and telemetry flushing. Never blocks rendering: children
|
|
8
|
+
* render immediately and flags resolve to their fallbacks until the first eval lands (unless a
|
|
9
|
+
* `fallback` node is provided to gate on readiness).
|
|
10
|
+
*/
|
|
11
|
+
export function BarricadorProvider({ clientKey, user, baseUrl, streaming, telemetry, flushIntervalMs, fallback, children, }) {
|
|
12
|
+
const storeRef = useRef(null);
|
|
13
|
+
if (storeRef.current === null) {
|
|
14
|
+
storeRef.current = new BarricadorStore({
|
|
15
|
+
clientKey,
|
|
16
|
+
user,
|
|
17
|
+
baseUrl,
|
|
18
|
+
streaming,
|
|
19
|
+
telemetry,
|
|
20
|
+
flushIntervalMs,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
const [ready, setReady] = useState(false);
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
const store = storeRef.current;
|
|
26
|
+
void store.start().finally(() => setReady(true));
|
|
27
|
+
return () => store.close();
|
|
28
|
+
// Intentionally run once; identity changes are handled by the effect below.
|
|
29
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
30
|
+
}, []);
|
|
31
|
+
// Re-evaluate when the user identity (key) changes — e.g. after login/logout.
|
|
32
|
+
const userKey = user.key;
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
if (ready)
|
|
35
|
+
void storeRef.current.identify(user);
|
|
36
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
37
|
+
}, [userKey]);
|
|
38
|
+
if (fallback !== undefined && !ready) {
|
|
39
|
+
return _jsx(_Fragment, { children: fallback });
|
|
40
|
+
}
|
|
41
|
+
return (_jsx(BarricadorContext.Provider, { value: storeRef.current, children: children }));
|
|
42
|
+
}
|
package/dist/context.js
ADDED
package/dist/hooks.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ConnectionStatus, FlagValue } from "./types";
|
|
2
|
+
/**
|
|
3
|
+
* Read a single flag's evaluated value. Backed by {@link useSyncExternalStore} with a per-key
|
|
4
|
+
* subscription, so a component using this hook re-renders ONLY when this flag's value changes — not
|
|
5
|
+
* when any other flag updates. Returns `fallback` until the value is known (or on any failure).
|
|
6
|
+
*/
|
|
7
|
+
export declare function useFeatureFlag<T extends FlagValue>(key: string, fallback: T): T;
|
|
8
|
+
/** Convenience boolean variant. */
|
|
9
|
+
export declare function useFeatureFlagEnabled(key: string, fallback?: boolean): boolean;
|
|
10
|
+
/** Observe the SDK connection status (initializing / ready / offline). */
|
|
11
|
+
export declare function useBarricadorStatus(): ConnectionStatus;
|
package/dist/hooks.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { useCallback, useContext, useSyncExternalStore } from "react";
|
|
2
|
+
import { BarricadorContext } from "./context";
|
|
3
|
+
function useStore() {
|
|
4
|
+
const store = useContext(BarricadorContext);
|
|
5
|
+
if (!store) {
|
|
6
|
+
throw new Error("useFeatureFlag must be used within a <BarricadorProvider>");
|
|
7
|
+
}
|
|
8
|
+
return store;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Read a single flag's evaluated value. Backed by {@link useSyncExternalStore} with a per-key
|
|
12
|
+
* subscription, so a component using this hook re-renders ONLY when this flag's value changes — not
|
|
13
|
+
* when any other flag updates. Returns `fallback` until the value is known (or on any failure).
|
|
14
|
+
*/
|
|
15
|
+
export function useFeatureFlag(key, fallback) {
|
|
16
|
+
const store = useStore();
|
|
17
|
+
const subscribe = useCallback((cb) => store.subscribeKey(key, cb), [store, key]);
|
|
18
|
+
const getSnapshot = useCallback(() => store.getValue(key, fallback), [store, key, fallback]);
|
|
19
|
+
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
20
|
+
}
|
|
21
|
+
/** Convenience boolean variant. */
|
|
22
|
+
export function useFeatureFlagEnabled(key, fallback = false) {
|
|
23
|
+
const value = useFeatureFlag(key, fallback);
|
|
24
|
+
return typeof value === "boolean" ? value : fallback;
|
|
25
|
+
}
|
|
26
|
+
/** Observe the SDK connection status (initializing / ready / offline). */
|
|
27
|
+
export function useBarricadorStatus() {
|
|
28
|
+
const store = useStore();
|
|
29
|
+
const subscribe = useCallback((cb) => store.subscribeStatus(cb), [store]);
|
|
30
|
+
const getSnapshot = useCallback(() => store.getStatus(), [store]);
|
|
31
|
+
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
32
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { BarricadorProvider } from "./BarricadorProvider";
|
|
2
|
+
export type { BarricadorProviderProps } from "./BarricadorProvider";
|
|
3
|
+
export { BarricadorErrorBoundary } from "./BarricadorErrorBoundary";
|
|
4
|
+
export type { BarricadorErrorBoundaryProps } from "./BarricadorErrorBoundary";
|
|
5
|
+
export { useFeatureFlag, useFeatureFlagEnabled, useBarricadorStatus } from "./hooks";
|
|
6
|
+
export { BarricadorStore } from "./store";
|
|
7
|
+
export type { BarricadorClientOptions, ConnectionStatus, EvalResponse, FlagValue, UserContext, } from "./types";
|
package/dist/index.js
ADDED
package/dist/store.d.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { BarricadorClientOptions, ConnectionStatus, FlagValue, UserContext } from "./types";
|
|
2
|
+
type Listener = () => void;
|
|
3
|
+
/**
|
|
4
|
+
* Framework-agnostic core of the client SDK.
|
|
5
|
+
*
|
|
6
|
+
* Security model: the browser holds a low-privilege `clientKey` and POSTs the {@link UserContext} to
|
|
7
|
+
* the backend's `/api/v1/flags/eval`. The server resolves all targeting internally and returns a
|
|
8
|
+
* flattened `key -> value` map — raw rules never reach the client. An SSE connection signals when the
|
|
9
|
+
* environment changes so the SDK re-fetches the evaluated map. Telemetry is buffered and flushed
|
|
10
|
+
* asynchronously. Reads are O(1) and never throw: on any failure the SDK keeps the last values, and
|
|
11
|
+
* unknown flags fall back to the caller-provided default in {@link getValue}.
|
|
12
|
+
*/
|
|
13
|
+
export declare class BarricadorStore {
|
|
14
|
+
private readonly baseUrl;
|
|
15
|
+
private readonly clientKey;
|
|
16
|
+
private user;
|
|
17
|
+
private readonly streaming;
|
|
18
|
+
private readonly telemetry;
|
|
19
|
+
private readonly flushIntervalMs;
|
|
20
|
+
private values;
|
|
21
|
+
private status;
|
|
22
|
+
/** Per-key listeners so a component only re-renders when *its* flag changes. */
|
|
23
|
+
private readonly keyListeners;
|
|
24
|
+
private readonly statusListeners;
|
|
25
|
+
private readonly evalCounts;
|
|
26
|
+
private eventSource;
|
|
27
|
+
private flushTimer;
|
|
28
|
+
private closed;
|
|
29
|
+
constructor(options: BarricadorClientOptions);
|
|
30
|
+
start(): Promise<void>;
|
|
31
|
+
/** Re-post the user context and swap in the freshly evaluated values. */
|
|
32
|
+
refresh(): Promise<void>;
|
|
33
|
+
/** Update the identified user and re-evaluate (e.g. after login). */
|
|
34
|
+
identify(user: UserContext): Promise<void>;
|
|
35
|
+
getValue(key: string, fallback: FlagValue): FlagValue;
|
|
36
|
+
getStatus(): ConnectionStatus;
|
|
37
|
+
subscribeKey(key: string, listener: Listener): () => void;
|
|
38
|
+
subscribeStatus(listener: Listener): () => void;
|
|
39
|
+
private applyValues;
|
|
40
|
+
private notifyKey;
|
|
41
|
+
private setStatus;
|
|
42
|
+
private connectStream;
|
|
43
|
+
flush(): Promise<void>;
|
|
44
|
+
close(): void;
|
|
45
|
+
}
|
|
46
|
+
export {};
|
package/dist/store.js
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Framework-agnostic core of the client SDK.
|
|
3
|
+
*
|
|
4
|
+
* Security model: the browser holds a low-privilege `clientKey` and POSTs the {@link UserContext} to
|
|
5
|
+
* the backend's `/api/v1/flags/eval`. The server resolves all targeting internally and returns a
|
|
6
|
+
* flattened `key -> value` map — raw rules never reach the client. An SSE connection signals when the
|
|
7
|
+
* environment changes so the SDK re-fetches the evaluated map. Telemetry is buffered and flushed
|
|
8
|
+
* asynchronously. Reads are O(1) and never throw: on any failure the SDK keeps the last values, and
|
|
9
|
+
* unknown flags fall back to the caller-provided default in {@link getValue}.
|
|
10
|
+
*/
|
|
11
|
+
export class BarricadorStore {
|
|
12
|
+
constructor(options) {
|
|
13
|
+
this.values = {};
|
|
14
|
+
this.status = "initializing";
|
|
15
|
+
/** Per-key listeners so a component only re-renders when *its* flag changes. */
|
|
16
|
+
this.keyListeners = new Map();
|
|
17
|
+
this.statusListeners = new Set();
|
|
18
|
+
this.evalCounts = new Map();
|
|
19
|
+
this.eventSource = null;
|
|
20
|
+
this.flushTimer = null;
|
|
21
|
+
this.closed = false;
|
|
22
|
+
if (!options.clientKey)
|
|
23
|
+
throw new Error("clientKey is required");
|
|
24
|
+
if (!options.user?.key)
|
|
25
|
+
throw new Error("user.key is required");
|
|
26
|
+
this.clientKey = options.clientKey;
|
|
27
|
+
this.user = options.user;
|
|
28
|
+
this.baseUrl = (options.baseUrl ?? "https://app.barricador.com").replace(/\/$/, "");
|
|
29
|
+
this.streaming = options.streaming ?? true;
|
|
30
|
+
this.telemetry = options.telemetry ?? true;
|
|
31
|
+
this.flushIntervalMs = options.flushIntervalMs ?? 30000;
|
|
32
|
+
}
|
|
33
|
+
async start() {
|
|
34
|
+
await this.refresh();
|
|
35
|
+
if (this.streaming)
|
|
36
|
+
this.connectStream();
|
|
37
|
+
if (this.telemetry && typeof setInterval !== "undefined") {
|
|
38
|
+
this.flushTimer = setInterval(() => void this.flush(), this.flushIntervalMs);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/** Re-post the user context and swap in the freshly evaluated values. */
|
|
42
|
+
async refresh() {
|
|
43
|
+
try {
|
|
44
|
+
const res = await fetch(`${this.baseUrl}/api/v1/flags/eval`, {
|
|
45
|
+
method: "POST",
|
|
46
|
+
headers: {
|
|
47
|
+
Authorization: `Bearer ${this.clientKey}`,
|
|
48
|
+
"Content-Type": "application/json",
|
|
49
|
+
},
|
|
50
|
+
body: JSON.stringify(this.user),
|
|
51
|
+
});
|
|
52
|
+
if (!res.ok)
|
|
53
|
+
throw new Error(`eval failed: HTTP ${res.status}`);
|
|
54
|
+
const body = (await res.json());
|
|
55
|
+
this.applyValues(body.values ?? {});
|
|
56
|
+
this.setStatus("ready");
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
// Network/eval failure: keep last values; hooks fall back to defaults. Never throw.
|
|
60
|
+
this.setStatus(this.status === "initializing" ? "offline" : this.status);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/** Update the identified user and re-evaluate (e.g. after login). */
|
|
64
|
+
async identify(user) {
|
|
65
|
+
this.user = user;
|
|
66
|
+
await this.refresh();
|
|
67
|
+
}
|
|
68
|
+
getValue(key, fallback) {
|
|
69
|
+
if (this.telemetry)
|
|
70
|
+
this.evalCounts.set(key, (this.evalCounts.get(key) ?? 0) + 1);
|
|
71
|
+
return key in this.values ? this.values[key] : fallback;
|
|
72
|
+
}
|
|
73
|
+
getStatus() {
|
|
74
|
+
return this.status;
|
|
75
|
+
}
|
|
76
|
+
// --- subscriptions (drive useSyncExternalStore) ---
|
|
77
|
+
subscribeKey(key, listener) {
|
|
78
|
+
let set = this.keyListeners.get(key);
|
|
79
|
+
if (!set) {
|
|
80
|
+
set = new Set();
|
|
81
|
+
this.keyListeners.set(key, set);
|
|
82
|
+
}
|
|
83
|
+
set.add(listener);
|
|
84
|
+
return () => set.delete(listener);
|
|
85
|
+
}
|
|
86
|
+
subscribeStatus(listener) {
|
|
87
|
+
this.statusListeners.add(listener);
|
|
88
|
+
return () => this.statusListeners.delete(listener);
|
|
89
|
+
}
|
|
90
|
+
// --- internals ---
|
|
91
|
+
applyValues(next) {
|
|
92
|
+
const previous = this.values;
|
|
93
|
+
this.values = next;
|
|
94
|
+
const changed = new Set([...Object.keys(previous), ...Object.keys(next)]);
|
|
95
|
+
for (const key of changed) {
|
|
96
|
+
if (!shallowEqual(previous[key], next[key]))
|
|
97
|
+
this.notifyKey(key);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
notifyKey(key) {
|
|
101
|
+
this.keyListeners.get(key)?.forEach((l) => l());
|
|
102
|
+
}
|
|
103
|
+
setStatus(status) {
|
|
104
|
+
if (this.status !== status) {
|
|
105
|
+
this.status = status;
|
|
106
|
+
this.statusListeners.forEach((l) => l());
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
connectStream() {
|
|
110
|
+
if (typeof EventSource === "undefined" || this.closed)
|
|
111
|
+
return;
|
|
112
|
+
// EventSource cannot set Authorization headers, so the client key travels as a query param.
|
|
113
|
+
const url = `${this.baseUrl}/api/v1/flags/stream?clientKey=${encodeURIComponent(this.clientKey)}`;
|
|
114
|
+
try {
|
|
115
|
+
const es = new EventSource(url);
|
|
116
|
+
this.eventSource = es;
|
|
117
|
+
// On any flag change in the environment, re-fetch the pre-evaluated map for this user.
|
|
118
|
+
es.addEventListener("flag-change", () => void this.refresh());
|
|
119
|
+
es.addEventListener("connected", () => this.setStatus("ready"));
|
|
120
|
+
es.onerror = () => {
|
|
121
|
+
// EventSource auto-reconnects with its own backoff; reflect the transient outage.
|
|
122
|
+
this.setStatus("offline");
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
this.setStatus("offline");
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
async flush() {
|
|
130
|
+
if (!this.telemetry || this.evalCounts.size === 0)
|
|
131
|
+
return;
|
|
132
|
+
const events = [...this.evalCounts.entries()].map(([flagKey, count]) => ({
|
|
133
|
+
flagKey,
|
|
134
|
+
count,
|
|
135
|
+
defaulted: !(flagKey in this.values),
|
|
136
|
+
}));
|
|
137
|
+
this.evalCounts.clear();
|
|
138
|
+
try {
|
|
139
|
+
await fetch(`${this.baseUrl}/api/v1/metrics/flush`, {
|
|
140
|
+
method: "POST",
|
|
141
|
+
headers: {
|
|
142
|
+
Authorization: `Bearer ${this.clientKey}`,
|
|
143
|
+
"Content-Type": "application/json",
|
|
144
|
+
},
|
|
145
|
+
body: JSON.stringify({ events }),
|
|
146
|
+
keepalive: true,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
// Telemetry is best-effort; drop on failure.
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
close() {
|
|
154
|
+
this.closed = true;
|
|
155
|
+
if (this.flushTimer)
|
|
156
|
+
clearInterval(this.flushTimer);
|
|
157
|
+
this.eventSource?.close();
|
|
158
|
+
void this.flush();
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
function shallowEqual(a, b) {
|
|
162
|
+
if (Object.is(a, b))
|
|
163
|
+
return true;
|
|
164
|
+
if (typeof a !== "object" || typeof b !== "object" || a === null || b === null)
|
|
165
|
+
return false;
|
|
166
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
167
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** Public types for the Barricador React client SDK. */
|
|
2
|
+
export type FlagValue = boolean | string | number | Record<string, unknown> | unknown[] | null;
|
|
3
|
+
/** The evaluation subject. `key` must be stable per subject for consistent rollouts. */
|
|
4
|
+
export interface UserContext {
|
|
5
|
+
key: string;
|
|
6
|
+
name?: string;
|
|
7
|
+
email?: string;
|
|
8
|
+
country?: string;
|
|
9
|
+
anonymous?: boolean;
|
|
10
|
+
custom?: Record<string, unknown>;
|
|
11
|
+
}
|
|
12
|
+
export interface BarricadorClientOptions {
|
|
13
|
+
clientKey: string;
|
|
14
|
+
user: UserContext;
|
|
15
|
+
baseUrl?: string;
|
|
16
|
+
/** Enable the live SSE connection (default true). */
|
|
17
|
+
streaming?: boolean;
|
|
18
|
+
/** Enable async telemetry flushing (default true). */
|
|
19
|
+
telemetry?: boolean;
|
|
20
|
+
/** Telemetry flush cadence in ms (default 30000). */
|
|
21
|
+
flushIntervalMs?: number;
|
|
22
|
+
}
|
|
23
|
+
/** Server response from POST /api/v1/flags/eval — only pre-evaluated values, never raw rules. */
|
|
24
|
+
export interface EvalResponse {
|
|
25
|
+
environmentId: string;
|
|
26
|
+
rulesVersion: number;
|
|
27
|
+
values: Record<string, FlagValue>;
|
|
28
|
+
details?: Record<string, {
|
|
29
|
+
variationId?: string;
|
|
30
|
+
reason?: string;
|
|
31
|
+
version?: number;
|
|
32
|
+
}>;
|
|
33
|
+
}
|
|
34
|
+
export type ConnectionStatus = "initializing" | "ready" | "offline";
|
package/dist/types.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@barricador/react-client",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Barricador client-side React SDK (pre-evaluated context model, SSE sync, telemetry)",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
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
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"sideEffects": false,
|
|
16
|
+
"files": [
|
|
17
|
+
"dist"
|
|
18
|
+
],
|
|
19
|
+
"keywords": [
|
|
20
|
+
"feature-flags",
|
|
21
|
+
"barricador",
|
|
22
|
+
"react",
|
|
23
|
+
"sdk",
|
|
24
|
+
"rollout",
|
|
25
|
+
"ab-testing"
|
|
26
|
+
],
|
|
27
|
+
"homepage": "https://github.com/barricador/barricador-react-client",
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "git+https://github.com/barricador/barricador-react-client.git"
|
|
31
|
+
},
|
|
32
|
+
"bugs": {
|
|
33
|
+
"url": "https://github.com/barricador/barricador-react-client/issues"
|
|
34
|
+
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "tsc -p tsconfig.json",
|
|
37
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
38
|
+
"prepublishOnly": "npm run build"
|
|
39
|
+
},
|
|
40
|
+
"peerDependencies": {
|
|
41
|
+
"react": ">=18.0.0"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@types/react": "^18.3.3",
|
|
45
|
+
"react": "^18.3.1",
|
|
46
|
+
"typescript": "^5.5.4"
|
|
47
|
+
},
|
|
48
|
+
"publishConfig": {
|
|
49
|
+
"access": "public",
|
|
50
|
+
"provenance": true
|
|
51
|
+
},
|
|
52
|
+
"license": "Apache-2.0"
|
|
53
|
+
}
|