@codefast/di 0.5.0-canary.5 → 0.5.0-canary.7

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 (214) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/README.md +73 -496
  3. package/dist/binding.d.ts +211 -0
  4. package/dist/binding.d.ts.map +1 -0
  5. package/dist/binding.js +46 -0
  6. package/dist/binding.js.map +1 -0
  7. package/dist/{constructor-type.d.mts → constructor-type.d.ts} +3 -5
  8. package/dist/constructor-type.d.ts.map +1 -0
  9. package/dist/constructor-type.js +2 -0
  10. package/dist/constructor-type.js.map +1 -0
  11. package/dist/container/binding-builders.d.ts +35 -0
  12. package/dist/container/binding-builders.d.ts.map +1 -0
  13. package/dist/container/binding-builders.js +247 -0
  14. package/dist/container/binding-builders.js.map +1 -0
  15. package/dist/container/container.d.ts +58 -0
  16. package/dist/container/container.d.ts.map +1 -0
  17. package/dist/container/container.js +641 -0
  18. package/dist/container/container.js.map +1 -0
  19. package/dist/decorators/inject.d.ts +57 -0
  20. package/dist/decorators/inject.d.ts.map +1 -0
  21. package/dist/decorators/inject.js +145 -0
  22. package/dist/decorators/inject.js.map +1 -0
  23. package/dist/decorators/injectable.d.ts +29 -0
  24. package/dist/decorators/injectable.d.ts.map +1 -0
  25. package/dist/decorators/injectable.js +53 -0
  26. package/dist/decorators/injectable.js.map +1 -0
  27. package/dist/decorators/lifecycle-decorators.d.ts +9 -0
  28. package/dist/decorators/lifecycle-decorators.d.ts.map +1 -0
  29. package/dist/decorators/lifecycle-decorators.js +40 -0
  30. package/dist/decorators/lifecycle-decorators.js.map +1 -0
  31. package/dist/errors.d.ts +150 -0
  32. package/dist/errors.d.ts.map +1 -0
  33. package/dist/errors.js +197 -0
  34. package/dist/errors.js.map +1 -0
  35. package/dist/index.d.ts +30 -0
  36. package/dist/index.d.ts.map +1 -0
  37. package/dist/index.js +25 -0
  38. package/dist/index.js.map +1 -0
  39. package/dist/introspection/dependency-graph.d.ts +40 -0
  40. package/dist/introspection/dependency-graph.d.ts.map +1 -0
  41. package/dist/introspection/dependency-graph.js +66 -0
  42. package/dist/introspection/dependency-graph.js.map +1 -0
  43. package/dist/introspection/graph-adapters/cytoscape.d.ts +33 -0
  44. package/dist/introspection/graph-adapters/cytoscape.d.ts.map +1 -0
  45. package/dist/introspection/graph-adapters/cytoscape.js +30 -0
  46. package/dist/introspection/graph-adapters/cytoscape.js.map +1 -0
  47. package/dist/introspection/graph-adapters/dot.d.ts +6 -0
  48. package/dist/introspection/graph-adapters/dot.d.ts.map +1 -0
  49. package/dist/introspection/graph-adapters/dot.js +18 -0
  50. package/dist/introspection/graph-adapters/dot.js.map +1 -0
  51. package/dist/introspection/graph-adapters/reactflow.d.ts +38 -0
  52. package/dist/introspection/graph-adapters/reactflow.d.ts.map +1 -0
  53. package/dist/introspection/graph-adapters/reactflow.js +33 -0
  54. package/dist/introspection/graph-adapters/reactflow.js.map +1 -0
  55. package/dist/introspection/inspector.d.ts +39 -0
  56. package/dist/introspection/inspector.d.ts.map +1 -0
  57. package/dist/introspection/inspector.js +82 -0
  58. package/dist/introspection/inspector.js.map +1 -0
  59. package/dist/metadata/{metadata-keys.d.mts → metadata-keys.d.ts} +5 -7
  60. package/dist/metadata/metadata-keys.d.ts.map +1 -0
  61. package/dist/metadata/metadata-keys.js +25 -0
  62. package/dist/metadata/metadata-keys.js.map +1 -0
  63. package/dist/metadata/metadata-reader-token.d.ts +7 -0
  64. package/dist/metadata/metadata-reader-token.d.ts.map +1 -0
  65. package/dist/metadata/metadata-reader-token.js +6 -0
  66. package/dist/metadata/metadata-reader-token.js.map +1 -0
  67. package/dist/metadata/metadata-types.d.ts +48 -0
  68. package/dist/metadata/metadata-types.d.ts.map +1 -0
  69. package/dist/metadata/metadata-types.js +2 -0
  70. package/dist/metadata/metadata-types.js.map +1 -0
  71. package/dist/metadata/symbol-metadata-reader.d.ts +20 -0
  72. package/dist/metadata/symbol-metadata-reader.d.ts.map +1 -0
  73. package/dist/metadata/symbol-metadata-reader.js +34 -0
  74. package/dist/metadata/symbol-metadata-reader.js.map +1 -0
  75. package/dist/module.d.ts +67 -0
  76. package/dist/module.d.ts.map +1 -0
  77. package/dist/module.js +54 -0
  78. package/dist/module.js.map +1 -0
  79. package/dist/registry.d.ts +33 -0
  80. package/dist/registry.d.ts.map +1 -0
  81. package/dist/registry.js +249 -0
  82. package/dist/registry.js.map +1 -0
  83. package/dist/resolution/binding-scope.d.ts +10 -0
  84. package/dist/resolution/binding-scope.d.ts.map +1 -0
  85. package/dist/resolution/binding-scope.js +24 -0
  86. package/dist/resolution/binding-scope.js.map +1 -0
  87. package/dist/resolution/binding-select.d.ts +16 -0
  88. package/dist/resolution/binding-select.d.ts.map +1 -0
  89. package/dist/resolution/binding-select.js +143 -0
  90. package/dist/resolution/binding-select.js.map +1 -0
  91. package/dist/resolution/constraints.d.ts +51 -0
  92. package/dist/resolution/constraints.d.ts.map +1 -0
  93. package/dist/resolution/constraints.js +88 -0
  94. package/dist/resolution/constraints.js.map +1 -0
  95. package/dist/resolution/environment.d.ts +47 -0
  96. package/dist/resolution/environment.d.ts.map +1 -0
  97. package/dist/resolution/environment.js +100 -0
  98. package/dist/resolution/environment.js.map +1 -0
  99. package/dist/resolution/instantiation-plan.d.ts +67 -0
  100. package/dist/resolution/instantiation-plan.d.ts.map +1 -0
  101. package/dist/resolution/instantiation-plan.js +163 -0
  102. package/dist/resolution/instantiation-plan.js.map +1 -0
  103. package/dist/resolution/lifecycle.d.ts +21 -0
  104. package/dist/resolution/lifecycle.d.ts.map +1 -0
  105. package/dist/resolution/lifecycle.js +178 -0
  106. package/dist/resolution/lifecycle.js.map +1 -0
  107. package/dist/resolution/resolution-path.d.ts +31 -0
  108. package/dist/resolution/resolution-path.d.ts.map +1 -0
  109. package/dist/resolution/resolution-path.js +53 -0
  110. package/dist/resolution/resolution-path.js.map +1 -0
  111. package/dist/resolution/resolve-options.d.ts +19 -0
  112. package/dist/resolution/resolve-options.d.ts.map +1 -0
  113. package/dist/resolution/resolve-options.js +32 -0
  114. package/dist/resolution/resolve-options.js.map +1 -0
  115. package/dist/resolution/resolver.d.ts +35 -0
  116. package/dist/resolution/resolver.d.ts.map +1 -0
  117. package/dist/resolution/resolver.js +1320 -0
  118. package/dist/resolution/resolver.js.map +1 -0
  119. package/dist/resolution/scope.d.ts +32 -0
  120. package/dist/resolution/scope.d.ts.map +1 -0
  121. package/dist/resolution/scope.js +76 -0
  122. package/dist/resolution/scope.js.map +1 -0
  123. package/dist/token.d.ts +23 -0
  124. package/dist/token.d.ts.map +1 -0
  125. package/dist/token.js +22 -0
  126. package/dist/token.js.map +1 -0
  127. package/dist/types.d.ts +90 -0
  128. package/dist/types.d.ts.map +1 -0
  129. package/dist/types.js +2 -0
  130. package/dist/types.js.map +1 -0
  131. package/package.json +143 -120
  132. package/src/binding.ts +38 -22
  133. package/src/container/binding-builders.ts +397 -0
  134. package/src/container/container.ts +838 -0
  135. package/src/decorators/inject.ts +16 -3
  136. package/src/decorators/injectable.ts +3 -3
  137. package/src/index.ts +14 -7
  138. package/src/{dependency-graph.ts → introspection/dependency-graph.ts} +1 -1
  139. package/src/{graph-adapters → introspection/graph-adapters}/cytoscape.ts +1 -1
  140. package/src/{graph-adapters → introspection/graph-adapters}/dot.ts +1 -1
  141. package/src/{graph-adapters → introspection/graph-adapters}/reactflow.ts +13 -2
  142. package/src/{inspector.ts → introspection/inspector.ts} +26 -21
  143. package/src/metadata/symbol-metadata-reader.ts +4 -4
  144. package/src/module.ts +14 -6
  145. package/src/registry.ts +115 -61
  146. package/src/{binding-select.ts → resolution/binding-select.ts} +16 -3
  147. package/src/{environment.ts → resolution/environment.ts} +35 -35
  148. package/src/resolution/instantiation-plan.ts +240 -0
  149. package/src/{lifecycle.ts → resolution/lifecycle.ts} +20 -13
  150. package/src/resolution/resolution-path.ts +77 -0
  151. package/src/resolution/resolver.ts +1755 -0
  152. package/src/{scope.ts → resolution/scope.ts} +35 -18
  153. package/src/token.ts +0 -3
  154. package/dist/binding-scope.d.mts +0 -13
  155. package/dist/binding-scope.mjs +0 -21
  156. package/dist/binding-select.d.mts +0 -19
  157. package/dist/binding-select.mjs +0 -72
  158. package/dist/binding.d.mts +0 -191
  159. package/dist/binding.mjs +0 -36
  160. package/dist/constraints.d.mts +0 -55
  161. package/dist/constraints.mjs +0 -87
  162. package/dist/constructor-type.mjs +0 -1
  163. package/dist/container.d.mts +0 -62
  164. package/dist/container.mjs +0 -741
  165. package/dist/decorators/inject.d.mts +0 -49
  166. package/dist/decorators/inject.mjs +0 -136
  167. package/dist/decorators/injectable.d.mts +0 -32
  168. package/dist/decorators/injectable.mjs +0 -57
  169. package/dist/decorators/lifecycle-decorators.d.mts +0 -11
  170. package/dist/decorators/lifecycle-decorators.mjs +0 -38
  171. package/dist/dependency-graph.d.mts +0 -43
  172. package/dist/dependency-graph.mjs +0 -63
  173. package/dist/environment.d.mts +0 -55
  174. package/dist/environment.mjs +0 -98
  175. package/dist/errors.d.mts +0 -153
  176. package/dist/errors.mjs +0 -197
  177. package/dist/graph-adapters/cytoscape.d.mts +0 -36
  178. package/dist/graph-adapters/cytoscape.mjs +0 -26
  179. package/dist/graph-adapters/dot.d.mts +0 -9
  180. package/dist/graph-adapters/dot.mjs +0 -20
  181. package/dist/graph-adapters/reactflow.d.mts +0 -41
  182. package/dist/graph-adapters/reactflow.mjs +0 -29
  183. package/dist/index.d.mts +0 -18
  184. package/dist/index.mjs +0 -12
  185. package/dist/inspector.d.mts +0 -48
  186. package/dist/inspector.mjs +0 -71
  187. package/dist/lifecycle.d.mts +0 -25
  188. package/dist/lifecycle.mjs +0 -118
  189. package/dist/metadata/metadata-keys.mjs +0 -27
  190. package/dist/metadata/metadata-reader-token.d.mts +0 -10
  191. package/dist/metadata/metadata-reader-token.mjs +0 -8
  192. package/dist/metadata/metadata-types.d.mts +0 -51
  193. package/dist/metadata/metadata-types.mjs +0 -1
  194. package/dist/metadata/symbol-metadata-reader.d.mts +0 -23
  195. package/dist/metadata/symbol-metadata-reader.mjs +0 -29
  196. package/dist/module.d.mts +0 -62
  197. package/dist/module.mjs +0 -42
  198. package/dist/registry.d.mts +0 -45
  199. package/dist/registry.mjs +0 -163
  200. package/dist/resolve-options.d.mts +0 -22
  201. package/dist/resolve-options.mjs +0 -28
  202. package/dist/resolver.d.mts +0 -94
  203. package/dist/resolver.mjs +0 -767
  204. package/dist/scope.d.mts +0 -28
  205. package/dist/scope.mjs +0 -58
  206. package/dist/token.d.mts +0 -25
  207. package/dist/token.mjs +0 -22
  208. package/dist/types.d.mts +0 -92
  209. package/dist/types.mjs +0 -1
  210. package/src/container.ts +0 -1141
  211. package/src/resolver.ts +0 -1626
  212. /package/src/{binding-scope.ts → resolution/binding-scope.ts} +0 -0
  213. /package/src/{constraints.ts → resolution/constraints.ts} +0 -0
  214. /package/src/{resolve-options.ts → resolution/resolve-options.ts} +0 -0
