@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,9 +1,15 @@
1
- import type { Binding } from "#/binding";
2
- import { AsyncActivationError, AsyncDeactivationError } from "#/errors";
1
+ import type { Binding } from "#/core/binding";
2
+ import type { Token } from "#/core/token";
3
+ import { tokenName } from "#/core/token";
4
+ import type {
5
+ ActivationHandler,
6
+ Constructor,
7
+ DeactivationHandler,
8
+ DependencyKey,
9
+ ResolutionContext,
10
+ } from "#/core/types";
11
+ import { AsyncActivationError, AsyncDeactivationError, InvalidMetadataError } from "#/errors/errors";
3
12
  import type { MetadataReader } from "#/metadata/metadata-types";
4
- import type { Token } from "#/token";
5
- import { tokenName } from "#/token";
6
- import type { ActivationHandler, Constructor, DeactivationHandler, ResolutionContext } from "#/types";
7
13
 
8
14
  /**
9
15
  * @since 0.3.16-canary.0
@@ -11,8 +17,8 @@ import type { ActivationHandler, Constructor, DeactivationHandler, ResolutionCon
11
17
  export class LifecycleManager {
12
18
  // Container-level activation/deactivation hooks per token — most containers register none, so
13
19
  // both tables stay unallocated until the first hook arrives.
14
- #activationHooks: Map<Token<unknown> | Constructor, Array<ActivationHandler<unknown>>> | undefined;
15
- #deactivationHooks: Map<Token<unknown> | Constructor, Array<DeactivationHandler<unknown>>> | undefined;
20
+ #activationHooks: Map<DependencyKey, Array<ActivationHandler<unknown>>> | undefined;
21
+ #deactivationHooks: Map<DependencyKey, Array<DeactivationHandler<unknown>>> | undefined;
16
22
  #activationVersion = 0;
17
23
 
18
24
  // One-entry cache in front of the map: a resolve loop asks about the same token over and over,
@@ -20,16 +26,15 @@ export class LifecycleManager {
20
26
  #cachedToken: Token<unknown> | Constructor | undefined;
21
27
  #cachedHooks: Array<ActivationHandler<unknown>> | undefined;
22
28
 
23
- registerActivation<const Value>(token: Token<Value> | Constructor<Value>, handler: ActivationHandler<Value>): void {
29
+ registerActivation<Value>(token: Token<Value> | Constructor<Value>, handler: ActivationHandler<Value>): void {
24
30
  this.#activationVersion += 1;
25
31
  this.#cachedToken = undefined;
26
32
  this.#cachedHooks = undefined;
27
- // ✓ TS6.0: Map.getOrInsert (ES2025)
28
- const list = (this.#activationHooks ??= new Map()).getOrInsert(token as Token<unknown> | Constructor, []);
33
+ const list = (this.#activationHooks ??= new Map()).getOrInsert(token, []);
29
34
  list.push(handler as ActivationHandler<unknown>);
30
35
  }
31
36
 
32
- hasActivationHandlers<const Value>(token: Token<Value> | Constructor<Value>): boolean {
37
+ hasActivationHandlers<Value>(token: Token<Value> | Constructor<Value>): boolean {
33
38
  const list = this.activationHandlersFor(token);
34
39
  return list !== undefined && list.length > 0;
35
40
  }
@@ -39,14 +44,14 @@ export class LifecycleManager {
39
44
  }
40
45
 
41
46
  /** Container-level activation handlers for a token — hot-path accessor, no copies. */
