@adimm/x-injection 3.0.0 → 3.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 (2) hide show
  1. package/README.md +2400 -246
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -13,40 +13,287 @@
13
13
 
14
14
  </p>
15
15
 
16
+ **A powerful, modular dependency injection library for TypeScript** — Built on [InversifyJS](https://github.com/inversify/InversifyJS), inspired by [NestJS](https://github.com/nestjs/nest)'s elegant module architecture.
17
+
16
18
  ## Table of Contents
17
19
 
18
20
  - [Table of Contents](#table-of-contents)
21
+ - [What Problems Does This Solve?](#what-problems-does-this-solve)
22
+ - [Problem 1: Manual Dependency Wiring](#problem-1-manual-dependency-wiring)
23
+ - [Problem 2: Tight Coupling and Testing Difficulty](#problem-2-tight-coupling-and-testing-difficulty)
24
+ - [Problem 3: Lack of Encapsulation](#problem-3-lack-of-encapsulation)
25
+ - [Problem 4: Lifecycle Management Complexity](#problem-4-lifecycle-management-complexity)
19
26
  - [Overview](#overview)
20
27
  - [Features](#features)
21
28
  - [Installation](#installation)
22
29
  - [Quick Start](#quick-start)
23
- - [OOP-Style Modules](#oop-style-modules)
30
+ - [Core Concepts](#core-concepts)
31
+ - [Services with @Injectable](#services-with-injectable)
32
+ - [Modules](#modules)
33
+ - [Blueprints](#blueprints)
34
+ - [AppModule](#appmodule)
35
+ - [OOP-Style Modules with ProviderModuleClass](#oop-style-modules-with-providermoduleclass)
24
36
  - [Basic OOP Module](#basic-oop-module)
25
37
  - [Advanced OOP Patterns](#advanced-oop-patterns)
26
38
  - [When to Use OOP vs Functional](#when-to-use-oop-vs-functional)
27
- - [Core Concepts](#core-concepts)
28
- - [ProviderModule](#providermodule)
29
- - [AppModule](#appmodule)
30
- - [Blueprints](#blueprints)
31
- - [Provider Tokens](#provider-tokens)
39
+ - [Provider Tokens](#provider-tokens)
40
+ - [1. Class Token](#1-class-token)
41
+ - [2. Class Token with Substitution](#2-class-token-with-substitution)
42
+ - [3. Value Token](#3-value-token)
43
+ - [4. Factory Token](#4-factory-token)
32
44
  - [Injection Scopes](#injection-scopes)
33
45
  - [Singleton (Default)](#singleton-default)
34
46
  - [Transient](#transient)
35
47
  - [Request](#request)
48
+ - [Scope Priority Order](#scope-priority-order)
36
49
  - [Module System](#module-system)
37
50
  - [Import/Export Pattern](#importexport-pattern)
38
51
  - [Re-exporting Modules](#re-exporting-modules)
39
52
  - [Dynamic Module Updates](#dynamic-module-updates)
40
53
  - [Global Modules](#global-modules)
41
- - [Advanced Features](#advanced-features)
42
- - [Events](#events)
43
- - [Middlewares](#middlewares)
54
+ - [Dependency Injection](#dependency-injection)
55
+ - [Constructor Injection](#constructor-injection)
56
+ - [@Inject Decorator](#inject-decorator)
57
+ - [@MultiInject Decorator](#multiinject-decorator)
58
+ - [Optional Dependencies](#optional-dependencies)
59
+ - [Lifecycle Hooks](#lifecycle-hooks)
60
+ - [onReady Hook](#onready-hook)
61
+ - [onReset Hook](#onreset-hook)
62
+ - [onDispose Hook](#ondispose-hook)
63
+ - [Events System](#events-system)
64
+ - [Subscribing to Events](#subscribing-to-events)
65
+ - [Available Event Types](#available-event-types)
66
+ - [Event Use Cases](#event-use-cases)
67
+ - [Middlewares](#middlewares)
68
+ - [BeforeGet Middleware](#beforeget-middleware)
69
+ - [BeforeAddProvider Middleware](#beforeaddprovider-middleware)
70
+ - [BeforeAddImport Middleware](#beforeaddimport-middleware)
71
+ - [OnExportAccess Middleware](#onexportaccess-middleware)
72
+ - [BeforeRemoveImport Middleware](#beforeremoveimport-middleware)
73
+ - [BeforeRemoveProvider Middleware](#beforeremoveprovider-middleware)
74
+ - [BeforeRemoveExport Middleware](#beforeremoveexport-middleware)
75
+ - [All Available Middleware Types](#all-available-middleware-types)
44
76
  - [Testing](#testing)
77
+ - [Blueprint Cloning](#blueprint-cloning)
78
+ - [Provider Substitution](#provider-substitution)
79
+ - [Mocking Services](#mocking-services)
80
+ - [Advanced Module API](#advanced-module-api)
81
+ - [Query Methods](#query-methods)
82
+ - [Multiple Provider Binding](#multiple-provider-binding)
83
+ - [Batch Resolution with getMany()](#batch-resolution-with-getmany)
45
84
  - [Resources](#resources)
46
85
  - [Contributing](#contributing)
47
86
  - [Credits](#credits)
48
87
  - [License](#license)
49
88
 
89
+ ## What Problems Does This Solve?
90
+
91
+ Modern applications face several dependency management challenges. Let's examine these problems and how xInjection solves them.
92
+
93
+ ### Problem 1: Manual Dependency Wiring
94
+
95
+ **Without xInjection:**
96
+
97
+ ```ts
98
+ // Manually creating and wiring dependencies
99
+ class DatabaseService {
100
+ constructor(private config: ConfigService) {}
101
+ }
102
+
103
+ class UserRepository {
104
+ constructor(private db: DatabaseService) {}
105
+ }
106
+
107
+ class AuthService {
108
+ constructor(private userRepo: UserRepository) {}
109
+ }
110
+
111
+ // Manual instantiation nightmare
112
+ const config = new ConfigService();
113
+ const database = new DatabaseService(config);
114
+ const userRepo = new UserRepository(database);
115
+ const authService = new AuthService(userRepo);
116
+
117
+ // Every file needs to repeat this setup
118
+ // Changes to constructors require updating all instantiation sites
119
+ ```
120
+
121
+ **With xInjection:**
122
+
123
+ ```ts
124
+ @Injectable()
125
+ class DatabaseService {
126
+ constructor(private config: ConfigService) {}
127
+ }
128
+
129
+ @Injectable()
130
+ class UserRepository {
131
+ constructor(private db: DatabaseService) {}
132
+ }
133
+
134
+ @Injectable()
135
+ class AuthService {
136
+ constructor(private userRepo: UserRepository) {}
137
+ }
138
+
139
+ const AuthModule = ProviderModule.create({
140
+ id: 'AuthModule',
141
+ providers: [ConfigService, DatabaseService, UserRepository, AuthService],
142
+ });
143
+
144
+ // Automatic dependency resolution
145
+ const authService = AuthModule.get(AuthService);
146
+ // All dependencies automatically injected!
147
+ ```
148
+
149
+ ### Problem 2: Tight Coupling and Testing Difficulty
150
+
151
+ **Without xInjection:**
152
+
153
+ ```ts
154
+ class PaymentService {
155
+ // Hardcoded dependency - impossible to mock
156
+ private stripe = new StripeClient('api-key');
157
+
158
+ async charge(amount: number) {
159
+ return this.stripe.charge(amount);
160
+ }
161
+ }
162
+
163
+ // Testing requires hitting the real Stripe API
164
+ // No way to inject a mock without changing production code
165
+ ```
166
+
167
+ **With xInjection:**
168
+
169
+ ```ts
170
+ @Injectable()
171
+ class PaymentService {
172
+ constructor(private paymentGateway: PaymentGateway) {}
173
+
174
+ async charge(amount: number) {
175
+ return this.paymentGateway.charge(amount);
176
+ }
177
+ }
178
+
179
+ // Production: Use real Stripe
180
+ const ProductionModule = ProviderModule.create({
181
+ providers: [{ provide: PaymentGateway, useClass: StripePaymentGateway }, PaymentService],
182
+ });
183
+
184
+ // Testing: Use mock (no production code changes needed!)
185
+ const TestModule = ProviderModule.create({
186
+ providers: [{ provide: PaymentGateway, useClass: MockPaymentGateway }, PaymentService],
187
+ });
188
+ ```
189
+
190
+ ### Problem 3: Lack of Encapsulation
191
+
192
+ **Without xInjection:**
193
+
194
+ ```ts
195
+ // Internal implementation details exposed
196
+ class CacheService {
197
+ // Should be private but other modules need access
198
+ public internalCache = new Map();
199
+ }
200
+
201
+ class DatabaseModule {
202
+ // Everything is public - no control over what gets used
203
+ public connection = createConnection();
204
+ public cache = new CacheService();
205
+ public queryBuilder = new QueryBuilder();
206
+ }
207
+
208
+ // Other modules can access internals they shouldn't
209
+ const cache = databaseModule.internalCache; // Bad!
210
+ ```
211
+
212
+ **With xInjection:**
213
+
214
+ ```ts
215
+ const DatabaseModule = ProviderModule.create({
216
+ id: 'DatabaseModule',
217
+ providers: [ConnectionPool, CacheService, QueryBuilder],
218
+ exports: [QueryBuilder], // Only expose public API
219
+ });
220
+
221
+ // Other modules can only access QueryBuilder
222
+ // ConnectionPool and CacheService remain internal
223
+ const ApiModule = ProviderModule.create({
224
+ imports: [DatabaseModule],
225
+ });
226
+
227
+ // ✅ Works - QueryBuilder is exported
228
+ const queryBuilder = ApiModule.get(QueryBuilder);
229
+
230
+ // ❌ Error - CacheService not exported (properly encapsulated!)
231
+ const cache = ApiModule.get(CacheService);
232
+ ```
233
+
234
+ ### Problem 4: Lifecycle Management Complexity
235
+
236
+ **Without xInjection:**
237
+
238
+ ```ts
239
+ class AppServices {
240
+ database: DatabaseService;
241
+ cache: CacheService;
242
+
243
+ async initialize() {
244
+ this.database = new DatabaseService();
245
+ await this.database.connect();
246
+
247
+ this.cache = new CacheService();
248
+ await this.cache.initialize();
249
+
250
+ // Manually track initialization order and cleanup
251
+ }
252
+
253
+ async cleanup() {
254
+ // Must remember to clean up in reverse order
255
+ await this.cache.dispose();
256
+ await this.database.disconnect();
257
+ }
258
+ }
259
+
260
+ // Easy to forget cleanup, leading to resource leaks
261
+ ```
262
+
263
+ **With xInjection:**
264
+
265
+ ```ts
266
+ const AppModule = ProviderModule.create({
267
+ id: 'AppModule',
268
+ providers: [DatabaseService, CacheService],
269
+ onReady: async (module) => {
270
+ // Initialization logic - called immediately after module creation
271
+ const db = module.get(DatabaseService);
272
+ await db.connect();
273
+ },
274
+ onDispose: () => {
275
+ return {
276
+ before: async (mod) => {
277
+ // Automatic cleanup in proper order
278
+ const db = mod.get(DatabaseService);
279
+ await db.disconnect();
280
+ },
281
+ };
282
+ },
283
+ });
284
+
285
+ // Lifecycle automatically managed
286
+ await AppModule.dispose(); // Everything cleaned up properly
287
+ ```
288
+
289
+ xInjection transforms these pain points into elegant, maintainable code through:
290
+
291
+ - **Automatic dependency resolution** - No manual wiring
292
+ - **Inversion of Control** - Easy testing and flexibility
293
+ - **Encapsulation** - Fine-grained control over module boundaries
294
+ - **Lifecycle hooks** - Proper initialization and cleanup
295
+ - **Modular architecture** - Scalable application structure
296
+
50
297
  ## Overview
51
298
 
52
299
  **xInjection** is a powerful [Dependency Injection](https://en.wikipedia.org/wiki/Dependency_injection) library built on [InversifyJS](https://github.com/inversify/InversifyJS), inspired by [NestJS](https://github.com/nestjs/nest)'s modular architecture. It provides fine-grained control over dependency encapsulation through a module-based system where each module manages its own container with explicit import/export boundaries.
@@ -59,6 +306,7 @@
59
306
  - **Lazy Loading** - Blueprint pattern for deferred module instantiation
60
307
  - **Lifecycle Hooks** - `onReady`, `onReset`, `onDispose` for module lifecycle management
61
308
  - **Events & Middlewares** - Deep customization through event subscriptions and middleware chains
309
+ - **OOP Support** - `ProviderModuleClass` for class-based module architecture
62
310
  - **Framework Agnostic** - Works in Node.js and browser environments
63
311
  - **TypeScript First** - Full type safety with decorator support
64
312
 
@@ -117,7 +365,232 @@ const authService = AuthModule.get(AuthService);
117
365
  console.log(authService.login('123')); // "Logged in as John Doe"
118
366
  ```
119
367
 
120
- ## OOP-Style Modules
368
+ ## Core Concepts
369
+
370
+ ### Services with @Injectable
371
+
372
+ The `@Injectable()` decorator marks a class as available for dependency injection. It enables automatic constructor parameter resolution.
373
+
374
+ ```ts
375
+ import { Injectable, InjectionScope } from '@adimm/x-injection';
376
+
377
+ // Basic injectable service (Singleton by default)
378
+ @Injectable()
379
+ class LoggerService {
380
+ log(message: string) {
381
+ console.log(`[LOG] ${message}`);
382
+ }
383
+ }
384
+
385
+ // Injectable with scope specification
386
+ @Injectable(InjectionScope.Request)
387
+ class RequestContext {
388
+ requestId = Math.random();
389
+ }
390
+
391
+ // Complex service with dependencies
392
+ @Injectable()
393
+ class ApiService {
394
+ constructor(
395
+ private logger: LoggerService,
396
+ private context: RequestContext
397
+ ) {}
398
+
399
+ async fetchData() {
400
+ this.logger.log(`Fetching data for request ${this.context.requestId}`);
401
+ return { data: 'example' };
402
+ }
403
+ }
404
+ ```
405
+
406
+ > [!IMPORTANT]
407
+ > The `@Injectable()` decorator is **required** for any class that:
408
+ >
409
+ > - Has constructor dependencies
410
+ > - Needs to be resolved from a module container
411
+ > - Should be managed by the dependency injection system
412
+
413
+ ### Modules
414
+
415
+ Modules are the fundamental building blocks of xInjection. Each module encapsulates providers with explicit control over imports and exports.
416
+
417
+ ```ts
418
+ import { ProviderModule } from '@adimm/x-injection';
419
+
420
+ // Define services
421
+ @Injectable()
422
+ class DatabaseService {
423
+ query(sql: string) {
424
+ return [{ id: 1, name: 'Result' }];
425
+ }
426
+ }
427
+
428
+ @Injectable()
429
+ class InternalCacheService {
430
+ // Internal-only service
431
+ private cache = new Map();
432
+ }
433
+
434
+ @Injectable()
435
+ class UserRepository {
436
+ constructor(
437
+ private db: DatabaseService,
438
+ private cache: InternalCacheService
439
+ ) {}
440
+
441
+ findById(id: string) {
442
+ return this.db.query(`SELECT * FROM users WHERE id = ${id}`);
443
+ }
444
+ }
445
+
446
+ // Create module with encapsulation
447
+ const DatabaseModule = ProviderModule.create({
448
+ id: 'DatabaseModule',
449
+ providers: [DatabaseService, InternalCacheService, UserRepository],
450
+ exports: [UserRepository], // Only UserRepository accessible to importers
451
+ });
452
+
453
+ // Use the module
454
+ const ApiModule = ProviderModule.create({
455
+ id: 'ApiModule',
456
+ imports: [DatabaseModule],
457
+ });
458
+
459
+ // ✅ Works - UserRepository is exported
460
+ const userRepo = ApiModule.get(UserRepository);
461
+
462
+ // ❌ Throws error - InternalCacheService not exported
463
+ // const cache = ApiModule.get(InternalCacheService);
464
+ ```
465
+
466
+ **Key Module Methods:**
467
+
468
+ ```ts
469
+ const MyModule = ProviderModule.create({ id: 'MyModule' });
470
+
471
+ // Resolution
472
+ MyModule.get(ServiceClass); // Get provider instance
473
+ MyModule.getMany(Service1, Service2); // Get multiple providers
474
+
475
+ // Queries
476
+ MyModule.hasProvider(ServiceClass); // Check if provider exists
477
+ MyModule.isImportingModule('ModuleId'); // Check if importing module
478
+ MyModule.isExportingProvider(ServiceClass); // Check if exporting provider
479
+ MyModule.isExportingModule('ModuleId'); // Check if exporting module
480
+
481
+ // Dynamic updates
482
+ MyModule.update.addProvider(NewService); // Add provider
483
+ MyModule.update.addImport(OtherModule); // Add import
484
+ MyModule.update.removeProvider(ServiceClass); // Remove provider
485
+ MyModule.update.removeImport(OtherModule); // Remove import
486
+ MyModule.update.removeFromExports(Service); // Remove from exports
487
+
488
+ // Lifecycle
489
+ await MyModule.reset(); // Reset module state
490
+ await MyModule.dispose(); // Clean up and dispose
491
+ MyModule.isDisposed; // Check disposal state
492
+ ```
493
+
494
+ ### Blueprints
495
+
496
+ Blueprints allow you to define module configurations without instantiating them, enabling lazy loading and template reuse.
497
+
498
+ ```ts
499
+ import { ProviderModule } from '@adimm/x-injection';
500
+
501
+ @Injectable()
502
+ class ConfigService {
503
+ getConfig() {
504
+ return { apiUrl: 'https://api.example.com' };
505
+ }
506
+ }
507
+
508
+ // Define blueprint (not instantiated yet)
509
+ const ConfigModuleBp = ProviderModule.blueprint({
510
+ id: 'ConfigModule',
511
+ providers: [ConfigService],
512
+ exports: [ConfigService],
513
+ });
514
+
515
+ // Use blueprint in imports (auto-converts to module)
516
+ const ApiModule = ProviderModule.create({
517
+ id: 'ApiModule',
518
+ imports: [ConfigModuleBp], // Instantiated automatically when needed
519
+ });
520
+
521
+ // Or create module from blueprint explicitly
522
+ const ConfigModule = ProviderModule.create(ConfigModuleBp);
523
+
524
+ // Clone blueprints for testing
525
+ const ConfigModuleMock = ConfigModuleBp.clone().updateDefinition({
526
+ id: 'ConfigModuleMock',
527
+ providers: [{ provide: ConfigService, useValue: { getConfig: () => ({ apiUrl: 'mock' }) } }],
528
+ });
529
+ ```
530
+
531
+ **Benefits of Blueprints:**
532
+
533
+ - **Deferred Instantiation** - Only create modules when needed
534
+ - **Reusable Templates** - Define once, use in multiple places
535
+ - **Testing** - Clone and modify for test scenarios
536
+ - **Scoped Singletons** - Each importer gets its own module instance
537
+
538
+ > [!TIP]
539
+ > Use blueprints when you need the same module configuration in multiple places, or when you want to delay module creation until runtime.
540
+
541
+ ### AppModule
542
+
543
+ The `AppModule` is a special global root module that's automatically available throughout your application. Global modules are auto-imported into `AppModule`.
544
+
545
+ ```ts
546
+ import { AppModule, ProviderModule } from '@adimm/x-injection';
547
+
548
+ @Injectable()
549
+ class LoggerService {
550
+ log(message: string) {
551
+ console.log(message);
552
+ }
553
+ }
554
+
555
+ // Add global providers to AppModule
556
+ AppModule.update.addProvider(LoggerService);
557
+
558
+ // Access from any module without explicit import
559
+ const FeatureModule = ProviderModule.create({
560
+ id: 'FeatureModule',
561
+ // No need to import AppModule
562
+ });
563
+
564
+ const logger = FeatureModule.get(LoggerService);
565
+ logger.log('Hello from FeatureModule!');
566
+ ```
567
+
568
+ **Global Module Pattern:**
569
+
570
+ ```ts
571
+ // Create a global module (auto-imports into AppModule)
572
+ const LoggerModule = ProviderModule.create({
573
+ id: 'LoggerModule',
574
+ isGlobal: true,
575
+ providers: [LoggerService],
576
+ exports: [LoggerService],
577
+ });
578
+
579
+ // Now all modules have access to LoggerService
580
+ const AnyModule = ProviderModule.create({
581
+ id: 'AnyModule',
582
+ });
583
+
584
+ const logger = AnyModule.get(LoggerService); // Works!
585
+ ```
586
+
587
+ > [!WARNING]
588
+ >
589
+ > - Cannot create a module with `id: 'AppModule'` - this is reserved
590
+ > - Cannot import `AppModule` into other modules
591
+ > - Use global modules sparingly to avoid implicit dependencies
592
+
593
+ ## OOP-Style Modules with ProviderModuleClass
121
594
 
122
595
  For developers who prefer class-based architecture, xInjection provides `ProviderModuleClass` - a composition-based wrapper that prevents naming conflicts between your custom methods and the DI container methods.
123
596
 
@@ -153,6 +626,7 @@ class AuthModule extends ProviderModuleClass {
153
626
  });
154
627
  }
155
628
 
629
+ // Custom business logic methods
156
630
  authenticateUser(userId: string): string {
157
631
  const authService = this.module.get(AuthService);
158
632
  return authService.login(userId);
@@ -162,29 +636,54 @@ class AuthModule extends ProviderModuleClass {
162
636
  const userService = this.module.get(UserService);
163
637
  return userService.get(userId);
164
638
  }
639
+
640
+ // Custom method named 'get' - no conflict!
641
+ get(): string {
642
+ return 'custom-get-value';
643
+ }
165
644
  }
166
645
 
167
646
  // Instantiate and use
168
647
  const authModule = new AuthModule();
648
+
649
+ // Use custom methods
169
650
  console.log(authModule.authenticateUser('123')); // "Logged in as John Doe"
651
+ console.log(authModule.get()); // "custom-get-value"
170
652
 
171
- // All ProviderModule methods are available through the `.module` property
653
+ // Access DI container through .module property
172
654
  const authService = authModule.module.get(AuthService);
173
655
  authModule.module.update.addProvider(NewService);
174
656
  ```
175
657
 
658
+ > [!IMPORTANT]
659
+ > All `ProviderModule` methods are available through the `.module` property to prevent naming conflicts with your custom methods.
660
+
176
661
  ### Advanced OOP Patterns
177
662
 
178
663
  **Module with Initialization Logic:**
179
664
 
180
665
  ```ts
666
+ @Injectable()
667
+ class DatabaseService {
668
+ private connected = false;
669
+
670
+ async connect(): Promise<void> {
671
+ // Connection logic
672
+ this.connected = true;
673
+ }
674
+
675
+ isConnected(): boolean {
676
+ return this.connected;
677
+ }
678
+ }
679
+
181
680
  class DatabaseModule extends ProviderModuleClass {
182
- private isConnected = false;
681
+ private isModuleConnected = false;
183
682
 
184
683
  constructor() {
185
684
  super({
186
685
  id: 'DatabaseModule',
187
- providers: [DatabaseService, ConnectionPool],
686
+ providers: [DatabaseService],
188
687
  exports: [DatabaseService],
189
688
  onReady: async (module) => {
190
689
  console.log('DatabaseModule ready!');
@@ -195,11 +694,11 @@ class DatabaseModule extends ProviderModuleClass {
195
694
  async connect(): Promise<void> {
196
695
  const dbService = this.module.get(DatabaseService);
197
696
  await dbService.connect();
198
- this.isConnected = true;
697
+ this.isModuleConnected = true;
199
698
  }
200
699
 
201
700
  getConnectionStatus(): boolean {
202
- return this.isConnected;
701
+ return this.isModuleConnected;
203
702
  }
204
703
  }
205
704
 
@@ -211,17 +710,30 @@ console.log(dbModule.getConnectionStatus()); // true
211
710
  **Module with Computed Properties:**
212
711
 
213
712
  ```ts
713
+ @Injectable()
714
+ class ApiService {
715
+ makeRequest() {
716
+ return 'response';
717
+ }
718
+ }
719
+
720
+ @Injectable()
721
+ class HttpClient {
722
+ get(url: string) {
723
+ return `GET ${url}`;
724
+ }
725
+ }
726
+
214
727
  class ApiModule extends ProviderModuleClass {
215
728
  constructor() {
216
729
  super({
217
730
  id: 'ApiModule',
218
- imports: [ConfigModule, LoggerModule],
219
731
  providers: [ApiService, HttpClient],
220
732
  exports: [ApiService],
221
733
  });
222
734
  }
223
735
 
224
- // Computed property - lazy evaluation
736
+ // Computed properties - lazy evaluation
225
737
  get apiService(): ApiService {
226
738
  return this.module.get(ApiService);
227
739
  }
@@ -233,245 +745,431 @@ class ApiModule extends ProviderModuleClass {
233
745
  // Business logic using multiple services
234
746
  async makeAuthenticatedRequest(url: string, token: string) {
235
747
  const client = this.httpClient;
236
- return client.request(url, {
237
- headers: { Authorization: `Bearer ${token}` },
238
- });
748
+ return client.get(url) + ` with token ${token}`;
239
749
  }
240
750
  }
241
751
 
242
752
  const apiModule = new ApiModule();
243
- const response = await apiModule.makeAuthenticatedRequest('/users', 'token');
753
+ const response = await apiModule.makeAuthenticatedRequest('/users', 'token123');
244
754
  ```
245
755
 
246
- **Module Composition:**
756
+ ### When to Use OOP vs Functional
247
757
 
248
- ```ts
249
- class BaseModule extends ProviderModuleClass {
250
- protected logAction(action: string): void {
251
- const logger = this.module.get(LoggerService);
252
- logger.log(`[${String(this.module.id)}] ${action}`);
253
- }
254
- }
758
+ **Use OOP-style (`extends ProviderModuleClass`) when:**
255
759
 
256
- class UserModule extends BaseModule {
257
- constructor() {
258
- super({
259
- id: 'UserModule',
260
- providers: [UserService, UserRepository],
261
- exports: [UserService],
262
- });
263
- }
760
+ - You need custom business logic methods on the module itself
761
+ - You prefer class-based architecture and inheritance patterns
762
+ - You want computed properties or getters for providers
763
+ - You need initialization logic or state management in the module
764
+ - You're building a complex module with multiple related operations
765
+ - You want to prevent naming conflicts (e.g., custom `get()` method)
264
766
 
265
- createUser(name: string) {
266
- this.logAction(`Creating user: ${name}`);
267
- const userService = this.module.get(UserService);
268
- return userService.create(name);
269
- }
270
-
271
- deleteUser(id: string) {
272
- this.logAction(`Deleting user: ${id}`);
273
- const userService = this.module.get(UserService);
274
- return userService.delete(id);
275
- }
276
- }
277
- ```
278
-
279
- ### When to Use OOP vs Functional
280
-
281
- **Use OOP-style (`extends ProviderModuleClass`) when:**
282
-
283
- - You need custom business logic methods on the module itself
284
- - You prefer class-based architecture
285
- - You want computed properties or getters for providers
286
- - You need initialization logic or state management in the module
287
- - You're building a complex module with multiple related operations
288
-
289
- **Use Functional-style (`ProviderModule.create()`) when:**
767
+ **Use Functional-style (`ProviderModule.create()`) when:**
290
768
 
291
769
  - You only need dependency injection without custom logic
292
- - You prefer functional composition
293
- - You want simpler, more concise code
770
+ - You prefer functional composition and simplicity
771
+ - You want more concise code
294
772
  - You're creating straightforward provider containers
773
+ - You don't need module-level state or behavior
295
774
 
296
- **Key Point:** Both styles are fully compatible and can be mixed within the same application. `ProviderModuleClass` uses composition (contains a `ProviderModule` as `this.module`), preventing method name conflicts while providing identical DI functionality.
775
+ **Key Point:** Both styles are fully compatible and can be mixed within the same application. `ProviderModuleClass` uses composition (contains a `ProviderModule` as `this.module`), providing identical DI functionality while preventing method name conflicts.
297
776
 
298
- ## Core Concepts
777
+ ## Provider Tokens
778
+
779
+ xInjection supports four types of provider tokens, each serving different use cases.
299
780
 
300
- ### ProviderModule
781
+ ### 1. Class Token
301
782
 
302
- The fundamental building block of xInjection. Similar to NestJS modules, each `ProviderModule` encapsulates related providers with explicit control over what's exposed.
783
+ The simplest form - just provide the class directly.
303
784
 
304
785
  ```ts
305
- const DatabaseModule = ProviderModule.create({
306
- id: 'DatabaseModule',
307
- imports: [ConfigModule], // Modules to import
308
- providers: [DatabaseService], // Services to register
309
- exports: [DatabaseService], // What to expose to importers
786
+ @Injectable()
787
+ class UserService {
788
+ getUsers() {
789
+ return [{ id: '1', name: 'Alice' }];
790
+ }
791
+ }
792
+
793
+ const MyModule = ProviderModule.create({
794
+ id: 'MyModule',
795
+ providers: [UserService], // Class token
310
796
  });
797
+
798
+ const userService = MyModule.get(UserService);
311
799
  ```
312
800
 
313
- **Key Methods:**
801
+ ### 2. Class Token with Substitution
314
802
 
315
- - `Module.get(token)` - Resolve a provider instance
316
- - `Module.update.addProvider()` - Dynamically add providers
317
- - `Module.update.addImport()` - Import other modules at runtime
318
- - `Module.dispose()` - Clean up module resources
803
+ Use one class as the token but instantiate a different class. Perfect for polymorphism and testing.
319
804
 
320
- [Full API Documentation →](https://adimarianmutu.github.io/x-injection/classes/IProviderModule.html)
805
+ ```ts
806
+ @Injectable()
807
+ abstract class PaymentGateway {
808
+ abstract charge(amount: number): Promise<void>;
809
+ }
321
810
 
322
- ### AppModule
811
+ @Injectable()
812
+ class StripePaymentGateway extends PaymentGateway {
813
+ async charge(amount: number) {
814
+ console.log(`Charging $${amount} via Stripe`);
815
+ }
816
+ }
323
817
 
324
- The global root module, automatically available in every application. Global modules are auto-imported into `AppModule`.
818
+ @Injectable()
819
+ class MockPaymentGateway extends PaymentGateway {
820
+ async charge(amount: number) {
821
+ console.log(`Mock charge: $${amount}`);
822
+ }
823
+ }
325
824
 
326
- ```ts
327
- import { AppModule } from '@adimm/x-injection';
825
+ // Production
826
+ const ProductionModule = ProviderModule.create({
827
+ id: 'ProductionModule',
828
+ providers: [{ provide: PaymentGateway, useClass: StripePaymentGateway }],
829
+ });
328
830
 
329
- // Add global providers
330
- AppModule.update.addProvider(LoggerService);
831
+ // Testing
832
+ const TestModule = ProviderModule.create({
833
+ id: 'TestModule',
834
+ providers: [{ provide: PaymentGateway, useClass: MockPaymentGateway }],
835
+ });
331
836
 
332
- // Access from any module
333
- const anyModule = ProviderModule.create({ id: 'AnyModule' });
334
- const logger = anyModule.get(LoggerService);
837
+ const prodGateway = ProductionModule.get(PaymentGateway); // StripePaymentGateway
838
+ const testGateway = TestModule.get(PaymentGateway); // MockPaymentGateway
335
839
  ```
336
840
 
337
- ### Blueprints
841
+ ### 3. Value Token
338
842
 
339
- Blueprints allow you to define module configurations without instantiating them, enabling lazy loading and template reuse.
843
+ Provide constant values or pre-instantiated objects.
340
844
 
341
845
  ```ts
342
- // Define blueprint
343
- const DatabaseModuleBp = ProviderModule.blueprint({
344
- id: 'DatabaseModule',
345
- providers: [DatabaseService],
346
- exports: [DatabaseService],
846
+ // Configuration values
847
+ const ConfigModule = ProviderModule.create({
848
+ id: 'ConfigModule',
849
+ providers: [
850
+ { provide: 'API_KEY', useValue: 'secret-key-123' },
851
+ { provide: 'API_URL', useValue: 'https://api.example.com' },
852
+ { provide: 'MAX_RETRIES', useValue: 3 },
853
+ ],
854
+ exports: ['API_KEY', 'API_URL', 'MAX_RETRIES'],
347
855
  });
348
856
 
349
- // Import blueprint (auto-converts to module)
350
- const AppModule = ProviderModule.create({
351
- id: 'AppModule',
352
- imports: [DatabaseModuleBp],
353
- });
857
+ const apiKey = ConfigModule.get('API_KEY'); // 'secret-key-123'
858
+ const apiUrl = ConfigModule.get('API_URL'); // 'https://api.example.com'
859
+ const maxRetries = ConfigModule.get('MAX_RETRIES'); // 3
354
860
 
355
- // Or create module from blueprint later
356
- const DatabaseModule = ProviderModule.create(DatabaseModuleBp);
861
+ // Pre-instantiated objects
862
+ const existingLogger = new Logger();
863
+ const LoggerModule = ProviderModule.create({
864
+ id: 'LoggerModule',
865
+ providers: [{ provide: Logger, useValue: existingLogger }],
866
+ });
357
867
  ```
358
868
 
359
- **Benefits:**
360
-
361
- - Deferred instantiation for better startup performance
362
- - Reusable module templates across your application
363
- - Scoped singletons per importing module
869
+ ### 4. Factory Token
364
870
 
365
- ### Provider Tokens
366
-
367
- xInjection supports four types of provider tokens:
368
-
369
- **1. Class Token** (simplest):
871
+ Use a factory function to create providers dynamically. The `inject` parameter specifies dependencies.
370
872
 
371
873
  ```ts
372
874
  @Injectable()
373
- class ApiService {}
875
+ class ConfigService {
876
+ dbUrl = 'postgres://localhost:5432/mydb';
877
+ dbPort = 5432;
878
+ }
374
879
 
375
- providers: [ApiService];
376
- ```
880
+ interface DatabaseConnection {
881
+ url: string;
882
+ port: number;
883
+ connected: boolean;
884
+ }
377
885
 
378
- **2. Class Token with Substitution**:
886
+ const DatabaseModule = ProviderModule.create({
887
+ id: 'DatabaseModule',
888
+ providers: [
889
+ ConfigService,
890
+ {
891
+ provide: 'DATABASE_CONNECTION',
892
+ useFactory: (config: ConfigService) => {
893
+ // Factory receives injected dependencies
894
+ return {
895
+ url: config.dbUrl,
896
+ port: config.dbPort,
897
+ connected: true,
898
+ };
899
+ },
900
+ inject: [ConfigService], // Dependencies to inject into factory
901
+ },
902
+ ],
903
+ exports: ['DATABASE_CONNECTION'],
904
+ });
379
905
 
380
- ```ts
381
- providers: [{ provide: ApiService, useClass: MockApiService }];
906
+ const connection = DatabaseModule.get<DatabaseConnection>('DATABASE_CONNECTION');
907
+ console.log(connection.url); // 'postgres://localhost:5432/mydb'
382
908
  ```
383
909
 
384
- **3. Value Token** (constants):
910
+ **Complex Factory Example with Multiple Dependencies:**
385
911
 
386
912
  ```ts
387
- providers: [{ provide: 'API_KEY', useValue: 'secret-key-123' }];
388
- ```
913
+ @Injectable()
914
+ class LoggerService {
915
+ log(message: string) {
916
+ console.log(message);
917
+ }
918
+ }
389
919
 
390
- **4. Factory Token** (dynamic):
920
+ @Injectable()
921
+ class MetricsService {
922
+ track(event: string) {
923
+ console.log(`Tracking: ${event}`);
924
+ }
925
+ }
391
926
 
392
- ```ts
393
- providers: [
394
- {
395
- provide: 'DATABASE_CONNECTION',
396
- useFactory: (config: ConfigService) => createConnection(config.dbUrl),
397
- inject: [ConfigService],
398
- },
399
- ];
927
+ interface ApiClient {
928
+ logger: LoggerService;
929
+ metrics: MetricsService;
930
+ baseUrl: string;
931
+ makeRequest(endpoint: string): void;
932
+ }
933
+
934
+ const ApiModule = ProviderModule.create({
935
+ id: 'ApiModule',
936
+ providers: [
937
+ LoggerService,
938
+ MetricsService,
939
+ { provide: 'BASE_URL', useValue: 'https://api.example.com' },
940
+ {
941
+ provide: 'API_CLIENT',
942
+ useFactory: (logger: LoggerService, metrics: MetricsService, baseUrl: string): ApiClient => {
943
+ return {
944
+ logger,
945
+ metrics,
946
+ baseUrl,
947
+ makeRequest(endpoint: string) {
948
+ this.logger.log(`Making request to ${this.baseUrl}${endpoint}`);
949
+ this.metrics.track('api_request');
950
+ },
951
+ };
952
+ },
953
+ inject: [LoggerService, MetricsService, 'BASE_URL'],
954
+ },
955
+ ],
956
+ });
957
+
958
+ const apiClient = ApiModule.get<ApiClient>('API_CLIENT');
959
+ apiClient.makeRequest('/users');
400
960
  ```
401
961
 
962
+ > [!TIP]
963
+ > Use factory tokens when:
964
+ >
965
+ > - Provider creation requires complex logic
966
+ > - You need to inject dependencies into the factory
967
+ > - You're creating providers that depend on runtime configuration
968
+ > - You need to create multiple instances with different configurations
969
+
402
970
  ## Injection Scopes
403
971
 
404
- Control provider lifecycle with three scope types (priority order: token > decorator > module default):
972
+ Control provider lifecycle with three scope types. Scope priority order: **token scope > decorator scope > module default scope**.
405
973
 
406
974
  ### Singleton (Default)
407
975
 
408
- Cached after first resolution - same instance every time:
976
+ Cached after first resolution - same instance returned every time.
409
977
 
410
978
  ```ts
411
979
  @Injectable() // Singleton by default
412
- class DatabaseService {}
980
+ class DatabaseService {
981
+ connectionId = Math.random();
982
+ }
983
+
984
+ const MyModule = ProviderModule.create({
985
+ id: 'MyModule',
986
+ providers: [DatabaseService],
987
+ });
988
+
989
+ const db1 = MyModule.get(DatabaseService);
990
+ const db2 = MyModule.get(DatabaseService);
413
991
 
414
- Module.get(DatabaseService) === Module.get(DatabaseService); // true
992
+ console.log(db1 === db2); // true
993
+ console.log(db1.connectionId === db2.connectionId); // true
415
994
  ```
416
995
 
417
996
  ### Transient
418
997
 
419
- New instance on every resolution:
998
+ New instance created on every resolution.
420
999
 
421
1000
  ```ts
422
1001
  @Injectable(InjectionScope.Transient)
423
- class RequestLogger {}
1002
+ class RequestLogger {
1003
+ requestId = Math.random();
1004
+ }
1005
+
1006
+ const MyModule = ProviderModule.create({
1007
+ id: 'MyModule',
1008
+ providers: [RequestLogger],
1009
+ });
424
1010
 
425
- Module.get(RequestLogger) === Module.get(RequestLogger); // false
1011
+ const logger1 = MyModule.get(RequestLogger);
1012
+ const logger2 = MyModule.get(RequestLogger);
1013
+
1014
+ console.log(logger1 === logger2); // false
1015
+ console.log(logger1.requestId === logger2.requestId); // false
426
1016
  ```
427
1017
 
428
1018
  ### Request
429
1019
 
430
- Single instance per resolution tree (useful for request-scoped data):
1020
+ Single instance per resolution tree. All dependencies resolved in the same `get()` call share the same instance.
431
1021
 
432
1022
  ```ts
433
1023
  @Injectable(InjectionScope.Request)
434
- class RequestContext {}
1024
+ class RequestContext {
1025
+ requestId = Math.random();
1026
+ }
1027
+
1028
+ @Injectable(InjectionScope.Transient)
1029
+ class ServiceA {
1030
+ constructor(public ctx: RequestContext) {}
1031
+ }
1032
+
1033
+ @Injectable(InjectionScope.Transient)
1034
+ class ServiceB {
1035
+ constructor(public ctx: RequestContext) {}
1036
+ }
435
1037
 
436
1038
  @Injectable(InjectionScope.Transient)
437
1039
  class Controller {
438
1040
  constructor(
439
- public ctx1: RequestContext,
440
- public ctx2: RequestContext
1041
+ public serviceA: ServiceA,
1042
+ public serviceB: ServiceB
441
1043
  ) {}
442
1044
  }
443
1045
 
444
- const controller = Module.get(Controller);
445
- controller.ctx1 === controller.ctx2; // true (same resolution)
1046
+ const MyModule = ProviderModule.create({
1047
+ id: 'MyModule',
1048
+ providers: [RequestContext, ServiceA, ServiceB, Controller],
1049
+ });
1050
+
1051
+ // First resolution tree
1052
+ const controller1 = MyModule.get(Controller);
1053
+ console.log(controller1.serviceA.ctx === controller1.serviceB.ctx); // true
1054
+ // ServiceA and ServiceB share the same RequestContext
1055
+
1056
+ // Second resolution tree
1057
+ const controller2 = MyModule.get(Controller);
1058
+ console.log(controller2.serviceA.ctx === controller2.serviceB.ctx); // true
1059
+ // New resolution, both services get a new shared RequestContext
1060
+
1061
+ // Different resolution trees get different contexts
1062
+ console.log(controller1.serviceA.ctx === controller2.serviceA.ctx); // false
1063
+ ```
1064
+
1065
+ **Visual Representation:**
1066
+
1067
+ ```
1068
+ First module.get(Controller):
1069
+ Controller (new) ──┬──> ServiceA (new) ──┐
1070
+ │ ├──> RequestContext (SAME instance)
1071
+ └──> ServiceB (new) ──┘
1072
+
1073
+ Second module.get(Controller):
1074
+ Controller (new) ──┬──> ServiceA (new) ──┐
1075
+ │ ├──> RequestContext (NEW instance)
1076
+ └──> ServiceB (new) ──┘
1077
+ ```
1078
+
1079
+ ### Scope Priority Order
1080
+
1081
+ Scopes are resolved in the following priority order (highest to lowest):
1082
+
1083
+ 1. **Token scope** (highest priority)
1084
+ 2. **Decorator scope**
1085
+ 3. **Module default scope** (lowest priority)
1086
+
1087
+ ```ts
1088
+ @Injectable(InjectionScope.Singleton) // Priority 2
1089
+ class MyService {}
1090
+
1091
+ const MyModule = ProviderModule.create({
1092
+ id: 'MyModule',
1093
+ defaultScope: InjectionScope.Singleton, // Priority 3 (lowest)
1094
+ providers: [
1095
+ {
1096
+ provide: MyService,
1097
+ useClass: MyService,
1098
+ scope: InjectionScope.Transient, // Priority 1 (highest) - WINS!
1099
+ },
1100
+ ],
1101
+ });
446
1102
 
447
- const controller2 = Module.get(Controller);
448
- controller.ctx1 === controller2.ctx1; // false (different resolution)
1103
+ // Token scope wins: new instance every time
1104
+ const s1 = MyModule.get(MyService);
1105
+ const s2 = MyModule.get(MyService);
1106
+ console.log(s1 === s2); // false
449
1107
  ```
450
1108
 
451
- **Setting Scopes:**
1109
+ **Examples of Each Priority:**
452
1110
 
453
1111
  ```ts
454
- // 1. In provider token (highest priority)
455
- providers: [{ provide: Service, useClass: Service, scope: InjectionScope.Transient }];
1112
+ // Priority 1: Token scope
1113
+ const Module1 = ProviderModule.create({
1114
+ id: 'Module1',
1115
+ defaultScope: InjectionScope.Singleton,
1116
+ providers: [
1117
+ {
1118
+ provide: MyService,
1119
+ useClass: MyService,
1120
+ scope: InjectionScope.Transient, // Token wins
1121
+ },
1122
+ ],
1123
+ });
456
1124
 
457
- // 2. In @Injectable decorator
1125
+ // Priority 2: Decorator scope (no token scope)
458
1126
  @Injectable(InjectionScope.Request)
459
- class Service {}
1127
+ class DecoratedService {}
460
1128
 
461
- // 3. Module default (lowest priority)
462
- ProviderModule.create({
463
- id: 'MyModule',
464
- defaultScope: InjectionScope.Transient,
1129
+ const Module2 = ProviderModule.create({
1130
+ id: 'Module2',
1131
+ defaultScope: InjectionScope.Singleton,
1132
+ providers: [DecoratedService], // Decorator wins
1133
+ });
1134
+
1135
+ // Priority 3: Module default (no token or decorator scope)
1136
+ @Injectable() // No scope specified
1137
+ class PlainService {}
1138
+
1139
+ const Module3 = ProviderModule.create({
1140
+ id: 'Module3',
1141
+ defaultScope: InjectionScope.Transient, // Module default wins
1142
+ providers: [PlainService],
465
1143
  });
466
1144
  ```
467
1145
 
1146
+ > [!IMPORTANT]
1147
+ > Request scope is useful for scenarios like:
1148
+ >
1149
+ > - HTTP request tracking (same request ID across all services in one request)
1150
+ > - Transaction contexts (same database transaction across all repositories)
1151
+ > - User context (same user data across all services in one operation)
1152
+
468
1153
  ## Module System
469
1154
 
470
1155
  ### Import/Export Pattern
471
1156
 
472
- Modules explicitly control dependency boundaries through imports and exports:
1157
+ Modules explicitly control dependency boundaries through imports and exports, providing encapsulation and clear interfaces.
473
1158
 
474
1159
  ```ts
1160
+ @Injectable()
1161
+ class DatabaseService {
1162
+ query(sql: string) {
1163
+ return [{ result: 'data' }];
1164
+ }
1165
+ }
1166
+
1167
+ @Injectable()
1168
+ class InternalCacheService {
1169
+ // Private to DatabaseModule
1170
+ cache = new Map();
1171
+ }
1172
+
475
1173
  const DatabaseModule = ProviderModule.create({
476
1174
  id: 'DatabaseModule',
477
1175
  providers: [DatabaseService, InternalCacheService],
@@ -480,186 +1178,1633 @@ const DatabaseModule = ProviderModule.create({
480
1178
 
481
1179
  const ApiModule = ProviderModule.create({
482
1180
  id: 'ApiModule',
483
- imports: [DatabaseModule], // Gets access to DatabaseService
1181
+ imports: [DatabaseModule],
484
1182
  providers: [ApiService],
485
1183
  });
486
1184
 
487
- // ✅ Works
1185
+ // ✅ Works - DatabaseService is exported
488
1186
  const dbService = ApiModule.get(DatabaseService);
489
1187
 
490
1188
  // ❌ Error - InternalCacheService not exported
491
- const cache = ApiModule.get(InternalCacheService);
1189
+ // const cache = ApiModule.get(InternalCacheService);
1190
+ ```
1191
+
1192
+ **Nested Imports:**
1193
+
1194
+ ```ts
1195
+ const LayerA = ProviderModule.create({
1196
+ id: 'LayerA',
1197
+ providers: [ServiceA],
1198
+ exports: [ServiceA],
1199
+ });
1200
+
1201
+ const LayerB = ProviderModule.create({
1202
+ id: 'LayerB',
1203
+ imports: [LayerA],
1204
+ providers: [ServiceB],
1205
+ exports: [ServiceB, LayerA], // Re-export LayerA
1206
+ });
1207
+
1208
+ const LayerC = ProviderModule.create({
1209
+ id: 'LayerC',
1210
+ imports: [LayerB],
1211
+ });
1212
+
1213
+ // ✅ Works - ServiceA accessible through LayerB's re-export
1214
+ const serviceA = LayerC.get(ServiceA);
1215
+
1216
+ // ✅ Works - ServiceB exported by LayerB
1217
+ const serviceB = LayerC.get(ServiceB);
492
1218
  ```
493
1219
 
494
1220
  ### Re-exporting Modules
495
1221
 
496
- Modules can re-export imported modules to create aggregation modules:
1222
+ Modules can re-export imported modules to create aggregation modules.
497
1223
 
498
1224
  ```ts
1225
+ const DatabaseModule = ProviderModule.create({
1226
+ id: 'DatabaseModule',
1227
+ providers: [DatabaseService],
1228
+ exports: [DatabaseService],
1229
+ });
1230
+
1231
+ const ConfigModule = ProviderModule.create({
1232
+ id: 'ConfigModule',
1233
+ providers: [ConfigService],
1234
+ exports: [ConfigService],
1235
+ });
1236
+
1237
+ const LoggerModule = ProviderModule.create({
1238
+ id: 'LoggerModule',
1239
+ providers: [LoggerService],
1240
+ exports: [LoggerService],
1241
+ });
1242
+
1243
+ // CoreModule aggregates common modules
499
1244
  const CoreModule = ProviderModule.create({
500
1245
  id: 'CoreModule',
501
- imports: [DatabaseModule, ConfigModule],
502
- exports: [DatabaseModule, ConfigModule], // Re-export both
1246
+ imports: [DatabaseModule, ConfigModule, LoggerModule],
1247
+ exports: [DatabaseModule, ConfigModule, LoggerModule], // Re-export all
503
1248
  });
504
1249
 
505
- // Consumers get both DatabaseModule and ConfigModule
506
- const AppModule = ProviderModule.create({
507
- imports: [CoreModule],
1250
+ // Consumers import CoreModule and get all three modules
1251
+ const FeatureModule = ProviderModule.create({
1252
+ id: 'FeatureModule',
1253
+ imports: [CoreModule], // Just import one module
508
1254
  });
1255
+
1256
+ // Access all re-exported providers
1257
+ const db = FeatureModule.get(DatabaseService);
1258
+ const config = FeatureModule.get(ConfigService);
1259
+ const logger = FeatureModule.get(LoggerService);
509
1260
  ```
510
1261
 
1262
+ > [!TIP]
1263
+ > Create "barrel" or "core" modules that re-export commonly used modules to simplify imports throughout your application.
1264
+
511
1265
  ### Dynamic Module Updates
512
1266
 
513
- Modules support runtime modifications (use sparingly for performance):
1267
+ Modules support runtime modifications for flexibility. Use sparingly as it can impact performance.
514
1268
 
515
1269
  ```ts
516
- const module = ProviderModule.create({ id: 'DynamicModule' });
1270
+ const DynamicModule = ProviderModule.create({
1271
+ id: 'DynamicModule',
1272
+ providers: [ServiceA],
1273
+ });
517
1274
 
518
1275
  // Add providers dynamically
519
- module.update.addProvider(NewService);
520
- module.update.addProvider(AnotherService, true); // true = also export
1276
+ DynamicModule.update.addProvider(ServiceB);
1277
+ DynamicModule.update.addProvider(ServiceC, true); // true = also export
521
1278
 
522
1279
  // Add imports dynamically
523
- module.update.addImport(DatabaseModule, true); // true = also export
1280
+ const DatabaseModule = ProviderModule.create({
1281
+ id: 'DatabaseModule',
1282
+ providers: [DatabaseService],
1283
+ exports: [DatabaseService],
1284
+ });
1285
+
1286
+ DynamicModule.update.addImport(DatabaseModule, true); // true = also export
1287
+
1288
+ // Check what's available
1289
+ console.log(DynamicModule.hasProvider(ServiceB)); // true
1290
+ console.log(DynamicModule.isImportingModule('DatabaseModule')); // true
1291
+ console.log(DynamicModule.isExportingProvider(ServiceC)); // true
1292
+
1293
+ // Remove providers and imports
1294
+ DynamicModule.update.removeProvider(ServiceB);
1295
+ DynamicModule.update.removeImport(DatabaseModule);
1296
+ DynamicModule.update.removeFromExports(ServiceC);
1297
+ ```
1298
+
1299
+ **Dynamic Import Propagation:**
1300
+
1301
+ ```ts
1302
+ const ModuleA = ProviderModule.create({
1303
+ id: 'ModuleA',
1304
+ providers: [ServiceA],
1305
+ exports: [ServiceA],
1306
+ });
1307
+
1308
+ const ModuleB = ProviderModule.create({
1309
+ id: 'ModuleB',
1310
+ imports: [ModuleA],
1311
+ exports: [ModuleA],
1312
+ });
1313
+
1314
+ const ModuleC = ProviderModule.create({
1315
+ id: 'ModuleC',
1316
+ providers: [ServiceC],
1317
+ exports: [ServiceC],
1318
+ });
1319
+
1320
+ // Initially, ModuleB doesn't have ServiceC
1321
+ console.log(ModuleB.hasProvider(ServiceC)); // false
1322
+
1323
+ // Dynamically import ModuleC into ModuleA and export it
1324
+ ModuleA.update.addImport(ModuleC, true);
1325
+
1326
+ // Now ModuleB automatically has ServiceC (import propagation!)
1327
+ console.log(ModuleB.hasProvider(ServiceC)); // true
524
1328
  ```
525
1329
 
526
- **Important:** Dynamic imports propagate automatically - if `ModuleA` imports `ModuleB`, and `ModuleB` dynamically imports `ModuleC` (with export), `ModuleA` automatically gets access to `ModuleC`'s exports.
1330
+ > [!WARNING]
1331
+ > Dynamic module updates:
1332
+ >
1333
+ > - Can impact performance if used frequently
1334
+ > - Should be used primarily for testing or plugin systems
1335
+ > - May make dependency graphs harder to understand
1336
+ > - Are propagated automatically to importing modules
527
1337
 
528
1338
  ### Global Modules
529
1339
 
530
- Mark modules as global to auto-import into `AppModule`:
1340
+ Mark modules as global to auto-import into `AppModule`, making them available everywhere.
531
1341
 
532
1342
  ```ts
1343
+ @Injectable()
1344
+ class LoggerService {
1345
+ log(message: string) {
1346
+ console.log(`[LOG] ${message}`);
1347
+ }
1348
+ }
1349
+
1350
+ // Create global module
533
1351
  const LoggerModule = ProviderModule.create({
534
1352
  id: 'LoggerModule',
535
- isGlobal: true,
1353
+ isGlobal: true, // Auto-imports into AppModule
536
1354
  providers: [LoggerService],
537
1355
  exports: [LoggerService],
538
1356
  });
539
1357
 
540
- // LoggerService now available in all modules without explicit import
541
- ```
542
-
543
- ## Advanced Features
544
-
545
- > [!WARNING]
546
- > These features provide deep customization but can add complexity. Use them only when necessary.
1358
+ // Now any module can access LoggerService without explicit import
1359
+ const FeatureModule = ProviderModule.create({
1360
+ id: 'FeatureModule',
1361
+ // No imports needed!
1362
+ });
547
1363
 
548
- ### Events
1364
+ const logger = FeatureModule.get(LoggerService); // Works!
1365
+ logger.log('Hello from FeatureModule');
1366
+ ```
549
1367
 
550
- Subscribe to module lifecycle events for monitoring and debugging:
1368
+ **Global Module with Blueprint:**
551
1369
 
552
1370
  ```ts
553
- import { DefinitionEventType } from '@adimm/x-injection';
1371
+ @Injectable()
1372
+ class ConfigService {
1373
+ apiUrl = 'https://api.example.com';
1374
+ }
554
1375
 
555
- const module = ProviderModule.create({
556
- id: 'MyModule',
557
- providers: [MyService],
1376
+ // Blueprint with global flag
1377
+ const ConfigModuleBp = ProviderModule.blueprint({
1378
+ id: 'ConfigModule',
1379
+ isGlobal: true,
1380
+ providers: [ConfigService],
1381
+ exports: [ConfigService],
558
1382
  });
559
1383
 
560
- const unsubscribe = module.update.subscribe(({ type, change }) => {
561
- if (type === DefinitionEventType.GetProvider) {
562
- console.log('Provider resolved:', change);
563
- }
564
- if (type === DefinitionEventType.Import) {
565
- console.log('Module imported:', change);
566
- }
567
- });
1384
+ // Automatically imports into AppModule
1385
+ console.log(AppModule.isImportingModule('ConfigModule')); // true
1386
+ console.log(AppModule.hasProvider(ConfigService)); // true
568
1387
 
569
- // Clean up when done
570
- unsubscribe();
1388
+ // Available in all modules
1389
+ const AnyModule = ProviderModule.create({ id: 'AnyModule' });
1390
+ const config = AnyModule.get(ConfigService); // Works!
571
1391
  ```
572
1392
 
573
- **Available Events:** `GetProvider`, `Import`, `Export`, `AddProvider`, `RemoveProvider`, `ExportModule` - [Full list →](https://adimarianmutu.github.io/x-injection/enums/DefinitionEventType.html)
1393
+ > [!CAUTION]
1394
+ > Use global modules sparingly:
1395
+ >
1396
+ > - They create implicit dependencies that can make code harder to understand
1397
+ > - They reduce encapsulation and explicit dependency graphs
1398
+ > - Best used for true cross-cutting concerns (logging, configuration, telemetry)
1399
+ > - Prefer explicit imports when possible for better maintainability
574
1400
 
575
- > [!WARNING]
576
- > Always unsubscribe to prevent memory leaks. Events fire **after** middlewares.
1401
+ ## Dependency Injection
577
1402
 
578
- ### Middlewares
1403
+ ### Constructor Injection
579
1404
 
580
- Intercept and transform provider resolution before values are returned:
1405
+ The primary way to inject dependencies. TypeScript metadata handles it automatically with `@Injectable()`.
581
1406
 
582
1407
  ```ts
583
- import { MiddlewareType } from '@adimm/x-injection';
1408
+ @Injectable()
1409
+ class DatabaseService {
1410
+ query(sql: string) {
1411
+ return [{ data: 'result' }];
1412
+ }
1413
+ }
584
1414
 
585
- const module = ProviderModule.create({
586
- id: 'MyModule',
587
- providers: [PaymentService],
588
- });
1415
+ @Injectable()
1416
+ class LoggerService {
1417
+ log(message: string) {
1418
+ console.log(message);
1419
+ }
1420
+ }
589
1421
 
590
- // Transform resolved values
591
- module.middlewares.add(MiddlewareType.BeforeGet, (provider, token, inject) => {
592
- // Pass through if not interested
593
- if (!(provider instanceof PaymentService)) return true;
1422
+ @Injectable()
1423
+ class UserRepository {
1424
+ // Dependencies automatically injected via constructor
1425
+ constructor(
1426
+ private db: DatabaseService,
1427
+ private logger: LoggerService
1428
+ ) {}
594
1429
 
595
- // Use inject() to avoid infinite loops
596
- const logger = inject(LoggerService);
597
- logger.log('Payment service accessed');
1430
+ findAll() {
1431
+ this.logger.log('Finding all users');
1432
+ return this.db.query('SELECT * FROM users');
1433
+ }
1434
+ }
598
1435
 
599
- // Transform the value
600
- return {
601
- timestamp: Date.now(),
602
- value: provider,
603
- };
1436
+ const UserModule = ProviderModule.create({
1437
+ id: 'UserModule',
1438
+ providers: [DatabaseService, LoggerService, UserRepository],
604
1439
  });
605
1440
 
606
- const payment = module.get(PaymentService);
607
- // { timestamp: 1234567890, value: PaymentService }
1441
+ // UserRepository automatically receives DatabaseService and LoggerService
1442
+ const userRepo = UserModule.get(UserRepository);
608
1443
  ```
609
1444
 
610
- **Control export access:**
1445
+ ### @Inject Decorator
1446
+
1447
+ Use `@Inject` for explicit injection when automatic resolution doesn't work (e.g., string tokens, interfaces).
611
1448
 
612
1449
  ```ts
613
- module.middlewares.add(MiddlewareType.OnExportAccess, (importerModule, exportToken) => {
614
- // Restrict access based on importer
615
- if (importerModule.id === 'UntrustedModule' && exportToken === SensitiveService) {
616
- return false; // Deny access
617
- }
618
- return true; // Allow
619
- });
620
- ```
1450
+ import { Inject, Injectable } from '@adimm/x-injection';
621
1451
 
622
- **Available Middlewares:** `BeforeGet`, `BeforeAddProvider`, `BeforeAddImport`, `OnExportAccess` - [Full list →](https://adimarianmutu.github.io/x-injection/enums/MiddlewareType.html)
1452
+ @Injectable()
1453
+ class ApiService {
1454
+ constructor(
1455
+ @Inject('API_KEY') private apiKey: string,
1456
+ @Inject('API_URL') private apiUrl: string,
1457
+ @Inject('MAX_RETRIES') private maxRetries: number
1458
+ ) {}
1459
+
1460
+ makeRequest() {
1461
+ console.log(`Calling ${this.apiUrl} with key ${this.apiKey}`);
1462
+ console.log(`Max retries: ${this.maxRetries}`);
1463
+ }
1464
+ }
1465
+
1466
+ const ApiModule = ProviderModule.create({
1467
+ id: 'ApiModule',
1468
+ providers: [
1469
+ { provide: 'API_KEY', useValue: 'secret-123' },
1470
+ { provide: 'API_URL', useValue: 'https://api.example.com' },
1471
+ { provide: 'MAX_RETRIES', useValue: 3 },
1472
+ ApiService,
1473
+ ],
1474
+ });
1475
+
1476
+ const apiService = ApiModule.get(ApiService);
1477
+ apiService.makeRequest();
1478
+ ```
1479
+
1480
+ **Injecting Abstract Classes:**
1481
+
1482
+ ```ts
1483
+ @Injectable()
1484
+ abstract class PaymentGateway {
1485
+ abstract charge(amount: number): Promise<void>;
1486
+ }
1487
+
1488
+ @Injectable()
1489
+ class StripePaymentGateway extends PaymentGateway {
1490
+ async charge(amount: number) {
1491
+ console.log(`Stripe: Charging $${amount}`);
1492
+ }
1493
+ }
1494
+
1495
+ @Injectable()
1496
+ class PaymentService {
1497
+ constructor(@Inject(PaymentGateway) private gateway: PaymentGateway) {}
1498
+
1499
+ async processPayment(amount: number) {
1500
+ await this.gateway.charge(amount);
1501
+ }
1502
+ }
1503
+
1504
+ const PaymentModule = ProviderModule.create({
1505
+ id: 'PaymentModule',
1506
+ providers: [{ provide: PaymentGateway, useClass: StripePaymentGateway }, PaymentService],
1507
+ });
1508
+ ```
1509
+
1510
+ ### @MultiInject Decorator
1511
+
1512
+ Inject multiple providers bound to the same token as an array.
1513
+
1514
+ ```ts
1515
+ import { Injectable, MultiInject } from '@adimm/x-injection';
1516
+
1517
+ @Injectable()
1518
+ class EmailNotifier {
1519
+ notify() {
1520
+ console.log('Email notification sent');
1521
+ }
1522
+ }
1523
+
1524
+ @Injectable()
1525
+ class SmsNotifier {
1526
+ notify() {
1527
+ console.log('SMS notification sent');
1528
+ }
1529
+ }
1530
+
1531
+ @Injectable()
1532
+ class PushNotifier {
1533
+ notify() {
1534
+ console.log('Push notification sent');
1535
+ }
1536
+ }
1537
+
1538
+ abstract class Notifier {
1539
+ abstract notify(): void;
1540
+ }
1541
+
1542
+ @Injectable()
1543
+ class NotificationService {
1544
+ constructor(@MultiInject(Notifier) private notifiers: Notifier[]) {}
1545
+
1546
+ notifyAll() {
1547
+ this.notifiers.forEach((notifier) => notifier.notify());
1548
+ }
1549
+ }
1550
+
1551
+ const NotificationModule = ProviderModule.create({
1552
+ id: 'NotificationModule',
1553
+ providers: [
1554
+ { provide: Notifier, useClass: EmailNotifier },
1555
+ { provide: Notifier, useClass: SmsNotifier },
1556
+ { provide: Notifier, useClass: PushNotifier },
1557
+ NotificationService,
1558
+ ],
1559
+ });
1560
+
1561
+ const service = NotificationModule.get(NotificationService);
1562
+ service.notifyAll();
1563
+ // Output:
1564
+ // Email notification sent
1565
+ // SMS notification sent
1566
+ // Push notification sent
1567
+ ```
1568
+
1569
+ **Alternative with module.get():**
1570
+
1571
+ ```ts
1572
+ const MyModule = ProviderModule.create({
1573
+ id: 'MyModule',
1574
+ providers: [
1575
+ { provide: 'Handler', useValue: 'Handler1' },
1576
+ { provide: 'Handler', useValue: 'Handler2' },
1577
+ { provide: 'Handler', useValue: 'Handler3' },
1578
+ ],
1579
+ });
1580
+
1581
+ // Get all providers bound to 'Handler'
1582
+ const handlers = MyModule.get('Handler', false, true); // Third param = asList
1583
+ console.log(handlers); // ['Handler1', 'Handler2', 'Handler3']
1584
+ ```
1585
+
1586
+ ### Optional Dependencies
1587
+
1588
+ Use the `isOptional` flag to handle missing dependencies gracefully.
1589
+
1590
+ ```ts
1591
+ @Injectable()
1592
+ class ServiceA {
1593
+ value = 'A';
1594
+ }
1595
+
1596
+ @Injectable()
1597
+ class ServiceB {
1598
+ constructor(
1599
+ private serviceA: ServiceA,
1600
+ @Inject('OPTIONAL_CONFIG') private config?: any
1601
+ ) {}
1602
+ }
1603
+
1604
+ const MyModule = ProviderModule.create({
1605
+ id: 'MyModule',
1606
+ providers: [ServiceA, ServiceB],
1607
+ });
1608
+
1609
+ // Get with optional flag
1610
+ const optionalService = MyModule.get('NOT_EXISTS', true); // isOptional = true
1611
+ console.log(optionalService); // undefined (no error thrown)
1612
+
1613
+ // Without optional flag (throws error)
1614
+ try {
1615
+ const service = MyModule.get('NOT_EXISTS'); // Throws!
1616
+ } catch (error) {
1617
+ console.error('Provider not found');
1618
+ }
1619
+ ```
1620
+
1621
+ > [!TIP]
1622
+ > Use `@Inject` when:
1623
+ >
1624
+ > - Injecting string tokens or symbols
1625
+ > - Injecting abstract classes
1626
+ > - TypeScript's automatic injection doesn't work (interfaces, etc.)
1627
+ >
1628
+ > Use `@MultiInject` when:
1629
+ >
1630
+ > - You want to collect all providers bound to a single token
1631
+ > - Implementing plugin systems
1632
+ > - Working with strategy patterns
1633
+
1634
+ ## Lifecycle Hooks
1635
+
1636
+ Lifecycle hooks allow you to execute code at specific points in a module's lifecycle.
1637
+
1638
+ ### onReady Hook
1639
+
1640
+ Invoked immediately after module creation. Perfect for initialization logic.
1641
+
1642
+ ```ts
1643
+ @Injectable()
1644
+ class DatabaseService {
1645
+ connected = false;
1646
+
1647
+ async connect() {
1648
+ console.log('Connecting to database...');
1649
+ this.connected = true;
1650
+ }
1651
+ }
1652
+
1653
+ const DatabaseModule = ProviderModule.create({
1654
+ id: 'DatabaseModule',
1655
+ providers: [DatabaseService],
1656
+ onReady: async (module) => {
1657
+ console.log('DatabaseModule is ready!');
1658
+
1659
+ // Initialize services
1660
+ const db = module.get(DatabaseService);
1661
+ await db.connect();
1662
+
1663
+ console.log('Database connected:', db.connected);
1664
+ },
1665
+ });
1666
+
1667
+ // Output:
1668
+ // DatabaseModule is ready!
1669
+ // Connecting to database...
1670
+ // Database connected: true
1671
+ ```
1672
+
1673
+ ### onReset Hook
1674
+
1675
+ Invoked when `module.reset()` is called. Provides `before` and `after` callbacks for cleanup and reinitialization.
1676
+
1677
+ ```ts
1678
+ @Injectable()
1679
+ class CacheService {
1680
+ cache = new Map();
1681
+
1682
+ clear() {
1683
+ this.cache.clear();
1684
+ }
1685
+ }
1686
+
1687
+ const CacheModule = ProviderModule.create({
1688
+ id: 'CacheModule',
1689
+ providers: [CacheService],
1690
+ onReset: () => {
1691
+ return {
1692
+ before: async (mod) => {
1693
+ console.log('Before reset - clearing cache');
1694
+ const cache = mod.get(CacheService);
1695
+ cache.clear();
1696
+ },
1697
+ after: async () => {
1698
+ console.log('After reset - cache reinitialized');
1699
+ },
1700
+ };
1701
+ },
1702
+ });
1703
+
1704
+ // Trigger reset
1705
+ await CacheModule.reset();
1706
+ // Output:
1707
+ // Before reset - clearing cache
1708
+ // After reset - cache reinitialized
1709
+ ```
1710
+
1711
+ ### onDispose Hook
1712
+
1713
+ Invoked when `module.dispose()` is called. Perfect for cleanup tasks like closing connections.
1714
+
1715
+ ```ts
1716
+ @Injectable()
1717
+ class DatabaseService {
1718
+ connected = true;
1719
+
1720
+ async disconnect() {
1721
+ console.log('Disconnecting from database...');
1722
+ this.connected = false;
1723
+ }
1724
+ }
1725
+
1726
+ @Injectable()
1727
+ class FileService {
1728
+ async closeFiles() {
1729
+ console.log('Closing open files...');
1730
+ }
1731
+ }
1732
+
1733
+ const AppModule = ProviderModule.create({
1734
+ id: 'AppModule',
1735
+ providers: [DatabaseService, FileService],
1736
+ onDispose: () => {
1737
+ return {
1738
+ before: async (mod) => {
1739
+ console.log('Cleanup started');
1740
+ const db = mod.get(DatabaseService);
1741
+ const files = mod.get(FileService);
1742
+
1743
+ await db.disconnect();
1744
+ await files.closeFiles();
1745
+ },
1746
+ after: async () => {
1747
+ console.log('Cleanup completed');
1748
+ },
1749
+ };
1750
+ },
1751
+ });
1752
+
1753
+ // Dispose module
1754
+ await AppModule.dispose();
1755
+ // Output:
1756
+ // Cleanup started
1757
+ // Disconnecting from database...
1758
+ // Closing open files...
1759
+ // Cleanup completed
1760
+
1761
+ // Module is now disposed
1762
+ console.log(AppModule.isDisposed); // true
1763
+
1764
+ // Subsequent operations throw error
1765
+ try {
1766
+ AppModule.get(DatabaseService);
1767
+ } catch (error) {
1768
+ console.error('Cannot access disposed module');
1769
+ }
1770
+ ```
1771
+
1772
+ **Complete Lifecycle Example:**
1773
+
1774
+ ```ts
1775
+ @Injectable()
1776
+ class ResourceService {
1777
+ initialized = false;
1778
+
1779
+ async initialize() {
1780
+ console.log('Initializing resource...');
1781
+ this.initialized = true;
1782
+ }
1783
+
1784
+ async cleanup() {
1785
+ console.log('Cleaning up resource...');
1786
+ this.initialized = false;
1787
+ }
1788
+ }
1789
+
1790
+ const ResourceModule = ProviderModule.create({
1791
+ id: 'ResourceModule',
1792
+ providers: [ResourceService],
1793
+
1794
+ onReady: async (module) => {
1795
+ console.log('[READY] Module created');
1796
+ const service = module.get(ResourceService);
1797
+ await service.initialize();
1798
+ },
1799
+
1800
+ onReset: () => {
1801
+ console.log('[RESET] Resetting module');
1802
+ return {
1803
+ before: async (mod) => {
1804
+ console.log('[RESET:BEFORE] Cleaning up before reset');
1805
+ const service = mod.get(ResourceService);
1806
+ await service.cleanup();
1807
+ },
1808
+ after: async () => {
1809
+ console.log('[RESET:AFTER] Reinitializing after reset');
1810
+ const service = mod.get(ResourceService);
1811
+ await service.initialize();
1812
+ },
1813
+ };
1814
+ },
1815
+
1816
+ onDispose: () => {
1817
+ console.log('[DISPOSE] Disposing module');
1818
+ return {
1819
+ before: async (mod) => {
1820
+ console.log('[DISPOSE:BEFORE] Final cleanup');
1821
+ const service = mod.get(ResourceService);
1822
+ await service.cleanup();
1823
+ },
1824
+ after: async () => {
1825
+ console.log('[DISPOSE:AFTER] Module fully disposed');
1826
+ },
1827
+ };
1828
+ },
1829
+ });
1830
+
1831
+ // Usage
1832
+ await ResourceModule.reset();
1833
+ await ResourceModule.dispose();
1834
+ ```
1835
+
1836
+ > [!IMPORTANT]
1837
+ > Lifecycle hook execution order:
1838
+ >
1839
+ > 1. **onReady** - Immediately after module creation
1840
+ > 2. **onReset** (before) → module reset → **onReset** (after)
1841
+ > 3. **onDispose** (before) → module disposal → **onDispose** (after)
1842
+
1843
+ > [!WARNING]
1844
+ > After calling `dispose()`:
1845
+ >
1846
+ > - All module operations will throw errors
1847
+ > - The module cannot be reused
1848
+ > - Internal resources are cleaned up
1849
+ > - Use for application shutdown or when modules are truly finished
1850
+
1851
+ ## Events System
1852
+
1853
+ The events system allows you to observe and react to module changes in real-time.
1854
+
1855
+ ### Subscribing to Events
1856
+
1857
+ ```ts
1858
+ import { DefinitionEventType } from '@adimm/x-injection';
1859
+
1860
+ const MyModule = ProviderModule.create({
1861
+ id: 'MyModule',
1862
+ providers: [ServiceA],
1863
+ });
1864
+
1865
+ // Subscribe to all events
1866
+ const unsubscribe = MyModule.update.subscribe(({ type, change }) => {
1867
+ console.log(`Event: ${DefinitionEventType[type]}`, change);
1868
+ });
1869
+
1870
+ // Trigger events
1871
+ MyModule.update.addProvider(ServiceB); // Event: Provider
1872
+ MyModule.update.addImport(OtherModule); // Event: Import
1873
+ const service = MyModule.get(ServiceA); // Event: GetProvider
1874
+
1875
+ // Clean up
1876
+ unsubscribe();
1877
+ ```
1878
+
1879
+ ### Available Event Types
1880
+
1881
+ ```ts
1882
+ enum DefinitionEventType {
1883
+ Noop, // No operation
1884
+ Import, // Module/blueprint added
1885
+ Provider, // Provider added
1886
+ GetProvider, // Provider resolved
1887
+ Export, // Export added
1888
+ ExportModule, // Module added to exports
1889
+ ExportProvider, // Provider added to exports
1890
+ ImportRemoved, // Module removed
1891
+ ProviderRemoved, // Provider removed
1892
+ ExportRemoved, // Export removed
1893
+ ExportModuleRemoved, // Module removed from exports
1894
+ ExportProviderRemoved, // Provider removed from exports
1895
+ }
1896
+ ```
1897
+
1898
+ ### Event Use Cases
1899
+
1900
+ **Monitoring Provider Resolution:**
1901
+
1902
+ ```ts
1903
+ const MonitoredModule = ProviderModule.create({
1904
+ id: 'MonitoredModule',
1905
+ providers: [DatabaseService, CacheService],
1906
+ });
1907
+
1908
+ MonitoredModule.update.subscribe(({ type, change }) => {
1909
+ if (type === DefinitionEventType.GetProvider) {
1910
+ console.log('Provider accessed:', change.constructor.name);
1911
+ console.log('Access time:', new Date().toISOString());
1912
+ }
1913
+ });
1914
+
1915
+ // Logs access
1916
+ const db = MonitoredModule.get(DatabaseService);
1917
+ // Output: Provider accessed: DatabaseService
1918
+ // Access time: 2024-01-15T10:30:00.000Z
1919
+ ```
1920
+
1921
+ **Tracking Module Composition:**
1922
+
1923
+ ```ts
1924
+ const RootModule = ProviderModule.create({
1925
+ id: 'RootModule',
1926
+ });
1927
+
1928
+ const compositionLog: string[] = [];
1929
+
1930
+ RootModule.update.subscribe(({ type, change }) => {
1931
+ switch (type) {
1932
+ case DefinitionEventType.Import:
1933
+ compositionLog.push(`Imported: ${change.id}`);
1934
+ break;
1935
+ case DefinitionEventType.Provider:
1936
+ const providerName = typeof change === 'function' ? change.name : change.provide;
1937
+ compositionLog.push(`Added provider: ${providerName}`);
1938
+ break;
1939
+ case DefinitionEventType.Export:
1940
+ compositionLog.push(`Exported: ${JSON.stringify(change)}`);
1941
+ break;
1942
+ }
1943
+ });
1944
+
1945
+ RootModule.update.addImport(DatabaseModule);
1946
+ RootModule.update.addProvider(ServiceA);
1947
+ RootModule.update.addProvider(ServiceB, true);
1948
+
1949
+ console.log(compositionLog);
1950
+ // [
1951
+ // 'Imported: DatabaseModule',
1952
+ // 'Added provider: ServiceA',
1953
+ // 'Added provider: ServiceB',
1954
+ // 'Exported: ServiceB'
1955
+ // ]
1956
+ ```
1957
+
1958
+ **Debugging Dynamic Changes:**
1959
+
1960
+ ```ts
1961
+ const DebugModule = ProviderModule.create({
1962
+ id: 'DebugModule',
1963
+ });
1964
+
1965
+ DebugModule.update.subscribe(({ type, change }) => {
1966
+ const eventName = DefinitionEventType[type];
1967
+
1968
+ if (type === DefinitionEventType.ImportRemoved) {
1969
+ console.warn(`⚠️ Module removed: ${change.id}`);
1970
+ } else if (type === DefinitionEventType.ProviderRemoved) {
1971
+ console.warn(`⚠️ Provider removed:`, change);
1972
+ } else {
1973
+ console.log(`✅ ${eventName}:`, change);
1974
+ }
1975
+ });
1976
+
1977
+ DebugModule.update.addProvider(ServiceA);
1978
+ DebugModule.update.removeProvider(ServiceA);
1979
+ ```
1980
+
1981
+ **Building a Module Activity Logger:**
1982
+
1983
+ ```ts
1984
+ class ModuleActivityLogger {
1985
+ private events: Array<{ timestamp: number; type: string; change: any }> = [];
1986
+
1987
+ constructor(module: ProviderModule) {
1988
+ module.update.subscribe(({ type, change }) => {
1989
+ this.events.push({
1990
+ timestamp: Date.now(),
1991
+ type: DefinitionEventType[type],
1992
+ change,
1993
+ });
1994
+ });
1995
+ }
1996
+
1997
+ getReport() {
1998
+ return {
1999
+ totalEvents: this.events.length,
2000
+ events: this.events,
2001
+ summary: this.events.reduce(
2002
+ (acc, event) => {
2003
+ acc[event.type] = (acc[event.type] || 0) + 1;
2004
+ return acc;
2005
+ },
2006
+ {} as Record<string, number>
2007
+ ),
2008
+ };
2009
+ }
2010
+ }
2011
+
2012
+ const TrackedModule = ProviderModule.create({ id: 'TrackedModule' });
2013
+ const logger = new ModuleActivityLogger(TrackedModule);
2014
+
2015
+ TrackedModule.update.addProvider(ServiceA);
2016
+ TrackedModule.update.addProvider(ServiceB);
2017
+ TrackedModule.get(ServiceA);
2018
+ TrackedModule.get(ServiceB);
2019
+
2020
+ console.log(logger.getReport());
2021
+ // {
2022
+ // totalEvents: 4,
2023
+ // events: [...],
2024
+ // summary: { Provider: 2, GetProvider: 2 }
2025
+ // }
2026
+ ```
2027
+
2028
+ > [!WARNING]
2029
+ >
2030
+ > - Always call `unsubscribe()` to prevent memory leaks
2031
+ > - Events fire **after** middlewares have executed
2032
+ > - Event handlers are synchronous - avoid heavy operations
2033
+ > - High-frequency events (like `GetProvider`) can impact performance
2034
+
2035
+ ## Middlewares
2036
+
2037
+ Middlewares intercept and transform module operations before they complete. They provide powerful customization capabilities.
2038
+
2039
+ ### BeforeGet Middleware
2040
+
2041
+ Transform provider values before they're returned to consumers.
2042
+
2043
+ ```ts
2044
+ import { MiddlewareType } from '@adimm/x-injection';
2045
+
2046
+ @Injectable()
2047
+ class UserService {
2048
+ getUser() {
2049
+ return { id: 1, name: 'Alice' };
2050
+ }
2051
+ }
2052
+
2053
+ const MyModule = ProviderModule.create({
2054
+ id: 'MyModule',
2055
+ providers: [UserService],
2056
+ });
2057
+
2058
+ // Wrap resolved providers with metadata
2059
+ MyModule.middlewares.add(MiddlewareType.BeforeGet, (provider, token, inject) => {
2060
+ // Return true to pass through unchanged
2061
+ if (!(provider instanceof UserService)) return true;
2062
+
2063
+ // Transform the value
2064
+ return {
2065
+ timestamp: Date.now(),
2066
+ instance: provider,
2067
+ metadata: { cached: false },
2068
+ };
2069
+ });
2070
+
2071
+ const result = MyModule.get(UserService);
2072
+ console.log(result);
2073
+ // {
2074
+ // timestamp: 1705320000000,
2075
+ // instance: UserService { ... },
2076
+ // metadata: { cached: false }
2077
+ // }
2078
+ ```
2079
+
2080
+ **Conditional Transformation:**
2081
+
2082
+ ```ts
2083
+ @Injectable()
2084
+ class ServiceA {}
2085
+
2086
+ @Injectable()
2087
+ class ServiceB {}
2088
+
2089
+ MyModule.middlewares.add(MiddlewareType.BeforeGet, (provider, token) => {
2090
+ // Only transform ServiceA
2091
+ if (provider instanceof ServiceA) {
2092
+ return { wrapped: provider, type: 'A' };
2093
+ }
2094
+
2095
+ // Pass through everything else unchanged
2096
+ return true;
2097
+ });
2098
+
2099
+ const serviceA = MyModule.get(ServiceA); // { wrapped: ServiceA, type: 'A' }
2100
+ const serviceB = MyModule.get(ServiceB); // ServiceB (unchanged)
2101
+ ```
2102
+
2103
+ **Using inject() to avoid infinite loops:**
2104
+
2105
+ ```ts
2106
+ @Injectable()
2107
+ class LoggerService {
2108
+ log(message: string) {
2109
+ console.log(message);
2110
+ }
2111
+ }
2112
+
2113
+ @Injectable()
2114
+ class PaymentService {}
2115
+
2116
+ MyModule.middlewares.add(MiddlewareType.BeforeGet, (provider, token, inject) => {
2117
+ if (!(provider instanceof PaymentService)) return true;
2118
+
2119
+ // Use inject() instead of module.get() to avoid infinite loop
2120
+ const logger = inject(LoggerService);
2121
+ logger.log('Payment service accessed');
2122
+
2123
+ return provider; // Or transform it
2124
+ });
2125
+ ```
2126
+
2127
+ ### BeforeAddProvider Middleware
2128
+
2129
+ Block specific providers:
2130
+
2131
+ ```ts
2132
+ MyModule.middlewares.add(MiddlewareType.BeforeAddProvider, (provider) => {
2133
+ // Block ServiceB from being added
2134
+ if ((provider as any).name === 'ServiceB') {
2135
+ return false; // Abort
2136
+ }
2137
+ return true; // Allow
2138
+ });
2139
+
2140
+ MyModule.update.addProvider(ServiceA);
2141
+ MyModule.update.addProvider(ServiceB); // Silently rejected
2142
+ MyModule.update.addProvider(ServiceC);
2143
+
2144
+ console.log(MyModule.hasProvider(ServiceA)); // true
2145
+ console.log(MyModule.hasProvider(ServiceB)); // false
2146
+ console.log(MyModule.hasProvider(ServiceC)); // true
2147
+ ```
2148
+
2149
+ ### BeforeAddImport Middleware
2150
+
2151
+ Intercept modules before they're imported.
2152
+
2153
+ ```ts
2154
+ const Module1 = ProviderModule.create({ id: 'Module1' });
2155
+ const Module2 = ProviderModule.create({ id: 'Module2' });
2156
+ const RestrictedModule = ProviderModule.create({ id: 'RestrictedModule' });
2157
+
2158
+ const MainModule = ProviderModule.create({ id: 'MainModule' });
2159
+
2160
+ // Block specific modules
2161
+ MainModule.middlewares.add(MiddlewareType.BeforeAddImport, (module) => {
2162
+ if (module.id === 'RestrictedModule') {
2163
+ console.warn(`❌ Cannot import ${module.id}`);
2164
+ return false; // Block
2165
+ }
2166
+ return true; // Allow
2167
+ });
2168
+
2169
+ MainModule.update.addImport(Module1); // ✅ Allowed
2170
+ MainModule.update.addImport(Module2); // ✅ Allowed
2171
+ MainModule.update.addImport(RestrictedModule); // ❌ Blocked
2172
+
2173
+ console.log(MainModule.isImportingModule('Module1')); // true
2174
+ console.log(MainModule.isImportingModule('RestrictedModule')); // false
2175
+ ```
2176
+
2177
+ **Auto-add providers to imported modules:**
2178
+
2179
+ ```ts
2180
+ MyModule.middlewares.add(MiddlewareType.BeforeAddImport, (importedModule) => {
2181
+ // Add logger to every imported module
2182
+ importedModule.update.addProvider(LoggerService, true);
2183
+ return importedModule; // Return modified module
2184
+ });
2185
+
2186
+ MyModule.update.addImport(FeatureModule);
2187
+ // FeatureModule now has LoggerService
2188
+ ```
2189
+
2190
+ ### OnExportAccess Middleware
2191
+
2192
+ Control which importing modules can access exports.
2193
+
2194
+ ```ts
2195
+ @Injectable()
2196
+ class SensitiveService {}
2197
+
2198
+ @Injectable()
2199
+ class PublicService {}
2200
+
2201
+ const SecureModule = ProviderModule.create({
2202
+ id: 'SecureModule',
2203
+ providers: [SensitiveService, PublicService],
2204
+ exports: [SensitiveService, PublicService],
2205
+ });
2206
+
2207
+ // Restrict access based on importer
2208
+ SecureModule.middlewares.add(MiddlewareType.OnExportAccess, (importerModule, exportToken) => {
2209
+ // Block untrusted modules from accessing SensitiveService
2210
+ if (importerModule.id === 'UntrustedModule' && exportToken === SensitiveService) {
2211
+ console.warn(`❌ ${importerModule.id} denied access to SensitiveService`);
2212
+ return false; // Deny
2213
+ }
2214
+ return true; // Allow
2215
+ });
2216
+
2217
+ const TrustedModule = ProviderModule.create({
2218
+ id: 'TrustedModule',
2219
+ imports: [SecureModule],
2220
+ });
2221
+
2222
+ const UntrustedModule = ProviderModule.create({
2223
+ id: 'UntrustedModule',
2224
+ imports: [SecureModule],
2225
+ });
2226
+
2227
+ // Trusted module can access both
2228
+ console.log(TrustedModule.hasProvider(SensitiveService)); // true
2229
+ console.log(TrustedModule.hasProvider(PublicService)); // true
2230
+
2231
+ // Untrusted module blocked from SensitiveService
2232
+ console.log(UntrustedModule.hasProvider(SensitiveService)); // false
2233
+ console.log(UntrustedModule.hasProvider(PublicService)); // true
2234
+ ```
2235
+
2236
+ **Complete access control:**
2237
+
2238
+ ```ts
2239
+ SecureModule.middlewares.add(MiddlewareType.OnExportAccess, (importer, exportToken) => {
2240
+ const allowlist = ['TrustedModule1', 'TrustedModule2'];
2241
+
2242
+ if (!allowlist.includes(String(importer.id))) {
2243
+ console.warn(`Access denied for ${importer.id}`);
2244
+ return false;
2245
+ }
2246
+
2247
+ return true;
2248
+ });
2249
+ ```
2250
+
2251
+ ### BeforeRemoveImport Middleware
2252
+
2253
+ Prevent specific modules from being removed.
2254
+
2255
+ ```ts
2256
+ const PermanentModule = ProviderModule.create({ id: 'PermanentModule' });
2257
+ const TemporaryModule = ProviderModule.create({ id: 'TemporaryModule' });
2258
+
2259
+ const MainModule = ProviderModule.create({ id: 'MainModule' });
2260
+
2261
+ // Protect PermanentModule
2262
+ MainModule.middlewares.add(MiddlewareType.BeforeRemoveImport, (module) => {
2263
+ if (module.id === 'PermanentModule') {
2264
+ console.warn(`⚠️ Cannot remove ${module.id}`);
2265
+ return false; // Block removal
2266
+ }
2267
+ return true; // Allow removal
2268
+ });
2269
+
2270
+ MainModule.update.addImport(PermanentModule);
2271
+ MainModule.update.addImport(TemporaryModule);
2272
+
2273
+ // Try to remove
2274
+ MainModule.update.removeImport(PermanentModule); // ❌ Blocked
2275
+ MainModule.update.removeImport(TemporaryModule); // ✅ Removed
2276
+
2277
+ console.log(MainModule.isImportingModule('PermanentModule')); // true
2278
+ console.log(MainModule.isImportingModule('TemporaryModule')); // false
2279
+ ```
2280
+
2281
+ ### BeforeRemoveProvider Middleware
2282
+
2283
+ Prevent specific providers from being removed.
2284
+
2285
+ ```ts
2286
+ MyModule.middlewares.add(MiddlewareType.BeforeRemoveProvider, (provider) => {
2287
+ // Block removal of critical services
2288
+ if (provider === DatabaseService) {
2289
+ console.warn('⚠️ Cannot remove DatabaseService');
2290
+ return false;
2291
+ }
2292
+ return true;
2293
+ });
2294
+
2295
+ MyModule.update.addProvider(DatabaseService);
2296
+ MyModule.update.addProvider(CacheService);
2297
+
2298
+ MyModule.update.removeProvider(DatabaseService); // ❌ Blocked
2299
+ MyModule.update.removeProvider(CacheService); // ✅ Removed
2300
+
2301
+ console.log(MyModule.hasProvider(DatabaseService)); // true
2302
+ console.log(MyModule.hasProvider(CacheService)); // false
2303
+ ```
2304
+
2305
+ ### BeforeRemoveExport Middleware
2306
+
2307
+ Prevent specific exports from being removed.
2308
+
2309
+ ```ts
2310
+ import { ProviderModuleHelpers } from '@adimm/x-injection';
2311
+
2312
+ const MyModule = ProviderModule.create({
2313
+ id: 'MyModule',
2314
+ providers: [ServiceA, ServiceB],
2315
+ exports: [ServiceA, ServiceB],
2316
+ });
2317
+
2318
+ MyModule.middlewares.add(MiddlewareType.BeforeRemoveExport, (exportDef) => {
2319
+ // Check if it's a module or provider
2320
+ if (ProviderModuleHelpers.isModule(exportDef)) {
2321
+ // Block module removal
2322
+ return exportDef.id !== 'ProtectedModule';
2323
+ } else {
2324
+ // Block ServiceA removal
2325
+ return exportDef !== ServiceA;
2326
+ }
2327
+ });
2328
+
2329
+ MyModule.update.removeFromExports(ServiceA); // ❌ Blocked
2330
+ MyModule.update.removeFromExports(ServiceB); // ✅ Removed
2331
+
2332
+ console.log(MyModule.isExportingProvider(ServiceA)); // true
2333
+ console.log(MyModule.isExportingProvider(ServiceB)); // false
2334
+ ```
2335
+
2336
+ ### All Available Middleware Types
2337
+
2338
+ ```ts
2339
+ enum MiddlewareType {
2340
+ BeforeAddImport, // Before importing a module
2341
+ BeforeAddProvider, // Before adding a provider
2342
+ BeforeGet, // Before returning provider to consumer
2343
+ BeforeRemoveImport, // Before removing an import
2344
+ BeforeRemoveProvider, // Before removing a provider
2345
+ BeforeRemoveExport, // Before removing an export
2346
+ OnExportAccess, // When importer accesses exports
2347
+ }
2348
+ ```
2349
+
2350
+ **Middleware Return Values:**
2351
+
2352
+ - `false` - Abort the operation (block it)
2353
+ - `true` - Pass through unchanged
2354
+ - Modified value - Transform and continue
2355
+ - For `BeforeGet`: Can return any value (transformation)
623
2356
 
624
2357
  > [!CAUTION]
2358
+ > Middleware best practices:
625
2359
  >
626
2360
  > - Returning `false` aborts the chain (no value returned)
627
- > - Returning `true` passes value unchanged
628
2361
  > - Middlewares execute in registration order
629
2362
  > - Always handle errors in middleware chains
2363
+ > - Use `inject()` parameter in BeforeGet to avoid infinite loops
2364
+ > - Be careful with performance - middlewares run on every operation
2365
+ > - Events fire **after** middlewares complete
630
2366
 
631
2367
  ## Testing
632
2368
 
633
- Create mock modules easily using blueprint cloning:
2369
+ xInjection makes testing easy through blueprint cloning and provider substitution.
2370
+
2371
+ ### Blueprint Cloning
2372
+
2373
+ Clone blueprints to create test-specific configurations without affecting production code.
2374
+
2375
+ ```ts
2376
+ // Production blueprint
2377
+ const DatabaseModuleBp = ProviderModule.blueprint({
2378
+ id: 'DatabaseModule',
2379
+ providers: [DatabaseService, ConnectionPool],
2380
+ exports: [DatabaseService],
2381
+ });
2382
+
2383
+ // Test blueprint - clone and modify
2384
+ const DatabaseModuleMock = DatabaseModuleBp.clone().updateDefinition({
2385
+ id: 'DatabaseModuleMock',
2386
+ providers: [
2387
+ { provide: DatabaseService, useClass: MockDatabaseService },
2388
+ { provide: ConnectionPool, useClass: MockConnectionPool },
2389
+ ],
2390
+ });
2391
+
2392
+ // Use in tests
2393
+ const TestModule = ProviderModule.create({
2394
+ id: 'TestModule',
2395
+ imports: [DatabaseModuleMock],
2396
+ });
2397
+
2398
+ const db = TestModule.get(DatabaseService); // MockDatabaseService
2399
+ ```
2400
+
2401
+ **Deep Blueprint Cloning:**
2402
+
2403
+ ```ts
2404
+ const OriginalBp = ProviderModule.blueprint({
2405
+ id: 'Original',
2406
+ providers: [ServiceA, ServiceB, ServiceC],
2407
+ exports: [ServiceA, ServiceB],
2408
+ onReady: (module) => console.log('Original ready'),
2409
+ });
2410
+
2411
+ // Clone and completely override
2412
+ const ClonedBp = OriginalBp.clone().updateDefinition({
2413
+ id: 'Cloned',
2414
+ providers: [MockServiceA, MockServiceB], // Different providers
2415
+ exports: [MockServiceA], // Different exports
2416
+ onReady: undefined, // Remove lifecycle hooks
2417
+ });
2418
+
2419
+ // Original blueprint unchanged
2420
+ console.log(OriginalBp.providers?.length); // 3
2421
+ console.log(ClonedBp.providers?.length); // 2
2422
+ ```
2423
+
2424
+ ### Provider Substitution
2425
+
2426
+ Replace real services with mocks for testing.
634
2427
 
635
2428
  ```ts
2429
+ // Production services
2430
+ @Injectable()
2431
+ class ApiService {
2432
+ async fetchData() {
2433
+ return fetch('https://api.example.com/data').then((r) => r.json());
2434
+ }
2435
+ }
2436
+
2437
+ @Injectable()
2438
+ class UserService {
2439
+ constructor(private api: ApiService) {}
2440
+
2441
+ async getUsers() {
2442
+ return this.api.fetchData();
2443
+ }
2444
+ }
2445
+
2446
+ // Mock service
2447
+ class MockApiService {
2448
+ async fetchData() {
2449
+ return { users: [{ id: 1, name: 'Mock User' }] };
2450
+ }
2451
+ }
2452
+
636
2453
  // Production module
637
- const ApiModuleBp = ProviderModule.blueprint({
638
- id: 'ApiModule',
639
- providers: [UserService, ApiService],
640
- exports: [ApiService],
2454
+ const ProductionModule = ProviderModule.create({
2455
+ id: 'ProductionModule',
2456
+ providers: [ApiService, UserService],
2457
+ });
2458
+
2459
+ // Test module with substitution
2460
+ const TestModule = ProviderModule.create({
2461
+ id: 'TestModule',
2462
+ providers: [
2463
+ { provide: ApiService, useClass: MockApiService },
2464
+ UserService, // Uses MockApiService automatically
2465
+ ],
2466
+ });
2467
+
2468
+ const userService = TestModule.get(UserService);
2469
+ const users = await userService.getUsers();
2470
+ console.log(users); // Mock data
2471
+ ```
2472
+
2473
+ ### Mocking Services
2474
+
2475
+ **Using useValue for simple mocks:**
2476
+
2477
+ ```ts
2478
+ const mockPaymentGateway = {
2479
+ charge: jest.fn().mockResolvedValue({ success: true }),
2480
+ refund: jest.fn().mockResolvedValue({ success: true }),
2481
+ };
2482
+
2483
+ const TestModule = ProviderModule.create({
2484
+ id: 'TestModule',
2485
+ providers: [{ provide: PaymentGateway, useValue: mockPaymentGateway }, PaymentService],
641
2486
  });
642
2487
 
643
- // Test module - clone and override
644
- const ApiModuleMock = ApiModuleBp.clone().updateDefinition({
645
- id: 'ApiModuleMock',
2488
+ const paymentService = TestModule.get(PaymentService);
2489
+ await paymentService.processPayment(100);
2490
+
2491
+ expect(mockPaymentGateway.charge).toHaveBeenCalledWith(100);
2492
+ ```
2493
+
2494
+ **Using useFactory for complex mocks:**
2495
+
2496
+ ```ts
2497
+ const TestModule = ProviderModule.create({
2498
+ id: 'TestModule',
646
2499
  providers: [
647
- { provide: UserService, useClass: MockUserService },
648
2500
  {
649
- provide: ApiService,
650
- useValue: {
651
- sendRequest: jest.fn().mockResolvedValue({ data: 'test' }),
2501
+ provide: 'DATABASE_CONNECTION',
2502
+ useFactory: () => {
2503
+ return {
2504
+ query: jest.fn().mockResolvedValue([{ id: 1, name: 'Test' }]),
2505
+ connect: jest.fn().mockResolvedValue(true),
2506
+ disconnect: jest.fn().mockResolvedValue(true),
2507
+ };
652
2508
  },
653
2509
  },
654
2510
  ],
655
2511
  });
656
2512
 
657
- // Use in tests
658
- const testModule = ProviderModule.create({
659
- imports: [ApiModuleMock],
2513
+ const db = TestModule.get('DATABASE_CONNECTION');
2514
+ const results = await db.query('SELECT * FROM users');
2515
+ expect(results).toEqual([{ id: 1, name: 'Test' }]);
2516
+ ```
2517
+
2518
+ **Complete Testing Example:**
2519
+
2520
+ ```ts
2521
+ // Production code
2522
+ @Injectable()
2523
+ class EmailService {
2524
+ async sendEmail(to: string, subject: string, body: string) {
2525
+ // Real email sending logic
2526
+ console.log(`Sending email to ${to}`);
2527
+ }
2528
+ }
2529
+
2530
+ @Injectable()
2531
+ class UserNotificationService {
2532
+ constructor(private emailService: EmailService) {}
2533
+
2534
+ async notifyUser(userId: string, message: string) {
2535
+ await this.emailService.sendEmail(`user${userId}@example.com`, 'Notification', message);
2536
+ }
2537
+ }
2538
+
2539
+ // Test code
2540
+ describe('UserNotificationService', () => {
2541
+ it('should send email notification', async () => {
2542
+ const mockEmailService = {
2543
+ sendEmail: jest.fn().mockResolvedValue(undefined),
2544
+ };
2545
+
2546
+ const TestModule = ProviderModule.create({
2547
+ id: 'TestModule',
2548
+ providers: [{ provide: EmailService, useValue: mockEmailService }, UserNotificationService],
2549
+ });
2550
+
2551
+ const notificationService = TestModule.get(UserNotificationService);
2552
+ await notificationService.notifyUser('123', 'Test message');
2553
+
2554
+ expect(mockEmailService.sendEmail).toHaveBeenCalledWith('user123@example.com', 'Notification', 'Test message');
2555
+ });
2556
+ });
2557
+ ```
2558
+
2559
+ **Testing with Multiple Module Layers:**
2560
+
2561
+ ```ts
2562
+ // Create mock blueprint
2563
+ const MockDataModuleBp = ProviderModule.blueprint({
2564
+ id: 'MockDataModule',
2565
+ providers: [
2566
+ { provide: DatabaseService, useClass: MockDatabaseService },
2567
+ { provide: CacheService, useClass: MockCacheService },
2568
+ ],
2569
+ exports: [DatabaseService, CacheService],
2570
+ });
2571
+
2572
+ // Use mock in feature module tests
2573
+ const FeatureModuleTest = ProviderModule.create({
2574
+ id: 'FeatureModuleTest',
2575
+ imports: [MockDataModuleBp],
2576
+ providers: [FeatureService],
2577
+ });
2578
+
2579
+ const featureService = FeatureModuleTest.get(FeatureService);
2580
+ // FeatureService receives mock dependencies
2581
+ ```
2582
+
2583
+ > [!TIP]
2584
+ > Testing strategies:
2585
+ >
2586
+ > - Use `blueprint.clone()` to create test variations without modifying originals
2587
+ > - Use `useValue` for simple mocks with jest.fn()
2588
+ > - Use `useClass` for class-based mocks with behavior
2589
+ > - Use `useFactory` for complex mock setup
2590
+ > - Test module isolation by mocking all external dependencies
2591
+ > - Verify mock calls with jest expectations
2592
+
2593
+ ## Advanced Module API
2594
+
2595
+ ### Query Methods
2596
+
2597
+ Check module state and relationships.
2598
+
2599
+ ```ts
2600
+ const MyModule = ProviderModule.create({
2601
+ id: 'MyModule',
2602
+ imports: [DatabaseModule, ConfigModule],
2603
+ providers: [ServiceA, ServiceB],
2604
+ exports: [ServiceA, DatabaseModule],
2605
+ });
2606
+
2607
+ // Provider queries
2608
+ MyModule.hasProvider(ServiceA); // true
2609
+ MyModule.hasProvider(ServiceC); // false
2610
+ MyModule.hasProvider(DatabaseService); // true (from import)
2611
+
2612
+ // Import queries
2613
+ MyModule.isImportingModule('DatabaseModule'); // true
2614
+ MyModule.isImportingModule(ConfigModule); // true (by reference)
2615
+ MyModule.isImportingModule('NonExistent'); // false
2616
+
2617
+ // Export queries
2618
+ MyModule.isExportingProvider(ServiceA); // true
2619
+ MyModule.isExportingProvider(ServiceB); // false
2620
+ MyModule.isExportingModule('DatabaseModule'); // true
2621
+ MyModule.isExportingModule(ConfigModule); // false
2622
+
2623
+ // State queries
2624
+ MyModule.isDisposed; // false
2625
+ MyModule.id; // 'MyModule'
2626
+ ```
2627
+
2628
+ **Using Symbol Identifiers:**
2629
+
2630
+ ```ts
2631
+ const MODULE_ID = Symbol('FeatureModule');
2632
+
2633
+ const FeatureModule = ProviderModule.create({
2634
+ id: MODULE_ID,
2635
+ providers: [FeatureService],
2636
+ exports: [FeatureService],
2637
+ });
2638
+
2639
+ const AppModule = ProviderModule.create({
2640
+ id: 'AppModule',
2641
+ imports: [FeatureModule],
2642
+ });
2643
+
2644
+ // Query using Symbol
2645
+ console.log(AppModule.isImportingModule(MODULE_ID)); // true
2646
+ ```
2647
+
2648
+ ### Multiple Provider Binding
2649
+
2650
+ Bind multiple providers to the same token and retrieve them as a list.
2651
+
2652
+ ```ts
2653
+ @Injectable()
2654
+ abstract class Plugin {
2655
+ abstract execute(): void;
2656
+ }
2657
+
2658
+ @Injectable()
2659
+ class PluginA extends Plugin {
2660
+ execute() {
2661
+ console.log('Plugin A executing');
2662
+ }
2663
+ }
2664
+
2665
+ @Injectable()
2666
+ class PluginB extends Plugin {
2667
+ execute() {
2668
+ console.log('Plugin B executing');
2669
+ }
2670
+ }
2671
+
2672
+ @Injectable()
2673
+ class PluginC extends Plugin {
2674
+ execute() {
2675
+ console.log('Plugin C executing');
2676
+ }
2677
+ }
2678
+
2679
+ const PluginModule = ProviderModule.create({
2680
+ id: 'PluginModule',
2681
+ providers: [
2682
+ { provide: Plugin, useClass: PluginA },
2683
+ { provide: Plugin, useClass: PluginB },
2684
+ { provide: Plugin, useClass: PluginC },
2685
+ ],
2686
+ });
2687
+
2688
+ // Get all plugins as array (third parameter = asList)
2689
+ const plugins = PluginModule.get(Plugin, false, true);
2690
+ console.log(plugins.length); // 3
2691
+
2692
+ // Execute all plugins
2693
+ plugins.forEach((plugin) => plugin.execute());
2694
+ // Output:
2695
+ // Plugin A executing
2696
+ // Plugin B executing
2697
+ // Plugin C executing
2698
+ ```
2699
+
2700
+ **String Token Example:**
2701
+
2702
+ ```ts
2703
+ const MyModule = ProviderModule.create({
2704
+ id: 'MyModule',
2705
+ providers: [
2706
+ { provide: 'Handler', useValue: 'Handler1' },
2707
+ { provide: 'Handler', useValue: 'Handler2' },
2708
+ { provide: 'Handler', useValue: 'Handler3' },
2709
+ ],
2710
+ });
2711
+
2712
+ const handlers = MyModule.get('Handler', false, true);
2713
+ console.log(handlers); // ['Handler1', 'Handler2', 'Handler3']
2714
+ ```
2715
+
2716
+ ### Batch Resolution with getMany()
2717
+
2718
+ Resolve multiple providers in a single call.
2719
+
2720
+ ```ts
2721
+ @Injectable()
2722
+ class ServiceA {
2723
+ name = 'A';
2724
+ }
2725
+
2726
+ @Injectable()
2727
+ class ServiceB {
2728
+ name = 'B';
2729
+ }
2730
+
2731
+ @Injectable()
2732
+ class ServiceC {
2733
+ name = 'C';
2734
+ }
2735
+
2736
+ const MyModule = ProviderModule.create({
2737
+ id: 'MyModule',
2738
+ providers: [
2739
+ ServiceA,
2740
+ ServiceB,
2741
+ ServiceC,
2742
+ { provide: 'CONFIG_A', useValue: 'config-a' },
2743
+ { provide: 'CONFIG_B', useValue: 'config-b' },
2744
+ ],
2745
+ });
2746
+
2747
+ // Simple getMany
2748
+ const [serviceA, serviceB, configA] = MyModule.getMany(ServiceA, ServiceB, 'CONFIG_A');
2749
+
2750
+ console.log(serviceA.name); // 'A'
2751
+ console.log(serviceB.name); // 'B'
2752
+ console.log(configA); // 'config-a'
2753
+ ```
2754
+
2755
+ **With Options:**
2756
+
2757
+ ```ts
2758
+ // Optional providers
2759
+ const [serviceA, missing, serviceC] = MyModule.getMany(
2760
+ ServiceA,
2761
+ { provider: 'NON_EXISTENT', isOptional: true },
2762
+ ServiceC
2763
+ );
2764
+
2765
+ console.log(serviceA); // ServiceA instance
2766
+ console.log(missing); // undefined (no error)
2767
+ console.log(serviceC); // ServiceC instance
2768
+
2769
+ // Get as list (multiple bindings)
2770
+ const HandlerModule = ProviderModule.create({
2771
+ id: 'HandlerModule',
2772
+ providers: [
2773
+ { provide: 'Handler', useValue: 'H1' },
2774
+ { provide: 'Handler', useValue: 'H2' },
2775
+ { provide: 'Handler', useValue: 'H3' },
2776
+ ],
2777
+ });
2778
+
2779
+ const [handlers] = HandlerModule.getMany({
2780
+ provider: 'Handler',
2781
+ asList: true,
660
2782
  });
2783
+
2784
+ console.log(handlers); // ['H1', 'H2', 'H3']
2785
+ ```
2786
+
2787
+ **Complex Example:**
2788
+
2789
+ ```ts
2790
+ const [database, cache, optionalLogger, allPlugins, config] = MyModule.getMany(
2791
+ DatabaseService,
2792
+ CacheService,
2793
+ { provider: LoggerService, isOptional: true },
2794
+ { provider: Plugin, asList: true },
2795
+ 'APP_CONFIG'
2796
+ );
2797
+
2798
+ // All providers resolved in one call
2799
+ // optionalLogger is undefined if not available
2800
+ // allPlugins is an array of all Plugin bindings
661
2801
  ```
662
2802
 
2803
+ > [!IMPORTANT] > `getMany()` parameter types:
2804
+ >
2805
+ > - **Simple**: Just pass the token directly
2806
+ > - **With options**: Use object with `provider`, `isOptional`, and/or `asList`
2807
+
663
2808
  ## Resources
664
2809
 
665
2810
  📚 **[Full API Documentation](https://adimarianmutu.github.io/x-injection/index.html)** - Complete TypeDoc reference
@@ -668,14 +2813,23 @@ const testModule = ProviderModule.create({
668
2813
 
669
2814
  💡 **[GitHub Issues](https://github.com/AdiMarianMutu/x-injection/issues)** - Bug reports and feature requests
670
2815
 
2816
+ 🌟 **[GitHub Repository](https://github.com/AdiMarianMutu/x-injection)** - Source code and examples
2817
+
671
2818
  ## Contributing
672
2819
 
673
- Contributions welcome! Please ensure code follows the project style guidelines.
2820
+ Contributions are welcome! Please ensure code follows the project style guidelines and includes appropriate tests.
2821
+
2822
+ 1. Fork the repository
2823
+ 2. Create a feature branch
2824
+ 3. Make your changes with tests
2825
+ 4. Submit a pull request
674
2826
 
675
2827
  ## Credits
676
2828
 
677
2829
  **Author:** [Adi-Marian Mutu](https://www.linkedin.com/in/mutu-adi-marian/)
2830
+
678
2831
  **Built on:** [InversifyJS](https://github.com/inversify/monorepo)
2832
+
679
2833
  **Logo:** [Alexandru Turica](https://www.linkedin.com/in/alexandru-turica-82215522b/)
680
2834
 
681
2835
  ## License