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

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 (214) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/README.md +73 -496
  3. package/dist/binding.d.ts +211 -0
  4. package/dist/binding.d.ts.map +1 -0
  5. package/dist/binding.js +46 -0
  6. package/dist/binding.js.map +1 -0
  7. package/dist/{constructor-type.d.mts → constructor-type.d.ts} +3 -5
  8. package/dist/constructor-type.d.ts.map +1 -0
  9. package/dist/constructor-type.js +2 -0
  10. package/dist/constructor-type.js.map +1 -0
  11. package/dist/container/binding-builders.d.ts +35 -0
  12. package/dist/container/binding-builders.d.ts.map +1 -0
  13. package/dist/container/binding-builders.js +247 -0
  14. package/dist/container/binding-builders.js.map +1 -0
  15. package/dist/container/container.d.ts +58 -0
  16. package/dist/container/container.d.ts.map +1 -0
  17. package/dist/container/container.js +641 -0
  18. package/dist/container/container.js.map +1 -0
  19. package/dist/decorators/inject.d.ts +57 -0
  20. package/dist/decorators/inject.d.ts.map +1 -0
  21. package/dist/decorators/inject.js +145 -0
  22. package/dist/decorators/inject.js.map +1 -0
  23. package/dist/decorators/injectable.d.ts +29 -0
  24. package/dist/decorators/injectable.d.ts.map +1 -0
  25. package/dist/decorators/injectable.js +53 -0
  26. package/dist/decorators/injectable.js.map +1 -0
  27. package/dist/decorators/lifecycle-decorators.d.ts +9 -0
  28. package/dist/decorators/lifecycle-decorators.d.ts.map +1 -0
  29. package/dist/decorators/lifecycle-decorators.js +40 -0
  30. package/dist/decorators/lifecycle-decorators.js.map +1 -0
  31. package/dist/errors.d.ts +150 -0
  32. package/dist/errors.d.ts.map +1 -0
  33. package/dist/errors.js +197 -0
  34. package/dist/errors.js.map +1 -0
  35. package/dist/index.d.ts +30 -0
  36. package/dist/index.d.ts.map +1 -0
  37. package/dist/index.js +25 -0
  38. package/dist/index.js.map +1 -0
  39. package/dist/introspection/dependency-graph.d.ts +40 -0
  40. package/dist/introspection/dependency-graph.d.ts.map +1 -0
  41. package/dist/introspection/dependency-graph.js +66 -0
  42. package/dist/introspection/dependency-graph.js.map +1 -0
  43. package/dist/introspection/graph-adapters/cytoscape.d.ts +33 -0
  44. package/dist/introspection/graph-adapters/cytoscape.d.ts.map +1 -0
  45. package/dist/introspection/graph-adapters/cytoscape.js +30 -0
  46. package/dist/introspection/graph-adapters/cytoscape.js.map +1 -0
  47. package/dist/introspection/graph-adapters/dot.d.ts +6 -0
  48. package/dist/introspection/graph-adapters/dot.d.ts.map +1 -0
  49. package/dist/introspection/graph-adapters/dot.js +18 -0
  50. package/dist/introspection/graph-adapters/dot.js.map +1 -0
  51. package/dist/introspection/graph-adapters/reactflow.d.ts +38 -0
  52. package/dist/introspection/graph-adapters/reactflow.d.ts.map +1 -0
  53. package/dist/introspection/graph-adapters/reactflow.js +33 -0
  54. package/dist/introspection/graph-adapters/reactflow.js.map +1 -0
  55. package/dist/introspection/inspector.d.ts +39 -0
  56. package/dist/introspection/inspector.d.ts.map +1 -0
  57. package/dist/introspection/inspector.js +82 -0
  58. package/dist/introspection/inspector.js.map +1 -0
  59. package/dist/metadata/{metadata-keys.d.mts → metadata-keys.d.ts} +5 -7
  60. package/dist/metadata/metadata-keys.d.ts.map +1 -0
  61. package/dist/metadata/metadata-keys.js +25 -0
  62. package/dist/metadata/metadata-keys.js.map +1 -0
  63. package/dist/metadata/metadata-reader-token.d.ts +7 -0
  64. package/dist/metadata/metadata-reader-token.d.ts.map +1 -0
  65. package/dist/metadata/metadata-reader-token.js +6 -0
  66. package/dist/metadata/metadata-reader-token.js.map +1 -0
  67. package/dist/metadata/metadata-types.d.ts +48 -0
  68. package/dist/metadata/metadata-types.d.ts.map +1 -0
  69. package/dist/metadata/metadata-types.js +2 -0
  70. package/dist/metadata/metadata-types.js.map +1 -0
  71. package/dist/metadata/symbol-metadata-reader.d.ts +20 -0
  72. package/dist/metadata/symbol-metadata-reader.d.ts.map +1 -0
  73. package/dist/metadata/symbol-metadata-reader.js +34 -0
  74. package/dist/metadata/symbol-metadata-reader.js.map +1 -0
  75. package/dist/module.d.ts +67 -0
  76. package/dist/module.d.ts.map +1 -0
  77. package/dist/module.js +54 -0
  78. package/dist/module.js.map +1 -0
  79. package/dist/registry.d.ts +33 -0
  80. package/dist/registry.d.ts.map +1 -0
  81. package/dist/registry.js +249 -0
  82. package/dist/registry.js.map +1 -0
  83. package/dist/resolution/binding-scope.d.ts +10 -0
  84. package/dist/resolution/binding-scope.d.ts.map +1 -0
  85. package/dist/resolution/binding-scope.js +24 -0
  86. package/dist/resolution/binding-scope.js.map +1 -0
  87. package/dist/resolution/binding-select.d.ts +16 -0
  88. package/dist/resolution/binding-select.d.ts.map +1 -0
  89. package/dist/resolution/binding-select.js +143 -0
  90. package/dist/resolution/binding-select.js.map +1 -0
  91. package/dist/resolution/constraints.d.ts +51 -0
  92. package/dist/resolution/constraints.d.ts.map +1 -0
  93. package/dist/resolution/constraints.js +88 -0
  94. package/dist/resolution/constraints.js.map +1 -0
  95. package/dist/resolution/environment.d.ts +47 -0
  96. package/dist/resolution/environment.d.ts.map +1 -0
  97. package/dist/resolution/environment.js +100 -0
  98. package/dist/resolution/environment.js.map +1 -0
  99. package/dist/resolution/instantiation-plan.d.ts +67 -0
  100. package/dist/resolution/instantiation-plan.d.ts.map +1 -0
  101. package/dist/resolution/instantiation-plan.js +163 -0
  102. package/dist/resolution/instantiation-plan.js.map +1 -0
  103. package/dist/resolution/lifecycle.d.ts +21 -0
  104. package/dist/resolution/lifecycle.d.ts.map +1 -0
  105. package/dist/resolution/lifecycle.js +178 -0
  106. package/dist/resolution/lifecycle.js.map +1 -0
  107. package/dist/resolution/resolution-path.d.ts +31 -0
  108. package/dist/resolution/resolution-path.d.ts.map +1 -0
  109. package/dist/resolution/resolution-path.js +53 -0
  110. package/dist/resolution/resolution-path.js.map +1 -0
  111. package/dist/resolution/resolve-options.d.ts +19 -0
  112. package/dist/resolution/resolve-options.d.ts.map +1 -0
  113. package/dist/resolution/resolve-options.js +32 -0
  114. package/dist/resolution/resolve-options.js.map +1 -0
  115. package/dist/resolution/resolver.d.ts +35 -0
  116. package/dist/resolution/resolver.d.ts.map +1 -0
  117. package/dist/resolution/resolver.js +1320 -0
  118. package/dist/resolution/resolver.js.map +1 -0
  119. package/dist/resolution/scope.d.ts +32 -0
  120. package/dist/resolution/scope.d.ts.map +1 -0
  121. package/dist/resolution/scope.js +76 -0
  122. package/dist/resolution/scope.js.map +1 -0
  123. package/dist/token.d.ts +23 -0
  124. package/dist/token.d.ts.map +1 -0
  125. package/dist/token.js +22 -0
  126. package/dist/token.js.map +1 -0
  127. package/dist/types.d.ts +90 -0
  128. package/dist/types.d.ts.map +1 -0
  129. package/dist/types.js +2 -0
  130. package/dist/types.js.map +1 -0
  131. package/package.json +143 -120
  132. package/src/binding.ts +38 -22
  133. package/src/container/binding-builders.ts +397 -0
  134. package/src/container/container.ts +838 -0
  135. package/src/decorators/inject.ts +16 -3
  136. package/src/decorators/injectable.ts +3 -3
  137. package/src/index.ts +14 -7
  138. package/src/{dependency-graph.ts → introspection/dependency-graph.ts} +1 -1
  139. package/src/{graph-adapters → introspection/graph-adapters}/cytoscape.ts +1 -1
  140. package/src/{graph-adapters → introspection/graph-adapters}/dot.ts +1 -1
  141. package/src/{graph-adapters → introspection/graph-adapters}/reactflow.ts +13 -2
  142. package/src/{inspector.ts → introspection/inspector.ts} +26 -21
  143. package/src/metadata/symbol-metadata-reader.ts +4 -4
  144. package/src/module.ts +14 -6
  145. package/src/registry.ts +115 -61
  146. package/src/{binding-select.ts → resolution/binding-select.ts} +16 -3
  147. package/src/{environment.ts → resolution/environment.ts} +35 -35
  148. package/src/resolution/instantiation-plan.ts +240 -0
  149. package/src/{lifecycle.ts → resolution/lifecycle.ts} +20 -13
  150. package/src/resolution/resolution-path.ts +77 -0
  151. package/src/resolution/resolver.ts +1755 -0
  152. package/src/{scope.ts → resolution/scope.ts} +35 -18
  153. package/src/token.ts +0 -3
  154. package/dist/binding-scope.d.mts +0 -13
  155. package/dist/binding-scope.mjs +0 -21
  156. package/dist/binding-select.d.mts +0 -19
  157. package/dist/binding-select.mjs +0 -72
  158. package/dist/binding.d.mts +0 -191
  159. package/dist/binding.mjs +0 -36
  160. package/dist/constraints.d.mts +0 -55
  161. package/dist/constraints.mjs +0 -87
  162. package/dist/constructor-type.mjs +0 -1
  163. package/dist/container.d.mts +0 -62
  164. package/dist/container.mjs +0 -741
  165. package/dist/decorators/inject.d.mts +0 -49
  166. package/dist/decorators/inject.mjs +0 -136
  167. package/dist/decorators/injectable.d.mts +0 -32
  168. package/dist/decorators/injectable.mjs +0 -57
  169. package/dist/decorators/lifecycle-decorators.d.mts +0 -11
  170. package/dist/decorators/lifecycle-decorators.mjs +0 -38
  171. package/dist/dependency-graph.d.mts +0 -43
  172. package/dist/dependency-graph.mjs +0 -63
  173. package/dist/environment.d.mts +0 -55
  174. package/dist/environment.mjs +0 -98
  175. package/dist/errors.d.mts +0 -153
  176. package/dist/errors.mjs +0 -197
  177. package/dist/graph-adapters/cytoscape.d.mts +0 -36
  178. package/dist/graph-adapters/cytoscape.mjs +0 -26
  179. package/dist/graph-adapters/dot.d.mts +0 -9
  180. package/dist/graph-adapters/dot.mjs +0 -20
  181. package/dist/graph-adapters/reactflow.d.mts +0 -41
  182. package/dist/graph-adapters/reactflow.mjs +0 -29
  183. package/dist/index.d.mts +0 -18
  184. package/dist/index.mjs +0 -12
  185. package/dist/inspector.d.mts +0 -48
  186. package/dist/inspector.mjs +0 -71
  187. package/dist/lifecycle.d.mts +0 -25
  188. package/dist/lifecycle.mjs +0 -118
  189. package/dist/metadata/metadata-keys.mjs +0 -27
  190. package/dist/metadata/metadata-reader-token.d.mts +0 -10
  191. package/dist/metadata/metadata-reader-token.mjs +0 -8
  192. package/dist/metadata/metadata-types.d.mts +0 -51
  193. package/dist/metadata/metadata-types.mjs +0 -1
  194. package/dist/metadata/symbol-metadata-reader.d.mts +0 -23
  195. package/dist/metadata/symbol-metadata-reader.mjs +0 -29
  196. package/dist/module.d.mts +0 -62
  197. package/dist/module.mjs +0 -42
  198. package/dist/registry.d.mts +0 -45
  199. package/dist/registry.mjs +0 -163
  200. package/dist/resolve-options.d.mts +0 -22
  201. package/dist/resolve-options.mjs +0 -28
  202. package/dist/resolver.d.mts +0 -94
  203. package/dist/resolver.mjs +0 -767
  204. package/dist/scope.d.mts +0 -28
  205. package/dist/scope.mjs +0 -58
  206. package/dist/token.d.mts +0 -25
  207. package/dist/token.mjs +0 -22
  208. package/dist/types.d.mts +0 -92
  209. package/dist/types.mjs +0 -1
  210. package/src/container.ts +0 -1141
  211. package/src/resolver.ts +0 -1626
  212. /package/src/{binding-scope.ts → resolution/binding-scope.ts} +0 -0
  213. /package/src/{constraints.ts → resolution/constraints.ts} +0 -0
  214. /package/src/{resolve-options.ts → resolution/resolve-options.ts} +0 -0
