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