@codefast/di 0.5.0-canary.7 → 0.5.0-canary.8

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 (100) hide show
  1. package/CHANGELOG.md +68 -0
  2. package/README.md +3 -1
  3. package/dist/binding.d.ts +47 -5
  4. package/dist/binding.d.ts.map +1 -1
  5. package/dist/binding.js +44 -0
  6. package/dist/binding.js.map +1 -1
  7. package/dist/constructor-type.d.ts +4 -5
  8. package/dist/constructor-type.d.ts.map +1 -1
  9. package/dist/container/binding-builders.d.ts +32 -11
  10. package/dist/container/binding-builders.d.ts.map +1 -1
  11. package/dist/container/binding-builders.js +140 -191
  12. package/dist/container/binding-builders.js.map +1 -1
  13. package/dist/container/container.d.ts.map +1 -1
  14. package/dist/container/container.js +75 -92
  15. package/dist/container/container.js.map +1 -1
  16. package/dist/decorators/inject.d.ts +2 -4
  17. package/dist/decorators/inject.d.ts.map +1 -1
  18. package/dist/decorators/inject.js.map +1 -1
  19. package/dist/errors.d.ts +14 -0
  20. package/dist/errors.d.ts.map +1 -1
  21. package/dist/errors.js +17 -0
  22. package/dist/errors.js.map +1 -1
  23. package/dist/index.d.ts +1 -1
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +1 -1
  26. package/dist/index.js.map +1 -1
  27. package/dist/introspection/inspector.js +1 -1
  28. package/dist/introspection/inspector.js.map +1 -1
  29. package/dist/metadata/metadata-keys.d.ts +3 -6
  30. package/dist/metadata/metadata-keys.d.ts.map +1 -1
  31. package/dist/metadata/metadata-keys.js +3 -6
  32. package/dist/metadata/metadata-keys.js.map +1 -1
  33. package/dist/registry.d.ts +14 -2
  34. package/dist/registry.d.ts.map +1 -1
  35. package/dist/registry.js +35 -45
  36. package/dist/registry.js.map +1 -1
  37. package/dist/resolution/activation-need.d.ts +25 -0
  38. package/dist/resolution/activation-need.d.ts.map +1 -0
  39. package/dist/resolution/activation-need.js +64 -0
  40. package/dist/resolution/activation-need.js.map +1 -0
  41. package/dist/resolution/binding-lookup-cache.d.ts +41 -0
  42. package/dist/resolution/binding-lookup-cache.d.ts.map +1 -0
  43. package/dist/resolution/binding-lookup-cache.js +102 -0
  44. package/dist/resolution/binding-lookup-cache.js.map +1 -0
  45. package/dist/resolution/binding-select.js +1 -4
  46. package/dist/resolution/binding-select.js.map +1 -1
  47. package/dist/resolution/class-introspector.d.ts +27 -0
  48. package/dist/resolution/class-introspector.d.ts.map +1 -0
  49. package/dist/resolution/class-introspector.js +60 -0
  50. package/dist/resolution/class-introspector.js.map +1 -0
  51. package/dist/resolution/diagnostics.d.ts +41 -0
  52. package/dist/resolution/diagnostics.d.ts.map +1 -0
  53. package/dist/resolution/diagnostics.js +18 -0
  54. package/dist/resolution/diagnostics.js.map +1 -0
  55. package/dist/resolution/environment.d.ts +22 -1
  56. package/dist/resolution/environment.d.ts.map +1 -1
  57. package/dist/resolution/environment.js +27 -1
  58. package/dist/resolution/environment.js.map +1 -1
  59. package/dist/resolution/instantiation-plan.d.ts +15 -15
  60. package/dist/resolution/instantiation-plan.d.ts.map +1 -1
  61. package/dist/resolution/instantiation-plan.js +68 -48
  62. package/dist/resolution/instantiation-plan.js.map +1 -1
  63. package/dist/resolution/lifecycle.d.ts +2 -0
  64. package/dist/resolution/lifecycle.d.ts.map +1 -1
  65. package/dist/resolution/lifecycle.js +16 -11
  66. package/dist/resolution/lifecycle.js.map +1 -1
  67. package/dist/resolution/resolution-path.d.ts +4 -13
  68. package/dist/resolution/resolution-path.d.ts.map +1 -1
  69. package/dist/resolution/resolution-path.js +3 -16
  70. package/dist/resolution/resolution-path.js.map +1 -1
  71. package/dist/resolution/resolver.d.ts +7 -2
  72. package/dist/resolution/resolver.d.ts.map +1 -1
  73. package/dist/resolution/resolver.js +116 -328
  74. package/dist/resolution/resolver.js.map +1 -1
  75. package/dist/resolution/scope.d.ts +7 -15
  76. package/dist/resolution/scope.d.ts.map +1 -1
  77. package/dist/resolution/scope.js +48 -44
  78. package/dist/resolution/scope.js.map +1 -1
  79. package/package.json +7 -97
  80. package/src/binding.ts +104 -5
  81. package/src/constructor-type.ts +4 -5
  82. package/src/container/binding-builders.ts +180 -283
  83. package/src/container/container.ts +90 -103
  84. package/src/decorators/inject.ts +3 -5
  85. package/src/errors.ts +21 -0
  86. package/src/index.ts +1 -0
  87. package/src/introspection/inspector.ts +1 -1
  88. package/src/metadata/metadata-keys.ts +3 -6
  89. package/src/registry.ts +38 -59
  90. package/src/resolution/activation-need.ts +81 -0
  91. package/src/resolution/binding-lookup-cache.ts +132 -0
  92. package/src/resolution/binding-select.ts +1 -4
  93. package/src/resolution/class-introspector.ts +74 -0
  94. package/src/resolution/diagnostics.ts +43 -0
  95. package/src/resolution/environment.ts +31 -1
  96. package/src/resolution/instantiation-plan.ts +113 -61
  97. package/src/resolution/lifecycle.ts +16 -11
  98. package/src/resolution/resolution-path.ts +5 -20
  99. package/src/resolution/resolver.ts +142 -371
  100. package/src/resolution/scope.ts +50 -49
