@mergedapp/feature-flags 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/react.cjs ADDED
@@ -0,0 +1,239 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
+
21
+ // src/react.ts
22
+ var react_exports = {};
23
+ __export(react_exports, {
24
+ FeatureFlagProvider: () => FeatureFlagProvider,
25
+ createTypedHooks: () => createTypedHooks
26
+ });
27
+ module.exports = __toCommonJS(react_exports);
28
+
29
+ // src/react/provider.tsx
30
+ var import_react = require("react");
31
+ var FeatureFlagContext = /* @__PURE__ */ (0, import_react.createContext)(null);
32
+ function createFeatureFlagLoadState(params) {
33
+ return {
34
+ status: params.status,
35
+ isLoading: params.status === "loading",
36
+ isReady: params.status === "ready",
37
+ error: params.error ?? params.runtimeStatus.lastError,
38
+ source: params.runtimeStatus.source,
39
+ isStale: params.runtimeStatus.isStale,
40
+ lastSuccessfulRefreshAt: params.runtimeStatus.lastSuccessfulRefreshAt,
41
+ tokenExpiresAt: params.runtimeStatus.tokenExpiresAt
42
+ };
43
+ }
44
+ __name(createFeatureFlagLoadState, "createFeatureFlagLoadState");
45
+ function shouldRenderFeatureFlagLoadingFallback(params) {
46
+ return params.blockUntilReady && params.loadState.isLoading;
47
+ }
48
+ __name(shouldRenderFeatureFlagLoadingFallback, "shouldRenderFeatureFlagLoadingFallback");
49
+ function createSnapshotStore(params) {
50
+ let cachedFlags = params.initialFlags ?? params.client.getSnapshot();
51
+ let hasObservedClientUpdate = false;
52
+ function areSnapshotsEqual(left, right) {
53
+ if (left.length !== right.length) {
54
+ return false;
55
+ }
56
+ for (let index = 0; index < left.length; index++) {
57
+ const leftFlag = left[index];
58
+ const rightFlag = right[index];
59
+ if (leftFlag?.id !== rightFlag?.id || leftFlag?.name !== rightFlag?.name || leftFlag?.type !== rightFlag?.type || leftFlag?.teamId !== rightFlag?.teamId || leftFlag?.enabled !== rightFlag?.enabled || JSON.stringify(leftFlag?.value) !== JSON.stringify(rightFlag?.value)) {
60
+ return false;
61
+ }
62
+ }
63
+ return true;
64
+ }
65
+ __name(areSnapshotsEqual, "areSnapshotsEqual");
66
+ return {
67
+ subscribe(onStoreChange) {
68
+ return params.client.subscribe(() => {
69
+ hasObservedClientUpdate = true;
70
+ onStoreChange();
71
+ });
72
+ },
73
+ getSnapshot() {
74
+ const current = params.client.getSnapshot();
75
+ const shouldAdoptClientSnapshot = current.length > 0 || !params.initialFlags || hasObservedClientUpdate;
76
+ if (shouldAdoptClientSnapshot && !areSnapshotsEqual(current, cachedFlags)) {
77
+ cachedFlags = current;
78
+ }
79
+ return cachedFlags;
80
+ },
81
+ getServerSnapshot() {
82
+ return params.initialFlags ?? [];
83
+ }
84
+ };
85
+ }
86
+ __name(createSnapshotStore, "createSnapshotStore");
87
+ function FeatureFlagProvider(props) {
88
+ const { client, blockUntilReady, loadingFallback, initialFlags, children } = props;
89
+ const [status, setStatus] = (0, import_react.useState)(initialFlags ? "ready" : "loading");
90
+ const [error, setError] = (0, import_react.useState)(null);
91
+ (0, import_react.useEffect)(() => {
92
+ let isDisposed = false;
93
+ setStatus(initialFlags ? "ready" : "loading");
94
+ setError(null);
95
+ client.initialize().then(() => {
96
+ if (isDisposed) {
97
+ return;
98
+ }
99
+ setStatus("ready");
100
+ }).catch((initializationError) => {
101
+ if (isDisposed) {
102
+ return;
103
+ }
104
+ setStatus("error");
105
+ setError(initializationError instanceof Error ? initializationError : new Error("Unknown feature flag initialization error."));
106
+ });
107
+ return () => {
108
+ isDisposed = true;
109
+ client.destroy();
110
+ };
111
+ }, [
112
+ client,
113
+ initialFlags
114
+ ]);
115
+ const snapshotStore = (0, import_react.useMemo)(() => createSnapshotStore({
116
+ client,
117
+ initialFlags
118
+ }), [
119
+ client,
120
+ initialFlags
121
+ ]);
122
+ const flags = (0, import_react.useSyncExternalStore)(snapshotStore.subscribe, snapshotStore.getSnapshot, snapshotStore.getServerSnapshot);
123
+ const runtimeStatus = (0, import_react.useSyncExternalStore)(client.subscribe, client.getStatus.bind(client), client.getStatus.bind(client));
124
+ const loadState = (0, import_react.useMemo)(() => createFeatureFlagLoadState({
125
+ status,
126
+ error,
127
+ runtimeStatus
128
+ }), [
129
+ error,
130
+ runtimeStatus,
131
+ status
132
+ ]);
133
+ const value = (0, import_react.useMemo)(() => ({
134
+ client,
135
+ flags,
136
+ loadState
137
+ }), [
138
+ client,
139
+ flags,
140
+ loadState
141
+ ]);
142
+ if (shouldRenderFeatureFlagLoadingFallback({
143
+ blockUntilReady,
144
+ loadState
145
+ })) {
146
+ return /* @__PURE__ */ React.createElement(React.Fragment, null, loadingFallback ?? null);
147
+ }
148
+ return /* @__PURE__ */ React.createElement(FeatureFlagContext.Provider, {
149
+ value
150
+ }, children);
151
+ }
152
+ __name(FeatureFlagProvider, "FeatureFlagProvider");
153
+
154
+ // src/react/hooks.ts
155
+ var import_react2 = require("react");
156
+ function useFeatureFlagContext() {
157
+ const context = (0, import_react2.useContext)(FeatureFlagContext);
158
+ if (!context) {
159
+ throw new Error("Feature flag hooks and components must be used within a <FeatureFlagProvider>.");
160
+ }
161
+ return context;
162
+ }
163
+ __name(useFeatureFlagContext, "useFeatureFlagContext");
164
+ function resolveFlagFromContext(params) {
165
+ return params.flags.find((flag) => flag.id === params.idOrName || flag.name === params.idOrName) ?? params.client.getFlag(params.idOrName);
166
+ }
167
+ __name(resolveFlagFromContext, "resolveFlagFromContext");
168
+ function useFeatureFlagStatus() {
169
+ const { loadState } = useFeatureFlagContext();
170
+ return loadState;
171
+ }
172
+ __name(useFeatureFlagStatus, "useFeatureFlagStatus");
173
+ function createTypedHooks() {
174
+ function TypedFeatureFlagProvider(props) {
175
+ return FeatureFlagProvider(props);
176
+ }
177
+ __name(TypedFeatureFlagProvider, "TypedFeatureFlagProvider");
178
+ function typedUseFeatureFlag(name) {
179
+ const { client, flags } = useFeatureFlagContext();
180
+ const flag = resolveFlagFromContext({
181
+ client,
182
+ flags,
183
+ idOrName: name
184
+ });
185
+ return {
186
+ enabled: flag?.enabled ?? false,
187
+ value: flag?.value
188
+ };
189
+ }
190
+ __name(typedUseFeatureFlag, "typedUseFeatureFlag");
191
+ function typedUseFeatureFlags() {
192
+ const { flags } = useFeatureFlagContext();
193
+ return flags;
194
+ }
195
+ __name(typedUseFeatureFlags, "typedUseFeatureFlags");
196
+ function typedUseFeatureFlagClient() {
197
+ const { client } = useFeatureFlagContext();
198
+ return client;
199
+ }
200
+ __name(typedUseFeatureFlagClient, "typedUseFeatureFlagClient");
201
+ function typedUseFeatureFlagStatus() {
202
+ return useFeatureFlagStatus();
203
+ }
204
+ __name(typedUseFeatureFlagStatus, "typedUseFeatureFlagStatus");
205
+ const TypedFeatureFlag = /* @__PURE__ */ __name((props) => {
206
+ const { children, fallback, loading, matchValue, name } = props;
207
+ const status = typedUseFeatureFlagStatus();
208
+ const { enabled, value } = typedUseFeatureFlag(name);
209
+ if (status.isLoading) {
210
+ return loading ?? fallback ?? null;
211
+ }
212
+ const shouldRenderChildren = typeof matchValue === "undefined" ? enabled : Object.is(value, matchValue);
213
+ if (!shouldRenderChildren) {
214
+ return fallback ?? null;
215
+ }
216
+ if (typeof children === "function") {
217
+ return children({
218
+ enabled,
219
+ value,
220
+ status
221
+ });
222
+ }
223
+ return children;
224
+ }, "TypedFeatureFlag");
225
+ return {
226
+ FeatureFlagProvider: TypedFeatureFlagProvider,
227
+ FeatureFlag: TypedFeatureFlag,
228
+ useFeatureFlag: typedUseFeatureFlag,
229
+ useFeatureFlags: typedUseFeatureFlags,
230
+ useFeatureFlagClient: typedUseFeatureFlagClient,
231
+ useFeatureFlagStatus: typedUseFeatureFlagStatus
232
+ };
233
+ }
234
+ __name(createTypedHooks, "createTypedHooks");
235
+ // Annotate the CommonJS export names for ESM import in node:
236
+ 0 && (module.exports = {
237
+ FeatureFlagProvider,
238
+ createTypedHooks
239
+ });
@@ -0,0 +1,223 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { ReactNode } from 'react';
3
+ import * as _repo_types_v2 from '@repo/types-v2';
4
+ import { EvaluatedFlag, FeatureFlagEvaluationContext } from '@repo/types-v2';
5
+
6
+ /** Base type for a flag registry mapping flag names to their value types. */
7
+ type FlagRegistry = object;
8
+ type FeatureFlagRuntimeSource = "network" | "persisted" | "defaults";
9
+ type FeatureFlagRuntimeStatus = {
10
+ source: FeatureFlagRuntimeSource;
11
+ isStale: boolean;
12
+ lastSuccessfulRefreshAt: string | null;
13
+ tokenExpiresAt: string | null;
14
+ lastError: Error | null;
15
+ };
16
+ type PersistedFeatureFlagSnapshot = {
17
+ schemaVersion: number;
18
+ scopeKey: string;
19
+ organizationId: string;
20
+ environmentId: string;
21
+ teamId: string | null;
22
+ clientKeyFingerprint: string;
23
+ contextHash: string;
24
+ token: string;
25
+ publicKeyPem: string;
26
+ fetchedAt: string;
27
+ tokenExpiresAt: string | null;
28
+ };
29
+ type FeatureFlagSnapshotStore = {
30
+ load(key: string): Promise<PersistedFeatureFlagSnapshot | null>;
31
+ save(key: string, snapshot: PersistedFeatureFlagSnapshot): Promise<void>;
32
+ remove(key: string): Promise<void>;
33
+ };
34
+ type FeatureFlagSnapshotPersistenceConfig = {
35
+ store?: FeatureFlagSnapshotStore;
36
+ keyPrefix?: string;
37
+ };
38
+ type FeatureFlagClientConfig<TFlags extends FlagRegistry = FlagRegistry> = {
39
+ /** API key with `lk_pub_*` or `lk_sec_*` prefix */
40
+ clientKey: string;
41
+ /** Base URL for the API (e.g. "https://api.merged.gg") */
42
+ apiUrl: string;
43
+ /** Organization scope for signed flag evaluation */
44
+ organizationId: string;
45
+ /** Environment scope for signed flag evaluation */
46
+ environmentId: string;
47
+ /** Optional team scope for signed flag evaluation */
48
+ teamId?: string;
49
+ /** Code-key-to-ID mapping from generated code. Enables type-safe lookups resolved to stable IDs. */
50
+ flagIds?: {
51
+ readonly [K in string & keyof TFlags]: string;
52
+ };
53
+ /** PEM-format ES256 public key. If omitted, auto-fetched from the public-key endpoint. */
54
+ publicKey?: string;
55
+ /** Polling interval in milliseconds. Default: 60_000 (60s). Set to 0 to disable. */
56
+ refreshInterval?: number;
57
+ /** Optional caller-provided evaluation context sent to the API when resolving flags. */
58
+ evaluationContext?: _repo_types_v2.FeatureFlagEvaluationContext;
59
+ /** Controls automatic persistence of the last successful signed evaluation. */
60
+ snapshotPersistence?: false | FeatureFlagSnapshotPersistenceConfig;
61
+ /** Called when an error occurs during refresh or verification */
62
+ onError?: (error: Error) => void;
63
+ /** Called when flags change after a refresh */
64
+ onFlagsChanged?: (flags: _repo_types_v2.EvaluatedFlag[]) => void;
65
+ };
66
+
67
+ /**
68
+ * Structural interface for the feature flag client. Using an interface instead of
69
+ * the concrete MergedFeatureFlags class avoids "separate declarations of a private
70
+ * property" errors when consumers resolve the class from a different module path
71
+ * (e.g., dist/ vs src/ in a monorepo workspace).
72
+ */
73
+ interface FeatureFlagClient {
74
+ initialize(): Promise<void>;
75
+ destroy(): void;
76
+ isEnabled(name: string): boolean;
77
+ getValue(name: string): unknown;
78
+ getFlag(name: string): EvaluatedFlag | undefined;
79
+ getAllFlags(): EvaluatedFlag[];
80
+ getSnapshot(): EvaluatedFlag[];
81
+ getStatus(): FeatureFlagRuntimeStatus;
82
+ subscribe(onStoreChange: () => void): () => void;
83
+ refresh(): Promise<void>;
84
+ }
85
+ type FeatureFlagProviderStatus = "idle" | "loading" | "ready" | "error";
86
+ type FeatureFlagLoadState = {
87
+ status: FeatureFlagProviderStatus;
88
+ isLoading: boolean;
89
+ isReady: boolean;
90
+ error: Error | null;
91
+ source: FeatureFlagRuntimeStatus["source"];
92
+ isStale: boolean;
93
+ lastSuccessfulRefreshAt: string | null;
94
+ tokenExpiresAt: string | null;
95
+ };
96
+ type FeatureFlagProviderProps = {
97
+ client: FeatureFlagClient;
98
+ blockUntilReady: boolean;
99
+ loadingFallback?: ReactNode;
100
+ /** Pre-evaluated flags for SSR hydration. Skips initial fetch on the server. */
101
+ initialFlags?: EvaluatedFlag[];
102
+ children: ReactNode;
103
+ };
104
+ declare function FeatureFlagProvider(props: FeatureFlagProviderProps): react_jsx_runtime.JSX.Element;
105
+
106
+ type ChangeListener = (flags: EvaluatedFlag[]) => void;
107
+ declare class MergedFeatureFlags<TFlags extends FlagRegistry = FlagRegistry> {
108
+ private flagsById;
109
+ private flagsByName;
110
+ private publicKeyPem;
111
+ private publicKeyCrypto;
112
+ private refreshTimer;
113
+ private backoffMs;
114
+ private consecutiveFailures;
115
+ private listeners;
116
+ private storeListeners;
117
+ private initialized;
118
+ private visibilityHandler;
119
+ private pendingScopeTransition;
120
+ private runtimeStatus;
121
+ private snapshotStorePromise;
122
+ private readonly keyPrefix;
123
+ private contextSignature;
124
+ private readonly flagIds;
125
+ private readonly clientKey;
126
+ private readonly apiUrl;
127
+ private readonly organizationId;
128
+ private readonly environmentId;
129
+ private readonly teamId;
130
+ private readonly refreshInterval;
131
+ private readonly onError;
132
+ private readonly onFlagsChanged;
133
+ private readonly snapshotPersistence;
134
+ private evaluationContext;
135
+ constructor(config: FeatureFlagClientConfig<TFlags>);
136
+ initialize(): Promise<void>;
137
+ /** Returns true if the flag is enabled. Returns false for unknown flags. */
138
+ isEnabled<K extends string & keyof TFlags>(name: K): boolean;
139
+ /** Returns the flag's value with the type inferred from the flag registry. */
140
+ getValue<K extends string & keyof TFlags>(name: K): TFlags[K] | undefined;
141
+ /** Returns the full evaluated flag entry, or undefined if not found. */
142
+ getFlag<K extends string & keyof TFlags>(name: K): EvaluatedFlag | undefined;
143
+ /** Returns all evaluated flags. */
144
+ getAllFlags(): EvaluatedFlag[];
145
+ getStatus(): FeatureFlagRuntimeStatus;
146
+ getStatusSnapshot(): FeatureFlagRuntimeStatus;
147
+ /** Manually trigger a refresh from the server. */
148
+ refresh(): Promise<void>;
149
+ /** Subscribe to flag changes. Returns an unsubscribe function. */
150
+ onChange(listener: ChangeListener): () => void;
151
+ /** Get a snapshot function for useSyncExternalStore. */
152
+ getSnapshot(): EvaluatedFlag[];
153
+ /** Replace the caller-provided evaluation context used on subsequent refreshes. */
154
+ setEvaluationContext(context: FeatureFlagEvaluationContext | null | undefined): void;
155
+ /** Subscribe function for useSyncExternalStore. */
156
+ subscribe(onStoreChange: () => void): () => void;
157
+ /** Clean up timers, listeners, and cached data. */
158
+ destroy(): void;
159
+ private resolveFlag;
160
+ private clearFlags;
161
+ private emitChange;
162
+ private updateFlags;
163
+ private hasFlagsChanged;
164
+ private setRuntimeStatus;
165
+ private fetchSignedFlags;
166
+ private fetchPublicKey;
167
+ private verifyWithRetry;
168
+ private ensureVerificationKey;
169
+ private getTrustedSnapshotVerificationKey;
170
+ private getSnapshotStore;
171
+ private getScopeParts;
172
+ private restorePersistedSnapshot;
173
+ private persistSnapshot;
174
+ private startPolling;
175
+ private scheduleNextPoll;
176
+ private safeRefresh;
177
+ private resetBackoff;
178
+ private incrementBackoff;
179
+ }
180
+
181
+ type FeatureFlagRenderState<TValue> = {
182
+ enabled: boolean;
183
+ value: TValue | undefined;
184
+ status: FeatureFlagLoadState;
185
+ };
186
+ type FeatureFlagMatchValue<TValue> = Extract<TValue, string | number | boolean>;
187
+ type TypedFeatureFlagProps<TFlags extends FlagRegistry, K extends string & keyof TFlags> = {
188
+ name: K;
189
+ children: ReactNode | ((props: FeatureFlagRenderState<TFlags[K]>) => ReactNode);
190
+ fallback?: ReactNode;
191
+ loading?: ReactNode;
192
+ matchValue?: FeatureFlagMatchValue<TFlags[K]>;
193
+ };
194
+ type TypedFeatureFlagComponent<TFlags extends FlagRegistry> = <K extends string & keyof TFlags>(props: TypedFeatureFlagProps<TFlags, K>) => ReactNode;
195
+ /**
196
+ * Creates type-safe React hooks and provider bound to a specific flag registry.
197
+ * Call this in your generated code and re-export the result.
198
+ *
199
+ * ```typescript
200
+ * // generated/feature-flags.ts
201
+ * export const { FeatureFlagProvider, FeatureFlag, useFeatureFlag, useFeatureFlags, useFeatureFlagClient, useFeatureFlagStatus } =
202
+ * createTypedHooks<FlagValues>()
203
+ * ```
204
+ */
205
+ declare function createTypedHooks<TFlags extends FlagRegistry>(): {
206
+ FeatureFlagProvider: (props: {
207
+ client: FeatureFlagClient;
208
+ blockUntilReady: boolean;
209
+ loadingFallback?: ReactNode;
210
+ initialFlags?: EvaluatedFlag[];
211
+ children: ReactNode;
212
+ }) => react_jsx_runtime.JSX.Element;
213
+ FeatureFlag: TypedFeatureFlagComponent<TFlags>;
214
+ useFeatureFlag: <K extends string & keyof TFlags>(name: K) => {
215
+ enabled: boolean;
216
+ value: TFlags[K] | undefined;
217
+ };
218
+ useFeatureFlags: () => EvaluatedFlag[];
219
+ useFeatureFlagClient: () => MergedFeatureFlags<TFlags>;
220
+ useFeatureFlagStatus: () => FeatureFlagLoadState;
221
+ };
222
+
223
+ export { type FeatureFlagClient, FeatureFlagProvider, type FeatureFlagProviderProps, createTypedHooks };
@@ -0,0 +1,223 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { ReactNode } from 'react';
3
+ import * as _repo_types_v2 from '@repo/types-v2';
4
+ import { EvaluatedFlag, FeatureFlagEvaluationContext } from '@repo/types-v2';
5
+
6
+ /** Base type for a flag registry mapping flag names to their value types. */
7
+ type FlagRegistry = object;
8
+ type FeatureFlagRuntimeSource = "network" | "persisted" | "defaults";
9
+ type FeatureFlagRuntimeStatus = {
10
+ source: FeatureFlagRuntimeSource;
11
+ isStale: boolean;
12
+ lastSuccessfulRefreshAt: string | null;
13
+ tokenExpiresAt: string | null;
14
+ lastError: Error | null;
15
+ };
16
+ type PersistedFeatureFlagSnapshot = {
17
+ schemaVersion: number;
18
+ scopeKey: string;
19
+ organizationId: string;
20
+ environmentId: string;
21
+ teamId: string | null;
22
+ clientKeyFingerprint: string;
23
+ contextHash: string;
24
+ token: string;
25
+ publicKeyPem: string;
26
+ fetchedAt: string;
27
+ tokenExpiresAt: string | null;
28
+ };
29
+ type FeatureFlagSnapshotStore = {
30
+ load(key: string): Promise<PersistedFeatureFlagSnapshot | null>;
31
+ save(key: string, snapshot: PersistedFeatureFlagSnapshot): Promise<void>;
32
+ remove(key: string): Promise<void>;
33
+ };
34
+ type FeatureFlagSnapshotPersistenceConfig = {
35
+ store?: FeatureFlagSnapshotStore;
36
+ keyPrefix?: string;
37
+ };
38
+ type FeatureFlagClientConfig<TFlags extends FlagRegistry = FlagRegistry> = {
39
+ /** API key with `lk_pub_*` or `lk_sec_*` prefix */
40
+ clientKey: string;
41
+ /** Base URL for the API (e.g. "https://api.merged.gg") */
42
+ apiUrl: string;
43
+ /** Organization scope for signed flag evaluation */
44
+ organizationId: string;
45
+ /** Environment scope for signed flag evaluation */
46
+ environmentId: string;
47
+ /** Optional team scope for signed flag evaluation */
48
+ teamId?: string;
49
+ /** Code-key-to-ID mapping from generated code. Enables type-safe lookups resolved to stable IDs. */
50
+ flagIds?: {
51
+ readonly [K in string & keyof TFlags]: string;
52
+ };
53
+ /** PEM-format ES256 public key. If omitted, auto-fetched from the public-key endpoint. */
54
+ publicKey?: string;
55
+ /** Polling interval in milliseconds. Default: 60_000 (60s). Set to 0 to disable. */
56
+ refreshInterval?: number;
57
+ /** Optional caller-provided evaluation context sent to the API when resolving flags. */
58
+ evaluationContext?: _repo_types_v2.FeatureFlagEvaluationContext;
59
+ /** Controls automatic persistence of the last successful signed evaluation. */
60
+ snapshotPersistence?: false | FeatureFlagSnapshotPersistenceConfig;
61
+ /** Called when an error occurs during refresh or verification */
62
+ onError?: (error: Error) => void;
63
+ /** Called when flags change after a refresh */
64
+ onFlagsChanged?: (flags: _repo_types_v2.EvaluatedFlag[]) => void;
65
+ };
66
+
67
+ /**
68
+ * Structural interface for the feature flag client. Using an interface instead of
69
+ * the concrete MergedFeatureFlags class avoids "separate declarations of a private
70
+ * property" errors when consumers resolve the class from a different module path
71
+ * (e.g., dist/ vs src/ in a monorepo workspace).
72
+ */
73
+ interface FeatureFlagClient {
74
+ initialize(): Promise<void>;
75
+ destroy(): void;
76
+ isEnabled(name: string): boolean;
77
+ getValue(name: string): unknown;
78
+ getFlag(name: string): EvaluatedFlag | undefined;
79
+ getAllFlags(): EvaluatedFlag[];
80
+ getSnapshot(): EvaluatedFlag[];
81
+ getStatus(): FeatureFlagRuntimeStatus;
82
+ subscribe(onStoreChange: () => void): () => void;
83
+ refresh(): Promise<void>;
84
+ }
85
+ type FeatureFlagProviderStatus = "idle" | "loading" | "ready" | "error";
86
+ type FeatureFlagLoadState = {
87
+ status: FeatureFlagProviderStatus;
88
+ isLoading: boolean;
89
+ isReady: boolean;
90
+ error: Error | null;
91
+ source: FeatureFlagRuntimeStatus["source"];
92
+ isStale: boolean;
93
+ lastSuccessfulRefreshAt: string | null;
94
+ tokenExpiresAt: string | null;
95
+ };
96
+ type FeatureFlagProviderProps = {
97
+ client: FeatureFlagClient;
98
+ blockUntilReady: boolean;
99
+ loadingFallback?: ReactNode;
100
+ /** Pre-evaluated flags for SSR hydration. Skips initial fetch on the server. */
101
+ initialFlags?: EvaluatedFlag[];
102
+ children: ReactNode;
103
+ };
104
+ declare function FeatureFlagProvider(props: FeatureFlagProviderProps): react_jsx_runtime.JSX.Element;
105
+
106
+ type ChangeListener = (flags: EvaluatedFlag[]) => void;
107
+ declare class MergedFeatureFlags<TFlags extends FlagRegistry = FlagRegistry> {
108
+ private flagsById;
109
+ private flagsByName;
110
+ private publicKeyPem;
111
+ private publicKeyCrypto;
112
+ private refreshTimer;
113
+ private backoffMs;
114
+ private consecutiveFailures;
115
+ private listeners;
116
+ private storeListeners;
117
+ private initialized;
118
+ private visibilityHandler;
119
+ private pendingScopeTransition;
120
+ private runtimeStatus;
121
+ private snapshotStorePromise;
122
+ private readonly keyPrefix;
123
+ private contextSignature;
124
+ private readonly flagIds;
125
+ private readonly clientKey;
126
+ private readonly apiUrl;
127
+ private readonly organizationId;
128
+ private readonly environmentId;
129
+ private readonly teamId;
130
+ private readonly refreshInterval;
131
+ private readonly onError;
132
+ private readonly onFlagsChanged;
133
+ private readonly snapshotPersistence;
134
+ private evaluationContext;
135
+ constructor(config: FeatureFlagClientConfig<TFlags>);
136
+ initialize(): Promise<void>;
137
+ /** Returns true if the flag is enabled. Returns false for unknown flags. */
138
+ isEnabled<K extends string & keyof TFlags>(name: K): boolean;
139
+ /** Returns the flag's value with the type inferred from the flag registry. */
140
+ getValue<K extends string & keyof TFlags>(name: K): TFlags[K] | undefined;
141
+ /** Returns the full evaluated flag entry, or undefined if not found. */
142
+ getFlag<K extends string & keyof TFlags>(name: K): EvaluatedFlag | undefined;
143
+ /** Returns all evaluated flags. */
144
+ getAllFlags(): EvaluatedFlag[];
145
+ getStatus(): FeatureFlagRuntimeStatus;
146
+ getStatusSnapshot(): FeatureFlagRuntimeStatus;
147
+ /** Manually trigger a refresh from the server. */
148
+ refresh(): Promise<void>;
149
+ /** Subscribe to flag changes. Returns an unsubscribe function. */
150
+ onChange(listener: ChangeListener): () => void;
151
+ /** Get a snapshot function for useSyncExternalStore. */
152
+ getSnapshot(): EvaluatedFlag[];
153
+ /** Replace the caller-provided evaluation context used on subsequent refreshes. */
154
+ setEvaluationContext(context: FeatureFlagEvaluationContext | null | undefined): void;
155
+ /** Subscribe function for useSyncExternalStore. */
156
+ subscribe(onStoreChange: () => void): () => void;
157
+ /** Clean up timers, listeners, and cached data. */
158
+ destroy(): void;
159
+ private resolveFlag;
160
+ private clearFlags;
161
+ private emitChange;
162
+ private updateFlags;
163
+ private hasFlagsChanged;
164
+ private setRuntimeStatus;
165
+ private fetchSignedFlags;
166
+ private fetchPublicKey;
167
+ private verifyWithRetry;
168
+ private ensureVerificationKey;
169
+ private getTrustedSnapshotVerificationKey;
170
+ private getSnapshotStore;
171
+ private getScopeParts;
172
+ private restorePersistedSnapshot;
173
+ private persistSnapshot;
174
+ private startPolling;
175
+ private scheduleNextPoll;
176
+ private safeRefresh;
177
+ private resetBackoff;
178
+ private incrementBackoff;
179
+ }
180
+
181
+ type FeatureFlagRenderState<TValue> = {
182
+ enabled: boolean;
183
+ value: TValue | undefined;
184
+ status: FeatureFlagLoadState;
185
+ };
186
+ type FeatureFlagMatchValue<TValue> = Extract<TValue, string | number | boolean>;
187
+ type TypedFeatureFlagProps<TFlags extends FlagRegistry, K extends string & keyof TFlags> = {
188
+ name: K;
189
+ children: ReactNode | ((props: FeatureFlagRenderState<TFlags[K]>) => ReactNode);
190
+ fallback?: ReactNode;
191
+ loading?: ReactNode;
192
+ matchValue?: FeatureFlagMatchValue<TFlags[K]>;
193
+ };
194
+ type TypedFeatureFlagComponent<TFlags extends FlagRegistry> = <K extends string & keyof TFlags>(props: TypedFeatureFlagProps<TFlags, K>) => ReactNode;
195
+ /**
196
+ * Creates type-safe React hooks and provider bound to a specific flag registry.
197
+ * Call this in your generated code and re-export the result.
198
+ *
199
+ * ```typescript
200
+ * // generated/feature-flags.ts
201
+ * export const { FeatureFlagProvider, FeatureFlag, useFeatureFlag, useFeatureFlags, useFeatureFlagClient, useFeatureFlagStatus } =
202
+ * createTypedHooks<FlagValues>()
203
+ * ```
204
+ */
205
+ declare function createTypedHooks<TFlags extends FlagRegistry>(): {
206
+ FeatureFlagProvider: (props: {
207
+ client: FeatureFlagClient;
208
+ blockUntilReady: boolean;
209
+ loadingFallback?: ReactNode;
210
+ initialFlags?: EvaluatedFlag[];
211
+ children: ReactNode;
212
+ }) => react_jsx_runtime.JSX.Element;
213
+ FeatureFlag: TypedFeatureFlagComponent<TFlags>;
214
+ useFeatureFlag: <K extends string & keyof TFlags>(name: K) => {
215
+ enabled: boolean;
216
+ value: TFlags[K] | undefined;
217
+ };
218
+ useFeatureFlags: () => EvaluatedFlag[];
219
+ useFeatureFlagClient: () => MergedFeatureFlags<TFlags>;
220
+ useFeatureFlagStatus: () => FeatureFlagLoadState;
221
+ };
222
+
223
+ export { type FeatureFlagClient, FeatureFlagProvider, type FeatureFlagProviderProps, createTypedHooks };