@codefast/di 0.10.1 → 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 +265 -0
- package/README.md +69 -6
- package/dist/ambient/active-container.d.ts +7 -2
- package/dist/ambient/active-container.js +6 -1
- package/dist/container/binding-builders.d.ts +19 -1
- package/dist/container/binding-builders.js +75 -16
- package/dist/container/container.js +224 -47
- package/dist/core/binding-declaration.d.ts +120 -0
- package/dist/core/binding-declaration.js +186 -0
- package/dist/core/binding.d.ts +57 -5
- package/dist/core/binding.js +48 -1
- package/dist/core/module.d.ts +7 -4
- package/dist/core/module.js +17 -3
- package/dist/core/registry.d.ts +11 -7
- package/dist/core/registry.js +122 -38
- 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/decorator-metadata.d.ts +9 -0
- package/dist/decorators/decorator-metadata.js +20 -0
- package/dist/decorators/inject.js +2 -1
- package/dist/decorators/injectable.js +3 -1
- package/dist/decorators/lifecycle-decorators.js +8 -2
- package/dist/errors/errors.d.ts +77 -3
- package/dist/errors/errors.js +93 -10
- package/dist/index.d.ts +3 -1
- package/dist/index.js +2 -1
- package/dist/injection/descriptor.js +3 -7
- package/dist/injection/resolve-options.js +6 -4
- package/dist/introspection/dependency-graph.d.ts +7 -2
- package/dist/introspection/dependency-graph.js +46 -23
- package/dist/introspection/graph-adapters/reactflow.js +6 -4
- package/dist/introspection/inspector.js +6 -9
- package/dist/lifecycle/lifecycle-manager.js +14 -2
- package/dist/lifecycle/scope-manager.js +28 -10
- package/dist/metadata/verifying-metadata-reader.d.ts +4 -3
- package/dist/metadata/verifying-metadata-reader.js +28 -6
- 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 +0 -1
- package/dist/resolution/cache/activation-need.js +11 -18
- package/dist/resolution/cache/binding-lookup-cache.d.ts +0 -7
- package/dist/resolution/cache/binding-lookup-cache.js +30 -17
- package/dist/resolution/cache/class-introspector.d.ts +11 -0
- package/dist/resolution/cache/class-introspector.js +18 -0
- package/dist/resolution/context.d.ts +15 -23
- package/dist/resolution/context.js +47 -56
- package/dist/resolution/path/resolution-path.d.ts +48 -13
- package/dist/resolution/path/resolution-path.js +89 -38
- package/dist/resolution/plan/instantiation-plan.js +61 -21
- package/dist/resolution/plan/plan-codegen.d.ts +7 -4
- package/dist/resolution/plan/plan-codegen.js +60 -32
- package/dist/resolution/resolver.d.ts +4 -5
- package/dist/resolution/resolver.js +288 -258
- package/package.json +14 -2
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { stringifyTagValue } from "#core/binding";
|
|
1
2
|
import { effectiveBindingScope } from "#core/binding-scope";
|
|
2
3
|
import { slotName } from "#core/tag";
|
|
3
4
|
import { tokenName } from "#core/token";
|
|
@@ -41,18 +42,31 @@ function edgeLabel(ref, index) {
|
|
|
41
42
|
const criterion = name !== undefined
|
|
42
43
|
? `name:${name}`
|
|
43
44
|
: firstPlainTag !== undefined
|
|
44
|
-
? `tag:${firstPlainTag.key.name}=${
|
|
45
|
+
? `tag:${firstPlainTag.key.name}=${stringifyTagValue(firstPlainTag.value)}`
|
|
45
46
|
: `[${index}]`;
|
|
46
47
|
return ref.optional ? `${criterion} optional` : criterion;
|
|
47
48
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
49
|
+
/**
|
|
50
|
+
* The bindings a dependency reaches, found the way resolution finds them.
|
|
51
|
+
*
|
|
52
|
+
* @remarks A single dependency stops at the nearest registry holding a binding its slot matches, and
|
|
53
|
+
* a collection gathers every registry's matches — `resolve` and `resolveAll` respectively.
|
|
54
|
+
*/
|
|
55
|
+
function dependencyTargets(chain, ref) {
|
|
56
|
+
if (ref.multi) {
|
|
57
|
+
const gathered = [];
|
|
58
|
+
for (const registry of chain) {
|
|
59
|
+
gathered.push(...matchingTargets(registry.getAll(ref.token), ref));
|
|
60
|
+
}
|
|
61
|
+
return gathered;
|
|
62
|
+
}
|
|
63
|
+
for (const registry of chain) {
|
|
64
|
+
const targets = matchingTargets(registry.getAll(ref.token), ref);
|
|
65
|
+
if (targets.length > 0) {
|
|
66
|
+
return targets;
|
|
53
67
|
}
|
|
54
|
-
|
|
55
|
-
|
|
68
|
+
}
|
|
69
|
+
return [];
|
|
56
70
|
}
|
|
57
71
|
/** The placeholder node an optional-but-unbound dependency points at, minted once per token. */
|
|
58
72
|
function unboundNodeIdFor(accumulator, dependency) {
|
|
@@ -73,8 +87,8 @@ function unboundNodeIdFor(accumulator, dependency) {
|
|
|
73
87
|
});
|
|
74
88
|
return id;
|
|
75
89
|
}
|
|
76
|
-
function addDependencyEdges(accumulator, from, ref, index,
|
|
77
|
-
const targets =
|
|
90
|
+
function addDependencyEdges(accumulator, from, ref, index, chain) {
|
|
91
|
+
const targets = dependencyTargets(chain, ref);
|
|
78
92
|
const label = edgeLabel(ref, index);
|
|
79
93
|
if (targets.length === 0) {
|
|
80
94
|
// A required-but-unbound dependency is validate()'s story, not the graph's.
|
|
@@ -108,25 +122,25 @@ function addDependencyEdges(accumulator, from, ref, index, lookup) {
|
|
|
108
122
|
}
|
|
109
123
|
}
|
|
110
124
|
/** What one binding declares up front — a class's params, a factory's descriptors, an alias's target. */
|
|
111
|
-
function addBindingEdges(accumulator, binding, metadataReader,
|
|
125
|
+
function addBindingEdges(accumulator, binding, metadataReader, chain) {
|
|
112
126
|
if (binding.kind === "class") {
|
|
113
127
|
const meta = metadataReader.getConstructorMetadata(binding.target);
|
|
114
128
|
if (meta !== undefined) {
|
|
115
129
|
for (const [index, param] of meta.params.entries()) {
|
|
116
|
-
addDependencyEdges(accumulator, String(binding.identifier), param, index,
|
|
130
|
+
addDependencyEdges(accumulator, String(binding.identifier), param, index, chain);
|
|
117
131
|
}
|
|
118
132
|
}
|
|
119
133
|
return;
|
|
120
134
|
}
|
|
121
135
|
if (binding.kind === "resolved" || binding.kind === "resolved-async") {
|
|
122
136
|
for (const [index, dependency] of binding.deps.entries()) {
|
|
123
|
-
addDependencyEdges(accumulator, String(binding.identifier), dependency, index,
|
|
137
|
+
addDependencyEdges(accumulator, String(binding.identifier), dependency, index, chain);
|
|
124
138
|
}
|
|
125
139
|
return;
|
|
126
140
|
}
|
|
127
141
|
if (binding.kind === "alias") {
|
|
128
142
|
const aliasRef = { token: binding.target, optional: false, multi: false };
|
|
129
|
-
for (const target of
|
|
143
|
+
for (const target of dependencyTargets(chain, aliasRef)) {
|
|
130
144
|
accumulator.edges.push({
|
|
131
145
|
from: String(binding.identifier),
|
|
132
146
|
to: String(target.identifier),
|
|
@@ -136,9 +150,9 @@ function addBindingEdges(accumulator, binding, metadataReader, lookup) {
|
|
|
136
150
|
}
|
|
137
151
|
}
|
|
138
152
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
for (const binding of
|
|
153
|
+
/** Adds the nodes of `chain[0]`, the registry the chain starts at, and the edges each of its bindings declares. */
|
|
154
|
+
function addRegistryBindings(accumulator, chain, metadataReader, fromParent) {
|
|
155
|
+
for (const binding of chain[0].allBindings()) {
|
|
142
156
|
accumulator.nodes.push({
|
|
143
157
|
id: String(binding.identifier),
|
|
144
158
|
tokenName: tokenName(binding.token),
|
|
@@ -147,20 +161,29 @@ function addRegistryBindings(accumulator, sourceRegistry, metadataReader, fromPa
|
|
|
147
161
|
scope: effectiveBindingScope(binding),
|
|
148
162
|
fromParent,
|
|
149
163
|
});
|
|
150
|
-
addBindingEdges(accumulator, binding, metadataReader,
|
|
164
|
+
addBindingEdges(accumulator, binding, metadataReader, chain);
|
|
151
165
|
}
|
|
152
166
|
}
|
|
153
167
|
/**
|
|
154
|
-
* Builds the JSON dependency graph of a registry's bindings, optionally including
|
|
168
|
+
* Builds the JSON dependency graph of a registry's bindings, optionally including its ancestors'.
|
|
169
|
+
*
|
|
170
|
+
* @param registry - The registry whose bindings the graph is for.
|
|
171
|
+
* @param metadataReader - The reader class dependencies are read through.
|
|
172
|
+
* @param options - Whether the ancestors' bindings join the graph.
|
|
173
|
+
* @param ancestorRegistries - The ancestor containers' registries, nearest first.
|
|
155
174
|
*
|
|
156
175
|
* @since 0.3.16-canary.0
|
|
157
176
|
*/
|
|
158
|
-
export function buildDependencyGraph(registry, metadataReader, options,
|
|
177
|
+
export function buildDependencyGraph(registry, metadataReader, options, ancestorRegistries = []) {
|
|
159
178
|
const accumulator = { nodes: [], edges: [], unboundNodeIds: new Map() };
|
|
160
179
|
const includesParent = options?.includeParent === true;
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
180
|
+
if (!includesParent) {
|
|
181
|
+
addRegistryBindings(accumulator, [registry], metadataReader, false);
|
|
182
|
+
return { nodes: accumulator.nodes, edges: accumulator.edges, includesParent };
|
|
183
|
+
}
|
|
184
|
+
const chain = [registry, ...ancestorRegistries];
|
|
185
|
+
for (let depth = 0; depth < chain.length; depth += 1) {
|
|
186
|
+
addRegistryBindings(accumulator, chain.slice(depth), metadataReader, depth > 0);
|
|
164
187
|
}
|
|
165
188
|
return { nodes: accumulator.nodes, edges: accumulator.edges, includesParent };
|
|
166
189
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Initial grid
|
|
2
|
+
* Initial grid cell, in the pixels React Flow positions in: a starting layout viewers re-lay out,
|
|
3
|
+
* fixed by no contract.
|
|
3
4
|
*
|
|
4
5
|
* @since 0.3.16-canary.0
|
|
5
6
|
*/
|
|
6
|
-
const GRID_COLUMN_COUNT = 5;
|
|
7
7
|
const GRID_CELL_WIDTH_PX = 200;
|
|
8
8
|
const GRID_CELL_HEIGHT_PX = 100;
|
|
9
9
|
/**
|
|
@@ -12,6 +12,8 @@ const GRID_CELL_HEIGHT_PX = 100;
|
|
|
12
12
|
* @since 0.5.0-canary.7
|
|
13
13
|
*/
|
|
14
14
|
export function toReactFlowGraph(graph) {
|
|
15
|
+
// A square grid over the graph: as many columns as rows, derived from the node count.
|
|
16
|
+
const columnCount = Math.max(1, Math.ceil(Math.sqrt(graph.nodes.length)));
|
|
15
17
|
const nodes = graph.nodes.map((node, idx) => ({
|
|
16
18
|
id: node.id,
|
|
17
19
|
data: {
|
|
@@ -22,8 +24,8 @@ export function toReactFlowGraph(graph) {
|
|
|
22
24
|
fromParent: node.fromParent,
|
|
23
25
|
},
|
|
24
26
|
position: {
|
|
25
|
-
x: (idx %
|
|
26
|
-
y: Math.floor(idx /
|
|
27
|
+
x: (idx % columnCount) * GRID_CELL_WIDTH_PX,
|
|
28
|
+
y: Math.floor(idx / columnCount) * GRID_CELL_HEIGHT_PX,
|
|
27
29
|
},
|
|
28
30
|
}));
|
|
29
31
|
const edges = graph.edges.map((edge, idx) => ({
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { effectiveBindingScope } from "#core/binding-scope";
|
|
2
2
|
import { tokenName } from "#core/token";
|
|
3
|
+
import { DefaultConstraintContext } from "#resolution/context";
|
|
3
4
|
import { selectAllBindings } from "#resolution/select/binding-select";
|
|
4
5
|
// ── Inspector ────────────────────────────────────────────────────────────────────────────────────────────────────────
|
|
5
6
|
/**
|
|
@@ -38,17 +39,13 @@ 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.getDefaultSlotBinding(token)?.kind === "alias"));
|
|
43
46
|
}
|
|
44
47
|
#makeConstraintContext(options) {
|
|
45
|
-
return
|
|
46
|
-
resolutionPath: [],
|
|
47
|
-
resolutionStack: [],
|
|
48
|
-
parent: undefined,
|
|
49
|
-
ancestors: [],
|
|
50
|
-
currentResolveOptions: options,
|
|
51
|
-
};
|
|
48
|
+
return new DefaultConstraintContext([], options);
|
|
52
49
|
}
|
|
53
50
|
#toSnapshot(binding) {
|
|
54
51
|
// Aliased, not copied: slot tags are frozen where they are built, so a caller's write throws
|
|
@@ -97,7 +97,11 @@ export class LifecycleManager {
|
|
|
97
97
|
let activatedInstance = instance;
|
|
98
98
|
// 1. @postConstruct() — must be sync (instance fully constructed per TC39 order)
|
|
99
99
|
for (const methodName of lifecycleMethods(binding, metadataReader, "postConstruct")) {
|
|
100
|
-
|
|
100
|
+
const hookResult = callHook(activatedInstance, methodName);
|
|
101
|
+
if (hookResult instanceof Promise) {
|
|
102
|
+
// The hook has already run; adopt its rejection so a failing async hook cannot become an
|
|
103
|
+
// unhandled rejection that ends the process, then report the sync-lane violation.
|
|
104
|
+
void hookResult.catch(() => { });
|
|
101
105
|
throw new AsyncActivationError(tokenName(binding.token), "postConstruct", methodName);
|
|
102
106
|
}
|
|
103
107
|
}
|
|
@@ -105,6 +109,7 @@ export class LifecycleManager {
|
|
|
105
109
|
if (binding.kind !== "alias" && binding.activationHook !== undefined) {
|
|
106
110
|
const activationResult = binding.activationHook(resolutionContext, activatedInstance);
|
|
107
111
|
if (activationResult instanceof Promise) {
|
|
112
|
+
void activationResult.catch(() => { });
|
|
108
113
|
throw new AsyncActivationError(tokenName(binding.token), "onActivation");
|
|
109
114
|
}
|
|
110
115
|
activatedInstance = activationResult;
|
|
@@ -116,6 +121,7 @@ export class LifecycleManager {
|
|
|
116
121
|
for (const hook of containerHooks) {
|
|
117
122
|
const activationResult = hook(resolutionContext, activatedInstance);
|
|
118
123
|
if (activationResult instanceof Promise) {
|
|
124
|
+
void activationResult.catch(() => { });
|
|
119
125
|
throw new AsyncActivationError(tokenDisplayName, "onActivation");
|
|
120
126
|
}
|
|
121
127
|
activatedInstance = activationResult;
|
|
@@ -159,6 +165,9 @@ export class LifecycleManager {
|
|
|
159
165
|
for (const hook of containerHooks) {
|
|
160
166
|
const hookResult = hook(instance);
|
|
161
167
|
if (hookResult instanceof Promise) {
|
|
168
|
+
// The hook has already run; adopt its rejection so a failing async hook cannot become an
|
|
169
|
+
// unhandled rejection that ends the process, then report the sync-lane violation.
|
|
170
|
+
void hookResult.catch(() => { });
|
|
162
171
|
throw new AsyncDeactivationError(tokenDisplayName);
|
|
163
172
|
}
|
|
164
173
|
}
|
|
@@ -167,12 +176,15 @@ export class LifecycleManager {
|
|
|
167
176
|
if (binding.kind !== "alias" && binding.deactivationHook !== undefined) {
|
|
168
177
|
const hookResult = binding.deactivationHook(instance);
|
|
169
178
|
if (hookResult instanceof Promise) {
|
|
179
|
+
void hookResult.catch(() => { });
|
|
170
180
|
throw new AsyncDeactivationError(tokenDisplayName);
|
|
171
181
|
}
|
|
172
182
|
}
|
|
173
183
|
// 3. @preDestroy()
|
|
174
184
|
for (const methodName of lifecycleMethods(binding, metadataReader, "preDestroy")) {
|
|
175
|
-
|
|
185
|
+
const hookResult = callHook(instance, methodName);
|
|
186
|
+
if (hookResult instanceof Promise) {
|
|
187
|
+
void hookResult.catch(() => { });
|
|
176
188
|
throw new AsyncDeactivationError(tokenDisplayName);
|
|
177
189
|
}
|
|
178
190
|
}
|
|
@@ -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,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
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
5
|
import { defaultMetadataReader } from "#metadata/symbol-metadata-reader";
|
|
6
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
|
+
}
|
|
@@ -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.
|
|
@@ -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
1
|
import { getOrInsertComputed } from "#core/map-upsert";
|
|
2
2
|
import { stateEpoch } from "#core/state-epoch";
|
|
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;
|
|
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);
|
|
@@ -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
|
*
|
|
@@ -173,6 +173,24 @@ export class ClassIntrospector {
|
|
|
173
173
|
(caches.constructorMetadata ??= new WeakMap()).set(target, metadata ?? null);
|
|
174
174
|
return metadata;
|
|
175
175
|
}
|
|
176
|
+
/**
|
|
177
|
+
* The nearest ancestor's own constructor metadata, for a subclass that declares none of its own.
|
|
178
|
+
*
|
|
179
|
+
* @remarks Constructor metadata is never borrowed down the chain, so a subclass with an implicit
|
|
180
|
+
* constructor would be built with zero arguments; this lets the resolver name the base whose
|
|
181
|
+
* declared deps the subclass silently drops.
|
|
182
|
+
*/
|
|
183
|
+
inheritedConstructorMetadata(target) {
|
|
184
|
+
let current = Object.getPrototypeOf(target);
|
|
185
|
+
while (typeof current === "function" && current !== Function.prototype) {
|
|
186
|
+
const metadata = this.constructorMetadata(current);
|
|
187
|
+
if (metadata !== undefined) {
|
|
188
|
+
return { base: current, metadata };
|
|
189
|
+
}
|
|
190
|
+
current = Object.getPrototypeOf(current);
|
|
191
|
+
}
|
|
192
|
+
return undefined;
|
|
193
|
+
}
|
|
176
194
|
/**
|
|
177
195
|
* Whether the class has a `@postConstruct` hook, or `undefined` until {@link discoverPostConstruct}.
|
|
178
196
|
*
|