katagami 3.0.3 → 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.
Files changed (44) hide show
  1. package/README.md +9 -7
  2. package/dist/chunk-R66WOZUM.js +71 -0
  3. package/dist/container/index.d.cts +142 -35
  4. package/dist/container/index.d.ts +142 -35
  5. package/dist/container/policy.d.cts +36 -0
  6. package/dist/container/policy.d.ts +36 -0
  7. package/dist/disposable/index.cjs +27 -5
  8. package/dist/disposable/index.d.cts +17 -7
  9. package/dist/disposable/index.d.ts +17 -7
  10. package/dist/disposable/index.js +2 -41
  11. package/dist/entrypoint/index.d.cts +20 -0
  12. package/dist/entrypoint/index.d.ts +20 -0
  13. package/dist/index.cjs +551 -149
  14. package/dist/index.d.cts +8 -0
  15. package/dist/index.d.ts +8 -0
  16. package/dist/index.js +495 -150
  17. package/dist/internal.d.cts +65 -0
  18. package/dist/internal.d.ts +65 -0
  19. package/dist/metadata/index.d.cts +67 -0
  20. package/dist/metadata/index.d.ts +67 -0
  21. package/dist/resolver/index.d.cts +5 -0
  22. package/dist/resolver/index.d.ts +5 -0
  23. package/dist/scope/index.d.cts +49 -24
  24. package/dist/scope/index.d.ts +49 -24
  25. package/dist/scope/operations.d.cts +22 -0
  26. package/dist/scope/operations.d.ts +22 -0
  27. package/docs/README.de.md +3 -2
  28. package/docs/README.es.md +3 -2
  29. package/docs/README.fr.md +3 -2
  30. package/docs/README.ja.md +7 -6
  31. package/docs/README.ko.md +3 -2
  32. package/docs/README.zh-CN.md +3 -2
  33. package/docs/README.zh-TW.md +3 -2
  34. package/docs/ai-coding-agents.md +3 -3
  35. package/docs/articles/ai-coding-agents.md +1 -1
  36. package/docs/articles/request-scope.md +2 -1
  37. package/docs/choosing-di.md +7 -6
  38. package/docs/guide.md +35 -4
  39. package/docs/registration-policies.ja.md +261 -0
  40. package/docs/registration-policies.md +435 -0
  41. package/docs/type-safety.md +13 -4
  42. package/llms.txt +1 -0
  43. package/package.json +1 -1
  44. package/dist/chunk-J2NYR3SH.js +0 -6
package/dist/index.js CHANGED
@@ -1,79 +1,289 @@
1
1
  import {
2
- INTERNALS
3
- } from "./chunk-J2NYR3SH.js";
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
- this.registrations = /* @__PURE__ */ new Map();
21
- this.singletonCache = /* @__PURE__ */ new Map();
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
- registerTransient(token, factory) {
36
- return this.addRegistration(token, factory, "transient");
233
+ registerScoped(token, factory, ...options) {
234
+ return this.addRegistration(token, factory, "scoped", options[0]?.metadata ?? []);
37
235
  }
38
- registerScoped(token, factory) {
39
- return this.addRegistration(token, factory, "scoped");
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
- * 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) {
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 !== void 0) {
58
- existing.push({ factory, lifetime });
278
+ if (existing) {
279
+ existing.push(registration);
59
280
  } else {
60
- this.registrations.set(token, [{ factory, lifetime }]);
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
- function createScope(source) {
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
- return new Scope(internals.registrations, internals.singletonCache);
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
- resolvingTokens;
115
- singletonDepth = 0;
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.resolvingTokens = /* @__PURE__ */ new Set();
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
- * 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
- */
158
- resolveToken(token, required) {
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 === void 0 || registrations.length === 0) {
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
- const singletonCached = this.singletonCache.get(registration);
171
- if (singletonCached !== void 0) {
172
- return singletonCached;
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
- if (registration.lifetime === "scoped" && this.singletonDepth > 0) {
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 scopedCached = this.scopedCache.get(registration);
180
- if (scopedCached !== void 0) {
181
- return scopedCached;
539
+ const construction = context.construction ?? [];
540
+ let waiting = construction.length;
541
+ while (waiting > 0 && construction[waiting - 1]?.active) {
542
+ waiting--;
182
543
  }
183
- if (this.resolvingTokens.has(token)) {
184
- throw new ContainerError(`Circular dependency detected: ${buildCircularPath(this.resolvingTokens, token)}`);
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.resolvingTokens.add(token);
548
+ let cache = this.scopedCache;
187
549
  if (registration.lifetime === "singleton") {
188
- this.singletonDepth++;
550
+ cache = this.singletonCache;
189
551
  }
190
- try {
191
- const instance = registration.factory(
192
- this
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
- this.resolvingTokens.delete(token);
557
+ return cached;
205
558
  }
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
- */
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
- if (this.resolvingTokens.has(token)) {
227
- throw new ContainerError(`Circular dependency detected: ${buildCircularPath(this.resolvingTokens, token)}`);
228
- }
229
- this.resolvingTokens.add(token);
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
- return registrations.map((registration) => {
232
- const reg = registration;
233
- const singletonCached = this.singletonCache.get(registration);
234
- if (singletonCached !== void 0) {
235
- return singletonCached;
236
- }
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;
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
- this.resolvingTokens.delete(token);
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
- createScope
618
+ createMetadataKey,
619
+ createScope,
620
+ entrypoint
276
621
  };