@@ -1,14 +1,14 @@
1
+ import { NO_INSTANCE } from "#/binding";
1
2
  import { AsyncActivationError, AsyncResolutionError, CircularDependencyError, InternalError, MissingMetadataError, MissingScopeContextError, NoMatchingBindingError, TokenNotBoundError, } from "#/errors";
3
+ import { ActivationNeedCache } from "#/resolution/activation-need";
4
+ import { BindingLookupCache } from "#/resolution/binding-lookup-cache";
2
5
  import { selectAllBindings, selectBinding } from "#/resolution/binding-select";
3
- import { buildResolutionFrame, DefaultResolutionContext, runWithContainer } from "#/resolution/environment";
6
+ import { ClassIntrospector } from "#/resolution/class-introspector";
7
+ import { buildResolutionFrame, DefaultResolutionContext } from "#/resolution/environment";
4
8
  import { InstantiationPlanCompiler, PLAN_RETRY } from "#/resolution/instantiation-plan";
5
9
  import { enterResolutionPath, exitResolutionPath } from "#/resolution/resolution-path";
6
10
  import { injectionSlotToResolveOptions } from "#/resolution/resolve-options";
7
- import { SINGLETON_MISS } from "#/resolution/scope";
8
11
  import { tokenName } from "#/token";
9
- // Fast-lane alias folding gives up past this many hops and defers to the resolve()
10
- // loop, whose Set-based traversal detects genuine cycles exactly (no arbitrary cap).
11
- const ALIAS_HOP_LIMIT = 32;
12
12
  const EMPTY_STRING_LIST = [];
13
13
  const EMPTY_FRAME_LIST = [];
