@featureflare/react 0.0.24 → 0.0.26
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 +74 -3
- package/dist/index.cjs +182 -84
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +33 -13
- package/dist/index.d.ts +33 -13
- package/dist/index.js +184 -86
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
2
|
import React from 'react';
|
|
3
|
-
import { FeatureFlareUserPayload, FeatureFlareClient } from '@featureflare/sdk-js';
|
|
3
|
+
import { FeatureFlareBootstrapPayload, FeatureFlarePersistentCacheAdapter, FeatureFlareMetricName, FeatureFlareMetricTags, FeatureFlareUserPayload, FeatureFlareClient, FeatureFlareEvaluationMetadata } from '@featureflare/sdk-js';
|
|
4
4
|
export { FeatureFlareUserPayload } from '@featureflare/sdk-js';
|
|
5
5
|
|
|
6
6
|
type FeatureFlareEnvironmentKey = 'development' | 'staging' | 'production';
|
|
@@ -12,6 +12,20 @@ type FeatureFlareReactConfig = {
|
|
|
12
12
|
/** Legacy/insecure browser mode: uses /api/v1/eval (no sdkKey). */
|
|
13
13
|
projectKey?: string;
|
|
14
14
|
envKey?: FeatureFlareEnvironmentKey | string;
|
|
15
|
+
timeoutMs?: number;
|
|
16
|
+
maxRetries?: number;
|
|
17
|
+
backoffMs?: number;
|
|
18
|
+
jitter?: number;
|
|
19
|
+
cacheTtlMs?: number;
|
|
20
|
+
staleTtlMs?: number;
|
|
21
|
+
bootstrap?: FeatureFlareBootstrapPayload;
|
|
22
|
+
persistentCache?: FeatureFlarePersistentCacheAdapter;
|
|
23
|
+
onMetric?: (metricName: FeatureFlareMetricName, value: number, tags?: FeatureFlareMetricTags) => void;
|
|
24
|
+
realtime?: {
|
|
25
|
+
enabled?: boolean;
|
|
26
|
+
pollingIntervalMs?: number;
|
|
27
|
+
ssePath?: string;
|
|
28
|
+
};
|
|
15
29
|
};
|
|
16
30
|
declare function resolveFeatureFlareBrowserConfig(input?: {
|
|
17
31
|
envKey?: FeatureFlareEnvironmentKey;
|
|
@@ -21,11 +35,13 @@ type FeatureFlareContextValue = {
|
|
|
21
35
|
client: FeatureFlareClient;
|
|
22
36
|
user: FeatureFlareUserPayload;
|
|
23
37
|
setUser: (next: FeatureFlareUserPayload) => void;
|
|
24
|
-
getFlagsState: (defaultValue: boolean) => FlagsState
|
|
38
|
+
getFlagsState: (defaultValue: boolean) => FlagsState;
|
|
25
39
|
refreshFlags: (defaultValue: boolean) => void;
|
|
26
40
|
subscribeFlags: (defaultValue: boolean, listener: () => void, options?: FlagsSubscriptionOptions) => () => void;
|
|
41
|
+
subscribeFlag: (flagKey: string, defaultValue: boolean, listener: () => void, options?: FlagsSubscriptionOptions) => () => void;
|
|
42
|
+
getFlagDiagnostics: (flagKey: string) => FeatureFlareEvaluationMetadata | null;
|
|
27
43
|
};
|
|
28
|
-
type FlagsState
|
|
44
|
+
type FlagsState = {
|
|
29
45
|
flags: Array<{
|
|
30
46
|
key: string;
|
|
31
47
|
value: boolean;
|
|
@@ -58,23 +74,27 @@ type BoolFlagsState = {
|
|
|
58
74
|
loading: boolean;
|
|
59
75
|
errors: Record<string, string>;
|
|
60
76
|
};
|
|
61
|
-
type FlagsState = {
|
|
62
|
-
flags: Array<{
|
|
63
|
-
key: string;
|
|
64
|
-
value: boolean;
|
|
65
|
-
}>;
|
|
66
|
-
loading: boolean;
|
|
67
|
-
error: string | null;
|
|
68
|
-
};
|
|
69
77
|
type UseFlagsOptions = FlagsSubscriptionOptions;
|
|
70
78
|
type UseFlagsInput = UseFlagsOptions & {
|
|
71
79
|
defaultValue?: boolean;
|
|
72
80
|
user?: FeatureFlareUserPayload;
|
|
73
81
|
};
|
|
82
|
+
type FlagDiagnostics = {
|
|
83
|
+
source: FeatureFlareEvaluationMetadata['source'] | 'unknown';
|
|
84
|
+
reason: FeatureFlareEvaluationMetadata['reason'] | 'unknown';
|
|
85
|
+
isStale: boolean;
|
|
86
|
+
updatedAt?: number;
|
|
87
|
+
staleAt?: number;
|
|
88
|
+
expiresAt?: number;
|
|
89
|
+
latencyMs?: number;
|
|
90
|
+
};
|
|
74
91
|
declare function useFeatureFlareUser(): [FeatureFlareUserPayload, (next: FeatureFlareUserPayload) => void];
|
|
92
|
+
declare function useFlag(flagKey: string, defaultValue?: boolean): BoolFlagState;
|
|
75
93
|
declare function useBoolFlag(flagKey: string, defaultValue?: boolean): BoolFlagState;
|
|
76
|
-
declare function
|
|
94
|
+
declare function useFlags(flagKeys: string[], defaultValue?: boolean): BoolFlagsState;
|
|
77
95
|
declare function useFlags(input?: UseFlagsInput): FlagsState;
|
|
78
96
|
declare function useFlags(defaultValue?: boolean, options?: UseFlagsOptions): FlagsState;
|
|
97
|
+
declare function useBoolFlags(flagKeys: string[], defaultValue?: boolean): BoolFlagsState;
|
|
98
|
+
declare function useFlagDiagnostics(flagKey: string, defaultValue?: boolean): FlagDiagnostics;
|
|
79
99
|
|
|
80
|
-
export { type FeatureFlareEnvironmentKey, FeatureFlareProvider, type FeatureFlareReactConfig, type
|
|
100
|
+
export { type FeatureFlareEnvironmentKey, FeatureFlareProvider, type FeatureFlareReactConfig, type FlagDiagnostics, type FlagsState, type FlagsSubscriptionOptions, resolveFeatureFlareBrowserConfig, useBoolFlag, useBoolFlags, useFeatureFlareContext, useFeatureFlareUser, useFlag, useFlagDiagnostics, useFlags };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
2
|
import React from 'react';
|
|
3
|
-
import { FeatureFlareUserPayload, FeatureFlareClient } from '@featureflare/sdk-js';
|
|
3
|
+
import { FeatureFlareBootstrapPayload, FeatureFlarePersistentCacheAdapter, FeatureFlareMetricName, FeatureFlareMetricTags, FeatureFlareUserPayload, FeatureFlareClient, FeatureFlareEvaluationMetadata } from '@featureflare/sdk-js';
|
|
4
4
|
export { FeatureFlareUserPayload } from '@featureflare/sdk-js';
|
|
5
5
|
|
|
6
6
|
type FeatureFlareEnvironmentKey = 'development' | 'staging' | 'production';
|
|
@@ -12,6 +12,20 @@ type FeatureFlareReactConfig = {
|
|
|
12
12
|
/** Legacy/insecure browser mode: uses /api/v1/eval (no sdkKey). */
|
|
13
13
|
projectKey?: string;
|
|
14
14
|
envKey?: FeatureFlareEnvironmentKey | string;
|
|
15
|
+
timeoutMs?: number;
|
|
16
|
+
maxRetries?: number;
|
|
17
|
+
backoffMs?: number;
|
|
18
|
+
jitter?: number;
|
|
19
|
+
cacheTtlMs?: number;
|
|
20
|
+
staleTtlMs?: number;
|
|
21
|
+
bootstrap?: FeatureFlareBootstrapPayload;
|
|
22
|
+
persistentCache?: FeatureFlarePersistentCacheAdapter;
|
|
23
|
+
onMetric?: (metricName: FeatureFlareMetricName, value: number, tags?: FeatureFlareMetricTags) => void;
|
|
24
|
+
realtime?: {
|
|
25
|
+
enabled?: boolean;
|
|
26
|
+
pollingIntervalMs?: number;
|
|
27
|
+
ssePath?: string;
|
|
28
|
+
};
|
|
15
29
|
};
|
|
16
30
|
declare function resolveFeatureFlareBrowserConfig(input?: {
|
|
17
31
|
envKey?: FeatureFlareEnvironmentKey;
|
|
@@ -21,11 +35,13 @@ type FeatureFlareContextValue = {
|
|
|
21
35
|
client: FeatureFlareClient;
|
|
22
36
|
user: FeatureFlareUserPayload;
|
|
23
37
|
setUser: (next: FeatureFlareUserPayload) => void;
|
|
24
|
-
getFlagsState: (defaultValue: boolean) => FlagsState
|
|
38
|
+
getFlagsState: (defaultValue: boolean) => FlagsState;
|
|
25
39
|
refreshFlags: (defaultValue: boolean) => void;
|
|
26
40
|
subscribeFlags: (defaultValue: boolean, listener: () => void, options?: FlagsSubscriptionOptions) => () => void;
|
|
41
|
+
subscribeFlag: (flagKey: string, defaultValue: boolean, listener: () => void, options?: FlagsSubscriptionOptions) => () => void;
|
|
42
|
+
getFlagDiagnostics: (flagKey: string) => FeatureFlareEvaluationMetadata | null;
|
|
27
43
|
};
|
|
28
|
-
type FlagsState
|
|
44
|
+
type FlagsState = {
|
|
29
45
|
flags: Array<{
|
|
30
46
|
key: string;
|
|
31
47
|
value: boolean;
|
|
@@ -58,23 +74,27 @@ type BoolFlagsState = {
|
|
|
58
74
|
loading: boolean;
|
|
59
75
|
errors: Record<string, string>;
|
|
60
76
|
};
|
|
61
|
-
type FlagsState = {
|
|
62
|
-
flags: Array<{
|
|
63
|
-
key: string;
|
|
64
|
-
value: boolean;
|
|
65
|
-
}>;
|
|
66
|
-
loading: boolean;
|
|
67
|
-
error: string | null;
|
|
68
|
-
};
|
|
69
77
|
type UseFlagsOptions = FlagsSubscriptionOptions;
|
|
70
78
|
type UseFlagsInput = UseFlagsOptions & {
|
|
71
79
|
defaultValue?: boolean;
|
|
72
80
|
user?: FeatureFlareUserPayload;
|
|
73
81
|
};
|
|
82
|
+
type FlagDiagnostics = {
|
|
83
|
+
source: FeatureFlareEvaluationMetadata['source'] | 'unknown';
|
|
84
|
+
reason: FeatureFlareEvaluationMetadata['reason'] | 'unknown';
|
|
85
|
+
isStale: boolean;
|
|
86
|
+
updatedAt?: number;
|
|
87
|
+
staleAt?: number;
|
|
88
|
+
expiresAt?: number;
|
|
89
|
+
latencyMs?: number;
|
|
90
|
+
};
|
|
74
91
|
declare function useFeatureFlareUser(): [FeatureFlareUserPayload, (next: FeatureFlareUserPayload) => void];
|
|
92
|
+
declare function useFlag(flagKey: string, defaultValue?: boolean): BoolFlagState;
|
|
75
93
|
declare function useBoolFlag(flagKey: string, defaultValue?: boolean): BoolFlagState;
|
|
76
|
-
declare function
|
|
94
|
+
declare function useFlags(flagKeys: string[], defaultValue?: boolean): BoolFlagsState;
|
|
77
95
|
declare function useFlags(input?: UseFlagsInput): FlagsState;
|
|
78
96
|
declare function useFlags(defaultValue?: boolean, options?: UseFlagsOptions): FlagsState;
|
|
97
|
+
declare function useBoolFlags(flagKeys: string[], defaultValue?: boolean): BoolFlagsState;
|
|
98
|
+
declare function useFlagDiagnostics(flagKey: string, defaultValue?: boolean): FlagDiagnostics;
|
|
79
99
|
|
|
80
|
-
export { type FeatureFlareEnvironmentKey, FeatureFlareProvider, type FeatureFlareReactConfig, type
|
|
100
|
+
export { type FeatureFlareEnvironmentKey, FeatureFlareProvider, type FeatureFlareReactConfig, type FlagDiagnostics, type FlagsState, type FlagsSubscriptionOptions, resolveFeatureFlareBrowserConfig, useBoolFlag, useBoolFlags, useFeatureFlareContext, useFeatureFlareUser, useFlag, useFlagDiagnostics, useFlags };
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// src/provider.tsx
|
|
2
2
|
import React, { createContext, useEffect, useMemo, useRef, useState } from "react";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
FeatureFlareClient
|
|
5
|
+
} from "@featureflare/sdk-js";
|
|
4
6
|
import { jsx } from "react/jsx-runtime";
|
|
5
7
|
function resolveFeatureFlareBrowserConfig(input) {
|
|
6
8
|
const explicitEnv = input?.envKey;
|
|
@@ -28,6 +30,29 @@ function normalizeSubscriptionOptions(options) {
|
|
|
28
30
|
enabled: options?.enabled ?? true
|
|
29
31
|
};
|
|
30
32
|
}
|
|
33
|
+
function flagsToMap(flags) {
|
|
34
|
+
const map = /* @__PURE__ */ new Map();
|
|
35
|
+
for (const flag of flags) {
|
|
36
|
+
map.set(flag.key, flag.value);
|
|
37
|
+
}
|
|
38
|
+
return map;
|
|
39
|
+
}
|
|
40
|
+
function diffFlagKeys(prev, next) {
|
|
41
|
+
const changed = /* @__PURE__ */ new Set();
|
|
42
|
+
const prevMap = flagsToMap(prev);
|
|
43
|
+
const nextMap = flagsToMap(next);
|
|
44
|
+
for (const [key, value] of prevMap.entries()) {
|
|
45
|
+
if (!nextMap.has(key) || nextMap.get(key) !== value) {
|
|
46
|
+
changed.add(key);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
for (const [key, value] of nextMap.entries()) {
|
|
50
|
+
if (!prevMap.has(key) || prevMap.get(key) !== value) {
|
|
51
|
+
changed.add(key);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return changed;
|
|
55
|
+
}
|
|
31
56
|
function createFlagsStore(client, getUser) {
|
|
32
57
|
const entries = /* @__PURE__ */ new Map();
|
|
33
58
|
let nextSubscriberId = 1;
|
|
@@ -36,10 +61,11 @@ function createFlagsStore(client, getUser) {
|
|
|
36
61
|
const key = defaultValue ? "1" : "0";
|
|
37
62
|
const existing = entries.get(key);
|
|
38
63
|
if (existing) return existing;
|
|
64
|
+
const cached = client.getCachedFlags();
|
|
39
65
|
const created = {
|
|
40
66
|
defaultValue,
|
|
41
|
-
snapshot: { flags:
|
|
42
|
-
listeners: /* @__PURE__ */ new
|
|
67
|
+
snapshot: { flags: cached.flags, loading: !cached.hasData, error: null },
|
|
68
|
+
listeners: /* @__PURE__ */ new Map(),
|
|
43
69
|
subscribers: /* @__PURE__ */ new Map(),
|
|
44
70
|
timer: null,
|
|
45
71
|
inFlight: false
|
|
@@ -47,8 +73,12 @@ function createFlagsStore(client, getUser) {
|
|
|
47
73
|
entries.set(key, created);
|
|
48
74
|
return created;
|
|
49
75
|
};
|
|
50
|
-
const emit = (entry) => {
|
|
51
|
-
for (const listener of entry.listeners)
|
|
76
|
+
const emit = (entry, changedKeys = null) => {
|
|
77
|
+
for (const { listener, flagKey } of entry.listeners.values()) {
|
|
78
|
+
if (!flagKey || changedKeys === null || changedKeys.has(flagKey)) {
|
|
79
|
+
listener();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
52
82
|
};
|
|
53
83
|
const getEffectiveOptions = (entry) => {
|
|
54
84
|
const active = [...entry.subscribers.values()].filter((s) => s.enabled);
|
|
@@ -94,13 +124,17 @@ function createFlagsStore(client, getUser) {
|
|
|
94
124
|
if (entry.inFlight) return;
|
|
95
125
|
entry.inFlight = true;
|
|
96
126
|
try {
|
|
127
|
+
const previousFlags = entry.snapshot.flags;
|
|
97
128
|
const flags = await client.flags(getUser(), defaultValue);
|
|
129
|
+
const changed = diffFlagKeys(previousFlags, flags);
|
|
98
130
|
entry.snapshot = { flags, loading: false, error: null };
|
|
99
|
-
|
|
131
|
+
if (changed.size > 0 || previousFlags.length === 0) {
|
|
132
|
+
emit(entry, changed);
|
|
133
|
+
}
|
|
100
134
|
} catch (error) {
|
|
101
135
|
const message = error instanceof Error ? error.message : String(error);
|
|
102
136
|
entry.snapshot = { ...entry.snapshot, loading: false, error: message };
|
|
103
|
-
emit(entry);
|
|
137
|
+
emit(entry, null);
|
|
104
138
|
} finally {
|
|
105
139
|
entry.inFlight = false;
|
|
106
140
|
schedule(entry);
|
|
@@ -109,14 +143,14 @@ function createFlagsStore(client, getUser) {
|
|
|
109
143
|
const refreshNow = (defaultValue) => {
|
|
110
144
|
const entry = getEntry(defaultValue);
|
|
111
145
|
entry.snapshot = { ...entry.snapshot, loading: true, error: null };
|
|
112
|
-
emit(entry);
|
|
146
|
+
emit(entry, null);
|
|
113
147
|
void refresh(defaultValue, true);
|
|
114
148
|
};
|
|
115
|
-
const subscribe = (defaultValue, listener, options) => {
|
|
149
|
+
const subscribe = (defaultValue, listener, options, flagKey) => {
|
|
116
150
|
const entry = getEntry(defaultValue);
|
|
117
151
|
const subscriberId = nextSubscriberId;
|
|
118
152
|
nextSubscriberId += 1;
|
|
119
|
-
entry.listeners.
|
|
153
|
+
entry.listeners.set(subscriberId, { listener, flagKey });
|
|
120
154
|
entry.subscribers.set(subscriberId, normalizeSubscriptionOptions(options));
|
|
121
155
|
const effective = getEffectiveOptions(entry);
|
|
122
156
|
if (effective.enabled && !entry.inFlight && entry.snapshot.loading) {
|
|
@@ -125,7 +159,7 @@ function createFlagsStore(client, getUser) {
|
|
|
125
159
|
schedule(entry);
|
|
126
160
|
}
|
|
127
161
|
return () => {
|
|
128
|
-
entry.listeners.delete(
|
|
162
|
+
entry.listeners.delete(subscriberId);
|
|
129
163
|
entry.subscribers.delete(subscriberId);
|
|
130
164
|
schedule(entry);
|
|
131
165
|
};
|
|
@@ -133,7 +167,7 @@ function createFlagsStore(client, getUser) {
|
|
|
133
167
|
const updateUser = () => {
|
|
134
168
|
for (const entry of entries.values()) {
|
|
135
169
|
entry.snapshot = { ...entry.snapshot, loading: true, error: null };
|
|
136
|
-
emit(entry);
|
|
170
|
+
emit(entry, null);
|
|
137
171
|
void refresh(entry.defaultValue);
|
|
138
172
|
}
|
|
139
173
|
};
|
|
@@ -145,10 +179,28 @@ function createFlagsStore(client, getUser) {
|
|
|
145
179
|
void refresh(entry.defaultValue);
|
|
146
180
|
}
|
|
147
181
|
};
|
|
182
|
+
const unsubscribeClientUpdate = typeof client.on === "function" ? client.on(
|
|
183
|
+
"update",
|
|
184
|
+
({ changedKeys }) => {
|
|
185
|
+
const changedSet = new Set(changedKeys);
|
|
186
|
+
for (const entry of entries.values()) {
|
|
187
|
+
const previous = entry.snapshot.flags;
|
|
188
|
+
const next = client.getCachedFlags().flags;
|
|
189
|
+
const diff = diffFlagKeys(previous, next);
|
|
190
|
+
if (diff.size === 0) continue;
|
|
191
|
+
const intersects = [...diff].some((key) => changedSet.has(key));
|
|
192
|
+
if (!intersects && changedSet.size > 0) continue;
|
|
193
|
+
entry.snapshot = { ...entry.snapshot, flags: next, loading: false, error: null };
|
|
194
|
+
emit(entry, diff);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
) : () => {
|
|
198
|
+
};
|
|
148
199
|
if (typeof document !== "undefined") {
|
|
149
200
|
document.addEventListener("visibilitychange", handleVisibilityChange);
|
|
150
201
|
}
|
|
151
202
|
const dispose = () => {
|
|
203
|
+
unsubscribeClientUpdate();
|
|
152
204
|
if (typeof document !== "undefined") {
|
|
153
205
|
document.removeEventListener("visibilitychange", handleVisibilityChange);
|
|
154
206
|
}
|
|
@@ -167,7 +219,12 @@ function createFlagsStore(client, getUser) {
|
|
|
167
219
|
return getEntry(defaultValue).snapshot;
|
|
168
220
|
},
|
|
169
221
|
refreshNow,
|
|
170
|
-
|
|
222
|
+
subscribeAll(defaultValue, listener, options) {
|
|
223
|
+
return subscribe(defaultValue, listener, options);
|
|
224
|
+
},
|
|
225
|
+
subscribeFlag(flagKey, defaultValue, listener, options) {
|
|
226
|
+
return subscribe(defaultValue, listener, options, flagKey);
|
|
227
|
+
},
|
|
171
228
|
updateUser,
|
|
172
229
|
dispose
|
|
173
230
|
};
|
|
@@ -186,13 +243,33 @@ function FeatureFlareProvider(props) {
|
|
|
186
243
|
apiBaseUrl: props.config.apiBaseUrl,
|
|
187
244
|
sdkKey: props.config.sdkKey,
|
|
188
245
|
projectKey: props.config.projectKey,
|
|
189
|
-
envKey: props.config.envKey
|
|
246
|
+
envKey: props.config.envKey,
|
|
247
|
+
timeoutMs: props.config.timeoutMs,
|
|
248
|
+
maxRetries: props.config.maxRetries,
|
|
249
|
+
backoffMs: props.config.backoffMs,
|
|
250
|
+
jitter: props.config.jitter,
|
|
251
|
+
cacheTtlMs: props.config.cacheTtlMs,
|
|
252
|
+
staleTtlMs: props.config.staleTtlMs,
|
|
253
|
+
bootstrap: props.config.bootstrap,
|
|
254
|
+
persistentCache: props.config.persistentCache,
|
|
255
|
+
onMetric: props.config.onMetric,
|
|
256
|
+
realtime: props.config.realtime
|
|
190
257
|
});
|
|
191
258
|
}, [
|
|
192
259
|
props.config.apiBaseUrl,
|
|
260
|
+
props.config.backoffMs,
|
|
261
|
+
props.config.bootstrap,
|
|
262
|
+
props.config.cacheTtlMs,
|
|
193
263
|
props.config.envKey,
|
|
264
|
+
props.config.jitter,
|
|
265
|
+
props.config.maxRetries,
|
|
266
|
+
props.config.onMetric,
|
|
267
|
+
props.config.persistentCache,
|
|
194
268
|
props.config.projectKey,
|
|
195
|
-
props.config.
|
|
269
|
+
props.config.realtime,
|
|
270
|
+
props.config.sdkKey,
|
|
271
|
+
props.config.staleTtlMs,
|
|
272
|
+
props.config.timeoutMs
|
|
196
273
|
]);
|
|
197
274
|
const flagsStore = useMemo(() => createFlagsStore(client, () => userRef.current), [client]);
|
|
198
275
|
useEffect(() => {
|
|
@@ -202,8 +279,9 @@ function FeatureFlareProvider(props) {
|
|
|
202
279
|
useEffect(() => {
|
|
203
280
|
return () => {
|
|
204
281
|
flagsStore.dispose();
|
|
282
|
+
client.dispose();
|
|
205
283
|
};
|
|
206
|
-
}, [flagsStore]);
|
|
284
|
+
}, [client, flagsStore]);
|
|
207
285
|
const value = useMemo(
|
|
208
286
|
() => ({
|
|
209
287
|
client,
|
|
@@ -211,7 +289,9 @@ function FeatureFlareProvider(props) {
|
|
|
211
289
|
setUser,
|
|
212
290
|
getFlagsState: flagsStore.getState,
|
|
213
291
|
refreshFlags: flagsStore.refreshNow,
|
|
214
|
-
subscribeFlags: flagsStore.
|
|
292
|
+
subscribeFlags: flagsStore.subscribeAll,
|
|
293
|
+
subscribeFlag: (flagKey, defaultValue, listener, options) => flagsStore.subscribeFlag(flagKey, defaultValue, listener, options),
|
|
294
|
+
getFlagDiagnostics: (flagKey) => client.getFlagDiagnostics(flagKey)
|
|
215
295
|
}),
|
|
216
296
|
[client, flagsStore, setUser, user]
|
|
217
297
|
);
|
|
@@ -224,7 +304,7 @@ function useFeatureFlareContext() {
|
|
|
224
304
|
}
|
|
225
305
|
|
|
226
306
|
// src/hooks.ts
|
|
227
|
-
import { useEffect as useEffect2, useMemo as useMemo2, useRef as useRef2,
|
|
307
|
+
import { useEffect as useEffect2, useMemo as useMemo2, useRef as useRef2, useSyncExternalStore } from "react";
|
|
228
308
|
var EMPTY_FLAGS_STATE = { flags: [], loading: true, error: null };
|
|
229
309
|
function userFingerprint(user) {
|
|
230
310
|
if (!user) return "";
|
|
@@ -235,83 +315,75 @@ function userFingerprint(user) {
|
|
|
235
315
|
meta: user.meta ?? {}
|
|
236
316
|
});
|
|
237
317
|
}
|
|
318
|
+
function mapFlags(flags) {
|
|
319
|
+
const values = {};
|
|
320
|
+
for (const flag of flags) {
|
|
321
|
+
values[flag.key] = flag.value;
|
|
322
|
+
}
|
|
323
|
+
return values;
|
|
324
|
+
}
|
|
238
325
|
function useFeatureFlareUser() {
|
|
239
326
|
const { user, setUser } = useFeatureFlareContext();
|
|
240
327
|
return [user, setUser];
|
|
241
328
|
}
|
|
242
|
-
function
|
|
243
|
-
const {
|
|
244
|
-
const [state, setState] = useState2({ value: defaultValue, loading: true, error: null });
|
|
245
|
-
const userId = user.id ?? user.key ?? "";
|
|
246
|
-
const key = useMemo2(() => `${flagKey}:${userId}`, [flagKey, userId]);
|
|
247
|
-
const lastKey = useRef2("");
|
|
329
|
+
function useFlag(flagKey, defaultValue = false) {
|
|
330
|
+
const { subscribeFlag, getFlagsState, refreshFlags } = useFeatureFlareContext();
|
|
248
331
|
useEffect2(() => {
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
263
|
-
setState({ value: defaultValue, loading: false, error: msg });
|
|
264
|
-
}
|
|
265
|
-
})();
|
|
266
|
-
return () => {
|
|
267
|
-
cancelled = true;
|
|
268
|
-
};
|
|
269
|
-
}, [client, defaultValue, flagKey, key, user]);
|
|
270
|
-
return state;
|
|
332
|
+
refreshFlags(defaultValue);
|
|
333
|
+
}, [defaultValue, refreshFlags]);
|
|
334
|
+
const subscribe = useMemo2(
|
|
335
|
+
() => (onStoreChange) => subscribeFlag(flagKey, defaultValue, onStoreChange),
|
|
336
|
+
[defaultValue, flagKey, subscribeFlag]
|
|
337
|
+
);
|
|
338
|
+
const state = useSyncExternalStore(subscribe, () => getFlagsState(defaultValue), () => EMPTY_FLAGS_STATE);
|
|
339
|
+
const value = state.flags.find((entry) => entry.key === flagKey)?.value ?? defaultValue;
|
|
340
|
+
return {
|
|
341
|
+
value,
|
|
342
|
+
loading: state.loading,
|
|
343
|
+
error: state.error
|
|
344
|
+
};
|
|
271
345
|
}
|
|
272
|
-
function
|
|
273
|
-
|
|
274
|
-
const sortedKeys = useMemo2(() => [...flagKeys].map((k) => k.trim()).filter(Boolean).sort(), [flagKeys]);
|
|
275
|
-
const userId = user.id ?? user.key ?? "";
|
|
276
|
-
const key = useMemo2(() => `${sortedKeys.join(",")}:${userId}`, [sortedKeys, userId]);
|
|
277
|
-
const [state, setState] = useState2({ values: {}, loading: true, errors: {} });
|
|
278
|
-
const lastKey = useRef2("");
|
|
279
|
-
useEffect2(() => {
|
|
280
|
-
let cancelled = false;
|
|
281
|
-
const nextKey = key;
|
|
282
|
-
lastKey.current = nextKey;
|
|
283
|
-
setState({ values: {}, loading: true, errors: {} });
|
|
284
|
-
(async () => {
|
|
285
|
-
const values = {};
|
|
286
|
-
const errors = {};
|
|
287
|
-
await Promise.all(
|
|
288
|
-
sortedKeys.map(async (flagKey) => {
|
|
289
|
-
try {
|
|
290
|
-
values[flagKey] = await client.bool(flagKey, user, defaultValue);
|
|
291
|
-
} catch (e) {
|
|
292
|
-
values[flagKey] = defaultValue;
|
|
293
|
-
errors[flagKey] = e instanceof Error ? e.message : String(e);
|
|
294
|
-
}
|
|
295
|
-
})
|
|
296
|
-
);
|
|
297
|
-
if (cancelled) return;
|
|
298
|
-
if (lastKey.current !== nextKey) return;
|
|
299
|
-
setState({ values, loading: false, errors });
|
|
300
|
-
})();
|
|
301
|
-
return () => {
|
|
302
|
-
cancelled = true;
|
|
303
|
-
};
|
|
304
|
-
}, [client, defaultValue, key, sortedKeys, user]);
|
|
305
|
-
return state;
|
|
346
|
+
function useBoolFlag(flagKey, defaultValue = false) {
|
|
347
|
+
return useFlag(flagKey, defaultValue);
|
|
306
348
|
}
|
|
307
|
-
function useFlags(
|
|
308
|
-
const { subscribeFlags, getFlagsState, refreshFlags, setUser } = useFeatureFlareContext();
|
|
349
|
+
function useFlags(defaultValueOrInputOrKeys = false, optionsOrDefaultValue = {}) {
|
|
350
|
+
const { subscribeFlags, subscribeFlag, getFlagsState, refreshFlags, setUser } = useFeatureFlareContext();
|
|
351
|
+
if (Array.isArray(defaultValueOrInputOrKeys)) {
|
|
352
|
+
const keys = [...defaultValueOrInputOrKeys].map((key) => key.trim()).filter(Boolean);
|
|
353
|
+
const defaultValue2 = typeof optionsOrDefaultValue === "boolean" ? optionsOrDefaultValue : false;
|
|
354
|
+
useEffect2(() => {
|
|
355
|
+
refreshFlags(defaultValue2);
|
|
356
|
+
}, [defaultValue2, refreshFlags]);
|
|
357
|
+
const subscribe2 = useMemo2(
|
|
358
|
+
() => (onStoreChange) => {
|
|
359
|
+
const unsubs = keys.map((key) => subscribeFlag(key, defaultValue2, onStoreChange));
|
|
360
|
+
return () => {
|
|
361
|
+
for (const unsub of unsubs) unsub();
|
|
362
|
+
};
|
|
363
|
+
},
|
|
364
|
+
[defaultValue2, keys, subscribeFlag]
|
|
365
|
+
);
|
|
366
|
+
const state = useSyncExternalStore(subscribe2, () => getFlagsState(defaultValue2), () => EMPTY_FLAGS_STATE);
|
|
367
|
+
const values = mapFlags(state.flags);
|
|
368
|
+
const filtered = {};
|
|
369
|
+
for (const key of keys) {
|
|
370
|
+
filtered[key] = values[key] ?? defaultValue2;
|
|
371
|
+
}
|
|
372
|
+
return {
|
|
373
|
+
values: filtered,
|
|
374
|
+
loading: state.loading,
|
|
375
|
+
errors: state.error ? { __global: state.error } : {}
|
|
376
|
+
};
|
|
377
|
+
}
|
|
309
378
|
const parsed = useMemo2(() => {
|
|
310
|
-
if (typeof
|
|
311
|
-
return {
|
|
379
|
+
if (typeof defaultValueOrInputOrKeys === "boolean") {
|
|
380
|
+
return {
|
|
381
|
+
...typeof optionsOrDefaultValue === "object" && optionsOrDefaultValue !== null ? optionsOrDefaultValue : {},
|
|
382
|
+
defaultValue: defaultValueOrInputOrKeys
|
|
383
|
+
};
|
|
312
384
|
}
|
|
313
|
-
return
|
|
314
|
-
}, [
|
|
385
|
+
return defaultValueOrInputOrKeys ?? {};
|
|
386
|
+
}, [defaultValueOrInputOrKeys, optionsOrDefaultValue]);
|
|
315
387
|
const defaultValue = parsed.defaultValue ?? false;
|
|
316
388
|
const normalizedOptions = useMemo2(
|
|
317
389
|
() => ({
|
|
@@ -340,6 +412,30 @@ function useFlags(defaultValueOrInput = false, options = {}) {
|
|
|
340
412
|
);
|
|
341
413
|
return useSyncExternalStore(subscribe, () => getFlagsState(defaultValue), () => EMPTY_FLAGS_STATE);
|
|
342
414
|
}
|
|
415
|
+
function useBoolFlags(flagKeys, defaultValue = false) {
|
|
416
|
+
return useFlags(flagKeys, defaultValue);
|
|
417
|
+
}
|
|
418
|
+
function useFlagDiagnostics(flagKey, defaultValue = false) {
|
|
419
|
+
const { getFlagDiagnostics, subscribeFlag } = useFeatureFlareContext();
|
|
420
|
+
const subscribe = useMemo2(
|
|
421
|
+
() => (onStoreChange) => subscribeFlag(flagKey, defaultValue, onStoreChange),
|
|
422
|
+
[defaultValue, flagKey, subscribeFlag]
|
|
423
|
+
);
|
|
424
|
+
const metadata = useSyncExternalStore(
|
|
425
|
+
subscribe,
|
|
426
|
+
() => getFlagDiagnostics(flagKey),
|
|
427
|
+
() => null
|
|
428
|
+
);
|
|
429
|
+
return {
|
|
430
|
+
source: metadata?.source ?? "unknown",
|
|
431
|
+
reason: metadata?.reason ?? "unknown",
|
|
432
|
+
isStale: metadata?.isStale ?? false,
|
|
433
|
+
updatedAt: metadata?.updatedAt,
|
|
434
|
+
staleAt: metadata?.staleAt,
|
|
435
|
+
expiresAt: metadata?.expiresAt,
|
|
436
|
+
latencyMs: metadata?.latencyMs
|
|
437
|
+
};
|
|
438
|
+
}
|
|
343
439
|
export {
|
|
344
440
|
FeatureFlareProvider,
|
|
345
441
|
resolveFeatureFlareBrowserConfig,
|
|
@@ -347,6 +443,8 @@ export {
|
|
|
347
443
|
useBoolFlags,
|
|
348
444
|
useFeatureFlareContext,
|
|
349
445
|
useFeatureFlareUser,
|
|
446
|
+
useFlag,
|
|
447
|
+
useFlagDiagnostics,
|
|
350
448
|
useFlags
|
|
351
449
|
};
|
|
352
450
|
//# sourceMappingURL=index.js.map
|