@codefast/di 0.5.0-canary.8 → 0.5.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 +387 -0
- package/README.md +3 -1
- package/dist/binding.d.ts +41 -22
- package/dist/binding.d.ts.map +1 -1
- package/dist/binding.js +11 -0
- package/dist/binding.js.map +1 -1
- package/dist/container/binding-builders.d.ts.map +1 -1
- package/dist/container/binding-builders.js +8 -5
- package/dist/container/binding-builders.js.map +1 -1
- package/dist/container/container.d.ts.map +1 -1
- package/dist/container/container.js +67 -110
- package/dist/container/container.js.map +1 -1
- package/dist/errors.d.ts +10 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +13 -0
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +1 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/introspection/inspector.d.ts +0 -1
- package/dist/introspection/inspector.d.ts.map +1 -1
- package/dist/introspection/inspector.js +2 -7
- package/dist/introspection/inspector.js.map +1 -1
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +46 -33
- package/dist/registry.js.map +1 -1
- package/dist/resolution/activation-need.d.ts +4 -2
- package/dist/resolution/activation-need.d.ts.map +1 -1
- package/dist/resolution/activation-need.js +15 -11
- package/dist/resolution/activation-need.js.map +1 -1
- package/dist/resolution/binding-lookup-cache.d.ts +1 -1
- package/dist/resolution/binding-lookup-cache.d.ts.map +1 -1
- package/dist/resolution/binding-lookup-cache.js +18 -2
- package/dist/resolution/binding-lookup-cache.js.map +1 -1
- package/dist/resolution/binding-scope.d.ts +5 -2
- package/dist/resolution/binding-scope.d.ts.map +1 -1
- package/dist/resolution/binding-scope.js +6 -17
- package/dist/resolution/binding-scope.js.map +1 -1
- package/dist/resolution/binding-select.d.ts +8 -1
- package/dist/resolution/binding-select.d.ts.map +1 -1
- package/dist/resolution/binding-select.js +13 -32
- package/dist/resolution/binding-select.js.map +1 -1
- package/dist/resolution/diagnostics.d.ts +2 -2
- package/dist/resolution/diagnostics.d.ts.map +1 -1
- package/dist/resolution/environment.d.ts +47 -21
- package/dist/resolution/environment.d.ts.map +1 -1
- package/dist/resolution/environment.js +134 -31
- package/dist/resolution/environment.js.map +1 -1
- package/dist/resolution/instantiation-plan.d.ts.map +1 -1
- package/dist/resolution/instantiation-plan.js +1 -1
- package/dist/resolution/instantiation-plan.js.map +1 -1
- package/dist/resolution/lifecycle.d.ts.map +1 -1
- package/dist/resolution/lifecycle.js +46 -53
- package/dist/resolution/lifecycle.js.map +1 -1
- package/dist/resolution/resolution-path.d.ts +82 -6
- package/dist/resolution/resolution-path.d.ts.map +1 -1
- package/dist/resolution/resolution-path.js +66 -8
- package/dist/resolution/resolution-path.js.map +1 -1
- package/dist/resolution/resolve-options.d.ts +41 -4
- package/dist/resolution/resolve-options.d.ts.map +1 -1
- package/dist/resolution/resolve-options.js +25 -1
- package/dist/resolution/resolve-options.js.map +1 -1
- package/dist/resolution/resolver.d.ts +29 -11
- package/dist/resolution/resolver.d.ts.map +1 -1
- package/dist/resolution/resolver.js +417 -564
- package/dist/resolution/resolver.js.map +1 -1
- package/dist/resolution/scope.d.ts +4 -0
- package/dist/resolution/scope.d.ts.map +1 -1
- package/dist/resolution/scope.js +8 -0
- package/dist/resolution/scope.js.map +1 -1
- package/package.json +5 -11
- package/src/binding.ts +46 -23
- package/src/container/binding-builders.ts +8 -5
- package/src/container/container.ts +72 -119
- package/src/errors.ts +17 -0
- package/src/index.ts +3 -1
- package/src/introspection/inspector.ts +2 -8
- package/src/registry.ts +52 -35
- package/src/resolution/activation-need.ts +18 -14
- package/src/resolution/binding-lookup-cache.ts +18 -2
- package/src/resolution/binding-scope.ts +6 -17
- package/src/resolution/binding-select.ts +14 -35
- package/src/resolution/diagnostics.ts +2 -2
- package/src/resolution/environment.ts +181 -35
- package/src/resolution/instantiation-plan.ts +5 -5
- package/src/resolution/lifecycle.ts +55 -53
- package/src/resolution/resolution-path.ts +119 -25
- package/src/resolution/resolve-options.ts +51 -4
- package/src/resolution/resolver.ts +582 -785
- package/src/resolution/scope.ts +10 -0
|
@@ -2,25 +2,14 @@ import type { Binding } from "#/binding";
|
|
|
2
2
|
import type { BindingScope } from "#/types";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* The scope a binding resolves under.
|
|
6
|
+
*
|
|
7
|
+
* @remarks Every kind declares one — an alias declares `transient`, since it defers scoping to the
|
|
8
|
+
* binding it points at — so this is a field read, kept as a named function because it is the
|
|
9
|
+
* vocabulary validation and introspection speak.
|
|
7
10
|
*
|
|
8
11
|
* @since 0.3.16-canary.0
|
|
9
12
|
*/
|
|
10
13
|
export function effectiveBindingScope(binding: Binding): BindingScope {
|
|
11
|
-
|
|
12
|
-
case "alias":
|
|
13
|
-
return "transient";
|
|
14
|
-
case "class":
|
|
15
|
-
case "constant":
|
|
16
|
-
case "dynamic":
|
|
17
|
-
case "dynamic-async":
|
|
18
|
-
case "resolved":
|
|
19
|
-
case "resolved-async":
|
|
20
|
-
return binding.scope;
|
|
21
|
-
default: {
|
|
22
|
-
const exhaustive: never = binding;
|
|
23
|
-
return exhaustive;
|
|
24
|
-
}
|
|
25
|
-
}
|
|
14
|
+
return binding.scope;
|
|
26
15
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Binding } from "#/binding";
|
|
1
|
+
import type { Binding, BindingSlot } from "#/binding";
|
|
2
2
|
import { AmbiguousBindingError } from "#/errors";
|
|
3
3
|
import type { BindingTag, ConstraintContext, ResolveOptions } from "#/types";
|
|
4
4
|
|
|
@@ -62,50 +62,29 @@ function filterBindings(
|
|
|
62
62
|
ctx: ConstraintContext,
|
|
63
63
|
selectionMode: "single" | "all" = "single",
|
|
64
64
|
): Array<Binding> {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
for (const binding of bindings) {
|
|
69
|
-
if (matchesPredicate(binding, ctx)) {
|
|
70
|
-
resultWithoutOptions.push(binding);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
} else {
|
|
74
|
-
for (const binding of bindings) {
|
|
75
|
-
const slot = binding.slot;
|
|
76
|
-
if (slot.name === undefined && slot.tags.length === 0 && matchesPredicate(binding, ctx)) {
|
|
77
|
-
resultWithoutOptions.push(binding);
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
return resultWithoutOptions;
|
|
82
|
-
}
|
|
83
|
-
|
|
65
|
+
// `resolveAll` with no slot criterion takes every binding; `resolve` always matches the slot,
|
|
66
|
+
// where an absent criterion means "the default slot".
|
|
67
|
+
const requiresSlotMatch = selectionMode === "single" || (options !== undefined && hasSlotCriterion(options));
|
|
84
68
|
const result: Array<Binding> = [];
|
|
85
69
|
for (const binding of bindings) {
|
|
86
|
-
|
|
87
|
-
selectionMode === "all" ? matchesSlotForResolveAll(binding, options) : matchesSlot(binding, options);
|
|
88
|
-
if (slotMatched && matchesPredicate(binding, ctx)) {
|
|
70
|
+
if ((!requiresSlotMatch || matchesSlot(binding.slot, options)) && matchesPredicate(binding, ctx)) {
|
|
89
71
|
result.push(binding);
|
|
90
72
|
}
|
|
91
73
|
}
|
|
92
74
|
return result;
|
|
93
75
|
}
|
|
94
76
|
|
|
95
|
-
function
|
|
96
|
-
|
|
97
|
-
options !== undefined &&
|
|
98
|
-
(options.name !== undefined ||
|
|
99
|
-
(options.tags !== undefined && options.tags.length > 0) ||
|
|
100
|
-
options.tag !== undefined);
|
|
101
|
-
if (!hasExplicitSlotFilter) {
|
|
102
|
-
return true;
|
|
103
|
-
}
|
|
104
|
-
return matchesSlot(binding, options);
|
|
77
|
+
function hasSlotCriterion(options: ResolveOptions): boolean {
|
|
78
|
+
return options.name !== undefined || options.tag !== undefined || (options.tags?.length ?? 0) > 0;
|
|
105
79
|
}
|
|
106
80
|
|
|
107
|
-
|
|
108
|
-
|
|
81
|
+
/**
|
|
82
|
+
* Whether a binding's slot satisfies a request: names must be equal, and every tag the slot
|
|
83
|
+
* declares must be among the tags requested (SPEC §6.9).
|
|
84
|
+
*
|
|
85
|
+
* @since 0.5.0-canary.9
|
|
86
|
+
*/
|
|
87
|
+
export function matchesSlot(slot: BindingSlot, options: ResolveOptions | undefined): boolean {
|
|
109
88
|
const requestedName = options?.name;
|
|
110
89
|
const requestedTags = options?.tags;
|
|
111
90
|
const singleRequestedTag = options?.tag;
|
|
@@ -27,8 +27,8 @@ export interface ResolutionDiagnostics {
|
|
|
27
27
|
readonly compiledPlanCount: number;
|
|
28
28
|
/** Contexts held by the depth-indexed sync pool. */
|
|
29
29
|
readonly syncContextPoolSize: number;
|
|
30
|
-
/**
|
|
31
|
-
readonly
|
|
30
|
+
/** Scoped instances currently cached by this container's scope manager. */
|
|
31
|
+
readonly scopedInstanceCount: number;
|
|
32
32
|
/** Deferred collaborators this container has had to build. */
|
|
33
33
|
readonly builtSubsystems: ReadonlyArray<string>;
|
|
34
34
|
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { Container } from "#/container/container";
|
|
2
|
+
import type { BranchDepth, OwnedBranchPath, OwnedBranchStack } from "#/resolution/resolution-path";
|
|
3
|
+
import { UNOWNED_BRANCH } from "#/resolution/resolution-path";
|
|
2
4
|
import type { Token } from "#/token";
|
|
3
5
|
import type {
|
|
4
6
|
BindingIdentifier,
|
|
@@ -56,8 +58,10 @@ export interface ResolverCallbacks {
|
|
|
56
58
|
token: Token<Value> | Constructor<Value>,
|
|
57
59
|
resolutionPath: Array<string>,
|
|
58
60
|
resolutionStack: Array<ResolutionFrame>,
|
|
59
|
-
|
|
61
|
+
branchDepth: BranchDepth,
|
|
60
62
|
): Promise<Value>;
|
|
63
|
+
/** Not one of the eight `Value`-naming entry points: its caller is, and casts once. */
|
|
64
|
+
resolveAsyncFromCascade(token: Token<unknown> | Constructor): Promise<unknown>;
|
|
61
65
|
resolveAsync<const Value>(
|
|
62
66
|
token: Token<Value> | Constructor<Value>,
|
|
63
67
|
options: ResolveOptions | undefined,
|
|
@@ -106,7 +110,6 @@ export class DefaultResolutionContext implements ResolutionContext {
|
|
|
106
110
|
currentOptions: ResolveOptions | undefined,
|
|
107
111
|
) {
|
|
108
112
|
this.#resolver = resolver;
|
|
109
|
-
this.owner = resolver;
|
|
110
113
|
this.#resolutionPath = resolutionPath;
|
|
111
114
|
this.#resolutionStack = resolutionStack;
|
|
112
115
|
this.#currentOptions = currentOptions;
|
|
@@ -114,30 +117,6 @@ export class DefaultResolutionContext implements ResolutionContext {
|
|
|
114
117
|
|
|
115
118
|
#graph: ConstraintContext | undefined;
|
|
116
119
|
|
|
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;
|
|
140
|
-
|
|
141
120
|
get graph(): ConstraintContext {
|
|
142
121
|
if (this.#graph === undefined) {
|
|
143
122
|
this.#graph = new DefaultConstraintContext(this.#resolutionPath, this.#resolutionStack, this.#currentOptions);
|
|
@@ -151,14 +130,21 @@ export class DefaultResolutionContext implements ResolutionContext {
|
|
|
151
130
|
resolutionStack: Array<ResolutionFrame>,
|
|
152
131
|
currentOptions: ResolveOptions | undefined,
|
|
153
132
|
): void {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
this.#
|
|
157
|
-
|
|
133
|
+
// Compared before storing: a pooled context lives long enough to be in old space, so storing a
|
|
134
|
+
// pointer costs a write barrier, and a sync resolve hands every depth the same two arrays.
|
|
135
|
+
if (this.#resolver !== resolver) {
|
|
136
|
+
this.#resolver = resolver;
|
|
137
|
+
}
|
|
138
|
+
if (this.#resolutionPath !== resolutionPath) {
|
|
139
|
+
this.#resolutionPath = resolutionPath;
|
|
140
|
+
}
|
|
141
|
+
if (this.#resolutionStack !== resolutionStack) {
|
|
142
|
+
this.#resolutionStack = resolutionStack;
|
|
143
|
+
}
|
|
158
144
|
this.#currentOptions = currentOptions;
|
|
159
|
-
this.#graph
|
|
160
|
-
|
|
161
|
-
|
|
145
|
+
if (this.#graph !== undefined) {
|
|
146
|
+
this.#graph = undefined;
|
|
147
|
+
}
|
|
162
148
|
}
|
|
163
149
|
|
|
164
150
|
resolve<const Value>(token: Token<Value> | Constructor<Value>, options?: ResolveOptions): Value {
|
|
@@ -170,8 +156,8 @@ export class DefaultResolutionContext implements ResolutionContext {
|
|
|
170
156
|
|
|
171
157
|
resolveAsync<const Value>(token: Token<Value> | Constructor<Value>, options?: ResolveOptions): Promise<Value> {
|
|
172
158
|
if (options === undefined) {
|
|
173
|
-
//
|
|
174
|
-
return this.#resolver.resolveAsyncFromContext(token, this.#resolutionPath, this.#resolutionStack,
|
|
159
|
+
// UNOWNED_BRANCH: this frame's array is a sync stack it will pop, so the lane must copy it.
|
|
160
|
+
return this.#resolver.resolveAsyncFromContext(token, this.#resolutionPath, this.#resolutionStack, UNOWNED_BRANCH);
|
|
175
161
|
}
|
|
176
162
|
return this.#resolver.resolveAsync(token, options, this.#resolutionPath, this.#resolutionStack);
|
|
177
163
|
}
|
|
@@ -199,6 +185,166 @@ export class DefaultResolutionContext implements ResolutionContext {
|
|
|
199
185
|
}
|
|
200
186
|
}
|
|
201
187
|
|
|
188
|
+
/**
|
|
189
|
+
* One async level's resolution context, which is also its branch of the resolution path.
|
|
190
|
+
*
|
|
191
|
+
* @remarks Separate from {@link DefaultResolutionContext} so the sync lane's pooled context keeps
|
|
192
|
+
* reading its arrays as plain fields: only an async branch has a prefix to take. See
|
|
193
|
+
* `ARCHITECTURE.md` — an async level owns its branch of the path.
|
|
194
|
+
*
|
|
195
|
+
* @since 0.5.0-canary.9
|
|
196
|
+
*/
|
|
197
|
+
export class AsyncLevelContext implements ResolutionContext {
|
|
198
|
+
readonly #resolver: ResolverCallbacks;
|
|
199
|
+
readonly #resolutionPath: OwnedBranchPath;
|
|
200
|
+
readonly #resolutionStack: OwnedBranchStack;
|
|
201
|
+
readonly #currentOptions: ResolveOptions | undefined;
|
|
202
|
+
readonly #branchDepth: BranchDepth;
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* @param resolutionPath - this level's own branch; the depth is read off it rather than passed,
|
|
206
|
+
* so the two cannot disagree about where this level sits
|
|
207
|
+
*/
|
|
208
|
+
constructor(
|
|
209
|
+
resolver: ResolverCallbacks,
|
|
210
|
+
resolutionPath: OwnedBranchPath,
|
|
211
|
+
resolutionStack: OwnedBranchStack,
|
|
212
|
+
currentOptions: ResolveOptions | undefined,
|
|
213
|
+
) {
|
|
214
|
+
this.#resolver = resolver;
|
|
215
|
+
this.#resolutionPath = resolutionPath;
|
|
216
|
+
this.#resolutionStack = resolutionStack;
|
|
217
|
+
this.#currentOptions = currentOptions;
|
|
218
|
+
this.#branchDepth = resolutionPath.length as BranchDepth;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
#graph: ConstraintContext | undefined;
|
|
222
|
+
#exactPathCache: Array<string> | undefined;
|
|
223
|
+
#exactStackCache: Array<ResolutionFrame> | undefined;
|
|
224
|
+
|
|
225
|
+
get graph(): ConstraintContext {
|
|
226
|
+
if (this.#graph === undefined) {
|
|
227
|
+
this.#graph = new DefaultConstraintContext(this.#exactPath(), this.#exactStack(), this.#currentOptions);
|
|
228
|
+
}
|
|
229
|
+
return this.#graph;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// The path is append-only and a descendant may already have grown it past this level, so every
|
|
233
|
+
// caller but the async lane is handed this branch's prefix. It is fixed for the level's lifetime.
|
|
234
|
+
#exactPath(): Array<string> {
|
|
235
|
+
return (this.#exactPathCache ??= this.#resolutionPath.slice(0, this.#branchDepth));
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
#exactStack(): Array<ResolutionFrame> {
|
|
239
|
+
return (this.#exactStackCache ??= this.#resolutionStack.slice(0, this.#branchDepth));
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
resolve<const Value>(token: Token<Value> | Constructor<Value>, options?: ResolveOptions): Value {
|
|
243
|
+
if (options === undefined) {
|
|
244
|
+
return this.#resolver.resolveFromContext(token, this.#exactPath(), this.#exactStack());
|
|
245
|
+
}
|
|
246
|
+
return this.#resolver.resolve(token, options, this.#exactPath(), this.#exactStack());
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
resolveAsync<const Value>(token: Token<Value> | Constructor<Value>, options?: ResolveOptions): Promise<Value> {
|
|
250
|
+
if (options === undefined) {
|
|
251
|
+
// The hot lane: the resolver reads this branch by depth, so nothing is materialized.
|
|
252
|
+
return this.#resolver.resolveAsyncFromContext(
|
|
253
|
+
token,
|
|
254
|
+
this.#resolutionPath,
|
|
255
|
+
this.#resolutionStack,
|
|
256
|
+
this.#branchDepth,
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
return this.#resolver.resolveAsync(token, options, this.#exactPath(), this.#exactStack());
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
resolveOptional<const Value>(token: Token<Value> | Constructor<Value>, options?: ResolveOptions): Value | undefined {
|
|
263
|
+
return this.#resolver.resolveOptional(token, options, this.#exactPath(), this.#exactStack());
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
resolveOptionalAsync<const Value>(
|
|
267
|
+
token: Token<Value> | Constructor<Value>,
|
|
268
|
+
options?: ResolveOptions,
|
|
269
|
+
): Promise<Value | undefined> {
|
|
270
|
+
return this.#resolver.resolveOptionalAsync(token, options, this.#exactPath(), this.#exactStack());
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
resolveAll<const Value>(token: Token<Value> | Constructor<Value>, options?: ResolveOptions): Array<Value> {
|
|
274
|
+
return this.#resolver.resolveAll(token, options, this.#exactPath(), this.#exactStack());
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
resolveAllAsync<const Value>(
|
|
278
|
+
token: Token<Value> | Constructor<Value>,
|
|
279
|
+
options?: ResolveOptions,
|
|
280
|
+
): Promise<Array<Value>> {
|
|
281
|
+
return this.#resolver.resolveAllAsync(token, options, this.#exactPath(), this.#exactStack());
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* The one context every level of an open synchronous factory cascade shares.
|
|
287
|
+
*
|
|
288
|
+
* @remarks It carries no per-level state at all: while the cascade is open, the resolver's arrays
|
|
289
|
+
* *are* this level's ancestor chain, so nothing has to be allocated per level. See
|
|
290
|
+
* `ARCHITECTURE.md` — the cascade lane.
|
|
291
|
+
*
|
|
292
|
+
* @since 0.5.0-canary.9
|
|
293
|
+
*/
|
|
294
|
+
export class AsyncCascadeContext implements ResolutionContext {
|
|
295
|
+
readonly #resolver: ResolverCallbacks;
|
|
296
|
+
readonly #cascadePath: Array<string>;
|
|
297
|
+
readonly #cascadeStack: Array<ResolutionFrame>;
|
|
298
|
+
|
|
299
|
+
constructor(resolver: ResolverCallbacks, cascadePath: Array<string>, cascadeStack: Array<ResolutionFrame>) {
|
|
300
|
+
this.#resolver = resolver;
|
|
301
|
+
this.#cascadePath = cascadePath;
|
|
302
|
+
this.#cascadeStack = cascadeStack;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
get graph(): ConstraintContext {
|
|
306
|
+
// Not memoized: this context outlives every level, so a cached graph would describe whichever
|
|
307
|
+
// level asked first. The cascade arrays are only this level's ancestors while it is open.
|
|
308
|
+
return new DefaultConstraintContext(this.#cascadePath, this.#cascadeStack, undefined);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
resolve<const Value>(token: Token<Value> | Constructor<Value>, options?: ResolveOptions): Value {
|
|
312
|
+
if (options === undefined) {
|
|
313
|
+
return this.#resolver.resolveFromContext(token, this.#cascadePath, this.#cascadeStack);
|
|
314
|
+
}
|
|
315
|
+
return this.#resolver.resolve(token, options, this.#cascadePath, this.#cascadeStack);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
resolveAsync<const Value>(token: Token<Value> | Constructor<Value>, options?: ResolveOptions): Promise<Value> {
|
|
319
|
+
if (options === undefined) {
|
|
320
|
+
return this.#resolver.resolveAsyncFromCascade(token) as Promise<Value>;
|
|
321
|
+
}
|
|
322
|
+
return this.#resolver.resolveAsync(token, options, [...this.#cascadePath], [...this.#cascadeStack]);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
resolveOptional<const Value>(token: Token<Value> | Constructor<Value>, options?: ResolveOptions): Value | undefined {
|
|
326
|
+
return this.#resolver.resolveOptional(token, options, this.#cascadePath, this.#cascadeStack);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
resolveOptionalAsync<const Value>(
|
|
330
|
+
token: Token<Value> | Constructor<Value>,
|
|
331
|
+
options?: ResolveOptions,
|
|
332
|
+
): Promise<Value | undefined> {
|
|
333
|
+
return this.#resolver.resolveOptionalAsync(token, options, [...this.#cascadePath], [...this.#cascadeStack]);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
resolveAll<const Value>(token: Token<Value> | Constructor<Value>, options?: ResolveOptions): Array<Value> {
|
|
337
|
+
return this.#resolver.resolveAll(token, options, this.#cascadePath, this.#cascadeStack);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
resolveAllAsync<const Value>(
|
|
341
|
+
token: Token<Value> | Constructor<Value>,
|
|
342
|
+
options?: ResolveOptions,
|
|
343
|
+
): Promise<Array<Value>> {
|
|
344
|
+
return this.#resolver.resolveAllAsync(token, options, [...this.#cascadePath], [...this.#cascadeStack]);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
202
348
|
class DefaultConstraintContext implements ConstraintContext {
|
|
203
349
|
readonly resolutionPath: ReadonlyArray<string>;
|
|
204
350
|
readonly resolutionStack: ReadonlyArray<ResolutionFrame>;
|
|
@@ -6,13 +6,13 @@
|
|
|
6
6
|
import type { Binding } from "#/binding";
|
|
7
7
|
import { NO_INSTANCE } from "#/binding";
|
|
8
8
|
import type { ConstructorInvocation } from "#/constructor-type";
|
|
9
|
-
import type { InjectionDescriptor } from "#/decorators/inject";
|
|
10
9
|
import { AsyncResolutionError } from "#/errors";
|
|
11
10
|
import type { ConstructorMetadata } from "#/metadata/metadata-types";
|
|
11
|
+
import type { DependencySlot } from "#/resolution/resolve-options";
|
|
12
12
|
import { injectionSlotToResolveOptions } from "#/resolution/resolve-options";
|
|
13
13
|
import type { Token } from "#/token";
|
|
14
14
|
import { tokenName } from "#/token";
|
|
15
|
-
import type {
|
|
15
|
+
import type { Constructor, ResolutionFrame, ResolveOptions } from "#/types";
|
|
16
16
|
|
|
17
17
|
// Past this depth a dependency escapes to the runtime path rather than inlining further —
|
|
18
18
|
// compiled closures nest one JS frame per level, and pathological graphs are the runtime's job.
|
|
@@ -160,12 +160,12 @@ export class InstantiationPlanCompiler {
|
|
|
160
160
|
* @remarks Anything but a plain required single dependency escapes to the runtime path.
|
|
161
161
|
*/
|
|
162
162
|
#compileInjectionThunk(
|
|
163
|
-
descriptor:
|
|
163
|
+
descriptor: DependencySlot,
|
|
164
164
|
compileStack: Set<Binding["id"]>,
|
|
165
165
|
depth: number,
|
|
166
166
|
ancestors: ReadonlyArray<Binding>,
|
|
167
167
|
): DependencyCompileResult {
|
|
168
|
-
const token = descriptor.token
|
|
168
|
+
const token = descriptor.token;
|
|
169
169
|
const options = injectionSlotToResolveOptions(descriptor);
|
|
170
170
|
if (descriptor.multi) {
|
|
171
171
|
return this.#compileEscapeThunk(token, ancestors, "all", options);
|
|
@@ -258,7 +258,7 @@ export class InstantiationPlanCompiler {
|
|
|
258
258
|
return () => value;
|
|
259
259
|
}
|
|
260
260
|
}
|
|
261
|
-
const scope =
|
|
261
|
+
const scope = binding.scope;
|
|
262
262
|
if (scope === "singleton") {
|
|
263
263
|
// Cached-singleton read; the first materialization escapes so it sees the same ancestors
|
|
264
264
|
// (and therefore the same cycle detection) the interpreted path would have built.
|
|
@@ -15,18 +15,22 @@ export class LifecycleManager {
|
|
|
15
15
|
#deactivationHooks: Map<Token<unknown> | Constructor, Array<DeactivationHandler<unknown>>> | undefined;
|
|
16
16
|
#activationVersion = 0;
|
|
17
17
|
|
|
18
|
+
// One-entry cache in front of the map: a resolve loop asks about the same token over and over,
|
|
19
|
+
// and registration is the only thing that can change the answer.
|
|
20
|
+
#cachedToken: Token<unknown> | Constructor | undefined;
|
|
21
|
+
#cachedHooks: Array<ActivationHandler<unknown>> | undefined;
|
|
22
|
+
|
|
18
23
|
registerActivation<const Value>(token: Token<Value> | Constructor<Value>, handler: ActivationHandler<Value>): void {
|
|
19
24
|
this.#activationVersion += 1;
|
|
25
|
+
this.#cachedToken = undefined;
|
|
26
|
+
this.#cachedHooks = undefined;
|
|
20
27
|
// ✓ TS6.0: Map.getOrInsert (ES2025)
|
|
21
28
|
const list = (this.#activationHooks ??= new Map()).getOrInsert(token as Token<unknown> | Constructor, []);
|
|
22
29
|
list.push(handler as ActivationHandler<unknown>);
|
|
23
30
|
}
|
|
24
31
|
|
|
25
32
|
hasActivationHandlers<const Value>(token: Token<Value> | Constructor<Value>): boolean {
|
|
26
|
-
|
|
27
|
-
return false;
|
|
28
|
-
}
|
|
29
|
-
const list = this.#activationHooks.get(token as Token<unknown> | Constructor);
|
|
33
|
+
const list = this.activationHandlersFor(token);
|
|
30
34
|
return list !== undefined && list.length > 0;
|
|
31
35
|
}
|
|
32
36
|
|
|
@@ -38,7 +42,18 @@ export class LifecycleManager {
|
|
|
38
42
|
activationHandlersFor<const Value>(
|
|
39
43
|
token: Token<Value> | Constructor<Value>,
|
|
40
44
|
): ReadonlyArray<ActivationHandler<unknown>> | undefined {
|
|
41
|
-
|
|
45
|
+
const hooks = this.#activationHooks;
|
|
46
|
+
if (hooks === undefined) {
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
const key = token as Token<unknown> | Constructor;
|
|
50
|
+
if (key === this.#cachedToken) {
|
|
51
|
+
return this.#cachedHooks;
|
|
52
|
+
}
|
|
53
|
+
const list = hooks.get(key);
|
|
54
|
+
this.#cachedToken = key;
|
|
55
|
+
this.#cachedHooks = list;
|
|
56
|
+
return list;
|
|
42
57
|
}
|
|
43
58
|
|
|
44
59
|
registerDeactivation<const Value>(
|
|
@@ -58,18 +73,10 @@ export class LifecycleManager {
|
|
|
58
73
|
let activatedInstance: Value = instance;
|
|
59
74
|
|
|
60
75
|
// 1. @postConstruct() — after TC39 construction (constructor + accessor addInitializer callbacks)
|
|
61
|
-
|
|
62
|
-
const
|
|
63
|
-
if (
|
|
64
|
-
|
|
65
|
-
const method = (activatedInstance as Record<string, unknown>)[methodName];
|
|
66
|
-
if (typeof method === "function") {
|
|
67
|
-
const hookResult = (method as () => unknown).call(activatedInstance);
|
|
68
|
-
if (hookResult instanceof Promise) {
|
|
69
|
-
await hookResult;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
}
|
|
76
|
+
for (const methodName of lifecycleMethods(binding, metadataReader, "postConstruct")) {
|
|
77
|
+
const hookResult = callHook(activatedInstance, methodName);
|
|
78
|
+
if (hookResult instanceof Promise) {
|
|
79
|
+
await hookResult;
|
|
73
80
|
}
|
|
74
81
|
}
|
|
75
82
|
|
|
@@ -100,18 +107,9 @@ export class LifecycleManager {
|
|
|
100
107
|
let activatedInstance: Value = instance;
|
|
101
108
|
|
|
102
109
|
// 1. @postConstruct() — must be sync (instance fully constructed per TC39 order)
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
for (const methodName of lifecycle.postConstruct) {
|
|
107
|
-
const method = (activatedInstance as Record<string, unknown>)[methodName];
|
|
108
|
-
if (typeof method === "function") {
|
|
109
|
-
const hookResult = (method as () => unknown).call(activatedInstance);
|
|
110
|
-
if (hookResult instanceof Promise) {
|
|
111
|
-
throw new AsyncActivationError(tokenName(binding.token), "postConstruct", methodName);
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
}
|
|
110
|
+
for (const methodName of lifecycleMethods(binding, metadataReader, "postConstruct")) {
|
|
111
|
+
if (callHook(activatedInstance, methodName) instanceof Promise) {
|
|
112
|
+
throw new AsyncActivationError(tokenName(binding.token), "postConstruct", methodName);
|
|
115
113
|
}
|
|
116
114
|
}
|
|
117
115
|
|
|
@@ -167,18 +165,10 @@ export class LifecycleManager {
|
|
|
167
165
|
}
|
|
168
166
|
|
|
169
167
|
// 3. @preDestroy() — all methods in declaration order
|
|
170
|
-
|
|
171
|
-
const
|
|
172
|
-
if (
|
|
173
|
-
|
|
174
|
-
const method = (instance as Record<string, unknown>)[methodName];
|
|
175
|
-
if (typeof method === "function") {
|
|
176
|
-
const hookResult = (method as () => unknown).call(instance);
|
|
177
|
-
if (hookResult instanceof Promise) {
|
|
178
|
-
await hookResult;
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
}
|
|
168
|
+
for (const methodName of lifecycleMethods(binding, metadataReader, "preDestroy")) {
|
|
169
|
+
const hookResult = callHook(instance, methodName);
|
|
170
|
+
if (hookResult instanceof Promise) {
|
|
171
|
+
await hookResult;
|
|
182
172
|
}
|
|
183
173
|
}
|
|
184
174
|
}
|
|
@@ -207,23 +197,35 @@ export class LifecycleManager {
|
|
|
207
197
|
}
|
|
208
198
|
|
|
209
199
|
// 3. @preDestroy()
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
for (const methodName of lifecycle.preDestroy) {
|
|
214
|
-
const method = (instance as Record<string, unknown>)[methodName];
|
|
215
|
-
if (typeof method === "function") {
|
|
216
|
-
const hookResult = (method as () => unknown).call(instance);
|
|
217
|
-
if (hookResult instanceof Promise) {
|
|
218
|
-
throw new AsyncDeactivationError(tokenDisplayName);
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
}
|
|
200
|
+
for (const methodName of lifecycleMethods(binding, metadataReader, "preDestroy")) {
|
|
201
|
+
if (callHook(instance, methodName) instanceof Promise) {
|
|
202
|
+
throw new AsyncDeactivationError(tokenDisplayName);
|
|
222
203
|
}
|
|
223
204
|
}
|
|
224
205
|
}
|
|
206
|
+
|
|
225
207
|
/** Whether the deferred table behind `#activationHooks` has had to be built. */
|
|
226
208
|
get isBuilt(): boolean {
|
|
227
209
|
return this.#activationHooks !== undefined;
|
|
228
210
|
}
|
|
229
211
|
}
|
|
212
|
+
|
|
213
|
+
const NO_METHODS: ReadonlyArray<string> = [];
|
|
214
|
+
|
|
215
|
+
/** The `@postConstruct` / `@preDestroy` methods a binding declares — only a class can declare any. */
|
|
216
|
+
function lifecycleMethods<const Value>(
|
|
217
|
+
binding: Binding<Value>,
|
|
218
|
+
metadataReader: MetadataReader,
|
|
219
|
+
phase: "postConstruct" | "preDestroy",
|
|
220
|
+
): ReadonlyArray<string> {
|
|
221
|
+
if (binding.kind !== "class") {
|
|
222
|
+
return NO_METHODS;
|
|
223
|
+
}
|
|
224
|
+
return metadataReader.getLifecycleMetadata(binding.target)?.[phase] ?? NO_METHODS;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** Invokes a hook by name, tolerating a name whose member is not (or no longer) a method. */
|
|
228
|
+
function callHook(instance: unknown, methodName: string): unknown {
|
|
229
|
+
const method = (instance as Record<string, unknown>)[methodName];
|
|
230
|
+
return typeof method === "function" ? (method as () => unknown).call(instance) : undefined;
|
|
231
|
+
}
|