@codefast/di 0.5.0-canary.7 → 0.5.0-canary.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +205 -0
- package/README.md +6 -2
- package/dist/binding.d.ts +85 -24
- package/dist/binding.d.ts.map +1 -1
- package/dist/binding.js +55 -0
- package/dist/binding.js.map +1 -1
- package/dist/constructor-type.d.ts +4 -5
- package/dist/constructor-type.d.ts.map +1 -1
- package/dist/container/binding-builders.d.ts +32 -11
- package/dist/container/binding-builders.d.ts.map +1 -1
- package/dist/container/binding-builders.js +144 -192
- package/dist/container/binding-builders.js.map +1 -1
- package/dist/container/container.d.ts.map +1 -1
- package/dist/container/container.js +141 -201
- package/dist/container/container.js.map +1 -1
- package/dist/decorators/inject.d.ts +2 -4
- package/dist/decorators/inject.d.ts.map +1 -1
- package/dist/decorators/inject.js.map +1 -1
- package/dist/errors.d.ts +24 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +30 -0
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +1 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/introspection/inspector.d.ts +0 -1
- package/dist/introspection/inspector.d.ts.map +1 -1
- package/dist/introspection/inspector.js +3 -8
- package/dist/introspection/inspector.js.map +1 -1
- package/dist/metadata/metadata-keys.d.ts +3 -6
- package/dist/metadata/metadata-keys.d.ts.map +1 -1
- package/dist/metadata/metadata-keys.js +3 -6
- package/dist/metadata/metadata-keys.js.map +1 -1
- package/dist/registry.d.ts +14 -2
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +81 -78
- package/dist/registry.js.map +1 -1
- package/dist/resolution/activation-need.d.ts +27 -0
- package/dist/resolution/activation-need.d.ts.map +1 -0
- package/dist/resolution/activation-need.js +68 -0
- package/dist/resolution/activation-need.js.map +1 -0
- package/dist/resolution/binding-lookup-cache.d.ts +41 -0
- package/dist/resolution/binding-lookup-cache.d.ts.map +1 -0
- package/dist/resolution/binding-lookup-cache.js +118 -0
- package/dist/resolution/binding-lookup-cache.js.map +1 -0
- package/dist/resolution/binding-scope.d.ts +5 -2
- package/dist/resolution/binding-scope.d.ts.map +1 -1
- package/dist/resolution/binding-scope.js +6 -17
- package/dist/resolution/binding-scope.js.map +1 -1
- package/dist/resolution/binding-select.d.ts +8 -1
- package/dist/resolution/binding-select.d.ts.map +1 -1
- package/dist/resolution/binding-select.js +14 -36
- package/dist/resolution/binding-select.js.map +1 -1
- package/dist/resolution/class-introspector.d.ts +27 -0
- package/dist/resolution/class-introspector.d.ts.map +1 -0
- package/dist/resolution/class-introspector.js +60 -0
- package/dist/resolution/class-introspector.js.map +1 -0
- package/dist/resolution/diagnostics.d.ts +41 -0
- package/dist/resolution/diagnostics.d.ts.map +1 -0
- package/dist/resolution/diagnostics.js +18 -0
- package/dist/resolution/diagnostics.js.map +1 -0
- package/dist/resolution/environment.d.ts +48 -1
- package/dist/resolution/environment.d.ts.map +1 -1
- package/dist/resolution/environment.js +134 -5
- package/dist/resolution/environment.js.map +1 -1
- package/dist/resolution/instantiation-plan.d.ts +15 -15
- package/dist/resolution/instantiation-plan.d.ts.map +1 -1
- package/dist/resolution/instantiation-plan.js +69 -49
- package/dist/resolution/instantiation-plan.js.map +1 -1
- package/dist/resolution/lifecycle.d.ts +2 -0
- package/dist/resolution/lifecycle.d.ts.map +1 -1
- package/dist/resolution/lifecycle.js +60 -62
- package/dist/resolution/lifecycle.js.map +1 -1
- package/dist/resolution/resolution-path.d.ts +84 -17
- package/dist/resolution/resolution-path.d.ts.map +1 -1
- package/dist/resolution/resolution-path.js +68 -23
- package/dist/resolution/resolution-path.js.map +1 -1
- package/dist/resolution/resolve-options.d.ts +41 -4
- package/dist/resolution/resolve-options.d.ts.map +1 -1
- package/dist/resolution/resolve-options.js +25 -1
- package/dist/resolution/resolve-options.js.map +1 -1
- package/dist/resolution/resolver.d.ts +33 -10
- package/dist/resolution/resolver.d.ts.map +1 -1
- package/dist/resolution/resolver.js +471 -830
- package/dist/resolution/resolver.js.map +1 -1
- package/dist/resolution/scope.d.ts +11 -15
- package/dist/resolution/scope.d.ts.map +1 -1
- package/dist/resolution/scope.js +55 -43
- package/dist/resolution/scope.js.map +1 -1
- package/package.json +10 -106
- package/src/binding.ts +146 -24
- package/src/constructor-type.ts +4 -5
- package/src/container/binding-builders.ts +184 -284
- package/src/container/container.ts +161 -221
- package/src/decorators/inject.ts +3 -5
- package/src/errors.ts +38 -0
- package/src/index.ts +4 -1
- package/src/introspection/inspector.ts +3 -9
- package/src/metadata/metadata-keys.ts +3 -6
- package/src/registry.ts +90 -94
- package/src/resolution/activation-need.ts +85 -0
- package/src/resolution/binding-lookup-cache.ts +148 -0
- package/src/resolution/binding-scope.ts +6 -17
- package/src/resolution/binding-select.ts +15 -39
- package/src/resolution/class-introspector.ts +74 -0
- package/src/resolution/diagnostics.ts +43 -0
- package/src/resolution/environment.ts +181 -5
- package/src/resolution/instantiation-plan.ts +116 -64
- package/src/resolution/lifecycle.ts +69 -62
- package/src/resolution/resolution-path.ts +122 -43
- package/src/resolution/resolve-options.ts +51 -4
- package/src/resolution/resolver.ts +649 -1081
- package/src/resolution/scope.ts +58 -47
|
@@ -1,28 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* class or resolved-factory binding whose dependency subgraph is pure static
|
|
4
|
-
* (class/constant/cached-singleton deps, no activation hooks or postConstruct)
|
|
5
|
-
* compiles once into a nested-constructor/factory closure, cycle-checked at
|
|
6
|
-
* compile time.
|
|
2
|
+
* Compiles a transient class or resolved-factory binding into a nested-constructor closure.
|
|
7
3
|
*
|
|
8
|
-
*
|
|
9
|
-
* closures it returns ARE the hot path and touch nothing but their captures.
|
|
10
|
-
* Anything dynamic refuses to compile so the runtime cycle guard stays in
|
|
11
|
-
* charge and error semantics never change.
|
|
4
|
+
* @see `ARCHITECTURE.md` — the escape contract every dependency the compiler cannot inline must honour.
|
|
12
5
|
*/
|
|
13
6
|
import type { Binding } from "#/binding";
|
|
7
|
+
import { NO_INSTANCE } from "#/binding";
|
|
14
8
|
import type { ConstructorInvocation } from "#/constructor-type";
|
|
15
|
-
import type { InjectionDescriptor } from "#/decorators/inject";
|
|
16
9
|
import { AsyncResolutionError } from "#/errors";
|
|
17
10
|
import type { ConstructorMetadata } from "#/metadata/metadata-types";
|
|
11
|
+
import type { DependencySlot } from "#/resolution/resolve-options";
|
|
18
12
|
import { injectionSlotToResolveOptions } from "#/resolution/resolve-options";
|
|
19
|
-
import type { ScopeManager } from "#/resolution/scope";
|
|
20
|
-
import { SINGLETON_MISS } from "#/resolution/scope";
|
|
21
13
|
import type { Token } from "#/token";
|
|
22
14
|
import { tokenName } from "#/token";
|
|
23
|
-
import type {
|
|
15
|
+
import type { Constructor, ResolutionFrame, ResolveOptions } from "#/types";
|
|
24
16
|
|
|
25
|
-
//
|
|
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.
|
|
26
19
|
const PLAN_DEPTH_LIMIT = 32;
|
|
27
20
|
|
|
28
21
|
/**
|
|
@@ -41,13 +34,20 @@ export const PLAN_RETRY: unique symbol = Symbol("di:plan-retry");
|
|
|
41
34
|
export type InstantiationPlanCompileResult = (() => unknown) | null | typeof PLAN_RETRY;
|
|
42
35
|
|
|
43
36
|
/**
|
|
44
|
-
*
|
|
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.
|
|
45
46
|
*
|
|
46
47
|
* @since 0.5.0-canary.7
|
|
47
48
|
*/
|
|
48
49
|
export interface InstantiationPlanDependencyEntry {
|
|
49
50
|
readonly binding: Binding;
|
|
50
|
-
readonly ownerScope: ScopeManager;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
/**
|
|
@@ -64,10 +64,26 @@ export interface InstantiationPlanHost {
|
|
|
64
64
|
getConstructorMetadata(target: Constructor): ConstructorMetadata | undefined;
|
|
65
65
|
/** Options-less lookup with alias hops folded; `null` when the fast lane can't answer. */
|
|
66
66
|
lookupDependencyEntry(token: Token<unknown> | Constructor): InstantiationPlanDependencyEntry | null;
|
|
67
|
-
/**
|
|
68
|
-
|
|
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;
|
|
69
77
|
}
|
|
70
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
|
+
|
|
71
87
|
/**
|
|
72
88
|
* @since 0.3.16-canary.1
|
|
73
89
|
*/
|
|
@@ -80,8 +96,26 @@ export class InstantiationPlanCompiler {
|
|
|
80
96
|
|
|
81
97
|
compile(binding: Binding & { kind: "class" | "resolved" }): InstantiationPlanCompileResult {
|
|
82
98
|
return binding.kind === "class"
|
|
83
|
-
? this.#compileClassPlan(binding, new Set(), 0)
|
|
84
|
-
: this.#compileResolvedPlan(binding, new Set(), 0);
|
|
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]);
|
|
85
119
|
}
|
|
86
120
|
|
|
87
121
|
// A resolved binding declares its deps as explicit descriptors — same rules as
|
|
@@ -90,21 +124,20 @@ export class InstantiationPlanCompiler {
|
|
|
90
124
|
binding: Binding & { kind: "resolved" },
|
|
91
125
|
compileStack: Set<Binding["id"]>,
|
|
92
126
|
depth: number,
|
|
127
|
+
ancestors: ReadonlyArray<Binding>,
|
|
93
128
|
): InstantiationPlanCompileResult {
|
|
94
|
-
if (depth > PLAN_DEPTH_LIMIT || compileStack.has(binding.id)) {
|
|
95
|
-
return null;
|
|
96
|
-
}
|
|
97
129
|
if (binding.onActivation !== undefined || this.#host.hasActivationHandlers(binding.token)) {
|
|
98
130
|
return null;
|
|
99
131
|
}
|
|
100
132
|
const factory = binding.factory;
|
|
101
133
|
const tokenDisplayName = tokenName(binding.token);
|
|
102
134
|
const depThunks = new Array<() => unknown>(binding.deps.length);
|
|
135
|
+
const depAncestors = [...ancestors, binding];
|
|
103
136
|
compileStack.add(binding.id);
|
|
104
137
|
try {
|
|
105
138
|
for (let index = 0; index < binding.deps.length; index += 1) {
|
|
106
|
-
const thunk = this.#
|
|
107
|
-
if (thunk ===
|
|
139
|
+
const thunk = this.#compileInjectionThunk(binding.deps[index]!, compileStack, depth, depAncestors);
|
|
140
|
+
if (thunk === PLAN_RETRY) {
|
|
108
141
|
return thunk;
|
|
109
142
|
}
|
|
110
143
|
depThunks[index] = thunk;
|
|
@@ -121,29 +154,41 @@ export class InstantiationPlanCompiler {
|
|
|
121
154
|
};
|
|
122
155
|
}
|
|
123
156
|
|
|
124
|
-
|
|
125
|
-
|
|
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: DependencySlot,
|
|
126
164
|
compileStack: Set<Binding["id"]>,
|
|
127
165
|
depth: number,
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
166
|
+
ancestors: ReadonlyArray<Binding>,
|
|
167
|
+
): DependencyCompileResult {
|
|
168
|
+
const token = descriptor.token;
|
|
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);
|
|
131
175
|
}
|
|
132
|
-
|
|
176
|
+
if (options !== undefined) {
|
|
177
|
+
return this.#compileEscapeThunk(token, ancestors, "single", options);
|
|
178
|
+
}
|
|
179
|
+
const entry = this.#host.lookupDependencyEntry(token);
|
|
133
180
|
if (entry === null) {
|
|
134
|
-
return
|
|
181
|
+
return this.#compileEscapeThunk(token, ancestors);
|
|
135
182
|
}
|
|
136
|
-
return this.#compileDepThunk(entry, compileStack, depth);
|
|
183
|
+
return this.#compileDepThunk(entry, compileStack, depth, ancestors);
|
|
137
184
|
}
|
|
138
185
|
|
|
139
186
|
#compileClassPlan(
|
|
140
187
|
binding: Binding & { kind: "class" },
|
|
141
188
|
compileStack: Set<Binding["id"]>,
|
|
142
189
|
depth: number,
|
|
190
|
+
ancestors: ReadonlyArray<Binding>,
|
|
143
191
|
): InstantiationPlanCompileResult {
|
|
144
|
-
if (depth > PLAN_DEPTH_LIMIT || compileStack.has(binding.id)) {
|
|
145
|
-
return null;
|
|
146
|
-
}
|
|
147
192
|
if (binding.onActivation !== undefined || this.#host.hasActivationHandlers(binding.token)) {
|
|
148
193
|
return null;
|
|
149
194
|
}
|
|
@@ -166,19 +211,12 @@ export class InstantiationPlanCompiler {
|
|
|
166
211
|
return () => new invokable();
|
|
167
212
|
}
|
|
168
213
|
const depThunks = new Array<() => unknown>(params.length);
|
|
214
|
+
const depAncestors = [...ancestors, binding];
|
|
169
215
|
compileStack.add(binding.id);
|
|
170
216
|
try {
|
|
171
217
|
for (let index = 0; index < params.length; index += 1) {
|
|
172
|
-
const
|
|
173
|
-
if (
|
|
174
|
-
return null;
|
|
175
|
-
}
|
|
176
|
-
const entry = this.#host.lookupDependencyEntry(param.token);
|
|
177
|
-
if (entry === null) {
|
|
178
|
-
return null;
|
|
179
|
-
}
|
|
180
|
-
const thunk = this.#compileDepThunk(entry, compileStack, depth);
|
|
181
|
-
if (thunk === null || thunk === PLAN_RETRY) {
|
|
218
|
+
const thunk = this.#compileInjectionThunk(params[index]!, compileStack, depth, depAncestors);
|
|
219
|
+
if (thunk === PLAN_RETRY) {
|
|
182
220
|
return thunk;
|
|
183
221
|
}
|
|
184
222
|
depThunks[index] = thunk;
|
|
@@ -211,30 +249,44 @@ export class InstantiationPlanCompiler {
|
|
|
211
249
|
entry: InstantiationPlanDependencyEntry,
|
|
212
250
|
compileStack: Set<Binding["id"]>,
|
|
213
251
|
depth: number,
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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;
|
|
219
259
|
}
|
|
220
|
-
const value = binding.value;
|
|
221
|
-
return () => value;
|
|
222
260
|
}
|
|
223
|
-
const scope =
|
|
261
|
+
const scope = binding.scope;
|
|
224
262
|
if (scope === "singleton") {
|
|
225
|
-
// Cached-singleton read
|
|
226
|
-
|
|
227
|
-
const
|
|
228
|
-
const
|
|
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;
|
|
229
267
|
return () => {
|
|
230
|
-
const
|
|
231
|
-
return
|
|
268
|
+
const cached = singletonBinding.instance;
|
|
269
|
+
return cached === NO_INSTANCE ? escape() : cached;
|
|
232
270
|
};
|
|
233
271
|
}
|
|
234
|
-
if (
|
|
235
|
-
|
|
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
|
+
}
|
|
236
287
|
}
|
|
237
|
-
//
|
|
238
|
-
|
|
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);
|
|
239
291
|
}
|
|
240
292
|
}
|
|
@@ -9,23 +9,28 @@ 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
|
-
|
|
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;
|
|
15
16
|
#activationVersion = 0;
|
|
16
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
|
+
|
|
17
23
|
registerActivation<const Value>(token: Token<Value> | Constructor<Value>, handler: ActivationHandler<Value>): void {
|
|
18
24
|
this.#activationVersion += 1;
|
|
25
|
+
this.#cachedToken = undefined;
|
|
26
|
+
this.#cachedHooks = undefined;
|
|
19
27
|
// ✓ TS6.0: Map.getOrInsert (ES2025)
|
|
20
|
-
const list = this.#activationHooks.getOrInsert(token as Token<unknown> | Constructor, []);
|
|
28
|
+
const list = (this.#activationHooks ??= new Map()).getOrInsert(token as Token<unknown> | Constructor, []);
|
|
21
29
|
list.push(handler as ActivationHandler<unknown>);
|
|
22
30
|
}
|
|
23
31
|
|
|
24
32
|
hasActivationHandlers<const Value>(token: Token<Value> | Constructor<Value>): boolean {
|
|
25
|
-
|
|
26
|
-
return false;
|
|
27
|
-
}
|
|
28
|
-
const list = this.#activationHooks.get(token as Token<unknown> | Constructor);
|
|
33
|
+
const list = this.activationHandlersFor(token);
|
|
29
34
|
return list !== undefined && list.length > 0;
|
|
30
35
|
}
|
|
31
36
|
|
|
@@ -37,14 +42,25 @@ export class LifecycleManager {
|
|
|
37
42
|
activationHandlersFor<const Value>(
|
|
38
43
|
token: Token<Value> | Constructor<Value>,
|
|
39
44
|
): ReadonlyArray<ActivationHandler<unknown>> | undefined {
|
|
40
|
-
|
|
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;
|
|
41
57
|
}
|
|
42
58
|
|
|
43
59
|
registerDeactivation<const Value>(
|
|
44
60
|
token: Token<Value> | Constructor<Value>,
|
|
45
61
|
handler: DeactivationHandler<Value>,
|
|
46
62
|
): void {
|
|
47
|
-
const list = this.#deactivationHooks.getOrInsert(token as Token<unknown> | Constructor, []);
|
|
63
|
+
const list = (this.#deactivationHooks ??= new Map()).getOrInsert(token as Token<unknown> | Constructor, []);
|
|
48
64
|
list.push(handler as DeactivationHandler<unknown>);
|
|
49
65
|
}
|
|
50
66
|
|
|
@@ -57,18 +73,10 @@ export class LifecycleManager {
|
|
|
57
73
|
let activatedInstance: Value = instance;
|
|
58
74
|
|
|
59
75
|
// 1. @postConstruct() — after TC39 construction (constructor + accessor addInitializer callbacks)
|
|
60
|
-
|
|
61
|
-
const
|
|
62
|
-
if (
|
|
63
|
-
|
|
64
|
-
const method = (activatedInstance as Record<string, unknown>)[methodName];
|
|
65
|
-
if (typeof method === "function") {
|
|
66
|
-
const hookResult = (method as () => unknown).call(activatedInstance);
|
|
67
|
-
if (hookResult instanceof Promise) {
|
|
68
|
-
await hookResult;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
}
|
|
76
|
+
for (const methodName of lifecycleMethods(binding, metadataReader, "postConstruct")) {
|
|
77
|
+
const hookResult = callHook(activatedInstance, methodName);
|
|
78
|
+
if (hookResult instanceof Promise) {
|
|
79
|
+
await hookResult;
|
|
72
80
|
}
|
|
73
81
|
}
|
|
74
82
|
|
|
@@ -79,7 +87,7 @@ export class LifecycleManager {
|
|
|
79
87
|
}
|
|
80
88
|
|
|
81
89
|
// 3. container-level onActivation
|
|
82
|
-
const containerHooks = this.#activationHooks
|
|
90
|
+
const containerHooks = this.#activationHooks?.get(binding.token as Token<unknown> | Constructor);
|
|
83
91
|
if (containerHooks !== undefined) {
|
|
84
92
|
for (const hook of containerHooks) {
|
|
85
93
|
const activationResult = hook(resolutionContext, activatedInstance);
|
|
@@ -99,18 +107,9 @@ export class LifecycleManager {
|
|
|
99
107
|
let activatedInstance: Value = instance;
|
|
100
108
|
|
|
101
109
|
// 1. @postConstruct() — must be sync (instance fully constructed per TC39 order)
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
for (const methodName of lifecycle.postConstruct) {
|
|
106
|
-
const method = (activatedInstance as Record<string, unknown>)[methodName];
|
|
107
|
-
if (typeof method === "function") {
|
|
108
|
-
const hookResult = (method as () => unknown).call(activatedInstance);
|
|
109
|
-
if (hookResult instanceof Promise) {
|
|
110
|
-
throw new AsyncActivationError(tokenName(binding.token), "postConstruct", methodName);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
}
|
|
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);
|
|
114
113
|
}
|
|
115
114
|
}
|
|
116
115
|
|
|
@@ -125,7 +124,7 @@ export class LifecycleManager {
|
|
|
125
124
|
|
|
126
125
|
// 3. container-level onActivation (must be sync)
|
|
127
126
|
const tokenDisplayName = tokenName(binding.token);
|
|
128
|
-
const containerHooks = this.#activationHooks
|
|
127
|
+
const containerHooks = this.#activationHooks?.get(binding.token as Token<unknown> | Constructor);
|
|
129
128
|
if (containerHooks !== undefined) {
|
|
130
129
|
for (const hook of containerHooks) {
|
|
131
130
|
const activationResult = hook(resolutionContext, activatedInstance);
|
|
@@ -147,7 +146,7 @@ export class LifecycleManager {
|
|
|
147
146
|
const tokenKey = binding.token as Token<unknown> | Constructor;
|
|
148
147
|
|
|
149
148
|
// 1. container-level onDeactivation
|
|
150
|
-
const containerHooks = this.#deactivationHooks
|
|
149
|
+
const containerHooks = this.#deactivationHooks?.get(tokenKey);
|
|
151
150
|
if (containerHooks !== undefined) {
|
|
152
151
|
for (const hook of containerHooks) {
|
|
153
152
|
const hookResult = hook(instance);
|
|
@@ -166,18 +165,10 @@ export class LifecycleManager {
|
|
|
166
165
|
}
|
|
167
166
|
|
|
168
167
|
// 3. @preDestroy() — all methods in declaration order
|
|
169
|
-
|
|
170
|
-
const
|
|
171
|
-
if (
|
|
172
|
-
|
|
173
|
-
const method = (instance as Record<string, unknown>)[methodName];
|
|
174
|
-
if (typeof method === "function") {
|
|
175
|
-
const hookResult = (method as () => unknown).call(instance);
|
|
176
|
-
if (hookResult instanceof Promise) {
|
|
177
|
-
await hookResult;
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
}
|
|
168
|
+
for (const methodName of lifecycleMethods(binding, metadataReader, "preDestroy")) {
|
|
169
|
+
const hookResult = callHook(instance, methodName);
|
|
170
|
+
if (hookResult instanceof Promise) {
|
|
171
|
+
await hookResult;
|
|
181
172
|
}
|
|
182
173
|
}
|
|
183
174
|
}
|
|
@@ -187,7 +178,7 @@ export class LifecycleManager {
|
|
|
187
178
|
const tokenKey = binding.token as Token<unknown> | Constructor;
|
|
188
179
|
|
|
189
180
|
// 1. container-level onDeactivation
|
|
190
|
-
const containerHooks = this.#deactivationHooks
|
|
181
|
+
const containerHooks = this.#deactivationHooks?.get(tokenKey);
|
|
191
182
|
if (containerHooks !== undefined) {
|
|
192
183
|
for (const hook of containerHooks) {
|
|
193
184
|
const hookResult = hook(instance);
|
|
@@ -206,19 +197,35 @@ export class LifecycleManager {
|
|
|
206
197
|
}
|
|
207
198
|
|
|
208
199
|
// 3. @preDestroy()
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
for (const methodName of lifecycle.preDestroy) {
|
|
213
|
-
const method = (instance as Record<string, unknown>)[methodName];
|
|
214
|
-
if (typeof method === "function") {
|
|
215
|
-
const hookResult = (method as () => unknown).call(instance);
|
|
216
|
-
if (hookResult instanceof Promise) {
|
|
217
|
-
throw new AsyncDeactivationError(tokenDisplayName);
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
}
|
|
200
|
+
for (const methodName of lifecycleMethods(binding, metadataReader, "preDestroy")) {
|
|
201
|
+
if (callHook(instance, methodName) instanceof Promise) {
|
|
202
|
+
throw new AsyncDeactivationError(tokenDisplayName);
|
|
221
203
|
}
|
|
222
204
|
}
|
|
223
205
|
}
|
|
206
|
+
|
|
207
|
+
/** Whether the deferred table behind `#activationHooks` has had to be built. */
|
|
208
|
+
get isBuilt(): boolean {
|
|
209
|
+
return this.#activationHooks !== undefined;
|
|
210
|
+
}
|
|
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;
|
|
224
231
|
}
|