@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,14 +1,18 @@
1
- import { NO_INSTANCE } from "#/binding";
2
- import { AsyncActivationError, AsyncResolutionError, CircularDependencyError, InternalError, MissingMetadataError, MissingScopeContextError, NoMatchingBindingError, TokenNotBoundError, } from "#/errors";
3
- import { ActivationNeedCache } from "#/resolution/activation-need";
4
- import { BindingLookupCache } from "#/resolution/binding-lookup-cache";
5
- import { matchesSlot, selectAllBindings, selectBinding } from "#/resolution/binding-select";
6
- import { ClassIntrospector } from "#/resolution/class-introspector";
7
- import { AsyncCascadeContext, AsyncLevelContext, buildResolutionFrame, DefaultResolutionContext, } from "#/resolution/environment";
8
- import { InstantiationPlanCompiler, PLAN_RETRY } from "#/resolution/instantiation-plan";
9
- import { branchDepthOf, enterResolutionPath, extendResolutionBranch, extendResolutionStackBranch, ROOT_BRANCH, UNOWNED_BRANCH, } from "#/resolution/resolution-path";
10
- import { injectionSlotToResolveOptions, isNameOnlyOptions, singleTagOnlyOf } from "#/resolution/resolve-options";
11
- import { tokenName } from "#/token";
1
+ import { NO_INSTANCE } from "#/core/binding";
2
+ import { NO_TAG_KEYS } from "#/core/tag";
3
+ import { tokenName } from "#/core/token";
4
+ import { AsyncActivationError, AsyncResolutionError, CircularDependencyError, DisposedContainerError, InternalError, MissingMetadataError, MissingScopeContextError, NoMatchingBindingError, TokenNotBoundError, } from "#/errors/errors";
5
+ import { isNameOnlyOptions, resolveOptionsForSlot, singleTagOnlyOf } from "#/injection/resolve-options";
6
+ import { SCOPED_MISS } from "#/lifecycle/scope-manager";
7
+ import { ActivationNeedCache } from "#/resolution/cache/activation-need";
8
+ import { BindingLookupCache } from "#/resolution/cache/binding-lookup-cache";
9
+ import { ClassIntrospector } from "#/resolution/cache/class-introspector";
10
+ import { AsyncCascadeContext, AsyncLevelContext, buildResolutionFrame, DefaultResolutionContext, } from "#/resolution/context";
11
+ import { branchDepthOf, cycleNamesOf, enterResolutionPath, extendResolutionBranch, ROOT_BRANCH, UNOWNED_BRANCH, } from "#/resolution/path/resolution-path";
12
+ import { InstantiationPlanCompiler, PLAN_RETRY } from "#/resolution/plan/instantiation-plan";
13
+ import { matchesSlot, requestedTagKeyMask, selectAllBindings, selectBinding } from "#/resolution/select/binding-select";
14
+ // Where a multi-tag resolve switches from scanning the token's list to walking the tag indexes.
15
+ const MULTI_TAG_INDEX_THRESHOLD = 8;
12
16
  const EMPTY_STRING_LIST = [];
13
17
  const EMPTY_FRAME_LIST = [];
14
18
  const EMPTY_PARAM_LIST = [];
