@codefast/di 0.5.0 → 0.6.1

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 (266) hide show
  1. package/CHANGELOG.md +712 -0
  2. package/README.md +119 -30
  3. package/dist/ambient/active-container.d.ts +37 -0
  4. package/dist/ambient/active-container.d.ts.map +1 -0
  5. package/dist/ambient/active-container.js +41 -0
  6. package/dist/ambient/active-container.js.map +1 -0
  7. package/dist/container/binding-builders.d.ts +12 -8
  8. package/dist/container/binding-builders.d.ts.map +1 -1
  9. package/dist/container/binding-builders.js +62 -31
  10. package/dist/container/binding-builders.js.map +1 -1
  11. package/dist/container/container.d.ts +33 -19
  12. package/dist/container/container.d.ts.map +1 -1
  13. package/dist/container/container.js +281 -118
  14. package/dist/container/container.js.map +1 -1
  15. package/dist/{resolution → core}/binding-scope.d.ts +2 -2
  16. package/dist/core/binding-scope.d.ts.map +1 -0
  17. package/dist/core/binding-scope.js.map +1 -0
  18. package/dist/{binding.d.ts → core/binding.d.ts} +70 -15
  19. package/dist/core/binding.d.ts.map +1 -0
  20. package/dist/{binding.js → core/binding.js} +27 -12
  21. package/dist/core/binding.js.map +1 -0
  22. package/dist/core/constraint-requirement.d.ts +53 -0
  23. package/dist/core/constraint-requirement.d.ts.map +1 -0
  24. package/dist/core/constraint-requirement.js +62 -0
  25. package/dist/core/constraint-requirement.js.map +1 -0
  26. package/dist/{constructor-type.d.ts → core/constructor-type.d.ts} +1 -1
  27. package/dist/core/constructor-type.d.ts.map +1 -0
  28. package/dist/core/constructor-type.js.map +1 -0
  29. package/dist/{module.d.ts → core/module.d.ts} +5 -5
  30. package/dist/core/module.d.ts.map +1 -0
  31. package/dist/{module.js → core/module.js} +4 -4
  32. package/dist/core/module.js.map +1 -0
  33. package/dist/{registry.d.ts → core/registry.d.ts} +27 -7
  34. package/dist/core/registry.d.ts.map +1 -0
  35. package/dist/{registry.js → core/registry.js} +125 -52
  36. package/dist/core/registry.js.map +1 -0
  37. package/dist/core/tag.d.ts +80 -0
  38. package/dist/core/tag.d.ts.map +1 -0
  39. package/dist/core/tag.js +96 -0
  40. package/dist/core/tag.js.map +1 -0
  41. package/dist/{token.d.ts → core/token.d.ts} +2 -6
  42. package/dist/core/token.d.ts.map +1 -0
  43. package/dist/core/token.js +13 -0
  44. package/dist/core/token.js.map +1 -0
  45. package/dist/{types.d.ts → core/types.d.ts} +27 -21
  46. package/dist/core/types.d.ts.map +1 -0
  47. package/dist/core/types.js.map +1 -0
  48. package/dist/decorators/inject.d.ts +5 -49
  49. package/dist/decorators/inject.d.ts.map +1 -1
  50. package/dist/decorators/inject.js +34 -104
  51. package/dist/decorators/inject.js.map +1 -1
  52. package/dist/decorators/injectable.d.ts +25 -5
  53. package/dist/decorators/injectable.d.ts.map +1 -1
  54. package/dist/decorators/injectable.js +2 -3
  55. package/dist/decorators/injectable.js.map +1 -1
  56. package/dist/decorators/lifecycle-decorators.d.ts +4 -2
  57. package/dist/decorators/lifecycle-decorators.d.ts.map +1 -1
  58. package/dist/decorators/lifecycle-decorators.js +19 -25
  59. package/dist/decorators/lifecycle-decorators.js.map +1 -1
  60. package/dist/{resolution → errors}/diagnostics.d.ts +2 -0
  61. package/dist/errors/diagnostics.d.ts.map +1 -0
  62. package/dist/errors/diagnostics.js.map +1 -0
  63. package/dist/{errors.d.ts → errors/errors.d.ts} +89 -4
  64. package/dist/errors/errors.d.ts.map +1 -0
  65. package/dist/{errors.js → errors/errors.js} +129 -7
  66. package/dist/errors/errors.js.map +1 -0
  67. package/dist/index.d.ts +21 -14
  68. package/dist/index.d.ts.map +1 -1
  69. package/dist/index.js +15 -7
  70. package/dist/index.js.map +1 -1
  71. package/dist/injection/descriptor.d.ts +79 -0
  72. package/dist/injection/descriptor.d.ts.map +1 -0
  73. package/dist/injection/descriptor.js +118 -0
  74. package/dist/injection/descriptor.js.map +1 -0
  75. package/dist/{resolution → injection}/resolve-options.d.ts +21 -11
  76. package/dist/injection/resolve-options.d.ts.map +1 -0
  77. package/dist/injection/resolve-options.js +101 -0
  78. package/dist/injection/resolve-options.js.map +1 -0
  79. package/dist/introspection/dependency-graph.d.ts +16 -5
  80. package/dist/introspection/dependency-graph.d.ts.map +1 -1
  81. package/dist/introspection/dependency-graph.js +138 -57
  82. package/dist/introspection/dependency-graph.js.map +1 -1
  83. package/dist/introspection/graph-adapters/cytoscape.d.ts +6 -3
  84. package/dist/introspection/graph-adapters/cytoscape.d.ts.map +1 -1
  85. package/dist/introspection/graph-adapters/cytoscape.js +3 -0
  86. package/dist/introspection/graph-adapters/cytoscape.js.map +1 -1
  87. package/dist/introspection/graph-adapters/dot.d.ts.map +1 -1
  88. package/dist/introspection/graph-adapters/dot.js +12 -5
  89. package/dist/introspection/graph-adapters/dot.js.map +1 -1
  90. package/dist/introspection/graph-adapters/mermaid.d.ts +9 -0
  91. package/dist/introspection/graph-adapters/mermaid.d.ts.map +1 -0
  92. package/dist/introspection/graph-adapters/mermaid.js +47 -0
  93. package/dist/introspection/graph-adapters/mermaid.js.map +1 -0
  94. package/dist/introspection/graph-adapters/reactflow.d.ts +6 -3
  95. package/dist/introspection/graph-adapters/reactflow.d.ts.map +1 -1
  96. package/dist/introspection/graph-adapters/reactflow.js +5 -1
  97. package/dist/introspection/graph-adapters/reactflow.js.map +1 -1
  98. package/dist/introspection/inspector.d.ts +4 -4
  99. package/dist/introspection/inspector.d.ts.map +1 -1
  100. package/dist/introspection/inspector.js +10 -6
  101. package/dist/introspection/inspector.js.map +1 -1
  102. package/dist/lifecycle/lifecycle-manager.d.ts +27 -0
  103. package/dist/lifecycle/lifecycle-manager.d.ts.map +1 -0
  104. package/dist/{resolution/lifecycle.js → lifecycle/lifecycle-manager.js} +39 -9
  105. package/dist/lifecycle/lifecycle-manager.js.map +1 -0
  106. package/dist/lifecycle/scope-manager.d.ts +48 -0
  107. package/dist/lifecycle/scope-manager.d.ts.map +1 -0
  108. package/dist/lifecycle/scope-manager.js +135 -0
  109. package/dist/lifecycle/scope-manager.js.map +1 -0
  110. package/dist/metadata/metadata-reader-token.d.ts +1 -1
  111. package/dist/metadata/metadata-reader-token.d.ts.map +1 -1
  112. package/dist/metadata/metadata-reader-token.js +1 -1
  113. package/dist/metadata/metadata-reader-token.js.map +1 -1
  114. package/dist/metadata/metadata-types.d.ts +9 -9
  115. package/dist/metadata/metadata-types.d.ts.map +1 -1
  116. package/dist/metadata/symbol-metadata-reader.d.ts +19 -6
  117. package/dist/metadata/symbol-metadata-reader.d.ts.map +1 -1
  118. package/dist/metadata/symbol-metadata-reader.js +89 -9
  119. package/dist/metadata/symbol-metadata-reader.js.map +1 -1
  120. package/dist/metadata/verifying-metadata-reader.d.ts +16 -0
  121. package/dist/metadata/verifying-metadata-reader.d.ts.map +1 -0
  122. package/dist/metadata/verifying-metadata-reader.js +32 -0
  123. package/dist/metadata/verifying-metadata-reader.js.map +1 -0
  124. package/dist/resolution/{activation-need.d.ts → cache/activation-need.d.ts} +5 -5
  125. package/dist/resolution/cache/activation-need.d.ts.map +1 -0
  126. package/dist/resolution/cache/activation-need.js.map +1 -0
  127. package/dist/resolution/{binding-lookup-cache.d.ts → cache/binding-lookup-cache.d.ts} +9 -5
  128. package/dist/resolution/cache/binding-lookup-cache.d.ts.map +1 -0
  129. package/dist/resolution/{binding-lookup-cache.js → cache/binding-lookup-cache.js} +55 -0
  130. package/dist/resolution/cache/binding-lookup-cache.js.map +1 -0
  131. package/dist/resolution/cache/class-introspector.d.ts +66 -0
  132. package/dist/resolution/cache/class-introspector.d.ts.map +1 -0
  133. package/dist/resolution/cache/class-introspector.js +194 -0
  134. package/dist/resolution/cache/class-introspector.js.map +1 -0
  135. package/dist/resolution/context.d.ts +84 -0
  136. package/dist/resolution/context.d.ts.map +1 -0
  137. package/dist/resolution/{environment.js → context.js} +54 -80
  138. package/dist/resolution/context.js.map +1 -0
  139. package/dist/resolution/path/resolution-path.d.ts +94 -0
  140. package/dist/resolution/path/resolution-path.d.ts.map +1 -0
  141. package/dist/resolution/path/resolution-path.js +117 -0
  142. package/dist/resolution/path/resolution-path.js.map +1 -0
  143. package/dist/resolution/{instantiation-plan.d.ts → plan/instantiation-plan.d.ts} +37 -7
  144. package/dist/resolution/plan/instantiation-plan.d.ts.map +1 -0
  145. package/dist/resolution/plan/instantiation-plan.js +419 -0
  146. package/dist/resolution/plan/instantiation-plan.js.map +1 -0
  147. package/dist/resolution/resolver.d.ts +28 -28
  148. package/dist/resolution/resolver.d.ts.map +1 -1
  149. package/dist/resolution/resolver.js +581 -239
  150. package/dist/resolution/resolver.js.map +1 -1
  151. package/dist/resolution/select/binding-select.d.ts +34 -0
  152. package/dist/resolution/select/binding-select.d.ts.map +1 -0
  153. package/dist/resolution/select/binding-select.js +175 -0
  154. package/dist/resolution/select/binding-select.js.map +1 -0
  155. package/dist/resolution/{constraints.d.ts → select/constraints.d.ts} +13 -12
  156. package/dist/resolution/select/constraints.d.ts.map +1 -0
  157. package/dist/resolution/{constraints.js → select/constraints.js} +33 -22
  158. package/dist/resolution/select/constraints.js.map +1 -0
  159. package/package.json +160 -20
  160. package/src/ambient/active-container.ts +65 -0
  161. package/src/container/binding-builders.ts +79 -44
  162. package/src/container/container.ts +357 -170
  163. package/src/{resolution → core}/binding-scope.ts +2 -2
  164. package/src/{binding.ts → core/binding.ts} +101 -39
  165. package/src/core/constraint-requirement.ts +94 -0
  166. package/src/{constructor-type.ts → core/constructor-type.ts} +1 -1
  167. package/src/{module.ts → core/module.ts} +10 -10
  168. package/src/core/registry.ts +365 -0
  169. package/src/core/tag.ts +158 -0
  170. package/src/{token.ts → core/token.ts} +2 -14
  171. package/src/{types.ts → core/types.ts} +37 -34
  172. package/src/decorators/inject.ts +39 -175
  173. package/src/decorators/injectable.ts +40 -10
  174. package/src/decorators/lifecycle-decorators.ts +22 -34
  175. package/src/{resolution → errors}/diagnostics.ts +2 -0
  176. package/src/{errors.ts → errors/errors.ts} +152 -8
  177. package/src/index.ts +43 -15
  178. package/src/injection/descriptor.ts +206 -0
  179. package/src/injection/resolve-options.ts +144 -0
  180. package/src/introspection/dependency-graph.ts +223 -63
  181. package/src/introspection/graph-adapters/cytoscape.ts +9 -3
  182. package/src/introspection/graph-adapters/dot.ts +13 -5
  183. package/src/introspection/graph-adapters/mermaid.ts +61 -0
  184. package/src/introspection/graph-adapters/reactflow.ts +11 -4
  185. package/src/introspection/inspector.ts +16 -12
  186. package/src/{resolution/lifecycle.ts → lifecycle/lifecycle-manager.ts} +68 -32
  187. package/src/lifecycle/scope-manager.ts +156 -0
  188. package/src/metadata/metadata-reader-token.ts +2 -2
  189. package/src/metadata/metadata-types.ts +9 -9
  190. package/src/metadata/symbol-metadata-reader.ts +95 -18
  191. package/src/metadata/verifying-metadata-reader.ts +41 -0
  192. package/src/resolution/{activation-need.ts → cache/activation-need.ts} +8 -8
  193. package/src/resolution/{binding-lookup-cache.ts → cache/binding-lookup-cache.ts} +63 -5
  194. package/src/resolution/cache/class-introspector.ts +251 -0
  195. package/src/resolution/{environment.ts → context.ts} +84 -145
  196. package/src/resolution/path/resolution-path.ts +167 -0
  197. package/src/resolution/plan/instantiation-plan.ts +615 -0
  198. package/src/resolution/resolver.ts +663 -313
  199. package/src/resolution/select/binding-select.ts +210 -0
  200. package/src/resolution/select/constraints.ts +130 -0
  201. package/dist/binding.d.ts.map +0 -1
  202. package/dist/binding.js.map +0 -1
  203. package/dist/constructor-type.d.ts.map +0 -1
  204. package/dist/constructor-type.js.map +0 -1
  205. package/dist/errors.d.ts.map +0 -1
  206. package/dist/errors.js.map +0 -1
  207. package/dist/module.d.ts.map +0 -1
  208. package/dist/module.js.map +0 -1
  209. package/dist/registry.d.ts.map +0 -1
  210. package/dist/registry.js.map +0 -1
  211. package/dist/resolution/activation-need.d.ts.map +0 -1
  212. package/dist/resolution/activation-need.js.map +0 -1
  213. package/dist/resolution/binding-lookup-cache.d.ts.map +0 -1
  214. package/dist/resolution/binding-lookup-cache.js.map +0 -1
  215. package/dist/resolution/binding-scope.d.ts.map +0 -1
  216. package/dist/resolution/binding-scope.js.map +0 -1
  217. package/dist/resolution/binding-select.d.ts +0 -23
  218. package/dist/resolution/binding-select.d.ts.map +0 -1
  219. package/dist/resolution/binding-select.js +0 -121
  220. package/dist/resolution/binding-select.js.map +0 -1
  221. package/dist/resolution/class-introspector.d.ts +0 -27
  222. package/dist/resolution/class-introspector.d.ts.map +0 -1
  223. package/dist/resolution/class-introspector.js +0 -60
  224. package/dist/resolution/class-introspector.js.map +0 -1
  225. package/dist/resolution/constraints.d.ts.map +0 -1
  226. package/dist/resolution/constraints.js.map +0 -1
  227. package/dist/resolution/diagnostics.d.ts.map +0 -1
  228. package/dist/resolution/diagnostics.js.map +0 -1
  229. package/dist/resolution/environment.d.ts +0 -94
  230. package/dist/resolution/environment.d.ts.map +0 -1
  231. package/dist/resolution/environment.js.map +0 -1
  232. package/dist/resolution/instantiation-plan.d.ts.map +0 -1
  233. package/dist/resolution/instantiation-plan.js +0 -183
  234. package/dist/resolution/instantiation-plan.js.map +0 -1
  235. package/dist/resolution/lifecycle.d.ts +0 -23
  236. package/dist/resolution/lifecycle.d.ts.map +0 -1
  237. package/dist/resolution/lifecycle.js.map +0 -1
  238. package/dist/resolution/resolution-path.d.ts +0 -98
  239. package/dist/resolution/resolution-path.d.ts.map +0 -1
  240. package/dist/resolution/resolution-path.js +0 -98
  241. package/dist/resolution/resolution-path.js.map +0 -1
  242. package/dist/resolution/resolve-options.d.ts.map +0 -1
  243. package/dist/resolution/resolve-options.js +0 -56
  244. package/dist/resolution/resolve-options.js.map +0 -1
  245. package/dist/resolution/scope.d.ts +0 -28
  246. package/dist/resolution/scope.d.ts.map +0 -1
  247. package/dist/resolution/scope.js +0 -88
  248. package/dist/resolution/scope.js.map +0 -1
  249. package/dist/token.d.ts.map +0 -1
  250. package/dist/token.js +0 -22
  251. package/dist/token.js.map +0 -1
  252. package/dist/types.d.ts.map +0 -1
  253. package/dist/types.js.map +0 -1
  254. package/src/registry.ts +0 -282
  255. package/src/resolution/binding-select.ts +0 -154
  256. package/src/resolution/class-introspector.ts +0 -74
  257. package/src/resolution/constraints.ts +0 -121
  258. package/src/resolution/instantiation-plan.ts +0 -292
  259. package/src/resolution/resolution-path.ts +0 -156
  260. package/src/resolution/resolve-options.ts +0 -89
  261. package/src/resolution/scope.ts +0 -105
  262. /package/dist/{resolution → core}/binding-scope.js +0 -0
  263. /package/dist/{constructor-type.js → core/constructor-type.js} +0 -0
  264. /package/dist/{types.js → core/types.js} +0 -0
  265. /package/dist/{resolution → errors}/diagnostics.js +0 -0
  266. /package/dist/resolution/{activation-need.js → cache/activation-need.js} +0 -0
