katagami 1.1.0 → 2.0.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
 
@@ -112,10 +112,11 @@ container.resolve(RequestHandler) === container.resolve(RequestHandler); // fals
112
112
 
113
113
  ### Scoped Lifetime & Child Containers
114
114
 
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.
115
+ 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
116
 
117
117
  ```ts
118
118
  import { createContainer } from 'katagami';
119
+ import { createScope } from 'katagami/scope';
119
120
 
120
121
  class DbPool {
121
122
  constructor(public name = 'main') {}
@@ -130,8 +131,8 @@ const root = createContainer()
130
131
  .registerScoped(RequestContext, () => new RequestContext());
131
132
 
132
133
  // Create a scope for each request
133
- const scope1 = root.createScope();
134
- const scope2 = root.createScope();
134
+ const scope1 = createScope(root);
135
+ const scope2 = createScope(root);
135
136
 
136
137
  // Scoped — same within a scope, different across scopes
137
138
  scope1.resolve(RequestContext) === scope1.resolve(RequestContext); // true
@@ -144,8 +145,8 @@ scope1.resolve(DbPool) === scope2.resolve(DbPool); // true
144
145
  Scopes can also be nested. Each nested scope has its own scoped instance cache while sharing singletons with its parent:
145
146
 
146
147
  ```ts
147
- const parentScope = root.createScope();
148
- const childScope = parentScope.createScope();
148
+ const parentScope = createScope(root);
149
+ const childScope = createScope(parentScope);
149
150
 
150
151
  // Each nested scope gets its own scoped instances
151
152
  parentScope.resolve(RequestContext) === childScope.resolve(RequestContext); // false
@@ -229,10 +230,11 @@ ContainerError: Circular dependency detected: ServiceX -> ServiceY -> ServiceZ -
229
230
 
230
231
  ### Disposable Support
231
232
 
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.
233
+ 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
234
 
234
235
  ```ts
235
236
  import { createContainer } from 'katagami';
237
+ import { disposable } from 'katagami/disposable';
236
238
 
237
239
  class Connection {
238
240
  async [Symbol.asyncDispose]() {
@@ -241,7 +243,9 @@ class Connection {
241
243
  }
242
244
 
243
245
  // Manual disposal
244
- const container = createContainer().registerSingleton(Connection, () => new Connection());
246
+ const container = disposable(
247
+ createContainer().registerSingleton(Connection, () => new Connection()),
248
+ );
245
249
 
246
250
  container.resolve(Connection);
247
251
  await container[Symbol.asyncDispose]();
@@ -251,12 +255,15 @@ await container[Symbol.asyncDispose]();
251
255
  With `await using`, scopes are automatically disposed at the end of the block:
252
256
 
253
257
  ```ts
258
+ import { createScope } from 'katagami/scope';
259
+ import { disposable } from 'katagami/disposable';
260
+
254
261
  const root = createContainer()
255
262
  .registerSingleton(DbPool, () => new DbPool())
256
263
  .registerScoped(Connection, () => new Connection());
257
264
 
258
265
  {
259
- await using scope = root.createScope();
266
+ await using scope = disposable(createScope(root));
260
267
  const conn = scope.resolve(Connection);
261
268
  // ... use conn ...
262
269
  } // scope is disposed here — Connection is cleaned up, DbPool is not
@@ -413,17 +420,17 @@ Resolves and returns the instance for the given token. Throws `ContainerError` i
413
420
 
414
421
  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
422
 
416
- ### `container.createScope()`
423
+ ### `createScope(source)` — `katagami/scope`
417
424
 
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.
425
+ 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.
419
426
 
420
427
  ### `Scope`
421
428
 
422
- A scoped child container created by `createScope()`. Provides `resolve(token)`, `tryResolve(token)`, `createScope()` (for nested scopes), and `[Symbol.asyncDispose]()`.
429
+ A scoped child container created by `createScope()`. Provides `resolve(token)` and `tryResolve(token)`.
423
430
 
424
- ### `container[Symbol.asyncDispose]()` / `scope[Symbol.asyncDispose]()`
431
+ ### `disposable(container)` `katagami/disposable`
425
432
 
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`.
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`.
427
434
 
428
435
  ### `ContainerError`
429
436
 
