@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,14 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Wraps a foreign {@link MetadataReader} so its answers are verified before anything dereferences
|
|
3
|
-
* them.
|
|
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
|
-
* The reader a container should hand its resolver: verified when it came from outside.
|
|
7
|
+
* The reader a container should hand its resolver: verified and memoized when it came from outside.
|
|
8
8
|
*
|
|
9
9
|
* @remarks The decorator reader writes the metadata it later reads, so there is nothing to check and
|
|
10
10
|
* nothing to pay — a container that supplies no reader of its own is left on the same code path it
|
|
11
|
-
* has always taken. A supplied reader is a claim, and only its callers can be charged for checking
|
|
11
|
+
* has always taken. A supplied reader is a claim, and only its callers can be charged for checking;
|
|
12
|
+
* it is asked about a class once, however many containers read through it.
|
|
12
13
|
*
|
|
13
14
|
* @since 0.6.0
|
|
14
15
|
*/
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wraps a foreign {@link MetadataReader} so its answers are verified before anything dereferences
|
|
3
|
+
* them, and asked for once per class.
|
|
4
|
+
*/
|
|
5
|
+
import { defaultMetadataReader } from "#metadata/symbol-reader";
|
|
6
|
+
import { verifyAccessorMetadata, verifyConstructorMetadata, verifyLifecycleMetadata } from "#metadata/verify";
|
|
7
|
+
// Wrapping a wrapper would stack a layer per child container, so each one is remembered.
|
|
8
|
+
const verifyingReaders = new WeakSet();
|
|
9
|
+
// One wrapper per foreign reader, so every container handed that reader shares its answers.
|
|
10
|
+
const wrapperByReader = new WeakMap();
|
|
11
|
+
// `null` records a verified "no metadata", so an absent answer is not asked for again either.
|
|
12
|
+
function memoized(ask) {
|
|
13
|
+
const answers = new WeakMap();
|
|
14
|
+
return (target) => {
|
|
15
|
+
const known = answers.get(target);
|
|
16
|
+
if (known !== undefined) {
|
|
17
|
+
return known === null ? undefined : known;
|
|
18
|
+
}
|
|
19
|
+
// Stored only after `ask` returns, so an answer that fails verification is asked for, and fails, again.
|
|
20
|
+
const answer = ask(target);
|
|
21
|
+
answers.set(target, answer ?? null);
|
|
22
|
+
return answer;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* The reader a container should hand its resolver: verified and memoized when it came from outside.
|
|
27
|
+
*
|
|
28
|
+
* @remarks The decorator reader writes the metadata it later reads, so there is nothing to check and
|
|
29
|
+
* nothing to pay — a container that supplies no reader of its own is left on the same code path it
|
|
30
|
+
* has always taken. A supplied reader is a claim, and only its callers can be charged for checking;
|
|
31
|
+
* it is asked about a class once, however many containers read through it.
|
|
32
|
+
*
|
|
33
|
+
* @since 0.6.0
|
|
34
|
+
*/
|
|
35
|
+
export function verifyingMetadataReader(reader) {
|
|
36
|
+
if (reader === defaultMetadataReader || verifyingReaders.has(reader)) {
|
|
37
|
+
return reader;
|
|
38
|
+
}
|
|
39
|
+
const existing = wrapperByReader.get(reader);
|
|
40
|
+
if (existing !== undefined) {
|
|
41
|
+
return existing;
|
|
42
|
+
}
|
|
43
|
+
const verifying = {
|
|
44
|
+
getConstructorMetadata: memoized((target) => verifyConstructorMetadata(reader, target)),
|
|
45
|
+
getLifecycleMetadata: memoized((target) => verifyLifecycleMetadata(reader, target)),
|
|
46
|
+
...(reader.getAccessorMetadata === undefined
|
|
47
|
+
? {}
|
|
48
|
+
: { getAccessorMetadata: memoized((target) => verifyAccessorMetadata(reader, target)) }),
|
|
49
|
+
};
|
|
50
|
+
verifyingReaders.add(verifying);
|
|
51
|
+
wrapperByReader.set(reader, verifying);
|
|
52
|
+
return verifying;
|
|
53
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Settles the concurrent dependencies of one async level and reports a failure in declaration order. */
|
|
2
|
+
/**
|
|
3
|
+
* Applies `apply` to every dependency's settled value, or rejects with the first failure in declaration order.
|
|
4
|
+
*
|
|
5
|
+
* @remarks Siblings start concurrently, and `Promise.all` alone would report whichever rejection
|
|
6
|
+
* settles first — a fact about microtask depth, not about the graph. The synchronous lanes report
|
|
7
|
+
* the first failing dependency in declaration order, so this does too: the happy path is `Promise.all`,
|
|
8
|
+
* and only a failure waits for every sibling to settle and then picks the earliest one that did not.
|
|
9
|
+
*
|
|
10
|
+
* @since 0.11.0
|
|
11
|
+
*/
|
|
12
|
+
export declare function settleInOrder<Result>(pending: ReadonlyArray<unknown>, apply: (values: Array<unknown>) => Result): Promise<Result>;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** Settles the concurrent dependencies of one async level and reports a failure in declaration order. */
|
|
2
|
+
/**
|
|
3
|
+
* Applies `apply` to every dependency's settled value, or rejects with the first failure in declaration order.
|
|
4
|
+
*
|
|
5
|
+
* @remarks Siblings start concurrently, and `Promise.all` alone would report whichever rejection
|
|
6
|
+
* settles first — a fact about microtask depth, not about the graph. The synchronous lanes report
|
|
7
|
+
* the first failing dependency in declaration order, so this does too: the happy path is `Promise.all`,
|
|
8
|
+
* and only a failure waits for every sibling to settle and then picks the earliest one that did not.
|
|
9
|
+
*
|
|
10
|
+
* @since 0.11.0
|
|
11
|
+
*/
|
|
12
|
+
export function settleInOrder(pending, apply) {
|
|
13
|
+
return Promise.all(pending).then(apply, () => firstRejectionInOrder(pending));
|
|
14
|
+
}
|
|
15
|
+
function firstRejectionInOrder(pending) {
|
|
16
|
+
return Promise.allSettled(pending).then((results) => {
|
|
17
|
+
for (let index = 0; index < results.length; index += 1) {
|
|
18
|
+
const result = results[index];
|
|
19
|
+
if (result.status === "rejected") {
|
|
20
|
+
throw result.reason;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
// Unreachable: this branch only runs once `Promise.all` has rejected, so a rejection is settled.
|
|
24
|
+
throw new Error("settleInOrder: a rejected fan-out settled with no rejection");
|
|
25
|
+
});
|
|
26
|
+
}
|
|
@@ -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.
|
|
@@ -17,7 +17,6 @@ export declare class ActivationNeedCache {
|
|
|
17
17
|
#private;
|
|
18
18
|
constructor(lifecycle: LifecycleManager, classes: ClassIntrospector, registry: BindingRegistry);
|
|
19
19
|
/** Whether an answer the early returns could not give has had to allocate the memo. */
|
|
20
|
-
get isMemoBuilt(): boolean;
|
|
21
20
|
needsActivation<Value>(binding: Binding<Value>): boolean;
|
|
22
21
|
/**
|
|
23
22
|
* Settles a class binding's answer once its lifecycle metadata has actually been read, which
|
|
@@ -1,13 +1,10 @@
|
|
|
1
|
+
import { NO_ACTIVATION_STAMP } from "#core/binding";
|
|
1
2
|
/**
|
|
2
3
|
* A per-binding cache of whether activation work — hooks or `@postConstruct` — is needed on resolve.
|
|
3
4
|
*
|
|
4
5
|
* @since 0.5.0-canary.8
|
|
5
6
|
*/
|
|
6
7
|
export class ActivationNeedCache {
|
|
7
|
-
// Allocated by the first answer the early returns cannot give, so a hook-free container that
|
|
8
|
-
// resolves no class or alias never pays for it.
|
|
9
|
-
#needByBindingId;
|
|
10
|
-
#version = -1;
|
|
11
8
|
#lifecycle;
|
|
12
9
|
#classes;
|
|
13
10
|
#registry;
|
|
@@ -17,9 +14,6 @@ export class ActivationNeedCache {
|
|
|
17
14
|
this.#registry = registry;
|
|
18
15
|
}
|
|
19
16
|
/** Whether an answer the early returns could not give has had to allocate the memo. */
|
|
20
|
-
get isMemoBuilt() {
|
|
21
|
-
return this.#needByBindingId !== undefined;
|
|
22
|
-
}
|
|
23
17
|
needsActivation(binding) {
|
|
24
18
|
// The chain writes a binding's own hook in place with no version anything here can see, so it
|
|
25
19
|
// is read fresh on every call; the memo covers only container hooks and lifecycle metadata.
|
|
@@ -32,19 +26,18 @@ export class ActivationNeedCache {
|
|
|
32
26
|
if (lifecycleVersion === 0 && binding.kind !== "class" && binding.kind !== "alias") {
|
|
33
27
|
return false;
|
|
34
28
|
}
|
|
35
|
-
// The
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
29
|
+
// The stamp is the version pair the answer was computed under, doubled, plus the answer: a
|
|
30
|
+
// registry or lifecycle mutation moves the version and retires every stamp at once.
|
|
31
|
+
const stampBase = (lifecycleVersion + this.#registry.version) * 2;
|
|
32
|
+
const stamp = binding.activationStamp;
|
|
33
|
+
if (stamp === stampBase) {
|
|
34
|
+
return false;
|
|
40
35
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
if (cached !== undefined) {
|
|
44
|
-
return cached;
|
|
36
|
+
if (stamp === stampBase + 1) {
|
|
37
|
+
return true;
|
|
45
38
|
}
|
|
46
39
|
const needsActivation = binding.kind === "class" ? this.#classNeedsActivation(binding) : this.#nonClassNeedsActivation(binding);
|
|
47
|
-
|
|
40
|
+
binding.activationStamp = needsActivation ? stampBase + 1 : stampBase;
|
|
48
41
|
return needsActivation;
|
|
49
42
|
}
|
|
50
43
|
/**
|
|
@@ -55,11 +48,15 @@ export class ActivationNeedCache {
|
|
|
55
48
|
* conservative `true`.
|
|
56
49
|
*/
|
|
57
50
|
refreshAfterFirstInstantiation(binding, needsActivation) {
|
|
58
|
-
|
|
51
|
+
// A class binding answers `false` only once its lifecycle metadata is known, so only a conservative
|
|
52
|
+
// `true` can still settle, and a `false` needs no second look at the class.
|
|
53
|
+
if (!needsActivation ||
|
|
54
|
+
binding.kind !== "class" ||
|
|
55
|
+
this.#classes.knownPostConstruct(binding.target) !== undefined) {
|
|
59
56
|
return needsActivation;
|
|
60
57
|
}
|
|
61
58
|
this.#classes.discoverPostConstruct(binding.target);
|
|
62
|
-
|
|
59
|
+
binding.activationStamp = NO_ACTIVATION_STAMP;
|
|
63
60
|
return this.needsActivation(binding);
|
|
64
61
|
}
|
|
65
62
|
// Own hooks are answered before the memo, so both computations cover the memoizable rest only.
|
|
@@ -3,57 +3,55 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @remarks Metadata cannot change once a class is defined, so nothing here needs version stamping.
|
|
5
5
|
*/
|
|
6
|
-
import type { AmbientResolution } from "#ambient
|
|
6
|
+
import type { AmbientResolution } from "#ambient-container";
|
|
7
7
|
import type { Container } from "#container/container";
|
|
8
8
|
import type { Constructor } from "#core/types";
|
|
9
|
-
import type {
|
|
10
|
-
import type { ConstructorMetadata, LifecycleMetadata, MetadataReader } from "#metadata/metadata-types";
|
|
9
|
+
import type { ConstructorMetadata, MetadataReader, ParamMetadata } from "#metadata/types";
|
|
11
10
|
/**
|
|
12
|
-
*
|
|
11
|
+
* What one reader has answered about one class so far, each field unknown until first asked.
|
|
13
12
|
*
|
|
14
|
-
* @remarks
|
|
15
|
-
*
|
|
16
|
-
* that repeatedly.
|
|
13
|
+
* @remarks One record per class, so a cold resolve looks the class up once and hands the record down
|
|
14
|
+
* rather than asking a map per fact.
|
|
17
15
|
*
|
|
18
|
-
* @since 0.
|
|
16
|
+
* @since 0.12.0
|
|
19
17
|
*/
|
|
20
|
-
export
|
|
21
|
-
/**
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
*
|
|
30
|
-
* @since 0.6.0
|
|
31
|
-
*/
|
|
32
|
-
export declare function assertConstructorMetadata(metadata: unknown, target: Constructor): ConstructorMetadata | undefined;
|
|
33
|
-
/**
|
|
34
|
-
* A reader's lifecycle metadata for a class, verified the first time this process asks.
|
|
35
|
-
*
|
|
36
|
-
* @since 0.6.0
|
|
37
|
-
*/
|
|
38
|
-
export declare function verifyLifecycleMetadata(reader: MetadataReader, target: Constructor): LifecycleMetadata | undefined;
|
|
39
|
-
/**
|
|
40
|
-
* A reader's accessor metadata for a class, verified the first time this process asks.
|
|
41
|
-
*
|
|
42
|
-
* @since 0.6.0
|
|
43
|
-
*/
|
|
44
|
-
export declare function verifyAccessorMetadata(reader: MetadataReader, target: Constructor): ReadonlyArray<{
|
|
45
|
-
readonly key: string | symbol;
|
|
46
|
-
readonly descriptor: InjectionDescriptor;
|
|
47
|
-
}> | undefined;
|
|
18
|
+
export interface ClassFacts {
|
|
19
|
+
/** The reader's constructor metadata, `null` once it answered that it knows nothing about the class. */
|
|
20
|
+
constructorMetadata: ConstructorMetadata | null | undefined;
|
|
21
|
+
/** Unknown until the first instantiation reads lifecycle metadata; callers read unknown as "it does". */
|
|
22
|
+
hasPostConstruct: boolean | undefined;
|
|
23
|
+
needsActiveContainer: boolean | undefined;
|
|
24
|
+
/** The parameters an instantiation injects, settled by the first one that succeeds. */
|
|
25
|
+
params: ReadonlyArray<ParamMetadata> | undefined;
|
|
26
|
+
}
|
|
48
27
|
/**
|
|
49
28
|
* A per-class cache of constructor metadata and the activation facts derived from it.
|
|
50
29
|
*
|
|
30
|
+
* @remarks A child inheriting its parent's reader takes the parent's facts too, and resolves a class
|
|
31
|
+
* the parent already met without reading its metadata again; a root finds them by reader on its first
|
|
32
|
+
* question.
|
|
33
|
+
*
|
|
51
34
|
* @since 0.5.0-canary.8
|
|
52
35
|
*/
|
|
53
36
|
export declare class ClassIntrospector {
|
|
54
37
|
#private;
|
|
55
38
|
constructor(reader: MetadataReader, container: Container, inherited: ClassIntrospector | undefined);
|
|
39
|
+
/** The record of what this reader has answered about a class, allocated on the class's first question. */
|
|
40
|
+
facts(target: Constructor): ClassFacts;
|
|
56
41
|
constructorMetadata(target: Constructor): ConstructorMetadata | undefined;
|
|
42
|
+
/** The constructor metadata of a class whose facts the caller already holds. */
|
|
43
|
+
constructorMetadataOf(target: Constructor, facts: ClassFacts): ConstructorMetadata | undefined;
|
|
44
|
+
/**
|
|
45
|
+
* The nearest ancestor's own constructor metadata, for a subclass that declares none of its own.
|
|
46
|
+
*
|
|
47
|
+
* @remarks Constructor metadata is never borrowed down the chain, so a subclass with an implicit
|
|
48
|
+
* constructor would be built with zero arguments; this lets the resolver name the base whose
|
|
49
|
+
* declared deps the subclass silently drops.
|
|
50
|
+
*/
|
|
51
|
+
inheritedConstructorMetadata(target: Constructor): {
|
|
52
|
+
base: Constructor;
|
|
53
|
+
metadata: ConstructorMetadata;
|
|
54
|
+
} | undefined;
|
|
57
55
|
/**
|
|
58
56
|
* Whether the class has a `@postConstruct` hook, or `undefined` until {@link discoverPostConstruct}.
|
|
59
57
|
*
|
|
@@ -63,5 +61,9 @@ export declare class ClassIntrospector {
|
|
|
63
61
|
discoverPostConstruct(target: Constructor): void;
|
|
64
62
|
/** True when the class has accessor injection, which reads the container during construction. */
|
|
65
63
|
needsActiveContainer(target: Constructor): boolean;
|
|
64
|
+
/** Whether a class whose facts the caller already holds reads the container during construction. */
|
|
65
|
+
needsActiveContainerOf(target: Constructor, facts: ClassFacts): boolean;
|
|
66
66
|
instantiate(target: Constructor, deps: Array<unknown>, resolution?: AmbientResolution): unknown;
|
|
67
|
+
/** Builds an instance whose accessor need the caller already knows; an empty `deps` is a call with no arguments. */
|
|
68
|
+
construct(target: Constructor, deps: ReadonlyArray<unknown>, needsActiveContainer: boolean, resolution: AmbientResolution | undefined): unknown;
|
|
67
69
|
}
|
|
@@ -3,175 +3,87 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @remarks Metadata cannot change once a class is defined, so nothing here needs version stamping.
|
|
5
5
|
*/
|
|
6
|
-
import { constructWithAmbientResolution, runWithAmbientResolution } from "#ambient
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
return cache.get(reader)?.has(target) ?? false;
|
|
15
|
-
}
|
|
16
|
-
function markVerified(cache, reader, target) {
|
|
17
|
-
let verified = cache.get(reader);
|
|
18
|
-
if (verified === undefined) {
|
|
19
|
-
verified = new WeakSet();
|
|
20
|
-
cache.set(reader, verified);
|
|
21
|
-
}
|
|
22
|
-
verified.add(target);
|
|
23
|
-
}
|
|
24
|
-
/**
|
|
25
|
-
* Runs `validate` against a reader's metadata the first time this process sees the pair, then remembers it.
|
|
26
|
-
*
|
|
27
|
-
* @remarks An absent metadata answer needs no check; a verified pair is returned untouched, so the
|
|
28
|
-
* shape assertion runs once per `(reader, target)` rather than per container.
|
|
29
|
-
*/
|
|
30
|
-
function verifyOnce(cache, reader, target, metadata, validate) {
|
|
31
|
-
if (metadata === undefined || isVerified(cache, reader, target)) {
|
|
32
|
-
return metadata;
|
|
6
|
+
import { constructWithAmbientResolution, runWithAmbientResolution } from "#ambient-container";
|
|
7
|
+
// A reader answers from the class alone, so its facts are shared by every container reading through it.
|
|
8
|
+
const factsByReader = new WeakMap();
|
|
9
|
+
function factsFor(reader) {
|
|
10
|
+
let byClass = factsByReader.get(reader);
|
|
11
|
+
if (byClass === undefined) {
|
|
12
|
+
byClass = new WeakMap();
|
|
13
|
+
factsByReader.set(reader, byClass);
|
|
33
14
|
}
|
|
34
|
-
|
|
35
|
-
markVerified(cache, reader, target);
|
|
36
|
-
return metadata;
|
|
37
|
-
}
|
|
38
|
-
/**
|
|
39
|
-
* A reader's constructor metadata for a class, verified the first time this process asks.
|
|
40
|
-
*
|
|
41
|
-
* @remarks Metadata cannot change once a class is defined, so re-checking per container would charge
|
|
42
|
-
* every fresh container for a fact already established — a per-request child or a cold boot pays
|
|
43
|
-
* that repeatedly.
|
|
44
|
-
*
|
|
45
|
-
* @since 0.6.0
|
|
46
|
-
*/
|
|
47
|
-
export function verifyConstructorMetadata(reader, target) {
|
|
48
|
-
return verifyOnce(verifiedTargets, reader, target, reader.getConstructorMetadata(target), (metadata) => {
|
|
49
|
-
assertConstructorMetadata(metadata, target);
|
|
50
|
-
});
|
|
51
|
-
}
|
|
52
|
-
/**
|
|
53
|
-
* Verifies what a reader claims about a class, since a `MetadataReader` is a public seam.
|
|
54
|
-
*
|
|
55
|
-
* @remarks Every path that reads constructor metadata comes through here, so a reader that answers
|
|
56
|
-
* wrongly is a named error at the class it described rather than a `TypeError` raised later inside a
|
|
57
|
-
* resolve. Only what a consumer dereferences is checked: `params` and each entry's `token`.
|
|
58
|
-
* `optional`/`multi` degrade to falsy without crashing, and `index` is decorative — dependencies are
|
|
59
|
-
* consumed positionally.
|
|
60
|
-
*
|
|
61
|
-
* @since 0.6.0
|
|
62
|
-
*/
|
|
63
|
-
export function assertConstructorMetadata(metadata, target) {
|
|
64
|
-
if (metadata === undefined) {
|
|
65
|
-
return undefined;
|
|
66
|
-
}
|
|
67
|
-
if (typeof metadata !== "object" || metadata === null) {
|
|
68
|
-
throw new InvalidMetadataError(target.name, `constructor metadata: expected an object, received ${typeof metadata}`);
|
|
69
|
-
}
|
|
70
|
-
const params = Reflect.get(metadata, "params");
|
|
71
|
-
if (!Array.isArray(params)) {
|
|
72
|
-
throw new InvalidMetadataError(target.name, "constructor metadata: params is not an array");
|
|
73
|
-
}
|
|
74
|
-
for (const [position, param] of params.entries()) {
|
|
75
|
-
if (typeof param !== "object" || param === null) {
|
|
76
|
-
throw new InvalidMetadataError(target.name, `constructor metadata: params[${String(position)}] is not an object`);
|
|
77
|
-
}
|
|
78
|
-
const dependency = Reflect.get(param, "token");
|
|
79
|
-
if (typeof dependency !== "object" && typeof dependency !== "function") {
|
|
80
|
-
throw new InvalidMetadataError(target.name, `constructor metadata: params[${String(position)}].token is not a token or a class`);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
return metadata;
|
|
84
|
-
}
|
|
85
|
-
/**
|
|
86
|
-
* A reader's lifecycle metadata for a class, verified the first time this process asks.
|
|
87
|
-
*
|
|
88
|
-
* @since 0.6.0
|
|
89
|
-
*/
|
|
90
|
-
export function verifyLifecycleMetadata(reader, target) {
|
|
91
|
-
return verifyOnce(verifiedLifecycleTargets, reader, target, reader.getLifecycleMetadata(target), (metadata) => {
|
|
92
|
-
if (typeof metadata !== "object" || metadata === null) {
|
|
93
|
-
throw new InvalidMetadataError(target.name, `lifecycle metadata: expected an object, received ${typeof metadata}`);
|
|
94
|
-
}
|
|
95
|
-
for (const phase of ["postConstruct", "preDestroy"]) {
|
|
96
|
-
const methods = Reflect.get(metadata, phase);
|
|
97
|
-
if (methods === undefined) {
|
|
98
|
-
continue;
|
|
99
|
-
}
|
|
100
|
-
if (!Array.isArray(methods)) {
|
|
101
|
-
throw new InvalidMetadataError(target.name, `lifecycle metadata: ${phase} is not an array`);
|
|
102
|
-
}
|
|
103
|
-
for (const [position, name] of methods.entries()) {
|
|
104
|
-
if (typeof name !== "string") {
|
|
105
|
-
throw new InvalidMetadataError(target.name, `lifecycle metadata: ${phase}[${String(position)}] is not a string`);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
/**
|
|
112
|
-
* A reader's accessor metadata for a class, verified the first time this process asks.
|
|
113
|
-
*
|
|
114
|
-
* @since 0.6.0
|
|
115
|
-
*/
|
|
116
|
-
export function verifyAccessorMetadata(reader, target) {
|
|
117
|
-
return verifyOnce(verifiedAccessorTargets, reader, target, reader.getAccessorMetadata?.(target), (metadata) => {
|
|
118
|
-
if (!Array.isArray(metadata)) {
|
|
119
|
-
throw new InvalidMetadataError(target.name, "accessor metadata: expected an array");
|
|
120
|
-
}
|
|
121
|
-
for (const [position, entry] of metadata.entries()) {
|
|
122
|
-
if (typeof entry !== "object" || entry === null) {
|
|
123
|
-
throw new InvalidMetadataError(target.name, `accessor metadata: [${String(position)}] is not an object`);
|
|
124
|
-
}
|
|
125
|
-
const key = Reflect.get(entry, "key");
|
|
126
|
-
if (typeof key !== "string" && typeof key !== "symbol") {
|
|
127
|
-
throw new InvalidMetadataError(target.name, `accessor metadata: [${String(position)}].key is not a string or symbol`);
|
|
128
|
-
}
|
|
129
|
-
const descriptor = Reflect.get(entry, "descriptor");
|
|
130
|
-
if (typeof descriptor !== "object" || descriptor === null) {
|
|
131
|
-
throw new InvalidMetadataError(target.name, `accessor metadata: [${String(position)}].descriptor is not an object`);
|
|
132
|
-
}
|
|
133
|
-
const dependency = Reflect.get(descriptor, "token");
|
|
134
|
-
if (typeof dependency !== "object" && typeof dependency !== "function") {
|
|
135
|
-
throw new InvalidMetadataError(target.name, `accessor metadata: [${String(position)}].descriptor.token is not a token or a class`);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
|
-
const cachesByReader = new WeakMap();
|
|
141
|
-
function cachesFor(reader) {
|
|
142
|
-
let caches = cachesByReader.get(reader);
|
|
143
|
-
if (caches === undefined) {
|
|
144
|
-
caches = { constructorMetadata: undefined, hasPostConstruct: undefined, needsActiveContainer: undefined };
|
|
145
|
-
cachesByReader.set(reader, caches);
|
|
146
|
-
}
|
|
147
|
-
return caches;
|
|
15
|
+
return byClass;
|
|
148
16
|
}
|
|
149
17
|
/**
|
|
150
18
|
* A per-class cache of constructor metadata and the activation facts derived from it.
|
|
151
19
|
*
|
|
20
|
+
* @remarks A child inheriting its parent's reader takes the parent's facts too, and resolves a class
|
|
21
|
+
* the parent already met without reading its metadata again; a root finds them by reader on its first
|
|
22
|
+
* question.
|
|
23
|
+
*
|
|
152
24
|
* @since 0.5.0-canary.8
|
|
153
25
|
*/
|
|
154
26
|
export class ClassIntrospector {
|
|
155
|
-
#
|
|
27
|
+
#byClass;
|
|
28
|
+
// The class asked about last and its record: a cold resolve asks about one class several times in a row.
|
|
29
|
+
#lastTarget;
|
|
30
|
+
#lastFacts;
|
|
156
31
|
#reader;
|
|
157
32
|
#container;
|
|
158
33
|
constructor(reader, container, inherited) {
|
|
159
|
-
this.#
|
|
34
|
+
this.#byClass = inherited !== undefined && inherited.#reader === reader ? inherited.#byClass : undefined;
|
|
160
35
|
this.#reader = reader;
|
|
161
36
|
this.#container = container;
|
|
162
37
|
}
|
|
163
|
-
|
|
164
|
-
|
|
38
|
+
/** The record of what this reader has answered about a class, allocated on the class's first question. */
|
|
39
|
+
facts(target) {
|
|
40
|
+
if (target === this.#lastTarget) {
|
|
41
|
+
return this.#lastFacts;
|
|
42
|
+
}
|
|
43
|
+
const byClass = (this.#byClass ??= factsFor(this.#reader));
|
|
44
|
+
let facts = byClass.get(target);
|
|
45
|
+
if (facts === undefined) {
|
|
46
|
+
facts = {
|
|
47
|
+
constructorMetadata: undefined,
|
|
48
|
+
hasPostConstruct: undefined,
|
|
49
|
+
needsActiveContainer: undefined,
|
|
50
|
+
params: undefined,
|
|
51
|
+
};
|
|
52
|
+
byClass.set(target, facts);
|
|
53
|
+
}
|
|
54
|
+
this.#lastTarget = target;
|
|
55
|
+
this.#lastFacts = facts;
|
|
56
|
+
return facts;
|
|
165
57
|
}
|
|
166
58
|
constructorMetadata(target) {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
59
|
+
return this.constructorMetadataOf(target, this.facts(target));
|
|
60
|
+
}
|
|
61
|
+
/** The constructor metadata of a class whose facts the caller already holds. */
|
|
62
|
+
constructorMetadataOf(target, facts) {
|
|
63
|
+
let metadata = facts.constructorMetadata;
|
|
64
|
+
if (metadata === undefined) {
|
|
65
|
+
metadata = this.#reader.getConstructorMetadata(target) ?? null;
|
|
66
|
+
facts.constructorMetadata = metadata;
|
|
67
|
+
}
|
|
68
|
+
return metadata === null ? undefined : metadata;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* The nearest ancestor's own constructor metadata, for a subclass that declares none of its own.
|
|
72
|
+
*
|
|
73
|
+
* @remarks Constructor metadata is never borrowed down the chain, so a subclass with an implicit
|
|
74
|
+
* constructor would be built with zero arguments; this lets the resolver name the base whose
|
|
75
|
+
* declared deps the subclass silently drops.
|
|
76
|
+
*/
|
|
77
|
+
inheritedConstructorMetadata(target) {
|
|
78
|
+
let current = Object.getPrototypeOf(target);
|
|
79
|
+
while (typeof current === "function" && current !== Function.prototype) {
|
|
80
|
+
const metadata = this.constructorMetadata(current);
|
|
81
|
+
if (metadata !== undefined) {
|
|
82
|
+
return { base: current, metadata };
|
|
83
|
+
}
|
|
84
|
+
current = Object.getPrototypeOf(current);
|
|
171
85
|
}
|
|
172
|
-
|
|
173
|
-
(caches.constructorMetadata ??= new WeakMap()).set(target, metadata ?? null);
|
|
174
|
-
return metadata;
|
|
86
|
+
return undefined;
|
|
175
87
|
}
|
|
176
88
|
/**
|
|
177
89
|
* Whether the class has a `@postConstruct` hook, or `undefined` until {@link discoverPostConstruct}.
|
|
@@ -179,26 +91,34 @@ export class ClassIntrospector {
|
|
|
179
91
|
* @remarks Callers treat unknown as "assume it does", so the first activation settles it.
|
|
180
92
|
*/
|
|
181
93
|
knownPostConstruct(target) {
|
|
182
|
-
return this
|
|
94
|
+
return this.facts(target).hasPostConstruct;
|
|
183
95
|
}
|
|
184
96
|
discoverPostConstruct(target) {
|
|
185
97
|
const lifecycle = this.#reader.getLifecycleMetadata(target);
|
|
186
|
-
|
|
98
|
+
this.facts(target).hasPostConstruct =
|
|
99
|
+
lifecycle !== undefined && lifecycle.postConstruct !== undefined && lifecycle.postConstruct.length > 0;
|
|
187
100
|
}
|
|
188
101
|
/** True when the class has accessor injection, which reads the container during construction. */
|
|
189
102
|
needsActiveContainer(target) {
|
|
190
|
-
|
|
191
|
-
|
|
103
|
+
return this.needsActiveContainerOf(target, this.facts(target));
|
|
104
|
+
}
|
|
105
|
+
/** Whether a class whose facts the caller already holds reads the container during construction. */
|
|
106
|
+
needsActiveContainerOf(target, facts) {
|
|
107
|
+
let needsActiveContainer = facts.needsActiveContainer;
|
|
192
108
|
if (needsActiveContainer === undefined) {
|
|
193
109
|
needsActiveContainer = (this.#reader.getAccessorMetadata?.(target)?.length ?? 0) > 0;
|
|
194
|
-
|
|
110
|
+
facts.needsActiveContainer = needsActiveContainer;
|
|
195
111
|
}
|
|
196
112
|
return needsActiveContainer;
|
|
197
113
|
}
|
|
198
114
|
instantiate(target, deps, resolution) {
|
|
115
|
+
return this.construct(target, deps, this.needsActiveContainer(target), resolution);
|
|
116
|
+
}
|
|
117
|
+
/** Builds an instance whose accessor need the caller already knows; an empty `deps` is a call with no arguments. */
|
|
118
|
+
construct(target, deps, needsActiveContainer, resolution) {
|
|
199
119
|
const invokable = target;
|
|
200
|
-
if (!
|
|
201
|
-
return new invokable(...deps);
|
|
120
|
+
if (!needsActiveContainer) {
|
|
121
|
+
return deps.length === 0 ? new invokable() : new invokable(...deps);
|
|
202
122
|
}
|
|
203
123
|
if (resolution === undefined) {
|
|
204
124
|
return runWithAmbientResolution(this.#container, undefined, () => new invokable(...deps));
|
|
@@ -25,7 +25,8 @@ export interface DefaultLookupEntry<Owner> {
|
|
|
25
25
|
*
|
|
26
26
|
* @remarks `values` is kept only while every member is a hook-free constant or a hook-free singleton
|
|
27
27
|
* whose instance is cached, and `activationVersion` is the chain's activation version that promise was
|
|
28
|
-
* made under.
|
|
28
|
+
* made under. It is settled on the read that repeats the first, which `readBefore` records, so a
|
|
29
|
+
* container read once never builds it.
|
|
29
30
|
*
|
|
30
31
|
* @since 0.10.0
|
|
31
32
|
*/
|
|
@@ -33,14 +34,8 @@ export interface CollectionEntry {
|
|
|
33
34
|
readonly candidates: ReadonlyArray<Binding>;
|
|
34
35
|
values: ReadonlyArray<unknown> | undefined;
|
|
35
36
|
activationVersion: number;
|
|
37
|
+
readBefore: boolean;
|
|
36
38
|
}
|
|
37
|
-
/**
|
|
38
|
-
* Alias folding gives up past this many hops and defers to the full resolve loop, whose
|
|
39
|
-
* Set-based traversal detects genuine cycles exactly rather than by an arbitrary cap.
|
|
40
|
-
*
|
|
41
|
-
* @since 0.5.0-canary.8
|
|
42
|
-
*/
|
|
43
|
-
export declare const ALIAS_HOP_LIMIT = 32;
|
|
44
39
|
/**
|
|
45
40
|
* A version-stamped cache of binding lookups by token and criterion across the container chain.
|
|
46
41
|
*
|
|
@@ -72,6 +67,9 @@ export declare class BindingLookupCache<Owner> {
|
|
|
72
67
|
namedTaggedEntry(token: Token<unknown> | Constructor, nameCriterion: BindingTag, tag: BindingTag): DefaultLookupEntry<Owner> | null;
|
|
73
68
|
/** The memoized root-level collection for a token, or `undefined` once the chain changed since it was stored. */
|
|
74
69
|
collection(token: Token<unknown> | Constructor): CollectionEntry | undefined;
|
|
75
|
-
/**
|
|
70
|
+
/**
|
|
71
|
+
* Stores a root-level collection under the chain version the last `collection()` read stamped, so a
|
|
72
|
+
* predicate that binds during the gather leaves the entry stale rather than current.
|
|
73
|
+
*/
|
|
76
74
|
rememberCollection(token: Token<unknown> | Constructor, entry: CollectionEntry): void;
|
|
77
75
|
}
|