@@ -1,8 +1,25 @@
1
- import type { Binding, BindToBuilder } from "#/binding";
2
- import { NO_INSTANCE } from "#/binding";
3
1
  import type { BindingRegistration } from "#/container/binding-builders";
4
2
  import { BindingChain } from "#/container/binding-builders";
3
+ import type { Binding, BindingBuilder, BindToBuilder, ConstantBinding } from "#/core/binding";
4
+ import { NO_INSTANCE } from "#/core/binding";
5
+ import { effectiveBindingScope } from "#/core/binding-scope";
6
+ import { constraintRequirementsOf } from "#/core/constraint-requirement";
7
+ import type { AsyncModule, AsyncModuleBuilder, ModuleBuilder, SyncModule } from "#/core/module";
8
+ import { isSyncModule, MODULE_SETUP } from "#/core/module";
9
+ import { BindingRegistry } from "#/core/registry";
10
+ import type { Token } from "#/core/token";
11
+ import { tokenName } from "#/core/token";
12
+ import type {
13
+ ActivationHandler,
14
+ BindingIdentifier,
15
+ BindingScope,
16
+ Constructor,
17
+ DeactivationHandler,
18
+ ResolveOptions,
19
+ } from "#/core/types";
5
20
  import type { AutoRegisterRegistry } from "#/decorators/injectable";
