katagami 3.0.2 → 4.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 +84 -37
- package/dist/chunk-R66WOZUM.js +71 -0
- package/dist/container/index.d.cts +142 -35
- package/dist/container/index.d.ts +142 -35
- package/dist/container/policy.d.cts +36 -0
- package/dist/container/policy.d.ts +36 -0
- package/dist/disposable/index.cjs +27 -5
- package/dist/disposable/index.d.cts +17 -7
- package/dist/disposable/index.d.ts +17 -7
- package/dist/disposable/index.js +2 -41
- package/dist/entrypoint/index.d.cts +20 -0
- package/dist/entrypoint/index.d.ts +20 -0
- package/dist/index.cjs +551 -149
- package/dist/index.d.cts +8 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +495 -150
- package/dist/internal.d.cts +65 -0
- package/dist/internal.d.ts +65 -0
- package/dist/metadata/index.d.cts +67 -0
- package/dist/metadata/index.d.ts +67 -0
- package/dist/resolver/index.d.cts +5 -0
- package/dist/resolver/index.d.ts +5 -0
- package/dist/scope/index.d.cts +49 -24
- package/dist/scope/index.d.ts +49 -24
- package/dist/scope/operations.d.cts +22 -0
- package/dist/scope/operations.d.ts +22 -0
- package/docs/README.de.md +3 -2
- package/docs/README.es.md +3 -2
- package/docs/README.fr.md +3 -2
- package/docs/README.ja.md +61 -27
- package/docs/README.ko.md +3 -2
- package/docs/README.zh-CN.md +3 -2
- package/docs/README.zh-TW.md +3 -2
- package/docs/ai-coding-agents.md +3 -3
- package/docs/articles/ai-coding-agents.md +1 -1
- package/docs/articles/request-scope.md +2 -1
- package/docs/choosing-di.md +36 -9
- package/docs/guide.md +35 -4
- package/docs/registration-policies.ja.md +261 -0
- package/docs/registration-policies.md +435 -0
- package/docs/type-safety.md +13 -4
- package/llms.txt +1 -0
- package/package.json +1 -1
- package/dist/chunk-J2NYR3SH.js +0 -6
package/dist/index.cjs
CHANGED
|
@@ -24,85 +24,311 @@ __export(index_exports, {
|
|
|
24
24
|
ContainerError: () => ContainerError,
|
|
25
25
|
Scope: () => Scope,
|
|
26
26
|
createContainer: () => createContainer,
|
|
27
|
-
|
|
27
|
+
createMetadataKey: () => createMetadataKey,
|
|
28
|
+
createScope: () => createScope,
|
|
29
|
+
entrypoint: () => entrypoint
|
|
28
30
|
});
|
|
29
31
|
module.exports = __toCommonJS(index_exports);
|
|
30
32
|
|
|
33
|
+
// src/entrypoint/index.ts
|
|
34
|
+
var ENTRYPOINT = /* @__PURE__ */ Symbol.for("katagami.entrypoint.v1");
|
|
35
|
+
function entrypoint(factory) {
|
|
36
|
+
return Object.assign((resolver) => factory(resolver), { [ENTRYPOINT]: true });
|
|
37
|
+
}
|
|
38
|
+
function isEntrypoint(factory) {
|
|
39
|
+
return typeof factory === "function" && ENTRYPOINT in factory && factory[ENTRYPOINT] === true;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// src/error/index.ts
|
|
43
|
+
var ContainerError = class extends Error {
|
|
44
|
+
/**
|
|
45
|
+
* @param message Error message
|
|
46
|
+
*/
|
|
47
|
+
constructor(message) {
|
|
48
|
+
super(message);
|
|
49
|
+
this.name = "ContainerError";
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
31
53
|
// src/internal.ts
|
|
32
|
-
var INTERNALS = /* @__PURE__ */ Symbol.for("katagami.internals.
|
|
54
|
+
var INTERNALS = /* @__PURE__ */ Symbol.for("katagami.internals.v4");
|
|
55
|
+
var CREATIONS = /* @__PURE__ */ Symbol.for("katagami.creations.v4");
|
|
56
|
+
var shared = globalThis;
|
|
57
|
+
if (!shared[CREATIONS]) {
|
|
58
|
+
Object.defineProperty(shared, CREATIONS, { value: /* @__PURE__ */ new WeakMap() });
|
|
59
|
+
}
|
|
60
|
+
var creations = shared[CREATIONS];
|
|
61
|
+
function settle(creation) {
|
|
62
|
+
return creation.then(
|
|
63
|
+
(value) => ({ failed: false, value }),
|
|
64
|
+
(error) => ({ error, failed: true })
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
var CONSTRUCTING = /* @__PURE__ */ Symbol.for("katagami.constructing.v4");
|
|
68
|
+
var running = globalThis;
|
|
69
|
+
if (!running[CONSTRUCTING]) {
|
|
70
|
+
Object.defineProperty(running, CONSTRUCTING, { value: [] });
|
|
71
|
+
}
|
|
72
|
+
var constructing = running[CONSTRUCTING];
|
|
73
|
+
|
|
74
|
+
// src/metadata/index.ts
|
|
75
|
+
var KEY = /* @__PURE__ */ Symbol.for("katagami.metadata-key.v1");
|
|
76
|
+
var ENTRY = /* @__PURE__ */ Symbol.for("katagami.metadata-entry.v1");
|
|
77
|
+
function createMetadataKey() {
|
|
78
|
+
return (name) => {
|
|
79
|
+
const key = (value) => Object.freeze({ key, value, [ENTRY]: true });
|
|
80
|
+
Object.defineProperties(key, { name: { value: name }, [KEY]: { value: true } });
|
|
81
|
+
return key;
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function readMetadata(entries, required) {
|
|
85
|
+
const values = /* @__PURE__ */ new Map();
|
|
86
|
+
const names = /* @__PURE__ */ new Set();
|
|
87
|
+
for (const entry of entries) {
|
|
88
|
+
if (entry?.[ENTRY] !== true || entry.key?.[KEY] !== true) {
|
|
89
|
+
throw new ContainerError("Invalid metadata entry.");
|
|
90
|
+
}
|
|
91
|
+
if (names.has(entry.key.name)) {
|
|
92
|
+
throw new ContainerError("Duplicate metadata key.");
|
|
93
|
+
}
|
|
94
|
+
names.add(entry.key.name);
|
|
95
|
+
values.set(entry.key, entry.value);
|
|
96
|
+
}
|
|
97
|
+
for (const key of required) {
|
|
98
|
+
if (!values.has(key)) {
|
|
99
|
+
throw new ContainerError("Required metadata is missing.");
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return Object.freeze({
|
|
103
|
+
pairs: Object.freeze([...values].map((pair) => Object.freeze(pair))),
|
|
104
|
+
reader: Object.freeze({
|
|
105
|
+
get: (key) => values.get(key),
|
|
106
|
+
has: (key) => values.has(key),
|
|
107
|
+
require: (key) => {
|
|
108
|
+
if (!values.has(key)) {
|
|
109
|
+
throw new ContainerError("Required metadata is missing.");
|
|
110
|
+
}
|
|
111
|
+
return values.get(key);
|
|
112
|
+
}
|
|
113
|
+
})
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
function validateMetadataKeys(keys) {
|
|
117
|
+
const names = /* @__PURE__ */ new Set();
|
|
118
|
+
for (const key of keys) {
|
|
119
|
+
if (key?.[KEY] !== true) {
|
|
120
|
+
throw new ContainerError("Invalid metadata key.");
|
|
121
|
+
}
|
|
122
|
+
if (names.has(key.name)) {
|
|
123
|
+
throw new ContainerError("Duplicate required metadata key.");
|
|
124
|
+
}
|
|
125
|
+
names.add(key.name);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// src/container/policy.ts
|
|
130
|
+
var POLICIES = /* @__PURE__ */ Symbol.for("katagami.policy-state.4.0.0.v1");
|
|
131
|
+
var shared2 = globalThis;
|
|
132
|
+
if (!shared2[POLICIES]) {
|
|
133
|
+
Object.defineProperty(shared2, POLICIES, { value: /* @__PURE__ */ new WeakMap() });
|
|
134
|
+
}
|
|
135
|
+
var policies = shared2[POLICIES];
|
|
136
|
+
function dataProperty(source, key) {
|
|
137
|
+
let current = source;
|
|
138
|
+
while (current !== null) {
|
|
139
|
+
const descriptor = Object.getOwnPropertyDescriptor(current, key);
|
|
140
|
+
if (descriptor) {
|
|
141
|
+
if (!("value" in descriptor)) {
|
|
142
|
+
throw new ContainerError("Policy settings must be data properties.");
|
|
143
|
+
}
|
|
144
|
+
return descriptor.value;
|
|
145
|
+
}
|
|
146
|
+
current = Object.getPrototypeOf(current);
|
|
147
|
+
}
|
|
148
|
+
return void 0;
|
|
149
|
+
}
|
|
150
|
+
function bindPolicy(definition) {
|
|
151
|
+
if (definition === void 0) {
|
|
152
|
+
return void 0;
|
|
153
|
+
}
|
|
154
|
+
if (definition === null || typeof definition !== "object") {
|
|
155
|
+
throw new ContainerError("Policy must be an object.");
|
|
156
|
+
}
|
|
157
|
+
const name = dataProperty(definition, "name");
|
|
158
|
+
const required = dataProperty(definition, "requiredMetadata");
|
|
159
|
+
const beforeReturn = dataProperty(definition, "beforeReturn");
|
|
160
|
+
if (name !== void 0 && typeof name !== "string") {
|
|
161
|
+
throw new ContainerError("Policy name must be a string.");
|
|
162
|
+
}
|
|
163
|
+
if (required !== void 0 && !Array.isArray(required)) {
|
|
164
|
+
throw new ContainerError("Required metadata must be an array.");
|
|
165
|
+
}
|
|
166
|
+
if (beforeReturn !== void 0 && typeof beforeReturn !== "function") {
|
|
167
|
+
throw new ContainerError("beforeReturn must be a function.");
|
|
168
|
+
}
|
|
169
|
+
const keys = [];
|
|
170
|
+
if (required !== void 0) {
|
|
171
|
+
for (let index = 0; index < required.length; index++) {
|
|
172
|
+
keys.push(dataProperty(required, index));
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
validateMetadataKeys(keys);
|
|
176
|
+
const previous = policies.get(definition);
|
|
177
|
+
if (previous) {
|
|
178
|
+
if (previous.name !== name || previous.beforeReturn !== beforeReturn || previous.requiredMetadata.length !== keys.length || previous.requiredMetadata.some((key, index) => key !== keys[index])) {
|
|
179
|
+
throw new ContainerError("Policy has changed since its first use.");
|
|
180
|
+
}
|
|
181
|
+
return previous;
|
|
182
|
+
}
|
|
183
|
+
const state = Object.freeze({
|
|
184
|
+
beforeReturn,
|
|
185
|
+
definition,
|
|
186
|
+
name,
|
|
187
|
+
origins: /* @__PURE__ */ new WeakMap(),
|
|
188
|
+
requiredMetadata: Object.freeze(keys)
|
|
189
|
+
});
|
|
190
|
+
policies.set(definition, state);
|
|
191
|
+
return state;
|
|
192
|
+
}
|
|
193
|
+
function observe(policy, registration, token, value) {
|
|
194
|
+
if (!policy || value === null || typeof value !== "object" && typeof value !== "function") {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
let origins = policy.origins.get(value);
|
|
198
|
+
if (!origins) {
|
|
199
|
+
origins = [];
|
|
200
|
+
policy.origins.set(value, origins);
|
|
201
|
+
}
|
|
202
|
+
if (origins.some((origin) => sameOrigin(origin, registration, token))) {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
origins.push(
|
|
206
|
+
Object.freeze({
|
|
207
|
+
description: Object.freeze({
|
|
208
|
+
entrypoint: registration.entrypoint,
|
|
209
|
+
lifetime: registration.lifetime,
|
|
210
|
+
metadata: registration.metadata,
|
|
211
|
+
token
|
|
212
|
+
}),
|
|
213
|
+
pairs: registration.metadataPairs
|
|
214
|
+
})
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
function sameOrigin(origin, registration, token) {
|
|
218
|
+
const { description } = origin;
|
|
219
|
+
return Object.is(description.token, token) && description.lifetime === registration.lifetime && description.entrypoint === registration.entrypoint && (description.metadata === registration.metadata || samePairs(origin.pairs, registration.metadataPairs));
|
|
220
|
+
}
|
|
221
|
+
function samePairs(left, right) {
|
|
222
|
+
return left !== void 0 && right !== void 0 && left.length === right.length && left.every(([key, value]) => right.some(([other, candidate]) => other === key && Object.is(candidate, value)));
|
|
223
|
+
}
|
|
224
|
+
function checkReturn(policy, value) {
|
|
225
|
+
if (!policy?.beforeReturn) {
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
let origins = [];
|
|
229
|
+
if (value !== null && (typeof value === "object" || typeof value === "function")) {
|
|
230
|
+
origins = policy.origins.get(value) ?? [];
|
|
231
|
+
}
|
|
232
|
+
const event = Object.freeze({
|
|
233
|
+
registrations: Object.freeze(origins.map((origin) => origin.description))
|
|
234
|
+
});
|
|
235
|
+
const result = policy.beforeReturn(event);
|
|
236
|
+
if (result !== void 0) {
|
|
237
|
+
void Promise.resolve(result).catch(() => void 0);
|
|
238
|
+
throw new ContainerError("beforeReturn must return undefined synchronously.");
|
|
239
|
+
}
|
|
240
|
+
}
|
|
33
241
|
|
|
34
242
|
// src/container/index.ts
|
|
35
|
-
function createContainer() {
|
|
36
|
-
return new Container();
|
|
243
|
+
function createContainer(options) {
|
|
244
|
+
return new Container(options);
|
|
37
245
|
}
|
|
38
246
|
var Container = class {
|
|
39
|
-
registrations;
|
|
40
|
-
singletonCache;
|
|
247
|
+
registrations = /* @__PURE__ */ new Map();
|
|
248
|
+
singletonCache = /* @__PURE__ */ new Map();
|
|
249
|
+
requiredMetadata;
|
|
250
|
+
policy;
|
|
41
251
|
disposed = false;
|
|
42
|
-
/**
|
|
43
|
-
* Internal state accessor for extension modules (scope, disposable).
|
|
44
|
-
*
|
|
45
|
-
* @internal
|
|
46
|
-
*/
|
|
47
252
|
[INTERNALS];
|
|
48
|
-
constructor() {
|
|
49
|
-
|
|
50
|
-
|
|
253
|
+
constructor(options) {
|
|
254
|
+
if (options && "requiredMetadata" in options) {
|
|
255
|
+
throw new ContainerError("requiredMetadata must be declared in policy.");
|
|
256
|
+
}
|
|
257
|
+
this.policy = bindPolicy(options?.policy);
|
|
258
|
+
this.requiredMetadata = this.policy?.requiredMetadata ?? [];
|
|
51
259
|
this[INTERNALS] = {
|
|
260
|
+
beforeResolve: [],
|
|
52
261
|
isDisposed: () => this.disposed,
|
|
262
|
+
kind: "container",
|
|
53
263
|
markDisposed: () => {
|
|
54
264
|
this.disposed = true;
|
|
55
265
|
},
|
|
56
266
|
ownCache: this.singletonCache,
|
|
267
|
+
policy: this.policy,
|
|
57
268
|
registrations: this.registrations,
|
|
58
269
|
singletonCache: this.singletonCache
|
|
59
270
|
};
|
|
60
271
|
}
|
|
61
|
-
registerSingleton(token, factory) {
|
|
62
|
-
return this.addRegistration(token, factory, "singleton");
|
|
272
|
+
registerSingleton(token, factory, ...options) {
|
|
273
|
+
return this.addRegistration(token, factory, "singleton", options[0]?.metadata ?? []);
|
|
274
|
+
}
|
|
275
|
+
registerTransient(token, factory, ...options) {
|
|
276
|
+
return this.addRegistration(token, factory, "transient", options[0]?.metadata ?? []);
|
|
63
277
|
}
|
|
64
|
-
|
|
65
|
-
return this.addRegistration(token, factory, "
|
|
278
|
+
registerScoped(token, factory, ...options) {
|
|
279
|
+
return this.addRegistration(token, factory, "scoped", options[0]?.metadata ?? []);
|
|
66
280
|
}
|
|
67
|
-
|
|
68
|
-
|
|
281
|
+
getMetadata(token) {
|
|
282
|
+
const entries = this.registrations.get(token);
|
|
283
|
+
if (!entries?.length) {
|
|
284
|
+
throw new ContainerError("Token is not registered.");
|
|
285
|
+
}
|
|
286
|
+
return entries[entries.length - 1].metadata;
|
|
69
287
|
}
|
|
70
288
|
use(source) {
|
|
289
|
+
if (this.disposed) {
|
|
290
|
+
throw new ContainerError("Cannot register on a disposed container.");
|
|
291
|
+
}
|
|
292
|
+
const sourcePolicy = source[INTERNALS].policy;
|
|
293
|
+
if (sourcePolicy && sourcePolicy !== this.policy) {
|
|
294
|
+
throw new ContainerError("Cannot compose containers with different policies.");
|
|
295
|
+
}
|
|
296
|
+
for (const registrations of source[INTERNALS].registrations.values()) {
|
|
297
|
+
for (const registration of registrations) {
|
|
298
|
+
for (const key of this.requiredMetadata) {
|
|
299
|
+
if (!registration.metadata.has(key)) {
|
|
300
|
+
throw new ContainerError("Required metadata is missing.");
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
71
305
|
for (const [token, registrations] of source[INTERNALS].registrations) {
|
|
72
306
|
this.registrations.set(token, [...registrations]);
|
|
73
307
|
}
|
|
74
308
|
return this;
|
|
75
309
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
310
|
+
addRegistration(token, factory, lifetime, entries) {
|
|
311
|
+
if (this.disposed) {
|
|
312
|
+
throw new ContainerError("Cannot register on a disposed container.");
|
|
313
|
+
}
|
|
314
|
+
const metadata = readMetadata(entries, this.requiredMetadata);
|
|
315
|
+
const registration = Object.freeze({
|
|
316
|
+
entrypoint: isEntrypoint(factory),
|
|
317
|
+
factory,
|
|
318
|
+
lifetime,
|
|
319
|
+
metadata: metadata.reader,
|
|
320
|
+
metadataPairs: metadata.pairs
|
|
321
|
+
});
|
|
85
322
|
const existing = this.registrations.get(token);
|
|
86
|
-
if (existing
|
|
87
|
-
existing.push(
|
|
323
|
+
if (existing) {
|
|
324
|
+
existing.push(registration);
|
|
88
325
|
} else {
|
|
89
|
-
this.registrations.set(token, [
|
|
326
|
+
this.registrations.set(token, [registration]);
|
|
90
327
|
}
|
|
91
328
|
return this;
|
|
92
329
|
}
|
|
93
330
|
};
|
|
94
331
|
|
|
95
|
-
// src/error/index.ts
|
|
96
|
-
var ContainerError = class extends Error {
|
|
97
|
-
/**
|
|
98
|
-
* @param message Error message
|
|
99
|
-
*/
|
|
100
|
-
constructor(message) {
|
|
101
|
-
super(message);
|
|
102
|
-
this.name = "ContainerError";
|
|
103
|
-
}
|
|
104
|
-
};
|
|
105
|
-
|
|
106
332
|
// src/resolver/index.ts
|
|
107
333
|
function tokenToString(token) {
|
|
108
334
|
if (typeof token === "function") {
|
|
@@ -128,171 +354,347 @@ function buildCircularPath(resolvingTokens, token) {
|
|
|
128
354
|
return path.join(" -> ");
|
|
129
355
|
}
|
|
130
356
|
|
|
357
|
+
// src/disposable/index.ts
|
|
358
|
+
function disposable(container) {
|
|
359
|
+
const asyncDispose = async () => {
|
|
360
|
+
const internals = container[INTERNALS];
|
|
361
|
+
if (internals.isDisposed()) {
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
internals.markDisposed();
|
|
365
|
+
const instances = [...internals.ownCache.values()].reverse();
|
|
366
|
+
const errors = [];
|
|
367
|
+
for (const instance of instances) {
|
|
368
|
+
let resolved = instance;
|
|
369
|
+
if (instance instanceof Promise) {
|
|
370
|
+
const creation = await (creations.get(instance) ?? settle(instance));
|
|
371
|
+
if (creation.failed) {
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
resolved = creation.value;
|
|
375
|
+
}
|
|
376
|
+
try {
|
|
377
|
+
if (resolved != null && typeof resolved === "object") {
|
|
378
|
+
if (Symbol.asyncDispose in resolved) {
|
|
379
|
+
await resolved[Symbol.asyncDispose]();
|
|
380
|
+
} else if (Symbol.dispose in resolved) {
|
|
381
|
+
resolved[Symbol.dispose]();
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
} catch (error) {
|
|
385
|
+
errors.push(error);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
internals.ownCache.clear();
|
|
389
|
+
if (errors.length > 0) {
|
|
390
|
+
throw new AggregateError(errors, "One or more errors occurred during disposal.");
|
|
391
|
+
}
|
|
392
|
+
};
|
|
393
|
+
Object.defineProperty(container, Symbol.asyncDispose, {
|
|
394
|
+
configurable: true,
|
|
395
|
+
value: asyncDispose
|
|
396
|
+
});
|
|
397
|
+
return container;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// src/scope/operations.ts
|
|
401
|
+
function operations(source) {
|
|
402
|
+
const internals = source[INTERNALS];
|
|
403
|
+
const scope = disposable(source);
|
|
404
|
+
let closed = false;
|
|
405
|
+
let disposal;
|
|
406
|
+
const pending = /* @__PURE__ */ new Set();
|
|
407
|
+
const assertOpen = () => {
|
|
408
|
+
if (closed || internals.isDisposed()) {
|
|
409
|
+
throw new ContainerError("Cannot invoke from a disposed scope.");
|
|
410
|
+
}
|
|
411
|
+
};
|
|
412
|
+
const assertEntrypoint = (token) => {
|
|
413
|
+
const registrations = internals.registrations.get(token);
|
|
414
|
+
const registration = registrations?.[registrations.length - 1];
|
|
415
|
+
if (!registration) {
|
|
416
|
+
throw new ContainerError("Token is not registered.");
|
|
417
|
+
}
|
|
418
|
+
if (!registration.entrypoint) {
|
|
419
|
+
throw new ContainerError("Token is not an entrypoint.");
|
|
420
|
+
}
|
|
421
|
+
};
|
|
422
|
+
const execute = (token, args) => {
|
|
423
|
+
try {
|
|
424
|
+
assertOpen();
|
|
425
|
+
} catch (error) {
|
|
426
|
+
return Promise.reject(error);
|
|
427
|
+
}
|
|
428
|
+
const body = Promise.resolve().then(async () => {
|
|
429
|
+
assertEntrypoint(token);
|
|
430
|
+
const callable = await internals.resolveInternal(token);
|
|
431
|
+
if (typeof callable !== "function") {
|
|
432
|
+
throw new ContainerError("Entrypoint factory must return a callable.");
|
|
433
|
+
}
|
|
434
|
+
const result = await callable(...args);
|
|
435
|
+
checkReturn(internals.policy, result);
|
|
436
|
+
return result;
|
|
437
|
+
});
|
|
438
|
+
const operation = body.then((result) => result);
|
|
439
|
+
const running2 = body.then(
|
|
440
|
+
() => {
|
|
441
|
+
pending.delete(running2);
|
|
442
|
+
},
|
|
443
|
+
() => {
|
|
444
|
+
pending.delete(running2);
|
|
445
|
+
}
|
|
446
|
+
);
|
|
447
|
+
pending.add(running2);
|
|
448
|
+
return operation;
|
|
449
|
+
};
|
|
450
|
+
return Object.freeze({
|
|
451
|
+
get: (token) => {
|
|
452
|
+
assertOpen();
|
|
453
|
+
assertEntrypoint(token);
|
|
454
|
+
return (...args) => execute(token, args);
|
|
455
|
+
},
|
|
456
|
+
[Symbol.asyncDispose]: () => {
|
|
457
|
+
if (!disposal) {
|
|
458
|
+
closed = true;
|
|
459
|
+
disposal = (async () => {
|
|
460
|
+
await Promise.all(pending);
|
|
461
|
+
await scope[Symbol.asyncDispose]();
|
|
462
|
+
})();
|
|
463
|
+
}
|
|
464
|
+
return disposal;
|
|
465
|
+
}
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
|
|
131
469
|
// src/scope/index.ts
|
|
132
|
-
|
|
470
|
+
var EXPOSED = Object.freeze({ exposed: true, path: Object.freeze([]), singleton: false });
|
|
471
|
+
function createScope(source, options) {
|
|
472
|
+
const access = options?.access;
|
|
473
|
+
if (access !== void 0 && access !== "operations") {
|
|
474
|
+
throw new ContainerError("Unknown scope access. Omit access or use 'operations'.");
|
|
475
|
+
}
|
|
133
476
|
const internals = source[INTERNALS];
|
|
134
477
|
if (internals.isDisposed()) {
|
|
135
478
|
throw new ContainerError("Cannot create a scope from a disposed container.");
|
|
136
479
|
}
|
|
137
|
-
|
|
480
|
+
const hooks = [...internals.beforeResolve];
|
|
481
|
+
if (options?.beforeResolve) {
|
|
482
|
+
hooks.push(options.beforeResolve);
|
|
483
|
+
}
|
|
484
|
+
const scope = new Scope(internals.registrations, internals.singletonCache, hooks, internals.policy);
|
|
485
|
+
if (access === "operations") {
|
|
486
|
+
return operations(scope);
|
|
487
|
+
}
|
|
488
|
+
return scope;
|
|
138
489
|
}
|
|
139
490
|
var Scope = class {
|
|
140
491
|
registrations;
|
|
141
492
|
singletonCache;
|
|
142
493
|
scopedCache;
|
|
143
|
-
|
|
144
|
-
|
|
494
|
+
beforeResolve;
|
|
495
|
+
policy;
|
|
145
496
|
disposed = false;
|
|
146
|
-
/**
|
|
147
|
-
* Internal state accessor for extension modules (scope, disposable).
|
|
148
|
-
*
|
|
149
|
-
* @internal
|
|
150
|
-
*/
|
|
151
497
|
[INTERNALS];
|
|
152
|
-
constructor(registrations, singletonCache) {
|
|
498
|
+
constructor(registrations, singletonCache, beforeResolve = [], policy) {
|
|
153
499
|
this.registrations = registrations;
|
|
154
500
|
this.singletonCache = singletonCache;
|
|
155
501
|
this.scopedCache = /* @__PURE__ */ new Map();
|
|
156
|
-
this.
|
|
502
|
+
this.beforeResolve = beforeResolve;
|
|
503
|
+
this.policy = policy;
|
|
157
504
|
this[INTERNALS] = {
|
|
505
|
+
beforeResolve: this.beforeResolve,
|
|
158
506
|
isDisposed: () => this.disposed,
|
|
507
|
+
kind: "scope",
|
|
159
508
|
markDisposed: () => {
|
|
160
509
|
this.disposed = true;
|
|
161
510
|
},
|
|
162
511
|
ownCache: this.scopedCache,
|
|
512
|
+
policy: this.policy,
|
|
163
513
|
registrations: this.registrations,
|
|
514
|
+
resolveInternal: (token) => this.resolveToken(token, true),
|
|
164
515
|
singletonCache: this.singletonCache
|
|
165
516
|
};
|
|
166
517
|
}
|
|
167
518
|
resolve(token) {
|
|
168
|
-
return this.resolveToken(token, true);
|
|
519
|
+
return this.resolveToken(token, true, this.entry());
|
|
169
520
|
}
|
|
170
521
|
tryResolve(token) {
|
|
171
|
-
return this.resolveToken(token, false);
|
|
522
|
+
return this.resolveToken(token, false, this.entry());
|
|
172
523
|
}
|
|
173
524
|
resolveAll(token) {
|
|
174
|
-
return this.resolveAllTokens(token, true);
|
|
525
|
+
return this.resolveAllTokens(token, true, this.entry());
|
|
175
526
|
}
|
|
176
527
|
tryResolveAll(token) {
|
|
177
|
-
return this.resolveAllTokens(token, false);
|
|
528
|
+
return this.resolveAllTokens(token, false, this.entry());
|
|
178
529
|
}
|
|
179
|
-
/**
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
530
|
+
/** The context of a call through a public method: the running factory's, or the outside of this scope. */
|
|
531
|
+
entry() {
|
|
532
|
+
const running2 = constructing.at(-1);
|
|
533
|
+
if (running2?.scope === this) {
|
|
534
|
+
return running2.context;
|
|
535
|
+
}
|
|
536
|
+
return EXPOSED;
|
|
537
|
+
}
|
|
538
|
+
describe(token, registration) {
|
|
539
|
+
return Object.freeze({
|
|
540
|
+
entrypoint: registration.entrypoint,
|
|
541
|
+
lifetime: registration.lifetime,
|
|
542
|
+
metadata: registration.metadata,
|
|
543
|
+
token
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
check(token, registration, context) {
|
|
547
|
+
if (this.beforeResolve.length === 0) {
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
const event = Object.freeze({
|
|
551
|
+
...this.describe(token, registration),
|
|
552
|
+
path: Object.freeze([...context.path, token]),
|
|
553
|
+
requester: context.requester
|
|
554
|
+
});
|
|
555
|
+
for (const hook of this.beforeResolve) {
|
|
556
|
+
const result = hook(event);
|
|
557
|
+
if (result !== void 0) {
|
|
558
|
+
void Promise.resolve(result).catch(() => void 0);
|
|
559
|
+
throw new ContainerError("beforeResolve must return undefined synchronously.");
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
lookup(token, required) {
|
|
188
564
|
if (this.disposed) {
|
|
189
565
|
throw new ContainerError("Cannot resolve from a disposed scope.");
|
|
190
566
|
}
|
|
191
567
|
const registrations = this.registrations.get(token);
|
|
192
|
-
if (registrations
|
|
568
|
+
if (!registrations?.length) {
|
|
193
569
|
if (required) {
|
|
194
570
|
throw new ContainerError(`Token "${tokenToString(token)}" is not registered.`);
|
|
195
571
|
}
|
|
196
572
|
return void 0;
|
|
197
573
|
}
|
|
574
|
+
return registrations;
|
|
575
|
+
}
|
|
576
|
+
/** Create a value that may leave this scope; a synchronous value passes the policy's return check here. */
|
|
577
|
+
create(token, registration, context) {
|
|
578
|
+
const instance = this.instantiate(token, registration, context);
|
|
579
|
+
if (context.exposed && !(instance instanceof Promise)) {
|
|
580
|
+
checkReturn(this.policy, instance);
|
|
581
|
+
}
|
|
582
|
+
return instance;
|
|
583
|
+
}
|
|
584
|
+
/**
|
|
585
|
+
* Shape a created value for the caller.
|
|
586
|
+
*
|
|
587
|
+
* The return check runs on every resolution, so under a policy with a return check, an asynchronous
|
|
588
|
+
* creation leaving the scope is handed out as a per-call Promise that checks the resolved value. No
|
|
589
|
+
* listener is attached to a Promise already handed out. The per-call Promise is built only after every
|
|
590
|
+
* synchronous failure is ruled out, so no Promise is left without a receiver.
|
|
591
|
+
*/
|
|
592
|
+
handOut(value, context) {
|
|
593
|
+
if (!context.exposed || !this.policy?.beforeReturn || !(value instanceof Promise)) {
|
|
594
|
+
return value;
|
|
595
|
+
}
|
|
596
|
+
return value.then((resolved) => {
|
|
597
|
+
checkReturn(this.policy, resolved);
|
|
598
|
+
return resolved;
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
resolveToken(token, required, context = { path: [], singleton: false }) {
|
|
602
|
+
const registrations = this.lookup(token, required);
|
|
603
|
+
if (!registrations) {
|
|
604
|
+
return void 0;
|
|
605
|
+
}
|
|
198
606
|
const registration = registrations[registrations.length - 1];
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
607
|
+
this.check(token, registration, context);
|
|
608
|
+
return this.handOut(this.create(token, registration, context), context);
|
|
609
|
+
}
|
|
610
|
+
resolveAllTokens(token, required, context = { path: [], singleton: false }) {
|
|
611
|
+
const registrations = this.lookup(token, required);
|
|
612
|
+
if (!registrations) {
|
|
613
|
+
return void 0;
|
|
614
|
+
}
|
|
615
|
+
for (const registration of registrations) {
|
|
616
|
+
this.check(token, registration, context);
|
|
202
617
|
}
|
|
203
|
-
|
|
618
|
+
const created = registrations.map((registration) => this.create(token, registration, context));
|
|
619
|
+
return created.map((value) => this.handOut(value, context));
|
|
620
|
+
}
|
|
621
|
+
instantiate(token, registration, context) {
|
|
622
|
+
if (registration.lifetime === "scoped" && context.singleton) {
|
|
204
623
|
throw new ContainerError(
|
|
205
624
|
`Captive dependency detected: scoped token "${tokenToString(token)}" cannot be resolved inside a singleton factory. Scoped instances must not be captured by singletons.`
|
|
206
625
|
);
|
|
207
626
|
}
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
|
|
627
|
+
const construction = context.construction ?? [];
|
|
628
|
+
let waiting = construction.length;
|
|
629
|
+
while (waiting > 0 && construction[waiting - 1]?.active) {
|
|
630
|
+
waiting--;
|
|
211
631
|
}
|
|
212
|
-
if (
|
|
213
|
-
|
|
632
|
+
if (construction.slice(waiting).some((frame2) => frame2.token === token)) {
|
|
633
|
+
const chain = new Set(construction.slice(waiting).map((frame2) => frame2.token));
|
|
634
|
+
throw new ContainerError(`Circular dependency detected: ${buildCircularPath(chain, token)}`);
|
|
214
635
|
}
|
|
215
|
-
this.
|
|
636
|
+
let cache = this.scopedCache;
|
|
216
637
|
if (registration.lifetime === "singleton") {
|
|
217
|
-
this.
|
|
218
|
-
}
|
|
219
|
-
try {
|
|
220
|
-
const instance = registration.factory(
|
|
221
|
-
this
|
|
222
|
-
);
|
|
223
|
-
if (registration.lifetime === "singleton") {
|
|
224
|
-
this.singletonCache.set(registration, instance);
|
|
225
|
-
} else if (registration.lifetime === "scoped") {
|
|
226
|
-
this.scopedCache.set(registration, instance);
|
|
227
|
-
}
|
|
228
|
-
return instance;
|
|
229
|
-
} finally {
|
|
230
|
-
if (registration.lifetime === "singleton") {
|
|
231
|
-
this.singletonDepth--;
|
|
232
|
-
}
|
|
233
|
-
this.resolvingTokens.delete(token);
|
|
638
|
+
cache = this.singletonCache;
|
|
234
639
|
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
-
*/
|
|
244
|
-
resolveAllTokens(token, required) {
|
|
245
|
-
if (this.disposed) {
|
|
246
|
-
throw new ContainerError("Cannot resolve from a disposed scope.");
|
|
247
|
-
}
|
|
248
|
-
const registrations = this.registrations.get(token);
|
|
249
|
-
if (registrations === void 0 || registrations.length === 0) {
|
|
250
|
-
if (required) {
|
|
251
|
-
throw new ContainerError(`Token "${tokenToString(token)}" is not registered.`);
|
|
640
|
+
if (registration.lifetime !== "transient" && cache.get(registration) !== void 0) {
|
|
641
|
+
const cached = cache.get(registration);
|
|
642
|
+
if (!(cached instanceof Promise)) {
|
|
643
|
+
observe(this.policy, registration, token, cached);
|
|
252
644
|
}
|
|
253
|
-
return
|
|
645
|
+
return cached;
|
|
254
646
|
}
|
|
255
|
-
|
|
256
|
-
|
|
647
|
+
const shares = (entry) => entry.scope === this || registration.lifetime === "singleton" && entry.registration === registration && entry.singletons === this.singletonCache;
|
|
648
|
+
if (constructing.some((entry) => entry.token === token && shares(entry))) {
|
|
649
|
+
const chain = new Set(constructing.filter(shares).map((entry) => entry.token));
|
|
650
|
+
throw new ContainerError(`Circular dependency detected: ${buildCircularPath(chain, token)}`);
|
|
257
651
|
}
|
|
258
|
-
|
|
652
|
+
const frame = { active: true, token };
|
|
653
|
+
const childContext = {
|
|
654
|
+
construction: [...construction, frame],
|
|
655
|
+
path: [...context.path, token],
|
|
656
|
+
requester: this.describe(token, registration),
|
|
657
|
+
singleton: context.singleton || registration.lifetime === "singleton"
|
|
658
|
+
};
|
|
659
|
+
const resolver = Object.freeze({
|
|
660
|
+
resolve: (target) => this.resolveToken(target, true, childContext),
|
|
661
|
+
resolveAll: (target) => this.resolveAllTokens(target, true, childContext),
|
|
662
|
+
tryResolve: (target) => this.resolveToken(target, false, childContext),
|
|
663
|
+
tryResolveAll: (target) => this.resolveAllTokens(target, false, childContext)
|
|
664
|
+
});
|
|
665
|
+
let pending = false;
|
|
666
|
+
constructing.push({ context: childContext, registration, scope: this, singletons: this.singletonCache, token });
|
|
259
667
|
try {
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
)
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
if (scopedCached !== void 0) {
|
|
273
|
-
return scopedCached;
|
|
274
|
-
}
|
|
275
|
-
if (reg.lifetime === "singleton") {
|
|
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--;
|
|
668
|
+
const instance = registration.factory(resolver);
|
|
669
|
+
if (instance instanceof Promise) {
|
|
670
|
+
pending = true;
|
|
671
|
+
const tracked = instance.then(
|
|
672
|
+
(value) => {
|
|
673
|
+
observe(this.policy, registration, token, value);
|
|
674
|
+
frame.active = false;
|
|
675
|
+
return value;
|
|
676
|
+
},
|
|
677
|
+
(error) => {
|
|
678
|
+
frame.active = false;
|
|
679
|
+
throw error;
|
|
291
680
|
}
|
|
681
|
+
);
|
|
682
|
+
if (registration.lifetime !== "transient") {
|
|
683
|
+
creations.set(tracked, settle(instance));
|
|
684
|
+
cache.set(registration, tracked);
|
|
292
685
|
}
|
|
293
|
-
|
|
686
|
+
return tracked;
|
|
687
|
+
}
|
|
688
|
+
observe(this.policy, registration, token, instance);
|
|
689
|
+
if (registration.lifetime !== "transient") {
|
|
690
|
+
cache.set(registration, instance);
|
|
691
|
+
}
|
|
692
|
+
return instance;
|
|
294
693
|
} finally {
|
|
295
|
-
|
|
694
|
+
constructing.pop();
|
|
695
|
+
if (!pending) {
|
|
696
|
+
frame.active = false;
|
|
697
|
+
}
|
|
296
698
|
}
|
|
297
699
|
}
|
|
298
700
|
};
|