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