@codefast/di 0.10.1 → 0.12.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 +376 -0
- package/README.md +94 -17
- package/dist/{ambient/active-container.d.ts → ambient-container.d.ts} +7 -2
- package/dist/{ambient/active-container.js → ambient-container.js} +6 -1
- package/dist/container/{binding-builders.d.ts → binding-chain.d.ts} +27 -16
- package/dist/container/binding-chain.js +254 -0
- package/dist/container/container.d.ts +11 -2
- package/dist/container/container.js +293 -73
- package/dist/core/binding-builders.d.ts +114 -0
- package/dist/core/binding-declaration.d.ts +127 -0
- package/dist/core/binding-declaration.js +187 -0
- package/dist/core/binding.d.ts +62 -138
- package/dist/core/binding.js +54 -17
- package/dist/core/module.d.ts +8 -5
- package/dist/core/module.js +17 -3
- package/dist/core/registry.d.ts +17 -28
- package/dist/core/registry.js +92 -133
- package/dist/core/state-epoch.d.ts +18 -1
- package/dist/core/state-epoch.js +17 -0
- package/dist/core/tag.js +1 -1
- package/dist/decorators/inject.js +6 -5
- package/dist/decorators/injectable.js +4 -2
- package/dist/decorators/{lifecycle-decorators.js → lifecycle.js} +9 -3
- package/dist/decorators/metadata-record.d.ts +9 -0
- package/dist/decorators/metadata-record.js +20 -0
- package/dist/{errors/errors.d.ts → errors.d.ts} +84 -9
- package/dist/{errors/errors.js → errors.js} +101 -17
- package/dist/index.d.ts +14 -10
- package/dist/index.js +7 -6
- package/dist/injection/{resolve-options.js → dependency-slot.js} +6 -4
- package/dist/injection/descriptor.d.ts +1 -1
- package/dist/injection/descriptor.js +3 -7
- package/dist/introspection/dependency-graph.d.ts +8 -3
- package/dist/introspection/dependency-graph.js +48 -26
- package/dist/introspection/explanation.d.ts +101 -0
- package/dist/introspection/explanation.js +151 -0
- package/dist/introspection/graph-adapters/reactflow.js +6 -4
- package/dist/introspection/inspector.d.ts +9 -2
- package/dist/introspection/inspector.js +30 -28
- package/dist/lifecycle/{lifecycle-manager.d.ts → hooks.d.ts} +9 -2
- package/dist/lifecycle/{lifecycle-manager.js → hooks.js} +59 -30
- package/dist/lifecycle/{scope-manager.d.ts → scopes.d.ts} +3 -5
- package/dist/lifecycle/{scope-manager.js → scopes.js} +36 -29
- 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} +5 -4
- package/dist/metadata/verifying-reader.js +53 -0
- package/dist/resolution/async-fan-out.d.ts +12 -0
- package/dist/resolution/async-fan-out.js +26 -0
- package/dist/resolution/cache/activation-need.d.ts +1 -2
- package/dist/resolution/cache/activation-need.js +16 -19
- package/dist/resolution/cache/class-introspector.d.ts +38 -36
- package/dist/resolution/cache/class-introspector.js +79 -159
- package/dist/resolution/cache/{binding-lookup-cache.d.ts → lookup.d.ts} +7 -9
- package/dist/resolution/cache/{binding-lookup-cache.js → lookup.js} +70 -25
- package/dist/resolution/context.d.ts +16 -24
- package/dist/resolution/context.js +47 -56
- package/dist/resolution/path.d.ts +128 -0
- package/dist/resolution/path.js +167 -0
- package/dist/resolution/plan/{plan-codegen.d.ts → codegen.d.ts} +5 -4
- package/dist/resolution/plan/{plan-codegen.js → codegen.js} +58 -32
- package/dist/resolution/plan/{instantiation-plan.d.ts → compiler.d.ts} +1 -1
- package/dist/resolution/plan/{instantiation-plan.js → compiler.js} +64 -24
- package/dist/resolution/resolver.d.ts +11 -12
- package/dist/resolution/resolver.js +573 -356
- 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 +87 -67
- package/dist/container/binding-builders.js +0 -305
- package/dist/core/binding-scope.d.ts +0 -12
- package/dist/core/binding-scope.js +0 -12
- package/dist/metadata/verifying-metadata-reader.js +0 -31
- package/dist/resolution/path/resolution-path.d.ts +0 -93
- package/dist/resolution/path/resolution-path.js +0 -116
- /package/dist/{metadata/metadata-types.js → core/binding-builders.js} +0 -0
- /package/dist/decorators/{lifecycle-decorators.d.ts → lifecycle.d.ts} +0 -0
- /package/dist/injection/{resolve-options.d.ts → dependency-slot.d.ts} +0 -0
- /package/dist/{errors → introspection}/diagnostics.d.ts +0 -0
- /package/dist/{errors → introspection}/diagnostics.js +0 -0
- /package/dist/metadata/{metadata-keys.d.ts → keys.d.ts} +0 -0
- /package/dist/metadata/{metadata-keys.js → keys.js} +0 -0
- /package/dist/metadata/{metadata-reader-token.js → reader-token.js} +0 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { effectiveBindingScope } from "#core/binding
|
|
1
|
+
import { effectiveBindingScope } from "#core/binding";
|
|
2
2
|
import { tokenName } from "#core/token";
|
|
3
|
-
import {
|
|
3
|
+
import { DefaultConstraintContext } from "#resolution/context";
|
|
4
|
+
import { selectAllBindings } from "#resolution/select/candidates";
|
|
4
5
|
// ── Inspector ────────────────────────────────────────────────────────────────────────────────────────────────────────
|
|
5
6
|
/**
|
|
6
7
|
* The read-only introspection surface behind a container's `inspect()`.
|
|
@@ -20,7 +21,7 @@ export class Inspector {
|
|
|
20
21
|
}
|
|
21
22
|
inspect() {
|
|
22
23
|
return {
|
|
23
|
-
ownBindings: this.#registry.allBindings().map((binding) =>
|
|
24
|
+
ownBindings: this.#registry.allBindings().map((binding) => snapshotOf(binding)),
|
|
24
25
|
cachedSingletonCount: this.#scope.cachedSingletons().length,
|
|
25
26
|
hasParent: this.#hasParent,
|
|
26
27
|
isDisposed: this.#isDisposed(),
|
|
@@ -28,7 +29,7 @@ export class Inspector {
|
|
|
28
29
|
}
|
|
29
30
|
lookupBindings(token) {
|
|
30
31
|
const bindings = this.#registry.getAll(token);
|
|
31
|
-
return bindings.map((binding) =>
|
|
32
|
+
return bindings.map((binding) => snapshotOf(binding));
|
|
32
33
|
}
|
|
33
34
|
/** Whether this container's own registry holds a binding the request could select. */
|
|
34
35
|
hasOwn(token, options) {
|
|
@@ -38,31 +39,32 @@ export class Inspector {
|
|
|
38
39
|
}
|
|
39
40
|
const bindings = this.#registry.getAll(token);
|
|
40
41
|
// An existence probe answers ambiguity with `true` — several matches still exist; only
|
|
41
|
-
// resolution has to pick one.
|
|
42
|
-
return bindings.length > 0 &&
|
|
42
|
+
// resolution has to pick one. A default-slot alias answers any criteria by forwarding them.
|
|
43
|
+
return (bindings.length > 0 &&
|
|
44
|
+
(selectAllBindings(bindings, options, this.#makeConstraintContext(options)).length > 0 ||
|
|
45
|
+
this.#registry.defaultSlotAlias(token) !== undefined));
|
|
43
46
|
}
|
|
44
47
|
#makeConstraintContext(options) {
|
|
45
|
-
return
|
|
46
|
-
resolutionPath: [],
|
|
47
|
-
resolutionStack: [],
|
|
48
|
-
parent: undefined,
|
|
49
|
-
ancestors: [],
|
|
50
|
-
currentResolveOptions: options,
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
#toSnapshot(binding) {
|
|
54
|
-
// Aliased, not copied: slot tags are frozen where they are built, so a caller's write throws
|
|
55
|
-
// instead of corrupting the registry — and the snapshot skips an allocation per binding.
|
|
56
|
-
const slot = binding.slot.name !== undefined
|
|
57
|
-
? { name: binding.slot.name, tags: binding.slot.tags }
|
|
58
|
-
: { tags: binding.slot.tags };
|
|
59
|
-
return {
|
|
60
|
-
tokenName: tokenName(binding.token),
|
|
61
|
-
kind: binding.kind,
|
|
62
|
-
scope: effectiveBindingScope(binding),
|
|
63
|
-
slot,
|
|
64
|
-
id: binding.identifier,
|
|
65
|
-
isMany: binding.isMany,
|
|
66
|
-
};
|
|
48
|
+
return new DefaultConstraintContext([], options);
|
|
67
49
|
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* The public snapshot of one binding, the shape every introspection read reports a binding in.
|
|
53
|
+
*
|
|
54
|
+
* @since 0.12.0
|
|
55
|
+
*/
|
|
56
|
+
export function snapshotOf(binding) {
|
|
57
|
+
// Aliased, not copied: slot tags are frozen where they are built, so a caller's write throws
|
|
58
|
+
// instead of corrupting the registry — and the snapshot skips an allocation per binding.
|
|
59
|
+
const slot = binding.slot.name !== undefined
|
|
60
|
+
? { name: binding.slot.name, tags: binding.slot.tags }
|
|
61
|
+
: { tags: binding.slot.tags };
|
|
62
|
+
return {
|
|
63
|
+
tokenName: tokenName(binding.token),
|
|
64
|
+
kind: binding.kind,
|
|
65
|
+
scope: effectiveBindingScope(binding),
|
|
66
|
+
slot,
|
|
67
|
+
id: binding.identifier,
|
|
68
|
+
isMany: binding.isMany,
|
|
69
|
+
};
|
|
68
70
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Binding } from "#core/binding";
|
|
2
2
|
import type { Token } from "#core/token";
|
|
3
3
|
import type { ActivationHandler, Constructor, DeactivationHandler, DependencyKey, ResolutionContext } from "#core/types";
|
|
4
|
-
import type { MetadataReader } from "#metadata/
|
|
4
|
+
import type { MetadataReader } from "#metadata/types";
|
|
5
5
|
/**
|
|
6
6
|
* One container's registry of container-level activation and deactivation hooks, keyed by token.
|
|
7
7
|
*
|
|
@@ -19,7 +19,14 @@ export declare class LifecycleManager {
|
|
|
19
19
|
hasDeactivationHandlers<Value>(token: Token<Value> | Constructor<Value>): boolean;
|
|
20
20
|
/** Every token carrying a container-level hook, paired with the phase that registered it. */
|
|
21
21
|
hookedTokens(): Array<[DependencyKey, "onActivation" | "onDeactivation"]>;
|
|
22
|
-
|
|
22
|
+
/**
|
|
23
|
+
* Runs the activation pipeline on the async lane: the activated instance, or its promise once a
|
|
24
|
+
* hook has returned one.
|
|
25
|
+
*
|
|
26
|
+
* @remarks A level yields only where a hook does, so a synchronous pipeline hands the instance
|
|
27
|
+
* back in the caller's own tick and a singleton it activates is cached before any sibling reads it.
|
|
28
|
+
*/
|
|
29
|
+
runActivation<Value>(resolutionContext: ResolutionContext, binding: Binding<Value>, instance: Value, metadataReader: MetadataReader): Value | Promise<Value>;
|
|
23
30
|
runActivationSync<Value>(resolutionContext: ResolutionContext, binding: Binding<Value>, instance: Value, metadataReader: MetadataReader): Value;
|
|
24
31
|
runDeactivation<Value>(binding: Binding<Value>, instance: Value, metadataReader: MetadataReader): Promise<void>;
|
|
25
32
|
runDeactivationSync<Value>(binding: Binding<Value>, instance: Value, metadataReader: MetadataReader): void;
|
|
@@ -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();
|
|
@@ -69,35 +73,44 @@ export class LifecycleManager {
|
|
|
69
73
|
}
|
|
70
74
|
return hooked;
|
|
71
75
|
}
|
|
72
|
-
|
|
73
|
-
|
|
76
|
+
/**
|
|
77
|
+
* Runs the activation pipeline on the async lane: the activated instance, or its promise once a
|
|
78
|
+
* hook has returned one.
|
|
79
|
+
*
|
|
80
|
+
* @remarks A level yields only where a hook does, so a synchronous pipeline hands the instance
|
|
81
|
+
* back in the caller's own tick and a singleton it activates is cached before any sibling reads it.
|
|
82
|
+
*/
|
|
83
|
+
runActivation(resolutionContext, binding, instance, metadataReader) {
|
|
84
|
+
let activated = instance;
|
|
74
85
|
// 1. @postConstruct() — after TC39 construction (constructor + accessor addInitializer callbacks)
|
|
75
|
-
for (const methodName of lifecycleMethods(binding, metadataReader, "postConstruct")) {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
}
|
|
86
|
+
for (const methodName of this.#lifecycleMethods(binding, metadataReader, "postConstruct")) {
|
|
87
|
+
activated = afterActivationStep(activated, (current) => {
|
|
88
|
+
const hookResult = callHook(current, methodName);
|
|
89
|
+
return hookResult instanceof Promise ? hookResult.then(() => current) : current;
|
|
90
|
+
});
|
|
80
91
|
}
|
|
81
92
|
// 2. per-binding onActivation
|
|
82
93
|
if (binding.kind !== "alias" && binding.activationHook !== undefined) {
|
|
83
|
-
|
|
84
|
-
activatedInstance = activationResult instanceof Promise ? await activationResult : activationResult;
|
|
94
|
+
activated = afterActivationStep(activated, (current) => binding.activationHook(resolutionContext, current));
|
|
85
95
|
}
|
|
86
96
|
// 3. container-level onActivation
|
|
87
97
|
const containerHooks = this.#activationHooks?.get(binding.token);
|
|
88
98
|
if (containerHooks !== undefined) {
|
|
89
99
|
for (const hook of containerHooks) {
|
|
90
|
-
|
|
91
|
-
activatedInstance = (activationResult instanceof Promise ? await activationResult : activationResult);
|
|
100
|
+
activated = afterActivationStep(activated, (current) => hook(resolutionContext, current));
|
|
92
101
|
}
|
|
93
102
|
}
|
|
94
|
-
return
|
|
103
|
+
return activated;
|
|
95
104
|
}
|
|
96
105
|
runActivationSync(resolutionContext, binding, instance, metadataReader) {
|
|
97
106
|
let activatedInstance = instance;
|
|
98
107
|
// 1. @postConstruct() — must be sync (instance fully constructed per TC39 order)
|
|
99
|
-
for (const methodName of lifecycleMethods(binding, metadataReader, "postConstruct")) {
|
|
100
|
-
|
|
108
|
+
for (const methodName of this.#lifecycleMethods(binding, metadataReader, "postConstruct")) {
|
|
109
|
+
const hookResult = callHook(activatedInstance, methodName);
|
|
110
|
+
if (hookResult instanceof Promise) {
|
|
111
|
+
// The hook has already run; adopt its rejection so a failing async hook cannot become an
|
|
112
|
+
// unhandled rejection that ends the process, then report the sync-lane violation.
|
|
113
|
+
void hookResult.catch(() => { });
|
|
101
114
|
throw new AsyncActivationError(tokenName(binding.token), "postConstruct", methodName);
|
|
102
115
|
}
|
|
103
116
|
}
|
|
@@ -105,18 +118,19 @@ export class LifecycleManager {
|
|
|
105
118
|
if (binding.kind !== "alias" && binding.activationHook !== undefined) {
|
|
106
119
|
const activationResult = binding.activationHook(resolutionContext, activatedInstance);
|
|
107
120
|
if (activationResult instanceof Promise) {
|
|
121
|
+
void activationResult.catch(() => { });
|
|
108
122
|
throw new AsyncActivationError(tokenName(binding.token), "onActivation");
|
|
109
123
|
}
|
|
110
124
|
activatedInstance = activationResult;
|
|
111
125
|
}
|
|
112
126
|
// 3. container-level onActivation (must be sync)
|
|
113
|
-
const tokenDisplayName = tokenName(binding.token);
|
|
114
127
|
const containerHooks = this.#activationHooks?.get(binding.token);
|
|
115
128
|
if (containerHooks !== undefined) {
|
|
116
129
|
for (const hook of containerHooks) {
|
|
117
130
|
const activationResult = hook(resolutionContext, activatedInstance);
|
|
118
131
|
if (activationResult instanceof Promise) {
|
|
119
|
-
|
|
132
|
+
void activationResult.catch(() => { });
|
|
133
|
+
throw new AsyncActivationError(tokenName(binding.token), "onActivation");
|
|
120
134
|
}
|
|
121
135
|
activatedInstance = activationResult;
|
|
122
136
|
}
|
|
@@ -143,7 +157,7 @@ export class LifecycleManager {
|
|
|
143
157
|
}
|
|
144
158
|
}
|
|
145
159
|
// 3. @preDestroy() — all methods in declaration order
|
|
146
|
-
for (const methodName of lifecycleMethods(binding, metadataReader, "preDestroy")) {
|
|
160
|
+
for (const methodName of this.#lifecycleMethods(binding, metadataReader, "preDestroy")) {
|
|
147
161
|
const hookResult = callHook(instance, methodName);
|
|
148
162
|
if (hookResult instanceof Promise) {
|
|
149
163
|
await hookResult;
|
|
@@ -151,7 +165,6 @@ export class LifecycleManager {
|
|
|
151
165
|
}
|
|
152
166
|
}
|
|
153
167
|
runDeactivationSync(binding, instance, metadataReader) {
|
|
154
|
-
const tokenDisplayName = tokenName(binding.token);
|
|
155
168
|
const tokenKey = binding.token;
|
|
156
169
|
// 1. container-level onDeactivation
|
|
157
170
|
const containerHooks = this.#deactivationHooks?.get(tokenKey);
|
|
@@ -159,7 +172,10 @@ export class LifecycleManager {
|
|
|
159
172
|
for (const hook of containerHooks) {
|
|
160
173
|
const hookResult = hook(instance);
|
|
161
174
|
if (hookResult instanceof Promise) {
|
|
162
|
-
|
|
175
|
+
// The hook has already run; adopt its rejection so a failing async hook cannot become an
|
|
176
|
+
// unhandled rejection that ends the process, then report the sync-lane violation.
|
|
177
|
+
void hookResult.catch(() => { });
|
|
178
|
+
throw new AsyncDeactivationError(tokenName(binding.token));
|
|
163
179
|
}
|
|
164
180
|
}
|
|
165
181
|
}
|
|
@@ -167,28 +183,41 @@ export class LifecycleManager {
|
|
|
167
183
|
if (binding.kind !== "alias" && binding.deactivationHook !== undefined) {
|
|
168
184
|
const hookResult = binding.deactivationHook(instance);
|
|
169
185
|
if (hookResult instanceof Promise) {
|
|
170
|
-
|
|
186
|
+
void hookResult.catch(() => { });
|
|
187
|
+
throw new AsyncDeactivationError(tokenName(binding.token));
|
|
171
188
|
}
|
|
172
189
|
}
|
|
173
190
|
// 3. @preDestroy()
|
|
174
|
-
for (const methodName of lifecycleMethods(binding, metadataReader, "preDestroy")) {
|
|
175
|
-
|
|
176
|
-
|
|
191
|
+
for (const methodName of this.#lifecycleMethods(binding, metadataReader, "preDestroy")) {
|
|
192
|
+
const hookResult = callHook(instance, methodName);
|
|
193
|
+
if (hookResult instanceof Promise) {
|
|
194
|
+
void hookResult.catch(() => { });
|
|
195
|
+
throw new AsyncDeactivationError(tokenName(binding.token));
|
|
177
196
|
}
|
|
178
197
|
}
|
|
179
198
|
}
|
|
199
|
+
/** The `@postConstruct` / `@preDestroy` methods a binding declares — only a class can declare any. */
|
|
200
|
+
#lifecycleMethods(binding, metadataReader, phase) {
|
|
201
|
+
if (binding.kind !== "class") {
|
|
202
|
+
return NO_METHODS;
|
|
203
|
+
}
|
|
204
|
+
const target = binding.target;
|
|
205
|
+
if (target !== this.#lastLifecycleTarget || metadataReader !== this.#lastLifecycleReader) {
|
|
206
|
+
this.#lastLifecycle = metadataReader.getLifecycleMetadata(target);
|
|
207
|
+
this.#lastLifecycleTarget = target;
|
|
208
|
+
this.#lastLifecycleReader = metadataReader;
|
|
209
|
+
}
|
|
210
|
+
return this.#lastLifecycle?.[phase] ?? NO_METHODS;
|
|
211
|
+
}
|
|
180
212
|
/** Whether the deferred activation-hook table has had to be built. */
|
|
181
213
|
get isActivationTableBuilt() {
|
|
182
214
|
return this.#activationHooks !== undefined;
|
|
183
215
|
}
|
|
184
216
|
}
|
|
185
217
|
const NO_METHODS = [];
|
|
186
|
-
/**
|
|
187
|
-
function
|
|
188
|
-
|
|
189
|
-
return NO_METHODS;
|
|
190
|
-
}
|
|
191
|
-
return metadataReader.getLifecycleMetadata(binding.target)?.[phase] ?? NO_METHODS;
|
|
218
|
+
/** Applies one activation step now, or once the step before it has settled. */
|
|
219
|
+
function afterActivationStep(activated, step) {
|
|
220
|
+
return activated instanceof Promise ? activated.then(step) : step(activated);
|
|
192
221
|
}
|
|
193
222
|
/**
|
|
194
223
|
* Invokes a lifecycle hook by name.
|
|
@@ -17,8 +17,6 @@ export declare class ScopeManager {
|
|
|
17
17
|
/** Every binding in this container holding a cached singleton. */
|
|
18
18
|
cachedSingletons(): ReadonlyArray<Binding<unknown>>;
|
|
19
19
|
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
20
|
getInflight(id: BindingIdentifier): Promise<unknown> | undefined;
|
|
23
21
|
setInflight(id: BindingIdentifier, promise: Promise<unknown>): void;
|
|
24
22
|
clearInflight(id: BindingIdentifier): void;
|
|
@@ -28,11 +26,11 @@ export declare class ScopeManager {
|
|
|
28
26
|
* @remarks One map read answers both existence and value; a cached `undefined` is the only
|
|
29
27
|
* shape that pays for a second, and it is the rare one.
|
|
30
28
|
*/
|
|
31
|
-
readScoped(
|
|
29
|
+
readScoped(key: BindingIdentifier): unknown;
|
|
32
30
|
/** Takes the binding rather than its id, so a failure here can name the token — as `setSingleton` does. */
|
|
33
31
|
setScoped(binding: Binding, instance: unknown): void;
|
|
34
|
-
/** Releases a removed binding's scoped instance. A scoped instance has no deactivation. */
|
|
35
|
-
deleteScoped(
|
|
32
|
+
/** Releases a removed binding's scoped instance, by its scoped cache key. A scoped instance has no deactivation. */
|
|
33
|
+
deleteScoped(key: BindingIdentifier): void;
|
|
36
34
|
/** Scoped instances currently cached — a structural count for diagnostics. */
|
|
37
35
|
get scopedCount(): number;
|
|
38
36
|
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
|
*
|
|
@@ -39,35 +39,41 @@ export class ScopeManager {
|
|
|
39
39
|
}
|
|
40
40
|
binding.instance = instance;
|
|
41
41
|
}
|
|
42
|
+
// A removal only clears the instance; the list keeps its entry until a read compacts it, so a
|
|
43
|
+
// teardown of a hundred singletons is a hundred field writes, not a hundred splices.
|
|
44
|
+
#compacted = true;
|
|
42
45
|
/** Every binding in this container holding a cached singleton. */
|
|
43
46
|
cachedSingletons() {
|
|
44
|
-
|
|
47
|
+
const tracked = this.#singletonBindings;
|
|
48
|
+
if (tracked === undefined) {
|
|
49
|
+
return EMPTY_BINDINGS;
|
|
50
|
+
}
|
|
51
|
+
if (!this.#compacted) {
|
|
52
|
+
// Latest materialization wins the position: walk from the end keeping each live binding once.
|
|
53
|
+
const seen = new Set();
|
|
54
|
+
const live = [];
|
|
55
|
+
for (let index = tracked.length - 1; index >= 0; index -= 1) {
|
|
56
|
+
const binding = tracked[index];
|
|
57
|
+
if (binding.instance !== NO_INSTANCE && !seen.has(binding)) {
|
|
58
|
+
seen.add(binding);
|
|
59
|
+
live.push(binding);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
live.reverse();
|
|
63
|
+
this.#singletonBindings = live;
|
|
64
|
+
this.#compacted = true;
|
|
65
|
+
return live;
|
|
66
|
+
}
|
|
67
|
+
return tracked;
|
|
45
68
|
}
|
|
46
69
|
deleteSingleton(binding) {
|
|
47
70
|
if (binding.instance === NO_INSTANCE) {
|
|
48
71
|
return false;
|
|
49
72
|
}
|
|
50
73
|
binding.instance = NO_INSTANCE;
|
|
51
|
-
|
|
52
|
-
if (tracked !== undefined) {
|
|
53
|
-
const index = tracked.indexOf(binding);
|
|
54
|
-
if (index !== -1) {
|
|
55
|
-
tracked.splice(index, 1);
|
|
56
|
-
}
|
|
57
|
-
}
|
|
74
|
+
this.#compacted = false;
|
|
58
75
|
return true;
|
|
59
76
|
}
|
|
60
|
-
/** Swaps a re-slotted binding's tracked entry, so teardown pairs the instance with the live object. */
|
|
61
|
-
replaceSingleton(previous, next) {
|
|
62
|
-
const tracked = this.#singletonBindings;
|
|
63
|
-
if (tracked === undefined) {
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
const index = tracked.indexOf(previous);
|
|
67
|
-
if (index !== -1) {
|
|
68
|
-
tracked[index] = next;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
77
|
getInflight(id) {
|
|
72
78
|
return this.#inflight?.get(id);
|
|
73
79
|
}
|
|
@@ -83,27 +89,27 @@ export class ScopeManager {
|
|
|
83
89
|
* @remarks One map read answers both existence and value; a cached `undefined` is the only
|
|
84
90
|
* shape that pays for a second, and it is the rare one.
|
|
85
91
|
*/
|
|
86
|
-
readScoped(
|
|
92
|
+
readScoped(key) {
|
|
87
93
|
const scoped = this.#scoped;
|
|
88
94
|
if (scoped === undefined) {
|
|
89
95
|
return SCOPED_MISS;
|
|
90
96
|
}
|
|
91
|
-
const cached = scoped.get(
|
|
97
|
+
const cached = scoped.get(key);
|
|
92
98
|
if (cached !== undefined) {
|
|
93
99
|
return cached;
|
|
94
100
|
}
|
|
95
|
-
return scoped.has(
|
|
101
|
+
return scoped.has(key) ? undefined : SCOPED_MISS;
|
|
96
102
|
}
|
|
97
103
|
/** Takes the binding rather than its id, so a failure here can name the token — as `setSingleton` does. */
|
|
98
104
|
setScoped(binding, instance) {
|
|
99
105
|
if (!this.isChild) {
|
|
100
106
|
throw new MissingScopeContextError(tokenName(binding.token));
|
|
101
107
|
}
|
|
102
|
-
(this.#scoped ??= new Map()).set(binding.
|
|
108
|
+
(this.#scoped ??= new Map()).set(binding.scopedCacheKey, instance);
|
|
103
109
|
}
|
|
104
|
-
/** Releases a removed binding's scoped instance. A scoped instance has no deactivation. */
|
|
105
|
-
deleteScoped(
|
|
106
|
-
this.#scoped?.delete(
|
|
110
|
+
/** Releases a removed binding's scoped instance, by its scoped cache key. A scoped instance has no deactivation. */
|
|
111
|
+
deleteScoped(key) {
|
|
112
|
+
this.#scoped?.delete(key);
|
|
107
113
|
}
|
|
108
114
|
/** Scoped instances currently cached — a structural count for diagnostics. */
|
|
109
115
|
get scopedCount() {
|
|
@@ -112,10 +118,11 @@ export class ScopeManager {
|
|
|
112
118
|
clearAll() {
|
|
113
119
|
const tracked = this.#singletonBindings;
|
|
114
120
|
if (tracked !== undefined) {
|
|
115
|
-
for (
|
|
116
|
-
|
|
121
|
+
for (let index = 0; index < tracked.length; index += 1) {
|
|
122
|
+
tracked[index].instance = NO_INSTANCE;
|
|
117
123
|
}
|
|
118
124
|
tracked.length = 0;
|
|
125
|
+
this.#compacted = true;
|
|
119
126
|
}
|
|
120
127
|
this.#inflight?.clear();
|
|
121
128
|
this.#scoped?.clear();
|
|
@@ -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
|
+
}
|