@codefast/di 0.11.0 → 0.12.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 (77) hide show
  1. package/CHANGELOG.md +111 -0
  2. package/README.md +26 -12
  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 -2
  6. package/dist/container/container.js +107 -64
  7. package/dist/core/binding-builders.d.ts +114 -0
  8. package/dist/core/binding-declaration.d.ts +7 -0
  9. package/dist/core/binding-declaration.js +3 -2
  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.js +5 -5
  17. package/dist/decorators/injectable.js +2 -2
  18. package/dist/decorators/{lifecycle-decorators.js → lifecycle.js} +3 -3
  19. package/dist/decorators/{decorator-metadata.js → metadata-record.js} +1 -1
  20. package/dist/{errors/errors.d.ts → errors.d.ts} +7 -6
  21. package/dist/{errors/errors.js → errors.js} +8 -7
  22. package/dist/index.d.ts +12 -10
  23. package/dist/index.js +6 -6
  24. package/dist/injection/descriptor.d.ts +1 -1
  25. package/dist/introspection/dependency-graph.d.ts +1 -1
  26. package/dist/introspection/dependency-graph.js +3 -4
  27. package/dist/introspection/explanation.d.ts +101 -0
  28. package/dist/introspection/explanation.js +151 -0
  29. package/dist/introspection/inspector.d.ts +9 -2
  30. package/dist/introspection/inspector.js +25 -20
  31. package/dist/lifecycle/{lifecycle-manager.d.ts → hooks.d.ts} +9 -2
  32. package/dist/lifecycle/{lifecycle-manager.js → hooks.js} +45 -28
  33. package/dist/lifecycle/{scope-manager.d.ts → scopes.d.ts} +3 -5
  34. package/dist/lifecycle/{scope-manager.js → scopes.js} +8 -19
  35. package/dist/metadata/{metadata-reader-token.d.ts → reader-token.d.ts} +1 -1
  36. package/dist/metadata/{symbol-metadata-reader.d.ts → symbol-reader.d.ts} +1 -1
  37. package/dist/metadata/{symbol-metadata-reader.js → symbol-reader.js} +1 -1
  38. package/dist/metadata/{metadata-types.d.ts → types.d.ts} +1 -1
  39. package/dist/metadata/types.js +1 -0
  40. package/dist/metadata/verify.d.ts +43 -0
  41. package/dist/metadata/verify.js +133 -0
  42. package/dist/metadata/{verifying-metadata-reader.d.ts → verifying-reader.d.ts} +1 -1
  43. package/dist/metadata/{verifying-metadata-reader.js → verifying-reader.js} +2 -2
  44. package/dist/resolution/cache/activation-need.d.ts +1 -1
  45. package/dist/resolution/cache/activation-need.js +5 -1
  46. package/dist/resolution/cache/class-introspector.d.ts +27 -36
  47. package/dist/resolution/cache/class-introspector.js +61 -159
  48. package/dist/resolution/cache/{binding-lookup-cache.d.ts → lookup.d.ts} +7 -2
  49. package/dist/resolution/cache/{binding-lookup-cache.js → lookup.js} +40 -8
  50. package/dist/resolution/context.d.ts +1 -1
  51. package/dist/resolution/context.js +1 -1
  52. package/dist/resolution/{path/resolution-path.js → path.js} +1 -1
  53. package/dist/resolution/plan/{plan-codegen.d.ts → codegen.d.ts} +2 -4
  54. package/dist/resolution/plan/{plan-codegen.js → codegen.js} +2 -4
  55. package/dist/resolution/plan/{instantiation-plan.d.ts → compiler.d.ts} +1 -1
  56. package/dist/resolution/plan/{instantiation-plan.js → compiler.js} +4 -4
  57. package/dist/resolution/resolver.d.ts +7 -7
  58. package/dist/resolution/resolver.js +308 -121
  59. package/dist/resolution/select/{binding-select.d.ts → candidates.d.ts} +30 -0
  60. package/dist/resolution/select/{binding-select.js → candidates.js} +51 -10
  61. package/dist/resolution/select/constraints.js +1 -1
  62. package/package.json +79 -71
  63. package/dist/core/binding-scope.d.ts +0 -12
  64. package/dist/core/binding-scope.js +0 -12
  65. /package/dist/{ambient/active-container.d.ts → ambient-container.d.ts} +0 -0
  66. /package/dist/{ambient/active-container.js → ambient-container.js} +0 -0
  67. /package/dist/{metadata/metadata-types.js → core/binding-builders.js} +0 -0
  68. /package/dist/decorators/{lifecycle-decorators.d.ts → lifecycle.d.ts} +0 -0
  69. /package/dist/decorators/{decorator-metadata.d.ts → metadata-record.d.ts} +0 -0
  70. /package/dist/injection/{resolve-options.d.ts → dependency-slot.d.ts} +0 -0
  71. /package/dist/injection/{resolve-options.js → dependency-slot.js} +0 -0
  72. /package/dist/{errors → introspection}/diagnostics.d.ts +0 -0
  73. /package/dist/{errors → introspection}/diagnostics.js +0 -0
  74. /package/dist/metadata/{metadata-keys.d.ts → keys.d.ts} +0 -0
  75. /package/dist/metadata/{metadata-keys.js → keys.js} +0 -0
  76. /package/dist/metadata/{metadata-reader-token.js → reader-token.js} +0 -0
  77. /package/dist/resolution/{path/resolution-path.d.ts → path.d.ts} +0 -0
