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