@codefast/di 0.11.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +151 -0
- package/README.md +128 -31
- package/dist/container/{binding-builders.d.ts → binding-chain.d.ts} +21 -28
- package/dist/container/{binding-builders.js → binding-chain.js} +53 -163
- package/dist/container/container.d.ts +11 -3
- package/dist/container/container.js +176 -102
- package/dist/core/binding-builders.d.ts +117 -0
- package/dist/core/binding-declaration.d.ts +9 -2
- package/dist/core/binding-declaration.js +5 -4
- package/dist/core/binding.d.ts +21 -149
- package/dist/core/binding.js +15 -25
- package/dist/core/module.d.ts +1 -1
- package/dist/core/registry.d.ts +9 -24
- package/dist/core/registry.js +30 -155
- package/dist/core/tag.js +1 -1
- package/dist/decorators/inject.d.ts +3 -1
- package/dist/decorators/inject.js +5 -5
- package/dist/decorators/injectable.js +2 -2
- package/dist/decorators/lifecycle.d.ts +20 -0
- package/dist/decorators/{lifecycle-decorators.js → lifecycle.js} +7 -3
- package/dist/decorators/{decorator-metadata.js → metadata-record.js} +1 -1
- package/dist/{errors/errors.d.ts → errors.d.ts} +29 -19
- package/dist/{errors/errors.js → errors.js} +43 -25
- package/dist/index.d.ts +12 -10
- package/dist/index.js +6 -6
- package/dist/injection/descriptor.d.ts +1 -1
- package/dist/introspection/dependency-graph.d.ts +1 -1
- package/dist/introspection/dependency-graph.js +3 -4
- package/dist/introspection/explanation.d.ts +101 -0
- package/dist/introspection/explanation.js +169 -0
- package/dist/introspection/inspector.d.ts +9 -2
- package/dist/introspection/inspector.js +25 -20
- package/dist/lifecycle/{lifecycle-manager.d.ts → hooks.d.ts} +11 -2
- package/dist/lifecycle/{lifecycle-manager.js → hooks.js} +51 -28
- package/dist/lifecycle/{scope-manager.d.ts → scopes.d.ts} +23 -6
- package/dist/lifecycle/{scope-manager.js → scopes.js} +93 -20
- package/dist/metadata/{metadata-keys.d.ts → keys.d.ts} +1 -1
- package/dist/metadata/{metadata-keys.js → keys.js} +1 -1
- package/dist/metadata/{metadata-reader-token.d.ts → reader-token.d.ts} +1 -1
- package/dist/metadata/{symbol-metadata-reader.d.ts → symbol-reader.d.ts} +1 -1
- package/dist/metadata/{symbol-metadata-reader.js → symbol-reader.js} +1 -1
- package/dist/metadata/{metadata-types.d.ts → types.d.ts} +1 -1
- package/dist/metadata/types.js +1 -0
- package/dist/metadata/verify.d.ts +43 -0
- package/dist/metadata/verify.js +133 -0
- package/dist/metadata/{verifying-metadata-reader.d.ts → verifying-reader.d.ts} +1 -1
- package/dist/metadata/{verifying-metadata-reader.js → verifying-reader.js} +2 -2
- package/dist/resolution/cache/activation-need.d.ts +1 -1
- package/dist/resolution/cache/activation-need.js +5 -1
- package/dist/resolution/cache/class-introspector.d.ts +27 -36
- package/dist/resolution/cache/class-introspector.js +61 -159
- package/dist/resolution/cache/{binding-lookup-cache.d.ts → lookup.d.ts} +7 -2
- package/dist/resolution/cache/{binding-lookup-cache.js → lookup.js} +40 -8
- package/dist/resolution/context.d.ts +1 -1
- package/dist/resolution/context.js +1 -1
- package/dist/resolution/{path/resolution-path.d.ts → path.d.ts} +12 -2
- package/dist/resolution/{path/resolution-path.js → path.js} +23 -3
- package/dist/resolution/plan/{plan-codegen.d.ts → codegen.d.ts} +2 -4
- package/dist/resolution/plan/{plan-codegen.js → codegen.js} +2 -4
- package/dist/resolution/plan/{instantiation-plan.d.ts → compiler.d.ts} +1 -1
- package/dist/resolution/plan/{instantiation-plan.js → compiler.js} +4 -4
- package/dist/resolution/resolver.d.ts +19 -7
- package/dist/resolution/resolver.js +343 -125
- package/dist/resolution/select/{binding-select.d.ts → candidates.d.ts} +30 -0
- package/dist/resolution/select/{binding-select.js → candidates.js} +51 -10
- package/dist/resolution/select/constraints.js +1 -1
- package/package.json +80 -72
- package/dist/core/binding-scope.d.ts +0 -12
- package/dist/core/binding-scope.js +0 -12
- package/dist/decorators/lifecycle-decorators.d.ts +0 -14
- /package/dist/{ambient/active-container.d.ts → ambient-container.d.ts} +0 -0
- /package/dist/{ambient/active-container.js → ambient-container.js} +0 -0
- /package/dist/{metadata/metadata-types.js → core/binding-builders.js} +0 -0
- /package/dist/decorators/{decorator-metadata.d.ts → metadata-record.d.ts} +0 -0
- /package/dist/injection/{resolve-options.d.ts → dependency-slot.d.ts} +0 -0
- /package/dist/injection/{resolve-options.js → dependency-slot.js} +0 -0
- /package/dist/{errors → introspection}/diagnostics.d.ts +0 -0
- /package/dist/{errors → introspection}/diagnostics.js +0 -0
- /package/dist/metadata/{metadata-reader-token.js → reader-token.js} +0 -0
|
@@ -1,23 +1,23 @@
|
|
|
1
|
-
import { BindingChain } from "#container/binding-
|
|
2
|
-
import { NO_INSTANCE } from "#core/binding";
|
|
3
|
-
import { effectiveBindingScope } from "#core/binding-scope";
|
|
1
|
+
import { BindingChain } from "#container/binding-chain";
|
|
2
|
+
import { effectiveBindingScope, NO_INSTANCE } from "#core/binding";
|
|
4
3
|
import { constraintRequirementsOf } from "#core/constraint-requirement";
|
|
5
4
|
import { getOrInsert, getOrInsertComputed } from "#core/map-upsert";
|
|
6
5
|
import { isSyncModule, MODULE_SETUP } from "#core/module";
|
|
7
6
|
import { BindingRegistry } from "#core/registry";
|
|
8
7
|
import { advanceDisposeEpoch, disposeEpochRef } from "#core/state-epoch";
|
|
9
8
|
import { tokenName } from "#core/token";
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import { injectionSlotToResolveOptions, bindingSlotToResolveOptions } from "#injection/resolve-options";
|
|
9
|
+
import { AsyncModuleLoadError, CircularDependencyError, DisposedContainerError, InternalError, RebindUnboundTokenError, ScopeViolationError, UnreachableConstraintError, UnreachableLifecycleHookError, } from "#errors";
|
|
10
|
+
import { injectionSlotToResolveOptions, bindingSlotToResolveOptions } from "#injection/dependency-slot";
|
|
13
11
|
import { buildDependencyGraph } from "#introspection/dependency-graph";
|
|
12
|
+
import { RESOLUTION_DIAGNOSTICS } from "#introspection/diagnostics";
|
|
13
|
+
import { explainRequest } from "#introspection/explanation";
|
|
14
14
|
import { Inspector } from "#introspection/inspector";
|
|
15
|
-
import { LifecycleManager } from "#lifecycle/
|
|
16
|
-
import { ScopeManager } from "#lifecycle/
|
|
17
|
-
import { MetadataReaderToken } from "#metadata/
|
|
18
|
-
import { defaultMetadataReader } from "#metadata/symbol-
|
|
19
|
-
import { verifyingMetadataReader } from "#metadata/verifying-
|
|
20
|
-
import { ROOT_BRANCH } from "#resolution/path
|
|
15
|
+
import { LifecycleManager } from "#lifecycle/hooks";
|
|
16
|
+
import { SCOPED_MISS, ScopeManager } from "#lifecycle/scopes";
|
|
17
|
+
import { MetadataReaderToken } from "#metadata/reader-token";
|
|
18
|
+
import { defaultMetadataReader } from "#metadata/symbol-reader";
|
|
19
|
+
import { verifyingMetadataReader } from "#metadata/verifying-reader";
|
|
20
|
+
import { ROOT_BRANCH } from "#resolution/path";
|
|
21
21
|
import { DependencyResolver } from "#resolution/resolver";
|
|
22
22
|
/** Whether a requirement's name is declared — on its token when it names one, on any token otherwise. */
|
|
23
23
|
function isSlotNameDeclared(declared, requirement) {
|
|
@@ -76,6 +76,8 @@ class DefaultContainer {
|
|
|
76
76
|
// unload of its module drains it, and dispose runs a constant's hook, which neither the registry nor the cache holds.
|
|
77
77
|
#orphanedBindings;
|
|
78
78
|
constructor(parent, options) {
|
|
79
|
+
// oxlint-disable-next-line typescript/unbound-method -- installed as this instance's own method, so `this` is it
|
|
80
|
+
this.resolve = parent === undefined ? DefaultContainer.#rootResolve : DefaultContainer.#childResolve;
|
|
79
81
|
this.#parent = parent;
|
|
80
82
|
this.#registry = new BindingRegistry();
|
|
81
83
|
this.#scope = new ScopeManager(parent !== undefined);
|
|
@@ -152,13 +154,10 @@ class DefaultContainer {
|
|
|
152
154
|
onDisplaced: (binding) => {
|
|
153
155
|
this.#recordDisplaced(binding);
|
|
154
156
|
},
|
|
155
|
-
onRestored: (binding) => {
|
|
156
|
-
this.#orphanedBindings?.delete(binding.identifier);
|
|
157
|
-
},
|
|
158
157
|
});
|
|
159
158
|
}
|
|
160
|
-
// A displaced binding holding a cached instance or owing a constant's deactivation is remembered
|
|
161
|
-
//
|
|
159
|
+
// A displaced binding holding a cached instance or owing a constant's deactivation is remembered until
|
|
160
|
+
// dispose, an unbind of its id or its module's unload; every other displaced binding is dropped.
|
|
162
161
|
#recordDisplaced(binding) {
|
|
163
162
|
if (binding.instance !== NO_INSTANCE || this.#owesConstantDeactivation(binding)) {
|
|
164
163
|
(this.#orphanedBindings ??= new Map()).set(binding.identifier, binding);
|
|
@@ -167,18 +166,17 @@ class DefaultContainer {
|
|
|
167
166
|
/**
|
|
168
167
|
* One registration per module load, holding that module's id list directly.
|
|
169
168
|
*
|
|
170
|
-
* @remarks It borrows the container's own displacement
|
|
171
|
-
* displaces is
|
|
169
|
+
* @remarks It borrows the container's own displacement callback, so a constant a module's bind
|
|
170
|
+
* displaces is kept for dispose exactly as one displaced by `bind()` is.
|
|
172
171
|
*/
|
|
173
172
|
#moduleRegistration(moduleRef) {
|
|
174
173
|
this.#moduleBindingIds ??= new Map();
|
|
175
|
-
const { onDisplaced
|
|
174
|
+
const { onDisplaced } = this.#ownRegistration();
|
|
176
175
|
return {
|
|
177
176
|
registry: this.#registry,
|
|
178
177
|
scope: this.#scope,
|
|
179
178
|
moduleBindingIds: getOrInsert(this.#moduleBindingIds, moduleRef, []),
|
|
180
179
|
onDisplaced,
|
|
181
|
-
onRestored,
|
|
182
180
|
};
|
|
183
181
|
}
|
|
184
182
|
#createBindToBuilder(token, registration = this.#ownRegistration()) {
|
|
@@ -198,13 +196,8 @@ class DefaultContainer {
|
|
|
198
196
|
// re-index the token's binding list once per binding.
|
|
199
197
|
return this.#drainSingletons(this.#registry.removeByToken(tokenOrId));
|
|
200
198
|
}
|
|
201
|
-
/**
|
|
202
|
-
* Retires an id the registry no longer holds, returning the displaced binding still owed a teardown.
|
|
203
|
-
*
|
|
204
|
-
* @remarks Counts as a registry write, so no chain holding the binding parked can restore it.
|
|
205
|
-
*/
|
|
199
|
+
/** Retires an id the registry no longer holds, returning the displaced binding still owed a teardown. */
|
|
206
200
|
#retireNotLive(id) {
|
|
207
|
-
this.#registry.touch();
|
|
208
201
|
const orphaned = this.#orphanedBindings?.get(id);
|
|
209
202
|
if (orphaned !== undefined) {
|
|
210
203
|
this.#orphanedBindings.delete(id);
|
|
@@ -217,18 +210,25 @@ class DefaultContainer {
|
|
|
217
210
|
// ever cached — the hot-swap shape — owes no deactivation and hands the shared empty list back.
|
|
218
211
|
let pairs;
|
|
219
212
|
for (let index = 0; index < bindings.length; index += 1) {
|
|
220
|
-
|
|
221
|
-
if (binding.instance !== NO_INSTANCE) {
|
|
222
|
-
(pairs ??= []).push(binding, binding.instance);
|
|
223
|
-
this.#scope.deleteSingleton(binding);
|
|
224
|
-
}
|
|
225
|
-
else if (this.#owesConstantDeactivation(binding)) {
|
|
226
|
-
(pairs ??= []).push(binding, binding.value);
|
|
227
|
-
}
|
|
228
|
-
this.#scope.deleteScoped(binding.identifier);
|
|
213
|
+
pairs = this.#drainSingleton(bindings[index], pairs);
|
|
229
214
|
}
|
|
230
215
|
return pairs ?? NO_DEACTIVATION_PAIRS;
|
|
231
216
|
}
|
|
217
|
+
/** One binding's share of {@link DefaultContainer.#drainSingletons}, allocating `pairs` on the first pair owed. */
|
|
218
|
+
#drainSingleton(binding, pairs) {
|
|
219
|
+
if (binding.instance !== NO_INSTANCE) {
|
|
220
|
+
(pairs ??= []).push(binding, binding.instance);
|
|
221
|
+
this.#scope.deleteSingleton(binding);
|
|
222
|
+
}
|
|
223
|
+
else if (this.#owesConstantDeactivation(binding)) {
|
|
224
|
+
(pairs ??= []).push(binding, binding.value);
|
|
225
|
+
}
|
|
226
|
+
const scopedInstance = this.#scope.takeScoped(binding.scopedCacheKey);
|
|
227
|
+
if (scopedInstance !== SCOPED_MISS) {
|
|
228
|
+
(pairs ??= []).push(binding, scopedInstance);
|
|
229
|
+
}
|
|
230
|
+
return pairs;
|
|
231
|
+
}
|
|
232
232
|
/**
|
|
233
233
|
* Whether a constant still owes its deactivation.
|
|
234
234
|
*
|
|
@@ -246,10 +246,11 @@ class DefaultContainer {
|
|
|
246
246
|
return;
|
|
247
247
|
}
|
|
248
248
|
const reader = this.#getMetadataReader();
|
|
249
|
+
const lifecycle = this.#lifecycle;
|
|
249
250
|
let errors;
|
|
250
251
|
for (let index = 0; index < pairs.length; index += 2) {
|
|
251
252
|
try {
|
|
252
|
-
|
|
253
|
+
lifecycle.runDeactivationSync(pairs[index], pairs[index + 1], reader);
|
|
253
254
|
}
|
|
254
255
|
catch (error) {
|
|
255
256
|
(errors ??= []).push(error);
|
|
@@ -264,10 +265,11 @@ class DefaultContainer {
|
|
|
264
265
|
return;
|
|
265
266
|
}
|
|
266
267
|
const reader = this.#getMetadataReader();
|
|
268
|
+
const lifecycle = this.#lifecycle;
|
|
267
269
|
let errors;
|
|
268
270
|
for (let index = 0; index < pairs.length; index += 2) {
|
|
269
271
|
try {
|
|
270
|
-
await
|
|
272
|
+
await lifecycle.runDeactivation(pairs[index], pairs[index + 1], reader);
|
|
271
273
|
}
|
|
272
274
|
catch (error) {
|
|
273
275
|
(errors ??= []).push(error);
|
|
@@ -317,14 +319,18 @@ class DefaultContainer {
|
|
|
317
319
|
return new BindingChain(token, this.#rebindRegistration());
|
|
318
320
|
}
|
|
319
321
|
#displacingRegistration;
|
|
320
|
-
/** A registration whose displaced binding is deactivated instead of
|
|
322
|
+
/** A registration whose displaced binding is deactivated on the spot instead of kept for dispose, for `rebind`. */
|
|
321
323
|
#rebindRegistration() {
|
|
322
324
|
return (this.#displacingRegistration ??= {
|
|
323
325
|
registry: this.#registry,
|
|
324
326
|
scope: this.#scope,
|
|
325
327
|
moduleBindingIds: undefined,
|
|
328
|
+
// One binding at a time, so the hot-swap rebind that owes nothing allocates nothing.
|
|
326
329
|
deactivateDisplaced: (binding) => {
|
|
327
|
-
this.#
|
|
330
|
+
const pairs = this.#drainSingleton(binding, undefined);
|
|
331
|
+
if (pairs !== undefined) {
|
|
332
|
+
this.#deactivatePairsSync(pairs);
|
|
333
|
+
}
|
|
328
334
|
},
|
|
329
335
|
});
|
|
330
336
|
}
|
|
@@ -530,9 +536,10 @@ class DefaultContainer {
|
|
|
530
536
|
this.#assertNotDisposed();
|
|
531
537
|
this.#lifecycle.registerDeactivation(token, handler);
|
|
532
538
|
}
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
539
|
+
static #rootResolve(token, options) {
|
|
540
|
+
if (this.#disposed) {
|
|
541
|
+
throw new DisposedContainerError();
|
|
542
|
+
}
|
|
536
543
|
const rootStack = this.#resolver.rootStack;
|
|
537
544
|
// A resolve already holding the shared stack means this one is nested; it mints its own.
|
|
538
545
|
if (rootStack.length !== 0) {
|
|
@@ -545,8 +552,28 @@ class DefaultContainer {
|
|
|
545
552
|
}
|
|
546
553
|
return this.#resolver.resolve(token, options, rootStack);
|
|
547
554
|
}
|
|
555
|
+
// The root's body again behind the child's check, so neither role's compiled resolve carries the other's path.
|
|
556
|
+
static #childResolve(token, options) {
|
|
557
|
+
this.#assertChainLive();
|
|
558
|
+
const rootStack = this.#resolver.rootStack;
|
|
559
|
+
if (rootStack.length !== 0) {
|
|
560
|
+
return options === undefined
|
|
561
|
+
? this.#resolver.resolveFromContext(token, [])
|
|
562
|
+
: this.#resolver.resolve(token, options, []);
|
|
563
|
+
}
|
|
564
|
+
if (options === undefined) {
|
|
565
|
+
return this.#resolver.resolveFromContext(token, rootStack);
|
|
566
|
+
}
|
|
567
|
+
return this.#resolver.resolve(token, options, rootStack);
|
|
568
|
+
}
|
|
548
569
|
resolveAsync(token, options) {
|
|
549
|
-
this
|
|
570
|
+
// Not `async`: the guard's throw becomes the rejection this method promises.
|
|
571
|
+
try {
|
|
572
|
+
this.#assertNotDisposed();
|
|
573
|
+
}
|
|
574
|
+
catch (disposedError) {
|
|
575
|
+
return Promise.reject(disposedError);
|
|
576
|
+
}
|
|
550
577
|
if (options === undefined) {
|
|
551
578
|
return this.#resolver.resolveAsyncFromRoot(token);
|
|
552
579
|
}
|
|
@@ -560,7 +587,12 @@ class DefaultContainer {
|
|
|
560
587
|
: this.#resolver.resolveOptional(token, options, []);
|
|
561
588
|
}
|
|
562
589
|
resolveOptionalAsync(token, options) {
|
|
563
|
-
|
|
590
|
+
try {
|
|
591
|
+
this.#assertNotDisposed();
|
|
592
|
+
}
|
|
593
|
+
catch (disposedError) {
|
|
594
|
+
return Promise.reject(disposedError);
|
|
595
|
+
}
|
|
564
596
|
return this.#resolver.resolveOptionalAsync(token, options, [], ROOT_BRANCH);
|
|
565
597
|
}
|
|
566
598
|
resolveAll(token, options) {
|
|
@@ -574,7 +606,12 @@ class DefaultContainer {
|
|
|
574
606
|
: this.#resolver.resolveAll(token, options, []);
|
|
575
607
|
}
|
|
576
608
|
resolveAllAsync(token, options) {
|
|
577
|
-
|
|
609
|
+
try {
|
|
610
|
+
this.#assertNotDisposed();
|
|
611
|
+
}
|
|
612
|
+
catch (disposedError) {
|
|
613
|
+
return Promise.reject(disposedError);
|
|
614
|
+
}
|
|
578
615
|
if (options === undefined) {
|
|
579
616
|
return this.#resolver.resolveRootCollectionAsync(token);
|
|
580
617
|
}
|
|
@@ -583,7 +620,10 @@ class DefaultContainer {
|
|
|
583
620
|
// ── Child ──────────────────────────────────────────────────────────────────────────────────────────────────────────
|
|
584
621
|
createChild() {
|
|
585
622
|
this.#assertNotDisposed();
|
|
586
|
-
|
|
623
|
+
const child = new DefaultContainer(this);
|
|
624
|
+
// Born confirmed live: the parent's chain was checked just above, so the child's first read skips the walk.
|
|
625
|
+
child.#liveGeneration = disposeEpochRef.value;
|
|
626
|
+
return child;
|
|
587
627
|
}
|
|
588
628
|
// ── Dispose ────────────────────────────────────────────────────────────────────────────────────────────────────────
|
|
589
629
|
dispose() {
|
|
@@ -600,6 +640,11 @@ class DefaultContainer {
|
|
|
600
640
|
await this.#scope.settleInflight();
|
|
601
641
|
const reader = this.#getMetadataReader();
|
|
602
642
|
const errors = [];
|
|
643
|
+
// A scoped instance may hold this container's singletons and never the reverse, so it goes first, latest first.
|
|
644
|
+
const scopedTeardown = this.#scope.deactivateScoped(errors);
|
|
645
|
+
if (scopedTeardown !== undefined) {
|
|
646
|
+
await scopedTeardown;
|
|
647
|
+
}
|
|
603
648
|
// Iterate a copy: a deactivation handler is user code, and the live list is what
|
|
604
649
|
// materializing or dropping a singleton mutates. Reverse materialization order, so a
|
|
605
650
|
// dependent tears down before the dependencies it may still reach through.
|
|
@@ -647,9 +692,6 @@ class DefaultContainer {
|
|
|
647
692
|
[Symbol.asyncDispose]() {
|
|
648
693
|
return this.dispose();
|
|
649
694
|
}
|
|
650
|
-
[Symbol.dispose]() {
|
|
651
|
-
throw new SyncDisposalNotSupportedError();
|
|
652
|
-
}
|
|
653
695
|
// ── Initialization ─────────────────────────────────────────────────────────────────────────────────────────────────
|
|
654
696
|
async initializeAsync() {
|
|
655
697
|
this.#assertNotDisposed();
|
|
@@ -692,8 +734,8 @@ class DefaultContainer {
|
|
|
692
734
|
if (!this.#isBoundInChain(hookToken)) {
|
|
693
735
|
throw new UnreachableLifecycleHookError(tokenName(hookToken), phase);
|
|
694
736
|
}
|
|
695
|
-
// A deactivation
|
|
696
|
-
//
|
|
737
|
+
// A deactivation never runs for a transient, so a hook on a token whose every binding is transient
|
|
738
|
+
// can never fire — the builder blocks it, `container.onDeactivation` cannot.
|
|
697
739
|
if (phase === "onDeactivation" && !this.#hasDeactivatableBindingInChain(hookToken)) {
|
|
698
740
|
throw new UnreachableLifecycleHookError(tokenName(hookToken), phase, "no-deactivatable-binding");
|
|
699
741
|
}
|
|
@@ -739,10 +781,10 @@ class DefaultContainer {
|
|
|
739
781
|
const parent = this.#parent;
|
|
740
782
|
return parent !== undefined && parent.#isBoundInChain(token);
|
|
741
783
|
}
|
|
742
|
-
// Whether the token has a binding a deactivation hook can run for —
|
|
784
|
+
// Whether the token has a binding a deactivation hook can run for — anything but a transient.
|
|
743
785
|
#hasDeactivatableBindingInChain(token) {
|
|
744
786
|
for (const binding of this.#registry.getAll(token)) {
|
|
745
|
-
if (effectiveBindingScope(binding)
|
|
787
|
+
if (effectiveBindingScope(binding) !== "transient") {
|
|
746
788
|
return true;
|
|
747
789
|
}
|
|
748
790
|
}
|
|
@@ -756,22 +798,21 @@ class DefaultContainer {
|
|
|
756
798
|
return binding.kind === "class" || binding.kind === "resolved" || binding.kind === "resolved-async";
|
|
757
799
|
}
|
|
758
800
|
/**
|
|
759
|
-
* DFS over explicit constructor / `toResolved*` dependency edges
|
|
760
|
-
* terminal binding for scope checks.
|
|
801
|
+
* DFS over explicit constructor / `toResolved*` dependency edges, following `toAlias` chains to the terminal binding.
|
|
761
802
|
*
|
|
762
|
-
* @remarks
|
|
763
|
-
*
|
|
803
|
+
* @remarks Every binding reached is a singleton resolved from its owner's chain, so a required miss or a repeat
|
|
804
|
+
* on one path fails however the graph is entered; a `toDynamic*` edge is scope-checked, its factory not descended.
|
|
764
805
|
*/
|
|
765
806
|
#validateSingletonBindingGraph(root, reader) {
|
|
766
807
|
const rootName = tokenName(root.token);
|
|
767
|
-
const dfs = (current, pathNames, pathBindingIds) => {
|
|
808
|
+
const dfs = (current, view, pathNames, pathBindingIds) => {
|
|
768
809
|
if (pathBindingIds.has(current.identifier)) {
|
|
769
|
-
|
|
810
|
+
throw new CircularDependencyError(pathNames);
|
|
770
811
|
}
|
|
771
812
|
const extendedPathIds = new Set(pathBindingIds);
|
|
772
813
|
extendedPathIds.add(current.identifier);
|
|
773
|
-
for (const edge of this.#collectStaticDependencyEdges(current, reader)) {
|
|
774
|
-
const { terminal, depTokenName } = edge;
|
|
814
|
+
for (const edge of this.#collectStaticDependencyEdges(current, view, reader, pathNames)) {
|
|
815
|
+
const { terminal, owner, depTokenName } = edge;
|
|
775
816
|
const depScope = this.#validationScopeFromTerminal(terminal);
|
|
776
817
|
if (depScope !== "singleton") {
|
|
777
818
|
throw new ScopeViolationError({
|
|
@@ -783,11 +824,11 @@ class DefaultContainer {
|
|
|
783
824
|
});
|
|
784
825
|
}
|
|
785
826
|
if (terminal.kind === "class" || terminal.kind === "resolved" || terminal.kind === "resolved-async") {
|
|
786
|
-
dfs(terminal, [...pathNames, depTokenName], extendedPathIds);
|
|
827
|
+
dfs(terminal, owner, [...pathNames, depTokenName], extendedPathIds);
|
|
787
828
|
}
|
|
788
829
|
}
|
|
789
830
|
};
|
|
790
|
-
dfs(root, [rootName], new Set());
|
|
831
|
+
dfs(root, this.#resolver, [rootName], new Set());
|
|
791
832
|
}
|
|
792
833
|
/**
|
|
793
834
|
* The scope this edge is judged against.
|
|
@@ -802,32 +843,35 @@ class DefaultContainer {
|
|
|
802
843
|
}
|
|
803
844
|
return terminal.scope;
|
|
804
845
|
}
|
|
805
|
-
|
|
846
|
+
/** Where an alias chain ends as `resolve` walks it from `view`: its terminal binding, or the names up to a miss. */
|
|
847
|
+
#followAliasChainToTerminal(entry, view, options) {
|
|
806
848
|
const cyclePath = [];
|
|
807
849
|
const seenAliasIds = new Set();
|
|
808
|
-
let current =
|
|
809
|
-
while (current
|
|
810
|
-
|
|
850
|
+
let current = entry;
|
|
851
|
+
while (current.binding.kind === "alias") {
|
|
852
|
+
const alias = current.binding;
|
|
853
|
+
if (seenAliasIds.has(alias.identifier)) {
|
|
811
854
|
throw new CircularDependencyError(cyclePath);
|
|
812
855
|
}
|
|
813
|
-
seenAliasIds.add(
|
|
814
|
-
cyclePath.push(tokenName(
|
|
815
|
-
const
|
|
816
|
-
const next = this.#resolver.peekBindingForValidate(nextToken, options);
|
|
856
|
+
seenAliasIds.add(alias.identifier);
|
|
857
|
+
cyclePath.push(tokenName(alias.token));
|
|
858
|
+
const next = view.peekBindingForValidate(alias.target, options);
|
|
817
859
|
if (next === undefined) {
|
|
818
|
-
return
|
|
860
|
+
return { found: false, missingToken: alias.target, hopNames: cyclePath };
|
|
819
861
|
}
|
|
820
|
-
current = next
|
|
862
|
+
current = next;
|
|
821
863
|
}
|
|
822
|
-
return current;
|
|
864
|
+
return { found: true, terminal: current.binding, owner: current.owner };
|
|
823
865
|
}
|
|
824
|
-
/** What one dependency could resolve to
|
|
825
|
-
#peekDependencyCandidates(dep, options) {
|
|
866
|
+
/** What one dependency could resolve to from `view`: every candidate for `injectAll`, else at most one. */
|
|
867
|
+
#peekDependencyCandidates(dep, view, options) {
|
|
826
868
|
if (dep.multi) {
|
|
827
|
-
return
|
|
869
|
+
return view
|
|
870
|
+
.peekCandidateBindingsForValidate(dep.token, options)
|
|
871
|
+
.map((binding) => ({ binding, owner: view.ownerForValidate(binding) }));
|
|
828
872
|
}
|
|
829
|
-
const found =
|
|
830
|
-
return found === undefined ? [] : [found
|
|
873
|
+
const found = view.peekBindingForValidate(dep.token, options);
|
|
874
|
+
return found === undefined ? [] : [found];
|
|
831
875
|
}
|
|
832
876
|
/** What a binding declares up front — a class's params, a factory's descriptors, else nothing. */
|
|
833
877
|
#staticDependencies(binding, reader) {
|
|
@@ -839,15 +883,39 @@ class DefaultContainer {
|
|
|
839
883
|
}
|
|
840
884
|
return [];
|
|
841
885
|
}
|
|
842
|
-
|
|
886
|
+
/**
|
|
887
|
+
* The bindings a singleton's declared dependencies resolve to, throwing the error `resolve` would for a required one
|
|
888
|
+
* nothing selects.
|
|
889
|
+
*/
|
|
890
|
+
#collectStaticDependencyEdges(binding, view, reader, pathNames) {
|
|
843
891
|
const edges = [];
|
|
844
892
|
for (const dep of this.#staticDependencies(binding, reader)) {
|
|
845
893
|
// An optional dependency that is absent peeks no candidate; one that is bound is captured like any other.
|
|
846
894
|
const depOptions = injectionSlotToResolveOptions(dep);
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
895
|
+
// A collection may be empty and an optional dependency absent, so only a required one can miss.
|
|
896
|
+
const required = !dep.multi && !dep.optional;
|
|
897
|
+
const candidates = this.#peekDependencyCandidates(dep, view, depOptions);
|
|
898
|
+
if (required && candidates.length === 0) {
|
|
899
|
+
const miss = view.missForValidate(dep.token, depOptions, [...pathNames, tokenName(dep.token)]);
|
|
900
|
+
if (miss !== undefined) {
|
|
901
|
+
throw miss;
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
for (const candidate of candidates) {
|
|
905
|
+
const end = this.#followAliasChainToTerminal(candidate, view, depOptions);
|
|
906
|
+
if (end.found) {
|
|
907
|
+
edges.push({ terminal: end.terminal, owner: end.owner, depTokenName: tokenName(end.terminal.token) });
|
|
908
|
+
continue;
|
|
909
|
+
}
|
|
910
|
+
if (required) {
|
|
911
|
+
const miss = view.missForValidate(end.missingToken, depOptions, [
|
|
912
|
+
...pathNames,
|
|
913
|
+
...end.hopNames,
|
|
914
|
+
tokenName(end.missingToken),
|
|
915
|
+
]);
|
|
916
|
+
if (miss !== undefined) {
|
|
917
|
+
throw miss;
|
|
918
|
+
}
|
|
851
919
|
}
|
|
852
920
|
}
|
|
853
921
|
}
|
|
@@ -879,33 +947,39 @@ class DefaultContainer {
|
|
|
879
947
|
this.#assertNotDisposed();
|
|
880
948
|
return this.#getInspector().inspect();
|
|
881
949
|
}
|
|
950
|
+
explain(token, options) {
|
|
951
|
+
this.#assertNotDisposed();
|
|
952
|
+
return explainRequest([this.#registry, ...this.#ancestorRegistries()], token, options);
|
|
953
|
+
}
|
|
882
954
|
generateDependencyGraph(options) {
|
|
883
955
|
this.#assertNotDisposed();
|
|
884
|
-
|
|
956
|
+
return buildDependencyGraph(this.#registry, this.#getMetadataReader(), options, this.#ancestorRegistries());
|
|
957
|
+
}
|
|
958
|
+
/** The registries above this container, nearest first — the order every chain walk reads them in. */
|
|
959
|
+
#ancestorRegistries() {
|
|
960
|
+
const registries = [];
|
|
885
961
|
for (let ancestor = this.#parent; ancestor !== undefined; ancestor = ancestor.#parent) {
|
|
886
|
-
|
|
962
|
+
registries.push(ancestor.#registry);
|
|
887
963
|
}
|
|
888
|
-
return
|
|
964
|
+
return registries;
|
|
889
965
|
}
|
|
890
966
|
// ── Internal ───────────────────────────────────────────────────────────────────────────────────────────────────────
|
|
891
|
-
//
|
|
892
|
-
//
|
|
893
|
-
// so an epoch unchanged since we last confirmed the chain live means it is still live. A moved epoch
|
|
894
|
-
// drops to the cold refresh, which walks self and ancestors and either re-confirms or throws.
|
|
895
|
-
// Hot guard. A root's whole answer is its own `#disposed`, kept the one field read it always was —
|
|
896
|
-
// the epoch is never read on the root path. A child adds a call-free epoch compare: an unchanged
|
|
897
|
-
// epoch means no container anywhere has been disposed since we last confirmed the chain live, so the
|
|
898
|
-
// ancestors need no walk; a moved epoch drops to the cold refresh that walks them and re-confirms or throws.
|
|
967
|
+
// Two field reads and no epoch on a live root. This and `#assertChainLive` each stay under V8's small-function
|
|
968
|
+
// size, so every entry point inlines both outright; the walk and the throw sit in `#confirmChainLive`.
|
|
899
969
|
#assertNotDisposed() {
|
|
900
|
-
if (this.#disposed) {
|
|
901
|
-
|
|
970
|
+
if (this.#disposed || this.#parent !== undefined) {
|
|
971
|
+
this.#assertChainLive();
|
|
902
972
|
}
|
|
903
|
-
|
|
904
|
-
|
|
973
|
+
}
|
|
974
|
+
// An unchanged dispose epoch means no container anywhere closed since this chain was last confirmed live;
|
|
975
|
+
// every dispose advances it, this container's own included, so a disposed container always reaches the walk.
|
|
976
|
+
#assertChainLive() {
|
|
977
|
+
if (disposeEpochRef.value !== this.#liveGeneration) {
|
|
978
|
+
this.#confirmChainLive();
|
|
905
979
|
}
|
|
906
980
|
}
|
|
907
|
-
#
|
|
908
|
-
if (this.#
|
|
981
|
+
#confirmChainLive() {
|
|
982
|
+
if (this.#isChainDisposed()) {
|
|
909
983
|
throw new DisposedContainerError();
|
|
910
984
|
}
|
|
911
985
|
this.#liveGeneration = disposeEpochRef.value;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The fluent chain's contract: the slot steps `bind()` offers before `to*()`, and the builder each `to*()` returns.
|
|
3
|
+
*/
|
|
4
|
+
import type { BindingTag } from "#core/tag";
|
|
5
|
+
import type { Token } from "#core/token";
|
|
6
|
+
import type { ActivationHandler, BindingConstraint, BindingIdentifier, Constructor, DeactivationHandler, ResolutionContext } from "#core/types";
|
|
7
|
+
import type { InjectableDependency, ResolvedDependencyValue } from "#injection/descriptor";
|
|
8
|
+
/**
|
|
9
|
+
* The slot steps of the fluent chain, taken before `to*()` so the binding registers once, in its final shape.
|
|
10
|
+
*
|
|
11
|
+
* @since 0.3.16-canary.0
|
|
12
|
+
*/
|
|
13
|
+
export interface SlotConstrainedBuilder<Names extends string = string> {
|
|
14
|
+
/** Narrows the binding to requests the predicate accepts, evaluated on every resolve. */
|
|
15
|
+
when(predicate: BindingConstraint): this;
|
|
16
|
+
/** Declares the binding's slot name, one of the names the token declares. */
|
|
17
|
+
whenNamed(name: Names): this;
|
|
18
|
+
/** Declares one criterion of the binding's slot, replacing any earlier criterion of the same key. */
|
|
19
|
+
whenTagged(criterion: BindingTag): this;
|
|
20
|
+
/** Keeps the binding on the default slot, the one an unconstrained request selects. */
|
|
21
|
+
whenDefault(): this;
|
|
22
|
+
/**
|
|
23
|
+
* Makes the binding a collection member: one of several the token's `resolveAll` returns, never
|
|
24
|
+
* what a single `resolve` selects, and outside slot last-wins. It keeps the default slot.
|
|
25
|
+
*/
|
|
26
|
+
many(): this;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The first step of the fluent chain: the slot, then the `to*` strategy that registers the binding.
|
|
30
|
+
*
|
|
31
|
+
* @since 0.3.16-canary.0
|
|
32
|
+
*/
|
|
33
|
+
export interface BindToBuilder<Value, Names extends string = string> extends SlotConstrainedBuilder<Names> {
|
|
34
|
+
to(type: Constructor<Value>): BindingBuilder<Value>;
|
|
35
|
+
toSelf(): BindingBuilder<Value>;
|
|
36
|
+
toConstantValue(value: Value): ConstantBindingBuilder<Value>;
|
|
37
|
+
toDynamic(factory: (ctx: ResolutionContext) => Value): BindingBuilder<Value>;
|
|
38
|
+
toDynamicAsync(factory: (ctx: ResolutionContext) => Promise<Value>): BindingBuilder<Value>;
|
|
39
|
+
toResolved<const Deps extends ReadonlyArray<InjectableDependency>>(factory: (...args: {
|
|
40
|
+
[K in keyof Deps]: ResolvedDependencyValue<NoInfer<Deps>[K]>;
|
|
41
|
+
}) => Value, deps: Deps): BindingBuilder<Value>;
|
|
42
|
+
toResolvedAsync<const Deps extends ReadonlyArray<InjectableDependency>>(factory: (...args: {
|
|
43
|
+
[K in keyof Deps]: ResolvedDependencyValue<NoInfer<Deps>[K]>;
|
|
44
|
+
}) => Promise<Value>, deps: Deps): BindingBuilder<Value>;
|
|
45
|
+
toAlias(target: Token<Value> | Constructor<Value>): AliasBindingBuilder;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* The scope-selection step of the fluent chain, which the binding enters once registered.
|
|
49
|
+
*
|
|
50
|
+
* @since 0.3.16-canary.0
|
|
51
|
+
*/
|
|
52
|
+
export interface BindingBuilder<Value> {
|
|
53
|
+
singleton(): SingletonBindingBuilder<Value>;
|
|
54
|
+
transient(): TransientBindingBuilder<Value>;
|
|
55
|
+
scoped(): ScopedBindingBuilder<Value>;
|
|
56
|
+
/** The identifier this binding is registered under. */
|
|
57
|
+
id(): BindingIdentifier;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The fluent chain for a constant — lifecycle hooks only, since the scope is fixed.
|
|
61
|
+
*
|
|
62
|
+
* @since 0.3.16-canary.0
|
|
63
|
+
*/
|
|
64
|
+
export interface ConstantBindingBuilder<Value> {
|
|
65
|
+
onActivation(fn: ActivationHandler<Value>): SingletonLifecycleBuilder<Value>;
|
|
66
|
+
onDeactivation(fn: DeactivationHandler<Value>): SingletonLifecycleBuilder<Value>;
|
|
67
|
+
/** The identifier this binding is registered under. */
|
|
68
|
+
id(): BindingIdentifier;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* The fluent chain for an alias — nothing to add, since scoping belongs to the target.
|
|
72
|
+
*
|
|
73
|
+
* @since 0.3.16-canary.0
|
|
74
|
+
*/
|
|
75
|
+
export interface AliasBindingBuilder {
|
|
76
|
+
/** The identifier this binding is registered under. */
|
|
77
|
+
id(): BindingIdentifier;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* The fluent chain after `singleton()`, where both lifecycle hooks stay available.
|
|
81
|
+
*
|
|
82
|
+
* @since 0.3.16-canary.0
|
|
83
|
+
*/
|
|
84
|
+
export interface SingletonBindingBuilder<Value> {
|
|
85
|
+
onActivation(fn: ActivationHandler<Value>): this;
|
|
86
|
+
onDeactivation(fn: DeactivationHandler<Value>): this;
|
|
87
|
+
id(): BindingIdentifier;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* The fluent chain after `transient()`, where activation is the one lifecycle hook offered.
|
|
91
|
+
*
|
|
92
|
+
* @since 0.3.16-canary.0
|
|
93
|
+
*/
|
|
94
|
+
export interface TransientBindingBuilder<Value> {
|
|
95
|
+
onActivation(fn: ActivationHandler<Value>): this;
|
|
96
|
+
id(): BindingIdentifier;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* The fluent chain after `scoped()`, where both lifecycle hooks stay available: a child deactivates what it cached.
|
|
100
|
+
*
|
|
101
|
+
* @since 0.3.16-canary.0
|
|
102
|
+
*/
|
|
103
|
+
export interface ScopedBindingBuilder<Value> {
|
|
104
|
+
onActivation(fn: ActivationHandler<Value>): this;
|
|
105
|
+
onDeactivation(fn: DeactivationHandler<Value>): this;
|
|
106
|
+
id(): BindingIdentifier;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* The fluent chain a constant enters once a lifecycle hook is added.
|
|
110
|
+
*
|
|
111
|
+
* @since 0.3.16-canary.0
|
|
112
|
+
*/
|
|
113
|
+
export interface SingletonLifecycleBuilder<Value> {
|
|
114
|
+
onActivation(fn: ActivationHandler<Value>): this;
|
|
115
|
+
onDeactivation(fn: DeactivationHandler<Value>): this;
|
|
116
|
+
id(): BindingIdentifier;
|
|
117
|
+
}
|
|
@@ -23,11 +23,11 @@ type SlotDefinition<Names extends string> = ({
|
|
|
23
23
|
} & Never<"whenNamed" | "whenTagged">);
|
|
24
24
|
/** How long an instance lives, and the hooks that lifetime allows. */
|
|
25
25
|
type LifetimeDefinition<Value> = {
|
|
26
|
-
readonly scope: "singleton";
|
|
26
|
+
readonly scope: "singleton" | "scoped";
|
|
27
27
|
readonly onActivation?: ActivationHandler<Value> | undefined;
|
|
28
28
|
readonly onDeactivation?: DeactivationHandler<Value> | undefined;
|
|
29
29
|
} | ({
|
|
30
|
-
readonly scope?: "transient" |
|
|
30
|
+
readonly scope?: "transient" | undefined;
|
|
31
31
|
readonly onActivation?: ActivationHandler<Value> | undefined;
|
|
32
32
|
} & Never<"onDeactivation">);
|
|
33
33
|
type BuiltDefinition<Value, Deps extends ReadonlyArray<InjectableDependency>> = ({
|
|
@@ -114,7 +114,14 @@ export declare function asDeclaredBinding(entry: BindingDeclaration, moduleName:
|
|
|
114
114
|
* @throws `InvalidBindingDeclarationError` for no strategy, several, or a key the strategy does not allow.
|
|
115
115
|
* @throws `ManyBindingSlotError` for a slot on a collection member.
|
|
116
116
|
* @throws `SelfBindingRequiresClassError` for `toSelf` on a key that is not a class.
|
|
117
|
+
*
|
|
118
|
+
* @since 0.11.0
|
|
117
119
|
*/
|
|
118
120
|
export declare function binding<Value, Names extends string, const Deps extends ReadonlyArray<InjectableDependency> = []>(key: Token<Value, Names>, definition: BindingDefinition<NoInfer<Value>, NoInfer<Names>, Deps>): BindingDeclaration;
|
|
121
|
+
/**
|
|
122
|
+
* Declares one binding keyed by a class, the one kind of key `toSelf` accepts.
|
|
123
|
+
*
|
|
124
|
+
* @since 0.11.0
|
|
125
|
+
*/
|
|
119
126
|
export declare function binding<Value, const Deps extends ReadonlyArray<InjectableDependency> = []>(key: Constructor<Value>, definition: BindingDefinition<NoInfer<Value>, string, Deps> | SelfDefinition<NoInfer<Value>>): BindingDeclaration;
|
|
120
127
|
export {};
|