@codefast/di 0.7.0 → 0.8.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 (173) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +1 -1
  3. package/dist/ambient/active-container.d.ts +4 -0
  4. package/dist/ambient/active-container.d.ts.map +1 -1
  5. package/dist/ambient/active-container.js +4 -0
  6. package/dist/ambient/active-container.js.map +1 -1
  7. package/dist/container/binding-builders.d.ts.map +1 -1
  8. package/dist/container/binding-builders.js +4 -5
  9. package/dist/container/binding-builders.js.map +1 -1
  10. package/dist/container/container.d.ts +6 -0
  11. package/dist/container/container.d.ts.map +1 -1
  12. package/dist/container/container.js +5 -4
  13. package/dist/container/container.js.map +1 -1
  14. package/dist/core/binding.d.ts +15 -2
  15. package/dist/core/binding.d.ts.map +1 -1
  16. package/dist/core/binding.js +29 -4
  17. package/dist/core/binding.js.map +1 -1
  18. package/dist/core/module.d.ts +16 -0
  19. package/dist/core/module.d.ts.map +1 -1
  20. package/dist/core/module.js +8 -0
  21. package/dist/core/module.js.map +1 -1
  22. package/dist/core/registry.d.ts +2 -3
  23. package/dist/core/registry.d.ts.map +1 -1
  24. package/dist/core/registry.js +16 -61
  25. package/dist/core/registry.js.map +1 -1
  26. package/dist/core/tag.d.ts +26 -0
  27. package/dist/core/tag.d.ts.map +1 -1
  28. package/dist/core/tag.js +54 -11
  29. package/dist/core/tag.js.map +1 -1
  30. package/dist/core/token.d.ts +6 -0
  31. package/dist/core/token.d.ts.map +1 -1
  32. package/dist/core/token.js +4 -0
  33. package/dist/core/token.js.map +1 -1
  34. package/dist/core/types.d.ts +20 -0
  35. package/dist/core/types.d.ts.map +1 -1
  36. package/dist/decorators/inject.d.ts +2 -0
  37. package/dist/decorators/inject.d.ts.map +1 -1
  38. package/dist/decorators/inject.js +2 -0
  39. package/dist/decorators/inject.js.map +1 -1
  40. package/dist/decorators/injectable.d.ts +6 -0
  41. package/dist/decorators/injectable.d.ts.map +1 -1
  42. package/dist/decorators/injectable.js +4 -0
  43. package/dist/decorators/injectable.js.map +1 -1
  44. package/dist/decorators/lifecycle-decorators.d.ts +4 -0
  45. package/dist/decorators/lifecycle-decorators.d.ts.map +1 -1
  46. package/dist/decorators/lifecycle-decorators.js +4 -0
  47. package/dist/decorators/lifecycle-decorators.js.map +1 -1
  48. package/dist/errors/errors.d.ts +34 -0
  49. package/dist/errors/errors.d.ts.map +1 -1
  50. package/dist/errors/errors.js +60 -3
  51. package/dist/errors/errors.js.map +1 -1
  52. package/dist/index.d.ts +1 -1
  53. package/dist/index.d.ts.map +1 -1
  54. package/dist/index.js +1 -1
  55. package/dist/index.js.map +1 -1
  56. package/dist/injection/descriptor.d.ts +12 -0
  57. package/dist/injection/descriptor.d.ts.map +1 -1
  58. package/dist/injection/descriptor.js +8 -0
  59. package/dist/injection/descriptor.js.map +1 -1
  60. package/dist/injection/resolve-options.d.ts +17 -12
  61. package/dist/injection/resolve-options.d.ts.map +1 -1
  62. package/dist/injection/resolve-options.js +69 -15
  63. package/dist/injection/resolve-options.js.map +1 -1
  64. package/dist/introspection/dependency-graph.d.ts +6 -0
  65. package/dist/introspection/dependency-graph.d.ts.map +1 -1
  66. package/dist/introspection/dependency-graph.js +24 -9
  67. package/dist/introspection/dependency-graph.js.map +1 -1
  68. package/dist/introspection/graph-adapters/cytoscape.d.ts +8 -0
  69. package/dist/introspection/graph-adapters/cytoscape.d.ts.map +1 -1
  70. package/dist/introspection/graph-adapters/cytoscape.js +2 -0
  71. package/dist/introspection/graph-adapters/cytoscape.js.map +1 -1
  72. package/dist/introspection/graph-adapters/dot.d.ts +2 -0
  73. package/dist/introspection/graph-adapters/dot.d.ts.map +1 -1
  74. package/dist/introspection/graph-adapters/dot.js +2 -0
  75. package/dist/introspection/graph-adapters/dot.js.map +1 -1
  76. package/dist/introspection/graph-adapters/reactflow.d.ts +8 -0
  77. package/dist/introspection/graph-adapters/reactflow.d.ts.map +1 -1
  78. package/dist/introspection/graph-adapters/reactflow.js +2 -0
  79. package/dist/introspection/graph-adapters/reactflow.js.map +1 -1
  80. package/dist/introspection/inspector.d.ts +6 -0
  81. package/dist/introspection/inspector.d.ts.map +1 -1
  82. package/dist/introspection/inspector.js +2 -0
  83. package/dist/introspection/inspector.js.map +1 -1
  84. package/dist/lifecycle/lifecycle-manager.d.ts +2 -0
  85. package/dist/lifecycle/lifecycle-manager.d.ts.map +1 -1
  86. package/dist/lifecycle/lifecycle-manager.js +2 -0
  87. package/dist/lifecycle/lifecycle-manager.js.map +1 -1
  88. package/dist/lifecycle/scope-manager.d.ts +2 -0
  89. package/dist/lifecycle/scope-manager.d.ts.map +1 -1
  90. package/dist/lifecycle/scope-manager.js +2 -0
  91. package/dist/lifecycle/scope-manager.js.map +1 -1
  92. package/dist/metadata/metadata-keys.d.ts +6 -0
  93. package/dist/metadata/metadata-keys.d.ts.map +1 -1
  94. package/dist/metadata/metadata-keys.js +6 -0
  95. package/dist/metadata/metadata-keys.js.map +1 -1
  96. package/dist/metadata/metadata-reader-token.d.ts +2 -0
  97. package/dist/metadata/metadata-reader-token.d.ts.map +1 -1
  98. package/dist/metadata/metadata-reader-token.js +2 -0
  99. package/dist/metadata/metadata-reader-token.js.map +1 -1
  100. package/dist/metadata/metadata-types.d.ts +6 -0
  101. package/dist/metadata/metadata-types.d.ts.map +1 -1
  102. package/dist/metadata/symbol-metadata-reader.d.ts +4 -0
  103. package/dist/metadata/symbol-metadata-reader.d.ts.map +1 -1
  104. package/dist/metadata/symbol-metadata-reader.js +4 -0
  105. package/dist/metadata/symbol-metadata-reader.js.map +1 -1
  106. package/dist/resolution/cache/activation-need.d.ts +2 -0
  107. package/dist/resolution/cache/activation-need.d.ts.map +1 -1
  108. package/dist/resolution/cache/activation-need.js +2 -0
  109. package/dist/resolution/cache/activation-need.js.map +1 -1
  110. package/dist/resolution/cache/binding-lookup-cache.d.ts +2 -2
  111. package/dist/resolution/cache/binding-lookup-cache.d.ts.map +1 -1
  112. package/dist/resolution/cache/binding-lookup-cache.js +2 -37
  113. package/dist/resolution/cache/binding-lookup-cache.js.map +1 -1
  114. package/dist/resolution/cache/class-introspector.d.ts +2 -0
  115. package/dist/resolution/cache/class-introspector.d.ts.map +1 -1
  116. package/dist/resolution/cache/class-introspector.js +2 -0
  117. package/dist/resolution/cache/class-introspector.js.map +1 -1
  118. package/dist/resolution/context.d.ts +6 -0
  119. package/dist/resolution/context.d.ts.map +1 -1
  120. package/dist/resolution/context.js +4 -0
  121. package/dist/resolution/context.js.map +1 -1
  122. package/dist/resolution/plan/instantiation-plan.d.ts +6 -12
  123. package/dist/resolution/plan/instantiation-plan.d.ts.map +1 -1
  124. package/dist/resolution/plan/instantiation-plan.js +11 -27
  125. package/dist/resolution/plan/instantiation-plan.js.map +1 -1
  126. package/dist/resolution/resolver.d.ts +14 -5
  127. package/dist/resolution/resolver.d.ts.map +1 -1
  128. package/dist/resolution/resolver.js +108 -141
  129. package/dist/resolution/resolver.js.map +1 -1
  130. package/dist/resolution/select/binding-select.d.ts +4 -5
  131. package/dist/resolution/select/binding-select.d.ts.map +1 -1
  132. package/dist/resolution/select/binding-select.js +18 -20
  133. package/dist/resolution/select/binding-select.js.map +1 -1
  134. package/dist/resolution/select/constraints.d.ts +16 -0
  135. package/dist/resolution/select/constraints.d.ts.map +1 -1
  136. package/dist/resolution/select/constraints.js +16 -0
  137. package/dist/resolution/select/constraints.js.map +1 -1
  138. package/package.json +13 -6
  139. package/src/ambient/active-container.ts +4 -0
  140. package/src/container/binding-builders.ts +10 -5
  141. package/src/container/container.ts +9 -4
  142. package/src/core/binding.ts +33 -5
  143. package/src/core/module.ts +16 -0
  144. package/src/core/registry.ts +16 -66
  145. package/src/core/tag.ts +71 -17
  146. package/src/core/token.ts +6 -0
  147. package/src/core/types.ts +20 -0
  148. package/src/decorators/inject.ts +2 -0
  149. package/src/decorators/injectable.ts +8 -0
  150. package/src/decorators/lifecycle-decorators.ts +4 -0
  151. package/src/errors/errors.ts +62 -5
  152. package/src/index.ts +1 -1
  153. package/src/injection/descriptor.ts +12 -0
  154. package/src/injection/resolve-options.ts +75 -18
  155. package/src/introspection/dependency-graph.ts +29 -9
  156. package/src/introspection/graph-adapters/cytoscape.ts +8 -0
  157. package/src/introspection/graph-adapters/dot.ts +2 -0
  158. package/src/introspection/graph-adapters/reactflow.ts +8 -0
  159. package/src/introspection/inspector.ts +6 -0
  160. package/src/lifecycle/lifecycle-manager.ts +2 -0
  161. package/src/lifecycle/scope-manager.ts +2 -0
  162. package/src/metadata/metadata-keys.ts +6 -0
  163. package/src/metadata/metadata-reader-token.ts +2 -0
  164. package/src/metadata/metadata-types.ts +6 -0
  165. package/src/metadata/symbol-metadata-reader.ts +4 -0
  166. package/src/resolution/cache/activation-need.ts +2 -0
  167. package/src/resolution/cache/binding-lookup-cache.ts +2 -40
  168. package/src/resolution/cache/class-introspector.ts +2 -0
  169. package/src/resolution/context.ts +6 -0
  170. package/src/resolution/plan/instantiation-plan.ts +15 -38
  171. package/src/resolution/resolver.ts +122 -141
  172. package/src/resolution/select/binding-select.ts +18 -21
  173. package/src/resolution/select/constraints.ts +16 -0
