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