@savvagent/angular 1.0.1
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/CHANGELOG.md +26 -0
- package/LICENSE +21 -0
- package/README.md +484 -0
- package/coverage/base.css +224 -0
- package/coverage/block-navigation.js +87 -0
- package/coverage/favicon.png +0 -0
- package/coverage/index.html +131 -0
- package/coverage/lcov-report/base.css +224 -0
- package/coverage/lcov-report/block-navigation.js +87 -0
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/index.html +131 -0
- package/coverage/lcov-report/module.ts.html +289 -0
- package/coverage/lcov-report/prettify.css +1 -0
- package/coverage/lcov-report/prettify.js +2 -0
- package/coverage/lcov-report/service.ts.html +1846 -0
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +210 -0
- package/coverage/lcov.info +242 -0
- package/coverage/module.ts.html +289 -0
- package/coverage/prettify.css +1 -0
- package/coverage/prettify.js +2 -0
- package/coverage/service.ts.html +1846 -0
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +210 -0
- package/dist/README.md +484 -0
- package/dist/esm2022/index.mjs +15 -0
- package/dist/esm2022/module.mjs +75 -0
- package/dist/esm2022/savvagent-angular.mjs +5 -0
- package/dist/esm2022/service.mjs +473 -0
- package/dist/fesm2022/savvagent-angular.mjs +563 -0
- package/dist/fesm2022/savvagent-angular.mjs.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/module.d.ts +57 -0
- package/dist/service.d.ts +319 -0
- package/jest.config.js +40 -0
- package/ng-package.json +8 -0
- package/package.json +73 -0
- package/setup-jest.ts +2 -0
- package/src/index.spec.ts +144 -0
- package/src/index.ts +38 -0
- package/src/module.spec.ts +283 -0
- package/src/module.ts +68 -0
- package/src/service.spec.ts +945 -0
- package/src/service.ts +587 -0
- package/test-utils/angular-core-mock.ts +28 -0
- package/test-utils/angular-testing-mock.ts +87 -0
- package/tsconfig.json +33 -0
- package/tsconfig.spec.json +11 -0
package/src/service.ts
ADDED
|
@@ -0,0 +1,587 @@
|
|
|
1
|
+
import { Injectable, OnDestroy, Inject, InjectionToken, Optional } from '@angular/core';
|
|
2
|
+
import { BehaviorSubject, Observable, from, of, Subject } from 'rxjs';
|
|
3
|
+
import { map, takeUntil, catchError, distinctUntilChanged } from 'rxjs/operators';
|
|
4
|
+
import { FlagClient, FlagClientConfig, FlagContext, FlagEvaluationResult, FlagDefinition } from '@savvagent/sdk';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Default context values that apply to all flag evaluations
|
|
8
|
+
* Per SDK Developer Guide: https://docs.savvagent.com/sdk-developer-guide
|
|
9
|
+
*/
|
|
10
|
+
export interface DefaultFlagContext {
|
|
11
|
+
/** Application ID for application-scoped flags */
|
|
12
|
+
applicationId?: string;
|
|
13
|
+
/** Environment (development, staging, production) */
|
|
14
|
+
environment?: string;
|
|
15
|
+
/** Organization ID for multi-tenant apps */
|
|
16
|
+
organizationId?: string;
|
|
17
|
+
/** Default user ID (required for percentage rollouts) */
|
|
18
|
+
userId?: string;
|
|
19
|
+
/** Default anonymous ID (alternative to userId for anonymous users) */
|
|
20
|
+
anonymousId?: string;
|
|
21
|
+
/** Session ID as fallback identifier */
|
|
22
|
+
sessionId?: string;
|
|
23
|
+
/** User's language code (e.g., "en", "es") */
|
|
24
|
+
language?: string;
|
|
25
|
+
/** Default attributes for targeting */
|
|
26
|
+
attributes?: Record<string, any>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Configuration for the Savvagent Angular service
|
|
31
|
+
*/
|
|
32
|
+
export interface SavvagentConfig {
|
|
33
|
+
/** SDK API key configuration */
|
|
34
|
+
config: FlagClientConfig;
|
|
35
|
+
/** Default context values applied to all flag evaluations */
|
|
36
|
+
defaultContext?: DefaultFlagContext;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Injection token for Savvagent configuration
|
|
41
|
+
*/
|
|
42
|
+
export const SAVVAGENT_CONFIG = new InjectionToken<SavvagentConfig>('SAVVAGENT_CONFIG');
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Result from flag evaluation as an Observable
|
|
46
|
+
*/
|
|
47
|
+
export interface FlagObservableResult {
|
|
48
|
+
/** Current flag value */
|
|
49
|
+
value: boolean;
|
|
50
|
+
/** Whether the flag is currently being evaluated */
|
|
51
|
+
loading: boolean;
|
|
52
|
+
/** Error if evaluation failed */
|
|
53
|
+
error: Error | null;
|
|
54
|
+
/** Detailed evaluation result */
|
|
55
|
+
result: FlagEvaluationResult | null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Options for flag evaluation
|
|
60
|
+
*/
|
|
61
|
+
export interface FlagOptions {
|
|
62
|
+
/** Context for flag evaluation (user_id, attributes, etc.) */
|
|
63
|
+
context?: FlagContext;
|
|
64
|
+
/** Default value to use while loading or on error */
|
|
65
|
+
defaultValue?: boolean;
|
|
66
|
+
/** Enable real-time updates for this flag */
|
|
67
|
+
realtime?: boolean;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Angular service for Savvagent feature flags.
|
|
72
|
+
* Provides reactive flag evaluation using RxJS Observables.
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* ```typescript
|
|
76
|
+
* // In your component
|
|
77
|
+
* @Component({...})
|
|
78
|
+
* export class MyComponent {
|
|
79
|
+
* newFeature$ = this.savvagent.flag$('new-feature');
|
|
80
|
+
*
|
|
81
|
+
* constructor(private savvagent: SavvagentService) {}
|
|
82
|
+
* }
|
|
83
|
+
*
|
|
84
|
+
* // In your template
|
|
85
|
+
* <div *ngIf="(newFeature$ | async)?.value">
|
|
86
|
+
* New feature content!
|
|
87
|
+
* </div>
|
|
88
|
+
* ```
|
|
89
|
+
*/
|
|
90
|
+
@Injectable({
|
|
91
|
+
providedIn: 'root'
|
|
92
|
+
})
|
|
93
|
+
export class SavvagentService implements OnDestroy {
|
|
94
|
+
private client: FlagClient | null = null;
|
|
95
|
+
private destroy$ = new Subject<void>();
|
|
96
|
+
private isReady$ = new BehaviorSubject<boolean>(false);
|
|
97
|
+
private defaultContext: FlagContext = {};
|
|
98
|
+
private flagSubjects = new Map<string, BehaviorSubject<FlagObservableResult>>();
|
|
99
|
+
|
|
100
|
+
constructor(
|
|
101
|
+
@Optional() @Inject(SAVVAGENT_CONFIG) config?: SavvagentConfig
|
|
102
|
+
) {
|
|
103
|
+
if (config) {
|
|
104
|
+
this.initialize(config);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Initialize the Savvagent client with configuration.
|
|
110
|
+
* Call this if not using the SAVVAGENT_CONFIG injection token.
|
|
111
|
+
*
|
|
112
|
+
* @param savvagentConfig - Configuration including API key and default context
|
|
113
|
+
*
|
|
114
|
+
* @example
|
|
115
|
+
* ```typescript
|
|
116
|
+
* @Component({...})
|
|
117
|
+
* export class AppComponent implements OnInit {
|
|
118
|
+
* constructor(private savvagent: SavvagentService) {}
|
|
119
|
+
*
|
|
120
|
+
* ngOnInit() {
|
|
121
|
+
* this.savvagent.initialize({
|
|
122
|
+
* config: { apiKey: 'sdk_...' },
|
|
123
|
+
* defaultContext: {
|
|
124
|
+
* applicationId: 'my-app',
|
|
125
|
+
* environment: 'development',
|
|
126
|
+
* userId: 'user-123'
|
|
127
|
+
* }
|
|
128
|
+
* });
|
|
129
|
+
* }
|
|
130
|
+
* }
|
|
131
|
+
* ```
|
|
132
|
+
*/
|
|
133
|
+
initialize(savvagentConfig: SavvagentConfig): void {
|
|
134
|
+
if (this.client) {
|
|
135
|
+
console.warn('[Savvagent] Client already initialized. Call close() first to reinitialize.');
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
this.client = new FlagClient(savvagentConfig.config);
|
|
141
|
+
|
|
142
|
+
// Convert DefaultFlagContext to FlagContext format (camelCase to snake_case)
|
|
143
|
+
if (savvagentConfig.defaultContext) {
|
|
144
|
+
this.defaultContext = {
|
|
145
|
+
application_id: savvagentConfig.defaultContext.applicationId,
|
|
146
|
+
environment: savvagentConfig.defaultContext.environment,
|
|
147
|
+
organization_id: savvagentConfig.defaultContext.organizationId,
|
|
148
|
+
user_id: savvagentConfig.defaultContext.userId,
|
|
149
|
+
anonymous_id: savvagentConfig.defaultContext.anonymousId,
|
|
150
|
+
session_id: savvagentConfig.defaultContext.sessionId,
|
|
151
|
+
language: savvagentConfig.defaultContext.language,
|
|
152
|
+
attributes: savvagentConfig.defaultContext.attributes,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
this.isReady$.next(true);
|
|
157
|
+
|
|
158
|
+
// Subscribe to override changes to re-evaluate all active flags
|
|
159
|
+
this.client.onOverrideChange(() => {
|
|
160
|
+
this.reEvaluateAllFlags();
|
|
161
|
+
});
|
|
162
|
+
} catch (error) {
|
|
163
|
+
console.error('[Savvagent] Failed to initialize client:', error);
|
|
164
|
+
savvagentConfig.config.onError?.(error as Error);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Observable that emits true when the client is ready.
|
|
170
|
+
*/
|
|
171
|
+
get ready$(): Observable<boolean> {
|
|
172
|
+
return this.isReady$.asObservable();
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Check if the client is ready.
|
|
177
|
+
*/
|
|
178
|
+
get isReady(): boolean {
|
|
179
|
+
return this.isReady$.value;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Get the underlying FlagClient instance for advanced use cases.
|
|
184
|
+
*/
|
|
185
|
+
get flagClient(): FlagClient | null {
|
|
186
|
+
return this.client;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Merge default context with per-call context.
|
|
191
|
+
*/
|
|
192
|
+
private mergeContext(context?: FlagContext): FlagContext {
|
|
193
|
+
return {
|
|
194
|
+
...this.defaultContext,
|
|
195
|
+
...context,
|
|
196
|
+
attributes: {
|
|
197
|
+
...this.defaultContext.attributes,
|
|
198
|
+
...context?.attributes,
|
|
199
|
+
},
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Get a reactive Observable for a feature flag.
|
|
205
|
+
* Automatically updates when the flag value changes.
|
|
206
|
+
*
|
|
207
|
+
* @param flagKey - The feature flag key to evaluate
|
|
208
|
+
* @param options - Configuration options
|
|
209
|
+
* @returns Observable of flag evaluation state
|
|
210
|
+
*
|
|
211
|
+
* @example
|
|
212
|
+
* ```typescript
|
|
213
|
+
* // In your component
|
|
214
|
+
* newFeature$ = this.savvagent.flag$('new-feature', {
|
|
215
|
+
* defaultValue: false,
|
|
216
|
+
* realtime: true,
|
|
217
|
+
* context: { attributes: { plan: 'pro' } }
|
|
218
|
+
* });
|
|
219
|
+
*
|
|
220
|
+
* // In template
|
|
221
|
+
* <ng-container *ngIf="newFeature$ | async as flag">
|
|
222
|
+
* <app-loading *ngIf="flag.loading"></app-loading>
|
|
223
|
+
* <app-new-feature *ngIf="flag.value"></app-new-feature>
|
|
224
|
+
* <app-old-feature *ngIf="!flag.value && !flag.loading"></app-old-feature>
|
|
225
|
+
* </ng-container>
|
|
226
|
+
* ```
|
|
227
|
+
*/
|
|
228
|
+
flag$(flagKey: string, options: FlagOptions = {}): Observable<FlagObservableResult> {
|
|
229
|
+
const { context, defaultValue = false, realtime = true } = options;
|
|
230
|
+
const mergedContext = this.mergeContext(context);
|
|
231
|
+
const cacheKey = this.getCacheKey(flagKey, mergedContext);
|
|
232
|
+
|
|
233
|
+
// Check if we already have a subject for this flag+context
|
|
234
|
+
if (!this.flagSubjects.has(cacheKey)) {
|
|
235
|
+
const subject = new BehaviorSubject<FlagObservableResult>({
|
|
236
|
+
value: defaultValue,
|
|
237
|
+
loading: true,
|
|
238
|
+
error: null,
|
|
239
|
+
result: null,
|
|
240
|
+
});
|
|
241
|
+
this.flagSubjects.set(cacheKey, subject);
|
|
242
|
+
|
|
243
|
+
// Initial evaluation
|
|
244
|
+
this.evaluateAndEmit(flagKey, mergedContext, defaultValue, subject);
|
|
245
|
+
|
|
246
|
+
// Set up real-time subscription if enabled
|
|
247
|
+
if (realtime && this.client) {
|
|
248
|
+
const unsubscribe = this.client.subscribe(flagKey, () => {
|
|
249
|
+
this.evaluateAndEmit(flagKey, mergedContext, defaultValue, subject);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
// Clean up subscription when subject is complete
|
|
253
|
+
subject.pipe(takeUntil(this.destroy$)).subscribe({
|
|
254
|
+
complete: () => unsubscribe(),
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return this.flagSubjects.get(cacheKey)!.asObservable().pipe(
|
|
260
|
+
takeUntil(this.destroy$),
|
|
261
|
+
distinctUntilChanged((a, b) =>
|
|
262
|
+
a.value === b.value &&
|
|
263
|
+
a.loading === b.loading &&
|
|
264
|
+
a.error === b.error
|
|
265
|
+
)
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Generate a cache key for a flag+context combination.
|
|
271
|
+
*/
|
|
272
|
+
private getCacheKey(flagKey: string, context: FlagContext): string {
|
|
273
|
+
return `${flagKey}:${JSON.stringify(context)}`;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Evaluate a flag and emit the result to a subject.
|
|
278
|
+
*/
|
|
279
|
+
private async evaluateAndEmit(
|
|
280
|
+
flagKey: string,
|
|
281
|
+
context: FlagContext,
|
|
282
|
+
defaultValue: boolean,
|
|
283
|
+
subject: BehaviorSubject<FlagObservableResult>
|
|
284
|
+
): Promise<void> {
|
|
285
|
+
if (!this.client) {
|
|
286
|
+
subject.next({
|
|
287
|
+
value: defaultValue,
|
|
288
|
+
loading: false,
|
|
289
|
+
error: new Error('Savvagent client not initialized'),
|
|
290
|
+
result: null,
|
|
291
|
+
});
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
try {
|
|
296
|
+
const result = await this.client.evaluate(flagKey, context);
|
|
297
|
+
subject.next({
|
|
298
|
+
value: result.value,
|
|
299
|
+
loading: false,
|
|
300
|
+
error: null,
|
|
301
|
+
result,
|
|
302
|
+
});
|
|
303
|
+
} catch (error) {
|
|
304
|
+
subject.next({
|
|
305
|
+
value: defaultValue,
|
|
306
|
+
loading: false,
|
|
307
|
+
error: error as Error,
|
|
308
|
+
result: null,
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Re-evaluate all active flag subscriptions.
|
|
315
|
+
* Called when overrides change.
|
|
316
|
+
*/
|
|
317
|
+
private reEvaluateAllFlags(): void {
|
|
318
|
+
this.flagSubjects.forEach((subject, cacheKey) => {
|
|
319
|
+
const [flagKey, contextJson] = cacheKey.split(':', 2);
|
|
320
|
+
const context = JSON.parse(contextJson || '{}');
|
|
321
|
+
const currentValue = subject.value;
|
|
322
|
+
this.evaluateAndEmit(flagKey, context, currentValue.value, subject);
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Get a flag value as a simple Observable<boolean>.
|
|
328
|
+
* Useful when you only need the value without loading/error states.
|
|
329
|
+
*
|
|
330
|
+
* @param flagKey - The feature flag key to evaluate
|
|
331
|
+
* @param options - Configuration options
|
|
332
|
+
* @returns Observable of boolean flag value
|
|
333
|
+
*
|
|
334
|
+
* @example
|
|
335
|
+
* ```typescript
|
|
336
|
+
* isFeatureEnabled$ = this.savvagent.flagValue$('my-feature');
|
|
337
|
+
*
|
|
338
|
+
* // In template
|
|
339
|
+
* <button *ngIf="isFeatureEnabled$ | async">New Button</button>
|
|
340
|
+
* ```
|
|
341
|
+
*/
|
|
342
|
+
flagValue$(flagKey: string, options: FlagOptions = {}): Observable<boolean> {
|
|
343
|
+
return this.flag$(flagKey, options).pipe(
|
|
344
|
+
map((result) => result.value),
|
|
345
|
+
distinctUntilChanged()
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Evaluate a feature flag once (non-reactive).
|
|
351
|
+
* For reactive updates, use flag$() instead.
|
|
352
|
+
*
|
|
353
|
+
* @param flagKey - The feature flag key to evaluate
|
|
354
|
+
* @param context - Optional context for targeting
|
|
355
|
+
* @returns Promise with detailed evaluation result
|
|
356
|
+
*
|
|
357
|
+
* @example
|
|
358
|
+
* ```typescript
|
|
359
|
+
* async checkFeature() {
|
|
360
|
+
* const result = await this.savvagent.evaluate('new-feature');
|
|
361
|
+
* if (result.value) {
|
|
362
|
+
* // Feature is enabled
|
|
363
|
+
* }
|
|
364
|
+
* }
|
|
365
|
+
* ```
|
|
366
|
+
*/
|
|
367
|
+
async evaluate(flagKey: string, context?: FlagContext): Promise<FlagEvaluationResult> {
|
|
368
|
+
if (!this.client) {
|
|
369
|
+
throw new Error('Savvagent client not initialized');
|
|
370
|
+
}
|
|
371
|
+
return this.client.evaluate(flagKey, this.mergeContext(context));
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Check if a feature flag is enabled (non-reactive).
|
|
376
|
+
*
|
|
377
|
+
* @param flagKey - The feature flag key to evaluate
|
|
378
|
+
* @param context - Optional context for targeting
|
|
379
|
+
* @returns Promise<boolean>
|
|
380
|
+
*/
|
|
381
|
+
async isEnabled(flagKey: string, context?: FlagContext): Promise<boolean> {
|
|
382
|
+
if (!this.client) {
|
|
383
|
+
return false;
|
|
384
|
+
}
|
|
385
|
+
return this.client.isEnabled(flagKey, this.mergeContext(context));
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Execute code conditionally based on flag value.
|
|
390
|
+
*
|
|
391
|
+
* @param flagKey - The flag key to check
|
|
392
|
+
* @param callback - Function to execute if flag is enabled
|
|
393
|
+
* @param context - Optional context for targeting
|
|
394
|
+
*
|
|
395
|
+
* @example
|
|
396
|
+
* ```typescript
|
|
397
|
+
* await this.savvagent.withFlag('analytics-enabled', async () => {
|
|
398
|
+
* await this.analytics.track('page_view');
|
|
399
|
+
* });
|
|
400
|
+
* ```
|
|
401
|
+
*/
|
|
402
|
+
async withFlag<T>(
|
|
403
|
+
flagKey: string,
|
|
404
|
+
callback: () => T | Promise<T>,
|
|
405
|
+
context?: FlagContext
|
|
406
|
+
): Promise<T | null> {
|
|
407
|
+
if (!this.client) {
|
|
408
|
+
return null;
|
|
409
|
+
}
|
|
410
|
+
return this.client.withFlag(flagKey, callback, this.mergeContext(context));
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Track an error with flag context.
|
|
415
|
+
*
|
|
416
|
+
* @param flagKey - The flag key associated with the error
|
|
417
|
+
* @param error - The error that occurred
|
|
418
|
+
* @param context - Optional context
|
|
419
|
+
*/
|
|
420
|
+
trackError(flagKey: string, error: Error, context?: FlagContext): void {
|
|
421
|
+
this.client?.trackError(flagKey, error, this.mergeContext(context));
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Set the user ID for logged-in users.
|
|
426
|
+
*
|
|
427
|
+
* @param userId - The user ID (or null to clear)
|
|
428
|
+
*/
|
|
429
|
+
setUserId(userId: string | null): void {
|
|
430
|
+
this.client?.setUserId(userId);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Get the current user ID.
|
|
435
|
+
*/
|
|
436
|
+
getUserId(): string | null {
|
|
437
|
+
return this.client?.getUserId() || null;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Get the current anonymous ID.
|
|
442
|
+
*/
|
|
443
|
+
getAnonymousId(): string | null {
|
|
444
|
+
return this.client?.getAnonymousId() || null;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Set a custom anonymous ID.
|
|
449
|
+
*/
|
|
450
|
+
setAnonymousId(id: string): void {
|
|
451
|
+
this.client?.setAnonymousId(id);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// =====================
|
|
455
|
+
// Local Override Methods
|
|
456
|
+
// =====================
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Set a local override for a flag.
|
|
460
|
+
* Overrides take precedence over server values.
|
|
461
|
+
*
|
|
462
|
+
* @param flagKey - The flag key to override
|
|
463
|
+
* @param value - The override value
|
|
464
|
+
*/
|
|
465
|
+
setOverride(flagKey: string, value: boolean): void {
|
|
466
|
+
this.client?.setOverride(flagKey, value);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Clear a local override for a flag.
|
|
471
|
+
*/
|
|
472
|
+
clearOverride(flagKey: string): void {
|
|
473
|
+
this.client?.clearOverride(flagKey);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Clear all local overrides.
|
|
478
|
+
*/
|
|
479
|
+
clearAllOverrides(): void {
|
|
480
|
+
this.client?.clearAllOverrides();
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Check if a flag has a local override.
|
|
485
|
+
*/
|
|
486
|
+
hasOverride(flagKey: string): boolean {
|
|
487
|
+
return this.client?.hasOverride(flagKey) || false;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Get the override value for a flag.
|
|
492
|
+
*/
|
|
493
|
+
getOverride(flagKey: string): boolean | undefined {
|
|
494
|
+
return this.client?.getOverride(flagKey);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Get all current overrides.
|
|
499
|
+
*/
|
|
500
|
+
getOverrides(): Record<string, boolean> {
|
|
501
|
+
return this.client?.getOverrides() || {};
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Set multiple overrides at once.
|
|
506
|
+
*/
|
|
507
|
+
setOverrides(overrides: Record<string, boolean>): void {
|
|
508
|
+
this.client?.setOverrides(overrides);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// =====================
|
|
512
|
+
// Flag Discovery Methods
|
|
513
|
+
// =====================
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* Get all flags for the application.
|
|
517
|
+
*
|
|
518
|
+
* @param environment - Environment to evaluate (default: 'development')
|
|
519
|
+
* @returns Observable of flag definitions
|
|
520
|
+
*/
|
|
521
|
+
getAllFlags$(environment: string = 'development'): Observable<FlagDefinition[]> {
|
|
522
|
+
if (!this.client) {
|
|
523
|
+
return of([]);
|
|
524
|
+
}
|
|
525
|
+
return from(this.client.getAllFlags(environment)).pipe(
|
|
526
|
+
catchError((error) => {
|
|
527
|
+
console.error('[Savvagent] Failed to fetch all flags:', error);
|
|
528
|
+
return of([]);
|
|
529
|
+
})
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Get all flags for the application (Promise-based).
|
|
535
|
+
*/
|
|
536
|
+
async getAllFlags(environment: string = 'development'): Promise<FlagDefinition[]> {
|
|
537
|
+
if (!this.client) {
|
|
538
|
+
return [];
|
|
539
|
+
}
|
|
540
|
+
return this.client.getAllFlags(environment);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* Get enterprise-scoped flags only.
|
|
545
|
+
*/
|
|
546
|
+
async getEnterpriseFlags(environment: string = 'development'): Promise<FlagDefinition[]> {
|
|
547
|
+
if (!this.client) {
|
|
548
|
+
return [];
|
|
549
|
+
}
|
|
550
|
+
return this.client.getEnterpriseFlags(environment);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// =====================
|
|
554
|
+
// Cache & Connection
|
|
555
|
+
// =====================
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Clear the flag cache.
|
|
559
|
+
*/
|
|
560
|
+
clearCache(): void {
|
|
561
|
+
this.client?.clearCache();
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* Check if real-time connection is active.
|
|
566
|
+
*/
|
|
567
|
+
isRealtimeConnected(): boolean {
|
|
568
|
+
return this.client?.isRealtimeConnected() || false;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* Close the client and cleanup resources.
|
|
573
|
+
*/
|
|
574
|
+
close(): void {
|
|
575
|
+
this.client?.close();
|
|
576
|
+
this.client = null;
|
|
577
|
+
this.isReady$.next(false);
|
|
578
|
+
this.flagSubjects.forEach((subject) => subject.complete());
|
|
579
|
+
this.flagSubjects.clear();
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
ngOnDestroy(): void {
|
|
583
|
+
this.destroy$.next();
|
|
584
|
+
this.destroy$.complete();
|
|
585
|
+
this.close();
|
|
586
|
+
}
|
|
587
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Mock Angular core for testing
|
|
2
|
+
|
|
3
|
+
export function NgModule(config: any): ClassDecorator {
|
|
4
|
+
return (target: any) => target;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function Injectable(config?: any): ClassDecorator {
|
|
8
|
+
return (target: any) => target;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function Inject(token: any): ParameterDecorator {
|
|
12
|
+
return (target: any, propertyKey: string | symbol | undefined, parameterIndex: number) => {};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function Optional(): ParameterDecorator {
|
|
16
|
+
return (target: any, propertyKey: string | symbol | undefined, parameterIndex: number) => {};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function OnDestroy(): void {}
|
|
20
|
+
|
|
21
|
+
export class InjectionToken<T> {
|
|
22
|
+
constructor(public description: string, options?: any) {}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface ModuleWithProviders<T = any> {
|
|
26
|
+
ngModule: any;
|
|
27
|
+
providers?: any[];
|
|
28
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// Mock Angular testing utilities
|
|
2
|
+
|
|
3
|
+
class MockTestBed {
|
|
4
|
+
private static providers: any[] = [];
|
|
5
|
+
private static instances = new Map<any, any>();
|
|
6
|
+
|
|
7
|
+
static configureTestingModule(config: { imports?: any[]; providers?: any[] }): typeof MockTestBed {
|
|
8
|
+
this.providers = [];
|
|
9
|
+
this.instances.clear();
|
|
10
|
+
|
|
11
|
+
if (config.imports) {
|
|
12
|
+
config.imports.forEach((imp) => {
|
|
13
|
+
if (imp.providers) {
|
|
14
|
+
this.providers.push(...imp.providers);
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (config.providers) {
|
|
20
|
+
this.providers.push(...config.providers);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return this;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
static inject<T>(token: any): T {
|
|
27
|
+
if (this.instances.has(token)) {
|
|
28
|
+
return this.instances.get(token);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Find provider for token
|
|
32
|
+
const provider = this.providers.find((p) => {
|
|
33
|
+
if (typeof p === 'function') return p === token;
|
|
34
|
+
if (p.provide) return p.provide === token;
|
|
35
|
+
return false;
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
let instance: any;
|
|
39
|
+
|
|
40
|
+
if (!provider) {
|
|
41
|
+
// Try to instantiate the token itself
|
|
42
|
+
if (typeof token === 'function') {
|
|
43
|
+
const deps = this.getDependencies(token);
|
|
44
|
+
instance = new token(...deps);
|
|
45
|
+
} else {
|
|
46
|
+
throw new Error(`No provider for ${token}`);
|
|
47
|
+
}
|
|
48
|
+
} else if (typeof provider === 'function') {
|
|
49
|
+
const deps = this.getDependencies(provider);
|
|
50
|
+
instance = new provider(...deps);
|
|
51
|
+
} else if (provider.useValue !== undefined) {
|
|
52
|
+
instance = provider.useValue;
|
|
53
|
+
} else if (provider.useClass) {
|
|
54
|
+
const deps = this.getDependencies(provider.useClass);
|
|
55
|
+
instance = new provider.useClass(...deps);
|
|
56
|
+
} else if (provider.useFactory) {
|
|
57
|
+
instance = provider.useFactory();
|
|
58
|
+
} else {
|
|
59
|
+
const deps = this.getDependencies(provider.provide);
|
|
60
|
+
instance = new provider.provide(...deps);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
this.instances.set(token, instance);
|
|
64
|
+
return instance;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
private static getDependencies(target: any): any[] {
|
|
68
|
+
// Look for providers that should be injected into the target
|
|
69
|
+
const deps: any[] = [];
|
|
70
|
+
|
|
71
|
+
// For services that expect config, find the config provider
|
|
72
|
+
this.providers.forEach((p) => {
|
|
73
|
+
if (typeof p === 'object' && p.provide && p.useValue !== undefined) {
|
|
74
|
+
deps.push(p.useValue);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
return deps;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
static resetTestingModule(): void {
|
|
82
|
+
this.providers = [];
|
|
83
|
+
this.instances.clear();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export const TestBed = MockTestBed;
|