@codefast/di 0.5.0-canary.9 → 0.6.0

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