@@ -1,11 +1,12 @@
1
- import type { BindToBuilder } from "#core/binding";
1
+ import type { BindToBuilder } from "#core/binding-builders";
2
2
  import type { AsyncModule, SyncModule } from "#core/module";
3
3
  import type { Token } from "#core/token";
4
4
  import type { ActivationHandler, BindingIdentifier, Constructor, DeactivationHandler, ResolveOptions } from "#core/types";
5
5
  import type { AutoRegisterRegistry } from "#decorators/injectable";
6
6
  import type { ContainerGraphJson, GraphOptions } from "#introspection/dependency-graph";
7
+ import type { ExplainOptions, ResolutionExplanation } from "#introspection/explanation";
7
8
  import type { BindingSnapshot, ContainerSnapshot } from "#introspection/inspector";
8
- import type { MetadataReader } from "#metadata/metadata-types";
9
+ import type { MetadataReader } from "#metadata/types";
9
10
  /**
10
11
  * The public surface: binding, resolving, modules, lifecycle hooks, child scopes, and disposal.
11
12
  *
@@ -42,6 +43,14 @@ export interface Container {
42
43
  hasOwn<Names extends string = string>(token: Token<unknown, Names> | Constructor, options?: NoInfer<ResolveOptions<Names>>): boolean;
43
44
  lookupBindings<Value>(token: Token<Value> | Constructor<Value>): ReadonlyArray<BindingSnapshot>;
44
45
  inspect(): ContainerSnapshot;
46
+ /**
47
+ * Explains why a request selects the binding it does, reading each registry the way `resolve` does and deciding
48
+ * by the same rules, without instantiating anything.
49
+ *
50
+ * @remarks A `when()` predicate runs here as it would in `resolve`. `options.ancestors` names the resolutions the
51
+ * request is nested in, so a predicate that reads the parent sees the one it would.
52
+ */
53
+ explain<Names extends string = string>(token: Token<unknown, Names> | Constructor, options?: NoInfer<ExplainOptions<Names>>): ResolutionExplanation;
45
54
  generateDependencyGraph(options?: GraphOptions): ContainerGraphJson;
46
55
  }
47
56
  /**
@@ -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, SyncDisposalNotSupportedError, 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 { 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,22 @@ 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
+ this.#scope.deleteScoped(binding.scopedCacheKey);
227
+ return pairs;
228
+ }
232
229
  /**
233
230
  * Whether a constant still owes its deactivation.
234
231
  *
@@ -246,10 +243,11 @@ class DefaultContainer {
246
243
  return;
247
244
  }
248
245
  const reader = this.#getMetadataReader();
246
+ const lifecycle = this.#lifecycle;
249
247
  let errors;
250
248
  for (let index = 0; index < pairs.length; index += 2) {
251
249
  try {
252
- this.#lifecycle.runDeactivationSync(pairs[index], pairs[index + 1], reader);
250
+ lifecycle.runDeactivationSync(pairs[index], pairs[index + 1], reader);
253
251
  }
254
252
  catch (error) {
255
253
  (errors ??= []).push(error);
@@ -264,10 +262,11 @@ class DefaultContainer {
264
262
  return;
265
263
  }
266
264
  const reader = this.#getMetadataReader();
265
+ const lifecycle = this.#lifecycle;
267
266
  let errors;
268
267
  for (let index = 0; index < pairs.length; index += 2) {
269
268
  try {
270
- await this.#lifecycle.runDeactivation(pairs[index], pairs[index + 1], reader);
269
+ await lifecycle.runDeactivation(pairs[index], pairs[index + 1], reader);
271
270
  }
272
271
  catch (error) {
273
272
  (errors ??= []).push(error);
@@ -317,14 +316,18 @@ class DefaultContainer {
317
316
  return new BindingChain(token, this.#rebindRegistration());
318
317
  }
319
318
  #displacingRegistration;
320
- /** A registration whose displaced binding is deactivated instead of parked, for `rebind`. */
319
+ /** A registration whose displaced binding is deactivated on the spot instead of kept for dispose, for `rebind`. */
321
320
  #rebindRegistration() {
