@codefast/di 0.5.0-canary.7 → 0.5.0-canary.9
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 +205 -0
- package/README.md +6 -2
- package/dist/binding.d.ts +85 -24
- package/dist/binding.d.ts.map +1 -1
- package/dist/binding.js +55 -0
- package/dist/binding.js.map +1 -1
- package/dist/constructor-type.d.ts +4 -5
- package/dist/constructor-type.d.ts.map +1 -1
- package/dist/container/binding-builders.d.ts +32 -11
- package/dist/container/binding-builders.d.ts.map +1 -1
- package/dist/container/binding-builders.js +144 -192
- package/dist/container/binding-builders.js.map +1 -1
- package/dist/container/container.d.ts.map +1 -1
- package/dist/container/container.js +141 -201
- package/dist/container/container.js.map +1 -1
- package/dist/decorators/inject.d.ts +2 -4
- package/dist/decorators/inject.d.ts.map +1 -1
- package/dist/decorators/inject.js.map +1 -1
- package/dist/errors.d.ts +24 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +30 -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 +3 -8
- package/dist/introspection/inspector.js.map +1 -1
- package/dist/metadata/metadata-keys.d.ts +3 -6
- package/dist/metadata/metadata-keys.d.ts.map +1 -1
- package/dist/metadata/metadata-keys.js +3 -6
- package/dist/metadata/metadata-keys.js.map +1 -1
- package/dist/registry.d.ts +14 -2
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +81 -78
- package/dist/registry.js.map +1 -1
- package/dist/resolution/activation-need.d.ts +27 -0
- package/dist/resolution/activation-need.d.ts.map +1 -0
- package/dist/resolution/activation-need.js +68 -0
- package/dist/resolution/activation-need.js.map +1 -0
- package/dist/resolution/binding-lookup-cache.d.ts +41 -0
- package/dist/resolution/binding-lookup-cache.d.ts.map +1 -0
- package/dist/resolution/binding-lookup-cache.js +118 -0
- package/dist/resolution/binding-lookup-cache.js.map +1 -0
- 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 +14 -36
- package/dist/resolution/binding-select.js.map +1 -1
- package/dist/resolution/class-introspector.d.ts +27 -0
- package/dist/resolution/class-introspector.d.ts.map +1 -0
- package/dist/resolution/class-introspector.js +60 -0
- package/dist/resolution/class-introspector.js.map +1 -0
- package/dist/resolution/diagnostics.d.ts +41 -0
- package/dist/resolution/diagnostics.d.ts.map +1 -0
- package/dist/resolution/diagnostics.js +18 -0
- package/dist/resolution/diagnostics.js.map +1 -0
- package/dist/resolution/environment.d.ts +48 -1
- package/dist/resolution/environment.d.ts.map +1 -1
- package/dist/resolution/environment.js +134 -5
- package/dist/resolution/environment.js.map +1 -1
- package/dist/resolution/instantiation-plan.d.ts +15 -15
- package/dist/resolution/instantiation-plan.d.ts.map +1 -1
- package/dist/resolution/instantiation-plan.js +69 -49
- package/dist/resolution/instantiation-plan.js.map +1 -1
- package/dist/resolution/lifecycle.d.ts +2 -0
- package/dist/resolution/lifecycle.d.ts.map +1 -1
- package/dist/resolution/lifecycle.js +60 -62
- package/dist/resolution/lifecycle.js.map +1 -1
- package/dist/resolution/resolution-path.d.ts +84 -17
- package/dist/resolution/resolution-path.d.ts.map +1 -1
- package/dist/resolution/resolution-path.js +68 -23
- 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 +33 -10
- package/dist/resolution/resolver.d.ts.map +1 -1
- package/dist/resolution/resolver.js +471 -830
- package/dist/resolution/resolver.js.map +1 -1
- package/dist/resolution/scope.d.ts +11 -15
- package/dist/resolution/scope.d.ts.map +1 -1
- package/dist/resolution/scope.js +55 -43
- package/dist/resolution/scope.js.map +1 -1
- package/package.json +10 -106
- package/src/binding.ts +146 -24
- package/src/constructor-type.ts +4 -5
- package/src/container/binding-builders.ts +184 -284
- package/src/container/container.ts +161 -221
- package/src/decorators/inject.ts +3 -5
- package/src/errors.ts +38 -0
- package/src/index.ts +4 -1
- package/src/introspection/inspector.ts +3 -9
- package/src/metadata/metadata-keys.ts +3 -6
- package/src/registry.ts +90 -94
- package/src/resolution/activation-need.ts +85 -0
- package/src/resolution/binding-lookup-cache.ts +148 -0
- package/src/resolution/binding-scope.ts +6 -17
- package/src/resolution/binding-select.ts +15 -39
- package/src/resolution/class-introspector.ts +74 -0
- package/src/resolution/diagnostics.ts +43 -0
- package/src/resolution/environment.ts +181 -5
- package/src/resolution/instantiation-plan.ts +116 -64
- package/src/resolution/lifecycle.ts +69 -62
- package/src/resolution/resolution-path.ts +122 -43
- package/src/resolution/resolve-options.ts +51 -4
- package/src/resolution/resolver.ts +649 -1081
- package/src/resolution/scope.ts +58 -47
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import type { Binding,
|
|
2
|
-
import
|
|
1
|
+
import type { Binding, ConstantBinding, DynamicAsyncBinding, DynamicBinding } from "#/binding";
|
|
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,43 +11,49 @@ import {
|
|
|
12
11
|
NoMatchingBindingError,
|
|
13
12
|
TokenNotBoundError,
|
|
14
13
|
} from "#/errors";
|
|
15
|
-
import type {
|
|
14
|
+
import type { MetadataReader, ParamMetadata } from "#/metadata/metadata-types";
|
|
16
15
|
import type { BindingRegistry } from "#/registry";
|
|
17
|
-
import {
|
|
16
|
+
import { ActivationNeedCache } from "#/resolution/activation-need";
|
|
17
|
+
import type { DefaultLookupEntry } from "#/resolution/binding-lookup-cache";
|
|
18
|
+
import { BindingLookupCache } from "#/resolution/binding-lookup-cache";
|
|
19
|
+
import { matchesSlot, selectAllBindings, selectBinding } from "#/resolution/binding-select";
|
|
20
|
+
import { ClassIntrospector } from "#/resolution/class-introspector";
|
|
21
|
+
import type { ResolutionDiagnostics } from "#/resolution/diagnostics";
|
|
18
22
|
import type { ResolverCallbacks } from "#/resolution/environment";
|
|
19
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
AsyncCascadeContext,
|
|
25
|
+
AsyncLevelContext,
|
|
26
|
+
buildResolutionFrame,
|
|
27
|
+
DefaultResolutionContext,
|
|
28
|
+
} from "#/resolution/environment";
|
|
20
29
|
import { InstantiationPlanCompiler, PLAN_RETRY } from "#/resolution/instantiation-plan";
|
|
21
30
|
import type { LifecycleManager } from "#/resolution/lifecycle";
|
|
22
|
-
import {
|
|
23
|
-
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";
|
|
24
42
|
import type { ScopeManager } from "#/resolution/scope";
|
|
25
|
-
import { SINGLETON_MISS } from "#/resolution/scope";
|
|
26
43
|
import type { Token } from "#/token";
|
|
27
44
|
import { tokenName } from "#/token";
|
|
28
45
|
import type {
|
|
29
46
|
ActivationHandler,
|
|
30
47
|
BindingIdentifier,
|
|
31
|
-
BindingScope,
|
|
32
|
-
BindingTag,
|
|
33
48
|
ConstraintContext,
|
|
34
49
|
Constructor,
|
|
35
50
|
ResolutionFrame,
|
|
36
51
|
ResolveOptions,
|
|
37
52
|
} from "#/types";
|
|
38
53
|
|
|
39
|
-
// Terminal result of the options-less lookup fast lane — alias hops already folded.
|
|
40
|
-
interface DefaultLookupEntry {
|
|
41
|
-
readonly binding: Binding;
|
|
42
|
-
readonly owner: DependencyResolver;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// Fast-lane alias folding gives up past this many hops and defers to the resolve()
|
|
46
|
-
// loop, whose Set-based traversal detects genuine cycles exactly (no arbitrary cap).
|
|
47
|
-
const ALIAS_HOP_LIMIT = 32;
|
|
48
|
-
|
|
49
|
-
type BindingWithScope = Binding & { scope: BindingScope };
|
|
50
54
|
const EMPTY_STRING_LIST: ReadonlyArray<string> = [];
|
|
51
55
|
const EMPTY_FRAME_LIST: ReadonlyArray<ResolutionFrame> = [];
|
|
56
|
+
const EMPTY_PARAM_LIST: ReadonlyArray<ParamMetadata> = [];
|
|
52
57
|
const ROOT_CONSTRAINT_CONTEXT = {
|
|
53
58
|
resolutionPath: EMPTY_STRING_LIST,
|
|
54
59
|
resolutionStack: EMPTY_FRAME_LIST,
|
|
@@ -60,53 +65,25 @@ const ROOT_CONSTRAINT_CONTEXT = {
|
|
|
60
65
|
/**
|
|
61
66
|
* @since 0.3.16-canary.0
|
|
62
67
|
*/
|
|
63
|
-
export class DependencyResolver {
|
|
68
|
+
export class DependencyResolver implements ResolverCallbacks {
|
|
64
69
|
readonly #syncResolutionContextPool: Array<DefaultResolutionContext> = [];
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
//
|
|
82
|
-
//
|
|
83
|
-
// The method is NOT declared async: that would allocate an async state machine and an implicit
|
|
84
|
-
// promise per level, where a `.then(settle, settle)` side listener costs neither.
|
|
85
|
-
#asyncChainCtx: DefaultResolutionContext | undefined;
|
|
86
|
-
#asyncChainCtxPath: Array<string> | undefined;
|
|
87
|
-
#asyncChainActiveLevels = 0;
|
|
88
|
-
// Settle callback shared by every level of the owning async chain: all of them pop the same
|
|
89
|
-
// resolutionPath and decrement the same counter, so one closure serves the whole chain instead
|
|
90
|
-
// of allocating one per level (the async lane's dominant per-level allocation).
|
|
91
|
-
#asyncChainSettle: (() => void) | undefined;
|
|
92
|
-
readonly #classHasPostConstruct = new WeakMap<Constructor, boolean>();
|
|
93
|
-
readonly #classNeedsActiveContainer = new WeakMap<Constructor, boolean>();
|
|
94
|
-
readonly #classConstructorMetadata = new WeakMap<Constructor, ConstructorMetadata | null>();
|
|
95
|
-
readonly #activationNeedByBindingId = new Map<BindingIdentifier, boolean>();
|
|
96
|
-
#activationCacheVersion = -1;
|
|
97
|
-
// Options-less lookup memo across the parent chain: token → terminal {binding, owner}
|
|
98
|
-
// (alias hops folded). `null` = token must take the slow lookup path. Invalidated when
|
|
99
|
-
// any registry in the chain mutates (monotonic version sum).
|
|
100
|
-
readonly #defaultLookupByToken = new Map<Token<unknown> | Constructor, DefaultLookupEntry | null>();
|
|
101
|
-
#defaultLookupVersion = -1;
|
|
102
|
-
// Name-only lookup memo (token → name → entry) with the same chain-version
|
|
103
|
-
// invalidation. `null` = shape needs the full selection path.
|
|
104
|
-
readonly #namedLookupByToken = new Map<Token<unknown> | Constructor, Map<string, DefaultLookupEntry | null>>();
|
|
105
|
-
#namedLookupVersion = -1;
|
|
106
|
-
// Compiled transient-class plans (Dagger-style): a pure-static subgraph (class/constant/
|
|
107
|
-
// cached-singleton deps only) compiles once into a nested-constructor closure — cycle
|
|
108
|
-
// checking happens at compile time, so execution skips all per-resolve bookkeeping.
|
|
109
|
-
// `null` = binding is not plannable under the current versions.
|
|
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;
|
|
86
|
+
// Compiled plans; `null` marks a binding as unplannable under the current cache versions.
|
|
110
87
|
readonly #classPlanByBindingId = new Map<BindingIdentifier, (() => unknown) | null>();
|
|
111
88
|
#classPlanRegistryVersion = -1;
|
|
112
89
|
#classPlanActivationVersion = -1;
|
|
@@ -115,8 +92,10 @@ export class DependencyResolver {
|
|
|
115
92
|
readonly #scope: ScopeManager;
|
|
116
93
|
readonly #lifecycle: LifecycleManager;
|
|
117
94
|
readonly #metadataReader: MetadataReader;
|
|
118
|
-
readonly #container: Container;
|
|
119
95
|
readonly #parent: DependencyResolver | undefined;
|
|
96
|
+
readonly #lookup: BindingLookupCache<DependencyResolver>;
|
|
97
|
+
readonly #classes: ClassIntrospector;
|
|
98
|
+
readonly #activation: ActivationNeedCache;
|
|
120
99
|
|
|
121
100
|
constructor(
|
|
122
101
|
registry: BindingRegistry,
|
|
@@ -130,8 +109,28 @@ export class DependencyResolver {
|
|
|
130
109
|
this.#scope = scope;
|
|
131
110
|
this.#lifecycle = lifecycle;
|
|
132
111
|
this.#metadataReader = metadataReader;
|
|
133
|
-
this.#container = container;
|
|
134
112
|
this.#parent = parent;
|
|
113
|
+
this.#lookup = new BindingLookupCache<DependencyResolver>(
|
|
114
|
+
registry,
|
|
115
|
+
this,
|
|
116
|
+
parent === undefined ? undefined : parent.#lookup,
|
|
117
|
+
);
|
|
118
|
+
this.#classes = new ClassIntrospector(metadataReader, container);
|
|
119
|
+
this.#activation = new ActivationNeedCache(lifecycle, this.#classes, registry);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Structural counts for {@link RESOLUTION_DIAGNOSTICS}; see `resolution/diagnostics.ts`. */
|
|
123
|
+
describeCaches(): Pick<ResolutionDiagnostics, "compiledPlanCount" | "syncContextPoolSize"> {
|
|
124
|
+
let compiledPlanCount = 0;
|
|
125
|
+
for (const plan of this.#classPlanByBindingId.values()) {
|
|
126
|
+
if (plan !== null) {
|
|
127
|
+
compiledPlanCount += 1;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
compiledPlanCount,
|
|
132
|
+
syncContextPoolSize: this.#syncResolutionContextPool.length,
|
|
133
|
+
};
|
|
135
134
|
}
|
|
136
135
|
|
|
137
136
|
// ── Binding lookup ─────────────────────────────────────────────────────────
|
|
@@ -141,15 +140,13 @@ export class DependencyResolver {
|
|
|
141
140
|
options: ResolveOptions | undefined,
|
|
142
141
|
resolutionPath: Array<string>,
|
|
143
142
|
resolutionStack: Array<ResolutionFrame>,
|
|
144
|
-
):
|
|
143
|
+
): DefaultLookupEntry<DependencyResolver> | undefined {
|
|
145
144
|
if (options === undefined) {
|
|
146
145
|
const fastDefaultBinding = this.#registry.getFastDefault(token);
|
|
147
146
|
if (fastDefaultBinding !== undefined) {
|
|
148
147
|
return { binding: fastDefaultBinding, owner: this };
|
|
149
148
|
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
if (options?.name !== undefined && options.tag === undefined && (options.tags?.length ?? 0) === 0) {
|
|
149
|
+
} else if (isNameOnlyOptions(options)) {
|
|
153
150
|
const namedBinding = this.#registry.getSimpleNamed(token, options.name);
|
|
154
151
|
if (
|
|
155
152
|
namedBinding !== undefined &&
|
|
@@ -157,37 +154,33 @@ export class DependencyResolver {
|
|
|
157
154
|
) {
|
|
158
155
|
return { binding: namedBinding, owner: this };
|
|
159
156
|
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
) {
|
|
168
|
-
const [tagKey, tagValue] = options.tags![0]!;
|
|
169
|
-
const tagged = this.#registry.getSimpleTagged(token, tagKey, tagValue);
|
|
170
|
-
if (tagged !== undefined) {
|
|
171
|
-
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
|
+
}
|
|
172
164
|
}
|
|
173
165
|
}
|
|
174
166
|
|
|
175
167
|
const bindings = this.#registry.getAll(token);
|
|
176
168
|
if (bindings.length > 0) {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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 };
|
|
191
184
|
}
|
|
192
185
|
}
|
|
193
186
|
if (this.#parent !== undefined) {
|
|
@@ -196,13 +189,55 @@ export class DependencyResolver {
|
|
|
196
189
|
return undefined;
|
|
197
190
|
}
|
|
198
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
|
+
|
|
199
234
|
/**
|
|
200
235
|
* Binding lookup aligned with `resolve` — used by `Container.validate` without instantiating.
|
|
201
236
|
*/
|
|
202
237
|
peekBindingForValidate(
|
|
203
238
|
token: Token<unknown> | Constructor,
|
|
204
239
|
options: ResolveOptions | undefined,
|
|
205
|
-
):
|
|
240
|
+
): DefaultLookupEntry<DependencyResolver> | undefined {
|
|
206
241
|
return this.#findBinding(token, options, [], []);
|
|
207
242
|
}
|
|
208
243
|
|
|
@@ -212,16 +247,8 @@ export class DependencyResolver {
|
|
|
212
247
|
peekCandidateBindingsForValidate(
|
|
213
248
|
token: Token<unknown> | Constructor,
|
|
214
249
|
options: ResolveOptions | undefined,
|
|
215
|
-
):
|
|
216
|
-
|
|
217
|
-
return this.#getSimpleNamedBindingsFromChain(token, options.name);
|
|
218
|
-
}
|
|
219
|
-
const allBindings = this.#getAllBindingsFromChain(token);
|
|
220
|
-
if (allBindings.length === 0) {
|
|
221
|
-
return [];
|
|
222
|
-
}
|
|
223
|
-
const ctx = this.#makeConstraintContext([], [], options);
|
|
224
|
-
return selectAllBindings(allBindings, options, ctx);
|
|
250
|
+
): ReadonlyArray<Binding> {
|
|
251
|
+
return this.#candidateBindings(token, options, [], []);
|
|
225
252
|
}
|
|
226
253
|
|
|
227
254
|
// ── Sync resolve ───────────────────────────────────────────────────────────
|
|
@@ -235,85 +262,77 @@ export class DependencyResolver {
|
|
|
235
262
|
// (parent-chain walk + alias folding) only on miss or alias.
|
|
236
263
|
const fastBinding = this.#registry.getFastDefault(token);
|
|
237
264
|
if (fastBinding !== undefined && fastBinding.kind !== "alias") {
|
|
238
|
-
return this.#resolveDefaultEntry
|
|
265
|
+
return this.#resolveDefaultEntry(fastBinding, this, resolutionPath, resolutionStack) as Value;
|
|
239
266
|
}
|
|
240
|
-
const entry = this.#
|
|
267
|
+
const entry = this.#lookup.defaultEntry(token);
|
|
241
268
|
if (entry === null) {
|
|
242
269
|
return this.resolve(token, undefined, resolutionPath, resolutionStack);
|
|
243
270
|
}
|
|
244
|
-
return this.#resolveDefaultEntry
|
|
271
|
+
return this.#resolveDefaultEntry(entry.binding, entry.owner, resolutionPath, resolutionStack) as Value;
|
|
245
272
|
}
|
|
246
273
|
|
|
247
|
-
#resolveDefaultEntry
|
|
274
|
+
#resolveDefaultEntry(
|
|
248
275
|
binding: Binding,
|
|
249
276
|
owner: DependencyResolver,
|
|
250
277
|
resolutionPath: Array<string>,
|
|
251
278
|
resolutionStack: Array<ResolutionFrame>,
|
|
252
|
-
):
|
|
253
|
-
|
|
254
|
-
binding.kind === "constant" &&
|
|
255
|
-
binding.onActivation === undefined &&
|
|
256
|
-
(this.#lifecycle.activationVersion === 0 || !this.#lifecycle.hasActivationHandlers(binding.token))
|
|
257
|
-
) {
|
|
258
|
-
return binding.value as Value;
|
|
259
|
-
}
|
|
260
|
-
const scope = (binding as BindingWithScope).scope ?? "transient";
|
|
279
|
+
): unknown {
|
|
280
|
+
const scope = binding.scope;
|
|
261
281
|
if (scope === "transient") {
|
|
262
282
|
if (binding.kind === "dynamic") {
|
|
263
283
|
const containerHooks =
|
|
264
284
|
this.#lifecycle.activationVersion === 0 ? undefined : this.#lifecycle.activationHandlersFor(binding.token);
|
|
265
285
|
if (binding.onActivation === undefined && (containerHooks === undefined || containerHooks.length === 0)) {
|
|
266
|
-
return this.#resolveTransientDynamicSyncFromContext(
|
|
267
|
-
binding as Binding<Value> & { kind: "dynamic" },
|
|
268
|
-
resolutionPath,
|
|
269
|
-
resolutionStack,
|
|
270
|
-
);
|
|
286
|
+
return this.#resolveTransientDynamicSyncFromContext(binding, resolutionPath, resolutionStack);
|
|
271
287
|
}
|
|
272
|
-
return this.#resolveTransientDynamicActivatedSync(
|
|
273
|
-
binding as Binding<Value> & { kind: "dynamic" },
|
|
274
|
-
containerHooks,
|
|
275
|
-
resolutionPath,
|
|
276
|
-
resolutionStack,
|
|
277
|
-
);
|
|
288
|
+
return this.#resolveTransientDynamicActivatedSync(binding, containerHooks, resolutionPath, resolutionStack);
|
|
278
289
|
}
|
|
279
290
|
// Compiled plans only run at the top level — inner levels keep the runtime cycle guard.
|
|
280
291
|
if ((binding.kind === "class" || binding.kind === "resolved") && resolutionPath.length === 0) {
|
|
281
292
|
const plan = this.#getInstantiationPlan(binding);
|
|
282
293
|
if (plan !== null) {
|
|
283
|
-
return plan()
|
|
294
|
+
return plan();
|
|
284
295
|
}
|
|
285
296
|
}
|
|
286
297
|
} else if (scope === "singleton") {
|
|
287
|
-
|
|
288
|
-
if (
|
|
289
|
-
return
|
|
298
|
+
// A constant is a singleton that is already its own instance.
|
|
299
|
+
if (this.#isPlainConstant(binding)) {
|
|
300
|
+
return binding.value;
|
|
301
|
+
}
|
|
302
|
+
const cachedSingleton = binding.instance;
|
|
303
|
+
if (cachedSingleton !== NO_INSTANCE) {
|
|
304
|
+
return cachedSingleton;
|
|
290
305
|
}
|
|
291
306
|
if (owner !== this) {
|
|
292
|
-
return owner.#resolveBinding(binding
|
|
307
|
+
return owner.#resolveBinding(binding, undefined, resolutionPath, resolutionStack);
|
|
293
308
|
}
|
|
294
309
|
} else {
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
if (this.#scope.hasScoped(binding.id)) {
|
|
299
|
-
return this.#scope.getScoped<Value>(binding.id);
|
|
310
|
+
const cachedScoped = this.#readScoped(binding);
|
|
311
|
+
if (cachedScoped !== SCOPED_MISS) {
|
|
312
|
+
return cachedScoped;
|
|
300
313
|
}
|
|
301
314
|
}
|
|
302
|
-
return this.#resolveBinding(binding
|
|
315
|
+
return this.#resolveBinding(binding, undefined, resolutionPath, resolutionStack);
|
|
303
316
|
}
|
|
304
317
|
|
|
305
318
|
// Lean lane for an activated transient dynamic binding: same observable behavior as the
|
|
306
319
|
// generic #resolveBinding path (guard, frame, ctx, per-binding then container hooks) with
|
|
307
320
|
// the kind/activation dispatch resolved statically.
|
|
308
|
-
#resolveTransientDynamicActivatedSync
|
|
309
|
-
binding:
|
|
321
|
+
#resolveTransientDynamicActivatedSync(
|
|
322
|
+
binding: DynamicBinding<unknown>,
|
|
310
323
|
containerHooks: ReadonlyArray<ActivationHandler<unknown>> | undefined,
|
|
311
324
|
resolutionPath: Array<string>,
|
|
312
325
|
resolutionStack: Array<ResolutionFrame>,
|
|
313
|
-
):
|
|
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.
|
|
314
329
|
const frame = this.#getResolutionFrame(binding);
|
|
315
330
|
const tokenDisplayName = frame.tokenName;
|
|
316
|
-
|
|
331
|
+
if (binding.inFlight) {
|
|
332
|
+
throw new CircularDependencyError([...resolutionPath, tokenDisplayName]);
|
|
333
|
+
}
|
|
334
|
+
binding.inFlight = true;
|
|
335
|
+
resolutionPath.push(tokenDisplayName);
|
|
317
336
|
resolutionStack.push(frame);
|
|
318
337
|
try {
|
|
319
338
|
const resolutionCtx = this.#acquireSyncResolutionContext(resolutionPath, resolutionStack, undefined);
|
|
@@ -335,100 +354,19 @@ export class DependencyResolver {
|
|
|
335
354
|
if (activationResult instanceof Promise) {
|
|
336
355
|
throw new AsyncActivationError(tokenDisplayName, "onActivation");
|
|
337
356
|
}
|
|
338
|
-
activated = activationResult
|
|
357
|
+
activated = activationResult;
|
|
339
358
|
}
|
|
340
359
|
}
|
|
341
360
|
return activated;
|
|
342
361
|
} finally {
|
|
343
362
|
resolutionStack.pop();
|
|
344
363
|
resolutionPath.pop();
|
|
345
|
-
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
#chainRegistryVersion(): number {
|
|
350
|
-
let version = this.#registry.version;
|
|
351
|
-
for (let resolver = this.#parent; resolver !== undefined; resolver = resolver.#parent) {
|
|
352
|
-
version += resolver.#registry.version;
|
|
353
|
-
}
|
|
354
|
-
return version;
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
#lookupDefaultEntry(token: Token<unknown> | Constructor): DefaultLookupEntry | null {
|
|
358
|
-
const version = this.#chainRegistryVersion();
|
|
359
|
-
if (version !== this.#defaultLookupVersion) {
|
|
360
|
-
this.#defaultLookupByToken.clear();
|
|
361
|
-
this.#defaultLookupVersion = version;
|
|
362
|
-
}
|
|
363
|
-
let entry = this.#defaultLookupByToken.get(token);
|
|
364
|
-
if (entry === undefined) {
|
|
365
|
-
entry = this.#computeDefaultEntry(token);
|
|
366
|
-
this.#defaultLookupByToken.set(token, entry);
|
|
367
|
-
}
|
|
368
|
-
return entry;
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
#computeDefaultEntry(token: Token<unknown> | Constructor): DefaultLookupEntry | null {
|
|
372
|
-
let current = token;
|
|
373
|
-
for (let hop = 0; hop < ALIAS_HOP_LIMIT; hop += 1) {
|
|
374
|
-
const entry = this.#findDefaultEntryInChain(current);
|
|
375
|
-
if (entry === null) {
|
|
376
|
-
return null;
|
|
377
|
-
}
|
|
378
|
-
if (entry.binding.kind === "alias") {
|
|
379
|
-
current = entry.binding.target;
|
|
380
|
-
continue;
|
|
381
|
-
}
|
|
382
|
-
return entry;
|
|
383
|
-
}
|
|
384
|
-
return null;
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
#lookupNamedEntry(token: Token<unknown> | Constructor, name: string): DefaultLookupEntry | null {
|
|
388
|
-
const version = this.#chainRegistryVersion();
|
|
389
|
-
if (version !== this.#namedLookupVersion) {
|
|
390
|
-
this.#namedLookupByToken.clear();
|
|
391
|
-
this.#namedLookupVersion = version;
|
|
392
|
-
}
|
|
393
|
-
// ✓ TS6.0: Map.getOrInsert (ES2025)
|
|
394
|
-
const entriesByName = this.#namedLookupByToken.getOrInsert(token, new Map<string, DefaultLookupEntry | null>());
|
|
395
|
-
let entry = entriesByName.get(name);
|
|
396
|
-
if (entry === undefined) {
|
|
397
|
-
entry = this.#findNamedEntryInChain(token, name);
|
|
398
|
-
entriesByName.set(name, entry);
|
|
399
|
-
}
|
|
400
|
-
return entry;
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
#findNamedEntryInChain(token: Token<unknown> | Constructor, name: string): DefaultLookupEntry | null {
|
|
404
|
-
const named = this.#registry.getSimpleNamed(token, name);
|
|
405
|
-
if (named !== undefined) {
|
|
406
|
-
// Predicates need a live context; aliases carry options through the full path.
|
|
407
|
-
if (named.predicate !== undefined || named.kind === "alias") {
|
|
408
|
-
return null;
|
|
409
|
-
}
|
|
410
|
-
return { binding: named, owner: this };
|
|
411
|
-
}
|
|
412
|
-
if (this.#registry.has(token)) {
|
|
413
|
-
return null;
|
|
414
|
-
}
|
|
415
|
-
return this.#parent === undefined ? null : this.#parent.#findNamedEntryInChain(token, name);
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
#findDefaultEntryInChain(token: Token<unknown> | Constructor): DefaultLookupEntry | null {
|
|
419
|
-
const fast = this.#registry.getFastDefault(token);
|
|
420
|
-
if (fast !== undefined) {
|
|
421
|
-
return { binding: fast, owner: this };
|
|
422
|
-
}
|
|
423
|
-
// A level with non-fast bindings (multi-slot / predicate) needs full selection — bail.
|
|
424
|
-
if (this.#registry.has(token)) {
|
|
425
|
-
return null;
|
|
364
|
+
binding.inFlight = false;
|
|
426
365
|
}
|
|
427
|
-
return this.#parent === undefined ? null : this.#parent.#findDefaultEntryInChain(token);
|
|
428
366
|
}
|
|
429
367
|
|
|
430
368
|
#getInstantiationPlan(binding: Binding & { kind: "class" | "resolved" }): (() => unknown) | null {
|
|
431
|
-
const registryVersion = this.#
|
|
369
|
+
const registryVersion = this.#lookup.chainVersion();
|
|
432
370
|
const activationVersion = this.#lifecycle.activationVersion;
|
|
433
371
|
if (registryVersion !== this.#classPlanRegistryVersion || activationVersion !== this.#classPlanActivationVersion) {
|
|
434
372
|
this.#classPlanByBindingId.clear();
|
|
@@ -448,25 +386,31 @@ export class DependencyResolver {
|
|
|
448
386
|
return compiled;
|
|
449
387
|
}
|
|
450
388
|
|
|
451
|
-
// Compiler behind #
|
|
389
|
+
// Compiler behind #getInstantiationPlan — cold path, so the host indirection costs nothing hot.
|
|
452
390
|
readonly #planCompiler = new InstantiationPlanCompiler({
|
|
453
391
|
hasActivationHandlers: (token) => this.#lifecycle.hasActivationHandlers(token),
|
|
454
|
-
knownPostConstruct: (target) => this.#
|
|
455
|
-
needsActiveContainer: (target) =>
|
|
456
|
-
|
|
457
|
-
if (needsActiveContainer === undefined) {
|
|
458
|
-
const accessorMetadata = this.#metadataReader.getAccessorMetadata?.(target);
|
|
459
|
-
needsActiveContainer = (accessorMetadata?.length ?? 0) > 0;
|
|
460
|
-
this.#classNeedsActiveContainer.set(target, needsActiveContainer);
|
|
461
|
-
}
|
|
462
|
-
return needsActiveContainer;
|
|
463
|
-
},
|
|
464
|
-
getConstructorMetadata: (target) => this.#getConstructorMetadata(target),
|
|
392
|
+
knownPostConstruct: (target) => this.#classes.knownPostConstruct(target),
|
|
393
|
+
needsActiveContainer: (target) => this.#classes.needsActiveContainer(target),
|
|
394
|
+
getConstructorMetadata: (target) => this.#classes.constructorMetadata(target),
|
|
465
395
|
lookupDependencyEntry: (token) => {
|
|
466
|
-
const entry = this.#
|
|
467
|
-
return entry === null ? null : { binding: entry.binding
|
|
396
|
+
const entry = this.#lookup.defaultEntry(token);
|
|
397
|
+
return entry === null ? null : { binding: entry.binding };
|
|
398
|
+
},
|
|
399
|
+
getResolutionFrame: (binding) => this.#getResolutionFrame(binding),
|
|
400
|
+
// Dispatches exactly as #resolveDep does, so an escaped dep is indistinguishable
|
|
401
|
+
// from the same dep on a fully interpreted resolve.
|
|
402
|
+
resolveEscaped: (token, options, arity, resolutionPath, resolutionStack) => {
|
|
403
|
+
if (arity === "all") {
|
|
404
|
+
return this.resolveAll(token, options, resolutionPath, resolutionStack);
|
|
405
|
+
}
|
|
406
|
+
if (arity === "optional") {
|
|
407
|
+
return this.resolveOptional(token, options, resolutionPath, resolutionStack);
|
|
408
|
+
}
|
|
409
|
+
if (options === undefined) {
|
|
410
|
+
return this.resolveFromContext(token, resolutionPath, resolutionStack);
|
|
411
|
+
}
|
|
412
|
+
return this.resolve(token, options, resolutionPath, resolutionStack);
|
|
468
413
|
},
|
|
469
|
-
resolveFallback: (token) => this.resolve(token, undefined, [], []),
|
|
470
414
|
});
|
|
471
415
|
|
|
472
416
|
resolve<const Value>(
|
|
@@ -477,142 +421,74 @@ export class DependencyResolver {
|
|
|
477
421
|
): Value {
|
|
478
422
|
// Name-only fast lane: memoized lookup, dispatching just the shapes whose
|
|
479
423
|
// semantics involve no resolution context (constants, cached singletons).
|
|
480
|
-
if (
|
|
481
|
-
|
|
482
|
-
options.name !== undefined &&
|
|
483
|
-
options.tag === undefined &&
|
|
484
|
-
(options.tags === undefined || options.tags.length === 0)
|
|
485
|
-
) {
|
|
486
|
-
const namedEntry = this.#lookupNamedEntry(token, options.name);
|
|
424
|
+
if (options !== undefined && isNameOnlyOptions(options)) {
|
|
425
|
+
const namedEntry = this.#lookup.namedEntry(token, options.name);
|
|
487
426
|
if (namedEntry !== null) {
|
|
488
427
|
const namedBinding = namedEntry.binding;
|
|
489
|
-
if (
|
|
490
|
-
namedBinding.kind === "constant" &&
|
|
491
|
-
namedBinding.onActivation === undefined &&
|
|
492
|
-
(this.#lifecycle.activationVersion === 0 || !this.#lifecycle.hasActivationHandlers(namedBinding.token))
|
|
493
|
-
) {
|
|
428
|
+
if (this.#isPlainConstant(namedBinding)) {
|
|
494
429
|
return namedBinding.value as Value;
|
|
495
430
|
}
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
const cachedSingleton = namedEntry.owner.#scope.peekSingleton(namedBinding.id);
|
|
499
|
-
if (cachedSingleton !== SINGLETON_MISS) {
|
|
500
|
-
return cachedSingleton as Value;
|
|
501
|
-
}
|
|
431
|
+
if (namedBinding.scope === "singleton" && namedBinding.instance !== NO_INSTANCE) {
|
|
432
|
+
return namedBinding.instance as Value;
|
|
502
433
|
}
|
|
503
434
|
// Everything else keeps the full path (context, activation, guards).
|
|
504
435
|
}
|
|
505
436
|
}
|
|
506
437
|
|
|
507
|
-
|
|
508
|
-
let visitedAliasTokens: Set<Token<unknown> | Constructor> | undefined;
|
|
509
|
-
let found = this.#findBinding(currentToken, options, resolutionPath, resolutionStack);
|
|
438
|
+
const { binding, owner } = this.#requireBinding(token, options, resolutionPath, resolutionStack);
|
|
510
439
|
|
|
511
|
-
//
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
const target = found.binding.target;
|
|
515
|
-
visitedAliasTokens ??= new Set([currentToken]);
|
|
516
|
-
if (visitedAliasTokens.has(target)) {
|
|
517
|
-
throw new CircularDependencyError([...visitedAliasTokens, target].map((entry) => tokenName(entry)));
|
|
518
|
-
}
|
|
519
|
-
visitedAliasTokens.add(target);
|
|
520
|
-
currentToken = target;
|
|
521
|
-
found = this.#findBinding(currentToken, 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;
|
|
522
443
|
}
|
|
523
|
-
|
|
524
|
-
if (found === undefined) {
|
|
525
|
-
const ownBindings = this.#registry.getAll(currentToken);
|
|
526
|
-
if (ownBindings.length > 0) {
|
|
527
|
-
throw new NoMatchingBindingError(
|
|
528
|
-
this.#getTokenName(currentToken),
|
|
529
|
-
options ?? {},
|
|
530
|
-
this.#getAvailableSlots(currentToken),
|
|
531
|
-
);
|
|
532
|
-
}
|
|
533
|
-
throw new TokenNotBoundError(this.#getTokenName(currentToken));
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
const { binding, owner } = found;
|
|
537
|
-
|
|
538
|
-
const scope = (binding as BindingWithScope).scope ?? "transient";
|
|
539
|
-
|
|
540
|
-
// Singleton from a parent resolver: delegate so the parent caches it correctly
|
|
541
|
-
if (scope === "singleton" && owner !== this) {
|
|
542
|
-
return owner.#resolveBinding(binding as Binding<Value>, options, resolutionPath, resolutionStack);
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
// Scoped/transient (or own singleton): resolve with this resolver's container/scope
|
|
546
|
-
return this.#resolveBinding(binding as Binding<Value>, options, resolutionPath, resolutionStack);
|
|
444
|
+
return this.#resolveBinding(binding, options, resolutionPath, resolutionStack) as Value;
|
|
547
445
|
}
|
|
548
446
|
|
|
549
|
-
#resolveBinding
|
|
550
|
-
binding: Binding
|
|
447
|
+
#resolveBinding(
|
|
448
|
+
binding: Binding,
|
|
551
449
|
options: ResolveOptions | undefined,
|
|
552
450
|
resolutionPath: Array<string>,
|
|
553
451
|
resolutionStack: Array<ResolutionFrame>,
|
|
554
|
-
):
|
|
555
|
-
if (
|
|
556
|
-
binding.kind === "constant" &&
|
|
557
|
-
binding.onActivation === undefined &&
|
|
558
|
-
(this.#lifecycle.activationVersion === 0 || !this.#lifecycle.hasActivationHandlers(binding.token))
|
|
559
|
-
) {
|
|
452
|
+
): unknown {
|
|
453
|
+
if (this.#isPlainConstant(binding)) {
|
|
560
454
|
return binding.value;
|
|
561
455
|
}
|
|
562
456
|
|
|
563
|
-
const scope =
|
|
564
|
-
|
|
565
|
-
// Singleton cache check
|
|
457
|
+
const scope = binding.scope;
|
|
566
458
|
if (scope === "singleton") {
|
|
567
|
-
if (
|
|
568
|
-
return
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
// Scoped cache check
|
|
573
|
-
if (scope === "scoped") {
|
|
574
|
-
if (!this.#scope.isChild) {
|
|
575
|
-
throw new MissingScopeContextError(this.#getTokenName(binding.token));
|
|
459
|
+
if (binding.instance !== NO_INSTANCE) {
|
|
460
|
+
return binding.instance;
|
|
576
461
|
}
|
|
577
|
-
|
|
578
|
-
|
|
462
|
+
} else if (scope === "scoped") {
|
|
463
|
+
const cachedScoped = this.#readScoped(binding);
|
|
464
|
+
if (cachedScoped !== SCOPED_MISS) {
|
|
465
|
+
return cachedScoped;
|
|
579
466
|
}
|
|
580
467
|
}
|
|
581
468
|
|
|
582
469
|
const frame = this.#getResolutionFrame(binding);
|
|
583
470
|
const tokenDisplayName = frame.tokenName;
|
|
584
|
-
const resolutionSet = enterResolutionPath(resolutionPath, tokenDisplayName
|
|
471
|
+
const resolutionSet = enterResolutionPath(resolutionPath, tokenDisplayName);
|
|
585
472
|
resolutionStack.push(frame);
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
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);
|
|
590
477
|
const dynamicResult = binding.factory(resolutionCtx);
|
|
591
478
|
if (dynamicResult instanceof Promise) {
|
|
592
|
-
throw new AsyncResolutionError(
|
|
479
|
+
throw new AsyncResolutionError(tokenDisplayName, tokenDisplayName);
|
|
593
480
|
}
|
|
594
|
-
resolutionStack.pop();
|
|
595
|
-
resolutionPath.pop();
|
|
596
|
-
resolutionSet?.delete(tokenDisplayName);
|
|
597
481
|
return dynamicResult;
|
|
598
|
-
} catch (error) {
|
|
599
|
-
resolutionStack.pop();
|
|
600
|
-
resolutionPath.pop();
|
|
601
|
-
resolutionSet?.delete(tokenDisplayName);
|
|
602
|
-
throw error;
|
|
603
482
|
}
|
|
604
|
-
}
|
|
605
483
|
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
: undefined;
|
|
484
|
+
const resolutionCtx =
|
|
485
|
+
needsActivation || requiresResolutionContext(binding)
|
|
486
|
+
? this.#acquireSyncResolutionContext(resolutionPath, resolutionStack, options)
|
|
487
|
+
: undefined;
|
|
611
488
|
|
|
612
489
|
const instance = this.#instantiateSync(binding, resolutionCtx, resolutionPath, resolutionStack);
|
|
613
490
|
|
|
614
|
-
const
|
|
615
|
-
const activated = shouldActivate
|
|
491
|
+
const activated = this.#activation.refreshAfterFirstInstantiation(binding, needsActivation)
|
|
616
492
|
? this.#lifecycle.runActivationSync(
|
|
617
493
|
resolutionCtx as DefaultResolutionContext,
|
|
618
494
|
binding,
|
|
@@ -621,9 +497,8 @@ export class DependencyResolver {
|
|
|
621
497
|
)
|
|
622
498
|
: instance;
|
|
623
499
|
|
|
624
|
-
// Cache by scope
|
|
625
500
|
if (scope === "singleton") {
|
|
626
|
-
this.#scope.setSingleton(binding
|
|
501
|
+
this.#scope.setSingleton(binding, activated);
|
|
627
502
|
} else if (scope === "scoped") {
|
|
628
503
|
this.#scope.setScoped(binding.id, activated);
|
|
629
504
|
}
|
|
@@ -636,12 +511,12 @@ export class DependencyResolver {
|
|
|
636
511
|
}
|
|
637
512
|
}
|
|
638
513
|
|
|
639
|
-
#instantiateSync
|
|
640
|
-
binding: Binding
|
|
514
|
+
#instantiateSync(
|
|
515
|
+
binding: Binding,
|
|
641
516
|
ctx: DefaultResolutionContext | undefined,
|
|
642
517
|
resolutionPath: Array<string>,
|
|
643
518
|
resolutionStack: Array<ResolutionFrame>,
|
|
644
|
-
):
|
|
519
|
+
): unknown {
|
|
645
520
|
switch (binding.kind) {
|
|
646
521
|
case "constant":
|
|
647
522
|
return binding.value;
|
|
@@ -661,13 +536,12 @@ export class DependencyResolver {
|
|
|
661
536
|
throw new AsyncResolutionError(tokenName(binding.token), tokenName(binding.token));
|
|
662
537
|
|
|
663
538
|
case "class": {
|
|
664
|
-
const deps = this.#
|
|
665
|
-
|
|
666
|
-
return instance as Value;
|
|
539
|
+
const deps = this.#resolveDeps(this.#constructorParams(binding.target), resolutionPath, resolutionStack);
|
|
540
|
+
return this.#classes.instantiate(binding.target, deps);
|
|
667
541
|
}
|
|
668
542
|
|
|
669
543
|
case "resolved": {
|
|
670
|
-
const deps = this.#
|
|
544
|
+
const deps = this.#resolveDeps(binding.deps, resolutionPath, resolutionStack);
|
|
671
545
|
const factoryResult = binding.factory(...deps);
|
|
672
546
|
if (factoryResult instanceof Promise) {
|
|
673
547
|
throw new AsyncResolutionError(tokenName(binding.token), tokenName(binding.token));
|
|
@@ -683,90 +557,58 @@ export class DependencyResolver {
|
|
|
683
557
|
}
|
|
684
558
|
}
|
|
685
559
|
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
throw new MissingMetadataError(target.name);
|
|
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> {
|
|
567
|
+
const meta = this.#classes.constructorMetadata(target);
|
|
568
|
+
if (meta !== undefined) {
|
|
569
|
+
return meta.params;
|
|
697
570
|
}
|
|
698
|
-
if (
|
|
699
|
-
return
|
|
571
|
+
if (target.length === 0) {
|
|
572
|
+
return EMPTY_PARAM_LIST;
|
|
700
573
|
}
|
|
701
|
-
|
|
702
|
-
const param = meta.params[0]!;
|
|
703
|
-
const paramOptions = injectionSlotToResolveOptions(param);
|
|
704
|
-
if (param.multi) {
|
|
705
|
-
return [this.resolveAll(param.token, paramOptions, resolutionPath, resolutionStack)];
|
|
706
|
-
}
|
|
707
|
-
if (param.optional) {
|
|
708
|
-
return [this.resolveOptional(param.token, paramOptions, resolutionPath, resolutionStack)];
|
|
709
|
-
}
|
|
710
|
-
if (paramOptions === undefined) {
|
|
711
|
-
return [this.resolveFromContext(param.token, resolutionPath, resolutionStack)];
|
|
712
|
-
}
|
|
713
|
-
return [this.resolve(param.token, paramOptions, resolutionPath, resolutionStack)];
|
|
714
|
-
}
|
|
715
|
-
const deps = new Array<unknown>(meta.params.length);
|
|
716
|
-
for (let index = 0; index < meta.params.length; index += 1) {
|
|
717
|
-
const param = meta.params[index]!;
|
|
718
|
-
const paramOptions = injectionSlotToResolveOptions(param);
|
|
719
|
-
if (param.multi) {
|
|
720
|
-
deps[index] = this.resolveAll(param.token, paramOptions, resolutionPath, resolutionStack);
|
|
721
|
-
continue;
|
|
722
|
-
}
|
|
723
|
-
if (param.optional) {
|
|
724
|
-
deps[index] = this.resolveOptional(param.token, paramOptions, resolutionPath, resolutionStack);
|
|
725
|
-
continue;
|
|
726
|
-
}
|
|
727
|
-
deps[index] =
|
|
728
|
-
paramOptions === undefined
|
|
729
|
-
? this.resolveFromContext(param.token, resolutionPath, resolutionStack)
|
|
730
|
-
: this.resolve(param.token, paramOptions, resolutionPath, resolutionStack);
|
|
731
|
-
}
|
|
732
|
-
return deps;
|
|
574
|
+
throw new MissingMetadataError(target.name);
|
|
733
575
|
}
|
|
734
576
|
|
|
735
|
-
|
|
736
|
-
|
|
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>,
|
|
737
581
|
resolutionPath: Array<string>,
|
|
738
582
|
resolutionStack: Array<ResolutionFrame>,
|
|
739
583
|
): Array<unknown> {
|
|
740
|
-
const
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
);
|
|
751
|
-
continue;
|
|
752
|
-
}
|
|
753
|
-
if (dep.optional) {
|
|
754
|
-
resolved[index] = this.resolveOptional(
|
|
755
|
-
dep.token as Token<unknown> | Constructor,
|
|
756
|
-
depOptions,
|
|
757
|
-
resolutionPath,
|
|
758
|
-
resolutionStack,
|
|
759
|
-
);
|
|
760
|
-
continue;
|
|
761
|
-
}
|
|
762
|
-
resolved[index] =
|
|
763
|
-
depOptions === undefined
|
|
764
|
-
? this.resolveFromContext(dep.token as Token<unknown> | Constructor, resolutionPath, resolutionStack)
|
|
765
|
-
: 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);
|
|
766
594
|
}
|
|
767
595
|
return resolved;
|
|
768
596
|
}
|
|
769
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
|
+
|
|
770
612
|
resolveOptional<const Value>(
|
|
771
613
|
token: Token<Value> | Constructor<Value>,
|
|
772
614
|
options: ResolveOptions | undefined,
|
|
@@ -785,49 +627,53 @@ export class DependencyResolver {
|
|
|
785
627
|
resolutionPath: Array<string>,
|
|
786
628
|
resolutionStack: Array<ResolutionFrame>,
|
|
787
629
|
): Array<Value> {
|
|
788
|
-
|
|
789
|
-
const namedCandidates = this.#getSimpleNamedBindingsFromChain(token, options.name);
|
|
790
|
-
if (namedCandidates.length === 0) {
|
|
791
|
-
return [];
|
|
792
|
-
}
|
|
793
|
-
const resolved = new Array<Value>(namedCandidates.length);
|
|
794
|
-
for (let index = 0; index < namedCandidates.length; index += 1) {
|
|
795
|
-
resolved[index] = this.#resolveCandidateSync(
|
|
796
|
-
namedCandidates[index] as Binding<Value>,
|
|
797
|
-
options,
|
|
798
|
-
resolutionPath,
|
|
799
|
-
resolutionStack,
|
|
800
|
-
);
|
|
801
|
-
}
|
|
802
|
-
return resolved;
|
|
803
|
-
}
|
|
804
|
-
|
|
805
|
-
const allBindings = this.#getAllBindingsFromChain(token);
|
|
806
|
-
if (allBindings.length === 0) {
|
|
807
|
-
return [];
|
|
808
|
-
}
|
|
809
|
-
|
|
810
|
-
const ctx = this.#makeConstraintContext(resolutionPath, resolutionStack, options);
|
|
811
|
-
const candidates = selectAllBindings(allBindings, options, ctx);
|
|
812
|
-
|
|
630
|
+
const candidates = this.#candidateBindings(token, options, resolutionPath, resolutionStack);
|
|
813
631
|
const resolved = new Array<Value>(candidates.length);
|
|
814
632
|
for (let index = 0; index < candidates.length; index += 1) {
|
|
815
633
|
resolved[index] = this.#resolveCandidateSync(
|
|
816
|
-
candidates[index]
|
|
634
|
+
candidates[index]!,
|
|
817
635
|
options,
|
|
818
636
|
resolutionPath,
|
|
819
637
|
resolutionStack,
|
|
820
|
-
);
|
|
638
|
+
) as Value;
|
|
821
639
|
}
|
|
822
640
|
return resolved;
|
|
823
641
|
}
|
|
824
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
|
+
|
|
825
670
|
// ── Async resolve ──────────────────────────────────────────────────────────
|
|
826
671
|
|
|
827
672
|
resolveAsyncFromContext<const Value>(
|
|
828
673
|
token: Token<Value> | Constructor<Value>,
|
|
829
674
|
resolutionPath: Array<string>,
|
|
830
675
|
resolutionStack: Array<ResolutionFrame>,
|
|
676
|
+
branchDepth: BranchDepth,
|
|
831
677
|
): Promise<Value> {
|
|
832
678
|
// Hot lane: own-registry fast default (async chains resolve sibling dynamic bindings).
|
|
833
679
|
// Fall back to the chain-versioned memo only on miss or alias.
|
|
@@ -837,67 +683,67 @@ export class DependencyResolver {
|
|
|
837
683
|
if (
|
|
838
684
|
(fastBinding.kind === "dynamic-async" || fastBinding.kind === "dynamic") &&
|
|
839
685
|
fastBinding.scope === "transient" &&
|
|
840
|
-
fastBinding
|
|
841
|
-
(this.#lifecycle.activationVersion === 0 || !this.#lifecycle.hasActivationHandlers(fastBinding.token))
|
|
686
|
+
!this.#hasAnyActivation(fastBinding)
|
|
842
687
|
) {
|
|
843
688
|
return this.#resolveTransientDynamicAsyncFromContext(
|
|
844
|
-
fastBinding
|
|
689
|
+
fastBinding,
|
|
845
690
|
resolutionPath,
|
|
846
691
|
resolutionStack,
|
|
847
|
-
|
|
692
|
+
branchDepth,
|
|
693
|
+
) as Promise<Value>;
|
|
848
694
|
}
|
|
849
|
-
return this.#resolveAsyncDefaultEntry
|
|
695
|
+
return this.#resolveAsyncDefaultEntry(
|
|
696
|
+
fastBinding,
|
|
697
|
+
this,
|
|
698
|
+
resolutionPath,
|
|
699
|
+
resolutionStack,
|
|
700
|
+
branchDepth,
|
|
701
|
+
) as Promise<Value>;
|
|
850
702
|
}
|
|
851
|
-
const entry = this.#
|
|
703
|
+
const entry = this.#lookup.defaultEntry(token);
|
|
852
704
|
if (entry === null) {
|
|
853
|
-
return this.resolveAsync(token, undefined, resolutionPath, resolutionStack);
|
|
705
|
+
return this.resolveAsync(token, undefined, resolutionPath, resolutionStack, branchDepth);
|
|
854
706
|
}
|
|
855
|
-
return this.#resolveAsyncDefaultEntry
|
|
707
|
+
return this.#resolveAsyncDefaultEntry(
|
|
708
|
+
entry.binding,
|
|
709
|
+
entry.owner,
|
|
710
|
+
resolutionPath,
|
|
711
|
+
resolutionStack,
|
|
712
|
+
branchDepth,
|
|
713
|
+
) as Promise<Value>;
|
|
856
714
|
}
|
|
857
715
|
|
|
858
|
-
#resolveAsyncDefaultEntry
|
|
716
|
+
#resolveAsyncDefaultEntry(
|
|
859
717
|
binding: Binding,
|
|
860
718
|
owner: DependencyResolver,
|
|
861
719
|
resolutionPath: Array<string>,
|
|
862
720
|
resolutionStack: Array<ResolutionFrame>,
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
binding.
|
|
867
|
-
(this.#lifecycle.activationVersion === 0 || !this.#lifecycle.hasActivationHandlers(binding.token))
|
|
868
|
-
) {
|
|
869
|
-
return Promise.resolve(binding.value as Value);
|
|
721
|
+
branchDepth: BranchDepth,
|
|
722
|
+
): Promise<unknown> {
|
|
723
|
+
if (this.#isPlainConstant(binding)) {
|
|
724
|
+
return Promise.resolve(binding.value);
|
|
870
725
|
}
|
|
871
|
-
const scope =
|
|
726
|
+
const scope = binding.scope;
|
|
872
727
|
if (scope === "transient") {
|
|
873
|
-
if (
|
|
874
|
-
(binding
|
|
875
|
-
binding.onActivation === undefined &&
|
|
876
|
-
(this.#lifecycle.activationVersion === 0 || !this.#lifecycle.hasActivationHandlers(binding.token))
|
|
877
|
-
) {
|
|
878
|
-
return this.#resolveTransientDynamicAsyncFromContext(
|
|
879
|
-
binding as Binding<Value> & { kind: "dynamic" | "dynamic-async" },
|
|
880
|
-
resolutionPath,
|
|
881
|
-
resolutionStack,
|
|
882
|
-
);
|
|
728
|
+
if ((binding.kind === "dynamic" || binding.kind === "dynamic-async") && !this.#hasAnyActivation(binding)) {
|
|
729
|
+
return this.#resolveTransientDynamicAsyncFromContext(binding, resolutionPath, resolutionStack, branchDepth);
|
|
883
730
|
}
|
|
884
731
|
} else if (scope === "singleton") {
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
return Promise.resolve(cachedSingleton as Value);
|
|
732
|
+
if (binding.instance !== NO_INSTANCE) {
|
|
733
|
+
return Promise.resolve(binding.instance);
|
|
888
734
|
}
|
|
889
735
|
if (owner !== this) {
|
|
890
|
-
return owner.#resolveBindingAsync(binding
|
|
891
|
-
}
|
|
892
|
-
} else {
|
|
893
|
-
if (!this.#scope.isChild) {
|
|
894
|
-
return Promise.reject(new MissingScopeContextError(this.#getTokenName(binding.token)));
|
|
736
|
+
return owner.#resolveBindingAsync(binding, undefined, resolutionPath, resolutionStack, branchDepth);
|
|
895
737
|
}
|
|
738
|
+
} else if (this.#scope.isChild) {
|
|
896
739
|
if (this.#scope.hasScoped(binding.id)) {
|
|
897
|
-
return Promise.resolve(this.#scope.getScoped
|
|
740
|
+
return Promise.resolve(this.#scope.getScoped(binding.id));
|
|
898
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)));
|
|
899
745
|
}
|
|
900
|
-
return this.#resolveBindingAsync(binding
|
|
746
|
+
return this.#resolveBindingAsync(binding, undefined, resolutionPath, resolutionStack, branchDepth);
|
|
901
747
|
}
|
|
902
748
|
|
|
903
749
|
async resolveAsync<const Value>(
|
|
@@ -905,178 +751,125 @@ export class DependencyResolver {
|
|
|
905
751
|
options: ResolveOptions | undefined,
|
|
906
752
|
resolutionPath: Array<string>,
|
|
907
753
|
resolutionStack: Array<ResolutionFrame>,
|
|
754
|
+
branchDepth: BranchDepth = UNOWNED_BRANCH,
|
|
908
755
|
): Promise<Value> {
|
|
909
|
-
const
|
|
910
|
-
|
|
911
|
-
if (found === undefined) {
|
|
912
|
-
const ownBindings = this.#registry.getAll(token);
|
|
913
|
-
if (ownBindings.length > 0) {
|
|
914
|
-
throw new NoMatchingBindingError(this.#getTokenName(token), options ?? {}, this.#getAvailableSlots(token));
|
|
915
|
-
}
|
|
916
|
-
throw new TokenNotBoundError(this.#getTokenName(token));
|
|
917
|
-
}
|
|
918
|
-
|
|
919
|
-
let currentToken: Token<unknown> | Constructor = token;
|
|
920
|
-
let visitedAliasTokens: Set<Token<unknown> | Constructor> | undefined;
|
|
921
|
-
let aliasFollowed: DefaultLookupEntry | undefined = found;
|
|
922
|
-
while (aliasFollowed !== undefined && aliasFollowed.binding.kind === "alias") {
|
|
923
|
-
const target = aliasFollowed.binding.target;
|
|
924
|
-
visitedAliasTokens ??= new Set([currentToken]);
|
|
925
|
-
if (visitedAliasTokens.has(target)) {
|
|
926
|
-
throw new CircularDependencyError([...visitedAliasTokens, target].map((entry) => tokenName(entry)));
|
|
927
|
-
}
|
|
928
|
-
visitedAliasTokens.add(target);
|
|
929
|
-
currentToken = target;
|
|
930
|
-
aliasFollowed = this.#findBinding(currentToken, options, resolutionPath, resolutionStack);
|
|
931
|
-
}
|
|
932
|
-
if (aliasFollowed === undefined) {
|
|
933
|
-
const ownBindings = this.#registry.getAll(currentToken);
|
|
934
|
-
if (ownBindings.length > 0) {
|
|
935
|
-
throw new NoMatchingBindingError(
|
|
936
|
-
this.#getTokenName(currentToken),
|
|
937
|
-
options ?? {},
|
|
938
|
-
this.#getAvailableSlots(currentToken),
|
|
939
|
-
);
|
|
940
|
-
}
|
|
941
|
-
throw new TokenNotBoundError(this.#getTokenName(currentToken));
|
|
942
|
-
}
|
|
943
|
-
const { binding, owner } = aliasFollowed;
|
|
756
|
+
const { binding, owner } = this.#requireBinding(token, options, resolutionPath, resolutionStack);
|
|
944
757
|
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
if (scope === "singleton" && owner !== this) {
|
|
948
|
-
return owner.#resolveBindingAsync(binding as Binding<Value>, options, resolutionPath, resolutionStack);
|
|
758
|
+
if (binding.scope === "singleton" && owner !== this) {
|
|
759
|
+
return owner.#resolveBindingAsync(binding, options, resolutionPath, resolutionStack, branchDepth) as Value;
|
|
949
760
|
}
|
|
950
|
-
|
|
951
|
-
return this.#resolveBindingAsync(binding as Binding<Value>, options, resolutionPath, resolutionStack);
|
|
761
|
+
return this.#resolveBindingAsync(binding, options, resolutionPath, resolutionStack, branchDepth) as Value;
|
|
952
762
|
}
|
|
953
763
|
|
|
954
|
-
async #resolveBindingAsync
|
|
955
|
-
binding: Binding
|
|
764
|
+
async #resolveBindingAsync(
|
|
765
|
+
binding: Binding,
|
|
956
766
|
options: ResolveOptions | undefined,
|
|
957
767
|
resolutionPath: Array<string>,
|
|
958
768
|
resolutionStack: Array<ResolutionFrame>,
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
binding.onActivation === undefined &&
|
|
963
|
-
!this.#lifecycle.hasActivationHandlers(binding.token)
|
|
964
|
-
) {
|
|
769
|
+
branchDepth: BranchDepth,
|
|
770
|
+
): Promise<unknown> {
|
|
771
|
+
if (this.#isPlainConstant(binding)) {
|
|
965
772
|
return binding.value;
|
|
966
773
|
}
|
|
967
774
|
|
|
968
|
-
const scope =
|
|
969
|
-
|
|
970
|
-
// Singleton cache
|
|
775
|
+
const scope = binding.scope;
|
|
971
776
|
if (scope === "singleton") {
|
|
972
|
-
if (
|
|
973
|
-
return
|
|
777
|
+
if (binding.instance !== NO_INSTANCE) {
|
|
778
|
+
return binding.instance;
|
|
974
779
|
}
|
|
975
|
-
// In-flight dedup
|
|
780
|
+
// In-flight dedup: concurrent callers share the first creation.
|
|
976
781
|
const inflight = this.#scope.getInflight(binding.id);
|
|
977
782
|
if (inflight !== undefined) {
|
|
978
|
-
return inflight
|
|
979
|
-
}
|
|
980
|
-
}
|
|
981
|
-
|
|
982
|
-
// Scoped cache
|
|
983
|
-
if (scope === "scoped") {
|
|
984
|
-
if (!this.#scope.isChild) {
|
|
985
|
-
throw new MissingScopeContextError(this.#getTokenName(binding.token));
|
|
783
|
+
return inflight;
|
|
986
784
|
}
|
|
987
|
-
|
|
988
|
-
|
|
785
|
+
} else if (scope === "scoped") {
|
|
786
|
+
const cachedScoped = this.#readScoped(binding);
|
|
787
|
+
if (cachedScoped !== SCOPED_MISS) {
|
|
788
|
+
return cachedScoped;
|
|
989
789
|
}
|
|
990
790
|
}
|
|
991
791
|
|
|
992
792
|
const frame = this.#getResolutionFrame(binding);
|
|
993
|
-
|
|
994
|
-
const
|
|
995
|
-
resolutionStack
|
|
996
|
-
const
|
|
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
|
+
|
|
798
|
+
const needsActivation = this.#activation.needsActivation(binding);
|
|
997
799
|
if (!needsActivation && scope === "transient" && (binding.kind === "dynamic" || binding.kind === "dynamic-async")) {
|
|
998
|
-
const resolutionCtx = new
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
resolutionStack,
|
|
1002
|
-
options,
|
|
1003
|
-
);
|
|
1004
|
-
try {
|
|
1005
|
-
if (binding.kind === "dynamic-async") {
|
|
1006
|
-
return await binding.factory(resolutionCtx);
|
|
1007
|
-
}
|
|
1008
|
-
const dynamicResult = binding.factory(resolutionCtx);
|
|
1009
|
-
return dynamicResult instanceof Promise ? await dynamicResult : dynamicResult;
|
|
1010
|
-
} finally {
|
|
1011
|
-
resolutionStack.pop();
|
|
1012
|
-
resolutionPath.pop();
|
|
1013
|
-
resolutionSet?.delete(frameName);
|
|
800
|
+
const resolutionCtx = new AsyncLevelContext(this, levelPath, levelStack, options);
|
|
801
|
+
if (binding.kind === "dynamic-async") {
|
|
802
|
+
return await binding.factory(resolutionCtx);
|
|
1014
803
|
}
|
|
804
|
+
const dynamicResult = binding.factory(resolutionCtx);
|
|
805
|
+
return dynamicResult instanceof Promise ? await dynamicResult : dynamicResult;
|
|
1015
806
|
}
|
|
1016
807
|
|
|
1017
|
-
const
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
808
|
+
const resolutionCtx =
|
|
809
|
+
needsActivation || requiresResolutionContext(binding)
|
|
810
|
+
? new AsyncLevelContext(this, levelPath, levelStack, options)
|
|
811
|
+
: undefined;
|
|
1021
812
|
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
)
|
|
1035
|
-
: instance;
|
|
1036
|
-
|
|
1037
|
-
this.#scope.setSingleton(binding.id, activated);
|
|
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) => {
|
|
824
|
+
this.#scope.setSingleton(binding, activated);
|
|
1038
825
|
this.#scope.clearInflight(binding.id);
|
|
1039
826
|
return activated;
|
|
1040
|
-
}
|
|
1041
|
-
|
|
1042
|
-
const singletonPromise = createSingletonPromise().catch((err: unknown) => {
|
|
827
|
+
},
|
|
828
|
+
(error: unknown) => {
|
|
1043
829
|
this.#scope.clearInflight(binding.id);
|
|
1044
|
-
throw
|
|
1045
|
-
}
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
const instance = await this.#instantiateAsync(binding, resolutionCtx, resolutionPath, resolutionStack);
|
|
1051
|
-
|
|
1052
|
-
const shouldActivate = this.#refreshActivationCacheIfNeeded(binding, needsActivation);
|
|
1053
|
-
const activated = shouldActivate
|
|
1054
|
-
? await this.#lifecycle.runActivation(
|
|
1055
|
-
resolutionCtx as DefaultResolutionContext,
|
|
1056
|
-
binding,
|
|
1057
|
-
instance,
|
|
1058
|
-
this.#metadataReader,
|
|
1059
|
-
)
|
|
1060
|
-
: instance;
|
|
1061
|
-
|
|
1062
|
-
if (scope === "scoped") {
|
|
1063
|
-
this.#scope.setScoped(binding.id, activated);
|
|
1064
|
-
}
|
|
830
|
+
throw error;
|
|
831
|
+
},
|
|
832
|
+
);
|
|
833
|
+
this.#scope.setInflight(binding.id, singletonPromise as Promise<unknown>);
|
|
834
|
+
return await singletonPromise;
|
|
835
|
+
}
|
|
1065
836
|
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
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);
|
|
1071
847
|
}
|
|
848
|
+
return activated;
|
|
1072
849
|
}
|
|
1073
850
|
|
|
1074
|
-
async #
|
|
1075
|
-
binding: Binding
|
|
1076
|
-
ctx:
|
|
851
|
+
async #instantiateAndActivateAsync(
|
|
852
|
+
binding: Binding,
|
|
853
|
+
ctx: AsyncLevelContext | undefined,
|
|
1077
854
|
resolutionPath: Array<string>,
|
|
1078
855
|
resolutionStack: Array<ResolutionFrame>,
|
|
1079
|
-
|
|
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> {
|
|
1080
873
|
switch (binding.kind) {
|
|
1081
874
|
case "constant":
|
|
1082
875
|
return binding.value;
|
|
@@ -1096,22 +889,26 @@ export class DependencyResolver {
|
|
|
1096
889
|
return binding.factory(ctx);
|
|
1097
890
|
|
|
1098
891
|
case "class": {
|
|
1099
|
-
const deps = await this.#
|
|
1100
|
-
|
|
1101
|
-
|
|
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);
|
|
1102
899
|
}
|
|
1103
900
|
|
|
1104
901
|
case "resolved": {
|
|
1105
902
|
if (ctx === undefined) {
|
|
1106
903
|
throw new InternalError("resolved binding requires resolution context");
|
|
1107
904
|
}
|
|
1108
|
-
const deps = await this.#
|
|
905
|
+
const deps = await this.#resolveDepsAsync(binding.deps, resolutionPath, resolutionStack, branchDepth);
|
|
1109
906
|
const factoryResult = binding.factory(...deps);
|
|
1110
907
|
return factoryResult instanceof Promise ? factoryResult : Promise.resolve(factoryResult);
|
|
1111
908
|
}
|
|
1112
909
|
|
|
1113
910
|
case "resolved-async": {
|
|
1114
|
-
const deps = await this.#
|
|
911
|
+
const deps = await this.#resolveDepsAsync(binding.deps, resolutionPath, resolutionStack, branchDepth);
|
|
1115
912
|
return binding.factory(...deps);
|
|
1116
913
|
}
|
|
1117
914
|
|
|
@@ -1120,114 +917,45 @@ export class DependencyResolver {
|
|
|
1120
917
|
}
|
|
1121
918
|
}
|
|
1122
919
|
|
|
1123
|
-
async #
|
|
1124
|
-
|
|
920
|
+
async #resolveDepsAsync(
|
|
921
|
+
deps: ReadonlyArray<DependencySlot>,
|
|
1125
922
|
resolutionPath: Array<string>,
|
|
1126
923
|
resolutionStack: Array<ResolutionFrame>,
|
|
924
|
+
branchDepth: BranchDepth,
|
|
1127
925
|
): Promise<Array<unknown>> {
|
|
1128
|
-
const
|
|
1129
|
-
if (
|
|
1130
|
-
if (target.length === 0) {
|
|
1131
|
-
return [];
|
|
1132
|
-
}
|
|
1133
|
-
throw new MissingMetadataError(target.name);
|
|
1134
|
-
}
|
|
1135
|
-
if (meta.params.length === 0) {
|
|
926
|
+
const count = deps.length;
|
|
927
|
+
if (count === 0) {
|
|
1136
928
|
return [];
|
|
1137
929
|
}
|
|
1138
|
-
if (
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
}
|
|
1147
|
-
if (paramOptions === undefined) {
|
|
1148
|
-
return [await this.resolveAsyncFromContext(param.token, resolutionPath, resolutionStack)];
|
|
1149
|
-
}
|
|
1150
|
-
return [await this.resolveAsync(param.token, paramOptions, resolutionPath, resolutionStack)];
|
|
1151
|
-
}
|
|
1152
|
-
const pending = new Array<Promise<unknown>>(meta.params.length);
|
|
1153
|
-
const shouldCloneContext = meta.params.length > 1;
|
|
1154
|
-
for (let index = 0; index < meta.params.length; index += 1) {
|
|
1155
|
-
const param = meta.params[index]!;
|
|
1156
|
-
const paramOptions = injectionSlotToResolveOptions(param);
|
|
1157
|
-
if (param.multi) {
|
|
1158
|
-
pending[index] = this.resolveAllAsync(
|
|
1159
|
-
param.token,
|
|
1160
|
-
paramOptions,
|
|
1161
|
-
shouldCloneContext ? [...resolutionPath] : resolutionPath,
|
|
1162
|
-
shouldCloneContext ? [...resolutionStack] : resolutionStack,
|
|
1163
|
-
);
|
|
1164
|
-
} else if (param.optional) {
|
|
1165
|
-
pending[index] = this.resolveOptionalAsync(
|
|
1166
|
-
param.token,
|
|
1167
|
-
paramOptions,
|
|
1168
|
-
shouldCloneContext ? [...resolutionPath] : resolutionPath,
|
|
1169
|
-
shouldCloneContext ? [...resolutionStack] : resolutionStack,
|
|
1170
|
-
);
|
|
1171
|
-
} else {
|
|
1172
|
-
pending[index] =
|
|
1173
|
-
paramOptions === undefined
|
|
1174
|
-
? this.resolveAsyncFromContext(
|
|
1175
|
-
param.token,
|
|
1176
|
-
shouldCloneContext ? [...resolutionPath] : resolutionPath,
|
|
1177
|
-
shouldCloneContext ? [...resolutionStack] : resolutionStack,
|
|
1178
|
-
)
|
|
1179
|
-
: this.resolveAsync(
|
|
1180
|
-
param.token,
|
|
1181
|
-
paramOptions,
|
|
1182
|
-
shouldCloneContext ? [...resolutionPath] : resolutionPath,
|
|
1183
|
-
shouldCloneContext ? [...resolutionStack] : resolutionStack,
|
|
1184
|
-
);
|
|
1185
|
-
}
|
|
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);
|
|
1186
938
|
}
|
|
1187
939
|
return Promise.all(pending);
|
|
1188
940
|
}
|
|
1189
941
|
|
|
1190
|
-
|
|
1191
|
-
|
|
942
|
+
#resolveDepAsync(
|
|
943
|
+
dep: DependencySlot,
|
|
1192
944
|
resolutionPath: Array<string>,
|
|
1193
945
|
resolutionStack: Array<ResolutionFrame>,
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
const
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
const depOptions = injectionSlotToResolveOptions(dep);
|
|
1200
|
-
if (dep.multi) {
|
|
1201
|
-
pending[index] = this.resolveAllAsync(
|
|
1202
|
-
dep.token as Token<unknown> | Constructor,
|
|
1203
|
-
depOptions,
|
|
1204
|
-
shouldCloneContext ? [...resolutionPath] : resolutionPath,
|
|
1205
|
-
shouldCloneContext ? [...resolutionStack] : resolutionStack,
|
|
1206
|
-
);
|
|
1207
|
-
} else if (dep.optional) {
|
|
1208
|
-
pending[index] = this.resolveOptionalAsync(
|
|
1209
|
-
dep.token as Token<unknown> | Constructor,
|
|
1210
|
-
depOptions,
|
|
1211
|
-
shouldCloneContext ? [...resolutionPath] : resolutionPath,
|
|
1212
|
-
shouldCloneContext ? [...resolutionStack] : resolutionStack,
|
|
1213
|
-
);
|
|
1214
|
-
} else {
|
|
1215
|
-
pending[index] =
|
|
1216
|
-
depOptions === undefined
|
|
1217
|
-
? this.resolveAsyncFromContext(
|
|
1218
|
-
dep.token as Token<unknown> | Constructor,
|
|
1219
|
-
shouldCloneContext ? [...resolutionPath] : resolutionPath,
|
|
1220
|
-
shouldCloneContext ? [...resolutionStack] : resolutionStack,
|
|
1221
|
-
)
|
|
1222
|
-
: this.resolveAsync(
|
|
1223
|
-
dep.token as Token<unknown> | Constructor,
|
|
1224
|
-
depOptions,
|
|
1225
|
-
shouldCloneContext ? [...resolutionPath] : resolutionPath,
|
|
1226
|
-
shouldCloneContext ? [...resolutionStack] : resolutionStack,
|
|
1227
|
-
);
|
|
1228
|
-
}
|
|
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);
|
|
1229
951
|
}
|
|
1230
|
-
|
|
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);
|
|
1231
959
|
}
|
|
1232
960
|
|
|
1233
961
|
async resolveOptionalAsync<const Value>(
|
|
@@ -1235,11 +963,12 @@ export class DependencyResolver {
|
|
|
1235
963
|
options: ResolveOptions | undefined,
|
|
1236
964
|
resolutionPath: Array<string>,
|
|
1237
965
|
resolutionStack: Array<ResolutionFrame>,
|
|
966
|
+
branchDepth: BranchDepth = UNOWNED_BRANCH,
|
|
1238
967
|
): Promise<Value | undefined> {
|
|
1239
968
|
if (this.#findBinding(token, options, resolutionPath, resolutionStack) === undefined) {
|
|
1240
969
|
return undefined;
|
|
1241
970
|
}
|
|
1242
|
-
return this.resolveAsync(token, options, resolutionPath, resolutionStack);
|
|
971
|
+
return this.resolveAsync(token, options, resolutionPath, resolutionStack, branchDepth);
|
|
1243
972
|
}
|
|
1244
973
|
|
|
1245
974
|
async resolveAllAsync<const Value>(
|
|
@@ -1247,85 +976,88 @@ export class DependencyResolver {
|
|
|
1247
976
|
options: ResolveOptions | undefined,
|
|
1248
977
|
resolutionPath: Array<string>,
|
|
1249
978
|
resolutionStack: Array<ResolutionFrame>,
|
|
979
|
+
branchDepth: BranchDepth = UNOWNED_BRANCH,
|
|
1250
980
|
): Promise<Array<Value>> {
|
|
1251
|
-
|
|
1252
|
-
const namedCandidates = this.#getSimpleNamedBindingsFromChain(token, options.name);
|
|
1253
|
-
if (namedCandidates.length === 0) {
|
|
1254
|
-
return [];
|
|
1255
|
-
}
|
|
1256
|
-
const pending = new Array<Promise<Value>>(namedCandidates.length);
|
|
1257
|
-
for (let index = 0; index < namedCandidates.length; index += 1) {
|
|
1258
|
-
pending[index] = this.#resolveCandidateAsync(
|
|
1259
|
-
namedCandidates[index] as Binding<Value>,
|
|
1260
|
-
options,
|
|
1261
|
-
resolutionPath,
|
|
1262
|
-
resolutionStack,
|
|
1263
|
-
);
|
|
1264
|
-
}
|
|
1265
|
-
return Promise.all(pending);
|
|
1266
|
-
}
|
|
1267
|
-
|
|
1268
|
-
const allBindings = this.#getAllBindingsFromChain(token);
|
|
1269
|
-
if (allBindings.length === 0) {
|
|
1270
|
-
return [];
|
|
1271
|
-
}
|
|
1272
|
-
|
|
1273
|
-
const ctx = this.#makeConstraintContext(resolutionPath, resolutionStack, options);
|
|
1274
|
-
const candidates = selectAllBindings(allBindings, options, ctx);
|
|
1275
|
-
|
|
981
|
+
const candidates = this.#candidateBindings(token, options, resolutionPath, resolutionStack);
|
|
1276
982
|
const pending = new Array<Promise<Value>>(candidates.length);
|
|
1277
983
|
for (let index = 0; index < candidates.length; index += 1) {
|
|
1278
984
|
pending[index] = this.#resolveCandidateAsync(
|
|
1279
|
-
candidates[index]
|
|
985
|
+
candidates[index]!,
|
|
1280
986
|
options,
|
|
1281
987
|
resolutionPath,
|
|
1282
988
|
resolutionStack,
|
|
1283
|
-
|
|
989
|
+
branchDepth,
|
|
990
|
+
) as Promise<Value>;
|
|
1284
991
|
}
|
|
1285
992
|
return Promise.all(pending);
|
|
1286
993
|
}
|
|
1287
994
|
|
|
1288
995
|
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
1289
996
|
|
|
1290
|
-
#
|
|
997
|
+
#allBindingsFromChain(token: Token<unknown> | Constructor): ReadonlyArray<Binding> {
|
|
1291
998
|
const ownBindings = this.#registry.getAll(token);
|
|
1292
999
|
if (this.#parent === undefined) {
|
|
1293
1000
|
return ownBindings;
|
|
1294
1001
|
}
|
|
1295
1002
|
const result: Array<Binding> = [...ownBindings];
|
|
1296
|
-
let current: DependencyResolver | undefined = this.#parent;
|
|
1297
|
-
while (current !== undefined) {
|
|
1003
|
+
for (let current: DependencyResolver | undefined = this.#parent; current !== undefined; current = current.#parent) {
|
|
1298
1004
|
const own = current.#registry.getAll(token);
|
|
1299
1005
|
if (own.length > 0) {
|
|
1300
1006
|
result.push(...own);
|
|
1301
1007
|
}
|
|
1302
|
-
current = current.#parent;
|
|
1303
1008
|
}
|
|
1304
1009
|
return result;
|
|
1305
1010
|
}
|
|
1306
1011
|
|
|
1307
|
-
|
|
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.
|
|
1308
1016
|
const ownBinding = this.#registry.getSimpleNamed(token, name);
|
|
1309
1017
|
if (this.#parent === undefined) {
|
|
1310
|
-
return ownBinding
|
|
1311
|
-
}
|
|
1312
|
-
const result: Array<Binding> = [];
|
|
1313
|
-
if (ownBinding !== undefined) {
|
|
1314
|
-
result.push(ownBinding);
|
|
1018
|
+
return ownBinding === undefined ? [] : [ownBinding];
|
|
1315
1019
|
}
|
|
1316
|
-
|
|
1317
|
-
|
|
1020
|
+
const result: Array<Binding> = ownBinding === undefined ? [] : [ownBinding];
|
|
1021
|
+
for (let current: DependencyResolver | undefined = this.#parent; current !== undefined; current = current.#parent) {
|
|
1318
1022
|
const binding = current.#registry.getSimpleNamed(token, name);
|
|
1319
1023
|
if (binding !== undefined) {
|
|
1320
1024
|
result.push(binding);
|
|
1321
1025
|
}
|
|
1322
|
-
current = current.#parent;
|
|
1323
1026
|
}
|
|
1324
1027
|
return result;
|
|
1325
1028
|
}
|
|
1326
1029
|
|
|
1327
|
-
|
|
1328
|
-
|
|
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;
|
|
1329
1061
|
}
|
|
1330
1062
|
|
|
1331
1063
|
#makeConstraintContext(
|
|
@@ -1336,13 +1068,11 @@ export class DependencyResolver {
|
|
|
1336
1068
|
if (options === undefined && resolutionPath.length === 0 && resolutionStack.length === 0) {
|
|
1337
1069
|
return ROOT_CONSTRAINT_CONTEXT;
|
|
1338
1070
|
}
|
|
1339
|
-
const parent = resolutionStack.at(-1);
|
|
1340
|
-
const ancestors = resolutionStack.length > 1 ? resolutionStack.slice(0, -1) : [];
|
|
1341
1071
|
return {
|
|
1342
1072
|
resolutionPath,
|
|
1343
1073
|
resolutionStack,
|
|
1344
|
-
parent,
|
|
1345
|
-
ancestors,
|
|
1074
|
+
parent: resolutionStack.at(-1),
|
|
1075
|
+
ancestors: resolutionStack.length > 1 ? resolutionStack.slice(0, -1) : [],
|
|
1346
1076
|
currentResolveOptions: options,
|
|
1347
1077
|
};
|
|
1348
1078
|
}
|
|
@@ -1353,108 +1083,21 @@ export class DependencyResolver {
|
|
|
1353
1083
|
resolutionPath: Array<string>,
|
|
1354
1084
|
resolutionStack: Array<ResolutionFrame>,
|
|
1355
1085
|
): boolean {
|
|
1356
|
-
if (!
|
|
1086
|
+
if (!matchesSlot(binding.slot, options)) {
|
|
1357
1087
|
return false;
|
|
1358
1088
|
}
|
|
1359
1089
|
if (binding.predicate === undefined) {
|
|
1360
1090
|
return true;
|
|
1361
1091
|
}
|
|
1362
|
-
|
|
1363
|
-
return binding.predicate(ctx);
|
|
1364
|
-
}
|
|
1365
|
-
|
|
1366
|
-
#matchesSlotFast(slot: BindingSlot, options: ResolveOptions | undefined): boolean {
|
|
1367
|
-
const requestedName = options?.name;
|
|
1368
|
-
const requestedTags = options?.tags;
|
|
1369
|
-
const singleRequestedTag = options?.tag;
|
|
1370
|
-
const hasRequestedTags = (requestedTags?.length ?? 0) > 0 || singleRequestedTag !== undefined;
|
|
1371
|
-
|
|
1372
|
-
if (slot.name !== undefined) {
|
|
1373
|
-
if (requestedName === undefined || slot.name !== requestedName) {
|
|
1374
|
-
return false;
|
|
1375
|
-
}
|
|
1376
|
-
} else if (requestedName !== undefined) {
|
|
1377
|
-
return false;
|
|
1378
|
-
}
|
|
1379
|
-
|
|
1380
|
-
if (slot.tags.length > 0) {
|
|
1381
|
-
if (!hasRequestedTags) {
|
|
1382
|
-
return false;
|
|
1383
|
-
}
|
|
1384
|
-
for (const [tagKey, tagValue] of slot.tags) {
|
|
1385
|
-
if (!this.#matchesRequestedTag(tagKey, tagValue, requestedTags, singleRequestedTag)) {
|
|
1386
|
-
return false;
|
|
1387
|
-
}
|
|
1388
|
-
}
|
|
1389
|
-
} else if (hasRequestedTags) {
|
|
1390
|
-
return false;
|
|
1391
|
-
}
|
|
1392
|
-
|
|
1393
|
-
return true;
|
|
1092
|
+
return binding.predicate(this.#makeConstraintContext(resolutionPath, resolutionStack, options));
|
|
1394
1093
|
}
|
|
1395
1094
|
|
|
1396
|
-
#
|
|
1397
|
-
|
|
1398
|
-
}
|
|
1399
|
-
|
|
1400
|
-
#getConstructorMetadata(target: Constructor): ConstructorMetadata | undefined {
|
|
1401
|
-
const cached = this.#classConstructorMetadata.get(target);
|
|
1402
|
-
if (cached !== undefined) {
|
|
1403
|
-
return cached === null ? undefined : cached;
|
|
1404
|
-
}
|
|
1405
|
-
const metadata = this.#metadataReader.getConstructorMetadata(target);
|
|
1406
|
-
this.#classConstructorMetadata.set(target, metadata ?? null);
|
|
1407
|
-
return metadata;
|
|
1408
|
-
}
|
|
1409
|
-
|
|
1410
|
-
#instantiateClass(target: Constructor, deps: Array<unknown>): unknown {
|
|
1411
|
-
let needsActiveContainer = this.#classNeedsActiveContainer.get(target);
|
|
1412
|
-
if (needsActiveContainer === undefined) {
|
|
1413
|
-
const accessorMetadata = this.#metadataReader.getAccessorMetadata?.(target);
|
|
1414
|
-
needsActiveContainer = (accessorMetadata?.length ?? 0) > 0;
|
|
1415
|
-
this.#classNeedsActiveContainer.set(target, needsActiveContainer);
|
|
1416
|
-
}
|
|
1417
|
-
const invokable = target as ConstructorInvocation;
|
|
1418
|
-
if (!needsActiveContainer) {
|
|
1419
|
-
return new invokable(...deps);
|
|
1420
|
-
}
|
|
1421
|
-
return runWithContainer(this.#container, () => new invokable(...deps));
|
|
1422
|
-
}
|
|
1423
|
-
|
|
1424
|
-
#matchesRequestedTag(
|
|
1425
|
-
tagKey: string,
|
|
1426
|
-
tagValue: unknown,
|
|
1427
|
-
requestedTags: ReadonlyArray<BindingTag> | undefined,
|
|
1428
|
-
singleRequestedTag: BindingTag | undefined,
|
|
1429
|
-
): boolean {
|
|
1430
|
-
if (
|
|
1431
|
-
singleRequestedTag !== undefined &&
|
|
1432
|
-
singleRequestedTag[0] === tagKey &&
|
|
1433
|
-
Object.is(singleRequestedTag[1], tagValue)
|
|
1434
|
-
) {
|
|
1435
|
-
return true;
|
|
1436
|
-
}
|
|
1437
|
-
if (requestedTags === undefined || requestedTags.length === 0) {
|
|
1438
|
-
return false;
|
|
1439
|
-
}
|
|
1440
|
-
for (let index = 0; index < requestedTags.length; index += 1) {
|
|
1441
|
-
const requestedTag = requestedTags[index]!;
|
|
1442
|
-
if (requestedTag[0] === tagKey && Object.is(requestedTag[1], tagValue)) {
|
|
1443
|
-
return true;
|
|
1444
|
-
}
|
|
1445
|
-
}
|
|
1446
|
-
return false;
|
|
1447
|
-
}
|
|
1448
|
-
|
|
1449
|
-
#resolveTransientDynamicSyncFromContext<const Value>(
|
|
1450
|
-
binding: Binding<Value> & { kind: "dynamic" },
|
|
1095
|
+
#resolveTransientDynamicSyncFromContext(
|
|
1096
|
+
binding: DynamicBinding<unknown>,
|
|
1451
1097
|
resolutionPath: Array<string>,
|
|
1452
1098
|
resolutionStack: Array<ResolutionFrame>,
|
|
1453
|
-
):
|
|
1454
|
-
// One lane at every depth.
|
|
1455
|
-
// an O(depth) `resolutionPath.includes()` scan, which had to be escaped past ~32 levels; with
|
|
1456
|
-
// the O(1) `binding.inFlight` mark there is nothing to escape, so the depth split — and the
|
|
1457
|
-
// divergent behaviour it caused — is gone.
|
|
1099
|
+
): unknown {
|
|
1100
|
+
// One lane at every depth: `binding.inFlight` is O(1), so there is nothing to escape.
|
|
1458
1101
|
const frame = this.#getResolutionFrame(binding);
|
|
1459
1102
|
const tokenDisplayName = frame.tokenName;
|
|
1460
1103
|
if (binding.inFlight) {
|
|
@@ -1477,180 +1120,149 @@ export class DependencyResolver {
|
|
|
1477
1120
|
}
|
|
1478
1121
|
}
|
|
1479
1122
|
|
|
1480
|
-
//
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
#resolveTransientDynamicAsyncFromContext<const Value>(
|
|
1484
|
-
binding: Binding<Value> & { kind: "dynamic" | "dynamic-async" },
|
|
1123
|
+
// Deliberately not `async`: that would allocate a state machine and a promise per level.
|
|
1124
|
+
#resolveTransientDynamicAsyncFromContext(
|
|
1125
|
+
binding: DynamicBinding<unknown> | DynamicAsyncBinding<unknown>,
|
|
1485
1126
|
resolutionPath: Array<string>,
|
|
1486
1127
|
resolutionStack: Array<ResolutionFrame>,
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
// path-scoped — the only mechanism that stays correct when chains interleave (Promise.all) —
|
|
1490
|
-
// and adapts on its own: a linear scan while the path is short, an attached Set past
|
|
1491
|
-
// RESOLUTION_SET_THRESHOLD. That removes the depth split, and with it a silent change of
|
|
1492
|
-
// behaviour (context identity, stack frames, promise shape) at the old threshold.
|
|
1493
|
-
//
|
|
1494
|
-
// For a sequential chain every level shares one resolutionPath/resolutionStack, so a single
|
|
1495
|
-
// DefaultResolutionContext serves the whole chain: inner levels of the owning chain allocate
|
|
1496
|
-
// nothing. A concurrent chain is detected by path identity and gets its own context.
|
|
1128
|
+
branchDepth: BranchDepth,
|
|
1129
|
+
): Promise<unknown> {
|
|
1497
1130
|
const frame = this.#getResolutionFrame(binding);
|
|
1498
|
-
|
|
1131
|
+
let levelPath: OwnedBranchPath;
|
|
1499
1132
|
try {
|
|
1500
|
-
|
|
1133
|
+
levelPath = extendResolutionBranch(resolutionPath, branchDepth, frame.tokenName);
|
|
1501
1134
|
} catch (cycleError) {
|
|
1502
1135
|
// This method is not `async`; keep failures as rejections rather than sync throws.
|
|
1503
1136
|
return Promise.reject(cycleError);
|
|
1504
1137
|
}
|
|
1138
|
+
const levelStack = extendResolutionStackBranch(resolutionStack, branchDepth, frame);
|
|
1505
1139
|
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
} else if (this.#asyncChainCtxPath === undefined) {
|
|
1512
|
-
const existing = this.#asyncChainCtx;
|
|
1513
|
-
if (existing === undefined) {
|
|
1514
|
-
ctx = new DefaultResolutionContext(
|
|
1515
|
-
this as unknown as ResolverCallbacks,
|
|
1516
|
-
resolutionPath,
|
|
1517
|
-
resolutionStack,
|
|
1518
|
-
undefined,
|
|
1519
|
-
);
|
|
1520
|
-
this.#asyncChainCtx = ctx;
|
|
1521
|
-
} else {
|
|
1522
|
-
existing.reset(this as unknown as ResolverCallbacks, resolutionPath, resolutionStack, undefined);
|
|
1523
|
-
ctx = existing;
|
|
1140
|
+
// Nothing this level appended is ever removed, so no level observes its own settlement.
|
|
1141
|
+
const ctx = new AsyncLevelContext(this, levelPath, levelStack, undefined);
|
|
1142
|
+
try {
|
|
1143
|
+
if (binding.kind === "dynamic-async") {
|
|
1144
|
+
return binding.factory(ctx);
|
|
1524
1145
|
}
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
}
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1146
|
+
const factoryResult = binding.factory(ctx);
|
|
1147
|
+
return factoryResult instanceof Promise ? factoryResult : Promise.resolve(factoryResult);
|
|
1148
|
+
} catch (factoryError) {
|
|
1149
|
+
return Promise.reject(factoryError);
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
|
|
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);
|
|
1535
1164
|
}
|
|
1165
|
+
return this.#dispatchCascade(token);
|
|
1166
|
+
}
|
|
1536
1167
|
|
|
1537
|
-
|
|
1538
|
-
|
|
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
|
+
}
|
|
1539
1191
|
}
|
|
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);
|
|
1195
|
+
}
|
|
1540
1196
|
|
|
1541
|
-
|
|
1542
|
-
|
|
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);
|
|
1543
1211
|
try {
|
|
1544
1212
|
if (binding.kind === "dynamic-async") {
|
|
1545
|
-
|
|
1546
|
-
} else {
|
|
1547
|
-
const factoryResult = binding.factory(ctx);
|
|
1548
|
-
factoryPromise =
|
|
1549
|
-
factoryResult instanceof Promise ? (factoryResult as Promise<Value>) : Promise.resolve(factoryResult);
|
|
1213
|
+
return binding.factory(ctx);
|
|
1550
1214
|
}
|
|
1215
|
+
const factoryResult = binding.factory(ctx);
|
|
1216
|
+
return factoryResult instanceof Promise ? factoryResult : Promise.resolve(factoryResult);
|
|
1551
1217
|
} catch (factoryError) {
|
|
1552
|
-
// Synchronous throw from the factory (rare) — clean up immediately.
|
|
1553
|
-
exitResolutionPath(resolutionPath);
|
|
1554
|
-
if (isOwnerLevel && --this.#asyncChainActiveLevels === 0) {
|
|
1555
|
-
this.#asyncChainCtxPath = undefined;
|
|
1556
|
-
this.#asyncChainSettle = undefined;
|
|
1557
|
-
}
|
|
1558
1218
|
return Promise.reject(factoryError);
|
|
1219
|
+
} finally {
|
|
1220
|
+
this.#cascadeStack.pop();
|
|
1221
|
+
this.#cascadePath.pop();
|
|
1222
|
+
binding.inFlight = false;
|
|
1559
1223
|
}
|
|
1560
|
-
|
|
1561
|
-
// Cleanup runs as a SIDE listener on the factory promise instead of a derived-promise chain:
|
|
1562
|
-
// registered synchronously here, it is FIFO-guaranteed to run before the awaiting caller
|
|
1563
|
-
// resumes, so ordering is identical while saving one intermediate promise and one microtask
|
|
1564
|
-
// hop per level. Trade-off: the settle handler marks a rejection as handled, so an unawaited
|
|
1565
|
-
// failing resolveAsync no longer surfaces as an unhandledRejection — callers are expected to
|
|
1566
|
-
// await (or .catch) the returned promise.
|
|
1567
|
-
//
|
|
1568
|
-
// Every level of the owning chain unwinds identically, so one closure serves them all.
|
|
1569
|
-
let settle: () => void;
|
|
1570
|
-
if (isOwnerLevel) {
|
|
1571
|
-
settle =
|
|
1572
|
-
this.#asyncChainSettle ??
|
|
1573
|
-
(this.#asyncChainSettle = (): void => {
|
|
1574
|
-
exitResolutionPath(resolutionPath);
|
|
1575
|
-
if (--this.#asyncChainActiveLevels === 0) {
|
|
1576
|
-
this.#asyncChainCtxPath = undefined;
|
|
1577
|
-
this.#asyncChainSettle = undefined;
|
|
1578
|
-
}
|
|
1579
|
-
});
|
|
1580
|
-
} else {
|
|
1581
|
-
settle = (): void => {
|
|
1582
|
-
exitResolutionPath(resolutionPath);
|
|
1583
|
-
};
|
|
1584
|
-
}
|
|
1585
|
-
factoryPromise.then(settle, settle);
|
|
1586
|
-
return factoryPromise;
|
|
1587
1224
|
}
|
|
1588
1225
|
|
|
1589
|
-
|
|
1590
|
-
|
|
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,
|
|
1591
1230
|
options: ResolveOptions | undefined,
|
|
1592
1231
|
resolutionPath: Array<string>,
|
|
1593
1232
|
resolutionStack: Array<ResolutionFrame>,
|
|
1594
|
-
):
|
|
1595
|
-
if (
|
|
1596
|
-
binding.kind === "constant" &&
|
|
1597
|
-
binding.onActivation === undefined &&
|
|
1598
|
-
!this.#lifecycle.hasActivationHandlers(binding.token)
|
|
1599
|
-
) {
|
|
1233
|
+
): unknown {
|
|
1234
|
+
if (this.#isPlainConstant(binding)) {
|
|
1600
1235
|
return binding.value;
|
|
1601
1236
|
}
|
|
1602
1237
|
if (binding.kind === "alias") {
|
|
1603
1238
|
return this.resolve(binding.target, options, resolutionPath, resolutionStack);
|
|
1604
1239
|
}
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
return this.#scope.getSingleton<Value>(binding.id);
|
|
1608
|
-
}
|
|
1609
|
-
if (scope === "scoped") {
|
|
1610
|
-
if (!this.#scope.isChild) {
|
|
1611
|
-
throw new MissingScopeContextError(this.#getTokenName(binding.token));
|
|
1612
|
-
}
|
|
1613
|
-
if (this.#scope.hasScoped(binding.id)) {
|
|
1614
|
-
return this.#scope.getScoped<Value>(binding.id);
|
|
1615
|
-
}
|
|
1240
|
+
if (binding.scope === "singleton" && binding.instance !== NO_INSTANCE) {
|
|
1241
|
+
return binding.instance;
|
|
1616
1242
|
}
|
|
1617
1243
|
return this.#resolveBinding(binding, options, resolutionPath, resolutionStack);
|
|
1618
1244
|
}
|
|
1619
1245
|
|
|
1620
|
-
#resolveCandidateAsync
|
|
1621
|
-
binding: Binding
|
|
1246
|
+
#resolveCandidateAsync(
|
|
1247
|
+
binding: Binding,
|
|
1622
1248
|
options: ResolveOptions | undefined,
|
|
1623
1249
|
resolutionPath: Array<string>,
|
|
1624
1250
|
resolutionStack: Array<ResolutionFrame>,
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
binding.onActivation === undefined &&
|
|
1629
|
-
!this.#lifecycle.hasActivationHandlers(binding.token)
|
|
1630
|
-
) {
|
|
1251
|
+
branchDepth: BranchDepth,
|
|
1252
|
+
): Promise<unknown> {
|
|
1253
|
+
if (this.#isPlainConstant(binding)) {
|
|
1631
1254
|
return Promise.resolve(binding.value);
|
|
1632
1255
|
}
|
|
1633
|
-
const isolatedPath = [...resolutionPath];
|
|
1634
|
-
const isolatedStack = [...resolutionStack];
|
|
1635
1256
|
if (binding.kind === "alias") {
|
|
1636
|
-
return this.resolveAsync(binding.target, options,
|
|
1257
|
+
return this.resolveAsync(binding.target, options, resolutionPath, resolutionStack, branchDepth);
|
|
1637
1258
|
}
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
return Promise.resolve(this.#scope.getSingleton<Value>(binding.id));
|
|
1259
|
+
if (binding.scope === "singleton" && binding.instance !== NO_INSTANCE) {
|
|
1260
|
+
return Promise.resolve(binding.instance);
|
|
1641
1261
|
}
|
|
1642
|
-
|
|
1643
|
-
if (!this.#scope.isChild) {
|
|
1644
|
-
return Promise.reject(new MissingScopeContextError(this.#getTokenName(binding.token)));
|
|
1645
|
-
}
|
|
1646
|
-
if (this.#scope.hasScoped(binding.id)) {
|
|
1647
|
-
return Promise.resolve(this.#scope.getScoped<Value>(binding.id));
|
|
1648
|
-
}
|
|
1649
|
-
}
|
|
1650
|
-
return this.#resolveBindingAsync(binding, options, isolatedPath, isolatedStack);
|
|
1262
|
+
return this.#resolveBindingAsync(binding, options, resolutionPath, resolutionStack, branchDepth);
|
|
1651
1263
|
}
|
|
1652
1264
|
|
|
1653
|
-
#getResolutionFrame
|
|
1265
|
+
#getResolutionFrame(binding: Binding): ResolutionFrame {
|
|
1654
1266
|
// Memoized on the binding rather than in a per-resolver Map: the frame derives only from
|
|
1655
1267
|
// immutable binding fields, so it is identical for every resolver, and a field read beats a
|
|
1656
1268
|
// Map lookup on every hop of a chain.
|
|
@@ -1658,80 +1270,11 @@ export class DependencyResolver {
|
|
|
1658
1270
|
if (existing !== undefined) {
|
|
1659
1271
|
return existing;
|
|
1660
1272
|
}
|
|
1661
|
-
const
|
|
1662
|
-
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);
|
|
1663
1274
|
binding.frame = frame;
|
|
1664
1275
|
return frame;
|
|
1665
1276
|
}
|
|
1666
1277
|
|
|
1667
|
-
#needsActivation<const Value>(binding: Binding<Value>): boolean {
|
|
1668
|
-
const lifecycleVersion = this.#lifecycle.activationVersion;
|
|
1669
|
-
if (
|
|
1670
|
-
lifecycleVersion === 0 &&
|
|
1671
|
-
binding.kind !== "class" &&
|
|
1672
|
-
binding.kind !== "alias" &&
|
|
1673
|
-
binding.onActivation === undefined
|
|
1674
|
-
) {
|
|
1675
|
-
return false;
|
|
1676
|
-
}
|
|
1677
|
-
if (this.#activationCacheVersion !== lifecycleVersion) {
|
|
1678
|
-
this.#activationNeedByBindingId.clear();
|
|
1679
|
-
this.#activationCacheVersion = lifecycleVersion;
|
|
1680
|
-
}
|
|
1681
|
-
|
|
1682
|
-
const cached = this.#activationNeedByBindingId.get(binding.id);
|
|
1683
|
-
if (cached !== undefined) {
|
|
1684
|
-
return cached;
|
|
1685
|
-
}
|
|
1686
|
-
|
|
1687
|
-
if (binding.kind === "class") {
|
|
1688
|
-
let hasActivation = this.#lifecycle.hasActivationHandlers(binding.token) || binding.onActivation !== undefined;
|
|
1689
|
-
const cachedPostConstruct = this.#classHasPostConstruct.get(binding.target);
|
|
1690
|
-
// Unknown class lifecycle metadata: activate once, then cache after first instantiation.
|
|
1691
|
-
if (cachedPostConstruct === undefined) {
|
|
1692
|
-
hasActivation = true;
|
|
1693
|
-
} else if (cachedPostConstruct) {
|
|
1694
|
-
hasActivation = true;
|
|
1695
|
-
}
|
|
1696
|
-
this.#activationNeedByBindingId.set(binding.id, hasActivation);
|
|
1697
|
-
return hasActivation;
|
|
1698
|
-
}
|
|
1699
|
-
|
|
1700
|
-
let hasActivation = false;
|
|
1701
|
-
if (binding.kind !== "alias" && binding.onActivation !== undefined) {
|
|
1702
|
-
hasActivation = true;
|
|
1703
|
-
} else if (this.#lifecycle.hasActivationHandlers(binding.token)) {
|
|
1704
|
-
hasActivation = true;
|
|
1705
|
-
}
|
|
1706
|
-
|
|
1707
|
-
this.#activationNeedByBindingId.set(binding.id, hasActivation);
|
|
1708
|
-
return hasActivation;
|
|
1709
|
-
}
|
|
1710
|
-
|
|
1711
|
-
#refreshClassPostConstructCache(target: Constructor): void {
|
|
1712
|
-
const lifecycle = this.#metadataReader.getLifecycleMetadata(target);
|
|
1713
|
-
const hasPostConstruct =
|
|
1714
|
-
lifecycle !== undefined && lifecycle.postConstruct !== undefined && lifecycle.postConstruct.length > 0;
|
|
1715
|
-
this.#classHasPostConstruct.set(target, hasPostConstruct);
|
|
1716
|
-
}
|
|
1717
|
-
|
|
1718
|
-
/**
|
|
1719
|
-
* Refreshes the post-construct cache for class bindings on first instantiation and
|
|
1720
|
-
* returns the (possibly updated) shouldActivate flag.
|
|
1721
|
-
*/
|
|
1722
|
-
#refreshActivationCacheIfNeeded<Value>(binding: Binding<Value>, needsActivation: boolean): boolean {
|
|
1723
|
-
if (binding.kind === "class" && this.#classHasPostConstruct.get(binding.target) === undefined) {
|
|
1724
|
-
this.#refreshClassPostConstructCache(binding.target);
|
|
1725
|
-
this.#activationNeedByBindingId.delete(binding.id);
|
|
1726
|
-
return this.#needsActivation(binding);
|
|
1727
|
-
}
|
|
1728
|
-
return needsActivation;
|
|
1729
|
-
}
|
|
1730
|
-
|
|
1731
|
-
#requiresResolutionContext<const Value>(binding: Binding<Value>): boolean {
|
|
1732
|
-
return binding.kind === "dynamic" || binding.kind === "dynamic-async";
|
|
1733
|
-
}
|
|
1734
|
-
|
|
1735
1278
|
#acquireSyncResolutionContext(
|
|
1736
1279
|
resolutionPath: Array<string>,
|
|
1737
1280
|
resolutionStack: Array<ResolutionFrame>,
|
|
@@ -1740,16 +1283,41 @@ export class DependencyResolver {
|
|
|
1740
1283
|
const depth = resolutionStack.length;
|
|
1741
1284
|
const existing = this.#syncResolutionContextPool[depth];
|
|
1742
1285
|
if (existing !== undefined) {
|
|
1743
|
-
existing.reset(this
|
|
1286
|
+
existing.reset(this, resolutionPath, resolutionStack, options);
|
|
1744
1287
|
return existing;
|
|
1745
1288
|
}
|
|
1746
|
-
const created = new DefaultResolutionContext(
|
|
1747
|
-
this as unknown as ResolverCallbacks,
|
|
1748
|
-
resolutionPath,
|
|
1749
|
-
resolutionStack,
|
|
1750
|
-
options,
|
|
1751
|
-
);
|
|
1289
|
+
const created = new DefaultResolutionContext(this, resolutionPath, resolutionStack, options);
|
|
1752
1290
|
this.#syncResolutionContextPool[depth] = created;
|
|
1753
1291
|
return created;
|
|
1754
1292
|
}
|
|
1755
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
|
+
}
|