katagami 1.1.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- [English](./README.md) | [日本語](./README.ja.md) | [한국어](./README.ko.md) | [繁體中文](./README.zh-TW.md) | [简体中文](./README.zh-CN.md) | [Español](./README.es.md) | [Deutsch](./README.de.md) | [Français](./README.fr.md)
1
+ [English](./README.md) | [日本語](./docs/README.ja.md) | [한국어](./docs/README.ko.md) | [繁體中文](./docs/README.zh-TW.md) | [简体中文](./docs/README.zh-CN.md) | [Español](./docs/README.es.md) | [Deutsch](./docs/README.de.md) | [Français](./docs/README.fr.md)
2
2
 
3
3
  # Katagami
4
4
 
@@ -8,22 +8,24 @@ Lightweight TypeScript DI container with full type inference.
8
8
  [![license](https://img.shields.io/npm/l/katagami)](https://github.com/hiroiku/katagami/blob/master/LICENSE)
9
9
  [![bundle size](https://img.shields.io/bundlephobia/minzip/katagami)](https://bundlephobia.com/package/katagami)
10
10
 
11
- > The name comes from 型紙 _(katagami)_ — precision stencil paper used in traditional Japanese dyeing to transfer exact patterns onto fabric. Multiple stencils are layered to compose intricate designs, just as types accumulate through each method-chain call. A stencil needs only paper and a brush, no elaborate machinery likewise, Katagami requires no decorators or metadata mechanisms and works with any build tool out of the box. And like stencils that work across different fabrics and techniques, Katagami adapts across TypeScript and JavaScript, class tokens and PropertyKey tokens a hybrid approach to strict, composable DI.
11
+ > The name comes from 型紙 _(katagami)_ — precision stencil paper used in traditional Japanese dyeing to transfer exact patterns onto fabric. Multiple stencils are layered to compose intricate designs, just as types accumulate through each method-chain call. Each stencil is a self-contained piece chosen only for the current work, the rest left behind just as subpath exports ensure only the code you use enters your bundle. The cut pattern determines exactly where dye passes and where it is blocked, much like Katagami's type system catches misuse at compile time, not at runtime. And a stencil needs only paper and a brush, no elaborate machinery — likewise, Katagami requires no decorators or metadata mechanisms and works with any build tool out of the box.
12
12
 
13
13
  ## Features
14
14
 
15
- | Feature | Description |
16
- | ----------------------------- | -------------------------------------------------------------------------------------------- |
17
- | Full type inference | Types accumulate through method chaining; unregistered tokens are compile-time errors |
18
- | Three lifetimes | Singleton, Transient, and Scoped with child containers |
19
- | Async factories | Promise-returning factories are automatically tracked by the type system |
20
- | Circular dependency detection | Clear error messages with the full cycle path |
21
- | Disposable support | TC39 Explicit Resource Management (`Symbol.dispose` / `Symbol.asyncDispose` / `await using`) |
22
- | Captive dependency prevention | Singleton/Transient factories cannot access scoped tokens; caught at compile time |
23
- | Optional resolution | `tryResolve` returns `undefined` for unregistered tokens instead of throwing |
24
- | Hybrid token strategy | Class tokens for strict type safety, PropertyKey tokens for flexibility |
25
- | Interface type map | Pass an interface to `createContainer<T>()` for order-independent registration |
26
- | Zero dependencies | No decorators, no reflect-metadata, no polyfills |
15
+ | Feature | Description |
16
+ | ----------------------------- | ---------------------------------------------------------------------------------------------------------- |
17
+ | Zero dependencies | No decorators, no reflect-metadata, no polyfills works with any bundler out of the box |
18
+ | Full type inference | Types accumulate through method chaining; unregistered tokens are compile-time errors |
19
+ | Tree-shakeable | Subpath exports (`katagami/scope`, `katagami/disposable`) and `sideEffects: false` for minimal bundle size |
20
+ | Captive dependency prevention | Singleton/Transient factories cannot access scoped tokens; caught at compile time |
21
+ | Hybrid token strategy | Class tokens for strict type safety, PropertyKey tokens for flexibility |
22
+ | Interface type map | Pass an interface to `createContainer<T>()` for order-independent registration |
23
+ | Three lifetimes | Singleton, Transient, and Scoped with child containers |
24
+ | Disposable support | TC39 Explicit Resource Management (`Symbol.dispose` / `Symbol.asyncDispose` / `await using`) |
25
+ | Module composition | Containers can be composed via `use()` to group and reuse registrations |
26
+ | Async factories | Promise-returning factories are automatically tracked by the type system |
27
+ | Circular dependency detection | Clear error messages with the full cycle path |
28
+ | Optional resolution | `tryResolve` returns `undefined` for unregistered tokens instead of throwing |
27
29
 
28
30
  ## Install
29
31
 
@@ -66,6 +68,19 @@ Most TypeScript DI containers rely on decorators, reflect-metadata, or string-ba
66
68
 
67
69
  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
70
 
71
+ ### Tree-shakeable
72
+
73
+ Katagami is split into subpath exports. Import only what you use — `katagami/scope` and `katagami/disposable` are completely eliminated from the bundle if not imported. Combined with `sideEffects: false`, bundlers can remove every unused byte.
74
+
75
+ ```ts
76
+ // Core only — scope and disposable are not included in the bundle
77
+ import { createContainer } from 'katagami';
78
+
79
+ // Import only what you need
80
+ import { createScope } from 'katagami/scope';
81
+ import { disposable } from 'katagami/disposable';
82
+ ```
83
+
69
84
  ### Full type inference from class tokens
70
85
 
71
86
  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.
@@ -112,10 +127,11 @@ container.resolve(RequestHandler) === container.resolve(RequestHandler); // fals
112
127
 
113
128
  ### Scoped Lifetime & Child Containers
114
129
 
115
- Scoped registrations behave like singletons within a scope but produce a fresh instance in each new scope. Use `createScope()` to create a child container. Scoped tokens cannot be resolved from the root container.
130
+ Scoped registrations behave like singletons within a scope but produce a fresh instance in each new scope. Import `createScope` from `katagami/scope` to create a child container. Scoped tokens cannot be resolved from the root container.
116
131
 
117
132
  ```ts
118
133
  import { createContainer } from 'katagami';
134
+ import { createScope } from 'katagami/scope';
119
135
 
120
136
  class DbPool {
121
137
  constructor(public name = 'main') {}
@@ -130,8 +146,8 @@ const root = createContainer()
130
146
  .registerScoped(RequestContext, () => new RequestContext());
131
147
 
132
148
  // Create a scope for each request
133
- const scope1 = root.createScope();
134
- const scope2 = root.createScope();
149
+ const scope1 = createScope(root);
150
+ const scope2 = createScope(root);
135
151
 
136
152
  // Scoped — same within a scope, different across scopes
137
153
  scope1.resolve(RequestContext) === scope1.resolve(RequestContext); // true
@@ -144,8 +160,8 @@ scope1.resolve(DbPool) === scope2.resolve(DbPool); // true
144
160
  Scopes can also be nested. Each nested scope has its own scoped instance cache while sharing singletons with its parent:
145
161
 
146
162
  ```ts
147
- const parentScope = root.createScope();
148
- const childScope = parentScope.createScope();
163
+ const parentScope = createScope(root);
164
+ const childScope = createScope(parentScope);
149
165
 
150
166
  // Each nested scope gets its own scoped instances
151
167
  parentScope.resolve(RequestContext) === childScope.resolve(RequestContext); // false
@@ -154,6 +170,53 @@ parentScope.resolve(RequestContext) === childScope.resolve(RequestContext); // f
154
170
  parentScope.resolve(DbPool) === childScope.resolve(DbPool); // true
155
171
  ```
156
172
 
173
+ ### Module Composition
174
+
175
+ 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.
176
+
177
+ ```ts
178
+ import { createContainer } from 'katagami';
179
+
180
+ class AuthService {
181
+ authenticate() {
182
+ return true;
183
+ }
184
+ }
185
+
186
+ class TokenService {
187
+ issue() {
188
+ return 'token';
189
+ }
190
+ }
191
+
192
+ class UserService {
193
+ constructor(private auth: AuthService, private tokens: TokenService) {}
194
+ }
195
+
196
+ // Define a reusable module
197
+ const authModule = createContainer()
198
+ .registerSingleton(AuthService, () => new AuthService())
199
+ .registerSingleton(TokenService, () => new TokenService());
200
+
201
+ // Compose modules
202
+ const container = createContainer()
203
+ .use(authModule)
204
+ .registerSingleton(UserService, r => new UserService(r.resolve(AuthService), r.resolve(TokenService)));
205
+ ```
206
+
207
+ Modules can also compose other modules:
208
+
209
+ ```ts
210
+ const infraModule = createContainer().registerSingleton(AuthService, () => new AuthService());
211
+
212
+ const appModule = createContainer()
213
+ .use(infraModule)
214
+ .registerSingleton(UserService, r => new UserService(r.resolve(AuthService), r.resolve(TokenService)));
215
+
216
+ // appModule includes both AuthService and UserService
217
+ const container = createContainer().use(appModule);
218
+ ```
219
+
157
220
  ### Async Factories
158
221
 
159
222
  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`:
@@ -229,10 +292,11 @@ ContainerError: Circular dependency detected: ServiceX -> ServiceY -> ServiceZ -
229
292
 
230
293
  ### Disposable Support
231
294
 
232
- Both `Container` and `Scope` implement `AsyncDisposable`. When disposed, managed instances are iterated in reverse creation order (LIFO) and their `[Symbol.asyncDispose]()` or `[Symbol.dispose]()` methods are called automatically.
295
+ Disposal is provided by the `disposable()` wrapper from `katagami/disposable`. Wrapping a container or scope attaches `[Symbol.asyncDispose]`, enabling `await using` syntax. When disposed, owned instances are iterated in reverse creation order (LIFO) and their `[Symbol.asyncDispose]()` or `[Symbol.dispose]()` methods are called automatically.
233
296
 
234
297
  ```ts
235
298
  import { createContainer } from 'katagami';
299
+ import { disposable } from 'katagami/disposable';
236
300
 
237
301
  class Connection {
238
302
  async [Symbol.asyncDispose]() {
@@ -241,7 +305,7 @@ class Connection {
241
305
  }
242
306
 
243
307
  // Manual disposal
244
- const container = createContainer().registerSingleton(Connection, () => new Connection());
308
+ const container = disposable(createContainer().registerSingleton(Connection, () => new Connection()));
245
309
 
246
310
  container.resolve(Connection);
247
311
  await container[Symbol.asyncDispose]();
@@ -251,12 +315,15 @@ await container[Symbol.asyncDispose]();
251
315
  With `await using`, scopes are automatically disposed at the end of the block:
252
316
 
253
317
  ```ts
318
+ import { createScope } from 'katagami/scope';
319
+ import { disposable } from 'katagami/disposable';
320
+
254
321
  const root = createContainer()
255
322
  .registerSingleton(DbPool, () => new DbPool())
256
323
  .registerScoped(Connection, () => new Connection());
257
324
 
258
325
  {
259
- await using scope = root.createScope();
326
+ await using scope = disposable(createScope(root));
260
327
  const conn = scope.resolve(Connection);
261
328
  // ... use conn ...
262
329
  } // scope is disposed here — Connection is cleaned up, DbPool is not
@@ -264,6 +331,28 @@ const root = createContainer()
264
331
 
265
332
  Scope disposal only affects scoped instances. Singleton instances are owned by the root container and are disposed when the container itself is disposed.
266
333
 
334
+ 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:
335
+
336
+ ```ts
337
+ const container = disposable(createContainer().registerSingleton(Connection, () => new Connection()));
338
+
339
+ container.resolve(Connection); // OK
340
+ container.registerSingleton(/* ... */); // Compile-time error
341
+ ```
342
+
343
+ ### Tree Shaking
344
+
345
+ Katagami uses subpath exports to split functionality into independent entry points. If you only need the core container, `katagami/scope` and `katagami/disposable` are completely excluded from the bundle. The package declares `sideEffects: false`, so bundlers can safely eliminate any unused code.
346
+
347
+ ```ts
348
+ // Core only — scope and disposable are not included in the bundle
349
+ import { createContainer } from 'katagami';
350
+
351
+ // Import only what you need
352
+ import { createScope } from 'katagami/scope';
353
+ import { disposable } from 'katagami/disposable';
354
+ ```
355
+
267
356
  ### Interface Type Map
268
357
 
269
358
  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:
@@ -393,43 +482,55 @@ const container = createContainer()
393
482
 
394
483
  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`).
395
484
 
396
- ### `container.registerSingleton(token, factory)`
485
+ ### `Container.prototype.registerSingleton(token, factory)`
397
486
 
398
487
  Registers a factory as a singleton. The instance is created on the first `resolve` and cached thereafter. Returns the container for method chaining.
399
488
 
400
- ### `container.registerTransient(token, factory)`
489
+ ### `Container.prototype.registerTransient(token, factory)`
401
490
 
402
491
  Registers a factory as transient. A new instance is created on every `resolve`. Returns the container for method chaining.
403
492
 
404
- ### `container.registerScoped(token, factory)`
493
+ ### `Container.prototype.registerScoped(token, factory)`
405
494
 
406
495
  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.
407
496
 
408
- ### `container.resolve(token)`
497
+ ### `Container.prototype.use(source)`
498
+
499
+ 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.
500
+
501
+ ### `Container.prototype.resolve(token)`
409
502
 
410
503
  Resolves and returns the instance for the given token. Throws `ContainerError` if the token is not registered or if a circular dependency is detected.
411
504
 
412
- ### `container.tryResolve(token)` / `scope.tryResolve(token)`
505
+ ### `Container.prototype.tryResolve(token)`
413
506
 
414
507
  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.
415
508
 
416
- ### `container.createScope()`
509
+ ### `createScope(source)` — `katagami/scope`
510
+
511
+ 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.
512
+
513
+ ### `class Scope`
514
+
515
+ A scoped child container created by `createScope()`.
516
+
517
+ ### `Scope.prototype.resolve(token)`
417
518
 
418
- Creates a new `Scope` (child container). The scope inherits all registrations from the parent. Singleton instances are shared with the parent, while scoped instances are local to the scope.
519
+ Resolves and returns the instance for the given token. Behaves the same as `Container.prototype.resolve`, but can also resolve scoped tokens.
419
520
 
420
- ### `Scope`
521
+ ### `Scope.prototype.tryResolve(token)`
421
522
 
422
- A scoped child container created by `createScope()`. Provides `resolve(token)`, `tryResolve(token)`, `createScope()` (for nested scopes), and `[Symbol.asyncDispose]()`.
523
+ 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.
423
524
 
424
- ### `container[Symbol.asyncDispose]()` / `scope[Symbol.asyncDispose]()`
525
+ ### `disposable(container)` `katagami/disposable`
425
526
 
426
- Disposes all managed 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`.
527
+ 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.
427
528
 
428
- ### `ContainerError`
529
+ ### `class ContainerError`
429
530
 
430
531
  Error class thrown for container failures such as resolving an unregistered token, circular dependencies, or operations on a disposed container/scope.
431
532
 
432
- ### `Resolver`
533
+ ### `type Resolver`
433
534
 
434
535
  Type export representing the resolver passed to factory callbacks. Useful when you need to type a function that accepts a resolver parameter.
435
536
 
@@ -1,5 +1,5 @@
1
+ import { type ContainerInternals, INTERNALS } from '../internal';
1
2
  import type { AbstractConstructor, Resolver } from '../resolver';
2
- import { Scope } from '../scope';
3
3
  /**
4
4
  * Create a new DI container.
5
5
  *
@@ -30,11 +30,17 @@ export declare function createContainer<T = Record<never, never>, ScopedT = Reco
30
30
  * @template ScopedSync Union of scoped sync class constructors (accumulated via chaining, order-dependent)
31
31
  * @template ScopedAsync Union of scoped async class constructors (accumulated via chaining, order-dependent)
32
32
  */
33
- 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> implements AsyncDisposable {
33
+ 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
34
  private readonly registrations;
35
35
  private readonly instances;
36
36
  private readonly resolvingTokens;
37
37
  private disposed;
38
+ /**
39
+ * Internal state accessor for extension modules (scope, disposable).
40
+ *
41
+ * @internal
42
+ */
43
+ readonly [INTERNALS]: ContainerInternals;
38
44
  constructor();
39
45
  /**
40
46
  * Register a factory function as a singleton for the given token.
@@ -78,15 +84,15 @@ export declare class Container<T = Record<never, never>, Sync extends AbstractCo
78
84
  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>;
79
85
  registerScoped<V>(token: unknown, factory: (resolver: Resolver<T & ScopedT, Sync | ScopedSync, Async | ScopedAsync>) => V): Container<T, Sync, Async, ScopedT, ScopedSync, ScopedAsync>;
80
86
  /**
81
- * Create a new scope (child container).
87
+ * Apply all registrations from another container (module) to this container.
82
88
  *
83
- * The scope inherits all registrations from this container.
84
- * Singleton instances are shared with the parent, while scoped instances are local to the scope.
89
+ * Copies only registration entries (factory + lifetime). Singleton instance caches
90
+ * are not shared each container manages its own.
85
91
  *
86
- * @returns A new Scope instance
87
- * @throws ContainerError if the container has been disposed
92
+ * @param source A container whose registrations will be copied into this container
93
+ * @returns The container for method chaining
88
94
  */
89
- createScope(): Scope<T, Sync, Async, ScopedT, ScopedSync, ScopedAsync>;
95
+ 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>;
90
96
  /**
91
97
  * Resolve an instance for the given token.
92
98
  *
@@ -122,18 +128,6 @@ export declare class Container<T = Record<never, never>, Sync extends AbstractCo
122
128
  * @returns The resolved instance, or undefined if not registered and required is false
123
129
  */
124
130
  private resolveToken;
125
- /**
126
- * Dispose all singleton instances managed by this container.
127
- *
128
- * Iterates through singleton instances in reverse creation order (LIFO) and calls
129
- * `[Symbol.asyncDispose]()` or `[Symbol.dispose]()` on each instance that implements them.
130
- *
131
- * This method is idempotent — subsequent calls after the first are no-ops.
132
- * After disposal, `resolve()` and `createScope()` will throw `ContainerError`.
133
- *
134
- * @throws AggregateError if one or more instances throw during disposal
135
- */
136
- [Symbol.asyncDispose](): Promise<void>;
137
131
  /**
138
132
  * Add a registration entry.
139
133
  *
@@ -0,0 +1,76 @@
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/disposable/index.ts
30
+ var exports_disposable = {};
31
+ __export(exports_disposable, {
32
+ disposable: () => disposable
33
+ });
34
+ module.exports = __toCommonJS(exports_disposable);
35
+
36
+ // src/internal.ts
37
+ var INTERNALS = Symbol("katagami.internals");
38
+
39
+ // src/disposable/index.ts
40
+ function disposable(container) {
41
+ const asyncDispose = async () => {
42
+ const internals = container[INTERNALS];
43
+ if (internals.isDisposed()) {
44
+ return;
45
+ }
46
+ internals.markDisposed();
47
+ const instances = [...internals.ownInstances.values()].reverse();
48
+ const errors = [];
49
+ for (const instance of instances) {
50
+ try {
51
+ let resolved = instance;
52
+ if (instance instanceof Promise) {
53
+ resolved = await instance;
54
+ }
55
+ if (resolved != null && typeof resolved === "object") {
56
+ if (Symbol.asyncDispose in resolved) {
57
+ await resolved[Symbol.asyncDispose]();
58
+ } else if (Symbol.dispose in resolved) {
59
+ resolved[Symbol.dispose]();
60
+ }
61
+ }
62
+ } catch (error) {
63
+ errors.push(error);
64
+ }
65
+ }
66
+ internals.ownInstances.clear();
67
+ if (errors.length > 0) {
68
+ throw new AggregateError(errors, "One or more errors occurred during disposal.");
69
+ }
70
+ };
71
+ Object.defineProperty(container, Symbol.asyncDispose, {
72
+ configurable: true,
73
+ value: asyncDispose
74
+ });
75
+ return container;
76
+ }
@@ -0,0 +1,61 @@
1
+ import type { Container } from '../container';
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` and `tryResolve` 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` and `tryResolve` 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
+ }
37
+ /**
38
+ * Add async disposal capability to a container or scope.
39
+ *
40
+ * Enables `await using` syntax by attaching `[Symbol.asyncDispose]` to the target.
41
+ * Disposes owned instances in reverse creation order (LIFO), calling
42
+ * `[Symbol.asyncDispose]()` or `[Symbol.dispose]()` on each instance that implements them.
43
+ *
44
+ * The returned type is narrowed to only expose `resolve` and `tryResolve`,
45
+ * preventing registration methods from being called on a potentially-disposed container.
46
+ *
47
+ * @param container A Container or Scope to make disposable
48
+ * @returns The same object with `AsyncDisposable` capability added and registration methods removed from the type
49
+ *
50
+ * @example
51
+ * ```ts
52
+ * import { createContainer } from 'katagami';
53
+ * import { disposable } from 'katagami/disposable';
54
+ *
55
+ * await using container = disposable(
56
+ * createContainer().registerSingleton(DB, () => new Database())
57
+ * );
58
+ * ```
59
+ */
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>;
@@ -0,0 +1,45 @@
1
+ import {
2
+ INTERNALS
3
+ } from "../index-jx8b52m0.js";
4
+
5
+ // src/disposable/index.ts
6
+ function disposable(container) {
7
+ const asyncDispose = async () => {
8
+ const internals = container[INTERNALS];
9
+ if (internals.isDisposed()) {
10
+ return;
11
+ }
12
+ internals.markDisposed();
13
+ const instances = [...internals.ownInstances.values()].reverse();
14
+ const errors = [];
15
+ for (const instance of instances) {
16
+ try {
17
+ let resolved = instance;
18
+ if (instance instanceof Promise) {
19
+ resolved = await instance;
20
+ }
21
+ if (resolved != null && typeof resolved === "object") {
22
+ if (Symbol.asyncDispose in resolved) {
23
+ await resolved[Symbol.asyncDispose]();
24
+ } else if (Symbol.dispose in resolved) {
25
+ resolved[Symbol.dispose]();
26
+ }
27
+ }
28
+ } catch (error) {
29
+ errors.push(error);
30
+ }
31
+ }
32
+ internals.ownInstances.clear();
33
+ if (errors.length > 0) {
34
+ throw new AggregateError(errors, "One or more errors occurred during disposal.");
35
+ }
36
+ };
37
+ Object.defineProperty(container, Symbol.asyncDispose, {
38
+ configurable: true,
39
+ value: asyncDispose
40
+ });
41
+ return container;
42
+ }
43
+ export {
44
+ disposable
45
+ };
@@ -0,0 +1,34 @@
1
+ // src/error/index.ts
2
+ class ContainerError extends Error {
3
+ constructor(message) {
4
+ super(message);
5
+ this.name = "ContainerError";
6
+ }
7
+ }
8
+
9
+ // src/resolver/index.ts
10
+ function tokenToString(token) {
11
+ if (typeof token === "function") {
12
+ return token.name || "anonymous function";
13
+ }
14
+ if (typeof token === "symbol") {
15
+ return token.toString();
16
+ }
17
+ return String(token);
18
+ }
19
+ function buildCircularPath(resolvingTokens, token) {
20
+ const path = [];
21
+ let found = false;
22
+ for (const t of resolvingTokens) {
23
+ if (t === token) {
24
+ found = true;
25
+ }
26
+ if (found) {
27
+ path.push(tokenToString(t));
28
+ }
29
+ }
30
+ path.push(tokenToString(token));
31
+ return path.join(" -> ");
32
+ }
33
+
34
+ export { ContainerError, tokenToString, buildCircularPath };
@@ -0,0 +1,4 @@
1
+ // src/internal.ts
2
+ var INTERNALS = Symbol("katagami.internals");
3
+
4
+ export { INTERNALS };