14
14
  const ROOT_CONSTRAINT_CONTEXT = {
@@ -23,51 +23,10 @@ const ROOT_CONSTRAINT_CONTEXT = {
23
23
  */
24
24
  export class DependencyResolver {
25
25
  #syncResolutionContextPool = [];
26
- // Cycle detection for the sync transient-dynamic lane lives on `binding.inFlight` — see the
27
- // field's doc comment in binding.ts. It is an O(1) field read with no hashing, no path scan and
28
- // no side table to allocate or grow, so the lane needs no depth split.
29
- // Shared-context state for the async transient-dynamic lane.
30
- //
31
- // Every level of a SEQUENTIAL async chain shares the same resolutionPath and resolutionStack
32
- // arrays (passed by reference through ctx.resolveAsync), and the context stores references
33
- // rather than snapshots, so one DefaultResolutionContext can serve the whole chain — the arrays
34
- // reflect the current state automatically as levels push and pop.
35
- //
36
- // #asyncChainCtx: the shared context, created on first use and reset at each new root call.
37
- // Inner levels of the same chain reuse it with zero setup.
38
- // #asyncChainCtxPath: identity of the resolutionPath array owning the shared context — same
39
- // reference means an inner level of that chain, a different one means a concurrent chain
40
- // (e.g. Promise.all) which gets its own context instead.
41
- // #asyncChainActiveLevels: active levels of the OWNING chain, so the path pointer is released
42
- // when the last one settles. Concurrent fallback calls are not counted.
43
- //
44
- // The method is NOT declared async: that would allocate an async state machine and an implicit
45
- // promise per level, where a `.then(settle, settle)` side listener costs neither.
46
- #asyncChainCtx;
47
- #asyncChainCtxPath;
48
- #asyncChainActiveLevels = 0;
49
- // Settle callback shared by every level of the owning async chain: all of them pop the same
50
- // resolutionPath and decrement the same counter, so one closure serves the whole chain instead
51
- // of allocating one per level (the async lane's dominant per-level allocation).
52
- #asyncChainSettle;
53
- #classHasPostConstruct = new WeakMap();
54
- #classNeedsActiveContainer = new WeakMap();
55
- #classConstructorMetadata = new WeakMap();
56
- #activationNeedByBindingId = new Map();
57
- #activationCacheVersion = -1;
58
- // Options-less lookup memo across the parent chain: token → terminal {binding, owner}
59
- // (alias hops folded). `null` = token must take the slow lookup path. Invalidated when
60
- // any registry in the chain mutates (monotonic version sum).
61
- #defaultLookupByToken = new Map();
62
- #defaultLookupVersion = -1;
63
- // Name-only lookup memo (token → name → entry) with the same chain-version
64
- // invalidation. `null` = shape needs the full selection path.
65
- #namedLookupByToken = new Map();
66
- #namedLookupVersion = -1;
67
- // Compiled transient-class plans (Dagger-style): a pure-static subgraph (class/constant/
68
- // cached-singleton deps only) compiles once into a nested-constructor closure — cycle
69
- // checking happens at compile time, so execution skips all per-resolve bookkeeping.
70
- // `null` = binding is not plannable under the current versions.
26
+ // Pure allocation pool: a chain's own context is threaded through the call, so nothing here
27
+ // identifies a chain.
28
+ #asyncChainContextPool = [];
29
+ // Compiled plans; `null` marks a binding as unplannable under the current cache versions.
71
30
  #classPlanByBindingId = new Map();
72
31
  #classPlanRegistryVersion = -1;
73
32
  #classPlanActivationVersion = -1;
@@ -75,15 +34,33 @@ export class DependencyResolver {
75
34
  #scope;
76
35
  #lifecycle;
77
36
  #metadataReader;
78
- #container;
79
37
  #parent;
38
+ #lookup;
39
+ #classes;
40
+ #activation;
80
41
  constructor(registry, scope, lifecycle, metadataReader, container, parent) {
81
42
  this.#registry = registry;
82
43
  this.#scope = scope;
83
44
  this.#lifecycle = lifecycle;
84
45
  this.#metadataReader = metadataReader;
85
- this.#container = container;
86
46
  this.#parent = parent;
47
+ this.#lookup = new BindingLookupCache(registry, this, parent === undefined ? undefined : parent.#lookup);
48
+ this.#classes = new ClassIntrospector(metadataReader, container);
49
+ this.#activation = new ActivationNeedCache(lifecycle, this.#classes);
50
+ }
51
+ /** Structural counts for {@link RESOLUTION_DIAGNOSTICS}; see `resolution/diagnostics.ts`. */
52
+ describeCaches() {
53
+ let compiledPlanCount = 0;
54
+ for (const plan of this.#classPlanByBindingId.values()) {
55
+ if (plan !== null) {
56
+ compiledPlanCount += 1;
57
+ }
58
+ }
59
+ return {
60
+ asyncContextPoolSize: this.#asyncChainContextPool.length,
61
+ compiledPlanCount,
62
+ syncContextPoolSize: this.#syncResolutionContextPool.length,
63
+ };
87
64
  }
88
65
  // ── Binding lookup ─────────────────────────────────────────────────────────
89
66
  #findBinding(token, options, resolutionPath, resolutionStack) {
@@ -161,7 +138,7 @@ export class DependencyResolver {
161
138
  if (fastBinding !== undefined && fastBinding.kind !== "alias") {
162
139
  return this.#resolveDefaultEntry(fastBinding, this, resolutionPath, resolutionStack);
163
140
  }
164
- const entry = this.#lookupDefaultEntry(token);
141
+ const entry = this.#lookup.defaultEntry(token);
165
142
  if (entry === null) {
166
143
  return this.resolve(token, undefined, resolutionPath, resolutionStack);
167
144
  }
@@ -191,8 +168,8 @@ export class DependencyResolver {
191
168
  }
192
169
  }
193
170
  else if (scope === "singleton") {
194
- const cachedSingleton = owner.#scope.peekSingleton(binding.id);
195
- if (cachedSingleton !== SINGLETON_MISS) {
171
+ const cachedSingleton = binding.instance;
172
+ if (cachedSingleton !== NO_INSTANCE) {
196
173
  return cachedSingleton;
197
174
  }
198
175
  if (owner !== this) {
@@ -248,83 +225,8 @@ export class DependencyResolver {
248
225
  resolutionSet?.delete(tokenDisplayName);
249
226
  }
250
227
  }
251
- #chainRegistryVersion() {
252
- let version = this.#registry.version;
253
- for (let resolver = this.#parent; resolver !== undefined; resolver = resolver.#parent) {
254
- version += resolver.#registry.version;
255
- }
256
- return version;
257
- }
258
- #lookupDefaultEntry(token) {
259
- const version = this.#chainRegistryVersion();
260
- if (version !== this.#defaultLookupVersion) {
261
- this.#defaultLookupByToken.clear();
262
- this.#defaultLookupVersion = version;
263
- }
264
- let entry = this.#defaultLookupByToken.get(token);
265
- if (entry === undefined) {
266
- entry = this.#computeDefaultEntry(token);
267
- this.#defaultLookupByToken.set(token, entry);
268
- }
269
- return entry;
270
- }
271
- #computeDefaultEntry(token) {
272
- let current = token;
273
- for (let hop = 0; hop < ALIAS_HOP_LIMIT; hop += 1) {
274
- const entry = this.#findDefaultEntryInChain(current);
275
- if (entry === null) {
276
- return null;
277
- }
278
- if (entry.binding.kind === "alias") {
279
- current = entry.binding.target;
280
- continue;
281
- }
282
- return entry;
283
- }
284
- return null;
285
- }
286
- #lookupNamedEntry(token, name) {
287
- const version = this.#chainRegistryVersion();
288
- if (version !== this.#namedLookupVersion) {
289
- this.#namedLookupByToken.clear();
290
- this.#namedLookupVersion = version;
291
- }
292
- // ✓ TS6.0: Map.getOrInsert (ES2025)
293
- const entriesByName = this.#namedLookupByToken.getOrInsert(token, new Map());
294
- let entry = entriesByName.get(name);
295
- if (entry === undefined) {
296
- entry = this.#findNamedEntryInChain(token, name);
297
- entriesByName.set(name, entry);
298
- }
299
- return entry;
300
- }
301
- #findNamedEntryInChain(token, name) {
302
- const named = this.#registry.getSimpleNamed(token, name);
303
- if (named !== undefined) {
304
- // Predicates need a live context; aliases carry options through the full path.
305
- if (named.predicate !== undefined || named.kind === "alias") {
306
- return null;
307
- }
308
- return { binding: named, owner: this };
309
- }
310
- if (this.#registry.has(token)) {
311
- return null;
312
- }
313
- return this.#parent === undefined ? null : this.#parent.#findNamedEntryInChain(token, name);
314
- }
315
- #findDefaultEntryInChain(token) {
316
- const fast = this.#registry.getFastDefault(token);
317
- if (fast !== undefined) {
318
- return { binding: fast, owner: this };
319
- }
320
- // A level with non-fast bindings (multi-slot / predicate) needs full selection — bail.
321
- if (this.#registry.has(token)) {
322
- return null;
323
- }
324
- return this.#parent === undefined ? null : this.#parent.#findDefaultEntryInChain(token);
325
- }
326
228
  #getInstantiationPlan(binding) {
