@codefast/di 0.10.1 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/CHANGELOG.md +376 -0
  2. package/README.md +94 -17
  3. package/dist/{ambient/active-container.d.ts → ambient-container.d.ts} +7 -2
  4. package/dist/{ambient/active-container.js → ambient-container.js} +6 -1
  5. package/dist/container/{binding-builders.d.ts → binding-chain.d.ts} +27 -16
  6. package/dist/container/binding-chain.js +254 -0
  7. package/dist/container/container.d.ts +11 -2
  8. package/dist/container/container.js +293 -73
  9. package/dist/core/binding-builders.d.ts +114 -0
  10. package/dist/core/binding-declaration.d.ts +127 -0
  11. package/dist/core/binding-declaration.js +187 -0
  12. package/dist/core/binding.d.ts +62 -138
  13. package/dist/core/binding.js +54 -17
  14. package/dist/core/module.d.ts +8 -5
  15. package/dist/core/module.js +17 -3
  16. package/dist/core/registry.d.ts +17 -28
  17. package/dist/core/registry.js +92 -133
  18. package/dist/core/state-epoch.d.ts +18 -1
  19. package/dist/core/state-epoch.js +17 -0
  20. package/dist/core/tag.js +1 -1
  21. package/dist/decorators/inject.js +6 -5
  22. package/dist/decorators/injectable.js +4 -2
  23. package/dist/decorators/{lifecycle-decorators.js → lifecycle.js} +9 -3
  24. package/dist/decorators/metadata-record.d.ts +9 -0
  25. package/dist/decorators/metadata-record.js +20 -0
  26. package/dist/{errors/errors.d.ts → errors.d.ts} +84 -9
  27. package/dist/{errors/errors.js → errors.js} +101 -17
  28. package/dist/index.d.ts +14 -10
  29. package/dist/index.js +7 -6
  30. package/dist/injection/{resolve-options.js → dependency-slot.js} +6 -4
  31. package/dist/injection/descriptor.d.ts +1 -1
  32. package/dist/injection/descriptor.js +3 -7
  33. package/dist/introspection/dependency-graph.d.ts +8 -3
  34. package/dist/introspection/dependency-graph.js +48 -26
  35. package/dist/introspection/explanation.d.ts +101 -0
  36. package/dist/introspection/explanation.js +151 -0
  37. package/dist/introspection/graph-adapters/reactflow.js +6 -4
  38. package/dist/introspection/inspector.d.ts +9 -2
  39. package/dist/introspection/inspector.js +30 -28
  40. package/dist/lifecycle/{lifecycle-manager.d.ts → hooks.d.ts} +9 -2
  41. package/dist/lifecycle/{lifecycle-manager.js → hooks.js} +59 -30
  42. package/dist/lifecycle/{scope-manager.d.ts → scopes.d.ts} +3 -5
  43. package/dist/lifecycle/{scope-manager.js → scopes.js} +36 -29
  44. package/dist/metadata/{metadata-reader-token.d.ts → reader-token.d.ts} +1 -1
  45. package/dist/metadata/{symbol-metadata-reader.d.ts → symbol-reader.d.ts} +1 -1
  46. package/dist/metadata/{symbol-metadata-reader.js → symbol-reader.js} +1 -1
  47. package/dist/metadata/{metadata-types.d.ts → types.d.ts} +1 -1
  48. package/dist/metadata/types.js +1 -0
  49. package/dist/metadata/verify.d.ts +43 -0
  50. package/dist/metadata/verify.js +133 -0
  51. package/dist/metadata/{verifying-metadata-reader.d.ts → verifying-reader.d.ts} +5 -4
  52. package/dist/metadata/verifying-reader.js +53 -0
  53. package/dist/resolution/async-fan-out.d.ts +12 -0
  54. package/dist/resolution/async-fan-out.js +26 -0
  55. package/dist/resolution/cache/activation-need.d.ts +1 -2
  56. package/dist/resolution/cache/activation-need.js +16 -19
  57. package/dist/resolution/cache/class-introspector.d.ts +38 -36
  58. package/dist/resolution/cache/class-introspector.js +79 -159
  59. package/dist/resolution/cache/{binding-lookup-cache.d.ts → lookup.d.ts} +7 -9
  60. package/dist/resolution/cache/{binding-lookup-cache.js → lookup.js} +70 -25
  61. package/dist/resolution/context.d.ts +16 -24
  62. package/dist/resolution/context.js +47 -56
  63. package/dist/resolution/path.d.ts +128 -0
  64. package/dist/resolution/path.js +167 -0
  65. package/dist/resolution/plan/{plan-codegen.d.ts → codegen.d.ts} +5 -4
  66. package/dist/resolution/plan/{plan-codegen.js → codegen.js} +58 -32
  67. package/dist/resolution/plan/{instantiation-plan.d.ts → compiler.d.ts} +1 -1
  68. package/dist/resolution/plan/{instantiation-plan.js → compiler.js} +64 -24
  69. package/dist/resolution/resolver.d.ts +11 -12
  70. package/dist/resolution/resolver.js +573 -356
  71. package/dist/resolution/select/{binding-select.d.ts → candidates.d.ts} +30 -0
  72. package/dist/resolution/select/{binding-select.js → candidates.js} +51 -10
  73. package/dist/resolution/select/constraints.js +1 -1
  74. package/package.json +87 -67
  75. package/dist/container/binding-builders.js +0 -305
  76. package/dist/core/binding-scope.d.ts +0 -12
  77. package/dist/core/binding-scope.js +0 -12
  78. package/dist/metadata/verifying-metadata-reader.js +0 -31
  79. package/dist/resolution/path/resolution-path.d.ts +0 -93
  80. package/dist/resolution/path/resolution-path.js +0 -116
  81. /package/dist/{metadata/metadata-types.js → core/binding-builders.js} +0 -0
  82. /package/dist/decorators/{lifecycle-decorators.d.ts → lifecycle.d.ts} +0 -0
  83. /package/dist/injection/{resolve-options.d.ts → dependency-slot.d.ts} +0 -0
  84. /package/dist/{errors → introspection}/diagnostics.d.ts +0 -0
  85. /package/dist/{errors → introspection}/diagnostics.js +0 -0
  86. /package/dist/metadata/{metadata-keys.d.ts → keys.d.ts} +0 -0
  87. /package/dist/metadata/{metadata-keys.js → keys.js} +0 -0
  88. /package/dist/metadata/{metadata-reader-token.js → reader-token.js} +0 -0
@@ -1,21 +1,21 @@
1
- import { NO_INSTANCE } from "#core/binding";
2
- import { NO_TAG_KEYS, slotNameCriterionOf } from "#core/tag";
1
+ import { bindingSlotToString, NO_INSTANCE } from "#core/binding";
2
+ import { slotNameCriterionOf } from "#core/tag";
3
3
  import { tokenName } from "#core/token";
4
- import { AsyncActivationError, AsyncResolutionError, CircularDependencyError, DisposedContainerError, InternalError, MissingMetadataError, MissingScopeContextError, NoMatchingBindingError, TokenNotBoundError, } from "#errors/errors";
5
- import { loneTagBesideNameOf, resolveOptionsForSlot, singleCriterionForSlot, singleCriterionOnlyOf, } from "#injection/resolve-options";
6
- import { SCOPED_MISS } from "#lifecycle/scope-manager";
4
+ import { AsyncActivationError, AsyncResolutionError, CircularDependencyError, DisposedContainerError, InternalError, MissingMetadataError, MissingScopeContextError, NoMatchingBindingError, TokenNotBoundError, } from "#errors";
5
+ import { loneTagBesideNameOf, resolveOptionsForSlot, singleCriterionForSlot, singleCriterionOnlyOf, } from "#injection/dependency-slot";
6
+ import { SCOPED_MISS } from "#lifecycle/scopes";
7
+ import { settleInOrder } from "#resolution/async-fan-out";
7
8
  import { ActivationNeedCache } from "#resolution/cache/activation-need";
8
- import { BindingLookupCache } from "#resolution/cache/binding-lookup-cache";
9
9
  import { ClassIntrospector } from "#resolution/cache/class-introspector";
10
- import { AsyncCascadeContext, AsyncLevelContext, buildResolutionFrame, DefaultResolutionContext, } from "#resolution/context";
11
- import { branchDepthOf, cycleNamesOf, enterResolutionPath, extendResolutionBranch, ROOT_BRANCH, UNOWNED_BRANCH, } from "#resolution/path/resolution-path";
12
- import { InstantiationPlanCompiler, PLAN_RETRY } from "#resolution/plan/instantiation-plan";
13
- import { matchesSlot, requestedTagKeyMask, selectAllBindings, selectBinding } from "#resolution/select/binding-select";
14
- // Where a multi-tag resolve switches from scanning the token's list to walking the tag indexes.
15
- const MULTI_TAG_INDEX_THRESHOLD = 8;
16
- const EMPTY_STRING_LIST = [];
10
+ import { BindingLookupCache } from "#resolution/cache/lookup";
11
+ import { AsyncLevelContext, DefaultConstraintContext, DefaultResolutionContext } from "#resolution/context";
12
+ import { branchDepthOf, buildResolutionFrame, cycleNamesOf, enterSyncPath, extendResolutionBranch, leaveSyncPath, ROOT_BRANCH, UNOWNED_BRANCH, } from "#resolution/path";
13
+ import { InstantiationPlanCompiler, PLAN_RETRY } from "#resolution/plan/compiler";
14
+ import { matchesSlot, selectAllBindings, selectBinding } from "#resolution/select/candidates";
17
15
  const EMPTY_FRAME_LIST = [];
18
16
  const EMPTY_PARAM_LIST = [];
17
+ // A class with no params is constructed with no arguments, so no dependency array is built for it.
18
+ const NO_ARGUMENTS = [];
19
19
  /** Plans compiled so far, with the `null` unplannable marks left out. */
20
20
  function countCompiledPlans(plans) {
21
21
  let count = 0;
@@ -28,22 +28,15 @@ function countCompiledPlans(plans) {
28
28
  }
29
29
  return count;
30
30
  }
31
- const ROOT_CONSTRAINT_CONTEXT = {
32
- resolutionPath: EMPTY_STRING_LIST,
33
- resolutionStack: EMPTY_FRAME_LIST,
34
- parent: undefined,
35
- ancestors: EMPTY_FRAME_LIST,
36
- currentResolveOptions: undefined,
37
- };
31
+ const ROOT_CONSTRAINT_CONTEXT = new DefaultConstraintContext(EMPTY_FRAME_LIST, undefined);
38
32
  /**
39
33
  * The resolution engine driving binding selection, instantiation, scoping, and lifecycle hooks.
40
34
  *
41
35
  * @since 0.3.16-canary.0
42
36
  */
