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

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 (114) hide show
  1. package/CHANGELOG.md +205 -0
  2. package/README.md +6 -2
  3. package/dist/binding.d.ts +85 -24
  4. package/dist/binding.d.ts.map +1 -1
  5. package/dist/binding.js +55 -0
  6. package/dist/binding.js.map +1 -1
  7. package/dist/constructor-type.d.ts +4 -5
  8. package/dist/constructor-type.d.ts.map +1 -1
  9. package/dist/container/binding-builders.d.ts +32 -11
  10. package/dist/container/binding-builders.d.ts.map +1 -1
  11. package/dist/container/binding-builders.js +144 -192
  12. package/dist/container/binding-builders.js.map +1 -1
  13. package/dist/container/container.d.ts.map +1 -1
  14. package/dist/container/container.js +141 -201
  15. package/dist/container/container.js.map +1 -1
  16. package/dist/decorators/inject.d.ts +2 -4
  17. package/dist/decorators/inject.d.ts.map +1 -1
  18. package/dist/decorators/inject.js.map +1 -1
  19. package/dist/errors.d.ts +24 -0
  20. package/dist/errors.d.ts.map +1 -1
  21. package/dist/errors.js +30 -0
  22. package/dist/errors.js.map +1 -1
  23. package/dist/index.d.ts +1 -2
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +3 -2
  26. package/dist/index.js.map +1 -1
  27. package/dist/introspection/inspector.d.ts +0 -1
  28. package/dist/introspection/inspector.d.ts.map +1 -1
  29. package/dist/introspection/inspector.js +3 -8
  30. package/dist/introspection/inspector.js.map +1 -1
  31. package/dist/metadata/metadata-keys.d.ts +3 -6
  32. package/dist/metadata/metadata-keys.d.ts.map +1 -1
  33. package/dist/metadata/metadata-keys.js +3 -6
  34. package/dist/metadata/metadata-keys.js.map +1 -1
  35. package/dist/registry.d.ts +14 -2
  36. package/dist/registry.d.ts.map +1 -1
  37. package/dist/registry.js +81 -78
  38. package/dist/registry.js.map +1 -1
  39. package/dist/resolution/activation-need.d.ts +27 -0
  40. package/dist/resolution/activation-need.d.ts.map +1 -0
  41. package/dist/resolution/activation-need.js +68 -0
  42. package/dist/resolution/activation-need.js.map +1 -0
  43. package/dist/resolution/binding-lookup-cache.d.ts +41 -0
  44. package/dist/resolution/binding-lookup-cache.d.ts.map +1 -0
  45. package/dist/resolution/binding-lookup-cache.js +118 -0
  46. package/dist/resolution/binding-lookup-cache.js.map +1 -0
  47. package/dist/resolution/binding-scope.d.ts +5 -2
  48. package/dist/resolution/binding-scope.d.ts.map +1 -1
  49. package/dist/resolution/binding-scope.js +6 -17
  50. package/dist/resolution/binding-scope.js.map +1 -1
  51. package/dist/resolution/binding-select.d.ts +8 -1
  52. package/dist/resolution/binding-select.d.ts.map +1 -1
  53. package/dist/resolution/binding-select.js +14 -36
  54. package/dist/resolution/binding-select.js.map +1 -1
  55. package/dist/resolution/class-introspector.d.ts +27 -0
  56. package/dist/resolution/class-introspector.d.ts.map +1 -0
  57. package/dist/resolution/class-introspector.js +60 -0
  58. package/dist/resolution/class-introspector.js.map +1 -0
  59. package/dist/resolution/diagnostics.d.ts +41 -0
  60. package/dist/resolution/diagnostics.d.ts.map +1 -0
  61. package/dist/resolution/diagnostics.js +18 -0
  62. package/dist/resolution/diagnostics.js.map +1 -0
  63. package/dist/resolution/environment.d.ts +48 -1
  64. package/dist/resolution/environment.d.ts.map +1 -1
  65. package/dist/resolution/environment.js +134 -5
  66. package/dist/resolution/environment.js.map +1 -1
  67. package/dist/resolution/instantiation-plan.d.ts +15 -15
  68. package/dist/resolution/instantiation-plan.d.ts.map +1 -1
  69. package/dist/resolution/instantiation-plan.js +69 -49
  70. package/dist/resolution/instantiation-plan.js.map +1 -1
  71. package/dist/resolution/lifecycle.d.ts +2 -0
  72. package/dist/resolution/lifecycle.d.ts.map +1 -1
  73. package/dist/resolution/lifecycle.js +60 -62
  74. package/dist/resolution/lifecycle.js.map +1 -1
  75. package/dist/resolution/resolution-path.d.ts +84 -17
  76. package/dist/resolution/resolution-path.d.ts.map +1 -1
  77. package/dist/resolution/resolution-path.js +68 -23
  78. package/dist/resolution/resolution-path.js.map +1 -1
  79. package/dist/resolution/resolve-options.d.ts +41 -4
  80. package/dist/resolution/resolve-options.d.ts.map +1 -1
  81. package/dist/resolution/resolve-options.js +25 -1
  82. package/dist/resolution/resolve-options.js.map +1 -1
  83. package/dist/resolution/resolver.d.ts +33 -10
  84. package/dist/resolution/resolver.d.ts.map +1 -1
  85. package/dist/resolution/resolver.js +471 -830
  86. package/dist/resolution/resolver.js.map +1 -1
  87. package/dist/resolution/scope.d.ts +11 -15
  88. package/dist/resolution/scope.d.ts.map +1 -1
  89. package/dist/resolution/scope.js +55 -43
  90. package/dist/resolution/scope.js.map +1 -1
  91. package/package.json +10 -106
  92. package/src/binding.ts +146 -24
  93. package/src/constructor-type.ts +4 -5
  94. package/src/container/binding-builders.ts +184 -284
  95. package/src/container/container.ts +161 -221
  96. package/src/decorators/inject.ts +3 -5
  97. package/src/errors.ts +38 -0
  98. package/src/index.ts +4 -1
  99. package/src/introspection/inspector.ts +3 -9
  100. package/src/metadata/metadata-keys.ts +3 -6
  101. package/src/registry.ts +90 -94
  102. package/src/resolution/activation-need.ts +85 -0
  103. package/src/resolution/binding-lookup-cache.ts +148 -0
  104. package/src/resolution/binding-scope.ts +6 -17
  105. package/src/resolution/binding-select.ts +15 -39
  106. package/src/resolution/class-introspector.ts +74 -0
  107. package/src/resolution/diagnostics.ts +43 -0
  108. package/src/resolution/environment.ts +181 -5
  109. package/src/resolution/instantiation-plan.ts +116 -64
  110. package/src/resolution/lifecycle.ts +69 -62
  111. package/src/resolution/resolution-path.ts +122 -43
  112. package/src/resolution/resolve-options.ts +51 -4
  113. package/src/resolution/resolver.ts +649 -1081
  114. package/src/resolution/scope.ts +58 -47
@@ -1,16 +1,17 @@
1
+ import { NO_INSTANCE } from "#/binding";
1
2
  import { AsyncActivationError, AsyncResolutionError, CircularDependencyError, InternalError, MissingMetadataError, MissingScopeContextError, NoMatchingBindingError, TokenNotBoundError, } from "#/errors";
2
- import { selectAllBindings, selectBinding } from "#/resolution/binding-select";
3
- import { buildResolutionFrame, DefaultResolutionContext, runWithContainer } from "#/resolution/environment";
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";
4
8
  import { InstantiationPlanCompiler, PLAN_RETRY } from "#/resolution/instantiation-plan";
5
- import { enterResolutionPath, exitResolutionPath } from "#/resolution/resolution-path";
6
- import { injectionSlotToResolveOptions } from "#/resolution/resolve-options";
7
- import { SINGLETON_MISS } from "#/resolution/scope";
9
+ import { branchDepthOf, enterResolutionPath, extendResolutionBranch, extendResolutionStackBranch, ROOT_BRANCH, UNOWNED_BRANCH, } from "#/resolution/resolution-path";
10
+ import { injectionSlotToResolveOptions, isNameOnlyOptions, singleTagOnlyOf } from "#/resolution/resolve-options";
8
11
  import { tokenName } from "#/token";
9
- // Fast-lane alias folding gives up past this many hops and defers to the resolve()
10
- // loop, whose Set-based traversal detects genuine cycles exactly (no arbitrary cap).
11
- const ALIAS_HOP_LIMIT = 32;
12
12
  const EMPTY_STRING_LIST = [];
13
13
  const EMPTY_FRAME_LIST = [];
