@codefast/di 0.5.0-canary.7 → 0.5.0-canary.9
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 +205 -0
- package/README.md +6 -2
- package/dist/binding.d.ts +85 -24
- package/dist/binding.d.ts.map +1 -1
- package/dist/binding.js +55 -0
- package/dist/binding.js.map +1 -1
- package/dist/constructor-type.d.ts +4 -5
- package/dist/constructor-type.d.ts.map +1 -1
- package/dist/container/binding-builders.d.ts +32 -11
- package/dist/container/binding-builders.d.ts.map +1 -1
- package/dist/container/binding-builders.js +144 -192
- package/dist/container/binding-builders.js.map +1 -1
- package/dist/container/container.d.ts.map +1 -1
- package/dist/container/container.js +141 -201
- package/dist/container/container.js.map +1 -1
- package/dist/decorators/inject.d.ts +2 -4
- package/dist/decorators/inject.d.ts.map +1 -1
- package/dist/decorators/inject.js.map +1 -1
- package/dist/errors.d.ts +24 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +30 -0
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +1 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/introspection/inspector.d.ts +0 -1
- package/dist/introspection/inspector.d.ts.map +1 -1
- package/dist/introspection/inspector.js +3 -8
- package/dist/introspection/inspector.js.map +1 -1
- package/dist/metadata/metadata-keys.d.ts +3 -6
- package/dist/metadata/metadata-keys.d.ts.map +1 -1
- package/dist/metadata/metadata-keys.js +3 -6
- package/dist/metadata/metadata-keys.js.map +1 -1
- package/dist/registry.d.ts +14 -2
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +81 -78
- package/dist/registry.js.map +1 -1
- package/dist/resolution/activation-need.d.ts +27 -0
- package/dist/resolution/activation-need.d.ts.map +1 -0
- package/dist/resolution/activation-need.js +68 -0
- package/dist/resolution/activation-need.js.map +1 -0
- package/dist/resolution/binding-lookup-cache.d.ts +41 -0
- package/dist/resolution/binding-lookup-cache.d.ts.map +1 -0
- package/dist/resolution/binding-lookup-cache.js +118 -0
- package/dist/resolution/binding-lookup-cache.js.map +1 -0
- package/dist/resolution/binding-scope.d.ts +5 -2
- package/dist/resolution/binding-scope.d.ts.map +1 -1
- package/dist/resolution/binding-scope.js +6 -17
- package/dist/resolution/binding-scope.js.map +1 -1
- package/dist/resolution/binding-select.d.ts +8 -1
- package/dist/resolution/binding-select.d.ts.map +1 -1
- package/dist/resolution/binding-select.js +14 -36
- package/dist/resolution/binding-select.js.map +1 -1
- package/dist/resolution/class-introspector.d.ts +27 -0
- package/dist/resolution/class-introspector.d.ts.map +1 -0
- package/dist/resolution/class-introspector.js +60 -0
- package/dist/resolution/class-introspector.js.map +1 -0
- package/dist/resolution/diagnostics.d.ts +41 -0
- package/dist/resolution/diagnostics.d.ts.map +1 -0
- package/dist/resolution/diagnostics.js +18 -0
- package/dist/resolution/diagnostics.js.map +1 -0
- package/dist/resolution/environment.d.ts +48 -1
- package/dist/resolution/environment.d.ts.map +1 -1
- package/dist/resolution/environment.js +134 -5
- package/dist/resolution/environment.js.map +1 -1
- package/dist/resolution/instantiation-plan.d.ts +15 -15
- package/dist/resolution/instantiation-plan.d.ts.map +1 -1
- package/dist/resolution/instantiation-plan.js +69 -49
- package/dist/resolution/instantiation-plan.js.map +1 -1
- package/dist/resolution/lifecycle.d.ts +2 -0
- package/dist/resolution/lifecycle.d.ts.map +1 -1
- package/dist/resolution/lifecycle.js +60 -62
- package/dist/resolution/lifecycle.js.map +1 -1
- package/dist/resolution/resolution-path.d.ts +84 -17
- package/dist/resolution/resolution-path.d.ts.map +1 -1
- package/dist/resolution/resolution-path.js +68 -23
- package/dist/resolution/resolution-path.js.map +1 -1
- package/dist/resolution/resolve-options.d.ts +41 -4
- package/dist/resolution/resolve-options.d.ts.map +1 -1
- package/dist/resolution/resolve-options.js +25 -1
- package/dist/resolution/resolve-options.js.map +1 -1
- package/dist/resolution/resolver.d.ts +33 -10
- package/dist/resolution/resolver.d.ts.map +1 -1
- package/dist/resolution/resolver.js +471 -830
- package/dist/resolution/resolver.js.map +1 -1
- package/dist/resolution/scope.d.ts +11 -15
- package/dist/resolution/scope.d.ts.map +1 -1
- package/dist/resolution/scope.js +55 -43
- package/dist/resolution/scope.js.map +1 -1
- package/package.json +10 -106
- package/src/binding.ts +146 -24
- package/src/constructor-type.ts +4 -5
- package/src/container/binding-builders.ts +184 -284
- package/src/container/container.ts +161 -221
- package/src/decorators/inject.ts +3 -5
- package/src/errors.ts +38 -0
- package/src/index.ts +4 -1
- package/src/introspection/inspector.ts +3 -9
- package/src/metadata/metadata-keys.ts +3 -6
- package/src/registry.ts +90 -94
- package/src/resolution/activation-need.ts +85 -0
- package/src/resolution/binding-lookup-cache.ts +148 -0
- package/src/resolution/binding-scope.ts +6 -17
- package/src/resolution/binding-select.ts +15 -39
- package/src/resolution/class-introspector.ts +74 -0
- package/src/resolution/diagnostics.ts +43 -0
- package/src/resolution/environment.ts +181 -5
- package/src/resolution/instantiation-plan.ts +116 -64
- package/src/resolution/lifecycle.ts +69 -62
- package/src/resolution/resolution-path.ts +122 -43
- package/src/resolution/resolve-options.ts +51 -4
- package/src/resolution/resolver.ts +649 -1081
- package/src/resolution/scope.ts +58 -47
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Binding, BindToBuilder } from "#/binding";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { NO_INSTANCE } from "#/binding";
|
|
3
|
+
import type { BindingRegistration } from "#/container/binding-builders";
|
|
4
|
+
import { BindingChain } from "#/container/binding-builders";
|
|
4
5
|
import type { AutoRegisterRegistry } from "#/decorators/injectable";
|
|
5
6
|
import {
|
|
6
7
|
AsyncModuleLoadError,
|
|
@@ -23,7 +24,11 @@ import type { AsyncModuleBuilder } from "#/module";
|
|
|
23
24
|
import { isSyncModule, MODULE_SETUP } from "#/module";
|
|
24
25
|
import { BindingRegistry } from "#/registry";
|
|
25
26
|
import { effectiveBindingScope } from "#/resolution/binding-scope";
|
|
27
|
+
import type { ResolutionDiagnostics } from "#/resolution/diagnostics";
|
|
28
|
+
import { RESOLUTION_DIAGNOSTICS } from "#/resolution/diagnostics";
|
|
26
29
|
import { LifecycleManager } from "#/resolution/lifecycle";
|
|
30
|
+
import { ROOT_BRANCH } from "#/resolution/resolution-path";
|
|
31
|
+
import type { DependencySlot } from "#/resolution/resolve-options";
|
|
27
32
|
import { injectionSlotToResolveOptions, bindingSlotToResolveOptions } from "#/resolution/resolve-options";
|
|
28
33
|
import { DependencyResolver } from "#/resolution/resolver";
|
|
29
34
|
import { ScopeManager } from "#/resolution/scope";
|
|
@@ -38,17 +43,6 @@ import type {
|
|
|
38
43
|
ResolveOptions,
|
|
39
44
|
} from "#/types";
|
|
40
45
|
|
|
41
|
-
// True when re-adding `restored` would immediately be displaced again by `current`
|
|
42
|
-
// (both slot-based with equal slots) — in that case the replacement was legitimate.
|
|
43
|
-
function displacesRestoredBinding(current: Binding, restored: Binding): boolean {
|
|
44
|
-
const currentIsPurePredicate =
|
|
45
|
-
current.predicate !== undefined && current.slot.name === undefined && current.slot.tags.length === 0;
|
|
46
|
-
if (currentIsPurePredicate) {
|
|
47
|
-
return false;
|
|
48
|
-
}
|
|
49
|
-
return bindingSlotEquals(current.slot, restored.slot);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
46
|
// ── Container interface ────────────────────────────────────────────────────────
|
|
53
47
|
|
|
54
48
|
/**
|
|
@@ -119,23 +113,54 @@ class DefaultContainer implements Container {
|
|
|
119
113
|
readonly #scope: ScopeManager;
|
|
120
114
|
readonly #lifecycle: LifecycleManager;
|
|
121
115
|
#resolver!: DependencyResolver;
|
|
122
|
-
|
|
116
|
+
// Built on the first introspecting call — a container that only binds and resolves never needs it.
|
|
117
|
+
#inspector: Inspector | undefined;
|
|
123
118
|
readonly #parent: DefaultContainer | undefined;
|
|
124
119
|
|
|
125
|
-
// Module tracking: module -> ref count
|
|
126
|
-
|
|
120
|
+
// Module tracking: module -> ref count. Both tables stay unallocated until a module is loaded.
|
|
121
|
+
#moduleRefs: Map<object, number> | undefined;
|
|
127
122
|
// Module bindings: module -> array of binding IDs registered by it
|
|
128
|
-
|
|
123
|
+
#moduleBindingIds: Map<object, Array<BindingIdentifier>> | undefined;
|
|
124
|
+
// One shared registration for every chain this container's own `bind()` creates.
|
|
125
|
+
#registration: BindingRegistration | undefined;
|
|
129
126
|
|
|
130
127
|
constructor(parent?: DefaultContainer) {
|
|
131
128
|
this.#parent = parent;
|
|
132
129
|
this.#registry = new BindingRegistry();
|
|
133
130
|
this.#scope = new ScopeManager(parent !== undefined);
|
|
134
131
|
this.#lifecycle = new LifecycleManager();
|
|
135
|
-
this.#inspector = new Inspector(this.#registry, this.#scope, parent !== undefined, () => this.#disposed);
|
|
136
132
|
this.#initResolver();
|
|
137
133
|
}
|
|
138
134
|
|
|
135
|
+
#getInspector(): Inspector {
|
|
136
|
+
return (this.#inspector ??= new Inspector(
|
|
137
|
+
this.#registry,
|
|
138
|
+
this.#scope,
|
|
139
|
+
this.#parent !== undefined,
|
|
140
|
+
() => this.#disposed,
|
|
141
|
+
));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
[RESOLUTION_DIAGNOSTICS](): ResolutionDiagnostics {
|
|
145
|
+
const builtSubsystems: Array<string> = [];
|
|
146
|
+
if (this.#inspector !== undefined) {
|
|
147
|
+
builtSubsystems.push("container.inspector");
|
|
148
|
+
}
|
|
149
|
+
if (this.#moduleRefs !== undefined || this.#moduleBindingIds !== undefined) {
|
|
150
|
+
builtSubsystems.push("container.moduleTables");
|
|
151
|
+
}
|
|
152
|
+
if (this.#registry.isBuilt) {
|
|
153
|
+
builtSubsystems.push("registry.namedIndex");
|
|
154
|
+
}
|
|
155
|
+
if (this.#scope.isBuilt) {
|
|
156
|
+
builtSubsystems.push("scope.scoped");
|
|
157
|
+
}
|
|
158
|
+
if (this.#lifecycle.isBuilt) {
|
|
159
|
+
builtSubsystems.push("lifecycle.activationHooks");
|
|
160
|
+
}
|
|
161
|
+
return { ...this.#resolver.describeCaches(), scopedInstanceCount: this.#scope.scopedCount, builtSubsystems };
|
|
162
|
+
}
|
|
163
|
+
|
|
139
164
|
#initResolver(): void {
|
|
140
165
|
const metadataReader = this.#getMetadataReader();
|
|
141
166
|
const parentResolver = this.#parent === undefined ? undefined : this.#parent.#resolver;
|
|
@@ -176,63 +201,24 @@ class DefaultContainer implements Container {
|
|
|
176
201
|
return this.#createBindToBuilder(token);
|
|
177
202
|
}
|
|
178
203
|
|
|
204
|
+
/** The registration every non-module chain shares, so `bind()` allocates only the builder. */
|
|
205
|
+
#ownRegistration(): BindingRegistration {
|
|
206
|
+
return (this.#registration ??= { registry: this.#registry, moduleBindingIds: undefined });
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** One registration per module load, holding that module's id list directly. */
|
|
210
|
+
#moduleRegistration(moduleRef: object): BindingRegistration {
|
|
211
|
+
return {
|
|
212
|
+
registry: this.#registry,
|
|
213
|
+
moduleBindingIds: (this.#moduleBindingIds ??= new Map()).getOrInsert(moduleRef, []),
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
179
217
|
#createBindToBuilder<const Value>(
|
|
180
218
|
token: Token<Value> | Constructor<Value>,
|
|
181
|
-
|
|
219
|
+
registration: BindingRegistration = this.#ownRegistration(),
|
|
182
220
|
): BindToBuilder<Value> {
|
|
183
|
-
|
|
184
|
-
// Bindings displaced by this fluent chain's commits — each stays restorable
|
|
185
|
-
// until the chain settles on a shape that genuinely conflicts with it. A list,
|
|
186
|
-
// because one chain can displace several bindings (the default at an
|
|
187
|
-
// intermediate commit, then a named/tagged binding at the final one).
|
|
188
|
-
const displacedByChain: Array<Binding> = [];
|
|
189
|
-
|
|
190
|
-
const commitBinding = <BindingValue>(
|
|
191
|
-
binding: Binding<BindingValue>,
|
|
192
|
-
previousId?: BindingIdentifier,
|
|
193
|
-
): BindingIdentifier => {
|
|
194
|
-
if (previousId !== undefined) {
|
|
195
|
-
registry.removeById(previousId);
|
|
196
|
-
if (moduleRef !== undefined) {
|
|
197
|
-
const ids = this.#moduleBindingIds.get(moduleRef);
|
|
198
|
-
if (ids !== undefined) {
|
|
199
|
-
const idx = ids.indexOf(previousId);
|
|
200
|
-
if (idx !== -1) {
|
|
201
|
-
ids.splice(idx, 1);
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
// The registry stores value-erased bindings — this is the single erasure point for the builder chain.
|
|
207
|
-
const displaced = registry.add(binding as Binding);
|
|
208
|
-
if (displaced !== undefined) {
|
|
209
|
-
// A fluent chain commits eagerly on each refinement, so an intermediate
|
|
210
|
-
// commit can displace a binding that the final shape would never conflict
|
|
211
|
-
// with. Remember it so a later re-commit that morphs away (named/tagged
|
|
212
|
-
// slot, pure predicate) can restore it.
|
|
213
|
-
displacedByChain.push(displaced);
|
|
214
|
-
}
|
|
215
|
-
if (previousId !== undefined && displacedByChain.length > 0) {
|
|
216
|
-
for (let index = displacedByChain.length - 1; index >= 0; index -= 1) {
|
|
217
|
-
const candidate = displacedByChain[index]!;
|
|
218
|
-
if (!displacesRestoredBinding(binding as Binding, candidate)) {
|
|
219
|
-
registry.add(candidate);
|
|
220
|
-
displacedByChain.splice(index, 1);
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
if (moduleRef !== undefined) {
|
|
225
|
-
let ids = this.#moduleBindingIds.get(moduleRef);
|
|
226
|
-
if (ids === undefined) {
|
|
227
|
-
ids = [];
|
|
228
|
-
this.#moduleBindingIds.set(moduleRef, ids);
|
|
229
|
-
}
|
|
230
|
-
ids.push(binding.id);
|
|
231
|
-
}
|
|
232
|
-
return binding.id;
|
|
233
|
-
};
|
|
234
|
-
|
|
235
|
-
return new BindingEntry<Value>(token, commitBinding);
|
|
221
|
+
return new BindingChain<Value>(token, registration);
|
|
236
222
|
}
|
|
237
223
|
|
|
238
224
|
unbind(tokenOrId: Token<unknown> | Constructor | BindingIdentifier): void {
|
|
@@ -251,14 +237,15 @@ class DefaultContainer implements Container {
|
|
|
251
237
|
return this.#drainSingletons(this.#registry.removeByToken(tokenOrId));
|
|
252
238
|
}
|
|
253
239
|
|
|
254
|
-
/** Drain
|
|
240
|
+
/** Drain scope entries for already-removed bindings; only singletons yield deactivation pairs. */
|
|
255
241
|
#drainSingletons(bindings: ReadonlyArray<Binding>): Array<[Binding, unknown]> {
|
|
256
242
|
const pairs: Array<[Binding, unknown]> = [];
|
|
257
243
|
for (const binding of bindings) {
|
|
258
|
-
if (
|
|
259
|
-
pairs.push([binding,
|
|
260
|
-
this.#scope.deleteSingleton(binding
|
|
244
|
+
if (binding.instance !== NO_INSTANCE) {
|
|
245
|
+
pairs.push([binding, binding.instance]);
|
|
246
|
+
this.#scope.deleteSingleton(binding);
|
|
261
247
|
}
|
|
248
|
+
this.#scope.deleteScoped(binding.id);
|
|
262
249
|
}
|
|
263
250
|
return pairs;
|
|
264
251
|
}
|
|
@@ -311,45 +298,26 @@ class DefaultContainer implements Container {
|
|
|
311
298
|
this.#loadSyncModules(modules);
|
|
312
299
|
}
|
|
313
300
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
for (const module of
|
|
301
|
+
// Imports nested inside a module's setup re-enter here through the builder, so a module listed
|
|
302
|
+
// twice in one call is deduped by identity and the rest is ref-counting.
|
|
303
|
+
#loadSyncModules(modules: ReadonlyArray<SyncModule | AsyncModule>): void {
|
|
304
|
+
for (const module of new Set(modules)) {
|
|
318
305
|
if (!isSyncModule(module)) {
|
|
319
306
|
throw new AsyncModuleLoadError(module.name);
|
|
320
307
|
}
|
|
321
308
|
const moduleRef = module as object;
|
|
322
|
-
const
|
|
309
|
+
const moduleRefs = (this.#moduleRefs ??= new Map());
|
|
310
|
+
const existing = moduleRefs.get(moduleRef);
|
|
323
311
|
if (existing !== undefined) {
|
|
324
|
-
|
|
312
|
+
moduleRefs.set(moduleRef, existing + 1);
|
|
325
313
|
continue;
|
|
326
314
|
}
|
|
327
|
-
|
|
315
|
+
moduleRefs.set(moduleRef, 1);
|
|
328
316
|
const builder = this.#createModuleBuilder(moduleRef);
|
|
329
317
|
module[MODULE_SETUP](builder);
|
|
330
318
|
}
|
|
331
319
|
}
|
|
332
320
|
|
|
333
|
-
#collectModuleDeps(modules: Array<SyncModule | AsyncModule>): Array<SyncModule | AsyncModule> {
|
|
334
|
-
const seen = new Set<object>();
|
|
335
|
-
const result: Array<SyncModule | AsyncModule> = [];
|
|
336
|
-
|
|
337
|
-
const visit = (module: SyncModule | AsyncModule): void => {
|
|
338
|
-
const moduleRef = module as object;
|
|
339
|
-
if (seen.has(moduleRef)) {
|
|
340
|
-
return;
|
|
341
|
-
}
|
|
342
|
-
seen.add(moduleRef);
|
|
343
|
-
// We'll collect deps during setup via the builder's import()
|
|
344
|
-
result.push(module);
|
|
345
|
-
};
|
|
346
|
-
|
|
347
|
-
for (const module of modules) {
|
|
348
|
-
visit(module);
|
|
349
|
-
}
|
|
350
|
-
return result;
|
|
351
|
-
}
|
|
352
|
-
|
|
353
321
|
async loadAsync(...modules: Array<SyncModule | AsyncModule>): Promise<void> {
|
|
354
322
|
this.#assertNotDisposed();
|
|
355
323
|
for (const module of modules) {
|
|
@@ -359,12 +327,13 @@ class DefaultContainer implements Container {
|
|
|
359
327
|
|
|
360
328
|
async #loadOneModuleAsync(module: SyncModule | AsyncModule): Promise<void> {
|
|
361
329
|
const moduleRef = module as object;
|
|
362
|
-
const
|
|
330
|
+
const moduleRefs = (this.#moduleRefs ??= new Map());
|
|
331
|
+
const existing = moduleRefs.get(moduleRef);
|
|
363
332
|
if (existing !== undefined) {
|
|
364
|
-
|
|
333
|
+
moduleRefs.set(moduleRef, existing + 1);
|
|
365
334
|
return;
|
|
366
335
|
}
|
|
367
|
-
|
|
336
|
+
moduleRefs.set(moduleRef, 1);
|
|
368
337
|
|
|
369
338
|
if (isSyncModule(module)) {
|
|
370
339
|
const builder = this.#createModuleBuilder(moduleRef);
|
|
@@ -381,9 +350,10 @@ class DefaultContainer implements Container {
|
|
|
381
350
|
}
|
|
382
351
|
|
|
383
352
|
#createModuleBuilder(moduleRef: object): ModuleBuilder {
|
|
353
|
+
const registration = this.#moduleRegistration(moduleRef);
|
|
384
354
|
return {
|
|
385
355
|
bind: <const Value>(token: Token<Value> | Constructor<Value>): BindToBuilder<Value> =>
|
|
386
|
-
this.#createBindToBuilder(token,
|
|
356
|
+
this.#createBindToBuilder(token, registration),
|
|
387
357
|
import: (...modules: Array<SyncModule>): void => {
|
|
388
358
|
this.#loadSyncModules(modules);
|
|
389
359
|
},
|
|
@@ -391,9 +361,10 @@ class DefaultContainer implements Container {
|
|
|
391
361
|
}
|
|
392
362
|
|
|
393
363
|
#createAsyncModuleBuilder(moduleRef: object, importPromises: Array<Promise<void>>): AsyncModuleBuilder {
|
|
364
|
+
const registration = this.#moduleRegistration(moduleRef);
|
|
394
365
|
return {
|
|
395
366
|
bind: <const Value>(token: Token<Value> | Constructor<Value>): BindToBuilder<Value> =>
|
|
396
|
-
this.#createBindToBuilder(token,
|
|
367
|
+
this.#createBindToBuilder(token, registration),
|
|
397
368
|
import: (...modules: Array<SyncModule | AsyncModule>): void => {
|
|
398
369
|
for (const module of modules) {
|
|
399
370
|
importPromises.push(this.#loadOneModuleAsync(module));
|
|
@@ -411,32 +382,33 @@ class DefaultContainer implements Container {
|
|
|
411
382
|
|
|
412
383
|
/** Unregister module bindings and collect [binding, instance] pairs for deactivation. */
|
|
413
384
|
#removeModuleBindings(ref: object): Array<[Binding, unknown]> {
|
|
414
|
-
this.#moduleRefs
|
|
415
|
-
const ids = this.#moduleBindingIds
|
|
416
|
-
this.#moduleBindingIds
|
|
385
|
+
this.#moduleRefs?.delete(ref);
|
|
386
|
+
const ids = this.#moduleBindingIds?.get(ref) ?? [];
|
|
387
|
+
this.#moduleBindingIds?.delete(ref);
|
|
417
388
|
const pairs: Array<[Binding, unknown]> = [];
|
|
418
389
|
for (const id of ids) {
|
|
419
390
|
const binding = this.#registry.getById(id);
|
|
420
391
|
if (binding !== undefined) {
|
|
421
392
|
this.#registry.removeById(id);
|
|
422
|
-
if (
|
|
423
|
-
pairs.push([binding,
|
|
424
|
-
this.#scope.deleteSingleton(
|
|
393
|
+
if (binding.instance !== NO_INSTANCE) {
|
|
394
|
+
pairs.push([binding, binding.instance]);
|
|
395
|
+
this.#scope.deleteSingleton(binding);
|
|
425
396
|
}
|
|
397
|
+
this.#scope.deleteScoped(binding.id);
|
|
426
398
|
}
|
|
427
399
|
}
|
|
428
400
|
return pairs;
|
|
429
401
|
}
|
|
430
402
|
|
|
431
403
|
#unloadModuleSync(ref: object): void {
|
|
432
|
-
const count = this.#moduleRefs
|
|
404
|
+
const count = this.#moduleRefs?.get(ref) ?? 0;
|
|
433
405
|
if (count <= 1) {
|
|
434
406
|
const reader = this.#getMetadataReader();
|
|
435
407
|
for (const [binding, instance] of this.#removeModuleBindings(ref)) {
|
|
436
408
|
this.#lifecycle.runDeactivationSync(binding, instance, reader);
|
|
437
409
|
}
|
|
438
410
|
} else {
|
|
439
|
-
this.#moduleRefs
|
|
411
|
+
this.#moduleRefs!.set(ref, count - 1);
|
|
440
412
|
}
|
|
441
413
|
}
|
|
442
414
|
|
|
@@ -448,14 +420,14 @@ class DefaultContainer implements Container {
|
|
|
448
420
|
}
|
|
449
421
|
|
|
450
422
|
async #unloadModuleAsync(ref: object): Promise<void> {
|
|
451
|
-
const count = this.#moduleRefs
|
|
423
|
+
const count = this.#moduleRefs?.get(ref) ?? 0;
|
|
452
424
|
if (count <= 1) {
|
|
453
425
|
const reader = this.#getMetadataReader();
|
|
454
426
|
for (const [binding, instance] of this.#removeModuleBindings(ref)) {
|
|
455
427
|
await this.#lifecycle.runDeactivation(binding, instance, reader);
|
|
456
428
|
}
|
|
457
429
|
} else {
|
|
458
|
-
this.#moduleRefs
|
|
430
|
+
this.#moduleRefs!.set(ref, count - 1);
|
|
459
431
|
}
|
|
460
432
|
}
|
|
461
433
|
|
|
@@ -492,23 +464,33 @@ class DefaultContainer implements Container {
|
|
|
492
464
|
|
|
493
465
|
resolve<const Value>(token: Token<Value> | Constructor<Value>, options?: ResolveOptions): Value {
|
|
494
466
|
this.#assertNotDisposed();
|
|
467
|
+
const rootStack = this.#resolver.rootStack;
|
|
468
|
+
// A resolve already holding the shared pair means this one is nested; it mints its own.
|
|
469
|
+
if (rootStack.length !== 0) {
|
|
470
|
+
return options === undefined
|
|
471
|
+
? this.#resolver.resolveFromContext(token, [], [])
|
|
472
|
+
: this.#resolver.resolve(token, options, [], []);
|
|
473
|
+
}
|
|
495
474
|
if (options === undefined) {
|
|
496
|
-
return this.#resolver.resolveFromContext(token,
|
|
475
|
+
return this.#resolver.resolveFromContext(token, this.#resolver.rootPath, rootStack);
|
|
497
476
|
}
|
|
498
|
-
return this.#resolver.resolve(token, options,
|
|
477
|
+
return this.#resolver.resolve(token, options, this.#resolver.rootPath, rootStack);
|
|
499
478
|
}
|
|
500
479
|
|
|
501
480
|
resolveAsync<const Value>(token: Token<Value> | Constructor<Value>, options?: ResolveOptions): Promise<Value> {
|
|
502
481
|
this.#assertNotDisposed();
|
|
503
482
|
if (options === undefined) {
|
|
504
|
-
return this.#resolver.
|
|
483
|
+
return this.#resolver.resolveAsyncFromRoot(token) as Promise<Value>;
|
|
505
484
|
}
|
|
506
|
-
return this.#resolver.resolveAsync(token, options, [], []);
|
|
485
|
+
return this.#resolver.resolveAsync(token, options, [], [], ROOT_BRANCH);
|
|
507
486
|
}
|
|
508
487
|
|
|
509
488
|
resolveOptional<const Value>(token: Token<Value> | Constructor<Value>, options?: ResolveOptions): Value | undefined {
|
|
510
489
|
this.#assertNotDisposed();
|
|
511
|
-
|
|
490
|
+
const rootStack = this.#resolver.rootStack;
|
|
491
|
+
return rootStack.length === 0
|
|
492
|
+
? this.#resolver.resolveOptional(token, options, this.#resolver.rootPath, rootStack)
|
|
493
|
+
: this.#resolver.resolveOptional(token, options, [], []);
|
|
512
494
|
}
|
|
513
495
|
|
|
514
496
|
resolveOptionalAsync<const Value>(
|
|
@@ -516,12 +498,15 @@ class DefaultContainer implements Container {
|
|
|
516
498
|
options?: ResolveOptions,
|
|
517
499
|
): Promise<Value | undefined> {
|
|
518
500
|
this.#assertNotDisposed();
|
|
519
|
-
return this.#resolver.resolveOptionalAsync(token, options, [], []);
|
|
501
|
+
return this.#resolver.resolveOptionalAsync(token, options, [], [], ROOT_BRANCH);
|
|
520
502
|
}
|
|
521
503
|
|
|
522
504
|
resolveAll<const Value>(token: Token<Value> | Constructor<Value>, options?: ResolveOptions): Array<Value> {
|
|
523
505
|
this.#assertNotDisposed();
|
|
524
|
-
|
|
506
|
+
const rootStack = this.#resolver.rootStack;
|
|
507
|
+
return rootStack.length === 0
|
|
508
|
+
? this.#resolver.resolveAll(token, options, this.#resolver.rootPath, rootStack)
|
|
509
|
+
: this.#resolver.resolveAll(token, options, [], []);
|
|
525
510
|
}
|
|
526
511
|
|
|
527
512
|
resolveAllAsync<const Value>(
|
|
@@ -529,7 +514,7 @@ class DefaultContainer implements Container {
|
|
|
529
514
|
options?: ResolveOptions,
|
|
530
515
|
): Promise<Array<Value>> {
|
|
531
516
|
this.#assertNotDisposed();
|
|
532
|
-
return this.#resolver.resolveAllAsync(token, options, [], []);
|
|
517
|
+
return this.#resolver.resolveAllAsync(token, options, [], [], ROOT_BRANCH);
|
|
533
518
|
}
|
|
534
519
|
|
|
535
520
|
// ── Child ─────────────────────────────────────────────────────────────────
|
|
@@ -549,11 +534,10 @@ class DefaultContainer implements Container {
|
|
|
549
534
|
|
|
550
535
|
// Deactivate all singletons in this container (own only)
|
|
551
536
|
const reader = this.#getMetadataReader();
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
}
|
|
537
|
+
// Iterate a copy: a deactivation handler is user code, and the live list is what
|
|
538
|
+
// materializing or dropping a singleton mutates.
|
|
539
|
+
for (const binding of this.#scope.cachedSingletons().slice()) {
|
|
540
|
+
await this.#lifecycle.runDeactivation(binding, binding.instance, reader);
|
|
557
541
|
}
|
|
558
542
|
|
|
559
543
|
this.#scope.clearAll();
|
|
@@ -577,7 +561,7 @@ class DefaultContainer implements Container {
|
|
|
577
561
|
continue;
|
|
578
562
|
}
|
|
579
563
|
const scope = effectiveBindingScope(binding);
|
|
580
|
-
if (scope === "singleton" &&
|
|
564
|
+
if (scope === "singleton" && binding.instance === NO_INSTANCE) {
|
|
581
565
|
if (binding.predicate !== undefined) {
|
|
582
566
|
continue;
|
|
583
567
|
}
|
|
@@ -633,10 +617,7 @@ class DefaultContainer implements Container {
|
|
|
633
617
|
for (const edge of this.#collectStaticDependencyEdges(current, reader)) {
|
|
634
618
|
const { terminal, depTokenName } = edge;
|
|
635
619
|
const depScope = this.#validationScopeFromTerminal(terminal);
|
|
636
|
-
if (depScope
|
|
637
|
-
continue;
|
|
638
|
-
}
|
|
639
|
-
if (depScope === "scoped" || depScope === "transient") {
|
|
620
|
+
if (depScope !== "singleton") {
|
|
640
621
|
throw new ScopeViolationError({
|
|
641
622
|
consumerToken: rootName,
|
|
642
623
|
consumerScope: "singleton",
|
|
@@ -654,27 +635,18 @@ class DefaultContainer implements Container {
|
|
|
654
635
|
dfs(root, [rootName], new Set());
|
|
655
636
|
}
|
|
656
637
|
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
case "class":
|
|
668
|
-
case "resolved":
|
|
669
|
-
case "resolved-async":
|
|
670
|
-
return terminal.scope;
|
|
671
|
-
case "alias":
|
|
672
|
-
throw new InternalError("validate: expected terminal binding after alias resolution");
|
|
673
|
-
default: {
|
|
674
|
-
const exhaustive: never = terminal;
|
|
675
|
-
return exhaustive;
|
|
676
|
-
}
|
|
638
|
+
/**
|
|
639
|
+
* The scope this edge is judged against.
|
|
640
|
+
*
|
|
641
|
+
* @remarks A factory's *body* is not statically analyzable, but the scope it was bound with is
|
|
642
|
+
* declared like any other — so the captive-dependency check applies to it too. The DFS still
|
|
643
|
+
* refuses to descend into a factory; only the edge is judged.
|
|
644
|
+
*/
|
|
645
|
+
#validationScopeFromTerminal(terminal: Binding): BindingScope {
|
|
646
|
+
if (terminal.kind === "alias") {
|
|
647
|
+
throw new InternalError("validate: expected terminal binding after alias resolution");
|
|
677
648
|
}
|
|
649
|
+
return terminal.scope;
|
|
678
650
|
}
|
|
679
651
|
|
|
680
652
|
#followAliasChainToTerminal(binding: Binding, options: ResolveOptions | undefined): Binding | undefined {
|
|
@@ -698,75 +670,43 @@ class DefaultContainer implements Container {
|
|
|
698
670
|
return current;
|
|
699
671
|
}
|
|
700
672
|
|
|
673
|
+
/** What one dependency could resolve to: every candidate for `injectAll`, else at most one. */
|
|
674
|
+
#peekDependencyCandidates(dep: DependencySlot, options: ResolveOptions | undefined): ReadonlyArray<Binding> {
|
|
675
|
+
if (dep.multi) {
|
|
676
|
+
return this.#resolver.peekCandidateBindingsForValidate(dep.token, options);
|
|
677
|
+
}
|
|
678
|
+
const found = this.#resolver.peekBindingForValidate(dep.token, options);
|
|
679
|
+
return found === undefined ? [] : [found.binding];
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
/** What a binding declares up front — a class's params, a factory's descriptors, else nothing. */
|
|
683
|
+
#staticDependencies(binding: Binding, reader: MetadataReader): ReadonlyArray<DependencySlot> {
|
|
684
|
+
if (binding.kind === "class") {
|
|
685
|
+
return reader.getConstructorMetadata(binding.target as Constructor)?.params ?? [];
|
|
686
|
+
}
|
|
687
|
+
if (binding.kind === "resolved" || binding.kind === "resolved-async") {
|
|
688
|
+
return binding.deps;
|
|
689
|
+
}
|
|
690
|
+
return [];
|
|
691
|
+
}
|
|
692
|
+
|
|
701
693
|
#collectStaticDependencyEdges(
|
|
702
694
|
binding: Binding,
|
|
703
695
|
reader: MetadataReader,
|
|
704
696
|
): Array<{ terminal: Binding; depTokenName: string }> {
|
|
705
697
|
const edges: Array<{ terminal: Binding; depTokenName: string }> = [];
|
|
706
698
|
|
|
707
|
-
const
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
edges.push({ terminal, depTokenName: displayName });
|
|
712
|
-
};
|
|
713
|
-
|
|
714
|
-
if (binding.kind === "class") {
|
|
715
|
-
const meta = reader.getConstructorMetadata(binding.target as Constructor);
|
|
716
|
-
if (meta === undefined) {
|
|
717
|
-
return edges;
|
|
718
|
-
}
|
|
719
|
-
for (const param of meta.params) {
|
|
720
|
-
const paramOptions = injectionSlotToResolveOptions(param);
|
|
721
|
-
if (param.optional) {
|
|
722
|
-
continue;
|
|
723
|
-
}
|
|
724
|
-
const tokenRef = param.token;
|
|
725
|
-
if (param.multi) {
|
|
726
|
-
const candidates = this.#resolver.peekCandidateBindingsForValidate(tokenRef, paramOptions);
|
|
727
|
-
for (const cand of candidates) {
|
|
728
|
-
const term = this.#followAliasChainToTerminal(cand, paramOptions);
|
|
729
|
-
pushTerminal(term, term !== undefined ? tokenName(term.token as Token<unknown>) : "");
|
|
730
|
-
}
|
|
731
|
-
continue;
|
|
732
|
-
}
|
|
733
|
-
const found =
|
|
734
|
-
paramOptions === undefined
|
|
735
|
-
? this.#resolver.peekBindingForValidate(tokenRef, undefined)
|
|
736
|
-
: this.#resolver.peekBindingForValidate(tokenRef, paramOptions);
|
|
737
|
-
if (found === undefined) {
|
|
738
|
-
continue;
|
|
739
|
-
}
|
|
740
|
-
const term = this.#followAliasChainToTerminal(found.binding, paramOptions);
|
|
741
|
-
pushTerminal(term, term !== undefined ? tokenName(term.token as Token<unknown>) : "");
|
|
699
|
+
for (const dep of this.#staticDependencies(binding, reader)) {
|
|
700
|
+
// An optional dependency imposes no scope constraint: it may legitimately be absent.
|
|
701
|
+
if (dep.optional) {
|
|
702
|
+
continue;
|
|
742
703
|
}
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
const depOptions = injectionSlotToResolveOptions(dep);
|
|
749
|
-
if (dep.optional) {
|
|
750
|
-
continue;
|
|
751
|
-
}
|
|
752
|
-
const tokenRef = dep.token as Token<unknown> | Constructor;
|
|
753
|
-
if (dep.multi) {
|
|
754
|
-
const candidates = this.#resolver.peekCandidateBindingsForValidate(tokenRef, depOptions);
|
|
755
|
-
for (const cand of candidates) {
|
|
756
|
-
const term = this.#followAliasChainToTerminal(cand, depOptions);
|
|
757
|
-
pushTerminal(term, term !== undefined ? tokenName(term.token as Token<unknown>) : "");
|
|
758
|
-
}
|
|
759
|
-
continue;
|
|
760
|
-
}
|
|
761
|
-
const found =
|
|
762
|
-
depOptions === undefined
|
|
763
|
-
? this.#resolver.peekBindingForValidate(tokenRef, undefined)
|
|
764
|
-
: this.#resolver.peekBindingForValidate(tokenRef, depOptions);
|
|
765
|
-
if (found === undefined) {
|
|
766
|
-
continue;
|
|
704
|
+
const depOptions = injectionSlotToResolveOptions(dep);
|
|
705
|
+
for (const candidate of this.#peekDependencyCandidates(dep, depOptions)) {
|
|
706
|
+
const terminal = this.#followAliasChainToTerminal(candidate, depOptions);
|
|
707
|
+
if (terminal !== undefined) {
|
|
708
|
+
edges.push({ terminal, depTokenName: tokenName(terminal.token as Token<unknown>) });
|
|
767
709
|
}
|
|
768
|
-
const term = this.#followAliasChainToTerminal(found.binding, depOptions);
|
|
769
|
-
pushTerminal(term, term !== undefined ? tokenName(term.token as Token<unknown>) : "");
|
|
770
710
|
}
|
|
771
711
|
}
|
|
772
712
|
|
|
@@ -777,22 +717,22 @@ class DefaultContainer implements Container {
|
|
|
777
717
|
|
|
778
718
|
has(token: Token<unknown> | Constructor, options?: ResolveOptions): boolean {
|
|
779
719
|
this.#assertNotDisposed();
|
|
780
|
-
return this.#
|
|
720
|
+
return this.#getInspector().has(token, options, () => this.#parent?.has(token, options) ?? false);
|
|
781
721
|
}
|
|
782
722
|
|
|
783
723
|
hasOwn(token: Token<unknown> | Constructor, options?: ResolveOptions): boolean {
|
|
784
724
|
this.#assertNotDisposed();
|
|
785
|
-
return this.#
|
|
725
|
+
return this.#getInspector().hasOwn(token, options);
|
|
786
726
|
}
|
|
787
727
|
|
|
788
728
|
lookupBindings<const Value>(token: Token<Value> | Constructor<Value>): ReadonlyArray<BindingSnapshot> {
|
|
789
729
|
this.#assertNotDisposed();
|
|
790
|
-
return this.#
|
|
730
|
+
return this.#getInspector().lookupBindings(token);
|
|
791
731
|
}
|
|
792
732
|
|
|
793
733
|
inspect(): ContainerSnapshot {
|
|
794
734
|
this.#assertNotDisposed();
|
|
795
|
-
return this.#
|
|
735
|
+
return this.#getInspector().inspect();
|
|
796
736
|
}
|
|
797
737
|
|
|
798
738
|
generateDependencyGraph(options?: GraphOptions): ContainerGraphJson {
|
package/src/decorators/inject.ts
CHANGED
|
@@ -34,10 +34,8 @@ export type InjectableDependency<Value = unknown> = Token<Value> | Constructor<V
|
|
|
34
34
|
/**
|
|
35
35
|
* The value a factory receives for one declared dependency.
|
|
36
36
|
*
|
|
37
|
-
* @remarks `optional()` and `injectAll()`
|
|
38
|
-
*
|
|
39
|
-
* — so reading that parameter back is enough; bare tokens and constructors fall through to
|
|
40
|
-
* {@link TokenValue}.
|
|
37
|
+
* @remarks `optional()` and `injectAll()` fold their effect into the descriptor's own type
|
|
38
|
+
* parameter, so reading it back is enough; bare tokens fall through to {@link TokenValue}.
|
|
41
39
|
*
|
|
42
40
|
* @since 0.5.0-canary.7
|
|
43
41
|
*/
|
|
@@ -195,7 +193,7 @@ export function inject<const Value>(
|
|
|
195
193
|
}
|
|
196
194
|
Object.defineProperties(decoratorFn, props);
|
|
197
195
|
|
|
198
|
-
return decoratorFn as
|
|
196
|
+
return decoratorFn as InjectionDescriptor<Value> & ClassAccessorDecorator<unknown, Value>;
|
|
199
197
|
}
|
|
200
198
|
|
|
201
199
|
// ── optional() ────────────────────────────────────────────────────────────────
|