@adimm/x-injection 3.0.0 → 3.0.2

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