@@ -0,0 +1,1755 @@
1
+ import type { Binding, BindingSlot } from "#/binding";
2
+ import type { ConstructorInvocation } from "#/constructor-type";
3
+ import type { Container } from "#/container/container";
4
+ import type { InjectionDescriptor } from "#/decorators/inject";
5
+ import {
6
+ AsyncActivationError,
7
+ AsyncResolutionError,
8
+ CircularDependencyError,
9
+ InternalError,
10
+ MissingMetadataError,
11
+ MissingScopeContextError,
12
+ NoMatchingBindingError,
13
+ TokenNotBoundError,
14
+ } from "#/errors";
15
+ import type { ConstructorMetadata, MetadataReader } from "#/metadata/metadata-types";
16
+ import type { BindingRegistry } from "#/registry";
17
+ import { selectAllBindings, selectBinding } from "#/resolution/binding-select";
18
+ import type { ResolverCallbacks } from "#/resolution/environment";
19
+ import { buildResolutionFrame, DefaultResolutionContext, runWithContainer } from "#/resolution/environment";
20
+ import { InstantiationPlanCompiler, PLAN_RETRY } from "#/resolution/instantiation-plan";
21
+ import type { LifecycleManager } from "#/resolution/lifecycle";
22
+ import { enterResolutionPath, exitResolutionPath } from "#/resolution/resolution-path";
23
+ import { injectionSlotToResolveOptions } from "#/resolution/resolve-options";
24
+ import type { ScopeManager } from "#/resolution/scope";
25
+ import { SINGLETON_MISS } from "#/resolution/scope";
26
+ import type { Token } from "#/token";
27
+ import { tokenName } from "#/token";
28
+ import type {
29
+ ActivationHandler,
30
+ BindingIdentifier,
31
+ BindingScope,
32
+ BindingTag,
33
+ ConstraintContext,
34
+ Constructor,
35
+ ResolutionFrame,
36
+ ResolveOptions,
37
+ } from "#/types";
38
+
39
+ // Terminal result of the options-less lookup fast lane — alias hops already folded.
40
+ interface DefaultLookupEntry {
41
+ readonly binding: Binding;
42
+ readonly owner: DependencyResolver;
43
+ }
44
+
45
+ // Fast-lane alias folding gives up past this many hops and defers to the resolve()
46
+ // loop, whose Set-based traversal detects genuine cycles exactly (no arbitrary cap).
47
+ const ALIAS_HOP_LIMIT = 32;
48
+
49
+ type BindingWithScope = Binding & { scope: BindingScope };
50
+ const EMPTY_STRING_LIST: ReadonlyArray<string> = [];
51
+ const EMPTY_FRAME_LIST: ReadonlyArray<ResolutionFrame> = [];
52
+ const ROOT_CONSTRAINT_CONTEXT = {
53
+ resolutionPath: EMPTY_STRING_LIST,
54
+ resolutionStack: EMPTY_FRAME_LIST,
55
+ parent: undefined,
56
+ ancestors: EMPTY_FRAME_LIST,
57
+ currentResolveOptions: undefined,
58
+ };
59
+
60
+ /**
61
+ * @since 0.3.16-canary.0
62
+ */
63
+ export class DependencyResolver {
64
+ readonly #syncResolutionContextPool: Array<DefaultResolutionContext> = [];
65
+ // Cycle detection for the sync transient-dynamic lane lives on `binding.inFlight` — see the
66
+ // field's doc comment in binding.ts. It is an O(1) field read with no hashing, no path scan and
67
+ // no side table to allocate or grow, so the lane needs no depth split.
68
+ // Shared-context state for the async transient-dynamic lane.
69
+ //
70
+ // Every level of a SEQUENTIAL async chain shares the same resolutionPath and resolutionStack
71
+ // arrays (passed by reference through ctx.resolveAsync), and the context stores references
72
+ // rather than snapshots, so one DefaultResolutionContext can serve the whole chain — the arrays
73
+ // reflect the current state automatically as levels push and pop.
74
+ //
75
+ // #asyncChainCtx: the shared context, created on first use and reset at each new root call.
76
+ // Inner levels of the same chain reuse it with zero setup.
77
+ // #asyncChainCtxPath: identity of the resolutionPath array owning the shared context — same
78
+ // reference means an inner level of that chain, a different one means a concurrent chain
79
+ // (e.g. Promise.all) which gets its own context instead.
80
+ // #asyncChainActiveLevels: active levels of the OWNING chain, so the path pointer is released
81
+ // when the last one settles. Concurrent fallback calls are not counted.
82
+ //
83
+ // The method is NOT declared async: that would allocate an async state machine and an implicit
84
+ // promise per level, where a `.then(settle, settle)` side listener costs neither.
85
+ #asyncChainCtx: DefaultResolutionContext | undefined;
86
+ #asyncChainCtxPath: Array<string> | undefined;
87
+ #asyncChainActiveLevels = 0;
88
+ // Settle callback shared by every level of the owning async chain: all of them pop the same
89
+ // resolutionPath and decrement the same counter, so one closure serves the whole chain instead
90
+ // of allocating one per level (the async lane's dominant per-level allocation).
91
+ #asyncChainSettle: (() => void) | undefined;
92
+ readonly #classHasPostConstruct = new WeakMap<Constructor, boolean>();
93
+ readonly #classNeedsActiveContainer = new WeakMap<Constructor, boolean>();
94
+ readonly #classConstructorMetadata = new WeakMap<Constructor, ConstructorMetadata | null>();
95
+ readonly #activationNeedByBindingId = new Map<BindingIdentifier, boolean>();
96
+ #activationCacheVersion = -1;
97
+ // Options-less lookup memo across the parent chain: token → terminal {binding, owner}
98
+ // (alias hops folded). `null` = token must take the slow lookup path. Invalidated when
99
+ // any registry in the chain mutates (monotonic version sum).
100
+ readonly #defaultLookupByToken = new Map<Token<unknown> | Constructor, DefaultLookupEntry | null>();
101
+ #defaultLookupVersion = -1;
102
+ // Name-only lookup memo (token → name → entry) with the same chain-version
103
+ // invalidation. `null` = shape needs the full selection path.
104
+ readonly #namedLookupByToken = new Map<Token<unknown> | Constructor, Map<string, DefaultLookupEntry | null>>();
105
+ #namedLookupVersion = -1;
106
+ // Compiled transient-class plans (Dagger-style): a pure-static subgraph (class/constant/
107
+ // cached-singleton deps only) compiles once into a nested-constructor closure — cycle
108
+ // checking happens at compile time, so execution skips all per-resolve bookkeeping.
109
+ // `null` = binding is not plannable under the current versions.
110
+ readonly #classPlanByBindingId = new Map<BindingIdentifier, (() => unknown) | null>();
111
+ #classPlanRegistryVersion = -1;
112
+ #classPlanActivationVersion = -1;
113
+
114
+ readonly #registry: BindingRegistry;
115
+ readonly #scope: ScopeManager;
116
+ readonly #lifecycle: LifecycleManager;
117
+ readonly #metadataReader: MetadataReader;
118
+ readonly #container: Container;
119
+ readonly #parent: DependencyResolver | undefined;
120
+
121
+ constructor(
122
+ registry: BindingRegistry,
123
+ scope: ScopeManager,
124
+ lifecycle: LifecycleManager,
125
+ metadataReader: MetadataReader,
126
+ container: Container,
127
+ parent: DependencyResolver | undefined,
128
+ ) {
129
+ this.#registry = registry;
130
+ this.#scope = scope;
131
+ this.#lifecycle = lifecycle;
132
+ this.#metadataReader = metadataReader;
133
+ this.#container = container;
134
+ this.#parent = parent;
135
+ }
136
+
137
+ // ── Binding lookup ─────────────────────────────────────────────────────────
138
+
139
+ #findBinding(
140
+ token: Token<unknown> | Constructor,
141
+ options: ResolveOptions | undefined,
142
+ resolutionPath: Array<string>,
143
+ resolutionStack: Array<ResolutionFrame>,
144
+ ): { binding: Binding; owner: DependencyResolver } | undefined {
145
+ if (options === undefined) {
146
+ const fastDefaultBinding = this.#registry.getFastDefault(token);
147
+ if (fastDefaultBinding !== undefined) {
148
+ return { binding: fastDefaultBinding, owner: this };
149
+ }
150
+ }
151
+
152
+ if (options?.name !== undefined && options.tag === undefined && (options.tags?.length ?? 0) === 0) {
153
+ const namedBinding = this.#registry.getSimpleNamed(token, options.name);
154
+ if (
155
+ namedBinding !== undefined &&
156
+ this.#matchesBindingFast(namedBinding, options, resolutionPath, resolutionStack)
157
+ ) {
158
+ return { binding: namedBinding, owner: this };
159
+ }
160
+ }
161
+
162
+ if (
163
+ options !== undefined &&
164
+ options.name === undefined &&
165
+ options.tag === undefined &&
166
+ (options.tags?.length ?? 0) === 1
167
+ ) {
168
+ const [tagKey, tagValue] = options.tags![0]!;
169
+ const tagged = this.#registry.getSimpleTagged(token, tagKey, tagValue);
170
+ if (tagged !== undefined) {
171
+ return { binding: tagged, owner: this };
172
+ }
173
+ }
174
+
175
+ const bindings = this.#registry.getAll(token);
176
+ if (bindings.length > 0) {
177
+ if (bindings.length === 1) {
178
+ const onlyBinding = bindings[0]!;
179
+ const isDefaultSlot = onlyBinding.slot.name === undefined && onlyBinding.slot.tags.length === 0;
180
+ if (options === undefined && isDefaultSlot && onlyBinding.predicate === undefined) {
181
+ return { binding: onlyBinding, owner: this };
182
+ }
183
+ if (this.#matchesBindingFast(onlyBinding, options, resolutionPath, resolutionStack)) {
184
+ return { binding: onlyBinding, owner: this };
185
+ }
186
+ }
187
+ const ctx = this.#makeConstraintContext(resolutionPath, resolutionStack, options);
188
+ const binding = selectBinding(bindings, options, ctx, this.#getTokenName(token));
189
+ if (binding !== undefined) {
190
+ return { binding, owner: this };
191
+ }
192
+ }
193
+ if (this.#parent !== undefined) {
194
+ return this.#parent.#findBinding(token, options, resolutionPath, resolutionStack);
195
+ }
196
+ return undefined;
197
+ }
198
+
199
+ /**
200
+ * Binding lookup aligned with `resolve` — used by `Container.validate` without instantiating.
201
+ */
202
+ peekBindingForValidate(
203
+ token: Token<unknown> | Constructor,
204
+ options: ResolveOptions | undefined,
205
+ ): { binding: Binding; owner: DependencyResolver } | undefined {
206
+ return this.#findBinding(token, options, [], []);
207
+ }
208
+
209
+ /**
210
+ * Mirrors {@link DependencyResolver.resolveAll} candidate selection only (no instantiation).
211
+ */
212
+ peekCandidateBindingsForValidate(
213
+ token: Token<unknown> | Constructor,
214
+ options: ResolveOptions | undefined,
215
+ ): Array<Binding> {
216
+ if (options?.name !== undefined && options.tag === undefined && (options.tags?.length ?? 0) === 0) {
217
+ return this.#getSimpleNamedBindingsFromChain(token, options.name);
218
+ }
219
+ const allBindings = this.#getAllBindingsFromChain(token);
220
+ if (allBindings.length === 0) {
221
+ return [];
222
+ }
223
+ const ctx = this.#makeConstraintContext([], [], options);
224
+ return selectAllBindings(allBindings, options, ctx);
225
+ }
226
+
227
+ // ── Sync resolve ───────────────────────────────────────────────────────────
228
+
229
+ resolveFromContext<const Value>(
230
+ token: Token<Value> | Constructor<Value>,
231
+ resolutionPath: Array<string>,
232
+ resolutionStack: Array<ResolutionFrame>,
233
+ ): Value {
234
+ // Hot lane: own-registry fast default. Fall back to the chain-versioned memo
235
+ // (parent-chain walk + alias folding) only on miss or alias.
236
+ const fastBinding = this.#registry.getFastDefault(token);
237
+ if (fastBinding !== undefined && fastBinding.kind !== "alias") {
238
+ return this.#resolveDefaultEntry<Value>(fastBinding, this, resolutionPath, resolutionStack);
239
+ }
240
+ const entry = this.#lookupDefaultEntry(token);
241
+ if (entry === null) {
242
+ return this.resolve(token, undefined, resolutionPath, resolutionStack);
243
+ }
244
+ return this.#resolveDefaultEntry<Value>(entry.binding, entry.owner, resolutionPath, resolutionStack);
245
+ }
246
+
247
+ #resolveDefaultEntry<const Value>(
248
+ binding: Binding,
249
+ owner: DependencyResolver,
250
+ resolutionPath: Array<string>,
251
+ resolutionStack: Array<ResolutionFrame>,
252
+ ): Value {
253
+ if (
254
+ binding.kind === "constant" &&
255
+ binding.onActivation === undefined &&
256
+ (this.#lifecycle.activationVersion === 0 || !this.#lifecycle.hasActivationHandlers(binding.token))
257
+ ) {
258
+ return binding.value as Value;
259
+ }
260
+ const scope = (binding as BindingWithScope).scope ?? "transient";
261
+ if (scope === "transient") {
262
+ if (binding.kind === "dynamic") {
263
+ const containerHooks =
264
+ this.#lifecycle.activationVersion === 0 ? undefined : this.#lifecycle.activationHandlersFor(binding.token);
265
+ if (binding.onActivation === undefined && (containerHooks === undefined || containerHooks.length === 0)) {
266
+ return this.#resolveTransientDynamicSyncFromContext(
267
+ binding as Binding<Value> & { kind: "dynamic" },
268
+ resolutionPath,
269
+ resolutionStack,
270
+ );
271
+ }
272
+ return this.#resolveTransientDynamicActivatedSync(
273
+ binding as Binding<Value> & { kind: "dynamic" },
274
+ containerHooks,
275
+ resolutionPath,
276
+ resolutionStack,
277
+ );
278
+ }
279
+ // Compiled plans only run at the top level — inner levels keep the runtime cycle guard.
280
+ if ((binding.kind === "class" || binding.kind === "resolved") && resolutionPath.length === 0) {
281
+ const plan = this.#getInstantiationPlan(binding);
282
+ if (plan !== null) {
283
+ return plan() as Value;
284
+ }
285
+ }
286
+ } else if (scope === "singleton") {
287
+ const cachedSingleton = owner.#scope.peekSingleton(binding.id);
288
+ if (cachedSingleton !== SINGLETON_MISS) {
289
+ return cachedSingleton as Value;
290
+ }
291
+ if (owner !== this) {
292
+ return owner.#resolveBinding(binding as Binding<Value>, undefined, resolutionPath, resolutionStack);
293
+ }
294
+ } else {
295
+ if (!this.#scope.isChild) {
296
+ throw new MissingScopeContextError(this.#getTokenName(binding.token));
297
+ }
298
+ if (this.#scope.hasScoped(binding.id)) {
299
+ return this.#scope.getScoped<Value>(binding.id);
300
+ }
301
+ }
302
+ return this.#resolveBinding(binding as Binding<Value>, undefined, resolutionPath, resolutionStack);
303
+ }
304
+
305
+ // Lean lane for an activated transient dynamic binding: same observable behavior as the
306
+ // generic #resolveBinding path (guard, frame, ctx, per-binding then container hooks) with
307
+ // the kind/activation dispatch resolved statically.
308
+ #resolveTransientDynamicActivatedSync<const Value>(
309
+ binding: Binding<Value> & { kind: "dynamic" },
310
+ containerHooks: ReadonlyArray<ActivationHandler<unknown>> | undefined,
311
+ resolutionPath: Array<string>,
312
+ resolutionStack: Array<ResolutionFrame>,
313
+ ): Value {
314
+ const frame = this.#getResolutionFrame(binding);
315
+ const tokenDisplayName = frame.tokenName;
316
+ const resolutionSet = enterResolutionPath(resolutionPath, tokenDisplayName, false);
317
+ resolutionStack.push(frame);
318
+ try {
319
+ const resolutionCtx = this.#acquireSyncResolutionContext(resolutionPath, resolutionStack, undefined);
320
+ const factoryResult = binding.factory(resolutionCtx);
321
+ if (factoryResult instanceof Promise) {
322
+ throw new AsyncResolutionError(tokenDisplayName, tokenDisplayName);
323
+ }
324
+ let activated = factoryResult;
325
+ if (binding.onActivation !== undefined) {
326
+ const activationResult = binding.onActivation(resolutionCtx, activated);
327
+ if (activationResult instanceof Promise) {
328
+ throw new AsyncActivationError(tokenDisplayName, "onActivation");
329
+ }
330
+ activated = activationResult;
331
+ }
332
+ if (containerHooks !== undefined) {
333
+ for (let index = 0; index < containerHooks.length; index += 1) {
334
+ const activationResult = containerHooks[index]!(resolutionCtx, activated);
335
+ if (activationResult instanceof Promise) {
336
+ throw new AsyncActivationError(tokenDisplayName, "onActivation");
337
+ }
338
+ activated = activationResult as Value;
339
+ }
340
+ }
341
+ return activated;
342
+ } finally {
343
+ resolutionStack.pop();
344
+ resolutionPath.pop();
345
+ resolutionSet?.delete(tokenDisplayName);
346
+ }
347
+ }
348
+
349
+ #chainRegistryVersion(): number {
350
+ let version = this.#registry.version;
351
+ for (let resolver = this.#parent; resolver !== undefined; resolver = resolver.#parent) {
352
+ version += resolver.#registry.version;
353
+ }
354
+ return version;
355
+ }
356
+
357
+ #lookupDefaultEntry(token: Token<unknown> | Constructor): DefaultLookupEntry | null {
358
+ const version = this.#chainRegistryVersion();
359
+ if (version !== this.#defaultLookupVersion) {
360
+ this.#defaultLookupByToken.clear();
361
+ this.#defaultLookupVersion = version;
362
+ }
363
+ let entry = this.#defaultLookupByToken.get(token);
364
+ if (entry === undefined) {
365
+ entry = this.#computeDefaultEntry(token);
366
+ this.#defaultLookupByToken.set(token, entry);
367
+ }
368
+ return entry;
369
+ }
370
+
371
+ #computeDefaultEntry(token: Token<unknown> | Constructor): DefaultLookupEntry | null {
372
+ let current = token;
373
+ for (let hop = 0; hop < ALIAS_HOP_LIMIT; hop += 1) {
374
+ const entry = this.#findDefaultEntryInChain(current);
375
+ if (entry === null) {
376
+ return null;
377
+ }
378
+ if (entry.binding.kind === "alias") {
379
+ current = entry.binding.target;
380
+ continue;
381
+ }
382
+ return entry;
383
+ }
384
+ return null;
385
+ }
386
+
387
+ #lookupNamedEntry(token: Token<unknown> | Constructor, name: string): DefaultLookupEntry | null {
388
+ const version = this.#chainRegistryVersion();
389
+ if (version !== this.#namedLookupVersion) {
390
+ this.#namedLookupByToken.clear();
391
+ this.#namedLookupVersion = version;
392
+ }
393
+ // ✓ TS6.0: Map.getOrInsert (ES2025)
394
+ const entriesByName = this.#namedLookupByToken.getOrInsert(token, new Map<string, DefaultLookupEntry | null>());
395
+ let entry = entriesByName.get(name);
396
+ if (entry === undefined) {
397
+ entry = this.#findNamedEntryInChain(token, name);
398
+ entriesByName.set(name, entry);
399
+ }
400
+ return entry;
401
+ }
402
+
403
+ #findNamedEntryInChain(token: Token<unknown> | Constructor, name: string): DefaultLookupEntry | null {
404
+ const named = this.#registry.getSimpleNamed(token, name);
405
+ if (named !== undefined) {
406
+ // Predicates need a live context; aliases carry options through the full path.
407
+ if (named.predicate !== undefined || named.kind === "alias") {
408
+ return null;
409
+ }
410
+ return { binding: named, owner: this };
411
+ }
412
+ if (this.#registry.has(token)) {
413
+ return null;
414
+ }
415
+ return this.#parent === undefined ? null : this.#parent.#findNamedEntryInChain(token, name);
416
+ }
417
+
418
+ #findDefaultEntryInChain(token: Token<unknown> | Constructor): DefaultLookupEntry | null {
419
+ const fast = this.#registry.getFastDefault(token);
420
+ if (fast !== undefined) {
421
+ return { binding: fast, owner: this };
422
+ }
423
+ // A level with non-fast bindings (multi-slot / predicate) needs full selection — bail.
424
+ if (this.#registry.has(token)) {
425
+ return null;
426
+ }
427
+ return this.#parent === undefined ? null : this.#parent.#findDefaultEntryInChain(token);
428
+ }
429
+
430
+ #getInstantiationPlan(binding: Binding & { kind: "class" | "resolved" }): (() => unknown) | null {
431
+ const registryVersion = this.#chainRegistryVersion();
432
+ const activationVersion = this.#lifecycle.activationVersion;
433
+ if (registryVersion !== this.#classPlanRegistryVersion || activationVersion !== this.#classPlanActivationVersion) {
434
+ this.#classPlanByBindingId.clear();
435
+ this.#classPlanRegistryVersion = registryVersion;
436
+ this.#classPlanActivationVersion = activationVersion;
437
+ }
438
+ const cached = this.#classPlanByBindingId.get(binding.id);
439
+ if (cached !== undefined) {
440
+ return cached;
441
+ }
442
+ const compiled = this.#planCompiler.compile(binding);
443
+ if (compiled === PLAN_RETRY) {
444
+ // Lifecycle metadata not discovered yet — the fallback resolve discovers it; retry then.
445
+ return null;
446
+ }
447
+ this.#classPlanByBindingId.set(binding.id, compiled);
448
+ return compiled;
449
+ }
450
+
451
+ // Compiler behind #getClassPlan — cold path, so the host indirection costs nothing hot.
452
+ readonly #planCompiler = new InstantiationPlanCompiler({
453
+ hasActivationHandlers: (token) => this.#lifecycle.hasActivationHandlers(token),
454
+ knownPostConstruct: (target) => this.#classHasPostConstruct.get(target),
455
+ needsActiveContainer: (target) => {
456
+ let needsActiveContainer = this.#classNeedsActiveContainer.get(target);
457
+ if (needsActiveContainer === undefined) {
458
+ const accessorMetadata = this.#metadataReader.getAccessorMetadata?.(target);
459
+ needsActiveContainer = (accessorMetadata?.length ?? 0) > 0;
460
+ this.#classNeedsActiveContainer.set(target, needsActiveContainer);
461
+ }
462
+ return needsActiveContainer;
463
+ },
464
+ getConstructorMetadata: (target) => this.#getConstructorMetadata(target),
465
+ lookupDependencyEntry: (token) => {
466
+ const entry = this.#lookupDefaultEntry(token);
467
+ return entry === null ? null : { binding: entry.binding, ownerScope: entry.owner.#scope };
468
+ },
469
+ resolveFallback: (token) => this.resolve(token, undefined, [], []),
470
+ });
471
+
472
+ resolve<const Value>(
473
+ token: Token<Value> | Constructor<Value>,
474
+ options: ResolveOptions | undefined,
475
+ resolutionPath: Array<string>,
476
+ resolutionStack: Array<ResolutionFrame>,
477
+ ): Value {
478
+ // Name-only fast lane: memoized lookup, dispatching just the shapes whose
479
+ // semantics involve no resolution context (constants, cached singletons).
480
+ if (
481
+ options !== undefined &&
482
+ options.name !== undefined &&
483
+ options.tag === undefined &&
484
+ (options.tags === undefined || options.tags.length === 0)
485
+ ) {
486
+ const namedEntry = this.#lookupNamedEntry(token, options.name);
487
+ if (namedEntry !== null) {
488
+ const namedBinding = namedEntry.binding;
489
+ if (
490
+ namedBinding.kind === "constant" &&
491
+ namedBinding.onActivation === undefined &&
492
+ (this.#lifecycle.activationVersion === 0 || !this.#lifecycle.hasActivationHandlers(namedBinding.token))
493
+ ) {
494
+ return namedBinding.value as Value;
495
+ }
496
+ const namedScope = (namedBinding as BindingWithScope).scope ?? "transient";
497
+ if (namedScope === "singleton") {
498
+ const cachedSingleton = namedEntry.owner.#scope.peekSingleton(namedBinding.id);
499
+ if (cachedSingleton !== SINGLETON_MISS) {
500
+ return cachedSingleton as Value;
501
+ }
502
+ }
503
+ // Everything else keeps the full path (context, activation, guards).
504
+ }
505
+ }
506
+
507
+ let currentToken: Token<unknown> | Constructor = token;
508
+ let visitedAliasTokens: Set<Token<unknown> | Constructor> | undefined;
509
+ let found = this.#findBinding(currentToken, options, resolutionPath, resolutionStack);
510
+
511
+ // Follow aliases iteratively with exact cycle detection — a revisited alias
512
+ // token throws CircularDependencyError instead of overflowing the call stack.
513
+ while (found !== undefined && found.binding.kind === "alias") {
514
+ const target = found.binding.target;
515
+ visitedAliasTokens ??= new Set([currentToken]);
516
+ if (visitedAliasTokens.has(target)) {
517
+ throw new CircularDependencyError([...visitedAliasTokens, target].map((entry) => tokenName(entry)));
518
+ }
519
+ visitedAliasTokens.add(target);
520
+ currentToken = target;
521
+ found = this.#findBinding(currentToken, options, resolutionPath, resolutionStack);
522
+ }
523
+
524
+ if (found === undefined) {
525
+ const ownBindings = this.#registry.getAll(currentToken);
526
+ if (ownBindings.length > 0) {
527
+ throw new NoMatchingBindingError(
528
+ this.#getTokenName(currentToken),
529
+ options ?? {},
530
+ this.#getAvailableSlots(currentToken),
531
+ );
532
+ }
533
+ throw new TokenNotBoundError(this.#getTokenName(currentToken));
534
+ }
535
+
536
+ const { binding, owner } = found;
537
+
538
+ const scope = (binding as BindingWithScope).scope ?? "transient";
539
+
540
+ // Singleton from a parent resolver: delegate so the parent caches it correctly
541
+ if (scope === "singleton" && owner !== this) {
542
+ return owner.#resolveBinding(binding as Binding<Value>, options, resolutionPath, resolutionStack);
543
+ }
544
+
545
+ // Scoped/transient (or own singleton): resolve with this resolver's container/scope
546
+ return this.#resolveBinding(binding as Binding<Value>, options, resolutionPath, resolutionStack);
547
+ }
548
+
549
+ #resolveBinding<const Value>(
550
+ binding: Binding<Value>,
551
+ options: ResolveOptions | undefined,
552
+ resolutionPath: Array<string>,
553
+ resolutionStack: Array<ResolutionFrame>,
554
+ ): Value {
555
+ if (
556
+ binding.kind === "constant" &&
557
+ binding.onActivation === undefined &&
558
+ (this.#lifecycle.activationVersion === 0 || !this.#lifecycle.hasActivationHandlers(binding.token))
559
+ ) {
560
+ return binding.value;
561
+ }
562
+
563
+ const scope = (binding as BindingWithScope).scope ?? "transient";
564
+
565
+ // Singleton cache check
566
+ if (scope === "singleton") {
567
+ if (this.#scope.hasSingleton(binding.id)) {
568
+ return this.#scope.getSingleton<Value>(binding.id);
569
+ }
570
+ }
571
+
572
+ // Scoped cache check
573
+ if (scope === "scoped") {
574
+ if (!this.#scope.isChild) {
575
+ throw new MissingScopeContextError(this.#getTokenName(binding.token));
576
+ }
577
+ if (this.#scope.hasScoped(binding.id)) {
578
+ return this.#scope.getScoped<Value>(binding.id);
579
+ }
580
+ }
581
+
582
+ const frame = this.#getResolutionFrame(binding);
583
+ const tokenDisplayName = frame.tokenName;
584
+ const resolutionSet = enterResolutionPath(resolutionPath, tokenDisplayName, false);
585
+ resolutionStack.push(frame);
586
+ const needsActivation = this.#needsActivation(binding);
587
+ if (!needsActivation && scope === "transient" && binding.kind === "dynamic") {
588
+ const resolutionCtx = this.#acquireSyncResolutionContext(resolutionPath, resolutionStack, options);
589
+ try {
590
+ const dynamicResult = binding.factory(resolutionCtx);
591
+ if (dynamicResult instanceof Promise) {
592
+ throw new AsyncResolutionError(tokenName(binding.token), tokenName(binding.token));
593
+ }
594
+ resolutionStack.pop();
595
+ resolutionPath.pop();
596
+ resolutionSet?.delete(tokenDisplayName);
597
+ return dynamicResult;
598
+ } catch (error) {
599
+ resolutionStack.pop();
600
+ resolutionPath.pop();
601
+ resolutionSet?.delete(tokenDisplayName);
602
+ throw error;
603
+ }
604
+ }
605
+
606
+ try {
607
+ const needsResolutionContext = needsActivation || this.#requiresResolutionContext(binding);
608
+ const resolutionCtx = needsResolutionContext
609
+ ? this.#acquireSyncResolutionContext(resolutionPath, resolutionStack, options)
610
+ : undefined;
611
+
612
+ const instance = this.#instantiateSync(binding, resolutionCtx, resolutionPath, resolutionStack);
613
+
614
+ const shouldActivate = this.#refreshActivationCacheIfNeeded(binding, needsActivation);
615
+ const activated = shouldActivate
616
+ ? this.#lifecycle.runActivationSync(
617
+ resolutionCtx as DefaultResolutionContext,
618
+ binding,
619
+ instance,
620
+ this.#metadataReader,
621
+ )
622
+ : instance;
623
+
624
+ // Cache by scope
625
+ if (scope === "singleton") {
626
+ this.#scope.setSingleton(binding.id, activated);
627
+ } else if (scope === "scoped") {
628
+ this.#scope.setScoped(binding.id, activated);
629
+ }
630
+
631
+ return activated;
632
+ } finally {
633
+ resolutionStack.pop();
634
+ resolutionPath.pop();
635
+ resolutionSet?.delete(tokenDisplayName);
636
+ }
637
+ }
638
+
639
+ #instantiateSync<const Value>(
640
+ binding: Binding<Value>,
641
+ ctx: DefaultResolutionContext | undefined,
642
+ resolutionPath: Array<string>,
643
+ resolutionStack: Array<ResolutionFrame>,
644
+ ): Value {
645
+ switch (binding.kind) {
646
+ case "constant":
647
+ return binding.value;
648
+
649
+ case "dynamic": {
650
+ if (ctx === undefined) {
651
+ throw new InternalError("dynamic binding requires resolution context");
652
+ }
653
+ const factoryResult = binding.factory(ctx);
654
+ if (factoryResult instanceof Promise) {
655
+ throw new AsyncResolutionError(tokenName(binding.token), tokenName(binding.token));
656
+ }
657
+ return factoryResult;
658
+ }
659
+
660
+ case "dynamic-async":
661
+ throw new AsyncResolutionError(tokenName(binding.token), tokenName(binding.token));
662
+
663
+ case "class": {
664
+ const deps = this.#resolveClassDeps(binding.target, resolutionPath, resolutionStack);
665
+ const instance = this.#instantiateClass(binding.target, deps);
666
+ return instance as Value;
667
+ }
668
+
669
+ case "resolved": {
670
+ const deps = this.#resolveDescriptorDeps(binding.deps, resolutionPath, resolutionStack);
671
+ const factoryResult = binding.factory(...deps);
672
+ if (factoryResult instanceof Promise) {
673
+ throw new AsyncResolutionError(tokenName(binding.token), tokenName(binding.token));
674
+ }
675
+ return factoryResult;
676
+ }
677
+
678
+ case "resolved-async":
679
+ throw new AsyncResolutionError(tokenName(binding.token), tokenName(binding.token));
680
+
681
+ case "alias":
682
+ throw new InternalError("alias should have been followed before instantiation");
683
+ }
684
+ }
685
+
686
+ #resolveClassDeps(
687
+ target: Constructor,
688
+ resolutionPath: Array<string>,
689
+ resolutionStack: Array<ResolutionFrame>,
690
+ ): Array<unknown> {
691
+ const meta = this.#getConstructorMetadata(target);
692
+ if (meta === undefined) {
693
+ if (target.length === 0) {
694
+ return [];
695
+ }
696
+ throw new MissingMetadataError(target.name);
697
+ }
698
+ if (meta.params.length === 0) {
699
+ return [];
700
+ }
701
+ if (meta.params.length === 1) {
702
+ const param = meta.params[0]!;
703
+ const paramOptions = injectionSlotToResolveOptions(param);
704
+ if (param.multi) {
705
+ return [this.resolveAll(param.token, paramOptions, resolutionPath, resolutionStack)];
706
+ }
707
+ if (param.optional) {
708
+ return [this.resolveOptional(param.token, paramOptions, resolutionPath, resolutionStack)];
709
+ }
710
+ if (paramOptions === undefined) {
711
+ return [this.resolveFromContext(param.token, resolutionPath, resolutionStack)];
712
+ }
713
+ return [this.resolve(param.token, paramOptions, resolutionPath, resolutionStack)];
714
+ }
715
+ const deps = new Array<unknown>(meta.params.length);
716
+ for (let index = 0; index < meta.params.length; index += 1) {
717
+ const param = meta.params[index]!;
718
+ const paramOptions = injectionSlotToResolveOptions(param);
719
+ if (param.multi) {
720
+ deps[index] = this.resolveAll(param.token, paramOptions, resolutionPath, resolutionStack);
721
+ continue;
722
+ }
723
+ if (param.optional) {
724
+ deps[index] = this.resolveOptional(param.token, paramOptions, resolutionPath, resolutionStack);
725
+ continue;
726
+ }
727
+ deps[index] =
728
+ paramOptions === undefined
729
+ ? this.resolveFromContext(param.token, resolutionPath, resolutionStack)
730
+ : this.resolve(param.token, paramOptions, resolutionPath, resolutionStack);
731
+ }
732
+ return deps;
733
+ }
734
+
735
+ #resolveDescriptorDeps(
736
+ deps: ReadonlyArray<InjectionDescriptor>,
737
+ resolutionPath: Array<string>,
738
+ resolutionStack: Array<ResolutionFrame>,
739
+ ): Array<unknown> {
740
+ const resolved = new Array<unknown>(deps.length);
741
+ for (let index = 0; index < deps.length; index += 1) {
742
+ const dep = deps[index]!;
743
+ const depOptions = injectionSlotToResolveOptions(dep);
744
+ if (dep.multi) {
745
+ resolved[index] = this.resolveAll(
746
+ dep.token as Token<unknown> | Constructor,
747
+ depOptions,
748
+ resolutionPath,
749
+ resolutionStack,
750
+ );
751
+ continue;
752
+ }
753
+ if (dep.optional) {
754
+ resolved[index] = this.resolveOptional(
755
+ dep.token as Token<unknown> | Constructor,
756
+ depOptions,
757
+ resolutionPath,
758
+ resolutionStack,
759
+ );
760
+ continue;
761
+ }
762
+ resolved[index] =
763
+ depOptions === undefined
764
+ ? this.resolveFromContext(dep.token as Token<unknown> | Constructor, resolutionPath, resolutionStack)
765
+ : this.resolve(dep.token as Token<unknown> | Constructor, depOptions, resolutionPath, resolutionStack);
766
+ }
767
+ return resolved;
768
+ }
769
+
770
+ resolveOptional<const Value>(
771
+ token: Token<Value> | Constructor<Value>,
772
+ options: ResolveOptions | undefined,
773
+ resolutionPath: Array<string>,
774
+ resolutionStack: Array<ResolutionFrame>,
775
+ ): Value | undefined {
776
+ if (this.#findBinding(token, options, resolutionPath, resolutionStack) === undefined) {
777
+ return undefined;
778
+ }
779
+ return this.resolve(token, options, resolutionPath, resolutionStack);
780
+ }
781
+
782
+ resolveAll<const Value>(
783
+ token: Token<Value> | Constructor<Value>,
784
+ options: ResolveOptions | undefined,
785
+ resolutionPath: Array<string>,
786
+ resolutionStack: Array<ResolutionFrame>,
787
+ ): Array<Value> {
788
+ if (options?.name !== undefined && options.tag === undefined && (options.tags?.length ?? 0) === 0) {
789
+ const namedCandidates = this.#getSimpleNamedBindingsFromChain(token, options.name);
790
+ if (namedCandidates.length === 0) {
791
+ return [];
792
+ }
793
+ const resolved = new Array<Value>(namedCandidates.length);
794
+ for (let index = 0; index < namedCandidates.length; index += 1) {
795
+ resolved[index] = this.#resolveCandidateSync(
796
+ namedCandidates[index] as Binding<Value>,
797
+ options,
798
+ resolutionPath,
799
+ resolutionStack,
800
+ );
801
+ }
802
+ return resolved;
803
+ }
804
+
805
+ const allBindings = this.#getAllBindingsFromChain(token);
806
+ if (allBindings.length === 0) {
807
+ return [];
808
+ }
809
+
810
+ const ctx = this.#makeConstraintContext(resolutionPath, resolutionStack, options);
811
+ const candidates = selectAllBindings(allBindings, options, ctx);
812
+
813
+ const resolved = new Array<Value>(candidates.length);
814
+ for (let index = 0; index < candidates.length; index += 1) {
815
+ resolved[index] = this.#resolveCandidateSync(
816
+ candidates[index] as Binding<Value>,
817
+ options,
818
+ resolutionPath,
819
+ resolutionStack,
820
+ );
821
+ }
822
+ return resolved;
823
+ }
824
+
825
+ // ── Async resolve ──────────────────────────────────────────────────────────
826
+
827
+ resolveAsyncFromContext<const Value>(
828
+ token: Token<Value> | Constructor<Value>,
829
+ resolutionPath: Array<string>,
830
+ resolutionStack: Array<ResolutionFrame>,
831
+ ): Promise<Value> {
832
+ // Hot lane: own-registry fast default (async chains resolve sibling dynamic bindings).
833
+ // Fall back to the chain-versioned memo only on miss or alias.
834
+ const fastBinding = this.#registry.getFastDefault(token);
835
+ if (fastBinding !== undefined && fastBinding.kind !== "alias") {
836
+ // Inline the dominant chain shape — transient dynamic factory with no activation.
837
+ if (
838
+ (fastBinding.kind === "dynamic-async" || fastBinding.kind === "dynamic") &&
839
+ fastBinding.scope === "transient" &&
840
+ fastBinding.onActivation === undefined &&
841
+ (this.#lifecycle.activationVersion === 0 || !this.#lifecycle.hasActivationHandlers(fastBinding.token))
842
+ ) {
843
+ return this.#resolveTransientDynamicAsyncFromContext(
844
+ fastBinding as Binding<Value> & { kind: "dynamic" | "dynamic-async" },
845
+ resolutionPath,
846
+ resolutionStack,
847
+ );
848
+ }
849
+ return this.#resolveAsyncDefaultEntry<Value>(fastBinding, this, resolutionPath, resolutionStack);
850
+ }
851
+ const entry = this.#lookupDefaultEntry(token);
852
+ if (entry === null) {
853
+ return this.resolveAsync(token, undefined, resolutionPath, resolutionStack);
854
+ }
855
+ return this.#resolveAsyncDefaultEntry<Value>(entry.binding, entry.owner, resolutionPath, resolutionStack);
856
+ }
857
+
858
+ #resolveAsyncDefaultEntry<const Value>(
859
+ binding: Binding,
860
+ owner: DependencyResolver,
861
+ resolutionPath: Array<string>,
862
+ resolutionStack: Array<ResolutionFrame>,
863
+ ): Promise<Value> {
864
+ if (
865
+ binding.kind === "constant" &&
866
+ binding.onActivation === undefined &&
867
+ (this.#lifecycle.activationVersion === 0 || !this.#lifecycle.hasActivationHandlers(binding.token))
868
+ ) {
869
+ return Promise.resolve(binding.value as Value);
870
+ }
871
+ const scope = (binding as BindingWithScope).scope ?? "transient";
872
+ if (scope === "transient") {
873
+ if (
874
+ (binding.kind === "dynamic" || binding.kind === "dynamic-async") &&
875
+ binding.onActivation === undefined &&
876
+ (this.#lifecycle.activationVersion === 0 || !this.#lifecycle.hasActivationHandlers(binding.token))
877
+ ) {
878
+ return this.#resolveTransientDynamicAsyncFromContext(
879
+ binding as Binding<Value> & { kind: "dynamic" | "dynamic-async" },
880
+ resolutionPath,
881
+ resolutionStack,
882
+ );
883
+ }
884
+ } else if (scope === "singleton") {
885
+ const cachedSingleton = owner.#scope.peekSingleton(binding.id);
886
+ if (cachedSingleton !== SINGLETON_MISS) {
887
+ return Promise.resolve(cachedSingleton as Value);
888
+ }
889
+ if (owner !== this) {
890
+ return owner.#resolveBindingAsync(binding as Binding<Value>, undefined, resolutionPath, resolutionStack);
891
+ }
892
+ } else {
893
+ if (!this.#scope.isChild) {
894
+ return Promise.reject(new MissingScopeContextError(this.#getTokenName(binding.token)));
895
+ }
896
+ if (this.#scope.hasScoped(binding.id)) {
897
+ return Promise.resolve(this.#scope.getScoped<Value>(binding.id));
898
+ }
899
+ }
900
+ return this.#resolveBindingAsync(binding as Binding<Value>, undefined, resolutionPath, resolutionStack);
901
+ }
902
+
903
+ async resolveAsync<const Value>(
904
+ token: Token<Value> | Constructor<Value>,
905
+ options: ResolveOptions | undefined,
906
+ resolutionPath: Array<string>,
907
+ resolutionStack: Array<ResolutionFrame>,
908
+ ): Promise<Value> {
909
+ const found = this.#findBinding(token, options, resolutionPath, resolutionStack);
910
+
911
+ if (found === undefined) {
912
+ const ownBindings = this.#registry.getAll(token);
913
+ if (ownBindings.length > 0) {
914
+ throw new NoMatchingBindingError(this.#getTokenName(token), options ?? {}, this.#getAvailableSlots(token));
915
+ }
916
+ throw new TokenNotBoundError(this.#getTokenName(token));
917
+ }
918
+
919
+ let currentToken: Token<unknown> | Constructor = token;
920
+ let visitedAliasTokens: Set<Token<unknown> | Constructor> | undefined;
921
+ let aliasFollowed: DefaultLookupEntry | undefined = found;
922
+ while (aliasFollowed !== undefined && aliasFollowed.binding.kind === "alias") {
923
+ const target = aliasFollowed.binding.target;
924
+ visitedAliasTokens ??= new Set([currentToken]);
925
+ if (visitedAliasTokens.has(target)) {
926
+ throw new CircularDependencyError([...visitedAliasTokens, target].map((entry) => tokenName(entry)));
927
+ }
928
+ visitedAliasTokens.add(target);
929
+ currentToken = target;
930
+ aliasFollowed = this.#findBinding(currentToken, options, resolutionPath, resolutionStack);
931
+ }
932
+ if (aliasFollowed === undefined) {
933
+ const ownBindings = this.#registry.getAll(currentToken);
934
+ if (ownBindings.length > 0) {
935
+ throw new NoMatchingBindingError(
936
+ this.#getTokenName(currentToken),
937
+ options ?? {},
938
+ this.#getAvailableSlots(currentToken),
939
+ );
940
+ }
941
+ throw new TokenNotBoundError(this.#getTokenName(currentToken));
942
+ }
943
+ const { binding, owner } = aliasFollowed;
944
+
945
+ const scope = (binding as BindingWithScope).scope ?? "transient";
946
+
947
+ if (scope === "singleton" && owner !== this) {
948
+ return owner.#resolveBindingAsync(binding as Binding<Value>, options, resolutionPath, resolutionStack);
949
+ }
950
+
951
+ return this.#resolveBindingAsync(binding as Binding<Value>, options, resolutionPath, resolutionStack);
952
+ }
953
+
954
+ async #resolveBindingAsync<const Value>(
955
+ binding: Binding<Value>,
956
+ options: ResolveOptions | undefined,
957
+ resolutionPath: Array<string>,
958
+ resolutionStack: Array<ResolutionFrame>,
959
+ ): Promise<Value> {
960
+ if (
961
+ binding.kind === "constant" &&
962
+ binding.onActivation === undefined &&
963
+ !this.#lifecycle.hasActivationHandlers(binding.token)
964
+ ) {
965
+ return binding.value;
966
+ }
967
+
968
+ const scope = (binding as BindingWithScope).scope ?? "transient";
969
+
970
+ // Singleton cache
971
+ if (scope === "singleton") {
972
+ if (this.#scope.hasSingleton(binding.id)) {
973
+ return this.#scope.getSingleton<Value>(binding.id);
974
+ }
975
+ // In-flight dedup
976
+ const inflight = this.#scope.getInflight(binding.id);
977
+ if (inflight !== undefined) {
978
+ return inflight as Promise<Value>;
979
+ }
980
+ }
981
+
982
+ // Scoped cache
983
+ if (scope === "scoped") {
984
+ if (!this.#scope.isChild) {
985
+ throw new MissingScopeContextError(this.#getTokenName(binding.token));
986
+ }
987
+ if (this.#scope.hasScoped(binding.id)) {
988
+ return this.#scope.getScoped<Value>(binding.id);
989
+ }
990
+ }
991
+
992
+ const frame = this.#getResolutionFrame(binding);
993
+ const frameName = frame.tokenName;
994
+ const resolutionSet = enterResolutionPath(resolutionPath, frameName, false);
995
+ resolutionStack.push(frame);
996
+ const needsActivation = this.#needsActivation(binding);
997
+ if (!needsActivation && scope === "transient" && (binding.kind === "dynamic" || binding.kind === "dynamic-async")) {
998
+ const resolutionCtx = new DefaultResolutionContext(
999
+ this as unknown as ResolverCallbacks,
1000
+ resolutionPath,
1001
+ resolutionStack,
1002
+ options,
1003
+ );
1004
+ try {
1005
+ if (binding.kind === "dynamic-async") {
1006
+ return await binding.factory(resolutionCtx);
1007
+ }
1008
+ const dynamicResult = binding.factory(resolutionCtx);
1009
+ return dynamicResult instanceof Promise ? await dynamicResult : dynamicResult;
1010
+ } finally {
1011
+ resolutionStack.pop();
1012
+ resolutionPath.pop();
1013
+ resolutionSet?.delete(frameName);
1014
+ }
1015
+ }
1016
+
1017
+ const needsResolutionContext = needsActivation || this.#requiresResolutionContext(binding);
1018
+ const resolutionCtx = needsResolutionContext
1019
+ ? new DefaultResolutionContext(this as unknown as ResolverCallbacks, resolutionPath, resolutionStack, options)
1020
+ : undefined;
1021
+
1022
+ try {
1023
+ if (scope === "singleton") {
1024
+ const createSingletonPromise = async (): Promise<Value> => {
1025
+ const instance = await this.#instantiateAsync(binding, resolutionCtx, resolutionPath, resolutionStack);
1026
+
1027
+ const shouldActivate = this.#refreshActivationCacheIfNeeded(binding, needsActivation);
1028
+ const activated = shouldActivate
1029
+ ? await this.#lifecycle.runActivation(
1030
+ resolutionCtx as DefaultResolutionContext,
1031
+ binding,
1032
+ instance,
1033
+ this.#metadataReader,
1034
+ )
1035
+ : instance;
1036
+
1037
+ this.#scope.setSingleton(binding.id, activated);
1038
+ this.#scope.clearInflight(binding.id);
1039
+ return activated;
1040
+ };
1041
+
1042
+ const singletonPromise = createSingletonPromise().catch((err: unknown) => {
1043
+ this.#scope.clearInflight(binding.id);
1044
+ throw err;
1045
+ });
1046
+ this.#scope.setInflight(binding.id, singletonPromise as Promise<unknown>);
1047
+ return await singletonPromise;
1048
+ }
1049
+
1050
+ const instance = await this.#instantiateAsync(binding, resolutionCtx, resolutionPath, resolutionStack);
1051
+
1052
+ const shouldActivate = this.#refreshActivationCacheIfNeeded(binding, needsActivation);
1053
+ const activated = shouldActivate
1054
+ ? await this.#lifecycle.runActivation(
1055
+ resolutionCtx as DefaultResolutionContext,
1056
+ binding,
1057
+ instance,
1058
+ this.#metadataReader,
1059
+ )
1060
+ : instance;
1061
+
1062
+ if (scope === "scoped") {
1063
+ this.#scope.setScoped(binding.id, activated);
1064
+ }
1065
+
1066
+ return activated;
1067
+ } finally {
1068
+ resolutionStack.pop();
1069
+ resolutionPath.pop();
1070
+ resolutionSet?.delete(frameName);
1071
+ }
1072
+ }
1073
+
1074
+ async #instantiateAsync<const Value>(
1075
+ binding: Binding<Value>,
1076
+ ctx: DefaultResolutionContext | undefined,
1077
+ resolutionPath: Array<string>,
1078
+ resolutionStack: Array<ResolutionFrame>,
1079
+ ): Promise<Value> {
1080
+ switch (binding.kind) {
1081
+ case "constant":
1082
+ return binding.value;
1083
+
1084
+ case "dynamic": {
1085
+ if (ctx === undefined) {
1086
+ throw new InternalError("dynamic binding requires resolution context");
1087
+ }
1088
+ const factoryResult = binding.factory(ctx);
1089
+ return factoryResult instanceof Promise ? factoryResult : Promise.resolve(factoryResult);
1090
+ }
1091
+
1092
+ case "dynamic-async":
1093
+ if (ctx === undefined) {
1094
+ throw new InternalError("dynamic-async binding requires resolution context");
1095
+ }
1096
+ return binding.factory(ctx);
1097
+
1098
+ case "class": {
1099
+ const deps = await this.#resolveClassDepsAsync(binding.target, resolutionPath, resolutionStack);
1100
+ const instance = this.#instantiateClass(binding.target, deps);
1101
+ return instance as Value;
1102
+ }
1103
+
1104
+ case "resolved": {
1105
+ if (ctx === undefined) {
1106
+ throw new InternalError("resolved binding requires resolution context");
1107
+ }
1108
+ const deps = await this.#resolveDescriptorDepsAsync(binding.deps, resolutionPath, resolutionStack);
1109
+ const factoryResult = binding.factory(...deps);
1110
+ return factoryResult instanceof Promise ? factoryResult : Promise.resolve(factoryResult);
1111
+ }
1112
+
1113
+ case "resolved-async": {
1114
+ const deps = await this.#resolveDescriptorDepsAsync(binding.deps, resolutionPath, resolutionStack);
1115
+ return binding.factory(...deps);
1116
+ }
1117
+
1118
+ case "alias":
1119
+ throw new InternalError("alias should have been followed before instantiation");
1120
+ }
1121
+ }
1122
+
1123
+ async #resolveClassDepsAsync(
1124
+ target: Constructor,
1125
+ resolutionPath: Array<string>,
1126
+ resolutionStack: Array<ResolutionFrame>,
1127
+ ): Promise<Array<unknown>> {
1128
+ const meta = this.#getConstructorMetadata(target);
1129
+ if (meta === undefined) {
1130
+ if (target.length === 0) {
1131
+ return [];
1132
+ }
1133
+ throw new MissingMetadataError(target.name);
1134
+ }
1135
+ if (meta.params.length === 0) {
1136
+ return [];
1137
+ }
1138
+ if (meta.params.length === 1) {
1139
+ const param = meta.params[0]!;
1140
+ const paramOptions = injectionSlotToResolveOptions(param);
1141
+ if (param.multi) {
1142
+ return [await this.resolveAllAsync(param.token, paramOptions, resolutionPath, resolutionStack)];
1143
+ }
1144
+ if (param.optional) {
1145
+ return [await this.resolveOptionalAsync(param.token, paramOptions, resolutionPath, resolutionStack)];
1146
+ }
1147
+ if (paramOptions === undefined) {
1148
+ return [await this.resolveAsyncFromContext(param.token, resolutionPath, resolutionStack)];
1149
+ }
1150
+ return [await this.resolveAsync(param.token, paramOptions, resolutionPath, resolutionStack)];
1151
+ }
1152
+ const pending = new Array<Promise<unknown>>(meta.params.length);
1153
+ const shouldCloneContext = meta.params.length > 1;
1154
+ for (let index = 0; index < meta.params.length; index += 1) {
1155
+ const param = meta.params[index]!;
1156
+ const paramOptions = injectionSlotToResolveOptions(param);
1157
+ if (param.multi) {
1158
+ pending[index] = this.resolveAllAsync(
1159
+ param.token,
1160
+ paramOptions,
1161
+ shouldCloneContext ? [...resolutionPath] : resolutionPath,
1162
+ shouldCloneContext ? [...resolutionStack] : resolutionStack,
1163
+ );
1164
+ } else if (param.optional) {
1165
+ pending[index] = this.resolveOptionalAsync(
1166
+ param.token,
1167
+ paramOptions,
1168
+ shouldCloneContext ? [...resolutionPath] : resolutionPath,
1169
+ shouldCloneContext ? [...resolutionStack] : resolutionStack,
1170
+ );
1171
+ } else {
1172
+ pending[index] =
1173
+ paramOptions === undefined
1174
+ ? this.resolveAsyncFromContext(
1175
+ param.token,
1176
+ shouldCloneContext ? [...resolutionPath] : resolutionPath,
1177
+ shouldCloneContext ? [...resolutionStack] : resolutionStack,
1178
+ )
1179
+ : this.resolveAsync(
1180
+ param.token,
1181
+ paramOptions,
1182
+ shouldCloneContext ? [...resolutionPath] : resolutionPath,
1183
+ shouldCloneContext ? [...resolutionStack] : resolutionStack,
1184
+ );
1185
+ }
1186
+ }
1187
+ return Promise.all(pending);
1188
+ }
1189
+
1190
+ async #resolveDescriptorDepsAsync(
1191
+ deps: ReadonlyArray<InjectionDescriptor>,
1192
+ resolutionPath: Array<string>,
1193
+ resolutionStack: Array<ResolutionFrame>,
1194
+ ): Promise<Array<unknown>> {
1195
+ const pending = new Array<Promise<unknown>>(deps.length);
1196
+ const shouldCloneContext = deps.length > 1;
1197
+ for (let index = 0; index < deps.length; index += 1) {
1198
+ const dep = deps[index]!;
1199
+ const depOptions = injectionSlotToResolveOptions(dep);
1200
+ if (dep.multi) {
1201
+ pending[index] = this.resolveAllAsync(
1202
+ dep.token as Token<unknown> | Constructor,
1203
+ depOptions,
1204
+ shouldCloneContext ? [...resolutionPath] : resolutionPath,
1205
+ shouldCloneContext ? [...resolutionStack] : resolutionStack,
1206
+ );
1207
+ } else if (dep.optional) {
1208
+ pending[index] = this.resolveOptionalAsync(
1209
+ dep.token as Token<unknown> | Constructor,
1210
+ depOptions,
1211
+ shouldCloneContext ? [...resolutionPath] : resolutionPath,
1212
+ shouldCloneContext ? [...resolutionStack] : resolutionStack,
1213
+ );
1214
+ } else {
1215
+ pending[index] =
1216
+ depOptions === undefined
1217
+ ? this.resolveAsyncFromContext(
1218
+ dep.token as Token<unknown> | Constructor,
1219
+ shouldCloneContext ? [...resolutionPath] : resolutionPath,
1220
+ shouldCloneContext ? [...resolutionStack] : resolutionStack,
1221
+ )
1222
+ : this.resolveAsync(
1223
+ dep.token as Token<unknown> | Constructor,
1224
+ depOptions,
1225
+ shouldCloneContext ? [...resolutionPath] : resolutionPath,
1226
+ shouldCloneContext ? [...resolutionStack] : resolutionStack,
1227
+ );
1228
+ }
1229
+ }
1230
+ return Promise.all(pending);
1231
+ }
1232
+
1233
+ async resolveOptionalAsync<const Value>(
1234
+ token: Token<Value> | Constructor<Value>,
1235
+ options: ResolveOptions | undefined,
1236
+ resolutionPath: Array<string>,
1237
+ resolutionStack: Array<ResolutionFrame>,
1238
+ ): Promise<Value | undefined> {
1239
+ if (this.#findBinding(token, options, resolutionPath, resolutionStack) === undefined) {
1240
+ return undefined;
1241
+ }
1242
+ return this.resolveAsync(token, options, resolutionPath, resolutionStack);
1243
+ }
1244
+
1245
+ async resolveAllAsync<const Value>(
1246
+ token: Token<Value> | Constructor<Value>,
1247
+ options: ResolveOptions | undefined,
1248
+ resolutionPath: Array<string>,
1249
+ resolutionStack: Array<ResolutionFrame>,
1250
+ ): Promise<Array<Value>> {
1251
+ if (options?.name !== undefined && options.tag === undefined && (options.tags?.length ?? 0) === 0) {
1252
+ const namedCandidates = this.#getSimpleNamedBindingsFromChain(token, options.name);
1253
+ if (namedCandidates.length === 0) {
1254
+ return [];
1255
+ }
1256
+ const pending = new Array<Promise<Value>>(namedCandidates.length);
1257
+ for (let index = 0; index < namedCandidates.length; index += 1) {
1258
+ pending[index] = this.#resolveCandidateAsync(
1259
+ namedCandidates[index] as Binding<Value>,
1260
+ options,
1261
+ resolutionPath,
1262
+ resolutionStack,
1263
+ );
1264
+ }
1265
+ return Promise.all(pending);
1266
+ }
1267
+
1268
+ const allBindings = this.#getAllBindingsFromChain(token);
1269
+ if (allBindings.length === 0) {
1270
+ return [];
1271
+ }
1272
+
1273
+ const ctx = this.#makeConstraintContext(resolutionPath, resolutionStack, options);
1274
+ const candidates = selectAllBindings(allBindings, options, ctx);
1275
+
1276
+ const pending = new Array<Promise<Value>>(candidates.length);
1277
+ for (let index = 0; index < candidates.length; index += 1) {
1278
+ pending[index] = this.#resolveCandidateAsync(
1279
+ candidates[index] as Binding<Value>,
1280
+ options,
1281
+ resolutionPath,
1282
+ resolutionStack,
1283
+ );
1284
+ }
1285
+ return Promise.all(pending);
1286
+ }
1287
+
1288
+ // ── Helpers ────────────────────────────────────────────────────────────────
1289
+
1290
+ #getAllBindingsFromChain(token: Token<unknown> | Constructor): ReadonlyArray<Binding> {
1291
+ const ownBindings = this.#registry.getAll(token);
1292
+ if (this.#parent === undefined) {
1293
+ return ownBindings;
1294
+ }
1295
+ const result: Array<Binding> = [...ownBindings];
1296
+ let current: DependencyResolver | undefined = this.#parent;
1297
+ while (current !== undefined) {
1298
+ const own = current.#registry.getAll(token);
1299
+ if (own.length > 0) {
1300
+ result.push(...own);
1301
+ }
1302
+ current = current.#parent;
1303
+ }
1304
+ return result;
1305
+ }
1306
+
1307
+ #getSimpleNamedBindingsFromChain(token: Token<unknown> | Constructor, name: string): Array<Binding> {
1308
+ const ownBinding = this.#registry.getSimpleNamed(token, name);
1309
+ if (this.#parent === undefined) {
1310
+ return ownBinding !== undefined ? [ownBinding] : [];
1311
+ }
1312
+ const result: Array<Binding> = [];
1313
+ if (ownBinding !== undefined) {
1314
+ result.push(ownBinding);
1315
+ }
1316
+ let current: DependencyResolver | undefined = this.#parent;
1317
+ while (current !== undefined) {
1318
+ const binding = current.#registry.getSimpleNamed(token, name);
1319
+ if (binding !== undefined) {
1320
+ result.push(binding);
1321
+ }
1322
+ current = current.#parent;
1323
+ }
1324
+ return result;
1325
+ }
1326
+
1327
+ #getAvailableSlots(token: Token<unknown> | Constructor): Array<string> {
1328
+ return this.#registry.availableSlotStrings(token);
1329
+ }
1330
+
1331
+ #makeConstraintContext(
1332
+ resolutionPath: Array<string>,
1333
+ resolutionStack: Array<ResolutionFrame>,
1334
+ options: ResolveOptions | undefined,
1335
+ ): ConstraintContext {
1336
+ if (options === undefined && resolutionPath.length === 0 && resolutionStack.length === 0) {
1337
+ return ROOT_CONSTRAINT_CONTEXT;
1338
+ }
1339
+ const parent = resolutionStack.at(-1);
1340
+ const ancestors = resolutionStack.length > 1 ? resolutionStack.slice(0, -1) : [];
1341
+ return {
1342
+ resolutionPath,
1343
+ resolutionStack,
1344
+ parent,
1345
+ ancestors,
1346
+ currentResolveOptions: options,
1347
+ };
1348
+ }
1349
+
1350
+ #matchesBindingFast(
1351
+ binding: Binding,
1352
+ options: ResolveOptions | undefined,
1353
+ resolutionPath: Array<string>,
1354
+ resolutionStack: Array<ResolutionFrame>,
1355
+ ): boolean {
1356
+ if (!this.#matchesSlotFast(binding.slot, options)) {
1357
+ return false;
1358
+ }
1359
+ if (binding.predicate === undefined) {
1360
+ return true;
1361
+ }
1362
+ const ctx = this.#makeConstraintContext(resolutionPath, resolutionStack, options);
1363
+ return binding.predicate(ctx);
1364
+ }
1365
+
1366
+ #matchesSlotFast(slot: BindingSlot, options: ResolveOptions | undefined): boolean {
1367
+ const requestedName = options?.name;
1368
+ const requestedTags = options?.tags;
1369
+ const singleRequestedTag = options?.tag;
1370
+ const hasRequestedTags = (requestedTags?.length ?? 0) > 0 || singleRequestedTag !== undefined;
1371
+
1372
+ if (slot.name !== undefined) {
1373
+ if (requestedName === undefined || slot.name !== requestedName) {
1374
+ return false;
1375
+ }
1376
+ } else if (requestedName !== undefined) {
1377
+ return false;
1378
+ }
1379
+
1380
+ if (slot.tags.length > 0) {
1381
+ if (!hasRequestedTags) {
1382
+ return false;
1383
+ }
1384
+ for (const [tagKey, tagValue] of slot.tags) {
1385
+ if (!this.#matchesRequestedTag(tagKey, tagValue, requestedTags, singleRequestedTag)) {
1386
+ return false;
1387
+ }
1388
+ }
1389
+ } else if (hasRequestedTags) {
1390
+ return false;
1391
+ }
1392
+
1393
+ return true;
1394
+ }
1395
+
1396
+ #getTokenName(token: Token<unknown> | Constructor): string {
1397
+ return tokenName(token);
1398
+ }
1399
+
1400
+ #getConstructorMetadata(target: Constructor): ConstructorMetadata | undefined {
1401
+ const cached = this.#classConstructorMetadata.get(target);
1402
+ if (cached !== undefined) {
1403
+ return cached === null ? undefined : cached;
1404
+ }
1405
+ const metadata = this.#metadataReader.getConstructorMetadata(target);
1406
+ this.#classConstructorMetadata.set(target, metadata ?? null);
1407
+ return metadata;
1408
+ }
1409
+
1410
+ #instantiateClass(target: Constructor, deps: Array<unknown>): unknown {
1411
+ let needsActiveContainer = this.#classNeedsActiveContainer.get(target);
1412
+ if (needsActiveContainer === undefined) {
1413
+ const accessorMetadata = this.#metadataReader.getAccessorMetadata?.(target);
1414
+ needsActiveContainer = (accessorMetadata?.length ?? 0) > 0;
1415
+ this.#classNeedsActiveContainer.set(target, needsActiveContainer);
1416
+ }
1417
+ const invokable = target as ConstructorInvocation;
1418
+ if (!needsActiveContainer) {
1419
+ return new invokable(...deps);
1420
+ }
1421
+ return runWithContainer(this.#container, () => new invokable(...deps));
1422
+ }
1423
+
1424
+ #matchesRequestedTag(
1425
+ tagKey: string,
1426
+ tagValue: unknown,
1427
+ requestedTags: ReadonlyArray<BindingTag> | undefined,
1428
+ singleRequestedTag: BindingTag | undefined,
1429
+ ): boolean {
1430
+ if (
1431
+ singleRequestedTag !== undefined &&
1432
+ singleRequestedTag[0] === tagKey &&
1433
+ Object.is(singleRequestedTag[1], tagValue)
1434
+ ) {
1435
+ return true;
1436
+ }
1437
+ if (requestedTags === undefined || requestedTags.length === 0) {
1438
+ return false;
1439
+ }
1440
+ for (let index = 0; index < requestedTags.length; index += 1) {
1441
+ const requestedTag = requestedTags[index]!;
1442
+ if (requestedTag[0] === tagKey && Object.is(requestedTag[1], tagValue)) {
1443
+ return true;
1444
+ }
1445
+ }
1446
+ return false;
1447
+ }
1448
+
1449
+ #resolveTransientDynamicSyncFromContext<const Value>(
1450
+ binding: Binding<Value> & { kind: "dynamic" },
1451
+ resolutionPath: Array<string>,
1452
+ resolutionStack: Array<ResolutionFrame>,
1453
+ ): Value {
1454
+ // One lane at every depth. The separate deep lane existed because cycle detection used to be
1455
+ // an O(depth) `resolutionPath.includes()` scan, which had to be escaped past ~32 levels; with
1456
+ // the O(1) `binding.inFlight` mark there is nothing to escape, so the depth split — and the
1457
+ // divergent behaviour it caused — is gone.
1458
+ const frame = this.#getResolutionFrame(binding);
1459
+ const tokenDisplayName = frame.tokenName;
1460
+ if (binding.inFlight) {
1461
+ throw new CircularDependencyError([...resolutionPath, tokenDisplayName]);
1462
+ }
1463
+ binding.inFlight = true;
1464
+ resolutionPath.push(tokenDisplayName);
1465
+ resolutionStack.push(frame);
1466
+ const resolutionCtx = this.#acquireSyncResolutionContext(resolutionPath, resolutionStack, undefined);
1467
+ try {
1468
+ const dynamicResult = binding.factory(resolutionCtx);
1469
+ if (dynamicResult instanceof Promise) {
1470
+ throw new AsyncResolutionError(tokenDisplayName, tokenDisplayName);
1471
+ }
1472
+ return dynamicResult;
1473
+ } finally {
1474
+ resolutionStack.pop();
1475
+ resolutionPath.pop();
1476
+ binding.inFlight = false;
1477
+ }
1478
+ }
1479
+
1480
+ // NOT declared `async` — avoids creating a JSAsyncGeneratorObject + implicit Promise wrapper on
1481
+ // every invocation. Cleanup is handled via .then(onFulfilled, onRejected) so the behaviour is
1482
+ // identical to a try/finally but without the async machinery overhead.
1483
+ #resolveTransientDynamicAsyncFromContext<const Value>(
1484
+ binding: Binding<Value> & { kind: "dynamic" | "dynamic-async" },
1485
+ resolutionPath: Array<string>,
1486
+ resolutionStack: Array<ResolutionFrame>,
1487
+ ): Promise<Value> {
1488
+ // One lane at every depth. Cycle detection goes through `enterResolutionPath`, which is
1489
+ // path-scoped — the only mechanism that stays correct when chains interleave (Promise.all) —
1490
+ // and adapts on its own: a linear scan while the path is short, an attached Set past
1491
+ // RESOLUTION_SET_THRESHOLD. That removes the depth split, and with it a silent change of
1492
+ // behaviour (context identity, stack frames, promise shape) at the old threshold.
1493
+ //
1494
+ // For a sequential chain every level shares one resolutionPath/resolutionStack, so a single
1495
+ // DefaultResolutionContext serves the whole chain: inner levels of the owning chain allocate
1496
+ // nothing. A concurrent chain is detected by path identity and gets its own context.
1497
+ const frame = this.#getResolutionFrame(binding);
1498
+ const tokenDisplayName = frame.tokenName;
1499
+ try {
1500
+ enterResolutionPath(resolutionPath, tokenDisplayName, false);
1501
+ } catch (cycleError) {
1502
+ // This method is not `async`; keep failures as rejections rather than sync throws.
1503
+ return Promise.reject(cycleError);
1504
+ }
1505
+
1506
+ let ctx: DefaultResolutionContext;
1507
+ let isOwnerLevel: boolean;
1508
+ if (this.#asyncChainCtxPath === resolutionPath) {
1509
+ ctx = this.#asyncChainCtx!;
1510
+ isOwnerLevel = true;
1511
+ } else if (this.#asyncChainCtxPath === undefined) {
1512
+ const existing = this.#asyncChainCtx;
1513
+ if (existing === undefined) {
1514
+ ctx = new DefaultResolutionContext(
1515
+ this as unknown as ResolverCallbacks,
1516
+ resolutionPath,
1517
+ resolutionStack,
1518
+ undefined,
1519
+ );
1520
+ this.#asyncChainCtx = ctx;
1521
+ } else {
1522
+ existing.reset(this as unknown as ResolverCallbacks, resolutionPath, resolutionStack, undefined);
1523
+ ctx = existing;
1524
+ }
1525
+ this.#asyncChainCtxPath = resolutionPath;
1526
+ isOwnerLevel = true;
1527
+ } else {
1528
+ ctx = new DefaultResolutionContext(
1529
+ this as unknown as ResolverCallbacks,
1530
+ resolutionPath,
1531
+ resolutionStack,
1532
+ undefined,
1533
+ );
1534
+ isOwnerLevel = false;
1535
+ }
1536
+
1537
+ if (isOwnerLevel) {
1538
+ this.#asyncChainActiveLevels++;
1539
+ }
1540
+
1541
+ // Invoke the factory synchronously to get its Promise (or a resolved value for "dynamic").
1542
+ let factoryPromise: Promise<Value>;
1543
+ try {
1544
+ if (binding.kind === "dynamic-async") {
1545
+ factoryPromise = binding.factory(ctx);
1546
+ } else {
1547
+ const factoryResult = binding.factory(ctx);
1548
+ factoryPromise =
1549
+ factoryResult instanceof Promise ? (factoryResult as Promise<Value>) : Promise.resolve(factoryResult);
1550
+ }
1551
+ } catch (factoryError) {
1552
+ // Synchronous throw from the factory (rare) — clean up immediately.
1553
+ exitResolutionPath(resolutionPath);
1554
+ if (isOwnerLevel && --this.#asyncChainActiveLevels === 0) {
1555
+ this.#asyncChainCtxPath = undefined;
1556
+ this.#asyncChainSettle = undefined;
1557
+ }
1558
+ return Promise.reject(factoryError);
1559
+ }
1560
+
1561
+ // Cleanup runs as a SIDE listener on the factory promise instead of a derived-promise chain:
1562
+ // registered synchronously here, it is FIFO-guaranteed to run before the awaiting caller
1563
+ // resumes, so ordering is identical while saving one intermediate promise and one microtask
1564
+ // hop per level. Trade-off: the settle handler marks a rejection as handled, so an unawaited
1565
+ // failing resolveAsync no longer surfaces as an unhandledRejection — callers are expected to
1566
+ // await (or .catch) the returned promise.
1567
+ //
1568
+ // Every level of the owning chain unwinds identically, so one closure serves them all.
1569
+ let settle: () => void;
1570
+ if (isOwnerLevel) {
1571
+ settle =
1572
+ this.#asyncChainSettle ??
1573
+ (this.#asyncChainSettle = (): void => {
1574
+ exitResolutionPath(resolutionPath);
1575
+ if (--this.#asyncChainActiveLevels === 0) {
1576
+ this.#asyncChainCtxPath = undefined;
1577
+ this.#asyncChainSettle = undefined;
1578
+ }
1579
+ });
1580
+ } else {
1581
+ settle = (): void => {
1582
+ exitResolutionPath(resolutionPath);
1583
+ };
1584
+ }
1585
+ factoryPromise.then(settle, settle);
1586
+ return factoryPromise;
1587
+ }
1588
+
1589
+ #resolveCandidateSync<const Value>(
1590
+ binding: Binding<Value>,
1591
+ options: ResolveOptions | undefined,
1592
+ resolutionPath: Array<string>,
1593
+ resolutionStack: Array<ResolutionFrame>,
1594
+ ): Value {
1595
+ if (
1596
+ binding.kind === "constant" &&
1597
+ binding.onActivation === undefined &&
1598
+ !this.#lifecycle.hasActivationHandlers(binding.token)
1599
+ ) {
1600
+ return binding.value;
1601
+ }
1602
+ if (binding.kind === "alias") {
1603
+ return this.resolve(binding.target, options, resolutionPath, resolutionStack);
1604
+ }
1605
+ const scope = (binding as BindingWithScope).scope ?? "transient";
1606
+ if (scope === "singleton" && this.#scope.hasSingleton(binding.id)) {
1607
+ return this.#scope.getSingleton<Value>(binding.id);
1608
+ }
1609
+ if (scope === "scoped") {
1610
+ if (!this.#scope.isChild) {
1611
+ throw new MissingScopeContextError(this.#getTokenName(binding.token));
1612
+ }
1613
+ if (this.#scope.hasScoped(binding.id)) {
1614
+ return this.#scope.getScoped<Value>(binding.id);
1615
+ }
1616
+ }
1617
+ return this.#resolveBinding(binding, options, resolutionPath, resolutionStack);
1618
+ }
1619
+
1620
+ #resolveCandidateAsync<const Value>(
1621
+ binding: Binding<Value>,
1622
+ options: ResolveOptions | undefined,
1623
+ resolutionPath: Array<string>,
1624
+ resolutionStack: Array<ResolutionFrame>,
1625
+ ): Promise<Value> {
1626
+ if (
1627
+ binding.kind === "constant" &&
1628
+ binding.onActivation === undefined &&
1629
+ !this.#lifecycle.hasActivationHandlers(binding.token)
1630
+ ) {
1631
+ return Promise.resolve(binding.value);
1632
+ }
1633
+ const isolatedPath = [...resolutionPath];
1634
+ const isolatedStack = [...resolutionStack];
1635
+ if (binding.kind === "alias") {
1636
+ return this.resolveAsync(binding.target, options, isolatedPath, isolatedStack);
1637
+ }
1638
+ const scope = (binding as BindingWithScope).scope ?? "transient";
1639
+ if (scope === "singleton" && this.#scope.hasSingleton(binding.id)) {
1640
+ return Promise.resolve(this.#scope.getSingleton<Value>(binding.id));
1641
+ }
1642
+ if (scope === "scoped") {
1643
+ if (!this.#scope.isChild) {
1644
+ return Promise.reject(new MissingScopeContextError(this.#getTokenName(binding.token)));
1645
+ }
1646
+ if (this.#scope.hasScoped(binding.id)) {
1647
+ return Promise.resolve(this.#scope.getScoped<Value>(binding.id));
1648
+ }
1649
+ }
1650
+ return this.#resolveBindingAsync(binding, options, isolatedPath, isolatedStack);
1651
+ }
1652
+
1653
+ #getResolutionFrame<const Value>(binding: Binding<Value>): ResolutionFrame {
1654
+ // Memoized on the binding rather than in a per-resolver Map: the frame derives only from
1655
+ // immutable binding fields, so it is identical for every resolver, and a field read beats a
1656
+ // Map lookup on every hop of a chain.
1657
+ const existing = binding.frame;
1658
+ if (existing !== undefined) {
1659
+ return existing;
1660
+ }
1661
+ const scope = (binding as BindingWithScope).scope ?? "transient";
1662
+ const frame = buildResolutionFrame(tokenName(binding.token), scope, binding.id, binding.kind, binding.slot);
1663
+ binding.frame = frame;
1664
+ return frame;
1665
+ }
1666
+
1667
+ #needsActivation<const Value>(binding: Binding<Value>): boolean {
1668
+ const lifecycleVersion = this.#lifecycle.activationVersion;
1669
+ if (
1670
+ lifecycleVersion === 0 &&
1671
+ binding.kind !== "class" &&
1672
+ binding.kind !== "alias" &&
1673
+ binding.onActivation === undefined
1674
+ ) {
1675
+ return false;
1676
+ }
1677
+ if (this.#activationCacheVersion !== lifecycleVersion) {
1678
+ this.#activationNeedByBindingId.clear();
1679
+ this.#activationCacheVersion = lifecycleVersion;
1680
+ }
1681
+
1682
+ const cached = this.#activationNeedByBindingId.get(binding.id);
1683
+ if (cached !== undefined) {
1684
+ return cached;
1685
+ }
1686
+
1687
+ if (binding.kind === "class") {
1688
+ let hasActivation = this.#lifecycle.hasActivationHandlers(binding.token) || binding.onActivation !== undefined;
1689
+ const cachedPostConstruct = this.#classHasPostConstruct.get(binding.target);
1690
+ // Unknown class lifecycle metadata: activate once, then cache after first instantiation.
1691
+ if (cachedPostConstruct === undefined) {
1692
+ hasActivation = true;
1693
+ } else if (cachedPostConstruct) {
1694
+ hasActivation = true;
1695
+ }
1696
+ this.#activationNeedByBindingId.set(binding.id, hasActivation);
1697
+ return hasActivation;
1698
+ }
1699
+
1700
+ let hasActivation = false;
1701
+ if (binding.kind !== "alias" && binding.onActivation !== undefined) {
1702
+ hasActivation = true;
1703
+ } else if (this.#lifecycle.hasActivationHandlers(binding.token)) {
1704
+ hasActivation = true;
1705
+ }
1706
+
1707
+ this.#activationNeedByBindingId.set(binding.id, hasActivation);
1708
+ return hasActivation;
1709
+ }
1710
+
1711
+ #refreshClassPostConstructCache(target: Constructor): void {
1712
+ const lifecycle = this.#metadataReader.getLifecycleMetadata(target);
1713
+ const hasPostConstruct =
1714
+ lifecycle !== undefined && lifecycle.postConstruct !== undefined && lifecycle.postConstruct.length > 0;
1715
+ this.#classHasPostConstruct.set(target, hasPostConstruct);
1716
+ }
1717
+
1718
+ /**
1719
+ * Refreshes the post-construct cache for class bindings on first instantiation and
1720
+ * returns the (possibly updated) shouldActivate flag.
1721
+ */
1722
+ #refreshActivationCacheIfNeeded<Value>(binding: Binding<Value>, needsActivation: boolean): boolean {
1723
+ if (binding.kind === "class" && this.#classHasPostConstruct.get(binding.target) === undefined) {
1724
+ this.#refreshClassPostConstructCache(binding.target);
1725
+ this.#activationNeedByBindingId.delete(binding.id);
1726
+ return this.#needsActivation(binding);
1727
+ }
1728
+ return needsActivation;
1729
+ }
1730
+
1731
+ #requiresResolutionContext<const Value>(binding: Binding<Value>): boolean {
1732
+ return binding.kind === "dynamic" || binding.kind === "dynamic-async";
1733
+ }
1734
+
1735
+ #acquireSyncResolutionContext(
1736
+ resolutionPath: Array<string>,
1737
+ resolutionStack: Array<ResolutionFrame>,
1738
+ options: ResolveOptions | undefined,
1739
+ ): DefaultResolutionContext {
1740
+ const depth = resolutionStack.length;
1741
+ const existing = this.#syncResolutionContextPool[depth];
1742
+ if (existing !== undefined) {
1743
+ existing.reset(this as unknown as ResolverCallbacks, resolutionPath, resolutionStack, options);
1744
+ return existing;
1745
+ }
1746
+ const created = new DefaultResolutionContext(
1747
+ this as unknown as ResolverCallbacks,
1748
+ resolutionPath,
1749
+ resolutionStack,
1750
+ options,
1751
+ );
1752
+ this.#syncResolutionContextPool[depth] = created;
1753
+ return created;
1754
+ }
1755
+ }