@lovart-open/flags 0.0.1-canary.pr4.7b8e2dd
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 +279 -0
- package/package.json +52 -0
- package/src/__tests__/flag-store.test.ts +102 -0
- package/src/__tests__/param-store.test.ts +233 -0
- package/src/index.ts +12 -0
- package/src/statsig/client.ts +123 -0
- package/src/statsig/flags.ts +148 -0
- package/src/statsig/index.ts +54 -0
- package/src/statsig/logger.ts +29 -0
- package/src/statsig/params.ts +390 -0
- package/src/statsig/types.ts +62 -0
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parameter Stores factory module.
|
|
3
|
+
* Use createParamStore to create type-safe parameter stores and hooks.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { merge, set } from 'lodash';
|
|
7
|
+
import { useParameterStore } from '@statsig/react-bindings';
|
|
8
|
+
import z from 'zod';
|
|
9
|
+
|
|
10
|
+
import { getStatsigClientSync, isTestEnv } from './client';
|
|
11
|
+
|
|
12
|
+
/** Parameter definition */
|
|
13
|
+
export interface ParamDefinition<T = unknown> {
|
|
14
|
+
schema: z.ZodType<T>;
|
|
15
|
+
fallback: T;
|
|
16
|
+
description?: string;
|
|
17
|
+
testOverride?: T;
|
|
18
|
+
override?: T;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Parameter store definition */
|
|
22
|
+
export interface ParamStoreDefinition<TParams extends Record<string, ParamDefinition<any>>> {
|
|
23
|
+
description?: string;
|
|
24
|
+
keep?: boolean;
|
|
25
|
+
params: TParams;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Helper function to define a single param with type inference */
|
|
29
|
+
export function defineParam<T>(def: ParamDefinition<T>): ParamDefinition<T> {
|
|
30
|
+
return def;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const hasWindow = typeof window !== 'undefined';
|
|
34
|
+
const FP_PREFIX = 'fp.';
|
|
35
|
+
|
|
36
|
+
/** Event name for schema mismatch errors */
|
|
37
|
+
export const PARAM_SCHEMA_MISMATCH_EVENT = 'param-schema-mismatch';
|
|
38
|
+
|
|
39
|
+
/** Detail payload for schema mismatch event */
|
|
40
|
+
export interface ParamSchemaMismatchDetail {
|
|
41
|
+
storeKey: string;
|
|
42
|
+
paramKey: string;
|
|
43
|
+
source: string;
|
|
44
|
+
value: unknown;
|
|
45
|
+
error: z.ZodError;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Dispatch schema mismatch event (browser only) */
|
|
49
|
+
function dispatchSchemaMismatchEvent(detail: ParamSchemaMismatchDetail): void {
|
|
50
|
+
if (!hasWindow) return;
|
|
51
|
+
window.dispatchEvent(new CustomEvent(PARAM_SCHEMA_MISMATCH_EVENT, { detail }));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Parse URL query string for param store overrides (standalone function).
|
|
56
|
+
* Format: ?fp.store.param=value or ?fp.store={"param":"value"}
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* parseParamStoreUrlOverrides('?fp.config.debug=true')
|
|
60
|
+
* // => { config: { debug: true } }
|
|
61
|
+
*/
|
|
62
|
+
export function parseParamStoreUrlOverrides(search?: string): Record<string, Record<string, unknown>> {
|
|
63
|
+
const searchString = search ?? (hasWindow ? window.location.search : '');
|
|
64
|
+
const params = new URLSearchParams(searchString);
|
|
65
|
+
const result: Record<string, Record<string, unknown>> = {};
|
|
66
|
+
|
|
67
|
+
for (const [key, value] of params.entries()) {
|
|
68
|
+
if (!key.startsWith(FP_PREFIX)) continue;
|
|
69
|
+
const rest = key.slice(FP_PREFIX.length);
|
|
70
|
+
const dotIndex = rest.indexOf('.');
|
|
71
|
+
|
|
72
|
+
if (dotIndex >= 0) {
|
|
73
|
+
const path = rest;
|
|
74
|
+
const coerced = tryCoerceBasic(value);
|
|
75
|
+
set(result, path, coerced);
|
|
76
|
+
} else {
|
|
77
|
+
const path = rest;
|
|
78
|
+
try {
|
|
79
|
+
const obj = JSON.parse(value);
|
|
80
|
+
if (typeof obj === 'object' && obj !== null && !Array.isArray(obj)) {
|
|
81
|
+
result[path] = merge(result[path] ?? {}, obj);
|
|
82
|
+
}
|
|
83
|
+
} catch {
|
|
84
|
+
// ignore
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return result;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Basic type coercion without schema (for standalone parse function) */
|
|
92
|
+
function tryCoerceBasic(raw: string): unknown {
|
|
93
|
+
const trimmed = raw.trim();
|
|
94
|
+
if ((trimmed.startsWith('{') && trimmed.endsWith('}')) || (trimmed.startsWith('[') && trimmed.endsWith(']'))) {
|
|
95
|
+
try {
|
|
96
|
+
return JSON.parse(trimmed);
|
|
97
|
+
} catch {
|
|
98
|
+
// ignore
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const num = Number(raw);
|
|
102
|
+
if (!Number.isNaN(num) && raw !== '') return num;
|
|
103
|
+
if (raw === 'true') return true;
|
|
104
|
+
if (raw === 'false') return false;
|
|
105
|
+
if (raw === 'null') return null;
|
|
106
|
+
return raw;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Try to coerce string value to the expected type based on schema */
|
|
110
|
+
function tryCoerce(raw: string, def: ParamDefinition | undefined): unknown {
|
|
111
|
+
const trimmed = raw.trim();
|
|
112
|
+
if ((trimmed.startsWith('{') && trimmed.endsWith('}')) || (trimmed.startsWith('[') && trimmed.endsWith(']'))) {
|
|
113
|
+
try {
|
|
114
|
+
return JSON.parse(trimmed);
|
|
115
|
+
} catch {
|
|
116
|
+
// ignore
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (def?.schema) {
|
|
120
|
+
const num = Number(raw);
|
|
121
|
+
if (!Number.isNaN(num) && def.schema.safeParse(num).success) {
|
|
122
|
+
return num;
|
|
123
|
+
}
|
|
124
|
+
if (raw === 'true' && def.schema.safeParse(true).success) return true;
|
|
125
|
+
if (raw === 'false' && def.schema.safeParse(false).success) return false;
|
|
126
|
+
if (raw === 'null' && def.schema.safeParse(null).success) return null;
|
|
127
|
+
}
|
|
128
|
+
return raw;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Single param state with value and source */
|
|
132
|
+
export interface ParamState<T> {
|
|
133
|
+
value: T;
|
|
134
|
+
source: 'url' | 'test' | 'override' | 'remote' | 'fallback';
|
|
135
|
+
error?: z.ZodError;
|
|
136
|
+
fallback?: T;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Store handle for accessing params */
|
|
140
|
+
export interface ParamStoreHandle<TParams extends Record<string, ParamDefinition<any>>> {
|
|
141
|
+
get<P extends keyof TParams>(paramKey: P): z.infer<TParams[P]['schema']>;
|
|
142
|
+
getState<P extends keyof TParams>(paramKey: P): ParamState<z.infer<TParams[P]['schema']>>;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Infer value types from param definitions */
|
|
146
|
+
export type ParamStoreValue<TParams extends Record<string, ParamDefinition<any>>> = {
|
|
147
|
+
[K in keyof TParams]: z.infer<TParams[K]['schema']>;
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
/** Statsig ParameterStore return type */
|
|
151
|
+
type StatsigParameterStore = ReturnType<ReturnType<typeof getStatsigClientSync>['getParameterStore']>;
|
|
152
|
+
|
|
153
|
+
/** Resolve options for param lookup */
|
|
154
|
+
export interface ParamResolveOptions {
|
|
155
|
+
statsigStore?: StatsigParameterStore | null;
|
|
156
|
+
search?: string;
|
|
157
|
+
disableExposureLog?: boolean;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Type-safe ParamStore class.
|
|
162
|
+
*/
|
|
163
|
+
export class ParamStore<TStores extends Record<string, ParamStoreDefinition<any>>> {
|
|
164
|
+
private readonly definitions: TStores;
|
|
165
|
+
|
|
166
|
+
constructor(definitions: TStores) {
|
|
167
|
+
this.definitions = definitions;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Parse URL query string for param overrides.
|
|
172
|
+
* Format: ?fp.store.param=value or ?fp.store={"param":"value"}
|
|
173
|
+
*/
|
|
174
|
+
private parseUrlOverrides(search?: string): Record<string, Record<string, unknown>> {
|
|
175
|
+
const searchString = search ?? (hasWindow ? window.location.search : '');
|
|
176
|
+
const params = new URLSearchParams(searchString);
|
|
177
|
+
const result: Record<string, Record<string, unknown>> = {};
|
|
178
|
+
|
|
179
|
+
for (const [key, value] of params.entries()) {
|
|
180
|
+
if (!key.startsWith(FP_PREFIX)) continue;
|
|
181
|
+
const rest = key.slice(FP_PREFIX.length);
|
|
182
|
+
const dotIndex = rest.indexOf('.');
|
|
183
|
+
|
|
184
|
+
if (dotIndex >= 0) {
|
|
185
|
+
const path = rest;
|
|
186
|
+
const storeKey = rest.slice(0, dotIndex);
|
|
187
|
+
const paramKey = rest.slice(dotIndex + 1);
|
|
188
|
+
const storeDef = this.definitions[storeKey];
|
|
189
|
+
const paramDef = storeDef?.params[paramKey];
|
|
190
|
+
set(result, path, tryCoerce(value, paramDef));
|
|
191
|
+
} else {
|
|
192
|
+
const path = rest;
|
|
193
|
+
try {
|
|
194
|
+
const obj = JSON.parse(value);
|
|
195
|
+
if (typeof obj === 'object' && obj !== null && !Array.isArray(obj)) {
|
|
196
|
+
result[path] = merge(result[path] ?? {}, obj);
|
|
197
|
+
}
|
|
198
|
+
} catch {
|
|
199
|
+
// ignore
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return result;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Resolve param value with priority: URL > test > override > remote > fallback
|
|
208
|
+
*/
|
|
209
|
+
private resolve<K extends keyof TStores, P extends keyof TStores[K]['params']>(
|
|
210
|
+
storeKey: K,
|
|
211
|
+
paramKey: P,
|
|
212
|
+
options?: ParamResolveOptions,
|
|
213
|
+
): ParamState<z.infer<TStores[K]['params'][P]['schema']>> {
|
|
214
|
+
const { statsigStore, search, ...evaluationOptions } = options ?? {};
|
|
215
|
+
const storeDef = this.definitions[storeKey];
|
|
216
|
+
if (!storeDef) {
|
|
217
|
+
throw new Error(`[ParamStore] Unknown store: ${String(storeKey)}`);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const def = storeDef.params[paramKey as string] as ParamDefinition;
|
|
221
|
+
if (!def) {
|
|
222
|
+
throw new Error(`[ParamStore] Unknown param: ${String(storeKey)}.${String(paramKey)}`);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const schema = def.schema;
|
|
226
|
+
let value: unknown;
|
|
227
|
+
let source: ParamState<unknown>['source'];
|
|
228
|
+
|
|
229
|
+
// 1. URL has highest priority
|
|
230
|
+
const urlOverrides = this.parseUrlOverrides(search);
|
|
231
|
+
const urlVal = urlOverrides[storeKey as string]?.[paramKey as string];
|
|
232
|
+
if (urlVal !== undefined) {
|
|
233
|
+
value = urlVal;
|
|
234
|
+
source = 'url';
|
|
235
|
+
}
|
|
236
|
+
// 2. Test environment override
|
|
237
|
+
else if (isTestEnv() && def.testOverride !== undefined) {
|
|
238
|
+
value = def.testOverride;
|
|
239
|
+
source = 'test';
|
|
240
|
+
}
|
|
241
|
+
// 3. Static override
|
|
242
|
+
else if (def.override !== undefined) {
|
|
243
|
+
value = def.override;
|
|
244
|
+
source = 'override';
|
|
245
|
+
}
|
|
246
|
+
// 4. Remote value from Statsig
|
|
247
|
+
else {
|
|
248
|
+
const clientStore = statsigStore ?? getStatsigClientSync().getParameterStore(storeKey as string, evaluationOptions);
|
|
249
|
+
const config = (clientStore as any)?.__configuration?.[paramKey as string];
|
|
250
|
+
if (config !== undefined) {
|
|
251
|
+
value = clientStore.get(paramKey as string, def.fallback);
|
|
252
|
+
source = 'remote';
|
|
253
|
+
} else {
|
|
254
|
+
value = def.fallback;
|
|
255
|
+
source = 'fallback';
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Schema validation
|
|
260
|
+
const parsed = schema.safeParse(value);
|
|
261
|
+
if (!parsed.success) {
|
|
262
|
+
console.error(`[ParamStore] Schema mismatch (${source}): ${String(storeKey)}.${String(paramKey)}`, value, parsed.error);
|
|
263
|
+
dispatchSchemaMismatchEvent({
|
|
264
|
+
storeKey: String(storeKey),
|
|
265
|
+
paramKey: String(paramKey),
|
|
266
|
+
source,
|
|
267
|
+
value,
|
|
268
|
+
error: parsed.error,
|
|
269
|
+
});
|
|
270
|
+
const fallbackVal = def.fallback;
|
|
271
|
+
if (source === 'remote') {
|
|
272
|
+
return { value: fallbackVal, source: 'fallback', error: parsed.error };
|
|
273
|
+
}
|
|
274
|
+
return { value: value as any, source, error: parsed.error, fallback: fallbackVal };
|
|
275
|
+
}
|
|
276
|
+
return { value: parsed.data, source };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** Get param state with value and source info */
|
|
280
|
+
getParamState<K extends keyof TStores, P extends keyof TStores[K]['params']>(
|
|
281
|
+
storeKey: K,
|
|
282
|
+
paramKey: P,
|
|
283
|
+
options?: ParamResolveOptions,
|
|
284
|
+
): ParamState<z.infer<TStores[K]['params'][P]['schema']>> {
|
|
285
|
+
return this.resolve(storeKey, paramKey, options);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** Get param value directly */
|
|
289
|
+
getParam<K extends keyof TStores, P extends keyof TStores[K]['params']>(
|
|
290
|
+
storeKey: K,
|
|
291
|
+
paramKey: P,
|
|
292
|
+
options?: ParamResolveOptions,
|
|
293
|
+
): z.infer<TStores[K]['params'][P]['schema']> {
|
|
294
|
+
return this.resolve(storeKey, paramKey, options).value;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** Get store handle for multiple param access */
|
|
298
|
+
getStore<K extends keyof TStores>(
|
|
299
|
+
storeKey: K,
|
|
300
|
+
options?: ParamResolveOptions,
|
|
301
|
+
): ParamStoreHandle<TStores[K]['params']> {
|
|
302
|
+
const { statsigStore, ...rest } = options ?? {};
|
|
303
|
+
let cachedStore: StatsigParameterStore | null | undefined = statsigStore;
|
|
304
|
+
if (cachedStore === undefined) {
|
|
305
|
+
try {
|
|
306
|
+
cachedStore = getStatsigClientSync().getParameterStore(storeKey as string, rest);
|
|
307
|
+
} catch {
|
|
308
|
+
cachedStore = null;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
const opts: ParamResolveOptions = { ...rest, statsigStore: cachedStore };
|
|
312
|
+
return {
|
|
313
|
+
get: <P extends keyof TStores[K]['params']>(paramKey: P) => this.resolve(storeKey, paramKey, opts).value,
|
|
314
|
+
getState: <P extends keyof TStores[K]['params']>(paramKey: P) => this.resolve(storeKey, paramKey, opts),
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Create a type-safe Param Store with React hooks.
|
|
321
|
+
*
|
|
322
|
+
* @example
|
|
323
|
+
* ```ts
|
|
324
|
+
* const MY_PARAMS = {
|
|
325
|
+
* homepage_cta: {
|
|
326
|
+
* description: 'Homepage CTA button',
|
|
327
|
+
* params: {
|
|
328
|
+
* text: defineParam({ schema: z.string(), fallback: 'Learn More' }),
|
|
329
|
+
* color: defineParam({ schema: z.enum(['red', 'blue']), fallback: 'blue' }),
|
|
330
|
+
* },
|
|
331
|
+
* },
|
|
332
|
+
* } as const satisfies Record<string, ParamStoreDefinition<any>>;
|
|
333
|
+
*
|
|
334
|
+
* export const { paramStore, useParam, useParamState } = createParamStore(MY_PARAMS);
|
|
335
|
+
*
|
|
336
|
+
* // Full type safety and autocomplete
|
|
337
|
+
* const text = useParam('homepage_cta', 'text'); // string type
|
|
338
|
+
* const color = useParam('homepage_cta', 'color'); // 'red' | 'blue' type
|
|
339
|
+
* ```
|
|
340
|
+
*/
|
|
341
|
+
export function createParamStore<T extends Record<string, ParamStoreDefinition<any>>>(definitions: T) {
|
|
342
|
+
const store = new ParamStore(definitions);
|
|
343
|
+
|
|
344
|
+
type StoreKey = Extract<keyof T, string>;
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* React Hook: Get store handle for multiple param access.
|
|
348
|
+
*/
|
|
349
|
+
function useParamStore<K extends StoreKey>(
|
|
350
|
+
storeKey: K,
|
|
351
|
+
options?: ParamResolveOptions,
|
|
352
|
+
): ParamStoreHandle<T[K]['params']> {
|
|
353
|
+
const statsigStore = useParameterStore(storeKey, options);
|
|
354
|
+
return store.getStore(storeKey, { ...options, statsigStore });
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* React Hook: Get single param state.
|
|
359
|
+
*/
|
|
360
|
+
function useParamState<K extends StoreKey, P extends keyof T[K]['params']>(
|
|
361
|
+
storeKey: K,
|
|
362
|
+
paramKey: P,
|
|
363
|
+
options?: ParamResolveOptions,
|
|
364
|
+
): ParamState<z.infer<T[K]['params'][P]['schema']>> {
|
|
365
|
+
const statsigStore = useParameterStore(storeKey, options);
|
|
366
|
+
return store.getParamState(storeKey, paramKey, { ...options, statsigStore });
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* React Hook: Get single param value.
|
|
371
|
+
*/
|
|
372
|
+
function useParam<K extends StoreKey, P extends keyof T[K]['params']>(
|
|
373
|
+
storeKey: K,
|
|
374
|
+
paramKey: P,
|
|
375
|
+
options?: ParamResolveOptions,
|
|
376
|
+
): z.infer<T[K]['params'][P]['schema']> {
|
|
377
|
+
return useParamState(storeKey, paramKey, options).value;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
return {
|
|
381
|
+
/** ParamStore instance */
|
|
382
|
+
paramStore: store,
|
|
383
|
+
/** React Hook: Get single param value */
|
|
384
|
+
useParam,
|
|
385
|
+
/** React Hook: Get single param state */
|
|
386
|
+
useParamState,
|
|
387
|
+
/** React Hook: Get store handle */
|
|
388
|
+
useParamStore,
|
|
389
|
+
};
|
|
390
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core types for the feature flag and parameter store.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Statsig Feature Gate type (internal use).
|
|
7
|
+
*/
|
|
8
|
+
export interface FeatureGate {
|
|
9
|
+
value: boolean;
|
|
10
|
+
idType?: string | null;
|
|
11
|
+
details?: { reason: string };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Statsig evaluation options.
|
|
16
|
+
*/
|
|
17
|
+
export interface EvaluationOptions {
|
|
18
|
+
/** Disable exposure logging */
|
|
19
|
+
disableExposureLog?: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Feature flag definition.
|
|
24
|
+
*/
|
|
25
|
+
export interface FlagDefinition {
|
|
26
|
+
/** Human-readable description */
|
|
27
|
+
description?: string;
|
|
28
|
+
/** Fixed value for test/E2E environments */
|
|
29
|
+
testOverride?: boolean;
|
|
30
|
+
/** Static override value (higher priority than remote) */
|
|
31
|
+
override?: boolean;
|
|
32
|
+
/** Mark as kept locally (no remote config needed) */
|
|
33
|
+
keep?: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Flag value source priority levels.
|
|
38
|
+
*/
|
|
39
|
+
export type FlagPriority = 'url' | 'test' | 'override' | 'remote' | 'fallback';
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Flag state with value and source information.
|
|
43
|
+
*/
|
|
44
|
+
export interface FlagState {
|
|
45
|
+
flag: boolean;
|
|
46
|
+
source: FlagPriority;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Extract string keys from flag definitions.
|
|
51
|
+
*/
|
|
52
|
+
export type FlagKeyOf<TDefinitions extends Record<string, FlagDefinition>> = Extract<keyof TDefinitions, string>;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Snapshot of all flag states keyed by flag key.
|
|
56
|
+
*/
|
|
57
|
+
export type FlagSnapshot<TKey extends string = string> = Record<TKey, FlagState>;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Remote flag values (boolean map).
|
|
61
|
+
*/
|
|
62
|
+
export type RemoteFlagValues<TKey extends string = string> = Partial<Record<TKey, boolean>>;
|