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/dist/index.js CHANGED
@@ -1,126 +1,11 @@
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
- // src/scope/index.ts
35
- class Scope {
36
- registrations;
37
- singletonInstances;
38
- scopedInstances;
39
- resolvingTokens;
40
- disposed = false;
41
- constructor(registrations, singletonInstances) {
42
- this.registrations = registrations;
43
- this.singletonInstances = singletonInstances;
44
- this.scopedInstances = new Map;
45
- this.resolvingTokens = new Set;
46
- }
47
- resolve(token) {
48
- return this.resolveToken(token, true);
49
- }
50
- tryResolve(token) {
51
- return this.resolveToken(token, false);
52
- }
53
- resolveToken(token, required) {
54
- if (this.disposed) {
55
- throw new ContainerError("Cannot resolve from a disposed scope.");
56
- }
57
- const singletonCached = this.singletonInstances.get(token);
58
- if (singletonCached !== undefined) {
59
- return singletonCached;
60
- }
61
- const scopedCached = this.scopedInstances.get(token);
62
- if (scopedCached !== undefined) {
63
- return scopedCached;
64
- }
65
- const registration = this.registrations.get(token);
66
- if (registration === undefined) {
67
- if (required) {
68
- throw new ContainerError(`Token "${tokenToString(token)}" is not registered.`);
69
- }
70
- return;
71
- }
72
- if (this.resolvingTokens.has(token)) {
73
- throw new ContainerError(`Circular dependency detected: ${buildCircularPath(this.resolvingTokens, token)}`);
74
- }
75
- this.resolvingTokens.add(token);
76
- try {
77
- const instance = registration.factory(this);
78
- if (registration.lifetime === "singleton") {
79
- this.singletonInstances.set(token, instance);
80
- } else if (registration.lifetime === "scoped") {
81
- this.scopedInstances.set(token, instance);
82
- }
83
- return instance;
84
- } finally {
85
- this.resolvingTokens.delete(token);
86
- }
87
- }
88
- createScope() {
89
- if (this.disposed) {
90
- throw new ContainerError("Cannot create a scope from a disposed scope.");
91
- }
92
- return new Scope(this.registrations, this.singletonInstances);
93
- }
94
- async[Symbol.asyncDispose]() {
95
- if (this.disposed) {
96
- return;
97
- }
98
- this.disposed = true;
99
- const instances = [...this.scopedInstances.values()].reverse();
100
- const errors = [];
101
- for (const instance of instances) {
102
- try {
103
- let resolved = instance;
104
- if (instance instanceof Promise) {
105
- resolved = await instance;
106
- }
107
- if (resolved != null && typeof resolved === "object") {
108
- if (Symbol.asyncDispose in resolved) {
109
- await resolved[Symbol.asyncDispose]();
110
- } else if (Symbol.dispose in resolved) {
111
- resolved[Symbol.dispose]();
112
- }
113
- }
114
- } catch (error) {
115
- errors.push(error);
116
- }
117
- }
118
- this.scopedInstances.clear();
119
- if (errors.length > 0) {
120
- throw new AggregateError(errors, "One or more errors occurred during disposal.");
121
- }
122
- }
123
- }
1
+ import {
2
+ ContainerError,
3
+ buildCircularPath,
4
+ tokenToString
5
+ } from "./index-g50fxds1.js";
6
+ import {
7
+ INTERNALS
8
+ } from "./index-jx8b52m0.js";
124
9
 
125
10
  // src/container/index.ts
