@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
@@ -3,7 +3,7 @@ import type { Container } from "#/container/container";
3
3
  import type { Binding, ConstantBinding, DynamicAsyncBinding, DynamicBinding } from "#/core/binding";
4
4
  import { NO_INSTANCE } from "#/core/binding";
5
5
  import type { BindingRegistry } from "#/core/registry";
6
- import { NO_TAG_KEYS } from "#/core/tag";
6
+ import { NO_TAG_KEYS, slotNameCriterionOf } from "#/core/tag";
7
7
  import type { Token } from "#/core/token";
8
8
  import { tokenName } from "#/core/token";
9
9
  import type {
@@ -28,7 +28,7 @@ import {
28
28
  TokenNotBoundError,
29
29
  } from "#/errors/errors";
30
30
  import type { DependencySlot } from "#/injection/resolve-options";
31
- import { isNameOnlyOptions, resolveOptionsForSlot, singleTagOnlyOf } from "#/injection/resolve-options";
31
+ import { resolveOptionsForSlot, singleCriterionForSlot, singleCriterionOnlyOf } from "#/injection/resolve-options";
32
32
  import type { LifecycleManager } from "#/lifecycle/lifecycle-manager";
33
33
  import type { ScopeManager } from "#/lifecycle/scope-manager";
34
34
  import { SCOPED_MISS } from "#/lifecycle/scope-manager";
@@ -71,6 +71,8 @@ const ROOT_CONSTRAINT_CONTEXT = {
71
71
  };
72
72
 
73
73
  /**
74
+ * The resolution engine driving binding selection, instantiation, scoping, and lifecycle hooks.
75
+ *
74
76
  * @since 0.3.16-canary.0
75
77
  */
76
78
  export class DependencyResolver implements ResolverCallbacks {
@@ -161,45 +163,44 @@ export class DependencyResolver implements ResolverCallbacks {
161
163
 
162
164
  // ── Binding lookup ─────────────────────────────────────────────────────────────────────────────────────────────────
163
165
 
166
+ /**
167
+ * Finds the binding a request selects in this container, walking up to the parent on a miss.
168
+ *
169
+ * @remarks `singleCriterion` is the request's lone criterion, folded once by the caller so alias
170
+ * hops and parent walks do not re-fold it — `undefined` when the request carries none or several.
171
+ */
164
172
  #findBinding(
165
173
  token: Token<unknown> | Constructor,
166
174
  options: ResolveOptions | undefined,
167
175
  resolutionStack: Array<ResolutionFrame>,
176
+ singleCriterion: BindingTag | undefined,
168
177
  ): DefaultLookupEntry<DependencyResolver> | undefined {
169
178
  if (options === undefined) {
170
179
  const fastDefaultBinding = this.#registry.getFastDefault(token);
171
180
  if (fastDefaultBinding !== undefined) {
172
181
  return { binding: fastDefaultBinding, owner: this };
173
182
  }
174
- } else if (isNameOnlyOptions(options)) {
175
- const namedBinding = this.#registry.getSimpleNamed(token, options.name);
176
- if (namedBinding !== undefined && this.#matchesBindingFast(namedBinding, options, resolutionStack)) {
177
- return { binding: namedBinding, owner: this };
178
- }
179
- } else {
180
- const singleTag = singleTagOnlyOf(options);
181
- if (singleTag !== undefined) {
182
- const tagged = this.#registry.getSimpleTagged(token, singleTag);
183
- if (tagged !== undefined && this.#satisfiesPredicate(tagged, options, resolutionStack)) {
184
- return { binding: tagged, owner: this };
185
- }
186
- } else if (
187
- options.name === undefined &&
188
- // A threshold switches the data structure, never the semantics: under it the generic scan
189
- // below beats walking the indexes, and both paths answer identically. Sized first, so a
190
- // small list pays one length read and nothing else.
191
- this.#registry.getAll(token).length > MULTI_TAG_INDEX_THRESHOLD &&
192
- requestedTagKeyMask(options) !== NO_TAG_KEYS
193
- ) {
194
- // A name-less multi-tag request matches only name-less tagged slots, and every such slot
195
- // lives in one of the two tag indexes — so their union is the whole candidate set and the
196
- // token's full list never needs scanning. Selection still owns predicates and specificity.
197
- const selected = this.#selectMultiTagged(token, options, resolutionStack);
198
- if (selected !== undefined) {
199
- return { binding: selected, owner: this };
200
- }
201
- return this.#parent === undefined ? undefined : this.#parent.#findBinding(token, options, resolutionStack);
183
+ } else if (singleCriterion !== undefined) {
184
+ const indexed = this.#registry.getSimpleTagged(token, singleCriterion);
185
+ if (indexed !== undefined && this.#satisfiesPredicate(indexed, options, resolutionStack)) {
186
+ return { binding: indexed, owner: this };
187
+ }
188
+ } else if (
189
+ // A threshold switches the data structure, never the semantics: under it the generic scan
190
+ // below beats walking the indexes, and both paths answer identically. Sized first, so a
191
+ // small list pays one length read and nothing else.
192
+ this.#registry.getAll(token).length > MULTI_TAG_INDEX_THRESHOLD &&
193
+ requestedTagKeyMask(options) !== NO_TAG_KEYS
194
+ ) {
195
+ // A multi-criterion request matches only slots whose every criterion it carries, and every
196
+ // such slot is in the two tag indexes — their union is the whole candidate set, unscanned.
197
+ const selected = this.#selectMultiTagged(token, options, resolutionStack);
198
+ if (selected !== undefined) {
199
+ return { binding: selected, owner: this };
202
200
  }
201
+ return this.#parent === undefined
202
+ ? undefined
203
+ : this.#parent.#findBinding(token, options, resolutionStack, singleCriterion);
203
204
  }
204
205
 
205
206
  const bindings = this.#registry.getAll(token);
@@ -208,7 +209,7 @@ export class DependencyResolver implements ResolverCallbacks {
208
209
  // specificity to weigh and no ambiguity to report.
209
210
  const selected =
210
211
  bindings.length === 1
211
- ? this.#matchesBindingFast(bindings[0]!, options, resolutionStack)
212
+ ? matchesSlot(bindings[0]!.slot, options) && this.#satisfiesPredicate(bindings[0]!, options, resolutionStack)
212
213
  ? bindings[0]
213
214
  : undefined
214
215
  : selectBinding(bindings, options, this.#makeConstraintContext(resolutionStack, options), tokenName(token));
@@ -217,7 +218,7 @@ export class DependencyResolver implements ResolverCallbacks {
217
218
  }
218
219
  }
219
220
  if (this.#parent !== undefined) {
220
- return this.#parent.#findBinding(token, options, resolutionStack);
221
+ return this.#parent.#findBinding(token, options, resolutionStack, singleCriterion);
221
222
  }
222
223
  return undefined;
223
224
  }
@@ -232,10 +233,14 @@ export class DependencyResolver implements ResolverCallbacks {
232
233
  token: Token<unknown> | Constructor,
233
234
  options: ResolveOptions | undefined,
234
235
  resolutionStack: Array<ResolutionFrame>,
236
+ precomputedCriterion?: BindingTag | null,
235
237
  ): DefaultLookupEntry<DependencyResolver> {
238
+ // `null` is a caller's "folded: none" — only an absent precomputation re-folds.
239
+ const singleCriterion =
240
+ precomputedCriterion === undefined ? singleCriterionOnlyOf(options) : (precomputedCriterion ?? undefined);
236
241
  let currentToken = token;
237
242
  let visitedAliasTokens: Set<Token<unknown> | Constructor> | undefined;
238
- let found = this.#findBinding(currentToken, options, resolutionStack);
243
+ let found = this.#findBinding(currentToken, options, resolutionStack, singleCriterion);
239
244
 
240
245
  while (found !== undefined && found.binding.kind === "alias") {
241
246
  const target = found.binding.target;
@@ -245,7 +250,7 @@ export class DependencyResolver implements ResolverCallbacks {
245
250
  }
246
251
  visitedAliasTokens.add(target);
247
252
  currentToken = target;
248
- found = this.#findBinding(currentToken, options, resolutionStack);
253
+ found = this.#findBinding(currentToken, options, resolutionStack, singleCriterion);
249
254
  }
250
255
 
251
256
  if (found === undefined) {
@@ -270,7 +275,7 @@ export class DependencyResolver implements ResolverCallbacks {
270
275
  token: Token<unknown> | Constructor,
271
276
  options: ResolveOptions | undefined,
272
277
  ): DefaultLookupEntry<DependencyResolver> | undefined {
273
- return this.#findBinding(token, options, []);
278
+ return this.#findBinding(token, options, [], singleCriterionOnlyOf(options));
274
279
  }
275
280
 
276
281
  /**
@@ -427,22 +432,14 @@ export class DependencyResolver implements ResolverCallbacks {
427
432
  const entry = this.#lookup.defaultEntry(token);
428
433
  return entry === null ? null : { binding: entry.binding };
429
434
  },
430
- // Exactly what #findBinding's named lane accepts, minus the half that reads a path: a predicate
431
- // is the compiler's cue to leave the selection to the runtime.
432
- lookupPathIndependentNamedEntry: (token, options) => {
433
- const entry = this.#lookup.namedEntry(token, options.name);
434
- if (entry === null || entry.binding.predicate !== undefined || !matchesSlot(entry.binding.slot, options)) {
435
+ // Exactly what #findBinding's single-criterion lane accepts, minus the half that reads a path:
436
+ // a predicate is the compiler's cue to leave the selection to the runtime.
437
+ lookupPathIndependentEntry: (token, options) => {
438
+ const singleCriterion = singleCriterionOnlyOf(options);
439
+ if (singleCriterion === undefined) {
435
440
  return null;
436
441
  }
437
- return { binding: entry.binding };
438
- },
439
- // The named rule verbatim, on the single-tag lane's memo.
440
- lookupPathIndependentTaggedEntry: (token, options) => {
441
- const singleTag = singleTagOnlyOf(options);
442
- if (singleTag === undefined) {
443
- return null;
444
- }
445
- const entry = this.#lookup.taggedEntry(token, singleTag);
442
+ const entry = this.#lookup.taggedEntry(token, singleCriterion);
446
443
  if (entry === null || entry.binding.predicate !== undefined || !matchesSlot(entry.binding.slot, options)) {
447
444
  return null;
448
445
  }
@@ -506,40 +503,30 @@ export class DependencyResolver implements ResolverCallbacks {
506
503
  token: Token<Value> | Constructor<Value>,
507
504
  options: ResolveOptions | undefined,
508
505
  resolutionStack: Array<ResolutionFrame>,
506
+ precomputedCriterion?: BindingTag | null,
509
507
  ): Value {
510
- // Name-only fast lane: memoized lookup, dispatching just the shapes whose
511
- // semantics involve no resolution context (constants, cached singletons).
512
- if (options !== undefined && isNameOnlyOptions(options)) {
513
- const namedEntry = this.#lookup.namedEntry(token, options.name);
514
- if (namedEntry !== null) {
515
- const namedBinding = namedEntry.binding;
516
- if (namedEntry.owner.#isPlainConstant(namedBinding)) {
517
- return namedBinding.value as Value;
518
- }
519
- if (namedBinding.scope === "singleton" && namedBinding.instance !== NO_INSTANCE) {
520
- return namedBinding.instance as Value;
521
- }
522
- // Everything else keeps the full path (context, activation, guards).
523
- }
524
- } else if (options !== undefined) {
525
- // Single-tag fast lane: the named lane's tagged twin, memoizing the chain walk.
526
- const singleTag = singleTagOnlyOf(options);
527
- if (singleTag !== undefined) {
528
- const taggedEntry = this.#lookup.taggedEntry(token, singleTag);
529
- if (taggedEntry !== null) {
530
- const taggedBinding = taggedEntry.binding;
531
- if (taggedEntry.owner.#isPlainConstant(taggedBinding)) {
532
- return taggedBinding.value as Value;
508
+ // Single-criterion fast lane (a lone name folds here too): memoized lookup, dispatching just
509
+ // the shapes whose semantics involve no resolution context (constants, cached singletons).
510
+ let singleCriterion: BindingTag | undefined;
511
+ if (options !== undefined) {
512
+ singleCriterion =
513
+ precomputedCriterion === undefined ? singleCriterionOnlyOf(options) : (precomputedCriterion ?? undefined);
514
+ if (singleCriterion !== undefined) {
515
+ const indexedEntry = this.#lookup.taggedEntry(token, singleCriterion);
516
+ if (indexedEntry !== null) {
517
+ const indexedBinding = indexedEntry.binding;
518
+ if (indexedEntry.owner.#isPlainConstant(indexedBinding)) {
519
+ return indexedBinding.value as Value;
533
520
  }
534
- if (taggedBinding.scope === "singleton" && taggedBinding.instance !== NO_INSTANCE) {
535
- return taggedBinding.instance as Value;
521
+ if (indexedBinding.scope === "singleton" && indexedBinding.instance !== NO_INSTANCE) {
522
+ return indexedBinding.instance as Value;
536
523
  }
537
524
  // Everything else keeps the full path (context, activation, guards).
538
525
  }
539
526
  }
540
527
  }
541
528
 
542
- const { binding, owner } = this.#requireBinding(token, options, resolutionStack);
529
+ const { binding, owner } = this.#requireBinding(token, options, resolutionStack, singleCriterion ?? null);
543
530
 
544
531
  // A singleton owned by a parent resolver is resolved there, so the parent caches it.
545
532
  if (binding.scope === "singleton" && owner !== this) {
@@ -734,20 +721,26 @@ export class DependencyResolver implements ResolverCallbacks {
734
721
  return this.resolveAll(dep.token, options, resolutionStack);
735
722
  }
736
723
  if (dep.optional) {
737
- return this.resolveOptional(dep.token, options, resolutionStack);
724
+ return this.resolveOptional(dep.token, options, resolutionStack, singleCriterionForSlot(dep));
738
725
  }
739
726
  if (options === undefined) {
740
727
  return this.resolveFromContext(dep.token, resolutionStack);
741
728
  }
742
- return this.resolve(dep.token, options, resolutionStack);
729
+ return this.resolve(dep.token, options, resolutionStack, singleCriterionForSlot(dep));
743
730
  }
744
731
 
745
732
  resolveOptional<Value>(
746
733
  token: Token<Value> | Constructor<Value>,
747
734
  options: ResolveOptions | undefined,
748
735
  resolutionStack: Array<ResolutionFrame>,
736
+ precomputedCriterion?: BindingTag | null,
749
737
  ): Value | undefined {
750
- const entry = this.#findBinding(token, options, resolutionStack);
738
+ const entry = this.#findBinding(
739
+ token,
740
+ options,
741
+ resolutionStack,
742
+ precomputedCriterion === undefined ? singleCriterionOnlyOf(options) : (precomputedCriterion ?? undefined),
743
+ );
751
744
  if (entry === undefined) {
752
745
  return undefined;
753
746
  }
@@ -860,13 +853,24 @@ export class DependencyResolver implements ResolverCallbacks {
860
853
  return this.#resolveBindingAsync(binding, undefined, resolutionStack, branchDepth, owner);
861
854
  }
862
855
 
856
+ /**
857
+ * Instantiates one owned binding directly, bypassing selection.
858
+ *
859
+ * @remarks Warm-up must build the binding it inspected: re-selecting by the slot's own criteria
860
+ * could pick a different candidate whose criteria are a subset of them.
861
+ */
862
+ warmBindingAsync(binding: Binding, options: ResolveOptions | undefined): Promise<unknown> {
863
+ return this.#resolveBindingAsync(binding, options, [], ROOT_BRANCH, this);
864
+ }
865
+
863
866
  async resolveAsync<Value>(
864
867
  token: Token<Value> | Constructor<Value>,
865
868
  options: ResolveOptions | undefined,
866
869
  resolutionStack: Array<ResolutionFrame>,
867
870
  branchDepth: BranchDepth = UNOWNED_BRANCH,
871
+ precomputedCriterion?: BindingTag | null,
868
872
  ): Promise<Value> {
869
- const { binding, owner } = this.#requireBinding(token, options, resolutionStack);
873
+ const { binding, owner } = this.#requireBinding(token, options, resolutionStack, precomputedCriterion);
870
874
 
871
875
  if (binding.scope === "singleton" && owner !== this) {
872
876
  return owner.#resolveBindingAsync(binding, options, resolutionStack, branchDepth, owner) as Promise<Value>;
@@ -1105,12 +1109,12 @@ export class DependencyResolver implements ResolverCallbacks {
1105
1109
  return this.resolveAllAsync(dep.token, options, resolutionStack, branchDepth);
1106
1110
  }
1107
1111
  if (dep.optional) {
1108
- return this.resolveOptionalAsync(dep.token, options, resolutionStack, branchDepth);
1112
+ return this.resolveOptionalAsync(dep.token, options, resolutionStack, branchDepth, singleCriterionForSlot(dep));
1109
1113
  }
1110
1114
  if (options === undefined) {
1111
1115
  return this.resolveAsyncFromContext(dep.token, resolutionStack, branchDepth);
1112
1116
  }
1113
- return this.resolveAsync(dep.token, options, resolutionStack, branchDepth);
1117
+ return this.resolveAsync(dep.token, options, resolutionStack, branchDepth, singleCriterionForSlot(dep));
1114
1118
  }
1115
1119
 
1116
1120
  async resolveOptionalAsync<Value>(
@@ -1118,8 +1122,14 @@ export class DependencyResolver implements ResolverCallbacks {
1118
1122
  options: ResolveOptions | undefined,
1119
1123
  resolutionStack: Array<ResolutionFrame>,
1120
1124
  branchDepth: BranchDepth = UNOWNED_BRANCH,
1125
+ precomputedCriterion?: BindingTag | null,
1121
1126
  ): Promise<Value | undefined> {
1122
- const entry = this.#findBinding(token, options, resolutionStack);
1127
+ const entry = this.#findBinding(
1128
+ token,
1129
+ options,
1130
+ resolutionStack,
1131
+ precomputedCriterion === undefined ? singleCriterionOnlyOf(options) : (precomputedCriterion ?? undefined),
1132
+ );
1123
1133
  if (entry === undefined) {
1124
1134
  return undefined;
1125
1135
  }
@@ -1170,29 +1180,11 @@ export class DependencyResolver implements ResolverCallbacks {
1170
1180
  return result;
1171
1181
  }
1172
1182
 
1173
- /** Every binding the chain's name indexes hold for one name, nearest container first. */
1174
- #namedBindingsFromChain(token: Token<unknown> | Constructor, name: string): Array<Binding> {
1175
- // A name resolves to at most one binding per registry, so a root container's answer is built
1176
- // whole rather than grown — the list is sized at its allocation.
1177
- const ownBinding = this.#registry.getSimpleNamed(token, name);
1178
- if (this.#parent === undefined) {
1179
- return ownBinding === undefined ? [] : [ownBinding];
1180
- }
1181
- const result: Array<Binding> = ownBinding === undefined ? [] : [ownBinding];
1182
- for (let current: DependencyResolver | undefined = this.#parent; current !== undefined; current = current.#parent) {
1183
- const binding = current.#registry.getSimpleNamed(token, name);
1184
- if (binding !== undefined) {
1185
- result.push(binding);
1186
- }
1187
- }
1188
- return result;
1189
- }
1190
-
1191
1183
  /**
1192
- * The candidates an index can name outright, or `null` when the request needs full selection.
1184
+ * The candidates the index can name outright, or `null` when the request needs full selection.
1193
1185
  *
1194
- * @remarks Kept off `#candidateBindings` so that method stays the size it was: a request neither
1195
- * index serves must not pay for the two that do.
1186
+ * @remarks Kept off `#candidateBindings` so that method stays the size it was: a request the
1187
+ * index cannot serve must not pay for the shape it does.
1196
1188
  * An index has matched the slot already, but a hit may still carry a predicate, and evaluating
1197
1189
  * that is the selection path's job.
1198
1190
  */
@@ -1201,31 +1193,26 @@ export class DependencyResolver implements ResolverCallbacks {
1201
1193
  options: ResolveOptions,
1202
1194
  resolutionStack: Array<ResolutionFrame>,
1203
1195
  ): ReadonlyArray<Binding> | null {
1204
- if (isNameOnlyOptions(options)) {
1205
- const named = this.#namedBindingsFromChain(token, options.name);
1206
- return anyPredicate(named)
1207
- ? selectAllBindings(named, options, this.#makeConstraintContext(resolutionStack, options))
1208
- : named;
1209
- }
1210
- const singleTag = singleTagOnlyOf(options);
1211
- if (singleTag === undefined) {
1196
+ const singleCriterion = singleCriterionOnlyOf(options);
1197
+ if (singleCriterion === undefined) {
1212
1198
  return null;
1213
1199
  }
1214
- const tagged = this.#taggedBindingsFromChain(token, singleTag);
1215
- return anyPredicate(tagged)
1216
- ? selectAllBindings(tagged, options, this.#makeConstraintContext(resolutionStack, options))
1217
- : tagged;
1200
+ const indexed = this.#taggedBindingsFromChain(token, singleCriterion);
1201
+ return anyPredicate(indexed)
1202
+ ? selectAllBindings(indexed, options, this.#makeConstraintContext(resolutionStack, options))
1203
+ : indexed;
1218
1204
  }
1219
1205
 
1220
1206
  /**
1221
- * Every binding the chain's tag indexes hold for one tag, nearest container first.
1207
+ * Every binding the chain's criterion indexes hold for one criterion, nearest container first.
1222
1208
  *
1223
- * @remarks A request for one tag and no name matches exactly the bindings the index keys, so this
1224
- * is the whole candidate set rather than a prefilter — a named or multi-tag slot cannot satisfy it.
1209
+ * @remarks A request for exactly one criterion matches exactly the bindings the index keys, so
1210
+ * this is the whole candidate set rather than a prefilter — a multi-criterion slot cannot satisfy
1211
+ * it.
1225
1212
  */
1226
1213
  #taggedBindingsFromChain(token: Token<unknown> | Constructor, tag: BindingTag): Array<Binding> {
1227
- // A tag matches at most one binding per registry, so a root container's answer is built whole
1228
- // rather than grown — the shape `#namedBindingsFromChain` takes, for the same reason.
1214
+ // A criterion matches at most one binding per registry, so a root container's answer is built
1215
+ // whole rather than grown — the list is sized at its allocation.
1229
1216
  const ownBinding = this.#registry.getSimpleTagged(token, tag);
1230
1217
  if (this.#parent === undefined) {
1231
1218
  return ownBinding === undefined ? [] : [ownBinding];
@@ -1282,18 +1269,23 @@ export class DependencyResolver implements ResolverCallbacks {
1282
1269
  return buildConstraintContext(resolutionStack, options);
1283
1270
  }
1284
1271
 
1285
- /** Selection for a name-less multi-tag request, over the union of the two tag indexes. */
1272
+ /** Selection for a multi-criterion request, over the union of the two tag indexes. */
1286
1273
  #selectMultiTagged(
1287
1274
  token: Token<unknown> | Constructor,
1288
1275
  options: ResolveOptions,
1289
1276
  resolutionStack: Array<ResolutionFrame>,
1290
1277
  ): Binding | undefined {
1291
1278
  const candidates: Array<Binding> = [];
1292
- this.#gatherTagCandidates(token, options.tag, candidates);
1279
+ const gathered: Array<BindingTag> = [];
1280
+ if (options.name !== undefined) {
1281
+ // Read, not minted: an unminted name has no criterion, so no slot can carry it.
1282
+ this.#gatherTagCandidates(token, slotNameCriterionOf(options.name), candidates, gathered);
1283
+ }
1284
+ this.#gatherTagCandidates(token, options.tag, candidates, gathered);
1293
1285
  const listed = options.tags;
1294
1286
  if (listed !== undefined) {
1295
1287
  for (let index = 0; index < listed.length; index += 1) {
1296
- this.#gatherTagCandidates(token, listed[index], candidates);
1288
+ this.#gatherTagCandidates(token, listed[index], candidates, gathered);
1297
1289
  }
1298
1290
  }
1299
1291
  if (candidates.length === 0) {
@@ -1307,37 +1299,26 @@ export class DependencyResolver implements ResolverCallbacks {
1307
1299
  token: Token<unknown> | Constructor,
1308
1300
  criterion: BindingTag | undefined,
1309
1301
  out: Array<Binding>,
1302
+ gathered: Array<BindingTag>,
1310
1303
  ): void {
1311
- if (criterion === undefined) {
1304
+ // Distinct criteria never share a binding — a slot lives in exactly one bucket — so deduping
1305
+ // by criterion covers a request repeating one across its spellings, without scanning `out`.
1306
+ if (criterion === undefined || gathered.includes(criterion)) {
1312
1307
  return;
1313
1308
  }
1314
- // The includes probes only guard a request repeating a criterion across its two spellings.
1309
+ gathered.push(criterion);
1315
1310
  const single = this.#registry.getSimpleTagged(token, criterion);
1316
- if (single !== undefined && !out.includes(single)) {
1311
+ if (single !== undefined) {
1317
1312
  out.push(single);
1318
1313
  }
1319
1314
  const bucket = this.#registry.getMultiTagged(token, criterion);
1320
1315
  if (bucket !== undefined) {
1321
1316
  for (let index = 0; index < bucket.length; index += 1) {
1322
- const candidate = bucket[index]!;
1323
- if (!out.includes(candidate)) {
1324
- out.push(candidate);
1325
- }
1317
+ out.push(bucket[index]!);
1326
1318
  }
1327
1319
  }
1328
1320
  }
1329
1321
 
1330
- #matchesBindingFast(
1331
- binding: Binding,
1332
- options: ResolveOptions | undefined,
1333
- resolutionStack: Array<ResolutionFrame>,
1334
- ): boolean {
1335
- if (!matchesSlot(binding.slot, options)) {
1336
- return false;
1337
- }
1338
- return this.#satisfiesPredicate(binding, options, resolutionStack);
1339
- }
1340
-
1341
1322
  /** The predicate half of a match, for a lane whose index has already settled the slot. */
1342
1323
  #satisfiesPredicate(
1343
1324
  binding: Binding,
@@ -1,6 +1,6 @@
1
1
  import type { Binding, BindingSlot } from "#/core/binding";
2
2
  import type { BindingTag, TagKeyMask } from "#/core/tag";
3
- import { coversTagKeys, NO_TAG_KEYS } from "#/core/tag";
3
+ import { coversTagKeys, NO_TAG_KEYS, slotName, slotNameCriterionOf } from "#/core/tag";
4
4
  import type { ConstraintContext, ResolveOptions } from "#/core/types";
5
5
  import { AmbiguousBindingError } from "#/errors/errors";
6
6
 
@@ -41,7 +41,7 @@ export function selectBinding(
41
41
  }
42
42
  // Reached only where the throw was: a slot declaring more of what the request carries is the more
43
43
  // specific match, so an over-specified request resolves instead of being ambiguous.
44
- const mostSpecific = mostSpecificByTagCount(candidates);
44
+ const mostSpecific = mostSpecificByCriterionCount(candidates);
45
45
  if (mostSpecific !== undefined) {
46
46
  return mostSpecific;
47
47
  }
@@ -51,8 +51,8 @@ export function selectBinding(
51
51
  );
52
52
  }
53
53
 
54
- /** The lone candidate declaring more tags than every other, or `undefined` when that is a tie. */
55
- function mostSpecificByTagCount(candidates: ReadonlyArray<Binding>): Binding | undefined {
54
+ /** The lone candidate declaring more criteria than every other, or `undefined` when that is a tie. */
55
+ function mostSpecificByCriterionCount(candidates: ReadonlyArray<Binding>): Binding | undefined {
56
56
  let best: Binding | undefined;
57
57
  let bestCount = -1;
58
58
  let tied = false;
@@ -118,35 +118,24 @@ function filterBindings(
118
118
  }
119
119
 
120
120
  function hasSlotCriterion(options: ResolveOptions): boolean {
121
- return options.name !== undefined || requestedTagKeyMask(options) !== NO_TAG_KEYS;
121
+ return requestedTagKeyMask(options) !== NO_TAG_KEYS;
122
122
  }
123
123
 
124
124
  /**
125
- * Whether a binding's slot satisfies a request: names must be equal, and every tag the slot
126
- * declares must be among the tags requested.
125
+ * Whether a binding's slot satisfies a request: every criterion the slot declares must be among
126
+ * the request's criteria, a name spelling either side folding to the reserved criterion.
127
127
  *
128
- * @remarks The tag half is a key-mask subset test before any criterion is read, so a slot the
129
- * request cannot satisfy is rejected in one word compare. Criteria are interned, so what follows is
130
- * identity.
128
+ * @remarks A key-mask subset test runs before any criterion is read, so a slot the request cannot
129
+ * satisfy is rejected in one word compare. Criteria are interned, so what follows is identity.
131
130
  *
132
131
  * @since 0.5.0-canary.9
133
132
  */
134
133
  export function matchesSlot(slot: BindingSlot, options: ResolveOptions | undefined): boolean {
135
- const requestedName = options?.name;
136
-
137
- if (slot.name !== undefined) {
138
- if (slot.name !== requestedName) {
139
- return false;
140
- }
141
- } else if (requestedName !== undefined) {
142
- return false;
143
- }
144
-
145
134
  const slotMask = slot.keyMask;
146
135
  const requestMask = requestedTagKeyMask(options);
147
136
 
148
137
  if (slotMask === NO_TAG_KEYS) {
149
- // A request carrying tags needs a tagged slot: an untagged binding never matches.
138
+ // A request carrying any criterion never falls back to the default slot.
150
139
  return requestMask === NO_TAG_KEYS;
151
140
  }
152
141
  if (!coversTagKeys(requestMask, slotMask)) {
@@ -178,6 +167,9 @@ export function requestedTagKeyMask(options: ResolveOptions | undefined): TagKey
178
167
  const listed = options.tags;
179
168
  let mask = single === undefined ? NO_TAG_KEYS : single.mask;
180
169
 
170
+ if (options.name !== undefined) {
171
+ mask = (mask | slotName.mask) as TagKeyMask;
172
+ }
181
173
  if (listed !== undefined) {
182
174
  for (let index = 0; index < listed.length; index += 1) {
183
175
  mask = (mask | listed[index]!.mask) as TagKeyMask;
@@ -192,6 +184,11 @@ function requestCarries(options: ResolveOptions | undefined, criterion: BindingT
192
184
  if (options === undefined) {
193
185
  return false;
194
186
  }
187
+ // The `name` spelling folds through the intern read, so this too is identity — a hand-built
188
+ // criterion matches nothing on any lane, and an unminted name retains nothing.
189
+ if (criterion.key === slotName && options.name !== undefined && criterion === slotNameCriterionOf(options.name)) {
190
+ return true;
191
+ }
195
192
  if (options.tag === criterion) {
196
193
  return true;
197
194
  }
@@ -7,6 +7,8 @@ import type { BindingConstraint, Constructor } from "#/core/types";
7
7
  import { EmptyTagCriteriaError } from "#/errors/errors";
8
8
 
9
9
  /**
10
+ * Matches when the direct parent frame resolves the given token.
11
+ *
10
12
  * @since 0.3.16-canary.0
11
13
  */
12
14
  export function whenParentIs(token: Token<unknown> | Constructor): BindingConstraint {
@@ -16,6 +18,8 @@ export function whenParentIs(token: Token<unknown> | Constructor): BindingConstr
16
18
  }
17
19
 
18
20
  /**
21
+ * Matches when there is no direct parent frame, or it resolves a different token.
22
+ *
19
23
  * @since 0.3.16-canary.0
20
24
  */
21
25
  export function whenNoParentIs(token: Token<unknown> | Constructor): BindingConstraint {
@@ -25,6 +29,8 @@ export function whenNoParentIs(token: Token<unknown> | Constructor): BindingCons
25
29
  }
26
30
 
27
31
  /**
32
+ * Matches when at least one ancestor frame resolves the given token.
33
+ *
28
34
  * @since 0.3.16-canary.0
29
35
  */
30
36
  export function whenAnyAncestorIs(token: Token<unknown> | Constructor): BindingConstraint {
@@ -34,6 +40,8 @@ export function whenAnyAncestorIs(token: Token<unknown> | Constructor): BindingC
34
40
  }
35
41
 
36
42
  /**
43
+ * Matches when no ancestor frame resolves the given token.
44
+ *
37
45
  * @since 0.3.16-canary.0
38
46
  */
39
47
  export function whenNoAncestorIs(token: Token<unknown> | Constructor): BindingConstraint {
@@ -43,6 +51,8 @@ export function whenNoAncestorIs(token: Token<unknown> | Constructor): BindingCo
43
51
  }
44
52
 
45
53
  /**
54
+ * Matches when the direct parent slot carries the given name.
55
+ *
46
56
  * @since 0.3.16-canary.0
47
57
  */
48
58
  export function whenParentNamed(name: string): BindingConstraint {
@@ -54,6 +64,8 @@ export function whenParentNamed(name: string): BindingConstraint {
54
64
  }
55
65
 
56
66
  /**
67
+ * Matches when at least one ancestor slot carries the given name.
68
+ *
57
69
  * @since 0.3.16-canary.0
58
70
  */
59
71
  export function whenAnyAncestorNamed(name: string): BindingConstraint {
@@ -65,6 +77,8 @@ export function whenAnyAncestorNamed(name: string): BindingConstraint {
65
77
  }
66
78
 
67
79
  /**
80
+ * Matches when the direct parent slot carries the given tag pair.
81
+ *
68
82
  * @since 0.3.16-canary.0
69
83
  */
70
84
  export function whenParentTagged(criterion: BindingTag): BindingConstraint {
@@ -73,6 +87,8 @@ export function whenParentTagged(criterion: BindingTag): BindingConstraint {
73
87
  }
74
88
 
75
89
  /**
90
+ * Matches when at least one ancestor slot carries the given tag pair.
91
+ *
76
92
  * @since 0.3.16-canary.0
77
93
  */
78
94
  export function whenAnyAncestorTagged(criterion: BindingTag): BindingConstraint {