42
- activationHandlersFor<const Value>(
47
+ activationHandlersFor<Value>(
43
48
  token: Token<Value> | Constructor<Value>,
44
49
  ): ReadonlyArray<ActivationHandler<unknown>> | undefined {
45
50
  const hooks = this.#activationHooks;
46
51
  if (hooks === undefined) {
47
52
  return undefined;
48
53
  }
49
- const key = token as Token<unknown> | Constructor;
54
+ const key: DependencyKey = token;
50
55
  if (key === this.#cachedToken) {
51
56
  return this.#cachedHooks;
52
57
  }
@@ -56,15 +61,30 @@ export class LifecycleManager {
56
61
  return list;
57
62
  }
58
63
 
59
- registerDeactivation<const Value>(
60
- token: Token<Value> | Constructor<Value>,
61
- handler: DeactivationHandler<Value>,
62
- ): void {
63
- const list = (this.#deactivationHooks ??= new Map()).getOrInsert(token as Token<unknown> | Constructor, []);
64
+ registerDeactivation<Value>(token: Token<Value> | Constructor<Value>, handler: DeactivationHandler<Value>): void {
65
+ const list = (this.#deactivationHooks ??= new Map()).getOrInsert(token, []);
64
66
  list.push(handler as DeactivationHandler<unknown>);
65
67
  }
66
68
 
67
- async runActivation<const Value>(
69
+ /** Asked only while tearing down, so it reads the map directly rather than caching like activation. */
70
+ hasDeactivationHandlers<Value>(token: Token<Value> | Constructor<Value>): boolean {
71
+ const list = this.#deactivationHooks?.get(token);
72
+ return list !== undefined && list.length > 0;
73
+ }
74
+
75
+ /** Every token carrying a container-level hook, paired with the phase that registered it. */
76
+ hookedTokens(): Array<[DependencyKey, "onActivation" | "onDeactivation"]> {
77
+ const hooked: Array<[DependencyKey, "onActivation" | "onDeactivation"]> = [];
78
+ for (const key of this.#activationHooks?.keys() ?? []) {
79
+ hooked.push([key, "onActivation"]);
80
+ }
81
+ for (const key of this.#deactivationHooks?.keys() ?? []) {
82
+ hooked.push([key, "onDeactivation"]);
83
+ }
84
+ return hooked;
85
+ }
86
+
87
+ async runActivation<Value>(
68
88
  resolutionContext: ResolutionContext,
69
89
  binding: Binding<Value>,
70
90
  instance: Value,
@@ -87,7 +107,7 @@ export class LifecycleManager {
87
107
  }
88
108
 
89
109
  // 3. container-level onActivation
90
- const containerHooks = this.#activationHooks?.get(binding.token as Token<unknown> | Constructor);
110
+ const containerHooks = this.#activationHooks?.get(binding.token);
91
111
  if (containerHooks !== undefined) {
92
112
  for (const hook of containerHooks) {
93
113
  const activationResult = hook(resolutionContext, activatedInstance);
@@ -98,7 +118,7 @@ export class LifecycleManager {
98
118
  return activatedInstance;
99
119
  }
100
120
 
101
- runActivationSync<const Value>(
121
+ runActivationSync<Value>(
102
122
  resolutionContext: ResolutionContext,
103
123
  binding: Binding<Value>,
104
124
  instance: Value,
@@ -124,7 +144,7 @@ export class LifecycleManager {
124
144
 
125
145
  // 3. container-level onActivation (must be sync)
126
146
  const tokenDisplayName = tokenName(binding.token);
127
- const containerHooks = this.#activationHooks?.get(binding.token as Token<unknown> | Constructor);
147
+ const containerHooks = this.#activationHooks?.get(binding.token);
128
148
  if (containerHooks !== undefined) {
129
149
  for (const hook of containerHooks) {
130
150
  const activationResult = hook(resolutionContext, activatedInstance);
@@ -138,12 +158,12 @@ export class LifecycleManager {
138
158
  return activatedInstance;
139
159
  }
140
160
 
141
- async runDeactivation<const Value>(
161
+ async runDeactivation<Value>(
142
162
  binding: Binding<Value>,
143
163
  instance: Value,
144
164
  metadataReader: MetadataReader,
145
165
  ): Promise<void> {
146
- const tokenKey = binding.token as Token<unknown> | Constructor;
166
+ const tokenKey: DependencyKey = binding.token;
147
167
 
148
168
  // 1. container-level onDeactivation
149
169
  const containerHooks = this.#deactivationHooks?.get(tokenKey);
@@ -173,9 +193,9 @@ export class LifecycleManager {
173
193
  }
174
194
  }
175
195
 
176
- runDeactivationSync<const Value>(binding: Binding<Value>, instance: Value, metadataReader: MetadataReader): void {
196
+ runDeactivationSync<Value>(binding: Binding<Value>, instance: Value, metadataReader: MetadataReader): void {
177
197
  const tokenDisplayName = tokenName(binding.token);
178
- const tokenKey = binding.token as Token<unknown> | Constructor;
198
+ const tokenKey: DependencyKey = binding.token;
179
199
 
180
200
  // 1. container-level onDeactivation
181
201
  const containerHooks = this.#deactivationHooks?.get(tokenKey);
@@ -204,8 +224,8 @@ export class LifecycleManager {
204
224
  }
205
225
  }
206
226
 
207
- /** Whether the deferred table behind `#activationHooks` has had to be built. */
208
- get isBuilt(): boolean {
227
+ /** Whether the deferred activation-hook table has had to be built. */
228
+ get isActivationTableBuilt(): boolean {
209
229
  return this.#activationHooks !== undefined;
210
230
  }
211
231
  }
@@ -213,7 +233,7 @@ export class LifecycleManager {
213
233
  const NO_METHODS: ReadonlyArray<string> = [];
214
234
 
215
235
  /** The `@postConstruct` / `@preDestroy` methods a binding declares — only a class can declare any. */
216
- function lifecycleMethods<const Value>(
236
+ function lifecycleMethods<Value>(
217
237
  binding: Binding<Value>,
218
238
  metadataReader: MetadataReader,
219
239
  phase: "postConstruct" | "preDestroy",
@@ -224,8 +244,24 @@ function lifecycleMethods<const Value>(
224
244
  return metadataReader.getLifecycleMetadata(binding.target)?.[phase] ?? NO_METHODS;
225
245
  }
226
246
 
227
- /** Invokes a hook by name, tolerating a name whose member is not (or no longer) a method. */
247
+ /**
248
+ * Invokes a lifecycle hook by name.
249
+ *
250
+ * @remarks A name that is not a method can only come from a {@link MetadataReader} that answered
251
+ * wrongly, so it is reported rather than skipped — a hook that never runs is the failure a caller
252
+ * cannot see. A non-object instance has no hooks to run and is not an error.
253
+ */
228
254
  function callHook(instance: unknown, methodName: string): unknown {
229
- const method = (instance as Record<string, unknown>)[methodName];
230
- return typeof method === "function" ? (method as () => unknown).call(instance) : undefined;
255
+ if (typeof instance !== "object" || instance === null) {
256
+ return undefined;
257
+ }
258
+ const method: unknown = Reflect.get(instance, methodName);
259
+ if (typeof method !== "function") {
260
+ // The class is derived here rather than passed in, so the happy path carries no extra argument.
261
+ const constructor: unknown = Reflect.get(instance, "constructor");
262
+ const className = typeof constructor === "function" && constructor.name !== "" ? constructor.name : "(anonymous)";
263
+ throw new InvalidMetadataError(className, `lifecycle method '${methodName}' is not a method on the instance`);
264
+ }
265
+
266
+ return method.call(instance);
231
267
  }
@@ -0,0 +1,156 @@
1
+ import type { Binding } from "#/core/binding";
2
+ import { NO_INSTANCE } from "#/core/binding";
3
+ import { tokenName } from "#/core/token";
4
+ import type { BindingIdentifier } from "#/core/types";
5
+ import { MissingScopeContextError } from "#/errors/errors";
6
+
7
+ /**
8
+ * @since 0.3.16-canary.0
9
+ */
10
+ export class ScopeManager {
11
+ // Instances live on their binding; this list only lets disposal and `inspect()` enumerate them.
12
+ #singletonBindings: Array<Binding<unknown>> | undefined;
13
+ // In-flight promises for async singleton creation — only an async resolve ever needs it.
14
+ #inflight: Map<BindingIdentifier, Promise<unknown>> | undefined;
15
+ // Scoped cache — only a child container resolving a `scoped` binding ever needs it.
16
+ #scoped: Map<BindingIdentifier, unknown> | undefined;
17
+ // Set once by the owning container's dispose — refuses new materializations into torn-down state.
18
+ #closed = false;
19
+
20
+ readonly isChild: boolean;
21
+
22
+ constructor(isChild = false) {
23
+ this.isChild = isChild;
24
+ }
25
+
26
+ get isClosed(): boolean {
27
+ return this.#closed;
28
+ }
29
+
30
+ markClosed(): void {
31
+ this.#closed = true;
32
+ }
33
+
34
+ /** Awaits every in-flight async materialization, so teardown deactivates what they produce. */
35
+ async settleInflight(): Promise<void> {
36
+ let previousSize = -1;
37
+ while (this.#inflight !== undefined && this.#inflight.size > 0 && this.#inflight.size !== previousSize) {
38
+ previousSize = this.#inflight.size;
39
+ await Promise.allSettled(this.#inflight.values());
40
+ }
41
+ }
42
+
43
+ setSingleton<Value>(binding: Binding<Value>, instance: unknown): void {
44
+ if (binding.instance === NO_INSTANCE) {
45
+ (this.#singletonBindings ??= []).push(binding as Binding<unknown>);
46
+ }
47
+ binding.instance = instance;
48
+ }
49
+
50
+ /** Every binding in this container holding a cached singleton. */
51
+ cachedSingletons(): ReadonlyArray<Binding<unknown>> {
52
+ return this.#singletonBindings ?? EMPTY_BINDINGS;
53
+ }
54
+
55
+ deleteSingleton<Value>(binding: Binding<Value>): boolean {
56
+ if (binding.instance === NO_INSTANCE) {
57
+ return false;
58
+ }
59
+ binding.instance = NO_INSTANCE;
60
+ const tracked = this.#singletonBindings;
61
+ if (tracked !== undefined) {
62
+ const index = tracked.indexOf(binding as Binding<unknown>);
63
+ if (index !== -1) {
64
+ tracked.splice(index, 1);
65
+ }
66
+ }
67
+ return true;
68
+ }
69
+
70
+ /** Swaps a re-slotted binding's tracked entry, so teardown pairs the instance with the live object. */
71
+ replaceSingleton(previous: Binding, next: Binding): void {
72
+ const tracked = this.#singletonBindings;
73
+ if (tracked === undefined) {
74
+ return;
75
+ }
76
+ const index = tracked.indexOf(previous as Binding<unknown>);
77
+ if (index !== -1) {
78
+ tracked[index] = next as Binding<unknown>;
79
+ }
80
+ }
81
+
82
+ getInflight(id: BindingIdentifier): Promise<unknown> | undefined {
83
+ return this.#inflight?.get(id);
84
+ }
85
+
86
+ setInflight(id: BindingIdentifier, promise: Promise<unknown>): void {
87
+ (this.#inflight ??= new Map()).set(id, promise);
88
+ }
89
+
90
+ clearInflight(id: BindingIdentifier): void {
91
+ this.#inflight?.delete(id);
92
+ }
93
+
94
+ /**
95
+ * The cached scoped instance, or {@link SCOPED_MISS}.
96
+ *
97
+ * @remarks One map read answers both existence and value; a cached `undefined` is the only
98
+ * shape that pays for a second, and it is the rare one.
99
+ */
100
+ readScoped(id: BindingIdentifier): unknown {
101
+ const scoped = this.#scoped;
102
+ if (scoped === undefined) {
103
+ return SCOPED_MISS;
104
+ }
105
+ const cached = scoped.get(id);
106
+ if (cached !== undefined) {
107
+ return cached;
108
+ }
109
+ return scoped.has(id) ? undefined : SCOPED_MISS;
110
+ }
111
+
112
+ /** Takes the binding rather than its id, so a failure here can name the token — as `setSingleton` does. */
113
+ setScoped(binding: Binding, instance: unknown): void {
114
+ if (!this.isChild) {
115
+ throw new MissingScopeContextError(tokenName(binding.token));
116
+ }
117
+ (this.#scoped ??= new Map()).set(binding.id, instance);
118
+ }
119
+
120
+ /** Releases a removed binding's scoped instance. A scoped instance has no deactivation. */
121
+ deleteScoped(id: BindingIdentifier): void {
122
+ this.#scoped?.delete(id);
123
+ }
124
+
125
+ /** Scoped instances currently cached — a structural count for diagnostics. */
126
+ get scopedCount(): number {
127
+ return this.#scoped?.size ?? 0;
128
+ }
129
+
130
+ clearAll(): void {
131
+ const tracked = this.#singletonBindings;
132
+ if (tracked !== undefined) {
133
+ for (const binding of tracked) {
134
+ binding.instance = NO_INSTANCE;
135
+ }
136
+ tracked.length = 0;
137
+ }
138
+ this.#inflight?.clear();
139
+ this.#scoped?.clear();
140
+ }
141
+ /** Whether the deferred scoped-instance cache has had to be built. */
142
+ get isScopedCacheBuilt(): boolean {
143
+ return this.#scoped !== undefined;
144
+ }
145
+ }
146
+
147
+ const EMPTY_BINDINGS: ReadonlyArray<Binding<unknown>> = [];
148
+
149
+ /**
150
+ * Absent scoped entry — distinguishes it from a cached `undefined`.
151
+ *
152
+ * @remarks A `unique symbol`, so no value a caller could cache can ever equal it.
153
+ *
154
+ * @since 0.6.0
155
+ */
156
+ export const SCOPED_MISS: unique symbol = Symbol("di:scoped-miss");
@@ -1,6 +1,6 @@
1
+ import { token } from "#/core/token";
2
+ import type { Token } from "#/core/token";
1
3
  import type { MetadataReader } from "#/metadata/metadata-types";
2
- import { token } from "#/token";
3
- import type { Token } from "#/token";
4
4
 
5
5
  /**
6
6
  * @since 0.3.16-canary.0
@@ -1,17 +1,17 @@
1
- import type { InjectionDescriptor } from "#/decorators/inject";
2
- import type { Token } from "#/token";
3
- import type { Constructor } from "#/types";
1
+ import type { Constructor } from "#/core/types";
2
+ import type { InjectionDescriptor } from "#/injection/descriptor";
3
+ import type { DependencySlot } from "#/injection/resolve-options";
4
4
 
5
5
  /**
6
+ * One constructor parameter's declaration.
7
+ *
8
+ * @remarks Extends {@link DependencySlot} so the two dependency sources stay literally one shape
9
+ * rather than two that happen to match; `index` is the only thing a parameter adds.
10
+ *
6
11
  * @since 0.3.16-canary.0
7
12
  */
8
- export interface ParamMetadata {
13
+ export interface ParamMetadata extends DependencySlot {
9
14
  readonly index: number;
10
- readonly token: Token<unknown> | Constructor;
11
- readonly optional: boolean;
12
- readonly multi: boolean;
13
- readonly name?: string;
14
- readonly tags?: ReadonlyArray<readonly [string, unknown]>;
15
15
  }
16
16
 
17
17
  /**
@@ -1,45 +1,122 @@
1
- import type { InjectionDescriptor } from "#/decorators/inject";
1
+ import type { Constructor } from "#/core/types";
2
+ import type { InjectionDescriptor } from "#/injection/descriptor";
2
3
  import { INJECT_ACCESSOR_KEY, INJECTABLE_KEY, LIFECYCLE_KEY, METADATA_SYMBOL } from "#/metadata/metadata-keys";
3
4
  import type { ConstructorMetadata, LifecycleMetadata, MetadataReader } from "#/metadata/metadata-types";
4
- import type { Constructor } from "#/types";
5
+
6
+ type AccessorEntry = { readonly key: string | symbol; readonly descriptor: InjectionDescriptor };
5
7
 
6
8
  /**
7
9
  * @since 0.3.16-canary.0
8
10
  */
9
11
  export class SymbolMetadataReader implements MetadataReader {
10
- #getMetadataRecord(target: Constructor, key: string | symbol): Record<string | symbol, unknown> | undefined {
12
+ // Merged chain answers, cached per class: decorator metadata cannot change once a class is defined.
13
+ #lifecycleByClass: WeakMap<Constructor, LifecycleMetadata | null> | undefined;
14
+ #accessorsByClass: WeakMap<Constructor, ReadonlyArray<AccessorEntry> | null> | undefined;
15
+
16
+ /**
17
+ * Whatever a decorator stored under `key` on this exact class, narrowed as far as the platform allows.
18
+ *
19
+ * @remarks Own-property only: an inherited `Symbol.metadata` belongs to the base class. Constructor
20
+ * metadata must not be borrowed (each class opts into its deps); the lifecycle and accessor readers
21
+ * walk the base chain themselves, because the platform runs inherited members regardless.
22
+ */
23
+ #read(target: object, key: string | symbol): unknown {
11
24
  const descriptor = Object.getOwnPropertyDescriptor(target, METADATA_SYMBOL);
12
25
  if (descriptor === undefined) {
13
26
  return undefined;
14
27
  }
15
- const record = descriptor.value as Record<string | symbol, unknown> | null | undefined;
16
- if (!record || typeof record !== "object" || !Object.hasOwn(record, key)) {
28
+ const record: unknown = descriptor.value;
29
+ if (typeof record !== "object" || record === null || !Object.hasOwn(record, key)) {
17
30
  return undefined;
18
31
  }
19
- return record;
32
+ return Reflect.get(record, key);
33
+ }
34
+
35
+ /** Each class level's own bucket under `key`, base first — the order construction runs them. */
36
+ #ownBucketsBaseFirst(target: Constructor, key: string | symbol): Array<unknown> {
37
+ const buckets: Array<unknown> = [];
38
+ let current: unknown = target;
39
+ while (typeof current === "function" && current !== Function.prototype) {
40
+ const bucket = this.#read(current, key);
41
+ if (bucket !== undefined) {
42
+ buckets.unshift(bucket);
43
+ }
44
+ current = Object.getPrototypeOf(current);
45
+ }
46
+ return buckets;
20
47
  }
21
48
 
22
49
  getConstructorMetadata(target: Constructor): ConstructorMetadata | undefined {
23
- const record = this.#getMetadataRecord(target, INJECTABLE_KEY);
24
- return record?.[INJECTABLE_KEY] as ConstructorMetadata | undefined;
50
+ return this.#read(target, INJECTABLE_KEY) as ConstructorMetadata | undefined;
25
51
  }
26
52
 
53
+ /**
54
+ * Lifecycle hooks aggregated over the base chain — an inherited hook method exists on the instance.
55
+ *
56
+ * @remarks `postConstruct` runs base first (a derived hook may rely on base state); `preDestroy`
57
+ * runs derived first, mirroring teardown. A method name repeated across levels runs once.
58
+ */
27
59
  getLifecycleMetadata(target: Constructor): LifecycleMetadata | undefined {
28
- const record = this.#getMetadataRecord(target, LIFECYCLE_KEY);
29
- return record?.[LIFECYCLE_KEY] as LifecycleMetadata | undefined;
60
+ const cached = this.#lifecycleByClass?.get(target);
61
+ if (cached !== undefined) {
62
+ return cached ?? undefined;
63
+ }
64
+ const buckets = this.#ownBucketsBaseFirst(target, LIFECYCLE_KEY) as Array<LifecycleMetadata>;
65
+ let merged: LifecycleMetadata | null;
66
+ if (buckets.length === 0) {
67
+ merged = null;
68
+ } else if (buckets.length === 1) {
69
+ merged = buckets[0]!;
70
+ } else {
71
+ merged = {
72
+ postConstruct: mergeUniqueNames(buckets.map((bucket) => bucket.postConstruct)),
73
+ preDestroy: mergeUniqueNames(buckets.map((bucket) => bucket.preDestroy).reverse()),
74
+ };
75
+ }
76
+ (this.#lifecycleByClass ??= new WeakMap()).set(target, merged);
77
+ return merged ?? undefined;
30
78
  }
31
79
 
32
- getAccessorMetadata(
33
- target: Constructor,
34
- ): Array<{ key: string | symbol; descriptor: InjectionDescriptor }> | undefined {
35
- const record = this.#getMetadataRecord(target, INJECT_ACCESSOR_KEY);
36
- return record?.[INJECT_ACCESSOR_KEY] as
37
- | Array<{ key: string | symbol; descriptor: InjectionDescriptor }>
38
- | undefined;
80
+ /**
81
+ * Accessor injections aggregated over the base chain, base first — the order initializers run.
82
+ *
83
+ * @remarks Not deduplicated: a derived accessor shadowing a base one still runs both initializers.
84
+ */
85
+ getAccessorMetadata(target: Constructor): ReadonlyArray<AccessorEntry> | undefined {
86
+ const cached = this.#accessorsByClass?.get(target);
87
+ if (cached !== undefined) {
88
+ return cached ?? undefined;
89
+ }
90
+ const buckets = this.#ownBucketsBaseFirst(target, INJECT_ACCESSOR_KEY) as Array<ReadonlyArray<AccessorEntry>>;
91
+ let merged: ReadonlyArray<AccessorEntry> | null;
92
+ if (buckets.length === 0) {
93
+ merged = null;
94
+ } else if (buckets.length === 1) {
95
+ merged = buckets[0]!;
96
+ } else {
97
+ merged = buckets.flat();
98
+ }
99
+ (this.#accessorsByClass ??= new WeakMap()).set(target, merged);
100
+ return merged ?? undefined;
101
+ }
102
+ }
103
+
104
+ /** Concatenates hook-name lists in the given order, keeping each name's first occurrence. */
105
+ function mergeUniqueNames(lists: Array<ReadonlyArray<string>>): Array<string> {
106
+ const seen = new Set<string>();
107
+ const merged: Array<string> = [];
108
+ for (const list of lists) {
109
+ for (const name of list) {
110
+ if (!seen.has(name)) {
111
+ seen.add(name);
112
+ merged.push(name);
113
+ }
114
+ }
39
115
  }
116
+ return merged;
40
117
  }
41
118
 
42
119
  /**
43
120
  * @since 0.3.16-canary.0
44
121
  */
45
- export const defaultMetadataReader = new SymbolMetadataReader();
122
+ export const defaultMetadataReader: SymbolMetadataReader = new SymbolMetadataReader();
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Wraps a foreign {@link MetadataReader} so its answers are verified before anything dereferences
3
+ * them.
4
+ */
5
+
6
+ import type { MetadataReader } from "#/metadata/metadata-types";
7
+ import { defaultMetadataReader } from "#/metadata/symbol-metadata-reader";
8
+ import {
9
+ verifyAccessorMetadata,
10
+ verifyConstructorMetadata,
11
+ verifyLifecycleMetadata,
12
+ } from "#/resolution/cache/class-introspector";
13
+
14
+ // Wrapping a wrapper would stack a layer per child container, so each one is remembered.
15
+ const verifyingReaders = new WeakSet<MetadataReader>();
16
+
17
+ /**
18
+ * The reader a container should hand its resolver: verified when it came from outside.
19
+ *
20
+ * @remarks The decorator reader writes the metadata it later reads, so there is nothing to check and
21
+ * nothing to pay — a container that supplies no reader of its own is left on the same code path it
22
+ * has always taken. A supplied reader is a claim, and only its callers can be charged for checking.
23
+ *
24
+ * @since 0.6.0
25
+ */
26
+ export function verifyingMetadataReader(reader: MetadataReader): MetadataReader {
27
+ if (reader === defaultMetadataReader || verifyingReaders.has(reader)) {
28
+ return reader;
29
+ }
30
+ const verifying: MetadataReader = {
31
+ getConstructorMetadata: (target) => verifyConstructorMetadata(reader, target),
32
+ getLifecycleMetadata: (target) => verifyLifecycleMetadata(reader, target),
33
+ ...(reader.getAccessorMetadata === undefined
34
+ ? {}
35
+ : { getAccessorMetadata: (target) => verifyAccessorMetadata(reader, target) }),
36
+ };
37
+
38
+ verifyingReaders.add(verifying);
39
+
40
+ return verifying;
41
+ }
@@ -4,11 +4,11 @@
4
4
  * @remarks Versioned on the lifecycle manager plus the own registry, since `onActivation` can be
5
5
  * registered at any time and a rebind mints binding ids the memo must not keep forever.
6
6
  */
7
- import type { Binding } from "#/binding";
8
- import type { BindingRegistry } from "#/registry";
9
- import type { ClassIntrospector } from "#/resolution/class-introspector";
10
- import type { LifecycleManager } from "#/resolution/lifecycle";
11
- import type { BindingIdentifier } from "#/types";
7
+ import type { Binding } from "#/core/binding";
8
+ import type { BindingRegistry } from "#/core/registry";
9
+ import type { BindingIdentifier } from "#/core/types";
10
+ import type { LifecycleManager } from "#/lifecycle/lifecycle-manager";
11
+ import type { ClassIntrospector } from "#/resolution/cache/class-introspector";
12
12
 
13
13
  /**
14
14
  * @since 0.5.0-canary.8
@@ -26,7 +26,7 @@ export class ActivationNeedCache {
26
26
  this.#registry = registry;
27
27
  }
28
28
 
29
- needsActivation<const Value>(binding: Binding<Value>): boolean {
29
+ needsActivation<Value>(binding: Binding<Value>): boolean {
30
30
  // The chain writes a binding's own hook in place with no version anything here can see, so it
31
31
  // is read fresh on every call; the memo covers only container hooks and lifecycle metadata.
32
32
  if (binding.kind !== "alias" && binding.onActivation !== undefined) {
@@ -71,7 +71,7 @@ export class ActivationNeedCache {
71
71
  }
72
72
 
73
73
  // Own hooks are answered before the memo, so both computations cover the memoizable rest only.
74
- #classNeedsActivation<const Value>(binding: Binding<Value> & { kind: "class" }): boolean {
74
+ #classNeedsActivation<Value>(binding: Binding<Value> & { kind: "class" }): boolean {
75
75
  if (this.#lifecycle.hasActivationHandlers(binding.token)) {
76
76
  return true;
77
77
  }
@@ -79,7 +79,7 @@ export class ActivationNeedCache {
79
79
  return this.#classes.knownPostConstruct(binding.target) !== false;
80
80
  }
81
81
 
82
- #nonClassNeedsActivation<const Value>(binding: Binding<Value>): boolean {
82
+ #nonClassNeedsActivation<Value>(binding: Binding<Value>): boolean {
83
83
  return this.#lifecycle.hasActivationHandlers(binding.token);
84
84
  }
85
85
  }