@codefast/di 0.5.0 → 0.6.1

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 +712 -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 +62 -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 +281 -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 +53 -0
  23. package/dist/core/constraint-requirement.d.ts.map +1 -0
  24. package/dist/core/constraint-requirement.js +62 -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 +96 -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} +9 -5
  128. package/dist/resolution/cache/binding-lookup-cache.d.ts.map +1 -0
  129. package/dist/resolution/{binding-lookup-cache.js → cache/binding-lookup-cache.js} +55 -0
  130. package/dist/resolution/cache/binding-lookup-cache.js.map +1 -0
  131. package/dist/resolution/cache/class-introspector.d.ts +66 -0
  132. package/dist/resolution/cache/class-introspector.d.ts.map +1 -0
  133. package/dist/resolution/cache/class-introspector.js +194 -0
  134. package/dist/resolution/cache/class-introspector.js.map +1 -0
  135. package/dist/resolution/context.d.ts +84 -0
  136. package/dist/resolution/context.d.ts.map +1 -0
  137. package/dist/resolution/{environment.js → context.js} +54 -80
  138. package/dist/resolution/context.js.map +1 -0
  139. package/dist/resolution/path/resolution-path.d.ts +94 -0
  140. package/dist/resolution/path/resolution-path.d.ts.map +1 -0
  141. package/dist/resolution/path/resolution-path.js +117 -0
  142. package/dist/resolution/path/resolution-path.js.map +1 -0
  143. package/dist/resolution/{instantiation-plan.d.ts → plan/instantiation-plan.d.ts} +37 -7
  144. package/dist/resolution/plan/instantiation-plan.d.ts.map +1 -0
  145. package/dist/resolution/plan/instantiation-plan.js +419 -0
  146. package/dist/resolution/plan/instantiation-plan.js.map +1 -0
  147. package/dist/resolution/resolver.d.ts +28 -28
  148. package/dist/resolution/resolver.d.ts.map +1 -1
  149. package/dist/resolution/resolver.js +581 -239
  150. package/dist/resolution/resolver.js.map +1 -1
  151. package/dist/resolution/select/binding-select.d.ts +34 -0
  152. package/dist/resolution/select/binding-select.d.ts.map +1 -0
  153. package/dist/resolution/select/binding-select.js +175 -0
  154. package/dist/resolution/select/binding-select.js.map +1 -0
  155. package/dist/resolution/{constraints.d.ts → select/constraints.d.ts} +13 -12
  156. package/dist/resolution/select/constraints.d.ts.map +1 -0
  157. package/dist/resolution/{constraints.js → select/constraints.js} +33 -22
  158. package/dist/resolution/select/constraints.js.map +1 -0
  159. package/package.json +160 -20
  160. package/src/ambient/active-container.ts +65 -0
  161. package/src/container/binding-builders.ts +79 -44
  162. package/src/container/container.ts +357 -170
  163. package/src/{resolution → core}/binding-scope.ts +2 -2
  164. package/src/{binding.ts → core/binding.ts} +101 -39
  165. package/src/core/constraint-requirement.ts +94 -0
  166. package/src/{constructor-type.ts → core/constructor-type.ts} +1 -1
  167. package/src/{module.ts → core/module.ts} +10 -10
  168. package/src/core/registry.ts +365 -0
  169. package/src/core/tag.ts +158 -0
  170. package/src/{token.ts → core/token.ts} +2 -14
  171. package/src/{types.ts → core/types.ts} +37 -34
  172. package/src/decorators/inject.ts +39 -175
  173. package/src/decorators/injectable.ts +40 -10
  174. package/src/decorators/lifecycle-decorators.ts +22 -34
  175. package/src/{resolution → errors}/diagnostics.ts +2 -0
  176. package/src/{errors.ts → errors/errors.ts} +152 -8
  177. package/src/index.ts +43 -15
  178. package/src/injection/descriptor.ts +206 -0
  179. package/src/injection/resolve-options.ts +144 -0
  180. package/src/introspection/dependency-graph.ts +223 -63
  181. package/src/introspection/graph-adapters/cytoscape.ts +9 -3
  182. package/src/introspection/graph-adapters/dot.ts +13 -5
  183. package/src/introspection/graph-adapters/mermaid.ts +61 -0
  184. package/src/introspection/graph-adapters/reactflow.ts +11 -4
  185. package/src/introspection/inspector.ts +16 -12
  186. package/src/{resolution/lifecycle.ts → lifecycle/lifecycle-manager.ts} +68 -32
  187. package/src/lifecycle/scope-manager.ts +156 -0
  188. package/src/metadata/metadata-reader-token.ts +2 -2
  189. package/src/metadata/metadata-types.ts +9 -9
  190. package/src/metadata/symbol-metadata-reader.ts +95 -18
  191. package/src/metadata/verifying-metadata-reader.ts +41 -0
  192. package/src/resolution/{activation-need.ts → cache/activation-need.ts} +8 -8
  193. package/src/resolution/{binding-lookup-cache.ts → cache/binding-lookup-cache.ts} +63 -5
  194. package/src/resolution/cache/class-introspector.ts +251 -0
  195. package/src/resolution/{environment.ts → context.ts} +84 -145
  196. package/src/resolution/path/resolution-path.ts +167 -0
  197. package/src/resolution/plan/instantiation-plan.ts +615 -0
  198. package/src/resolution/resolver.ts +663 -313
  199. package/src/resolution/select/binding-select.ts +210 -0
  200. package/src/resolution/select/constraints.ts +130 -0
  201. package/dist/binding.d.ts.map +0 -1
  202. package/dist/binding.js.map +0 -1
  203. package/dist/constructor-type.d.ts.map +0 -1
  204. package/dist/constructor-type.js.map +0 -1
  205. package/dist/errors.d.ts.map +0 -1
  206. package/dist/errors.js.map +0 -1
  207. package/dist/module.d.ts.map +0 -1
  208. package/dist/module.js.map +0 -1
  209. package/dist/registry.d.ts.map +0 -1
  210. package/dist/registry.js.map +0 -1
  211. package/dist/resolution/activation-need.d.ts.map +0 -1
  212. package/dist/resolution/activation-need.js.map +0 -1
  213. package/dist/resolution/binding-lookup-cache.d.ts.map +0 -1
  214. package/dist/resolution/binding-lookup-cache.js.map +0 -1
  215. package/dist/resolution/binding-scope.d.ts.map +0 -1
  216. package/dist/resolution/binding-scope.js.map +0 -1
  217. package/dist/resolution/binding-select.d.ts +0 -23
  218. package/dist/resolution/binding-select.d.ts.map +0 -1
  219. package/dist/resolution/binding-select.js +0 -121
  220. package/dist/resolution/binding-select.js.map +0 -1
  221. package/dist/resolution/class-introspector.d.ts +0 -27
  222. package/dist/resolution/class-introspector.d.ts.map +0 -1
  223. package/dist/resolution/class-introspector.js +0 -60
  224. package/dist/resolution/class-introspector.js.map +0 -1
  225. package/dist/resolution/constraints.d.ts.map +0 -1
  226. package/dist/resolution/constraints.js.map +0 -1
  227. package/dist/resolution/diagnostics.d.ts.map +0 -1
  228. package/dist/resolution/diagnostics.js.map +0 -1
  229. package/dist/resolution/environment.d.ts +0 -94
  230. package/dist/resolution/environment.d.ts.map +0 -1
  231. package/dist/resolution/environment.js.map +0 -1
  232. package/dist/resolution/instantiation-plan.d.ts.map +0 -1
  233. package/dist/resolution/instantiation-plan.js +0 -183
  234. package/dist/resolution/instantiation-plan.js.map +0 -1
  235. package/dist/resolution/lifecycle.d.ts +0 -23
  236. package/dist/resolution/lifecycle.d.ts.map +0 -1
  237. package/dist/resolution/lifecycle.js.map +0 -1
  238. package/dist/resolution/resolution-path.d.ts +0 -98
  239. package/dist/resolution/resolution-path.d.ts.map +0 -1
  240. package/dist/resolution/resolution-path.js +0 -98
  241. package/dist/resolution/resolution-path.js.map +0 -1
  242. package/dist/resolution/resolve-options.d.ts.map +0 -1
  243. package/dist/resolution/resolve-options.js +0 -56
  244. package/dist/resolution/resolve-options.js.map +0 -1
  245. package/dist/resolution/scope.d.ts +0 -28
  246. package/dist/resolution/scope.d.ts.map +0 -1
  247. package/dist/resolution/scope.js +0 -88
  248. package/dist/resolution/scope.js.map +0 -1
  249. package/dist/token.d.ts.map +0 -1
  250. package/dist/token.js +0 -22
  251. package/dist/token.js.map +0 -1
  252. package/dist/types.d.ts.map +0 -1
  253. package/dist/types.js.map +0 -1
  254. package/src/registry.ts +0 -282
  255. package/src/resolution/binding-select.ts +0 -154
  256. package/src/resolution/class-introspector.ts +0 -74
  257. package/src/resolution/constraints.ts +0 -121
  258. package/src/resolution/instantiation-plan.ts +0 -292
  259. package/src/resolution/resolution-path.ts +0 -156
  260. package/src/resolution/resolve-options.ts +0 -89
  261. package/src/resolution/scope.ts +0 -105
  262. /package/dist/{resolution → core}/binding-scope.js +0 -0
  263. /package/dist/{constructor-type.js → core/constructor-type.js} +0 -0
  264. /package/dist/{types.js → core/types.js} +0 -0
  265. /package/dist/{resolution → errors}/diagnostics.js +0 -0
  266. /package/dist/resolution/{activation-need.js → cache/activation-need.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,84 @@ 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
+ },
439
+ // The named rule verbatim, on the single-tag lane's memo.
440
+ lookupPathIndependentTaggedEntry: (token, options) => {
441
+ const singleTag = singleTagOnlyOf(options);
442
+ if (singleTag === undefined) {
443
+ return null;
444
+ }
445
+ const entry = this.#lookup.taggedEntry(token, singleTag);
446
+ if (entry === null || entry.binding.predicate !== undefined || !matchesSlot(entry.binding.slot, options)) {
447
+ return null;
448
+ }
449
+ return { binding: entry.binding };
450
+ },
399
451
  getResolutionFrame: (binding) => this.#getResolutionFrame(binding),