322
321
  return (this.#displacingRegistration ??= {
323
322
  registry: this.#registry,
324
323
  scope: this.#scope,
325
324
  moduleBindingIds: undefined,
325
+ // One binding at a time, so the hot-swap rebind that owes nothing allocates nothing.
326
326
  deactivateDisplaced: (binding) => {
327
- this.#deactivatePairsSync(this.#drainSingletons([binding]));
327
+ const pairs = this.#drainSingleton(binding, undefined);
328
+ if (pairs !== undefined) {
329
+ this.#deactivatePairsSync(pairs);
330
+ }
328
331
  },
329
332
  });
330
333
  }
@@ -530,9 +533,10 @@ class DefaultContainer {
530
533
  this.#assertNotDisposed();
531
534
  this.#lifecycle.registerDeactivation(token, handler);
532
535
  }
533
- // ── Resolution ─────────────────────────────────────────────────────────────────────────────────────────────────────
534
- resolve(token, options) {
535
- this.#assertNotDisposed();
536
+ static #rootResolve(token, options) {
537
+ if (this.#disposed) {
538
+ throw new DisposedContainerError();
539
+ }
536
540
  const rootStack = this.#resolver.rootStack;
537
541
  // A resolve already holding the shared stack means this one is nested; it mints its own.
538
542
  if (rootStack.length !== 0) {
@@ -545,8 +549,28 @@ class DefaultContainer {
545
549
  }
546
550
  return this.#resolver.resolve(token, options, rootStack);
547
551
  }
552
+ // The root's body again behind the child's check, so neither role's compiled resolve carries the other's path.
553
+ static #childResolve(token, options) {
554
+ this.#assertChainLive();
555
+ const rootStack = this.#resolver.rootStack;
556
+ if (rootStack.length !== 0) {
557
+ return options === undefined
558
+ ? this.#resolver.resolveFromContext(token, [])
559
+ : this.#resolver.resolve(token, options, []);
560
+ }
561
+ if (options === undefined) {
562
+ return this.#resolver.resolveFromContext(token, rootStack);
563
+ }
564
+ return this.#resolver.resolve(token, options, rootStack);
565
+ }
548
566
  resolveAsync(token, options) {
549
- this.#assertNotDisposed();
567
+ // Not `async`: the guard's throw becomes the rejection this method promises.
568
+ try {
569
+ this.#assertNotDisposed();
570
+ }
571
+ catch (disposedError) {
572
+ return Promise.reject(disposedError);
573
+ }
550
574
  if (options === undefined) {
551
575
  return this.#resolver.resolveAsyncFromRoot(token);
552
576
  }
@@ -560,7 +584,12 @@ class DefaultContainer {
560
584
  : this.#resolver.resolveOptional(token, options, []);
561
585
  }
562
586
  resolveOptionalAsync(token, options) {
563
- this.#assertNotDisposed();
587
+ try {
588
+ this.#assertNotDisposed();
589
+ }
590
+ catch (disposedError) {
591
+ return Promise.reject(disposedError);
592
+ }
564
593
  return this.#resolver.resolveOptionalAsync(token, options, [], ROOT_BRANCH);
565
594
  }
566
595
  resolveAll(token, options) {
@@ -574,7 +603,12 @@ class DefaultContainer {
574
603
  : this.#resolver.resolveAll(token, options, []);
575
604
  }
576
605
  resolveAllAsync(token, options) {
577
- this.#assertNotDisposed();
606
+ try {
607
+ this.#assertNotDisposed();
608
+ }
609
+ catch (disposedError) {
610
+ return Promise.reject(disposedError);
611
+ }
578
612
  if (options === undefined) {
579
613
  return this.#resolver.resolveRootCollectionAsync(token);
580
614
  }
@@ -583,7 +617,10 @@ class DefaultContainer {
583
617
  // ── Child ──────────────────────────────────────────────────────────────────────────────────────────────────────────
584
618
  createChild() {
585
619
  this.#assertNotDisposed();
586
- return new DefaultContainer(this);
620
+ const child = new DefaultContainer(this);
621
+ // Born confirmed live: the parent's chain was checked just above, so the child's first read skips the walk.
622
+ child.#liveGeneration = disposeEpochRef.value;
623
+ return child;
587
624
  }
588
625
  // ── Dispose ────────────────────────────────────────────────────────────────────────────────────────────────────────
589
626
  dispose() {
@@ -879,33 +916,39 @@ class DefaultContainer {
879
916
  this.#assertNotDisposed();
880
917
  return this.#getInspector().inspect();
881
918
  }
919
+ explain(token, options) {
920
+ this.#assertNotDisposed();
921
+ return explainRequest([this.#registry, ...this.#ancestorRegistries()], token, options);
922
+ }
882
923
  generateDependencyGraph(options) {
883
924
  this.#assertNotDisposed();
884
- const ancestorRegistries = [];
925
+ return buildDependencyGraph(this.#registry, this.#getMetadataReader(), options, this.#ancestorRegistries());
926
+ }
927
+ /** The registries above this container, nearest first — the order every chain walk reads them in. */
928
+ #ancestorRegistries() {
929
+ const registries = [];
885
930
  for (let ancestor = this.#parent; ancestor !== undefined; ancestor = ancestor.#parent) {
886
- ancestorRegistries.push(ancestor.#registry);
931
+ registries.push(ancestor.#registry);
887
932
  }
888
- return buildDependencyGraph(this.#registry, this.#getMetadataReader(), options, ancestorRegistries);
933
+ return registries;
889
934
  }
890
935
  // ── 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.
936
+ // Two field reads and no epoch on a live root. This and `#assertChainLive` each stay under V8's small-function
937
+ // size, so every entry point inlines both outright; the walk and the throw sit in `#confirmChainLive`.
899
938
  #assertNotDisposed() {
900
- if (this.#disposed) {
901
- throw new DisposedContainerError();
939
+ if (this.#disposed || this.#parent !== undefined) {
940
+ this.#assertChainLive();
902
941
  }
903
- if (this.#parent !== undefined && disposeEpochRef.value !== this.#liveGeneration) {
904
- this.#refreshAncestors();
942
+ }
943
+ // An unchanged dispose epoch means no container anywhere closed since this chain was last confirmed live;
944
+ // every dispose advances it, this container's own included, so a disposed container always reaches the walk.
945
+ #assertChainLive() {
946
+ if (disposeEpochRef.value !== this.#liveGeneration) {
947
+ this.#confirmChainLive();
905
948
  }
906
949
  }
907
- #refreshAncestors() {
908
- if (this.#parent.#isChainDisposed()) {
950
+ #confirmChainLive() {
951
+ if (this.#isChainDisposed()) {
909
952
  throw new DisposedContainerError();
910
953
  }
911
954
  this.#liveGeneration = disposeEpochRef.value;
@@ -0,0 +1,114 @@
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()`, sharing the `transient()` surface.
100
+ *
101
+ * @since 0.3.16-canary.0
102
+ */
103
+ export interface ScopedBindingBuilder<Value> extends TransientBindingBuilder<Value> {
104
+ }
105
+ /**
106
+ * The fluent chain a constant enters once a lifecycle hook is added.
107
+ *
108
+ * @since 0.3.16-canary.0
109
+ */
110
+ export interface SingletonLifecycleBuilder<Value> {
111
+ onActivation(fn: ActivationHandler<Value>): this;
112
+ onDeactivation(fn: DeactivationHandler<Value>): this;
113
+ id(): BindingIdentifier;
114
+ }
@@ -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 {};
@@ -1,7 +1,7 @@
1
1
  import { DEFAULT_BINDING_SLOT, withSlotCriterion } from "#core/binding";
2
2
  import { slotName } from "#core/tag";
3
3
  import { tokenName } from "#core/token";
4
- import { InvalidBindingDeclarationError, ManyBindingSlotError, SelfBindingRequiresClassError } from "#errors/errors";
4
+ import { InvalidBindingDeclarationError, ManyBindingSlotError, SelfBindingRequiresClassError } from "#errors";
5
5
  import { normalizeToDescriptor } from "#injection/descriptor";
6
6
  // Only this module can mint the symbol, so an entry carrying it was made by `binding()`.
7
7
  const declarationBrand = Symbol("di:binding-declaration");
@@ -83,8 +83,9 @@ function slotOf(definition) {
83
83
  }
84
84
  return slot;
85
85
  }
86
- // The value type is erased here, once: the overloads above are what a caller is checked against.
87
86
  /**
87
+ * Implements both overloads with the value type erased once, since a caller is checked against the overloads above.
88
+ *
88
89
  * @since 0.11.0
89
90
  */
90
91
  export function binding(key, definition) {