ts-ioc-container 46.6.1 → 46.6.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.
Files changed (84) hide show
  1. package/README.md +1254 -639
  2. package/cjm/container/Container.js +4 -3
  3. package/cjm/hooks/HooksRunner.js +2 -2
  4. package/cjm/hooks/hook.js +2 -2
  5. package/cjm/index.js +18 -11
  6. package/cjm/injector/IInjector.js +2 -2
  7. package/cjm/injector/inject.js +6 -5
  8. package/cjm/metadata/class.js +12 -0
  9. package/cjm/metadata/method.js +10 -0
  10. package/cjm/metadata/parameter.js +13 -0
  11. package/cjm/provider/IProvider.js +2 -2
  12. package/cjm/provider/Provider.js +2 -2
  13. package/cjm/registration/IRegistration.js +3 -3
  14. package/cjm/registration/Registration.js +5 -4
  15. package/cjm/token/BindToken.js +2 -2
  16. package/cjm/token/GroupAliasToken.js +1 -1
  17. package/cjm/token/toToken.js +2 -2
  18. package/cjm/utils/array.js +20 -0
  19. package/cjm/utils/basic.js +9 -0
  20. package/cjm/utils/fp.js +9 -0
  21. package/cjm/utils/promise.js +5 -0
  22. package/cjm/utils/proxy.js +20 -0
  23. package/esm/container/Container.js +2 -1
  24. package/esm/hooks/HooksRunner.js +1 -1
  25. package/esm/hooks/hook.js +1 -1
  26. package/esm/index.js +8 -2
  27. package/esm/injector/IInjector.js +1 -1
  28. package/esm/injector/inject.js +3 -2
  29. package/esm/metadata/class.js +7 -0
  30. package/esm/metadata/method.js +5 -0
  31. package/esm/metadata/parameter.js +8 -0
  32. package/esm/provider/IProvider.js +1 -1
  33. package/esm/provider/Provider.js +1 -1
  34. package/esm/registration/IRegistration.js +1 -1
  35. package/esm/registration/Registration.js +2 -1
  36. package/esm/token/BindToken.js +1 -1
  37. package/esm/token/GroupAliasToken.js +1 -1
  38. package/esm/token/toToken.js +1 -1
  39. package/esm/utils/array.js +16 -0
  40. package/esm/utils/basic.js +6 -0
  41. package/esm/utils/fp.js +6 -0
  42. package/esm/utils/promise.js +1 -0
  43. package/esm/utils/proxy.js +16 -0
  44. package/package.json +1 -1
  45. package/typings/container/Container.d.ts +1 -1
  46. package/typings/container/EmptyContainer.d.ts +1 -1
  47. package/typings/container/IContainer.d.ts +1 -1
  48. package/typings/hooks/hook.d.ts +1 -1
  49. package/typings/hooks/injectProp.d.ts +1 -1
  50. package/typings/hooks/onConstruct.d.ts +1 -1
  51. package/typings/hooks/onDispose.d.ts +1 -1
  52. package/typings/index.d.ts +9 -3
  53. package/typings/injector/IInjector.d.ts +1 -1
  54. package/typings/injector/MetadataInjector.d.ts +1 -1
  55. package/typings/injector/ProxyInjector.d.ts +1 -1
  56. package/typings/injector/SimpleInjector.d.ts +1 -1
  57. package/typings/injector/inject.d.ts +1 -1
  58. package/typings/metadata/class.d.ts +2 -0
  59. package/typings/metadata/method.d.ts +2 -0
  60. package/typings/metadata/parameter.d.ts +3 -0
  61. package/typings/provider/IProvider.d.ts +1 -1
  62. package/typings/provider/Provider.d.ts +2 -1
  63. package/typings/registration/IRegistration.d.ts +2 -1
  64. package/typings/registration/Registration.d.ts +2 -1
  65. package/typings/select.d.ts +1 -1
  66. package/typings/token/ClassToken.d.ts +3 -3
  67. package/typings/token/GroupAliasToken.d.ts +2 -2
  68. package/typings/token/GroupInstanceToken.d.ts +1 -1
  69. package/typings/token/SingleAliasToken.d.ts +3 -3
  70. package/typings/token/toToken.d.ts +1 -1
  71. package/typings/utils/array.d.ts +4 -0
  72. package/typings/utils/basic.d.ts +13 -0
  73. package/typings/utils/fp.d.ts +13 -0
  74. package/typings/utils/promise.d.ts +1 -0
  75. package/typings/utils/proxy.d.ts +2 -0
  76. package/cjm/metadata.js +0 -29
  77. package/cjm/types.js +0 -2
  78. package/cjm/utils.js +0 -48
  79. package/esm/metadata.js +0 -20
  80. package/esm/types.js +0 -1
  81. package/esm/utils.js +0 -40
  82. package/typings/metadata.d.ts +0 -7
  83. package/typings/types.d.ts +0 -8
  84. package/typings/utils.d.ts +0 -15
package/README.md CHANGED
@@ -22,6 +22,9 @@
22
22
  ## Content
23
23
 