43
37
  export class DependencyResolver {
44
- #syncResolutionContextPool = [];
45
- // Contexts bound to the cascade pair — deferred: only an async cascade's sync resolves need it.
46
- #cascadeContextPool;
38
+ // Contexts pooled by depth over the root stack — deferred: a plan-served or constant-only container never needs one.
39
+ #syncResolutionContextPool;
47
40
  /**
48
41
  * The stack a top-level **sync** resolve reuses instead of minting an array per call.
49
42
  *
@@ -53,25 +46,31 @@ export class DependencyResolver {
53
46
  * mints its own. Keeping it stable is also what lets a pooled context skip re-storing it.
54
47
  */
55
48
  rootStack = [];
56
- // The open synchronous factory cascade: its stack is the ancestor chain, and it is balanced
57
- // because synchronous code does not interleave.
58
- #cascadeStack = [];
59
- #cascadeContext;
49
+ // The last root level's context, kept so one instance is always live: a full collection that finds
50
+ // none deoptimizes every optimized site that embedded the class's shape, and does so on every
51
+ // collection after — measured on the async chain rows, and removed by this one field.
52
+ #recentRootLevelContext;
60
53
  // Compiled plans; `null` marks a binding as unplannable under the current cache versions. Both
61
- // maps are allocated by the first plan request, which only a class or resolved binding makes.
54
+ // maps are allocated by the first plan request, which only a class or resolved binding makes. A
55
+ // root is compiled on the request that repeats it — the first interprets, so a container that
56
+ // resolves a root once never compiles — and the set below remembers the first.
62
57
  #classPlanByBindingId;
58
+ #classPlanRequestedOnce;
63
59
  #classPlanRegistryVersion = -1;
64
60
  #classPlanActivationVersion = -1;
65
61
  // The async lane's plans, stamped and invalidated apart so neither lane pays the other's misses.
66
62
  #asyncPlanByBindingId;
63
+ #asyncPlanRequestedOnce;
67
64
  #asyncPlanRegistryVersion = -1;
68
65
  #asyncPlanActivationVersion = -1;
69
66
  #registry;
70
67
  #scope;
71
68
  #lifecycle;
72
69
  #metadataReader;
70
+ #container;
73
71
  #parent;
74
72
  #lookup;
73
+ // Built by the first class resolve: a container that never resolves a class never pays for it.
75
74
  #classes;
76
75
  // Built by the first interpreted resolve that asks; a plan-served or constant-only container never does.
77
76
  #activation;
@@ -80,16 +79,19 @@ export class DependencyResolver {
80
79
  this.#scope = scope;
81
80
  this.#lifecycle = lifecycle;
82
81
  this.#metadataReader = metadataReader;
82
+ this.#container = container;
83
83
  this.#parent = parent;
84
84
  this.#lookup = new BindingLookupCache(registry, this, parent === undefined ? undefined : parent.#lookup);
85
- this.#classes = new ClassIntrospector(metadataReader, container, parent === undefined ? undefined : parent.#classes);
85
+ }
86
+ #introspector() {
87
+ return (this.#classes ??= new ClassIntrospector(this.#metadataReader, this.#container, this.#parent === undefined ? undefined : this.#parent.#introspector()));
86
88
  }
87
89
  /** The reader this resolver was built with, which is the one its container answers with. */
88
90
  get metadataReader() {
89
91
  return this.#metadataReader;
90
92
  }
91
93
  #activationNeed() {
92
- return (this.#activation ??= new ActivationNeedCache(this.#lifecycle, this.#classes, this.#registry));
94
+ return (this.#activation ??= new ActivationNeedCache(this.#lifecycle, this.#introspector(), this.#registry));
93
95
  }
94
96
  /** Structural counts and the resolver-owned collaborators built so far, for the {@link ResolutionDiagnostics} a container reports. */
95
97
  describeCaches() {
@@ -100,18 +102,18 @@ export class DependencyResolver {
100
102
  if (this.#lookup.isMemoBuilt) {
101
103
  builtSubsystems.push("resolver.lookupMemo");
102
104
  }
103
- if (this.#activation?.isMemoBuilt === true) {
104
- builtSubsystems.push("resolver.activationNeedMemo");
105
- }
106
105
  const generatedPlanCount = this.#planCompiler?.generatedPlanCount ?? 0;
107
106
  if (generatedPlanCount > 0) {
108
107
  builtSubsystems.push("resolver.planCodegen");
109
108
  }
109
+ if (this.#recentRootLevelContext !== undefined) {
110
+ builtSubsystems.push("resolver.asyncRootLevel");
111
+ }
110
112
  return {
111
113
  compiledPlanCount: countCompiledPlans(this.#classPlanByBindingId),
112
114
  compiledAsyncPlanCount: countCompiledPlans(this.#asyncPlanByBindingId),
113
115
  generatedPlanCount,
114
- syncContextPoolSize: this.#syncResolutionContextPool.length,
116
+ syncContextPoolSize: this.#syncResolutionContextPool?.length ?? 0,
115
117
  builtSubsystems,
116
118
  };
117
119
  }
@@ -134,23 +136,19 @@ export class DependencyResolver {
134
136
  if (indexed === undefined) {
135
137
  // A one-criterion request matches only a slot carrying exactly that criterion, and every such
136
138
  // slot is in the index, so a miss here is a miss for this registry: nothing left to scan.
137
- return this.#parent === undefined
138
- ? undefined
139
- : this.#parent.#findBinding(token, options, resolutionStack, singleCriterion);
139
+ return this.#forwardingAliasOrParent(token, options, resolutionStack, singleCriterion);
140
140
  }
141
141
  if (this.#satisfiesPredicate(indexed, options, resolutionStack)) {
142
142
  return { binding: indexed, owner: this };
143
143
  }
144
144
  }
145
- else {
146
- if (options.name !== undefined) {
147
- const pairTag = loneTagBesideNameOf(options);
148
- if (pairTag !== undefined) {
149
- const nameCriterion = slotNameCriterionOf(options.name);
150
- if (nameCriterion === undefined) {
151
- // No binding anywhere has declared this name, so no slot in any registry can carry it.
152
- return undefined;
153
- }
145
+ else if (options.name !== undefined) {
146
+ const pairTag = loneTagBesideNameOf(options);
147
+ if (pairTag !== undefined) {
148
+ const nameCriterion = slotNameCriterionOf(options.name);
149
+ // A name interned nowhere cannot key a slot, but a slot whose criteria are a subset of the
150
+ // request's still matches — so an index miss falls through to the scan, never a clean miss.
151
+ if (nameCriterion !== undefined) {
154
152
  // The exact two-criterion slot, memoized over the chain; a predicate or an alias declines to the scan.
155
153
  const entry = this.#lookup.namedTaggedEntry(token, nameCriterion, pairTag);
156
154
  if (entry !== null) {
@@ -158,22 +156,6 @@ export class DependencyResolver {
158
156
  }
159
157
  }
160
158
  }
161
- if (
162
- // A threshold switches the data structure, never the semantics: under it the generic scan
163
- // below beats walking the indexes, and both paths answer identically. Sized first, so a
164
- // small list pays one length read and nothing else.
165
- this.#registry.countBindings(token) > MULTI_TAG_INDEX_THRESHOLD &&
166
- requestedTagKeyMask(options) !== NO_TAG_KEYS) {
167
- // A multi-criterion request matches only slots whose every criterion it carries, and every
168
- // such slot is in the two tag indexes — their union is the whole candidate set, unscanned.
169
- const selected = this.#selectMultiTagged(token, options, resolutionStack);
170
- if (selected !== undefined) {
171
- return { binding: selected, owner: this };
172
- }
173
- return this.#parent === undefined
174
- ? undefined
175
- : this.#parent.#findBinding(token, options, resolutionStack, singleCriterion);
176
- }
177
159
  }
178
160
  // A lone default-slot candidate is its own selection: the slot match is the whole decision,
179
161
  // it carries no predicate, and asking for it first keeps the registry from materialising its list.
@@ -194,10 +176,29 @@ export class DependencyResolver {
194
176
  }
195
177
  }
196
178
  }
197
- if (this.#parent !== undefined) {
198
- return this.#parent.#findBinding(token, options, resolutionStack, singleCriterion);
179
+ if (options === undefined) {
180
+ return this.#parent === undefined
181
+ ? undefined
182
+ : this.#parent.#findBinding(token, options, resolutionStack, singleCriterion);
183
+ }
184
+ return this.#forwardingAliasOrParent(token, options, resolutionStack, singleCriterion);
185
+ }
186
+ /**
187
+ * What a request with criteria finds once no slot of this registry matches them: the token's
188
+ * default-slot alias, which forwards them to its target, else the parent's answer.
189
+ *
190
+ * @remarks A default-slot alias carries no criterion, predicate or membership, so it is a pointer
191
+ * rather than a candidate — every exact slot here was tried first, and the nearest container that
192
+ * can answer still does.
193
+ */
194
+ #forwardingAliasOrParent(token, options, resolutionStack, singleCriterion) {
195
+ const defaultSlotAlias = this.#registry.defaultSlotAlias(token);
196
+ if (defaultSlotAlias !== undefined) {
197
+ return { binding: defaultSlotAlias, owner: this };
199
198
  }
200
- return undefined;
199
+ return this.#parent === undefined
200
+ ? undefined
201
+ : this.#parent.#findBinding(token, options, resolutionStack, singleCriterion);
201
202
  }
202
203
  /**
203
204
  * The scan a candidate list gets before full selection.
@@ -248,14 +249,40 @@ export class DependencyResolver {
248
249
  }
249
250
  if (found === undefined) {
250
251
  // Thrown here rather than from a helper: the error captures this stack, and an error path is
251
- // dominated by that capture. Bindings under the token mean the request matched none of them.
252
- if (this.#registry.getAll(currentToken).length > 0) {
253
- throw new NoMatchingBindingError(tokenName(currentToken), options ?? {}, this.#registry.availableSlotStrings(currentToken));
252
+ // dominated by that capture. Bindings under the token anywhere in the chain mean the request
253
+ // matched none of them, so a child reports the same miss its parent would.
254
+ const bound = this.#allBindingsFromChain(currentToken);
255
+ if (bound.length > 0) {
256
+ throw new NoMatchingBindingError(tokenName(currentToken), options ?? {}, bound.map((binding) => bindingSlotToString(binding.slot)));
254
257
  }
255
258
  throw new TokenNotBoundError(tokenName(currentToken));
256
259
  }
257
260
  return found;
258
261
  }
262
+ /**
263
+ * Follows an alias binding's chain to its terminal binding, or `undefined` when the chain ends at
264
+ * a token nothing matches.
265
+ *
266
+ * @remarks The non-throwing twin of {@link DependencyResolver.#requireBinding}'s alias walk, for
267
+ * the optional and collection lanes: a dangling chain is a miss, but a revisited alias token still
268
+ * raises {@link CircularDependencyError} — a cycle has no absent reading.
269
+ */
270
+ #terminalOfAliasBinding(alias, options, resolutionStack, singleCriterion) {
271
+ let currentToken = alias.token;
272
+ let found = { binding: alias, owner: this };
273
+ let visitedAliasTokens;
274
+ while (found !== undefined && found.binding.kind === "alias") {
275
+ const target = found.binding.target;
276
+ visitedAliasTokens ??= new Set([currentToken]);
277
+ if (visitedAliasTokens.has(target)) {
278
+ throw new CircularDependencyError([...visitedAliasTokens, target].map((entry) => tokenName(entry)));
279
+ }
280
+ visitedAliasTokens.add(target);
281
+ currentToken = target;
282
+ found = this.#findBinding(currentToken, options, resolutionStack, singleCriterion);
283
+ }
284
+ return found;
285
+ }
259
286
  /**
260
287
  * Binding lookup aligned with `resolve` — used by `Container.validate` without instantiating.
261
288
  */
@@ -346,6 +373,8 @@ export class DependencyResolver {
346
373
  if (binding.activationHook !== undefined) {
347
374
  const activationResult = binding.activationHook(resolutionCtx, activated);
348
375
  if (activationResult instanceof Promise) {
376
+ // The hook has already run; adopt its rejection so it cannot surface as unhandled.
377
+ void activationResult.catch(() => { });
349
378
  throw new AsyncActivationError(tokenDisplayName, "onActivation");
350
379
  }
351
380
  activated = activationResult;
@@ -354,6 +383,7 @@ export class DependencyResolver {
354
383
  for (let index = 0; index < containerHooks.length; index += 1) {
355
384
  const activationResult = containerHooks[index](resolutionCtx, activated);
356
385
  if (activationResult instanceof Promise) {
386
+ void activationResult.catch(() => { });
357
387
  throw new AsyncActivationError(tokenDisplayName, "onActivation");
358
388
  }
359
389
  activated = activationResult;
@@ -390,6 +420,7 @@ export class DependencyResolver {
390
420
  }
391
421
  else {
392
422
  this.#classPlanByBindingId.clear();
423
+ this.#classPlanRequestedOnce?.clear();
393
424
  }
394
425
  this.#classPlanRegistryVersion = registryVersion;
395
426
  this.#classPlanActivationVersion = activationVersion;
@@ -399,6 +430,12 @@ export class DependencyResolver {
399
430
  if (cached !== undefined) {
400
431
  return cached;
401
432
  }
433
+ const requestedOnce = (this.#classPlanRequestedOnce ??= new Set());
434
+ if (!requestedOnce.has(binding.identifier)) {
435
+ // The first request interprets; the one that repeats it compiles.
436
+ requestedOnce.add(binding.identifier);
437
+ return null;
438
+ }
402
439
  const compiled = this.#compiler().compile(binding);
403
440
  if (compiled === PLAN_RETRY) {
404
441
  // Lifecycle metadata not discovered yet — the fallback resolve discovers it; retry then.
@@ -416,25 +453,24 @@ export class DependencyResolver {
416
453
  // The behaviour the plan compiler needs from this resolver — lookups, escapes, plan swaps, and the
417
454
  // accessor construction path. Built once with the compiler, so each closure is allocated once.
418
455
  #buildPlanCompilerHost() {
456
+ const classes = this.#introspector();
419
457
  return {
420
458
  hasActivationHandlers: (binding) => this.#ownerOf(binding).#lifecycle.hasActivationHandlers(binding.token),
421
- knownPostConstruct: (target) => this.#classes.knownPostConstruct(target),
422
- needsActiveContainer: (target) => this.#classes.needsActiveContainer(target),
459
+ knownPostConstruct: (target) => classes.knownPostConstruct(target),
460
+ needsActiveContainer: (target) => classes.needsActiveContainer(target),
423
461
  // A plan runs at the top level, so the lent root stack is free when it is; a plan reached
424
462
  // with the root stack held mints its own path, exactly as the interpreted lane would.
425
463
  constructWithAccessors: (binding, target, deps) => {
426
464
  const stack = this.rootStack.length === 0 ? this.rootStack : [];
427
- const frame = this.#getResolutionFrame(binding);
428
- const resolutionSet = enterResolutionPath(stack, frame);
465
+ enterSyncPath(stack, binding, this.#getResolutionFrame(binding));
429
466
  try {
430
- return this.#classes.instantiate(target, deps, this.#ambientResolutionFor(stack));
467
+ return classes.instantiate(target, deps, this.#ambientResolutionFor(stack));
431
468
  }
432
469
  finally {
433
- stack.pop();
434
- resolutionSet?.delete(frame.bindingId);
470
+ leaveSyncPath(stack, binding);
435
471
  }
436
472
  },
437
- getConstructorMetadata: (target) => this.#classes.constructorMetadata(target),
473
+ getConstructorMetadata: (target) => classes.constructorMetadata(target),
438
474
  lookupDependencyEntry: (token) => {
439
475
  const entry = this.#lookup.defaultEntry(token);
440
476
  return entry === null ? null : { binding: entry.binding };
@@ -480,7 +516,7 @@ export class DependencyResolver {
480
516
  }
481
517
  return this.resolve(token, options, resolutionStack);
482
518
  },
483
- // Dispatches exactly as #resolveDepAsync does, for the async lane's escapes.
519
+ // Dispatches exactly as #settleDep does, for the async lane's escapes.
484
520
  resolveEscapedAsync: (token, options, arity, resolutionStack) => {
485
521
  if (arity === "all") {
486
522
  return this.resolveAllAsync(token, options, resolutionStack, UNOWNED_BRANCH);
@@ -505,6 +541,7 @@ export class DependencyResolver {
505
541
  }
506
542
  else {
507
543
  this.#asyncPlanByBindingId.clear();
544
+ this.#asyncPlanRequestedOnce?.clear();
508
545
  }
509
546
  this.#asyncPlanRegistryVersion = registryVersion;
510
547
  this.#asyncPlanActivationVersion = activationVersion;
@@ -514,6 +551,11 @@ export class DependencyResolver {
514
551
  if (cached !== undefined) {
515
552
  return cached;
516
553
  }
554
+ const requestedOnce = (this.#asyncPlanRequestedOnce ??= new Set());
555
+ if (!requestedOnce.has(binding.identifier)) {
556
+ requestedOnce.add(binding.identifier);
557
+ return null;
558
+ }
517
559
  const compiled = this.#compiler().compileAsync(binding);
518
560
  if (compiled === PLAN_RETRY) {
519
561
  // Lifecycle metadata not discovered yet — the fallback resolve discovers it; retry then.
@@ -581,7 +623,8 @@ export class DependencyResolver {
581
623
  }
582
624
  const frame = this.#getResolutionFrame(binding);
583
625
  const tokenDisplayName = frame.tokenName;
584
- const resolutionSet = enterResolutionPath(resolutionStack, frame);
626
+ // The level holds its binding's flag for its whole duration, so a factory below runs bare.
627
+ enterSyncPath(resolutionStack, binding, frame);
585
628
  try {
586
629
  const needsActivation = owner.#activationNeed().needsActivation(binding);
587
630
  if (!needsActivation && scope === "transient" && binding.kind === "dynamic") {
@@ -609,8 +652,7 @@ export class DependencyResolver {
609
652
  return activated;
610
653
  }
611
654
  finally {
612
- resolutionStack.pop();
613
- resolutionSet?.delete(frame.bindingId);
655
+ leaveSyncPath(resolutionStack, binding);
614
656
  }
615
657
  }
616
658
  /**
@@ -651,8 +693,13 @@ export class DependencyResolver {
651
693
  case "dynamic-async":
652
694
  throw asyncResolutionErrorFor(binding, resolutionStack);
653
695
  case "class": {
654
- const deps = this.#resolveDeps(this.#constructorParams(binding.target), resolutionStack);
655
- return this.#classes.instantiate(binding.target, deps, this.#classes.needsActiveContainer(binding.target) ? this.#ambientResolutionFor(resolutionStack) : undefined);
696
+ // One lookup of the class's facts serves the params, the accessor need and the construction.
697
+ const introspector = this.#introspector();
698
+ const target = binding.target;
699
+ const facts = introspector.facts(target);
700
+ const params = this.#constructorParams(target, facts);
701
+ const needsActiveContainer = introspector.needsActiveContainerOf(target, facts);
702
+ return introspector.construct(target, params.length === 0 ? NO_ARGUMENTS : this.#resolveDeps(params, resolutionStack), needsActiveContainer, needsActiveContainer ? this.#ambientResolutionFor(resolutionStack) : undefined);
656
703
  }
657
704
  case "resolved": {
658
705
  const deps = this.#resolveDeps(binding.deps, resolutionStack);
@@ -674,12 +721,25 @@ export class DependencyResolver {
674
721
  * @remarks A class the metadata reader knows nothing about is constructible only if it declares
675
722
  * no parameters; anything else is a missing `@injectable()`.
676
723
  */
677
- #constructorParams(target) {
678
- const meta = this.#classes.constructorMetadata(target);
724
+ #constructorParams(target, facts) {
725
+ // Metadata is fixed once a class is defined, so the answer is too; only a throw is left unsettled.
726
+ return (facts.params ??= this.#settleConstructorParams(target, facts));
727
+ }
728
+ #settleConstructorParams(target, facts) {
729
+ const meta = this.#introspector().constructorMetadataOf(target, facts);
679
730
  if (meta !== undefined) {
680
731
  return meta.params;
681
732
  }
682
733
  if (target.length === 0) {
734
+ // A subclass with an implicit constructor and no own metadata inherits its base's declared
735
+ // deps but would be built with zero arguments, injecting `undefined` silently — reject it.
736
+ const inherited = this.#introspector().inheritedConstructorMetadata(target);
737
+ if (inherited !== undefined && inherited.metadata.params.length > 0) {
738
+ throw new MissingMetadataError(target.name, {
739
+ baseName: inherited.base.name,
740
+ dependencyCount: inherited.metadata.params.length,
741
+ });
742
+ }
683
743
  return EMPTY_PARAM_LIST;
684
744
  }
685
745
  throw new MissingMetadataError(target.name);
@@ -727,16 +787,18 @@ export class DependencyResolver {
727
787
  return undefined;
728
788
  }
729
789
  }
730
- const entry = this.#findBinding(token, options, resolutionStack, precomputedCriterion === undefined ? singleCriterionOnlyOf(options) : (precomputedCriterion ?? undefined));
790
+ const singleCriterion = precomputedCriterion === undefined ? singleCriterionOnlyOf(options) : (precomputedCriterion ?? undefined);
791
+ let entry = this.#findBinding(token, options, resolutionStack, singleCriterion);
792
+ // Resolve the entry the probe found: re-looking the token up would evaluate every `when()`
793
+ // predicate a second time, and a changed answer would throw where `undefined` was promised.
794
+ if (entry !== undefined && entry.binding.kind === "alias") {
795
+ // Follow the alias off the throwing path: a chain that ends nowhere is a miss, not an error.
796
+ entry = this.#terminalOfAliasBinding(entry.binding, options, resolutionStack, singleCriterion);
797
+ }
731
798
  if (entry === undefined) {
732
799
  return undefined;
733
800
  }
734
- // Resolve the entry the probe found: re-looking the token up would evaluate every `when()`
735
- // predicate a second time, and a changed answer would throw where `undefined` was promised.
736
801
  const { binding, owner } = entry;
737
- if (binding.kind === "alias") {
738
- return this.resolve(token, options, resolutionStack);
739
- }
740
802
  if (binding.scope === "singleton" && owner !== this) {
741
803
  return owner.#resolveBinding(binding, options, resolutionStack, owner);
742
804
  }
@@ -761,37 +823,45 @@ export class DependencyResolver {
761
823
  const resolutionStack = this.rootStack;
762
824
  const memo = this.#rootCollection(token, resolutionStack);
763
825
  if (memo.values !== undefined && memo.activationVersion === this.#chainActivationVersion()) {
764
- return memo.values;
826
+ // A copy, never the memo itself: a caller that writes into its result must not rewrite the
827
+ // next caller's. Reading a frozen array is several times slower than reading a plain one, so
828
+ // the copy is the cheaper guard by far, and it costs one allocation the read already paid before the memo.
829
+ return memo.values.slice();
765
830
  }
766
831
  const { candidates } = memo;
767
832
  const resolved = new Array(candidates.length);
768
833
  for (let index = 0; index < candidates.length; index += 1) {
769
834
  resolved[index] = this.#resolveCandidateSync(candidates[index], undefined, resolutionStack);
770
835
  }
771
- // The members this read materialised may have made the list stable for the next one.
772
- this.#settleCollectionValues(memo);
836
+ this.#settleOnRepeat(memo);
773
837
  return resolved;
774
838
  }
775
- /** The async twin of `resolveRootCollection`: a stable value list settles at once, candidates fan out as usual. */
839
+ /** The async twin of `resolveRootCollection`: a stable value list answers at once, candidates fan out as usual. */
776
840
  resolveRootCollectionAsync(token) {
777
841
  // The async lane appends to its branch and never unwinds, so it works on a stack of its own.
778
842
  const resolutionStack = [];
779
- const memo = this.#rootCollection(token, resolutionStack);
780
- if (memo.values !== undefined && memo.activationVersion === this.#chainActivationVersion()) {
781
- return Promise.resolve(memo.values);
843
+ try {
844
+ // Selection runs `when()` predicates and follows aliases, either of which may throw.
845
+ const memo = this.#rootCollection(token, resolutionStack);
846
+ if (memo.values !== undefined && memo.activationVersion === this.#chainActivationVersion()) {
847
+ return Promise.resolve(memo.values.slice());
848
+ }
849
+ return asPromise(this.#settleCandidates(memo.candidates, undefined, resolutionStack, UNOWNED_BRANCH, (values) => {
850
+ this.#settleOnRepeat(memo);
851
+ return values;
852
+ }));
853
+ }
854
+ catch (collectionError) {
855
+ return Promise.reject(collectionError);
782
856
  }
783
- return Promise.all(memo.candidates.map((candidate) => this.#resolveCandidateAsync(candidate, undefined, resolutionStack, UNOWNED_BRANCH))).then((values) => {
784
- this.#settleCollectionValues(memo);
785
- return values;
786
- });
787
857
  }
788
858
  /**
789
859
  * The memoized candidate list of a root-level, options-less collection, built on its first read.
790
860
  *
791
861
  * @remarks Sound because a `when()` predicate is pure over its context and the root context is a
792
862
  * constant: the list can only change when a registry in the chain does, which is the version the
793
- * memo is stamped with. The value list is kept too while every member is a hook-free constant
794
- * and no activation hook exists anywhere in the chain.
863
+ * memo is stamped with. From the second read on, the value list is kept too while every member is
864
+ * a hook-free constant and no activation hook exists anywhere in the chain.
795
865
  */
796
866
  #rootCollection(token, resolutionStack) {
797
867
  const memo = this.#lookup.collection(token);
@@ -799,11 +869,20 @@ export class DependencyResolver {
799
869
  return memo;
800
870
  }
801
871
  const candidates = this.#candidateBindings(token, undefined, resolutionStack);
802
- const entry = { candidates, values: undefined, activationVersion: -1 };
803
- this.#settleCollectionValues(entry);
872
+ const entry = { candidates, values: undefined, activationVersion: -1, readBefore: false };
804
873
  this.#lookup.rememberCollection(token, entry);
805
874
  return entry;
806
875
  }
876
+ // The first read interprets and the read that repeats it settles the value list, as a plan compiles on repeat;
877
+ // the members either read materialised may have made the list stable for the next one.
878
+ #settleOnRepeat(entry) {
879
+ if (entry.readBefore) {
880
+ this.#settleCollectionValues(entry);
881
+ }
882
+ else {
883
+ entry.readBefore = true;
884
+ }
885
+ }
807
886
  /**
808
887
  * Fills a collection memo's value list once every member is stable: a hook-free constant, or a
809
888
  * hook-free singleton whose instance is cached — anything that changes either bumps a registry
@@ -819,8 +898,7 @@ export class DependencyResolver {
819
898
  return;
820
899
  }
821
900
  }
822
- // Handed out as is, unfrozen: a frozen array iterates through a slow elements kind, so the
823
- // contract's read-only return is the guard against a caller writing into the memo.
901
+ // Kept unfrozen: a frozen array reads through a slow elements kind, and every read copies it anyway.
824
902
  entry.values = candidates.map(stableMemberValue);
825
903
  entry.activationVersion = 0;
826
904
  }
@@ -829,14 +907,52 @@ export class DependencyResolver {
829
907
  if (options !== undefined) {
830
908
  const indexed = this.#indexedCandidates(token, options, resolutionStack);
831
909
  if (indexed !== null) {
832
- return indexed;
910
+ return this.#withoutDanglingAliases(indexed, options, resolutionStack);
833
911
  }
834
912
  }
835
913
  const allBindings = this.#allBindingsFromChain(token);
836
914
  if (allBindings.length === 0) {
837
915
  return allBindings;
838
916
  }
839
- return selectAllBindings(allBindings, options, this.#makeConstraintContext(resolutionStack, options));
917
+ const selected = selectAllBindings(allBindings, options, this.#makeConstraintContext(resolutionStack, options));
918
+ return this.#withoutDanglingAliases(selected, options, resolutionStack);
919
+ }
920
+ // A chain none of whose registries ever held an alias has none to drop, which spares the scan.
921
+ #chainHeldAlias() {
922
+ if (this.#registry.hasHeldAlias) {
923
+ return true;
924
+ }
925
+ for (let current = this.#parent; current !== undefined; current = current.#parent) {
926
+ if (current.#registry.hasHeldAlias) {
927
+ return true;
928
+ }
929
+ }
930
+ return false;
931
+ }
932
+ /**
933
+ * The candidates with any alias whose chain ends nowhere dropped, so a fan-out skips a dangling
934
+ * alias rather than throwing on it.
935
+ *
936
+ * @remarks A live alias stays and resolves as before; the common list carries no alias at all and
937
+ * is returned untouched, so nothing is allocated for it.
938
+ */
939
+ #withoutDanglingAliases(candidates, options, resolutionStack) {
940
+ if (!this.#chainHeldAlias()) {
941
+ return candidates;
942
+ }
943
+ let hasAlias = false;
944
+ for (let index = 0; index < candidates.length; index += 1) {
945
+ if (candidates[index].kind === "alias") {
946
+ hasAlias = true;
947
+ break;
948
+ }
949
+ }
950
+ if (!hasAlias) {
951
+ return candidates;
952
+ }
953
+ const singleCriterion = singleCriterionOnlyOf(options);
954
+ return candidates.filter((candidate) => candidate.kind !== "alias" ||
955
+ this.#terminalOfAliasBinding(candidate, options, resolutionStack, singleCriterion) !== undefined);
840
956
  }
841
957
  // ── Async resolve ──────────────────────────────────────────────────────────────────────────────────────────────────
842
958
  resolveAsyncFromContext(token, resolutionStack, branchDepth) {
@@ -858,35 +974,55 @@ export class DependencyResolver {
858
974
  }
859
975
  return this.#resolveAsyncDefaultEntry(entry.binding, entry.owner, resolutionStack, branchDepth);
860
976
  }
977
+ // A `#settle*` lane answers with the value when its level completed in this tick and with a promise
978
+ // only where something had to be awaited, so a singleton or scoped instance built without yielding
979
+ // is cached before the fan-out that started it moves on to a sibling that may read it synchronously.
861
980
  #resolveAsyncDefaultEntry(binding, owner, resolutionStack, branchDepth) {
981
+ try {
982
+ return asPromise(this.#settleDefaultEntry(binding, owner, resolutionStack, branchDepth));
983
+ }
984
+ catch (entryError) {
985
+ // Not `async`: a failure is turned into the rejection the entry point promises.
986
+ return Promise.reject(entryError);
987
+ }
988
+ }
989
+ /** `resolveAsyncFromContext` for a dependency slot: the same lanes, answered in this tick where they can be. */
990
+ #settleDefault(token, resolutionStack, branchDepth) {
991
+ const fastBinding = this.#registry.getFastDefault(token);
992
+ if (fastBinding !== undefined && fastBinding.kind !== "alias") {
993
+ return this.#settleDefaultEntry(fastBinding, this, resolutionStack, branchDepth);
994
+ }
995
+ const entry = this.#lookup.defaultEntry(token);
996
+ if (entry === null) {
997
+ return this.#settleRequest(token, undefined, resolutionStack, branchDepth);
998
+ }
999
+ return this.#settleDefaultEntry(entry.binding, entry.owner, resolutionStack, branchDepth);
1000
+ }
1001
+ #settleDefaultEntry(binding, owner, resolutionStack, branchDepth) {
862
1002
  if (owner.#isPlainConstant(binding)) {
863
- return Promise.resolve(binding.value);
1003
+ return binding.value;
864
1004
  }
865
1005
  const scope = binding.scope;
866
1006
  if (scope === "transient") {
867
1007
  if ((binding.kind === "dynamic" || binding.kind === "dynamic-async") && !owner.#hasAnyActivation(binding)) {
868
- return this.#resolveTransientDynamicAsyncFromContext(binding, resolutionStack, branchDepth);
1008
+ return this.#settleTransientFactory(binding, resolutionStack, branchDepth);
869
1009
  }
870
1010
  }
871
1011
  else if (scope === "singleton") {
872
1012
  if (binding.instance !== NO_INSTANCE) {
873
- return Promise.resolve(binding.instance);
1013
+ return binding.instance;
874
1014
  }
875
1015
  if (owner !== this) {
876
- return owner.#resolveBindingAsync(binding, undefined, resolutionStack, branchDepth, owner);
1016
+ return owner.#settleBinding(binding, undefined, resolutionStack, branchDepth, owner);
877
1017
  }
878
1018
  }
879
- else if (this.#scope.isChild) {
880
- const cachedScoped = this.#scope.readScoped(binding.identifier);
1019
+ else {
1020
+ const cachedScoped = this.#readScoped(binding);
881
1021
  if (cachedScoped !== SCOPED_MISS) {
882
- return Promise.resolve(cachedScoped);
1022
+ return cachedScoped;
883
1023
  }
884
1024
  }
885
- else {
886
- // Not `#readScoped`: this entry point reports failure as a rejection, never a sync throw.
887
- return Promise.reject(new MissingScopeContextError(tokenName(binding.token)));
888
- }
889
- return this.#resolveBindingAsync(binding, undefined, resolutionStack, branchDepth, owner);
1025
+ return this.#settleBinding(binding, undefined, resolutionStack, branchDepth, owner);
890
1026
  }
891
1027
  /**
892
1028
  * Instantiates one owned binding directly, bypassing selection.
@@ -895,16 +1031,38 @@ export class DependencyResolver {
895
1031
  * could pick a different candidate whose criteria are a subset of them.
896
1032
  */
897
1033
  warmBindingAsync(binding, options) {
898
- return this.#resolveBindingAsync(binding, options, [], ROOT_BRANCH, this);
1034
+ try {
1035
+ return asPromise(this.#settleBinding(binding, options, [], ROOT_BRANCH, this));
1036
+ }
1037
+ catch (bindingError) {
1038
+ return Promise.reject(bindingError);
1039
+ }
1040
+ }
1041
+ resolveAsync(token, options, resolutionStack, branchDepth = UNOWNED_BRANCH, precomputedCriterion) {
1042
+ try {
1043
+ return asPromise(this.#settleRequest(token, options, resolutionStack, branchDepth, precomputedCriterion));
1044
+ }
1045
+ catch (requestError) {
1046
+ return Promise.reject(requestError);
1047
+ }
899
1048
  }
900
- async resolveAsync(token, options, resolutionStack, branchDepth = UNOWNED_BRANCH, precomputedCriterion) {
901
- const { binding, owner } = this.#requireBinding(token, options, resolutionStack, precomputedCriterion);
1049
+ /** Selects the binding a request names and settles it on the async lane. */
1050
+ #settleRequest(token, options, resolutionStack, branchDepth, precomputedCriterion) {
1051
+ const path = ownPrefixOf(resolutionStack, branchDepth);
1052
+ const depth = path === resolutionStack ? branchDepth : branchDepthOf(path);
1053
+ const { binding, owner } = this.#requireBinding(token, options, path, precomputedCriterion);
902
1054
  if (binding.scope === "singleton" && owner !== this) {
903
- return owner.#resolveBindingAsync(binding, options, resolutionStack, branchDepth, owner);
1055
+ return owner.#settleBinding(binding, options, path, depth, owner);
904
1056
  }
905
- return this.#resolveBindingAsync(binding, options, resolutionStack, branchDepth, owner);
1057
+ return this.#settleBinding(binding, options, path, depth, owner);
906
1058
  }
907
- async #resolveBindingAsync(binding, options, resolutionStack, branchDepth, owner) {
1059
+ /**
1060
+ * Materialises one binding on the async lane: the instance once nothing yielded, else its promise.
1061
+ *
1062
+ * @remarks A singleton or scoped instance built in this tick is cached in this tick, so a sibling
1063
+ * reading it synchronously finds it; only a materialisation still pending is published in flight.
1064
+ */
1065
+ #settleBinding(binding, options, resolutionStack, branchDepth, owner) {
908
1066
  if (owner.#isPlainConstant(binding)) {
909
1067
  return binding.value;
910
1068
  }
@@ -916,7 +1074,7 @@ export class DependencyResolver {
916
1074
  // In-flight dedup: concurrent callers share the first creation.
917
1075
  const inflight = this.#scope.getInflight(binding.identifier);
918
1076
  if (inflight !== undefined) {
919
- return inflight;
1077
+ return ownPromiseOf(inflight);
920
1078
  }
921
1079
  if (this.#scope.isClosed) {
922
1080
  throw new DisposedContainerError();
@@ -930,7 +1088,7 @@ export class DependencyResolver {
930
1088
  // In-flight dedup, scoped flavor: one instance per scope even under concurrency.
931
1089
  const inflight = this.#scope.getInflight(binding.identifier);
932
1090
  if (inflight !== undefined) {
933
- return inflight;
1091
+ return ownPromiseOf(inflight);
934
1092
  }
935
1093
  if (this.#scope.isClosed) {
936
1094
  throw new DisposedContainerError();
@@ -943,42 +1101,52 @@ export class DependencyResolver {
943
1101
  const needsActivation = owner.#activationNeed().needsActivation(binding);
944
1102
  if (!needsActivation && scope === "transient" && (binding.kind === "dynamic" || binding.kind === "dynamic-async")) {
945
1103
  const resolutionCtx = new AsyncLevelContext(this, levelStack, options);
946
- if (binding.kind === "dynamic-async") {
947
- return await binding.factory(resolutionCtx);
1104
+ if (branchDepth === ROOT_BRANCH) {
1105
+ this.#recentRootLevelContext = resolutionCtx;
948
1106
  }
949
- const dynamicResult = binding.factory(resolutionCtx);
950
- return dynamicResult instanceof Promise ? await dynamicResult : dynamicResult;
1107
+ return runFactoryPrefix(binding, resolutionCtx, levelStack);
951
1108
  }
952
1109
  const resolutionCtx = needsActivation || requiresResolutionContext(binding)
953
1110
  ? new AsyncLevelContext(this, levelStack, options)
954
1111
  : undefined;
1112
+ if (resolutionCtx !== undefined && branchDepth === ROOT_BRANCH) {
1113
+ this.#recentRootLevelContext = resolutionCtx;
1114
+ }
1115
+ const activated = this.#settleActivated(binding, resolutionCtx, levelStack, levelDepth, needsActivation, owner);
1116
+ if (scope === "transient") {
1117
+ return activated;
1118
+ }
1119
+ if (isPending(activated)) {
1120
+ return this.#publishInFlight(binding, activated, scope);
1121
+ }
955
1122
  if (scope === "singleton") {
956
- // The promise is published before it settles, so concurrent callers dedup onto it.
957
- const singletonPromise = this.#instantiateAndActivateAsync(binding, resolutionCtx, levelStack, levelDepth, needsActivation, owner).then((activated) => {
1123
+ this.#scope.setSingleton(binding, activated);
1124
+ }
1125
+ else {
1126
+ this.#scope.setScoped(binding, activated);
1127
+ }
1128
+ return activated;
1129
+ }
1130
+ /**
1131
+ * Publishes a pending singleton or scoped materialisation so concurrent callers dedup onto it,
1132
+ * and caches what it settles to; the creator, like every joiner, is handed a promise of its own.
1133
+ */
1134
+ #publishInFlight(binding, pending, scope) {
1135
+ const published = pending.then((activated) => {
1136
+ if (scope === "singleton") {
958
1137
  this.#scope.setSingleton(binding, activated);
959
- this.#scope.clearInflight(binding.identifier);
960
- return activated;
961
- }, (error) => {
962
- this.#scope.clearInflight(binding.identifier);
963
- throw error;
964
- });
965
- this.#scope.setInflight(binding.identifier, singletonPromise);
966
- return await singletonPromise;
967
- }
968
- if (scope === "scoped") {
969
- // Published before it settles, like the singleton lane: concurrent callers share one creation.
970
- const scopedPromise = this.#instantiateAndActivateAsync(binding, resolutionCtx, levelStack, levelDepth, needsActivation, owner).then((activated) => {
1138
+ }
1139
+ else {
971
1140
  this.#scope.setScoped(binding, activated);
972
- this.#scope.clearInflight(binding.identifier);
973
- return activated;
974
- }, (error) => {
975
- this.#scope.clearInflight(binding.identifier);
976
- throw error;
977
- });
978
- this.#scope.setInflight(binding.identifier, scopedPromise);
979
- return await scopedPromise;
980
- }
981
- return await this.#instantiateAndActivateAsync(binding, resolutionCtx, levelStack, levelDepth, needsActivation, owner);
1141
+ }
1142
+ this.#scope.clearInflight(binding.identifier);
1143
+ return activated;
1144
+ }, (error) => {
1145
+ this.#scope.clearInflight(binding.identifier);
1146
+ throw error;
1147
+ });
1148
+ this.#scope.setInflight(binding.identifier, published);
1149
+ return ownPromiseOf(published);
982
1150
  }
983
1151
  /**
984
1152
  * Settles this resolver's own `postConstruct` answer for a class binding a parent owns.
@@ -988,103 +1156,179 @@ export class DependencyResolver {
988
1156
  * this binding on every call, forever.
989
1157
  */
990
1158
  #mirrorPostConstructFromOwner(binding, owner) {
991
- if (owner !== this && binding.kind === "class" && this.#classes.knownPostConstruct(binding.target) === undefined) {
992
- this.#classes.discoverPostConstruct(binding.target);
1159
+ if (owner !== this &&
1160
+ binding.kind === "class" &&
1161
+ this.#introspector().knownPostConstruct(binding.target) === undefined) {
1162
+ this.#introspector().discoverPostConstruct(binding.target);
1163
+ }
1164
+ }
1165
+ /** The activated instance: built and activated in this tick when nothing yielded, else its promise. */
1166
+ #settleActivated(binding, ctx, resolutionStack, branchDepth, needsActivation, owner) {
1167
+ const instance = this.#settleInstance(binding, ctx, resolutionStack, branchDepth);
1168
+ if (isPending(instance)) {
1169
+ return instance.then((built) => this.#activateAsync(binding, ctx, built, needsActivation, owner));
993
1170
  }
1171
+ return this.#activateAsync(binding, ctx, instance, needsActivation, owner);
994
1172
  }
995
- async #instantiateAndActivateAsync(binding, ctx, resolutionStack, branchDepth, needsActivation, owner) {
996
- const instance = await this.#instantiateAsync(binding, ctx, resolutionStack, branchDepth);
1173
+ #activateAsync(binding, ctx, instance, needsActivation, owner) {
997
1174
  this.#mirrorPostConstructFromOwner(binding, owner);
998
1175
  if (!owner.#activationNeed().refreshAfterFirstInstantiation(binding, needsActivation)) {
999
1176
  return instance;
1000
1177
  }
1001
1178
  return owner.#lifecycle.runActivation(ctx, binding, instance, owner.#metadataReader);
1002
1179
  }
1003
- async #instantiateAsync(binding, ctx, resolutionStack, branchDepth) {
1180
+ /** The instance a binding builds: in this tick when every dependency settled in it, else its promise. */
1181
+ #settleInstance(binding, ctx, resolutionStack, branchDepth) {
1004
1182
  switch (binding.kind) {
1005
1183
  case "constant":
1006
1184
  return binding.value;
1007
- case "dynamic": {
1185
+ case "dynamic":
1186
+ case "dynamic-async": {
1008
1187
  if (ctx === undefined) {
1009
1188
  throw new InternalError("dynamic binding requires resolution context");
1010
1189
  }
1011
- const factoryResult = binding.factory(ctx);
1012
- return factoryResult instanceof Promise ? factoryResult : Promise.resolve(factoryResult);
1190
+ return runFactoryPrefix(binding, ctx, resolutionStack);
1013
1191
  }
1014
- case "dynamic-async":
1015
- if (ctx === undefined) {
1016
- throw new InternalError("dynamic-async binding requires resolution context");
1017
- }
1018
- return binding.factory(ctx);
1019
1192
  case "class": {
1020
- const deps = await this.#resolveDepsAsync(this.#constructorParams(binding.target), resolutionStack, branchDepth);
1193
+ const introspector = this.#introspector();
1194
+ const target = binding.target;
1195
+ const facts = introspector.facts(target);
1196
+ const params = this.#constructorParams(target, facts);
1197
+ const needsActiveContainer = introspector.needsActiveContainerOf(target, facts);
1021
1198
  // Accessor initializers resolve synchronously, so the branch-owned path serves them directly.
1022
- return this.#classes.instantiate(binding.target, deps, this.#classes.needsActiveContainer(binding.target) ? this.#ambientResolutionFor(resolutionStack) : undefined);
1199
+ const ambient = needsActiveContainer ? this.#ambientResolutionFor(resolutionStack) : undefined;
1200
+ if (params.length === 0) {
1201
+ return introspector.construct(target, NO_ARGUMENTS, needsActiveContainer, ambient);
1202
+ }
1203
+ const deps = this.#settleDeps(params, resolutionStack, branchDepth);
1204
+ if (isPending(deps)) {
1205
+ return deps.then((settled) => introspector.construct(target, settled, needsActiveContainer, ambient));
1206
+ }
1207
+ return introspector.construct(target, deps, needsActiveContainer, ambient);
1023
1208
  }
1024
1209
  case "resolved": {
1025
- const deps = await this.#resolveDepsAsync(binding.deps, resolutionStack, branchDepth);
1026
- const factoryResult = binding.factory(...deps);
1027
- return factoryResult instanceof Promise ? factoryResult : Promise.resolve(factoryResult);
1210
+ const factory = binding.factory;
1211
+ const deps = this.#settleDeps(binding.deps, resolutionStack, branchDepth);
1212
+ return isPending(deps) ? deps.then((settled) => factory(...settled)) : factory(...deps);
1028
1213
  }
1029
1214
  case "resolved-async": {
1030
- const deps = await this.#resolveDepsAsync(binding.deps, resolutionStack, branchDepth);
1031
- return binding.factory(...deps);
1215
+ const factory = binding.factory;
1216
+ const deps = this.#settleDeps(binding.deps, resolutionStack, branchDepth);
1217
+ return isPending(deps) ? deps.then((settled) => factory(...settled)) : factory(...deps);
1032
1218
  }
1033
1219
  case "alias":
1034
1220
  throw new InternalError("alias should have been followed before instantiation");
1035
1221
  }
1036
1222
  }
1037
- async #resolveDepsAsync(deps, resolutionStack, branchDepth) {
1223
+ /**
1224
+ * One level's dependencies: their values when every one settled in this tick, else the fan-out
1225
+ * that settles them in declaration order.
1226
+ */
1227
+ #settleDeps(deps, resolutionStack, branchDepth) {
1038
1228
  const count = deps.length;
1039
1229
  if (count === 0) {
1040
1230
  return [];
1041
1231
  }
1042
1232
  if (count === 1) {
1043
- return [await this.#resolveDepAsync(deps[0], resolutionStack, branchDepth)];
1233
+ const only = this.#settleDep(deps[0], resolutionStack, branchDepth);
1234
+ return isPending(only) ? only.then(settledAlone) : [only];
1044
1235
  }
1045
- // Siblings resolve concurrently and each extends the same branch, so the first appends in
1046
- // place and the rest copy the prefix — no caller has to isolate them.
1236
+ // Siblings start concurrently and each extends the same branch, so the first appends in
1237
+ // place and the rest copy the prefix — no caller has to isolate them. A sibling's sync throw
1238
+ // becomes its slot's rejection so the ones after it still start, as on every other lane.
1047
1239
  const pending = new Array(count);
1240
+ let yielded = false;
1048
1241
  for (let index = 0; index < count; index += 1) {
1049
- pending[index] = this.#resolveDepAsync(deps[index], resolutionStack, branchDepth);
1242
+ let settled;
1243
+ try {
1244
+ settled = this.#settleDep(deps[index], resolutionStack, branchDepth);
1245
+ }
1246
+ catch (dependencyError) {
1247
+ settled = Promise.reject(dependencyError);
1248
+ }
1249
+ if (isPending(settled)) {
1250
+ yielded = true;
1251
+ }
1252
+ pending[index] = settled;
1050
1253
  }
1051
- return Promise.all(pending);
1254
+ return yielded ? settleInOrder(pending, identity) : pending;
1052
1255
  }
1053
- #resolveDepAsync(dep, resolutionStack, branchDepth) {
1256
+ #settleDep(dep, resolutionStack, branchDepth) {
1054
1257
  const options = resolveOptionsForSlot(dep);
1055
1258
  if (dep.multi) {
1056
- return this.resolveAllAsync(dep.token, options, resolutionStack, branchDepth);
1259
+ return this.#settleCollection(dep.token, options, resolutionStack, branchDepth);
1057
1260
  }
1058
1261
  if (dep.optional) {
1059
- return this.resolveOptionalAsync(dep.token, options, resolutionStack, branchDepth, singleCriterionForSlot(dep));
1262
+ return this.#settleOptionalRequest(dep.token, options, resolutionStack, branchDepth, singleCriterionForSlot(dep));
1060
1263
  }
1061
1264
  if (options === undefined) {
1062
- return this.resolveAsyncFromContext(dep.token, resolutionStack, branchDepth);
1265
+ return this.#settleDefault(dep.token, resolutionStack, branchDepth);
1266
+ }
1267
+ return this.#settleRequest(dep.token, options, resolutionStack, branchDepth, singleCriterionForSlot(dep));
1268
+ }
1269
+ resolveOptionalAsync(token, options, resolutionStack, branchDepth = UNOWNED_BRANCH, precomputedCriterion) {
1270
+ try {
1271
+ return asPromise(this.#settleOptionalRequest(token, options, resolutionStack, branchDepth, precomputedCriterion));
1272
+ }
1273
+ catch (requestError) {
1274
+ return Promise.reject(requestError);
1063
1275
  }
1064
- return this.resolveAsync(dep.token, options, resolutionStack, branchDepth, singleCriterionForSlot(dep));
1065
1276
  }
1066
- async resolveOptionalAsync(token, options, resolutionStack, branchDepth = UNOWNED_BRANCH, precomputedCriterion) {
1067
- const entry = this.#findBinding(token, options, resolutionStack, precomputedCriterion === undefined ? singleCriterionOnlyOf(options) : (precomputedCriterion ?? undefined));
1277
+ #settleOptionalRequest(token, options, resolutionStack, branchDepth, precomputedCriterion) {
1278
+ const path = ownPrefixOf(resolutionStack, branchDepth);
1279
+ const depth = path === resolutionStack ? branchDepth : branchDepthOf(path);
1280
+ const singleCriterion = precomputedCriterion === undefined ? singleCriterionOnlyOf(options) : (precomputedCriterion ?? undefined);
1281
+ let entry = this.#findBinding(token, options, path, singleCriterion);
1282
+ // Same single-evaluation contract as the sync lane: resolve what the probe found.
1283
+ if (entry !== undefined && entry.binding.kind === "alias") {
1284
+ // Follow the alias off the throwing path: a chain that ends nowhere is a miss, not an error.
1285
+ entry = this.#terminalOfAliasBinding(entry.binding, options, path, singleCriterion);
1286
+ }
1068
1287
  if (entry === undefined) {
1069
1288
  return undefined;
1070
1289
  }
1071
- // Same single-evaluation contract as the sync lane: resolve what the probe found.
1072
1290
  const { binding, owner } = entry;
1073
- if (binding.kind === "alias") {
1074
- return this.resolveAsync(token, options, resolutionStack, branchDepth);
1075
- }
1076
1291
  if (binding.scope === "singleton" && owner !== this) {
1077
- return owner.#resolveBindingAsync(binding, options, resolutionStack, branchDepth, owner);
1292
+ return owner.#settleBinding(binding, options, path, depth, owner);
1078
1293
  }
1079
- return this.#resolveBindingAsync(binding, options, resolutionStack, branchDepth, owner);
1294
+ return this.#settleBinding(binding, options, path, depth, owner);
1080
1295
  }
1081
- async resolveAllAsync(token, options, resolutionStack, branchDepth = UNOWNED_BRANCH) {
1082
- const candidates = this.#candidateBindings(token, options, resolutionStack);
1083
- const pending = new Array(candidates.length);
1084
- for (let index = 0; index < candidates.length; index += 1) {
1085
- pending[index] = this.#resolveCandidateAsync(candidates[index], options, resolutionStack, branchDepth);
1296
+ resolveAllAsync(token, options, resolutionStack, branchDepth = UNOWNED_BRANCH) {
1297
+ try {
1298
+ return asPromise(this.#settleCollection(token, options, resolutionStack, branchDepth));
1299
+ }
1300
+ catch (collectionError) {
1301
+ return Promise.reject(collectionError);
1086
1302
  }
1087
- return Promise.all(pending);
1303
+ }
1304
+ #settleCollection(token, options, resolutionStack, branchDepth) {
1305
+ const path = ownPrefixOf(resolutionStack, branchDepth);
1306
+ const depth = path === resolutionStack ? branchDepth : branchDepthOf(path);
1307
+ const candidates = this.#candidateBindings(token, options, path);
1308
+ return this.#settleCandidates(candidates, options, path, depth, identity);
1309
+ }
1310
+ /**
1311
+ * Every candidate of a fan-out, started in order: `apply` over their values when all settled in
1312
+ * this tick, else over the fan-out settled in declaration order.
1313
+ */
1314
+ #settleCandidates(candidates, options, resolutionStack, branchDepth, apply) {
1315
+ const count = candidates.length;
1316
+ const pending = new Array(count);
1317
+ let yielded = false;
1318
+ for (let index = 0; index < count; index += 1) {
1319
+ let settled;
1320
+ try {
1321
+ settled = this.#settleCandidate(candidates[index], options, resolutionStack, branchDepth);
1322
+ }
1323
+ catch (memberError) {
1324
+ settled = Promise.reject(memberError);
1325
+ }
1326
+ if (isPending(settled)) {
1327
+ yielded = true;
1328
+ }
1329
+ pending[index] = settled;
1330
+ }
1331
+ return yielded ? settleInOrder(pending, apply) : apply(pending);
1088
1332
  }
1089
1333
  // ── Helpers ────────────────────────────────────────────────────────────────────────────────────────────────────────
1090
1334
  #allBindingsFromChain(token) {
@@ -1165,7 +1409,7 @@ export class DependencyResolver {
1165
1409
  if (!this.#scope.isChild) {
1166
1410
  throw new MissingScopeContextError(tokenName(binding.token));
1167
1411
  }
1168
- return this.#scope.readScoped(binding.identifier);
1412
+ return this.#scope.readScoped(binding.scopedCacheKey);
1169
1413
  }
1170
1414
  // The shared root context answers every top-level request; building one is the rarer half and
1171
1415
  // lives outside, so what a selection inlines is the test and not the literal.
@@ -1173,46 +1417,7 @@ export class DependencyResolver {
1173
1417
  if (options === undefined && resolutionStack.length === 0) {
1174
1418
  return ROOT_CONSTRAINT_CONTEXT;
1175
1419
  }
1176
- return buildConstraintContext(resolutionStack, options);
1177
- }
1178
- /** Selection for a multi-criterion request, over the union of the two tag indexes. */
1179
- #selectMultiTagged(token, options, resolutionStack) {
1180
- const candidates = [];
1181
- const gathered = [];
1182
- if (options.name !== undefined) {
1183
- // Read, not minted: an unminted name has no criterion, so no slot can carry it.
1184
- this.#gatherTagCandidates(token, slotNameCriterionOf(options.name), candidates, gathered);
1185
- }
1186
- this.#gatherTagCandidates(token, options.tag, candidates, gathered);
1187
- const listed = options.tags;
1188
- if (listed !== undefined) {
1189
- for (let index = 0; index < listed.length; index += 1) {
1190
- this.#gatherTagCandidates(token, listed[index], candidates, gathered);
1191
- }
1192
- }
1193
- if (candidates.length === 0) {
1194
- return undefined;
1195
- }
1196
- return selectBinding(candidates, options, this.#makeConstraintContext(resolutionStack, options), tokenName(token));
1197
- }
1198
- /** One request criterion's candidates: its exact single-tag binding, plus its first-tag bucket. */
1199
- #gatherTagCandidates(token, criterion, out, gathered) {
1200
- // Distinct criteria never share a binding — a slot lives in exactly one bucket — so deduping
1201
- // by criterion covers a request repeating one across its spellings, without scanning `out`.
1202
- if (criterion === undefined || gathered.includes(criterion)) {
1203
- return;
1204
- }
1205
- gathered.push(criterion);
1206
- const single = this.#registry.getSimpleTagged(token, criterion);
1207
- if (single !== undefined) {
1208
- out.push(single);
1209
- }
1210
- const bucket = this.#registry.getMultiTagged(token, criterion);
1211
- if (bucket !== undefined) {
1212
- for (let index = 0; index < bucket.length; index += 1) {
1213
- out.push(bucket[index]);
1214
- }
1215
- }
1420
+ return new DefaultConstraintContext(resolutionStack, options);
1216
1421
  }
1217
1422
  /** The predicate half of a match, for a lane whose index has already settled the slot. */
1218
1423
  #satisfiesPredicate(binding, options, resolutionStack) {
@@ -1244,7 +1449,19 @@ export class DependencyResolver {
1244
1449
  binding.inFlight = false;
1245
1450
  }
1246
1451
  }
1247
- // Deliberately not `async`: that would allocate a state machine and a promise per level.
1452
+ /** A transient factory level with no activation: its factory's own result over the level's context. */
1453
+ #settleTransientFactory(binding, resolutionStack, branchDepth) {
1454
+ const frame = this.#getResolutionFrame(binding);
1455
+ const levelStack = extendResolutionBranch(resolutionStack, branchDepth, frame);
1456
+ // Nothing this level appended is ever removed, so no level observes its own settlement.
1457
+ const ctx = new AsyncLevelContext(this, levelStack, undefined);
1458
+ if (branchDepth === ROOT_BRANCH) {
1459
+ this.#recentRootLevelContext = ctx;
1460
+ }
1461
+ return runFactoryPrefix(binding, ctx, levelStack);
1462
+ }
1463
+ // Deliberately not `async`: that would allocate a state machine and a promise per level. The
1464
+ // dominant lane keeps its body whole rather than reaching the settle core through a call.
1248
1465
  #resolveTransientDynamicAsyncFromContext(binding, resolutionStack, branchDepth) {
1249
1466
  const frame = this.#getResolutionFrame(binding);
1250
1467
  let levelStack;
@@ -1257,68 +1474,64 @@ export class DependencyResolver {
1257
1474
  }
1258
1475
  // Nothing this level appended is ever removed, so no level observes its own settlement.
1259
1476
  const ctx = new AsyncLevelContext(this, levelStack, undefined);
1477
+ if (branchDepth === ROOT_BRANCH) {
1478
+ this.#recentRootLevelContext = ctx;
1479
+ }
1260
1480
  try {
1261
- if (binding.kind === "dynamic-async") {
1262
- return binding.factory(ctx);
1263
- }
1264
- const factoryResult = binding.factory(ctx);
1481
+ const factoryResult = runFactoryPrefix(binding, ctx, levelStack);
1265
1482
  return factoryResult instanceof Promise ? factoryResult : Promise.resolve(factoryResult);
1266
1483
  }
1267
1484
  catch (factoryError) {
1268
1485
  return Promise.reject(factoryError);
1269
1486
  }
1270
1487
  }
1271
- // ── The cascade lane ───────────────────────────────────────────────────────────────────────────────────────────────
1488
+ // ── The async root ─────────────────────────────────────────────────────────────────────────────────────────────────
1272
1489
  /**
1273
- * Entry for a request a factory makes from inside an open synchronous cascade.
1490
+ * Entry for an options-less resolve the container starts: the root of a branch of its own.
1274
1491
  *
1275
- * @remarks A request arriving with no cascade open came out of a continuation, so its ancestors
1276
- * are on no call stack — it escapes to the branch lane.
1492
+ * @remarks A statically-visible transient graph answers from its compiled async plan; everything
1493
+ * else opens a branch over a fresh array, so every level's context keeps its own ancestors for as
1494
+ * long as the factory holds it — across an `await` included.
1277
1495
  */
1278
- resolveAsyncFromCascade(token) {
1279
- if (this.#cascadeStack.length === 0) {
1280
- return this.resolveAsyncFromContext(token, [], ROOT_BRANCH);
1281
- }
1282
- return this.#dispatchCascade(token);
1283
- }
1284
- /** Entry for a resolve the container starts, which opens the cascade rather than joining one. */
1285
1496
  resolveAsyncFromRoot(token) {
1286
- return this.#dispatchCascade(token);
1287
- }
1288
- #dispatchCascade(token) {
1289
1497
  const fastBinding = this.#registry.getFastDefault(token);
1290
1498
  if (fastBinding !== undefined) {
1291
- if ((fastBinding.kind === "dynamic-async" || fastBinding.kind === "dynamic") &&
1292
- fastBinding.scope === "transient" &&
1293
- !this.#hasAnyActivation(fastBinding)) {
1294
- return this.#resolveTransientDynamicAsyncCascade(fastBinding);
1295
- }
1296
- // A value that already exists answers here: escaping would snapshot the cascade for a resolve
1297
- // that never looks at a path.
1298
- if (this.#isPlainConstant(fastBinding)) {
1299
- return Promise.resolve(fastBinding.value);
1300
- }
1301
- if (fastBinding.scope === "singleton" && fastBinding.instance !== NO_INSTANCE) {
1302
- return Promise.resolve(fastBinding.instance);
1303
- }
1304
- const planned = this.#plannedCascadeAnswer(fastBinding);
1499
+ const planned = this.#plannedRootAnswer(fastBinding);
1305
1500
  if (planned !== null) {
1306
1501
  return planned;
1307
1502
  }
1503
+ if (fastBinding.scope === "transient" &&
1504
+ (fastBinding.kind === "dynamic" || fastBinding.kind === "dynamic-async") &&
1505
+ !this.#hasAnyActivation(fastBinding)) {
1506
+ return this.#rootFactoryAnswer(fastBinding);
1507
+ }
1308
1508
  }
1309
- // Anything else leaves the cascade lane for good, seeded with a snapshot of the ancestors it
1310
- // accumulated — so a cycle across the boundary is still on one path.
1311
- return this.resolveAsyncFromContext(token, [...this.#cascadeStack], UNOWNED_BRANCH);
1509
+ return this.resolveAsyncFromContext(token, [], ROOT_BRANCH);
1312
1510
  }
1313
1511
  /**
1314
- * A statically-visible transient graph at a true root answers from its compiled async plan.
1512
+ * A transient factory root's answer: its factory run over the one context the binding keeps.
1315
1513
  *
1316
- * @remarks Kept out of the dispatcher so its size stays inlinable. Inside an open cascade the
1317
- * graph must escape instead, so its escapes carry the live ancestors — hence the idle gate.
1514
+ * @remarks The root's path is its own frame alone and the request carries no options, so the level's
1515
+ * context is a function of the binding, built on the first resolve and reused by every later one —
1516
+ * a concurrent root reads the same path, and a descendant that outgrows the branch copies its prefix.
1318
1517
  */
1319
- #plannedCascadeAnswer(fastBinding) {
1320
- if (this.#cascadeStack.length !== 0 ||
1321
- fastBinding.scope !== "transient" ||
1518
+ #rootFactoryAnswer(binding) {
1519
+ let ctx = binding.rootContext;
1520
+ if (ctx === undefined) {
1521
+ ctx = new AsyncLevelContext(this, extendResolutionBranch([], ROOT_BRANCH, this.#getResolutionFrame(binding)), undefined);
1522
+ binding.rootContext = ctx;
1523
+ }
1524
+ try {
1525
+ const factoryResult = runFactoryPrefix(binding, ctx, ctx.ownPath);
1526
+ return factoryResult instanceof Promise ? factoryResult : Promise.resolve(factoryResult);
1527
+ }
1528
+ catch (factoryError) {
1529
+ return Promise.reject(factoryError);
1530
+ }
1531
+ }
1532
+ /** The compiled async plan's answer for a transient class or factory root, or `null` when there is none. */
1533
+ #plannedRootAnswer(fastBinding) {
1534
+ if (fastBinding.scope !== "transient" ||
1322
1535
  (fastBinding.kind !== "class" && fastBinding.kind !== "resolved" && fastBinding.kind !== "resolved-async")) {
1323
1536
  return null;
1324
1537
  }
@@ -1335,32 +1548,6 @@ export class DependencyResolver {
1335
1548
  return Promise.reject(planError);
1336
1549
  }
1337
1550
  }
1338
- #resolveTransientDynamicAsyncCascade(binding) {
1339
- const frame = this.#getResolutionFrame(binding);
1340
- // The request that closes a cycle is made from a factory's synchronous prefix, and synchronous
1341
- // code does not interleave — so the O(1) flag is exact path membership here, as it is for the
1342
- // sync lane. It is cleared when the factory returns its promise, not when that promise settles.
1343
- if (binding.inFlight) {
1344
- return Promise.reject(new CircularDependencyError(cycleNamesOf(this.#cascadeStack, frame.tokenName)));
1345
- }
1346
- const ctx = (this.#cascadeContext ??= new AsyncCascadeContext(this, this.#cascadeStack));
1347
- binding.inFlight = true;
1348
- this.#cascadeStack.push(frame);
1349
- try {
1350
- if (binding.kind === "dynamic-async") {
1351
- return binding.factory(ctx);
1352
- }
1353
- const factoryResult = binding.factory(ctx);
1354
- return factoryResult instanceof Promise ? factoryResult : Promise.resolve(factoryResult);
1355
- }
1356
- catch (factoryError) {
1357
- return Promise.reject(factoryError);
1358
- }
1359
- finally {
1360
- this.#cascadeStack.pop();
1361
- binding.inFlight = false;
1362
- }
1363
- }
1364
1551
  // A cached candidate answers here rather than re-entering the generic path: `resolveAll` pays
1365
1552
  // this per candidate, and a fan-out over cached handlers is the shape that makes it matter.
1366
1553
  #resolveCandidateSync(binding, options, resolutionStack) {
@@ -1385,33 +1572,33 @@ export class DependencyResolver {
1385
1572
  }
1386
1573
  return this.#resolveBinding(binding, options, resolutionStack, owner);
1387
1574
  }
1388
- #resolveCandidateAsync(binding, options, resolutionStack, branchDepth) {
1575
+ #settleCandidate(binding, options, resolutionStack, branchDepth) {
1389
1576
  if (binding.kind === "constant" && binding.activationHook === undefined && this.#chainActivationVersion() === 0) {
1390
- return Promise.resolve(binding.value);
1577
+ return binding.value;
1391
1578
  }
1392
1579
  const owner = this.#ownerOf(binding);
1393
1580
  if (owner.#isPlainConstant(binding)) {
1394
- return Promise.resolve(binding.value);
1581
+ return binding.value;
1395
1582
  }
1396
1583
  if (binding.kind === "alias") {
1397
- return this.resolveAsync(binding.target, options, resolutionStack, branchDepth);
1584
+ return this.#settleRequest(binding.target, options, resolutionStack, branchDepth);
1398
1585
  }
1399
1586
  if (binding.scope === "singleton") {
1400
1587
  if (binding.instance !== NO_INSTANCE) {
1401
- return Promise.resolve(binding.instance);
1588
+ return binding.instance;
1402
1589
  }
1403
- return owner.#resolveBindingAsync(binding, options, resolutionStack, branchDepth, owner);
1590
+ return owner.#settleBinding(binding, options, resolutionStack, branchDepth, owner);
1404
1591
  }
1405
1592
  // The dominant collection member — a transient factory with no activation, asked with no
1406
- // options — takes the non-async lane a single resolve takes, so a fan-out costs one factory
1407
- // promise per member rather than a state machine on top of each.
1593
+ // options — takes the lane a single resolve takes, so a fan-out costs one factory result per
1594
+ // member rather than a promise on top of each.
1408
1595
  if (options === undefined &&
1409
1596
  binding.scope === "transient" &&
1410
1597
  (binding.kind === "dynamic" || binding.kind === "dynamic-async") &&
1411
1598
  !owner.#hasAnyActivation(binding)) {
1412
- return this.#resolveTransientDynamicAsyncFromContext(binding, resolutionStack, branchDepth);
1599
+ return this.#settleTransientFactory(binding, resolutionStack, branchDepth);
1413
1600
  }
1414
- return this.#resolveBindingAsync(binding, options, resolutionStack, branchDepth, owner);
1601
+ return this.#settleBinding(binding, options, resolutionStack, branchDepth, owner);
1415
1602
  }
1416
1603
  /** The resolver whose registry holds `binding` — `this` (the common case) when it is own. */
1417
1604
  #ownerOf(binding) {
@@ -1434,7 +1621,7 @@ export class DependencyResolver {
1434
1621
  if (existing !== undefined) {
1435
1622
  return existing;
1436
1623
  }
1437
- const frame = buildResolutionFrame(tokenName(binding.token), binding.scope, binding.identifier, binding.kind, binding.slot);
1624
+ const frame = buildResolutionFrame(binding, tokenName(binding.token));
1438
1625
  binding.frame = frame;
1439
1626
  return frame;
1440
1627
  }
@@ -1444,32 +1631,18 @@ export class DependencyResolver {
1444
1631
  #acquireSyncResolutionContext(resolutionStack, options) {
1445
1632
  if (resolutionStack === this.rootStack) {
1446
1633
  const depth = resolutionStack.length;
1447
- const existing = this.#syncResolutionContextPool[depth];
1634
+ const pool = (this.#syncResolutionContextPool ??= []);
1635
+ const existing = pool[depth];
1448
1636
  if (existing !== undefined) {
1449
1637
  existing.reset(this, resolutionStack, options);
1450
1638
  return existing;
1451
1639
  }
1452
1640
  const created = new DefaultResolutionContext(this, resolutionStack, options);
1453
- this.#syncResolutionContextPool[depth] = created;
1641
+ pool[depth] = created;
1454
1642
  return created;
1455
1643
  }
1456
- return this.#acquireOffRootSyncContext(resolutionStack, options);
1457
- }
1458
- /** The cascade pair pools separately; a throwaway pair (nested resolve, async snapshot) mints per call. */
1459
- #acquireOffRootSyncContext(resolutionStack, options) {
1460
- if (resolutionStack !== this.#cascadeStack) {
1461
- return new DefaultResolutionContext(this, resolutionStack, options);
1462
- }
1463
- const depth = resolutionStack.length;
1464
- const pool = (this.#cascadeContextPool ??= []);
1465
- const existing = pool[depth];
1466
- if (existing !== undefined) {
1467
- existing.reset(this, resolutionStack, options);
1468
- return existing;
1469
- }
1470
- const created = new DefaultResolutionContext(this, resolutionStack, options);
1471
- pool[depth] = created;
1472
- return created;
1644
+ // A throwaway pair — a nested resolve, an async level's prefix — mints per call.
1645
+ return new DefaultResolutionContext(this, resolutionStack, options);
1473
1646
  }
1474
1647
  }
1475
1648
  /**
@@ -1496,27 +1669,71 @@ function anyPredicate(bindings) {
1496
1669
  }
1497
1670
  return false;
1498
1671
  }
1499
- function buildConstraintContext(resolutionStack, options) {
1500
- return {
1501
- // Derived per read, never cached: the stack may be live, and the names must report it as it stands.
1502
- get resolutionPath() {
1503
- const names = new Array(resolutionStack.length);
1504
- for (let index = 0; index < resolutionStack.length; index += 1) {
1505
- names[index] = resolutionStack[index].tokenName;
1506
- }
1507
- return names;
1508
- },
1509
- resolutionStack,
1510
- parent: resolutionStack.at(-1),
1511
- ancestors: resolutionStack.length > 1 ? resolutionStack.slice(0, -1) : [],
1512
- currentResolveOptions: options,
1513
- };
1672
+ /**
1673
+ * Runs an async lane's factory with its binding flagged in flight for the factory's synchronous prefix.
1674
+ *
1675
+ * @remarks The flag is exact path membership only while synchronous code runs, so it is cleared when
1676
+ * the factory returns — its promise included — never when that promise settles: two branches that
1677
+ * await one binding are a diamond, not a cycle. A factory that resolves its own token from that
1678
+ * prefix is caught before it runs again, on the branch lane as on the sync lanes, where the level's
1679
+ * own flag already covers the whole call.
1680
+ */
1681
+ function runFactoryPrefix(binding, ctx, levelStack) {
1682
+ if (binding.inFlight) {
1683
+ // The level's own frame is the last one, and the path an error names ends where the cycle closed.
1684
+ throw new CircularDependencyError(cycleNamesOf(levelStack.slice(0, -1), tokenName(binding.token)));
1685
+ }
1686
+ binding.inFlight = true;
1687
+ try {
1688
+ return binding.factory(ctx);
1689
+ }
1690
+ finally {
1691
+ binding.inFlight = false;
1692
+ }
1693
+ }
1694
+ /**
1695
+ * The frames a level may read as its own path: the array itself while nothing has grown it past the
1696
+ * level's depth, else a copy of the level's prefix.
1697
+ *
1698
+ * @remarks Siblings start concurrently on one branch and the first appends in place, so a later
1699
+ * sibling that read the whole array would hand a `when()` predicate the first sibling's frame as its
1700
+ * parent. The copy is the one the branch lane would take for that sibling anyway, and it owns itself.
1701
+ */
1702
+ function ownPrefixOf(resolutionStack, branchDepth) {
1703
+ if (branchDepth === UNOWNED_BRANCH || resolutionStack.length === branchDepth) {
1704
+ return resolutionStack;
1705
+ }
1706
+ return resolutionStack.slice(0, branchDepth);
1514
1707
  }
1515
1708
  /** The async-resolution failure for a binding reached on a sync path, naming what to await instead. */
1516
1709
  function asyncResolutionErrorFor(binding, resolutionStack) {
1517
1710
  const sourceName = tokenName(binding.token);
1518
1711
  return new AsyncResolutionError(resolutionStack[0]?.tokenName ?? sourceName, sourceName);
1519
1712
  }
1713
+ /** The settled values of a fan-out, as they are. */
1714
+ function identity(values) {
1715
+ return values;
1716
+ }
1717
+ /** A settled value, as it is. */
1718
+ function settledValue(value) {
1719
+ return value;
1720
+ }
1721
+ /** A promise of its own over a shared in-flight one: the same settlement, never the stored object. */
1722
+ function ownPromiseOf(shared) {
1723
+ return shared.then(settledValue);
1724
+ }
1725
+ /** A lone dependency's settled value as the argument list its level applies. */
1726
+ function settledAlone(value) {
1727
+ return [value];
1728
+ }
1729
+ /** Whether an async-lane answer is still pending, narrowing it to the promise it is. */
1730
+ function isPending(answer) {
1731
+ return answer instanceof Promise;
1732
+ }
1733
+ /** A settled answer as the promise an async entry point hands out; a pending one passes through. */
1734
+ function asPromise(answer) {
1735
+ return answer instanceof Promise ? answer : Promise.resolve(answer);
1736
+ }
1520
1737
  /** Only a factory is handed the resolution context; everything else gets its deps directly. */
1521
1738
  function requiresResolutionContext(binding) {
1522
1739
  return binding.kind === "dynamic" || binding.kind === "dynamic-async";