@codefast/di 0.10.0 → 0.11.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 +293 -0
- package/README.md +71 -7
- package/dist/ambient/active-container.d.ts +13 -11
- package/dist/ambient/active-container.js +8 -6
- package/dist/container/binding-builders.d.ts +25 -7
- package/dist/container/binding-builders.js +110 -42
- package/dist/container/container.d.ts +8 -8
- package/dist/container/container.js +244 -67
- package/dist/core/binding-declaration.d.ts +120 -0
- package/dist/core/binding-declaration.js +186 -0
- package/dist/core/binding-scope.d.ts +2 -2
- package/dist/core/binding.d.ts +61 -9
- package/dist/core/binding.js +49 -2
- package/dist/core/constraint-requirement.d.ts +1 -1
- package/dist/core/module.d.ts +10 -7
- package/dist/core/module.js +17 -3
- package/dist/core/registry.d.ts +17 -13
- package/dist/core/registry.js +155 -59
- 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/core/token.d.ts +1 -1
- package/dist/core/types.d.ts +5 -5
- package/dist/decorators/decorator-metadata.d.ts +9 -0
- package/dist/decorators/decorator-metadata.js +20 -0
- package/dist/decorators/inject.d.ts +3 -3
- package/dist/decorators/inject.js +7 -6
- package/dist/decorators/injectable.d.ts +2 -2
- package/dist/decorators/injectable.js +5 -3
- package/dist/decorators/lifecycle-decorators.js +9 -3
- package/dist/errors/errors.d.ts +85 -14
- package/dist/errors/errors.js +100 -20
- package/dist/index.d.ts +37 -35
- package/dist/index.js +20 -19
- package/dist/injection/descriptor.d.ts +3 -3
- package/dist/injection/descriptor.js +3 -7
- package/dist/injection/resolve-options.d.ts +3 -3
- package/dist/injection/resolve-options.js +7 -5
- package/dist/introspection/dependency-graph.d.ts +10 -5
- package/dist/introspection/dependency-graph.js +51 -28
- package/dist/introspection/graph-adapters/cytoscape.d.ts +1 -1
- package/dist/introspection/graph-adapters/dot.d.ts +1 -1
- package/dist/introspection/graph-adapters/mermaid.d.ts +1 -1
- package/dist/introspection/graph-adapters/reactflow.d.ts +1 -1
- package/dist/introspection/graph-adapters/reactflow.js +6 -4
- package/dist/introspection/inspector.d.ts +4 -4
- package/dist/introspection/inspector.js +9 -12
- package/dist/lifecycle/lifecycle-manager.d.ts +4 -4
- package/dist/lifecycle/lifecycle-manager.js +18 -6
- package/dist/lifecycle/scope-manager.d.ts +2 -2
- package/dist/lifecycle/scope-manager.js +31 -13
- package/dist/metadata/metadata-reader-token.d.ts +2 -2
- package/dist/metadata/metadata-reader-token.js +1 -1
- package/dist/metadata/metadata-types.d.ts +3 -3
- package/dist/metadata/symbol-metadata-reader.d.ts +3 -3
- package/dist/metadata/symbol-metadata-reader.js +1 -1
- package/dist/metadata/verifying-metadata-reader.d.ts +5 -4
- package/dist/metadata/verifying-metadata-reader.js +30 -8
- 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 +4 -5
- package/dist/resolution/cache/activation-need.js +11 -18
- package/dist/resolution/cache/binding-lookup-cache.d.ts +5 -12
- package/dist/resolution/cache/binding-lookup-cache.js +32 -19
- package/dist/resolution/cache/class-introspector.d.ts +16 -5
- package/dist/resolution/cache/class-introspector.js +73 -56
- package/dist/resolution/context.d.ts +17 -25
- package/dist/resolution/context.js +47 -56
- package/dist/resolution/path/resolution-path.d.ts +48 -13
- package/dist/resolution/path/resolution-path.js +90 -39
- package/dist/resolution/plan/instantiation-plan.d.ts +5 -5
- package/dist/resolution/plan/instantiation-plan.js +66 -26
- package/dist/resolution/plan/plan-codegen.d.ts +10 -7
- package/dist/resolution/plan/plan-codegen.js +62 -34
- package/dist/resolution/resolver.d.ts +16 -17
- package/dist/resolution/resolver.js +316 -273
- package/dist/resolution/select/binding-select.d.ts +6 -5
- package/dist/resolution/select/binding-select.js +5 -4
- package/dist/resolution/select/constraints.d.ts +3 -3
- package/dist/resolution/select/constraints.js +4 -4
- package/package.json +14 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { NO_INSTANCE } from "
|
|
2
|
-
import { tokenName } from "
|
|
3
|
-
import { MissingScopeContextError } from "
|
|
1
|
+
import { NO_INSTANCE } from "#core/binding";
|
|
2
|
+
import { tokenName } from "#core/token";
|
|
3
|
+
import { MissingScopeContextError } from "#errors/errors";
|
|
4
4
|
/**
|
|
5
5
|
* One container's instance caches — singletons, in-flight async creations, and the scoped cache.
|
|
6
6
|
*
|
|
@@ -39,22 +39,39 @@ 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
77
|
/** Swaps a re-slotted binding's tracked entry, so teardown pairs the instance with the live object. */
|
|
@@ -112,10 +129,11 @@ export class ScopeManager {
|
|
|
112
129
|
clearAll() {
|
|
113
130
|
const tracked = this.#singletonBindings;
|
|
114
131
|
if (tracked !== undefined) {
|
|
115
|
-
for (
|
|
116
|
-
|
|
132
|
+
for (let index = 0; index < tracked.length; index += 1) {
|
|
133
|
+
tracked[index].instance = NO_INSTANCE;
|
|
117
134
|
}
|
|
118
135
|
tracked.length = 0;
|
|
136
|
+
this.#compacted = true;
|
|
119
137
|
}
|
|
120
138
|
this.#inflight?.clear();
|
|
121
139
|
this.#scoped?.clear();
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { Token } from "
|
|
2
|
-
import type { MetadataReader } from "
|
|
1
|
+
import type { Token } from "#core/token";
|
|
2
|
+
import type { MetadataReader } from "#metadata/metadata-types";
|
|
3
3
|
/**
|
|
4
4
|
* The token a custom `MetadataReader` is bound to for a container to pick up.
|
|
5
5
|
*
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { Constructor } from "
|
|
2
|
-
import type { InjectionDescriptor } from "
|
|
3
|
-
import type { DependencySlot } from "
|
|
1
|
+
import type { Constructor } from "#core/types";
|
|
2
|
+
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
|
*
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { Constructor } from "
|
|
2
|
-
import type { InjectionDescriptor } from "
|
|
3
|
-
import type { ConstructorMetadata, LifecycleMetadata, MetadataReader } from "
|
|
1
|
+
import type { Constructor } from "#core/types";
|
|
2
|
+
import type { InjectionDescriptor } from "#injection/descriptor";
|
|
3
|
+
import type { ConstructorMetadata, LifecycleMetadata, MetadataReader } from "#metadata/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 "
|
|
1
|
+
import { INJECT_ACCESSOR_KEY, INJECTABLE_KEY, LIFECYCLE_KEY, METADATA_SYMBOL } from "#metadata/metadata-keys";
|
|
2
2
|
/**
|
|
3
3
|
* The default `MetadataReader`, reading decorator metadata stored under `Symbol.metadata`.
|
|
4
4
|
*
|
|
@@ -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 "
|
|
5
|
+
import type { MetadataReader } from "#metadata/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
|
*/
|
|
@@ -1,17 +1,34 @@
|
|
|
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 { defaultMetadataReader } from "
|
|
6
|
-
import { verifyAccessorMetadata, verifyConstructorMetadata, verifyLifecycleMetadata, } from "
|
|
5
|
+
import { defaultMetadataReader } from "#metadata/symbol-metadata-reader";
|
|
6
|
+
import { verifyAccessorMetadata, verifyConstructorMetadata, verifyLifecycleMetadata, } from "#resolution/cache/class-introspector";
|
|
7
7
|
// Wrapping a wrapper would stack a layer per child container, so each one is remembered.
|
|
8
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
|
+
}
|
|
9
25
|
/**
|
|
10
|
-
* The reader a container should hand its resolver: verified when it came from outside.
|
|
26
|
+
* The reader a container should hand its resolver: verified and memoized when it came from outside.
|
|
11
27
|
*
|
|
12
28
|
* @remarks The decorator reader writes the metadata it later reads, so there is nothing to check and
|
|
13
29
|
* nothing to pay — a container that supplies no reader of its own is left on the same code path it
|
|
14
|
-
* has always taken. A supplied reader is a claim, and only its callers can be charged for checking
|
|
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.
|
|
15
32
|
*
|
|
16
33
|
* @since 0.6.0
|
|
17
34
|
*/
|
|
@@ -19,13 +36,18 @@ export function verifyingMetadataReader(reader) {
|
|
|
19
36
|
if (reader === defaultMetadataReader || verifyingReaders.has(reader)) {
|
|
20
37
|
return reader;
|
|
21
38
|
}
|
|
39
|
+
const existing = wrapperByReader.get(reader);
|
|
40
|
+
if (existing !== undefined) {
|
|
41
|
+
return existing;
|
|
42
|
+
}
|
|
22
43
|
const verifying = {
|
|
23
|
-
getConstructorMetadata: (target) => verifyConstructorMetadata(reader, target),
|
|
24
|
-
getLifecycleMetadata: (target) => verifyLifecycleMetadata(reader, target),
|
|
44
|
+
getConstructorMetadata: memoized((target) => verifyConstructorMetadata(reader, target)),
|
|
45
|
+
getLifecycleMetadata: memoized((target) => verifyLifecycleMetadata(reader, target)),
|
|
25
46
|
...(reader.getAccessorMetadata === undefined
|
|
26
47
|
? {}
|
|
27
|
-
: { getAccessorMetadata: (target) => verifyAccessorMetadata(reader, target) }),
|
|
48
|
+
: { getAccessorMetadata: memoized((target) => verifyAccessorMetadata(reader, target)) }),
|
|
28
49
|
};
|
|
29
50
|
verifyingReaders.add(verifying);
|
|
51
|
+
wrapperByReader.set(reader, verifying);
|
|
30
52
|
return verifying;
|
|
31
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
|
+
}
|
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
* @remarks Versioned on the lifecycle manager plus the own registry, since `onActivation` can be
|
|
5
5
|
* registered at any time and a rebind mints binding ids the memo must not keep forever.
|
|
6
6
|
*/
|
|
7
|
-
import type { Binding } from "
|
|
8
|
-
import type { BindingRegistry } from "
|
|
9
|
-
import type { LifecycleManager } from "
|
|
10
|
-
import type { ClassIntrospector } from "
|
|
7
|
+
import type { Binding } from "#core/binding";
|
|
8
|
+
import type { BindingRegistry } from "#core/registry";
|
|
9
|
+
import type { LifecycleManager } from "#lifecycle/lifecycle-manager";
|
|
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.
|
|
13
13
|
*
|
|
@@ -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
|
/**
|
|
@@ -59,7 +52,7 @@ export class ActivationNeedCache {
|
|
|
59
52
|
return needsActivation;
|
|
60
53
|
}
|
|
61
54
|
this.#classes.discoverPostConstruct(binding.target);
|
|
62
|
-
|
|
55
|
+
binding.activationStamp = NO_ACTIVATION_STAMP;
|
|
63
56
|
return this.needsActivation(binding);
|
|
64
57
|
}
|
|
65
58
|
// Own hooks are answered before the memo, so both computations cover the memoizable rest only.
|
|
@@ -4,11 +4,11 @@
|
|
|
4
4
|
* @remarks Each cache links to its parent's, so a child answers from its own chain without walking
|
|
5
5
|
* the container hierarchy again.
|
|
6
6
|
*/
|
|
7
|
-
import type { Binding } from "
|
|
8
|
-
import type { BindingRegistry } from "
|
|
9
|
-
import type { BindingTag } from "
|
|
10
|
-
import type { Token } from "
|
|
11
|
-
import type { Constructor } from "
|
|
7
|
+
import type { Binding } from "#core/binding";
|
|
8
|
+
import type { BindingRegistry } from "#core/registry";
|
|
9
|
+
import type { BindingTag } from "#core/tag";
|
|
10
|
+
import type { Token } from "#core/token";
|
|
11
|
+
import type { Constructor } from "#core/types";
|
|
12
12
|
/**
|
|
13
13
|
* A token's terminal binding with alias hops already folded, plus the container that owns it.
|
|
14
14
|
*
|
|
@@ -34,13 +34,6 @@ export interface CollectionEntry {
|
|
|
34
34
|
values: ReadonlyArray<unknown> | undefined;
|
|
35
35
|
activationVersion: number;
|
|
36
36
|
}
|
|
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
37
|
/**
|
|
45
38
|
* A version-stamped cache of binding lookups by token and criterion across the container chain.
|
|
46
39
|
*
|
|
@@ -1,12 +1,5 @@
|
|
|
1
|
-
import { getOrInsertComputed } from "
|
|
2
|
-
import { stateEpoch } from "
|
|
3
|
-
/**
|
|
4
|
-
* Alias folding gives up past this many hops and defers to the full resolve loop, whose
|
|
5
|
-
* Set-based traversal detects genuine cycles exactly rather than by an arbitrary cap.
|
|
6
|
-
*
|
|
7
|
-
* @since 0.5.0-canary.8
|
|
8
|
-
*/
|
|
9
|
-
export const ALIAS_HOP_LIMIT = 32;
|
|
1
|
+
import { getOrInsertComputed } from "#core/map-upsert";
|
|
2
|
+
import { stateEpoch } from "#core/state-epoch";
|
|
10
3
|
const newTagToEntryMap = () => new Map();
|
|
11
4
|
const newNameToTagMap = () => new Map();
|
|
12
5
|
/**
|
|
@@ -76,15 +69,19 @@ export class BindingLookupCache {
|
|
|
76
69
|
}
|
|
77
70
|
/** `null` when the token's shape needs the full selection path. */
|
|
78
71
|
defaultEntry(token) {
|
|
72
|
+
// The repeat hit is the whole method, small enough for a hot caller to inline; the fill is the miss.
|
|
73
|
+
if (token === this.#lastToken && this.chainVersion() === this.#version) {
|
|
74
|
+
return this.#lastEntry;
|
|
75
|
+
}
|
|
76
|
+
return this.#defaultEntryMiss(token);
|
|
77
|
+
}
|
|
78
|
+
#defaultEntryMiss(token) {
|
|
79
79
|
const version = this.chainVersion();
|
|
80
80
|
if (version !== this.#version) {
|
|
81
81
|
this.#byToken?.clear();
|
|
82
82
|
this.#version = version;
|
|
83
83
|
this.#lastToken = undefined;
|
|
84
84
|
}
|
|
85
|
-
else if (token === this.#lastToken) {
|
|
86
|
-
return this.#lastEntry;
|
|
87
|
-
}
|
|
88
85
|
let entry;
|
|
89
86
|
if (this.#lastToken === undefined) {
|
|
90
87
|
// First token this cache generation sees: answer from the walk and defer the map entirely.
|
|
@@ -159,7 +156,12 @@ export class BindingLookupCache {
|
|
|
159
156
|
#findPairInChain(token, nameCriterion, tag) {
|
|
160
157
|
const found = this.#registry.getPairTagged(token, nameCriterion, tag);
|
|
161
158
|
if (found !== undefined) {
|
|
162
|
-
|
|
159
|
+
// A predicate candidate at this level can win the more-specific rule's first step, which this
|
|
160
|
+
// lane cannot weigh; an alias carries options through the full path. Either declines to selection.
|
|
161
|
+
if (found.kind === "alias" || this.#registry.hasPredicateCandidate(token)) {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
return { binding: found, owner: this.#owner };
|
|
163
165
|
}
|
|
164
166
|
if (this.#registry.has(token)) {
|
|
165
167
|
return null;
|
|
@@ -180,19 +182,30 @@ export class BindingLookupCache {
|
|
|
180
182
|
rememberCollection(token, entry) {
|
|
181
183
|
(this.#collections ??= new Map()).set(token, entry);
|
|
182
184
|
}
|
|
185
|
+
/**
|
|
186
|
+
* Follows alias hops to the terminal entry, or declines with `null` on a cycle, which the full
|
|
187
|
+
* resolve loop reports.
|
|
188
|
+
*
|
|
189
|
+
* @remarks The origin and the current token are compared by hand and the set holds only what lies
|
|
190
|
+
* between them, so the common one-hop alias allocates nothing and any chain is folded exactly.
|
|
191
|
+
*/
|
|
183
192
|
#foldAliases(token) {
|
|
184
193
|
let current = token;
|
|
185
|
-
|
|
194
|
+
let visited;
|
|
195
|
+
for (;;) {
|
|
186
196
|
const entry = this.#findDefaultInChain(current);
|
|
187
|
-
if (entry === null) {
|
|
197
|
+
if (entry === null || entry.binding.kind !== "alias") {
|
|
198
|
+
return entry;
|
|
199
|
+
}
|
|
200
|
+
const next = entry.binding.target;
|
|
201
|
+
if (next === token || next === current || visited?.has(next) === true) {
|
|
188
202
|
return null;
|
|
189
203
|
}
|
|
190
|
-
if (
|
|
191
|
-
|
|
204
|
+
if (current !== token) {
|
|
205
|
+
(visited ??= new Set()).add(current);
|
|
192
206
|
}
|
|
193
|
-
current =
|
|
207
|
+
current = next;
|
|
194
208
|
}
|
|
195
|
-
return null;
|
|
196
209
|
}
|
|
197
210
|
#findDefaultInChain(token) {
|
|
198
211
|
const fast = this.#registry.getFastDefault(token);
|
|
@@ -3,11 +3,11 @@
|
|
|
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 "
|
|
7
|
-
import type { Container } from "
|
|
8
|
-
import type { Constructor } from "
|
|
9
|
-
import type { InjectionDescriptor } from "
|
|
10
|
-
import type { ConstructorMetadata, LifecycleMetadata, MetadataReader } from "
|
|
6
|
+
import type { AmbientResolution } from "#ambient/active-container";
|
|
7
|
+
import type { Container } from "#container/container";
|
|
8
|
+
import type { Constructor } from "#core/types";
|
|
9
|
+
import type { InjectionDescriptor } from "#injection/descriptor";
|
|
10
|
+
import type { ConstructorMetadata, LifecycleMetadata, MetadataReader } from "#metadata/metadata-types";
|
|
11
11
|
/**
|
|
12
12
|
* A reader's constructor metadata for a class, verified the first time this process asks.
|
|
13
13
|
*
|
|
@@ -54,6 +54,17 @@ export declare class ClassIntrospector {
|
|
|
54
54
|
#private;
|
|
55
55
|
constructor(reader: MetadataReader, container: Container, inherited: ClassIntrospector | undefined);
|
|
56
56
|
constructorMetadata(target: Constructor): ConstructorMetadata | undefined;
|
|
57
|
+
/**
|
|
58
|
+
* The nearest ancestor's own constructor metadata, for a subclass that declares none of its own.
|
|
59
|
+
*
|
|
60
|
+
* @remarks Constructor metadata is never borrowed down the chain, so a subclass with an implicit
|
|
61
|
+
* constructor would be built with zero arguments; this lets the resolver name the base whose
|
|
62
|
+
* declared deps the subclass silently drops.
|
|
63
|
+
*/
|
|
64
|
+
inheritedConstructorMetadata(target: Constructor): {
|
|
65
|
+
base: Constructor;
|
|
66
|
+
metadata: ConstructorMetadata;
|
|
67
|
+
} | undefined;
|
|
57
68
|
/**
|
|
58
69
|
* Whether the class has a `@postConstruct` hook, or `undefined` until {@link discoverPostConstruct}.
|
|
59
70
|
*
|