@codefast/di 0.5.0-canary.6 → 0.5.0-canary.8
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 +123 -0
- package/README.md +15 -4
- package/dist/binding.d.ts +78 -19
- package/dist/binding.d.ts.map +1 -1
- package/dist/binding.js +46 -2
- package/dist/binding.js.map +1 -1
- package/dist/constructor-type.d.ts +4 -5
- package/dist/constructor-type.d.ts.map +1 -1
- package/dist/container/binding-builders.d.ts +56 -0
- package/dist/container/binding-builders.d.ts.map +1 -0
- package/dist/container/binding-builders.js +196 -0
- package/dist/container/binding-builders.js.map +1 -0
- package/dist/{container.d.ts → container/container.d.ts} +2 -2
- package/dist/container/container.d.ts.map +1 -0
- package/dist/container/container.js +624 -0
- package/dist/container/container.js.map +1 -0
- package/dist/decorators/inject.d.ts +10 -1
- package/dist/decorators/inject.d.ts.map +1 -1
- package/dist/decorators/inject.js +2 -2
- package/dist/decorators/inject.js.map +1 -1
- package/dist/decorators/injectable.js +3 -3
- package/dist/decorators/injectable.js.map +1 -1
- package/dist/errors.d.ts +14 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +17 -0
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +13 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -5
- package/dist/index.js.map +1 -1
- package/dist/introspection/dependency-graph.d.ts.map +1 -0
- package/dist/{dependency-graph.js → introspection/dependency-graph.js} +1 -1
- package/dist/introspection/dependency-graph.js.map +1 -0
- package/dist/{graph-adapters → introspection/graph-adapters}/cytoscape.d.ts +1 -1
- package/dist/introspection/graph-adapters/cytoscape.d.ts.map +1 -0
- package/dist/introspection/graph-adapters/cytoscape.js.map +1 -0
- package/dist/introspection/graph-adapters/dot.d.ts +6 -0
- package/dist/introspection/graph-adapters/dot.d.ts.map +1 -0
- package/dist/introspection/graph-adapters/dot.js.map +1 -0
- package/dist/{graph-adapters → introspection/graph-adapters}/reactflow.d.ts +2 -2
- package/dist/introspection/graph-adapters/reactflow.d.ts.map +1 -0
- package/dist/{graph-adapters → introspection/graph-adapters}/reactflow.js +11 -1
- package/dist/introspection/graph-adapters/reactflow.js.map +1 -0
- package/dist/{inspector.d.ts → introspection/inspector.d.ts} +3 -8
- package/dist/introspection/inspector.d.ts.map +1 -0
- package/dist/{inspector.js → introspection/inspector.js} +23 -23
- package/dist/introspection/inspector.js.map +1 -0
- package/dist/metadata/metadata-keys.d.ts +3 -6
- package/dist/metadata/metadata-keys.d.ts.map +1 -1
- package/dist/metadata/metadata-keys.js +3 -6
- package/dist/metadata/metadata-keys.js.map +1 -1
- package/dist/metadata/symbol-metadata-reader.d.ts +1 -1
- package/dist/metadata/symbol-metadata-reader.d.ts.map +1 -1
- package/dist/metadata/symbol-metadata-reader.js +4 -4
- package/dist/metadata/symbol-metadata-reader.js.map +1 -1
- package/dist/module.d.ts +9 -2
- package/dist/module.d.ts.map +1 -1
- package/dist/module.js +9 -2
- package/dist/module.js.map +1 -1
- package/dist/registry.d.ts +17 -13
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +94 -63
- package/dist/registry.js.map +1 -1
- package/dist/resolution/activation-need.d.ts +25 -0
- package/dist/resolution/activation-need.d.ts.map +1 -0
- package/dist/resolution/activation-need.js +64 -0
- package/dist/resolution/activation-need.js.map +1 -0
- package/dist/resolution/binding-lookup-cache.d.ts +41 -0
- package/dist/resolution/binding-lookup-cache.d.ts.map +1 -0
- package/dist/resolution/binding-lookup-cache.js +102 -0
- package/dist/resolution/binding-lookup-cache.js.map +1 -0
- package/dist/resolution/binding-scope.d.ts.map +1 -0
- package/dist/resolution/binding-scope.js.map +1 -0
- package/dist/resolution/binding-select.d.ts.map +1 -0
- package/dist/{binding-select.js → resolution/binding-select.js} +17 -7
- package/dist/resolution/binding-select.js.map +1 -0
- package/dist/resolution/class-introspector.d.ts +27 -0
- package/dist/resolution/class-introspector.d.ts.map +1 -0
- package/dist/resolution/class-introspector.js +60 -0
- package/dist/resolution/class-introspector.js.map +1 -0
- package/dist/resolution/constraints.d.ts.map +1 -0
- package/dist/resolution/constraints.js.map +1 -0
- package/dist/resolution/diagnostics.d.ts +41 -0
- package/dist/resolution/diagnostics.d.ts.map +1 -0
- package/dist/resolution/diagnostics.js +18 -0
- package/dist/resolution/diagnostics.js.map +1 -0
- package/dist/{environment.d.ts → resolution/environment.d.ts} +24 -7
- package/dist/resolution/environment.d.ts.map +1 -0
- package/dist/resolution/environment.js +126 -0
- package/dist/resolution/environment.js.map +1 -0
- package/dist/resolution/instantiation-plan.d.ts +67 -0
- package/dist/resolution/instantiation-plan.d.ts.map +1 -0
- package/dist/resolution/instantiation-plan.js +183 -0
- package/dist/resolution/instantiation-plan.js.map +1 -0
- package/dist/{lifecycle.d.ts → resolution/lifecycle.d.ts} +5 -3
- package/dist/resolution/lifecycle.d.ts.map +1 -0
- package/dist/{lifecycle.js → resolution/lifecycle.js} +23 -14
- package/dist/resolution/lifecycle.js.map +1 -0
- package/dist/resolution/resolution-path.d.ts +22 -0
- package/dist/resolution/resolution-path.d.ts.map +1 -0
- package/dist/resolution/resolution-path.js +40 -0
- package/dist/resolution/resolution-path.js.map +1 -0
- package/dist/resolution/resolve-options.d.ts.map +1 -0
- package/dist/resolution/resolve-options.js.map +1 -0
- package/dist/resolution/resolver.d.ts +40 -0
- package/dist/resolution/resolver.d.ts.map +1 -0
- package/dist/resolution/resolver.js +1108 -0
- package/dist/resolution/resolver.js.map +1 -0
- package/dist/{scope.d.ts → resolution/scope.d.ts} +8 -9
- package/dist/resolution/scope.d.ts.map +1 -0
- package/dist/resolution/scope.js +80 -0
- package/dist/resolution/scope.js.map +1 -0
- package/dist/token.d.ts.map +1 -1
- package/dist/token.js +0 -3
- package/dist/token.js.map +1 -1
- package/package.json +19 -94
- package/src/binding.ts +137 -22
- package/src/constructor-type.ts +4 -5
- package/src/container/binding-builders.ts +294 -0
- package/src/container/container.ts +825 -0
- package/src/decorators/inject.ts +15 -4
- package/src/decorators/injectable.ts +3 -3
- package/src/errors.ts +21 -0
- package/src/index.ts +15 -7
- package/src/{dependency-graph.ts → introspection/dependency-graph.ts} +1 -1
- package/src/{graph-adapters → introspection/graph-adapters}/cytoscape.ts +1 -1
- package/src/{graph-adapters → introspection/graph-adapters}/dot.ts +1 -1
- package/src/{graph-adapters → introspection/graph-adapters}/reactflow.ts +13 -2
- package/src/{inspector.ts → introspection/inspector.ts} +26 -21
- package/src/metadata/metadata-keys.ts +3 -6
- package/src/metadata/symbol-metadata-reader.ts +4 -4
- package/src/module.ts +14 -6
- package/src/registry.ts +97 -64
- package/src/resolution/activation-need.ts +81 -0
- package/src/resolution/binding-lookup-cache.ts +132 -0
- package/src/{binding-select.ts → resolution/binding-select.ts} +17 -7
- package/src/resolution/class-introspector.ts +74 -0
- package/src/resolution/diagnostics.ts +43 -0
- package/src/{environment.ts → resolution/environment.ts} +65 -35
- package/src/resolution/instantiation-plan.ts +292 -0
- package/src/{lifecycle.ts → resolution/lifecycle.ts} +26 -14
- package/src/resolution/resolution-path.ts +62 -0
- package/src/resolution/resolver.ts +1526 -0
- package/src/resolution/scope.ts +95 -0
- package/src/token.ts +0 -3
- package/dist/binding-scope.d.ts.map +0 -1
- package/dist/binding-scope.js.map +0 -1
- package/dist/binding-select.d.ts.map +0 -1
- package/dist/binding-select.js.map +0 -1
- package/dist/constraints.d.ts.map +0 -1
- package/dist/constraints.js.map +0 -1
- package/dist/container.d.ts.map +0 -1
- package/dist/container.js +0 -853
- package/dist/container.js.map +0 -1
- package/dist/dependency-graph.d.ts.map +0 -1
- package/dist/dependency-graph.js.map +0 -1
- package/dist/environment.d.ts.map +0 -1
- package/dist/environment.js +0 -100
- package/dist/environment.js.map +0 -1
- package/dist/graph-adapters/cytoscape.d.ts.map +0 -1
- package/dist/graph-adapters/cytoscape.js.map +0 -1
- package/dist/graph-adapters/dot.d.ts +0 -6
- package/dist/graph-adapters/dot.d.ts.map +0 -1
- package/dist/graph-adapters/dot.js.map +0 -1
- package/dist/graph-adapters/reactflow.d.ts.map +0 -1
- package/dist/graph-adapters/reactflow.js.map +0 -1
- package/dist/inspector.d.ts.map +0 -1
- package/dist/inspector.js.map +0 -1
- package/dist/lifecycle.d.ts.map +0 -1
- package/dist/lifecycle.js.map +0 -1
- package/dist/resolve-options.d.ts.map +0 -1
- package/dist/resolve-options.js.map +0 -1
- package/dist/resolver.d.ts +0 -90
- package/dist/resolver.d.ts.map +0 -1
- package/dist/resolver.js +0 -1208
- package/dist/resolver.js.map +0 -1
- package/dist/scope.d.ts.map +0 -1
- package/dist/scope.js +0 -61
- package/dist/scope.js.map +0 -1
- package/src/container.ts +0 -1141
- package/src/resolver.ts +0 -1626
- package/src/scope.ts +0 -77
- /package/dist/{dependency-graph.d.ts → introspection/dependency-graph.d.ts} +0 -0
- /package/dist/{graph-adapters → introspection/graph-adapters}/cytoscape.js +0 -0
- /package/dist/{graph-adapters → introspection/graph-adapters}/dot.js +0 -0
- /package/dist/{binding-scope.d.ts → resolution/binding-scope.d.ts} +0 -0
- /package/dist/{binding-scope.js → resolution/binding-scope.js} +0 -0
- /package/dist/{binding-select.d.ts → resolution/binding-select.d.ts} +0 -0
- /package/dist/{constraints.d.ts → resolution/constraints.d.ts} +0 -0
- /package/dist/{constraints.js → resolution/constraints.js} +0 -0
- /package/dist/{resolve-options.d.ts → resolution/resolve-options.d.ts} +0 -0
- /package/dist/{resolve-options.js → resolution/resolve-options.js} +0 -0
- /package/src/{binding-scope.ts → resolution/binding-scope.ts} +0 -0
- /package/src/{constraints.ts → resolution/constraints.ts} +0 -0
- /package/src/{resolve-options.ts → resolution/resolve-options.ts} +0 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal seam letting tests assert that an optimization is *active*, not merely that the result
|
|
3
|
+
* is correct.
|
|
4
|
+
*
|
|
5
|
+
* @remarks Timing belongs in the benchmark, which needs a quiet machine and twenty minutes; these
|
|
6
|
+
* are structural counts, so CI can hold the invariants that make the benchmark fast. Reached
|
|
7
|
+
* through a symbol from a module the package does not publish, so it is not public API.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Key for the diagnostics accessor on a container.
|
|
12
|
+
*
|
|
13
|
+
* @remarks A symbol rather than a method name, so it cannot collide with the public surface or be
|
|
14
|
+
* reached by anyone who has not imported this module.
|
|
15
|
+
*
|
|
16
|
+
* @since 0.5.0-canary.8
|
|
17
|
+
*/
|
|
18
|
+
export const RESOLUTION_DIAGNOSTICS: unique symbol = Symbol("di:resolution-diagnostics");
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Structural facts about a container's resolution caches.
|
|
22
|
+
*
|
|
23
|
+
* @since 0.5.0-canary.8
|
|
24
|
+
*/
|
|
25
|
+
export interface ResolutionDiagnostics {
|
|
26
|
+
/** Bindings with a compiled instantiation plan. */
|
|
27
|
+
readonly compiledPlanCount: number;
|
|
28
|
+
/** Contexts held by the depth-indexed sync pool. */
|
|
29
|
+
readonly syncContextPoolSize: number;
|
|
30
|
+
/** Contexts returned to the async chain pool and available for reuse. */
|
|
31
|
+
readonly asyncContextPoolSize: number;
|
|
32
|
+
/** Deferred collaborators this container has had to build. */
|
|
33
|
+
readonly builtSubsystems: ReadonlyArray<string>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* A container that can report on its resolution caches.
|
|
38
|
+
*
|
|
39
|
+
* @since 0.5.0-canary.8
|
|
40
|
+
*/
|
|
41
|
+
export interface DiagnosableContainer {
|
|
42
|
+
[RESOLUTION_DIAGNOSTICS](): ResolutionDiagnostics;
|
|
43
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Container } from "#/container";
|
|
1
|
+
import type { Container } from "#/container/container";
|
|
2
2
|
import type { Token } from "#/token";
|
|
3
3
|
import type {
|
|
4
4
|
BindingIdentifier,
|
|
@@ -13,18 +13,18 @@ import type {
|
|
|
13
13
|
|
|
14
14
|
// ── Active container ──────────────────────────────────────────────────────────
|
|
15
15
|
|
|
16
|
-
let
|
|
16
|
+
let activeContainer: Container | undefined;
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
19
|
* @since 0.3.16-canary.0
|
|
20
20
|
*/
|
|
21
21
|
export function runWithContainer<Result>(container: Container, fn: () => Result): Result {
|
|
22
|
-
const prev =
|
|
23
|
-
|
|
22
|
+
const prev = activeContainer;
|
|
23
|
+
activeContainer = container;
|
|
24
24
|
try {
|
|
25
25
|
return fn();
|
|
26
26
|
} finally {
|
|
27
|
-
|
|
27
|
+
activeContainer = prev;
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
30
|
|
|
@@ -32,7 +32,7 @@ export function runWithContainer<Result>(container: Container, fn: () => Result)
|
|
|
32
32
|
* @since 0.3.16-canary.0
|
|
33
33
|
*/
|
|
34
34
|
export function getActiveContainer(): Container | undefined {
|
|
35
|
-
return
|
|
35
|
+
return activeContainer;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
// ── ResolutionContext implementation ──────────────────────────────────────────
|
|
@@ -56,6 +56,7 @@ export interface ResolverCallbacks {
|
|
|
56
56
|
token: Token<Value> | Constructor<Value>,
|
|
57
57
|
resolutionPath: Array<string>,
|
|
58
58
|
resolutionStack: Array<ResolutionFrame>,
|
|
59
|
+
callerContext?: DefaultResolutionContext,
|
|
59
60
|
): Promise<Value>;
|
|
60
61
|
resolveAsync<const Value>(
|
|
61
62
|
token: Token<Value> | Constructor<Value>,
|
|
@@ -93,10 +94,10 @@ export interface ResolverCallbacks {
|
|
|
93
94
|
* @since 0.3.16-canary.0
|
|
94
95
|
*/
|
|
95
96
|
export class DefaultResolutionContext implements ResolutionContext {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
97
|
+
#resolver: ResolverCallbacks;
|
|
98
|
+
#resolutionPath: Array<string>;
|
|
99
|
+
#resolutionStack: Array<ResolutionFrame>;
|
|
100
|
+
#currentOptions: ResolveOptions | undefined;
|
|
100
101
|
|
|
101
102
|
constructor(
|
|
102
103
|
resolver: ResolverCallbacks,
|
|
@@ -104,19 +105,44 @@ export class DefaultResolutionContext implements ResolutionContext {
|
|
|
104
105
|
resolutionStack: Array<ResolutionFrame>,
|
|
105
106
|
currentOptions: ResolveOptions | undefined,
|
|
106
107
|
) {
|
|
107
|
-
this
|
|
108
|
-
this.
|
|
109
|
-
this
|
|
110
|
-
this
|
|
108
|
+
this.#resolver = resolver;
|
|
109
|
+
this.owner = resolver;
|
|
110
|
+
this.#resolutionPath = resolutionPath;
|
|
111
|
+
this.#resolutionStack = resolutionStack;
|
|
112
|
+
this.#currentOptions = currentOptions;
|
|
111
113
|
}
|
|
112
114
|
|
|
113
|
-
|
|
115
|
+
#graph: ConstraintContext | undefined;
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* The unwind callback shared by every level of the async chain this context serves.
|
|
119
|
+
*
|
|
120
|
+
* @remarks A plain field, not a lazy accessor — a getter taking a factory would allocate that
|
|
121
|
+
* factory on every level, which is the allocation this exists to avoid.
|
|
122
|
+
*/
|
|
123
|
+
chainSettle: (() => void) | undefined;
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* How many levels of the async chain are still in flight on this context.
|
|
127
|
+
*
|
|
128
|
+
* @remarks The chain's first level acquires the context and every level increments; the last
|
|
129
|
+
* one to settle returns it to the resolver's pool. Counting here rather than on the resolver
|
|
130
|
+
* is what lets two concurrent chains run without a shared counter to get wrong.
|
|
131
|
+
*/
|
|
132
|
+
chainLevels = 0;
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* The resolver this context speaks to — an inner async level checks it before reusing this.
|
|
136
|
+
*
|
|
137
|
+
* @remarks A field, not a method, because the check runs on every hop of every chain.
|
|
138
|
+
*/
|
|
139
|
+
owner: ResolverCallbacks;
|
|
114
140
|
|
|
115
141
|
get graph(): ConstraintContext {
|
|
116
|
-
if (this
|
|
117
|
-
this
|
|
142
|
+
if (this.#graph === undefined) {
|
|
143
|
+
this.#graph = new DefaultConstraintContext(this.#resolutionPath, this.#resolutionStack, this.#currentOptions);
|
|
118
144
|
}
|
|
119
|
-
return this
|
|
145
|
+
return this.#graph;
|
|
120
146
|
}
|
|
121
147
|
|
|
122
148
|
reset(
|
|
@@ -125,47 +151,51 @@ export class DefaultResolutionContext implements ResolutionContext {
|
|
|
125
151
|
resolutionStack: Array<ResolutionFrame>,
|
|
126
152
|
currentOptions: ResolveOptions | undefined,
|
|
127
153
|
): void {
|
|
128
|
-
this
|
|
129
|
-
this.
|
|
130
|
-
this
|
|
131
|
-
this
|
|
132
|
-
this
|
|
154
|
+
this.#resolver = resolver;
|
|
155
|
+
this.owner = resolver;
|
|
156
|
+
this.#resolutionPath = resolutionPath;
|
|
157
|
+
this.#resolutionStack = resolutionStack;
|
|
158
|
+
this.#currentOptions = currentOptions;
|
|
159
|
+
this.#graph = undefined;
|
|
160
|
+
this.chainSettle = undefined;
|
|
161
|
+
this.chainLevels = 0;
|
|
133
162
|
}
|
|
134
163
|
|
|
135
164
|
resolve<const Value>(token: Token<Value> | Constructor<Value>, options?: ResolveOptions): Value {
|
|
136
165
|
if (options === undefined) {
|
|
137
|
-
return this.
|
|
166
|
+
return this.#resolver.resolveFromContext(token, this.#resolutionPath, this.#resolutionStack);
|
|
138
167
|
}
|
|
139
|
-
return this.
|
|
168
|
+
return this.#resolver.resolve(token, options, this.#resolutionPath, this.#resolutionStack);
|
|
140
169
|
}
|
|
141
170
|
|
|
142
171
|
resolveAsync<const Value>(token: Token<Value> | Constructor<Value>, options?: ResolveOptions): Promise<Value> {
|
|
143
172
|
if (options === undefined) {
|
|
144
|
-
|
|
173
|
+
// Hand the callee this context: an inner level of the same chain reuses it as-is.
|
|
174
|
+
return this.#resolver.resolveAsyncFromContext(token, this.#resolutionPath, this.#resolutionStack, this);
|
|
145
175
|
}
|
|
146
|
-
return this.
|
|
176
|
+
return this.#resolver.resolveAsync(token, options, this.#resolutionPath, this.#resolutionStack);
|
|
147
177
|
}
|
|
148
178
|
|
|
149
179
|
resolveOptional<const Value>(token: Token<Value> | Constructor<Value>, options?: ResolveOptions): Value | undefined {
|
|
150
|
-
return this.
|
|
180
|
+
return this.#resolver.resolveOptional(token, options, this.#resolutionPath, this.#resolutionStack);
|
|
151
181
|
}
|
|
152
182
|
|
|
153
183
|
resolveOptionalAsync<const Value>(
|
|
154
184
|
token: Token<Value> | Constructor<Value>,
|
|
155
185
|
options?: ResolveOptions,
|
|
156
186
|
): Promise<Value | undefined> {
|
|
157
|
-
return this.
|
|
187
|
+
return this.#resolver.resolveOptionalAsync(token, options, this.#resolutionPath, this.#resolutionStack);
|
|
158
188
|
}
|
|
159
189
|
|
|
160
190
|
resolveAll<const Value>(token: Token<Value> | Constructor<Value>, options?: ResolveOptions): Array<Value> {
|
|
161
|
-
return this.
|
|
191
|
+
return this.#resolver.resolveAll(token, options, this.#resolutionPath, this.#resolutionStack);
|
|
162
192
|
}
|
|
163
193
|
|
|
164
194
|
resolveAllAsync<const Value>(
|
|
165
195
|
token: Token<Value> | Constructor<Value>,
|
|
166
196
|
options?: ResolveOptions,
|
|
167
197
|
): Promise<Array<Value>> {
|
|
168
|
-
return this.
|
|
198
|
+
return this.#resolver.resolveAllAsync(token, options, this.#resolutionPath, this.#resolutionStack);
|
|
169
199
|
}
|
|
170
200
|
}
|
|
171
201
|
|
|
@@ -186,13 +216,13 @@ class DefaultConstraintContext implements ConstraintContext {
|
|
|
186
216
|
this.currentResolveOptions = currentResolveOptions;
|
|
187
217
|
}
|
|
188
218
|
|
|
189
|
-
|
|
219
|
+
#ancestors: ReadonlyArray<ResolutionFrame> | undefined;
|
|
190
220
|
|
|
191
221
|
get ancestors(): ReadonlyArray<ResolutionFrame> {
|
|
192
|
-
if (this
|
|
193
|
-
this
|
|
222
|
+
if (this.#ancestors === undefined) {
|
|
223
|
+
this.#ancestors = this.resolutionStack.length > 1 ? this.resolutionStack.slice(0, -1) : [];
|
|
194
224
|
}
|
|
195
|
-
return this
|
|
225
|
+
return this.#ancestors;
|
|
196
226
|
}
|
|
197
227
|
}
|
|
198
228
|
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compiles a transient class or resolved-factory binding into a nested-constructor closure.
|
|
3
|
+
*
|
|
4
|
+
* @see `ARCHITECTURE.md` — the escape contract every dependency the compiler cannot inline must honour.
|
|
5
|
+
*/
|
|
6
|
+
import type { Binding } from "#/binding";
|
|
7
|
+
import { NO_INSTANCE } from "#/binding";
|
|
8
|
+
import type { ConstructorInvocation } from "#/constructor-type";
|
|
9
|
+
import type { InjectionDescriptor } from "#/decorators/inject";
|
|
10
|
+
import { AsyncResolutionError } from "#/errors";
|
|
11
|
+
import type { ConstructorMetadata } from "#/metadata/metadata-types";
|
|
12
|
+
import { injectionSlotToResolveOptions } from "#/resolution/resolve-options";
|
|
13
|
+
import type { Token } from "#/token";
|
|
14
|
+
import { tokenName } from "#/token";
|
|
15
|
+
import type { BindingScope, Constructor, ResolutionFrame, ResolveOptions } from "#/types";
|
|
16
|
+
|
|
17
|
+
// Past this depth a dependency escapes to the runtime path rather than inlining further —
|
|
18
|
+
// compiled closures nest one JS frame per level, and pathological graphs are the runtime's job.
|
|
19
|
+
const PLAN_DEPTH_LIMIT = 32;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Compilation asked to retry later (class lifecycle metadata not discovered yet).
|
|
23
|
+
*
|
|
24
|
+
* @since 0.5.0-canary.7
|
|
25
|
+
*/
|
|
26
|
+
export const PLAN_RETRY: unique symbol = Symbol("di:plan-retry");
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* A compiled plan, `null` for "not plannable under the current cache versions",
|
|
30
|
+
* or {@link PLAN_RETRY} when a first runtime resolve must discover metadata first.
|
|
31
|
+
*
|
|
32
|
+
* @since 0.5.0-canary.7
|
|
33
|
+
*/
|
|
34
|
+
export type InstantiationPlanCompileResult = (() => unknown) | null | typeof PLAN_RETRY;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* What compiling one *dependency* can yield.
|
|
38
|
+
*
|
|
39
|
+
* @remarks No `null`: a dependency escapes rather than failing, so "no plan" is only ever a
|
|
40
|
+
* verdict on a plan's root.
|
|
41
|
+
*/
|
|
42
|
+
type DependencyCompileResult = (() => unknown) | typeof PLAN_RETRY;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* A dependency's terminal binding — all a compiled thunk needs.
|
|
46
|
+
*
|
|
47
|
+
* @since 0.5.0-canary.7
|
|
48
|
+
*/
|
|
49
|
+
export interface InstantiationPlanDependencyEntry {
|
|
50
|
+
readonly binding: Binding;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Everything the compiler needs from its resolver, expressed as behavior so the
|
|
55
|
+
* compiler stays independently testable and free of resolver internals.
|
|
56
|
+
*
|
|
57
|
+
* @since 0.5.0-canary.7
|
|
58
|
+
*/
|
|
59
|
+
export interface InstantiationPlanHost {
|
|
60
|
+
hasActivationHandlers(token: Token<unknown> | Constructor): boolean;
|
|
61
|
+
/** Cached postConstruct presence — `undefined` until a runtime resolve discovers it. */
|
|
62
|
+
knownPostConstruct(target: Constructor): boolean | undefined;
|
|
63
|
+
needsActiveContainer(target: Constructor): boolean;
|
|
64
|
+
getConstructorMetadata(target: Constructor): ConstructorMetadata | undefined;
|
|
65
|
+
/** Options-less lookup with alias hops folded; `null` when the fast lane can't answer. */
|
|
66
|
+
lookupDependencyEntry(token: Token<unknown> | Constructor): InstantiationPlanDependencyEntry | null;
|
|
67
|
+
/** The frame the interpreted path pushes for this binding, so escapes can replay it. */
|
|
68
|
+
getResolutionFrame(binding: Binding): ResolutionFrame;
|
|
69
|
+
/** Runtime resolve for an escaped dependency, seeded with the ancestors above it. */
|
|
70
|
+
resolveEscaped(
|
|
71
|
+
token: Token<unknown> | Constructor,
|
|
72
|
+
options: ResolveOptions | undefined,
|
|
73
|
+
arity: EscapeArity,
|
|
74
|
+
resolutionPath: Array<string>,
|
|
75
|
+
resolutionStack: Array<ResolutionFrame>,
|
|
76
|
+
): unknown;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Which resolve an escaped dependency replays — mirrors how the interpreted path dispatches
|
|
81
|
+
* a constructor param.
|
|
82
|
+
*
|
|
83
|
+
* @since 0.5.0-canary.8
|
|
84
|
+
*/
|
|
85
|
+
export type EscapeArity = "all" | "optional" | "single";
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* @since 0.3.16-canary.1
|
|
89
|
+
*/
|
|
90
|
+
export class InstantiationPlanCompiler {
|
|
91
|
+
readonly #host: InstantiationPlanHost;
|
|
92
|
+
|
|
93
|
+
constructor(host: InstantiationPlanHost) {
|
|
94
|
+
this.#host = host;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
compile(binding: Binding & { kind: "class" | "resolved" }): InstantiationPlanCompileResult {
|
|
98
|
+
return binding.kind === "class"
|
|
99
|
+
? this.#compileClassPlan(binding, new Set(), 0, [])
|
|
100
|
+
: this.#compileResolvedPlan(binding, new Set(), 0, []);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Re-entry into the runtime resolver for a dependency the plan can't see through.
|
|
105
|
+
*
|
|
106
|
+
* The ancestors are fixed at compile time, so the seeds are built once; each call copies
|
|
107
|
+
* them because the resolver pushes and pops on the arrays it is given.
|
|
108
|
+
*/
|
|
109
|
+
#compileEscapeThunk(
|
|
110
|
+
token: Token<unknown> | Constructor,
|
|
111
|
+
ancestors: ReadonlyArray<Binding>,
|
|
112
|
+
arity: EscapeArity = "single",
|
|
113
|
+
options?: ResolveOptions,
|
|
114
|
+
): () => unknown {
|
|
115
|
+
const host = this.#host;
|
|
116
|
+
const frames = ancestors.map((ancestor) => host.getResolutionFrame(ancestor));
|
|
117
|
+
const names = frames.map((frame) => frame.tokenName);
|
|
118
|
+
return () => host.resolveEscaped(token, options, arity, [...names], [...frames]);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// A resolved binding declares its deps as explicit descriptors — same rules as
|
|
122
|
+
// class params, with the factory call (and its sync-only check) in place of `new`.
|
|
123
|
+
#compileResolvedPlan(
|
|
124
|
+
binding: Binding & { kind: "resolved" },
|
|
125
|
+
compileStack: Set<Binding["id"]>,
|
|
126
|
+
depth: number,
|
|
127
|
+
ancestors: ReadonlyArray<Binding>,
|
|
128
|
+
): InstantiationPlanCompileResult {
|
|
129
|
+
if (binding.onActivation !== undefined || this.#host.hasActivationHandlers(binding.token)) {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
const factory = binding.factory;
|
|
133
|
+
const tokenDisplayName = tokenName(binding.token);
|
|
134
|
+
const depThunks = new Array<() => unknown>(binding.deps.length);
|
|
135
|
+
const depAncestors = [...ancestors, binding];
|
|
136
|
+
compileStack.add(binding.id);
|
|
137
|
+
try {
|
|
138
|
+
for (let index = 0; index < binding.deps.length; index += 1) {
|
|
139
|
+
const thunk = this.#compileInjectionThunk(binding.deps[index]!, compileStack, depth, depAncestors);
|
|
140
|
+
if (thunk === PLAN_RETRY) {
|
|
141
|
+
return thunk;
|
|
142
|
+
}
|
|
143
|
+
depThunks[index] = thunk;
|
|
144
|
+
}
|
|
145
|
+
} finally {
|
|
146
|
+
compileStack.delete(binding.id);
|
|
147
|
+
}
|
|
148
|
+
return () => {
|
|
149
|
+
const factoryResult = factory(...depThunks.map((thunk) => thunk()));
|
|
150
|
+
if (factoryResult instanceof Promise) {
|
|
151
|
+
throw new AsyncResolutionError(tokenDisplayName, tokenDisplayName);
|
|
152
|
+
}
|
|
153
|
+
return factoryResult;
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* One dependency of a plan node — a constructor param or a `toResolved` descriptor.
|
|
159
|
+
*
|
|
160
|
+
* @remarks Anything but a plain required single dependency escapes to the runtime path.
|
|
161
|
+
*/
|
|
162
|
+
#compileInjectionThunk(
|
|
163
|
+
descriptor: InjectionDescriptor,
|
|
164
|
+
compileStack: Set<Binding["id"]>,
|
|
165
|
+
depth: number,
|
|
166
|
+
ancestors: ReadonlyArray<Binding>,
|
|
167
|
+
): DependencyCompileResult {
|
|
168
|
+
const token = descriptor.token as Token<unknown> | Constructor;
|
|
169
|
+
const options = injectionSlotToResolveOptions(descriptor);
|
|
170
|
+
if (descriptor.multi) {
|
|
171
|
+
return this.#compileEscapeThunk(token, ancestors, "all", options);
|
|
172
|
+
}
|
|
173
|
+
if (descriptor.optional) {
|
|
174
|
+
return this.#compileEscapeThunk(token, ancestors, "optional", options);
|
|
175
|
+
}
|
|
176
|
+
if (options !== undefined) {
|
|
177
|
+
return this.#compileEscapeThunk(token, ancestors, "single", options);
|
|
178
|
+
}
|
|
179
|
+
const entry = this.#host.lookupDependencyEntry(token);
|
|
180
|
+
if (entry === null) {
|
|
181
|
+
return this.#compileEscapeThunk(token, ancestors);
|
|
182
|
+
}
|
|
183
|
+
return this.#compileDepThunk(entry, compileStack, depth, ancestors);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
#compileClassPlan(
|
|
187
|
+
binding: Binding & { kind: "class" },
|
|
188
|
+
compileStack: Set<Binding["id"]>,
|
|
189
|
+
depth: number,
|
|
190
|
+
ancestors: ReadonlyArray<Binding>,
|
|
191
|
+
): InstantiationPlanCompileResult {
|
|
192
|
+
if (binding.onActivation !== undefined || this.#host.hasActivationHandlers(binding.token)) {
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
const target = binding.target;
|
|
196
|
+
const hasPostConstruct = this.#host.knownPostConstruct(target);
|
|
197
|
+
if (hasPostConstruct === undefined) {
|
|
198
|
+
return PLAN_RETRY;
|
|
199
|
+
}
|
|
200
|
+
if (hasPostConstruct || this.#host.needsActiveContainer(target)) {
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
const invokable = target as ConstructorInvocation;
|
|
204
|
+
const meta = this.#host.getConstructorMetadata(target);
|
|
205
|
+
if (meta === undefined) {
|
|
206
|
+
// Metadata-less classes with required params throw on the runtime path — keep them there.
|
|
207
|
+
return target.length === 0 ? () => new invokable() : null;
|
|
208
|
+
}
|
|
209
|
+
const params = meta.params;
|
|
210
|
+
if (params.length === 0) {
|
|
211
|
+
return () => new invokable();
|
|
212
|
+
}
|
|
213
|
+
const depThunks = new Array<() => unknown>(params.length);
|
|
214
|
+
const depAncestors = [...ancestors, binding];
|
|
215
|
+
compileStack.add(binding.id);
|
|
216
|
+
try {
|
|
217
|
+
for (let index = 0; index < params.length; index += 1) {
|
|
218
|
+
const thunk = this.#compileInjectionThunk(params[index]!, compileStack, depth, depAncestors);
|
|
219
|
+
if (thunk === PLAN_RETRY) {
|
|
220
|
+
return thunk;
|
|
221
|
+
}
|
|
222
|
+
depThunks[index] = thunk;
|
|
223
|
+
}
|
|
224
|
+
} finally {
|
|
225
|
+
compileStack.delete(binding.id);
|
|
226
|
+
}
|
|
227
|
+
switch (depThunks.length) {
|
|
228
|
+
case 1: {
|
|
229
|
+
const dep0 = depThunks[0]!;
|
|
230
|
+
return () => new invokable(dep0());
|
|
231
|
+
}
|
|
232
|
+
case 2: {
|
|
233
|
+
const dep0 = depThunks[0]!;
|
|
234
|
+
const dep1 = depThunks[1]!;
|
|
235
|
+
return () => new invokable(dep0(), dep1());
|
|
236
|
+
}
|
|
237
|
+
case 3: {
|
|
238
|
+
const dep0 = depThunks[0]!;
|
|
239
|
+
const dep1 = depThunks[1]!;
|
|
240
|
+
const dep2 = depThunks[2]!;
|
|
241
|
+
return () => new invokable(dep0(), dep1(), dep2());
|
|
242
|
+
}
|
|
243
|
+
default:
|
|
244
|
+
return () => new invokable(...depThunks.map((thunk) => thunk()));
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
#compileDepThunk(
|
|
249
|
+
entry: InstantiationPlanDependencyEntry,
|
|
250
|
+
compileStack: Set<Binding["id"]>,
|
|
251
|
+
depth: number,
|
|
252
|
+
ancestors: ReadonlyArray<Binding>,
|
|
253
|
+
): DependencyCompileResult {
|
|
254
|
+
const { binding } = entry;
|
|
255
|
+
if (binding.kind === "constant" && binding.onActivation === undefined) {
|
|
256
|
+
if (!this.#host.hasActivationHandlers(binding.token)) {
|
|
257
|
+
const value = binding.value;
|
|
258
|
+
return () => value;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
const scope = (binding as Binding & { scope: BindingScope }).scope ?? "transient";
|
|
262
|
+
if (scope === "singleton") {
|
|
263
|
+
// Cached-singleton read; the first materialization escapes so it sees the same ancestors
|
|
264
|
+
// (and therefore the same cycle detection) the interpreted path would have built.
|
|
265
|
+
const escape = this.#compileEscapeThunk(binding.token, ancestors);
|
|
266
|
+
const singletonBinding = binding;
|
|
267
|
+
return () => {
|
|
268
|
+
const cached = singletonBinding.instance;
|
|
269
|
+
return cached === NO_INSTANCE ? escape() : cached;
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
if (
|
|
273
|
+
scope === "transient" &&
|
|
274
|
+
binding.kind === "class" &&
|
|
275
|
+
depth < PLAN_DEPTH_LIMIT &&
|
|
276
|
+
!compileStack.has(binding.id)
|
|
277
|
+
) {
|
|
278
|
+
const inlined = this.#compileClassPlan(
|
|
279
|
+
binding as Binding & { kind: "class" },
|
|
280
|
+
compileStack,
|
|
281
|
+
depth + 1,
|
|
282
|
+
ancestors,
|
|
283
|
+
);
|
|
284
|
+
if (inlined !== null) {
|
|
285
|
+
return inlined;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
// Anything opaque — a factory, a scoped binding, an activation hook, a class the compiler
|
|
289
|
+
// declined — runs on the runtime path, seeded with this plan's ancestors.
|
|
290
|
+
return this.#compileEscapeThunk(binding.token, ancestors);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
@@ -9,35 +9,43 @@ import type { ActivationHandler, Constructor, DeactivationHandler, ResolutionCon
|
|
|
9
9
|
* @since 0.3.16-canary.0
|
|
10
10
|
*/
|
|
11
11
|
export class LifecycleManager {
|
|
12
|
-
// Container-level activation/deactivation hooks per token
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
// Container-level activation/deactivation hooks per token — most containers register none, so
|
|
13
|
+
// both tables stay unallocated until the first hook arrives.
|
|
14
|
+
#activationHooks: Map<Token<unknown> | Constructor, Array<ActivationHandler<unknown>>> | undefined;
|
|
15
|
+
#deactivationHooks: Map<Token<unknown> | Constructor, Array<DeactivationHandler<unknown>>> | undefined;
|
|
16
|
+
#activationVersion = 0;
|
|
16
17
|
|
|
17
18
|
registerActivation<const Value>(token: Token<Value> | Constructor<Value>, handler: ActivationHandler<Value>): void {
|
|
18
|
-
this
|
|
19
|
+
this.#activationVersion += 1;
|
|
19
20
|
// ✓ TS6.0: Map.getOrInsert (ES2025)
|
|
20
|
-
const list = this.
|
|
21
|
+
const list = (this.#activationHooks ??= new Map()).getOrInsert(token as Token<unknown> | Constructor, []);
|
|
21
22
|
list.push(handler as ActivationHandler<unknown>);
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
hasActivationHandlers<const Value>(token: Token<Value> | Constructor<Value>): boolean {
|
|
25
|
-
if (this
|
|
26
|
+
if (this.#activationHooks === undefined) {
|
|
26
27
|
return false;
|
|
27
28
|
}
|
|
28
|
-
const list = this.
|
|
29
|
+
const list = this.#activationHooks.get(token as Token<unknown> | Constructor);
|
|
29
30
|
return list !== undefined && list.length > 0;
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
get activationVersion(): number {
|
|
33
|
-
return this
|
|
34
|
+
return this.#activationVersion;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Container-level activation handlers for a token — hot-path accessor, no copies. */
|
|
38
|
+
activationHandlersFor<const Value>(
|
|
39
|
+
token: Token<Value> | Constructor<Value>,
|
|
40
|
+
): ReadonlyArray<ActivationHandler<unknown>> | undefined {
|
|
41
|
+
return this.#activationHooks?.get(token as Token<unknown> | Constructor);
|
|
34
42
|
}
|
|
35
43
|
|
|
36
44
|
registerDeactivation<const Value>(
|
|
37
45
|
token: Token<Value> | Constructor<Value>,
|
|
38
46
|
handler: DeactivationHandler<Value>,
|
|
39
47
|
): void {
|
|
40
|
-
const list = this.
|
|
48
|
+
const list = (this.#deactivationHooks ??= new Map()).getOrInsert(token as Token<unknown> | Constructor, []);
|
|
41
49
|
list.push(handler as DeactivationHandler<unknown>);
|
|
42
50
|
}
|
|
43
51
|
|
|
@@ -72,7 +80,7 @@ export class LifecycleManager {
|
|
|
72
80
|
}
|
|
73
81
|
|
|
74
82
|
// 3. container-level onActivation
|
|
75
|
-
const containerHooks = this
|
|
83
|
+
const containerHooks = this.#activationHooks?.get(binding.token as Token<unknown> | Constructor);
|
|
76
84
|
if (containerHooks !== undefined) {
|
|
77
85
|
for (const hook of containerHooks) {
|
|
78
86
|
const activationResult = hook(resolutionContext, activatedInstance);
|
|
@@ -118,7 +126,7 @@ export class LifecycleManager {
|
|
|
118
126
|
|
|
119
127
|
// 3. container-level onActivation (must be sync)
|
|
120
128
|
const tokenDisplayName = tokenName(binding.token);
|
|
121
|
-
const containerHooks = this
|
|
129
|
+
const containerHooks = this.#activationHooks?.get(binding.token as Token<unknown> | Constructor);
|
|
122
130
|
if (containerHooks !== undefined) {
|
|
123
131
|
for (const hook of containerHooks) {
|
|
124
132
|
const activationResult = hook(resolutionContext, activatedInstance);
|
|
@@ -140,7 +148,7 @@ export class LifecycleManager {
|
|
|
140
148
|
const tokenKey = binding.token as Token<unknown> | Constructor;
|
|
141
149
|
|
|
142
150
|
// 1. container-level onDeactivation
|
|
143
|
-
const containerHooks = this
|
|
151
|
+
const containerHooks = this.#deactivationHooks?.get(tokenKey);
|
|
144
152
|
if (containerHooks !== undefined) {
|
|
145
153
|
for (const hook of containerHooks) {
|
|
146
154
|
const hookResult = hook(instance);
|
|
@@ -180,7 +188,7 @@ export class LifecycleManager {
|
|
|
180
188
|
const tokenKey = binding.token as Token<unknown> | Constructor;
|
|
181
189
|
|
|
182
190
|
// 1. container-level onDeactivation
|
|
183
|
-
const containerHooks = this
|
|
191
|
+
const containerHooks = this.#deactivationHooks?.get(tokenKey);
|
|
184
192
|
if (containerHooks !== undefined) {
|
|
185
193
|
for (const hook of containerHooks) {
|
|
186
194
|
const hookResult = hook(instance);
|
|
@@ -214,4 +222,8 @@ export class LifecycleManager {
|
|
|
214
222
|
}
|
|
215
223
|
}
|
|
216
224
|
}
|
|
225
|
+
/** Whether the deferred table behind `#activationHooks` has had to be built. */
|
|
226
|
+
get isBuilt(): boolean {
|
|
227
|
+
return this.#activationHooks !== undefined;
|
|
228
|
+
}
|
|
217
229
|
}
|