ts-ioc-container 49.1.0 → 50.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -36,7 +36,7 @@ no global container objects.
36
36
  - [Setup](#setup)
37
37
  - [Quickstart](#quickstart)
38
38
  - [Cheatsheet](#cheatsheet)
39
- - [Spec-driven workflow](#spec-driven-workflow)
39
+ - [Specs-driven workflow](#specs-driven-workflow)
40
40
  - [Product capability map](#product-capability-map)
41
41
  - [Acceptance specs](#acceptance-specs)
42
42
  - [tsyringe alternative](https://igorbabkin.github.io/ts-ioc-container/tsyringe-alternative)
@@ -144,7 +144,7 @@ container.resolve(App).start();
144
144
  > for `R.fromValue(...)` and `R.fromFn(...)` (which have no class to decorate)
145
145
  > or for third-party classes you don't own.
146
146
 
147
- ## Spec-driven workflow
147
+ ## Specs-driven workflow
148
148
 
149
149
  Public behavior is described as product capabilities before it is implemented.
150
150
  The repository keeps the same chain visible in specs, tests, docs, and this
@@ -184,49 +184,203 @@ tests stay in the feature folders under `__tests__/`.
184
184
 
185
185
  ### Express/Next handler (per-request scope)
186
186
 
187
- ```typescript
188
- const app = new Container({ tags: ['application'] }).addRegistration(R.fromClass(Logger).pipe(singleton()));
187
+ import 'reflect-metadata';
188
+ import { bindTo, Container, inject, register, Registration as R, singleton } from 'ts-ioc-container';
189
+
190
+ /**
191
+ * Web Framework Integration - Per-Request Scope
192
+ *
193
+ * In Express/Next.js applications, each HTTP request typically gets its own
194
+ * scope. This ensures request-specific state (logger context, current user,
195
+ * correlation IDs) is isolated between concurrent requests.
196
+ *
197
+ * Scope hierarchy:
198
+ * Application (singleton services — live for entire app lifetime)
199
+ * └── Request (per-request services — created and disposed per request)
200
+ */
189
201
 
190
- function handleRequest() {
191
- const requestScope = app.createScope({ tags: ['request'] });
192
- const logger = requestScope.resolve<Logger>('Logger');
193
- logger.log('req started');
202
+ @register(bindTo('ILogger'), singleton())
203
+ class Logger {
204
+ readonly messages: string[] = [];
205
+
206
+ log(message: string) {
207
+ this.messages.push(message);
208
+ }
194
209
  }
195
- ```
210
+
211
+ describe('Express/Next per-request scope', () => {
212
+ it('should give each request its own Logger instance', () => {
213
+ const app = new Container({ tags: ['application'] }).addRegistration(R.fromClass(Logger));
214
+
215
+ // Simulate two concurrent HTTP requests
216
+ const request1Scope = app.createScope({ tags: ['request'] });
217
+ const request2Scope = app.createScope({ tags: ['request'] });
218
+
219
+ const logger1 = request1Scope.resolve<Logger>('ILogger');
220
+ const logger2 = request2Scope.resolve<Logger>('ILogger');
221
+
222
+ logger1.log('req 1 started');
223
+ logger2.log('req 2 started');
224
+
225
+ // Each request has its own Logger — logs don't leak between requests
226
+ expect(logger1.messages).toEqual(['req 1 started']);
227
+ expect(logger2.messages).toEqual(['req 2 started']);
228
+ expect(logger1).not.toBe(logger2);
229
+ });
230
+
231
+ it('should resolve the same Logger within a single request', () => {
232
+ const app = new Container({ tags: ['application'] }).addRegistration(R.fromClass(Logger));
233
+
234
+ const requestScope = app.createScope({ tags: ['request'] });
235
+
236
+ const logger1 = requestScope.resolve<Logger>('ILogger');
237
+ const logger2 = requestScope.resolve<Logger>('ILogger');
238
+
239
+ // Within one request, singleton is maintained
240
+ expect(logger1).toBe(logger2);
241
+ });
242
+ });
243
+
196
244
 
197
245
  ### Background worker (singleton client, transient jobs)
198
246
 
199
- ```typescript
247
+ import 'reflect-metadata';
248
+ import { Container, inject, register, Registration as R, singleton } from 'ts-ioc-container';
249
+
250
+ /**
251
+ * Background Worker - Singleton Client, Transient Jobs
252
+ *
253
+ * A queue worker typically needs:
254
+ * - A single shared QueueClient (expensive to create, holds connections)
255
+ * - A new JobHandler per job (stateful — holds job-specific data)
256
+ *
257
+ * singleton() on QueueClient ensures one shared connection pool.
258
+ * No singleton on JobHandler gives a fresh instance per resolve.
259
+ */
260
+
200
261
  @register(singleton())
201
- class QueueClient {}
262
+ class QueueClient {
263
+ readonly connected = true;
264
+
265
+ dequeue(): string {
266
+ return 'job-payload';
267
+ }
268
+ }
202
269
 
203
270
  class JobHandler {
204
- constructor(@inject('QueueClient') private queue: QueueClient) {}
271
+ readonly result: string;
272
+
273
+ constructor(@inject('QueueClient') private queue: QueueClient) {
274
+ this.result = this.queue.dequeue();
275
+ }
205
276
  }
206
277
 
207
- const worker = new Container({ tags: ['worker'] })
208
- .addRegistration(R.fromClass(QueueClient))
209
- .addRegistration(R.fromClass(JobHandler));
210
- ```
278
+ describe('Background worker', () => {
279
+ function createWorker() {
280
+ return new Container({ tags: ['worker'] })
281
+ .addRegistration(R.fromClass(QueueClient))
282
+ .addRegistration(R.fromClass(JobHandler));
283
+ }
284
+
285
+ it('should share a single QueueClient across all job handlers', () => {
286
+ const worker = createWorker();
287
+
288
+ const handler1 = worker.resolve(JobHandler);
289
+ const handler2 = worker.resolve(JobHandler);
290
+
291
+ const client1 = worker.resolve<QueueClient>('QueueClient');
292
+ const client2 = worker.resolve<QueueClient>('QueueClient');
293
+
294
+ // QueueClient is a singleton — same connection shared everywhere
295
+ expect(client1).toBe(client2);
296
+ expect(client1.connected).toBe(true);
297
+
298
+ // JobHandler is transient — fresh instance per job
299
+ expect(handler1).not.toBe(handler2);
300
+ });
301
+
302
+ it('should inject the shared QueueClient into each JobHandler', () => {
303
+ const worker = createWorker();
304
+
305
+ const handler = worker.resolve(JobHandler);
306
+
307
+ expect(handler.result).toBe('job-payload');
308
+ });
309
+ });
310
+
211
311
 
212
312
  ### Frontend widget/page scope with lazy dependency
213
313
 
214
- ```typescript
314
+ import 'reflect-metadata';
315
+ import { bindTo, Container, inject, register, Registration as R, select, singleton } from 'ts-ioc-container';
316
+
317
+ /**
318
+ * Frontend Widget - Page Scope with Lazy Dependency
319
+ *
320
+ * In frontend applications, feature flags are fetched once per page load
321
+ * (singleton per page scope) but a widget may not need them on every render.
322
+ * Lazy injection defers instantiation until the widget actually reads the flags,
323
+ * avoiding unnecessary work for widgets that never display flag-gated content.
324
+ *
325
+ * Scope hierarchy:
326
+ * Application
327
+ * └── Page (singleton flags fetched once)
328
+ * └── Widget (lazy flag access)
329
+ */
330
+
215
331
  @register(bindTo('FeatureFlags'), singleton())
216
332
  class FeatureFlags {
217
- load() {
218
- /* fetch flags */
333
+ load(): Record<string, boolean> {
334
+ return { newDashboard: true };
219
335
  }
220
336
  }
221
337
 
222
338
  class Widget {
223
- constructor(@inject(select.token('FeatureFlags').lazy()) private flags: FeatureFlags) {}
339
+ constructor(@inject(select.token('FeatureFlags').lazy()) public flags: FeatureFlags) {}
224
340
  }
225
341
 
226
- const page = new Container({ tags: ['page'] })
227
- .addRegistration(R.fromClass(FeatureFlags))
228
- .addRegistration(R.fromClass(Widget));
229
- ```
342
+ describe('Frontend widget/page scope with lazy dependency', () => {
343
+ function createPage() {
344
+ return new Container({ tags: ['page'] })
345
+ .addRegistration(R.fromClass(FeatureFlags))
346
+ .addRegistration(R.fromClass(Widget));
347
+ }
348
+
349
+ it('should not instantiate FeatureFlags until the widget actually accesses it', () => {
350
+ const page = createPage();
351
+
352
+ const widget = page.resolve(Widget);
353
+
354
+ // Widget is resolved, but FeatureFlags has not been instantiated yet
355
+ let instances = Array.from(page.getInstances()).filter((x) => x instanceof FeatureFlags);
356
+ expect(instances).toHaveLength(0);
357
+
358
+ // Accessing any property on the lazy proxy triggers instantiation
359
+ const _load = widget.flags.load;
360
+ expect(_load).toBeDefined();
361
+
362
+ instances = Array.from(page.getInstances()).filter((x) => x instanceof FeatureFlags);
363
+ expect(instances).toHaveLength(1);
364
+ });
365
+
366
+ it('should share the same FeatureFlags singleton across widgets on the same page', () => {
367
+ const page = createPage();
368
+
369
+ const widget1 = page.resolve(Widget);
370
+ const widget2 = page.resolve(Widget);
371
+
372
+ // Trigger instantiation through both widgets
373
+ const _load1 = widget1.flags.load;
374
+ const _load2 = widget2.flags.load;
375
+ expect(_load1).toBeDefined();
376
+ expect(_load2).toBeDefined();
377
+
378
+ // Only one FeatureFlags instance was created across the whole page scope
379
+ const instances = Array.from(page.getInstances()).filter((x) => x instanceof FeatureFlags);
380
+ expect(instances).toHaveLength(1);
381
+ });
382
+ });
383
+
230
384
 
231
385
  ## Container
232
386
 
@@ -1461,51 +1615,22 @@ This type of injector just passes container to constructor with others arguments
1461
1615
  ```typescript
1462
1616
  import { bindTo, Container, type IContainer, register, Registration as R, SimpleInjector } from 'ts-ioc-container';
1463
1617
 
1464
- /**
1465
- * Command Pattern - Simple Injector
1466
- *
1467
- * The SimpleInjector passes the container itself as the first argument to the constructor.
1468
- * This is useful for:
1469
- * - Service Locators (like Command Dispatchers or Routers)
1470
- * - Factory classes that need to resolve dependencies dynamically
1471
- * - Legacy code migration where passing the container is common
1472
- *
1473
- * In this example, a CommandDispatcher uses the container to dynamically
1474
- * resolve the correct handler for each command type.
1475
- */
1476
-
1477
- interface ICommand {
1478
- type: string;
1479
- }
1480
-
1481
- interface ICommandHandler {
1482
- handle(command: ICommand): string;
1483
- }
1484
-
1485
- class CreateUserCommand implements ICommand {
1486
- readonly type = 'CreateUser';
1487
- constructor(readonly username: string) {}
1488
- }
1489
-
1490
1618
  @register(bindTo('HandlerCreateUser'))
1491
- class CreateUserHandler implements ICommandHandler {
1492
- handle(command: CreateUserCommand): string {
1493
- return `User ${command.username} created`;
1619
+ class CreateUserHandler {
1620
+ handle(username: string): string {
1621
+ return `User ${username} created`;
1494
1622
  }
1495
1623
  }
1496
1624
 
1497
1625
  describe('SimpleInjector', function () {
1498
- it('should inject container to allow dynamic resolution (Service Locator pattern)', function () {
1499
- // Dispatcher needs the container to find handlers dynamically based on command type
1626
+ it('should inject container to allow dynamic resolution', function () {
1500
1627
  @register(bindTo('Dispatcher'))
1501
1628
  class CommandDispatcher {
1502
1629
  constructor(private container: IContainer) {}
1503
1630
 
1504
- dispatch(command: ICommand): string {
1505
- // Dynamically resolve handler: "Handler" + "CreateUser"
1506
- const handlerKey = `Handler${command.type}`;
1507
- const handler = this.container.resolve<ICommandHandler>(handlerKey);
1508
- return handler.handle(command);
1631
+ dispatch(type: string, payload: string): string {
1632
+ const handler = this.container.resolve<CreateUserHandler>(`Handler${type}`);
1633
+ return handler.handle(payload);
1509
1634
  }
1510
1635
  }
1511
1636
 
@@ -1514,30 +1639,27 @@ describe('SimpleInjector', function () {
1514
1639
  .addRegistration(R.fromClass(CreateUserHandler));
1515
1640
 
1516
1641
  const dispatcher = container.resolve<CommandDispatcher>('Dispatcher');
1517
- const result = dispatcher.dispatch(new CreateUserCommand('alice'));
1518
1642
 
1519
- expect(result).toBe('User alice created');
1643
+ expect(dispatcher.dispatch('CreateUser', 'alice')).toBe('User alice created');
1520
1644
  });
1521
1645
 
1522
1646
  it('should pass additional arguments alongside the container', function () {
1523
- // Factory that creates widgets with a specific theme
1524
1647
  class WidgetFactory {
1525
1648
  constructor(
1526
1649
  private container: IContainer,
1527
- private theme: string, // Passed as argument during resolve
1650
+ private theme: string,
1528
1651
  ) {}
1529
1652
 
1530
1653
  createWidget(name: string): string {
1531
- return `Widget ${name} with ${this.theme} theme (Container available: ${!!this.container})`;
1654
+ return `Widget ${name} with ${this.theme} theme (container: ${!!this.container})`;
1532
1655
  }
1533
1656
  }
1534
1657
 
1535
1658
  const container = new Container({ injector: new SimpleInjector() }).addRegistration(R.fromClass(WidgetFactory));
1536
1659
 
1537
- // Pass "dark" as the theme argument
1538
1660
  const factory = container.resolve<WidgetFactory>('WidgetFactory', { args: ['dark'] });
1539
1661
 
1540
- expect(factory.createWidget('Button')).toBe('Widget Button with dark theme (Container available: true)');
1662
+ expect(factory.createWidget('Button')).toBe('Widget Button with dark theme (container: true)');
1541
1663
  });
1542
1664
  });
1543
1665
 
@@ -1562,16 +1684,11 @@ describe('ProxyInjector', function () {
1562
1684
  }
1563
1685
  }
1564
1686
 
1565
- interface UserControllerDeps {
1566
- logger: Logger;
1567
- prefix: string;
1568
- }
1569
-
1570
1687
  class UserController {
1571
1688
  private logger: Logger;
1572
1689
  private prefix: string;
1573
1690
 
1574
- constructor({ logger, prefix }: UserControllerDeps) {
1691
+ constructor({ logger, prefix }: { logger: Logger; prefix: string }) {
1575
1692
  this.logger = logger;
1576
1693
  this.prefix = prefix;
1577
1694
  }
@@ -1586,21 +1703,14 @@ describe('ProxyInjector', function () {
1586
1703
  .addRegistration(R.fromValue('USER:').bindToKey('prefix'))
1587
1704
  .addRegistration(R.fromClass(UserController));
1588
1705
 
1589
- const controller = container.resolve<UserController>('UserController');
1590
-
1591
- expect(controller.createUser('bob')).toBe('Logged: USER: bob');
1706
+ expect(container.resolve<UserController>('UserController').createUser('bob')).toBe('Logged: USER: bob');
1592
1707
  });
1593
1708
 
1594
1709
  it('should expose runtime args through the reserved "args" property', function () {
1595
- @register(bindTo('database'))
1596
- class Database {}
1597
-
1598
1710
  class ReportGenerator {
1599
- database: Database;
1600
1711
  format: string;
1601
1712
 
1602
- constructor({ database, args }: { database: Database; args: string[] }) {
1603
- this.database = database;
1713
+ constructor({ args }: { args: string[] }) {
1604
1714
  this.format = args[0];
1605
1715
  }
1606
1716
 
@@ -1609,40 +1719,12 @@ describe('ProxyInjector', function () {
1609
1719
  }
1610
1720
  }
1611
1721
 
1612
- const container = new Container({ injector: new ProxyInjector() })
1613
- .addRegistration(R.fromClass(Database))
1614
- .addRegistration(R.fromClass(ReportGenerator));
1722
+ const container = new Container({ injector: new ProxyInjector() }).addRegistration(R.fromClass(ReportGenerator));
1615
1723
 
1616
- const generator = container.resolve<ReportGenerator>('ReportGenerator', {
1617
- args: ['PDF'],
1618
- });
1724
+ const generator = container.resolve<ReportGenerator>('ReportGenerator', { args: ['PDF'] });
1619
1725
 
1620
- expect(generator.database).toBeInstanceOf(Database);
1621
1726
  expect(generator.generate()).toBe('Report in PDF');
1622
1727
  });
1623
-
1624
- it('should resolve dependencies by alias when property name contains "alias"', function () {
1625
- class FileLogger {}
1626
- class ConsoleLogger {}
1627
-
1628
- interface AppDeps {
1629
- loggersAlias: any[];
1630
- }
1631
-
1632
- class App {
1633
- constructor(public deps: AppDeps) {}
1634
- }
1635
-
1636
- const container = new Container({ injector: new ProxyInjector() });
1637
-
1638
- const mockLoggers = [new FileLogger(), new ConsoleLogger()];
1639
- container.resolveByAlias = vi.fn().mockReturnValue(mockLoggers);
1640
-
1641
- const app = container.resolve(App);
1642
-
1643
- expect(app.deps.loggersAlias).toBe(mockLoggers);
1644
- expect(container.resolveByAlias).toHaveBeenCalledWith('loggersAlias');
1645
- });
1646
1728
  });
1647
1729
 
1648
1730
  ```
@@ -1898,7 +1980,7 @@ describe('Singleton', function () {
1898
1980
 
1899
1981
  ### Arguments
1900
1982
 
1901
- Sometimes you want to bind some arguments to provider. This is what `ArgsProvider` is for.
1983
+ Sometimes you want to bind some arguments to provider.
1902
1984
 
1903
1985
  - `provider(setArgs('someArgument'))`
1904
1986
  - `provider(setArgsFn((container) => [container.resolve(Logger), 'someValue']))`
@@ -1934,17 +2016,18 @@ const userToken = ApiToken.args('https://users.api.com', 1000);
1934
2016
 
1935
2017
  ```typescript
1936
2018
  import {
1937
- setArgs,
1938
- setArgsFn,
2019
+ addArgs,
2020
+ addArgsFn,
2021
+ args,
1939
2022
  bindTo,
1940
2023
  Container,
1941
2024
  inject,
1942
- args,
1943
- MultiCache,
1944
2025
  register,
1945
2026
  Registration as R,
1946
- singleton,
2027
+ setArgs,
2028
+ setArgsFn,
1947
2029
  SingleToken,
2030
+ singleton,
1948
2031
  } from 'ts-ioc-container';
1949
2032
 
1950
2033
  /**
@@ -1957,7 +2040,7 @@ import {
1957
2040
  * - Generic classes (like Repositories) that need to know what they are managing
1958
2041
  */
1959
2042
 
1960
- describe('ArgsProvider', function () {
2043
+ describe('IProvider', function () {
1961
2044
  function createContainer() {
1962
2045
  return new Container();
1963
2046
  }
@@ -2014,6 +2097,49 @@ describe('ArgsProvider', function () {
2014
2097
  });
2015
2098
  });
2016
2099
 
2100
+ describe('Appending Arguments', () => {
2101
+ it('can append static arguments after existing resolve arguments', function () {
2102
+ @register(addArgs('configured'))
2103
+ class Service {
2104
+ constructor(
2105
+ @inject(args(0)) public runtime: string,
2106
+ @inject(args(1)) public configured: string,
2107
+ ) {}
2108
+ }
2109
+
2110
+ const root = createContainer().addRegistration(R.fromClass(Service));
2111
+
2112
+ const service = root.resolve<Service>('Service', { args: ['runtime'] });
2113
+ expect(service.runtime).toBe('runtime');
2114
+ expect(service.configured).toBe('configured');
2115
+ });
2116
+
2117
+ it('can append dynamic arguments after an existing argsFn', function () {
2118
+ class Config {
2119
+ tenant = 'tenant-a';
2120
+ }
2121
+
2122
+ @register(
2123
+ setArgs('fixed'),
2124
+ addArgsFn((scope, { args = [] } = {}) => [scope.resolve<Config>('Config').tenant, ...args]),
2125
+ )
2126
+ class Service {
2127
+ constructor(
2128
+ @inject(args(0)) public fixed: string,
2129
+ @inject(args(1)) public tenant: string,
2130
+ @inject(args(2)) public runtime: string,
2131
+ ) {}
2132
+ }
2133
+
2134
+ const root = createContainer().addRegistration(R.fromClass(Config)).addRegistration(R.fromClass(Service));
2135
+
2136
+ const service = root.resolve<Service>('Service', { args: ['runtime'] });
2137
+ expect(service.fixed).toBe('fixed');
2138
+ expect(service.tenant).toBe('tenant-a');
2139
+ expect(service.runtime).toBe('runtime');
2140
+ });
2141
+ });
2142
+
2017
2143
  describe('Generic Repositories (Advanced Pattern)', () => {
2018
2144
  // This example demonstrates how to implement the Generic Repository pattern
2019
2145
  // where a generic EntityManager needs to know WHICH repository to use.
@@ -2044,7 +2170,7 @@ describe('ArgsProvider', function () {
2044
2170
 
2045
2171
  @register(
2046
2172
  bindTo(EntityManagerToken),
2047
- singleton(MultiCache.fromFirstArg), // Cache unique instance per repository type
2173
+ singleton((arg1) => (arg1 as SingleToken).token), // Cache unique instance per repository type
2048
2174
  )
2049
2175
  class EntityManager {
2050
2176
  constructor(@inject(args(0)) public repository: IRepository) {}
@@ -2574,37 +2700,91 @@ Sometimes you need to register provider only in scope which matches to certain c
2574
2700
  - `Registration.fromClass(Logger).when((container) => container.hasTag('root'))`
2575
2701
 
2576
2702
  ```typescript
2577
- import { bindTo, Container, register, Registration as R, scope, singleton } from 'ts-ioc-container';
2703
+ import 'reflect-metadata';
2704
+ import {
2705
+ bindTo,
2706
+ Container,
2707
+ DependencyNotFoundError,
2708
+ type IContainer,
2709
+ inject,
2710
+ register,
2711
+ Registration as R,
2712
+ scope,
2713
+ select,
2714
+ singleton,
2715
+ } from 'ts-ioc-container';
2578
2716
 
2579
2717
  /**
2580
- * Scoping - Scope Match Rule
2718
+ * User Management Domain - Request Scopes
2581
2719
  *
2582
- * You can restrict WHERE a provider is registered.
2583
- * This is useful for singleton services that should only exist in the root scope,
2584
- * or per-request services that should only exist in request scopes.
2720
+ * In web applications, each HTTP request typically gets its own scope.
2721
+ * This allows request-specific data (current user, request ID, etc.)
2722
+ * to be isolated between concurrent requests.
2723
+ *
2724
+ * Scope hierarchy:
2725
+ * Application (singleton services)
2726
+ * └── Request (per-request services)
2727
+ * └── Transaction (database transaction boundary)
2585
2728
  */
2586
2729
 
2587
- describe('ScopeProvider', function () {
2588
- it('should register provider only in matching scope', function () {
2589
- // SharedState should be a singleton in the root 'application' scope
2590
- // It will be visible to all child scopes, but physically resides in 'application'
2591
- @register(
2592
- bindTo('SharedState'),
2593
- scope((s) => s.hasTag('application')), // Only register in application scope
2594
- singleton(), // One instance per application
2595
- )
2596
- class SharedState {
2597
- data = 'shared';
2598
- }
2730
+ // SessionService is only available in request scope - not at application level
2731
+ @register(bindTo('ISessionService'), scope((s) => s.hasTag('request')), singleton())
2732
+ class SessionService {
2733
+ private userId: string | null = null;
2599
2734
 
2600
- const appContainer = new Container({ tags: ['application'] }).addRegistration(R.fromClass(SharedState));
2601
- const requestScope = appContainer.createScope({ tags: ['request'] });
2735
+ setCurrentUser(userId: string) {
2736
+ this.userId = userId;
2737
+ }
2602
2738
 
2603
- // Both resolve to the SAME instance because it's a singleton in the app scope
2604
- const appState = appContainer.resolve('SharedState');
2605
- const requestState = requestScope.resolve('SharedState');
2739
+ getCurrentUserId(): string | null {
2740
+ return this.userId;
2741
+ }
2742
+ }
2606
2743
 
2607
- expect(appState).toBe(requestState);
2744
+ describe('Scopes', function () {
2745
+ it('should isolate request-scoped services', function () {
2746
+ // Application container - lives for entire app lifetime
2747
+ const appContainer = new Container({ tags: ['application'] }).addRegistration(R.fromClass(SessionService));
2748
+
2749
+ // Simulate two concurrent HTTP requests
2750
+ const request1Scope = appContainer.createScope({ tags: ['request'] });
2751
+ const request2Scope = appContainer.createScope({ tags: ['request'] });
2752
+
2753
+ // Each request has its own SessionService instance
2754
+ const session1 = request1Scope.resolve<SessionService>('ISessionService');
2755
+ const session2 = request2Scope.resolve<SessionService>('ISessionService');
2756
+
2757
+ session1.setCurrentUser('user-1');
2758
+ session2.setCurrentUser('user-2');
2759
+
2760
+ // Sessions are isolated - user data doesn't leak between requests
2761
+ expect(session1.getCurrentUserId()).toBe('user-1');
2762
+ expect(session2.getCurrentUserId()).toBe('user-2');
2763
+ expect(session1).not.toBe(session2);
2764
+
2765
+ // SessionService is NOT available at application level (security!)
2766
+ expect(() => appContainer.resolve('ISessionService')).toThrow(DependencyNotFoundError);
2767
+ });
2768
+
2769
+ it('should create child scopes for transactions', function () {
2770
+ const appContainer = new Container({ tags: ['application'] });
2771
+
2772
+ // RequestHandler can create a transaction scope for database operations
2773
+ class RequestHandler {
2774
+ constructor(@inject(select.scope.create({ tags: ['transaction'] })) public transactionScope: IContainer) {}
2775
+
2776
+ executeInTransaction(): boolean {
2777
+ // Transaction scope inherits from request scope
2778
+ // Database operations can be rolled back together
2779
+ return this.transactionScope.hasTag('transaction');
2780
+ }
2781
+ }
2782
+
2783
+ const handler = appContainer.resolve(RequestHandler);
2784
+
2785
+ expect(handler.transactionScope).not.toBe(appContainer);
2786
+ expect(handler.transactionScope.hasTag('transaction')).toBe(true);
2787
+ expect(handler.executeInTransaction()).toBe(true);
2608
2788
  });
2609
2789
  });
2610
2790
 
@@ -2741,30 +2921,10 @@ Sometimes you need to invoke methods after construct or dispose of class. This i
2741
2921
  ### OnConstruct
2742
2922
 
2743
2923
  ```typescript
2744
- import {
2745
- AddOnConstructHookModule,
2746
- Container,
2747
- IHookContext,
2748
- HookFn,
2749
- inject,
2750
- onConstruct,
2751
- Registration as R,
2752
- } from 'ts-ioc-container';
2753
-
2754
- /**
2755
- * Lifecycle - OnConstruct Hook
2756
- *
2757
- * The @onConstruct hook allows you to run logic immediately after an object is created.
2758
- * This is useful for:
2759
- * - Initialization logic that depends on injected services
2760
- * - Setting up event listeners
2761
- * - Establishing connections (though lazy is often better)
2762
- * - Computing initial state
2763
- *
2764
- * Note: You must register the AddOnConstructHookModule or manually add the hook runner.
2765
- */
2924
+ import 'reflect-metadata';
2925
+ import { AddOnConstructHookModule, Container, type HookFn, inject, onConstruct, Registration as R } from 'ts-ioc-container';
2766
2926
 
2767
- const execute: HookFn = (ctx: IHookContext) => {
2927
+ const execute: HookFn = (ctx) => {
2768
2928
  ctx.invokeMethod({ args: ctx.resolveArgs() });
2769
2929
  };
2770
2930
 
@@ -2774,8 +2934,6 @@ describe('onConstruct', function () {
2774
2934
  isConnected = false;
2775
2935
  connectionString = '';
2776
2936
 
2777
- // @onConstruct marks this method to be called after instantiation
2778
- // Arguments are resolved from the container like constructor params
2779
2937
  @onConstruct(execute)
2780
2938
  connect(@inject('ConnectionString') connectionString: string) {
2781
2939
  this.connectionString = connectionString;
@@ -2784,12 +2942,9 @@ describe('onConstruct', function () {
2784
2942
  }
2785
2943
 
2786
2944
  const container = new Container()
2787
- // Enable @onConstruct support
2788
2945
  .useModule(new AddOnConstructHookModule())
2789
- // Register config
2790
2946
  .addRegistration(R.fromValue('postgres://localhost:5432').bindTo('ConnectionString'));
2791
2947
 
2792
- // Resolve class - constructor is called, then @onConstruct method
2793
2948
  const db = container.resolve(DatabaseConnection);
2794
2949
 
2795
2950
  expect(db.isConnected).toBe(true);
@@ -2802,6 +2957,7 @@ describe('onConstruct', function () {
2802
2957
  ### OnDispose
2803
2958
 
2804
2959
  ```typescript
2960
+ import 'reflect-metadata';
2805
2961
  import {
2806
2962
  AddOnDisposeHookModule,
2807
2963
  bindTo,
@@ -2829,10 +2985,6 @@ class LogsRepo {
2829
2985
 
2830
2986
  @register(bindTo('logger'))
2831
2987
  class Logger {
2832
- @onDispose(({ instance, methodName }) => {
2833
- // @ts-ignore
2834
- instance[methodName].push('world');
2835
- }) // <--- or extract it to @onDispose
2836
2988
  private messages: string[] = [];
2837
2989
 
2838
2990
  constructor(@inject('logsRepo') private logsRepo: LogsRepo) {}
@@ -2848,7 +3000,7 @@ class Logger {
2848
3000
  }
2849
3001
 
2850
3002
  describe('onDispose', function () {
2851
- it('should invoke hooks on all instances', function () {
3003
+ it('should invoke hooks on all instances when container is disposed', function () {
2852
3004
  const container = new Container()
2853
3005
  .useModule(new AddOnDisposeHookModule())
2854
3006
  .addRegistration(R.fromClass(Logger))
@@ -2860,7 +3012,7 @@ describe('onDispose', function () {
2860
3012
 
2861
3013
  container.dispose();
2862
3014
 
2863
- expect(logsRepo.savedLogs.join(',')).toBe('Hello,world');
3015
+ expect(logsRepo.savedLogs).toEqual(['Hello']);
2864
3016
  });
2865
3017
  });
2866
3018
 
package/cjm/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.toSingleAlias = exports.SingleAliasToken = exports.toGroupAlias = exports.GroupAliasToken = exports.InjectionToken = exports.HooksRunner = exports.AddOnDisposeHookModule = exports.onDispose = exports.onDisposeHooksRunner = exports.AddOnConstructHookModule = exports.onConstruct = exports.onConstructHooksRunner = exports.injectProp = exports.createHookContext = exports.createHookContextFactory = exports.HookContext = exports.hasHooks = exports.hook = exports.getHooks = exports.UnexpectedHookResultError = exports.ContainerDisposedError = exports.MethodNotImplementedError = exports.DependencyMissingKeyError = exports.DependencyNotFoundError = exports.Registration = exports.bindTo = exports.register = exports.scope = exports.decorate = exports.MultiCache = exports.multiCache = exports.SingletonProvider = exports.singleton = exports.Provider = exports.ProviderDecorator = exports.setArgs = exports.setArgsFn = exports.lazy = exports.scopeAccess = exports.ProxyInjector = exports.SimpleInjector = exports.MetadataInjector = exports.Injector = exports.argsFn = exports.args = exports.resolveArgs = exports.inject = exports.EmptyContainer = exports.Container = exports.isDependencyKey = void 0;
3
+ exports.toSingleAlias = exports.SingleAliasToken = exports.toGroupAlias = exports.GroupAliasToken = exports.InjectionToken = exports.HooksRunner = exports.AddOnDisposeHookModule = exports.onDispose = exports.onDisposeHooksRunner = exports.AddOnConstructHookModule = exports.onConstruct = exports.onConstructHooksRunner = exports.injectProp = exports.createHookContext = exports.createHookContextFactory = exports.HookContext = exports.hasHooks = exports.hook = exports.getHooks = exports.UnexpectedHookResultError = exports.ContainerDisposedError = exports.MethodNotImplementedError = exports.DependencyMissingKeyError = exports.DependencyNotFoundError = exports.Registration = exports.bindTo = exports.register = exports.scope = exports.decorate = exports.SingletonProvider = exports.singleton = exports.Provider = exports.ProviderDecorator = exports.setArgs = exports.setArgsFn = exports.addArgsFn = exports.addArgs = exports.lazy = exports.scopeAccess = exports.ProxyInjector = exports.SimpleInjector = exports.MetadataInjector = exports.Injector = exports.argsFn = exports.args = exports.resolveArgs = exports.inject = exports.EmptyContainer = exports.Container = exports.isDependencyKey = void 0;
4
4
  exports.resolveConstructor = exports.Is = exports.pipe = exports.select = exports.once = exports.shallowCache = exports.debounce = exports.throttle = exports.handleAsyncError = exports.handleError = exports.getMethodTags = exports.methodTag = exports.getMethodLabels = exports.methodLabel = exports.getMethodMeta = exports.methodMeta = exports.getParamTags = exports.paramTag = exports.getParamLabels = exports.paramLabel = exports.getParamMeta = exports.paramMeta = exports.getClassTags = exports.classTag = exports.getClassLabels = exports.classLabel = exports.getClassMeta = exports.classMeta = exports.GroupInstanceToken = exports.ConstantToken = exports.FunctionToken = exports.SingleToken = exports.ClassToken = void 0;
5
5
  // Containers
6
6
  var IContainer_1 = require("./container/IContainer");
@@ -27,6 +27,8 @@ Object.defineProperty(exports, "ProxyInjector", { enumerable: true, get: functio
27
27
  var IProvider_1 = require("./provider/IProvider");
28
28
  Object.defineProperty(exports, "scopeAccess", { enumerable: true, get: function () { return IProvider_1.scopeAccess; } });
29
29
  Object.defineProperty(exports, "lazy", { enumerable: true, get: function () { return IProvider_1.lazy; } });
30
+ Object.defineProperty(exports, "addArgs", { enumerable: true, get: function () { return IProvider_1.addArgs; } });
31
+ Object.defineProperty(exports, "addArgsFn", { enumerable: true, get: function () { return IProvider_1.addArgsFn; } });
30
32
  Object.defineProperty(exports, "setArgsFn", { enumerable: true, get: function () { return IProvider_1.setArgsFn; } });
31
33
  Object.defineProperty(exports, "setArgs", { enumerable: true, get: function () { return IProvider_1.setArgs; } });
32
34
  Object.defineProperty(exports, "ProviderDecorator", { enumerable: true, get: function () { return IProvider_1.ProviderDecorator; } });
@@ -35,9 +37,6 @@ Object.defineProperty(exports, "Provider", { enumerable: true, get: function ()
35
37
  var SingletonProvider_1 = require("./provider/SingletonProvider");
36
38
  Object.defineProperty(exports, "singleton", { enumerable: true, get: function () { return SingletonProvider_1.singleton; } });
37
39
  Object.defineProperty(exports, "SingletonProvider", { enumerable: true, get: function () { return SingletonProvider_1.SingletonProvider; } });
38
- var Cache_1 = require("./provider/Cache");
39
- Object.defineProperty(exports, "multiCache", { enumerable: true, get: function () { return Cache_1.multiCache; } });
40
- Object.defineProperty(exports, "MultiCache", { enumerable: true, get: function () { return Cache_1.MultiCache; } });
41
40
  var DecoratorProvider_1 = require("./provider/DecoratorProvider");
42
41
  Object.defineProperty(exports, "decorate", { enumerable: true, get: function () { return DecoratorProvider_1.decorate; } });
43
42
  // Registrations
@@ -1,8 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.resolveArgs = exports.argsFn = exports.args = exports.inject = void 0;
4
- const InjectionToken_1 = require("../token/InjectionToken");
5
- const ConstantToken_1 = require("../token/ConstantToken");
6
4
  const toToken_1 = require("../token/toToken");
7
5
  const basic_1 = require("../utils/basic");
8
6
  const parameter_1 = require("../metadata/parameter");
@@ -17,9 +15,8 @@ const args = (index) => (c, { args = [] }) => {
17
15
  exports.args = args;
18
16
  const argsFn = (fn) => (c, options) => fn(...(options.args ?? []));
19
17
  exports.argsFn = argsFn;
20
- const argToToken = (v) => ((0, InjectionToken_1.isInjectionToken)(v) ? v : new ConstantToken_1.ConstantToken(v));
21
18
  const resolveArgs = (Target, methodName) => {
22
19
  const tokens = (0, parameter_1.getParamMeta)(hookMetaKey(methodName), Target);
23
- return (scope, { args = [], lazy }) => tokens.map((fn) => fn.resolve(scope, { args: args.map(argToToken).map((t) => t.resolve(scope)), lazy }));
20
+ return (scope, { args = [], lazy }) => tokens.map((fn) => fn.resolve(scope, { args: args.map(toToken_1.argToToken).map((t) => t.resolve(scope)), lazy }));
24
21
  };
25
22
  exports.resolveArgs = resolveArgs;
@@ -1,11 +1,15 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.ProviderDecorator = exports.lazy = exports.scopeAccess = exports.setArgsFn = exports.setArgs = void 0;
3
+ exports.ProviderDecorator = exports.lazy = exports.scopeAccess = exports.addArgsFn = exports.addArgs = exports.setArgsFn = exports.setArgs = void 0;
4
4
  const ProviderPipe_1 = require("./ProviderPipe");
5
5
  const setArgs = (...extraArgs) => (0, ProviderPipe_1.registerPipe)((p) => p.setArgs(() => extraArgs));
6
6
  exports.setArgs = setArgs;
7
7
  const setArgsFn = (fn) => (0, ProviderPipe_1.registerPipe)((p) => p.setArgs(fn));
8
8
  exports.setArgsFn = setArgsFn;
9
+ const addArgs = (...extraArgs) => (0, ProviderPipe_1.registerPipe)((p) => p.addArgs(...extraArgs));
10
+ exports.addArgs = addArgs;
11
+ const addArgsFn = (fn) => (0, ProviderPipe_1.registerPipe)((p) => p.addArgsFn(fn));
12
+ exports.addArgsFn = addArgsFn;
9
13
  const scopeAccess = (rule) => (0, ProviderPipe_1.registerPipe)((p) => p.setAccessRule(rule));
10
14
  exports.scopeAccess = scopeAccess;
11
15
  const lazy = () => (0, ProviderPipe_1.registerPipe)((p) => p.lazy());
@@ -39,6 +43,14 @@ class ProviderDecorator {
39
43
  this.decorated.setArgs(argsFn);
40
44
  return this;
41
45
  }
46
+ addArgs(...extraArgs) {
47
+ this.decorated.addArgs(...extraArgs);
48
+ return this;
49
+ }
50
+ addArgsFn(argsFn) {
51
+ this.decorated.addArgsFn(argsFn);
52
+ return this;
53
+ }
42
54
  lazy() {
43
55
  this.decorated.lazy();
44
56
  return this;
@@ -42,6 +42,16 @@ class Provider {
42
42
  this.argsFn = argsFn;
43
43
  return this;
44
44
  }
45
+ addArgs(...extraArgs) {
46
+ const parentFn = this.argsFn;
47
+ this.argsFn = (container, options) => [...parentFn(container, options), ...extraArgs];
48
+ return this;
49
+ }
50
+ addArgsFn(argsFn) {
51
+ const parentFn = this.argsFn;
52
+ this.argsFn = (container, options) => [...parentFn(container, options), ...argsFn(container, options)];
53
+ return this;
54
+ }
45
55
  hasAccess(options) {
46
56
  return this.checkAccess(options);
47
57
  }
@@ -2,25 +2,25 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.singleton = exports.SingletonProvider = void 0;
4
4
  const IProvider_1 = require("./IProvider");
5
- const Cache_1 = require("./Cache");
6
5
  const ProviderPipe_1 = require("./ProviderPipe");
7
6
  class SingletonProvider extends IProvider_1.ProviderDecorator {
8
7
  provider;
9
- cache;
10
- constructor(provider, cache) {
8
+ getKey;
9
+ cache = new Map();
10
+ constructor(provider, getKey) {
11
11
  super(provider);
12
12
  this.provider = provider;
13
- this.cache = cache;
13
+ this.getKey = getKey;
14
14
  }
15
15
  resolve(container, options) {
16
16
  const { args = [] } = options;
17
- const key = this.cache.getKey(...args);
18
- if (!this.cache.hasValue(key)) {
19
- this.cache.setValue(key, this.provider.resolve(container, options));
17
+ const key = this.getKey(...args);
18
+ if (!this.cache.has(key)) {
19
+ this.cache.set(key, this.provider.resolve(container, options));
20
20
  }
21
- return this.cache.getValue(key);
21
+ return this.cache.get(key);
22
22
  }
23
23
  }
24
24
  exports.SingletonProvider = SingletonProvider;
25
- const singleton = (cacheProvider) => (0, ProviderPipe_1.registerPipe)((p) => new SingletonProvider(p, cacheProvider ? cacheProvider() : new Cache_1.SingleCache()));
25
+ const singleton = (getCacheKey = () => '1') => (0, ProviderPipe_1.registerPipe)((p) => new SingletonProvider(p, getCacheKey));
26
26
  exports.singleton = singleton;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.toToken = void 0;
3
+ exports.argToToken = exports.toToken = void 0;
4
4
  const IContainer_1 = require("../container/IContainer");
5
5
  const SingleToken_1 = require("./SingleToken");
6
6
  const ClassToken_1 = require("./ClassToken");
@@ -8,6 +8,7 @@ const FunctionToken_1 = require("./FunctionToken");
8
8
  const UnsupportedTokenTypeError_1 = require("../errors/UnsupportedTokenTypeError");
9
9
  const InjectionToken_1 = require("./InjectionToken");
10
10
  const basic_1 = require("../utils/basic");
11
+ const ConstantToken_1 = require("./ConstantToken");
11
12
  const toToken = (token) => {
12
13
  if (token instanceof InjectionToken_1.InjectionToken) {
13
14
  return token;
@@ -24,3 +25,5 @@ const toToken = (token) => {
24
25
  throw new UnsupportedTokenTypeError_1.UnsupportedTokenTypeError(`Unknown token ${token}`);
25
26
  };
26
27
  exports.toToken = toToken;
28
+ const argToToken = (v) => ((0, InjectionToken_1.isInjectionToken)(v) ? v : new ConstantToken_1.ConstantToken(v));
29
+ exports.argToToken = argToToken;
package/esm/index.js CHANGED
@@ -9,10 +9,9 @@ export { MetadataInjector } from './injector/MetadataInjector';
9
9
  export { SimpleInjector } from './injector/SimpleInjector';
10
10
  export { ProxyInjector } from './injector/ProxyInjector';
11
11
  // Providers
12
- export { scopeAccess, lazy, setArgsFn, setArgs, ProviderDecorator, } from './provider/IProvider';
12
+ export { scopeAccess, lazy, addArgs, addArgsFn, setArgsFn, setArgs, ProviderDecorator, } from './provider/IProvider';
13
13
  export { Provider } from './provider/Provider';
14
14
  export { singleton, SingletonProvider } from './provider/SingletonProvider';
15
- export { multiCache, MultiCache } from './provider/Cache';
16
15
  export { decorate } from './provider/DecoratorProvider';
17
16
  // Registrations
18
17
  export { scope, register, bindTo, } from './registration/IRegistration';
@@ -1,6 +1,4 @@
1
- import { isInjectionToken } from '../token/InjectionToken';
2
- import { ConstantToken } from '../token/ConstantToken';
3
- import { toToken } from '../token/toToken';
1
+ import { argToToken, toToken } from '../token/toToken';
4
2
  import { Is } from '../utils/basic';
5
3
  import { getParamMeta, paramMeta } from '../metadata/parameter';
6
4
  const hookMetaKey = (methodName = 'constructor') => `inject:${methodName}`;
@@ -11,7 +9,6 @@ export const args = (index) => (c, { args = [] }) => {
11
9
  return args[index];
12
10
  };
13
11
  export const argsFn = (fn) => (c, options) => fn(...(options.args ?? []));
14
- const argToToken = (v) => (isInjectionToken(v) ? v : new ConstantToken(v));
15
12
  export const resolveArgs = (Target, methodName) => {
16
13
  const tokens = getParamMeta(hookMetaKey(methodName), Target);
17
14
  return (scope, { args = [], lazy }) => tokens.map((fn) => fn.resolve(scope, { args: args.map(argToToken).map((t) => t.resolve(scope)), lazy }));
@@ -1,6 +1,8 @@
1
1
  import { isProviderPipe, registerPipe } from './ProviderPipe';
2
2
  export const setArgs = (...extraArgs) => registerPipe((p) => p.setArgs(() => extraArgs));
3
3
  export const setArgsFn = (fn) => registerPipe((p) => p.setArgs(fn));
4
+ export const addArgs = (...extraArgs) => registerPipe((p) => p.addArgs(...extraArgs));
5
+ export const addArgsFn = (fn) => registerPipe((p) => p.addArgsFn(fn));
4
6
  export const scopeAccess = (rule) => registerPipe((p) => p.setAccessRule(rule));
5
7
  export const lazy = () => registerPipe((p) => p.lazy());
6
8
  export class ProviderDecorator {
@@ -32,6 +34,14 @@ export class ProviderDecorator {
32
34
  this.decorated.setArgs(argsFn);
33
35
  return this;
34
36
  }
37
+ addArgs(...extraArgs) {
38
+ this.decorated.addArgs(...extraArgs);
39
+ return this;
40
+ }
41
+ addArgsFn(argsFn) {
42
+ this.decorated.addArgsFn(argsFn);
43
+ return this;
44
+ }
35
45
  lazy() {
36
46
  this.decorated.lazy();
37
47
  return this;
@@ -39,6 +39,16 @@ export class Provider {
39
39
  this.argsFn = argsFn;
40
40
  return this;
41
41
  }
42
+ addArgs(...extraArgs) {
43
+ const parentFn = this.argsFn;
44
+ this.argsFn = (container, options) => [...parentFn(container, options), ...extraArgs];
45
+ return this;
46
+ }
47
+ addArgsFn(argsFn) {
48
+ const parentFn = this.argsFn;
49
+ this.argsFn = (container, options) => [...parentFn(container, options), ...argsFn(container, options)];
50
+ return this;
51
+ }
42
52
  hasAccess(options) {
43
53
  return this.checkAccess(options);
44
54
  }
@@ -1,21 +1,21 @@
1
1
  import { ProviderDecorator } from './IProvider';
2
- import { SingleCache } from './Cache';
3
2
  import { registerPipe } from './ProviderPipe';
4
3
  export class SingletonProvider extends ProviderDecorator {
5
4
  provider;
6
- cache;
7
- constructor(provider, cache) {
5
+ getKey;
6
+ cache = new Map();
7
+ constructor(provider, getKey) {
8
8
  super(provider);
9
9
  this.provider = provider;
10
- this.cache = cache;
10
+ this.getKey = getKey;
11
11
  }
12
12
  resolve(container, options) {
13
13
  const { args = [] } = options;
14
- const key = this.cache.getKey(...args);
15
- if (!this.cache.hasValue(key)) {
16
- this.cache.setValue(key, this.provider.resolve(container, options));
14
+ const key = this.getKey(...args);
15
+ if (!this.cache.has(key)) {
16
+ this.cache.set(key, this.provider.resolve(container, options));
17
17
  }
18
- return this.cache.getValue(key);
18
+ return this.cache.get(key);
19
19
  }
20
20
  }
21
- export const singleton = (cacheProvider) => registerPipe((p) => new SingletonProvider(p, cacheProvider ? cacheProvider() : new SingleCache()));
21
+ export const singleton = (getCacheKey = () => '1') => registerPipe((p) => new SingletonProvider(p, getCacheKey));
@@ -3,8 +3,9 @@ import { SingleToken } from './SingleToken';
3
3
  import { ClassToken } from './ClassToken';
4
4
  import { FunctionToken } from './FunctionToken';
5
5
  import { UnsupportedTokenTypeError } from '../errors/UnsupportedTokenTypeError';
6
- import { InjectionToken } from './InjectionToken';
6
+ import { InjectionToken, isInjectionToken } from './InjectionToken';
7
7
  import { Is } from '../utils/basic';
8
+ import { ConstantToken } from './ConstantToken';
8
9
  export const toToken = (token) => {
9
10
  if (token instanceof InjectionToken) {
10
11
  return token;
@@ -20,3 +21,4 @@ export const toToken = (token) => {
20
21
  }
21
22
  throw new UnsupportedTokenTypeError(`Unknown token ${token}`);
22
23
  };
24
+ export const argToToken = (v) => (isInjectionToken(v) ? v : new ConstantToken(v));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ts-ioc-container",
3
- "version": "49.1.0",
3
+ "version": "50.1.0",
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"
@@ -90,11 +90,11 @@
90
90
  "@swc/core": "^1.15.24",
91
91
  "@types/node": "^25.5.2",
92
92
  "@typescript-eslint/eslint-plugin": "8.32.1",
93
- "@typescript-eslint/parser": "8.29.1",
93
+ "@typescript-eslint/parser": "8.58.2",
94
94
  "@vitest/coverage-v8": "^4.1.2",
95
95
  "cz-conventional-changelog": "^3.3.0",
96
96
  "eslint": "9.24.0",
97
- "eslint-config-prettier": "10.1.1",
97
+ "eslint-config-prettier": "10.1.8",
98
98
  "eslint-plugin-prettier": "5.2.6",
99
99
  "handlebars": "^4.7.8",
100
100
  "husky": "^9.1.7",
@@ -6,10 +6,9 @@ export { type IInjector, type InjectOptions, type IInjectFnResolver, Injector }
6
6
  export { MetadataInjector } from './injector/MetadataInjector';
7
7
  export { SimpleInjector } from './injector/SimpleInjector';
8
8
  export { ProxyInjector } from './injector/ProxyInjector';
9
- export { type ResolveDependency, type IProvider, scopeAccess, lazy, setArgsFn, setArgs, type ArgsFn, ProviderDecorator, type IMapper, type ProviderOptions, } from './provider/IProvider';
9
+ export { type ResolveDependency, type IProvider, scopeAccess, lazy, addArgs, addArgsFn, setArgsFn, setArgs, type ArgsFn, ProviderDecorator, type IMapper, type ProviderOptions, } from './provider/IProvider';
10
10
  export { Provider } from './provider/Provider';
11
11
  export { singleton, SingletonProvider } from './provider/SingletonProvider';
12
- export { type Cache, multiCache, MultiCache } from './provider/Cache';
13
12
  export { decorate, type DecorateFn } from './provider/DecoratorProvider';
14
13
  export { type ProviderPipe } from './provider/ProviderPipe';
15
14
  export { type IRegistration, type ReturnTypeOfRegistration, scope, register, type ScopeMatchRule, bindTo, } from './registration/IRegistration';
@@ -22,10 +22,14 @@ export interface IProvider<T = any> {
22
22
  pipe(...mappers: (MapFn<IProvider<T>> | ProviderPipe<T>)[]): IProvider<T>;
23
23
  setAccessRule(hasAccessWhen: ScopeAccessRule): this;
24
24
  setArgs(argsFn: ArgsFn): this;
25
+ addArgs(...extraArgs: unknown[]): this;
26
+ addArgsFn(argsFn: ArgsFn): this;
25
27
  lazy(): this;
26
28
  }
27
29
  export declare const setArgs: <T>(...extraArgs: unknown[]) => ProviderPipe<T>;
28
30
  export declare const setArgsFn: <T>(fn: ArgsFn) => ProviderPipe<T>;
31
+ export declare const addArgs: <T>(...extraArgs: unknown[]) => ProviderPipe<T>;
32
+ export declare const addArgsFn: <T>(fn: ArgsFn) => ProviderPipe<T>;
29
33
  export declare const scopeAccess: <T>(rule: ScopeAccessRule) => ProviderPipe<T>;
30
34
  export declare const lazy: <T>() => ProviderPipe<T>;
31
35
  export declare abstract class ProviderDecorator<T> implements IProvider<T> {
@@ -36,5 +40,7 @@ export declare abstract class ProviderDecorator<T> implements IProvider<T> {
36
40
  resolve(container: IContainer, options: ProviderOptions): T;
37
41
  pipe(...mappers: (MapFn<IProvider<T>> | ProviderPipe<T>)[]): IProvider<T>;
38
42
  setArgs(argsFn: ArgsFn): this;
43
+ addArgs(...extraArgs: unknown[]): this;
44
+ addArgsFn(argsFn: ArgsFn): this;
39
45
  lazy(): this;
40
46
  }
@@ -17,5 +17,7 @@ export declare class Provider<T = any> implements IProvider<T> {
17
17
  setAccessRule(predicate: ScopeAccessRule): this;
18
18
  lazy(): this;
19
19
  setArgs(argsFn: ArgsFn): this;
20
+ addArgs(...extraArgs: unknown[]): this;
21
+ addArgsFn(argsFn: ArgsFn): this;
20
22
  hasAccess(options: ScopeAccessOptions): boolean;
21
23
  }
@@ -1,12 +1,14 @@
1
1
  import type { IContainer } from '../container/IContainer';
2
2
  import type { IProvider } from './IProvider';
3
3
  import { ProviderDecorator } from './IProvider';
4
- import type { Cache } from './Cache';
5
4
  import { InjectOptions } from '../injector/IInjector';
5
+ type GetCacheKey = (...args: unknown[]) => string | symbol;
6
6
  export declare class SingletonProvider<T> extends ProviderDecorator<T> {
7
- private readonly provider;
8
- private readonly cache;
9
- constructor(provider: IProvider<T>, cache: Cache<unknown, T>);
7
+ private provider;
8
+ private getKey;
9
+ private cache;
10
+ constructor(provider: IProvider<T>, getKey: GetCacheKey);
10
11
  resolve(container: IContainer, options: InjectOptions): T;
11
12
  }
12
- export declare const singleton: <T = unknown>(cacheProvider?: () => Cache<unknown, T>) => import("./ProviderPipe").ProviderPipe<T>;
13
+ export declare const singleton: <T = unknown>(getCacheKey?: GetCacheKey) => import("./ProviderPipe").ProviderPipe<T>;
14
+ export {};
@@ -3,3 +3,4 @@ import { InjectionToken } from './InjectionToken';
3
3
  import { InjectFn } from '../hooks/hook';
4
4
  import { type constructor } from '../utils/basic';
5
5
  export declare const toToken: <T = any>(token: InjectFn<T> | InjectionToken<T> | DependencyKey | constructor<T>) => InjectionToken<T>;
6
+ export declare const argToToken: (v: unknown) => InjectionToken<unknown>;
@@ -1,41 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.SingleCache = exports.multiCache = exports.MultiCache = void 0;
4
- class MultiCache {
5
- getKey;
6
- instances = new Map();
7
- static fromFirstArg() {
8
- return new MultiCache((key) => key);
9
- }
10
- constructor(getKey = () => '1') {
11
- this.getKey = getKey;
12
- }
13
- hasValue(token) {
14
- return this.instances.has(token);
15
- }
16
- getValue(token) {
17
- return this.instances.get(token);
18
- }
19
- setValue(token, value) {
20
- this.instances.set(token, value);
21
- }
22
- }
23
- exports.MultiCache = MultiCache;
24
- const multiCache = (getKey) => new MultiCache(getKey);
25
- exports.multiCache = multiCache;
26
- class SingleCache {
27
- instance;
28
- getKey(...args) {
29
- return '1';
30
- }
31
- getValue(key) {
32
- return this.instance.value;
33
- }
34
- hasValue(key) {
35
- return this.instance !== undefined;
36
- }
37
- setValue(key, value) {
38
- this.instance = { value };
39
- }
40
- }
41
- exports.SingleCache = SingleCache;
@@ -1,35 +0,0 @@
1
- export class MultiCache {
2
- getKey;
3
- instances = new Map();
4
- static fromFirstArg() {
5
- return new MultiCache((key) => key);
6
- }
7
- constructor(getKey = () => '1') {
8
- this.getKey = getKey;
9
- }
10
- hasValue(token) {
11
- return this.instances.has(token);
12
- }
13
- getValue(token) {
14
- return this.instances.get(token);
15
- }
16
- setValue(token, value) {
17
- this.instances.set(token, value);
18
- }
19
- }
20
- export const multiCache = (getKey) => new MultiCache(getKey);
21
- export class SingleCache {
22
- instance;
23
- getKey(...args) {
24
- return '1';
25
- }
26
- getValue(key) {
27
- return this.instance.value;
28
- }
29
- hasValue(key) {
30
- return this.instance !== undefined;
31
- }
32
- setValue(key, value) {
33
- this.instance = { value };
34
- }
35
- }
@@ -1,24 +0,0 @@
1
- import type { DependencyKey } from '../container/IContainer';
2
- export interface Cache<K, V> {
3
- getKey(...args: unknown[]): K;
4
- hasValue(key: K): boolean;
5
- getValue(key: K): V;
6
- setValue(key: K, value: V): void;
7
- }
8
- export declare class MultiCache<K, V> implements Cache<K, V> {
9
- readonly getKey: (...args: unknown[]) => K;
10
- private instances;
11
- static fromFirstArg(): MultiCache<DependencyKey, unknown>;
12
- constructor(getKey?: (...args: unknown[]) => K);
13
- hasValue(token: K): boolean;
14
- getValue(token: K): V;
15
- setValue(token: K, value: V): void;
16
- }
17
- export declare const multiCache: <K, V>(getKey: (...args: unknown[]) => K) => MultiCache<K, V>;
18
- export declare class SingleCache<V> implements Cache<string, V> {
19
- private instance?;
20
- getKey(...args: unknown[]): string;
21
- getValue(key: string): V;
22
- hasValue(key: string): boolean;
23
- setValue(key: string, value: V): void;
24
- }