@codefast/di 0.5.0-canary.6 → 0.5.0-canary.7
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 +55 -0
- package/README.md +13 -4
- package/dist/binding.d.ts +36 -19
- package/dist/binding.d.ts.map +1 -1
- package/dist/binding.js +2 -2
- package/dist/binding.js.map +1 -1
- package/dist/container/binding-builders.d.ts +35 -0
- package/dist/container/binding-builders.d.ts.map +1 -0
- package/dist/container/binding-builders.js +247 -0
- package/dist/container/binding-builders.js.map +1 -0
- package/dist/{container.d.ts → container/container.d.ts} +2 -2
- package/dist/container/container.d.ts.map +1 -0
- package/dist/container/container.js +641 -0
- package/dist/container/container.js.map +1 -0
- package/dist/decorators/inject.d.ts +12 -1
- package/dist/decorators/inject.d.ts.map +1 -1
- package/dist/decorators/inject.js +2 -2
- package/dist/decorators/inject.js.map +1 -1
- package/dist/decorators/injectable.js +3 -3
- package/dist/decorators/injectable.js.map +1 -1
- package/dist/index.d.ts +12 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -4
- package/dist/index.js.map +1 -1
- package/dist/introspection/dependency-graph.d.ts.map +1 -0
- package/dist/{dependency-graph.js → introspection/dependency-graph.js} +1 -1
- package/dist/introspection/dependency-graph.js.map +1 -0
- package/dist/{graph-adapters → introspection/graph-adapters}/cytoscape.d.ts +1 -1
- package/dist/introspection/graph-adapters/cytoscape.d.ts.map +1 -0
- package/dist/introspection/graph-adapters/cytoscape.js.map +1 -0
- package/dist/introspection/graph-adapters/dot.d.ts +6 -0
- package/dist/introspection/graph-adapters/dot.d.ts.map +1 -0
- package/dist/introspection/graph-adapters/dot.js.map +1 -0
- package/dist/{graph-adapters → introspection/graph-adapters}/reactflow.d.ts +2 -2
- package/dist/introspection/graph-adapters/reactflow.d.ts.map +1 -0
- package/dist/{graph-adapters → introspection/graph-adapters}/reactflow.js +11 -1
- package/dist/introspection/graph-adapters/reactflow.js.map +1 -0
- package/dist/{inspector.d.ts → introspection/inspector.d.ts} +3 -8
- package/dist/introspection/inspector.d.ts.map +1 -0
- package/dist/{inspector.js → introspection/inspector.js} +23 -23
- package/dist/introspection/inspector.js.map +1 -0
- package/dist/metadata/symbol-metadata-reader.d.ts +1 -1
- package/dist/metadata/symbol-metadata-reader.d.ts.map +1 -1
- package/dist/metadata/symbol-metadata-reader.js +4 -4
- package/dist/metadata/symbol-metadata-reader.js.map +1 -1
- package/dist/module.d.ts +9 -2
- package/dist/module.d.ts.map +1 -1
- package/dist/module.js +9 -2
- package/dist/module.js.map +1 -1
- package/dist/registry.d.ts +5 -13
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +102 -61
- package/dist/registry.js.map +1 -1
- package/dist/resolution/binding-scope.d.ts.map +1 -0
- package/dist/resolution/binding-scope.js.map +1 -0
- package/dist/resolution/binding-select.d.ts.map +1 -0
- package/dist/{binding-select.js → resolution/binding-select.js} +16 -3
- package/dist/resolution/binding-select.js.map +1 -0
- package/dist/resolution/constraints.d.ts.map +1 -0
- package/dist/resolution/constraints.js.map +1 -0
- package/dist/{environment.d.ts → resolution/environment.d.ts} +2 -6
- package/dist/resolution/environment.d.ts.map +1 -0
- package/dist/resolution/environment.js +100 -0
- package/dist/resolution/environment.js.map +1 -0
- package/dist/resolution/instantiation-plan.d.ts +67 -0
- package/dist/resolution/instantiation-plan.d.ts.map +1 -0
- package/dist/resolution/instantiation-plan.js +163 -0
- package/dist/resolution/instantiation-plan.js.map +1 -0
- package/dist/{lifecycle.d.ts → resolution/lifecycle.d.ts} +3 -3
- package/dist/resolution/lifecycle.d.ts.map +1 -0
- package/dist/{lifecycle.js → resolution/lifecycle.js} +17 -13
- package/dist/resolution/lifecycle.js.map +1 -0
- package/dist/resolution/resolution-path.d.ts +31 -0
- package/dist/resolution/resolution-path.d.ts.map +1 -0
- package/dist/resolution/resolution-path.js +53 -0
- package/dist/resolution/resolution-path.js.map +1 -0
- package/dist/resolution/resolve-options.d.ts.map +1 -0
- package/dist/resolution/resolve-options.js.map +1 -0
- package/dist/resolution/resolver.d.ts +35 -0
- package/dist/resolution/resolver.d.ts.map +1 -0
- package/dist/resolution/resolver.js +1320 -0
- package/dist/resolution/resolver.js.map +1 -0
- package/dist/{scope.d.ts → resolution/scope.d.ts} +10 -3
- package/dist/resolution/scope.d.ts.map +1 -0
- package/dist/resolution/scope.js +76 -0
- package/dist/resolution/scope.js.map +1 -0
- package/dist/token.d.ts.map +1 -1
- package/dist/token.js +0 -3
- package/dist/token.js.map +1 -1
- package/package.json +80 -65
- package/src/binding.ts +38 -22
- package/src/container/binding-builders.ts +397 -0
- package/src/container/container.ts +838 -0
- package/src/decorators/inject.ts +16 -3
- package/src/decorators/injectable.ts +3 -3
- package/src/index.ts +14 -7
- package/src/{dependency-graph.ts → introspection/dependency-graph.ts} +1 -1
- package/src/{graph-adapters → introspection/graph-adapters}/cytoscape.ts +1 -1
- package/src/{graph-adapters → introspection/graph-adapters}/dot.ts +1 -1
- package/src/{graph-adapters → introspection/graph-adapters}/reactflow.ts +13 -2
- package/src/{inspector.ts → introspection/inspector.ts} +26 -21
- package/src/metadata/symbol-metadata-reader.ts +4 -4
- package/src/module.ts +14 -6
- package/src/registry.ts +115 -61
- package/src/{binding-select.ts → resolution/binding-select.ts} +16 -3
- package/src/{environment.ts → resolution/environment.ts} +35 -35
- package/src/resolution/instantiation-plan.ts +240 -0
- package/src/{lifecycle.ts → resolution/lifecycle.ts} +20 -13
- package/src/resolution/resolution-path.ts +77 -0
- package/src/resolution/resolver.ts +1755 -0
- package/src/{scope.ts → resolution/scope.ts} +35 -18
- package/src/token.ts +0 -3
- package/dist/binding-scope.d.ts.map +0 -1
- package/dist/binding-scope.js.map +0 -1
- package/dist/binding-select.d.ts.map +0 -1
- package/dist/binding-select.js.map +0 -1
- package/dist/constraints.d.ts.map +0 -1
- package/dist/constraints.js.map +0 -1
- package/dist/container.d.ts.map +0 -1
- package/dist/container.js +0 -853
- package/dist/container.js.map +0 -1
- package/dist/dependency-graph.d.ts.map +0 -1
- package/dist/dependency-graph.js.map +0 -1
- package/dist/environment.d.ts.map +0 -1
- package/dist/environment.js +0 -100
- package/dist/environment.js.map +0 -1
- package/dist/graph-adapters/cytoscape.d.ts.map +0 -1
- package/dist/graph-adapters/cytoscape.js.map +0 -1
- package/dist/graph-adapters/dot.d.ts +0 -6
- package/dist/graph-adapters/dot.d.ts.map +0 -1
- package/dist/graph-adapters/dot.js.map +0 -1
- package/dist/graph-adapters/reactflow.d.ts.map +0 -1
- package/dist/graph-adapters/reactflow.js.map +0 -1
- package/dist/inspector.d.ts.map +0 -1
- package/dist/inspector.js.map +0 -1
- package/dist/lifecycle.d.ts.map +0 -1
- package/dist/lifecycle.js.map +0 -1
- package/dist/resolve-options.d.ts.map +0 -1
- package/dist/resolve-options.js.map +0 -1
- package/dist/resolver.d.ts +0 -90
- package/dist/resolver.d.ts.map +0 -1
- package/dist/resolver.js +0 -1208
- package/dist/resolver.js.map +0 -1
- package/dist/scope.d.ts.map +0 -1
- package/dist/scope.js +0 -61
- package/dist/scope.js.map +0 -1
- package/src/container.ts +0 -1141
- package/src/resolver.ts +0 -1626
- /package/dist/{dependency-graph.d.ts → introspection/dependency-graph.d.ts} +0 -0
- /package/dist/{graph-adapters → introspection/graph-adapters}/cytoscape.js +0 -0
- /package/dist/{graph-adapters → introspection/graph-adapters}/dot.js +0 -0
- /package/dist/{binding-scope.d.ts → resolution/binding-scope.d.ts} +0 -0
- /package/dist/{binding-scope.js → resolution/binding-scope.js} +0 -0
- /package/dist/{binding-select.d.ts → resolution/binding-select.d.ts} +0 -0
- /package/dist/{constraints.d.ts → resolution/constraints.d.ts} +0 -0
- /package/dist/{constraints.js → resolution/constraints.js} +0 -0
- /package/dist/{resolve-options.d.ts → resolution/resolve-options.d.ts} +0 -0
- /package/dist/{resolve-options.js → resolution/resolve-options.js} +0 -0
- /package/src/{binding-scope.ts → resolution/binding-scope.ts} +0 -0
- /package/src/{constraints.ts → resolution/constraints.ts} +0 -0
- /package/src/{resolve-options.ts → resolution/resolve-options.ts} +0 -0
|
@@ -0,0 +1,641 @@
|
|
|
1
|
+
import { bindingSlotEquals } from "#/binding";
|
|
2
|
+
import { BindingEntry } from "#/container/binding-builders";
|
|
3
|
+
import { AsyncModuleLoadError, CircularDependencyError, DisposedContainerError, InternalError, RebindUnboundTokenError, ScopeViolationError, SyncDisposalNotSupportedError, } from "#/errors";
|
|
4
|
+
import { buildDependencyGraph } from "#/introspection/dependency-graph";
|
|
5
|
+
import { Inspector } from "#/introspection/inspector";
|
|
6
|
+
import { MetadataReaderToken } from "#/metadata/metadata-reader-token";
|
|
7
|
+
import { defaultMetadataReader } from "#/metadata/symbol-metadata-reader";
|
|
8
|
+
import { isSyncModule, MODULE_SETUP } from "#/module";
|
|
9
|
+
import { BindingRegistry } from "#/registry";
|
|
10
|
+
import { effectiveBindingScope } from "#/resolution/binding-scope";
|
|
11
|
+
import { LifecycleManager } from "#/resolution/lifecycle";
|
|
12
|
+
import { injectionSlotToResolveOptions, bindingSlotToResolveOptions } from "#/resolution/resolve-options";
|
|
13
|
+
import { DependencyResolver } from "#/resolution/resolver";
|
|
14
|
+
import { ScopeManager } from "#/resolution/scope";
|
|
15
|
+
import { tokenName } from "#/token";
|
|
16
|
+
// True when re-adding `restored` would immediately be displaced again by `current`
|
|
17
|
+
// (both slot-based with equal slots) — in that case the replacement was legitimate.
|
|
18
|
+
function displacesRestoredBinding(current, restored) {
|
|
19
|
+
const currentIsPurePredicate = current.predicate !== undefined && current.slot.name === undefined && current.slot.tags.length === 0;
|
|
20
|
+
if (currentIsPurePredicate) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
return bindingSlotEquals(current.slot, restored.slot);
|
|
24
|
+
}
|
|
25
|
+
// ── DefaultContainer ──────────────────────────────────────────────────────────
|
|
26
|
+
class DefaultContainer {
|
|
27
|
+
#disposed = false;
|
|
28
|
+
#registry;
|
|
29
|
+
#scope;
|
|
30
|
+
#lifecycle;
|
|
31
|
+
#resolver;
|
|
32
|
+
#inspector;
|
|
33
|
+
#parent;
|
|
34
|
+
// Module tracking: module -> ref count
|
|
35
|
+
#moduleRefs = new Map();
|
|
36
|
+
// Module bindings: module -> array of binding IDs registered by it
|
|
37
|
+
#moduleBindingIds = new Map();
|
|
38
|
+
constructor(parent) {
|
|
39
|
+
this.#parent = parent;
|
|
40
|
+
this.#registry = new BindingRegistry();
|
|
41
|
+
this.#scope = new ScopeManager(parent !== undefined);
|
|
42
|
+
this.#lifecycle = new LifecycleManager();
|
|
43
|
+
this.#inspector = new Inspector(this.#registry, this.#scope, parent !== undefined, () => this.#disposed);
|
|
44
|
+
this.#initResolver();
|
|
45
|
+
}
|
|
46
|
+
#initResolver() {
|
|
47
|
+
const metadataReader = this.#getMetadataReader();
|
|
48
|
+
const parentResolver = this.#parent === undefined ? undefined : this.#parent.#resolver;
|
|
49
|
+
this.#resolver = new DependencyResolver(this.#registry, this.#scope, this.#lifecycle, metadataReader, this, parentResolver);
|
|
50
|
+
}
|
|
51
|
+
#getMetadataReader() {
|
|
52
|
+
// Check if a custom MetadataReader has been bound
|
|
53
|
+
const metaBindings = this.#registry.getAll(MetadataReaderToken);
|
|
54
|
+
if (metaBindings.length > 0) {
|
|
55
|
+
try {
|
|
56
|
+
return this.#resolver.resolve(MetadataReaderToken, undefined, [], []);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
// fall through to default
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (this.#parent !== undefined) {
|
|
63
|
+
return this.#parent.#getMetadataReader();
|
|
64
|
+
}
|
|
65
|
+
return defaultMetadataReader;
|
|
66
|
+
}
|
|
67
|
+
get isDisposed() {
|
|
68
|
+
return this.#disposed;
|
|
69
|
+
}
|
|
70
|
+
// ── Binding ──────────────────────────────────────────────────────────────
|
|
71
|
+
bind(token) {
|
|
72
|
+
this.#assertNotDisposed();
|
|
73
|
+
return this.#createBindToBuilder(token);
|
|
74
|
+
}
|
|
75
|
+
#createBindToBuilder(token, moduleRef) {
|
|
76
|
+
const registry = this.#registry;
|
|
77
|
+
// Bindings displaced by this fluent chain's commits — each stays restorable
|
|
78
|
+
// until the chain settles on a shape that genuinely conflicts with it. A list,
|
|
79
|
+
// because one chain can displace several bindings (the default at an
|
|
80
|
+
// intermediate commit, then a named/tagged binding at the final one).
|
|
81
|
+
const displacedByChain = [];
|
|
82
|
+
const commitBinding = (binding, previousId) => {
|
|
83
|
+
if (previousId !== undefined) {
|
|
84
|
+
registry.removeById(previousId);
|
|
85
|
+
if (moduleRef !== undefined) {
|
|
86
|
+
const ids = this.#moduleBindingIds.get(moduleRef);
|
|
87
|
+
if (ids !== undefined) {
|
|
88
|
+
const idx = ids.indexOf(previousId);
|
|
89
|
+
if (idx !== -1) {
|
|
90
|
+
ids.splice(idx, 1);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
// The registry stores value-erased bindings — this is the single erasure point for the builder chain.
|
|
96
|
+
const displaced = registry.add(binding);
|
|
97
|
+
if (displaced !== undefined) {
|
|
98
|
+
// A fluent chain commits eagerly on each refinement, so an intermediate
|
|
99
|
+
// commit can displace a binding that the final shape would never conflict
|
|
100
|
+
// with. Remember it so a later re-commit that morphs away (named/tagged
|
|
101
|
+
// slot, pure predicate) can restore it.
|
|
102
|
+
displacedByChain.push(displaced);
|
|
103
|
+
}
|
|
104
|
+
if (previousId !== undefined && displacedByChain.length > 0) {
|
|
105
|
+
for (let index = displacedByChain.length - 1; index >= 0; index -= 1) {
|
|
106
|
+
const candidate = displacedByChain[index];
|
|
107
|
+
if (!displacesRestoredBinding(binding, candidate)) {
|
|
108
|
+
registry.add(candidate);
|
|
109
|
+
displacedByChain.splice(index, 1);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (moduleRef !== undefined) {
|
|
114
|
+
let ids = this.#moduleBindingIds.get(moduleRef);
|
|
115
|
+
if (ids === undefined) {
|
|
116
|
+
ids = [];
|
|
117
|
+
this.#moduleBindingIds.set(moduleRef, ids);
|
|
118
|
+
}
|
|
119
|
+
ids.push(binding.id);
|
|
120
|
+
}
|
|
121
|
+
return binding.id;
|
|
122
|
+
};
|
|
123
|
+
return new BindingEntry(token, commitBinding);
|
|
124
|
+
}
|
|
125
|
+
unbind(tokenOrId) {
|
|
126
|
+
this.#assertNotDisposed();
|
|
127
|
+
this.#unbindSync(tokenOrId);
|
|
128
|
+
}
|
|
129
|
+
/** Remove bindings from registry + scope and collect [binding, instance] pairs for deactivation. */
|
|
130
|
+
#collectDeactivationPairs(tokenOrId) {
|
|
131
|
+
if (typeof tokenOrId === "string") {
|
|
132
|
+
const binding = this.#registry.removeById(tokenOrId);
|
|
133
|
+
return binding === undefined ? [] : this.#drainSingletons([binding]);
|
|
134
|
+
}
|
|
135
|
+
// Dropping the whole token in one pass: removing each binding by id instead would re-scan and
|
|
136
|
+
// re-index the token's binding list once per binding.
|
|
137
|
+
return this.#drainSingletons(this.#registry.removeByToken(tokenOrId));
|
|
138
|
+
}
|
|
139
|
+
/** Drain singleton scope entries for a set of already-removed bindings. */
|
|
140
|
+
#drainSingletons(bindings) {
|
|
141
|
+
const pairs = [];
|
|
142
|
+
for (const binding of bindings) {
|
|
143
|
+
if (this.#scope.hasSingleton(binding.id)) {
|
|
144
|
+
pairs.push([binding, this.#scope.getSingleton(binding.id)]);
|
|
145
|
+
this.#scope.deleteSingleton(binding.id);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return pairs;
|
|
149
|
+
}
|
|
150
|
+
#unbindSync(tokenOrId) {
|
|
151
|
+
const reader = this.#getMetadataReader();
|
|
152
|
+
for (const [binding, instance] of this.#collectDeactivationPairs(tokenOrId)) {
|
|
153
|
+
this.#lifecycle.runDeactivationSync(binding, instance, reader);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
async unbindAsync(tokenOrId) {
|
|
157
|
+
this.#assertNotDisposed();
|
|
158
|
+
const reader = this.#getMetadataReader();
|
|
159
|
+
for (const [binding, instance] of this.#collectDeactivationPairs(tokenOrId)) {
|
|
160
|
+
await this.#lifecycle.runDeactivation(binding, instance, reader);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
unbindAll() {
|
|
164
|
+
this.#assertNotDisposed();
|
|
165
|
+
const reader = this.#getMetadataReader();
|
|
166
|
+
for (const [binding, instance] of this.#drainSingletons(this.#registry.clear())) {
|
|
167
|
+
this.#lifecycle.runDeactivationSync(binding, instance, reader);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
async unbindAllAsync() {
|
|
171
|
+
this.#assertNotDisposed();
|
|
172
|
+
const reader = this.#getMetadataReader();
|
|
173
|
+
for (const [binding, instance] of this.#drainSingletons(this.#registry.clear())) {
|
|
174
|
+
await this.#lifecycle.runDeactivation(binding, instance, reader);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
rebind(token) {
|
|
178
|
+
this.#assertNotDisposed();
|
|
179
|
+
if (!this.#registry.has(token)) {
|
|
180
|
+
throw new RebindUnboundTokenError(tokenName(token));
|
|
181
|
+
}
|
|
182
|
+
// Unbind existing (sync — if async deactivation, will throw AsyncDeactivationError)
|
|
183
|
+
this.#unbindSync(token);
|
|
184
|
+
return this.#createBindToBuilder(token);
|
|
185
|
+
}
|
|
186
|
+
// ── Module ────────────────────────────────────────────────────────────────
|
|
187
|
+
load(...modules) {
|
|
188
|
+
this.#assertNotDisposed();
|
|
189
|
+
this.#loadSyncModules(modules);
|
|
190
|
+
}
|
|
191
|
+
#loadSyncModules(modules) {
|
|
192
|
+
// Collect all modules in topological order (dedup by identity)
|
|
193
|
+
const toLoad = this.#collectModuleDeps(modules);
|
|
194
|
+
for (const module of toLoad) {
|
|
195
|
+
if (!isSyncModule(module)) {
|
|
196
|
+
throw new AsyncModuleLoadError(module.name);
|
|
197
|
+
}
|
|
198
|
+
const moduleRef = module;
|
|
199
|
+
const existing = this.#moduleRefs.get(moduleRef);
|
|
200
|
+
if (existing !== undefined) {
|
|
201
|
+
this.#moduleRefs.set(moduleRef, existing + 1);
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
this.#moduleRefs.set(moduleRef, 1);
|
|
205
|
+
const builder = this.#createModuleBuilder(moduleRef);
|
|
206
|
+
module[MODULE_SETUP](builder);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
#collectModuleDeps(modules) {
|
|
210
|
+
const seen = new Set();
|
|
211
|
+
const result = [];
|
|
212
|
+
const visit = (module) => {
|
|
213
|
+
const moduleRef = module;
|
|
214
|
+
if (seen.has(moduleRef)) {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
seen.add(moduleRef);
|
|
218
|
+
// We'll collect deps during setup via the builder's import()
|
|
219
|
+
result.push(module);
|
|
220
|
+
};
|
|
221
|
+
for (const module of modules) {
|
|
222
|
+
visit(module);
|
|
223
|
+
}
|
|
224
|
+
return result;
|
|
225
|
+
}
|
|
226
|
+
async loadAsync(...modules) {
|
|
227
|
+
this.#assertNotDisposed();
|
|
228
|
+
for (const module of modules) {
|
|
229
|
+
await this.#loadOneModuleAsync(module);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
async #loadOneModuleAsync(module) {
|
|
233
|
+
const moduleRef = module;
|
|
234
|
+
const existing = this.#moduleRefs.get(moduleRef);
|
|
235
|
+
if (existing !== undefined) {
|
|
236
|
+
this.#moduleRefs.set(moduleRef, existing + 1);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
this.#moduleRefs.set(moduleRef, 1);
|
|
240
|
+
if (isSyncModule(module)) {
|
|
241
|
+
const builder = this.#createModuleBuilder(moduleRef);
|
|
242
|
+
module[MODULE_SETUP](builder);
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
const importPromises = [];
|
|
246
|
+
const builder = this.#createAsyncModuleBuilder(moduleRef, importPromises);
|
|
247
|
+
await module[MODULE_SETUP](builder);
|
|
248
|
+
// Await nested async imports triggered inside the setup callback
|
|
249
|
+
if (importPromises.length > 0) {
|
|
250
|
+
await Promise.all(importPromises);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
#createModuleBuilder(moduleRef) {
|
|
255
|
+
return {
|
|
256
|
+
bind: (token) => this.#createBindToBuilder(token, moduleRef),
|
|
257
|
+
import: (...modules) => {
|
|
258
|
+
this.#loadSyncModules(modules);
|
|
259
|
+
},
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
#createAsyncModuleBuilder(moduleRef, importPromises) {
|
|
263
|
+
return {
|
|
264
|
+
bind: (token) => this.#createBindToBuilder(token, moduleRef),
|
|
265
|
+
import: (...modules) => {
|
|
266
|
+
for (const module of modules) {
|
|
267
|
+
importPromises.push(this.#loadOneModuleAsync(module));
|
|
268
|
+
}
|
|
269
|
+
},
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
unload(...modules) {
|
|
273
|
+
this.#assertNotDisposed();
|
|
274
|
+
for (const module of modules) {
|
|
275
|
+
this.#unloadModuleSync(module);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
/** Unregister module bindings and collect [binding, instance] pairs for deactivation. */
|
|
279
|
+
#removeModuleBindings(ref) {
|
|
280
|
+
this.#moduleRefs.delete(ref);
|
|
281
|
+
const ids = this.#moduleBindingIds.get(ref) ?? [];
|
|
282
|
+
this.#moduleBindingIds.delete(ref);
|
|
283
|
+
const pairs = [];
|
|
284
|
+
for (const id of ids) {
|
|
285
|
+
const binding = this.#registry.getById(id);
|
|
286
|
+
if (binding !== undefined) {
|
|
287
|
+
this.#registry.removeById(id);
|
|
288
|
+
if (this.#scope.hasSingleton(id)) {
|
|
289
|
+
pairs.push([binding, this.#scope.getSingleton(id)]);
|
|
290
|
+
this.#scope.deleteSingleton(id);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return pairs;
|
|
295
|
+
}
|
|
296
|
+
#unloadModuleSync(ref) {
|
|
297
|
+
const count = this.#moduleRefs.get(ref) ?? 0;
|
|
298
|
+
if (count <= 1) {
|
|
299
|
+
const reader = this.#getMetadataReader();
|
|
300
|
+
for (const [binding, instance] of this.#removeModuleBindings(ref)) {
|
|
301
|
+
this.#lifecycle.runDeactivationSync(binding, instance, reader);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
else {
|
|
305
|
+
this.#moduleRefs.set(ref, count - 1);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
async unloadAsync(...modules) {
|
|
309
|
+
this.#assertNotDisposed();
|
|
310
|
+
for (const module of modules) {
|
|
311
|
+
await this.#unloadModuleAsync(module);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
async #unloadModuleAsync(ref) {
|
|
315
|
+
const count = this.#moduleRefs.get(ref) ?? 0;
|
|
316
|
+
if (count <= 1) {
|
|
317
|
+
const reader = this.#getMetadataReader();
|
|
318
|
+
for (const [binding, instance] of this.#removeModuleBindings(ref)) {
|
|
319
|
+
await this.#lifecycle.runDeactivation(binding, instance, reader);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
else {
|
|
323
|
+
this.#moduleRefs.set(ref, count - 1);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
loadAutoRegistered(registry) {
|
|
327
|
+
this.#assertNotDisposed();
|
|
328
|
+
const entries = registry.entries();
|
|
329
|
+
for (const { target, scope } of entries) {
|
|
330
|
+
const builder = this.#createBindToBuilder(target);
|
|
331
|
+
const bindingBuilder = builder.toSelf();
|
|
332
|
+
if (scope === "singleton") {
|
|
333
|
+
bindingBuilder.singleton();
|
|
334
|
+
}
|
|
335
|
+
else if (scope === "scoped") {
|
|
336
|
+
bindingBuilder.scoped();
|
|
337
|
+
}
|
|
338
|
+
else {
|
|
339
|
+
bindingBuilder.transient();
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return entries.length;
|
|
343
|
+
}
|
|
344
|
+
// ── Lifecycle hooks ────────────────────────────────────────────────────────
|
|
345
|
+
onActivation(token, handler) {
|
|
346
|
+
this.#assertNotDisposed();
|
|
347
|
+
this.#lifecycle.registerActivation(token, handler);
|
|
348
|
+
}
|
|
349
|
+
onDeactivation(token, handler) {
|
|
350
|
+
this.#assertNotDisposed();
|
|
351
|
+
this.#lifecycle.registerDeactivation(token, handler);
|
|
352
|
+
}
|
|
353
|
+
// ── Resolution ────────────────────────────────────────────────────────────
|
|
354
|
+
resolve(token, options) {
|
|
355
|
+
this.#assertNotDisposed();
|
|
356
|
+
if (options === undefined) {
|
|
357
|
+
return this.#resolver.resolveFromContext(token, [], []);
|
|
358
|
+
}
|
|
359
|
+
return this.#resolver.resolve(token, options, [], []);
|
|
360
|
+
}
|
|
361
|
+
resolveAsync(token, options) {
|
|
362
|
+
this.#assertNotDisposed();
|
|
363
|
+
if (options === undefined) {
|
|
364
|
+
return this.#resolver.resolveAsyncFromContext(token, [], []);
|
|
365
|
+
}
|
|
366
|
+
return this.#resolver.resolveAsync(token, options, [], []);
|
|
367
|
+
}
|
|
368
|
+
resolveOptional(token, options) {
|
|
369
|
+
this.#assertNotDisposed();
|
|
370
|
+
return this.#resolver.resolveOptional(token, options, [], []);
|
|
371
|
+
}
|
|
372
|
+
resolveOptionalAsync(token, options) {
|
|
373
|
+
this.#assertNotDisposed();
|
|
374
|
+
return this.#resolver.resolveOptionalAsync(token, options, [], []);
|
|
375
|
+
}
|
|
376
|
+
resolveAll(token, options) {
|
|
377
|
+
this.#assertNotDisposed();
|
|
378
|
+
return this.#resolver.resolveAll(token, options, [], []);
|
|
379
|
+
}
|
|
380
|
+
resolveAllAsync(token, options) {
|
|
381
|
+
this.#assertNotDisposed();
|
|
382
|
+
return this.#resolver.resolveAllAsync(token, options, [], []);
|
|
383
|
+
}
|
|
384
|
+
// ── Child ─────────────────────────────────────────────────────────────────
|
|
385
|
+
createChild() {
|
|
386
|
+
this.#assertNotDisposed();
|
|
387
|
+
return new DefaultContainer(this);
|
|
388
|
+
}
|
|
389
|
+
// ── Dispose ───────────────────────────────────────────────────────────────
|
|
390
|
+
async dispose() {
|
|
391
|
+
if (this.#disposed) {
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
this.#disposed = true;
|
|
395
|
+
// Deactivate all singletons in this container (own only)
|
|
396
|
+
const reader = this.#getMetadataReader();
|
|
397
|
+
for (const [id, instance] of this.#scope.getAllSingletons()) {
|
|
398
|
+
const binding = this.#registry.getById(id);
|
|
399
|
+
if (binding !== undefined) {
|
|
400
|
+
await this.#lifecycle.runDeactivation(binding, instance, reader);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
this.#scope.clearAll();
|
|
404
|
+
}
|
|
405
|
+
[Symbol.asyncDispose]() {
|
|
406
|
+
return this.dispose();
|
|
407
|
+
}
|
|
408
|
+
[Symbol.dispose]() {
|
|
409
|
+
throw new SyncDisposalNotSupportedError();
|
|
410
|
+
}
|
|
411
|
+
// ── Initialization ────────────────────────────────────────────────────────
|
|
412
|
+
async initializeAsync() {
|
|
413
|
+
this.#assertNotDisposed();
|
|
414
|
+
const allBindings = this.#registry.allBindings();
|
|
415
|
+
for (const binding of allBindings) {
|
|
416
|
+
if (binding.kind === "alias") {
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
const scope = effectiveBindingScope(binding);
|
|
420
|
+
if (scope === "singleton" && !this.#scope.hasSingleton(binding.id)) {
|
|
421
|
+
if (binding.predicate !== undefined) {
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
// Has activation — need to resolve
|
|
425
|
+
if (binding.kind === "constant" && binding.onActivation === undefined) {
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
const slotOptions = bindingSlotToResolveOptions(binding.slot);
|
|
429
|
+
await this.resolveAsync(binding.token, slotOptions);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
// ── Validate ──────────────────────────────────────────────────────────────
|
|
434
|
+
validate() {
|
|
435
|
+
this.#assertNotDisposed();
|
|
436
|
+
const reader = this.#getMetadataReader();
|
|
437
|
+
const allBindings = this.#registry.allBindings();
|
|
438
|
+
for (const binding of allBindings) {
|
|
439
|
+
if (!this.#isSingletonStaticAnalyzableBinding(binding)) {
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
this.#validateSingletonBindingGraph(binding, reader);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
#isSingletonStaticAnalyzableBinding(binding) {
|
|
446
|
+
if (effectiveBindingScope(binding) !== "singleton") {
|
|
447
|
+
return false;
|
|
448
|
+
}
|
|
449
|
+
return binding.kind === "class" || binding.kind === "resolved" || binding.kind === "resolved-async";
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* DFS over explicit constructor / `toResolved*` dependency edges. Follows `toAlias` chains to the
|
|
453
|
+
* terminal binding for scope checks (SPEC §6.9).
|
|
454
|
+
*
|
|
455
|
+
* @remarks A `toDynamic*` dependency is scope-checked like any other — its declared scope is what
|
|
456
|
+
* makes it captive — but the DFS does not descend into the factory, whose body is opaque.
|
|
457
|
+
*/
|
|
458
|
+
#validateSingletonBindingGraph(root, reader) {
|
|
459
|
+
const rootName = tokenName(root.token);
|
|
460
|
+
const dfs = (current, pathNames, pathBindingIds) => {
|
|
461
|
+
if (pathBindingIds.has(current.id)) {
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
const extendedPathIds = new Set(pathBindingIds);
|
|
465
|
+
extendedPathIds.add(current.id);
|
|
466
|
+
for (const edge of this.#collectStaticDependencyEdges(current, reader)) {
|
|
467
|
+
const { terminal, depTokenName } = edge;
|
|
468
|
+
const depScope = this.#validationScopeFromTerminal(terminal);
|
|
469
|
+
if (depScope === "opaque") {
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
if (depScope === "scoped" || depScope === "transient") {
|
|
473
|
+
throw new ScopeViolationError({
|
|
474
|
+
consumerToken: rootName,
|
|
475
|
+
consumerScope: "singleton",
|
|
476
|
+
dependencyToken: depTokenName,
|
|
477
|
+
dependencyScope: depScope,
|
|
478
|
+
path: [...pathNames, depTokenName],
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
if (terminal.kind === "class" || terminal.kind === "resolved" || terminal.kind === "resolved-async") {
|
|
482
|
+
dfs(terminal, [...pathNames, depTokenName], extendedPathIds);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
};
|
|
486
|
+
dfs(root, [rootName], new Set());
|
|
487
|
+
}
|
|
488
|
+
#validationScopeFromTerminal(terminal) {
|
|
489
|
+
switch (terminal.kind) {
|
|
490
|
+
case "constant":
|
|
491
|
+
return "singleton";
|
|
492
|
+
// A factory's *body* is not statically analyzable, but the scope it was bound with is
|
|
493
|
+
// declared like any other — so the captive-dependency check applies. The DFS below still
|
|
494
|
+
// refuses to descend into the factory; only this edge is judged.
|
|
495
|
+
case "dynamic":
|
|
496
|
+
case "dynamic-async":
|
|
497
|
+
return terminal.scope;
|
|
498
|
+
case "class":
|
|
499
|
+
case "resolved":
|
|
500
|
+
case "resolved-async":
|
|
501
|
+
return terminal.scope;
|
|
502
|
+
case "alias":
|
|
503
|
+
throw new InternalError("validate: expected terminal binding after alias resolution");
|
|
504
|
+
default: {
|
|
505
|
+
const exhaustive = terminal;
|
|
506
|
+
return exhaustive;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
#followAliasChainToTerminal(binding, options) {
|
|
511
|
+
const cyclePath = [];
|
|
512
|
+
const seenAliasIds = new Set();
|
|
513
|
+
let current = binding;
|
|
514
|
+
while (current !== undefined && current.kind === "alias") {
|
|
515
|
+
if (seenAliasIds.has(current.id)) {
|
|
516
|
+
throw new CircularDependencyError(cyclePath);
|
|
517
|
+
}
|
|
518
|
+
seenAliasIds.add(current.id);
|
|
519
|
+
cyclePath.push(tokenName(current.token));
|
|
520
|
+
const nextToken = current.target;
|
|
521
|
+
const next = this.#resolver.peekBindingForValidate(nextToken, options);
|
|
522
|
+
if (next === undefined) {
|
|
523
|
+
return undefined;
|
|
524
|
+
}
|
|
525
|
+
current = next.binding;
|
|
526
|
+
}
|
|
527
|
+
return current;
|
|
528
|
+
}
|
|
529
|
+
#collectStaticDependencyEdges(binding, reader) {
|
|
530
|
+
const edges = [];
|
|
531
|
+
const pushTerminal = (terminal, displayName) => {
|
|
532
|
+
if (terminal === undefined) {
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
edges.push({ terminal, depTokenName: displayName });
|
|
536
|
+
};
|
|
537
|
+
if (binding.kind === "class") {
|
|
538
|
+
const meta = reader.getConstructorMetadata(binding.target);
|
|
539
|
+
if (meta === undefined) {
|
|
540
|
+
return edges;
|
|
541
|
+
}
|
|
542
|
+
for (const param of meta.params) {
|
|
543
|
+
const paramOptions = injectionSlotToResolveOptions(param);
|
|
544
|
+
if (param.optional) {
|
|
545
|
+
continue;
|
|
546
|
+
}
|
|
547
|
+
const tokenRef = param.token;
|
|
548
|
+
if (param.multi) {
|
|
549
|
+
const candidates = this.#resolver.peekCandidateBindingsForValidate(tokenRef, paramOptions);
|
|
550
|
+
for (const cand of candidates) {
|
|
551
|
+
const term = this.#followAliasChainToTerminal(cand, paramOptions);
|
|
552
|
+
pushTerminal(term, term !== undefined ? tokenName(term.token) : "");
|
|
553
|
+
}
|
|
554
|
+
continue;
|
|
555
|
+
}
|
|
556
|
+
const found = paramOptions === undefined
|
|
557
|
+
? this.#resolver.peekBindingForValidate(tokenRef, undefined)
|
|
558
|
+
: this.#resolver.peekBindingForValidate(tokenRef, paramOptions);
|
|
559
|
+
if (found === undefined) {
|
|
560
|
+
continue;
|
|
561
|
+
}
|
|
562
|
+
const term = this.#followAliasChainToTerminal(found.binding, paramOptions);
|
|
563
|
+
pushTerminal(term, term !== undefined ? tokenName(term.token) : "");
|
|
564
|
+
}
|
|
565
|
+
return edges;
|
|
566
|
+
}
|
|
567
|
+
if (binding.kind === "resolved" || binding.kind === "resolved-async") {
|
|
568
|
+
for (const dep of binding.deps) {
|
|
569
|
+
const depOptions = injectionSlotToResolveOptions(dep);
|
|
570
|
+
if (dep.optional) {
|
|
571
|
+
continue;
|
|
572
|
+
}
|
|
573
|
+
const tokenRef = dep.token;
|
|
574
|
+
if (dep.multi) {
|
|
575
|
+
const candidates = this.#resolver.peekCandidateBindingsForValidate(tokenRef, depOptions);
|
|
576
|
+
for (const cand of candidates) {
|
|
577
|
+
const term = this.#followAliasChainToTerminal(cand, depOptions);
|
|
578
|
+
pushTerminal(term, term !== undefined ? tokenName(term.token) : "");
|
|
579
|
+
}
|
|
580
|
+
continue;
|
|
581
|
+
}
|
|
582
|
+
const found = depOptions === undefined
|
|
583
|
+
? this.#resolver.peekBindingForValidate(tokenRef, undefined)
|
|
584
|
+
: this.#resolver.peekBindingForValidate(tokenRef, depOptions);
|
|
585
|
+
if (found === undefined) {
|
|
586
|
+
continue;
|
|
587
|
+
}
|
|
588
|
+
const term = this.#followAliasChainToTerminal(found.binding, depOptions);
|
|
589
|
+
pushTerminal(term, term !== undefined ? tokenName(term.token) : "");
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
return edges;
|
|
593
|
+
}
|
|
594
|
+
// ── Introspection ─────────────────────────────────────────────────────────
|
|
595
|
+
has(token, options) {
|
|
596
|
+
this.#assertNotDisposed();
|
|
597
|
+
return this.#inspector.has(token, options, () => this.#parent?.has(token, options) ?? false);
|
|
598
|
+
}
|
|
599
|
+
hasOwn(token, options) {
|
|
600
|
+
this.#assertNotDisposed();
|
|
601
|
+
return this.#inspector.hasOwn(token, options);
|
|
602
|
+
}
|
|
603
|
+
lookupBindings(token) {
|
|
604
|
+
this.#assertNotDisposed();
|
|
605
|
+
return this.#inspector.lookupBindings(token);
|
|
606
|
+
}
|
|
607
|
+
inspect() {
|
|
608
|
+
this.#assertNotDisposed();
|
|
609
|
+
return this.#inspector.inspect();
|
|
610
|
+
}
|
|
611
|
+
generateDependencyGraph(options) {
|
|
612
|
+
this.#assertNotDisposed();
|
|
613
|
+
return buildDependencyGraph(this.#registry, this.#getMetadataReader(), options, this.#parent === undefined ? undefined : this.#parent.#registry);
|
|
614
|
+
}
|
|
615
|
+
// ── Internal ──────────────────────────────────────────────────────────────
|
|
616
|
+
#assertNotDisposed() {
|
|
617
|
+
if (this.#disposed) {
|
|
618
|
+
throw new DisposedContainerError();
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
// ── Container static ──────────────────────────────────────────────────────────
|
|
623
|
+
/**
|
|
624
|
+
* @since 0.3.16-canary.0
|
|
625
|
+
*/
|
|
626
|
+
export const Container = {
|
|
627
|
+
create() {
|
|
628
|
+
return new DefaultContainer();
|
|
629
|
+
},
|
|
630
|
+
fromModules(...modules) {
|
|
631
|
+
const container = new DefaultContainer();
|
|
632
|
+
container.load(...modules);
|
|
633
|
+
return container;
|
|
634
|
+
},
|
|
635
|
+
async fromModulesAsync(...modules) {
|
|
636
|
+
const container = new DefaultContainer();
|
|
637
|
+
await container.loadAsync(...modules);
|
|
638
|
+
return container;
|
|
639
|
+
},
|
|
640
|
+
};
|
|
641
|
+
//# sourceMappingURL=container.js.map
|