katagami 2.3.0 → 3.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.cjs CHANGED
@@ -29,12 +29,65 @@ var __export = (target, all) => {
29
29
  // src/index.ts
30
30
  var exports_src = {};
31
31
  __export(exports_src, {
32
+ createScope: () => createScope,
32
33
  createContainer: () => createContainer,
34
+ Scope: () => Scope,
33
35
  ContainerError: () => ContainerError,
34
36
  Container: () => Container
35
37
  });
36
38
  module.exports = __toCommonJS(exports_src);
37
39
 
40
+ // src/internal.ts
41
+ var INTERNALS = Symbol("katagami.internals");
42
+
43
+ // src/container/index.ts
44
+ function createContainer() {
45
+ return new Container;
46
+ }
47
+
48
+ class Container {
49
+ registrations;
50
+ singletonCache;
51
+ disposed = false;
52
+ [INTERNALS];
53
+ constructor() {
54
+ this.registrations = new Map;
55
+ this.singletonCache = new Map;
56
+ this[INTERNALS] = {
57
+ isDisposed: () => this.disposed,
58
+ markDisposed: () => {
59
+ this.disposed = true;
60
+ },
61
+ ownCache: this.singletonCache,
62
+ registrations: this.registrations,
63
+ singletonCache: this.singletonCache
64
+ };
65
+ }
66
+ registerSingleton(token, factory) {
67
+ return this.addRegistration(token, factory, "singleton");
68
+ }
69
+ registerTransient(token, factory) {
70
+ return this.addRegistration(token, factory, "transient");
71
+ }
72
+ registerScoped(token, factory) {
73
+ return this.addRegistration(token, factory, "scoped");
74
+ }
75
+ use(source) {
76
+ for (const [token, registrations] of source[INTERNALS].registrations) {
77
+ this.registrations.set(token, [...registrations]);
78
+ }
79
+ return this;
80
+ }
81
+ addRegistration(token, factory, lifetime) {
82
+ const existing = this.registrations.get(token);
83
+ if (existing !== undefined) {
84
+ existing.push({ factory, lifetime });
85
+ } else {
86
+ this.registrations.set(token, [{ factory, lifetime }]);
87
+ }
88
+ return this;
89
+ }
90
+ }
38
91
  // src/error/index.ts
39
92
  class ContainerError extends Error {
40
93
  constructor(message) {
@@ -42,10 +95,6 @@ class ContainerError extends Error {
42
95
  this.name = "ContainerError";
43
96
  }
44
97
  }
45
-
46
- // src/internal.ts
47
- var INTERNALS = Symbol("katagami.internals");
48
-
49
98
  // src/resolver/index.ts
50
99
  function tokenToString(token) {
51
100
  if (typeof token === "function") {
@@ -71,46 +120,38 @@ function buildCircularPath(resolvingTokens, token) {
71
120
  return path.join(" -> ");
72
121
  }
73
122
 
74
- // src/container/index.ts
75
- function createContainer() {
76
- return new Container;
123
+ // src/scope/index.ts
124
+ function createScope(source) {
125
+ const internals = source[INTERNALS];
126
+ if (internals.isDisposed()) {
127
+ throw new ContainerError("Cannot create a scope from a disposed container.");
128
+ }
129
+ return new Scope(internals.registrations, internals.singletonCache);
77
130
  }
78
131
 
79
- class Container {
132
+ class Scope {
80
133
  registrations;
81
134
  singletonCache;
135
+ scopedCache;
82
136
  resolvingTokens;
137
+ singletonDepth = 0;
83
138
  disposed = false;
84
139
  [INTERNALS];
85
- constructor() {
86
- this.registrations = new Map;
87
- this.singletonCache = new Map;
140
+ constructor(registrations, singletonCache) {
141
+ this.registrations = registrations;
142
+ this.singletonCache = singletonCache;
143
+ this.scopedCache = new Map;
88
144
  this.resolvingTokens = new Set;
89
145
  this[INTERNALS] = {
90
146
  isDisposed: () => this.disposed,
91
147
  markDisposed: () => {
92
148
  this.disposed = true;
93
149
  },
94
- ownCache: this.singletonCache,
150
+ ownCache: this.scopedCache,
95
151
  registrations: this.registrations,
96
152
  singletonCache: this.singletonCache
97
153
  };
98
154
  }
99
- registerSingleton(token, factory) {
100
- return this.addRegistration(token, factory, "singleton");
101
- }
102
- registerTransient(token, factory) {
103
- return this.addRegistration(token, factory, "transient");
104
- }
105
- registerScoped(token, factory) {
106
- return this.addRegistration(token, factory, "scoped");
107
- }
108
- use(source) {
109
- for (const [token, registrations] of source[INTERNALS].registrations) {
110
- this.registrations.set(token, [...registrations]);
111
- }
112
- return this;
113
- }
114
155
  resolve(token) {
115
156
  return this.resolveToken(token, true);
116
157
  }
@@ -125,7 +166,7 @@ class Container {
125
166
  }
126
167
  resolveToken(token, required) {
127
168
  if (this.disposed) {
128
- throw new ContainerError("Cannot resolve from a disposed container.");
169
+ throw new ContainerError("Cannot resolve from a disposed scope.");
129
170
  }
130
171
  const registrations = this.registrations.get(token);
131
172
  if (registrations === undefined || registrations.length === 0) {
@@ -135,30 +176,42 @@ class Container {
135
176
  return;
136
177
  }
137
178
  const registration = registrations[registrations.length - 1];
138
- const cached = this.singletonCache.get(registration);
139
- if (cached !== undefined) {
140
- return cached;
179
+ const singletonCached = this.singletonCache.get(registration);
180
+ if (singletonCached !== undefined) {
181
+ return singletonCached;
182
+ }
183
+ if (registration.lifetime === "scoped" && this.singletonDepth > 0) {
184
+ throw new ContainerError(`Captive dependency detected: scoped token "${tokenToString(token)}" cannot be resolved inside a singleton factory. Scoped instances must not be captured by singletons.`);
141
185
  }
142
- if (registration.lifetime === "scoped") {
143
- throw new ContainerError(`Cannot resolve scoped token "${tokenToString(token)}" from the root container. Use createScope() to create a scope first.`);
186
+ const scopedCached = this.scopedCache.get(registration);
187
+ if (scopedCached !== undefined) {
188
+ return scopedCached;
144
189
  }
145
190
  if (this.resolvingTokens.has(token)) {
146
191
  throw new ContainerError(`Circular dependency detected: ${buildCircularPath(this.resolvingTokens, token)}`);
147
192
  }
148
193
  this.resolvingTokens.add(token);
194
+ if (registration.lifetime === "singleton") {
195
+ this.singletonDepth++;
196
+ }
149
197
  try {
150
198
  const instance = registration.factory(this);
151
199
  if (registration.lifetime === "singleton") {
152
200
  this.singletonCache.set(registration, instance);
201
+ } else if (registration.lifetime === "scoped") {
202
+ this.scopedCache.set(registration, instance);
153
203
  }
154
204
  return instance;
155
205
  } finally {
206
+ if (registration.lifetime === "singleton") {
207
+ this.singletonDepth--;
208
+ }
156
209
  this.resolvingTokens.delete(token);
157
210
  }
158
211
  }
159
212
  resolveAllTokens(token, required) {
160
213
  if (this.disposed) {
161
- throw new ContainerError("Cannot resolve from a disposed container.");
214
+ throw new ContainerError("Cannot resolve from a disposed scope.");
162
215
  }
163
216
  const registrations = this.registrations.get(token);
164
217
  if (registrations === undefined || registrations.length === 0) {
@@ -174,30 +227,36 @@ class Container {
174
227
  try {
175
228
  return registrations.map((registration) => {
176
229
  const reg = registration;
177
- const cached = this.singletonCache.get(registration);
178
- if (cached !== undefined) {
179
- return cached;
230
+ const singletonCached = this.singletonCache.get(registration);
231
+ if (singletonCached !== undefined) {
232
+ return singletonCached;
180
233
  }
181
- if (reg.lifetime === "scoped") {
182
- throw new ContainerError(`Cannot resolve scoped token "${tokenToString(token)}" from the root container. Use createScope() to create a scope first.`);
234
+ if (reg.lifetime === "scoped" && this.singletonDepth > 0) {
235
+ throw new ContainerError(`Captive dependency detected: scoped token "${tokenToString(token)}" cannot be resolved inside a singleton factory. Scoped instances must not be captured by singletons.`);
236
+ }
237
+ const scopedCached = this.scopedCache.get(registration);
238
+ if (scopedCached !== undefined) {
239
+ return scopedCached;
183
240
  }
184
- const instance = reg.factory(this);
185
241
  if (reg.lifetime === "singleton") {
186
- this.singletonCache.set(registration, instance);
242
+ this.singletonDepth++;
243
+ }
244
+ try {
245
+ const instance = reg.factory(this);
246
+ if (reg.lifetime === "singleton") {
247
+ this.singletonCache.set(registration, instance);
248
+ } else if (reg.lifetime === "scoped") {
249
+ this.scopedCache.set(registration, instance);
250
+ }
251
+ return instance;
252
+ } finally {
253
+ if (reg.lifetime === "singleton") {
254
+ this.singletonDepth--;
255
+ }
187
256
  }
188
- return instance;
189
257
  });
190
258
  } finally {
191
259
  this.resolvingTokens.delete(token);
192
260
  }
193
261
  }
194
- addRegistration(token, factory, lifetime) {
195
- const existing = this.registrations.get(token);
196
- if (existing !== undefined) {
197
- existing.push({ factory, lifetime });
198
- } else {
199
- this.registrations.set(token, [{ factory, lifetime }]);
200
- }
201
- return this;
202
- }
203
262
  }
package/dist/index.d.ts CHANGED
@@ -3,4 +3,4 @@ export type { DisposableContainer, DisposableScope, disposable } from './disposa
3
3
  export { ContainerError } from './error';
4
4
  export type { lazy } from './lazy';
5
5
  export type { Resolver } from './resolver';
6
- export type { createScope, Scope } from './scope';
6
+ export { createScope, Scope } from './scope';
package/dist/index.js CHANGED
@@ -1,8 +1,3 @@
1
- import {
2
- ContainerError,
3
- buildCircularPath,
4
- tokenToString
5
- } from "./index-g50fxds1.js";
6
1
  import {
7
2
  INTERNALS
8
3
  } from "./index-jx8b52m0.js";
@@ -15,13 +10,11 @@ function createContainer() {
15
10
  class Container {
16
11
  registrations;
17
12
  singletonCache;
18
- resolvingTokens;
19
13
  disposed = false;
20
14
  [INTERNALS];
21
15
  constructor() {
22
16
  this.registrations = new Map;
23
17
  this.singletonCache = new Map;
24
- this.resolvingTokens = new Set;
25
18
  this[INTERNALS] = {
26
19
  isDisposed: () => this.disposed,
27
20
  markDisposed: () => {
@@ -47,6 +40,80 @@ class Container {
47
40
  }
48
41
  return this;
49
42
  }
43
+ addRegistration(token, factory, lifetime) {
44
+ const existing = this.registrations.get(token);
45
+ if (existing !== undefined) {
46
+ existing.push({ factory, lifetime });
47
+ } else {
48
+ this.registrations.set(token, [{ factory, lifetime }]);
49
+ }
50
+ return this;
51
+ }
52
+ }
53
+ // src/error/index.ts
54
+ class ContainerError extends Error {
55
+ constructor(message) {
56
+ super(message);
57
+ this.name = "ContainerError";
58
+ }
59
+ }
60
+ // src/resolver/index.ts
61
+ function tokenToString(token) {
62
+ if (typeof token === "function") {
63
+ return token.name || "anonymous function";
64
+ }
65
+ if (typeof token === "symbol") {
66
+ return token.toString();
67
+ }
68
+ return String(token);
69
+ }
70
+ function buildCircularPath(resolvingTokens, token) {
71
+ const path = [];
72
+ let found = false;
73
+ for (const t of resolvingTokens) {
74
+ if (t === token) {
75
+ found = true;
76
+ }
77
+ if (found) {
78
+ path.push(tokenToString(t));
79
+ }
80
+ }
81
+ path.push(tokenToString(token));
82
+ return path.join(" -> ");
83
+ }
84
+
85
+ // src/scope/index.ts
86
+ function createScope(source) {
87
+ const internals = source[INTERNALS];
88
+ if (internals.isDisposed()) {
89
+ throw new ContainerError("Cannot create a scope from a disposed container.");
90
+ }
91
+ return new Scope(internals.registrations, internals.singletonCache);
92
+ }
93
+
94
+ class Scope {
95
+ registrations;
96
+ singletonCache;
97
+ scopedCache;
98
+ resolvingTokens;
99
+ singletonDepth = 0;
100
+ disposed = false;
101
+ [INTERNALS];
102
+ constructor(registrations, singletonCache) {
103
+ this.registrations = registrations;
104
+ this.singletonCache = singletonCache;
105
+ this.scopedCache = new Map;
106
+ this.resolvingTokens = new Set;
107
+ this[INTERNALS] = {
108
+ isDisposed: () => this.disposed,
109
+ markDisposed: () => {
110
+ this.disposed = true;
111
+ },
112
+ ownCache: this.scopedCache,
113
+ registrations: this.registrations,
114
+ singletonCache: this.singletonCache
115
+ };
116
+ }
50
117
  resolve(token) {
51
118
  return this.resolveToken(token, true);
52
119
  }
@@ -61,7 +128,7 @@ class Container {
61
128
  }
62
129
  resolveToken(token, required) {
63
130
  if (this.disposed) {
64
- throw new ContainerError("Cannot resolve from a disposed container.");
131
+ throw new ContainerError("Cannot resolve from a disposed scope.");
65
132
  }
66
133
  const registrations = this.registrations.get(token);
67
134
  if (registrations === undefined || registrations.length === 0) {
@@ -71,30 +138,42 @@ class Container {
71
138
  return;
72
139
  }
73
140
  const registration = registrations[registrations.length - 1];
74
- const cached = this.singletonCache.get(registration);
75
- if (cached !== undefined) {
76
- return cached;
141
+ const singletonCached = this.singletonCache.get(registration);
142
+ if (singletonCached !== undefined) {
143
+ return singletonCached;
144
+ }
145
+ if (registration.lifetime === "scoped" && this.singletonDepth > 0) {
146
+ throw new ContainerError(`Captive dependency detected: scoped token "${tokenToString(token)}" cannot be resolved inside a singleton factory. Scoped instances must not be captured by singletons.`);
77
147
  }
78
- if (registration.lifetime === "scoped") {
79
- throw new ContainerError(`Cannot resolve scoped token "${tokenToString(token)}" from the root container. Use createScope() to create a scope first.`);
148
+ const scopedCached = this.scopedCache.get(registration);
149
+ if (scopedCached !== undefined) {
150
+ return scopedCached;
80
151
  }
81
152
  if (this.resolvingTokens.has(token)) {
82
153
  throw new ContainerError(`Circular dependency detected: ${buildCircularPath(this.resolvingTokens, token)}`);
83
154
  }
84
155
  this.resolvingTokens.add(token);
156
+ if (registration.lifetime === "singleton") {
157
+ this.singletonDepth++;
158
+ }
85
159
  try {
86
160
  const instance = registration.factory(this);
87
161
  if (registration.lifetime === "singleton") {
88
162
  this.singletonCache.set(registration, instance);
163
+ } else if (registration.lifetime === "scoped") {
164
+ this.scopedCache.set(registration, instance);
89
165
  }
90
166
  return instance;
91
167
  } finally {
168
+ if (registration.lifetime === "singleton") {
169
+ this.singletonDepth--;
170
+ }
92
171
  this.resolvingTokens.delete(token);
93
172
  }
94
173
  }
95
174
  resolveAllTokens(token, required) {
96
175
  if (this.disposed) {
97
- throw new ContainerError("Cannot resolve from a disposed container.");
176
+ throw new ContainerError("Cannot resolve from a disposed scope.");
98
177
  }
99
178
  const registrations = this.registrations.get(token);
100
179
  if (registrations === undefined || registrations.length === 0) {
@@ -110,35 +189,43 @@ class Container {
110
189
  try {
111
190
  return registrations.map((registration) => {
112
191
  const reg = registration;
113
- const cached = this.singletonCache.get(registration);
114
- if (cached !== undefined) {
115
- return cached;
192
+ const singletonCached = this.singletonCache.get(registration);
193
+ if (singletonCached !== undefined) {
194
+ return singletonCached;
195
+ }
196
+ if (reg.lifetime === "scoped" && this.singletonDepth > 0) {
197
+ throw new ContainerError(`Captive dependency detected: scoped token "${tokenToString(token)}" cannot be resolved inside a singleton factory. Scoped instances must not be captured by singletons.`);
116
198
  }
117
- if (reg.lifetime === "scoped") {
118
- throw new ContainerError(`Cannot resolve scoped token "${tokenToString(token)}" from the root container. Use createScope() to create a scope first.`);
199
+ const scopedCached = this.scopedCache.get(registration);
200
+ if (scopedCached !== undefined) {
201
+ return scopedCached;
119
202
  }
120
- const instance = reg.factory(this);
121
203
  if (reg.lifetime === "singleton") {
122
- this.singletonCache.set(registration, instance);
204
+ this.singletonDepth++;
205
+ }
206
+ try {
207
+ const instance = reg.factory(this);
208
+ if (reg.lifetime === "singleton") {
209
+ this.singletonCache.set(registration, instance);
210
+ } else if (reg.lifetime === "scoped") {
211
+ this.scopedCache.set(registration, instance);
212
+ }
213
+ return instance;
214
+ } finally {
215
+ if (reg.lifetime === "singleton") {
216
+ this.singletonDepth--;
217
+ }
123
218
  }
124
- return instance;
125
219
  });
126
220
  } finally {
127
221
  this.resolvingTokens.delete(token);
128
222
  }
129
223
  }
130
- addRegistration(token, factory, lifetime) {
131
- const existing = this.registrations.get(token);
132
- if (existing !== undefined) {
133
- existing.push({ factory, lifetime });
134
- } else {
135
- this.registrations.set(token, [{ factory, lifetime }]);
136
- }
137
- return this;
138
- }
139
224
  }
140
225
  export {
226
+ createScope,
141
227
  createContainer,
228
+ Scope,
142
229
  ContainerError,
143
230
  Container
144
231
  };
@@ -1,5 +1,4 @@
1
- import type { Container } from '../container';
2
- import type { DisposableContainer, DisposableScope } from '../disposable';
1
+ import type { DisposableScope } from '../disposable';
3
2
  import type { AbstractConstructor } from '../resolver';
4
3
  import type { Scope } from '../scope';
5
4
  /**
@@ -12,24 +11,23 @@ import type { Scope } from '../scope';
12
11
  * Only **sync class tokens** are supported. Async tokens and PropertyKey tokens
13
12
  * are rejected at the type level.
14
13
  *
15
- * @param source A Container, Scope, DisposableContainer, or DisposableScope
14
+ * @param source A Scope or DisposableScope
16
15
  * @param token A sync class constructor token
17
16
  * @returns A proxy that transparently forwards to the lazily-resolved instance
18
17
  *
19
18
  * @example
20
19
  * ```ts
21
- * import { createContainer } from 'katagami';
20
+ * import { createContainer, createScope } from 'katagami';
22
21
  * import { lazy } from 'katagami/lazy';
23
22
  *
24
23
  * const container = createContainer()
25
24
  * .registerSingleton(HeavyService, () => new HeavyService());
26
25
  *
27
- * const service = lazy(container, HeavyService);
26
+ * const scope = createScope(container);
27
+ * const service = lazy(scope, HeavyService);
28
28
  * // Instance is NOT created yet
29
29
  * service.doSomething(); // resolved here, then cached
30
30
  * ```
31
31
  */
32
- export declare function lazy<T, Sync extends AbstractConstructor, Async extends AbstractConstructor, ScopedT, ScopedSync extends AbstractConstructor, ScopedAsync extends AbstractConstructor, V>(source: Container<T, Sync, Async, ScopedT, ScopedSync, ScopedAsync>, token: AbstractConstructor<V> & Sync): V;
33
32
  export declare function lazy<T, Sync extends AbstractConstructor, Async extends AbstractConstructor, ScopedT, ScopedSync extends AbstractConstructor, ScopedAsync extends AbstractConstructor, V>(source: Scope<T, Sync, Async, ScopedT, ScopedSync, ScopedAsync>, token: AbstractConstructor<V> & (Sync | ScopedSync)): V;
34
- export declare function lazy<T, Sync extends AbstractConstructor, Async extends AbstractConstructor, ScopedT, ScopedSync extends AbstractConstructor, ScopedAsync extends AbstractConstructor, V>(source: DisposableContainer<T, Sync, Async, ScopedT, ScopedSync, ScopedAsync>, token: AbstractConstructor<V> & Sync): V;
35
33
  export declare function lazy<T, Sync extends AbstractConstructor, Async extends AbstractConstructor, ScopedT, ScopedSync extends AbstractConstructor, ScopedAsync extends AbstractConstructor, V>(source: DisposableScope<T, Sync, Async, ScopedT, ScopedSync, ScopedAsync>, token: AbstractConstructor<V> & (Sync | ScopedSync)): V;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "katagami",
3
- "version": "2.3.0",
3
+ "version": "3.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",
@@ -11,11 +11,6 @@
11
11
  "import": "./dist/index.js",
12
12
  "require": "./dist/index.cjs"
13
13
  },
14
- "./scope": {
15
- "types": "./dist/scope/index.d.ts",
16
- "import": "./dist/scope/index.js",
17
- "require": "./dist/scope/index.cjs"
18
- },
19
14
  "./disposable": {
20
15
  "types": "./dist/disposable/index.d.ts",
21
16
  "import": "./dist/disposable/index.js",
@@ -50,8 +45,8 @@
50
45
  "clean": "rm -rf dist",
51
46
  "build": "bun run clean && bun run build:types && bun run build:esm && bun run build:cjs",
52
47
  "build:types": "tsc -p tsconfig.build.json",
53
- "build:esm": "bun build ./src/index.ts ./src/scope/index.ts ./src/disposable/index.ts ./src/lazy/index.ts --outdir dist --format esm --splitting",
54
- "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 && bun build ./src/lazy/index.ts --outfile dist/lazy/index.cjs --format cjs",
48
+ "build:esm": "bun build ./src/index.ts ./src/disposable/index.ts ./src/lazy/index.ts --outdir dist --format esm --splitting",
49
+ "build:cjs": "bun build ./src/index.ts --outfile dist/index.cjs --format cjs && bun build ./src/disposable/index.ts --outfile dist/disposable/index.cjs --format cjs && bun build ./src/lazy/index.ts --outfile dist/lazy/index.cjs --format cjs",
55
50
  "prepublishOnly": "bun run build",
56
51
  "check": "bun run format",
57
52
  "lint": "biome lint .",
@@ -1,34 +0,0 @@
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 };