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

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 (100) hide show
  1. package/CHANGELOG.md +68 -0
  2. package/README.md +3 -1
  3. package/dist/binding.d.ts +47 -5
  4. package/dist/binding.d.ts.map +1 -1
  5. package/dist/binding.js +44 -0
  6. package/dist/binding.js.map +1 -1
  7. package/dist/constructor-type.d.ts +4 -5
  8. package/dist/constructor-type.d.ts.map +1 -1
  9. package/dist/container/binding-builders.d.ts +32 -11
  10. package/dist/container/binding-builders.d.ts.map +1 -1
  11. package/dist/container/binding-builders.js +140 -191
  12. package/dist/container/binding-builders.js.map +1 -1
  13. package/dist/container/container.d.ts.map +1 -1
  14. package/dist/container/container.js +75 -92
  15. package/dist/container/container.js.map +1 -1
  16. package/dist/decorators/inject.d.ts +2 -4
  17. package/dist/decorators/inject.d.ts.map +1 -1
  18. package/dist/decorators/inject.js.map +1 -1
  19. package/dist/errors.d.ts +14 -0
  20. package/dist/errors.d.ts.map +1 -1
  21. package/dist/errors.js +17 -0
  22. package/dist/errors.js.map +1 -1
  23. package/dist/index.d.ts +1 -1
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +1 -1
  26. package/dist/index.js.map +1 -1
  27. package/dist/introspection/inspector.js +1 -1
  28. package/dist/introspection/inspector.js.map +1 -1
  29. package/dist/metadata/metadata-keys.d.ts +3 -6
  30. package/dist/metadata/metadata-keys.d.ts.map +1 -1
  31. package/dist/metadata/metadata-keys.js +3 -6
  32. package/dist/metadata/metadata-keys.js.map +1 -1
  33. package/dist/registry.d.ts +14 -2
  34. package/dist/registry.d.ts.map +1 -1
  35. package/dist/registry.js +35 -45
  36. package/dist/registry.js.map +1 -1
  37. package/dist/resolution/activation-need.d.ts +25 -0
  38. package/dist/resolution/activation-need.d.ts.map +1 -0
  39. package/dist/resolution/activation-need.js +64 -0
  40. package/dist/resolution/activation-need.js.map +1 -0
  41. package/dist/resolution/binding-lookup-cache.d.ts +41 -0
  42. package/dist/resolution/binding-lookup-cache.d.ts.map +1 -0
  43. package/dist/resolution/binding-lookup-cache.js +102 -0
  44. package/dist/resolution/binding-lookup-cache.js.map +1 -0
  45. package/dist/resolution/binding-select.js +1 -4
  46. package/dist/resolution/binding-select.js.map +1 -1
  47. package/dist/resolution/class-introspector.d.ts +27 -0
  48. package/dist/resolution/class-introspector.d.ts.map +1 -0
  49. package/dist/resolution/class-introspector.js +60 -0
  50. package/dist/resolution/class-introspector.js.map +1 -0
  51. package/dist/resolution/diagnostics.d.ts +41 -0
  52. package/dist/resolution/diagnostics.d.ts.map +1 -0
  53. package/dist/resolution/diagnostics.js +18 -0
  54. package/dist/resolution/diagnostics.js.map +1 -0
  55. package/dist/resolution/environment.d.ts +22 -1
  56. package/dist/resolution/environment.d.ts.map +1 -1
  57. package/dist/resolution/environment.js +27 -1
  58. package/dist/resolution/environment.js.map +1 -1
  59. package/dist/resolution/instantiation-plan.d.ts +15 -15
  60. package/dist/resolution/instantiation-plan.d.ts.map +1 -1
  61. package/dist/resolution/instantiation-plan.js +68 -48
  62. package/dist/resolution/instantiation-plan.js.map +1 -1
  63. package/dist/resolution/lifecycle.d.ts +2 -0
  64. package/dist/resolution/lifecycle.d.ts.map +1 -1
  65. package/dist/resolution/lifecycle.js +16 -11
  66. package/dist/resolution/lifecycle.js.map +1 -1
  67. package/dist/resolution/resolution-path.d.ts +4 -13
  68. package/dist/resolution/resolution-path.d.ts.map +1 -1
  69. package/dist/resolution/resolution-path.js +3 -16
  70. package/dist/resolution/resolution-path.js.map +1 -1
  71. package/dist/resolution/resolver.d.ts +7 -2
  72. package/dist/resolution/resolver.d.ts.map +1 -1
  73. package/dist/resolution/resolver.js +116 -328
  74. package/dist/resolution/resolver.js.map +1 -1
  75. package/dist/resolution/scope.d.ts +7 -15
  76. package/dist/resolution/scope.d.ts.map +1 -1
  77. package/dist/resolution/scope.js +48 -44
  78. package/dist/resolution/scope.js.map +1 -1
  79. package/package.json +7 -97
  80. package/src/binding.ts +104 -5
  81. package/src/constructor-type.ts +4 -5
  82. package/src/container/binding-builders.ts +180 -283
  83. package/src/container/container.ts +90 -103
  84. package/src/decorators/inject.ts +3 -5
  85. package/src/errors.ts +21 -0
  86. package/src/index.ts +1 -0
  87. package/src/introspection/inspector.ts +1 -1
  88. package/src/metadata/metadata-keys.ts +3 -6
  89. package/src/registry.ts +38 -59
  90. package/src/resolution/activation-need.ts +81 -0
  91. package/src/resolution/binding-lookup-cache.ts +132 -0
  92. package/src/resolution/binding-select.ts +1 -4
  93. package/src/resolution/class-introspector.ts +74 -0
  94. package/src/resolution/diagnostics.ts +43 -0
  95. package/src/resolution/environment.ts +31 -1
  96. package/src/resolution/instantiation-plan.ts +113 -61
  97. package/src/resolution/lifecycle.ts +16 -11
  98. package/src/resolution/resolution-path.ts +5 -20
  99. package/src/resolution/resolver.ts +142 -371
  100. package/src/resolution/scope.ts +50 -49