126
11
  function createContainer() {
@@ -132,10 +17,20 @@ class Container {
132
17
  instances;
133
18
  resolvingTokens;
134
19
  disposed = false;
20
+ [INTERNALS];
135
21
  constructor() {
136
22
  this.registrations = new Map;
137
23
  this.instances = new Map;
138
24
  this.resolvingTokens = new Set;
25
+ this[INTERNALS] = {
26
+ instances: this.instances,
27
+ isDisposed: () => this.disposed,
28
+ markDisposed: () => {
29
+ this.disposed = true;
30
+ },
31
+ ownInstances: this.instances,
32
+ registrations: this.registrations
33
+ };
139
34
  }
140
35
  registerSingleton(token, factory) {
141
36
  return this.addRegistration(token, factory, "singleton");
@@ -146,12 +41,6 @@ class Container {
146
41
  registerScoped(token, factory) {
147
42
  return this.addRegistration(token, factory, "scoped");
148
43
  }
149
- createScope() {
150
- if (this.disposed) {
151
- throw new ContainerError("Cannot create a scope from a disposed container.");
152
- }
153
- return new Scope(this.registrations, this.instances);
154
- }
155
44
  resolve(token) {
156
45
  return this.resolveToken(token, true);
157
46
  }
@@ -190,35 +79,6 @@ class Container {
190
79
  this.resolvingTokens.delete(token);
191
80
  }
192
81
  }
193
- async[Symbol.asyncDispose]() {
194
- if (this.disposed) {
195
- return;
196
- }
197
- this.disposed = true;
198
- const instances = [...this.instances.values()].reverse();
199
- const errors = [];
200
- for (const instance of instances) {
201
- try {
202
- let resolved = instance;
203
- if (instance instanceof Promise) {
204
- resolved = await instance;
205
- }
206
- if (resolved != null && typeof resolved === "object") {
207
- if (Symbol.asyncDispose in resolved) {
208
- await resolved[Symbol.asyncDispose]();
209
- } else if (Symbol.dispose in resolved) {
210
- resolved[Symbol.dispose]();
211
- }
212
- }
213
- } catch (error) {
214
- errors.push(error);
215
- }
216
- }
217
- this.instances.clear();
218
- if (errors.length > 0) {
219
- throw new AggregateError(errors, "One or more errors occurred during disposal.");
220
- }
221
- }
222
82
  addRegistration(token, factory, lifetime) {
223
83
  this.registrations.set(token, { factory, lifetime });
224
84
  return this;
@@ -226,7 +86,6 @@ class Container {
226
86
  }
227
87
  export {
228
88
  createContainer,
229
- Scope,
230
89
  ContainerError,
231
90
  Container
232
91
  };
@@ -0,0 +1,27 @@
1
+ import type { Registration } from './resolver';
2
+ /**
3
+ * Symbol used by extension modules (scope, disposable) to access container/scope internals.
4
+ *
5
+ * @internal
6
+ */
7
+ export declare const INTERNALS: unique symbol;
8
+ /**
9
+ * Internal state exposed via the INTERNALS symbol.
10
+ *
11
+ * Both Container and Scope implement this interface so that extension modules
12
+ * (scope, disposable) can operate on either without importing the concrete class.
13
+ *
14
+ * @internal
15
+ */
16
+ export interface ContainerInternals {
17
+ /** All registrations (singleton / transient / scoped). */
18
+ readonly registrations: Map<unknown, Registration>;
19
+ /** Instance cache used during resolution (Container: singletons, Scope: singletonInstances). */
20
+ readonly instances: Map<unknown, unknown>;
21
+ /** Instances owned by this container / scope (disposal target). */
22
+ readonly ownInstances: Map<unknown, unknown>;
23
+ /** Whether this container / scope has been disposed. */
24
+ isDisposed(): boolean;
25
+ /** Mark this container / scope as disposed. */
26
+ markDisposed(): void;
27
+ }
@@ -0,0 +1,145 @@
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/scope/index.ts
30
+ var exports_scope = {};
31
+ __export(exports_scope, {
32
+ createScope: () => createScope,
33
+ Scope: () => Scope
34
+ });
35
+ module.exports = __toCommonJS(exports_scope);
36
+
37
+ // src/error/index.ts
38
+ class ContainerError extends Error {
39
+ constructor(message) {
40
+ super(message);
41
+ this.name = "ContainerError";
42
+ }
43
+ }
44
+
45
+ // src/internal.ts
46
+ var INTERNALS = Symbol("katagami.internals");
47
+
48
+ // src/resolver/index.ts
49
+ function tokenToString(token) {
50
+ if (typeof token === "function") {
51
+ return token.name || "anonymous function";
52
+ }
53
+ if (typeof token === "symbol") {
54
+ return token.toString();
55
+ }
56
+ return String(token);
57
+ }
58
+ function buildCircularPath(resolvingTokens, token) {
59
+ const path = [];
60
+ let found = false;
61
+ for (const t of resolvingTokens) {
62
+ if (t === token) {
63
+ found = true;
64
+ }
65
+ if (found) {
66
+ path.push(tokenToString(t));
67
+ }
68
+ }
69
+ path.push(tokenToString(token));
70
+ return path.join(" -> ");
71
+ }
72
+
73
+ // src/scope/index.ts
74
+ function createScope(source) {
75
+ const internals = source[INTERNALS];
76
+ if (internals.isDisposed()) {
77
+ throw new ContainerError("Cannot create a scope from a disposed container.");
78
+ }
79
+ return new Scope(internals.registrations, internals.instances);
80
+ }
81
+
82
+ class Scope {
83
+ registrations;
84
+ singletonInstances;
85
+ scopedInstances;
86
+ resolvingTokens;
87
+ disposed = false;
88
+ [INTERNALS];
89
+ constructor(registrations, singletonInstances) {
90
+ this.registrations = registrations;
91
+ this.singletonInstances = singletonInstances;
92
+ this.scopedInstances = new Map;
93
+ this.resolvingTokens = new Set;
94
+ this[INTERNALS] = {
95
+ instances: this.singletonInstances,
96
+ isDisposed: () => this.disposed,
97
+ markDisposed: () => {
98
+ this.disposed = true;
99
+ },
100
+ ownInstances: this.scopedInstances,
101
+ registrations: this.registrations
102
+ };
103
+ }
104
+ resolve(token) {
105
+ return this.resolveToken(token, true);
106
+ }
107
+ tryResolve(token) {
108
+ return this.resolveToken(token, false);
109
+ }
110
+ resolveToken(token, required) {
111
+ if (this.disposed) {
112
+ throw new ContainerError("Cannot resolve from a disposed scope.");
113
+ }
114
+ const singletonCached = this.singletonInstances.get(token);
115
+ if (singletonCached !== undefined) {
116
+ return singletonCached;
117
+ }
118
+ const scopedCached = this.scopedInstances.get(token);
119
+ if (scopedCached !== undefined) {
120
+ return scopedCached;
121
+ }
122
+ const registration = this.registrations.get(token);
123
+ if (registration === undefined) {
124
+ if (required) {
125
+ throw new ContainerError(`Token "${tokenToString(token)}" is not registered.`);
126
+ }
127
+ return;
128
+ }
129
+ if (this.resolvingTokens.has(token)) {
130
+ throw new ContainerError(`Circular dependency detected: ${buildCircularPath(this.resolvingTokens, token)}`);
131
+ }
132
+ this.resolvingTokens.add(token);
133
+ try {
134
+ const instance = registration.factory(this);
135
+ if (registration.lifetime === "singleton") {
136
+ this.singletonInstances.set(token, instance);
137
+ } else if (registration.lifetime === "scoped") {
138
+ this.scopedInstances.set(token, instance);
139
+ }
140
+ return instance;
141
+ } finally {
142
+ this.resolvingTokens.delete(token);
143
+ }
144
+ }
145
+ }
@@ -1,4 +1,18 @@
1
+ import type { Container } from '../container';
2
+ import { type ContainerInternals, INTERNALS } from '../internal';
1
3
  import type { AbstractConstructor, Registration } from '../resolver';
4
+ /**
5
+ * Create a new scope (child container) from a Container or an existing Scope.
6
+ *
7
+ * The scope inherits all registrations from the source.
8
+ * Singleton instances are shared with the parent, while scoped instances are local to the scope.
9
+ *
10
+ * @param source A Container or Scope to create a child scope from
11
+ * @returns A new Scope instance
12
+ * @throws ContainerError if the source has been disposed
13
+ */
14
+ export declare function createScope<T, Sync extends AbstractConstructor, Async extends AbstractConstructor, ScopedT, ScopedSync extends AbstractConstructor, ScopedAsync extends AbstractConstructor>(source: Container<T, Sync, Async, ScopedT, ScopedSync, ScopedAsync>): Scope<T, Sync, Async, ScopedT, ScopedSync, ScopedAsync>;
15
+ export declare function createScope<T, Sync extends AbstractConstructor, Async extends AbstractConstructor, ScopedT, ScopedSync extends AbstractConstructor, ScopedAsync extends AbstractConstructor>(source: Scope<T, Sync, Async, ScopedT, ScopedSync, ScopedAsync>): Scope<T, Sync, Async, ScopedT, ScopedSync, ScopedAsync>;
2
16
  /**
3
17
  * Scoped child container.
4
18
  *
@@ -13,12 +27,18 @@ import type { AbstractConstructor, Registration } from '../resolver';
13
27
  * @template ScopedSync Union of scoped sync class constructors
14
28
  * @template ScopedAsync Union of scoped async class constructors
15
29
  */
16
- export declare class Scope<T = Record<never, never>, Sync extends AbstractConstructor = never, Async extends AbstractConstructor = never, ScopedT = Record<never, never>, ScopedSync extends AbstractConstructor = never, ScopedAsync extends AbstractConstructor = never> implements AsyncDisposable {
30
+ export declare class Scope<T = Record<never, never>, Sync extends AbstractConstructor = never, Async extends AbstractConstructor = never, ScopedT = Record<never, never>, ScopedSync extends AbstractConstructor = never, ScopedAsync extends AbstractConstructor = never> {
17
31
  private readonly registrations;
18
32
  private readonly singletonInstances;
19
33
  private readonly scopedInstances;
20
34
  private readonly resolvingTokens;
21
35
  private disposed;
36
+ /**
37
+ * Internal state accessor for extension modules (scope, disposable).
38
+ *
39
+ * @internal
40
+ */
41
+ readonly [INTERNALS]: ContainerInternals;
22
42
  constructor(registrations: Map<unknown, Registration>, singletonInstances: Map<unknown, unknown>);
23
43
  /**
24
44
  * Resolve an instance for the given token.
@@ -56,26 +76,4 @@ export declare class Scope<T = Record<never, never>, Sync extends AbstractConstr
56
76
  * @returns The resolved instance, or undefined if not registered and required is false
57
77
  */
58
78
  private resolveToken;
59
- /**
60
- * Create a nested scope.
61
- *
62
- * The nested scope shares singleton instances with the parent but has its own scoped instance cache.
63
- *
64
- * @returns A new Scope instance
65
- * @throws ContainerError if the scope has been disposed
66
- */
67
- createScope(): Scope<T, Sync, Async, ScopedT, ScopedSync, ScopedAsync>;
68
- /**
69
- * Dispose all scoped instances managed by this scope.
70
- *
71
- * Iterates through scoped instances in reverse creation order (LIFO) and calls
72
- * `[Symbol.asyncDispose]()` or `[Symbol.dispose]()` on each instance that implements them.
73
- * Singleton instances are not disposed as they are owned by the parent container.
74
- *
75
- * This method is idempotent — subsequent calls after the first are no-ops.
76
- * After disposal, `resolve()` and `createScope()` will throw `ContainerError`.
77
- *
78
- * @throws AggregateError if one or more instances throw during disposal
79
- */
80
- [Symbol.asyncDispose](): Promise<void>;
81
79
  }
@@ -0,0 +1,86 @@
1
+ import {
2
+ ContainerError,
3
+ buildCircularPath,
4
+ tokenToString
5
+ } from "../index-g50fxds1.js";
6
+ import {
7
+ INTERNALS
8
+ } from "../index-jx8b52m0.js";
9
+
10
+ // src/scope/index.ts
11
+ function createScope(source) {
12
+ const internals = source[INTERNALS];
13
+ if (internals.isDisposed()) {
14
+ throw new ContainerError("Cannot create a scope from a disposed container.");
15
+ }
16
+ return new Scope(internals.registrations, internals.instances);
17
+ }
18
+
19
+ class Scope {
20
+ registrations;
21
+ singletonInstances;
22
+ scopedInstances;
23
+ resolvingTokens;
24
+ disposed = false;
25
+ [INTERNALS];
26
+ constructor(registrations, singletonInstances) {
27
+ this.registrations = registrations;
28
+ this.singletonInstances = singletonInstances;
29
+ this.scopedInstances = new Map;
30
+ this.resolvingTokens = new Set;
31
+ this[INTERNALS] = {
32
+ instances: this.singletonInstances,
33
+ isDisposed: () => this.disposed,
34
+ markDisposed: () => {
35
+ this.disposed = true;
36
+ },
37
+ ownInstances: this.scopedInstances,
38
+ registrations: this.registrations
39
+ };
40
+ }
41
+ resolve(token) {
42
+ return this.resolveToken(token, true);
43
+ }
44
+ tryResolve(token) {
45
+ return this.resolveToken(token, false);
46
+ }
47
+ resolveToken(token, required) {
48
+ if (this.disposed) {
49
+ throw new ContainerError("Cannot resolve from a disposed scope.");
50
+ }
51
+ const singletonCached = this.singletonInstances.get(token);
52
+ if (singletonCached !== undefined) {
53
+ return singletonCached;
54
+ }
55
+ const scopedCached = this.scopedInstances.get(token);
56
+ if (scopedCached !== undefined) {
57
+ return scopedCached;
58
+ }
59
+ const registration = this.registrations.get(token);
60
+ if (registration === undefined) {
61
+ if (required) {
62
+ throw new ContainerError(`Token "${tokenToString(token)}" is not registered.`);
63
+ }
64
+ return;
65
+ }
66
+ if (this.resolvingTokens.has(token)) {
67
+ throw new ContainerError(`Circular dependency detected: ${buildCircularPath(this.resolvingTokens, token)}`);
68
+ }
69
+ this.resolvingTokens.add(token);
70
+ try {
71
+ const instance = registration.factory(this);
72
+ if (registration.lifetime === "singleton") {
73
+ this.singletonInstances.set(token, instance);
74
+ } else if (registration.lifetime === "scoped") {
75
+ this.scopedInstances.set(token, instance);
76
+ }
77
+ return instance;
78
+ } finally {
79
+ this.resolvingTokens.delete(token);
80
+ }
81
+ }
82
+ }
83
+ export {
84
+ createScope,
85
+ Scope
86
+ };
package/package.json CHANGED
@@ -1,14 +1,25 @@
1
1
  {
2
2
  "name": "katagami",
3
- "version": "1.1.0",
3
+ "version": "2.0.0",
4
4
  "description": "Lightweight DI container for TypeScript and JavaScript — full type inference, no decorators, no reflect-metadata, hybrid class & PropertyKey tokens.",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
+ "sideEffects": false,
7
8
  "exports": {
8
9
  ".": {
9
10
  "types": "./dist/index.d.ts",
10
11
  "import": "./dist/index.js",
11
12
  "require": "./dist/index.cjs"
13
+ },
14
+ "./scope": {
15
+ "types": "./dist/scope/index.d.ts",
16
+ "import": "./dist/scope/index.js",
17
+ "require": "./dist/scope/index.cjs"
18
+ },
19
+ "./disposable": {
20
+ "types": "./dist/disposable/index.d.ts",
21
+ "import": "./dist/disposable/index.js",
22
+ "require": "./dist/disposable/index.cjs"
12
23
  }
13
24
  },
14
25
  "main": "./dist/index.cjs",
@@ -34,8 +45,8 @@
34
45
  "clean": "rm -rf dist",
35
46
  "build": "bun run clean && bun run build:types && bun run build:esm && bun run build:cjs",
36
47
  "build:types": "tsc -p tsconfig.build.json",
37
- "build:esm": "bun build ./src/index.ts --outdir dist --format esm",
38
- "build:cjs": "bun build ./src/index.ts --outfile dist/index.cjs --format cjs",
48
+ "build:esm": "bun build ./src/index.ts ./src/scope/index.ts ./src/disposable/index.ts --outdir dist --format esm --splitting",
49
+ "build:cjs": "bun build ./src/index.ts --outfile dist/index.cjs --format cjs && bun build ./src/scope/index.ts --outfile dist/scope/index.cjs --format cjs && bun build ./src/disposable/index.ts --outfile dist/disposable/index.cjs --format cjs",
39
50
  "test": "bun test --coverage --dots",
40
51
  "prepublishOnly": "bun run build",
41
52
  "check": "bun run format",