@codefast/di 0.7.1 → 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 (63) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/dist/container/binding-builders.d.ts.map +1 -1
  3. package/dist/container/binding-builders.js +4 -5
  4. package/dist/container/binding-builders.js.map +1 -1
  5. package/dist/container/container.d.ts.map +1 -1
  6. package/dist/container/container.js +3 -4
  7. package/dist/container/container.js.map +1 -1
  8. package/dist/core/binding.d.ts +15 -2
  9. package/dist/core/binding.d.ts.map +1 -1
  10. package/dist/core/binding.js +29 -4
  11. package/dist/core/binding.js.map +1 -1
  12. package/dist/core/registry.d.ts +0 -3
  13. package/dist/core/registry.d.ts.map +1 -1
  14. package/dist/core/registry.js +14 -61
  15. package/dist/core/registry.js.map +1 -1
  16. package/dist/core/tag.d.ts +26 -0
  17. package/dist/core/tag.d.ts.map +1 -1
  18. package/dist/core/tag.js +54 -11
  19. package/dist/core/tag.js.map +1 -1
  20. package/dist/errors/errors.d.ts.map +1 -1
  21. package/dist/errors/errors.js +28 -3
  22. package/dist/errors/errors.js.map +1 -1
  23. package/dist/index.d.ts +1 -1
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +1 -1
  26. package/dist/index.js.map +1 -1
  27. package/dist/injection/resolve-options.d.ts +17 -12
  28. package/dist/injection/resolve-options.d.ts.map +1 -1
  29. package/dist/injection/resolve-options.js +69 -15
  30. package/dist/injection/resolve-options.js.map +1 -1
  31. package/dist/introspection/dependency-graph.d.ts.map +1 -1
  32. package/dist/introspection/dependency-graph.js +22 -9
  33. package/dist/introspection/dependency-graph.js.map +1 -1
  34. package/dist/resolution/cache/binding-lookup-cache.d.ts +1 -3
  35. package/dist/resolution/cache/binding-lookup-cache.d.ts.map +1 -1
  36. package/dist/resolution/cache/binding-lookup-cache.js +1 -38
  37. package/dist/resolution/cache/binding-lookup-cache.js.map +1 -1
  38. package/dist/resolution/plan/instantiation-plan.d.ts +4 -12
  39. package/dist/resolution/plan/instantiation-plan.d.ts.map +1 -1
  40. package/dist/resolution/plan/instantiation-plan.js +9 -27
  41. package/dist/resolution/plan/instantiation-plan.js.map +1 -1
  42. package/dist/resolution/resolver.d.ts +12 -5
  43. package/dist/resolution/resolver.d.ts.map +1 -1
  44. package/dist/resolution/resolver.js +106 -141
  45. package/dist/resolution/resolver.js.map +1 -1
  46. package/dist/resolution/select/binding-select.d.ts +4 -5
  47. package/dist/resolution/select/binding-select.d.ts.map +1 -1
  48. package/dist/resolution/select/binding-select.js +18 -20
  49. package/dist/resolution/select/binding-select.js.map +1 -1
  50. package/package.json +2 -2
  51. package/src/container/binding-builders.ts +10 -5
  52. package/src/container/container.ts +3 -4
  53. package/src/core/binding.ts +33 -5
  54. package/src/core/registry.ts +14 -66
  55. package/src/core/tag.ts +71 -17
  56. package/src/errors/errors.ts +28 -5
  57. package/src/index.ts +1 -1
  58. package/src/injection/resolve-options.ts +75 -18
  59. package/src/introspection/dependency-graph.ts +23 -9
  60. package/src/resolution/cache/binding-lookup-cache.ts +1 -41
  61. package/src/resolution/plan/instantiation-plan.ts +13 -38
  62. package/src/resolution/resolver.ts +120 -141
  63. package/src/resolution/select/binding-select.ts +18 -21
@@ -12,7 +12,7 @@ import { tokenName } from "#/core/token";
12
12
  import type { Constructor, ResolutionFrame, ResolveOptions } from "#/core/types";
