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.cjs CHANGED
@@ -24,85 +24,311 @@ __export(index_exports, {
24
24
  ContainerError: () => ContainerError,
25
25
  Scope: () => Scope,
26
26
  createContainer: () => createContainer,
27
- createScope: () => createScope
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.v3");
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
- this.registrations = /* @__PURE__ */ new Map();
50
- this.singletonCache = /* @__PURE__ */ new Map();
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
- registerTransient(token, factory) {
65
- return this.addRegistration(token, factory, "transient");
278
+ registerScoped(token, factory, ...options) {
279
+ return this.addRegistration(token, factory, "scoped", options[0]?.metadata ?? []);
66
280
  }
67
- registerScoped(token, factory) {
68
- return this.addRegistration(token, factory, "scoped");
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
- * Add a registration entry. Accumulates registrations for the same token.
78
- *
79
- * @param token Token
80
- * @param factory Factory function
81
- * @param lifetime Lifetime of the registration
82
- * @returns The container for method chaining
83
- */
84
- addRegistration(token, factory, lifetime) {
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 !== void 0) {
87
- existing.push({ factory, lifetime });
323
+ if (existing) {
324
+ existing.push(registration);
88
325
  } else {
89
- this.registrations.set(token, [{ factory, lifetime }]);
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
- function createScope(source) {
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
- return new Scope(internals.registrations, internals.singletonCache);
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
- resolvingTokens;
144
- singletonDepth = 0;
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.resolvingTokens = /* @__PURE__ */ new Set();
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
- * Internal resolution logic shared by resolve and tryResolve.
181
- * Resolves the last registered factory for the token.
182
- *
183
- * @param token Token to resolve
184
- * @param required If true, throws when the token is not registered. If false, returns undefined.
185
- * @returns The resolved instance, or undefined if not registered and required is false
186
- */
187
- resolveToken(token, required) {
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 === void 0 || registrations.length === 0) {
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
- const singletonCached = this.singletonCache.get(registration);
200
- if (singletonCached !== void 0) {
201
- return singletonCached;
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
- if (registration.lifetime === "scoped" && this.singletonDepth > 0) {
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 scopedCached = this.scopedCache.get(registration);
209
- if (scopedCached !== void 0) {
210
- return scopedCached;
627
+ const construction = context.construction ?? [];
628
+ let waiting = construction.length;
629
+ while (waiting > 0 && construction[waiting - 1]?.active) {
630
+ waiting--;
211
631
  }
212
- if (this.resolvingTokens.has(token)) {
213
- throw new ContainerError(`Circular dependency detected: ${buildCircularPath(this.resolvingTokens, token)}`);
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.resolvingTokens.add(token);
636
+ let cache = this.scopedCache;
216
637
  if (registration.lifetime === "singleton") {
217
- this.singletonDepth++;
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
- * Internal resolution logic shared by resolveAll and tryResolveAll.
238
- * Resolves all registered factories for the token.
239
- *
240
- * @param token Token to resolve
241
- * @param required If true, throws when the token is not registered. If false, returns undefined.
242
- * @returns An array of resolved instances, or undefined if not registered and required is false
243
- */
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 void 0;
645
+ return cached;
254
646
  }
255
- if (this.resolvingTokens.has(token)) {
256
- throw new ContainerError(`Circular dependency detected: ${buildCircularPath(this.resolvingTokens, token)}`);
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
- this.resolvingTokens.add(token);
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
- return registrations.map((registration) => {
261
- const reg = registration;
262
- const singletonCached = this.singletonCache.get(registration);
263
- if (singletonCached !== void 0) {
264
- return singletonCached;
265
- }
266
- if (reg.lifetime === "scoped" && this.singletonDepth > 0) {
267
- throw new ContainerError(
268
- `Captive dependency detected: scoped token "${tokenToString(token)}" cannot be resolved inside a singleton factory. Scoped instances must not be captured by singletons.`
269
- );
270
- }
271
- const scopedCached = this.scopedCache.get(registration);
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
- this.resolvingTokens.delete(token);
694
+ constructing.pop();
695
+ if (!pending) {
696
+ frame.active = false;
697
+ }
296
698
  }
297
699
  }
298
700
  };