katagami 2.1.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
@@ -26,6 +26,7 @@ Lightweight TypeScript DI container with full type inference.
26
26
  | Async factories | Promise-returning factories are automatically tracked by the type system |
27
27
  | Circular dependency detection | Clear error messages with the full cycle path |
28
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 |
29
30
 
30
31
  ## Install
31
32
 
@@ -70,15 +71,16 @@ Decorator-based DI requires `experimentalDecorators` and `emitDecoratorMetadata`
70
71
 
71
72
  ### Tree-shakeable
72
73
 
73
- Katagami is split into subpath exports. Import only what you use — `katagami/scope` and `katagami/disposable` are completely eliminated from the bundle if not imported. Combined with `sideEffects: false`, bundlers can remove every unused byte.
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.
74
75
 
75
76
  ```ts
76
- // Core only — scope and disposable are not included in the bundle
77
+ // Core only — scope, disposable, and lazy are not included in the bundle
77
78
  import { createContainer } from 'katagami';
78
79
 
79
80
  // Import only what you need
80
81
  import { createScope } from 'katagami/scope';
81
82
  import { disposable } from 'katagami/disposable';
83
+ import { lazy } from 'katagami/lazy';
82
84
  ```
83
85
 
84
86
  ### Full type inference from class tokens
@@ -340,17 +342,59 @@ container.resolve(Connection); // OK
340
342
  container.registerSingleton(/* ... */); // Compile-time error
341
343
  ```
342
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
+
343
386
  ### Tree Shaking
344
387
 
345
- Katagami uses subpath exports to split functionality into independent entry points. If you only need the core container, `katagami/scope` and `katagami/disposable` are completely excluded from the bundle. The package declares `sideEffects: false`, so bundlers can safely eliminate any unused code.
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.
346
389
 
347
390
  ```ts
348
- // Core only — scope and disposable are not included in the bundle
391
+ // Core only — scope, disposable, and lazy are not included in the bundle
349
392
  import { createContainer } from 'katagami';
350
393
 
351
394
  // Import only what you need
352
395
  import { createScope } from 'katagami/scope';
353
396
  import { disposable } from 'katagami/disposable';
397
+ import { lazy } from 'katagami/lazy';
354
398
  ```
355
399
 
356
400
  ### Interface Type Map
@@ -522,6 +566,10 @@ Resolves and returns the instance for the given token. Behaves the same as `Cont
522
566
 
523
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.
524
568
 
