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

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 (114) hide show
  1. package/CHANGELOG.md +205 -0
  2. package/README.md +6 -2
  3. package/dist/binding.d.ts +85 -24
  4. package/dist/binding.d.ts.map +1 -1
  5. package/dist/binding.js +55 -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 +144 -192
  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 +141 -201
  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 +24 -0
  20. package/dist/errors.d.ts.map +1 -1
  21. package/dist/errors.js +30 -0
  22. package/dist/errors.js.map +1 -1
  23. package/dist/index.d.ts +1 -2
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +3 -2
  26. package/dist/index.js.map +1 -1
  27. package/dist/introspection/inspector.d.ts +0 -1
  28. package/dist/introspection/inspector.d.ts.map +1 -1
  29. package/dist/introspection/inspector.js +3 -8
  30. package/dist/introspection/inspector.js.map +1 -1
  31. package/dist/metadata/metadata-keys.d.ts +3 -6
  32. package/dist/metadata/metadata-keys.d.ts.map +1 -1
  33. package/dist/metadata/metadata-keys.js +3 -6
  34. package/dist/metadata/metadata-keys.js.map +1 -1
  35. package/dist/registry.d.ts +14 -2
  36. package/dist/registry.d.ts.map +1 -1
  37. package/dist/registry.js +81 -78
  38. package/dist/registry.js.map +1 -1
  39. package/dist/resolution/activation-need.d.ts +27 -0
  40. package/dist/resolution/activation-need.d.ts.map +1 -0
  41. package/dist/resolution/activation-need.js +68 -0
  42. package/dist/resolution/activation-need.js.map +1 -0
  43. package/dist/resolution/binding-lookup-cache.d.ts +41 -0
  44. package/dist/resolution/binding-lookup-cache.d.ts.map +1 -0
  45. package/dist/resolution/binding-lookup-cache.js +118 -0
  46. package/dist/resolution/binding-lookup-cache.js.map +1 -0
  47. package/dist/resolution/binding-scope.d.ts +5 -2
  48. package/dist/resolution/binding-scope.d.ts.map +1 -1
  49. package/dist/resolution/binding-scope.js +6 -17
  50. package/dist/resolution/binding-scope.js.map +1 -1
  51. package/dist/resolution/binding-select.d.ts +8 -1
  52. package/dist/resolution/binding-select.d.ts.map +1 -1
  53. package/dist/resolution/binding-select.js +14 -36
  54. package/dist/resolution/binding-select.js.map +1 -1
  55. package/dist/resolution/class-introspector.d.ts +27 -0
  56. package/dist/resolution/class-introspector.d.ts.map +1 -0
  57. package/dist/resolution/class-introspector.js +60 -0
  58. package/dist/resolution/class-introspector.js.map +1 -0
  59. package/dist/resolution/diagnostics.d.ts +41 -0
  60. package/dist/resolution/diagnostics.d.ts.map +1 -0
  61. package/dist/resolution/diagnostics.js +18 -0
  62. package/dist/resolution/diagnostics.js.map +1 -0
  63. package/dist/resolution/environment.d.ts +48 -1
  64. package/dist/resolution/environment.d.ts.map +1 -1
  65. package/dist/resolution/environment.js +134 -5
  66. package/dist/resolution/environment.js.map +1 -1
  67. package/dist/resolution/instantiation-plan.d.ts +15 -15
  68. package/dist/resolution/instantiation-plan.d.ts.map +1 -1
  69. package/dist/resolution/instantiation-plan.js +69 -49
  70. package/dist/resolution/instantiation-plan.js.map +1 -1
  71. package/dist/resolution/lifecycle.d.ts +2 -0
  72. package/dist/resolution/lifecycle.d.ts.map +1 -1
  73. package/dist/resolution/lifecycle.js +60 -62
  74. package/dist/resolution/lifecycle.js.map +1 -1
  75. package/dist/resolution/resolution-path.d.ts +84 -17
  76. package/dist/resolution/resolution-path.d.ts.map +1 -1
  77. package/dist/resolution/resolution-path.js +68 -23
  78. package/dist/resolution/resolution-path.js.map +1 -1
  79. package/dist/resolution/resolve-options.d.ts +41 -4
  80. package/dist/resolution/resolve-options.d.ts.map +1 -1
  81. package/dist/resolution/resolve-options.js +25 -1
  82. package/dist/resolution/resolve-options.js.map +1 -1
  83. package/dist/resolution/resolver.d.ts +33 -10
  84. package/dist/resolution/resolver.d.ts.map +1 -1
  85. package/dist/resolution/resolver.js +471 -830
  86. package/dist/resolution/resolver.js.map +1 -1
  87. package/dist/resolution/scope.d.ts +11 -15
  88. package/dist/resolution/scope.d.ts.map +1 -1
  89. package/dist/resolution/scope.js +55 -43
  90. package/dist/resolution/scope.js.map +1 -1
  91. package/package.json +10 -106
  92. package/src/binding.ts +146 -24
  93. package/src/constructor-type.ts +4 -5
  94. package/src/container/binding-builders.ts +184 -284
  95. package/src/container/container.ts +161 -221
  96. package/src/decorators/inject.ts +3 -5
  97. package/src/errors.ts +38 -0
  98. package/src/index.ts +4 -1
  99. package/src/introspection/inspector.ts +3 -9
  100. package/src/metadata/metadata-keys.ts +3 -6
  101. package/src/registry.ts +90 -94
  102. package/src/resolution/activation-need.ts +85 -0
  103. package/src/resolution/binding-lookup-cache.ts +148 -0
  104. package/src/resolution/binding-scope.ts +6 -17
  105. package/src/resolution/binding-select.ts +15 -39
  106. package/src/resolution/class-introspector.ts +74 -0
  107. package/src/resolution/diagnostics.ts +43 -0
  108. package/src/resolution/environment.ts +181 -5
  109. package/src/resolution/instantiation-plan.ts +116 -64
  110. package/src/resolution/lifecycle.ts +69 -62
  111. package/src/resolution/resolution-path.ts +122 -43
  112. package/src/resolution/resolve-options.ts +51 -4
  113. package/src/resolution/resolver.ts +649 -1081
  114. package/src/resolution/scope.ts +58 -47
