katagami 2.0.0 → 2.2.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
@@ -8,22 +8,25 @@ Lightweight TypeScript DI container with full type inference.
8
8
  [![license](https://img.shields.io/npm/l/katagami)](https://github.com/hiroiku/katagami/blob/master/LICENSE)
9
9
  [![bundle size](https://img.shields.io/bundlephobia/minzip/katagami)](https://bundlephobia.com/package/katagami)
10
10
 
11
- > The name comes from 型紙 _(katagami)_ — precision stencil paper used in traditional Japanese dyeing to transfer exact patterns onto fabric. Multiple stencils are layered to compose intricate designs, just as types accumulate through each method-chain call. A stencil needs only paper and a brush, no elaborate machinery likewise, Katagami requires no decorators or metadata mechanisms and works with any build tool out of the box. And like stencils that work across different fabrics and techniques, Katagami adapts across TypeScript and JavaScript, class tokens and PropertyKey tokens a hybrid approach to strict, composable DI.
11
+ > The name comes from 型紙 _(katagami)_ — precision stencil paper used in traditional Japanese dyeing to transfer exact patterns onto fabric. Multiple stencils are layered to compose intricate designs, just as types accumulate through each method-chain call. Each stencil is a self-contained piece chosen only for the current work, the rest left behind just as subpath exports ensure only the code you use enters your bundle. The cut pattern determines exactly where dye passes and where it is blocked, much like Katagami's type system catches misuse at compile time, not at runtime. And a stencil needs only paper and a brush, no elaborate machinery — likewise, Katagami requires no decorators or metadata mechanisms and works with any build tool out of the box.
12
12
 
13
13
  ## Features
14
14
 
15
- | Feature | Description |
16
- | ----------------------------- | -------------------------------------------------------------------------------------------- |
17
- | Full type inference | Types accumulate through method chaining; unregistered tokens are compile-time errors |
18
- | Three lifetimes | Singleton, Transient, and Scoped with child containers |
19
- | Async factories | Promise-returning factories are automatically tracked by the type system |
20
- | Circular dependency detection | Clear error messages with the full cycle path |
21
- | Disposable support | TC39 Explicit Resource Management (`Symbol.dispose` / `Symbol.asyncDispose` / `await using`) |
22
- | Captive dependency prevention | Singleton/Transient factories cannot access scoped tokens; caught at compile time |
23
- | Optional resolution | `tryResolve` returns `undefined` for unregistered tokens instead of throwing |
24
- | Hybrid token strategy | Class tokens for strict type safety, PropertyKey tokens for flexibility |
25
- | Interface type map | Pass an interface to `createContainer<T>()` for order-independent registration |
26
- | Zero dependencies | No decorators, no reflect-metadata, no polyfills |
15
+ | Feature | Description |
16
+ | ----------------------------- | ---------------------------------------------------------------------------------------------------------- |
17
+ | Zero dependencies | No decorators, no reflect-metadata, no polyfills works with any bundler out of the box |
18
+ | Full type inference | Types accumulate through method chaining; unregistered tokens are compile-time errors |
19
+ | Tree-shakeable | Subpath exports (`katagami/scope`, `katagami/disposable`) and `sideEffects: false` for minimal bundle size |
20
+ | Captive dependency prevention | Singleton/Transient factories cannot access scoped tokens; caught at compile time |
21
+ | Hybrid token strategy | Class tokens for strict type safety, PropertyKey tokens for flexibility |
22
+ | Interface type map | Pass an interface to `createContainer<T>()` for order-independent registration |
23
+ | Three lifetimes | Singleton, Transient, and Scoped with child containers |
24
+ | Disposable support | TC39 Explicit Resource Management (`Symbol.dispose` / `Symbol.asyncDispose` / `await using`) |
25
+ | Module composition | Containers can be composed via `use()` to group and reuse registrations |
26
+ | Async factories | Promise-returning factories are automatically tracked by the type system |
27
+ | Circular dependency detection | Clear error messages with the full cycle path |
28
+ | Optional resolution | `tryResolve` returns `undefined` for unregistered tokens instead of throwing |
29
+ | Lazy resolution | Proxy-based deferred instantiation via `lazy()` from `katagami/lazy`; instance created on first access |
27
30
 
28
31
  ## Install
29
32
 
@@ -66,6 +69,20 @@ Most TypeScript DI containers rely on decorators, reflect-metadata, or string-ba
66
69
 
67
70
  Decorator-based DI requires `experimentalDecorators` and `emitDecoratorMetadata` compiler options. Modern build tools such as esbuild and Vite (default configuration) do not support `emitDecoratorMetadata`, and the TC39 standard decorators proposal does not include an equivalent for automatic type metadata emission. Katagami depends on none of these — it works with any build tool out of the box.
68
71
 
72
+ ### Tree-shakeable
73
+
74
+ Katagami is split into subpath exports. Import only what you use — `katagami/scope`, `katagami/disposable`, and `katagami/lazy` are completely eliminated from the bundle if not imported. Combined with `sideEffects: false`, bundlers can remove every unused byte.
75
+
76
+ ```ts
77
+ // Core only — scope, disposable, and lazy are not included in the bundle
78
+ import { createContainer } from 'katagami';
79
+
80
+ // Import only what you need
81
+ import { createScope } from 'katagami/scope';
82
+ import { disposable } from 'katagami/disposable';
83
+ import { lazy } from 'katagami/lazy';
84
+ ```
85
+
69
86
  ### Full type inference from class tokens
70
87
 
71
88
  String-token DI forces you to maintain manual token-to-type mappings. Parameter-name matching breaks under minification. Katagami uses classes directly as tokens, so `resolve` automatically infers the correct return type — synchronous or `Promise` — with no extra annotations.
@@ -155,6 +172,53 @@ parentScope.resolve(RequestContext) === childScope.resolve(RequestContext); // f
155
172
  parentScope.resolve(DbPool) === childScope.resolve(DbPool); // true
156
173
  ```
157
174
 
175
+ ### Module Composition
176
+
177
+ Group related registrations into a reusable module by creating a container with `createContainer()`, then apply it to another container with `use()`. Only registration entries are copied — singleton instance caches are not shared.
178
+
179
+ ```ts
180
+ import { createContainer } from 'katagami';
181
+
182
+ class AuthService {
183
+ authenticate() {
184
+ return true;
185
+ }
186
+ }
187
+
188
+ class TokenService {
189
+ issue() {
190
+ return 'token';
191
+ }
192
+ }
193
+
194
+ class UserService {
195
+ constructor(private auth: AuthService, private tokens: TokenService) {}
196
+ }
197
+
198
+ // Define a reusable module
199
+ const authModule = createContainer()
200
+ .registerSingleton(AuthService, () => new AuthService())
201
+ .registerSingleton(TokenService, () => new TokenService());
202
+
203
+ // Compose modules
204
+ const container = createContainer()
205
+ .use(authModule)
206
+ .registerSingleton(UserService, r => new UserService(r.resolve(AuthService), r.resolve(TokenService)));
207
+ ```
208
+
209
+ Modules can also compose other modules:
210
+
211
+ ```ts
212
+ const infraModule = createContainer().registerSingleton(AuthService, () => new AuthService());
213
+
214
+ const appModule = createContainer()
215
+ .use(infraModule)
216
+ .registerSingleton(UserService, r => new UserService(r.resolve(AuthService), r.resolve(TokenService)));
217
+
218
+ // appModule includes both AuthService and UserService
219
+ const container = createContainer().use(appModule);
220
+ ```
221
+
158
222
  ### Async Factories
159
223
 
160
224
  Factories that return a `Promise` are automatically tracked by the type system. When you `resolve` an async token, the return type is `Promise<V>` instead of `V`:
@@ -243,9 +307,7 @@ class Connection {
243
307
  }
244
308
 
245
309
  // Manual disposal
246
- const container = disposable(
247
- createContainer().registerSingleton(Connection, () => new Connection()),
248
- );
310
+ const container = disposable(createContainer().registerSingleton(Connection, () => new Connection()));
249
311
 
250
312
  container.resolve(Connection);
251
313
  await container[Symbol.asyncDispose]();
@@ -271,6 +333,70 @@ const root = createContainer()
271
333
 
272
334
  Scope disposal only affects scoped instances. Singleton instances are owned by the root container and are disposed when the container itself is disposed.
273
335
 
336
+ The `disposable()` wrapper also narrows the returned type so that registration methods (`registerSingleton`, `registerTransient`, `registerScoped`, `use`) are removed at the type level. This prevents accidental registration on a potentially-disposed container:
337
+
338
+ ```ts
339
+ const container = disposable(createContainer().registerSingleton(Connection, () => new Connection()));
340
+
341
+ container.resolve(Connection); // OK
342
+ container.registerSingleton(/* ... */); // Compile-time error
343
+ ```
344
+
345
+ ### Lazy Resolution
346
+
347
+ The `lazy()` function from `katagami/lazy` creates a proxy that defers instance creation until the first property access. This is useful for optimizing startup time or breaking circular dependencies.
348
+
349
+ ```ts
350
+ import { createContainer } from 'katagami';
351
+ import { lazy } from 'katagami/lazy';
352
+
353
+ class HeavyService {
354
+ constructor() {
355
+ // expensive initialization
356
+ }
357
+ process() {
358
+ return 'done';
359
+ }
360
+ }
361
+
362
+ const container = createContainer().registerSingleton(HeavyService, () => new HeavyService());
363
+
364
+ const service = lazy(container, HeavyService);
365
+ // HeavyService is NOT instantiated yet
366
+
367
+ service.process(); // instance created here, then cached
368
+ service.process(); // uses the cached instance
369
+ ```
370
+
371
+ The proxy transparently forwards all property access, method calls, `in` checks, and prototype lookups to the real instance. Methods are automatically bound to the real instance, so `this` works correctly even when destructured.
372
+
373
+ Only **sync class tokens** are supported. Async tokens and PropertyKey tokens are rejected at the type level because Proxy traps are synchronous.
374
+
375
+ `lazy()` works with Container, Scope, DisposableContainer, and DisposableScope:
376
+
377
+ ```ts
378
+ import { createScope } from 'katagami/scope';
379
+
380
+ const root = createContainer().registerScoped(RequestContext, () => new RequestContext());
381
+ const scope = createScope(root);
382
+
383
+ const ctx = lazy(scope, RequestContext); // deferred scoped resolution
384
+ ```
385
+
386
+ ### Tree Shaking
387
+
388
+ Katagami uses subpath exports to split functionality into independent entry points. If you only need the core container, `katagami/scope`, `katagami/disposable`, and `katagami/lazy` are completely excluded from the bundle. The package declares `sideEffects: false`, so bundlers can safely eliminate any unused code.
389
+
390
+ ```ts
391
+ // Core only — scope, disposable, and lazy are not included in the bundle
392
+ import { createContainer } from 'katagami';
393
+
394
+ // Import only what you need
395
+ import { createScope } from 'katagami/scope';
396
+ import { disposable } from 'katagami/disposable';
397
+ import { lazy } from 'katagami/lazy';
398
+ ```
399
+
274
400
  ### Interface Type Map
275
401
 
276
402
  When you pass an interface to `createContainer<T>()`, PropertyKey tokens are typed from the interface rather than accumulated through chaining. This means you can register and resolve tokens in any order:
@@ -400,23 +526,27 @@ const container = createContainer()
400
526
 
401
527
  Creates a new DI container. Pass an interface as `T` to define the type map for PropertyKey tokens. Pass `ScopedT` to define a separate type map for scoped PropertyKey tokens (order-independent, just like `T`).
402
528
 
403
- ### `container.registerSingleton(token, factory)`
529
+ ### `Container.prototype.registerSingleton(token, factory)`
404
530
 
405
531
  Registers a factory as a singleton. The instance is created on the first `resolve` and cached thereafter. Returns the container for method chaining.
406
532
 
407
- ### `container.registerTransient(token, factory)`
533
+ ### `Container.prototype.registerTransient(token, factory)`
408
534
 
409
535
  Registers a factory as transient. A new instance is created on every `resolve`. Returns the container for method chaining.
410
536
 
411
- ### `container.registerScoped(token, factory)`
537
+ ### `Container.prototype.registerScoped(token, factory)`
412
538
 
413
539
  Registers a factory as scoped. Within a scope, the instance is created on the first `resolve` and cached for that scope. Each scope maintains its own cache. Scoped tokens cannot be resolved from the root container. Returns the container for method chaining.
414
540
 
415
- ### `container.resolve(token)`
541
+ ### `Container.prototype.use(source)`
542
+
543
+ Copies all registrations from `source` (another `Container`) into this container. Only factory and lifetime entries are copied — singleton instance caches are not shared. Returns the container for method chaining.
544
+
545
+ ### `Container.prototype.resolve(token)`
416
546
 
417
547
  Resolves and returns the instance for the given token. Throws `ContainerError` if the token is not registered or if a circular dependency is detected.
418
548
 
419
- ### `container.tryResolve(token)` / `scope.tryResolve(token)`
549
+ ### `Container.prototype.tryResolve(token)`
420
550
 
421
551
  Attempts to resolve the instance for the given token. Returns `undefined` if the token is not registered, instead of throwing. Still throws `ContainerError` for circular dependencies or operations on disposed containers/scopes.
422
552
 
@@ -424,19 +554,31 @@ Attempts to resolve the instance for the given token. Returns `undefined` if the
424
554
 
425
555
  Creates a new `Scope` (child container) from a `Container` or an existing `Scope`. The scope inherits all registrations from the source. Singleton instances are shared with the parent, while scoped instances are local to the scope.
426
556
 
427
- ### `Scope`
557
+ ### `class Scope`
558
+
559
+ A scoped child container created by `createScope()`.
560
+
561
+ ### `Scope.prototype.resolve(token)`
562
+
563
+ Resolves and returns the instance for the given token. Behaves the same as `Container.prototype.resolve`, but can also resolve scoped tokens.
564
+
565
+ ### `Scope.prototype.tryResolve(token)`
566
+
567
+ Attempts to resolve the instance for the given token. Returns `undefined` if the token is not registered, instead of throwing. Still throws `ContainerError` for circular dependencies or operations on disposed scopes.
568
+
569
+ ### `lazy(source, token)` — `katagami/lazy`
428
570
 
429
- A scoped child container created by `createScope()`. Provides `resolve(token)` and `tryResolve(token)`.
571
+ Creates a Proxy that defers `resolve()` until the first property access. The resolved instance is cached — subsequent accesses use the cache. Only sync class tokens are supported; async tokens and PropertyKey tokens are rejected at the type level. Works with `Container`, `Scope`, `DisposableContainer`, and `DisposableScope`.
430
572
 
431
573
  ### `disposable(container)` — `katagami/disposable`
432
574
 
433
- Attaches `[Symbol.asyncDispose]` to a `Container` or `Scope`, enabling `await using` syntax. Disposes all owned instances in reverse creation order (LIFO). Calls `[Symbol.asyncDispose]()` or `[Symbol.dispose]()` on each instance that implements them. Idempotent — subsequent calls are no-ops. After disposal, `resolve()` and `createScope()` will throw `ContainerError`.
575
+ Attaches `[Symbol.asyncDispose]` to a `Container` or `Scope`, enabling `await using` syntax. Disposes all owned instances in reverse creation order (LIFO). Calls `[Symbol.asyncDispose]()` or `[Symbol.dispose]()` on each instance that implements them. Idempotent — subsequent calls are no-ops. After disposal, `resolve()` and `createScope()` will throw `ContainerError`. The returned type is narrowed to `DisposableContainer` or `DisposableScope`, which only expose `resolve` and `tryResolve` — registration methods are excluded at the type level.
434
576
 
435
- ### `ContainerError`
577
+ ### `class ContainerError`
436
578
 
437
579
  Error class thrown for container failures such as resolving an unregistered token, circular dependencies, or operations on a disposed container/scope.
438
580
 
439
- ### `Resolver`
581
+ ### `type Resolver`
440
582
 
441
583
  Type export representing the resolver passed to factory callbacks. Useful when you need to type a function that accepts a resolver parameter.
442
584
 
@@ -23,6 +23,9 @@ export declare function createContainer<T = Record<never, never>, ScopedT = Reco
23
23
  * - Singleton: Creates the instance on the first resolve and returns the cached value thereafter.
24
24
  * - Transient: Creates a new instance via the factory function on every resolve.
25
25
  *
26
+ * Registering the same token multiple times accumulates all factories.
27
+ * `resolve()` returns the last registered instance, while `resolveAll()` returns all.
28
+ *
26
29
  * @template T PropertyKey-based token type map (defined via interface, order-independent)
27
30
  * @template Sync Union of registered sync class constructors (accumulated via chaining, order-dependent)
28
31
  * @template Async Union of registered async class constructors (accumulated via chaining, order-dependent)
@@ -32,7 +35,7 @@ export declare function createContainer<T = Record<never, never>, ScopedT = Reco
32
35
  */
33
36
  export declare class Container<T = Record<never, never>, Sync extends AbstractConstructor = never, Async extends AbstractConstructor = never, ScopedT = Record<never, never>, ScopedSync extends AbstractConstructor = never, ScopedAsync extends AbstractConstructor = never> {
34
37
  private readonly registrations;
35
- private readonly instances;
38
+ private readonly singletonCache;
36
39
  private readonly resolvingTokens;
37
40
  private disposed;
38
41
  /**
@@ -46,6 +49,8 @@ export declare class Container<T = Record<never, never>, Sync extends AbstractCo
46
49
  * Register a factory function as a singleton for the given token.
47
50
  *
48
51
  * Creates the instance on the first resolve and returns the cached value thereafter.
52
+ * If the same token is registered multiple times, all factories are accumulated.
53
+ * `resolve()` returns the last registered instance; `resolveAll()` returns all.
49
54
  *
50
55
  * @param token Any value to use as a token
51
56
  * @param factory Factory function that receives a resolver and returns an instance
@@ -59,6 +64,8 @@ export declare class Container<T = Record<never, never>, Sync extends AbstractCo
59
64
  * Register a factory function as transient for the given token.
60
65
  *
61
66
  * Creates a new instance via the factory function on every resolve.
67
+ * If the same token is registered multiple times, all factories are accumulated.
68
+ * `resolve()` returns the last registered instance; `resolveAll()` returns all.
62
69
  *
63
70
  * @param token Any value to use as a token
64
71
  * @param factory Factory function that receives a resolver and returns an instance
@@ -83,9 +90,20 @@ export declare class Container<T = Record<never, never>, Sync extends AbstractCo
83
90
  registerScoped<V>(token: AbstractConstructor<V>, factory: (resolver: Resolver<T & ScopedT, Sync | ScopedSync, Async | ScopedAsync>) => V): Container<T, Sync, Async, ScopedT, ScopedSync | AbstractConstructor<V>, ScopedAsync>;
84
91
  registerScoped<K extends PropertyKey, V>(token: K, factory: (resolver: Resolver<T & ScopedT, Sync | ScopedSync, Async | ScopedAsync>) => V): Container<T, Sync, Async, Record<K, V> & ScopedT, ScopedSync, ScopedAsync>;
85
92
  registerScoped<V>(token: unknown, factory: (resolver: Resolver<T & ScopedT, Sync | ScopedSync, Async | ScopedAsync>) => V): Container<T, Sync, Async, ScopedT, ScopedSync, ScopedAsync>;
93
+ /**
94
+ * Apply all registrations from another container (module) to this container.
95
+ *
96
+ * Copies registration entries (factory + lifetime) by replacing existing entries for each token.
97
+ * Singleton instance caches are not shared — each container manages its own.
98
+ *
99
+ * @param source A container whose registrations will be copied into this container
100
+ * @returns The container for method chaining
101
+ */
102
+ use<MT, MSync extends AbstractConstructor, MAsync extends AbstractConstructor, MScopedT, MScopedSync extends AbstractConstructor, MScopedAsync extends AbstractConstructor>(source: Container<MT, MSync, MAsync, MScopedT, MScopedSync, MScopedAsync>): Container<T & MT, Sync | MSync, Async | MAsync, ScopedT & MScopedT, ScopedSync | MScopedSync, ScopedAsync | MScopedAsync>;
86
103
  /**
87
104
  * Resolve an instance for the given token.
88
105
  *
106
+ * Returns the instance from the last registered factory for the token.
89
107
  * For singleton registrations, creates the instance on the first call and caches it.
90
108
  * For transient registrations, creates a new instance on every call.
91
109
  *
@@ -110,8 +128,36 @@ export declare class Container<T = Record<never, never>, Sync extends AbstractCo
110
128
  tryResolve<K extends keyof T>(token: K): T[K] | undefined;
111
129
  tryResolve<V>(token: AbstractConstructor<V>): V | Promise<V> | undefined;
112
130
  tryResolve(token: PropertyKey): unknown;
131
+ /**
132
+ * Resolve all instances for the given token.
133
+ *
134
+ * Returns an array of instances from all registered factories for the token,
135
+ * in registration order.
136
+ *
137
+ * @param token A registered token
138
+ * @returns An array of instances associated with the token
139
+ * @throws ContainerError if the token is not registered
140
+ */
141
+ resolveAll<V>(token: AbstractConstructor<V> & Async): Promise<V>[];
142
+ resolveAll<V>(token: AbstractConstructor<V> & Sync): V[];
143
+ resolveAll<K extends keyof T>(token: K): T[K][];
144
+ /**
145
+ * Try to resolve all instances for the given token.
146
+ *
147
+ * Returns `undefined` instead of throwing when the token is not registered.
148
+ * Other errors (circular dependency, disposed container) are still thrown.
149
+ *
150
+ * @param token A token to resolve
151
+ * @returns An array of instances associated with the token, or `undefined` if not registered
152
+ */
153
+ tryResolveAll<V>(token: AbstractConstructor<V> & Async): Promise<V>[] | undefined;
154
+ tryResolveAll<V>(token: AbstractConstructor<V> & Sync): V[] | undefined;
155
+ tryResolveAll<K extends keyof T>(token: K): T[K][] | undefined;
156
+ tryResolveAll<V>(token: AbstractConstructor<V>): (V | Promise<V>)[] | undefined;
157
+ tryResolveAll(token: PropertyKey): unknown;
113
158
  /**
114
159
  * Internal resolution logic shared by resolve and tryResolve.
160
+ * Resolves the last registered factory for the token.
115
161
  *
116
162
  * @param token Token to resolve
117
163
  * @param required If true, throws when the token is not registered. If false, returns undefined.
@@ -119,7 +165,16 @@ export declare class Container<T = Record<never, never>, Sync extends AbstractCo
119
165
  */
120
166
  private resolveToken;
121
167
  /**
122
- * Add a registration entry.
168
+ * Internal resolution logic shared by resolveAll and tryResolveAll.
169
+ * Resolves all registered factories for the token.
170
+ *
171
+ * @param token Token to resolve
172
+ * @param required If true, throws when the token is not registered. If false, returns undefined.
173
+ * @returns An array of resolved instances, or undefined if not registered and required is false
174
+ */
175
+ private resolveAllTokens;
176
+ /**
177
+ * Add a registration entry. Accumulates registrations for the same token.
123
178
  *
124
179
  * @param token Token
125
180
  * @param factory Factory function
@@ -44,7 +44,7 @@ function disposable(container) {
44
44
  return;
45
45
  }
46
46
  internals.markDisposed();
47
- const instances = [...internals.ownInstances.values()].reverse();
47
+ const instances = [...internals.ownCache.values()].reverse();
48
48
  const errors = [];
49
49
  for (const instance of instances) {
50
50
  try {
@@ -63,7 +63,7 @@ function disposable(container) {
63
63
  errors.push(error);
64
64
  }
65
65
  }
66
- internals.ownInstances.clear();
66
+ internals.ownCache.clear();
67
67
  if (errors.length > 0) {
68
68
  throw new AggregateError(errors, "One or more errors occurred during disposal.");
69
69
  }
@@ -1,4 +1,39 @@
1
+ import type { Container } from '../container';
1
2
  import { type ContainerInternals, INTERNALS } from '../internal';
3
+ import type { AbstractConstructor, Resolver } from '../resolver';
4
+ import type { Scope } from '../scope';
5
+ /**
6
+ * A container wrapped with `disposable()`.
7
+ *
8
+ * Only `resolve`, `tryResolve`, `resolveAll`, and `tryResolveAll` are available at the type level.
9
+ * Registration methods (`registerSingleton`, `registerTransient`, `registerScoped`, `use`)
10
+ * are excluded, preventing accidental registration on a potentially-disposed container.
11
+ *
12
+ * @template T PropertyKey-based token type map
13
+ * @template Sync Union of registered sync class constructors
14
+ * @template Async Union of registered async class constructors
15
+ * @template ScopedT PropertyKey-based token type map for scoped registrations
16
+ * @template ScopedSync Union of scoped sync class constructors
17
+ * @template ScopedAsync Union of scoped async class constructors
18
+ */
19
+ export interface DisposableContainer<T = Record<never, never>, Sync extends AbstractConstructor = never, Async extends AbstractConstructor = never, _ScopedT = Record<never, never>, _ScopedSync extends AbstractConstructor = never, _ScopedAsync extends AbstractConstructor = never> extends Resolver<T, Sync, Async>, AsyncDisposable {
20
+ readonly [INTERNALS]: ContainerInternals;
21
+ }
22
+ /**
23
+ * A scope wrapped with `disposable()`.
24
+ *
25
+ * Only `resolve`, `tryResolve`, `resolveAll`, and `tryResolveAll` are available at the type level.
26
+ *
27
+ * @template T PropertyKey-based token type map
28
+ * @template Sync Union of registered sync class constructors
29
+ * @template Async Union of registered async class constructors
30
+ * @template ScopedT PropertyKey-based token type map for scoped registrations
31
+ * @template ScopedSync Union of scoped sync class constructors
32
+ * @template ScopedAsync Union of scoped async class constructors
33
+ */
34
+ export interface DisposableScope<T = Record<never, never>, Sync extends AbstractConstructor = never, Async extends AbstractConstructor = never, ScopedT = Record<never, never>, ScopedSync extends AbstractConstructor = never, ScopedAsync extends AbstractConstructor = never> extends Resolver<T & ScopedT, Sync | ScopedSync, Async | ScopedAsync>, AsyncDisposable {
35
+ readonly [INTERNALS]: ContainerInternals;
36
+ }
2
37
  /**
3
38
  * Add async disposal capability to a container or scope.
4
39
  *
@@ -6,8 +41,11 @@ import { type ContainerInternals, INTERNALS } from '../internal';
6
41
  * Disposes owned instances in reverse creation order (LIFO), calling
7
42
  * `[Symbol.asyncDispose]()` or `[Symbol.dispose]()` on each instance that implements them.
8
43
  *
44
+ * The returned type is narrowed to only expose `resolve`, `tryResolve`, `resolveAll`, and `tryResolveAll`,
45
+ * preventing registration methods from being called on a potentially-disposed container.
46
+ *
9
47
  * @param container A Container or Scope to make disposable
10
- * @returns The same object with `AsyncDisposable` capability added
48
+ * @returns The same object with `AsyncDisposable` capability added and registration methods removed from the type
11
49
  *
12
50
  * @example
13
51
  * ```ts
@@ -19,6 +57,5 @@ import { type ContainerInternals, INTERNALS } from '../internal';
19
57
  * );
20
58
  * ```
21
59
  */
22
- export declare function disposable<C extends {
23
- readonly [INTERNALS]: ContainerInternals;
24
- }>(container: C): C & AsyncDisposable;
60
+ export declare function disposable<T, Sync extends AbstractConstructor, Async extends AbstractConstructor, ScopedT, ScopedSync extends AbstractConstructor, ScopedAsync extends AbstractConstructor>(container: Container<T, Sync, Async, ScopedT, ScopedSync, ScopedAsync>): DisposableContainer<T, Sync, Async, ScopedT, ScopedSync, ScopedAsync>;
61
+ export declare function disposable<T, Sync extends AbstractConstructor, Async extends AbstractConstructor, ScopedT, ScopedSync extends AbstractConstructor, ScopedAsync extends AbstractConstructor>(scope: Scope<T, Sync, Async, ScopedT, ScopedSync, ScopedAsync>): DisposableScope<T, Sync, Async, ScopedT, ScopedSync, ScopedAsync>;
@@ -10,7 +10,7 @@ function disposable(container) {
10
10
  return;
11
11
  }
12
12
  internals.markDisposed();
13
- const instances = [...internals.ownInstances.values()].reverse();
13
+ const instances = [...internals.ownCache.values()].reverse();
14
14
  const errors = [];
15
15
  for (const instance of instances) {
16
16
  try {
@@ -29,7 +29,7 @@ function disposable(container) {
29
29
  errors.push(error);
30
30
  }
31
31
  }
32
- internals.ownInstances.clear();
32
+ internals.ownCache.clear();
33
33
  if (errors.length > 0) {
34
34
  throw new AggregateError(errors, "One or more errors occurred during disposal.");
35
35
  }
package/dist/index.cjs CHANGED
@@ -78,22 +78,22 @@ function createContainer() {
78
78
 
79
79
  class Container {
80
80
  registrations;
81
- instances;
81
+ singletonCache;
82
82
  resolvingTokens;
83
83
  disposed = false;
84
84
  [INTERNALS];
85
85
  constructor() {
86
86
  this.registrations = new Map;
87
- this.instances = new Map;
87
+ this.singletonCache = new Map;
88
88
  this.resolvingTokens = new Set;
89
89
  this[INTERNALS] = {
90
- instances: this.instances,
91
90
  isDisposed: () => this.disposed,
92
91
  markDisposed: () => {
93
92
  this.disposed = true;
94
93
  },
95
- ownInstances: this.instances,
96
- registrations: this.registrations
94
+ ownCache: this.singletonCache,
95
+ registrations: this.registrations,
96
+ singletonCache: this.singletonCache
97
97
  };
98
98
  }
99
99
  registerSingleton(token, factory) {
@@ -105,27 +105,40 @@ class Container {
105
105
  registerScoped(token, factory) {
106
106
  return this.addRegistration(token, factory, "scoped");
107
107
  }
108
+ use(source) {
109
+ for (const [token, registrations] of source[INTERNALS].registrations) {
110
+ this.registrations.set(token, [...registrations]);
111
+ }
112
+ return this;
113
+ }
108
114
  resolve(token) {
109
115
  return this.resolveToken(token, true);
110
116
  }
111
117
  tryResolve(token) {
112
118
  return this.resolveToken(token, false);
113
119
  }
120
+ resolveAll(token) {
121
+ return this.resolveAllTokens(token, true);
122
+ }
123
+ tryResolveAll(token) {
124
+ return this.resolveAllTokens(token, false);
125
+ }
114
126
  resolveToken(token, required) {
115
127
  if (this.disposed) {
116
128
  throw new ContainerError("Cannot resolve from a disposed container.");
117
129
  }
118
- const cached = this.instances.get(token);
119
- if (cached !== undefined) {
120
- return cached;
121
- }
122
- const registration = this.registrations.get(token);
123
- if (registration === undefined) {
130
+ const registrations = this.registrations.get(token);
131
+ if (registrations === undefined || registrations.length === 0) {
124
132
  if (required) {
125
133
  throw new ContainerError(`Token "${tokenToString(token)}" is not registered.`);
126
134
  }
127
135
  return;
128
136
  }
137
+ const registration = registrations[registrations.length - 1];
138
+ const cached = this.singletonCache.get(registration);
139
+ if (cached !== undefined) {
140
+ return cached;
141
+ }
129
142
  if (registration.lifetime === "scoped") {
130
143
  throw new ContainerError(`Cannot resolve scoped token "${tokenToString(token)}" from the root container. Use createScope() to create a scope first.`);
131
144
  }
@@ -136,15 +149,55 @@ class Container {
136
149
  try {
137
150
  const instance = registration.factory(this);
138
151
  if (registration.lifetime === "singleton") {
139
- this.instances.set(token, instance);
152
+ this.singletonCache.set(registration, instance);
140
153
  }
141
154
  return instance;
142
155
  } finally {
143
156
  this.resolvingTokens.delete(token);
144
157
  }
145
158
  }
159
+ resolveAllTokens(token, required) {
160
+ if (this.disposed) {
161
+ throw new ContainerError("Cannot resolve from a disposed container.");
162
+ }
163
+ const registrations = this.registrations.get(token);
164
+ if (registrations === undefined || registrations.length === 0) {
165
+ if (required) {
166
+ throw new ContainerError(`Token "${tokenToString(token)}" is not registered.`);
167
+ }
168
+ return;
169
+ }
170
+ if (this.resolvingTokens.has(token)) {
171
+ throw new ContainerError(`Circular dependency detected: ${buildCircularPath(this.resolvingTokens, token)}`);
172
+ }
173
+ this.resolvingTokens.add(token);
174
+ try {
175
+ return registrations.map((registration) => {
176
+ const reg = registration;
177
+ const cached = this.singletonCache.get(registration);
178
+ if (cached !== undefined) {
179
+ return cached;
180
+ }
181
+ if (reg.lifetime === "scoped") {
182
+ throw new ContainerError(`Cannot resolve scoped token "${tokenToString(token)}" from the root container. Use createScope() to create a scope first.`);
183
+ }
184
+ const instance = reg.factory(this);
185
+ if (reg.lifetime === "singleton") {
186
+ this.singletonCache.set(registration, instance);
187
+ }
188
+ return instance;
189
+ });
190
+ } finally {
191
+ this.resolvingTokens.delete(token);
192
+ }
193
+ }
146
194
  addRegistration(token, factory, lifetime) {
147
- this.registrations.set(token, { factory, lifetime });
195
+ const existing = this.registrations.get(token);
196
+ if (existing !== undefined) {
197
+ existing.push({ factory, lifetime });
198
+ } else {
199
+ this.registrations.set(token, [{ factory, lifetime }]);
200
+ }
148
201
  return this;
149
202
  }
150
203
  }
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export { Container, createContainer } from './container';
2
- export type { disposable } from './disposable';
2
+ export type { DisposableContainer, DisposableScope, disposable } from './disposable';
3
3
  export { ContainerError } from './error';
4
+ export type { lazy } from './lazy';
4
5
  export type { Resolver } from './resolver';
5
6
  export type { createScope, Scope } from './scope';