@@ -0,0 +1,240 @@
1
+ /**
2
+ * Compiler for the resolver's Dagger-style instantiation plans: a transient
3
+ * class or resolved-factory binding whose dependency subgraph is pure static
4
+ * (class/constant/cached-singleton deps, no activation hooks or postConstruct)
5
+ * compiles once into a nested-constructor/factory closure, cycle-checked at
6
+ * compile time.
7
+ *
8
+ * Compilation is cold-path — it runs once per (binding, cache version). The
9
+ * closures it returns ARE the hot path and touch nothing but their captures.
10
+ * Anything dynamic refuses to compile so the runtime cycle guard stays in
11
+ * charge and error semantics never change.
12
+ */
13
+ import type { Binding } from "#/binding";
14
+ import type { ConstructorInvocation } from "#/constructor-type";
15
+ import type { InjectionDescriptor } from "#/decorators/inject";
16
+ import { AsyncResolutionError } from "#/errors";
17
+ import type { ConstructorMetadata } from "#/metadata/metadata-types";
18
+ import { injectionSlotToResolveOptions } from "#/resolution/resolve-options";
19
+ import type { ScopeManager } from "#/resolution/scope";
20
+ import { SINGLETON_MISS } from "#/resolution/scope";
21
+ import type { Token } from "#/token";
22
+ import { tokenName } from "#/token";
23
+ import type { BindingScope, Constructor } from "#/types";
24
+
25
+ // Bail out of pathological graphs — the runtime path handles them correctly.
26
+ const PLAN_DEPTH_LIMIT = 32;
27
+
28
+ /**
29
+ * Compilation asked to retry later (class lifecycle metadata not discovered yet).
30
+ *
31
+ * @since 0.5.0-canary.7
32
+ */
33
+ export const PLAN_RETRY: unique symbol = Symbol("di:plan-retry");
34
+
35
+ /**
36
+ * A compiled plan, `null` for "not plannable under the current cache versions",
37
+ * or {@link PLAN_RETRY} when a first runtime resolve must discover metadata first.
38
+ *
39
+ * @since 0.5.0-canary.7
40
+ */
41
+ export type InstantiationPlanCompileResult = (() => unknown) | null | typeof PLAN_RETRY;
42
+
43
+ /**
44
+ * A dependency's terminal binding plus the scope cache of the resolver that owns it.
45
+ *
46
+ * @since 0.5.0-canary.7
47
+ */
48
+ export interface InstantiationPlanDependencyEntry {
49
+ readonly binding: Binding;
50
+ readonly ownerScope: ScopeManager;
51
+ }
52
+
53
+ /**
54
+ * Everything the compiler needs from its resolver, expressed as behavior so the
55
+ * compiler stays independently testable and free of resolver internals.
56
+ *
57
+ * @since 0.5.0-canary.7
58
+ */
59
+ export interface InstantiationPlanHost {
60
+ hasActivationHandlers(token: Token<unknown> | Constructor): boolean;
61
+ /** Cached postConstruct presence — `undefined` until a runtime resolve discovers it. */
62
+ knownPostConstruct(target: Constructor): boolean | undefined;
63
+ needsActiveContainer(target: Constructor): boolean;
64
+ getConstructorMetadata(target: Constructor): ConstructorMetadata | undefined;
65
+ /** Options-less lookup with alias hops folded; `null` when the fast lane can't answer. */
66
+ lookupDependencyEntry(token: Token<unknown> | Constructor): InstantiationPlanDependencyEntry | null;
67
+ /** Full runtime resolve — used by singleton thunks for the first materialization. */
68
+ resolveFallback(token: Token<unknown> | Constructor): unknown;
69
+ }
70
+
71
+ /**
72
+ * @since 0.3.16-canary.1
73
+ */
74
+ export class InstantiationPlanCompiler {
75
+ readonly #host: InstantiationPlanHost;
76
+
77
+ constructor(host: InstantiationPlanHost) {
78
+ this.#host = host;
79
+ }
80
+
81
+ compile(binding: Binding & { kind: "class" | "resolved" }): InstantiationPlanCompileResult {
82
+ return binding.kind === "class"
83
+ ? this.#compileClassPlan(binding, new Set(), 0)
84
+ : this.#compileResolvedPlan(binding, new Set(), 0);
85
+ }
86
+
87
+ // A resolved binding declares its deps as explicit descriptors — same rules as
88
+ // class params, with the factory call (and its sync-only check) in place of `new`.
89
+ #compileResolvedPlan(
90
+ binding: Binding & { kind: "resolved" },
91
+ compileStack: Set<Binding["id"]>,
92
+ depth: number,
93
+ ): InstantiationPlanCompileResult {
94
+ if (depth > PLAN_DEPTH_LIMIT || compileStack.has(binding.id)) {
95
+ return null;
96
+ }
97
+ if (binding.onActivation !== undefined || this.#host.hasActivationHandlers(binding.token)) {
98
+ return null;
99
+ }
100
+ const factory = binding.factory;
101
+ const tokenDisplayName = tokenName(binding.token);
102
+ const depThunks = new Array<() => unknown>(binding.deps.length);
103
+ compileStack.add(binding.id);
104
+ try {
105
+ for (let index = 0; index < binding.deps.length; index += 1) {
106
+ const thunk = this.#compileDescriptorThunk(binding.deps[index]!, compileStack, depth);
107
+ if (thunk === null || thunk === PLAN_RETRY) {
108
+ return thunk;
109
+ }
110
+ depThunks[index] = thunk;
111
+ }
112
+ } finally {
113
+ compileStack.delete(binding.id);
114
+ }
115
+ return () => {
116
+ const factoryResult = factory(...depThunks.map((thunk) => thunk()));
117
+ if (factoryResult instanceof Promise) {
118
+ throw new AsyncResolutionError(tokenDisplayName, tokenDisplayName);
119
+ }
120
+ return factoryResult;
121
+ };
122
+ }
123
+
124
+ #compileDescriptorThunk(
125
+ descriptor: InjectionDescriptor,
126
+ compileStack: Set<Binding["id"]>,
127
+ depth: number,
128
+ ): InstantiationPlanCompileResult {
129
+ if (descriptor.multi || descriptor.optional || injectionSlotToResolveOptions(descriptor) !== undefined) {
130
+ return null;
131
+ }
132
+ const entry = this.#host.lookupDependencyEntry(descriptor.token as Token<unknown> | Constructor);
133
+ if (entry === null) {
134
+ return null;
135
+ }
136
+ return this.#compileDepThunk(entry, compileStack, depth);
137
+ }
138
+
139
+ #compileClassPlan(
140
+ binding: Binding & { kind: "class" },
141
+ compileStack: Set<Binding["id"]>,
142
+ depth: number,
143
+ ): InstantiationPlanCompileResult {
144
+ if (depth > PLAN_DEPTH_LIMIT || compileStack.has(binding.id)) {
145
+ return null;
146
+ }
147
+ if (binding.onActivation !== undefined || this.#host.hasActivationHandlers(binding.token)) {
148
+ return null;
149
+ }
150
+ const target = binding.target;
151
+ const hasPostConstruct = this.#host.knownPostConstruct(target);
152
+ if (hasPostConstruct === undefined) {
153
+ return PLAN_RETRY;
154
+ }
155
+ if (hasPostConstruct || this.#host.needsActiveContainer(target)) {
156
+ return null;
157
+ }
158
+ const invokable = target as ConstructorInvocation;
159
+ const meta = this.#host.getConstructorMetadata(target);
160
+ if (meta === undefined) {
161
+ // Metadata-less classes with required params throw on the runtime path — keep them there.
162
+ return target.length === 0 ? () => new invokable() : null;
163
+ }
164
+ const params = meta.params;
165
+ if (params.length === 0) {
166
+ return () => new invokable();
167
+ }
168
+ const depThunks = new Array<() => unknown>(params.length);
169
+ compileStack.add(binding.id);
170
+ try {
171
+ for (let index = 0; index < params.length; index += 1) {
172
+ const param = params[index]!;
173
+ if (param.multi || param.optional || injectionSlotToResolveOptions(param) !== undefined) {
174
+ return null;
175
+ }
176
+ const entry = this.#host.lookupDependencyEntry(param.token);
177
+ if (entry === null) {
178
+ return null;
179
+ }
180
+ const thunk = this.#compileDepThunk(entry, compileStack, depth);
181
+ if (thunk === null || thunk === PLAN_RETRY) {
182
+ return thunk;
183
+ }
184
+ depThunks[index] = thunk;
185
+ }
186
+ } finally {
187
+ compileStack.delete(binding.id);
188
+ }
189
+ switch (depThunks.length) {
190
+ case 1: {
191
+ const dep0 = depThunks[0]!;
192
+ return () => new invokable(dep0());
193
+ }
194
+ case 2: {
195
+ const dep0 = depThunks[0]!;
196
+ const dep1 = depThunks[1]!;
197
+ return () => new invokable(dep0(), dep1());
198
+ }
199
+ case 3: {
200
+ const dep0 = depThunks[0]!;
201
+ const dep1 = depThunks[1]!;
202
+ const dep2 = depThunks[2]!;
203
+ return () => new invokable(dep0(), dep1(), dep2());
204
+ }
205
+ default:
206
+ return () => new invokable(...depThunks.map((thunk) => thunk()));
207
+ }
208
+ }
209
+
210
+ #compileDepThunk(
211
+ entry: InstantiationPlanDependencyEntry,
212
+ compileStack: Set<Binding["id"]>,
213
+ depth: number,
214
+ ): InstantiationPlanCompileResult {
215
+ const { binding, ownerScope } = entry;
216
+ if (binding.kind === "constant") {
217
+ if (binding.onActivation !== undefined || this.#host.hasActivationHandlers(binding.token)) {
218
+ return null;
219
+ }
220
+ const value = binding.value;
221
+ return () => value;
222
+ }
223
+ const scope = (binding as Binding & { scope: BindingScope }).scope ?? "transient";
224
+ if (scope === "singleton") {
225
+ // Cached-singleton read with a full-resolve fallback for the first materialization.
226
+ const host = this.#host;
227
+ const bindingId = binding.id;
228
+ const singletonToken = binding.token;
229
+ return () => {
230
+ const cachedSingleton = ownerScope.peekSingleton(bindingId);
231
+ return cachedSingleton === SINGLETON_MISS ? host.resolveFallback(singletonToken) : cachedSingleton;
232
+ };
233
+ }
234
+ if (scope === "transient" && binding.kind === "class") {
235
+ return this.#compileClassPlan(binding as Binding & { kind: "class" }, compileStack, depth + 1);
236
+ }
237
+ // Dynamic/resolved/scoped deps keep the runtime path (and its cycle guard).
238
+ return null;
239
+ }
240
+ }
@@ -10,34 +10,41 @@ import type { ActivationHandler, Constructor, DeactivationHandler, ResolutionCon
10
10
  */
11
11
  export class LifecycleManager {
12
12
  // Container-level activation/deactivation hooks per token
13
- private readonly _activationHooks = new Map<Token<unknown> | Constructor, Array<ActivationHandler<unknown>>>();
14
- private readonly _deactivationHooks = new Map<Token<unknown> | Constructor, Array<DeactivationHandler<unknown>>>();
15
- private _activationVersion = 0;
13
+ readonly #activationHooks = new Map<Token<unknown> | Constructor, Array<ActivationHandler<unknown>>>();
14
+ readonly #deactivationHooks = new Map<Token<unknown> | Constructor, Array<DeactivationHandler<unknown>>>();
15
+ #activationVersion = 0;
16
16
 
17
17
  registerActivation<const Value>(token: Token<Value> | Constructor<Value>, handler: ActivationHandler<Value>): void {
18
- this._activationVersion += 1;
18
+ this.#activationVersion += 1;
19
19
  // ✓ TS6.0: Map.getOrInsert (ES2025)
20
- const list = this._activationHooks.getOrInsert(token as Token<unknown> | Constructor, []);
20
+ const list = this.#activationHooks.getOrInsert(token as Token<unknown> | Constructor, []);
21
21
  list.push(handler as ActivationHandler<unknown>);
22
22
  }
23
23
 
24
24
  hasActivationHandlers<const Value>(token: Token<Value> | Constructor<Value>): boolean {
25
- if (this._activationHooks.size === 0) {
25
+ if (this.#activationHooks.size === 0) {
26
26
  return false;
27
27
  }
28
- const list = this._activationHooks.get(token as Token<unknown> | Constructor);
28
+ const list = this.#activationHooks.get(token as Token<unknown> | Constructor);
29
29
  return list !== undefined && list.length > 0;
30
30
  }
31
31
 
32
32
  get activationVersion(): number {
33
- return this._activationVersion;
33
+ return this.#activationVersion;
34
+ }
35
+
36
+ /** Container-level activation handlers for a token — hot-path accessor, no copies. */
37
+ activationHandlersFor<const Value>(
38
+ token: Token<Value> | Constructor<Value>,
39
+ ): ReadonlyArray<ActivationHandler<unknown>> | undefined {
40
+ return this.#activationHooks.get(token as Token<unknown> | Constructor);
34
41
  }
35
42
 
36
43
  registerDeactivation<const Value>(
37
44
  token: Token<Value> | Constructor<Value>,
38
45
  handler: DeactivationHandler<Value>,
39
46
  ): void {
40
- const list = this._deactivationHooks.getOrInsert(token as Token<unknown> | Constructor, []);
47
+ const list = this.#deactivationHooks.getOrInsert(token as Token<unknown> | Constructor, []);
41
48
  list.push(handler as DeactivationHandler<unknown>);
42
49
  }
43
50
 
@@ -72,7 +79,7 @@ export class LifecycleManager {
72
79
  }
73
80
 
74
81
  // 3. container-level onActivation
75
- const containerHooks = this._activationHooks.get(binding.token as Token<unknown> | Constructor);
82
+ const containerHooks = this.#activationHooks.get(binding.token as Token<unknown> | Constructor);
76
83
  if (containerHooks !== undefined) {
77
84
  for (const hook of containerHooks) {
78
85
  const activationResult = hook(resolutionContext, activatedInstance);
@@ -118,7 +125,7 @@ export class LifecycleManager {
118
125
 
119
126
  // 3. container-level onActivation (must be sync)
120
127
  const tokenDisplayName = tokenName(binding.token);
121
- const containerHooks = this._activationHooks.get(binding.token as Token<unknown> | Constructor);
128
+ const containerHooks = this.#activationHooks.get(binding.token as Token<unknown> | Constructor);
122
129
  if (containerHooks !== undefined) {
123
130
  for (const hook of containerHooks) {
124
131
  const activationResult = hook(resolutionContext, activatedInstance);
@@ -140,7 +147,7 @@ export class LifecycleManager {
140
147
  const tokenKey = binding.token as Token<unknown> | Constructor;
141
148
 
142
149
  // 1. container-level onDeactivation
143
- const containerHooks = this._deactivationHooks.get(tokenKey);
150
+ const containerHooks = this.#deactivationHooks.get(tokenKey);
144
151
  if (containerHooks !== undefined) {
145
152
  for (const hook of containerHooks) {
146
153
  const hookResult = hook(instance);
@@ -180,7 +187,7 @@ export class LifecycleManager {
180
187
  const tokenKey = binding.token as Token<unknown> | Constructor;
181
188
 
182
189
  // 1. container-level onDeactivation
183
- const containerHooks = this._deactivationHooks.get(tokenKey);
190
+ const containerHooks = this.#deactivationHooks.get(tokenKey);
184
191
  if (containerHooks !== undefined) {
185
192
  for (const hook of containerHooks) {
186
193
  const hookResult = hook(instance);
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Cycle-detection bookkeeping shared by every resolution path.
3
+ *
4
+ * The resolution path is a plain string array for cheap push/pop; past
5
+ * RESOLUTION_SET_THRESHOLD entries an O(1) membership Set is attached to the
6
+ * array itself (symbol-keyed) so very deep graphs keep bounded cycle checks.
7
+ */
8
+ import { CircularDependencyError } from "#/errors";
9
+
10
+ const RESOLUTION_SET_KEY: unique symbol = Symbol("di:resolution-set");
11
+ /**
12
+ * Where the cycle check switches from a linear `Array.includes` scan to an attached Set.
13
+ *
14
+ * @remarks Re-measured on Node 26 / M3 Max over an async transient chain (ns/op at depth
15
+ * 16 / 32 / 64 / 128): a threshold of 128 gives 1275 / 3641 / 9645 / 26082, of 32 gives
16
+ * 1202 / 3285 / 7735 / 16837, of 16 gives 1299 / 3694 / 7449 / 15625. Switching at 32 wins
17
+ * the shallow-to-mid depths real graphs actually have while staying close to the best deep
18
+ * numbers; the previous value of 128 was the worst of the three almost everywhere.
19
+ *
20
+ * @since 0.5.0-canary.7
21
+ */
22
+ export const RESOLUTION_SET_THRESHOLD = 32;
23
+ type ResolutionPathWithSet = Array<string> & { [RESOLUTION_SET_KEY]?: Set<string> };
24
+
25
+ /**
26
+ * Shared cycle guard for every transient resolution path: attaches the O(1) membership
27
+ * Set to the path array (lazily past RESOLUTION_SET_THRESHOLD, eagerly when `forceSet`),
28
+ * throws on a repeated token, then marks the token on both structures.
29
+ *
30
+ * Callers unmark with `resolutionPath.pop()` + `set?.delete(name)` on unwind.
31
+ */
32
+ export function enterResolutionPath(
33
+ resolutionPath: Array<string>,
34
+ tokenDisplayName: string,
35
+ forceSet: true,
36
+ ): Set<string>;
37
+ export function enterResolutionPath(
38
+ resolutionPath: Array<string>,
39
+ tokenDisplayName: string,
40
+ forceSet: boolean,
41
+ ): Set<string> | undefined;
42
+ /**
43
+ * @since 0.5.0-canary.7
44
+ */
45
+ export function enterResolutionPath(
46
+ resolutionPath: Array<string>,
47
+ tokenDisplayName: string,
48
+ forceSet: boolean,
49
+ ): Set<string> | undefined {
50
+ const pathWithSet = resolutionPath as ResolutionPathWithSet;
51
+ let resolutionSet = pathWithSet[RESOLUTION_SET_KEY];
52
+ if (resolutionSet === undefined && (forceSet || resolutionPath.length >= RESOLUTION_SET_THRESHOLD)) {
53
+ resolutionSet = new Set<string>(resolutionPath);
54
+ pathWithSet[RESOLUTION_SET_KEY] = resolutionSet;
55
+ }
56
+ if (resolutionSet === undefined ? resolutionPath.includes(tokenDisplayName) : resolutionSet.has(tokenDisplayName)) {
57
+ throw new CircularDependencyError([...resolutionPath, tokenDisplayName]);
58
+ }
59
+ resolutionPath.push(tokenDisplayName);
60
+ resolutionSet?.add(tokenDisplayName);
61
+ return resolutionSet;
62
+ }
63
+
64
+ /**
65
+ * Unwinds the innermost `enterResolutionPath` entry, keeping the attached membership Set in
66
+ * sync. Callers that already hold the entry's name can pop and delete directly; this exists for
67
+ * unwind paths that only have the path array — notably the async chain's shared settle callback,
68
+ * which serves every level and therefore cannot capture a per-level name.
69
+ *
70
+ * @since 0.5.0-canary.7
71
+ */
72
+ export function exitResolutionPath(resolutionPath: Array<string>): void {
73
+ const tokenDisplayName = resolutionPath.pop();
74
+ if (tokenDisplayName !== undefined) {
75
+ (resolutionPath as ResolutionPathWithSet)[RESOLUTION_SET_KEY]?.delete(tokenDisplayName);
76
+ }
77
+ }