package/src/errors.ts CHANGED
@@ -212,6 +212,27 @@ export class MissingContainerContextError extends DiError {
212
212
  }
213
213
  }
214
214
 
215
+ /**
216
+ * A fluent chain was refined before a `to*()` call gave it a binding to refine.
217
+ *
218
+ * @remarks The builder types make this unreachable from TypeScript — `bind()` returns
219
+ * `BindToBuilder`, which exposes only `to*()`. It exists for JavaScript callers and for anyone who
220
+ * casts past the types, so the misuse fails loudly instead of mutating nothing.
221
+ *
222
+ * @since 0.5.0-canary.8
223
+ */
224
+ export class ChainNotRegisteredError extends DiError {
225
+ readonly code = "CHAIN_NOT_REGISTERED";
226
+ readonly tokenName: string;
227
+
228
+ constructor(tokenName: string) {
229
+ super(
230
+ `Cannot refine the binding for token '${tokenName}' before choosing a target. Call a to*() method first — for example .to(SomeClass), .toConstantValue(value) or .toDynamic(factory).`,
231
+ );
232
+ this.tokenName = tokenName;
233
+ }
234
+ }
235
+
215
236
  /**
216
237
  * @since 0.3.16-canary.0
217
238
  */
@@ -227,6 +248,23 @@ export class RebindUnboundTokenError extends DiError {
227
248
  }
228
249
  }
229
250
 
251
+ /**
252
+ * `toSelf()` on a token that is not a class, so there is nothing to construct.
253
+ *
254
+ * @since 0.5.0-canary.9
255
+ */
256
+ export class SelfBindingRequiresClassError extends DiError {
257
+ readonly code = "SELF_BINDING_REQUIRES_CLASS";
258
+ readonly tokenName: string;
259
+
260
+ constructor(tokenName: string) {
261
+ super(
262
+ `toSelf() needs the token to be the class it constructs, and '${tokenName}' is not a class. Use .to(SomeClass) to name the implementation, or bind the class itself with container.bind(SomeClass).toSelf().`,
263
+ );
264
+ this.tokenName = tokenName;
265
+ }
266
+ }
267
+
230
268
  /**
231
269
  * @since 0.3.16-canary.0
232
270
  */