569
+ ### `lazy(source, token)` — `katagami/lazy`
570
+
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`.
572
+
525
573
  ### `disposable(container)` — `katagami/disposable`
526
574
 
527
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.
@@ -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
@@ -86,8 +93,8 @@ export declare class Container<T = Record<never, never>, Sync extends AbstractCo
86
93
  /**
87
94
  * Apply all registrations from another container (module) to this container.
88
95
  *
89
- * Copies only registration entries (factory + lifetime). Singleton instance caches
90
- * are not shared — each container manages its own.
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.
91
98
  *
92
99
  * @param source A container whose registrations will be copied into this container
93
100
  * @returns The container for method chaining
@@ -96,6 +103,7 @@ export declare class Container<T = Record<never, never>, Sync extends AbstractCo
96
103
  /**
97
104
  * Resolve an instance for the given token.
98
105
  *
106
+ * Returns the instance from the last registered factory for the token.
99
107
  * For singleton registrations, creates the instance on the first call and caches it.
100
108
  * For transient registrations, creates a new instance on every call.
101
109
  *
@@ -120,8 +128,36 @@ export declare class Container<T = Record<never, never>, Sync extends AbstractCo
120
128
  tryResolve<K extends keyof T>(token: K): T[K] | undefined;
121
129
  tryResolve<V>(token: AbstractConstructor<V>): V | Promise<V> | undefined;
122
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;
123
158
  /**
124
159
  * Internal resolution logic shared by resolve and tryResolve.
160
+ * Resolves the last registered factory for the token.
125
161
  *
126
162
  * @param token Token to resolve
127
163
  * @param required If true, throws when the token is not registered. If false, returns undefined.
@@ -129,7 +165,16 @@ export declare class Container<T = Record<never, never>, Sync extends AbstractCo
129
165
  */
130
166
  private resolveToken;
131
167
  /**
132
- * 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.
133
178
  *
134
179
  * @param token Token
135
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
  }
@@ -5,7 +5,7 @@ import type { Scope } from '../scope';
5
5
  /**
6
6
  * A container wrapped with `disposable()`.
7
7
  *
8
- * Only `resolve` and `tryResolve` are available at the type level.
8
+ * Only `resolve`, `tryResolve`, `resolveAll`, and `tryResolveAll` are available at the type level.
9
9
  * Registration methods (`registerSingleton`, `registerTransient`, `registerScoped`, `use`)
10
10
  * are excluded, preventing accidental registration on a potentially-disposed container.
11
11
  *
@@ -22,7 +22,7 @@ export interface DisposableContainer<T = Record<never, never>, Sync extends Abst
22
22
  /**
23
23
  * A scope wrapped with `disposable()`.
24
24
  *
25
- * Only `resolve` and `tryResolve` are available at the type level.
25
+ * Only `resolve`, `tryResolve`, `resolveAll`, and `tryResolveAll` are available at the type level.
26
26
  *
27
27
  * @template T PropertyKey-based token type map
28
28
  * @template Sync Union of registered sync class constructors
@@ -41,7 +41,7 @@ export interface DisposableScope<T = Record<never, never>, Sync extends Abstract
41
41
  * Disposes owned instances in reverse creation order (LIFO), calling
42
42
  * `[Symbol.asyncDispose]()` or `[Symbol.dispose]()` on each instance that implements them.
43
43
  *
44
- * The returned type is narrowed to only expose `resolve` and `tryResolve`,
44
+ * The returned type is narrowed to only expose `resolve`, `tryResolve`, `resolveAll`, and `tryResolveAll`,
45
45
  * preventing registration methods from being called on a potentially-disposed container.
46
46
  *
47
47
  * @param container A Container or Scope to make disposable
@@ -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) {
@@ -106,8 +106,8 @@ class Container {
106
106
  return this.addRegistration(token, factory, "scoped");
107
107
  }
108
108
  use(source) {
109
- for (const [token, registration] of source[INTERNALS].registrations) {
110
- this.registrations.set(token, registration);
109
+ for (const [token, registrations] of source[INTERNALS].registrations) {
110
+ this.registrations.set(token, [...registrations]);
111
111
  }
112
112
  return this;
113
113
  }
@@ -117,21 +117,28 @@ class Container {
117
117
  tryResolve(token) {
118
118
  return this.resolveToken(token, false);
119
119
  }
120
+ resolveAll(token) {
121
+ return this.resolveAllTokens(token, true);
122
+ }
123
+ tryResolveAll(token) {
124
+ return this.resolveAllTokens(token, false);
125
+ }
120
126
  resolveToken(token, required) {
121
127
  if (this.disposed) {
122
128
  throw new ContainerError("Cannot resolve from a disposed container.");
123
129
  }
124
- const cached = this.instances.get(token);
125
- if (cached !== undefined) {
126
- return cached;
127
- }
128
- const registration = this.registrations.get(token);
129
- if (registration === undefined) {
130
+ const registrations = this.registrations.get(token);
131
+ if (registrations === undefined || registrations.length === 0) {
130
132
  if (required) {
131
133
  throw new ContainerError(`Token "${tokenToString(token)}" is not registered.`);
132
134
  }
133
135
  return;
134
136
  }
137
+ const registration = registrations[registrations.length - 1];
138
+ const cached = this.singletonCache.get(registration);
139
+ if (cached !== undefined) {
140
+ return cached;
141
+ }
135
142
  if (registration.lifetime === "scoped") {
136
143
  throw new ContainerError(`Cannot resolve scoped token "${tokenToString(token)}" from the root container. Use createScope() to create a scope first.`);
137
144
  }
@@ -142,15 +149,55 @@ class Container {
142
149
  try {
143
150
  const instance = registration.factory(this);
144
151
  if (registration.lifetime === "singleton") {
145
- this.instances.set(token, instance);
152
+ this.singletonCache.set(registration, instance);
146
153
  }
147
154
  return instance;
148
155
  } finally {
149
156
  this.resolvingTokens.delete(token);
150
157
  }
151
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
+ }
152
194
  addRegistration(token, factory, lifetime) {
153
- 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
+ }
154
201
  return this;
155
202
  }
156
203
  }
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export { Container, createContainer } from './container';
2
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';
package/dist/index.js CHANGED
@@ -14,22 +14,22 @@ function createContainer() {
14
14
 
15
15
  class Container {
16
16
  registrations;
17
- instances;
17
+ singletonCache;
18
18
  resolvingTokens;
19
19
  disposed = false;
20
20
  [INTERNALS];
21
21
  constructor() {
22
22
  this.registrations = new Map;
23
- this.instances = new Map;
23
+ this.singletonCache = new Map;
24
24
  this.resolvingTokens = new Set;
25
25
  this[INTERNALS] = {
26
- instances: this.instances,
27
26
  isDisposed: () => this.disposed,
28
27
  markDisposed: () => {
29
28
  this.disposed = true;
30
29
  },
31
- ownInstances: this.instances,
32
- registrations: this.registrations
30
+ ownCache: this.singletonCache,
31
+ registrations: this.registrations,
32
+ singletonCache: this.singletonCache
33
33
  };
34
34
  }
35
35
  registerSingleton(token, factory) {
@@ -42,8 +42,8 @@ class Container {
42
42
  return this.addRegistration(token, factory, "scoped");
43
43
  }
44
44
  use(source) {
45
- for (const [token, registration] of source[INTERNALS].registrations) {
46
- this.registrations.set(token, registration);
45
+ for (const [token, registrations] of source[INTERNALS].registrations) {
46
+ this.registrations.set(token, [...registrations]);
47
47
  }
48
48
  return this;
49
49
  }
@@ -53,21 +53,28 @@ class Container {
53
53
  tryResolve(token) {
54
54
  return this.resolveToken(token, false);
55
55
  }
56
+ resolveAll(token) {
57
+ return this.resolveAllTokens(token, true);
58
+ }
59
+ tryResolveAll(token) {
60
+ return this.resolveAllTokens(token, false);
61
+ }
56
62
  resolveToken(token, required) {
57
63
  if (this.disposed) {
58
64
  throw new ContainerError("Cannot resolve from a disposed container.");
59
65
  }
60
- const cached = this.instances.get(token);
61
- if (cached !== undefined) {
62
- return cached;
63
- }
64
- const registration = this.registrations.get(token);
65
- if (registration === undefined) {
66
+ const registrations = this.registrations.get(token);
67
+ if (registrations === undefined || registrations.length === 0) {
66
68
  if (required) {
67
69
  throw new ContainerError(`Token "${tokenToString(token)}" is not registered.`);
68
70
  }
69
71
  return;
70
72
  }
73
+ const registration = registrations[registrations.length - 1];
74
+ const cached = this.singletonCache.get(registration);
75
+ if (cached !== undefined) {
76
+ return cached;
77
+ }
71
78
  if (registration.lifetime === "scoped") {
72
79
  throw new ContainerError(`Cannot resolve scoped token "${tokenToString(token)}" from the root container. Use createScope() to create a scope first.`);
73
80
  }
@@ -78,15 +85,55 @@ class Container {
78
85
  try {
79
86
  const instance = registration.factory(this);
80
87
  if (registration.lifetime === "singleton") {
81
- this.instances.set(token, instance);
88
+ this.singletonCache.set(registration, instance);
82
89
  }
83
90
  return instance;
84
91
  } finally {
85
92
  this.resolvingTokens.delete(token);
86
93
  }
87
94
  }
95
+ resolveAllTokens(token, required) {
96
+ if (this.disposed) {
97
+ throw new ContainerError("Cannot resolve from a disposed container.");
98
+ }
99
+ const registrations = this.registrations.get(token);
100
+ if (registrations === undefined || registrations.length === 0) {
101
+ if (required) {
102
+ throw new ContainerError(`Token "${tokenToString(token)}" is not registered.`);
103
+ }
104
+ return;
105
+ }
106
+ if (this.resolvingTokens.has(token)) {
107
+ throw new ContainerError(`Circular dependency detected: ${buildCircularPath(this.resolvingTokens, token)}`);
108
+ }
109
+ this.resolvingTokens.add(token);
110
+ try {
111
+ return registrations.map((registration) => {
112
+ const reg = registration;
113
+ const cached = this.singletonCache.get(registration);
114
+ if (cached !== undefined) {
115
+ return cached;
116
+ }
117
+ if (reg.lifetime === "scoped") {
118
+ throw new ContainerError(`Cannot resolve scoped token "${tokenToString(token)}" from the root container. Use createScope() to create a scope first.`);
119
+ }
120
+ const instance = reg.factory(this);
121
+ if (reg.lifetime === "singleton") {
122
+ this.singletonCache.set(registration, instance);
123
+ }
124
+ return instance;
125
+ });
126
+ } finally {
127
+ this.resolvingTokens.delete(token);
128
+ }
129
+ }
88
130
  addRegistration(token, factory, lifetime) {
89
- this.registrations.set(token, { factory, lifetime });
131
+ const existing = this.registrations.get(token);
132
+ if (existing !== undefined) {
133
+ existing.push({ factory, lifetime });
134
+ } else {
135
+ this.registrations.set(token, [{ factory, lifetime }]);
136
+ }
90
137
  return this;
91
138
  }
92
139
  }
@@ -14,12 +14,12 @@ export declare const INTERNALS: unique symbol;
14
14
  * @internal
15
15
  */
16
16
  export interface ContainerInternals {
17
- /** All registrations (singleton / transient / scoped). */
18
- readonly registrations: Map<unknown, Registration>;
19
- /** Instance cache used during resolution (Container: singletons, Scope: singletonInstances). */
20
- readonly instances: Map<unknown, unknown>;
21
- /** Instances owned by this container / scope (disposal target). */
22
- readonly ownInstances: Map<unknown, unknown>;
17
+ /** All registrations (singleton / transient / scoped). Each token maps to an array of registrations. */
18
+ readonly registrations: Map<unknown, Registration[]>;
19
+ /** Singleton cache keyed by Registration object (Container: singletons, Scope: shared with parent). */
20
+ readonly singletonCache: Map<Registration, unknown>;
21
+ /** Instances owned by this container / scope (disposal target), keyed by Registration object. */
22
+ readonly ownCache: Map<Registration, unknown>;
23
23
  /** Whether this container / scope has been disposed. */
24
24
  isDisposed(): boolean;
25
25
  /** Mark this container / scope as disposed. */
@@ -0,0 +1,88 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropNames = Object.getOwnPropertyNames;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
5
+ var __moduleCache = /* @__PURE__ */ new WeakMap;
6
+ var __toCommonJS = (from) => {
7
+ var entry = __moduleCache.get(from), desc;
8
+ if (entry)
9
+ return entry;
10
+ entry = __defProp({}, "__esModule", { value: true });
11
+ if (from && typeof from === "object" || typeof from === "function")
12
+ __getOwnPropNames(from).map((key) => !__hasOwnProp.call(entry, key) && __defProp(entry, key, {
13
+ get: () => from[key],
14
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
15
+ }));
16
+ __moduleCache.set(from, entry);
17
+ return entry;
18
+ };
19
+ var __export = (target, all) => {
20
+ for (var name in all)
21
+ __defProp(target, name, {
22
+ get: all[name],
23
+ enumerable: true,
24
+ configurable: true,
25
+ set: (newValue) => all[name] = () => newValue
26
+ });
27
+ };
28
+
29
+ // src/lazy/index.ts
30
+ var exports_lazy = {};
31
+ __export(exports_lazy, {
32
+ lazy: () => lazy
33
+ });
34
+ module.exports = __toCommonJS(exports_lazy);
35
+ function lazy(source, token) {
36
+ let instance;
37
+ let resolved = false;
38
+ const ensureResolved = () => {
39
+ if (!resolved) {
40
+ instance = source.resolve(token);
41
+ resolved = true;
42
+ }
43
+ return instance;
44
+ };
45
+ const proxyTarget = Object.create(null);
46
+ return new Proxy(proxyTarget, {
47
+ defineProperty(_, prop, desc) {
48
+ return Reflect.defineProperty(ensureResolved(), prop, desc);
49
+ },
50
+ deleteProperty(_, prop) {
51
+ return Reflect.deleteProperty(ensureResolved(), prop);
52
+ },
53
+ get(_, prop) {
54
+ const target = ensureResolved();
55
+ const value = Reflect.get(target, prop, target);
56
+ if (typeof value === "function") {
57
+ return value.bind(target);
58
+ }
59
+ return value;
60
+ },
61
+ getOwnPropertyDescriptor(_, prop) {
62
+ return Reflect.getOwnPropertyDescriptor(ensureResolved(), prop);
63
+ },
64
+ getPrototypeOf() {
65
+ return Reflect.getPrototypeOf(ensureResolved());
66
+ },
67
+ has(_, prop) {
68
+ return Reflect.has(ensureResolved(), prop);
69
+ },
70
+ isExtensible() {
71
+ return Reflect.isExtensible(ensureResolved());
72
+ },
73
+ ownKeys() {
74
+ return Reflect.ownKeys(ensureResolved());
75
+ },
76
+ preventExtensions() {
77
+ Reflect.preventExtensions(ensureResolved());
78
+ Reflect.preventExtensions(proxyTarget);
79
+ return true;
80
+ },
81
+ set(_, prop, value) {
82
+ return Reflect.set(ensureResolved(), prop, value, ensureResolved());
83
+ },
84
+ setPrototypeOf(_, proto) {
85
+ return Reflect.setPrototypeOf(ensureResolved(), proto);
86
+ }
87
+ });
88
+ }
@@ -0,0 +1,35 @@
1
+ import type { Container } from '../container';
2
+ import type { DisposableContainer, DisposableScope } from '../disposable';
3
+ import type { AbstractConstructor } from '../resolver';
4
+ import type { Scope } from '../scope';
5
+ /**
6
+ * Create a lazy proxy that defers resolution until the first property access.
7
+ *
8
+ * The returned object looks and behaves like `V`, but the underlying instance
9
+ * is not created until a property is read, written, or otherwise accessed.
10
+ * Once resolved the instance is cached — subsequent accesses hit the cache.
11
+ *
12
+ * Only **sync class tokens** are supported. Async tokens and PropertyKey tokens
13
+ * are rejected at the type level.
14
+ *
15
+ * @param source A Container, Scope, DisposableContainer, or DisposableScope
16
+ * @param token A sync class constructor token
17
+ * @returns A proxy that transparently forwards to the lazily-resolved instance
18
+ *
19
+ * @example
20
+ * ```ts
21
+ * import { createContainer } from 'katagami';
22
+ * import { lazy } from 'katagami/lazy';
23
+ *
24
+ * const container = createContainer()
25
+ * .registerSingleton(HeavyService, () => new HeavyService());
26
+ *
27
+ * const service = lazy(container, HeavyService);
28
+ * // Instance is NOT created yet
29
+ * service.doSomething(); // resolved here, then cached
30
+ * ```
31
+ */
32
+ export declare function lazy<T, Sync extends AbstractConstructor, Async extends AbstractConstructor, ScopedT, ScopedSync extends AbstractConstructor, ScopedAsync extends AbstractConstructor, V>(source: Container<T, Sync, Async, ScopedT, ScopedSync, ScopedAsync>, token: AbstractConstructor<V> & Sync): V;
33
+ export declare function lazy<T, Sync extends AbstractConstructor, Async extends AbstractConstructor, ScopedT, ScopedSync extends AbstractConstructor, ScopedAsync extends AbstractConstructor, V>(source: Scope<T, Sync, Async, ScopedT, ScopedSync, ScopedAsync>, token: AbstractConstructor<V> & (Sync | ScopedSync)): V;
34
+ export declare function lazy<T, Sync extends AbstractConstructor, Async extends AbstractConstructor, ScopedT, ScopedSync extends AbstractConstructor, ScopedAsync extends AbstractConstructor, V>(source: DisposableContainer<T, Sync, Async, ScopedT, ScopedSync, ScopedAsync>, token: AbstractConstructor<V> & Sync): V;
35
+ export declare function lazy<T, Sync extends AbstractConstructor, Async extends AbstractConstructor, ScopedT, ScopedSync extends AbstractConstructor, ScopedAsync extends AbstractConstructor, V>(source: DisposableScope<T, Sync, Async, ScopedT, ScopedSync, ScopedAsync>, token: AbstractConstructor<V> & (Sync | ScopedSync)): V;
@@ -0,0 +1,58 @@
1
+ // src/lazy/index.ts
2
+ function lazy(source, token) {
3
+ let instance;
4
+ let resolved = false;
5
+ const ensureResolved = () => {
6
+ if (!resolved) {
7
+ instance = source.resolve(token);
8
+ resolved = true;
9
+ }
10
+ return instance;
11
+ };
12
+ const proxyTarget = Object.create(null);
13
+ return new Proxy(proxyTarget, {
14
+ defineProperty(_, prop, desc) {
15
+ return Reflect.defineProperty(ensureResolved(), prop, desc);
16
+ },
17
+ deleteProperty(_, prop) {
18
+ return Reflect.deleteProperty(ensureResolved(), prop);
19
+ },
20
+ get(_, prop) {
21
+ const target = ensureResolved();
22
+ const value = Reflect.get(target, prop, target);
23
+ if (typeof value === "function") {
24
+ return value.bind(target);
25
+ }
26
+ return value;
27
+ },
28
+ getOwnPropertyDescriptor(_, prop) {
29
+ return Reflect.getOwnPropertyDescriptor(ensureResolved(), prop);
30
+ },
31
+ getPrototypeOf() {
32
+ return Reflect.getPrototypeOf(ensureResolved());
33
+ },
34
+ has(_, prop) {
35
+ return Reflect.has(ensureResolved(), prop);
36
+ },
37
+ isExtensible() {
38
+ return Reflect.isExtensible(ensureResolved());
39
+ },
40
+ ownKeys() {
41
+ return Reflect.ownKeys(ensureResolved());
42
+ },
43
+ preventExtensions() {
44
+ Reflect.preventExtensions(ensureResolved());
45
+ Reflect.preventExtensions(proxyTarget);
46
+ return true;
47
+ },
48
+ set(_, prop, value) {
49
+ return Reflect.set(ensureResolved(), prop, value, ensureResolved());
50
+ },
51
+ setPrototypeOf(_, proto) {
52
+ return Reflect.setPrototypeOf(ensureResolved(), proto);
53
+ }
54
+ });
55
+ }
56
+ export {
57
+ lazy
58
+ };
@@ -30,6 +30,31 @@ export interface Resolver<T, Sync extends AbstractConstructor = AbstractConstruc
30
30
  tryResolve<K extends keyof T>(token: K): T[K] | undefined;
31
31
  tryResolve<V>(token: AbstractConstructor<V>): V | Promise<V> | undefined;
32
32
  tryResolve(token: PropertyKey): unknown;
33
+ /**
34
+ * Resolve all instances for the given token.
35
+ *
36
+ * Returns an array of instances from all registrations for the token.
37
+ *
38
+ * @param token A registered token
39
+ * @returns An array of instances associated with the token
40
+ */
41
+ resolveAll<V>(token: AbstractConstructor<V> & Async): Promise<V>[];
42
+ resolveAll<V>(token: AbstractConstructor<V> & Sync): V[];
43
+ resolveAll<K extends keyof T>(token: K): T[K][];
44
+ /**
45
+ * Try to resolve all instances for the given token.
46
+ *
47
+ * Returns `undefined` instead of throwing when the token is not registered.
48
+ * Other errors (circular dependency, disposed container) are still thrown.
49
+ *
50
+ * @param token A token to resolve
51
+ * @returns An array of instances associated with the token, or `undefined` if not registered
52
+ */
53
+ tryResolveAll<V>(token: AbstractConstructor<V> & Async): Promise<V>[] | undefined;
54
+ tryResolveAll<V>(token: AbstractConstructor<V> & Sync): V[] | undefined;
55
+ tryResolveAll<K extends keyof T>(token: K): T[K][] | undefined;
56
+ tryResolveAll<V>(token: AbstractConstructor<V>): (V | Promise<V>)[] | undefined;
57
+ tryResolveAll(token: PropertyKey): unknown;
33
58
  }
34
59
  /**
35
60
  * Lifetime of a registration.
@@ -76,29 +76,29 @@ function createScope(source) {
76
76
  if (internals.isDisposed()) {
77
77
  throw new ContainerError("Cannot create a scope from a disposed container.");
78
78
  }
79
- return new Scope(internals.registrations, internals.instances);
79
+ return new Scope(internals.registrations, internals.singletonCache);
80
80
  }
81
81
 
82
82
  class Scope {
83
83
  registrations;
84
- singletonInstances;
85
- scopedInstances;
84
+ singletonCache;
85
+ scopedCache;
86
86
  resolvingTokens;
87
87
  disposed = false;
88
88
  [INTERNALS];
89
- constructor(registrations, singletonInstances) {
89
+ constructor(registrations, singletonCache) {
90
90
  this.registrations = registrations;
91
- this.singletonInstances = singletonInstances;
92
- this.scopedInstances = new Map;
91
+ this.singletonCache = singletonCache;
92
+ this.scopedCache = new Map;
93
93
  this.resolvingTokens = new Set;
94
94
  this[INTERNALS] = {
95
- instances: this.singletonInstances,
96
95
  isDisposed: () => this.disposed,
97
96
  markDisposed: () => {
98
97
  this.disposed = true;
99
98
  },
100
- ownInstances: this.scopedInstances,
101
- registrations: this.registrations
99
+ ownCache: this.scopedCache,
100
+ registrations: this.registrations,
101
+ singletonCache: this.singletonCache
102
102
  };
103
103
  }
104
104
  resolve(token) {
@@ -107,25 +107,32 @@ class Scope {
107
107
  tryResolve(token) {
108
108
  return this.resolveToken(token, false);
109
109
  }
110
+ resolveAll(token) {
111
+ return this.resolveAllTokens(token, true);
112
+ }
113
+ tryResolveAll(token) {
114
+ return this.resolveAllTokens(token, false);
115
+ }
110
116
  resolveToken(token, required) {
111
117
  if (this.disposed) {
112
118
  throw new ContainerError("Cannot resolve from a disposed scope.");
113
119
  }
114
- const singletonCached = this.singletonInstances.get(token);
120
+ const registrations = this.registrations.get(token);
121
+ if (registrations === undefined || registrations.length === 0) {
122
+ if (required) {
123
+ throw new ContainerError(`Token "${tokenToString(token)}" is not registered.`);
124
+ }
125
+ return;
126
+ }
127
+ const registration = registrations[registrations.length - 1];
128
+ const singletonCached = this.singletonCache.get(registration);
115
129
  if (singletonCached !== undefined) {
116
130
  return singletonCached;
117
131
  }
118
- const scopedCached = this.scopedInstances.get(token);
132
+ const scopedCached = this.scopedCache.get(registration);
119
133
  if (scopedCached !== undefined) {
120
134
  return scopedCached;
121
135
  }
122
- const registration = this.registrations.get(token);
123
- if (registration === undefined) {
124
- if (required) {
125
- throw new ContainerError(`Token "${tokenToString(token)}" is not registered.`);
126
- }
127
- return;
128
- }
129
136
  if (this.resolvingTokens.has(token)) {
130
137
  throw new ContainerError(`Circular dependency detected: ${buildCircularPath(this.resolvingTokens, token)}`);
131
138
  }
@@ -133,13 +140,51 @@ class Scope {
133
140
  try {
134
141
  const instance = registration.factory(this);
135
142
  if (registration.lifetime === "singleton") {
136
- this.singletonInstances.set(token, instance);
143
+ this.singletonCache.set(registration, instance);
137
144
  } else if (registration.lifetime === "scoped") {
138
- this.scopedInstances.set(token, instance);
145
+ this.scopedCache.set(registration, instance);
139
146
  }
140
147
  return instance;
141
148
  } finally {
142
149
  this.resolvingTokens.delete(token);
143
150
  }
144
151
  }
152
+ resolveAllTokens(token, required) {
153
+ if (this.disposed) {
154
+ throw new ContainerError("Cannot resolve from a disposed scope.");
155
+ }
156
+ const registrations = this.registrations.get(token);
157
+ if (registrations === undefined || registrations.length === 0) {
158
+ if (required) {
159
+ throw new ContainerError(`Token "${tokenToString(token)}" is not registered.`);
160
+ }
161
+ return;
162
+ }
163
+ if (this.resolvingTokens.has(token)) {
164
+ throw new ContainerError(`Circular dependency detected: ${buildCircularPath(this.resolvingTokens, token)}`);
165
+ }
166
+ this.resolvingTokens.add(token);
167
+ try {
168
+ return registrations.map((registration) => {
169
+ const reg = registration;
170
+ const singletonCached = this.singletonCache.get(registration);
171
+ if (singletonCached !== undefined) {
172
+ return singletonCached;
173
+ }
174
+ const scopedCached = this.scopedCache.get(registration);
175
+ if (scopedCached !== undefined) {
176
+ return scopedCached;
177
+ }
178
+ const instance = reg.factory(this);
179
+ if (reg.lifetime === "singleton") {
180
+ this.singletonCache.set(registration, instance);
181
+ } else if (reg.lifetime === "scoped") {
182
+ this.scopedCache.set(registration, instance);
183
+ }
184
+ return instance;
185
+ });
186
+ } finally {
187
+ this.resolvingTokens.delete(token);
188
+ }
189
+ }
145
190
  }
@@ -32,8 +32,8 @@ export declare function createScope<T, Sync extends AbstractConstructor, Async e
32
32
  */
33
33
  export declare class Scope<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
34
  private readonly registrations;
35
- private readonly singletonInstances;
36
- private readonly scopedInstances;
35
+ private readonly singletonCache;
36
+ private readonly scopedCache;
37
37
  private readonly resolvingTokens;
38
38
  private disposed;
39
39
  /**
@@ -42,7 +42,7 @@ export declare class Scope<T = Record<never, never>, Sync extends AbstractConstr
42
42
  * @internal
43
43
  */
44
44
  readonly [INTERNALS]: ContainerInternals;
45
- constructor(registrations: Map<unknown, Registration>, singletonInstances: Map<unknown, unknown>);
45
+ constructor(registrations: Map<unknown, Registration[]>, singletonCache: Map<Registration, unknown>);
46
46
  /**
47
47
  * Resolve an instance for the given token.
48
48
  *
@@ -71,12 +71,49 @@ export declare class Scope<T = Record<never, never>, Sync extends AbstractConstr
71
71
  tryResolve<K extends keyof (T & ScopedT)>(token: K): (T & ScopedT)[K] | undefined;
72
72
  tryResolve<V>(token: AbstractConstructor<V>): V | Promise<V> | undefined;
73
73
  tryResolve(token: PropertyKey): unknown;
74
+ /**
75
+ * Resolve all instances for the given token.
76
+ *
77
+ * Returns an array of instances from all registered factories for the token,
78
+ * in registration order.
79
+ *
80
+ * @param token A registered token
81
+ * @returns An array of instances associated with the token
82
+ * @throws ContainerError if the token is not registered
83
+ */
84
+ resolveAll<V>(token: AbstractConstructor<V> & (Async | ScopedAsync)): Promise<V>[];
85
+ resolveAll<V>(token: AbstractConstructor<V> & (Sync | ScopedSync)): V[];
86
+ resolveAll<K extends keyof (T & ScopedT)>(token: K): (T & ScopedT)[K][];
87
+ /**
88
+ * Try to resolve all instances for the given token.
89
+ *
90
+ * Returns `undefined` instead of throwing when the token is not registered.
91
+ * Other errors (circular dependency, disposed scope) are still thrown.
92
+ *
93
+ * @param token A token to resolve
94
+ * @returns An array of instances associated with the token, or `undefined` if not registered
95
+ */
96
+ tryResolveAll<V>(token: AbstractConstructor<V> & (Async | ScopedAsync)): Promise<V>[] | undefined;
97
+ tryResolveAll<V>(token: AbstractConstructor<V> & (Sync | ScopedSync)): V[] | undefined;
98
+ tryResolveAll<K extends keyof (T & ScopedT)>(token: K): (T & ScopedT)[K][] | undefined;
99
+ tryResolveAll<V>(token: AbstractConstructor<V>): (V | Promise<V>)[] | undefined;
100
+ tryResolveAll(token: PropertyKey): unknown;
74
101
  /**
75
102
  * Internal resolution logic shared by resolve and tryResolve.
103
+ * Resolves the last registered factory for the token.
76
104
  *
77
105
  * @param token Token to resolve
78
106
  * @param required If true, throws when the token is not registered. If false, returns undefined.
79
107
  * @returns The resolved instance, or undefined if not registered and required is false
80
108
  */
81
109
  private resolveToken;
110
+ /**
111
+ * Internal resolution logic shared by resolveAll and tryResolveAll.
112
+ * Resolves all registered factories for the token.
113
+ *
114
+ * @param token Token to resolve
115
+ * @param required If true, throws when the token is not registered. If false, returns undefined.
116
+ * @returns An array of resolved instances, or undefined if not registered and required is false
117
+ */
118
+ private resolveAllTokens;
82
119
  }
@@ -13,29 +13,29 @@ function createScope(source) {
13
13
  if (internals.isDisposed()) {
14
14
  throw new ContainerError("Cannot create a scope from a disposed container.");
15
15
  }
16
- return new Scope(internals.registrations, internals.instances);
16
+ return new Scope(internals.registrations, internals.singletonCache);
17
17
  }
18
18
 
19
19
  class Scope {
20
20
  registrations;
21
- singletonInstances;
22
- scopedInstances;
21
+ singletonCache;
22
+ scopedCache;
23
23
  resolvingTokens;
24
24
  disposed = false;
25
25
  [INTERNALS];
26
- constructor(registrations, singletonInstances) {
26
+ constructor(registrations, singletonCache) {
27
27
  this.registrations = registrations;
28
- this.singletonInstances = singletonInstances;
29
- this.scopedInstances = new Map;
28
+ this.singletonCache = singletonCache;
29
+ this.scopedCache = new Map;
30
30
  this.resolvingTokens = new Set;
31
31
  this[INTERNALS] = {
32
- instances: this.singletonInstances,
33
32
  isDisposed: () => this.disposed,
34
33
  markDisposed: () => {
35
34
  this.disposed = true;
36
35
  },
37
- ownInstances: this.scopedInstances,
38
- registrations: this.registrations
36
+ ownCache: this.scopedCache,
37
+ registrations: this.registrations,
38
+ singletonCache: this.singletonCache
39
39
  };
40
40
  }
41
41
  resolve(token) {
@@ -44,25 +44,32 @@ class Scope {
44
44
  tryResolve(token) {
45
45
  return this.resolveToken(token, false);
46
46
  }
47
+ resolveAll(token) {
48
+ return this.resolveAllTokens(token, true);
49
+ }
50
+ tryResolveAll(token) {
51
+ return this.resolveAllTokens(token, false);
52
+ }
47
53
  resolveToken(token, required) {
48
54
  if (this.disposed) {
49
55
  throw new ContainerError("Cannot resolve from a disposed scope.");
50
56
  }
51
- const singletonCached = this.singletonInstances.get(token);
57
+ const registrations = this.registrations.get(token);
58
+ if (registrations === undefined || registrations.length === 0) {
59
+ if (required) {
60
+ throw new ContainerError(`Token "${tokenToString(token)}" is not registered.`);
61
+ }
62
+ return;
63
+ }
64
+ const registration = registrations[registrations.length - 1];
65
+ const singletonCached = this.singletonCache.get(registration);
52
66
  if (singletonCached !== undefined) {
53
67
  return singletonCached;
54
68
  }
55
- const scopedCached = this.scopedInstances.get(token);
69
+ const scopedCached = this.scopedCache.get(registration);
56
70
  if (scopedCached !== undefined) {
57
71
  return scopedCached;
58
72
  }
59
- const registration = this.registrations.get(token);
60
- if (registration === undefined) {
61
- if (required) {
62
- throw new ContainerError(`Token "${tokenToString(token)}" is not registered.`);
63
- }
64
- return;
65
- }
66
73
  if (this.resolvingTokens.has(token)) {
67
74
  throw new ContainerError(`Circular dependency detected: ${buildCircularPath(this.resolvingTokens, token)}`);
68
75
  }
@@ -70,15 +77,53 @@ class Scope {
70
77
  try {
71
78
  const instance = registration.factory(this);
72
79
  if (registration.lifetime === "singleton") {
73
- this.singletonInstances.set(token, instance);
80
+ this.singletonCache.set(registration, instance);
74
81
  } else if (registration.lifetime === "scoped") {
75
- this.scopedInstances.set(token, instance);
82
+ this.scopedCache.set(registration, instance);
76
83
  }
77
84
  return instance;
78
85
  } finally {
79
86
  this.resolvingTokens.delete(token);
80
87
  }
81
88
  }
89
+ resolveAllTokens(token, required) {
90
+ if (this.disposed) {
91
+ throw new ContainerError("Cannot resolve from a disposed scope.");
92
+ }
93
+ const registrations = this.registrations.get(token);
94
+ if (registrations === undefined || registrations.length === 0) {
95
+ if (required) {
96
+ throw new ContainerError(`Token "${tokenToString(token)}" is not registered.`);
97
+ }
98
+ return;
99
+ }
100
+ if (this.resolvingTokens.has(token)) {
101
+ throw new ContainerError(`Circular dependency detected: ${buildCircularPath(this.resolvingTokens, token)}`);
102
+ }
103
+ this.resolvingTokens.add(token);
104
+ try {
105
+ return registrations.map((registration) => {
106
+ const reg = registration;
107
+ const singletonCached = this.singletonCache.get(registration);
108
+ if (singletonCached !== undefined) {
109
+ return singletonCached;
110
+ }
111
+ const scopedCached = this.scopedCache.get(registration);
112
+ if (scopedCached !== undefined) {
113
+ return scopedCached;
114
+ }
115
+ const instance = reg.factory(this);
116
+ if (reg.lifetime === "singleton") {
117
+ this.singletonCache.set(registration, instance);
118
+ } else if (reg.lifetime === "scoped") {
119
+ this.scopedCache.set(registration, instance);
120
+ }
121
+ return instance;
122
+ });
123
+ } finally {
124
+ this.resolvingTokens.delete(token);
125
+ }
126
+ }
82
127
  }
83
128
  export {
84
129
  createScope,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "katagami",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
4
  "description": "Lightweight DI container for TypeScript and JavaScript — full type inference, no decorators, no reflect-metadata, hybrid class & PropertyKey tokens.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -20,6 +20,11 @@
20
20
  "types": "./dist/disposable/index.d.ts",
21
21
  "import": "./dist/disposable/index.js",
22
22
  "require": "./dist/disposable/index.cjs"
23
+ },
24
+ "./lazy": {
25
+ "types": "./dist/lazy/index.d.ts",
26
+ "import": "./dist/lazy/index.js",
27
+ "require": "./dist/lazy/index.cjs"
23
28
  }
24
29
  },
25
30
  "main": "./dist/index.cjs",
@@ -45,14 +50,13 @@
45
50
  "clean": "rm -rf dist",
46
51
  "build": "bun run clean && bun run build:types && bun run build:esm && bun run build:cjs",
47
52
  "build:types": "tsc -p tsconfig.build.json",
48
- "build:esm": "bun build ./src/index.ts ./src/scope/index.ts ./src/disposable/index.ts --outdir dist --format esm --splitting",
49
- "build:cjs": "bun build ./src/index.ts --outfile dist/index.cjs --format cjs && bun build ./src/scope/index.ts --outfile dist/scope/index.cjs --format cjs && bun build ./src/disposable/index.ts --outfile dist/disposable/index.cjs --format cjs",
50
- "test": "bun test --coverage --dots",
53
+ "build:esm": "bun build ./src/index.ts ./src/scope/index.ts ./src/disposable/index.ts ./src/lazy/index.ts --outdir dist --format esm --splitting",
54
+ "build:cjs": "bun build ./src/index.ts --outfile dist/index.cjs --format cjs && bun build ./src/scope/index.ts --outfile dist/scope/index.cjs --format cjs && bun build ./src/disposable/index.ts --outfile dist/disposable/index.cjs --format cjs && bun build ./src/lazy/index.ts --outfile dist/lazy/index.cjs --format cjs",
51
55
  "prepublishOnly": "bun run build",
52
56
  "check": "bun run format",
53
57
  "lint": "biome lint .",
54
58
  "format": "biome check --write .",
55
- "verify": "bun run build:types && bun run check && bun run test"
59
+ "verify": "bun run build:types && bun run check && bun test"
56
60
  },
57
61
  "dependencies": {
58
62
  "@types/bun": "^1.3.8"