24
24
  - [Setup](#setup)
25
+ - [Quickstart](#quickstart)
26
+ - [Cheatsheet](#cheatsheet)
27
+ - [Recipes](#recipes)
25
28
  - [Container](#container)
26
29
  - [Basic usage](#basic-usage)
27
30
  - [Scope](#scope) `tags`
@@ -74,6 +77,86 @@ And `tsconfig.json` should have next options:
74
77
  }
75
78
  ```
76
79
 
80
+ ## Quickstart
81
+
82
+ ```typescript
83
+ import 'reflect-metadata';
84
+ import { Container, register, bindTo, singleton } from 'ts-ioc-container';
85
+
86
+ @register(bindTo('ILogger'), singleton())
87
+ class Logger {
88
+ log(message: string) {
89
+ console.log(message);
90
+ }
91
+ }
92
+
93
+ class App {
94
+ constructor(private logger = container.resolve<Logger>('ILogger')) {}
95
+ start() {
96
+ this.logger.log('hello');
97
+ }
98
+ }
99
+
100
+ const container = new Container({ tags: ['application'] }).addRegistration(Logger);
101
+ container.resolve(App).start();
102
+ ```
103
+
104
+ ## Cheatsheet
105
+
106
+ - Register class with key: `@register(bindTo('Key')) class Service {}`
107
+ - Register value: `R.fromValue(config).bindToKey('Config')`
108
+ - Singleton: `@register(singleton())`
109
+ - Scoped registration: `@register(scope((s) => s.hasTag('request')))`
110
+ - Resolve by alias: `container.resolveByAlias('Alias')`
111
+ - Current scope token: `select.scope.current`
112
+ - Lazy token: `select.token('Service').lazy()`
113
+ - Inject decorator: `@inject('Key')`
114
+ - Property inject: `injectProp(target, 'propName', select.token('Key'))`
115
+
116
+ ## Recipes
117
+
118
+ ### Express/Next handler (per-request scope)
119
+ ```typescript
120
+ const app = new Container({ tags: ['application'] })
121
+ .addRegistration(R.fromClass(Logger).pipe(singleton()));
122
+
123
+ function handleRequest() {
124
+ const requestScope = app.createScope({ tags: ['request'] });
125
+ const logger = requestScope.resolve<Logger>('Logger');
126
+ logger.log('req started');
127
+ }
128
+ ```
129
+
130
+ ### Background worker (singleton client, transient jobs)
131
+ ```typescript
132
+ @register(singleton())
133
+ class QueueClient {}
134
+
135
+ class JobHandler {
136
+ constructor(@inject('QueueClient') private queue: QueueClient) {}
137
+ }
138
+
139
+ const worker = new Container({ tags: ['worker'] })
140
+ .addRegistration(R.fromClass(QueueClient))
141
+ .addRegistration(R.fromClass(JobHandler));
142
+ ```
143
+
144
+ ### Frontend widget/page scope with lazy dependency
145
+ ```typescript
146
+ @register(bindTo('FeatureFlags'), singleton())
147
+ class FeatureFlags {
148
+ load() { /* fetch flags */ }
149
+ }
150
+
151
+ class Widget {
152
+ constructor(@inject(select.token('FeatureFlags').lazy()) private flags: FeatureFlags) {}
153
+ }
154
+
155
+ const page = new Container({ tags: ['page'] })
156
+ .addRegistration(R.fromClass(FeatureFlags))
157
+ .addRegistration(R.fromClass(Widget));
158
+ ```
159
+
77
160
  ## Container
78
161
  `IContainer` consists of:
79
162
 
@@ -84,33 +167,77 @@ And `tsconfig.json` should have next options:
84
167
  ### Basic usage
85
168
 
86
169
  ```typescript
170
+ import 'reflect-metadata';
87
171
  import { Container, type IContainer, inject, Registration as R, select } from 'ts-ioc-container';
88
172
 
173
+ /**
174
+ * User Management Domain - Basic Dependency Injection
175
+ *
176
+ * This example demonstrates how to wire up a simple authentication service
177
+ * that depends on a user repository. This pattern is common in web applications
178
+ * where services need database access.
179
+ */
89
180
  describe('Basic usage', function () {
90
- class Logger {
91
- name = 'Logger';
181
+ // Domain types
182
+ interface User {
183
+ id: string;
184
+ email: string;
185
+ passwordHash: string;
186
+ }
187
+
188
+ // Repository interface - abstracts database access
189
+ interface IUserRepository {
190
+ findByEmail(email: string): User | undefined;
191
+ }
192
+
193
+ // Concrete implementation
194
+ class UserRepository implements IUserRepository {
195
+ private users: User[] = [{ id: '1', email: 'admin@example.com', passwordHash: 'hashed_password' }];
196
+
197
+ findByEmail(email: string): User | undefined {
198
+ return this.users.find((u) => u.email === email);
199
+ }
92
200
  }
93
201
 
94
202
  it('should inject dependencies', function () {
95
- class App {
96
- constructor(@inject('ILogger') public logger: Logger) {}
203
+ // AuthService depends on IUserRepository
204
+ class AuthService {
205
+ constructor(@inject('IUserRepository') private userRepo: IUserRepository) {}
206
+
207
+ authenticate(email: string): boolean {
208
+ const user = this.userRepo.findByEmail(email);
209
+ return user !== undefined;
210
+ }
97
211
  }
98
212
 
99
- const container = new Container().addRegistration(R.fromClass(Logger).bindToKey('ILogger'));
213
+ // Wire up the container
214
+ const container = new Container().addRegistration(R.fromClass(UserRepository).bindTo('IUserRepository'));
215
+
216
+ // Resolve AuthService - UserRepository is automatically injected
217
+ const authService = container.resolve(AuthService);
100
218
 
101
- expect(container.resolve(App).logger.name).toBe('Logger');
219
+ expect(authService.authenticate('admin@example.com')).toBe(true);
220
+ expect(authService.authenticate('unknown@example.com')).toBe(false);
102
221
  });
103
222
 
104
- it('should inject current scope', function () {
105
- const root = new Container({ tags: ['root'] });
223
+ it('should inject current scope for request context', function () {
224
+ // In Express.js, each request gets its own scope
225
+ // Services can access the current scope to resolve request-specific dependencies
226
+ const appContainer = new Container({ tags: ['application'] });
106
227
 
107
- class App {
108
- constructor(@inject(select.scope.current) public scope: IContainer) {}
228
+ class RequestHandler {
229
+ constructor(@inject(select.scope.current) public requestScope: IContainer) {}
230
+
231
+ handleRequest(): string {
232
+ // Access request-scoped dependencies
233
+ return this.requestScope.hasTag('application') ? 'app-scope' : 'request-scope';
234
+ }
109
235
  }
110
236
 
111
- const app = root.resolve(App);
237
+ const handler = appContainer.resolve(RequestHandler);
112
238
 
113
- expect(app.scope).toBe(root);
239
+ expect(handler.requestScope).toBe(appContainer);
240
+ expect(handler.handleRequest()).toBe('app-scope');
114
241
  });
115
242
  });
116
243
 
@@ -124,6 +251,7 @@ Sometimes you need to create a scope of container. For example, when you want to
124
251
  - NOTICE: when you create a scope then we clone ONLY tags-matched providers.
125
252
 
126
253
  ```typescript
254
+ import 'reflect-metadata';
127
255
  import {
128
256
  bindTo,
129
257
  Container,
@@ -137,29 +265,78 @@ import {
137
265
  singleton,
138
266
  } from 'ts-ioc-container';
139
267
 
140
- @register(bindTo('ILogger'), scope((s) => s.hasTag('child')), singleton())
141
- class Logger {}
268
+ /**
269
+ * User Management Domain - Request Scopes
270
+ *
271
+ * In web applications, each HTTP request typically gets its own scope.
272
+ * This allows request-specific data (current user, request ID, etc.)
273
+ * to be isolated between concurrent requests.
274
+ *
275
+ * Scope hierarchy:
276
+ * Application (singleton services)
277
+ * └── Request (per-request services)
278
+ * └── Transaction (database transaction boundary)
279
+ */
280
+
281
+ // SessionService is only available in request scope - not at application level
282
+ // This prevents accidental access to request-specific data from singletons
283
+ @register(bindTo('ISessionService'), scope((s) => s.hasTag('request')), singleton())
284
+ class SessionService {
285
+ private userId: string | null = null;
286
+
287
+ setCurrentUser(userId: string) {
288
+ this.userId = userId;
289
+ }
290
+
291
+ getCurrentUserId(): string | null {
292
+ return this.userId;
293
+ }
294
+ }
142
295
 
143
296
  describe('Scopes', function () {
144
- it('should resolve dependencies from scope', function () {
145
- const root = new Container({ tags: ['root'] }).addRegistration(R.fromClass(Logger));
146
- const child = root.createScope({ tags: ['child'] });
297
+ it('should isolate request-scoped services', function () {
298
+ // Application container - lives for entire app lifetime
299
+ const appContainer = new Container({ tags: ['application'] }).addRegistration(R.fromClass(SessionService));
300
+
301
+ // Simulate two concurrent HTTP requests
302
+ const request1Scope = appContainer.createScope({ tags: ['request'] });
303
+ const request2Scope = appContainer.createScope({ tags: ['request'] });
304
+
305
+ // Each request has its own SessionService instance
306
+ const session1 = request1Scope.resolve<SessionService>('ISessionService');
307
+ const session2 = request2Scope.resolve<SessionService>('ISessionService');
147
308
 
148
- expect(child.resolve('ILogger')).toBe(child.resolve('ILogger'));
149
- expect(() => root.resolve('ILogger')).toThrow(DependencyNotFoundError);
309
+ session1.setCurrentUser('user-1');
310
+ session2.setCurrentUser('user-2');
311
+
312
+ // Sessions are isolated - user data doesn't leak between requests
313
+ expect(session1.getCurrentUserId()).toBe('user-1');
314
+ expect(session2.getCurrentUserId()).toBe('user-2');
315
+ expect(session1).not.toBe(session2);
316
+
317
+ // SessionService is NOT available at application level (security!)
318
+ expect(() => appContainer.resolve('ISessionService')).toThrow(DependencyNotFoundError);
150
319
  });
151
320
 
152
- it('should inject new scope', function () {
153
- const root = new Container({ tags: ['root'] });
321
+ it('should create child scopes for transactions', function () {
322
+ const appContainer = new Container({ tags: ['application'] });
154
323
 
155
- class App {
156
- constructor(@inject(select.scope.create({ tags: ['child'] })) public scope: IContainer) {}
324
+ // RequestHandler can create a transaction scope for database operations
325
+ class RequestHandler {
326
+ constructor(@inject(select.scope.create({ tags: ['transaction'] })) public transactionScope: IContainer) {}
327
+
328
+ executeInTransaction(): boolean {
329
+ // Transaction scope inherits from request scope
330
+ // Database operations can be rolled back together
331
+ return this.transactionScope.hasTag('transaction');
332
+ }
157
333
  }
158
334
 
159
- const app = root.resolve(App);
335
+ const handler = appContainer.resolve(RequestHandler);
160
336
 
161
- expect(app.scope).not.toBe(root);
162
- expect(app.scope.hasTag('child')).toBe(true);
337
+ expect(handler.transactionScope).not.toBe(appContainer);
338
+ expect(handler.transactionScope.hasTag('transaction')).toBe(true);
339
+ expect(handler.executeInTransaction()).toBe(true);
163
340
  });
164
341
  });
165
342
 
@@ -173,52 +350,69 @@ Sometimes you want to get all instances from container and its scopes. For examp
173
350
  ```typescript
174
351
  import { bindTo, Container, inject, register, Registration as R, select } from 'ts-ioc-container';
175
352
 
353
+ /**
354
+ * User Management Domain - Instance Collection
355
+ *
356
+ * Sometimes you need access to all instances of a certain type:
357
+ * - Collect all active database connections for health checks
358
+ * - Gather all loggers to flush buffers before shutdown
359
+ * - Find all request handlers for metrics collection
360
+ *
361
+ * The `select.instances()` token resolves all created instances,
362
+ * optionally filtered by a predicate function.
363
+ */
176
364
  describe('Instances', function () {
177
365
  @register(bindTo('ILogger'))
178
366
  class Logger {}
179
367
 
180
- it('should return injected instances', () => {
368
+ it('should collect instances across scope hierarchy', () => {
369
+ // App that needs access to all logger instances (e.g., for flushing)
181
370
  class App {
182
371
  constructor(@inject(select.instances()) public loggers: Logger[]) {}
183
372
  }
184
373
 
185
- const root = new Container({ tags: ['root'] }).addRegistration(R.fromClass(Logger));
186
- const child = root.createScope({ tags: ['child'] });
374
+ const appContainer = new Container({ tags: ['application'] }).addRegistration(R.fromClass(Logger));
375
+ const requestScope = appContainer.createScope({ tags: ['request'] });
187
376
 
188
- root.resolve('ILogger');
189
- child.resolve('ILogger');
377
+ // Create loggers in different scopes
378
+ appContainer.resolve('ILogger');
379
+ requestScope.resolve('ILogger');
190
380
 
191
- const rootApp = root.resolve(App);
192
- const childApp = child.resolve(App);
381
+ const appLevel = appContainer.resolve(App);
382
+ const requestLevel = requestScope.resolve(App);
193
383
 
194
- expect(childApp.loggers.length).toBe(1);
195
- expect(rootApp.loggers.length).toBe(2);
384
+ // Request scope sees only its own instance
385
+ expect(requestLevel.loggers.length).toBe(1);
386
+ // Application scope sees all instances (cascades up from children)
387
+ expect(appLevel.loggers.length).toBe(2);
196
388
  });
197
389
 
198
- it('should return only current scope instances', () => {
390
+ it('should return only current scope instances when cascade is disabled', () => {
391
+ // Only get instances from current scope, not parent scopes
199
392
  class App {
200
393
  constructor(@inject(select.instances().cascade(false)) public loggers: Logger[]) {}
201
394
  }
202
395
 
203
- const root = new Container({ tags: ['root'] }).addRegistration(R.fromClass(Logger));
204
- const child = root.createScope({ tags: ['child'] });
396
+ const appContainer = new Container({ tags: ['application'] }).addRegistration(R.fromClass(Logger));
397
+ const requestScope = appContainer.createScope({ tags: ['request'] });
205
398
 
206
- root.resolve('ILogger');
207
- child.resolve('ILogger');
399
+ appContainer.resolve('ILogger');
400
+ requestScope.resolve('ILogger');
208
401
 
209
- const rootApp = root.resolve(App);
402
+ const appLevel = appContainer.resolve(App);
210
403
 
211
- expect(rootApp.loggers.length).toBe(1);
404
+ // Only application-level instance, not request-level
405
+ expect(appLevel.loggers.length).toBe(1);
212
406
  });
213
407
 
214
- it('should return injected instances by decorator', () => {
408
+ it('should filter instances by predicate', () => {
215
409
  const isLogger = (instance: unknown) => instance instanceof Logger;
216
410
 
217
411
  class App {
218
412
  constructor(@inject(select.instances(isLogger)) public loggers: Logger[]) {}
219
413
  }
220
414
 
221
- const container = new Container().addRegistration(R.fromClass(Logger));
415
+ const container = new Container({ tags: ['application'] }).addRegistration(R.fromClass(Logger));
222
416
 
223
417
  const logger0 = container.resolve('ILogger');
224
418
  const logger1 = container.resolve('ILogger');
@@ -240,18 +434,99 @@ Sometimes you want to dispose container and all its scopes. For example, when yo
240
434
  - when container is disposed then it unregisters all providers and remove all instances
241
435
 
242
436
  ```typescript
437
+ import 'reflect-metadata';
243
438
  import { Container, ContainerDisposedError, Registration as R, select } from 'ts-ioc-container';
244
439
 
245
- class Logger {}
440
+ /**
441
+ * User Management Domain - Resource Cleanup
442
+ *
443
+ * When a scope ends (e.g., HTTP request completes), resources must be cleaned up:
444
+ * - Database connections returned to pool
445
+ * - File handles closed
446
+ * - Temporary files deleted
447
+ * - Cache entries cleared
448
+ *
449
+ * The container.dispose() method:
450
+ * 1. Executes all onDispose hooks
451
+ * 2. Clears all instances and registrations
452
+ * 3. Detaches from parent scope
453
+ * 4. Prevents further resolution
454
+ */
455
+
456
+ // Simulates a database connection that must be closed
457
+ class DatabaseConnection {
458
+ isClosed = false;
459
+
460
+ query(sql: string): string[] {
461
+ if (this.isClosed) {
462
+ throw new Error('Connection is closed');
463
+ }
464
+ return [`Result for: ${sql}`];
465
+ }
466
+
467
+ close(): void {
468
+ this.isClosed = true;
469
+ }
470
+ }
246
471
 
247
472
  describe('Disposing', function () {
248
- it('should container and make it unavailable for the further usage', function () {
249
- const root = new Container({ tags: ['root'] }).addRegistration(R.fromClass(Logger).bindToKey('ILogger'));
473
+ it('should dispose container and prevent further usage', function () {
474
+ const appContainer = new Container({ tags: ['application'] }).addRegistration(
475
+ R.fromClass(DatabaseConnection).bindTo('IDatabase'),
476
+ );
477
+
478
+ // Create a request scope with a database connection
479
+ const requestScope = appContainer.createScope({ tags: ['request'] });
480
+ const connection = requestScope.resolve<DatabaseConnection>('IDatabase');
481
+
482
+ // Connection works normally
483
+ expect(connection.query('SELECT * FROM users')).toEqual(['Result for: SELECT * FROM users']);
250
484
 
251
- root.dispose();
485
+ // Request ends - dispose the scope
486
+ requestScope.dispose();
252
487
 
253
- expect(() => root.resolve('ILogger')).toThrow(ContainerDisposedError);
254
- expect(select.instances().resolve(root).length).toBe(0);
488
+ // Scope is now unusable
489
+ expect(() => requestScope.resolve('IDatabase')).toThrow(ContainerDisposedError);
490
+
491
+ // All instances are cleared
492
+ expect(select.instances().resolve(requestScope).length).toBe(0);
493
+
494
+ // Application container is still functional
495
+ expect(appContainer.resolve<DatabaseConnection>('IDatabase')).toBeDefined();
496
+ });
497
+
498
+ it('should clean up request-scoped resources on request end', function () {
499
+ const appContainer = new Container({ tags: ['application'] }).addRegistration(
500
+ R.fromClass(DatabaseConnection).bindTo('IDatabase'),
501
+ );
502
+
503
+ // Simulate Express.js request lifecycle
504
+ function handleRequest(): { connection: DatabaseConnection; scope: Container } {
505
+ const requestScope = appContainer.createScope({ tags: ['request'] }) as Container;
506
+ const connection = requestScope.resolve<DatabaseConnection>('IDatabase');
507
+
508
+ // Do some work...
509
+ connection.query('INSERT INTO sessions VALUES (...)');
510
+
511
+ return { connection, scope: requestScope };
512
+ }
513
+
514
+ // Request 1
515
+ const request1 = handleRequest();
516
+ expect(request1.connection.isClosed).toBe(false);
517
+
518
+ // Request 1 ends - in Express, this would be in res.on('finish')
519
+ request1.connection.close();
520
+ request1.scope.dispose();
521
+
522
+ // Request 2 gets a fresh connection
523
+ const request2 = handleRequest();
524
+ expect(request2.connection.isClosed).toBe(false);
525
+ expect(request2.connection).not.toBe(request1.connection);
526
+
527
+ // Cleanup
528
+ request2.connection.close();
529
+ request2.scope.dispose();
255
530
  });
256
531
  });
257
532
 
@@ -261,93 +536,124 @@ describe('Disposing', function () {
261
536
  Sometimes you want to create dependency only when somebody want to invoke it's method or property. This is what `lazy` is for.
262
537
 
263
538
  ```typescript
539
+ import 'reflect-metadata';
264
540
  import { Container, inject, register, Registration as R, select as s, singleton } from 'ts-ioc-container';
265
541
 
542
+ /**
543
+ * User Management Domain - Lazy Loading
544
+ *
545
+ * Some services are expensive to initialize:
546
+ * - EmailNotifier: Establishes SMTP connection
547
+ * - ReportGenerator: Loads templates, initializes PDF engine
548
+ * - ExternalApiClient: Authenticates with third-party service
549
+ *
550
+ * Lazy loading defers instantiation until first use.
551
+ * This improves startup time and avoids initializing unused services.
552
+ *
553
+ * Use cases:
554
+ * - Services used only in specific code paths (error notification)
555
+ * - Optional features that may not be triggered
556
+ * - Breaking circular dependencies
557
+ */
266
558
  describe('lazy provider', () => {
559
+ // Tracks whether SMTP connection was established
267
560
  @register(singleton())
268
- class Flag {
269
- isSet = false;
561
+ class SmtpConnectionStatus {
562
+ isConnected = false;
270
563
 
271
- set() {
272
- this.isSet = true;
564
+ connect() {
565
+ this.isConnected = true;
273
566
  }
274
567
  }
275
568
 
276
- class Service {
277
- name = 'Service';
278
-
279
- constructor(@inject('Flag') private flag: Flag) {
280
- this.flag.set();
569
+ // EmailNotifier is expensive - establishes SMTP connection on construction
570
+ class EmailNotifier {
571
+ constructor(@inject('SmtpConnectionStatus') private smtp: SmtpConnectionStatus) {
572
+ // Simulate expensive SMTP connection
573
+ this.smtp.connect();
281
574
  }
282
575
 
283
- greet() {
284
- return 'Hello';
576
+ sendPasswordReset(email: string): string {
577
+ return `Password reset sent to ${email}`;
285
578
  }
286
579
  }
287
580
 
288
- class App {
289
- constructor(@inject(s.token('Service').lazy()) public service: Service) {}
581
+ // AuthService might need to send password reset emails
582
+ // But most login requests don't need email (only password reset does)
583
+ class AuthService {
584
+ constructor(@inject(s.token('EmailNotifier').lazy()) public emailNotifier: EmailNotifier) {}
585
+
586
+ login(email: string, password: string): boolean {
587
+ // Most requests just validate credentials - no email needed
588
+ return email === 'admin@example.com' && password === 'secret';
589
+ }
290
590
 
291
- run() {
292
- return this.service.greet();
591
+ requestPasswordReset(email: string): string {
592
+ // Only here do we actually need the EmailNotifier
593
+ return this.emailNotifier.sendPasswordReset(email);
293
594
  }
294
595
  }
295
596
 
296
597
  function createContainer() {
297
598
  const container = new Container();
298
- container.addRegistration(R.fromClass(Flag)).addRegistration(R.fromClass(Service));
599
+ container.addRegistration(R.fromClass(SmtpConnectionStatus)).addRegistration(R.fromClass(EmailNotifier));
299
600
  return container;
300
601
  }
301
602
 
302
- it('should not create an instance until method is not invoked', () => {
303
- // Arrange
603
+ it('should not connect to SMTP until email is actually needed', () => {
304
604
  const container = createContainer();
305
605
 
306
- // Act
307
- container.resolve(App);
308
- const flag = container.resolve<Flag>('Flag');
606
+ // AuthService is created, but EmailNotifier is NOT instantiated yet
607
+ container.resolve(AuthService);
608
+ const smtp = container.resolve<SmtpConnectionStatus>('SmtpConnectionStatus');
309
609
 
310
- // Assert
311
- expect(flag.isSet).toBe(false);
610
+ // SMTP connection was NOT established - lazy loading deferred it
611
+ expect(smtp.isConnected).toBe(false);
312
612
  });
313
613
 
314
- it('should create an instance only when some method/property is invoked', () => {
315
- // Arrange
614
+ it('should connect to SMTP only when sending email', () => {
316
615
  const container = createContainer();
317
616
 
318
- // Act
319
- const app = container.resolve(App);
320
- const flag = container.resolve<Flag>('Flag');
617
+ const authService = container.resolve(AuthService);
618
+ const smtp = container.resolve<SmtpConnectionStatus>('SmtpConnectionStatus');
619
+
620
+ // Trigger password reset - this actually uses EmailNotifier
621
+ const result = authService.requestPasswordReset('user@example.com');
321
622
 
322
- // Assert
323
- expect(app.run()).toBe('Hello');
324
- expect(flag.isSet).toBe(true);
623
+ // Now SMTP connection was established
624
+ expect(result).toBe('Password reset sent to user@example.com');
625
+ expect(smtp.isConnected).toBe(true);
325
626
  });
326
627
 
327
- it('should not create instance on every method invoked', () => {
328
- // Arrange
628
+ it('should only create one instance even with multiple method calls', () => {
329
629
  const container = createContainer();
330
630
 
331
- // Act
332
- const app = container.resolve(App);
631
+ const authService = container.resolve(AuthService);
333
632
 
334
- // Assert
335
- expect(app.run()).toBe('Hello');
336
- expect(app.run()).toBe('Hello');
337
- expect(Array.from(container.getInstances()).filter((x) => x instanceof Service).length).toBe(1);
633
+ // Multiple password resets
634
+ authService.requestPasswordReset('user1@example.com');
635
+ authService.requestPasswordReset('user2@example.com');
636
+
637
+ // Only one EmailNotifier instance was created
638
+ const emailNotifiers = Array.from(container.getInstances()).filter((x) => x instanceof EmailNotifier);
639
+ expect(emailNotifiers.length).toBe(1);
338
640
  });
339
641
 
340
- it('should create instance when property is invoked', () => {
341
- // Arrange
642
+ it('should trigger instantiation when accessing property on lazy object', () => {
342
643
  const container = createContainer();
343
644
 
344
- // Act
345
- const app = container.resolve(App);
346
- const flag = container.resolve<Flag>('Flag');
645
+ const authService = container.resolve(AuthService);
646
+ const smtp = container.resolve<SmtpConnectionStatus>('SmtpConnectionStatus');
647
+
648
+ // Just getting the proxy doesn't trigger instantiation
649
+ const notifier = authService.emailNotifier;
650
+ expect(notifier).toBeDefined();
651
+ expect(smtp.isConnected).toBe(false); // Still lazy!
347
652
 
348
- // Assert
349
- expect(app.service.name).toBe('Service');
350
- expect(flag.isSet).toBe(true);
653
+ // Accessing a property ON the lazy object triggers instantiation
654
+ const method = notifier.sendPasswordReset;
655
+ expect(method).toBeDefined();
656
+ expect(smtp.isConnected).toBe(true); // Now instantiated!
351
657
  });
352
658
  });
353
659
 
@@ -367,26 +673,44 @@ Also you can [inject property.](#inject-property)
367
673
  ```typescript
368
674
  import { Container, inject, Registration as R } from 'ts-ioc-container';
369
675
 
676
+ /**
677
+ * User Management Domain - Metadata Injection
678
+ *
679
+ * The MetadataInjector (default) uses TypeScript decorators and reflect-metadata
680
+ * to automatically inject dependencies into constructor parameters.
681
+ *
682
+ * How it works:
683
+ * 1. @inject('key') decorator marks a parameter for injection
684
+ * 2. Container reads metadata at resolution time
685
+ * 3. Dependencies are resolved and passed to constructor
686
+ *
687
+ * This is the most common pattern in Angular, NestJS, and similar frameworks.
688
+ * Requires: "experimentalDecorators" and "emitDecoratorMetadata" in tsconfig.
689
+ */
690
+
370
691
  class Logger {
371
692
  name = 'Logger';
372
693
  }
373
694
 
374
695
  class App {
696
+ // @inject tells the container which dependency to resolve for this parameter
375
697
  constructor(@inject('ILogger') private logger: Logger) {}
376
698
 
377
- // OR
378
- // constructor(@inject((container, ...args) => container.resolve('ILogger', ...args)) private logger: ILogger) {
379
- // }
699
+ // Alternative: inject via function for dynamic resolution
700
+ // constructor(@inject((container, ...args) => container.resolve('ILogger', ...args)) private logger: ILogger) {}
380
701
 
381
702
  getLoggerName(): string {
382
703
  return this.logger.name;
383
704
  }
384
705
  }
385
706
 
386
- describe('Reflection Injector', function () {
387
- it('should inject dependencies by @inject decorator', function () {
388
- const container = new Container().addRegistration(R.fromClass(Logger).bindToKey('ILogger'));
707
+ describe('Metadata Injector', function () {
708
+ it('should inject dependencies using @inject decorator', function () {
709
+ const container = new Container({ tags: ['application'] }).addRegistration(
710
+ R.fromClass(Logger).bindToKey('ILogger'),
711
+ );
389
712
 
713
+ // Container reads @inject metadata and resolves 'ILogger' for the logger parameter
390
714
  const app = container.resolve(App);
391
715
 
392
716
  expect(app.getLoggerName()).toBe('Logger');
@@ -401,34 +725,83 @@ This type of injector just passes container to constructor with others arguments
401
725
  ```typescript
402
726
  import { Container, type IContainer, Registration as R, SimpleInjector } from 'ts-ioc-container';
403
727
 
728
+ /**
729
+ * Command Pattern - Simple Injector
730
+ *
731
+ * The SimpleInjector passes the container itself as the first argument to the constructor.
732
+ * This is useful for:
733
+ * - Service Locators (like Command Dispatchers or Routers)
734
+ * - Factory classes that need to resolve dependencies dynamically
735
+ * - Legacy code migration where passing the container is common
736
+ *
737
+ * In this example, a CommandDispatcher uses the container to dynamically
738
+ * resolve the correct handler for each command type.
739
+ */
740
+
741
+ interface ICommand {
742
+ type: string;
743
+ }
744
+
745
+ interface ICommandHandler {
746
+ handle(command: ICommand): string;
747
+ }
748
+
749
+ class CreateUserCommand implements ICommand {
750
+ readonly type = 'CreateUser';
751
+ constructor(readonly username: string) {}
752
+ }
753
+
754
+ class CreateUserHandler implements ICommandHandler {
755
+ handle(command: CreateUserCommand): string {
756
+ return `User ${command.username} created`;
757
+ }
758
+ }
759
+
404
760
  describe('SimpleInjector', function () {
405
- it('should pass container as first parameter', function () {
406
- class App {
407
- constructor(public container: IContainer) {}
761
+ it('should inject container to allow dynamic resolution (Service Locator pattern)', function () {
762
+ // Dispatcher needs the container to find handlers dynamically based on command type
763
+ class CommandDispatcher {
764
+ constructor(private container: IContainer) {}
765
+
766
+ dispatch(command: ICommand): string {
767
+ // Dynamically resolve handler: "Handler" + "CreateUser"
768
+ const handlerKey = `Handler${command.type}`;
769
+ const handler = this.container.resolve<ICommandHandler>(handlerKey);
770
+ return handler.handle(command);
771
+ }
408
772
  }
409
773
 
410
- const container = new Container({ injector: new SimpleInjector() }).addRegistration(
411
- R.fromClass(App).bindToKey('App'),
412
- );
413
- const app = container.resolve<App>('App');
774
+ const container = new Container({ injector: new SimpleInjector() })
775
+ .addRegistration(R.fromClass(CommandDispatcher).bindToKey('Dispatcher'))
776
+ .addRegistration(R.fromClass(CreateUserHandler).bindToKey('HandlerCreateUser'));
777
+
778
+ const dispatcher = container.resolve<CommandDispatcher>('Dispatcher');
779
+ const result = dispatcher.dispatch(new CreateUserCommand('alice'));
414
780
 
415
- expect(app.container).toBeInstanceOf(Container);
781
+ expect(result).toBe('User alice created');
416
782
  });
417
783
 
418
- it('should pass parameters alongside with container', function () {
419
- class App {
784
+ it('should pass additional arguments alongside the container', function () {
785
+ // Factory that creates widgets with a specific theme
786
+ class WidgetFactory {
420
787
  constructor(
421
- container: IContainer,
422
- public greeting: string,
788
+ private container: IContainer,
789
+ private theme: string, // Passed as argument during resolve
423
790
  ) {}
791
+
792
+ createWidget(name: string): string {
793
+ return `Widget ${name} with ${this.theme} theme (Container available: ${!!this.container})`;
794
+ }
424
795
  }
425
796
 
426
797
  const container = new Container({ injector: new SimpleInjector() }).addRegistration(
427
- R.fromClass(App).bindToKey('App'),
798
+ R.fromClass(WidgetFactory).bindToKey('WidgetFactory'),
428
799
  );
429
- const app = container.resolve<App>('App', { args: ['Hello world'] });
430
800
 
431
- expect(app.greeting).toBe('Hello world');
801
+ // Pass "dark" as the theme argument
802
+ const factory = container.resolve<WidgetFactory>('WidgetFactory', { args: ['dark'] });
803
+
804
+ expect(factory.createWidget('Button')).toBe('Widget Button with dark theme (Container available: true)');
432
805
  });
433
806
  });
434
807
 
@@ -438,89 +811,117 @@ describe('SimpleInjector', function () {
438
811
  This type of injector injects dependencies as dictionary `Record<string, unknown>`.
439
812
 
440
813
  ```typescript
441
- import { args, Container, ProxyInjector, Registration as R } from 'ts-ioc-container';
814
+ import { Container, ProxyInjector, Registration as R } from 'ts-ioc-container';
815
+
816
+ /**
817
+ * Clean Architecture - Proxy Injector
818
+ *
819
+ * The ProxyInjector injects dependencies as a single object (props/options pattern).
820
+ * This is popular in modern JavaScript/TypeScript (like React props or destructuring).
821
+ *
822
+ * Advantages:
823
+ * - Named parameters are more readable than positional arguments
824
+ * - Order of arguments doesn't matter
825
+ * - Easy to add/remove dependencies without breaking inheritance chains
826
+ * - Works well with "Clean Architecture" adapters
827
+ */
442
828
 
443
829
  describe('ProxyInjector', function () {
444
- it('should pass dependency to constructor as dictionary', function () {
445
- class Logger {}
830
+ it('should inject dependencies as a props object', function () {
831
+ class Logger {
832
+ log(msg: string) {
833
+ return `Logged: ${msg}`;
834
+ }
835
+ }
446
836
 
447
- class App {
837
+ // Dependencies defined as an interface
838
+ interface UserControllerDeps {
448
839
  logger: Logger;
840
+ prefix: string;
841
+ }
842
+
843
+ // Controller receives all dependencies in a single object
844
+ class UserController {
845
+ private logger: Logger;
846
+ private prefix: string;
449
847
 
450
- constructor({ logger }: { logger: Logger }) {
848
+ constructor({ logger, prefix }: UserControllerDeps) {
451
849
  this.logger = logger;
850
+ this.prefix = prefix;
851
+ }
852
+
853
+ createUser(name: string): string {
854
+ return this.logger.log(`${this.prefix} ${name}`);
452
855
  }
453
856
  }
454
857
 
455
- const container = new Container({ injector: new ProxyInjector() }).addRegistration(
456
- R.fromClass(Logger).bindToKey('logger'),
457
- );
858
+ const container = new Container({ injector: new ProxyInjector() })
859
+ .addRegistration(R.fromClass(Logger).bindToKey('logger'))
860
+ .addRegistration(R.fromValue('USER:').bindToKey('prefix'))
861
+ .addRegistration(R.fromClass(UserController).bindToKey('UserController'));
458
862
 
459
- const app = container.resolve(App);
460
- expect(app.logger).toBeInstanceOf(Logger);
863
+ const controller = container.resolve<UserController>('UserController');
864
+
865
+ expect(controller.createUser('bob')).toBe('Logged: USER: bob');
461
866
  });
462
867
 
463
- it('should pass arguments as objects', function () {
464
- class Logger {}
868
+ it('should support mixing injected dependencies with runtime arguments', function () {
869
+ class Database {}
465
870
 
466
- class App {
467
- logger: Logger;
468
- greeting: string;
469
-
470
- constructor({
471
- logger,
472
- greetingTemplate,
473
- name,
474
- }: {
475
- logger: Logger;
476
- greetingTemplate: (name: string) => string;
477
- name: string;
478
- }) {
479
- this.logger = logger;
480
- this.greeting = greetingTemplate(name);
481
- }
871
+ interface ReportGeneratorDeps {
872
+ database: Database;
873
+ format: string; // Runtime argument
482
874
  }
483
875
 
484
- const greetingTemplate = (name: string) => `Hello ${name}`;
876
+ class ReportGenerator {
877
+ constructor(public deps: ReportGeneratorDeps) {}
878
+
879
+ generate(): string {
880
+ return `Report in ${this.deps.format}`;
881
+ }
882
+ }
485
883
 
486
884
  const container = new Container({ injector: new ProxyInjector() })
487
- .addRegistration(R.fromClass(App).bindToKey('App').pipe(args({ greetingTemplate })))
488
- .addRegistration(R.fromClass(Logger).bindToKey('logger'));
885
+ .addRegistration(R.fromClass(Database).bindToKey('database'))
886
+ .addRegistration(R.fromClass(ReportGenerator).bindToKey('ReportGenerator'));
887
+
888
+ // "format" is passed at resolution time
889
+ const generator = container.resolve<ReportGenerator>('ReportGenerator', {
890
+ args: [{ format: 'PDF' }],
891
+ });
489
892
 
490
- const app = container.resolve<App>('App', { args: [{ name: `world` }] });
491
- expect(app.greeting).toBe('Hello world');
893
+ expect(generator.deps.database).toBeInstanceOf(Database);
894
+ expect(generator.generate()).toBe('Report in PDF');
492
895
  });
493
896
 
494
- it('should resolve array dependencies when property name contains "array"', function () {
495
- class Logger {}
496
- class Service {}
897
+ it('should resolve array dependencies by alias (convention over configuration)', function () {
898
+ // If a property is named "loggersArray", it looks for alias "loggersArray"
899
+ // and resolves it as an array of all matches.
497
900
 
498
- class App {
499
- loggers: Logger[];
500
- service: Service;
901
+ class FileLogger {}
902
+ class ConsoleLogger {}
501
903
 
502
- constructor({ loggersArray, service }: { loggersArray: Logger[]; service: Service }) {
503
- this.loggers = loggersArray;
504
- this.service = service;
505
- }
904
+ interface AppDeps {
905
+ loggersArray: any[]; // Injected as array of all loggers
506
906
  }
507
907
 
508
- // Mock container's resolveByAlias to return an array with a Logger instance
509
- const mockLogger = new Logger();
510
- const mockContainer = new Container({ injector: new ProxyInjector() });
511
- mockContainer.resolveByAlias = jest.fn().mockImplementation((key) => {
512
- // Always return the mock array for simplicity
513
- return [mockLogger];
514
- });
515
- mockContainer.addRegistration(R.fromClass(Service).bindToKey('service'));
516
-
517
- const app = mockContainer.resolve(App);
518
- expect(app.loggers).toBeInstanceOf(Array);
519
- expect(app.loggers.length).toBe(1);
520
- expect(app.loggers[0]).toBe(mockLogger);
521
- expect(app.service).toBeInstanceOf(Service);
522
- // Verify that resolveByAlias was called with the correct key
523
- expect(mockContainer.resolveByAlias).toHaveBeenCalledWith('loggersArray');
908
+ class App {
909
+ constructor(public deps: AppDeps) {}
910
+ }
911
+
912
+ const container = new Container({ injector: new ProxyInjector() });
913
+
914
+ // Mocking the behavior for this specific test as ProxyInjector uses resolveByAlias
915
+ // which delegates to the container.
916
+ // In a real scenario, you'd register multiple loggers with the same alias.
917
+ const mockLoggers = [new FileLogger(), new ConsoleLogger()];
918
+
919
+ container.resolveByAlias = jest.fn().mockReturnValue(mockLoggers);
920
+
921
+ const app = container.resolve(App);
922
+
923
+ expect(app.deps.loggersArray).toBe(mockLoggers);
924
+ expect(container.resolveByAlias).toHaveBeenCalledWith('loggersArray');
524
925
  });
525
926
  });
526
927
 
@@ -539,270 +940,138 @@ import {
539
940
  argsFn,
540
941
  bindTo,
541
942
  Container,
542
- inject,
543
943
  lazy,
544
944
  Provider,
545
945
  register,
546
946
  Registration as R,
547
947
  scopeAccess,
548
- select as s,
549
948
  singleton,
550
949
  } from 'ts-ioc-container';
551
950
 
552
- class Logger {}
553
-
554
- class ConfigService {
555
- constructor(private readonly configPath: string) {}
556
-
557
- getPath(): string {
558
- return this.configPath;
559
- }
560
- }
561
-
562
- class UserService {}
951
+ /**
952
+ * Data Processing Pipeline - Provider Patterns
953
+ *
954
+ * Providers are the recipes for creating objects. This suite demonstrates
955
+ * how to customize object creation for a Data Processing Pipeline.
956
+ *
957
+ * Scenarios:
958
+ * - FileProcessor: Created as a class instance
959
+ * - Config: Created from a simple value object
960
+ * - BatchProcessor: Singleton to coordinate across the app
961
+ * - StreamProcessor: Lazy loaded only when needed
962
+ */
563
963
 
564
- class TestClass {}
565
-
566
- class ClassWithoutTransformers {}
964
+ class Logger {}
567
965
 
568
966
  describe('Provider', () => {
569
- it('can be registered as a function', () => {
967
+ it('can be registered as a function (Factory Pattern)', () => {
968
+ // dynamic factory
570
969
  const container = new Container().register('ILogger', new Provider(() => new Logger()));
571
970
  expect(container.resolve('ILogger')).not.toBe(container.resolve('ILogger'));
572
971
  });
573
972
 
574
- it('can be registered as a value', () => {
575
- const container = new Container().register('ILogger', Provider.fromValue(new Logger()));
576
- expect(container.resolve('ILogger')).toBe(container.resolve('ILogger'));
973
+ it('can be registered as a value (Config Pattern)', () => {
974
+ // constant value
975
+ const config = { maxRetries: 3 };
976
+ const container = new Container().register('Config', Provider.fromValue(config));
977
+ expect(container.resolve('Config')).toBe(config);
577
978
  });
578
979
 
579
- it('can be registered as a class', () => {
980
+ it('can be registered as a class (Standard Pattern)', () => {
580
981
  const container = new Container().register('ILogger', Provider.fromClass(Logger));
581
- expect(container.resolve('ILogger')).not.toBe(container.resolve('ILogger'));
982
+ expect(container.resolve('ILogger')).toBeInstanceOf(Logger);
582
983
  });
583
984
 
584
- it('can be featured by pipe method', () => {
585
- const root = new Container({ tags: ['root'] }).register('ILogger', Provider.fromClass(Logger).pipe(singleton()));
586
- expect(root.resolve('ILogger')).toBe(root.resolve('ILogger'));
985
+ it('can be featured by fp method (Singleton Pattern)', () => {
986
+ // Pipe "singleton()" to cache the instance
987
+ const appContainer = new Container({ tags: ['application'] }).register(
988
+ 'SharedLogger',
989
+ Provider.fromClass(Logger).pipe(singleton()),
990
+ );
991
+ expect(appContainer.resolve('SharedLogger')).toBe(appContainer.resolve('SharedLogger'));
587
992
  });
588
993
 
589
- it('can be created from a dependency key', () => {
994
+ it('can be created from a dependency key (Alias/Redirect Pattern)', () => {
995
+ // "LoggerAlias" redirects to "ILogger"
590
996
  const container = new Container()
591
997
  .register('ILogger', Provider.fromClass(Logger))
592
998
  .register('LoggerAlias', Provider.fromKey('ILogger'));
593
- const logger1 = container.resolve('ILogger');
594
- const logger2 = container.resolve('LoggerAlias');
595
- expect(logger2).toBeInstanceOf(Logger);
596
- expect(logger2).not.toBe(logger1);
999
+
1000
+ const logger = container.resolve('LoggerAlias');
1001
+ expect(logger).toBeInstanceOf(Logger);
597
1002
  });
598
1003
 
599
- it('supports lazy resolution', () => {
1004
+ it('supports lazy resolution (Performance Optimization)', () => {
1005
+ // Logger is not created until accessed
600
1006
  const container = new Container().register('ILogger', Provider.fromClass(Logger));
601
1007
  const lazyLogger = container.resolve('ILogger', { lazy: true });
1008
+
1009
+ // It's a proxy, not the real instance yet
602
1010
  expect(typeof lazyLogger).toBe('object');
603
- const loggerPrototype = Object.getPrototypeOf(lazyLogger);
604
- expect(loggerPrototype).toBeDefined();
1011
+ // Accessing it would trigger creation
605
1012
  });
606
1013
 
607
1014
  it('supports args decorator for providing extra arguments', () => {
608
- const container = new Container().register(
609
- 'ConfigService',
610
- Provider.fromClass(ConfigService).pipe(args('/etc/config.json')),
611
- );
612
- const config = container.resolve<ConfigService>('ConfigService');
613
- expect(config.getPath()).toBe('/etc/config.json');
614
- });
1015
+ class FileService {
1016
+ constructor(readonly basePath: string) {}
1017
+ }
615
1018
 
616
- it('supports argsFn decorator for dynamic arguments', () => {
617
- const container = new Container()
618
- .register('Logger', Provider.fromClass(Logger))
619
- .register(
620
- 'ConfigService',
621
- Provider.fromClass(ConfigService).pipe(argsFn((container) => ['/dynamic/config.json'])),
622
- );
623
- const config = container.resolve<ConfigService>('ConfigService');
624
- expect(config.getPath()).toBe('/dynamic/config.json');
625
- });
1019
+ const container = new Container().register('FileService', Provider.fromClass(FileService).pipe(args('/var/data')));
626
1020
 
627
- it('combines args from argsFn with manually provided args', () => {
628
- const container = new Container()
629
- .register('Logger', Provider.fromClass(Logger))
630
- .register(
631
- 'UserService',
632
- Provider.fromClass(UserService).pipe(argsFn((container) => [container.resolve('Logger')])),
633
- );
634
- const configService = new ConfigService('/test/config.json');
635
- const userService = container.resolve<UserService>('UserService', { args: [configService] });
636
- expect(userService).toBeInstanceOf(UserService);
1021
+ const service = container.resolve<FileService>('FileService');
1022
+ expect(service.basePath).toBe('/var/data');
637
1023
  });
638
1024
 
639
- it('supports visibility control between parent and child containers', () => {
640
- const rootContainer = new Container({ tags: ['root'] }).register(
641
- 'ILogger',
642
- Provider.fromClass(Logger).pipe(
643
- scopeAccess(({ invocationScope, providerScope }) => invocationScope.hasTag('admin')),
644
- ),
645
- );
646
- const adminChild = rootContainer.createScope({ tags: ['admin'] });
647
- const userChild = rootContainer.createScope({ tags: ['user'] });
648
- expect(() => adminChild.resolve('ILogger')).not.toThrow();
649
- expect(() => userChild.resolve('ILogger')).toThrow();
650
- });
1025
+ it('supports argsFn decorator for dynamic arguments', () => {
1026
+ class Database {
1027
+ constructor(readonly connectionString: string) {}
1028
+ }
651
1029
 
652
- it('supports chaining multiple pipe transformations', () => {
653
- const container = new Container().register(
654
- 'ConfigService',
655
- Provider.fromClass(ConfigService).pipe(args('/default/config.json'), singleton()),
1030
+ const container = new Container().register('DbPath', Provider.fromValue('localhost:5432')).register(
1031
+ 'Database',
1032
+ Provider.fromClass(Database).pipe(
1033
+ // Dynamically resolve connection string at creation time
1034
+ argsFn((scope) => [`postgres://${scope.resolve('DbPath')}`]),
1035
+ ),
656
1036
  );
657
- const config1 = container.resolve<ConfigService>('ConfigService');
658
- const config2 = container.resolve<ConfigService>('ConfigService');
659
- expect(config1).toBe(config2);
660
- expect(config1.getPath()).toBe('/default/config.json');
661
- });
662
-
663
- it('applies transformers when registering a class constructor as a value', () => {
664
- const container = new Container()
665
- .register('ClassConstructor', Provider.fromValue(TestClass))
666
- .register('ClassInstance', Provider.fromClass(TestClass));
667
- const instance1 = container.resolve('ClassConstructor');
668
- const instance2 = container.resolve('ClassConstructor');
669
- const instance3 = container.resolve('ClassInstance');
670
- expect(instance1).toBe(TestClass);
671
- expect(instance2).toBe(TestClass);
672
- expect(instance3).toBeInstanceOf(TestClass);
673
- });
674
1037
 
675
- it('handles primitive values in Provider.fromValue', () => {
676
- const container = new Container()
677
- .register('StringValue', Provider.fromValue('test-string'))
678
- .register('NumberValue', Provider.fromValue(42))
679
- .register('BooleanValue', Provider.fromValue(true))
680
- .register('ObjectValue', Provider.fromValue({ key: 'value' }));
681
- expect(container.resolve('StringValue')).toBe('test-string');
682
- expect(container.resolve('NumberValue')).toBe(42);
683
- expect(container.resolve('BooleanValue')).toBe(true);
684
- expect(container.resolve('ObjectValue')).toEqual({ key: 'value' });
1038
+ const db = container.resolve<Database>('Database');
1039
+ expect(db.connectionString).toBe('postgres://localhost:5432');
685
1040
  });
686
1041
 
687
- it('resolves dependencies with empty args', () => {
688
- const container = new Container().register('Logger', Provider.fromClass(Logger));
689
- const logger = container.resolve('Logger', { args: [] });
690
- expect(logger).toBeInstanceOf(Logger);
691
- });
1042
+ it('supports visibility control (Security Pattern)', () => {
1043
+ // AdminService only visible in admin scope
1044
+ class AdminService {}
692
1045
 
693
- it('allows direct manipulation of visibility predicate', () => {
694
- const provider = Provider.fromClass(Logger);
695
- provider.setAccessRule(({ invocationScope }) => invocationScope.hasTag('special'));
696
- const container = new Container({ tags: ['root'] }).register('Logger', provider);
697
- const specialChild = container.createScope({ tags: ['special'] });
698
- const regularChild = container.createScope({ tags: ['regular'] });
699
- expect(() => specialChild.resolve('Logger')).not.toThrow();
700
- expect(() => regularChild.resolve('Logger')).toThrow();
701
- });
1046
+ const appContainer = new Container({ tags: ['application'] }).register(
1047
+ 'AdminService',
1048
+ Provider.fromClass(AdminService).pipe(scopeAccess(({ invocationScope }) => invocationScope.hasTag('admin'))),
1049
+ );
702
1050
 
703
- it('allows direct manipulation of args function', () => {
704
- const provider = Provider.fromClass(ConfigService);
705
- provider.setArgs(() => ['/custom/path.json']);
706
- const container = new Container().register('ConfigService', provider);
707
- const config = container.resolve<ConfigService>('ConfigService');
708
- expect(config.getPath()).toBe('/custom/path.json');
709
- });
1051
+ const adminScope = appContainer.createScope({ tags: ['admin'] });
1052
+ const publicScope = appContainer.createScope({ tags: ['public'] });
710
1053
 
711
- it('handles class constructors when getTransformers returns null', () => {
712
- const container = new Container().register('NoTransformers', Provider.fromValue(ClassWithoutTransformers));
713
- const result = container.resolve('NoTransformers');
714
- expect(result).toBe(ClassWithoutTransformers);
1054
+ expect(() => adminScope.resolve('AdminService')).not.toThrow();
1055
+ expect(() => publicScope.resolve('AdminService')).toThrow();
715
1056
  });
716
1057
 
717
- it('allows to register lazy provider', () => {
718
- let isLoggerCreated = false;
719
-
720
- @register(bindTo('Logger'), lazy())
721
- class Logger {
722
- private logs: string[] = [];
1058
+ it('allows to register lazy provider via decorator', () => {
1059
+ let created = false;
723
1060
 
1061
+ @register(bindTo('HeavyService'), lazy())
1062
+ class HeavyService {
724
1063
  constructor() {
725
- isLoggerCreated = true;
726
- }
727
-
728
- info(message: string, context: Record<string, unknown>): void {
729
- this.logs.push(JSON.stringify({ ...context, level: 'info', message }));
730
- }
731
-
732
- serialize(): string {
733
- return this.logs.join('\n');
1064
+ created = true;
734
1065
  }
1066
+ doWork() {}
735
1067
  }
736
1068
 
737
- class Main {
738
- constructor(@inject('Logger') private logger: Logger) {}
1069
+ const container = new Container().addRegistration(R.fromClass(HeavyService));
1070
+ const service = container.resolve<HeavyService>('HeavyService');
739
1071
 
740
- getLogs(): string {
741
- return this.logger.serialize();
742
- }
743
- }
744
-
745
- const root = new Container({ tags: ['root'] }).addRegistration(R.fromClass(Logger));
746
- const main = root.resolve(Main);
747
-
748
- expect(isLoggerCreated).toBe(false);
749
-
750
- main.getLogs();
751
-
752
- expect(isLoggerCreated).toBe(true);
753
- });
754
-
755
- it('allows to resolve with args', () => {
756
- @register(bindTo('ILogger'))
757
- class Logger {
758
- readonly channel: string;
759
-
760
- constructor(options: { channel: string }) {
761
- this.channel = options.channel;
762
- }
763
- }
764
-
765
- class Main {
766
- constructor(@inject(s.token('ILogger').args({ channel: 'file' })) private logger: Logger) {}
767
-
768
- getChannel(): string {
769
- return this.logger.channel;
770
- }
771
- }
772
-
773
- const root = new Container({ tags: ['root'] }).addRegistration(R.fromClass(Logger));
774
- const main = root.resolve(Main);
775
-
776
- expect(main.getChannel()).toBe('file');
777
- });
778
-
779
- it('allows to resolve with argsFn', () => {
780
- @register(bindTo('ILogger'))
781
- class Logger {
782
- readonly channel: string;
783
-
784
- constructor(options: { channel: string }) {
785
- this.channel = options.channel;
786
- }
787
- }
788
-
789
- class Main {
790
- constructor(
791
- @inject(s.token('ILogger').argsFn((s) => [{ channel: s.resolve('channel') }])) private logger: Logger,
792
- ) {}
793
-
794
- getChannel(): string {
795
- return this.logger.channel;
796
- }
797
- }
798
-
799
- const root = new Container({ tags: ['root'] })
800
- .addRegistration(R.fromValue('file').bindToKey('channel'))
801
- .addRegistration(R.fromClass(Logger));
802
-
803
- const main = root.resolve(Main);
804
-
805
- expect(main.getChannel()).toBe('file');
1072
+ expect(created).toBe(false); // Not created yet
1073
+ service.doWork(); // Access triggers creation
1074
+ expect(created).toBe(true);
806
1075
  });
807
1076
  });
808
1077
 
@@ -815,34 +1084,84 @@ Sometimes you need to create only one instance of dependency per scope. For exam
815
1084
  - NOTICE: if you create a scope 'A' of container 'root' then Logger of A !== Logger of root.
816
1085
 
817
1086
  ```typescript
1087
+ import 'reflect-metadata';
818
1088
  import { bindTo, Container, register, Registration as R, singleton } from 'ts-ioc-container';
819
1089
 
820
- @register(bindTo('logger'), singleton())
821
- class Logger {}
1090
+ /**
1091
+ * User Management Domain - Singleton Pattern
1092
+ *
1093
+ * Singletons are services that should only have one instance per scope.
1094
+ * Common examples:
1095
+ * - PasswordHasher: Expensive to initialize (loads crypto config)
1096
+ * - DatabasePool: Connection pool shared across requests
1097
+ * - ConfigService: Application configuration loaded once
1098
+ *
1099
+ * Note: "singleton" in ts-ioc-container means "one instance per scope",
1100
+ * not "one instance globally". Each scope gets its own singleton instance.
1101
+ */
1102
+
1103
+ // PasswordHasher is expensive to create - should be singleton
1104
+ @register(bindTo('IPasswordHasher'), singleton())
1105
+ class PasswordHasher {
1106
+ private readonly salt: string;
1107
+
1108
+ constructor() {
1109
+ // Simulate expensive initialization (loading crypto config, etc.)
1110
+ this.salt = 'random_salt_' + Math.random().toString(36);
1111
+ }
1112
+
1113
+ hash(password: string): string {
1114
+ return `hashed_${password}_${this.salt}`;
1115
+ }
1116
+
1117
+ verify(password: string, hash: string): boolean {
1118
+ return this.hash(password) === hash;
1119
+ }
1120
+ }
822
1121
 
823
1122
  describe('Singleton', function () {
824
- function createContainer() {
825
- return new Container();
1123
+ function createAppContainer() {
1124
+ return new Container({ tags: ['application'] });
826
1125
  }
827
1126
 
828
- it('should resolve the same container per every request', function () {
829
- const container = createContainer().addRegistration(R.fromClass(Logger));
1127
+ it('should resolve the same PasswordHasher for every request in same scope', function () {
1128
+ const appContainer = createAppContainer().addRegistration(R.fromClass(PasswordHasher));
830
1129
 
831
- expect(container.resolve('logger')).toBe(container.resolve('logger'));
1130
+ // Multiple resolves return the same instance
1131
+ const hasher1 = appContainer.resolve<PasswordHasher>('IPasswordHasher');
1132
+ const hasher2 = appContainer.resolve<PasswordHasher>('IPasswordHasher');
1133
+
1134
+ expect(hasher1).toBe(hasher2);
1135
+ expect(hasher1.hash('password')).toBe(hasher2.hash('password'));
832
1136
  });
833
1137
 
834
- it('should resolve different dependency per scope', function () {
835
- const container = createContainer().addRegistration(R.fromClass(Logger));
836
- const child = container.createScope();
1138
+ it('should create different singleton per request scope', function () {
1139
+ // Application-level singleton
1140
+ const appContainer = createAppContainer().addRegistration(R.fromClass(PasswordHasher));
1141
+
1142
+ // Each request scope gets its own singleton instance
1143
+ // This is useful when you want per-request caching
1144
+ const request1 = appContainer.createScope({ tags: ['request'] });
1145
+ const request2 = appContainer.createScope({ tags: ['request'] });
1146
+
1147
+ const appHasher = appContainer.resolve<PasswordHasher>('IPasswordHasher');
1148
+ const request1Hasher = request1.resolve<PasswordHasher>('IPasswordHasher');
1149
+ const request2Hasher = request2.resolve<PasswordHasher>('IPasswordHasher');
837
1150
 
838
- expect(container.resolve('logger')).not.toBe(child.resolve('logger'));
1151
+ // Each scope has its own instance
1152
+ expect(appHasher).not.toBe(request1Hasher);
1153
+ expect(request1Hasher).not.toBe(request2Hasher);
839
1154
  });
840
1155
 
841
- it('should resolve the same dependency for scope', function () {
842
- const container = createContainer().addRegistration(R.fromClass(Logger));
843
- const child = container.createScope();
1156
+ it('should maintain singleton within a scope', function () {
1157
+ const appContainer = createAppContainer().addRegistration(R.fromClass(PasswordHasher));
1158
+ const requestScope = appContainer.createScope({ tags: ['request'] });
844
1159
 
845
- expect(child.resolve('logger')).toBe(child.resolve('logger'));
1160
+ // Within the same scope, singleton is maintained
1161
+ const hasher1 = requestScope.resolve<PasswordHasher>('IPasswordHasher');
1162
+ const hasher2 = requestScope.resolve<PasswordHasher>('IPasswordHasher');
1163
+
1164
+ expect(hasher1).toBe(hasher2);
846
1165
  });
847
1166
  });
848
1167
 
@@ -861,146 +1180,163 @@ import {
861
1180
  argsFn,
862
1181
  bindTo,
863
1182
  Container,
864
- SingleToken,
865
1183
  inject,
866
1184
  MultiCache,
867
1185
  register,
868
1186
  Registration as R,
869
1187
  resolveByArgs,
870
1188
  singleton,
1189
+ SingleToken,
871
1190
  } from 'ts-ioc-container';
872
1191
 
873
- @register(bindTo('logger'))
874
- class Logger {
875
- constructor(
876
- public name: string,
877
- public type?: string,
878
- ) {}
879
- }
1192
+ /**
1193
+ * Advanced - Arguments Provider
1194
+ *
1195
+ * You can inject arguments into providers at registration time or resolution time.
1196
+ * This is powerful for:
1197
+ * - Configuration injection
1198
+ * - Factory patterns
1199
+ * - Generic classes (like Repositories) that need to know what they are managing
1200
+ */
880
1201
 
881
1202
  describe('ArgsProvider', function () {
882
1203
  function createContainer() {
883
1204
  return new Container();
884
1205
  }
885
1206
 
886
- it('can assign argument function to provider', function () {
887
- const root = createContainer().addRegistration(R.fromClass(Logger).pipe(argsFn(() => ['name'])));
1207
+ describe('Static Arguments', () => {
1208
+ it('can pass static arguments to constructor', function () {
1209
+ class FileLogger {
1210
+ constructor(public filename: string) {}
1211
+ }
888
1212
 
889
- const logger = root.createScope().resolve<Logger>('logger');
890
- expect(logger.name).toBe('name');
891
- });
1213
+ // Pre-configure the logger with a filename
1214
+ const root = createContainer().addRegistration(R.fromClass(FileLogger).pipe(args('/var/log/app.log')));
1215
+
1216
+ // Resolve by class name (default key) to use the registered provider
1217
+ const logger = root.resolve<FileLogger>('FileLogger');
1218
+ expect(logger.filename).toBe('/var/log/app.log');
1219
+ });
1220
+
1221
+ it('prioritizes provided args over resolve args', function () {
1222
+ class Logger {
1223
+ constructor(public context: string) {}
1224
+ }
892
1225
 
893
- it('can assign argument to provider', function () {
894
- const root = createContainer().addRegistration(R.fromClass(Logger).pipe(args('name')));
1226
+ // 'FixedContext' wins over any runtime args
1227
+ const root = createContainer().addRegistration(R.fromClass(Logger).pipe(args('FixedContext')));
895
1228
 
896
- const logger = root.resolve<Logger>('logger');
897
- expect(logger.name).toBe('name');
1229
+ // Even if we ask for 'RuntimeContext', we get 'FixedContext'
1230
+ // Resolve by class name to use the registered provider
1231
+ const logger = root.resolve<Logger>('Logger', { args: ['RuntimeContext'] });
1232
+
1233
+ expect(logger.context).toBe('FixedContext');
1234
+ });
898
1235
  });
899
1236
 
900
- it('should set provider arguments with highest priority in compare to resolve arguments', function () {
901
- const root = createContainer().addRegistration(R.fromClass(Logger).pipe(args('name')));
1237
+ describe('Dynamic Arguments (Factory)', () => {
1238
+ it('can resolve arguments dynamically from container', function () {
1239
+ class Config {
1240
+ env = 'production';
1241
+ }
902
1242
 
903
- const logger = root.resolve<Logger>('logger', { args: ['file'] });
1243
+ class Service {
1244
+ constructor(public env: string) {}
1245
+ }
904
1246
 
905
- expect(logger.name).toBe('name');
906
- expect(logger.type).toBe('file');
1247
+ const root = createContainer()
1248
+ .addRegistration(R.fromClass(Config)) // Key: 'Config'
1249
+ .addRegistration(
1250
+ R.fromClass(Service).pipe(
1251
+ // Extract 'env' from Config service dynamically
1252
+ // Note: We resolve 'Config' by string key to get the registered instance (if it were singleton)
1253
+ argsFn((scope) => [scope.resolve<Config>('Config').env]),
1254
+ ),
1255
+ );
1256
+
1257
+ const service = root.resolve<Service>('Service');
1258
+ expect(service.env).toBe('production');
1259
+ });
907
1260
  });
908
1261
 
909
- it('should resolve dependency by passing arguments resolve from container by another argument', function () {
1262
+ describe('Generic Repositories (Advanced Pattern)', () => {
1263
+ // This example demonstrates how to implement the Generic Repository pattern
1264
+ // where a generic EntityManager needs to know WHICH repository to use.
1265
+
910
1266
  interface IRepository {
911
1267
  name: string;
912
1268
  }
913
1269
 
914
- const IUserRepositoryKey = new SingleToken<IRepository>('IUserRepository');
915
- const ITodoRepositoryKey = new SingleToken<IRepository>('ITodoRepository');
1270
+ // Tokens for specific repository types
1271
+ const UserRepositoryToken = new SingleToken<IRepository>('UserRepository');
1272
+ const TodoRepositoryToken = new SingleToken<IRepository>('TodoRepository');
916
1273
 
917
- @register(bindTo(IUserRepositoryKey))
1274
+ @register(bindTo(UserRepositoryToken))
918
1275
  class UserRepository implements IRepository {
919
1276
  name = 'UserRepository';
920
1277
  }
921
1278
 
922
- @register(bindTo(ITodoRepositoryKey))
1279
+ @register(bindTo(TodoRepositoryToken))
923
1280
  class TodoRepository implements IRepository {
924
1281
  name = 'TodoRepository';
925
1282
  }
926
1283
 
927
- interface IEntityManager {
928
- repository: IRepository;
929
- }
930
-
931
- const IEntityManagerKey = new SingleToken<IEntityManager>('IEntityManager');
1284
+ // EntityManager is generic - it works with ANY repository
1285
+ // We use argsFn(resolveByArgs) to tell it to look at the arguments passed to .args()
1286
+ const EntityManagerToken = new SingleToken<EntityManager>('EntityManager');
932
1287
 
933
- @register(bindTo(IEntityManagerKey), argsFn(resolveByArgs))
1288
+ @register(
1289
+ bindTo(EntityManagerToken),
1290
+ argsFn(resolveByArgs), // <--- Key magic: resolves dependencies based on arguments passed to token
1291
+ singleton(MultiCache.fromFirstArg), // Cache unique instance per repository type
1292
+ )
934
1293
  class EntityManager {
935
1294
  constructor(public repository: IRepository) {}
936
1295
  }
937
1296
 
938
- class Main {
1297
+ class App {
939
1298
  constructor(
940
- @inject(IEntityManagerKey.args(IUserRepositoryKey)) public userEntities: EntityManager,
941
- @inject(IEntityManagerKey.args(ITodoRepositoryKey)) public todoEntities: EntityManager,
942
- ) {}
943
- }
944
-
945
- const root = createContainer()
946
- .addRegistration(R.fromClass(EntityManager))
947
- .addRegistration(R.fromClass(UserRepository))
948
- .addRegistration(R.fromClass(TodoRepository));
949
- const main = root.resolve(Main);
950
-
951
- expect(main.userEntities.repository).toBeInstanceOf(UserRepository);
952
- expect(main.todoEntities.repository).toBeInstanceOf(TodoRepository);
953
- });
954
-
955
- it('should resolve memoized dependency by passing arguments resolve from container by another argument', function () {
956
- interface IRepository {
957
- name: string;
958
- }
959
-
960
- const IUserRepositoryKey = new SingleToken<IRepository>('IUserRepository');
961
- const ITodoRepositoryKey = new SingleToken<IRepository>('ITodoRepository');
1299
+ // Inject EntityManager configured for Users
1300
+ @inject(EntityManagerToken.args(UserRepositoryToken))
1301
+ public userManager: EntityManager,
962
1302
 
963
- @register(bindTo(IUserRepositoryKey))
964
- class UserRepository implements IRepository {
965
- name = 'UserRepository';
1303
+ // Inject EntityManager configured for Todos
1304
+ @inject(EntityManagerToken.args(TodoRepositoryToken))
1305
+ public todoManager: EntityManager,
1306
+ ) {}
966
1307
  }
967
1308
 
968
- @register(bindTo(ITodoRepositoryKey))
969
- class TodoRepository implements IRepository {
970
- name = 'TodoRepository';
971
- }
1309
+ it('should create specialized instances based on token arguments', function () {
1310
+ const root = createContainer()
1311
+ .addRegistration(R.fromClass(EntityManager))
1312
+ .addRegistration(R.fromClass(UserRepository))
1313
+ .addRegistration(R.fromClass(TodoRepository));
972
1314
 
973
- interface IEntityManager {
974
- repository: IRepository;
975
- }
1315
+ const app = root.resolve(App);
976
1316
 
977
- const IEntityManagerKey = new SingleToken<IEntityManager>('IEntityManager');
1317
+ expect(app.userManager.repository).toBeInstanceOf(UserRepository);
1318
+ expect(app.todoManager.repository).toBeInstanceOf(TodoRepository);
1319
+ });
978
1320
 
979
- @register(bindTo(IEntityManagerKey), argsFn(resolveByArgs), singleton(MultiCache.fromFirstArg))
980
- class EntityManager {
981
- constructor(public repository: IRepository) {}
982
- }
1321
+ it('should cache specialized instances separately', function () {
1322
+ const root = createContainer()
1323
+ .addRegistration(R.fromClass(EntityManager))
1324
+ .addRegistration(R.fromClass(UserRepository))
1325
+ .addRegistration(R.fromClass(TodoRepository));
983
1326
 
984
- class Main {
985
- constructor(
986
- @inject(IEntityManagerKey.args(IUserRepositoryKey)) public userEntities: EntityManager,
987
- @inject(IEntityManagerKey.args(ITodoRepositoryKey)) public todoEntities: EntityManager,
988
- ) {}
989
- }
1327
+ // Resolve user manager twice
1328
+ const userManager1 = EntityManagerToken.args(UserRepositoryToken).resolve(root);
1329
+ const userManager2 = EntityManagerToken.args(UserRepositoryToken).resolve(root);
990
1330
 
991
- const root = createContainer()
992
- .addRegistration(R.fromClass(EntityManager))
993
- .addRegistration(R.fromClass(UserRepository))
994
- .addRegistration(R.fromClass(TodoRepository));
995
- const main = root.resolve(Main);
1331
+ // Should be same instance (cached)
1332
+ expect(userManager1).toBe(userManager2);
996
1333
 
997
- const userRepository = IEntityManagerKey.args(IUserRepositoryKey).resolve(root).repository;
998
- expect(userRepository).toBeInstanceOf(UserRepository);
999
- expect(main.userEntities.repository).toBe(userRepository);
1334
+ // Resolve todo manager
1335
+ const todoManager = EntityManagerToken.args(TodoRepositoryToken).resolve(root);
1000
1336
 
1001
- const todoRepository = IEntityManagerKey.args(ITodoRepositoryKey).resolve(root).repository;
1002
- expect(todoRepository).toBeInstanceOf(TodoRepository);
1003
- expect(main.todoEntities.repository).toBe(todoRepository);
1337
+ // Should be different from user manager
1338
+ expect(todoManager).not.toBe(userManager1);
1339
+ });
1004
1340
  });
1005
1341
  });
1006
1342
 
@@ -1012,6 +1348,7 @@ Sometimes you want to hide dependency if somebody wants to resolve it from certa
1012
1348
  - `Provider.fromClass(Logger).pipe(scopeAccess(({ invocationScope, providerScope }) => invocationScope === providerScope))`
1013
1349
 
1014
1350
  ```typescript
1351
+ import 'reflect-metadata';
1015
1352
  import {
1016
1353
  bindTo,
1017
1354
  Container,
@@ -1023,22 +1360,79 @@ import {
1023
1360
  singleton,
1024
1361
  } from 'ts-ioc-container';
1025
1362
 
1363
+ /**
1364
+ * User Management Domain - Visibility Control
1365
+ *
1366
+ * Some services should only be accessible in specific scopes:
1367
+ * - AdminService: Only accessible in admin routes
1368
+ * - AuditLogger: Only accessible at application level (not per-request)
1369
+ * - DebugService: Only accessible in development environment
1370
+ *
1371
+ * scopeAccess() controls VISIBILITY - whether a registered service
1372
+ * can be resolved from a particular scope.
1373
+ *
1374
+ * This provides security-by-design:
1375
+ * - Prevents accidental access to sensitive services
1376
+ * - Enforces architectural boundaries
1377
+ * - Catches misuse at resolution time (not runtime)
1378
+ */
1026
1379
  describe('Visibility', function () {
1027
- it('should hide from children', () => {
1380
+ it('should restrict admin services to admin routes only', () => {
1381
+ // UserManagementService can delete users - admin only!
1028
1382
  @register(
1029
- bindTo('logger'),
1030
- scope((s) => s.hasTag('root')),
1383
+ bindTo('IUserManagement'),
1384
+ scope((s) => s.hasTag('application')), // Registered at app level
1031
1385
  singleton(),
1386
+ // Only accessible from admin scope, not regular request scope
1387
+ scopeAccess(({ invocationScope }) => invocationScope.hasTag('admin')),
1388
+ )
1389
+ class UserManagementService {
1390
+ deleteUser(userId: string): string {
1391
+ return `Deleted user ${userId}`;
1392
+ }
1393
+ }
1394
+
1395
+ const appContainer = new Container({ tags: ['application'] }).addRegistration(R.fromClass(UserManagementService));
1396
+
1397
+ // Admin route scope
1398
+ const adminScope = appContainer.createScope({ tags: ['request', 'admin'] });
1399
+
1400
+ // Regular user route scope
1401
+ const userScope = appContainer.createScope({ tags: ['request', 'user'] });
1402
+
1403
+ // Admin can access UserManagementService
1404
+ const adminService = adminScope.resolve<UserManagementService>('IUserManagement');
1405
+ expect(adminService.deleteUser('user-123')).toBe('Deleted user user-123');
1406
+
1407
+ // Regular users cannot access it - security enforced at DI level
1408
+ expect(() => userScope.resolve('IUserManagement')).toThrowError(DependencyNotFoundError);
1409
+ });
1410
+
1411
+ it('should restrict application-level services from request scope', () => {
1412
+ // AuditLogger should only be used at application initialization
1413
+ // Not from request handlers (to prevent log corruption from concurrent access)
1414
+ @register(
1415
+ bindTo('IAuditLogger'),
1416
+ scope((s) => s.hasTag('application')),
1417
+ singleton(),
1418
+ // Only accessible from the scope where it was registered
1032
1419
  scopeAccess(({ invocationScope, providerScope }) => invocationScope === providerScope),
1033
1420
  )
1034
- class FileLogger {}
1421
+ class AuditLogger {
1422
+ log(message: string): string {
1423
+ return `AUDIT: ${message}`;
1424
+ }
1425
+ }
1035
1426
 
1036
- const parent = new Container({ tags: ['root'] }).addRegistration(R.fromClass(FileLogger));
1427
+ const appContainer = new Container({ tags: ['application'] }).addRegistration(R.fromClass(AuditLogger));
1037
1428
 
1038
- const child = parent.createScope({ tags: ['child'] });
1429
+ const requestScope = appContainer.createScope({ tags: ['request'] });
1039
1430
 
1040
- expect(() => child.resolve('logger')).toThrowError(DependencyNotFoundError);
1041
- expect(parent.resolve('logger')).toBeInstanceOf(FileLogger);
1431
+ // Application can use AuditLogger (for startup logging)
1432
+ expect(appContainer.resolve<AuditLogger>('IAuditLogger').log('App started')).toBe('AUDIT: App started');
1433
+
1434
+ // Request handlers cannot access it directly
1435
+ expect(() => requestScope.resolve('IAuditLogger')).toThrowError(DependencyNotFoundError);
1042
1436
  });
1043
1437
  });
1044
1438
 
@@ -1051,6 +1445,7 @@ Alias is needed to group keys
1051
1445
  - `Provider.fromClass(Logger).pipe(alias('logger'))`
1052
1446
 
1053
1447
  ```typescript
1448
+ import 'reflect-metadata';
1054
1449
  import {
1055
1450
  bindTo,
1056
1451
  Container,
@@ -1062,95 +1457,127 @@ import {
1062
1457
  select as s,
1063
1458
  } from 'ts-ioc-container';
1064
1459
 
1460
+ /**
1461
+ * User Management Domain - Alias Pattern (Multiple Implementations)
1462
+ *
1463
+ * Aliases allow multiple services to be registered under the same key.
1464
+ * This is useful for:
1465
+ * - Plugin systems (multiple notification channels)
1466
+ * - Strategy pattern (multiple authentication providers)
1467
+ * - Event handlers (multiple listeners for same event)
1468
+ *
1469
+ * Example: NotificationService with Email, SMS, and Push implementations
1470
+ */
1065
1471
  describe('alias', () => {
1066
- const IMiddlewareKey = 'IMiddleware';
1067
- const middleware = register(bindTo(s.alias(IMiddlewareKey)));
1472
+ // All notification services share this alias
1473
+ const INotificationChannel = 'INotificationChannel';
1474
+ const notificationChannel = register(bindTo(s.alias(INotificationChannel)));
1068
1475
 
1069
- interface IMiddleware {
1070
- applyTo(application: IApplication): void;
1476
+ interface INotificationChannel {
1477
+ send(userId: string, message: string): void;
1478
+ getDeliveredMessages(): string[];
1071
1479
  }
1072
1480
 
1073
- interface IApplication {
1074
- use(module: IMiddleware): void;
1075
- markMiddlewareAsApplied(name: string): void;
1076
- }
1481
+ // Email notification - always available
1482
+ @notificationChannel
1483
+ class EmailNotifier implements INotificationChannel {
1484
+ private delivered: string[] = [];
1077
1485
 
1078
- @middleware
1079
- class LoggerMiddleware implements IMiddleware {
1080
- applyTo(application: IApplication): void {
1081
- application.markMiddlewareAsApplied('LoggerMiddleware');
1486
+ send(userId: string, message: string): void {
1487
+ this.delivered.push(`EMAIL to ${userId}: ${message}`);
1082
1488
  }
1083
- }
1084
1489
 
1085
- @middleware
1086
- class ErrorHandlerMiddleware implements IMiddleware {
1087
- applyTo(application: IApplication): void {
1088
- application.markMiddlewareAsApplied('ErrorHandlerMiddleware');
1490
+ getDeliveredMessages(): string[] {
1491
+ return this.delivered;
1089
1492
  }
1090
1493
  }
1091
1494
 
1092
- it('should resolve by some alias', () => {
1093
- class App implements IApplication {
1094
- private appliedMiddleware: Set<string> = new Set();
1095
- constructor(@inject(s.alias(IMiddlewareKey)) public middleware: IMiddleware[]) {}
1495
+ // SMS notification - for urgent messages
1496
+ @notificationChannel
1497
+ class SmsNotifier implements INotificationChannel {
1498
+ private delivered: string[] = [];
1096
1499
 
1097
- markMiddlewareAsApplied(name: string): void {
1098
- this.appliedMiddleware.add(name);
1099
- }
1500
+ send(userId: string, message: string): void {
1501
+ this.delivered.push(`SMS to ${userId}: ${message}`);
1502
+ }
1100
1503
 
1101
- isMiddlewareApplied(name: string): boolean {
1102
- return this.appliedMiddleware.has(name);
1103
- }
1504
+ getDeliveredMessages(): string[] {
1505
+ return this.delivered;
1506
+ }
1507
+ }
1104
1508
 
1105
- use(module: IMiddleware): void {
1106
- module.applyTo(this);
1107
- }
1509
+ it('should notify through all channels', () => {
1510
+ // NotificationManager broadcasts to ALL registered channels
1511
+ class NotificationManager {
1512
+ constructor(@inject(s.alias(INotificationChannel)) private channels: INotificationChannel[]) {}
1108
1513
 
1109
- run() {
1110
- for (const module of this.middleware) {
1111
- module.applyTo(this);
1514
+ notifyUser(userId: string, message: string): void {
1515
+ for (const channel of this.channels) {
1516
+ channel.send(userId, message);
1112
1517
  }
1113
1518
  }
1519
+
1520
+ getChannelCount(): number {
1521
+ return this.channels.length;
1522
+ }
1114
1523
  }
1115
1524
 
1116
1525
  const container = new Container()
1117
- .addRegistration(R.fromClass(LoggerMiddleware))
1118
- .addRegistration(R.fromClass(ErrorHandlerMiddleware));
1526
+ .addRegistration(R.fromClass(EmailNotifier))
1527
+ .addRegistration(R.fromClass(SmsNotifier));
1119
1528
 
1120
- const app = container.resolve(App);
1121
- app.run();
1529
+ const manager = container.resolve(NotificationManager);
1530
+ manager.notifyUser('user-123', 'Your password was reset');
1122
1531
 
1123
- expect(app.isMiddlewareApplied('LoggerMiddleware')).toBe(true);
1124
- expect(app.isMiddlewareApplied('ErrorHandlerMiddleware')).toBe(true);
1532
+ // Both channels received the message
1533
+ expect(manager.getChannelCount()).toBe(2);
1125
1534
  });
1126
1535
 
1127
- it('should resolve by some alias', () => {
1128
- @register(bindTo(s.alias('ILogger')))
1129
- class FileLogger {}
1536
+ it('should resolve single implementation by alias', () => {
1537
+ // Sometimes you only need one implementation (e.g., primary email service)
1538
+ @register(bindTo(s.alias('IPrimaryNotifier')))
1539
+ class PrimaryEmailNotifier {
1540
+ readonly type = 'email';
1541
+ }
1130
1542
 
1131
- const container = new Container().addRegistration(R.fromClass(FileLogger));
1543
+ const container = new Container().addRegistration(R.fromClass(PrimaryEmailNotifier));
1132
1544
 
1133
- expect(container.resolveOneByAlias('ILogger')).toBeInstanceOf(FileLogger);
1134
- expect(() => container.resolve('logger')).toThrowError(DependencyNotFoundError);
1545
+ // resolveOneByAlias returns first matching implementation
1546
+ const notifier = container.resolveOneByAlias<PrimaryEmailNotifier>('IPrimaryNotifier');
1547
+ expect(notifier.type).toBe('email');
1548
+
1549
+ // Direct key resolution fails - only alias is registered
1550
+ expect(() => container.resolve('IPrimaryNotifier')).toThrowError(DependencyNotFoundError);
1135
1551
  });
1136
1552
 
1137
- it('should resolve by alias', () => {
1138
- @register(bindTo(s.alias('ILogger')), scope((s) => s.hasTag('root')))
1139
- class FileLogger {}
1553
+ it('should use different implementations per scope', () => {
1554
+ // Development: Console logger for easy debugging
1555
+ @register(bindTo(s.alias('ILogger')), scope((s) => s.hasTag('development')))
1556
+ class ConsoleLogger {
1557
+ readonly type = 'console';
1558
+ }
1140
1559
 
1141
- @register(bindTo(s.alias('ILogger')), scope((s) => s.hasTag('child')))
1142
- class DbLogger {}
1560
+ // Production: Database logger for audit trail
1561
+ @register(bindTo(s.alias('ILogger')), scope((s) => s.hasTag('production')))
1562
+ class DatabaseLogger {
1563
+ readonly type = 'database';
1564
+ }
1565
+
1566
+ // Development environment
1567
+ const devContainer = new Container({ tags: ['development'] })
1568
+ .addRegistration(R.fromClass(ConsoleLogger))
1569
+ .addRegistration(R.fromClass(DatabaseLogger));
1143
1570
 
1144
- const container = new Container({ tags: ['root'] })
1145
- .addRegistration(R.fromClass(FileLogger))
1146
- .addRegistration(R.fromClass(DbLogger));
1571
+ // Production environment
1572
+ const prodContainer = new Container({ tags: ['production'] })
1573
+ .addRegistration(R.fromClass(ConsoleLogger))
1574
+ .addRegistration(R.fromClass(DatabaseLogger));
1147
1575
 
1148
- const result1 = container.resolveOneByAlias('ILogger');
1149
- const child = container.createScope({ tags: ['child'] });
1150
- const result2 = child.resolveOneByAlias('ILogger');
1576
+ const devLogger = devContainer.resolveOneByAlias<ConsoleLogger | DatabaseLogger>('ILogger');
1577
+ const prodLogger = prodContainer.resolveOneByAlias<ConsoleLogger | DatabaseLogger>('ILogger');
1151
1578
 
1152
- expect(result1).toBeInstanceOf(FileLogger);
1153
- expect(result2).toBeInstanceOf(DbLogger);
1579
+ expect(devLogger.type).toBe('console');
1580
+ expect(prodLogger.type).toBe('database');
1154
1581
  });
1155
1582
  });
1156
1583
 
@@ -1173,7 +1600,23 @@ import {
1173
1600
  singleton,
1174
1601
  } from 'ts-ioc-container';
1175
1602
 
1176
- describe('lazy provider', () => {
1603
+ /**
1604
+ * User Management Domain - Decorator Pattern
1605
+ *
1606
+ * The decorator pattern wraps a service with additional behavior:
1607
+ * - Logging: Log all repository operations for audit
1608
+ * - Caching: Cache results of expensive operations
1609
+ * - Retry: Automatically retry failed operations
1610
+ * - Validation: Validate inputs before processing
1611
+ *
1612
+ * In DI, decorators are applied at registration time, so consumers
1613
+ * get the decorated version without knowing about the decoration.
1614
+ *
1615
+ * This example shows a TodoRepository decorated with logging -
1616
+ * every save operation is automatically logged.
1617
+ */
1618
+ describe('Decorator Pattern', () => {
1619
+ // Singleton logger collects all log entries
1177
1620
  @register(singleton())
1178
1621
  class Logger {
1179
1622
  private logs: string[] = [];
@@ -1196,50 +1639,58 @@ describe('lazy provider', () => {
1196
1639
  text: string;
1197
1640
  }
1198
1641
 
1199
- class LogRepository implements IRepository {
1642
+ // Decorator: Wraps any IRepository with logging behavior
1643
+ class LoggingRepository implements IRepository {
1200
1644
  constructor(
1201
1645
  private repository: IRepository,
1202
1646
  @inject(s.token('Logger').lazy()) private logger: Logger,
1203
1647
  ) {}
1204
1648
 
1205
1649
  async save(item: Todo): Promise<void> {
1650
+ // Log the operation
1206
1651
  this.logger.log(item.id);
1652
+ // Delegate to the wrapped repository
1207
1653
  return this.repository.save(item);
1208
1654
  }
1209
1655
  }
1210
1656
 
1211
- const logRepo = (dep: IRepository, scope: IContainer) => scope.resolve(LogRepository, { args: [dep] });
1657
+ // Decorator factory - creates LoggingRepository wrapping the original
1658
+ const withLogging = (repository: IRepository, scope: IContainer) =>
1659
+ scope.resolve(LoggingRepository, { args: [repository] });
1212
1660
 
1213
- @register(bindTo('IRepository'), decorate(logRepo))
1661
+ // TodoRepository is automatically decorated with logging
1662
+ @register(bindTo('IRepository'), decorate(withLogging))
1214
1663
  class TodoRepository implements IRepository {
1215
- async save(item: Todo): Promise<void> {}
1664
+ async save(item: Todo): Promise<void> {
1665
+ // Actual database save logic would go here
1666
+ }
1216
1667
  }
1217
1668
 
1218
1669
  class App {
1219
1670
  constructor(@inject('IRepository') public repository: IRepository) {}
1220
1671
 
1221
1672
  async run() {
1222
- await this.repository.save({ id: '1', text: 'Hello' });
1223
- await this.repository.save({ id: '2', text: 'Hello' });
1673
+ await this.repository.save({ id: '1', text: 'Buy groceries' });
1674
+ await this.repository.save({ id: '2', text: 'Walk the dog' });
1224
1675
  }
1225
1676
  }
1226
1677
 
1227
- function createContainer() {
1228
- const container = new Container();
1229
- container.addRegistration(R.fromClass(TodoRepository)).addRegistration(R.fromClass(Logger));
1230
- return container;
1678
+ function createAppContainer() {
1679
+ return new Container({ tags: ['application'] })
1680
+ .addRegistration(R.fromClass(TodoRepository))
1681
+ .addRegistration(R.fromClass(Logger));
1231
1682
  }
1232
1683
 
1233
- it('should decorate repo by logger middleware', async () => {
1234
- // Arrange
1235
- const container = createContainer();
1684
+ it('should automatically log all repository operations via decorator', async () => {
1685
+ const container = createAppContainer();
1236
1686
 
1237
- // Act
1238
1687
  const app = container.resolve(App);
1239
1688
  const logger = container.resolve<Logger>('Logger');
1689
+
1690
+ // App uses repository normally - unaware of logging decorator
1240
1691
  await app.run();
1241
1692
 
1242
- // Assert
1693
+ // All operations were logged transparently
1243
1694
  expect(logger.printLogs()).toBe('1,2');
1244
1695
  });
1245
1696
  });
@@ -1272,52 +1723,75 @@ import {
1272
1723
  singleton,
1273
1724
  } from 'ts-ioc-container';
1274
1725
 
1726
+ /**
1727
+ * User Management Domain - Registration Patterns
1728
+ *
1729
+ * Registrations define how dependencies are bound to the container.
1730
+ * Common patterns:
1731
+ * - Register by class (auto-generates key from class name)
1732
+ * - Register by value (constants, configuration)
1733
+ * - Register by factory function (dynamic creation)
1734
+ * - Register with aliases (multiple keys for same service)
1735
+ *
1736
+ * This is the foundation for dependency injection - telling the container
1737
+ * "when someone asks for X, give them Y".
1738
+ */
1275
1739
  describe('Registration module', function () {
1276
- const createContainer = () => new Container({ tags: ['root'] });
1740
+ const createAppContainer = () => new Container({ tags: ['application'] });
1277
1741
 
1278
- it('should register class', function () {
1279
- @register(bindTo('ILogger'), scope((s) => s.hasTag('root')), singleton())
1742
+ it('should register class with scope and lifecycle', function () {
1743
+ // Logger is registered at application scope as a singleton
1744
+ @register(bindTo('ILogger'), scope((s) => s.hasTag('application')), singleton())
1280
1745
  class Logger {}
1281
1746
 
1282
- const root = createContainer().addRegistration(R.fromClass(Logger));
1747
+ const appContainer = createAppContainer().addRegistration(R.fromClass(Logger));
1283
1748
 
1284
- expect(root.resolve('ILogger')).toBeInstanceOf(Logger);
1749
+ expect(appContainer.resolve('ILogger')).toBeInstanceOf(Logger);
1285
1750
  });
1286
1751
 
1287
- it('should register value', function () {
1288
- const root = createContainer().addRegistration(R.fromValue('smth').bindToKey('ISmth'));
1752
+ it('should register configuration value', function () {
1753
+ // Register application configuration as a value
1754
+ const appContainer = createAppContainer().addRegistration(R.fromValue('production').bindToKey('Environment'));
1289
1755
 
1290
- expect(root.resolve('ISmth')).toBe('smth');
1756
+ expect(appContainer.resolve('Environment')).toBe('production');
1291
1757
  });
1292
1758
 
1293
- it('should register fn', function () {
1294
- const root = createContainer().addRegistration(R.fromFn(() => 'smth').bindToKey('ISmth'));
1759
+ it('should register factory function', function () {
1760
+ // Factory functions are useful for dynamic creation
1761
+ const appContainer = createAppContainer().addRegistration(
1762
+ R.fromFn(() => `app-${Date.now()}`).bindToKey('RequestId'),
1763
+ );
1295
1764
 
1296
- expect(root.resolve('ISmth')).toBe('smth');
1765
+ expect(appContainer.resolve('RequestId')).toContain('app-');
1297
1766
  });
1298
1767
 
1299
- it('should raise an error if key is not provider', () => {
1768
+ it('should raise an error if binding key is not provided', () => {
1769
+ // Values and functions must have explicit keys (classes use class name by default)
1300
1770
  expect(() => {
1301
- createContainer().addRegistration(R.fromValue('smth'));
1771
+ createAppContainer().addRegistration(R.fromValue('orphan-value'));
1302
1772
  }).toThrowError(DependencyMissingKeyError);
1303
1773
  });
1304
1774
 
1305
- it('should register dependency by class name if @key is not provided', function () {
1775
+ it('should register dependency by class name when no key decorator is used', function () {
1776
+ // Without @register(bindTo('key')), the class name becomes the key
1306
1777
  class FileLogger {}
1307
1778
 
1308
- const root = createContainer().addRegistration(R.fromClass(FileLogger));
1779
+ const appContainer = createAppContainer().addRegistration(R.fromClass(FileLogger));
1309
1780
 
1310
- expect(root.resolve('FileLogger')).toBeInstanceOf(FileLogger);
1781
+ expect(appContainer.resolve('FileLogger')).toBeInstanceOf(FileLogger);
1311
1782
  });
1312
1783
 
1313
- it('should assign additional key which redirects to original one', function () {
1784
+ it('should register with multiple keys using aliases', function () {
1785
+ // Same service accessible via direct key and alias
1314
1786
  @register(bindTo('ILogger'), bindTo(s.alias('Logger')), singleton())
1315
1787
  class Logger {}
1316
1788
 
1317
- const root = createContainer().addRegistration(R.fromClass(Logger));
1789
+ const appContainer = createAppContainer().addRegistration(R.fromClass(Logger));
1318
1790
 
1319
- expect(root.resolveByAlias('Logger')[0]).toBeInstanceOf(Logger);
1320
- expect(root.resolve('ILogger')).toBeInstanceOf(Logger);
1791
+ // Accessible via alias (for group resolution)
1792
+ expect(appContainer.resolveByAlias('Logger')[0]).toBeInstanceOf(Logger);
1793
+ // Accessible via direct key
1794
+ expect(appContainer.resolve('ILogger')).toBeInstanceOf(Logger);
1321
1795
  });
1322
1796
  });
1323
1797
 
@@ -1331,13 +1805,35 @@ Sometimes you need to register provider only in scope which matches to certain c
1331
1805
  ```typescript
1332
1806
  import { bindTo, Container, register, Registration as R, scope, singleton } from 'ts-ioc-container';
1333
1807
 
1334
- @register(bindTo('ILogger'), scope((s) => s.hasTag('root')), singleton())
1335
- class Logger {}
1808
+ /**
1809
+ * Scoping - Scope Match Rule
1810
+ *
1811
+ * You can restrict WHERE a provider is registered.
1812
+ * This is useful for singleton services that should only exist in the root scope,
1813
+ * or per-request services that should only exist in request scopes.
1814
+ */
1815
+
1336
1816
  describe('ScopeProvider', function () {
1337
- it('should return the same instance', function () {
1338
- const root = new Container({ tags: ['root'] }).addRegistration(R.fromClass(Logger));
1339
- const child = root.createScope();
1340
- expect(root.resolve('ILogger')).toBe(child.resolve('ILogger'));
1817
+ it('should register provider only in matching scope', function () {
1818
+ // SharedState should be a singleton in the root 'application' scope
1819
+ // It will be visible to all child scopes, but physically resides in 'application'
1820
+ @register(
1821
+ bindTo('SharedState'),
1822
+ scope((s) => s.hasTag('application')), // Only register in application scope
1823
+ singleton(), // One instance per application
1824
+ )
1825
+ class SharedState {
1826
+ data = 'shared';
1827
+ }
1828
+
1829
+ const appContainer = new Container({ tags: ['application'] }).addRegistration(R.fromClass(SharedState));
1830
+ const requestScope = appContainer.createScope({ tags: ['request'] });
1831
+
1832
+ // Both resolve to the SAME instance because it's a singleton in the app scope
1833
+ const appState = appContainer.resolve('SharedState');
1834
+ const requestState = requestScope.resolve('SharedState');
1835
+
1836
+ expect(appState).toBe(requestState);
1341
1837
  });
1342
1838
  });
1343
1839
 
@@ -1347,41 +1843,120 @@ describe('ScopeProvider', function () {
1347
1843
  Sometimes you want to encapsulate registration logic in separate module. This is what `IContainerModule` is for.
1348
1844
 
1349
1845
  ```typescript
1846
+ import 'reflect-metadata';
1350
1847
  import { bindTo, Container, type IContainer, type IContainerModule, register, Registration as R } from 'ts-ioc-container';
1351
1848
 
1352
- @register(bindTo('ILogger'))
1353
- class Logger {}
1849
+ /**
1850
+ * User Management Domain - Container Modules
1851
+ *
1852
+ * Modules organize related registrations and allow swapping implementations
1853
+ * based on environment (development, testing, production).
1854
+ *
1855
+ * Common module patterns:
1856
+ * - ProductionModule: Real database, external APIs, email service
1857
+ * - DevelopmentModule: In-memory database, mock APIs, console logging
1858
+ * - TestingModule: Mocks with assertion capabilities
1859
+ *
1860
+ * This enables:
1861
+ * - Easy environment switching
1862
+ * - Isolated testing without external dependencies
1863
+ * - Feature flags via module composition
1864
+ */
1865
+
1866
+ // Auth service interface - same API for all environments
1867
+ interface IAuthService {
1868
+ authenticate(email: string, password: string): boolean;
1869
+ getServiceType(): string;
1870
+ }
1871
+
1872
+ // Production: Real authentication with database lookup
1873
+ @register(bindTo('IAuthService'))
1874
+ class ProductionAuthService implements IAuthService {
1875
+ authenticate(email: string, password: string): boolean {
1876
+ // In production, this would query the database
1877
+ return email === 'admin@example.com' && password === 'secure_password';
1878
+ }
1879
+
1880
+ getServiceType(): string {
1881
+ return 'production';
1882
+ }
1883
+ }
1884
+
1885
+ // Development: Accepts any credentials for easy testing
1886
+ @register(bindTo('IAuthService'))
1887
+ class DevelopmentAuthService implements IAuthService {
1888
+ authenticate(_email: string, _password: string): boolean {
1889
+ // Always succeed in development for easier testing
1890
+ return true;
1891
+ }
1354
1892
 
1355
- @register(bindTo('ILogger'))
1356
- class TestLogger {}
1893
+ getServiceType(): string {
1894
+ return 'development';
1895
+ }
1896
+ }
1357
1897
 
1358
- class Production implements IContainerModule {
1898
+ // Production module - real services with security
1899
+ class ProductionModule implements IContainerModule {
1359
1900
  applyTo(container: IContainer): void {
1360
- container.addRegistration(R.fromClass(Logger));
1901
+ container.addRegistration(R.fromClass(ProductionAuthService));
1902
+ // In a real app, also register:
1903
+ // - Real database connection
1904
+ // - External email service
1905
+ // - Payment gateway
1361
1906
  }
1362
1907
  }
1363
1908
 
1364
- class Development implements IContainerModule {
1909
+ // Development module - mocks and conveniences
1910
+ class DevelopmentModule implements IContainerModule {
1365
1911
  applyTo(container: IContainer): void {
1366
- container.addRegistration(R.fromClass(TestLogger));
1912
+ container.addRegistration(R.fromClass(DevelopmentAuthService));
1913
+ // In a real app, also register:
1914
+ // - In-memory database
1915
+ // - Console email logger
1916
+ // - Mock payment gateway
1367
1917
  }
1368
1918
  }
1369
1919
 
1370
1920
  describe('Container Modules', function () {
1371
1921
  function createContainer(isProduction: boolean) {
1372
- return new Container().useModule(isProduction ? new Production() : new Development());
1922
+ const module = isProduction ? new ProductionModule() : new DevelopmentModule();
1923
+ return new Container().useModule(module);
1373
1924
  }
1374
1925
 
1375
- it('should register production dependencies', function () {
1926
+ it('should use production auth with strict validation', function () {
1376
1927
  const container = createContainer(true);
1928
+ const auth = container.resolve<IAuthService>('IAuthService');
1377
1929
 
1378
- expect(container.resolve('ILogger')).toBeInstanceOf(Logger);
1930
+ expect(auth.getServiceType()).toBe('production');
1931
+ expect(auth.authenticate('admin@example.com', 'secure_password')).toBe(true);
1932
+ expect(auth.authenticate('admin@example.com', 'wrong_password')).toBe(false);
1379
1933
  });
1380
1934
 
1381
- it('should register development dependencies', function () {
1935
+ it('should use development auth with permissive validation', function () {
1382
1936
  const container = createContainer(false);
1937
+ const auth = container.resolve<IAuthService>('IAuthService');
1938
+
1939
+ expect(auth.getServiceType()).toBe('development');
1940
+ // Development mode accepts any credentials
1941
+ expect(auth.authenticate('any@email.com', 'any_password')).toBe(true);
1942
+ });
1943
+
1944
+ it('should allow composing multiple modules', function () {
1945
+ // Modules can be composed for feature flags or A/B testing
1946
+ class FeatureFlagModule implements IContainerModule {
1947
+ constructor(private enableNewFeature: boolean) {}
1383
1948
 
1384
- expect(container.resolve('ILogger')).toBeInstanceOf(TestLogger);
1949
+ applyTo(container: IContainer): void {
1950
+ if (this.enableNewFeature) {
1951
+ // Register new feature implementations
1952
+ }
1953
+ }
1954
+ }
1955
+
1956
+ const container = new Container().useModule(new ProductionModule()).useModule(new FeatureFlagModule(true));
1957
+
1958
+ // Base services from ProductionModule
1959
+ expect(container.resolve<IAuthService>('IAuthService').getServiceType()).toBe('production');
1385
1960
  });
1386
1961
  });
1387
1962
 
@@ -1402,32 +1977,49 @@ import {
1402
1977
  Registration as R,
1403
1978
  } from 'ts-ioc-container';
1404
1979
 
1980
+ /**
1981
+ * Lifecycle - OnConstruct Hook
1982
+ *
1983
+ * The @onConstruct hook allows you to run logic immediately after an object is created.
1984
+ * This is useful for:
1985
+ * - Initialization logic that depends on injected services
1986
+ * - Setting up event listeners
1987
+ * - Establishing connections (though lazy is often better)
1988
+ * - Computing initial state
1989
+ *
1990
+ * Note: You must register the AddOnConstructHookModule or manually add the hook runner.
1991
+ */
1992
+
1405
1993
  const execute: HookFn = (ctx: HookContext) => {
1406
1994
  ctx.invokeMethod({ args: ctx.resolveArgs() });
1407
1995
  };
1408
1996
 
1409
- class Car {
1410
- private engine!: string;
1411
-
1412
- @onConstruct(execute)
1413
- setEngine(@inject('engine') engine: string) {
1414
- this.engine = engine;
1415
- }
1416
-
1417
- getEngine() {
1418
- return this.engine;
1419
- }
1420
- }
1421
-
1422
1997
  describe('onConstruct', function () {
1423
- it('should run methods and resolve arguments from container', function () {
1424
- const root = new Container()
1998
+ it('should run initialization method after dependencies are resolved', function () {
1999
+ class DatabaseConnection {
2000
+ isConnected = false;
2001
+ connectionString = '';
2002
+
2003
+ // @onConstruct marks this method to be called after instantiation
2004
+ // Arguments are resolved from the container like constructor params
2005
+ @onConstruct(execute)
2006
+ connect(@inject('ConnectionString') connectionString: string) {
2007
+ this.connectionString = connectionString;
2008
+ this.isConnected = true;
2009
+ }
2010
+ }
2011
+
2012
+ const container = new Container()
2013
+ // Enable @onConstruct support
1425
2014
  .useModule(new AddOnConstructHookModule())
1426
- .addRegistration(R.fromValue('bmw').bindTo('engine'));
2015
+ // Register config
2016
+ .addRegistration(R.fromValue('postgres://localhost:5432').bindTo('ConnectionString'));
1427
2017
 
1428
- const car = root.resolve(Car);
2018
+ // Resolve class - constructor is called, then @onConstruct method
2019
+ const db = container.resolve(DatabaseConnection);
1429
2020
 
1430
- expect(car.getEngine()).toBe('bmw');
2021
+ expect(db.isConnected).toBe(true);
2022
+ expect(db.connectionString).toBe('postgres://localhost:5432');
1431
2023
  });
1432
2024
  });
1433
2025
 
@@ -1502,22 +2094,45 @@ describe('onDispose', function () {
1502
2094
  ### Inject property
1503
2095
 
1504
2096
  ```typescript
2097
+ import 'reflect-metadata';
1505
2098
  import { Container, hook, HooksRunner, injectProp, Registration } from 'ts-ioc-container';
1506
2099
 
1507
- const onInitHookRunner = new HooksRunner('onInit');
2100
+ /**
2101
+ * UI Components - Property Injection
2102
+ *
2103
+ * Property injection is useful when you don't control the class instantiation
2104
+ * (like in some UI frameworks, Web Components, or legacy systems) or when
2105
+ * you want to avoid massive constructors in base classes.
2106
+ *
2107
+ * This example demonstrates a ViewModel that gets dependencies injected
2108
+ * AFTER construction via an initialization hook.
2109
+ */
2110
+
1508
2111
  describe('inject property', () => {
1509
2112
  it('should inject property', () => {
1510
- class App {
1511
- @hook('onInit', injectProp('greeting'))
1512
- greeting!: string;
2113
+ // Runner for the 'onInit' lifecycle hook
2114
+ const onInitHookRunner = new HooksRunner('onInit');
2115
+
2116
+ class UserViewModel {
2117
+ // Inject 'GreetingService' into 'greeting' property during 'onInit'
2118
+ @hook('onInit', injectProp('GreetingService'))
2119
+ greetingService!: string;
2120
+
2121
+ display(): string {
2122
+ return `${this.greetingService} User`;
2123
+ }
1513
2124
  }
1514
- const expected = 'Hello world!';
1515
2125
 
1516
- const scope = new Container().addRegistration(Registration.fromValue(expected).bindToKey('greeting'));
1517
- const app = scope.resolve(App);
1518
- onInitHookRunner.execute(app, { scope });
2126
+ const container = new Container().addRegistration(Registration.fromValue('Hello').bindToKey('GreetingService'));
2127
+
2128
+ // 1. Create instance (dependencies not yet injected)
2129
+ const viewModel = container.resolve(UserViewModel);
2130
+
2131
+ // 2. Run lifecycle hooks to inject properties
2132
+ onInitHookRunner.execute(viewModel, { scope: container });
1519
2133
 
1520
- expect(app.greeting).toBe(expected);
2134
+ expect(viewModel.greetingService).toBe('Hello');
2135
+ expect(viewModel.display()).toBe('Hello User');
1521
2136
  });
1522
2137
  });
1523
2138