21
+ import type { ResolutionDiagnostics } from "#/errors/diagnostics";
22
+ import { RESOLUTION_DIAGNOSTICS } from "#/errors/diagnostics";
6
23
  import {
7
24
  AsyncModuleLoadError,
8
25
  CircularDependencyError,
@@ -11,39 +28,25 @@ import {
11
28
  RebindUnboundTokenError,
12
29
  ScopeViolationError,
13
30
  SyncDisposalNotSupportedError,
14
- } from "#/errors";
31
+ UnreachableConstraintError,
32
+ UnreachableLifecycleHookError,
33
+ } from "#/errors/errors";
34
+ import type { DependencySlot } from "#/injection/resolve-options";
35
+ import { injectionSlotToResolveOptions, bindingSlotToResolveOptions } from "#/injection/resolve-options";
15
36
  import type { ContainerGraphJson, GraphOptions } from "#/introspection/dependency-graph";
16
37
  import { buildDependencyGraph } from "#/introspection/dependency-graph";
17
38
  import type { BindingSnapshot, ContainerSnapshot } from "#/introspection/inspector";
18
39
  import { Inspector } from "#/introspection/inspector";
40
+ import { LifecycleManager } from "#/lifecycle/lifecycle-manager";
41
+ import { ScopeManager } from "#/lifecycle/scope-manager";
19
42
  import { MetadataReaderToken } from "#/metadata/metadata-reader-token";
20
43
  import type { MetadataReader } from "#/metadata/metadata-types";
21
44
  import { defaultMetadataReader } from "#/metadata/symbol-metadata-reader";
22
- import type { AsyncModule, ModuleBuilder, SyncModule } from "#/module";
23
- import type { AsyncModuleBuilder } from "#/module";
24
- import { isSyncModule, MODULE_SETUP } from "#/module";
25
- import { BindingRegistry } from "#/registry";
26
- import { effectiveBindingScope } from "#/resolution/binding-scope";
27
- import type { ResolutionDiagnostics } from "#/resolution/diagnostics";
28
- import { RESOLUTION_DIAGNOSTICS } from "#/resolution/diagnostics";
29
- import { LifecycleManager } from "#/resolution/lifecycle";
30
- import { ROOT_BRANCH } from "#/resolution/resolution-path";
31
- import type { DependencySlot } from "#/resolution/resolve-options";
32
- import { injectionSlotToResolveOptions, bindingSlotToResolveOptions } from "#/resolution/resolve-options";
45
+ import { verifyingMetadataReader } from "#/metadata/verifying-metadata-reader";
46
+ import { ROOT_BRANCH } from "#/resolution/path/resolution-path";
33
47
  import { DependencyResolver } from "#/resolution/resolver";
34
- import { ScopeManager } from "#/resolution/scope";
35
- import type { Token } from "#/token";
36
- import { tokenName } from "#/token";
37
- import type {
38
- ActivationHandler,
39
- BindingIdentifier,
40
- BindingScope,
41
- Constructor,
42
- DeactivationHandler,
43
- ResolveOptions,
44
- } from "#/types";
45
48
 
46
- // ── Container interface ────────────────────────────────────────────────────────
49
+ // ── Container interface ──────────────────────────────────────────────────────────────────────────────────────────────
47
50
 