@@ -1,8 +1,8 @@
1
1
  import { NO_INSTANCE } from "#/core/binding";
2
- import { NO_TAG_KEYS } from "#/core/tag";
2
+ import { NO_TAG_KEYS, slotNameCriterionOf } from "#/core/tag";
3
3
  import { tokenName } from "#/core/token";
4
4
  import { AsyncActivationError, AsyncResolutionError, CircularDependencyError, DisposedContainerError, InternalError, MissingMetadataError, MissingScopeContextError, NoMatchingBindingError, TokenNotBoundError, } from "#/errors/errors";
5
- import { isNameOnlyOptions, resolveOptionsForSlot, singleTagOnlyOf } from "#/injection/resolve-options";
5
+ import { resolveOptionsForSlot, singleCriterionForSlot, singleCriterionOnlyOf } from "#/injection/resolve-options";
6
6
  import { SCOPED_MISS } from "#/lifecycle/scope-manager";
7
7
  import { ActivationNeedCache } from "#/resolution/cache/activation-need";
8
8
  import { BindingLookupCache } from "#/resolution/cache/binding-lookup-cache";
@@ -24,6 +24,8 @@ const ROOT_CONSTRAINT_CONTEXT = {
24
24
  currentResolveOptions: undefined,
25
25
  };
