@codefast/di 0.5.0-canary.6 → 0.5.0-canary.8
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 +123 -0
- package/README.md +15 -4
- package/dist/binding.d.ts +78 -19
- package/dist/binding.d.ts.map +1 -1
- package/dist/binding.js +46 -2
- package/dist/binding.js.map +1 -1
- package/dist/constructor-type.d.ts +4 -5
- package/dist/constructor-type.d.ts.map +1 -1
- package/dist/container/binding-builders.d.ts +56 -0
- package/dist/container/binding-builders.d.ts.map +1 -0
- package/dist/container/binding-builders.js +196 -0
- package/dist/container/binding-builders.js.map +1 -0
- package/dist/{container.d.ts → container/container.d.ts} +2 -2
- package/dist/container/container.d.ts.map +1 -0
- package/dist/container/container.js +624 -0
- package/dist/container/container.js.map +1 -0
- package/dist/decorators/inject.d.ts +10 -1
- package/dist/decorators/inject.d.ts.map +1 -1
- package/dist/decorators/inject.js +2 -2
- package/dist/decorators/inject.js.map +1 -1
- package/dist/decorators/injectable.js +3 -3
- package/dist/decorators/injectable.js.map +1 -1
- package/dist/errors.d.ts +14 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +17 -0
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +13 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -5
- package/dist/index.js.map +1 -1
- package/dist/introspection/dependency-graph.d.ts.map +1 -0
- package/dist/{dependency-graph.js → introspection/dependency-graph.js} +1 -1
- package/dist/introspection/dependency-graph.js.map +1 -0
- package/dist/{graph-adapters → introspection/graph-adapters}/cytoscape.d.ts +1 -1
- package/dist/introspection/graph-adapters/cytoscape.d.ts.map +1 -0
- package/dist/introspection/graph-adapters/cytoscape.js.map +1 -0
- package/dist/introspection/graph-adapters/dot.d.ts +6 -0
- package/dist/introspection/graph-adapters/dot.d.ts.map +1 -0
- package/dist/introspection/graph-adapters/dot.js.map +1 -0
- package/dist/{graph-adapters → introspection/graph-adapters}/reactflow.d.ts +2 -2
- package/dist/introspection/graph-adapters/reactflow.d.ts.map +1 -0
- package/dist/{graph-adapters → introspection/graph-adapters}/reactflow.js +11 -1
- package/dist/introspection/graph-adapters/reactflow.js.map +1 -0
- package/dist/{inspector.d.ts → introspection/inspector.d.ts} +3 -8
- package/dist/introspection/inspector.d.ts.map +1 -0
- package/dist/{inspector.js → introspection/inspector.js} +23 -23
- package/dist/introspection/inspector.js.map +1 -0
- package/dist/metadata/metadata-keys.d.ts +3 -6
- package/dist/metadata/metadata-keys.d.ts.map +1 -1
- package/dist/metadata/metadata-keys.js +3 -6
- package/dist/metadata/metadata-keys.js.map +1 -1
- package/dist/metadata/symbol-metadata-reader.d.ts +1 -1
- package/dist/metadata/symbol-metadata-reader.d.ts.map +1 -1
- package/dist/metadata/symbol-metadata-reader.js +4 -4
- package/dist/metadata/symbol-metadata-reader.js.map +1 -1
- package/dist/module.d.ts +9 -2
- package/dist/module.d.ts.map +1 -1
- package/dist/module.js +9 -2
- package/dist/module.js.map +1 -1
- package/dist/registry.d.ts +17 -13
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +94 -63
- package/dist/registry.js.map +1 -1
- package/dist/resolution/activation-need.d.ts +25 -0
- package/dist/resolution/activation-need.d.ts.map +1 -0
- package/dist/resolution/activation-need.js +64 -0
- package/dist/resolution/activation-need.js.map +1 -0
- package/dist/resolution/binding-lookup-cache.d.ts +41 -0
- package/dist/resolution/binding-lookup-cache.d.ts.map +1 -0
- package/dist/resolution/binding-lookup-cache.js +102 -0
- package/dist/resolution/binding-lookup-cache.js.map +1 -0
- package/dist/resolution/binding-scope.d.ts.map +1 -0
- package/dist/resolution/binding-scope.js.map +1 -0
- package/dist/resolution/binding-select.d.ts.map +1 -0
- package/dist/{binding-select.js → resolution/binding-select.js} +17 -7
- package/dist/resolution/binding-select.js.map +1 -0
- package/dist/resolution/class-introspector.d.ts +27 -0
- package/dist/resolution/class-introspector.d.ts.map +1 -0
- package/dist/resolution/class-introspector.js +60 -0
- package/dist/resolution/class-introspector.js.map +1 -0
- package/dist/resolution/constraints.d.ts.map +1 -0
- package/dist/resolution/constraints.js.map +1 -0
- package/dist/resolution/diagnostics.d.ts +41 -0
- package/dist/resolution/diagnostics.d.ts.map +1 -0
- package/dist/resolution/diagnostics.js +18 -0
- package/dist/resolution/diagnostics.js.map +1 -0
- package/dist/{environment.d.ts → resolution/environment.d.ts} +24 -7
- package/dist/resolution/environment.d.ts.map +1 -0
- package/dist/resolution/environment.js +126 -0
- package/dist/resolution/environment.js.map +1 -0
- package/dist/resolution/instantiation-plan.d.ts +67 -0
- package/dist/resolution/instantiation-plan.d.ts.map +1 -0
- package/dist/resolution/instantiation-plan.js +183 -0
- package/dist/resolution/instantiation-plan.js.map +1 -0
- package/dist/{lifecycle.d.ts → resolution/lifecycle.d.ts} +5 -3
- package/dist/resolution/lifecycle.d.ts.map +1 -0
- package/dist/{lifecycle.js → resolution/lifecycle.js} +23 -14
- package/dist/resolution/lifecycle.js.map +1 -0
- package/dist/resolution/resolution-path.d.ts +22 -0
- package/dist/resolution/resolution-path.d.ts.map +1 -0
- package/dist/resolution/resolution-path.js +40 -0
- package/dist/resolution/resolution-path.js.map +1 -0
- package/dist/resolution/resolve-options.d.ts.map +1 -0
- package/dist/resolution/resolve-options.js.map +1 -0
- package/dist/resolution/resolver.d.ts +40 -0
- package/dist/resolution/resolver.d.ts.map +1 -0
- package/dist/resolution/resolver.js +1108 -0
- package/dist/resolution/resolver.js.map +1 -0
- package/dist/{scope.d.ts → resolution/scope.d.ts} +8 -9
- package/dist/resolution/scope.d.ts.map +1 -0
- package/dist/resolution/scope.js +80 -0
- package/dist/resolution/scope.js.map +1 -0
- package/dist/token.d.ts.map +1 -1
- package/dist/token.js +0 -3
- package/dist/token.js.map +1 -1
- package/package.json +19 -94
- package/src/binding.ts +137 -22
- package/src/constructor-type.ts +4 -5
- package/src/container/binding-builders.ts +294 -0
- package/src/container/container.ts +825 -0
- package/src/decorators/inject.ts +15 -4
- package/src/decorators/injectable.ts +3 -3
- package/src/errors.ts +21 -0
- package/src/index.ts +15 -7
- package/src/{dependency-graph.ts → introspection/dependency-graph.ts} +1 -1
- package/src/{graph-adapters → introspection/graph-adapters}/cytoscape.ts +1 -1
- package/src/{graph-adapters → introspection/graph-adapters}/dot.ts +1 -1
- package/src/{graph-adapters → introspection/graph-adapters}/reactflow.ts +13 -2
- package/src/{inspector.ts → introspection/inspector.ts} +26 -21
- package/src/metadata/metadata-keys.ts +3 -6
- package/src/metadata/symbol-metadata-reader.ts +4 -4
- package/src/module.ts +14 -6
- package/src/registry.ts +97 -64
- package/src/resolution/activation-need.ts +81 -0
- package/src/resolution/binding-lookup-cache.ts +132 -0
- package/src/{binding-select.ts → resolution/binding-select.ts} +17 -7
- package/src/resolution/class-introspector.ts +74 -0
- package/src/resolution/diagnostics.ts +43 -0
- package/src/{environment.ts → resolution/environment.ts} +65 -35
- package/src/resolution/instantiation-plan.ts +292 -0
- package/src/{lifecycle.ts → resolution/lifecycle.ts} +26 -14
- package/src/resolution/resolution-path.ts +62 -0
- package/src/resolution/resolver.ts +1526 -0
- package/src/resolution/scope.ts +95 -0
- package/src/token.ts +0 -3
- package/dist/binding-scope.d.ts.map +0 -1
- package/dist/binding-scope.js.map +0 -1
- package/dist/binding-select.d.ts.map +0 -1
- package/dist/binding-select.js.map +0 -1
- package/dist/constraints.d.ts.map +0 -1
- package/dist/constraints.js.map +0 -1
- package/dist/container.d.ts.map +0 -1
- package/dist/container.js +0 -853
- package/dist/container.js.map +0 -1
- package/dist/dependency-graph.d.ts.map +0 -1
- package/dist/dependency-graph.js.map +0 -1
- package/dist/environment.d.ts.map +0 -1
- package/dist/environment.js +0 -100
- package/dist/environment.js.map +0 -1
- package/dist/graph-adapters/cytoscape.d.ts.map +0 -1
- package/dist/graph-adapters/cytoscape.js.map +0 -1
- package/dist/graph-adapters/dot.d.ts +0 -6
- package/dist/graph-adapters/dot.d.ts.map +0 -1
- package/dist/graph-adapters/dot.js.map +0 -1
- package/dist/graph-adapters/reactflow.d.ts.map +0 -1
- package/dist/graph-adapters/reactflow.js.map +0 -1
- package/dist/inspector.d.ts.map +0 -1
- package/dist/inspector.js.map +0 -1
- package/dist/lifecycle.d.ts.map +0 -1
- package/dist/lifecycle.js.map +0 -1
- package/dist/resolve-options.d.ts.map +0 -1
- package/dist/resolve-options.js.map +0 -1
- package/dist/resolver.d.ts +0 -90
- package/dist/resolver.d.ts.map +0 -1
- package/dist/resolver.js +0 -1208
- package/dist/resolver.js.map +0 -1
- package/dist/scope.d.ts.map +0 -1
- package/dist/scope.js +0 -61
- package/dist/scope.js.map +0 -1
- package/src/container.ts +0 -1141
- package/src/resolver.ts +0 -1626
- package/src/scope.ts +0 -77
- /package/dist/{dependency-graph.d.ts → introspection/dependency-graph.d.ts} +0 -0
- /package/dist/{graph-adapters → introspection/graph-adapters}/cytoscape.js +0 -0
- /package/dist/{graph-adapters → introspection/graph-adapters}/dot.js +0 -0
- /package/dist/{binding-scope.d.ts → resolution/binding-scope.d.ts} +0 -0
- /package/dist/{binding-scope.js → resolution/binding-scope.js} +0 -0
- /package/dist/{binding-select.d.ts → resolution/binding-select.d.ts} +0 -0
- /package/dist/{constraints.d.ts → resolution/constraints.d.ts} +0 -0
- /package/dist/{constraints.js → resolution/constraints.js} +0 -0
- /package/dist/{resolve-options.d.ts → resolution/resolve-options.d.ts} +0 -0
- /package/dist/{resolve-options.js → resolution/resolve-options.js} +0 -0
- /package/src/{binding-scope.ts → resolution/binding-scope.ts} +0 -0
- /package/src/{constraints.ts → resolution/constraints.ts} +0 -0
- /package/src/{resolve-options.ts → resolution/resolve-options.ts} +0 -0
package/src/registry.ts
CHANGED
|
@@ -7,79 +7,107 @@ import type { BindingIdentifier, Constructor, DependencyKey } from "#/types";
|
|
|
7
7
|
* @since 0.3.16-canary.0
|
|
8
8
|
*/
|
|
9
9
|
export class BindingRegistry {
|
|
10
|
+
// Monotonic mutation counter — lets resolvers version-stamp lookup caches across a container chain.
|
|
11
|
+
#version = 0;
|
|
10
12
|
// Map from token key -> array of bindings (order matters for last-wins)
|
|
11
|
-
|
|
13
|
+
readonly #bindings = new Map<DependencyKey, Array<Binding>>();
|
|
12
14
|
// Fast lookup by binding ID
|
|
13
|
-
|
|
14
|
-
// Fast lookup for slot { name, tags: [] }
|
|
15
|
-
|
|
15
|
+
readonly #byId = new Map<BindingIdentifier, Binding>();
|
|
16
|
+
// Fast lookup for slot { name, tags: [] } — unallocated until a named binding is registered.
|
|
17
|
+
#simpleNamed: Map<DependencyKey, Map<string, Binding>> | undefined;
|
|
16
18
|
// Fast path for one default slot binding with no predicate
|
|
17
|
-
|
|
18
|
-
// Fast lookup for slot { name: undefined, tags: [[key, value]] } with no predicate
|
|
19
|
-
|
|
19
|
+
readonly #fastDefault = new Map<DependencyKey, Binding>();
|
|
20
|
+
// Fast lookup for slot { name: undefined, tags: [[key, value]] } with no predicate — likewise
|
|
21
|
+
// unallocated until a tagged binding is registered.
|
|
22
|
+
#simpleTagged: Map<DependencyKey, Map<string, Map<unknown, Binding>>> | undefined;
|
|
20
23
|
|
|
21
|
-
/**
|
|
22
|
-
|
|
24
|
+
/** Monotonic version — increments on every mutation. */
|
|
25
|
+
get version(): number {
|
|
26
|
+
return this.#version;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Registers a mutation the indexes don't care about (a fluent chain refining scope or an
|
|
31
|
+
* activation hook in place), so version-stamped resolver caches still invalidate.
|
|
32
|
+
*/
|
|
33
|
+
touch(): void {
|
|
34
|
+
this.#version += 1;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Add or replace binding using slot-aware last-wins. Returns the displaced binding, if any.
|
|
39
|
+
*
|
|
40
|
+
* @remarks The binding is stored by reference — it must come from `createBinding`, which is
|
|
41
|
+
* what guarantees the single hidden class the resolver's hot reads depend on.
|
|
42
|
+
*/
|
|
43
|
+
add(binding: Binding): Binding | undefined {
|
|
44
|
+
this.#version += 1;
|
|
23
45
|
const key = binding.token as DependencyKey;
|
|
24
46
|
// ✓ TS6.0: Map.getOrInsert (ES2025) replaces the manual get+check+set upsert
|
|
25
|
-
const bindingsForToken = this.
|
|
47
|
+
const bindingsForToken = this.#bindings.getOrInsert(key, []);
|
|
26
48
|
|
|
27
49
|
// Only apply last-wins for slot-based bindings (not predicate-only)
|
|
28
|
-
|
|
50
|
+
let displacedBinding: Binding | undefined;
|
|
51
|
+
if (!this.#isPurePredicateBinding(binding)) {
|
|
29
52
|
const existingIndex = bindingsForToken.findIndex(
|
|
30
|
-
(candidate) => !this
|
|
53
|
+
(candidate) => !this.#isPurePredicateBinding(candidate) && bindingSlotEquals(candidate.slot, binding.slot),
|
|
31
54
|
);
|
|
32
55
|
if (existingIndex !== -1) {
|
|
33
|
-
|
|
34
|
-
this.
|
|
56
|
+
displacedBinding = bindingsForToken[existingIndex]!;
|
|
57
|
+
this.#byId.delete(displacedBinding.id);
|
|
35
58
|
bindingsForToken.splice(existingIndex, 1);
|
|
59
|
+
this.#deindexSimpleNamedBinding(key, displacedBinding);
|
|
60
|
+
this.#deindexSimpleTaggedBinding(key, displacedBinding);
|
|
36
61
|
}
|
|
37
62
|
}
|
|
38
63
|
|
|
39
64
|
bindingsForToken.push(binding);
|
|
40
|
-
this.
|
|
41
|
-
this
|
|
42
|
-
this
|
|
43
|
-
this
|
|
65
|
+
this.#byId.set(binding.id, binding);
|
|
66
|
+
this.#indexSimpleNamedBinding(key, binding);
|
|
67
|
+
this.#indexSimpleTaggedBinding(key, binding);
|
|
68
|
+
this.#refreshFastDefaultForToken(key);
|
|
69
|
+
return displacedBinding;
|
|
44
70
|
}
|
|
45
71
|
|
|
46
72
|
/** Remove all bindings for a token. Returns removed bindings. */
|
|
47
73
|
removeByToken(token: Token<unknown> | Constructor): Array<Binding> {
|
|
74
|
+
this.#version += 1;
|
|
48
75
|
const key = token as DependencyKey;
|
|
49
|
-
const bindingsForToken = this.
|
|
50
|
-
this.
|
|
51
|
-
this
|
|
52
|
-
this
|
|
53
|
-
this.
|
|
76
|
+
const bindingsForToken = this.#bindings.get(key) ?? [];
|
|
77
|
+
this.#bindings.delete(key);
|
|
78
|
+
this.#simpleNamed?.delete(key);
|
|
79
|
+
this.#simpleTagged?.delete(key);
|
|
80
|
+
this.#fastDefault.delete(key);
|
|
54
81
|
for (const binding of bindingsForToken) {
|
|
55
|
-
this.
|
|
82
|
+
this.#byId.delete(binding.id);
|
|
56
83
|
}
|
|
57
84
|
return bindingsForToken;
|
|
58
85
|
}
|
|
59
86
|
|
|
60
87
|
/** Remove a specific binding by ID. Returns the removed binding or undefined. */
|
|
61
88
|
removeById(id: BindingIdentifier): Binding | undefined {
|
|
62
|
-
const binding = this.
|
|
89
|
+
const binding = this.#byId.get(id);
|
|
63
90
|
if (binding === undefined) {
|
|
64
91
|
return undefined;
|
|
65
92
|
}
|
|
66
|
-
this
|
|
93
|
+
this.#version += 1;
|
|
94
|
+
this.#byId.delete(id);
|
|
67
95
|
const key = binding.token as DependencyKey;
|
|
68
|
-
const bindingsForToken = this.
|
|
96
|
+
const bindingsForToken = this.#bindings.get(key);
|
|
69
97
|
if (bindingsForToken !== undefined) {
|
|
70
98
|
const bindingIndex = bindingsForToken.findIndex((candidate) => candidate.id === id);
|
|
71
99
|
if (bindingIndex !== -1) {
|
|
72
100
|
bindingsForToken.splice(bindingIndex, 1);
|
|
73
101
|
}
|
|
74
|
-
this
|
|
75
|
-
this
|
|
102
|
+
this.#deindexSimpleNamedBinding(key, binding);
|
|
103
|
+
this.#deindexSimpleTaggedBinding(key, binding);
|
|
76
104
|
if (bindingsForToken.length === 0) {
|
|
77
|
-
this.
|
|
78
|
-
this
|
|
79
|
-
this
|
|
80
|
-
this.
|
|
105
|
+
this.#bindings.delete(key);
|
|
106
|
+
this.#simpleNamed?.delete(key);
|
|
107
|
+
this.#simpleTagged?.delete(key);
|
|
108
|
+
this.#fastDefault.delete(key);
|
|
81
109
|
} else {
|
|
82
|
-
this
|
|
110
|
+
this.#refreshFastDefaultForToken(key);
|
|
83
111
|
}
|
|
84
112
|
}
|
|
85
113
|
return binding;
|
|
@@ -87,25 +115,25 @@ export class BindingRegistry {
|
|
|
87
115
|
|
|
88
116
|
/** Get all bindings for a token. */
|
|
89
117
|
getAll(token: Token<unknown> | Constructor): ReadonlyArray<Binding> {
|
|
90
|
-
return this.
|
|
118
|
+
return this.#bindings.get(token as DependencyKey) ?? [];
|
|
91
119
|
}
|
|
92
120
|
|
|
93
121
|
/** Get binding by ID. */
|
|
94
122
|
getById(id: BindingIdentifier): Binding | undefined {
|
|
95
|
-
return this.
|
|
123
|
+
return this.#byId.get(id);
|
|
96
124
|
}
|
|
97
125
|
|
|
98
126
|
/** Check if any binding exists for token. */
|
|
99
127
|
has(token: Token<unknown> | Constructor): boolean {
|
|
100
128
|
const key = token as DependencyKey;
|
|
101
|
-
const list = this.
|
|
129
|
+
const list = this.#bindings.get(key);
|
|
102
130
|
return list !== undefined && list.length > 0;
|
|
103
131
|
}
|
|
104
132
|
|
|
105
133
|
/** All bindings in the registry. */
|
|
106
134
|
allBindings(): ReadonlyArray<Binding> {
|
|
107
135
|
const allBindings: Array<Binding> = [];
|
|
108
|
-
for (const bindingsForToken of this.
|
|
136
|
+
for (const bindingsForToken of this.#bindings.values()) {
|
|
109
137
|
allBindings.push(...bindingsForToken);
|
|
110
138
|
}
|
|
111
139
|
return allBindings;
|
|
@@ -113,54 +141,55 @@ export class BindingRegistry {
|
|
|
113
141
|
|
|
114
142
|
/** Remove all bindings. Returns all removed. */
|
|
115
143
|
clear(): ReadonlyArray<Binding> {
|
|
144
|
+
this.#version += 1;
|
|
116
145
|
const all = this.allBindings();
|
|
117
|
-
this.
|
|
118
|
-
this.
|
|
119
|
-
this
|
|
120
|
-
this
|
|
121
|
-
this.
|
|
146
|
+
this.#bindings.clear();
|
|
147
|
+
this.#byId.clear();
|
|
148
|
+
this.#simpleNamed?.clear();
|
|
149
|
+
this.#simpleTagged?.clear();
|
|
150
|
+
this.#fastDefault.clear();
|
|
122
151
|
return all;
|
|
123
152
|
}
|
|
124
153
|
|
|
125
154
|
getSimpleNamed(token: Token<unknown> | Constructor, name: string): Binding | undefined {
|
|
126
|
-
return this
|
|
155
|
+
return this.#simpleNamed?.get(token as DependencyKey)?.get(name);
|
|
127
156
|
}
|
|
128
157
|
|
|
129
158
|
getSimpleTagged(token: Token<unknown> | Constructor, tagKey: string, tagValue: unknown): Binding | undefined {
|
|
130
|
-
return this
|
|
131
|
-
|
|
159
|
+
return this.#simpleTagged
|
|
160
|
+
?.get(token as DependencyKey)
|
|
132
161
|
?.get(tagKey)
|
|
133
162
|
?.get(tagValue);
|
|
134
163
|
}
|
|
135
164
|
|
|
136
165
|
getFastDefault(token: Token<unknown> | Constructor): Binding | undefined {
|
|
137
|
-
return this.
|
|
166
|
+
return this.#fastDefault.get(token as DependencyKey);
|
|
138
167
|
}
|
|
139
168
|
|
|
140
169
|
/** Summarize available slot strings for a token (for error messages). */
|
|
141
170
|
availableSlotStrings(token: Token<unknown> | Constructor): Array<string> {
|
|
142
|
-
const bindingsForToken = this.
|
|
171
|
+
const bindingsForToken = this.#bindings.get(token as DependencyKey) ?? [];
|
|
143
172
|
return bindingsForToken.map((binding) => bindingSlotToString(binding.slot));
|
|
144
173
|
}
|
|
145
174
|
|
|
146
|
-
|
|
175
|
+
#indexSimpleTaggedBinding(tokenKey: DependencyKey, binding: Binding): void {
|
|
147
176
|
const slot = binding.slot;
|
|
148
177
|
if (slot.name !== undefined || slot.tags.length !== 1 || binding.predicate !== undefined) {
|
|
149
178
|
return;
|
|
150
179
|
}
|
|
151
180
|
const [tagKey, tagValue] = slot.tags[0]!;
|
|
152
|
-
const byTagKey = this.
|
|
181
|
+
const byTagKey = (this.#simpleTagged ??= new Map()).getOrInsert(tokenKey, new Map<string, Map<unknown, Binding>>());
|
|
153
182
|
const byTagValue = byTagKey.getOrInsert(tagKey, new Map<unknown, Binding>());
|
|
154
183
|
byTagValue.set(tagValue, binding);
|
|
155
184
|
}
|
|
156
185
|
|
|
157
|
-
|
|
186
|
+
#deindexSimpleTaggedBinding(tokenKey: DependencyKey, binding: Binding): void {
|
|
158
187
|
const slot = binding.slot;
|
|
159
188
|
if (slot.name !== undefined || slot.tags.length !== 1 || binding.predicate !== undefined) {
|
|
160
189
|
return;
|
|
161
190
|
}
|
|
162
191
|
const [tagKey, tagValue] = slot.tags[0]!;
|
|
163
|
-
const byTagKey = this
|
|
192
|
+
const byTagKey = this.#simpleTagged?.get(tokenKey);
|
|
164
193
|
if (byTagKey === undefined) {
|
|
165
194
|
return;
|
|
166
195
|
}
|
|
@@ -174,13 +203,13 @@ export class BindingRegistry {
|
|
|
174
203
|
if (byTagValue.size === 0) {
|
|
175
204
|
byTagKey.delete(tagKey);
|
|
176
205
|
if (byTagKey.size === 0) {
|
|
177
|
-
this
|
|
206
|
+
this.#simpleTagged!.delete(tokenKey);
|
|
178
207
|
}
|
|
179
208
|
}
|
|
180
209
|
}
|
|
181
210
|
}
|
|
182
211
|
|
|
183
|
-
|
|
212
|
+
#isPurePredicateBinding(binding: Binding): boolean {
|
|
184
213
|
const slot = binding.slot;
|
|
185
214
|
const hasPredicate = binding.predicate !== undefined;
|
|
186
215
|
const hasConstraint = slot.name !== undefined || slot.tags.length > 0;
|
|
@@ -188,21 +217,21 @@ export class BindingRegistry {
|
|
|
188
217
|
return hasPredicate && !hasConstraint;
|
|
189
218
|
}
|
|
190
219
|
|
|
191
|
-
|
|
220
|
+
#indexSimpleNamedBinding(tokenKey: DependencyKey, binding: Binding): void {
|
|
192
221
|
const slot = binding.slot;
|
|
193
222
|
if (slot.name === undefined || slot.tags.length > 0) {
|
|
194
223
|
return;
|
|
195
224
|
}
|
|
196
|
-
const bindingsByName = this.
|
|
225
|
+
const bindingsByName = (this.#simpleNamed ??= new Map()).getOrInsert(tokenKey, new Map<string, Binding>());
|
|
197
226
|
bindingsByName.set(slot.name, binding);
|
|
198
227
|
}
|
|
199
228
|
|
|
200
|
-
|
|
229
|
+
#deindexSimpleNamedBinding(tokenKey: DependencyKey, binding: Binding): void {
|
|
201
230
|
const slot = binding.slot;
|
|
202
231
|
if (slot.name === undefined || slot.tags.length > 0) {
|
|
203
232
|
return;
|
|
204
233
|
}
|
|
205
|
-
const bindingsByName = this
|
|
234
|
+
const bindingsByName = this.#simpleNamed?.get(tokenKey);
|
|
206
235
|
if (bindingsByName === undefined) {
|
|
207
236
|
return;
|
|
208
237
|
}
|
|
@@ -210,23 +239,27 @@ export class BindingRegistry {
|
|
|
210
239
|
if (currentBinding?.id === binding.id) {
|
|
211
240
|
bindingsByName.delete(slot.name);
|
|
212
241
|
if (bindingsByName.size === 0) {
|
|
213
|
-
this
|
|
242
|
+
this.#simpleNamed!.delete(tokenKey);
|
|
214
243
|
}
|
|
215
244
|
}
|
|
216
245
|
}
|
|
217
246
|
|
|
218
|
-
|
|
219
|
-
const bindingsForToken = this.
|
|
247
|
+
#refreshFastDefaultForToken(tokenKey: DependencyKey): void {
|
|
248
|
+
const bindingsForToken = this.#bindings.get(tokenKey);
|
|
220
249
|
if (bindingsForToken === undefined || bindingsForToken.length !== 1) {
|
|
221
|
-
this.
|
|
250
|
+
this.#fastDefault.delete(tokenKey);
|
|
222
251
|
return;
|
|
223
252
|
}
|
|
224
253
|
const onlyBinding = bindingsForToken[0]!;
|
|
225
254
|
const isDefaultSlot = onlyBinding.slot.name === undefined && onlyBinding.slot.tags.length === 0;
|
|
226
255
|
if (!isDefaultSlot || onlyBinding.predicate !== undefined) {
|
|
227
|
-
this.
|
|
256
|
+
this.#fastDefault.delete(tokenKey);
|
|
228
257
|
return;
|
|
229
258
|
}
|
|
230
|
-
this.
|
|
259
|
+
this.#fastDefault.set(tokenKey, onlyBinding);
|
|
260
|
+
}
|
|
261
|
+
/** Whether the deferred table behind `#simpleNamed` has had to be built. */
|
|
262
|
+
get isBuilt(): boolean {
|
|
263
|
+
return this.#simpleNamed !== undefined;
|
|
231
264
|
}
|
|
232
265
|
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per binding: does resolving it have to go through the activation pipeline?
|
|
3
|
+
*
|
|
4
|
+
* @remarks Versioned on the lifecycle manager, since `onActivation` can be registered at any time.
|
|
5
|
+
*/
|
|
6
|
+
import type { Binding } from "#/binding";
|
|
7
|
+
import type { ClassIntrospector } from "#/resolution/class-introspector";
|
|
8
|
+
import type { LifecycleManager } from "#/resolution/lifecycle";
|
|
9
|
+
import type { BindingIdentifier } from "#/types";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @since 0.5.0-canary.8
|
|
13
|
+
*/
|
|
14
|
+
export class ActivationNeedCache {
|
|
15
|
+
readonly #needByBindingId = new Map<BindingIdentifier, boolean>();
|
|
16
|
+
#version = -1;
|
|
17
|
+
readonly #lifecycle: LifecycleManager;
|
|
18
|
+
readonly #classes: ClassIntrospector;
|
|
19
|
+
|
|
20
|
+
constructor(lifecycle: LifecycleManager, classes: ClassIntrospector) {
|
|
21
|
+
this.#lifecycle = lifecycle;
|
|
22
|
+
this.#classes = classes;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
needsActivation<const Value>(binding: Binding<Value>): boolean {
|
|
26
|
+
const lifecycleVersion = this.#lifecycle.activationVersion;
|
|
27
|
+
// No hooks registered anywhere and none on the binding: only classes can still surprise us,
|
|
28
|
+
// via a @postConstruct we have not looked for yet.
|
|
29
|
+
if (
|
|
30
|
+
lifecycleVersion === 0 &&
|
|
31
|
+
binding.kind !== "class" &&
|
|
32
|
+
binding.kind !== "alias" &&
|
|
33
|
+
binding.onActivation === undefined
|
|
34
|
+
) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
if (this.#version !== lifecycleVersion) {
|
|
38
|
+
this.#needByBindingId.clear();
|
|
39
|
+
this.#version = lifecycleVersion;
|
|
40
|
+
}
|
|
41
|
+
const cached = this.#needByBindingId.get(binding.id);
|
|
42
|
+
if (cached !== undefined) {
|
|
43
|
+
return cached;
|
|
44
|
+
}
|
|
45
|
+
const needsActivation =
|
|
46
|
+
binding.kind === "class" ? this.#classNeedsActivation(binding) : this.#nonClassNeedsActivation(binding);
|
|
47
|
+
this.#needByBindingId.set(binding.id, needsActivation);
|
|
48
|
+
return needsActivation;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Settles a class binding's answer once its lifecycle metadata has actually been read, which
|
|
53
|
+
* only happens on the first instantiation.
|
|
54
|
+
*
|
|
55
|
+
* @returns the answer to use for this resolve — possibly now `false` where it was a
|
|
56
|
+
* conservative `true`.
|
|
57
|
+
*/
|
|
58
|
+
refreshAfterFirstInstantiation<Value>(binding: Binding<Value>, needsActivation: boolean): boolean {
|
|
59
|
+
if (binding.kind !== "class" || this.#classes.knownPostConstruct(binding.target) !== undefined) {
|
|
60
|
+
return needsActivation;
|
|
61
|
+
}
|
|
62
|
+
this.#classes.discoverPostConstruct(binding.target);
|
|
63
|
+
this.#needByBindingId.delete(binding.id);
|
|
64
|
+
return this.needsActivation(binding);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
#classNeedsActivation<const Value>(binding: Binding<Value> & { kind: "class" }): boolean {
|
|
68
|
+
if (this.#lifecycle.hasActivationHandlers(binding.token) || binding.onActivation !== undefined) {
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
// Unknown lifecycle metadata: activate once so the first instantiation can settle it.
|
|
72
|
+
return this.#classes.knownPostConstruct(binding.target) !== false;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
#nonClassNeedsActivation<const Value>(binding: Binding<Value>): boolean {
|
|
76
|
+
if (binding.kind !== "alias" && binding.onActivation !== undefined) {
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
return this.#lifecycle.hasActivationHandlers(binding.token);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Options-less token → terminal binding, memoized per container chain.
|
|
3
|
+
*
|
|
4
|
+
* @see `ARCHITECTURE.md` — why these caches form their own parent chain.
|
|
5
|
+
*/
|
|
6
|
+
import type { Binding } from "#/binding";
|
|
7
|
+
import type { BindingRegistry } from "#/registry";
|
|
8
|
+
import type { Token } from "#/token";
|
|
9
|
+
import type { Constructor } from "#/types";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* A token's terminal binding with alias hops already folded, plus the container that owns it.
|
|
13
|
+
*
|
|
14
|
+
* @typeParam Owner - the resolver type, kept generic so this module stays free of resolver internals
|
|
15
|
+
*
|
|
16
|
+
* @since 0.5.0-canary.8
|
|
17
|
+
*/
|
|
18
|
+
export interface DefaultLookupEntry<Owner> {
|
|
19
|
+
readonly binding: Binding;
|
|
20
|
+
readonly owner: Owner;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Alias folding gives up past this many hops and defers to the full resolve loop, whose
|
|
25
|
+
* Set-based traversal detects genuine cycles exactly rather than by an arbitrary cap.
|
|
26
|
+
*
|
|
27
|
+
* @since 0.5.0-canary.8
|
|
28
|
+
*/
|
|
29
|
+
export const ALIAS_HOP_LIMIT = 32;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @since 0.5.0-canary.8
|
|
33
|
+
*/
|
|
34
|
+
export class BindingLookupCache<Owner> {
|
|
35
|
+
readonly #byToken = new Map<Token<unknown> | Constructor, DefaultLookupEntry<Owner> | null>();
|
|
36
|
+
#version = -1;
|
|
37
|
+
readonly #byTokenAndName = new Map<Token<unknown> | Constructor, Map<string, DefaultLookupEntry<Owner> | null>>();
|
|
38
|
+
#namedVersion = -1;
|
|
39
|
+
|
|
40
|
+
readonly #registry: BindingRegistry;
|
|
41
|
+
readonly #owner: Owner;
|
|
42
|
+
readonly #parent: BindingLookupCache<Owner> | undefined;
|
|
43
|
+
|
|
44
|
+
constructor(registry: BindingRegistry, owner: Owner, parent: BindingLookupCache<Owner> | undefined) {
|
|
45
|
+
this.#registry = registry;
|
|
46
|
+
this.#owner = owner;
|
|
47
|
+
this.#parent = parent;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Summed registry versions of this cache's whole chain — the memo stamp. */
|
|
51
|
+
chainVersion(): number {
|
|
52
|
+
let version = this.#registry.version;
|
|
53
|
+
for (let cache = this.#parent; cache !== undefined; cache = cache.#parent) {
|
|
54
|
+
version += cache.#registry.version;
|
|
55
|
+
}
|
|
56
|
+
return version;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** `null` when the token's shape needs the full selection path. */
|
|
60
|
+
defaultEntry(token: Token<unknown> | Constructor): DefaultLookupEntry<Owner> | null {
|
|
61
|
+
const version = this.chainVersion();
|
|
62
|
+
if (version !== this.#version) {
|
|
63
|
+
this.#byToken.clear();
|
|
64
|
+
this.#version = version;
|
|
65
|
+
}
|
|
66
|
+
let entry = this.#byToken.get(token);
|
|
67
|
+
if (entry === undefined) {
|
|
68
|
+
entry = this.#foldAliases(token);
|
|
69
|
+
this.#byToken.set(token, entry);
|
|
70
|
+
}
|
|
71
|
+
return entry;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** `null` when the name's shape needs the full selection path. */
|
|
75
|
+
namedEntry(token: Token<unknown> | Constructor, name: string): DefaultLookupEntry<Owner> | null {
|
|
76
|
+
const version = this.chainVersion();
|
|
77
|
+
if (version !== this.#namedVersion) {
|
|
78
|
+
this.#byTokenAndName.clear();
|
|
79
|
+
this.#namedVersion = version;
|
|
80
|
+
}
|
|
81
|
+
// ✓ TS6.0: Map.getOrInsert (ES2025)
|
|
82
|
+
const byName = this.#byTokenAndName.getOrInsert(token, new Map<string, DefaultLookupEntry<Owner> | null>());
|
|
83
|
+
let entry = byName.get(name);
|
|
84
|
+
if (entry === undefined) {
|
|
85
|
+
entry = this.#findNamedInChain(token, name);
|
|
86
|
+
byName.set(name, entry);
|
|
87
|
+
}
|
|
88
|
+
return entry;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
#foldAliases(token: Token<unknown> | Constructor): DefaultLookupEntry<Owner> | null {
|
|
92
|
+
let current = token;
|
|
93
|
+
for (let hop = 0; hop < ALIAS_HOP_LIMIT; hop += 1) {
|
|
94
|
+
const entry = this.#findDefaultInChain(current);
|
|
95
|
+
if (entry === null) {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
if (entry.binding.kind !== "alias") {
|
|
99
|
+
return entry;
|
|
100
|
+
}
|
|
101
|
+
current = entry.binding.target;
|
|
102
|
+
}
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
#findDefaultInChain(token: Token<unknown> | Constructor): DefaultLookupEntry<Owner> | null {
|
|
107
|
+
const fast = this.#registry.getFastDefault(token);
|
|
108
|
+
if (fast !== undefined) {
|
|
109
|
+
return { binding: fast, owner: this.#owner };
|
|
110
|
+
}
|
|
111
|
+
// A level with non-fast bindings (multi-slot / predicate) needs full selection — bail.
|
|
112
|
+
if (this.#registry.has(token)) {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
return this.#parent === undefined ? null : this.#parent.#findDefaultInChain(token);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
#findNamedInChain(token: Token<unknown> | Constructor, name: string): DefaultLookupEntry<Owner> | null {
|
|
119
|
+
const named = this.#registry.getSimpleNamed(token, name);
|
|
120
|
+
if (named !== undefined) {
|
|
121
|
+
// Predicates need a live context; aliases carry options through the full path.
|
|
122
|
+
if (named.predicate !== undefined || named.kind === "alias") {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
return { binding: named, owner: this.#owner };
|
|
126
|
+
}
|
|
127
|
+
if (this.#registry.has(token)) {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
return this.#parent === undefined ? null : this.#parent.#findNamedInChain(token, name);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
@@ -21,9 +21,22 @@ export function selectBinding(
|
|
|
21
21
|
if (candidates.length === 1) {
|
|
22
22
|
return candidates[0];
|
|
23
23
|
}
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
24
|
+
// Most specific wins: a single matching predicate-carrying candidate beats
|
|
25
|
+
// predicate-less ones (a predicate is a deliberate specialization of the
|
|
26
|
+
// default). Two matching predicates are genuinely ambiguous.
|
|
27
|
+
let predicatedCandidate: Binding | undefined;
|
|
28
|
+
for (const candidate of candidates) {
|
|
29
|
+
if (candidate.predicate !== undefined) {
|
|
30
|
+
if (predicatedCandidate !== undefined) {
|
|
31
|
+
predicatedCandidate = undefined;
|
|
32
|
+
break;
|
|
33
|
+
}
|
|
34
|
+
predicatedCandidate = candidate;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (predicatedCandidate !== undefined) {
|
|
38
|
+
return predicatedCandidate;
|
|
39
|
+
}
|
|
27
40
|
throw new AmbiguousBindingError(
|
|
28
41
|
tokenDisplayName,
|
|
29
42
|
candidates.map((c) => c.id),
|
|
@@ -122,10 +135,7 @@ function matchesSlot(binding: Binding, options: ResolveOptions | undefined): boo
|
|
|
122
135
|
}
|
|
123
136
|
}
|
|
124
137
|
} else if (hasRequestedTags) {
|
|
125
|
-
//
|
|
126
|
-
// But: default slot (no tags, no name) can match when options has tags if there are no tag-slotted bindings
|
|
127
|
-
// Actually per spec: resolveAll with tags only returns bindings that have those tags
|
|
128
|
-
// and resolve with tags requires exact match
|
|
138
|
+
// Requested tags require a tagged slot: an untagged binding never matches (SPEC §6.9).
|
|
129
139
|
return false;
|
|
130
140
|
}
|
|
131
141
|
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-class decorator metadata, cached by constructor.
|
|
3
|
+
*
|
|
4
|
+
* @remarks Metadata cannot change once a class is defined, so nothing here needs version stamping.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { ConstructorInvocation } from "#/constructor-type";
|
|
8
|
+
import type { Container } from "#/container/container";
|
|
9
|
+
import type { ConstructorMetadata, MetadataReader } from "#/metadata/metadata-types";
|
|
10
|
+
import { runWithContainer } from "#/resolution/environment";
|
|
11
|
+
import type { Constructor } from "#/types";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @since 0.5.0-canary.8
|
|
15
|
+
*/
|
|
16
|
+
export class ClassIntrospector {
|
|
17
|
+
// Unallocated until the container resolves its first class binding — a container bound entirely
|
|
18
|
+
// to constants, factories or aliases never introspects one.
|
|
19
|
+
#constructorMetadata: WeakMap<Constructor, ConstructorMetadata | null> | undefined;
|
|
20
|
+
#hasPostConstruct: WeakMap<Constructor, boolean> | undefined;
|
|
21
|
+
#needsActiveContainer: WeakMap<Constructor, boolean> | undefined;
|
|
22
|
+
readonly #reader: MetadataReader;
|
|
23
|
+
readonly #container: Container;
|
|
24
|
+
|
|
25
|
+
constructor(reader: MetadataReader, container: Container) {
|
|
26
|
+
this.#reader = reader;
|
|
27
|
+
this.#container = container;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
constructorMetadata(target: Constructor): ConstructorMetadata | undefined {
|
|
31
|
+
const cached = this.#constructorMetadata?.get(target);
|
|
32
|
+
if (cached !== undefined) {
|
|
33
|
+
return cached === null ? undefined : cached;
|
|
34
|
+
}
|
|
35
|
+
const metadata = this.#reader.getConstructorMetadata(target);
|
|
36
|
+
(this.#constructorMetadata ??= new WeakMap()).set(target, metadata ?? null);
|
|
37
|
+
return metadata;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Whether the class has a `@postConstruct` hook, or `undefined` until {@link discoverPostConstruct}.
|
|
42
|
+
*
|
|
43
|
+
* @remarks Callers treat unknown as "assume it does", so the first activation settles it.
|
|
44
|
+
*/
|
|
45
|
+
knownPostConstruct(target: Constructor): boolean | undefined {
|
|
46
|
+
return this.#hasPostConstruct?.get(target);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
discoverPostConstruct(target: Constructor): void {
|
|
50
|
+
const lifecycle = this.#reader.getLifecycleMetadata(target);
|
|
51
|
+
(this.#hasPostConstruct ??= new WeakMap()).set(
|
|
52
|
+
target,
|
|
53
|
+
lifecycle !== undefined && lifecycle.postConstruct !== undefined && lifecycle.postConstruct.length > 0,
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** True when the class has accessor injection, which reads the container during construction. */
|
|
58
|
+
needsActiveContainer(target: Constructor): boolean {
|
|
59
|
+
let needsActiveContainer = this.#needsActiveContainer?.get(target);
|
|
60
|
+
if (needsActiveContainer === undefined) {
|
|
61
|
+
needsActiveContainer = (this.#reader.getAccessorMetadata?.(target)?.length ?? 0) > 0;
|
|
62
|
+
(this.#needsActiveContainer ??= new WeakMap()).set(target, needsActiveContainer);
|
|
63
|
+
}
|
|
64
|
+
return needsActiveContainer;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
instantiate(target: Constructor, deps: Array<unknown>): unknown {
|
|
68
|
+
const invokable = target as ConstructorInvocation;
|
|
69
|
+
if (!this.needsActiveContainer(target)) {
|
|
70
|
+
return new invokable(...deps);
|
|
71
|
+
}
|
|
72
|
+
return runWithContainer(this.#container, () => new invokable(...deps));
|
|
73
|
+
}
|
|
74
|
+
}
|