@codefast/di 0.8.1 → 0.10.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 +164 -0
- package/LICENSE +1 -1
- package/README.md +29 -20
- package/dist/ambient/active-container.d.ts +13 -0
- package/dist/ambient/active-container.js +24 -0
- package/dist/container/binding-builders.d.ts +42 -20
- package/dist/container/binding-builders.js +155 -87
- package/dist/container/container.d.ts +10 -10
- package/dist/container/container.js +55 -23
- package/dist/core/binding.d.ts +55 -57
- package/dist/core/binding.js +11 -37
- package/dist/core/constraint-requirement.d.ts +19 -4
- package/dist/core/constraint-requirement.js +19 -9
- package/dist/core/registry.d.ts +47 -4
- package/dist/core/registry.js +361 -159
- package/dist/core/state-epoch.d.ts +16 -0
- package/dist/core/state-epoch.js +21 -0
- package/dist/core/tag.d.ts +2 -2
- package/dist/core/tag.js +2 -2
- package/dist/core/token.d.ts +24 -4
- package/dist/core/token.js +1 -1
- package/dist/core/types.d.ts +15 -6
- package/dist/decorators/inject.d.ts +1 -1
- package/dist/decorators/inject.js +6 -4
- package/dist/errors/diagnostics.d.ts +2 -0
- package/dist/errors/errors.d.ts +33 -1
- package/dist/errors/errors.js +44 -3
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/injection/descriptor.d.ts +16 -8
- package/dist/injection/descriptor.js +3 -1
- package/dist/injection/resolve-options.d.ts +6 -0
- package/dist/injection/resolve-options.js +16 -0
- package/dist/introspection/dependency-graph.js +10 -5
- package/dist/introspection/inspector.d.ts +3 -1
- package/dist/introspection/inspector.js +10 -24
- package/dist/lifecycle/lifecycle-manager.js +10 -8
- package/dist/lifecycle/scope-manager.js +1 -1
- package/dist/metadata/metadata-reader-token.js +1 -1
- package/dist/resolution/cache/activation-need.d.ts +2 -0
- package/dist/resolution/cache/activation-need.js +13 -6
- package/dist/resolution/cache/binding-lookup-cache.d.ts +34 -1
- package/dist/resolution/cache/binding-lookup-cache.js +96 -12
- package/dist/resolution/cache/class-introspector.d.ts +1 -1
- package/dist/resolution/cache/class-introspector.js +28 -14
- package/dist/resolution/context.d.ts +8 -8
- package/dist/resolution/plan/instantiation-plan.d.ts +12 -0
- package/dist/resolution/plan/instantiation-plan.js +156 -65
- package/dist/resolution/plan/plan-codegen.d.ts +100 -0
- package/dist/resolution/plan/plan-codegen.js +185 -0
- package/dist/resolution/resolver.d.ts +14 -4
- package/dist/resolution/resolver.js +375 -134
- package/dist/resolution/select/binding-select.js +12 -7
- package/dist/resolution/select/constraints.d.ts +7 -4
- package/dist/resolution/select/constraints.js +34 -13
- package/package.json +14 -40
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { clearBindingFrame,
|
|
1
|
+
import { clearBindingFrame, createBindingSlot, DEFAULT_BINDING_SLOT, generateBindingId, NO_INSTANCE, } from "#/core/binding";
|
|
2
2
|
import { mergingConstraintRequirements } from "#/core/constraint-requirement";
|
|
3
3
|
import { slotName } from "#/core/tag";
|
|
4
4
|
import { tokenName } from "#/core/token";
|
|
5
|
-
import { ChainNotRegisteredError, SelfBindingRequiresClassError } from "#/errors/errors";
|
|
5
|
+
import { ChainAlreadyRegisteredError, ChainNotRegisteredError, ManyBindingSlotError, SelfBindingRequiresClassError, } from "#/errors/errors";
|
|
6
6
|
import { normalizeToDescriptor } from "#/injection/descriptor";
|
|
7
7
|
/** One criterion per key: re-tagging the same key replaces it rather than asking for both values. */
|
|
8
8
|
function updateSlotTag(slot, criterion) {
|
|
@@ -16,112 +16,139 @@ function updateSlotTag(slot, criterion) {
|
|
|
16
16
|
}
|
|
17
17
|
return createBindingSlot(tags);
|
|
18
18
|
}
|
|
19
|
-
/** Record a module's binding id, dropping the id the chain re-slotted away from. */
|
|
20
|
-
function trackBindingForModule(ids, id, previousId) {
|
|
21
|
-
if (previousId !== undefined) {
|
|
22
|
-
const previousIndex = ids.indexOf(previousId);
|
|
23
|
-
if (previousIndex !== -1) {
|
|
24
|
-
ids.splice(previousIndex, 1);
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
ids.push(id);
|
|
28
|
-
}
|
|
29
19
|
// ── BindingChain ─────────────────────────────────────────────────────────────────────────────────────────────────────
|
|
30
20
|
/**
|
|
31
|
-
* The one builder behind `bind()` and every `to*()` return type
|
|
32
|
-
*
|
|
33
|
-
*
|
|
21
|
+
* The one builder behind `bind()` and every `to*()` return type — and the binding it registers.
|
|
22
|
+
*
|
|
23
|
+
* @remarks The binding fields come first, in the order every binding shares, and the chain's own
|
|
24
|
+
* bookkeeping follows as private fields; `to*()` fills the fields in place and hands this object to
|
|
25
|
+
* the registry, so a plain bind is one allocation. Refinements write the registered object: a scope
|
|
26
|
+
* or hook in place, a slot or predicate through the registry so its indexes follow.
|
|
34
27
|
*
|
|
35
28
|
* @since 0.5.0-canary.8
|
|
36
29
|
*/
|
|
37
30
|
export class BindingChain {
|
|
38
|
-
//
|
|
39
|
-
|
|
31
|
+
// The binding. Every field, every kind, written in this order and nowhere else: one hidden class.
|
|
32
|
+
kind = "constant";
|
|
33
|
+
identifier = generateBindingId();
|
|
34
|
+
inFlight = false;
|
|
35
|
+
frame = undefined;
|
|
36
|
+
instance = NO_INSTANCE;
|
|
37
|
+
token;
|
|
38
|
+
slot = DEFAULT_BINDING_SLOT;
|
|
39
|
+
predicate = undefined;
|
|
40
|
+
isMany = false;
|
|
41
|
+
scope = "singleton";
|
|
42
|
+
target = undefined;
|
|
43
|
+
factory = undefined;
|
|
44
|
+
deps = undefined;
|
|
45
|
+
value = undefined;
|
|
46
|
+
activationHook = undefined;
|
|
47
|
+
deactivationHook = undefined;
|
|
48
|
+
// The chain. Set by the first `to*()`, which is the only one a chain accepts.
|
|
49
|
+
#isRegistered = false;
|
|
40
50
|
// Allocated only by a chain that actually displaces something — most never do.
|
|
41
51
|
#displacedByChain;
|
|
42
52
|
// Registry version after this chain's last write — a mismatch means someone else wrote in between.
|
|
43
53
|
#versionAfterLastWrite = -1;
|
|
44
|
-
#token;
|
|
45
54
|
#registration;
|
|
46
55
|
constructor(token, registration) {
|
|
47
|
-
this
|
|
56
|
+
this.token = token;
|
|
48
57
|
this.#registration = registration;
|
|
49
58
|
}
|
|
50
|
-
/**
|
|
51
|
-
#
|
|
52
|
-
|
|
53
|
-
|
|
59
|
+
/** This object as the registry and the resolver see it: the value type erased once, here. */
|
|
60
|
+
get #binding() {
|
|
61
|
+
return this;
|
|
62
|
+
}
|
|
63
|
+
/** Loud failure for a refinement before `to*()`. */
|
|
64
|
+
#requireRegistered() {
|
|
65
|
+
if (!this.#isRegistered) {
|
|
66
|
+
throw new ChainNotRegisteredError(tokenName(this.token));
|
|
54
67
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
#
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
68
|
+
}
|
|
69
|
+
/** Loud failure for a second `to*()`, raised before the first one's fields can be overwritten. */
|
|
70
|
+
#requireUnregistered() {
|
|
71
|
+
if (this.#isRegistered) {
|
|
72
|
+
throw new ChainAlreadyRegisteredError(tokenName(this.token));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
#register(kind, scope) {
|
|
76
|
+
this.kind = kind;
|
|
77
|
+
this.scope = scope;
|
|
78
|
+
this.#isRegistered = true;
|
|
79
|
+
this.#commit(undefined);
|
|
63
80
|
return this;
|
|
64
81
|
}
|
|
65
82
|
// ── Registration ───────────────────────────────────────────────────────────────────────────────────────────────────
|
|
66
83
|
to(type) {
|
|
67
|
-
|
|
84
|
+
this.#requireUnregistered();
|
|
85
|
+
this.target = type;
|
|
86
|
+
return this.#register("class", "transient");
|
|
68
87
|
}
|
|
69
88
|
toSelf() {
|
|
70
|
-
|
|
71
|
-
|
|
89
|
+
this.#requireUnregistered();
|
|
90
|
+
if (typeof this.token !== "function") {
|
|
91
|
+
throw new SelfBindingRequiresClassError(tokenName(this.token));
|
|
72
92
|
}
|
|
73
|
-
|
|
93
|
+
this.target = this.token;
|
|
94
|
+
return this.#register("class", "transient");
|
|
74
95
|
}
|
|
75
96
|
toConstantValue(value) {
|
|
76
|
-
|
|
97
|
+
this.#requireUnregistered();
|
|
98
|
+
this.value = value;
|
|
99
|
+
return this.#register("constant", "singleton");
|
|
77
100
|
}
|
|
78
101
|
toDynamic(factory) {
|
|
79
|
-
|
|
102
|
+
this.#requireUnregistered();
|
|
103
|
+
this.factory = factory;
|
|
104
|
+
return this.#register("dynamic", "transient");
|
|
80
105
|
}
|
|
81
106
|
toDynamicAsync(factory) {
|
|
82
|
-
|
|
107
|
+
this.#requireUnregistered();
|
|
108
|
+
this.factory = factory;
|
|
109
|
+
return this.#register("dynamic-async", "transient");
|
|
83
110
|
}
|
|
84
111
|
toResolved(factory, deps) {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
scope: "transient",
|
|
90
|
-
});
|
|
112
|
+
this.#requireUnregistered();
|
|
113
|
+
this.factory = factory;
|
|
114
|
+
this.deps = deps.map((dependency) => normalizeToDescriptor(dependency));
|
|
115
|
+
return this.#register("resolved", "transient");
|
|
91
116
|
}
|
|
92
117
|
toResolvedAsync(factory, deps) {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
scope: "transient",
|
|
98
|
-
});
|
|
118
|
+
this.#requireUnregistered();
|
|
119
|
+
this.factory = factory;
|
|
120
|
+
this.deps = deps.map((dependency) => normalizeToDescriptor(dependency));
|
|
121
|
+
return this.#register("resolved-async", "transient");
|
|
99
122
|
}
|
|
100
123
|
toAlias(target) {
|
|
101
|
-
|
|
124
|
+
this.#requireUnregistered();
|
|
125
|
+
this.target = target;
|
|
126
|
+
return this.#register("alias", "transient");
|
|
102
127
|
}
|
|
103
128
|
// ── Refinement ─────────────────────────────────────────────────────────────────────────────────────────────────────
|
|
104
|
-
// Slot and predicate are what the registry indexes on, so a re-slot
|
|
105
|
-
//
|
|
129
|
+
// Slot and predicate are what the registry indexes on, so a re-slot takes the binding out of the
|
|
130
|
+
// registry, rewrites the two fields while it is out, and registers it again — same object, same id.
|
|
106
131
|
#reslot(slot, predicate) {
|
|
107
|
-
|
|
108
|
-
this.#
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
132
|
+
this.#requireRegistered();
|
|
133
|
+
this.#commit(() => {
|
|
134
|
+
this.slot = slot;
|
|
135
|
+
this.predicate = predicate;
|
|
136
|
+
// The frame reports the slot, so a resolve before this call memoized the previous one.
|
|
137
|
+
clearBindingFrame(this.#binding);
|
|
138
|
+
});
|
|
112
139
|
return this;
|
|
113
140
|
}
|
|
114
141
|
#withScope(scope) {
|
|
115
|
-
|
|
116
|
-
if (
|
|
142
|
+
this.#requireRegistered();
|
|
143
|
+
if (this.scope !== scope) {
|
|
117
144
|
// An instance cached under the old scope must not survive the change — a later flip back
|
|
118
145
|
// to that scope would resurrect it.
|
|
119
|
-
this.#registration.scope.deleteSingleton(binding);
|
|
120
|
-
this.#registration.scope.deleteScoped(
|
|
121
|
-
|
|
146
|
+
this.#registration.scope.deleteSingleton(this.#binding);
|
|
147
|
+
this.#registration.scope.deleteScoped(this.identifier);
|
|
148
|
+
this.scope = scope;
|
|
122
149
|
}
|
|
123
150
|
// The frame reports the scope, so a resolve before this call memoized the previous one.
|
|
124
|
-
clearBindingFrame(binding);
|
|
151
|
+
clearBindingFrame(this.#binding);
|
|
125
152
|
this.#registration.registry.touch();
|
|
126
153
|
this.#versionAfterLastWrite = this.#registration.registry.version;
|
|
127
154
|
return this;
|
|
@@ -129,26 +156,60 @@ export class BindingChain {
|
|
|
129
156
|
// SPEC calls a candidate a binding that passes *all* of a chain's predicates, and the chain type
|
|
130
157
|
// reads as refinement, so a second `when()` narrows rather than replaces.
|
|
131
158
|
when(predicate) {
|
|
132
|
-
|
|
133
|
-
const previous =
|
|
134
|
-
if (previous === undefined) {
|
|
135
|
-
return this.#reslot(binding.slot, predicate);
|
|
136
|
-
}
|
|
159
|
+
this.#requireRegistered();
|
|
160
|
+
const previous = this.predicate;
|
|
137
161
|
// The composite carries both sides' requirements, so validate() still sees them.
|
|
138
|
-
const
|
|
139
|
-
|
|
162
|
+
const narrowed = previous === undefined
|
|
163
|
+
? predicate
|
|
164
|
+
: mergingConstraintRequirements((ctx) => previous(ctx) && predicate(ctx), previous, predicate);
|
|
165
|
+
const { registry } = this.#registration;
|
|
166
|
+
// The slot is unchanged, so nothing has to be re-indexed or displaced. With the last registry
|
|
167
|
+
// write this chain's own and nothing parked, the binding is provably live and is rewritten in
|
|
168
|
+
// place; otherwise the re-slot path re-checks liveness and restores what the shape frees.
|
|
169
|
+
if (registry.version === this.#versionAfterLastWrite && this.#displacedByChain === undefined) {
|
|
170
|
+
registry.setPredicate(this.#binding, narrowed);
|
|
171
|
+
this.#versionAfterLastWrite = registry.version;
|
|
172
|
+
return this;
|
|
173
|
+
}
|
|
174
|
+
return this.#reslot(this.slot, narrowed);
|
|
140
175
|
}
|
|
141
176
|
whenNamed(name) {
|
|
142
177
|
return this.whenTagged(slotName.of(name));
|
|
143
178
|
}
|
|
144
179
|
whenTagged(criterion) {
|
|
145
|
-
|
|
146
|
-
|
|
180
|
+
this.#requireRegistered();
|
|
181
|
+
if (this.isMany) {
|
|
182
|
+
throw new ManyBindingSlotError(tokenName(this.token));
|
|
183
|
+
}
|
|
184
|
+
return this.#reslot(updateSlotTag(this.slot, criterion), this.predicate);
|
|
185
|
+
}
|
|
186
|
+
many() {
|
|
187
|
+
this.#requireRegistered();
|
|
188
|
+
if (this.slot.tags.length !== 0) {
|
|
189
|
+
throw new ManyBindingSlotError(tokenName(this.token));
|
|
190
|
+
}
|
|
191
|
+
if (this.isMany) {
|
|
192
|
+
return this;
|
|
193
|
+
}
|
|
194
|
+
const { registry } = this.#registration;
|
|
195
|
+
// With the last registry write this chain's own and nothing parked, the binding is provably live
|
|
196
|
+
// and displaced nobody, so membership is written in place and the registry only moves it out of
|
|
197
|
+
// the lone map. Otherwise it goes through the re-slot path, which re-checks liveness and restores
|
|
198
|
+
// an ordinary binding this chain's `to*()` displaced, now that the member frees its slot.
|
|
199
|
+
if (registry.version === this.#versionAfterLastWrite && this.#displacedByChain === undefined) {
|
|
200
|
+
registry.setMany(this.#binding);
|
|
201
|
+
this.#versionAfterLastWrite = registry.version;
|
|
202
|
+
return this;
|
|
203
|
+
}
|
|
204
|
+
this.#commit(() => {
|
|
205
|
+
this.isMany = true;
|
|
206
|
+
});
|
|
207
|
+
return this;
|
|
147
208
|
}
|
|
148
209
|
whenDefault() {
|
|
149
210
|
// The default slot is what a fresh registration already has, so there is nothing to re-slot —
|
|
150
211
|
// but an unregistered chain must fail here exactly as it does in every other refinement.
|
|
151
|
-
this.#
|
|
212
|
+
this.#requireRegistered();
|
|
152
213
|
return this;
|
|
153
214
|
}
|
|
154
215
|
singleton() {
|
|
@@ -161,40 +222,42 @@ export class BindingChain {
|
|
|
161
222
|
return this.#withScope("scoped");
|
|
162
223
|
}
|
|
163
224
|
onActivation(fn) {
|
|
164
|
-
|
|
225
|
+
this.#requireRegistered();
|
|
226
|
+
this.activationHook = fn;
|
|
165
227
|
this.#registration.registry.touch();
|
|
166
228
|
this.#versionAfterLastWrite = this.#registration.registry.version;
|
|
167
229
|
return this;
|
|
168
230
|
}
|
|
169
231
|
onDeactivation(fn) {
|
|
170
|
-
|
|
232
|
+
this.#requireRegistered();
|
|
233
|
+
this.deactivationHook = fn;
|
|
171
234
|
this.#registration.registry.touch();
|
|
172
235
|
this.#versionAfterLastWrite = this.#registration.registry.version;
|
|
173
236
|
return this;
|
|
174
237
|
}
|
|
175
238
|
id() {
|
|
176
|
-
|
|
239
|
+
this.#requireRegistered();
|
|
240
|
+
return this.identifier;
|
|
177
241
|
}
|
|
178
242
|
// ── Registry ───────────────────────────────────────────────────────────────────────────────────────────────────────
|
|
179
243
|
/**
|
|
180
|
-
* Registers
|
|
244
|
+
* Registers this binding; with `rewrite` given, first takes the live binding out, rewrites it, and
|
|
245
|
+
* registers it again.
|
|
181
246
|
*
|
|
182
247
|
* @remarks A `when*()` that follows `to*()` re-slots an already-live binding and can displace one
|
|
183
248
|
* the final shape would never conflict with. Those stay parked in `#displacedByChain` until the
|
|
184
249
|
* chain settles, then get restored.
|
|
185
250
|
*/
|
|
186
|
-
#commit(
|
|
251
|
+
#commit(rewrite) {
|
|
187
252
|
const { registry, moduleBindingIds } = this.#registration;
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
const registered = binding;
|
|
191
|
-
if (previousId !== undefined) {
|
|
253
|
+
const registered = this.#binding;
|
|
254
|
+
if (rewrite !== undefined) {
|
|
192
255
|
// A registry someone else wrote since this chain's last write invalidates the parked
|
|
193
256
|
// snapshot: restoring it could undo an unbind or shadow a newer binding.
|
|
194
257
|
if (registry.version !== this.#versionAfterLastWrite) {
|
|
195
258
|
this.#displacedByChain = undefined;
|
|
196
259
|
}
|
|
197
|
-
if (registry.
|
|
260
|
+
if (!registry.reslot(registered, rewrite)) {
|
|
198
261
|
// The chain's binding is no longer live (unbound or displaced) — a refinement must not
|
|
199
262
|
// resurrect it, so the chain goes inert against the registry.
|
|
200
263
|
this.#displacedByChain = undefined;
|
|
@@ -202,15 +265,20 @@ export class BindingChain {
|
|
|
202
265
|
return;
|
|
203
266
|
}
|
|
204
267
|
}
|
|
268
|
+
else {
|
|
269
|
+
// Each `to*()` starts its own registration, so anything a previous one displaced is not this
|
|
270
|
+
// registration's to restore.
|
|
271
|
+
this.#displacedByChain = undefined;
|
|
272
|
+
}
|
|
205
273
|
const displaced = registry.add(registered);
|
|
206
274
|
if (displaced !== undefined) {
|
|
207
275
|
(this.#displacedByChain ??= []).push(displaced);
|
|
208
276
|
}
|
|
209
|
-
if (
|
|
277
|
+
if (rewrite !== undefined && this.#displacedByChain !== undefined) {
|
|
210
278
|
this.#restoreNonConflicting(this.#displacedByChain);
|
|
211
279
|
}
|
|
212
|
-
if (moduleBindingIds !== undefined) {
|
|
213
|
-
|
|
280
|
+
if (rewrite === undefined && moduleBindingIds !== undefined) {
|
|
281
|
+
moduleBindingIds.push(this.identifier);
|
|
214
282
|
}
|
|
215
283
|
this.#versionAfterLastWrite = registry.version;
|
|
216
284
|
}
|
|
@@ -13,12 +13,12 @@ import type { MetadataReader } from "#/metadata/metadata-types";
|
|
|
13
13
|
*/
|
|
14
14
|
export interface Container {
|
|
15
15
|
readonly isDisposed: boolean;
|
|
16
|
-
bind<Value>(token: Token<Value> | Constructor<Value>): BindToBuilder<Value>;
|
|
16
|
+
bind<Value, Names extends string = string>(token: Token<Value, Names> | Constructor<Value>): BindToBuilder<Value, Names>;
|
|
17
17
|
unbind(tokenOrId: Token<unknown> | Constructor | BindingIdentifier): void;
|
|
18
18
|
unbindAsync(tokenOrId: Token<unknown> | Constructor | BindingIdentifier): Promise<void>;
|
|
19
19
|
unbindAll(): void;
|
|
20
20
|
unbindAllAsync(): Promise<void>;
|
|
21
|
-
rebind<Value>(token: Token<Value> | Constructor<Value>): BindToBuilder<Value>;
|
|
21
|
+
rebind<Value, Names extends string = string>(token: Token<Value, Names> | Constructor<Value>): BindToBuilder<Value, Names>;
|
|
22
22
|
load(...modules: Array<SyncModule>): void;
|
|
23
23
|
loadAsync(...modules: Array<SyncModule | AsyncModule>): Promise<void>;
|
|
24
24
|
unload(...modules: Array<SyncModule>): void;
|
|
@@ -26,20 +26,20 @@ export interface Container {
|
|
|
26
26
|
loadAutoRegistered(registry: AutoRegisterRegistry): number;
|
|
27
27
|
onActivation<Value>(token: Token<Value> | Constructor<Value>, handler: ActivationHandler<Value>): void;
|
|
28
28
|
onDeactivation<Value>(token: Token<Value> | Constructor<Value>, handler: DeactivationHandler<Value>): void;
|
|
29
|
-
resolve<Value>(token: Token<Value> | Constructor<Value>, options?: ResolveOptions): Value;
|
|
30
|
-
resolveAsync<Value>(token: Token<Value> | Constructor<Value>, options?: ResolveOptions): Promise<Value>;
|
|
31
|
-
resolveOptional<Value>(token: Token<Value> | Constructor<Value>, options?: ResolveOptions): Value | undefined;
|
|
32
|
-
resolveOptionalAsync<Value>(token: Token<Value> | Constructor<Value>, options?: ResolveOptions): Promise<Value | undefined>;
|
|
33
|
-
resolveAll<Value>(token: Token<Value> | Constructor<Value>, options?: ResolveOptions):
|
|
34
|
-
resolveAllAsync<Value>(token: Token<Value> | Constructor<Value>, options?: ResolveOptions): Promise<
|
|
29
|
+
resolve<Value, Names extends string = string>(token: Token<Value, Names> | Constructor<Value>, options?: NoInfer<ResolveOptions<Names>>): Value;
|
|
30
|
+
resolveAsync<Value, Names extends string = string>(token: Token<Value, Names> | Constructor<Value>, options?: NoInfer<ResolveOptions<Names>>): Promise<Value>;
|
|
31
|
+
resolveOptional<Value, Names extends string = string>(token: Token<Value, Names> | Constructor<Value>, options?: NoInfer<ResolveOptions<Names>>): Value | undefined;
|
|
32
|
+
resolveOptionalAsync<Value, Names extends string = string>(token: Token<Value, Names> | Constructor<Value>, options?: NoInfer<ResolveOptions<Names>>): Promise<Value | undefined>;
|
|
33
|
+
resolveAll<Value, Names extends string = string>(token: Token<Value, Names> | Constructor<Value>, options?: NoInfer<ResolveOptions<Names>>): ReadonlyArray<Value>;
|
|
34
|
+
resolveAllAsync<Value, Names extends string = string>(token: Token<Value, Names> | Constructor<Value>, options?: NoInfer<ResolveOptions<Names>>): Promise<ReadonlyArray<Value>>;
|
|
35
35
|
createChild(): Container;
|
|
36
36
|
dispose(): Promise<void>;
|
|
37
37
|
[Symbol.asyncDispose](): Promise<void>;
|
|
38
38
|
[Symbol.dispose](): never;
|
|
39
39
|
initializeAsync(): Promise<void>;
|
|
40
40
|
validate(): void;
|
|
41
|
-
has(token: Token<unknown> | Constructor, options?: ResolveOptions): boolean;
|
|
42
|
-
hasOwn(token: Token<unknown> | Constructor, options?: ResolveOptions): boolean;
|
|
41
|
+
has<Names extends string = string>(token: Token<unknown, Names> | Constructor, options?: NoInfer<ResolveOptions<Names>>): boolean;
|
|
42
|
+
hasOwn<Names extends string = string>(token: Token<unknown, Names> | Constructor, options?: NoInfer<ResolveOptions<Names>>): boolean;
|
|
43
43
|
lookupBindings<Value>(token: Token<Value> | Constructor<Value>): ReadonlyArray<BindingSnapshot>;
|
|
44
44
|
inspect(): ContainerSnapshot;
|
|
45
45
|
generateDependencyGraph(options?: GraphOptions): ContainerGraphJson;
|
|
@@ -2,7 +2,7 @@ import { BindingChain } from "#/container/binding-builders";
|
|
|
2
2
|
import { NO_INSTANCE } from "#/core/binding";
|
|
3
3
|
import { effectiveBindingScope } from "#/core/binding-scope";
|
|
4
4
|
import { constraintRequirementsOf } from "#/core/constraint-requirement";
|
|
5
|
-
import { getOrInsert } from "#/core/map-upsert";
|
|
5
|
+
import { getOrInsert, getOrInsertComputed } from "#/core/map-upsert";
|
|
6
6
|
import { isSyncModule, MODULE_SETUP } from "#/core/module";
|
|
7
7
|
import { BindingRegistry } from "#/core/registry";
|
|
8
8
|
import { tokenName } from "#/core/token";
|
|
@@ -18,6 +18,20 @@ import { defaultMetadataReader } from "#/metadata/symbol-metadata-reader";
|
|
|
18
18
|
import { verifyingMetadataReader } from "#/metadata/verifying-metadata-reader";
|
|
19
19
|
import { ROOT_BRANCH } from "#/resolution/path/resolution-path";
|
|
20
20
|
import { DependencyResolver } from "#/resolution/resolver";
|
|
21
|
+
/** Whether a requirement's name is declared — on its token when it names one, on any token otherwise. */
|
|
22
|
+
function isSlotNameDeclared(declared, requirement) {
|
|
23
|
+
if (requirement.tokenName !== undefined) {
|
|
24
|
+
return declared.get(requirement.tokenName)?.has(requirement.name) ?? false;
|
|
25
|
+
}
|
|
26
|
+
for (const names of declared.values()) {
|
|
27
|
+
if (names.has(requirement.name)) {
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
/** Hoisted so the slot-name index's `getOrInsertComputed` allocates only on a miss, no closure per call. */
|
|
34
|
+
const newSlotNameSet = () => new Set();
|
|
21
35
|
// A Record rather than an if-chain, so a new `BindingScope` is a compile error here instead of
|
|
22
36
|
// silently landing in whichever branch happened to be last.
|
|
23
37
|
const APPLY_BINDING_SCOPE = {
|
|
@@ -31,6 +45,7 @@ const APPLY_BINDING_SCOPE = {
|
|
|
31
45
|
builder.transient();
|
|
32
46
|
},
|
|
33
47
|
};
|
|
48
|
+
const NO_DEACTIVATION_PAIRS = Object.freeze([]);
|
|
34
49
|
// ── DefaultContainer ─────────────────────────────────────────────────────────────────────────────────────────────────
|
|
35
50
|
class DefaultContainer {
|
|
36
51
|
#disposed = false;
|
|
@@ -60,6 +75,7 @@ class DefaultContainer {
|
|
|
60
75
|
return (this.#inspector ??= new Inspector(this.#registry, this.#scope, this.#parent !== undefined, () => this.#disposed));
|
|
61
76
|
}
|
|
62
77
|
[RESOLUTION_DIAGNOSTICS]() {
|
|
78
|
+
const resolverCaches = this.#resolver.describeCaches();
|
|
63
79
|
const builtSubsystems = [];
|
|
64
80
|
if (this.#inspector !== undefined) {
|
|
65
81
|
builtSubsystems.push("container.inspector");
|
|
@@ -67,6 +83,12 @@ class DefaultContainer {
|
|
|
67
83
|
if (this.#moduleRefs !== undefined || this.#moduleBindingIds !== undefined) {
|
|
68
84
|
builtSubsystems.push("container.moduleTables");
|
|
69
85
|
}
|
|
86
|
+
if (this.#registry.isRecordMapBuilt) {
|
|
87
|
+
builtSubsystems.push("registry.records");
|
|
88
|
+
}
|
|
89
|
+
if (this.#registry.isIdIndexBuilt) {
|
|
90
|
+
builtSubsystems.push("registry.idIndex");
|
|
91
|
+
}
|
|
70
92
|
if (this.#registry.isTaggedIndexBuilt) {
|
|
71
93
|
builtSubsystems.push("registry.taggedIndex");
|
|
72
94
|
}
|
|
@@ -76,7 +98,8 @@ class DefaultContainer {
|
|
|
76
98
|
if (this.#lifecycle.isActivationTableBuilt) {
|
|
77
99
|
builtSubsystems.push("lifecycle.activationHooks");
|
|
78
100
|
}
|
|
79
|
-
|
|
101
|
+
builtSubsystems.push(...resolverCaches.builtSubsystems);
|
|
102
|
+
return { ...resolverCaches, scopedInstanceCount: this.#scope.scopedCount, builtSubsystems };
|
|
80
103
|
}
|
|
81
104
|
#initResolver(configuredReader) {
|
|
82
105
|
const parent = this.#parent;
|
|
@@ -86,7 +109,7 @@ class DefaultContainer {
|
|
|
86
109
|
}
|
|
87
110
|
/** What a container being constructed under this one inherits: a reader bound here, else this one's. */
|
|
88
111
|
#readerForChild() {
|
|
89
|
-
if (this.#registry.
|
|
112
|
+
if (this.#registry.has(MetadataReaderToken)) {
|
|
90
113
|
try {
|
|
91
114
|
return this.#resolver.resolve(MetadataReaderToken, undefined, []);
|
|
92
115
|
}
|
|
@@ -130,9 +153,9 @@ class DefaultContainer {
|
|
|
130
153
|
}
|
|
131
154
|
/** Remove bindings from registry + scope and collect [binding, instance] pairs for deactivation. */
|
|
132
155
|
#collectDeactivationPairs(tokenOrId) {
|
|
133
|
-
if (typeof tokenOrId === "
|
|
156
|
+
if (typeof tokenOrId === "number") {
|
|
134
157
|
const binding = this.#registry.removeById(tokenOrId);
|
|
135
|
-
return binding === undefined ?
|
|
158
|
+
return binding === undefined ? NO_DEACTIVATION_PAIRS : this.#drainSingletons([binding]);
|
|
136
159
|
}
|
|
137
160
|
// Dropping the whole token in one pass: removing each binding by id instead would re-scan and
|
|
138
161
|
// re-index the token's binding list once per binding.
|
|
@@ -140,18 +163,20 @@ class DefaultContainer {
|
|
|
140
163
|
}
|
|
141
164
|
/** Drain scope entries for already-removed bindings, and pair each one that still owes a deactivation. */
|
|
142
165
|
#drainSingletons(bindings) {
|
|
143
|
-
|
|
166
|
+
// Allocated by the first pair owed: an unbind or rebind of a binding nothing ever cached — the
|
|
167
|
+
// hot-swap shape — owes no deactivation and hands the shared empty list back.
|
|
168
|
+
let pairs;
|
|
144
169
|
for (const binding of bindings) {
|
|
145
170
|
if (binding.instance !== NO_INSTANCE) {
|
|
146
|
-
pairs.push([binding, binding.instance]);
|
|
171
|
+
(pairs ??= []).push([binding, binding.instance]);
|
|
147
172
|
this.#scope.deleteSingleton(binding);
|
|
148
173
|
}
|
|
149
174
|
else if (this.#owesConstantDeactivation(binding)) {
|
|
150
|
-
pairs.push([binding, binding.value]);
|
|
175
|
+
(pairs ??= []).push([binding, binding.value]);
|
|
151
176
|
}
|
|
152
|
-
this.#scope.deleteScoped(binding.
|
|
177
|
+
this.#scope.deleteScoped(binding.identifier);
|
|
153
178
|
}
|
|
154
|
-
return pairs;
|
|
179
|
+
return pairs ?? NO_DEACTIVATION_PAIRS;
|
|
155
180
|
}
|
|
156
181
|
/**
|
|
157
182
|
* Whether a constant still owes its deactivation.
|
|
@@ -162,7 +187,7 @@ class DefaultContainer {
|
|
|
162
187
|
*/
|
|
163
188
|
#owesConstantDeactivation(binding) {
|
|
164
189
|
return (binding.kind === "constant" &&
|
|
165
|
-
(binding.
|
|
190
|
+
(binding.deactivationHook !== undefined || this.#lifecycle.hasDeactivationHandlers(binding.token)));
|
|
166
191
|
}
|
|
167
192
|
/** Runs every pair's deactivation even when one throws, then reports what threw. */
|
|
168
193
|
#deactivatePairsSync(pairs) {
|
|
@@ -431,12 +456,18 @@ class DefaultContainer {
|
|
|
431
456
|
resolveAll(token, options) {
|
|
432
457
|
this.#assertNotDisposed();
|
|
433
458
|
const rootStack = this.#resolver.rootStack;
|
|
459
|
+
if (options === undefined && rootStack.length === 0) {
|
|
460
|
+
return this.#resolver.resolveRootCollection(token);
|
|
461
|
+
}
|
|
434
462
|
return rootStack.length === 0
|
|
435
463
|
? this.#resolver.resolveAll(token, options, rootStack)
|
|
436
464
|
: this.#resolver.resolveAll(token, options, []);
|
|
437
465
|
}
|
|
438
466
|
resolveAllAsync(token, options) {
|
|
439
467
|
this.#assertNotDisposed();
|
|
468
|
+
if (options === undefined) {
|
|
469
|
+
return this.#resolver.resolveRootCollectionAsync(token);
|
|
470
|
+
}
|
|
440
471
|
return this.#resolver.resolveAllAsync(token, options, [], ROOT_BRANCH);
|
|
441
472
|
}
|
|
442
473
|
// ── Child ──────────────────────────────────────────────────────────────────────────────────────────────────────────
|
|
@@ -509,7 +540,7 @@ class DefaultContainer {
|
|
|
509
540
|
// A constant is already its own instance, so only an activation hook gives the warm-up
|
|
510
541
|
// something to run — and a container-level hook counts as one just as a per-binding hook does.
|
|
511
542
|
if (binding.kind === "constant" &&
|
|
512
|
-
binding.
|
|
543
|
+
binding.activationHook === undefined &&
|
|
513
544
|
!this.#lifecycle.hasActivationHandlers(binding.token)) {
|
|
514
545
|
continue;
|
|
515
546
|
}
|
|
@@ -552,18 +583,19 @@ class DefaultContainer {
|
|
|
552
583
|
}
|
|
553
584
|
declaredSlotNames ??= this.#slotNamesInChain();
|
|
554
585
|
for (const requirement of requirements) {
|
|
555
|
-
if (!declaredSlotNames
|
|
556
|
-
throw new UnreachableConstraintError(tokenName(binding.token), requirement
|
|
586
|
+
if (!isSlotNameDeclared(declaredSlotNames, requirement)) {
|
|
587
|
+
throw new UnreachableConstraintError(tokenName(binding.token), requirement);
|
|
557
588
|
}
|
|
558
589
|
}
|
|
559
590
|
}
|
|
560
591
|
}
|
|
561
|
-
/** Every slot name declared anywhere a resolve through this container could reach. */
|
|
592
|
+
/** Every slot name declared anywhere a resolve through this container could reach, by declaring token. */
|
|
562
593
|
#slotNamesInChain() {
|
|
563
|
-
const names = this.#parent === undefined ? new
|
|
594
|
+
const names = this.#parent === undefined ? new Map() : this.#parent.#slotNamesInChain();
|
|
564
595
|
for (const binding of this.#registry.allBindings()) {
|
|
565
|
-
|
|
566
|
-
|
|
596
|
+
const name = binding.slot.name;
|
|
597
|
+
if (name !== undefined) {
|
|
598
|
+
getOrInsertComputed(names, tokenName(binding.token), newSlotNameSet).add(name);
|
|
567
599
|
}
|
|
568
600
|
}
|
|
569
601
|
return names;
|
|
@@ -592,11 +624,11 @@ class DefaultContainer {
|
|
|
592
624
|
#validateSingletonBindingGraph(root, reader) {
|
|
593
625
|
const rootName = tokenName(root.token);
|
|
594
626
|
const dfs = (current, pathNames, pathBindingIds) => {
|
|
595
|
-
if (pathBindingIds.has(current.
|
|
627
|
+
if (pathBindingIds.has(current.identifier)) {
|
|
596
628
|
return;
|
|
597
629
|
}
|
|
598
630
|
const extendedPathIds = new Set(pathBindingIds);
|
|
599
|
-
extendedPathIds.add(current.
|
|
631
|
+
extendedPathIds.add(current.identifier);
|
|
600
632
|
for (const edge of this.#collectStaticDependencyEdges(current, reader)) {
|
|
601
633
|
const { terminal, depTokenName } = edge;
|
|
602
634
|
const depScope = this.#validationScopeFromTerminal(terminal);
|
|
@@ -634,10 +666,10 @@ class DefaultContainer {
|
|
|
634
666
|
const seenAliasIds = new Set();
|
|
635
667
|
let current = binding;
|
|
636
668
|
while (current !== undefined && current.kind === "alias") {
|
|
637
|
-
if (seenAliasIds.has(current.
|
|
669
|
+
if (seenAliasIds.has(current.identifier)) {
|
|
638
670
|
throw new CircularDependencyError(cyclePath);
|
|
639
671
|
}
|
|
640
|
-
seenAliasIds.add(current.
|
|
672
|
+
seenAliasIds.add(current.identifier);
|
|
641
673
|
cyclePath.push(tokenName(current.token));
|
|
642
674
|
const nextToken = current.target;
|
|
643
675
|
const next = this.#resolver.peekBindingForValidate(nextToken, options);
|
|
@@ -686,7 +718,7 @@ class DefaultContainer {
|
|
|
686
718
|
// ── Introspection ──────────────────────────────────────────────────────────────────────────────────────────────────
|
|
687
719
|
has(token, options) {
|
|
688
720
|
this.#assertNotDisposed();
|
|
689
|
-
return this.#getInspector().
|
|
721
|
+
return this.#getInspector().hasOwn(token, options) || (this.#parent?.has(token, options) ?? false);
|
|
690
722
|
}
|
|
691
723
|
hasOwn(token, options) {
|
|
692
724
|
this.#assertNotDisposed();
|