@@ -1,28 +1,21 @@
1
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.
2
+ * Compiles a transient class or resolved-factory binding into a nested-constructor closure.
7
3
  *
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.
4
+ * @see `ARCHITECTURE.md` — the escape contract every dependency the compiler cannot inline must honour.
12
5
  */
13
6
  import type { Binding } from "#/binding";
7
+ import { NO_INSTANCE } from "#/binding";
14
8
  import type { ConstructorInvocation } from "#/constructor-type";
15
9
  import type { InjectionDescriptor } from "#/decorators/inject";
16
10
  import { AsyncResolutionError } from "#/errors";
17
11
  import type { ConstructorMetadata } from "#/metadata/metadata-types";
18
12
  import { injectionSlotToResolveOptions } from "#/resolution/resolve-options";
19
- import type { ScopeManager } from "#/resolution/scope";
20
- import { SINGLETON_MISS } from "#/resolution/scope";
21
13
  import type { Token } from "#/token";
22
14
  import { tokenName } from "#/token";
23
- import type { BindingScope, Constructor } from "#/types";
15
+ import type { BindingScope, Constructor, ResolutionFrame, ResolveOptions } from "#/types";
24
16
 
25
- // Bail out of pathological graphs — the runtime path handles them correctly.
17
+ // Past this depth a dependency escapes to the runtime path rather than inlining further —
18
+ // compiled closures nest one JS frame per level, and pathological graphs are the runtime's job.
26
19
  const PLAN_DEPTH_LIMIT = 32;
27
20
 
28
21
  /**
@@ -41,13 +34,20 @@ export const PLAN_RETRY: unique symbol = Symbol("di:plan-retry");
41
34
  export type InstantiationPlanCompileResult = (() => unknown) | null | typeof PLAN_RETRY;
42
35
 
43
36
  /**
44
- * A dependency's terminal binding plus the scope cache of the resolver that owns it.
37
+ * What compiling one *dependency* can yield.
38
+ *
39
+ * @remarks No `null`: a dependency escapes rather than failing, so "no plan" is only ever a
40
+ * verdict on a plan's root.
41
+ */
42
+ type DependencyCompileResult = (() => unknown) | typeof PLAN_RETRY;
43
+
44
+ /**
45
+ * A dependency's terminal binding — all a compiled thunk needs.
45
46
  *
46
47
  * @since 0.5.0-canary.7
47
48
  */