400
452
  // Dispatches exactly as #resolveDep does, so an escaped dep is indistinguishable
401
453
  // from the same dep on a fully interpreted resolve.
402
- resolveEscaped: (token, options, arity, resolutionPath, resolutionStack) => {
454
+ resolveEscaped: (token, options, arity, resolutionStack) => {
403
455
  if (arity === "all") {
404
- return this.resolveAll(token, options, resolutionPath, resolutionStack);
456
+ return this.resolveAll(token, options, resolutionStack);
405
457
  }
406
458
  if (arity === "optional") {
407
- return this.resolveOptional(token, options, resolutionPath, resolutionStack);
459
+ return this.resolveOptional(token, options, resolutionStack);
408
460
  }
409
461
  if (options === undefined) {
410
- return this.resolveFromContext(token, resolutionPath, resolutionStack);
462
+ return this.resolveFromContext(token, resolutionStack);
411
463
  }
412
- return this.resolve(token, options, resolutionPath, resolutionStack);
464
+ return this.resolve(token, options, resolutionStack);
465
+ },
466
+ // Dispatches exactly as #resolveDepAsync does, for the async lane's escapes.
467
+ resolveEscapedAsync: (token, options, arity, resolutionStack) => {
468
+ if (arity === "all") {
469
+ return this.resolveAllAsync(token, options, resolutionStack, UNOWNED_BRANCH);
470
+ }
471
+ if (arity === "optional") {
472
+ return this.resolveOptionalAsync(token, options, resolutionStack, UNOWNED_BRANCH);
473
+ }
474
+ if (options === undefined) {
475
+ return this.resolveAsyncFromContext(token, resolutionStack, UNOWNED_BRANCH);
476
+ }
477
+ return this.resolveAsync(token, options, resolutionStack, UNOWNED_BRANCH);
413
478
  },
414
479
  });
415
480
 
416
- resolve<const Value>(
481
+ /** The async lane's plan for a statically-visible transient binding, mirroring the sync getter. */
482
+ #getAsyncInstantiationPlan(
483
+ binding: Binding & { kind: "class" | "resolved" | "resolved-async" },
484
+ ): (() => unknown) | null {
485
+ const registryVersion = this.#lookup.chainVersion();
486
+ const activationVersion = this.#chainActivationVersion();
487
+ if (registryVersion !== this.#asyncPlanRegistryVersion || activationVersion !== this.#asyncPlanActivationVersion) {
488
+ this.#asyncPlanByBindingId.clear();
489
+ this.#asyncPlanRegistryVersion = registryVersion;
490
+ this.#asyncPlanActivationVersion = activationVersion;
491
+ }
492
+ const cached = this.#asyncPlanByBindingId.get(binding.id);
493
+ if (cached !== undefined) {
494
+ return cached;
495
+ }
496
+ const compiled = this.#planCompiler.compileAsync(binding);
497
+ if (compiled === PLAN_RETRY) {
498
+ // Lifecycle metadata not discovered yet — the fallback resolve discovers it; retry then.
499
+ return null;
500
+ }
501
+ this.#asyncPlanByBindingId.set(binding.id, compiled);
502
+ return compiled;
503
+ }
504
+
505
+ resolve<Value>(
417
506
  token: Token<Value> | Constructor<Value>,
418
507
  options: ResolveOptions | undefined,
419
- resolutionPath: Array<string>,
420
508
  resolutionStack: Array<ResolutionFrame>,
421
509
  ): Value {
422
510
  // Name-only fast lane: memoized lookup, dispatching just the shapes whose
@@ -425,7 +513,7 @@ export class DependencyResolver implements ResolverCallbacks {
425
513
  const namedEntry = this.#lookup.namedEntry(token, options.name);
426
514
  if (namedEntry !== null) {
427
515
  const namedBinding = namedEntry.binding;
428
- if (this.#isPlainConstant(namedBinding)) {
516
+ if (namedEntry.owner.#isPlainConstant(namedBinding)) {
429
517
  return namedBinding.value as Value;
430
518
  }
431
519
  if (namedBinding.scope === "singleton" && namedBinding.instance !== NO_INSTANCE) {
@@ -433,24 +521,40 @@ export class DependencyResolver implements ResolverCallbacks {
433
521
  }
434
522
  // Everything else keeps the full path (context, activation, guards).
435
523
  }
524
+ } else if (options !== undefined) {
525
+ // Single-tag fast lane: the named lane's tagged twin, memoizing the chain walk.
526
+ const singleTag = singleTagOnlyOf(options);
527
+ if (singleTag !== undefined) {
528
+ const taggedEntry = this.#lookup.taggedEntry(token, singleTag);
529
+ if (taggedEntry !== null) {
530
+ const taggedBinding = taggedEntry.binding;
531
+ if (taggedEntry.owner.#isPlainConstant(taggedBinding)) {
532
+ return taggedBinding.value as Value;
533
+ }
534
+ if (taggedBinding.scope === "singleton" && taggedBinding.instance !== NO_INSTANCE) {
535
+ return taggedBinding.instance as Value;
536
+ }
537
+ // Everything else keeps the full path (context, activation, guards).
538
+ }
539
+ }
436
540
  }
437
541
 
438
- const { binding, owner } = this.#requireBinding(token, options, resolutionPath, resolutionStack);
542
+ const { binding, owner } = this.#requireBinding(token, options, resolutionStack);
439
543
 
440
544
  // A singleton owned by a parent resolver is resolved there, so the parent caches it.
441
545
  if (binding.scope === "singleton" && owner !== this) {
442
- return owner.#resolveBinding(binding, options, resolutionPath, resolutionStack) as Value;
546
+ return owner.#resolveBinding(binding, options, resolutionStack, owner) as Value;
443
547
  }
444
- return this.#resolveBinding(binding, options, resolutionPath, resolutionStack) as Value;
548
+ return this.#resolveBinding(binding, options, resolutionStack, owner) as Value;
445
549
  }
446
550
 
447
551
  #resolveBinding(
448
552
  binding: Binding,
449
553
  options: ResolveOptions | undefined,
450
- resolutionPath: Array<string>,
451
554
  resolutionStack: Array<ResolutionFrame>,
555
+ owner: DependencyResolver,
452
556
  ): unknown {
453
- if (this.#isPlainConstant(binding)) {
557
+ if (owner.#isPlainConstant(binding)) {
454
558
  return binding.value;
455
559
  }
456
560
 
@@ -459,62 +563,91 @@ export class DependencyResolver implements ResolverCallbacks {
459
563
  if (binding.instance !== NO_INSTANCE) {
460
564
  return binding.instance;
461
565
  }
566
+ // An async materialization already in flight must not be raced by a second, sync one.
567
+ if (this.#scope.getInflight(binding.id) !== undefined) {
568
+ throw new AsyncResolutionError(
569
+ resolutionStack[0]?.tokenName ?? tokenName(binding.token),
570
+ tokenName(binding.token),
571
+ );
572
+ }
573
+ if (this.#scope.isClosed) {
574
+ throw new DisposedContainerError();
575
+ }
462
576
  } else if (scope === "scoped") {
463
577
  const cachedScoped = this.#readScoped(binding);
464
578
  if (cachedScoped !== SCOPED_MISS) {
465
579
  return cachedScoped;
466
580
  }
581
+ if (this.#scope.getInflight(binding.id) !== undefined) {
582
+ throw new AsyncResolutionError(
583
+ resolutionStack[0]?.tokenName ?? tokenName(binding.token),
584
+ tokenName(binding.token),
585
+ );
586
+ }
587
+ if (this.#scope.isClosed) {
588
+ throw new DisposedContainerError();
589
+ }
467
590
  }
468
591
 
469
592
  const frame = this.#getResolutionFrame(binding);
470
593
  const tokenDisplayName = frame.tokenName;
471
- const resolutionSet = enterResolutionPath(resolutionPath, tokenDisplayName);
472
- resolutionStack.push(frame);
594
+ const resolutionSet = enterResolutionPath(resolutionStack, frame);
473
595
  try {
474
- const needsActivation = this.#activation.needsActivation(binding);
596
+ const needsActivation = owner.#activation.needsActivation(binding);
475
597
  if (!needsActivation && scope === "transient" && binding.kind === "dynamic") {
476
- const resolutionCtx = this.#acquireSyncResolutionContext(resolutionPath, resolutionStack, options);
598
+ const resolutionCtx = this.#acquireSyncResolutionContext(resolutionStack, options);
477
599
  const dynamicResult = binding.factory(resolutionCtx);
478
600
  if (dynamicResult instanceof Promise) {
479
- throw new AsyncResolutionError(tokenDisplayName, tokenDisplayName);
601
+ throw new AsyncResolutionError(resolutionStack[0]?.tokenName ?? tokenDisplayName, tokenDisplayName);
480
602
  }
481
603
  return dynamicResult;
482
604
  }
483
605
 
484
606
  const resolutionCtx =
485
607
  needsActivation || requiresResolutionContext(binding)
486
- ? this.#acquireSyncResolutionContext(resolutionPath, resolutionStack, options)
608
+ ? this.#acquireSyncResolutionContext(resolutionStack, options)
487
609
  : undefined;
488
610
 
489
- const instance = this.#instantiateSync(binding, resolutionCtx, resolutionPath, resolutionStack);
611
+ const instance = this.#instantiateSync(binding, resolutionCtx, resolutionStack);
490
612
 
491
- const activated = this.#activation.refreshAfterFirstInstantiation(binding, needsActivation)
492
- ? this.#lifecycle.runActivationSync(
613
+ this.#mirrorPostConstructFromOwner(binding, owner);
614
+ const activated = owner.#activation.refreshAfterFirstInstantiation(binding, needsActivation)
615
+ ? owner.#lifecycle.runActivationSync(
493
616
  resolutionCtx as DefaultResolutionContext,
494
617
  binding,
495
618
  instance,
496
- this.#metadataReader,
619
+ owner.#metadataReader,
497
620
  )
498
621
  : instance;
499
622
 
500
623
  if (scope === "singleton") {
501
624
  this.#scope.setSingleton(binding, activated);
502
625
  } else if (scope === "scoped") {
503
- this.#scope.setScoped(binding.id, activated);
626
+ this.#scope.setScoped(binding, activated);
504
627
  }
505
628
 
506
629
  return activated;
507
630
  } finally {
508
631
  resolutionStack.pop();
509
- resolutionPath.pop();
510
- resolutionSet?.delete(tokenDisplayName);
632
+ resolutionSet?.delete(frame.bindingId);
511
633
  }
512
634
  }
513
635
 
636
+ /** Path-continuing resolution handed to the ambient slot while an accessor class constructs. */
637
+ #ambientResolutionFor(resolutionStack: Array<ResolutionFrame>): AmbientResolution {
638
+ return {
639
+ resolve: <Value>(token: Token<Value> | Constructor<Value>, options?: ResolveOptions): Value =>
640
+ options === undefined
641
+ ? this.resolveFromContext(token, resolutionStack)
642
+ : this.resolve(token, options, resolutionStack),
643
+ resolveOptional: <Value>(token: Token<Value> | Constructor<Value>, options?: ResolveOptions): Value | undefined =>
644
+ this.resolveOptional(token, options, resolutionStack),
645
+ };
646
+ }
647
+
514
648
  #instantiateSync(