48
51
  /**
49
52
  * @since 0.3.16-canary.0
@@ -51,12 +54,12 @@ import type {
51
54
  export interface Container {
52
55
  readonly isDisposed: boolean;
53
56
 
54
- bind<const Value>(token: Token<Value> | Constructor<Value>): BindToBuilder<Value>;
57
+ bind<Value>(token: Token<Value> | Constructor<Value>): BindToBuilder<Value>;
55
58
  unbind(tokenOrId: Token<unknown> | Constructor | BindingIdentifier): void;
56
59
  unbindAsync(tokenOrId: Token<unknown> | Constructor | BindingIdentifier): Promise<void>;
57
60
  unbindAll(): void;
58
61
  unbindAllAsync(): Promise<void>;
59
- rebind<const Value>(token: Token<Value> | Constructor<Value>): BindToBuilder<Value>;
62
+ rebind<Value>(token: Token<Value> | Constructor<Value>): BindToBuilder<Value>;
60
63
 
61
64
  load(...modules: Array<SyncModule>): void;
62
65
  loadAsync(...modules: Array<SyncModule | AsyncModule>): Promise<void>;
@@ -64,21 +67,18 @@ export interface Container {
64
67
  unloadAsync(...modules: Array<SyncModule | AsyncModule>): Promise<void>;
65
68
  loadAutoRegistered(registry: AutoRegisterRegistry): number;
66
69
 
67
- onActivation<const Value>(token: Token<Value> | Constructor<Value>, handler: ActivationHandler<Value>): void;
68
- onDeactivation<const Value>(token: Token<Value> | Constructor<Value>, handler: DeactivationHandler<Value>): void;
70
+ onActivation<Value>(token: Token<Value> | Constructor<Value>, handler: ActivationHandler<Value>): void;
71
+ onDeactivation<Value>(token: Token<Value> | Constructor<Value>, handler: DeactivationHandler<Value>): void;
69
72
 
70
- resolve<const Value>(token: Token<Value> | Constructor<Value>, options?: ResolveOptions): Value;
71
- resolveAsync<const Value>(token: Token<Value> | Constructor<Value>, options?: ResolveOptions): Promise<Value>;
72
- resolveOptional<const Value>(token: Token<Value> | Constructor<Value>, options?: ResolveOptions): Value | undefined;
73
- resolveOptionalAsync<const Value>(
73
+ resolve<Value>(token: Token<Value> | Constructor<Value>, options?: ResolveOptions): Value;
74
+ resolveAsync<Value>(token: Token<Value> | Constructor<Value>, options?: ResolveOptions): Promise<Value>;
75
+ resolveOptional<Value>(token: Token<Value> | Constructor<Value>, options?: ResolveOptions): Value | undefined;
76
+ resolveOptionalAsync<Value>(
74
77
  token: Token<Value> | Constructor<Value>,
75
78
  options?: ResolveOptions,
76
79
  ): Promise<Value | undefined>;
77
- resolveAll<const Value>(token: Token<Value> | Constructor<Value>, options?: ResolveOptions): Array<Value>;
78
- resolveAllAsync<const Value>(
79
- token: Token<Value> | Constructor<Value>,
80
- options?: ResolveOptions,
81
- ): Promise<Array<Value>>;
80
+ resolveAll<Value>(token: Token<Value> | Constructor<Value>, options?: ResolveOptions): Array<Value>;
81
+ resolveAllAsync<Value>(token: Token<Value> | Constructor<Value>, options?: ResolveOptions): Promise<Array<Value>>;
82
82
 
83
83
  createChild(): Container;
84
84
 
@@ -91,24 +91,56 @@ export interface Container {
91
91
 
92
92
  has(token: Token<unknown> | Constructor, options?: ResolveOptions): boolean;
93
93
  hasOwn(token: Token<unknown> | Constructor, options?: ResolveOptions): boolean;
94
- lookupBindings<const Value>(token: Token<Value> | Constructor<Value>): ReadonlyArray<BindingSnapshot>;
94
+ lookupBindings<Value>(token: Token<Value> | Constructor<Value>): ReadonlyArray<BindingSnapshot>;
95
95
  inspect(): ContainerSnapshot;
96
96
  generateDependencyGraph(options?: GraphOptions): ContainerGraphJson;
97
97
  }
98
98
 
99
+ /**
100
+ * What a container has to be told before it exists, as opposed to what it can be bound later.
101
+ *
102
+ * @since 0.6.0
103
+ */
104
+ export interface ContainerOptions {
105
+ /**
106
+ * Reader the resolver consults for class metadata, replacing the decorator reader.
107
+ *
108
+ * @remarks Takes precedence over a {@link MetadataReaderToken} binding, and children inherit it.
109
+ * A resolver is handed its reader when it is built, so this is the only way to give the container
110
+ * you are creating a reader of your own.
111
+ */
112
+ readonly metadataReader?: MetadataReader | undefined;
113
+ }
114
+
99
115
  /**
100
116
  * @since 0.3.16-canary.0
101
117
  */
102
118
  export interface ContainerStatic {
103
- create(): Container;
119
+ create(options?: ContainerOptions): Container;
104
120
  fromModules(...modules: Array<SyncModule>): Container;
105
121
  fromModulesAsync(...modules: Array<SyncModule | AsyncModule>): Promise<Container>;
106
122
  }
107
123
 
108
- // ── DefaultContainer ──────────────────────────────────────────────────────────
124
+ // A Record rather than an if-chain, so a new `BindingScope` is a compile error here instead of
125
+ // silently landing in whichever branch happened to be last.
126
+ const APPLY_BINDING_SCOPE: Record<BindingScope, (builder: BindingBuilder<unknown>) => void> = {
127
+ singleton: (builder) => {
128
+ builder.singleton();
129
+ },
130
+ scoped: (builder) => {
131
+ builder.scoped();
132
+ },
133
+ transient: (builder) => {
134
+ builder.transient();
135
+ },
136
+ };
137
+
138
+ // ── DefaultContainer ─────────────────────────────────────────────────────────────────────────────────────────────────
109
139
 