48
49
  export interface InstantiationPlanDependencyEntry {
49
50
  readonly binding: Binding;
50
- readonly ownerScope: ScopeManager;
51
51
  }
52
52
 
53
53
  /**
@@ -64,10 +64,26 @@ export interface InstantiationPlanHost {
64
64
  getConstructorMetadata(target: Constructor): ConstructorMetadata | undefined;
65
65
  /** Options-less lookup with alias hops folded; `null` when the fast lane can't answer. */
66
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;
67
+ /** The frame the interpreted path pushes for this binding, so escapes can replay it. */
68
+ getResolutionFrame(binding: Binding): ResolutionFrame;
69
+ /** Runtime resolve for an escaped dependency, seeded with the ancestors above it. */
70
+ resolveEscaped(
71
+ token: Token<unknown> | Constructor,
72
+ options: ResolveOptions | undefined,
73
+ arity: EscapeArity,
74
+ resolutionPath: Array<string>,
75
+ resolutionStack: Array<ResolutionFrame>,
76
+ ): unknown;
69
77
  }
70
78
 
79
+ /**
80
+ * Which resolve an escaped dependency replays — mirrors how the interpreted path dispatches
81
+ * a constructor param.
82
+ *
83
+ * @since 0.5.0-canary.8
84
+ */
85
+ export type EscapeArity = "all" | "optional" | "single";
86
+
71
87
  /**
72
88
  * @since 0.3.16-canary.1
73
89
  */
@@ -80,8 +96,26 @@ export class InstantiationPlanCompiler {
80
96
 
81
97
  compile(binding: Binding & { kind: "class" | "resolved" }): InstantiationPlanCompileResult {
82
98
  return binding.kind === "class"
83
- ? this.#compileClassPlan(binding, new Set(), 0)
84
- : this.#compileResolvedPlan(binding, new Set(), 0);
99
+ ? this.#compileClassPlan(binding, new Set(), 0, [])
100
+ : this.#compileResolvedPlan(binding, new Set(), 0, []);
101
+ }
102
+
103
+ /**
104
+ * Re-entry into the runtime resolver for a dependency the plan can't see through.
105
+ *
106
+ * The ancestors are fixed at compile time, so the seeds are built once; each call copies
107
+ * them because the resolver pushes and pops on the arrays it is given.
108
+ */
109
+ #compileEscapeThunk(
110
+ token: Token<unknown> | Constructor,
111
+ ancestors: ReadonlyArray<Binding>,
112
+ arity: EscapeArity = "single",
113
+ options?: ResolveOptions,
114
+ ): () => unknown {
115
+ const host = this.#host;
116
+ const frames = ancestors.map((ancestor) => host.getResolutionFrame(ancestor));
117
+ const names = frames.map((frame) => frame.tokenName);
118
+ return () => host.resolveEscaped(token, options, arity, [...names], [...frames]);
85
119
  }
86
120
 
87
121
  // A resolved binding declares its deps as explicit descriptors — same rules as