515
649
  binding: Binding,
516
650
  ctx: DefaultResolutionContext | undefined,
517
- resolutionPath: Array<string>,
518
651
  resolutionStack: Array<ResolutionFrame>,
519
652
  ): unknown {
520
653
  switch (binding.kind) {
@@ -527,30 +660,34 @@ export class DependencyResolver implements ResolverCallbacks {
527
660
  }
528
661
  const factoryResult = binding.factory(ctx);
529
662
  if (factoryResult instanceof Promise) {
530
- throw new AsyncResolutionError(tokenName(binding.token), tokenName(binding.token));
663
+ throw asyncResolutionErrorFor(binding, resolutionStack);
531
664
  }
532
665
  return factoryResult;
533
666
  }
534
667
 
535
668
  case "dynamic-async":
536
- throw new AsyncResolutionError(tokenName(binding.token), tokenName(binding.token));
669
+ throw asyncResolutionErrorFor(binding, resolutionStack);
537
670
 
538
671
  case "class": {
539
- const deps = this.#resolveDeps(this.#constructorParams(binding.target), resolutionPath, resolutionStack);
540
- return this.#classes.instantiate(binding.target, deps);
672
+ const deps = this.#resolveDeps(this.#constructorParams(binding.target), resolutionStack);
673
+ return this.#classes.instantiate(
674
+ binding.target,
675
+ deps,
676
+ this.#classes.needsActiveContainer(binding.target) ? this.#ambientResolutionFor(resolutionStack) : undefined,
677
+ );
541
678
  }
542
679
 
543
680
  case "resolved": {
544
- const deps = this.#resolveDeps(binding.deps, resolutionPath, resolutionStack);
681
+ const deps = this.#resolveDeps(binding.deps, resolutionStack);
545
682
  const factoryResult = binding.factory(...deps);
546
683
  if (factoryResult instanceof Promise) {
547
- throw new AsyncResolutionError(tokenName(binding.token), tokenName(binding.token));
684
+ throw asyncResolutionErrorFor(binding, resolutionStack);
548
685
  }
549
686
  return factoryResult;
550
687
  }
551
688
 
552
689
  case "resolved-async":
553
- throw new AsyncResolutionError(tokenName(binding.token), tokenName(binding.token));
690
+ throw asyncResolutionErrorFor(binding, resolutionStack);
554
691
 
555
692
  case "alias":
556
693
  throw new InternalError("alias should have been followed before instantiation");
