@codefast/di 0.5.0-canary.8 → 0.5.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 +387 -0
- package/README.md +3 -1
- package/dist/binding.d.ts +41 -22
- package/dist/binding.d.ts.map +1 -1
- package/dist/binding.js +11 -0
- package/dist/binding.js.map +1 -1
- package/dist/container/binding-builders.d.ts.map +1 -1
- package/dist/container/binding-builders.js +8 -5
- package/dist/container/binding-builders.js.map +1 -1
- package/dist/container/container.d.ts.map +1 -1
- package/dist/container/container.js +67 -110
- package/dist/container/container.js.map +1 -1
- package/dist/errors.d.ts +10 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +13 -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 +2 -7
- package/dist/introspection/inspector.js.map +1 -1
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +46 -33
- package/dist/registry.js.map +1 -1
- package/dist/resolution/activation-need.d.ts +4 -2
- package/dist/resolution/activation-need.d.ts.map +1 -1
- package/dist/resolution/activation-need.js +15 -11
- package/dist/resolution/activation-need.js.map +1 -1
- package/dist/resolution/binding-lookup-cache.d.ts +1 -1
- package/dist/resolution/binding-lookup-cache.d.ts.map +1 -1
- package/dist/resolution/binding-lookup-cache.js +18 -2
- package/dist/resolution/binding-lookup-cache.js.map +1 -1
- 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 +13 -32
- package/dist/resolution/binding-select.js.map +1 -1
- package/dist/resolution/diagnostics.d.ts +2 -2
- package/dist/resolution/diagnostics.d.ts.map +1 -1
- package/dist/resolution/environment.d.ts +47 -21
- package/dist/resolution/environment.d.ts.map +1 -1
- package/dist/resolution/environment.js +134 -31
- package/dist/resolution/environment.js.map +1 -1
- package/dist/resolution/instantiation-plan.d.ts.map +1 -1
- package/dist/resolution/instantiation-plan.js +1 -1
- package/dist/resolution/instantiation-plan.js.map +1 -1
- package/dist/resolution/lifecycle.d.ts.map +1 -1
- package/dist/resolution/lifecycle.js +46 -53
- package/dist/resolution/lifecycle.js.map +1 -1
- package/dist/resolution/resolution-path.d.ts +82 -6
- package/dist/resolution/resolution-path.d.ts.map +1 -1
- package/dist/resolution/resolution-path.js +66 -8
- 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 +29 -11
- package/dist/resolution/resolver.d.ts.map +1 -1
- package/dist/resolution/resolver.js +417 -564
- package/dist/resolution/resolver.js.map +1 -1
- package/dist/resolution/scope.d.ts +4 -0
- package/dist/resolution/scope.d.ts.map +1 -1
- package/dist/resolution/scope.js +8 -0
- package/dist/resolution/scope.js.map +1 -1
- package/package.json +5 -11
- package/src/binding.ts +46 -23
- package/src/container/binding-builders.ts +8 -5
- package/src/container/container.ts +72 -119
- package/src/errors.ts +17 -0
- package/src/index.ts +3 -1
- package/src/introspection/inspector.ts +2 -8
- package/src/registry.ts +52 -35
- package/src/resolution/activation-need.ts +18 -14
- package/src/resolution/binding-lookup-cache.ts +18 -2
- package/src/resolution/binding-scope.ts +6 -17
- package/src/resolution/binding-select.ts +14 -35
- package/src/resolution/diagnostics.ts +2 -2
- package/src/resolution/environment.ts +181 -35
- package/src/resolution/instantiation-plan.ts +5 -5
- package/src/resolution/lifecycle.ts +55 -53
- package/src/resolution/resolution-path.ts +119 -25
- package/src/resolution/resolve-options.ts +51 -4
- package/src/resolution/resolver.ts +582 -785
- package/src/resolution/scope.ts +10 -0
|
@@ -27,6 +27,8 @@ import { effectiveBindingScope } from "#/resolution/binding-scope";
|
|
|
27
27
|
import type { ResolutionDiagnostics } from "#/resolution/diagnostics";
|
|
28
28
|
import { RESOLUTION_DIAGNOSTICS } from "#/resolution/diagnostics";
|
|
29
29
|
import { LifecycleManager } from "#/resolution/lifecycle";
|
|
30
|
+
import { ROOT_BRANCH } from "#/resolution/resolution-path";
|
|
31
|
+
import type { DependencySlot } from "#/resolution/resolve-options";
|
|
30
32
|
import { injectionSlotToResolveOptions, bindingSlotToResolveOptions } from "#/resolution/resolve-options";
|
|
31
33
|
import { DependencyResolver } from "#/resolution/resolver";
|
|
32
34
|
import { ScopeManager } from "#/resolution/scope";
|
|
@@ -156,7 +158,7 @@ class DefaultContainer implements Container {
|
|
|
156
158
|
if (this.#lifecycle.isBuilt) {
|
|
157
159
|
builtSubsystems.push("lifecycle.activationHooks");
|
|
158
160
|
}
|
|
159
|
-
return { ...this.#resolver.describeCaches(), builtSubsystems };
|
|
161
|
+
return { ...this.#resolver.describeCaches(), scopedInstanceCount: this.#scope.scopedCount, builtSubsystems };
|
|
160
162
|
}
|
|
161
163
|
|
|
162
164
|
#initResolver(): void {
|
|
@@ -235,7 +237,7 @@ class DefaultContainer implements Container {
|
|
|
235
237
|
return this.#drainSingletons(this.#registry.removeByToken(tokenOrId));
|
|
236
238
|
}
|
|
237
239
|
|
|
238
|
-
/** Drain
|
|
240
|
+
/** Drain scope entries for already-removed bindings; only singletons yield deactivation pairs. */
|
|
239
241
|
#drainSingletons(bindings: ReadonlyArray<Binding>): Array<[Binding, unknown]> {
|
|
240
242
|
const pairs: Array<[Binding, unknown]> = [];
|
|
241
243
|
for (const binding of bindings) {
|
|
@@ -243,6 +245,7 @@ class DefaultContainer implements Container {
|
|
|
243
245
|
pairs.push([binding, binding.instance]);
|
|
244
246
|
this.#scope.deleteSingleton(binding);
|
|
245
247
|
}
|
|
248
|
+
this.#scope.deleteScoped(binding.id);
|
|
246
249
|
}
|
|
247
250
|
return pairs;
|
|
248
251
|
}
|
|
@@ -295,10 +298,10 @@ class DefaultContainer implements Container {
|
|
|
295
298
|
this.#loadSyncModules(modules);
|
|
296
299
|
}
|
|
297
300
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
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)) {
|
|
302
305
|
if (!isSyncModule(module)) {
|
|
303
306
|
throw new AsyncModuleLoadError(module.name);
|
|
304
307
|
}
|
|
@@ -315,26 +318,6 @@ class DefaultContainer implements Container {
|
|
|
315
318
|
}
|
|
316
319
|
}
|
|
317
320
|
|
|
318
|
-
#collectModuleDeps(modules: Array<SyncModule | AsyncModule>): Array<SyncModule | AsyncModule> {
|
|
319
|
-
const seen = new Set<object>();
|
|
320
|
-
const result: Array<SyncModule | AsyncModule> = [];
|
|
321
|
-
|
|
322
|
-
const visit = (module: SyncModule | AsyncModule): void => {
|
|
323
|
-
const moduleRef = module as object;
|
|
324
|
-
if (seen.has(moduleRef)) {
|
|
325
|
-
return;
|
|
326
|
-
}
|
|
327
|
-
seen.add(moduleRef);
|
|
328
|
-
// We'll collect deps during setup via the builder's import()
|
|
329
|
-
result.push(module);
|
|
330
|
-
};
|
|
331
|
-
|
|
332
|
-
for (const module of modules) {
|
|
333
|
-
visit(module);
|
|
334
|
-
}
|
|
335
|
-
return result;
|
|
336
|
-
}
|
|
337
|
-
|
|
338
321
|
async loadAsync(...modules: Array<SyncModule | AsyncModule>): Promise<void> {
|
|
339
322
|
this.#assertNotDisposed();
|
|
340
323
|
for (const module of modules) {
|
|
@@ -411,6 +394,7 @@ class DefaultContainer implements Container {
|
|
|
411
394
|
pairs.push([binding, binding.instance]);
|
|
412
395
|
this.#scope.deleteSingleton(binding);
|
|
413
396
|
}
|
|
397
|
+
this.#scope.deleteScoped(binding.id);
|
|
414
398
|
}
|
|
415
399
|
}
|
|
416
400
|
return pairs;
|
|
@@ -480,23 +464,33 @@ class DefaultContainer implements Container {
|
|
|
480
464
|
|
|
481
465
|
resolve<const Value>(token: Token<Value> | Constructor<Value>, options?: ResolveOptions): Value {
|
|
482
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
|
+
}
|
|
483
474
|
if (options === undefined) {
|
|
484
|
-
return this.#resolver.resolveFromContext(token,
|
|
475
|
+
return this.#resolver.resolveFromContext(token, this.#resolver.rootPath, rootStack);
|
|
485
476
|
}
|
|
486
|
-
return this.#resolver.resolve(token, options,
|
|
477
|
+
return this.#resolver.resolve(token, options, this.#resolver.rootPath, rootStack);
|
|
487
478
|
}
|
|
488
479
|
|
|
489
480
|
resolveAsync<const Value>(token: Token<Value> | Constructor<Value>, options?: ResolveOptions): Promise<Value> {
|
|
490
481
|
this.#assertNotDisposed();
|
|
491
482
|
if (options === undefined) {
|
|
492
|
-
return this.#resolver.
|
|
483
|
+
return this.#resolver.resolveAsyncFromRoot(token) as Promise<Value>;
|
|
493
484
|
}
|
|
494
|
-
return this.#resolver.resolveAsync(token, options, [], []);
|
|
485
|
+
return this.#resolver.resolveAsync(token, options, [], [], ROOT_BRANCH);
|
|
495
486
|
}
|
|
496
487
|
|
|
497
488
|
resolveOptional<const Value>(token: Token<Value> | Constructor<Value>, options?: ResolveOptions): Value | undefined {
|
|
498
489
|
this.#assertNotDisposed();
|
|
499
|
-
|
|
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, [], []);
|
|
500
494
|
}
|
|
501
495
|
|
|
502
496
|
resolveOptionalAsync<const Value>(
|
|
@@ -504,12 +498,15 @@ class DefaultContainer implements Container {
|
|
|
504
498
|
options?: ResolveOptions,
|
|
505
499
|
): Promise<Value | undefined> {
|
|
506
500
|
this.#assertNotDisposed();
|
|
507
|
-
return this.#resolver.resolveOptionalAsync(token, options, [], []);
|
|
501
|
+
return this.#resolver.resolveOptionalAsync(token, options, [], [], ROOT_BRANCH);
|
|
508
502
|
}
|
|
509
503
|
|
|
510
504
|
resolveAll<const Value>(token: Token<Value> | Constructor<Value>, options?: ResolveOptions): Array<Value> {
|
|
511
505
|
this.#assertNotDisposed();
|
|
512
|
-
|
|
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, [], []);
|
|
513
510
|
}
|
|
514
511
|
|
|
515
512
|
resolveAllAsync<const Value>(
|
|
@@ -517,7 +514,7 @@ class DefaultContainer implements Container {
|
|
|
517
514
|
options?: ResolveOptions,
|
|
518
515
|
): Promise<Array<Value>> {
|
|
519
516
|
this.#assertNotDisposed();
|
|
520
|
-
return this.#resolver.resolveAllAsync(token, options, [], []);
|
|
517
|
+
return this.#resolver.resolveAllAsync(token, options, [], [], ROOT_BRANCH);
|
|
521
518
|
}
|
|
522
519
|
|
|
523
520
|
// ── Child ─────────────────────────────────────────────────────────────────
|
|
@@ -620,10 +617,7 @@ class DefaultContainer implements Container {
|
|
|
620
617
|
for (const edge of this.#collectStaticDependencyEdges(current, reader)) {
|
|
621
618
|
const { terminal, depTokenName } = edge;
|
|
622
619
|
const depScope = this.#validationScopeFromTerminal(terminal);
|
|
623
|
-
if (depScope
|
|
624
|
-
continue;
|
|
625
|
-
}
|
|
626
|
-
if (depScope === "scoped" || depScope === "transient") {
|
|
620
|
+
if (depScope !== "singleton") {
|
|
627
621
|
throw new ScopeViolationError({
|
|
628
622
|
consumerToken: rootName,
|
|
629
623
|
consumerScope: "singleton",
|
|
@@ -641,27 +635,18 @@ class DefaultContainer implements Container {
|
|
|
641
635
|
dfs(root, [rootName], new Set());
|
|
642
636
|
}
|
|
643
637
|
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
case "class":
|
|
655
|
-
case "resolved":
|
|
656
|
-
case "resolved-async":
|
|
657
|
-
return terminal.scope;
|
|
658
|
-
case "alias":
|
|
659
|
-
throw new InternalError("validate: expected terminal binding after alias resolution");
|
|
660
|
-
default: {
|
|
661
|
-
const exhaustive: never = terminal;
|
|
662
|
-
return exhaustive;
|
|
663
|
-
}
|
|
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");
|
|
664
648
|
}
|
|
649
|
+
return terminal.scope;
|
|
665
650
|
}
|
|
666
651
|
|
|
667
652
|
#followAliasChainToTerminal(binding: Binding, options: ResolveOptions | undefined): Binding | undefined {
|
|
@@ -685,75 +670,43 @@ class DefaultContainer implements Container {
|
|
|
685
670
|
return current;
|
|
686
671
|
}
|
|
687
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
|
+
|
|
688
693
|
#collectStaticDependencyEdges(
|
|
689
694
|
binding: Binding,
|
|
690
695
|
reader: MetadataReader,
|
|
691
696
|
): Array<{ terminal: Binding; depTokenName: string }> {
|
|
692
697
|
const edges: Array<{ terminal: Binding; depTokenName: string }> = [];
|
|
693
698
|
|
|
694
|
-
const
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
edges.push({ terminal, depTokenName: displayName });
|
|
699
|
-
};
|
|
700
|
-
|
|
701
|
-
if (binding.kind === "class") {
|
|
702
|
-
const meta = reader.getConstructorMetadata(binding.target as Constructor);
|
|
703
|
-
if (meta === undefined) {
|
|
704
|
-
return edges;
|
|
705
|
-
}
|
|
706
|
-
for (const param of meta.params) {
|
|
707
|
-
const paramOptions = injectionSlotToResolveOptions(param);
|
|
708
|
-
if (param.optional) {
|
|
709
|
-
continue;
|
|
710
|
-
}
|
|
711
|
-
const tokenRef = param.token;
|
|
712
|
-
if (param.multi) {
|
|
713
|
-
const candidates = this.#resolver.peekCandidateBindingsForValidate(tokenRef, paramOptions);
|
|
714
|
-
for (const cand of candidates) {
|
|
715
|
-
const term = this.#followAliasChainToTerminal(cand, paramOptions);
|
|
716
|
-
pushTerminal(term, term !== undefined ? tokenName(term.token as Token<unknown>) : "");
|
|
717
|
-
}
|
|
718
|
-
continue;
|
|
719
|
-
}
|
|
720
|
-
const found =
|
|
721
|
-
paramOptions === undefined
|
|
722
|
-
? this.#resolver.peekBindingForValidate(tokenRef, undefined)
|
|
723
|
-
: this.#resolver.peekBindingForValidate(tokenRef, paramOptions);
|
|
724
|
-
if (found === undefined) {
|
|
725
|
-
continue;
|
|
726
|
-
}
|
|
727
|
-
const term = this.#followAliasChainToTerminal(found.binding, paramOptions);
|
|
728
|
-
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;
|
|
729
703
|
}
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
const depOptions = injectionSlotToResolveOptions(dep);
|
|
736
|
-
if (dep.optional) {
|
|
737
|
-
continue;
|
|
738
|
-
}
|
|
739
|
-
const tokenRef = dep.token as Token<unknown> | Constructor;
|
|
740
|
-
if (dep.multi) {
|
|
741
|
-
const candidates = this.#resolver.peekCandidateBindingsForValidate(tokenRef, depOptions);
|
|
742
|
-
for (const cand of candidates) {
|
|
743
|
-
const term = this.#followAliasChainToTerminal(cand, depOptions);
|
|
744
|
-
pushTerminal(term, term !== undefined ? tokenName(term.token as Token<unknown>) : "");
|
|
745
|
-
}
|
|
746
|
-
continue;
|
|
747
|
-
}
|
|
748
|
-
const found =
|
|
749
|
-
depOptions === undefined
|
|
750
|
-
? this.#resolver.peekBindingForValidate(tokenRef, undefined)
|
|
751
|
-
: this.#resolver.peekBindingForValidate(tokenRef, depOptions);
|
|
752
|
-
if (found === undefined) {
|
|
753
|
-
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>) });
|
|
754
709
|
}
|
|
755
|
-
const term = this.#followAliasChainToTerminal(found.binding, depOptions);
|
|
756
|
-
pushTerminal(term, term !== undefined ? tokenName(term.token as Token<unknown>) : "");
|
|
757
710
|
}
|
|
758
711
|
}
|
|
759
712
|
|
package/src/errors.ts
CHANGED
|
@@ -248,6 +248,23 @@ export class RebindUnboundTokenError extends DiError {
|
|
|
248
248
|
}
|
|
249
249
|
}
|
|
250
250
|
|
|
251
|
+
/**
|
|
252
|
+
* `toSelf()` on a token that is not a class, so there is nothing to construct.
|
|
253
|
+
*
|
|
254
|
+
* @since 0.5.0-canary.9
|
|
255
|
+
*/
|
|
256
|
+
export class SelfBindingRequiresClassError extends DiError {
|
|
257
|
+
readonly code = "SELF_BINDING_REQUIRES_CLASS";
|
|
258
|
+
readonly tokenName: string;
|
|
259
|
+
|
|
260
|
+
constructor(tokenName: string) {
|
|
261
|
+
super(
|
|
262
|
+
`toSelf() needs the token to be the class it constructs, and '${tokenName}' is not a class. Use .to(SomeClass) to name the implementation, or bind the class itself with container.bind(SomeClass).toSelf().`,
|
|
263
|
+
);
|
|
264
|
+
this.tokenName = tokenName;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
251
268
|
/**
|
|
252
269
|
* @since 0.3.16-canary.0
|
|
253
270
|
*/
|
package/src/index.ts
CHANGED
|
@@ -36,7 +36,8 @@ export type {
|
|
|
36
36
|
export { Container } from "#/container/container";
|
|
37
37
|
export type { Container as ContainerInterface, ContainerStatic } from "#/container/container";
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
// `effectiveBindingScope` is deliberately absent: it reads a `Binding`, which is internal, and no
|
|
40
|
+
// public API hands one out. `BindingSnapshot.scope` and `GraphNode.scope` are the public answers.
|
|
40
41
|
export { injectionSlotToResolveOptions, bindingSlotToResolveOptions } from "#/resolution/resolve-options";
|
|
41
42
|
|
|
42
43
|
// Introspection types
|
|
@@ -96,6 +97,7 @@ export {
|
|
|
96
97
|
NoMatchingBindingError,
|
|
97
98
|
RebindUnboundTokenError,
|
|
98
99
|
ScopeViolationError,
|
|
100
|
+
SelfBindingRequiresClassError,
|
|
99
101
|
SyncDisposalNotSupportedError,
|
|
100
102
|
TokenNotBoundError,
|
|
101
103
|
} from "#/errors";
|
|
@@ -60,9 +60,8 @@ export class Inspector {
|
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
inspect(): ContainerSnapshot {
|
|
63
|
-
const snapshots = this.allBindingSnapshots();
|
|
64
63
|
return {
|
|
65
|
-
ownBindings:
|
|
64
|
+
ownBindings: this.#registry.allBindings().map((binding) => this.#toSnapshot(binding)),
|
|
66
65
|
cachedSingletonCount: this.#scope.cachedSingletons().length,
|
|
67
66
|
hasParent: this.#hasParent,
|
|
68
67
|
isDisposed: this.#isDisposed(),
|
|
@@ -109,12 +108,7 @@ export class Inspector {
|
|
|
109
108
|
};
|
|
110
109
|
}
|
|
111
110
|
|
|
112
|
-
private allBindingSnapshots(): ReadonlyArray<BindingSnapshot> {
|
|
113
|
-
return this.#registry.allBindings().map((binding) => this.#toSnapshot(binding));
|
|
114
|
-
}
|
|
115
|
-
|
|
116
111
|
#toSnapshot(binding: Binding): BindingSnapshot {
|
|
117
|
-
const scope = effectiveBindingScope(binding);
|
|
118
112
|
const slot: BindingSnapshot["slot"] =
|
|
119
113
|
binding.slot.name !== undefined
|
|
120
114
|
? { name: binding.slot.name, tags: binding.slot.tags }
|
|
@@ -122,7 +116,7 @@ export class Inspector {
|
|
|
122
116
|
return {
|
|
123
117
|
tokenName: tokenName(binding.token),
|
|
124
118
|
kind: binding.kind,
|
|
125
|
-
scope,
|
|
119
|
+
scope: effectiveBindingScope(binding),
|
|
126
120
|
slot,
|
|
127
121
|
id: binding.id,
|
|
128
122
|
};
|
package/src/registry.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Binding } from "#/binding";
|
|
2
2
|
import { bindingSlotEquals, bindingSlotToString } from "#/binding";
|
|
3
3
|
import type { Token } from "#/token";
|
|
4
|
-
import type { BindingIdentifier, Constructor, DependencyKey } from "#/types";
|
|
4
|
+
import type { BindingIdentifier, BindingTag, Constructor, DependencyKey } from "#/types";
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* @since 0.3.16-canary.0
|
|
@@ -43,14 +43,15 @@ export class BindingRegistry {
|
|
|
43
43
|
add(binding: Binding): Binding | undefined {
|
|
44
44
|
this.#version += 1;
|
|
45
45
|
const key = binding.token as DependencyKey;
|
|
46
|
-
//
|
|
46
|
+
// Eager, not computed: a bind is usually the token's first, so the fallback is usually the
|
|
47
|
+
// one that gets stored — and the computed form would add a call to allocating it anyway.
|
|
47
48
|
const bindingsForToken = this.#bindings.getOrInsert(key, []);
|
|
48
49
|
|
|
49
50
|
// Only apply last-wins for slot-based bindings (not predicate-only)
|
|
50
51
|
let displacedBinding: Binding | undefined;
|
|
51
|
-
if (!
|
|
52
|
+
if (!isPurePredicateBinding(binding)) {
|
|
52
53
|
const existingIndex = bindingsForToken.findIndex(
|
|
53
|
-
(candidate) => !
|
|
54
|
+
(candidate) => !isPurePredicateBinding(candidate) && bindingSlotEquals(candidate.slot, binding.slot),
|
|
54
55
|
);
|
|
55
56
|
if (existingIndex !== -1) {
|
|
56
57
|
displacedBinding = bindingsForToken[existingIndex]!;
|
|
@@ -173,22 +174,22 @@ export class BindingRegistry {
|
|
|
173
174
|
}
|
|
174
175
|
|
|
175
176
|
#indexSimpleTaggedBinding(tokenKey: DependencyKey, binding: Binding): void {
|
|
176
|
-
const
|
|
177
|
-
if (
|
|
177
|
+
const tag = simpleTagOf(binding);
|
|
178
|
+
if (tag === undefined) {
|
|
178
179
|
return;
|
|
179
180
|
}
|
|
180
|
-
const [tagKey, tagValue] =
|
|
181
|
+
const [tagKey, tagValue] = tag;
|
|
181
182
|
const byTagKey = (this.#simpleTagged ??= new Map()).getOrInsert(tokenKey, new Map<string, Map<unknown, Binding>>());
|
|
182
183
|
const byTagValue = byTagKey.getOrInsert(tagKey, new Map<unknown, Binding>());
|
|
183
184
|
byTagValue.set(tagValue, binding);
|
|
184
185
|
}
|
|
185
186
|
|
|
186
187
|
#deindexSimpleTaggedBinding(tokenKey: DependencyKey, binding: Binding): void {
|
|
187
|
-
const
|
|
188
|
-
if (
|
|
188
|
+
const tag = simpleTagOf(binding);
|
|
189
|
+
if (tag === undefined) {
|
|
189
190
|
return;
|
|
190
191
|
}
|
|
191
|
-
const [tagKey, tagValue] =
|
|
192
|
+
const [tagKey, tagValue] = tag;
|
|
192
193
|
const byTagKey = this.#simpleTagged?.get(tokenKey);
|
|
193
194
|
if (byTagKey === undefined) {
|
|
194
195
|
return;
|
|
@@ -209,35 +210,26 @@ export class BindingRegistry {
|
|
|
209
210
|
}
|
|
210
211
|
}
|
|
211
212
|
|
|
212
|
-
#isPurePredicateBinding(binding: Binding): boolean {
|
|
213
|
-
const slot = binding.slot;
|
|
214
|
-
const hasPredicate = binding.predicate !== undefined;
|
|
215
|
-
const hasConstraint = slot.name !== undefined || slot.tags.length > 0;
|
|
216
|
-
// Pure predicate = has predicate but no slot constraint (name/tags)
|
|
217
|
-
return hasPredicate && !hasConstraint;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
213
|
#indexSimpleNamedBinding(tokenKey: DependencyKey, binding: Binding): void {
|
|
221
|
-
const
|
|
222
|
-
if (
|
|
214
|
+
const name = simpleNameOf(binding);
|
|
215
|
+
if (name === undefined) {
|
|
223
216
|
return;
|
|
224
217
|
}
|
|
225
218
|
const bindingsByName = (this.#simpleNamed ??= new Map()).getOrInsert(tokenKey, new Map<string, Binding>());
|
|
226
|
-
bindingsByName.set(
|
|
219
|
+
bindingsByName.set(name, binding);
|
|
227
220
|
}
|
|
228
221
|
|
|
229
222
|
#deindexSimpleNamedBinding(tokenKey: DependencyKey, binding: Binding): void {
|
|
230
|
-
const
|
|
231
|
-
if (
|
|
223
|
+
const name = simpleNameOf(binding);
|
|
224
|
+
if (name === undefined) {
|
|
232
225
|
return;
|
|
233
226
|
}
|
|
234
227
|
const bindingsByName = this.#simpleNamed?.get(tokenKey);
|
|
235
228
|
if (bindingsByName === undefined) {
|
|
236
229
|
return;
|
|
237
230
|
}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
bindingsByName.delete(slot.name);
|
|
231
|
+
if (bindingsByName.get(name)?.id === binding.id) {
|
|
232
|
+
bindingsByName.delete(name);
|
|
241
233
|
if (bindingsByName.size === 0) {
|
|
242
234
|
this.#simpleNamed!.delete(tokenKey);
|
|
243
235
|
}
|
|
@@ -246,20 +238,45 @@ export class BindingRegistry {
|
|
|
246
238
|
|
|
247
239
|
#refreshFastDefaultForToken(tokenKey: DependencyKey): void {
|
|
248
240
|
const bindingsForToken = this.#bindings.get(tokenKey);
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
}
|
|
253
|
-
const onlyBinding = bindingsForToken[0]!;
|
|
254
|
-
const isDefaultSlot = onlyBinding.slot.name === undefined && onlyBinding.slot.tags.length === 0;
|
|
255
|
-
if (!isDefaultSlot || onlyBinding.predicate !== undefined) {
|
|
256
|
-
this.#fastDefault.delete(tokenKey);
|
|
241
|
+
const onlyBinding = bindingsForToken?.length === 1 ? bindingsForToken[0]! : undefined;
|
|
242
|
+
if (onlyBinding !== undefined && isDefaultSlotBinding(onlyBinding)) {
|
|
243
|
+
this.#fastDefault.set(tokenKey, onlyBinding);
|
|
257
244
|
return;
|
|
258
245
|
}
|
|
259
|
-
this.#fastDefault.
|
|
246
|
+
this.#fastDefault.delete(tokenKey);
|
|
260
247
|
}
|
|
248
|
+
|
|
261
249
|
/** Whether the deferred table behind `#simpleNamed` has had to be built. */
|
|
262
250
|
get isBuilt(): boolean {
|
|
263
251
|
return this.#simpleNamed !== undefined;
|
|
264
252
|
}
|
|
265
253
|
}
|
|
254
|
+
|
|
255
|
+
/** The name a binding is indexed under, or `undefined` when its slot is more than a plain name. */
|
|
256
|
+
function simpleNameOf(binding: Binding): string | undefined {
|
|
257
|
+
const { name, tags } = binding.slot;
|
|
258
|
+
return name !== undefined && tags.length === 0 ? name : undefined;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* The tag a binding is indexed under, or `undefined` when its slot is more than one plain tag.
|
|
263
|
+
*
|
|
264
|
+
* @remarks A predicate is excluded here — unlike the name index — because the tag index is read
|
|
265
|
+
* without a re-check, so an indexed hit must be unconditional.
|
|
266
|
+
*/
|
|
267
|
+
function simpleTagOf(binding: Binding): BindingTag | undefined {
|
|
268
|
+
const { name, tags } = binding.slot;
|
|
269
|
+
return name === undefined && tags.length === 1 && binding.predicate === undefined ? tags[0] : undefined;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** A binding nothing has to be matched against: the default slot, no predicate. */
|
|
273
|
+
function isDefaultSlotBinding(binding: Binding): boolean {
|
|
274
|
+
const { name, tags } = binding.slot;
|
|
275
|
+
return name === undefined && tags.length === 0 && binding.predicate === undefined;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** A predicate with no slot constraint: last-wins does not apply to it. */
|
|
279
|
+
function isPurePredicateBinding(binding: Binding): boolean {
|
|
280
|
+
const { name, tags } = binding.slot;
|
|
281
|
+
return binding.predicate !== undefined && name === undefined && tags.length === 0;
|
|
282
|
+
}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Per binding: does resolving it have to go through the activation pipeline?
|
|
3
3
|
*
|
|
4
|
-
* @remarks Versioned on the lifecycle manager, since `onActivation` can be
|
|
4
|
+
* @remarks Versioned on the lifecycle manager plus the own registry, since `onActivation` can be
|
|
5
|
+
* registered at any time and a rebind mints binding ids the memo must not keep forever.
|
|
5
6
|
*/
|
|
6
7
|
import type { Binding } from "#/binding";
|
|
8
|
+
import type { BindingRegistry } from "#/registry";
|
|
7
9
|
import type { ClassIntrospector } from "#/resolution/class-introspector";
|
|
8
10
|
import type { LifecycleManager } from "#/resolution/lifecycle";
|
|
9
11
|
import type { BindingIdentifier } from "#/types";
|
|
@@ -16,27 +18,31 @@ export class ActivationNeedCache {
|
|
|
16
18
|
#version = -1;
|
|
17
19
|
readonly #lifecycle: LifecycleManager;
|
|
18
20
|
readonly #classes: ClassIntrospector;
|
|
21
|
+
readonly #registry: BindingRegistry;
|
|
19
22
|
|
|
20
|
-
constructor(lifecycle: LifecycleManager, classes: ClassIntrospector) {
|
|
23
|
+
constructor(lifecycle: LifecycleManager, classes: ClassIntrospector, registry: BindingRegistry) {
|
|
21
24
|
this.#lifecycle = lifecycle;
|
|
22
25
|
this.#classes = classes;
|
|
26
|
+
this.#registry = registry;
|
|
23
27
|
}
|
|
24
28
|
|
|
25
29
|
needsActivation<const Value>(binding: Binding<Value>): boolean {
|
|
30
|
+
// The chain writes a binding's own hook in place with no version anything here can see, so it
|
|
31
|
+
// is read fresh on every call; the memo covers only container hooks and lifecycle metadata.
|
|
32
|
+
if (binding.kind !== "alias" && binding.onActivation !== undefined) {
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
26
35
|
const lifecycleVersion = this.#lifecycle.activationVersion;
|
|
27
36
|
// No hooks registered anywhere and none on the binding: only classes can still surprise us,
|
|
28
37
|
// via a @postConstruct we have not looked for yet.
|
|
29
|
-
if (
|
|
30
|
-
lifecycleVersion === 0 &&
|
|
31
|
-
binding.kind !== "class" &&
|
|
32
|
-
binding.kind !== "alias" &&
|
|
33
|
-
binding.onActivation === undefined
|
|
34
|
-
) {
|
|
38
|
+
if (lifecycleVersion === 0 && binding.kind !== "class" && binding.kind !== "alias") {
|
|
35
39
|
return false;
|
|
36
40
|
}
|
|
37
|
-
|
|
41
|
+
// The registry version evicts entries for binding ids a rebind has retired.
|
|
42
|
+
const version = lifecycleVersion + this.#registry.version;
|
|
43
|
+
if (this.#version !== version) {
|
|
38
44
|
this.#needByBindingId.clear();
|
|
39
|
-
this.#version =
|
|
45
|
+
this.#version = version;
|
|
40
46
|
}
|
|
41
47
|
const cached = this.#needByBindingId.get(binding.id);
|
|
42
48
|
if (cached !== undefined) {
|
|
@@ -64,8 +70,9 @@ export class ActivationNeedCache {
|
|
|
64
70
|
return this.needsActivation(binding);
|
|
65
71
|
}
|
|
66
72
|
|
|
73
|
+
// Own hooks are answered before the memo, so both computations cover the memoizable rest only.
|
|
67
74
|
#classNeedsActivation<const Value>(binding: Binding<Value> & { kind: "class" }): boolean {
|
|
68
|
-
if (this.#lifecycle.hasActivationHandlers(binding.token)
|
|
75
|
+
if (this.#lifecycle.hasActivationHandlers(binding.token)) {
|
|
69
76
|
return true;
|
|
70
77
|
}
|
|
71
78
|
// Unknown lifecycle metadata: activate once so the first instantiation can settle it.
|
|
@@ -73,9 +80,6 @@ export class ActivationNeedCache {
|
|
|
73
80
|
}
|
|
74
81
|
|
|
75
82
|
#nonClassNeedsActivation<const Value>(binding: Binding<Value>): boolean {
|
|
76
|
-
if (binding.kind !== "alias" && binding.onActivation !== undefined) {
|
|
77
|
-
return true;
|
|
78
|
-
}
|
|
79
83
|
return this.#lifecycle.hasActivationHandlers(binding.token);
|
|
80
84
|
}
|
|
81
85
|
}
|
|
@@ -31,9 +31,19 @@ export const ALIAS_HOP_LIMIT = 32;
|
|
|
31
31
|
/**
|
|
32
32
|
* @since 0.5.0-canary.8
|
|
33
33
|
*/
|
|
34
|
+
const newNameToEntryMap = <Owner>(): Map<string, DefaultLookupEntry<Owner> | null> => new Map();
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @since 0.5.0-canary.9
|
|
38
|
+
*/
|
|
34
39
|
export class BindingLookupCache<Owner> {
|
|
35
40
|
readonly #byToken = new Map<Token<unknown> | Constructor, DefaultLookupEntry<Owner> | null>();
|
|
36
41
|
#version = -1;
|
|
42
|
+
// One entry in front of the map: the two shapes that reach here — an alias, and a token owned by
|
|
43
|
+
// a parent — are both resolved in a loop over the same token. `null` is a real answer, so absence
|
|
44
|
+
// is tracked by the token slot rather than by the entry.
|
|
45
|
+
#lastToken: Token<unknown> | Constructor | undefined;
|
|
46
|
+
#lastEntry: DefaultLookupEntry<Owner> | null = null;
|
|
37
47
|
readonly #byTokenAndName = new Map<Token<unknown> | Constructor, Map<string, DefaultLookupEntry<Owner> | null>>();
|
|
38
48
|
#namedVersion = -1;
|
|
39
49
|
|
|
@@ -62,12 +72,17 @@ export class BindingLookupCache<Owner> {
|
|
|
62
72
|
if (version !== this.#version) {
|
|
63
73
|
this.#byToken.clear();
|
|
64
74
|
this.#version = version;
|
|
75
|
+
this.#lastToken = undefined;
|
|
76
|
+
} else if (token === this.#lastToken) {
|
|
77
|
+
return this.#lastEntry;
|
|
65
78
|
}
|
|
66
79
|
let entry = this.#byToken.get(token);
|
|
67
80
|
if (entry === undefined) {
|
|
68
81
|
entry = this.#foldAliases(token);
|
|
69
82
|
this.#byToken.set(token, entry);
|
|
70
83
|
}
|
|
84
|
+
this.#lastToken = token;
|
|
85
|
+
this.#lastEntry = entry;
|
|
71
86
|
return entry;
|
|
72
87
|
}
|
|
73
88
|
|
|
@@ -78,8 +93,9 @@ export class BindingLookupCache<Owner> {
|
|
|
78
93
|
this.#byTokenAndName.clear();
|
|
79
94
|
this.#namedVersion = version;
|
|
80
95
|
}
|
|
81
|
-
//
|
|
82
|
-
|
|
96
|
+
// Computed, not eager: this runs on every named resolve, and `getOrInsert(token, new Map())`
|
|
97
|
+
// would allocate a Map per call only to discard it on the hit that follows.
|
|
98
|
+
const byName = this.#byTokenAndName.getOrInsertComputed(token, newNameToEntryMap);
|
|
83
99
|
let entry = byName.get(name);
|
|
84
100
|
if (entry === undefined) {
|
|
85
101
|
entry = this.#findNamedInChain(token, name);
|