14
+ const EMPTY_PARAM_LIST = [];
14
15
  const ROOT_CONSTRAINT_CONTEXT = {
15
16
  resolutionPath: EMPTY_STRING_LIST,
16
17
  resolutionStack: EMPTY_FRAME_LIST,
@@ -23,51 +24,23 @@ const ROOT_CONSTRAINT_CONTEXT = {
23
24
  */
24
25
  export class DependencyResolver {
25
26
  #syncResolutionContextPool = [];
26
- // Cycle detection for the sync transient-dynamic lane lives on `binding.inFlight` — see the
27
- // field's doc comment in binding.ts. It is an O(1) field read with no hashing, no path scan and
28
- // no side table to allocate or grow, so the lane needs no depth split.
29
- // Shared-context state for the async transient-dynamic lane.
30
- //
31
- // Every level of a SEQUENTIAL async chain shares the same resolutionPath and resolutionStack
32
- // arrays (passed by reference through ctx.resolveAsync), and the context stores references
33
- // rather than snapshots, so one DefaultResolutionContext can serve the whole chain — the arrays
34
- // reflect the current state automatically as levels push and pop.
35
- //
36
- // #asyncChainCtx: the shared context, created on first use and reset at each new root call.
37
- // Inner levels of the same chain reuse it with zero setup.
38
- // #asyncChainCtxPath: identity of the resolutionPath array owning the shared context — same
39
- // reference means an inner level of that chain, a different one means a concurrent chain
40
- // (e.g. Promise.all) which gets its own context instead.
41
- // #asyncChainActiveLevels: active levels of the OWNING chain, so the path pointer is released
42
- // when the last one settles. Concurrent fallback calls are not counted.
43
- //
44
- // The method is NOT declared async: that would allocate an async state machine and an implicit
45
- // promise per level, where a `.then(settle, settle)` side listener costs neither.
46
- #asyncChainCtx;
47
- #asyncChainCtxPath;
48
- #asyncChainActiveLevels = 0;
49
- // Settle callback shared by every level of the owning async chain: all of them pop the same
50
- // resolutionPath and decrement the same counter, so one closure serves the whole chain instead
51
- // of allocating one per level (the async lane's dominant per-level allocation).
52
- #asyncChainSettle;
53
- #classHasPostConstruct = new WeakMap();
54
- #classNeedsActiveContainer = new WeakMap();
55
- #classConstructorMetadata = new WeakMap();
56
- #activationNeedByBindingId = new Map();
57
- #activationCacheVersion = -1;
58
- // Options-less lookup memo across the parent chain: token → terminal {binding, owner}
59
- // (alias hops folded). `null` = token must take the slow lookup path. Invalidated when
60
- // any registry in the chain mutates (monotonic version sum).
61
- #defaultLookupByToken = new Map();
62
- #defaultLookupVersion = -1;
63
- // Name-only lookup memo (token → name → entry) with the same chain-version
64
- // invalidation. `null` = shape needs the full selection path.
65
- #namedLookupByToken = new Map();
66
- #namedLookupVersion = -1;
67
- // Compiled transient-class plans (Dagger-style): a pure-static subgraph (class/constant/
68
- // cached-singleton deps only) compiles once into a nested-constructor closure — cycle
69
- // checking happens at compile time, so execution skips all per-resolve bookkeeping.
70
- // `null` = binding is not plannable under the current versions.
27
+ /**
28
+ * The pair a top-level **sync** resolve reuses instead of minting two arrays per call.
29
+ *
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.
35
+ */
36
+ rootPath = [];
37
+ rootStack = [];
38
+ // The open synchronous factory cascade: its arrays are the ancestor chain, and they are balanced
39
+ // because synchronous code does not interleave.
40
+ #cascadePath = [];
41
+ #cascadeStack = [];
42
+ #cascadeContext;
43
+ // Compiled plans; `null` marks a binding as unplannable under the current cache versions.
71
44
  #classPlanByBindingId = new Map();
72
45
  #classPlanRegistryVersion = -1;
73
46
  #classPlanActivationVersion = -1;
@@ -75,15 +48,32 @@ export class DependencyResolver {
75
48
  #scope;
76
49
  #lifecycle;
77
50
  #metadataReader;
78
- #container;
79
51
  #parent;
52
+ #lookup;
53
+ #classes;
54
+ #activation;
80
55
  constructor(registry, scope, lifecycle, metadataReader, container, parent) {
81
56
  this.#registry = registry;
82
57
  this.#scope = scope;
83
58
  this.#lifecycle = lifecycle;
84
59
  this.#metadataReader = metadataReader;
85
- this.#container = container;
86
60
  this.#parent = parent;
61
+ this.#lookup = new BindingLookupCache(registry, this, parent === undefined ? undefined : parent.#lookup);
62
+ this.#classes = new ClassIntrospector(metadataReader, container);
63
+ this.#activation = new ActivationNeedCache(lifecycle, this.#classes, registry);
64
+ }
65
+ /** Structural counts for {@link RESOLUTION_DIAGNOSTICS}; see `resolution/diagnostics.ts`. */
66
+ describeCaches() {
67
+ let compiledPlanCount = 0;
68
+ for (const plan of this.#classPlanByBindingId.values()) {
69
+ if (plan !== null) {
70
+ compiledPlanCount += 1;
71
+ }
72
+ }
73
+ return {
74
+ compiledPlanCount,
75
+ syncContextPoolSize: this.#syncResolutionContextPool.length,
76
+ };
87
77
  }
88
78
  // ── Binding lookup ─────────────────────────────────────────────────────────
89
79
  #findBinding(token, options, resolutionPath, resolutionStack) {
@@ -93,39 +83,33 @@ export class DependencyResolver {
93
83
  return { binding: fastDefaultBinding, owner: this };
94
84
  }
95
85
  }
96
- if (options?.name !== undefined && options.tag === undefined && (options.tags?.length ?? 0) === 0) {
86
+ else if (isNameOnlyOptions(options)) {
97
87
  const namedBinding = this.#registry.getSimpleNamed(token, options.name);
98
88
  if (namedBinding !== undefined &&
99
89
  this.#matchesBindingFast(namedBinding, options, resolutionPath, resolutionStack)) {
100
90
  return { binding: namedBinding, owner: this };
101
91
  }
102
92
  }
103
- if (options !== undefined &&
104
- options.name === undefined &&
105
- options.tag === undefined &&
106
- (options.tags?.length ?? 0) === 1) {
107
- const [tagKey, tagValue] = options.tags[0];
108
- const tagged = this.#registry.getSimpleTagged(token, tagKey, tagValue);
109
- if (tagged !== undefined) {
110
- return { binding: tagged, owner: this };
93
+ else {
94
+ const singleTag = singleTagOnlyOf(options);
95
+ if (singleTag !== undefined) {
96
+ const tagged = this.#registry.getSimpleTagged(token, singleTag[0], singleTag[1]);
97
+ if (tagged !== undefined && matchesIndexedTagValue(tagged, singleTag[1])) {
98
+ return { binding: tagged, owner: this };
99
+ }
111
100
  }
112
101
  }
113
102
  const bindings = this.#registry.getAll(token);
114
103
  if (bindings.length > 0) {
115
- if (bindings.length === 1) {
116
- const onlyBinding = bindings[0];
117
- const isDefaultSlot = onlyBinding.slot.name === undefined && onlyBinding.slot.tags.length === 0;
118
- if (options === undefined && isDefaultSlot && onlyBinding.predicate === undefined) {
119
- return { binding: onlyBinding, owner: this };
120
- }
121
- if (this.#matchesBindingFast(onlyBinding, options, resolutionPath, resolutionStack)) {
122
- return { binding: onlyBinding, owner: this };
123
- }
124
- }
125
- const ctx = this.#makeConstraintContext(resolutionPath, resolutionStack, options);
126
- const binding = selectBinding(bindings, options, ctx, this.#getTokenName(token));
127
- if (binding !== undefined) {
128
- return { binding, owner: this };
104
+ // A lone candidate is its own selection: matching it is the whole decision, with no
105
+ // specificity to weigh and no ambiguity to report.
106
+ const selected = bindings.length === 1
107
+ ? this.#matchesBindingFast(bindings[0], options, resolutionPath, resolutionStack)
108
+ ? bindings[0]
109
+ : undefined
110
+ : selectBinding(bindings, options, this.#makeConstraintContext(resolutionPath, resolutionStack, options), tokenName(token));
111
+ if (selected !== undefined) {
112
+ return { binding: selected, owner: this };
129
113
  }
130
114
  }
131
115
  if (this.#parent !== undefined) {
@@ -133,6 +117,36 @@ export class DependencyResolver {
133
117
  }
134
118
  return undefined;
135
119
  }
120
+ /**
121
+ * The binding a token resolves to with alias hops followed, or a diagnostic throw.
122
+ *
123
+ * @remarks Alias hops are followed iteratively with exact cycle detection — a revisited alias
124
+ * token raises {@link CircularDependencyError} instead of overflowing the call stack.
125
+ */
126
+ #requireBinding(token, options, resolutionPath, resolutionStack) {
127
+ let currentToken = token;
128
+ let visitedAliasTokens;
129
+ let found = this.#findBinding(currentToken, options, resolutionPath, resolutionStack);
130
+ while (found !== undefined && found.binding.kind === "alias") {
131
+ const target = found.binding.target;
132
+ visitedAliasTokens ??= new Set([currentToken]);
133
+ if (visitedAliasTokens.has(target)) {
134
+ throw new CircularDependencyError([...visitedAliasTokens, target].map((entry) => tokenName(entry)));
135
+ }
136
+ visitedAliasTokens.add(target);
137
+ currentToken = target;
138
+ found = this.#findBinding(currentToken, options, resolutionPath, resolutionStack);
139
+ }
140
+ if (found === undefined) {
141
+ // Thrown here rather than from a helper: the error captures this stack, and an error path is
142
+ // dominated by that capture. Bindings under the token mean the request matched none of them.
143
+ if (this.#registry.getAll(currentToken).length > 0) {
144
+ throw new NoMatchingBindingError(tokenName(currentToken), options ?? {}, this.#registry.availableSlotStrings(currentToken));
145
+ }
146
+ throw new TokenNotBoundError(tokenName(currentToken));
147
+ }
148
+ return found;
149
+ }
136
150
  /**
137
151
  * Binding lookup aligned with `resolve` — used by `Container.validate` without instantiating.
138
152
  */
@@ -143,15 +157,7 @@ export class DependencyResolver {
143
157
  * Mirrors {@link DependencyResolver.resolveAll} candidate selection only (no instantiation).
144
158
  */
145
159
  peekCandidateBindingsForValidate(token, options) {
146
- if (options?.name !== undefined && options.tag === undefined && (options.tags?.length ?? 0) === 0) {
147
- return this.#getSimpleNamedBindingsFromChain(token, options.name);
148
- }
149
- const allBindings = this.#getAllBindingsFromChain(token);
150
- if (allBindings.length === 0) {
151
- return [];
152
- }
153
- const ctx = this.#makeConstraintContext([], [], options);
154
- return selectAllBindings(allBindings, options, ctx);
160
+ return this.#candidateBindings(token, options, [], []);
155
161
  }
156
162
  // ── Sync resolve ───────────────────────────────────────────────────────────
157
163
  resolveFromContext(token, resolutionPath, resolutionStack) {
@@ -161,19 +167,14 @@ export class DependencyResolver {
161
167
  if (fastBinding !== undefined && fastBinding.kind !== "alias") {
162
168
  return this.#resolveDefaultEntry(fastBinding, this, resolutionPath, resolutionStack);
163
169
  }
164
- const entry = this.#lookupDefaultEntry(token);
170
+ const entry = this.#lookup.defaultEntry(token);
165
171
  if (entry === null) {
166
172
  return this.resolve(token, undefined, resolutionPath, resolutionStack);
167
173
  }
168
174
  return this.#resolveDefaultEntry(entry.binding, entry.owner, resolutionPath, resolutionStack);
169
175
  }
170
176
  #resolveDefaultEntry(binding, owner, resolutionPath, resolutionStack) {
171
- if (binding.kind === "constant" &&
172
- binding.onActivation === undefined &&
173
- (this.#lifecycle.activationVersion === 0 || !this.#lifecycle.hasActivationHandlers(binding.token))) {
174
- return binding.value;
175
- }
176
- const scope = binding.scope ?? "transient";
177
+ const scope = binding.scope;
177
178
  if (scope === "transient") {
178
179
  if (binding.kind === "dynamic") {
179
180
  const containerHooks = this.#lifecycle.activationVersion === 0 ? undefined : this.#lifecycle.activationHandlersFor(binding.token);
@@ -191,8 +192,12 @@ export class DependencyResolver {
191
192
  }
192
193
  }
193
194
  else if (scope === "singleton") {
194
- const cachedSingleton = owner.#scope.peekSingleton(binding.id);
195
- if (cachedSingleton !== SINGLETON_MISS) {
195
+ // A constant is a singleton that is already its own instance.
196
+ if (this.#isPlainConstant(binding)) {
197
+ return binding.value;
198
+ }
199
+ const cachedSingleton = binding.instance;
200
+ if (cachedSingleton !== NO_INSTANCE) {
196
201
  return cachedSingleton;
197
202
  }
198
203
  if (owner !== this) {
@@ -200,11 +205,9 @@ export class DependencyResolver {
200
205
  }
201
206
  }
202
207
  else {
203
- if (!this.#scope.isChild) {
204
- throw new MissingScopeContextError(this.#getTokenName(binding.token));
205
- }
206
- if (this.#scope.hasScoped(binding.id)) {
207
- return this.#scope.getScoped(binding.id);
208
+ const cachedScoped = this.#readScoped(binding);
209
+ if (cachedScoped !== SCOPED_MISS) {
210
+ return cachedScoped;
208
211
  }
209
212
  }
210
213
  return this.#resolveBinding(binding, undefined, resolutionPath, resolutionStack);
@@ -213,9 +216,15 @@ export class DependencyResolver {
213
216
  // generic #resolveBinding path (guard, frame, ctx, per-binding then container hooks) with
214
217
  // the kind/activation dispatch resolved statically.
215
218
  #resolveTransientDynamicActivatedSync(binding, containerHooks, resolutionPath, resolutionStack) {
219
+ // 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.
216
221
  const frame = this.#getResolutionFrame(binding);
217
222
  const tokenDisplayName = frame.tokenName;
218
- const resolutionSet = enterResolutionPath(resolutionPath, tokenDisplayName, false);
223
+ if (binding.inFlight) {
224
+ throw new CircularDependencyError([...resolutionPath, tokenDisplayName]);
225
+ }
226
+ binding.inFlight = true;
227
+ resolutionPath.push(tokenDisplayName);
219
228
  resolutionStack.push(frame);
220
229
  try {
221
230
  const resolutionCtx = this.#acquireSyncResolutionContext(resolutionPath, resolutionStack, undefined);
@@ -245,86 +254,11 @@ export class DependencyResolver {
245
254
  finally {
246
255
  resolutionStack.pop();
247
256
  resolutionPath.pop();
248
- resolutionSet?.delete(tokenDisplayName);
249
- }
250
- }
251
- #chainRegistryVersion() {
252
- let version = this.#registry.version;
253
- for (let resolver = this.#parent; resolver !== undefined; resolver = resolver.#parent) {
254
- version += resolver.#registry.version;
255
- }
256
- return version;
257
- }
258
- #lookupDefaultEntry(token) {
259
- const version = this.#chainRegistryVersion();
260
- if (version !== this.#defaultLookupVersion) {
261
- this.#defaultLookupByToken.clear();
262
- this.#defaultLookupVersion = version;
263
- }
264
- let entry = this.#defaultLookupByToken.get(token);
265
- if (entry === undefined) {
266
- entry = this.#computeDefaultEntry(token);
267
- this.#defaultLookupByToken.set(token, entry);
268
- }
269
- return entry;
270
- }
271
- #computeDefaultEntry(token) {
272
- let current = token;
273
- for (let hop = 0; hop < ALIAS_HOP_LIMIT; hop += 1) {
274
- const entry = this.#findDefaultEntryInChain(current);
275
- if (entry === null) {
276
- return null;
277
- }
278
- if (entry.binding.kind === "alias") {
279
- current = entry.binding.target;
280
- continue;
281
- }
282
- return entry;
283
- }
284
- return null;
285
- }
286
- #lookupNamedEntry(token, name) {
287
- const version = this.#chainRegistryVersion();
288
- if (version !== this.#namedLookupVersion) {
289
- this.#namedLookupByToken.clear();
290
- this.#namedLookupVersion = version;
291
- }
292
- // ✓ TS6.0: Map.getOrInsert (ES2025)
293
- const entriesByName = this.#namedLookupByToken.getOrInsert(token, new Map());
294
- let entry = entriesByName.get(name);
295
- if (entry === undefined) {
296
- entry = this.#findNamedEntryInChain(token, name);
297
- entriesByName.set(name, entry);
298
- }
299
- return entry;
300
- }
301
- #findNamedEntryInChain(token, name) {
302
- const named = this.#registry.getSimpleNamed(token, name);
303
- if (named !== undefined) {
304
- // Predicates need a live context; aliases carry options through the full path.
305
- if (named.predicate !== undefined || named.kind === "alias") {
306
- return null;
307
- }
308
- return { binding: named, owner: this };
309
- }
310
- if (this.#registry.has(token)) {
311
- return null;
312
- }
313
- return this.#parent === undefined ? null : this.#parent.#findNamedEntryInChain(token, name);
314
- }
315
- #findDefaultEntryInChain(token) {
316
- const fast = this.#registry.getFastDefault(token);
317
- if (fast !== undefined) {
318
- return { binding: fast, owner: this };
319
- }
320
- // A level with non-fast bindings (multi-slot / predicate) needs full selection — bail.
321
- if (this.#registry.has(token)) {
322
- return null;
257
+ binding.inFlight = false;
323
258
  }
324
- return this.#parent === undefined ? null : this.#parent.#findDefaultEntryInChain(token);
325
259
  }
326
260
  #getInstantiationPlan(binding) {
327
- const registryVersion = this.#chainRegistryVersion();
261
+ const registryVersion = this.#lookup.chainVersion();
328
262
  const activationVersion = this.#lifecycle.activationVersion;
329
263
  if (registryVersion !== this.#classPlanRegistryVersion || activationVersion !== this.#classPlanActivationVersion) {
330
264
  this.#classPlanByBindingId.clear();
@@ -343,141 +277,94 @@ export class DependencyResolver {
343
277
  this.#classPlanByBindingId.set(binding.id, compiled);
344
278
  return compiled;
345
279
  }
346
- // Compiler behind #getClassPlan — cold path, so the host indirection costs nothing hot.
280
+ // Compiler behind #getInstantiationPlan — cold path, so the host indirection costs nothing hot.
347
281
  #planCompiler = new InstantiationPlanCompiler({
348
282
  hasActivationHandlers: (token) => this.#lifecycle.hasActivationHandlers(token),
349
- knownPostConstruct: (target) => this.#classHasPostConstruct.get(target),
350
- needsActiveContainer: (target) => {
351
- let needsActiveContainer = this.#classNeedsActiveContainer.get(target);
352
- if (needsActiveContainer === undefined) {
353
- const accessorMetadata = this.#metadataReader.getAccessorMetadata?.(target);
354
- needsActiveContainer = (accessorMetadata?.length ?? 0) > 0;
355
- this.#classNeedsActiveContainer.set(target, needsActiveContainer);
356
- }
357
- return needsActiveContainer;
358
- },
359
- getConstructorMetadata: (target) => this.#getConstructorMetadata(target),
283
+ knownPostConstruct: (target) => this.#classes.knownPostConstruct(target),
284
+ needsActiveContainer: (target) => this.#classes.needsActiveContainer(target),
285
+ getConstructorMetadata: (target) => this.#classes.constructorMetadata(target),
360
286
  lookupDependencyEntry: (token) => {
361
- const entry = this.#lookupDefaultEntry(token);
362
- return entry === null ? null : { binding: entry.binding, ownerScope: entry.owner.#scope };
287
+ const entry = this.#lookup.defaultEntry(token);
288
+ return entry === null ? null : { binding: entry.binding };
289
+ },
290
+ getResolutionFrame: (binding) => this.#getResolutionFrame(binding),
291
+ // Dispatches exactly as #resolveDep does, so an escaped dep is indistinguishable
292
+ // from the same dep on a fully interpreted resolve.
293
+ resolveEscaped: (token, options, arity, resolutionPath, resolutionStack) => {
294
+ if (arity === "all") {
295
+ return this.resolveAll(token, options, resolutionPath, resolutionStack);
296
+ }
297
+ if (arity === "optional") {
298
+ return this.resolveOptional(token, options, resolutionPath, resolutionStack);
299
+ }
300
+ if (options === undefined) {
301
+ return this.resolveFromContext(token, resolutionPath, resolutionStack);
302
+ }
303
+ return this.resolve(token, options, resolutionPath, resolutionStack);
363
304
  },
364
- resolveFallback: (token) => this.resolve(token, undefined, [], []),
365
305
  });
366
306
  resolve(token, options, resolutionPath, resolutionStack) {
367
307
  // Name-only fast lane: memoized lookup, dispatching just the shapes whose
368
308
  // semantics involve no resolution context (constants, cached singletons).
369
- if (options !== undefined &&
370
- options.name !== undefined &&
371
- options.tag === undefined &&
372
- (options.tags === undefined || options.tags.length === 0)) {
373
- const namedEntry = this.#lookupNamedEntry(token, options.name);
309
+ if (options !== undefined && isNameOnlyOptions(options)) {
310
+ const namedEntry = this.#lookup.namedEntry(token, options.name);
374
311
  if (namedEntry !== null) {
375
312
  const namedBinding = namedEntry.binding;
376
- if (namedBinding.kind === "constant" &&
377
- namedBinding.onActivation === undefined &&
378
- (this.#lifecycle.activationVersion === 0 || !this.#lifecycle.hasActivationHandlers(namedBinding.token))) {
313
+ if (this.#isPlainConstant(namedBinding)) {
379
314
  return namedBinding.value;
380
315
  }
381
- const namedScope = namedBinding.scope ?? "transient";
382
- if (namedScope === "singleton") {
383
- const cachedSingleton = namedEntry.owner.#scope.peekSingleton(namedBinding.id);
384
- if (cachedSingleton !== SINGLETON_MISS) {
385
- return cachedSingleton;
386
- }
316
+ if (namedBinding.scope === "singleton" && namedBinding.instance !== NO_INSTANCE) {
317
+ return namedBinding.instance;
387
318
  }
388
319
  // Everything else keeps the full path (context, activation, guards).
389
320
  }
390
321
  }
391
- let currentToken = token;
392
- let visitedAliasTokens;
393
- let found = this.#findBinding(currentToken, options, resolutionPath, resolutionStack);
394
- // Follow aliases iteratively with exact cycle detection — a revisited alias
395
- // token throws CircularDependencyError instead of overflowing the call stack.
396
- while (found !== undefined && found.binding.kind === "alias") {
397
- const target = found.binding.target;
398
- visitedAliasTokens ??= new Set([currentToken]);
399
- if (visitedAliasTokens.has(target)) {
400
- throw new CircularDependencyError([...visitedAliasTokens, target].map((entry) => tokenName(entry)));
401
- }
402
- visitedAliasTokens.add(target);
403
- currentToken = target;
404
- found = this.#findBinding(currentToken, options, resolutionPath, resolutionStack);
405
- }
406
- if (found === undefined) {
407
- const ownBindings = this.#registry.getAll(currentToken);
408
- if (ownBindings.length > 0) {
409
- throw new NoMatchingBindingError(this.#getTokenName(currentToken), options ?? {}, this.#getAvailableSlots(currentToken));
410
- }
411
- throw new TokenNotBoundError(this.#getTokenName(currentToken));
412
- }
413
- const { binding, owner } = found;
414
- const scope = binding.scope ?? "transient";
415
- // Singleton from a parent resolver: delegate so the parent caches it correctly
416
- if (scope === "singleton" && owner !== this) {
322
+ const { binding, owner } = this.#requireBinding(token, options, resolutionPath, resolutionStack);
323
+ // A singleton owned by a parent resolver is resolved there, so the parent caches it.
324
+ if (binding.scope === "singleton" && owner !== this) {
417
325
  return owner.#resolveBinding(binding, options, resolutionPath, resolutionStack);
418
326
  }
419
- // Scoped/transient (or own singleton): resolve with this resolver's container/scope
420
327
  return this.#resolveBinding(binding, options, resolutionPath, resolutionStack);
421
328
  }
422
329
  #resolveBinding(binding, options, resolutionPath, resolutionStack) {
423
- if (binding.kind === "constant" &&
424
- binding.onActivation === undefined &&
425
- (this.#lifecycle.activationVersion === 0 || !this.#lifecycle.hasActivationHandlers(binding.token))) {
330
+ if (this.#isPlainConstant(binding)) {
426
331
  return binding.value;
427
332
  }
428
- const scope = binding.scope ?? "transient";
429
- // Singleton cache check
333
+ const scope = binding.scope;
430
334
  if (scope === "singleton") {
431
- if (this.#scope.hasSingleton(binding.id)) {
432
- return this.#scope.getSingleton(binding.id);
335
+ if (binding.instance !== NO_INSTANCE) {
336
+ return binding.instance;
433
337
  }
434
338
  }
435
- // Scoped cache check
436
- if (scope === "scoped") {
437
- if (!this.#scope.isChild) {
438
- throw new MissingScopeContextError(this.#getTokenName(binding.token));
439
- }
440
- if (this.#scope.hasScoped(binding.id)) {
441
- return this.#scope.getScoped(binding.id);
339
+ else if (scope === "scoped") {
340
+ const cachedScoped = this.#readScoped(binding);
341
+ if (cachedScoped !== SCOPED_MISS) {
342
+ return cachedScoped;
442
343
  }
443
344
  }
444
345
  const frame = this.#getResolutionFrame(binding);
445
346
  const tokenDisplayName = frame.tokenName;
446
- const resolutionSet = enterResolutionPath(resolutionPath, tokenDisplayName, false);
347
+ const resolutionSet = enterResolutionPath(resolutionPath, tokenDisplayName);
447
348
  resolutionStack.push(frame);
448
- const needsActivation = this.#needsActivation(binding);
449
- if (!needsActivation && scope === "transient" && binding.kind === "dynamic") {
450
- const resolutionCtx = this.#acquireSyncResolutionContext(resolutionPath, resolutionStack, options);
451
- try {
349
+ try {
350
+ const needsActivation = this.#activation.needsActivation(binding);
351
+ if (!needsActivation && scope === "transient" && binding.kind === "dynamic") {
352
+ const resolutionCtx = this.#acquireSyncResolutionContext(resolutionPath, resolutionStack, options);
452
353
  const dynamicResult = binding.factory(resolutionCtx);
453
354
  if (dynamicResult instanceof Promise) {
454
- throw new AsyncResolutionError(tokenName(binding.token), tokenName(binding.token));
355
+ throw new AsyncResolutionError(tokenDisplayName, tokenDisplayName);
455
356
  }
456
- resolutionStack.pop();
457
- resolutionPath.pop();
458
- resolutionSet?.delete(tokenDisplayName);
459
357
  return dynamicResult;
460
358
  }
461
- catch (error) {
462
- resolutionStack.pop();
463
- resolutionPath.pop();
464
- resolutionSet?.delete(tokenDisplayName);
465
- throw error;
466
- }
467
- }
468
- try {
469
- const needsResolutionContext = needsActivation || this.#requiresResolutionContext(binding);
470
- const resolutionCtx = needsResolutionContext
359
+ const resolutionCtx = needsActivation || requiresResolutionContext(binding)
471
360
  ? this.#acquireSyncResolutionContext(resolutionPath, resolutionStack, options)
472
361
  : undefined;
473
362
  const instance = this.#instantiateSync(binding, resolutionCtx, resolutionPath, resolutionStack);
474
- const shouldActivate = this.#refreshActivationCacheIfNeeded(binding, needsActivation);
475
- const activated = shouldActivate
363
+ const activated = this.#activation.refreshAfterFirstInstantiation(binding, needsActivation)
476
364
  ? this.#lifecycle.runActivationSync(resolutionCtx, binding, instance, this.#metadataReader)
477
365
  : instance;
478
- // Cache by scope
479
366
  if (scope === "singleton") {
480
- this.#scope.setSingleton(binding.id, activated);
367
+ this.#scope.setSingleton(binding, activated);
481
368
  }
482
369
  else if (scope === "scoped") {
483
370
  this.#scope.setScoped(binding.id, activated);
@@ -507,12 +394,11 @@ export class DependencyResolver {
507
394
  case "dynamic-async":
508
395
  throw new AsyncResolutionError(tokenName(binding.token), tokenName(binding.token));
509
396
  case "class": {
510
- const deps = this.#resolveClassDeps(binding.target, resolutionPath, resolutionStack);
511
- const instance = this.#instantiateClass(binding.target, deps);
512
- return instance;
397
+ const deps = this.#resolveDeps(this.#constructorParams(binding.target), resolutionPath, resolutionStack);
398
+ return this.#classes.instantiate(binding.target, deps);
513
399
  }
514
400
  case "resolved": {
515
- const deps = this.#resolveDescriptorDeps(binding.deps, resolutionPath, resolutionStack);
401
+ const deps = this.#resolveDeps(binding.deps, resolutionPath, resolutionStack);
516
402
  const factoryResult = binding.factory(...deps);
517
403
  if (factoryResult instanceof Promise) {
518
404
  throw new AsyncResolutionError(tokenName(binding.token), tokenName(binding.token));
@@ -525,70 +411,51 @@ export class DependencyResolver {
525
411
  throw new InternalError("alias should have been followed before instantiation");
526
412
  }
527
413
  }
528
- #resolveClassDeps(target, resolutionPath, resolutionStack) {
529
- const meta = this.#getConstructorMetadata(target);
530
- if (meta === undefined) {
531
- if (target.length === 0) {
532
- return [];
533
- }
534
- throw new MissingMetadataError(target.name);
414
+ /**
415
+ * The parameters a class binding injects.
416
+ *
417
+ * @remarks A class the metadata reader knows nothing about is constructible only if it declares
418
+ * no parameters; anything else is a missing `@injectable()`.
419
+ */
420
+ #constructorParams(target) {
421
+ const meta = this.#classes.constructorMetadata(target);
422
+ if (meta !== undefined) {
423
+ return meta.params;
424
+ }
425
+ if (target.length === 0) {
426
+ return EMPTY_PARAM_LIST;
535
427
  }
536
- if (meta.params.length === 0) {
428
+ throw new MissingMetadataError(target.name);
429
+ }
430
+ // One dispatch table for both dependency sources — constructor params and `toResolved`
431
+ // descriptors declare the same four things.
432
+ #resolveDeps(deps, resolutionPath, resolutionStack) {
433
+ const count = deps.length;
434
+ if (count === 0) {
537
435
  return [];
538
436
  }
539
- if (meta.params.length === 1) {
540
- const param = meta.params[0];
541
- const paramOptions = injectionSlotToResolveOptions(param);
542
- if (param.multi) {
543
- return [this.resolveAll(param.token, paramOptions, resolutionPath, resolutionStack)];
544
- }
545
- if (param.optional) {
546
- return [this.resolveOptional(param.token, paramOptions, resolutionPath, resolutionStack)];
547
- }
548
- if (paramOptions === undefined) {
549
- return [this.resolveFromContext(param.token, resolutionPath, resolutionStack)];
550
- }
551
- return [this.resolve(param.token, paramOptions, resolutionPath, resolutionStack)];
552
- }
553
- const deps = new Array(meta.params.length);
554
- for (let index = 0; index < meta.params.length; index += 1) {
555
- const param = meta.params[index];
556
- const paramOptions = injectionSlotToResolveOptions(param);
557
- if (param.multi) {
558
- deps[index] = this.resolveAll(param.token, paramOptions, resolutionPath, resolutionStack);
559
- continue;
560
- }
561
- if (param.optional) {
562
- deps[index] = this.resolveOptional(param.token, paramOptions, resolutionPath, resolutionStack);
563
- continue;
564
- }
565
- deps[index] =
566
- paramOptions === undefined
567
- ? this.resolveFromContext(param.token, resolutionPath, resolutionStack)
568
- : this.resolve(param.token, paramOptions, resolutionPath, resolutionStack);
437
+ if (count === 1) {
438
+ return [this.#resolveDep(deps[0], resolutionPath, resolutionStack)];
569
439
  }
570
- return deps;
571
- }
572
- #resolveDescriptorDeps(deps, resolutionPath, resolutionStack) {
573
- const resolved = new Array(deps.length);
574
- for (let index = 0; index < deps.length; index += 1) {
575
- const dep = deps[index];
576
- const depOptions = injectionSlotToResolveOptions(dep);
577
- if (dep.multi) {
578
- resolved[index] = this.resolveAll(dep.token, depOptions, resolutionPath, resolutionStack);
579
- continue;
580
- }
581
- if (dep.optional) {
582
- resolved[index] = this.resolveOptional(dep.token, depOptions, resolutionPath, resolutionStack);
583
- continue;
584
- }
585
- resolved[index] =
586
- depOptions === undefined
587
- ? this.resolveFromContext(dep.token, resolutionPath, resolutionStack)
588
- : this.resolve(dep.token, depOptions, resolutionPath, resolutionStack);
440
+ const resolved = new Array(count);
441
+ for (let index = 0; index < count; index += 1) {
442
+ resolved[index] = this.#resolveDep(deps[index], resolutionPath, resolutionStack);
589
443
  }
590
444
  return resolved;
591
445
  }
446
+ #resolveDep(dep, resolutionPath, resolutionStack) {
447
+ const options = injectionSlotToResolveOptions(dep);
448
+ if (dep.multi) {
449
+ return this.resolveAll(dep.token, options, resolutionPath, resolutionStack);
450
+ }
451
+ if (dep.optional) {
452
+ return this.resolveOptional(dep.token, options, resolutionPath, resolutionStack);
453
+ }
454
+ if (options === undefined) {
455
+ return this.resolveFromContext(dep.token, resolutionPath, resolutionStack);
456
+ }
457
+ return this.resolve(dep.token, options, resolutionPath, resolutionStack);
458
+ }
592
459
  resolveOptional(token, options, resolutionPath, resolutionStack) {
593
460
  if (this.#findBinding(token, options, resolutionPath, resolutionStack) === undefined) {
594
461
  return undefined;
@@ -596,31 +463,32 @@ export class DependencyResolver {
596
463
  return this.resolve(token, options, resolutionPath, resolutionStack);
597
464
  }
598
465
  resolveAll(token, options, resolutionPath, resolutionStack) {
599
- if (options?.name !== undefined && options.tag === undefined && (options.tags?.length ?? 0) === 0) {
600
- const namedCandidates = this.#getSimpleNamedBindingsFromChain(token, options.name);
601
- if (namedCandidates.length === 0) {
602
- return [];
603
- }
604
- const resolved = new Array(namedCandidates.length);
605
- for (let index = 0; index < namedCandidates.length; index += 1) {
606
- resolved[index] = this.#resolveCandidateSync(namedCandidates[index], options, resolutionPath, resolutionStack);
607
- }
608
- return resolved;
609
- }
610
- const allBindings = this.#getAllBindingsFromChain(token);
611
- if (allBindings.length === 0) {
612
- return [];
613
- }
614
- const ctx = this.#makeConstraintContext(resolutionPath, resolutionStack, options);
615
- const candidates = selectAllBindings(allBindings, options, ctx);
466
+ const candidates = this.#candidateBindings(token, options, resolutionPath, resolutionStack);
616
467
  const resolved = new Array(candidates.length);
617
468
  for (let index = 0; index < candidates.length; index += 1) {
618
469
  resolved[index] = this.#resolveCandidateSync(candidates[index], options, resolutionPath, resolutionStack);
619
470
  }
620
471
  return resolved;
621
472
  }
473
+ /** 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;
481
+ }
482
+ return selectAllBindings(named, options, this.#makeConstraintContext(resolutionPath, resolutionStack, options));
483
+ }
484
+ const allBindings = this.#allBindingsFromChain(token);
485
+ if (allBindings.length === 0) {
486
+ return allBindings;
487
+ }
488
+ return selectAllBindings(allBindings, options, this.#makeConstraintContext(resolutionPath, resolutionStack, options));
489
+ }
622
490
  // ── Async resolve ──────────────────────────────────────────────────────────
623
- resolveAsyncFromContext(token, resolutionPath, resolutionStack) {
491
+ resolveAsyncFromContext(token, resolutionPath, resolutionStack, branchDepth) {
624
492
  // Hot lane: own-registry fast default (async chains resolve sibling dynamic bindings).
625
493
  // Fall back to the chain-versioned memo only on miss or alias.
626
494
  const fastBinding = this.#registry.getFastDefault(token);
@@ -628,174 +496,118 @@ export class DependencyResolver {
628
496
  // Inline the dominant chain shape — transient dynamic factory with no activation.
629
497
  if ((fastBinding.kind === "dynamic-async" || fastBinding.kind === "dynamic") &&
630
498
  fastBinding.scope === "transient" &&
631
- fastBinding.onActivation === undefined &&
632
- (this.#lifecycle.activationVersion === 0 || !this.#lifecycle.hasActivationHandlers(fastBinding.token))) {
633
- return this.#resolveTransientDynamicAsyncFromContext(fastBinding, resolutionPath, resolutionStack);
499
+ !this.#hasAnyActivation(fastBinding)) {
500
+ return this.#resolveTransientDynamicAsyncFromContext(fastBinding, resolutionPath, resolutionStack, branchDepth);
634
501
  }
635
- return this.#resolveAsyncDefaultEntry(fastBinding, this, resolutionPath, resolutionStack);
502
+ return this.#resolveAsyncDefaultEntry(fastBinding, this, resolutionPath, resolutionStack, branchDepth);
636
503
  }
637
- const entry = this.#lookupDefaultEntry(token);
504
+ const entry = this.#lookup.defaultEntry(token);
638
505
  if (entry === null) {
639
- return this.resolveAsync(token, undefined, resolutionPath, resolutionStack);
506
+ return this.resolveAsync(token, undefined, resolutionPath, resolutionStack, branchDepth);
640
507
  }
641
- return this.#resolveAsyncDefaultEntry(entry.binding, entry.owner, resolutionPath, resolutionStack);
508
+ return this.#resolveAsyncDefaultEntry(entry.binding, entry.owner, resolutionPath, resolutionStack, branchDepth);
642
509
  }
643
- #resolveAsyncDefaultEntry(binding, owner, resolutionPath, resolutionStack) {
644
- if (binding.kind === "constant" &&
645
- binding.onActivation === undefined &&
646
- (this.#lifecycle.activationVersion === 0 || !this.#lifecycle.hasActivationHandlers(binding.token))) {
510
+ #resolveAsyncDefaultEntry(binding, owner, resolutionPath, resolutionStack, branchDepth) {
511
+ if (this.#isPlainConstant(binding)) {
647
512
  return Promise.resolve(binding.value);
648
513
  }
649
- const scope = binding.scope ?? "transient";
514
+ const scope = binding.scope;
650
515
  if (scope === "transient") {
651
- if ((binding.kind === "dynamic" || binding.kind === "dynamic-async") &&
652
- binding.onActivation === undefined &&
653
- (this.#lifecycle.activationVersion === 0 || !this.#lifecycle.hasActivationHandlers(binding.token))) {
654
- return this.#resolveTransientDynamicAsyncFromContext(binding, resolutionPath, resolutionStack);
516
+ if ((binding.kind === "dynamic" || binding.kind === "dynamic-async") && !this.#hasAnyActivation(binding)) {
517
+ return this.#resolveTransientDynamicAsyncFromContext(binding, resolutionPath, resolutionStack, branchDepth);
655
518
  }
656
519
  }
657
520
  else if (scope === "singleton") {
658
- const cachedSingleton = owner.#scope.peekSingleton(binding.id);
659
- if (cachedSingleton !== SINGLETON_MISS) {
660
- return Promise.resolve(cachedSingleton);
521
+ if (binding.instance !== NO_INSTANCE) {
522
+ return Promise.resolve(binding.instance);
661
523
  }
662
524
  if (owner !== this) {
663
- return owner.#resolveBindingAsync(binding, undefined, resolutionPath, resolutionStack);
525
+ return owner.#resolveBindingAsync(binding, undefined, resolutionPath, resolutionStack, branchDepth);
664
526
  }
665
527
  }
666
- else {
667
- if (!this.#scope.isChild) {
668
- return Promise.reject(new MissingScopeContextError(this.#getTokenName(binding.token)));
669
- }
528
+ else if (this.#scope.isChild) {
670
529
  if (this.#scope.hasScoped(binding.id)) {
671
530
  return Promise.resolve(this.#scope.getScoped(binding.id));
672
531
  }
673
532
  }
674
- return this.#resolveBindingAsync(binding, undefined, resolutionPath, resolutionStack);
675
- }
676
- async resolveAsync(token, options, resolutionPath, resolutionStack) {
677
- const found = this.#findBinding(token, options, resolutionPath, resolutionStack);
678
- if (found === undefined) {
679
- const ownBindings = this.#registry.getAll(token);
680
- if (ownBindings.length > 0) {
681
- throw new NoMatchingBindingError(this.#getTokenName(token), options ?? {}, this.#getAvailableSlots(token));
682
- }
683
- throw new TokenNotBoundError(this.#getTokenName(token));
684
- }
685
- let currentToken = token;
686
- let visitedAliasTokens;
687
- let aliasFollowed = found;
688
- while (aliasFollowed !== undefined && aliasFollowed.binding.kind === "alias") {
689
- const target = aliasFollowed.binding.target;
690
- visitedAliasTokens ??= new Set([currentToken]);
691
- if (visitedAliasTokens.has(target)) {
692
- throw new CircularDependencyError([...visitedAliasTokens, target].map((entry) => tokenName(entry)));
693
- }
694
- visitedAliasTokens.add(target);
695
- currentToken = target;
696
- aliasFollowed = this.#findBinding(currentToken, options, resolutionPath, resolutionStack);
697
- }
698
- if (aliasFollowed === undefined) {
699
- const ownBindings = this.#registry.getAll(currentToken);
700
- if (ownBindings.length > 0) {
701
- throw new NoMatchingBindingError(this.#getTokenName(currentToken), options ?? {}, this.#getAvailableSlots(currentToken));
702
- }
703
- throw new TokenNotBoundError(this.#getTokenName(currentToken));
533
+ else {
534
+ // Not `#readScoped`: this entry point reports failure as a rejection, never a sync throw.
535
+ return Promise.reject(new MissingScopeContextError(tokenName(binding.token)));
704
536
  }
705
- const { binding, owner } = aliasFollowed;
706
- const scope = binding.scope ?? "transient";
707
- if (scope === "singleton" && owner !== this) {
708
- return owner.#resolveBindingAsync(binding, options, resolutionPath, resolutionStack);
537
+ return this.#resolveBindingAsync(binding, undefined, resolutionPath, resolutionStack, branchDepth);
538
+ }
539
+ async resolveAsync(token, options, resolutionPath, resolutionStack, branchDepth = UNOWNED_BRANCH) {
540
+ const { binding, owner } = this.#requireBinding(token, options, resolutionPath, resolutionStack);
541
+ if (binding.scope === "singleton" && owner !== this) {
542
+ return owner.#resolveBindingAsync(binding, options, resolutionPath, resolutionStack, branchDepth);
709
543
  }
710
- return this.#resolveBindingAsync(binding, options, resolutionPath, resolutionStack);
544
+ return this.#resolveBindingAsync(binding, options, resolutionPath, resolutionStack, branchDepth);
711
545
  }
712
- async #resolveBindingAsync(binding, options, resolutionPath, resolutionStack) {
713
- if (binding.kind === "constant" &&
714
- binding.onActivation === undefined &&
715
- !this.#lifecycle.hasActivationHandlers(binding.token)) {
546
+ async #resolveBindingAsync(binding, options, resolutionPath, resolutionStack, branchDepth) {
547
+ if (this.#isPlainConstant(binding)) {
716
548
  return binding.value;
717
549
  }
718
- const scope = binding.scope ?? "transient";
719
- // Singleton cache
550
+ const scope = binding.scope;
720
551
  if (scope === "singleton") {
721
- if (this.#scope.hasSingleton(binding.id)) {
722
- return this.#scope.getSingleton(binding.id);
552
+ if (binding.instance !== NO_INSTANCE) {
553
+ return binding.instance;
723
554
  }
724
- // In-flight dedup
555
+ // In-flight dedup: concurrent callers share the first creation.
725
556
  const inflight = this.#scope.getInflight(binding.id);
726
557
  if (inflight !== undefined) {
727
558
  return inflight;
728
559
  }
729
560
  }
730
- // Scoped cache
731
- if (scope === "scoped") {
732
- if (!this.#scope.isChild) {
733
- throw new MissingScopeContextError(this.#getTokenName(binding.token));
734
- }
735
- if (this.#scope.hasScoped(binding.id)) {
736
- return this.#scope.getScoped(binding.id);
561
+ else if (scope === "scoped") {
562
+ const cachedScoped = this.#readScoped(binding);
563
+ if (cachedScoped !== SCOPED_MISS) {
564
+ return cachedScoped;
737
565
  }
738
566
  }
739
567
  const frame = this.#getResolutionFrame(binding);
740
- const frameName = frame.tokenName;
741
- const resolutionSet = enterResolutionPath(resolutionPath, frameName, false);
742
- resolutionStack.push(frame);
743
- const needsActivation = this.#needsActivation(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);
744
573
  if (!needsActivation && scope === "transient" && (binding.kind === "dynamic" || binding.kind === "dynamic-async")) {
745
- const resolutionCtx = new DefaultResolutionContext(this, resolutionPath, resolutionStack, options);
746
- try {
747
- if (binding.kind === "dynamic-async") {
748
- return await binding.factory(resolutionCtx);
749
- }
750
- const dynamicResult = binding.factory(resolutionCtx);
751
- return dynamicResult instanceof Promise ? await dynamicResult : dynamicResult;
752
- }
753
- finally {
754
- resolutionStack.pop();
755
- resolutionPath.pop();
756
- resolutionSet?.delete(frameName);
574
+ const resolutionCtx = new AsyncLevelContext(this, levelPath, levelStack, options);
575
+ if (binding.kind === "dynamic-async") {
576
+ return await binding.factory(resolutionCtx);
757
577
  }
578
+ const dynamicResult = binding.factory(resolutionCtx);
579
+ return dynamicResult instanceof Promise ? await dynamicResult : dynamicResult;
758
580
  }
759
- const needsResolutionContext = needsActivation || this.#requiresResolutionContext(binding);
760
- const resolutionCtx = needsResolutionContext
761
- ? new DefaultResolutionContext(this, resolutionPath, resolutionStack, options)
581
+ const resolutionCtx = needsActivation || requiresResolutionContext(binding)
582
+ ? new AsyncLevelContext(this, levelPath, levelStack, options)
762
583
  : undefined;
763
- try {
764
- if (scope === "singleton") {
765
- const createSingletonPromise = async () => {
766
- const instance = await this.#instantiateAsync(binding, resolutionCtx, resolutionPath, resolutionStack);
767
- const shouldActivate = this.#refreshActivationCacheIfNeeded(binding, needsActivation);
768
- const activated = shouldActivate
769
- ? await this.#lifecycle.runActivation(resolutionCtx, binding, instance, this.#metadataReader)
770
- : instance;
771
- this.#scope.setSingleton(binding.id, activated);
772
- this.#scope.clearInflight(binding.id);
773
- return activated;
774
- };
775
- const singletonPromise = createSingletonPromise().catch((err) => {
776
- this.#scope.clearInflight(binding.id);
777
- throw err;
778
- });
779
- this.#scope.setInflight(binding.id, singletonPromise);
780
- return await singletonPromise;
781
- }
782
- const instance = await this.#instantiateAsync(binding, resolutionCtx, resolutionPath, resolutionStack);
783
- const shouldActivate = this.#refreshActivationCacheIfNeeded(binding, needsActivation);
784
- const activated = shouldActivate
785
- ? await this.#lifecycle.runActivation(resolutionCtx, binding, instance, this.#metadataReader)
786
- : instance;
787
- if (scope === "scoped") {
788
- this.#scope.setScoped(binding.id, activated);
789
- }
790
- return activated;
584
+ if (scope === "singleton") {
585
+ // 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) => {
587
+ this.#scope.setSingleton(binding, activated);
588
+ this.#scope.clearInflight(binding.id);
589
+ return activated;
590
+ }, (error) => {
591
+ this.#scope.clearInflight(binding.id);
592
+ throw error;
593
+ });
594
+ this.#scope.setInflight(binding.id, singletonPromise);
595
+ return await singletonPromise;
791
596
  }
792
- finally {
793
- resolutionStack.pop();
794
- resolutionPath.pop();
795
- resolutionSet?.delete(frameName);
597
+ const activated = await this.#instantiateAndActivateAsync(binding, resolutionCtx, levelPath, levelStack, levelDepth, needsActivation);
598
+ if (scope === "scoped") {
599
+ this.#scope.setScoped(binding.id, activated);
796
600
  }
601
+ return activated;
797
602
  }
798
- async #instantiateAsync(binding, ctx, resolutionPath, resolutionStack) {
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)) {
606
+ return instance;
607
+ }
608
+ return this.#lifecycle.runActivation(ctx, binding, instance, this.#metadataReader);
609
+ }
610
+ async #instantiateAsync(binding, ctx, resolutionPath, resolutionStack, branchDepth) {
799
611
  switch (binding.kind) {
800
612
  case "constant":
801
613
  return binding.value;
@@ -812,260 +624,151 @@ export class DependencyResolver {
812
624
  }
813
625
  return binding.factory(ctx);
814
626
  case "class": {
815
- const deps = await this.#resolveClassDepsAsync(binding.target, resolutionPath, resolutionStack);
816
- const instance = this.#instantiateClass(binding.target, deps);
817
- return instance;
627
+ const deps = await this.#resolveDepsAsync(this.#constructorParams(binding.target), resolutionPath, resolutionStack, branchDepth);
628
+ return this.#classes.instantiate(binding.target, deps);
818
629
  }
819
630
  case "resolved": {
820
631
  if (ctx === undefined) {
821
632
  throw new InternalError("resolved binding requires resolution context");
822
633
  }
823
- const deps = await this.#resolveDescriptorDepsAsync(binding.deps, resolutionPath, resolutionStack);
634
+ const deps = await this.#resolveDepsAsync(binding.deps, resolutionPath, resolutionStack, branchDepth);
824
635
  const factoryResult = binding.factory(...deps);
825
636
  return factoryResult instanceof Promise ? factoryResult : Promise.resolve(factoryResult);
826
637
  }
827
638
  case "resolved-async": {
828
- const deps = await this.#resolveDescriptorDepsAsync(binding.deps, resolutionPath, resolutionStack);
639
+ const deps = await this.#resolveDepsAsync(binding.deps, resolutionPath, resolutionStack, branchDepth);
829
640
  return binding.factory(...deps);
830
641
  }
831
642
  case "alias":
832
643
  throw new InternalError("alias should have been followed before instantiation");
833
644
  }
834
645
  }
835
- async #resolveClassDepsAsync(target, resolutionPath, resolutionStack) {
836
- const meta = this.#getConstructorMetadata(target);
837
- if (meta === undefined) {
838
- if (target.length === 0) {
839
- return [];
840
- }
841
- throw new MissingMetadataError(target.name);
842
- }
843
- if (meta.params.length === 0) {
646
+ async #resolveDepsAsync(deps, resolutionPath, resolutionStack, branchDepth) {
647
+ const count = deps.length;
648
+ if (count === 0) {
844
649
  return [];
845
650
  }
846
- if (meta.params.length === 1) {
847
- const param = meta.params[0];
848
- const paramOptions = injectionSlotToResolveOptions(param);
849
- if (param.multi) {
850
- return [await this.resolveAllAsync(param.token, paramOptions, resolutionPath, resolutionStack)];
851
- }
852
- if (param.optional) {
853
- return [await this.resolveOptionalAsync(param.token, paramOptions, resolutionPath, resolutionStack)];
854
- }
855
- if (paramOptions === undefined) {
856
- return [await this.resolveAsyncFromContext(param.token, resolutionPath, resolutionStack)];
857
- }
858
- return [await this.resolveAsync(param.token, paramOptions, resolutionPath, resolutionStack)];
859
- }
860
- const pending = new Array(meta.params.length);
861
- const shouldCloneContext = meta.params.length > 1;
862
- for (let index = 0; index < meta.params.length; index += 1) {
863
- const param = meta.params[index];
864
- const paramOptions = injectionSlotToResolveOptions(param);
865
- if (param.multi) {
866
- pending[index] = this.resolveAllAsync(param.token, paramOptions, shouldCloneContext ? [...resolutionPath] : resolutionPath, shouldCloneContext ? [...resolutionStack] : resolutionStack);
867
- }
868
- else if (param.optional) {
869
- pending[index] = this.resolveOptionalAsync(param.token, paramOptions, shouldCloneContext ? [...resolutionPath] : resolutionPath, shouldCloneContext ? [...resolutionStack] : resolutionStack);
870
- }
871
- else {
872
- pending[index] =
873
- paramOptions === undefined
874
- ? this.resolveAsyncFromContext(param.token, shouldCloneContext ? [...resolutionPath] : resolutionPath, shouldCloneContext ? [...resolutionStack] : resolutionStack)
875
- : this.resolveAsync(param.token, paramOptions, shouldCloneContext ? [...resolutionPath] : resolutionPath, shouldCloneContext ? [...resolutionStack] : resolutionStack);
876
- }
651
+ if (count === 1) {
652
+ return [await this.#resolveDepAsync(deps[0], resolutionPath, resolutionStack, branchDepth)];
653
+ }
654
+ // Siblings resolve concurrently and each extends the same branch, so the first appends in
655
+ // place and the rest copy the prefix — no caller has to isolate them.
656
+ const pending = new Array(count);
657
+ for (let index = 0; index < count; index += 1) {
658
+ pending[index] = this.#resolveDepAsync(deps[index], resolutionPath, resolutionStack, branchDepth);
877
659
  }
878
660
  return Promise.all(pending);
879
661
  }
880
- async #resolveDescriptorDepsAsync(deps, resolutionPath, resolutionStack) {
881
- const pending = new Array(deps.length);
882
- const shouldCloneContext = deps.length > 1;
883
- for (let index = 0; index < deps.length; index += 1) {
884
- const dep = deps[index];
885
- const depOptions = injectionSlotToResolveOptions(dep);
886
- if (dep.multi) {
887
- pending[index] = this.resolveAllAsync(dep.token, depOptions, shouldCloneContext ? [...resolutionPath] : resolutionPath, shouldCloneContext ? [...resolutionStack] : resolutionStack);
888
- }
889
- else if (dep.optional) {
890
- pending[index] = this.resolveOptionalAsync(dep.token, depOptions, shouldCloneContext ? [...resolutionPath] : resolutionPath, shouldCloneContext ? [...resolutionStack] : resolutionStack);
891
- }
892
- else {
893
- pending[index] =
894
- depOptions === undefined
895
- ? this.resolveAsyncFromContext(dep.token, shouldCloneContext ? [...resolutionPath] : resolutionPath, shouldCloneContext ? [...resolutionStack] : resolutionStack)
896
- : this.resolveAsync(dep.token, depOptions, shouldCloneContext ? [...resolutionPath] : resolutionPath, shouldCloneContext ? [...resolutionStack] : resolutionStack);
897
- }
662
+ #resolveDepAsync(dep, resolutionPath, resolutionStack, branchDepth) {
663
+ const options = injectionSlotToResolveOptions(dep);
664
+ if (dep.multi) {
665
+ return this.resolveAllAsync(dep.token, options, resolutionPath, resolutionStack, branchDepth);
898
666
  }
899
- return Promise.all(pending);
667
+ if (dep.optional) {
668
+ return this.resolveOptionalAsync(dep.token, options, resolutionPath, resolutionStack, branchDepth);
669
+ }
670
+ if (options === undefined) {
671
+ return this.resolveAsyncFromContext(dep.token, resolutionPath, resolutionStack, branchDepth);
672
+ }
673
+ return this.resolveAsync(dep.token, options, resolutionPath, resolutionStack, branchDepth);
900
674
  }
901
- async resolveOptionalAsync(token, options, resolutionPath, resolutionStack) {
675
+ async resolveOptionalAsync(token, options, resolutionPath, resolutionStack, branchDepth = UNOWNED_BRANCH) {
902
676
  if (this.#findBinding(token, options, resolutionPath, resolutionStack) === undefined) {
903
677
  return undefined;
904
678
  }
905
- return this.resolveAsync(token, options, resolutionPath, resolutionStack);
679
+ return this.resolveAsync(token, options, resolutionPath, resolutionStack, branchDepth);
906
680
  }
907
- async resolveAllAsync(token, options, resolutionPath, resolutionStack) {
908
- if (options?.name !== undefined && options.tag === undefined && (options.tags?.length ?? 0) === 0) {
909
- const namedCandidates = this.#getSimpleNamedBindingsFromChain(token, options.name);
910
- if (namedCandidates.length === 0) {
911
- return [];
912
- }
913
- const pending = new Array(namedCandidates.length);
914
- for (let index = 0; index < namedCandidates.length; index += 1) {
915
- pending[index] = this.#resolveCandidateAsync(namedCandidates[index], options, resolutionPath, resolutionStack);
916
- }
917
- return Promise.all(pending);
918
- }
919
- const allBindings = this.#getAllBindingsFromChain(token);
920
- if (allBindings.length === 0) {
921
- return [];
922
- }
923
- const ctx = this.#makeConstraintContext(resolutionPath, resolutionStack, options);
924
- const candidates = selectAllBindings(allBindings, options, ctx);
681
+ async resolveAllAsync(token, options, resolutionPath, resolutionStack, branchDepth = UNOWNED_BRANCH) {
682
+ const candidates = this.#candidateBindings(token, options, resolutionPath, resolutionStack);
925
683
  const pending = new Array(candidates.length);
926
684
  for (let index = 0; index < candidates.length; index += 1) {
927
- pending[index] = this.#resolveCandidateAsync(candidates[index], options, resolutionPath, resolutionStack);
685
+ pending[index] = this.#resolveCandidateAsync(candidates[index], options, resolutionPath, resolutionStack, branchDepth);
928
686
  }
929
687
  return Promise.all(pending);
930
688
  }
931
689
  // ── Helpers ────────────────────────────────────────────────────────────────
932
- #getAllBindingsFromChain(token) {
690
+ #allBindingsFromChain(token) {
933
691
  const ownBindings = this.#registry.getAll(token);
934
692
  if (this.#parent === undefined) {
935
693
  return ownBindings;
936
694
  }
937
695
  const result = [...ownBindings];
938
- let current = this.#parent;
939
- while (current !== undefined) {
696
+ for (let current = this.#parent; current !== undefined; current = current.#parent) {
940
697
  const own = current.#registry.getAll(token);
941
698
  if (own.length > 0) {
942
699
  result.push(...own);
943
700
  }
944
- current = current.#parent;
945
701
  }
946
702
  return result;
947
703
  }
948
- #getSimpleNamedBindingsFromChain(token, name) {
704
+ /** Every binding the chain's name indexes hold for one name, nearest container first. */
705
+ #namedBindingsFromChain(token, name) {
706
+ // A name resolves to at most one binding per registry, so a root container's answer is built
707
+ // whole rather than grown — the list is sized at its allocation.
949
708
  const ownBinding = this.#registry.getSimpleNamed(token, name);
950
709
  if (this.#parent === undefined) {
951
- return ownBinding !== undefined ? [ownBinding] : [];
952
- }
953
- const result = [];
954
- if (ownBinding !== undefined) {
955
- result.push(ownBinding);
710
+ return ownBinding === undefined ? [] : [ownBinding];
956
711
  }
957
- let current = this.#parent;
958
- while (current !== undefined) {
712
+ const result = ownBinding === undefined ? [] : [ownBinding];
713
+ for (let current = this.#parent; current !== undefined; current = current.#parent) {
959
714
  const binding = current.#registry.getSimpleNamed(token, name);
960
715
  if (binding !== undefined) {
961
716
  result.push(binding);
962
717
  }
963
- current = current.#parent;
964
718
  }
965
719
  return result;
966
720
  }
967
- #getAvailableSlots(token) {
968
- return this.#registry.availableSlotStrings(token);
721
+ /** A constant with no activation anywhere resolves to its value with no pipeline at all. */
722
+ #isPlainConstant(binding) {
723
+ return (binding.kind === "constant" &&
724
+ binding.onActivation === undefined &&
725
+ (this.#lifecycle.activationVersion === 0 || !this.#lifecycle.hasActivationHandlers(binding.token)));
726
+ }
727
+ /** Whether either an own hook or a container-level hook would run for this binding. */
728
+ #hasAnyActivation(binding) {
729
+ if (binding.onActivation !== undefined) {
730
+ return true;
731
+ }
732
+ return this.#lifecycle.activationVersion !== 0 && this.#lifecycle.hasActivationHandlers(binding.token);
733
+ }
734
+ /**
735
+ * The cached instance of a `scoped` binding, or {@link SCOPED_MISS}.
736
+ *
737
+ * @remarks A `scoped` binding outside a child container is a configuration error, not a miss, so
738
+ * the check lives with the read that depends on it.
739
+ */
740
+ #readScoped(binding) {
741
+ if (!this.#scope.isChild) {
742
+ throw new MissingScopeContextError(tokenName(binding.token));
743
+ }
744
+ if (this.#scope.hasScoped(binding.id)) {
745
+ return this.#scope.getScoped(binding.id);
746
+ }
747
+ return SCOPED_MISS;
969
748
  }
970
749
  #makeConstraintContext(resolutionPath, resolutionStack, options) {
971
750
  if (options === undefined && resolutionPath.length === 0 && resolutionStack.length === 0) {
972
751
  return ROOT_CONSTRAINT_CONTEXT;
973
752
  }
974
- const parent = resolutionStack.at(-1);
975
- const ancestors = resolutionStack.length > 1 ? resolutionStack.slice(0, -1) : [];
976
753
  return {
977
754
  resolutionPath,
978
755
  resolutionStack,
979
- parent,
980
- ancestors,
756
+ parent: resolutionStack.at(-1),
757
+ ancestors: resolutionStack.length > 1 ? resolutionStack.slice(0, -1) : [],
981
758
  currentResolveOptions: options,
982
759
  };
983
760
  }
984
761
  #matchesBindingFast(binding, options, resolutionPath, resolutionStack) {
985
- if (!this.#matchesSlotFast(binding.slot, options)) {
762
+ if (!matchesSlot(binding.slot, options)) {
986
763
  return false;
987
764
  }
988
765
  if (binding.predicate === undefined) {
989
766
  return true;
990
767
  }
991
- const ctx = this.#makeConstraintContext(resolutionPath, resolutionStack, options);
992
- return binding.predicate(ctx);
993
- }
994
- #matchesSlotFast(slot, options) {
995
- const requestedName = options?.name;
996
- const requestedTags = options?.tags;
997
- const singleRequestedTag = options?.tag;
998
- const hasRequestedTags = (requestedTags?.length ?? 0) > 0 || singleRequestedTag !== undefined;
999
- if (slot.name !== undefined) {
1000
- if (requestedName === undefined || slot.name !== requestedName) {
1001
- return false;
1002
- }
1003
- }
1004
- else if (requestedName !== undefined) {
1005
- return false;
1006
- }
1007
- if (slot.tags.length > 0) {
1008
- if (!hasRequestedTags) {
1009
- return false;
1010
- }
1011
- for (const [tagKey, tagValue] of slot.tags) {
1012
- if (!this.#matchesRequestedTag(tagKey, tagValue, requestedTags, singleRequestedTag)) {
1013
- return false;
1014
- }
1015
- }
1016
- }
1017
- else if (hasRequestedTags) {
1018
- return false;
1019
- }
1020
- return true;
1021
- }
1022
- #getTokenName(token) {
1023
- return tokenName(token);
1024
- }
1025
- #getConstructorMetadata(target) {
1026
- const cached = this.#classConstructorMetadata.get(target);
1027
- if (cached !== undefined) {
1028
- return cached === null ? undefined : cached;
1029
- }
1030
- const metadata = this.#metadataReader.getConstructorMetadata(target);
1031
- this.#classConstructorMetadata.set(target, metadata ?? null);
1032
- return metadata;
1033
- }
1034
- #instantiateClass(target, deps) {
1035
- let needsActiveContainer = this.#classNeedsActiveContainer.get(target);
1036
- if (needsActiveContainer === undefined) {
1037
- const accessorMetadata = this.#metadataReader.getAccessorMetadata?.(target);
1038
- needsActiveContainer = (accessorMetadata?.length ?? 0) > 0;
1039
- this.#classNeedsActiveContainer.set(target, needsActiveContainer);
1040
- }
1041
- const invokable = target;
1042
- if (!needsActiveContainer) {
1043
- return new invokable(...deps);
1044
- }
1045
- return runWithContainer(this.#container, () => new invokable(...deps));
1046
- }
1047
- #matchesRequestedTag(tagKey, tagValue, requestedTags, singleRequestedTag) {
1048
- if (singleRequestedTag !== undefined &&
1049
- singleRequestedTag[0] === tagKey &&
1050
- Object.is(singleRequestedTag[1], tagValue)) {
1051
- return true;
1052
- }
1053
- if (requestedTags === undefined || requestedTags.length === 0) {
1054
- return false;
1055
- }
1056
- for (let index = 0; index < requestedTags.length; index += 1) {
1057
- const requestedTag = requestedTags[index];
1058
- if (requestedTag[0] === tagKey && Object.is(requestedTag[1], tagValue)) {
1059
- return true;
1060
- }
1061
- }
1062
- return false;
768
+ return binding.predicate(this.#makeConstraintContext(resolutionPath, resolutionStack, options));
1063
769
  }
1064
770
  #resolveTransientDynamicSyncFromContext(binding, resolutionPath, resolutionStack) {
1065
- // One lane at every depth. The separate deep lane existed because cycle detection used to be
1066
- // an O(depth) `resolutionPath.includes()` scan, which had to be escaped past ~32 levels; with
1067
- // the O(1) `binding.inFlight` mark there is nothing to escape, so the depth split — and the
1068
- // divergent behaviour it caused — is gone.
771
+ // One lane at every depth: `binding.inFlight` is O(1), so there is nothing to escape.
1069
772
  const frame = this.#getResolutionFrame(binding);
1070
773
  const tokenDisplayName = frame.tokenName;
1071
774
  if (binding.inFlight) {
@@ -1088,150 +791,122 @@ export class DependencyResolver {
1088
791
  binding.inFlight = false;
1089
792
  }
1090
793
  }
1091
- // NOT declared `async` — avoids creating a JSAsyncGeneratorObject + implicit Promise wrapper on
1092
- // every invocation. Cleanup is handled via .then(onFulfilled, onRejected) so the behaviour is
1093
- // identical to a try/finally but without the async machinery overhead.
1094
- #resolveTransientDynamicAsyncFromContext(binding, resolutionPath, resolutionStack) {
1095
- // One lane at every depth. Cycle detection goes through `enterResolutionPath`, which is
1096
- // path-scoped — the only mechanism that stays correct when chains interleave (Promise.all) —
1097
- // and adapts on its own: a linear scan while the path is short, an attached Set past
1098
- // RESOLUTION_SET_THRESHOLD. That removes the depth split, and with it a silent change of
1099
- // behaviour (context identity, stack frames, promise shape) at the old threshold.
1100
- //
1101
- // For a sequential chain every level shares one resolutionPath/resolutionStack, so a single
1102
- // DefaultResolutionContext serves the whole chain: inner levels of the owning chain allocate
1103
- // nothing. A concurrent chain is detected by path identity and gets its own context.
794
+ // Deliberately not `async`: that would allocate a state machine and a promise per level.
795
+ #resolveTransientDynamicAsyncFromContext(binding, resolutionPath, resolutionStack, branchDepth) {
1104
796
  const frame = this.#getResolutionFrame(binding);
1105
- const tokenDisplayName = frame.tokenName;
797
+ let levelPath;
1106
798
  try {
1107
- enterResolutionPath(resolutionPath, tokenDisplayName, false);
799
+ levelPath = extendResolutionBranch(resolutionPath, branchDepth, frame.tokenName);
1108
800
  }
1109
801
  catch (cycleError) {
1110
802
  // This method is not `async`; keep failures as rejections rather than sync throws.
1111
803
  return Promise.reject(cycleError);
1112
804
  }
1113
- let ctx;
1114
- let isOwnerLevel;
1115
- if (this.#asyncChainCtxPath === resolutionPath) {
1116
- ctx = this.#asyncChainCtx;
1117
- isOwnerLevel = true;
1118
- }
1119
- else if (this.#asyncChainCtxPath === undefined) {
1120
- const existing = this.#asyncChainCtx;
1121
- if (existing === undefined) {
1122
- ctx = new DefaultResolutionContext(this, resolutionPath, resolutionStack, undefined);
1123
- this.#asyncChainCtx = ctx;
1124
- }
1125
- else {
1126
- existing.reset(this, resolutionPath, resolutionStack, undefined);
1127
- ctx = existing;
805
+ const levelStack = extendResolutionStackBranch(resolutionStack, branchDepth, frame);
806
+ // Nothing this level appended is ever removed, so no level observes its own settlement.
807
+ const ctx = new AsyncLevelContext(this, levelPath, levelStack, undefined);
808
+ try {
809
+ if (binding.kind === "dynamic-async") {
810
+ return binding.factory(ctx);
1128
811
  }
1129
- this.#asyncChainCtxPath = resolutionPath;
1130
- isOwnerLevel = true;
812
+ const factoryResult = binding.factory(ctx);
813
+ return factoryResult instanceof Promise ? factoryResult : Promise.resolve(factoryResult);
1131
814
  }
1132
- else {
1133
- ctx = new DefaultResolutionContext(this, resolutionPath, resolutionStack, undefined);
1134
- isOwnerLevel = false;
815
+ catch (factoryError) {
816
+ return Promise.reject(factoryError);
817
+ }
818
+ }
819
+ // ── The cascade lane ───────────────────────────────────────────────────────
820
+ /**
821
+ * Entry for a request a factory makes from inside an open synchronous cascade.
822
+ *
823
+ * @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`.
825
+ */
826
+ resolveAsyncFromCascade(token) {
827
+ if (this.#cascadePath.length === 0) {
828
+ return this.resolveAsyncFromContext(token, [], [], ROOT_BRANCH);
829
+ }
830
+ return this.#dispatchCascade(token);
831
+ }
832
+ /** Entry for a resolve the container starts, which opens the cascade rather than joining one. */
833
+ resolveAsyncFromRoot(token) {
834
+ return this.#dispatchCascade(token);
835
+ }
836
+ #dispatchCascade(token) {
837
+ const fastBinding = this.#registry.getFastDefault(token);
838
+ if (fastBinding !== undefined) {
839
+ if ((fastBinding.kind === "dynamic-async" || fastBinding.kind === "dynamic") &&
840
+ fastBinding.scope === "transient" &&
841
+ !this.#hasAnyActivation(fastBinding)) {
842
+ return this.#resolveTransientDynamicAsyncCascade(fastBinding);
843
+ }
844
+ // A value that already exists answers here: escaping would snapshot the cascade for a resolve
845
+ // that never looks at a path.
846
+ if (this.#isPlainConstant(fastBinding)) {
847
+ return Promise.resolve(fastBinding.value);
848
+ }
849
+ if (fastBinding.scope === "singleton" && fastBinding.instance !== NO_INSTANCE) {
850
+ return Promise.resolve(fastBinding.instance);
851
+ }
1135
852
  }
1136
- if (isOwnerLevel) {
1137
- this.#asyncChainActiveLevels++;
853
+ // Anything else leaves the cascade lane for good, seeded with a snapshot of the ancestors it
854
+ // accumulated — so a cycle across the boundary is still on one path.
855
+ return this.resolveAsyncFromContext(token, [...this.#cascadePath], [...this.#cascadeStack], UNOWNED_BRANCH);
856
+ }
857
+ #resolveTransientDynamicAsyncCascade(binding) {
858
+ const frame = this.#getResolutionFrame(binding);
859
+ // The request that closes a cycle is made from a factory's synchronous prefix, and synchronous
860
+ // code does not interleave — so the O(1) flag is exact path membership here, as it is for the
861
+ // sync lane. It is cleared when the factory returns its promise, not when that promise settles.
862
+ if (binding.inFlight) {
863
+ return Promise.reject(new CircularDependencyError([...this.#cascadePath, frame.tokenName]));
1138
864
  }
1139
- // Invoke the factory synchronously to get its Promise (or a resolved value for "dynamic").
1140
- let factoryPromise;
865
+ const ctx = (this.#cascadeContext ??= new AsyncCascadeContext(this, this.#cascadePath, this.#cascadeStack));
866
+ binding.inFlight = true;
867
+ this.#cascadePath.push(frame.tokenName);
868
+ this.#cascadeStack.push(frame);
1141
869
  try {
1142
870
  if (binding.kind === "dynamic-async") {
1143
- factoryPromise = binding.factory(ctx);
1144
- }
1145
- else {
1146
- const factoryResult = binding.factory(ctx);
1147
- factoryPromise =
1148
- factoryResult instanceof Promise ? factoryResult : Promise.resolve(factoryResult);
871
+ return binding.factory(ctx);
1149
872
  }
873
+ const factoryResult = binding.factory(ctx);
874
+ return factoryResult instanceof Promise ? factoryResult : Promise.resolve(factoryResult);
1150
875
  }
1151
876
  catch (factoryError) {
1152
- // Synchronous throw from the factory (rare) — clean up immediately.
1153
- exitResolutionPath(resolutionPath);
1154
- if (isOwnerLevel && --this.#asyncChainActiveLevels === 0) {
1155
- this.#asyncChainCtxPath = undefined;
1156
- this.#asyncChainSettle = undefined;
1157
- }
1158
877
  return Promise.reject(factoryError);
1159
878
  }
1160
- // Cleanup runs as a SIDE listener on the factory promise instead of a derived-promise chain:
1161
- // registered synchronously here, it is FIFO-guaranteed to run before the awaiting caller
1162
- // resumes, so ordering is identical while saving one intermediate promise and one microtask
1163
- // hop per level. Trade-off: the settle handler marks a rejection as handled, so an unawaited
1164
- // failing resolveAsync no longer surfaces as an unhandledRejection — callers are expected to
1165
- // await (or .catch) the returned promise.
1166
- //
1167
- // Every level of the owning chain unwinds identically, so one closure serves them all.
1168
- let settle;
1169
- if (isOwnerLevel) {
1170
- settle =
1171
- this.#asyncChainSettle ??
1172
- (this.#asyncChainSettle = () => {
1173
- exitResolutionPath(resolutionPath);
1174
- if (--this.#asyncChainActiveLevels === 0) {
1175
- this.#asyncChainCtxPath = undefined;
1176
- this.#asyncChainSettle = undefined;
1177
- }
1178
- });
1179
- }
1180
- else {
1181
- settle = () => {
1182
- exitResolutionPath(resolutionPath);
1183
- };
879
+ finally {
880
+ this.#cascadeStack.pop();
881
+ this.#cascadePath.pop();
882
+ binding.inFlight = false;
1184
883
  }
1185
- factoryPromise.then(settle, settle);
1186
- return factoryPromise;
1187
884
  }
885
+ // A cached candidate answers here rather than re-entering the generic path: `resolveAll` pays
886
+ // this per candidate, and a fan-out over cached handlers is the shape that makes it matter.
1188
887
  #resolveCandidateSync(binding, options, resolutionPath, resolutionStack) {
1189
- if (binding.kind === "constant" &&
1190
- binding.onActivation === undefined &&
1191
- !this.#lifecycle.hasActivationHandlers(binding.token)) {
888
+ if (this.#isPlainConstant(binding)) {
1192
889
  return binding.value;
1193
890
  }
1194
891
  if (binding.kind === "alias") {
1195
892
  return this.resolve(binding.target, options, resolutionPath, resolutionStack);
1196
893
  }
1197
- const scope = binding.scope ?? "transient";
1198
- if (scope === "singleton" && this.#scope.hasSingleton(binding.id)) {
1199
- return this.#scope.getSingleton(binding.id);
1200
- }
1201
- if (scope === "scoped") {
1202
- if (!this.#scope.isChild) {
1203
- throw new MissingScopeContextError(this.#getTokenName(binding.token));
1204
- }
1205
- if (this.#scope.hasScoped(binding.id)) {
1206
- return this.#scope.getScoped(binding.id);
1207
- }
894
+ if (binding.scope === "singleton" && binding.instance !== NO_INSTANCE) {
895
+ return binding.instance;
1208
896
  }
1209
897
  return this.#resolveBinding(binding, options, resolutionPath, resolutionStack);
1210
898
  }
1211
- #resolveCandidateAsync(binding, options, resolutionPath, resolutionStack) {
1212
- if (binding.kind === "constant" &&
1213
- binding.onActivation === undefined &&
1214
- !this.#lifecycle.hasActivationHandlers(binding.token)) {
899
+ #resolveCandidateAsync(binding, options, resolutionPath, resolutionStack, branchDepth) {
900
+ if (this.#isPlainConstant(binding)) {
1215
901
  return Promise.resolve(binding.value);
1216
902
  }
1217
- const isolatedPath = [...resolutionPath];
1218
- const isolatedStack = [...resolutionStack];
1219
903
  if (binding.kind === "alias") {
1220
- return this.resolveAsync(binding.target, options, isolatedPath, isolatedStack);
904
+ return this.resolveAsync(binding.target, options, resolutionPath, resolutionStack, branchDepth);
1221
905
  }
1222
- const scope = binding.scope ?? "transient";
1223
- if (scope === "singleton" && this.#scope.hasSingleton(binding.id)) {
1224
- return Promise.resolve(this.#scope.getSingleton(binding.id));
906
+ if (binding.scope === "singleton" && binding.instance !== NO_INSTANCE) {
907
+ return Promise.resolve(binding.instance);
1225
908
  }
1226
- if (scope === "scoped") {
1227
- if (!this.#scope.isChild) {
1228
- return Promise.reject(new MissingScopeContextError(this.#getTokenName(binding.token)));
1229
- }
1230
- if (this.#scope.hasScoped(binding.id)) {
1231
- return Promise.resolve(this.#scope.getScoped(binding.id));
1232
- }
1233
- }
1234
- return this.#resolveBindingAsync(binding, options, isolatedPath, isolatedStack);
909
+ return this.#resolveBindingAsync(binding, options, resolutionPath, resolutionStack, branchDepth);
1235
910
  }
1236
911
  #getResolutionFrame(binding) {
1237
912
  // Memoized on the binding rather than in a per-resolver Map: the frame derives only from
@@ -1241,70 +916,10 @@ export class DependencyResolver {
1241
916
  if (existing !== undefined) {
1242
917
  return existing;
1243
918
  }
1244
- const scope = binding.scope ?? "transient";
1245
- const frame = buildResolutionFrame(tokenName(binding.token), scope, binding.id, binding.kind, binding.slot);
919
+ const frame = buildResolutionFrame(tokenName(binding.token), binding.scope, binding.id, binding.kind, binding.slot);
1246
920
  binding.frame = frame;
1247
921
  return frame;
1248
922
  }
1249
- #needsActivation(binding) {
1250
- const lifecycleVersion = this.#lifecycle.activationVersion;
1251
- if (lifecycleVersion === 0 &&
1252
- binding.kind !== "class" &&
1253
- binding.kind !== "alias" &&
1254
- binding.onActivation === undefined) {
1255
- return false;
1256
- }
1257
- if (this.#activationCacheVersion !== lifecycleVersion) {
1258
- this.#activationNeedByBindingId.clear();
1259
- this.#activationCacheVersion = lifecycleVersion;
1260
- }
1261
- const cached = this.#activationNeedByBindingId.get(binding.id);
1262
- if (cached !== undefined) {
1263
- return cached;
1264
- }
1265
- if (binding.kind === "class") {
1266
- let hasActivation = this.#lifecycle.hasActivationHandlers(binding.token) || binding.onActivation !== undefined;
1267
- const cachedPostConstruct = this.#classHasPostConstruct.get(binding.target);
1268
- // Unknown class lifecycle metadata: activate once, then cache after first instantiation.
1269
- if (cachedPostConstruct === undefined) {
1270
- hasActivation = true;
1271
- }
1272
- else if (cachedPostConstruct) {
1273
- hasActivation = true;
1274
- }
1275
- this.#activationNeedByBindingId.set(binding.id, hasActivation);
1276
- return hasActivation;
1277
- }
1278
- let hasActivation = false;
1279
- if (binding.kind !== "alias" && binding.onActivation !== undefined) {
1280
- hasActivation = true;
1281
- }
1282
- else if (this.#lifecycle.hasActivationHandlers(binding.token)) {
1283
- hasActivation = true;
1284
- }
1285
- this.#activationNeedByBindingId.set(binding.id, hasActivation);
1286
- return hasActivation;
1287
- }
1288
- #refreshClassPostConstructCache(target) {
1289
- const lifecycle = this.#metadataReader.getLifecycleMetadata(target);
1290
- const hasPostConstruct = lifecycle !== undefined && lifecycle.postConstruct !== undefined && lifecycle.postConstruct.length > 0;
1291
- this.#classHasPostConstruct.set(target, hasPostConstruct);
1292
- }
1293
- /**
1294
- * Refreshes the post-construct cache for class bindings on first instantiation and
1295
- * returns the (possibly updated) shouldActivate flag.
1296
- */
1297
- #refreshActivationCacheIfNeeded(binding, needsActivation) {
1298
- if (binding.kind === "class" && this.#classHasPostConstruct.get(binding.target) === undefined) {
1299
- this.#refreshClassPostConstructCache(binding.target);
1300
- this.#activationNeedByBindingId.delete(binding.id);
1301
- return this.#needsActivation(binding);
1302
- }
1303
- return needsActivation;
1304
- }
1305
- #requiresResolutionContext(binding) {
1306
- return binding.kind === "dynamic" || binding.kind === "dynamic-async";
1307
- }
1308
923
  #acquireSyncResolutionContext(resolutionPath, resolutionStack, options) {
1309
924
  const depth = resolutionStack.length;
1310
925
  const existing = this.#syncResolutionContextPool[depth];
@@ -1317,4 +932,30 @@ export class DependencyResolver {
1317
932
  return created;
1318
933
  }
1319
934
  }
935
+ /** Absent scoped entry — distinguishes it from a cached `undefined`. */
936
+ const SCOPED_MISS = Symbol("di:scoped-miss");
937
+ function anyPredicate(bindings) {
938
+ for (let index = 0; index < bindings.length; index += 1) {
939
+ if (bindings[index].predicate !== undefined) {
940
+ return true;
941
+ }
942
+ }
943
+ return false;
944
+ }
945
+ /** Only a factory is handed the resolution context; everything else gets its deps directly. */
946
+ function requiresResolutionContext(binding) {
947
+ return binding.kind === "dynamic" || binding.kind === "dynamic-async";
948
+ }
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
+ }
1320
961
  //# sourceMappingURL=resolver.js.map