@codefast/di 0.3.16-canary.2 → 0.4.0-canary.4
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 +16 -0
- package/README.md +5 -17
- package/dist/binding.d.mts +24 -24
- package/dist/constraints.d.mts +3 -3
- package/dist/container.d.mts +3 -3
- package/dist/container.mjs +89 -157
- package/dist/decorators/inject.d.mts +3 -2
- package/dist/decorators/inject.mjs +14 -41
- package/dist/decorators/injectable.mjs +4 -12
- package/dist/decorators/lifecycle-decorators.mjs +13 -74
- package/dist/dependency-graph.d.mts +1 -1
- package/dist/dependency-graph.mjs +6 -4
- package/dist/graph-adapters/cytoscape.d.mts +12 -12
- package/dist/graph-adapters/cytoscape.mjs +7 -7
- package/dist/graph-adapters/reactflow.d.mts +15 -15
- package/dist/graph-adapters/reactflow.mjs +6 -9
- package/dist/index.d.mts +6 -6
- package/dist/index.mjs +1 -1
- package/dist/inspector.d.mts +3 -2
- package/dist/inspector.mjs +7 -10
- package/dist/lifecycle.mjs +2 -12
- package/dist/metadata/metadata-keys.d.mts +8 -33
- package/dist/metadata/metadata-keys.mjs +8 -21
- package/dist/metadata/metadata-types.d.mts +5 -0
- package/dist/metadata/symbol-metadata-reader.d.mts +1 -0
- package/dist/metadata/symbol-metadata-reader.mjs +11 -33
- package/dist/module.mjs +1 -1
- package/dist/registry.mjs +3 -22
- package/dist/resolve-options.d.mts +2 -2
- package/dist/resolve-options.mjs +10 -8
- package/dist/resolver.d.mts +6 -1
- package/dist/resolver.mjs +15 -21
- package/dist/types.d.mts +10 -4
- package/package.json +40 -14
- package/src/binding-scope.ts +26 -0
- package/src/binding-select.ts +158 -0
- package/src/binding.ts +277 -0
- package/src/constraints.ts +121 -0
- package/src/constructor-type.ts +19 -0
- package/src/container.ts +1135 -0
- package/src/decorators/inject.ts +222 -0
- package/src/decorators/injectable.ts +85 -0
- package/src/decorators/lifecycle-decorators.ts +51 -0
- package/src/dependency-graph.ts +116 -0
- package/src/environment.ts +207 -0
- package/src/errors.ts +260 -0
- package/src/graph-adapters/cytoscape.ts +64 -0
- package/src/graph-adapters/dot.ts +22 -0
- package/src/graph-adapters/reactflow.ts +58 -0
- package/src/index.ts +101 -0
- package/src/inspector.ts +125 -0
- package/src/lifecycle.ts +217 -0
- package/src/metadata/metadata-keys.ts +25 -0
- package/src/metadata/metadata-reader-token.ts +8 -0
- package/src/metadata/metadata-types.ts +51 -0
- package/src/metadata/symbol-metadata-reader.ts +45 -0
- package/src/module.ts +93 -0
- package/src/registry.ts +232 -0
- package/src/resolve-options.ts +42 -0
- package/src/resolver.ts +1609 -0
- package/src/scope.ts +77 -0
- package/src/token.ts +40 -0
- package/src/types.ts +123 -0
- package/dist/graph-adapters/types.d.mts +0 -2
- package/dist/graph-adapters/types.mjs +0 -1
package/src/container.ts
ADDED
|
@@ -0,0 +1,1135 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AliasBindingBuilder,
|
|
3
|
+
Binding,
|
|
4
|
+
BindingBuilder,
|
|
5
|
+
BindToBuilder,
|
|
6
|
+
ConstantBindingBuilder,
|
|
7
|
+
ScopedBindingBuilder,
|
|
8
|
+
SingletonBindingBuilder,
|
|
9
|
+
SingletonLifecycleBuilder,
|
|
10
|
+
PartialBinding,
|
|
11
|
+
BindingSlot,
|
|
12
|
+
TransientBindingBuilder,
|
|
13
|
+
} from "#/binding";
|
|
14
|
+
import { DEFAULT_BINDING_SLOT, generateBindingId } from "#/binding";
|
|
15
|
+
import { effectiveBindingScope } from "#/binding-scope";
|
|
16
|
+
import { normalizeToDescriptor } from "#/decorators/inject";
|
|
17
|
+
import type { AutoRegisterRegistry } from "#/decorators/injectable";
|
|
18
|
+
import type { ContainerGraphJson, GraphOptions } from "#/dependency-graph";
|
|
19
|
+
import { buildDependencyGraph } from "#/dependency-graph";
|
|
20
|
+
import {
|
|
21
|
+
AsyncModuleLoadError,
|
|
22
|
+
CircularDependencyError,
|
|
23
|
+
DisposedContainerError,
|
|
24
|
+
InternalError,
|
|
25
|
+
RebindUnboundTokenError,
|
|
26
|
+
ScopeViolationError,
|
|
27
|
+
SyncDisposalNotSupportedError,
|
|
28
|
+
} from "#/errors";
|
|
29
|
+
import type { BindingSnapshot, ContainerSnapshot } from "#/inspector";
|
|
30
|
+
import { Inspector } from "#/inspector";
|
|
31
|
+
import { LifecycleManager } from "#/lifecycle";
|
|
32
|
+
import { MetadataReaderToken } from "#/metadata/metadata-reader-token";
|
|
33
|
+
import type { MetadataReader } from "#/metadata/metadata-types";
|
|
34
|
+
import { defaultMetadataReader } from "#/metadata/symbol-metadata-reader";
|
|
35
|
+
import type { AsyncModule, ModuleBuilder, SyncModule } from "#/module";
|
|
36
|
+
import type { AsyncModuleBuilder } from "#/module";
|
|
37
|
+
import { isSyncModule } from "#/module";
|
|
38
|
+
import { BindingRegistry } from "#/registry";
|
|
39
|
+
import { injectionSlotToResolveOptions, bindingSlotToResolveOptions } from "#/resolve-options";
|
|
40
|
+
import { DependencyResolver } from "#/resolver";
|
|
41
|
+
import { ScopeManager } from "#/scope";
|
|
42
|
+
import type { Token } from "#/token";
|
|
43
|
+
import { tokenName } from "#/token";
|
|
44
|
+
import type {
|
|
45
|
+
ActivationHandler,
|
|
46
|
+
BindingIdentifier,
|
|
47
|
+
BindingScope,
|
|
48
|
+
ConstraintContext,
|
|
49
|
+
Constructor,
|
|
50
|
+
DeactivationHandler,
|
|
51
|
+
DependencyKey,
|
|
52
|
+
ResolutionContext,
|
|
53
|
+
ResolveOptions,
|
|
54
|
+
TokenValue,
|
|
55
|
+
} from "#/types";
|
|
56
|
+
|
|
57
|
+
// ── Container interface ────────────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @since 0.3.16-canary.0
|
|
61
|
+
*/
|
|
62
|
+
export interface Container {
|
|
63
|
+
readonly isDisposed: boolean;
|
|
64
|
+
|
|
65
|
+
bind<const Value>(token: Token<Value> | Constructor<Value>): BindToBuilder<Value>;
|
|
66
|
+
unbind(tokenOrId: Token<unknown> | Constructor | BindingIdentifier): void;
|
|
67
|
+
unbindAsync(tokenOrId: Token<unknown> | Constructor | BindingIdentifier): Promise<void>;
|
|
68
|
+
unbindAll(): void;
|
|
69
|
+
unbindAllAsync(): Promise<void>;
|
|
70
|
+
rebind<const Value>(token: Token<Value> | Constructor<Value>): BindToBuilder<Value>;
|
|
71
|
+
|
|
72
|
+
load(...modules: Array<SyncModule>): void;
|
|
73
|
+
loadAsync(...modules: Array<SyncModule | AsyncModule>): Promise<void>;
|
|
74
|
+
unload(...modules: Array<SyncModule>): void;
|
|
75
|
+
unloadAsync(...modules: Array<SyncModule | AsyncModule>): Promise<void>;
|
|
76
|
+
loadAutoRegistered(registry: AutoRegisterRegistry): number;
|
|
77
|
+
|
|
78
|
+
onActivation<const Value>(token: Token<Value> | Constructor<Value>, handler: ActivationHandler<Value>): void;
|
|
79
|
+
onDeactivation<const Value>(token: Token<Value> | Constructor<Value>, handler: DeactivationHandler<Value>): void;
|
|
80
|
+
|
|
81
|
+
resolve<const Value>(token: Token<Value> | Constructor<Value>, hint?: ResolveOptions): Value;
|
|
82
|
+
resolveAsync<const Value>(token: Token<Value> | Constructor<Value>, hint?: ResolveOptions): Promise<Value>;
|
|
83
|
+
resolveOptional<const Value>(token: Token<Value> | Constructor<Value>, hint?: ResolveOptions): Value | undefined;
|
|
84
|
+
resolveOptionalAsync<const Value>(
|
|
85
|
+
token: Token<Value> | Constructor<Value>,
|
|
86
|
+
hint?: ResolveOptions,
|
|
87
|
+
): Promise<Value | undefined>;
|
|
88
|
+
resolveAll<const Value>(token: Token<Value> | Constructor<Value>, hint?: ResolveOptions): Array<Value>;
|
|
89
|
+
resolveAllAsync<const Value>(token: Token<Value> | Constructor<Value>, hint?: ResolveOptions): Promise<Array<Value>>;
|
|
90
|
+
|
|
91
|
+
createChild(): Container;
|
|
92
|
+
|
|
93
|
+
dispose(): Promise<void>;
|
|
94
|
+
[Symbol.asyncDispose](): Promise<void>;
|
|
95
|
+
[Symbol.dispose](): never;
|
|
96
|
+
|
|
97
|
+
initializeAsync(): Promise<void>;
|
|
98
|
+
validate(): void;
|
|
99
|
+
|
|
100
|
+
has(token: Token<unknown> | Constructor, hint?: ResolveOptions): boolean;
|
|
101
|
+
hasOwn(token: Token<unknown> | Constructor, hint?: ResolveOptions): boolean;
|
|
102
|
+
lookupBindings<const Value>(token: Token<Value> | Constructor<Value>): ReadonlyArray<BindingSnapshot>;
|
|
103
|
+
inspect(): ContainerSnapshot;
|
|
104
|
+
generateDependencyGraph(options?: GraphOptions): ContainerGraphJson;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* @since 0.3.16-canary.0
|
|
109
|
+
*/
|
|
110
|
+
export interface ContainerStatic {
|
|
111
|
+
create(): Container;
|
|
112
|
+
fromModules(...modules: Array<SyncModule>): Container;
|
|
113
|
+
fromModulesAsync(...modules: Array<SyncModule | AsyncModule>): Promise<Container>;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ── DefaultContainer ──────────────────────────────────────────────────────────
|
|
117
|
+
|
|
118
|
+
class DefaultContainer implements Container {
|
|
119
|
+
private _disposed = false;
|
|
120
|
+
private readonly _registry: BindingRegistry;
|
|
121
|
+
private readonly _scope: ScopeManager;
|
|
122
|
+
private readonly _lifecycle: LifecycleManager;
|
|
123
|
+
private _resolver!: DependencyResolver;
|
|
124
|
+
private readonly _inspector: Inspector;
|
|
125
|
+
private readonly _parent: DefaultContainer | undefined;
|
|
126
|
+
|
|
127
|
+
// Module tracking: module -> ref count
|
|
128
|
+
private readonly _moduleRefs = new Map<object, number>();
|
|
129
|
+
// Module bindings: module -> array of binding IDs registered by it
|
|
130
|
+
private readonly _moduleBindingIds = new Map<object, Array<BindingIdentifier>>();
|
|
131
|
+
|
|
132
|
+
constructor(parent?: DefaultContainer) {
|
|
133
|
+
this._parent = parent;
|
|
134
|
+
this._registry = new BindingRegistry();
|
|
135
|
+
this._scope = new ScopeManager(parent !== undefined);
|
|
136
|
+
this._lifecycle = new LifecycleManager();
|
|
137
|
+
this._inspector = new Inspector(this._registry, this._scope, parent !== undefined, () => this._disposed);
|
|
138
|
+
this._initResolver();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private _initResolver(): void {
|
|
142
|
+
const metadataReader = this._getMetadataReader();
|
|
143
|
+
const parentResolver = this._parent?._resolver;
|
|
144
|
+
this._resolver = new DependencyResolver(
|
|
145
|
+
this._registry,
|
|
146
|
+
this._scope,
|
|
147
|
+
this._lifecycle,
|
|
148
|
+
metadataReader,
|
|
149
|
+
this,
|
|
150
|
+
parentResolver,
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
private _getMetadataReader(): MetadataReader {
|
|
155
|
+
// Check if a custom MetadataReader has been bound
|
|
156
|
+
const metaBindings = this._registry.getAll(MetadataReaderToken);
|
|
157
|
+
if (metaBindings.length > 0) {
|
|
158
|
+
try {
|
|
159
|
+
return this._resolver.resolve(MetadataReaderToken, undefined, [], []);
|
|
160
|
+
} catch {
|
|
161
|
+
// fall through to default
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
if (this._parent !== undefined) {
|
|
165
|
+
return this._parent._getMetadataReader();
|
|
166
|
+
}
|
|
167
|
+
return defaultMetadataReader;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
get isDisposed(): boolean {
|
|
171
|
+
return this._disposed;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ── Binding ──────────────────────────────────────────────────────────────
|
|
175
|
+
|
|
176
|
+
bind<const Value>(token: Token<Value> | Constructor<Value>): BindToBuilder<Value> {
|
|
177
|
+
this._assertNotDisposed();
|
|
178
|
+
return this._createBindToBuilder(token);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
private _createBindToBuilder<const Value>(
|
|
182
|
+
token: Token<Value> | Constructor<Value>,
|
|
183
|
+
moduleRef?: object,
|
|
184
|
+
): BindToBuilder<Value> {
|
|
185
|
+
const registry = this._registry;
|
|
186
|
+
|
|
187
|
+
const commitBinding = (binding: Binding, previousId?: BindingIdentifier): BindingIdentifier => {
|
|
188
|
+
if (previousId !== undefined) {
|
|
189
|
+
registry.removeById(previousId);
|
|
190
|
+
if (moduleRef !== undefined) {
|
|
191
|
+
const ids = this._moduleBindingIds.get(moduleRef);
|
|
192
|
+
if (ids !== undefined) {
|
|
193
|
+
const idx = ids.indexOf(previousId);
|
|
194
|
+
if (idx !== -1) {
|
|
195
|
+
ids.splice(idx, 1);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
registry.add(binding);
|
|
201
|
+
if (moduleRef !== undefined) {
|
|
202
|
+
let ids = this._moduleBindingIds.get(moduleRef);
|
|
203
|
+
if (ids === undefined) {
|
|
204
|
+
ids = [];
|
|
205
|
+
this._moduleBindingIds.set(moduleRef, ids);
|
|
206
|
+
}
|
|
207
|
+
ids.push(binding.id);
|
|
208
|
+
}
|
|
209
|
+
return binding.id;
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
return new BindingEntry<Value>(token, commitBinding);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
unbind(tokenOrId: Token<unknown> | Constructor | BindingIdentifier): void {
|
|
216
|
+
this._assertNotDisposed();
|
|
217
|
+
this._unbindSync(tokenOrId);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** Remove bindings from registry + scope and collect [binding, instance] pairs for deactivation. */
|
|
221
|
+
private _collectDeactivationPairs(
|
|
222
|
+
tokenOrId: Token<unknown> | Constructor | BindingIdentifier,
|
|
223
|
+
): Array<[Binding, unknown]> {
|
|
224
|
+
const pairs: Array<[Binding, unknown]> = [];
|
|
225
|
+
for (const binding of this._getBindingsForTokenOrId(tokenOrId)) {
|
|
226
|
+
this._registry.removeById(binding.id);
|
|
227
|
+
if (this._scope.hasSingleton(binding.id)) {
|
|
228
|
+
pairs.push([binding, this._scope.getSingleton(binding.id)]);
|
|
229
|
+
this._scope.deleteSingleton(binding.id);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return pairs;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** Drain singleton scope entries for a set of already-removed bindings. */
|
|
236
|
+
private _drainSingletons(bindings: ReadonlyArray<Binding>): Array<[Binding, unknown]> {
|
|
237
|
+
const pairs: Array<[Binding, unknown]> = [];
|
|
238
|
+
for (const binding of bindings) {
|
|
239
|
+
if (this._scope.hasSingleton(binding.id)) {
|
|
240
|
+
pairs.push([binding, this._scope.getSingleton(binding.id)]);
|
|
241
|
+
this._scope.deleteSingleton(binding.id);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return pairs;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
private _unbindSync(tokenOrId: Token<unknown> | Constructor | BindingIdentifier): void {
|
|
248
|
+
const reader = this._getMetadataReader();
|
|
249
|
+
for (const [binding, instance] of this._collectDeactivationPairs(tokenOrId)) {
|
|
250
|
+
this._lifecycle.runDeactivationSync(binding, instance, reader);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async unbindAsync(tokenOrId: Token<unknown> | Constructor | BindingIdentifier): Promise<void> {
|
|
255
|
+
this._assertNotDisposed();
|
|
256
|
+
const reader = this._getMetadataReader();
|
|
257
|
+
for (const [binding, instance] of this._collectDeactivationPairs(tokenOrId)) {
|
|
258
|
+
await this._lifecycle.runDeactivation(binding, instance, reader);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
unbindAll(): void {
|
|
263
|
+
this._assertNotDisposed();
|
|
264
|
+
const reader = this._getMetadataReader();
|
|
265
|
+
for (const [binding, instance] of this._drainSingletons(this._registry.clear())) {
|
|
266
|
+
this._lifecycle.runDeactivationSync(binding, instance, reader);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async unbindAllAsync(): Promise<void> {
|
|
271
|
+
this._assertNotDisposed();
|
|
272
|
+
const reader = this._getMetadataReader();
|
|
273
|
+
for (const [binding, instance] of this._drainSingletons(this._registry.clear())) {
|
|
274
|
+
await this._lifecycle.runDeactivation(binding, instance, reader);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
rebind<const Value>(token: Token<Value> | Constructor<Value>): BindToBuilder<Value> {
|
|
279
|
+
this._assertNotDisposed();
|
|
280
|
+
if (!this._registry.has(token)) {
|
|
281
|
+
throw new RebindUnboundTokenError(tokenName(token));
|
|
282
|
+
}
|
|
283
|
+
// Unbind existing (sync — if async deactivation, will throw AsyncDeactivationError)
|
|
284
|
+
this._unbindSync(token);
|
|
285
|
+
return this._createBindToBuilder(token);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
private _getBindingsForTokenOrId(tokenOrId: Token<unknown> | Constructor | BindingIdentifier): Array<Binding> {
|
|
289
|
+
if (typeof tokenOrId === "string") {
|
|
290
|
+
// It's a BindingIdentifier (string)
|
|
291
|
+
const binding = this._registry.getById(tokenOrId);
|
|
292
|
+
return binding !== undefined ? [binding] : [];
|
|
293
|
+
}
|
|
294
|
+
return [...this._registry.getAll(tokenOrId)];
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// ── Module ────────────────────────────────────────────────────────────────
|
|
298
|
+
|
|
299
|
+
load(...modules: Array<SyncModule>): void {
|
|
300
|
+
this._assertNotDisposed();
|
|
301
|
+
this._loadSyncModules(modules);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
private _loadSyncModules(modules: Array<SyncModule>): void {
|
|
305
|
+
// Collect all modules in topological order (dedup by identity)
|
|
306
|
+
const toLoad = this._collectModuleDeps(modules);
|
|
307
|
+
for (const module of toLoad) {
|
|
308
|
+
if (!isSyncModule(module)) {
|
|
309
|
+
throw new AsyncModuleLoadError(module.name);
|
|
310
|
+
}
|
|
311
|
+
const moduleRef = module as object;
|
|
312
|
+
const existing = this._moduleRefs.get(moduleRef);
|
|
313
|
+
if (existing !== undefined) {
|
|
314
|
+
this._moduleRefs.set(moduleRef, existing + 1);
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
this._moduleRefs.set(moduleRef, 1);
|
|
318
|
+
const builder = this._createModuleBuilder(moduleRef);
|
|
319
|
+
module._setup(builder);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
private _collectModuleDeps(modules: Array<SyncModule | AsyncModule>): Array<SyncModule | AsyncModule> {
|
|
324
|
+
const seen = new Set<object>();
|
|
325
|
+
const result: Array<SyncModule | AsyncModule> = [];
|
|
326
|
+
|
|
327
|
+
const visit = (module: SyncModule | AsyncModule): void => {
|
|
328
|
+
const moduleRef = module as object;
|
|
329
|
+
if (seen.has(moduleRef)) {
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
seen.add(moduleRef);
|
|
333
|
+
// We'll collect deps during setup via the builder's import()
|
|
334
|
+
result.push(module);
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
for (const module of modules) {
|
|
338
|
+
visit(module);
|
|
339
|
+
}
|
|
340
|
+
return result;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
async loadAsync(...modules: Array<SyncModule | AsyncModule>): Promise<void> {
|
|
344
|
+
this._assertNotDisposed();
|
|
345
|
+
for (const module of modules) {
|
|
346
|
+
await this._loadOneModuleAsync(module);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
private async _loadOneModuleAsync(module: SyncModule | AsyncModule): Promise<void> {
|
|
351
|
+
const moduleRef = module as object;
|
|
352
|
+
const existing = this._moduleRefs.get(moduleRef);
|
|
353
|
+
if (existing !== undefined) {
|
|
354
|
+
this._moduleRefs.set(moduleRef, existing + 1);
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
this._moduleRefs.set(moduleRef, 1);
|
|
358
|
+
|
|
359
|
+
if (isSyncModule(module)) {
|
|
360
|
+
const builder = this._createModuleBuilder(moduleRef);
|
|
361
|
+
module._setup(builder);
|
|
362
|
+
} else {
|
|
363
|
+
const importPromises: Array<Promise<void>> = [];
|
|
364
|
+
const builder = this._createAsyncModuleBuilder(moduleRef, importPromises);
|
|
365
|
+
await module._setup(builder);
|
|
366
|
+
// Await nested async imports triggered inside _setup
|
|
367
|
+
if (importPromises.length > 0) {
|
|
368
|
+
await Promise.all(importPromises);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
private _createModuleBuilder(moduleRef: object): ModuleBuilder {
|
|
374
|
+
return {
|
|
375
|
+
bind: <const Value>(token: Token<Value> | Constructor<Value>): BindToBuilder<Value> =>
|
|
376
|
+
this._createBindToBuilder(token, moduleRef),
|
|
377
|
+
import: (...modules: Array<SyncModule>): void => {
|
|
378
|
+
this._loadSyncModules(modules);
|
|
379
|
+
},
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
private _createAsyncModuleBuilder(moduleRef: object, importPromises: Array<Promise<void>>): AsyncModuleBuilder {
|
|
384
|
+
return {
|
|
385
|
+
bind: <const Value>(token: Token<Value> | Constructor<Value>): BindToBuilder<Value> =>
|
|
386
|
+
this._createBindToBuilder(token, moduleRef),
|
|
387
|
+
import: (...modules: Array<SyncModule | AsyncModule>): void => {
|
|
388
|
+
for (const module of modules) {
|
|
389
|
+
importPromises.push(this._loadOneModuleAsync(module));
|
|
390
|
+
}
|
|
391
|
+
},
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
unload(...modules: Array<SyncModule>): void {
|
|
396
|
+
this._assertNotDisposed();
|
|
397
|
+
for (const module of modules) {
|
|
398
|
+
this._unloadModuleSync(module as object);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/** Unregister module bindings and collect [binding, instance] pairs for deactivation. */
|
|
403
|
+
private _removeModuleBindings(ref: object): Array<[Binding, unknown]> {
|
|
404
|
+
this._moduleRefs.delete(ref);
|
|
405
|
+
const ids = this._moduleBindingIds.get(ref) ?? [];
|
|
406
|
+
this._moduleBindingIds.delete(ref);
|
|
407
|
+
const pairs: Array<[Binding, unknown]> = [];
|
|
408
|
+
for (const id of ids) {
|
|
409
|
+
const binding = this._registry.getById(id);
|
|
410
|
+
if (binding !== undefined) {
|
|
411
|
+
this._registry.removeById(id);
|
|
412
|
+
if (this._scope.hasSingleton(id)) {
|
|
413
|
+
pairs.push([binding, this._scope.getSingleton(id)]);
|
|
414
|
+
this._scope.deleteSingleton(id);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
return pairs;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
private _unloadModuleSync(ref: object): void {
|
|
422
|
+
const count = this._moduleRefs.get(ref) ?? 0;
|
|
423
|
+
if (count <= 1) {
|
|
424
|
+
const reader = this._getMetadataReader();
|
|
425
|
+
for (const [binding, instance] of this._removeModuleBindings(ref)) {
|
|
426
|
+
this._lifecycle.runDeactivationSync(binding, instance, reader);
|
|
427
|
+
}
|
|
428
|
+
} else {
|
|
429
|
+
this._moduleRefs.set(ref, count - 1);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
async unloadAsync(...modules: Array<SyncModule | AsyncModule>): Promise<void> {
|
|
434
|
+
this._assertNotDisposed();
|
|
435
|
+
for (const module of modules) {
|
|
436
|
+
await this._unloadModuleAsync(module as object);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
private async _unloadModuleAsync(ref: object): Promise<void> {
|
|
441
|
+
const count = this._moduleRefs.get(ref) ?? 0;
|
|
442
|
+
if (count <= 1) {
|
|
443
|
+
const reader = this._getMetadataReader();
|
|
444
|
+
for (const [binding, instance] of this._removeModuleBindings(ref)) {
|
|
445
|
+
await this._lifecycle.runDeactivation(binding, instance, reader);
|
|
446
|
+
}
|
|
447
|
+
} else {
|
|
448
|
+
this._moduleRefs.set(ref, count - 1);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
loadAutoRegistered(registry: AutoRegisterRegistry): number {
|
|
453
|
+
this._assertNotDisposed();
|
|
454
|
+
const entries = registry.entries();
|
|
455
|
+
for (const { target, scope } of entries) {
|
|
456
|
+
const builder = this._createBindToBuilder(target);
|
|
457
|
+
const bindingBuilder = builder.toSelf();
|
|
458
|
+
if (scope === "singleton") {
|
|
459
|
+
bindingBuilder.singleton();
|
|
460
|
+
} else if (scope === "scoped") {
|
|
461
|
+
bindingBuilder.scoped();
|
|
462
|
+
} else {
|
|
463
|
+
bindingBuilder.transient();
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
return entries.length;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// ── Lifecycle hooks ────────────────────────────────────────────────────────
|
|
470
|
+
|
|
471
|
+
onActivation<const Value>(token: Token<Value> | Constructor<Value>, handler: ActivationHandler<Value>): void {
|
|
472
|
+
this._assertNotDisposed();
|
|
473
|
+
this._lifecycle.registerActivation(token, handler);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
onDeactivation<const Value>(token: Token<Value> | Constructor<Value>, handler: DeactivationHandler<Value>): void {
|
|
477
|
+
this._assertNotDisposed();
|
|
478
|
+
this._lifecycle.registerDeactivation(token, handler);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// ── Resolution ────────────────────────────────────────────────────────────
|
|
482
|
+
|
|
483
|
+
resolve<const Value>(token: Token<Value> | Constructor<Value>, hint?: ResolveOptions): Value {
|
|
484
|
+
this._assertNotDisposed();
|
|
485
|
+
if (hint === undefined) {
|
|
486
|
+
return this._resolver.resolveFromContext(token, [], []);
|
|
487
|
+
}
|
|
488
|
+
return this._resolver.resolve(token, hint, [], []);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
resolveAsync<const Value>(token: Token<Value> | Constructor<Value>, hint?: ResolveOptions): Promise<Value> {
|
|
492
|
+
this._assertNotDisposed();
|
|
493
|
+
if (hint === undefined) {
|
|
494
|
+
return this._resolver.resolveAsyncFromContext(token, [], []);
|
|
495
|
+
}
|
|
496
|
+
return this._resolver.resolveAsync(token, hint, [], []);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
resolveOptional<const Value>(token: Token<Value> | Constructor<Value>, hint?: ResolveOptions): Value | undefined {
|
|
500
|
+
this._assertNotDisposed();
|
|
501
|
+
return this._resolver.resolveOptional(token, hint, [], []);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
resolveOptionalAsync<const Value>(
|
|
505
|
+
token: Token<Value> | Constructor<Value>,
|
|
506
|
+
hint?: ResolveOptions,
|
|
507
|
+
): Promise<Value | undefined> {
|
|
508
|
+
this._assertNotDisposed();
|
|
509
|
+
return this._resolver.resolveOptionalAsync(token, hint, [], []);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
resolveAll<const Value>(token: Token<Value> | Constructor<Value>, hint?: ResolveOptions): Array<Value> {
|
|
513
|
+
this._assertNotDisposed();
|
|
514
|
+
return this._resolver.resolveAll(token, hint, [], []);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
resolveAllAsync<const Value>(token: Token<Value> | Constructor<Value>, hint?: ResolveOptions): Promise<Array<Value>> {
|
|
518
|
+
this._assertNotDisposed();
|
|
519
|
+
return this._resolver.resolveAllAsync(token, hint, [], []);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// ── Child ─────────────────────────────────────────────────────────────────
|
|
523
|
+
|
|
524
|
+
createChild(): Container {
|
|
525
|
+
this._assertNotDisposed();
|
|
526
|
+
return new DefaultContainer(this);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// ── Dispose ───────────────────────────────────────────────────────────────
|
|
530
|
+
|
|
531
|
+
async dispose(): Promise<void> {
|
|
532
|
+
if (this._disposed) {
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
this._disposed = true;
|
|
536
|
+
|
|
537
|
+
// Deactivate all singletons in this container (own only)
|
|
538
|
+
const reader = this._getMetadataReader();
|
|
539
|
+
for (const [id, instance] of this._scope.getAllSingletons()) {
|
|
540
|
+
const binding = this._registry.getById(id);
|
|
541
|
+
if (binding !== undefined) {
|
|
542
|
+
await this._lifecycle.runDeactivation(binding, instance, reader);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
this._scope.clearAll();
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
[Symbol.asyncDispose](): Promise<void> {
|
|
550
|
+
return this.dispose();
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
[Symbol.dispose](): never {
|
|
554
|
+
throw new SyncDisposalNotSupportedError();
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// ── Initialization ────────────────────────────────────────────────────────
|
|
558
|
+
|
|
559
|
+
async initializeAsync(): Promise<void> {
|
|
560
|
+
this._assertNotDisposed();
|
|
561
|
+
const allBindings = this._registry.allBindings();
|
|
562
|
+
for (const binding of allBindings) {
|
|
563
|
+
if (binding.kind === "alias") {
|
|
564
|
+
continue;
|
|
565
|
+
}
|
|
566
|
+
const scope = effectiveBindingScope(binding);
|
|
567
|
+
if (scope === "singleton" && !this._scope.hasSingleton(binding.id)) {
|
|
568
|
+
if (binding.predicate !== undefined) {
|
|
569
|
+
continue;
|
|
570
|
+
}
|
|
571
|
+
// Has activation — need to resolve
|
|
572
|
+
if (binding.kind === "constant" && binding.onActivation === undefined) {
|
|
573
|
+
continue;
|
|
574
|
+
}
|
|
575
|
+
const slotHint = bindingSlotToResolveOptions(binding.slot);
|
|
576
|
+
await this.resolveAsync(binding.token as Token<unknown>, slotHint);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// ── Validate ──────────────────────────────────────────────────────────────
|
|
582
|
+
|
|
583
|
+
validate(): void {
|
|
584
|
+
this._assertNotDisposed();
|
|
585
|
+
const reader = this._getMetadataReader();
|
|
586
|
+
const allBindings = this._registry.allBindings();
|
|
587
|
+
|
|
588
|
+
for (const binding of allBindings) {
|
|
589
|
+
if (!this._isSingletonStaticAnalyzableBinding(binding)) {
|
|
590
|
+
continue;
|
|
591
|
+
}
|
|
592
|
+
this._validateSingletonBindingGraph(binding, reader);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
private _isSingletonStaticAnalyzableBinding(binding: Binding): boolean {
|
|
597
|
+
if (effectiveBindingScope(binding) !== "singleton") {
|
|
598
|
+
return false;
|
|
599
|
+
}
|
|
600
|
+
return binding.kind === "class" || binding.kind === "resolved" || binding.kind === "resolved-async";
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/**
|
|
604
|
+
* DFS over explicit constructor / `toResolved*` dependency edges. Follows `toAlias` chains to the
|
|
605
|
+
* terminal binding for scope checks (SPEC §6.9). Skips `toDynamic*` subtrees (opaque).
|
|
606
|
+
*/
|
|
607
|
+
private _validateSingletonBindingGraph(root: Binding, reader: MetadataReader): void {
|
|
608
|
+
const rootName = tokenName(root.token as Token<unknown>);
|
|
609
|
+
|
|
610
|
+
const dfs = (current: Binding, pathNames: Array<string>, pathBindingIds: Set<BindingIdentifier>): void => {
|
|
611
|
+
if (pathBindingIds.has(current.id)) {
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
const extendedPathIds = new Set(pathBindingIds);
|
|
615
|
+
extendedPathIds.add(current.id);
|
|
616
|
+
|
|
617
|
+
for (const edge of this._collectStaticDependencyEdges(current, reader)) {
|
|
618
|
+
const { terminal, depTokenName } = edge;
|
|
619
|
+
const depScope = this._validationScopeFromTerminal(terminal);
|
|
620
|
+
if (depScope === "opaque") {
|
|
621
|
+
continue;
|
|
622
|
+
}
|
|
623
|
+
if (depScope === "scoped" || depScope === "transient") {
|
|
624
|
+
throw new ScopeViolationError({
|
|
625
|
+
consumerToken: rootName,
|
|
626
|
+
consumerScope: "singleton",
|
|
627
|
+
dependencyToken: depTokenName,
|
|
628
|
+
dependencyScope: depScope,
|
|
629
|
+
path: [...pathNames, depTokenName],
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
if (terminal.kind === "class" || terminal.kind === "resolved" || terminal.kind === "resolved-async") {
|
|
633
|
+
dfs(terminal, [...pathNames, depTokenName], extendedPathIds);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
};
|
|
637
|
+
|
|
638
|
+
dfs(root, [rootName], new Set());
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
private _validationScopeFromTerminal(terminal: Binding): BindingScope | "opaque" {
|
|
642
|
+
switch (terminal.kind) {
|
|
643
|
+
case "constant":
|
|
644
|
+
return "singleton";
|
|
645
|
+
case "dynamic":
|
|
646
|
+
case "dynamic-async":
|
|
647
|
+
return "opaque";
|
|
648
|
+
case "class":
|
|
649
|
+
case "resolved":
|
|
650
|
+
case "resolved-async":
|
|
651
|
+
return terminal.scope;
|
|
652
|
+
case "alias":
|
|
653
|
+
throw new InternalError("validate: expected terminal binding after alias resolution");
|
|
654
|
+
default: {
|
|
655
|
+
const exhaustive: never = terminal;
|
|
656
|
+
return exhaustive;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
private _followAliasChainToTerminal(binding: Binding, hint: ResolveOptions | undefined): Binding | undefined {
|
|
662
|
+
const cyclePath: Array<string> = [];
|
|
663
|
+
const seenAliasIds = new Set<BindingIdentifier>();
|
|
664
|
+
let current: Binding | undefined = binding;
|
|
665
|
+
|
|
666
|
+
while (current !== undefined && current.kind === "alias") {
|
|
667
|
+
if (seenAliasIds.has(current.id)) {
|
|
668
|
+
throw new CircularDependencyError(cyclePath);
|
|
669
|
+
}
|
|
670
|
+
seenAliasIds.add(current.id);
|
|
671
|
+
cyclePath.push(tokenName(current.token as Token<unknown>));
|
|
672
|
+
const nextToken = current.target as Token<unknown> | Constructor;
|
|
673
|
+
const next = this._resolver.peekBindingForValidate(nextToken, hint);
|
|
674
|
+
if (next === undefined) {
|
|
675
|
+
return undefined;
|
|
676
|
+
}
|
|
677
|
+
current = next.binding;
|
|
678
|
+
}
|
|
679
|
+
return current;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
private _collectStaticDependencyEdges(
|
|
683
|
+
binding: Binding,
|
|
684
|
+
reader: MetadataReader,
|
|
685
|
+
): Array<{ terminal: Binding; depTokenName: string }> {
|
|
686
|
+
const edges: Array<{ terminal: Binding; depTokenName: string }> = [];
|
|
687
|
+
|
|
688
|
+
const pushTerminal = (terminal: Binding | undefined, displayName: string): void => {
|
|
689
|
+
if (terminal === undefined) {
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
edges.push({ terminal, depTokenName: displayName });
|
|
693
|
+
};
|
|
694
|
+
|
|
695
|
+
if (binding.kind === "class") {
|
|
696
|
+
const meta = reader.getConstructorMetadata(binding.target as Constructor);
|
|
697
|
+
if (meta === undefined) {
|
|
698
|
+
return edges;
|
|
699
|
+
}
|
|
700
|
+
for (const param of meta.params) {
|
|
701
|
+
const paramHint = injectionSlotToResolveOptions(param);
|
|
702
|
+
if (param.optional) {
|
|
703
|
+
continue;
|
|
704
|
+
}
|
|
705
|
+
const tokenRef = param.token;
|
|
706
|
+
if (param.multi) {
|
|
707
|
+
const candidates = this._resolver.peekCandidateBindingsForValidate(tokenRef, paramHint);
|
|
708
|
+
for (const cand of candidates) {
|
|
709
|
+
const term = this._followAliasChainToTerminal(cand, paramHint);
|
|
710
|
+
pushTerminal(term, term !== undefined ? tokenName(term.token as Token<unknown>) : "");
|
|
711
|
+
}
|
|
712
|
+
continue;
|
|
713
|
+
}
|
|
714
|
+
const found =
|
|
715
|
+
paramHint === undefined
|
|
716
|
+
? this._resolver.peekBindingForValidate(tokenRef, undefined)
|
|
717
|
+
: this._resolver.peekBindingForValidate(tokenRef, paramHint);
|
|
718
|
+
if (found === undefined) {
|
|
719
|
+
continue;
|
|
720
|
+
}
|
|
721
|
+
const term = this._followAliasChainToTerminal(found.binding, paramHint);
|
|
722
|
+
pushTerminal(term, term !== undefined ? tokenName(term.token as Token<unknown>) : "");
|
|
723
|
+
}
|
|
724
|
+
return edges;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
if (binding.kind === "resolved" || binding.kind === "resolved-async") {
|
|
728
|
+
for (const dep of binding.deps) {
|
|
729
|
+
const depHint = injectionSlotToResolveOptions(dep);
|
|
730
|
+
if (dep.optional) {
|
|
731
|
+
continue;
|
|
732
|
+
}
|
|
733
|
+
const tokenRef = dep.token as Token<unknown> | Constructor;
|
|
734
|
+
if (dep.multi) {
|
|
735
|
+
const candidates = this._resolver.peekCandidateBindingsForValidate(tokenRef, depHint);
|
|
736
|
+
for (const cand of candidates) {
|
|
737
|
+
const term = this._followAliasChainToTerminal(cand, depHint);
|
|
738
|
+
pushTerminal(term, term !== undefined ? tokenName(term.token as Token<unknown>) : "");
|
|
739
|
+
}
|
|
740
|
+
continue;
|
|
741
|
+
}
|
|
742
|
+
const found =
|
|
743
|
+
depHint === undefined
|
|
744
|
+
? this._resolver.peekBindingForValidate(tokenRef, undefined)
|
|
745
|
+
: this._resolver.peekBindingForValidate(tokenRef, depHint);
|
|
746
|
+
if (found === undefined) {
|
|
747
|
+
continue;
|
|
748
|
+
}
|
|
749
|
+
const term = this._followAliasChainToTerminal(found.binding, depHint);
|
|
750
|
+
pushTerminal(term, term !== undefined ? tokenName(term.token as Token<unknown>) : "");
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
return edges;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// ── Introspection ─────────────────────────────────────────────────────────
|
|
758
|
+
|
|
759
|
+
has(token: Token<unknown> | Constructor, hint?: ResolveOptions): boolean {
|
|
760
|
+
this._assertNotDisposed();
|
|
761
|
+
return this._inspector.has(token, hint, () => this._parent?.has(token, hint) ?? false);
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
hasOwn(token: Token<unknown> | Constructor, hint?: ResolveOptions): boolean {
|
|
765
|
+
this._assertNotDisposed();
|
|
766
|
+
return this._inspector.hasOwn(token, hint);
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
lookupBindings<const Value>(token: Token<Value> | Constructor<Value>): ReadonlyArray<BindingSnapshot> {
|
|
770
|
+
this._assertNotDisposed();
|
|
771
|
+
return this._inspector.lookupBindings(token);
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
inspect(): ContainerSnapshot {
|
|
775
|
+
this._assertNotDisposed();
|
|
776
|
+
return this._inspector.inspect();
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
generateDependencyGraph(options?: GraphOptions): ContainerGraphJson {
|
|
780
|
+
this._assertNotDisposed();
|
|
781
|
+
return buildDependencyGraph(this._registry, this._getMetadataReader(), options, this._parent?._registry);
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// ── Internal ──────────────────────────────────────────────────────────────
|
|
785
|
+
|
|
786
|
+
private _assertNotDisposed(): void {
|
|
787
|
+
if (this._disposed) {
|
|
788
|
+
throw new DisposedContainerError();
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
// ── Shared builder helpers ────────────────────────────────────────────────────
|
|
794
|
+
|
|
795
|
+
type CommitFn = (binding: Binding, previousId?: BindingIdentifier) => BindingIdentifier;
|
|
796
|
+
|
|
797
|
+
function updateSlotTag(slot: BindingSlot, tag: string, value: unknown): BindingSlot {
|
|
798
|
+
const existing = [...slot.tags];
|
|
799
|
+
const idx = existing.findIndex(([k]) => k === tag);
|
|
800
|
+
if (idx !== -1) {
|
|
801
|
+
existing[idx] = [tag, value];
|
|
802
|
+
} else {
|
|
803
|
+
existing.push([tag, value]);
|
|
804
|
+
}
|
|
805
|
+
return { ...slot, tags: existing };
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
// ── SlotBuilder ───────────────────────────────────────────────────────────────
|
|
809
|
+
|
|
810
|
+
abstract class SlotBuilder {
|
|
811
|
+
protected _slot: BindingSlot = DEFAULT_BINDING_SLOT; // ✓ T3-1: no redundant spread
|
|
812
|
+
protected _predicate: ((ctx: ConstraintContext) => boolean) | undefined;
|
|
813
|
+
protected _committed: BindingIdentifier | undefined;
|
|
814
|
+
|
|
815
|
+
protected abstract _doCommit(): void;
|
|
816
|
+
|
|
817
|
+
when(predicate: (ctx: ConstraintContext) => boolean): this {
|
|
818
|
+
this._predicate = predicate;
|
|
819
|
+
this._doCommit();
|
|
820
|
+
return this;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
whenNamed(name: string): this {
|
|
824
|
+
this._slot = { ...this._slot, name };
|
|
825
|
+
this._doCommit();
|
|
826
|
+
return this;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
whenTagged(tag: string, value: unknown): this {
|
|
830
|
+
this._slot = updateSlotTag(this._slot, tag, value);
|
|
831
|
+
this._doCommit();
|
|
832
|
+
return this;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
whenDefault(): this {
|
|
836
|
+
return this;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
id(): BindingIdentifier {
|
|
840
|
+
return this._committed!;
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
// ── BindingEntry ──────────────────────────────────────────────────────────────
|
|
845
|
+
|
|
846
|
+
class BindingEntry<Value> implements BindToBuilder<Value> {
|
|
847
|
+
constructor(
|
|
848
|
+
private readonly _token: Token<Value> | Constructor<Value>,
|
|
849
|
+
private readonly _commit: CommitFn,
|
|
850
|
+
) {}
|
|
851
|
+
|
|
852
|
+
to(type: Constructor<Value>): BindingBuilder<Value> {
|
|
853
|
+
return new ConstraintBuilder<Value>(this._token, { kind: "class", target: type, scope: "transient" }, this._commit);
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
toSelf(): BindingBuilder<Value> {
|
|
857
|
+
if (typeof this._token !== "function") {
|
|
858
|
+
throw new Error("toSelf() requires token to be a Constructor");
|
|
859
|
+
}
|
|
860
|
+
return new ConstraintBuilder<Value>(
|
|
861
|
+
this._token,
|
|
862
|
+
{ kind: "class", target: this._token, scope: "transient" },
|
|
863
|
+
this._commit,
|
|
864
|
+
);
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
toConstantValue(value: Value): ConstantBindingBuilder<Value> {
|
|
868
|
+
return new ConstantBuilder<Value>(this._token, value, this._commit);
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
toDynamic(factory: (ctx: ResolutionContext) => Value): BindingBuilder<Value> {
|
|
872
|
+
return new ConstraintBuilder<Value>(this._token, { kind: "dynamic", factory, scope: "transient" }, this._commit);
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
toDynamicAsync(factory: (ctx: ResolutionContext) => Promise<Value>): BindingBuilder<Value> {
|
|
876
|
+
return new ConstraintBuilder<Value>(
|
|
877
|
+
this._token,
|
|
878
|
+
{ kind: "dynamic-async", factory, scope: "transient" },
|
|
879
|
+
this._commit,
|
|
880
|
+
);
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
toResolved<const Deps extends ReadonlyArray<DependencyKey>>(
|
|
884
|
+
factory: (...args: { [K in keyof Deps]: TokenValue<NoInfer<Deps>[K]> }) => Value,
|
|
885
|
+
deps: Deps,
|
|
886
|
+
): BindingBuilder<Value> {
|
|
887
|
+
const normalizedDeps = deps.map((dependency) => normalizeToDescriptor(dependency));
|
|
888
|
+
return new ConstraintBuilder<Value>(
|
|
889
|
+
this._token,
|
|
890
|
+
{
|
|
891
|
+
kind: "resolved",
|
|
892
|
+
factory: factory as (...args: Array<unknown>) => Value,
|
|
893
|
+
deps: normalizedDeps,
|
|
894
|
+
scope: "transient",
|
|
895
|
+
},
|
|
896
|
+
this._commit,
|
|
897
|
+
);
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
toResolvedAsync<const Deps extends ReadonlyArray<DependencyKey>>(
|
|
901
|
+
factory: (...args: { [K in keyof Deps]: TokenValue<NoInfer<Deps>[K]> }) => Promise<Value>,
|
|
902
|
+
deps: Deps,
|
|
903
|
+
): BindingBuilder<Value> {
|
|
904
|
+
const normalizedDeps = deps.map((dependency) => normalizeToDescriptor(dependency));
|
|
905
|
+
return new ConstraintBuilder<Value>(
|
|
906
|
+
this._token,
|
|
907
|
+
{
|
|
908
|
+
kind: "resolved-async",
|
|
909
|
+
factory: factory as (...args: Array<unknown>) => Promise<Value>,
|
|
910
|
+
deps: normalizedDeps,
|
|
911
|
+
scope: "transient",
|
|
912
|
+
},
|
|
913
|
+
this._commit,
|
|
914
|
+
);
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
toAlias(target: Token<Value> | Constructor<Value>): AliasBindingBuilder {
|
|
918
|
+
return new AliasBuilder<Value>(this._token, target, this._commit);
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
// ── ConstraintBuilder ─────────────────────────────────────────────────────────
|
|
923
|
+
|
|
924
|
+
class ConstraintBuilder<Value> extends SlotBuilder implements BindingBuilder<Value> {
|
|
925
|
+
constructor(
|
|
926
|
+
private readonly _token: Token<Value> | Constructor<Value>,
|
|
927
|
+
private readonly _partial: PartialBinding<Value>,
|
|
928
|
+
private readonly _commit: CommitFn,
|
|
929
|
+
) {
|
|
930
|
+
super();
|
|
931
|
+
this._doCommit();
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
protected _doCommit(): void {
|
|
935
|
+
const previousId = this._committed;
|
|
936
|
+
const binding: Binding<Value> = {
|
|
937
|
+
...(this._partial as unknown as object),
|
|
938
|
+
id: generateBindingId(),
|
|
939
|
+
token: this._token,
|
|
940
|
+
slot: this._slot,
|
|
941
|
+
predicate: this._predicate,
|
|
942
|
+
} as Binding<Value>;
|
|
943
|
+
this._committed = this._commit(binding as Binding, previousId);
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
singleton(): SingletonBindingBuilder<Value> {
|
|
947
|
+
const previousId = this._committed;
|
|
948
|
+
this._committed = undefined;
|
|
949
|
+
return new ScopeBuilder<Value, "singleton">(
|
|
950
|
+
this._token,
|
|
951
|
+
{ ...this._partial, scope: "singleton" } as PartialBinding<Value>,
|
|
952
|
+
this._slot,
|
|
953
|
+
this._predicate,
|
|
954
|
+
this._commit,
|
|
955
|
+
"singleton",
|
|
956
|
+
previousId,
|
|
957
|
+
);
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
transient(): TransientBindingBuilder<Value> {
|
|
961
|
+
const previousId = this._committed;
|
|
962
|
+
this._committed = undefined;
|
|
963
|
+
return new ScopeBuilder<Value, "transient">(
|
|
964
|
+
this._token,
|
|
965
|
+
{ ...this._partial, scope: "transient" } as PartialBinding<Value>,
|
|
966
|
+
this._slot,
|
|
967
|
+
this._predicate,
|
|
968
|
+
this._commit,
|
|
969
|
+
"transient",
|
|
970
|
+
previousId,
|
|
971
|
+
);
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
scoped(): ScopedBindingBuilder<Value> {
|
|
975
|
+
const previousId = this._committed;
|
|
976
|
+
this._committed = undefined;
|
|
977
|
+
return new ScopeBuilder<Value, "scoped">(
|
|
978
|
+
this._token,
|
|
979
|
+
{ ...this._partial, scope: "scoped" } as PartialBinding<Value>,
|
|
980
|
+
this._slot,
|
|
981
|
+
this._predicate,
|
|
982
|
+
this._commit,
|
|
983
|
+
"scoped",
|
|
984
|
+
previousId,
|
|
985
|
+
);
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
// ── ScopeBuilder ─────────────────────────────────────────────────────────────
|
|
990
|
+
|
|
991
|
+
class ScopeBuilder<Value, Scope extends BindingScope>
|
|
992
|
+
implements SingletonBindingBuilder<Value>, TransientBindingBuilder<Value>
|
|
993
|
+
{
|
|
994
|
+
private _onActivation: ActivationHandler<Value> | undefined;
|
|
995
|
+
private _onDeactivation: DeactivationHandler<Value> | undefined;
|
|
996
|
+
private _committed: BindingIdentifier | undefined;
|
|
997
|
+
|
|
998
|
+
constructor(
|
|
999
|
+
private readonly _token: Token<Value> | Constructor<Value>,
|
|
1000
|
+
private readonly _partial: PartialBinding<Value>,
|
|
1001
|
+
private readonly _slot: BindingSlot,
|
|
1002
|
+
private readonly _predicate: ((ctx: ConstraintContext) => boolean) | undefined,
|
|
1003
|
+
private readonly _commit: CommitFn,
|
|
1004
|
+
private readonly _scope: Scope,
|
|
1005
|
+
private readonly _initialPreviousId?: BindingIdentifier,
|
|
1006
|
+
) {
|
|
1007
|
+
this._doCommit();
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
private _doCommit(): void {
|
|
1011
|
+
const previousId = this._committed ?? this._initialPreviousId;
|
|
1012
|
+
const binding = {
|
|
1013
|
+
...(this._partial as unknown as object),
|
|
1014
|
+
id: generateBindingId(),
|
|
1015
|
+
token: this._token,
|
|
1016
|
+
slot: this._slot,
|
|
1017
|
+
predicate: this._predicate,
|
|
1018
|
+
onActivation: this._onActivation,
|
|
1019
|
+
onDeactivation: this._onDeactivation,
|
|
1020
|
+
} as Binding<Value>;
|
|
1021
|
+
this._committed = this._commit(binding as Binding, previousId);
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
onActivation(fn: ActivationHandler<Value>): this {
|
|
1025
|
+
this._onActivation = fn;
|
|
1026
|
+
this._doCommit();
|
|
1027
|
+
return this;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
onDeactivation(fn: DeactivationHandler<Value>): this {
|
|
1031
|
+
this._onDeactivation = fn;
|
|
1032
|
+
this._doCommit();
|
|
1033
|
+
return this;
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
id(): BindingIdentifier {
|
|
1037
|
+
return this._committed!;
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
// ── ConstantBuilder ───────────────────────────────────────────────────────────
|
|
1042
|
+
|
|
1043
|
+
class ConstantBuilder<Value>
|
|
1044
|
+
extends SlotBuilder
|
|
1045
|
+
implements ConstantBindingBuilder<Value>, SingletonLifecycleBuilder<Value>
|
|
1046
|
+
{
|
|
1047
|
+
private _onActivation: ActivationHandler<Value> | undefined;
|
|
1048
|
+
private _onDeactivation: DeactivationHandler<Value> | undefined;
|
|
1049
|
+
|
|
1050
|
+
constructor(
|
|
1051
|
+
private readonly _token: Token<Value> | Constructor<Value>,
|
|
1052
|
+
private readonly _value: Value,
|
|
1053
|
+
private readonly _commit: CommitFn,
|
|
1054
|
+
) {
|
|
1055
|
+
super();
|
|
1056
|
+
this._doCommit();
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
protected _doCommit(): void {
|
|
1060
|
+
const previousId = this._committed;
|
|
1061
|
+
const binding: Binding<Value> = {
|
|
1062
|
+
kind: "constant",
|
|
1063
|
+
id: generateBindingId(),
|
|
1064
|
+
token: this._token,
|
|
1065
|
+
slot: this._slot,
|
|
1066
|
+
predicate: this._predicate,
|
|
1067
|
+
value: this._value,
|
|
1068
|
+
scope: "singleton",
|
|
1069
|
+
onActivation: this._onActivation,
|
|
1070
|
+
onDeactivation: this._onDeactivation,
|
|
1071
|
+
} as unknown as Binding<Value>;
|
|
1072
|
+
this._committed = this._commit(binding as Binding, previousId);
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
onActivation(fn: ActivationHandler<Value>): this {
|
|
1076
|
+
this._onActivation = fn;
|
|
1077
|
+
this._doCommit();
|
|
1078
|
+
return this;
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
onDeactivation(fn: DeactivationHandler<Value>): this {
|
|
1082
|
+
this._onDeactivation = fn;
|
|
1083
|
+
this._doCommit();
|
|
1084
|
+
return this;
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
// ── AliasBuilder ──────────────────────────────────────────────────────────────
|
|
1089
|
+
|
|
1090
|
+
class AliasBuilder<Value> extends SlotBuilder implements AliasBindingBuilder {
|
|
1091
|
+
constructor(
|
|
1092
|
+
private readonly _token: Token<Value> | Constructor<Value>,
|
|
1093
|
+
private readonly _target: Token<Value> | Constructor<Value>,
|
|
1094
|
+
private readonly _commit: CommitFn,
|
|
1095
|
+
) {
|
|
1096
|
+
super();
|
|
1097
|
+
this._doCommit();
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
protected _doCommit(): void {
|
|
1101
|
+
const previousId = this._committed;
|
|
1102
|
+
const binding: Binding<Value> = {
|
|
1103
|
+
kind: "alias",
|
|
1104
|
+
id: generateBindingId(),
|
|
1105
|
+
token: this._token,
|
|
1106
|
+
slot: this._slot,
|
|
1107
|
+
predicate: this._predicate,
|
|
1108
|
+
target: this._target,
|
|
1109
|
+
} as unknown as Binding<Value>;
|
|
1110
|
+
this._committed = this._commit(binding as Binding, previousId);
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
// ── Container static ──────────────────────────────────────────────────────────
|
|
1115
|
+
|
|
1116
|
+
/**
|
|
1117
|
+
* @since 0.3.16-canary.0
|
|
1118
|
+
*/
|
|
1119
|
+
export const Container: ContainerStatic & { create(): Container } = {
|
|
1120
|
+
create(): Container {
|
|
1121
|
+
return new DefaultContainer();
|
|
1122
|
+
},
|
|
1123
|
+
|
|
1124
|
+
fromModules(...modules: Array<SyncModule>): Container {
|
|
1125
|
+
const container = new DefaultContainer();
|
|
1126
|
+
container.load(...modules);
|
|
1127
|
+
return container;
|
|
1128
|
+
},
|
|
1129
|
+
|
|
1130
|
+
async fromModulesAsync(...modules: Array<SyncModule | AsyncModule>): Promise<Container> {
|
|
1131
|
+
const container = new DefaultContainer();
|
|
1132
|
+
await container.loadAsync(...modules);
|
|
1133
|
+
return container;
|
|
1134
|
+
},
|
|
1135
|
+
};
|