katagami 2.3.0 → 3.0.1
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 +82 -592
- package/dist/chunk-J2NYR3SH.js +6 -0
- package/dist/container/index.d.cts +108 -0
- package/dist/container/index.d.ts +5 -82
- package/dist/disposable/index.cjs +16 -25
- package/dist/disposable/index.d.cts +69 -0
- package/dist/disposable/index.d.ts +16 -8
- package/dist/disposable/index.js +1 -1
- package/dist/error/index.d.cts +11 -0
- package/dist/index.cjs +181 -86
- package/dist/index.d.cts +6 -0
- package/dist/index.d.ts +6 -6
- package/dist/index.js +177 -45
- package/dist/internal.d.cts +29 -0
- package/dist/internal.d.ts +3 -1
- package/dist/lazy/index.cjs +16 -25
- package/dist/lazy/index.d.cts +33 -0
- package/dist/lazy/index.d.ts +7 -9
- package/dist/lazy/index.js +1 -1
- package/dist/resolver/index.d.cts +93 -0
- package/dist/scope/index.d.cts +120 -0
- package/dist/scope/index.d.ts +4 -4
- package/docs/README.de.md +65 -0
- package/docs/README.es.md +65 -0
- package/docs/README.fr.md +65 -0
- package/docs/README.ja.md +66 -0
- package/docs/README.ko.md +65 -0
- package/docs/README.zh-CN.md +65 -0
- package/docs/README.zh-TW.md +65 -0
- package/docs/ai-coding-agents.md +78 -0
- package/docs/articles/ai-coding-agents.ja.md +83 -0
- package/docs/articles/ai-coding-agents.md +70 -0
- package/docs/articles/request-scope.md +48 -0
- package/docs/articles/without-decorators.md +54 -0
- package/docs/choosing-di.md +30 -0
- package/docs/growth/baseline-2026-09-11.json +68 -0
- package/docs/growth/github-metadata.json +13 -0
- package/docs/growth/rollout.md +77 -0
- package/docs/guide.md +186 -0
- package/docs/type-safety.md +126 -0
- package/examples/request-scope/README.md +37 -0
- package/examples/request-scope/app.ts +31 -0
- package/examples/request-scope/demo.ts +10 -0
- package/examples/request-scope/tsconfig.json +11 -0
- package/llms.txt +16 -0
- package/package.json +56 -28
- package/dist/index-g50fxds1.js +0 -34
- package/dist/index-jx8b52m0.js +0 -4
- package/dist/scope/index.cjs +0 -212
- package/dist/scope/index.js +0 -153
package/dist/index.cjs
CHANGED
|
@@ -1,50 +1,107 @@
|
|
|
1
|
+
"use strict";
|
|
1
2
|
var __defProp = Object.defineProperty;
|
|
2
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
3
3
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
5
|
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
6
|
var __export = (target, all) => {
|
|
20
7
|
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
|
-
});
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
27
9
|
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
28
19
|
|
|
29
20
|
// src/index.ts
|
|
30
|
-
var
|
|
31
|
-
__export(
|
|
32
|
-
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
Container: () => Container,
|
|
33
24
|
ContainerError: () => ContainerError,
|
|
34
|
-
|
|
25
|
+
Scope: () => Scope,
|
|
26
|
+
createContainer: () => createContainer,
|
|
27
|
+
createScope: () => createScope
|
|
35
28
|
});
|
|
36
|
-
module.exports = __toCommonJS(
|
|
29
|
+
module.exports = __toCommonJS(index_exports);
|
|
30
|
+
|
|
31
|
+
// src/internal.ts
|
|
32
|
+
var INTERNALS = /* @__PURE__ */ Symbol.for("katagami.internals.v3");
|
|
33
|
+
|
|
34
|
+
// src/container/index.ts
|
|
35
|
+
function createContainer() {
|
|
36
|
+
return new Container();
|
|
37
|
+
}
|
|
38
|
+
var Container = class {
|
|
39
|
+
registrations;
|
|
40
|
+
singletonCache;
|
|
41
|
+
disposed = false;
|
|
42
|
+
/**
|
|
43
|
+
* Internal state accessor for extension modules (scope, disposable).
|
|
44
|
+
*
|
|
45
|
+
* @internal
|
|
46
|
+
*/
|
|
47
|
+
[INTERNALS];
|
|
48
|
+
constructor() {
|
|
49
|
+
this.registrations = /* @__PURE__ */ new Map();
|
|
50
|
+
this.singletonCache = /* @__PURE__ */ new Map();
|
|
51
|
+
this[INTERNALS] = {
|
|
52
|
+
isDisposed: () => this.disposed,
|
|
53
|
+
markDisposed: () => {
|
|
54
|
+
this.disposed = true;
|
|
55
|
+
},
|
|
56
|
+
ownCache: this.singletonCache,
|
|
57
|
+
registrations: this.registrations,
|
|
58
|
+
singletonCache: this.singletonCache
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
registerSingleton(token, factory) {
|
|
62
|
+
return this.addRegistration(token, factory, "singleton");
|
|
63
|
+
}
|
|
64
|
+
registerTransient(token, factory) {
|
|
65
|
+
return this.addRegistration(token, factory, "transient");
|
|
66
|
+
}
|
|
67
|
+
registerScoped(token, factory) {
|
|
68
|
+
return this.addRegistration(token, factory, "scoped");
|
|
69
|
+
}
|
|
70
|
+
use(source) {
|
|
71
|
+
for (const [token, registrations] of source[INTERNALS].registrations) {
|
|
72
|
+
this.registrations.set(token, [...registrations]);
|
|
73
|
+
}
|
|
74
|
+
return this;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Add a registration entry. Accumulates registrations for the same token.
|
|
78
|
+
*
|
|
79
|
+
* @param token Token
|
|
80
|
+
* @param factory Factory function
|
|
81
|
+
* @param lifetime Lifetime of the registration
|
|
82
|
+
* @returns The container for method chaining
|
|
83
|
+
*/
|
|
84
|
+
addRegistration(token, factory, lifetime) {
|
|
85
|
+
const existing = this.registrations.get(token);
|
|
86
|
+
if (existing !== void 0) {
|
|
87
|
+
existing.push({ factory, lifetime });
|
|
88
|
+
} else {
|
|
89
|
+
this.registrations.set(token, [{ factory, lifetime }]);
|
|
90
|
+
}
|
|
91
|
+
return this;
|
|
92
|
+
}
|
|
93
|
+
};
|
|
37
94
|
|
|
38
95
|
// src/error/index.ts
|
|
39
|
-
|
|
96
|
+
var ContainerError = class extends Error {
|
|
97
|
+
/**
|
|
98
|
+
* @param message Error message
|
|
99
|
+
*/
|
|
40
100
|
constructor(message) {
|
|
41
101
|
super(message);
|
|
42
102
|
this.name = "ContainerError";
|
|
43
103
|
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// src/internal.ts
|
|
47
|
-
var INTERNALS = Symbol("katagami.internals");
|
|
104
|
+
};
|
|
48
105
|
|
|
49
106
|
// src/resolver/index.ts
|
|
50
107
|
function tokenToString(token) {
|
|
@@ -71,46 +128,42 @@ function buildCircularPath(resolvingTokens, token) {
|
|
|
71
128
|
return path.join(" -> ");
|
|
72
129
|
}
|
|
73
130
|
|
|
74
|
-
// src/
|
|
75
|
-
function
|
|
76
|
-
|
|
131
|
+
// src/scope/index.ts
|
|
132
|
+
function createScope(source) {
|
|
133
|
+
const internals = source[INTERNALS];
|
|
134
|
+
if (internals.isDisposed()) {
|
|
135
|
+
throw new ContainerError("Cannot create a scope from a disposed container.");
|
|
136
|
+
}
|
|
137
|
+
return new Scope(internals.registrations, internals.singletonCache);
|
|
77
138
|
}
|
|
78
|
-
|
|
79
|
-
class Container {
|
|
139
|
+
var Scope = class {
|
|
80
140
|
registrations;
|
|
81
141
|
singletonCache;
|
|
142
|
+
scopedCache;
|
|
82
143
|
resolvingTokens;
|
|
144
|
+
singletonDepth = 0;
|
|
83
145
|
disposed = false;
|
|
146
|
+
/**
|
|
147
|
+
* Internal state accessor for extension modules (scope, disposable).
|
|
148
|
+
*
|
|
149
|
+
* @internal
|
|
150
|
+
*/
|
|
84
151
|
[INTERNALS];
|
|
85
|
-
constructor() {
|
|
86
|
-
this.registrations =
|
|
87
|
-
this.singletonCache =
|
|
88
|
-
this.
|
|
152
|
+
constructor(registrations, singletonCache) {
|
|
153
|
+
this.registrations = registrations;
|
|
154
|
+
this.singletonCache = singletonCache;
|
|
155
|
+
this.scopedCache = /* @__PURE__ */ new Map();
|
|
156
|
+
this.resolvingTokens = /* @__PURE__ */ new Set();
|
|
89
157
|
this[INTERNALS] = {
|
|
90
158
|
isDisposed: () => this.disposed,
|
|
91
159
|
markDisposed: () => {
|
|
92
160
|
this.disposed = true;
|
|
93
161
|
},
|
|
94
|
-
ownCache: this.
|
|
162
|
+
ownCache: this.scopedCache,
|
|
95
163
|
registrations: this.registrations,
|
|
96
164
|
singletonCache: this.singletonCache
|
|
97
165
|
};
|
|
98
166
|
}
|
|
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
167
|
resolve(token) {
|
|
115
168
|
return this.resolveToken(token, true);
|
|
116
169
|
}
|
|
@@ -123,49 +176,81 @@ class Container {
|
|
|
123
176
|
tryResolveAll(token) {
|
|
124
177
|
return this.resolveAllTokens(token, false);
|
|
125
178
|
}
|
|
179
|
+
/**
|
|
180
|
+
* Internal resolution logic shared by resolve and tryResolve.
|
|
181
|
+
* Resolves the last registered factory for the token.
|
|
182
|
+
*
|
|
183
|
+
* @param token Token to resolve
|
|
184
|
+
* @param required If true, throws when the token is not registered. If false, returns undefined.
|
|
185
|
+
* @returns The resolved instance, or undefined if not registered and required is false
|
|
186
|
+
*/
|
|
126
187
|
resolveToken(token, required) {
|
|
127
188
|
if (this.disposed) {
|
|
128
|
-
throw new ContainerError("Cannot resolve from a disposed
|
|
189
|
+
throw new ContainerError("Cannot resolve from a disposed scope.");
|
|
129
190
|
}
|
|
130
191
|
const registrations = this.registrations.get(token);
|
|
131
|
-
if (registrations ===
|
|
192
|
+
if (registrations === void 0 || registrations.length === 0) {
|
|
132
193
|
if (required) {
|
|
133
194
|
throw new ContainerError(`Token "${tokenToString(token)}" is not registered.`);
|
|
134
195
|
}
|
|
135
|
-
return;
|
|
196
|
+
return void 0;
|
|
136
197
|
}
|
|
137
198
|
const registration = registrations[registrations.length - 1];
|
|
138
|
-
const
|
|
139
|
-
if (
|
|
140
|
-
return
|
|
199
|
+
const singletonCached = this.singletonCache.get(registration);
|
|
200
|
+
if (singletonCached !== void 0) {
|
|
201
|
+
return singletonCached;
|
|
202
|
+
}
|
|
203
|
+
if (registration.lifetime === "scoped" && this.singletonDepth > 0) {
|
|
204
|
+
throw new ContainerError(
|
|
205
|
+
`Captive dependency detected: scoped token "${tokenToString(token)}" cannot be resolved inside a singleton factory. Scoped instances must not be captured by singletons.`
|
|
206
|
+
);
|
|
141
207
|
}
|
|
142
|
-
|
|
143
|
-
|
|
208
|
+
const scopedCached = this.scopedCache.get(registration);
|
|
209
|
+
if (scopedCached !== void 0) {
|
|
210
|
+
return scopedCached;
|
|
144
211
|
}
|
|
145
212
|
if (this.resolvingTokens.has(token)) {
|
|
146
213
|
throw new ContainerError(`Circular dependency detected: ${buildCircularPath(this.resolvingTokens, token)}`);
|
|
147
214
|
}
|
|
148
215
|
this.resolvingTokens.add(token);
|
|
216
|
+
if (registration.lifetime === "singleton") {
|
|
217
|
+
this.singletonDepth++;
|
|
218
|
+
}
|
|
149
219
|
try {
|
|
150
|
-
const instance = registration.factory(
|
|
220
|
+
const instance = registration.factory(
|
|
221
|
+
this
|
|
222
|
+
);
|
|
151
223
|
if (registration.lifetime === "singleton") {
|
|
152
224
|
this.singletonCache.set(registration, instance);
|
|
225
|
+
} else if (registration.lifetime === "scoped") {
|
|
226
|
+
this.scopedCache.set(registration, instance);
|
|
153
227
|
}
|
|
154
228
|
return instance;
|
|
155
229
|
} finally {
|
|
230
|
+
if (registration.lifetime === "singleton") {
|
|
231
|
+
this.singletonDepth--;
|
|
232
|
+
}
|
|
156
233
|
this.resolvingTokens.delete(token);
|
|
157
234
|
}
|
|
158
235
|
}
|
|
236
|
+
/**
|
|
237
|
+
* Internal resolution logic shared by resolveAll and tryResolveAll.
|
|
238
|
+
* Resolves all registered factories for the token.
|
|
239
|
+
*
|
|
240
|
+
* @param token Token to resolve
|
|
241
|
+
* @param required If true, throws when the token is not registered. If false, returns undefined.
|
|
242
|
+
* @returns An array of resolved instances, or undefined if not registered and required is false
|
|
243
|
+
*/
|
|
159
244
|
resolveAllTokens(token, required) {
|
|
160
245
|
if (this.disposed) {
|
|
161
|
-
throw new ContainerError("Cannot resolve from a disposed
|
|
246
|
+
throw new ContainerError("Cannot resolve from a disposed scope.");
|
|
162
247
|
}
|
|
163
248
|
const registrations = this.registrations.get(token);
|
|
164
|
-
if (registrations ===
|
|
249
|
+
if (registrations === void 0 || registrations.length === 0) {
|
|
165
250
|
if (required) {
|
|
166
251
|
throw new ContainerError(`Token "${tokenToString(token)}" is not registered.`);
|
|
167
252
|
}
|
|
168
|
-
return;
|
|
253
|
+
return void 0;
|
|
169
254
|
}
|
|
170
255
|
if (this.resolvingTokens.has(token)) {
|
|
171
256
|
throw new ContainerError(`Circular dependency detected: ${buildCircularPath(this.resolvingTokens, token)}`);
|
|
@@ -174,30 +259,40 @@ class Container {
|
|
|
174
259
|
try {
|
|
175
260
|
return registrations.map((registration) => {
|
|
176
261
|
const reg = registration;
|
|
177
|
-
const
|
|
178
|
-
if (
|
|
179
|
-
return
|
|
262
|
+
const singletonCached = this.singletonCache.get(registration);
|
|
263
|
+
if (singletonCached !== void 0) {
|
|
264
|
+
return singletonCached;
|
|
265
|
+
}
|
|
266
|
+
if (reg.lifetime === "scoped" && this.singletonDepth > 0) {
|
|
267
|
+
throw new ContainerError(
|
|
268
|
+
`Captive dependency detected: scoped token "${tokenToString(token)}" cannot be resolved inside a singleton factory. Scoped instances must not be captured by singletons.`
|
|
269
|
+
);
|
|
180
270
|
}
|
|
181
|
-
|
|
182
|
-
|
|
271
|
+
const scopedCached = this.scopedCache.get(registration);
|
|
272
|
+
if (scopedCached !== void 0) {
|
|
273
|
+
return scopedCached;
|
|
183
274
|
}
|
|
184
|
-
const instance = reg.factory(this);
|
|
185
275
|
if (reg.lifetime === "singleton") {
|
|
186
|
-
this.
|
|
276
|
+
this.singletonDepth++;
|
|
277
|
+
}
|
|
278
|
+
try {
|
|
279
|
+
const instance = reg.factory(
|
|
280
|
+
this
|
|
281
|
+
);
|
|
282
|
+
if (reg.lifetime === "singleton") {
|
|
283
|
+
this.singletonCache.set(registration, instance);
|
|
284
|
+
} else if (reg.lifetime === "scoped") {
|
|
285
|
+
this.scopedCache.set(registration, instance);
|
|
286
|
+
}
|
|
287
|
+
return instance;
|
|
288
|
+
} finally {
|
|
289
|
+
if (reg.lifetime === "singleton") {
|
|
290
|
+
this.singletonDepth--;
|
|
291
|
+
}
|
|
187
292
|
}
|
|
188
|
-
return instance;
|
|
189
293
|
});
|
|
190
294
|
} finally {
|
|
191
295
|
this.resolvingTokens.delete(token);
|
|
192
296
|
}
|
|
193
297
|
}
|
|
194
|
-
|
|
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
|
-
}
|
|
298
|
+
};
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { Container, createContainer } from './container/index.cjs';
|
|
2
|
+
export type { DisposableContainer, DisposableScope, disposable } from './disposable/index.cjs';
|
|
3
|
+
export { ContainerError } from './error/index.cjs';
|
|
4
|
+
export type { lazy } from './lazy/index.cjs';
|
|
5
|
+
export type { Resolver } from './resolver/index.cjs';
|
|
6
|
+
export { createScope, Scope } from './scope/index.cjs';
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
export { Container, createContainer } from './container';
|
|
2
|
-
export type { DisposableContainer, DisposableScope, disposable } from './disposable';
|
|
3
|
-
export { ContainerError } from './error';
|
|
4
|
-
export type { lazy } from './lazy';
|
|
5
|
-
export type { Resolver } from './resolver';
|
|
6
|
-
export
|
|
1
|
+
export { Container, createContainer } from './container/index.js';
|
|
2
|
+
export type { DisposableContainer, DisposableScope, disposable } from './disposable/index.js';
|
|
3
|
+
export { ContainerError } from './error/index.js';
|
|
4
|
+
export type { lazy } from './lazy/index.js';
|
|
5
|
+
export type { Resolver } from './resolver/index.js';
|
|
6
|
+
export { createScope, Scope } from './scope/index.js';
|
package/dist/index.js
CHANGED
|
@@ -1,27 +1,24 @@
|
|
|
1
|
-
import {
|
|
2
|
-
ContainerError,
|
|
3
|
-
buildCircularPath,
|
|
4
|
-
tokenToString
|
|
5
|
-
} from "./index-g50fxds1.js";
|
|
6
1
|
import {
|
|
7
2
|
INTERNALS
|
|
8
|
-
} from "./
|
|
3
|
+
} from "./chunk-J2NYR3SH.js";
|
|
9
4
|
|
|
10
5
|
// src/container/index.ts
|
|
11
6
|
function createContainer() {
|
|
12
|
-
return new Container;
|
|
7
|
+
return new Container();
|
|
13
8
|
}
|
|
14
|
-
|
|
15
|
-
class Container {
|
|
9
|
+
var Container = class {
|
|
16
10
|
registrations;
|
|
17
11
|
singletonCache;
|
|
18
|
-
resolvingTokens;
|
|
19
12
|
disposed = false;
|
|
13
|
+
/**
|
|
14
|
+
* Internal state accessor for extension modules (scope, disposable).
|
|
15
|
+
*
|
|
16
|
+
* @internal
|
|
17
|
+
*/
|
|
20
18
|
[INTERNALS];
|
|
21
19
|
constructor() {
|
|
22
|
-
this.registrations = new Map;
|
|
23
|
-
this.singletonCache = new Map;
|
|
24
|
-
this.resolvingTokens = new Set;
|
|
20
|
+
this.registrations = /* @__PURE__ */ new Map();
|
|
21
|
+
this.singletonCache = /* @__PURE__ */ new Map();
|
|
25
22
|
this[INTERNALS] = {
|
|
26
23
|
isDisposed: () => this.disposed,
|
|
27
24
|
markDisposed: () => {
|
|
@@ -47,6 +44,97 @@ class Container {
|
|
|
47
44
|
}
|
|
48
45
|
return this;
|
|
49
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* Add a registration entry. Accumulates registrations for the same token.
|
|
49
|
+
*
|
|
50
|
+
* @param token Token
|
|
51
|
+
* @param factory Factory function
|
|
52
|
+
* @param lifetime Lifetime of the registration
|
|
53
|
+
* @returns The container for method chaining
|
|
54
|
+
*/
|
|
55
|
+
addRegistration(token, factory, lifetime) {
|
|
56
|
+
const existing = this.registrations.get(token);
|
|
57
|
+
if (existing !== void 0) {
|
|
58
|
+
existing.push({ factory, lifetime });
|
|
59
|
+
} else {
|
|
60
|
+
this.registrations.set(token, [{ factory, lifetime }]);
|
|
61
|
+
}
|
|
62
|
+
return this;
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// src/error/index.ts
|
|
67
|
+
var ContainerError = class extends Error {
|
|
68
|
+
/**
|
|
69
|
+
* @param message Error message
|
|
70
|
+
*/
|
|
71
|
+
constructor(message) {
|
|
72
|
+
super(message);
|
|
73
|
+
this.name = "ContainerError";
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// src/resolver/index.ts
|
|
78
|
+
function tokenToString(token) {
|
|
79
|
+
if (typeof token === "function") {
|
|
80
|
+
return token.name || "anonymous function";
|
|
81
|
+
}
|
|
82
|
+
if (typeof token === "symbol") {
|
|
83
|
+
return token.toString();
|
|
84
|
+
}
|
|
85
|
+
return String(token);
|
|
86
|
+
}
|
|
87
|
+
function buildCircularPath(resolvingTokens, token) {
|
|
88
|
+
const path = [];
|
|
89
|
+
let found = false;
|
|
90
|
+
for (const t of resolvingTokens) {
|
|
91
|
+
if (t === token) {
|
|
92
|
+
found = true;
|
|
93
|
+
}
|
|
94
|
+
if (found) {
|
|
95
|
+
path.push(tokenToString(t));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
path.push(tokenToString(token));
|
|
99
|
+
return path.join(" -> ");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// src/scope/index.ts
|
|
103
|
+
function createScope(source) {
|
|
104
|
+
const internals = source[INTERNALS];
|
|
105
|
+
if (internals.isDisposed()) {
|
|
106
|
+
throw new ContainerError("Cannot create a scope from a disposed container.");
|
|
107
|
+
}
|
|
108
|
+
return new Scope(internals.registrations, internals.singletonCache);
|
|
109
|
+
}
|
|
110
|
+
var Scope = class {
|
|
111
|
+
registrations;
|
|
112
|
+
singletonCache;
|
|
113
|
+
scopedCache;
|
|
114
|
+
resolvingTokens;
|
|
115
|
+
singletonDepth = 0;
|
|
116
|
+
disposed = false;
|
|
117
|
+
/**
|
|
118
|
+
* Internal state accessor for extension modules (scope, disposable).
|
|
119
|
+
*
|
|
120
|
+
* @internal
|
|
121
|
+
*/
|
|
122
|
+
[INTERNALS];
|
|
123
|
+
constructor(registrations, singletonCache) {
|
|
124
|
+
this.registrations = registrations;
|
|
125
|
+
this.singletonCache = singletonCache;
|
|
126
|
+
this.scopedCache = /* @__PURE__ */ new Map();
|
|
127
|
+
this.resolvingTokens = /* @__PURE__ */ new Set();
|
|
128
|
+
this[INTERNALS] = {
|
|
129
|
+
isDisposed: () => this.disposed,
|
|
130
|
+
markDisposed: () => {
|
|
131
|
+
this.disposed = true;
|
|
132
|
+
},
|
|
133
|
+
ownCache: this.scopedCache,
|
|
134
|
+
registrations: this.registrations,
|
|
135
|
+
singletonCache: this.singletonCache
|
|
136
|
+
};
|
|
137
|
+
}
|
|
50
138
|
resolve(token) {
|
|
51
139
|
return this.resolveToken(token, true);
|
|
52
140
|
}
|
|
@@ -59,49 +147,81 @@ class Container {
|
|
|
59
147
|
tryResolveAll(token) {
|
|
60
148
|
return this.resolveAllTokens(token, false);
|
|
61
149
|
}
|
|
150
|
+
/**
|
|
151
|
+
* Internal resolution logic shared by resolve and tryResolve.
|
|
152
|
+
* Resolves the last registered factory for the token.
|
|
153
|
+
*
|
|
154
|
+
* @param token Token to resolve
|
|
155
|
+
* @param required If true, throws when the token is not registered. If false, returns undefined.
|
|
156
|
+
* @returns The resolved instance, or undefined if not registered and required is false
|
|
157
|
+
*/
|
|
62
158
|
resolveToken(token, required) {
|
|
63
159
|
if (this.disposed) {
|
|
64
|
-
throw new ContainerError("Cannot resolve from a disposed
|
|
160
|
+
throw new ContainerError("Cannot resolve from a disposed scope.");
|
|
65
161
|
}
|
|
66
162
|
const registrations = this.registrations.get(token);
|
|
67
|
-
if (registrations ===
|
|
163
|
+
if (registrations === void 0 || registrations.length === 0) {
|
|
68
164
|
if (required) {
|
|
69
165
|
throw new ContainerError(`Token "${tokenToString(token)}" is not registered.`);
|
|
70
166
|
}
|
|
71
|
-
return;
|
|
167
|
+
return void 0;
|
|
72
168
|
}
|
|
73
169
|
const registration = registrations[registrations.length - 1];
|
|
74
|
-
const
|
|
75
|
-
if (
|
|
76
|
-
return
|
|
170
|
+
const singletonCached = this.singletonCache.get(registration);
|
|
171
|
+
if (singletonCached !== void 0) {
|
|
172
|
+
return singletonCached;
|
|
173
|
+
}
|
|
174
|
+
if (registration.lifetime === "scoped" && this.singletonDepth > 0) {
|
|
175
|
+
throw new ContainerError(
|
|
176
|
+
`Captive dependency detected: scoped token "${tokenToString(token)}" cannot be resolved inside a singleton factory. Scoped instances must not be captured by singletons.`
|
|
177
|
+
);
|
|
77
178
|
}
|
|
78
|
-
|
|
79
|
-
|
|
179
|
+
const scopedCached = this.scopedCache.get(registration);
|
|
180
|
+
if (scopedCached !== void 0) {
|
|
181
|
+
return scopedCached;
|
|
80
182
|
}
|
|
81
183
|
if (this.resolvingTokens.has(token)) {
|
|
82
184
|
throw new ContainerError(`Circular dependency detected: ${buildCircularPath(this.resolvingTokens, token)}`);
|
|
83
185
|
}
|
|
84
186
|
this.resolvingTokens.add(token);
|
|
187
|
+
if (registration.lifetime === "singleton") {
|
|
188
|
+
this.singletonDepth++;
|
|
189
|
+
}
|
|
85
190
|
try {
|
|
86
|
-
const instance = registration.factory(
|
|
191
|
+
const instance = registration.factory(
|
|
192
|
+
this
|
|
193
|
+
);
|
|
87
194
|
if (registration.lifetime === "singleton") {
|
|
88
195
|
this.singletonCache.set(registration, instance);
|
|
196
|
+
} else if (registration.lifetime === "scoped") {
|
|
197
|
+
this.scopedCache.set(registration, instance);
|
|
89
198
|
}
|
|
90
199
|
return instance;
|
|
91
200
|
} finally {
|
|
201
|
+
if (registration.lifetime === "singleton") {
|
|
202
|
+
this.singletonDepth--;
|
|
203
|
+
}
|
|
92
204
|
this.resolvingTokens.delete(token);
|
|
93
205
|
}
|
|
94
206
|
}
|
|
207
|
+
/**
|
|
208
|
+
* Internal resolution logic shared by resolveAll and tryResolveAll.
|
|
209
|
+
* Resolves all registered factories for the token.
|
|
210
|
+
*
|
|
211
|
+
* @param token Token to resolve
|
|
212
|
+
* @param required If true, throws when the token is not registered. If false, returns undefined.
|
|
213
|
+
* @returns An array of resolved instances, or undefined if not registered and required is false
|
|
214
|
+
*/
|
|
95
215
|
resolveAllTokens(token, required) {
|
|
96
216
|
if (this.disposed) {
|
|
97
|
-
throw new ContainerError("Cannot resolve from a disposed
|
|
217
|
+
throw new ContainerError("Cannot resolve from a disposed scope.");
|
|
98
218
|
}
|
|
99
219
|
const registrations = this.registrations.get(token);
|
|
100
|
-
if (registrations ===
|
|
220
|
+
if (registrations === void 0 || registrations.length === 0) {
|
|
101
221
|
if (required) {
|
|
102
222
|
throw new ContainerError(`Token "${tokenToString(token)}" is not registered.`);
|
|
103
223
|
}
|
|
104
|
-
return;
|
|
224
|
+
return void 0;
|
|
105
225
|
}
|
|
106
226
|
if (this.resolvingTokens.has(token)) {
|
|
107
227
|
throw new ContainerError(`Circular dependency detected: ${buildCircularPath(this.resolvingTokens, token)}`);
|
|
@@ -110,35 +230,47 @@ class Container {
|
|
|
110
230
|
try {
|
|
111
231
|
return registrations.map((registration) => {
|
|
112
232
|
const reg = registration;
|
|
113
|
-
const
|
|
114
|
-
if (
|
|
115
|
-
return
|
|
233
|
+
const singletonCached = this.singletonCache.get(registration);
|
|
234
|
+
if (singletonCached !== void 0) {
|
|
235
|
+
return singletonCached;
|
|
116
236
|
}
|
|
117
|
-
if (reg.lifetime === "scoped") {
|
|
118
|
-
throw new ContainerError(
|
|
237
|
+
if (reg.lifetime === "scoped" && this.singletonDepth > 0) {
|
|
238
|
+
throw new ContainerError(
|
|
239
|
+
`Captive dependency detected: scoped token "${tokenToString(token)}" cannot be resolved inside a singleton factory. Scoped instances must not be captured by singletons.`
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
const scopedCached = this.scopedCache.get(registration);
|
|
243
|
+
if (scopedCached !== void 0) {
|
|
244
|
+
return scopedCached;
|
|
119
245
|
}
|
|
120
|
-
const instance = reg.factory(this);
|
|
121
246
|
if (reg.lifetime === "singleton") {
|
|
122
|
-
this.
|
|
247
|
+
this.singletonDepth++;
|
|
248
|
+
}
|
|
249
|
+
try {
|
|
250
|
+
const instance = reg.factory(
|
|
251
|
+
this
|
|
252
|
+
);
|
|
253
|
+
if (reg.lifetime === "singleton") {
|
|
254
|
+
this.singletonCache.set(registration, instance);
|
|
255
|
+
} else if (reg.lifetime === "scoped") {
|
|
256
|
+
this.scopedCache.set(registration, instance);
|
|
257
|
+
}
|
|
258
|
+
return instance;
|
|
259
|
+
} finally {
|
|
260
|
+
if (reg.lifetime === "singleton") {
|
|
261
|
+
this.singletonDepth--;
|
|
262
|
+
}
|
|
123
263
|
}
|
|
124
|
-
return instance;
|
|
125
264
|
});
|
|
126
265
|
} finally {
|
|
127
266
|
this.resolvingTokens.delete(token);
|
|
128
267
|
}
|
|
129
268
|
}
|
|
130
|
-
|
|
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
|
-
}
|
|
269
|
+
};
|
|
140
270
|
export {
|
|
141
|
-
|
|
271
|
+
Container,
|
|
142
272
|
ContainerError,
|
|
143
|
-
|
|
273
|
+
Scope,
|
|
274
|
+
createContainer,
|
|
275
|
+
createScope
|
|
144
276
|
};
|