@codefast/di 0.10.0 → 0.11.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 +293 -0
- package/README.md +71 -7
- package/dist/ambient/active-container.d.ts +13 -11
- package/dist/ambient/active-container.js +8 -6
- package/dist/container/binding-builders.d.ts +25 -7
- package/dist/container/binding-builders.js +110 -42
- package/dist/container/container.d.ts +8 -8
- package/dist/container/container.js +244 -67
- package/dist/core/binding-declaration.d.ts +120 -0
- package/dist/core/binding-declaration.js +186 -0
- package/dist/core/binding-scope.d.ts +2 -2
- package/dist/core/binding.d.ts +61 -9
- package/dist/core/binding.js +49 -2
- package/dist/core/constraint-requirement.d.ts +1 -1
- package/dist/core/module.d.ts +10 -7
- package/dist/core/module.js +17 -3
- package/dist/core/registry.d.ts +17 -13
- package/dist/core/registry.js +155 -59
- 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/core/token.d.ts +1 -1
- package/dist/core/types.d.ts +5 -5
- package/dist/decorators/decorator-metadata.d.ts +9 -0
- package/dist/decorators/decorator-metadata.js +20 -0
- package/dist/decorators/inject.d.ts +3 -3
- package/dist/decorators/inject.js +7 -6
- package/dist/decorators/injectable.d.ts +2 -2
- package/dist/decorators/injectable.js +5 -3
- package/dist/decorators/lifecycle-decorators.js +9 -3
- package/dist/errors/errors.d.ts +85 -14
- package/dist/errors/errors.js +100 -20
- package/dist/index.d.ts +37 -35
- package/dist/index.js +20 -19
- package/dist/injection/descriptor.d.ts +3 -3
- package/dist/injection/descriptor.js +3 -7
- package/dist/injection/resolve-options.d.ts +3 -3
- package/dist/injection/resolve-options.js +7 -5
- package/dist/introspection/dependency-graph.d.ts +10 -5
- package/dist/introspection/dependency-graph.js +51 -28
- package/dist/introspection/graph-adapters/cytoscape.d.ts +1 -1
- package/dist/introspection/graph-adapters/dot.d.ts +1 -1
- package/dist/introspection/graph-adapters/mermaid.d.ts +1 -1
- package/dist/introspection/graph-adapters/reactflow.d.ts +1 -1
- package/dist/introspection/graph-adapters/reactflow.js +6 -4
- package/dist/introspection/inspector.d.ts +4 -4
- package/dist/introspection/inspector.js +9 -12
- package/dist/lifecycle/lifecycle-manager.d.ts +4 -4
- package/dist/lifecycle/lifecycle-manager.js +18 -6
- package/dist/lifecycle/scope-manager.d.ts +2 -2
- package/dist/lifecycle/scope-manager.js +31 -13
- package/dist/metadata/metadata-reader-token.d.ts +2 -2
- package/dist/metadata/metadata-reader-token.js +1 -1
- package/dist/metadata/metadata-types.d.ts +3 -3
- package/dist/metadata/symbol-metadata-reader.d.ts +3 -3
- package/dist/metadata/symbol-metadata-reader.js +1 -1
- package/dist/metadata/verifying-metadata-reader.d.ts +5 -4
- package/dist/metadata/verifying-metadata-reader.js +30 -8
- 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 +4 -5
- package/dist/resolution/cache/activation-need.js +11 -18
- package/dist/resolution/cache/binding-lookup-cache.d.ts +5 -12
- package/dist/resolution/cache/binding-lookup-cache.js +32 -19
- package/dist/resolution/cache/class-introspector.d.ts +16 -5
- package/dist/resolution/cache/class-introspector.js +73 -56
- package/dist/resolution/context.d.ts +17 -25
- package/dist/resolution/context.js +47 -56
- package/dist/resolution/path/resolution-path.d.ts +48 -13
- package/dist/resolution/path/resolution-path.js +90 -39
- package/dist/resolution/plan/instantiation-plan.d.ts +5 -5
- package/dist/resolution/plan/instantiation-plan.js +66 -26
- package/dist/resolution/plan/plan-codegen.d.ts +10 -7
- package/dist/resolution/plan/plan-codegen.js +62 -34
- package/dist/resolution/resolver.d.ts +16 -17
- package/dist/resolution/resolver.js +316 -273
- package/dist/resolution/select/binding-select.d.ts +6 -5
- package/dist/resolution/select/binding-select.js +5 -4
- package/dist/resolution/select/constraints.d.ts +3 -3
- package/dist/resolution/select/constraints.js +4 -4
- package/package.json +14 -2
|
@@ -1,19 +1,17 @@
|
|
|
1
|
-
import { NO_INSTANCE } from "
|
|
2
|
-
import {
|
|
3
|
-
import { tokenName } from "
|
|
4
|
-
import { AsyncActivationError, AsyncResolutionError, CircularDependencyError, DisposedContainerError, InternalError, MissingMetadataError, MissingScopeContextError, NoMatchingBindingError, TokenNotBoundError, } from "
|
|
5
|
-
import { loneTagBesideNameOf, resolveOptionsForSlot, singleCriterionForSlot, singleCriterionOnlyOf, } from "
|
|
6
|
-
import { SCOPED_MISS } from "
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
|
|
15
|
-
const MULTI_TAG_INDEX_THRESHOLD = 8;
|
|
16
|
-
const EMPTY_STRING_LIST = [];
|
|
1
|
+
import { bindingSlotToString, NO_INSTANCE } from "#core/binding";
|
|
2
|
+
import { slotNameCriterionOf } from "#core/tag";
|
|
3
|
+
import { tokenName } from "#core/token";
|
|
4
|
+
import { AsyncActivationError, AsyncResolutionError, CircularDependencyError, DisposedContainerError, InternalError, MissingMetadataError, MissingScopeContextError, NoMatchingBindingError, TokenNotBoundError, } from "#errors/errors";
|
|
5
|
+
import { loneTagBesideNameOf, resolveOptionsForSlot, singleCriterionForSlot, singleCriterionOnlyOf, } from "#injection/resolve-options";
|
|
6
|
+
import { SCOPED_MISS } from "#lifecycle/scope-manager";
|
|
7
|
+
import { settleInOrder } from "#resolution/async-fan-out";
|
|
8
|
+
import { ActivationNeedCache } from "#resolution/cache/activation-need";
|
|
9
|
+
import { BindingLookupCache } from "#resolution/cache/binding-lookup-cache";
|
|
10
|
+
import { ClassIntrospector } from "#resolution/cache/class-introspector";
|
|
11
|
+
import { AsyncLevelContext, DefaultConstraintContext, DefaultResolutionContext } from "#resolution/context";
|
|
12
|
+
import { branchDepthOf, buildResolutionFrame, cycleNamesOf, enterSyncPath, extendResolutionBranch, leaveSyncPath, ROOT_BRANCH, UNOWNED_BRANCH, } from "#resolution/path/resolution-path";
|
|
13
|
+
import { InstantiationPlanCompiler, PLAN_RETRY } from "#resolution/plan/instantiation-plan";
|
|
14
|
+
import { matchesSlot, selectAllBindings, selectBinding } from "#resolution/select/binding-select";
|
|
17
15
|
const EMPTY_FRAME_LIST = [];
|
|
18
16
|
const EMPTY_PARAM_LIST = [];
|
|
19
17
|
/** Plans compiled so far, with the `null` unplannable marks left out. */
|
|
@@ -28,22 +26,15 @@ function countCompiledPlans(plans) {
|
|
|
28
26
|
}
|
|
29
27
|
return count;
|
|
30
28
|
}
|
|
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
|
-
};
|
|
29
|
+
const ROOT_CONSTRAINT_CONTEXT = new DefaultConstraintContext(EMPTY_FRAME_LIST, undefined);
|
|
38
30
|
/**
|
|
39
31
|
* The resolution engine driving binding selection, instantiation, scoping, and lifecycle hooks.
|
|
40
32
|
*
|
|
41
33
|
* @since 0.3.16-canary.0
|
|
42
34
|
*/
|
|
43
35
|
export class DependencyResolver {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
#cascadeContextPool;
|
|
36
|
+
// Contexts pooled by depth over the root stack — deferred: a plan-served or constant-only container never needs one.
|
|
37
|
+
#syncResolutionContextPool;
|
|
47
38
|
/**
|
|
48
39
|
* The stack a top-level **sync** resolve reuses instead of minting an array per call.
|
|
49
40
|
*
|
|
@@ -53,25 +44,31 @@ export class DependencyResolver {
|
|
|
53
44
|
* mints its own. Keeping it stable is also what lets a pooled context skip re-storing it.
|
|
54
45
|
*/
|
|
55
46
|
rootStack = [];
|
|
56
|
-
// The
|
|
57
|
-
//
|
|
58
|
-
|
|
59
|
-
#
|
|
47
|
+
// The last root level's context, kept so one instance is always live: a full collection that finds
|
|
48
|
+
// none deoptimizes every optimized site that embedded the class's shape, and does so on every
|
|
49
|
+
// collection after — measured on the async chain rows, and removed by this one field.
|
|
50
|
+
#recentRootLevelContext;
|
|
60
51
|
// 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.
|
|
52
|
+
// maps are allocated by the first plan request, which only a class or resolved binding makes. A
|
|
53
|
+
// root is compiled on the request that repeats it — the first interprets, so a container that
|
|
54
|
+
// resolves a root once never compiles — and the set below remembers the first.
|
|
62
55
|
#classPlanByBindingId;
|
|
56
|
+
#classPlanRequestedOnce;
|
|
63
57
|
#classPlanRegistryVersion = -1;
|
|
64
58
|
#classPlanActivationVersion = -1;
|
|
65
59
|
// The async lane's plans, stamped and invalidated apart so neither lane pays the other's misses.
|
|
66
60
|
#asyncPlanByBindingId;
|
|
61
|
+
#asyncPlanRequestedOnce;
|
|
67
62
|
#asyncPlanRegistryVersion = -1;
|
|
68
63
|
#asyncPlanActivationVersion = -1;
|
|
69
64
|
#registry;
|
|
70
65
|
#scope;
|
|
71
66
|
#lifecycle;
|
|
72
67
|
#metadataReader;
|
|
68
|
+
#container;
|
|
73
69
|
#parent;
|
|
74
70
|
#lookup;
|
|
71
|
+
// Built by the first class resolve: a container that never resolves a class never pays for it.
|
|
75
72
|
#classes;
|
|
76
73
|
// Built by the first interpreted resolve that asks; a plan-served or constant-only container never does.
|
|
77
74
|
#activation;
|
|
@@ -80,16 +77,19 @@ export class DependencyResolver {
|
|
|
80
77
|
this.#scope = scope;
|
|
81
78
|
this.#lifecycle = lifecycle;
|
|
82
79
|
this.#metadataReader = metadataReader;
|
|
80
|
+
this.#container = container;
|
|
83
81
|
this.#parent = parent;
|
|
84
82
|
this.#lookup = new BindingLookupCache(registry, this, parent === undefined ? undefined : parent.#lookup);
|
|
85
|
-
|
|
83
|
+
}
|
|
84
|
+
#introspector() {
|
|
85
|
+
return (this.#classes ??= new ClassIntrospector(this.#metadataReader, this.#container, this.#parent === undefined ? undefined : this.#parent.#introspector()));
|
|
86
86
|
}
|
|
87
87
|
/** The reader this resolver was built with, which is the one its container answers with. */
|
|
88
88
|
get metadataReader() {
|
|
89
89
|
return this.#metadataReader;
|
|
90
90
|
}
|
|
91
91
|
#activationNeed() {
|
|
92
|
-
return (this.#activation ??= new ActivationNeedCache(this.#lifecycle, this.#
|
|
92
|
+
return (this.#activation ??= new ActivationNeedCache(this.#lifecycle, this.#introspector(), this.#registry));
|
|
93
93
|
}
|
|
94
94
|
/** Structural counts and the resolver-owned collaborators built so far, for the {@link ResolutionDiagnostics} a container reports. */
|
|
95
95
|
describeCaches() {
|
|
@@ -100,18 +100,18 @@ export class DependencyResolver {
|
|
|
100
100
|
if (this.#lookup.isMemoBuilt) {
|
|
101
101
|
builtSubsystems.push("resolver.lookupMemo");
|
|
102
102
|
}
|
|
103
|
-
if (this.#activation?.isMemoBuilt === true) {
|
|
104
|
-
builtSubsystems.push("resolver.activationNeedMemo");
|
|
105
|
-
}
|
|
106
103
|
const generatedPlanCount = this.#planCompiler?.generatedPlanCount ?? 0;
|
|
107
104
|
if (generatedPlanCount > 0) {
|
|
108
105
|
builtSubsystems.push("resolver.planCodegen");
|
|
109
106
|
}
|
|
107
|
+
if (this.#recentRootLevelContext !== undefined) {
|
|
108
|
+
builtSubsystems.push("resolver.asyncRootLevel");
|
|
109
|
+
}
|
|
110
110
|
return {
|
|
111
111
|
compiledPlanCount: countCompiledPlans(this.#classPlanByBindingId),
|
|
112
112
|
compiledAsyncPlanCount: countCompiledPlans(this.#asyncPlanByBindingId),
|
|
113
113
|
generatedPlanCount,
|
|
114
|
-
syncContextPoolSize: this.#syncResolutionContextPool
|
|
114
|
+
syncContextPoolSize: this.#syncResolutionContextPool?.length ?? 0,
|
|
115
115
|
builtSubsystems,
|
|
116
116
|
};
|
|
117
117
|
}
|
|
@@ -134,23 +134,19 @@ export class DependencyResolver {
|
|
|
134
134
|
if (indexed === undefined) {
|
|
135
135
|
// A one-criterion request matches only a slot carrying exactly that criterion, and every such
|
|
136
136
|
// 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);
|
|
137
|
+
return this.#forwardingAliasOrParent(token, options, resolutionStack, singleCriterion);
|
|
140
138
|
}
|
|
141
139
|
if (this.#satisfiesPredicate(indexed, options, resolutionStack)) {
|
|
142
140
|
return { binding: indexed, owner: this };
|
|
143
141
|
}
|
|
144
142
|
}
|
|
145
|
-
else {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
return undefined;
|
|
153
|
-
}
|
|
143
|
+
else if (options.name !== undefined) {
|
|
144
|
+
const pairTag = loneTagBesideNameOf(options);
|
|
145
|
+
if (pairTag !== undefined) {
|
|
146
|
+
const nameCriterion = slotNameCriterionOf(options.name);
|
|
147
|
+
// A name interned nowhere cannot key a slot, but a slot whose criteria are a subset of the
|
|
148
|
+
// request's still matches — so an index miss falls through to the scan, never a clean miss.
|
|
149
|
+
if (nameCriterion !== undefined) {
|
|
154
150
|
// The exact two-criterion slot, memoized over the chain; a predicate or an alias declines to the scan.
|
|
155
151
|
const entry = this.#lookup.namedTaggedEntry(token, nameCriterion, pairTag);
|
|
156
152
|
if (entry !== null) {
|
|
@@ -158,22 +154,6 @@ export class DependencyResolver {
|
|
|
158
154
|
}
|
|
159
155
|
}
|
|
160
156
|
}
|
|
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
157
|
}
|
|
178
158
|
// A lone default-slot candidate is its own selection: the slot match is the whole decision,
|
|
179
159
|
// it carries no predicate, and asking for it first keeps the registry from materialising its list.
|
|
@@ -194,10 +174,29 @@ export class DependencyResolver {
|
|
|
194
174
|
}
|
|
195
175
|
}
|
|
196
176
|
}
|
|
197
|
-
if (
|
|
198
|
-
return this.#parent
|
|
177
|
+
if (options === undefined) {
|
|
178
|
+
return this.#parent === undefined
|
|
179
|
+
? undefined
|
|
180
|
+
: this.#parent.#findBinding(token, options, resolutionStack, singleCriterion);
|
|
199
181
|
}
|
|
200
|
-
return
|
|
182
|
+
return this.#forwardingAliasOrParent(token, options, resolutionStack, singleCriterion);
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* What a request with criteria finds once no slot of this registry matches them: the token's
|
|
186
|
+
* default-slot alias, which forwards them to its target, else the parent's answer.
|
|
187
|
+
*
|
|
188
|
+
* @remarks A default-slot alias carries no criterion, predicate or membership, so it is a pointer
|
|
189
|
+
* rather than a candidate — every exact slot here was tried first, and the nearest container that
|
|
190
|
+
* can answer still does.
|
|
191
|
+
*/
|
|
192
|
+
#forwardingAliasOrParent(token, options, resolutionStack, singleCriterion) {
|
|
193
|
+
const defaultSlot = this.#registry.getDefaultSlotBinding(token);
|
|
194
|
+
if (defaultSlot !== undefined && defaultSlot.kind === "alias") {
|
|
195
|
+
return { binding: defaultSlot, owner: this };
|
|
196
|
+
}
|
|
197
|
+
return this.#parent === undefined
|
|
198
|
+
? undefined
|
|
199
|
+
: this.#parent.#findBinding(token, options, resolutionStack, singleCriterion);
|
|
201
200
|
}
|
|
202
201
|
/**
|
|
203
202
|
* The scan a candidate list gets before full selection.
|
|
@@ -248,14 +247,40 @@ export class DependencyResolver {
|
|
|
248
247
|
}
|
|
249
248
|
if (found === undefined) {
|
|
250
249
|
// 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
|
-
|
|
250
|
+
// dominated by that capture. Bindings under the token anywhere in the chain mean the request
|
|
251
|
+
// matched none of them, so a child reports the same miss its parent would.
|
|
252
|
+
const bound = this.#allBindingsFromChain(currentToken);
|
|
253
|
+
if (bound.length > 0) {
|
|
254
|
+
throw new NoMatchingBindingError(tokenName(currentToken), options ?? {}, bound.map((binding) => bindingSlotToString(binding.slot)));
|
|
254
255
|
}
|
|
255
256
|
throw new TokenNotBoundError(tokenName(currentToken));
|
|
256
257
|
}
|
|
257
258
|
return found;
|
|
258
259
|
}
|
|
260
|
+
/**
|
|
261
|
+
* Follows an alias binding's chain to its terminal binding, or `undefined` when the chain ends at
|
|
262
|
+
* a token nothing matches.
|
|
263
|
+
*
|
|
264
|
+
* @remarks The non-throwing twin of {@link DependencyResolver.#requireBinding}'s alias walk, for
|
|
265
|
+
* the optional and collection lanes: a dangling chain is a miss, but a revisited alias token still
|
|
266
|
+
* raises {@link CircularDependencyError} — a cycle has no absent reading.
|
|
267
|
+
*/
|
|
268
|
+
#terminalOfAliasBinding(alias, options, resolutionStack, singleCriterion) {
|
|
269
|
+
let currentToken = alias.token;
|
|
270
|
+
let found = { binding: alias, owner: this };
|
|
271
|
+
let visitedAliasTokens;
|
|
272
|
+
while (found !== undefined && found.binding.kind === "alias") {
|
|
273
|
+
const target = found.binding.target;
|
|
274
|
+
visitedAliasTokens ??= new Set([currentToken]);
|
|
275
|
+
if (visitedAliasTokens.has(target)) {
|
|
276
|
+
throw new CircularDependencyError([...visitedAliasTokens, target].map((entry) => tokenName(entry)));
|
|
277
|
+
}
|
|
278
|
+
visitedAliasTokens.add(target);
|
|
279
|
+
currentToken = target;
|
|
280
|
+
found = this.#findBinding(currentToken, options, resolutionStack, singleCriterion);
|
|
281
|
+
}
|
|
282
|
+
return found;
|
|
283
|
+
}
|
|
259
284
|
/**
|
|
260
285
|
* Binding lookup aligned with `resolve` — used by `Container.validate` without instantiating.
|
|
261
286
|
*/
|
|
@@ -346,6 +371,8 @@ export class DependencyResolver {
|
|
|
346
371
|
if (binding.activationHook !== undefined) {
|
|
347
372
|
const activationResult = binding.activationHook(resolutionCtx, activated);
|
|
348
373
|
if (activationResult instanceof Promise) {
|
|
374
|
+
// The hook has already run; adopt its rejection so it cannot surface as unhandled.
|
|
375
|
+
void activationResult.catch(() => { });
|
|
349
376
|
throw new AsyncActivationError(tokenDisplayName, "onActivation");
|
|
350
377
|
}
|
|
351
378
|
activated = activationResult;
|
|
@@ -354,6 +381,7 @@ export class DependencyResolver {
|
|
|
354
381
|
for (let index = 0; index < containerHooks.length; index += 1) {
|
|
355
382
|
const activationResult = containerHooks[index](resolutionCtx, activated);
|
|
356
383
|
if (activationResult instanceof Promise) {
|
|
384
|
+
void activationResult.catch(() => { });
|
|
357
385
|
throw new AsyncActivationError(tokenDisplayName, "onActivation");
|
|
358
386
|
}
|
|
359
387
|
activated = activationResult;
|
|
@@ -390,6 +418,7 @@ export class DependencyResolver {
|
|
|
390
418
|
}
|
|
391
419
|
else {
|
|
392
420
|
this.#classPlanByBindingId.clear();
|
|
421
|
+
this.#classPlanRequestedOnce?.clear();
|
|
393
422
|
}
|
|
394
423
|
this.#classPlanRegistryVersion = registryVersion;
|
|
395
424
|
this.#classPlanActivationVersion = activationVersion;
|
|
@@ -399,6 +428,12 @@ export class DependencyResolver {
|
|
|
399
428
|
if (cached !== undefined) {
|
|
400
429
|
return cached;
|
|
401
430
|
}
|
|
431
|
+
const requestedOnce = (this.#classPlanRequestedOnce ??= new Set());
|
|
432
|
+
if (!requestedOnce.has(binding.identifier)) {
|
|
433
|
+
// The first request interprets; the one that repeats it compiles.
|
|
434
|
+
requestedOnce.add(binding.identifier);
|
|
435
|
+
return null;
|
|
436
|
+
}
|
|
402
437
|
const compiled = this.#compiler().compile(binding);
|
|
403
438
|
if (compiled === PLAN_RETRY) {
|
|
404
439
|
// Lifecycle metadata not discovered yet — the fallback resolve discovers it; retry then.
|
|
@@ -411,25 +446,29 @@ export class DependencyResolver {
|
|
|
411
446
|
// that never resolves a class or a resolved factory never pays for the host.
|
|
412
447
|
#planCompiler;
|
|
413
448
|
#compiler() {
|
|
414
|
-
return (this.#planCompiler ??= new InstantiationPlanCompiler(
|
|
449
|
+
return (this.#planCompiler ??= new InstantiationPlanCompiler(this.#buildPlanCompilerHost()));
|
|
450
|
+
}
|
|
451
|
+
// The behaviour the plan compiler needs from this resolver — lookups, escapes, plan swaps, and the
|
|
452
|
+
// accessor construction path. Built once with the compiler, so each closure is allocated once.
|
|
453
|
+
#buildPlanCompilerHost() {
|
|
454
|
+
const classes = this.#introspector();
|
|
455
|
+
return {
|
|
415
456
|
hasActivationHandlers: (binding) => this.#ownerOf(binding).#lifecycle.hasActivationHandlers(binding.token),
|
|
416
|
-
knownPostConstruct: (target) =>
|
|
417
|
-
needsActiveContainer: (target) =>
|
|
457
|
+
knownPostConstruct: (target) => classes.knownPostConstruct(target),
|
|
458
|
+
needsActiveContainer: (target) => classes.needsActiveContainer(target),
|
|
418
459
|
// A plan runs at the top level, so the lent root stack is free when it is; a plan reached
|
|
419
460
|
// with the root stack held mints its own path, exactly as the interpreted lane would.
|
|
420
461
|
constructWithAccessors: (binding, target, deps) => {
|
|
421
462
|
const stack = this.rootStack.length === 0 ? this.rootStack : [];
|
|
422
|
-
|
|
423
|
-
const resolutionSet = enterResolutionPath(stack, frame);
|
|
463
|
+
enterSyncPath(stack, binding, this.#getResolutionFrame(binding));
|
|
424
464
|
try {
|
|
425
|
-
return
|
|
465
|
+
return classes.instantiate(target, deps, this.#ambientResolutionFor(stack));
|
|
426
466
|
}
|
|
427
467
|
finally {
|
|
428
|
-
stack
|
|
429
|
-
resolutionSet?.delete(frame.bindingId);
|
|
468
|
+
leaveSyncPath(stack, binding);
|
|
430
469
|
}
|
|
431
470
|
},
|
|
432
|
-
getConstructorMetadata: (target) =>
|
|
471
|
+
getConstructorMetadata: (target) => classes.constructorMetadata(target),
|
|
433
472
|
lookupDependencyEntry: (token) => {
|
|
434
473
|
const entry = this.#lookup.defaultEntry(token);
|
|
435
474
|
return entry === null ? null : { binding: entry.binding };
|
|
@@ -488,7 +527,7 @@ export class DependencyResolver {
|
|
|
488
527
|
}
|
|
489
528
|
return this.resolveAsync(token, options, resolutionStack, UNOWNED_BRANCH);
|
|
490
529
|
},
|
|
491
|
-
}
|
|
530
|
+
};
|
|
492
531
|
}
|
|
493
532
|
/** The async lane's plan for a statically-visible transient binding, mirroring the sync getter. */
|
|
494
533
|
#getAsyncInstantiationPlan(binding) {
|
|
@@ -500,6 +539,7 @@ export class DependencyResolver {
|
|
|
500
539
|
}
|
|
501
540
|
else {
|
|
502
541
|
this.#asyncPlanByBindingId.clear();
|
|
542
|
+
this.#asyncPlanRequestedOnce?.clear();
|
|
503
543
|
}
|
|
504
544
|
this.#asyncPlanRegistryVersion = registryVersion;
|
|
505
545
|
this.#asyncPlanActivationVersion = activationVersion;
|
|
@@ -509,6 +549,11 @@ export class DependencyResolver {
|
|
|
509
549
|
if (cached !== undefined) {
|
|
510
550
|
return cached;
|
|
511
551
|
}
|
|
552
|
+
const requestedOnce = (this.#asyncPlanRequestedOnce ??= new Set());
|
|
553
|
+
if (!requestedOnce.has(binding.identifier)) {
|
|
554
|
+
requestedOnce.add(binding.identifier);
|
|
555
|
+
return null;
|
|
556
|
+
}
|
|
512
557
|
const compiled = this.#compiler().compileAsync(binding);
|
|
513
558
|
if (compiled === PLAN_RETRY) {
|
|
514
559
|
// Lifecycle metadata not discovered yet — the fallback resolve discovers it; retry then.
|
|
@@ -576,7 +621,8 @@ export class DependencyResolver {
|
|
|
576
621
|
}
|
|
577
622
|
const frame = this.#getResolutionFrame(binding);
|
|
578
623
|
const tokenDisplayName = frame.tokenName;
|
|
579
|
-
|
|
624
|
+
// The level holds its binding's flag for its whole duration, so a factory below runs bare.
|
|
625
|
+
enterSyncPath(resolutionStack, binding, frame);
|
|
580
626
|
try {
|
|
581
627
|
const needsActivation = owner.#activationNeed().needsActivation(binding);
|
|
582
628
|
if (!needsActivation && scope === "transient" && binding.kind === "dynamic") {
|
|
@@ -604,13 +650,15 @@ export class DependencyResolver {
|
|
|
604
650
|
return activated;
|
|
605
651
|
}
|
|
606
652
|
finally {
|
|
607
|
-
resolutionStack
|
|
608
|
-
resolutionSet?.delete(frame.bindingId);
|
|
653
|
+
leaveSyncPath(resolutionStack, binding);
|
|
609
654
|
}
|
|
610
655
|
}
|
|
611
|
-
/**
|
|
612
|
-
|
|
613
|
-
|
|
656
|
+
/**
|
|
657
|
+
* Path-continuing resolution handed to the ambient slot while an accessor class constructs.
|
|
658
|
+
*
|
|
659
|
+
* @remarks The lent root stack is one array for the resolver's lifetime, so the closure pair over
|
|
660
|
+
* it is built once and reused.
|
|
661
|
+
*/
|
|
614
662
|
#rootAmbientResolution;
|
|
615
663
|
#ambientResolutionFor(resolutionStack) {
|
|
616
664
|
if (resolutionStack === this.rootStack) {
|
|
@@ -644,7 +692,9 @@ export class DependencyResolver {
|
|
|
644
692
|
throw asyncResolutionErrorFor(binding, resolutionStack);
|
|
645
693
|
case "class": {
|
|
646
694
|
const deps = this.#resolveDeps(this.#constructorParams(binding.target), resolutionStack);
|
|
647
|
-
return this.#
|
|
695
|
+
return this.#introspector().instantiate(binding.target, deps, this.#introspector().needsActiveContainer(binding.target)
|
|
696
|
+
? this.#ambientResolutionFor(resolutionStack)
|
|
697
|
+
: undefined);
|
|
648
698
|
}
|
|
649
699
|
case "resolved": {
|
|
650
700
|
const deps = this.#resolveDeps(binding.deps, resolutionStack);
|
|
@@ -667,11 +717,20 @@ export class DependencyResolver {
|
|
|
667
717
|
* no parameters; anything else is a missing `@injectable()`.
|
|
668
718
|
*/
|
|
669
719
|
#constructorParams(target) {
|
|
670
|
-
const meta = this.#
|
|
720
|
+
const meta = this.#introspector().constructorMetadata(target);
|
|
671
721
|
if (meta !== undefined) {
|
|
672
722
|
return meta.params;
|
|
673
723
|
}
|
|
674
724
|
if (target.length === 0) {
|
|
725
|
+
// A subclass with an implicit constructor and no own metadata inherits its base's declared
|
|
726
|
+
// deps but would be built with zero arguments, injecting `undefined` silently — reject it.
|
|
727
|
+
const inherited = this.#introspector().inheritedConstructorMetadata(target);
|
|
728
|
+
if (inherited !== undefined && inherited.metadata.params.length > 0) {
|
|
729
|
+
throw new MissingMetadataError(target.name, {
|
|
730
|
+
baseName: inherited.base.name,
|
|
731
|
+
dependencyCount: inherited.metadata.params.length,
|
|
732
|
+
});
|
|
733
|
+
}
|
|
675
734
|
return EMPTY_PARAM_LIST;
|
|
676
735
|
}
|
|
677
736
|
throw new MissingMetadataError(target.name);
|
|
@@ -719,16 +778,18 @@ export class DependencyResolver {
|
|
|
719
778
|
return undefined;
|
|
720
779
|
}
|
|
721
780
|
}
|
|
722
|
-
const
|
|
781
|
+
const singleCriterion = precomputedCriterion === undefined ? singleCriterionOnlyOf(options) : (precomputedCriterion ?? undefined);
|
|
782
|
+
let entry = this.#findBinding(token, options, resolutionStack, singleCriterion);
|
|
783
|
+
// Resolve the entry the probe found: re-looking the token up would evaluate every `when()`
|
|
784
|
+
// predicate a second time, and a changed answer would throw where `undefined` was promised.
|
|
785
|
+
if (entry !== undefined && entry.binding.kind === "alias") {
|
|
786
|
+
// Follow the alias off the throwing path: a chain that ends nowhere is a miss, not an error.
|
|
787
|
+
entry = this.#terminalOfAliasBinding(entry.binding, options, resolutionStack, singleCriterion);
|
|
788
|
+
}
|
|
723
789
|
if (entry === undefined) {
|
|
724
790
|
return undefined;
|
|
725
791
|
}
|
|
726
|
-
// Resolve the entry the probe found: re-looking the token up would evaluate every `when()`
|
|
727
|
-
// predicate a second time, and a changed answer would throw where `undefined` was promised.
|
|
728
792
|
const { binding, owner } = entry;
|
|
729
|
-
if (binding.kind === "alias") {
|
|
730
|
-
return this.resolve(token, options, resolutionStack);
|
|
731
|
-
}
|
|
732
793
|
if (binding.scope === "singleton" && owner !== this) {
|
|
733
794
|
return owner.#resolveBinding(binding, options, resolutionStack, owner);
|
|
734
795
|
}
|
|
@@ -753,7 +814,10 @@ export class DependencyResolver {
|
|
|
753
814
|
const resolutionStack = this.rootStack;
|
|
754
815
|
const memo = this.#rootCollection(token, resolutionStack);
|
|
755
816
|
if (memo.values !== undefined && memo.activationVersion === this.#chainActivationVersion()) {
|
|
756
|
-
|
|
817
|
+
// A copy, never the memo itself: a caller that writes into its result must not rewrite the
|
|
818
|
+
// next caller's. Reading a frozen array is several times slower than reading a plain one, so
|
|
819
|
+
// the copy is the cheaper guard by far, and it costs one allocation the read already paid before the memo.
|
|
820
|
+
return memo.values.slice();
|
|
757
821
|
}
|
|
758
822
|
const { candidates } = memo;
|
|
759
823
|
const resolved = new Array(candidates.length);
|
|
@@ -770,9 +834,9 @@ export class DependencyResolver {
|
|
|
770
834
|
const resolutionStack = [];
|
|
771
835
|
const memo = this.#rootCollection(token, resolutionStack);
|
|
772
836
|
if (memo.values !== undefined && memo.activationVersion === this.#chainActivationVersion()) {
|
|
773
|
-
return Promise.resolve(memo.values);
|
|
837
|
+
return Promise.resolve(memo.values.slice());
|
|
774
838
|
}
|
|
775
|
-
return
|
|
839
|
+
return settleInOrder(memo.candidates.map((candidate) => this.#resolveCandidateAsync(candidate, undefined, resolutionStack, UNOWNED_BRANCH)), (values) => {
|
|
776
840
|
this.#settleCollectionValues(memo);
|
|
777
841
|
return values;
|
|
778
842
|
});
|
|
@@ -811,8 +875,7 @@ export class DependencyResolver {
|
|
|
811
875
|
return;
|
|
812
876
|
}
|
|
813
877
|
}
|
|
814
|
-
//
|
|
815
|
-
// contract's read-only return is the guard against a caller writing into the memo.
|
|
878
|
+
// Kept unfrozen: a frozen array reads through a slow elements kind, and every read copies it anyway.
|
|
816
879
|
entry.values = candidates.map(stableMemberValue);
|
|
817
880
|
entry.activationVersion = 0;
|
|
818
881
|
}
|
|
@@ -821,14 +884,37 @@ export class DependencyResolver {
|
|
|
821
884
|
if (options !== undefined) {
|
|
822
885
|
const indexed = this.#indexedCandidates(token, options, resolutionStack);
|
|
823
886
|
if (indexed !== null) {
|
|
824
|
-
return indexed;
|
|
887
|
+
return this.#withoutDanglingAliases(indexed, options, resolutionStack);
|
|
825
888
|
}
|
|
826
889
|
}
|
|
827
890
|
const allBindings = this.#allBindingsFromChain(token);
|
|
828
891
|
if (allBindings.length === 0) {
|
|
829
892
|
return allBindings;
|
|
830
893
|
}
|
|
831
|
-
|
|
894
|
+
const selected = selectAllBindings(allBindings, options, this.#makeConstraintContext(resolutionStack, options));
|
|
895
|
+
return this.#withoutDanglingAliases(selected, options, resolutionStack);
|
|
896
|
+
}
|
|
897
|
+
/**
|
|
898
|
+
* The candidates with any alias whose chain ends nowhere dropped, so a fan-out skips a dangling
|
|
899
|
+
* alias rather than throwing on it.
|
|
900
|
+
*
|
|
901
|
+
* @remarks A live alias stays and resolves as before; the common list carries no alias at all and
|
|
902
|
+
* is returned untouched, so nothing is allocated for it.
|
|
903
|
+
*/
|
|
904
|
+
#withoutDanglingAliases(candidates, options, resolutionStack) {
|
|
905
|
+
let hasAlias = false;
|
|
906
|
+
for (let index = 0; index < candidates.length; index += 1) {
|
|
907
|
+
if (candidates[index].kind === "alias") {
|
|
908
|
+
hasAlias = true;
|
|
909
|
+
break;
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
if (!hasAlias) {
|
|
913
|
+
return candidates;
|
|
914
|
+
}
|
|
915
|
+
const singleCriterion = singleCriterionOnlyOf(options);
|
|
916
|
+
return candidates.filter((candidate) => candidate.kind !== "alias" ||
|
|
917
|
+
this.#terminalOfAliasBinding(candidate, options, resolutionStack, singleCriterion) !== undefined);
|
|
832
918
|
}
|
|
833
919
|
// ── Async resolve ──────────────────────────────────────────────────────────────────────────────────────────────────
|
|
834
920
|
resolveAsyncFromContext(token, resolutionStack, branchDepth) {
|
|
@@ -890,11 +976,13 @@ export class DependencyResolver {
|
|
|
890
976
|
return this.#resolveBindingAsync(binding, options, [], ROOT_BRANCH, this);
|
|
891
977
|
}
|
|
892
978
|
async resolveAsync(token, options, resolutionStack, branchDepth = UNOWNED_BRANCH, precomputedCriterion) {
|
|
893
|
-
const
|
|
979
|
+
const path = ownPrefixOf(resolutionStack, branchDepth);
|
|
980
|
+
const depth = path === resolutionStack ? branchDepth : branchDepthOf(path);
|
|
981
|
+
const { binding, owner } = this.#requireBinding(token, options, path, precomputedCriterion);
|
|
894
982
|
if (binding.scope === "singleton" && owner !== this) {
|
|
895
|
-
return owner.#resolveBindingAsync(binding, options,
|
|
983
|
+
return owner.#resolveBindingAsync(binding, options, path, depth, owner);
|
|
896
984
|
}
|
|
897
|
-
return this.#resolveBindingAsync(binding, options,
|
|
985
|
+
return this.#resolveBindingAsync(binding, options, path, depth, owner);
|
|
898
986
|
}
|
|
899
987
|
async #resolveBindingAsync(binding, options, resolutionStack, branchDepth, owner) {
|
|
900
988
|
if (owner.#isPlainConstant(binding)) {
|
|
@@ -935,15 +1023,18 @@ export class DependencyResolver {
|
|
|
935
1023
|
const needsActivation = owner.#activationNeed().needsActivation(binding);
|
|
936
1024
|
if (!needsActivation && scope === "transient" && (binding.kind === "dynamic" || binding.kind === "dynamic-async")) {
|
|
937
1025
|
const resolutionCtx = new AsyncLevelContext(this, levelStack, options);
|
|
938
|
-
if (
|
|
939
|
-
|
|
1026
|
+
if (branchDepth === ROOT_BRANCH) {
|
|
1027
|
+
this.#recentRootLevelContext = resolutionCtx;
|
|
940
1028
|
}
|
|
941
|
-
const dynamicResult = binding
|
|
1029
|
+
const dynamicResult = runFactoryPrefix(binding, resolutionCtx, levelStack);
|
|
942
1030
|
return dynamicResult instanceof Promise ? await dynamicResult : dynamicResult;
|
|
943
1031
|
}
|
|
944
1032
|
const resolutionCtx = needsActivation || requiresResolutionContext(binding)
|
|
945
1033
|
? new AsyncLevelContext(this, levelStack, options)
|
|
946
1034
|
: undefined;
|
|
1035
|
+
if (resolutionCtx !== undefined && branchDepth === ROOT_BRANCH) {
|
|
1036
|
+
this.#recentRootLevelContext = resolutionCtx;
|
|
1037
|
+
}
|
|
947
1038
|
if (scope === "singleton") {
|
|
948
1039
|
// The promise is published before it settles, so concurrent callers dedup onto it.
|
|
949
1040
|
const singletonPromise = this.#instantiateAndActivateAsync(binding, resolutionCtx, levelStack, levelDepth, needsActivation, owner).then((activated) => {
|
|
@@ -980,8 +1071,10 @@ export class DependencyResolver {
|
|
|
980
1071
|
* this binding on every call, forever.
|
|
981
1072
|
*/
|
|
982
1073
|
#mirrorPostConstructFromOwner(binding, owner) {
|
|
983
|
-
if (owner !== this &&
|
|
984
|
-
|
|
1074
|
+
if (owner !== this &&
|
|
1075
|
+
binding.kind === "class" &&
|
|
1076
|
+
this.#introspector().knownPostConstruct(binding.target) === undefined) {
|
|
1077
|
+
this.#introspector().discoverPostConstruct(binding.target);
|
|
985
1078
|
}
|
|
986
1079
|
}
|
|
987
1080
|
async #instantiateAndActivateAsync(binding, ctx, resolutionStack, branchDepth, needsActivation, owner) {
|
|
@@ -996,22 +1089,20 @@ export class DependencyResolver {
|
|
|
996
1089
|
switch (binding.kind) {
|
|
997
1090
|
case "constant":
|
|
998
1091
|
return binding.value;
|
|
999
|
-
case "dynamic":
|
|
1092
|
+
case "dynamic":
|
|
1093
|
+
case "dynamic-async": {
|
|
1000
1094
|
if (ctx === undefined) {
|
|
1001
1095
|
throw new InternalError("dynamic binding requires resolution context");
|
|
1002
1096
|
}
|
|
1003
|
-
const factoryResult = binding
|
|
1097
|
+
const factoryResult = runFactoryPrefix(binding, ctx, resolutionStack);
|
|
1004
1098
|
return factoryResult instanceof Promise ? factoryResult : Promise.resolve(factoryResult);
|
|
1005
1099
|
}
|
|
1006
|
-
case "dynamic-async":
|
|
1007
|
-
if (ctx === undefined) {
|
|
1008
|
-
throw new InternalError("dynamic-async binding requires resolution context");
|
|
1009
|
-
}
|
|
1010
|
-
return binding.factory(ctx);
|
|
1011
1100
|
case "class": {
|
|
1012
1101
|
const deps = await this.#resolveDepsAsync(this.#constructorParams(binding.target), resolutionStack, branchDepth);
|
|
1013
1102
|
// Accessor initializers resolve synchronously, so the branch-owned path serves them directly.
|
|
1014
|
-
return this.#
|
|
1103
|
+
return this.#introspector().instantiate(binding.target, deps, this.#introspector().needsActiveContainer(binding.target)
|
|
1104
|
+
? this.#ambientResolutionFor(resolutionStack)
|
|
1105
|
+
: undefined);
|
|
1015
1106
|
}
|
|
1016
1107
|
case "resolved": {
|
|
1017
1108
|
const deps = await this.#resolveDepsAsync(binding.deps, resolutionStack, branchDepth);
|
|
@@ -1040,7 +1131,7 @@ export class DependencyResolver {
|
|
|
1040
1131
|
for (let index = 0; index < count; index += 1) {
|
|
1041
1132
|
pending[index] = this.#resolveDepAsync(deps[index], resolutionStack, branchDepth);
|
|
1042
1133
|
}
|
|
1043
|
-
return
|
|
1134
|
+
return settleInOrder(pending, identity);
|
|
1044
1135
|
}
|
|
1045
1136
|
#resolveDepAsync(dep, resolutionStack, branchDepth) {
|
|
1046
1137
|
const options = resolveOptionsForSlot(dep);
|
|
@@ -1056,27 +1147,33 @@ export class DependencyResolver {
|
|
|
1056
1147
|
return this.resolveAsync(dep.token, options, resolutionStack, branchDepth, singleCriterionForSlot(dep));
|
|
1057
1148
|
}
|
|
1058
1149
|
async resolveOptionalAsync(token, options, resolutionStack, branchDepth = UNOWNED_BRANCH, precomputedCriterion) {
|
|
1059
|
-
const
|
|
1150
|
+
const path = ownPrefixOf(resolutionStack, branchDepth);
|
|
1151
|
+
const depth = path === resolutionStack ? branchDepth : branchDepthOf(path);
|
|
1152
|
+
const singleCriterion = precomputedCriterion === undefined ? singleCriterionOnlyOf(options) : (precomputedCriterion ?? undefined);
|
|
1153
|
+
let entry = this.#findBinding(token, options, path, singleCriterion);
|
|
1154
|
+
// Same single-evaluation contract as the sync lane: resolve what the probe found.
|
|
1155
|
+
if (entry !== undefined && entry.binding.kind === "alias") {
|
|
1156
|
+
// Follow the alias off the throwing path: a chain that ends nowhere is a miss, not an error.
|
|
1157
|
+
entry = this.#terminalOfAliasBinding(entry.binding, options, path, singleCriterion);
|
|
1158
|
+
}
|
|
1060
1159
|
if (entry === undefined) {
|
|
1061
1160
|
return undefined;
|
|
1062
1161
|
}
|
|
1063
|
-
// Same single-evaluation contract as the sync lane: resolve what the probe found.
|
|
1064
1162
|
const { binding, owner } = entry;
|
|
1065
|
-
if (binding.kind === "alias") {
|
|
1066
|
-
return this.resolveAsync(token, options, resolutionStack, branchDepth);
|
|
1067
|
-
}
|
|
1068
1163
|
if (binding.scope === "singleton" && owner !== this) {
|
|
1069
|
-
return owner.#resolveBindingAsync(binding, options,
|
|
1164
|
+
return owner.#resolveBindingAsync(binding, options, path, depth, owner);
|
|
1070
1165
|
}
|
|
1071
|
-
return this.#resolveBindingAsync(binding, options,
|
|
1166
|
+
return this.#resolveBindingAsync(binding, options, path, depth, owner);
|
|
1072
1167
|
}
|
|
1073
1168
|
async resolveAllAsync(token, options, resolutionStack, branchDepth = UNOWNED_BRANCH) {
|
|
1074
|
-
const
|
|
1169
|
+
const path = ownPrefixOf(resolutionStack, branchDepth);
|
|
1170
|
+
const depth = path === resolutionStack ? branchDepth : branchDepthOf(path);
|
|
1171
|
+
const candidates = this.#candidateBindings(token, options, path);
|
|
1075
1172
|
const pending = new Array(candidates.length);
|
|
1076
1173
|
for (let index = 0; index < candidates.length; index += 1) {
|
|
1077
|
-
pending[index] = this.#resolveCandidateAsync(candidates[index], options,
|
|
1174
|
+
pending[index] = this.#resolveCandidateAsync(candidates[index], options, path, depth);
|
|
1078
1175
|
}
|
|
1079
|
-
return
|
|
1176
|
+
return settleInOrder(pending, identity);
|
|
1080
1177
|
}
|
|
1081
1178
|
// ── Helpers ────────────────────────────────────────────────────────────────────────────────────────────────────────
|
|
1082
1179
|
#allBindingsFromChain(token) {
|
|
@@ -1165,46 +1262,7 @@ export class DependencyResolver {
|
|
|
1165
1262
|
if (options === undefined && resolutionStack.length === 0) {
|
|
1166
1263
|
return ROOT_CONSTRAINT_CONTEXT;
|
|
1167
1264
|
}
|
|
1168
|
-
return
|
|
1169
|
-
}
|
|
1170
|
-
/** Selection for a multi-criterion request, over the union of the two tag indexes. */
|
|
1171
|
-
#selectMultiTagged(token, options, resolutionStack) {
|
|
1172
|
-
const candidates = [];
|
|
1173
|
-
const gathered = [];
|
|
1174
|
-
if (options.name !== undefined) {
|
|
1175
|
-
// Read, not minted: an unminted name has no criterion, so no slot can carry it.
|
|
1176
|
-
this.#gatherTagCandidates(token, slotNameCriterionOf(options.name), candidates, gathered);
|
|
1177
|
-
}
|
|
1178
|
-
this.#gatherTagCandidates(token, options.tag, candidates, gathered);
|
|
1179
|
-
const listed = options.tags;
|
|
1180
|
-
if (listed !== undefined) {
|
|
1181
|
-
for (let index = 0; index < listed.length; index += 1) {
|
|
1182
|
-
this.#gatherTagCandidates(token, listed[index], candidates, gathered);
|
|
1183
|
-
}
|
|
1184
|
-
}
|
|
1185
|
-
if (candidates.length === 0) {
|
|
1186
|
-
return undefined;
|
|
1187
|
-
}
|
|
1188
|
-
return selectBinding(candidates, options, this.#makeConstraintContext(resolutionStack, options), tokenName(token));
|
|
1189
|
-
}
|
|
1190
|
-
/** One request criterion's candidates: its exact single-tag binding, plus its first-tag bucket. */
|
|
1191
|
-
#gatherTagCandidates(token, criterion, out, gathered) {
|
|
1192
|
-
// Distinct criteria never share a binding — a slot lives in exactly one bucket — so deduping
|
|
1193
|
-
// by criterion covers a request repeating one across its spellings, without scanning `out`.
|
|
1194
|
-
if (criterion === undefined || gathered.includes(criterion)) {
|
|
1195
|
-
return;
|
|
1196
|
-
}
|
|
1197
|
-
gathered.push(criterion);
|
|
1198
|
-
const single = this.#registry.getSimpleTagged(token, criterion);
|
|
1199
|
-
if (single !== undefined) {
|
|
1200
|
-
out.push(single);
|
|
1201
|
-
}
|
|
1202
|
-
const bucket = this.#registry.getMultiTagged(token, criterion);
|
|
1203
|
-
if (bucket !== undefined) {
|
|
1204
|
-
for (let index = 0; index < bucket.length; index += 1) {
|
|
1205
|
-
out.push(bucket[index]);
|
|
1206
|
-
}
|
|
1207
|
-
}
|
|
1265
|
+
return new DefaultConstraintContext(resolutionStack, options);
|
|
1208
1266
|
}
|
|
1209
1267
|
/** The predicate half of a match, for a lane whose index has already settled the slot. */
|
|
1210
1268
|
#satisfiesPredicate(binding, options, resolutionStack) {
|
|
@@ -1249,68 +1307,64 @@ export class DependencyResolver {
|
|
|
1249
1307
|
}
|
|
1250
1308
|
// Nothing this level appended is ever removed, so no level observes its own settlement.
|
|
1251
1309
|
const ctx = new AsyncLevelContext(this, levelStack, undefined);
|
|
1310
|
+
if (branchDepth === ROOT_BRANCH) {
|
|
1311
|
+
this.#recentRootLevelContext = ctx;
|
|
1312
|
+
}
|
|
1252
1313
|
try {
|
|
1253
|
-
|
|
1254
|
-
return binding.factory(ctx);
|
|
1255
|
-
}
|
|
1256
|
-
const factoryResult = binding.factory(ctx);
|
|
1314
|
+
const factoryResult = runFactoryPrefix(binding, ctx, levelStack);
|
|
1257
1315
|
return factoryResult instanceof Promise ? factoryResult : Promise.resolve(factoryResult);
|
|
1258
1316
|
}
|
|
1259
1317
|
catch (factoryError) {
|
|
1260
1318
|
return Promise.reject(factoryError);
|
|
1261
1319
|
}
|
|
1262
1320
|
}
|
|
1263
|
-
// ── The
|
|
1321
|
+
// ── The async root ─────────────────────────────────────────────────────────────────────────────────────────────────
|
|
1264
1322
|
/**
|
|
1265
|
-
* Entry for
|
|
1323
|
+
* Entry for an options-less resolve the container starts: the root of a branch of its own.
|
|
1266
1324
|
*
|
|
1267
|
-
* @remarks A
|
|
1268
|
-
*
|
|
1325
|
+
* @remarks A statically-visible transient graph answers from its compiled async plan; everything
|
|
1326
|
+
* else opens a branch over a fresh array, so every level's context keeps its own ancestors for as
|
|
1327
|
+
* long as the factory holds it — across an `await` included.
|
|
1269
1328
|
*/
|
|
1270
|
-
resolveAsyncFromCascade(token) {
|
|
1271
|
-
if (this.#cascadeStack.length === 0) {
|
|
1272
|
-
return this.resolveAsyncFromContext(token, [], ROOT_BRANCH);
|
|
1273
|
-
}
|
|
1274
|
-
return this.#dispatchCascade(token);
|
|
1275
|
-
}
|
|
1276
|
-
/** Entry for a resolve the container starts, which opens the cascade rather than joining one. */
|
|
1277
1329
|
resolveAsyncFromRoot(token) {
|
|
1278
|
-
return this.#dispatchCascade(token);
|
|
1279
|
-
}
|
|
1280
|
-
#dispatchCascade(token) {
|
|
1281
1330
|
const fastBinding = this.#registry.getFastDefault(token);
|
|
1282
1331
|
if (fastBinding !== undefined) {
|
|
1283
|
-
|
|
1284
|
-
fastBinding.scope === "transient" &&
|
|
1285
|
-
!this.#hasAnyActivation(fastBinding)) {
|
|
1286
|
-
return this.#resolveTransientDynamicAsyncCascade(fastBinding);
|
|
1287
|
-
}
|
|
1288
|
-
// A value that already exists answers here: escaping would snapshot the cascade for a resolve
|
|
1289
|
-
// that never looks at a path.
|
|
1290
|
-
if (this.#isPlainConstant(fastBinding)) {
|
|
1291
|
-
return Promise.resolve(fastBinding.value);
|
|
1292
|
-
}
|
|
1293
|
-
if (fastBinding.scope === "singleton" && fastBinding.instance !== NO_INSTANCE) {
|
|
1294
|
-
return Promise.resolve(fastBinding.instance);
|
|
1295
|
-
}
|
|
1296
|
-
const planned = this.#plannedCascadeAnswer(fastBinding);
|
|
1332
|
+
const planned = this.#plannedRootAnswer(fastBinding);
|
|
1297
1333
|
if (planned !== null) {
|
|
1298
1334
|
return planned;
|
|
1299
1335
|
}
|
|
1336
|
+
if (fastBinding.scope === "transient" &&
|
|
1337
|
+
(fastBinding.kind === "dynamic" || fastBinding.kind === "dynamic-async") &&
|
|
1338
|
+
!this.#hasAnyActivation(fastBinding)) {
|
|
1339
|
+
return this.#rootFactoryAnswer(fastBinding);
|
|
1340
|
+
}
|
|
1300
1341
|
}
|
|
1301
|
-
|
|
1302
|
-
// accumulated — so a cycle across the boundary is still on one path.
|
|
1303
|
-
return this.resolveAsyncFromContext(token, [...this.#cascadeStack], UNOWNED_BRANCH);
|
|
1342
|
+
return this.resolveAsyncFromContext(token, [], ROOT_BRANCH);
|
|
1304
1343
|
}
|
|
1305
1344
|
/**
|
|
1306
|
-
* A
|
|
1345
|
+
* A transient factory root's answer: its factory run over the one context the binding keeps.
|
|
1307
1346
|
*
|
|
1308
|
-
* @remarks
|
|
1309
|
-
*
|
|
1347
|
+
* @remarks The root's path is its own frame alone and the request carries no options, so the level's
|
|
1348
|
+
* context is a function of the binding, built on the first resolve and reused by every later one —
|
|
1349
|
+
* a concurrent root reads the same path, and a descendant that outgrows the branch copies its prefix.
|
|
1310
1350
|
*/
|
|
1311
|
-
#
|
|
1312
|
-
|
|
1313
|
-
|
|
1351
|
+
#rootFactoryAnswer(binding) {
|
|
1352
|
+
let ctx = binding.rootContext;
|
|
1353
|
+
if (ctx === undefined) {
|
|
1354
|
+
ctx = new AsyncLevelContext(this, extendResolutionBranch([], ROOT_BRANCH, this.#getResolutionFrame(binding)), undefined);
|
|
1355
|
+
binding.rootContext = ctx;
|
|
1356
|
+
}
|
|
1357
|
+
try {
|
|
1358
|
+
const factoryResult = runFactoryPrefix(binding, ctx, ctx.ownPath);
|
|
1359
|
+
return factoryResult instanceof Promise ? factoryResult : Promise.resolve(factoryResult);
|
|
1360
|
+
}
|
|
1361
|
+
catch (factoryError) {
|
|
1362
|
+
return Promise.reject(factoryError);
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
/** The compiled async plan's answer for a transient class or factory root, or `null` when there is none. */
|
|
1366
|
+
#plannedRootAnswer(fastBinding) {
|
|
1367
|
+
if (fastBinding.scope !== "transient" ||
|
|
1314
1368
|
(fastBinding.kind !== "class" && fastBinding.kind !== "resolved" && fastBinding.kind !== "resolved-async")) {
|
|
1315
1369
|
return null;
|
|
1316
1370
|
}
|
|
@@ -1327,32 +1381,6 @@ export class DependencyResolver {
|
|
|
1327
1381
|
return Promise.reject(planError);
|
|
1328
1382
|
}
|
|
1329
1383
|
}
|
|
1330
|
-
#resolveTransientDynamicAsyncCascade(binding) {
|
|
1331
|
-
const frame = this.#getResolutionFrame(binding);
|
|
1332
|
-
// The request that closes a cycle is made from a factory's synchronous prefix, and synchronous
|
|
1333
|
-
// code does not interleave — so the O(1) flag is exact path membership here, as it is for the
|
|
1334
|
-
// sync lane. It is cleared when the factory returns its promise, not when that promise settles.
|
|
1335
|
-
if (binding.inFlight) {
|
|
1336
|
-
return Promise.reject(new CircularDependencyError(cycleNamesOf(this.#cascadeStack, frame.tokenName)));
|
|
1337
|
-
}
|
|
1338
|
-
const ctx = (this.#cascadeContext ??= new AsyncCascadeContext(this, this.#cascadeStack));
|
|
1339
|
-
binding.inFlight = true;
|
|
1340
|
-
this.#cascadeStack.push(frame);
|
|
1341
|
-
try {
|
|
1342
|
-
if (binding.kind === "dynamic-async") {
|
|
1343
|
-
return binding.factory(ctx);
|
|
1344
|
-
}
|
|
1345
|
-
const factoryResult = binding.factory(ctx);
|
|
1346
|
-
return factoryResult instanceof Promise ? factoryResult : Promise.resolve(factoryResult);
|
|
1347
|
-
}
|
|
1348
|
-
catch (factoryError) {
|
|
1349
|
-
return Promise.reject(factoryError);
|
|
1350
|
-
}
|
|
1351
|
-
finally {
|
|
1352
|
-
this.#cascadeStack.pop();
|
|
1353
|
-
binding.inFlight = false;
|
|
1354
|
-
}
|
|
1355
|
-
}
|
|
1356
1384
|
// A cached candidate answers here rather than re-entering the generic path: `resolveAll` pays
|
|
1357
1385
|
// this per candidate, and a fan-out over cached handlers is the shape that makes it matter.
|
|
1358
1386
|
#resolveCandidateSync(binding, options, resolutionStack) {
|
|
@@ -1426,7 +1454,7 @@ export class DependencyResolver {
|
|
|
1426
1454
|
if (existing !== undefined) {
|
|
1427
1455
|
return existing;
|
|
1428
1456
|
}
|
|
1429
|
-
const frame = buildResolutionFrame(tokenName(binding.token)
|
|
1457
|
+
const frame = buildResolutionFrame(binding, tokenName(binding.token));
|
|
1430
1458
|
binding.frame = frame;
|
|
1431
1459
|
return frame;
|
|
1432
1460
|
}
|
|
@@ -1436,36 +1464,27 @@ export class DependencyResolver {
|
|
|
1436
1464
|
#acquireSyncResolutionContext(resolutionStack, options) {
|
|
1437
1465
|
if (resolutionStack === this.rootStack) {
|
|
1438
1466
|
const depth = resolutionStack.length;
|
|
1439
|
-
const
|
|
1467
|
+
const pool = (this.#syncResolutionContextPool ??= []);
|
|
1468
|
+
const existing = pool[depth];
|
|
1440
1469
|
if (existing !== undefined) {
|
|
1441
1470
|
existing.reset(this, resolutionStack, options);
|
|
1442
1471
|
return existing;
|
|
1443
1472
|
}
|
|
1444
1473
|
const created = new DefaultResolutionContext(this, resolutionStack, options);
|
|
1445
|
-
|
|
1474
|
+
pool[depth] = created;
|
|
1446
1475
|
return created;
|
|
1447
1476
|
}
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
/** The cascade pair pools separately; a throwaway pair (nested resolve, async snapshot) mints per call. */
|
|
1451
|
-
#acquireOffRootSyncContext(resolutionStack, options) {
|
|
1452
|
-
if (resolutionStack !== this.#cascadeStack) {
|
|
1453
|
-
return new DefaultResolutionContext(this, resolutionStack, options);
|
|
1454
|
-
}
|
|
1455
|
-
const depth = resolutionStack.length;
|
|
1456
|
-
const pool = (this.#cascadeContextPool ??= []);
|
|
1457
|
-
const existing = pool[depth];
|
|
1458
|
-
if (existing !== undefined) {
|
|
1459
|
-
existing.reset(this, resolutionStack, options);
|
|
1460
|
-
return existing;
|
|
1461
|
-
}
|
|
1462
|
-
const created = new DefaultResolutionContext(this, resolutionStack, options);
|
|
1463
|
-
pool[depth] = created;
|
|
1464
|
-
return created;
|
|
1477
|
+
// A throwaway pair — a nested resolve, an async level's prefix — mints per call.
|
|
1478
|
+
return new DefaultResolutionContext(this, resolutionStack, options);
|
|
1465
1479
|
}
|
|
1466
1480
|
}
|
|
1467
|
-
/**
|
|
1468
|
-
|
|
1481
|
+
/**
|
|
1482
|
+
* Whether a binding answers a collection read with a fixed value: a hook-free constant, or a
|
|
1483
|
+
* singleton whose instance is already cached.
|
|
1484
|
+
*
|
|
1485
|
+
* @remarks A cached singleton reads like a constant until a registry change evicts it, which also
|
|
1486
|
+
* drops the memo.
|
|
1487
|
+
*/
|
|
1469
1488
|
function isStableCollectionMember(binding) {
|
|
1470
1489
|
if (binding.kind === "alias" || binding.activationHook !== undefined) {
|
|
1471
1490
|
return false;
|
|
@@ -1483,27 +1502,51 @@ function anyPredicate(bindings) {
|
|
|
1483
1502
|
}
|
|
1484
1503
|
return false;
|
|
1485
1504
|
}
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1505
|
+
/**
|
|
1506
|
+
* Runs an async lane's factory with its binding flagged in flight for the factory's synchronous prefix.
|
|
1507
|
+
*
|
|
1508
|
+
* @remarks The flag is exact path membership only while synchronous code runs, so it is cleared when
|
|
1509
|
+
* the factory returns — its promise included — never when that promise settles: two branches that
|
|
1510
|
+
* await one binding are a diamond, not a cycle. A factory that resolves its own token from that
|
|
1511
|
+
* prefix is caught before it runs again, on the branch lane as on the sync lanes, where the level's
|
|
1512
|
+
* own flag already covers the whole call.
|
|
1513
|
+
*/
|
|
1514
|
+
function runFactoryPrefix(binding, ctx, levelStack) {
|
|
1515
|
+
if (binding.inFlight) {
|
|
1516
|
+
// The level's own frame is the last one, and the path an error names ends where the cycle closed.
|
|
1517
|
+
throw new CircularDependencyError(cycleNamesOf(levelStack.slice(0, -1), tokenName(binding.token)));
|
|
1518
|
+
}
|
|
1519
|
+
binding.inFlight = true;
|
|
1520
|
+
try {
|
|
1521
|
+
return binding.factory(ctx);
|
|
1522
|
+
}
|
|
1523
|
+
finally {
|
|
1524
|
+
binding.inFlight = false;
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
/**
|
|
1528
|
+
* The frames a level may read as its own path: the array itself while nothing has grown it past the
|
|
1529
|
+
* level's depth, else a copy of the level's prefix.
|
|
1530
|
+
*
|
|
1531
|
+
* @remarks Siblings start concurrently on one branch and the first appends in place, so a later
|
|
1532
|
+
* sibling that read the whole array would hand a `when()` predicate the first sibling's frame as its
|
|
1533
|
+
* parent. The copy is the one the branch lane would take for that sibling anyway, and it owns itself.
|
|
1534
|
+
*/
|
|
1535
|
+
function ownPrefixOf(resolutionStack, branchDepth) {
|
|
1536
|
+
if (branchDepth === UNOWNED_BRANCH || resolutionStack.length === branchDepth) {
|
|
1537
|
+
return resolutionStack;
|
|
1538
|
+
}
|
|
1539
|
+
return resolutionStack.slice(0, branchDepth);
|
|
1501
1540
|
}
|
|
1502
1541
|
/** The async-resolution failure for a binding reached on a sync path, naming what to await instead. */
|
|
1503
1542
|
function asyncResolutionErrorFor(binding, resolutionStack) {
|
|
1504
1543
|
const sourceName = tokenName(binding.token);
|
|
1505
1544
|
return new AsyncResolutionError(resolutionStack[0]?.tokenName ?? sourceName, sourceName);
|
|
1506
1545
|
}
|
|
1546
|
+
/** The settled values of a fan-out, as they are. */
|
|
1547
|
+
function identity(values) {
|
|
1548
|
+
return values;
|
|
1549
|
+
}
|
|
1507
1550
|
/** Only a factory is handed the resolution context; everything else gets its deps directly. */
|
|
1508
1551
|
function requiresResolutionContext(binding) {
|
|
1509
1552
|
return binding.kind === "dynamic" || binding.kind === "dynamic-async";
|