@codefast/di 0.10.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +293 -0
- package/README.md +71 -7
- package/dist/ambient/active-container.d.ts +13 -11
- package/dist/ambient/active-container.js +8 -6
- package/dist/container/binding-builders.d.ts +25 -7
- package/dist/container/binding-builders.js +110 -42
- package/dist/container/container.d.ts +8 -8
- package/dist/container/container.js +244 -67
- package/dist/core/binding-declaration.d.ts +120 -0
- package/dist/core/binding-declaration.js +186 -0
- package/dist/core/binding-scope.d.ts +2 -2
- package/dist/core/binding.d.ts +61 -9
- package/dist/core/binding.js +49 -2
- package/dist/core/constraint-requirement.d.ts +1 -1
- package/dist/core/module.d.ts +10 -7
- package/dist/core/module.js +17 -3
- package/dist/core/registry.d.ts +17 -13
- package/dist/core/registry.js +155 -59
- 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/core/token.d.ts +1 -1
- package/dist/core/types.d.ts +5 -5
- package/dist/decorators/decorator-metadata.d.ts +9 -0
- package/dist/decorators/decorator-metadata.js +20 -0
- package/dist/decorators/inject.d.ts +3 -3
- package/dist/decorators/inject.js +7 -6
- package/dist/decorators/injectable.d.ts +2 -2
- package/dist/decorators/injectable.js +5 -3
- package/dist/decorators/lifecycle-decorators.js +9 -3
- package/dist/errors/errors.d.ts +85 -14
- package/dist/errors/errors.js +100 -20
- package/dist/index.d.ts +37 -35
- package/dist/index.js +20 -19
- package/dist/injection/descriptor.d.ts +3 -3
- package/dist/injection/descriptor.js +3 -7
- package/dist/injection/resolve-options.d.ts +3 -3
- package/dist/injection/resolve-options.js +7 -5
- package/dist/introspection/dependency-graph.d.ts +10 -5
- package/dist/introspection/dependency-graph.js +51 -28
- package/dist/introspection/graph-adapters/cytoscape.d.ts +1 -1
- package/dist/introspection/graph-adapters/dot.d.ts +1 -1
- package/dist/introspection/graph-adapters/mermaid.d.ts +1 -1
- package/dist/introspection/graph-adapters/reactflow.d.ts +1 -1
- package/dist/introspection/graph-adapters/reactflow.js +6 -4
- package/dist/introspection/inspector.d.ts +4 -4
- package/dist/introspection/inspector.js +9 -12
- package/dist/lifecycle/lifecycle-manager.d.ts +4 -4
- package/dist/lifecycle/lifecycle-manager.js +18 -6
- package/dist/lifecycle/scope-manager.d.ts +2 -2
- package/dist/lifecycle/scope-manager.js +31 -13
- package/dist/metadata/metadata-reader-token.d.ts +2 -2
- package/dist/metadata/metadata-reader-token.js +1 -1
- package/dist/metadata/metadata-types.d.ts +3 -3
- package/dist/metadata/symbol-metadata-reader.d.ts +3 -3
- package/dist/metadata/symbol-metadata-reader.js +1 -1
- package/dist/metadata/verifying-metadata-reader.d.ts +5 -4
- package/dist/metadata/verifying-metadata-reader.js +30 -8
- 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 +4 -5
- package/dist/resolution/cache/activation-need.js +11 -18
- package/dist/resolution/cache/binding-lookup-cache.d.ts +5 -12
- package/dist/resolution/cache/binding-lookup-cache.js +32 -19
- package/dist/resolution/cache/class-introspector.d.ts +16 -5
- package/dist/resolution/cache/class-introspector.js +73 -56
- package/dist/resolution/context.d.ts +17 -25
- package/dist/resolution/context.js +47 -56
- package/dist/resolution/path/resolution-path.d.ts +48 -13
- package/dist/resolution/path/resolution-path.js +90 -39
- package/dist/resolution/plan/instantiation-plan.d.ts +5 -5
- package/dist/resolution/plan/instantiation-plan.js +66 -26
- package/dist/resolution/plan/plan-codegen.d.ts +10 -7
- package/dist/resolution/plan/plan-codegen.js +62 -34
- package/dist/resolution/resolver.d.ts +16 -17
- package/dist/resolution/resolver.js +316 -273
- package/dist/resolution/select/binding-select.d.ts +6 -5
- package/dist/resolution/select/binding-select.js +5 -4
- package/dist/resolution/select/constraints.d.ts +3 -3
- package/dist/resolution/select/constraints.js +4 -4
- package/package.json +14 -2
|
@@ -1,23 +1,24 @@
|
|
|
1
|
-
import { BindingChain } from "
|
|
2
|
-
import { NO_INSTANCE } from "
|
|
3
|
-
import { effectiveBindingScope } from "
|
|
4
|
-
import { constraintRequirementsOf } from "
|
|
5
|
-
import { getOrInsert, getOrInsertComputed } from "
|
|
6
|
-
import { isSyncModule, MODULE_SETUP } from "
|
|
7
|
-
import { BindingRegistry } from "
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
1
|
+
import { BindingChain } from "#container/binding-builders";
|
|
2
|
+
import { NO_INSTANCE } from "#core/binding";
|
|
3
|
+
import { effectiveBindingScope } from "#core/binding-scope";
|
|
4
|
+
import { constraintRequirementsOf } from "#core/constraint-requirement";
|
|
5
|
+
import { getOrInsert, getOrInsertComputed } from "#core/map-upsert";
|
|
6
|
+
import { isSyncModule, MODULE_SETUP } from "#core/module";
|
|
7
|
+
import { BindingRegistry } from "#core/registry";
|
|
8
|
+
import { advanceDisposeEpoch, disposeEpochRef } from "#core/state-epoch";
|
|
9
|
+
import { tokenName } from "#core/token";
|
|
10
|
+
import { RESOLUTION_DIAGNOSTICS } from "#errors/diagnostics";
|
|
11
|
+
import { AsyncModuleLoadError, CircularDependencyError, DisposedContainerError, InternalError, RebindUnboundTokenError, ScopeViolationError, SyncDisposalNotSupportedError, UnreachableConstraintError, UnreachableLifecycleHookError, } from "#errors/errors";
|
|
12
|
+
import { injectionSlotToResolveOptions, bindingSlotToResolveOptions } from "#injection/resolve-options";
|
|
13
|
+
import { buildDependencyGraph } from "#introspection/dependency-graph";
|
|
14
|
+
import { Inspector } from "#introspection/inspector";
|
|
15
|
+
import { LifecycleManager } from "#lifecycle/lifecycle-manager";
|
|
16
|
+
import { ScopeManager } from "#lifecycle/scope-manager";
|
|
17
|
+
import { MetadataReaderToken } from "#metadata/metadata-reader-token";
|
|
18
|
+
import { defaultMetadataReader } from "#metadata/symbol-metadata-reader";
|
|
19
|
+
import { verifyingMetadataReader } from "#metadata/verifying-metadata-reader";
|
|
20
|
+
import { ROOT_BRANCH } from "#resolution/path/resolution-path";
|
|
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) {
|
|
23
24
|
if (requirement.tokenName !== undefined) {
|
|
@@ -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,8 +68,13 @@ 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) {
|
|
68
79
|
this.#parent = parent;
|
|
69
80
|
this.#registry = new BindingRegistry();
|
|
@@ -80,7 +91,7 @@ class DefaultContainer {
|
|
|
80
91
|
if (this.#inspector !== undefined) {
|
|
81
92
|
builtSubsystems.push("container.inspector");
|
|
82
93
|
}
|
|
83
|
-
if (this.#moduleRefs !== undefined || this.#moduleBindingIds !== undefined) {
|
|
94
|
+
if (this.#moduleRefs !== undefined || this.#moduleBindingIds !== undefined || this.#moduleImports !== undefined) {
|
|
84
95
|
builtSubsystems.push("container.moduleTables");
|
|
85
96
|
}
|
|
86
97
|
if (this.#registry.isRecordMapBuilt) {
|
|
@@ -103,13 +114,14 @@ class DefaultContainer {
|
|
|
103
114
|
}
|
|
104
115
|
#initResolver(configuredReader) {
|
|
105
116
|
const parent = this.#parent;
|
|
117
|
+
this.#hasConfiguredReader = configuredReader !== undefined || (parent !== undefined && parent.#hasConfiguredReader);
|
|
106
118
|
const metadataReader = verifyingMetadataReader(configuredReader ?? (parent === undefined ? defaultMetadataReader : parent.#readerForChild()));
|
|
107
119
|
const parentResolver = parent === undefined ? undefined : parent.#resolver;
|
|
108
120
|
this.#resolver = new DependencyResolver(this.#registry, this.#scope, this.#lifecycle, metadataReader, this, parentResolver);
|
|
109
121
|
}
|
|
110
|
-
/** What a container being constructed under this one inherits: a reader bound here, else this one's. */
|
|
122
|
+
/** What a container being constructed under this one inherits: a configured reader, else one bound here, else this one's. */
|
|
111
123
|
#readerForChild() {
|
|
112
|
-
if (this.#registry.has(MetadataReaderToken)) {
|
|
124
|
+
if (!this.#hasConfiguredReader && this.#registry.has(MetadataReaderToken)) {
|
|
113
125
|
try {
|
|
114
126
|
return this.#resolver.resolve(MetadataReaderToken, undefined, []);
|
|
115
127
|
}
|
|
@@ -124,7 +136,7 @@ class DefaultContainer {
|
|
|
124
136
|
return this.#resolver.metadataReader;
|
|
125
137
|
}
|
|
126
138
|
get isDisposed() {
|
|
127
|
-
return this.#
|
|
139
|
+
return this.#isChainDisposed();
|
|
128
140
|
}
|
|
129
141
|
// ── Binding ────────────────────────────────────────────────────────────────────────────────────────────────────────
|
|
130
142
|
bind(token) {
|
|
@@ -133,15 +145,40 @@ class DefaultContainer {
|
|
|
133
145
|
}
|
|
134
146
|
/** The registration every non-module chain shares, so `bind()` allocates only the builder. */
|
|
135
147
|
#ownRegistration() {
|
|
136
|
-
return (this.#registration ??= {
|
|
148
|
+
return (this.#registration ??= {
|
|
149
|
+
registry: this.#registry,
|
|
150
|
+
scope: this.#scope,
|
|
151
|
+
moduleBindingIds: undefined,
|
|
152
|
+
onDisplaced: (binding) => {
|
|
153
|
+
this.#recordDisplaced(binding);
|
|
154
|
+
},
|
|
155
|
+
onRestored: (binding) => {
|
|
156
|
+
this.#orphanedBindings?.delete(binding.identifier);
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
// A displaced binding holding a cached instance or owing a constant's deactivation is remembered;
|
|
161
|
+
// a later restore takes it back out, and every other displaced binding is dropped as before.
|
|
162
|
+
#recordDisplaced(binding) {
|
|
163
|
+
if (binding.instance !== NO_INSTANCE || this.#owesConstantDeactivation(binding)) {
|
|
164
|
+
(this.#orphanedBindings ??= new Map()).set(binding.identifier, binding);
|
|
165
|
+
}
|
|
137
166
|
}
|
|
138
|
-
/**
|
|
167
|
+
/**
|
|
168
|
+
* One registration per module load, holding that module's id list directly.
|
|
169
|
+
*
|
|
170
|
+
* @remarks It borrows the container's own displacement callbacks, so a constant a module's bind
|
|
171
|
+
* displaces is parked for dispose exactly as one displaced by `bind()` is.
|
|
172
|
+
*/
|
|
139
173
|
#moduleRegistration(moduleRef) {
|
|
140
174
|
this.#moduleBindingIds ??= new Map();
|
|
175
|
+
const { onDisplaced, onRestored } = this.#ownRegistration();
|
|
141
176
|
return {
|
|
142
177
|
registry: this.#registry,
|
|
143
178
|
scope: this.#scope,
|
|
144
179
|
moduleBindingIds: getOrInsert(this.#moduleBindingIds, moduleRef, []),
|
|
180
|
+
onDisplaced,
|
|
181
|
+
onRestored,
|
|
145
182
|
};
|
|
146
183
|
}
|
|
147
184
|
#createBindToBuilder(token, registration = this.#ownRegistration()) {
|
|
@@ -154,25 +191,39 @@ class DefaultContainer {
|
|
|
154
191
|
/** Remove bindings from registry + scope and collect [binding, instance] pairs for deactivation. */
|
|
155
192
|
#collectDeactivationPairs(tokenOrId) {
|
|
156
193
|
if (typeof tokenOrId === "number") {
|
|
157
|
-
const binding = this.#registry.removeById(tokenOrId);
|
|
194
|
+
const binding = this.#registry.removeById(tokenOrId) ?? this.#retireNotLive(tokenOrId);
|
|
158
195
|
return binding === undefined ? NO_DEACTIVATION_PAIRS : this.#drainSingletons([binding]);
|
|
159
196
|
}
|
|
160
197
|
// Dropping the whole token in one pass: removing each binding by id instead would re-scan and
|
|
161
198
|
// re-index the token's binding list once per binding.
|
|
162
199
|
return this.#drainSingletons(this.#registry.removeByToken(tokenOrId));
|
|
163
200
|
}
|
|
201
|
+
/**
|
|
202
|
+
* Retires an id the registry no longer holds, returning the displaced binding still owed a teardown.
|
|
203
|
+
*
|
|
204
|
+
* @remarks Counts as a registry write, so no chain holding the binding parked can restore it.
|
|
205
|
+
*/
|
|
206
|
+
#retireNotLive(id) {
|
|
207
|
+
this.#registry.touch();
|
|
208
|
+
const orphaned = this.#orphanedBindings?.get(id);
|
|
209
|
+
if (orphaned !== undefined) {
|
|
210
|
+
this.#orphanedBindings.delete(id);
|
|
211
|
+
}
|
|
212
|
+
return orphaned;
|
|
213
|
+
}
|
|
164
214
|
/** Drain scope entries for already-removed bindings, and pair each one that still owes a deactivation. */
|
|
165
215
|
#drainSingletons(bindings) {
|
|
166
|
-
//
|
|
167
|
-
// hot-swap shape — owes no deactivation and hands the shared empty list back.
|
|
216
|
+
// One flat list, allocated by the first pair owed: an unbind or rebind of a binding nothing
|
|
217
|
+
// ever cached — the hot-swap shape — owes no deactivation and hands the shared empty list back.
|
|
168
218
|
let pairs;
|
|
169
|
-
for (
|
|
219
|
+
for (let index = 0; index < bindings.length; index += 1) {
|
|
220
|
+
const binding = bindings[index];
|
|
170
221
|
if (binding.instance !== NO_INSTANCE) {
|
|
171
|
-
(pairs ??= []).push(
|
|
222
|
+
(pairs ??= []).push(binding, binding.instance);
|
|
172
223
|
this.#scope.deleteSingleton(binding);
|
|
173
224
|
}
|
|
174
225
|
else if (this.#owesConstantDeactivation(binding)) {
|
|
175
|
-
(pairs ??= []).push(
|
|
226
|
+
(pairs ??= []).push(binding, binding.value);
|
|
176
227
|
}
|
|
177
228
|
this.#scope.deleteScoped(binding.identifier);
|
|
178
229
|
}
|
|
@@ -195,32 +246,36 @@ class DefaultContainer {
|
|
|
195
246
|
return;
|
|
196
247
|
}
|
|
197
248
|
const reader = this.#getMetadataReader();
|
|
198
|
-
|
|
199
|
-
for (
|
|
249
|
+
let errors;
|
|
250
|
+
for (let index = 0; index < pairs.length; index += 2) {
|
|
200
251
|
try {
|
|
201
|
-
this.#lifecycle.runDeactivationSync(
|
|
252
|
+
this.#lifecycle.runDeactivationSync(pairs[index], pairs[index + 1], reader);
|
|
202
253
|
}
|
|
203
254
|
catch (error) {
|
|
204
|
-
errors.push(error);
|
|
255
|
+
(errors ??= []).push(error);
|
|
205
256
|
}
|
|
206
257
|
}
|
|
207
|
-
|
|
258
|
+
if (errors !== undefined) {
|
|
259
|
+
throwCollected(errors, "unbind completed, but deactivation hooks threw");
|
|
260
|
+
}
|
|
208
261
|
}
|
|
209
262
|
async #deactivatePairs(pairs) {
|
|
210
263
|
if (pairs.length === 0) {
|
|
211
264
|
return;
|
|
212
265
|
}
|
|
213
266
|
const reader = this.#getMetadataReader();
|
|
214
|
-
|
|
215
|
-
for (
|
|
267
|
+
let errors;
|
|
268
|
+
for (let index = 0; index < pairs.length; index += 2) {
|
|
216
269
|
try {
|
|
217
|
-
await this.#lifecycle.runDeactivation(
|
|
270
|
+
await this.#lifecycle.runDeactivation(pairs[index], pairs[index + 1], reader);
|
|
218
271
|
}
|
|
219
272
|
catch (error) {
|
|
220
|
-
errors.push(error);
|
|
273
|
+
(errors ??= []).push(error);
|
|
221
274
|
}
|
|
222
275
|
}
|
|
223
|
-
|
|
276
|
+
if (errors !== undefined) {
|
|
277
|
+
throwCollected(errors, "unbind completed, but deactivation hooks threw");
|
|
278
|
+
}
|
|
224
279
|
}
|
|
225
280
|
#unbindSync(tokenOrId) {
|
|
226
281
|
this.#deactivatePairsSync(this.#collectDeactivationPairs(tokenOrId));
|
|
@@ -232,19 +287,46 @@ class DefaultContainer {
|
|
|
232
287
|
unbindAll() {
|
|
233
288
|
this.#assertNotDisposed();
|
|
234
289
|
this.#deactivatePairsSync(this.#drainSingletons(this.#registry.clear()));
|
|
290
|
+
this.#resetModuleTables();
|
|
235
291
|
}
|
|
236
292
|
async unbindAllAsync() {
|
|
237
293
|
this.#assertNotDisposed();
|
|
238
294
|
await this.#deactivatePairs(this.#drainSingletons(this.#registry.clear()));
|
|
295
|
+
this.#resetModuleTables();
|
|
296
|
+
}
|
|
297
|
+
// Clearing the registry also clears module bookkeeping, so a later `load()` of the same module is
|
|
298
|
+
// not silently skipped as already-loaded. Run after deactivation so the module's hooks still fire.
|
|
299
|
+
#resetModuleTables() {
|
|
300
|
+
this.#moduleRefs = undefined;
|
|
301
|
+
this.#moduleBindingIds = undefined;
|
|
302
|
+
this.#moduleImports = undefined;
|
|
239
303
|
}
|
|
240
304
|
rebind(token) {
|
|
241
305
|
this.#assertNotDisposed();
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
this.#
|
|
247
|
-
|
|
306
|
+
// A lone default binding that owes no deactivation is replaced by the new chain's own
|
|
307
|
+
// registration, so the swap unbinds nothing up front. Anything else is unbound first, so an
|
|
308
|
+
// async deactivation throws here — before a replacement exists — exactly as `unbind()` does.
|
|
309
|
+
const lone = this.#registry.getFastDefault(token);
|
|
310
|
+
if (lone === undefined || lone.instance !== NO_INSTANCE || this.#owesConstantDeactivation(lone)) {
|
|
311
|
+
if (!this.#registry.has(token)) {
|
|
312
|
+
throw new RebindUnboundTokenError(tokenName(token));
|
|
313
|
+
}
|
|
314
|
+
this.#unbindSync(token);
|
|
315
|
+
return this.#createBindToBuilder(token);
|
|
316
|
+
}
|
|
317
|
+
return new BindingChain(token, this.#rebindRegistration());
|
|
318
|
+
}
|
|
319
|
+
#displacingRegistration;
|
|
320
|
+
/** A registration whose displaced binding is deactivated instead of parked, for `rebind`. */
|
|
321
|
+
#rebindRegistration() {
|
|
322
|
+
return (this.#displacingRegistration ??= {
|
|
323
|
+
registry: this.#registry,
|
|
324
|
+
scope: this.#scope,
|
|
325
|
+
moduleBindingIds: undefined,
|
|
326
|
+
deactivateDisplaced: (binding) => {
|
|
327
|
+
this.#deactivatePairsSync(this.#drainSingletons([binding]));
|
|
328
|
+
},
|
|
329
|
+
});
|
|
248
330
|
}
|
|
249
331
|
// ── Module ─────────────────────────────────────────────────────────────────────────────────────────────────────────
|
|
250
332
|
load(...modules) {
|
|
@@ -266,14 +348,23 @@ class DefaultContainer {
|
|
|
266
348
|
continue;
|
|
267
349
|
}
|
|
268
350
|
moduleRefs.set(moduleRef, 1);
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
351
|
+
this.#applySyncModule(module, moduleRef);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
/** Runs a sync module's setup, or files its declarations, rolling the load back if either throws. */
|
|
355
|
+
#applySyncModule(module, moduleRef) {
|
|
356
|
+
const setup = module[MODULE_SETUP];
|
|
357
|
+
try {
|
|
358
|
+
if (typeof setup === "function") {
|
|
359
|
+
setup(this.#createModuleBuilder(moduleRef));
|
|
272
360
|
}
|
|
273
|
-
|
|
274
|
-
|
|
361
|
+
else {
|
|
362
|
+
BindingChain.registerDeclared(setup, this.#moduleRegistration(moduleRef));
|
|
275
363
|
}
|
|
276
364
|
}
|
|
365
|
+
catch (error) {
|
|
366
|
+
throw this.#rollbackFailedLoadSync(moduleRef, error);
|
|
367
|
+
}
|
|
277
368
|
}
|
|
278
369
|
async loadAsync(...modules) {
|
|
279
370
|
this.#assertNotDisposed();
|
|
@@ -313,13 +404,7 @@ class DefaultContainer {
|
|
|
313
404
|
}
|
|
314
405
|
moduleRefs.set(moduleRef, 1);
|
|
315
406
|
if (isSyncModule(module)) {
|
|
316
|
-
|
|
317
|
-
try {
|
|
318
|
-
module[MODULE_SETUP](builder);
|
|
319
|
-
}
|
|
320
|
-
catch (error) {
|
|
321
|
-
throw this.#rollbackFailedLoadSync(moduleRef, error);
|
|
322
|
-
}
|
|
407
|
+
this.#applySyncModule(module, moduleRef);
|
|
323
408
|
}
|
|
324
409
|
else {
|
|
325
410
|
const importPromises = [];
|
|
@@ -344,6 +429,7 @@ class DefaultContainer {
|
|
|
344
429
|
return {
|
|
345
430
|
bind: (token) => this.#createBindToBuilder(token, registration),
|
|
346
431
|
import: (...modules) => {
|
|
432
|
+
this.#recordModuleImports(moduleRef, modules);
|
|
347
433
|
this.#loadSyncModules(modules);
|
|
348
434
|
},
|
|
349
435
|
};
|
|
@@ -353,12 +439,21 @@ class DefaultContainer {
|
|
|
353
439
|
return {
|
|
354
440
|
bind: (token) => this.#createBindToBuilder(token, registration),
|
|
355
441
|
import: (...modules) => {
|
|
442
|
+
this.#recordModuleImports(moduleRef, modules);
|
|
356
443
|
for (const module of modules) {
|
|
357
444
|
importPromises.push(this.#loadOneModuleAsync(module));
|
|
358
445
|
}
|
|
359
446
|
},
|
|
360
447
|
};
|
|
361
448
|
}
|
|
449
|
+
// Records that `moduleRef` imported each module, so unloading it later releases the reference the
|
|
450
|
+
// import took — otherwise an imported module's ref-count only ever climbs.
|
|
451
|
+
#recordModuleImports(moduleRef, modules) {
|
|
452
|
+
const list = getOrInsert((this.#moduleImports ??= new Map()), moduleRef, []);
|
|
453
|
+
for (const module of modules) {
|
|
454
|
+
list.push(module);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
362
457
|
unload(...modules) {
|
|
363
458
|
this.#assertNotDisposed();
|
|
364
459
|
for (const module of modules) {
|
|
@@ -368,11 +463,12 @@ class DefaultContainer {
|
|
|
368
463
|
/** Unregister module bindings and collect [binding, instance] pairs for deactivation. */
|
|
369
464
|
#removeModuleBindings(ref) {
|
|
370
465
|
this.#moduleRefs?.delete(ref);
|
|
466
|
+
this.#moduleImports?.delete(ref);
|
|
371
467
|
const ids = this.#moduleBindingIds?.get(ref) ?? [];
|
|
372
468
|
this.#moduleBindingIds?.delete(ref);
|
|
373
469
|
const removed = [];
|
|
374
470
|
for (const id of ids) {
|
|
375
|
-
const binding = this.#registry.removeById(id);
|
|
471
|
+
const binding = this.#registry.removeById(id) ?? this.#retireNotLive(id);
|
|
376
472
|
if (binding !== undefined) {
|
|
377
473
|
removed.push(binding);
|
|
378
474
|
}
|
|
@@ -382,7 +478,15 @@ class DefaultContainer {
|
|
|
382
478
|
#unloadModuleSync(ref) {
|
|
383
479
|
const count = this.#moduleRefs?.get(ref) ?? 0;
|
|
384
480
|
if (count <= 1) {
|
|
481
|
+
// Read the imports before removal clears them, then release each — a child only unbinds once
|
|
482
|
+
// its own ref-count reaches zero, mirroring the increment `import` took on load.
|
|
483
|
+
const imported = this.#moduleImports?.get(ref);
|
|
385
484
|
this.#deactivatePairsSync(this.#removeModuleBindings(ref));
|
|
485
|
+
if (imported !== undefined) {
|
|
486
|
+
for (const child of imported) {
|
|
487
|
+
this.#unloadModuleSync(child);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
386
490
|
}
|
|
387
491
|
else {
|
|
388
492
|
this.#moduleRefs.set(ref, count - 1);
|
|
@@ -397,7 +501,13 @@ class DefaultContainer {
|
|
|
397
501
|
async #unloadModuleAsync(ref) {
|
|
398
502
|
const count = this.#moduleRefs?.get(ref) ?? 0;
|
|
399
503
|
if (count <= 1) {
|
|
504
|
+
const imported = this.#moduleImports?.get(ref);
|
|
400
505
|
await this.#deactivatePairs(this.#removeModuleBindings(ref));
|
|
506
|
+
if (imported !== undefined) {
|
|
507
|
+
for (const child of imported) {
|
|
508
|
+
await this.#unloadModuleAsync(child);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
401
511
|
}
|
|
402
512
|
else {
|
|
403
513
|
this.#moduleRefs.set(ref, count - 1);
|
|
@@ -485,6 +595,8 @@ class DefaultContainer {
|
|
|
485
595
|
this.#disposed = true;
|
|
486
596
|
// Refuses new materializations immediately; in-flight ones are drained just below.
|
|
487
597
|
this.#scope.markClosed();
|
|
598
|
+
// Descendants read this to learn their chain closed — a resolve through a disposed ancestor is refused.
|
|
599
|
+
advanceDisposeEpoch();
|
|
488
600
|
await this.#scope.settleInflight();
|
|
489
601
|
const reader = this.#getMetadataReader();
|
|
490
602
|
const errors = [];
|
|
@@ -515,6 +627,20 @@ class DefaultContainer {
|
|
|
515
627
|
}
|
|
516
628
|
}
|
|
517
629
|
}
|
|
630
|
+
// Constants a plain last-wins bind() displaced left the registry with their hook still owed; one
|
|
631
|
+
// whose activation cached an instance was drained above, so only the untouched ones remain.
|
|
632
|
+
if (this.#orphanedBindings !== undefined) {
|
|
633
|
+
for (const binding of this.#orphanedBindings.values()) {
|
|
634
|
+
if (binding.instance === NO_INSTANCE && this.#owesConstantDeactivation(binding)) {
|
|
635
|
+
try {
|
|
636
|
+
await this.#lifecycle.runDeactivation(binding, binding.value, reader);
|
|
637
|
+
}
|
|
638
|
+
catch (error) {
|
|
639
|
+
errors.push(error);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
}
|
|
518
644
|
this.#scope.clearAll();
|
|
519
645
|
throwCollected(errors, "dispose() completed, but deactivation hooks threw");
|
|
520
646
|
}
|
|
@@ -566,6 +692,11 @@ class DefaultContainer {
|
|
|
566
692
|
if (!this.#isBoundInChain(hookToken)) {
|
|
567
693
|
throw new UnreachableLifecycleHookError(tokenName(hookToken), phase);
|
|
568
694
|
}
|
|
695
|
+
// A deactivation only ever runs for a singleton or constant, so a hook on a token whose every
|
|
696
|
+
// binding is scoped or transient can never fire — the builder blocks it, `container.onDeactivation` cannot.
|
|
697
|
+
if (phase === "onDeactivation" && !this.#hasDeactivatableBindingInChain(hookToken)) {
|
|
698
|
+
throw new UnreachableLifecycleHookError(tokenName(hookToken), phase, "no-deactivatable-binding");
|
|
699
|
+
}
|
|
569
700
|
}
|
|
570
701
|
this.#validateConstraintRequirements(allBindings);
|
|
571
702
|
}
|
|
@@ -608,6 +739,16 @@ class DefaultContainer {
|
|
|
608
739
|
const parent = this.#parent;
|
|
609
740
|
return parent !== undefined && parent.#isBoundInChain(token);
|
|
610
741
|
}
|
|
742
|
+
// Whether the token has a binding a deactivation hook can run for — a singleton or a constant.
|
|
743
|
+
#hasDeactivatableBindingInChain(token) {
|
|
744
|
+
for (const binding of this.#registry.getAll(token)) {
|
|
745
|
+
if (effectiveBindingScope(binding) === "singleton") {
|
|
746
|
+
return true;
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
const parent = this.#parent;
|
|
750
|
+
return parent !== undefined && parent.#hasDeactivatableBindingInChain(token);
|
|
751
|
+
}
|
|
611
752
|
#isSingletonStaticAnalyzableBinding(binding) {
|
|
612
753
|
if (effectiveBindingScope(binding) !== "singleton") {
|
|
613
754
|
return false;
|
|
@@ -701,10 +842,7 @@ class DefaultContainer {
|
|
|
701
842
|
#collectStaticDependencyEdges(binding, reader) {
|
|
702
843
|
const edges = [];
|
|
703
844
|
for (const dep of this.#staticDependencies(binding, reader)) {
|
|
704
|
-
// An optional dependency
|
|
705
|
-
if (dep.optional) {
|
|
706
|
-
continue;
|
|
707
|
-
}
|
|
845
|
+
// An optional dependency that is absent peeks no candidate; one that is bound is captured like any other.
|
|
708
846
|
const depOptions = injectionSlotToResolveOptions(dep);
|
|
709
847
|
for (const candidate of this.#peekDependencyCandidates(dep, depOptions)) {
|
|
710
848
|
const terminal = this.#followAliasChainToTerminal(candidate, depOptions);
|
|
@@ -718,7 +856,16 @@ class DefaultContainer {
|
|
|
718
856
|
// ── Introspection ──────────────────────────────────────────────────────────────────────────────────────────────────
|
|
719
857
|
has(token, options) {
|
|
720
858
|
this.#assertNotDisposed();
|
|
721
|
-
return this.#
|
|
859
|
+
return this.#hasInChain(token, options);
|
|
860
|
+
}
|
|
861
|
+
// Recurses through the parent's registry, not its public `has`, so a live child answers even when
|
|
862
|
+
// an ancestor is disposed — the disposed-guard belongs on the entry point, not every chain hop.
|
|
863
|
+
#hasInChain(token, options) {
|
|
864
|
+
if (this.#getInspector().hasOwn(token, options)) {
|
|
865
|
+
return true;
|
|
866
|
+
}
|
|
867
|
+
const parent = this.#parent;
|
|
868
|
+
return parent !== undefined && parent.#hasInChain(token, options);
|
|
722
869
|
}
|
|
723
870
|
hasOwn(token, options) {
|
|
724
871
|
this.#assertNotDisposed();
|
|
@@ -734,13 +881,43 @@ class DefaultContainer {
|
|
|
734
881
|
}
|
|
735
882
|
generateDependencyGraph(options) {
|
|
736
883
|
this.#assertNotDisposed();
|
|
737
|
-
|
|
884
|
+
const ancestorRegistries = [];
|
|
885
|
+
for (let ancestor = this.#parent; ancestor !== undefined; ancestor = ancestor.#parent) {
|
|
886
|
+
ancestorRegistries.push(ancestor.#registry);
|
|
887
|
+
}
|
|
888
|
+
return buildDependencyGraph(this.#registry, this.#getMetadataReader(), options, ancestorRegistries);
|
|
738
889
|
}
|
|
739
890
|
// ── Internal ───────────────────────────────────────────────────────────────────────────────────────────────────────
|
|
891
|
+
// Hot guard, one call-free epoch read and one compare for every container, root or child. Every
|
|
892
|
+
// dispose anywhere — self-dispose included, since `#runDispose` bumps the epoch — moves the epoch,
|
|
893
|
+
// so an epoch unchanged since we last confirmed the chain live means it is still live. A moved epoch
|
|
894
|
+
// drops to the cold refresh, which walks self and ancestors and either re-confirms or throws.
|
|
895
|
+
// Hot guard. A root's whole answer is its own `#disposed`, kept the one field read it always was —
|
|
896
|
+
// the epoch is never read on the root path. A child adds a call-free epoch compare: an unchanged
|
|
897
|
+
// epoch means no container anywhere has been disposed since we last confirmed the chain live, so the
|
|
898
|
+
// ancestors need no walk; a moved epoch drops to the cold refresh that walks them and re-confirms or throws.
|
|
740
899
|
#assertNotDisposed() {
|
|
741
900
|
if (this.#disposed) {
|
|
742
901
|
throw new DisposedContainerError();
|
|
743
902
|
}
|
|
903
|
+
if (this.#parent !== undefined && disposeEpochRef.value !== this.#liveGeneration) {
|
|
904
|
+
this.#refreshAncestors();
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
#refreshAncestors() {
|
|
908
|
+
if (this.#parent.#isChainDisposed()) {
|
|
909
|
+
throw new DisposedContainerError();
|
|
910
|
+
}
|
|
911
|
+
this.#liveGeneration = disposeEpochRef.value;
|
|
912
|
+
}
|
|
913
|
+
// Whether this container or any ancestor is disposed — the plain walk, taken only on the cold refresh
|
|
914
|
+
// and by the `isDisposed` getter, so it needs no memo of its own.
|
|
915
|
+
#isChainDisposed() {
|
|
916
|
+
if (this.#disposed) {
|
|
917
|
+
return true;
|
|
918
|
+
}
|
|
919
|
+
const parent = this.#parent;
|
|
920
|
+
return parent !== undefined && parent.#isChainDisposed();
|
|
744
921
|
}
|
|
745
922
|
}
|
|
746
923
|
/** Reports collected hook failures once teardown finished: the one error as itself, several aggregated. */
|