@codefast/di 0.5.0-canary.7 → 0.5.0-canary.9
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 +205 -0
- package/README.md +6 -2
- package/dist/binding.d.ts +85 -24
- package/dist/binding.d.ts.map +1 -1
- package/dist/binding.js +55 -0
- 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 +32 -11
- package/dist/container/binding-builders.d.ts.map +1 -1
- package/dist/container/binding-builders.js +144 -192
- package/dist/container/binding-builders.js.map +1 -1
- package/dist/container/container.d.ts.map +1 -1
- package/dist/container/container.js +141 -201
- package/dist/container/container.js.map +1 -1
- package/dist/decorators/inject.d.ts +2 -4
- package/dist/decorators/inject.d.ts.map +1 -1
- package/dist/decorators/inject.js.map +1 -1
- package/dist/errors.d.ts +24 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +30 -0
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +1 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/introspection/inspector.d.ts +0 -1
- package/dist/introspection/inspector.d.ts.map +1 -1
- package/dist/introspection/inspector.js +3 -8
- package/dist/introspection/inspector.js.map +1 -1
- 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/registry.d.ts +14 -2
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +81 -78
- package/dist/registry.js.map +1 -1
- package/dist/resolution/activation-need.d.ts +27 -0
- package/dist/resolution/activation-need.d.ts.map +1 -0
- package/dist/resolution/activation-need.js +68 -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 +118 -0
- package/dist/resolution/binding-lookup-cache.js.map +1 -0
- package/dist/resolution/binding-scope.d.ts +5 -2
- package/dist/resolution/binding-scope.d.ts.map +1 -1
- package/dist/resolution/binding-scope.js +6 -17
- package/dist/resolution/binding-scope.js.map +1 -1
- package/dist/resolution/binding-select.d.ts +8 -1
- package/dist/resolution/binding-select.d.ts.map +1 -1
- package/dist/resolution/binding-select.js +14 -36
- package/dist/resolution/binding-select.js.map +1 -1
- 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/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/resolution/environment.d.ts +48 -1
- package/dist/resolution/environment.d.ts.map +1 -1
- package/dist/resolution/environment.js +134 -5
- package/dist/resolution/environment.js.map +1 -1
- package/dist/resolution/instantiation-plan.d.ts +15 -15
- package/dist/resolution/instantiation-plan.d.ts.map +1 -1
- package/dist/resolution/instantiation-plan.js +69 -49
- package/dist/resolution/instantiation-plan.js.map +1 -1
- package/dist/resolution/lifecycle.d.ts +2 -0
- package/dist/resolution/lifecycle.d.ts.map +1 -1
- package/dist/resolution/lifecycle.js +60 -62
- package/dist/resolution/lifecycle.js.map +1 -1
- package/dist/resolution/resolution-path.d.ts +84 -17
- package/dist/resolution/resolution-path.d.ts.map +1 -1
- package/dist/resolution/resolution-path.js +68 -23
- package/dist/resolution/resolution-path.js.map +1 -1
- package/dist/resolution/resolve-options.d.ts +41 -4
- package/dist/resolution/resolve-options.d.ts.map +1 -1
- package/dist/resolution/resolve-options.js +25 -1
- package/dist/resolution/resolve-options.js.map +1 -1
- package/dist/resolution/resolver.d.ts +33 -10
- package/dist/resolution/resolver.d.ts.map +1 -1
- package/dist/resolution/resolver.js +471 -830
- package/dist/resolution/resolver.js.map +1 -1
- package/dist/resolution/scope.d.ts +11 -15
- package/dist/resolution/scope.d.ts.map +1 -1
- package/dist/resolution/scope.js +55 -43
- package/dist/resolution/scope.js.map +1 -1
- package/package.json +10 -106
- package/src/binding.ts +146 -24
- package/src/constructor-type.ts +4 -5
- package/src/container/binding-builders.ts +184 -284
- package/src/container/container.ts +161 -221
- package/src/decorators/inject.ts +3 -5
- package/src/errors.ts +38 -0
- package/src/index.ts +4 -1
- package/src/introspection/inspector.ts +3 -9
- package/src/metadata/metadata-keys.ts +3 -6
- package/src/registry.ts +90 -94
- package/src/resolution/activation-need.ts +85 -0
- package/src/resolution/binding-lookup-cache.ts +148 -0
- package/src/resolution/binding-scope.ts +6 -17
- package/src/resolution/binding-select.ts +15 -39
- package/src/resolution/class-introspector.ts +74 -0
- package/src/resolution/diagnostics.ts +43 -0
- package/src/resolution/environment.ts +181 -5
- package/src/resolution/instantiation-plan.ts +116 -64
- package/src/resolution/lifecycle.ts +69 -62
- package/src/resolution/resolution-path.ts +122 -43
- package/src/resolution/resolve-options.ts +51 -4
- package/src/resolution/resolver.ts +649 -1081
- package/src/resolution/scope.ts +58 -47
package/src/errors.ts
CHANGED
|
@@ -212,6 +212,27 @@ export class MissingContainerContextError extends DiError {
|
|
|
212
212
|
}
|
|
213
213
|
}
|
|
214
214
|
|
|
215
|
+
/**
|
|
216
|
+
* A fluent chain was refined before a `to*()` call gave it a binding to refine.
|
|
217
|
+
*
|
|
218
|
+
* @remarks The builder types make this unreachable from TypeScript — `bind()` returns
|
|
219
|
+
* `BindToBuilder`, which exposes only `to*()`. It exists for JavaScript callers and for anyone who
|
|
220
|
+
* casts past the types, so the misuse fails loudly instead of mutating nothing.
|
|
221
|
+
*
|
|
222
|
+
* @since 0.5.0-canary.8
|
|
223
|
+
*/
|
|
224
|
+
export class ChainNotRegisteredError extends DiError {
|
|
225
|
+
readonly code = "CHAIN_NOT_REGISTERED";
|
|
226
|
+
readonly tokenName: string;
|
|
227
|
+
|
|
228
|
+
constructor(tokenName: string) {
|
|
229
|
+
super(
|
|
230
|
+
`Cannot refine the binding for token '${tokenName}' before choosing a target. Call a to*() method first — for example .to(SomeClass), .toConstantValue(value) or .toDynamic(factory).`,
|
|
231
|
+
);
|
|
232
|
+
this.tokenName = tokenName;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
215
236
|
/**
|
|
216
237
|
* @since 0.3.16-canary.0
|
|
217
238
|
*/
|
|
@@ -227,6 +248,23 @@ export class RebindUnboundTokenError extends DiError {
|
|
|
227
248
|
}
|
|
228
249
|
}
|
|
229
250
|
|
|
251
|
+
/**
|
|
252
|
+
* `toSelf()` on a token that is not a class, so there is nothing to construct.
|
|
253
|
+
*
|
|
254
|
+
* @since 0.5.0-canary.9
|
|
255
|
+
*/
|
|
256
|
+
export class SelfBindingRequiresClassError extends DiError {
|
|
257
|
+
readonly code = "SELF_BINDING_REQUIRES_CLASS";
|
|
258
|
+
readonly tokenName: string;
|
|
259
|
+
|
|
260
|
+
constructor(tokenName: string) {
|
|
261
|
+
super(
|
|
262
|
+
`toSelf() needs the token to be the class it constructs, and '${tokenName}' is not a class. Use .to(SomeClass) to name the implementation, or bind the class itself with container.bind(SomeClass).toSelf().`,
|
|
263
|
+
);
|
|
264
|
+
this.tokenName = tokenName;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
230
268
|
/**
|
|
231
269
|
* @since 0.3.16-canary.0
|
|
232
270
|
*/
|
package/src/index.ts
CHANGED
|
@@ -36,7 +36,8 @@ export type {
|
|
|
36
36
|
export { Container } from "#/container/container";
|
|
37
37
|
export type { Container as ContainerInterface, ContainerStatic } from "#/container/container";
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
// `effectiveBindingScope` is deliberately absent: it reads a `Binding`, which is internal, and no
|
|
40
|
+
// public API hands one out. `BindingSnapshot.scope` and `GraphNode.scope` are the public answers.
|
|
40
41
|
export { injectionSlotToResolveOptions, bindingSlotToResolveOptions } from "#/resolution/resolve-options";
|
|
41
42
|
|
|
42
43
|
// Introspection types
|
|
@@ -85,6 +86,7 @@ export {
|
|
|
85
86
|
AsyncDeactivationError,
|
|
86
87
|
AsyncModuleLoadError,
|
|
87
88
|
AsyncResolutionError,
|
|
89
|
+
ChainNotRegisteredError,
|
|
88
90
|
CircularDependencyError,
|
|
89
91
|
DiError,
|
|
90
92
|
DisposedContainerError,
|
|
@@ -95,6 +97,7 @@ export {
|
|
|
95
97
|
NoMatchingBindingError,
|
|
96
98
|
RebindUnboundTokenError,
|
|
97
99
|
ScopeViolationError,
|
|
100
|
+
SelfBindingRequiresClassError,
|
|
98
101
|
SyncDisposalNotSupportedError,
|
|
99
102
|
TokenNotBoundError,
|
|
100
103
|
} from "#/errors";
|
|
@@ -60,10 +60,9 @@ export class Inspector {
|
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
inspect(): ContainerSnapshot {
|
|
63
|
-
const snapshots = this.allBindingSnapshots();
|
|
64
63
|
return {
|
|
65
|
-
ownBindings:
|
|
66
|
-
cachedSingletonCount: this.#scope.
|
|
64
|
+
ownBindings: this.#registry.allBindings().map((binding) => this.#toSnapshot(binding)),
|
|
65
|
+
cachedSingletonCount: this.#scope.cachedSingletons().length,
|
|
67
66
|
hasParent: this.#hasParent,
|
|
68
67
|
isDisposed: this.#isDisposed(),
|
|
69
68
|
};
|
|
@@ -109,12 +108,7 @@ export class Inspector {
|
|
|
109
108
|
};
|
|
110
109
|
}
|
|
111
110
|
|
|
112
|
-
private allBindingSnapshots(): ReadonlyArray<BindingSnapshot> {
|
|
113
|
-
return this.#registry.allBindings().map((binding) => this.#toSnapshot(binding));
|
|
114
|
-
}
|
|
115
|
-
|
|
116
111
|
#toSnapshot(binding: Binding): BindingSnapshot {
|
|
117
|
-
const scope = effectiveBindingScope(binding);
|
|
118
112
|
const slot: BindingSnapshot["slot"] =
|
|
119
113
|
binding.slot.name !== undefined
|
|
120
114
|
? { name: binding.slot.name, tags: binding.slot.tags }
|
|
@@ -122,7 +116,7 @@ export class Inspector {
|
|
|
122
116
|
return {
|
|
123
117
|
tokenName: tokenName(binding.token),
|
|
124
118
|
kind: binding.kind,
|
|
125
|
-
scope,
|
|
119
|
+
scope: effectiveBindingScope(binding),
|
|
126
120
|
slot,
|
|
127
121
|
id: binding.id,
|
|
128
122
|
};
|
|
@@ -12,13 +12,10 @@ export const LIFECYCLE_KEY: unique symbol = Symbol("di:lifecycle");
|
|
|
12
12
|
export const INJECT_ACCESSOR_KEY: unique symbol = Symbol("di:inject-accessor");
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
|
-
* The
|
|
15
|
+
* The symbol TC39 Stage 3 decorator transforms store class metadata under.
|
|
16
16
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* `Symbol.for("Symbol.metadata")` — a global-registry symbol with the same string key.
|
|
20
|
-
* Resolving it here once keeps the reader and the decorator transforms in sync regardless
|
|
21
|
-
* of which path is taken.
|
|
17
|
+
* @remarks Falls back to the global-registry symbol, which is what Babel and esbuild emit until
|
|
18
|
+
* a runtime ships `Symbol.metadata` natively.
|
|
22
19
|
*
|
|
23
20
|
* @since 0.3.16-canary.0
|
|
24
21
|
*/
|
package/src/registry.ts
CHANGED
|
@@ -1,45 +1,7 @@
|
|
|
1
1
|
import type { Binding } from "#/binding";
|
|
2
2
|
import { bindingSlotEquals, bindingSlotToString } from "#/binding";
|
|
3
3
|
import type { Token } from "#/token";
|
|
4
|
-
import type { BindingIdentifier, Constructor, DependencyKey } from "#/types";
|
|
5
|
-
|
|
6
|
-
// Superset of every binding kind's fields, used to normalize hidden classes below.
|
|
7
|
-
type BindingFieldSuperset = Binding & {
|
|
8
|
-
readonly scope?: unknown;
|
|
9
|
-
readonly target?: unknown;
|
|
10
|
-
readonly factory?: unknown;
|
|
11
|
-
readonly deps?: unknown;
|
|
12
|
-
readonly value?: unknown;
|
|
13
|
-
readonly onActivation?: unknown;
|
|
14
|
-
readonly onDeactivation?: unknown;
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Rebuilds a binding with every kind's fields in one fixed key order, so all stored
|
|
19
|
-
* bindings share a single V8 hidden class. Mixed binding kinds otherwise turn the
|
|
20
|
-
* resolver's hot property reads (kind/scope/factory/...) megamorphic, which costs
|
|
21
|
-
* ~30% throughput in processes that exercise many kinds. The value-erasing cast is
|
|
22
|
-
* safe: every declared field is copied verbatim and `kind` stays the discriminant.
|
|
23
|
-
*/
|
|
24
|
-
function normalizeBindingShape(binding: Binding): Binding {
|
|
25
|
-
const source = binding as BindingFieldSuperset;
|
|
26
|
-
return {
|
|
27
|
-
kind: source.kind,
|
|
28
|
-
id: source.id,
|
|
29
|
-
inFlight: false,
|
|
30
|
-
frame: undefined,
|
|
31
|
-
token: source.token,
|
|
32
|
-
slot: source.slot,
|
|
33
|
-
predicate: source.predicate,
|
|
34
|
-
scope: source.scope,
|
|
35
|
-
target: source.target,
|
|
36
|
-
factory: source.factory,
|
|
37
|
-
deps: source.deps,
|
|
38
|
-
value: source.value,
|
|
39
|
-
onActivation: source.onActivation,
|
|
40
|
-
onDeactivation: source.onDeactivation,
|
|
41
|
-
} as Binding;
|
|
42
|
-
}
|
|
4
|
+
import type { BindingIdentifier, BindingTag, Constructor, DependencyKey } from "#/types";
|
|
43
5
|
|
|
44
6
|
/**
|
|
45
7
|
* @since 0.3.16-canary.0
|
|
@@ -51,31 +13,45 @@ export class BindingRegistry {
|
|
|
51
13
|
readonly #bindings = new Map<DependencyKey, Array<Binding>>();
|
|
52
14
|
// Fast lookup by binding ID
|
|
53
15
|
readonly #byId = new Map<BindingIdentifier, Binding>();
|
|
54
|
-
// Fast lookup for slot { name, tags: [] }
|
|
55
|
-
|
|
16
|
+
// Fast lookup for slot { name, tags: [] } — unallocated until a named binding is registered.
|
|
17
|
+
#simpleNamed: Map<DependencyKey, Map<string, Binding>> | undefined;
|
|
56
18
|
// Fast path for one default slot binding with no predicate
|
|
57
19
|
readonly #fastDefault = new Map<DependencyKey, Binding>();
|
|
58
|
-
// Fast lookup for slot { name: undefined, tags: [[key, value]] } with no predicate
|
|
59
|
-
|
|
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;
|
|
60
23
|
|
|
61
24
|
/** Monotonic version — increments on every mutation. */
|
|
62
25
|
get version(): number {
|
|
63
26
|
return this.#version;
|
|
64
27
|
}
|
|
65
28
|
|
|
66
|
-
/**
|
|
67
|
-
|
|
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 {
|
|
68
44
|
this.#version += 1;
|
|
69
|
-
const binding = normalizeBindingShape(uncommittedBinding);
|
|
70
45
|
const key = binding.token as DependencyKey;
|
|
71
|
-
//
|
|
46
|
+
// Eager, not computed: a bind is usually the token's first, so the fallback is usually the
|
|
47
|
+
// one that gets stored — and the computed form would add a call to allocating it anyway.
|
|
72
48
|
const bindingsForToken = this.#bindings.getOrInsert(key, []);
|
|
73
49
|
|
|
74
50
|
// Only apply last-wins for slot-based bindings (not predicate-only)
|
|
75
51
|
let displacedBinding: Binding | undefined;
|
|
76
|
-
if (!
|
|
52
|
+
if (!isPurePredicateBinding(binding)) {
|
|
77
53
|
const existingIndex = bindingsForToken.findIndex(
|
|
78
|
-
(candidate) => !
|
|
54
|
+
(candidate) => !isPurePredicateBinding(candidate) && bindingSlotEquals(candidate.slot, binding.slot),
|
|
79
55
|
);
|
|
80
56
|
if (existingIndex !== -1) {
|
|
81
57
|
displacedBinding = bindingsForToken[existingIndex]!;
|
|
@@ -100,8 +76,8 @@ export class BindingRegistry {
|
|
|
100
76
|
const key = token as DependencyKey;
|
|
101
77
|
const bindingsForToken = this.#bindings.get(key) ?? [];
|
|
102
78
|
this.#bindings.delete(key);
|
|
103
|
-
this.#simpleNamed
|
|
104
|
-
this.#simpleTagged
|
|
79
|
+
this.#simpleNamed?.delete(key);
|
|
80
|
+
this.#simpleTagged?.delete(key);
|
|
105
81
|
this.#fastDefault.delete(key);
|
|
106
82
|
for (const binding of bindingsForToken) {
|
|
107
83
|
this.#byId.delete(binding.id);
|
|
@@ -128,8 +104,8 @@ export class BindingRegistry {
|
|
|
128
104
|
this.#deindexSimpleTaggedBinding(key, binding);
|
|
129
105
|
if (bindingsForToken.length === 0) {
|
|
130
106
|
this.#bindings.delete(key);
|
|
131
|
-
this.#simpleNamed
|
|
132
|
-
this.#simpleTagged
|
|
107
|
+
this.#simpleNamed?.delete(key);
|
|
108
|
+
this.#simpleTagged?.delete(key);
|
|
133
109
|
this.#fastDefault.delete(key);
|
|
134
110
|
} else {
|
|
135
111
|
this.#refreshFastDefaultForToken(key);
|
|
@@ -170,19 +146,19 @@ export class BindingRegistry {
|
|
|
170
146
|
const all = this.allBindings();
|
|
171
147
|
this.#bindings.clear();
|
|
172
148
|
this.#byId.clear();
|
|
173
|
-
this.#simpleNamed
|
|
174
|
-
this.#simpleTagged
|
|
149
|
+
this.#simpleNamed?.clear();
|
|
150
|
+
this.#simpleTagged?.clear();
|
|
175
151
|
this.#fastDefault.clear();
|
|
176
152
|
return all;
|
|
177
153
|
}
|
|
178
154
|
|
|
179
155
|
getSimpleNamed(token: Token<unknown> | Constructor, name: string): Binding | undefined {
|
|
180
|
-
return this.#simpleNamed
|
|
156
|
+
return this.#simpleNamed?.get(token as DependencyKey)?.get(name);
|
|
181
157
|
}
|
|
182
158
|
|
|
183
159
|
getSimpleTagged(token: Token<unknown> | Constructor, tagKey: string, tagValue: unknown): Binding | undefined {
|
|
184
160
|
return this.#simpleTagged
|
|
185
|
-
|
|
161
|
+
?.get(token as DependencyKey)
|
|
186
162
|
?.get(tagKey)
|
|
187
163
|
?.get(tagValue);
|
|
188
164
|
}
|
|
@@ -198,23 +174,23 @@ export class BindingRegistry {
|
|
|
198
174
|
}
|
|
199
175
|
|
|
200
176
|
#indexSimpleTaggedBinding(tokenKey: DependencyKey, binding: Binding): void {
|
|
201
|
-
const
|
|
202
|
-
if (
|
|
177
|
+
const tag = simpleTagOf(binding);
|
|
178
|
+
if (tag === undefined) {
|
|
203
179
|
return;
|
|
204
180
|
}
|
|
205
|
-
const [tagKey, tagValue] =
|
|
206
|
-
const byTagKey = this.#simpleTagged.getOrInsert(tokenKey, new Map<string, Map<unknown, Binding>>());
|
|
181
|
+
const [tagKey, tagValue] = tag;
|
|
182
|
+
const byTagKey = (this.#simpleTagged ??= new Map()).getOrInsert(tokenKey, new Map<string, Map<unknown, Binding>>());
|
|
207
183
|
const byTagValue = byTagKey.getOrInsert(tagKey, new Map<unknown, Binding>());
|
|
208
184
|
byTagValue.set(tagValue, binding);
|
|
209
185
|
}
|
|
210
186
|
|
|
211
187
|
#deindexSimpleTaggedBinding(tokenKey: DependencyKey, binding: Binding): void {
|
|
212
|
-
const
|
|
213
|
-
if (
|
|
188
|
+
const tag = simpleTagOf(binding);
|
|
189
|
+
if (tag === undefined) {
|
|
214
190
|
return;
|
|
215
191
|
}
|
|
216
|
-
const [tagKey, tagValue] =
|
|
217
|
-
const byTagKey = this.#simpleTagged
|
|
192
|
+
const [tagKey, tagValue] = tag;
|
|
193
|
+
const byTagKey = this.#simpleTagged?.get(tokenKey);
|
|
218
194
|
if (byTagKey === undefined) {
|
|
219
195
|
return;
|
|
220
196
|
}
|
|
@@ -228,59 +204,79 @@ export class BindingRegistry {
|
|
|
228
204
|
if (byTagValue.size === 0) {
|
|
229
205
|
byTagKey.delete(tagKey);
|
|
230
206
|
if (byTagKey.size === 0) {
|
|
231
|
-
this.#simpleTagged
|
|
207
|
+
this.#simpleTagged!.delete(tokenKey);
|
|
232
208
|
}
|
|
233
209
|
}
|
|
234
210
|
}
|
|
235
211
|
}
|
|
236
212
|
|
|
237
|
-
#isPurePredicateBinding(binding: Binding): boolean {
|
|
238
|
-
const slot = binding.slot;
|
|
239
|
-
const hasPredicate = binding.predicate !== undefined;
|
|
240
|
-
const hasConstraint = slot.name !== undefined || slot.tags.length > 0;
|
|
241
|
-
// Pure predicate = has predicate but no slot constraint (name/tags)
|
|
242
|
-
return hasPredicate && !hasConstraint;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
213
|
#indexSimpleNamedBinding(tokenKey: DependencyKey, binding: Binding): void {
|
|
246
|
-
const
|
|
247
|
-
if (
|
|
214
|
+
const name = simpleNameOf(binding);
|
|
215
|
+
if (name === undefined) {
|
|
248
216
|
return;
|
|
249
217
|
}
|
|
250
|
-
const bindingsByName = this.#simpleNamed.getOrInsert(tokenKey, new Map<string, Binding>());
|
|
251
|
-
bindingsByName.set(
|
|
218
|
+
const bindingsByName = (this.#simpleNamed ??= new Map()).getOrInsert(tokenKey, new Map<string, Binding>());
|
|
219
|
+
bindingsByName.set(name, binding);
|
|
252
220
|
}
|
|
253
221
|
|
|
254
222
|
#deindexSimpleNamedBinding(tokenKey: DependencyKey, binding: Binding): void {
|
|
255
|
-
const
|
|
256
|
-
if (
|
|
223
|
+
const name = simpleNameOf(binding);
|
|
224
|
+
if (name === undefined) {
|
|
257
225
|
return;
|
|
258
226
|
}
|
|
259
|
-
const bindingsByName = this.#simpleNamed
|
|
227
|
+
const bindingsByName = this.#simpleNamed?.get(tokenKey);
|
|
260
228
|
if (bindingsByName === undefined) {
|
|
261
229
|
return;
|
|
262
230
|
}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
bindingsByName.delete(slot.name);
|
|
231
|
+
if (bindingsByName.get(name)?.id === binding.id) {
|
|
232
|
+
bindingsByName.delete(name);
|
|
266
233
|
if (bindingsByName.size === 0) {
|
|
267
|
-
this.#simpleNamed
|
|
234
|
+
this.#simpleNamed!.delete(tokenKey);
|
|
268
235
|
}
|
|
269
236
|
}
|
|
270
237
|
}
|
|
271
238
|
|
|
272
239
|
#refreshFastDefaultForToken(tokenKey: DependencyKey): void {
|
|
273
240
|
const bindingsForToken = this.#bindings.get(tokenKey);
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
}
|
|
278
|
-
const onlyBinding = bindingsForToken[0]!;
|
|
279
|
-
const isDefaultSlot = onlyBinding.slot.name === undefined && onlyBinding.slot.tags.length === 0;
|
|
280
|
-
if (!isDefaultSlot || onlyBinding.predicate !== undefined) {
|
|
281
|
-
this.#fastDefault.delete(tokenKey);
|
|
241
|
+
const onlyBinding = bindingsForToken?.length === 1 ? bindingsForToken[0]! : undefined;
|
|
242
|
+
if (onlyBinding !== undefined && isDefaultSlotBinding(onlyBinding)) {
|
|
243
|
+
this.#fastDefault.set(tokenKey, onlyBinding);
|
|
282
244
|
return;
|
|
283
245
|
}
|
|
284
|
-
this.#fastDefault.
|
|
246
|
+
this.#fastDefault.delete(tokenKey);
|
|
285
247
|
}
|
|
248
|
+
|
|
249
|
+
/** Whether the deferred table behind `#simpleNamed` has had to be built. */
|
|
250
|
+
get isBuilt(): boolean {
|
|
251
|
+
return this.#simpleNamed !== undefined;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** The name a binding is indexed under, or `undefined` when its slot is more than a plain name. */
|
|
256
|
+
function simpleNameOf(binding: Binding): string | undefined {
|
|
257
|
+
const { name, tags } = binding.slot;
|
|
258
|
+
return name !== undefined && tags.length === 0 ? name : undefined;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* The tag a binding is indexed under, or `undefined` when its slot is more than one plain tag.
|
|
263
|
+
*
|
|
264
|
+
* @remarks A predicate is excluded here — unlike the name index — because the tag index is read
|
|
265
|
+
* without a re-check, so an indexed hit must be unconditional.
|
|
266
|
+
*/
|
|
267
|
+
function simpleTagOf(binding: Binding): BindingTag | undefined {
|
|
268
|
+
const { name, tags } = binding.slot;
|
|
269
|
+
return name === undefined && tags.length === 1 && binding.predicate === undefined ? tags[0] : undefined;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** A binding nothing has to be matched against: the default slot, no predicate. */
|
|
273
|
+
function isDefaultSlotBinding(binding: Binding): boolean {
|
|
274
|
+
const { name, tags } = binding.slot;
|
|
275
|
+
return name === undefined && tags.length === 0 && binding.predicate === undefined;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** A predicate with no slot constraint: last-wins does not apply to it. */
|
|
279
|
+
function isPurePredicateBinding(binding: Binding): boolean {
|
|
280
|
+
const { name, tags } = binding.slot;
|
|
281
|
+
return binding.predicate !== undefined && name === undefined && tags.length === 0;
|
|
286
282
|
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per binding: does resolving it have to go through the activation pipeline?
|
|
3
|
+
*
|
|
4
|
+
* @remarks Versioned on the lifecycle manager plus the own registry, since `onActivation` can be
|
|
5
|
+
* registered at any time and a rebind mints binding ids the memo must not keep forever.
|
|
6
|
+
*/
|
|
7
|
+
import type { Binding } from "#/binding";
|
|
8
|
+
import type { BindingRegistry } from "#/registry";
|
|
9
|
+
import type { ClassIntrospector } from "#/resolution/class-introspector";
|
|
10
|
+
import type { LifecycleManager } from "#/resolution/lifecycle";
|
|
11
|
+
import type { BindingIdentifier } from "#/types";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @since 0.5.0-canary.8
|
|
15
|
+
*/
|
|
16
|
+
export class ActivationNeedCache {
|
|
17
|
+
readonly #needByBindingId = new Map<BindingIdentifier, boolean>();
|
|
18
|
+
#version = -1;
|
|
19
|
+
readonly #lifecycle: LifecycleManager;
|
|
20
|
+
readonly #classes: ClassIntrospector;
|
|
21
|
+
readonly #registry: BindingRegistry;
|
|
22
|
+
|
|
23
|
+
constructor(lifecycle: LifecycleManager, classes: ClassIntrospector, registry: BindingRegistry) {
|
|
24
|
+
this.#lifecycle = lifecycle;
|
|
25
|
+
this.#classes = classes;
|
|
26
|
+
this.#registry = registry;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
needsActivation<const Value>(binding: Binding<Value>): boolean {
|
|
30
|
+
// The chain writes a binding's own hook in place with no version anything here can see, so it
|
|
31
|
+
// is read fresh on every call; the memo covers only container hooks and lifecycle metadata.
|
|
32
|
+
if (binding.kind !== "alias" && binding.onActivation !== undefined) {
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
const lifecycleVersion = this.#lifecycle.activationVersion;
|
|
36
|
+
// No hooks registered anywhere and none on the binding: only classes can still surprise us,
|
|
37
|
+
// via a @postConstruct we have not looked for yet.
|
|
38
|
+
if (lifecycleVersion === 0 && binding.kind !== "class" && binding.kind !== "alias") {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
// The registry version evicts entries for binding ids a rebind has retired.
|
|
42
|
+
const version = lifecycleVersion + this.#registry.version;
|
|
43
|
+
if (this.#version !== version) {
|
|
44
|
+
this.#needByBindingId.clear();
|
|
45
|
+
this.#version = version;
|
|
46
|
+
}
|
|
47
|
+
const cached = this.#needByBindingId.get(binding.id);
|
|
48
|
+
if (cached !== undefined) {
|
|
49
|
+
return cached;
|
|
50
|
+
}
|
|
51
|
+
const needsActivation =
|
|
52
|
+
binding.kind === "class" ? this.#classNeedsActivation(binding) : this.#nonClassNeedsActivation(binding);
|
|
53
|
+
this.#needByBindingId.set(binding.id, needsActivation);
|
|
54
|
+
return needsActivation;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Settles a class binding's answer once its lifecycle metadata has actually been read, which
|
|
59
|
+
* only happens on the first instantiation.
|
|
60
|
+
*
|
|
61
|
+
* @returns the answer to use for this resolve — possibly now `false` where it was a
|
|
62
|
+
* conservative `true`.
|
|
63
|
+
*/
|
|
64
|
+
refreshAfterFirstInstantiation<Value>(binding: Binding<Value>, needsActivation: boolean): boolean {
|
|
65
|
+
if (binding.kind !== "class" || this.#classes.knownPostConstruct(binding.target) !== undefined) {
|
|
66
|
+
return needsActivation;
|
|
67
|
+
}
|
|
68
|
+
this.#classes.discoverPostConstruct(binding.target);
|
|
69
|
+
this.#needByBindingId.delete(binding.id);
|
|
70
|
+
return this.needsActivation(binding);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Own hooks are answered before the memo, so both computations cover the memoizable rest only.
|
|
74
|
+
#classNeedsActivation<const Value>(binding: Binding<Value> & { kind: "class" }): boolean {
|
|
75
|
+
if (this.#lifecycle.hasActivationHandlers(binding.token)) {
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
// Unknown lifecycle metadata: activate once so the first instantiation can settle it.
|
|
79
|
+
return this.#classes.knownPostConstruct(binding.target) !== false;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
#nonClassNeedsActivation<const Value>(binding: Binding<Value>): boolean {
|
|
83
|
+
return this.#lifecycle.hasActivationHandlers(binding.token);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
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
|
+
const newNameToEntryMap = <Owner>(): Map<string, DefaultLookupEntry<Owner> | null> => new Map();
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @since 0.5.0-canary.9
|
|
38
|
+
*/
|
|
39
|
+
export class BindingLookupCache<Owner> {
|
|
40
|
+
readonly #byToken = new Map<Token<unknown> | Constructor, DefaultLookupEntry<Owner> | null>();
|
|
41
|
+
#version = -1;
|
|
42
|
+
// One entry in front of the map: the two shapes that reach here — an alias, and a token owned by
|
|
43
|
+
// a parent — are both resolved in a loop over the same token. `null` is a real answer, so absence
|
|
44
|
+
// is tracked by the token slot rather than by the entry.
|
|
45
|
+
#lastToken: Token<unknown> | Constructor | undefined;
|
|
46
|
+
#lastEntry: DefaultLookupEntry<Owner> | null = null;
|
|
47
|
+
readonly #byTokenAndName = new Map<Token<unknown> | Constructor, Map<string, DefaultLookupEntry<Owner> | null>>();
|
|
48
|
+
#namedVersion = -1;
|
|
49
|
+
|
|
50
|
+
readonly #registry: BindingRegistry;
|
|
51
|
+
readonly #owner: Owner;
|
|
52
|
+
readonly #parent: BindingLookupCache<Owner> | undefined;
|
|
53
|
+
|
|
54
|
+
constructor(registry: BindingRegistry, owner: Owner, parent: BindingLookupCache<Owner> | undefined) {
|
|
55
|
+
this.#registry = registry;
|
|
56
|
+
this.#owner = owner;
|
|
57
|
+
this.#parent = parent;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Summed registry versions of this cache's whole chain — the memo stamp. */
|
|
61
|
+
chainVersion(): number {
|
|
62
|
+
let version = this.#registry.version;
|
|
63
|
+
for (let cache = this.#parent; cache !== undefined; cache = cache.#parent) {
|
|
64
|
+
version += cache.#registry.version;
|
|
65
|
+
}
|
|
66
|
+
return version;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** `null` when the token's shape needs the full selection path. */
|
|
70
|
+
defaultEntry(token: Token<unknown> | Constructor): DefaultLookupEntry<Owner> | null {
|
|
71
|
+
const version = this.chainVersion();
|
|
72
|
+
if (version !== this.#version) {
|
|
73
|
+
this.#byToken.clear();
|
|
74
|
+
this.#version = version;
|
|
75
|
+
this.#lastToken = undefined;
|
|
76
|
+
} else if (token === this.#lastToken) {
|
|
77
|
+
return this.#lastEntry;
|
|
78
|
+
}
|
|
79
|
+
let entry = this.#byToken.get(token);
|
|
80
|
+
if (entry === undefined) {
|
|
81
|
+
entry = this.#foldAliases(token);
|
|
82
|
+
this.#byToken.set(token, entry);
|
|
83
|
+
}
|
|
84
|
+
this.#lastToken = token;
|
|
85
|
+
this.#lastEntry = entry;
|
|
86
|
+
return entry;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** `null` when the name's shape needs the full selection path. */
|
|
90
|
+
namedEntry(token: Token<unknown> | Constructor, name: string): DefaultLookupEntry<Owner> | null {
|
|
91
|
+
const version = this.chainVersion();
|
|
92
|
+
if (version !== this.#namedVersion) {
|
|
93
|
+
this.#byTokenAndName.clear();
|
|
94
|
+
this.#namedVersion = version;
|
|
95
|
+
}
|
|
96
|
+
// Computed, not eager: this runs on every named resolve, and `getOrInsert(token, new Map())`
|
|
97
|
+
// would allocate a Map per call only to discard it on the hit that follows.
|
|
98
|
+
const byName = this.#byTokenAndName.getOrInsertComputed(token, newNameToEntryMap);
|
|
99
|
+
let entry = byName.get(name);
|
|
100
|
+
if (entry === undefined) {
|
|
101
|
+
entry = this.#findNamedInChain(token, name);
|
|
102
|
+
byName.set(name, entry);
|
|
103
|
+
}
|
|
104
|
+
return entry;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
#foldAliases(token: Token<unknown> | Constructor): DefaultLookupEntry<Owner> | null {
|
|
108
|
+
let current = token;
|
|
109
|
+
for (let hop = 0; hop < ALIAS_HOP_LIMIT; hop += 1) {
|
|
110
|
+
const entry = this.#findDefaultInChain(current);
|
|
111
|
+
if (entry === null) {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
if (entry.binding.kind !== "alias") {
|
|
115
|
+
return entry;
|
|
116
|
+
}
|
|
117
|
+
current = entry.binding.target;
|
|
118
|
+
}
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
#findDefaultInChain(token: Token<unknown> | Constructor): DefaultLookupEntry<Owner> | null {
|
|
123
|
+
const fast = this.#registry.getFastDefault(token);
|
|
124
|
+
if (fast !== undefined) {
|
|
125
|
+
return { binding: fast, owner: this.#owner };
|
|
126
|
+
}
|
|
127
|
+
// A level with non-fast bindings (multi-slot / predicate) needs full selection — bail.
|
|
128
|
+
if (this.#registry.has(token)) {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
return this.#parent === undefined ? null : this.#parent.#findDefaultInChain(token);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
#findNamedInChain(token: Token<unknown> | Constructor, name: string): DefaultLookupEntry<Owner> | null {
|
|
135
|
+
const named = this.#registry.getSimpleNamed(token, name);
|
|
136
|
+
if (named !== undefined) {
|
|
137
|
+
// Predicates need a live context; aliases carry options through the full path.
|
|
138
|
+
if (named.predicate !== undefined || named.kind === "alias") {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
return { binding: named, owner: this.#owner };
|
|
142
|
+
}
|
|
143
|
+
if (this.#registry.has(token)) {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
return this.#parent === undefined ? null : this.#parent.#findNamedInChain(token, name);
|
|
147
|
+
}
|
|
148
|
+
}
|