110
140
  class DefaultContainer implements Container {
111
141
  #disposed = false;
142
+ // The one teardown run — every dispose() call returns it once it exists.
143
+ #disposePromise: Promise<void> | undefined;
112
144
  readonly #registry: BindingRegistry;
113
145
  readonly #scope: ScopeManager;
114
146
  readonly #lifecycle: LifecycleManager;
@@ -124,12 +156,12 @@ class DefaultContainer implements Container {
124
156
  // One shared registration for every chain this container's own `bind()` creates.
125
157
  #registration: BindingRegistration | undefined;
126
158
 
127
- constructor(parent?: DefaultContainer) {
159
+ constructor(parent?: DefaultContainer, options?: ContainerOptions) {
128
160
  this.#parent = parent;
129
161
  this.#registry = new BindingRegistry();
130
162
  this.#scope = new ScopeManager(parent !== undefined);
131
163
  this.#lifecycle = new LifecycleManager();
132
- this.#initResolver();
164
+ this.#initResolver(options?.metadataReader);
133
165
  }
134
166
 
135
167
  #getInspector(): Inspector {
@@ -149,21 +181,27 @@ class DefaultContainer implements Container {
149
181
  if (this.#moduleRefs !== undefined || this.#moduleBindingIds !== undefined) {
150
182
  builtSubsystems.push("container.moduleTables");
151
183
  }
152
- if (this.#registry.isBuilt) {
184
+ if (this.#registry.isNamedIndexBuilt) {
153
185
  builtSubsystems.push("registry.namedIndex");
154
186
  }
155
- if (this.#scope.isBuilt) {
187
+ if (this.#registry.isTaggedIndexBuilt) {
188
+ builtSubsystems.push("registry.taggedIndex");
189
+ }
190
+ if (this.#scope.isScopedCacheBuilt) {
156
191
  builtSubsystems.push("scope.scoped");
157
192
  }
158
- if (this.#lifecycle.isBuilt) {
193
+ if (this.#lifecycle.isActivationTableBuilt) {
159
194
  builtSubsystems.push("lifecycle.activationHooks");
160
195
  }
161
196
  return { ...this.#resolver.describeCaches(), scopedInstanceCount: this.#scope.scopedCount, builtSubsystems };
162
197
  }
163
198
 
164
- #initResolver(): void {
165
- const metadataReader = this.#getMetadataReader();
166
- const parentResolver = this.#parent === undefined ? undefined : this.#parent.#resolver;
199
+ #initResolver(configuredReader: MetadataReader | undefined): void {
200
+ const parent = this.#parent;
201
+ const metadataReader = verifyingMetadataReader(
202
+ configuredReader ?? (parent === undefined ? defaultMetadataReader : parent.#readerForChild()),
203
+ );
204
+ const parentResolver = parent === undefined ? undefined : parent.#resolver;
167
205
  this.#resolver = new DependencyResolver(
168
206
  this.#registry,
169
207
  this.#scope,
@@ -174,47 +212,49 @@ class DefaultContainer implements Container {
174
212
  );
175
213
  }
176
214
 
177
- #getMetadataReader(): MetadataReader {
178
- // Check if a custom MetadataReader has been bound
179
- const metaBindings = this.#registry.getAll(MetadataReaderToken);
180
- if (metaBindings.length > 0) {
215
+ /** What a container being constructed under this one inherits: a reader bound here, else this one's. */
216
+ #readerForChild(): MetadataReader {
217
+ if (this.#registry.getAll(MetadataReaderToken).length > 0) {
181
218
  try {
182
- return this.#resolver.resolve(MetadataReaderToken, undefined, [], []);
219
+ return this.#resolver.resolve(MetadataReaderToken, undefined, []);
183
220
  } catch {
184
- // fall through to default
221
+ // An unresolvable reader binding is not worth failing a child over.
185
222
  }
186
223
  }
187
- if (this.#parent !== undefined) {
188
- return this.#parent.#getMetadataReader();
189
- }
190
- return defaultMetadataReader;
224
+ return this.#resolver.metadataReader;
225
+ }
226
+
227
+ /** One reader per container, fixed when its resolver was built — so every path agrees on it. */
228
+ #getMetadataReader(): MetadataReader {
229
+ return this.#resolver.metadataReader;
191
230
  }
192
231
 
193
232
  get isDisposed(): boolean {
194
233
  return this.#disposed;
195
234
  }
196
235
 
197
- // ── Binding ──────────────────────────────────────────────────────────────
236
+ // ── Binding ────────────────────────────────────────────────────────────────────────────────────────────────────────
198
237
 
199
- bind<const Value>(token: Token<Value> | Constructor<Value>): BindToBuilder<Value> {
238
+ bind<Value>(token: Token<Value> | Constructor<Value>): BindToBuilder<Value> {
200
239
  this.#assertNotDisposed();
201
240
  return this.#createBindToBuilder(token);
202
241
  }
203
242
 
204
243
  /** The registration every non-module chain shares, so `bind()` allocates only the builder. */
205
244
  #ownRegistration(): BindingRegistration {
206
- return (this.#registration ??= { registry: this.#registry, moduleBindingIds: undefined });
245
+ return (this.#registration ??= { registry: this.#registry, scope: this.#scope, moduleBindingIds: undefined });
207
246
  }
208
247
 
209
248
  /** One registration per module load, holding that module's id list directly. */
210
249
  #moduleRegistration(moduleRef: object): BindingRegistration {
211
250
  return {
212
251
  registry: this.#registry,
252
+ scope: this.#scope,
213
253
  moduleBindingIds: (this.#moduleBindingIds ??= new Map()).getOrInsert(moduleRef, []),
214
254
  };
215
255
  }
216
256
 
217
- #createBindToBuilder<const Value>(
257
+ #createBindToBuilder<Value>(
218
258
  token: Token<Value> | Constructor<Value>,
219
259
  registration: BindingRegistration = this.#ownRegistration(),
220
260
  ): BindToBuilder<Value> {
@@ -237,51 +277,88 @@ class DefaultContainer implements Container {
237
277
  return this.#drainSingletons(this.#registry.removeByToken(tokenOrId));
238
278
  }
239
279
 
240
- /** Drain scope entries for already-removed bindings; only singletons yield deactivation pairs. */
280
+ /** Drain scope entries for already-removed bindings, and pair each one that still owes a deactivation. */
241
281
  #drainSingletons(bindings: ReadonlyArray<Binding>): Array<[Binding, unknown]> {
242
282
  const pairs: Array<[Binding, unknown]> = [];
243
283
  for (const binding of bindings) {
244
284
  if (binding.instance !== NO_INSTANCE) {
245
285
  pairs.push([binding, binding.instance]);
246
286
  this.#scope.deleteSingleton(binding);
287
+ } else if (this.#owesConstantDeactivation(binding)) {
288
+ pairs.push([binding, binding.value]);
247
289
  }
248
290
  this.#scope.deleteScoped(binding.id);
249
291
  }
250
292
  return pairs;
251
293
  }
252
294
 
253
- #unbindSync(tokenOrId: Token<unknown> | Constructor | BindingIdentifier): void {
295
+ /**
296
+ * Whether a constant still owes its deactivation.
297
+ *
298
+ * @remarks A constant's value is handed in at bind time rather than built on demand, so its hook is
299
+ * owed whether or not anything ever resolved it. Callers check `instance` first: a constant that
300
+ * carries one was cached through activation and is deactivated with that value instead.
301
+ */
302
+ #owesConstantDeactivation(binding: Binding): binding is ConstantBinding<unknown> {
303
+ return (
304
+ binding.kind === "constant" &&
305
+ (binding.onDeactivation !== undefined || this.#lifecycle.hasDeactivationHandlers(binding.token))
306
+ );
307
+ }
308
+
309
+ /** Runs every pair's deactivation even when one throws, then reports what threw. */
310
+ #deactivatePairsSync(pairs: ReadonlyArray<[Binding, unknown]>): void {
311
+ if (pairs.length === 0) {
312
+ return;
313
+ }
254
314
  const reader = this.#getMetadataReader();
255
- for (const [binding, instance] of this.#collectDeactivationPairs(tokenOrId)) {
256
- this.#lifecycle.runDeactivationSync(binding, instance, reader);
315
+ const errors: Array<unknown> = [];
316
+ for (const [binding, instance] of pairs) {
317
+ try {
318
+ this.#lifecycle.runDeactivationSync(binding, instance, reader);
319
+ } catch (error) {
320
+ errors.push(error);
321
+ }
257
322
  }
323
+ throwCollected(errors, "unbind completed, but deactivation hooks threw");
258
324
  }
259
325
 
260
- async unbindAsync(tokenOrId: Token<unknown> | Constructor | BindingIdentifier): Promise<void> {
261
- this.#assertNotDisposed();
326
+ async #deactivatePairs(pairs: ReadonlyArray<[Binding, unknown]>): Promise<void> {
327
+ if (pairs.length === 0) {
328
+ return;
329
+ }
262
330
  const reader = this.#getMetadataReader();
263
- for (const [binding, instance] of this.#collectDeactivationPairs(tokenOrId)) {
264
- await this.#lifecycle.runDeactivation(binding, instance, reader);
331
+ const errors: Array<unknown> = [];
332
+ for (const [binding, instance] of pairs) {
333
+ try {
334
+ await this.#lifecycle.runDeactivation(binding, instance, reader);
335
+ } catch (error) {
336
+ errors.push(error);
337
+ }
265
338
  }
339
+ throwCollected(errors, "unbind completed, but deactivation hooks threw");
340
+ }
341
+
342
+ #unbindSync(tokenOrId: Token<unknown> | Constructor | BindingIdentifier): void {
343
+ this.#deactivatePairsSync(this.#collectDeactivationPairs(tokenOrId));
344
+ }
345
+
346
+ async unbindAsync(tokenOrId: Token<unknown> | Constructor | BindingIdentifier): Promise<void> {
347
+ this.#assertNotDisposed();
348
+ await this.#deactivatePairs(this.#collectDeactivationPairs(tokenOrId));
266
349
  }
267
350
 
268
351
  unbindAll(): void {
269
352
  this.#assertNotDisposed();
270
- const reader = this.#getMetadataReader();
271
- for (const [binding, instance] of this.#drainSingletons(this.#registry.clear())) {
272
- this.#lifecycle.runDeactivationSync(binding, instance, reader);
273
- }
353
+ this.#deactivatePairsSync(this.#drainSingletons(this.#registry.clear()));
274
354
  }
275
355
 
276
356
  async unbindAllAsync(): Promise<void> {
277
357
  this.#assertNotDisposed();
278
- const reader = this.#getMetadataReader();
279
- for (const [binding, instance] of this.#drainSingletons(this.#registry.clear())) {
280
- await this.#lifecycle.runDeactivation(binding, instance, reader);
281
- }
358
+ await this.#deactivatePairs(this.#drainSingletons(this.#registry.clear()));
282
359
  }
283
360
 
284
- rebind<const Value>(token: Token<Value> | Constructor<Value>): BindToBuilder<Value> {
361
+ rebind<Value>(token: Token<Value> | Constructor<Value>): BindToBuilder<Value> {
285
362
  this.#assertNotDisposed();
286
363
  if (!this.#registry.has(token)) {
287
364
  throw new RebindUnboundTokenError(tokenName(token));
@@ -291,7 +368,7 @@ class DefaultContainer implements Container {
291
368
  return this.#createBindToBuilder(token);
292
369
  }
293
370
 
294
- // ── Module ────────────────────────────────────────────────────────────────
371
+ // ── Module ─────────────────────────────────────────────────────────────────────────────────────────────────────────
295
372
 
296
373
  load(...modules: Array<SyncModule>): void {
297
374
  this.#assertNotDisposed();
@@ -314,17 +391,43 @@ class DefaultContainer implements Container {
314
391
  }
315
392
  moduleRefs.set(moduleRef, 1);
316
393
  const builder = this.#createModuleBuilder(moduleRef);
317
- module[MODULE_SETUP](builder);
394
+ try {
395
+ module[MODULE_SETUP](builder);
396
+ } catch (error) {
397
+ throw this.#rollbackFailedLoadSync(moduleRef, error);
398
+ }
318
399
  }
319
400
  }
320
401
 
321
402
  async loadAsync(...modules: Array<SyncModule | AsyncModule>): Promise<void> {
322
403
  this.#assertNotDisposed();
323
- for (const module of modules) {
404
+ // Deduped by identity like the sync lane, so `loadAsync(m, m)` ref-counts once.
405
+ for (const module of new Set(modules)) {
324
406
  await this.#loadOneModuleAsync(module);
325
407
  }
326
408
  }
327
409
 
410
+ /** Undo a failed load — bindings registered before the throw and the ref-count both roll back. */
411
+ #rollbackFailedLoadSync(moduleRef: object, cause: unknown): unknown {
412
+ const pairs = this.#removeModuleBindings(moduleRef);
413
+ try {
414
+ this.#deactivatePairsSync(pairs);
415
+ } catch (rollbackError) {
416
+ return new AggregateError([cause, rollbackError], "module load failed, and rolling it back threw too");
417
+ }
418
+ return cause;
419
+ }
420
+
421
+ async #rollbackFailedLoad(moduleRef: object, cause: unknown): Promise<unknown> {
422
+ const pairs = this.#removeModuleBindings(moduleRef);
423
+ try {
424
+ await this.#deactivatePairs(pairs);
425
+ } catch (rollbackError) {
426
+ return new AggregateError([cause, rollbackError], "module load failed, and rolling it back threw too");
427
+ }
428
+ return cause;
429
+ }
430
+
328
431
  async #loadOneModuleAsync(module: SyncModule | AsyncModule): Promise<void> {
329
432
  const moduleRef = module as object;
330
433
  const moduleRefs = (this.#moduleRefs ??= new Map());
@@ -337,14 +440,25 @@ class DefaultContainer implements Container {
337
440
 
338
441
  if (isSyncModule(module)) {
339
442
  const builder = this.#createModuleBuilder(moduleRef);
340
- module[MODULE_SETUP](builder);
443
+ try {
444
+ module[MODULE_SETUP](builder);
445
+ } catch (error) {
446
+ throw this.#rollbackFailedLoadSync(moduleRef, error);
447
+ }
341
448
  } else {
342
449
  const importPromises: Array<Promise<void>> = [];
343
450
  const builder = this.#createAsyncModuleBuilder(moduleRef, importPromises);
344
- await module[MODULE_SETUP](builder);
345
- // Await nested async imports triggered inside the setup callback
346
- if (importPromises.length > 0) {
347
- await Promise.all(importPromises);
451
+ try {
452
+ await module[MODULE_SETUP](builder);
453
+ // Await nested async imports triggered inside the setup callback
454
+ if (importPromises.length > 0) {
455
+ await Promise.all(importPromises);
456
+ }
457
+ } catch (error) {
458
+ // Nested imports may still be registering — let them settle before rolling back, so no
459
+ // registration lands after the rejection and none becomes an unhandled rejection.
460
+ await Promise.allSettled(importPromises);
461
+ throw await this.#rollbackFailedLoad(moduleRef, error);
348
462
  }
349
463
  }
350
464
  }
@@ -352,7 +466,7 @@ class DefaultContainer implements Container {
352
466
  #createModuleBuilder(moduleRef: object): ModuleBuilder {
353
467
  const registration = this.#moduleRegistration(moduleRef);
354
468
  return {
355
- bind: <const Value>(token: Token<Value> | Constructor<Value>): BindToBuilder<Value> =>
469
+ bind: <Value>(token: Token<Value> | Constructor<Value>): BindToBuilder<Value> =>
356
470
  this.#createBindToBuilder(token, registration),
357
471
  import: (...modules: Array<SyncModule>): void => {
358
472
  this.#loadSyncModules(modules);
@@ -363,7 +477,7 @@ class DefaultContainer implements Container {
363
477
  #createAsyncModuleBuilder(moduleRef: object, importPromises: Array<Promise<void>>): AsyncModuleBuilder {
364
478
  const registration = this.#moduleRegistration(moduleRef);
365
479
  return {
366
- bind: <const Value>(token: Token<Value> | Constructor<Value>): BindToBuilder<Value> =>
480
+ bind: <Value>(token: Token<Value> | Constructor<Value>): BindToBuilder<Value> =>
367
481
  this.#createBindToBuilder(token, registration),
368
482
  import: (...modules: Array<SyncModule | AsyncModule>): void => {
369
483
  for (const module of modules) {
@@ -385,28 +499,20 @@ class DefaultContainer implements Container {
385
499
  this.#moduleRefs?.delete(ref);
386
500
  const ids = this.#moduleBindingIds?.get(ref) ?? [];
387
501
  this.#moduleBindingIds?.delete(ref);
388
- const pairs: Array<[Binding, unknown]> = [];
502
+ const removed: Array<Binding> = [];
389
503
  for (const id of ids) {
390
- const binding = this.#registry.getById(id);
504
+ const binding = this.#registry.removeById(id);
391
505
  if (binding !== undefined) {
392
- this.#registry.removeById(id);
393
- if (binding.instance !== NO_INSTANCE) {
394
- pairs.push([binding, binding.instance]);
395
- this.#scope.deleteSingleton(binding);
396
- }
397
- this.#scope.deleteScoped(binding.id);
506
+ removed.push(binding);
398
507
  }
399
508
  }
400
- return pairs;
509
+ return this.#drainSingletons(removed);
401
510
  }
402
511
 
403
512
  #unloadModuleSync(ref: object): void {
404
513
  const count = this.#moduleRefs?.get(ref) ?? 0;
405
514
  if (count <= 1) {
406
- const reader = this.#getMetadataReader();
407
- for (const [binding, instance] of this.#removeModuleBindings(ref)) {
408
- this.#lifecycle.runDeactivationSync(binding, instance, reader);
409
- }
515
+ this.#deactivatePairsSync(this.#removeModuleBindings(ref));
410
516
  } else {
411
517
  this.#moduleRefs!.set(ref, count - 1);
412
518
  }
@@ -422,10 +528,7 @@ class DefaultContainer implements Container {
422
528
  async #unloadModuleAsync(ref: object): Promise<void> {
423
529
  const count = this.#moduleRefs?.get(ref) ?? 0;
424
530
  if (count <= 1) {
425
- const reader = this.#getMetadataReader();
426
- for (const [binding, instance] of this.#removeModuleBindings(ref)) {
427
- await this.#lifecycle.runDeactivation(binding, instance, reader);
428
- }
531
+ await this.#deactivatePairs(this.#removeModuleBindings(ref));
429
532
  } else {
430
533
  this.#moduleRefs!.set(ref, count - 1);
431
534
  }
@@ -435,112 +538,128 @@ class DefaultContainer implements Container {
435
538
  this.#assertNotDisposed();
436
539
  const entries = registry.entries();
437
540
  for (const { target, scope } of entries) {
438
- const builder = this.#createBindToBuilder(target);
439
- const bindingBuilder = builder.toSelf();
440
- if (scope === "singleton") {
441
- bindingBuilder.singleton();
442
- } else if (scope === "scoped") {
443
- bindingBuilder.scoped();
444
- } else {
445
- bindingBuilder.transient();
446
- }
541
+ APPLY_BINDING_SCOPE[scope](this.#createBindToBuilder(target).toSelf());
447
542
  }
448
543
  return entries.length;
449
544
  }
450
545
 
451
- // ── Lifecycle hooks ────────────────────────────────────────────────────────
546
+ // ── Lifecycle hooks ────────────────────────────────────────────────────────────────────────────────────────────────
452
547
 
453
- onActivation<const Value>(token: Token<Value> | Constructor<Value>, handler: ActivationHandler<Value>): void {
548
+ onActivation<Value>(token: Token<Value> | Constructor<Value>, handler: ActivationHandler<Value>): void {
454
549
  this.#assertNotDisposed();
455
550
  this.#lifecycle.registerActivation(token, handler);
456
551
  }
457
552
 
458
- onDeactivation<const Value>(token: Token<Value> | Constructor<Value>, handler: DeactivationHandler<Value>): void {
553
+ onDeactivation<Value>(token: Token<Value> | Constructor<Value>, handler: DeactivationHandler<Value>): void {
459
554
  this.#assertNotDisposed();
460
555
  this.#lifecycle.registerDeactivation(token, handler);
461
556
  }
462
557
 
463
- // ── Resolution ────────────────────────────────────────────────────────────
558
+ // ── Resolution ─────────────────────────────────────────────────────────────────────────────────────────────────────
464
559
 
465
- resolve<const Value>(token: Token<Value> | Constructor<Value>, options?: ResolveOptions): Value {
560
+ resolve<Value>(token: Token<Value> | Constructor<Value>, options?: ResolveOptions): Value {
466
561
  this.#assertNotDisposed();
467
562
  const rootStack = this.#resolver.rootStack;
468
- // A resolve already holding the shared pair means this one is nested; it mints its own.
563
+ // A resolve already holding the shared stack means this one is nested; it mints its own.
469
564
  if (rootStack.length !== 0) {
470
565
  return options === undefined
471
- ? this.#resolver.resolveFromContext(token, [], [])
472
- : this.#resolver.resolve(token, options, [], []);
566
+ ? this.#resolver.resolveFromContext(token, [])
567
+ : this.#resolver.resolve(token, options, []);
473
568
  }
474
569
  if (options === undefined) {
475
- return this.#resolver.resolveFromContext(token, this.#resolver.rootPath, rootStack);
570
+ return this.#resolver.resolveFromContext(token, rootStack);
476
571
  }
477
- return this.#resolver.resolve(token, options, this.#resolver.rootPath, rootStack);
572
+ return this.#resolver.resolve(token, options, rootStack);
478
573
  }
479
574
 
480
- resolveAsync<const Value>(token: Token<Value> | Constructor<Value>, options?: ResolveOptions): Promise<Value> {
575
+ resolveAsync<Value>(token: Token<Value> | Constructor<Value>, options?: ResolveOptions): Promise<Value> {
481
576
  this.#assertNotDisposed();
482
577
  if (options === undefined) {
483
578
  return this.#resolver.resolveAsyncFromRoot(token) as Promise<Value>;
484
579
  }
485
- return this.#resolver.resolveAsync(token, options, [], [], ROOT_BRANCH);
580
+ return this.#resolver.resolveAsync(token, options, [], ROOT_BRANCH);
486
581
  }
487
582
 
488
- resolveOptional<const Value>(token: Token<Value> | Constructor<Value>, options?: ResolveOptions): Value | undefined {
583
+ resolveOptional<Value>(token: Token<Value> | Constructor<Value>, options?: ResolveOptions): Value | undefined {
489
584
  this.#assertNotDisposed();
490
585
  const rootStack = this.#resolver.rootStack;
491
586
  return rootStack.length === 0
492
- ? this.#resolver.resolveOptional(token, options, this.#resolver.rootPath, rootStack)
493
- : this.#resolver.resolveOptional(token, options, [], []);
587
+ ? this.#resolver.resolveOptional(token, options, rootStack)
588
+ : this.#resolver.resolveOptional(token, options, []);
494
589
  }
495
590
 
496
- resolveOptionalAsync<const Value>(
591
+ resolveOptionalAsync<Value>(
497
592
  token: Token<Value> | Constructor<Value>,
498
593
  options?: ResolveOptions,
499
594
  ): Promise<Value | undefined> {
500
595
  this.#assertNotDisposed();
501
- return this.#resolver.resolveOptionalAsync(token, options, [], [], ROOT_BRANCH);
596
+ return this.#resolver.resolveOptionalAsync(token, options, [], ROOT_BRANCH);
502
597
  }
503
598
 
504
- resolveAll<const Value>(token: Token<Value> | Constructor<Value>, options?: ResolveOptions): Array<Value> {
599
+ resolveAll<Value>(token: Token<Value> | Constructor<Value>, options?: ResolveOptions): Array<Value> {
505
600
  this.#assertNotDisposed();
506
601
  const rootStack = this.#resolver.rootStack;
507
602
  return rootStack.length === 0
508
- ? this.#resolver.resolveAll(token, options, this.#resolver.rootPath, rootStack)
509
- : this.#resolver.resolveAll(token, options, [], []);
603
+ ? this.#resolver.resolveAll(token, options, rootStack)
604
+ : this.#resolver.resolveAll(token, options, []);
510
605
  }
511
606
 
512
- resolveAllAsync<const Value>(
513
- token: Token<Value> | Constructor<Value>,
514
- options?: ResolveOptions,
515
- ): Promise<Array<Value>> {
607
+ resolveAllAsync<Value>(token: Token<Value> | Constructor<Value>, options?: ResolveOptions): Promise<Array<Value>> {
516
608
  this.#assertNotDisposed();
517
- return this.#resolver.resolveAllAsync(token, options, [], [], ROOT_BRANCH);
609
+ return this.#resolver.resolveAllAsync(token, options, [], ROOT_BRANCH);
518
610
  }
519
611
 
520
- // ── Child ─────────────────────────────────────────────────────────────────
612
+ // ── Child ──────────────────────────────────────────────────────────────────────────────────────────────────────────
521
613
 
522
614
  createChild(): Container {
523
615
  this.#assertNotDisposed();
524
616
  return new DefaultContainer(this);
525
617
  }
526
618
 
527
- // ── Dispose ───────────────────────────────────────────────────────────────
619
+ // ── Dispose ────────────────────────────────────────────────────────────────────────────────────────────────────────
528
620
 
529
- async dispose(): Promise<void> {
530
- if (this.#disposed) {
531
- return;
532
- }
621
+ dispose(): Promise<void> {
622
+ // One teardown, shared: a second caller awaits the same run instead of returning early while
623
+ // hooks from the first are still executing.
624
+ return (this.#disposePromise ??= this.#runDispose());
625
+ }
626
+
627
+ async #runDispose(): Promise<void> {
533
628
  this.#disposed = true;
629
+ // Refuses new materializations immediately; in-flight ones are drained just below.
630
+ this.#scope.markClosed();
631
+ await this.#scope.settleInflight();
534
632
 
535
- // Deactivate all singletons in this container (own only)
536
633
  const reader = this.#getMetadataReader();
634
+ const errors: Array<unknown> = [];
537
635
  // Iterate a copy: a deactivation handler is user code, and the live list is what
538
- // materializing or dropping a singleton mutates.
539
- for (const binding of this.#scope.cachedSingletons().slice()) {
540
- await this.#lifecycle.runDeactivation(binding, binding.instance, reader);
636
+ // materializing or dropping a singleton mutates. Reverse materialization order, so a
637
+ // dependent tears down before the dependencies it may still reach through.
638
+ const cached = this.#scope.cachedSingletons().slice();
639
+ for (let index = cached.length - 1; index >= 0; index -= 1) {
640
+ const binding = cached[index]!;
641
+ try {
642
+ await this.#lifecycle.runDeactivation(binding, binding.instance, reader);
643
+ } catch (error) {
644
+ errors.push(error);
645
+ }
646
+ }
647
+ // A constant never reaches the singleton cache unless activation put it there, so its hook is
648
+ // owed from the registry instead — and only a container that has held one pays for looking.
649
+ if (this.#registry.hasHeldConstantBinding) {
650
+ for (const binding of this.#registry.allBindings()) {
651
+ if (binding.instance === NO_INSTANCE && this.#owesConstantDeactivation(binding)) {
652
+ try {
653
+ await this.#lifecycle.runDeactivation(binding, binding.value, reader);
654
+ } catch (error) {
655
+ errors.push(error);
656
+ }
657
+ }
658
+ }
541
659
  }
542
660
 
543
661
  this.#scope.clearAll();
662
+ throwCollected(errors, "dispose() completed, but deactivation hooks threw");
544
663
  }
545
664
 
546
665
  [Symbol.asyncDispose](): Promise<void> {
@@ -551,7 +670,7 @@ class DefaultContainer implements Container {
551
670
  throw new SyncDisposalNotSupportedError();
552
671
  }
553
672
 
554
- // ── Initialization ────────────────────────────────────────────────────────
673
+ // ── Initialization ─────────────────────────────────────────────────────────────────────────────────────────────────
555
674
 
556
675
  async initializeAsync(): Promise<void> {
557
676
  this.#assertNotDisposed();
@@ -565,17 +684,22 @@ class DefaultContainer implements Container {
565
684
  if (binding.predicate !== undefined) {
566
685
  continue;
567
686
  }
568
- // Has activation — need to resolve
569
- if (binding.kind === "constant" && binding.onActivation === undefined) {
687
+ // A constant is already its own instance, so only an activation hook gives the warm-up
688
+ // something to run — and a container-level hook counts as one just as a per-binding hook does.
689
+ if (
690
+ binding.kind === "constant" &&
691
+ binding.onActivation === undefined &&
692
+ !this.#lifecycle.hasActivationHandlers(binding.token)
693
+ ) {
570
694
  continue;
571
695
  }
572
696
  const slotOptions = bindingSlotToResolveOptions(binding.slot);
573
- await this.resolveAsync(binding.token as Token<unknown>, slotOptions);
697
+ await this.resolveAsync(binding.token, slotOptions);
574
698
  }
575
699
  }
576
700
  }
577
701
 
578
- // ── Validate ──────────────────────────────────────────────────────────────
702
+ // ── Validate ───────────────────────────────────────────────────────────────────────────────────────────────────────
579
703
 
580
704
  validate(): void {
581
705
  this.#assertNotDisposed();
@@ -588,6 +712,56 @@ class DefaultContainer implements Container {
588
712
  }
589
713
  this.#validateSingletonBindingGraph(binding, reader);
590
714
  }
715
+
716
+ for (const [hookToken, phase] of this.#lifecycle.hookedTokens()) {
717
+ if (!this.#isBoundInChain(hookToken)) {
718
+ throw new UnreachableLifecycleHookError(tokenName(hookToken), phase);
719
+ }
720
+ }
721
+
722
+ this.#validateConstraintRequirements(allBindings);
723
+ }
724
+
725
+ /** A constraint waiting on a slot name no binding declares can never hold. */
726
+ #validateConstraintRequirements(allBindings: ReadonlyArray<Binding>): void {
727
+ let declaredSlotNames: Set<string> | undefined;
728
+
729
+ for (const binding of allBindings) {
730
+ const { predicate } = binding;
731
+ if (predicate === undefined) {
732
+ continue;
733
+ }
734
+ const requirements = constraintRequirementsOf(predicate);
735
+ if (requirements.length === 0) {
736
+ continue;
737
+ }
738
+ declaredSlotNames ??= this.#slotNamesInChain();
739
+ for (const requirement of requirements) {
740
+ if (!declaredSlotNames.has(requirement.name)) {
741
+ throw new UnreachableConstraintError(tokenName(binding.token), requirement.name, requirement.helperName);
742
+ }
743
+ }
744
+ }
745
+ }
746
+
747
+ /** Every slot name declared anywhere a resolve through this container could reach. */
748
+ #slotNamesInChain(): Set<string> {
749
+ const names = this.#parent === undefined ? new Set<string>() : this.#parent.#slotNamesInChain();
750
+ for (const binding of this.#registry.allBindings()) {
751
+ if (binding.slot.name !== undefined) {
752
+ names.add(binding.slot.name);
753
+ }
754
+ }
755
+ return names;
756
+ }
757
+
758
+ // Ancestors count: a parent-owned binding is one this container can still resolve through.
759
+ #isBoundInChain(token: Token<unknown> | Constructor): boolean {
760
+ if (this.#registry.has(token)) {
761
+ return true;
762
+ }
763
+ const parent = this.#parent;
764
+ return parent !== undefined && parent.#isBoundInChain(token);
591
765
  }
592
766
 
593
767
  #isSingletonStaticAnalyzableBinding(binding: Binding): boolean {
@@ -599,13 +773,13 @@ class DefaultContainer implements Container {
599
773
 
600
774
  /**
601
775
  * DFS over explicit constructor / `toResolved*` dependency edges. Follows `toAlias` chains to the
602
- * terminal binding for scope checks (SPEC §6.9).
776
+ * terminal binding for scope checks.
603
777
  *
604
778
  * @remarks A `toDynamic*` dependency is scope-checked like any other — its declared scope is what
605
779
  * makes it captive — but the DFS does not descend into the factory, whose body is opaque.
606
780
  */
607
781
  #validateSingletonBindingGraph(root: Binding, reader: MetadataReader): void {
608
- const rootName = tokenName(root.token as Token<unknown>);
782
+ const rootName = tokenName(root.token);
609
783
 
610
784
  const dfs = (current: Binding, pathNames: Array<string>, pathBindingIds: Set<BindingIdentifier>): void => {
611
785
  if (pathBindingIds.has(current.id)) {
@@ -659,8 +833,8 @@ class DefaultContainer implements Container {
659
833
  throw new CircularDependencyError(cyclePath);
660
834
  }
661
835
  seenAliasIds.add(current.id);
662
- cyclePath.push(tokenName(current.token as Token<unknown>));
663
- const nextToken = current.target as Token<unknown> | Constructor;
836
+ cyclePath.push(tokenName(current.token));
837
+ const nextToken = current.target;
664
838
  const next = this.#resolver.peekBindingForValidate(nextToken, options);
665
839
  if (next === undefined) {
666
840
  return undefined;
@@ -682,7 +856,7 @@ class DefaultContainer implements Container {
682
856
  /** What a binding declares up front — a class's params, a factory's descriptors, else nothing. */
683
857
  #staticDependencies(binding: Binding, reader: MetadataReader): ReadonlyArray<DependencySlot> {
684
858
  if (binding.kind === "class") {
685
- return reader.getConstructorMetadata(binding.target as Constructor)?.params ?? [];
859
+ return reader.getConstructorMetadata(binding.target)?.params ?? [];
686
860
  }
687
861
  if (binding.kind === "resolved" || binding.kind === "resolved-async") {
688
862
  return binding.deps;
@@ -705,7 +879,7 @@ class DefaultContainer implements Container {
705
879
  for (const candidate of this.#peekDependencyCandidates(dep, depOptions)) {
706
880
  const terminal = this.#followAliasChainToTerminal(candidate, depOptions);
707
881
  if (terminal !== undefined) {
708
- edges.push({ terminal, depTokenName: tokenName(terminal.token as Token<unknown>) });
882
+ edges.push({ terminal, depTokenName: tokenName(terminal.token) });
709
883
  }
710
884
  }
711
885
  }
@@ -713,7 +887,7 @@ class DefaultContainer implements Container {
713
887
  return edges;
714
888
  }
715
889
 
716
- // ── Introspection ─────────────────────────────────────────────────────────
890
+ // ── Introspection ──────────────────────────────────────────────────────────────────────────────────────────────────
717
891
 
718
892
  has(token: Token<unknown> | Constructor, options?: ResolveOptions): boolean {
719
893
  this.#assertNotDisposed();
@@ -725,7 +899,7 @@ class DefaultContainer implements Container {
725
899
  return this.#getInspector().hasOwn(token, options);
726
900
  }
727
901
 
728
- lookupBindings<const Value>(token: Token<Value> | Constructor<Value>): ReadonlyArray<BindingSnapshot> {
902
+ lookupBindings<Value>(token: Token<Value> | Constructor<Value>): ReadonlyArray<BindingSnapshot> {
729
903
  this.#assertNotDisposed();
730
904
  return this.#getInspector().lookupBindings(token);
731
905
  }
@@ -745,7 +919,7 @@ class DefaultContainer implements Container {
745
919
  );
746
920
  }
747
921
 
748
- // ── Internal ──────────────────────────────────────────────────────────────
922
+ // ── Internal ───────────────────────────────────────────────────────────────────────────────────────────────────────
749
923
 
750
924
  #assertNotDisposed(): void {
751
925
  if (this.#disposed) {
@@ -754,16 +928,29 @@ class DefaultContainer implements Container {
754
928
  }
755
929
  }
756
930
 
757
- // ── Container static ──────────────────────────────────────────────────────────
931
+ /** Reports collected hook failures once teardown finished: the one error as itself, several aggregated. */
932
+ function throwCollected(errors: ReadonlyArray<unknown>, message: string): void {
933
+ if (errors.length === 0) {
934
+ return;
935
+ }
936
+ if (errors.length === 1) {
937
+ throw errors[0];
938
+ }
939
+ throw new AggregateError(errors, message);
940
+ }
941
+
942
+ // ── Container static ─────────────────────────────────────────────────────────────────────────────────────────────────
758
943
 
759
944
  /**
760
945
  * @since 0.3.16-canary.0
761
946
  */
762
- export const Container: ContainerStatic & { create(): Container } = {
763
- create(): Container {
764
- return new DefaultContainer();
947
+ export const Container: ContainerStatic = {
948
+ create(options?: ContainerOptions): Container {
949
+ return new DefaultContainer(undefined, options);
765
950
  },
766
951
 
952
+ // Variadic modules leave no room for an options argument. A container that needs both is
953
+ // `Container.create(options)` followed by `load(...)`, which is what these two do anyway.
767
954
  fromModules(...modules: Array<SyncModule>): Container {
768
955
  const container = new DefaultContainer();
769
956
  container.load(...modules);