@@ -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.
@@ -77,16 +83,6 @@ export declare class Container<T = Record<never, never>, Sync extends AbstractCo
77
83
  registerScoped<V>(token: AbstractConstructor<V>, factory: (resolver: Resolver<T & ScopedT, Sync | ScopedSync, Async | ScopedAsync>) => V): Container<T, Sync, Async, ScopedT, ScopedSync | AbstractConstructor<V>, ScopedAsync>;
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
- /**
81
- * Create a new scope (child container).
82
- *
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.
85
- *
86
- * @returns A new Scope instance
87
- * @throws ContainerError if the container has been disposed
88
- */
89
- createScope(): Scope<T, Sync, Async, ScopedT, ScopedSync, ScopedAsync>;
90
86
  /**
91
87
  * Resolve an instance for the given token.
92
88
  *
@@ -122,18 +118,6 @@ export declare class Container<T = Record<never, never>, Sync extends AbstractCo
122
118
  * @returns The resolved instance, or undefined if not registered and required is false
123
119
  */
124
120
  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
121
  /**
138
122
  * Add a registration entry.
139
123
  *
@@ -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,24 @@
1
+ import { type ContainerInternals, INTERNALS } from '../internal';
2
+ /**
3
+ * Add async disposal capability to a container or scope.
4
+ *
5
+ * Enables `await using` syntax by attaching `[Symbol.asyncDispose]` to the target.
6
+ * Disposes owned instances in reverse creation order (LIFO), calling
7
+ * `[Symbol.asyncDispose]()` or `[Symbol.dispose]()` on each instance that implements them.
8
+ *
9
+ * @param container A Container or Scope to make disposable
10
+ * @returns The same object with `AsyncDisposable` capability added
11
+ *
12
+ * @example
13
+ * ```ts
14
+ * import { createContainer } from 'katagami';
15
+ * import { disposable } from 'katagami/disposable';
16
+ *
17
+ * await using container = disposable(
18
+ * createContainer().registerSingleton(DB, () => new Database())
19
+ * );
20
+ * ```
21
+ */
22
+ export declare function disposable<C extends {
23
+ readonly [INTERNALS]: ContainerInternals;
24
+ }>(container: C): C & AsyncDisposable;
@@ -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 };
package/dist/index.cjs CHANGED
@@ -30,7 +30,6 @@ var __export = (target, all) => {
30
30
  var exports_src = {};
31
31
  __export(exports_src, {
32
32
  createContainer: () => createContainer,
33
- Scope: () => Scope,
34
33
  ContainerError: () => ContainerError,
35
34
  Container: () => Container
36
35
  });
@@ -44,6 +43,9 @@ class ContainerError extends Error {
44
43
  }
45
44
  }
46
45
 
46
+ // src/internal.ts
47
+ var INTERNALS = Symbol("katagami.internals");
48
+
47
49
  // src/resolver/index.ts