327
- const registryVersion = this.#chainRegistryVersion();
229
+ const registryVersion = this.#lookup.chainVersion();
328
230
  const activationVersion = this.#lifecycle.activationVersion;
329
231
  if (registryVersion !== this.#classPlanRegistryVersion || activationVersion !== this.#classPlanActivationVersion) {
330
232
  this.#classPlanByBindingId.clear();
@@ -346,22 +248,28 @@ export class DependencyResolver {
346
248
  // Compiler behind #getClassPlan — cold path, so the host indirection costs nothing hot.
347
249
  #planCompiler = new InstantiationPlanCompiler({
348
250
  hasActivationHandlers: (token) => this.#lifecycle.hasActivationHandlers(token),
349
- knownPostConstruct: (target) => this.#classHasPostConstruct.get(target),
350
- needsActiveContainer: (target) => {
351
- let needsActiveContainer = this.#classNeedsActiveContainer.get(target);
352
- if (needsActiveContainer === undefined) {
353
- const accessorMetadata = this.#metadataReader.getAccessorMetadata?.(target);
354
- needsActiveContainer = (accessorMetadata?.length ?? 0) > 0;
355
- this.#classNeedsActiveContainer.set(target, needsActiveContainer);
356
- }
357
- return needsActiveContainer;
358
- },
359
- getConstructorMetadata: (target) => this.#getConstructorMetadata(target),
251
+ knownPostConstruct: (target) => this.#classes.knownPostConstruct(target),
252
+ needsActiveContainer: (target) => this.#classes.needsActiveContainer(target),
253
+ getConstructorMetadata: (target) => this.#classes.constructorMetadata(target),
360
254
  lookupDependencyEntry: (token) => {
361
- const entry = this.#lookupDefaultEntry(token);
362
- return entry === null ? null : { binding: entry.binding, ownerScope: entry.owner.#scope };
255
+ const entry = this.#lookup.defaultEntry(token);
256
+ return entry === null ? null : { binding: entry.binding };
257
+ },
258
+ getResolutionFrame: (binding) => this.#getResolutionFrame(binding),
259
+ // Dispatches exactly as #resolveClassDeps does, so an escaped dep is indistinguishable
260
+ // from the same dep on a fully interpreted resolve.
261
+ resolveEscaped: (token, options, arity, resolutionPath, resolutionStack) => {
262
+ if (arity === "all") {
263
+ return this.resolveAll(token, options, resolutionPath, resolutionStack);
264
+ }
265
+ if (arity === "optional") {
266
+ return this.resolveOptional(token, options, resolutionPath, resolutionStack);
267
+ }
268
+ if (options === undefined) {
269
+ return this.resolveFromContext(token, resolutionPath, resolutionStack);
270
+ }
271
+ return this.resolve(token, options, resolutionPath, resolutionStack);
363
272
  },
364
- resolveFallback: (token) => this.resolve(token, undefined, [], []),
365
273
  });
366
274
  resolve(token, options, resolutionPath, resolutionStack) {
367
275
  // Name-only fast lane: memoized lookup, dispatching just the shapes whose
@@ -370,7 +278,7 @@ export class DependencyResolver {
370
278
  options.name !== undefined &&
371
279
  options.tag === undefined &&
372
280
  (options.tags === undefined || options.tags.length === 0)) {
373
- const namedEntry = this.#lookupNamedEntry(token, options.name);
281
+ const namedEntry = this.#lookup.namedEntry(token, options.name);
374
282
  if (namedEntry !== null) {
375
283
  const namedBinding = namedEntry.binding;
376
284
  if (namedBinding.kind === "constant" &&
@@ -380,9 +288,8 @@ export class DependencyResolver {
380
288
  }
381
289
  const namedScope = namedBinding.scope ?? "transient";
382
290
  if (namedScope === "singleton") {
383
- const cachedSingleton = namedEntry.owner.#scope.peekSingleton(namedBinding.id);
384
- if (cachedSingleton !== SINGLETON_MISS) {
385
- return cachedSingleton;
291
+ if (namedBinding.instance !== NO_INSTANCE) {
292
+ return namedBinding.instance;
386
293
  }
387
294
  }
388
295
  // Everything else keeps the full path (context, activation, guards).
@@ -427,10 +334,8 @@ export class DependencyResolver {
427
334
  }
428
335
  const scope = binding.scope ?? "transient";
429
336
  // Singleton cache check
430
- if (scope === "singleton") {
431
- if (this.#scope.hasSingleton(binding.id)) {
432
- return this.#scope.getSingleton(binding.id);
433
- }
337
+ if (scope === "singleton" && binding.instance !== NO_INSTANCE) {
338
+ return binding.instance;
434
339
  }
435
340
  // Scoped cache check
436
341
  if (scope === "scoped") {
@@ -445,7 +350,7 @@ export class DependencyResolver {
445
350
  const tokenDisplayName = frame.tokenName;
446
351
  const resolutionSet = enterResolutionPath(resolutionPath, tokenDisplayName, false);
447
352
  resolutionStack.push(frame);
448
- const needsActivation = this.#needsActivation(binding);
353
+ const needsActivation = this.#activation.needsActivation(binding);
449
354
  if (!needsActivation && scope === "transient" && binding.kind === "dynamic") {
450
355
  const resolutionCtx = this.#acquireSyncResolutionContext(resolutionPath, resolutionStack, options);
451
356
  try {
@@ -471,13 +376,13 @@ export class DependencyResolver {
471
376
  ? this.#acquireSyncResolutionContext(resolutionPath, resolutionStack, options)
472
377
  : undefined;
473
378
  const instance = this.#instantiateSync(binding, resolutionCtx, resolutionPath, resolutionStack);
474
- const shouldActivate = this.#refreshActivationCacheIfNeeded(binding, needsActivation);
379
+ const shouldActivate = this.#activation.refreshAfterFirstInstantiation(binding, needsActivation);
475
380
  const activated = shouldActivate
476
381
  ? this.#lifecycle.runActivationSync(resolutionCtx, binding, instance, this.#metadataReader)
477
382
  : instance;
478
383
  // Cache by scope
479
384
  if (scope === "singleton") {
480
- this.#scope.setSingleton(binding.id, activated);
385
+ this.#scope.setSingleton(binding, activated);
481
386
  }
482
387
  else if (scope === "scoped") {
483
388
  this.#scope.setScoped(binding.id, activated);
@@ -508,7 +413,7 @@ export class DependencyResolver {
508
413
  throw new AsyncResolutionError(tokenName(binding.token), tokenName(binding.token));
509
414
  case "class": {
510
415
  const deps = this.#resolveClassDeps(binding.target, resolutionPath, resolutionStack);
511
- const instance = this.#instantiateClass(binding.target, deps);
416
+ const instance = this.#classes.instantiate(binding.target, deps);
512
417
  return instance;
513
418
  }
514
419
  case "resolved": {
@@ -526,7 +431,7 @@ export class DependencyResolver {
526
431
  }
527
432
  }
528
433
  #resolveClassDeps(target, resolutionPath, resolutionStack) {
529
- const meta = this.#getConstructorMetadata(target);
434
+ const meta = this.#classes.constructorMetadata(target);
530
435
  if (meta === undefined) {
531
436
  if (target.length === 0) {
532
437
  return [];
@@ -620,7 +525,7 @@ export class DependencyResolver {
620
525
  return resolved;
621
526
  }
622
527
  // ── Async resolve ──────────────────────────────────────────────────────────
623
- resolveAsyncFromContext(token, resolutionPath, resolutionStack) {
528
+ resolveAsyncFromContext(token, resolutionPath, resolutionStack, callerContext) {
624
529
  // Hot lane: own-registry fast default (async chains resolve sibling dynamic bindings).
625
530
  // Fall back to the chain-versioned memo only on miss or alias.
626
531
  const fastBinding = this.#registry.getFastDefault(token);
@@ -630,17 +535,17 @@ export class DependencyResolver {
630
535
  fastBinding.scope === "transient" &&
631
536
  fastBinding.onActivation === undefined &&
632
537
  (this.#lifecycle.activationVersion === 0 || !this.#lifecycle.hasActivationHandlers(fastBinding.token))) {
633
- return this.#resolveTransientDynamicAsyncFromContext(fastBinding, resolutionPath, resolutionStack);
538
+ return this.#resolveTransientDynamicAsyncFromContext(fastBinding, resolutionPath, resolutionStack, callerContext);
634
539
  }
635
- return this.#resolveAsyncDefaultEntry(fastBinding, this, resolutionPath, resolutionStack);
540
+ return this.#resolveAsyncDefaultEntry(fastBinding, this, resolutionPath, resolutionStack, callerContext);
636
541
  }
637
- const entry = this.#lookupDefaultEntry(token);
542
+ const entry = this.#lookup.defaultEntry(token);
638
543
  if (entry === null) {
639
544
  return this.resolveAsync(token, undefined, resolutionPath, resolutionStack);
640
545
  }
641
- return this.#resolveAsyncDefaultEntry(entry.binding, entry.owner, resolutionPath, resolutionStack);
546
+ return this.#resolveAsyncDefaultEntry(entry.binding, entry.owner, resolutionPath, resolutionStack, callerContext);
642
547
  }
643
- #resolveAsyncDefaultEntry(binding, owner, resolutionPath, resolutionStack) {
548
+ #resolveAsyncDefaultEntry(binding, owner, resolutionPath, resolutionStack, callerContext) {
644
549
  if (binding.kind === "constant" &&
645
550
  binding.onActivation === undefined &&
646
551
  (this.#lifecycle.activationVersion === 0 || !this.#lifecycle.hasActivationHandlers(binding.token))) {
@@ -651,13 +556,12 @@ export class DependencyResolver {
651
556
  if ((binding.kind === "dynamic" || binding.kind === "dynamic-async") &&
652
557
  binding.onActivation === undefined &&
653
558
  (this.#lifecycle.activationVersion === 0 || !this.#lifecycle.hasActivationHandlers(binding.token))) {
654
- return this.#resolveTransientDynamicAsyncFromContext(binding, resolutionPath, resolutionStack);
559
+ return this.#resolveTransientDynamicAsyncFromContext(binding, resolutionPath, resolutionStack, callerContext);
655
560
  }
656
561
  }
657
562
  else if (scope === "singleton") {
658
- const cachedSingleton = owner.#scope.peekSingleton(binding.id);
659
- if (cachedSingleton !== SINGLETON_MISS) {
660
- return Promise.resolve(cachedSingleton);
563
+ if (binding.instance !== NO_INSTANCE) {
564
+ return Promise.resolve(binding.instance);
661
565
  }
662
566
  if (owner !== this) {
663
567
  return owner.#resolveBindingAsync(binding, undefined, resolutionPath, resolutionStack);
@@ -718,8 +622,8 @@ export class DependencyResolver {
718
622
  const scope = binding.scope ?? "transient";
719
623
  // Singleton cache
720
624
  if (scope === "singleton") {
721
- if (this.#scope.hasSingleton(binding.id)) {
722
- return this.#scope.getSingleton(binding.id);
625
+ if (binding.instance !== NO_INSTANCE) {
626
+ return binding.instance;
723
627
  }
724
628
  // In-flight dedup
725
629
  const inflight = this.#scope.getInflight(binding.id);
@@ -740,7 +644,7 @@ export class DependencyResolver {
740
644
  const frameName = frame.tokenName;
741
645
  const resolutionSet = enterResolutionPath(resolutionPath, frameName, false);
742
646
  resolutionStack.push(frame);
743
- const needsActivation = this.#needsActivation(binding);
647
+ const needsActivation = this.#activation.needsActivation(binding);
744
648
  if (!needsActivation && scope === "transient" && (binding.kind === "dynamic" || binding.kind === "dynamic-async")) {
745
649
  const resolutionCtx = new DefaultResolutionContext(this, resolutionPath, resolutionStack, options);
746
650
  try {
@@ -764,11 +668,12 @@ export class DependencyResolver {
764
668
  if (scope === "singleton") {
765
669
  const createSingletonPromise = async () => {
766
670
  const instance = await this.#instantiateAsync(binding, resolutionCtx, resolutionPath, resolutionStack);
767
- const shouldActivate = this.#refreshActivationCacheIfNeeded(binding, needsActivation);
671
+ const shouldActivate = this.#activation.refreshAfterFirstInstantiation(binding, needsActivation);
768
672
  const activated = shouldActivate
769
673
  ? await this.#lifecycle.runActivation(resolutionCtx, binding, instance, this.#metadataReader)
770
674
  : instance;
771
- this.#scope.setSingleton(binding.id, activated);
675
+ this.#scope.setSingleton(binding, activated);
676
+ binding.instance = activated;
772
677
  this.#scope.clearInflight(binding.id);
773
678
  return activated;
774
679
  };
@@ -780,7 +685,7 @@ export class DependencyResolver {
780
685
  return await singletonPromise;
781
686
  }
782
687
  const instance = await this.#instantiateAsync(binding, resolutionCtx, resolutionPath, resolutionStack);
783
- const shouldActivate = this.#refreshActivationCacheIfNeeded(binding, needsActivation);
688
+ const shouldActivate = this.#activation.refreshAfterFirstInstantiation(binding, needsActivation);
784
689
  const activated = shouldActivate
785
690
  ? await this.#lifecycle.runActivation(resolutionCtx, binding, instance, this.#metadataReader)
786
691
  : instance;
@@ -813,7 +718,7 @@ export class DependencyResolver {
813
718
  return binding.factory(ctx);
814
719
  case "class": {
815
720
  const deps = await this.#resolveClassDepsAsync(binding.target, resolutionPath, resolutionStack);
816
- const instance = this.#instantiateClass(binding.target, deps);
721
+ const instance = this.#classes.instantiate(binding.target, deps);
817
722
  return instance;
818
723
  }
819
724
  case "resolved": {
@@ -833,7 +738,7 @@ export class DependencyResolver {
833
738
  }
834
739
  }
835
740
  async #resolveClassDepsAsync(target, resolutionPath, resolutionStack) {
836
- const meta = this.#getConstructorMetadata(target);
741
+ const meta = this.#classes.constructorMetadata(target);
837
742
  if (meta === undefined) {
838
743
  if (target.length === 0) {
839
744
  return [];
@@ -1022,28 +927,6 @@ export class DependencyResolver {
1022
927
  #getTokenName(token) {
1023
928
  return tokenName(token);
1024
929
  }
1025
- #getConstructorMetadata(target) {
1026
- const cached = this.#classConstructorMetadata.get(target);
1027
- if (cached !== undefined) {
1028
- return cached === null ? undefined : cached;
1029
- }
1030
- const metadata = this.#metadataReader.getConstructorMetadata(target);
1031
- this.#classConstructorMetadata.set(target, metadata ?? null);
1032
- return metadata;
1033
- }
1034
- #instantiateClass(target, deps) {
1035
- let needsActiveContainer = this.#classNeedsActiveContainer.get(target);
1036
- if (needsActiveContainer === undefined) {
1037
- const accessorMetadata = this.#metadataReader.getAccessorMetadata?.(target);
1038
- needsActiveContainer = (accessorMetadata?.length ?? 0) > 0;
1039
- this.#classNeedsActiveContainer.set(target, needsActiveContainer);
1040
- }
1041
- const invokable = target;
1042
- if (!needsActiveContainer) {
1043
- return new invokable(...deps);
1044
- }
1045
- return runWithContainer(this.#container, () => new invokable(...deps));
1046
- }
1047
930
  #matchesRequestedTag(tagKey, tagValue, requestedTags, singleRequestedTag) {
1048
931
  if (singleRequestedTag !== undefined &&
1049
932
  singleRequestedTag[0] === tagKey &&
@@ -1062,10 +945,7 @@ export class DependencyResolver {
1062
945
  return false;
1063
946
  }
1064
947
  #resolveTransientDynamicSyncFromContext(binding, resolutionPath, resolutionStack) {
1065
- // One lane at every depth. The separate deep lane existed because cycle detection used to be
1066
- // an O(depth) `resolutionPath.includes()` scan, which had to be escaped past ~32 levels; with
1067
- // the O(1) `binding.inFlight` mark there is nothing to escape, so the depth split — and the
1068
- // divergent behaviour it caused — is gone.
948
+ // One lane at every depth: `binding.inFlight` is O(1), so there is nothing to escape.
1069
949
  const frame = this.#getResolutionFrame(binding);
1070
950
  const tokenDisplayName = frame.tokenName;
1071
951
  if (binding.inFlight) {
@@ -1088,19 +968,10 @@ export class DependencyResolver {
1088
968
  binding.inFlight = false;
1089
969
  }
1090
970
  }
1091
- // NOT declared `async` — avoids creating a JSAsyncGeneratorObject + implicit Promise wrapper on
1092
- // every invocation. Cleanup is handled via .then(onFulfilled, onRejected) so the behaviour is
1093
- // identical to a try/finally but without the async machinery overhead.
1094
- #resolveTransientDynamicAsyncFromContext(binding, resolutionPath, resolutionStack) {
1095
- // One lane at every depth. Cycle detection goes through `enterResolutionPath`, which is
1096
- // path-scoped — the only mechanism that stays correct when chains interleave (Promise.all) —
1097
- // and adapts on its own: a linear scan while the path is short, an attached Set past
1098
- // RESOLUTION_SET_THRESHOLD. That removes the depth split, and with it a silent change of
1099
- // behaviour (context identity, stack frames, promise shape) at the old threshold.
1100
- //
1101
- // For a sequential chain every level shares one resolutionPath/resolutionStack, so a single
1102
- // DefaultResolutionContext serves the whole chain: inner levels of the owning chain allocate
1103
- // nothing. A concurrent chain is detected by path identity and gets its own context.
971
+ // Deliberately not `async`: that would allocate a state machine and a promise per level.
972
+ #resolveTransientDynamicAsyncFromContext(binding, resolutionPath, resolutionStack, callerContext) {
973
+ // Path-scoped cycle detection, because async chains interleave — see ARCHITECTURE.md.
974
+ const pool = this.#asyncChainContextPool;
1104
975
  const frame = this.#getResolutionFrame(binding);
1105
976
  const tokenDisplayName = frame.tokenName;
1106
977
  try {
@@ -1110,32 +981,12 @@ export class DependencyResolver {
1110
981
  // This method is not `async`; keep failures as rejections rather than sync throws.
1111
982
  return Promise.reject(cycleError);
1112
983
  }
1113
- let ctx;
1114
- let isOwnerLevel;
1115
- if (this.#asyncChainCtxPath === resolutionPath) {
1116
- ctx = this.#asyncChainCtx;
1117
- isOwnerLevel = true;
1118
- }
1119
- else if (this.#asyncChainCtxPath === undefined) {
1120
- const existing = this.#asyncChainCtx;
1121
- if (existing === undefined) {
1122
- ctx = new DefaultResolutionContext(this, resolutionPath, resolutionStack, undefined);
1123
- this.#asyncChainCtx = ctx;
1124
- }
1125
- else {
1126
- existing.reset(this, resolutionPath, resolutionStack, undefined);
1127
- ctx = existing;
1128
- }
1129
- this.#asyncChainCtxPath = resolutionPath;
1130
- isOwnerLevel = true;
1131
- }
1132
- else {
1133
- ctx = new DefaultResolutionContext(this, resolutionPath, resolutionStack, undefined);
1134
- isOwnerLevel = false;
1135
- }
1136
- if (isOwnerLevel) {
1137
- this.#asyncChainActiveLevels++;
1138
- }
984
+ // An inner level reuses the context its caller passed down; only a chain's first level
985
+ // borrows from the pool. Pooling is load-bearing here — see ARCHITECTURE.md.
986
+ const ctx = callerContext !== undefined && callerContext.owner === this
987
+ ? callerContext
988
+ : this.#acquireAsyncChainContext(resolutionPath, resolutionStack);
989
+ ctx.chainLevels += 1;
1139
990
  // Invoke the factory synchronously to get its Promise (or a resolved value for "dynamic").
1140
991
  let factoryPromise;
1141
992
  try {
@@ -1151,40 +1002,33 @@ export class DependencyResolver {
1151
1002
  catch (factoryError) {
1152
1003
  // Synchronous throw from the factory (rare) — clean up immediately.
1153
1004
  exitResolutionPath(resolutionPath);
1154
- if (isOwnerLevel && --this.#asyncChainActiveLevels === 0) {
1155
- this.#asyncChainCtxPath = undefined;
1156
- this.#asyncChainSettle = undefined;
1005
+ ctx.chainLevels -= 1;
1006
+ if (ctx.chainLevels === 0) {
1007
+ pool.push(ctx);
1157
1008
  }
1158
1009
  return Promise.reject(factoryError);
1159
1010
  }
1160
- // Cleanup runs as a SIDE listener on the factory promise instead of a derived-promise chain:
1161
- // registered synchronously here, it is FIFO-guaranteed to run before the awaiting caller
1162
- // resumes, so ordering is identical while saving one intermediate promise and one microtask
1163
- // hop per level. Trade-off: the settle handler marks a rejection as handled, so an unawaited
1164
- // failing resolveAsync no longer surfaces as an unhandledRejection — callers are expected to
1165
- // await (or .catch) the returned promise.
1166
- //
1167
- // Every level of the owning chain unwinds identically, so one closure serves them all.
1168
- let settle;
1169
- if (isOwnerLevel) {
1170
- settle =
1171
- this.#asyncChainSettle ??
1172
- (this.#asyncChainSettle = () => {
1173
- exitResolutionPath(resolutionPath);
1174
- if (--this.#asyncChainActiveLevels === 0) {
1175
- this.#asyncChainCtxPath = undefined;
1176
- this.#asyncChainSettle = undefined;
1177
- }
1178
- });
1179
- }
1180
- else {
1181
- settle = () => {
1011
+ // A side listener, not a derived chain: FIFO puts it before the awaiting caller resumes.
1012
+ // It marks a rejection as handled, so callers must await or `.catch` the returned promise.
1013
+ const settle = ctx.chainSettle ??
1014
+ (ctx.chainSettle = () => {
1182
1015
  exitResolutionPath(resolutionPath);
1183
- };
1184
- }
1016
+ ctx.chainLevels -= 1;
1017
+ if (ctx.chainLevels === 0) {
1018
+ pool.push(ctx);
1019
+ }
1020
+ });
1185
1021
  factoryPromise.then(settle, settle);
1186
1022
  return factoryPromise;
1187
1023
  }
1024
+ #acquireAsyncChainContext(resolutionPath, resolutionStack) {
1025
+ const pooled = this.#asyncChainContextPool.pop();
1026
+ if (pooled === undefined) {
1027
+ return new DefaultResolutionContext(this, resolutionPath, resolutionStack, undefined);
1028
+ }
1029
+ pooled.reset(this, resolutionPath, resolutionStack, undefined);
1030
+ return pooled;
1031
+ }
1188
1032
  #resolveCandidateSync(binding, options, resolutionPath, resolutionStack) {
1189
1033
  if (binding.kind === "constant" &&
1190
1034
  binding.onActivation === undefined &&
@@ -1195,8 +1039,8 @@ export class DependencyResolver {
1195
1039
  return this.resolve(binding.target, options, resolutionPath, resolutionStack);
1196
1040
  }
1197
1041
  const scope = binding.scope ?? "transient";
1198
- if (scope === "singleton" && this.#scope.hasSingleton(binding.id)) {
1199
- return this.#scope.getSingleton(binding.id);
1042
+ if (scope === "singleton" && binding.instance !== NO_INSTANCE) {
1043
+ return binding.instance;
1200
1044
  }
1201
1045
  if (scope === "scoped") {
1202
1046
  if (!this.#scope.isChild) {
@@ -1220,8 +1064,8 @@ export class DependencyResolver {
1220
1064
  return this.resolveAsync(binding.target, options, isolatedPath, isolatedStack);
1221
1065
  }
1222
1066
  const scope = binding.scope ?? "transient";
1223
- if (scope === "singleton" && this.#scope.hasSingleton(binding.id)) {
1224
- return Promise.resolve(this.#scope.getSingleton(binding.id));
1067
+ if (scope === "singleton" && binding.instance !== NO_INSTANCE) {
1068
+ return Promise.resolve(binding.instance);
1225
1069
  }
1226
1070
  if (scope === "scoped") {
1227
1071
  if (!this.#scope.isChild) {
@@ -1246,62 +1090,6 @@ export class DependencyResolver {
1246
1090
  binding.frame = frame;
1247
1091
  return frame;
1248
1092
  }
1249
- #needsActivation(binding) {
1250
- const lifecycleVersion = this.#lifecycle.activationVersion;
1251
- if (lifecycleVersion === 0 &&
1252
- binding.kind !== "class" &&
1253
- binding.kind !== "alias" &&
1254
- binding.onActivation === undefined) {
1255
- return false;
1256
- }
1257
- if (this.#activationCacheVersion !== lifecycleVersion) {
1258
- this.#activationNeedByBindingId.clear();
1259
- this.#activationCacheVersion = lifecycleVersion;
1260
- }
1261
- const cached = this.#activationNeedByBindingId.get(binding.id);
1262
- if (cached !== undefined) {
1263
- return cached;
1264
- }
1265
- if (binding.kind === "class") {
1266
- let hasActivation = this.#lifecycle.hasActivationHandlers(binding.token) || binding.onActivation !== undefined;
1267
- const cachedPostConstruct = this.#classHasPostConstruct.get(binding.target);
1268
- // Unknown class lifecycle metadata: activate once, then cache after first instantiation.
1269
- if (cachedPostConstruct === undefined) {
1270
- hasActivation = true;
1271
- }
1272
- else if (cachedPostConstruct) {
1273
- hasActivation = true;
1274
- }
1275
- this.#activationNeedByBindingId.set(binding.id, hasActivation);
1276
- return hasActivation;
1277
- }
1278
- let hasActivation = false;
1279
- if (binding.kind !== "alias" && binding.onActivation !== undefined) {
1280
- hasActivation = true;
1281
- }
1282
- else if (this.#lifecycle.hasActivationHandlers(binding.token)) {
1283
- hasActivation = true;
1284
- }
1285
- this.#activationNeedByBindingId.set(binding.id, hasActivation);
1286
- return hasActivation;
1287
- }
1288
- #refreshClassPostConstructCache(target) {
1289
- const lifecycle = this.#metadataReader.getLifecycleMetadata(target);
1290
- const hasPostConstruct = lifecycle !== undefined && lifecycle.postConstruct !== undefined && lifecycle.postConstruct.length > 0;
1291
- this.#classHasPostConstruct.set(target, hasPostConstruct);
1292
- }
1293
- /**
1294
- * Refreshes the post-construct cache for class bindings on first instantiation and
1295
- * returns the (possibly updated) shouldActivate flag.
1296
- */
1297
- #refreshActivationCacheIfNeeded(binding, needsActivation) {
1298
- if (binding.kind === "class" && this.#classHasPostConstruct.get(binding.target) === undefined) {
1299
- this.#refreshClassPostConstructCache(binding.target);
1300
- this.#activationNeedByBindingId.delete(binding.id);
1301
- return this.#needsActivation(binding);
1302
- }
1303
- return needsActivation;
1304
- }
1305
1093
  #requiresResolutionContext(binding) {
1306
1094
  return binding.kind === "dynamic" || binding.kind === "dynamic-async";
1307
1095
  }