@@ -24,26 +28,29 @@ const ROOT_CONSTRAINT_CONTEXT = {
24
28
  */
25
29
  export class DependencyResolver {
26
30
  #syncResolutionContextPool = [];
31
+ // Contexts bound to the cascade pair — deferred: only an async cascade's sync resolves need it.
32
+ #cascadeContextPool;
27
33
  /**
28
- * The pair a top-level **sync** resolve reuses instead of minting two arrays per call.
34
+ * The stack a top-level **sync** resolve reuses instead of minting an array per call.
29
35
  *
30
- * @remarks Read directly rather than through an accessor returning both: a shallow resolve is one
31
- * top-level call, so a call and an object literal there are not amortised over anything. Every sync
32
- * lane pops what it pushes, so `rootStack.length === 0` means no resolve holds the pair; async
33
- * appends without popping and mints its own. Keeping the pair stable is also what lets a pooled
34
- * context skip storing pointers it already holds.
36
+ * @remarks Read directly rather than through an accessor: a shallow resolve is one top-level
37
+ * call, so a call there is not amortised over anything. Every sync lane pops what it pushes, so
38
+ * `rootStack.length === 0` means no resolve holds the stack; async appends without popping and
39
+ * mints its own. Keeping it stable is also what lets a pooled context skip re-storing it.
35
40
  */
36
- rootPath = [];
37
41
  rootStack = [];
38
- // The open synchronous factory cascade: its arrays are the ancestor chain, and they are balanced
42
+ // The open synchronous factory cascade: its stack is the ancestor chain, and it is balanced
39
43
  // because synchronous code does not interleave.
40
- #cascadePath = [];
41
44
  #cascadeStack = [];
42
45
  #cascadeContext;
43
46
  // Compiled plans; `null` marks a binding as unplannable under the current cache versions.
44
47
  #classPlanByBindingId = new Map();
45
48
  #classPlanRegistryVersion = -1;
46
49
  #classPlanActivationVersion = -1;
50
+ // The async lane's plans, stamped and invalidated apart so neither lane pays the other's misses.
51
+ #asyncPlanByBindingId = new Map();
52
+ #asyncPlanRegistryVersion = -1;
53
+ #asyncPlanActivationVersion = -1;
47
54
  #registry;
48
55
  #scope;
49
56
  #lifecycle;
@@ -62,7 +69,11 @@ export class DependencyResolver {
62
69
  this.#classes = new ClassIntrospector(metadataReader, container);
63
70
  this.#activation = new ActivationNeedCache(lifecycle, this.#classes, registry);
64
71
  }
65
- /** Structural counts for {@link RESOLUTION_DIAGNOSTICS}; see `resolution/diagnostics.ts`. */
72
+ /** The reader this resolver was built with, which is the one its container answers with. */
73
+ get metadataReader() {
74
+ return this.#metadataReader;
75
+ }
76
+ /** Structural counts for the {@link ResolutionDiagnostics} a container reports. */
66
77
  describeCaches() {
67
78
  let compiledPlanCount = 0;
68
79
  for (const plan of this.#classPlanByBindingId.values()) {
@@ -70,13 +81,20 @@ export class DependencyResolver {
70
81
  compiledPlanCount += 1;
71
82
  }
72
83
  }
84
+ let compiledAsyncPlanCount = 0;
85
+ for (const plan of this.#asyncPlanByBindingId.values()) {
86
+ if (plan !== null) {
87
+ compiledAsyncPlanCount += 1;
88
+ }
89
+ }
73
90
  return {
74
91
  compiledPlanCount,
92
+ compiledAsyncPlanCount,
75
93
  syncContextPoolSize: this.#syncResolutionContextPool.length,
76
94
  };
77
95
  }
78
- // ── Binding lookup ─────────────────────────────────────────────────────────
79
- #findBinding(token, options, resolutionPath, resolutionStack) {
96
+ // ── Binding lookup ─────────────────────────────────────────────────────────────────────────────────────────────────
97
+ #findBinding(token, options, resolutionStack) {
80
98
  if (options === undefined) {
81
99
  const fastDefaultBinding = this.#registry.getFastDefault(token);
82
100
  if (fastDefaultBinding !== undefined) {
@@ -85,35 +103,49 @@ export class DependencyResolver {
85
103
  }
86
104
  else if (isNameOnlyOptions(options)) {
87
105
  const namedBinding = this.#registry.getSimpleNamed(token, options.name);
88
- if (namedBinding !== undefined &&
89
- this.#matchesBindingFast(namedBinding, options, resolutionPath, resolutionStack)) {
106
+ if (namedBinding !== undefined && this.#matchesBindingFast(namedBinding, options, resolutionStack)) {
90
107
  return { binding: namedBinding, owner: this };
91
108
  }
92
109
  }
93
110
  else {
94
111
  const singleTag = singleTagOnlyOf(options);
95
112
  if (singleTag !== undefined) {
96
- const tagged = this.#registry.getSimpleTagged(token, singleTag[0], singleTag[1]);
97
- if (tagged !== undefined && matchesIndexedTagValue(tagged, singleTag[1])) {
113
+ const tagged = this.#registry.getSimpleTagged(token, singleTag);
114
+ if (tagged !== undefined && this.#satisfiesPredicate(tagged, options, resolutionStack)) {
98
115
  return { binding: tagged, owner: this };
99
116
  }
100
117
  }
118
+ else if (options.name === undefined &&
119
+ // A threshold switches the data structure, never the semantics: under it the generic scan
120
+ // below beats walking the indexes, and both paths answer identically. Sized first, so a
121
+ // small list pays one length read and nothing else.
122
+ this.#registry.getAll(token).length > MULTI_TAG_INDEX_THRESHOLD &&
123
+ requestedTagKeyMask(options) !== NO_TAG_KEYS) {
124
+ // A name-less multi-tag request matches only name-less tagged slots, and every such slot
125
+ // lives in one of the two tag indexes — so their union is the whole candidate set and the
126
+ // token's full list never needs scanning. Selection still owns predicates and specificity.
127
+ const selected = this.#selectMultiTagged(token, options, resolutionStack);
128
+ if (selected !== undefined) {
129
+ return { binding: selected, owner: this };
130
+ }
131
+ return this.#parent === undefined ? undefined : this.#parent.#findBinding(token, options, resolutionStack);
132
+ }
101
133
  }
102
134
  const bindings = this.#registry.getAll(token);
103
135
  if (bindings.length > 0) {
104
136
  // A lone candidate is its own selection: matching it is the whole decision, with no
105
137
  // specificity to weigh and no ambiguity to report.
106
138
  const selected = bindings.length === 1
107
- ? this.#matchesBindingFast(bindings[0], options, resolutionPath, resolutionStack)
139
+ ? this.#matchesBindingFast(bindings[0], options, resolutionStack)
108
140
  ? bindings[0]
109
141
  : undefined
110
- : selectBinding(bindings, options, this.#makeConstraintContext(resolutionPath, resolutionStack, options), tokenName(token));
142
+ : selectBinding(bindings, options, this.#makeConstraintContext(resolutionStack, options), tokenName(token));
111
143
  if (selected !== undefined) {
112
144
  return { binding: selected, owner: this };
113
145
  }
114
146
  }
115
147
  if (this.#parent !== undefined) {
116
- return this.#parent.#findBinding(token, options, resolutionPath, resolutionStack);
148
+ return this.#parent.#findBinding(token, options, resolutionStack);
117
149
  }
118
150
  return undefined;
119
151
  }
@@ -123,10 +155,10 @@ export class DependencyResolver {
123
155
  * @remarks Alias hops are followed iteratively with exact cycle detection — a revisited alias
124
156
  * token raises {@link CircularDependencyError} instead of overflowing the call stack.
125
157
  */
126
- #requireBinding(token, options, resolutionPath, resolutionStack) {
158
+ #requireBinding(token, options, resolutionStack) {
127
159
  let currentToken = token;
128
160
  let visitedAliasTokens;
129
- let found = this.#findBinding(currentToken, options, resolutionPath, resolutionStack);
161
+ let found = this.#findBinding(currentToken, options, resolutionStack);
130
162
  while (found !== undefined && found.binding.kind === "alias") {
131
163
  const target = found.binding.target;
132
164
  visitedAliasTokens ??= new Set([currentToken]);
@@ -135,7 +167,7 @@ export class DependencyResolver {
135
167
  }
136
168
  visitedAliasTokens.add(target);
137
169
  currentToken = target;
138
- found = this.#findBinding(currentToken, options, resolutionPath, resolutionStack);
170
+ found = this.#findBinding(currentToken, options, resolutionStack);
139
171
  }
140
172
  if (found === undefined) {
141
173
  // Thrown here rather than from a helper: the error captures this stack, and an error path is
@@ -151,40 +183,42 @@ export class DependencyResolver {
151
183
  * Binding lookup aligned with `resolve` — used by `Container.validate` without instantiating.
152
184
  */
153
185
  peekBindingForValidate(token, options) {
154
- return this.#findBinding(token, options, [], []);
186
+ return this.#findBinding(token, options, []);
155
187
  }
156
188
  /**
157
189
  * Mirrors {@link DependencyResolver.resolveAll} candidate selection only (no instantiation).
158
190
  */
159
191
  peekCandidateBindingsForValidate(token, options) {
160
- return this.#candidateBindings(token, options, [], []);
192
+ return this.#candidateBindings(token, options, []);
161
193
  }
162
- // ── Sync resolve ───────────────────────────────────────────────────────────
163
- resolveFromContext(token, resolutionPath, resolutionStack) {
194
+ // ── Sync resolve ───────────────────────────────────────────────────────────────────────────────────────────────────
195
+ resolveFromContext(token, resolutionStack) {
164
196
  // Hot lane: own-registry fast default. Fall back to the chain-versioned memo
165
197
  // (parent-chain walk + alias folding) only on miss or alias.
166
198
  const fastBinding = this.#registry.getFastDefault(token);
167
199
  if (fastBinding !== undefined && fastBinding.kind !== "alias") {
168
- return this.#resolveDefaultEntry(fastBinding, this, resolutionPath, resolutionStack);
200
+ return this.#resolveDefaultEntry(fastBinding, this, resolutionStack);
169
201
  }
170
202
  const entry = this.#lookup.defaultEntry(token);
171
203
  if (entry === null) {
172
- return this.resolve(token, undefined, resolutionPath, resolutionStack);
204
+ return this.resolve(token, undefined, resolutionStack);
173
205
  }
174
- return this.#resolveDefaultEntry(entry.binding, entry.owner, resolutionPath, resolutionStack);
206
+ return this.#resolveDefaultEntry(entry.binding, entry.owner, resolutionStack);
175
207
  }
176
- #resolveDefaultEntry(binding, owner, resolutionPath, resolutionStack) {
208
+ #resolveDefaultEntry(binding, owner, resolutionStack) {
177
209
  const scope = binding.scope;
178
210
  if (scope === "transient") {
179
211
  if (binding.kind === "dynamic") {
180
- const containerHooks = this.#lifecycle.activationVersion === 0 ? undefined : this.#lifecycle.activationHandlersFor(binding.token);
212
+ // Container-level hooks belong to the binding's owner — a child-registered hook must not
213
+ // fire for a parent-owned binding, and the owner's must.
214
+ const containerHooks = owner.#lifecycle.activationVersion === 0 ? undefined : owner.#lifecycle.activationHandlersFor(binding.token);
181
215
  if (binding.onActivation === undefined && (containerHooks === undefined || containerHooks.length === 0)) {
182
- return this.#resolveTransientDynamicSyncFromContext(binding, resolutionPath, resolutionStack);
216
+ return this.#resolveTransientDynamicSyncFromContext(binding, resolutionStack);
183
217
  }
184
- return this.#resolveTransientDynamicActivatedSync(binding, containerHooks, resolutionPath, resolutionStack);
218
+ return this.#resolveTransientDynamicActivatedSync(binding, containerHooks, resolutionStack);
185
219
  }
186
220
  // Compiled plans only run at the top level — inner levels keep the runtime cycle guard.
187
- if ((binding.kind === "class" || binding.kind === "resolved") && resolutionPath.length === 0) {
221
+ if ((binding.kind === "class" || binding.kind === "resolved") && resolutionStack.length === 0) {
188
222
  const plan = this.#getInstantiationPlan(binding);
189
223
  if (plan !== null) {
190
224
  return plan();
@@ -193,7 +227,7 @@ export class DependencyResolver {
193
227
  }
194
228
  else if (scope === "singleton") {
195
229
  // A constant is a singleton that is already its own instance.
196
- if (this.#isPlainConstant(binding)) {
230
+ if (owner.#isPlainConstant(binding)) {
197
231
  return binding.value;
198
232
  }
199
233
  const cachedSingleton = binding.instance;
@@ -201,7 +235,7 @@ export class DependencyResolver {
201
235
  return cachedSingleton;
202
236
  }
203
237
  if (owner !== this) {
204
- return owner.#resolveBinding(binding, undefined, resolutionPath, resolutionStack);
238
+ return owner.#resolveBinding(binding, undefined, resolutionStack, owner);
205
239
  }
206
240
  }
207
241
  else {
@@ -210,27 +244,26 @@ export class DependencyResolver {
210
244
  return cachedScoped;
211
245
  }
212
246
  }
213
- return this.#resolveBinding(binding, undefined, resolutionPath, resolutionStack);
247
+ return this.#resolveBinding(binding, undefined, resolutionStack, owner);
214
248
  }
215
249
  // Lean lane for an activated transient dynamic binding: same observable behavior as the
216
250
  // generic #resolveBinding path (guard, frame, ctx, per-binding then container hooks) with
217
251
  // the kind/activation dispatch resolved statically.
218
- #resolveTransientDynamicActivatedSync(binding, containerHooks, resolutionPath, resolutionStack) {
252
+ #resolveTransientDynamicActivatedSync(binding, containerHooks, resolutionStack) {
219
253
  // Same O(1) cycle guard as the unhooked lane: this is still one sync call stack, so the flag
220
- // *is* exact path membership — see ARCHITECTURE.md.
254
+ // *is* exact path membership.
221
255
  const frame = this.#getResolutionFrame(binding);
222
256
  const tokenDisplayName = frame.tokenName;
223
257
  if (binding.inFlight) {
224
- throw new CircularDependencyError([...resolutionPath, tokenDisplayName]);
258
+ throw new CircularDependencyError(cycleNamesOf(resolutionStack, tokenDisplayName));
225
259
  }
226
260
  binding.inFlight = true;
227
- resolutionPath.push(tokenDisplayName);
228
261
  resolutionStack.push(frame);
229
262
  try {
230
- const resolutionCtx = this.#acquireSyncResolutionContext(resolutionPath, resolutionStack, undefined);
263
+ const resolutionCtx = this.#acquireSyncResolutionContext(resolutionStack, undefined);
231
264
  const factoryResult = binding.factory(resolutionCtx);
232
265
  if (factoryResult instanceof Promise) {
233
- throw new AsyncResolutionError(tokenDisplayName, tokenDisplayName);
266
+ throw new AsyncResolutionError(resolutionStack[0]?.tokenName ?? tokenDisplayName, tokenDisplayName);
234
267
  }
235
268
  let activated = factoryResult;
236
269
  if (binding.onActivation !== undefined) {
@@ -253,13 +286,20 @@ export class DependencyResolver {
253
286
  }
254
287
  finally {
255
288
  resolutionStack.pop();
256
- resolutionPath.pop();
257
289
  binding.inFlight = false;
258
290
  }
259
291
  }
292
+ /** Chain-summed activation version: a plan can inline a parent-owned binding, so a parent's hook registration must invalidate it. */
293
+ #chainActivationVersion() {
294
+ let version = this.#lifecycle.activationVersion;
295
+ for (let current = this.#parent; current !== undefined; current = current.#parent) {
296
+ version += current.#lifecycle.activationVersion;
297
+ }
298
+ return version;
299
+ }
260
300
  #getInstantiationPlan(binding) {
261
301
  const registryVersion = this.#lookup.chainVersion();
262
- const activationVersion = this.#lifecycle.activationVersion;
302
+ const activationVersion = this.#chainActivationVersion();
263
303
  if (registryVersion !== this.#classPlanRegistryVersion || activationVersion !== this.#classPlanActivationVersion) {
264
304
  this.#classPlanByBindingId.clear();
265
305
  this.#classPlanRegistryVersion = registryVersion;
@@ -279,7 +319,7 @@ export class DependencyResolver {
279
319
  }
280
320
  // Compiler behind #getInstantiationPlan — cold path, so the host indirection costs nothing hot.
281
321
  #planCompiler = new InstantiationPlanCompiler({
282
- hasActivationHandlers: (token) => this.#lifecycle.hasActivationHandlers(token),
322
+ hasActivationHandlers: (binding) => this.#ownerOf(binding).#lifecycle.hasActivationHandlers(binding.token),
283
323
  knownPostConstruct: (target) => this.#classes.knownPostConstruct(target),
284
324
  needsActiveContainer: (target) => this.#classes.needsActiveContainer(target),
285
325
  getConstructorMetadata: (target) => this.#classes.constructorMetadata(target),
@@ -287,30 +327,85 @@ export class DependencyResolver {
287
327
  const entry = this.#lookup.defaultEntry(token);
288
328
  return entry === null ? null : { binding: entry.binding };
289
329
  },
330
+ // Exactly what #findBinding's named lane accepts, minus the half that reads a path: a predicate
331
+ // is the compiler's cue to leave the selection to the runtime.
332
+ lookupPathIndependentNamedEntry: (token, options) => {
333
+ const entry = this.#lookup.namedEntry(token, options.name);
334
+ if (entry === null || entry.binding.predicate !== undefined || !matchesSlot(entry.binding.slot, options)) {
335
+ return null;
336
+ }
337
+ return { binding: entry.binding };
338
+ },
339
+ // The named rule verbatim, on the single-tag lane's memo.
340
+ lookupPathIndependentTaggedEntry: (token, options) => {
341
+ const singleTag = singleTagOnlyOf(options);
342
+ if (singleTag === undefined) {
343
+ return null;
344
+ }
345
+ const entry = this.#lookup.taggedEntry(token, singleTag);
346
+ if (entry === null || entry.binding.predicate !== undefined || !matchesSlot(entry.binding.slot, options)) {
347
+ return null;
348
+ }
349
+ return { binding: entry.binding };
350
+ },
290
351
  getResolutionFrame: (binding) => this.#getResolutionFrame(binding),
291
352
  // Dispatches exactly as #resolveDep does, so an escaped dep is indistinguishable
292
353
  // from the same dep on a fully interpreted resolve.
293
- resolveEscaped: (token, options, arity, resolutionPath, resolutionStack) => {
354
+ resolveEscaped: (token, options, arity, resolutionStack) => {
355
+ if (arity === "all") {
356
+ return this.resolveAll(token, options, resolutionStack);
357
+ }
358
+ if (arity === "optional") {
359
+ return this.resolveOptional(token, options, resolutionStack);
360
+ }
361
+ if (options === undefined) {
362
+ return this.resolveFromContext(token, resolutionStack);
363
+ }
364
+ return this.resolve(token, options, resolutionStack);
365
+ },
366
+ // Dispatches exactly as #resolveDepAsync does, for the async lane's escapes.
367
+ resolveEscapedAsync: (token, options, arity, resolutionStack) => {
294
368
  if (arity === "all") {
295
- return this.resolveAll(token, options, resolutionPath, resolutionStack);
369
+ return this.resolveAllAsync(token, options, resolutionStack, UNOWNED_BRANCH);
296
370
  }
297
371
  if (arity === "optional") {
298
- return this.resolveOptional(token, options, resolutionPath, resolutionStack);
372
+ return this.resolveOptionalAsync(token, options, resolutionStack, UNOWNED_BRANCH);
299
373
  }
300
374
  if (options === undefined) {
301
- return this.resolveFromContext(token, resolutionPath, resolutionStack);
375
+ return this.resolveAsyncFromContext(token, resolutionStack, UNOWNED_BRANCH);
302
376
  }
303
- return this.resolve(token, options, resolutionPath, resolutionStack);
377
+ return this.resolveAsync(token, options, resolutionStack, UNOWNED_BRANCH);
304
378
  },
305
379
  });
306
- resolve(token, options, resolutionPath, resolutionStack) {
380
+ /** The async lane's plan for a statically-visible transient binding, mirroring the sync getter. */
381
+ #getAsyncInstantiationPlan(binding) {
382
+ const registryVersion = this.#lookup.chainVersion();
383
+ const activationVersion = this.#chainActivationVersion();
384
+ if (registryVersion !== this.#asyncPlanRegistryVersion || activationVersion !== this.#asyncPlanActivationVersion) {
385
+ this.#asyncPlanByBindingId.clear();
386
+ this.#asyncPlanRegistryVersion = registryVersion;
387
+ this.#asyncPlanActivationVersion = activationVersion;
388
+ }
389
+ const cached = this.#asyncPlanByBindingId.get(binding.id);
390
+ if (cached !== undefined) {
391
+ return cached;
392
+ }
393
+ const compiled = this.#planCompiler.compileAsync(binding);
394
+ if (compiled === PLAN_RETRY) {
395
+ // Lifecycle metadata not discovered yet — the fallback resolve discovers it; retry then.
396
+ return null;
397
+ }
398
+ this.#asyncPlanByBindingId.set(binding.id, compiled);
399
+ return compiled;
400
+ }
401
+ resolve(token, options, resolutionStack) {
307
402
  // Name-only fast lane: memoized lookup, dispatching just the shapes whose
308
403
  // semantics involve no resolution context (constants, cached singletons).
309
404
  if (options !== undefined && isNameOnlyOptions(options)) {
310
405
  const namedEntry = this.#lookup.namedEntry(token, options.name);
311
406
  if (namedEntry !== null) {
312
407
  const namedBinding = namedEntry.binding;
313
- if (this.#isPlainConstant(namedBinding)) {
408
+ if (namedEntry.owner.#isPlainConstant(namedBinding)) {
314
409
  return namedBinding.value;
315
410
  }
316
411
  if (namedBinding.scope === "singleton" && namedBinding.instance !== NO_INSTANCE) {
@@ -319,15 +414,32 @@ export class DependencyResolver {
319
414
  // Everything else keeps the full path (context, activation, guards).
320
415
  }
321
416
  }
322
- const { binding, owner } = this.#requireBinding(token, options, resolutionPath, resolutionStack);
417
+ else if (options !== undefined) {
418
+ // Single-tag fast lane: the named lane's tagged twin, memoizing the chain walk.
419
+ const singleTag = singleTagOnlyOf(options);
420
+ if (singleTag !== undefined) {
421
+ const taggedEntry = this.#lookup.taggedEntry(token, singleTag);
422
+ if (taggedEntry !== null) {
423
+ const taggedBinding = taggedEntry.binding;
424
+ if (taggedEntry.owner.#isPlainConstant(taggedBinding)) {
425
+ return taggedBinding.value;
426
+ }
427
+ if (taggedBinding.scope === "singleton" && taggedBinding.instance !== NO_INSTANCE) {
428
+ return taggedBinding.instance;
429
+ }
430
+ // Everything else keeps the full path (context, activation, guards).
431
+ }
432
+ }
433
+ }
434
+ const { binding, owner } = this.#requireBinding(token, options, resolutionStack);
323
435
  // A singleton owned by a parent resolver is resolved there, so the parent caches it.
324
436
  if (binding.scope === "singleton" && owner !== this) {
325
- return owner.#resolveBinding(binding, options, resolutionPath, resolutionStack);
437
+ return owner.#resolveBinding(binding, options, resolutionStack, owner);
326
438
  }
327
- return this.#resolveBinding(binding, options, resolutionPath, resolutionStack);
439
+ return this.#resolveBinding(binding, options, resolutionStack, owner);
328
440
  }
329
- #resolveBinding(binding, options, resolutionPath, resolutionStack) {
330
- if (this.#isPlainConstant(binding)) {
441
+ #resolveBinding(binding, options, resolutionStack, owner) {
442
+ if (owner.#isPlainConstant(binding)) {
331
443
  return binding.value;
332
444
  }
333
445
  const scope = binding.scope;
@@ -335,49 +447,70 @@ export class DependencyResolver {
335
447
  if (binding.instance !== NO_INSTANCE) {
336
448
  return binding.instance;
337
449
  }
450
+ // An async materialization already in flight must not be raced by a second, sync one.
451
+ if (this.#scope.getInflight(binding.id) !== undefined) {
452
+ throw new AsyncResolutionError(resolutionStack[0]?.tokenName ?? tokenName(binding.token), tokenName(binding.token));
453
+ }
454
+ if (this.#scope.isClosed) {
455
+ throw new DisposedContainerError();
456
+ }
338
457
  }
339
458
  else if (scope === "scoped") {
340
459
  const cachedScoped = this.#readScoped(binding);
341
460
  if (cachedScoped !== SCOPED_MISS) {
342
461
  return cachedScoped;
343
462
  }
463
+ if (this.#scope.getInflight(binding.id) !== undefined) {
464
+ throw new AsyncResolutionError(resolutionStack[0]?.tokenName ?? tokenName(binding.token), tokenName(binding.token));
465
+ }
466
+ if (this.#scope.isClosed) {
467
+ throw new DisposedContainerError();
468
+ }
344
469
  }
345
470
  const frame = this.#getResolutionFrame(binding);
346
471
  const tokenDisplayName = frame.tokenName;
347
- const resolutionSet = enterResolutionPath(resolutionPath, tokenDisplayName);
348
- resolutionStack.push(frame);
472
+ const resolutionSet = enterResolutionPath(resolutionStack, frame);
349
473
  try {
350
- const needsActivation = this.#activation.needsActivation(binding);
474
+ const needsActivation = owner.#activation.needsActivation(binding);
351
475
  if (!needsActivation && scope === "transient" && binding.kind === "dynamic") {
352
- const resolutionCtx = this.#acquireSyncResolutionContext(resolutionPath, resolutionStack, options);
476
+ const resolutionCtx = this.#acquireSyncResolutionContext(resolutionStack, options);
353
477
  const dynamicResult = binding.factory(resolutionCtx);
354
478
  if (dynamicResult instanceof Promise) {
355
- throw new AsyncResolutionError(tokenDisplayName, tokenDisplayName);
479
+ throw new AsyncResolutionError(resolutionStack[0]?.tokenName ?? tokenDisplayName, tokenDisplayName);
356
480
  }
357
481
  return dynamicResult;
358
482
  }
359
483
  const resolutionCtx = needsActivation || requiresResolutionContext(binding)
360
- ? this.#acquireSyncResolutionContext(resolutionPath, resolutionStack, options)
484
+ ? this.#acquireSyncResolutionContext(resolutionStack, options)
361
485
  : undefined;
362
- const instance = this.#instantiateSync(binding, resolutionCtx, resolutionPath, resolutionStack);
363
- const activated = this.#activation.refreshAfterFirstInstantiation(binding, needsActivation)
364
- ? this.#lifecycle.runActivationSync(resolutionCtx, binding, instance, this.#metadataReader)
486
+ const instance = this.#instantiateSync(binding, resolutionCtx, resolutionStack);
487
+ this.#mirrorPostConstructFromOwner(binding, owner);
488
+ const activated = owner.#activation.refreshAfterFirstInstantiation(binding, needsActivation)
489
+ ? owner.#lifecycle.runActivationSync(resolutionCtx, binding, instance, owner.#metadataReader)
365
490
  : instance;
366
491
  if (scope === "singleton") {
367
492
  this.#scope.setSingleton(binding, activated);
368
493
  }
369
494
  else if (scope === "scoped") {
370
- this.#scope.setScoped(binding.id, activated);
495
+ this.#scope.setScoped(binding, activated);
371
496
  }
372
497
  return activated;
373
498
  }
374
499
  finally {
375
500
  resolutionStack.pop();
376
- resolutionPath.pop();
377
- resolutionSet?.delete(tokenDisplayName);
501
+ resolutionSet?.delete(frame.bindingId);
378
502
  }
379
503
  }
380
- #instantiateSync(binding, ctx, resolutionPath, resolutionStack) {
504
+ /** Path-continuing resolution handed to the ambient slot while an accessor class constructs. */
505
+ #ambientResolutionFor(resolutionStack) {
506
+ return {
507
+ resolve: (token, options) => options === undefined
508
+ ? this.resolveFromContext(token, resolutionStack)
509
+ : this.resolve(token, options, resolutionStack),
510
+ resolveOptional: (token, options) => this.resolveOptional(token, options, resolutionStack),
511
+ };
512
+ }
513
+ #instantiateSync(binding, ctx, resolutionStack) {
381
514
  switch (binding.kind) {
382
515
  case "constant":
383
516
  return binding.value;
@@ -387,26 +520,26 @@ export class DependencyResolver {
387
520
  }
388
521
  const factoryResult = binding.factory(ctx);
389
522
  if (factoryResult instanceof Promise) {
390
- throw new AsyncResolutionError(tokenName(binding.token), tokenName(binding.token));
523
+ throw asyncResolutionErrorFor(binding, resolutionStack);
391
524
  }
392
525
  return factoryResult;
393
526
  }
394
527
  case "dynamic-async":
395
- throw new AsyncResolutionError(tokenName(binding.token), tokenName(binding.token));
528
+ throw asyncResolutionErrorFor(binding, resolutionStack);
396
529
  case "class": {
397
- const deps = this.#resolveDeps(this.#constructorParams(binding.target), resolutionPath, resolutionStack);
398
- return this.#classes.instantiate(binding.target, deps);
530
+ const deps = this.#resolveDeps(this.#constructorParams(binding.target), resolutionStack);
531
+ return this.#classes.instantiate(binding.target, deps, this.#classes.needsActiveContainer(binding.target) ? this.#ambientResolutionFor(resolutionStack) : undefined);
399
532
  }
400
533
  case "resolved": {
401
- const deps = this.#resolveDeps(binding.deps, resolutionPath, resolutionStack);
534
+ const deps = this.#resolveDeps(binding.deps, resolutionStack);
402
535
  const factoryResult = binding.factory(...deps);
403
536
  if (factoryResult instanceof Promise) {
404
- throw new AsyncResolutionError(tokenName(binding.token), tokenName(binding.token));
537
+ throw asyncResolutionErrorFor(binding, resolutionStack);
405
538
  }
406
539
  return factoryResult;
407
540
  }
408
541
  case "resolved-async":
409
- throw new AsyncResolutionError(tokenName(binding.token), tokenName(binding.token));
542
+ throw asyncResolutionErrorFor(binding, resolutionStack);
410
543
  case "alias":
411
544
  throw new InternalError("alias should have been followed before instantiation");
412
545
  }
@@ -429,66 +562,73 @@ export class DependencyResolver {
429
562
  }
430
563
  // One dispatch table for both dependency sources — constructor params and `toResolved`
431
564
  // descriptors declare the same four things.
432
- #resolveDeps(deps, resolutionPath, resolutionStack) {
565
+ #resolveDeps(deps, resolutionStack) {
433
566
  const count = deps.length;
434
567
  if (count === 0) {
435
568
  return [];
436
569
  }
437
570
  if (count === 1) {
438
- return [this.#resolveDep(deps[0], resolutionPath, resolutionStack)];
571
+ return [this.#resolveDep(deps[0], resolutionStack)];
439
572
  }
440
573
  const resolved = new Array(count);
441
574
  for (let index = 0; index < count; index += 1) {
442
- resolved[index] = this.#resolveDep(deps[index], resolutionPath, resolutionStack);
575
+ resolved[index] = this.#resolveDep(deps[index], resolutionStack);
443
576
  }
444
577
  return resolved;
445
578
  }
446
- #resolveDep(dep, resolutionPath, resolutionStack) {
447
- const options = injectionSlotToResolveOptions(dep);
579
+ #resolveDep(dep, resolutionStack) {
580
+ const options = resolveOptionsForSlot(dep);
448
581
  if (dep.multi) {
449
- return this.resolveAll(dep.token, options, resolutionPath, resolutionStack);
582
+ return this.resolveAll(dep.token, options, resolutionStack);
450
583
  }
451
584
  if (dep.optional) {
452
- return this.resolveOptional(dep.token, options, resolutionPath, resolutionStack);
585
+ return this.resolveOptional(dep.token, options, resolutionStack);
453
586
  }
454
587
  if (options === undefined) {
455
- return this.resolveFromContext(dep.token, resolutionPath, resolutionStack);
588
+ return this.resolveFromContext(dep.token, resolutionStack);
456
589
  }
457
- return this.resolve(dep.token, options, resolutionPath, resolutionStack);
590
+ return this.resolve(dep.token, options, resolutionStack);
458
591
  }
459
- resolveOptional(token, options, resolutionPath, resolutionStack) {
460
- if (this.#findBinding(token, options, resolutionPath, resolutionStack) === undefined) {
592
+ resolveOptional(token, options, resolutionStack) {
593
+ const entry = this.#findBinding(token, options, resolutionStack);
594
+ if (entry === undefined) {
461
595
  return undefined;
462
596
  }
463
- return this.resolve(token, options, resolutionPath, resolutionStack);
597
+ // Resolve the entry the probe found: re-looking the token up would evaluate every `when()`
598
+ // predicate a second time, and a changed answer would throw where `undefined` was promised.
599
+ const { binding, owner } = entry;
600
+ if (binding.kind === "alias") {
601
+ return this.resolve(token, options, resolutionStack);
602
+ }
603
+ if (binding.scope === "singleton" && owner !== this) {
604
+ return owner.#resolveBinding(binding, options, resolutionStack, owner);
605
+ }
606
+ return this.#resolveBinding(binding, options, resolutionStack, owner);
464
607
  }
465
- resolveAll(token, options, resolutionPath, resolutionStack) {
466
- const candidates = this.#candidateBindings(token, options, resolutionPath, resolutionStack);
608
+ resolveAll(token, options, resolutionStack) {
609
+ const candidates = this.#candidateBindings(token, options, resolutionStack);
467
610
  const resolved = new Array(candidates.length);
468
611
  for (let index = 0; index < candidates.length; index += 1) {
469
- resolved[index] = this.#resolveCandidateSync(candidates[index], options, resolutionPath, resolutionStack);
612
+ resolved[index] = this.#resolveCandidateSync(candidates[index], options, resolutionStack);
470
613
  }
471
614
  return resolved;
472
615
  }
473
616
  /** Every binding in the chain a `resolveAll` request matches, in chain order. */
474
- #candidateBindings(token, options, resolutionPath, resolutionStack) {
475
- if (options !== undefined && isNameOnlyOptions(options)) {
476
- // The name index has matched the slot already, but a hit may still carry a predicate —
477
- // and that is the selection path's job to evaluate.
478
- const named = this.#namedBindingsFromChain(token, options.name);
479
- if (!anyPredicate(named)) {
480
- return named;
617
+ #candidateBindings(token, options, resolutionStack) {
618
+ if (options !== undefined) {
619
+ const indexed = this.#indexedCandidates(token, options, resolutionStack);
620
+ if (indexed !== null) {
621
+ return indexed;
481
622
  }
482
- return selectAllBindings(named, options, this.#makeConstraintContext(resolutionPath, resolutionStack, options));
483
623
  }
484
624
  const allBindings = this.#allBindingsFromChain(token);
485
625
  if (allBindings.length === 0) {
486
626
  return allBindings;
487
627
  }
488
- return selectAllBindings(allBindings, options, this.#makeConstraintContext(resolutionPath, resolutionStack, options));
628
+ return selectAllBindings(allBindings, options, this.#makeConstraintContext(resolutionStack, options));
489
629
  }
490
- // ── Async resolve ──────────────────────────────────────────────────────────
491
- resolveAsyncFromContext(token, resolutionPath, resolutionStack, branchDepth) {
630
+ // ── Async resolve ──────────────────────────────────────────────────────────────────────────────────────────────────
631
+ resolveAsyncFromContext(token, resolutionStack, branchDepth) {
492
632
  // Hot lane: own-registry fast default (async chains resolve sibling dynamic bindings).
493
633
  // Fall back to the chain-versioned memo only on miss or alias.
494
634
  const fastBinding = this.#registry.getFastDefault(token);
@@ -497,24 +637,24 @@ export class DependencyResolver {
497
637
  if ((fastBinding.kind === "dynamic-async" || fastBinding.kind === "dynamic") &&
498
638
  fastBinding.scope === "transient" &&
499
639
  !this.#hasAnyActivation(fastBinding)) {
500
- return this.#resolveTransientDynamicAsyncFromContext(fastBinding, resolutionPath, resolutionStack, branchDepth);
640
+ return this.#resolveTransientDynamicAsyncFromContext(fastBinding, resolutionStack, branchDepth);
501
641
  }
502
- return this.#resolveAsyncDefaultEntry(fastBinding, this, resolutionPath, resolutionStack, branchDepth);
642
+ return this.#resolveAsyncDefaultEntry(fastBinding, this, resolutionStack, branchDepth);
503
643
  }
504
644
  const entry = this.#lookup.defaultEntry(token);
505
645
  if (entry === null) {
506
- return this.resolveAsync(token, undefined, resolutionPath, resolutionStack, branchDepth);
646
+ return this.resolveAsync(token, undefined, resolutionStack, branchDepth);
507
647
  }
508
- return this.#resolveAsyncDefaultEntry(entry.binding, entry.owner, resolutionPath, resolutionStack, branchDepth);
648
+ return this.#resolveAsyncDefaultEntry(entry.binding, entry.owner, resolutionStack, branchDepth);
509
649
  }
510
- #resolveAsyncDefaultEntry(binding, owner, resolutionPath, resolutionStack, branchDepth) {
511
- if (this.#isPlainConstant(binding)) {
650
+ #resolveAsyncDefaultEntry(binding, owner, resolutionStack, branchDepth) {
651
+ if (owner.#isPlainConstant(binding)) {
512
652
  return Promise.resolve(binding.value);
513
653
  }
514
654
  const scope = binding.scope;
515
655
  if (scope === "transient") {
516
- if ((binding.kind === "dynamic" || binding.kind === "dynamic-async") && !this.#hasAnyActivation(binding)) {
517
- return this.#resolveTransientDynamicAsyncFromContext(binding, resolutionPath, resolutionStack, branchDepth);
656
+ if ((binding.kind === "dynamic" || binding.kind === "dynamic-async") && !owner.#hasAnyActivation(binding)) {
657
+ return this.#resolveTransientDynamicAsyncFromContext(binding, resolutionStack, branchDepth);
518
658
  }
519
659
  }
520
660
  else if (scope === "singleton") {
@@ -522,29 +662,30 @@ export class DependencyResolver {
522
662
  return Promise.resolve(binding.instance);
523
663
  }
524
664
  if (owner !== this) {
525
- return owner.#resolveBindingAsync(binding, undefined, resolutionPath, resolutionStack, branchDepth);
665
+ return owner.#resolveBindingAsync(binding, undefined, resolutionStack, branchDepth, owner);
526
666
  }
527
667
  }
528
668
  else if (this.#scope.isChild) {
529
- if (this.#scope.hasScoped(binding.id)) {
530
- return Promise.resolve(this.#scope.getScoped(binding.id));
669
+ const cachedScoped = this.#scope.readScoped(binding.id);
670
+ if (cachedScoped !== SCOPED_MISS) {
671
+ return Promise.resolve(cachedScoped);
531
672
  }
532
673
  }
533
674
  else {
534
675
  // Not `#readScoped`: this entry point reports failure as a rejection, never a sync throw.
535
676
  return Promise.reject(new MissingScopeContextError(tokenName(binding.token)));
536
677
  }
537
- return this.#resolveBindingAsync(binding, undefined, resolutionPath, resolutionStack, branchDepth);
678
+ return this.#resolveBindingAsync(binding, undefined, resolutionStack, branchDepth, owner);
538
679
  }
539
- async resolveAsync(token, options, resolutionPath, resolutionStack, branchDepth = UNOWNED_BRANCH) {
540
- const { binding, owner } = this.#requireBinding(token, options, resolutionPath, resolutionStack);
680
+ async resolveAsync(token, options, resolutionStack, branchDepth = UNOWNED_BRANCH) {
681
+ const { binding, owner } = this.#requireBinding(token, options, resolutionStack);
541
682
  if (binding.scope === "singleton" && owner !== this) {
542
- return owner.#resolveBindingAsync(binding, options, resolutionPath, resolutionStack, branchDepth);
683
+ return owner.#resolveBindingAsync(binding, options, resolutionStack, branchDepth, owner);
543
684
  }
544
- return this.#resolveBindingAsync(binding, options, resolutionPath, resolutionStack, branchDepth);
685
+ return this.#resolveBindingAsync(binding, options, resolutionStack, branchDepth, owner);
545
686
  }
546
- async #resolveBindingAsync(binding, options, resolutionPath, resolutionStack, branchDepth) {
547
- if (this.#isPlainConstant(binding)) {
687
+ async #resolveBindingAsync(binding, options, resolutionStack, branchDepth, owner) {
688
+ if (owner.#isPlainConstant(binding)) {
548
689
  return binding.value;
549
690
  }
550
691
  const scope = binding.scope;
@@ -557,21 +698,31 @@ export class DependencyResolver {
557
698
  if (inflight !== undefined) {
558
699
  return inflight;
559
700
  }
701
+ if (this.#scope.isClosed) {
702
+ throw new DisposedContainerError();
703
+ }
560
704
  }
561
705
  else if (scope === "scoped") {
562
706
  const cachedScoped = this.#readScoped(binding);
563
707
  if (cachedScoped !== SCOPED_MISS) {
564
708
  return cachedScoped;
565
709
  }
710
+ // In-flight dedup, scoped flavor: one instance per scope even under concurrency.
711
+ const inflight = this.#scope.getInflight(binding.id);
712
+ if (inflight !== undefined) {
713
+ return inflight;
714
+ }
715
+ if (this.#scope.isClosed) {
716
+ throw new DisposedContainerError();
717
+ }
566
718
  }
567
719
  const frame = this.#getResolutionFrame(binding);
568
- // This level appends to its own branch and never unwinds — see ARCHITECTURE.md.
569
- const levelPath = extendResolutionBranch(resolutionPath, branchDepth, frame.tokenName);
570
- const levelStack = extendResolutionStackBranch(resolutionStack, branchDepth, frame);
571
- const levelDepth = branchDepthOf(levelPath);
572
- const needsActivation = this.#activation.needsActivation(binding);
720
+ // This level appends to its own branch and never unwinds.
721
+ const levelStack = extendResolutionBranch(resolutionStack, branchDepth, frame);
722
+ const levelDepth = branchDepthOf(levelStack);
723
+ const needsActivation = owner.#activation.needsActivation(binding);
573
724
  if (!needsActivation && scope === "transient" && (binding.kind === "dynamic" || binding.kind === "dynamic-async")) {
574
- const resolutionCtx = new AsyncLevelContext(this, levelPath, levelStack, options);
725
+ const resolutionCtx = new AsyncLevelContext(this, levelStack, options);
575
726
  if (binding.kind === "dynamic-async") {
576
727
  return await binding.factory(resolutionCtx);
577
728
  }
@@ -579,11 +730,11 @@ export class DependencyResolver {
579
730
  return dynamicResult instanceof Promise ? await dynamicResult : dynamicResult;
580
731
  }
581
732
  const resolutionCtx = needsActivation || requiresResolutionContext(binding)
582
- ? new AsyncLevelContext(this, levelPath, levelStack, options)
733
+ ? new AsyncLevelContext(this, levelStack, options)
583
734
  : undefined;
584
735
  if (scope === "singleton") {
585
736
  // The promise is published before it settles, so concurrent callers dedup onto it.
586
- const singletonPromise = this.#instantiateAndActivateAsync(binding, resolutionCtx, levelPath, levelStack, levelDepth, needsActivation).then((activated) => {
737
+ const singletonPromise = this.#instantiateAndActivateAsync(binding, resolutionCtx, levelStack, levelDepth, needsActivation, owner).then((activated) => {
587
738
  this.#scope.setSingleton(binding, activated);
588
739
  this.#scope.clearInflight(binding.id);
589
740
  return activated;
@@ -594,20 +745,42 @@ export class DependencyResolver {
594
745
  this.#scope.setInflight(binding.id, singletonPromise);
595
746
  return await singletonPromise;
596
747
  }
597
- const activated = await this.#instantiateAndActivateAsync(binding, resolutionCtx, levelPath, levelStack, levelDepth, needsActivation);
598
748
  if (scope === "scoped") {
599
- this.#scope.setScoped(binding.id, activated);
749
+ // Published before it settles, like the singleton lane: concurrent callers share one creation.
750
+ const scopedPromise = this.#instantiateAndActivateAsync(binding, resolutionCtx, levelStack, levelDepth, needsActivation, owner).then((activated) => {
751
+ this.#scope.setScoped(binding, activated);
752
+ this.#scope.clearInflight(binding.id);
753
+ return activated;
754
+ }, (error) => {
755
+ this.#scope.clearInflight(binding.id);
756
+ throw error;
757
+ });
758
+ this.#scope.setInflight(binding.id, scopedPromise);
759
+ return await scopedPromise;
600
760
  }
601
- return activated;
761
+ return await this.#instantiateAndActivateAsync(binding, resolutionCtx, levelStack, levelDepth, needsActivation, owner);
602
762
  }
603
- async #instantiateAndActivateAsync(binding, ctx, resolutionPath, resolutionStack, branchDepth, needsActivation) {
604
- const instance = await this.#instantiateAsync(binding, ctx, resolutionPath, resolutionStack, branchDepth);
605
- if (!this.#activation.refreshAfterFirstInstantiation(binding, needsActivation)) {
763
+ /**
764
+ * Settles this resolver's own `postConstruct` answer for a class binding a parent owns.
765
+ *
766
+ * @remarks The owner discovers it on first instantiation, but the plan compiler reads the
767
+ * introspector of whoever is resolving — left unknown, that resolver refuses to compile a plan for
768
+ * this binding on every call, forever.
769
+ */
770
+ #mirrorPostConstructFromOwner(binding, owner) {
771
+ if (owner !== this && binding.kind === "class" && this.#classes.knownPostConstruct(binding.target) === undefined) {
772
+ this.#classes.discoverPostConstruct(binding.target);
773
+ }
774
+ }
775
+ async #instantiateAndActivateAsync(binding, ctx, resolutionStack, branchDepth, needsActivation, owner) {
776
+ const instance = await this.#instantiateAsync(binding, ctx, resolutionStack, branchDepth);
777
+ this.#mirrorPostConstructFromOwner(binding, owner);
778
+ if (!owner.#activation.refreshAfterFirstInstantiation(binding, needsActivation)) {
606
779
  return instance;
607
780
  }
608
- return this.#lifecycle.runActivation(ctx, binding, instance, this.#metadataReader);
781
+ return owner.#lifecycle.runActivation(ctx, binding, instance, owner.#metadataReader);
609
782
  }
610
- async #instantiateAsync(binding, ctx, resolutionPath, resolutionStack, branchDepth) {
783
+ async #instantiateAsync(binding, ctx, resolutionStack, branchDepth) {
611
784
  switch (binding.kind) {
612
785
  case "constant":
613
786
  return binding.value;
@@ -624,69 +797,76 @@ export class DependencyResolver {
624
797
  }
625
798
  return binding.factory(ctx);
626
799
  case "class": {
627
- const deps = await this.#resolveDepsAsync(this.#constructorParams(binding.target), resolutionPath, resolutionStack, branchDepth);
628
- return this.#classes.instantiate(binding.target, deps);
800
+ const deps = await this.#resolveDepsAsync(this.#constructorParams(binding.target), resolutionStack, branchDepth);
801
+ // Accessor initializers resolve synchronously, so the branch-owned path serves them directly.
802
+ return this.#classes.instantiate(binding.target, deps, this.#classes.needsActiveContainer(binding.target) ? this.#ambientResolutionFor(resolutionStack) : undefined);
629
803
  }
630
804
  case "resolved": {
631
- if (ctx === undefined) {
632
- throw new InternalError("resolved binding requires resolution context");
633
- }
634
- const deps = await this.#resolveDepsAsync(binding.deps, resolutionPath, resolutionStack, branchDepth);
805
+ const deps = await this.#resolveDepsAsync(binding.deps, resolutionStack, branchDepth);
635
806
  const factoryResult = binding.factory(...deps);
636
807
  return factoryResult instanceof Promise ? factoryResult : Promise.resolve(factoryResult);
637
808
  }
638
809
  case "resolved-async": {
639
- const deps = await this.#resolveDepsAsync(binding.deps, resolutionPath, resolutionStack, branchDepth);
810
+ const deps = await this.#resolveDepsAsync(binding.deps, resolutionStack, branchDepth);
640
811
  return binding.factory(...deps);
641
812
  }
642
813
  case "alias":
643
814
  throw new InternalError("alias should have been followed before instantiation");
644
815
  }
645
816
  }
646
- async #resolveDepsAsync(deps, resolutionPath, resolutionStack, branchDepth) {
817
+ async #resolveDepsAsync(deps, resolutionStack, branchDepth) {
647
818
  const count = deps.length;
648
819
  if (count === 0) {
649
820
  return [];
650
821
  }
651
822
  if (count === 1) {
652
- return [await this.#resolveDepAsync(deps[0], resolutionPath, resolutionStack, branchDepth)];
823
+ return [await this.#resolveDepAsync(deps[0], resolutionStack, branchDepth)];
653
824
  }
654
825
  // Siblings resolve concurrently and each extends the same branch, so the first appends in
655
826
  // place and the rest copy the prefix — no caller has to isolate them.
656
827
  const pending = new Array(count);
657
828
  for (let index = 0; index < count; index += 1) {
658
- pending[index] = this.#resolveDepAsync(deps[index], resolutionPath, resolutionStack, branchDepth);
829
+ pending[index] = this.#resolveDepAsync(deps[index], resolutionStack, branchDepth);
659
830
  }
660
831
  return Promise.all(pending);
661
832
  }
662
- #resolveDepAsync(dep, resolutionPath, resolutionStack, branchDepth) {
663
- const options = injectionSlotToResolveOptions(dep);
833
+ #resolveDepAsync(dep, resolutionStack, branchDepth) {
834
+ const options = resolveOptionsForSlot(dep);
664
835
  if (dep.multi) {
665
- return this.resolveAllAsync(dep.token, options, resolutionPath, resolutionStack, branchDepth);
836
+ return this.resolveAllAsync(dep.token, options, resolutionStack, branchDepth);
666
837
  }
667
838
  if (dep.optional) {
668
- return this.resolveOptionalAsync(dep.token, options, resolutionPath, resolutionStack, branchDepth);
839
+ return this.resolveOptionalAsync(dep.token, options, resolutionStack, branchDepth);
669
840
  }
670
841
  if (options === undefined) {
671
- return this.resolveAsyncFromContext(dep.token, resolutionPath, resolutionStack, branchDepth);
842
+ return this.resolveAsyncFromContext(dep.token, resolutionStack, branchDepth);
672
843
  }
673
- return this.resolveAsync(dep.token, options, resolutionPath, resolutionStack, branchDepth);
844
+ return this.resolveAsync(dep.token, options, resolutionStack, branchDepth);
674
845
  }
675
- async resolveOptionalAsync(token, options, resolutionPath, resolutionStack, branchDepth = UNOWNED_BRANCH) {
676
- if (this.#findBinding(token, options, resolutionPath, resolutionStack) === undefined) {
846
+ async resolveOptionalAsync(token, options, resolutionStack, branchDepth = UNOWNED_BRANCH) {
847
+ const entry = this.#findBinding(token, options, resolutionStack);
848
+ if (entry === undefined) {
677
849
  return undefined;
678
850
  }
679
- return this.resolveAsync(token, options, resolutionPath, resolutionStack, branchDepth);
851
+ // Same single-evaluation contract as the sync lane: resolve what the probe found.
852
+ const { binding, owner } = entry;
853
+ if (binding.kind === "alias") {
854
+ return this.resolveAsync(token, options, resolutionStack, branchDepth);
855
+ }
856
+ if (binding.scope === "singleton" && owner !== this) {
857
+ return owner.#resolveBindingAsync(binding, options, resolutionStack, branchDepth, owner);
858
+ }
859
+ return this.#resolveBindingAsync(binding, options, resolutionStack, branchDepth, owner);
680
860
  }
681
- async resolveAllAsync(token, options, resolutionPath, resolutionStack, branchDepth = UNOWNED_BRANCH) {
682
- const candidates = this.#candidateBindings(token, options, resolutionPath, resolutionStack);
861
+ async resolveAllAsync(token, options, resolutionStack, branchDepth = UNOWNED_BRANCH) {
862
+ const candidates = this.#candidateBindings(token, options, resolutionStack);
683
863
  const pending = new Array(candidates.length);
684
864
  for (let index = 0; index < candidates.length; index += 1) {
685
- pending[index] = this.#resolveCandidateAsync(candidates[index], options, resolutionPath, resolutionStack, branchDepth);
865
+ pending[index] = this.#resolveCandidateAsync(candidates[index], options, resolutionStack, branchDepth);
686
866
  }
687
867
  return Promise.all(pending);
688
868
  }
689
- // ── Helpers ────────────────────────────────────────────────────────────────
869
+ // ── Helpers ────────────────────────────────────────────────────────────────────────────────────────────────────────
690
870
  #allBindingsFromChain(token) {
691
871
  const ownBindings = this.#registry.getAll(token);
692
872
  if (this.#parent === undefined) {
@@ -718,6 +898,52 @@ export class DependencyResolver {
718
898
  }
719
899
  return result;
720
900
  }
901
+ /**
902
+ * The candidates an index can name outright, or `null` when the request needs full selection.
903
+ *
904
+ * @remarks Kept off `#candidateBindings` so that method stays the size it was: a request neither
905
+ * index serves must not pay for the two that do.
906
+ * An index has matched the slot already, but a hit may still carry a predicate, and evaluating
907
+ * that is the selection path's job.
908
+ */
909
+ #indexedCandidates(token, options, resolutionStack) {
910
+ if (isNameOnlyOptions(options)) {
911
+ const named = this.#namedBindingsFromChain(token, options.name);
912
+ return anyPredicate(named)
913
+ ? selectAllBindings(named, options, this.#makeConstraintContext(resolutionStack, options))
914
+ : named;
915
+ }
916
+ const singleTag = singleTagOnlyOf(options);
917
+ if (singleTag === undefined) {
918
+ return null;
919
+ }
920
+ const tagged = this.#taggedBindingsFromChain(token, singleTag);
921
+ return anyPredicate(tagged)
922
+ ? selectAllBindings(tagged, options, this.#makeConstraintContext(resolutionStack, options))
923
+ : tagged;
924
+ }
925
+ /**
926
+ * Every binding the chain's tag indexes hold for one tag, nearest container first.
927
+ *
928
+ * @remarks A request for one tag and no name matches exactly the bindings the index keys, so this
929
+ * is the whole candidate set rather than a prefilter — a named or multi-tag slot cannot satisfy it.
930
+ */
931
+ #taggedBindingsFromChain(token, tag) {
932
+ // A tag matches at most one binding per registry, so a root container's answer is built whole
933
+ // rather than grown — the shape `#namedBindingsFromChain` takes, for the same reason.
934
+ const ownBinding = this.#registry.getSimpleTagged(token, tag);
935
+ if (this.#parent === undefined) {
936
+ return ownBinding === undefined ? [] : [ownBinding];
937
+ }
938
+ const result = ownBinding === undefined ? [] : [ownBinding];
939
+ for (let current = this.#parent; current !== undefined; current = current.#parent) {
940
+ const binding = current.#registry.getSimpleTagged(token, tag);
941
+ if (binding !== undefined) {
942
+ result.push(binding);
943
+ }
944
+ }
945
+ return result;
946
+ }
721
947
  /** A constant with no activation anywhere resolves to its value with no pipeline at all. */
722
948
  #isPlainConstant(binding) {
723
949
  return (binding.kind === "constant" &&
@@ -741,70 +967,100 @@ export class DependencyResolver {
741
967
  if (!this.#scope.isChild) {
742
968
  throw new MissingScopeContextError(tokenName(binding.token));
743
969
  }
744
- if (this.#scope.hasScoped(binding.id)) {
745
- return this.#scope.getScoped(binding.id);
746
- }
747
- return SCOPED_MISS;
970
+ return this.#scope.readScoped(binding.id);
748
971
  }
749
- #makeConstraintContext(resolutionPath, resolutionStack, options) {
750
- if (options === undefined && resolutionPath.length === 0 && resolutionStack.length === 0) {
972
+ // The shared root context answers every top-level request; building one is the rarer half and
973
+ // lives outside, so what a selection inlines is the test and not the literal.
974
+ #makeConstraintContext(resolutionStack, options) {
975
+ if (options === undefined && resolutionStack.length === 0) {
751
976
  return ROOT_CONSTRAINT_CONTEXT;
752
977
  }
753
- return {
754
- resolutionPath,
755
- resolutionStack,
756
- parent: resolutionStack.at(-1),
757
- ancestors: resolutionStack.length > 1 ? resolutionStack.slice(0, -1) : [],
758
- currentResolveOptions: options,
759
- };
978
+ return buildConstraintContext(resolutionStack, options);
760
979
  }
761
- #matchesBindingFast(binding, options, resolutionPath, resolutionStack) {
980
+ /** Selection for a name-less multi-tag request, over the union of the two tag indexes. */
981
+ #selectMultiTagged(token, options, resolutionStack) {
982
+ const candidates = [];
983
+ this.#gatherTagCandidates(token, options.tag, candidates);
984
+ const listed = options.tags;
985
+ if (listed !== undefined) {
986
+ for (let index = 0; index < listed.length; index += 1) {
987
+ this.#gatherTagCandidates(token, listed[index], candidates);
988
+ }
989
+ }
990
+ if (candidates.length === 0) {
991
+ return undefined;
992
+ }
993
+ return selectBinding(candidates, options, this.#makeConstraintContext(resolutionStack, options), tokenName(token));
994
+ }
995
+ /** One request criterion's candidates: its exact single-tag binding, plus its first-tag bucket. */
996
+ #gatherTagCandidates(token, criterion, out) {
997
+ if (criterion === undefined) {
998
+ return;
999
+ }
1000
+ // The includes probes only guard a request repeating a criterion across its two spellings.
1001
+ const single = this.#registry.getSimpleTagged(token, criterion);
1002
+ if (single !== undefined && !out.includes(single)) {
1003
+ out.push(single);
1004
+ }
1005
+ const bucket = this.#registry.getMultiTagged(token, criterion);
1006
+ if (bucket !== undefined) {
1007
+ for (let index = 0; index < bucket.length; index += 1) {
1008
+ const candidate = bucket[index];
1009
+ if (!out.includes(candidate)) {
1010
+ out.push(candidate);
1011
+ }
1012
+ }
1013
+ }
1014
+ }
1015
+ #matchesBindingFast(binding, options, resolutionStack) {
762
1016
  if (!matchesSlot(binding.slot, options)) {
763
1017
  return false;
764
1018
  }
765
- if (binding.predicate === undefined) {
1019
+ return this.#satisfiesPredicate(binding, options, resolutionStack);
1020
+ }
1021
+ /** The predicate half of a match, for a lane whose index has already settled the slot. */
1022
+ #satisfiesPredicate(binding, options, resolutionStack) {
1023
+ const predicate = binding.predicate;
1024
+ if (predicate === undefined) {
766
1025
  return true;
767
1026
  }
768
- return binding.predicate(this.#makeConstraintContext(resolutionPath, resolutionStack, options));
1027
+ return predicate(this.#makeConstraintContext(resolutionStack, options));
769
1028
  }
770
- #resolveTransientDynamicSyncFromContext(binding, resolutionPath, resolutionStack) {
1029
+ #resolveTransientDynamicSyncFromContext(binding, resolutionStack) {
771
1030
  // One lane at every depth: `binding.inFlight` is O(1), so there is nothing to escape.
772
1031
  const frame = this.#getResolutionFrame(binding);
773
1032
  const tokenDisplayName = frame.tokenName;
774
1033
  if (binding.inFlight) {
775
- throw new CircularDependencyError([...resolutionPath, tokenDisplayName]);
1034
+ throw new CircularDependencyError(cycleNamesOf(resolutionStack, tokenDisplayName));
776
1035
  }
777
1036
  binding.inFlight = true;
778
- resolutionPath.push(tokenDisplayName);
779
1037
  resolutionStack.push(frame);
780
- const resolutionCtx = this.#acquireSyncResolutionContext(resolutionPath, resolutionStack, undefined);
1038
+ const resolutionCtx = this.#acquireSyncResolutionContext(resolutionStack, undefined);
781
1039
  try {
782
1040
  const dynamicResult = binding.factory(resolutionCtx);
783
1041
  if (dynamicResult instanceof Promise) {
784
- throw new AsyncResolutionError(tokenDisplayName, tokenDisplayName);
1042
+ throw new AsyncResolutionError(resolutionStack[0]?.tokenName ?? tokenDisplayName, tokenDisplayName);
785
1043
  }
786
1044
  return dynamicResult;
787
1045
  }
788
1046
  finally {
789
1047
  resolutionStack.pop();
790
- resolutionPath.pop();
791
1048
  binding.inFlight = false;
792
1049
  }
793
1050
  }
794
1051
  // Deliberately not `async`: that would allocate a state machine and a promise per level.
795
- #resolveTransientDynamicAsyncFromContext(binding, resolutionPath, resolutionStack, branchDepth) {
1052
+ #resolveTransientDynamicAsyncFromContext(binding, resolutionStack, branchDepth) {
796
1053
  const frame = this.#getResolutionFrame(binding);
797
- let levelPath;
1054
+ let levelStack;
798
1055
  try {
799
- levelPath = extendResolutionBranch(resolutionPath, branchDepth, frame.tokenName);
1056
+ levelStack = extendResolutionBranch(resolutionStack, branchDepth, frame);
800
1057
  }
801
1058
  catch (cycleError) {
802
1059
  // This method is not `async`; keep failures as rejections rather than sync throws.
803
1060
  return Promise.reject(cycleError);
804
1061
  }
805
- const levelStack = extendResolutionStackBranch(resolutionStack, branchDepth, frame);
806
1062
  // Nothing this level appended is ever removed, so no level observes its own settlement.
807
- const ctx = new AsyncLevelContext(this, levelPath, levelStack, undefined);
1063
+ const ctx = new AsyncLevelContext(this, levelStack, undefined);
808
1064
  try {
809
1065
  if (binding.kind === "dynamic-async") {
810
1066
  return binding.factory(ctx);
@@ -816,16 +1072,16 @@ export class DependencyResolver {
816
1072
  return Promise.reject(factoryError);
817
1073
  }
818
1074
  }
819
- // ── The cascade lane ───────────────────────────────────────────────────────
1075
+ // ── The cascade lane ───────────────────────────────────────────────────────────────────────────────────────────────
820
1076
  /**
821
1077
  * Entry for a request a factory makes from inside an open synchronous cascade.
822
1078
  *
823
1079
  * @remarks A request arriving with no cascade open came out of a continuation, so its ancestors
824
- * are on no call stack — it escapes to the branch lane. See `ARCHITECTURE.md`.
1080
+ * are on no call stack — it escapes to the branch lane.
825
1081
  */
826
1082
  resolveAsyncFromCascade(token) {
827
- if (this.#cascadePath.length === 0) {
828
- return this.resolveAsyncFromContext(token, [], [], ROOT_BRANCH);
1083
+ if (this.#cascadeStack.length === 0) {
1084
+ return this.resolveAsyncFromContext(token, [], ROOT_BRANCH);
829
1085
  }
830
1086
  return this.#dispatchCascade(token);
831
1087
  }
@@ -849,10 +1105,39 @@ export class DependencyResolver {
849
1105
  if (fastBinding.scope === "singleton" && fastBinding.instance !== NO_INSTANCE) {
850
1106
  return Promise.resolve(fastBinding.instance);
851
1107
  }
1108
+ const planned = this.#plannedCascadeAnswer(fastBinding);
1109
+ if (planned !== null) {
1110
+ return planned;
1111
+ }
852
1112
  }
853
1113
  // Anything else leaves the cascade lane for good, seeded with a snapshot of the ancestors it
854
1114
  // accumulated — so a cycle across the boundary is still on one path.
855
- return this.resolveAsyncFromContext(token, [...this.#cascadePath], [...this.#cascadeStack], UNOWNED_BRANCH);
1115
+ return this.resolveAsyncFromContext(token, [...this.#cascadeStack], UNOWNED_BRANCH);
1116
+ }
1117
+ /**
1118
+ * A statically-visible transient graph at a true root answers from its compiled async plan.
1119
+ *
1120
+ * @remarks Kept out of the dispatcher so its size stays inlinable. Inside an open cascade the
1121
+ * graph must escape instead, so its escapes carry the live ancestors — hence the idle gate.
1122
+ */
1123
+ #plannedCascadeAnswer(fastBinding) {
1124
+ if (this.#cascadeStack.length !== 0 ||
1125
+ fastBinding.scope !== "transient" ||
1126
+ (fastBinding.kind !== "class" && fastBinding.kind !== "resolved" && fastBinding.kind !== "resolved-async")) {
1127
+ return null;
1128
+ }
1129
+ const plan = this.#getAsyncInstantiationPlan(fastBinding);
1130
+ if (plan === null) {
1131
+ return null;
1132
+ }
1133
+ try {
1134
+ const planned = plan();
1135
+ return planned instanceof Promise ? planned : Promise.resolve(planned);
1136
+ }
1137
+ catch (planError) {
1138
+ // The interpreted lane is async, so a sync throw is a rejection there too.
1139
+ return Promise.reject(planError);
1140
+ }
856
1141
  }
857
1142
  #resolveTransientDynamicAsyncCascade(binding) {
858
1143
  const frame = this.#getResolutionFrame(binding);
@@ -860,11 +1145,10 @@ export class DependencyResolver {
860
1145
  // code does not interleave — so the O(1) flag is exact path membership here, as it is for the
861
1146
  // sync lane. It is cleared when the factory returns its promise, not when that promise settles.
862
1147
  if (binding.inFlight) {
863
- return Promise.reject(new CircularDependencyError([...this.#cascadePath, frame.tokenName]));
1148
+ return Promise.reject(new CircularDependencyError(cycleNamesOf(this.#cascadeStack, frame.tokenName)));
864
1149
  }
865
- const ctx = (this.#cascadeContext ??= new AsyncCascadeContext(this, this.#cascadePath, this.#cascadeStack));
1150
+ const ctx = (this.#cascadeContext ??= new AsyncCascadeContext(this, this.#cascadeStack));
866
1151
  binding.inFlight = true;
867
- this.#cascadePath.push(frame.tokenName);
868
1152
  this.#cascadeStack.push(frame);
869
1153
  try {
870
1154
  if (binding.kind === "dynamic-async") {
@@ -878,35 +1162,64 @@ export class DependencyResolver {
878
1162
  }
879
1163
  finally {
880
1164
  this.#cascadeStack.pop();
881
- this.#cascadePath.pop();
882
1165
  binding.inFlight = false;
883
1166
  }
884
1167
  }
885
1168
  // A cached candidate answers here rather than re-entering the generic path: `resolveAll` pays
886
1169
  // this per candidate, and a fan-out over cached handlers is the shape that makes it matter.
887
- #resolveCandidateSync(binding, options, resolutionPath, resolutionStack) {
888
- if (this.#isPlainConstant(binding)) {
1170
+ #resolveCandidateSync(binding, options, resolutionStack) {
1171
+ // Fan-outs are dominated by constants: with no activation hook anywhere in the chain, a
1172
+ // hook-free constant is plain no matter which container owns it — skip the owner probe.
1173
+ if (binding.kind === "constant" && binding.onActivation === undefined && this.#chainActivationVersion() === 0) {
1174
+ return binding.value;
1175
+ }
1176
+ const owner = this.#ownerOf(binding);
1177
+ if (owner.#isPlainConstant(binding)) {
889
1178
  return binding.value;
890
1179
  }
891
1180
  if (binding.kind === "alias") {
892
- return this.resolve(binding.target, options, resolutionPath, resolutionStack);
1181
+ return this.resolve(binding.target, options, resolutionStack);
893
1182
  }
894
- if (binding.scope === "singleton" && binding.instance !== NO_INSTANCE) {
895
- return binding.instance;
1183
+ if (binding.scope === "singleton") {
1184
+ if (binding.instance !== NO_INSTANCE) {
1185
+ return binding.instance;
1186
+ }
1187
+ // Owner-routed like `resolve`: the owner materializes and caches its own singleton.
1188
+ return owner.#resolveBinding(binding, options, resolutionStack, owner);
896
1189
  }
897
- return this.#resolveBinding(binding, options, resolutionPath, resolutionStack);
1190
+ return this.#resolveBinding(binding, options, resolutionStack, owner);
898
1191
  }
899
- #resolveCandidateAsync(binding, options, resolutionPath, resolutionStack, branchDepth) {
900
- if (this.#isPlainConstant(binding)) {
1192
+ #resolveCandidateAsync(binding, options, resolutionStack, branchDepth) {
1193
+ if (binding.kind === "constant" && binding.onActivation === undefined && this.#chainActivationVersion() === 0) {
1194
+ return Promise.resolve(binding.value);
1195
+ }
1196
+ const owner = this.#ownerOf(binding);
1197
+ if (owner.#isPlainConstant(binding)) {
901
1198
  return Promise.resolve(binding.value);
902
1199
  }
903
1200
  if (binding.kind === "alias") {
904
- return this.resolveAsync(binding.target, options, resolutionPath, resolutionStack, branchDepth);
1201
+ return this.resolveAsync(binding.target, options, resolutionStack, branchDepth);
905
1202
  }
906
- if (binding.scope === "singleton" && binding.instance !== NO_INSTANCE) {
907
- return Promise.resolve(binding.instance);
1203
+ if (binding.scope === "singleton") {
1204
+ if (binding.instance !== NO_INSTANCE) {
1205
+ return Promise.resolve(binding.instance);
1206
+ }
1207
+ return owner.#resolveBindingAsync(binding, options, resolutionStack, branchDepth, owner);
908
1208
  }
909
- return this.#resolveBindingAsync(binding, options, resolutionPath, resolutionStack, branchDepth);
1209
+ return this.#resolveBindingAsync(binding, options, resolutionStack, branchDepth, owner);
1210
+ }
1211
+ /** The resolver whose registry holds `binding` — `this` (the common case) when it is own. */
1212
+ #ownerOf(binding) {
1213
+ // A root resolver can only hold its own bindings, so the per-candidate id probe is chain-only.
1214
+ if (this.#parent === undefined || this.#registry.getById(binding.id) !== undefined) {
1215
+ return this;
1216
+ }
1217
+ for (let current = this.#parent; current !== undefined; current = current.#parent) {
1218
+ if (current.#registry.getById(binding.id) !== undefined) {
1219
+ return current;
1220
+ }
1221
+ }
1222
+ return this;
910
1223
  }
911
1224
  #getResolutionFrame(binding) {
912
1225
  // Memoized on the binding rather than in a per-resolver Map: the frame derives only from
@@ -920,20 +1233,40 @@ export class DependencyResolver {
920
1233
  binding.frame = frame;
921
1234
  return frame;
922
1235
  }
923
- #acquireSyncResolutionContext(resolutionPath, resolutionStack, options) {
1236
+ // A pool is keyed by the one array pair its contexts hold, so reuse can never re-point a context
1237
+ // a live frame still reads — a nested top-level resolve reaches the same depth while the outer
1238
+ // factory runs, and it must get its own context, not the outer frame's re-bound.
1239
+ #acquireSyncResolutionContext(resolutionStack, options) {
1240
+ if (resolutionStack === this.rootStack) {
1241
+ const depth = resolutionStack.length;
1242
+ const existing = this.#syncResolutionContextPool[depth];
1243
+ if (existing !== undefined) {
1244
+ existing.reset(this, resolutionStack, options);
1245
+ return existing;
1246
+ }
1247
+ const created = new DefaultResolutionContext(this, resolutionStack, options);
1248
+ this.#syncResolutionContextPool[depth] = created;
1249
+ return created;
1250
+ }
1251
+ return this.#acquireOffRootSyncContext(resolutionStack, options);
1252
+ }
1253
+ /** The cascade pair pools separately; a throwaway pair (nested resolve, async snapshot) mints per call. */
1254
+ #acquireOffRootSyncContext(resolutionStack, options) {
1255
+ if (resolutionStack !== this.#cascadeStack) {
1256
+ return new DefaultResolutionContext(this, resolutionStack, options);
1257
+ }
924
1258
  const depth = resolutionStack.length;
925
- const existing = this.#syncResolutionContextPool[depth];
1259
+ const pool = (this.#cascadeContextPool ??= []);
1260
+ const existing = pool[depth];
926
1261
  if (existing !== undefined) {
927
- existing.reset(this, resolutionPath, resolutionStack, options);
1262
+ existing.reset(this, resolutionStack, options);
928
1263
  return existing;
929
1264
  }
930
- const created = new DefaultResolutionContext(this, resolutionPath, resolutionStack, options);
931
- this.#syncResolutionContextPool[depth] = created;
1265
+ const created = new DefaultResolutionContext(this, resolutionStack, options);
1266
+ pool[depth] = created;
932
1267
  return created;
933
1268
  }
934
1269
  }
935
- /** Absent scoped entry — distinguishes it from a cached `undefined`. */
936
- const SCOPED_MISS = Symbol("di:scoped-miss");
937
1270
  function anyPredicate(bindings) {
938
1271
  for (let index = 0; index < bindings.length; index += 1) {
939
1272
  if (bindings[index].predicate !== undefined) {
@@ -942,20 +1275,29 @@ function anyPredicate(bindings) {
942
1275
  }
943
1276
  return false;
944
1277
  }
1278
+ function buildConstraintContext(resolutionStack, options) {
1279
+ return {
1280
+ // Derived per read, never cached: the stack may be live, and the names must report it as it stands.
1281
+ get resolutionPath() {
1282
+ const names = new Array(resolutionStack.length);
1283
+ for (let index = 0; index < resolutionStack.length; index += 1) {
1284
+ names[index] = resolutionStack[index].tokenName;
1285
+ }
1286
+ return names;
1287
+ },
1288
+ resolutionStack,
1289
+ parent: resolutionStack.at(-1),
1290
+ ancestors: resolutionStack.length > 1 ? resolutionStack.slice(0, -1) : [],
1291
+ currentResolveOptions: options,
1292
+ };
1293
+ }
1294
+ /** The async-resolution failure for a binding reached on a sync path, naming what to await instead. */
1295
+ function asyncResolutionErrorFor(binding, resolutionStack) {
1296
+ const sourceName = tokenName(binding.token);
1297
+ return new AsyncResolutionError(resolutionStack[0]?.tokenName ?? sourceName, sourceName);
1298
+ }
945
1299
  /** Only a factory is handed the resolution context; everything else gets its deps directly. */
946
1300
  function requiresResolutionContext(binding) {
947
1301
  return binding.kind === "dynamic" || binding.kind === "dynamic-async";
948
1302
  }
949
- /**
950
- * Whether the tag index's answer is the one `Object.is` would give.
951
- *
952
- * @remarks An indexed binding has no name, no predicate and exactly one tag, and the request carries
953
- * only that tag, so `matchesSlot` reduces to the tag values — and the index matched the key already.
954
- * It answers by SameValueZero, which parts from `Object.is` (SPEC §3.5) on exactly one pair: `+0` and
955
- * `-0`. So a request whose value is not zero is already exact, and only a zero-valued one is worth
956
- * reading the stored value for.
957
- */
958
- function matchesIndexedTagValue(binding, requestedValue) {
959
- return requestedValue !== 0 || Object.is(binding.slot.tags[0][1], requestedValue);
960
- }
961
1303
  //# sourceMappingURL=resolver.js.map