@codefast/di 0.5.0-canary.9 → 0.6.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 (266) hide show
  1. package/CHANGELOG.md +865 -0
  2. package/README.md +119 -30
  3. package/dist/ambient/active-container.d.ts +37 -0
  4. package/dist/ambient/active-container.d.ts.map +1 -0
  5. package/dist/ambient/active-container.js +41 -0
  6. package/dist/ambient/active-container.js.map +1 -0
  7. package/dist/container/binding-builders.d.ts +12 -8
  8. package/dist/container/binding-builders.d.ts.map +1 -1
  9. package/dist/container/binding-builders.js +56 -31
  10. package/dist/container/binding-builders.js.map +1 -1
  11. package/dist/container/container.d.ts +33 -19
  12. package/dist/container/container.d.ts.map +1 -1
  13. package/dist/container/container.js +279 -118
  14. package/dist/container/container.js.map +1 -1
  15. package/dist/{resolution → core}/binding-scope.d.ts +2 -2
  16. package/dist/core/binding-scope.d.ts.map +1 -0
  17. package/dist/core/binding-scope.js.map +1 -0
  18. package/dist/{binding.d.ts → core/binding.d.ts} +70 -15
  19. package/dist/core/binding.d.ts.map +1 -0
  20. package/dist/{binding.js → core/binding.js} +27 -12
  21. package/dist/core/binding.js.map +1 -0
  22. package/dist/core/constraint-requirement.d.ts +38 -0
  23. package/dist/core/constraint-requirement.d.ts.map +1 -0
  24. package/dist/core/constraint-requirement.js +28 -0
  25. package/dist/core/constraint-requirement.js.map +1 -0
  26. package/dist/{constructor-type.d.ts → core/constructor-type.d.ts} +1 -1
  27. package/dist/core/constructor-type.d.ts.map +1 -0
  28. package/dist/core/constructor-type.js.map +1 -0
  29. package/dist/{module.d.ts → core/module.d.ts} +5 -5
  30. package/dist/core/module.d.ts.map +1 -0
  31. package/dist/{module.js → core/module.js} +4 -4
  32. package/dist/core/module.js.map +1 -0
  33. package/dist/{registry.d.ts → core/registry.d.ts} +27 -7
  34. package/dist/core/registry.d.ts.map +1 -0
  35. package/dist/{registry.js → core/registry.js} +125 -52
  36. package/dist/core/registry.js.map +1 -0
  37. package/dist/core/tag.d.ts +80 -0
  38. package/dist/core/tag.d.ts.map +1 -0
  39. package/dist/core/tag.js +84 -0
  40. package/dist/core/tag.js.map +1 -0
  41. package/dist/{token.d.ts → core/token.d.ts} +2 -6
  42. package/dist/core/token.d.ts.map +1 -0
  43. package/dist/core/token.js +13 -0
  44. package/dist/core/token.js.map +1 -0
  45. package/dist/{types.d.ts → core/types.d.ts} +27 -21
  46. package/dist/core/types.d.ts.map +1 -0
  47. package/dist/core/types.js.map +1 -0
  48. package/dist/decorators/inject.d.ts +5 -49
  49. package/dist/decorators/inject.d.ts.map +1 -1
  50. package/dist/decorators/inject.js +34 -104
  51. package/dist/decorators/inject.js.map +1 -1
  52. package/dist/decorators/injectable.d.ts +25 -5
  53. package/dist/decorators/injectable.d.ts.map +1 -1
  54. package/dist/decorators/injectable.js +2 -3
  55. package/dist/decorators/injectable.js.map +1 -1
  56. package/dist/decorators/lifecycle-decorators.d.ts +4 -2
  57. package/dist/decorators/lifecycle-decorators.d.ts.map +1 -1
  58. package/dist/decorators/lifecycle-decorators.js +19 -25
  59. package/dist/decorators/lifecycle-decorators.js.map +1 -1
  60. package/dist/{resolution → errors}/diagnostics.d.ts +2 -0
  61. package/dist/errors/diagnostics.d.ts.map +1 -0
  62. package/dist/errors/diagnostics.js.map +1 -0
  63. package/dist/{errors.d.ts → errors/errors.d.ts} +89 -4
  64. package/dist/errors/errors.d.ts.map +1 -0
  65. package/dist/{errors.js → errors/errors.js} +129 -7
  66. package/dist/errors/errors.js.map +1 -0
  67. package/dist/index.d.ts +21 -14
  68. package/dist/index.d.ts.map +1 -1
  69. package/dist/index.js +15 -7
  70. package/dist/index.js.map +1 -1
  71. package/dist/injection/descriptor.d.ts +79 -0
  72. package/dist/injection/descriptor.d.ts.map +1 -0
  73. package/dist/injection/descriptor.js +118 -0
  74. package/dist/injection/descriptor.js.map +1 -0
  75. package/dist/{resolution → injection}/resolve-options.d.ts +21 -11
  76. package/dist/injection/resolve-options.d.ts.map +1 -0
  77. package/dist/injection/resolve-options.js +101 -0
  78. package/dist/injection/resolve-options.js.map +1 -0
  79. package/dist/introspection/dependency-graph.d.ts +16 -5
  80. package/dist/introspection/dependency-graph.d.ts.map +1 -1
  81. package/dist/introspection/dependency-graph.js +138 -57
  82. package/dist/introspection/dependency-graph.js.map +1 -1
  83. package/dist/introspection/graph-adapters/cytoscape.d.ts +6 -3
  84. package/dist/introspection/graph-adapters/cytoscape.d.ts.map +1 -1
  85. package/dist/introspection/graph-adapters/cytoscape.js +3 -0
  86. package/dist/introspection/graph-adapters/cytoscape.js.map +1 -1
  87. package/dist/introspection/graph-adapters/dot.d.ts.map +1 -1
  88. package/dist/introspection/graph-adapters/dot.js +12 -5
  89. package/dist/introspection/graph-adapters/dot.js.map +1 -1
  90. package/dist/introspection/graph-adapters/mermaid.d.ts +9 -0
  91. package/dist/introspection/graph-adapters/mermaid.d.ts.map +1 -0
  92. package/dist/introspection/graph-adapters/mermaid.js +47 -0
  93. package/dist/introspection/graph-adapters/mermaid.js.map +1 -0
  94. package/dist/introspection/graph-adapters/reactflow.d.ts +6 -3
  95. package/dist/introspection/graph-adapters/reactflow.d.ts.map +1 -1
  96. package/dist/introspection/graph-adapters/reactflow.js +5 -1
  97. package/dist/introspection/graph-adapters/reactflow.js.map +1 -1
  98. package/dist/introspection/inspector.d.ts +4 -4
  99. package/dist/introspection/inspector.d.ts.map +1 -1
  100. package/dist/introspection/inspector.js +10 -6
  101. package/dist/introspection/inspector.js.map +1 -1
  102. package/dist/lifecycle/lifecycle-manager.d.ts +27 -0
  103. package/dist/lifecycle/lifecycle-manager.d.ts.map +1 -0
  104. package/dist/{resolution/lifecycle.js → lifecycle/lifecycle-manager.js} +39 -9
  105. package/dist/lifecycle/lifecycle-manager.js.map +1 -0
  106. package/dist/lifecycle/scope-manager.d.ts +48 -0
  107. package/dist/lifecycle/scope-manager.d.ts.map +1 -0
  108. package/dist/lifecycle/scope-manager.js +135 -0
  109. package/dist/lifecycle/scope-manager.js.map +1 -0
  110. package/dist/metadata/metadata-reader-token.d.ts +1 -1
  111. package/dist/metadata/metadata-reader-token.d.ts.map +1 -1
  112. package/dist/metadata/metadata-reader-token.js +1 -1
  113. package/dist/metadata/metadata-reader-token.js.map +1 -1
  114. package/dist/metadata/metadata-types.d.ts +9 -9
  115. package/dist/metadata/metadata-types.d.ts.map +1 -1
  116. package/dist/metadata/symbol-metadata-reader.d.ts +19 -6
  117. package/dist/metadata/symbol-metadata-reader.d.ts.map +1 -1
  118. package/dist/metadata/symbol-metadata-reader.js +89 -9
  119. package/dist/metadata/symbol-metadata-reader.js.map +1 -1
  120. package/dist/metadata/verifying-metadata-reader.d.ts +16 -0
  121. package/dist/metadata/verifying-metadata-reader.d.ts.map +1 -0
  122. package/dist/metadata/verifying-metadata-reader.js +32 -0
  123. package/dist/metadata/verifying-metadata-reader.js.map +1 -0
  124. package/dist/resolution/{activation-need.d.ts → cache/activation-need.d.ts} +5 -5
  125. package/dist/resolution/cache/activation-need.d.ts.map +1 -0
  126. package/dist/resolution/cache/activation-need.js.map +1 -0
  127. package/dist/resolution/{binding-lookup-cache.d.ts → cache/binding-lookup-cache.d.ts} +6 -5
  128. package/dist/resolution/cache/binding-lookup-cache.d.ts.map +1 -0
  129. package/dist/resolution/cache/binding-lookup-cache.js.map +1 -0
  130. package/dist/resolution/cache/class-introspector.d.ts +66 -0
  131. package/dist/resolution/cache/class-introspector.d.ts.map +1 -0
  132. package/dist/resolution/cache/class-introspector.js +194 -0
  133. package/dist/resolution/cache/class-introspector.js.map +1 -0
  134. package/dist/resolution/context.d.ts +84 -0
  135. package/dist/resolution/context.d.ts.map +1 -0
  136. package/dist/resolution/{environment.js → context.js} +54 -80
  137. package/dist/resolution/context.js.map +1 -0
  138. package/dist/resolution/path/resolution-path.d.ts +94 -0
  139. package/dist/resolution/path/resolution-path.d.ts.map +1 -0
  140. package/dist/resolution/path/resolution-path.js +117 -0
  141. package/dist/resolution/path/resolution-path.js.map +1 -0
  142. package/dist/resolution/{instantiation-plan.d.ts → plan/instantiation-plan.d.ts} +30 -7
  143. package/dist/resolution/plan/instantiation-plan.d.ts.map +1 -0
  144. package/dist/resolution/plan/instantiation-plan.js +390 -0
  145. package/dist/resolution/plan/instantiation-plan.js.map +1 -0
  146. package/dist/resolution/resolver.d.ts +28 -28
  147. package/dist/resolution/resolver.d.ts.map +1 -1
  148. package/dist/resolution/resolver.js +552 -239
  149. package/dist/resolution/resolver.js.map +1 -1
  150. package/dist/resolution/select/binding-select.d.ts +34 -0
  151. package/dist/resolution/select/binding-select.d.ts.map +1 -0
  152. package/dist/resolution/select/binding-select.js +175 -0
  153. package/dist/resolution/select/binding-select.js.map +1 -0
  154. package/dist/resolution/{constraints.d.ts → select/constraints.d.ts} +13 -12
  155. package/dist/resolution/select/constraints.d.ts.map +1 -0
  156. package/dist/resolution/{constraints.js → select/constraints.js} +33 -22
  157. package/dist/resolution/select/constraints.js.map +1 -0
  158. package/package.json +160 -20
  159. package/src/ambient/active-container.ts +65 -0
  160. package/src/container/binding-builders.ts +72 -44
  161. package/src/container/container.ts +355 -170
  162. package/src/{resolution → core}/binding-scope.ts +2 -2
  163. package/src/{binding.ts → core/binding.ts} +101 -39
  164. package/src/core/constraint-requirement.ts +51 -0
  165. package/src/{constructor-type.ts → core/constructor-type.ts} +1 -1
  166. package/src/{module.ts → core/module.ts} +10 -10
  167. package/src/core/registry.ts +365 -0
  168. package/src/core/tag.ts +144 -0
  169. package/src/{token.ts → core/token.ts} +2 -14
  170. package/src/{types.ts → core/types.ts} +37 -34
  171. package/src/decorators/inject.ts +39 -175
  172. package/src/decorators/injectable.ts +40 -10
  173. package/src/decorators/lifecycle-decorators.ts +22 -34
  174. package/src/{resolution → errors}/diagnostics.ts +2 -0
  175. package/src/{errors.ts → errors/errors.ts} +152 -8
  176. package/src/index.ts +43 -15
  177. package/src/injection/descriptor.ts +206 -0
  178. package/src/injection/resolve-options.ts +144 -0
  179. package/src/introspection/dependency-graph.ts +223 -63
  180. package/src/introspection/graph-adapters/cytoscape.ts +9 -3
  181. package/src/introspection/graph-adapters/dot.ts +13 -5
  182. package/src/introspection/graph-adapters/mermaid.ts +61 -0
  183. package/src/introspection/graph-adapters/reactflow.ts +11 -4
  184. package/src/introspection/inspector.ts +16 -12
  185. package/src/{resolution/lifecycle.ts → lifecycle/lifecycle-manager.ts} +68 -32
  186. package/src/lifecycle/scope-manager.ts +156 -0
  187. package/src/metadata/metadata-reader-token.ts +2 -2
  188. package/src/metadata/metadata-types.ts +9 -9
  189. package/src/metadata/symbol-metadata-reader.ts +95 -18
  190. package/src/metadata/verifying-metadata-reader.ts +41 -0
  191. package/src/resolution/{activation-need.ts → cache/activation-need.ts} +8 -8
  192. package/src/resolution/{binding-lookup-cache.ts → cache/binding-lookup-cache.ts} +6 -5
  193. package/src/resolution/cache/class-introspector.ts +251 -0
  194. package/src/resolution/{environment.ts → context.ts} +84 -145
  195. package/src/resolution/path/resolution-path.ts +167 -0
  196. package/src/resolution/plan/instantiation-plan.ts +579 -0
  197. package/src/resolution/resolver.ts +635 -313
  198. package/src/resolution/select/binding-select.ts +210 -0
  199. package/src/resolution/select/constraints.ts +130 -0
  200. package/dist/binding.d.ts.map +0 -1
  201. package/dist/binding.js.map +0 -1
  202. package/dist/constructor-type.d.ts.map +0 -1
  203. package/dist/constructor-type.js.map +0 -1
  204. package/dist/errors.d.ts.map +0 -1
  205. package/dist/errors.js.map +0 -1
  206. package/dist/module.d.ts.map +0 -1
  207. package/dist/module.js.map +0 -1
  208. package/dist/registry.d.ts.map +0 -1
  209. package/dist/registry.js.map +0 -1
  210. package/dist/resolution/activation-need.d.ts.map +0 -1
  211. package/dist/resolution/activation-need.js.map +0 -1
  212. package/dist/resolution/binding-lookup-cache.d.ts.map +0 -1
  213. package/dist/resolution/binding-lookup-cache.js.map +0 -1
  214. package/dist/resolution/binding-scope.d.ts.map +0 -1
  215. package/dist/resolution/binding-scope.js.map +0 -1
  216. package/dist/resolution/binding-select.d.ts +0 -23
  217. package/dist/resolution/binding-select.d.ts.map +0 -1
  218. package/dist/resolution/binding-select.js +0 -121
  219. package/dist/resolution/binding-select.js.map +0 -1
  220. package/dist/resolution/class-introspector.d.ts +0 -27
  221. package/dist/resolution/class-introspector.d.ts.map +0 -1
  222. package/dist/resolution/class-introspector.js +0 -60
  223. package/dist/resolution/class-introspector.js.map +0 -1
  224. package/dist/resolution/constraints.d.ts.map +0 -1
  225. package/dist/resolution/constraints.js.map +0 -1
  226. package/dist/resolution/diagnostics.d.ts.map +0 -1
  227. package/dist/resolution/diagnostics.js.map +0 -1
  228. package/dist/resolution/environment.d.ts +0 -94
  229. package/dist/resolution/environment.d.ts.map +0 -1
  230. package/dist/resolution/environment.js.map +0 -1
  231. package/dist/resolution/instantiation-plan.d.ts.map +0 -1
  232. package/dist/resolution/instantiation-plan.js +0 -183
  233. package/dist/resolution/instantiation-plan.js.map +0 -1
  234. package/dist/resolution/lifecycle.d.ts +0 -23
  235. package/dist/resolution/lifecycle.d.ts.map +0 -1
  236. package/dist/resolution/lifecycle.js.map +0 -1
  237. package/dist/resolution/resolution-path.d.ts +0 -98
  238. package/dist/resolution/resolution-path.d.ts.map +0 -1
  239. package/dist/resolution/resolution-path.js +0 -98
  240. package/dist/resolution/resolution-path.js.map +0 -1
  241. package/dist/resolution/resolve-options.d.ts.map +0 -1
  242. package/dist/resolution/resolve-options.js +0 -56
  243. package/dist/resolution/resolve-options.js.map +0 -1
  244. package/dist/resolution/scope.d.ts +0 -28
  245. package/dist/resolution/scope.d.ts.map +0 -1
  246. package/dist/resolution/scope.js +0 -88
  247. package/dist/resolution/scope.js.map +0 -1
  248. package/dist/token.d.ts.map +0 -1
  249. package/dist/token.js +0 -22
  250. package/dist/token.js.map +0 -1
  251. package/dist/types.d.ts.map +0 -1
  252. package/dist/types.js.map +0 -1
  253. package/src/registry.ts +0 -282
  254. package/src/resolution/binding-select.ts +0 -154
  255. package/src/resolution/class-introspector.ts +0 -74
  256. package/src/resolution/constraints.ts +0 -121
  257. package/src/resolution/instantiation-plan.ts +0 -292
  258. package/src/resolution/resolution-path.ts +0 -156
  259. package/src/resolution/resolve-options.ts +0 -89
  260. package/src/resolution/scope.ts +0 -105
  261. /package/dist/{resolution → core}/binding-scope.js +0 -0
  262. /package/dist/{constructor-type.js → core/constructor-type.js} +0 -0
  263. /package/dist/{types.js → core/types.js} +0 -0
  264. /package/dist/{resolution → errors}/diagnostics.js +0 -0
  265. /package/dist/resolution/{activation-need.js → cache/activation-need.js} +0 -0
  266. /package/dist/resolution/{binding-lookup-cache.js → cache/binding-lookup-cache.js} +0 -0
