ts-ioc-container 50.2.1 → 50.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/README.md +3 -416
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -36,11 +36,9 @@ no global container objects.
36
36
  - [Cheatsheet](#cheatsheet)
37
37
  - [tsyringe alternative](https://igorbabkin.github.io/ts-ioc-container/tsyringe-alternative)
38
38
  - [Inversify and Awilix alternative](https://igorbabkin.github.io/ts-ioc-container/inversify-awilix-alternative)
39
- - [Recipes](#recipes)
40
39
  - [Container](#container)
41
40
  - [Basic usage](#basic-usage)
42
41
  - [Scope](#scope) `tags`
43
- - [Dynamic Tag Management](#dynamic-tag-management) `addTags`
44
42
  - [Instances](#instances)
45
43
  - [Dispose](#dispose)
46
44
  - [Lazy](#lazy) `lazy`
@@ -153,208 +151,6 @@ describe('Quickstart', function () {
153
151
  > for `R.fromValue(...)` and `R.fromFn(...)` (which have no class to decorate)
154
152
  > or for third-party classes you don't own.
155
153
 
156
- ## Recipes
157
-
158
- ### Express/Next handler (per-request scope)
159
-
160
- import 'reflect-metadata';
161
- import { bindTo, Container, inject, register, Registration as R, singleton } from 'ts-ioc-container';
162
-
163
- /**
164
- * Web Framework Integration - Per-Request Scope
165
- *
166
- * In Express/Next.js applications, each HTTP request typically gets its own
167
- * scope. This ensures request-specific state (logger context, current user,
168
- * correlation IDs) is isolated between concurrent requests.
169
- *
170
- * Scope hierarchy:
171
- * Application (singleton services — live for entire app lifetime)
172
- * └── Request (per-request services — created and disposed per request)
173
- */
174
-
175
- @register(bindTo('ILogger'), singleton())
176
- class Logger {
177
- readonly messages: string[] = [];
178
-
179
- log(message: string) {
180
- this.messages.push(message);
181
- }
182
- }
183
-
184
- describe('Express/Next per-request scope', () => {
185
- it('should give each request its own Logger instance', () => {
186
- const app = new Container({ tags: ['application'] }).addRegistration(R.fromClass(Logger));
187
-
188
- // Simulate two concurrent HTTP requests
189
- const request1Scope = app.createScope({ tags: ['request'] });
190
- const request2Scope = app.createScope({ tags: ['request'] });
191
-
192
- const logger1 = request1Scope.resolve<Logger>('ILogger');
193
- const logger2 = request2Scope.resolve<Logger>('ILogger');
194
-
195
- logger1.log('req 1 started');
196
- logger2.log('req 2 started');
197
-
198
- // Each request has its own Logger — logs don't leak between requests
199
- expect(logger1.messages).toEqual(['req 1 started']);
200
- expect(logger2.messages).toEqual(['req 2 started']);
201
- expect(logger1).not.toBe(logger2);
202
- });
203
-
204
- it('should resolve the same Logger within a single request', () => {
205
- const app = new Container({ tags: ['application'] }).addRegistration(R.fromClass(Logger));
206
-
207
- const requestScope = app.createScope({ tags: ['request'] });
208
-
209
- const logger1 = requestScope.resolve<Logger>('ILogger');
210
- const logger2 = requestScope.resolve<Logger>('ILogger');
211
-
212
- // Within one request, singleton is maintained
213
- expect(logger1).toBe(logger2);
214
- });
215
- });
216
-
217
-
218
- ### Background worker (singleton client, transient jobs)
219
-
220
- import 'reflect-metadata';
221
- import { Container, inject, register, Registration as R, singleton } from 'ts-ioc-container';
222
-
223
- /**
224
- * Background Worker - Singleton Client, Transient Jobs
225
- *
226
- * A queue worker typically needs:
227
- * - A single shared QueueClient (expensive to create, holds connections)
228
- * - A new JobHandler per job (stateful — holds job-specific data)
229
- *
230
- * singleton() on QueueClient ensures one shared connection pool.
231
- * No singleton on JobHandler gives a fresh instance per resolve.
232
- */
233
-
234
- @register(singleton())
235
- class QueueClient {
236
- readonly connected = true;
237
-
238
- dequeue(): string {
239
- return 'job-payload';
240
- }
241
- }
242
-
243
- class JobHandler {
244
- readonly result: string;
245
-
246
- constructor(@inject('QueueClient') private queue: QueueClient) {
247
- this.result = this.queue.dequeue();
248
- }
249
- }
250
-
251
- describe('Background worker', () => {
252
- function createWorker() {
253
- return new Container({ tags: ['worker'] })
254
- .addRegistration(R.fromClass(QueueClient))
255
- .addRegistration(R.fromClass(JobHandler));
256
- }
257
-
258
- it('should share a single QueueClient across all job handlers', () => {
259
- const worker = createWorker();
260
-
261
- const handler1 = worker.resolve(JobHandler);
262
- const handler2 = worker.resolve(JobHandler);
263
-
264
- const client1 = worker.resolve<QueueClient>('QueueClient');
265
- const client2 = worker.resolve<QueueClient>('QueueClient');
266
-
267
- // QueueClient is a singleton — same connection shared everywhere
268
- expect(client1).toBe(client2);
269
- expect(client1.connected).toBe(true);
270
-
271
- // JobHandler is transient — fresh instance per job
272
- expect(handler1).not.toBe(handler2);
273
- });
274
-
275
- it('should inject the shared QueueClient into each JobHandler', () => {
276
- const worker = createWorker();
277
-
278
- const handler = worker.resolve(JobHandler);
279
-
280
- expect(handler.result).toBe('job-payload');
281
- });
282
- });
283
-
284
-
285
- ### Frontend widget/page scope with lazy dependency
286
-
287
- import 'reflect-metadata';
288
- import { bindTo, Container, inject, register, Registration as R, select, singleton } from 'ts-ioc-container';
289
-
290
- /**
291
- * Frontend Widget - Page Scope with Lazy Dependency
292
- *
293
- * In frontend applications, feature flags are fetched once per page load
294
- * (singleton per page scope) but a widget may not need them on every render.
295
- * Lazy injection defers instantiation until the widget actually reads the flags,
296
- * avoiding unnecessary work for widgets that never display flag-gated content.
297
- *
298
- * Scope hierarchy:
299
- * Application
300
- * └── Page (singleton flags fetched once)
301
- * └── Widget (lazy flag access)
302
- */
303
-
304
- @register(bindTo('FeatureFlags'), singleton())
305
- class FeatureFlags {
306
- load(): Record<string, boolean> {
307
- return { newDashboard: true };
308
- }
309
- }
310
-
311
- class Widget {
312
- constructor(@inject(select.token('FeatureFlags').lazy()) public flags: FeatureFlags) {}
313
- }
314
-
315
- describe('Frontend widget/page scope with lazy dependency', () => {
316
- function createPage() {
317
- return new Container({ tags: ['page'] })
318
- .addRegistration(R.fromClass(FeatureFlags))
319
- .addRegistration(R.fromClass(Widget));
320
- }
321
-
322
- it('should not instantiate FeatureFlags until the widget actually accesses it', () => {
323
- const page = createPage();
324
-
325
- const widget = page.resolve(Widget);
326
-
327
- // Widget is resolved, but FeatureFlags has not been instantiated yet
328
- let instances = Array.from(page.getInstances()).filter((x) => x instanceof FeatureFlags);
329
- expect(instances).toHaveLength(0);
330
-
331
- // Accessing any property on the lazy proxy triggers instantiation
332
- const _load = widget.flags.load;
333
- expect(_load).toBeDefined();
334
-
335
- instances = Array.from(page.getInstances()).filter((x) => x instanceof FeatureFlags);
336
- expect(instances).toHaveLength(1);
337
- });
338
-
339
- it('should share the same FeatureFlags singleton across widgets on the same page', () => {
340
- const page = createPage();
341
-
342
- const widget1 = page.resolve(Widget);
343
- const widget2 = page.resolve(Widget);
344
-
345
- // Trigger instantiation through both widgets
346
- const _load1 = widget1.flags.load;
347
- const _load2 = widget2.flags.load;
348
- expect(_load1).toBeDefined();
349
- expect(_load2).toBeDefined();
350
-
351
- // Only one FeatureFlags instance was created across the whole page scope
352
- const instances = Array.from(page.getInstances()).filter((x) => x instanceof FeatureFlags);
353
- expect(instances).toHaveLength(1);
354
- });
355
- });
356
-
357
-
358
154
  ## Container
359
155
 
360
156
  `IContainer` consists of:
@@ -547,116 +343,6 @@ describe('Scopes', function () {
547
343
 
548
344
  ```
549
345
 
550
- ### Dynamic Tag Management
551
-
552
- You can dynamically add tags to a container after it's been created using the `addTags()` method. This is useful for environment-based configuration, feature flags, and progressive container setup.
553
-
554
- - Tags can be added one at a time or multiple at once
555
- - Useful for conditional configuration based on `NODE_ENV` or runtime flags
556
- - Container can be configured incrementally as the application initializes
557
-
558
- > [!WARNING]
559
- > Tags must be added **before** registrations are applied. Scope matching happens at registration time, so adding tags later does not retroactively make providers available.
560
-
561
- ```typescript
562
- import { bindTo, Container, register, Registration as R, scope } from 'ts-ioc-container';
563
-
564
- describe('addTags', () => {
565
- it('should dynamically add tags to enable environment-based registration', () => {
566
- @register(bindTo('logger'), scope((s) => s.hasTag('development')))
567
- class ConsoleLogger {
568
- log(message: string) {
569
- console.log(`[DEV] ${message}`);
570
- }
571
- }
572
-
573
- @register(bindTo('logger'), scope((s) => s.hasTag('production')))
574
- class FileLogger {
575
- log(message: string) {
576
- console.log(`[PROD] ${message}`);
577
- }
578
- }
579
-
580
- // Create container and configure for environment
581
- const container = new Container();
582
- const environment = 'development';
583
- container.addTags(environment); // Add tag dynamically based on environment
584
-
585
- // Register services after tag is set
586
- container.addRegistration(R.fromClass(ConsoleLogger)).addRegistration(R.fromClass(FileLogger));
587
-
588
- // Resolve logger - gets ConsoleLogger because 'development' tag was added
589
- const logger = container.resolve<ConsoleLogger>('logger');
590
- expect(logger).toBeInstanceOf(ConsoleLogger);
591
- });
592
-
593
- it('should add multiple tags for feature-based configuration', () => {
594
- @register(bindTo('premiumFeature'), scope((s) => s.hasTag('premium')))
595
- class PremiumFeature {}
596
-
597
- @register(bindTo('betaFeature'), scope((s) => s.hasTag('beta')))
598
- class BetaFeature {}
599
-
600
- const container = new Container();
601
-
602
- // Add multiple tags at once
603
- container.addTags('premium', 'beta', 'experimental');
604
-
605
- // Verify all tags are present
606
- expect(container.hasTag('premium')).toBe(true);
607
- expect(container.hasTag('beta')).toBe(true);
608
- expect(container.hasTag('experimental')).toBe(true);
609
-
610
- // Register features after tags are added
611
- container.addRegistration(R.fromClass(PremiumFeature)).addRegistration(R.fromClass(BetaFeature));
612
-
613
- // Both features are available because container has both tags
614
- expect(container.resolve('premiumFeature')).toBeInstanceOf(PremiumFeature);
615
- expect(container.resolve('betaFeature')).toBeInstanceOf(BetaFeature);
616
- });
617
-
618
- it('should affect child scope creation', () => {
619
- @register(bindTo('service'), scope((s) => s.hasTag('api')))
620
- class ApiService {
621
- handleRequest() {
622
- return 'API response';
623
- }
624
- }
625
-
626
- const appContainer = new Container();
627
-
628
- // Add tag to parent
629
- appContainer.addTags('api');
630
- appContainer.addRegistration(R.fromClass(ApiService));
631
-
632
- // Create child scopes - they inherit parent's registrations
633
- const requestScope1 = appContainer.createScope({ tags: ['request'] });
634
- const requestScope2 = appContainer.createScope({ tags: ['request'] });
635
-
636
- // Both scopes can access the ApiService from parent
637
- expect(requestScope1.resolve<ApiService>('service').handleRequest()).toBe('API response');
638
- expect(requestScope2.resolve<ApiService>('service').handleRequest()).toBe('API response');
639
- });
640
-
641
- it('should enable incremental tag addition', () => {
642
- const container = new Container();
643
-
644
- // Start with basic tags
645
- container.addTags('application');
646
- expect(container.hasTag('application')).toBe(true);
647
-
648
- // Add more tags as needed
649
- container.addTags('monitoring', 'logging');
650
- expect(container.hasTag('monitoring')).toBe(true);
651
- expect(container.hasTag('logging')).toBe(true);
652
-
653
- // All tags are retained
654
- expect(container.hasTag('application')).toBe(true);
655
- });
656
- });
657
-
658
- ```
659
-
660
346
  ### Instances
661
347
 
662
348
  Sometimes you want to get all instances from container and its scopes. For example, when you want to dispose all instances of container.
@@ -742,108 +428,6 @@ describe('Instances', function () {
742
428
 
743
429
  ```
744
430
 
745
- ### Check Registration
746
-
747
- Sometimes you want to check if a registration with a specific key exists in the container. This is useful for conditional registration logic, validation, and debugging.
748
-
749
- - `hasRegistration(key)` checks if a registration exists in the current container or parent containers
750
- - Checks both the current container's registrations and parent container registrations
751
- - Works with string keys, symbol keys, and token keys
752
- - Returns false after container disposal
753
-
754
- ```typescript
755
- import { Container, Registration as R, bindTo, register, SingleToken } from 'ts-ioc-container';
756
-
757
- /**
758
- * Container Registration Checking - hasRegistration
759
- *
760
- * The `hasRegistration` method allows you to check if a registration with a specific key
761
- * exists in the current container. This is useful for conditional registration logic,
762
- * validation, and debugging.
763
- *
764
- * Key points:
765
- * - Checks only the current container's registrations (not parent containers)
766
- * - Works with string keys, symbol keys, and token keys
767
- * - Returns false after container disposal
768
- * - Useful for conditional registration patterns
769
- */
770
- describe('hasRegistration', function () {
771
- const createAppContainer = () => new Container({ tags: ['application'] });
772
-
773
- it('should return true when registration exists with string key', function () {
774
- const container = createAppContainer();
775
- container.addRegistration(R.fromValue('production').bindToKey('Environment'));
776
-
777
- expect(container.hasRegistration('Environment')).toBe(true);
778
- });
779
-
780
- it('should return false when registration does not exist', function () {
781
- const container = createAppContainer();
782
-
783
- expect(container.hasRegistration('NonExistentService')).toBe(false);
784
- });
785
-
786
- it('should work with symbol keys', function () {
787
- const container = createAppContainer();
788
- const serviceKey = Symbol('IService');
789
- container.addRegistration(R.fromValue({ name: 'Service' }).bindToKey(serviceKey));
790
-
791
- expect(container.hasRegistration(serviceKey)).toBe(true);
792
- });
793
-
794
- it('should work with token keys', function () {
795
- const container = createAppContainer();
796
- const loggerToken = new SingleToken<{ log: (msg: string) => void }>('ILogger');
797
- container.addRegistration(R.fromValue({ log: () => {} }).bindTo(loggerToken));
798
-
799
- expect(container.hasRegistration(loggerToken.token)).toBe(true);
800
- });
801
-
802
- it('should check current container and parent registrations', function () {
803
- // Parent container has a registration
804
- const parent = createAppContainer();
805
- parent.addRegistration(R.fromValue('parent-config').bindToKey('Config'));
806
-
807
- // Child scope does not have the registration
808
- const child = parent.createScope();
809
- child.addRegistration(R.fromValue('child-service').bindToKey('Service'));
810
-
811
- // Child should see parent's registration (checks parent as well)
812
- expect(child.hasRegistration('Config')).toBe(true);
813
- // Child should see its own registration
814
- expect(child.hasRegistration('Service')).toBe(true);
815
- // Parent should see its own registration
816
- expect(parent.hasRegistration('Config')).toBe(true);
817
- });
818
-
819
- it('should work with class-based registrations', function () {
820
- @register(bindTo('ILogger'))
821
- class Logger {}
822
-
823
- const container = createAppContainer();
824
- container.addRegistration(R.fromClass(Logger));
825
-
826
- expect(container.hasRegistration('ILogger')).toBe(true);
827
- });
828
-
829
- it('should be useful for conditional registration patterns', function () {
830
- const container = createAppContainer();
831
-
832
- // Register a base service
833
- container.addRegistration(R.fromValue('base-service').bindToKey('BaseService'));
834
-
835
- // Conditionally register an extension only if base exists
836
- if (container.hasRegistration('BaseService')) {
837
- container.addRegistration(R.fromValue('extension-service').bindToKey('ExtensionService'));
838
- }
839
-
840
- expect(container.hasRegistration('BaseService')).toBe(true);
841
- expect(container.hasRegistration('ExtensionService')).toBe(true);
842
- });
843
- });
844
-
845
- ```
846
-
847
431
  ### Dispose
848
432
 
849
433
  Sometimes you want to dispose a container or scope. For example, when a request, page, widget, or other local lifecycle ends.
@@ -2576,6 +2160,9 @@ Sometimes you want to register provider with certain key. This is what `key` is
2576
2160
  - by default, key is class name
2577
2161
  - you can assign the same key to different registrations
2578
2162
 
2163
+ > [!TIP]
2164
+ > Prefer `SingleToken<T>` over plain string literals as registration keys. Tokens are type-safe, rename-friendly, and prevent typos that only surface at runtime.
2165
+
2579
2166
  ```typescript
2580
2167
  import {
2581
2168
  bindTo,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ts-ioc-container",
3
- "version": "50.2.1",
3
+ "version": "50.2.3",
4
4
  "description": "Fast, lightweight TypeScript dependency injection container with a clean API, scoped lifecycles, decorators, tokens, hooks, lazy injection, customizable providers, and no global container objects.",
5
5
  "workspaces": [
6
6
  "docs"