@@ -576,66 +713,65 @@ export class DependencyResolver implements ResolverCallbacks {
576
713
 
577
714
  // One dispatch table for both dependency sources — constructor params and `toResolved`
578
715
  // descriptors declare the same four things.
579
- #resolveDeps(
580
- deps: ReadonlyArray<DependencySlot>,
581
- resolutionPath: Array<string>,
582
- resolutionStack: Array<ResolutionFrame>,
583
- ): Array<unknown> {
716
+ #resolveDeps(deps: ReadonlyArray<DependencySlot>, resolutionStack: Array<ResolutionFrame>): Array<unknown> {
584
717
  const count = deps.length;
585
718
  if (count === 0) {
586
719
  return [];
587
720
  }
588
721
  if (count === 1) {
589
- return [this.#resolveDep(deps[0]!, resolutionPath, resolutionStack)];
722
+ return [this.#resolveDep(deps[0]!, resolutionStack)];
590
723
  }
591
724
  const resolved = new Array<unknown>(count);
592
725
  for (let index = 0; index < count; index += 1) {
593
- resolved[index] = this.#resolveDep(deps[index]!, resolutionPath, resolutionStack);
726
+ resolved[index] = this.#resolveDep(deps[index]!, resolutionStack);
594
727
  }
595
728
  return resolved;
596
729
  }
597
730
 
598
- #resolveDep(dep: DependencySlot, resolutionPath: Array<string>, resolutionStack: Array<ResolutionFrame>): unknown {
599
- const options = injectionSlotToResolveOptions(dep);
731
+ #resolveDep(dep: DependencySlot, resolutionStack: Array<ResolutionFrame>): unknown {
732
+ const options = resolveOptionsForSlot(dep);
600
733
  if (dep.multi) {
601
- return this.resolveAll(dep.token, options, resolutionPath, resolutionStack);
734
+ return this.resolveAll(dep.token, options, resolutionStack);
602
735
  }
603
736
  if (dep.optional) {
604
- return this.resolveOptional(dep.token, options, resolutionPath, resolutionStack);
737
+ return this.resolveOptional(dep.token, options, resolutionStack);
605
738
  }
606
739
  if (options === undefined) {
607
- return this.resolveFromContext(dep.token, resolutionPath, resolutionStack);
740
+ return this.resolveFromContext(dep.token, resolutionStack);
608
741
  }
609
- return this.resolve(dep.token, options, resolutionPath, resolutionStack);
742
+ return this.resolve(dep.token, options, resolutionStack);
610
743
  }
611
744
 
612
- resolveOptional<const Value>(
745
+ resolveOptional<Value>(
613
746
  token: Token<Value> | Constructor<Value>,
614
747
  options: ResolveOptions | undefined,
615
- resolutionPath: Array<string>,
616
748
  resolutionStack: Array<ResolutionFrame>,
617
749
  ): Value | undefined {
618
- if (this.#findBinding(token, options, resolutionPath, resolutionStack) === undefined) {
750
+ const entry = this.#findBinding(token, options, resolutionStack);
751
+ if (entry === undefined) {
619
752
  return undefined;
620
753
  }
621
- return this.resolve(token, options, resolutionPath, resolutionStack);
754
+ // Resolve the entry the probe found: re-looking the token up would evaluate every `when()`
755
+ // predicate a second time, and a changed answer would throw where `undefined` was promised.
756
+ const { binding, owner } = entry;
757
+ if (binding.kind === "alias") {
758
+ return this.resolve(token, options, resolutionStack);
759
+ }
760
+ if (binding.scope === "singleton" && owner !== this) {
761
+ return owner.#resolveBinding(binding, options, resolutionStack, owner) as Value;
762
+ }
763
+ return this.#resolveBinding(binding, options, resolutionStack, owner) as Value;
622
764
  }
623
765
 
624
- resolveAll<const Value>(
766
+ resolveAll<Value>(
625
767
  token: Token<Value> | Constructor<Value>,
626
768
  options: ResolveOptions | undefined,
627
- resolutionPath: Array<string>,
628
769
  resolutionStack: Array<ResolutionFrame>,
629
770
  ): Array<Value> {
630
- const candidates = this.#candidateBindings(token, options, resolutionPath, resolutionStack);
771
+ const candidates = this.#candidateBindings(token, options, resolutionStack);
631
772
  const resolved = new Array<Value>(candidates.length);
632
773
  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;
774
+ resolved[index] = this.#resolveCandidateSync(candidates[index]!, options, resolutionStack) as Value;
639
775
  }
640
776
  return resolved;
641
777
  }
@@ -644,34 +780,25 @@ export class DependencyResolver implements ResolverCallbacks {
644
780
  #candidateBindings(
645
781
  token: Token<unknown> | Constructor,
646
782
  options: ResolveOptions | undefined,
647
- resolutionPath: Array<string>,
648
783
  resolutionStack: Array<ResolutionFrame>,
649
784
  ): 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;
785
+ if (options !== undefined) {
786
+ const indexed = this.#indexedCandidates(token, options, resolutionStack);
787
+ if (indexed !== null) {
788
+ return indexed;
656
789
  }
657
- return selectAllBindings(named, options, this.#makeConstraintContext(resolutionPath, resolutionStack, options));
658
790
  }
659
791
  const allBindings = this.#allBindingsFromChain(token);
660
792
  if (allBindings.length === 0) {
661
793
  return allBindings;
662
794
  }
663
- return selectAllBindings(
664
- allBindings,
665
- options,
666
- this.#makeConstraintContext(resolutionPath, resolutionStack, options),
667
- );
795
+ return selectAllBindings(allBindings, options, this.#makeConstraintContext(resolutionStack, options));
668
796
  }
669
797
 
670
- // ── Async resolve ──────────────────────────────────────────────────────────
798
+ // ── Async resolve ──────────────────────────────────────────────────────────────────────────────────────────────────
671
799
 
672
- resolveAsyncFromContext<const Value>(
800
+ resolveAsyncFromContext<Value>(
673
801
  token: Token<Value> | Constructor<Value>,
674
- resolutionPath: Array<string>,
675
802
  resolutionStack: Array<ResolutionFrame>,
676
803
  branchDepth: BranchDepth,
677
804
  ): Promise<Value> {
@@ -687,88 +814,74 @@ export class DependencyResolver implements ResolverCallbacks {
687
814
  ) {
688
815
  return this.#resolveTransientDynamicAsyncFromContext(
689
816
  fastBinding,
690
- resolutionPath,
691
817
  resolutionStack,
692
818
  branchDepth,
693
819
  ) as Promise<Value>;
694
820
  }
695
- return this.#resolveAsyncDefaultEntry(
696
- fastBinding,
697
- this,
698
- resolutionPath,
699
- resolutionStack,
700
- branchDepth,
701
- ) as Promise<Value>;
821
+ return this.#resolveAsyncDefaultEntry(fastBinding, this, resolutionStack, branchDepth) as Promise<Value>;
702
822
  }
703
823
  const entry = this.#lookup.defaultEntry(token);
704
824
  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>;
825
+ return this.resolveAsync(token, undefined, resolutionStack, branchDepth);
826
+ }
827
+ return this.#resolveAsyncDefaultEntry(entry.binding, entry.owner, resolutionStack, branchDepth) as Promise<Value>;
714
828
  }
715
829
 
716
830
  #resolveAsyncDefaultEntry(
717
831
  binding: Binding,
718
832
  owner: DependencyResolver,
719
- resolutionPath: Array<string>,
720
833
  resolutionStack: Array<ResolutionFrame>,
721
834
  branchDepth: BranchDepth,
722
835
  ): Promise<unknown> {
723
- if (this.#isPlainConstant(binding)) {
836
+ if (owner.#isPlainConstant(binding)) {
724
837
  return Promise.resolve(binding.value);
725
838
  }
726
839
  const scope = binding.scope;
727
840
  if (scope === "transient") {
728
- if ((binding.kind === "dynamic" || binding.kind === "dynamic-async") && !this.#hasAnyActivation(binding)) {
729
- return this.#resolveTransientDynamicAsyncFromContext(binding, resolutionPath, resolutionStack, branchDepth);
841
+ if ((binding.kind === "dynamic" || binding.kind === "dynamic-async") && !owner.#hasAnyActivation(binding)) {
842
+ return this.#resolveTransientDynamicAsyncFromContext(binding, resolutionStack, branchDepth);
730
843
  }
731
844
  } else if (scope === "singleton") {
732
845
  if (binding.instance !== NO_INSTANCE) {
733
846
  return Promise.resolve(binding.instance);
734
847
  }
735
848
  if (owner !== this) {
736
- return owner.#resolveBindingAsync(binding, undefined, resolutionPath, resolutionStack, branchDepth);
849
+ return owner.#resolveBindingAsync(binding, undefined, resolutionStack, branchDepth, owner);
737
850
  }
738
851
  } else if (this.#scope.isChild) {
739
- if (this.#scope.hasScoped(binding.id)) {
740
- return Promise.resolve(this.#scope.getScoped(binding.id));
852
+ const cachedScoped = this.#scope.readScoped(binding.id);
853
+ if (cachedScoped !== SCOPED_MISS) {
854
+ return Promise.resolve(cachedScoped);
741
855
  }
742
856
  } else {
743
857
  // Not `#readScoped`: this entry point reports failure as a rejection, never a sync throw.
744
858
  return Promise.reject(new MissingScopeContextError(tokenName(binding.token)));
745
859
  }
746
- return this.#resolveBindingAsync(binding, undefined, resolutionPath, resolutionStack, branchDepth);
860
+ return this.#resolveBindingAsync(binding, undefined, resolutionStack, branchDepth, owner);
747
861
  }
748
862
 
749
- async resolveAsync<const Value>(
863
+ async resolveAsync<Value>(
750
864
  token: Token<Value> | Constructor<Value>,
751
865
  options: ResolveOptions | undefined,
752
- resolutionPath: Array<string>,
753
866
  resolutionStack: Array<ResolutionFrame>,
754
867
  branchDepth: BranchDepth = UNOWNED_BRANCH,
755
868
  ): Promise<Value> {
756
- const { binding, owner } = this.#requireBinding(token, options, resolutionPath, resolutionStack);
869
+ const { binding, owner } = this.#requireBinding(token, options, resolutionStack);
757
870
 
758
871
  if (binding.scope === "singleton" && owner !== this) {
759
- return owner.#resolveBindingAsync(binding, options, resolutionPath, resolutionStack, branchDepth) as Value;
872
+ return owner.#resolveBindingAsync(binding, options, resolutionStack, branchDepth, owner) as Promise<Value>;
760
873
  }
761
- return this.#resolveBindingAsync(binding, options, resolutionPath, resolutionStack, branchDepth) as Value;
874
+ return this.#resolveBindingAsync(binding, options, resolutionStack, branchDepth, owner) as Promise<Value>;
762
875
  }
763
876
 
764
877
  async #resolveBindingAsync(
765
878
  binding: Binding,
766
879
  options: ResolveOptions | undefined,
767
- resolutionPath: Array<string>,
768
880
  resolutionStack: Array<ResolutionFrame>,
769
881
  branchDepth: BranchDepth,
882
+ owner: DependencyResolver,
770
883
  ): Promise<unknown> {
771
- if (this.#isPlainConstant(binding)) {
884
+ if (owner.#isPlainConstant(binding)) {
772
885
  return binding.value;
773
886
  }
774
887
 
@@ -782,22 +895,32 @@ export class DependencyResolver implements ResolverCallbacks {
782
895
  if (inflight !== undefined) {
783
896
  return inflight;
784
897
  }
898
+ if (this.#scope.isClosed) {
899
+ throw new DisposedContainerError();
900
+ }
785
901
  } else if (scope === "scoped") {
786
902
  const cachedScoped = this.#readScoped(binding);
787
903
  if (cachedScoped !== SCOPED_MISS) {
788
904
  return cachedScoped;
789
905
  }
906
+ // In-flight dedup, scoped flavor: one instance per scope even under concurrency.
907
+ const inflight = this.#scope.getInflight(binding.id);
908
+ if (inflight !== undefined) {
909
+ return inflight;
910
+ }
911
+ if (this.#scope.isClosed) {
912
+ throw new DisposedContainerError();
913
+ }
790
914
  }
791
915
 
792
916
  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);
917
+ // This level appends to its own branch and never unwinds.
918
+ const levelStack = extendResolutionBranch(resolutionStack, branchDepth, frame);
919
+ const levelDepth = branchDepthOf(levelStack);
797
920
 
798
- const needsActivation = this.#activation.needsActivation(binding);
921
+ const needsActivation = owner.#activation.needsActivation(binding);
799
922
  if (!needsActivation && scope === "transient" && (binding.kind === "dynamic" || binding.kind === "dynamic-async")) {
800
- const resolutionCtx = new AsyncLevelContext(this, levelPath, levelStack, options);
923
+ const resolutionCtx = new AsyncLevelContext(this, levelStack, options);
801
924
  if (binding.kind === "dynamic-async") {
802
925
  return await binding.factory(resolutionCtx);
803
926
  }
@@ -807,7 +930,7 @@ export class DependencyResolver implements ResolverCallbacks {
807
930
 
808
931
  const resolutionCtx =
809
932
  needsActivation || requiresResolutionContext(binding)
810
- ? new AsyncLevelContext(this, levelPath, levelStack, options)
933
+ ? new AsyncLevelContext(this, levelStack, options)
811
934
  : undefined;
812
935
 
813
936
  if (scope === "singleton") {
@@ -815,10 +938,10 @@ export class DependencyResolver implements ResolverCallbacks {
815
938
  const singletonPromise = this.#instantiateAndActivateAsync(
816
939
  binding,
817
940
  resolutionCtx,
818
- levelPath,
819
941
  levelStack,
820
942
  levelDepth,
821
943
  needsActivation,
944
+ owner,
822
945
  ).then(
823
946
  (activated) => {
824
947
  this.#scope.setSingleton(binding, activated);
@@ -834,39 +957,72 @@ export class DependencyResolver implements ResolverCallbacks {
834
957
  return await singletonPromise;
835
958
  }
836
959
 
837
- const activated = await this.#instantiateAndActivateAsync(
960
+ if (scope === "scoped") {
961
+ // Published before it settles, like the singleton lane: concurrent callers share one creation.
962
+ const scopedPromise = this.#instantiateAndActivateAsync(
963
+ binding,
964
+ resolutionCtx,
965
+ levelStack,
966
+ levelDepth,
967
+ needsActivation,
968
+ owner,
969
+ ).then(
970
+ (activated) => {
971
+ this.#scope.setScoped(binding, activated);
972
+ this.#scope.clearInflight(binding.id);
973
+ return activated;
974
+ },
975
+ (error: unknown) => {
976
+ this.#scope.clearInflight(binding.id);
977
+ throw error;
978
+ },
979
+ );
980
+ this.#scope.setInflight(binding.id, scopedPromise as Promise<unknown>);
981
+ return await scopedPromise;
982
+ }
983
+
984
+ return await this.#instantiateAndActivateAsync(
838
985
  binding,
839
986
  resolutionCtx,
840
- levelPath,
841
987
  levelStack,
842
988
  levelDepth,
843
989
  needsActivation,
990
+ owner,
844
991
  );
845
- if (scope === "scoped") {
846
- this.#scope.setScoped(binding.id, activated);
992
+ }
993
+
994
+ /**
995
+ * Settles this resolver's own `postConstruct` answer for a class binding a parent owns.
996
+ *
997
+ * @remarks The owner discovers it on first instantiation, but the plan compiler reads the
998
+ * introspector of whoever is resolving — left unknown, that resolver refuses to compile a plan for
999
+ * this binding on every call, forever.
1000
+ */
1001
+ #mirrorPostConstructFromOwner(binding: Binding, owner: DependencyResolver): void {
1002
+ if (owner !== this && binding.kind === "class" && this.#classes.knownPostConstruct(binding.target) === undefined) {
1003
+ this.#classes.discoverPostConstruct(binding.target);
847
1004
  }
848
- return activated;
849
1005
  }
850
1006
 
851
1007
  async #instantiateAndActivateAsync(
852
1008
  binding: Binding,
853
1009
  ctx: AsyncLevelContext | undefined,
854
- resolutionPath: Array<string>,
855
1010
  resolutionStack: Array<ResolutionFrame>,
856
1011
  branchDepth: BranchDepth,
857
1012
  needsActivation: boolean,
1013
+ owner: DependencyResolver,
858
1014
  ): Promise<unknown> {
859
- const instance = await this.#instantiateAsync(binding, ctx, resolutionPath, resolutionStack, branchDepth);
860
- if (!this.#activation.refreshAfterFirstInstantiation(binding, needsActivation)) {
1015
+ const instance = await this.#instantiateAsync(binding, ctx, resolutionStack, branchDepth);
1016
+ this.#mirrorPostConstructFromOwner(binding, owner);
1017
+ if (!owner.#activation.refreshAfterFirstInstantiation(binding, needsActivation)) {
861
1018
  return instance;
862
1019
  }
863
- return this.#lifecycle.runActivation(ctx as AsyncLevelContext, binding, instance, this.#metadataReader);
1020
+ return owner.#lifecycle.runActivation(ctx as AsyncLevelContext, binding, instance, owner.#metadataReader);
864
1021
  }
865
1022
 
866
1023
  async #instantiateAsync(
867
1024
  binding: Binding,
868
1025
  ctx: AsyncLevelContext | undefined,
869
- resolutionPath: Array<string>,
870
1026
  resolutionStack: Array<ResolutionFrame>,
871
1027
  branchDepth: BranchDepth,
872
1028
  ): Promise<unknown> {
@@ -891,24 +1047,25 @@ export class DependencyResolver implements ResolverCallbacks {
891
1047
  case "class": {
892
1048
  const deps = await this.#resolveDepsAsync(
893
1049
  this.#constructorParams(binding.target),
894
- resolutionPath,
895
1050
  resolutionStack,
896
1051
  branchDepth,
897
1052
  );
898
- return this.#classes.instantiate(binding.target, deps);
1053
+ // Accessor initializers resolve synchronously, so the branch-owned path serves them directly.
1054
+ return this.#classes.instantiate(
1055
+ binding.target,
1056
+ deps,
1057
+ this.#classes.needsActiveContainer(binding.target) ? this.#ambientResolutionFor(resolutionStack) : undefined,
1058
+ );
899
1059
  }
900
1060
 
901
1061
  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);
1062
+ const deps = await this.#resolveDepsAsync(binding.deps, resolutionStack, branchDepth);
906
1063
  const factoryResult = binding.factory(...deps);
907
1064
  return factoryResult instanceof Promise ? factoryResult : Promise.resolve(factoryResult);
908
1065
  }
909
1066
 
910
1067
  case "resolved-async": {
911
- const deps = await this.#resolveDepsAsync(binding.deps, resolutionPath, resolutionStack, branchDepth);
1068
+ const deps = await this.#resolveDepsAsync(binding.deps, resolutionStack, branchDepth);
912
1069
  return binding.factory(...deps);
913
1070
  }
914
1071
 
@@ -919,7 +1076,6 @@ export class DependencyResolver implements ResolverCallbacks {
919
1076
 
920
1077
  async #resolveDepsAsync(
921
1078
  deps: ReadonlyArray<DependencySlot>,
922
- resolutionPath: Array<string>,
923
1079
  resolutionStack: Array<ResolutionFrame>,
924
1080
  branchDepth: BranchDepth,
925
1081
  ): Promise<Array<unknown>> {
@@ -928,63 +1084,68 @@ export class DependencyResolver implements ResolverCallbacks {
928
1084
  return [];
929
1085
  }
930
1086
  if (count === 1) {
931
- return [await this.#resolveDepAsync(deps[0]!, resolutionPath, resolutionStack, branchDepth)];
1087
+ return [await this.#resolveDepAsync(deps[0]!, resolutionStack, branchDepth)];
932
1088
  }
933
1089
  // Siblings resolve concurrently and each extends the same branch, so the first appends in
934
1090
  // place and the rest copy the prefix — no caller has to isolate them.
935
1091
  const pending = new Array<Promise<unknown>>(count);
936
1092
  for (let index = 0; index < count; index += 1) {
937
- pending[index] = this.#resolveDepAsync(deps[index]!, resolutionPath, resolutionStack, branchDepth);
1093
+ pending[index] = this.#resolveDepAsync(deps[index]!, resolutionStack, branchDepth);
938
1094
  }
939
1095
  return Promise.all(pending);
940
1096
  }
941
1097
 
942
1098
  #resolveDepAsync(
943
1099
  dep: DependencySlot,
944
- resolutionPath: Array<string>,
945
1100
  resolutionStack: Array<ResolutionFrame>,
946
1101
  branchDepth: BranchDepth,
947
1102
  ): Promise<unknown> {
948
- const options = injectionSlotToResolveOptions(dep);
1103
+ const options = resolveOptionsForSlot(dep);
949
1104
  if (dep.multi) {
950
- return this.resolveAllAsync(dep.token, options, resolutionPath, resolutionStack, branchDepth);
1105
+ return this.resolveAllAsync(dep.token, options, resolutionStack, branchDepth);
951
1106
  }
952
1107
  if (dep.optional) {
953
- return this.resolveOptionalAsync(dep.token, options, resolutionPath, resolutionStack, branchDepth);
1108
+ return this.resolveOptionalAsync(dep.token, options, resolutionStack, branchDepth);
954
1109
  }
955
1110
  if (options === undefined) {
956
- return this.resolveAsyncFromContext(dep.token, resolutionPath, resolutionStack, branchDepth);
1111
+ return this.resolveAsyncFromContext(dep.token, resolutionStack, branchDepth);
957
1112
  }
958
- return this.resolveAsync(dep.token, options, resolutionPath, resolutionStack, branchDepth);
1113
+ return this.resolveAsync(dep.token, options, resolutionStack, branchDepth);
959
1114
  }
960
1115
 
961
- async resolveOptionalAsync<const Value>(
1116
+ async resolveOptionalAsync<Value>(
962
1117
  token: Token<Value> | Constructor<Value>,
963
1118
  options: ResolveOptions | undefined,
964
- resolutionPath: Array<string>,
965
1119
  resolutionStack: Array<ResolutionFrame>,
966
1120
  branchDepth: BranchDepth = UNOWNED_BRANCH,
967
1121
  ): Promise<Value | undefined> {
968
- if (this.#findBinding(token, options, resolutionPath, resolutionStack) === undefined) {
1122
+ const entry = this.#findBinding(token, options, resolutionStack);
1123
+ if (entry === undefined) {
969
1124
  return undefined;
970
1125
  }
971
- return this.resolveAsync(token, options, resolutionPath, resolutionStack, branchDepth);
1126
+ // Same single-evaluation contract as the sync lane: resolve what the probe found.
1127
+ const { binding, owner } = entry;
1128
+ if (binding.kind === "alias") {
1129
+ return this.resolveAsync(token, options, resolutionStack, branchDepth);
1130
+ }
1131
+ if (binding.scope === "singleton" && owner !== this) {
1132
+ return owner.#resolveBindingAsync(binding, options, resolutionStack, branchDepth, owner) as Promise<Value>;
1133
+ }
1134
+ return this.#resolveBindingAsync(binding, options, resolutionStack, branchDepth, owner) as Promise<Value>;
972
1135
  }
973
1136
 
974
- async resolveAllAsync<const Value>(
1137
+ async resolveAllAsync<Value>(
975
1138
  token: Token<Value> | Constructor<Value>,
976
1139
  options: ResolveOptions | undefined,
977
- resolutionPath: Array<string>,
978
1140
  resolutionStack: Array<ResolutionFrame>,
979
1141
  branchDepth: BranchDepth = UNOWNED_BRANCH,
980
1142
  ): Promise<Array<Value>> {
981
- const candidates = this.#candidateBindings(token, options, resolutionPath, resolutionStack);
1143
+ const candidates = this.#candidateBindings(token, options, resolutionStack);
982
1144
  const pending = new Array<Promise<Value>>(candidates.length);
983
1145
  for (let index = 0; index < candidates.length; index += 1) {
984
1146
  pending[index] = this.#resolveCandidateAsync(
985
1147
  candidates[index]!,
986
1148
  options,
987
- resolutionPath,
988
1149
  resolutionStack,
989
1150
  branchDepth,
990
1151
  ) as Promise<Value>;
@@ -992,7 +1153,7 @@ export class DependencyResolver implements ResolverCallbacks {
992
1153
  return Promise.all(pending);
993
1154
  }
994
1155
 
995
- // ── Helpers ────────────────────────────────────────────────────────────────
1156
+ // ── Helpers ────────────────────────────────────────────────────────────────────────────────────────────────────────
996
1157
 
997
1158
  #allBindingsFromChain(token: Token<unknown> | Constructor): ReadonlyArray<Binding> {
998
1159
  const ownBindings = this.#registry.getAll(token);
@@ -1027,6 +1188,58 @@ export class DependencyResolver implements ResolverCallbacks {
1027
1188
  return result;
1028
1189
  }
1029
1190
 
1191
+ /**
1192
+ * The candidates an index can name outright, or `null` when the request needs full selection.
1193
+ *
1194
+ * @remarks Kept off `#candidateBindings` so that method stays the size it was: a request neither
1195
+ * index serves must not pay for the two that do.
1196
+ * An index has matched the slot already, but a hit may still carry a predicate, and evaluating
1197
+ * that is the selection path's job.
1198
+ */
1199
+ #indexedCandidates(
1200
+ token: Token<unknown> | Constructor,
1201
+ options: ResolveOptions,
1202
+ resolutionStack: Array<ResolutionFrame>,
1203
+ ): ReadonlyArray<Binding> | null {
1204
+ if (isNameOnlyOptions(options)) {
1205
+ const named = this.#namedBindingsFromChain(token, options.name);
1206
+ return anyPredicate(named)
1207
+ ? selectAllBindings(named, options, this.#makeConstraintContext(resolutionStack, options))
1208
+ : named;
1209
+ }
1210
+ const singleTag = singleTagOnlyOf(options);
1211
+ if (singleTag === undefined) {
1212
+ return null;
1213
+ }
1214
+ const tagged = this.#taggedBindingsFromChain(token, singleTag);
1215
+ return anyPredicate(tagged)
1216
+ ? selectAllBindings(tagged, options, this.#makeConstraintContext(resolutionStack, options))
1217
+ : tagged;
1218
+ }
1219
+
1220
+ /**
1221
+ * Every binding the chain's tag indexes hold for one tag, nearest container first.
1222
+ *
1223
+ * @remarks A request for one tag and no name matches exactly the bindings the index keys, so this
1224
+ * is the whole candidate set rather than a prefilter — a named or multi-tag slot cannot satisfy it.
1225
+ */
1226
+ #taggedBindingsFromChain(token: Token<unknown> | Constructor, tag: BindingTag): Array<Binding> {
1227
+ // A tag matches at most one binding per registry, so a root container's answer is built whole
1228
+ // rather than grown — the shape `#namedBindingsFromChain` takes, for the same reason.
1229
+ const ownBinding = this.#registry.getSimpleTagged(token, tag);
1230
+ if (this.#parent === undefined) {
1231
+ return ownBinding === undefined ? [] : [ownBinding];
1232
+ }
1233
+ const result: Array<Binding> = ownBinding === undefined ? [] : [ownBinding];
1234
+ for (let current: DependencyResolver | undefined = this.#parent; current !== undefined; current = current.#parent) {
1235
+ const binding = current.#registry.getSimpleTagged(token, tag);
1236
+ if (binding !== undefined) {
1237
+ result.push(binding);
1238
+ }
1239
+ }
1240
+ return result;
1241
+ }
1242
+
1030
1243
  /** A constant with no activation anywhere resolves to its value with no pipeline at all. */
1031
1244
  #isPlainConstant(binding: Binding): binding is ConstantBinding<unknown> {
1032
1245
  return (
@@ -1054,68 +1267,111 @@ export class DependencyResolver implements ResolverCallbacks {
1054
1267
  if (!this.#scope.isChild) {
1055
1268
  throw new MissingScopeContextError(tokenName(binding.token));
1056
1269
  }
1057
- if (this.#scope.hasScoped(binding.id)) {
1058
- return this.#scope.getScoped(binding.id);
1059
- }
1060
- return SCOPED_MISS;
1270
+ return this.#scope.readScoped(binding.id);
1061
1271
  }
1062
1272
 
1273
+ // The shared root context answers every top-level request; building one is the rarer half and
1274
+ // lives outside, so what a selection inlines is the test and not the literal.
1063
1275
  #makeConstraintContext(
1064
- resolutionPath: Array<string>,
1065
1276
  resolutionStack: Array<ResolutionFrame>,
1066
1277
  options: ResolveOptions | undefined,
1067
1278
  ): ConstraintContext {
1068
- if (options === undefined && resolutionPath.length === 0 && resolutionStack.length === 0) {
1279
+ if (options === undefined && resolutionStack.length === 0) {
1069
1280
  return ROOT_CONSTRAINT_CONTEXT;
1070
1281
  }
1071
- return {
1072
- resolutionPath,
1073
- resolutionStack,
1074
- parent: resolutionStack.at(-1),
1075
- ancestors: resolutionStack.length > 1 ? resolutionStack.slice(0, -1) : [],
1076
- currentResolveOptions: options,
1077
- };
1282
+ return buildConstraintContext(resolutionStack, options);
1283
+ }
1284
+
1285
+ /** Selection for a name-less multi-tag request, over the union of the two tag indexes. */
1286
+ #selectMultiTagged(
1287
+ token: Token<unknown> | Constructor,
1288
+ options: ResolveOptions,
1289
+ resolutionStack: Array<ResolutionFrame>,
1290
+ ): Binding | undefined {
1291
+ const candidates: Array<Binding> = [];
1292
+ this.#gatherTagCandidates(token, options.tag, candidates);
1293
+ const listed = options.tags;
1294
+ if (listed !== undefined) {
1295
+ for (let index = 0; index < listed.length; index += 1) {
1296
+ this.#gatherTagCandidates(token, listed[index], candidates);
1297
+ }
1298
+ }
1299
+ if (candidates.length === 0) {
1300
+ return undefined;
1301
+ }
1302
+ return selectBinding(candidates, options, this.#makeConstraintContext(resolutionStack, options), tokenName(token));
1303
+ }
1304
+
1305
+ /** One request criterion's candidates: its exact single-tag binding, plus its first-tag bucket. */
1306
+ #gatherTagCandidates(
1307
+ token: Token<unknown> | Constructor,
1308
+ criterion: BindingTag | undefined,
1309
+ out: Array<Binding>,
1310
+ ): void {
1311
+ if (criterion === undefined) {
1312
+ return;
1313
+ }
1314
+ // The includes probes only guard a request repeating a criterion across its two spellings.
1315
+ const single = this.#registry.getSimpleTagged(token, criterion);
1316
+ if (single !== undefined && !out.includes(single)) {
1317
+ out.push(single);
1318
+ }
1319
+ const bucket = this.#registry.getMultiTagged(token, criterion);
1320
+ if (bucket !== undefined) {
1321
+ for (let index = 0; index < bucket.length; index += 1) {
1322
+ const candidate = bucket[index]!;
1323
+ if (!out.includes(candidate)) {
1324
+ out.push(candidate);
1325
+ }
1326
+ }
1327
+ }
1078
1328
  }
1079
1329
 
1080
1330
  #matchesBindingFast(
1081
1331
  binding: Binding,
1082
1332
  options: ResolveOptions | undefined,
1083
- resolutionPath: Array<string>,
1084
1333
  resolutionStack: Array<ResolutionFrame>,
1085
1334
  ): boolean {
1086
1335
  if (!matchesSlot(binding.slot, options)) {
1087
1336
  return false;
1088
1337
  }
1089
- if (binding.predicate === undefined) {
1338
+ return this.#satisfiesPredicate(binding, options, resolutionStack);
1339
+ }
1340
+
1341
+ /** The predicate half of a match, for a lane whose index has already settled the slot. */
1342
+ #satisfiesPredicate(
1343
+ binding: Binding,
1344
+ options: ResolveOptions | undefined,
1345
+ resolutionStack: Array<ResolutionFrame>,
1346
+ ): boolean {
1347
+ const predicate = binding.predicate;
1348
+ if (predicate === undefined) {
1090
1349
  return true;
1091
1350
  }
1092
- return binding.predicate(this.#makeConstraintContext(resolutionPath, resolutionStack, options));
1351
+ return predicate(this.#makeConstraintContext(resolutionStack, options));
1093
1352
  }
1094
1353
 
1095
1354
  #resolveTransientDynamicSyncFromContext(
1096
1355
  binding: DynamicBinding<unknown>,
1097
- resolutionPath: Array<string>,
1098
1356
  resolutionStack: Array<ResolutionFrame>,
1099
1357
  ): unknown {
1100
1358
  // One lane at every depth: `binding.inFlight` is O(1), so there is nothing to escape.
1101
1359
  const frame = this.#getResolutionFrame(binding);
1102
1360
  const tokenDisplayName = frame.tokenName;
1103
1361
  if (binding.inFlight) {
1104
- throw new CircularDependencyError([...resolutionPath, tokenDisplayName]);
1362
+ throw new CircularDependencyError(cycleNamesOf(resolutionStack, tokenDisplayName));
1105
1363
  }
1106
1364
  binding.inFlight = true;
1107
- resolutionPath.push(tokenDisplayName);
1108
1365
  resolutionStack.push(frame);
1109
- const resolutionCtx = this.#acquireSyncResolutionContext(resolutionPath, resolutionStack, undefined);
1366
+ const resolutionCtx = this.#acquireSyncResolutionContext(resolutionStack, undefined);
1110
1367
  try {
1111
1368
  const dynamicResult = binding.factory(resolutionCtx);
1112
1369
  if (dynamicResult instanceof Promise) {
1113
- throw new AsyncResolutionError(tokenDisplayName, tokenDisplayName);
1370
+ throw new AsyncResolutionError(resolutionStack[0]?.tokenName ?? tokenDisplayName, tokenDisplayName);
1114
1371
  }
1115
1372
  return dynamicResult;
1116
1373
  } finally {
1117
1374
  resolutionStack.pop();
1118
- resolutionPath.pop();
1119
1375
  binding.inFlight = false;
1120
1376
  }
1121
1377
  }
@@ -1123,22 +1379,20 @@ export class DependencyResolver implements ResolverCallbacks {
1123
1379
  // Deliberately not `async`: that would allocate a state machine and a promise per level.
1124
1380
  #resolveTransientDynamicAsyncFromContext(
1125
1381
  binding: DynamicBinding<unknown> | DynamicAsyncBinding<unknown>,
1126
- resolutionPath: Array<string>,
1127
1382
  resolutionStack: Array<ResolutionFrame>,
1128
1383
  branchDepth: BranchDepth,
1129
1384
  ): Promise<unknown> {
1130
1385
  const frame = this.#getResolutionFrame(binding);
1131
- let levelPath: OwnedBranchPath;
1386
+ let levelStack: OwnedBranchStack;
1132
1387
  try {
1133
- levelPath = extendResolutionBranch(resolutionPath, branchDepth, frame.tokenName);
1388
+ levelStack = extendResolutionBranch(resolutionStack, branchDepth, frame);
1134
1389
  } catch (cycleError) {
1135
1390
  // This method is not `async`; keep failures as rejections rather than sync throws.
1136
1391
  return Promise.reject(cycleError);
1137
1392
  }
1138
- const levelStack = extendResolutionStackBranch(resolutionStack, branchDepth, frame);
1139
1393
 
1140
1394
  // Nothing this level appended is ever removed, so no level observes its own settlement.
1141
- const ctx = new AsyncLevelContext(this, levelPath, levelStack, undefined);
1395
+ const ctx = new AsyncLevelContext(this, levelStack, undefined);
1142
1396
  try {
1143
1397
  if (binding.kind === "dynamic-async") {
1144
1398
  return binding.factory(ctx);
@@ -1150,17 +1404,17 @@ export class DependencyResolver implements ResolverCallbacks {
1150
1404
  }
1151
1405
  }
1152
1406
 
1153
- // ── The cascade lane ───────────────────────────────────────────────────────
1407
+ // ── The cascade lane ───────────────────────────────────────────────────────────────────────────────────────────────
1154
1408
 
1155
1409
  /**
1156
1410
  * Entry for a request a factory makes from inside an open synchronous cascade.
1157
1411
  *
1158
1412
  * @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`.
1413
+ * are on no call stack — it escapes to the branch lane.
1160
1414
  */
1161
1415
  resolveAsyncFromCascade(token: Token<unknown> | Constructor): Promise<unknown> {
1162
- if (this.#cascadePath.length === 0) {
1163
- return this.resolveAsyncFromContext(token, [], [], ROOT_BRANCH);
1416
+ if (this.#cascadeStack.length === 0) {
1417
+ return this.resolveAsyncFromContext(token, [], ROOT_BRANCH);
1164
1418
  }
1165
1419
  return this.#dispatchCascade(token);
1166
1420
  }
@@ -1188,10 +1442,41 @@ export class DependencyResolver implements ResolverCallbacks {
1188
1442
  if (fastBinding.scope === "singleton" && fastBinding.instance !== NO_INSTANCE) {
1189
1443
  return Promise.resolve(fastBinding.instance);
1190
1444
  }
1445
+ const planned = this.#plannedCascadeAnswer(fastBinding);
1446
+ if (planned !== null) {
1447
+ return planned;
1448
+ }
1191
1449
  }
1192
1450
  // Anything else leaves the cascade lane for good, seeded with a snapshot of the ancestors it
1193
1451
  // accumulated — so a cycle across the boundary is still on one path.
1194
- return this.resolveAsyncFromContext(token, [...this.#cascadePath], [...this.#cascadeStack], UNOWNED_BRANCH);
1452
+ return this.resolveAsyncFromContext(token, [...this.#cascadeStack], UNOWNED_BRANCH);
1453
+ }
1454
+
1455
+ /**
1456
+ * A statically-visible transient graph at a true root answers from its compiled async plan.
1457
+ *
1458
+ * @remarks Kept out of the dispatcher so its size stays inlinable. Inside an open cascade the
1459
+ * graph must escape instead, so its escapes carry the live ancestors — hence the idle gate.
1460
+ */
1461
+ #plannedCascadeAnswer(fastBinding: Binding): Promise<unknown> | null {
1462
+ if (
1463
+ this.#cascadeStack.length !== 0 ||
1464
+ fastBinding.scope !== "transient" ||
1465
+ (fastBinding.kind !== "class" && fastBinding.kind !== "resolved" && fastBinding.kind !== "resolved-async")
1466
+ ) {
1467
+ return null;
1468
+ }
1469
+ const plan = this.#getAsyncInstantiationPlan(fastBinding);
1470
+ if (plan === null) {
1471
+ return null;
1472
+ }
1473
+ try {
1474
+ const planned = plan();
1475
+ return planned instanceof Promise ? planned : Promise.resolve(planned);
1476
+ } catch (planError) {
1477
+ // The interpreted lane is async, so a sync throw is a rejection there too.
1478
+ return Promise.reject(planError);
1479
+ }
1195
1480
  }
1196
1481
 
1197
1482
  #resolveTransientDynamicAsyncCascade(
@@ -1202,11 +1487,10 @@ export class DependencyResolver implements ResolverCallbacks {
1202
1487
  // code does not interleave — so the O(1) flag is exact path membership here, as it is for the
1203
1488
  // sync lane. It is cleared when the factory returns its promise, not when that promise settles.
1204
1489
  if (binding.inFlight) {
1205
- return Promise.reject(new CircularDependencyError([...this.#cascadePath, frame.tokenName]));
1490
+ return Promise.reject(new CircularDependencyError(cycleNamesOf(this.#cascadeStack, frame.tokenName)));
1206
1491
  }
1207
- const ctx = (this.#cascadeContext ??= new AsyncCascadeContext(this, this.#cascadePath, this.#cascadeStack));
1492
+ const ctx = (this.#cascadeContext ??= new AsyncCascadeContext(this, this.#cascadeStack));
1208
1493
  binding.inFlight = true;
1209
- this.#cascadePath.push(frame.tokenName);
1210
1494
  this.#cascadeStack.push(frame);
1211
1495
  try {
1212
1496
  if (binding.kind === "dynamic-async") {
@@ -1218,7 +1502,6 @@ export class DependencyResolver implements ResolverCallbacks {
1218
1502
  return Promise.reject(factoryError);
1219
1503
  } finally {
1220
1504
  this.#cascadeStack.pop();
1221
- this.#cascadePath.pop();
1222
1505
  binding.inFlight = false;
1223
1506
  }
1224
1507
  }
@@ -1228,38 +1511,67 @@ export class DependencyResolver implements ResolverCallbacks {
1228
1511
  #resolveCandidateSync(
1229
1512
  binding: Binding,
1230
1513
  options: ResolveOptions | undefined,
1231
- resolutionPath: Array<string>,
1232
1514
  resolutionStack: Array<ResolutionFrame>,
1233
1515
  ): unknown {
1234
- if (this.#isPlainConstant(binding)) {
1516
+ // Fan-outs are dominated by constants: with no activation hook anywhere in the chain, a
1517
+ // hook-free constant is plain no matter which container owns it — skip the owner probe.
1518
+ if (binding.kind === "constant" && binding.onActivation === undefined && this.#chainActivationVersion() === 0) {
1519
+ return binding.value;
1520
+ }
1521
+ const owner = this.#ownerOf(binding);
1522
+ if (owner.#isPlainConstant(binding)) {
1235
1523
  return binding.value;
1236
1524
  }
1237
1525
  if (binding.kind === "alias") {
1238
- return this.resolve(binding.target, options, resolutionPath, resolutionStack);
1526
+ return this.resolve(binding.target, options, resolutionStack);
1239
1527
  }
1240
- if (binding.scope === "singleton" && binding.instance !== NO_INSTANCE) {
1241
- return binding.instance;
1528
+ if (binding.scope === "singleton") {
1529
+ if (binding.instance !== NO_INSTANCE) {
1530
+ return binding.instance;
1531
+ }
1532
+ // Owner-routed like `resolve`: the owner materializes and caches its own singleton.
1533
+ return owner.#resolveBinding(binding, options, resolutionStack, owner);
1242
1534
  }
1243
- return this.#resolveBinding(binding, options, resolutionPath, resolutionStack);
1535
+ return this.#resolveBinding(binding, options, resolutionStack, owner);
1244
1536
  }
1245
1537
 
1246
1538
  #resolveCandidateAsync(
1247
1539
  binding: Binding,
1248
1540
  options: ResolveOptions | undefined,
1249
- resolutionPath: Array<string>,
1250
1541
  resolutionStack: Array<ResolutionFrame>,
1251
1542
  branchDepth: BranchDepth,
1252
1543
  ): Promise<unknown> {
1253
- if (this.#isPlainConstant(binding)) {
1544
+ if (binding.kind === "constant" && binding.onActivation === undefined && this.#chainActivationVersion() === 0) {
1545
+ return Promise.resolve(binding.value);
1546
+ }
1547
+ const owner = this.#ownerOf(binding);
1548
+ if (owner.#isPlainConstant(binding)) {
1254
1549
  return Promise.resolve(binding.value);
1255
1550
  }
1256
1551
  if (binding.kind === "alias") {
1257
- return this.resolveAsync(binding.target, options, resolutionPath, resolutionStack, branchDepth);
1552
+ return this.resolveAsync(binding.target, options, resolutionStack, branchDepth);
1258
1553
  }
1259
- if (binding.scope === "singleton" && binding.instance !== NO_INSTANCE) {
1260
- return Promise.resolve(binding.instance);
1554
+ if (binding.scope === "singleton") {
1555
+ if (binding.instance !== NO_INSTANCE) {
1556
+ return Promise.resolve(binding.instance);
1557
+ }
1558
+ return owner.#resolveBindingAsync(binding, options, resolutionStack, branchDepth, owner);
1559
+ }
1560
+ return this.#resolveBindingAsync(binding, options, resolutionStack, branchDepth, owner);
1561
+ }
1562
+
1563
+ /** The resolver whose registry holds `binding` — `this` (the common case) when it is own. */
1564
+ #ownerOf(binding: Binding): DependencyResolver {
1565
+ // A root resolver can only hold its own bindings, so the per-candidate id probe is chain-only.
1566
+ if (this.#parent === undefined || this.#registry.getById(binding.id) !== undefined) {
1567
+ return this;
1568
+ }
1569
+ for (let current: DependencyResolver | undefined = this.#parent; current !== undefined; current = current.#parent) {
1570
+ if (current.#registry.getById(binding.id) !== undefined) {
1571
+ return current;
1572
+ }
1261
1573
  }
1262
- return this.#resolveBindingAsync(binding, options, resolutionPath, resolutionStack, branchDepth);
1574
+ return this;
1263
1575
  }
1264
1576
 
1265
1577
  #getResolutionFrame(binding: Binding): ResolutionFrame {
@@ -1275,26 +1587,48 @@ export class DependencyResolver implements ResolverCallbacks {
1275
1587
  return frame;
1276
1588
  }
1277
1589
 
1590
+ // A pool is keyed by the one array pair its contexts hold, so reuse can never re-point a context
1591
+ // a live frame still reads — a nested top-level resolve reaches the same depth while the outer
1592
+ // factory runs, and it must get its own context, not the outer frame's re-bound.
1278
1593
  #acquireSyncResolutionContext(
1279
- resolutionPath: Array<string>,
1280
1594
  resolutionStack: Array<ResolutionFrame>,
1281
1595
  options: ResolveOptions | undefined,
1282
1596
  ): DefaultResolutionContext {
1597
+ if (resolutionStack === this.rootStack) {
1598
+ const depth = resolutionStack.length;
1599
+ const existing = this.#syncResolutionContextPool[depth];
1600
+ if (existing !== undefined) {
1601
+ existing.reset(this, resolutionStack, options);
1602
+ return existing;
1603
+ }
1604
+ const created = new DefaultResolutionContext(this, resolutionStack, options);
1605
+ this.#syncResolutionContextPool[depth] = created;
1606
+ return created;
1607
+ }
1608
+ return this.#acquireOffRootSyncContext(resolutionStack, options);
1609
+ }
1610
+
1611
+ /** The cascade pair pools separately; a throwaway pair (nested resolve, async snapshot) mints per call. */
1612
+ #acquireOffRootSyncContext(
1613
+ resolutionStack: Array<ResolutionFrame>,
1614
+ options: ResolveOptions | undefined,
1615
+ ): DefaultResolutionContext {
1616
+ if (resolutionStack !== this.#cascadeStack) {
1617
+ return new DefaultResolutionContext(this, resolutionStack, options);
1618
+ }
1283
1619
  const depth = resolutionStack.length;
1284
- const existing = this.#syncResolutionContextPool[depth];
1620
+ const pool = (this.#cascadeContextPool ??= []);
1621
+ const existing = pool[depth];
1285
1622
  if (existing !== undefined) {
1286
- existing.reset(this, resolutionPath, resolutionStack, options);
1623
+ existing.reset(this, resolutionStack, options);
1287
1624
  return existing;
1288
1625
  }
1289
- const created = new DefaultResolutionContext(this, resolutionPath, resolutionStack, options);
1290
- this.#syncResolutionContextPool[depth] = created;
1626
+ const created = new DefaultResolutionContext(this, resolutionStack, options);
1627
+ pool[depth] = created;
1291
1628
  return created;
1292
1629
  }
1293
1630
  }
1294
1631
 
1295
- /** Absent scoped entry — distinguishes it from a cached `undefined`. */
1296
- const SCOPED_MISS: unique symbol = Symbol("di:scoped-miss");
1297
-
1298
1632
  function anyPredicate(bindings: ReadonlyArray<Binding>): boolean {
1299
1633
  for (let index = 0; index < bindings.length; index += 1) {
1300
1634
  if (bindings[index]!.predicate !== undefined) {
@@ -1304,20 +1638,36 @@ function anyPredicate(bindings: ReadonlyArray<Binding>): boolean {
1304
1638
  return false;
1305
1639
  }
1306
1640
 
1641
+ function buildConstraintContext(
1642
+ resolutionStack: Array<ResolutionFrame>,
1643
+ options: ResolveOptions | undefined,
1644
+ ): ConstraintContext {
1645
+ return {
1646
+ // Derived per read, never cached: the stack may be live, and the names must report it as it stands.
1647
+ get resolutionPath(): ReadonlyArray<string> {
1648
+ const names = new Array<string>(resolutionStack.length);
1649
+ for (let index = 0; index < resolutionStack.length; index += 1) {
1650
+ names[index] = resolutionStack[index]!.tokenName;
1651
+ }
1652
+ return names;
1653
+ },
1654
+ resolutionStack,
1655
+ parent: resolutionStack.at(-1),
1656
+ ancestors: resolutionStack.length > 1 ? resolutionStack.slice(0, -1) : [],
1657
+ currentResolveOptions: options,
1658
+ };
1659
+ }
1660
+
1661
+ /** The async-resolution failure for a binding reached on a sync path, naming what to await instead. */
1662
+ function asyncResolutionErrorFor(
1663
+ binding: Binding,
1664
+ resolutionStack: ReadonlyArray<ResolutionFrame>,
1665
+ ): AsyncResolutionError {
1666
+ const sourceName = tokenName(binding.token);
1667
+ return new AsyncResolutionError(resolutionStack[0]?.tokenName ?? sourceName, sourceName);
1668
+ }
1669
+
1307
1670
  /** Only a factory is handed the resolution context; everything else gets its deps directly. */
1308
1671
  function requiresResolutionContext(binding: Binding): boolean {
1309
1672
  return binding.kind === "dynamic" || binding.kind === "dynamic-async";
1310
1673
  }
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
- }