@codefast/di 0.5.0-canary.8 → 0.5.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 +387 -0
- package/README.md +3 -1
- package/dist/binding.d.ts +41 -22
- package/dist/binding.d.ts.map +1 -1
- package/dist/binding.js +11 -0
- package/dist/binding.js.map +1 -1
- package/dist/container/binding-builders.d.ts.map +1 -1
- package/dist/container/binding-builders.js +8 -5
- package/dist/container/binding-builders.js.map +1 -1
- package/dist/container/container.d.ts.map +1 -1
- package/dist/container/container.js +67 -110
- package/dist/container/container.js.map +1 -1
- package/dist/errors.d.ts +10 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +13 -0
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +1 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/introspection/inspector.d.ts +0 -1
- package/dist/introspection/inspector.d.ts.map +1 -1
- package/dist/introspection/inspector.js +2 -7
- package/dist/introspection/inspector.js.map +1 -1
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +46 -33
- package/dist/registry.js.map +1 -1
- package/dist/resolution/activation-need.d.ts +4 -2
- package/dist/resolution/activation-need.d.ts.map +1 -1
- package/dist/resolution/activation-need.js +15 -11
- package/dist/resolution/activation-need.js.map +1 -1
- package/dist/resolution/binding-lookup-cache.d.ts +1 -1
- package/dist/resolution/binding-lookup-cache.d.ts.map +1 -1
- package/dist/resolution/binding-lookup-cache.js +18 -2
- package/dist/resolution/binding-lookup-cache.js.map +1 -1
- package/dist/resolution/binding-scope.d.ts +5 -2
- package/dist/resolution/binding-scope.d.ts.map +1 -1
- package/dist/resolution/binding-scope.js +6 -17
- package/dist/resolution/binding-scope.js.map +1 -1
- package/dist/resolution/binding-select.d.ts +8 -1
- package/dist/resolution/binding-select.d.ts.map +1 -1
- package/dist/resolution/binding-select.js +13 -32
- package/dist/resolution/binding-select.js.map +1 -1
- package/dist/resolution/diagnostics.d.ts +2 -2
- package/dist/resolution/diagnostics.d.ts.map +1 -1
- package/dist/resolution/environment.d.ts +47 -21
- package/dist/resolution/environment.d.ts.map +1 -1
- package/dist/resolution/environment.js +134 -31
- package/dist/resolution/environment.js.map +1 -1
- package/dist/resolution/instantiation-plan.d.ts.map +1 -1
- package/dist/resolution/instantiation-plan.js +1 -1
- package/dist/resolution/instantiation-plan.js.map +1 -1
- package/dist/resolution/lifecycle.d.ts.map +1 -1
- package/dist/resolution/lifecycle.js +46 -53
- package/dist/resolution/lifecycle.js.map +1 -1
- package/dist/resolution/resolution-path.d.ts +82 -6
- package/dist/resolution/resolution-path.d.ts.map +1 -1
- package/dist/resolution/resolution-path.js +66 -8
- package/dist/resolution/resolution-path.js.map +1 -1
- package/dist/resolution/resolve-options.d.ts +41 -4
- package/dist/resolution/resolve-options.d.ts.map +1 -1
- package/dist/resolution/resolve-options.js +25 -1
- package/dist/resolution/resolve-options.js.map +1 -1
- package/dist/resolution/resolver.d.ts +29 -11
- package/dist/resolution/resolver.d.ts.map +1 -1
- package/dist/resolution/resolver.js +417 -564
- package/dist/resolution/resolver.js.map +1 -1
- package/dist/resolution/scope.d.ts +4 -0
- package/dist/resolution/scope.d.ts.map +1 -1
- package/dist/resolution/scope.js +8 -0
- package/dist/resolution/scope.js.map +1 -1
- package/package.json +5 -11
- package/src/binding.ts +46 -23
- package/src/container/binding-builders.ts +8 -5
- package/src/container/container.ts +72 -119
- package/src/errors.ts +17 -0
- package/src/index.ts +3 -1
- package/src/introspection/inspector.ts +2 -8
- package/src/registry.ts +52 -35
- package/src/resolution/activation-need.ts +18 -14
- package/src/resolution/binding-lookup-cache.ts +18 -2
- package/src/resolution/binding-scope.ts +6 -17
- package/src/resolution/binding-select.ts +14 -35
- package/src/resolution/diagnostics.ts +2 -2
- package/src/resolution/environment.ts +181 -35
- package/src/resolution/instantiation-plan.ts +5 -5
- package/src/resolution/lifecycle.ts +55 -53
- package/src/resolution/resolution-path.ts +119 -25
- package/src/resolution/resolve-options.ts +51 -4
- package/src/resolution/resolver.ts +582 -785
- package/src/resolution/scope.ts +10 -0
|
@@ -2,15 +2,16 @@ import { NO_INSTANCE } from "#/binding";
|
|
|
2
2
|
import { AsyncActivationError, AsyncResolutionError, CircularDependencyError, InternalError, MissingMetadataError, MissingScopeContextError, NoMatchingBindingError, TokenNotBoundError, } from "#/errors";
|
|
3
3
|
import { ActivationNeedCache } from "#/resolution/activation-need";
|
|
4
4
|
import { BindingLookupCache } from "#/resolution/binding-lookup-cache";
|
|
5
|
-
import { selectAllBindings, selectBinding } from "#/resolution/binding-select";
|
|
5
|
+
import { matchesSlot, selectAllBindings, selectBinding } from "#/resolution/binding-select";
|
|
6
6
|
import { ClassIntrospector } from "#/resolution/class-introspector";
|
|
7
|
-
import { buildResolutionFrame, DefaultResolutionContext } from "#/resolution/environment";
|
|
7
|
+
import { AsyncCascadeContext, AsyncLevelContext, buildResolutionFrame, DefaultResolutionContext, } from "#/resolution/environment";
|
|
8
8
|
import { InstantiationPlanCompiler, PLAN_RETRY } from "#/resolution/instantiation-plan";
|
|
9
|
-
import { enterResolutionPath,
|
|
10
|
-
import { injectionSlotToResolveOptions } from "#/resolution/resolve-options";
|
|
9
|
+
import { branchDepthOf, enterResolutionPath, extendResolutionBranch, extendResolutionStackBranch, ROOT_BRANCH, UNOWNED_BRANCH, } from "#/resolution/resolution-path";
|
|
10
|
+
import { injectionSlotToResolveOptions, isNameOnlyOptions, singleTagOnlyOf } from "#/resolution/resolve-options";
|
|
11
11
|
import { tokenName } from "#/token";
|
|
12
12
|
const EMPTY_STRING_LIST = [];
|
|
13
13
|
const EMPTY_FRAME_LIST = [];
|
|
14
|
+
const EMPTY_PARAM_LIST = [];
|
|
14
15
|
const ROOT_CONSTRAINT_CONTEXT = {
|
|
15
16
|
resolutionPath: EMPTY_STRING_LIST,
|
|
16
17
|
resolutionStack: EMPTY_FRAME_LIST,
|
|
@@ -23,9 +24,22 @@ const ROOT_CONSTRAINT_CONTEXT = {
|
|
|
23
24
|
*/
|
|
24
25
|
export class DependencyResolver {
|
|
25
26
|
#syncResolutionContextPool = [];
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
/**
|
|
28
|
+
* The pair a top-level **sync** resolve reuses instead of minting two arrays per call.
|
|
29
|
+
*
|
|
30
|
+
* @remarks Read directly rather than through an accessor returning both: a shallow resolve is one
|
|
31
|
+
* top-level call, so a call and an object literal there are not amortised over anything. Every sync
|
|
32
|
+
* lane pops what it pushes, so `rootStack.length === 0` means no resolve holds the pair; async
|
|
33
|
+
* appends without popping and mints its own. Keeping the pair stable is also what lets a pooled
|
|
34
|
+
* context skip storing pointers it already holds.
|
|
35
|
+
*/
|
|
36
|
+
rootPath = [];
|
|
37
|
+
rootStack = [];
|
|
38
|
+
// The open synchronous factory cascade: its arrays are the ancestor chain, and they are balanced
|
|
39
|
+
// because synchronous code does not interleave.
|
|
40
|
+
#cascadePath = [];
|
|
41
|
+
#cascadeStack = [];
|
|
42
|
+
#cascadeContext;
|
|
29
43
|
// Compiled plans; `null` marks a binding as unplannable under the current cache versions.
|
|
30
44
|
#classPlanByBindingId = new Map();
|
|
31
45
|
#classPlanRegistryVersion = -1;
|
|
@@ -46,7 +60,7 @@ export class DependencyResolver {
|
|
|
46
60
|
this.#parent = parent;
|
|
47
61
|
this.#lookup = new BindingLookupCache(registry, this, parent === undefined ? undefined : parent.#lookup);
|
|
48
62
|
this.#classes = new ClassIntrospector(metadataReader, container);
|
|
49
|
-
this.#activation = new ActivationNeedCache(lifecycle, this.#classes);
|
|
63
|
+
this.#activation = new ActivationNeedCache(lifecycle, this.#classes, registry);
|
|
50
64
|
}
|
|
51
65
|
/** Structural counts for {@link RESOLUTION_DIAGNOSTICS}; see `resolution/diagnostics.ts`. */
|
|
52
66
|
describeCaches() {
|
|
@@ -57,7 +71,6 @@ export class DependencyResolver {
|
|
|
57
71
|
}
|
|
58
72
|
}
|
|
59
73
|
return {
|
|
60
|
-
asyncContextPoolSize: this.#asyncChainContextPool.length,
|
|
61
74
|
compiledPlanCount,
|
|
62
75
|
syncContextPoolSize: this.#syncResolutionContextPool.length,
|
|
63
76
|
};
|
|
@@ -70,39 +83,33 @@ export class DependencyResolver {
|
|
|
70
83
|
return { binding: fastDefaultBinding, owner: this };
|
|
71
84
|
}
|
|
72
85
|
}
|
|
73
|
-
if (
|
|
86
|
+
else if (isNameOnlyOptions(options)) {
|
|
74
87
|
const namedBinding = this.#registry.getSimpleNamed(token, options.name);
|
|
75
88
|
if (namedBinding !== undefined &&
|
|
76
89
|
this.#matchesBindingFast(namedBinding, options, resolutionPath, resolutionStack)) {
|
|
77
90
|
return { binding: namedBinding, owner: this };
|
|
78
91
|
}
|
|
79
92
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
return { binding: tagged, owner: this };
|
|
93
|
+
else {
|
|
94
|
+
const singleTag = singleTagOnlyOf(options);
|
|
95
|
+
if (singleTag !== undefined) {
|
|
96
|
+
const tagged = this.#registry.getSimpleTagged(token, singleTag[0], singleTag[1]);
|
|
97
|
+
if (tagged !== undefined && matchesIndexedTagValue(tagged, singleTag[1])) {
|
|
98
|
+
return { binding: tagged, owner: this };
|
|
99
|
+
}
|
|
88
100
|
}
|
|
89
101
|
}
|
|
90
102
|
const bindings = this.#registry.getAll(token);
|
|
91
103
|
if (bindings.length > 0) {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
const ctx = this.#makeConstraintContext(resolutionPath, resolutionStack, options);
|
|
103
|
-
const binding = selectBinding(bindings, options, ctx, this.#getTokenName(token));
|
|
104
|
-
if (binding !== undefined) {
|
|
105
|
-
return { binding, owner: this };
|
|
104
|
+
// A lone candidate is its own selection: matching it is the whole decision, with no
|
|
105
|
+
// specificity to weigh and no ambiguity to report.
|
|
106
|
+
const selected = bindings.length === 1
|
|
107
|
+
? this.#matchesBindingFast(bindings[0], options, resolutionPath, resolutionStack)
|
|
108
|
+
? bindings[0]
|
|
109
|
+
: undefined
|
|
110
|
+
: selectBinding(bindings, options, this.#makeConstraintContext(resolutionPath, resolutionStack, options), tokenName(token));
|
|
111
|
+
if (selected !== undefined) {
|
|
112
|
+
return { binding: selected, owner: this };
|
|
106
113
|
}
|
|
107
114
|
}
|
|
108
115
|
if (this.#parent !== undefined) {
|
|
@@ -110,6 +117,36 @@ export class DependencyResolver {
|
|
|
110
117
|
}
|
|
111
118
|
return undefined;
|
|
112
119
|
}
|
|
120
|
+
/**
|
|
121
|
+
* The binding a token resolves to with alias hops followed, or a diagnostic throw.
|
|
122
|
+
*
|
|
123
|
+
* @remarks Alias hops are followed iteratively with exact cycle detection — a revisited alias
|
|
124
|
+
* token raises {@link CircularDependencyError} instead of overflowing the call stack.
|
|
125
|
+
*/
|
|
126
|
+
#requireBinding(token, options, resolutionPath, resolutionStack) {
|
|
127
|
+
let currentToken = token;
|
|
128
|
+
let visitedAliasTokens;
|
|
129
|
+
let found = this.#findBinding(currentToken, options, resolutionPath, resolutionStack);
|
|
130
|
+
while (found !== undefined && found.binding.kind === "alias") {
|
|
131
|
+
const target = found.binding.target;
|
|
132
|
+
visitedAliasTokens ??= new Set([currentToken]);
|
|
133
|
+
if (visitedAliasTokens.has(target)) {
|
|
134
|
+
throw new CircularDependencyError([...visitedAliasTokens, target].map((entry) => tokenName(entry)));
|
|
135
|
+
}
|
|
136
|
+
visitedAliasTokens.add(target);
|
|
137
|
+
currentToken = target;
|
|
138
|
+
found = this.#findBinding(currentToken, options, resolutionPath, resolutionStack);
|
|
139
|
+
}
|
|
140
|
+
if (found === undefined) {
|
|
141
|
+
// Thrown here rather than from a helper: the error captures this stack, and an error path is
|
|
142
|
+
// dominated by that capture. Bindings under the token mean the request matched none of them.
|
|
143
|
+
if (this.#registry.getAll(currentToken).length > 0) {
|
|
144
|
+
throw new NoMatchingBindingError(tokenName(currentToken), options ?? {}, this.#registry.availableSlotStrings(currentToken));
|
|
145
|
+
}
|
|
146
|
+
throw new TokenNotBoundError(tokenName(currentToken));
|
|
147
|
+
}
|
|
148
|
+
return found;
|
|
149
|
+
}
|
|
113
150
|
/**
|
|
114
151
|
* Binding lookup aligned with `resolve` — used by `Container.validate` without instantiating.
|
|
115
152
|
*/
|
|
@@ -120,15 +157,7 @@ export class DependencyResolver {
|
|
|
120
157
|
* Mirrors {@link DependencyResolver.resolveAll} candidate selection only (no instantiation).
|
|
121
158
|
*/
|
|
122
159
|
peekCandidateBindingsForValidate(token, options) {
|
|
123
|
-
|
|
124
|
-
return this.#getSimpleNamedBindingsFromChain(token, options.name);
|
|
125
|
-
}
|
|
126
|
-
const allBindings = this.#getAllBindingsFromChain(token);
|
|
127
|
-
if (allBindings.length === 0) {
|
|
128
|
-
return [];
|
|
129
|
-
}
|
|
130
|
-
const ctx = this.#makeConstraintContext([], [], options);
|
|
131
|
-
return selectAllBindings(allBindings, options, ctx);
|
|
160
|
+
return this.#candidateBindings(token, options, [], []);
|
|
132
161
|
}
|
|
133
162
|
// ── Sync resolve ───────────────────────────────────────────────────────────
|
|
134
163
|
resolveFromContext(token, resolutionPath, resolutionStack) {
|
|
@@ -145,12 +174,7 @@ export class DependencyResolver {
|
|
|
145
174
|
return this.#resolveDefaultEntry(entry.binding, entry.owner, resolutionPath, resolutionStack);
|
|
146
175
|
}
|
|
147
176
|
#resolveDefaultEntry(binding, owner, resolutionPath, resolutionStack) {
|
|
148
|
-
|
|
149
|
-
binding.onActivation === undefined &&
|
|
150
|
-
(this.#lifecycle.activationVersion === 0 || !this.#lifecycle.hasActivationHandlers(binding.token))) {
|
|
151
|
-
return binding.value;
|
|
152
|
-
}
|
|
153
|
-
const scope = binding.scope ?? "transient";
|
|
177
|
+
const scope = binding.scope;
|
|
154
178
|
if (scope === "transient") {
|
|
155
179
|
if (binding.kind === "dynamic") {
|
|
156
180
|
const containerHooks = this.#lifecycle.activationVersion === 0 ? undefined : this.#lifecycle.activationHandlersFor(binding.token);
|
|
@@ -168,6 +192,10 @@ export class DependencyResolver {
|
|
|
168
192
|
}
|
|
169
193
|
}
|
|
170
194
|
else if (scope === "singleton") {
|
|
195
|
+
// A constant is a singleton that is already its own instance.
|
|
196
|
+
if (this.#isPlainConstant(binding)) {
|
|
197
|
+
return binding.value;
|
|
198
|
+
}
|
|
171
199
|
const cachedSingleton = binding.instance;
|
|
172
200
|
if (cachedSingleton !== NO_INSTANCE) {
|
|
173
201
|
return cachedSingleton;
|
|
@@ -177,11 +205,9 @@ export class DependencyResolver {
|
|
|
177
205
|
}
|
|
178
206
|
}
|
|
179
207
|
else {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
if (this.#scope.hasScoped(binding.id)) {
|
|
184
|
-
return this.#scope.getScoped(binding.id);
|
|
208
|
+
const cachedScoped = this.#readScoped(binding);
|
|
209
|
+
if (cachedScoped !== SCOPED_MISS) {
|
|
210
|
+
return cachedScoped;
|
|
185
211
|
}
|
|
186
212
|
}
|
|
187
213
|
return this.#resolveBinding(binding, undefined, resolutionPath, resolutionStack);
|
|
@@ -190,9 +216,15 @@ export class DependencyResolver {
|
|
|
190
216
|
// generic #resolveBinding path (guard, frame, ctx, per-binding then container hooks) with
|
|
191
217
|
// the kind/activation dispatch resolved statically.
|
|
192
218
|
#resolveTransientDynamicActivatedSync(binding, containerHooks, resolutionPath, resolutionStack) {
|
|
219
|
+
// Same O(1) cycle guard as the unhooked lane: this is still one sync call stack, so the flag
|
|
220
|
+
// *is* exact path membership — see ARCHITECTURE.md.
|
|
193
221
|
const frame = this.#getResolutionFrame(binding);
|
|
194
222
|
const tokenDisplayName = frame.tokenName;
|
|
195
|
-
|
|
223
|
+
if (binding.inFlight) {
|
|
224
|
+
throw new CircularDependencyError([...resolutionPath, tokenDisplayName]);
|
|
225
|
+
}
|
|
226
|
+
binding.inFlight = true;
|
|
227
|
+
resolutionPath.push(tokenDisplayName);
|
|
196
228
|
resolutionStack.push(frame);
|
|
197
229
|
try {
|
|
198
230
|
const resolutionCtx = this.#acquireSyncResolutionContext(resolutionPath, resolutionStack, undefined);
|
|
@@ -222,7 +254,7 @@ export class DependencyResolver {
|
|
|
222
254
|
finally {
|
|
223
255
|
resolutionStack.pop();
|
|
224
256
|
resolutionPath.pop();
|
|
225
|
-
|
|
257
|
+
binding.inFlight = false;
|
|
226
258
|
}
|
|
227
259
|
}
|
|
228
260
|
#getInstantiationPlan(binding) {
|
|
@@ -245,7 +277,7 @@ export class DependencyResolver {
|
|
|
245
277
|
this.#classPlanByBindingId.set(binding.id, compiled);
|
|
246
278
|
return compiled;
|
|
247
279
|
}
|
|
248
|
-
// Compiler behind #
|
|
280
|
+
// Compiler behind #getInstantiationPlan — cold path, so the host indirection costs nothing hot.
|
|
249
281
|
#planCompiler = new InstantiationPlanCompiler({
|
|
250
282
|
hasActivationHandlers: (token) => this.#lifecycle.hasActivationHandlers(token),
|
|
251
283
|
knownPostConstruct: (target) => this.#classes.knownPostConstruct(target),
|
|
@@ -256,7 +288,7 @@ export class DependencyResolver {
|
|
|
256
288
|
return entry === null ? null : { binding: entry.binding };
|
|
257
289
|
},
|
|
258
290
|
getResolutionFrame: (binding) => this.#getResolutionFrame(binding),
|
|
259
|
-
// Dispatches exactly as #
|
|
291
|
+
// Dispatches exactly as #resolveDep does, so an escaped dep is indistinguishable
|
|
260
292
|
// from the same dep on a fully interpreted resolve.
|
|
261
293
|
resolveEscaped: (token, options, arity, resolutionPath, resolutionStack) => {
|
|
262
294
|
if (arity === "all") {
|
|
@@ -274,113 +306,63 @@ export class DependencyResolver {
|
|
|
274
306
|
resolve(token, options, resolutionPath, resolutionStack) {
|
|
275
307
|
// Name-only fast lane: memoized lookup, dispatching just the shapes whose
|
|
276
308
|
// semantics involve no resolution context (constants, cached singletons).
|
|
277
|
-
if (options !== undefined &&
|
|
278
|
-
options.name !== undefined &&
|
|
279
|
-
options.tag === undefined &&
|
|
280
|
-
(options.tags === undefined || options.tags.length === 0)) {
|
|
309
|
+
if (options !== undefined && isNameOnlyOptions(options)) {
|
|
281
310
|
const namedEntry = this.#lookup.namedEntry(token, options.name);
|
|
282
311
|
if (namedEntry !== null) {
|
|
283
312
|
const namedBinding = namedEntry.binding;
|
|
284
|
-
if (namedBinding
|
|
285
|
-
namedBinding.onActivation === undefined &&
|
|
286
|
-
(this.#lifecycle.activationVersion === 0 || !this.#lifecycle.hasActivationHandlers(namedBinding.token))) {
|
|
313
|
+
if (this.#isPlainConstant(namedBinding)) {
|
|
287
314
|
return namedBinding.value;
|
|
288
315
|
}
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
if (namedBinding.instance !== NO_INSTANCE) {
|
|
292
|
-
return namedBinding.instance;
|
|
293
|
-
}
|
|
316
|
+
if (namedBinding.scope === "singleton" && namedBinding.instance !== NO_INSTANCE) {
|
|
317
|
+
return namedBinding.instance;
|
|
294
318
|
}
|
|
295
319
|
// Everything else keeps the full path (context, activation, guards).
|
|
296
320
|
}
|
|
297
321
|
}
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
// Follow aliases iteratively with exact cycle detection — a revisited alias
|
|
302
|
-
// token throws CircularDependencyError instead of overflowing the call stack.
|
|
303
|
-
while (found !== undefined && found.binding.kind === "alias") {
|
|
304
|
-
const target = found.binding.target;
|
|
305
|
-
visitedAliasTokens ??= new Set([currentToken]);
|
|
306
|
-
if (visitedAliasTokens.has(target)) {
|
|
307
|
-
throw new CircularDependencyError([...visitedAliasTokens, target].map((entry) => tokenName(entry)));
|
|
308
|
-
}
|
|
309
|
-
visitedAliasTokens.add(target);
|
|
310
|
-
currentToken = target;
|
|
311
|
-
found = this.#findBinding(currentToken, options, resolutionPath, resolutionStack);
|
|
312
|
-
}
|
|
313
|
-
if (found === undefined) {
|
|
314
|
-
const ownBindings = this.#registry.getAll(currentToken);
|
|
315
|
-
if (ownBindings.length > 0) {
|
|
316
|
-
throw new NoMatchingBindingError(this.#getTokenName(currentToken), options ?? {}, this.#getAvailableSlots(currentToken));
|
|
317
|
-
}
|
|
318
|
-
throw new TokenNotBoundError(this.#getTokenName(currentToken));
|
|
319
|
-
}
|
|
320
|
-
const { binding, owner } = found;
|
|
321
|
-
const scope = binding.scope ?? "transient";
|
|
322
|
-
// Singleton from a parent resolver: delegate so the parent caches it correctly
|
|
323
|
-
if (scope === "singleton" && owner !== this) {
|
|
322
|
+
const { binding, owner } = this.#requireBinding(token, options, resolutionPath, resolutionStack);
|
|
323
|
+
// A singleton owned by a parent resolver is resolved there, so the parent caches it.
|
|
324
|
+
if (binding.scope === "singleton" && owner !== this) {
|
|
324
325
|
return owner.#resolveBinding(binding, options, resolutionPath, resolutionStack);
|
|
325
326
|
}
|
|
326
|
-
// Scoped/transient (or own singleton): resolve with this resolver's container/scope
|
|
327
327
|
return this.#resolveBinding(binding, options, resolutionPath, resolutionStack);
|
|
328
328
|
}
|
|
329
329
|
#resolveBinding(binding, options, resolutionPath, resolutionStack) {
|
|
330
|
-
if (binding
|
|
331
|
-
binding.onActivation === undefined &&
|
|
332
|
-
(this.#lifecycle.activationVersion === 0 || !this.#lifecycle.hasActivationHandlers(binding.token))) {
|
|
330
|
+
if (this.#isPlainConstant(binding)) {
|
|
333
331
|
return binding.value;
|
|
334
332
|
}
|
|
335
|
-
const scope = binding.scope
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
}
|
|
340
|
-
// Scoped cache check
|
|
341
|
-
if (scope === "scoped") {
|
|
342
|
-
if (!this.#scope.isChild) {
|
|
343
|
-
throw new MissingScopeContextError(this.#getTokenName(binding.token));
|
|
333
|
+
const scope = binding.scope;
|
|
334
|
+
if (scope === "singleton") {
|
|
335
|
+
if (binding.instance !== NO_INSTANCE) {
|
|
336
|
+
return binding.instance;
|
|
344
337
|
}
|
|
345
|
-
|
|
346
|
-
|
|
338
|
+
}
|
|
339
|
+
else if (scope === "scoped") {
|
|
340
|
+
const cachedScoped = this.#readScoped(binding);
|
|
341
|
+
if (cachedScoped !== SCOPED_MISS) {
|
|
342
|
+
return cachedScoped;
|
|
347
343
|
}
|
|
348
344
|
}
|
|
349
345
|
const frame = this.#getResolutionFrame(binding);
|
|
350
346
|
const tokenDisplayName = frame.tokenName;
|
|
351
|
-
const resolutionSet = enterResolutionPath(resolutionPath, tokenDisplayName
|
|
347
|
+
const resolutionSet = enterResolutionPath(resolutionPath, tokenDisplayName);
|
|
352
348
|
resolutionStack.push(frame);
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
349
|
+
try {
|
|
350
|
+
const needsActivation = this.#activation.needsActivation(binding);
|
|
351
|
+
if (!needsActivation && scope === "transient" && binding.kind === "dynamic") {
|
|
352
|
+
const resolutionCtx = this.#acquireSyncResolutionContext(resolutionPath, resolutionStack, options);
|
|
357
353
|
const dynamicResult = binding.factory(resolutionCtx);
|
|
358
354
|
if (dynamicResult instanceof Promise) {
|
|
359
|
-
throw new AsyncResolutionError(
|
|
355
|
+
throw new AsyncResolutionError(tokenDisplayName, tokenDisplayName);
|
|
360
356
|
}
|
|
361
|
-
resolutionStack.pop();
|
|
362
|
-
resolutionPath.pop();
|
|
363
|
-
resolutionSet?.delete(tokenDisplayName);
|
|
364
357
|
return dynamicResult;
|
|
365
358
|
}
|
|
366
|
-
|
|
367
|
-
resolutionStack.pop();
|
|
368
|
-
resolutionPath.pop();
|
|
369
|
-
resolutionSet?.delete(tokenDisplayName);
|
|
370
|
-
throw error;
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
try {
|
|
374
|
-
const needsResolutionContext = needsActivation || this.#requiresResolutionContext(binding);
|
|
375
|
-
const resolutionCtx = needsResolutionContext
|
|
359
|
+
const resolutionCtx = needsActivation || requiresResolutionContext(binding)
|
|
376
360
|
? this.#acquireSyncResolutionContext(resolutionPath, resolutionStack, options)
|
|
377
361
|
: undefined;
|
|
378
362
|
const instance = this.#instantiateSync(binding, resolutionCtx, resolutionPath, resolutionStack);
|
|
379
|
-
const
|
|
380
|
-
const activated = shouldActivate
|
|
363
|
+
const activated = this.#activation.refreshAfterFirstInstantiation(binding, needsActivation)
|
|
381
364
|
? this.#lifecycle.runActivationSync(resolutionCtx, binding, instance, this.#metadataReader)
|
|
382
365
|
: instance;
|
|
383
|
-
// Cache by scope
|
|
384
366
|
if (scope === "singleton") {
|
|
385
367
|
this.#scope.setSingleton(binding, activated);
|
|
386
368
|
}
|
|
@@ -412,12 +394,11 @@ export class DependencyResolver {
|
|
|
412
394
|
case "dynamic-async":
|
|
413
395
|
throw new AsyncResolutionError(tokenName(binding.token), tokenName(binding.token));
|
|
414
396
|
case "class": {
|
|
415
|
-
const deps = this.#
|
|
416
|
-
|
|
417
|
-
return instance;
|
|
397
|
+
const deps = this.#resolveDeps(this.#constructorParams(binding.target), resolutionPath, resolutionStack);
|
|
398
|
+
return this.#classes.instantiate(binding.target, deps);
|
|
418
399
|
}
|
|
419
400
|
case "resolved": {
|
|
420
|
-
const deps = this.#
|
|
401
|
+
const deps = this.#resolveDeps(binding.deps, resolutionPath, resolutionStack);
|
|
421
402
|
const factoryResult = binding.factory(...deps);
|
|
422
403
|
if (factoryResult instanceof Promise) {
|
|
423
404
|
throw new AsyncResolutionError(tokenName(binding.token), tokenName(binding.token));
|
|
@@ -430,70 +411,51 @@ export class DependencyResolver {
|
|
|
430
411
|
throw new InternalError("alias should have been followed before instantiation");
|
|
431
412
|
}
|
|
432
413
|
}
|
|
433
|
-
|
|
414
|
+
/**
|
|
415
|
+
* The parameters a class binding injects.
|
|
416
|
+
*
|
|
417
|
+
* @remarks A class the metadata reader knows nothing about is constructible only if it declares
|
|
418
|
+
* no parameters; anything else is a missing `@injectable()`.
|
|
419
|
+
*/
|
|
420
|
+
#constructorParams(target) {
|
|
434
421
|
const meta = this.#classes.constructorMetadata(target);
|
|
435
|
-
if (meta
|
|
436
|
-
|
|
437
|
-
return [];
|
|
438
|
-
}
|
|
439
|
-
throw new MissingMetadataError(target.name);
|
|
422
|
+
if (meta !== undefined) {
|
|
423
|
+
return meta.params;
|
|
440
424
|
}
|
|
441
|
-
if (
|
|
425
|
+
if (target.length === 0) {
|
|
426
|
+
return EMPTY_PARAM_LIST;
|
|
427
|
+
}
|
|
428
|
+
throw new MissingMetadataError(target.name);
|
|
429
|
+
}
|
|
430
|
+
// One dispatch table for both dependency sources — constructor params and `toResolved`
|
|
431
|
+
// descriptors declare the same four things.
|
|
432
|
+
#resolveDeps(deps, resolutionPath, resolutionStack) {
|
|
433
|
+
const count = deps.length;
|
|
434
|
+
if (count === 0) {
|
|
442
435
|
return [];
|
|
443
436
|
}
|
|
444
|
-
if (
|
|
445
|
-
|
|
446
|
-
const paramOptions = injectionSlotToResolveOptions(param);
|
|
447
|
-
if (param.multi) {
|
|
448
|
-
return [this.resolveAll(param.token, paramOptions, resolutionPath, resolutionStack)];
|
|
449
|
-
}
|
|
450
|
-
if (param.optional) {
|
|
451
|
-
return [this.resolveOptional(param.token, paramOptions, resolutionPath, resolutionStack)];
|
|
452
|
-
}
|
|
453
|
-
if (paramOptions === undefined) {
|
|
454
|
-
return [this.resolveFromContext(param.token, resolutionPath, resolutionStack)];
|
|
455
|
-
}
|
|
456
|
-
return [this.resolve(param.token, paramOptions, resolutionPath, resolutionStack)];
|
|
457
|
-
}
|
|
458
|
-
const deps = new Array(meta.params.length);
|
|
459
|
-
for (let index = 0; index < meta.params.length; index += 1) {
|
|
460
|
-
const param = meta.params[index];
|
|
461
|
-
const paramOptions = injectionSlotToResolveOptions(param);
|
|
462
|
-
if (param.multi) {
|
|
463
|
-
deps[index] = this.resolveAll(param.token, paramOptions, resolutionPath, resolutionStack);
|
|
464
|
-
continue;
|
|
465
|
-
}
|
|
466
|
-
if (param.optional) {
|
|
467
|
-
deps[index] = this.resolveOptional(param.token, paramOptions, resolutionPath, resolutionStack);
|
|
468
|
-
continue;
|
|
469
|
-
}
|
|
470
|
-
deps[index] =
|
|
471
|
-
paramOptions === undefined
|
|
472
|
-
? this.resolveFromContext(param.token, resolutionPath, resolutionStack)
|
|
473
|
-
: this.resolve(param.token, paramOptions, resolutionPath, resolutionStack);
|
|
437
|
+
if (count === 1) {
|
|
438
|
+
return [this.#resolveDep(deps[0], resolutionPath, resolutionStack)];
|
|
474
439
|
}
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
const resolved = new Array(deps.length);
|
|
479
|
-
for (let index = 0; index < deps.length; index += 1) {
|
|
480
|
-
const dep = deps[index];
|
|
481
|
-
const depOptions = injectionSlotToResolveOptions(dep);
|
|
482
|
-
if (dep.multi) {
|
|
483
|
-
resolved[index] = this.resolveAll(dep.token, depOptions, resolutionPath, resolutionStack);
|
|
484
|
-
continue;
|
|
485
|
-
}
|
|
486
|
-
if (dep.optional) {
|
|
487
|
-
resolved[index] = this.resolveOptional(dep.token, depOptions, resolutionPath, resolutionStack);
|
|
488
|
-
continue;
|
|
489
|
-
}
|
|
490
|
-
resolved[index] =
|
|
491
|
-
depOptions === undefined
|
|
492
|
-
? this.resolveFromContext(dep.token, resolutionPath, resolutionStack)
|
|
493
|
-
: this.resolve(dep.token, depOptions, resolutionPath, resolutionStack);
|
|
440
|
+
const resolved = new Array(count);
|
|
441
|
+
for (let index = 0; index < count; index += 1) {
|
|
442
|
+
resolved[index] = this.#resolveDep(deps[index], resolutionPath, resolutionStack);
|
|
494
443
|
}
|
|
495
444
|
return resolved;
|
|
496
445
|
}
|
|
446
|
+
#resolveDep(dep, resolutionPath, resolutionStack) {
|
|
447
|
+
const options = injectionSlotToResolveOptions(dep);
|
|
448
|
+
if (dep.multi) {
|
|
449
|
+
return this.resolveAll(dep.token, options, resolutionPath, resolutionStack);
|
|
450
|
+
}
|
|
451
|
+
if (dep.optional) {
|
|
452
|
+
return this.resolveOptional(dep.token, options, resolutionPath, resolutionStack);
|
|
453
|
+
}
|
|
454
|
+
if (options === undefined) {
|
|
455
|
+
return this.resolveFromContext(dep.token, resolutionPath, resolutionStack);
|
|
456
|
+
}
|
|
457
|
+
return this.resolve(dep.token, options, resolutionPath, resolutionStack);
|
|
458
|
+
}
|
|
497
459
|
resolveOptional(token, options, resolutionPath, resolutionStack) {
|
|
498
460
|
if (this.#findBinding(token, options, resolutionPath, resolutionStack) === undefined) {
|
|
499
461
|
return undefined;
|
|
@@ -501,31 +463,32 @@ export class DependencyResolver {
|
|
|
501
463
|
return this.resolve(token, options, resolutionPath, resolutionStack);
|
|
502
464
|
}
|
|
503
465
|
resolveAll(token, options, resolutionPath, resolutionStack) {
|
|
504
|
-
|
|
505
|
-
const namedCandidates = this.#getSimpleNamedBindingsFromChain(token, options.name);
|
|
506
|
-
if (namedCandidates.length === 0) {
|
|
507
|
-
return [];
|
|
508
|
-
}
|
|
509
|
-
const resolved = new Array(namedCandidates.length);
|
|
510
|
-
for (let index = 0; index < namedCandidates.length; index += 1) {
|
|
511
|
-
resolved[index] = this.#resolveCandidateSync(namedCandidates[index], options, resolutionPath, resolutionStack);
|
|
512
|
-
}
|
|
513
|
-
return resolved;
|
|
514
|
-
}
|
|
515
|
-
const allBindings = this.#getAllBindingsFromChain(token);
|
|
516
|
-
if (allBindings.length === 0) {
|
|
517
|
-
return [];
|
|
518
|
-
}
|
|
519
|
-
const ctx = this.#makeConstraintContext(resolutionPath, resolutionStack, options);
|
|
520
|
-
const candidates = selectAllBindings(allBindings, options, ctx);
|
|
466
|
+
const candidates = this.#candidateBindings(token, options, resolutionPath, resolutionStack);
|
|
521
467
|
const resolved = new Array(candidates.length);
|
|
522
468
|
for (let index = 0; index < candidates.length; index += 1) {
|
|
523
469
|
resolved[index] = this.#resolveCandidateSync(candidates[index], options, resolutionPath, resolutionStack);
|
|
524
470
|
}
|
|
525
471
|
return resolved;
|
|
526
472
|
}
|
|
473
|
+
/** Every binding in the chain a `resolveAll` request matches, in chain order. */
|
|
474
|
+
#candidateBindings(token, options, resolutionPath, resolutionStack) {
|
|
475
|
+
if (options !== undefined && isNameOnlyOptions(options)) {
|
|
476
|
+
// The name index has matched the slot already, but a hit may still carry a predicate —
|
|
477
|
+
// and that is the selection path's job to evaluate.
|
|
478
|
+
const named = this.#namedBindingsFromChain(token, options.name);
|
|
479
|
+
if (!anyPredicate(named)) {
|
|
480
|
+
return named;
|
|
481
|
+
}
|
|
482
|
+
return selectAllBindings(named, options, this.#makeConstraintContext(resolutionPath, resolutionStack, options));
|
|
483
|
+
}
|
|
484
|
+
const allBindings = this.#allBindingsFromChain(token);
|
|
485
|
+
if (allBindings.length === 0) {
|
|
486
|
+
return allBindings;
|
|
487
|
+
}
|
|
488
|
+
return selectAllBindings(allBindings, options, this.#makeConstraintContext(resolutionPath, resolutionStack, options));
|
|
489
|
+
}
|
|
527
490
|
// ── Async resolve ──────────────────────────────────────────────────────────
|
|
528
|
-
resolveAsyncFromContext(token, resolutionPath, resolutionStack,
|
|
491
|
+
resolveAsyncFromContext(token, resolutionPath, resolutionStack, branchDepth) {
|
|
529
492
|
// Hot lane: own-registry fast default (async chains resolve sibling dynamic bindings).
|
|
530
493
|
// Fall back to the chain-versioned memo only on miss or alias.
|
|
531
494
|
const fastBinding = this.#registry.getFastDefault(token);
|
|
@@ -533,30 +496,25 @@ export class DependencyResolver {
|
|
|
533
496
|
// Inline the dominant chain shape — transient dynamic factory with no activation.
|
|
534
497
|
if ((fastBinding.kind === "dynamic-async" || fastBinding.kind === "dynamic") &&
|
|
535
498
|
fastBinding.scope === "transient" &&
|
|
536
|
-
fastBinding
|
|
537
|
-
|
|
538
|
-
return this.#resolveTransientDynamicAsyncFromContext(fastBinding, resolutionPath, resolutionStack, callerContext);
|
|
499
|
+
!this.#hasAnyActivation(fastBinding)) {
|
|
500
|
+
return this.#resolveTransientDynamicAsyncFromContext(fastBinding, resolutionPath, resolutionStack, branchDepth);
|
|
539
501
|
}
|
|
540
|
-
return this.#resolveAsyncDefaultEntry(fastBinding, this, resolutionPath, resolutionStack,
|
|
502
|
+
return this.#resolveAsyncDefaultEntry(fastBinding, this, resolutionPath, resolutionStack, branchDepth);
|
|
541
503
|
}
|
|
542
504
|
const entry = this.#lookup.defaultEntry(token);
|
|
543
505
|
if (entry === null) {
|
|
544
|
-
return this.resolveAsync(token, undefined, resolutionPath, resolutionStack);
|
|
506
|
+
return this.resolveAsync(token, undefined, resolutionPath, resolutionStack, branchDepth);
|
|
545
507
|
}
|
|
546
|
-
return this.#resolveAsyncDefaultEntry(entry.binding, entry.owner, resolutionPath, resolutionStack,
|
|
508
|
+
return this.#resolveAsyncDefaultEntry(entry.binding, entry.owner, resolutionPath, resolutionStack, branchDepth);
|
|
547
509
|
}
|
|
548
|
-
#resolveAsyncDefaultEntry(binding, owner, resolutionPath, resolutionStack,
|
|
549
|
-
if (binding
|
|
550
|
-
binding.onActivation === undefined &&
|
|
551
|
-
(this.#lifecycle.activationVersion === 0 || !this.#lifecycle.hasActivationHandlers(binding.token))) {
|
|
510
|
+
#resolveAsyncDefaultEntry(binding, owner, resolutionPath, resolutionStack, branchDepth) {
|
|
511
|
+
if (this.#isPlainConstant(binding)) {
|
|
552
512
|
return Promise.resolve(binding.value);
|
|
553
513
|
}
|
|
554
|
-
const scope = binding.scope
|
|
514
|
+
const scope = binding.scope;
|
|
555
515
|
if (scope === "transient") {
|
|
556
|
-
if ((binding.kind === "dynamic" || binding.kind === "dynamic-async") &&
|
|
557
|
-
binding
|
|
558
|
-
(this.#lifecycle.activationVersion === 0 || !this.#lifecycle.hasActivationHandlers(binding.token))) {
|
|
559
|
-
return this.#resolveTransientDynamicAsyncFromContext(binding, resolutionPath, resolutionStack, callerContext);
|
|
516
|
+
if ((binding.kind === "dynamic" || binding.kind === "dynamic-async") && !this.#hasAnyActivation(binding)) {
|
|
517
|
+
return this.#resolveTransientDynamicAsyncFromContext(binding, resolutionPath, resolutionStack, branchDepth);
|
|
560
518
|
}
|
|
561
519
|
}
|
|
562
520
|
else if (scope === "singleton") {
|
|
@@ -564,143 +522,92 @@ export class DependencyResolver {
|
|
|
564
522
|
return Promise.resolve(binding.instance);
|
|
565
523
|
}
|
|
566
524
|
if (owner !== this) {
|
|
567
|
-
return owner.#resolveBindingAsync(binding, undefined, resolutionPath, resolutionStack);
|
|
525
|
+
return owner.#resolveBindingAsync(binding, undefined, resolutionPath, resolutionStack, branchDepth);
|
|
568
526
|
}
|
|
569
527
|
}
|
|
570
|
-
else {
|
|
571
|
-
if (!this.#scope.isChild) {
|
|
572
|
-
return Promise.reject(new MissingScopeContextError(this.#getTokenName(binding.token)));
|
|
573
|
-
}
|
|
528
|
+
else if (this.#scope.isChild) {
|
|
574
529
|
if (this.#scope.hasScoped(binding.id)) {
|
|
575
530
|
return Promise.resolve(this.#scope.getScoped(binding.id));
|
|
576
531
|
}
|
|
577
532
|
}
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
const found = this.#findBinding(token, options, resolutionPath, resolutionStack);
|
|
582
|
-
if (found === undefined) {
|
|
583
|
-
const ownBindings = this.#registry.getAll(token);
|
|
584
|
-
if (ownBindings.length > 0) {
|
|
585
|
-
throw new NoMatchingBindingError(this.#getTokenName(token), options ?? {}, this.#getAvailableSlots(token));
|
|
586
|
-
}
|
|
587
|
-
throw new TokenNotBoundError(this.#getTokenName(token));
|
|
588
|
-
}
|
|
589
|
-
let currentToken = token;
|
|
590
|
-
let visitedAliasTokens;
|
|
591
|
-
let aliasFollowed = found;
|
|
592
|
-
while (aliasFollowed !== undefined && aliasFollowed.binding.kind === "alias") {
|
|
593
|
-
const target = aliasFollowed.binding.target;
|
|
594
|
-
visitedAliasTokens ??= new Set([currentToken]);
|
|
595
|
-
if (visitedAliasTokens.has(target)) {
|
|
596
|
-
throw new CircularDependencyError([...visitedAliasTokens, target].map((entry) => tokenName(entry)));
|
|
597
|
-
}
|
|
598
|
-
visitedAliasTokens.add(target);
|
|
599
|
-
currentToken = target;
|
|
600
|
-
aliasFollowed = this.#findBinding(currentToken, options, resolutionPath, resolutionStack);
|
|
601
|
-
}
|
|
602
|
-
if (aliasFollowed === undefined) {
|
|
603
|
-
const ownBindings = this.#registry.getAll(currentToken);
|
|
604
|
-
if (ownBindings.length > 0) {
|
|
605
|
-
throw new NoMatchingBindingError(this.#getTokenName(currentToken), options ?? {}, this.#getAvailableSlots(currentToken));
|
|
606
|
-
}
|
|
607
|
-
throw new TokenNotBoundError(this.#getTokenName(currentToken));
|
|
533
|
+
else {
|
|
534
|
+
// Not `#readScoped`: this entry point reports failure as a rejection, never a sync throw.
|
|
535
|
+
return Promise.reject(new MissingScopeContextError(tokenName(binding.token)));
|
|
608
536
|
}
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
537
|
+
return this.#resolveBindingAsync(binding, undefined, resolutionPath, resolutionStack, branchDepth);
|
|
538
|
+
}
|
|
539
|
+
async resolveAsync(token, options, resolutionPath, resolutionStack, branchDepth = UNOWNED_BRANCH) {
|
|
540
|
+
const { binding, owner } = this.#requireBinding(token, options, resolutionPath, resolutionStack);
|
|
541
|
+
if (binding.scope === "singleton" && owner !== this) {
|
|
542
|
+
return owner.#resolveBindingAsync(binding, options, resolutionPath, resolutionStack, branchDepth);
|
|
613
543
|
}
|
|
614
|
-
return this.#resolveBindingAsync(binding, options, resolutionPath, resolutionStack);
|
|
544
|
+
return this.#resolveBindingAsync(binding, options, resolutionPath, resolutionStack, branchDepth);
|
|
615
545
|
}
|
|
616
|
-
async #resolveBindingAsync(binding, options, resolutionPath, resolutionStack) {
|
|
617
|
-
if (binding
|
|
618
|
-
binding.onActivation === undefined &&
|
|
619
|
-
!this.#lifecycle.hasActivationHandlers(binding.token)) {
|
|
546
|
+
async #resolveBindingAsync(binding, options, resolutionPath, resolutionStack, branchDepth) {
|
|
547
|
+
if (this.#isPlainConstant(binding)) {
|
|
620
548
|
return binding.value;
|
|
621
549
|
}
|
|
622
|
-
const scope = binding.scope
|
|
623
|
-
// Singleton cache
|
|
550
|
+
const scope = binding.scope;
|
|
624
551
|
if (scope === "singleton") {
|
|
625
552
|
if (binding.instance !== NO_INSTANCE) {
|
|
626
553
|
return binding.instance;
|
|
627
554
|
}
|
|
628
|
-
// In-flight dedup
|
|
555
|
+
// In-flight dedup: concurrent callers share the first creation.
|
|
629
556
|
const inflight = this.#scope.getInflight(binding.id);
|
|
630
557
|
if (inflight !== undefined) {
|
|
631
558
|
return inflight;
|
|
632
559
|
}
|
|
633
560
|
}
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
if (
|
|
637
|
-
|
|
638
|
-
}
|
|
639
|
-
if (this.#scope.hasScoped(binding.id)) {
|
|
640
|
-
return this.#scope.getScoped(binding.id);
|
|
561
|
+
else if (scope === "scoped") {
|
|
562
|
+
const cachedScoped = this.#readScoped(binding);
|
|
563
|
+
if (cachedScoped !== SCOPED_MISS) {
|
|
564
|
+
return cachedScoped;
|
|
641
565
|
}
|
|
642
566
|
}
|
|
643
567
|
const frame = this.#getResolutionFrame(binding);
|
|
644
|
-
|
|
645
|
-
const
|
|
646
|
-
resolutionStack
|
|
568
|
+
// This level appends to its own branch and never unwinds — see ARCHITECTURE.md.
|
|
569
|
+
const levelPath = extendResolutionBranch(resolutionPath, branchDepth, frame.tokenName);
|
|
570
|
+
const levelStack = extendResolutionStackBranch(resolutionStack, branchDepth, frame);
|
|
571
|
+
const levelDepth = branchDepthOf(levelPath);
|
|
647
572
|
const needsActivation = this.#activation.needsActivation(binding);
|
|
648
573
|
if (!needsActivation && scope === "transient" && (binding.kind === "dynamic" || binding.kind === "dynamic-async")) {
|
|
649
|
-
const resolutionCtx = new
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
return await binding.factory(resolutionCtx);
|
|
653
|
-
}
|
|
654
|
-
const dynamicResult = binding.factory(resolutionCtx);
|
|
655
|
-
return dynamicResult instanceof Promise ? await dynamicResult : dynamicResult;
|
|
656
|
-
}
|
|
657
|
-
finally {
|
|
658
|
-
resolutionStack.pop();
|
|
659
|
-
resolutionPath.pop();
|
|
660
|
-
resolutionSet?.delete(frameName);
|
|
574
|
+
const resolutionCtx = new AsyncLevelContext(this, levelPath, levelStack, options);
|
|
575
|
+
if (binding.kind === "dynamic-async") {
|
|
576
|
+
return await binding.factory(resolutionCtx);
|
|
661
577
|
}
|
|
578
|
+
const dynamicResult = binding.factory(resolutionCtx);
|
|
579
|
+
return dynamicResult instanceof Promise ? await dynamicResult : dynamicResult;
|
|
662
580
|
}
|
|
663
|
-
const
|
|
664
|
-
|
|
665
|
-
? new DefaultResolutionContext(this, resolutionPath, resolutionStack, options)
|
|
581
|
+
const resolutionCtx = needsActivation || requiresResolutionContext(binding)
|
|
582
|
+
? new AsyncLevelContext(this, levelPath, levelStack, options)
|
|
666
583
|
: undefined;
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
};
|
|
680
|
-
const singletonPromise = createSingletonPromise().catch((err) => {
|
|
681
|
-
this.#scope.clearInflight(binding.id);
|
|
682
|
-
throw err;
|
|
683
|
-
});
|
|
684
|
-
this.#scope.setInflight(binding.id, singletonPromise);
|
|
685
|
-
return await singletonPromise;
|
|
686
|
-
}
|
|
687
|
-
const instance = await this.#instantiateAsync(binding, resolutionCtx, resolutionPath, resolutionStack);
|
|
688
|
-
const shouldActivate = this.#activation.refreshAfterFirstInstantiation(binding, needsActivation);
|
|
689
|
-
const activated = shouldActivate
|
|
690
|
-
? await this.#lifecycle.runActivation(resolutionCtx, binding, instance, this.#metadataReader)
|
|
691
|
-
: instance;
|
|
692
|
-
if (scope === "scoped") {
|
|
693
|
-
this.#scope.setScoped(binding.id, activated);
|
|
694
|
-
}
|
|
695
|
-
return activated;
|
|
584
|
+
if (scope === "singleton") {
|
|
585
|
+
// The promise is published before it settles, so concurrent callers dedup onto it.
|
|
586
|
+
const singletonPromise = this.#instantiateAndActivateAsync(binding, resolutionCtx, levelPath, levelStack, levelDepth, needsActivation).then((activated) => {
|
|
587
|
+
this.#scope.setSingleton(binding, activated);
|
|
588
|
+
this.#scope.clearInflight(binding.id);
|
|
589
|
+
return activated;
|
|
590
|
+
}, (error) => {
|
|
591
|
+
this.#scope.clearInflight(binding.id);
|
|
592
|
+
throw error;
|
|
593
|
+
});
|
|
594
|
+
this.#scope.setInflight(binding.id, singletonPromise);
|
|
595
|
+
return await singletonPromise;
|
|
696
596
|
}
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
597
|
+
const activated = await this.#instantiateAndActivateAsync(binding, resolutionCtx, levelPath, levelStack, levelDepth, needsActivation);
|
|
598
|
+
if (scope === "scoped") {
|
|
599
|
+
this.#scope.setScoped(binding.id, activated);
|
|
600
|
+
}
|
|
601
|
+
return activated;
|
|
602
|
+
}
|
|
603
|
+
async #instantiateAndActivateAsync(binding, ctx, resolutionPath, resolutionStack, branchDepth, needsActivation) {
|
|
604
|
+
const instance = await this.#instantiateAsync(binding, ctx, resolutionPath, resolutionStack, branchDepth);
|
|
605
|
+
if (!this.#activation.refreshAfterFirstInstantiation(binding, needsActivation)) {
|
|
606
|
+
return instance;
|
|
701
607
|
}
|
|
608
|
+
return this.#lifecycle.runActivation(ctx, binding, instance, this.#metadataReader);
|
|
702
609
|
}
|
|
703
|
-
async #instantiateAsync(binding, ctx, resolutionPath, resolutionStack) {
|
|
610
|
+
async #instantiateAsync(binding, ctx, resolutionPath, resolutionStack, branchDepth) {
|
|
704
611
|
switch (binding.kind) {
|
|
705
612
|
case "constant":
|
|
706
613
|
return binding.value;
|
|
@@ -717,232 +624,148 @@ export class DependencyResolver {
|
|
|
717
624
|
}
|
|
718
625
|
return binding.factory(ctx);
|
|
719
626
|
case "class": {
|
|
720
|
-
const deps = await this.#
|
|
721
|
-
|
|
722
|
-
return instance;
|
|
627
|
+
const deps = await this.#resolveDepsAsync(this.#constructorParams(binding.target), resolutionPath, resolutionStack, branchDepth);
|
|
628
|
+
return this.#classes.instantiate(binding.target, deps);
|
|
723
629
|
}
|
|
724
630
|
case "resolved": {
|
|
725
631
|
if (ctx === undefined) {
|
|
726
632
|
throw new InternalError("resolved binding requires resolution context");
|
|
727
633
|
}
|
|
728
|
-
const deps = await this.#
|
|
634
|
+
const deps = await this.#resolveDepsAsync(binding.deps, resolutionPath, resolutionStack, branchDepth);
|
|
729
635
|
const factoryResult = binding.factory(...deps);
|
|
730
636
|
return factoryResult instanceof Promise ? factoryResult : Promise.resolve(factoryResult);
|
|
731
637
|
}
|
|
732
638
|
case "resolved-async": {
|
|
733
|
-
const deps = await this.#
|
|
639
|
+
const deps = await this.#resolveDepsAsync(binding.deps, resolutionPath, resolutionStack, branchDepth);
|
|
734
640
|
return binding.factory(...deps);
|
|
735
641
|
}
|
|
736
642
|
case "alias":
|
|
737
643
|
throw new InternalError("alias should have been followed before instantiation");
|
|
738
644
|
}
|
|
739
645
|
}
|
|
740
|
-
async #
|
|
741
|
-
const
|
|
742
|
-
if (
|
|
743
|
-
if (target.length === 0) {
|
|
744
|
-
return [];
|
|
745
|
-
}
|
|
746
|
-
throw new MissingMetadataError(target.name);
|
|
747
|
-
}
|
|
748
|
-
if (meta.params.length === 0) {
|
|
646
|
+
async #resolveDepsAsync(deps, resolutionPath, resolutionStack, branchDepth) {
|
|
647
|
+
const count = deps.length;
|
|
648
|
+
if (count === 0) {
|
|
749
649
|
return [];
|
|
750
650
|
}
|
|
751
|
-
if (
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
}
|
|
760
|
-
if (paramOptions === undefined) {
|
|
761
|
-
return [await this.resolveAsyncFromContext(param.token, resolutionPath, resolutionStack)];
|
|
762
|
-
}
|
|
763
|
-
return [await this.resolveAsync(param.token, paramOptions, resolutionPath, resolutionStack)];
|
|
764
|
-
}
|
|
765
|
-
const pending = new Array(meta.params.length);
|
|
766
|
-
const shouldCloneContext = meta.params.length > 1;
|
|
767
|
-
for (let index = 0; index < meta.params.length; index += 1) {
|
|
768
|
-
const param = meta.params[index];
|
|
769
|
-
const paramOptions = injectionSlotToResolveOptions(param);
|
|
770
|
-
if (param.multi) {
|
|
771
|
-
pending[index] = this.resolveAllAsync(param.token, paramOptions, shouldCloneContext ? [...resolutionPath] : resolutionPath, shouldCloneContext ? [...resolutionStack] : resolutionStack);
|
|
772
|
-
}
|
|
773
|
-
else if (param.optional) {
|
|
774
|
-
pending[index] = this.resolveOptionalAsync(param.token, paramOptions, shouldCloneContext ? [...resolutionPath] : resolutionPath, shouldCloneContext ? [...resolutionStack] : resolutionStack);
|
|
775
|
-
}
|
|
776
|
-
else {
|
|
777
|
-
pending[index] =
|
|
778
|
-
paramOptions === undefined
|
|
779
|
-
? this.resolveAsyncFromContext(param.token, shouldCloneContext ? [...resolutionPath] : resolutionPath, shouldCloneContext ? [...resolutionStack] : resolutionStack)
|
|
780
|
-
: this.resolveAsync(param.token, paramOptions, shouldCloneContext ? [...resolutionPath] : resolutionPath, shouldCloneContext ? [...resolutionStack] : resolutionStack);
|
|
781
|
-
}
|
|
651
|
+
if (count === 1) {
|
|
652
|
+
return [await this.#resolveDepAsync(deps[0], resolutionPath, resolutionStack, branchDepth)];
|
|
653
|
+
}
|
|
654
|
+
// Siblings resolve concurrently and each extends the same branch, so the first appends in
|
|
655
|
+
// place and the rest copy the prefix — no caller has to isolate them.
|
|
656
|
+
const pending = new Array(count);
|
|
657
|
+
for (let index = 0; index < count; index += 1) {
|
|
658
|
+
pending[index] = this.#resolveDepAsync(deps[index], resolutionPath, resolutionStack, branchDepth);
|
|
782
659
|
}
|
|
783
660
|
return Promise.all(pending);
|
|
784
661
|
}
|
|
785
|
-
|
|
786
|
-
const
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
const dep = deps[index];
|
|
790
|
-
const depOptions = injectionSlotToResolveOptions(dep);
|
|
791
|
-
if (dep.multi) {
|
|
792
|
-
pending[index] = this.resolveAllAsync(dep.token, depOptions, shouldCloneContext ? [...resolutionPath] : resolutionPath, shouldCloneContext ? [...resolutionStack] : resolutionStack);
|
|
793
|
-
}
|
|
794
|
-
else if (dep.optional) {
|
|
795
|
-
pending[index] = this.resolveOptionalAsync(dep.token, depOptions, shouldCloneContext ? [...resolutionPath] : resolutionPath, shouldCloneContext ? [...resolutionStack] : resolutionStack);
|
|
796
|
-
}
|
|
797
|
-
else {
|
|
798
|
-
pending[index] =
|
|
799
|
-
depOptions === undefined
|
|
800
|
-
? this.resolveAsyncFromContext(dep.token, shouldCloneContext ? [...resolutionPath] : resolutionPath, shouldCloneContext ? [...resolutionStack] : resolutionStack)
|
|
801
|
-
: this.resolveAsync(dep.token, depOptions, shouldCloneContext ? [...resolutionPath] : resolutionPath, shouldCloneContext ? [...resolutionStack] : resolutionStack);
|
|
802
|
-
}
|
|
662
|
+
#resolveDepAsync(dep, resolutionPath, resolutionStack, branchDepth) {
|
|
663
|
+
const options = injectionSlotToResolveOptions(dep);
|
|
664
|
+
if (dep.multi) {
|
|
665
|
+
return this.resolveAllAsync(dep.token, options, resolutionPath, resolutionStack, branchDepth);
|
|
803
666
|
}
|
|
804
|
-
|
|
667
|
+
if (dep.optional) {
|
|
668
|
+
return this.resolveOptionalAsync(dep.token, options, resolutionPath, resolutionStack, branchDepth);
|
|
669
|
+
}
|
|
670
|
+
if (options === undefined) {
|
|
671
|
+
return this.resolveAsyncFromContext(dep.token, resolutionPath, resolutionStack, branchDepth);
|
|
672
|
+
}
|
|
673
|
+
return this.resolveAsync(dep.token, options, resolutionPath, resolutionStack, branchDepth);
|
|
805
674
|
}
|
|
806
|
-
async resolveOptionalAsync(token, options, resolutionPath, resolutionStack) {
|
|
675
|
+
async resolveOptionalAsync(token, options, resolutionPath, resolutionStack, branchDepth = UNOWNED_BRANCH) {
|
|
807
676
|
if (this.#findBinding(token, options, resolutionPath, resolutionStack) === undefined) {
|
|
808
677
|
return undefined;
|
|
809
678
|
}
|
|
810
|
-
return this.resolveAsync(token, options, resolutionPath, resolutionStack);
|
|
679
|
+
return this.resolveAsync(token, options, resolutionPath, resolutionStack, branchDepth);
|
|
811
680
|
}
|
|
812
|
-
async resolveAllAsync(token, options, resolutionPath, resolutionStack) {
|
|
813
|
-
|
|
814
|
-
const namedCandidates = this.#getSimpleNamedBindingsFromChain(token, options.name);
|
|
815
|
-
if (namedCandidates.length === 0) {
|
|
816
|
-
return [];
|
|
817
|
-
}
|
|
818
|
-
const pending = new Array(namedCandidates.length);
|
|
819
|
-
for (let index = 0; index < namedCandidates.length; index += 1) {
|
|
820
|
-
pending[index] = this.#resolveCandidateAsync(namedCandidates[index], options, resolutionPath, resolutionStack);
|
|
821
|
-
}
|
|
822
|
-
return Promise.all(pending);
|
|
823
|
-
}
|
|
824
|
-
const allBindings = this.#getAllBindingsFromChain(token);
|
|
825
|
-
if (allBindings.length === 0) {
|
|
826
|
-
return [];
|
|
827
|
-
}
|
|
828
|
-
const ctx = this.#makeConstraintContext(resolutionPath, resolutionStack, options);
|
|
829
|
-
const candidates = selectAllBindings(allBindings, options, ctx);
|
|
681
|
+
async resolveAllAsync(token, options, resolutionPath, resolutionStack, branchDepth = UNOWNED_BRANCH) {
|
|
682
|
+
const candidates = this.#candidateBindings(token, options, resolutionPath, resolutionStack);
|
|
830
683
|
const pending = new Array(candidates.length);
|
|
831
684
|
for (let index = 0; index < candidates.length; index += 1) {
|
|
832
|
-
pending[index] = this.#resolveCandidateAsync(candidates[index], options, resolutionPath, resolutionStack);
|
|
685
|
+
pending[index] = this.#resolveCandidateAsync(candidates[index], options, resolutionPath, resolutionStack, branchDepth);
|
|
833
686
|
}
|
|
834
687
|
return Promise.all(pending);
|
|
835
688
|
}
|
|
836
689
|
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
837
|
-
#
|
|
690
|
+
#allBindingsFromChain(token) {
|
|
838
691
|
const ownBindings = this.#registry.getAll(token);
|
|
839
692
|
if (this.#parent === undefined) {
|
|
840
693
|
return ownBindings;
|
|
841
694
|
}
|
|
842
695
|
const result = [...ownBindings];
|
|
843
|
-
let current = this.#parent;
|
|
844
|
-
while (current !== undefined) {
|
|
696
|
+
for (let current = this.#parent; current !== undefined; current = current.#parent) {
|
|
845
697
|
const own = current.#registry.getAll(token);
|
|
846
698
|
if (own.length > 0) {
|
|
847
699
|
result.push(...own);
|
|
848
700
|
}
|
|
849
|
-
current = current.#parent;
|
|
850
701
|
}
|
|
851
702
|
return result;
|
|
852
703
|
}
|
|
853
|
-
|
|
704
|
+
/** Every binding the chain's name indexes hold for one name, nearest container first. */
|
|
705
|
+
#namedBindingsFromChain(token, name) {
|
|
706
|
+
// A name resolves to at most one binding per registry, so a root container's answer is built
|
|
707
|
+
// whole rather than grown — the list is sized at its allocation.
|
|
854
708
|
const ownBinding = this.#registry.getSimpleNamed(token, name);
|
|
855
709
|
if (this.#parent === undefined) {
|
|
856
|
-
return ownBinding
|
|
710
|
+
return ownBinding === undefined ? [] : [ownBinding];
|
|
857
711
|
}
|
|
858
|
-
const result = [];
|
|
859
|
-
|
|
860
|
-
result.push(ownBinding);
|
|
861
|
-
}
|
|
862
|
-
let current = this.#parent;
|
|
863
|
-
while (current !== undefined) {
|
|
712
|
+
const result = ownBinding === undefined ? [] : [ownBinding];
|
|
713
|
+
for (let current = this.#parent; current !== undefined; current = current.#parent) {
|
|
864
714
|
const binding = current.#registry.getSimpleNamed(token, name);
|
|
865
715
|
if (binding !== undefined) {
|
|
866
716
|
result.push(binding);
|
|
867
717
|
}
|
|
868
|
-
current = current.#parent;
|
|
869
718
|
}
|
|
870
719
|
return result;
|
|
871
720
|
}
|
|
872
|
-
|
|
873
|
-
|
|
721
|
+
/** A constant with no activation anywhere resolves to its value with no pipeline at all. */
|
|
722
|
+
#isPlainConstant(binding) {
|
|
723
|
+
return (binding.kind === "constant" &&
|
|
724
|
+
binding.onActivation === undefined &&
|
|
725
|
+
(this.#lifecycle.activationVersion === 0 || !this.#lifecycle.hasActivationHandlers(binding.token)));
|
|
726
|
+
}
|
|
727
|
+
/** Whether either an own hook or a container-level hook would run for this binding. */
|
|
728
|
+
#hasAnyActivation(binding) {
|
|
729
|
+
if (binding.onActivation !== undefined) {
|
|
730
|
+
return true;
|
|
731
|
+
}
|
|
732
|
+
return this.#lifecycle.activationVersion !== 0 && this.#lifecycle.hasActivationHandlers(binding.token);
|
|
733
|
+
}
|
|
734
|
+
/**
|
|
735
|
+
* The cached instance of a `scoped` binding, or {@link SCOPED_MISS}.
|
|
736
|
+
*
|
|
737
|
+
* @remarks A `scoped` binding outside a child container is a configuration error, not a miss, so
|
|
738
|
+
* the check lives with the read that depends on it.
|
|
739
|
+
*/
|
|
740
|
+
#readScoped(binding) {
|
|
741
|
+
if (!this.#scope.isChild) {
|
|
742
|
+
throw new MissingScopeContextError(tokenName(binding.token));
|
|
743
|
+
}
|
|
744
|
+
if (this.#scope.hasScoped(binding.id)) {
|
|
745
|
+
return this.#scope.getScoped(binding.id);
|
|
746
|
+
}
|
|
747
|
+
return SCOPED_MISS;
|
|
874
748
|
}
|
|
875
749
|
#makeConstraintContext(resolutionPath, resolutionStack, options) {
|
|
876
750
|
if (options === undefined && resolutionPath.length === 0 && resolutionStack.length === 0) {
|
|
877
751
|
return ROOT_CONSTRAINT_CONTEXT;
|
|
878
752
|
}
|
|
879
|
-
const parent = resolutionStack.at(-1);
|
|
880
|
-
const ancestors = resolutionStack.length > 1 ? resolutionStack.slice(0, -1) : [];
|
|
881
753
|
return {
|
|
882
754
|
resolutionPath,
|
|
883
755
|
resolutionStack,
|
|
884
|
-
parent,
|
|
885
|
-
ancestors,
|
|
756
|
+
parent: resolutionStack.at(-1),
|
|
757
|
+
ancestors: resolutionStack.length > 1 ? resolutionStack.slice(0, -1) : [],
|
|
886
758
|
currentResolveOptions: options,
|
|
887
759
|
};
|
|
888
760
|
}
|
|
889
761
|
#matchesBindingFast(binding, options, resolutionPath, resolutionStack) {
|
|
890
|
-
if (!
|
|
762
|
+
if (!matchesSlot(binding.slot, options)) {
|
|
891
763
|
return false;
|
|
892
764
|
}
|
|
893
765
|
if (binding.predicate === undefined) {
|
|
894
766
|
return true;
|
|
895
767
|
}
|
|
896
|
-
|
|
897
|
-
return binding.predicate(ctx);
|
|
898
|
-
}
|
|
899
|
-
#matchesSlotFast(slot, options) {
|
|
900
|
-
const requestedName = options?.name;
|
|
901
|
-
const requestedTags = options?.tags;
|
|
902
|
-
const singleRequestedTag = options?.tag;
|
|
903
|
-
const hasRequestedTags = (requestedTags?.length ?? 0) > 0 || singleRequestedTag !== undefined;
|
|
904
|
-
if (slot.name !== undefined) {
|
|
905
|
-
if (requestedName === undefined || slot.name !== requestedName) {
|
|
906
|
-
return false;
|
|
907
|
-
}
|
|
908
|
-
}
|
|
909
|
-
else if (requestedName !== undefined) {
|
|
910
|
-
return false;
|
|
911
|
-
}
|
|
912
|
-
if (slot.tags.length > 0) {
|
|
913
|
-
if (!hasRequestedTags) {
|
|
914
|
-
return false;
|
|
915
|
-
}
|
|
916
|
-
for (const [tagKey, tagValue] of slot.tags) {
|
|
917
|
-
if (!this.#matchesRequestedTag(tagKey, tagValue, requestedTags, singleRequestedTag)) {
|
|
918
|
-
return false;
|
|
919
|
-
}
|
|
920
|
-
}
|
|
921
|
-
}
|
|
922
|
-
else if (hasRequestedTags) {
|
|
923
|
-
return false;
|
|
924
|
-
}
|
|
925
|
-
return true;
|
|
926
|
-
}
|
|
927
|
-
#getTokenName(token) {
|
|
928
|
-
return tokenName(token);
|
|
929
|
-
}
|
|
930
|
-
#matchesRequestedTag(tagKey, tagValue, requestedTags, singleRequestedTag) {
|
|
931
|
-
if (singleRequestedTag !== undefined &&
|
|
932
|
-
singleRequestedTag[0] === tagKey &&
|
|
933
|
-
Object.is(singleRequestedTag[1], tagValue)) {
|
|
934
|
-
return true;
|
|
935
|
-
}
|
|
936
|
-
if (requestedTags === undefined || requestedTags.length === 0) {
|
|
937
|
-
return false;
|
|
938
|
-
}
|
|
939
|
-
for (let index = 0; index < requestedTags.length; index += 1) {
|
|
940
|
-
const requestedTag = requestedTags[index];
|
|
941
|
-
if (requestedTag[0] === tagKey && Object.is(requestedTag[1], tagValue)) {
|
|
942
|
-
return true;
|
|
943
|
-
}
|
|
944
|
-
}
|
|
945
|
-
return false;
|
|
768
|
+
return binding.predicate(this.#makeConstraintContext(resolutionPath, resolutionStack, options));
|
|
946
769
|
}
|
|
947
770
|
#resolveTransientDynamicSyncFromContext(binding, resolutionPath, resolutionStack) {
|
|
948
771
|
// One lane at every depth: `binding.inFlight` is O(1), so there is nothing to escape.
|
|
@@ -969,113 +792,121 @@ export class DependencyResolver {
|
|
|
969
792
|
}
|
|
970
793
|
}
|
|
971
794
|
// Deliberately not `async`: that would allocate a state machine and a promise per level.
|
|
972
|
-
#resolveTransientDynamicAsyncFromContext(binding, resolutionPath, resolutionStack,
|
|
973
|
-
// Path-scoped cycle detection, because async chains interleave — see ARCHITECTURE.md.
|
|
974
|
-
const pool = this.#asyncChainContextPool;
|
|
795
|
+
#resolveTransientDynamicAsyncFromContext(binding, resolutionPath, resolutionStack, branchDepth) {
|
|
975
796
|
const frame = this.#getResolutionFrame(binding);
|
|
976
|
-
|
|
797
|
+
let levelPath;
|
|
977
798
|
try {
|
|
978
|
-
|
|
799
|
+
levelPath = extendResolutionBranch(resolutionPath, branchDepth, frame.tokenName);
|
|
979
800
|
}
|
|
980
801
|
catch (cycleError) {
|
|
981
802
|
// This method is not `async`; keep failures as rejections rather than sync throws.
|
|
982
803
|
return Promise.reject(cycleError);
|
|
983
804
|
}
|
|
984
|
-
|
|
985
|
-
//
|
|
986
|
-
const ctx =
|
|
987
|
-
? callerContext
|
|
988
|
-
: this.#acquireAsyncChainContext(resolutionPath, resolutionStack);
|
|
989
|
-
ctx.chainLevels += 1;
|
|
990
|
-
// Invoke the factory synchronously to get its Promise (or a resolved value for "dynamic").
|
|
991
|
-
let factoryPromise;
|
|
805
|
+
const levelStack = extendResolutionStackBranch(resolutionStack, branchDepth, frame);
|
|
806
|
+
// Nothing this level appended is ever removed, so no level observes its own settlement.
|
|
807
|
+
const ctx = new AsyncLevelContext(this, levelPath, levelStack, undefined);
|
|
992
808
|
try {
|
|
993
809
|
if (binding.kind === "dynamic-async") {
|
|
994
|
-
|
|
995
|
-
}
|
|
996
|
-
else {
|
|
997
|
-
const factoryResult = binding.factory(ctx);
|
|
998
|
-
factoryPromise =
|
|
999
|
-
factoryResult instanceof Promise ? factoryResult : Promise.resolve(factoryResult);
|
|
810
|
+
return binding.factory(ctx);
|
|
1000
811
|
}
|
|
812
|
+
const factoryResult = binding.factory(ctx);
|
|
813
|
+
return factoryResult instanceof Promise ? factoryResult : Promise.resolve(factoryResult);
|
|
1001
814
|
}
|
|
1002
815
|
catch (factoryError) {
|
|
1003
|
-
// Synchronous throw from the factory (rare) — clean up immediately.
|
|
1004
|
-
exitResolutionPath(resolutionPath);
|
|
1005
|
-
ctx.chainLevels -= 1;
|
|
1006
|
-
if (ctx.chainLevels === 0) {
|
|
1007
|
-
pool.push(ctx);
|
|
1008
|
-
}
|
|
1009
816
|
return Promise.reject(factoryError);
|
|
1010
817
|
}
|
|
1011
|
-
// A side listener, not a derived chain: FIFO puts it before the awaiting caller resumes.
|
|
1012
|
-
// It marks a rejection as handled, so callers must await or `.catch` the returned promise.
|
|
1013
|
-
const settle = ctx.chainSettle ??
|
|
1014
|
-
(ctx.chainSettle = () => {
|
|
1015
|
-
exitResolutionPath(resolutionPath);
|
|
1016
|
-
ctx.chainLevels -= 1;
|
|
1017
|
-
if (ctx.chainLevels === 0) {
|
|
1018
|
-
pool.push(ctx);
|
|
1019
|
-
}
|
|
1020
|
-
});
|
|
1021
|
-
factoryPromise.then(settle, settle);
|
|
1022
|
-
return factoryPromise;
|
|
1023
818
|
}
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
819
|
+
// ── The cascade lane ───────────────────────────────────────────────────────
|
|
820
|
+
/**
|
|
821
|
+
* Entry for a request a factory makes from inside an open synchronous cascade.
|
|
822
|
+
*
|
|
823
|
+
* @remarks A request arriving with no cascade open came out of a continuation, so its ancestors
|
|
824
|
+
* are on no call stack — it escapes to the branch lane. See `ARCHITECTURE.md`.
|
|
825
|
+
*/
|
|
826
|
+
resolveAsyncFromCascade(token) {
|
|
827
|
+
if (this.#cascadePath.length === 0) {
|
|
828
|
+
return this.resolveAsyncFromContext(token, [], [], ROOT_BRANCH);
|
|
829
|
+
}
|
|
830
|
+
return this.#dispatchCascade(token);
|
|
831
|
+
}
|
|
832
|
+
/** Entry for a resolve the container starts, which opens the cascade rather than joining one. */
|
|
833
|
+
resolveAsyncFromRoot(token) {
|
|
834
|
+
return this.#dispatchCascade(token);
|
|
835
|
+
}
|
|
836
|
+
#dispatchCascade(token) {
|
|
837
|
+
const fastBinding = this.#registry.getFastDefault(token);
|
|
838
|
+
if (fastBinding !== undefined) {
|
|
839
|
+
if ((fastBinding.kind === "dynamic-async" || fastBinding.kind === "dynamic") &&
|
|
840
|
+
fastBinding.scope === "transient" &&
|
|
841
|
+
!this.#hasAnyActivation(fastBinding)) {
|
|
842
|
+
return this.#resolveTransientDynamicAsyncCascade(fastBinding);
|
|
843
|
+
}
|
|
844
|
+
// A value that already exists answers here: escaping would snapshot the cascade for a resolve
|
|
845
|
+
// that never looks at a path.
|
|
846
|
+
if (this.#isPlainConstant(fastBinding)) {
|
|
847
|
+
return Promise.resolve(fastBinding.value);
|
|
848
|
+
}
|
|
849
|
+
if (fastBinding.scope === "singleton" && fastBinding.instance !== NO_INSTANCE) {
|
|
850
|
+
return Promise.resolve(fastBinding.instance);
|
|
851
|
+
}
|
|
1028
852
|
}
|
|
1029
|
-
|
|
1030
|
-
|
|
853
|
+
// Anything else leaves the cascade lane for good, seeded with a snapshot of the ancestors it
|
|
854
|
+
// accumulated — so a cycle across the boundary is still on one path.
|
|
855
|
+
return this.resolveAsyncFromContext(token, [...this.#cascadePath], [...this.#cascadeStack], UNOWNED_BRANCH);
|
|
1031
856
|
}
|
|
857
|
+
#resolveTransientDynamicAsyncCascade(binding) {
|
|
858
|
+
const frame = this.#getResolutionFrame(binding);
|
|
859
|
+
// The request that closes a cycle is made from a factory's synchronous prefix, and synchronous
|
|
860
|
+
// code does not interleave — so the O(1) flag is exact path membership here, as it is for the
|
|
861
|
+
// sync lane. It is cleared when the factory returns its promise, not when that promise settles.
|
|
862
|
+
if (binding.inFlight) {
|
|
863
|
+
return Promise.reject(new CircularDependencyError([...this.#cascadePath, frame.tokenName]));
|
|
864
|
+
}
|
|
865
|
+
const ctx = (this.#cascadeContext ??= new AsyncCascadeContext(this, this.#cascadePath, this.#cascadeStack));
|
|
866
|
+
binding.inFlight = true;
|
|
867
|
+
this.#cascadePath.push(frame.tokenName);
|
|
868
|
+
this.#cascadeStack.push(frame);
|
|
869
|
+
try {
|
|
870
|
+
if (binding.kind === "dynamic-async") {
|
|
871
|
+
return binding.factory(ctx);
|
|
872
|
+
}
|
|
873
|
+
const factoryResult = binding.factory(ctx);
|
|
874
|
+
return factoryResult instanceof Promise ? factoryResult : Promise.resolve(factoryResult);
|
|
875
|
+
}
|
|
876
|
+
catch (factoryError) {
|
|
877
|
+
return Promise.reject(factoryError);
|
|
878
|
+
}
|
|
879
|
+
finally {
|
|
880
|
+
this.#cascadeStack.pop();
|
|
881
|
+
this.#cascadePath.pop();
|
|
882
|
+
binding.inFlight = false;
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
// A cached candidate answers here rather than re-entering the generic path: `resolveAll` pays
|
|
886
|
+
// this per candidate, and a fan-out over cached handlers is the shape that makes it matter.
|
|
1032
887
|
#resolveCandidateSync(binding, options, resolutionPath, resolutionStack) {
|
|
1033
|
-
if (binding
|
|
1034
|
-
binding.onActivation === undefined &&
|
|
1035
|
-
!this.#lifecycle.hasActivationHandlers(binding.token)) {
|
|
888
|
+
if (this.#isPlainConstant(binding)) {
|
|
1036
889
|
return binding.value;
|
|
1037
890
|
}
|
|
1038
891
|
if (binding.kind === "alias") {
|
|
1039
892
|
return this.resolve(binding.target, options, resolutionPath, resolutionStack);
|
|
1040
893
|
}
|
|
1041
|
-
|
|
1042
|
-
if (scope === "singleton" && binding.instance !== NO_INSTANCE) {
|
|
894
|
+
if (binding.scope === "singleton" && binding.instance !== NO_INSTANCE) {
|
|
1043
895
|
return binding.instance;
|
|
1044
896
|
}
|
|
1045
|
-
if (scope === "scoped") {
|
|
1046
|
-
if (!this.#scope.isChild) {
|
|
1047
|
-
throw new MissingScopeContextError(this.#getTokenName(binding.token));
|
|
1048
|
-
}
|
|
1049
|
-
if (this.#scope.hasScoped(binding.id)) {
|
|
1050
|
-
return this.#scope.getScoped(binding.id);
|
|
1051
|
-
}
|
|
1052
|
-
}
|
|
1053
897
|
return this.#resolveBinding(binding, options, resolutionPath, resolutionStack);
|
|
1054
898
|
}
|
|
1055
|
-
#resolveCandidateAsync(binding, options, resolutionPath, resolutionStack) {
|
|
1056
|
-
if (binding
|
|
1057
|
-
binding.onActivation === undefined &&
|
|
1058
|
-
!this.#lifecycle.hasActivationHandlers(binding.token)) {
|
|
899
|
+
#resolveCandidateAsync(binding, options, resolutionPath, resolutionStack, branchDepth) {
|
|
900
|
+
if (this.#isPlainConstant(binding)) {
|
|
1059
901
|
return Promise.resolve(binding.value);
|
|
1060
902
|
}
|
|
1061
|
-
const isolatedPath = [...resolutionPath];
|
|
1062
|
-
const isolatedStack = [...resolutionStack];
|
|
1063
903
|
if (binding.kind === "alias") {
|
|
1064
|
-
return this.resolveAsync(binding.target, options,
|
|
904
|
+
return this.resolveAsync(binding.target, options, resolutionPath, resolutionStack, branchDepth);
|
|
1065
905
|
}
|
|
1066
|
-
|
|
1067
|
-
if (scope === "singleton" && binding.instance !== NO_INSTANCE) {
|
|
906
|
+
if (binding.scope === "singleton" && binding.instance !== NO_INSTANCE) {
|
|
1068
907
|
return Promise.resolve(binding.instance);
|
|
1069
908
|
}
|
|
1070
|
-
|
|
1071
|
-
if (!this.#scope.isChild) {
|
|
1072
|
-
return Promise.reject(new MissingScopeContextError(this.#getTokenName(binding.token)));
|
|
1073
|
-
}
|
|
1074
|
-
if (this.#scope.hasScoped(binding.id)) {
|
|
1075
|
-
return Promise.resolve(this.#scope.getScoped(binding.id));
|
|
1076
|
-
}
|
|
1077
|
-
}
|
|
1078
|
-
return this.#resolveBindingAsync(binding, options, isolatedPath, isolatedStack);
|
|
909
|
+
return this.#resolveBindingAsync(binding, options, resolutionPath, resolutionStack, branchDepth);
|
|
1079
910
|
}
|
|
1080
911
|
#getResolutionFrame(binding) {
|
|
1081
912
|
// Memoized on the binding rather than in a per-resolver Map: the frame derives only from
|
|
@@ -1085,14 +916,10 @@ export class DependencyResolver {
|
|
|
1085
916
|
if (existing !== undefined) {
|
|
1086
917
|
return existing;
|
|
1087
918
|
}
|
|
1088
|
-
const
|
|
1089
|
-
const frame = buildResolutionFrame(tokenName(binding.token), scope, binding.id, binding.kind, binding.slot);
|
|
919
|
+
const frame = buildResolutionFrame(tokenName(binding.token), binding.scope, binding.id, binding.kind, binding.slot);
|
|
1090
920
|
binding.frame = frame;
|
|
1091
921
|
return frame;
|
|
1092
922
|
}
|
|
1093
|
-
#requiresResolutionContext(binding) {
|
|
1094
|
-
return binding.kind === "dynamic" || binding.kind === "dynamic-async";
|
|
1095
|
-
}
|
|
1096
923
|
#acquireSyncResolutionContext(resolutionPath, resolutionStack, options) {
|
|
1097
924
|
const depth = resolutionStack.length;
|
|
1098
925
|
const existing = this.#syncResolutionContextPool[depth];
|
|
@@ -1105,4 +932,30 @@ export class DependencyResolver {
|
|
|
1105
932
|
return created;
|
|
1106
933
|
}
|
|
1107
934
|
}
|
|
935
|
+
/** Absent scoped entry — distinguishes it from a cached `undefined`. */
|
|
936
|
+
const SCOPED_MISS = Symbol("di:scoped-miss");
|
|
937
|
+
function anyPredicate(bindings) {
|
|
938
|
+
for (let index = 0; index < bindings.length; index += 1) {
|
|
939
|
+
if (bindings[index].predicate !== undefined) {
|
|
940
|
+
return true;
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
return false;
|
|
944
|
+
}
|
|
945
|
+
/** Only a factory is handed the resolution context; everything else gets its deps directly. */
|
|
946
|
+
function requiresResolutionContext(binding) {
|
|
947
|
+
return binding.kind === "dynamic" || binding.kind === "dynamic-async";
|
|
948
|
+
}
|
|
949
|
+
/**
|
|
950
|
+
* Whether the tag index's answer is the one `Object.is` would give.
|
|
951
|
+
*
|
|
952
|
+
* @remarks An indexed binding has no name, no predicate and exactly one tag, and the request carries
|
|
953
|
+
* only that tag, so `matchesSlot` reduces to the tag values — and the index matched the key already.
|
|
954
|
+
* It answers by SameValueZero, which parts from `Object.is` (SPEC §3.5) on exactly one pair: `+0` and
|
|
955
|
+
* `-0`. So a request whose value is not zero is already exact, and only a zero-valued one is worth
|
|
956
|
+
* reading the stored value for.
|
|
957
|
+
*/
|
|
958
|
+
function matchesIndexedTagValue(binding, requestedValue) {
|
|
959
|
+
return requestedValue !== 0 || Object.is(binding.slot.tags[0][1], requestedValue);
|
|
960
|
+
}
|
|
1108
961
|
//# sourceMappingURL=resolver.js.map
|