@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.
Files changed (79) hide show
  1. package/CHANGELOG.md +151 -0
  2. package/README.md +128 -31
  3. package/dist/container/{binding-builders.d.ts → binding-chain.d.ts} +21 -28
  4. package/dist/container/{binding-builders.js → binding-chain.js} +53 -163
  5. package/dist/container/container.d.ts +11 -3
  6. package/dist/container/container.js +176 -102
  7. package/dist/core/binding-builders.d.ts +117 -0
  8. package/dist/core/binding-declaration.d.ts +9 -2
  9. package/dist/core/binding-declaration.js +5 -4
  10. package/dist/core/binding.d.ts +21 -149
  11. package/dist/core/binding.js +15 -25
  12. package/dist/core/module.d.ts +1 -1
  13. package/dist/core/registry.d.ts +9 -24
  14. package/dist/core/registry.js +30 -155
  15. package/dist/core/tag.js +1 -1
  16. package/dist/decorators/inject.d.ts +3 -1
  17. package/dist/decorators/inject.js +5 -5
  18. package/dist/decorators/injectable.js +2 -2
  19. package/dist/decorators/lifecycle.d.ts +20 -0
  20. package/dist/decorators/{lifecycle-decorators.js → lifecycle.js} +7 -3
  21. package/dist/decorators/{decorator-metadata.js → metadata-record.js} +1 -1
  22. package/dist/{errors/errors.d.ts → errors.d.ts} +29 -19
  23. package/dist/{errors/errors.js → errors.js} +43 -25
  24. package/dist/index.d.ts +12 -10
  25. package/dist/index.js +6 -6
  26. package/dist/injection/descriptor.d.ts +1 -1
  27. package/dist/introspection/dependency-graph.d.ts +1 -1
  28. package/dist/introspection/dependency-graph.js +3 -4
  29. package/dist/introspection/explanation.d.ts +101 -0
  30. package/dist/introspection/explanation.js +169 -0
  31. package/dist/introspection/inspector.d.ts +9 -2
  32. package/dist/introspection/inspector.js +25 -20
  33. package/dist/lifecycle/{lifecycle-manager.d.ts → hooks.d.ts} +11 -2
  34. package/dist/lifecycle/{lifecycle-manager.js → hooks.js} +51 -28
  35. package/dist/lifecycle/{scope-manager.d.ts → scopes.d.ts} +23 -6
  36. package/dist/lifecycle/{scope-manager.js → scopes.js} +93 -20
  37. package/dist/metadata/{metadata-keys.d.ts → keys.d.ts} +1 -1
  38. package/dist/metadata/{metadata-keys.js → keys.js} +1 -1
  39. package/dist/metadata/{metadata-reader-token.d.ts → reader-token.d.ts} +1 -1
  40. package/dist/metadata/{symbol-metadata-reader.d.ts → symbol-reader.d.ts} +1 -1
  41. package/dist/metadata/{symbol-metadata-reader.js → symbol-reader.js} +1 -1
  42. package/dist/metadata/{metadata-types.d.ts → types.d.ts} +1 -1
  43. package/dist/metadata/types.js +1 -0
  44. package/dist/metadata/verify.d.ts +43 -0
  45. package/dist/metadata/verify.js +133 -0
  46. package/dist/metadata/{verifying-metadata-reader.d.ts → verifying-reader.d.ts} +1 -1
  47. package/dist/metadata/{verifying-metadata-reader.js → verifying-reader.js} +2 -2
  48. package/dist/resolution/cache/activation-need.d.ts +1 -1
  49. package/dist/resolution/cache/activation-need.js +5 -1
  50. package/dist/resolution/cache/class-introspector.d.ts +27 -36
  51. package/dist/resolution/cache/class-introspector.js +61 -159
  52. package/dist/resolution/cache/{binding-lookup-cache.d.ts → lookup.d.ts} +7 -2
  53. package/dist/resolution/cache/{binding-lookup-cache.js → lookup.js} +40 -8
  54. package/dist/resolution/context.d.ts +1 -1
  55. package/dist/resolution/context.js +1 -1
  56. package/dist/resolution/{path/resolution-path.d.ts → path.d.ts} +12 -2
  57. package/dist/resolution/{path/resolution-path.js → path.js} +23 -3
  58. package/dist/resolution/plan/{plan-codegen.d.ts → codegen.d.ts} +2 -4
  59. package/dist/resolution/plan/{plan-codegen.js → codegen.js} +2 -4
  60. package/dist/resolution/plan/{instantiation-plan.d.ts → compiler.d.ts} +1 -1
  61. package/dist/resolution/plan/{instantiation-plan.js → compiler.js} +4 -4
  62. package/dist/resolution/resolver.d.ts +19 -7
  63. package/dist/resolution/resolver.js +343 -125
  64. package/dist/resolution/select/{binding-select.d.ts → candidates.d.ts} +30 -0
  65. package/dist/resolution/select/{binding-select.js → candidates.js} +51 -10
  66. package/dist/resolution/select/constraints.js +1 -1
  67. package/package.json +80 -72
  68. package/dist/core/binding-scope.d.ts +0 -12
  69. package/dist/core/binding-scope.js +0 -12
  70. package/dist/decorators/lifecycle-decorators.d.ts +0 -14
  71. /package/dist/{ambient/active-container.d.ts → ambient-container.d.ts} +0 -0
  72. /package/dist/{ambient/active-container.js → ambient-container.js} +0 -0
  73. /package/dist/{metadata/metadata-types.js → core/binding-builders.js} +0 -0
  74. /package/dist/decorators/{decorator-metadata.d.ts → metadata-record.d.ts} +0 -0
  75. /package/dist/injection/{resolve-options.d.ts → dependency-slot.d.ts} +0 -0
  76. /package/dist/injection/{resolve-options.js → dependency-slot.js} +0 -0
  77. /package/dist/{errors → introspection}/diagnostics.d.ts +0 -0
  78. /package/dist/{errors → introspection}/diagnostics.js +0 -0
  79. /package/dist/metadata/{metadata-reader-token.js → reader-token.js} +0 -0