@@ -1,55 +1,63 @@
1
- import type { Binding, ConstantBinding, DynamicAsyncBinding, DynamicBinding } from "#/binding";
2
- import { NO_INSTANCE } from "#/binding";
1
+ import type { AmbientResolution } from "#/ambient/active-container";
3
2
  import type { Container } from "#/container/container";
3
+ import type { Binding, ConstantBinding, DynamicAsyncBinding, DynamicBinding } from "#/core/binding";
4
+ import { NO_INSTANCE } from "#/core/binding";
5
+ import type { BindingRegistry } from "#/core/registry";
6
+ import { NO_TAG_KEYS } from "#/core/tag";
7
+ import type { Token } from "#/core/token";
8
+ import { tokenName } from "#/core/token";
9
+ import type {
10
+ ActivationHandler,
11
+ BindingIdentifier,
12
+ BindingTag,
13
+ ConstraintContext,
14
+ Constructor,
15
+ ResolutionFrame,
16
+ ResolveOptions,
17
+ } from "#/core/types";
18
+ import type { ResolutionDiagnostics } from "#/errors/diagnostics";
4
19
  import {
5
20
  AsyncActivationError,
6
21
  AsyncResolutionError,
7
22
  CircularDependencyError,
23
+ DisposedContainerError,
8
24
  InternalError,
9
25
  MissingMetadataError,
10
26
  MissingScopeContextError,
11
27
  NoMatchingBindingError,
12
28
  TokenNotBoundError,
13
- } from "#/errors";
29
+ } from "#/errors/errors";
30
+ import type { DependencySlot } from "#/injection/resolve-options";
31
+ import { isNameOnlyOptions, resolveOptionsForSlot, singleTagOnlyOf } from "#/injection/resolve-options";
32
+ import type { LifecycleManager } from "#/lifecycle/lifecycle-manager";
33
+ import type { ScopeManager } from "#/lifecycle/scope-manager";
34
+ import { SCOPED_MISS } from "#/lifecycle/scope-manager";
14
35
  import type { MetadataReader, ParamMetadata } from "#/metadata/metadata-types";
15
- import type { BindingRegistry } from "#/registry";
16
- import { ActivationNeedCache } from "#/resolution/activation-need";
17
- import type { DefaultLookupEntry } from "#/resolution/binding-lookup-cache";
18
- import { BindingLookupCache } from "#/resolution/binding-lookup-cache";
19
- import { matchesSlot, selectAllBindings, selectBinding } from "#/resolution/binding-select";
20
- import { ClassIntrospector } from "#/resolution/class-introspector";
21
- import type { ResolutionDiagnostics } from "#/resolution/diagnostics";
22
- import type { ResolverCallbacks } from "#/resolution/environment";
36
+ import { ActivationNeedCache } from "#/resolution/cache/activation-need";
37
+ import type { DefaultLookupEntry } from "#/resolution/cache/binding-lookup-cache";
38
+ import { BindingLookupCache } from "#/resolution/cache/binding-lookup-cache";
39
+ import { ClassIntrospector } from "#/resolution/cache/class-introspector";
40
+ import type { ResolverCallbacks } from "#/resolution/context";
23
41
  import {
24
42
  AsyncCascadeContext,
25
43
  AsyncLevelContext,
26
44
  buildResolutionFrame,
27
45
  DefaultResolutionContext,
28
- } from "#/resolution/environment";
29
- import { InstantiationPlanCompiler, PLAN_RETRY } from "#/resolution/instantiation-plan";
30
- import type { LifecycleManager } from "#/resolution/lifecycle";
31
- import type { BranchDepth, OwnedBranchPath } from "#/resolution/resolution-path";
46
+ } from "#/resolution/context";
47
+ import type { BranchDepth, OwnedBranchStack } from "#/resolution/path/resolution-path";
32
48
  import {
33
49
  branchDepthOf,
50
+ cycleNamesOf,
34
51
  enterResolutionPath,
35
52
  extendResolutionBranch,
36
- extendResolutionStackBranch,
37
53
  ROOT_BRANCH,
38
54
  UNOWNED_BRANCH,
39
- } from "#/resolution/resolution-path";
40
- import type { DependencySlot } from "#/resolution/resolve-options";
41
- import { injectionSlotToResolveOptions, isNameOnlyOptions, singleTagOnlyOf } from "#/resolution/resolve-options";
42
- import type { ScopeManager } from "#/resolution/scope";
43
- import type { Token } from "#/token";
44
- import { tokenName } from "#/token";
45
- import type {
46
- ActivationHandler,
47
- BindingIdentifier,
48
- ConstraintContext,
49
- Constructor,
50
- ResolutionFrame,
51
- ResolveOptions,
52
- } from "#/types";
55
+ } from "#/resolution/path/resolution-path";
56
+ import { InstantiationPlanCompiler, PLAN_RETRY } from "#/resolution/plan/instantiation-plan";
57
+ import { matchesSlot, requestedTagKeyMask, selectAllBindings, selectBinding } from "#/resolution/select/binding-select";
58
+
59
+ // Where a multi-tag resolve switches from scanning the token's list to walking the tag indexes.
60
+ const MULTI_TAG_INDEX_THRESHOLD = 8;
53
61
 
54
62
  const EMPTY_STRING_LIST: ReadonlyArray<string> = [];
55
63
  const EMPTY_FRAME_LIST: ReadonlyArray<ResolutionFrame> = [];
