@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
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import type { Binding,
|
|
1
|
+
import type { Binding, ConstantBinding, DynamicAsyncBinding, DynamicBinding } from "#/binding";
|
|
2
2
|
import { NO_INSTANCE } from "#/binding";
|
|
3
3
|
import type { Container } from "#/container/container";
|
|
4
|
-
import type { InjectionDescriptor } from "#/decorators/inject";
|
|
5
4
|
import {
|
|
6
5
|
AsyncActivationError,
|
|
7
6
|
AsyncResolutionError,
|
|
@@ -12,37 +11,49 @@ import {
|
|
|
12
11
|
NoMatchingBindingError,
|
|
13
12
|
TokenNotBoundError,
|
|
14
13
|
} from "#/errors";
|
|
15
|
-
import type { MetadataReader } from "#/metadata/metadata-types";
|
|
14
|
+
import type { MetadataReader, ParamMetadata } from "#/metadata/metadata-types";
|
|
16
15
|
import type { BindingRegistry } from "#/registry";
|
|
17
16
|
import { ActivationNeedCache } from "#/resolution/activation-need";
|
|
18
17
|
import type { DefaultLookupEntry } from "#/resolution/binding-lookup-cache";
|
|
19
18
|
import { BindingLookupCache } from "#/resolution/binding-lookup-cache";
|
|
20
|
-
import { selectAllBindings, selectBinding } from "#/resolution/binding-select";
|
|
19
|
+
import { matchesSlot, selectAllBindings, selectBinding } from "#/resolution/binding-select";
|
|
21
20
|
import { ClassIntrospector } from "#/resolution/class-introspector";
|
|
22
21
|
import type { ResolutionDiagnostics } from "#/resolution/diagnostics";
|
|
23
22
|
import type { ResolverCallbacks } from "#/resolution/environment";
|
|
24
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
AsyncCascadeContext,
|
|
25
|
+
AsyncLevelContext,
|
|
26
|
+
buildResolutionFrame,
|
|
27
|
+
DefaultResolutionContext,
|
|
28
|
+
} from "#/resolution/environment";
|
|
25
29
|
import { InstantiationPlanCompiler, PLAN_RETRY } from "#/resolution/instantiation-plan";
|
|
26
30
|
import type { LifecycleManager } from "#/resolution/lifecycle";
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
31
|
+
import type { BranchDepth, OwnedBranchPath } from "#/resolution/resolution-path";
|
|
32
|
+
import {
|
|
33
|
+
branchDepthOf,
|
|
34
|
+
enterResolutionPath,
|
|
35
|
+
extendResolutionBranch,
|
|
36
|
+
extendResolutionStackBranch,
|
|
37
|
+
ROOT_BRANCH,
|
|
38
|
+
UNOWNED_BRANCH,
|
|
39
|
+
} from "#/resolution/resolution-path";
|
|
40
|
+
import type { DependencySlot } from "#/resolution/resolve-options";
|
|
41
|
+
import { injectionSlotToResolveOptions, isNameOnlyOptions, singleTagOnlyOf } from "#/resolution/resolve-options";
|
|
29
42
|
import type { ScopeManager } from "#/resolution/scope";
|
|
30
43
|
import type { Token } from "#/token";
|
|
31
44
|
import { tokenName } from "#/token";
|
|
32
45
|
import type {
|
|
33
46
|
ActivationHandler,
|
|
34
47
|
BindingIdentifier,
|
|
35
|
-
BindingScope,
|
|
36
|
-
BindingTag,
|
|
37
48
|
ConstraintContext,
|
|
38
49
|
Constructor,
|
|
39
50
|
ResolutionFrame,
|
|
40
51
|
ResolveOptions,
|
|
41
52
|
} from "#/types";
|
|
42
53
|
|
|
43
|
-
type BindingWithScope = Binding & { scope: BindingScope };
|
|
44
54
|
const EMPTY_STRING_LIST: ReadonlyArray<string> = [];
|
|
45
55
|
const EMPTY_FRAME_LIST: ReadonlyArray<ResolutionFrame> = [];
|
|
56
|
+
const EMPTY_PARAM_LIST: ReadonlyArray<ParamMetadata> = [];
|
|
46
57
|
const ROOT_CONSTRAINT_CONTEXT = {
|
|
47
58
|
resolutionPath: EMPTY_STRING_LIST,
|
|
48
59
|
resolutionStack: EMPTY_FRAME_LIST,
|
|
@@ -56,9 +67,22 @@ const ROOT_CONSTRAINT_CONTEXT = {
|
|
|
56
67
|
*/
|
|
57
68
|
export class DependencyResolver implements ResolverCallbacks {
|
|
58
69
|
readonly #syncResolutionContextPool: Array<DefaultResolutionContext> = [];
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
70
|
+
/**
|
|
71
|
+
* The pair a top-level **sync** resolve reuses instead of minting two arrays per call.
|
|
72
|
+
*
|
|
73
|
+
* @remarks Read directly rather than through an accessor returning both: a shallow resolve is one
|
|
74
|
+
* top-level call, so a call and an object literal there are not amortised over anything. Every sync
|
|
75
|
+
* lane pops what it pushes, so `rootStack.length === 0` means no resolve holds the pair; async
|
|
76
|
+
* appends without popping and mints its own. Keeping the pair stable is also what lets a pooled
|
|
77
|
+
* context skip storing pointers it already holds.
|
|
78
|
+
*/
|
|
79
|
+
readonly rootPath: Array<string> = [];
|
|
80
|
+
readonly rootStack: Array<ResolutionFrame> = [];
|
|
81
|
+
// The open synchronous factory cascade: its arrays are the ancestor chain, and they are balanced
|
|
82
|
+
// because synchronous code does not interleave.
|
|
83
|
+
readonly #cascadePath: Array<string> = [];
|
|
84
|
+
readonly #cascadeStack: Array<ResolutionFrame> = [];
|
|
85
|
+
#cascadeContext: AsyncCascadeContext | undefined;
|
|
62
86
|
// Compiled plans; `null` marks a binding as unplannable under the current cache versions.
|
|
63
87
|
readonly #classPlanByBindingId = new Map<BindingIdentifier, (() => unknown) | null>();
|
|
64
88
|
#classPlanRegistryVersion = -1;
|
|
@@ -92,11 +116,11 @@ export class DependencyResolver implements ResolverCallbacks {
|
|
|
92
116
|
parent === undefined ? undefined : parent.#lookup,
|
|
93
117
|
);
|
|
94
118
|
this.#classes = new ClassIntrospector(metadataReader, container);
|
|
95
|
-
this.#activation = new ActivationNeedCache(lifecycle, this.#classes);
|
|
119
|
+
this.#activation = new ActivationNeedCache(lifecycle, this.#classes, registry);
|
|
96
120
|
}
|
|
97
121
|
|
|
98
122
|
/** Structural counts for {@link RESOLUTION_DIAGNOSTICS}; see `resolution/diagnostics.ts`. */
|
|
99
|
-
describeCaches(): Pick<ResolutionDiagnostics, "
|
|
123
|
+
describeCaches(): Pick<ResolutionDiagnostics, "compiledPlanCount" | "syncContextPoolSize"> {
|
|
100
124
|
let compiledPlanCount = 0;
|
|
101
125
|
for (const plan of this.#classPlanByBindingId.values()) {
|
|
102
126
|
if (plan !== null) {
|
|
@@ -104,7 +128,6 @@ export class DependencyResolver implements ResolverCallbacks {
|
|
|
104
128
|
}
|
|
105
129
|
}
|
|
106
130
|
return {
|
|
107
|
-
asyncContextPoolSize: this.#asyncChainContextPool.length,
|
|
108
131
|
compiledPlanCount,
|
|
109
132
|
syncContextPoolSize: this.#syncResolutionContextPool.length,
|
|
110
133
|
};
|
|
@@ -117,15 +140,13 @@ export class DependencyResolver implements ResolverCallbacks {
|
|
|
117
140
|
options: ResolveOptions | undefined,
|
|
118
141
|
resolutionPath: Array<string>,
|
|
119
142
|
resolutionStack: Array<ResolutionFrame>,
|
|
120
|
-
):
|
|
143
|
+
): DefaultLookupEntry<DependencyResolver> | undefined {
|
|
121
144
|
if (options === undefined) {
|
|
122
145
|
const fastDefaultBinding = this.#registry.getFastDefault(token);
|
|
123
146
|
if (fastDefaultBinding !== undefined) {
|
|
124
147
|
return { binding: fastDefaultBinding, owner: this };
|
|
125
148
|
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
if (options?.name !== undefined && options.tag === undefined && (options.tags?.length ?? 0) === 0) {
|
|
149
|
+
} else if (isNameOnlyOptions(options)) {
|
|
129
150
|
const namedBinding = this.#registry.getSimpleNamed(token, options.name);
|
|
130
151
|
if (
|
|
131
152
|
namedBinding !== undefined &&
|
|
@@ -133,37 +154,33 @@ export class DependencyResolver implements ResolverCallbacks {
|
|
|
133
154
|
) {
|
|
134
155
|
return { binding: namedBinding, owner: this };
|
|
135
156
|
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
) {
|
|
144
|
-
const [tagKey, tagValue] = options.tags![0]!;
|
|
145
|
-
const tagged = this.#registry.getSimpleTagged(token, tagKey, tagValue);
|
|
146
|
-
if (tagged !== undefined) {
|
|
147
|
-
return { binding: tagged, owner: this };
|
|
157
|
+
} else {
|
|
158
|
+
const singleTag = singleTagOnlyOf(options);
|
|
159
|
+
if (singleTag !== undefined) {
|
|
160
|
+
const tagged = this.#registry.getSimpleTagged(token, singleTag[0], singleTag[1]);
|
|
161
|
+
if (tagged !== undefined && matchesIndexedTagValue(tagged, singleTag[1])) {
|
|
162
|
+
return { binding: tagged, owner: this };
|
|
163
|
+
}
|
|
148
164
|
}
|
|
149
165
|
}
|
|
150
166
|
|
|
151
167
|
const bindings = this.#registry.getAll(token);
|
|
152
168
|
if (bindings.length > 0) {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
169
|
+
// A lone candidate is its own selection: matching it is the whole decision, with no
|
|
170
|
+
// specificity to weigh and no ambiguity to report.
|
|
171
|
+
const selected =
|
|
172
|
+
bindings.length === 1
|
|
173
|
+
? this.#matchesBindingFast(bindings[0]!, options, resolutionPath, resolutionStack)
|
|
174
|
+
? bindings[0]
|
|
175
|
+
: undefined
|
|
176
|
+
: selectBinding(
|
|
177
|
+
bindings,
|
|
178
|
+
options,
|
|
179
|
+
this.#makeConstraintContext(resolutionPath, resolutionStack, options),
|
|
180
|
+
tokenName(token),
|
|
181
|
+
);
|
|
182
|
+
if (selected !== undefined) {
|
|
183
|
+
return { binding: selected, owner: this };
|
|
167
184
|
}
|
|
168
185
|
}
|
|
169
186
|
if (this.#parent !== undefined) {
|
|
@@ -172,13 +189,55 @@ export class DependencyResolver implements ResolverCallbacks {
|
|
|
172
189
|
return undefined;
|
|
173
190
|
}
|
|
174
191
|
|
|
192
|
+
/**
|
|
193
|
+
* The binding a token resolves to with alias hops followed, or a diagnostic throw.
|
|
194
|
+
*
|
|
195
|
+
* @remarks Alias hops are followed iteratively with exact cycle detection — a revisited alias
|
|
196
|
+
* token raises {@link CircularDependencyError} instead of overflowing the call stack.
|
|
197
|
+
*/
|
|
198
|
+
#requireBinding(
|
|
199
|
+
token: Token<unknown> | Constructor,
|
|
200
|
+
options: ResolveOptions | undefined,
|
|
201
|
+
resolutionPath: Array<string>,
|
|
202
|
+
resolutionStack: Array<ResolutionFrame>,
|
|
203
|
+
): DefaultLookupEntry<DependencyResolver> {
|
|
204
|
+
let currentToken = token;
|
|
205
|
+
let visitedAliasTokens: Set<Token<unknown> | Constructor> | undefined;
|
|
206
|
+
let found = this.#findBinding(currentToken, options, resolutionPath, resolutionStack);
|
|
207
|
+
|
|
208
|
+
while (found !== undefined && found.binding.kind === "alias") {
|
|
209
|
+
const target = found.binding.target;
|
|
210
|
+
visitedAliasTokens ??= new Set([currentToken]);
|
|
211
|
+
if (visitedAliasTokens.has(target)) {
|
|
212
|
+
throw new CircularDependencyError([...visitedAliasTokens, target].map((entry) => tokenName(entry)));
|
|
213
|
+
}
|
|
214
|
+
visitedAliasTokens.add(target);
|
|
215
|
+
currentToken = target;
|
|
216
|
+
found = this.#findBinding(currentToken, options, resolutionPath, resolutionStack);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (found === undefined) {
|
|
220
|
+
// Thrown here rather than from a helper: the error captures this stack, and an error path is
|
|
221
|
+
// dominated by that capture. Bindings under the token mean the request matched none of them.
|
|
222
|
+
if (this.#registry.getAll(currentToken).length > 0) {
|
|
223
|
+
throw new NoMatchingBindingError(
|
|
224
|
+
tokenName(currentToken),
|
|
225
|
+
options ?? {},
|
|
226
|
+
this.#registry.availableSlotStrings(currentToken),
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
throw new TokenNotBoundError(tokenName(currentToken));
|
|
230
|
+
}
|
|
231
|
+
return found;
|
|
232
|
+
}
|
|
233
|
+
|
|
175
234
|
/**
|
|
176
235
|
* Binding lookup aligned with `resolve` — used by `Container.validate` without instantiating.
|
|
177
236
|
*/
|
|
178
237
|
peekBindingForValidate(
|
|
179
238
|
token: Token<unknown> | Constructor,
|
|
180
239
|
options: ResolveOptions | undefined,
|
|
181
|
-
):
|
|
240
|
+
): DefaultLookupEntry<DependencyResolver> | undefined {
|
|
182
241
|
return this.#findBinding(token, options, [], []);
|
|
183
242
|
}
|
|
184
243
|
|
|
@@ -188,16 +247,8 @@ export class DependencyResolver implements ResolverCallbacks {
|
|
|
188
247
|
peekCandidateBindingsForValidate(
|
|
189
248
|
token: Token<unknown> | Constructor,
|
|
190
249
|
options: ResolveOptions | undefined,
|
|
191
|
-
):
|
|
192
|
-
|
|
193
|
-
return this.#getSimpleNamedBindingsFromChain(token, options.name);
|
|
194
|
-
}
|
|
195
|
-
const allBindings = this.#getAllBindingsFromChain(token);
|
|
196
|
-
if (allBindings.length === 0) {
|
|
197
|
-
return [];
|
|
198
|
-
}
|
|
199
|
-
const ctx = this.#makeConstraintContext([], [], options);
|
|
200
|
-
return selectAllBindings(allBindings, options, ctx);
|
|
250
|
+
): ReadonlyArray<Binding> {
|
|
251
|
+
return this.#candidateBindings(token, options, [], []);
|
|
201
252
|
}
|
|
202
253
|
|
|
203
254
|
// ── Sync resolve ───────────────────────────────────────────────────────────
|
|
@@ -211,85 +262,77 @@ export class DependencyResolver implements ResolverCallbacks {
|
|
|
211
262
|
// (parent-chain walk + alias folding) only on miss or alias.
|
|
212
263
|
const fastBinding = this.#registry.getFastDefault(token);
|
|
213
264
|
if (fastBinding !== undefined && fastBinding.kind !== "alias") {
|
|
214
|
-
return this.#resolveDefaultEntry
|
|
265
|
+
return this.#resolveDefaultEntry(fastBinding, this, resolutionPath, resolutionStack) as Value;
|
|
215
266
|
}
|
|
216
267
|
const entry = this.#lookup.defaultEntry(token);
|
|
217
268
|
if (entry === null) {
|
|
218
269
|
return this.resolve(token, undefined, resolutionPath, resolutionStack);
|
|
219
270
|
}
|
|
220
|
-
return this.#resolveDefaultEntry
|
|
271
|
+
return this.#resolveDefaultEntry(entry.binding, entry.owner, resolutionPath, resolutionStack) as Value;
|
|
221
272
|
}
|
|
222
273
|
|
|
223
|
-
#resolveDefaultEntry
|
|
274
|
+
#resolveDefaultEntry(
|
|
224
275
|
binding: Binding,
|
|
225
276
|
owner: DependencyResolver,
|
|
226
277
|
resolutionPath: Array<string>,
|
|
227
278
|
resolutionStack: Array<ResolutionFrame>,
|
|
228
|
-
):
|
|
229
|
-
|
|
230
|
-
binding.kind === "constant" &&
|
|
231
|
-
binding.onActivation === undefined &&
|
|
232
|
-
(this.#lifecycle.activationVersion === 0 || !this.#lifecycle.hasActivationHandlers(binding.token))
|
|
233
|
-
) {
|
|
234
|
-
return binding.value as Value;
|
|
235
|
-
}
|
|
236
|
-
const scope = (binding as BindingWithScope).scope ?? "transient";
|
|
279
|
+
): unknown {
|
|
280
|
+
const scope = binding.scope;
|
|
237
281
|
if (scope === "transient") {
|
|
238
282
|
if (binding.kind === "dynamic") {
|
|
239
283
|
const containerHooks =
|
|
240
284
|
this.#lifecycle.activationVersion === 0 ? undefined : this.#lifecycle.activationHandlersFor(binding.token);
|
|
241
285
|
if (binding.onActivation === undefined && (containerHooks === undefined || containerHooks.length === 0)) {
|
|
242
|
-
return this.#resolveTransientDynamicSyncFromContext(
|
|
243
|
-
binding as Binding<Value> & { kind: "dynamic" },
|
|
244
|
-
resolutionPath,
|
|
245
|
-
resolutionStack,
|
|
246
|
-
);
|
|
286
|
+
return this.#resolveTransientDynamicSyncFromContext(binding, resolutionPath, resolutionStack);
|
|
247
287
|
}
|
|
248
|
-
return this.#resolveTransientDynamicActivatedSync(
|
|
249
|
-
binding as Binding<Value> & { kind: "dynamic" },
|
|
250
|
-
containerHooks,
|
|
251
|
-
resolutionPath,
|
|
252
|
-
resolutionStack,
|
|
253
|
-
);
|
|
288
|
+
return this.#resolveTransientDynamicActivatedSync(binding, containerHooks, resolutionPath, resolutionStack);
|
|
254
289
|
}
|
|
255
290
|
// Compiled plans only run at the top level — inner levels keep the runtime cycle guard.
|
|
256
291
|
if ((binding.kind === "class" || binding.kind === "resolved") && resolutionPath.length === 0) {
|
|
257
292
|
const plan = this.#getInstantiationPlan(binding);
|
|
258
293
|
if (plan !== null) {
|
|
259
|
-
return plan()
|
|
294
|
+
return plan();
|
|
260
295
|
}
|
|
261
296
|
}
|
|
262
297
|
} else if (scope === "singleton") {
|
|
298
|
+
// A constant is a singleton that is already its own instance.
|
|
299
|
+
if (this.#isPlainConstant(binding)) {
|
|
300
|
+
return binding.value;
|
|
301
|
+
}
|
|
263
302
|
const cachedSingleton = binding.instance;
|
|
264
303
|
if (cachedSingleton !== NO_INSTANCE) {
|
|
265
|
-
return cachedSingleton
|
|
304
|
+
return cachedSingleton;
|
|
266
305
|
}
|
|
267
306
|
if (owner !== this) {
|
|
268
|
-
return owner.#resolveBinding(binding
|
|
307
|
+
return owner.#resolveBinding(binding, undefined, resolutionPath, resolutionStack);
|
|
269
308
|
}
|
|
270
309
|
} else {
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
if (this.#scope.hasScoped(binding.id)) {
|
|
275
|
-
return this.#scope.getScoped<Value>(binding.id);
|
|
310
|
+
const cachedScoped = this.#readScoped(binding);
|
|
311
|
+
if (cachedScoped !== SCOPED_MISS) {
|
|
312
|
+
return cachedScoped;
|
|
276
313
|
}
|
|
277
314
|
}
|
|
278
|
-
return this.#resolveBinding(binding
|
|
315
|
+
return this.#resolveBinding(binding, undefined, resolutionPath, resolutionStack);
|
|
279
316
|
}
|
|
280
317
|
|
|
281
318
|
// Lean lane for an activated transient dynamic binding: same observable behavior as the
|
|
282
319
|
// generic #resolveBinding path (guard, frame, ctx, per-binding then container hooks) with
|
|
283
320
|
// the kind/activation dispatch resolved statically.
|
|
284
|
-
#resolveTransientDynamicActivatedSync
|
|
285
|
-
binding:
|
|
321
|
+
#resolveTransientDynamicActivatedSync(
|
|
322
|
+
binding: DynamicBinding<unknown>,
|
|
286
323
|
containerHooks: ReadonlyArray<ActivationHandler<unknown>> | undefined,
|
|
287
324
|
resolutionPath: Array<string>,
|
|
288
325
|
resolutionStack: Array<ResolutionFrame>,
|
|
289
|
-
):
|
|
326
|
+
): unknown {
|
|
327
|
+
// Same O(1) cycle guard as the unhooked lane: this is still one sync call stack, so the flag
|
|
328
|
+
// *is* exact path membership — see ARCHITECTURE.md.
|
|
290
329
|
const frame = this.#getResolutionFrame(binding);
|
|
291
330
|
const tokenDisplayName = frame.tokenName;
|
|
292
|
-
|
|
331
|
+
if (binding.inFlight) {
|
|
332
|
+
throw new CircularDependencyError([...resolutionPath, tokenDisplayName]);
|
|
333
|
+
}
|
|
334
|
+
binding.inFlight = true;
|
|
335
|
+
resolutionPath.push(tokenDisplayName);
|
|
293
336
|
resolutionStack.push(frame);
|
|
294
337
|
try {
|
|
295
338
|
const resolutionCtx = this.#acquireSyncResolutionContext(resolutionPath, resolutionStack, undefined);
|
|
@@ -311,14 +354,14 @@ export class DependencyResolver implements ResolverCallbacks {
|
|
|
311
354
|
if (activationResult instanceof Promise) {
|
|
312
355
|
throw new AsyncActivationError(tokenDisplayName, "onActivation");
|
|
313
356
|
}
|
|
314
|
-
activated = activationResult
|
|
357
|
+
activated = activationResult;
|
|
315
358
|
}
|
|
316
359
|
}
|
|
317
360
|
return activated;
|
|
318
361
|
} finally {
|
|
319
362
|
resolutionStack.pop();
|
|
320
363
|
resolutionPath.pop();
|
|
321
|
-
|
|
364
|
+
binding.inFlight = false;
|
|
322
365
|
}
|
|
323
366
|
}
|
|
324
367
|
|
|
@@ -343,7 +386,7 @@ export class DependencyResolver implements ResolverCallbacks {
|
|
|
343
386
|
return compiled;
|
|
344
387
|
}
|
|
345
388
|
|
|
346
|
-
// Compiler behind #
|
|
389
|
+
// Compiler behind #getInstantiationPlan — cold path, so the host indirection costs nothing hot.
|
|
347
390
|
readonly #planCompiler = new InstantiationPlanCompiler({
|
|
348
391
|
hasActivationHandlers: (token) => this.#lifecycle.hasActivationHandlers(token),
|
|
349
392
|
knownPostConstruct: (target) => this.#classes.knownPostConstruct(target),
|
|
@@ -354,7 +397,7 @@ export class DependencyResolver implements ResolverCallbacks {
|
|
|
354
397
|
return entry === null ? null : { binding: entry.binding };
|
|
355
398
|
},
|
|
356
399
|
getResolutionFrame: (binding) => this.#getResolutionFrame(binding),
|
|
357
|
-
// Dispatches exactly as #
|
|
400
|
+
// Dispatches exactly as #resolveDep does, so an escaped dep is indistinguishable
|
|
358
401
|
// from the same dep on a fully interpreted resolve.
|
|
359
402
|
resolveEscaped: (token, options, arity, resolutionPath, resolutionStack) => {
|
|
360
403
|
if (arity === "all") {
|
|
@@ -378,139 +421,74 @@ export class DependencyResolver implements ResolverCallbacks {
|
|
|
378
421
|
): Value {
|
|
379
422
|
// Name-only fast lane: memoized lookup, dispatching just the shapes whose
|
|
380
423
|
// semantics involve no resolution context (constants, cached singletons).
|
|
381
|
-
if (
|
|
382
|
-
options !== undefined &&
|
|
383
|
-
options.name !== undefined &&
|
|
384
|
-
options.tag === undefined &&
|
|
385
|
-
(options.tags === undefined || options.tags.length === 0)
|
|
386
|
-
) {
|
|
424
|
+
if (options !== undefined && isNameOnlyOptions(options)) {
|
|
387
425
|
const namedEntry = this.#lookup.namedEntry(token, options.name);
|
|
388
426
|
if (namedEntry !== null) {
|
|
389
427
|
const namedBinding = namedEntry.binding;
|
|
390
|
-
if (
|
|
391
|
-
namedBinding.kind === "constant" &&
|
|
392
|
-
namedBinding.onActivation === undefined &&
|
|
393
|
-
(this.#lifecycle.activationVersion === 0 || !this.#lifecycle.hasActivationHandlers(namedBinding.token))
|
|
394
|
-
) {
|
|
428
|
+
if (this.#isPlainConstant(namedBinding)) {
|
|
395
429
|
return namedBinding.value as Value;
|
|
396
430
|
}
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
if (namedBinding.instance !== NO_INSTANCE) {
|
|
400
|
-
return namedBinding.instance as Value;
|
|
401
|
-
}
|
|
431
|
+
if (namedBinding.scope === "singleton" && namedBinding.instance !== NO_INSTANCE) {
|
|
432
|
+
return namedBinding.instance as Value;
|
|
402
433
|
}
|
|
403
434
|
// Everything else keeps the full path (context, activation, guards).
|
|
404
435
|
}
|
|
405
436
|
}
|
|
406
437
|
|
|
407
|
-
|
|
408
|
-
let visitedAliasTokens: Set<Token<unknown> | Constructor> | undefined;
|
|
409
|
-
let found = this.#findBinding(currentToken, options, resolutionPath, resolutionStack);
|
|
410
|
-
|
|
411
|
-
// Follow aliases iteratively with exact cycle detection — a revisited alias
|
|
412
|
-
// token throws CircularDependencyError instead of overflowing the call stack.
|
|
413
|
-
while (found !== undefined && found.binding.kind === "alias") {
|
|
414
|
-
const target = found.binding.target;
|
|
415
|
-
visitedAliasTokens ??= new Set([currentToken]);
|
|
416
|
-
if (visitedAliasTokens.has(target)) {
|
|
417
|
-
throw new CircularDependencyError([...visitedAliasTokens, target].map((entry) => tokenName(entry)));
|
|
418
|
-
}
|
|
419
|
-
visitedAliasTokens.add(target);
|
|
420
|
-
currentToken = target;
|
|
421
|
-
found = this.#findBinding(currentToken, options, resolutionPath, resolutionStack);
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
if (found === undefined) {
|
|
425
|
-
const ownBindings = this.#registry.getAll(currentToken);
|
|
426
|
-
if (ownBindings.length > 0) {
|
|
427
|
-
throw new NoMatchingBindingError(
|
|
428
|
-
this.#getTokenName(currentToken),
|
|
429
|
-
options ?? {},
|
|
430
|
-
this.#getAvailableSlots(currentToken),
|
|
431
|
-
);
|
|
432
|
-
}
|
|
433
|
-
throw new TokenNotBoundError(this.#getTokenName(currentToken));
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
const { binding, owner } = found;
|
|
438
|
+
const { binding, owner } = this.#requireBinding(token, options, resolutionPath, resolutionStack);
|
|
437
439
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
if (scope === "singleton" && owner !== this) {
|
|
442
|
-
return owner.#resolveBinding(binding as Binding<Value>, options, resolutionPath, resolutionStack);
|
|
440
|
+
// A singleton owned by a parent resolver is resolved there, so the parent caches it.
|
|
441
|
+
if (binding.scope === "singleton" && owner !== this) {
|
|
442
|
+
return owner.#resolveBinding(binding, options, resolutionPath, resolutionStack) as Value;
|
|
443
443
|
}
|
|
444
|
-
|
|
445
|
-
// Scoped/transient (or own singleton): resolve with this resolver's container/scope
|
|
446
|
-
return this.#resolveBinding(binding as Binding<Value>, options, resolutionPath, resolutionStack);
|
|
444
|
+
return this.#resolveBinding(binding, options, resolutionPath, resolutionStack) as Value;
|
|
447
445
|
}
|
|
448
446
|
|
|
449
|
-
#resolveBinding
|
|
450
|
-
binding: Binding
|
|
447
|
+
#resolveBinding(
|
|
448
|
+
binding: Binding,
|
|
451
449
|
options: ResolveOptions | undefined,
|
|
452
450
|
resolutionPath: Array<string>,
|
|
453
451
|
resolutionStack: Array<ResolutionFrame>,
|
|
454
|
-
):
|
|
455
|
-
if (
|
|
456
|
-
binding.kind === "constant" &&
|
|
457
|
-
binding.onActivation === undefined &&
|
|
458
|
-
(this.#lifecycle.activationVersion === 0 || !this.#lifecycle.hasActivationHandlers(binding.token))
|
|
459
|
-
) {
|
|
452
|
+
): unknown {
|
|
453
|
+
if (this.#isPlainConstant(binding)) {
|
|
460
454
|
return binding.value;
|
|
461
455
|
}
|
|
462
456
|
|
|
463
|
-
const scope =
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
return binding.instance as Value;
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
// Scoped cache check
|
|
471
|
-
if (scope === "scoped") {
|
|
472
|
-
if (!this.#scope.isChild) {
|
|
473
|
-
throw new MissingScopeContextError(this.#getTokenName(binding.token));
|
|
457
|
+
const scope = binding.scope;
|
|
458
|
+
if (scope === "singleton") {
|
|
459
|
+
if (binding.instance !== NO_INSTANCE) {
|
|
460
|
+
return binding.instance;
|
|
474
461
|
}
|
|
475
|
-
|
|
476
|
-
|
|
462
|
+
} else if (scope === "scoped") {
|
|
463
|
+
const cachedScoped = this.#readScoped(binding);
|
|
464
|
+
if (cachedScoped !== SCOPED_MISS) {
|
|
465
|
+
return cachedScoped;
|
|
477
466
|
}
|
|
478
467
|
}
|
|
479
468
|
|
|
480
469
|
const frame = this.#getResolutionFrame(binding);
|
|
481
470
|
const tokenDisplayName = frame.tokenName;
|
|
482
|
-
const resolutionSet = enterResolutionPath(resolutionPath, tokenDisplayName
|
|
471
|
+
const resolutionSet = enterResolutionPath(resolutionPath, tokenDisplayName);
|
|
483
472
|
resolutionStack.push(frame);
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
473
|
+
try {
|
|
474
|
+
const needsActivation = this.#activation.needsActivation(binding);
|
|
475
|
+
if (!needsActivation && scope === "transient" && binding.kind === "dynamic") {
|
|
476
|
+
const resolutionCtx = this.#acquireSyncResolutionContext(resolutionPath, resolutionStack, options);
|
|
488
477
|
const dynamicResult = binding.factory(resolutionCtx);
|
|
489
478
|
if (dynamicResult instanceof Promise) {
|
|
490
|
-
throw new AsyncResolutionError(
|
|
479
|
+
throw new AsyncResolutionError(tokenDisplayName, tokenDisplayName);
|
|
491
480
|
}
|
|
492
|
-
resolutionStack.pop();
|
|
493
|
-
resolutionPath.pop();
|
|
494
|
-
resolutionSet?.delete(tokenDisplayName);
|
|
495
481
|
return dynamicResult;
|
|
496
|
-
} catch (error) {
|
|
497
|
-
resolutionStack.pop();
|
|
498
|
-
resolutionPath.pop();
|
|
499
|
-
resolutionSet?.delete(tokenDisplayName);
|
|
500
|
-
throw error;
|
|
501
482
|
}
|
|
502
|
-
}
|
|
503
483
|
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
: undefined;
|
|
484
|
+
const resolutionCtx =
|
|
485
|
+
needsActivation || requiresResolutionContext(binding)
|
|
486
|
+
? this.#acquireSyncResolutionContext(resolutionPath, resolutionStack, options)
|
|
487
|
+
: undefined;
|
|
509
488
|
|
|
510
489
|
const instance = this.#instantiateSync(binding, resolutionCtx, resolutionPath, resolutionStack);
|
|
511
490
|
|
|
512
|
-
const
|
|
513
|
-
const activated = shouldActivate
|
|
491
|
+
const activated = this.#activation.refreshAfterFirstInstantiation(binding, needsActivation)
|
|
514
492
|
? this.#lifecycle.runActivationSync(
|
|
515
493
|
resolutionCtx as DefaultResolutionContext,
|
|
516
494
|
binding,
|
|
@@ -519,7 +497,6 @@ export class DependencyResolver implements ResolverCallbacks {
|
|
|
519
497
|
)
|
|
520
498
|
: instance;
|
|
521
499
|
|
|
522
|
-
// Cache by scope
|
|
523
500
|
if (scope === "singleton") {
|
|
524
501
|
this.#scope.setSingleton(binding, activated);
|
|
525
502
|
} else if (scope === "scoped") {
|
|
@@ -534,12 +511,12 @@ export class DependencyResolver implements ResolverCallbacks {
|
|
|
534
511
|
}
|
|
535
512
|
}
|
|
536
513
|
|
|
537
|
-
#instantiateSync
|
|
538
|
-
binding: Binding
|
|
514
|
+
#instantiateSync(
|
|
515
|
+
binding: Binding,
|
|
539
516
|
ctx: DefaultResolutionContext | undefined,
|
|
540
517
|
resolutionPath: Array<string>,
|
|
541
518
|
resolutionStack: Array<ResolutionFrame>,
|
|
542
|
-
):
|
|
519
|
+
): unknown {
|
|
543
520
|
switch (binding.kind) {
|
|
544
521
|
case "constant":
|
|
545
522
|
return binding.value;
|
|
@@ -559,13 +536,12 @@ export class DependencyResolver implements ResolverCallbacks {
|
|
|
559
536
|
throw new AsyncResolutionError(tokenName(binding.token), tokenName(binding.token));
|
|
560
537
|
|
|
561
538
|
case "class": {
|
|
562
|
-
const deps = this.#
|
|
563
|
-
|
|
564
|
-
return instance as Value;
|
|
539
|
+
const deps = this.#resolveDeps(this.#constructorParams(binding.target), resolutionPath, resolutionStack);
|
|
540
|
+
return this.#classes.instantiate(binding.target, deps);
|
|
565
541
|
}
|
|
566
542
|
|
|
567
543
|
case "resolved": {
|
|
568
|
-
const deps = this.#
|
|
544
|
+
const deps = this.#resolveDeps(binding.deps, resolutionPath, resolutionStack);
|
|
569
545
|
const factoryResult = binding.factory(...deps);
|
|
570
546
|
if (factoryResult instanceof Promise) {
|
|
571
547
|
throw new AsyncResolutionError(tokenName(binding.token), tokenName(binding.token));
|
|
@@ -581,90 +557,58 @@ export class DependencyResolver implements ResolverCallbacks {
|
|
|
581
557
|
}
|
|
582
558
|
}
|
|
583
559
|
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
560
|
+
/**
|
|
561
|
+
* The parameters a class binding injects.
|
|
562
|
+
*
|
|
563
|
+
* @remarks A class the metadata reader knows nothing about is constructible only if it declares
|
|
564
|
+
* no parameters; anything else is a missing `@injectable()`.
|
|
565
|
+
*/
|
|
566
|
+
#constructorParams(target: Constructor): ReadonlyArray<ParamMetadata> {
|
|
589
567
|
const meta = this.#classes.constructorMetadata(target);
|
|
590
|
-
if (meta
|
|
591
|
-
|
|
592
|
-
return [];
|
|
593
|
-
}
|
|
594
|
-
throw new MissingMetadataError(target.name);
|
|
568
|
+
if (meta !== undefined) {
|
|
569
|
+
return meta.params;
|
|
595
570
|
}
|
|
596
|
-
if (
|
|
597
|
-
return
|
|
598
|
-
}
|
|
599
|
-
if (meta.params.length === 1) {
|
|
600
|
-
const param = meta.params[0]!;
|
|
601
|
-
const paramOptions = injectionSlotToResolveOptions(param);
|
|
602
|
-
if (param.multi) {
|
|
603
|
-
return [this.resolveAll(param.token, paramOptions, resolutionPath, resolutionStack)];
|
|
604
|
-
}
|
|
605
|
-
if (param.optional) {
|
|
606
|
-
return [this.resolveOptional(param.token, paramOptions, resolutionPath, resolutionStack)];
|
|
607
|
-
}
|
|
608
|
-
if (paramOptions === undefined) {
|
|
609
|
-
return [this.resolveFromContext(param.token, resolutionPath, resolutionStack)];
|
|
610
|
-
}
|
|
611
|
-
return [this.resolve(param.token, paramOptions, resolutionPath, resolutionStack)];
|
|
612
|
-
}
|
|
613
|
-
const deps = new Array<unknown>(meta.params.length);
|
|
614
|
-
for (let index = 0; index < meta.params.length; index += 1) {
|
|
615
|
-
const param = meta.params[index]!;
|
|
616
|
-
const paramOptions = injectionSlotToResolveOptions(param);
|
|
617
|
-
if (param.multi) {
|
|
618
|
-
deps[index] = this.resolveAll(param.token, paramOptions, resolutionPath, resolutionStack);
|
|
619
|
-
continue;
|
|
620
|
-
}
|
|
621
|
-
if (param.optional) {
|
|
622
|
-
deps[index] = this.resolveOptional(param.token, paramOptions, resolutionPath, resolutionStack);
|
|
623
|
-
continue;
|
|
624
|
-
}
|
|
625
|
-
deps[index] =
|
|
626
|
-
paramOptions === undefined
|
|
627
|
-
? this.resolveFromContext(param.token, resolutionPath, resolutionStack)
|
|
628
|
-
: this.resolve(param.token, paramOptions, resolutionPath, resolutionStack);
|
|
571
|
+
if (target.length === 0) {
|
|
572
|
+
return EMPTY_PARAM_LIST;
|
|
629
573
|
}
|
|
630
|
-
|
|
574
|
+
throw new MissingMetadataError(target.name);
|
|
631
575
|
}
|
|
632
576
|
|
|
633
|
-
|
|
634
|
-
|
|
577
|
+
// One dispatch table for both dependency sources — constructor params and `toResolved`
|
|
578
|
+
// descriptors declare the same four things.
|
|
579
|
+
#resolveDeps(
|
|
580
|
+
deps: ReadonlyArray<DependencySlot>,
|
|
635
581
|
resolutionPath: Array<string>,
|
|
636
582
|
resolutionStack: Array<ResolutionFrame>,
|
|
637
583
|
): Array<unknown> {
|
|
638
|
-
const
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
);
|
|
649
|
-
continue;
|
|
650
|
-
}
|
|
651
|
-
if (dep.optional) {
|
|
652
|
-
resolved[index] = this.resolveOptional(
|
|
653
|
-
dep.token as Token<unknown> | Constructor,
|
|
654
|
-
depOptions,
|
|
655
|
-
resolutionPath,
|
|
656
|
-
resolutionStack,
|
|
657
|
-
);
|
|
658
|
-
continue;
|
|
659
|
-
}
|
|
660
|
-
resolved[index] =
|
|
661
|
-
depOptions === undefined
|
|
662
|
-
? this.resolveFromContext(dep.token as Token<unknown> | Constructor, resolutionPath, resolutionStack)
|
|
663
|
-
: this.resolve(dep.token as Token<unknown> | Constructor, depOptions, resolutionPath, resolutionStack);
|
|
584
|
+
const count = deps.length;
|
|
585
|
+
if (count === 0) {
|
|
586
|
+
return [];
|
|
587
|
+
}
|
|
588
|
+
if (count === 1) {
|
|
589
|
+
return [this.#resolveDep(deps[0]!, resolutionPath, resolutionStack)];
|
|
590
|
+
}
|
|
591
|
+
const resolved = new Array<unknown>(count);
|
|
592
|
+
for (let index = 0; index < count; index += 1) {
|
|
593
|
+
resolved[index] = this.#resolveDep(deps[index]!, resolutionPath, resolutionStack);
|
|
664
594
|
}
|
|
665
595
|
return resolved;
|
|
666
596
|
}
|
|
667
597
|
|
|
598
|
+
#resolveDep(dep: DependencySlot, resolutionPath: Array<string>, resolutionStack: Array<ResolutionFrame>): unknown {
|
|
599
|
+
const options = injectionSlotToResolveOptions(dep);
|
|
600
|
+
if (dep.multi) {
|
|
601
|
+
return this.resolveAll(dep.token, options, resolutionPath, resolutionStack);
|
|
602
|
+
}
|
|
603
|
+
if (dep.optional) {
|
|
604
|
+
return this.resolveOptional(dep.token, options, resolutionPath, resolutionStack);
|
|
605
|
+
}
|
|
606
|
+
if (options === undefined) {
|
|
607
|
+
return this.resolveFromContext(dep.token, resolutionPath, resolutionStack);
|
|
608
|
+
}
|
|
609
|
+
return this.resolve(dep.token, options, resolutionPath, resolutionStack);
|
|
610
|
+
}
|
|
611
|
+
|
|
668
612
|
resolveOptional<const Value>(
|
|
669
613
|
token: Token<Value> | Constructor<Value>,
|
|
670
614
|
options: ResolveOptions | undefined,
|
|
@@ -683,50 +627,53 @@ export class DependencyResolver implements ResolverCallbacks {
|
|
|
683
627
|
resolutionPath: Array<string>,
|
|
684
628
|
resolutionStack: Array<ResolutionFrame>,
|
|
685
629
|
): Array<Value> {
|
|
686
|
-
|
|
687
|
-
const namedCandidates = this.#getSimpleNamedBindingsFromChain(token, options.name);
|
|
688
|
-
if (namedCandidates.length === 0) {
|
|
689
|
-
return [];
|
|
690
|
-
}
|
|
691
|
-
const resolved = new Array<Value>(namedCandidates.length);
|
|
692
|
-
for (let index = 0; index < namedCandidates.length; index += 1) {
|
|
693
|
-
resolved[index] = this.#resolveCandidateSync(
|
|
694
|
-
namedCandidates[index] as Binding<Value>,
|
|
695
|
-
options,
|
|
696
|
-
resolutionPath,
|
|
697
|
-
resolutionStack,
|
|
698
|
-
);
|
|
699
|
-
}
|
|
700
|
-
return resolved;
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
const allBindings = this.#getAllBindingsFromChain(token);
|
|
704
|
-
if (allBindings.length === 0) {
|
|
705
|
-
return [];
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
const ctx = this.#makeConstraintContext(resolutionPath, resolutionStack, options);
|
|
709
|
-
const candidates = selectAllBindings(allBindings, options, ctx);
|
|
710
|
-
|
|
630
|
+
const candidates = this.#candidateBindings(token, options, resolutionPath, resolutionStack);
|
|
711
631
|
const resolved = new Array<Value>(candidates.length);
|
|
712
632
|
for (let index = 0; index < candidates.length; index += 1) {
|
|
713
633
|
resolved[index] = this.#resolveCandidateSync(
|
|
714
|
-
candidates[index]
|
|
634
|
+
candidates[index]!,
|
|
715
635
|
options,
|
|
716
636
|
resolutionPath,
|
|
717
637
|
resolutionStack,
|
|
718
|
-
);
|
|
638
|
+
) as Value;
|
|
719
639
|
}
|
|
720
640
|
return resolved;
|
|
721
641
|
}
|
|
722
642
|
|
|
643
|
+
/** Every binding in the chain a `resolveAll` request matches, in chain order. */
|
|
644
|
+
#candidateBindings(
|
|
645
|
+
token: Token<unknown> | Constructor,
|
|
646
|
+
options: ResolveOptions | undefined,
|
|
647
|
+
resolutionPath: Array<string>,
|
|
648
|
+
resolutionStack: Array<ResolutionFrame>,
|
|
649
|
+
): ReadonlyArray<Binding> {
|
|
650
|
+
if (options !== undefined && isNameOnlyOptions(options)) {
|
|
651
|
+
// The name index has matched the slot already, but a hit may still carry a predicate —
|
|
652
|
+
// and that is the selection path's job to evaluate.
|
|
653
|
+
const named = this.#namedBindingsFromChain(token, options.name);
|
|
654
|
+
if (!anyPredicate(named)) {
|
|
655
|
+
return named;
|
|
656
|
+
}
|
|
657
|
+
return selectAllBindings(named, options, this.#makeConstraintContext(resolutionPath, resolutionStack, options));
|
|
658
|
+
}
|
|
659
|
+
const allBindings = this.#allBindingsFromChain(token);
|
|
660
|
+
if (allBindings.length === 0) {
|
|
661
|
+
return allBindings;
|
|
662
|
+
}
|
|
663
|
+
return selectAllBindings(
|
|
664
|
+
allBindings,
|
|
665
|
+
options,
|
|
666
|
+
this.#makeConstraintContext(resolutionPath, resolutionStack, options),
|
|
667
|
+
);
|
|
668
|
+
}
|
|
669
|
+
|
|
723
670
|
// ── Async resolve ──────────────────────────────────────────────────────────
|
|
724
671
|
|
|
725
672
|
resolveAsyncFromContext<const Value>(
|
|
726
673
|
token: Token<Value> | Constructor<Value>,
|
|
727
674
|
resolutionPath: Array<string>,
|
|
728
675
|
resolutionStack: Array<ResolutionFrame>,
|
|
729
|
-
|
|
676
|
+
branchDepth: BranchDepth,
|
|
730
677
|
): Promise<Value> {
|
|
731
678
|
// Hot lane: own-registry fast default (async chains resolve sibling dynamic bindings).
|
|
732
679
|
// Fall back to the chain-versioned memo only on miss or alias.
|
|
@@ -736,75 +683,67 @@ export class DependencyResolver implements ResolverCallbacks {
|
|
|
736
683
|
if (
|
|
737
684
|
(fastBinding.kind === "dynamic-async" || fastBinding.kind === "dynamic") &&
|
|
738
685
|
fastBinding.scope === "transient" &&
|
|
739
|
-
fastBinding
|
|
740
|
-
(this.#lifecycle.activationVersion === 0 || !this.#lifecycle.hasActivationHandlers(fastBinding.token))
|
|
686
|
+
!this.#hasAnyActivation(fastBinding)
|
|
741
687
|
) {
|
|
742
688
|
return this.#resolveTransientDynamicAsyncFromContext(
|
|
743
|
-
fastBinding
|
|
689
|
+
fastBinding,
|
|
744
690
|
resolutionPath,
|
|
745
691
|
resolutionStack,
|
|
746
|
-
|
|
747
|
-
)
|
|
692
|
+
branchDepth,
|
|
693
|
+
) as Promise<Value>;
|
|
748
694
|
}
|
|
749
|
-
return this.#resolveAsyncDefaultEntry
|
|
695
|
+
return this.#resolveAsyncDefaultEntry(
|
|
696
|
+
fastBinding,
|
|
697
|
+
this,
|
|
698
|
+
resolutionPath,
|
|
699
|
+
resolutionStack,
|
|
700
|
+
branchDepth,
|
|
701
|
+
) as Promise<Value>;
|
|
750
702
|
}
|
|
751
703
|
const entry = this.#lookup.defaultEntry(token);
|
|
752
704
|
if (entry === null) {
|
|
753
|
-
return this.resolveAsync(token, undefined, resolutionPath, resolutionStack);
|
|
705
|
+
return this.resolveAsync(token, undefined, resolutionPath, resolutionStack, branchDepth);
|
|
754
706
|
}
|
|
755
|
-
return this.#resolveAsyncDefaultEntry
|
|
707
|
+
return this.#resolveAsyncDefaultEntry(
|
|
756
708
|
entry.binding,
|
|
757
709
|
entry.owner,
|
|
758
710
|
resolutionPath,
|
|
759
711
|
resolutionStack,
|
|
760
|
-
|
|
761
|
-
)
|
|
712
|
+
branchDepth,
|
|
713
|
+
) as Promise<Value>;
|
|
762
714
|
}
|
|
763
715
|
|
|
764
|
-
#resolveAsyncDefaultEntry
|
|
716
|
+
#resolveAsyncDefaultEntry(
|
|
765
717
|
binding: Binding,
|
|
766
718
|
owner: DependencyResolver,
|
|
767
719
|
resolutionPath: Array<string>,
|
|
768
720
|
resolutionStack: Array<ResolutionFrame>,
|
|
769
|
-
|
|
770
|
-
): Promise<
|
|
771
|
-
if (
|
|
772
|
-
binding.
|
|
773
|
-
binding.onActivation === undefined &&
|
|
774
|
-
(this.#lifecycle.activationVersion === 0 || !this.#lifecycle.hasActivationHandlers(binding.token))
|
|
775
|
-
) {
|
|
776
|
-
return Promise.resolve(binding.value as Value);
|
|
721
|
+
branchDepth: BranchDepth,
|
|
722
|
+
): Promise<unknown> {
|
|
723
|
+
if (this.#isPlainConstant(binding)) {
|
|
724
|
+
return Promise.resolve(binding.value);
|
|
777
725
|
}
|
|
778
|
-
const scope =
|
|
726
|
+
const scope = binding.scope;
|
|
779
727
|
if (scope === "transient") {
|
|
780
|
-
if (
|
|
781
|
-
(binding
|
|
782
|
-
binding.onActivation === undefined &&
|
|
783
|
-
(this.#lifecycle.activationVersion === 0 || !this.#lifecycle.hasActivationHandlers(binding.token))
|
|
784
|
-
) {
|
|
785
|
-
return this.#resolveTransientDynamicAsyncFromContext(
|
|
786
|
-
binding as Binding<Value> & { kind: "dynamic" | "dynamic-async" },
|
|
787
|
-
resolutionPath,
|
|
788
|
-
resolutionStack,
|
|
789
|
-
callerContext,
|
|
790
|
-
);
|
|
728
|
+
if ((binding.kind === "dynamic" || binding.kind === "dynamic-async") && !this.#hasAnyActivation(binding)) {
|
|
729
|
+
return this.#resolveTransientDynamicAsyncFromContext(binding, resolutionPath, resolutionStack, branchDepth);
|
|
791
730
|
}
|
|
792
731
|
} else if (scope === "singleton") {
|
|
793
732
|
if (binding.instance !== NO_INSTANCE) {
|
|
794
|
-
return Promise.resolve(binding.instance
|
|
733
|
+
return Promise.resolve(binding.instance);
|
|
795
734
|
}
|
|
796
735
|
if (owner !== this) {
|
|
797
|
-
return owner.#resolveBindingAsync(binding
|
|
798
|
-
}
|
|
799
|
-
} else {
|
|
800
|
-
if (!this.#scope.isChild) {
|
|
801
|
-
return Promise.reject(new MissingScopeContextError(this.#getTokenName(binding.token)));
|
|
736
|
+
return owner.#resolveBindingAsync(binding, undefined, resolutionPath, resolutionStack, branchDepth);
|
|
802
737
|
}
|
|
738
|
+
} else if (this.#scope.isChild) {
|
|
803
739
|
if (this.#scope.hasScoped(binding.id)) {
|
|
804
|
-
return Promise.resolve(this.#scope.getScoped
|
|
740
|
+
return Promise.resolve(this.#scope.getScoped(binding.id));
|
|
805
741
|
}
|
|
742
|
+
} else {
|
|
743
|
+
// Not `#readScoped`: this entry point reports failure as a rejection, never a sync throw.
|
|
744
|
+
return Promise.reject(new MissingScopeContextError(tokenName(binding.token)));
|
|
806
745
|
}
|
|
807
|
-
return this.#resolveBindingAsync(binding
|
|
746
|
+
return this.#resolveBindingAsync(binding, undefined, resolutionPath, resolutionStack, branchDepth);
|
|
808
747
|
}
|
|
809
748
|
|
|
810
749
|
async resolveAsync<const Value>(
|
|
@@ -812,174 +751,125 @@ export class DependencyResolver implements ResolverCallbacks {
|
|
|
812
751
|
options: ResolveOptions | undefined,
|
|
813
752
|
resolutionPath: Array<string>,
|
|
814
753
|
resolutionStack: Array<ResolutionFrame>,
|
|
754
|
+
branchDepth: BranchDepth = UNOWNED_BRANCH,
|
|
815
755
|
): Promise<Value> {
|
|
816
|
-
const
|
|
817
|
-
|
|
818
|
-
if (found === undefined) {
|
|
819
|
-
const ownBindings = this.#registry.getAll(token);
|
|
820
|
-
if (ownBindings.length > 0) {
|
|
821
|
-
throw new NoMatchingBindingError(this.#getTokenName(token), options ?? {}, this.#getAvailableSlots(token));
|
|
822
|
-
}
|
|
823
|
-
throw new TokenNotBoundError(this.#getTokenName(token));
|
|
824
|
-
}
|
|
825
|
-
|
|
826
|
-
let currentToken: Token<unknown> | Constructor = token;
|
|
827
|
-
let visitedAliasTokens: Set<Token<unknown> | Constructor> | undefined;
|
|
828
|
-
let aliasFollowed: DefaultLookupEntry<DependencyResolver> | undefined = found;
|
|
829
|
-
while (aliasFollowed !== undefined && aliasFollowed.binding.kind === "alias") {
|
|
830
|
-
const target = aliasFollowed.binding.target;
|
|
831
|
-
visitedAliasTokens ??= new Set([currentToken]);
|
|
832
|
-
if (visitedAliasTokens.has(target)) {
|
|
833
|
-
throw new CircularDependencyError([...visitedAliasTokens, target].map((entry) => tokenName(entry)));
|
|
834
|
-
}
|
|
835
|
-
visitedAliasTokens.add(target);
|
|
836
|
-
currentToken = target;
|
|
837
|
-
aliasFollowed = this.#findBinding(currentToken, options, resolutionPath, resolutionStack);
|
|
838
|
-
}
|
|
839
|
-
if (aliasFollowed === undefined) {
|
|
840
|
-
const ownBindings = this.#registry.getAll(currentToken);
|
|
841
|
-
if (ownBindings.length > 0) {
|
|
842
|
-
throw new NoMatchingBindingError(
|
|
843
|
-
this.#getTokenName(currentToken),
|
|
844
|
-
options ?? {},
|
|
845
|
-
this.#getAvailableSlots(currentToken),
|
|
846
|
-
);
|
|
847
|
-
}
|
|
848
|
-
throw new TokenNotBoundError(this.#getTokenName(currentToken));
|
|
849
|
-
}
|
|
850
|
-
const { binding, owner } = aliasFollowed;
|
|
851
|
-
|
|
852
|
-
const scope = (binding as BindingWithScope).scope ?? "transient";
|
|
756
|
+
const { binding, owner } = this.#requireBinding(token, options, resolutionPath, resolutionStack);
|
|
853
757
|
|
|
854
|
-
if (scope === "singleton" && owner !== this) {
|
|
855
|
-
return owner.#resolveBindingAsync(binding
|
|
758
|
+
if (binding.scope === "singleton" && owner !== this) {
|
|
759
|
+
return owner.#resolveBindingAsync(binding, options, resolutionPath, resolutionStack, branchDepth) as Value;
|
|
856
760
|
}
|
|
857
|
-
|
|
858
|
-
return this.#resolveBindingAsync(binding as Binding<Value>, options, resolutionPath, resolutionStack);
|
|
761
|
+
return this.#resolveBindingAsync(binding, options, resolutionPath, resolutionStack, branchDepth) as Value;
|
|
859
762
|
}
|
|
860
763
|
|
|
861
|
-
async #resolveBindingAsync
|
|
862
|
-
binding: Binding
|
|
764
|
+
async #resolveBindingAsync(
|
|
765
|
+
binding: Binding,
|
|
863
766
|
options: ResolveOptions | undefined,
|
|
864
767
|
resolutionPath: Array<string>,
|
|
865
768
|
resolutionStack: Array<ResolutionFrame>,
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
binding.onActivation === undefined &&
|
|
870
|
-
!this.#lifecycle.hasActivationHandlers(binding.token)
|
|
871
|
-
) {
|
|
769
|
+
branchDepth: BranchDepth,
|
|
770
|
+
): Promise<unknown> {
|
|
771
|
+
if (this.#isPlainConstant(binding)) {
|
|
872
772
|
return binding.value;
|
|
873
773
|
}
|
|
874
774
|
|
|
875
|
-
const scope =
|
|
876
|
-
|
|
877
|
-
// Singleton cache
|
|
775
|
+
const scope = binding.scope;
|
|
878
776
|
if (scope === "singleton") {
|
|
879
777
|
if (binding.instance !== NO_INSTANCE) {
|
|
880
|
-
return binding.instance
|
|
778
|
+
return binding.instance;
|
|
881
779
|
}
|
|
882
|
-
// In-flight dedup
|
|
780
|
+
// In-flight dedup: concurrent callers share the first creation.
|
|
883
781
|
const inflight = this.#scope.getInflight(binding.id);
|
|
884
782
|
if (inflight !== undefined) {
|
|
885
|
-
return inflight
|
|
886
|
-
}
|
|
887
|
-
}
|
|
888
|
-
|
|
889
|
-
// Scoped cache
|
|
890
|
-
if (scope === "scoped") {
|
|
891
|
-
if (!this.#scope.isChild) {
|
|
892
|
-
throw new MissingScopeContextError(this.#getTokenName(binding.token));
|
|
783
|
+
return inflight;
|
|
893
784
|
}
|
|
894
|
-
|
|
895
|
-
|
|
785
|
+
} else if (scope === "scoped") {
|
|
786
|
+
const cachedScoped = this.#readScoped(binding);
|
|
787
|
+
if (cachedScoped !== SCOPED_MISS) {
|
|
788
|
+
return cachedScoped;
|
|
896
789
|
}
|
|
897
790
|
}
|
|
898
791
|
|
|
899
792
|
const frame = this.#getResolutionFrame(binding);
|
|
900
|
-
|
|
901
|
-
const
|
|
902
|
-
resolutionStack
|
|
793
|
+
// This level appends to its own branch and never unwinds — see ARCHITECTURE.md.
|
|
794
|
+
const levelPath = extendResolutionBranch(resolutionPath, branchDepth, frame.tokenName);
|
|
795
|
+
const levelStack = extendResolutionStackBranch(resolutionStack, branchDepth, frame);
|
|
796
|
+
const levelDepth = branchDepthOf(levelPath);
|
|
797
|
+
|
|
903
798
|
const needsActivation = this.#activation.needsActivation(binding);
|
|
904
799
|
if (!needsActivation && scope === "transient" && (binding.kind === "dynamic" || binding.kind === "dynamic-async")) {
|
|
905
|
-
const resolutionCtx = new
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
return await binding.factory(resolutionCtx);
|
|
909
|
-
}
|
|
910
|
-
const dynamicResult = binding.factory(resolutionCtx);
|
|
911
|
-
return dynamicResult instanceof Promise ? await dynamicResult : dynamicResult;
|
|
912
|
-
} finally {
|
|
913
|
-
resolutionStack.pop();
|
|
914
|
-
resolutionPath.pop();
|
|
915
|
-
resolutionSet?.delete(frameName);
|
|
800
|
+
const resolutionCtx = new AsyncLevelContext(this, levelPath, levelStack, options);
|
|
801
|
+
if (binding.kind === "dynamic-async") {
|
|
802
|
+
return await binding.factory(resolutionCtx);
|
|
916
803
|
}
|
|
804
|
+
const dynamicResult = binding.factory(resolutionCtx);
|
|
805
|
+
return dynamicResult instanceof Promise ? await dynamicResult : dynamicResult;
|
|
917
806
|
}
|
|
918
807
|
|
|
919
|
-
const
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
try {
|
|
925
|
-
if (scope === "singleton") {
|
|
926
|
-
const createSingletonPromise = async (): Promise<Value> => {
|
|
927
|
-
const instance = await this.#instantiateAsync(binding, resolutionCtx, resolutionPath, resolutionStack);
|
|
928
|
-
|
|
929
|
-
const shouldActivate = this.#activation.refreshAfterFirstInstantiation(binding, needsActivation);
|
|
930
|
-
const activated = shouldActivate
|
|
931
|
-
? await this.#lifecycle.runActivation(
|
|
932
|
-
resolutionCtx as DefaultResolutionContext,
|
|
933
|
-
binding,
|
|
934
|
-
instance,
|
|
935
|
-
this.#metadataReader,
|
|
936
|
-
)
|
|
937
|
-
: instance;
|
|
808
|
+
const resolutionCtx =
|
|
809
|
+
needsActivation || requiresResolutionContext(binding)
|
|
810
|
+
? new AsyncLevelContext(this, levelPath, levelStack, options)
|
|
811
|
+
: undefined;
|
|
938
812
|
|
|
813
|
+
if (scope === "singleton") {
|
|
814
|
+
// The promise is published before it settles, so concurrent callers dedup onto it.
|
|
815
|
+
const singletonPromise = this.#instantiateAndActivateAsync(
|
|
816
|
+
binding,
|
|
817
|
+
resolutionCtx,
|
|
818
|
+
levelPath,
|
|
819
|
+
levelStack,
|
|
820
|
+
levelDepth,
|
|
821
|
+
needsActivation,
|
|
822
|
+
).then(
|
|
823
|
+
(activated) => {
|
|
939
824
|
this.#scope.setSingleton(binding, activated);
|
|
940
|
-
binding.instance = activated;
|
|
941
825
|
this.#scope.clearInflight(binding.id);
|
|
942
826
|
return activated;
|
|
943
|
-
}
|
|
944
|
-
|
|
945
|
-
const singletonPromise = createSingletonPromise().catch((err: unknown) => {
|
|
827
|
+
},
|
|
828
|
+
(error: unknown) => {
|
|
946
829
|
this.#scope.clearInflight(binding.id);
|
|
947
|
-
throw
|
|
948
|
-
}
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
const instance = await this.#instantiateAsync(binding, resolutionCtx, resolutionPath, resolutionStack);
|
|
954
|
-
|
|
955
|
-
const shouldActivate = this.#activation.refreshAfterFirstInstantiation(binding, needsActivation);
|
|
956
|
-
const activated = shouldActivate
|
|
957
|
-
? await this.#lifecycle.runActivation(
|
|
958
|
-
resolutionCtx as DefaultResolutionContext,
|
|
959
|
-
binding,
|
|
960
|
-
instance,
|
|
961
|
-
this.#metadataReader,
|
|
962
|
-
)
|
|
963
|
-
: instance;
|
|
964
|
-
|
|
965
|
-
if (scope === "scoped") {
|
|
966
|
-
this.#scope.setScoped(binding.id, activated);
|
|
967
|
-
}
|
|
830
|
+
throw error;
|
|
831
|
+
},
|
|
832
|
+
);
|
|
833
|
+
this.#scope.setInflight(binding.id, singletonPromise as Promise<unknown>);
|
|
834
|
+
return await singletonPromise;
|
|
835
|
+
}
|
|
968
836
|
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
837
|
+
const activated = await this.#instantiateAndActivateAsync(
|
|
838
|
+
binding,
|
|
839
|
+
resolutionCtx,
|
|
840
|
+
levelPath,
|
|
841
|
+
levelStack,
|
|
842
|
+
levelDepth,
|
|
843
|
+
needsActivation,
|
|
844
|
+
);
|
|
845
|
+
if (scope === "scoped") {
|
|
846
|
+
this.#scope.setScoped(binding.id, activated);
|
|
974
847
|
}
|
|
848
|
+
return activated;
|
|
975
849
|
}
|
|
976
850
|
|
|
977
|
-
async #
|
|
978
|
-
binding: Binding
|
|
979
|
-
ctx:
|
|
851
|
+
async #instantiateAndActivateAsync(
|
|
852
|
+
binding: Binding,
|
|
853
|
+
ctx: AsyncLevelContext | undefined,
|
|
980
854
|
resolutionPath: Array<string>,
|
|
981
855
|
resolutionStack: Array<ResolutionFrame>,
|
|
982
|
-
|
|
856
|
+
branchDepth: BranchDepth,
|
|
857
|
+
needsActivation: boolean,
|
|
858
|
+
): Promise<unknown> {
|
|
859
|
+
const instance = await this.#instantiateAsync(binding, ctx, resolutionPath, resolutionStack, branchDepth);
|
|
860
|
+
if (!this.#activation.refreshAfterFirstInstantiation(binding, needsActivation)) {
|
|
861
|
+
return instance;
|
|
862
|
+
}
|
|
863
|
+
return this.#lifecycle.runActivation(ctx as AsyncLevelContext, binding, instance, this.#metadataReader);
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
async #instantiateAsync(
|
|
867
|
+
binding: Binding,
|
|
868
|
+
ctx: AsyncLevelContext | undefined,
|
|
869
|
+
resolutionPath: Array<string>,
|
|
870
|
+
resolutionStack: Array<ResolutionFrame>,
|
|
871
|
+
branchDepth: BranchDepth,
|
|
872
|
+
): Promise<unknown> {
|
|
983
873
|
switch (binding.kind) {
|
|
984
874
|
case "constant":
|
|
985
875
|
return binding.value;
|
|
@@ -999,22 +889,26 @@ export class DependencyResolver implements ResolverCallbacks {
|
|
|
999
889
|
return binding.factory(ctx);
|
|
1000
890
|
|
|
1001
891
|
case "class": {
|
|
1002
|
-
const deps = await this.#
|
|
1003
|
-
|
|
1004
|
-
|
|
892
|
+
const deps = await this.#resolveDepsAsync(
|
|
893
|
+
this.#constructorParams(binding.target),
|
|
894
|
+
resolutionPath,
|
|
895
|
+
resolutionStack,
|
|
896
|
+
branchDepth,
|
|
897
|
+
);
|
|
898
|
+
return this.#classes.instantiate(binding.target, deps);
|
|
1005
899
|
}
|
|
1006
900
|
|
|
1007
901
|
case "resolved": {
|
|
1008
902
|
if (ctx === undefined) {
|
|
1009
903
|
throw new InternalError("resolved binding requires resolution context");
|
|
1010
904
|
}
|
|
1011
|
-
const deps = await this.#
|
|
905
|
+
const deps = await this.#resolveDepsAsync(binding.deps, resolutionPath, resolutionStack, branchDepth);
|
|
1012
906
|
const factoryResult = binding.factory(...deps);
|
|
1013
907
|
return factoryResult instanceof Promise ? factoryResult : Promise.resolve(factoryResult);
|
|
1014
908
|
}
|
|
1015
909
|
|
|
1016
910
|
case "resolved-async": {
|
|
1017
|
-
const deps = await this.#
|
|
911
|
+
const deps = await this.#resolveDepsAsync(binding.deps, resolutionPath, resolutionStack, branchDepth);
|
|
1018
912
|
return binding.factory(...deps);
|
|
1019
913
|
}
|
|
1020
914
|
|
|
@@ -1023,114 +917,45 @@ export class DependencyResolver implements ResolverCallbacks {
|
|
|
1023
917
|
}
|
|
1024
918
|
}
|
|
1025
919
|
|
|
1026
|
-
async #
|
|
1027
|
-
|
|
920
|
+
async #resolveDepsAsync(
|
|
921
|
+
deps: ReadonlyArray<DependencySlot>,
|
|
1028
922
|
resolutionPath: Array<string>,
|
|
1029
923
|
resolutionStack: Array<ResolutionFrame>,
|
|
924
|
+
branchDepth: BranchDepth,
|
|
1030
925
|
): Promise<Array<unknown>> {
|
|
1031
|
-
const
|
|
1032
|
-
if (
|
|
1033
|
-
if (target.length === 0) {
|
|
1034
|
-
return [];
|
|
1035
|
-
}
|
|
1036
|
-
throw new MissingMetadataError(target.name);
|
|
1037
|
-
}
|
|
1038
|
-
if (meta.params.length === 0) {
|
|
926
|
+
const count = deps.length;
|
|
927
|
+
if (count === 0) {
|
|
1039
928
|
return [];
|
|
1040
929
|
}
|
|
1041
|
-
if (
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
}
|
|
1050
|
-
if (paramOptions === undefined) {
|
|
1051
|
-
return [await this.resolveAsyncFromContext(param.token, resolutionPath, resolutionStack)];
|
|
1052
|
-
}
|
|
1053
|
-
return [await this.resolveAsync(param.token, paramOptions, resolutionPath, resolutionStack)];
|
|
1054
|
-
}
|
|
1055
|
-
const pending = new Array<Promise<unknown>>(meta.params.length);
|
|
1056
|
-
const shouldCloneContext = meta.params.length > 1;
|
|
1057
|
-
for (let index = 0; index < meta.params.length; index += 1) {
|
|
1058
|
-
const param = meta.params[index]!;
|
|
1059
|
-
const paramOptions = injectionSlotToResolveOptions(param);
|
|
1060
|
-
if (param.multi) {
|
|
1061
|
-
pending[index] = this.resolveAllAsync(
|
|
1062
|
-
param.token,
|
|
1063
|
-
paramOptions,
|
|
1064
|
-
shouldCloneContext ? [...resolutionPath] : resolutionPath,
|
|
1065
|
-
shouldCloneContext ? [...resolutionStack] : resolutionStack,
|
|
1066
|
-
);
|
|
1067
|
-
} else if (param.optional) {
|
|
1068
|
-
pending[index] = this.resolveOptionalAsync(
|
|
1069
|
-
param.token,
|
|
1070
|
-
paramOptions,
|
|
1071
|
-
shouldCloneContext ? [...resolutionPath] : resolutionPath,
|
|
1072
|
-
shouldCloneContext ? [...resolutionStack] : resolutionStack,
|
|
1073
|
-
);
|
|
1074
|
-
} else {
|
|
1075
|
-
pending[index] =
|
|
1076
|
-
paramOptions === undefined
|
|
1077
|
-
? this.resolveAsyncFromContext(
|
|
1078
|
-
param.token,
|
|
1079
|
-
shouldCloneContext ? [...resolutionPath] : resolutionPath,
|
|
1080
|
-
shouldCloneContext ? [...resolutionStack] : resolutionStack,
|
|
1081
|
-
)
|
|
1082
|
-
: this.resolveAsync(
|
|
1083
|
-
param.token,
|
|
1084
|
-
paramOptions,
|
|
1085
|
-
shouldCloneContext ? [...resolutionPath] : resolutionPath,
|
|
1086
|
-
shouldCloneContext ? [...resolutionStack] : resolutionStack,
|
|
1087
|
-
);
|
|
1088
|
-
}
|
|
930
|
+
if (count === 1) {
|
|
931
|
+
return [await this.#resolveDepAsync(deps[0]!, resolutionPath, resolutionStack, branchDepth)];
|
|
932
|
+
}
|
|
933
|
+
// Siblings resolve concurrently and each extends the same branch, so the first appends in
|
|
934
|
+
// place and the rest copy the prefix — no caller has to isolate them.
|
|
935
|
+
const pending = new Array<Promise<unknown>>(count);
|
|
936
|
+
for (let index = 0; index < count; index += 1) {
|
|
937
|
+
pending[index] = this.#resolveDepAsync(deps[index]!, resolutionPath, resolutionStack, branchDepth);
|
|
1089
938
|
}
|
|
1090
939
|
return Promise.all(pending);
|
|
1091
940
|
}
|
|
1092
941
|
|
|
1093
|
-
|
|
1094
|
-
|
|
942
|
+
#resolveDepAsync(
|
|
943
|
+
dep: DependencySlot,
|
|
1095
944
|
resolutionPath: Array<string>,
|
|
1096
945
|
resolutionStack: Array<ResolutionFrame>,
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
const
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
const depOptions = injectionSlotToResolveOptions(dep);
|
|
1103
|
-
if (dep.multi) {
|
|
1104
|
-
pending[index] = this.resolveAllAsync(
|
|
1105
|
-
dep.token as Token<unknown> | Constructor,
|
|
1106
|
-
depOptions,
|
|
1107
|
-
shouldCloneContext ? [...resolutionPath] : resolutionPath,
|
|
1108
|
-
shouldCloneContext ? [...resolutionStack] : resolutionStack,
|
|
1109
|
-
);
|
|
1110
|
-
} else if (dep.optional) {
|
|
1111
|
-
pending[index] = this.resolveOptionalAsync(
|
|
1112
|
-
dep.token as Token<unknown> | Constructor,
|
|
1113
|
-
depOptions,
|
|
1114
|
-
shouldCloneContext ? [...resolutionPath] : resolutionPath,
|
|
1115
|
-
shouldCloneContext ? [...resolutionStack] : resolutionStack,
|
|
1116
|
-
);
|
|
1117
|
-
} else {
|
|
1118
|
-
pending[index] =
|
|
1119
|
-
depOptions === undefined
|
|
1120
|
-
? this.resolveAsyncFromContext(
|
|
1121
|
-
dep.token as Token<unknown> | Constructor,
|
|
1122
|
-
shouldCloneContext ? [...resolutionPath] : resolutionPath,
|
|
1123
|
-
shouldCloneContext ? [...resolutionStack] : resolutionStack,
|
|
1124
|
-
)
|
|
1125
|
-
: this.resolveAsync(
|
|
1126
|
-
dep.token as Token<unknown> | Constructor,
|
|
1127
|
-
depOptions,
|
|
1128
|
-
shouldCloneContext ? [...resolutionPath] : resolutionPath,
|
|
1129
|
-
shouldCloneContext ? [...resolutionStack] : resolutionStack,
|
|
1130
|
-
);
|
|
1131
|
-
}
|
|
946
|
+
branchDepth: BranchDepth,
|
|
947
|
+
): Promise<unknown> {
|
|
948
|
+
const options = injectionSlotToResolveOptions(dep);
|
|
949
|
+
if (dep.multi) {
|
|
950
|
+
return this.resolveAllAsync(dep.token, options, resolutionPath, resolutionStack, branchDepth);
|
|
1132
951
|
}
|
|
1133
|
-
|
|
952
|
+
if (dep.optional) {
|
|
953
|
+
return this.resolveOptionalAsync(dep.token, options, resolutionPath, resolutionStack, branchDepth);
|
|
954
|
+
}
|
|
955
|
+
if (options === undefined) {
|
|
956
|
+
return this.resolveAsyncFromContext(dep.token, resolutionPath, resolutionStack, branchDepth);
|
|
957
|
+
}
|
|
958
|
+
return this.resolveAsync(dep.token, options, resolutionPath, resolutionStack, branchDepth);
|
|
1134
959
|
}
|
|
1135
960
|
|
|
1136
961
|
async resolveOptionalAsync<const Value>(
|
|
@@ -1138,11 +963,12 @@ export class DependencyResolver implements ResolverCallbacks {
|
|
|
1138
963
|
options: ResolveOptions | undefined,
|
|
1139
964
|
resolutionPath: Array<string>,
|
|
1140
965
|
resolutionStack: Array<ResolutionFrame>,
|
|
966
|
+
branchDepth: BranchDepth = UNOWNED_BRANCH,
|
|
1141
967
|
): Promise<Value | undefined> {
|
|
1142
968
|
if (this.#findBinding(token, options, resolutionPath, resolutionStack) === undefined) {
|
|
1143
969
|
return undefined;
|
|
1144
970
|
}
|
|
1145
|
-
return this.resolveAsync(token, options, resolutionPath, resolutionStack);
|
|
971
|
+
return this.resolveAsync(token, options, resolutionPath, resolutionStack, branchDepth);
|
|
1146
972
|
}
|
|
1147
973
|
|
|
1148
974
|
async resolveAllAsync<const Value>(
|
|
@@ -1150,85 +976,88 @@ export class DependencyResolver implements ResolverCallbacks {
|
|
|
1150
976
|
options: ResolveOptions | undefined,
|
|
1151
977
|
resolutionPath: Array<string>,
|
|
1152
978
|
resolutionStack: Array<ResolutionFrame>,
|
|
979
|
+
branchDepth: BranchDepth = UNOWNED_BRANCH,
|
|
1153
980
|
): Promise<Array<Value>> {
|
|
1154
|
-
|
|
1155
|
-
const namedCandidates = this.#getSimpleNamedBindingsFromChain(token, options.name);
|
|
1156
|
-
if (namedCandidates.length === 0) {
|
|
1157
|
-
return [];
|
|
1158
|
-
}
|
|
1159
|
-
const pending = new Array<Promise<Value>>(namedCandidates.length);
|
|
1160
|
-
for (let index = 0; index < namedCandidates.length; index += 1) {
|
|
1161
|
-
pending[index] = this.#resolveCandidateAsync(
|
|
1162
|
-
namedCandidates[index] as Binding<Value>,
|
|
1163
|
-
options,
|
|
1164
|
-
resolutionPath,
|
|
1165
|
-
resolutionStack,
|
|
1166
|
-
);
|
|
1167
|
-
}
|
|
1168
|
-
return Promise.all(pending);
|
|
1169
|
-
}
|
|
1170
|
-
|
|
1171
|
-
const allBindings = this.#getAllBindingsFromChain(token);
|
|
1172
|
-
if (allBindings.length === 0) {
|
|
1173
|
-
return [];
|
|
1174
|
-
}
|
|
1175
|
-
|
|
1176
|
-
const ctx = this.#makeConstraintContext(resolutionPath, resolutionStack, options);
|
|
1177
|
-
const candidates = selectAllBindings(allBindings, options, ctx);
|
|
1178
|
-
|
|
981
|
+
const candidates = this.#candidateBindings(token, options, resolutionPath, resolutionStack);
|
|
1179
982
|
const pending = new Array<Promise<Value>>(candidates.length);
|
|
1180
983
|
for (let index = 0; index < candidates.length; index += 1) {
|
|
1181
984
|
pending[index] = this.#resolveCandidateAsync(
|
|
1182
|
-
candidates[index]
|
|
985
|
+
candidates[index]!,
|
|
1183
986
|
options,
|
|
1184
987
|
resolutionPath,
|
|
1185
988
|
resolutionStack,
|
|
1186
|
-
|
|
989
|
+
branchDepth,
|
|
990
|
+
) as Promise<Value>;
|
|
1187
991
|
}
|
|
1188
992
|
return Promise.all(pending);
|
|
1189
993
|
}
|
|
1190
994
|
|
|
1191
995
|
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
1192
996
|
|
|
1193
|
-
#
|
|
997
|
+
#allBindingsFromChain(token: Token<unknown> | Constructor): ReadonlyArray<Binding> {
|
|
1194
998
|
const ownBindings = this.#registry.getAll(token);
|
|
1195
999
|
if (this.#parent === undefined) {
|
|
1196
1000
|
return ownBindings;
|
|
1197
1001
|
}
|
|
1198
1002
|
const result: Array<Binding> = [...ownBindings];
|
|
1199
|
-
let current: DependencyResolver | undefined = this.#parent;
|
|
1200
|
-
while (current !== undefined) {
|
|
1003
|
+
for (let current: DependencyResolver | undefined = this.#parent; current !== undefined; current = current.#parent) {
|
|
1201
1004
|
const own = current.#registry.getAll(token);
|
|
1202
1005
|
if (own.length > 0) {
|
|
1203
1006
|
result.push(...own);
|
|
1204
1007
|
}
|
|
1205
|
-
current = current.#parent;
|
|
1206
1008
|
}
|
|
1207
1009
|
return result;
|
|
1208
1010
|
}
|
|
1209
1011
|
|
|
1210
|
-
|
|
1012
|
+
/** Every binding the chain's name indexes hold for one name, nearest container first. */
|
|
1013
|
+
#namedBindingsFromChain(token: Token<unknown> | Constructor, name: string): Array<Binding> {
|
|
1014
|
+
// A name resolves to at most one binding per registry, so a root container's answer is built
|
|
1015
|
+
// whole rather than grown — the list is sized at its allocation.
|
|
1211
1016
|
const ownBinding = this.#registry.getSimpleNamed(token, name);
|
|
1212
1017
|
if (this.#parent === undefined) {
|
|
1213
|
-
return ownBinding
|
|
1214
|
-
}
|
|
1215
|
-
const result: Array<Binding> = [];
|
|
1216
|
-
if (ownBinding !== undefined) {
|
|
1217
|
-
result.push(ownBinding);
|
|
1018
|
+
return ownBinding === undefined ? [] : [ownBinding];
|
|
1218
1019
|
}
|
|
1219
|
-
|
|
1220
|
-
|
|
1020
|
+
const result: Array<Binding> = ownBinding === undefined ? [] : [ownBinding];
|
|
1021
|
+
for (let current: DependencyResolver | undefined = this.#parent; current !== undefined; current = current.#parent) {
|
|
1221
1022
|
const binding = current.#registry.getSimpleNamed(token, name);
|
|
1222
1023
|
if (binding !== undefined) {
|
|
1223
1024
|
result.push(binding);
|
|
1224
1025
|
}
|
|
1225
|
-
current = current.#parent;
|
|
1226
1026
|
}
|
|
1227
1027
|
return result;
|
|
1228
1028
|
}
|
|
1229
1029
|
|
|
1230
|
-
|
|
1231
|
-
|
|
1030
|
+
/** A constant with no activation anywhere resolves to its value with no pipeline at all. */
|
|
1031
|
+
#isPlainConstant(binding: Binding): binding is ConstantBinding<unknown> {
|
|
1032
|
+
return (
|
|
1033
|
+
binding.kind === "constant" &&
|
|
1034
|
+
binding.onActivation === undefined &&
|
|
1035
|
+
(this.#lifecycle.activationVersion === 0 || !this.#lifecycle.hasActivationHandlers(binding.token))
|
|
1036
|
+
);
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
/** Whether either an own hook or a container-level hook would run for this binding. */
|
|
1040
|
+
#hasAnyActivation(binding: DynamicBinding<unknown> | DynamicAsyncBinding<unknown>): boolean {
|
|
1041
|
+
if (binding.onActivation !== undefined) {
|
|
1042
|
+
return true;
|
|
1043
|
+
}
|
|
1044
|
+
return this.#lifecycle.activationVersion !== 0 && this.#lifecycle.hasActivationHandlers(binding.token);
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
/**
|
|
1048
|
+
* The cached instance of a `scoped` binding, or {@link SCOPED_MISS}.
|
|
1049
|
+
*
|
|
1050
|
+
* @remarks A `scoped` binding outside a child container is a configuration error, not a miss, so
|
|
1051
|
+
* the check lives with the read that depends on it.
|
|
1052
|
+
*/
|
|
1053
|
+
#readScoped(binding: Binding): unknown {
|
|
1054
|
+
if (!this.#scope.isChild) {
|
|
1055
|
+
throw new MissingScopeContextError(tokenName(binding.token));
|
|
1056
|
+
}
|
|
1057
|
+
if (this.#scope.hasScoped(binding.id)) {
|
|
1058
|
+
return this.#scope.getScoped(binding.id);
|
|
1059
|
+
}
|
|
1060
|
+
return SCOPED_MISS;
|
|
1232
1061
|
}
|
|
1233
1062
|
|
|
1234
1063
|
#makeConstraintContext(
|
|
@@ -1239,13 +1068,11 @@ export class DependencyResolver implements ResolverCallbacks {
|
|
|
1239
1068
|
if (options === undefined && resolutionPath.length === 0 && resolutionStack.length === 0) {
|
|
1240
1069
|
return ROOT_CONSTRAINT_CONTEXT;
|
|
1241
1070
|
}
|
|
1242
|
-
const parent = resolutionStack.at(-1);
|
|
1243
|
-
const ancestors = resolutionStack.length > 1 ? resolutionStack.slice(0, -1) : [];
|
|
1244
1071
|
return {
|
|
1245
1072
|
resolutionPath,
|
|
1246
1073
|
resolutionStack,
|
|
1247
|
-
parent,
|
|
1248
|
-
ancestors,
|
|
1074
|
+
parent: resolutionStack.at(-1),
|
|
1075
|
+
ancestors: resolutionStack.length > 1 ? resolutionStack.slice(0, -1) : [],
|
|
1249
1076
|
currentResolveOptions: options,
|
|
1250
1077
|
};
|
|
1251
1078
|
}
|
|
@@ -1256,80 +1083,20 @@ export class DependencyResolver implements ResolverCallbacks {
|
|
|
1256
1083
|
resolutionPath: Array<string>,
|
|
1257
1084
|
resolutionStack: Array<ResolutionFrame>,
|
|
1258
1085
|
): boolean {
|
|
1259
|
-
if (!
|
|
1086
|
+
if (!matchesSlot(binding.slot, options)) {
|
|
1260
1087
|
return false;
|
|
1261
1088
|
}
|
|
1262
1089
|
if (binding.predicate === undefined) {
|
|
1263
1090
|
return true;
|
|
1264
1091
|
}
|
|
1265
|
-
|
|
1266
|
-
return binding.predicate(ctx);
|
|
1267
|
-
}
|
|
1268
|
-
|
|
1269
|
-
#matchesSlotFast(slot: BindingSlot, options: ResolveOptions | undefined): boolean {
|
|
1270
|
-
const requestedName = options?.name;
|
|
1271
|
-
const requestedTags = options?.tags;
|
|
1272
|
-
const singleRequestedTag = options?.tag;
|
|
1273
|
-
const hasRequestedTags = (requestedTags?.length ?? 0) > 0 || singleRequestedTag !== undefined;
|
|
1274
|
-
|
|
1275
|
-
if (slot.name !== undefined) {
|
|
1276
|
-
if (requestedName === undefined || slot.name !== requestedName) {
|
|
1277
|
-
return false;
|
|
1278
|
-
}
|
|
1279
|
-
} else if (requestedName !== undefined) {
|
|
1280
|
-
return false;
|
|
1281
|
-
}
|
|
1282
|
-
|
|
1283
|
-
if (slot.tags.length > 0) {
|
|
1284
|
-
if (!hasRequestedTags) {
|
|
1285
|
-
return false;
|
|
1286
|
-
}
|
|
1287
|
-
for (const [tagKey, tagValue] of slot.tags) {
|
|
1288
|
-
if (!this.#matchesRequestedTag(tagKey, tagValue, requestedTags, singleRequestedTag)) {
|
|
1289
|
-
return false;
|
|
1290
|
-
}
|
|
1291
|
-
}
|
|
1292
|
-
} else if (hasRequestedTags) {
|
|
1293
|
-
return false;
|
|
1294
|
-
}
|
|
1295
|
-
|
|
1296
|
-
return true;
|
|
1297
|
-
}
|
|
1298
|
-
|
|
1299
|
-
#getTokenName(token: Token<unknown> | Constructor): string {
|
|
1300
|
-
return tokenName(token);
|
|
1301
|
-
}
|
|
1302
|
-
|
|
1303
|
-
#matchesRequestedTag(
|
|
1304
|
-
tagKey: string,
|
|
1305
|
-
tagValue: unknown,
|
|
1306
|
-
requestedTags: ReadonlyArray<BindingTag> | undefined,
|
|
1307
|
-
singleRequestedTag: BindingTag | undefined,
|
|
1308
|
-
): boolean {
|
|
1309
|
-
if (
|
|
1310
|
-
singleRequestedTag !== undefined &&
|
|
1311
|
-
singleRequestedTag[0] === tagKey &&
|
|
1312
|
-
Object.is(singleRequestedTag[1], tagValue)
|
|
1313
|
-
) {
|
|
1314
|
-
return true;
|
|
1315
|
-
}
|
|
1316
|
-
if (requestedTags === undefined || requestedTags.length === 0) {
|
|
1317
|
-
return false;
|
|
1318
|
-
}
|
|
1319
|
-
for (let index = 0; index < requestedTags.length; index += 1) {
|
|
1320
|
-
const requestedTag = requestedTags[index]!;
|
|
1321
|
-
if (requestedTag[0] === tagKey && Object.is(requestedTag[1], tagValue)) {
|
|
1322
|
-
return true;
|
|
1323
|
-
}
|
|
1324
|
-
}
|
|
1325
|
-
return false;
|
|
1092
|
+
return binding.predicate(this.#makeConstraintContext(resolutionPath, resolutionStack, options));
|
|
1326
1093
|
}
|
|
1327
1094
|
|
|
1328
|
-
#resolveTransientDynamicSyncFromContext
|
|
1329
|
-
binding:
|
|
1095
|
+
#resolveTransientDynamicSyncFromContext(
|
|
1096
|
+
binding: DynamicBinding<unknown>,
|
|
1330
1097
|
resolutionPath: Array<string>,
|
|
1331
1098
|
resolutionStack: Array<ResolutionFrame>,
|
|
1332
|
-
):
|
|
1099
|
+
): unknown {
|
|
1333
1100
|
// One lane at every depth: `binding.inFlight` is O(1), so there is nothing to escape.
|
|
1334
1101
|
const frame = this.#getResolutionFrame(binding);
|
|
1335
1102
|
const tokenDisplayName = frame.tokenName;
|
|
@@ -1354,143 +1121,148 @@ export class DependencyResolver implements ResolverCallbacks {
|
|
|
1354
1121
|
}
|
|
1355
1122
|
|
|
1356
1123
|
// Deliberately not `async`: that would allocate a state machine and a promise per level.
|
|
1357
|
-
#resolveTransientDynamicAsyncFromContext
|
|
1358
|
-
binding:
|
|
1124
|
+
#resolveTransientDynamicAsyncFromContext(
|
|
1125
|
+
binding: DynamicBinding<unknown> | DynamicAsyncBinding<unknown>,
|
|
1359
1126
|
resolutionPath: Array<string>,
|
|
1360
1127
|
resolutionStack: Array<ResolutionFrame>,
|
|
1361
|
-
|
|
1362
|
-
): Promise<
|
|
1363
|
-
// Path-scoped cycle detection, because async chains interleave — see ARCHITECTURE.md.
|
|
1364
|
-
const pool = this.#asyncChainContextPool;
|
|
1128
|
+
branchDepth: BranchDepth,
|
|
1129
|
+
): Promise<unknown> {
|
|
1365
1130
|
const frame = this.#getResolutionFrame(binding);
|
|
1366
|
-
|
|
1131
|
+
let levelPath: OwnedBranchPath;
|
|
1367
1132
|
try {
|
|
1368
|
-
|
|
1133
|
+
levelPath = extendResolutionBranch(resolutionPath, branchDepth, frame.tokenName);
|
|
1369
1134
|
} catch (cycleError) {
|
|
1370
1135
|
// This method is not `async`; keep failures as rejections rather than sync throws.
|
|
1371
1136
|
return Promise.reject(cycleError);
|
|
1372
1137
|
}
|
|
1138
|
+
const levelStack = extendResolutionStackBranch(resolutionStack, branchDepth, frame);
|
|
1373
1139
|
|
|
1374
|
-
//
|
|
1375
|
-
|
|
1376
|
-
const ctx =
|
|
1377
|
-
callerContext !== undefined && callerContext.owner === this
|
|
1378
|
-
? callerContext
|
|
1379
|
-
: this.#acquireAsyncChainContext(resolutionPath, resolutionStack);
|
|
1380
|
-
ctx.chainLevels += 1;
|
|
1381
|
-
|
|
1382
|
-
// Invoke the factory synchronously to get its Promise (or a resolved value for "dynamic").
|
|
1383
|
-
let factoryPromise: Promise<Value>;
|
|
1140
|
+
// Nothing this level appended is ever removed, so no level observes its own settlement.
|
|
1141
|
+
const ctx = new AsyncLevelContext(this, levelPath, levelStack, undefined);
|
|
1384
1142
|
try {
|
|
1385
1143
|
if (binding.kind === "dynamic-async") {
|
|
1386
|
-
|
|
1387
|
-
} else {
|
|
1388
|
-
const factoryResult = binding.factory(ctx);
|
|
1389
|
-
factoryPromise =
|
|
1390
|
-
factoryResult instanceof Promise ? (factoryResult as Promise<Value>) : Promise.resolve(factoryResult);
|
|
1144
|
+
return binding.factory(ctx);
|
|
1391
1145
|
}
|
|
1146
|
+
const factoryResult = binding.factory(ctx);
|
|
1147
|
+
return factoryResult instanceof Promise ? factoryResult : Promise.resolve(factoryResult);
|
|
1392
1148
|
} catch (factoryError) {
|
|
1393
|
-
// Synchronous throw from the factory (rare) — clean up immediately.
|
|
1394
|
-
exitResolutionPath(resolutionPath);
|
|
1395
|
-
ctx.chainLevels -= 1;
|
|
1396
|
-
if (ctx.chainLevels === 0) {
|
|
1397
|
-
pool.push(ctx);
|
|
1398
|
-
}
|
|
1399
1149
|
return Promise.reject(factoryError);
|
|
1400
1150
|
}
|
|
1151
|
+
}
|
|
1401
1152
|
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
return
|
|
1153
|
+
// ── The cascade lane ───────────────────────────────────────────────────────
|
|
1154
|
+
|
|
1155
|
+
/**
|
|
1156
|
+
* Entry for a request a factory makes from inside an open synchronous cascade.
|
|
1157
|
+
*
|
|
1158
|
+
* @remarks A request arriving with no cascade open came out of a continuation, so its ancestors
|
|
1159
|
+
* are on no call stack — it escapes to the branch lane. See `ARCHITECTURE.md`.
|
|
1160
|
+
*/
|
|
1161
|
+
resolveAsyncFromCascade(token: Token<unknown> | Constructor): Promise<unknown> {
|
|
1162
|
+
if (this.#cascadePath.length === 0) {
|
|
1163
|
+
return this.resolveAsyncFromContext(token, [], [], ROOT_BRANCH);
|
|
1164
|
+
}
|
|
1165
|
+
return this.#dispatchCascade(token);
|
|
1415
1166
|
}
|
|
1416
1167
|
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1168
|
+
/** Entry for a resolve the container starts, which opens the cascade rather than joining one. */
|
|
1169
|
+
resolveAsyncFromRoot(token: Token<unknown> | Constructor): Promise<unknown> {
|
|
1170
|
+
return this.#dispatchCascade(token);
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
#dispatchCascade(token: Token<unknown> | Constructor): Promise<unknown> {
|
|
1174
|
+
const fastBinding = this.#registry.getFastDefault(token);
|
|
1175
|
+
if (fastBinding !== undefined) {
|
|
1176
|
+
if (
|
|
1177
|
+
(fastBinding.kind === "dynamic-async" || fastBinding.kind === "dynamic") &&
|
|
1178
|
+
fastBinding.scope === "transient" &&
|
|
1179
|
+
!this.#hasAnyActivation(fastBinding)
|
|
1180
|
+
) {
|
|
1181
|
+
return this.#resolveTransientDynamicAsyncCascade(fastBinding);
|
|
1182
|
+
}
|
|
1183
|
+
// A value that already exists answers here: escaping would snapshot the cascade for a resolve
|
|
1184
|
+
// that never looks at a path.
|
|
1185
|
+
if (this.#isPlainConstant(fastBinding)) {
|
|
1186
|
+
return Promise.resolve(fastBinding.value);
|
|
1187
|
+
}
|
|
1188
|
+
if (fastBinding.scope === "singleton" && fastBinding.instance !== NO_INSTANCE) {
|
|
1189
|
+
return Promise.resolve(fastBinding.instance);
|
|
1190
|
+
}
|
|
1424
1191
|
}
|
|
1425
|
-
|
|
1426
|
-
|
|
1192
|
+
// Anything else leaves the cascade lane for good, seeded with a snapshot of the ancestors it
|
|
1193
|
+
// accumulated — so a cycle across the boundary is still on one path.
|
|
1194
|
+
return this.resolveAsyncFromContext(token, [...this.#cascadePath], [...this.#cascadeStack], UNOWNED_BRANCH);
|
|
1427
1195
|
}
|
|
1428
1196
|
|
|
1429
|
-
#
|
|
1430
|
-
binding:
|
|
1197
|
+
#resolveTransientDynamicAsyncCascade(
|
|
1198
|
+
binding: DynamicBinding<unknown> | DynamicAsyncBinding<unknown>,
|
|
1199
|
+
): Promise<unknown> {
|
|
1200
|
+
const frame = this.#getResolutionFrame(binding);
|
|
1201
|
+
// The request that closes a cycle is made from a factory's synchronous prefix, and synchronous
|
|
1202
|
+
// code does not interleave — so the O(1) flag is exact path membership here, as it is for the
|
|
1203
|
+
// sync lane. It is cleared when the factory returns its promise, not when that promise settles.
|
|
1204
|
+
if (binding.inFlight) {
|
|
1205
|
+
return Promise.reject(new CircularDependencyError([...this.#cascadePath, frame.tokenName]));
|
|
1206
|
+
}
|
|
1207
|
+
const ctx = (this.#cascadeContext ??= new AsyncCascadeContext(this, this.#cascadePath, this.#cascadeStack));
|
|
1208
|
+
binding.inFlight = true;
|
|
1209
|
+
this.#cascadePath.push(frame.tokenName);
|
|
1210
|
+
this.#cascadeStack.push(frame);
|
|
1211
|
+
try {
|
|
1212
|
+
if (binding.kind === "dynamic-async") {
|
|
1213
|
+
return binding.factory(ctx);
|
|
1214
|
+
}
|
|
1215
|
+
const factoryResult = binding.factory(ctx);
|
|
1216
|
+
return factoryResult instanceof Promise ? factoryResult : Promise.resolve(factoryResult);
|
|
1217
|
+
} catch (factoryError) {
|
|
1218
|
+
return Promise.reject(factoryError);
|
|
1219
|
+
} finally {
|
|
1220
|
+
this.#cascadeStack.pop();
|
|
1221
|
+
this.#cascadePath.pop();
|
|
1222
|
+
binding.inFlight = false;
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
// A cached candidate answers here rather than re-entering the generic path: `resolveAll` pays
|
|
1227
|
+
// this per candidate, and a fan-out over cached handlers is the shape that makes it matter.
|
|
1228
|
+
#resolveCandidateSync(
|
|
1229
|
+
binding: Binding,
|
|
1431
1230
|
options: ResolveOptions | undefined,
|
|
1432
1231
|
resolutionPath: Array<string>,
|
|
1433
1232
|
resolutionStack: Array<ResolutionFrame>,
|
|
1434
|
-
):
|
|
1435
|
-
if (
|
|
1436
|
-
binding.kind === "constant" &&
|
|
1437
|
-
binding.onActivation === undefined &&
|
|
1438
|
-
!this.#lifecycle.hasActivationHandlers(binding.token)
|
|
1439
|
-
) {
|
|
1233
|
+
): unknown {
|
|
1234
|
+
if (this.#isPlainConstant(binding)) {
|
|
1440
1235
|
return binding.value;
|
|
1441
1236
|
}
|
|
1442
1237
|
if (binding.kind === "alias") {
|
|
1443
1238
|
return this.resolve(binding.target, options, resolutionPath, resolutionStack);
|
|
1444
1239
|
}
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
return binding.instance as Value;
|
|
1448
|
-
}
|
|
1449
|
-
if (scope === "scoped") {
|
|
1450
|
-
if (!this.#scope.isChild) {
|
|
1451
|
-
throw new MissingScopeContextError(this.#getTokenName(binding.token));
|
|
1452
|
-
}
|
|
1453
|
-
if (this.#scope.hasScoped(binding.id)) {
|
|
1454
|
-
return this.#scope.getScoped<Value>(binding.id);
|
|
1455
|
-
}
|
|
1240
|
+
if (binding.scope === "singleton" && binding.instance !== NO_INSTANCE) {
|
|
1241
|
+
return binding.instance;
|
|
1456
1242
|
}
|
|
1457
1243
|
return this.#resolveBinding(binding, options, resolutionPath, resolutionStack);
|
|
1458
1244
|
}
|
|
1459
1245
|
|
|
1460
|
-
#resolveCandidateAsync
|
|
1461
|
-
binding: Binding
|
|
1246
|
+
#resolveCandidateAsync(
|
|
1247
|
+
binding: Binding,
|
|
1462
1248
|
options: ResolveOptions | undefined,
|
|
1463
1249
|
resolutionPath: Array<string>,
|
|
1464
1250
|
resolutionStack: Array<ResolutionFrame>,
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
binding.onActivation === undefined &&
|
|
1469
|
-
!this.#lifecycle.hasActivationHandlers(binding.token)
|
|
1470
|
-
) {
|
|
1251
|
+
branchDepth: BranchDepth,
|
|
1252
|
+
): Promise<unknown> {
|
|
1253
|
+
if (this.#isPlainConstant(binding)) {
|
|
1471
1254
|
return Promise.resolve(binding.value);
|
|
1472
1255
|
}
|
|
1473
|
-
const isolatedPath = [...resolutionPath];
|
|
1474
|
-
const isolatedStack = [...resolutionStack];
|
|
1475
1256
|
if (binding.kind === "alias") {
|
|
1476
|
-
return this.resolveAsync(binding.target, options,
|
|
1477
|
-
}
|
|
1478
|
-
const scope = (binding as BindingWithScope).scope ?? "transient";
|
|
1479
|
-
if (scope === "singleton" && binding.instance !== NO_INSTANCE) {
|
|
1480
|
-
return Promise.resolve(binding.instance as Value);
|
|
1257
|
+
return this.resolveAsync(binding.target, options, resolutionPath, resolutionStack, branchDepth);
|
|
1481
1258
|
}
|
|
1482
|
-
if (scope === "
|
|
1483
|
-
|
|
1484
|
-
return Promise.reject(new MissingScopeContextError(this.#getTokenName(binding.token)));
|
|
1485
|
-
}
|
|
1486
|
-
if (this.#scope.hasScoped(binding.id)) {
|
|
1487
|
-
return Promise.resolve(this.#scope.getScoped<Value>(binding.id));
|
|
1488
|
-
}
|
|
1259
|
+
if (binding.scope === "singleton" && binding.instance !== NO_INSTANCE) {
|
|
1260
|
+
return Promise.resolve(binding.instance);
|
|
1489
1261
|
}
|
|
1490
|
-
return this.#resolveBindingAsync(binding, options,
|
|
1262
|
+
return this.#resolveBindingAsync(binding, options, resolutionPath, resolutionStack, branchDepth);
|
|
1491
1263
|
}
|
|
1492
1264
|
|
|
1493
|
-
#getResolutionFrame
|
|
1265
|
+
#getResolutionFrame(binding: Binding): ResolutionFrame {
|
|
1494
1266
|
// Memoized on the binding rather than in a per-resolver Map: the frame derives only from
|
|
1495
1267
|
// immutable binding fields, so it is identical for every resolver, and a field read beats a
|
|
1496
1268
|
// Map lookup on every hop of a chain.
|
|
@@ -1498,16 +1270,11 @@ export class DependencyResolver implements ResolverCallbacks {
|
|
|
1498
1270
|
if (existing !== undefined) {
|
|
1499
1271
|
return existing;
|
|
1500
1272
|
}
|
|
1501
|
-
const
|
|
1502
|
-
const frame = buildResolutionFrame(tokenName(binding.token), scope, binding.id, binding.kind, binding.slot);
|
|
1273
|
+
const frame = buildResolutionFrame(tokenName(binding.token), binding.scope, binding.id, binding.kind, binding.slot);
|
|
1503
1274
|
binding.frame = frame;
|
|
1504
1275
|
return frame;
|
|
1505
1276
|
}
|
|
1506
1277
|
|
|
1507
|
-
#requiresResolutionContext<const Value>(binding: Binding<Value>): boolean {
|
|
1508
|
-
return binding.kind === "dynamic" || binding.kind === "dynamic-async";
|
|
1509
|
-
}
|
|
1510
|
-
|
|
1511
1278
|
#acquireSyncResolutionContext(
|
|
1512
1279
|
resolutionPath: Array<string>,
|
|
1513
1280
|
resolutionStack: Array<ResolutionFrame>,
|
|
@@ -1524,3 +1291,33 @@ export class DependencyResolver implements ResolverCallbacks {
|
|
|
1524
1291
|
return created;
|
|
1525
1292
|
}
|
|
1526
1293
|
}
|
|
1294
|
+
|
|
1295
|
+
/** Absent scoped entry — distinguishes it from a cached `undefined`. */
|
|
1296
|
+
const SCOPED_MISS: unique symbol = Symbol("di:scoped-miss");
|
|
1297
|
+
|
|
1298
|
+
function anyPredicate(bindings: ReadonlyArray<Binding>): boolean {
|
|
1299
|
+
for (let index = 0; index < bindings.length; index += 1) {
|
|
1300
|
+
if (bindings[index]!.predicate !== undefined) {
|
|
1301
|
+
return true;
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
return false;
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
/** Only a factory is handed the resolution context; everything else gets its deps directly. */
|
|
1308
|
+
function requiresResolutionContext(binding: Binding): boolean {
|
|
1309
|
+
return binding.kind === "dynamic" || binding.kind === "dynamic-async";
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
/**
|
|
1313
|
+
* Whether the tag index's answer is the one `Object.is` would give.
|
|
1314
|
+
*
|
|
1315
|
+
* @remarks An indexed binding has no name, no predicate and exactly one tag, and the request carries
|
|
1316
|
+
* only that tag, so `matchesSlot` reduces to the tag values — and the index matched the key already.
|
|
1317
|
+
* It answers by SameValueZero, which parts from `Object.is` (SPEC §3.5) on exactly one pair: `+0` and
|
|
1318
|
+
* `-0`. So a request whose value is not zero is already exact, and only a zero-valued one is worth
|
|
1319
|
+
* reading the stored value for.
|
|
1320
|
+
*/
|
|
1321
|
+
function matchesIndexedTagValue(binding: Binding, requestedValue: unknown): boolean {
|
|
1322
|
+
return requestedValue !== 0 || Object.is(binding.slot.tags[0]![1], requestedValue);
|
|
1323
|
+
}
|