@@ -90,21 +124,20 @@ export class InstantiationPlanCompiler {
90
124
  binding: Binding & { kind: "resolved" },
91
125
  compileStack: Set<Binding["id"]>,
92
126
  depth: number,
127
+ ancestors: ReadonlyArray<Binding>,
93
128
  ): InstantiationPlanCompileResult {
94
- if (depth > PLAN_DEPTH_LIMIT || compileStack.has(binding.id)) {
95
- return null;
96
- }
97
129
  if (binding.onActivation !== undefined || this.#host.hasActivationHandlers(binding.token)) {
98
130
  return null;
99
131
  }
100
132
  const factory = binding.factory;
101
133
  const tokenDisplayName = tokenName(binding.token);
102
134
  const depThunks = new Array<() => unknown>(binding.deps.length);
135
+ const depAncestors = [...ancestors, binding];
103
136
  compileStack.add(binding.id);
104
137
  try {
105
138
  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) {
139
+ const thunk = this.#compileInjectionThunk(binding.deps[index]!, compileStack, depth, depAncestors);
140
+ if (thunk === PLAN_RETRY) {
108
141
  return thunk;
109
142
  }
110
143
  depThunks[index] = thunk;
@@ -121,29 +154,41 @@ export class InstantiationPlanCompiler {
121
154
  };
122
155
  }
123
156
 
124
- #compileDescriptorThunk(
157
+ /**
158
+ * One dependency of a plan node — a constructor param or a `toResolved` descriptor.
159
+ *
160
+ * @remarks Anything but a plain required single dependency escapes to the runtime path.
161
+ */
162
+ #compileInjectionThunk(
125
163
  descriptor: InjectionDescriptor,
126
164
  compileStack: Set<Binding["id"]>,
127
165
  depth: number,
128
- ): InstantiationPlanCompileResult {
129
- if (descriptor.multi || descriptor.optional || injectionSlotToResolveOptions(descriptor) !== undefined) {
130
- return null;
166
+ ancestors: ReadonlyArray<Binding>,
167
+ ): DependencyCompileResult {
168
+ const token = descriptor.token as Token<unknown> | Constructor;
169
+ const options = injectionSlotToResolveOptions(descriptor);
170
+ if (descriptor.multi) {
171
+ return this.#compileEscapeThunk(token, ancestors, "all", options);
172
+ }
173
+ if (descriptor.optional) {
174
+ return this.#compileEscapeThunk(token, ancestors, "optional", options);
131
175
  }
132
- const entry = this.#host.lookupDependencyEntry(descriptor.token as Token<unknown> | Constructor);
176
+ if (options !== undefined) {
177
+ return this.#compileEscapeThunk(token, ancestors, "single", options);
178
+ }
179
+ const entry = this.#host.lookupDependencyEntry(token);
133
180
  if (entry === null) {
134
- return null;
181
+ return this.#compileEscapeThunk(token, ancestors);
135
182
  }
136
- return this.#compileDepThunk(entry, compileStack, depth);
183
+ return this.#compileDepThunk(entry, compileStack, depth, ancestors);
137
184
  }
138
185
 
139
186
  #compileClassPlan(
140
187
  binding: Binding & { kind: "class" },
141
188
  compileStack: Set<Binding["id"]>,
142
189
  depth: number,
190
+ ancestors: ReadonlyArray<Binding>,
143
191
  ): InstantiationPlanCompileResult {
144
- if (depth > PLAN_DEPTH_LIMIT || compileStack.has(binding.id)) {
145
- return null;
146
- }
147
192
  if (binding.onActivation !== undefined || this.#host.hasActivationHandlers(binding.token)) {
148
193
  return null;
149
194
  }
@@ -166,19 +211,12 @@ export class InstantiationPlanCompiler {
166
211
  return () => new invokable();
167
212
  }
168
213
  const depThunks = new Array<() => unknown>(params.length);
214
+ const depAncestors = [...ancestors, binding];
169
215
  compileStack.add(binding.id);
170
216
  try {
171
217
  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) {
218
+ const thunk = this.#compileInjectionThunk(params[index]!, compileStack, depth, depAncestors);
219
+ if (thunk === PLAN_RETRY) {
182
220
  return thunk;
183
221
  }
184
222
  depThunks[index] = thunk;
@@ -211,30 +249,44 @@ export class InstantiationPlanCompiler {
211
249
  entry: InstantiationPlanDependencyEntry,
212
250
  compileStack: Set<Binding["id"]>,
213
251
  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;
252
+ ancestors: ReadonlyArray<Binding>,
253
+ ): DependencyCompileResult {
254
+ const { binding } = entry;
255
+ if (binding.kind === "constant" && binding.onActivation === undefined) {
256
+ if (!this.#host.hasActivationHandlers(binding.token)) {
257
+ const value = binding.value;
258
+ return () => value;
219
259
  }
220
- const value = binding.value;
221
- return () => value;
222
260
  }
223
261
  const scope = (binding as Binding & { scope: BindingScope }).scope ?? "transient";
224
262
  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;
263
+ // Cached-singleton read; the first materialization escapes so it sees the same ancestors
264
+ // (and therefore the same cycle detection) the interpreted path would have built.
265
+ const escape = this.#compileEscapeThunk(binding.token, ancestors);
266
+ const singletonBinding = binding;
229
267
  return () => {
230
- const cachedSingleton = ownerScope.peekSingleton(bindingId);
231
- return cachedSingleton === SINGLETON_MISS ? host.resolveFallback(singletonToken) : cachedSingleton;
268
+ const cached = singletonBinding.instance;
269
+ return cached === NO_INSTANCE ? escape() : cached;
232
270
  };
233
271
  }
234
- if (scope === "transient" && binding.kind === "class") {
235
- return this.#compileClassPlan(binding as Binding & { kind: "class" }, compileStack, depth + 1);
272
+ if (
273
+ scope === "transient" &&
274
+ binding.kind === "class" &&
275
+ depth < PLAN_DEPTH_LIMIT &&
276
+ !compileStack.has(binding.id)
277
+ ) {
278
+ const inlined = this.#compileClassPlan(
279
+ binding as Binding & { kind: "class" },
280
+ compileStack,
281
+ depth + 1,
282
+ ancestors,
283
+ );
284
+ if (inlined !== null) {
285
+ return inlined;
286
+ }
236
287
  }
237
- // Dynamic/resolved/scoped deps keep the runtime path (and its cycle guard).
238
- return null;
288
+ // Anything opaque — a factory, a scoped binding, an activation hook, a class the compiler
289
+ // declined — runs on the runtime path, seeded with this plan's ancestors.
290
+ return this.#compileEscapeThunk(binding.token, ancestors);
239
291
  }
240
292
  }