@@ -67,26 +75,29 @@ const ROOT_CONSTRAINT_CONTEXT = {
67
75
  */
68
76
  export class DependencyResolver implements ResolverCallbacks {
69
77
  readonly #syncResolutionContextPool: Array<DefaultResolutionContext> = [];
78
+ // Contexts bound to the cascade pair — deferred: only an async cascade's sync resolves need it.
79
+ #cascadeContextPool: Array<DefaultResolutionContext> | undefined;
70
80
  /**
71
- * The pair a top-level **sync** resolve reuses instead of minting two arrays per call.
81
+ * The stack a top-level **sync** resolve reuses instead of minting an array per call.
72
82
  *
73
- * @remarks Read directly rather than through an accessor returning both: a shallow resolve is one
74
- * top-level call, so a call and an object literal there are not amortised over anything. Every sync
75
- * lane pops what it pushes, so `rootStack.length === 0` means no resolve holds the pair; async
76
- * appends without popping and mints its own. Keeping the pair stable is also what lets a pooled
77
- * context skip storing pointers it already holds.
83
+ * @remarks Read directly rather than through an accessor: a shallow resolve is one top-level
84
+ * call, so a call there is not amortised over anything. Every sync lane pops what it pushes, so
85
+ * `rootStack.length === 0` means no resolve holds the stack; async appends without popping and
86
+ * mints its own. Keeping it stable is also what lets a pooled context skip re-storing it.
78
87
  */
79
- readonly rootPath: Array<string> = [];
80
88
  readonly rootStack: Array<ResolutionFrame> = [];
81
- // The open synchronous factory cascade: its arrays are the ancestor chain, and they are balanced
89
+ // The open synchronous factory cascade: its stack is the ancestor chain, and it is balanced
82
90
  // because synchronous code does not interleave.
83
- readonly #cascadePath: Array<string> = [];
84
91
  readonly #cascadeStack: Array<ResolutionFrame> = [];
85
92
  #cascadeContext: AsyncCascadeContext | undefined;
86
93
  // Compiled plans; `null` marks a binding as unplannable under the current cache versions.
87
94
  readonly #classPlanByBindingId = new Map<BindingIdentifier, (() => unknown) | null>();
88
95
  #classPlanRegistryVersion = -1;
89
96
  #classPlanActivationVersion = -1;
97
+ // The async lane's plans, stamped and invalidated apart so neither lane pays the other's misses.
98
+ readonly #asyncPlanByBindingId = new Map<BindingIdentifier, (() => unknown) | null>();
99
+ #asyncPlanRegistryVersion = -1;
100
+ #asyncPlanActivationVersion = -1;
90
101
 
91
102
  readonly #registry: BindingRegistry;
92
103
  readonly #scope: ScopeManager;
@@ -119,26 +130,40 @@ export class DependencyResolver implements ResolverCallbacks {
119
130
  this.#activation = new ActivationNeedCache(lifecycle, this.#classes, registry);
120
131
  }
121
132
 
122
- /** Structural counts for {@link RESOLUTION_DIAGNOSTICS}; see `resolution/diagnostics.ts`. */
123
- describeCaches(): Pick<ResolutionDiagnostics, "compiledPlanCount" | "syncContextPoolSize"> {
133
+ /** The reader this resolver was built with, which is the one its container answers with. */
134
+ get metadataReader(): MetadataReader {
135
+ return this.#metadataReader;
136
+ }
137
+
138
+ /** Structural counts for the {@link ResolutionDiagnostics} a container reports. */
139
+ describeCaches(): Pick<
140
+ ResolutionDiagnostics,
141
+ "compiledPlanCount" | "compiledAsyncPlanCount" | "syncContextPoolSize"
142
+ > {
124
143
  let compiledPlanCount = 0;
125
144
  for (const plan of this.#classPlanByBindingId.values()) {
126
145
  if (plan !== null) {
127
146
  compiledPlanCount += 1;
128
147
  }
129
148
  }
149
+ let compiledAsyncPlanCount = 0;
150
+ for (const plan of this.#asyncPlanByBindingId.values()) {
151
+ if (plan !== null) {
152
+ compiledAsyncPlanCount += 1;
153
+ }
154
+ }
130
155
  return {
131
156
  compiledPlanCount,
157
+ compiledAsyncPlanCount,
132
158
  syncContextPoolSize: this.#syncResolutionContextPool.length,
133
159
  };
134
160
  }
135
161
 
136
- // ── Binding lookup ─────────────────────────────────────────────────────────
162
+ // ── Binding lookup ─────────────────────────────────────────────────────────────────────────────────────────────────
137
163
 
138
164
  #findBinding(
139
165
  token: Token<unknown> | Constructor,
140
166
  options: ResolveOptions | undefined,
141
- resolutionPath: Array<string>,
142
167
  resolutionStack: Array<ResolutionFrame>,
143
168
  ): DefaultLookupEntry<DependencyResolver> | undefined {
144
169
  if (options === undefined) {
@@ -148,19 +173,32 @@ export class DependencyResolver implements ResolverCallbacks {
148
173
  }
149
174
  } else if (isNameOnlyOptions(options)) {
150
175
  const namedBinding = this.#registry.getSimpleNamed(token, options.name);
151
- if (
152
- namedBinding !== undefined &&
153
- this.#matchesBindingFast(namedBinding, options, resolutionPath, resolutionStack)
154
- ) {
176
+ if (namedBinding !== undefined && this.#matchesBindingFast(namedBinding, options, resolutionStack)) {
155
177
  return { binding: namedBinding, owner: this };
156
178
  }
157
179
  } else {
158
180
  const singleTag = singleTagOnlyOf(options);
159
181
  if (singleTag !== undefined) {
160
- const tagged = this.#registry.getSimpleTagged(token, singleTag[0], singleTag[1]);
161
- if (tagged !== undefined && matchesIndexedTagValue(tagged, singleTag[1])) {
182
+ const tagged = this.#registry.getSimpleTagged(token, singleTag);
183
+ if (tagged !== undefined && this.#satisfiesPredicate(tagged, options, resolutionStack)) {
162
184
  return { binding: tagged, owner: this };
163
185
  }
186
+ } else if (
187
+ options.name === undefined &&
188
+ // A threshold switches the data structure, never the semantics: under it the generic scan
189
+ // below beats walking the indexes, and both paths answer identically. Sized first, so a
190
+ // small list pays one length read and nothing else.
191
+ this.#registry.getAll(token).length > MULTI_TAG_INDEX_THRESHOLD &&
192
+ requestedTagKeyMask(options) !== NO_TAG_KEYS
193
+ ) {
194
+ // A name-less multi-tag request matches only name-less tagged slots, and every such slot
195
+ // lives in one of the two tag indexes — so their union is the whole candidate set and the
196
+ // token's full list never needs scanning. Selection still owns predicates and specificity.
197
+ const selected = this.#selectMultiTagged(token, options, resolutionStack);
198
+ if (selected !== undefined) {
199
+ return { binding: selected, owner: this };
200
+ }
201
+ return this.#parent === undefined ? undefined : this.#parent.#findBinding(token, options, resolutionStack);
164
202
  }
165
203
  }
166
204
 
@@ -170,21 +208,16 @@ export class DependencyResolver implements ResolverCallbacks {
170
208
  // specificity to weigh and no ambiguity to report.
171
209
  const selected =
172
210
  bindings.length === 1
173
- ? this.#matchesBindingFast(bindings[0]!, options, resolutionPath, resolutionStack)
211
+ ? this.#matchesBindingFast(bindings[0]!, options, resolutionStack)
174
212
  ? bindings[0]
175
213
  : undefined
176
- : selectBinding(
177
- bindings,
178
- options,
179
- this.#makeConstraintContext(resolutionPath, resolutionStack, options),
180
- tokenName(token),
181
- );
214
+ : selectBinding(bindings, options, this.#makeConstraintContext(resolutionStack, options), tokenName(token));
182
215
  if (selected !== undefined) {
183
216
  return { binding: selected, owner: this };
184
217
  }
185
218
  }
186
219
  if (this.#parent !== undefined) {
187
- return this.#parent.#findBinding(token, options, resolutionPath, resolutionStack);
220
+ return this.#parent.#findBinding(token, options, resolutionStack);
188
221
  }
189
222
  return undefined;
190
223
  }
@@ -198,12 +231,11 @@ export class DependencyResolver implements ResolverCallbacks {
198
231
  #requireBinding(
199
232
  token: Token<unknown> | Constructor,
200
233
  options: ResolveOptions | undefined,
201
- resolutionPath: Array<string>,
202
234
  resolutionStack: Array<ResolutionFrame>,
203
235
  ): DefaultLookupEntry<DependencyResolver> {
204
236
  let currentToken = token;
205
237
  let visitedAliasTokens: Set<Token<unknown> | Constructor> | undefined;
206
- let found = this.#findBinding(currentToken, options, resolutionPath, resolutionStack);
238
+ let found = this.#findBinding(currentToken, options, resolutionStack);
207
239
 
208
240
  while (found !== undefined && found.binding.kind === "alias") {
209
241
  const target = found.binding.target;
@@ -213,7 +245,7 @@ export class DependencyResolver implements ResolverCallbacks {
213
245
  }
214
246
  visitedAliasTokens.add(target);
215
247
  currentToken = target;
216
- found = this.#findBinding(currentToken, options, resolutionPath, resolutionStack);
248
+ found = this.#findBinding(currentToken, options, resolutionStack);
217
249
  }
218
250
 
219
251
  if (found === undefined) {
@@ -238,7 +270,7 @@ export class DependencyResolver implements ResolverCallbacks {
238
270
  token: Token<unknown> | Constructor,
239
271
  options: ResolveOptions | undefined,
240
272
  ): DefaultLookupEntry<DependencyResolver> | undefined {
241
- return this.#findBinding(token, options, [], []);
273
+ return this.#findBinding(token, options, []);
242
274
  }
243
275
 
244
276
  /**
@@ -248,47 +280,40 @@ export class DependencyResolver implements ResolverCallbacks {
248
280
  token: Token<unknown> | Constructor,
249
281
  options: ResolveOptions | undefined,
250
282
  ): ReadonlyArray<Binding> {
251
- return this.#candidateBindings(token, options, [], []);
283
+ return this.#candidateBindings(token, options, []);
252
284
  }
253
285
 
254
- // ── Sync resolve ───────────────────────────────────────────────────────────
286
+ // ── Sync resolve ───────────────────────────────────────────────────────────────────────────────────────────────────
255
287
 
256
- resolveFromContext<const Value>(
257
- token: Token<Value> | Constructor<Value>,
258
- resolutionPath: Array<string>,
259
- resolutionStack: Array<ResolutionFrame>,
260
- ): Value {
288
+ resolveFromContext<Value>(token: Token<Value> | Constructor<Value>, resolutionStack: Array<ResolutionFrame>): Value {
261
289
  // Hot lane: own-registry fast default. Fall back to the chain-versioned memo
262
290
  // (parent-chain walk + alias folding) only on miss or alias.
263
291
  const fastBinding = this.#registry.getFastDefault(token);
264
292
  if (fastBinding !== undefined && fastBinding.kind !== "alias") {
265
- return this.#resolveDefaultEntry(fastBinding, this, resolutionPath, resolutionStack) as Value;
293
+ return this.#resolveDefaultEntry(fastBinding, this, resolutionStack) as Value;
266
294
  }
267
295
  const entry = this.#lookup.defaultEntry(token);
268
296
  if (entry === null) {
269
- return this.resolve(token, undefined, resolutionPath, resolutionStack);
297
+ return this.resolve(token, undefined, resolutionStack);
270
298
  }
271
- return this.#resolveDefaultEntry(entry.binding, entry.owner, resolutionPath, resolutionStack) as Value;
299
+ return this.#resolveDefaultEntry(entry.binding, entry.owner, resolutionStack) as Value;
272
300
  }
273
301
 
274
- #resolveDefaultEntry(
275
- binding: Binding,
276
- owner: DependencyResolver,
277
- resolutionPath: Array<string>,
278
- resolutionStack: Array<ResolutionFrame>,
279
- ): unknown {
302
+ #resolveDefaultEntry(binding: Binding, owner: DependencyResolver, resolutionStack: Array<ResolutionFrame>): unknown {
280
303
  const scope = binding.scope;
281
304
  if (scope === "transient") {
282
305
  if (binding.kind === "dynamic") {
306
+ // Container-level hooks belong to the binding's owner — a child-registered hook must not
307
+ // fire for a parent-owned binding, and the owner's must.
283
308
  const containerHooks =
284
- this.#lifecycle.activationVersion === 0 ? undefined : this.#lifecycle.activationHandlersFor(binding.token);
309
+ owner.#lifecycle.activationVersion === 0 ? undefined : owner.#lifecycle.activationHandlersFor(binding.token);
285
310
  if (binding.onActivation === undefined && (containerHooks === undefined || containerHooks.length === 0)) {
286
- return this.#resolveTransientDynamicSyncFromContext(binding, resolutionPath, resolutionStack);
311
+ return this.#resolveTransientDynamicSyncFromContext(binding, resolutionStack);
287
312
  }
288
- return this.#resolveTransientDynamicActivatedSync(binding, containerHooks, resolutionPath, resolutionStack);
313
+ return this.#resolveTransientDynamicActivatedSync(binding, containerHooks, resolutionStack);
289
314
  }
290
315
  // Compiled plans only run at the top level — inner levels keep the runtime cycle guard.
291
- if ((binding.kind === "class" || binding.kind === "resolved") && resolutionPath.length === 0) {
316
+ if ((binding.kind === "class" || binding.kind === "resolved") && resolutionStack.length === 0) {
292
317
  const plan = this.#getInstantiationPlan(binding);
293
318
  if (plan !== null) {
294
319
  return plan();
@@ -296,7 +321,7 @@ export class DependencyResolver implements ResolverCallbacks {
296
321
  }
297
322
  } else if (scope === "singleton") {
298
323
  // A constant is a singleton that is already its own instance.
299
- if (this.#isPlainConstant(binding)) {
324
+ if (owner.#isPlainConstant(binding)) {
300
325
  return binding.value;
301
326
  }
302
327
  const cachedSingleton = binding.instance;
@@ -304,7 +329,7 @@ export class DependencyResolver implements ResolverCallbacks {
304
329
  return cachedSingleton;
305
330
  }
306
331
  if (owner !== this) {
307
- return owner.#resolveBinding(binding, undefined, resolutionPath, resolutionStack);
332
+ return owner.#resolveBinding(binding, undefined, resolutionStack, owner);
308
333
  }
309
334
  } else {
310
335
  const cachedScoped = this.#readScoped(binding);
@@ -312,7 +337,7 @@ export class DependencyResolver implements ResolverCallbacks {
312
337
  return cachedScoped;
313
338
  }
314
339
  }
315
- return this.#resolveBinding(binding, undefined, resolutionPath, resolutionStack);
340
+ return this.#resolveBinding(binding, undefined, resolutionStack, owner);
316
341
  }
317
342
 
318
343
  // Lean lane for an activated transient dynamic binding: same observable behavior as the
@@ -321,24 +346,22 @@ export class DependencyResolver implements ResolverCallbacks {
321
346
  #resolveTransientDynamicActivatedSync(
322
347
  binding: DynamicBinding<unknown>,
323
348
  containerHooks: ReadonlyArray<ActivationHandler<unknown>> | undefined,
324
- resolutionPath: Array<string>,
325
349
  resolutionStack: Array<ResolutionFrame>,
326
350
  ): unknown {
327
351
  // Same O(1) cycle guard as the unhooked lane: this is still one sync call stack, so the flag
328
- // *is* exact path membership — see ARCHITECTURE.md.
352
+ // *is* exact path membership.
329
353
  const frame = this.#getResolutionFrame(binding);
330
354
  const tokenDisplayName = frame.tokenName;
331
355
  if (binding.inFlight) {
332
- throw new CircularDependencyError([...resolutionPath, tokenDisplayName]);
356
+ throw new CircularDependencyError(cycleNamesOf(resolutionStack, tokenDisplayName));
333
357
  }
334
358
  binding.inFlight = true;
335
- resolutionPath.push(tokenDisplayName);
336
359
  resolutionStack.push(frame);
337
360
  try {
338
- const resolutionCtx = this.#acquireSyncResolutionContext(resolutionPath, resolutionStack, undefined);
361
+ const resolutionCtx = this.#acquireSyncResolutionContext(resolutionStack, undefined);
339
362
  const factoryResult = binding.factory(resolutionCtx);
340
363
  if (factoryResult instanceof Promise) {
341
- throw new AsyncResolutionError(tokenDisplayName, tokenDisplayName);
364
+ throw new AsyncResolutionError(resolutionStack[0]?.tokenName ?? tokenDisplayName, tokenDisplayName);
342
365
  }
343
366
  let activated = factoryResult;
344
367
  if (binding.onActivation !== undefined) {
@@ -360,14 +383,22 @@ export class DependencyResolver implements ResolverCallbacks {
360
383
  return activated;
361
384
  } finally {
362
385
  resolutionStack.pop();
363
- resolutionPath.pop();
364
386
  binding.inFlight = false;
365
387
  }
366
388
  }
367
389
 
390
+ /** Chain-summed activation version: a plan can inline a parent-owned binding, so a parent's hook registration must invalidate it. */
391
+ #chainActivationVersion(): number {
392
+ let version = this.#lifecycle.activationVersion;
393
+ for (let current = this.#parent; current !== undefined; current = current.#parent) {
394
+ version += current.#lifecycle.activationVersion;
395
+ }
396
+ return version;
397
+ }
398
+
368
399
  #getInstantiationPlan(binding: Binding & { kind: "class" | "resolved" }): (() => unknown) | null {
369
400
  const registryVersion = this.#lookup.chainVersion();
370
- const activationVersion = this.#lifecycle.activationVersion;
401
+ const activationVersion = this.#chainActivationVersion();
371
402
  if (registryVersion !== this.#classPlanRegistryVersion || activationVersion !== this.#classPlanActivationVersion) {
372
403
  this.#classPlanByBindingId.clear();
373
404
  this.#classPlanRegistryVersion = registryVersion;
@@ -388,7 +419,7 @@ export class DependencyResolver implements ResolverCallbacks {
388
419
 
389
420
  // Compiler behind #getInstantiationPlan — cold path, so the host indirection costs nothing hot.
390
421
  readonly #planCompiler = new InstantiationPlanCompiler({
391
- hasActivationHandlers: (token) => this.#lifecycle.hasActivationHandlers(token),
422
+ hasActivationHandlers: (binding) => this.#ownerOf(binding).#lifecycle.hasActivationHandlers(binding.token),
392
423
  knownPostConstruct: (target) => this.#classes.knownPostConstruct(target),
393
424
  needsActiveContainer: (target) => this.#classes.needsActiveContainer(target),
394
425
  getConstructorMetadata: (target) => this.#classes.constructorMetadata(target),
@@ -396,27 +427,72 @@ export class DependencyResolver implements ResolverCallbacks {
396
427
  const entry = this.#lookup.defaultEntry(token);
397
428
  return entry === null ? null : { binding: entry.binding };
398
429
  },
430
+ // Exactly what #findBinding's named lane accepts, minus the half that reads a path: a predicate
431
+ // is the compiler's cue to leave the selection to the runtime.
432
+ lookupPathIndependentNamedEntry: (token, options) => {
433
+ const entry = this.#lookup.namedEntry(token, options.name);
434
+ if (entry === null || entry.binding.predicate !== undefined || !matchesSlot(entry.binding.slot, options)) {
435
+ return null;
436
+ }
437
+ return { binding: entry.binding };
438
+ },
399
439
  getResolutionFrame: (binding) => this.#getResolutionFrame(binding),
400
440
  // Dispatches exactly as #resolveDep does, so an escaped dep is indistinguishable
401
441
  // from the same dep on a fully interpreted resolve.
402
- resolveEscaped: (token, options, arity, resolutionPath, resolutionStack) => {
442
+ resolveEscaped: (token, options, arity, resolutionStack) => {
403
443
  if (arity === "all") {
404
- return this.resolveAll(token, options, resolutionPath, resolutionStack);
444
+ return this.resolveAll(token, options, resolutionStack);
405
445
  }
406
446
  if (arity === "optional") {
407
- return this.resolveOptional(token, options, resolutionPath, resolutionStack);
447
+ return this.resolveOptional(token, options, resolutionStack);
408
448
  }
409
449
  if (options === undefined) {
410
- return this.resolveFromContext(token, resolutionPath, resolutionStack);
450
+ return this.resolveFromContext(token, resolutionStack);
411
451
  }
412
- return this.resolve(token, options, resolutionPath, resolutionStack);
452
+ return this.resolve(token, options, resolutionStack);
453
+ },
454
+ // Dispatches exactly as #resolveDepAsync does, for the async lane's escapes.
455
+ resolveEscapedAsync: (token, options, arity, resolutionStack) => {
456
+ if (arity === "all") {
457
+ return this.resolveAllAsync(token, options, resolutionStack, UNOWNED_BRANCH);
458
+ }
459
+ if (arity === "optional") {
460
+ return this.resolveOptionalAsync(token, options, resolutionStack, UNOWNED_BRANCH);
461
+ }
462
+ if (options === undefined) {
463
+ return this.resolveAsyncFromContext(token, resolutionStack, UNOWNED_BRANCH);
464
+ }
465
+ return this.resolveAsync(token, options, resolutionStack, UNOWNED_BRANCH);
413
466
  },
414
467
  });
415
468
 
416
- resolve<const Value>(
469
+ /** The async lane's plan for a statically-visible transient binding, mirroring the sync getter. */
470
+ #getAsyncInstantiationPlan(
471
+ binding: Binding & { kind: "class" | "resolved" | "resolved-async" },
472
+ ): (() => unknown) | null {
473
+ const registryVersion = this.#lookup.chainVersion();
474
+ const activationVersion = this.#chainActivationVersion();
475
+ if (registryVersion !== this.#asyncPlanRegistryVersion || activationVersion !== this.#asyncPlanActivationVersion) {
476
+ this.#asyncPlanByBindingId.clear();
477
+ this.#asyncPlanRegistryVersion = registryVersion;
478
+ this.#asyncPlanActivationVersion = activationVersion;
479
+ }
480
+ const cached = this.#asyncPlanByBindingId.get(binding.id);
481
+ if (cached !== undefined) {
482
+ return cached;
483
+ }
484
+ const compiled = this.#planCompiler.compileAsync(binding);
485
+ if (compiled === PLAN_RETRY) {
486
+ // Lifecycle metadata not discovered yet — the fallback resolve discovers it; retry then.
487
+ return null;
488
+ }
489
+ this.#asyncPlanByBindingId.set(binding.id, compiled);
490
+ return compiled;
491
+ }
492
+
493
+ resolve<Value>(
417
494
  token: Token<Value> | Constructor<Value>,
418
495
  options: ResolveOptions | undefined,
419
- resolutionPath: Array<string>,
420
496
  resolutionStack: Array<ResolutionFrame>,
421
497
  ): Value {
422
498
  // Name-only fast lane: memoized lookup, dispatching just the shapes whose
@@ -425,7 +501,7 @@ export class DependencyResolver implements ResolverCallbacks {
425
501
  const namedEntry = this.#lookup.namedEntry(token, options.name);
426
502
  if (namedEntry !== null) {
427
503
  const namedBinding = namedEntry.binding;
428
- if (this.#isPlainConstant(namedBinding)) {
504
+ if (namedEntry.owner.#isPlainConstant(namedBinding)) {
429
505
  return namedBinding.value as Value;
430
506
  }
431
507
  if (namedBinding.scope === "singleton" && namedBinding.instance !== NO_INSTANCE) {
@@ -435,22 +511,22 @@ export class DependencyResolver implements ResolverCallbacks {
435
511
  }
436
512
  }
437
513
 
438
- const { binding, owner } = this.#requireBinding(token, options, resolutionPath, resolutionStack);
514
+ const { binding, owner } = this.#requireBinding(token, options, resolutionStack);
439
515
 
440
516
  // A singleton owned by a parent resolver is resolved there, so the parent caches it.
441
517
  if (binding.scope === "singleton" && owner !== this) {
442
- return owner.#resolveBinding(binding, options, resolutionPath, resolutionStack) as Value;
518
+ return owner.#resolveBinding(binding, options, resolutionStack, owner) as Value;
443
519
  }
444
- return this.#resolveBinding(binding, options, resolutionPath, resolutionStack) as Value;
520
+ return this.#resolveBinding(binding, options, resolutionStack, owner) as Value;
445
521
  }
446
522
 
447
523
  #resolveBinding(
448
524
  binding: Binding,
449
525
  options: ResolveOptions | undefined,
450
- resolutionPath: Array<string>,
451
526
  resolutionStack: Array<ResolutionFrame>,
527
+ owner: DependencyResolver,
452
528
  ): unknown {
453
- if (this.#isPlainConstant(binding)) {
529
+ if (owner.#isPlainConstant(binding)) {
454
530
  return binding.value;
455
531
  }
456
532
 
@@ -459,62 +535,91 @@ export class DependencyResolver implements ResolverCallbacks {
459
535
  if (binding.instance !== NO_INSTANCE) {
460
536
  return binding.instance;
461
537
  }
538
+ // An async materialization already in flight must not be raced by a second, sync one.
539
+ if (this.#scope.getInflight(binding.id) !== undefined) {
540
+ throw new AsyncResolutionError(
541
+ resolutionStack[0]?.tokenName ?? tokenName(binding.token),
542
+ tokenName(binding.token),
543
+ );
544
+ }
545
+ if (this.#scope.isClosed) {
546
+ throw new DisposedContainerError();
547
+ }
462
548
  } else if (scope === "scoped") {
463
549
  const cachedScoped = this.#readScoped(binding);
464
550
  if (cachedScoped !== SCOPED_MISS) {
465
551
  return cachedScoped;
466
552
  }
553
+ if (this.#scope.getInflight(binding.id) !== undefined) {
554
+ throw new AsyncResolutionError(
555
+ resolutionStack[0]?.tokenName ?? tokenName(binding.token),
556
+ tokenName(binding.token),
557
+ );
558
+ }
559
+ if (this.#scope.isClosed) {
560
+ throw new DisposedContainerError();
561
+ }
467
562
  }
468
563
 
469
564
  const frame = this.#getResolutionFrame(binding);
470
565
  const tokenDisplayName = frame.tokenName;
471
- const resolutionSet = enterResolutionPath(resolutionPath, tokenDisplayName);
472
- resolutionStack.push(frame);
566
+ const resolutionSet = enterResolutionPath(resolutionStack, frame);
473
567
  try {
474
- const needsActivation = this.#activation.needsActivation(binding);
568
+ const needsActivation = owner.#activation.needsActivation(binding);
475
569
  if (!needsActivation && scope === "transient" && binding.kind === "dynamic") {
476
- const resolutionCtx = this.#acquireSyncResolutionContext(resolutionPath, resolutionStack, options);
570
+ const resolutionCtx = this.#acquireSyncResolutionContext(resolutionStack, options);
477
571
  const dynamicResult = binding.factory(resolutionCtx);
478
572
  if (dynamicResult instanceof Promise) {
479
- throw new AsyncResolutionError(tokenDisplayName, tokenDisplayName);
573
+ throw new AsyncResolutionError(resolutionStack[0]?.tokenName ?? tokenDisplayName, tokenDisplayName);
480
574
  }
481
575
  return dynamicResult;
482
576
  }
483
577
 
484
578
  const resolutionCtx =
485
579
  needsActivation || requiresResolutionContext(binding)
486
- ? this.#acquireSyncResolutionContext(resolutionPath, resolutionStack, options)
580
+ ? this.#acquireSyncResolutionContext(resolutionStack, options)
487
581
  : undefined;
488
582
 
489
- const instance = this.#instantiateSync(binding, resolutionCtx, resolutionPath, resolutionStack);
583
+ const instance = this.#instantiateSync(binding, resolutionCtx, resolutionStack);
490
584
 
491
- const activated = this.#activation.refreshAfterFirstInstantiation(binding, needsActivation)
492
- ? this.#lifecycle.runActivationSync(
585
+ this.#mirrorPostConstructFromOwner(binding, owner);
586
+ const activated = owner.#activation.refreshAfterFirstInstantiation(binding, needsActivation)
587
+ ? owner.#lifecycle.runActivationSync(
493
588
  resolutionCtx as DefaultResolutionContext,
494
589
  binding,
495
590
  instance,
496
- this.#metadataReader,
591
+ owner.#metadataReader,
497
592
  )
498
593
  : instance;
499
594
 
500
595
  if (scope === "singleton") {
501
596
  this.#scope.setSingleton(binding, activated);
502
597
  } else if (scope === "scoped") {
503
- this.#scope.setScoped(binding.id, activated);
598
+ this.#scope.setScoped(binding, activated);
504
599
  }
505
600
 
506
601
  return activated;
507
602
  } finally {
508
603
  resolutionStack.pop();
509
- resolutionPath.pop();
510
- resolutionSet?.delete(tokenDisplayName);
604
+ resolutionSet?.delete(frame.bindingId);
511
605
  }
512
606
  }
513
607
 
608
+ /** Path-continuing resolution handed to the ambient slot while an accessor class constructs. */
609
+ #ambientResolutionFor(resolutionStack: Array<ResolutionFrame>): AmbientResolution {
610
+ return {
611
+ resolve: <Value>(token: Token<Value> | Constructor<Value>, options?: ResolveOptions): Value =>
612
+ options === undefined
613
+ ? this.resolveFromContext(token, resolutionStack)
614
+ : this.resolve(token, options, resolutionStack),
615
+ resolveOptional: <Value>(token: Token<Value> | Constructor<Value>, options?: ResolveOptions): Value | undefined =>
616
+ this.resolveOptional(token, options, resolutionStack),
617
+ };
618
+ }
619
+
514
620
  #instantiateSync(
515
621
  binding: Binding,
516
622
  ctx: DefaultResolutionContext | undefined,
517
- resolutionPath: Array<string>,
518
623
  resolutionStack: Array<ResolutionFrame>,
519
624
  ): unknown {
520
625
  switch (binding.kind) {
@@ -527,30 +632,34 @@ export class DependencyResolver implements ResolverCallbacks {
527
632
  }
528
633
  const factoryResult = binding.factory(ctx);
529
634
  if (factoryResult instanceof Promise) {
530
- throw new AsyncResolutionError(tokenName(binding.token), tokenName(binding.token));
635
+ throw asyncResolutionErrorFor(binding, resolutionStack);
531
636
  }
532
637
  return factoryResult;
533
638
  }
534
639
 
535
640
  case "dynamic-async":
536
- throw new AsyncResolutionError(tokenName(binding.token), tokenName(binding.token));
641
+ throw asyncResolutionErrorFor(binding, resolutionStack);
537
642
 
538
643
  case "class": {
539
- const deps = this.#resolveDeps(this.#constructorParams(binding.target), resolutionPath, resolutionStack);
540
- return this.#classes.instantiate(binding.target, deps);
644
+ const deps = this.#resolveDeps(this.#constructorParams(binding.target), resolutionStack);
645
+ return this.#classes.instantiate(
646
+ binding.target,
647
+ deps,
648
+ this.#classes.needsActiveContainer(binding.target) ? this.#ambientResolutionFor(resolutionStack) : undefined,
649
+ );
541
650
  }
542
651
 
543
652
  case "resolved": {
544
- const deps = this.#resolveDeps(binding.deps, resolutionPath, resolutionStack);
653
+ const deps = this.#resolveDeps(binding.deps, resolutionStack);
545
654
  const factoryResult = binding.factory(...deps);
546
655
  if (factoryResult instanceof Promise) {
547
- throw new AsyncResolutionError(tokenName(binding.token), tokenName(binding.token));
656
+ throw asyncResolutionErrorFor(binding, resolutionStack);
548
657
  }
549
658
  return factoryResult;
550
659
  }
551
660
 
552
661
  case "resolved-async":
553
- throw new AsyncResolutionError(tokenName(binding.token), tokenName(binding.token));
662
+ throw asyncResolutionErrorFor(binding, resolutionStack);
554
663
 
555
664
  case "alias":
556
665
  throw new InternalError("alias should have been followed before instantiation");
@@ -576,66 +685,65 @@ export class DependencyResolver implements ResolverCallbacks {
576
685
 
577
686
  // One dispatch table for both dependency sources — constructor params and `toResolved`
578
687
  // descriptors declare the same four things.
579
- #resolveDeps(
580
- deps: ReadonlyArray<DependencySlot>,
581
- resolutionPath: Array<string>,
582
- resolutionStack: Array<ResolutionFrame>,
583
- ): Array<unknown> {
688
+ #resolveDeps(deps: ReadonlyArray<DependencySlot>, resolutionStack: Array<ResolutionFrame>): Array<unknown> {
584
689
  const count = deps.length;
585
690
  if (count === 0) {
586
691
  return [];
587
692
  }
588
693
  if (count === 1) {
589
- return [this.#resolveDep(deps[0]!, resolutionPath, resolutionStack)];
694
+ return [this.#resolveDep(deps[0]!, resolutionStack)];
590
695
  }
591
696
  const resolved = new Array<unknown>(count);
592
697
  for (let index = 0; index < count; index += 1) {
593
- resolved[index] = this.#resolveDep(deps[index]!, resolutionPath, resolutionStack);
698
+ resolved[index] = this.#resolveDep(deps[index]!, resolutionStack);
594
699
  }
595
700
  return resolved;
596
701
  }
597
702
 
598
- #resolveDep(dep: DependencySlot, resolutionPath: Array<string>, resolutionStack: Array<ResolutionFrame>): unknown {
599
- const options = injectionSlotToResolveOptions(dep);
703
+ #resolveDep(dep: DependencySlot, resolutionStack: Array<ResolutionFrame>): unknown {
704
+ const options = resolveOptionsForSlot(dep);
600
705
  if (dep.multi) {
601
- return this.resolveAll(dep.token, options, resolutionPath, resolutionStack);
706
+ return this.resolveAll(dep.token, options, resolutionStack);
602
707
  }
603
708
  if (dep.optional) {
604
- return this.resolveOptional(dep.token, options, resolutionPath, resolutionStack);
709
+ return this.resolveOptional(dep.token, options, resolutionStack);
605
710
  }
606
711
  if (options === undefined) {
607
- return this.resolveFromContext(dep.token, resolutionPath, resolutionStack);
712
+ return this.resolveFromContext(dep.token, resolutionStack);
608
713
  }
609
- return this.resolve(dep.token, options, resolutionPath, resolutionStack);
714
+ return this.resolve(dep.token, options, resolutionStack);
610
715
  }
611
716
 
612
- resolveOptional<const Value>(
717
+ resolveOptional<Value>(
613
718
  token: Token<Value> | Constructor<Value>,
614
719
  options: ResolveOptions | undefined,
615
- resolutionPath: Array<string>,
616
720
  resolutionStack: Array<ResolutionFrame>,
617
721
  ): Value | undefined {
618
- if (this.#findBinding(token, options, resolutionPath, resolutionStack) === undefined) {
722
+ const entry = this.#findBinding(token, options, resolutionStack);
723
+ if (entry === undefined) {
619
724
  return undefined;
620
725
  }
621
- return this.resolve(token, options, resolutionPath, resolutionStack);
726
+ // Resolve the entry the probe found: re-looking the token up would evaluate every `when()`
727
+ // predicate a second time, and a changed answer would throw where `undefined` was promised.
728
+ const { binding, owner } = entry;
729
+ if (binding.kind === "alias") {
730
+ return this.resolve(token, options, resolutionStack);
731
+ }
732
+ if (binding.scope === "singleton" && owner !== this) {
733
+ return owner.#resolveBinding(binding, options, resolutionStack, owner) as Value;
734
+ }
735
+ return this.#resolveBinding(binding, options, resolutionStack, owner) as Value;
622
736
  }
623
737
 
624
- resolveAll<const Value>(
738
+ resolveAll<Value>(
625
739
  token: Token<Value> | Constructor<Value>,
626
740
  options: ResolveOptions | undefined,
627
- resolutionPath: Array<string>,
628
741
  resolutionStack: Array<ResolutionFrame>,
629
742
  ): Array<Value> {
630
- const candidates = this.#candidateBindings(token, options, resolutionPath, resolutionStack);
743
+ const candidates = this.#candidateBindings(token, options, resolutionStack);
631
744
  const resolved = new Array<Value>(candidates.length);
632
745
  for (let index = 0; index < candidates.length; index += 1) {
633
- resolved[index] = this.#resolveCandidateSync(
634
- candidates[index]!,
635
- options,
636
- resolutionPath,
637
- resolutionStack,
638
- ) as Value;
746
+ resolved[index] = this.#resolveCandidateSync(candidates[index]!, options, resolutionStack) as Value;
639
747
  }
640
748
  return resolved;
641
749
  }
@@ -644,34 +752,25 @@ export class DependencyResolver implements ResolverCallbacks {
644
752
  #candidateBindings(
645
753
  token: Token<unknown> | Constructor,
646
754
  options: ResolveOptions | undefined,
647
- resolutionPath: Array<string>,
648
755
  resolutionStack: Array<ResolutionFrame>,
649
756
  ): ReadonlyArray<Binding> {
650
- if (options !== undefined && isNameOnlyOptions(options)) {
651
- // The name index has matched the slot already, but a hit may still carry a predicate —
652
- // and that is the selection path's job to evaluate.
653
- const named = this.#namedBindingsFromChain(token, options.name);
654
- if (!anyPredicate(named)) {
655
- return named;
757
+ if (options !== undefined) {
758
+ const indexed = this.#indexedCandidates(token, options, resolutionStack);
759
+ if (indexed !== null) {
760
+ return indexed;
656
761
  }
657
- return selectAllBindings(named, options, this.#makeConstraintContext(resolutionPath, resolutionStack, options));
658
762
  }
659
763
  const allBindings = this.#allBindingsFromChain(token);
660
764
  if (allBindings.length === 0) {
661
765
  return allBindings;
662
766
  }
663
- return selectAllBindings(
664
- allBindings,
665
- options,
666
- this.#makeConstraintContext(resolutionPath, resolutionStack, options),
667
- );
767
+ return selectAllBindings(allBindings, options, this.#makeConstraintContext(resolutionStack, options));
668
768
  }
669
769
 
670
- // ── Async resolve ──────────────────────────────────────────────────────────
770
+ // ── Async resolve ──────────────────────────────────────────────────────────────────────────────────────────────────
671
771
 
672
- resolveAsyncFromContext<const Value>(
772
+ resolveAsyncFromContext<Value>(
673
773
  token: Token<Value> | Constructor<Value>,
674
- resolutionPath: Array<string>,
675
774
  resolutionStack: Array<ResolutionFrame>,
676
775
  branchDepth: BranchDepth,
677
776
  ): Promise<Value> {
@@ -687,88 +786,74 @@ export class DependencyResolver implements ResolverCallbacks {
687
786
  ) {
688
787
  return this.#resolveTransientDynamicAsyncFromContext(
689
788
  fastBinding,
690
- resolutionPath,
691
789
  resolutionStack,
692
790
  branchDepth,
693
791
  ) as Promise<Value>;
694
792
  }
695
- return this.#resolveAsyncDefaultEntry(
696
- fastBinding,
697
- this,
698
- resolutionPath,
699
- resolutionStack,
700
- branchDepth,
701
- ) as Promise<Value>;
793
+ return this.#resolveAsyncDefaultEntry(fastBinding, this, resolutionStack, branchDepth) as Promise<Value>;
702
794
  }
703
795
  const entry = this.#lookup.defaultEntry(token);
704
796
  if (entry === null) {
705
- return this.resolveAsync(token, undefined, resolutionPath, resolutionStack, branchDepth);
706
- }
707
- return this.#resolveAsyncDefaultEntry(
708
- entry.binding,
709
- entry.owner,
710
- resolutionPath,
711
- resolutionStack,
712
- branchDepth,
713
- ) as Promise<Value>;
797
+ return this.resolveAsync(token, undefined, resolutionStack, branchDepth);
798
+ }
799
+ return this.#resolveAsyncDefaultEntry(entry.binding, entry.owner, resolutionStack, branchDepth) as Promise<Value>;
714
800
  }
715
801
 
716
802
  #resolveAsyncDefaultEntry(
717
803
  binding: Binding,
718
804
  owner: DependencyResolver,
719
- resolutionPath: Array<string>,
720
805
  resolutionStack: Array<ResolutionFrame>,
721
806
  branchDepth: BranchDepth,
722
807
  ): Promise<unknown> {
723
- if (this.#isPlainConstant(binding)) {
808
+ if (owner.#isPlainConstant(binding)) {
724
809
  return Promise.resolve(binding.value);
725
810
  }
726
811
  const scope = binding.scope;
727
812
  if (scope === "transient") {
728
- if ((binding.kind === "dynamic" || binding.kind === "dynamic-async") && !this.#hasAnyActivation(binding)) {
729
- return this.#resolveTransientDynamicAsyncFromContext(binding, resolutionPath, resolutionStack, branchDepth);
813
+ if ((binding.kind === "dynamic" || binding.kind === "dynamic-async") && !owner.#hasAnyActivation(binding)) {
814
+ return this.#resolveTransientDynamicAsyncFromContext(binding, resolutionStack, branchDepth);
730
815
  }
731
816
  } else if (scope === "singleton") {
732
817
  if (binding.instance !== NO_INSTANCE) {
733
818
  return Promise.resolve(binding.instance);
734
819
  }
735
820
  if (owner !== this) {
736
- return owner.#resolveBindingAsync(binding, undefined, resolutionPath, resolutionStack, branchDepth);
821
+ return owner.#resolveBindingAsync(binding, undefined, resolutionStack, branchDepth, owner);
737
822
  }
738
823
  } else if (this.#scope.isChild) {
739
- if (this.#scope.hasScoped(binding.id)) {
740
- return Promise.resolve(this.#scope.getScoped(binding.id));
824
+ const cachedScoped = this.#scope.readScoped(binding.id);
825
+ if (cachedScoped !== SCOPED_MISS) {
826
+ return Promise.resolve(cachedScoped);
741
827
  }
742
828
  } else {
743
829
  // Not `#readScoped`: this entry point reports failure as a rejection, never a sync throw.
744
830
  return Promise.reject(new MissingScopeContextError(tokenName(binding.token)));
745
831
  }
746
- return this.#resolveBindingAsync(binding, undefined, resolutionPath, resolutionStack, branchDepth);
832
+ return this.#resolveBindingAsync(binding, undefined, resolutionStack, branchDepth, owner);
747
833
  }
748
834
 
749
- async resolveAsync<const Value>(
835
+ async resolveAsync<Value>(
750
836
  token: Token<Value> | Constructor<Value>,
751
837
  options: ResolveOptions | undefined,
752
- resolutionPath: Array<string>,
753
838
  resolutionStack: Array<ResolutionFrame>,
754
839
  branchDepth: BranchDepth = UNOWNED_BRANCH,
755
840
  ): Promise<Value> {
756
- const { binding, owner } = this.#requireBinding(token, options, resolutionPath, resolutionStack);
841
+ const { binding, owner } = this.#requireBinding(token, options, resolutionStack);
757
842
 
758
843
  if (binding.scope === "singleton" && owner !== this) {
759
- return owner.#resolveBindingAsync(binding, options, resolutionPath, resolutionStack, branchDepth) as Value;
844
+ return owner.#resolveBindingAsync(binding, options, resolutionStack, branchDepth, owner) as Promise<Value>;
760
845
  }
761
- return this.#resolveBindingAsync(binding, options, resolutionPath, resolutionStack, branchDepth) as Value;
846
+ return this.#resolveBindingAsync(binding, options, resolutionStack, branchDepth, owner) as Promise<Value>;
762
847
  }
763
848
 
764
849
  async #resolveBindingAsync(
765
850
  binding: Binding,
766
851
  options: ResolveOptions | undefined,
767
- resolutionPath: Array<string>,
768
852
  resolutionStack: Array<ResolutionFrame>,
769
853
  branchDepth: BranchDepth,
854
+ owner: DependencyResolver,
770
855
  ): Promise<unknown> {
771
- if (this.#isPlainConstant(binding)) {
856
+ if (owner.#isPlainConstant(binding)) {
772
857
  return binding.value;
773
858
  }
774
859
 
@@ -782,22 +867,32 @@ export class DependencyResolver implements ResolverCallbacks {
782
867
  if (inflight !== undefined) {
783
868
  return inflight;
784
869
  }
870
+ if (this.#scope.isClosed) {
871
+ throw new DisposedContainerError();
872
+ }
785
873
  } else if (scope === "scoped") {
786
874
  const cachedScoped = this.#readScoped(binding);
787
875
  if (cachedScoped !== SCOPED_MISS) {
788
876
  return cachedScoped;
789
877
  }
878
+ // In-flight dedup, scoped flavor: one instance per scope even under concurrency.
879
+ const inflight = this.#scope.getInflight(binding.id);
880
+ if (inflight !== undefined) {
881
+ return inflight;
882
+ }
883
+ if (this.#scope.isClosed) {
884
+ throw new DisposedContainerError();
885
+ }
790
886
  }
791
887
 
792
888
  const frame = this.#getResolutionFrame(binding);
793
- // This level appends to its own branch and never unwinds — see ARCHITECTURE.md.
794
- const levelPath = extendResolutionBranch(resolutionPath, branchDepth, frame.tokenName);
795
- const levelStack = extendResolutionStackBranch(resolutionStack, branchDepth, frame);
796
- const levelDepth = branchDepthOf(levelPath);
889
+ // This level appends to its own branch and never unwinds.
890
+ const levelStack = extendResolutionBranch(resolutionStack, branchDepth, frame);
891
+ const levelDepth = branchDepthOf(levelStack);
797
892
 
798
- const needsActivation = this.#activation.needsActivation(binding);
893
+ const needsActivation = owner.#activation.needsActivation(binding);
799
894
  if (!needsActivation && scope === "transient" && (binding.kind === "dynamic" || binding.kind === "dynamic-async")) {
800
- const resolutionCtx = new AsyncLevelContext(this, levelPath, levelStack, options);
895
+ const resolutionCtx = new AsyncLevelContext(this, levelStack, options);
801
896
  if (binding.kind === "dynamic-async") {
802
897
  return await binding.factory(resolutionCtx);
803
898
  }
@@ -807,7 +902,7 @@ export class DependencyResolver implements ResolverCallbacks {
807
902
 
808
903
  const resolutionCtx =
809
904
  needsActivation || requiresResolutionContext(binding)
810
- ? new AsyncLevelContext(this, levelPath, levelStack, options)
905
+ ? new AsyncLevelContext(this, levelStack, options)
811
906
  : undefined;
812
907
 
813
908
  if (scope === "singleton") {
@@ -815,10 +910,10 @@ export class DependencyResolver implements ResolverCallbacks {
815
910
  const singletonPromise = this.#instantiateAndActivateAsync(
816
911
  binding,
817
912
  resolutionCtx,
818
- levelPath,
819
913
  levelStack,
820
914
  levelDepth,
821
915
  needsActivation,
916
+ owner,
822
917
  ).then(
823
918
  (activated) => {
824
919
  this.#scope.setSingleton(binding, activated);
@@ -834,39 +929,72 @@ export class DependencyResolver implements ResolverCallbacks {
834
929
  return await singletonPromise;
835
930
  }
836
931
 
837
- const activated = await this.#instantiateAndActivateAsync(
932
+ if (scope === "scoped") {
933
+ // Published before it settles, like the singleton lane: concurrent callers share one creation.
934
+ const scopedPromise = this.#instantiateAndActivateAsync(
935
+ binding,
936
+ resolutionCtx,
937
+ levelStack,
938
+ levelDepth,
939
+ needsActivation,
940
+ owner,
941
+ ).then(
942
+ (activated) => {
943
+ this.#scope.setScoped(binding, activated);
944
+ this.#scope.clearInflight(binding.id);
945
+ return activated;
946
+ },
947
+ (error: unknown) => {
948
+ this.#scope.clearInflight(binding.id);
949
+ throw error;
950
+ },
951
+ );
952
+ this.#scope.setInflight(binding.id, scopedPromise as Promise<unknown>);
953
+ return await scopedPromise;
954
+ }
955
+
956
+ return await this.#instantiateAndActivateAsync(
838
957
  binding,
839
958
  resolutionCtx,
840
- levelPath,
841
959
  levelStack,
842
960
  levelDepth,
843
961
  needsActivation,
962
+ owner,
844
963
  );
845
- if (scope === "scoped") {
846
- this.#scope.setScoped(binding.id, activated);
964
+ }
965
+
966
+ /**
967
+ * Settles this resolver's own `postConstruct` answer for a class binding a parent owns.
968
+ *
969
+ * @remarks The owner discovers it on first instantiation, but the plan compiler reads the
970
+ * introspector of whoever is resolving — left unknown, that resolver refuses to compile a plan for
971
+ * this binding on every call, forever.
972
+ */
973
+ #mirrorPostConstructFromOwner(binding: Binding, owner: DependencyResolver): void {
974
+ if (owner !== this && binding.kind === "class" && this.#classes.knownPostConstruct(binding.target) === undefined) {
975
+ this.#classes.discoverPostConstruct(binding.target);
847
976
  }
848
- return activated;
849
977
  }
850
978
 
851
979
  async #instantiateAndActivateAsync(
852
980
  binding: Binding,
853
981
  ctx: AsyncLevelContext | undefined,
854
- resolutionPath: Array<string>,
855
982
  resolutionStack: Array<ResolutionFrame>,
856
983
  branchDepth: BranchDepth,
857
984
  needsActivation: boolean,
985
+ owner: DependencyResolver,
858
986
  ): Promise<unknown> {
859
- const instance = await this.#instantiateAsync(binding, ctx, resolutionPath, resolutionStack, branchDepth);
860
- if (!this.#activation.refreshAfterFirstInstantiation(binding, needsActivation)) {
987
+ const instance = await this.#instantiateAsync(binding, ctx, resolutionStack, branchDepth);
988
+ this.#mirrorPostConstructFromOwner(binding, owner);
989
+ if (!owner.#activation.refreshAfterFirstInstantiation(binding, needsActivation)) {
861
990
  return instance;
862
991
  }
863
- return this.#lifecycle.runActivation(ctx as AsyncLevelContext, binding, instance, this.#metadataReader);
992
+ return owner.#lifecycle.runActivation(ctx as AsyncLevelContext, binding, instance, owner.#metadataReader);
864
993
  }
865
994
 
866
995
  async #instantiateAsync(
867
996
  binding: Binding,
868
997
  ctx: AsyncLevelContext | undefined,
869
- resolutionPath: Array<string>,
870
998
  resolutionStack: Array<ResolutionFrame>,
871
999
  branchDepth: BranchDepth,
872
1000
  ): Promise<unknown> {
@@ -891,24 +1019,25 @@ export class DependencyResolver implements ResolverCallbacks {
891
1019
  case "class": {
892
1020
  const deps = await this.#resolveDepsAsync(
893
1021
  this.#constructorParams(binding.target),
894
- resolutionPath,
895
1022
  resolutionStack,
896
1023
  branchDepth,
897
1024
  );
898
- return this.#classes.instantiate(binding.target, deps);
1025
+ // Accessor initializers resolve synchronously, so the branch-owned path serves them directly.
1026
+ return this.#classes.instantiate(
1027
+ binding.target,
1028
+ deps,
1029
+ this.#classes.needsActiveContainer(binding.target) ? this.#ambientResolutionFor(resolutionStack) : undefined,
1030
+ );
899
1031
  }
900
1032
 
901
1033
  case "resolved": {
902
- if (ctx === undefined) {
903
- throw new InternalError("resolved binding requires resolution context");
904
- }
905
- const deps = await this.#resolveDepsAsync(binding.deps, resolutionPath, resolutionStack, branchDepth);
1034
+ const deps = await this.#resolveDepsAsync(binding.deps, resolutionStack, branchDepth);
906
1035
  const factoryResult = binding.factory(...deps);
907
1036
  return factoryResult instanceof Promise ? factoryResult : Promise.resolve(factoryResult);
908
1037
  }
909
1038
 
910
1039
  case "resolved-async": {
911
- const deps = await this.#resolveDepsAsync(binding.deps, resolutionPath, resolutionStack, branchDepth);
1040
+ const deps = await this.#resolveDepsAsync(binding.deps, resolutionStack, branchDepth);
912
1041
  return binding.factory(...deps);
913
1042
  }
914
1043
 
@@ -919,7 +1048,6 @@ export class DependencyResolver implements ResolverCallbacks {
919
1048
 
920
1049
  async #resolveDepsAsync(
921
1050
  deps: ReadonlyArray<DependencySlot>,
922
- resolutionPath: Array<string>,
923
1051
  resolutionStack: Array<ResolutionFrame>,
924
1052
  branchDepth: BranchDepth,
925
1053
  ): Promise<Array<unknown>> {
@@ -928,63 +1056,68 @@ export class DependencyResolver implements ResolverCallbacks {
928
1056
  return [];
929
1057
  }
930
1058
  if (count === 1) {
931
- return [await this.#resolveDepAsync(deps[0]!, resolutionPath, resolutionStack, branchDepth)];
1059
+ return [await this.#resolveDepAsync(deps[0]!, resolutionStack, branchDepth)];
932
1060
  }
933
1061
  // Siblings resolve concurrently and each extends the same branch, so the first appends in
934
1062
  // place and the rest copy the prefix — no caller has to isolate them.
935
1063
  const pending = new Array<Promise<unknown>>(count);
936
1064
  for (let index = 0; index < count; index += 1) {
937
- pending[index] = this.#resolveDepAsync(deps[index]!, resolutionPath, resolutionStack, branchDepth);
1065
+ pending[index] = this.#resolveDepAsync(deps[index]!, resolutionStack, branchDepth);
938
1066
  }
939
1067
  return Promise.all(pending);
940
1068
  }
941
1069
 
942
1070
  #resolveDepAsync(
943
1071
  dep: DependencySlot,
944
- resolutionPath: Array<string>,
945
1072
  resolutionStack: Array<ResolutionFrame>,
946
1073
  branchDepth: BranchDepth,
947
1074
  ): Promise<unknown> {
948
- const options = injectionSlotToResolveOptions(dep);
1075
+ const options = resolveOptionsForSlot(dep);
949
1076
  if (dep.multi) {
950
- return this.resolveAllAsync(dep.token, options, resolutionPath, resolutionStack, branchDepth);
1077
+ return this.resolveAllAsync(dep.token, options, resolutionStack, branchDepth);
951
1078
  }
952
1079
  if (dep.optional) {
953
- return this.resolveOptionalAsync(dep.token, options, resolutionPath, resolutionStack, branchDepth);
1080
+ return this.resolveOptionalAsync(dep.token, options, resolutionStack, branchDepth);
954
1081
  }
955
1082
  if (options === undefined) {
956
- return this.resolveAsyncFromContext(dep.token, resolutionPath, resolutionStack, branchDepth);
1083
+ return this.resolveAsyncFromContext(dep.token, resolutionStack, branchDepth);
957
1084
  }
958
- return this.resolveAsync(dep.token, options, resolutionPath, resolutionStack, branchDepth);
1085
+ return this.resolveAsync(dep.token, options, resolutionStack, branchDepth);
959
1086
  }
960
1087
 
961
- async resolveOptionalAsync<const Value>(
1088
+ async resolveOptionalAsync<Value>(
962
1089
  token: Token<Value> | Constructor<Value>,
963
1090
  options: ResolveOptions | undefined,
964
- resolutionPath: Array<string>,
965
1091
  resolutionStack: Array<ResolutionFrame>,
966
1092
  branchDepth: BranchDepth = UNOWNED_BRANCH,
967
1093
  ): Promise<Value | undefined> {
968
- if (this.#findBinding(token, options, resolutionPath, resolutionStack) === undefined) {
1094
+ const entry = this.#findBinding(token, options, resolutionStack);
1095
+ if (entry === undefined) {
969
1096
  return undefined;
970
1097
  }
971
- return this.resolveAsync(token, options, resolutionPath, resolutionStack, branchDepth);
1098
+ // Same single-evaluation contract as the sync lane: resolve what the probe found.
1099
+ const { binding, owner } = entry;
1100
+ if (binding.kind === "alias") {
1101
+ return this.resolveAsync(token, options, resolutionStack, branchDepth);
1102
+ }
1103
+ if (binding.scope === "singleton" && owner !== this) {
1104
+ return owner.#resolveBindingAsync(binding, options, resolutionStack, branchDepth, owner) as Promise<Value>;
1105
+ }
1106
+ return this.#resolveBindingAsync(binding, options, resolutionStack, branchDepth, owner) as Promise<Value>;
972
1107
  }
973
1108
 
974
- async resolveAllAsync<const Value>(
1109
+ async resolveAllAsync<Value>(
975
1110
  token: Token<Value> | Constructor<Value>,
976
1111
  options: ResolveOptions | undefined,
977
- resolutionPath: Array<string>,
978
1112
  resolutionStack: Array<ResolutionFrame>,
979
1113
  branchDepth: BranchDepth = UNOWNED_BRANCH,
980
1114
  ): Promise<Array<Value>> {
981
- const candidates = this.#candidateBindings(token, options, resolutionPath, resolutionStack);
1115
+ const candidates = this.#candidateBindings(token, options, resolutionStack);
982
1116
  const pending = new Array<Promise<Value>>(candidates.length);
983
1117
  for (let index = 0; index < candidates.length; index += 1) {
984
1118
  pending[index] = this.#resolveCandidateAsync(
985
1119
  candidates[index]!,
986
1120
  options,
987
- resolutionPath,
988
1121
  resolutionStack,
989
1122
  branchDepth,
990
1123
  ) as Promise<Value>;
@@ -992,7 +1125,7 @@ export class DependencyResolver implements ResolverCallbacks {
992
1125
  return Promise.all(pending);
993
1126
  }
994
1127
 
995
- // ── Helpers ────────────────────────────────────────────────────────────────
1128
+ // ── Helpers ────────────────────────────────────────────────────────────────────────────────────────────────────────
996
1129
 
997
1130
  #allBindingsFromChain(token: Token<unknown> | Constructor): ReadonlyArray<Binding> {
998
1131
  const ownBindings = this.#registry.getAll(token);
@@ -1027,6 +1160,58 @@ export class DependencyResolver implements ResolverCallbacks {
1027
1160
  return result;
1028
1161
  }
1029
1162
 
1163
+ /**
1164
+ * The candidates an index can name outright, or `null` when the request needs full selection.
1165
+ *
1166
+ * @remarks Kept off `#candidateBindings` so that method stays the size it was: a request neither
1167
+ * index serves must not pay for the two that do.
1168
+ * An index has matched the slot already, but a hit may still carry a predicate, and evaluating
1169
+ * that is the selection path's job.
1170
+ */
1171
+ #indexedCandidates(
1172
+ token: Token<unknown> | Constructor,
1173
+ options: ResolveOptions,
1174
+ resolutionStack: Array<ResolutionFrame>,
1175
+ ): ReadonlyArray<Binding> | null {
1176
+ if (isNameOnlyOptions(options)) {
1177
+ const named = this.#namedBindingsFromChain(token, options.name);
1178
+ return anyPredicate(named)
1179
+ ? selectAllBindings(named, options, this.#makeConstraintContext(resolutionStack, options))
1180
+ : named;
1181
+ }
1182
+ const singleTag = singleTagOnlyOf(options);
1183
+ if (singleTag === undefined) {
1184
+ return null;
1185
+ }
1186
+ const tagged = this.#taggedBindingsFromChain(token, singleTag);
1187
+ return anyPredicate(tagged)
1188
+ ? selectAllBindings(tagged, options, this.#makeConstraintContext(resolutionStack, options))
1189
+ : tagged;
1190
+ }
1191
+
1192
+ /**
1193
+ * Every binding the chain's tag indexes hold for one tag, nearest container first.
1194
+ *
1195
+ * @remarks A request for one tag and no name matches exactly the bindings the index keys, so this
1196
+ * is the whole candidate set rather than a prefilter — a named or multi-tag slot cannot satisfy it.
1197
+ */
1198
+ #taggedBindingsFromChain(token: Token<unknown> | Constructor, tag: BindingTag): Array<Binding> {
1199
+ // A tag matches at most one binding per registry, so a root container's answer is built whole
1200
+ // rather than grown — the shape `#namedBindingsFromChain` takes, for the same reason.
1201
+ const ownBinding = this.#registry.getSimpleTagged(token, tag);
1202
+ if (this.#parent === undefined) {
1203
+ return ownBinding === undefined ? [] : [ownBinding];
1204
+ }
1205
+ const result: Array<Binding> = ownBinding === undefined ? [] : [ownBinding];
1206
+ for (let current: DependencyResolver | undefined = this.#parent; current !== undefined; current = current.#parent) {
1207
+ const binding = current.#registry.getSimpleTagged(token, tag);
1208
+ if (binding !== undefined) {
1209
+ result.push(binding);
1210
+ }
1211
+ }
1212
+ return result;
1213
+ }
1214
+
1030
1215
  /** A constant with no activation anywhere resolves to its value with no pipeline at all. */
1031
1216
  #isPlainConstant(binding: Binding): binding is ConstantBinding<unknown> {
1032
1217
  return (
@@ -1054,68 +1239,111 @@ export class DependencyResolver implements ResolverCallbacks {
1054
1239
  if (!this.#scope.isChild) {
1055
1240
  throw new MissingScopeContextError(tokenName(binding.token));
1056
1241
  }
1057
- if (this.#scope.hasScoped(binding.id)) {
1058
- return this.#scope.getScoped(binding.id);
1059
- }
1060
- return SCOPED_MISS;
1242
+ return this.#scope.readScoped(binding.id);
1061
1243
  }
1062
1244
 
1245
+ // The shared root context answers every top-level request; building one is the rarer half and
1246
+ // lives outside, so what a selection inlines is the test and not the literal.
1063
1247
  #makeConstraintContext(
1064
- resolutionPath: Array<string>,
1065
1248
  resolutionStack: Array<ResolutionFrame>,
1066
1249
  options: ResolveOptions | undefined,
1067
1250
  ): ConstraintContext {
1068
- if (options === undefined && resolutionPath.length === 0 && resolutionStack.length === 0) {
1251
+ if (options === undefined && resolutionStack.length === 0) {
1069
1252
  return ROOT_CONSTRAINT_CONTEXT;
1070
1253
  }
1071
- return {
1072
- resolutionPath,
1073
- resolutionStack,
1074
- parent: resolutionStack.at(-1),
1075
- ancestors: resolutionStack.length > 1 ? resolutionStack.slice(0, -1) : [],
1076
- currentResolveOptions: options,
1077
- };
1254
+ return buildConstraintContext(resolutionStack, options);
1255
+ }
1256
+
1257
+ /** Selection for a name-less multi-tag request, over the union of the two tag indexes. */
1258
+ #selectMultiTagged(
1259
+ token: Token<unknown> | Constructor,
1260
+ options: ResolveOptions,
1261
+ resolutionStack: Array<ResolutionFrame>,
1262
+ ): Binding | undefined {
1263
+ const candidates: Array<Binding> = [];
1264
+ this.#gatherTagCandidates(token, options.tag, candidates);
1265
+ const listed = options.tags;
1266
+ if (listed !== undefined) {
1267
+ for (let index = 0; index < listed.length; index += 1) {
1268
+ this.#gatherTagCandidates(token, listed[index], candidates);
1269
+ }
1270
+ }
1271
+ if (candidates.length === 0) {
1272
+ return undefined;
1273
+ }
1274
+ return selectBinding(candidates, options, this.#makeConstraintContext(resolutionStack, options), tokenName(token));
1275
+ }
1276
+
1277
+ /** One request criterion's candidates: its exact single-tag binding, plus its first-tag bucket. */
1278
+ #gatherTagCandidates(
1279
+ token: Token<unknown> | Constructor,
1280
+ criterion: BindingTag | undefined,
1281
+ out: Array<Binding>,
1282
+ ): void {
1283
+ if (criterion === undefined) {
1284
+ return;
1285
+ }
1286
+ // The includes probes only guard a request repeating a criterion across its two spellings.
1287
+ const single = this.#registry.getSimpleTagged(token, criterion);
1288
+ if (single !== undefined && !out.includes(single)) {
1289
+ out.push(single);
1290
+ }
1291
+ const bucket = this.#registry.getMultiTagged(token, criterion);
1292
+ if (bucket !== undefined) {
1293
+ for (let index = 0; index < bucket.length; index += 1) {
1294
+ const candidate = bucket[index]!;
1295
+ if (!out.includes(candidate)) {
1296
+ out.push(candidate);
1297
+ }
1298
+ }
1299
+ }
1078
1300
  }
1079
1301
 
1080
1302
  #matchesBindingFast(
1081
1303
  binding: Binding,
1082
1304
  options: ResolveOptions | undefined,
1083
- resolutionPath: Array<string>,
1084
1305
  resolutionStack: Array<ResolutionFrame>,
1085
1306
  ): boolean {
1086
1307
  if (!matchesSlot(binding.slot, options)) {
1087
1308
  return false;
1088
1309
  }
1089
- if (binding.predicate === undefined) {
1310
+ return this.#satisfiesPredicate(binding, options, resolutionStack);
1311
+ }
1312
+
1313
+ /** The predicate half of a match, for a lane whose index has already settled the slot. */
1314
+ #satisfiesPredicate(
1315
+ binding: Binding,
1316
+ options: ResolveOptions | undefined,
1317
+ resolutionStack: Array<ResolutionFrame>,
1318
+ ): boolean {
1319
+ const predicate = binding.predicate;
1320
+ if (predicate === undefined) {
1090
1321
  return true;
1091
1322
  }
1092
- return binding.predicate(this.#makeConstraintContext(resolutionPath, resolutionStack, options));
1323
+ return predicate(this.#makeConstraintContext(resolutionStack, options));
1093
1324
  }
1094
1325
 
1095
1326
  #resolveTransientDynamicSyncFromContext(
1096
1327
  binding: DynamicBinding<unknown>,
1097
- resolutionPath: Array<string>,
1098
1328
  resolutionStack: Array<ResolutionFrame>,
1099
1329
  ): unknown {
1100
1330
  // One lane at every depth: `binding.inFlight` is O(1), so there is nothing to escape.
1101
1331
  const frame = this.#getResolutionFrame(binding);
1102
1332
  const tokenDisplayName = frame.tokenName;
1103
1333
  if (binding.inFlight) {
1104
- throw new CircularDependencyError([...resolutionPath, tokenDisplayName]);
1334
+ throw new CircularDependencyError(cycleNamesOf(resolutionStack, tokenDisplayName));
1105
1335
  }
1106
1336
  binding.inFlight = true;
1107
- resolutionPath.push(tokenDisplayName);
1108
1337
  resolutionStack.push(frame);
1109
- const resolutionCtx = this.#acquireSyncResolutionContext(resolutionPath, resolutionStack, undefined);
1338
+ const resolutionCtx = this.#acquireSyncResolutionContext(resolutionStack, undefined);
1110
1339
  try {
1111
1340
  const dynamicResult = binding.factory(resolutionCtx);
1112
1341
  if (dynamicResult instanceof Promise) {
1113
- throw new AsyncResolutionError(tokenDisplayName, tokenDisplayName);
1342
+ throw new AsyncResolutionError(resolutionStack[0]?.tokenName ?? tokenDisplayName, tokenDisplayName);
1114
1343
  }
1115
1344
  return dynamicResult;
1116
1345
  } finally {
1117
1346
  resolutionStack.pop();
1118
- resolutionPath.pop();
1119
1347
  binding.inFlight = false;
1120
1348
  }
1121
1349
  }
@@ -1123,22 +1351,20 @@ export class DependencyResolver implements ResolverCallbacks {
1123
1351
  // Deliberately not `async`: that would allocate a state machine and a promise per level.
1124
1352
  #resolveTransientDynamicAsyncFromContext(
1125
1353
  binding: DynamicBinding<unknown> | DynamicAsyncBinding<unknown>,
1126
- resolutionPath: Array<string>,
1127
1354
  resolutionStack: Array<ResolutionFrame>,
1128
1355
  branchDepth: BranchDepth,
1129
1356
  ): Promise<unknown> {
1130
1357
  const frame = this.#getResolutionFrame(binding);
1131
- let levelPath: OwnedBranchPath;
1358
+ let levelStack: OwnedBranchStack;
1132
1359
  try {
1133
- levelPath = extendResolutionBranch(resolutionPath, branchDepth, frame.tokenName);
1360
+ levelStack = extendResolutionBranch(resolutionStack, branchDepth, frame);
1134
1361
  } catch (cycleError) {
1135
1362
  // This method is not `async`; keep failures as rejections rather than sync throws.
1136
1363
  return Promise.reject(cycleError);
1137
1364
  }
1138
- const levelStack = extendResolutionStackBranch(resolutionStack, branchDepth, frame);
1139
1365
 
1140
1366
  // Nothing this level appended is ever removed, so no level observes its own settlement.
1141
- const ctx = new AsyncLevelContext(this, levelPath, levelStack, undefined);
1367
+ const ctx = new AsyncLevelContext(this, levelStack, undefined);
1142
1368
  try {
1143
1369
  if (binding.kind === "dynamic-async") {
1144
1370
  return binding.factory(ctx);
@@ -1150,17 +1376,17 @@ export class DependencyResolver implements ResolverCallbacks {
1150
1376
  }
1151
1377
  }
1152
1378
 
1153
- // ── The cascade lane ───────────────────────────────────────────────────────
1379
+ // ── The cascade lane ───────────────────────────────────────────────────────────────────────────────────────────────
1154
1380
 
1155
1381
  /**
1156
1382
  * Entry for a request a factory makes from inside an open synchronous cascade.
1157
1383
  *
1158
1384
  * @remarks A request arriving with no cascade open came out of a continuation, so its ancestors
1159
- * are on no call stack — it escapes to the branch lane. See `ARCHITECTURE.md`.
1385
+ * are on no call stack — it escapes to the branch lane.
1160
1386
  */
1161
1387
  resolveAsyncFromCascade(token: Token<unknown> | Constructor): Promise<unknown> {
1162
- if (this.#cascadePath.length === 0) {
1163
- return this.resolveAsyncFromContext(token, [], [], ROOT_BRANCH);
1388
+ if (this.#cascadeStack.length === 0) {
1389
+ return this.resolveAsyncFromContext(token, [], ROOT_BRANCH);
1164
1390
  }
1165
1391
  return this.#dispatchCascade(token);
1166
1392
  }
@@ -1188,10 +1414,41 @@ export class DependencyResolver implements ResolverCallbacks {
1188
1414
  if (fastBinding.scope === "singleton" && fastBinding.instance !== NO_INSTANCE) {
1189
1415
  return Promise.resolve(fastBinding.instance);
1190
1416
  }
1417
+ const planned = this.#plannedCascadeAnswer(fastBinding);
1418
+ if (planned !== null) {
1419
+ return planned;
1420
+ }
1191
1421
  }
1192
1422
  // Anything else leaves the cascade lane for good, seeded with a snapshot of the ancestors it
1193
1423
  // accumulated — so a cycle across the boundary is still on one path.
1194
- return this.resolveAsyncFromContext(token, [...this.#cascadePath], [...this.#cascadeStack], UNOWNED_BRANCH);
1424
+ return this.resolveAsyncFromContext(token, [...this.#cascadeStack], UNOWNED_BRANCH);
1425
+ }
1426
+
1427
+ /**
1428
+ * A statically-visible transient graph at a true root answers from its compiled async plan.
1429
+ *
1430
+ * @remarks Kept out of the dispatcher so its size stays inlinable. Inside an open cascade the
1431
+ * graph must escape instead, so its escapes carry the live ancestors — hence the idle gate.
1432
+ */
1433
+ #plannedCascadeAnswer(fastBinding: Binding): Promise<unknown> | null {
1434
+ if (
1435
+ this.#cascadeStack.length !== 0 ||
1436
+ fastBinding.scope !== "transient" ||
1437
+ (fastBinding.kind !== "class" && fastBinding.kind !== "resolved" && fastBinding.kind !== "resolved-async")
1438
+ ) {
1439
+ return null;
1440
+ }
1441
+ const plan = this.#getAsyncInstantiationPlan(fastBinding);
1442
+ if (plan === null) {
1443
+ return null;
1444
+ }
1445
+ try {
1446
+ const planned = plan();
1447
+ return planned instanceof Promise ? planned : Promise.resolve(planned);
1448
+ } catch (planError) {
1449
+ // The interpreted lane is async, so a sync throw is a rejection there too.
1450
+ return Promise.reject(planError);
1451
+ }
1195
1452
  }
1196
1453
 
1197
1454
  #resolveTransientDynamicAsyncCascade(
@@ -1202,11 +1459,10 @@ export class DependencyResolver implements ResolverCallbacks {
1202
1459
  // code does not interleave — so the O(1) flag is exact path membership here, as it is for the
1203
1460
  // sync lane. It is cleared when the factory returns its promise, not when that promise settles.
1204
1461
  if (binding.inFlight) {
1205
- return Promise.reject(new CircularDependencyError([...this.#cascadePath, frame.tokenName]));
1462
+ return Promise.reject(new CircularDependencyError(cycleNamesOf(this.#cascadeStack, frame.tokenName)));
1206
1463
  }
1207
- const ctx = (this.#cascadeContext ??= new AsyncCascadeContext(this, this.#cascadePath, this.#cascadeStack));
1464
+ const ctx = (this.#cascadeContext ??= new AsyncCascadeContext(this, this.#cascadeStack));
1208
1465
  binding.inFlight = true;
1209
- this.#cascadePath.push(frame.tokenName);
1210
1466
  this.#cascadeStack.push(frame);
1211
1467
  try {
1212
1468
  if (binding.kind === "dynamic-async") {
@@ -1218,7 +1474,6 @@ export class DependencyResolver implements ResolverCallbacks {
1218
1474
  return Promise.reject(factoryError);
1219
1475
  } finally {
1220
1476
  this.#cascadeStack.pop();
1221
- this.#cascadePath.pop();
1222
1477
  binding.inFlight = false;
1223
1478
  }
1224
1479
  }
@@ -1228,38 +1483,67 @@ export class DependencyResolver implements ResolverCallbacks {
1228
1483
  #resolveCandidateSync(
1229
1484
  binding: Binding,
1230
1485
  options: ResolveOptions | undefined,
1231
- resolutionPath: Array<string>,
1232
1486
  resolutionStack: Array<ResolutionFrame>,
1233
1487
  ): unknown {
1234
- if (this.#isPlainConstant(binding)) {
1488
+ // Fan-outs are dominated by constants: with no activation hook anywhere in the chain, a
1489
+ // hook-free constant is plain no matter which container owns it — skip the owner probe.
1490
+ if (binding.kind === "constant" && binding.onActivation === undefined && this.#chainActivationVersion() === 0) {
1491
+ return binding.value;
1492
+ }
1493
+ const owner = this.#ownerOf(binding);
1494
+ if (owner.#isPlainConstant(binding)) {
1235
1495
  return binding.value;
1236
1496
  }
1237
1497
  if (binding.kind === "alias") {
1238
- return this.resolve(binding.target, options, resolutionPath, resolutionStack);
1498
+ return this.resolve(binding.target, options, resolutionStack);
1239
1499
  }
1240
- if (binding.scope === "singleton" && binding.instance !== NO_INSTANCE) {
1241
- return binding.instance;
1500
+ if (binding.scope === "singleton") {
1501
+ if (binding.instance !== NO_INSTANCE) {
1502
+ return binding.instance;
1503
+ }
1504
+ // Owner-routed like `resolve`: the owner materializes and caches its own singleton.
1505
+ return owner.#resolveBinding(binding, options, resolutionStack, owner);
1242
1506
  }
1243
- return this.#resolveBinding(binding, options, resolutionPath, resolutionStack);
1507
+ return this.#resolveBinding(binding, options, resolutionStack, owner);
1244
1508
  }
1245
1509
 
1246
1510
  #resolveCandidateAsync(
1247
1511
  binding: Binding,
1248
1512
  options: ResolveOptions | undefined,
1249
- resolutionPath: Array<string>,
1250
1513
  resolutionStack: Array<ResolutionFrame>,
1251
1514
  branchDepth: BranchDepth,
1252
1515
  ): Promise<unknown> {
1253
- if (this.#isPlainConstant(binding)) {
1516
+ if (binding.kind === "constant" && binding.onActivation === undefined && this.#chainActivationVersion() === 0) {
1517
+ return Promise.resolve(binding.value);
1518
+ }
1519
+ const owner = this.#ownerOf(binding);
1520
+ if (owner.#isPlainConstant(binding)) {
1254
1521
  return Promise.resolve(binding.value);
1255
1522
  }
1256
1523
  if (binding.kind === "alias") {
1257
- return this.resolveAsync(binding.target, options, resolutionPath, resolutionStack, branchDepth);
1524
+ return this.resolveAsync(binding.target, options, resolutionStack, branchDepth);
1258
1525
  }
1259
- if (binding.scope === "singleton" && binding.instance !== NO_INSTANCE) {
1260
- return Promise.resolve(binding.instance);
1526
+ if (binding.scope === "singleton") {
1527
+ if (binding.instance !== NO_INSTANCE) {
1528
+ return Promise.resolve(binding.instance);
1529
+ }
1530
+ return owner.#resolveBindingAsync(binding, options, resolutionStack, branchDepth, owner);
1531
+ }
1532
+ return this.#resolveBindingAsync(binding, options, resolutionStack, branchDepth, owner);
1533
+ }
1534
+
1535
+ /** The resolver whose registry holds `binding` — `this` (the common case) when it is own. */
1536
+ #ownerOf(binding: Binding): DependencyResolver {
1537
+ // A root resolver can only hold its own bindings, so the per-candidate id probe is chain-only.
1538
+ if (this.#parent === undefined || this.#registry.getById(binding.id) !== undefined) {
1539
+ return this;
1540
+ }
1541
+ for (let current: DependencyResolver | undefined = this.#parent; current !== undefined; current = current.#parent) {
1542
+ if (current.#registry.getById(binding.id) !== undefined) {
1543
+ return current;
1544
+ }
1261
1545
  }
1262
- return this.#resolveBindingAsync(binding, options, resolutionPath, resolutionStack, branchDepth);
1546
+ return this;
1263
1547
  }
1264
1548
 
1265
1549
  #getResolutionFrame(binding: Binding): ResolutionFrame {
@@ -1275,26 +1559,48 @@ export class DependencyResolver implements ResolverCallbacks {
1275
1559
  return frame;
1276
1560
  }
1277
1561
 
1562
+ // A pool is keyed by the one array pair its contexts hold, so reuse can never re-point a context
1563
+ // a live frame still reads — a nested top-level resolve reaches the same depth while the outer
1564
+ // factory runs, and it must get its own context, not the outer frame's re-bound.
1278
1565
  #acquireSyncResolutionContext(
1279
- resolutionPath: Array<string>,
1280
1566
  resolutionStack: Array<ResolutionFrame>,
1281
1567
  options: ResolveOptions | undefined,
1282
1568
  ): DefaultResolutionContext {
1569
+ if (resolutionStack === this.rootStack) {
1570
+ const depth = resolutionStack.length;
1571
+ const existing = this.#syncResolutionContextPool[depth];
1572
+ if (existing !== undefined) {
1573
+ existing.reset(this, resolutionStack, options);
1574
+ return existing;
1575
+ }
1576
+ const created = new DefaultResolutionContext(this, resolutionStack, options);
1577
+ this.#syncResolutionContextPool[depth] = created;
1578
+ return created;
1579
+ }
1580
+ return this.#acquireOffRootSyncContext(resolutionStack, options);
1581
+ }
1582
+
1583
+ /** The cascade pair pools separately; a throwaway pair (nested resolve, async snapshot) mints per call. */
1584
+ #acquireOffRootSyncContext(
1585
+ resolutionStack: Array<ResolutionFrame>,
1586
+ options: ResolveOptions | undefined,
1587
+ ): DefaultResolutionContext {
1588
+ if (resolutionStack !== this.#cascadeStack) {
1589
+ return new DefaultResolutionContext(this, resolutionStack, options);
1590
+ }
1283
1591
  const depth = resolutionStack.length;
1284
- const existing = this.#syncResolutionContextPool[depth];
1592
+ const pool = (this.#cascadeContextPool ??= []);
1593
+ const existing = pool[depth];
1285
1594
  if (existing !== undefined) {
1286
- existing.reset(this, resolutionPath, resolutionStack, options);
1595
+ existing.reset(this, resolutionStack, options);
1287
1596
  return existing;
1288
1597
  }
1289
- const created = new DefaultResolutionContext(this, resolutionPath, resolutionStack, options);
1290
- this.#syncResolutionContextPool[depth] = created;
1598
+ const created = new DefaultResolutionContext(this, resolutionStack, options);
1599
+ pool[depth] = created;
1291
1600
  return created;
1292
1601
  }
1293
1602
  }
1294
1603
 
1295
- /** Absent scoped entry — distinguishes it from a cached `undefined`. */
1296
- const SCOPED_MISS: unique symbol = Symbol("di:scoped-miss");
1297
-
1298
1604
  function anyPredicate(bindings: ReadonlyArray<Binding>): boolean {
1299
1605
  for (let index = 0; index < bindings.length; index += 1) {
1300
1606
  if (bindings[index]!.predicate !== undefined) {
@@ -1304,20 +1610,36 @@ function anyPredicate(bindings: ReadonlyArray<Binding>): boolean {
1304
1610
  return false;
1305
1611
  }
1306
1612
 
1613
+ function buildConstraintContext(
1614
+ resolutionStack: Array<ResolutionFrame>,
1615
+ options: ResolveOptions | undefined,
1616
+ ): ConstraintContext {
1617
+ return {
1618
+ // Derived per read, never cached: the stack may be live, and the names must report it as it stands.
1619
+ get resolutionPath(): ReadonlyArray<string> {
1620
+ const names = new Array<string>(resolutionStack.length);
1621
+ for (let index = 0; index < resolutionStack.length; index += 1) {
1622
+ names[index] = resolutionStack[index]!.tokenName;
1623
+ }
1624
+ return names;
1625
+ },
1626
+ resolutionStack,
1627
+ parent: resolutionStack.at(-1),
1628
+ ancestors: resolutionStack.length > 1 ? resolutionStack.slice(0, -1) : [],
1629
+ currentResolveOptions: options,
1630
+ };
1631
+ }
1632
+
1633
+ /** The async-resolution failure for a binding reached on a sync path, naming what to await instead. */
1634
+ function asyncResolutionErrorFor(
1635
+ binding: Binding,
1636
+ resolutionStack: ReadonlyArray<ResolutionFrame>,
1637
+ ): AsyncResolutionError {
1638
+ const sourceName = tokenName(binding.token);
1639
+ return new AsyncResolutionError(resolutionStack[0]?.tokenName ?? sourceName, sourceName);
1640
+ }
1641
+
1307
1642
  /** Only a factory is handed the resolution context; everything else gets its deps directly. */
1308
1643
  function requiresResolutionContext(binding: Binding): boolean {
1309
1644
  return binding.kind === "dynamic" || binding.kind === "dynamic-async";
1310
1645
  }
1311
-
1312
- /**
1313
- * Whether the tag index's answer is the one `Object.is` would give.
1314
- *
1315
- * @remarks An indexed binding has no name, no predicate and exactly one tag, and the request carries
1316
- * only that tag, so `matchesSlot` reduces to the tag values — and the index matched the key already.
1317
- * It answers by SameValueZero, which parts from `Object.is` (SPEC §3.5) on exactly one pair: `+0` and
1318
- * `-0`. So a request whose value is not zero is already exact, and only a zero-valued one is worth
1319
- * reading the stored value for.
1320
- */
1321
- function matchesIndexedTagValue(binding: Binding, requestedValue: unknown): boolean {
1322
- return requestedValue !== 0 || Object.is(binding.slot.tags[0]![1], requestedValue);
1323
- }