@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.
- package/CHANGELOG.md +27 -0
- package/dist/container/binding-builders.d.ts.map +1 -1
- package/dist/container/binding-builders.js +4 -5
- package/dist/container/binding-builders.js.map +1 -1
- package/dist/container/container.d.ts.map +1 -1
- package/dist/container/container.js +3 -4
- package/dist/container/container.js.map +1 -1
- package/dist/core/binding.d.ts +15 -2
- package/dist/core/binding.d.ts.map +1 -1
- package/dist/core/binding.js +29 -4
- package/dist/core/binding.js.map +1 -1
- package/dist/core/registry.d.ts +0 -3
- package/dist/core/registry.d.ts.map +1 -1
- package/dist/core/registry.js +14 -61
- package/dist/core/registry.js.map +1 -1
- package/dist/core/tag.d.ts +26 -0
- package/dist/core/tag.d.ts.map +1 -1
- package/dist/core/tag.js +54 -11
- package/dist/core/tag.js.map +1 -1
- package/dist/errors/errors.d.ts.map +1 -1
- package/dist/errors/errors.js +28 -3
- package/dist/errors/errors.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/injection/resolve-options.d.ts +17 -12
- package/dist/injection/resolve-options.d.ts.map +1 -1
- package/dist/injection/resolve-options.js +69 -15
- package/dist/injection/resolve-options.js.map +1 -1
- package/dist/introspection/dependency-graph.d.ts.map +1 -1
- package/dist/introspection/dependency-graph.js +22 -9
- package/dist/introspection/dependency-graph.js.map +1 -1
- package/dist/resolution/cache/binding-lookup-cache.d.ts +1 -3
- package/dist/resolution/cache/binding-lookup-cache.d.ts.map +1 -1
- package/dist/resolution/cache/binding-lookup-cache.js +1 -38
- package/dist/resolution/cache/binding-lookup-cache.js.map +1 -1
- package/dist/resolution/plan/instantiation-plan.d.ts +4 -12
- package/dist/resolution/plan/instantiation-plan.d.ts.map +1 -1
- package/dist/resolution/plan/instantiation-plan.js +9 -27
- package/dist/resolution/plan/instantiation-plan.js.map +1 -1
- package/dist/resolution/resolver.d.ts +12 -5
- package/dist/resolution/resolver.d.ts.map +1 -1
- package/dist/resolution/resolver.js +106 -141
- package/dist/resolution/resolver.js.map +1 -1
- package/dist/resolution/select/binding-select.d.ts +4 -5
- package/dist/resolution/select/binding-select.d.ts.map +1 -1
- package/dist/resolution/select/binding-select.js +18 -20
- package/dist/resolution/select/binding-select.js.map +1 -1
- package/package.json +2 -2
- package/src/container/binding-builders.ts +10 -5
- package/src/container/container.ts +3 -4
- package/src/core/binding.ts +33 -5
- package/src/core/registry.ts +14 -66
- package/src/core/tag.ts +71 -17
- package/src/errors/errors.ts +28 -5
- package/src/index.ts +1 -1
- package/src/injection/resolve-options.ts +75 -18
- package/src/introspection/dependency-graph.ts +23 -9
- package/src/resolution/cache/binding-lookup-cache.ts +1 -41
- package/src/resolution/plan/instantiation-plan.ts +13 -38
- package/src/resolution/resolver.ts +120 -141
- 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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
269
|
-
// escapes only because
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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
|
-
|
|
545
|
-
|
|
546
|
-
|
|
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 {
|
|
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 (
|
|
177
|
-
const
|
|
178
|
-
if (
|
|
179
|
-
return { binding:
|
|
180
|
-
}
|
|
181
|
-
} else
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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.#
|
|
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
|
|
433
|
-
// is the compiler's cue to leave the selection to the runtime.
|
|
434
|
-
|
|
435
|
-
const
|
|
436
|
-
if (
|
|
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
|
-
|
|
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
|
-
//
|
|
513
|
-
// semantics involve no resolution context (constants, cached singletons).
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
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 (
|
|
537
|
-
return
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
1197
|
-
* index
|
|
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
|
-
|
|
1207
|
-
|
|
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
|
|
1217
|
-
return anyPredicate(
|
|
1218
|
-
? selectAllBindings(
|
|
1219
|
-
:
|
|
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
|
|
1207
|
+
* Every binding the chain's criterion indexes hold for one criterion, nearest container first.
|
|
1224
1208
|
*
|
|
1225
|
-
* @remarks A request for one
|
|
1226
|
-
* is the whole candidate set rather than a prefilter — a
|
|
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
|
|
1230
|
-
// rather than grown — the
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1309
|
+
gathered.push(criterion);
|
|
1317
1310
|
const single = this.#registry.getSimpleTagged(token, criterion);
|
|
1318
|
-
if (single !== undefined
|
|
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
|
-
|
|
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 =
|
|
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
|
|
55
|
-
function
|
|
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
|
|
121
|
+
return requestedTagKeyMask(options) !== NO_TAG_KEYS;
|
|
122
122
|
}
|
|
123
123
|
|
|
124
124
|
/**
|
|
125
|
-
* Whether a binding's slot satisfies a request:
|
|
126
|
-
*
|
|
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
|
|
129
|
-
*
|
|
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
|
|
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
|
}
|