@codefast/di 0.9.0 → 0.10.1
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 +165 -0
- package/README.md +6 -5
- package/dist/ambient/active-container.d.ts +14 -4
- package/dist/ambient/active-container.js +22 -1
- package/dist/container/binding-builders.d.ts +35 -13
- package/dist/container/binding-builders.js +174 -97
- package/dist/container/container.d.ts +10 -10
- package/dist/container/container.js +53 -36
- package/dist/core/binding-scope.d.ts +2 -2
- package/dist/core/binding.d.ts +42 -49
- package/dist/core/binding.js +12 -38
- package/dist/core/constraint-requirement.d.ts +1 -1
- package/dist/core/module.d.ts +3 -3
- package/dist/core/registry.d.ts +52 -9
- package/dist/core/registry.js +376 -162
- package/dist/core/state-epoch.d.ts +16 -0
- package/dist/core/state-epoch.js +21 -0
- package/dist/core/token.d.ts +1 -1
- package/dist/core/types.d.ts +12 -9
- package/dist/decorators/inject.d.ts +3 -3
- package/dist/decorators/inject.js +5 -5
- package/dist/decorators/injectable.d.ts +2 -2
- package/dist/decorators/injectable.js +2 -2
- package/dist/decorators/lifecycle-decorators.js +2 -2
- package/dist/errors/diagnostics.d.ts +2 -0
- package/dist/errors/errors.d.ts +29 -3
- package/dist/errors/errors.js +34 -2
- package/dist/index.d.ts +35 -35
- package/dist/index.js +19 -19
- package/dist/injection/descriptor.d.ts +9 -7
- package/dist/injection/descriptor.js +3 -1
- package/dist/injection/resolve-options.d.ts +9 -3
- package/dist/injection/resolve-options.js +17 -1
- package/dist/introspection/dependency-graph.d.ts +3 -3
- package/dist/introspection/dependency-graph.js +15 -10
- 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/inspector.d.ts +7 -5
- package/dist/introspection/inspector.js +13 -27
- package/dist/lifecycle/lifecycle-manager.d.ts +4 -4
- package/dist/lifecycle/lifecycle-manager.js +13 -11
- package/dist/lifecycle/scope-manager.d.ts +2 -2
- package/dist/lifecycle/scope-manager.js +4 -4
- 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 +1 -1
- package/dist/metadata/verifying-metadata-reader.js +2 -2
- package/dist/resolution/cache/activation-need.d.ts +6 -4
- package/dist/resolution/cache/activation-need.js +13 -6
- package/dist/resolution/cache/binding-lookup-cache.d.ts +39 -6
- package/dist/resolution/cache/binding-lookup-cache.js +97 -13
- package/dist/resolution/cache/class-introspector.d.ts +6 -6
- package/dist/resolution/cache/class-introspector.js +82 -69
- package/dist/resolution/context.d.ts +11 -11
- package/dist/resolution/context.js +1 -1
- package/dist/resolution/path/resolution-path.d.ts +1 -1
- package/dist/resolution/path/resolution-path.js +1 -1
- package/dist/resolution/plan/instantiation-plan.d.ts +16 -4
- package/dist/resolution/plan/instantiation-plan.js +160 -69
- package/dist/resolution/plan/plan-codegen.d.ts +100 -0
- package/dist/resolution/plan/plan-codegen.js +185 -0
- package/dist/resolution/resolver.d.ts +26 -16
- package/dist/resolution/resolver.js +401 -147
- package/dist/resolution/select/binding-select.d.ts +6 -5
- package/dist/resolution/select/binding-select.js +17 -11
- package/dist/resolution/select/constraints.d.ts +3 -3
- package/dist/resolution/select/constraints.js +4 -4
- package/package.json +11 -3
|
@@ -1,21 +1,33 @@
|
|
|
1
|
-
import { NO_INSTANCE } from "
|
|
2
|
-
import { NO_TAG_KEYS, slotNameCriterionOf } from "
|
|
3
|
-
import { tokenName } from "
|
|
4
|
-
import { AsyncActivationError, AsyncResolutionError, CircularDependencyError, DisposedContainerError, InternalError, MissingMetadataError, MissingScopeContextError, NoMatchingBindingError, TokenNotBoundError, } from "
|
|
5
|
-
import { resolveOptionsForSlot, singleCriterionForSlot, singleCriterionOnlyOf } from "
|
|
6
|
-
import { SCOPED_MISS } from "
|
|
7
|
-
import { ActivationNeedCache } from "
|
|
8
|
-
import { BindingLookupCache } from "
|
|
9
|
-
import { ClassIntrospector } from "
|
|
10
|
-
import { AsyncCascadeContext, AsyncLevelContext, buildResolutionFrame, DefaultResolutionContext, } from "
|
|
11
|
-
import { branchDepthOf, cycleNamesOf, enterResolutionPath, extendResolutionBranch, ROOT_BRANCH, UNOWNED_BRANCH, } from "
|
|
12
|
-
import { InstantiationPlanCompiler, PLAN_RETRY } from "
|
|
13
|
-
import { matchesSlot, requestedTagKeyMask, selectAllBindings, selectBinding } from "
|
|
1
|
+
import { NO_INSTANCE } from "#core/binding";
|
|
2
|
+
import { NO_TAG_KEYS, 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 { ActivationNeedCache } from "#resolution/cache/activation-need";
|
|
8
|
+
import { BindingLookupCache } from "#resolution/cache/binding-lookup-cache";
|
|
9
|
+
import { ClassIntrospector } from "#resolution/cache/class-introspector";
|
|
10
|
+
import { AsyncCascadeContext, AsyncLevelContext, buildResolutionFrame, DefaultResolutionContext, } from "#resolution/context";
|
|
11
|
+
import { branchDepthOf, cycleNamesOf, enterResolutionPath, extendResolutionBranch, ROOT_BRANCH, UNOWNED_BRANCH, } from "#resolution/path/resolution-path";
|
|
12
|
+
import { InstantiationPlanCompiler, PLAN_RETRY } from "#resolution/plan/instantiation-plan";
|
|
13
|
+
import { matchesSlot, requestedTagKeyMask, selectAllBindings, selectBinding } from "#resolution/select/binding-select";
|
|
14
14
|
// Where a multi-tag resolve switches from scanning the token's list to walking the tag indexes.
|
|
15
15
|
const MULTI_TAG_INDEX_THRESHOLD = 8;
|
|
16
16
|
const EMPTY_STRING_LIST = [];
|
|
17
17
|
const EMPTY_FRAME_LIST = [];
|
|
18
18
|
const EMPTY_PARAM_LIST = [];
|
|
19
|
+
/** Plans compiled so far, with the `null` unplannable marks left out. */
|
|
20
|
+
function countCompiledPlans(plans) {
|
|
21
|
+
let count = 0;
|
|
22
|
+
if (plans !== undefined) {
|
|
23
|
+
for (const plan of plans.values()) {
|
|
24
|
+
if (plan !== null) {
|
|
25
|
+
count += 1;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return count;
|
|
30
|
+
}
|
|
19
31
|
const ROOT_CONSTRAINT_CONTEXT = {
|
|
20
32
|
resolutionPath: EMPTY_STRING_LIST,
|
|
21
33
|
resolutionStack: EMPTY_FRAME_LIST,
|
|
@@ -45,12 +57,13 @@ export class DependencyResolver {
|
|
|
45
57
|
// because synchronous code does not interleave.
|
|
46
58
|
#cascadeStack = [];
|
|
47
59
|
#cascadeContext;
|
|
48
|
-
// Compiled plans; `null` marks a binding as unplannable under the current cache versions.
|
|
49
|
-
|
|
60
|
+
// 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.
|
|
62
|
+
#classPlanByBindingId;
|
|
50
63
|
#classPlanRegistryVersion = -1;
|
|
51
64
|
#classPlanActivationVersion = -1;
|
|
52
65
|
// The async lane's plans, stamped and invalidated apart so neither lane pays the other's misses.
|
|
53
|
-
#asyncPlanByBindingId
|
|
66
|
+
#asyncPlanByBindingId;
|
|
54
67
|
#asyncPlanRegistryVersion = -1;
|
|
55
68
|
#asyncPlanActivationVersion = -1;
|
|
56
69
|
#registry;
|
|
@@ -60,6 +73,7 @@ export class DependencyResolver {
|
|
|
60
73
|
#parent;
|
|
61
74
|
#lookup;
|
|
62
75
|
#classes;
|
|
76
|
+
// Built by the first interpreted resolve that asks; a plan-served or constant-only container never does.
|
|
63
77
|
#activation;
|
|
64
78
|
constructor(registry, scope, lifecycle, metadataReader, container, parent) {
|
|
65
79
|
this.#registry = registry;
|
|
@@ -68,31 +82,37 @@ export class DependencyResolver {
|
|
|
68
82
|
this.#metadataReader = metadataReader;
|
|
69
83
|
this.#parent = parent;
|
|
70
84
|
this.#lookup = new BindingLookupCache(registry, this, parent === undefined ? undefined : parent.#lookup);
|
|
71
|
-
this.#classes = new ClassIntrospector(metadataReader, container);
|
|
72
|
-
this.#activation = new ActivationNeedCache(lifecycle, this.#classes, registry);
|
|
85
|
+
this.#classes = new ClassIntrospector(metadataReader, container, parent === undefined ? undefined : parent.#classes);
|
|
73
86
|
}
|
|
74
87
|
/** The reader this resolver was built with, which is the one its container answers with. */
|
|
75
88
|
get metadataReader() {
|
|
76
89
|
return this.#metadataReader;
|
|
77
90
|
}
|
|
78
|
-
|
|
91
|
+
#activationNeed() {
|
|
92
|
+
return (this.#activation ??= new ActivationNeedCache(this.#lifecycle, this.#classes, this.#registry));
|
|
93
|
+
}
|
|
94
|
+
/** Structural counts and the resolver-owned collaborators built so far, for the {@link ResolutionDiagnostics} a container reports. */
|
|
79
95
|
describeCaches() {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
compiledPlanCount += 1;
|
|
84
|
-
}
|
|
96
|
+
const builtSubsystems = [];
|
|
97
|
+
if (this.#planCompiler !== undefined) {
|
|
98
|
+
builtSubsystems.push("resolver.planCompiler");
|
|
85
99
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
100
|
+
if (this.#lookup.isMemoBuilt) {
|
|
101
|
+
builtSubsystems.push("resolver.lookupMemo");
|
|
102
|
+
}
|
|
103
|
+
if (this.#activation?.isMemoBuilt === true) {
|
|
104
|
+
builtSubsystems.push("resolver.activationNeedMemo");
|
|
105
|
+
}
|
|
106
|
+
const generatedPlanCount = this.#planCompiler?.generatedPlanCount ?? 0;
|
|
107
|
+
if (generatedPlanCount > 0) {
|
|
108
|
+
builtSubsystems.push("resolver.planCodegen");
|
|
91
109
|
}
|
|
92
110
|
return {
|
|
93
|
-
compiledPlanCount,
|
|
94
|
-
compiledAsyncPlanCount,
|
|
111
|
+
compiledPlanCount: countCompiledPlans(this.#classPlanByBindingId),
|
|
112
|
+
compiledAsyncPlanCount: countCompiledPlans(this.#asyncPlanByBindingId),
|
|
113
|
+
generatedPlanCount,
|
|
95
114
|
syncContextPoolSize: this.#syncResolutionContextPool.length,
|
|
115
|
+
builtSubsystems,
|
|
96
116
|
};
|
|
97
117
|
}
|
|
98
118
|
// ── Binding lookup ─────────────────────────────────────────────────────────────────────────────────────────────────
|
|
@@ -111,37 +131,67 @@ export class DependencyResolver {
|
|
|
111
131
|
}
|
|
112
132
|
else if (singleCriterion !== undefined) {
|
|
113
133
|
const indexed = this.#registry.getSimpleTagged(token, singleCriterion);
|
|
114
|
-
if (indexed
|
|
134
|
+
if (indexed === undefined) {
|
|
135
|
+
// A one-criterion request matches only a slot carrying exactly that criterion, and every such
|
|
136
|
+
// slot is in the index, so a miss here is a miss for this registry: nothing left to scan.
|
|
137
|
+
return this.#parent === undefined
|
|
138
|
+
? undefined
|
|
139
|
+
: this.#parent.#findBinding(token, options, resolutionStack, singleCriterion);
|
|
140
|
+
}
|
|
141
|
+
if (this.#satisfiesPredicate(indexed, options, resolutionStack)) {
|
|
115
142
|
return { binding: indexed, owner: this };
|
|
116
143
|
}
|
|
117
144
|
}
|
|
118
|
-
else
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
//
|
|
137
|
-
//
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
+
else {
|
|
146
|
+
if (options.name !== undefined) {
|
|
147
|
+
const pairTag = loneTagBesideNameOf(options);
|
|
148
|
+
if (pairTag !== undefined) {
|
|
149
|
+
const nameCriterion = slotNameCriterionOf(options.name);
|
|
150
|
+
if (nameCriterion === undefined) {
|
|
151
|
+
// No binding anywhere has declared this name, so no slot in any registry can carry it.
|
|
152
|
+
return undefined;
|
|
153
|
+
}
|
|
154
|
+
// The exact two-criterion slot, memoized over the chain; a predicate or an alias declines to the scan.
|
|
155
|
+
const entry = this.#lookup.namedTaggedEntry(token, nameCriterion, pairTag);
|
|
156
|
+
if (entry !== null) {
|
|
157
|
+
return entry;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
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
|
+
}
|
|
178
|
+
// A lone default-slot candidate is its own selection: the slot match is the whole decision,
|
|
179
|
+
// it carries no predicate, and asking for it first keeps the registry from materialising its list.
|
|
180
|
+
// An options-less request already probed the lone map above, so only a request with options asks again.
|
|
181
|
+
const lone = options === undefined ? undefined : this.#registry.getFastDefault(token);
|
|
182
|
+
if (lone !== undefined) {
|
|
183
|
+
if (matchesSlot(lone.slot, options)) {
|
|
184
|
+
return { binding: lone, owner: this };
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
// The lone map has missed either way, so the record map is all that is left to read.
|
|
189
|
+
const bindings = this.#registry.getRecorded(token);
|
|
190
|
+
if (bindings.length > 0) {
|
|
191
|
+
const selected = this.#selectFromList(bindings, options, resolutionStack, token);
|
|
192
|
+
if (selected !== undefined) {
|
|
193
|
+
return { binding: selected, owner: this };
|
|
194
|
+
}
|
|
145
195
|
}
|
|
146
196
|
}
|
|
147
197
|
if (this.#parent !== undefined) {
|
|
@@ -149,6 +199,31 @@ export class DependencyResolver {
|
|
|
149
199
|
}
|
|
150
200
|
return undefined;
|
|
151
201
|
}
|
|
202
|
+
/**
|
|
203
|
+
* The scan a candidate list gets before full selection.
|
|
204
|
+
*
|
|
205
|
+
* @remarks A single slot match is the whole answer once its predicate, if any, agrees, and no
|
|
206
|
+
* match is a clean miss; neither needs a display name or a candidate array. Two slot matches
|
|
207
|
+
* hand the same list to full selection, which weighs specificity and reports ambiguity — so both
|
|
208
|
+
* lanes answer identically.
|
|
209
|
+
*/
|
|
210
|
+
#selectFromList(bindings, options, resolutionStack, token) {
|
|
211
|
+
let match;
|
|
212
|
+
for (let index = 0; index < bindings.length; index += 1) {
|
|
213
|
+
const candidate = bindings[index];
|
|
214
|
+
if (candidate.isMany || !matchesSlot(candidate.slot, options)) {
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
if (match !== undefined) {
|
|
218
|
+
return selectBinding(bindings, options, this.#makeConstraintContext(resolutionStack, options), tokenName(token));
|
|
219
|
+
}
|
|
220
|
+
match = candidate;
|
|
221
|
+
}
|
|
222
|
+
if (match === undefined) {
|
|
223
|
+
return undefined;
|
|
224
|
+
}
|
|
225
|
+
return this.#satisfiesPredicate(match, options, resolutionStack) ? match : undefined;
|
|
226
|
+
}
|
|
152
227
|
/**
|
|
153
228
|
* The binding a token resolves to with alias hops followed, or a diagnostic throw.
|
|
154
229
|
*
|
|
@@ -214,7 +289,7 @@ export class DependencyResolver {
|
|
|
214
289
|
// Container-level hooks belong to the binding's owner — a child-registered hook must not
|
|
215
290
|
// fire for a parent-owned binding, and the owner's must.
|
|
216
291
|
const containerHooks = owner.#lifecycle.activationVersion === 0 ? undefined : owner.#lifecycle.activationHandlersFor(binding.token);
|
|
217
|
-
if (binding.
|
|
292
|
+
if (binding.activationHook === undefined && (containerHooks === undefined || containerHooks.length === 0)) {
|
|
218
293
|
return this.#resolveTransientDynamicSyncFromContext(binding, resolutionStack);
|
|
219
294
|
}
|
|
220
295
|
return this.#resolveTransientDynamicActivatedSync(binding, containerHooks, resolutionStack);
|
|
@@ -268,8 +343,8 @@ export class DependencyResolver {
|
|
|
268
343
|
throw new AsyncResolutionError(resolutionStack[0]?.tokenName ?? tokenDisplayName, tokenDisplayName);
|
|
269
344
|
}
|
|
270
345
|
let activated = factoryResult;
|
|
271
|
-
if (binding.
|
|
272
|
-
const activationResult = binding.
|
|
346
|
+
if (binding.activationHook !== undefined) {
|
|
347
|
+
const activationResult = binding.activationHook(resolutionCtx, activated);
|
|
273
348
|
if (activationResult instanceof Promise) {
|
|
274
349
|
throw new AsyncActivationError(tokenDisplayName, "onActivation");
|
|
275
350
|
}
|
|
@@ -291,8 +366,14 @@ export class DependencyResolver {
|
|
|
291
366
|
binding.inFlight = false;
|
|
292
367
|
}
|
|
293
368
|
}
|
|
294
|
-
/**
|
|
369
|
+
/**
|
|
370
|
+
* Chain-summed activation version: a plan can inline a parent-owned binding, so a parent's hook
|
|
371
|
+
* registration must invalidate it.
|
|
372
|
+
*/
|
|
295
373
|
#chainActivationVersion() {
|
|
374
|
+
if (this.#parent === undefined) {
|
|
375
|
+
return this.#lifecycle.activationVersion;
|
|
376
|
+
}
|
|
296
377
|
let version = this.#lifecycle.activationVersion;
|
|
297
378
|
for (let current = this.#parent; current !== undefined; current = current.#parent) {
|
|
298
379
|
version += current.#lifecycle.activationVersion;
|
|
@@ -303,93 +384,142 @@ export class DependencyResolver {
|
|
|
303
384
|
const registryVersion = this.#lookup.chainVersion();
|
|
304
385
|
const activationVersion = this.#chainActivationVersion();
|
|
305
386
|
if (registryVersion !== this.#classPlanRegistryVersion || activationVersion !== this.#classPlanActivationVersion) {
|
|
306
|
-
|
|
387
|
+
// The stamps start at -1, so the first request always lands here: allocate then, clear after.
|
|
388
|
+
if (this.#classPlanByBindingId === undefined) {
|
|
389
|
+
this.#classPlanByBindingId = new Map();
|
|
390
|
+
}
|
|
391
|
+
else {
|
|
392
|
+
this.#classPlanByBindingId.clear();
|
|
393
|
+
}
|
|
307
394
|
this.#classPlanRegistryVersion = registryVersion;
|
|
308
395
|
this.#classPlanActivationVersion = activationVersion;
|
|
309
396
|
}
|
|
310
|
-
const
|
|
397
|
+
const plans = this.#classPlanByBindingId;
|
|
398
|
+
const cached = plans.get(binding.identifier);
|
|
311
399
|
if (cached !== undefined) {
|
|
312
400
|
return cached;
|
|
313
401
|
}
|
|
314
|
-
const compiled = this.#
|
|
402
|
+
const compiled = this.#compiler().compile(binding);
|
|
315
403
|
if (compiled === PLAN_RETRY) {
|
|
316
404
|
// Lifecycle metadata not discovered yet — the fallback resolve discovers it; retry then.
|
|
317
405
|
return null;
|
|
318
406
|
}
|
|
319
|
-
|
|
407
|
+
plans.set(binding.identifier, compiled);
|
|
320
408
|
return compiled;
|
|
321
409
|
}
|
|
322
|
-
// Compiler behind #getInstantiationPlan — cold path,
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
410
|
+
// Compiler behind #getInstantiationPlan — a cold path, and built on the first plan so a container
|
|
411
|
+
// that never resolves a class or a resolved factory never pays for the host.
|
|
412
|
+
#planCompiler;
|
|
413
|
+
#compiler() {
|
|
414
|
+
return (this.#planCompiler ??= new InstantiationPlanCompiler(this.#buildPlanCompilerHost()));
|
|
415
|
+
}
|
|
416
|
+
// The behaviour the plan compiler needs from this resolver — lookups, escapes, plan swaps, and the
|
|
417
|
+
// accessor construction path. Built once with the compiler, so each closure is allocated once.
|
|
418
|
+
#buildPlanCompilerHost() {
|
|
419
|
+
return {
|
|
420
|
+
hasActivationHandlers: (binding) => this.#ownerOf(binding).#lifecycle.hasActivationHandlers(binding.token),
|
|
421
|
+
knownPostConstruct: (target) => this.#classes.knownPostConstruct(target),
|
|
422
|
+
needsActiveContainer: (target) => this.#classes.needsActiveContainer(target),
|
|
423
|
+
// A plan runs at the top level, so the lent root stack is free when it is; a plan reached
|
|
424
|
+
// with the root stack held mints its own path, exactly as the interpreted lane would.
|
|
425
|
+
constructWithAccessors: (binding, target, deps) => {
|
|
426
|
+
const stack = this.rootStack.length === 0 ? this.rootStack : [];
|
|
427
|
+
const frame = this.#getResolutionFrame(binding);
|
|
428
|
+
const resolutionSet = enterResolutionPath(stack, frame);
|
|
429
|
+
try {
|
|
430
|
+
return this.#classes.instantiate(target, deps, this.#ambientResolutionFor(stack));
|
|
431
|
+
}
|
|
432
|
+
finally {
|
|
433
|
+
stack.pop();
|
|
434
|
+
resolutionSet?.delete(frame.bindingId);
|
|
435
|
+
}
|
|
436
|
+
},
|
|
437
|
+
getConstructorMetadata: (target) => this.#classes.constructorMetadata(target),
|
|
438
|
+
lookupDependencyEntry: (token) => {
|
|
439
|
+
const entry = this.#lookup.defaultEntry(token);
|
|
440
|
+
return entry === null ? null : { binding: entry.binding };
|
|
441
|
+
},
|
|
442
|
+
// Exactly what #findBinding's single-criterion lane accepts, minus the half that reads a path:
|
|
443
|
+
// a predicate is the compiler's cue to leave the selection to the runtime.
|
|
444
|
+
lookupPathIndependentEntry: (token, options) => {
|
|
445
|
+
const singleCriterion = singleCriterionOnlyOf(options);
|
|
446
|
+
if (singleCriterion === undefined) {
|
|
447
|
+
return null;
|
|
448
|
+
}
|
|
449
|
+
const entry = this.#lookup.taggedEntry(token, singleCriterion);
|
|
450
|
+
if (entry === null || entry.binding.predicate !== undefined || !matchesSlot(entry.binding.slot, options)) {
|
|
451
|
+
return null;
|
|
452
|
+
}
|
|
453
|
+
return { binding: entry.binding };
|
|
454
|
+
},
|
|
455
|
+
getResolutionFrame: (binding) => this.#getResolutionFrame(binding),
|
|
456
|
+
// Identity-guarded: a map cleared and recompiled since no longer holds the plan being replaced.
|
|
457
|
+
replacePlan: (binding, current, next) => {
|
|
458
|
+
const plans = this.#classPlanByBindingId;
|
|
459
|
+
if (plans !== undefined && plans.get(binding.identifier) === current) {
|
|
460
|
+
plans.set(binding.identifier, next);
|
|
461
|
+
}
|
|
462
|
+
},
|
|
463
|
+
replaceAsyncPlan: (binding, current, next) => {
|
|
464
|
+
const plans = this.#asyncPlanByBindingId;
|
|
465
|
+
if (plans !== undefined && plans.get(binding.identifier) === current) {
|
|
466
|
+
plans.set(binding.identifier, next);
|
|
467
|
+
}
|
|
468
|
+
},
|
|
469
|
+
// Dispatches exactly as #resolveDep does, so an escaped dep is indistinguishable
|
|
470
|
+
// from the same dep on a fully interpreted resolve.
|
|
471
|
+
resolveEscaped: (token, options, arity, resolutionStack) => {
|
|
472
|
+
if (arity === "all") {
|
|
473
|
+
return this.resolveAll(token, options, resolutionStack);
|
|
474
|
+
}
|
|
475
|
+
if (arity === "optional") {
|
|
476
|
+
return this.resolveOptional(token, options, resolutionStack);
|
|
477
|
+
}
|
|
478
|
+
if (options === undefined) {
|
|
479
|
+
return this.resolveFromContext(token, resolutionStack);
|
|
480
|
+
}
|
|
481
|
+
return this.resolve(token, options, resolutionStack);
|
|
482
|
+
},
|
|
483
|
+
// Dispatches exactly as #resolveDepAsync does, for the async lane's escapes.
|
|
484
|
+
resolveEscapedAsync: (token, options, arity, resolutionStack) => {
|
|
485
|
+
if (arity === "all") {
|
|
486
|
+
return this.resolveAllAsync(token, options, resolutionStack, UNOWNED_BRANCH);
|
|
487
|
+
}
|
|
488
|
+
if (arity === "optional") {
|
|
489
|
+
return this.resolveOptionalAsync(token, options, resolutionStack, UNOWNED_BRANCH);
|
|
490
|
+
}
|
|
491
|
+
if (options === undefined) {
|
|
492
|
+
return this.resolveAsyncFromContext(token, resolutionStack, UNOWNED_BRANCH);
|
|
493
|
+
}
|
|
494
|
+
return this.resolveAsync(token, options, resolutionStack, UNOWNED_BRANCH);
|
|
495
|
+
},
|
|
496
|
+
};
|
|
497
|
+
}
|
|
374
498
|
/** The async lane's plan for a statically-visible transient binding, mirroring the sync getter. */
|
|
375
499
|
#getAsyncInstantiationPlan(binding) {
|
|
376
500
|
const registryVersion = this.#lookup.chainVersion();
|
|
377
501
|
const activationVersion = this.#chainActivationVersion();
|
|
378
502
|
if (registryVersion !== this.#asyncPlanRegistryVersion || activationVersion !== this.#asyncPlanActivationVersion) {
|
|
379
|
-
this.#asyncPlanByBindingId
|
|
503
|
+
if (this.#asyncPlanByBindingId === undefined) {
|
|
504
|
+
this.#asyncPlanByBindingId = new Map();
|
|
505
|
+
}
|
|
506
|
+
else {
|
|
507
|
+
this.#asyncPlanByBindingId.clear();
|
|
508
|
+
}
|
|
380
509
|
this.#asyncPlanRegistryVersion = registryVersion;
|
|
381
510
|
this.#asyncPlanActivationVersion = activationVersion;
|
|
382
511
|
}
|
|
383
|
-
const
|
|
512
|
+
const plans = this.#asyncPlanByBindingId;
|
|
513
|
+
const cached = plans.get(binding.identifier);
|
|
384
514
|
if (cached !== undefined) {
|
|
385
515
|
return cached;
|
|
386
516
|
}
|
|
387
|
-
const compiled = this.#
|
|
517
|
+
const compiled = this.#compiler().compileAsync(binding);
|
|
388
518
|
if (compiled === PLAN_RETRY) {
|
|
389
519
|
// Lifecycle metadata not discovered yet — the fallback resolve discovers it; retry then.
|
|
390
520
|
return null;
|
|
391
521
|
}
|
|
392
|
-
|
|
522
|
+
plans.set(binding.identifier, compiled);
|
|
393
523
|
return compiled;
|
|
394
524
|
}
|
|
395
525
|
resolve(token, options, resolutionStack, precomputedCriterion) {
|
|
@@ -430,7 +560,7 @@ export class DependencyResolver {
|
|
|
430
560
|
return binding.instance;
|
|
431
561
|
}
|
|
432
562
|
// An async materialization already in flight must not be raced by a second, sync one.
|
|
433
|
-
if (this.#scope.getInflight(binding.
|
|
563
|
+
if (this.#scope.getInflight(binding.identifier) !== undefined) {
|
|
434
564
|
throw new AsyncResolutionError(resolutionStack[0]?.tokenName ?? tokenName(binding.token), tokenName(binding.token));
|
|
435
565
|
}
|
|
436
566
|
if (this.#scope.isClosed) {
|
|
@@ -442,7 +572,7 @@ export class DependencyResolver {
|
|
|
442
572
|
if (cachedScoped !== SCOPED_MISS) {
|
|
443
573
|
return cachedScoped;
|
|
444
574
|
}
|
|
445
|
-
if (this.#scope.getInflight(binding.
|
|
575
|
+
if (this.#scope.getInflight(binding.identifier) !== undefined) {
|
|
446
576
|
throw new AsyncResolutionError(resolutionStack[0]?.tokenName ?? tokenName(binding.token), tokenName(binding.token));
|
|
447
577
|
}
|
|
448
578
|
if (this.#scope.isClosed) {
|
|
@@ -453,7 +583,7 @@ export class DependencyResolver {
|
|
|
453
583
|
const tokenDisplayName = frame.tokenName;
|
|
454
584
|
const resolutionSet = enterResolutionPath(resolutionStack, frame);
|
|
455
585
|
try {
|
|
456
|
-
const needsActivation = owner.#
|
|
586
|
+
const needsActivation = owner.#activationNeed().needsActivation(binding);
|
|
457
587
|
if (!needsActivation && scope === "transient" && binding.kind === "dynamic") {
|
|
458
588
|
const resolutionCtx = this.#acquireSyncResolutionContext(resolutionStack, options);
|
|
459
589
|
const dynamicResult = binding.factory(resolutionCtx);
|
|
@@ -467,7 +597,7 @@ export class DependencyResolver {
|
|
|
467
597
|
: undefined;
|
|
468
598
|
const instance = this.#instantiateSync(binding, resolutionCtx, resolutionStack);
|
|
469
599
|
this.#mirrorPostConstructFromOwner(binding, owner);
|
|
470
|
-
const activated = owner.#
|
|
600
|
+
const activated = owner.#activationNeed().refreshAfterFirstInstantiation(binding, needsActivation)
|
|
471
601
|
? owner.#lifecycle.runActivationSync(resolutionCtx, binding, instance, owner.#metadataReader)
|
|
472
602
|
: instance;
|
|
473
603
|
if (scope === "singleton") {
|
|
@@ -483,8 +613,20 @@ export class DependencyResolver {
|
|
|
483
613
|
resolutionSet?.delete(frame.bindingId);
|
|
484
614
|
}
|
|
485
615
|
}
|
|
486
|
-
/**
|
|
616
|
+
/**
|
|
617
|
+
* Path-continuing resolution handed to the ambient slot while an accessor class constructs.
|
|
618
|
+
*
|
|
619
|
+
* @remarks The lent root stack is one array for the resolver's lifetime, so the closure pair over
|
|
620
|
+
* it is built once and reused.
|
|
621
|
+
*/
|
|
622
|
+
#rootAmbientResolution;
|
|
487
623
|
#ambientResolutionFor(resolutionStack) {
|
|
624
|
+
if (resolutionStack === this.rootStack) {
|
|
625
|
+
return (this.#rootAmbientResolution ??= this.#buildAmbientResolution(resolutionStack));
|
|
626
|
+
}
|
|
627
|
+
return this.#buildAmbientResolution(resolutionStack);
|
|
628
|
+
}
|
|
629
|
+
#buildAmbientResolution(resolutionStack) {
|
|
488
630
|
return {
|
|
489
631
|
resolve: (token, options) => options === undefined
|
|
490
632
|
? this.resolveFromContext(token, resolutionStack)
|
|
@@ -572,6 +714,19 @@ export class DependencyResolver {
|
|
|
572
714
|
return this.resolve(dep.token, options, resolutionStack, singleCriterionForSlot(dep));
|
|
573
715
|
}
|
|
574
716
|
resolveOptional(token, options, resolutionStack, precomputedCriterion) {
|
|
717
|
+
if (options === undefined) {
|
|
718
|
+
// The lane a plain resolve takes: a lone default in this registry is the answer, predicate-free by
|
|
719
|
+
// construction, and a root that keeps no records has nothing else that could hold the token.
|
|
720
|
+
const fastBinding = this.#registry.getFastDefault(token);
|
|
721
|
+
if (fastBinding !== undefined) {
|
|
722
|
+
if (fastBinding.kind !== "alias") {
|
|
723
|
+
return this.#resolveDefaultEntry(fastBinding, this, resolutionStack);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
else if (this.#parent === undefined && !this.#registry.isRecordMapBuilt) {
|
|
727
|
+
return undefined;
|
|
728
|
+
}
|
|
729
|
+
}
|
|
575
730
|
const entry = this.#findBinding(token, options, resolutionStack, precomputedCriterion === undefined ? singleCriterionOnlyOf(options) : (precomputedCriterion ?? undefined));
|
|
576
731
|
if (entry === undefined) {
|
|
577
732
|
return undefined;
|
|
@@ -595,6 +750,80 @@ export class DependencyResolver {
|
|
|
595
750
|
}
|
|
596
751
|
return resolved;
|
|
597
752
|
}
|
|
753
|
+
/**
|
|
754
|
+
* A root-level, options-less collection read through its memo: the value list when it is stable,
|
|
755
|
+
* the candidate list otherwise.
|
|
756
|
+
*
|
|
757
|
+
* @remarks Its own entry rather than a branch in `resolveAll`, so the options lane keeps the exact
|
|
758
|
+
* shape it had; the container routes a top-level read with no options here.
|
|
759
|
+
*/
|
|
760
|
+
resolveRootCollection(token) {
|
|
761
|
+
const resolutionStack = this.rootStack;
|
|
762
|
+
const memo = this.#rootCollection(token, resolutionStack);
|
|
763
|
+
if (memo.values !== undefined && memo.activationVersion === this.#chainActivationVersion()) {
|
|
764
|
+
return memo.values;
|
|
765
|
+
}
|
|
766
|
+
const { candidates } = memo;
|
|
767
|
+
const resolved = new Array(candidates.length);
|
|
768
|
+
for (let index = 0; index < candidates.length; index += 1) {
|
|
769
|
+
resolved[index] = this.#resolveCandidateSync(candidates[index], undefined, resolutionStack);
|
|
770
|
+
}
|
|
771
|
+
// The members this read materialised may have made the list stable for the next one.
|
|
772
|
+
this.#settleCollectionValues(memo);
|
|
773
|
+
return resolved;
|
|
774
|
+
}
|
|
775
|
+
/** The async twin of `resolveRootCollection`: a stable value list settles at once, candidates fan out as usual. */
|
|
776
|
+
resolveRootCollectionAsync(token) {
|
|
777
|
+
// The async lane appends to its branch and never unwinds, so it works on a stack of its own.
|
|
778
|
+
const resolutionStack = [];
|
|
779
|
+
const memo = this.#rootCollection(token, resolutionStack);
|
|
780
|
+
if (memo.values !== undefined && memo.activationVersion === this.#chainActivationVersion()) {
|
|
781
|
+
return Promise.resolve(memo.values);
|
|
782
|
+
}
|
|
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
|
+
}
|
|
788
|
+
/**
|
|
789
|
+
* The memoized candidate list of a root-level, options-less collection, built on its first read.
|
|
790
|
+
*
|
|
791
|
+
* @remarks Sound because a `when()` predicate is pure over its context and the root context is a
|
|
792
|
+
* constant: the list can only change when a registry in the chain does, which is the version the
|
|
793
|
+
* memo is stamped with. The value list is kept too while every member is a hook-free constant
|
|
794
|
+
* and no activation hook exists anywhere in the chain.
|
|
795
|
+
*/
|
|
796
|
+
#rootCollection(token, resolutionStack) {
|
|
797
|
+
const memo = this.#lookup.collection(token);
|
|
798
|
+
if (memo !== undefined) {
|
|
799
|
+
return memo;
|
|
800
|
+
}
|
|
801
|
+
const candidates = this.#candidateBindings(token, undefined, resolutionStack);
|
|
802
|
+
const entry = { candidates, values: undefined, activationVersion: -1 };
|
|
803
|
+
this.#settleCollectionValues(entry);
|
|
804
|
+
this.#lookup.rememberCollection(token, entry);
|
|
805
|
+
return entry;
|
|
806
|
+
}
|
|
807
|
+
/**
|
|
808
|
+
* Fills a collection memo's value list once every member is stable: a hook-free constant, or a
|
|
809
|
+
* hook-free singleton whose instance is cached — anything that changes either bumps a registry
|
|
810
|
+
* version the memo is keyed on. A member still to be materialised leaves the list unfilled.
|
|
811
|
+
*/
|
|
812
|
+
#settleCollectionValues(entry) {
|
|
813
|
+
if (entry.values !== undefined || this.#chainActivationVersion() !== 0) {
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
const { candidates } = entry;
|
|
817
|
+
for (let index = 0; index < candidates.length; index += 1) {
|
|
818
|
+
if (!isStableCollectionMember(candidates[index])) {
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
// Handed out as is, unfrozen: a frozen array iterates through a slow elements kind, so the
|
|
823
|
+
// contract's read-only return is the guard against a caller writing into the memo.
|
|
824
|
+
entry.values = candidates.map(stableMemberValue);
|
|
825
|
+
entry.activationVersion = 0;
|
|
826
|
+
}
|
|
598
827
|
/** Every binding in the chain a `resolveAll` request matches, in chain order. */
|
|
599
828
|
#candidateBindings(token, options, resolutionStack) {
|
|
600
829
|
if (options !== undefined) {
|
|
@@ -648,7 +877,7 @@ export class DependencyResolver {
|
|
|
648
877
|
}
|
|
649
878
|
}
|
|
650
879
|
else if (this.#scope.isChild) {
|
|
651
|
-
const cachedScoped = this.#scope.readScoped(binding.
|
|
880
|
+
const cachedScoped = this.#scope.readScoped(binding.identifier);
|
|
652
881
|
if (cachedScoped !== SCOPED_MISS) {
|
|
653
882
|
return Promise.resolve(cachedScoped);
|
|
654
883
|
}
|
|
@@ -685,7 +914,7 @@ export class DependencyResolver {
|
|
|
685
914
|
return binding.instance;
|
|
686
915
|
}
|
|
687
916
|
// In-flight dedup: concurrent callers share the first creation.
|
|
688
|
-
const inflight = this.#scope.getInflight(binding.
|
|
917
|
+
const inflight = this.#scope.getInflight(binding.identifier);
|
|
689
918
|
if (inflight !== undefined) {
|
|
690
919
|
return inflight;
|
|
691
920
|
}
|
|
@@ -699,7 +928,7 @@ export class DependencyResolver {
|
|
|
699
928
|
return cachedScoped;
|
|
700
929
|
}
|
|
701
930
|
// In-flight dedup, scoped flavor: one instance per scope even under concurrency.
|
|
702
|
-
const inflight = this.#scope.getInflight(binding.
|
|
931
|
+
const inflight = this.#scope.getInflight(binding.identifier);
|
|
703
932
|
if (inflight !== undefined) {
|
|
704
933
|
return inflight;
|
|
705
934
|
}
|
|
@@ -711,7 +940,7 @@ export class DependencyResolver {
|
|
|
711
940
|
// This level appends to its own branch and never unwinds.
|
|
712
941
|
const levelStack = extendResolutionBranch(resolutionStack, branchDepth, frame);
|
|
713
942
|
const levelDepth = branchDepthOf(levelStack);
|
|
714
|
-
const needsActivation = owner.#
|
|
943
|
+
const needsActivation = owner.#activationNeed().needsActivation(binding);
|
|
715
944
|
if (!needsActivation && scope === "transient" && (binding.kind === "dynamic" || binding.kind === "dynamic-async")) {
|
|
716
945
|
const resolutionCtx = new AsyncLevelContext(this, levelStack, options);
|
|
717
946
|
if (binding.kind === "dynamic-async") {
|
|
@@ -727,26 +956,26 @@ export class DependencyResolver {
|
|
|
727
956
|
// The promise is published before it settles, so concurrent callers dedup onto it.
|
|
728
957
|
const singletonPromise = this.#instantiateAndActivateAsync(binding, resolutionCtx, levelStack, levelDepth, needsActivation, owner).then((activated) => {
|
|
729
958
|
this.#scope.setSingleton(binding, activated);
|
|
730
|
-
this.#scope.clearInflight(binding.
|
|
959
|
+
this.#scope.clearInflight(binding.identifier);
|
|
731
960
|
return activated;
|
|
732
961
|
}, (error) => {
|
|
733
|
-
this.#scope.clearInflight(binding.
|
|
962
|
+
this.#scope.clearInflight(binding.identifier);
|
|
734
963
|
throw error;
|
|
735
964
|
});
|
|
736
|
-
this.#scope.setInflight(binding.
|
|
965
|
+
this.#scope.setInflight(binding.identifier, singletonPromise);
|
|
737
966
|
return await singletonPromise;
|
|
738
967
|
}
|
|
739
968
|
if (scope === "scoped") {
|
|
740
969
|
// Published before it settles, like the singleton lane: concurrent callers share one creation.
|
|
741
970
|
const scopedPromise = this.#instantiateAndActivateAsync(binding, resolutionCtx, levelStack, levelDepth, needsActivation, owner).then((activated) => {
|
|
742
971
|
this.#scope.setScoped(binding, activated);
|
|
743
|
-
this.#scope.clearInflight(binding.
|
|
972
|
+
this.#scope.clearInflight(binding.identifier);
|
|
744
973
|
return activated;
|
|
745
974
|
}, (error) => {
|
|
746
|
-
this.#scope.clearInflight(binding.
|
|
975
|
+
this.#scope.clearInflight(binding.identifier);
|
|
747
976
|
throw error;
|
|
748
977
|
});
|
|
749
|
-
this.#scope.setInflight(binding.
|
|
978
|
+
this.#scope.setInflight(binding.identifier, scopedPromise);
|
|
750
979
|
return await scopedPromise;
|
|
751
980
|
}
|
|
752
981
|
return await this.#instantiateAndActivateAsync(binding, resolutionCtx, levelStack, levelDepth, needsActivation, owner);
|
|
@@ -766,7 +995,7 @@ export class DependencyResolver {
|
|
|
766
995
|
async #instantiateAndActivateAsync(binding, ctx, resolutionStack, branchDepth, needsActivation, owner) {
|
|
767
996
|
const instance = await this.#instantiateAsync(binding, ctx, resolutionStack, branchDepth);
|
|
768
997
|
this.#mirrorPostConstructFromOwner(binding, owner);
|
|
769
|
-
if (!owner.#
|
|
998
|
+
if (!owner.#activationNeed().refreshAfterFirstInstantiation(binding, needsActivation)) {
|
|
770
999
|
return instance;
|
|
771
1000
|
}
|
|
772
1001
|
return owner.#lifecycle.runActivation(ctx, binding, instance, owner.#metadataReader);
|
|
@@ -916,12 +1145,12 @@ export class DependencyResolver {
|
|
|
916
1145
|
/** A constant with no activation anywhere resolves to its value with no pipeline at all. */
|
|
917
1146
|
#isPlainConstant(binding) {
|
|
918
1147
|
return (binding.kind === "constant" &&
|
|
919
|
-
binding.
|
|
1148
|
+
binding.activationHook === undefined &&
|
|
920
1149
|
(this.#lifecycle.activationVersion === 0 || !this.#lifecycle.hasActivationHandlers(binding.token)));
|
|
921
1150
|
}
|
|
922
1151
|
/** Whether either an own hook or a container-level hook would run for this binding. */
|
|
923
1152
|
#hasAnyActivation(binding) {
|
|
924
|
-
if (binding.
|
|
1153
|
+
if (binding.activationHook !== undefined) {
|
|
925
1154
|
return true;
|
|
926
1155
|
}
|
|
927
1156
|
return this.#lifecycle.activationVersion !== 0 && this.#lifecycle.hasActivationHandlers(binding.token);
|
|
@@ -936,7 +1165,7 @@ export class DependencyResolver {
|
|
|
936
1165
|
if (!this.#scope.isChild) {
|
|
937
1166
|
throw new MissingScopeContextError(tokenName(binding.token));
|
|
938
1167
|
}
|
|
939
|
-
return this.#scope.readScoped(binding.
|
|
1168
|
+
return this.#scope.readScoped(binding.identifier);
|
|
940
1169
|
}
|
|
941
1170
|
// The shared root context answers every top-level request; building one is the rarer half and
|
|
942
1171
|
// lives outside, so what a selection inlines is the test and not the literal.
|
|
@@ -1137,7 +1366,7 @@ export class DependencyResolver {
|
|
|
1137
1366
|
#resolveCandidateSync(binding, options, resolutionStack) {
|
|
1138
1367
|
// Fan-outs are dominated by constants: with no activation hook anywhere in the chain, a
|
|
1139
1368
|
// hook-free constant is plain no matter which container owns it — skip the owner probe.
|
|
1140
|
-
if (binding.kind === "constant" && binding.
|
|
1369
|
+
if (binding.kind === "constant" && binding.activationHook === undefined && this.#chainActivationVersion() === 0) {
|
|
1141
1370
|
return binding.value;
|
|
1142
1371
|
}
|
|
1143
1372
|
const owner = this.#ownerOf(binding);
|
|
@@ -1157,7 +1386,7 @@ export class DependencyResolver {
|
|
|
1157
1386
|
return this.#resolveBinding(binding, options, resolutionStack, owner);
|
|
1158
1387
|
}
|
|
1159
1388
|
#resolveCandidateAsync(binding, options, resolutionStack, branchDepth) {
|
|
1160
|
-
if (binding.kind === "constant" && binding.
|
|
1389
|
+
if (binding.kind === "constant" && binding.activationHook === undefined && this.#chainActivationVersion() === 0) {
|
|
1161
1390
|
return Promise.resolve(binding.value);
|
|
1162
1391
|
}
|
|
1163
1392
|
const owner = this.#ownerOf(binding);
|
|
@@ -1173,16 +1402,25 @@ export class DependencyResolver {
|
|
|
1173
1402
|
}
|
|
1174
1403
|
return owner.#resolveBindingAsync(binding, options, resolutionStack, branchDepth, owner);
|
|
1175
1404
|
}
|
|
1405
|
+
// The dominant collection member — a transient factory with no activation, asked with no
|
|
1406
|
+
// options — takes the non-async lane a single resolve takes, so a fan-out costs one factory
|
|
1407
|
+
// promise per member rather than a state machine on top of each.
|
|
1408
|
+
if (options === undefined &&
|
|
1409
|
+
binding.scope === "transient" &&
|
|
1410
|
+
(binding.kind === "dynamic" || binding.kind === "dynamic-async") &&
|
|
1411
|
+
!owner.#hasAnyActivation(binding)) {
|
|
1412
|
+
return this.#resolveTransientDynamicAsyncFromContext(binding, resolutionStack, branchDepth);
|
|
1413
|
+
}
|
|
1176
1414
|
return this.#resolveBindingAsync(binding, options, resolutionStack, branchDepth, owner);
|
|
1177
1415
|
}
|
|
1178
1416
|
/** The resolver whose registry holds `binding` — `this` (the common case) when it is own. */
|
|
1179
1417
|
#ownerOf(binding) {
|
|
1180
1418
|
// A root resolver can only hold its own bindings, so the per-candidate id probe is chain-only.
|
|
1181
|
-
if (this.#parent === undefined || this.#registry.getById(binding.
|
|
1419
|
+
if (this.#parent === undefined || this.#registry.getById(binding.identifier) !== undefined) {
|
|
1182
1420
|
return this;
|
|
1183
1421
|
}
|
|
1184
1422
|
for (let current = this.#parent; current !== undefined; current = current.#parent) {
|
|
1185
|
-
if (current.#registry.getById(binding.
|
|
1423
|
+
if (current.#registry.getById(binding.identifier) !== undefined) {
|
|
1186
1424
|
return current;
|
|
1187
1425
|
}
|
|
1188
1426
|
}
|
|
@@ -1196,7 +1434,7 @@ export class DependencyResolver {
|
|
|
1196
1434
|
if (existing !== undefined) {
|
|
1197
1435
|
return existing;
|
|
1198
1436
|
}
|
|
1199
|
-
const frame = buildResolutionFrame(tokenName(binding.token), binding.scope, binding.
|
|
1437
|
+
const frame = buildResolutionFrame(tokenName(binding.token), binding.scope, binding.identifier, binding.kind, binding.slot);
|
|
1200
1438
|
binding.frame = frame;
|
|
1201
1439
|
return frame;
|
|
1202
1440
|
}
|
|
@@ -1234,6 +1472,22 @@ export class DependencyResolver {
|
|
|
1234
1472
|
return created;
|
|
1235
1473
|
}
|
|
1236
1474
|
}
|
|
1475
|
+
/**
|
|
1476
|
+
* Whether a binding answers a collection read with a fixed value: a hook-free constant, or a
|
|
1477
|
+
* singleton whose instance is already cached.
|
|
1478
|
+
*
|
|
1479
|
+
* @remarks A cached singleton reads like a constant until a registry change evicts it, which also
|
|
1480
|
+
* drops the memo.
|
|
1481
|
+
*/
|
|
1482
|
+
function isStableCollectionMember(binding) {
|
|
1483
|
+
if (binding.kind === "alias" || binding.activationHook !== undefined) {
|
|
1484
|
+
return false;
|
|
1485
|
+
}
|
|
1486
|
+
return binding.kind === "constant" || (binding.scope === "singleton" && binding.instance !== NO_INSTANCE);
|
|
1487
|
+
}
|
|
1488
|
+
function stableMemberValue(binding) {
|
|
1489
|
+
return binding.kind === "constant" ? binding.value : binding.instance;
|
|
1490
|
+
}
|
|
1237
1491
|
function anyPredicate(bindings) {
|
|
1238
1492
|
for (let index = 0; index < bindings.length; index += 1) {
|
|
1239
1493
|
if (bindings[index].predicate !== undefined) {
|