@codefast/di 0.11.0 → 0.13.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 +151 -0
- package/README.md +128 -31
- package/dist/container/{binding-builders.d.ts → binding-chain.d.ts} +21 -28
- package/dist/container/{binding-builders.js → binding-chain.js} +53 -163
- package/dist/container/container.d.ts +11 -3
- package/dist/container/container.js +176 -102
- package/dist/core/binding-builders.d.ts +117 -0
- package/dist/core/binding-declaration.d.ts +9 -2
- package/dist/core/binding-declaration.js +5 -4
- package/dist/core/binding.d.ts +21 -149
- package/dist/core/binding.js +15 -25
- package/dist/core/module.d.ts +1 -1
- package/dist/core/registry.d.ts +9 -24
- package/dist/core/registry.js +30 -155
- package/dist/core/tag.js +1 -1
- package/dist/decorators/inject.d.ts +3 -1
- package/dist/decorators/inject.js +5 -5
- package/dist/decorators/injectable.js +2 -2
- package/dist/decorators/lifecycle.d.ts +20 -0
- package/dist/decorators/{lifecycle-decorators.js → lifecycle.js} +7 -3
- package/dist/decorators/{decorator-metadata.js → metadata-record.js} +1 -1
- package/dist/{errors/errors.d.ts → errors.d.ts} +29 -19
- package/dist/{errors/errors.js → errors.js} +43 -25
- package/dist/index.d.ts +12 -10
- package/dist/index.js +6 -6
- package/dist/injection/descriptor.d.ts +1 -1
- package/dist/introspection/dependency-graph.d.ts +1 -1
- package/dist/introspection/dependency-graph.js +3 -4
- package/dist/introspection/explanation.d.ts +101 -0
- package/dist/introspection/explanation.js +169 -0
- package/dist/introspection/inspector.d.ts +9 -2
- package/dist/introspection/inspector.js +25 -20
- package/dist/lifecycle/{lifecycle-manager.d.ts → hooks.d.ts} +11 -2
- package/dist/lifecycle/{lifecycle-manager.js → hooks.js} +51 -28
- package/dist/lifecycle/{scope-manager.d.ts → scopes.d.ts} +23 -6
- package/dist/lifecycle/{scope-manager.js → scopes.js} +93 -20
- package/dist/metadata/{metadata-keys.d.ts → keys.d.ts} +1 -1
- package/dist/metadata/{metadata-keys.js → keys.js} +1 -1
- package/dist/metadata/{metadata-reader-token.d.ts → reader-token.d.ts} +1 -1
- package/dist/metadata/{symbol-metadata-reader.d.ts → symbol-reader.d.ts} +1 -1
- package/dist/metadata/{symbol-metadata-reader.js → symbol-reader.js} +1 -1
- package/dist/metadata/{metadata-types.d.ts → types.d.ts} +1 -1
- package/dist/metadata/types.js +1 -0
- package/dist/metadata/verify.d.ts +43 -0
- package/dist/metadata/verify.js +133 -0
- package/dist/metadata/{verifying-metadata-reader.d.ts → verifying-reader.d.ts} +1 -1
- package/dist/metadata/{verifying-metadata-reader.js → verifying-reader.js} +2 -2
- package/dist/resolution/cache/activation-need.d.ts +1 -1
- package/dist/resolution/cache/activation-need.js +5 -1
- package/dist/resolution/cache/class-introspector.d.ts +27 -36
- package/dist/resolution/cache/class-introspector.js +61 -159
- package/dist/resolution/cache/{binding-lookup-cache.d.ts → lookup.d.ts} +7 -2
- package/dist/resolution/cache/{binding-lookup-cache.js → lookup.js} +40 -8
- package/dist/resolution/context.d.ts +1 -1
- package/dist/resolution/context.js +1 -1
- package/dist/resolution/{path/resolution-path.d.ts → path.d.ts} +12 -2
- package/dist/resolution/{path/resolution-path.js → path.js} +23 -3
- package/dist/resolution/plan/{plan-codegen.d.ts → codegen.d.ts} +2 -4
- package/dist/resolution/plan/{plan-codegen.js → codegen.js} +2 -4
- package/dist/resolution/plan/{instantiation-plan.d.ts → compiler.d.ts} +1 -1
- package/dist/resolution/plan/{instantiation-plan.js → compiler.js} +4 -4
- package/dist/resolution/resolver.d.ts +19 -7
- package/dist/resolution/resolver.js +343 -125
- package/dist/resolution/select/{binding-select.d.ts → candidates.d.ts} +30 -0
- package/dist/resolution/select/{binding-select.js → candidates.js} +51 -10
- package/dist/resolution/select/constraints.js +1 -1
- package/package.json +80 -72
- package/dist/core/binding-scope.d.ts +0 -12
- package/dist/core/binding-scope.js +0 -12
- package/dist/decorators/lifecycle-decorators.d.ts +0 -14
- /package/dist/{ambient/active-container.d.ts → ambient-container.d.ts} +0 -0
- /package/dist/{ambient/active-container.js → ambient-container.js} +0 -0
- /package/dist/{metadata/metadata-types.js → core/binding-builders.js} +0 -0
- /package/dist/decorators/{decorator-metadata.d.ts → metadata-record.d.ts} +0 -0
- /package/dist/injection/{resolve-options.d.ts → dependency-slot.d.ts} +0 -0
- /package/dist/injection/{resolve-options.js → dependency-slot.js} +0 -0
- /package/dist/{errors → introspection}/diagnostics.d.ts +0 -0
- /package/dist/{errors → introspection}/diagnostics.js +0 -0
- /package/dist/metadata/{metadata-reader-token.js → reader-token.js} +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { getOrInsert } from "#core/map-upsert";
|
|
2
2
|
import { advanceStateEpoch } from "#core/state-epoch";
|
|
3
3
|
import { tokenName } from "#core/token";
|
|
4
|
-
import { AsyncActivationError, AsyncDeactivationError, InvalidMetadataError } from "#errors
|
|
4
|
+
import { AsyncActivationError, AsyncDeactivationError, InvalidMetadataError } from "#errors";
|
|
5
5
|
/**
|
|
6
6
|
* One container's registry of container-level activation and deactivation hooks, keyed by token.
|
|
7
7
|
*
|
|
@@ -17,6 +17,10 @@ export class LifecycleManager {
|
|
|
17
17
|
// and registration is the only thing that can change the answer.
|
|
18
18
|
#cachedToken;
|
|
19
19
|
#cachedHooks;
|
|
20
|
+
// The class whose hooks were read last, and through which reader: a teardown walks many bindings of one class.
|
|
21
|
+
#lastLifecycleTarget;
|
|
22
|
+
#lastLifecycleReader;
|
|
23
|
+
#lastLifecycle;
|
|
20
24
|
registerActivation(token, handler) {
|
|
21
25
|
this.#activationVersion += 1;
|
|
22
26
|
advanceStateEpoch();
|
|
@@ -58,6 +62,12 @@ export class LifecycleManager {
|
|
|
58
62
|
const list = this.#deactivationHooks?.get(token);
|
|
59
63
|
return list !== undefined && list.length > 0;
|
|
60
64
|
}
|
|
65
|
+
/** Whether deactivating an instance of a binding runs anything: a container hook, the binding's own, or `@preDestroy()`. */
|
|
66
|
+
owesDeactivation(binding, metadataReader) {
|
|
67
|
+
return (this.hasDeactivationHandlers(binding.token) ||
|
|
68
|
+
(binding.kind !== "alias" && binding.deactivationHook !== undefined) ||
|
|
69
|
+
this.#lifecycleMethods(binding, metadataReader, "preDestroy").length > 0);
|
|
70
|
+
}
|
|
61
71
|
/** Every token carrying a container-level hook, paired with the phase that registered it. */
|
|
62
72
|
hookedTokens() {
|
|
63
73
|
const hooked = [];
|
|
@@ -69,34 +79,39 @@ export class LifecycleManager {
|
|
|
69
79
|
}
|
|
70
80
|
return hooked;
|
|
71
81
|
}
|
|
72
|
-
|
|
73
|
-
|
|
82
|
+
/**
|
|
83
|
+
* Runs the activation pipeline on the async lane: the activated instance, or its promise once a
|
|
84
|
+
* hook has returned one.
|
|
85
|
+
*
|
|
86
|
+
* @remarks A level yields only where a hook does, so a synchronous pipeline hands the instance
|
|
87
|
+
* back in the caller's own tick and a singleton it activates is cached before any sibling reads it.
|
|
88
|
+
*/
|
|
89
|
+
runActivation(resolutionContext, binding, instance, metadataReader) {
|
|
90
|
+
let activated = instance;
|
|
74
91
|
// 1. @postConstruct() — after TC39 construction (constructor + accessor addInitializer callbacks)
|
|
75
|
-
for (const methodName of lifecycleMethods(binding, metadataReader, "postConstruct")) {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
}
|
|
92
|
+
for (const methodName of this.#lifecycleMethods(binding, metadataReader, "postConstruct")) {
|
|
93
|
+
activated = afterActivationStep(activated, (current) => {
|
|
94
|
+
const hookResult = callHook(current, methodName);
|
|
95
|
+
return hookResult instanceof Promise ? hookResult.then(() => current) : current;
|
|
96
|
+
});
|
|
80
97
|
}
|
|
81
98
|
// 2. per-binding onActivation
|
|
82
99
|
if (binding.kind !== "alias" && binding.activationHook !== undefined) {
|
|
83
|
-
|
|
84
|
-
activatedInstance = activationResult instanceof Promise ? await activationResult : activationResult;
|
|
100
|
+
activated = afterActivationStep(activated, (current) => binding.activationHook(resolutionContext, current));
|
|
85
101
|
}
|
|
86
102
|
// 3. container-level onActivation
|
|
87
103
|
const containerHooks = this.#activationHooks?.get(binding.token);
|
|
88
104
|
if (containerHooks !== undefined) {
|
|
89
105
|
for (const hook of containerHooks) {
|
|
90
|
-
|
|
91
|
-
activatedInstance = (activationResult instanceof Promise ? await activationResult : activationResult);
|
|
106
|
+
activated = afterActivationStep(activated, (current) => hook(resolutionContext, current));
|
|
92
107
|
}
|
|
93
108
|
}
|
|
94
|
-
return
|
|
109
|
+
return activated;
|
|
95
110
|
}
|
|
96
111
|
runActivationSync(resolutionContext, binding, instance, metadataReader) {
|
|
97
112
|
let activatedInstance = instance;
|
|
98
113
|
// 1. @postConstruct() — must be sync (instance fully constructed per TC39 order)
|
|
99
|
-
for (const methodName of lifecycleMethods(binding, metadataReader, "postConstruct")) {
|
|
114
|
+
for (const methodName of this.#lifecycleMethods(binding, metadataReader, "postConstruct")) {
|
|
100
115
|
const hookResult = callHook(activatedInstance, methodName);
|
|
101
116
|
if (hookResult instanceof Promise) {
|
|
102
117
|
// The hook has already run; adopt its rejection so a failing async hook cannot become an
|
|
@@ -115,14 +130,13 @@ export class LifecycleManager {
|
|
|
115
130
|
activatedInstance = activationResult;
|
|
116
131
|
}
|
|
117
132
|
// 3. container-level onActivation (must be sync)
|
|
118
|
-
const tokenDisplayName = tokenName(binding.token);
|
|
119
133
|
const containerHooks = this.#activationHooks?.get(binding.token);
|
|
120
134
|
if (containerHooks !== undefined) {
|
|
121
135
|
for (const hook of containerHooks) {
|
|
122
136
|
const activationResult = hook(resolutionContext, activatedInstance);
|
|
123
137
|
if (activationResult instanceof Promise) {
|
|
124
138
|
void activationResult.catch(() => { });
|
|
125
|
-
throw new AsyncActivationError(
|
|
139
|
+
throw new AsyncActivationError(tokenName(binding.token), "onActivation");
|
|
126
140
|
}
|
|
127
141
|
activatedInstance = activationResult;
|
|
128
142
|
}
|
|
@@ -149,7 +163,7 @@ export class LifecycleManager {
|
|
|
149
163
|
}
|
|
150
164
|
}
|
|
151
165
|
// 3. @preDestroy() — all methods in declaration order
|
|
152
|
-
for (const methodName of lifecycleMethods(binding, metadataReader, "preDestroy")) {
|
|
166
|
+
for (const methodName of this.#lifecycleMethods(binding, metadataReader, "preDestroy")) {
|
|
153
167
|
const hookResult = callHook(instance, methodName);
|
|
154
168
|
if (hookResult instanceof Promise) {
|
|
155
169
|
await hookResult;
|
|
@@ -157,7 +171,6 @@ export class LifecycleManager {
|
|
|
157
171
|
}
|
|
158
172
|
}
|
|
159
173
|
runDeactivationSync(binding, instance, metadataReader) {
|
|
160
|
-
const tokenDisplayName = tokenName(binding.token);
|
|
161
174
|
const tokenKey = binding.token;
|
|
162
175
|
// 1. container-level onDeactivation
|
|
163
176
|
const containerHooks = this.#deactivationHooks?.get(tokenKey);
|
|
@@ -168,7 +181,7 @@ export class LifecycleManager {
|
|
|
168
181
|
// The hook has already run; adopt its rejection so a failing async hook cannot become an
|
|
169
182
|
// unhandled rejection that ends the process, then report the sync-lane violation.
|
|
170
183
|
void hookResult.catch(() => { });
|
|
171
|
-
throw new AsyncDeactivationError(
|
|
184
|
+
throw new AsyncDeactivationError(tokenName(binding.token));
|
|
172
185
|
}
|
|
173
186
|
}
|
|
174
187
|
}
|
|
@@ -177,30 +190,40 @@ export class LifecycleManager {
|
|
|
177
190
|
const hookResult = binding.deactivationHook(instance);
|
|
178
191
|
if (hookResult instanceof Promise) {
|
|
179
192
|
void hookResult.catch(() => { });
|
|
180
|
-
throw new AsyncDeactivationError(
|
|
193
|
+
throw new AsyncDeactivationError(tokenName(binding.token));
|
|
181
194
|
}
|
|
182
195
|
}
|
|
183
196
|
// 3. @preDestroy()
|
|
184
|
-
for (const methodName of lifecycleMethods(binding, metadataReader, "preDestroy")) {
|
|
197
|
+
for (const methodName of this.#lifecycleMethods(binding, metadataReader, "preDestroy")) {
|
|
185
198
|
const hookResult = callHook(instance, methodName);
|
|
186
199
|
if (hookResult instanceof Promise) {
|
|
187
200
|
void hookResult.catch(() => { });
|
|
188
|
-
throw new AsyncDeactivationError(
|
|
201
|
+
throw new AsyncDeactivationError(tokenName(binding.token));
|
|
189
202
|
}
|
|
190
203
|
}
|
|
191
204
|
}
|
|
205
|
+
/** The `@postConstruct` / `@preDestroy` methods a binding declares — only a class can declare any. */
|
|
206
|
+
#lifecycleMethods(binding, metadataReader, phase) {
|
|
207
|
+
if (binding.kind !== "class") {
|
|
208
|
+
return NO_METHODS;
|
|
209
|
+
}
|
|
210
|
+
const target = binding.target;
|
|
211
|
+
if (target !== this.#lastLifecycleTarget || metadataReader !== this.#lastLifecycleReader) {
|
|
212
|
+
this.#lastLifecycle = metadataReader.getLifecycleMetadata(target);
|
|
213
|
+
this.#lastLifecycleTarget = target;
|
|
214
|
+
this.#lastLifecycleReader = metadataReader;
|
|
215
|
+
}
|
|
216
|
+
return this.#lastLifecycle?.[phase] ?? NO_METHODS;
|
|
217
|
+
}
|
|
192
218
|
/** Whether the deferred activation-hook table has had to be built. */
|
|
193
219
|
get isActivationTableBuilt() {
|
|
194
220
|
return this.#activationHooks !== undefined;
|
|
195
221
|
}
|
|
196
222
|
}
|
|
197
223
|
const NO_METHODS = [];
|
|
198
|
-
/**
|
|
199
|
-
function
|
|
200
|
-
|
|
201
|
-
return NO_METHODS;
|
|
202
|
-
}
|
|
203
|
-
return metadataReader.getLifecycleMetadata(binding.target)?.[phase] ?? NO_METHODS;
|
|
224
|
+
/** Applies one activation step now, or once the step before it has settled. */
|
|
225
|
+
function afterActivationStep(activated, step) {
|
|
226
|
+
return activated instanceof Promise ? activated.then(step) : step(activated);
|
|
204
227
|
}
|
|
205
228
|
/**
|
|
206
229
|
* Invokes a lifecycle hook by name.
|
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
import type { Binding } from "#core/binding";
|
|
2
2
|
import type { BindingIdentifier } from "#core/types";
|
|
3
|
+
/**
|
|
4
|
+
* What deactivates a scoped instance: the resolver owning its binding, which activated it.
|
|
5
|
+
*
|
|
6
|
+
* @remarks Answers `undefined` when the binding owes no teardown, so disposing a per-request child awaits nothing.
|
|
7
|
+
*
|
|
8
|
+
* @since 0.13.0
|
|
9
|
+
*/
|
|
10
|
+
export interface ScopedInstanceOwner {
|
|
11
|
+
deactivateScoped(binding: Binding, instance: unknown): Promise<void> | undefined;
|
|
12
|
+
}
|
|
3
13
|
/**
|
|
4
14
|
* One container's instance caches — singletons, in-flight async creations, and the scoped cache.
|
|
5
15
|
*
|
|
@@ -17,8 +27,6 @@ export declare class ScopeManager {
|
|
|
17
27
|
/** Every binding in this container holding a cached singleton. */
|
|
18
28
|
cachedSingletons(): ReadonlyArray<Binding<unknown>>;
|
|
19
29
|
deleteSingleton<Value>(binding: Binding<Value>): boolean;
|
|
20
|
-
/** Swaps a re-slotted binding's tracked entry, so teardown pairs the instance with the live object. */
|
|
21
|
-
replaceSingleton(previous: Binding, next: Binding): void;
|
|
22
30
|
getInflight(id: BindingIdentifier): Promise<unknown> | undefined;
|
|
23
31
|
setInflight(id: BindingIdentifier, promise: Promise<unknown>): void;
|
|
24
32
|
clearInflight(id: BindingIdentifier): void;
|
|
@@ -28,11 +36,20 @@ export declare class ScopeManager {
|
|
|
28
36
|
* @remarks One map read answers both existence and value; a cached `undefined` is the only
|
|
29
37
|
* shape that pays for a second, and it is the rare one.
|
|
30
38
|
*/
|
|
31
|
-
readScoped(
|
|
39
|
+
readScoped(key: BindingIdentifier): unknown;
|
|
32
40
|
/** Takes the binding rather than its id, so a failure here can name the token — as `setSingleton` does. */
|
|
33
|
-
setScoped(binding: Binding, instance: unknown): void;
|
|
34
|
-
/**
|
|
35
|
-
deleteScoped(
|
|
41
|
+
setScoped(binding: Binding, instance: unknown, owner: ScopedInstanceOwner): void;
|
|
42
|
+
/** Drops the instance cached under a scoped cache key, owing it nothing — what a binding leaving `scoped` retires. */
|
|
43
|
+
deleteScoped(key: BindingIdentifier): void;
|
|
44
|
+
/** Removes the instance cached under a scoped cache key and hands it back, or {@link SCOPED_MISS} when none is. */
|
|
45
|
+
takeScoped(key: BindingIdentifier): unknown;
|
|
46
|
+
/**
|
|
47
|
+
* Deactivates every cached scoped instance through its owner, the latest cached first and one at a time, collecting
|
|
48
|
+
* each failure into `errors` — or answers `undefined` when no owner had a teardown to run.
|
|
49
|
+
*
|
|
50
|
+
* @remarks A binding cached again after its instance was taken is listed twice; only its current instance is owed.
|
|
51
|
+
*/
|
|
52
|
+
deactivateScoped(errors: Array<unknown>): Promise<void> | undefined;
|
|
36
53
|
/** Scoped instances currently cached — a structural count for diagnostics. */
|
|
37
54
|
get scopedCount(): number;
|
|
38
55
|
clearAll(): void;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { NO_INSTANCE } from "#core/binding";
|
|
2
2
|
import { tokenName } from "#core/token";
|
|
3
|
-
import { MissingScopeContextError } from "#errors
|
|
3
|
+
import { MissingScopeContextError } from "#errors";
|
|
4
4
|
/**
|
|
5
5
|
* One container's instance caches — singletons, in-flight async creations, and the scoped cache.
|
|
6
6
|
*
|
|
@@ -13,6 +13,12 @@ export class ScopeManager {
|
|
|
13
13
|
#inflight;
|
|
14
14
|
// Scoped cache — only a child container resolving a `scoped` binding ever needs it.
|
|
15
15
|
#scoped;
|
|
16
|
+
// Scoped bindings in the order their instances were cached, each beside its owner, so disposal can deactivate them
|
|
17
|
+
// latest first. The first sits in fields: a per-request child caching one scoped instance allocates nothing for it.
|
|
18
|
+
#firstScopedBinding;
|
|
19
|
+
#firstScopedOwner;
|
|
20
|
+
#laterScopedBindings;
|
|
21
|
+
#laterScopedOwners;
|
|
16
22
|
// Set once by the owning container's dispose — refuses new materializations into torn-down state.
|
|
17
23
|
#closed = false;
|
|
18
24
|
isChild;
|
|
@@ -74,17 +80,6 @@ export class ScopeManager {
|
|
|
74
80
|
this.#compacted = false;
|
|
75
81
|
return true;
|
|
76
82
|
}
|
|
77
|
-
/** Swaps a re-slotted binding's tracked entry, so teardown pairs the instance with the live object. */
|
|
78
|
-
replaceSingleton(previous, next) {
|
|
79
|
-
const tracked = this.#singletonBindings;
|
|
80
|
-
if (tracked === undefined) {
|
|
81
|
-
return;
|
|
82
|
-
}
|
|
83
|
-
const index = tracked.indexOf(previous);
|
|
84
|
-
if (index !== -1) {
|
|
85
|
-
tracked[index] = next;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
83
|
getInflight(id) {
|
|
89
84
|
return this.#inflight?.get(id);
|
|
90
85
|
}
|
|
@@ -100,27 +95,101 @@ export class ScopeManager {
|
|
|
100
95
|
* @remarks One map read answers both existence and value; a cached `undefined` is the only
|
|
101
96
|
* shape that pays for a second, and it is the rare one.
|
|
102
97
|
*/
|
|
103
|
-
readScoped(
|
|
98
|
+
readScoped(key) {
|
|
104
99
|
const scoped = this.#scoped;
|
|
105
100
|
if (scoped === undefined) {
|
|
106
101
|
return SCOPED_MISS;
|
|
107
102
|
}
|
|
108
|
-
const cached = scoped.get(
|
|
103
|
+
const cached = scoped.get(key);
|
|
109
104
|
if (cached !== undefined) {
|
|
110
105
|
return cached;
|
|
111
106
|
}
|
|
112
|
-
return scoped.has(
|
|
107
|
+
return scoped.has(key) ? undefined : SCOPED_MISS;
|
|
113
108
|
}
|
|
114
109
|
/** Takes the binding rather than its id, so a failure here can name the token — as `setSingleton` does. */
|
|
115
|
-
setScoped(binding, instance) {
|
|
110
|
+
setScoped(binding, instance, owner) {
|
|
116
111
|
if (!this.isChild) {
|
|
117
112
|
throw new MissingScopeContextError(tokenName(binding.token));
|
|
118
113
|
}
|
|
119
|
-
(this.#scoped ??= new Map()).set(binding.
|
|
114
|
+
(this.#scoped ??= new Map()).set(binding.scopedCacheKey, instance);
|
|
115
|
+
if (this.#firstScopedBinding === undefined) {
|
|
116
|
+
this.#firstScopedBinding = binding;
|
|
117
|
+
this.#firstScopedOwner = owner;
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
(this.#laterScopedBindings ??= []).push(binding);
|
|
121
|
+
(this.#laterScopedOwners ??= []).push(owner);
|
|
122
|
+
}
|
|
120
123
|
}
|
|
121
|
-
/**
|
|
122
|
-
deleteScoped(
|
|
123
|
-
this.#scoped?.delete(
|
|
124
|
+
/** Drops the instance cached under a scoped cache key, owing it nothing — what a binding leaving `scoped` retires. */
|
|
125
|
+
deleteScoped(key) {
|
|
126
|
+
this.#scoped?.delete(key);
|
|
127
|
+
}
|
|
128
|
+
/** Removes the instance cached under a scoped cache key and hands it back, or {@link SCOPED_MISS} when none is. */
|
|
129
|
+
takeScoped(key) {
|
|
130
|
+
const scoped = this.#scoped;
|
|
131
|
+
if (scoped === undefined || !scoped.has(key)) {
|
|
132
|
+
return SCOPED_MISS;
|
|
133
|
+
}
|
|
134
|
+
const instance = scoped.get(key);
|
|
135
|
+
scoped.delete(key);
|
|
136
|
+
return instance;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Deactivates every cached scoped instance through its owner, the latest cached first and one at a time, collecting
|
|
140
|
+
* each failure into `errors` — or answers `undefined` when no owner had a teardown to run.
|
|
141
|
+
*
|
|
142
|
+
* @remarks A binding cached again after its instance was taken is listed twice; only its current instance is owed.
|
|
143
|
+
*/
|
|
144
|
+
deactivateScoped(errors) {
|
|
145
|
+
if (this.#firstScopedBinding === undefined) {
|
|
146
|
+
return undefined;
|
|
147
|
+
}
|
|
148
|
+
return this.#deactivateScopedFrom(this.#laterScopedBindings?.length ?? 0, errors);
|
|
149
|
+
}
|
|
150
|
+
// Positions count down from the latest: `position - 1` in the later lists while positive, then the first at 0.
|
|
151
|
+
#deactivateScopedFrom(position, errors) {
|
|
152
|
+
for (let current = position; current >= 0; current -= 1) {
|
|
153
|
+
const pending = this.#deactivateScopedAt(current, errors);
|
|
154
|
+
if (pending !== undefined) {
|
|
155
|
+
return this.#finishDeactivatingScoped(pending, current - 1, errors);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return undefined;
|
|
159
|
+
}
|
|
160
|
+
// The rest of the walk once a teardown is pending: each one settles before the next starts.
|
|
161
|
+
async #finishDeactivatingScoped(pending, position, errors) {
|
|
162
|
+
let current = pending;
|
|
163
|
+
for (let next = position;; next -= 1) {
|
|
164
|
+
if (current !== undefined) {
|
|
165
|
+
try {
|
|
166
|
+
await current;
|
|
167
|
+
}
|
|
168
|
+
catch (error) {
|
|
169
|
+
errors.push(error);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (next < 0) {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
current = this.#deactivateScopedAt(next, errors);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
#deactivateScopedAt(position, errors) {
|
|
179
|
+
const binding = position === 0 ? this.#firstScopedBinding : this.#laterScopedBindings[position - 1];
|
|
180
|
+
const owner = position === 0 ? this.#firstScopedOwner : this.#laterScopedOwners[position - 1];
|
|
181
|
+
// The first is read last, so only a later entry can find a key cached twice; taking it leaves the rest a miss.
|
|
182
|
+
const instance = position === 0 ? this.readScoped(binding.scopedCacheKey) : this.takeScoped(binding.scopedCacheKey);
|
|
183
|
+
if (instance === SCOPED_MISS) {
|
|
184
|
+
return undefined;
|
|
185
|
+
}
|
|
186
|
+
try {
|
|
187
|
+
return owner.deactivateScoped(binding, instance);
|
|
188
|
+
}
|
|
189
|
+
catch (error) {
|
|
190
|
+
errors.push(error);
|
|
191
|
+
return undefined;
|
|
192
|
+
}
|
|
124
193
|
}
|
|
125
194
|
/** Scoped instances currently cached — a structural count for diagnostics. */
|
|
126
195
|
get scopedCount() {
|
|
@@ -137,6 +206,10 @@ export class ScopeManager {
|
|
|
137
206
|
}
|
|
138
207
|
this.#inflight?.clear();
|
|
139
208
|
this.#scoped?.clear();
|
|
209
|
+
this.#firstScopedBinding = undefined;
|
|
210
|
+
this.#firstScopedOwner = undefined;
|
|
211
|
+
this.#laterScopedBindings = undefined;
|
|
212
|
+
this.#laterScopedOwners = undefined;
|
|
140
213
|
}
|
|
141
214
|
/** Whether the deferred scoped-instance cache has had to be built. */
|
|
142
215
|
get isScopedCacheBuilt() {
|
|
@@ -17,7 +17,7 @@ export declare const LIFECYCLE_KEY: unique symbol;
|
|
|
17
17
|
*/
|
|
18
18
|
export declare const INJECT_ACCESSOR_KEY: unique symbol;
|
|
19
19
|
/**
|
|
20
|
-
* The symbol
|
|
20
|
+
* The symbol standard decorator transforms store class metadata under.
|
|
21
21
|
*
|
|
22
22
|
* @remarks Falls back to the global-registry symbol, which is what Babel and esbuild emit until
|
|
23
23
|
* a runtime ships `Symbol.metadata` natively.
|
|
@@ -17,7 +17,7 @@ export const LIFECYCLE_KEY = Symbol("di:lifecycle");
|
|
|
17
17
|
*/
|
|
18
18
|
export const INJECT_ACCESSOR_KEY = Symbol("di:inject-accessor");
|
|
19
19
|
/**
|
|
20
|
-
* The symbol
|
|
20
|
+
* The symbol standard decorator transforms store class metadata under.
|
|
21
21
|
*
|
|
22
22
|
* @remarks Falls back to the global-registry symbol, which is what Babel and esbuild emit until
|
|
23
23
|
* a runtime ships `Symbol.metadata` natively.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Constructor } from "#core/types";
|
|
2
2
|
import type { InjectionDescriptor } from "#injection/descriptor";
|
|
3
|
-
import type { ConstructorMetadata, LifecycleMetadata, MetadataReader } from "#metadata/
|
|
3
|
+
import type { ConstructorMetadata, LifecycleMetadata, MetadataReader } from "#metadata/types";
|
|
4
4
|
type AccessorEntry = {
|
|
5
5
|
readonly key: string | symbol;
|
|
6
6
|
readonly descriptor: InjectionDescriptor;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { INJECT_ACCESSOR_KEY, INJECTABLE_KEY, LIFECYCLE_KEY, METADATA_SYMBOL } from "#metadata/
|
|
1
|
+
import { INJECT_ACCESSOR_KEY, INJECTABLE_KEY, LIFECYCLE_KEY, METADATA_SYMBOL } from "#metadata/keys";
|
|
2
2
|
/**
|
|
3
3
|
* The default `MetadataReader`, reading decorator metadata stored under `Symbol.metadata`.
|
|
4
4
|
*
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Constructor } from "#core/types";
|
|
2
|
+
import type { DependencySlot } from "#injection/dependency-slot";
|
|
2
3
|
import type { InjectionDescriptor } from "#injection/descriptor";
|
|
3
|
-
import type { DependencySlot } from "#injection/resolve-options";
|
|
4
4
|
/**
|
|
5
5
|
* One constructor parameter's declaration.
|
|
6
6
|
*
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Verification of what a foreign `MetadataReader` claims about a class, done once per reader and class.
|
|
3
|
+
*/
|
|
4
|
+
import type { Constructor } from "#core/types";
|
|
5
|
+
import type { InjectionDescriptor } from "#injection/descriptor";
|
|
6
|
+
import type { ConstructorMetadata, LifecycleMetadata, MetadataReader } from "#metadata/types";
|
|
7
|
+
/**
|
|
8
|
+
* A reader's constructor metadata for a class, verified the first time this process asks.
|
|
9
|
+
*
|
|
10
|
+
* @remarks Metadata cannot change once a class is defined, so re-checking per container would charge
|
|
11
|
+
* every fresh container for a fact already established — a per-request child or a cold boot pays
|
|
12
|
+
* that repeatedly.
|
|
13
|
+
*
|
|
14
|
+
* @since 0.6.0
|
|
15
|
+
*/
|
|
16
|
+
export declare function verifyConstructorMetadata(reader: MetadataReader, target: Constructor): ConstructorMetadata | undefined;
|
|
17
|
+
/**
|
|
18
|
+
* Verifies what a reader claims about a class, since a `MetadataReader` is a public seam.
|
|
19
|
+
*
|
|
20
|
+
* @remarks Every path that reads constructor metadata comes through here, so a reader that answers
|
|
21
|
+
* wrongly is a named error at the class it described rather than a `TypeError` raised later inside a
|
|
22
|
+
* resolve. Only what a consumer dereferences is checked: `params` and each entry's `token`.
|
|
23
|
+
* `optional`/`multi` degrade to falsy without crashing, and `index` is decorative — dependencies are
|
|
24
|
+
* consumed positionally.
|
|
25
|
+
*
|
|
26
|
+
* @since 0.6.0
|
|
27
|
+
*/
|
|
28
|
+
export declare function assertConstructorMetadata(metadata: unknown, target: Constructor): ConstructorMetadata | undefined;
|
|
29
|
+
/**
|
|
30
|
+
* A reader's lifecycle metadata for a class, verified the first time this process asks.
|
|
31
|
+
*
|
|
32
|
+
* @since 0.6.0
|
|
33
|
+
*/
|
|
34
|
+
export declare function verifyLifecycleMetadata(reader: MetadataReader, target: Constructor): LifecycleMetadata | undefined;
|
|
35
|
+
/**
|
|
36
|
+
* A reader's accessor metadata for a class, verified the first time this process asks.
|
|
37
|
+
*
|
|
38
|
+
* @since 0.6.0
|
|
39
|
+
*/
|
|
40
|
+
export declare function verifyAccessorMetadata(reader: MetadataReader, target: Constructor): ReadonlyArray<{
|
|
41
|
+
readonly key: string | symbol;
|
|
42
|
+
readonly descriptor: InjectionDescriptor;
|
|
43
|
+
}> | undefined;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { InvalidMetadataError } from "#errors";
|
|
2
|
+
// Verified pairs, not verified classes: two readers may disagree about the same class, and a reader
|
|
3
|
+
// that goes out of scope takes its record with it.
|
|
4
|
+
const verifiedTargets = new WeakMap();
|
|
5
|
+
const verifiedLifecycleTargets = new WeakMap();
|
|
6
|
+
const verifiedAccessorTargets = new WeakMap();
|
|
7
|
+
function isVerified(cache, reader, target) {
|
|
8
|
+
return cache.get(reader)?.has(target) ?? false;
|
|
9
|
+
}
|
|
10
|
+
function markVerified(cache, reader, target) {
|
|
11
|
+
let verified = cache.get(reader);
|
|
12
|
+
if (verified === undefined) {
|
|
13
|
+
verified = new WeakSet();
|
|
14
|
+
cache.set(reader, verified);
|
|
15
|
+
}
|
|
16
|
+
verified.add(target);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Runs `validate` against a reader's metadata the first time this process sees the pair, then remembers it.
|
|
20
|
+
*
|
|
21
|
+
* @remarks An absent metadata answer needs no check; a verified pair is returned untouched, so the
|
|
22
|
+
* shape assertion runs once per `(reader, target)` rather than per container.
|
|
23
|
+
*/
|
|
24
|
+
function verifyOnce(cache, reader, target, metadata, validate) {
|
|
25
|
+
if (metadata === undefined || isVerified(cache, reader, target)) {
|
|
26
|
+
return metadata;
|
|
27
|
+
}
|
|
28
|
+
validate(metadata);
|
|
29
|
+
markVerified(cache, reader, target);
|
|
30
|
+
return metadata;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* A reader's constructor metadata for a class, verified the first time this process asks.
|
|
34
|
+
*
|
|
35
|
+
* @remarks Metadata cannot change once a class is defined, so re-checking per container would charge
|
|
36
|
+
* every fresh container for a fact already established — a per-request child or a cold boot pays
|
|
37
|
+
* that repeatedly.
|
|
38
|
+
*
|
|
39
|
+
* @since 0.6.0
|
|
40
|
+
*/
|
|
41
|
+
export function verifyConstructorMetadata(reader, target) {
|
|
42
|
+
return verifyOnce(verifiedTargets, reader, target, reader.getConstructorMetadata(target), (metadata) => {
|
|
43
|
+
assertConstructorMetadata(metadata, target);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Verifies what a reader claims about a class, since a `MetadataReader` is a public seam.
|
|
48
|
+
*
|
|
49
|
+
* @remarks Every path that reads constructor metadata comes through here, so a reader that answers
|
|
50
|
+
* wrongly is a named error at the class it described rather than a `TypeError` raised later inside a
|
|
51
|
+
* resolve. Only what a consumer dereferences is checked: `params` and each entry's `token`.
|
|
52
|
+
* `optional`/`multi` degrade to falsy without crashing, and `index` is decorative — dependencies are
|
|
53
|
+
* consumed positionally.
|
|
54
|
+
*
|
|
55
|
+
* @since 0.6.0
|
|
56
|
+
*/
|
|
57
|
+
export function assertConstructorMetadata(metadata, target) {
|
|
58
|
+
if (metadata === undefined) {
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
if (typeof metadata !== "object" || metadata === null) {
|
|
62
|
+
throw new InvalidMetadataError(target.name, `constructor metadata: expected an object, received ${typeof metadata}`);
|
|
63
|
+
}
|
|
64
|
+
const params = Reflect.get(metadata, "params");
|
|
65
|
+
if (!Array.isArray(params)) {
|
|
66
|
+
throw new InvalidMetadataError(target.name, "constructor metadata: params is not an array");
|
|
67
|
+
}
|
|
68
|
+
for (const [position, param] of params.entries()) {
|
|
69
|
+
if (typeof param !== "object" || param === null) {
|
|
70
|
+
throw new InvalidMetadataError(target.name, `constructor metadata: params[${String(position)}] is not an object`);
|
|
71
|
+
}
|
|
72
|
+
const dependency = Reflect.get(param, "token");
|
|
73
|
+
if (typeof dependency !== "object" && typeof dependency !== "function") {
|
|
74
|
+
throw new InvalidMetadataError(target.name, `constructor metadata: params[${String(position)}].token is not a token or a class`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return metadata;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* A reader's lifecycle metadata for a class, verified the first time this process asks.
|
|
81
|
+
*
|
|
82
|
+
* @since 0.6.0
|
|
83
|
+
*/
|
|
84
|
+
export function verifyLifecycleMetadata(reader, target) {
|
|
85
|
+
return verifyOnce(verifiedLifecycleTargets, reader, target, reader.getLifecycleMetadata(target), (metadata) => {
|
|
86
|
+
if (typeof metadata !== "object" || metadata === null) {
|
|
87
|
+
throw new InvalidMetadataError(target.name, `lifecycle metadata: expected an object, received ${typeof metadata}`);
|
|
88
|
+
}
|
|
89
|
+
for (const phase of ["postConstruct", "preDestroy"]) {
|
|
90
|
+
const methods = Reflect.get(metadata, phase);
|
|
91
|
+
if (methods === undefined) {
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (!Array.isArray(methods)) {
|
|
95
|
+
throw new InvalidMetadataError(target.name, `lifecycle metadata: ${phase} is not an array`);
|
|
96
|
+
}
|
|
97
|
+
for (const [position, name] of methods.entries()) {
|
|
98
|
+
if (typeof name !== "string") {
|
|
99
|
+
throw new InvalidMetadataError(target.name, `lifecycle metadata: ${phase}[${String(position)}] is not a string`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* A reader's accessor metadata for a class, verified the first time this process asks.
|
|
107
|
+
*
|
|
108
|
+
* @since 0.6.0
|
|
109
|
+
*/
|
|
110
|
+
export function verifyAccessorMetadata(reader, target) {
|
|
111
|
+
return verifyOnce(verifiedAccessorTargets, reader, target, reader.getAccessorMetadata?.(target), (metadata) => {
|
|
112
|
+
if (!Array.isArray(metadata)) {
|
|
113
|
+
throw new InvalidMetadataError(target.name, "accessor metadata: expected an array");
|
|
114
|
+
}
|
|
115
|
+
for (const [position, entry] of metadata.entries()) {
|
|
116
|
+
if (typeof entry !== "object" || entry === null) {
|
|
117
|
+
throw new InvalidMetadataError(target.name, `accessor metadata: [${String(position)}] is not an object`);
|
|
118
|
+
}
|
|
119
|
+
const key = Reflect.get(entry, "key");
|
|
120
|
+
if (typeof key !== "string" && typeof key !== "symbol") {
|
|
121
|
+
throw new InvalidMetadataError(target.name, `accessor metadata: [${String(position)}].key is not a string or symbol`);
|
|
122
|
+
}
|
|
123
|
+
const descriptor = Reflect.get(entry, "descriptor");
|
|
124
|
+
if (typeof descriptor !== "object" || descriptor === null) {
|
|
125
|
+
throw new InvalidMetadataError(target.name, `accessor metadata: [${String(position)}].descriptor is not an object`);
|
|
126
|
+
}
|
|
127
|
+
const dependency = Reflect.get(descriptor, "token");
|
|
128
|
+
if (typeof dependency !== "object" && typeof dependency !== "function") {
|
|
129
|
+
throw new InvalidMetadataError(target.name, `accessor metadata: [${String(position)}].descriptor.token is not a token or a class`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Wraps a foreign {@link MetadataReader} so its answers are verified before anything dereferences
|
|
3
3
|
* them, and asked for once per class.
|
|
4
4
|
*/
|
|
5
|
-
import type { MetadataReader } from "#metadata/
|
|
5
|
+
import type { MetadataReader } from "#metadata/types";
|
|
6
6
|
/**
|
|
7
7
|
* The reader a container should hand its resolver: verified and memoized when it came from outside.
|
|
8
8
|
*
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* Wraps a foreign {@link MetadataReader} so its answers are verified before anything dereferences
|
|
3
3
|
* them, and asked for once per class.
|
|
4
4
|
*/
|
|
5
|
-
import { defaultMetadataReader } from "#metadata/symbol-
|
|
6
|
-
import { verifyAccessorMetadata, verifyConstructorMetadata, verifyLifecycleMetadata
|
|
5
|
+
import { defaultMetadataReader } from "#metadata/symbol-reader";
|
|
6
|
+
import { verifyAccessorMetadata, verifyConstructorMetadata, verifyLifecycleMetadata } from "#metadata/verify";
|
|
7
7
|
// Wrapping a wrapper would stack a layer per child container, so each one is remembered.
|
|
8
8
|
const verifyingReaders = new WeakSet();
|
|
9
9
|
// One wrapper per foreign reader, so every container handed that reader shares its answers.
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import type { Binding } from "#core/binding";
|
|
8
8
|
import type { BindingRegistry } from "#core/registry";
|
|
9
|
-
import type { LifecycleManager } from "#lifecycle/
|
|
9
|
+
import type { LifecycleManager } from "#lifecycle/hooks";
|
|
10
10
|
import type { ClassIntrospector } from "#resolution/cache/class-introspector";
|
|
11
11
|
/**
|
|
12
12
|
* A per-binding cache of whether activation work — hooks or `@postConstruct` — is needed on resolve.
|