13
13
  import { AsyncResolutionError } from "#/errors/errors";
14
14
  import type { DependencySlot } from "#/injection/resolve-options";
15
- import { injectionSlotToResolveOptions, isNameOnlyOptions } from "#/injection/resolve-options";
15
+ import { injectionSlotToResolveOptions } from "#/injection/resolve-options";
16
16
  import type { ConstructorMetadata } from "#/metadata/metadata-types";
17
17
 
18
18
  // Past this depth a dependency escapes to the runtime path rather than inlining further —
@@ -115,22 +115,13 @@ export interface InstantiationPlanHost {
115
115
  /** Options-less lookup with alias hops folded; `null` when the fast lane can't answer. */
116
116
  lookupDependencyEntry(token: Token<unknown> | Constructor): InstantiationPlanDependencyEntry | null;
117
117
  /**
118
- * A name-only lookup a plan may bake in, or `null` when the answer is not the compiler's to make.
118
+ * A single-criterion lookup a plan may bake in, or `null` when the answer is not the compiler's
119
+ * to make.
119
120
  *
120
- * @remarks Selection for a named request is an index hit *and* a predicate, and a predicate reads
121
+ * @remarks Selection for such a request is an index hit *and* a predicate, and a predicate reads
121
122
  * the resolution path — so only a candidate carrying none of one can be decided ahead of time.
122
123
  */
123
- lookupPathIndependentNamedEntry(
124
- token: Token<unknown> | Constructor,
125
- options: ResolveOptions & { name: string },
126
- ): InstantiationPlanDependencyEntry | null;
127
- /**
128
- * The named lookup's single-tag twin, or `null` under the same rule.
129
- *
130
- * @remarks Optional so a host predating it stays a valid host — a compiler given none simply
131
- * escapes the dependency, which is exactly the pre-settlement behavior.
132
- */
133
- lookupPathIndependentTaggedEntry?(
124
+ lookupPathIndependentEntry(
134
125
  token: Token<unknown> | Constructor,
135
126
  options: ResolveOptions,
136
127
  ): InstantiationPlanDependencyEntry | null;
@@ -265,19 +256,11 @@ export class InstantiationPlanCompiler {
265
256
  return this.#compileEscapeThunk(token, ancestors, "optional", options);
266
257
  }
267
258
  if (options !== undefined) {
268
- // A name the registry can settle without reading a path is a dependency like any other: it
269
- // escapes only because it carries a criterion, not because anything about it is opaque.
270
- if (isNameOnlyOptions(options)) {
271
- const named = this.#host.lookupPathIndependentNamedEntry(token, options);
272
- if (named !== null) {
273
- return this.#compileDepThunk(named, compileStack, depth, ancestors, options);
274
- }
275
- } else {
276
- // The host owns the whole single-tag decision, including whether the options qualify.
277
- const tagged = this.#host.lookupPathIndependentTaggedEntry?.(token, options);
278
- if (tagged !== null && tagged !== undefined) {
279
- return this.#compileDepThunk(tagged, compileStack, depth, ancestors, options);
280
- }
259
+ // A criterion the registry can settle without reading a path is a dependency like any other:
260
+ // it escapes only because the host cannot decide it ahead of time.
261
+ const indexed = this.#host.lookupPathIndependentEntry(token, options);
262
+ if (indexed !== null) {
263
+ return this.#compileDepThunk(indexed, compileStack, depth, ancestors, options);
281
264
  }
282
265
  return this.#compileEscapeThunk(token, ancestors, "single", options);
283
266
  }
@@ -541,17 +524,9 @@ export class InstantiationPlanCompiler {
541
524
  return this.#compileAsyncEscapeThunk(token, ancestors, "optional", options);
542
525
  }
543
526
  if (options !== undefined) {
544
- if (isNameOnlyOptions(options)) {
545
- const named = this.#host.lookupPathIndependentNamedEntry(token, options);
546
- if (named !== null) {
547
- return this.#compileAsyncDepThunk(named, compileStack, depth, ancestors, options);
548
- }
549
- } else {
550
- // The host owns the whole single-tag decision, including whether the options qualify.
551
- const tagged = this.#host.lookupPathIndependentTaggedEntry?.(token, options);
552
- if (tagged !== null && tagged !== undefined) {
553
- return this.#compileAsyncDepThunk(tagged, compileStack, depth, ancestors, options);
554
- }
527
+ const indexed = this.#host.lookupPathIndependentEntry(token, options);
528
+ if (indexed !== null) {
529
+ return this.#compileAsyncDepThunk(indexed, compileStack, depth, ancestors, options);
555
530
  }
556
531
  return this.#compileAsyncEscapeThunk(token, ancestors, "single", options);
557
532
  }
@@ -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";
@@ -163,45 +163,44 @@ export class DependencyResolver implements ResolverCallbacks {
163
163
 
164
164
  // ── Binding lookup ─────────────────────────────────────────────────────────────────────────────────────────────────
165
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
+ */
166
172
  #findBinding(
167
173
  token: Token<unknown> | Constructor,
168
174
  options: ResolveOptions | undefined,
169
175
  resolutionStack: Array<ResolutionFrame>,
176
+ singleCriterion: BindingTag | undefined,
170
177
  ): DefaultLookupEntry<DependencyResolver> | undefined {
171
178
  if (options === undefined) {
172
179
  const fastDefaultBinding = this.#registry.getFastDefault(token);
173
180
  if (fastDefaultBinding !== undefined) {
174
181
  return { binding: fastDefaultBinding, owner: this };
175
182
  }
176
- } else if (isNameOnlyOptions(options)) {
177
- const namedBinding = this.#registry.getSimpleNamed(token, options.name);
178
- if (namedBinding !== undefined && this.#matchesBindingFast(namedBinding, options, resolutionStack)) {
179
- return { binding: namedBinding, owner: this };
180
- }
181
- } else {
182
- const singleTag = singleTagOnlyOf(options);
183
- if (singleTag !== undefined) {
184
- const tagged = this.#registry.getSimpleTagged(token, singleTag);
185
- if (tagged !== undefined && this.#satisfiesPredicate(tagged, options, resolutionStack)) {
186
- return { binding: tagged, owner: this };
187
- }
188
- } else if (
189
- options.name === undefined &&
190
- // A threshold switches the data structure, never the semantics: under it the generic scan
191
- // below beats walking the indexes, and both paths answer identically. Sized first, so a
192
- // small list pays one length read and nothing else.
193
- this.#registry.getAll(token).length > MULTI_TAG_INDEX_THRESHOLD &&
194
- requestedTagKeyMask(options) !== NO_TAG_KEYS
195
- ) {
196
- // A name-less multi-tag request matches only name-less tagged slots, and every such slot
197
- // lives in one of the two tag indexes — so their union is the whole candidate set and the
198
- // token's full list never needs scanning. Selection still owns predicates and specificity.
199
- const selected = this.#selectMultiTagged(token, options, resolutionStack);
200
- if (selected !== undefined) {
201
- return { binding: selected, owner: this };
202
- }
203
- 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 };
204
200
  }
201
+ return this.#parent === undefined
202
+ ? undefined
203
+ : this.#parent.#findBinding(token, options, resolutionStack, singleCriterion);
205
204
  }
206
205
 
207
206
  const bindings = this.#registry.getAll(token);
@@ -210,7 +209,7 @@ export class DependencyResolver implements ResolverCallbacks {
210
209
  // specificity to weigh and no ambiguity to report.
211
210
  const selected =
212
211
  bindings.length === 1
213
- ? this.#matchesBindingFast(bindings[0]!, options, resolutionStack)
212
+ ? matchesSlot(bindings[0]!.slot, options) && this.#satisfiesPredicate(bindings[0]!, options, resolutionStack)
214
213
  ? bindings[0]
215
214
  : undefined
216
215
  : selectBinding(bindings, options, this.#makeConstraintContext(resolutionStack, options), tokenName(token));
@@ -219,7 +218,7 @@ export class DependencyResolver implements ResolverCallbacks {
219
218
  }
220
219
  }
221
220
  if (this.#parent !== undefined) {
222
- return this.#parent.#findBinding(token, options, resolutionStack);
221
+ return this.#parent.#findBinding(token, options, resolutionStack, singleCriterion);
223
222
  }
224
223
  return undefined;
225
224
  }
@@ -234,10 +233,14 @@ export class DependencyResolver implements ResolverCallbacks {
234
233
  token: Token<unknown> | Constructor,
235
234
  options: ResolveOptions | undefined,
236
235
  resolutionStack: Array<ResolutionFrame>,
236
+ precomputedCriterion?: BindingTag | null,
237
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);
238
241
  let currentToken = token;
239
242
  let visitedAliasTokens: Set<Token<unknown> | Constructor> | undefined;
240
- let found = this.#findBinding(currentToken, options, resolutionStack);
243
+ let found = this.#findBinding(currentToken, options, resolutionStack, singleCriterion);
241
244
 
242
245
  while (found !== undefined && found.binding.kind === "alias") {
243
246
  const target = found.binding.target;
@@ -247,7 +250,7 @@ export class DependencyResolver implements ResolverCallbacks {
247
250
  }
248
251
  visitedAliasTokens.add(target);
249
252
  currentToken = target;
250
- found = this.#findBinding(currentToken, options, resolutionStack);
253
+ found = this.#findBinding(currentToken, options, resolutionStack, singleCriterion);
251
254
  }
252
255
 
253
256
  if (found === undefined) {
@@ -272,7 +275,7 @@ export class DependencyResolver implements ResolverCallbacks {
272
275
  token: Token<unknown> | Constructor,
273
276
  options: ResolveOptions | undefined,
274
277
  ): DefaultLookupEntry<DependencyResolver> | undefined {
275
- return this.#findBinding(token, options, []);
278
+ return this.#findBinding(token, options, [], singleCriterionOnlyOf(options));
276
279
  }
277
280
 
278
281
  /**
@@ -429,22 +432,14 @@ export class DependencyResolver implements ResolverCallbacks {
429
432
  const entry = this.#lookup.defaultEntry(token);
430
433
  return entry === null ? null : { binding: entry.binding };
431
434
  },
432
- // Exactly what #findBinding's named lane accepts, minus the half that reads a path: a predicate
433
- // is the compiler's cue to leave the selection to the runtime.
434
- lookupPathIndependentNamedEntry: (token, options) => {
435
- const entry = this.#lookup.namedEntry(token, options.name);
436
- 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) {
437
440
  return null;
438
441
  }
439
- return { binding: entry.binding };
440
- },
441
- // The named rule verbatim, on the single-tag lane's memo.
442
- lookupPathIndependentTaggedEntry: (token, options) => {
443
- const singleTag = singleTagOnlyOf(options);
444
- if (singleTag === undefined) {
445
- return null;
446
- }
447
- const entry = this.#lookup.taggedEntry(token, singleTag);
442
+ const entry = this.#lookup.taggedEntry(token, singleCriterion);
448
443
  if (entry === null || entry.binding.predicate !== undefined || !matchesSlot(entry.binding.slot, options)) {
449
444
  return null;
450
445
  }
@@ -508,40 +503,30 @@ export class DependencyResolver implements ResolverCallbacks {
508
503
  token: Token<Value> | Constructor<Value>,
509
504
  options: ResolveOptions | undefined,
510
505
  resolutionStack: Array<ResolutionFrame>,
506
+ precomputedCriterion?: BindingTag | null,
511
507
  ): Value {
512
- // Name-only fast lane: memoized lookup, dispatching just the shapes whose
513
- // semantics involve no resolution context (constants, cached singletons).
514
- if (options !== undefined && isNameOnlyOptions(options)) {
515
- const namedEntry = this.#lookup.namedEntry(token, options.name);
516
- if (namedEntry !== null) {
517
- const namedBinding = namedEntry.binding;
518
- if (namedEntry.owner.#isPlainConstant(namedBinding)) {
519
- return namedBinding.value as Value;
520
- }
521
- if (namedBinding.scope === "singleton" && namedBinding.instance !== NO_INSTANCE) {
522
- return namedBinding.instance as Value;
523
- }
524
- // Everything else keeps the full path (context, activation, guards).
525
- }
526
- } else if (options !== undefined) {
527
- // Single-tag fast lane: the named lane's tagged twin, memoizing the chain walk.
528
- const singleTag = singleTagOnlyOf(options);
529
- if (singleTag !== undefined) {
530
- const taggedEntry = this.#lookup.taggedEntry(token, singleTag);
531
- if (taggedEntry !== null) {
532
- const taggedBinding = taggedEntry.binding;
533
- if (taggedEntry.owner.#isPlainConstant(taggedBinding)) {
534
- 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;
535
520
  }
536
- if (taggedBinding.scope === "singleton" && taggedBinding.instance !== NO_INSTANCE) {
537
- return taggedBinding.instance as Value;
521
+ if (indexedBinding.scope === "singleton" && indexedBinding.instance !== NO_INSTANCE) {
522
+ return indexedBinding.instance as Value;
538
523
  }
539
524
  // Everything else keeps the full path (context, activation, guards).
540
525
  }
541
526
  }
542
527
  }
543
528
 
544
- const { binding, owner } = this.#requireBinding(token, options, resolutionStack);
529
+ const { binding, owner } = this.#requireBinding(token, options, resolutionStack, singleCriterion ?? null);
545
530
 
546
531
  // A singleton owned by a parent resolver is resolved there, so the parent caches it.
547
532
  if (binding.scope === "singleton" && owner !== this) {
@@ -736,20 +721,26 @@ export class DependencyResolver implements ResolverCallbacks {
736
721
  return this.resolveAll(dep.token, options, resolutionStack);
737
722
  }
738
723
  if (dep.optional) {
739
- return this.resolveOptional(dep.token, options, resolutionStack);
724
+ return this.resolveOptional(dep.token, options, resolutionStack, singleCriterionForSlot(dep));
740
725
  }
741
726
  if (options === undefined) {
742
727
  return this.resolveFromContext(dep.token, resolutionStack);
743
728
  }
744
- return this.resolve(dep.token, options, resolutionStack);
729
+ return this.resolve(dep.token, options, resolutionStack, singleCriterionForSlot(dep));
745
730
  }
746
731
 
747
732
  resolveOptional<Value>(
748
733
  token: Token<Value> | Constructor<Value>,
749
734
  options: ResolveOptions | undefined,
750
735
  resolutionStack: Array<ResolutionFrame>,
736
+ precomputedCriterion?: BindingTag | null,
751
737
  ): Value | undefined {
752
- 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
+ );
753
744
  if (entry === undefined) {
754
745
  return undefined;
755
746
  }
@@ -862,13 +853,24 @@ export class DependencyResolver implements ResolverCallbacks {
862
853
  return this.#resolveBindingAsync(binding, undefined, resolutionStack, branchDepth, owner);
863
854
  }
864
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
+
865
866
  async resolveAsync<Value>(
866
867
  token: Token<Value> | Constructor<Value>,
867
868
  options: ResolveOptions | undefined,
868
869
  resolutionStack: Array<ResolutionFrame>,
869
870
  branchDepth: BranchDepth = UNOWNED_BRANCH,
871
+ precomputedCriterion?: BindingTag | null,
870
872
  ): Promise<Value> {
871
- const { binding, owner } = this.#requireBinding(token, options, resolutionStack);
873
+ const { binding, owner } = this.#requireBinding(token, options, resolutionStack, precomputedCriterion);
872
874
 
873
875
  if (binding.scope === "singleton" && owner !== this) {
874
876
  return owner.#resolveBindingAsync(binding, options, resolutionStack, branchDepth, owner) as Promise<Value>;
@@ -1107,12 +1109,12 @@ export class DependencyResolver implements ResolverCallbacks {
1107
1109
  return this.resolveAllAsync(dep.token, options, resolutionStack, branchDepth);
1108
1110
  }
1109
1111
  if (dep.optional) {
1110
- return this.resolveOptionalAsync(dep.token, options, resolutionStack, branchDepth);
1112
+ return this.resolveOptionalAsync(dep.token, options, resolutionStack, branchDepth, singleCriterionForSlot(dep));
1111
1113
  }
1112
1114
  if (options === undefined) {
1113
1115
  return this.resolveAsyncFromContext(dep.token, resolutionStack, branchDepth);
1114
1116
  }
1115
- return this.resolveAsync(dep.token, options, resolutionStack, branchDepth);
1117
+ return this.resolveAsync(dep.token, options, resolutionStack, branchDepth, singleCriterionForSlot(dep));
1116
1118
  }
1117
1119
 
1118
1120
  async resolveOptionalAsync<Value>(
@@ -1120,8 +1122,14 @@ export class DependencyResolver implements ResolverCallbacks {
1120
1122
  options: ResolveOptions | undefined,
1121
1123
  resolutionStack: Array<ResolutionFrame>,
1122
1124
  branchDepth: BranchDepth = UNOWNED_BRANCH,
1125
+ precomputedCriterion?: BindingTag | null,
1123
1126
  ): Promise<Value | undefined> {
1124
- 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
+ );
1125
1133
  if (entry === undefined) {
1126
1134
  return undefined;
1127
1135
  }
@@ -1172,29 +1180,11 @@ export class DependencyResolver implements ResolverCallbacks {
1172
1180
  return result;
1173
1181
  }
1174
1182
 
1175
- /** Every binding the chain's name indexes hold for one name, nearest container first. */
1176
- #namedBindingsFromChain(token: Token<unknown> | Constructor, name: string): Array<Binding> {
1177
- // A name resolves to at most one binding per registry, so a root container's answer is built
1178
- // whole rather than grown — the list is sized at its allocation.
1179
- const ownBinding = this.#registry.getSimpleNamed(token, name);
1180
- if (this.#parent === undefined) {
1181
- return ownBinding === undefined ? [] : [ownBinding];
1182
- }
1183
- const result: Array<Binding> = ownBinding === undefined ? [] : [ownBinding];
1184
- for (let current: DependencyResolver | undefined = this.#parent; current !== undefined; current = current.#parent) {
1185
- const binding = current.#registry.getSimpleNamed(token, name);
1186
- if (binding !== undefined) {
1187
- result.push(binding);
1188
- }
1189
- }
1190
- return result;
1191
- }
1192
-
1193
1183
  /**
1194
- * 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.
1195
1185
  *
1196
- * @remarks Kept off `#candidateBindings` so that method stays the size it was: a request neither
1197
- * 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.
1198
1188
  * An index has matched the slot already, but a hit may still carry a predicate, and evaluating
1199
1189
  * that is the selection path's job.
1200
1190
  */
@@ -1203,31 +1193,26 @@ export class DependencyResolver implements ResolverCallbacks {
1203
1193
  options: ResolveOptions,
1204
1194
  resolutionStack: Array<ResolutionFrame>,
1205
1195
  ): ReadonlyArray<Binding> | null {
1206
- if (isNameOnlyOptions(options)) {
1207
- const named = this.#namedBindingsFromChain(token, options.name);
1208
- return anyPredicate(named)
1209
- ? selectAllBindings(named, options, this.#makeConstraintContext(resolutionStack, options))
1210
- : named;
1211
- }
1212
- const singleTag = singleTagOnlyOf(options);
1213
- if (singleTag === undefined) {
1196
+ const singleCriterion = singleCriterionOnlyOf(options);
1197
+ if (singleCriterion === undefined) {
1214
1198
  return null;
1215
1199
  }
1216
- const tagged = this.#taggedBindingsFromChain(token, singleTag);
1217
- return anyPredicate(tagged)
1218
- ? selectAllBindings(tagged, options, this.#makeConstraintContext(resolutionStack, options))
1219
- : tagged;
1200
+ const indexed = this.#taggedBindingsFromChain(token, singleCriterion);
1201
+ return anyPredicate(indexed)
1202
+ ? selectAllBindings(indexed, options, this.#makeConstraintContext(resolutionStack, options))
1203
+ : indexed;
1220
1204
  }
1221
1205
 
1222
1206
  /**
1223
- * 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.
1224
1208
  *
1225
- * @remarks A request for one tag and no name matches exactly the bindings the index keys, so this
1226
- * 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.
1227
1212
  */
1228
1213
  #taggedBindingsFromChain(token: Token<unknown> | Constructor, tag: BindingTag): Array<Binding> {
1229
- // A tag matches at most one binding per registry, so a root container's answer is built whole
1230
- // 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.
1231
1216
  const ownBinding = this.#registry.getSimpleTagged(token, tag);
1232
1217
  if (this.#parent === undefined) {
1233
1218
  return ownBinding === undefined ? [] : [ownBinding];
@@ -1284,18 +1269,23 @@ export class DependencyResolver implements ResolverCallbacks {
1284
1269
  return buildConstraintContext(resolutionStack, options);
1285
1270
  }
1286
1271
 
1287
- /** 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. */
1288
1273
  #selectMultiTagged(
1289
1274
  token: Token<unknown> | Constructor,
1290
1275
  options: ResolveOptions,
1291
1276
  resolutionStack: Array<ResolutionFrame>,
1292
1277
  ): Binding | undefined {
1293
1278
  const candidates: Array<Binding> = [];
1294
- 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);
1295
1285
  const listed = options.tags;
1296
1286
  if (listed !== undefined) {
1297
1287
  for (let index = 0; index < listed.length; index += 1) {
1298
- this.#gatherTagCandidates(token, listed[index], candidates);
1288
+ this.#gatherTagCandidates(token, listed[index], candidates, gathered);
1299
1289
  }
1300
1290
  }
1301
1291
  if (candidates.length === 0) {
@@ -1309,37 +1299,26 @@ export class DependencyResolver implements ResolverCallbacks {
1309
1299
  token: Token<unknown> | Constructor,
1310
1300
  criterion: BindingTag | undefined,
1311
1301
  out: Array<Binding>,
1302
+ gathered: Array<BindingTag>,
1312
1303
  ): void {
1313
- 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)) {
1314
1307
  return;
1315
1308
  }
1316
- // The includes probes only guard a request repeating a criterion across its two spellings.
1309
+ gathered.push(criterion);
1317
1310
  const single = this.#registry.getSimpleTagged(token, criterion);
1318
- if (single !== undefined && !out.includes(single)) {
1311
+ if (single !== undefined) {
1319
1312
  out.push(single);
1320
1313
  }
1321
1314
  const bucket = this.#registry.getMultiTagged(token, criterion);
1322
1315
  if (bucket !== undefined) {
1323
1316
  for (let index = 0; index < bucket.length; index += 1) {
1324
- const candidate = bucket[index]!;
1325
- if (!out.includes(candidate)) {
1326
- out.push(candidate);
1327
- }
1317
+ out.push(bucket[index]!);
1328
1318
  }
1329
1319
  }
1330
1320
  }
1331
1321
 
1332
- #matchesBindingFast(
1333
- binding: Binding,
1334
- options: ResolveOptions | undefined,
1335
- resolutionStack: Array<ResolutionFrame>,
1336
- ): boolean {
1337
- if (!matchesSlot(binding.slot, options)) {
1338
- return false;
1339
- }
1340
- return this.#satisfiesPredicate(binding, options, resolutionStack);
1341
- }
1342
-
1343
1322
  /** The predicate half of a match, for a lane whose index has already settled the slot. */
1344
1323
  #satisfiesPredicate(
1345
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
  }