48
50
  function tokenToString(token) {
49
51
  if (typeof token === "function") {
@@ -69,97 +71,6 @@ function buildCircularPath(resolvingTokens, token) {
69
71
  return path.join(" -> ");
70
72
  }
71
73
 
72
- // src/scope/index.ts
73
- class Scope {
74
- registrations;
75
- singletonInstances;
76
- scopedInstances;
77
- resolvingTokens;
78
- disposed = false;
79
- constructor(registrations, singletonInstances) {
80
- this.registrations = registrations;
81
- this.singletonInstances = singletonInstances;
82
- this.scopedInstances = new Map;
83
- this.resolvingTokens = new Set;
84
- }
85
- resolve(token) {
86
- return this.resolveToken(token, true);
87
- }
88
- tryResolve(token) {
89
- return this.resolveToken(token, false);
90
- }
91
- resolveToken(token, required) {
92
- if (this.disposed) {
93
- throw new ContainerError("Cannot resolve from a disposed scope.");
94
- }
95
- const singletonCached = this.singletonInstances.get(token);
96
- if (singletonCached !== undefined) {
97
- return singletonCached;
98
- }
99
- const scopedCached = this.scopedInstances.get(token);
100
- if (scopedCached !== undefined) {
101
- return scopedCached;
102
- }
103
- const registration = this.registrations.get(token);
104
- if (registration === undefined) {
105
- if (required) {
106
- throw new ContainerError(`Token "${tokenToString(token)}" is not registered.`);
107
- }
108
- return;
109
- }
110
- if (this.resolvingTokens.has(token)) {
111
- throw new ContainerError(`Circular dependency detected: ${buildCircularPath(this.resolvingTokens, token)}`);
112
- }
113
- this.resolvingTokens.add(token);
114
- try {
115
- const instance = registration.factory(this);
116
- if (registration.lifetime === "singleton") {
117
- this.singletonInstances.set(token, instance);
118
- } else if (registration.lifetime === "scoped") {
119
- this.scopedInstances.set(token, instance);
120
- }
121
- return instance;
122
- } finally {
123
- this.resolvingTokens.delete(token);
124
- }
125
- }
126
- createScope() {
127
- if (this.disposed) {
128
- throw new ContainerError("Cannot create a scope from a disposed scope.");
129
- }
130
- return new Scope(this.registrations, this.singletonInstances);
131
- }
132
- async[Symbol.asyncDispose]() {
133
- if (this.disposed) {
134
- return;
135
- }
136
- this.disposed = true;
137
- const instances = [...this.scopedInstances.values()].reverse();
138
- const errors = [];
139
- for (const instance of instances) {
140
- try {
141
- let resolved = instance;
142
- if (instance instanceof Promise) {
143
- resolved = await instance;
144
- }
145
- if (resolved != null && typeof resolved === "object") {
146
- if (Symbol.asyncDispose in resolved) {
147
- await resolved[Symbol.asyncDispose]();
148
- } else if (Symbol.dispose in resolved) {
149
- resolved[Symbol.dispose]();
150
- }
151
- }
152
- } catch (error) {
153
- errors.push(error);
154
- }
155
- }
156
- this.scopedInstances.clear();
157
- if (errors.length > 0) {
158
- throw new AggregateError(errors, "One or more errors occurred during disposal.");
159
- }
160
- }
161
- }
162
-
163
74
  // src/container/index.ts
164
75
  function createContainer() {
165
76
  return new Container;
@@ -170,10 +81,20 @@ class Container {
170
81
  instances;
171
82
  resolvingTokens;
172
83
  disposed = false;
84
+ [INTERNALS];
173
85
  constructor() {
174
86
  this.registrations = new Map;
175
87
  this.instances = new Map;
176
88
  this.resolvingTokens = new Set;
89
+ this[INTERNALS] = {
90
+ instances: this.instances,
91
+ isDisposed: () => this.disposed,
92
+ markDisposed: () => {
93
+ this.disposed = true;
94
+ },
95
+ ownInstances: this.instances,
96
+ registrations: this.registrations
97
+ };
177
98
  }
178
99
  registerSingleton(token, factory) {
179
100
  return this.addRegistration(token, factory, "singleton");
@@ -184,12 +105,6 @@ class Container {
184
105
  registerScoped(token, factory) {
185
106
  return this.addRegistration(token, factory, "scoped");
186
107
  }
187
- createScope() {
188
- if (this.disposed) {
189
- throw new ContainerError("Cannot create a scope from a disposed container.");
190
- }
191
- return new Scope(this.registrations, this.instances);
192
- }
193
108
  resolve(token) {
194
109
  return this.resolveToken(token, true);
195
110
  }
@@ -228,35 +143,6 @@ class Container {
228
143
  this.resolvingTokens.delete(token);
229
144
  }
230
145
  }
231
- async[Symbol.asyncDispose]() {
232
- if (this.disposed) {
233
- return;
234
- }
235
- this.disposed = true;
236
- const instances = [...this.instances.values()].reverse();
237
- const errors = [];
238
- for (const instance of instances) {
239
- try {
240
- let resolved = instance;
241
- if (instance instanceof Promise) {
242
- resolved = await instance;
243
- }
244
- if (resolved != null && typeof resolved === "object") {
245
- if (Symbol.asyncDispose in resolved) {
246
- await resolved[Symbol.asyncDispose]();
247
- } else if (Symbol.dispose in resolved) {
248
- resolved[Symbol.dispose]();
249
- }
250
- }
251
- } catch (error) {
252
- errors.push(error);
253
- }
254
- }
255
- this.instances.clear();
256
- if (errors.length > 0) {
257
- throw new AggregateError(errors, "One or more errors occurred during disposal.");
258
- }
259
- }
260
146
  addRegistration(token, factory, lifetime) {
261
147
  this.registrations.set(token, { factory, lifetime });
262
148
  return this;
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export { Container, createContainer } from './container';
2
+ export type { disposable } from './disposable';
2
3
  export { ContainerError } from './error';
3
4
  export type { Resolver } from './resolver';
4
- export { Scope } from './scope';
5
+ export type { createScope, Scope } from './scope';