@@ -1,23 +1,23 @@
1
- import { BindingChain } from "#container/binding-builders";
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 { RESOLUTION_DIAGNOSTICS } from "#errors/diagnostics";
11
- import { AsyncModuleLoadError, CircularDependencyError, DisposedContainerError, InternalError, RebindUnboundTokenError, ScopeViolationError, SyncDisposalNotSupportedError, UnreachableConstraintError, UnreachableLifecycleHookError, } from "#errors/errors";
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/lifecycle-manager";
16
- import { ScopeManager } from "#lifecycle/scope-manager";
17
- import { MetadataReaderToken } from "#metadata/metadata-reader-token";
18
- import { defaultMetadataReader } from "#metadata/symbol-metadata-reader";
19
- import { verifyingMetadataReader } from "#metadata/verifying-metadata-reader";
20
- import { ROOT_BRANCH } from "#resolution/path/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
- // a later restore takes it back out, and every other displaced binding is dropped as before.
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 callbacks, so a constant a module's bind
171
- * displaces is parked for dispose exactly as one displaced by `bind()` 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, onRestored } = this.#ownRegistration();
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
- const binding = bindings[index];
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
- this.#lifecycle.runDeactivationSync(pairs[index], pairs[index + 1], reader);
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 this.#lifecycle.runDeactivation(pairs[index], pairs[index + 1], reader);
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 parked, for `rebind`. */
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.#deactivatePairsSync(this.#drainSingletons([binding]));
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
- // ── Resolution ─────────────────────────────────────────────────────────────────────────────────────────────────────
534
- resolve(token, options) {
535
- this.#assertNotDisposed();
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.#assertNotDisposed();
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
- this.#assertNotDisposed();
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
- this.#assertNotDisposed();
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
- return new DefaultContainer(this);
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 only ever runs for a singleton or constant, so a hook on a token whose every
696
- // binding is scoped or transient can never fire — the builder blocks it, `container.onDeactivation` cannot.
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 — a singleton or a constant.
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) === "singleton") {
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. Follows `toAlias` chains to the
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 A `toDynamic*` dependency is scope-checked like any other — its declared scope is what
763
- * makes it captive — but the DFS does not descend into the factory, whose body is opaque.
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
- return;
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
- #followAliasChainToTerminal(binding, options) {
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 = binding;
809
- while (current !== undefined && current.kind === "alias") {
810
- if (seenAliasIds.has(current.identifier)) {
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(current.identifier);
814
- cyclePath.push(tokenName(current.token));
815
- const nextToken = current.target;
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 undefined;
860
+ return { found: false, missingToken: alias.target, hopNames: cyclePath };
819
861
  }
820
- current = next.binding;
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: every candidate for `injectAll`, else at most one. */
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 this.#resolver.peekCandidateBindingsForValidate(dep.token, options);
869
+ return view
870
+ .peekCandidateBindingsForValidate(dep.token, options)
871
+ .map((binding) => ({ binding, owner: view.ownerForValidate(binding) }));
828
872
  }
829
- const found = this.#resolver.peekBindingForValidate(dep.token, options);
830
- return found === undefined ? [] : [found.binding];
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
- #collectStaticDependencyEdges(binding, reader) {
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
- for (const candidate of this.#peekDependencyCandidates(dep, depOptions)) {
848
- const terminal = this.#followAliasChainToTerminal(candidate, depOptions);
849
- if (terminal !== undefined) {
850
- edges.push({ terminal, depTokenName: tokenName(terminal.token) });
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
- const ancestorRegistries = [];
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
- ancestorRegistries.push(ancestor.#registry);
962
+ registries.push(ancestor.#registry);
887
963
  }
888
- return buildDependencyGraph(this.#registry, this.#getMetadataReader(), options, ancestorRegistries);
964
+ return registries;
889
965
  }
890
966
  // ── Internal ───────────────────────────────────────────────────────────────────────────────────────────────────────
891
- // Hot guard, one call-free epoch read and one compare for every container, root or child. Every
892
- // dispose anywhere — self-dispose included, since `#runDispose` bumps the epoch — moves the epoch,
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
- throw new DisposedContainerError();
970
+ if (this.#disposed || this.#parent !== undefined) {
971
+ this.#assertChainLive();
902
972
  }
903
- if (this.#parent !== undefined && disposeEpochRef.value !== this.#liveGeneration) {
904
- this.#refreshAncestors();
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
- #refreshAncestors() {
908
- if (this.#parent.#isChainDisposed()) {
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" | "scoped" | undefined;
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 {};