26
26
  /**
27
+ * The resolution engine driving binding selection, instantiation, scoping, and lifecycle hooks.
28
+ *
27
29
  * @since 0.3.16-canary.0
28
30
  */
29
31
  export class DependencyResolver {
@@ -94,49 +96,47 @@ export class DependencyResolver {
94
96
  };
95
97
  }
96
98
  // ── Binding lookup ─────────────────────────────────────────────────────────────────────────────────────────────────
97
- #findBinding(token, options, resolutionStack) {
99
+ /**
100
+ * Finds the binding a request selects in this container, walking up to the parent on a miss.
101
+ *
102
+ * @remarks `singleCriterion` is the request's lone criterion, folded once by the caller so alias
103
+ * hops and parent walks do not re-fold it — `undefined` when the request carries none or several.
104
+ */
105
+ #findBinding(token, options, resolutionStack, singleCriterion) {
98
106
  if (options === undefined) {
99
107
  const fastDefaultBinding = this.#registry.getFastDefault(token);
100
108
  if (fastDefaultBinding !== undefined) {
101
109
  return { binding: fastDefaultBinding, owner: this };
102
110
  }
103
111
  }
104
- else if (isNameOnlyOptions(options)) {
105
- const namedBinding = this.#registry.getSimpleNamed(token, options.name);
106
- if (namedBinding !== undefined && this.#matchesBindingFast(namedBinding, options, resolutionStack)) {
107
- return { binding: namedBinding, owner: this };
112
+ else if (singleCriterion !== undefined) {
113
+ const indexed = this.#registry.getSimpleTagged(token, singleCriterion);
114
+ if (indexed !== undefined && this.#satisfiesPredicate(indexed, options, resolutionStack)) {
115
+ return { binding: indexed, owner: this };
108
116
  }
109
117
  }
110
- else {
111
- const singleTag = singleTagOnlyOf(options);
112
- if (singleTag !== undefined) {
113
- const tagged = this.#registry.getSimpleTagged(token, singleTag);
114
- if (tagged !== undefined && this.#satisfiesPredicate(tagged, options, resolutionStack)) {
115
- return { binding: tagged, owner: this };
116
- }
117
- }
118
- else if (options.name === undefined &&
119
- // A threshold switches the data structure, never the semantics: under it the generic scan
120
- // below beats walking the indexes, and both paths answer identically. Sized first, so a
121
- // small list pays one length read and nothing else.
122
- this.#registry.getAll(token).length > MULTI_TAG_INDEX_THRESHOLD &&
123
- requestedTagKeyMask(options) !== NO_TAG_KEYS) {
124
- // A name-less multi-tag request matches only name-less tagged slots, and every such slot
125
- // lives in one of the two tag indexes — so their union is the whole candidate set and the
126
- // token's full list never needs scanning. Selection still owns predicates and specificity.
127
- const selected = this.#selectMultiTagged(token, options, resolutionStack);
128
- if (selected !== undefined) {
129
- return { binding: selected, owner: this };
130
- }
131
- return this.#parent === undefined ? undefined : this.#parent.#findBinding(token, options, resolutionStack);
118
+ else if (
119
+ // A threshold switches the data structure, never the semantics: under it the generic scan
120
+ // below beats walking the indexes, and both paths answer identically. Sized first, so a
121
+ // small list pays one length read and nothing else.
122
+ this.#registry.getAll(token).length > MULTI_TAG_INDEX_THRESHOLD &&
123
+ requestedTagKeyMask(options) !== NO_TAG_KEYS) {
124
+ // A multi-criterion request matches only slots whose every criterion it carries, and every
125
+ // such slot is in the two tag indexes — their union is the whole candidate set, unscanned.
126
+ const selected = this.#selectMultiTagged(token, options, resolutionStack);
127
+ if (selected !== undefined) {
128
+ return { binding: selected, owner: this };
132
129
  }
130
+ return this.#parent === undefined
131
+ ? undefined
132
+ : this.#parent.#findBinding(token, options, resolutionStack, singleCriterion);
133
133
  }
134
134
  const bindings = this.#registry.getAll(token);
135
135
  if (bindings.length > 0) {
136
136
  // A lone candidate is its own selection: matching it is the whole decision, with no
137
137
  // specificity to weigh and no ambiguity to report.
138
138
  const selected = bindings.length === 1
139
- ? this.#matchesBindingFast(bindings[0], options, resolutionStack)
139
+ ? matchesSlot(bindings[0].slot, options) && this.#satisfiesPredicate(bindings[0], options, resolutionStack)
140
140
  ? bindings[0]
141
141
  : undefined
142
142
  : selectBinding(bindings, options, this.#makeConstraintContext(resolutionStack, options), tokenName(token));
@@ -145,7 +145,7 @@ export class DependencyResolver {
145
145
  }
146
146
  }
147
147
  if (this.#parent !== undefined) {
148
- return this.#parent.#findBinding(token, options, resolutionStack);
148
+ return this.#parent.#findBinding(token, options, resolutionStack, singleCriterion);
149
149
  }
150
150
  return undefined;
151
151
  }
@@ -155,10 +155,12 @@ export class DependencyResolver {
155
155
  * @remarks Alias hops are followed iteratively with exact cycle detection — a revisited alias
156
156
  * token raises {@link CircularDependencyError} instead of overflowing the call stack.
157
157
  */
158
- #requireBinding(token, options, resolutionStack) {
158
+ #requireBinding(token, options, resolutionStack, precomputedCriterion) {
159
+ // `null` is a caller's "folded: none" — only an absent precomputation re-folds.
160
+ const singleCriterion = precomputedCriterion === undefined ? singleCriterionOnlyOf(options) : (precomputedCriterion ?? undefined);
159
161
  let currentToken = token;
160
162
  let visitedAliasTokens;
161
- let found = this.#findBinding(currentToken, options, resolutionStack);
163
+ let found = this.#findBinding(currentToken, options, resolutionStack, singleCriterion);
162
164
  while (found !== undefined && found.binding.kind === "alias") {
163
165
  const target = found.binding.target;
164
166
  visitedAliasTokens ??= new Set([currentToken]);
@@ -167,7 +169,7 @@ export class DependencyResolver {
167
169
  }
168
170
  visitedAliasTokens.add(target);
169
171
  currentToken = target;
170
- found = this.#findBinding(currentToken, options, resolutionStack);
172
+ found = this.#findBinding(currentToken, options, resolutionStack, singleCriterion);
171
173
  }
172
174
  if (found === undefined) {
173
175
  // Thrown here rather than from a helper: the error captures this stack, and an error path is
@@ -183,7 +185,7 @@ export class DependencyResolver {
183
185
  * Binding lookup aligned with `resolve` — used by `Container.validate` without instantiating.
184
186
  */
185
187
  peekBindingForValidate(token, options) {
186
- return this.#findBinding(token, options, []);
188
+ return this.#findBinding(token, options, [], singleCriterionOnlyOf(options));
187
189
  }
188
190
  /**
189
191
  * Mirrors {@link DependencyResolver.resolveAll} candidate selection only (no instantiation).
@@ -327,22 +329,14 @@ export class DependencyResolver {
327
329
  const entry = this.#lookup.defaultEntry(token);
328
330
  return entry === null ? null : { binding: entry.binding };
329
331
  },
330
- // Exactly what #findBinding's named lane accepts, minus the half that reads a path: a predicate
331
- // is the compiler's cue to leave the selection to the runtime.
332
- lookupPathIndependentNamedEntry: (token, options) => {
333
- const entry = this.#lookup.namedEntry(token, options.name);
334
- if (entry === null || entry.binding.predicate !== undefined || !matchesSlot(entry.binding.slot, options)) {
332
+ // Exactly what #findBinding's single-criterion lane accepts, minus the half that reads a path:
333
+ // a predicate is the compiler's cue to leave the selection to the runtime.
334
+ lookupPathIndependentEntry: (token, options) => {
335
+ const singleCriterion = singleCriterionOnlyOf(options);
336
+ if (singleCriterion === undefined) {
335
337
  return null;
336
338
  }
337
- return { binding: entry.binding };
338
- },
339
- // The named rule verbatim, on the single-tag lane's memo.
340
- lookupPathIndependentTaggedEntry: (token, options) => {
341
- const singleTag = singleTagOnlyOf(options);
342
- if (singleTag === undefined) {
343
- return null;
344
- }
345
- const entry = this.#lookup.taggedEntry(token, singleTag);
339
+ const entry = this.#lookup.taggedEntry(token, singleCriterion);
346
340
  if (entry === null || entry.binding.predicate !== undefined || !matchesSlot(entry.binding.slot, options)) {
347
341
  return null;
348
342
  }
@@ -398,40 +392,28 @@ export class DependencyResolver {
398
392
  this.#asyncPlanByBindingId.set(binding.id, compiled);
399
393
  return compiled;
400
394
  }
401
- resolve(token, options, resolutionStack) {
402
- // Name-only fast lane: memoized lookup, dispatching just the shapes whose
403
- // semantics involve no resolution context (constants, cached singletons).
404
- if (options !== undefined && isNameOnlyOptions(options)) {
405
- const namedEntry = this.#lookup.namedEntry(token, options.name);
406
- if (namedEntry !== null) {
407
- const namedBinding = namedEntry.binding;
408
- if (namedEntry.owner.#isPlainConstant(namedBinding)) {
409
- return namedBinding.value;
410
- }
411
- if (namedBinding.scope === "singleton" && namedBinding.instance !== NO_INSTANCE) {
412
- return namedBinding.instance;
413
- }
414
- // Everything else keeps the full path (context, activation, guards).
415
- }
416
- }
417
- else if (options !== undefined) {
418
- // Single-tag fast lane: the named lane's tagged twin, memoizing the chain walk.
419
- const singleTag = singleTagOnlyOf(options);
420
- if (singleTag !== undefined) {
421
- const taggedEntry = this.#lookup.taggedEntry(token, singleTag);
422
- if (taggedEntry !== null) {
423
- const taggedBinding = taggedEntry.binding;
424
- if (taggedEntry.owner.#isPlainConstant(taggedBinding)) {
425
- return taggedBinding.value;
395
+ resolve(token, options, resolutionStack, precomputedCriterion) {
396
+ // Single-criterion fast lane (a lone name folds here too): memoized lookup, dispatching just
397
+ // the shapes whose semantics involve no resolution context (constants, cached singletons).
398
+ let singleCriterion;
399
+ if (options !== undefined) {
400
+ singleCriterion =
401
+ precomputedCriterion === undefined ? singleCriterionOnlyOf(options) : (precomputedCriterion ?? undefined);
402
+ if (singleCriterion !== undefined) {
403
+ const indexedEntry = this.#lookup.taggedEntry(token, singleCriterion);
404
+ if (indexedEntry !== null) {
405
+ const indexedBinding = indexedEntry.binding;
406
+ if (indexedEntry.owner.#isPlainConstant(indexedBinding)) {
407
+ return indexedBinding.value;
426
408
  }
427
- if (taggedBinding.scope === "singleton" && taggedBinding.instance !== NO_INSTANCE) {
428
- return taggedBinding.instance;
409
+ if (indexedBinding.scope === "singleton" && indexedBinding.instance !== NO_INSTANCE) {
410
+ return indexedBinding.instance;
429
411
  }
430
412
  // Everything else keeps the full path (context, activation, guards).
431
413
  }
432
414
  }
433
415
  }
434
- const { binding, owner } = this.#requireBinding(token, options, resolutionStack);
416
+ const { binding, owner } = this.#requireBinding(token, options, resolutionStack, singleCriterion ?? null);
435
417
  // A singleton owned by a parent resolver is resolved there, so the parent caches it.
436
418
  if (binding.scope === "singleton" && owner !== this) {
437
419
  return owner.#resolveBinding(binding, options, resolutionStack, owner);
@@ -582,15 +564,15 @@ export class DependencyResolver {
582
564
  return this.resolveAll(dep.token, options, resolutionStack);
583
565
  }
584
566
  if (dep.optional) {
585
- return this.resolveOptional(dep.token, options, resolutionStack);
567
+ return this.resolveOptional(dep.token, options, resolutionStack, singleCriterionForSlot(dep));
586
568
  }
587
569
  if (options === undefined) {
588
570
  return this.resolveFromContext(dep.token, resolutionStack);
589
571
  }
590
- return this.resolve(dep.token, options, resolutionStack);
572
+ return this.resolve(dep.token, options, resolutionStack, singleCriterionForSlot(dep));
591
573
  }
592
- resolveOptional(token, options, resolutionStack) {
593
- const entry = this.#findBinding(token, options, resolutionStack);
574
+ resolveOptional(token, options, resolutionStack, precomputedCriterion) {
575
+ const entry = this.#findBinding(token, options, resolutionStack, precomputedCriterion === undefined ? singleCriterionOnlyOf(options) : (precomputedCriterion ?? undefined));
594
576
  if (entry === undefined) {
595
577
  return undefined;
596
578
  }
@@ -677,8 +659,17 @@ export class DependencyResolver {
677
659
  }
678
660
  return this.#resolveBindingAsync(binding, undefined, resolutionStack, branchDepth, owner);
679
661
  }
680
- async resolveAsync(token, options, resolutionStack, branchDepth = UNOWNED_BRANCH) {
681
- const { binding, owner } = this.#requireBinding(token, options, resolutionStack);
662
+ /**
663
+ * Instantiates one owned binding directly, bypassing selection.
664
+ *
665
+ * @remarks Warm-up must build the binding it inspected: re-selecting by the slot's own criteria
666
+ * could pick a different candidate whose criteria are a subset of them.
667
+ */
668
+ warmBindingAsync(binding, options) {
669
+ return this.#resolveBindingAsync(binding, options, [], ROOT_BRANCH, this);
670
+ }
671
+ async resolveAsync(token, options, resolutionStack, branchDepth = UNOWNED_BRANCH, precomputedCriterion) {
672
+ const { binding, owner } = this.#requireBinding(token, options, resolutionStack, precomputedCriterion);
682
673
  if (binding.scope === "singleton" && owner !== this) {
683
674
  return owner.#resolveBindingAsync(binding, options, resolutionStack, branchDepth, owner);
684
675
  }
@@ -836,15 +827,15 @@ export class DependencyResolver {
836
827
  return this.resolveAllAsync(dep.token, options, resolutionStack, branchDepth);
837
828
  }
838
829
  if (dep.optional) {
839
- return this.resolveOptionalAsync(dep.token, options, resolutionStack, branchDepth);
830
+ return this.resolveOptionalAsync(dep.token, options, resolutionStack, branchDepth, singleCriterionForSlot(dep));
840
831
  }
841
832
  if (options === undefined) {
842
833
  return this.resolveAsyncFromContext(dep.token, resolutionStack, branchDepth);
843
834
  }
844
- return this.resolveAsync(dep.token, options, resolutionStack, branchDepth);
835
+ return this.resolveAsync(dep.token, options, resolutionStack, branchDepth, singleCriterionForSlot(dep));
845
836
  }
846
- async resolveOptionalAsync(token, options, resolutionStack, branchDepth = UNOWNED_BRANCH) {
847
- const entry = this.#findBinding(token, options, resolutionStack);
837
+ async resolveOptionalAsync(token, options, resolutionStack, branchDepth = UNOWNED_BRANCH, precomputedCriterion) {
838
+ const entry = this.#findBinding(token, options, resolutionStack, precomputedCriterion === undefined ? singleCriterionOnlyOf(options) : (precomputedCriterion ?? undefined));
848
839
  if (entry === undefined) {
849
840
  return undefined;
850
841
  }
@@ -881,56 +872,34 @@ export class DependencyResolver {
881
872
  }
882
873
  return result;
883
874
  }
884
- /** Every binding the chain's name indexes hold for one name, nearest container first. */
885
- #namedBindingsFromChain(token, name) {
886
- // A name resolves to at most one binding per registry, so a root container's answer is built
887
- // whole rather than grown — the list is sized at its allocation.
888
- const ownBinding = this.#registry.getSimpleNamed(token, name);
889
- if (this.#parent === undefined) {
890
- return ownBinding === undefined ? [] : [ownBinding];
891
- }
892
- const result = ownBinding === undefined ? [] : [ownBinding];
893
- for (let current = this.#parent; current !== undefined; current = current.#parent) {
894
- const binding = current.#registry.getSimpleNamed(token, name);
895
- if (binding !== undefined) {
896
- result.push(binding);
897
- }
898
- }
899
- return result;
900
- }
901
875
  /**
902
- * The candidates an index can name outright, or `null` when the request needs full selection.
876
+ * The candidates the index can name outright, or `null` when the request needs full selection.
903
877
  *
904
- * @remarks Kept off `#candidateBindings` so that method stays the size it was: a request neither
905
- * index serves must not pay for the two that do.
878
+ * @remarks Kept off `#candidateBindings` so that method stays the size it was: a request the
879
+ * index cannot serve must not pay for the shape it does.
906
880
  * An index has matched the slot already, but a hit may still carry a predicate, and evaluating
907
881
  * that is the selection path's job.
908
882
  */
909
883
  #indexedCandidates(token, options, resolutionStack) {
910
- if (isNameOnlyOptions(options)) {
911
- const named = this.#namedBindingsFromChain(token, options.name);
912
- return anyPredicate(named)
913
- ? selectAllBindings(named, options, this.#makeConstraintContext(resolutionStack, options))
914
- : named;
915
- }
916
- const singleTag = singleTagOnlyOf(options);
917
- if (singleTag === undefined) {
884
+ const singleCriterion = singleCriterionOnlyOf(options);
885
+ if (singleCriterion === undefined) {
918
886
  return null;
919
887
  }
920
- const tagged = this.#taggedBindingsFromChain(token, singleTag);
921
- return anyPredicate(tagged)
922
- ? selectAllBindings(tagged, options, this.#makeConstraintContext(resolutionStack, options))
923
- : tagged;
888
+ const indexed = this.#taggedBindingsFromChain(token, singleCriterion);
889
+ return anyPredicate(indexed)
890
+ ? selectAllBindings(indexed, options, this.#makeConstraintContext(resolutionStack, options))
891
+ : indexed;
924
892
  }
925
893
  /**
926
- * Every binding the chain's tag indexes hold for one tag, nearest container first.
894
+ * Every binding the chain's criterion indexes hold for one criterion, nearest container first.
927
895
  *
928
- * @remarks A request for one tag and no name matches exactly the bindings the index keys, so this
929
- * is the whole candidate set rather than a prefilter — a named or multi-tag slot cannot satisfy it.
896
+ * @remarks A request for exactly one criterion matches exactly the bindings the index keys, so
897
+ * this is the whole candidate set rather than a prefilter — a multi-criterion slot cannot satisfy
898
+ * it.
930
899
  */
931
900
  #taggedBindingsFromChain(token, tag) {
932
- // A tag matches at most one binding per registry, so a root container's answer is built whole
933
- // rather than grown — the shape `#namedBindingsFromChain` takes, for the same reason.
901
+ // A criterion matches at most one binding per registry, so a root container's answer is built
902
+ // whole rather than grown — the list is sized at its allocation.
934
903
  const ownBinding = this.#registry.getSimpleTagged(token, tag);
935
904
  if (this.#parent === undefined) {
936
905
  return ownBinding === undefined ? [] : [ownBinding];
@@ -977,14 +946,19 @@ export class DependencyResolver {
977
946
  }
978
947
  return buildConstraintContext(resolutionStack, options);
979
948
  }
980
- /** Selection for a name-less multi-tag request, over the union of the two tag indexes. */
949
+ /** Selection for a multi-criterion request, over the union of the two tag indexes. */
981
950
  #selectMultiTagged(token, options, resolutionStack) {
982
951
  const candidates = [];
983
- this.#gatherTagCandidates(token, options.tag, candidates);
952
+ const gathered = [];
953
+ if (options.name !== undefined) {
954
+ // Read, not minted: an unminted name has no criterion, so no slot can carry it.
955
+ this.#gatherTagCandidates(token, slotNameCriterionOf(options.name), candidates, gathered);
956
+ }
957
+ this.#gatherTagCandidates(token, options.tag, candidates, gathered);
984
958
  const listed = options.tags;
985
959
  if (listed !== undefined) {
986
960
  for (let index = 0; index < listed.length; index += 1) {
987
- this.#gatherTagCandidates(token, listed[index], candidates);
961
+ this.#gatherTagCandidates(token, listed[index], candidates, gathered);
988
962
  }
989
963
  }
990
964
  if (candidates.length === 0) {
@@ -993,31 +967,24 @@ export class DependencyResolver {
993
967
  return selectBinding(candidates, options, this.#makeConstraintContext(resolutionStack, options), tokenName(token));
994
968
  }
995
969
  /** One request criterion's candidates: its exact single-tag binding, plus its first-tag bucket. */
996
- #gatherTagCandidates(token, criterion, out) {
997
- if (criterion === undefined) {
970
+ #gatherTagCandidates(token, criterion, out, gathered) {
971
+ // Distinct criteria never share a binding — a slot lives in exactly one bucket — so deduping
972
+ // by criterion covers a request repeating one across its spellings, without scanning `out`.
973
+ if (criterion === undefined || gathered.includes(criterion)) {
998
974
  return;
999
975
  }
1000
- // The includes probes only guard a request repeating a criterion across its two spellings.
976
+ gathered.push(criterion);
1001
977
  const single = this.#registry.getSimpleTagged(token, criterion);
1002
- if (single !== undefined && !out.includes(single)) {
978
+ if (single !== undefined) {
1003
979
  out.push(single);
1004
980
  }
1005
981
  const bucket = this.#registry.getMultiTagged(token, criterion);
1006
982
  if (bucket !== undefined) {
1007
983
  for (let index = 0; index < bucket.length; index += 1) {
1008
- const candidate = bucket[index];
1009
- if (!out.includes(candidate)) {
1010
- out.push(candidate);
1011
- }
984
+ out.push(bucket[index]);
1012
985
  }
1013
986
  }
1014
987
  }
1015
- #matchesBindingFast(binding, options, resolutionStack) {
1016
- if (!matchesSlot(binding.slot, options)) {
1017
- return false;
1018
- }
1019
- return this.#satisfiesPredicate(binding, options, resolutionStack);
1020
- }
1021
988
  /** The predicate half of a match, for a lane whose index has already settled the slot. */
1022
989
  #satisfiesPredicate(binding, options, resolutionStack) {
1023
990
  const predicate = binding.predicate;