@@ -9,20 +9,21 @@ import type { ActivationHandler, Constructor, DeactivationHandler, ResolutionCon
9
9
  * @since 0.3.16-canary.0
10
10
  */
11
11
  export class LifecycleManager {
12
- // Container-level activation/deactivation hooks per token
13
- readonly #activationHooks = new Map<Token<unknown> | Constructor, Array<ActivationHandler<unknown>>>();
14
- readonly #deactivationHooks = new Map<Token<unknown> | Constructor, Array<DeactivationHandler<unknown>>>();
12
+ // Container-level activation/deactivation hooks per token — most containers register none, so
13
+ // 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;
15
16
  #activationVersion = 0;
16
17
 
17
18
  registerActivation<const Value>(token: Token<Value> | Constructor<Value>, handler: ActivationHandler<Value>): void {
18
19
  this.#activationVersion += 1;
19
20
  // ✓ TS6.0: Map.getOrInsert (ES2025)
20
- const list = this.#activationHooks.getOrInsert(token as Token<unknown> | Constructor, []);
21
+ const list = (this.#activationHooks ??= new Map()).getOrInsert(token as Token<unknown> | Constructor, []);
21
22
  list.push(handler as ActivationHandler<unknown>);
22
23
  }
23
24
 
24
25
  hasActivationHandlers<const Value>(token: Token<Value> | Constructor<Value>): boolean {
25
- if (this.#activationHooks.size === 0) {
26
+ if (this.#activationHooks === undefined) {
26
27
  return false;
27
28
  }
28
29
  const list = this.#activationHooks.get(token as Token<unknown> | Constructor);
@@ -37,14 +38,14 @@ export class LifecycleManager {
37
38
  activationHandlersFor<const Value>(
38
39
  token: Token<Value> | Constructor<Value>,
39
40
  ): ReadonlyArray<ActivationHandler<unknown>> | undefined {
40
- return this.#activationHooks.get(token as Token<unknown> | Constructor);
41
+ return this.#activationHooks?.get(token as Token<unknown> | Constructor);
41
42
  }
42
43
 
43
44
  registerDeactivation<const Value>(
44
45
  token: Token<Value> | Constructor<Value>,
45
46
  handler: DeactivationHandler<Value>,
46
47
  ): void {
47
- const list = this.#deactivationHooks.getOrInsert(token as Token<unknown> | Constructor, []);
48
+ const list = (this.#deactivationHooks ??= new Map()).getOrInsert(token as Token<unknown> | Constructor, []);
48
49
  list.push(handler as DeactivationHandler<unknown>);
49
50
  }
50
51
 
@@ -79,7 +80,7 @@ export class LifecycleManager {
79
80
  }
80
81
 
81
82
  // 3. container-level onActivation
82
- const containerHooks = this.#activationHooks.get(binding.token as Token<unknown> | Constructor);
83
+ const containerHooks = this.#activationHooks?.get(binding.token as Token<unknown> | Constructor);
83
84
  if (containerHooks !== undefined) {
84
85
  for (const hook of containerHooks) {
85
86
  const activationResult = hook(resolutionContext, activatedInstance);
@@ -125,7 +126,7 @@ export class LifecycleManager {
125
126
 
126
127
  // 3. container-level onActivation (must be sync)
127
128
  const tokenDisplayName = tokenName(binding.token);
128
- const containerHooks = this.#activationHooks.get(binding.token as Token<unknown> | Constructor);
129
+ const containerHooks = this.#activationHooks?.get(binding.token as Token<unknown> | Constructor);
129
130
  if (containerHooks !== undefined) {
130
131
  for (const hook of containerHooks) {
131
132
  const activationResult = hook(resolutionContext, activatedInstance);
@@ -147,7 +148,7 @@ export class LifecycleManager {
147
148
  const tokenKey = binding.token as Token<unknown> | Constructor;
148
149
 
149
150
  // 1. container-level onDeactivation
150
- const containerHooks = this.#deactivationHooks.get(tokenKey);
151
+ const containerHooks = this.#deactivationHooks?.get(tokenKey);
151
152
  if (containerHooks !== undefined) {
152
153
  for (const hook of containerHooks) {
153
154
  const hookResult = hook(instance);
@@ -187,7 +188,7 @@ export class LifecycleManager {
187
188
  const tokenKey = binding.token as Token<unknown> | Constructor;
188
189
 
189
190
  // 1. container-level onDeactivation
190
- const containerHooks = this.#deactivationHooks.get(tokenKey);
191
+ const containerHooks = this.#deactivationHooks?.get(tokenKey);
191
192
  if (containerHooks !== undefined) {
192
193
  for (const hook of containerHooks) {
193
194
  const hookResult = hook(instance);
@@ -221,4 +222,8 @@ export class LifecycleManager {
221
222
  }
222
223
  }
223
224
  }
225
+ /** Whether the deferred table behind `#activationHooks` has had to be built. */
226
+ get isBuilt(): boolean {
227
+ return this.#activationHooks !== undefined;
228
+ }
224
229
  }
@@ -1,21 +1,11 @@
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
- */
1
+ /** Cycle-detection bookkeeping carried on the resolution path itself. */
8
2
  import { CircularDependencyError } from "#/errors";
9
3
 
10
4
  const RESOLUTION_SET_KEY: unique symbol = Symbol("di:resolution-set");
11
5
  /**
12
6
  * Where the cycle check switches from a linear `Array.includes` scan to an attached Set.
13
7
  *
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.
8
+ * @see `ARCHITECTURE.md` — the depth sweep this value comes from.
19
9
  *
20
10
  * @since 0.5.0-canary.7
21
11
  */
@@ -23,11 +13,9 @@ export const RESOLUTION_SET_THRESHOLD = 32;
23
13
  type ResolutionPathWithSet = Array<string> & { [RESOLUTION_SET_KEY]?: Set<string> };
24
14
 
25
15
  /**
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.
16
+ * Marks a token as in-flight on this path, throwing if it is already there.
29
17
  *
30
- * Callers unmark with `resolutionPath.pop()` + `set?.delete(name)` on unwind.
18
+ * @remarks Unmark with `resolutionPath.pop()` plus `set?.delete(name)`, or {@link exitResolutionPath}.
31
19
  */
32
20
  export function enterResolutionPath(
33
21
  resolutionPath: Array<string>,
@@ -62,10 +50,7 @@ export function enterResolutionPath(
62
50
  }
63
51
 
64
52
  /**
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.
53
+ * Unwinds the innermost {@link enterResolutionPath} entry, for callers that hold only the path.
69
54
  *
70
55
  * @since 0.5.0-canary.7
71
56
  */