featurefly 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.
@@ -0,0 +1,560 @@
1
+ import axios from 'axios';
2
+ import { InMemoryCache } from './cache';
3
+ import { ConsoleLogger } from './logger';
4
+ import { CircuitBreaker } from './circuit-breaker';
5
+ import { EventEmitter } from './event-emitter';
6
+ import { withRetry } from './retry';
7
+ import { FlagStreamClient } from './streaming';
8
+ import { EdgeEvaluator } from './edge-evaluator';
9
+ import { ImpactMetrics } from './metrics';
10
+ // ═══════════════════════════════════════════════════════════════════════════════
11
+ // DEFAULTS
12
+ // ═══════════════════════════════════════════════════════════════════════════════
13
+ const DEFAULT_TIMEOUT = 10000;
14
+ const DEFAULT_CACHE_TTL = 60000;
15
+ const DEFAULT_RETRY = {
16
+ maxAttempts: 3,
17
+ baseDelayMs: 1000,
18
+ maxDelayMs: 10000,
19
+ };
20
+ const DEFAULT_CIRCUIT_BREAKER = {
21
+ failureThreshold: 5,
22
+ resetTimeoutMs: 30000,
23
+ };
24
+ // ═══════════════════════════════════════════════════════════════════════════════
25
+ // CLIENT
26
+ // ═══════════════════════════════════════════════════════════════════════════════
27
+ /**
28
+ * FeatureFly SDK Client
29
+ *
30
+ * Framework-agnostic feature flags client with:
31
+ * - In-memory caching with TTL
32
+ * - Retry with exponential backoff + jitter
33
+ * - Circuit breaker for resilience
34
+ * - Typed event system
35
+ * - Local overrides for dev/testing
36
+ * - Fallback defaults for graceful degradation
37
+ * - Multi-type flag values (boolean, string, number, JSON)
38
+ *
39
+ * @example
40
+ * ```ts
41
+ * const client = new FeatureFlagsClient({
42
+ * baseUrl: 'https://api.example.com',
43
+ * apiKey: 'your-key',
44
+ * });
45
+ *
46
+ * const isEnabled = await client.evaluateFlag('new-feature', { workspaceId: '123' });
47
+ * ```
48
+ */
49
+ export class FeatureFlagsClient {
50
+ constructor(config) {
51
+ this.previousValues = new Map();
52
+ this.disposed = false;
53
+ // Logger
54
+ this.logger = config.logger ?? new ConsoleLogger(config.logLevel ?? 'warn');
55
+ // Cache
56
+ const cacheTtl = config.cacheEnabled === false ? 0 : (config.cacheTtlMs ?? DEFAULT_CACHE_TTL);
57
+ this.cache = new InMemoryCache(cacheTtl);
58
+ // Retry
59
+ this.retryConfig = { ...DEFAULT_RETRY, ...config.retry };
60
+ // Event emitter
61
+ this.events = new EventEmitter();
62
+ // Circuit breaker
63
+ const cbConfig = { ...DEFAULT_CIRCUIT_BREAKER, ...config.circuitBreaker };
64
+ this.circuitBreaker = new CircuitBreaker({
65
+ ...cbConfig,
66
+ logger: this.logger,
67
+ onStateChange: (state, failures) => {
68
+ const eventMap = {
69
+ 'open': 'circuitOpen',
70
+ 'closed': 'circuitClosed',
71
+ 'half-open': 'circuitHalfOpen',
72
+ };
73
+ const event = eventMap[state];
74
+ if (event) {
75
+ this.events.emit(event, { state, failures });
76
+ }
77
+ },
78
+ });
79
+ // Local overrides & fallbacks
80
+ this.localOverrides = { ...config.localOverrides };
81
+ this.fallbackDefaults = { ...config.fallbackDefaults };
82
+ // HTTP client
83
+ this.http = axios.create({
84
+ baseURL: config.baseUrl,
85
+ timeout: config.timeout ?? DEFAULT_TIMEOUT,
86
+ headers: {
87
+ 'Content-Type': 'application/json',
88
+ ...(config.apiKey && { Authorization: `Bearer ${config.apiKey}` }),
89
+ },
90
+ });
91
+ // Edge Evaluator Initialization
92
+ if (config.edgeDocument) {
93
+ this.edgeEvaluator = new EdgeEvaluator(config.edgeDocument, this.fallbackDefaults, config.trackingCallback);
94
+ this.logger.info('Edge evaluator initialized from provided document');
95
+ }
96
+ // Streaming Initialization
97
+ if (config.streaming) {
98
+ const streamConfig = typeof config.streaming === 'object' ? config.streaming : {};
99
+ this.streamClient = new FlagStreamClient(config.baseUrl, config.apiKey, streamConfig, this.logger, this.events);
100
+ // Auto-connect stream on boot
101
+ this.streamClient.connect();
102
+ // When stream notifies of updates, we should refresh the edge document if in edge mode
103
+ // or simply clear the cache if in remote mode
104
+ this.events.on('flagsUpdated', () => {
105
+ if (this.edgeEvaluator) {
106
+ this.refreshEdgeDocument().catch(e => this.logger.error('Failed to refresh edge doc on stream update', e));
107
+ }
108
+ else {
109
+ this.cache.clear();
110
+ }
111
+ });
112
+ }
113
+ // Impact Metrics
114
+ this.metrics = new ImpactMetrics(this.events);
115
+ this.logger.debug(`Initialized with baseUrl=${config.baseUrl}, cache=${cacheTtl}ms, retry=${this.retryConfig.maxAttempts}`);
116
+ }
117
+ // ═══════════════════════════════════════════════════════════════════════════
118
+ // STREAMING & EDGE MANAGERS
119
+ // ═══════════════════════════════════════════════════════════════════════════
120
+ /**
121
+ * Start or resume the SSE streaming connection.
122
+ */
123
+ startStreaming() {
124
+ this.assertNotDisposed();
125
+ if (!this.streamClient) {
126
+ this.logger.warn('Streaming was not configured. Use startStreaming(config) to enable it.');
127
+ return;
128
+ }
129
+ this.streamClient.connect();
130
+ }
131
+ /**
132
+ * Stop the SSE streaming connection.
133
+ */
134
+ stopStreaming() {
135
+ this.streamClient?.disconnect();
136
+ }
137
+ /**
138
+ * Fetch a full FlagDocument from the API to initialize Edge Evaluation mode.
139
+ * If streaming is enabled, updates will auto-refresh the document.
140
+ */
141
+ async loadEdgeDocument() {
142
+ this.assertNotDisposed();
143
+ const doc = await this.fetchWithResiliency(async () => {
144
+ const response = await this.http.get('/feature-flags/document');
145
+ return response.data;
146
+ });
147
+ if (this.edgeEvaluator) {
148
+ this.edgeEvaluator.updateDocument(doc);
149
+ }
150
+ else {
151
+ this.edgeEvaluator = new EdgeEvaluator(doc, this.fallbackDefaults);
152
+ }
153
+ this.logger.info('Edge document loaded. Client is now in offline evaluation mode.');
154
+ }
155
+ async refreshEdgeDocument() {
156
+ if (!this.edgeEvaluator)
157
+ return;
158
+ try {
159
+ const response = await this.http.get('/feature-flags/document');
160
+ this.edgeEvaluator.updateDocument(response.data);
161
+ this.logger.debug('Edge document refreshed from stream trigger');
162
+ }
163
+ catch (e) {
164
+ this.logger.error('Failed to refresh edge document', e);
165
+ }
166
+ }
167
+ // ═══════════════════════════════════════════════════════════════════════════
168
+ // EVENT SYSTEM
169
+ // ═══════════════════════════════════════════════════════════════════════════
170
+ /**
171
+ * Subscribe to SDK events.
172
+ * @returns Unsubscribe function
173
+ */
174
+ on(event, handler) {
175
+ return this.events.on(event, handler);
176
+ }
177
+ /**
178
+ * Subscribe to an event once.
179
+ */
180
+ once(event, handler) {
181
+ return this.events.once(event, handler);
182
+ }
183
+ // ═══════════════════════════════════════════════════════════════════════════
184
+ // FLAG EVALUATION
185
+ // ═══════════════════════════════════════════════════════════════════════════
186
+ /**
187
+ * Evaluate a single flag. Returns the flag value.
188
+ *
189
+ * Resolution order:
190
+ * 1. Local overrides (dev/testing)
191
+ * 2. Cache hit
192
+ * 3. Remote API call
193
+ * 4. Fallback defaults
194
+ */
195
+ async evaluateFlag(slug, context) {
196
+ this.assertNotDisposed();
197
+ const start = Date.now();
198
+ // 1. Local overrides always skip HTTP & Edge processing
199
+ if (slug in this.localOverrides) {
200
+ const value = this.localOverrides[slug];
201
+ this.logger.debug(`Flag "${slug}" resolved from local override: ${String(value)}`);
202
+ this.emitEvaluated(slug, value, 'LOCAL_OVERRIDE', start);
203
+ return value;
204
+ }
205
+ // 2. Edge Evaluation (zero HTTP, done purely in memory)
206
+ if (this.edgeEvaluator) {
207
+ const { value, reason } = this.edgeEvaluator.evaluate(slug, context || {}, this.localOverrides);
208
+ this.detectChange(slug, value);
209
+ this.emitEvaluated(slug, value, reason, start);
210
+ return value;
211
+ }
212
+ // 3. Cache hit (Remote Evaluation mode)
213
+ const cacheKey = this.buildCacheKey('evaluate', slug, context);
214
+ const cached = this.cache.get(cacheKey);
215
+ if (cached.hit) {
216
+ this.logger.debug(`Flag "${slug}" resolved from cache: ${String(cached.value)}`);
217
+ this.events.emit('cacheHit', { key: cacheKey });
218
+ this.emitEvaluated(slug, cached.value, 'CACHE_HIT', start);
219
+ return cached.value;
220
+ }
221
+ this.events.emit('cacheMiss', { key: cacheKey });
222
+ // 4. Remote call
223
+ try {
224
+ const value = await this.fetchWithResiliency(async () => {
225
+ const params = context ?? {};
226
+ const response = await this.http.get(`/feature-flags/${slug}/evaluate`, { params });
227
+ return response.data.value;
228
+ });
229
+ this.cache.set(cacheKey, value);
230
+ this.detectChange(slug, value);
231
+ this.emitEvaluated(slug, value, 'DEFAULT', start);
232
+ return value;
233
+ }
234
+ catch (error) {
235
+ // 5. Fallback
236
+ if (slug in this.fallbackDefaults) {
237
+ const value = this.fallbackDefaults[slug];
238
+ this.logger.warn(`Flag "${slug}" using fallback default: ${String(value)}`);
239
+ this.emitEvaluated(slug, value, 'FALLBACK', start);
240
+ return value;
241
+ }
242
+ this.logger.error(`Flag "${slug}" evaluation failed with no fallback`, error);
243
+ this.emitEvaluated(slug, false, 'ERROR', start);
244
+ return false;
245
+ }
246
+ }
247
+ /**
248
+ * Evaluate all flags in a single batch request.
249
+ */
250
+ async evaluateAllFlags(context) {
251
+ this.assertNotDisposed();
252
+ // 1. Edge Evaluation Batch
253
+ if (this.edgeEvaluator) {
254
+ return this.edgeEvaluator.evaluateAll(context || {}, this.localOverrides);
255
+ }
256
+ // 2. Remote Evaluation
257
+ const cacheKey = this.buildCacheKey('batch-evaluate', undefined, context);
258
+ const cached = this.cache.get(cacheKey);
259
+ if (cached.hit) {
260
+ this.events.emit('cacheHit', { key: cacheKey });
261
+ return cached.value;
262
+ }
263
+ this.events.emit('cacheMiss', { key: cacheKey });
264
+ try {
265
+ const result = await this.fetchWithResiliency(async () => {
266
+ const params = context ?? {};
267
+ const response = await this.http.get('/feature-flags/batch/evaluate', { params });
268
+ return response.data;
269
+ });
270
+ // Merge local overrides on top
271
+ const merged = { ...result, ...this.localOverrides };
272
+ this.cache.set(cacheKey, merged);
273
+ return merged;
274
+ }
275
+ catch (error) {
276
+ this.logger.error('Batch evaluation failed, returning fallback defaults', error);
277
+ return { ...this.fallbackDefaults, ...this.localOverrides };
278
+ }
279
+ }
280
+ // ═══════════════════════════════════════════════════════════════════════════
281
+ // FLAG MANAGEMENT (CRUD)
282
+ // ═══════════════════════════════════════════════════════════════════════════
283
+ async createFlag(data) {
284
+ this.assertNotDisposed();
285
+ const response = await this.fetchWithResiliency(() => this.http.post('/feature-flags', data));
286
+ this.cache.clear();
287
+ this.events.emit('cacheCleared', undefined);
288
+ return response.data;
289
+ }
290
+ async getAllFlags() {
291
+ this.assertNotDisposed();
292
+ const cacheKey = 'all-flags';
293
+ const cached = this.cache.get(cacheKey);
294
+ if (cached.hit)
295
+ return cached.value;
296
+ const response = await this.fetchWithResiliency(() => this.http.get('/feature-flags'));
297
+ this.cache.set(cacheKey, response.data);
298
+ return response.data;
299
+ }
300
+ async getFlagById(id) {
301
+ this.assertNotDisposed();
302
+ const cacheKey = `flag-${id}`;
303
+ const cached = this.cache.get(cacheKey);
304
+ if (cached.hit)
305
+ return cached.value;
306
+ try {
307
+ const response = await this.fetchWithResiliency(() => this.http.get(`/feature-flags/${id}`));
308
+ this.cache.set(cacheKey, response.data);
309
+ return response.data;
310
+ }
311
+ catch (error) {
312
+ if (axios.isAxiosError(error) && error.response?.status === 404)
313
+ return null;
314
+ throw error;
315
+ }
316
+ }
317
+ async getFlagBySlug(slug) {
318
+ this.assertNotDisposed();
319
+ const cacheKey = `flag-slug-${slug}`;
320
+ const cached = this.cache.get(cacheKey);
321
+ if (cached.hit)
322
+ return cached.value;
323
+ try {
324
+ const response = await this.fetchWithResiliency(() => this.http.get(`/feature-flags/slug/${slug}`));
325
+ this.cache.set(cacheKey, response.data);
326
+ return response.data;
327
+ }
328
+ catch (error) {
329
+ if (axios.isAxiosError(error) && error.response?.status === 404)
330
+ return null;
331
+ throw error;
332
+ }
333
+ }
334
+ async updateFlag(id, data) {
335
+ this.assertNotDisposed();
336
+ const response = await this.fetchWithResiliency(() => this.http.patch(`/feature-flags/${id}`, data));
337
+ this.cache.clear();
338
+ this.events.emit('cacheCleared', undefined);
339
+ return response.data;
340
+ }
341
+ async deleteFlag(id) {
342
+ this.assertNotDisposed();
343
+ await this.fetchWithResiliency(() => this.http.delete(`/feature-flags/${id}`));
344
+ this.cache.clear();
345
+ this.events.emit('cacheCleared', undefined);
346
+ }
347
+ // ═══════════════════════════════════════════════════════════════════════════
348
+ // WORKSPACE FLAGS
349
+ // ═══════════════════════════════════════════════════════════════════════════
350
+ async setWorkspaceFlag(slug, workspaceId, value) {
351
+ this.assertNotDisposed();
352
+ const data = { value };
353
+ const response = await this.fetchWithResiliency(() => this.http.post(`/feature-flags/${slug}/workspaces/${workspaceId}`, data));
354
+ // Invalidate relevant cache entries
355
+ this.cache.delete(this.buildCacheKey('evaluate', slug, { workspaceId }));
356
+ this.cache.delete(this.buildCacheKey('batch-evaluate', undefined, { workspaceId }));
357
+ this.cache.delete(`workspace-flags-${workspaceId}`);
358
+ return response.data;
359
+ }
360
+ async removeWorkspaceFlag(slug, workspaceId) {
361
+ this.assertNotDisposed();
362
+ await this.fetchWithResiliency(() => this.http.delete(`/feature-flags/${slug}/workspaces/${workspaceId}`));
363
+ this.cache.delete(this.buildCacheKey('evaluate', slug, { workspaceId }));
364
+ this.cache.delete(this.buildCacheKey('batch-evaluate', undefined, { workspaceId }));
365
+ this.cache.delete(`workspace-flags-${workspaceId}`);
366
+ }
367
+ async getWorkspaceFlags(workspaceId) {
368
+ this.assertNotDisposed();
369
+ const cacheKey = `workspace-flags-${workspaceId}`;
370
+ const cached = this.cache.get(cacheKey);
371
+ if (cached.hit)
372
+ return cached.value;
373
+ const response = await this.fetchWithResiliency(() => this.http.get(`/feature-flags/workspaces/${workspaceId}/flags`));
374
+ this.cache.set(cacheKey, response.data);
375
+ return response.data;
376
+ }
377
+ // ═══════════════════════════════════════════════════════════════════════════
378
+ // ANALYTICS
379
+ // ═══════════════════════════════════════════════════════════════════════════
380
+ async getFlagStats() {
381
+ this.assertNotDisposed();
382
+ const cacheKey = 'flag-stats';
383
+ const cached = this.cache.get(cacheKey);
384
+ if (cached.hit)
385
+ return cached.value;
386
+ const response = await this.fetchWithResiliency(() => this.http.get('/feature-flags/stats/overview'));
387
+ this.cache.set(cacheKey, response.data);
388
+ return response.data;
389
+ }
390
+ async getFlagsByCategory(category) {
391
+ this.assertNotDisposed();
392
+ const cacheKey = `flags-by-category-${category}`;
393
+ const cached = this.cache.get(cacheKey);
394
+ if (cached.hit)
395
+ return cached.value;
396
+ const response = await this.fetchWithResiliency(() => this.http.get(`/feature-flags/category/${category}`));
397
+ this.cache.set(cacheKey, response.data);
398
+ return response.data;
399
+ }
400
+ async getFlagsByTargetService(serviceName) {
401
+ this.assertNotDisposed();
402
+ const cacheKey = `flags-by-service-${serviceName}`;
403
+ const cached = this.cache.get(cacheKey);
404
+ if (cached.hit)
405
+ return cached.value;
406
+ const response = await this.fetchWithResiliency(() => this.http.get(`/feature-flags/service/${serviceName}`));
407
+ this.cache.set(cacheKey, response.data);
408
+ return response.data;
409
+ }
410
+ // ═══════════════════════════════════════════════════════════════════════════
411
+ // LOCAL OVERRIDES (dev/testing)
412
+ // ═══════════════════════════════════════════════════════════════════════════
413
+ /**
414
+ * Set a local override for a flag. Overrides skip HTTP entirely.
415
+ * Useful for development and testing.
416
+ */
417
+ setLocalOverride(slug, value) {
418
+ this.localOverrides[slug] = value;
419
+ this.logger.debug(`Local override set: "${slug}" = ${String(value)}`);
420
+ }
421
+ /**
422
+ * Remove a local override.
423
+ */
424
+ removeLocalOverride(slug) {
425
+ delete this.localOverrides[slug];
426
+ this.logger.debug(`Local override removed: "${slug}"`);
427
+ }
428
+ /**
429
+ * Get all local overrides.
430
+ */
431
+ getLocalOverrides() {
432
+ return { ...this.localOverrides };
433
+ }
434
+ /**
435
+ * Clear all local overrides.
436
+ */
437
+ clearLocalOverrides() {
438
+ Object.keys(this.localOverrides).forEach((key) => delete this.localOverrides[key]);
439
+ this.logger.debug('All local overrides cleared');
440
+ }
441
+ // ═══════════════════════════════════════════════════════════════════════════
442
+ // UTILITY
443
+ // ═══════════════════════════════════════════════════════════════════════════
444
+ /**
445
+ * Clear all cached data.
446
+ */
447
+ clearCache() {
448
+ this.cache.clear();
449
+ this.events.emit('cacheCleared', undefined);
450
+ this.logger.debug('Cache cleared');
451
+ }
452
+ /**
453
+ * Get cache statistics.
454
+ */
455
+ getCacheStats() {
456
+ return this.cache.getStats();
457
+ }
458
+ /**
459
+ * Get current circuit breaker state.
460
+ */
461
+ getCircuitBreakerState() {
462
+ return {
463
+ state: this.circuitBreaker.getState(),
464
+ failures: this.circuitBreaker.getFailures(),
465
+ };
466
+ }
467
+ /**
468
+ * Reset the circuit breaker to closed state.
469
+ */
470
+ resetCircuitBreaker() {
471
+ this.circuitBreaker.reset();
472
+ this.logger.info('Circuit breaker manually reset');
473
+ }
474
+ /**
475
+ * Check if the client has been disposed.
476
+ */
477
+ isDisposed() {
478
+ return this.disposed;
479
+ }
480
+ /**
481
+ * Get a snapshot of all collected impact metrics.
482
+ * Includes per-flag evaluation counts, cache hit rates, latency percentiles,
483
+ * and experiment exposure counts.
484
+ */
485
+ getImpactMetrics() {
486
+ return this.metrics.getSnapshot();
487
+ }
488
+ /**
489
+ * Reset all collected impact metrics counters.
490
+ */
491
+ resetMetrics() {
492
+ this.metrics.reset();
493
+ }
494
+ /**
495
+ * Dispose the client, releasing all resources (timers, listeners, metrics).
496
+ * After calling dispose, the client cannot be used again.
497
+ */
498
+ dispose() {
499
+ this.disposed = true;
500
+ this.cache.destroy();
501
+ this.metrics.destroy();
502
+ this.streamClient?.dispose();
503
+ this.events.removeAllListeners();
504
+ this.previousValues.clear();
505
+ this.logger.debug('Client disposed');
506
+ }
507
+ // ═══════════════════════════════════════════════════════════════════════════
508
+ // INTERNALS
509
+ // ═══════════════════════════════════════════════════════════════════════════
510
+ async fetchWithResiliency(fn) {
511
+ return this.circuitBreaker.execute(() => withRetry(fn, this.retryConfig, this.logger, (attempt, error) => {
512
+ const errorMessage = error instanceof Error ? error.message : String(error);
513
+ this.events.emit('requestFailed', {
514
+ endpoint: 'unknown',
515
+ error: errorMessage,
516
+ attempt,
517
+ });
518
+ }));
519
+ }
520
+ buildCacheKey(prefix, slug, context) {
521
+ const parts = [prefix];
522
+ if (slug)
523
+ parts.push(slug);
524
+ if (context?.workspaceId)
525
+ parts.push(`w:${context.workspaceId}`);
526
+ if (context?.userId)
527
+ parts.push(`u:${context.userId}`);
528
+ if (context?.attributes) {
529
+ const sorted = Object.keys(context.attributes).sort();
530
+ for (const k of sorted) {
531
+ parts.push(`${k}:${context.attributes[k]}`);
532
+ }
533
+ }
534
+ return parts.join(':');
535
+ }
536
+ detectChange(slug, newValue) {
537
+ const previousValue = this.previousValues.get(slug);
538
+ if (previousValue !== undefined && previousValue !== newValue) {
539
+ this.events.emit('flagChanged', {
540
+ slug,
541
+ previousValue,
542
+ newValue,
543
+ });
544
+ }
545
+ this.previousValues.set(slug, newValue);
546
+ }
547
+ emitEvaluated(slug, value, reason, startTime) {
548
+ this.events.emit('flagEvaluated', {
549
+ slug,
550
+ value,
551
+ reason: reason,
552
+ durationMs: Date.now() - startTime,
553
+ });
554
+ }
555
+ assertNotDisposed() {
556
+ if (this.disposed) {
557
+ throw new Error('FeatureFlagsClient has been disposed. Create a new instance.');
558
+ }
559
+ }
560
+ }
@@ -0,0 +1,35 @@
1
+ import { FlagDocument, EvaluationContext, FlagValue, ExperimentAssignment, TrackingCallback } from './types';
2
+ /**
3
+ * Result of an edge evaluation for a single flag.
4
+ */
5
+ export interface EdgeEvaluationResult {
6
+ value: FlagValue;
7
+ reason: 'TARGETING_MATCH' | 'PERCENTAGE_ROLLOUT' | 'EXPERIMENT_ASSIGNMENT' | 'DEFAULT' | 'LOCAL_OVERRIDE';
8
+ assignment?: ExperimentAssignment;
9
+ }
10
+ /**
11
+ * Offline / Local Evaluator Engine.
12
+ * Takes a pre-fetched `FlagDocument` and evaluates flags entirely in-memory
13
+ * without making any HTTP calls. Perfect for edge workers or serverless.
14
+ */
15
+ export declare class EdgeEvaluator {
16
+ private document;
17
+ private flagIndex;
18
+ private readonly fallbackDefaults;
19
+ private readonly trackingCallback?;
20
+ constructor(document: FlagDocument, fallbackDefaults?: Record<string, FlagValue>, trackingCallback?: TrackingCallback);
21
+ /**
22
+ * Update the internal document with a fresh one.
23
+ */
24
+ updateDocument(document: FlagDocument): void;
25
+ private rebuildIndex;
26
+ /**
27
+ * Evaluate a single flag against a context.
28
+ */
29
+ evaluate<T extends FlagValue = boolean>(slug: string, context: EvaluationContext, localOverrides?: Record<string, FlagValue>): EdgeEvaluationResult;
30
+ /**
31
+ * Evaluate all flags in the document at once.
32
+ */
33
+ evaluateAll(context: EvaluationContext, localOverrides?: Record<string, FlagValue>): Record<string, FlagValue>;
34
+ private evaluateInner;
35
+ }