katagami 2.1.0 → 2.3.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 +94 -5
- package/dist/container/index.d.ts +49 -4
- package/dist/disposable/index.cjs +2 -2
- package/dist/disposable/index.d.ts +3 -3
- package/dist/disposable/index.js +2 -2
- package/dist/index.cjs +62 -15
- package/dist/index.d.ts +1 -0
- package/dist/index.js +62 -15
- package/dist/internal.d.ts +6 -6
- package/dist/lazy/index.cjs +88 -0
- package/dist/lazy/index.d.ts +35 -0
- package/dist/lazy/index.js +58 -0
- package/dist/resolver/index.d.ts +25 -0
- package/dist/scope/index.cjs +87 -20
- package/dist/scope/index.d.ts +41 -3
- package/dist/scope/index.js +87 -20
- package/package.json +9 -5
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@ Lightweight TypeScript DI container with full type inference.
|
|
|
17
17
|
| Zero dependencies | No decorators, no reflect-metadata, no polyfills — works with any bundler out of the box |
|
|
18
18
|
| Full type inference | Types accumulate through method chaining; unregistered tokens are compile-time errors |
|
|
19
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
|
|
20
|
+
| Captive dependency prevention | Singleton/Transient factories cannot access scoped tokens; caught at compile time and runtime in scopes |
|
|
21
21
|
| Hybrid token strategy | Class tokens for strict type safety, PropertyKey tokens for flexibility |
|
|
22
22
|
| Interface type map | Pass an interface to `createContainer<T>()` for order-independent registration |
|
|
23
23
|
| Three lifetimes | Singleton, Transient, and Scoped with child containers |
|
|
@@ -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
|
|
|
@@ -64,21 +65,45 @@ userService.greet('world');
|
|
|
64
65
|
|
|
65
66
|
Most TypeScript DI containers rely on decorators, reflect-metadata, or string-based tokens — each bringing trade-offs in tooling compatibility, type safety, or bundle size. Katagami takes a different approach.
|
|
66
67
|
|
|
68
|
+
> **Note:** This comparison was researched on 2026-02-09. Features may have changed since then.
|
|
69
|
+
|
|
70
|
+
| Aspect | Katagami | InversifyJS | tsyringe | TypeDI | Awilix | NestJS | Effect | typed-inject |
|
|
71
|
+
| --------------------------------- | ---------------------------------------------------------------- | -------------------------------- | ----------------------------------------------- | --------------------------------------- | ----------------------------------- | ------------------------------------- | --------------------------------- | ------------------------------ |
|
|
72
|
+
| **Runtime requirements** | ✅ None | ❌ reflect-metadata, decorators | ❌ reflect-metadata, decorators | ❌ reflect-metadata, decorators | ✅ None | ❌ reflect-metadata, decorators | ✅ None | ✅ None |
|
|
73
|
+
| **Lifetimes** | ✅ Singleton, Transient, Scoped | ✅ Singleton, Transient, Request | ✅ Singleton, Transient, Resolution / Container | Singleton, Transient (named containers) | ✅ Singleton, Transient, Scoped | ✅ Singleton, Transient, Request | Shared (memoized), Scoped | ❌ Singleton, Transient |
|
|
74
|
+
| **Injection style** | Constructor (explicit factory) | Constructor, Property | Constructor | Constructor, Property | Constructor (proxy / classic) | Constructor | Functional (Tag + Layer) | Constructor (static inject) |
|
|
75
|
+
| **Token types** | Class, PropertyKey, Interface map | Class, String, Symbol | Class, String, Symbol | Class, String, Token\<T\> | String | Class, String, Symbol, InjectionToken | Context.Tag | String literal |
|
|
76
|
+
| **Type safety** | ✅ Compile-time; full inference, captive-dep guard (+ runtime) | ❌ Generic binding types | ❌ Generic types | ❌ Generic types, Token\<T\> | ❌ Cradle interface typing | ❌ Generic types | ✅ Compile-time; R type parameter | ✅ Compile-time; static inject |
|
|
77
|
+
| **Resource cleanup** | ✅ TC39 Symbol.dispose / asyncDispose; await using | Deactivation handlers | container.dispose() | Container.reset() | Disposer functions | Lifecycle hooks (onModuleDestroy) | Scope finalizers; acquireRelease | injector.dispose() |
|
|
78
|
+
| **Tree-shaking** | ✅ Subpath exports; sideEffects: false | ❌ | ❌ | ❌ | ✅ No decorator / metadata overhead | ❌ | ✅ Subpath exports; ESM | ✅ Zero deps; small bundle |
|
|
79
|
+
| **Async factories** | ✅ | ✅ | ❌ | ❌ | ❌ | ✅ | ✅ | ❌ |
|
|
80
|
+
| **Optional resolution** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ |
|
|
81
|
+
| **Multi-binding** | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ |
|
|
82
|
+
| **Lazy resolution** | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ LazyModuleLoader | ✅ Lazy by design | ❌ |
|
|
83
|
+
| **Conditional bindings** | ✅ Token separation + factory logic + scopes | ✅ Named, tagged, contextual | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
|
|
84
|
+
| **Auto-loading** | ✅ use() module composition (explicit, decorator-free by design) | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ |
|
|
85
|
+
| **Child containers** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
86
|
+
| **Module system** | ✅ | ✅ | ❌ | ❌ | ✅ | ✅ | ✅ | ❌ |
|
|
87
|
+
| **Circular dependency detection** | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ | ❌ |
|
|
88
|
+
| **Middleware / Interceptors** | ✅ Higher-order factory wrappers | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
|
|
89
|
+
| **Snapshot / Restore** | ✅ Immutable containers; use() for test isolation | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
|
|
90
|
+
|
|
67
91
|
### No decorators, no reflect-metadata
|
|
68
92
|
|
|
69
93
|
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.
|
|
70
94
|
|
|
71
95
|
### Tree-shakeable
|
|
72
96
|
|
|
73
|
-
Katagami is split into subpath exports. Import only what you use — `katagami/scope` and `katagami/
|
|
97
|
+
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
98
|
|
|
75
99
|
```ts
|
|
76
|
-
// Core only — scope and
|
|
100
|
+
// Core only — scope, disposable, and lazy are not included in the bundle
|
|
77
101
|
import { createContainer } from 'katagami';
|
|
78
102
|
|
|
79
103
|
// Import only what you need
|
|
80
104
|
import { createScope } from 'katagami/scope';
|
|
81
105
|
import { disposable } from 'katagami/disposable';
|
|
106
|
+
import { lazy } from 'katagami/lazy';
|
|
82
107
|
```
|
|
83
108
|
|
|
84
109
|
### Full type inference from class tokens
|
|
@@ -340,17 +365,59 @@ container.resolve(Connection); // OK
|
|
|
340
365
|
container.registerSingleton(/* ... */); // Compile-time error
|
|
341
366
|
```
|
|
342
367
|
|
|
368
|
+
### Lazy Resolution
|
|
369
|
+
|
|
370
|
+
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.
|
|
371
|
+
|
|
372
|
+
```ts
|
|
373
|
+
import { createContainer } from 'katagami';
|
|
374
|
+
import { lazy } from 'katagami/lazy';
|
|
375
|
+
|
|
376
|
+
class HeavyService {
|
|
377
|
+
constructor() {
|
|
378
|
+
// expensive initialization
|
|
379
|
+
}
|
|
380
|
+
process() {
|
|
381
|
+
return 'done';
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const container = createContainer().registerSingleton(HeavyService, () => new HeavyService());
|
|
386
|
+
|
|
387
|
+
const service = lazy(container, HeavyService);
|
|
388
|
+
// HeavyService is NOT instantiated yet
|
|
389
|
+
|
|
390
|
+
service.process(); // instance created here, then cached
|
|
391
|
+
service.process(); // uses the cached instance
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
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.
|
|
395
|
+
|
|
396
|
+
Only **sync class tokens** are supported. Async tokens and PropertyKey tokens are rejected at the type level because Proxy traps are synchronous.
|
|
397
|
+
|
|
398
|
+
`lazy()` works with Container, Scope, DisposableContainer, and DisposableScope:
|
|
399
|
+
|
|
400
|
+
```ts
|
|
401
|
+
import { createScope } from 'katagami/scope';
|
|
402
|
+
|
|
403
|
+
const root = createContainer().registerScoped(RequestContext, () => new RequestContext());
|
|
404
|
+
const scope = createScope(root);
|
|
405
|
+
|
|
406
|
+
const ctx = lazy(scope, RequestContext); // deferred scoped resolution
|
|
407
|
+
```
|
|
408
|
+
|
|
343
409
|
### Tree Shaking
|
|
344
410
|
|
|
345
|
-
Katagami uses subpath exports to split functionality into independent entry points. If you only need the core container, `katagami/scope` and `katagami/
|
|
411
|
+
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
412
|
|
|
347
413
|
```ts
|
|
348
|
-
// Core only — scope and
|
|
414
|
+
// Core only — scope, disposable, and lazy are not included in the bundle
|
|
349
415
|
import { createContainer } from 'katagami';
|
|
350
416
|
|
|
351
417
|
// Import only what you need
|
|
352
418
|
import { createScope } from 'katagami/scope';
|
|
353
419
|
import { disposable } from 'katagami/disposable';
|
|
420
|
+
import { lazy } from 'katagami/lazy';
|
|
354
421
|
```
|
|
355
422
|
|
|
356
423
|
### Interface Type Map
|
|
@@ -424,6 +491,24 @@ const container = createContainer()
|
|
|
424
491
|
});
|
|
425
492
|
```
|
|
426
493
|
|
|
494
|
+
Katagami also enforces this rule at runtime within scopes. If a singleton factory attempts to resolve a scoped token — directly or through intermediaries — a `ContainerError` is thrown:
|
|
495
|
+
|
|
496
|
+
```ts
|
|
497
|
+
import { createContainer } from 'katagami';
|
|
498
|
+
import { createScope } from 'katagami/scope';
|
|
499
|
+
|
|
500
|
+
class DbPool {}
|
|
501
|
+
class RequestContext {}
|
|
502
|
+
|
|
503
|
+
const container = createContainer()
|
|
504
|
+
.registerScoped(RequestContext, () => new RequestContext())
|
|
505
|
+
.registerSingleton(DbPool, r => new DbPool(r.resolve(RequestContext)));
|
|
506
|
+
|
|
507
|
+
const scope = createScope(container);
|
|
508
|
+
scope.resolve(DbPool);
|
|
509
|
+
// ContainerError: Captive dependency detected: scoped token "RequestContext" cannot be resolved inside a singleton factory.
|
|
510
|
+
```
|
|
511
|
+
|
|
427
512
|
### Optional Resolution (tryResolve)
|
|
428
513
|
|
|
429
514
|
When you need to handle optional dependencies or want to check if a token is registered without throwing an error, use `tryResolve`. Unlike `resolve`, it returns `undefined` for unregistered tokens instead of throwing `ContainerError`:
|
|
@@ -522,6 +607,10 @@ Resolves and returns the instance for the given token. Behaves the same as `Cont
|
|
|
522
607
|
|
|
523
608
|
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
609
|
|
|
610
|
+
### `lazy(source, token)` — `katagami/lazy`
|
|
611
|
+
|
|
612
|
+
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`.
|
|
613
|
+
|
|
525
614
|
### `disposable(container)` — `katagami/disposable`
|
|
526
615
|
|
|
527
616
|
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
|
|
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
|
|
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
|
-
*
|
|
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.
|
|
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.
|
|
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 `
|
|
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 `
|
|
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 `
|
|
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
|
package/dist/disposable/index.js
CHANGED
|
@@ -10,7 +10,7 @@ function disposable(container) {
|
|
|
10
10
|
return;
|
|
11
11
|
}
|
|
12
12
|
internals.markDisposed();
|
|
13
|
-
const instances = [...internals.
|
|
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.
|
|
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
|
-
|
|
81
|
+
singletonCache;
|
|
82
82
|
resolvingTokens;
|
|
83
83
|
disposed = false;
|
|
84
84
|
[INTERNALS];
|
|
85
85
|
constructor() {
|
|
86
86
|
this.registrations = new Map;
|
|
87
|
-
this.
|
|
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
|
-
|
|
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,
|
|
110
|
-
this.registrations.set(token,
|
|
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
|
|
125
|
-
if (
|
|
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.
|
|
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.
|
|
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
|
-
|
|
17
|
+
singletonCache;
|
|
18
18
|
resolvingTokens;
|
|
19
19
|
disposed = false;
|
|
20
20
|
[INTERNALS];
|
|
21
21
|
constructor() {
|
|
22
22
|
this.registrations = new Map;
|
|
23
|
-
this.
|
|
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
|
-
|
|
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,
|
|
46
|
-
this.registrations.set(token,
|
|
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
|
|
61
|
-
if (
|
|
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.
|
|
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.
|
|
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
|
}
|
package/dist/internal.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
20
|
-
readonly
|
|
21
|
-
/** Instances owned by this container / scope (disposal target). */
|
|
22
|
-
readonly
|
|
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
|
+
};
|
package/dist/resolver/index.d.ts
CHANGED
|
@@ -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.
|
package/dist/scope/index.cjs
CHANGED
|
@@ -76,29 +76,30 @@ 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.
|
|
79
|
+
return new Scope(internals.registrations, internals.singletonCache);
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
class Scope {
|
|
83
83
|
registrations;
|
|
84
|
-
|
|
85
|
-
|
|
84
|
+
singletonCache;
|
|
85
|
+
scopedCache;
|
|
86
86
|
resolvingTokens;
|
|
87
|
+
singletonDepth = 0;
|
|
87
88
|
disposed = false;
|
|
88
89
|
[INTERNALS];
|
|
89
|
-
constructor(registrations,
|
|
90
|
+
constructor(registrations, singletonCache) {
|
|
90
91
|
this.registrations = registrations;
|
|
91
|
-
this.
|
|
92
|
-
this.
|
|
92
|
+
this.singletonCache = singletonCache;
|
|
93
|
+
this.scopedCache = new Map;
|
|
93
94
|
this.resolvingTokens = new Set;
|
|
94
95
|
this[INTERNALS] = {
|
|
95
|
-
instances: this.singletonInstances,
|
|
96
96
|
isDisposed: () => this.disposed,
|
|
97
97
|
markDisposed: () => {
|
|
98
98
|
this.disposed = true;
|
|
99
99
|
},
|
|
100
|
-
|
|
101
|
-
registrations: this.registrations
|
|
100
|
+
ownCache: this.scopedCache,
|
|
101
|
+
registrations: this.registrations,
|
|
102
|
+
singletonCache: this.singletonCache
|
|
102
103
|
};
|
|
103
104
|
}
|
|
104
105
|
resolve(token) {
|
|
@@ -107,37 +108,103 @@ class Scope {
|
|
|
107
108
|
tryResolve(token) {
|
|
108
109
|
return this.resolveToken(token, false);
|
|
109
110
|
}
|
|
111
|
+
resolveAll(token) {
|
|
112
|
+
return this.resolveAllTokens(token, true);
|
|
113
|
+
}
|
|
114
|
+
tryResolveAll(token) {
|
|
115
|
+
return this.resolveAllTokens(token, false);
|
|
116
|
+
}
|
|
110
117
|
resolveToken(token, required) {
|
|
111
118
|
if (this.disposed) {
|
|
112
119
|
throw new ContainerError("Cannot resolve from a disposed scope.");
|
|
113
120
|
}
|
|
114
|
-
const
|
|
121
|
+
const registrations = this.registrations.get(token);
|
|
122
|
+
if (registrations === undefined || registrations.length === 0) {
|
|
123
|
+
if (required) {
|
|
124
|
+
throw new ContainerError(`Token "${tokenToString(token)}" is not registered.`);
|
|
125
|
+
}
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const registration = registrations[registrations.length - 1];
|
|
129
|
+
const singletonCached = this.singletonCache.get(registration);
|
|
115
130
|
if (singletonCached !== undefined) {
|
|
116
131
|
return singletonCached;
|
|
117
132
|
}
|
|
118
|
-
|
|
133
|
+
if (registration.lifetime === "scoped" && this.singletonDepth > 0) {
|
|
134
|
+
throw new ContainerError(`Captive dependency detected: scoped token "${tokenToString(token)}" cannot be resolved inside a singleton factory. Scoped instances must not be captured by singletons.`);
|
|
135
|
+
}
|
|
136
|
+
const scopedCached = this.scopedCache.get(registration);
|
|
119
137
|
if (scopedCached !== undefined) {
|
|
120
138
|
return scopedCached;
|
|
121
139
|
}
|
|
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
140
|
if (this.resolvingTokens.has(token)) {
|
|
130
141
|
throw new ContainerError(`Circular dependency detected: ${buildCircularPath(this.resolvingTokens, token)}`);
|
|
131
142
|
}
|
|
132
143
|
this.resolvingTokens.add(token);
|
|
144
|
+
if (registration.lifetime === "singleton") {
|
|
145
|
+
this.singletonDepth++;
|
|
146
|
+
}
|
|
133
147
|
try {
|
|
134
148
|
const instance = registration.factory(this);
|
|
135
149
|
if (registration.lifetime === "singleton") {
|
|
136
|
-
this.
|
|
150
|
+
this.singletonCache.set(registration, instance);
|
|
137
151
|
} else if (registration.lifetime === "scoped") {
|
|
138
|
-
this.
|
|
152
|
+
this.scopedCache.set(registration, instance);
|
|
139
153
|
}
|
|
140
154
|
return instance;
|
|
155
|
+
} finally {
|
|
156
|
+
if (registration.lifetime === "singleton") {
|
|
157
|
+
this.singletonDepth--;
|
|
158
|
+
}
|
|
159
|
+
this.resolvingTokens.delete(token);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
resolveAllTokens(token, required) {
|
|
163
|
+
if (this.disposed) {
|
|
164
|
+
throw new ContainerError("Cannot resolve from a disposed scope.");
|
|
165
|
+
}
|
|
166
|
+
const registrations = this.registrations.get(token);
|
|
167
|
+
if (registrations === undefined || registrations.length === 0) {
|
|
168
|
+
if (required) {
|
|
169
|
+
throw new ContainerError(`Token "${tokenToString(token)}" is not registered.`);
|
|
170
|
+
}
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
if (this.resolvingTokens.has(token)) {
|
|
174
|
+
throw new ContainerError(`Circular dependency detected: ${buildCircularPath(this.resolvingTokens, token)}`);
|
|
175
|
+
}
|
|
176
|
+
this.resolvingTokens.add(token);
|
|
177
|
+
try {
|
|
178
|
+
return registrations.map((registration) => {
|
|
179
|
+
const reg = registration;
|
|
180
|
+
const singletonCached = this.singletonCache.get(registration);
|
|
181
|
+
if (singletonCached !== undefined) {
|
|
182
|
+
return singletonCached;
|
|
183
|
+
}
|
|
184
|
+
if (reg.lifetime === "scoped" && this.singletonDepth > 0) {
|
|
185
|
+
throw new ContainerError(`Captive dependency detected: scoped token "${tokenToString(token)}" cannot be resolved inside a singleton factory. Scoped instances must not be captured by singletons.`);
|
|
186
|
+
}
|
|
187
|
+
const scopedCached = this.scopedCache.get(registration);
|
|
188
|
+
if (scopedCached !== undefined) {
|
|
189
|
+
return scopedCached;
|
|
190
|
+
}
|
|
191
|
+
if (reg.lifetime === "singleton") {
|
|
192
|
+
this.singletonDepth++;
|
|
193
|
+
}
|
|
194
|
+
try {
|
|
195
|
+
const instance = reg.factory(this);
|
|
196
|
+
if (reg.lifetime === "singleton") {
|
|
197
|
+
this.singletonCache.set(registration, instance);
|
|
198
|
+
} else if (reg.lifetime === "scoped") {
|
|
199
|
+
this.scopedCache.set(registration, instance);
|
|
200
|
+
}
|
|
201
|
+
return instance;
|
|
202
|
+
} finally {
|
|
203
|
+
if (reg.lifetime === "singleton") {
|
|
204
|
+
this.singletonDepth--;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
});
|
|
141
208
|
} finally {
|
|
142
209
|
this.resolvingTokens.delete(token);
|
|
143
210
|
}
|
package/dist/scope/index.d.ts
CHANGED
|
@@ -32,9 +32,10 @@ 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
|
|
36
|
-
private readonly
|
|
35
|
+
private readonly singletonCache;
|
|
36
|
+
private readonly scopedCache;
|
|
37
37
|
private readonly resolvingTokens;
|
|
38
|
+
private singletonDepth;
|
|
38
39
|
private disposed;
|
|
39
40
|
/**
|
|
40
41
|
* Internal state accessor for extension modules (scope, disposable).
|
|
@@ -42,7 +43,7 @@ export declare class Scope<T = Record<never, never>, Sync extends AbstractConstr
|
|
|
42
43
|
* @internal
|
|
43
44
|
*/
|
|
44
45
|
readonly [INTERNALS]: ContainerInternals;
|
|
45
|
-
constructor(registrations: Map<unknown, Registration>,
|
|
46
|
+
constructor(registrations: Map<unknown, Registration[]>, singletonCache: Map<Registration, unknown>);
|
|
46
47
|
/**
|
|
47
48
|
* Resolve an instance for the given token.
|
|
48
49
|
*
|
|
@@ -71,12 +72,49 @@ export declare class Scope<T = Record<never, never>, Sync extends AbstractConstr
|
|
|
71
72
|
tryResolve<K extends keyof (T & ScopedT)>(token: K): (T & ScopedT)[K] | undefined;
|
|
72
73
|
tryResolve<V>(token: AbstractConstructor<V>): V | Promise<V> | undefined;
|
|
73
74
|
tryResolve(token: PropertyKey): unknown;
|
|
75
|
+
/**
|
|
76
|
+
* Resolve all instances for the given token.
|
|
77
|
+
*
|
|
78
|
+
* Returns an array of instances from all registered factories for the token,
|
|
79
|
+
* in registration order.
|
|
80
|
+
*
|
|
81
|
+
* @param token A registered token
|
|
82
|
+
* @returns An array of instances associated with the token
|
|
83
|
+
* @throws ContainerError if the token is not registered
|
|
84
|
+
*/
|
|
85
|
+
resolveAll<V>(token: AbstractConstructor<V> & (Async | ScopedAsync)): Promise<V>[];
|
|
86
|
+
resolveAll<V>(token: AbstractConstructor<V> & (Sync | ScopedSync)): V[];
|
|
87
|
+
resolveAll<K extends keyof (T & ScopedT)>(token: K): (T & ScopedT)[K][];
|
|
88
|
+
/**
|
|
89
|
+
* Try to resolve all instances for the given token.
|
|
90
|
+
*
|
|
91
|
+
* Returns `undefined` instead of throwing when the token is not registered.
|
|
92
|
+
* Other errors (circular dependency, disposed scope) are still thrown.
|
|
93
|
+
*
|
|
94
|
+
* @param token A token to resolve
|
|
95
|
+
* @returns An array of instances associated with the token, or `undefined` if not registered
|
|
96
|
+
*/
|
|
97
|
+
tryResolveAll<V>(token: AbstractConstructor<V> & (Async | ScopedAsync)): Promise<V>[] | undefined;
|
|
98
|
+
tryResolveAll<V>(token: AbstractConstructor<V> & (Sync | ScopedSync)): V[] | undefined;
|
|
99
|
+
tryResolveAll<K extends keyof (T & ScopedT)>(token: K): (T & ScopedT)[K][] | undefined;
|
|
100
|
+
tryResolveAll<V>(token: AbstractConstructor<V>): (V | Promise<V>)[] | undefined;
|
|
101
|
+
tryResolveAll(token: PropertyKey): unknown;
|
|
74
102
|
/**
|
|
75
103
|
* Internal resolution logic shared by resolve and tryResolve.
|
|
104
|
+
* Resolves the last registered factory for the token.
|
|
76
105
|
*
|
|
77
106
|
* @param token Token to resolve
|
|
78
107
|
* @param required If true, throws when the token is not registered. If false, returns undefined.
|
|
79
108
|
* @returns The resolved instance, or undefined if not registered and required is false
|
|
80
109
|
*/
|
|
81
110
|
private resolveToken;
|
|
111
|
+
/**
|
|
112
|
+
* Internal resolution logic shared by resolveAll and tryResolveAll.
|
|
113
|
+
* Resolves all registered factories for the token.
|
|
114
|
+
*
|
|
115
|
+
* @param token Token to resolve
|
|
116
|
+
* @param required If true, throws when the token is not registered. If false, returns undefined.
|
|
117
|
+
* @returns An array of resolved instances, or undefined if not registered and required is false
|
|
118
|
+
*/
|
|
119
|
+
private resolveAllTokens;
|
|
82
120
|
}
|
package/dist/scope/index.js
CHANGED
|
@@ -13,29 +13,30 @@ 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.
|
|
16
|
+
return new Scope(internals.registrations, internals.singletonCache);
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
class Scope {
|
|
20
20
|
registrations;
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
singletonCache;
|
|
22
|
+
scopedCache;
|
|
23
23
|
resolvingTokens;
|
|
24
|
+
singletonDepth = 0;
|
|
24
25
|
disposed = false;
|
|
25
26
|
[INTERNALS];
|
|
26
|
-
constructor(registrations,
|
|
27
|
+
constructor(registrations, singletonCache) {
|
|
27
28
|
this.registrations = registrations;
|
|
28
|
-
this.
|
|
29
|
-
this.
|
|
29
|
+
this.singletonCache = singletonCache;
|
|
30
|
+
this.scopedCache = new Map;
|
|
30
31
|
this.resolvingTokens = new Set;
|
|
31
32
|
this[INTERNALS] = {
|
|
32
|
-
instances: this.singletonInstances,
|
|
33
33
|
isDisposed: () => this.disposed,
|
|
34
34
|
markDisposed: () => {
|
|
35
35
|
this.disposed = true;
|
|
36
36
|
},
|
|
37
|
-
|
|
38
|
-
registrations: this.registrations
|
|
37
|
+
ownCache: this.scopedCache,
|
|
38
|
+
registrations: this.registrations,
|
|
39
|
+
singletonCache: this.singletonCache
|
|
39
40
|
};
|
|
40
41
|
}
|
|
41
42
|
resolve(token) {
|
|
@@ -44,37 +45,103 @@ class Scope {
|
|
|
44
45
|
tryResolve(token) {
|
|
45
46
|
return this.resolveToken(token, false);
|
|
46
47
|
}
|
|
48
|
+
resolveAll(token) {
|
|
49
|
+
return this.resolveAllTokens(token, true);
|
|
50
|
+
}
|
|
51
|
+
tryResolveAll(token) {
|
|
52
|
+
return this.resolveAllTokens(token, false);
|
|
53
|
+
}
|
|
47
54
|
resolveToken(token, required) {
|
|
48
55
|
if (this.disposed) {
|
|
49
56
|
throw new ContainerError("Cannot resolve from a disposed scope.");
|
|
50
57
|
}
|
|
51
|
-
const
|
|
58
|
+
const registrations = this.registrations.get(token);
|
|
59
|
+
if (registrations === undefined || registrations.length === 0) {
|
|
60
|
+
if (required) {
|
|
61
|
+
throw new ContainerError(`Token "${tokenToString(token)}" is not registered.`);
|
|
62
|
+
}
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const registration = registrations[registrations.length - 1];
|
|
66
|
+
const singletonCached = this.singletonCache.get(registration);
|
|
52
67
|
if (singletonCached !== undefined) {
|
|
53
68
|
return singletonCached;
|
|
54
69
|
}
|
|
55
|
-
|
|
70
|
+
if (registration.lifetime === "scoped" && this.singletonDepth > 0) {
|
|
71
|
+
throw new ContainerError(`Captive dependency detected: scoped token "${tokenToString(token)}" cannot be resolved inside a singleton factory. Scoped instances must not be captured by singletons.`);
|
|
72
|
+
}
|
|
73
|
+
const scopedCached = this.scopedCache.get(registration);
|
|
56
74
|
if (scopedCached !== undefined) {
|
|
57
75
|
return scopedCached;
|
|
58
76
|
}
|
|
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
77
|
if (this.resolvingTokens.has(token)) {
|
|
67
78
|
throw new ContainerError(`Circular dependency detected: ${buildCircularPath(this.resolvingTokens, token)}`);
|
|
68
79
|
}
|
|
69
80
|
this.resolvingTokens.add(token);
|
|
81
|
+
if (registration.lifetime === "singleton") {
|
|
82
|
+
this.singletonDepth++;
|
|
83
|
+
}
|
|
70
84
|
try {
|
|
71
85
|
const instance = registration.factory(this);
|
|
72
86
|
if (registration.lifetime === "singleton") {
|
|
73
|
-
this.
|
|
87
|
+
this.singletonCache.set(registration, instance);
|
|
74
88
|
} else if (registration.lifetime === "scoped") {
|
|
75
|
-
this.
|
|
89
|
+
this.scopedCache.set(registration, instance);
|
|
76
90
|
}
|
|
77
91
|
return instance;
|
|
92
|
+
} finally {
|
|
93
|
+
if (registration.lifetime === "singleton") {
|
|
94
|
+
this.singletonDepth--;
|
|
95
|
+
}
|
|
96
|
+
this.resolvingTokens.delete(token);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
resolveAllTokens(token, required) {
|
|
100
|
+
if (this.disposed) {
|
|
101
|
+
throw new ContainerError("Cannot resolve from a disposed scope.");
|
|
102
|
+
}
|
|
103
|
+
const registrations = this.registrations.get(token);
|
|
104
|
+
if (registrations === undefined || registrations.length === 0) {
|
|
105
|
+
if (required) {
|
|
106
|
+
throw new ContainerError(`Token "${tokenToString(token)}" is not registered.`);
|
|
107
|
+
}
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (this.resolvingTokens.has(token)) {
|
|
111
|
+
throw new ContainerError(`Circular dependency detected: ${buildCircularPath(this.resolvingTokens, token)}`);
|
|
112
|
+
}
|
|
113
|
+
this.resolvingTokens.add(token);
|
|
114
|
+
try {
|
|
115
|
+
return registrations.map((registration) => {
|
|
116
|
+
const reg = registration;
|
|
117
|
+
const singletonCached = this.singletonCache.get(registration);
|
|
118
|
+
if (singletonCached !== undefined) {
|
|
119
|
+
return singletonCached;
|
|
120
|
+
}
|
|
121
|
+
if (reg.lifetime === "scoped" && this.singletonDepth > 0) {
|
|
122
|
+
throw new ContainerError(`Captive dependency detected: scoped token "${tokenToString(token)}" cannot be resolved inside a singleton factory. Scoped instances must not be captured by singletons.`);
|
|
123
|
+
}
|
|
124
|
+
const scopedCached = this.scopedCache.get(registration);
|
|
125
|
+
if (scopedCached !== undefined) {
|
|
126
|
+
return scopedCached;
|
|
127
|
+
}
|
|
128
|
+
if (reg.lifetime === "singleton") {
|
|
129
|
+
this.singletonDepth++;
|
|
130
|
+
}
|
|
131
|
+
try {
|
|
132
|
+
const instance = reg.factory(this);
|
|
133
|
+
if (reg.lifetime === "singleton") {
|
|
134
|
+
this.singletonCache.set(registration, instance);
|
|
135
|
+
} else if (reg.lifetime === "scoped") {
|
|
136
|
+
this.scopedCache.set(registration, instance);
|
|
137
|
+
}
|
|
138
|
+
return instance;
|
|
139
|
+
} finally {
|
|
140
|
+
if (reg.lifetime === "singleton") {
|
|
141
|
+
this.singletonDepth--;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
});
|
|
78
145
|
} finally {
|
|
79
146
|
this.resolvingTokens.delete(token);
|
|
80
147
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "katagami",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.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
|
|
59
|
+
"verify": "bun run build:types && bun run check && bun test"
|
|
56
60
|
},
|
|
57
61
|
"dependencies": {
|
|
58
62
|
"@types/bun": "^1.3.8"
|