@codefast/di 0.8.1 → 0.10.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 +164 -0
- package/LICENSE +1 -1
- package/README.md +29 -20
- package/dist/ambient/active-container.d.ts +13 -0
- package/dist/ambient/active-container.js +24 -0
- package/dist/container/binding-builders.d.ts +42 -20
- package/dist/container/binding-builders.js +155 -87
- package/dist/container/container.d.ts +10 -10
- package/dist/container/container.js +55 -23
- package/dist/core/binding.d.ts +55 -57
- package/dist/core/binding.js +11 -37
- package/dist/core/constraint-requirement.d.ts +19 -4
- package/dist/core/constraint-requirement.js +19 -9
- package/dist/core/registry.d.ts +47 -4
- package/dist/core/registry.js +361 -159
- package/dist/core/state-epoch.d.ts +16 -0
- package/dist/core/state-epoch.js +21 -0
- package/dist/core/tag.d.ts +2 -2
- package/dist/core/tag.js +2 -2
- package/dist/core/token.d.ts +24 -4
- package/dist/core/token.js +1 -1
- package/dist/core/types.d.ts +15 -6
- package/dist/decorators/inject.d.ts +1 -1
- package/dist/decorators/inject.js +6 -4
- package/dist/errors/diagnostics.d.ts +2 -0
- package/dist/errors/errors.d.ts +33 -1
- package/dist/errors/errors.js +44 -3
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/injection/descriptor.d.ts +16 -8
- package/dist/injection/descriptor.js +3 -1
- package/dist/injection/resolve-options.d.ts +6 -0
- package/dist/injection/resolve-options.js +16 -0
- package/dist/introspection/dependency-graph.js +10 -5
- package/dist/introspection/inspector.d.ts +3 -1
- package/dist/introspection/inspector.js +10 -24
- package/dist/lifecycle/lifecycle-manager.js +10 -8
- package/dist/lifecycle/scope-manager.js +1 -1
- package/dist/metadata/metadata-reader-token.js +1 -1
- package/dist/resolution/cache/activation-need.d.ts +2 -0
- package/dist/resolution/cache/activation-need.js +13 -6
- package/dist/resolution/cache/binding-lookup-cache.d.ts +34 -1
- package/dist/resolution/cache/binding-lookup-cache.js +96 -12
- package/dist/resolution/cache/class-introspector.d.ts +1 -1
- package/dist/resolution/cache/class-introspector.js +28 -14
- package/dist/resolution/context.d.ts +8 -8
- package/dist/resolution/plan/instantiation-plan.d.ts +12 -0
- package/dist/resolution/plan/instantiation-plan.js +156 -65
- 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 +14 -4
- package/dist/resolution/resolver.js +375 -134
- package/dist/resolution/select/binding-select.js +12 -7
- package/dist/resolution/select/constraints.d.ts +7 -4
- package/dist/resolution/select/constraints.js +34 -13
- package/package.json +14 -40
|
@@ -2,7 +2,7 @@ import { NO_INSTANCE } from "#/core/binding";
|
|
|
2
2
|
import { NO_TAG_KEYS, slotNameCriterionOf } from "#/core/tag";
|
|
3
3
|
import { tokenName } from "#/core/token";
|
|
4
4
|
import { AsyncActivationError, AsyncResolutionError, CircularDependencyError, DisposedContainerError, InternalError, MissingMetadataError, MissingScopeContextError, NoMatchingBindingError, TokenNotBoundError, } from "#/errors/errors";
|
|
5
|
-
import { resolveOptionsForSlot, singleCriterionForSlot, singleCriterionOnlyOf } from "#/injection/resolve-options";
|
|
5
|
+
import { loneTagBesideNameOf, resolveOptionsForSlot, singleCriterionForSlot, singleCriterionOnlyOf, } from "#/injection/resolve-options";
|
|
6
6
|
import { SCOPED_MISS } from "#/lifecycle/scope-manager";
|
|
7
7
|
import { ActivationNeedCache } from "#/resolution/cache/activation-need";
|
|
8
8
|
import { BindingLookupCache } from "#/resolution/cache/binding-lookup-cache";
|
|
@@ -16,6 +16,18 @@ 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,137 @@ 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({
|
|
415
|
+
hasActivationHandlers: (binding) => this.#ownerOf(binding).#lifecycle.hasActivationHandlers(binding.token),
|
|
416
|
+
knownPostConstruct: (target) => this.#classes.knownPostConstruct(target),
|
|
417
|
+
needsActiveContainer: (target) => this.#classes.needsActiveContainer(target),
|
|
418
|
+
// A plan runs at the top level, so the lent root stack is free when it is; a plan reached
|
|
419
|
+
// with the root stack held mints its own path, exactly as the interpreted lane would.
|
|
420
|
+
constructWithAccessors: (binding, target, deps) => {
|
|
421
|
+
const stack = this.rootStack.length === 0 ? this.rootStack : [];
|
|
422
|
+
const frame = this.#getResolutionFrame(binding);
|
|
423
|
+
const resolutionSet = enterResolutionPath(stack, frame);
|
|
424
|
+
try {
|
|
425
|
+
return this.#classes.instantiate(target, deps, this.#ambientResolutionFor(stack));
|
|
426
|
+
}
|
|
427
|
+
finally {
|
|
428
|
+
stack.pop();
|
|
429
|
+
resolutionSet?.delete(frame.bindingId);
|
|
430
|
+
}
|
|
431
|
+
},
|
|
432
|
+
getConstructorMetadata: (target) => this.#classes.constructorMetadata(target),
|
|
433
|
+
lookupDependencyEntry: (token) => {
|
|
434
|
+
const entry = this.#lookup.defaultEntry(token);
|
|
435
|
+
return entry === null ? null : { binding: entry.binding };
|
|
436
|
+
},
|
|
437
|
+
// Exactly what #findBinding's single-criterion lane accepts, minus the half that reads a path:
|
|
438
|
+
// a predicate is the compiler's cue to leave the selection to the runtime.
|
|
439
|
+
lookupPathIndependentEntry: (token, options) => {
|
|
440
|
+
const singleCriterion = singleCriterionOnlyOf(options);
|
|
441
|
+
if (singleCriterion === undefined) {
|
|
442
|
+
return null;
|
|
443
|
+
}
|
|
444
|
+
const entry = this.#lookup.taggedEntry(token, singleCriterion);
|
|
445
|
+
if (entry === null || entry.binding.predicate !== undefined || !matchesSlot(entry.binding.slot, options)) {
|
|
446
|
+
return null;
|
|
447
|
+
}
|
|
448
|
+
return { binding: entry.binding };
|
|
449
|
+
},
|
|
450
|
+
getResolutionFrame: (binding) => this.#getResolutionFrame(binding),
|
|
451
|
+
// Identity-guarded: a map cleared and recompiled since no longer holds the plan being replaced.
|
|
452
|
+
replacePlan: (binding, current, next) => {
|
|
453
|
+
const plans = this.#classPlanByBindingId;
|
|
454
|
+
if (plans !== undefined && plans.get(binding.identifier) === current) {
|
|
455
|
+
plans.set(binding.identifier, next);
|
|
456
|
+
}
|
|
457
|
+
},
|
|
458
|
+
replaceAsyncPlan: (binding, current, next) => {
|
|
459
|
+
const plans = this.#asyncPlanByBindingId;
|
|
460
|
+
if (plans !== undefined && plans.get(binding.identifier) === current) {
|
|
461
|
+
plans.set(binding.identifier, next);
|
|
462
|
+
}
|
|
463
|
+
},
|
|
464
|
+
// Dispatches exactly as #resolveDep does, so an escaped dep is indistinguishable
|
|
465
|
+
// from the same dep on a fully interpreted resolve.
|
|
466
|
+
resolveEscaped: (token, options, arity, resolutionStack) => {
|
|
467
|
+
if (arity === "all") {
|
|
468
|
+
return this.resolveAll(token, options, resolutionStack);
|
|
469
|
+
}
|
|
470
|
+
if (arity === "optional") {
|
|
471
|
+
return this.resolveOptional(token, options, resolutionStack);
|
|
472
|
+
}
|
|
473
|
+
if (options === undefined) {
|
|
474
|
+
return this.resolveFromContext(token, resolutionStack);
|
|
475
|
+
}
|
|
476
|
+
return this.resolve(token, options, resolutionStack);
|
|
477
|
+
},
|
|
478
|
+
// Dispatches exactly as #resolveDepAsync does, for the async lane's escapes.
|
|
479
|
+
resolveEscapedAsync: (token, options, arity, resolutionStack) => {
|
|
480
|
+
if (arity === "all") {
|
|
481
|
+
return this.resolveAllAsync(token, options, resolutionStack, UNOWNED_BRANCH);
|
|
482
|
+
}
|
|
483
|
+
if (arity === "optional") {
|
|
484
|
+
return this.resolveOptionalAsync(token, options, resolutionStack, UNOWNED_BRANCH);
|
|
485
|
+
}
|
|
486
|
+
if (options === undefined) {
|
|
487
|
+
return this.resolveAsyncFromContext(token, resolutionStack, UNOWNED_BRANCH);
|
|
488
|
+
}
|
|
489
|
+
return this.resolveAsync(token, options, resolutionStack, UNOWNED_BRANCH);
|
|
490
|
+
},
|
|
491
|
+
}));
|
|
492
|
+
}
|
|
374
493
|
/** The async lane's plan for a statically-visible transient binding, mirroring the sync getter. */
|
|
375
494
|
#getAsyncInstantiationPlan(binding) {
|
|
376
495
|
const registryVersion = this.#lookup.chainVersion();
|
|
377
496
|
const activationVersion = this.#chainActivationVersion();
|
|
378
497
|
if (registryVersion !== this.#asyncPlanRegistryVersion || activationVersion !== this.#asyncPlanActivationVersion) {
|
|
379
|
-
this.#asyncPlanByBindingId
|
|
498
|
+
if (this.#asyncPlanByBindingId === undefined) {
|
|
499
|
+
this.#asyncPlanByBindingId = new Map();
|
|
500
|
+
}
|
|
501
|
+
else {
|
|
502
|
+
this.#asyncPlanByBindingId.clear();
|
|
503
|
+
}
|
|
380
504
|
this.#asyncPlanRegistryVersion = registryVersion;
|
|
381
505
|
this.#asyncPlanActivationVersion = activationVersion;
|
|
382
506
|
}
|
|
383
|
-
const
|
|
507
|
+
const plans = this.#asyncPlanByBindingId;
|
|
508
|
+
const cached = plans.get(binding.identifier);
|
|
384
509
|
if (cached !== undefined) {
|
|
385
510
|
return cached;
|
|
386
511
|
}
|
|
387
|
-
const compiled = this.#
|
|
512
|
+
const compiled = this.#compiler().compileAsync(binding);
|
|
388
513
|
if (compiled === PLAN_RETRY) {
|
|
389
514
|
// Lifecycle metadata not discovered yet — the fallback resolve discovers it; retry then.
|
|
390
515
|
return null;
|
|
391
516
|
}
|
|
392
|
-
|
|
517
|
+
plans.set(binding.identifier, compiled);
|
|
393
518
|
return compiled;
|
|
394
519
|
}
|
|
395
520
|
resolve(token, options, resolutionStack, precomputedCriterion) {
|
|
@@ -430,7 +555,7 @@ export class DependencyResolver {
|
|
|
430
555
|
return binding.instance;
|
|
431
556
|
}
|
|
432
557
|
// An async materialization already in flight must not be raced by a second, sync one.
|
|
433
|
-
if (this.#scope.getInflight(binding.
|
|
558
|
+
if (this.#scope.getInflight(binding.identifier) !== undefined) {
|
|
434
559
|
throw new AsyncResolutionError(resolutionStack[0]?.tokenName ?? tokenName(binding.token), tokenName(binding.token));
|
|
435
560
|
}
|
|
436
561
|
if (this.#scope.isClosed) {
|
|
@@ -442,7 +567,7 @@ export class DependencyResolver {
|
|
|
442
567
|
if (cachedScoped !== SCOPED_MISS) {
|
|
443
568
|
return cachedScoped;
|
|
444
569
|
}
|
|
445
|
-
if (this.#scope.getInflight(binding.
|
|
570
|
+
if (this.#scope.getInflight(binding.identifier) !== undefined) {
|
|
446
571
|
throw new AsyncResolutionError(resolutionStack[0]?.tokenName ?? tokenName(binding.token), tokenName(binding.token));
|
|
447
572
|
}
|
|
448
573
|
if (this.#scope.isClosed) {
|
|
@@ -453,7 +578,7 @@ export class DependencyResolver {
|
|
|
453
578
|
const tokenDisplayName = frame.tokenName;
|
|
454
579
|
const resolutionSet = enterResolutionPath(resolutionStack, frame);
|
|
455
580
|
try {
|
|
456
|
-
const needsActivation = owner.#
|
|
581
|
+
const needsActivation = owner.#activationNeed().needsActivation(binding);
|
|
457
582
|
if (!needsActivation && scope === "transient" && binding.kind === "dynamic") {
|
|
458
583
|
const resolutionCtx = this.#acquireSyncResolutionContext(resolutionStack, options);
|
|
459
584
|
const dynamicResult = binding.factory(resolutionCtx);
|
|
@@ -467,7 +592,7 @@ export class DependencyResolver {
|
|
|
467
592
|
: undefined;
|
|
468
593
|
const instance = this.#instantiateSync(binding, resolutionCtx, resolutionStack);
|
|
469
594
|
this.#mirrorPostConstructFromOwner(binding, owner);
|
|
470
|
-
const activated = owner.#
|
|
595
|
+
const activated = owner.#activationNeed().refreshAfterFirstInstantiation(binding, needsActivation)
|
|
471
596
|
? owner.#lifecycle.runActivationSync(resolutionCtx, binding, instance, owner.#metadataReader)
|
|
472
597
|
: instance;
|
|
473
598
|
if (scope === "singleton") {
|
|
@@ -484,7 +609,16 @@ export class DependencyResolver {
|
|
|
484
609
|
}
|
|
485
610
|
}
|
|
486
611
|
/** Path-continuing resolution handed to the ambient slot while an accessor class constructs. */
|
|
612
|
+
// The ambient resolution a top-level construction hands its accessors: the lent root stack is one
|
|
613
|
+
// array for the resolver's lifetime, so the pair of closures over it is built once and reused.
|
|
614
|
+
#rootAmbientResolution;
|
|
487
615
|
#ambientResolutionFor(resolutionStack) {
|
|
616
|
+
if (resolutionStack === this.rootStack) {
|
|
617
|
+
return (this.#rootAmbientResolution ??= this.#buildAmbientResolution(resolutionStack));
|
|
618
|
+
}
|
|
619
|
+
return this.#buildAmbientResolution(resolutionStack);
|
|
620
|
+
}
|
|
621
|
+
#buildAmbientResolution(resolutionStack) {
|
|
488
622
|
return {
|
|
489
623
|
resolve: (token, options) => options === undefined
|
|
490
624
|
? this.resolveFromContext(token, resolutionStack)
|
|
@@ -572,6 +706,19 @@ export class DependencyResolver {
|
|
|
572
706
|
return this.resolve(dep.token, options, resolutionStack, singleCriterionForSlot(dep));
|
|
573
707
|
}
|
|
574
708
|
resolveOptional(token, options, resolutionStack, precomputedCriterion) {
|
|
709
|
+
if (options === undefined) {
|
|
710
|
+
// The lane a plain resolve takes: a lone default in this registry is the answer, predicate-free by
|
|
711
|
+
// construction, and a root that keeps no records has nothing else that could hold the token.
|
|
712
|
+
const fastBinding = this.#registry.getFastDefault(token);
|
|
713
|
+
if (fastBinding !== undefined) {
|
|
714
|
+
if (fastBinding.kind !== "alias") {
|
|
715
|
+
return this.#resolveDefaultEntry(fastBinding, this, resolutionStack);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
else if (this.#parent === undefined && !this.#registry.isRecordMapBuilt) {
|
|
719
|
+
return undefined;
|
|
720
|
+
}
|
|
721
|
+
}
|
|
575
722
|
const entry = this.#findBinding(token, options, resolutionStack, precomputedCriterion === undefined ? singleCriterionOnlyOf(options) : (precomputedCriterion ?? undefined));
|
|
576
723
|
if (entry === undefined) {
|
|
577
724
|
return undefined;
|
|
@@ -595,6 +742,80 @@ export class DependencyResolver {
|
|
|
595
742
|
}
|
|
596
743
|
return resolved;
|
|
597
744
|
}
|
|
745
|
+
/**
|
|
746
|
+
* A root-level, options-less collection read through its memo: the value list when it is stable,
|
|
747
|
+
* the candidate list otherwise.
|
|
748
|
+
*
|
|
749
|
+
* @remarks Its own entry rather than a branch in `resolveAll`, so the options lane keeps the exact
|
|
750
|
+
* shape it had; the container routes a top-level read with no options here.
|
|
751
|
+
*/
|
|
752
|
+
resolveRootCollection(token) {
|
|
753
|
+
const resolutionStack = this.rootStack;
|
|
754
|
+
const memo = this.#rootCollection(token, resolutionStack);
|
|
755
|
+
if (memo.values !== undefined && memo.activationVersion === this.#chainActivationVersion()) {
|
|
756
|
+
return memo.values;
|
|
757
|
+
}
|
|
758
|
+
const { candidates } = memo;
|
|
759
|
+
const resolved = new Array(candidates.length);
|
|
760
|
+
for (let index = 0; index < candidates.length; index += 1) {
|
|
761
|
+
resolved[index] = this.#resolveCandidateSync(candidates[index], undefined, resolutionStack);
|
|
762
|
+
}
|
|
763
|
+
// The members this read materialised may have made the list stable for the next one.
|
|
764
|
+
this.#settleCollectionValues(memo);
|
|
765
|
+
return resolved;
|
|
766
|
+
}
|
|
767
|
+
/** The async twin of `resolveRootCollection`: a stable value list settles at once, candidates fan out as usual. */
|
|
768
|
+
resolveRootCollectionAsync(token) {
|
|
769
|
+
// The async lane appends to its branch and never unwinds, so it works on a stack of its own.
|
|
770
|
+
const resolutionStack = [];
|
|
771
|
+
const memo = this.#rootCollection(token, resolutionStack);
|
|
772
|
+
if (memo.values !== undefined && memo.activationVersion === this.#chainActivationVersion()) {
|
|
773
|
+
return Promise.resolve(memo.values);
|
|
774
|
+
}
|
|
775
|
+
return Promise.all(memo.candidates.map((candidate) => this.#resolveCandidateAsync(candidate, undefined, resolutionStack, UNOWNED_BRANCH))).then((values) => {
|
|
776
|
+
this.#settleCollectionValues(memo);
|
|
777
|
+
return values;
|
|
778
|
+
});
|
|
779
|
+
}
|
|
780
|
+
/**
|
|
781
|
+
* The memoized candidate list of a root-level, options-less collection, built on its first read.
|
|
782
|
+
*
|
|
783
|
+
* @remarks Sound because a `when()` predicate is pure over its context and the root context is a
|
|
784
|
+
* constant: the list can only change when a registry in the chain does, which is the version the
|
|
785
|
+
* memo is stamped with. The value list is kept too while every member is a hook-free constant
|
|
786
|
+
* and no activation hook exists anywhere in the chain.
|
|
787
|
+
*/
|
|
788
|
+
#rootCollection(token, resolutionStack) {
|
|
789
|
+
const memo = this.#lookup.collection(token);
|
|
790
|
+
if (memo !== undefined) {
|
|
791
|
+
return memo;
|
|
792
|
+
}
|
|
793
|
+
const candidates = this.#candidateBindings(token, undefined, resolutionStack);
|
|
794
|
+
const entry = { candidates, values: undefined, activationVersion: -1 };
|
|
795
|
+
this.#settleCollectionValues(entry);
|
|
796
|
+
this.#lookup.rememberCollection(token, entry);
|
|
797
|
+
return entry;
|
|
798
|
+
}
|
|
799
|
+
/**
|
|
800
|
+
* Fills a collection memo's value list once every member is stable: a hook-free constant, or a
|
|
801
|
+
* hook-free singleton whose instance is cached — anything that changes either bumps a registry
|
|
802
|
+
* version the memo is keyed on. A member still to be materialised leaves the list unfilled.
|
|
803
|
+
*/
|
|
804
|
+
#settleCollectionValues(entry) {
|
|
805
|
+
if (entry.values !== undefined || this.#chainActivationVersion() !== 0) {
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
const { candidates } = entry;
|
|
809
|
+
for (let index = 0; index < candidates.length; index += 1) {
|
|
810
|
+
if (!isStableCollectionMember(candidates[index])) {
|
|
811
|
+
return;
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
// Handed out as is, unfrozen: a frozen array iterates through a slow elements kind, so the
|
|
815
|
+
// contract's read-only return is the guard against a caller writing into the memo.
|
|
816
|
+
entry.values = candidates.map(stableMemberValue);
|
|
817
|
+
entry.activationVersion = 0;
|
|
818
|
+
}
|
|
598
819
|
/** Every binding in the chain a `resolveAll` request matches, in chain order. */
|
|
599
820
|
#candidateBindings(token, options, resolutionStack) {
|
|
600
821
|
if (options !== undefined) {
|
|
@@ -648,7 +869,7 @@ export class DependencyResolver {
|
|
|
648
869
|
}
|
|
649
870
|
}
|
|
650
871
|
else if (this.#scope.isChild) {
|
|
651
|
-
const cachedScoped = this.#scope.readScoped(binding.
|
|
872
|
+
const cachedScoped = this.#scope.readScoped(binding.identifier);
|
|
652
873
|
if (cachedScoped !== SCOPED_MISS) {
|
|
653
874
|
return Promise.resolve(cachedScoped);
|
|
654
875
|
}
|
|
@@ -685,7 +906,7 @@ export class DependencyResolver {
|
|
|
685
906
|
return binding.instance;
|
|
686
907
|
}
|
|
687
908
|
// In-flight dedup: concurrent callers share the first creation.
|
|
688
|
-
const inflight = this.#scope.getInflight(binding.
|
|
909
|
+
const inflight = this.#scope.getInflight(binding.identifier);
|
|
689
910
|
if (inflight !== undefined) {
|
|
690
911
|
return inflight;
|
|
691
912
|
}
|
|
@@ -699,7 +920,7 @@ export class DependencyResolver {
|
|
|
699
920
|
return cachedScoped;
|
|
700
921
|
}
|
|
701
922
|
// In-flight dedup, scoped flavor: one instance per scope even under concurrency.
|
|
702
|
-
const inflight = this.#scope.getInflight(binding.
|
|
923
|
+
const inflight = this.#scope.getInflight(binding.identifier);
|
|
703
924
|
if (inflight !== undefined) {
|
|
704
925
|
return inflight;
|
|
705
926
|
}
|
|
@@ -711,7 +932,7 @@ export class DependencyResolver {
|
|
|
711
932
|
// This level appends to its own branch and never unwinds.
|
|
712
933
|
const levelStack = extendResolutionBranch(resolutionStack, branchDepth, frame);
|
|
713
934
|
const levelDepth = branchDepthOf(levelStack);
|
|
714
|
-
const needsActivation = owner.#
|
|
935
|
+
const needsActivation = owner.#activationNeed().needsActivation(binding);
|
|
715
936
|
if (!needsActivation && scope === "transient" && (binding.kind === "dynamic" || binding.kind === "dynamic-async")) {
|
|
716
937
|
const resolutionCtx = new AsyncLevelContext(this, levelStack, options);
|
|
717
938
|
if (binding.kind === "dynamic-async") {
|
|
@@ -727,26 +948,26 @@ export class DependencyResolver {
|
|
|
727
948
|
// The promise is published before it settles, so concurrent callers dedup onto it.
|
|
728
949
|
const singletonPromise = this.#instantiateAndActivateAsync(binding, resolutionCtx, levelStack, levelDepth, needsActivation, owner).then((activated) => {
|
|
729
950
|
this.#scope.setSingleton(binding, activated);
|
|
730
|
-
this.#scope.clearInflight(binding.
|
|
951
|
+
this.#scope.clearInflight(binding.identifier);
|
|
731
952
|
return activated;
|
|
732
953
|
}, (error) => {
|
|
733
|
-
this.#scope.clearInflight(binding.
|
|
954
|
+
this.#scope.clearInflight(binding.identifier);
|
|
734
955
|
throw error;
|
|
735
956
|
});
|
|
736
|
-
this.#scope.setInflight(binding.
|
|
957
|
+
this.#scope.setInflight(binding.identifier, singletonPromise);
|
|
737
958
|
return await singletonPromise;
|
|
738
959
|
}
|
|
739
960
|
if (scope === "scoped") {
|
|
740
961
|
// Published before it settles, like the singleton lane: concurrent callers share one creation.
|
|
741
962
|
const scopedPromise = this.#instantiateAndActivateAsync(binding, resolutionCtx, levelStack, levelDepth, needsActivation, owner).then((activated) => {
|
|
742
963
|
this.#scope.setScoped(binding, activated);
|
|
743
|
-
this.#scope.clearInflight(binding.
|
|
964
|
+
this.#scope.clearInflight(binding.identifier);
|
|
744
965
|
return activated;
|
|
745
966
|
}, (error) => {
|
|
746
|
-
this.#scope.clearInflight(binding.
|
|
967
|
+
this.#scope.clearInflight(binding.identifier);
|
|
747
968
|
throw error;
|
|
748
969
|
});
|
|
749
|
-
this.#scope.setInflight(binding.
|
|
970
|
+
this.#scope.setInflight(binding.identifier, scopedPromise);
|
|
750
971
|
return await scopedPromise;
|
|
751
972
|
}
|
|
752
973
|
return await this.#instantiateAndActivateAsync(binding, resolutionCtx, levelStack, levelDepth, needsActivation, owner);
|
|
@@ -766,7 +987,7 @@ export class DependencyResolver {
|
|
|
766
987
|
async #instantiateAndActivateAsync(binding, ctx, resolutionStack, branchDepth, needsActivation, owner) {
|
|
767
988
|
const instance = await this.#instantiateAsync(binding, ctx, resolutionStack, branchDepth);
|
|
768
989
|
this.#mirrorPostConstructFromOwner(binding, owner);
|
|
769
|
-
if (!owner.#
|
|
990
|
+
if (!owner.#activationNeed().refreshAfterFirstInstantiation(binding, needsActivation)) {
|
|
770
991
|
return instance;
|
|
771
992
|
}
|
|
772
993
|
return owner.#lifecycle.runActivation(ctx, binding, instance, owner.#metadataReader);
|
|
@@ -916,12 +1137,12 @@ export class DependencyResolver {
|
|
|
916
1137
|
/** A constant with no activation anywhere resolves to its value with no pipeline at all. */
|
|
917
1138
|
#isPlainConstant(binding) {
|
|
918
1139
|
return (binding.kind === "constant" &&
|
|
919
|
-
binding.
|
|
1140
|
+
binding.activationHook === undefined &&
|
|
920
1141
|
(this.#lifecycle.activationVersion === 0 || !this.#lifecycle.hasActivationHandlers(binding.token)));
|
|
921
1142
|
}
|
|
922
1143
|
/** Whether either an own hook or a container-level hook would run for this binding. */
|
|
923
1144
|
#hasAnyActivation(binding) {
|
|
924
|
-
if (binding.
|
|
1145
|
+
if (binding.activationHook !== undefined) {
|
|
925
1146
|
return true;
|
|
926
1147
|
}
|
|
927
1148
|
return this.#lifecycle.activationVersion !== 0 && this.#lifecycle.hasActivationHandlers(binding.token);
|
|
@@ -936,7 +1157,7 @@ export class DependencyResolver {
|
|
|
936
1157
|
if (!this.#scope.isChild) {
|
|
937
1158
|
throw new MissingScopeContextError(tokenName(binding.token));
|
|
938
1159
|
}
|
|
939
|
-
return this.#scope.readScoped(binding.
|
|
1160
|
+
return this.#scope.readScoped(binding.identifier);
|
|
940
1161
|
}
|
|
941
1162
|
// The shared root context answers every top-level request; building one is the rarer half and
|
|
942
1163
|
// lives outside, so what a selection inlines is the test and not the literal.
|
|
@@ -1137,7 +1358,7 @@ export class DependencyResolver {
|
|
|
1137
1358
|
#resolveCandidateSync(binding, options, resolutionStack) {
|
|
1138
1359
|
// Fan-outs are dominated by constants: with no activation hook anywhere in the chain, a
|
|
1139
1360
|
// hook-free constant is plain no matter which container owns it — skip the owner probe.
|
|
1140
|
-
if (binding.kind === "constant" && binding.
|
|
1361
|
+
if (binding.kind === "constant" && binding.activationHook === undefined && this.#chainActivationVersion() === 0) {
|
|
1141
1362
|
return binding.value;
|
|
1142
1363
|
}
|
|
1143
1364
|
const owner = this.#ownerOf(binding);
|
|
@@ -1157,7 +1378,7 @@ export class DependencyResolver {
|
|
|
1157
1378
|
return this.#resolveBinding(binding, options, resolutionStack, owner);
|
|
1158
1379
|
}
|
|
1159
1380
|
#resolveCandidateAsync(binding, options, resolutionStack, branchDepth) {
|
|
1160
|
-
if (binding.kind === "constant" && binding.
|
|
1381
|
+
if (binding.kind === "constant" && binding.activationHook === undefined && this.#chainActivationVersion() === 0) {
|
|
1161
1382
|
return Promise.resolve(binding.value);
|
|
1162
1383
|
}
|
|
1163
1384
|
const owner = this.#ownerOf(binding);
|
|
@@ -1173,16 +1394,25 @@ export class DependencyResolver {
|
|
|
1173
1394
|
}
|
|
1174
1395
|
return owner.#resolveBindingAsync(binding, options, resolutionStack, branchDepth, owner);
|
|
1175
1396
|
}
|
|
1397
|
+
// The dominant collection member — a transient factory with no activation, asked with no
|
|
1398
|
+
// options — takes the non-async lane a single resolve takes, so a fan-out costs one factory
|
|
1399
|
+
// promise per member rather than a state machine on top of each.
|
|
1400
|
+
if (options === undefined &&
|
|
1401
|
+
binding.scope === "transient" &&
|
|
1402
|
+
(binding.kind === "dynamic" || binding.kind === "dynamic-async") &&
|
|
1403
|
+
!owner.#hasAnyActivation(binding)) {
|
|
1404
|
+
return this.#resolveTransientDynamicAsyncFromContext(binding, resolutionStack, branchDepth);
|
|
1405
|
+
}
|
|
1176
1406
|
return this.#resolveBindingAsync(binding, options, resolutionStack, branchDepth, owner);
|
|
1177
1407
|
}
|
|
1178
1408
|
/** The resolver whose registry holds `binding` — `this` (the common case) when it is own. */
|
|
1179
1409
|
#ownerOf(binding) {
|
|
1180
1410
|
// 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.
|
|
1411
|
+
if (this.#parent === undefined || this.#registry.getById(binding.identifier) !== undefined) {
|
|
1182
1412
|
return this;
|
|
1183
1413
|
}
|
|
1184
1414
|
for (let current = this.#parent; current !== undefined; current = current.#parent) {
|
|
1185
|
-
if (current.#registry.getById(binding.
|
|
1415
|
+
if (current.#registry.getById(binding.identifier) !== undefined) {
|
|
1186
1416
|
return current;
|
|
1187
1417
|
}
|
|
1188
1418
|
}
|
|
@@ -1196,7 +1426,7 @@ export class DependencyResolver {
|
|
|
1196
1426
|
if (existing !== undefined) {
|
|
1197
1427
|
return existing;
|
|
1198
1428
|
}
|
|
1199
|
-
const frame = buildResolutionFrame(tokenName(binding.token), binding.scope, binding.
|
|
1429
|
+
const frame = buildResolutionFrame(tokenName(binding.token), binding.scope, binding.identifier, binding.kind, binding.slot);
|
|
1200
1430
|
binding.frame = frame;
|
|
1201
1431
|
return frame;
|
|
1202
1432
|
}
|
|
@@ -1234,6 +1464,17 @@ export class DependencyResolver {
|
|
|
1234
1464
|
return created;
|
|
1235
1465
|
}
|
|
1236
1466
|
}
|
|
1467
|
+
/** A constant whose value is its answer on every read: no own hook, and the caller has ruled out container hooks. */
|
|
1468
|
+
// A cached singleton reads like a constant until a registry change evicts it, which also drops the memo.
|
|
1469
|
+
function isStableCollectionMember(binding) {
|
|
1470
|
+
if (binding.kind === "alias" || binding.activationHook !== undefined) {
|
|
1471
|
+
return false;
|
|
1472
|
+
}
|
|
1473
|
+
return binding.kind === "constant" || (binding.scope === "singleton" && binding.instance !== NO_INSTANCE);
|
|
1474
|
+
}
|
|
1475
|
+
function stableMemberValue(binding) {
|
|
1476
|
+
return binding.kind === "constant" ? binding.value : binding.instance;
|
|
1477
|
+
}
|
|
1237
1478
|
function anyPredicate(bindings) {
|
|
1238
1479
|
for (let index = 0; index < bindings.length; index += 1) {
|
|
1239
1480
|
if (bindings[index].predicate !== undefined) {
|