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 +136 -35
- package/dist/container/index.d.ts +14 -20
- package/dist/disposable/index.cjs +76 -0
- package/dist/disposable/index.d.ts +61 -0
- package/dist/disposable/index.js +45 -0
- package/dist/index-g50fxds1.js +34 -0
- package/dist/index-jx8b52m0.js +4 -0
- package/dist/index.cjs +17 -125
- package/dist/index.d.ts +2 -1
- package/dist/index.js +22 -157
- package/dist/internal.d.ts +27 -0
- package/dist/scope/index.cjs +145 -0
- package/dist/scope/index.d.ts +24 -23
- package/dist/scope/index.js +86 -0
- package/package.json +14 -3
- package/README.de.md +0 -438
- package/README.es.md +0 -386
- package/README.fr.md +0 -386
- package/README.ja.md +0 -438
- package/README.ko.md +0 -438
- package/README.zh-CN.md +0 -438
- package/README.zh-TW.md +0 -438
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
|
[](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 |
|
|
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.
|
|
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 =
|
|
134
|
-
const scope2 =
|
|
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 =
|
|
148
|
-
const childScope =
|
|
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
|
-
|
|
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 =
|
|
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
|
-
### `
|
|
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
|
-
### `
|
|
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
|
-
### `
|
|
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
|
-
### `
|
|
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
|
-
### `
|
|
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
|
-
### `
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
525
|
+
### `disposable(container)` — `katagami/disposable`
|
|
425
526
|
|
|
426
|
-
Disposes all
|
|
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>
|
|
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
|
-
*
|
|
87
|
+
* Apply all registrations from another container (module) to this container.
|
|
82
88
|
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
89
|
+
* Copies only registration entries (factory + lifetime). Singleton instance caches
|
|
90
|
+
* are not shared — each container manages its own.
|
|
85
91
|
*
|
|
86
|
-
* @
|
|
87
|
-
* @
|
|
92
|
+
* @param source A container whose registrations will be copied into this container
|
|
93
|
+
* @returns The container for method chaining
|
|
88
94
|
*/
|
|
89
|
-
|
|
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 };
|