@codefast/di 0.7.1 → 0.8.1
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 +40 -0
- package/README.md +242 -159
- package/dist/ambient/active-container.d.ts +1 -2
- package/dist/ambient/active-container.js +1 -2
- package/dist/container/binding-builders.d.ts +1 -2
- package/dist/container/binding-builders.js +5 -7
- package/dist/container/container.d.ts +1 -2
- package/dist/container/container.js +4 -6
- package/dist/core/binding-scope.d.ts +1 -2
- package/dist/core/binding-scope.js +1 -2
- package/dist/core/binding.d.ts +16 -4
- package/dist/core/binding.js +30 -6
- package/dist/core/constraint-requirement.d.ts +1 -2
- package/dist/core/constraint-requirement.js +1 -2
- package/dist/core/constructor-type.d.ts +1 -2
- package/dist/core/constructor-type.js +1 -2
- package/dist/core/map-upsert.d.ts +1 -2
- package/dist/core/map-upsert.js +1 -2
- package/dist/core/module.d.ts +1 -2
- package/dist/core/module.js +1 -2
- package/dist/core/registry.d.ts +1 -5
- package/dist/core/registry.js +15 -63
- package/dist/core/tag.d.ts +27 -2
- package/dist/core/tag.js +55 -13
- package/dist/core/token.d.ts +1 -2
- package/dist/core/token.js +1 -2
- package/dist/core/types.d.ts +1 -2
- package/dist/core/types.js +1 -2
- package/dist/decorators/inject.d.ts +1 -2
- package/dist/decorators/inject.js +1 -2
- package/dist/decorators/injectable.d.ts +1 -2
- package/dist/decorators/injectable.js +1 -2
- package/dist/decorators/lifecycle-decorators.d.ts +1 -2
- package/dist/decorators/lifecycle-decorators.js +1 -2
- package/dist/errors/diagnostics.d.ts +1 -2
- package/dist/errors/diagnostics.js +1 -2
- package/dist/errors/errors.d.ts +1 -2
- package/dist/errors/errors.js +29 -5
- package/dist/index.d.ts +2 -3
- package/dist/index.js +2 -3
- package/dist/injection/descriptor.d.ts +1 -2
- package/dist/injection/descriptor.js +1 -2
- package/dist/injection/resolve-options.d.ts +18 -14
- package/dist/injection/resolve-options.js +70 -17
- package/dist/introspection/dependency-graph.d.ts +1 -2
- package/dist/introspection/dependency-graph.js +23 -11
- package/dist/introspection/graph-adapters/cytoscape.d.ts +1 -2
- package/dist/introspection/graph-adapters/cytoscape.js +1 -2
- package/dist/introspection/graph-adapters/dot.d.ts +1 -2
- package/dist/introspection/graph-adapters/dot.js +1 -2
- package/dist/introspection/graph-adapters/mermaid.d.ts +1 -2
- package/dist/introspection/graph-adapters/mermaid.js +1 -2
- package/dist/introspection/graph-adapters/reactflow.d.ts +1 -2
- package/dist/introspection/graph-adapters/reactflow.js +1 -2
- package/dist/introspection/inspector.d.ts +1 -2
- package/dist/introspection/inspector.js +1 -2
- package/dist/lifecycle/lifecycle-manager.d.ts +1 -2
- package/dist/lifecycle/lifecycle-manager.js +1 -2
- package/dist/lifecycle/scope-manager.d.ts +1 -2
- package/dist/lifecycle/scope-manager.js +1 -2
- package/dist/metadata/metadata-keys.d.ts +1 -2
- package/dist/metadata/metadata-keys.js +1 -2
- package/dist/metadata/metadata-reader-token.d.ts +1 -2
- package/dist/metadata/metadata-reader-token.js +1 -2
- package/dist/metadata/metadata-types.d.ts +1 -2
- package/dist/metadata/metadata-types.js +1 -2
- package/dist/metadata/symbol-metadata-reader.d.ts +1 -2
- package/dist/metadata/symbol-metadata-reader.js +1 -2
- package/dist/metadata/verifying-metadata-reader.d.ts +1 -2
- package/dist/metadata/verifying-metadata-reader.js +1 -2
- package/dist/resolution/cache/activation-need.d.ts +1 -2
- package/dist/resolution/cache/activation-need.js +1 -2
- package/dist/resolution/cache/binding-lookup-cache.d.ts +2 -5
- package/dist/resolution/cache/binding-lookup-cache.js +2 -40
- package/dist/resolution/cache/class-introspector.d.ts +1 -2
- package/dist/resolution/cache/class-introspector.js +1 -2
- package/dist/resolution/context.d.ts +1 -2
- package/dist/resolution/context.js +1 -2
- package/dist/resolution/path/resolution-path.d.ts +1 -2
- package/dist/resolution/path/resolution-path.js +1 -2
- package/dist/resolution/plan/instantiation-plan.d.ts +5 -14
- package/dist/resolution/plan/instantiation-plan.js +10 -29
- package/dist/resolution/resolver.d.ts +13 -7
- package/dist/resolution/resolver.js +107 -143
- package/dist/resolution/select/binding-select.d.ts +5 -7
- package/dist/resolution/select/binding-select.js +19 -22
- package/dist/resolution/select/constraints.d.ts +1 -2
- package/dist/resolution/select/constraints.js +1 -2
- package/package.json +6 -58
- package/dist/ambient/active-container.d.ts.map +0 -1
- package/dist/ambient/active-container.js.map +0 -1
- package/dist/container/binding-builders.d.ts.map +0 -1
- package/dist/container/binding-builders.js.map +0 -1
- package/dist/container/container.d.ts.map +0 -1
- package/dist/container/container.js.map +0 -1
- package/dist/core/binding-scope.d.ts.map +0 -1
- package/dist/core/binding-scope.js.map +0 -1
- package/dist/core/binding.d.ts.map +0 -1
- package/dist/core/binding.js.map +0 -1
- package/dist/core/constraint-requirement.d.ts.map +0 -1
- package/dist/core/constraint-requirement.js.map +0 -1
- package/dist/core/constructor-type.d.ts.map +0 -1
- package/dist/core/constructor-type.js.map +0 -1
- package/dist/core/map-upsert.d.ts.map +0 -1
- package/dist/core/map-upsert.js.map +0 -1
- package/dist/core/module.d.ts.map +0 -1
- package/dist/core/module.js.map +0 -1
- package/dist/core/registry.d.ts.map +0 -1
- package/dist/core/registry.js.map +0 -1
- package/dist/core/tag.d.ts.map +0 -1
- package/dist/core/tag.js.map +0 -1
- package/dist/core/token.d.ts.map +0 -1
- package/dist/core/token.js.map +0 -1
- package/dist/core/types.d.ts.map +0 -1
- package/dist/core/types.js.map +0 -1
- package/dist/decorators/inject.d.ts.map +0 -1
- package/dist/decorators/inject.js.map +0 -1
- package/dist/decorators/injectable.d.ts.map +0 -1
- package/dist/decorators/injectable.js.map +0 -1
- package/dist/decorators/lifecycle-decorators.d.ts.map +0 -1
- package/dist/decorators/lifecycle-decorators.js.map +0 -1
- package/dist/errors/diagnostics.d.ts.map +0 -1
- package/dist/errors/diagnostics.js.map +0 -1
- package/dist/errors/errors.d.ts.map +0 -1
- package/dist/errors/errors.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/injection/descriptor.d.ts.map +0 -1
- package/dist/injection/descriptor.js.map +0 -1
- package/dist/injection/resolve-options.d.ts.map +0 -1
- package/dist/injection/resolve-options.js.map +0 -1
- package/dist/introspection/dependency-graph.d.ts.map +0 -1
- package/dist/introspection/dependency-graph.js.map +0 -1
- package/dist/introspection/graph-adapters/cytoscape.d.ts.map +0 -1
- package/dist/introspection/graph-adapters/cytoscape.js.map +0 -1
- package/dist/introspection/graph-adapters/dot.d.ts.map +0 -1
- package/dist/introspection/graph-adapters/dot.js.map +0 -1
- package/dist/introspection/graph-adapters/mermaid.d.ts.map +0 -1
- package/dist/introspection/graph-adapters/mermaid.js.map +0 -1
- package/dist/introspection/graph-adapters/reactflow.d.ts.map +0 -1
- package/dist/introspection/graph-adapters/reactflow.js.map +0 -1
- package/dist/introspection/inspector.d.ts.map +0 -1
- package/dist/introspection/inspector.js.map +0 -1
- package/dist/lifecycle/lifecycle-manager.d.ts.map +0 -1
- package/dist/lifecycle/lifecycle-manager.js.map +0 -1
- package/dist/lifecycle/scope-manager.d.ts.map +0 -1
- package/dist/lifecycle/scope-manager.js.map +0 -1
- package/dist/metadata/metadata-keys.d.ts.map +0 -1
- package/dist/metadata/metadata-keys.js.map +0 -1
- package/dist/metadata/metadata-reader-token.d.ts.map +0 -1
- package/dist/metadata/metadata-reader-token.js.map +0 -1
- package/dist/metadata/metadata-types.d.ts.map +0 -1
- package/dist/metadata/metadata-types.js.map +0 -1
- package/dist/metadata/symbol-metadata-reader.d.ts.map +0 -1
- package/dist/metadata/symbol-metadata-reader.js.map +0 -1
- package/dist/metadata/verifying-metadata-reader.d.ts.map +0 -1
- package/dist/metadata/verifying-metadata-reader.js.map +0 -1
- package/dist/resolution/cache/activation-need.d.ts.map +0 -1
- package/dist/resolution/cache/activation-need.js.map +0 -1
- package/dist/resolution/cache/binding-lookup-cache.d.ts.map +0 -1
- package/dist/resolution/cache/binding-lookup-cache.js.map +0 -1
- package/dist/resolution/cache/class-introspector.d.ts.map +0 -1
- package/dist/resolution/cache/class-introspector.js.map +0 -1
- package/dist/resolution/context.d.ts.map +0 -1
- package/dist/resolution/context.js.map +0 -1
- package/dist/resolution/path/resolution-path.d.ts.map +0 -1
- package/dist/resolution/path/resolution-path.js.map +0 -1
- package/dist/resolution/plan/instantiation-plan.d.ts.map +0 -1
- package/dist/resolution/plan/instantiation-plan.js.map +0 -1
- package/dist/resolution/resolver.d.ts.map +0 -1
- package/dist/resolution/resolver.js.map +0 -1
- package/dist/resolution/select/binding-select.d.ts.map +0 -1
- package/dist/resolution/select/binding-select.js.map +0 -1
- package/dist/resolution/select/constraints.d.ts.map +0 -1
- package/dist/resolution/select/constraints.js.map +0 -1
- package/src/ambient/active-container.ts +0 -69
- package/src/container/binding-builders.ts +0 -332
- package/src/container/container.ts +0 -973
- package/src/core/binding-scope.ts +0 -15
- package/src/core/binding.ts +0 -477
- package/src/core/constraint-requirement.ts +0 -94
- package/src/core/constructor-type.ts +0 -18
- package/src/core/map-upsert.ts +0 -53
- package/src/core/module.ts +0 -117
- package/src/core/registry.ts +0 -371
- package/src/core/tag.ts +0 -158
- package/src/core/token.ts +0 -31
- package/src/core/types.ts +0 -149
- package/src/decorators/inject.ts +0 -101
- package/src/decorators/injectable.ts +0 -134
- package/src/decorators/lifecycle-decorators.ts +0 -43
- package/src/errors/diagnostics.ts +0 -45
- package/src/errors/errors.ts +0 -476
- package/src/index.ts +0 -139
- package/src/injection/descriptor.ts +0 -218
- package/src/injection/resolve-options.ts +0 -144
- package/src/introspection/dependency-graph.ts +0 -282
- package/src/introspection/graph-adapters/cytoscape.ts +0 -78
- package/src/introspection/graph-adapters/dot.ts +0 -32
- package/src/introspection/graph-adapters/mermaid.ts +0 -61
- package/src/introspection/graph-adapters/reactflow.ts +0 -84
- package/src/introspection/inspector.ts +0 -134
- package/src/lifecycle/lifecycle-manager.ts +0 -272
- package/src/lifecycle/scope-manager.ts +0 -158
- package/src/metadata/metadata-keys.ts +0 -28
- package/src/metadata/metadata-reader-token.ts +0 -10
- package/src/metadata/metadata-types.ts +0 -57
- package/src/metadata/symbol-metadata-reader.ts +0 -126
- package/src/metadata/verifying-metadata-reader.ts +0 -41
- package/src/resolution/cache/activation-need.ts +0 -87
- package/src/resolution/cache/binding-lookup-cache.ts +0 -209
- package/src/resolution/cache/class-introspector.ts +0 -256
- package/src/resolution/context.ts +0 -331
- package/src/resolution/path/resolution-path.ts +0 -167
- package/src/resolution/plan/instantiation-plan.ts +0 -617
- package/src/resolution/resolver.ts +0 -1675
- package/src/resolution/select/binding-select.ts +0 -210
- package/src/resolution/select/constraints.ts +0 -146
|
@@ -1,1675 +0,0 @@
|
|
|
1
|
-
import type { AmbientResolution } from "#/ambient/active-container";
|
|
2
|
-
import type { Container } from "#/container/container";
|
|
3
|
-
import type { Binding, ConstantBinding, DynamicAsyncBinding, DynamicBinding } from "#/core/binding";
|
|
4
|
-
import { NO_INSTANCE } from "#/core/binding";
|
|
5
|
-
import type { BindingRegistry } from "#/core/registry";
|
|
6
|
-
import { NO_TAG_KEYS } from "#/core/tag";
|
|
7
|
-
import type { Token } from "#/core/token";
|
|
8
|
-
import { tokenName } from "#/core/token";
|
|
9
|
-
import type {
|
|
10
|
-
ActivationHandler,
|
|
11
|
-
BindingIdentifier,
|
|
12
|
-
BindingTag,
|
|
13
|
-
ConstraintContext,
|
|
14
|
-
Constructor,
|
|
15
|
-
ResolutionFrame,
|
|
16
|
-
ResolveOptions,
|
|
17
|
-
} from "#/core/types";
|
|
18
|
-
import type { ResolutionDiagnostics } from "#/errors/diagnostics";
|
|
19
|
-
import {
|
|
20
|
-
AsyncActivationError,
|
|
21
|
-
AsyncResolutionError,
|
|
22
|
-
CircularDependencyError,
|
|
23
|
-
DisposedContainerError,
|
|
24
|
-
InternalError,
|
|
25
|
-
MissingMetadataError,
|
|
26
|
-
MissingScopeContextError,
|
|
27
|
-
NoMatchingBindingError,
|
|
28
|
-
TokenNotBoundError,
|
|
29
|
-
} from "#/errors/errors";
|
|
30
|
-
import type { DependencySlot } from "#/injection/resolve-options";
|
|
31
|
-
import { isNameOnlyOptions, resolveOptionsForSlot, singleTagOnlyOf } from "#/injection/resolve-options";
|
|
32
|
-
import type { LifecycleManager } from "#/lifecycle/lifecycle-manager";
|
|
33
|
-
import type { ScopeManager } from "#/lifecycle/scope-manager";
|
|
34
|
-
import { SCOPED_MISS } from "#/lifecycle/scope-manager";
|
|
35
|
-
import type { MetadataReader, ParamMetadata } from "#/metadata/metadata-types";
|
|
36
|
-
import { ActivationNeedCache } from "#/resolution/cache/activation-need";
|
|
37
|
-
import type { DefaultLookupEntry } from "#/resolution/cache/binding-lookup-cache";
|
|
38
|
-
import { BindingLookupCache } from "#/resolution/cache/binding-lookup-cache";
|
|
39
|
-
import { ClassIntrospector } from "#/resolution/cache/class-introspector";
|
|
40
|
-
import type { ResolverCallbacks } from "#/resolution/context";
|
|
41
|
-
import {
|
|
42
|
-
AsyncCascadeContext,
|
|
43
|
-
AsyncLevelContext,
|
|
44
|
-
buildResolutionFrame,
|
|
45
|
-
DefaultResolutionContext,
|
|
46
|
-
} from "#/resolution/context";
|
|
47
|
-
import type { BranchDepth, OwnedBranchStack } from "#/resolution/path/resolution-path";
|
|
48
|
-
import {
|
|
49
|
-
branchDepthOf,
|
|
50
|
-
cycleNamesOf,
|
|
51
|
-
enterResolutionPath,
|
|
52
|
-
extendResolutionBranch,
|
|
53
|
-
ROOT_BRANCH,
|
|
54
|
-
UNOWNED_BRANCH,
|
|
55
|
-
} from "#/resolution/path/resolution-path";
|
|
56
|
-
import { InstantiationPlanCompiler, PLAN_RETRY } from "#/resolution/plan/instantiation-plan";
|
|
57
|
-
import { matchesSlot, requestedTagKeyMask, selectAllBindings, selectBinding } from "#/resolution/select/binding-select";
|
|
58
|
-
|
|
59
|
-
// Where a multi-tag resolve switches from scanning the token's list to walking the tag indexes.
|
|
60
|
-
const MULTI_TAG_INDEX_THRESHOLD = 8;
|
|
61
|
-
|
|
62
|
-
const EMPTY_STRING_LIST: ReadonlyArray<string> = [];
|
|
63
|
-
const EMPTY_FRAME_LIST: ReadonlyArray<ResolutionFrame> = [];
|
|
64
|
-
const EMPTY_PARAM_LIST: ReadonlyArray<ParamMetadata> = [];
|
|
65
|
-
const ROOT_CONSTRAINT_CONTEXT = {
|
|
66
|
-
resolutionPath: EMPTY_STRING_LIST,
|
|
67
|
-
resolutionStack: EMPTY_FRAME_LIST,
|
|
68
|
-
parent: undefined,
|
|
69
|
-
ancestors: EMPTY_FRAME_LIST,
|
|
70
|
-
currentResolveOptions: undefined,
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* The resolution engine driving binding selection, instantiation, scoping, and lifecycle hooks.
|
|
75
|
-
*
|
|
76
|
-
* @since 0.3.16-canary.0
|
|
77
|
-
*/
|
|
78
|
-
export class DependencyResolver implements ResolverCallbacks {
|
|
79
|
-
readonly #syncResolutionContextPool: Array<DefaultResolutionContext> = [];
|
|
80
|
-
// Contexts bound to the cascade pair — deferred: only an async cascade's sync resolves need it.
|
|
81
|
-
#cascadeContextPool: Array<DefaultResolutionContext> | undefined;
|
|
82
|
-
/**
|
|
83
|
-
* The stack a top-level **sync** resolve reuses instead of minting an array per call.
|
|
84
|
-
*
|
|
85
|
-
* @remarks Read directly rather than through an accessor: a shallow resolve is one top-level
|
|
86
|
-
* call, so a call there is not amortised over anything. Every sync lane pops what it pushes, so
|
|
87
|
-
* `rootStack.length === 0` means no resolve holds the stack; async appends without popping and
|
|
88
|
-
* mints its own. Keeping it stable is also what lets a pooled context skip re-storing it.
|
|
89
|
-
*/
|
|
90
|
-
readonly rootStack: Array<ResolutionFrame> = [];
|
|
91
|
-
// The open synchronous factory cascade: its stack is the ancestor chain, and it is balanced
|
|
92
|
-
// because synchronous code does not interleave.
|
|
93
|
-
readonly #cascadeStack: Array<ResolutionFrame> = [];
|
|
94
|
-
#cascadeContext: AsyncCascadeContext | undefined;
|
|
95
|
-
// Compiled plans; `null` marks a binding as unplannable under the current cache versions.
|
|
96
|
-
readonly #classPlanByBindingId = new Map<BindingIdentifier, (() => unknown) | null>();
|
|
97
|
-
#classPlanRegistryVersion = -1;
|
|
98
|
-
#classPlanActivationVersion = -1;
|
|
99
|
-
// The async lane's plans, stamped and invalidated apart so neither lane pays the other's misses.
|
|
100
|
-
readonly #asyncPlanByBindingId = new Map<BindingIdentifier, (() => unknown) | null>();
|
|
101
|
-
#asyncPlanRegistryVersion = -1;
|
|
102
|
-
#asyncPlanActivationVersion = -1;
|
|
103
|
-
|
|
104
|
-
readonly #registry: BindingRegistry;
|
|
105
|
-
readonly #scope: ScopeManager;
|
|
106
|
-
readonly #lifecycle: LifecycleManager;
|
|
107
|
-
readonly #metadataReader: MetadataReader;
|
|
108
|
-
readonly #parent: DependencyResolver | undefined;
|
|
109
|
-
readonly #lookup: BindingLookupCache<DependencyResolver>;
|
|
110
|
-
readonly #classes: ClassIntrospector;
|
|
111
|
-
readonly #activation: ActivationNeedCache;
|
|
112
|
-
|
|
113
|
-
constructor(
|
|
114
|
-
registry: BindingRegistry,
|
|
115
|
-
scope: ScopeManager,
|
|
116
|
-
lifecycle: LifecycleManager,
|
|
117
|
-
metadataReader: MetadataReader,
|
|
118
|
-
container: Container,
|
|
119
|
-
parent: DependencyResolver | undefined,
|
|
120
|
-
) {
|
|
121
|
-
this.#registry = registry;
|
|
122
|
-
this.#scope = scope;
|
|
123
|
-
this.#lifecycle = lifecycle;
|
|
124
|
-
this.#metadataReader = metadataReader;
|
|
125
|
-
this.#parent = parent;
|
|
126
|
-
this.#lookup = new BindingLookupCache<DependencyResolver>(
|
|
127
|
-
registry,
|
|
128
|
-
this,
|
|
129
|
-
parent === undefined ? undefined : parent.#lookup,
|
|
130
|
-
);
|
|
131
|
-
this.#classes = new ClassIntrospector(metadataReader, container);
|
|
132
|
-
this.#activation = new ActivationNeedCache(lifecycle, this.#classes, registry);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/** The reader this resolver was built with, which is the one its container answers with. */
|
|
136
|
-
get metadataReader(): MetadataReader {
|
|
137
|
-
return this.#metadataReader;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/** Structural counts for the {@link ResolutionDiagnostics} a container reports. */
|
|
141
|
-
describeCaches(): Pick<
|
|
142
|
-
ResolutionDiagnostics,
|
|
143
|
-
"compiledPlanCount" | "compiledAsyncPlanCount" | "syncContextPoolSize"
|
|
144
|
-
> {
|
|
145
|
-
let compiledPlanCount = 0;
|
|
146
|
-
for (const plan of this.#classPlanByBindingId.values()) {
|
|
147
|
-
if (plan !== null) {
|
|
148
|
-
compiledPlanCount += 1;
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
let compiledAsyncPlanCount = 0;
|
|
152
|
-
for (const plan of this.#asyncPlanByBindingId.values()) {
|
|
153
|
-
if (plan !== null) {
|
|
154
|
-
compiledAsyncPlanCount += 1;
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
return {
|
|
158
|
-
compiledPlanCount,
|
|
159
|
-
compiledAsyncPlanCount,
|
|
160
|
-
syncContextPoolSize: this.#syncResolutionContextPool.length,
|
|
161
|
-
};
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// ── Binding lookup ─────────────────────────────────────────────────────────────────────────────────────────────────
|
|
165
|
-
|
|
166
|
-
#findBinding(
|
|
167
|
-
token: Token<unknown> | Constructor,
|
|
168
|
-
options: ResolveOptions | undefined,
|
|
169
|
-
resolutionStack: Array<ResolutionFrame>,
|
|
170
|
-
): DefaultLookupEntry<DependencyResolver> | undefined {
|
|
171
|
-
if (options === undefined) {
|
|
172
|
-
const fastDefaultBinding = this.#registry.getFastDefault(token);
|
|
173
|
-
if (fastDefaultBinding !== undefined) {
|
|
174
|
-
return { binding: fastDefaultBinding, owner: this };
|
|
175
|
-
}
|
|
176
|
-
} else if (isNameOnlyOptions(options)) {
|
|
177
|
-
const namedBinding = this.#registry.getSimpleNamed(token, options.name);
|
|
178
|
-
if (namedBinding !== undefined && this.#matchesBindingFast(namedBinding, options, resolutionStack)) {
|
|
179
|
-
return { binding: namedBinding, owner: this };
|
|
180
|
-
}
|
|
181
|
-
} else {
|
|
182
|
-
const singleTag = singleTagOnlyOf(options);
|
|
183
|
-
if (singleTag !== undefined) {
|
|
184
|
-
const tagged = this.#registry.getSimpleTagged(token, singleTag);
|
|
185
|
-
if (tagged !== undefined && this.#satisfiesPredicate(tagged, options, resolutionStack)) {
|
|
186
|
-
return { binding: tagged, owner: this };
|
|
187
|
-
}
|
|
188
|
-
} else if (
|
|
189
|
-
options.name === undefined &&
|
|
190
|
-
// A threshold switches the data structure, never the semantics: under it the generic scan
|
|
191
|
-
// below beats walking the indexes, and both paths answer identically. Sized first, so a
|
|
192
|
-
// small list pays one length read and nothing else.
|
|
193
|
-
this.#registry.getAll(token).length > MULTI_TAG_INDEX_THRESHOLD &&
|
|
194
|
-
requestedTagKeyMask(options) !== NO_TAG_KEYS
|
|
195
|
-
) {
|
|
196
|
-
// A name-less multi-tag request matches only name-less tagged slots, and every such slot
|
|
197
|
-
// lives in one of the two tag indexes — so their union is the whole candidate set and the
|
|
198
|
-
// token's full list never needs scanning. Selection still owns predicates and specificity.
|
|
199
|
-
const selected = this.#selectMultiTagged(token, options, resolutionStack);
|
|
200
|
-
if (selected !== undefined) {
|
|
201
|
-
return { binding: selected, owner: this };
|
|
202
|
-
}
|
|
203
|
-
return this.#parent === undefined ? undefined : this.#parent.#findBinding(token, options, resolutionStack);
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
const bindings = this.#registry.getAll(token);
|
|
208
|
-
if (bindings.length > 0) {
|
|
209
|
-
// A lone candidate is its own selection: matching it is the whole decision, with no
|
|
210
|
-
// specificity to weigh and no ambiguity to report.
|
|
211
|
-
const selected =
|
|
212
|
-
bindings.length === 1
|
|
213
|
-
? this.#matchesBindingFast(bindings[0]!, options, resolutionStack)
|
|
214
|
-
? bindings[0]
|
|
215
|
-
: undefined
|
|
216
|
-
: selectBinding(bindings, options, this.#makeConstraintContext(resolutionStack, options), tokenName(token));
|
|
217
|
-
if (selected !== undefined) {
|
|
218
|
-
return { binding: selected, owner: this };
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
if (this.#parent !== undefined) {
|
|
222
|
-
return this.#parent.#findBinding(token, options, resolutionStack);
|
|
223
|
-
}
|
|
224
|
-
return undefined;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
/**
|
|
228
|
-
* The binding a token resolves to with alias hops followed, or a diagnostic throw.
|
|
229
|
-
*
|
|
230
|
-
* @remarks Alias hops are followed iteratively with exact cycle detection — a revisited alias
|
|
231
|
-
* token raises {@link CircularDependencyError} instead of overflowing the call stack.
|
|
232
|
-
*/
|
|
233
|
-
#requireBinding(
|
|
234
|
-
token: Token<unknown> | Constructor,
|
|
235
|
-
options: ResolveOptions | undefined,
|
|
236
|
-
resolutionStack: Array<ResolutionFrame>,
|
|
237
|
-
): DefaultLookupEntry<DependencyResolver> {
|
|
238
|
-
let currentToken = token;
|
|
239
|
-
let visitedAliasTokens: Set<Token<unknown> | Constructor> | undefined;
|
|
240
|
-
let found = this.#findBinding(currentToken, options, resolutionStack);
|
|
241
|
-
|
|
242
|
-
while (found !== undefined && found.binding.kind === "alias") {
|
|
243
|
-
const target = found.binding.target;
|
|
244
|
-
visitedAliasTokens ??= new Set([currentToken]);
|
|
245
|
-
if (visitedAliasTokens.has(target)) {
|
|
246
|
-
throw new CircularDependencyError([...visitedAliasTokens, target].map((entry) => tokenName(entry)));
|
|
247
|
-
}
|
|
248
|
-
visitedAliasTokens.add(target);
|
|
249
|
-
currentToken = target;
|
|
250
|
-
found = this.#findBinding(currentToken, options, resolutionStack);
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
if (found === undefined) {
|
|
254
|
-
// Thrown here rather than from a helper: the error captures this stack, and an error path is
|
|
255
|
-
// dominated by that capture. Bindings under the token mean the request matched none of them.
|
|
256
|
-
if (this.#registry.getAll(currentToken).length > 0) {
|
|
257
|
-
throw new NoMatchingBindingError(
|
|
258
|
-
tokenName(currentToken),
|
|
259
|
-
options ?? {},
|
|
260
|
-
this.#registry.availableSlotStrings(currentToken),
|
|
261
|
-
);
|
|
262
|
-
}
|
|
263
|
-
throw new TokenNotBoundError(tokenName(currentToken));
|
|
264
|
-
}
|
|
265
|
-
return found;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
/**
|
|
269
|
-
* Binding lookup aligned with `resolve` — used by `Container.validate` without instantiating.
|
|
270
|
-
*/
|
|
271
|
-
peekBindingForValidate(
|
|
272
|
-
token: Token<unknown> | Constructor,
|
|
273
|
-
options: ResolveOptions | undefined,
|
|
274
|
-
): DefaultLookupEntry<DependencyResolver> | undefined {
|
|
275
|
-
return this.#findBinding(token, options, []);
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
/**
|
|
279
|
-
* Mirrors {@link DependencyResolver.resolveAll} candidate selection only (no instantiation).
|
|
280
|
-
*/
|
|
281
|
-
peekCandidateBindingsForValidate(
|
|
282
|
-
token: Token<unknown> | Constructor,
|
|
283
|
-
options: ResolveOptions | undefined,
|
|
284
|
-
): ReadonlyArray<Binding> {
|
|
285
|
-
return this.#candidateBindings(token, options, []);
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
// ── Sync resolve ───────────────────────────────────────────────────────────────────────────────────────────────────
|
|
289
|
-
|
|
290
|
-
resolveFromContext<Value>(token: Token<Value> | Constructor<Value>, resolutionStack: Array<ResolutionFrame>): Value {
|
|
291
|
-
// Hot lane: own-registry fast default. Fall back to the chain-versioned memo
|
|
292
|
-
// (parent-chain walk + alias folding) only on miss or alias.
|
|
293
|
-
const fastBinding = this.#registry.getFastDefault(token);
|
|
294
|
-
if (fastBinding !== undefined && fastBinding.kind !== "alias") {
|
|
295
|
-
return this.#resolveDefaultEntry(fastBinding, this, resolutionStack) as Value;
|
|
296
|
-
}
|
|
297
|
-
const entry = this.#lookup.defaultEntry(token);
|
|
298
|
-
if (entry === null) {
|
|
299
|
-
return this.resolve(token, undefined, resolutionStack);
|
|
300
|
-
}
|
|
301
|
-
return this.#resolveDefaultEntry(entry.binding, entry.owner, resolutionStack) as Value;
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
#resolveDefaultEntry(binding: Binding, owner: DependencyResolver, resolutionStack: Array<ResolutionFrame>): unknown {
|
|
305
|
-
const scope = binding.scope;
|
|
306
|
-
if (scope === "transient") {
|
|
307
|
-
if (binding.kind === "dynamic") {
|
|
308
|
-
// Container-level hooks belong to the binding's owner — a child-registered hook must not
|
|
309
|
-
// fire for a parent-owned binding, and the owner's must.
|
|
310
|
-
const containerHooks =
|
|
311
|
-
owner.#lifecycle.activationVersion === 0 ? undefined : owner.#lifecycle.activationHandlersFor(binding.token);
|
|
312
|
-
if (binding.onActivation === undefined && (containerHooks === undefined || containerHooks.length === 0)) {
|
|
313
|
-
return this.#resolveTransientDynamicSyncFromContext(binding, resolutionStack);
|
|
314
|
-
}
|
|
315
|
-
return this.#resolveTransientDynamicActivatedSync(binding, containerHooks, resolutionStack);
|
|
316
|
-
}
|
|
317
|
-
// Compiled plans only run at the top level — inner levels keep the runtime cycle guard.
|
|
318
|
-
if ((binding.kind === "class" || binding.kind === "resolved") && resolutionStack.length === 0) {
|
|
319
|
-
const plan = this.#getInstantiationPlan(binding);
|
|
320
|
-
if (plan !== null) {
|
|
321
|
-
return plan();
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
} else if (scope === "singleton") {
|
|
325
|
-
// A constant is a singleton that is already its own instance.
|
|
326
|
-
if (owner.#isPlainConstant(binding)) {
|
|
327
|
-
return binding.value;
|
|
328
|
-
}
|
|
329
|
-
const cachedSingleton = binding.instance;
|
|
330
|
-
if (cachedSingleton !== NO_INSTANCE) {
|
|
331
|
-
return cachedSingleton;
|
|
332
|
-
}
|
|
333
|
-
if (owner !== this) {
|
|
334
|
-
return owner.#resolveBinding(binding, undefined, resolutionStack, owner);
|
|
335
|
-
}
|
|
336
|
-
} else {
|
|
337
|
-
const cachedScoped = this.#readScoped(binding);
|
|
338
|
-
if (cachedScoped !== SCOPED_MISS) {
|
|
339
|
-
return cachedScoped;
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
return this.#resolveBinding(binding, undefined, resolutionStack, owner);
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
// Lean lane for an activated transient dynamic binding: same observable behavior as the
|
|
346
|
-
// generic #resolveBinding path (guard, frame, ctx, per-binding then container hooks) with
|
|
347
|
-
// the kind/activation dispatch resolved statically.
|
|
348
|
-
#resolveTransientDynamicActivatedSync(
|
|
349
|
-
binding: DynamicBinding<unknown>,
|
|
350
|
-
containerHooks: ReadonlyArray<ActivationHandler<unknown>> | undefined,
|
|
351
|
-
resolutionStack: Array<ResolutionFrame>,
|
|
352
|
-
): unknown {
|
|
353
|
-
// Same O(1) cycle guard as the unhooked lane: this is still one sync call stack, so the flag
|
|
354
|
-
// *is* exact path membership.
|
|
355
|
-
const frame = this.#getResolutionFrame(binding);
|
|
356
|
-
const tokenDisplayName = frame.tokenName;
|
|
357
|
-
if (binding.inFlight) {
|
|
358
|
-
throw new CircularDependencyError(cycleNamesOf(resolutionStack, tokenDisplayName));
|
|
359
|
-
}
|
|
360
|
-
binding.inFlight = true;
|
|
361
|
-
resolutionStack.push(frame);
|
|
362
|
-
try {
|
|
363
|
-
const resolutionCtx = this.#acquireSyncResolutionContext(resolutionStack, undefined);
|
|
364
|
-
const factoryResult = binding.factory(resolutionCtx);
|
|
365
|
-
if (factoryResult instanceof Promise) {
|
|
366
|
-
throw new AsyncResolutionError(resolutionStack[0]?.tokenName ?? tokenDisplayName, tokenDisplayName);
|
|
367
|
-
}
|
|
368
|
-
let activated = factoryResult;
|
|
369
|
-
if (binding.onActivation !== undefined) {
|
|
370
|
-
const activationResult = binding.onActivation(resolutionCtx, activated);
|
|
371
|
-
if (activationResult instanceof Promise) {
|
|
372
|
-
throw new AsyncActivationError(tokenDisplayName, "onActivation");
|
|
373
|
-
}
|
|
374
|
-
activated = activationResult;
|
|
375
|
-
}
|
|
376
|
-
if (containerHooks !== undefined) {
|
|
377
|
-
for (let index = 0; index < containerHooks.length; index += 1) {
|
|
378
|
-
const activationResult = containerHooks[index]!(resolutionCtx, activated);
|
|
379
|
-
if (activationResult instanceof Promise) {
|
|
380
|
-
throw new AsyncActivationError(tokenDisplayName, "onActivation");
|
|
381
|
-
}
|
|
382
|
-
activated = activationResult;
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
return activated;
|
|
386
|
-
} finally {
|
|
387
|
-
resolutionStack.pop();
|
|
388
|
-
binding.inFlight = false;
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
/** Chain-summed activation version: a plan can inline a parent-owned binding, so a parent's hook registration must invalidate it. */
|
|
393
|
-
#chainActivationVersion(): number {
|
|
394
|
-
let version = this.#lifecycle.activationVersion;
|
|
395
|
-
for (let current = this.#parent; current !== undefined; current = current.#parent) {
|
|
396
|
-
version += current.#lifecycle.activationVersion;
|
|
397
|
-
}
|
|
398
|
-
return version;
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
#getInstantiationPlan(binding: Binding & { kind: "class" | "resolved" }): (() => unknown) | null {
|
|
402
|
-
const registryVersion = this.#lookup.chainVersion();
|
|
403
|
-
const activationVersion = this.#chainActivationVersion();
|
|
404
|
-
if (registryVersion !== this.#classPlanRegistryVersion || activationVersion !== this.#classPlanActivationVersion) {
|
|
405
|
-
this.#classPlanByBindingId.clear();
|
|
406
|
-
this.#classPlanRegistryVersion = registryVersion;
|
|
407
|
-
this.#classPlanActivationVersion = activationVersion;
|
|
408
|
-
}
|
|
409
|
-
const cached = this.#classPlanByBindingId.get(binding.id);
|
|
410
|
-
if (cached !== undefined) {
|
|
411
|
-
return cached;
|
|
412
|
-
}
|
|
413
|
-
const compiled = this.#planCompiler.compile(binding);
|
|
414
|
-
if (compiled === PLAN_RETRY) {
|
|
415
|
-
// Lifecycle metadata not discovered yet — the fallback resolve discovers it; retry then.
|
|
416
|
-
return null;
|
|
417
|
-
}
|
|
418
|
-
this.#classPlanByBindingId.set(binding.id, compiled);
|
|
419
|
-
return compiled;
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
// Compiler behind #getInstantiationPlan — cold path, so the host indirection costs nothing hot.
|
|
423
|
-
readonly #planCompiler = new InstantiationPlanCompiler({
|
|
424
|
-
hasActivationHandlers: (binding) => this.#ownerOf(binding).#lifecycle.hasActivationHandlers(binding.token),
|
|
425
|
-
knownPostConstruct: (target) => this.#classes.knownPostConstruct(target),
|
|
426
|
-
needsActiveContainer: (target) => this.#classes.needsActiveContainer(target),
|
|
427
|
-
getConstructorMetadata: (target) => this.#classes.constructorMetadata(target),
|
|
428
|
-
lookupDependencyEntry: (token) => {
|
|
429
|
-
const entry = this.#lookup.defaultEntry(token);
|
|
430
|
-
return entry === null ? null : { binding: entry.binding };
|
|
431
|
-
},
|
|
432
|
-
// Exactly what #findBinding's named lane accepts, minus the half that reads a path: a predicate
|
|
433
|
-
// is the compiler's cue to leave the selection to the runtime.
|
|
434
|
-
lookupPathIndependentNamedEntry: (token, options) => {
|
|
435
|
-
const entry = this.#lookup.namedEntry(token, options.name);
|
|
436
|
-
if (entry === null || entry.binding.predicate !== undefined || !matchesSlot(entry.binding.slot, options)) {
|
|
437
|
-
return null;
|
|
438
|
-
}
|
|
439
|
-
return { binding: entry.binding };
|
|
440
|
-
},
|
|
441
|
-
// The named rule verbatim, on the single-tag lane's memo.
|
|
442
|
-
lookupPathIndependentTaggedEntry: (token, options) => {
|
|
443
|
-
const singleTag = singleTagOnlyOf(options);
|
|
444
|
-
if (singleTag === undefined) {
|
|
445
|
-
return null;
|
|
446
|
-
}
|
|
447
|
-
const entry = this.#lookup.taggedEntry(token, singleTag);
|
|
448
|
-
if (entry === null || entry.binding.predicate !== undefined || !matchesSlot(entry.binding.slot, options)) {
|
|
449
|
-
return null;
|
|
450
|
-
}
|
|
451
|
-
return { binding: entry.binding };
|
|
452
|
-
},
|
|
453
|
-
getResolutionFrame: (binding) => this.#getResolutionFrame(binding),
|
|
454
|
-
// Dispatches exactly as #resolveDep does, so an escaped dep is indistinguishable
|
|
455
|
-
// from the same dep on a fully interpreted resolve.
|
|
456
|
-
resolveEscaped: (token, options, arity, resolutionStack) => {
|
|
457
|
-
if (arity === "all") {
|
|
458
|
-
return this.resolveAll(token, options, resolutionStack);
|
|
459
|
-
}
|
|
460
|
-
if (arity === "optional") {
|
|
461
|
-
return this.resolveOptional(token, options, resolutionStack);
|
|
462
|
-
}
|
|
463
|
-
if (options === undefined) {
|
|
464
|
-
return this.resolveFromContext(token, resolutionStack);
|
|
465
|
-
}
|
|
466
|
-
return this.resolve(token, options, resolutionStack);
|
|
467
|
-
},
|
|
468
|
-
// Dispatches exactly as #resolveDepAsync does, for the async lane's escapes.
|
|
469
|
-
resolveEscapedAsync: (token, options, arity, resolutionStack) => {
|
|
470
|
-
if (arity === "all") {
|
|
471
|
-
return this.resolveAllAsync(token, options, resolutionStack, UNOWNED_BRANCH);
|
|
472
|
-
}
|
|
473
|
-
if (arity === "optional") {
|
|
474
|
-
return this.resolveOptionalAsync(token, options, resolutionStack, UNOWNED_BRANCH);
|
|
475
|
-
}
|
|
476
|
-
if (options === undefined) {
|
|
477
|
-
return this.resolveAsyncFromContext(token, resolutionStack, UNOWNED_BRANCH);
|
|
478
|
-
}
|
|
479
|
-
return this.resolveAsync(token, options, resolutionStack, UNOWNED_BRANCH);
|
|
480
|
-
},
|
|
481
|
-
});
|
|
482
|
-
|
|
483
|
-
/** The async lane's plan for a statically-visible transient binding, mirroring the sync getter. */
|
|
484
|
-
#getAsyncInstantiationPlan(
|
|
485
|
-
binding: Binding & { kind: "class" | "resolved" | "resolved-async" },
|
|
486
|
-
): (() => unknown) | null {
|
|
487
|
-
const registryVersion = this.#lookup.chainVersion();
|
|
488
|
-
const activationVersion = this.#chainActivationVersion();
|
|
489
|
-
if (registryVersion !== this.#asyncPlanRegistryVersion || activationVersion !== this.#asyncPlanActivationVersion) {
|
|
490
|
-
this.#asyncPlanByBindingId.clear();
|
|
491
|
-
this.#asyncPlanRegistryVersion = registryVersion;
|
|
492
|
-
this.#asyncPlanActivationVersion = activationVersion;
|
|
493
|
-
}
|
|
494
|
-
const cached = this.#asyncPlanByBindingId.get(binding.id);
|
|
495
|
-
if (cached !== undefined) {
|
|
496
|
-
return cached;
|
|
497
|
-
}
|
|
498
|
-
const compiled = this.#planCompiler.compileAsync(binding);
|
|
499
|
-
if (compiled === PLAN_RETRY) {
|
|
500
|
-
// Lifecycle metadata not discovered yet — the fallback resolve discovers it; retry then.
|
|
501
|
-
return null;
|
|
502
|
-
}
|
|
503
|
-
this.#asyncPlanByBindingId.set(binding.id, compiled);
|
|
504
|
-
return compiled;
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
resolve<Value>(
|
|
508
|
-
token: Token<Value> | Constructor<Value>,
|
|
509
|
-
options: ResolveOptions | undefined,
|
|
510
|
-
resolutionStack: Array<ResolutionFrame>,
|
|
511
|
-
): Value {
|
|
512
|
-
// Name-only fast lane: memoized lookup, dispatching just the shapes whose
|
|
513
|
-
// semantics involve no resolution context (constants, cached singletons).
|
|
514
|
-
if (options !== undefined && isNameOnlyOptions(options)) {
|
|
515
|
-
const namedEntry = this.#lookup.namedEntry(token, options.name);
|
|
516
|
-
if (namedEntry !== null) {
|
|
517
|
-
const namedBinding = namedEntry.binding;
|
|
518
|
-
if (namedEntry.owner.#isPlainConstant(namedBinding)) {
|
|
519
|
-
return namedBinding.value as Value;
|
|
520
|
-
}
|
|
521
|
-
if (namedBinding.scope === "singleton" && namedBinding.instance !== NO_INSTANCE) {
|
|
522
|
-
return namedBinding.instance as Value;
|
|
523
|
-
}
|
|
524
|
-
// Everything else keeps the full path (context, activation, guards).
|
|
525
|
-
}
|
|
526
|
-
} else if (options !== undefined) {
|
|
527
|
-
// Single-tag fast lane: the named lane's tagged twin, memoizing the chain walk.
|
|
528
|
-
const singleTag = singleTagOnlyOf(options);
|
|
529
|
-
if (singleTag !== undefined) {
|
|
530
|
-
const taggedEntry = this.#lookup.taggedEntry(token, singleTag);
|
|
531
|
-
if (taggedEntry !== null) {
|
|
532
|
-
const taggedBinding = taggedEntry.binding;
|
|
533
|
-
if (taggedEntry.owner.#isPlainConstant(taggedBinding)) {
|
|
534
|
-
return taggedBinding.value as Value;
|
|
535
|
-
}
|
|
536
|
-
if (taggedBinding.scope === "singleton" && taggedBinding.instance !== NO_INSTANCE) {
|
|
537
|
-
return taggedBinding.instance as Value;
|
|
538
|
-
}
|
|
539
|
-
// Everything else keeps the full path (context, activation, guards).
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
const { binding, owner } = this.#requireBinding(token, options, resolutionStack);
|
|
545
|
-
|
|
546
|
-
// A singleton owned by a parent resolver is resolved there, so the parent caches it.
|
|
547
|
-
if (binding.scope === "singleton" && owner !== this) {
|
|
548
|
-
return owner.#resolveBinding(binding, options, resolutionStack, owner) as Value;
|
|
549
|
-
}
|
|
550
|
-
return this.#resolveBinding(binding, options, resolutionStack, owner) as Value;
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
#resolveBinding(
|
|
554
|
-
binding: Binding,
|
|
555
|
-
options: ResolveOptions | undefined,
|
|
556
|
-
resolutionStack: Array<ResolutionFrame>,
|
|
557
|
-
owner: DependencyResolver,
|
|
558
|
-
): unknown {
|
|
559
|
-
if (owner.#isPlainConstant(binding)) {
|
|
560
|
-
return binding.value;
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
const scope = binding.scope;
|
|
564
|
-
if (scope === "singleton") {
|
|
565
|
-
if (binding.instance !== NO_INSTANCE) {
|
|
566
|
-
return binding.instance;
|
|
567
|
-
}
|
|
568
|
-
// An async materialization already in flight must not be raced by a second, sync one.
|
|
569
|
-
if (this.#scope.getInflight(binding.id) !== undefined) {
|
|
570
|
-
throw new AsyncResolutionError(
|
|
571
|
-
resolutionStack[0]?.tokenName ?? tokenName(binding.token),
|
|
572
|
-
tokenName(binding.token),
|
|
573
|
-
);
|
|
574
|
-
}
|
|
575
|
-
if (this.#scope.isClosed) {
|
|
576
|
-
throw new DisposedContainerError();
|
|
577
|
-
}
|
|
578
|
-
} else if (scope === "scoped") {
|
|
579
|
-
const cachedScoped = this.#readScoped(binding);
|
|
580
|
-
if (cachedScoped !== SCOPED_MISS) {
|
|
581
|
-
return cachedScoped;
|
|
582
|
-
}
|
|
583
|
-
if (this.#scope.getInflight(binding.id) !== undefined) {
|
|
584
|
-
throw new AsyncResolutionError(
|
|
585
|
-
resolutionStack[0]?.tokenName ?? tokenName(binding.token),
|
|
586
|
-
tokenName(binding.token),
|
|
587
|
-
);
|
|
588
|
-
}
|
|
589
|
-
if (this.#scope.isClosed) {
|
|
590
|
-
throw new DisposedContainerError();
|
|
591
|
-
}
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
const frame = this.#getResolutionFrame(binding);
|
|
595
|
-
const tokenDisplayName = frame.tokenName;
|
|
596
|
-
const resolutionSet = enterResolutionPath(resolutionStack, frame);
|
|
597
|
-
try {
|
|
598
|
-
const needsActivation = owner.#activation.needsActivation(binding);
|
|
599
|
-
if (!needsActivation && scope === "transient" && binding.kind === "dynamic") {
|
|
600
|
-
const resolutionCtx = this.#acquireSyncResolutionContext(resolutionStack, options);
|
|
601
|
-
const dynamicResult = binding.factory(resolutionCtx);
|
|
602
|
-
if (dynamicResult instanceof Promise) {
|
|
603
|
-
throw new AsyncResolutionError(resolutionStack[0]?.tokenName ?? tokenDisplayName, tokenDisplayName);
|
|
604
|
-
}
|
|
605
|
-
return dynamicResult;
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
const resolutionCtx =
|
|
609
|
-
needsActivation || requiresResolutionContext(binding)
|
|
610
|
-
? this.#acquireSyncResolutionContext(resolutionStack, options)
|
|
611
|
-
: undefined;
|
|
612
|
-
|
|
613
|
-
const instance = this.#instantiateSync(binding, resolutionCtx, resolutionStack);
|
|
614
|
-
|
|
615
|
-
this.#mirrorPostConstructFromOwner(binding, owner);
|
|
616
|
-
const activated = owner.#activation.refreshAfterFirstInstantiation(binding, needsActivation)
|
|
617
|
-
? owner.#lifecycle.runActivationSync(
|
|
618
|
-
resolutionCtx as DefaultResolutionContext,
|
|
619
|
-
binding,
|
|
620
|
-
instance,
|
|
621
|
-
owner.#metadataReader,
|
|
622
|
-
)
|
|
623
|
-
: instance;
|
|
624
|
-
|
|
625
|
-
if (scope === "singleton") {
|
|
626
|
-
this.#scope.setSingleton(binding, activated);
|
|
627
|
-
} else if (scope === "scoped") {
|
|
628
|
-
this.#scope.setScoped(binding, activated);
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
return activated;
|
|
632
|
-
} finally {
|
|
633
|
-
resolutionStack.pop();
|
|
634
|
-
resolutionSet?.delete(frame.bindingId);
|
|
635
|
-
}
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
/** Path-continuing resolution handed to the ambient slot while an accessor class constructs. */
|
|
639
|
-
#ambientResolutionFor(resolutionStack: Array<ResolutionFrame>): AmbientResolution {
|
|
640
|
-
return {
|
|
641
|
-
resolve: <Value>(token: Token<Value> | Constructor<Value>, options?: ResolveOptions): Value =>
|
|
642
|
-
options === undefined
|
|
643
|
-
? this.resolveFromContext(token, resolutionStack)
|
|
644
|
-
: this.resolve(token, options, resolutionStack),
|
|
645
|
-
resolveOptional: <Value>(token: Token<Value> | Constructor<Value>, options?: ResolveOptions): Value | undefined =>
|
|
646
|
-
this.resolveOptional(token, options, resolutionStack),
|
|
647
|
-
};
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
#instantiateSync(
|
|
651
|
-
binding: Binding,
|
|
652
|
-
ctx: DefaultResolutionContext | undefined,
|
|
653
|
-
resolutionStack: Array<ResolutionFrame>,
|
|
654
|
-
): unknown {
|
|
655
|
-
switch (binding.kind) {
|
|
656
|
-
case "constant":
|
|
657
|
-
return binding.value;
|
|
658
|
-
|
|
659
|
-
case "dynamic": {
|
|
660
|
-
if (ctx === undefined) {
|
|
661
|
-
throw new InternalError("dynamic binding requires resolution context");
|
|
662
|
-
}
|
|
663
|
-
const factoryResult = binding.factory(ctx);
|
|
664
|
-
if (factoryResult instanceof Promise) {
|
|
665
|
-
throw asyncResolutionErrorFor(binding, resolutionStack);
|
|
666
|
-
}
|
|
667
|
-
return factoryResult;
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
case "dynamic-async":
|
|
671
|
-
throw asyncResolutionErrorFor(binding, resolutionStack);
|
|
672
|
-
|
|
673
|
-
case "class": {
|
|
674
|
-
const deps = this.#resolveDeps(this.#constructorParams(binding.target), resolutionStack);
|
|
675
|
-
return this.#classes.instantiate(
|
|
676
|
-
binding.target,
|
|
677
|
-
deps,
|
|
678
|
-
this.#classes.needsActiveContainer(binding.target) ? this.#ambientResolutionFor(resolutionStack) : undefined,
|
|
679
|
-
);
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
case "resolved": {
|
|
683
|
-
const deps = this.#resolveDeps(binding.deps, resolutionStack);
|
|
684
|
-
const factoryResult = binding.factory(...deps);
|
|
685
|
-
if (factoryResult instanceof Promise) {
|
|
686
|
-
throw asyncResolutionErrorFor(binding, resolutionStack);
|
|
687
|
-
}
|
|
688
|
-
return factoryResult;
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
case "resolved-async":
|
|
692
|
-
throw asyncResolutionErrorFor(binding, resolutionStack);
|
|
693
|
-
|
|
694
|
-
case "alias":
|
|
695
|
-
throw new InternalError("alias should have been followed before instantiation");
|
|
696
|
-
}
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
/**
|
|
700
|
-
* The parameters a class binding injects.
|
|
701
|
-
*
|
|
702
|
-
* @remarks A class the metadata reader knows nothing about is constructible only if it declares
|
|
703
|
-
* no parameters; anything else is a missing `@injectable()`.
|
|
704
|
-
*/
|
|
705
|
-
#constructorParams(target: Constructor): ReadonlyArray<ParamMetadata> {
|
|
706
|
-
const meta = this.#classes.constructorMetadata(target);
|
|
707
|
-
if (meta !== undefined) {
|
|
708
|
-
return meta.params;
|
|
709
|
-
}
|
|
710
|
-
if (target.length === 0) {
|
|
711
|
-
return EMPTY_PARAM_LIST;
|
|
712
|
-
}
|
|
713
|
-
throw new MissingMetadataError(target.name);
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
// One dispatch table for both dependency sources — constructor params and `toResolved`
|
|
717
|
-
// descriptors declare the same four things.
|
|
718
|
-
#resolveDeps(deps: ReadonlyArray<DependencySlot>, resolutionStack: Array<ResolutionFrame>): Array<unknown> {
|
|
719
|
-
const count = deps.length;
|
|
720
|
-
if (count === 0) {
|
|
721
|
-
return [];
|
|
722
|
-
}
|
|
723
|
-
if (count === 1) {
|
|
724
|
-
return [this.#resolveDep(deps[0]!, resolutionStack)];
|
|
725
|
-
}
|
|
726
|
-
const resolved = new Array<unknown>(count);
|
|
727
|
-
for (let index = 0; index < count; index += 1) {
|
|
728
|
-
resolved[index] = this.#resolveDep(deps[index]!, resolutionStack);
|
|
729
|
-
}
|
|
730
|
-
return resolved;
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
#resolveDep(dep: DependencySlot, resolutionStack: Array<ResolutionFrame>): unknown {
|
|
734
|
-
const options = resolveOptionsForSlot(dep);
|
|
735
|
-
if (dep.multi) {
|
|
736
|
-
return this.resolveAll(dep.token, options, resolutionStack);
|
|
737
|
-
}
|
|
738
|
-
if (dep.optional) {
|
|
739
|
-
return this.resolveOptional(dep.token, options, resolutionStack);
|
|
740
|
-
}
|
|
741
|
-
if (options === undefined) {
|
|
742
|
-
return this.resolveFromContext(dep.token, resolutionStack);
|
|
743
|
-
}
|
|
744
|
-
return this.resolve(dep.token, options, resolutionStack);
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
resolveOptional<Value>(
|
|
748
|
-
token: Token<Value> | Constructor<Value>,
|
|
749
|
-
options: ResolveOptions | undefined,
|
|
750
|
-
resolutionStack: Array<ResolutionFrame>,
|
|
751
|
-
): Value | undefined {
|
|
752
|
-
const entry = this.#findBinding(token, options, resolutionStack);
|
|
753
|
-
if (entry === undefined) {
|
|
754
|
-
return undefined;
|
|
755
|
-
}
|
|
756
|
-
// Resolve the entry the probe found: re-looking the token up would evaluate every `when()`
|
|
757
|
-
// predicate a second time, and a changed answer would throw where `undefined` was promised.
|
|
758
|
-
const { binding, owner } = entry;
|
|
759
|
-
if (binding.kind === "alias") {
|
|
760
|
-
return this.resolve(token, options, resolutionStack);
|
|
761
|
-
}
|
|
762
|
-
if (binding.scope === "singleton" && owner !== this) {
|
|
763
|
-
return owner.#resolveBinding(binding, options, resolutionStack, owner) as Value;
|
|
764
|
-
}
|
|
765
|
-
return this.#resolveBinding(binding, options, resolutionStack, owner) as Value;
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
resolveAll<Value>(
|
|
769
|
-
token: Token<Value> | Constructor<Value>,
|
|
770
|
-
options: ResolveOptions | undefined,
|
|
771
|
-
resolutionStack: Array<ResolutionFrame>,
|
|
772
|
-
): Array<Value> {
|
|
773
|
-
const candidates = this.#candidateBindings(token, options, resolutionStack);
|
|
774
|
-
const resolved = new Array<Value>(candidates.length);
|
|
775
|
-
for (let index = 0; index < candidates.length; index += 1) {
|
|
776
|
-
resolved[index] = this.#resolveCandidateSync(candidates[index]!, options, resolutionStack) as Value;
|
|
777
|
-
}
|
|
778
|
-
return resolved;
|
|
779
|
-
}
|
|
780
|
-
|
|
781
|
-
/** Every binding in the chain a `resolveAll` request matches, in chain order. */
|
|
782
|
-
#candidateBindings(
|
|
783
|
-
token: Token<unknown> | Constructor,
|
|
784
|
-
options: ResolveOptions | undefined,
|
|
785
|
-
resolutionStack: Array<ResolutionFrame>,
|
|
786
|
-
): ReadonlyArray<Binding> {
|
|
787
|
-
if (options !== undefined) {
|
|
788
|
-
const indexed = this.#indexedCandidates(token, options, resolutionStack);
|
|
789
|
-
if (indexed !== null) {
|
|
790
|
-
return indexed;
|
|
791
|
-
}
|
|
792
|
-
}
|
|
793
|
-
const allBindings = this.#allBindingsFromChain(token);
|
|
794
|
-
if (allBindings.length === 0) {
|
|
795
|
-
return allBindings;
|
|
796
|
-
}
|
|
797
|
-
return selectAllBindings(allBindings, options, this.#makeConstraintContext(resolutionStack, options));
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
// ── Async resolve ──────────────────────────────────────────────────────────────────────────────────────────────────
|
|
801
|
-
|
|
802
|
-
resolveAsyncFromContext<Value>(
|
|
803
|
-
token: Token<Value> | Constructor<Value>,
|
|
804
|
-
resolutionStack: Array<ResolutionFrame>,
|
|
805
|
-
branchDepth: BranchDepth,
|
|
806
|
-
): Promise<Value> {
|
|
807
|
-
// Hot lane: own-registry fast default (async chains resolve sibling dynamic bindings).
|
|
808
|
-
// Fall back to the chain-versioned memo only on miss or alias.
|
|
809
|
-
const fastBinding = this.#registry.getFastDefault(token);
|
|
810
|
-
if (fastBinding !== undefined && fastBinding.kind !== "alias") {
|
|
811
|
-
// Inline the dominant chain shape — transient dynamic factory with no activation.
|
|
812
|
-
if (
|
|
813
|
-
(fastBinding.kind === "dynamic-async" || fastBinding.kind === "dynamic") &&
|
|
814
|
-
fastBinding.scope === "transient" &&
|
|
815
|
-
!this.#hasAnyActivation(fastBinding)
|
|
816
|
-
) {
|
|
817
|
-
return this.#resolveTransientDynamicAsyncFromContext(
|
|
818
|
-
fastBinding,
|
|
819
|
-
resolutionStack,
|
|
820
|
-
branchDepth,
|
|
821
|
-
) as Promise<Value>;
|
|
822
|
-
}
|
|
823
|
-
return this.#resolveAsyncDefaultEntry(fastBinding, this, resolutionStack, branchDepth) as Promise<Value>;
|
|
824
|
-
}
|
|
825
|
-
const entry = this.#lookup.defaultEntry(token);
|
|
826
|
-
if (entry === null) {
|
|
827
|
-
return this.resolveAsync(token, undefined, resolutionStack, branchDepth);
|
|
828
|
-
}
|
|
829
|
-
return this.#resolveAsyncDefaultEntry(entry.binding, entry.owner, resolutionStack, branchDepth) as Promise<Value>;
|
|
830
|
-
}
|
|
831
|
-
|
|
832
|
-
#resolveAsyncDefaultEntry(
|
|
833
|
-
binding: Binding,
|
|
834
|
-
owner: DependencyResolver,
|
|
835
|
-
resolutionStack: Array<ResolutionFrame>,
|
|
836
|
-
branchDepth: BranchDepth,
|
|
837
|
-
): Promise<unknown> {
|
|
838
|
-
if (owner.#isPlainConstant(binding)) {
|
|
839
|
-
return Promise.resolve(binding.value);
|
|
840
|
-
}
|
|
841
|
-
const scope = binding.scope;
|
|
842
|
-
if (scope === "transient") {
|
|
843
|
-
if ((binding.kind === "dynamic" || binding.kind === "dynamic-async") && !owner.#hasAnyActivation(binding)) {
|
|
844
|
-
return this.#resolveTransientDynamicAsyncFromContext(binding, resolutionStack, branchDepth);
|
|
845
|
-
}
|
|
846
|
-
} else if (scope === "singleton") {
|
|
847
|
-
if (binding.instance !== NO_INSTANCE) {
|
|
848
|
-
return Promise.resolve(binding.instance);
|
|
849
|
-
}
|
|
850
|
-
if (owner !== this) {
|
|
851
|
-
return owner.#resolveBindingAsync(binding, undefined, resolutionStack, branchDepth, owner);
|
|
852
|
-
}
|
|
853
|
-
} else if (this.#scope.isChild) {
|
|
854
|
-
const cachedScoped = this.#scope.readScoped(binding.id);
|
|
855
|
-
if (cachedScoped !== SCOPED_MISS) {
|
|
856
|
-
return Promise.resolve(cachedScoped);
|
|
857
|
-
}
|
|
858
|
-
} else {
|
|
859
|
-
// Not `#readScoped`: this entry point reports failure as a rejection, never a sync throw.
|
|
860
|
-
return Promise.reject(new MissingScopeContextError(tokenName(binding.token)));
|
|
861
|
-
}
|
|
862
|
-
return this.#resolveBindingAsync(binding, undefined, resolutionStack, branchDepth, owner);
|
|
863
|
-
}
|
|
864
|
-
|
|
865
|
-
async resolveAsync<Value>(
|
|
866
|
-
token: Token<Value> | Constructor<Value>,
|
|
867
|
-
options: ResolveOptions | undefined,
|
|
868
|
-
resolutionStack: Array<ResolutionFrame>,
|
|
869
|
-
branchDepth: BranchDepth = UNOWNED_BRANCH,
|
|
870
|
-
): Promise<Value> {
|
|
871
|
-
const { binding, owner } = this.#requireBinding(token, options, resolutionStack);
|
|
872
|
-
|
|
873
|
-
if (binding.scope === "singleton" && owner !== this) {
|
|
874
|
-
return owner.#resolveBindingAsync(binding, options, resolutionStack, branchDepth, owner) as Promise<Value>;
|
|
875
|
-
}
|
|
876
|
-
return this.#resolveBindingAsync(binding, options, resolutionStack, branchDepth, owner) as Promise<Value>;
|
|
877
|
-
}
|
|
878
|
-
|
|
879
|
-
async #resolveBindingAsync(
|
|
880
|
-
binding: Binding,
|
|
881
|
-
options: ResolveOptions | undefined,
|
|
882
|
-
resolutionStack: Array<ResolutionFrame>,
|
|
883
|
-
branchDepth: BranchDepth,
|
|
884
|
-
owner: DependencyResolver,
|
|
885
|
-
): Promise<unknown> {
|
|
886
|
-
if (owner.#isPlainConstant(binding)) {
|
|
887
|
-
return binding.value;
|
|
888
|
-
}
|
|
889
|
-
|
|
890
|
-
const scope = binding.scope;
|
|
891
|
-
if (scope === "singleton") {
|
|
892
|
-
if (binding.instance !== NO_INSTANCE) {
|
|
893
|
-
return binding.instance;
|
|
894
|
-
}
|
|
895
|
-
// In-flight dedup: concurrent callers share the first creation.
|
|
896
|
-
const inflight = this.#scope.getInflight(binding.id);
|
|
897
|
-
if (inflight !== undefined) {
|
|
898
|
-
return inflight;
|
|
899
|
-
}
|
|
900
|
-
if (this.#scope.isClosed) {
|
|
901
|
-
throw new DisposedContainerError();
|
|
902
|
-
}
|
|
903
|
-
} else if (scope === "scoped") {
|
|
904
|
-
const cachedScoped = this.#readScoped(binding);
|
|
905
|
-
if (cachedScoped !== SCOPED_MISS) {
|
|
906
|
-
return cachedScoped;
|
|
907
|
-
}
|
|
908
|
-
// In-flight dedup, scoped flavor: one instance per scope even under concurrency.
|
|
909
|
-
const inflight = this.#scope.getInflight(binding.id);
|
|
910
|
-
if (inflight !== undefined) {
|
|
911
|
-
return inflight;
|
|
912
|
-
}
|
|
913
|
-
if (this.#scope.isClosed) {
|
|
914
|
-
throw new DisposedContainerError();
|
|
915
|
-
}
|
|
916
|
-
}
|
|
917
|
-
|
|
918
|
-
const frame = this.#getResolutionFrame(binding);
|
|
919
|
-
// This level appends to its own branch and never unwinds.
|
|
920
|
-
const levelStack = extendResolutionBranch(resolutionStack, branchDepth, frame);
|
|
921
|
-
const levelDepth = branchDepthOf(levelStack);
|
|
922
|
-
|
|
923
|
-
const needsActivation = owner.#activation.needsActivation(binding);
|
|
924
|
-
if (!needsActivation && scope === "transient" && (binding.kind === "dynamic" || binding.kind === "dynamic-async")) {
|
|
925
|
-
const resolutionCtx = new AsyncLevelContext(this, levelStack, options);
|
|
926
|
-
if (binding.kind === "dynamic-async") {
|
|
927
|
-
return await binding.factory(resolutionCtx);
|
|
928
|
-
}
|
|
929
|
-
const dynamicResult = binding.factory(resolutionCtx);
|
|
930
|
-
return dynamicResult instanceof Promise ? await dynamicResult : dynamicResult;
|
|
931
|
-
}
|
|
932
|
-
|
|
933
|
-
const resolutionCtx =
|
|
934
|
-
needsActivation || requiresResolutionContext(binding)
|
|
935
|
-
? new AsyncLevelContext(this, levelStack, options)
|
|
936
|
-
: undefined;
|
|
937
|
-
|
|
938
|
-
if (scope === "singleton") {
|
|
939
|
-
// The promise is published before it settles, so concurrent callers dedup onto it.
|
|
940
|
-
const singletonPromise = this.#instantiateAndActivateAsync(
|
|
941
|
-
binding,
|
|
942
|
-
resolutionCtx,
|
|
943
|
-
levelStack,
|
|
944
|
-
levelDepth,
|
|
945
|
-
needsActivation,
|
|
946
|
-
owner,
|
|
947
|
-
).then(
|
|
948
|
-
(activated) => {
|
|
949
|
-
this.#scope.setSingleton(binding, activated);
|
|
950
|
-
this.#scope.clearInflight(binding.id);
|
|
951
|
-
return activated;
|
|
952
|
-
},
|
|
953
|
-
(error: unknown) => {
|
|
954
|
-
this.#scope.clearInflight(binding.id);
|
|
955
|
-
throw error;
|
|
956
|
-
},
|
|
957
|
-
);
|
|
958
|
-
this.#scope.setInflight(binding.id, singletonPromise as Promise<unknown>);
|
|
959
|
-
return await singletonPromise;
|
|
960
|
-
}
|
|
961
|
-
|
|
962
|
-
if (scope === "scoped") {
|
|
963
|
-
// Published before it settles, like the singleton lane: concurrent callers share one creation.
|
|
964
|
-
const scopedPromise = this.#instantiateAndActivateAsync(
|
|
965
|
-
binding,
|
|
966
|
-
resolutionCtx,
|
|
967
|
-
levelStack,
|
|
968
|
-
levelDepth,
|
|
969
|
-
needsActivation,
|
|
970
|
-
owner,
|
|
971
|
-
).then(
|
|
972
|
-
(activated) => {
|
|
973
|
-
this.#scope.setScoped(binding, activated);
|
|
974
|
-
this.#scope.clearInflight(binding.id);
|
|
975
|
-
return activated;
|
|
976
|
-
},
|
|
977
|
-
(error: unknown) => {
|
|
978
|
-
this.#scope.clearInflight(binding.id);
|
|
979
|
-
throw error;
|
|
980
|
-
},
|
|
981
|
-
);
|
|
982
|
-
this.#scope.setInflight(binding.id, scopedPromise as Promise<unknown>);
|
|
983
|
-
return await scopedPromise;
|
|
984
|
-
}
|
|
985
|
-
|
|
986
|
-
return await this.#instantiateAndActivateAsync(
|
|
987
|
-
binding,
|
|
988
|
-
resolutionCtx,
|
|
989
|
-
levelStack,
|
|
990
|
-
levelDepth,
|
|
991
|
-
needsActivation,
|
|
992
|
-
owner,
|
|
993
|
-
);
|
|
994
|
-
}
|
|
995
|
-
|
|
996
|
-
/**
|
|
997
|
-
* Settles this resolver's own `postConstruct` answer for a class binding a parent owns.
|
|
998
|
-
*
|
|
999
|
-
* @remarks The owner discovers it on first instantiation, but the plan compiler reads the
|
|
1000
|
-
* introspector of whoever is resolving — left unknown, that resolver refuses to compile a plan for
|
|
1001
|
-
* this binding on every call, forever.
|
|
1002
|
-
*/
|
|
1003
|
-
#mirrorPostConstructFromOwner(binding: Binding, owner: DependencyResolver): void {
|
|
1004
|
-
if (owner !== this && binding.kind === "class" && this.#classes.knownPostConstruct(binding.target) === undefined) {
|
|
1005
|
-
this.#classes.discoverPostConstruct(binding.target);
|
|
1006
|
-
}
|
|
1007
|
-
}
|
|
1008
|
-
|
|
1009
|
-
async #instantiateAndActivateAsync(
|
|
1010
|
-
binding: Binding,
|
|
1011
|
-
ctx: AsyncLevelContext | undefined,
|
|
1012
|
-
resolutionStack: Array<ResolutionFrame>,
|
|
1013
|
-
branchDepth: BranchDepth,
|
|
1014
|
-
needsActivation: boolean,
|
|
1015
|
-
owner: DependencyResolver,
|
|
1016
|
-
): Promise<unknown> {
|
|
1017
|
-
const instance = await this.#instantiateAsync(binding, ctx, resolutionStack, branchDepth);
|
|
1018
|
-
this.#mirrorPostConstructFromOwner(binding, owner);
|
|
1019
|
-
if (!owner.#activation.refreshAfterFirstInstantiation(binding, needsActivation)) {
|
|
1020
|
-
return instance;
|
|
1021
|
-
}
|
|
1022
|
-
return owner.#lifecycle.runActivation(ctx as AsyncLevelContext, binding, instance, owner.#metadataReader);
|
|
1023
|
-
}
|
|
1024
|
-
|
|
1025
|
-
async #instantiateAsync(
|
|
1026
|
-
binding: Binding,
|
|
1027
|
-
ctx: AsyncLevelContext | undefined,
|
|
1028
|
-
resolutionStack: Array<ResolutionFrame>,
|
|
1029
|
-
branchDepth: BranchDepth,
|
|
1030
|
-
): Promise<unknown> {
|
|
1031
|
-
switch (binding.kind) {
|
|
1032
|
-
case "constant":
|
|
1033
|
-
return binding.value;
|
|
1034
|
-
|
|
1035
|
-
case "dynamic": {
|
|
1036
|
-
if (ctx === undefined) {
|
|
1037
|
-
throw new InternalError("dynamic binding requires resolution context");
|
|
1038
|
-
}
|
|
1039
|
-
const factoryResult = binding.factory(ctx);
|
|
1040
|
-
return factoryResult instanceof Promise ? factoryResult : Promise.resolve(factoryResult);
|
|
1041
|
-
}
|
|
1042
|
-
|
|
1043
|
-
case "dynamic-async":
|
|
1044
|
-
if (ctx === undefined) {
|
|
1045
|
-
throw new InternalError("dynamic-async binding requires resolution context");
|
|
1046
|
-
}
|
|
1047
|
-
return binding.factory(ctx);
|
|
1048
|
-
|
|
1049
|
-
case "class": {
|
|
1050
|
-
const deps = await this.#resolveDepsAsync(
|
|
1051
|
-
this.#constructorParams(binding.target),
|
|
1052
|
-
resolutionStack,
|
|
1053
|
-
branchDepth,
|
|
1054
|
-
);
|
|
1055
|
-
// Accessor initializers resolve synchronously, so the branch-owned path serves them directly.
|
|
1056
|
-
return this.#classes.instantiate(
|
|
1057
|
-
binding.target,
|
|
1058
|
-
deps,
|
|
1059
|
-
this.#classes.needsActiveContainer(binding.target) ? this.#ambientResolutionFor(resolutionStack) : undefined,
|
|
1060
|
-
);
|
|
1061
|
-
}
|
|
1062
|
-
|
|
1063
|
-
case "resolved": {
|
|
1064
|
-
const deps = await this.#resolveDepsAsync(binding.deps, resolutionStack, branchDepth);
|
|
1065
|
-
const factoryResult = binding.factory(...deps);
|
|
1066
|
-
return factoryResult instanceof Promise ? factoryResult : Promise.resolve(factoryResult);
|
|
1067
|
-
}
|
|
1068
|
-
|
|
1069
|
-
case "resolved-async": {
|
|
1070
|
-
const deps = await this.#resolveDepsAsync(binding.deps, resolutionStack, branchDepth);
|
|
1071
|
-
return binding.factory(...deps);
|
|
1072
|
-
}
|
|
1073
|
-
|
|
1074
|
-
case "alias":
|
|
1075
|
-
throw new InternalError("alias should have been followed before instantiation");
|
|
1076
|
-
}
|
|
1077
|
-
}
|
|
1078
|
-
|
|
1079
|
-
async #resolveDepsAsync(
|
|
1080
|
-
deps: ReadonlyArray<DependencySlot>,
|
|
1081
|
-
resolutionStack: Array<ResolutionFrame>,
|
|
1082
|
-
branchDepth: BranchDepth,
|
|
1083
|
-
): Promise<Array<unknown>> {
|
|
1084
|
-
const count = deps.length;
|
|
1085
|
-
if (count === 0) {
|
|
1086
|
-
return [];
|
|
1087
|
-
}
|
|
1088
|
-
if (count === 1) {
|
|
1089
|
-
return [await this.#resolveDepAsync(deps[0]!, resolutionStack, branchDepth)];
|
|
1090
|
-
}
|
|
1091
|
-
// Siblings resolve concurrently and each extends the same branch, so the first appends in
|
|
1092
|
-
// place and the rest copy the prefix — no caller has to isolate them.
|
|
1093
|
-
const pending = new Array<Promise<unknown>>(count);
|
|
1094
|
-
for (let index = 0; index < count; index += 1) {
|
|
1095
|
-
pending[index] = this.#resolveDepAsync(deps[index]!, resolutionStack, branchDepth);
|
|
1096
|
-
}
|
|
1097
|
-
return Promise.all(pending);
|
|
1098
|
-
}
|
|
1099
|
-
|
|
1100
|
-
#resolveDepAsync(
|
|
1101
|
-
dep: DependencySlot,
|
|
1102
|
-
resolutionStack: Array<ResolutionFrame>,
|
|
1103
|
-
branchDepth: BranchDepth,
|
|
1104
|
-
): Promise<unknown> {
|
|
1105
|
-
const options = resolveOptionsForSlot(dep);
|
|
1106
|
-
if (dep.multi) {
|
|
1107
|
-
return this.resolveAllAsync(dep.token, options, resolutionStack, branchDepth);
|
|
1108
|
-
}
|
|
1109
|
-
if (dep.optional) {
|
|
1110
|
-
return this.resolveOptionalAsync(dep.token, options, resolutionStack, branchDepth);
|
|
1111
|
-
}
|
|
1112
|
-
if (options === undefined) {
|
|
1113
|
-
return this.resolveAsyncFromContext(dep.token, resolutionStack, branchDepth);
|
|
1114
|
-
}
|
|
1115
|
-
return this.resolveAsync(dep.token, options, resolutionStack, branchDepth);
|
|
1116
|
-
}
|
|
1117
|
-
|
|
1118
|
-
async resolveOptionalAsync<Value>(
|
|
1119
|
-
token: Token<Value> | Constructor<Value>,
|
|
1120
|
-
options: ResolveOptions | undefined,
|
|
1121
|
-
resolutionStack: Array<ResolutionFrame>,
|
|
1122
|
-
branchDepth: BranchDepth = UNOWNED_BRANCH,
|
|
1123
|
-
): Promise<Value | undefined> {
|
|
1124
|
-
const entry = this.#findBinding(token, options, resolutionStack);
|
|
1125
|
-
if (entry === undefined) {
|
|
1126
|
-
return undefined;
|
|
1127
|
-
}
|
|
1128
|
-
// Same single-evaluation contract as the sync lane: resolve what the probe found.
|
|
1129
|
-
const { binding, owner } = entry;
|
|
1130
|
-
if (binding.kind === "alias") {
|
|
1131
|
-
return this.resolveAsync(token, options, resolutionStack, branchDepth);
|
|
1132
|
-
}
|
|
1133
|
-
if (binding.scope === "singleton" && owner !== this) {
|
|
1134
|
-
return owner.#resolveBindingAsync(binding, options, resolutionStack, branchDepth, owner) as Promise<Value>;
|
|
1135
|
-
}
|
|
1136
|
-
return this.#resolveBindingAsync(binding, options, resolutionStack, branchDepth, owner) as Promise<Value>;
|
|
1137
|
-
}
|
|
1138
|
-
|
|
1139
|
-
async resolveAllAsync<Value>(
|
|
1140
|
-
token: Token<Value> | Constructor<Value>,
|
|
1141
|
-
options: ResolveOptions | undefined,
|
|
1142
|
-
resolutionStack: Array<ResolutionFrame>,
|
|
1143
|
-
branchDepth: BranchDepth = UNOWNED_BRANCH,
|
|
1144
|
-
): Promise<Array<Value>> {
|
|
1145
|
-
const candidates = this.#candidateBindings(token, options, resolutionStack);
|
|
1146
|
-
const pending = new Array<Promise<Value>>(candidates.length);
|
|
1147
|
-
for (let index = 0; index < candidates.length; index += 1) {
|
|
1148
|
-
pending[index] = this.#resolveCandidateAsync(
|
|
1149
|
-
candidates[index]!,
|
|
1150
|
-
options,
|
|
1151
|
-
resolutionStack,
|
|
1152
|
-
branchDepth,
|
|
1153
|
-
) as Promise<Value>;
|
|
1154
|
-
}
|
|
1155
|
-
return Promise.all(pending);
|
|
1156
|
-
}
|
|
1157
|
-
|
|
1158
|
-
// ── Helpers ────────────────────────────────────────────────────────────────────────────────────────────────────────
|
|
1159
|
-
|
|
1160
|
-
#allBindingsFromChain(token: Token<unknown> | Constructor): ReadonlyArray<Binding> {
|
|
1161
|
-
const ownBindings = this.#registry.getAll(token);
|
|
1162
|
-
if (this.#parent === undefined) {
|
|
1163
|
-
return ownBindings;
|
|
1164
|
-
}
|
|
1165
|
-
const result: Array<Binding> = [...ownBindings];
|
|
1166
|
-
for (let current: DependencyResolver | undefined = this.#parent; current !== undefined; current = current.#parent) {
|
|
1167
|
-
const own = current.#registry.getAll(token);
|
|
1168
|
-
if (own.length > 0) {
|
|
1169
|
-
result.push(...own);
|
|
1170
|
-
}
|
|
1171
|
-
}
|
|
1172
|
-
return result;
|
|
1173
|
-
}
|
|
1174
|
-
|
|
1175
|
-
/** Every binding the chain's name indexes hold for one name, nearest container first. */
|
|
1176
|
-
#namedBindingsFromChain(token: Token<unknown> | Constructor, name: string): Array<Binding> {
|
|
1177
|
-
// A name resolves to at most one binding per registry, so a root container's answer is built
|
|
1178
|
-
// whole rather than grown — the list is sized at its allocation.
|
|
1179
|
-
const ownBinding = this.#registry.getSimpleNamed(token, name);
|
|
1180
|
-
if (this.#parent === undefined) {
|
|
1181
|
-
return ownBinding === undefined ? [] : [ownBinding];
|
|
1182
|
-
}
|
|
1183
|
-
const result: Array<Binding> = ownBinding === undefined ? [] : [ownBinding];
|
|
1184
|
-
for (let current: DependencyResolver | undefined = this.#parent; current !== undefined; current = current.#parent) {
|
|
1185
|
-
const binding = current.#registry.getSimpleNamed(token, name);
|
|
1186
|
-
if (binding !== undefined) {
|
|
1187
|
-
result.push(binding);
|
|
1188
|
-
}
|
|
1189
|
-
}
|
|
1190
|
-
return result;
|
|
1191
|
-
}
|
|
1192
|
-
|
|
1193
|
-
/**
|
|
1194
|
-
* The candidates an index can name outright, or `null` when the request needs full selection.
|
|
1195
|
-
*
|
|
1196
|
-
* @remarks Kept off `#candidateBindings` so that method stays the size it was: a request neither
|
|
1197
|
-
* index serves must not pay for the two that do.
|
|
1198
|
-
* An index has matched the slot already, but a hit may still carry a predicate, and evaluating
|
|
1199
|
-
* that is the selection path's job.
|
|
1200
|
-
*/
|
|
1201
|
-
#indexedCandidates(
|
|
1202
|
-
token: Token<unknown> | Constructor,
|
|
1203
|
-
options: ResolveOptions,
|
|
1204
|
-
resolutionStack: Array<ResolutionFrame>,
|
|
1205
|
-
): ReadonlyArray<Binding> | null {
|
|
1206
|
-
if (isNameOnlyOptions(options)) {
|
|
1207
|
-
const named = this.#namedBindingsFromChain(token, options.name);
|
|
1208
|
-
return anyPredicate(named)
|
|
1209
|
-
? selectAllBindings(named, options, this.#makeConstraintContext(resolutionStack, options))
|
|
1210
|
-
: named;
|
|
1211
|
-
}
|
|
1212
|
-
const singleTag = singleTagOnlyOf(options);
|
|
1213
|
-
if (singleTag === undefined) {
|
|
1214
|
-
return null;
|
|
1215
|
-
}
|
|
1216
|
-
const tagged = this.#taggedBindingsFromChain(token, singleTag);
|
|
1217
|
-
return anyPredicate(tagged)
|
|
1218
|
-
? selectAllBindings(tagged, options, this.#makeConstraintContext(resolutionStack, options))
|
|
1219
|
-
: tagged;
|
|
1220
|
-
}
|
|
1221
|
-
|
|
1222
|
-
/**
|
|
1223
|
-
* Every binding the chain's tag indexes hold for one tag, nearest container first.
|
|
1224
|
-
*
|
|
1225
|
-
* @remarks A request for one tag and no name matches exactly the bindings the index keys, so this
|
|
1226
|
-
* is the whole candidate set rather than a prefilter — a named or multi-tag slot cannot satisfy it.
|
|
1227
|
-
*/
|
|
1228
|
-
#taggedBindingsFromChain(token: Token<unknown> | Constructor, tag: BindingTag): Array<Binding> {
|
|
1229
|
-
// A tag matches at most one binding per registry, so a root container's answer is built whole
|
|
1230
|
-
// rather than grown — the shape `#namedBindingsFromChain` takes, for the same reason.
|
|
1231
|
-
const ownBinding = this.#registry.getSimpleTagged(token, tag);
|
|
1232
|
-
if (this.#parent === undefined) {
|
|
1233
|
-
return ownBinding === undefined ? [] : [ownBinding];
|
|
1234
|
-
}
|
|
1235
|
-
const result: Array<Binding> = ownBinding === undefined ? [] : [ownBinding];
|
|
1236
|
-
for (let current: DependencyResolver | undefined = this.#parent; current !== undefined; current = current.#parent) {
|
|
1237
|
-
const binding = current.#registry.getSimpleTagged(token, tag);
|
|
1238
|
-
if (binding !== undefined) {
|
|
1239
|
-
result.push(binding);
|
|
1240
|
-
}
|
|
1241
|
-
}
|
|
1242
|
-
return result;
|
|
1243
|
-
}
|
|
1244
|
-
|
|
1245
|
-
/** A constant with no activation anywhere resolves to its value with no pipeline at all. */
|
|
1246
|
-
#isPlainConstant(binding: Binding): binding is ConstantBinding<unknown> {
|
|
1247
|
-
return (
|
|
1248
|
-
binding.kind === "constant" &&
|
|
1249
|
-
binding.onActivation === undefined &&
|
|
1250
|
-
(this.#lifecycle.activationVersion === 0 || !this.#lifecycle.hasActivationHandlers(binding.token))
|
|
1251
|
-
);
|
|
1252
|
-
}
|
|
1253
|
-
|
|
1254
|
-
/** Whether either an own hook or a container-level hook would run for this binding. */
|
|
1255
|
-
#hasAnyActivation(binding: DynamicBinding<unknown> | DynamicAsyncBinding<unknown>): boolean {
|
|
1256
|
-
if (binding.onActivation !== undefined) {
|
|
1257
|
-
return true;
|
|
1258
|
-
}
|
|
1259
|
-
return this.#lifecycle.activationVersion !== 0 && this.#lifecycle.hasActivationHandlers(binding.token);
|
|
1260
|
-
}
|
|
1261
|
-
|
|
1262
|
-
/**
|
|
1263
|
-
* The cached instance of a `scoped` binding, or {@link SCOPED_MISS}.
|
|
1264
|
-
*
|
|
1265
|
-
* @remarks A `scoped` binding outside a child container is a configuration error, not a miss, so
|
|
1266
|
-
* the check lives with the read that depends on it.
|
|
1267
|
-
*/
|
|
1268
|
-
#readScoped(binding: Binding): unknown {
|
|
1269
|
-
if (!this.#scope.isChild) {
|
|
1270
|
-
throw new MissingScopeContextError(tokenName(binding.token));
|
|
1271
|
-
}
|
|
1272
|
-
return this.#scope.readScoped(binding.id);
|
|
1273
|
-
}
|
|
1274
|
-
|
|
1275
|
-
// The shared root context answers every top-level request; building one is the rarer half and
|
|
1276
|
-
// lives outside, so what a selection inlines is the test and not the literal.
|
|
1277
|
-
#makeConstraintContext(
|
|
1278
|
-
resolutionStack: Array<ResolutionFrame>,
|
|
1279
|
-
options: ResolveOptions | undefined,
|
|
1280
|
-
): ConstraintContext {
|
|
1281
|
-
if (options === undefined && resolutionStack.length === 0) {
|
|
1282
|
-
return ROOT_CONSTRAINT_CONTEXT;
|
|
1283
|
-
}
|
|
1284
|
-
return buildConstraintContext(resolutionStack, options);
|
|
1285
|
-
}
|
|
1286
|
-
|
|
1287
|
-
/** Selection for a name-less multi-tag request, over the union of the two tag indexes. */
|
|
1288
|
-
#selectMultiTagged(
|
|
1289
|
-
token: Token<unknown> | Constructor,
|
|
1290
|
-
options: ResolveOptions,
|
|
1291
|
-
resolutionStack: Array<ResolutionFrame>,
|
|
1292
|
-
): Binding | undefined {
|
|
1293
|
-
const candidates: Array<Binding> = [];
|
|
1294
|
-
this.#gatherTagCandidates(token, options.tag, candidates);
|
|
1295
|
-
const listed = options.tags;
|
|
1296
|
-
if (listed !== undefined) {
|
|
1297
|
-
for (let index = 0; index < listed.length; index += 1) {
|
|
1298
|
-
this.#gatherTagCandidates(token, listed[index], candidates);
|
|
1299
|
-
}
|
|
1300
|
-
}
|
|
1301
|
-
if (candidates.length === 0) {
|
|
1302
|
-
return undefined;
|
|
1303
|
-
}
|
|
1304
|
-
return selectBinding(candidates, options, this.#makeConstraintContext(resolutionStack, options), tokenName(token));
|
|
1305
|
-
}
|
|
1306
|
-
|
|
1307
|
-
/** One request criterion's candidates: its exact single-tag binding, plus its first-tag bucket. */
|
|
1308
|
-
#gatherTagCandidates(
|
|
1309
|
-
token: Token<unknown> | Constructor,
|
|
1310
|
-
criterion: BindingTag | undefined,
|
|
1311
|
-
out: Array<Binding>,
|
|
1312
|
-
): void {
|
|
1313
|
-
if (criterion === undefined) {
|
|
1314
|
-
return;
|
|
1315
|
-
}
|
|
1316
|
-
// The includes probes only guard a request repeating a criterion across its two spellings.
|
|
1317
|
-
const single = this.#registry.getSimpleTagged(token, criterion);
|
|
1318
|
-
if (single !== undefined && !out.includes(single)) {
|
|
1319
|
-
out.push(single);
|
|
1320
|
-
}
|
|
1321
|
-
const bucket = this.#registry.getMultiTagged(token, criterion);
|
|
1322
|
-
if (bucket !== undefined) {
|
|
1323
|
-
for (let index = 0; index < bucket.length; index += 1) {
|
|
1324
|
-
const candidate = bucket[index]!;
|
|
1325
|
-
if (!out.includes(candidate)) {
|
|
1326
|
-
out.push(candidate);
|
|
1327
|
-
}
|
|
1328
|
-
}
|
|
1329
|
-
}
|
|
1330
|
-
}
|
|
1331
|
-
|
|
1332
|
-
#matchesBindingFast(
|
|
1333
|
-
binding: Binding,
|
|
1334
|
-
options: ResolveOptions | undefined,
|
|
1335
|
-
resolutionStack: Array<ResolutionFrame>,
|
|
1336
|
-
): boolean {
|
|
1337
|
-
if (!matchesSlot(binding.slot, options)) {
|
|
1338
|
-
return false;
|
|
1339
|
-
}
|
|
1340
|
-
return this.#satisfiesPredicate(binding, options, resolutionStack);
|
|
1341
|
-
}
|
|
1342
|
-
|
|
1343
|
-
/** The predicate half of a match, for a lane whose index has already settled the slot. */
|
|
1344
|
-
#satisfiesPredicate(
|
|
1345
|
-
binding: Binding,
|
|
1346
|
-
options: ResolveOptions | undefined,
|
|
1347
|
-
resolutionStack: Array<ResolutionFrame>,
|
|
1348
|
-
): boolean {
|
|
1349
|
-
const predicate = binding.predicate;
|
|
1350
|
-
if (predicate === undefined) {
|
|
1351
|
-
return true;
|
|
1352
|
-
}
|
|
1353
|
-
return predicate(this.#makeConstraintContext(resolutionStack, options));
|
|
1354
|
-
}
|
|
1355
|
-
|
|
1356
|
-
#resolveTransientDynamicSyncFromContext(
|
|
1357
|
-
binding: DynamicBinding<unknown>,
|
|
1358
|
-
resolutionStack: Array<ResolutionFrame>,
|
|
1359
|
-
): unknown {
|
|
1360
|
-
// One lane at every depth: `binding.inFlight` is O(1), so there is nothing to escape.
|
|
1361
|
-
const frame = this.#getResolutionFrame(binding);
|
|
1362
|
-
const tokenDisplayName = frame.tokenName;
|
|
1363
|
-
if (binding.inFlight) {
|
|
1364
|
-
throw new CircularDependencyError(cycleNamesOf(resolutionStack, tokenDisplayName));
|
|
1365
|
-
}
|
|
1366
|
-
binding.inFlight = true;
|
|
1367
|
-
resolutionStack.push(frame);
|
|
1368
|
-
const resolutionCtx = this.#acquireSyncResolutionContext(resolutionStack, undefined);
|
|
1369
|
-
try {
|
|
1370
|
-
const dynamicResult = binding.factory(resolutionCtx);
|
|
1371
|
-
if (dynamicResult instanceof Promise) {
|
|
1372
|
-
throw new AsyncResolutionError(resolutionStack[0]?.tokenName ?? tokenDisplayName, tokenDisplayName);
|
|
1373
|
-
}
|
|
1374
|
-
return dynamicResult;
|
|
1375
|
-
} finally {
|
|
1376
|
-
resolutionStack.pop();
|
|
1377
|
-
binding.inFlight = false;
|
|
1378
|
-
}
|
|
1379
|
-
}
|
|
1380
|
-
|
|
1381
|
-
// Deliberately not `async`: that would allocate a state machine and a promise per level.
|
|
1382
|
-
#resolveTransientDynamicAsyncFromContext(
|
|
1383
|
-
binding: DynamicBinding<unknown> | DynamicAsyncBinding<unknown>,
|
|
1384
|
-
resolutionStack: Array<ResolutionFrame>,
|
|
1385
|
-
branchDepth: BranchDepth,
|
|
1386
|
-
): Promise<unknown> {
|
|
1387
|
-
const frame = this.#getResolutionFrame(binding);
|
|
1388
|
-
let levelStack: OwnedBranchStack;
|
|
1389
|
-
try {
|
|
1390
|
-
levelStack = extendResolutionBranch(resolutionStack, branchDepth, frame);
|
|
1391
|
-
} catch (cycleError) {
|
|
1392
|
-
// This method is not `async`; keep failures as rejections rather than sync throws.
|
|
1393
|
-
return Promise.reject(cycleError);
|
|
1394
|
-
}
|
|
1395
|
-
|
|
1396
|
-
// Nothing this level appended is ever removed, so no level observes its own settlement.
|
|
1397
|
-
const ctx = new AsyncLevelContext(this, levelStack, undefined);
|
|
1398
|
-
try {
|
|
1399
|
-
if (binding.kind === "dynamic-async") {
|
|
1400
|
-
return binding.factory(ctx);
|
|
1401
|
-
}
|
|
1402
|
-
const factoryResult = binding.factory(ctx);
|
|
1403
|
-
return factoryResult instanceof Promise ? factoryResult : Promise.resolve(factoryResult);
|
|
1404
|
-
} catch (factoryError) {
|
|
1405
|
-
return Promise.reject(factoryError);
|
|
1406
|
-
}
|
|
1407
|
-
}
|
|
1408
|
-
|
|
1409
|
-
// ── The cascade lane ───────────────────────────────────────────────────────────────────────────────────────────────
|
|
1410
|
-
|
|
1411
|
-
/**
|
|
1412
|
-
* Entry for a request a factory makes from inside an open synchronous cascade.
|
|
1413
|
-
*
|
|
1414
|
-
* @remarks A request arriving with no cascade open came out of a continuation, so its ancestors
|
|
1415
|
-
* are on no call stack — it escapes to the branch lane.
|
|
1416
|
-
*/
|
|
1417
|
-
resolveAsyncFromCascade(token: Token<unknown> | Constructor): Promise<unknown> {
|
|
1418
|
-
if (this.#cascadeStack.length === 0) {
|
|
1419
|
-
return this.resolveAsyncFromContext(token, [], ROOT_BRANCH);
|
|
1420
|
-
}
|
|
1421
|
-
return this.#dispatchCascade(token);
|
|
1422
|
-
}
|
|
1423
|
-
|
|
1424
|
-
/** Entry for a resolve the container starts, which opens the cascade rather than joining one. */
|
|
1425
|
-
resolveAsyncFromRoot(token: Token<unknown> | Constructor): Promise<unknown> {
|
|
1426
|
-
return this.#dispatchCascade(token);
|
|
1427
|
-
}
|
|
1428
|
-
|
|
1429
|
-
#dispatchCascade(token: Token<unknown> | Constructor): Promise<unknown> {
|
|
1430
|
-
const fastBinding = this.#registry.getFastDefault(token);
|
|
1431
|
-
if (fastBinding !== undefined) {
|
|
1432
|
-
if (
|
|
1433
|
-
(fastBinding.kind === "dynamic-async" || fastBinding.kind === "dynamic") &&
|
|
1434
|
-
fastBinding.scope === "transient" &&
|
|
1435
|
-
!this.#hasAnyActivation(fastBinding)
|
|
1436
|
-
) {
|
|
1437
|
-
return this.#resolveTransientDynamicAsyncCascade(fastBinding);
|
|
1438
|
-
}
|
|
1439
|
-
// A value that already exists answers here: escaping would snapshot the cascade for a resolve
|
|
1440
|
-
// that never looks at a path.
|
|
1441
|
-
if (this.#isPlainConstant(fastBinding)) {
|
|
1442
|
-
return Promise.resolve(fastBinding.value);
|
|
1443
|
-
}
|
|
1444
|
-
if (fastBinding.scope === "singleton" && fastBinding.instance !== NO_INSTANCE) {
|
|
1445
|
-
return Promise.resolve(fastBinding.instance);
|
|
1446
|
-
}
|
|
1447
|
-
const planned = this.#plannedCascadeAnswer(fastBinding);
|
|
1448
|
-
if (planned !== null) {
|
|
1449
|
-
return planned;
|
|
1450
|
-
}
|
|
1451
|
-
}
|
|
1452
|
-
// Anything else leaves the cascade lane for good, seeded with a snapshot of the ancestors it
|
|
1453
|
-
// accumulated — so a cycle across the boundary is still on one path.
|
|
1454
|
-
return this.resolveAsyncFromContext(token, [...this.#cascadeStack], UNOWNED_BRANCH);
|
|
1455
|
-
}
|
|
1456
|
-
|
|
1457
|
-
/**
|
|
1458
|
-
* A statically-visible transient graph at a true root answers from its compiled async plan.
|
|
1459
|
-
*
|
|
1460
|
-
* @remarks Kept out of the dispatcher so its size stays inlinable. Inside an open cascade the
|
|
1461
|
-
* graph must escape instead, so its escapes carry the live ancestors — hence the idle gate.
|
|
1462
|
-
*/
|
|
1463
|
-
#plannedCascadeAnswer(fastBinding: Binding): Promise<unknown> | null {
|
|
1464
|
-
if (
|
|
1465
|
-
this.#cascadeStack.length !== 0 ||
|
|
1466
|
-
fastBinding.scope !== "transient" ||
|
|
1467
|
-
(fastBinding.kind !== "class" && fastBinding.kind !== "resolved" && fastBinding.kind !== "resolved-async")
|
|
1468
|
-
) {
|
|
1469
|
-
return null;
|
|
1470
|
-
}
|
|
1471
|
-
const plan = this.#getAsyncInstantiationPlan(fastBinding);
|
|
1472
|
-
if (plan === null) {
|
|
1473
|
-
return null;
|
|
1474
|
-
}
|
|
1475
|
-
try {
|
|
1476
|
-
const planned = plan();
|
|
1477
|
-
return planned instanceof Promise ? planned : Promise.resolve(planned);
|
|
1478
|
-
} catch (planError) {
|
|
1479
|
-
// The interpreted lane is async, so a sync throw is a rejection there too.
|
|
1480
|
-
return Promise.reject(planError);
|
|
1481
|
-
}
|
|
1482
|
-
}
|
|
1483
|
-
|
|
1484
|
-
#resolveTransientDynamicAsyncCascade(
|
|
1485
|
-
binding: DynamicBinding<unknown> | DynamicAsyncBinding<unknown>,
|
|
1486
|
-
): Promise<unknown> {
|
|
1487
|
-
const frame = this.#getResolutionFrame(binding);
|
|
1488
|
-
// The request that closes a cycle is made from a factory's synchronous prefix, and synchronous
|
|
1489
|
-
// code does not interleave — so the O(1) flag is exact path membership here, as it is for the
|
|
1490
|
-
// sync lane. It is cleared when the factory returns its promise, not when that promise settles.
|
|
1491
|
-
if (binding.inFlight) {
|
|
1492
|
-
return Promise.reject(new CircularDependencyError(cycleNamesOf(this.#cascadeStack, frame.tokenName)));
|
|
1493
|
-
}
|
|
1494
|
-
const ctx = (this.#cascadeContext ??= new AsyncCascadeContext(this, this.#cascadeStack));
|
|
1495
|
-
binding.inFlight = true;
|
|
1496
|
-
this.#cascadeStack.push(frame);
|
|
1497
|
-
try {
|
|
1498
|
-
if (binding.kind === "dynamic-async") {
|
|
1499
|
-
return binding.factory(ctx);
|
|
1500
|
-
}
|
|
1501
|
-
const factoryResult = binding.factory(ctx);
|
|
1502
|
-
return factoryResult instanceof Promise ? factoryResult : Promise.resolve(factoryResult);
|
|
1503
|
-
} catch (factoryError) {
|
|
1504
|
-
return Promise.reject(factoryError);
|
|
1505
|
-
} finally {
|
|
1506
|
-
this.#cascadeStack.pop();
|
|
1507
|
-
binding.inFlight = false;
|
|
1508
|
-
}
|
|
1509
|
-
}
|
|
1510
|
-
|
|
1511
|
-
// A cached candidate answers here rather than re-entering the generic path: `resolveAll` pays
|
|
1512
|
-
// this per candidate, and a fan-out over cached handlers is the shape that makes it matter.
|
|
1513
|
-
#resolveCandidateSync(
|
|
1514
|
-
binding: Binding,
|
|
1515
|
-
options: ResolveOptions | undefined,
|
|
1516
|
-
resolutionStack: Array<ResolutionFrame>,
|
|
1517
|
-
): unknown {
|
|
1518
|
-
// Fan-outs are dominated by constants: with no activation hook anywhere in the chain, a
|
|
1519
|
-
// hook-free constant is plain no matter which container owns it — skip the owner probe.
|
|
1520
|
-
if (binding.kind === "constant" && binding.onActivation === undefined && this.#chainActivationVersion() === 0) {
|
|
1521
|
-
return binding.value;
|
|
1522
|
-
}
|
|
1523
|
-
const owner = this.#ownerOf(binding);
|
|
1524
|
-
if (owner.#isPlainConstant(binding)) {
|
|
1525
|
-
return binding.value;
|
|
1526
|
-
}
|
|
1527
|
-
if (binding.kind === "alias") {
|
|
1528
|
-
return this.resolve(binding.target, options, resolutionStack);
|
|
1529
|
-
}
|
|
1530
|
-
if (binding.scope === "singleton") {
|
|
1531
|
-
if (binding.instance !== NO_INSTANCE) {
|
|
1532
|
-
return binding.instance;
|
|
1533
|
-
}
|
|
1534
|
-
// Owner-routed like `resolve`: the owner materializes and caches its own singleton.
|
|
1535
|
-
return owner.#resolveBinding(binding, options, resolutionStack, owner);
|
|
1536
|
-
}
|
|
1537
|
-
return this.#resolveBinding(binding, options, resolutionStack, owner);
|
|
1538
|
-
}
|
|
1539
|
-
|
|
1540
|
-
#resolveCandidateAsync(
|
|
1541
|
-
binding: Binding,
|
|
1542
|
-
options: ResolveOptions | undefined,
|
|
1543
|
-
resolutionStack: Array<ResolutionFrame>,
|
|
1544
|
-
branchDepth: BranchDepth,
|
|
1545
|
-
): Promise<unknown> {
|
|
1546
|
-
if (binding.kind === "constant" && binding.onActivation === undefined && this.#chainActivationVersion() === 0) {
|
|
1547
|
-
return Promise.resolve(binding.value);
|
|
1548
|
-
}
|
|
1549
|
-
const owner = this.#ownerOf(binding);
|
|
1550
|
-
if (owner.#isPlainConstant(binding)) {
|
|
1551
|
-
return Promise.resolve(binding.value);
|
|
1552
|
-
}
|
|
1553
|
-
if (binding.kind === "alias") {
|
|
1554
|
-
return this.resolveAsync(binding.target, options, resolutionStack, branchDepth);
|
|
1555
|
-
}
|
|
1556
|
-
if (binding.scope === "singleton") {
|
|
1557
|
-
if (binding.instance !== NO_INSTANCE) {
|
|
1558
|
-
return Promise.resolve(binding.instance);
|
|
1559
|
-
}
|
|
1560
|
-
return owner.#resolveBindingAsync(binding, options, resolutionStack, branchDepth, owner);
|
|
1561
|
-
}
|
|
1562
|
-
return this.#resolveBindingAsync(binding, options, resolutionStack, branchDepth, owner);
|
|
1563
|
-
}
|
|
1564
|
-
|
|
1565
|
-
/** The resolver whose registry holds `binding` — `this` (the common case) when it is own. */
|
|
1566
|
-
#ownerOf(binding: Binding): DependencyResolver {
|
|
1567
|
-
// A root resolver can only hold its own bindings, so the per-candidate id probe is chain-only.
|
|
1568
|
-
if (this.#parent === undefined || this.#registry.getById(binding.id) !== undefined) {
|
|
1569
|
-
return this;
|
|
1570
|
-
}
|
|
1571
|
-
for (let current: DependencyResolver | undefined = this.#parent; current !== undefined; current = current.#parent) {
|
|
1572
|
-
if (current.#registry.getById(binding.id) !== undefined) {
|
|
1573
|
-
return current;
|
|
1574
|
-
}
|
|
1575
|
-
}
|
|
1576
|
-
return this;
|
|
1577
|
-
}
|
|
1578
|
-
|
|
1579
|
-
#getResolutionFrame(binding: Binding): ResolutionFrame {
|
|
1580
|
-
// Memoized on the binding rather than in a per-resolver Map: the frame derives only from
|
|
1581
|
-
// immutable binding fields, so it is identical for every resolver, and a field read beats a
|
|
1582
|
-
// Map lookup on every hop of a chain.
|
|
1583
|
-
const existing = binding.frame;
|
|
1584
|
-
if (existing !== undefined) {
|
|
1585
|
-
return existing;
|
|
1586
|
-
}
|
|
1587
|
-
const frame = buildResolutionFrame(tokenName(binding.token), binding.scope, binding.id, binding.kind, binding.slot);
|
|
1588
|
-
binding.frame = frame;
|
|
1589
|
-
return frame;
|
|
1590
|
-
}
|
|
1591
|
-
|
|
1592
|
-
// A pool is keyed by the one array pair its contexts hold, so reuse can never re-point a context
|
|
1593
|
-
// a live frame still reads — a nested top-level resolve reaches the same depth while the outer
|
|
1594
|
-
// factory runs, and it must get its own context, not the outer frame's re-bound.
|
|
1595
|
-
#acquireSyncResolutionContext(
|
|
1596
|
-
resolutionStack: Array<ResolutionFrame>,
|
|
1597
|
-
options: ResolveOptions | undefined,
|
|
1598
|
-
): DefaultResolutionContext {
|
|
1599
|
-
if (resolutionStack === this.rootStack) {
|
|
1600
|
-
const depth = resolutionStack.length;
|
|
1601
|
-
const existing = this.#syncResolutionContextPool[depth];
|
|
1602
|
-
if (existing !== undefined) {
|
|
1603
|
-
existing.reset(this, resolutionStack, options);
|
|
1604
|
-
return existing;
|
|
1605
|
-
}
|
|
1606
|
-
const created = new DefaultResolutionContext(this, resolutionStack, options);
|
|
1607
|
-
this.#syncResolutionContextPool[depth] = created;
|
|
1608
|
-
return created;
|
|
1609
|
-
}
|
|
1610
|
-
return this.#acquireOffRootSyncContext(resolutionStack, options);
|
|
1611
|
-
}
|
|
1612
|
-
|
|
1613
|
-
/** The cascade pair pools separately; a throwaway pair (nested resolve, async snapshot) mints per call. */
|
|
1614
|
-
#acquireOffRootSyncContext(
|
|
1615
|
-
resolutionStack: Array<ResolutionFrame>,
|
|
1616
|
-
options: ResolveOptions | undefined,
|
|
1617
|
-
): DefaultResolutionContext {
|
|
1618
|
-
if (resolutionStack !== this.#cascadeStack) {
|
|
1619
|
-
return new DefaultResolutionContext(this, resolutionStack, options);
|
|
1620
|
-
}
|
|
1621
|
-
const depth = resolutionStack.length;
|
|
1622
|
-
const pool = (this.#cascadeContextPool ??= []);
|
|
1623
|
-
const existing = pool[depth];
|
|
1624
|
-
if (existing !== undefined) {
|
|
1625
|
-
existing.reset(this, resolutionStack, options);
|
|
1626
|
-
return existing;
|
|
1627
|
-
}
|
|
1628
|
-
const created = new DefaultResolutionContext(this, resolutionStack, options);
|
|
1629
|
-
pool[depth] = created;
|
|
1630
|
-
return created;
|
|
1631
|
-
}
|
|
1632
|
-
}
|
|
1633
|
-
|
|
1634
|
-
function anyPredicate(bindings: ReadonlyArray<Binding>): boolean {
|
|
1635
|
-
for (let index = 0; index < bindings.length; index += 1) {
|
|
1636
|
-
if (bindings[index]!.predicate !== undefined) {
|
|
1637
|
-
return true;
|
|
1638
|
-
}
|
|
1639
|
-
}
|
|
1640
|
-
return false;
|
|
1641
|
-
}
|
|
1642
|
-
|
|
1643
|
-
function buildConstraintContext(
|
|
1644
|
-
resolutionStack: Array<ResolutionFrame>,
|
|
1645
|
-
options: ResolveOptions | undefined,
|
|
1646
|
-
): ConstraintContext {
|
|
1647
|
-
return {
|
|
1648
|
-
// Derived per read, never cached: the stack may be live, and the names must report it as it stands.
|
|
1649
|
-
get resolutionPath(): ReadonlyArray<string> {
|
|
1650
|
-
const names = new Array<string>(resolutionStack.length);
|
|
1651
|
-
for (let index = 0; index < resolutionStack.length; index += 1) {
|
|
1652
|
-
names[index] = resolutionStack[index]!.tokenName;
|
|
1653
|
-
}
|
|
1654
|
-
return names;
|
|
1655
|
-
},
|
|
1656
|
-
resolutionStack,
|
|
1657
|
-
parent: resolutionStack.at(-1),
|
|
1658
|
-
ancestors: resolutionStack.length > 1 ? resolutionStack.slice(0, -1) : [],
|
|
1659
|
-
currentResolveOptions: options,
|
|
1660
|
-
};
|
|
1661
|
-
}
|
|
1662
|
-
|
|
1663
|
-
/** The async-resolution failure for a binding reached on a sync path, naming what to await instead. */
|
|
1664
|
-
function asyncResolutionErrorFor(
|
|
1665
|
-
binding: Binding,
|
|
1666
|
-
resolutionStack: ReadonlyArray<ResolutionFrame>,
|
|
1667
|
-
): AsyncResolutionError {
|
|
1668
|
-
const sourceName = tokenName(binding.token);
|
|
1669
|
-
return new AsyncResolutionError(resolutionStack[0]?.tokenName ?? sourceName, sourceName);
|
|
1670
|
-
}
|
|
1671
|
-
|
|
1672
|
-
/** Only a factory is handed the resolution context; everything else gets its deps directly. */
|
|
1673
|
-
function requiresResolutionContext(binding: Binding): boolean {
|
|
1674
|
-
return binding.kind === "dynamic" || binding.kind === "dynamic-async";
|
|
1675
|
-
}
|