@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.
Files changed (48) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/LICENSE +21 -0
  3. package/README.md +484 -0
  4. package/coverage/base.css +224 -0
  5. package/coverage/block-navigation.js +87 -0
  6. package/coverage/favicon.png +0 -0
  7. package/coverage/index.html +131 -0
  8. package/coverage/lcov-report/base.css +224 -0
  9. package/coverage/lcov-report/block-navigation.js +87 -0
  10. package/coverage/lcov-report/favicon.png +0 -0
  11. package/coverage/lcov-report/index.html +131 -0
  12. package/coverage/lcov-report/module.ts.html +289 -0
  13. package/coverage/lcov-report/prettify.css +1 -0
  14. package/coverage/lcov-report/prettify.js +2 -0
  15. package/coverage/lcov-report/service.ts.html +1846 -0
  16. package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  17. package/coverage/lcov-report/sorter.js +210 -0
  18. package/coverage/lcov.info +242 -0
  19. package/coverage/module.ts.html +289 -0
  20. package/coverage/prettify.css +1 -0
  21. package/coverage/prettify.js +2 -0
  22. package/coverage/service.ts.html +1846 -0
  23. package/coverage/sort-arrow-sprite.png +0 -0
  24. package/coverage/sorter.js +210 -0
  25. package/dist/README.md +484 -0
  26. package/dist/esm2022/index.mjs +15 -0
  27. package/dist/esm2022/module.mjs +75 -0
  28. package/dist/esm2022/savvagent-angular.mjs +5 -0
  29. package/dist/esm2022/service.mjs +473 -0
  30. package/dist/fesm2022/savvagent-angular.mjs +563 -0
  31. package/dist/fesm2022/savvagent-angular.mjs.map +1 -0
  32. package/dist/index.d.ts +13 -0
  33. package/dist/module.d.ts +57 -0
  34. package/dist/service.d.ts +319 -0
  35. package/jest.config.js +40 -0
  36. package/ng-package.json +8 -0
  37. package/package.json +73 -0
  38. package/setup-jest.ts +2 -0
  39. package/src/index.spec.ts +144 -0
  40. package/src/index.ts +38 -0
  41. package/src/module.spec.ts +283 -0
  42. package/src/module.ts +68 -0
  43. package/src/service.spec.ts +945 -0
  44. package/src/service.ts +587 -0
  45. package/test-utils/angular-core-mock.ts +28 -0
  46. package/test-utils/angular-testing-mock.ts +87 -0
  47. package/tsconfig.json +33 -0
  48. 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;