@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,16 +1,17 @@
|
|
|
1
|
+
import { NO_INSTANCE } from "#/binding";
|
|
1
2
|
import { AsyncActivationError, AsyncResolutionError, CircularDependencyError, InternalError, MissingMetadataError, MissingScopeContextError, NoMatchingBindingError, TokenNotBoundError, } from "#/errors";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
3
|
+
import { ActivationNeedCache } from "#/resolution/activation-need";
|
|
4
|
+
import { BindingLookupCache } from "#/resolution/binding-lookup-cache";
|
|
5
|
+
import { matchesSlot, selectAllBindings, selectBinding } from "#/resolution/binding-select";
|
|
6
|
+
import { ClassIntrospector } from "#/resolution/class-introspector";
|
|
7
|
+
import { AsyncCascadeContext, AsyncLevelContext, buildResolutionFrame, DefaultResolutionContext, } from "#/resolution/environment";
|
|
4
8
|
import { InstantiationPlanCompiler, PLAN_RETRY } from "#/resolution/instantiation-plan";
|
|
5
|
-
import { enterResolutionPath,
|
|
6
|
-
import { injectionSlotToResolveOptions } from "#/resolution/resolve-options";
|
|
7
|
-
import { SINGLETON_MISS } from "#/resolution/scope";
|
|
9
|
+
import { branchDepthOf, enterResolutionPath, extendResolutionBranch, extendResolutionStackBranch, ROOT_BRANCH, UNOWNED_BRANCH, } from "#/resolution/resolution-path";
|
|
10
|
+
import { injectionSlotToResolveOptions, isNameOnlyOptions, singleTagOnlyOf } from "#/resolution/resolve-options";
|
|
8
11
|
import { tokenName } from "#/token";
|
|
9
|
-
// Fast-lane alias folding gives up past this many hops and defers to the resolve()
|
|
10
|
-
// loop, whose Set-based traversal detects genuine cycles exactly (no arbitrary cap).
|
|
11
|
-
const ALIAS_HOP_LIMIT = 32;
|
|
12
12
|
const EMPTY_STRING_LIST = [];
|
|
13
13
|
const EMPTY_FRAME_LIST = [];
|
|
14
|
+
const EMPTY_PARAM_LIST = [];
|
|
14
15
|
const ROOT_CONSTRAINT_CONTEXT = {
|
|
15
16
|
resolutionPath: EMPTY_STRING_LIST,
|
|
16
17
|
resolutionStack: EMPTY_FRAME_LIST,
|
|
@@ -23,51 +24,23 @@ const ROOT_CONSTRAINT_CONTEXT = {
|
|
|
23
24
|
*/
|
|
24
25
|
export class DependencyResolver {
|
|
25
26
|
#syncResolutionContextPool = [];
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
// The method is NOT declared async: that would allocate an async state machine and an implicit
|
|
45
|
-
// promise per level, where a `.then(settle, settle)` side listener costs neither.
|
|
46
|
-
#asyncChainCtx;
|
|
47
|
-
#asyncChainCtxPath;
|
|
48
|
-
#asyncChainActiveLevels = 0;
|
|
49
|
-
// Settle callback shared by every level of the owning async chain: all of them pop the same
|
|
50
|
-
// resolutionPath and decrement the same counter, so one closure serves the whole chain instead
|
|
51
|
-
// of allocating one per level (the async lane's dominant per-level allocation).
|
|
52
|
-
#asyncChainSettle;
|
|
53
|
-
#classHasPostConstruct = new WeakMap();
|
|
54
|
-
#classNeedsActiveContainer = new WeakMap();
|
|
55
|
-
#classConstructorMetadata = new WeakMap();
|
|
56
|
-
#activationNeedByBindingId = new Map();
|
|
57
|
-
#activationCacheVersion = -1;
|
|
58
|
-
// Options-less lookup memo across the parent chain: token → terminal {binding, owner}
|
|
59
|
-
// (alias hops folded). `null` = token must take the slow lookup path. Invalidated when
|
|
60
|
-
// any registry in the chain mutates (monotonic version sum).
|
|
61
|
-
#defaultLookupByToken = new Map();
|
|
62
|
-
#defaultLookupVersion = -1;
|
|
63
|
-
// Name-only lookup memo (token → name → entry) with the same chain-version
|
|
64
|
-
// invalidation. `null` = shape needs the full selection path.
|
|
65
|
-
#namedLookupByToken = new Map();
|
|
66
|
-
#namedLookupVersion = -1;
|
|
67
|
-
// Compiled transient-class plans (Dagger-style): a pure-static subgraph (class/constant/
|
|
68
|
-
// cached-singleton deps only) compiles once into a nested-constructor closure — cycle
|
|
69
|
-
// checking happens at compile time, so execution skips all per-resolve bookkeeping.
|
|
70
|
-
// `null` = binding is not plannable under the current versions.
|
|
27
|
+
/**
|
|
28
|
+
* The pair a top-level **sync** resolve reuses instead of minting two arrays per call.
|
|
29
|
+
*
|
|
30
|
+
* @remarks Read directly rather than through an accessor returning both: a shallow resolve is one
|
|
31
|
+
* top-level call, so a call and an object literal there are not amortised over anything. Every sync
|
|
32
|
+
* lane pops what it pushes, so `rootStack.length === 0` means no resolve holds the pair; async
|
|
33
|
+
* appends without popping and mints its own. Keeping the pair stable is also what lets a pooled
|
|
34
|
+
* context skip storing pointers it already holds.
|
|
35
|
+
*/
|
|
36
|
+
rootPath = [];
|
|
37
|
+
rootStack = [];
|
|
38
|
+
// The open synchronous factory cascade: its arrays are the ancestor chain, and they are balanced
|
|
39
|
+
// because synchronous code does not interleave.
|
|
40
|
+
#cascadePath = [];
|
|
41
|
+
#cascadeStack = [];
|
|
42
|
+
#cascadeContext;
|
|
43
|
+
// Compiled plans; `null` marks a binding as unplannable under the current cache versions.
|
|
71
44
|
#classPlanByBindingId = new Map();
|
|
72
45
|
#classPlanRegistryVersion = -1;
|
|
73
46
|
#classPlanActivationVersion = -1;
|
|
@@ -75,15 +48,32 @@ export class DependencyResolver {
|
|
|
75
48
|
#scope;
|
|
76
49
|
#lifecycle;
|
|
77
50
|
#metadataReader;
|
|
78
|
-
#container;
|
|
79
51
|
#parent;
|
|
52
|
+
#lookup;
|
|
53
|
+
#classes;
|
|
54
|
+
#activation;
|
|
80
55
|
constructor(registry, scope, lifecycle, metadataReader, container, parent) {
|
|
81
56
|
this.#registry = registry;
|
|
82
57
|
this.#scope = scope;
|
|
83
58
|
this.#lifecycle = lifecycle;
|
|
84
59
|
this.#metadataReader = metadataReader;
|
|
85
|
-
this.#container = container;
|
|
86
60
|
this.#parent = parent;
|
|
61
|
+
this.#lookup = new BindingLookupCache(registry, this, parent === undefined ? undefined : parent.#lookup);
|
|
62
|
+
this.#classes = new ClassIntrospector(metadataReader, container);
|
|
63
|
+
this.#activation = new ActivationNeedCache(lifecycle, this.#classes, registry);
|
|
64
|
+
}
|
|
65
|
+
/** Structural counts for {@link RESOLUTION_DIAGNOSTICS}; see `resolution/diagnostics.ts`. */
|
|
66
|
+
describeCaches() {
|
|
67
|
+
let compiledPlanCount = 0;
|
|
68
|
+
for (const plan of this.#classPlanByBindingId.values()) {
|
|
69
|
+
if (plan !== null) {
|
|
70
|
+
compiledPlanCount += 1;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
compiledPlanCount,
|
|
75
|
+
syncContextPoolSize: this.#syncResolutionContextPool.length,
|
|
76
|
+
};
|
|
87
77
|
}
|
|
88
78
|
// ── Binding lookup ─────────────────────────────────────────────────────────
|
|
89
79
|
#findBinding(token, options, resolutionPath, resolutionStack) {
|
|
@@ -93,39 +83,33 @@ export class DependencyResolver {
|
|
|
93
83
|
return { binding: fastDefaultBinding, owner: this };
|
|
94
84
|
}
|
|
95
85
|
}
|
|
96
|
-
if (
|
|
86
|
+
else if (isNameOnlyOptions(options)) {
|
|
97
87
|
const namedBinding = this.#registry.getSimpleNamed(token, options.name);
|
|
98
88
|
if (namedBinding !== undefined &&
|
|
99
89
|
this.#matchesBindingFast(namedBinding, options, resolutionPath, resolutionStack)) {
|
|
100
90
|
return { binding: namedBinding, owner: this };
|
|
101
91
|
}
|
|
102
92
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
return { binding: tagged, owner: this };
|
|
93
|
+
else {
|
|
94
|
+
const singleTag = singleTagOnlyOf(options);
|
|
95
|
+
if (singleTag !== undefined) {
|
|
96
|
+
const tagged = this.#registry.getSimpleTagged(token, singleTag[0], singleTag[1]);
|
|
97
|
+
if (tagged !== undefined && matchesIndexedTagValue(tagged, singleTag[1])) {
|
|
98
|
+
return { binding: tagged, owner: this };
|
|
99
|
+
}
|
|
111
100
|
}
|
|
112
101
|
}
|
|
113
102
|
const bindings = this.#registry.getAll(token);
|
|
114
103
|
if (bindings.length > 0) {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
const ctx = this.#makeConstraintContext(resolutionPath, resolutionStack, options);
|
|
126
|
-
const binding = selectBinding(bindings, options, ctx, this.#getTokenName(token));
|
|
127
|
-
if (binding !== undefined) {
|
|
128
|
-
return { binding, owner: this };
|
|
104
|
+
// A lone candidate is its own selection: matching it is the whole decision, with no
|
|
105
|
+
// specificity to weigh and no ambiguity to report.
|
|
106
|
+
const selected = bindings.length === 1
|
|
107
|
+
? this.#matchesBindingFast(bindings[0], options, resolutionPath, resolutionStack)
|
|
108
|
+
? bindings[0]
|
|
109
|
+
: undefined
|
|
110
|
+
: selectBinding(bindings, options, this.#makeConstraintContext(resolutionPath, resolutionStack, options), tokenName(token));
|
|
111
|
+
if (selected !== undefined) {
|
|
112
|
+
return { binding: selected, owner: this };
|
|
129
113
|
}
|
|
130
114
|
}
|
|
131
115
|
if (this.#parent !== undefined) {
|
|
@@ -133,6 +117,36 @@ export class DependencyResolver {
|
|
|
133
117
|
}
|
|
134
118
|
return undefined;
|
|
135
119
|
}
|
|
120
|
+
/**
|
|
121
|
+
* The binding a token resolves to with alias hops followed, or a diagnostic throw.
|
|
122
|
+
*
|
|
123
|
+
* @remarks Alias hops are followed iteratively with exact cycle detection — a revisited alias
|
|
124
|
+
* token raises {@link CircularDependencyError} instead of overflowing the call stack.
|
|
125
|
+
*/
|
|
126
|
+
#requireBinding(token, options, resolutionPath, resolutionStack) {
|
|
127
|
+
let currentToken = token;
|
|
128
|
+
let visitedAliasTokens;
|
|
129
|
+
let found = this.#findBinding(currentToken, options, resolutionPath, resolutionStack);
|
|
130
|
+
while (found !== undefined && found.binding.kind === "alias") {
|
|
131
|
+
const target = found.binding.target;
|
|
132
|
+
visitedAliasTokens ??= new Set([currentToken]);
|
|
133
|
+
if (visitedAliasTokens.has(target)) {
|
|
134
|
+
throw new CircularDependencyError([...visitedAliasTokens, target].map((entry) => tokenName(entry)));
|
|
135
|
+
}
|
|
136
|
+
visitedAliasTokens.add(target);
|
|
137
|
+
currentToken = target;
|
|
138
|
+
found = this.#findBinding(currentToken, options, resolutionPath, resolutionStack);
|
|
139
|
+
}
|
|
140
|
+
if (found === undefined) {
|
|
141
|
+
// Thrown here rather than from a helper: the error captures this stack, and an error path is
|
|
142
|
+
// dominated by that capture. Bindings under the token mean the request matched none of them.
|
|
143
|
+
if (this.#registry.getAll(currentToken).length > 0) {
|
|
144
|
+
throw new NoMatchingBindingError(tokenName(currentToken), options ?? {}, this.#registry.availableSlotStrings(currentToken));
|
|
145
|
+
}
|
|
146
|
+
throw new TokenNotBoundError(tokenName(currentToken));
|
|
147
|
+
}
|
|
148
|
+
return found;
|
|
149
|
+
}
|
|
136
150
|
/**
|
|
137
151
|
* Binding lookup aligned with `resolve` — used by `Container.validate` without instantiating.
|
|
138
152
|
*/
|
|
@@ -143,15 +157,7 @@ export class DependencyResolver {
|
|
|
143
157
|
* Mirrors {@link DependencyResolver.resolveAll} candidate selection only (no instantiation).
|
|
144
158
|
*/
|
|
145
159
|
peekCandidateBindingsForValidate(token, options) {
|
|
146
|
-
|
|
147
|
-
return this.#getSimpleNamedBindingsFromChain(token, options.name);
|
|
148
|
-
}
|
|
149
|
-
const allBindings = this.#getAllBindingsFromChain(token);
|
|
150
|
-
if (allBindings.length === 0) {
|
|
151
|
-
return [];
|
|
152
|
-
}
|
|
153
|
-
const ctx = this.#makeConstraintContext([], [], options);
|
|
154
|
-
return selectAllBindings(allBindings, options, ctx);
|
|
160
|
+
return this.#candidateBindings(token, options, [], []);
|
|
155
161
|
}
|
|
156
162
|
// ── Sync resolve ───────────────────────────────────────────────────────────
|
|
157
163
|
resolveFromContext(token, resolutionPath, resolutionStack) {
|
|
@@ -161,19 +167,14 @@ export class DependencyResolver {
|
|
|
161
167
|
if (fastBinding !== undefined && fastBinding.kind !== "alias") {
|
|
162
168
|
return this.#resolveDefaultEntry(fastBinding, this, resolutionPath, resolutionStack);
|
|
163
169
|
}
|
|
164
|
-
const entry = this.#
|
|
170
|
+
const entry = this.#lookup.defaultEntry(token);
|
|
165
171
|
if (entry === null) {
|
|
166
172
|
return this.resolve(token, undefined, resolutionPath, resolutionStack);
|
|
167
173
|
}
|
|
168
174
|
return this.#resolveDefaultEntry(entry.binding, entry.owner, resolutionPath, resolutionStack);
|
|
169
175
|
}
|
|
170
176
|
#resolveDefaultEntry(binding, owner, resolutionPath, resolutionStack) {
|
|
171
|
-
|
|
172
|
-
binding.onActivation === undefined &&
|
|
173
|
-
(this.#lifecycle.activationVersion === 0 || !this.#lifecycle.hasActivationHandlers(binding.token))) {
|
|
174
|
-
return binding.value;
|
|
175
|
-
}
|
|
176
|
-
const scope = binding.scope ?? "transient";
|
|
177
|
+
const scope = binding.scope;
|
|
177
178
|
if (scope === "transient") {
|
|
178
179
|
if (binding.kind === "dynamic") {
|
|
179
180
|
const containerHooks = this.#lifecycle.activationVersion === 0 ? undefined : this.#lifecycle.activationHandlersFor(binding.token);
|
|
@@ -191,8 +192,12 @@ export class DependencyResolver {
|
|
|
191
192
|
}
|
|
192
193
|
}
|
|
193
194
|
else if (scope === "singleton") {
|
|
194
|
-
|
|
195
|
-
if (
|
|
195
|
+
// A constant is a singleton that is already its own instance.
|
|
196
|
+
if (this.#isPlainConstant(binding)) {
|
|
197
|
+
return binding.value;
|
|
198
|
+
}
|
|
199
|
+
const cachedSingleton = binding.instance;
|
|
200
|
+
if (cachedSingleton !== NO_INSTANCE) {
|
|
196
201
|
return cachedSingleton;
|
|
197
202
|
}
|
|
198
203
|
if (owner !== this) {
|
|
@@ -200,11 +205,9 @@ export class DependencyResolver {
|
|
|
200
205
|
}
|
|
201
206
|
}
|
|
202
207
|
else {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
if (this.#scope.hasScoped(binding.id)) {
|
|
207
|
-
return this.#scope.getScoped(binding.id);
|
|
208
|
+
const cachedScoped = this.#readScoped(binding);
|
|
209
|
+
if (cachedScoped !== SCOPED_MISS) {
|
|
210
|
+
return cachedScoped;
|
|
208
211
|
}
|
|
209
212
|
}
|
|
210
213
|
return this.#resolveBinding(binding, undefined, resolutionPath, resolutionStack);
|
|
@@ -213,9 +216,15 @@ export class DependencyResolver {
|
|
|
213
216
|
// generic #resolveBinding path (guard, frame, ctx, per-binding then container hooks) with
|
|
214
217
|
// the kind/activation dispatch resolved statically.
|
|
215
218
|
#resolveTransientDynamicActivatedSync(binding, containerHooks, resolutionPath, resolutionStack) {
|
|
219
|
+
// Same O(1) cycle guard as the unhooked lane: this is still one sync call stack, so the flag
|
|
220
|
+
// *is* exact path membership — see ARCHITECTURE.md.
|
|
216
221
|
const frame = this.#getResolutionFrame(binding);
|
|
217
222
|
const tokenDisplayName = frame.tokenName;
|
|
218
|
-
|
|
223
|
+
if (binding.inFlight) {
|
|
224
|
+
throw new CircularDependencyError([...resolutionPath, tokenDisplayName]);
|
|
225
|
+
}
|
|
226
|
+
binding.inFlight = true;
|
|
227
|
+
resolutionPath.push(tokenDisplayName);
|
|
219
228
|
resolutionStack.push(frame);
|
|
220
229
|
try {
|
|
221
230
|
const resolutionCtx = this.#acquireSyncResolutionContext(resolutionPath, resolutionStack, undefined);
|
|
@@ -245,86 +254,11 @@ export class DependencyResolver {
|
|
|
245
254
|
finally {
|
|
246
255
|
resolutionStack.pop();
|
|
247
256
|
resolutionPath.pop();
|
|
248
|
-
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
#chainRegistryVersion() {
|
|
252
|
-
let version = this.#registry.version;
|
|
253
|
-
for (let resolver = this.#parent; resolver !== undefined; resolver = resolver.#parent) {
|
|
254
|
-
version += resolver.#registry.version;
|
|
255
|
-
}
|
|
256
|
-
return version;
|
|
257
|
-
}
|
|
258
|
-
#lookupDefaultEntry(token) {
|
|
259
|
-
const version = this.#chainRegistryVersion();
|
|
260
|
-
if (version !== this.#defaultLookupVersion) {
|
|
261
|
-
this.#defaultLookupByToken.clear();
|
|
262
|
-
this.#defaultLookupVersion = version;
|
|
263
|
-
}
|
|
264
|
-
let entry = this.#defaultLookupByToken.get(token);
|
|
265
|
-
if (entry === undefined) {
|
|
266
|
-
entry = this.#computeDefaultEntry(token);
|
|
267
|
-
this.#defaultLookupByToken.set(token, entry);
|
|
268
|
-
}
|
|
269
|
-
return entry;
|
|
270
|
-
}
|
|
271
|
-
#computeDefaultEntry(token) {
|
|
272
|
-
let current = token;
|
|
273
|
-
for (let hop = 0; hop < ALIAS_HOP_LIMIT; hop += 1) {
|
|
274
|
-
const entry = this.#findDefaultEntryInChain(current);
|
|
275
|
-
if (entry === null) {
|
|
276
|
-
return null;
|
|
277
|
-
}
|
|
278
|
-
if (entry.binding.kind === "alias") {
|
|
279
|
-
current = entry.binding.target;
|
|
280
|
-
continue;
|
|
281
|
-
}
|
|
282
|
-
return entry;
|
|
283
|
-
}
|
|
284
|
-
return null;
|
|
285
|
-
}
|
|
286
|
-
#lookupNamedEntry(token, name) {
|
|
287
|
-
const version = this.#chainRegistryVersion();
|
|
288
|
-
if (version !== this.#namedLookupVersion) {
|
|
289
|
-
this.#namedLookupByToken.clear();
|
|
290
|
-
this.#namedLookupVersion = version;
|
|
291
|
-
}
|
|
292
|
-
// ✓ TS6.0: Map.getOrInsert (ES2025)
|
|
293
|
-
const entriesByName = this.#namedLookupByToken.getOrInsert(token, new Map());
|
|
294
|
-
let entry = entriesByName.get(name);
|
|
295
|
-
if (entry === undefined) {
|
|
296
|
-
entry = this.#findNamedEntryInChain(token, name);
|
|
297
|
-
entriesByName.set(name, entry);
|
|
298
|
-
}
|
|
299
|
-
return entry;
|
|
300
|
-
}
|
|
301
|
-
#findNamedEntryInChain(token, name) {
|
|
302
|
-
const named = this.#registry.getSimpleNamed(token, name);
|
|
303
|
-
if (named !== undefined) {
|
|
304
|
-
// Predicates need a live context; aliases carry options through the full path.
|
|
305
|
-
if (named.predicate !== undefined || named.kind === "alias") {
|
|
306
|
-
return null;
|
|
307
|
-
}
|
|
308
|
-
return { binding: named, owner: this };
|
|
309
|
-
}
|
|
310
|
-
if (this.#registry.has(token)) {
|
|
311
|
-
return null;
|
|
312
|
-
}
|
|
313
|
-
return this.#parent === undefined ? null : this.#parent.#findNamedEntryInChain(token, name);
|
|
314
|
-
}
|
|
315
|
-
#findDefaultEntryInChain(token) {
|
|
316
|
-
const fast = this.#registry.getFastDefault(token);
|
|
317
|
-
if (fast !== undefined) {
|
|
318
|
-
return { binding: fast, owner: this };
|
|
319
|
-
}
|
|
320
|
-
// A level with non-fast bindings (multi-slot / predicate) needs full selection — bail.
|
|
321
|
-
if (this.#registry.has(token)) {
|
|
322
|
-
return null;
|
|
257
|
+
binding.inFlight = false;
|
|
323
258
|
}
|
|
324
|
-
return this.#parent === undefined ? null : this.#parent.#findDefaultEntryInChain(token);
|
|
325
259
|
}
|
|
326
260
|
#getInstantiationPlan(binding) {
|
|
327
|
-
const registryVersion = this.#
|
|
261
|
+
const registryVersion = this.#lookup.chainVersion();
|
|
328
262
|
const activationVersion = this.#lifecycle.activationVersion;
|
|
329
263
|
if (registryVersion !== this.#classPlanRegistryVersion || activationVersion !== this.#classPlanActivationVersion) {
|
|
330
264
|
this.#classPlanByBindingId.clear();
|
|
@@ -343,141 +277,94 @@ export class DependencyResolver {
|
|
|
343
277
|
this.#classPlanByBindingId.set(binding.id, compiled);
|
|
344
278
|
return compiled;
|
|
345
279
|
}
|
|
346
|
-
// Compiler behind #
|
|
280
|
+
// Compiler behind #getInstantiationPlan — cold path, so the host indirection costs nothing hot.
|
|
347
281
|
#planCompiler = new InstantiationPlanCompiler({
|
|
348
282
|
hasActivationHandlers: (token) => this.#lifecycle.hasActivationHandlers(token),
|
|
349
|
-
knownPostConstruct: (target) => this.#
|
|
350
|
-
needsActiveContainer: (target) =>
|
|
351
|
-
|
|
352
|
-
if (needsActiveContainer === undefined) {
|
|
353
|
-
const accessorMetadata = this.#metadataReader.getAccessorMetadata?.(target);
|
|
354
|
-
needsActiveContainer = (accessorMetadata?.length ?? 0) > 0;
|
|
355
|
-
this.#classNeedsActiveContainer.set(target, needsActiveContainer);
|
|
356
|
-
}
|
|
357
|
-
return needsActiveContainer;
|
|
358
|
-
},
|
|
359
|
-
getConstructorMetadata: (target) => this.#getConstructorMetadata(target),
|
|
283
|
+
knownPostConstruct: (target) => this.#classes.knownPostConstruct(target),
|
|
284
|
+
needsActiveContainer: (target) => this.#classes.needsActiveContainer(target),
|
|
285
|
+
getConstructorMetadata: (target) => this.#classes.constructorMetadata(target),
|
|
360
286
|
lookupDependencyEntry: (token) => {
|
|
361
|
-
const entry = this.#
|
|
362
|
-
return entry === null ? null : { binding: entry.binding
|
|
287
|
+
const entry = this.#lookup.defaultEntry(token);
|
|
288
|
+
return entry === null ? null : { binding: entry.binding };
|
|
289
|
+
},
|
|
290
|
+
getResolutionFrame: (binding) => this.#getResolutionFrame(binding),
|
|
291
|
+
// Dispatches exactly as #resolveDep does, so an escaped dep is indistinguishable
|
|
292
|
+
// from the same dep on a fully interpreted resolve.
|
|
293
|
+
resolveEscaped: (token, options, arity, resolutionPath, resolutionStack) => {
|
|
294
|
+
if (arity === "all") {
|
|
295
|
+
return this.resolveAll(token, options, resolutionPath, resolutionStack);
|
|
296
|
+
}
|
|
297
|
+
if (arity === "optional") {
|
|
298
|
+
return this.resolveOptional(token, options, resolutionPath, resolutionStack);
|
|
299
|
+
}
|
|
300
|
+
if (options === undefined) {
|
|
301
|
+
return this.resolveFromContext(token, resolutionPath, resolutionStack);
|
|
302
|
+
}
|
|
303
|
+
return this.resolve(token, options, resolutionPath, resolutionStack);
|
|
363
304
|
},
|
|
364
|
-
resolveFallback: (token) => this.resolve(token, undefined, [], []),
|
|
365
305
|
});
|
|
366
306
|
resolve(token, options, resolutionPath, resolutionStack) {
|
|
367
307
|
// Name-only fast lane: memoized lookup, dispatching just the shapes whose
|
|
368
308
|
// semantics involve no resolution context (constants, cached singletons).
|
|
369
|
-
if (options !== undefined &&
|
|
370
|
-
options.name
|
|
371
|
-
options.tag === undefined &&
|
|
372
|
-
(options.tags === undefined || options.tags.length === 0)) {
|
|
373
|
-
const namedEntry = this.#lookupNamedEntry(token, options.name);
|
|
309
|
+
if (options !== undefined && isNameOnlyOptions(options)) {
|
|
310
|
+
const namedEntry = this.#lookup.namedEntry(token, options.name);
|
|
374
311
|
if (namedEntry !== null) {
|
|
375
312
|
const namedBinding = namedEntry.binding;
|
|
376
|
-
if (namedBinding
|
|
377
|
-
namedBinding.onActivation === undefined &&
|
|
378
|
-
(this.#lifecycle.activationVersion === 0 || !this.#lifecycle.hasActivationHandlers(namedBinding.token))) {
|
|
313
|
+
if (this.#isPlainConstant(namedBinding)) {
|
|
379
314
|
return namedBinding.value;
|
|
380
315
|
}
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
const cachedSingleton = namedEntry.owner.#scope.peekSingleton(namedBinding.id);
|
|
384
|
-
if (cachedSingleton !== SINGLETON_MISS) {
|
|
385
|
-
return cachedSingleton;
|
|
386
|
-
}
|
|
316
|
+
if (namedBinding.scope === "singleton" && namedBinding.instance !== NO_INSTANCE) {
|
|
317
|
+
return namedBinding.instance;
|
|
387
318
|
}
|
|
388
319
|
// Everything else keeps the full path (context, activation, guards).
|
|
389
320
|
}
|
|
390
321
|
}
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
// Follow aliases iteratively with exact cycle detection — a revisited alias
|
|
395
|
-
// token throws CircularDependencyError instead of overflowing the call stack.
|
|
396
|
-
while (found !== undefined && found.binding.kind === "alias") {
|
|
397
|
-
const target = found.binding.target;
|
|
398
|
-
visitedAliasTokens ??= new Set([currentToken]);
|
|
399
|
-
if (visitedAliasTokens.has(target)) {
|
|
400
|
-
throw new CircularDependencyError([...visitedAliasTokens, target].map((entry) => tokenName(entry)));
|
|
401
|
-
}
|
|
402
|
-
visitedAliasTokens.add(target);
|
|
403
|
-
currentToken = target;
|
|
404
|
-
found = this.#findBinding(currentToken, options, resolutionPath, resolutionStack);
|
|
405
|
-
}
|
|
406
|
-
if (found === undefined) {
|
|
407
|
-
const ownBindings = this.#registry.getAll(currentToken);
|
|
408
|
-
if (ownBindings.length > 0) {
|
|
409
|
-
throw new NoMatchingBindingError(this.#getTokenName(currentToken), options ?? {}, this.#getAvailableSlots(currentToken));
|
|
410
|
-
}
|
|
411
|
-
throw new TokenNotBoundError(this.#getTokenName(currentToken));
|
|
412
|
-
}
|
|
413
|
-
const { binding, owner } = found;
|
|
414
|
-
const scope = binding.scope ?? "transient";
|
|
415
|
-
// Singleton from a parent resolver: delegate so the parent caches it correctly
|
|
416
|
-
if (scope === "singleton" && owner !== this) {
|
|
322
|
+
const { binding, owner } = this.#requireBinding(token, options, resolutionPath, resolutionStack);
|
|
323
|
+
// A singleton owned by a parent resolver is resolved there, so the parent caches it.
|
|
324
|
+
if (binding.scope === "singleton" && owner !== this) {
|
|
417
325
|
return owner.#resolveBinding(binding, options, resolutionPath, resolutionStack);
|
|
418
326
|
}
|
|
419
|
-
// Scoped/transient (or own singleton): resolve with this resolver's container/scope
|
|
420
327
|
return this.#resolveBinding(binding, options, resolutionPath, resolutionStack);
|
|
421
328
|
}
|
|
422
329
|
#resolveBinding(binding, options, resolutionPath, resolutionStack) {
|
|
423
|
-
if (binding
|
|
424
|
-
binding.onActivation === undefined &&
|
|
425
|
-
(this.#lifecycle.activationVersion === 0 || !this.#lifecycle.hasActivationHandlers(binding.token))) {
|
|
330
|
+
if (this.#isPlainConstant(binding)) {
|
|
426
331
|
return binding.value;
|
|
427
332
|
}
|
|
428
|
-
const scope = binding.scope
|
|
429
|
-
// Singleton cache check
|
|
333
|
+
const scope = binding.scope;
|
|
430
334
|
if (scope === "singleton") {
|
|
431
|
-
if (
|
|
432
|
-
return
|
|
335
|
+
if (binding.instance !== NO_INSTANCE) {
|
|
336
|
+
return binding.instance;
|
|
433
337
|
}
|
|
434
338
|
}
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
if (
|
|
438
|
-
|
|
439
|
-
}
|
|
440
|
-
if (this.#scope.hasScoped(binding.id)) {
|
|
441
|
-
return this.#scope.getScoped(binding.id);
|
|
339
|
+
else if (scope === "scoped") {
|
|
340
|
+
const cachedScoped = this.#readScoped(binding);
|
|
341
|
+
if (cachedScoped !== SCOPED_MISS) {
|
|
342
|
+
return cachedScoped;
|
|
442
343
|
}
|
|
443
344
|
}
|
|
444
345
|
const frame = this.#getResolutionFrame(binding);
|
|
445
346
|
const tokenDisplayName = frame.tokenName;
|
|
446
|
-
const resolutionSet = enterResolutionPath(resolutionPath, tokenDisplayName
|
|
347
|
+
const resolutionSet = enterResolutionPath(resolutionPath, tokenDisplayName);
|
|
447
348
|
resolutionStack.push(frame);
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
349
|
+
try {
|
|
350
|
+
const needsActivation = this.#activation.needsActivation(binding);
|
|
351
|
+
if (!needsActivation && scope === "transient" && binding.kind === "dynamic") {
|
|
352
|
+
const resolutionCtx = this.#acquireSyncResolutionContext(resolutionPath, resolutionStack, options);
|
|
452
353
|
const dynamicResult = binding.factory(resolutionCtx);
|
|
453
354
|
if (dynamicResult instanceof Promise) {
|
|
454
|
-
throw new AsyncResolutionError(
|
|
355
|
+
throw new AsyncResolutionError(tokenDisplayName, tokenDisplayName);
|
|
455
356
|
}
|
|
456
|
-
resolutionStack.pop();
|
|
457
|
-
resolutionPath.pop();
|
|
458
|
-
resolutionSet?.delete(tokenDisplayName);
|
|
459
357
|
return dynamicResult;
|
|
460
358
|
}
|
|
461
|
-
|
|
462
|
-
resolutionStack.pop();
|
|
463
|
-
resolutionPath.pop();
|
|
464
|
-
resolutionSet?.delete(tokenDisplayName);
|
|
465
|
-
throw error;
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
try {
|
|
469
|
-
const needsResolutionContext = needsActivation || this.#requiresResolutionContext(binding);
|
|
470
|
-
const resolutionCtx = needsResolutionContext
|
|
359
|
+
const resolutionCtx = needsActivation || requiresResolutionContext(binding)
|
|
471
360
|
? this.#acquireSyncResolutionContext(resolutionPath, resolutionStack, options)
|
|
472
361
|
: undefined;
|
|
473
362
|
const instance = this.#instantiateSync(binding, resolutionCtx, resolutionPath, resolutionStack);
|
|
474
|
-
const
|
|
475
|
-
const activated = shouldActivate
|
|
363
|
+
const activated = this.#activation.refreshAfterFirstInstantiation(binding, needsActivation)
|
|
476
364
|
? this.#lifecycle.runActivationSync(resolutionCtx, binding, instance, this.#metadataReader)
|
|
477
365
|
: instance;
|
|
478
|
-
// Cache by scope
|
|
479
366
|
if (scope === "singleton") {
|
|
480
|
-
this.#scope.setSingleton(binding
|
|
367
|
+
this.#scope.setSingleton(binding, activated);
|
|
481
368
|
}
|
|
482
369
|
else if (scope === "scoped") {
|
|
483
370
|
this.#scope.setScoped(binding.id, activated);
|
|
@@ -507,12 +394,11 @@ export class DependencyResolver {
|
|
|
507
394
|
case "dynamic-async":
|
|
508
395
|
throw new AsyncResolutionError(tokenName(binding.token), tokenName(binding.token));
|
|
509
396
|
case "class": {
|
|
510
|
-
const deps = this.#
|
|
511
|
-
|
|
512
|
-
return instance;
|
|
397
|
+
const deps = this.#resolveDeps(this.#constructorParams(binding.target), resolutionPath, resolutionStack);
|
|
398
|
+
return this.#classes.instantiate(binding.target, deps);
|
|
513
399
|
}
|
|
514
400
|
case "resolved": {
|
|
515
|
-
const deps = this.#
|
|
401
|
+
const deps = this.#resolveDeps(binding.deps, resolutionPath, resolutionStack);
|
|
516
402
|
const factoryResult = binding.factory(...deps);
|
|
517
403
|
if (factoryResult instanceof Promise) {
|
|
518
404
|
throw new AsyncResolutionError(tokenName(binding.token), tokenName(binding.token));
|
|
@@ -525,70 +411,51 @@ export class DependencyResolver {
|
|
|
525
411
|
throw new InternalError("alias should have been followed before instantiation");
|
|
526
412
|
}
|
|
527
413
|
}
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
414
|
+
/**
|
|
415
|
+
* The parameters a class binding injects.
|
|
416
|
+
*
|
|
417
|
+
* @remarks A class the metadata reader knows nothing about is constructible only if it declares
|
|
418
|
+
* no parameters; anything else is a missing `@injectable()`.
|
|
419
|
+
*/
|
|
420
|
+
#constructorParams(target) {
|
|
421
|
+
const meta = this.#classes.constructorMetadata(target);
|
|
422
|
+
if (meta !== undefined) {
|
|
423
|
+
return meta.params;
|
|
424
|
+
}
|
|
425
|
+
if (target.length === 0) {
|
|
426
|
+
return EMPTY_PARAM_LIST;
|
|
535
427
|
}
|
|
536
|
-
|
|
428
|
+
throw new MissingMetadataError(target.name);
|
|
429
|
+
}
|
|
430
|
+
// One dispatch table for both dependency sources — constructor params and `toResolved`
|
|
431
|
+
// descriptors declare the same four things.
|
|
432
|
+
#resolveDeps(deps, resolutionPath, resolutionStack) {
|
|
433
|
+
const count = deps.length;
|
|
434
|
+
if (count === 0) {
|
|
537
435
|
return [];
|
|
538
436
|
}
|
|
539
|
-
if (
|
|
540
|
-
|
|
541
|
-
const paramOptions = injectionSlotToResolveOptions(param);
|
|
542
|
-
if (param.multi) {
|
|
543
|
-
return [this.resolveAll(param.token, paramOptions, resolutionPath, resolutionStack)];
|
|
544
|
-
}
|
|
545
|
-
if (param.optional) {
|
|
546
|
-
return [this.resolveOptional(param.token, paramOptions, resolutionPath, resolutionStack)];
|
|
547
|
-
}
|
|
548
|
-
if (paramOptions === undefined) {
|
|
549
|
-
return [this.resolveFromContext(param.token, resolutionPath, resolutionStack)];
|
|
550
|
-
}
|
|
551
|
-
return [this.resolve(param.token, paramOptions, resolutionPath, resolutionStack)];
|
|
552
|
-
}
|
|
553
|
-
const deps = new Array(meta.params.length);
|
|
554
|
-
for (let index = 0; index < meta.params.length; index += 1) {
|
|
555
|
-
const param = meta.params[index];
|
|
556
|
-
const paramOptions = injectionSlotToResolveOptions(param);
|
|
557
|
-
if (param.multi) {
|
|
558
|
-
deps[index] = this.resolveAll(param.token, paramOptions, resolutionPath, resolutionStack);
|
|
559
|
-
continue;
|
|
560
|
-
}
|
|
561
|
-
if (param.optional) {
|
|
562
|
-
deps[index] = this.resolveOptional(param.token, paramOptions, resolutionPath, resolutionStack);
|
|
563
|
-
continue;
|
|
564
|
-
}
|
|
565
|
-
deps[index] =
|
|
566
|
-
paramOptions === undefined
|
|
567
|
-
? this.resolveFromContext(param.token, resolutionPath, resolutionStack)
|
|
568
|
-
: this.resolve(param.token, paramOptions, resolutionPath, resolutionStack);
|
|
437
|
+
if (count === 1) {
|
|
438
|
+
return [this.#resolveDep(deps[0], resolutionPath, resolutionStack)];
|
|
569
439
|
}
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
const resolved = new Array(deps.length);
|
|
574
|
-
for (let index = 0; index < deps.length; index += 1) {
|
|
575
|
-
const dep = deps[index];
|
|
576
|
-
const depOptions = injectionSlotToResolveOptions(dep);
|
|
577
|
-
if (dep.multi) {
|
|
578
|
-
resolved[index] = this.resolveAll(dep.token, depOptions, resolutionPath, resolutionStack);
|
|
579
|
-
continue;
|
|
580
|
-
}
|
|
581
|
-
if (dep.optional) {
|
|
582
|
-
resolved[index] = this.resolveOptional(dep.token, depOptions, resolutionPath, resolutionStack);
|
|
583
|
-
continue;
|
|
584
|
-
}
|
|
585
|
-
resolved[index] =
|
|
586
|
-
depOptions === undefined
|
|
587
|
-
? this.resolveFromContext(dep.token, resolutionPath, resolutionStack)
|
|
588
|
-
: this.resolve(dep.token, depOptions, resolutionPath, resolutionStack);
|
|
440
|
+
const resolved = new Array(count);
|
|
441
|
+
for (let index = 0; index < count; index += 1) {
|
|
442
|
+
resolved[index] = this.#resolveDep(deps[index], resolutionPath, resolutionStack);
|
|
589
443
|
}
|
|
590
444
|
return resolved;
|
|
591
445
|
}
|
|
446
|
+
#resolveDep(dep, resolutionPath, resolutionStack) {
|
|
447
|
+
const options = injectionSlotToResolveOptions(dep);
|
|
448
|
+
if (dep.multi) {
|
|
449
|
+
return this.resolveAll(dep.token, options, resolutionPath, resolutionStack);
|
|
450
|
+
}
|
|
451
|
+
if (dep.optional) {
|
|
452
|
+
return this.resolveOptional(dep.token, options, resolutionPath, resolutionStack);
|
|
453
|
+
}
|
|
454
|
+
if (options === undefined) {
|
|
455
|
+
return this.resolveFromContext(dep.token, resolutionPath, resolutionStack);
|
|
456
|
+
}
|
|
457
|
+
return this.resolve(dep.token, options, resolutionPath, resolutionStack);
|
|
458
|
+
}
|
|
592
459
|
resolveOptional(token, options, resolutionPath, resolutionStack) {
|
|
593
460
|
if (this.#findBinding(token, options, resolutionPath, resolutionStack) === undefined) {
|
|
594
461
|
return undefined;
|
|
@@ -596,31 +463,32 @@ export class DependencyResolver {
|
|
|
596
463
|
return this.resolve(token, options, resolutionPath, resolutionStack);
|
|
597
464
|
}
|
|
598
465
|
resolveAll(token, options, resolutionPath, resolutionStack) {
|
|
599
|
-
|
|
600
|
-
const namedCandidates = this.#getSimpleNamedBindingsFromChain(token, options.name);
|
|
601
|
-
if (namedCandidates.length === 0) {
|
|
602
|
-
return [];
|
|
603
|
-
}
|
|
604
|
-
const resolved = new Array(namedCandidates.length);
|
|
605
|
-
for (let index = 0; index < namedCandidates.length; index += 1) {
|
|
606
|
-
resolved[index] = this.#resolveCandidateSync(namedCandidates[index], options, resolutionPath, resolutionStack);
|
|
607
|
-
}
|
|
608
|
-
return resolved;
|
|
609
|
-
}
|
|
610
|
-
const allBindings = this.#getAllBindingsFromChain(token);
|
|
611
|
-
if (allBindings.length === 0) {
|
|
612
|
-
return [];
|
|
613
|
-
}
|
|
614
|
-
const ctx = this.#makeConstraintContext(resolutionPath, resolutionStack, options);
|
|
615
|
-
const candidates = selectAllBindings(allBindings, options, ctx);
|
|
466
|
+
const candidates = this.#candidateBindings(token, options, resolutionPath, resolutionStack);
|
|
616
467
|
const resolved = new Array(candidates.length);
|
|
617
468
|
for (let index = 0; index < candidates.length; index += 1) {
|
|
618
469
|
resolved[index] = this.#resolveCandidateSync(candidates[index], options, resolutionPath, resolutionStack);
|
|
619
470
|
}
|
|
620
471
|
return resolved;
|
|
621
472
|
}
|
|
473
|
+
/** Every binding in the chain a `resolveAll` request matches, in chain order. */
|
|
474
|
+
#candidateBindings(token, options, resolutionPath, resolutionStack) {
|
|
475
|
+
if (options !== undefined && isNameOnlyOptions(options)) {
|
|
476
|
+
// The name index has matched the slot already, but a hit may still carry a predicate —
|
|
477
|
+
// and that is the selection path's job to evaluate.
|
|
478
|
+
const named = this.#namedBindingsFromChain(token, options.name);
|
|
479
|
+
if (!anyPredicate(named)) {
|
|
480
|
+
return named;
|
|
481
|
+
}
|
|
482
|
+
return selectAllBindings(named, options, this.#makeConstraintContext(resolutionPath, resolutionStack, options));
|
|
483
|
+
}
|
|
484
|
+
const allBindings = this.#allBindingsFromChain(token);
|
|
485
|
+
if (allBindings.length === 0) {
|
|
486
|
+
return allBindings;
|
|
487
|
+
}
|
|
488
|
+
return selectAllBindings(allBindings, options, this.#makeConstraintContext(resolutionPath, resolutionStack, options));
|
|
489
|
+
}
|
|
622
490
|
// ── Async resolve ──────────────────────────────────────────────────────────
|
|
623
|
-
resolveAsyncFromContext(token, resolutionPath, resolutionStack) {
|
|
491
|
+
resolveAsyncFromContext(token, resolutionPath, resolutionStack, branchDepth) {
|
|
624
492
|
// Hot lane: own-registry fast default (async chains resolve sibling dynamic bindings).
|
|
625
493
|
// Fall back to the chain-versioned memo only on miss or alias.
|
|
626
494
|
const fastBinding = this.#registry.getFastDefault(token);
|
|
@@ -628,174 +496,118 @@ export class DependencyResolver {
|
|
|
628
496
|
// Inline the dominant chain shape — transient dynamic factory with no activation.
|
|
629
497
|
if ((fastBinding.kind === "dynamic-async" || fastBinding.kind === "dynamic") &&
|
|
630
498
|
fastBinding.scope === "transient" &&
|
|
631
|
-
fastBinding
|
|
632
|
-
|
|
633
|
-
return this.#resolveTransientDynamicAsyncFromContext(fastBinding, resolutionPath, resolutionStack);
|
|
499
|
+
!this.#hasAnyActivation(fastBinding)) {
|
|
500
|
+
return this.#resolveTransientDynamicAsyncFromContext(fastBinding, resolutionPath, resolutionStack, branchDepth);
|
|
634
501
|
}
|
|
635
|
-
return this.#resolveAsyncDefaultEntry(fastBinding, this, resolutionPath, resolutionStack);
|
|
502
|
+
return this.#resolveAsyncDefaultEntry(fastBinding, this, resolutionPath, resolutionStack, branchDepth);
|
|
636
503
|
}
|
|
637
|
-
const entry = this.#
|
|
504
|
+
const entry = this.#lookup.defaultEntry(token);
|
|
638
505
|
if (entry === null) {
|
|
639
|
-
return this.resolveAsync(token, undefined, resolutionPath, resolutionStack);
|
|
506
|
+
return this.resolveAsync(token, undefined, resolutionPath, resolutionStack, branchDepth);
|
|
640
507
|
}
|
|
641
|
-
return this.#resolveAsyncDefaultEntry(entry.binding, entry.owner, resolutionPath, resolutionStack);
|
|
508
|
+
return this.#resolveAsyncDefaultEntry(entry.binding, entry.owner, resolutionPath, resolutionStack, branchDepth);
|
|
642
509
|
}
|
|
643
|
-
#resolveAsyncDefaultEntry(binding, owner, resolutionPath, resolutionStack) {
|
|
644
|
-
if (binding
|
|
645
|
-
binding.onActivation === undefined &&
|
|
646
|
-
(this.#lifecycle.activationVersion === 0 || !this.#lifecycle.hasActivationHandlers(binding.token))) {
|
|
510
|
+
#resolveAsyncDefaultEntry(binding, owner, resolutionPath, resolutionStack, branchDepth) {
|
|
511
|
+
if (this.#isPlainConstant(binding)) {
|
|
647
512
|
return Promise.resolve(binding.value);
|
|
648
513
|
}
|
|
649
|
-
const scope = binding.scope
|
|
514
|
+
const scope = binding.scope;
|
|
650
515
|
if (scope === "transient") {
|
|
651
|
-
if ((binding.kind === "dynamic" || binding.kind === "dynamic-async") &&
|
|
652
|
-
binding
|
|
653
|
-
(this.#lifecycle.activationVersion === 0 || !this.#lifecycle.hasActivationHandlers(binding.token))) {
|
|
654
|
-
return this.#resolveTransientDynamicAsyncFromContext(binding, resolutionPath, resolutionStack);
|
|
516
|
+
if ((binding.kind === "dynamic" || binding.kind === "dynamic-async") && !this.#hasAnyActivation(binding)) {
|
|
517
|
+
return this.#resolveTransientDynamicAsyncFromContext(binding, resolutionPath, resolutionStack, branchDepth);
|
|
655
518
|
}
|
|
656
519
|
}
|
|
657
520
|
else if (scope === "singleton") {
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
return Promise.resolve(cachedSingleton);
|
|
521
|
+
if (binding.instance !== NO_INSTANCE) {
|
|
522
|
+
return Promise.resolve(binding.instance);
|
|
661
523
|
}
|
|
662
524
|
if (owner !== this) {
|
|
663
|
-
return owner.#resolveBindingAsync(binding, undefined, resolutionPath, resolutionStack);
|
|
525
|
+
return owner.#resolveBindingAsync(binding, undefined, resolutionPath, resolutionStack, branchDepth);
|
|
664
526
|
}
|
|
665
527
|
}
|
|
666
|
-
else {
|
|
667
|
-
if (!this.#scope.isChild) {
|
|
668
|
-
return Promise.reject(new MissingScopeContextError(this.#getTokenName(binding.token)));
|
|
669
|
-
}
|
|
528
|
+
else if (this.#scope.isChild) {
|
|
670
529
|
if (this.#scope.hasScoped(binding.id)) {
|
|
671
530
|
return Promise.resolve(this.#scope.getScoped(binding.id));
|
|
672
531
|
}
|
|
673
532
|
}
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
const found = this.#findBinding(token, options, resolutionPath, resolutionStack);
|
|
678
|
-
if (found === undefined) {
|
|
679
|
-
const ownBindings = this.#registry.getAll(token);
|
|
680
|
-
if (ownBindings.length > 0) {
|
|
681
|
-
throw new NoMatchingBindingError(this.#getTokenName(token), options ?? {}, this.#getAvailableSlots(token));
|
|
682
|
-
}
|
|
683
|
-
throw new TokenNotBoundError(this.#getTokenName(token));
|
|
684
|
-
}
|
|
685
|
-
let currentToken = token;
|
|
686
|
-
let visitedAliasTokens;
|
|
687
|
-
let aliasFollowed = found;
|
|
688
|
-
while (aliasFollowed !== undefined && aliasFollowed.binding.kind === "alias") {
|
|
689
|
-
const target = aliasFollowed.binding.target;
|
|
690
|
-
visitedAliasTokens ??= new Set([currentToken]);
|
|
691
|
-
if (visitedAliasTokens.has(target)) {
|
|
692
|
-
throw new CircularDependencyError([...visitedAliasTokens, target].map((entry) => tokenName(entry)));
|
|
693
|
-
}
|
|
694
|
-
visitedAliasTokens.add(target);
|
|
695
|
-
currentToken = target;
|
|
696
|
-
aliasFollowed = this.#findBinding(currentToken, options, resolutionPath, resolutionStack);
|
|
697
|
-
}
|
|
698
|
-
if (aliasFollowed === undefined) {
|
|
699
|
-
const ownBindings = this.#registry.getAll(currentToken);
|
|
700
|
-
if (ownBindings.length > 0) {
|
|
701
|
-
throw new NoMatchingBindingError(this.#getTokenName(currentToken), options ?? {}, this.#getAvailableSlots(currentToken));
|
|
702
|
-
}
|
|
703
|
-
throw new TokenNotBoundError(this.#getTokenName(currentToken));
|
|
533
|
+
else {
|
|
534
|
+
// Not `#readScoped`: this entry point reports failure as a rejection, never a sync throw.
|
|
535
|
+
return Promise.reject(new MissingScopeContextError(tokenName(binding.token)));
|
|
704
536
|
}
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
537
|
+
return this.#resolveBindingAsync(binding, undefined, resolutionPath, resolutionStack, branchDepth);
|
|
538
|
+
}
|
|
539
|
+
async resolveAsync(token, options, resolutionPath, resolutionStack, branchDepth = UNOWNED_BRANCH) {
|
|
540
|
+
const { binding, owner } = this.#requireBinding(token, options, resolutionPath, resolutionStack);
|
|
541
|
+
if (binding.scope === "singleton" && owner !== this) {
|
|
542
|
+
return owner.#resolveBindingAsync(binding, options, resolutionPath, resolutionStack, branchDepth);
|
|
709
543
|
}
|
|
710
|
-
return this.#resolveBindingAsync(binding, options, resolutionPath, resolutionStack);
|
|
544
|
+
return this.#resolveBindingAsync(binding, options, resolutionPath, resolutionStack, branchDepth);
|
|
711
545
|
}
|
|
712
|
-
async #resolveBindingAsync(binding, options, resolutionPath, resolutionStack) {
|
|
713
|
-
if (binding
|
|
714
|
-
binding.onActivation === undefined &&
|
|
715
|
-
!this.#lifecycle.hasActivationHandlers(binding.token)) {
|
|
546
|
+
async #resolveBindingAsync(binding, options, resolutionPath, resolutionStack, branchDepth) {
|
|
547
|
+
if (this.#isPlainConstant(binding)) {
|
|
716
548
|
return binding.value;
|
|
717
549
|
}
|
|
718
|
-
const scope = binding.scope
|
|
719
|
-
// Singleton cache
|
|
550
|
+
const scope = binding.scope;
|
|
720
551
|
if (scope === "singleton") {
|
|
721
|
-
if (
|
|
722
|
-
return
|
|
552
|
+
if (binding.instance !== NO_INSTANCE) {
|
|
553
|
+
return binding.instance;
|
|
723
554
|
}
|
|
724
|
-
// In-flight dedup
|
|
555
|
+
// In-flight dedup: concurrent callers share the first creation.
|
|
725
556
|
const inflight = this.#scope.getInflight(binding.id);
|
|
726
557
|
if (inflight !== undefined) {
|
|
727
558
|
return inflight;
|
|
728
559
|
}
|
|
729
560
|
}
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
if (
|
|
733
|
-
|
|
734
|
-
}
|
|
735
|
-
if (this.#scope.hasScoped(binding.id)) {
|
|
736
|
-
return this.#scope.getScoped(binding.id);
|
|
561
|
+
else if (scope === "scoped") {
|
|
562
|
+
const cachedScoped = this.#readScoped(binding);
|
|
563
|
+
if (cachedScoped !== SCOPED_MISS) {
|
|
564
|
+
return cachedScoped;
|
|
737
565
|
}
|
|
738
566
|
}
|
|
739
567
|
const frame = this.#getResolutionFrame(binding);
|
|
740
|
-
|
|
741
|
-
const
|
|
742
|
-
resolutionStack
|
|
743
|
-
const
|
|
568
|
+
// This level appends to its own branch and never unwinds — see ARCHITECTURE.md.
|
|
569
|
+
const levelPath = extendResolutionBranch(resolutionPath, branchDepth, frame.tokenName);
|
|
570
|
+
const levelStack = extendResolutionStackBranch(resolutionStack, branchDepth, frame);
|
|
571
|
+
const levelDepth = branchDepthOf(levelPath);
|
|
572
|
+
const needsActivation = this.#activation.needsActivation(binding);
|
|
744
573
|
if (!needsActivation && scope === "transient" && (binding.kind === "dynamic" || binding.kind === "dynamic-async")) {
|
|
745
|
-
const resolutionCtx = new
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
return await binding.factory(resolutionCtx);
|
|
749
|
-
}
|
|
750
|
-
const dynamicResult = binding.factory(resolutionCtx);
|
|
751
|
-
return dynamicResult instanceof Promise ? await dynamicResult : dynamicResult;
|
|
752
|
-
}
|
|
753
|
-
finally {
|
|
754
|
-
resolutionStack.pop();
|
|
755
|
-
resolutionPath.pop();
|
|
756
|
-
resolutionSet?.delete(frameName);
|
|
574
|
+
const resolutionCtx = new AsyncLevelContext(this, levelPath, levelStack, options);
|
|
575
|
+
if (binding.kind === "dynamic-async") {
|
|
576
|
+
return await binding.factory(resolutionCtx);
|
|
757
577
|
}
|
|
578
|
+
const dynamicResult = binding.factory(resolutionCtx);
|
|
579
|
+
return dynamicResult instanceof Promise ? await dynamicResult : dynamicResult;
|
|
758
580
|
}
|
|
759
|
-
const
|
|
760
|
-
|
|
761
|
-
? new DefaultResolutionContext(this, resolutionPath, resolutionStack, options)
|
|
581
|
+
const resolutionCtx = needsActivation || requiresResolutionContext(binding)
|
|
582
|
+
? new AsyncLevelContext(this, levelPath, levelStack, options)
|
|
762
583
|
: undefined;
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
const singletonPromise = createSingletonPromise().catch((err) => {
|
|
776
|
-
this.#scope.clearInflight(binding.id);
|
|
777
|
-
throw err;
|
|
778
|
-
});
|
|
779
|
-
this.#scope.setInflight(binding.id, singletonPromise);
|
|
780
|
-
return await singletonPromise;
|
|
781
|
-
}
|
|
782
|
-
const instance = await this.#instantiateAsync(binding, resolutionCtx, resolutionPath, resolutionStack);
|
|
783
|
-
const shouldActivate = this.#refreshActivationCacheIfNeeded(binding, needsActivation);
|
|
784
|
-
const activated = shouldActivate
|
|
785
|
-
? await this.#lifecycle.runActivation(resolutionCtx, binding, instance, this.#metadataReader)
|
|
786
|
-
: instance;
|
|
787
|
-
if (scope === "scoped") {
|
|
788
|
-
this.#scope.setScoped(binding.id, activated);
|
|
789
|
-
}
|
|
790
|
-
return activated;
|
|
584
|
+
if (scope === "singleton") {
|
|
585
|
+
// The promise is published before it settles, so concurrent callers dedup onto it.
|
|
586
|
+
const singletonPromise = this.#instantiateAndActivateAsync(binding, resolutionCtx, levelPath, levelStack, levelDepth, needsActivation).then((activated) => {
|
|
587
|
+
this.#scope.setSingleton(binding, activated);
|
|
588
|
+
this.#scope.clearInflight(binding.id);
|
|
589
|
+
return activated;
|
|
590
|
+
}, (error) => {
|
|
591
|
+
this.#scope.clearInflight(binding.id);
|
|
592
|
+
throw error;
|
|
593
|
+
});
|
|
594
|
+
this.#scope.setInflight(binding.id, singletonPromise);
|
|
595
|
+
return await singletonPromise;
|
|
791
596
|
}
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
resolutionSet?.delete(frameName);
|
|
597
|
+
const activated = await this.#instantiateAndActivateAsync(binding, resolutionCtx, levelPath, levelStack, levelDepth, needsActivation);
|
|
598
|
+
if (scope === "scoped") {
|
|
599
|
+
this.#scope.setScoped(binding.id, activated);
|
|
796
600
|
}
|
|
601
|
+
return activated;
|
|
797
602
|
}
|
|
798
|
-
async #
|
|
603
|
+
async #instantiateAndActivateAsync(binding, ctx, resolutionPath, resolutionStack, branchDepth, needsActivation) {
|
|
604
|
+
const instance = await this.#instantiateAsync(binding, ctx, resolutionPath, resolutionStack, branchDepth);
|
|
605
|
+
if (!this.#activation.refreshAfterFirstInstantiation(binding, needsActivation)) {
|
|
606
|
+
return instance;
|
|
607
|
+
}
|
|
608
|
+
return this.#lifecycle.runActivation(ctx, binding, instance, this.#metadataReader);
|
|
609
|
+
}
|
|
610
|
+
async #instantiateAsync(binding, ctx, resolutionPath, resolutionStack, branchDepth) {
|
|
799
611
|
switch (binding.kind) {
|
|
800
612
|
case "constant":
|
|
801
613
|
return binding.value;
|
|
@@ -812,260 +624,151 @@ export class DependencyResolver {
|
|
|
812
624
|
}
|
|
813
625
|
return binding.factory(ctx);
|
|
814
626
|
case "class": {
|
|
815
|
-
const deps = await this.#
|
|
816
|
-
|
|
817
|
-
return instance;
|
|
627
|
+
const deps = await this.#resolveDepsAsync(this.#constructorParams(binding.target), resolutionPath, resolutionStack, branchDepth);
|
|
628
|
+
return this.#classes.instantiate(binding.target, deps);
|
|
818
629
|
}
|
|
819
630
|
case "resolved": {
|
|
820
631
|
if (ctx === undefined) {
|
|
821
632
|
throw new InternalError("resolved binding requires resolution context");
|
|
822
633
|
}
|
|
823
|
-
const deps = await this.#
|
|
634
|
+
const deps = await this.#resolveDepsAsync(binding.deps, resolutionPath, resolutionStack, branchDepth);
|
|
824
635
|
const factoryResult = binding.factory(...deps);
|
|
825
636
|
return factoryResult instanceof Promise ? factoryResult : Promise.resolve(factoryResult);
|
|
826
637
|
}
|
|
827
638
|
case "resolved-async": {
|
|
828
|
-
const deps = await this.#
|
|
639
|
+
const deps = await this.#resolveDepsAsync(binding.deps, resolutionPath, resolutionStack, branchDepth);
|
|
829
640
|
return binding.factory(...deps);
|
|
830
641
|
}
|
|
831
642
|
case "alias":
|
|
832
643
|
throw new InternalError("alias should have been followed before instantiation");
|
|
833
644
|
}
|
|
834
645
|
}
|
|
835
|
-
async #
|
|
836
|
-
const
|
|
837
|
-
if (
|
|
838
|
-
if (target.length === 0) {
|
|
839
|
-
return [];
|
|
840
|
-
}
|
|
841
|
-
throw new MissingMetadataError(target.name);
|
|
842
|
-
}
|
|
843
|
-
if (meta.params.length === 0) {
|
|
646
|
+
async #resolveDepsAsync(deps, resolutionPath, resolutionStack, branchDepth) {
|
|
647
|
+
const count = deps.length;
|
|
648
|
+
if (count === 0) {
|
|
844
649
|
return [];
|
|
845
650
|
}
|
|
846
|
-
if (
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
}
|
|
855
|
-
if (paramOptions === undefined) {
|
|
856
|
-
return [await this.resolveAsyncFromContext(param.token, resolutionPath, resolutionStack)];
|
|
857
|
-
}
|
|
858
|
-
return [await this.resolveAsync(param.token, paramOptions, resolutionPath, resolutionStack)];
|
|
859
|
-
}
|
|
860
|
-
const pending = new Array(meta.params.length);
|
|
861
|
-
const shouldCloneContext = meta.params.length > 1;
|
|
862
|
-
for (let index = 0; index < meta.params.length; index += 1) {
|
|
863
|
-
const param = meta.params[index];
|
|
864
|
-
const paramOptions = injectionSlotToResolveOptions(param);
|
|
865
|
-
if (param.multi) {
|
|
866
|
-
pending[index] = this.resolveAllAsync(param.token, paramOptions, shouldCloneContext ? [...resolutionPath] : resolutionPath, shouldCloneContext ? [...resolutionStack] : resolutionStack);
|
|
867
|
-
}
|
|
868
|
-
else if (param.optional) {
|
|
869
|
-
pending[index] = this.resolveOptionalAsync(param.token, paramOptions, shouldCloneContext ? [...resolutionPath] : resolutionPath, shouldCloneContext ? [...resolutionStack] : resolutionStack);
|
|
870
|
-
}
|
|
871
|
-
else {
|
|
872
|
-
pending[index] =
|
|
873
|
-
paramOptions === undefined
|
|
874
|
-
? this.resolveAsyncFromContext(param.token, shouldCloneContext ? [...resolutionPath] : resolutionPath, shouldCloneContext ? [...resolutionStack] : resolutionStack)
|
|
875
|
-
: this.resolveAsync(param.token, paramOptions, shouldCloneContext ? [...resolutionPath] : resolutionPath, shouldCloneContext ? [...resolutionStack] : resolutionStack);
|
|
876
|
-
}
|
|
651
|
+
if (count === 1) {
|
|
652
|
+
return [await this.#resolveDepAsync(deps[0], resolutionPath, resolutionStack, branchDepth)];
|
|
653
|
+
}
|
|
654
|
+
// Siblings resolve concurrently and each extends the same branch, so the first appends in
|
|
655
|
+
// place and the rest copy the prefix — no caller has to isolate them.
|
|
656
|
+
const pending = new Array(count);
|
|
657
|
+
for (let index = 0; index < count; index += 1) {
|
|
658
|
+
pending[index] = this.#resolveDepAsync(deps[index], resolutionPath, resolutionStack, branchDepth);
|
|
877
659
|
}
|
|
878
660
|
return Promise.all(pending);
|
|
879
661
|
}
|
|
880
|
-
|
|
881
|
-
const
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
const dep = deps[index];
|
|
885
|
-
const depOptions = injectionSlotToResolveOptions(dep);
|
|
886
|
-
if (dep.multi) {
|
|
887
|
-
pending[index] = this.resolveAllAsync(dep.token, depOptions, shouldCloneContext ? [...resolutionPath] : resolutionPath, shouldCloneContext ? [...resolutionStack] : resolutionStack);
|
|
888
|
-
}
|
|
889
|
-
else if (dep.optional) {
|
|
890
|
-
pending[index] = this.resolveOptionalAsync(dep.token, depOptions, shouldCloneContext ? [...resolutionPath] : resolutionPath, shouldCloneContext ? [...resolutionStack] : resolutionStack);
|
|
891
|
-
}
|
|
892
|
-
else {
|
|
893
|
-
pending[index] =
|
|
894
|
-
depOptions === undefined
|
|
895
|
-
? this.resolveAsyncFromContext(dep.token, shouldCloneContext ? [...resolutionPath] : resolutionPath, shouldCloneContext ? [...resolutionStack] : resolutionStack)
|
|
896
|
-
: this.resolveAsync(dep.token, depOptions, shouldCloneContext ? [...resolutionPath] : resolutionPath, shouldCloneContext ? [...resolutionStack] : resolutionStack);
|
|
897
|
-
}
|
|
662
|
+
#resolveDepAsync(dep, resolutionPath, resolutionStack, branchDepth) {
|
|
663
|
+
const options = injectionSlotToResolveOptions(dep);
|
|
664
|
+
if (dep.multi) {
|
|
665
|
+
return this.resolveAllAsync(dep.token, options, resolutionPath, resolutionStack, branchDepth);
|
|
898
666
|
}
|
|
899
|
-
|
|
667
|
+
if (dep.optional) {
|
|
668
|
+
return this.resolveOptionalAsync(dep.token, options, resolutionPath, resolutionStack, branchDepth);
|
|
669
|
+
}
|
|
670
|
+
if (options === undefined) {
|
|
671
|
+
return this.resolveAsyncFromContext(dep.token, resolutionPath, resolutionStack, branchDepth);
|
|
672
|
+
}
|
|
673
|
+
return this.resolveAsync(dep.token, options, resolutionPath, resolutionStack, branchDepth);
|
|
900
674
|
}
|
|
901
|
-
async resolveOptionalAsync(token, options, resolutionPath, resolutionStack) {
|
|
675
|
+
async resolveOptionalAsync(token, options, resolutionPath, resolutionStack, branchDepth = UNOWNED_BRANCH) {
|
|
902
676
|
if (this.#findBinding(token, options, resolutionPath, resolutionStack) === undefined) {
|
|
903
677
|
return undefined;
|
|
904
678
|
}
|
|
905
|
-
return this.resolveAsync(token, options, resolutionPath, resolutionStack);
|
|
679
|
+
return this.resolveAsync(token, options, resolutionPath, resolutionStack, branchDepth);
|
|
906
680
|
}
|
|
907
|
-
async resolveAllAsync(token, options, resolutionPath, resolutionStack) {
|
|
908
|
-
|
|
909
|
-
const namedCandidates = this.#getSimpleNamedBindingsFromChain(token, options.name);
|
|
910
|
-
if (namedCandidates.length === 0) {
|
|
911
|
-
return [];
|
|
912
|
-
}
|
|
913
|
-
const pending = new Array(namedCandidates.length);
|
|
914
|
-
for (let index = 0; index < namedCandidates.length; index += 1) {
|
|
915
|
-
pending[index] = this.#resolveCandidateAsync(namedCandidates[index], options, resolutionPath, resolutionStack);
|
|
916
|
-
}
|
|
917
|
-
return Promise.all(pending);
|
|
918
|
-
}
|
|
919
|
-
const allBindings = this.#getAllBindingsFromChain(token);
|
|
920
|
-
if (allBindings.length === 0) {
|
|
921
|
-
return [];
|
|
922
|
-
}
|
|
923
|
-
const ctx = this.#makeConstraintContext(resolutionPath, resolutionStack, options);
|
|
924
|
-
const candidates = selectAllBindings(allBindings, options, ctx);
|
|
681
|
+
async resolveAllAsync(token, options, resolutionPath, resolutionStack, branchDepth = UNOWNED_BRANCH) {
|
|
682
|
+
const candidates = this.#candidateBindings(token, options, resolutionPath, resolutionStack);
|
|
925
683
|
const pending = new Array(candidates.length);
|
|
926
684
|
for (let index = 0; index < candidates.length; index += 1) {
|
|
927
|
-
pending[index] = this.#resolveCandidateAsync(candidates[index], options, resolutionPath, resolutionStack);
|
|
685
|
+
pending[index] = this.#resolveCandidateAsync(candidates[index], options, resolutionPath, resolutionStack, branchDepth);
|
|
928
686
|
}
|
|
929
687
|
return Promise.all(pending);
|
|
930
688
|
}
|
|
931
689
|
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
932
|
-
#
|
|
690
|
+
#allBindingsFromChain(token) {
|
|
933
691
|
const ownBindings = this.#registry.getAll(token);
|
|
934
692
|
if (this.#parent === undefined) {
|
|
935
693
|
return ownBindings;
|
|
936
694
|
}
|
|
937
695
|
const result = [...ownBindings];
|
|
938
|
-
let current = this.#parent;
|
|
939
|
-
while (current !== undefined) {
|
|
696
|
+
for (let current = this.#parent; current !== undefined; current = current.#parent) {
|
|
940
697
|
const own = current.#registry.getAll(token);
|
|
941
698
|
if (own.length > 0) {
|
|
942
699
|
result.push(...own);
|
|
943
700
|
}
|
|
944
|
-
current = current.#parent;
|
|
945
701
|
}
|
|
946
702
|
return result;
|
|
947
703
|
}
|
|
948
|
-
|
|
704
|
+
/** Every binding the chain's name indexes hold for one name, nearest container first. */
|
|
705
|
+
#namedBindingsFromChain(token, name) {
|
|
706
|
+
// A name resolves to at most one binding per registry, so a root container's answer is built
|
|
707
|
+
// whole rather than grown — the list is sized at its allocation.
|
|
949
708
|
const ownBinding = this.#registry.getSimpleNamed(token, name);
|
|
950
709
|
if (this.#parent === undefined) {
|
|
951
|
-
return ownBinding
|
|
952
|
-
}
|
|
953
|
-
const result = [];
|
|
954
|
-
if (ownBinding !== undefined) {
|
|
955
|
-
result.push(ownBinding);
|
|
710
|
+
return ownBinding === undefined ? [] : [ownBinding];
|
|
956
711
|
}
|
|
957
|
-
|
|
958
|
-
|
|
712
|
+
const result = ownBinding === undefined ? [] : [ownBinding];
|
|
713
|
+
for (let current = this.#parent; current !== undefined; current = current.#parent) {
|
|
959
714
|
const binding = current.#registry.getSimpleNamed(token, name);
|
|
960
715
|
if (binding !== undefined) {
|
|
961
716
|
result.push(binding);
|
|
962
717
|
}
|
|
963
|
-
current = current.#parent;
|
|
964
718
|
}
|
|
965
719
|
return result;
|
|
966
720
|
}
|
|
967
|
-
|
|
968
|
-
|
|
721
|
+
/** A constant with no activation anywhere resolves to its value with no pipeline at all. */
|
|
722
|
+
#isPlainConstant(binding) {
|
|
723
|
+
return (binding.kind === "constant" &&
|
|
724
|
+
binding.onActivation === undefined &&
|
|
725
|
+
(this.#lifecycle.activationVersion === 0 || !this.#lifecycle.hasActivationHandlers(binding.token)));
|
|
726
|
+
}
|
|
727
|
+
/** Whether either an own hook or a container-level hook would run for this binding. */
|
|
728
|
+
#hasAnyActivation(binding) {
|
|
729
|
+
if (binding.onActivation !== undefined) {
|
|
730
|
+
return true;
|
|
731
|
+
}
|
|
732
|
+
return this.#lifecycle.activationVersion !== 0 && this.#lifecycle.hasActivationHandlers(binding.token);
|
|
733
|
+
}
|
|
734
|
+
/**
|
|
735
|
+
* The cached instance of a `scoped` binding, or {@link SCOPED_MISS}.
|
|
736
|
+
*
|
|
737
|
+
* @remarks A `scoped` binding outside a child container is a configuration error, not a miss, so
|
|
738
|
+
* the check lives with the read that depends on it.
|
|
739
|
+
*/
|
|
740
|
+
#readScoped(binding) {
|
|
741
|
+
if (!this.#scope.isChild) {
|
|
742
|
+
throw new MissingScopeContextError(tokenName(binding.token));
|
|
743
|
+
}
|
|
744
|
+
if (this.#scope.hasScoped(binding.id)) {
|
|
745
|
+
return this.#scope.getScoped(binding.id);
|
|
746
|
+
}
|
|
747
|
+
return SCOPED_MISS;
|
|
969
748
|
}
|
|
970
749
|
#makeConstraintContext(resolutionPath, resolutionStack, options) {
|
|
971
750
|
if (options === undefined && resolutionPath.length === 0 && resolutionStack.length === 0) {
|
|
972
751
|
return ROOT_CONSTRAINT_CONTEXT;
|
|
973
752
|
}
|
|
974
|
-
const parent = resolutionStack.at(-1);
|
|
975
|
-
const ancestors = resolutionStack.length > 1 ? resolutionStack.slice(0, -1) : [];
|
|
976
753
|
return {
|
|
977
754
|
resolutionPath,
|
|
978
755
|
resolutionStack,
|
|
979
|
-
parent,
|
|
980
|
-
ancestors,
|
|
756
|
+
parent: resolutionStack.at(-1),
|
|
757
|
+
ancestors: resolutionStack.length > 1 ? resolutionStack.slice(0, -1) : [],
|
|
981
758
|
currentResolveOptions: options,
|
|
982
759
|
};
|
|
983
760
|
}
|
|
984
761
|
#matchesBindingFast(binding, options, resolutionPath, resolutionStack) {
|
|
985
|
-
if (!
|
|
762
|
+
if (!matchesSlot(binding.slot, options)) {
|
|
986
763
|
return false;
|
|
987
764
|
}
|
|
988
765
|
if (binding.predicate === undefined) {
|
|
989
766
|
return true;
|
|
990
767
|
}
|
|
991
|
-
|
|
992
|
-
return binding.predicate(ctx);
|
|
993
|
-
}
|
|
994
|
-
#matchesSlotFast(slot, options) {
|
|
995
|
-
const requestedName = options?.name;
|
|
996
|
-
const requestedTags = options?.tags;
|
|
997
|
-
const singleRequestedTag = options?.tag;
|
|
998
|
-
const hasRequestedTags = (requestedTags?.length ?? 0) > 0 || singleRequestedTag !== undefined;
|
|
999
|
-
if (slot.name !== undefined) {
|
|
1000
|
-
if (requestedName === undefined || slot.name !== requestedName) {
|
|
1001
|
-
return false;
|
|
1002
|
-
}
|
|
1003
|
-
}
|
|
1004
|
-
else if (requestedName !== undefined) {
|
|
1005
|
-
return false;
|
|
1006
|
-
}
|
|
1007
|
-
if (slot.tags.length > 0) {
|
|
1008
|
-
if (!hasRequestedTags) {
|
|
1009
|
-
return false;
|
|
1010
|
-
}
|
|
1011
|
-
for (const [tagKey, tagValue] of slot.tags) {
|
|
1012
|
-
if (!this.#matchesRequestedTag(tagKey, tagValue, requestedTags, singleRequestedTag)) {
|
|
1013
|
-
return false;
|
|
1014
|
-
}
|
|
1015
|
-
}
|
|
1016
|
-
}
|
|
1017
|
-
else if (hasRequestedTags) {
|
|
1018
|
-
return false;
|
|
1019
|
-
}
|
|
1020
|
-
return true;
|
|
1021
|
-
}
|
|
1022
|
-
#getTokenName(token) {
|
|
1023
|
-
return tokenName(token);
|
|
1024
|
-
}
|
|
1025
|
-
#getConstructorMetadata(target) {
|
|
1026
|
-
const cached = this.#classConstructorMetadata.get(target);
|
|
1027
|
-
if (cached !== undefined) {
|
|
1028
|
-
return cached === null ? undefined : cached;
|
|
1029
|
-
}
|
|
1030
|
-
const metadata = this.#metadataReader.getConstructorMetadata(target);
|
|
1031
|
-
this.#classConstructorMetadata.set(target, metadata ?? null);
|
|
1032
|
-
return metadata;
|
|
1033
|
-
}
|
|
1034
|
-
#instantiateClass(target, deps) {
|
|
1035
|
-
let needsActiveContainer = this.#classNeedsActiveContainer.get(target);
|
|
1036
|
-
if (needsActiveContainer === undefined) {
|
|
1037
|
-
const accessorMetadata = this.#metadataReader.getAccessorMetadata?.(target);
|
|
1038
|
-
needsActiveContainer = (accessorMetadata?.length ?? 0) > 0;
|
|
1039
|
-
this.#classNeedsActiveContainer.set(target, needsActiveContainer);
|
|
1040
|
-
}
|
|
1041
|
-
const invokable = target;
|
|
1042
|
-
if (!needsActiveContainer) {
|
|
1043
|
-
return new invokable(...deps);
|
|
1044
|
-
}
|
|
1045
|
-
return runWithContainer(this.#container, () => new invokable(...deps));
|
|
1046
|
-
}
|
|
1047
|
-
#matchesRequestedTag(tagKey, tagValue, requestedTags, singleRequestedTag) {
|
|
1048
|
-
if (singleRequestedTag !== undefined &&
|
|
1049
|
-
singleRequestedTag[0] === tagKey &&
|
|
1050
|
-
Object.is(singleRequestedTag[1], tagValue)) {
|
|
1051
|
-
return true;
|
|
1052
|
-
}
|
|
1053
|
-
if (requestedTags === undefined || requestedTags.length === 0) {
|
|
1054
|
-
return false;
|
|
1055
|
-
}
|
|
1056
|
-
for (let index = 0; index < requestedTags.length; index += 1) {
|
|
1057
|
-
const requestedTag = requestedTags[index];
|
|
1058
|
-
if (requestedTag[0] === tagKey && Object.is(requestedTag[1], tagValue)) {
|
|
1059
|
-
return true;
|
|
1060
|
-
}
|
|
1061
|
-
}
|
|
1062
|
-
return false;
|
|
768
|
+
return binding.predicate(this.#makeConstraintContext(resolutionPath, resolutionStack, options));
|
|
1063
769
|
}
|
|
1064
770
|
#resolveTransientDynamicSyncFromContext(binding, resolutionPath, resolutionStack) {
|
|
1065
|
-
// One lane at every depth.
|
|
1066
|
-
// an O(depth) `resolutionPath.includes()` scan, which had to be escaped past ~32 levels; with
|
|
1067
|
-
// the O(1) `binding.inFlight` mark there is nothing to escape, so the depth split — and the
|
|
1068
|
-
// divergent behaviour it caused — is gone.
|
|
771
|
+
// One lane at every depth: `binding.inFlight` is O(1), so there is nothing to escape.
|
|
1069
772
|
const frame = this.#getResolutionFrame(binding);
|
|
1070
773
|
const tokenDisplayName = frame.tokenName;
|
|
1071
774
|
if (binding.inFlight) {
|
|
@@ -1088,150 +791,122 @@ export class DependencyResolver {
|
|
|
1088
791
|
binding.inFlight = false;
|
|
1089
792
|
}
|
|
1090
793
|
}
|
|
1091
|
-
//
|
|
1092
|
-
|
|
1093
|
-
// identical to a try/finally but without the async machinery overhead.
|
|
1094
|
-
#resolveTransientDynamicAsyncFromContext(binding, resolutionPath, resolutionStack) {
|
|
1095
|
-
// One lane at every depth. Cycle detection goes through `enterResolutionPath`, which is
|
|
1096
|
-
// path-scoped — the only mechanism that stays correct when chains interleave (Promise.all) —
|
|
1097
|
-
// and adapts on its own: a linear scan while the path is short, an attached Set past
|
|
1098
|
-
// RESOLUTION_SET_THRESHOLD. That removes the depth split, and with it a silent change of
|
|
1099
|
-
// behaviour (context identity, stack frames, promise shape) at the old threshold.
|
|
1100
|
-
//
|
|
1101
|
-
// For a sequential chain every level shares one resolutionPath/resolutionStack, so a single
|
|
1102
|
-
// DefaultResolutionContext serves the whole chain: inner levels of the owning chain allocate
|
|
1103
|
-
// nothing. A concurrent chain is detected by path identity and gets its own context.
|
|
794
|
+
// Deliberately not `async`: that would allocate a state machine and a promise per level.
|
|
795
|
+
#resolveTransientDynamicAsyncFromContext(binding, resolutionPath, resolutionStack, branchDepth) {
|
|
1104
796
|
const frame = this.#getResolutionFrame(binding);
|
|
1105
|
-
|
|
797
|
+
let levelPath;
|
|
1106
798
|
try {
|
|
1107
|
-
|
|
799
|
+
levelPath = extendResolutionBranch(resolutionPath, branchDepth, frame.tokenName);
|
|
1108
800
|
}
|
|
1109
801
|
catch (cycleError) {
|
|
1110
802
|
// This method is not `async`; keep failures as rejections rather than sync throws.
|
|
1111
803
|
return Promise.reject(cycleError);
|
|
1112
804
|
}
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
else if (this.#asyncChainCtxPath === undefined) {
|
|
1120
|
-
const existing = this.#asyncChainCtx;
|
|
1121
|
-
if (existing === undefined) {
|
|
1122
|
-
ctx = new DefaultResolutionContext(this, resolutionPath, resolutionStack, undefined);
|
|
1123
|
-
this.#asyncChainCtx = ctx;
|
|
1124
|
-
}
|
|
1125
|
-
else {
|
|
1126
|
-
existing.reset(this, resolutionPath, resolutionStack, undefined);
|
|
1127
|
-
ctx = existing;
|
|
805
|
+
const levelStack = extendResolutionStackBranch(resolutionStack, branchDepth, frame);
|
|
806
|
+
// Nothing this level appended is ever removed, so no level observes its own settlement.
|
|
807
|
+
const ctx = new AsyncLevelContext(this, levelPath, levelStack, undefined);
|
|
808
|
+
try {
|
|
809
|
+
if (binding.kind === "dynamic-async") {
|
|
810
|
+
return binding.factory(ctx);
|
|
1128
811
|
}
|
|
1129
|
-
|
|
1130
|
-
|
|
812
|
+
const factoryResult = binding.factory(ctx);
|
|
813
|
+
return factoryResult instanceof Promise ? factoryResult : Promise.resolve(factoryResult);
|
|
1131
814
|
}
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
815
|
+
catch (factoryError) {
|
|
816
|
+
return Promise.reject(factoryError);
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
// ── The cascade lane ───────────────────────────────────────────────────────
|
|
820
|
+
/**
|
|
821
|
+
* Entry for a request a factory makes from inside an open synchronous cascade.
|
|
822
|
+
*
|
|
823
|
+
* @remarks A request arriving with no cascade open came out of a continuation, so its ancestors
|
|
824
|
+
* are on no call stack — it escapes to the branch lane. See `ARCHITECTURE.md`.
|
|
825
|
+
*/
|
|
826
|
+
resolveAsyncFromCascade(token) {
|
|
827
|
+
if (this.#cascadePath.length === 0) {
|
|
828
|
+
return this.resolveAsyncFromContext(token, [], [], ROOT_BRANCH);
|
|
829
|
+
}
|
|
830
|
+
return this.#dispatchCascade(token);
|
|
831
|
+
}
|
|
832
|
+
/** Entry for a resolve the container starts, which opens the cascade rather than joining one. */
|
|
833
|
+
resolveAsyncFromRoot(token) {
|
|
834
|
+
return this.#dispatchCascade(token);
|
|
835
|
+
}
|
|
836
|
+
#dispatchCascade(token) {
|
|
837
|
+
const fastBinding = this.#registry.getFastDefault(token);
|
|
838
|
+
if (fastBinding !== undefined) {
|
|
839
|
+
if ((fastBinding.kind === "dynamic-async" || fastBinding.kind === "dynamic") &&
|
|
840
|
+
fastBinding.scope === "transient" &&
|
|
841
|
+
!this.#hasAnyActivation(fastBinding)) {
|
|
842
|
+
return this.#resolveTransientDynamicAsyncCascade(fastBinding);
|
|
843
|
+
}
|
|
844
|
+
// A value that already exists answers here: escaping would snapshot the cascade for a resolve
|
|
845
|
+
// that never looks at a path.
|
|
846
|
+
if (this.#isPlainConstant(fastBinding)) {
|
|
847
|
+
return Promise.resolve(fastBinding.value);
|
|
848
|
+
}
|
|
849
|
+
if (fastBinding.scope === "singleton" && fastBinding.instance !== NO_INSTANCE) {
|
|
850
|
+
return Promise.resolve(fastBinding.instance);
|
|
851
|
+
}
|
|
1135
852
|
}
|
|
1136
|
-
|
|
1137
|
-
|
|
853
|
+
// Anything else leaves the cascade lane for good, seeded with a snapshot of the ancestors it
|
|
854
|
+
// accumulated — so a cycle across the boundary is still on one path.
|
|
855
|
+
return this.resolveAsyncFromContext(token, [...this.#cascadePath], [...this.#cascadeStack], UNOWNED_BRANCH);
|
|
856
|
+
}
|
|
857
|
+
#resolveTransientDynamicAsyncCascade(binding) {
|
|
858
|
+
const frame = this.#getResolutionFrame(binding);
|
|
859
|
+
// The request that closes a cycle is made from a factory's synchronous prefix, and synchronous
|
|
860
|
+
// code does not interleave — so the O(1) flag is exact path membership here, as it is for the
|
|
861
|
+
// sync lane. It is cleared when the factory returns its promise, not when that promise settles.
|
|
862
|
+
if (binding.inFlight) {
|
|
863
|
+
return Promise.reject(new CircularDependencyError([...this.#cascadePath, frame.tokenName]));
|
|
1138
864
|
}
|
|
1139
|
-
|
|
1140
|
-
|
|
865
|
+
const ctx = (this.#cascadeContext ??= new AsyncCascadeContext(this, this.#cascadePath, this.#cascadeStack));
|
|
866
|
+
binding.inFlight = true;
|
|
867
|
+
this.#cascadePath.push(frame.tokenName);
|
|
868
|
+
this.#cascadeStack.push(frame);
|
|
1141
869
|
try {
|
|
1142
870
|
if (binding.kind === "dynamic-async") {
|
|
1143
|
-
|
|
1144
|
-
}
|
|
1145
|
-
else {
|
|
1146
|
-
const factoryResult = binding.factory(ctx);
|
|
1147
|
-
factoryPromise =
|
|
1148
|
-
factoryResult instanceof Promise ? factoryResult : Promise.resolve(factoryResult);
|
|
871
|
+
return binding.factory(ctx);
|
|
1149
872
|
}
|
|
873
|
+
const factoryResult = binding.factory(ctx);
|
|
874
|
+
return factoryResult instanceof Promise ? factoryResult : Promise.resolve(factoryResult);
|
|
1150
875
|
}
|
|
1151
876
|
catch (factoryError) {
|
|
1152
|
-
// Synchronous throw from the factory (rare) — clean up immediately.
|
|
1153
|
-
exitResolutionPath(resolutionPath);
|
|
1154
|
-
if (isOwnerLevel && --this.#asyncChainActiveLevels === 0) {
|
|
1155
|
-
this.#asyncChainCtxPath = undefined;
|
|
1156
|
-
this.#asyncChainSettle = undefined;
|
|
1157
|
-
}
|
|
1158
877
|
return Promise.reject(factoryError);
|
|
1159
878
|
}
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
// failing resolveAsync no longer surfaces as an unhandledRejection — callers are expected to
|
|
1165
|
-
// await (or .catch) the returned promise.
|
|
1166
|
-
//
|
|
1167
|
-
// Every level of the owning chain unwinds identically, so one closure serves them all.
|
|
1168
|
-
let settle;
|
|
1169
|
-
if (isOwnerLevel) {
|
|
1170
|
-
settle =
|
|
1171
|
-
this.#asyncChainSettle ??
|
|
1172
|
-
(this.#asyncChainSettle = () => {
|
|
1173
|
-
exitResolutionPath(resolutionPath);
|
|
1174
|
-
if (--this.#asyncChainActiveLevels === 0) {
|
|
1175
|
-
this.#asyncChainCtxPath = undefined;
|
|
1176
|
-
this.#asyncChainSettle = undefined;
|
|
1177
|
-
}
|
|
1178
|
-
});
|
|
1179
|
-
}
|
|
1180
|
-
else {
|
|
1181
|
-
settle = () => {
|
|
1182
|
-
exitResolutionPath(resolutionPath);
|
|
1183
|
-
};
|
|
879
|
+
finally {
|
|
880
|
+
this.#cascadeStack.pop();
|
|
881
|
+
this.#cascadePath.pop();
|
|
882
|
+
binding.inFlight = false;
|
|
1184
883
|
}
|
|
1185
|
-
factoryPromise.then(settle, settle);
|
|
1186
|
-
return factoryPromise;
|
|
1187
884
|
}
|
|
885
|
+
// A cached candidate answers here rather than re-entering the generic path: `resolveAll` pays
|
|
886
|
+
// this per candidate, and a fan-out over cached handlers is the shape that makes it matter.
|
|
1188
887
|
#resolveCandidateSync(binding, options, resolutionPath, resolutionStack) {
|
|
1189
|
-
if (binding
|
|
1190
|
-
binding.onActivation === undefined &&
|
|
1191
|
-
!this.#lifecycle.hasActivationHandlers(binding.token)) {
|
|
888
|
+
if (this.#isPlainConstant(binding)) {
|
|
1192
889
|
return binding.value;
|
|
1193
890
|
}
|
|
1194
891
|
if (binding.kind === "alias") {
|
|
1195
892
|
return this.resolve(binding.target, options, resolutionPath, resolutionStack);
|
|
1196
893
|
}
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
return this.#scope.getSingleton(binding.id);
|
|
1200
|
-
}
|
|
1201
|
-
if (scope === "scoped") {
|
|
1202
|
-
if (!this.#scope.isChild) {
|
|
1203
|
-
throw new MissingScopeContextError(this.#getTokenName(binding.token));
|
|
1204
|
-
}
|
|
1205
|
-
if (this.#scope.hasScoped(binding.id)) {
|
|
1206
|
-
return this.#scope.getScoped(binding.id);
|
|
1207
|
-
}
|
|
894
|
+
if (binding.scope === "singleton" && binding.instance !== NO_INSTANCE) {
|
|
895
|
+
return binding.instance;
|
|
1208
896
|
}
|
|
1209
897
|
return this.#resolveBinding(binding, options, resolutionPath, resolutionStack);
|
|
1210
898
|
}
|
|
1211
|
-
#resolveCandidateAsync(binding, options, resolutionPath, resolutionStack) {
|
|
1212
|
-
if (binding
|
|
1213
|
-
binding.onActivation === undefined &&
|
|
1214
|
-
!this.#lifecycle.hasActivationHandlers(binding.token)) {
|
|
899
|
+
#resolveCandidateAsync(binding, options, resolutionPath, resolutionStack, branchDepth) {
|
|
900
|
+
if (this.#isPlainConstant(binding)) {
|
|
1215
901
|
return Promise.resolve(binding.value);
|
|
1216
902
|
}
|
|
1217
|
-
const isolatedPath = [...resolutionPath];
|
|
1218
|
-
const isolatedStack = [...resolutionStack];
|
|
1219
903
|
if (binding.kind === "alias") {
|
|
1220
|
-
return this.resolveAsync(binding.target, options,
|
|
904
|
+
return this.resolveAsync(binding.target, options, resolutionPath, resolutionStack, branchDepth);
|
|
1221
905
|
}
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
return Promise.resolve(this.#scope.getSingleton(binding.id));
|
|
906
|
+
if (binding.scope === "singleton" && binding.instance !== NO_INSTANCE) {
|
|
907
|
+
return Promise.resolve(binding.instance);
|
|
1225
908
|
}
|
|
1226
|
-
|
|
1227
|
-
if (!this.#scope.isChild) {
|
|
1228
|
-
return Promise.reject(new MissingScopeContextError(this.#getTokenName(binding.token)));
|
|
1229
|
-
}
|
|
1230
|
-
if (this.#scope.hasScoped(binding.id)) {
|
|
1231
|
-
return Promise.resolve(this.#scope.getScoped(binding.id));
|
|
1232
|
-
}
|
|
1233
|
-
}
|
|
1234
|
-
return this.#resolveBindingAsync(binding, options, isolatedPath, isolatedStack);
|
|
909
|
+
return this.#resolveBindingAsync(binding, options, resolutionPath, resolutionStack, branchDepth);
|
|
1235
910
|
}
|
|
1236
911
|
#getResolutionFrame(binding) {
|
|
1237
912
|
// Memoized on the binding rather than in a per-resolver Map: the frame derives only from
|
|
@@ -1241,70 +916,10 @@ export class DependencyResolver {
|
|
|
1241
916
|
if (existing !== undefined) {
|
|
1242
917
|
return existing;
|
|
1243
918
|
}
|
|
1244
|
-
const
|
|
1245
|
-
const frame = buildResolutionFrame(tokenName(binding.token), scope, binding.id, binding.kind, binding.slot);
|
|
919
|
+
const frame = buildResolutionFrame(tokenName(binding.token), binding.scope, binding.id, binding.kind, binding.slot);
|
|
1246
920
|
binding.frame = frame;
|
|
1247
921
|
return frame;
|
|
1248
922
|
}
|
|
1249
|
-
#needsActivation(binding) {
|
|
1250
|
-
const lifecycleVersion = this.#lifecycle.activationVersion;
|
|
1251
|
-
if (lifecycleVersion === 0 &&
|
|
1252
|
-
binding.kind !== "class" &&
|
|
1253
|
-
binding.kind !== "alias" &&
|
|
1254
|
-
binding.onActivation === undefined) {
|
|
1255
|
-
return false;
|
|
1256
|
-
}
|
|
1257
|
-
if (this.#activationCacheVersion !== lifecycleVersion) {
|
|
1258
|
-
this.#activationNeedByBindingId.clear();
|
|
1259
|
-
this.#activationCacheVersion = lifecycleVersion;
|
|
1260
|
-
}
|
|
1261
|
-
const cached = this.#activationNeedByBindingId.get(binding.id);
|
|
1262
|
-
if (cached !== undefined) {
|
|
1263
|
-
return cached;
|
|
1264
|
-
}
|
|
1265
|
-
if (binding.kind === "class") {
|
|
1266
|
-
let hasActivation = this.#lifecycle.hasActivationHandlers(binding.token) || binding.onActivation !== undefined;
|
|
1267
|
-
const cachedPostConstruct = this.#classHasPostConstruct.get(binding.target);
|
|
1268
|
-
// Unknown class lifecycle metadata: activate once, then cache after first instantiation.
|
|
1269
|
-
if (cachedPostConstruct === undefined) {
|
|
1270
|
-
hasActivation = true;
|
|
1271
|
-
}
|
|
1272
|
-
else if (cachedPostConstruct) {
|
|
1273
|
-
hasActivation = true;
|
|
1274
|
-
}
|
|
1275
|
-
this.#activationNeedByBindingId.set(binding.id, hasActivation);
|
|
1276
|
-
return hasActivation;
|
|
1277
|
-
}
|
|
1278
|
-
let hasActivation = false;
|
|
1279
|
-
if (binding.kind !== "alias" && binding.onActivation !== undefined) {
|
|
1280
|
-
hasActivation = true;
|
|
1281
|
-
}
|
|
1282
|
-
else if (this.#lifecycle.hasActivationHandlers(binding.token)) {
|
|
1283
|
-
hasActivation = true;
|
|
1284
|
-
}
|
|
1285
|
-
this.#activationNeedByBindingId.set(binding.id, hasActivation);
|
|
1286
|
-
return hasActivation;
|
|
1287
|
-
}
|
|
1288
|
-
#refreshClassPostConstructCache(target) {
|
|
1289
|
-
const lifecycle = this.#metadataReader.getLifecycleMetadata(target);
|
|
1290
|
-
const hasPostConstruct = lifecycle !== undefined && lifecycle.postConstruct !== undefined && lifecycle.postConstruct.length > 0;
|
|
1291
|
-
this.#classHasPostConstruct.set(target, hasPostConstruct);
|
|
1292
|
-
}
|
|
1293
|
-
/**
|
|
1294
|
-
* Refreshes the post-construct cache for class bindings on first instantiation and
|
|
1295
|
-
* returns the (possibly updated) shouldActivate flag.
|
|
1296
|
-
*/
|
|
1297
|
-
#refreshActivationCacheIfNeeded(binding, needsActivation) {
|
|
1298
|
-
if (binding.kind === "class" && this.#classHasPostConstruct.get(binding.target) === undefined) {
|
|
1299
|
-
this.#refreshClassPostConstructCache(binding.target);
|
|
1300
|
-
this.#activationNeedByBindingId.delete(binding.id);
|
|
1301
|
-
return this.#needsActivation(binding);
|
|
1302
|
-
}
|
|
1303
|
-
return needsActivation;
|
|
1304
|
-
}
|
|
1305
|
-
#requiresResolutionContext(binding) {
|
|
1306
|
-
return binding.kind === "dynamic" || binding.kind === "dynamic-async";
|
|
1307
|
-
}
|
|
1308
923
|
#acquireSyncResolutionContext(resolutionPath, resolutionStack, options) {
|
|
1309
924
|
const depth = resolutionStack.length;
|
|
1310
925
|
const existing = this.#syncResolutionContextPool[depth];
|
|
@@ -1317,4 +932,30 @@ export class DependencyResolver {
|
|
|
1317
932
|
return created;
|
|
1318
933
|
}
|
|
1319
934
|
}
|
|
935
|
+
/** Absent scoped entry — distinguishes it from a cached `undefined`. */
|
|
936
|
+
const SCOPED_MISS = Symbol("di:scoped-miss");
|
|
937
|
+
function anyPredicate(bindings) {
|
|
938
|
+
for (let index = 0; index < bindings.length; index += 1) {
|
|
939
|
+
if (bindings[index].predicate !== undefined) {
|
|
940
|
+
return true;
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
return false;
|
|
944
|
+
}
|
|
945
|
+
/** Only a factory is handed the resolution context; everything else gets its deps directly. */
|
|
946
|
+
function requiresResolutionContext(binding) {
|
|
947
|
+
return binding.kind === "dynamic" || binding.kind === "dynamic-async";
|
|
948
|
+
}
|
|
949
|
+
/**
|
|
950
|
+
* Whether the tag index's answer is the one `Object.is` would give.
|
|
951
|
+
*
|
|
952
|
+
* @remarks An indexed binding has no name, no predicate and exactly one tag, and the request carries
|
|
953
|
+
* only that tag, so `matchesSlot` reduces to the tag values — and the index matched the key already.
|
|
954
|
+
* It answers by SameValueZero, which parts from `Object.is` (SPEC §3.5) on exactly one pair: `+0` and
|
|
955
|
+
* `-0`. So a request whose value is not zero is already exact, and only a zero-valued one is worth
|
|
956
|
+
* reading the stored value for.
|
|
957
|
+
*/
|
|
958
|
+
function matchesIndexedTagValue(binding, requestedValue) {
|
|
959
|
+
return requestedValue !== 0 || Object.is(binding.slot.tags[0][1], requestedValue);
|
|
960
|
+
}
|
|
1320
961
|
//# sourceMappingURL=resolver.js.map
|