@codefast/di 0.10.1 → 0.12.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 +376 -0
- package/README.md +94 -17
- package/dist/{ambient/active-container.d.ts → ambient-container.d.ts} +7 -2
- package/dist/{ambient/active-container.js → ambient-container.js} +6 -1
- package/dist/container/{binding-builders.d.ts → binding-chain.d.ts} +27 -16
- package/dist/container/binding-chain.js +254 -0
- package/dist/container/container.d.ts +11 -2
- package/dist/container/container.js +293 -73
- package/dist/core/binding-builders.d.ts +114 -0
- package/dist/core/binding-declaration.d.ts +127 -0
- package/dist/core/binding-declaration.js +187 -0
- package/dist/core/binding.d.ts +62 -138
- package/dist/core/binding.js +54 -17
- package/dist/core/module.d.ts +8 -5
- package/dist/core/module.js +17 -3
- package/dist/core/registry.d.ts +17 -28
- package/dist/core/registry.js +92 -133
- package/dist/core/state-epoch.d.ts +18 -1
- package/dist/core/state-epoch.js +17 -0
- package/dist/core/tag.js +1 -1
- package/dist/decorators/inject.js +6 -5
- package/dist/decorators/injectable.js +4 -2
- package/dist/decorators/{lifecycle-decorators.js → lifecycle.js} +9 -3
- package/dist/decorators/metadata-record.d.ts +9 -0
- package/dist/decorators/metadata-record.js +20 -0
- package/dist/{errors/errors.d.ts → errors.d.ts} +84 -9
- package/dist/{errors/errors.js → errors.js} +101 -17
- package/dist/index.d.ts +14 -10
- package/dist/index.js +7 -6
- package/dist/injection/{resolve-options.js → dependency-slot.js} +6 -4
- package/dist/injection/descriptor.d.ts +1 -1
- package/dist/injection/descriptor.js +3 -7
- package/dist/introspection/dependency-graph.d.ts +8 -3
- package/dist/introspection/dependency-graph.js +48 -26
- package/dist/introspection/explanation.d.ts +101 -0
- package/dist/introspection/explanation.js +151 -0
- package/dist/introspection/graph-adapters/reactflow.js +6 -4
- package/dist/introspection/inspector.d.ts +9 -2
- package/dist/introspection/inspector.js +30 -28
- package/dist/lifecycle/{lifecycle-manager.d.ts → hooks.d.ts} +9 -2
- package/dist/lifecycle/{lifecycle-manager.js → hooks.js} +59 -30
- package/dist/lifecycle/{scope-manager.d.ts → scopes.d.ts} +3 -5
- package/dist/lifecycle/{scope-manager.js → scopes.js} +36 -29
- package/dist/metadata/{metadata-reader-token.d.ts → reader-token.d.ts} +1 -1
- package/dist/metadata/{symbol-metadata-reader.d.ts → symbol-reader.d.ts} +1 -1
- package/dist/metadata/{symbol-metadata-reader.js → symbol-reader.js} +1 -1
- package/dist/metadata/{metadata-types.d.ts → types.d.ts} +1 -1
- package/dist/metadata/types.js +1 -0
- package/dist/metadata/verify.d.ts +43 -0
- package/dist/metadata/verify.js +133 -0
- package/dist/metadata/{verifying-metadata-reader.d.ts → verifying-reader.d.ts} +5 -4
- package/dist/metadata/verifying-reader.js +53 -0
- package/dist/resolution/async-fan-out.d.ts +12 -0
- package/dist/resolution/async-fan-out.js +26 -0
- package/dist/resolution/cache/activation-need.d.ts +1 -2
- package/dist/resolution/cache/activation-need.js +16 -19
- package/dist/resolution/cache/class-introspector.d.ts +38 -36
- package/dist/resolution/cache/class-introspector.js +79 -159
- package/dist/resolution/cache/{binding-lookup-cache.d.ts → lookup.d.ts} +7 -9
- package/dist/resolution/cache/{binding-lookup-cache.js → lookup.js} +70 -25
- package/dist/resolution/context.d.ts +16 -24
- package/dist/resolution/context.js +47 -56
- package/dist/resolution/path.d.ts +128 -0
- package/dist/resolution/path.js +167 -0
- package/dist/resolution/plan/{plan-codegen.d.ts → codegen.d.ts} +5 -4
- package/dist/resolution/plan/{plan-codegen.js → codegen.js} +58 -32
- package/dist/resolution/plan/{instantiation-plan.d.ts → compiler.d.ts} +1 -1
- package/dist/resolution/plan/{instantiation-plan.js → compiler.js} +64 -24
- package/dist/resolution/resolver.d.ts +11 -12
- package/dist/resolution/resolver.js +573 -356
- package/dist/resolution/select/{binding-select.d.ts → candidates.d.ts} +30 -0
- package/dist/resolution/select/{binding-select.js → candidates.js} +51 -10
- package/dist/resolution/select/constraints.js +1 -1
- package/package.json +87 -67
- package/dist/container/binding-builders.js +0 -305
- package/dist/core/binding-scope.d.ts +0 -12
- package/dist/core/binding-scope.js +0 -12
- package/dist/metadata/verifying-metadata-reader.js +0 -31
- package/dist/resolution/path/resolution-path.d.ts +0 -93
- package/dist/resolution/path/resolution-path.js +0 -116
- /package/dist/{metadata/metadata-types.js → core/binding-builders.js} +0 -0
- /package/dist/decorators/{lifecycle-decorators.d.ts → lifecycle.d.ts} +0 -0
- /package/dist/injection/{resolve-options.d.ts → dependency-slot.d.ts} +0 -0
- /package/dist/{errors → introspection}/diagnostics.d.ts +0 -0
- /package/dist/{errors → introspection}/diagnostics.js +0 -0
- /package/dist/metadata/{metadata-keys.d.ts → keys.d.ts} +0 -0
- /package/dist/metadata/{metadata-keys.js → keys.js} +0 -0
- /package/dist/metadata/{metadata-reader-token.js → reader-token.js} +0 -0
|
@@ -1,22 +1,23 @@
|
|
|
1
|
-
import { BindingChain } from "#container/binding-
|
|
2
|
-
import { NO_INSTANCE } from "#core/binding";
|
|
3
|
-
import { effectiveBindingScope } from "#core/binding-scope";
|
|
1
|
+
import { BindingChain } from "#container/binding-chain";
|
|
2
|
+
import { effectiveBindingScope, NO_INSTANCE } from "#core/binding";
|
|
4
3
|
import { constraintRequirementsOf } from "#core/constraint-requirement";
|
|
5
4
|
import { getOrInsert, getOrInsertComputed } from "#core/map-upsert";
|
|
6
5
|
import { isSyncModule, MODULE_SETUP } from "#core/module";
|
|
7
6
|
import { BindingRegistry } from "#core/registry";
|
|
7
|
+
import { advanceDisposeEpoch, disposeEpochRef } from "#core/state-epoch";
|
|
8
8
|
import { tokenName } from "#core/token";
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import { injectionSlotToResolveOptions, bindingSlotToResolveOptions } from "#injection/resolve-options";
|
|
9
|
+
import { AsyncModuleLoadError, CircularDependencyError, DisposedContainerError, InternalError, RebindUnboundTokenError, ScopeViolationError, SyncDisposalNotSupportedError, UnreachableConstraintError, UnreachableLifecycleHookError, } from "#errors";
|
|
10
|
+
import { injectionSlotToResolveOptions, bindingSlotToResolveOptions } from "#injection/dependency-slot";
|
|
12
11
|
import { buildDependencyGraph } from "#introspection/dependency-graph";
|
|
12
|
+
import { RESOLUTION_DIAGNOSTICS } from "#introspection/diagnostics";
|
|
13
|
+
import { explainRequest } from "#introspection/explanation";
|
|
13
14
|
import { Inspector } from "#introspection/inspector";
|
|
14
|
-
import { LifecycleManager } from "#lifecycle/
|
|
15
|
-
import { ScopeManager } from "#lifecycle/
|
|
16
|
-
import { MetadataReaderToken } from "#metadata/
|
|
17
|
-
import { defaultMetadataReader } from "#metadata/symbol-
|
|
18
|
-
import { verifyingMetadataReader } from "#metadata/verifying-
|
|
19
|
-
import { ROOT_BRANCH } from "#resolution/path
|
|
15
|
+
import { LifecycleManager } from "#lifecycle/hooks";
|
|
16
|
+
import { ScopeManager } from "#lifecycle/scopes";
|
|
17
|
+
import { MetadataReaderToken } from "#metadata/reader-token";
|
|
18
|
+
import { defaultMetadataReader } from "#metadata/symbol-reader";
|
|
19
|
+
import { verifyingMetadataReader } from "#metadata/verifying-reader";
|
|
20
|
+
import { ROOT_BRANCH } from "#resolution/path";
|
|
20
21
|
import { DependencyResolver } from "#resolution/resolver";
|
|
21
22
|
/** Whether a requirement's name is declared — on its token when it names one, on any token otherwise. */
|
|
22
23
|
function isSlotNameDeclared(declared, requirement) {
|
|
@@ -49,12 +50,17 @@ const NO_DEACTIVATION_PAIRS = Object.freeze([]);
|
|
|
49
50
|
// ── DefaultContainer ─────────────────────────────────────────────────────────────────────────────────────────────────
|
|
50
51
|
class DefaultContainer {
|
|
51
52
|
#disposed = false;
|
|
53
|
+
// The dispose epoch at which this child's chain was last confirmed live. Every dispose anywhere
|
|
54
|
+
// bumps the epoch, so an unchanged epoch means no ancestor closed since — the chain needs no walk.
|
|
55
|
+
#liveGeneration = -1;
|
|
52
56
|
// The one teardown run — every dispose() call returns it once it exists.
|
|
53
57
|
#disposePromise;
|
|
54
58
|
#registry;
|
|
55
59
|
#scope;
|
|
56
60
|
#lifecycle;
|
|
57
61
|
#resolver;
|
|
62
|
+
// Whether the reader came from `ContainerOptions` here or on an ancestor, which no token binding may override.
|
|
63
|
+
#hasConfiguredReader = false;
|
|
58
64
|
// Built on the first introspecting call — a container that only binds and resolves never needs it.
|
|
59
65
|
#inspector;
|
|
60
66
|
#parent;
|
|
@@ -62,9 +68,16 @@ class DefaultContainer {
|
|
|
62
68
|
#moduleRefs;
|
|
63
69
|
// Module bindings: module -> array of binding IDs registered by it
|
|
64
70
|
#moduleBindingIds;
|
|
71
|
+
// Module imports: module -> the modules it imported, so unload decrements their ref-count too.
|
|
72
|
+
#moduleImports;
|
|
65
73
|
// One shared registration for every chain this container's own `bind()` creates.
|
|
66
74
|
#registration;
|
|
75
|
+
// Bindings a plain last-wins bind displaced with a teardown still owed, by id: an unbind of that id or an
|
|
76
|
+
// unload of its module drains it, and dispose runs a constant's hook, which neither the registry nor the cache holds.
|
|
77
|
+
#orphanedBindings;
|
|
67
78
|
constructor(parent, options) {
|
|
79
|
+
// oxlint-disable-next-line typescript/unbound-method -- installed as this instance's own method, so `this` is it
|
|
80
|
+
this.resolve = parent === undefined ? DefaultContainer.#rootResolve : DefaultContainer.#childResolve;
|
|
68
81
|
this.#parent = parent;
|
|
69
82
|
this.#registry = new BindingRegistry();
|
|
70
83
|
this.#scope = new ScopeManager(parent !== undefined);
|
|
@@ -80,7 +93,7 @@ class DefaultContainer {
|
|
|
80
93
|
if (this.#inspector !== undefined) {
|
|
81
94
|
builtSubsystems.push("container.inspector");
|
|
82
95
|
}
|
|
83
|
-
if (this.#moduleRefs !== undefined || this.#moduleBindingIds !== undefined) {
|
|
96
|
+
if (this.#moduleRefs !== undefined || this.#moduleBindingIds !== undefined || this.#moduleImports !== undefined) {
|
|
84
97
|
builtSubsystems.push("container.moduleTables");
|
|
85
98
|
}
|
|
86
99
|
if (this.#registry.isRecordMapBuilt) {
|
|
@@ -103,13 +116,14 @@ class DefaultContainer {
|
|
|
103
116
|
}
|
|
104
117
|
#initResolver(configuredReader) {
|
|
105
118
|
const parent = this.#parent;
|
|
119
|
+
this.#hasConfiguredReader = configuredReader !== undefined || (parent !== undefined && parent.#hasConfiguredReader);
|
|
106
120
|
const metadataReader = verifyingMetadataReader(configuredReader ?? (parent === undefined ? defaultMetadataReader : parent.#readerForChild()));
|
|
107
121
|
const parentResolver = parent === undefined ? undefined : parent.#resolver;
|
|
108
122
|
this.#resolver = new DependencyResolver(this.#registry, this.#scope, this.#lifecycle, metadataReader, this, parentResolver);
|
|
109
123
|
}
|
|
110
|
-
/** What a container being constructed under this one inherits: a reader bound here, else this one's. */
|
|
124
|
+
/** What a container being constructed under this one inherits: a configured reader, else one bound here, else this one's. */
|
|
111
125
|
#readerForChild() {
|
|
112
|
-
if (this.#registry.has(MetadataReaderToken)) {
|
|
126
|
+
if (!this.#hasConfiguredReader && this.#registry.has(MetadataReaderToken)) {
|
|
113
127
|
try {
|
|
114
128
|
return this.#resolver.resolve(MetadataReaderToken, undefined, []);
|
|
115
129
|
}
|
|
@@ -124,7 +138,7 @@ class DefaultContainer {
|
|
|
124
138
|
return this.#resolver.metadataReader;
|
|
125
139
|
}
|
|
126
140
|
get isDisposed() {
|
|
127
|
-
return this.#
|
|
141
|
+
return this.#isChainDisposed();
|
|
128
142
|
}
|
|
129
143
|
// ── Binding ────────────────────────────────────────────────────────────────────────────────────────────────────────
|
|
130
144
|
bind(token) {
|
|
@@ -133,15 +147,36 @@ class DefaultContainer {
|
|
|
133
147
|
}
|
|
134
148
|
/** The registration every non-module chain shares, so `bind()` allocates only the builder. */
|
|
135
149
|
#ownRegistration() {
|
|
136
|
-
return (this.#registration ??= {
|
|
150
|
+
return (this.#registration ??= {
|
|
151
|
+
registry: this.#registry,
|
|
152
|
+
scope: this.#scope,
|
|
153
|
+
moduleBindingIds: undefined,
|
|
154
|
+
onDisplaced: (binding) => {
|
|
155
|
+
this.#recordDisplaced(binding);
|
|
156
|
+
},
|
|
157
|
+
});
|
|
137
158
|
}
|
|
138
|
-
|
|
159
|
+
// A displaced binding holding a cached instance or owing a constant's deactivation is remembered until
|
|
160
|
+
// dispose, an unbind of its id or its module's unload; every other displaced binding is dropped.
|
|
161
|
+
#recordDisplaced(binding) {
|
|
162
|
+
if (binding.instance !== NO_INSTANCE || this.#owesConstantDeactivation(binding)) {
|
|
163
|
+
(this.#orphanedBindings ??= new Map()).set(binding.identifier, binding);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* One registration per module load, holding that module's id list directly.
|
|
168
|
+
*
|
|
169
|
+
* @remarks It borrows the container's own displacement callback, so a constant a module's bind
|
|
170
|
+
* displaces is kept for dispose exactly as one displaced by `bind()` is.
|
|
171
|
+
*/
|
|
139
172
|
#moduleRegistration(moduleRef) {
|
|
140
173
|
this.#moduleBindingIds ??= new Map();
|
|
174
|
+
const { onDisplaced } = this.#ownRegistration();
|
|
141
175
|
return {
|
|
142
176
|
registry: this.#registry,
|
|
143
177
|
scope: this.#scope,
|
|
144
178
|
moduleBindingIds: getOrInsert(this.#moduleBindingIds, moduleRef, []),
|
|
179
|
+
onDisplaced,
|
|
145
180
|
};
|
|
146
181
|
}
|
|
147
182
|
#createBindToBuilder(token, registration = this.#ownRegistration()) {
|
|
@@ -154,30 +189,43 @@ class DefaultContainer {
|
|
|
154
189
|
/** Remove bindings from registry + scope and collect [binding, instance] pairs for deactivation. */
|
|
155
190
|
#collectDeactivationPairs(tokenOrId) {
|
|
156
191
|
if (typeof tokenOrId === "number") {
|
|
157
|
-
const binding = this.#registry.removeById(tokenOrId);
|
|
192
|
+
const binding = this.#registry.removeById(tokenOrId) ?? this.#retireNotLive(tokenOrId);
|
|
158
193
|
return binding === undefined ? NO_DEACTIVATION_PAIRS : this.#drainSingletons([binding]);
|
|
159
194
|
}
|
|
160
195
|
// Dropping the whole token in one pass: removing each binding by id instead would re-scan and
|
|
161
196
|
// re-index the token's binding list once per binding.
|
|
162
197
|
return this.#drainSingletons(this.#registry.removeByToken(tokenOrId));
|
|
163
198
|
}
|
|
199
|
+
/** Retires an id the registry no longer holds, returning the displaced binding still owed a teardown. */
|
|
200
|
+
#retireNotLive(id) {
|
|
201
|
+
const orphaned = this.#orphanedBindings?.get(id);
|
|
202
|
+
if (orphaned !== undefined) {
|
|
203
|
+
this.#orphanedBindings.delete(id);
|
|
204
|
+
}
|
|
205
|
+
return orphaned;
|
|
206
|
+
}
|
|
164
207
|
/** Drain scope entries for already-removed bindings, and pair each one that still owes a deactivation. */
|
|
165
208
|
#drainSingletons(bindings) {
|
|
166
|
-
//
|
|
167
|
-
// hot-swap shape — owes no deactivation and hands the shared empty list back.
|
|
209
|
+
// One flat list, allocated by the first pair owed: an unbind or rebind of a binding nothing
|
|
210
|
+
// ever cached — the hot-swap shape — owes no deactivation and hands the shared empty list back.
|
|
168
211
|
let pairs;
|
|
169
|
-
for (
|
|
170
|
-
|
|
171
|
-
(pairs ??= []).push([binding, binding.instance]);
|
|
172
|
-
this.#scope.deleteSingleton(binding);
|
|
173
|
-
}
|
|
174
|
-
else if (this.#owesConstantDeactivation(binding)) {
|
|
175
|
-
(pairs ??= []).push([binding, binding.value]);
|
|
176
|
-
}
|
|
177
|
-
this.#scope.deleteScoped(binding.identifier);
|
|
212
|
+
for (let index = 0; index < bindings.length; index += 1) {
|
|
213
|
+
pairs = this.#drainSingleton(bindings[index], pairs);
|
|
178
214
|
}
|
|
179
215
|
return pairs ?? NO_DEACTIVATION_PAIRS;
|
|
180
216
|
}
|
|
217
|
+
/** One binding's share of {@link DefaultContainer.#drainSingletons}, allocating `pairs` on the first pair owed. */
|
|
218
|
+
#drainSingleton(binding, pairs) {
|
|
219
|
+
if (binding.instance !== NO_INSTANCE) {
|
|
220
|
+
(pairs ??= []).push(binding, binding.instance);
|
|
221
|
+
this.#scope.deleteSingleton(binding);
|
|
222
|
+
}
|
|
223
|
+
else if (this.#owesConstantDeactivation(binding)) {
|
|
224
|
+
(pairs ??= []).push(binding, binding.value);
|
|
225
|
+
}
|
|
226
|
+
this.#scope.deleteScoped(binding.scopedCacheKey);
|
|
227
|
+
return pairs;
|
|
228
|
+
}
|
|
181
229
|
/**
|
|
182
230
|
* Whether a constant still owes its deactivation.
|
|
183
231
|
*
|
|
@@ -195,32 +243,38 @@ class DefaultContainer {
|
|
|
195
243
|
return;
|
|
196
244
|
}
|
|
197
245
|
const reader = this.#getMetadataReader();
|
|
198
|
-
const
|
|
199
|
-
|
|
246
|
+
const lifecycle = this.#lifecycle;
|
|
247
|
+
let errors;
|
|
248
|
+
for (let index = 0; index < pairs.length; index += 2) {
|
|
200
249
|
try {
|
|
201
|
-
|
|
250
|
+
lifecycle.runDeactivationSync(pairs[index], pairs[index + 1], reader);
|
|
202
251
|
}
|
|
203
252
|
catch (error) {
|
|
204
|
-
errors.push(error);
|
|
253
|
+
(errors ??= []).push(error);
|
|
205
254
|
}
|
|
206
255
|
}
|
|
207
|
-
|
|
256
|
+
if (errors !== undefined) {
|
|
257
|
+
throwCollected(errors, "unbind completed, but deactivation hooks threw");
|
|
258
|
+
}
|
|
208
259
|
}
|
|
209
260
|
async #deactivatePairs(pairs) {
|
|
210
261
|
if (pairs.length === 0) {
|
|
211
262
|
return;
|
|
212
263
|
}
|
|
213
264
|
const reader = this.#getMetadataReader();
|
|
214
|
-
const
|
|
215
|
-
|
|
265
|
+
const lifecycle = this.#lifecycle;
|
|
266
|
+
let errors;
|
|
267
|
+
for (let index = 0; index < pairs.length; index += 2) {
|
|
216
268
|
try {
|
|
217
|
-
await
|
|
269
|
+
await lifecycle.runDeactivation(pairs[index], pairs[index + 1], reader);
|
|
218
270
|
}
|
|
219
271
|
catch (error) {
|
|
220
|
-
errors.push(error);
|
|
272
|
+
(errors ??= []).push(error);
|
|
221
273
|
}
|
|
222
274
|
}
|
|
223
|
-
|
|
275
|
+
if (errors !== undefined) {
|
|
276
|
+
throwCollected(errors, "unbind completed, but deactivation hooks threw");
|
|
277
|
+
}
|
|
224
278
|
}
|
|
225
279
|
#unbindSync(tokenOrId) {
|
|
226
280
|
this.#deactivatePairsSync(this.#collectDeactivationPairs(tokenOrId));
|
|
@@ -232,19 +286,50 @@ class DefaultContainer {
|
|
|
232
286
|
unbindAll() {
|
|
233
287
|
this.#assertNotDisposed();
|
|
234
288
|
this.#deactivatePairsSync(this.#drainSingletons(this.#registry.clear()));
|
|
289
|
+
this.#resetModuleTables();
|
|
235
290
|
}
|
|
236
291
|
async unbindAllAsync() {
|
|
237
292
|
this.#assertNotDisposed();
|
|
238
293
|
await this.#deactivatePairs(this.#drainSingletons(this.#registry.clear()));
|
|
294
|
+
this.#resetModuleTables();
|
|
295
|
+
}
|
|
296
|
+
// Clearing the registry also clears module bookkeeping, so a later `load()` of the same module is
|
|
297
|
+
// not silently skipped as already-loaded. Run after deactivation so the module's hooks still fire.
|
|
298
|
+
#resetModuleTables() {
|
|
299
|
+
this.#moduleRefs = undefined;
|
|
300
|
+
this.#moduleBindingIds = undefined;
|
|
301
|
+
this.#moduleImports = undefined;
|
|
239
302
|
}
|
|
240
303
|
rebind(token) {
|
|
241
304
|
this.#assertNotDisposed();
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
this.#
|
|
247
|
-
|
|
305
|
+
// A lone default binding that owes no deactivation is replaced by the new chain's own
|
|
306
|
+
// registration, so the swap unbinds nothing up front. Anything else is unbound first, so an
|
|
307
|
+
// async deactivation throws here — before a replacement exists — exactly as `unbind()` does.
|
|
308
|
+
const lone = this.#registry.getFastDefault(token);
|
|
309
|
+
if (lone === undefined || lone.instance !== NO_INSTANCE || this.#owesConstantDeactivation(lone)) {
|
|
310
|
+
if (!this.#registry.has(token)) {
|
|
311
|
+
throw new RebindUnboundTokenError(tokenName(token));
|
|
312
|
+
}
|
|
313
|
+
this.#unbindSync(token);
|
|
314
|
+
return this.#createBindToBuilder(token);
|
|
315
|
+
}
|
|
316
|
+
return new BindingChain(token, this.#rebindRegistration());
|
|
317
|
+
}
|
|
318
|
+
#displacingRegistration;
|
|
319
|
+
/** A registration whose displaced binding is deactivated on the spot instead of kept for dispose, for `rebind`. */
|
|
320
|
+
#rebindRegistration() {
|
|
321
|
+
return (this.#displacingRegistration ??= {
|
|
322
|
+
registry: this.#registry,
|
|
323
|
+
scope: this.#scope,
|
|
324
|
+
moduleBindingIds: undefined,
|
|
325
|
+
// One binding at a time, so the hot-swap rebind that owes nothing allocates nothing.
|
|
326
|
+
deactivateDisplaced: (binding) => {
|
|
327
|
+
const pairs = this.#drainSingleton(binding, undefined);
|
|
328
|
+
if (pairs !== undefined) {
|
|
329
|
+
this.#deactivatePairsSync(pairs);
|
|
330
|
+
}
|
|
331
|
+
},
|
|
332
|
+
});
|
|
248
333
|
}
|
|
249
334
|
// ── Module ─────────────────────────────────────────────────────────────────────────────────────────────────────────
|
|
250
335
|
load(...modules) {
|
|
@@ -266,14 +351,23 @@ class DefaultContainer {
|
|
|
266
351
|
continue;
|
|
267
352
|
}
|
|
268
353
|
moduleRefs.set(moduleRef, 1);
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
354
|
+
this.#applySyncModule(module, moduleRef);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
/** Runs a sync module's setup, or files its declarations, rolling the load back if either throws. */
|
|
358
|
+
#applySyncModule(module, moduleRef) {
|
|
359
|
+
const setup = module[MODULE_SETUP];
|
|
360
|
+
try {
|
|
361
|
+
if (typeof setup === "function") {
|
|
362
|
+
setup(this.#createModuleBuilder(moduleRef));
|
|
272
363
|
}
|
|
273
|
-
|
|
274
|
-
|
|
364
|
+
else {
|
|
365
|
+
BindingChain.registerDeclared(setup, this.#moduleRegistration(moduleRef));
|
|
275
366
|
}
|
|
276
367
|
}
|
|
368
|
+
catch (error) {
|
|
369
|
+
throw this.#rollbackFailedLoadSync(moduleRef, error);
|
|
370
|
+
}
|
|
277
371
|
}
|
|
278
372
|
async loadAsync(...modules) {
|
|
279
373
|
this.#assertNotDisposed();
|
|
@@ -313,13 +407,7 @@ class DefaultContainer {
|
|
|
313
407
|
}
|
|
314
408
|
moduleRefs.set(moduleRef, 1);
|
|
315
409
|
if (isSyncModule(module)) {
|
|
316
|
-
|
|
317
|
-
try {
|
|
318
|
-
module[MODULE_SETUP](builder);
|
|
319
|
-
}
|
|
320
|
-
catch (error) {
|
|
321
|
-
throw this.#rollbackFailedLoadSync(moduleRef, error);
|
|
322
|
-
}
|
|
410
|
+
this.#applySyncModule(module, moduleRef);
|
|
323
411
|
}
|
|
324
412
|
else {
|
|
325
413
|
const importPromises = [];
|
|
@@ -344,6 +432,7 @@ class DefaultContainer {
|
|
|
344
432
|
return {
|
|
345
433
|
bind: (token) => this.#createBindToBuilder(token, registration),
|
|
346
434
|
import: (...modules) => {
|
|
435
|
+
this.#recordModuleImports(moduleRef, modules);
|
|
347
436
|
this.#loadSyncModules(modules);
|
|
348
437
|
},
|
|
349
438
|
};
|
|
@@ -353,12 +442,21 @@ class DefaultContainer {
|
|
|
353
442
|
return {
|
|
354
443
|
bind: (token) => this.#createBindToBuilder(token, registration),
|
|
355
444
|
import: (...modules) => {
|
|
445
|
+
this.#recordModuleImports(moduleRef, modules);
|
|
356
446
|
for (const module of modules) {
|
|
357
447
|
importPromises.push(this.#loadOneModuleAsync(module));
|
|
358
448
|
}
|
|
359
449
|
},
|
|
360
450
|
};
|
|
361
451
|
}
|
|
452
|
+
// Records that `moduleRef` imported each module, so unloading it later releases the reference the
|
|
453
|
+
// import took — otherwise an imported module's ref-count only ever climbs.
|
|
454
|
+
#recordModuleImports(moduleRef, modules) {
|
|
455
|
+
const list = getOrInsert((this.#moduleImports ??= new Map()), moduleRef, []);
|
|
456
|
+
for (const module of modules) {
|
|
457
|
+
list.push(module);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
362
460
|
unload(...modules) {
|
|
363
461
|
this.#assertNotDisposed();
|
|
364
462
|
for (const module of modules) {
|
|
@@ -368,11 +466,12 @@ class DefaultContainer {
|
|
|
368
466
|
/** Unregister module bindings and collect [binding, instance] pairs for deactivation. */
|
|
369
467
|
#removeModuleBindings(ref) {
|
|
370
468
|
this.#moduleRefs?.delete(ref);
|
|
469
|
+
this.#moduleImports?.delete(ref);
|
|
371
470
|
const ids = this.#moduleBindingIds?.get(ref) ?? [];
|
|
372
471
|
this.#moduleBindingIds?.delete(ref);
|
|
373
472
|
const removed = [];
|
|
374
473
|
for (const id of ids) {
|
|
375
|
-
const binding = this.#registry.removeById(id);
|
|
474
|
+
const binding = this.#registry.removeById(id) ?? this.#retireNotLive(id);
|
|
376
475
|
if (binding !== undefined) {
|
|
377
476
|
removed.push(binding);
|
|
378
477
|
}
|
|
@@ -382,7 +481,15 @@ class DefaultContainer {
|
|
|
382
481
|
#unloadModuleSync(ref) {
|
|
383
482
|
const count = this.#moduleRefs?.get(ref) ?? 0;
|
|
384
483
|
if (count <= 1) {
|
|
484
|
+
// Read the imports before removal clears them, then release each — a child only unbinds once
|
|
485
|
+
// its own ref-count reaches zero, mirroring the increment `import` took on load.
|
|
486
|
+
const imported = this.#moduleImports?.get(ref);
|
|
385
487
|
this.#deactivatePairsSync(this.#removeModuleBindings(ref));
|
|
488
|
+
if (imported !== undefined) {
|
|
489
|
+
for (const child of imported) {
|
|
490
|
+
this.#unloadModuleSync(child);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
386
493
|
}
|
|
387
494
|
else {
|
|
388
495
|
this.#moduleRefs.set(ref, count - 1);
|
|
@@ -397,7 +504,13 @@ class DefaultContainer {
|
|
|
397
504
|
async #unloadModuleAsync(ref) {
|
|
398
505
|
const count = this.#moduleRefs?.get(ref) ?? 0;
|
|
399
506
|
if (count <= 1) {
|
|
507
|
+
const imported = this.#moduleImports?.get(ref);
|
|
400
508
|
await this.#deactivatePairs(this.#removeModuleBindings(ref));
|
|
509
|
+
if (imported !== undefined) {
|
|
510
|
+
for (const child of imported) {
|
|
511
|
+
await this.#unloadModuleAsync(child);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
401
514
|
}
|
|
402
515
|
else {
|
|
403
516
|
this.#moduleRefs.set(ref, count - 1);
|
|
@@ -420,9 +533,10 @@ class DefaultContainer {
|
|
|
420
533
|
this.#assertNotDisposed();
|
|
421
534
|
this.#lifecycle.registerDeactivation(token, handler);
|
|
422
535
|
}
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
536
|
+
static #rootResolve(token, options) {
|
|
537
|
+
if (this.#disposed) {
|
|
538
|
+
throw new DisposedContainerError();
|
|
539
|
+
}
|
|
426
540
|
const rootStack = this.#resolver.rootStack;
|
|
427
541
|
// A resolve already holding the shared stack means this one is nested; it mints its own.
|
|
428
542
|
if (rootStack.length !== 0) {
|
|
@@ -435,8 +549,28 @@ class DefaultContainer {
|
|
|
435
549
|
}
|
|
436
550
|
return this.#resolver.resolve(token, options, rootStack);
|
|
437
551
|
}
|
|
552
|
+
// The root's body again behind the child's check, so neither role's compiled resolve carries the other's path.
|
|
553
|
+
static #childResolve(token, options) {
|
|
554
|
+
this.#assertChainLive();
|
|
555
|
+
const rootStack = this.#resolver.rootStack;
|
|
556
|
+
if (rootStack.length !== 0) {
|
|
557
|
+
return options === undefined
|
|
558
|
+
? this.#resolver.resolveFromContext(token, [])
|
|
559
|
+
: this.#resolver.resolve(token, options, []);
|
|
560
|
+
}
|
|
561
|
+
if (options === undefined) {
|
|
562
|
+
return this.#resolver.resolveFromContext(token, rootStack);
|
|
563
|
+
}
|
|
564
|
+
return this.#resolver.resolve(token, options, rootStack);
|
|
565
|
+
}
|
|
438
566
|
resolveAsync(token, options) {
|
|
439
|
-
this
|
|
567
|
+
// Not `async`: the guard's throw becomes the rejection this method promises.
|
|
568
|
+
try {
|
|
569
|
+
this.#assertNotDisposed();
|
|
570
|
+
}
|
|
571
|
+
catch (disposedError) {
|
|
572
|
+
return Promise.reject(disposedError);
|
|
573
|
+
}
|
|
440
574
|
if (options === undefined) {
|
|
441
575
|
return this.#resolver.resolveAsyncFromRoot(token);
|
|
442
576
|
}
|
|
@@ -450,7 +584,12 @@ class DefaultContainer {
|
|
|
450
584
|
: this.#resolver.resolveOptional(token, options, []);
|
|
451
585
|
}
|
|
452
586
|
resolveOptionalAsync(token, options) {
|
|
453
|
-
|
|
587
|
+
try {
|
|
588
|
+
this.#assertNotDisposed();
|
|
589
|
+
}
|
|
590
|
+
catch (disposedError) {
|
|
591
|
+
return Promise.reject(disposedError);
|
|
592
|
+
}
|
|
454
593
|
return this.#resolver.resolveOptionalAsync(token, options, [], ROOT_BRANCH);
|
|
455
594
|
}
|
|
456
595
|
resolveAll(token, options) {
|
|
@@ -464,7 +603,12 @@ class DefaultContainer {
|
|
|
464
603
|
: this.#resolver.resolveAll(token, options, []);
|
|
465
604
|
}
|
|
466
605
|
resolveAllAsync(token, options) {
|
|
467
|
-
|
|
606
|
+
try {
|
|
607
|
+
this.#assertNotDisposed();
|
|
608
|
+
}
|
|
609
|
+
catch (disposedError) {
|
|
610
|
+
return Promise.reject(disposedError);
|
|
611
|
+
}
|
|
468
612
|
if (options === undefined) {
|
|
469
613
|
return this.#resolver.resolveRootCollectionAsync(token);
|
|
470
614
|
}
|
|
@@ -473,7 +617,10 @@ class DefaultContainer {
|
|
|
473
617
|
// ── Child ──────────────────────────────────────────────────────────────────────────────────────────────────────────
|
|
474
618
|
createChild() {
|
|
475
619
|
this.#assertNotDisposed();
|
|
476
|
-
|
|
620
|
+
const child = new DefaultContainer(this);
|
|
621
|
+
// Born confirmed live: the parent's chain was checked just above, so the child's first read skips the walk.
|
|
622
|
+
child.#liveGeneration = disposeEpochRef.value;
|
|
623
|
+
return child;
|
|
477
624
|
}
|
|
478
625
|
// ── Dispose ────────────────────────────────────────────────────────────────────────────────────────────────────────
|
|
479
626
|
dispose() {
|
|
@@ -485,6 +632,8 @@ class DefaultContainer {
|
|
|
485
632
|
this.#disposed = true;
|
|
486
633
|
// Refuses new materializations immediately; in-flight ones are drained just below.
|
|
487
634
|
this.#scope.markClosed();
|
|
635
|
+
// Descendants read this to learn their chain closed — a resolve through a disposed ancestor is refused.
|
|
636
|
+
advanceDisposeEpoch();
|
|
488
637
|
await this.#scope.settleInflight();
|
|
489
638
|
const reader = this.#getMetadataReader();
|
|
490
639
|
const errors = [];
|
|
@@ -515,6 +664,20 @@ class DefaultContainer {
|
|
|
515
664
|
}
|
|
516
665
|
}
|
|
517
666
|
}
|
|
667
|
+
// Constants a plain last-wins bind() displaced left the registry with their hook still owed; one
|
|
668
|
+
// whose activation cached an instance was drained above, so only the untouched ones remain.
|
|
669
|
+
if (this.#orphanedBindings !== undefined) {
|
|
670
|
+
for (const binding of this.#orphanedBindings.values()) {
|
|
671
|
+
if (binding.instance === NO_INSTANCE && this.#owesConstantDeactivation(binding)) {
|
|
672
|
+
try {
|
|
673
|
+
await this.#lifecycle.runDeactivation(binding, binding.value, reader);
|
|
674
|
+
}
|
|
675
|
+
catch (error) {
|
|
676
|
+
errors.push(error);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
}
|
|
518
681
|
this.#scope.clearAll();
|
|
519
682
|
throwCollected(errors, "dispose() completed, but deactivation hooks threw");
|
|
520
683
|
}
|
|
@@ -566,6 +729,11 @@ class DefaultContainer {
|
|
|
566
729
|
if (!this.#isBoundInChain(hookToken)) {
|
|
567
730
|
throw new UnreachableLifecycleHookError(tokenName(hookToken), phase);
|
|
568
731
|
}
|
|
732
|
+
// A deactivation only ever runs for a singleton or constant, so a hook on a token whose every
|
|
733
|
+
// binding is scoped or transient can never fire — the builder blocks it, `container.onDeactivation` cannot.
|
|
734
|
+
if (phase === "onDeactivation" && !this.#hasDeactivatableBindingInChain(hookToken)) {
|
|
735
|
+
throw new UnreachableLifecycleHookError(tokenName(hookToken), phase, "no-deactivatable-binding");
|
|
736
|
+
}
|
|
569
737
|
}
|
|
570
738
|
this.#validateConstraintRequirements(allBindings);
|
|
571
739
|
}
|
|
@@ -608,6 +776,16 @@ class DefaultContainer {
|
|
|
608
776
|
const parent = this.#parent;
|
|
609
777
|
return parent !== undefined && parent.#isBoundInChain(token);
|
|
610
778
|
}
|
|
779
|
+
// Whether the token has a binding a deactivation hook can run for — a singleton or a constant.
|
|
780
|
+
#hasDeactivatableBindingInChain(token) {
|
|
781
|
+
for (const binding of this.#registry.getAll(token)) {
|
|
782
|
+
if (effectiveBindingScope(binding) === "singleton") {
|
|
783
|
+
return true;
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
const parent = this.#parent;
|
|
787
|
+
return parent !== undefined && parent.#hasDeactivatableBindingInChain(token);
|
|
788
|
+
}
|
|
611
789
|
#isSingletonStaticAnalyzableBinding(binding) {
|
|
612
790
|
if (effectiveBindingScope(binding) !== "singleton") {
|
|
613
791
|
return false;
|
|
@@ -701,10 +879,7 @@ class DefaultContainer {
|
|
|
701
879
|
#collectStaticDependencyEdges(binding, reader) {
|
|
702
880
|
const edges = [];
|
|
703
881
|
for (const dep of this.#staticDependencies(binding, reader)) {
|
|
704
|
-
// An optional dependency
|
|
705
|
-
if (dep.optional) {
|
|
706
|
-
continue;
|
|
707
|
-
}
|
|
882
|
+
// An optional dependency that is absent peeks no candidate; one that is bound is captured like any other.
|
|
708
883
|
const depOptions = injectionSlotToResolveOptions(dep);
|
|
709
884
|
for (const candidate of this.#peekDependencyCandidates(dep, depOptions)) {
|
|
710
885
|
const terminal = this.#followAliasChainToTerminal(candidate, depOptions);
|
|
@@ -718,7 +893,16 @@ class DefaultContainer {
|
|
|
718
893
|
// ── Introspection ──────────────────────────────────────────────────────────────────────────────────────────────────
|
|
719
894
|
has(token, options) {
|
|
720
895
|
this.#assertNotDisposed();
|
|
721
|
-
return this.#
|
|
896
|
+
return this.#hasInChain(token, options);
|
|
897
|
+
}
|
|
898
|
+
// Recurses through the parent's registry, not its public `has`, so a live child answers even when
|
|
899
|
+
// an ancestor is disposed — the disposed-guard belongs on the entry point, not every chain hop.
|
|
900
|
+
#hasInChain(token, options) {
|
|
901
|
+
if (this.#getInspector().hasOwn(token, options)) {
|
|
902
|
+
return true;
|
|
903
|
+
}
|
|
904
|
+
const parent = this.#parent;
|
|
905
|
+
return parent !== undefined && parent.#hasInChain(token, options);
|
|
722
906
|
}
|
|
723
907
|
hasOwn(token, options) {
|
|
724
908
|
this.#assertNotDisposed();
|
|
@@ -732,15 +916,51 @@ class DefaultContainer {
|
|
|
732
916
|
this.#assertNotDisposed();
|
|
733
917
|
return this.#getInspector().inspect();
|
|
734
918
|
}
|
|
919
|
+
explain(token, options) {
|
|
920
|
+
this.#assertNotDisposed();
|
|
921
|
+
return explainRequest([this.#registry, ...this.#ancestorRegistries()], token, options);
|
|
922
|
+
}
|
|
735
923
|
generateDependencyGraph(options) {
|
|
736
924
|
this.#assertNotDisposed();
|
|
737
|
-
return buildDependencyGraph(this.#registry, this.#getMetadataReader(), options, this.#
|
|
925
|
+
return buildDependencyGraph(this.#registry, this.#getMetadataReader(), options, this.#ancestorRegistries());
|
|
926
|
+
}
|
|
927
|
+
/** The registries above this container, nearest first — the order every chain walk reads them in. */
|
|
928
|
+
#ancestorRegistries() {
|
|
929
|
+
const registries = [];
|
|
930
|
+
for (let ancestor = this.#parent; ancestor !== undefined; ancestor = ancestor.#parent) {
|
|
931
|
+
registries.push(ancestor.#registry);
|
|
932
|
+
}
|
|
933
|
+
return registries;
|
|
738
934
|
}
|
|
739
935
|
// ── Internal ───────────────────────────────────────────────────────────────────────────────────────────────────────
|
|
936
|
+
// Two field reads and no epoch on a live root. This and `#assertChainLive` each stay under V8's small-function
|
|
937
|
+
// size, so every entry point inlines both outright; the walk and the throw sit in `#confirmChainLive`.
|
|
740
938
|
#assertNotDisposed() {
|
|
741
|
-
if (this.#disposed) {
|
|
939
|
+
if (this.#disposed || this.#parent !== undefined) {
|
|
940
|
+
this.#assertChainLive();
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
// An unchanged dispose epoch means no container anywhere closed since this chain was last confirmed live;
|
|
944
|
+
// every dispose advances it, this container's own included, so a disposed container always reaches the walk.
|
|
945
|
+
#assertChainLive() {
|
|
946
|
+
if (disposeEpochRef.value !== this.#liveGeneration) {
|
|
947
|
+
this.#confirmChainLive();
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
#confirmChainLive() {
|
|
951
|
+
if (this.#isChainDisposed()) {
|
|
742
952
|
throw new DisposedContainerError();
|
|
743
953
|
}
|
|
954
|
+
this.#liveGeneration = disposeEpochRef.value;
|
|
955
|
+
}
|
|
956
|
+
// Whether this container or any ancestor is disposed — the plain walk, taken only on the cold refresh
|
|
957
|
+
// and by the `isDisposed` getter, so it needs no memo of its own.
|
|
958
|
+
#isChainDisposed() {
|
|
959
|
+
if (this.#disposed) {
|
|
960
|
+
return true;
|
|
961
|
+
}
|
|
962
|
+
const parent = this.#parent;
|
|
963
|
+
return parent !== undefined && parent.#isChainDisposed();
|
|
744
964
|
}
|
|
745
965
|
}
|
|
746
966
|
/** Reports collected hook failures once teardown finished: the one error as itself, several aggregated. */
|