package/src/index.ts CHANGED
@@ -36,7 +36,8 @@ export type {
36
36
  export { Container } from "#/container/container";
37
37
  export type { Container as ContainerInterface, ContainerStatic } from "#/container/container";
38
38
 
39
- export { effectiveBindingScope } from "#/resolution/binding-scope";
39
+ // `effectiveBindingScope` is deliberately absent: it reads a `Binding`, which is internal, and no
40
+ // public API hands one out. `BindingSnapshot.scope` and `GraphNode.scope` are the public answers.
40
41
  export { injectionSlotToResolveOptions, bindingSlotToResolveOptions } from "#/resolution/resolve-options";
41
42
 
42
43
  // Introspection types
@@ -85,6 +86,7 @@ export {
85
86
  AsyncDeactivationError,
86
87
  AsyncModuleLoadError,
87
88
  AsyncResolutionError,
89
+ ChainNotRegisteredError,
88
90
  CircularDependencyError,
89
91
  DiError,
90
92
  DisposedContainerError,
@@ -95,6 +97,7 @@ export {
95
97
  NoMatchingBindingError,
96
98
  RebindUnboundTokenError,
97
99
  ScopeViolationError,
100
+ SelfBindingRequiresClassError,
98
101
  SyncDisposalNotSupportedError,
99
102
  TokenNotBoundError,
100
103
  } from "#/errors";
@@ -60,10 +60,9 @@ export class Inspector {
60
60
  }
61
61
 
62
62
  inspect(): ContainerSnapshot {
63
- const snapshots = this.allBindingSnapshots();
64
63
  return {
65
- ownBindings: snapshots,
66
- cachedSingletonCount: this.#scope.getAllSingletons().size,
64
+ ownBindings: this.#registry.allBindings().map((binding) => this.#toSnapshot(binding)),
65
+ cachedSingletonCount: this.#scope.cachedSingletons().length,
67
66
  hasParent: this.#hasParent,
68
67
  isDisposed: this.#isDisposed(),
69
68
  };
@@ -109,12 +108,7 @@ export class Inspector {
109
108
  };
110
109
  }
111
110
 
112
- private allBindingSnapshots(): ReadonlyArray<BindingSnapshot> {
113
- return this.#registry.allBindings().map((binding) => this.#toSnapshot(binding));
114
- }
115
-
116
111
  #toSnapshot(binding: Binding): BindingSnapshot {
117
- const scope = effectiveBindingScope(binding);
118
112
  const slot: BindingSnapshot["slot"] =
119
113
  binding.slot.name !== undefined
120
114
  ? { name: binding.slot.name, tags: binding.slot.tags }
@@ -122,7 +116,7 @@ export class Inspector {
122
116
  return {
123
117
  tokenName: tokenName(binding.token),
124
118
  kind: binding.kind,
125
- scope,
119
+ scope: effectiveBindingScope(binding),
126
120
  slot,
127
121
  id: binding.id,
128
122
  };
@@ -12,13 +12,10 @@ export const LIFECYCLE_KEY: unique symbol = Symbol("di:lifecycle");
12
12
  export const INJECT_ACCESSOR_KEY: unique symbol = Symbol("di:inject-accessor");
13
13
 
14
14
  /**
15
- * The well-known symbol used by TC39 Stage 3 decorator transforms to store class metadata.
15
+ * The symbol TC39 Stage 3 decorator transforms store class metadata under.
16
16
  *
17
- * `Symbol.metadata` is defined natively once the runtime ships the full TC39 decorator
18
- * proposal. Until then (current Node.js / browsers), Babel and esbuild both fall back to
19
- * `Symbol.for("Symbol.metadata")` — a global-registry symbol with the same string key.
20
- * Resolving it here once keeps the reader and the decorator transforms in sync regardless
21
- * of which path is taken.
17
+ * @remarks Falls back to the global-registry symbol, which is what Babel and esbuild emit until
18
+ * a runtime ships `Symbol.metadata` natively.
22
19
  *
23
20
  * @since 0.3.16-canary.0
24
21
  */
package/src/registry.ts CHANGED
@@ -1,45 +1,7 @@
1
1
  import type { Binding } from "#/binding";
2
2
  import { bindingSlotEquals, bindingSlotToString } from "#/binding";
3
3
  import type { Token } from "#/token";
4
- import type { BindingIdentifier, Constructor, DependencyKey } from "#/types";
5
-
6
- // Superset of every binding kind's fields, used to normalize hidden classes below.
7
- type BindingFieldSuperset = Binding & {
8
- readonly scope?: unknown;
9
- readonly target?: unknown;
10
- readonly factory?: unknown;
11
- readonly deps?: unknown;
12
- readonly value?: unknown;
13
- readonly onActivation?: unknown;
14
- readonly onDeactivation?: unknown;
15
- };
16
-
17
- /**
18
- * Rebuilds a binding with every kind's fields in one fixed key order, so all stored
19
- * bindings share a single V8 hidden class. Mixed binding kinds otherwise turn the
20
- * resolver's hot property reads (kind/scope/factory/...) megamorphic, which costs
21
- * ~30% throughput in processes that exercise many kinds. The value-erasing cast is
22
- * safe: every declared field is copied verbatim and `kind` stays the discriminant.
23
- */
24
- function normalizeBindingShape(binding: Binding): Binding {
25
- const source = binding as BindingFieldSuperset;
26
- return {
27
- kind: source.kind,
28
- id: source.id,
29
- inFlight: false,
30
- frame: undefined,
31
- token: source.token,
32
- slot: source.slot,
33
- predicate: source.predicate,
34
- scope: source.scope,
35
- target: source.target,
36
- factory: source.factory,
37
- deps: source.deps,
38
- value: source.value,
39
- onActivation: source.onActivation,
40
- onDeactivation: source.onDeactivation,
41
- } as Binding;
42
- }
4
+ import type { BindingIdentifier, BindingTag, Constructor, DependencyKey } from "#/types";
43
5
 
44
6
  /**
45
7
  * @since 0.3.16-canary.0
@@ -51,31 +13,45 @@ export class BindingRegistry {
51
13
  readonly #bindings = new Map<DependencyKey, Array<Binding>>();
52
14
  // Fast lookup by binding ID
53
15
  readonly #byId = new Map<BindingIdentifier, Binding>();
54
- // Fast lookup for slot { name, tags: [] }
55
- readonly #simpleNamed = new Map<DependencyKey, Map<string, Binding>>();
16
+ // Fast lookup for slot { name, tags: [] } — unallocated until a named binding is registered.
17
+ #simpleNamed: Map<DependencyKey, Map<string, Binding>> | undefined;
56
18
  // Fast path for one default slot binding with no predicate
57
19
  readonly #fastDefault = new Map<DependencyKey, Binding>();
58
- // Fast lookup for slot { name: undefined, tags: [[key, value]] } with no predicate
59
- readonly #simpleTagged = new Map<DependencyKey, Map<string, Map<unknown, Binding>>>();
20
+ // Fast lookup for slot { name: undefined, tags: [[key, value]] } with no predicate — likewise
21
+ // unallocated until a tagged binding is registered.
22
+ #simpleTagged: Map<DependencyKey, Map<string, Map<unknown, Binding>>> | undefined;
60
23
 
61
24
  /** Monotonic version — increments on every mutation. */
62
25
  get version(): number {
63
26
  return this.#version;
64
27
  }
65
28
 
66
- /** Add or replace binding using slot-aware last-wins. Returns the displaced binding, if any. */
67
- add(uncommittedBinding: Binding): Binding | undefined {
29
+ /**
30
+ * Registers a mutation the indexes don't care about (a fluent chain refining scope or an
31
+ * activation hook in place), so version-stamped resolver caches still invalidate.
32
+ */
33
+ touch(): void {
34
+ this.#version += 1;
35
+ }
36
+
37
+ /**
38
+ * Add or replace binding using slot-aware last-wins. Returns the displaced binding, if any.
39
+ *
40
+ * @remarks The binding is stored by reference — it must come from `createBinding`, which is
41
+ * what guarantees the single hidden class the resolver's hot reads depend on.
42
+ */
43
+ add(binding: Binding): Binding | undefined {
68
44
  this.#version += 1;
69
- const binding = normalizeBindingShape(uncommittedBinding);
70
45
  const key = binding.token as DependencyKey;
71
- // ✓ TS6.0: Map.getOrInsert (ES2025) replaces the manual get+check+set upsert
46
+ // Eager, not computed: a bind is usually the token's first, so the fallback is usually the
47
+ // one that gets stored — and the computed form would add a call to allocating it anyway.
72
48
  const bindingsForToken = this.#bindings.getOrInsert(key, []);
73
49
 
74
50
  // Only apply last-wins for slot-based bindings (not predicate-only)
75
51
  let displacedBinding: Binding | undefined;
76
- if (!this.#isPurePredicateBinding(binding)) {
52
+ if (!isPurePredicateBinding(binding)) {
77
53
  const existingIndex = bindingsForToken.findIndex(
78
- (candidate) => !this.#isPurePredicateBinding(candidate) && bindingSlotEquals(candidate.slot, binding.slot),
54
+ (candidate) => !isPurePredicateBinding(candidate) && bindingSlotEquals(candidate.slot, binding.slot),
79
55
  );
80
56
  if (existingIndex !== -1) {
81
57
  displacedBinding = bindingsForToken[existingIndex]!;
@@ -100,8 +76,8 @@ export class BindingRegistry {
100
76
  const key = token as DependencyKey;
101
77
  const bindingsForToken = this.#bindings.get(key) ?? [];
102
78
  this.#bindings.delete(key);
103
- this.#simpleNamed.delete(key);
104
- this.#simpleTagged.delete(key);
79
+ this.#simpleNamed?.delete(key);
80
+ this.#simpleTagged?.delete(key);
105
81
  this.#fastDefault.delete(key);
106
82
  for (const binding of bindingsForToken) {
107
83
  this.#byId.delete(binding.id);
@@ -128,8 +104,8 @@ export class BindingRegistry {
128
104
  this.#deindexSimpleTaggedBinding(key, binding);
129
105
  if (bindingsForToken.length === 0) {
130
106
  this.#bindings.delete(key);
131
- this.#simpleNamed.delete(key);
132
- this.#simpleTagged.delete(key);
107
+ this.#simpleNamed?.delete(key);
108
+ this.#simpleTagged?.delete(key);
133
109
  this.#fastDefault.delete(key);
134
110
  } else {
135
111
  this.#refreshFastDefaultForToken(key);
@@ -170,19 +146,19 @@ export class BindingRegistry {
170
146
  const all = this.allBindings();
171
147
  this.#bindings.clear();
172
148
  this.#byId.clear();
173
- this.#simpleNamed.clear();
174
- this.#simpleTagged.clear();
149
+ this.#simpleNamed?.clear();
150
+ this.#simpleTagged?.clear();
175
151
  this.#fastDefault.clear();
176
152
  return all;
177
153
  }
178
154
 
179
155
  getSimpleNamed(token: Token<unknown> | Constructor, name: string): Binding | undefined {
180
- return this.#simpleNamed.get(token as DependencyKey)?.get(name);
156
+ return this.#simpleNamed?.get(token as DependencyKey)?.get(name);
181
157
  }
182
158
 
183
159
  getSimpleTagged(token: Token<unknown> | Constructor, tagKey: string, tagValue: unknown): Binding | undefined {
184
160
  return this.#simpleTagged
185
- .get(token as DependencyKey)
161
+ ?.get(token as DependencyKey)
186
162
  ?.get(tagKey)
187
163
  ?.get(tagValue);
188
164
  }
@@ -198,23 +174,23 @@ export class BindingRegistry {
198
174
  }
199
175
 
200
176
  #indexSimpleTaggedBinding(tokenKey: DependencyKey, binding: Binding): void {
201
- const slot = binding.slot;
202
- if (slot.name !== undefined || slot.tags.length !== 1 || binding.predicate !== undefined) {
177
+ const tag = simpleTagOf(binding);
178
+ if (tag === undefined) {
203
179
  return;
204
180
  }
205
- const [tagKey, tagValue] = slot.tags[0]!;
206
- const byTagKey = this.#simpleTagged.getOrInsert(tokenKey, new Map<string, Map<unknown, Binding>>());
181
+ const [tagKey, tagValue] = tag;
182
+ const byTagKey = (this.#simpleTagged ??= new Map()).getOrInsert(tokenKey, new Map<string, Map<unknown, Binding>>());
207
183
  const byTagValue = byTagKey.getOrInsert(tagKey, new Map<unknown, Binding>());
208
184
  byTagValue.set(tagValue, binding);
209
185
  }
210
186
 
211
187
  #deindexSimpleTaggedBinding(tokenKey: DependencyKey, binding: Binding): void {
212
- const slot = binding.slot;
213
- if (slot.name !== undefined || slot.tags.length !== 1 || binding.predicate !== undefined) {
188
+ const tag = simpleTagOf(binding);
189
+ if (tag === undefined) {
214
190
  return;
215
191
  }
216
- const [tagKey, tagValue] = slot.tags[0]!;
217
- const byTagKey = this.#simpleTagged.get(tokenKey);
192
+ const [tagKey, tagValue] = tag;
193
+ const byTagKey = this.#simpleTagged?.get(tokenKey);
218
194
  if (byTagKey === undefined) {
219
195
  return;
220
196
  }
@@ -228,59 +204,79 @@ export class BindingRegistry {
228
204
  if (byTagValue.size === 0) {
229
205
  byTagKey.delete(tagKey);
230
206
  if (byTagKey.size === 0) {
231
- this.#simpleTagged.delete(tokenKey);
207
+ this.#simpleTagged!.delete(tokenKey);
232
208
  }
233
209
  }
234
210
  }
235
211
  }
236
212
 
237
- #isPurePredicateBinding(binding: Binding): boolean {
238
- const slot = binding.slot;
239
- const hasPredicate = binding.predicate !== undefined;
240
- const hasConstraint = slot.name !== undefined || slot.tags.length > 0;
241
- // Pure predicate = has predicate but no slot constraint (name/tags)
242
- return hasPredicate && !hasConstraint;
243
- }
244
-
245
213
  #indexSimpleNamedBinding(tokenKey: DependencyKey, binding: Binding): void {
246
- const slot = binding.slot;
247
- if (slot.name === undefined || slot.tags.length > 0) {
214
+ const name = simpleNameOf(binding);
215
+ if (name === undefined) {
248
216
  return;
249
217
  }
250
- const bindingsByName = this.#simpleNamed.getOrInsert(tokenKey, new Map<string, Binding>());
251
- bindingsByName.set(slot.name, binding);
218
+ const bindingsByName = (this.#simpleNamed ??= new Map()).getOrInsert(tokenKey, new Map<string, Binding>());
219
+ bindingsByName.set(name, binding);
252
220
  }
253
221
 
254
222
  #deindexSimpleNamedBinding(tokenKey: DependencyKey, binding: Binding): void {
255
- const slot = binding.slot;
256
- if (slot.name === undefined || slot.tags.length > 0) {
223
+ const name = simpleNameOf(binding);
224
+ if (name === undefined) {
257
225
  return;
258
226
  }
259
- const bindingsByName = this.#simpleNamed.get(tokenKey);
227
+ const bindingsByName = this.#simpleNamed?.get(tokenKey);
260
228
  if (bindingsByName === undefined) {
261
229
  return;
262
230
  }
263
- const currentBinding = bindingsByName.get(slot.name);
264
- if (currentBinding?.id === binding.id) {
265
- bindingsByName.delete(slot.name);
231
+ if (bindingsByName.get(name)?.id === binding.id) {
232
+ bindingsByName.delete(name);
266
233
  if (bindingsByName.size === 0) {
267
- this.#simpleNamed.delete(tokenKey);
234
+ this.#simpleNamed!.delete(tokenKey);
268
235
  }
269
236
  }
270
237
  }
271
238
 
272
239
  #refreshFastDefaultForToken(tokenKey: DependencyKey): void {
273
240
  const bindingsForToken = this.#bindings.get(tokenKey);
274
- if (bindingsForToken === undefined || bindingsForToken.length !== 1) {
275
- this.#fastDefault.delete(tokenKey);
276
- return;
277
- }
278
- const onlyBinding = bindingsForToken[0]!;
279
- const isDefaultSlot = onlyBinding.slot.name === undefined && onlyBinding.slot.tags.length === 0;
280
- if (!isDefaultSlot || onlyBinding.predicate !== undefined) {
281
- this.#fastDefault.delete(tokenKey);
241
+ const onlyBinding = bindingsForToken?.length === 1 ? bindingsForToken[0]! : undefined;
242
+ if (onlyBinding !== undefined && isDefaultSlotBinding(onlyBinding)) {
243
+ this.#fastDefault.set(tokenKey, onlyBinding);
282
244
  return;
283
245
  }
284
- this.#fastDefault.set(tokenKey, onlyBinding);
246
+ this.#fastDefault.delete(tokenKey);
285
247
  }
248
+
249
+ /** Whether the deferred table behind `#simpleNamed` has had to be built. */
250
+ get isBuilt(): boolean {
251
+ return this.#simpleNamed !== undefined;
252
+ }
253
+ }
254
+
255
+ /** The name a binding is indexed under, or `undefined` when its slot is more than a plain name. */
256
+ function simpleNameOf(binding: Binding): string | undefined {
257
+ const { name, tags } = binding.slot;
258
+ return name !== undefined && tags.length === 0 ? name : undefined;
259
+ }
260
+
261
+ /**
262
+ * The tag a binding is indexed under, or `undefined` when its slot is more than one plain tag.
263
+ *
264
+ * @remarks A predicate is excluded here — unlike the name index — because the tag index is read
265
+ * without a re-check, so an indexed hit must be unconditional.
266
+ */
267
+ function simpleTagOf(binding: Binding): BindingTag | undefined {
268
+ const { name, tags } = binding.slot;
269
+ return name === undefined && tags.length === 1 && binding.predicate === undefined ? tags[0] : undefined;
270
+ }
271
+
272
+ /** A binding nothing has to be matched against: the default slot, no predicate. */
273
+ function isDefaultSlotBinding(binding: Binding): boolean {
274
+ const { name, tags } = binding.slot;
275
+ return name === undefined && tags.length === 0 && binding.predicate === undefined;
276
+ }
277
+
278
+ /** A predicate with no slot constraint: last-wins does not apply to it. */
279
+ function isPurePredicateBinding(binding: Binding): boolean {
280
+ const { name, tags } = binding.slot;
281
+ return binding.predicate !== undefined && name === undefined && tags.length === 0;
286
282
  }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Per binding: does resolving it have to go through the activation pipeline?
3
+ *
4
+ * @remarks Versioned on the lifecycle manager plus the own registry, since `onActivation` can be
5
+ * registered at any time and a rebind mints binding ids the memo must not keep forever.
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";
12
+
13
+ /**
14
+ * @since 0.5.0-canary.8
15
+ */
16
+ export class ActivationNeedCache {
17
+ readonly #needByBindingId = new Map<BindingIdentifier, boolean>();
18
+ #version = -1;
19
+ readonly #lifecycle: LifecycleManager;
20
+ readonly #classes: ClassIntrospector;
21
+ readonly #registry: BindingRegistry;
22
+
23
+ constructor(lifecycle: LifecycleManager, classes: ClassIntrospector, registry: BindingRegistry) {
24
+ this.#lifecycle = lifecycle;
25
+ this.#classes = classes;
26
+ this.#registry = registry;
27
+ }
28
+
29
+ needsActivation<const Value>(binding: Binding<Value>): boolean {
30
+ // The chain writes a binding's own hook in place with no version anything here can see, so it
31
+ // is read fresh on every call; the memo covers only container hooks and lifecycle metadata.
32
+ if (binding.kind !== "alias" && binding.onActivation !== undefined) {
33
+ return true;
34
+ }
35
+ const lifecycleVersion = this.#lifecycle.activationVersion;
36
+ // No hooks registered anywhere and none on the binding: only classes can still surprise us,
37
+ // via a @postConstruct we have not looked for yet.
38
+ if (lifecycleVersion === 0 && binding.kind !== "class" && binding.kind !== "alias") {
39
+ return false;
40
+ }
41
+ // The registry version evicts entries for binding ids a rebind has retired.
42
+ const version = lifecycleVersion + this.#registry.version;
43
+ if (this.#version !== version) {
44
+ this.#needByBindingId.clear();
45
+ this.#version = version;
46
+ }
47
+ const cached = this.#needByBindingId.get(binding.id);
48
+ if (cached !== undefined) {
49
+ return cached;
50
+ }
51
+ const needsActivation =
52
+ binding.kind === "class" ? this.#classNeedsActivation(binding) : this.#nonClassNeedsActivation(binding);
53
+ this.#needByBindingId.set(binding.id, needsActivation);
54
+ return needsActivation;
55
+ }
56
+
57
+ /**
58
+ * Settles a class binding's answer once its lifecycle metadata has actually been read, which
59
+ * only happens on the first instantiation.
60
+ *
61
+ * @returns the answer to use for this resolve — possibly now `false` where it was a
62
+ * conservative `true`.
63
+ */
64
+ refreshAfterFirstInstantiation<Value>(binding: Binding<Value>, needsActivation: boolean): boolean {
65
+ if (binding.kind !== "class" || this.#classes.knownPostConstruct(binding.target) !== undefined) {
66
+ return needsActivation;
67
+ }
68
+ this.#classes.discoverPostConstruct(binding.target);
69
+ this.#needByBindingId.delete(binding.id);
70
+ return this.needsActivation(binding);
71
+ }
72
+
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 {
75
+ if (this.#lifecycle.hasActivationHandlers(binding.token)) {
76
+ return true;
77
+ }
78
+ // Unknown lifecycle metadata: activate once so the first instantiation can settle it.
79
+ return this.#classes.knownPostConstruct(binding.target) !== false;
80
+ }
81
+
82
+ #nonClassNeedsActivation<const Value>(binding: Binding<Value>): boolean {
83
+ return this.#lifecycle.hasActivationHandlers(binding.token);
84
+ }
85
+ }
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Options-less token → terminal binding, memoized per container chain.
3
+ *
4
+ * @see `ARCHITECTURE.md` — why these caches form their own parent chain.
5
+ */
6
+ import type { Binding } from "#/binding";
7
+ import type { BindingRegistry } from "#/registry";
8
+ import type { Token } from "#/token";
9
+ import type { Constructor } from "#/types";
10
+
11
+ /**
12
+ * A token's terminal binding with alias hops already folded, plus the container that owns it.
13
+ *
14
+ * @typeParam Owner - the resolver type, kept generic so this module stays free of resolver internals
15
+ *
16
+ * @since 0.5.0-canary.8
17
+ */
18
+ export interface DefaultLookupEntry<Owner> {
19
+ readonly binding: Binding;
20
+ readonly owner: Owner;
21
+ }
22
+
23
+ /**
24
+ * Alias folding gives up past this many hops and defers to the full resolve loop, whose
25
+ * Set-based traversal detects genuine cycles exactly rather than by an arbitrary cap.
26
+ *
27
+ * @since 0.5.0-canary.8
28
+ */
29
+ export const ALIAS_HOP_LIMIT = 32;
30
+
31
+ /**
32
+ * @since 0.5.0-canary.8
33
+ */
34
+ const newNameToEntryMap = <Owner>(): Map<string, DefaultLookupEntry<Owner> | null> => new Map();
35
+
36
+ /**
37
+ * @since 0.5.0-canary.9
38
+ */
39
+ export class BindingLookupCache<Owner> {
40
+ readonly #byToken = new Map<Token<unknown> | Constructor, DefaultLookupEntry<Owner> | null>();
41
+ #version = -1;
42
+ // One entry in front of the map: the two shapes that reach here — an alias, and a token owned by
43
+ // a parent — are both resolved in a loop over the same token. `null` is a real answer, so absence
44
+ // is tracked by the token slot rather than by the entry.
45
+ #lastToken: Token<unknown> | Constructor | undefined;
46
+ #lastEntry: DefaultLookupEntry<Owner> | null = null;
47
+ readonly #byTokenAndName = new Map<Token<unknown> | Constructor, Map<string, DefaultLookupEntry<Owner> | null>>();
48
+ #namedVersion = -1;
49
+
50
+ readonly #registry: BindingRegistry;
51
+ readonly #owner: Owner;
52
+ readonly #parent: BindingLookupCache<Owner> | undefined;
53
+
54
+ constructor(registry: BindingRegistry, owner: Owner, parent: BindingLookupCache<Owner> | undefined) {
55
+ this.#registry = registry;
56
+ this.#owner = owner;
57
+ this.#parent = parent;
58
+ }
59
+
60
+ /** Summed registry versions of this cache's whole chain — the memo stamp. */
61
+ chainVersion(): number {
62
+ let version = this.#registry.version;
63
+ for (let cache = this.#parent; cache !== undefined; cache = cache.#parent) {
64
+ version += cache.#registry.version;
65
+ }
66
+ return version;
67
+ }
68
+
69
+ /** `null` when the token's shape needs the full selection path. */
70
+ defaultEntry(token: Token<unknown> | Constructor): DefaultLookupEntry<Owner> | null {
71
+ const version = this.chainVersion();
72
+ if (version !== this.#version) {
73
+ this.#byToken.clear();
74
+ this.#version = version;
75
+ this.#lastToken = undefined;
76
+ } else if (token === this.#lastToken) {
77
+ return this.#lastEntry;
78
+ }
79
+ let entry = this.#byToken.get(token);
80
+ if (entry === undefined) {
81
+ entry = this.#foldAliases(token);
82
+ this.#byToken.set(token, entry);
83
+ }
84
+ this.#lastToken = token;
85
+ this.#lastEntry = entry;
86
+ return entry;
87
+ }
88
+
89
+ /** `null` when the name's shape needs the full selection path. */
90
+ namedEntry(token: Token<unknown> | Constructor, name: string): DefaultLookupEntry<Owner> | null {
91
+ const version = this.chainVersion();
92
+ if (version !== this.#namedVersion) {
93
+ this.#byTokenAndName.clear();
94
+ this.#namedVersion = version;
95
+ }
96
+ // Computed, not eager: this runs on every named resolve, and `getOrInsert(token, new Map())`
97
+ // would allocate a Map per call only to discard it on the hit that follows.
98
+ const byName = this.#byTokenAndName.getOrInsertComputed(token, newNameToEntryMap);
99
+ let entry = byName.get(name);
100
+ if (entry === undefined) {
101
+ entry = this.#findNamedInChain(token, name);
102
+ byName.set(name, entry);
103
+ }
104
+ return entry;
105
+ }
106
+
107
+ #foldAliases(token: Token<unknown> | Constructor): DefaultLookupEntry<Owner> | null {
108
+ let current = token;
109
+ for (let hop = 0; hop < ALIAS_HOP_LIMIT; hop += 1) {
110
+ const entry = this.#findDefaultInChain(current);
111
+ if (entry === null) {
112
+ return null;
113
+ }
114
+ if (entry.binding.kind !== "alias") {
115
+ return entry;
116
+ }
117
+ current = entry.binding.target;
118
+ }
119
+ return null;
120
+ }
121
+
122
+ #findDefaultInChain(token: Token<unknown> | Constructor): DefaultLookupEntry<Owner> | null {
123
+ const fast = this.#registry.getFastDefault(token);
124
+ if (fast !== undefined) {
125
+ return { binding: fast, owner: this.#owner };
126
+ }
127
+ // A level with non-fast bindings (multi-slot / predicate) needs full selection — bail.
128
+ if (this.#registry.has(token)) {
129
+ return null;
130
+ }
131
+ return this.#parent === undefined ? null : this.#parent.#findDefaultInChain(token);
132
+ }
133
+
134
+ #findNamedInChain(token: Token<unknown> | Constructor, name: string): DefaultLookupEntry<Owner> | null {
135
+ const named = this.#registry.getSimpleNamed(token, name);
136
+ if (named !== undefined) {
137
+ // Predicates need a live context; aliases carry options through the full path.
138
+ if (named.predicate !== undefined || named.kind === "alias") {
139
+ return null;
140
+ }
141
+ return { binding: named, owner: this.#owner };
142
+ }
143
+ if (this.#registry.has(token)) {
144
+ return null;
145
+ }
146
+ return this.#parent === undefined ? null : this.#parent.#findNamedInChain(token, name);
147
+ }
148
+ }