@codefast/di 0.5.0-canary.6 → 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 (195) hide show
  1. package/CHANGELOG.md +123 -0
  2. package/README.md +15 -4
  3. package/dist/binding.d.ts +78 -19
  4. package/dist/binding.d.ts.map +1 -1
  5. package/dist/binding.js +46 -2
  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 +56 -0
  10. package/dist/container/binding-builders.d.ts.map +1 -0
  11. package/dist/container/binding-builders.js +196 -0
  12. package/dist/container/binding-builders.js.map +1 -0
  13. package/dist/{container.d.ts → container/container.d.ts} +2 -2
  14. package/dist/container/container.d.ts.map +1 -0
  15. package/dist/container/container.js +624 -0
  16. package/dist/container/container.js.map +1 -0
  17. package/dist/decorators/inject.d.ts +10 -1
  18. package/dist/decorators/inject.d.ts.map +1 -1
  19. package/dist/decorators/inject.js +2 -2
  20. package/dist/decorators/inject.js.map +1 -1
  21. package/dist/decorators/injectable.js +3 -3
  22. package/dist/decorators/injectable.js.map +1 -1
  23. package/dist/errors.d.ts +14 -0
  24. package/dist/errors.d.ts.map +1 -1
  25. package/dist/errors.js +17 -0
  26. package/dist/errors.js.map +1 -1
  27. package/dist/index.d.ts +13 -8
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +9 -5
  30. package/dist/index.js.map +1 -1
  31. package/dist/introspection/dependency-graph.d.ts.map +1 -0
  32. package/dist/{dependency-graph.js → introspection/dependency-graph.js} +1 -1
  33. package/dist/introspection/dependency-graph.js.map +1 -0
  34. package/dist/{graph-adapters → introspection/graph-adapters}/cytoscape.d.ts +1 -1
  35. package/dist/introspection/graph-adapters/cytoscape.d.ts.map +1 -0
  36. package/dist/introspection/graph-adapters/cytoscape.js.map +1 -0
  37. package/dist/introspection/graph-adapters/dot.d.ts +6 -0
  38. package/dist/introspection/graph-adapters/dot.d.ts.map +1 -0
  39. package/dist/introspection/graph-adapters/dot.js.map +1 -0
  40. package/dist/{graph-adapters → introspection/graph-adapters}/reactflow.d.ts +2 -2
  41. package/dist/introspection/graph-adapters/reactflow.d.ts.map +1 -0
  42. package/dist/{graph-adapters → introspection/graph-adapters}/reactflow.js +11 -1
  43. package/dist/introspection/graph-adapters/reactflow.js.map +1 -0
  44. package/dist/{inspector.d.ts → introspection/inspector.d.ts} +3 -8
  45. package/dist/introspection/inspector.d.ts.map +1 -0
  46. package/dist/{inspector.js → introspection/inspector.js} +23 -23
  47. package/dist/introspection/inspector.js.map +1 -0
  48. package/dist/metadata/metadata-keys.d.ts +3 -6
  49. package/dist/metadata/metadata-keys.d.ts.map +1 -1
  50. package/dist/metadata/metadata-keys.js +3 -6
  51. package/dist/metadata/metadata-keys.js.map +1 -1
  52. package/dist/metadata/symbol-metadata-reader.d.ts +1 -1
  53. package/dist/metadata/symbol-metadata-reader.d.ts.map +1 -1
  54. package/dist/metadata/symbol-metadata-reader.js +4 -4
  55. package/dist/metadata/symbol-metadata-reader.js.map +1 -1
  56. package/dist/module.d.ts +9 -2
  57. package/dist/module.d.ts.map +1 -1
  58. package/dist/module.js +9 -2
  59. package/dist/module.js.map +1 -1
  60. package/dist/registry.d.ts +17 -13
  61. package/dist/registry.d.ts.map +1 -1
  62. package/dist/registry.js +94 -63
  63. package/dist/registry.js.map +1 -1
  64. package/dist/resolution/activation-need.d.ts +25 -0
  65. package/dist/resolution/activation-need.d.ts.map +1 -0
  66. package/dist/resolution/activation-need.js +64 -0
  67. package/dist/resolution/activation-need.js.map +1 -0
  68. package/dist/resolution/binding-lookup-cache.d.ts +41 -0
  69. package/dist/resolution/binding-lookup-cache.d.ts.map +1 -0
  70. package/dist/resolution/binding-lookup-cache.js +102 -0
  71. package/dist/resolution/binding-lookup-cache.js.map +1 -0
  72. package/dist/resolution/binding-scope.d.ts.map +1 -0
  73. package/dist/resolution/binding-scope.js.map +1 -0
  74. package/dist/resolution/binding-select.d.ts.map +1 -0
  75. package/dist/{binding-select.js → resolution/binding-select.js} +17 -7
  76. package/dist/resolution/binding-select.js.map +1 -0
  77. package/dist/resolution/class-introspector.d.ts +27 -0
  78. package/dist/resolution/class-introspector.d.ts.map +1 -0
  79. package/dist/resolution/class-introspector.js +60 -0
  80. package/dist/resolution/class-introspector.js.map +1 -0
  81. package/dist/resolution/constraints.d.ts.map +1 -0
  82. package/dist/resolution/constraints.js.map +1 -0
  83. package/dist/resolution/diagnostics.d.ts +41 -0
  84. package/dist/resolution/diagnostics.d.ts.map +1 -0
  85. package/dist/resolution/diagnostics.js +18 -0
  86. package/dist/resolution/diagnostics.js.map +1 -0
  87. package/dist/{environment.d.ts → resolution/environment.d.ts} +24 -7
  88. package/dist/resolution/environment.d.ts.map +1 -0
  89. package/dist/resolution/environment.js +126 -0
  90. package/dist/resolution/environment.js.map +1 -0
  91. package/dist/resolution/instantiation-plan.d.ts +67 -0
  92. package/dist/resolution/instantiation-plan.d.ts.map +1 -0
  93. package/dist/resolution/instantiation-plan.js +183 -0
  94. package/dist/resolution/instantiation-plan.js.map +1 -0
  95. package/dist/{lifecycle.d.ts → resolution/lifecycle.d.ts} +5 -3
  96. package/dist/resolution/lifecycle.d.ts.map +1 -0
  97. package/dist/{lifecycle.js → resolution/lifecycle.js} +23 -14
  98. package/dist/resolution/lifecycle.js.map +1 -0
  99. package/dist/resolution/resolution-path.d.ts +22 -0
  100. package/dist/resolution/resolution-path.d.ts.map +1 -0
  101. package/dist/resolution/resolution-path.js +40 -0
  102. package/dist/resolution/resolution-path.js.map +1 -0
  103. package/dist/resolution/resolve-options.d.ts.map +1 -0
  104. package/dist/resolution/resolve-options.js.map +1 -0
  105. package/dist/resolution/resolver.d.ts +40 -0
  106. package/dist/resolution/resolver.d.ts.map +1 -0
  107. package/dist/resolution/resolver.js +1108 -0
  108. package/dist/resolution/resolver.js.map +1 -0
  109. package/dist/{scope.d.ts → resolution/scope.d.ts} +8 -9
  110. package/dist/resolution/scope.d.ts.map +1 -0
  111. package/dist/resolution/scope.js +80 -0
  112. package/dist/resolution/scope.js.map +1 -0
  113. package/dist/token.d.ts.map +1 -1
  114. package/dist/token.js +0 -3
  115. package/dist/token.js.map +1 -1
  116. package/package.json +19 -94
  117. package/src/binding.ts +137 -22
  118. package/src/constructor-type.ts +4 -5
  119. package/src/container/binding-builders.ts +294 -0
  120. package/src/container/container.ts +825 -0
  121. package/src/decorators/inject.ts +15 -4
  122. package/src/decorators/injectable.ts +3 -3
  123. package/src/errors.ts +21 -0
  124. package/src/index.ts +15 -7
  125. package/src/{dependency-graph.ts → introspection/dependency-graph.ts} +1 -1
  126. package/src/{graph-adapters → introspection/graph-adapters}/cytoscape.ts +1 -1
  127. package/src/{graph-adapters → introspection/graph-adapters}/dot.ts +1 -1
  128. package/src/{graph-adapters → introspection/graph-adapters}/reactflow.ts +13 -2
  129. package/src/{inspector.ts → introspection/inspector.ts} +26 -21
  130. package/src/metadata/metadata-keys.ts +3 -6
  131. package/src/metadata/symbol-metadata-reader.ts +4 -4
  132. package/src/module.ts +14 -6
  133. package/src/registry.ts +97 -64
  134. package/src/resolution/activation-need.ts +81 -0
  135. package/src/resolution/binding-lookup-cache.ts +132 -0
  136. package/src/{binding-select.ts → resolution/binding-select.ts} +17 -7
  137. package/src/resolution/class-introspector.ts +74 -0
  138. package/src/resolution/diagnostics.ts +43 -0
  139. package/src/{environment.ts → resolution/environment.ts} +65 -35
  140. package/src/resolution/instantiation-plan.ts +292 -0
  141. package/src/{lifecycle.ts → resolution/lifecycle.ts} +26 -14
  142. package/src/resolution/resolution-path.ts +62 -0
  143. package/src/resolution/resolver.ts +1526 -0
  144. package/src/resolution/scope.ts +95 -0
  145. package/src/token.ts +0 -3
  146. package/dist/binding-scope.d.ts.map +0 -1
  147. package/dist/binding-scope.js.map +0 -1
  148. package/dist/binding-select.d.ts.map +0 -1
  149. package/dist/binding-select.js.map +0 -1
  150. package/dist/constraints.d.ts.map +0 -1
  151. package/dist/constraints.js.map +0 -1
  152. package/dist/container.d.ts.map +0 -1
  153. package/dist/container.js +0 -853
  154. package/dist/container.js.map +0 -1
  155. package/dist/dependency-graph.d.ts.map +0 -1
  156. package/dist/dependency-graph.js.map +0 -1
  157. package/dist/environment.d.ts.map +0 -1
  158. package/dist/environment.js +0 -100
  159. package/dist/environment.js.map +0 -1
  160. package/dist/graph-adapters/cytoscape.d.ts.map +0 -1
  161. package/dist/graph-adapters/cytoscape.js.map +0 -1
  162. package/dist/graph-adapters/dot.d.ts +0 -6
  163. package/dist/graph-adapters/dot.d.ts.map +0 -1
  164. package/dist/graph-adapters/dot.js.map +0 -1
  165. package/dist/graph-adapters/reactflow.d.ts.map +0 -1
  166. package/dist/graph-adapters/reactflow.js.map +0 -1
  167. package/dist/inspector.d.ts.map +0 -1
  168. package/dist/inspector.js.map +0 -1
  169. package/dist/lifecycle.d.ts.map +0 -1
  170. package/dist/lifecycle.js.map +0 -1
  171. package/dist/resolve-options.d.ts.map +0 -1
  172. package/dist/resolve-options.js.map +0 -1
  173. package/dist/resolver.d.ts +0 -90
  174. package/dist/resolver.d.ts.map +0 -1
  175. package/dist/resolver.js +0 -1208
  176. package/dist/resolver.js.map +0 -1
  177. package/dist/scope.d.ts.map +0 -1
  178. package/dist/scope.js +0 -61
  179. package/dist/scope.js.map +0 -1
  180. package/src/container.ts +0 -1141
  181. package/src/resolver.ts +0 -1626
  182. package/src/scope.ts +0 -77
  183. /package/dist/{dependency-graph.d.ts → introspection/dependency-graph.d.ts} +0 -0
  184. /package/dist/{graph-adapters → introspection/graph-adapters}/cytoscape.js +0 -0
  185. /package/dist/{graph-adapters → introspection/graph-adapters}/dot.js +0 -0
  186. /package/dist/{binding-scope.d.ts → resolution/binding-scope.d.ts} +0 -0
  187. /package/dist/{binding-scope.js → resolution/binding-scope.js} +0 -0
  188. /package/dist/{binding-select.d.ts → resolution/binding-select.d.ts} +0 -0
  189. /package/dist/{constraints.d.ts → resolution/constraints.d.ts} +0 -0
  190. /package/dist/{constraints.js → resolution/constraints.js} +0 -0
  191. /package/dist/{resolve-options.d.ts → resolution/resolve-options.d.ts} +0 -0
  192. /package/dist/{resolve-options.js → resolution/resolve-options.js} +0 -0
  193. /package/src/{binding-scope.ts → resolution/binding-scope.ts} +0 -0
  194. /package/src/{constraints.ts → resolution/constraints.ts} +0 -0
  195. /package/src/{resolve-options.ts → resolution/resolve-options.ts} +0 -0
package/src/registry.ts CHANGED
@@ -7,79 +7,107 @@ import type { BindingIdentifier, Constructor, DependencyKey } from "#/types";
7
7
  * @since 0.3.16-canary.0
8
8
  */
9
9
  export class BindingRegistry {
10
+ // Monotonic mutation counter — lets resolvers version-stamp lookup caches across a container chain.
11
+ #version = 0;
10
12
  // Map from token key -> array of bindings (order matters for last-wins)
11
- private readonly _bindings = new Map<DependencyKey, Array<Binding>>();
13
+ readonly #bindings = new Map<DependencyKey, Array<Binding>>();
12
14
  // Fast lookup by binding ID
13
- private readonly _byId = new Map<BindingIdentifier, Binding>();
14
- // Fast lookup for slot { name, tags: [] }
15
- private readonly _simpleNamed = new Map<DependencyKey, Map<string, Binding>>();
15
+ readonly #byId = new Map<BindingIdentifier, Binding>();
16
+ // Fast lookup for slot { name, tags: [] } — unallocated until a named binding is registered.
17
+ #simpleNamed: Map<DependencyKey, Map<string, Binding>> | undefined;
16
18
  // Fast path for one default slot binding with no predicate
17
- private readonly _fastDefault = new Map<DependencyKey, Binding>();
18
- // Fast lookup for slot { name: undefined, tags: [[key, value]] } with no predicate
19
- private readonly _simpleTagged = new Map<DependencyKey, Map<string, Map<unknown, Binding>>>();
19
+ readonly #fastDefault = new Map<DependencyKey, 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;
20
23
 
21
- /** Add or replace binding using slot-aware last-wins. */
22
- add(binding: Binding): void {
24
+ /** Monotonic version — increments on every mutation. */
25
+ get version(): number {
26
+ return this.#version;
27
+ }
28
+
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 {
44
+ this.#version += 1;
23
45
  const key = binding.token as DependencyKey;
24
46
  // ✓ TS6.0: Map.getOrInsert (ES2025) replaces the manual get+check+set upsert
25
- const bindingsForToken = this._bindings.getOrInsert(key, []);
47
+ const bindingsForToken = this.#bindings.getOrInsert(key, []);
26
48
 
27
49
  // Only apply last-wins for slot-based bindings (not predicate-only)
28
- if (!this._isPurePredicateBinding(binding)) {
50
+ let displacedBinding: Binding | undefined;
51
+ if (!this.#isPurePredicateBinding(binding)) {
29
52
  const existingIndex = bindingsForToken.findIndex(
30
- (candidate) => !this._isPurePredicateBinding(candidate) && bindingSlotEquals(candidate.slot, binding.slot),
53
+ (candidate) => !this.#isPurePredicateBinding(candidate) && bindingSlotEquals(candidate.slot, binding.slot),
31
54
  );
32
55
  if (existingIndex !== -1) {
33
- const replacedBinding = bindingsForToken[existingIndex]!;
34
- this._byId.delete(replacedBinding.id);
56
+ displacedBinding = bindingsForToken[existingIndex]!;
57
+ this.#byId.delete(displacedBinding.id);
35
58
  bindingsForToken.splice(existingIndex, 1);
59
+ this.#deindexSimpleNamedBinding(key, displacedBinding);
60
+ this.#deindexSimpleTaggedBinding(key, displacedBinding);
36
61
  }
37
62
  }
38
63
 
39
64
  bindingsForToken.push(binding);
40
- this._byId.set(binding.id, binding);
41
- this._indexSimpleNamedBinding(key, binding);
42
- this._indexSimpleTaggedBinding(key, binding);
43
- this._refreshFastDefaultForToken(key);
65
+ this.#byId.set(binding.id, binding);
66
+ this.#indexSimpleNamedBinding(key, binding);
67
+ this.#indexSimpleTaggedBinding(key, binding);
68
+ this.#refreshFastDefaultForToken(key);
69
+ return displacedBinding;
44
70
  }
45
71
 
46
72
  /** Remove all bindings for a token. Returns removed bindings. */
47
73
  removeByToken(token: Token<unknown> | Constructor): Array<Binding> {
74
+ this.#version += 1;
48
75
  const key = token as DependencyKey;
49
- const bindingsForToken = this._bindings.get(key) ?? [];
50
- this._bindings.delete(key);
51
- this._simpleNamed.delete(key);
52
- this._simpleTagged.delete(key);
53
- this._fastDefault.delete(key);
76
+ const bindingsForToken = this.#bindings.get(key) ?? [];
77
+ this.#bindings.delete(key);
78
+ this.#simpleNamed?.delete(key);
79
+ this.#simpleTagged?.delete(key);
80
+ this.#fastDefault.delete(key);
54
81
  for (const binding of bindingsForToken) {
55
- this._byId.delete(binding.id);
82
+ this.#byId.delete(binding.id);
56
83
  }
57
84
  return bindingsForToken;
58
85
  }
59
86
 
60
87
  /** Remove a specific binding by ID. Returns the removed binding or undefined. */
61
88
  removeById(id: BindingIdentifier): Binding | undefined {
62
- const binding = this._byId.get(id);
89
+ const binding = this.#byId.get(id);
63
90
  if (binding === undefined) {
64
91
  return undefined;
65
92
  }
66
- this._byId.delete(id);
93
+ this.#version += 1;
94
+ this.#byId.delete(id);
67
95
  const key = binding.token as DependencyKey;
68
- const bindingsForToken = this._bindings.get(key);
96
+ const bindingsForToken = this.#bindings.get(key);
69
97
  if (bindingsForToken !== undefined) {
70
98
  const bindingIndex = bindingsForToken.findIndex((candidate) => candidate.id === id);
71
99
  if (bindingIndex !== -1) {
72
100
  bindingsForToken.splice(bindingIndex, 1);
73
101
  }
74
- this._deindexSimpleNamedBinding(key, binding);
75
- this._deindexSimpleTaggedBinding(key, binding);
102
+ this.#deindexSimpleNamedBinding(key, binding);
103
+ this.#deindexSimpleTaggedBinding(key, binding);
76
104
  if (bindingsForToken.length === 0) {
77
- this._bindings.delete(key);
78
- this._simpleNamed.delete(key);
79
- this._simpleTagged.delete(key);
80
- this._fastDefault.delete(key);
105
+ this.#bindings.delete(key);
106
+ this.#simpleNamed?.delete(key);
107
+ this.#simpleTagged?.delete(key);
108
+ this.#fastDefault.delete(key);
81
109
  } else {
82
- this._refreshFastDefaultForToken(key);
110
+ this.#refreshFastDefaultForToken(key);
83
111
  }
84
112
  }
85
113
  return binding;
@@ -87,25 +115,25 @@ export class BindingRegistry {
87
115
 
88
116
  /** Get all bindings for a token. */
89
117
  getAll(token: Token<unknown> | Constructor): ReadonlyArray<Binding> {
90
- return this._bindings.get(token as DependencyKey) ?? [];
118
+ return this.#bindings.get(token as DependencyKey) ?? [];
91
119
  }
92
120
 
93
121
  /** Get binding by ID. */
94
122
  getById(id: BindingIdentifier): Binding | undefined {
95
- return this._byId.get(id);
123
+ return this.#byId.get(id);
96
124
  }
97
125
 
98
126
  /** Check if any binding exists for token. */
99
127
  has(token: Token<unknown> | Constructor): boolean {
100
128
  const key = token as DependencyKey;
101
- const list = this._bindings.get(key);
129
+ const list = this.#bindings.get(key);
102
130
  return list !== undefined && list.length > 0;
103
131
  }
104
132
 
105
133
  /** All bindings in the registry. */
106
134
  allBindings(): ReadonlyArray<Binding> {
107
135
  const allBindings: Array<Binding> = [];
108
- for (const bindingsForToken of this._bindings.values()) {
136
+ for (const bindingsForToken of this.#bindings.values()) {
109
137
  allBindings.push(...bindingsForToken);
110
138
  }
111
139
  return allBindings;
@@ -113,54 +141,55 @@ export class BindingRegistry {
113
141
 
114
142
  /** Remove all bindings. Returns all removed. */
115
143
  clear(): ReadonlyArray<Binding> {
144
+ this.#version += 1;
116
145
  const all = this.allBindings();
117
- this._bindings.clear();
118
- this._byId.clear();
119
- this._simpleNamed.clear();
120
- this._simpleTagged.clear();
121
- this._fastDefault.clear();
146
+ this.#bindings.clear();
147
+ this.#byId.clear();
148
+ this.#simpleNamed?.clear();
149
+ this.#simpleTagged?.clear();
150
+ this.#fastDefault.clear();
122
151
  return all;
123
152
  }
124
153
 
125
154
  getSimpleNamed(token: Token<unknown> | Constructor, name: string): Binding | undefined {
126
- return this._simpleNamed.get(token as DependencyKey)?.get(name);
155
+ return this.#simpleNamed?.get(token as DependencyKey)?.get(name);
127
156
  }
128
157
 
129
158
  getSimpleTagged(token: Token<unknown> | Constructor, tagKey: string, tagValue: unknown): Binding | undefined {
130
- return this._simpleTagged
131
- .get(token as DependencyKey)
159
+ return this.#simpleTagged
160
+ ?.get(token as DependencyKey)
132
161
  ?.get(tagKey)
133
162
  ?.get(tagValue);
134
163
  }
135
164
 
136
165
  getFastDefault(token: Token<unknown> | Constructor): Binding | undefined {
137
- return this._fastDefault.get(token as DependencyKey);
166
+ return this.#fastDefault.get(token as DependencyKey);
138
167
  }
139
168
 
140
169
  /** Summarize available slot strings for a token (for error messages). */
141
170
  availableSlotStrings(token: Token<unknown> | Constructor): Array<string> {
142
- const bindingsForToken = this._bindings.get(token as DependencyKey) ?? [];
171
+ const bindingsForToken = this.#bindings.get(token as DependencyKey) ?? [];
143
172
  return bindingsForToken.map((binding) => bindingSlotToString(binding.slot));
144
173
  }
145
174
 
146
- private _indexSimpleTaggedBinding(tokenKey: DependencyKey, binding: Binding): void {
175
+ #indexSimpleTaggedBinding(tokenKey: DependencyKey, binding: Binding): void {
147
176
  const slot = binding.slot;
148
177
  if (slot.name !== undefined || slot.tags.length !== 1 || binding.predicate !== undefined) {
149
178
  return;
150
179
  }
151
180
  const [tagKey, tagValue] = slot.tags[0]!;
152
- const byTagKey = this._simpleTagged.getOrInsert(tokenKey, new Map<string, Map<unknown, Binding>>());
181
+ const byTagKey = (this.#simpleTagged ??= new Map()).getOrInsert(tokenKey, new Map<string, Map<unknown, Binding>>());
153
182
  const byTagValue = byTagKey.getOrInsert(tagKey, new Map<unknown, Binding>());
154
183
  byTagValue.set(tagValue, binding);
155
184
  }
156
185
 
157
- private _deindexSimpleTaggedBinding(tokenKey: DependencyKey, binding: Binding): void {
186
+ #deindexSimpleTaggedBinding(tokenKey: DependencyKey, binding: Binding): void {
158
187
  const slot = binding.slot;
159
188
  if (slot.name !== undefined || slot.tags.length !== 1 || binding.predicate !== undefined) {
160
189
  return;
161
190
  }
162
191
  const [tagKey, tagValue] = slot.tags[0]!;
163
- const byTagKey = this._simpleTagged.get(tokenKey);
192
+ const byTagKey = this.#simpleTagged?.get(tokenKey);
164
193
  if (byTagKey === undefined) {
165
194
  return;
166
195
  }
@@ -174,13 +203,13 @@ export class BindingRegistry {
174
203
  if (byTagValue.size === 0) {
175
204
  byTagKey.delete(tagKey);
176
205
  if (byTagKey.size === 0) {
177
- this._simpleTagged.delete(tokenKey);
206
+ this.#simpleTagged!.delete(tokenKey);
178
207
  }
179
208
  }
180
209
  }
181
210
  }
182
211
 
183
- private _isPurePredicateBinding(binding: Binding): boolean {
212
+ #isPurePredicateBinding(binding: Binding): boolean {
184
213
  const slot = binding.slot;
185
214
  const hasPredicate = binding.predicate !== undefined;
186
215
  const hasConstraint = slot.name !== undefined || slot.tags.length > 0;
@@ -188,21 +217,21 @@ export class BindingRegistry {
188
217
  return hasPredicate && !hasConstraint;
189
218
  }
190
219
 
191
- private _indexSimpleNamedBinding(tokenKey: DependencyKey, binding: Binding): void {
220
+ #indexSimpleNamedBinding(tokenKey: DependencyKey, binding: Binding): void {
192
221
  const slot = binding.slot;
193
222
  if (slot.name === undefined || slot.tags.length > 0) {
194
223
  return;
195
224
  }
196
- const bindingsByName = this._simpleNamed.getOrInsert(tokenKey, new Map<string, Binding>());
225
+ const bindingsByName = (this.#simpleNamed ??= new Map()).getOrInsert(tokenKey, new Map<string, Binding>());
197
226
  bindingsByName.set(slot.name, binding);
198
227
  }
199
228
 
200
- private _deindexSimpleNamedBinding(tokenKey: DependencyKey, binding: Binding): void {
229
+ #deindexSimpleNamedBinding(tokenKey: DependencyKey, binding: Binding): void {
201
230
  const slot = binding.slot;
202
231
  if (slot.name === undefined || slot.tags.length > 0) {
203
232
  return;
204
233
  }
205
- const bindingsByName = this._simpleNamed.get(tokenKey);
234
+ const bindingsByName = this.#simpleNamed?.get(tokenKey);
206
235
  if (bindingsByName === undefined) {
207
236
  return;
208
237
  }
@@ -210,23 +239,27 @@ export class BindingRegistry {
210
239
  if (currentBinding?.id === binding.id) {
211
240
  bindingsByName.delete(slot.name);
212
241
  if (bindingsByName.size === 0) {
213
- this._simpleNamed.delete(tokenKey);
242
+ this.#simpleNamed!.delete(tokenKey);
214
243
  }
215
244
  }
216
245
  }
217
246
 
218
- private _refreshFastDefaultForToken(tokenKey: DependencyKey): void {
219
- const bindingsForToken = this._bindings.get(tokenKey);
247
+ #refreshFastDefaultForToken(tokenKey: DependencyKey): void {
248
+ const bindingsForToken = this.#bindings.get(tokenKey);
220
249
  if (bindingsForToken === undefined || bindingsForToken.length !== 1) {
221
- this._fastDefault.delete(tokenKey);
250
+ this.#fastDefault.delete(tokenKey);
222
251
  return;
223
252
  }
224
253
  const onlyBinding = bindingsForToken[0]!;
225
254
  const isDefaultSlot = onlyBinding.slot.name === undefined && onlyBinding.slot.tags.length === 0;
226
255
  if (!isDefaultSlot || onlyBinding.predicate !== undefined) {
227
- this._fastDefault.delete(tokenKey);
256
+ this.#fastDefault.delete(tokenKey);
228
257
  return;
229
258
  }
230
- this._fastDefault.set(tokenKey, onlyBinding);
259
+ this.#fastDefault.set(tokenKey, onlyBinding);
260
+ }
261
+ /** Whether the deferred table behind `#simpleNamed` has had to be built. */
262
+ get isBuilt(): boolean {
263
+ return this.#simpleNamed !== undefined;
231
264
  }
232
265
  }
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Per binding: does resolving it have to go through the activation pipeline?
3
+ *
4
+ * @remarks Versioned on the lifecycle manager, since `onActivation` can be registered at any time.
5
+ */
6
+ import type { Binding } from "#/binding";
7
+ import type { ClassIntrospector } from "#/resolution/class-introspector";
8
+ import type { LifecycleManager } from "#/resolution/lifecycle";
9
+ import type { BindingIdentifier } from "#/types";
10
+
11
+ /**
12
+ * @since 0.5.0-canary.8
13
+ */
14
+ export class ActivationNeedCache {
15
+ readonly #needByBindingId = new Map<BindingIdentifier, boolean>();
16
+ #version = -1;
17
+ readonly #lifecycle: LifecycleManager;
18
+ readonly #classes: ClassIntrospector;
19
+
20
+ constructor(lifecycle: LifecycleManager, classes: ClassIntrospector) {
21
+ this.#lifecycle = lifecycle;
22
+ this.#classes = classes;
23
+ }
24
+
25
+ needsActivation<const Value>(binding: Binding<Value>): boolean {
26
+ const lifecycleVersion = this.#lifecycle.activationVersion;
27
+ // No hooks registered anywhere and none on the binding: only classes can still surprise us,
28
+ // via a @postConstruct we have not looked for yet.
29
+ if (
30
+ lifecycleVersion === 0 &&
31
+ binding.kind !== "class" &&
32
+ binding.kind !== "alias" &&
33
+ binding.onActivation === undefined
34
+ ) {
35
+ return false;
36
+ }
37
+ if (this.#version !== lifecycleVersion) {
38
+ this.#needByBindingId.clear();
39
+ this.#version = lifecycleVersion;
40
+ }
41
+ const cached = this.#needByBindingId.get(binding.id);
42
+ if (cached !== undefined) {
43
+ return cached;
44
+ }
45
+ const needsActivation =
46
+ binding.kind === "class" ? this.#classNeedsActivation(binding) : this.#nonClassNeedsActivation(binding);
47
+ this.#needByBindingId.set(binding.id, needsActivation);
48
+ return needsActivation;
49
+ }
50
+
51
+ /**
52
+ * Settles a class binding's answer once its lifecycle metadata has actually been read, which
53
+ * only happens on the first instantiation.
54
+ *
55
+ * @returns the answer to use for this resolve — possibly now `false` where it was a
56
+ * conservative `true`.
57
+ */
58
+ refreshAfterFirstInstantiation<Value>(binding: Binding<Value>, needsActivation: boolean): boolean {
59
+ if (binding.kind !== "class" || this.#classes.knownPostConstruct(binding.target) !== undefined) {
60
+ return needsActivation;
61
+ }
62
+ this.#classes.discoverPostConstruct(binding.target);
63
+ this.#needByBindingId.delete(binding.id);
64
+ return this.needsActivation(binding);
65
+ }
66
+
67
+ #classNeedsActivation<const Value>(binding: Binding<Value> & { kind: "class" }): boolean {
68
+ if (this.#lifecycle.hasActivationHandlers(binding.token) || binding.onActivation !== undefined) {
69
+ return true;
70
+ }
71
+ // Unknown lifecycle metadata: activate once so the first instantiation can settle it.
72
+ return this.#classes.knownPostConstruct(binding.target) !== false;
73
+ }
74
+
75
+ #nonClassNeedsActivation<const Value>(binding: Binding<Value>): boolean {
76
+ if (binding.kind !== "alias" && binding.onActivation !== undefined) {
77
+ return true;
78
+ }
79
+ return this.#lifecycle.hasActivationHandlers(binding.token);
80
+ }
81
+ }
@@ -0,0 +1,132 @@
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
+ export class BindingLookupCache<Owner> {
35
+ readonly #byToken = new Map<Token<unknown> | Constructor, DefaultLookupEntry<Owner> | null>();
36
+ #version = -1;
37
+ readonly #byTokenAndName = new Map<Token<unknown> | Constructor, Map<string, DefaultLookupEntry<Owner> | null>>();
38
+ #namedVersion = -1;
39
+
40
+ readonly #registry: BindingRegistry;
41
+ readonly #owner: Owner;
42
+ readonly #parent: BindingLookupCache<Owner> | undefined;
43
+
44
+ constructor(registry: BindingRegistry, owner: Owner, parent: BindingLookupCache<Owner> | undefined) {
45
+ this.#registry = registry;
46
+ this.#owner = owner;
47
+ this.#parent = parent;
48
+ }
49
+
50
+ /** Summed registry versions of this cache's whole chain — the memo stamp. */
51
+ chainVersion(): number {
52
+ let version = this.#registry.version;
53
+ for (let cache = this.#parent; cache !== undefined; cache = cache.#parent) {
54
+ version += cache.#registry.version;
55
+ }
56
+ return version;
57
+ }
58
+
59
+ /** `null` when the token's shape needs the full selection path. */
60
+ defaultEntry(token: Token<unknown> | Constructor): DefaultLookupEntry<Owner> | null {
61
+ const version = this.chainVersion();
62
+ if (version !== this.#version) {
63
+ this.#byToken.clear();
64
+ this.#version = version;
65
+ }
66
+ let entry = this.#byToken.get(token);
67
+ if (entry === undefined) {
68
+ entry = this.#foldAliases(token);
69
+ this.#byToken.set(token, entry);
70
+ }
71
+ return entry;
72
+ }
73
+
74
+ /** `null` when the name's shape needs the full selection path. */
75
+ namedEntry(token: Token<unknown> | Constructor, name: string): DefaultLookupEntry<Owner> | null {
76
+ const version = this.chainVersion();
77
+ if (version !== this.#namedVersion) {
78
+ this.#byTokenAndName.clear();
79
+ this.#namedVersion = version;
80
+ }
81
+ // ✓ TS6.0: Map.getOrInsert (ES2025)
82
+ const byName = this.#byTokenAndName.getOrInsert(token, new Map<string, DefaultLookupEntry<Owner> | null>());
83
+ let entry = byName.get(name);
84
+ if (entry === undefined) {
85
+ entry = this.#findNamedInChain(token, name);
86
+ byName.set(name, entry);
87
+ }
88
+ return entry;
89
+ }
90
+
91
+ #foldAliases(token: Token<unknown> | Constructor): DefaultLookupEntry<Owner> | null {
92
+ let current = token;
93
+ for (let hop = 0; hop < ALIAS_HOP_LIMIT; hop += 1) {
94
+ const entry = this.#findDefaultInChain(current);
95
+ if (entry === null) {
96
+ return null;
97
+ }
98
+ if (entry.binding.kind !== "alias") {
99
+ return entry;
100
+ }
101
+ current = entry.binding.target;
102
+ }
103
+ return null;
104
+ }
105
+
106
+ #findDefaultInChain(token: Token<unknown> | Constructor): DefaultLookupEntry<Owner> | null {
107
+ const fast = this.#registry.getFastDefault(token);
108
+ if (fast !== undefined) {
109
+ return { binding: fast, owner: this.#owner };
110
+ }
111
+ // A level with non-fast bindings (multi-slot / predicate) needs full selection — bail.
112
+ if (this.#registry.has(token)) {
113
+ return null;
114
+ }
115
+ return this.#parent === undefined ? null : this.#parent.#findDefaultInChain(token);
116
+ }
117
+
118
+ #findNamedInChain(token: Token<unknown> | Constructor, name: string): DefaultLookupEntry<Owner> | null {
119
+ const named = this.#registry.getSimpleNamed(token, name);
120
+ if (named !== undefined) {
121
+ // Predicates need a live context; aliases carry options through the full path.
122
+ if (named.predicate !== undefined || named.kind === "alias") {
123
+ return null;
124
+ }
125
+ return { binding: named, owner: this.#owner };
126
+ }
127
+ if (this.#registry.has(token)) {
128
+ return null;
129
+ }
130
+ return this.#parent === undefined ? null : this.#parent.#findNamedInChain(token, name);
131
+ }
132
+ }
@@ -21,9 +21,22 @@ export function selectBinding(
21
21
  if (candidates.length === 1) {
22
22
  return candidates[0];
23
23
  }
24
- // Multiple candidates — check if any is unambiguous (slot-based selection)
25
- // Slot-based bindings already have last-wins applied in registry, so
26
- // multiple candidates here means ambiguous predicate-only bindings
24
+ // Most specific wins: a single matching predicate-carrying candidate beats
25
+ // predicate-less ones (a predicate is a deliberate specialization of the
26
+ // default). Two matching predicates are genuinely ambiguous.
27
+ let predicatedCandidate: Binding | undefined;
28
+ for (const candidate of candidates) {
29
+ if (candidate.predicate !== undefined) {
30
+ if (predicatedCandidate !== undefined) {
31
+ predicatedCandidate = undefined;
32
+ break;
33
+ }
34
+ predicatedCandidate = candidate;
35
+ }
36
+ }
37
+ if (predicatedCandidate !== undefined) {
38
+ return predicatedCandidate;
39
+ }
27
40
  throw new AmbiguousBindingError(
28
41
  tokenDisplayName,
29
42
  candidates.map((c) => c.id),
@@ -122,10 +135,7 @@ function matchesSlot(binding: Binding, options: ResolveOptions | undefined): boo
122
135
  }
123
136
  }
124
137
  } else if (hasRequestedTags) {
125
- // Binding has no tags but options requires tags — no match for tagged slots
126
- // But: default slot (no tags, no name) can match when options has tags if there are no tag-slotted bindings
127
- // Actually per spec: resolveAll with tags only returns bindings that have those tags
128
- // and resolve with tags requires exact match
138
+ // Requested tags require a tagged slot: an untagged binding never matches (SPEC §6.9).
129
139
  return false;
130
140
  }
131
141
 
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Per-class decorator metadata, cached by constructor.
3
+ *
4
+ * @remarks Metadata cannot change once a class is defined, so nothing here needs version stamping.
5
+ */
6
+
7
+ import type { ConstructorInvocation } from "#/constructor-type";
8
+ import type { Container } from "#/container/container";
9
+ import type { ConstructorMetadata, MetadataReader } from "#/metadata/metadata-types";
10
+ import { runWithContainer } from "#/resolution/environment";
11
+ import type { Constructor } from "#/types";
12
+
13
+ /**
14
+ * @since 0.5.0-canary.8
15
+ */
16
+ export class ClassIntrospector {
17
+ // Unallocated until the container resolves its first class binding — a container bound entirely
18
+ // to constants, factories or aliases never introspects one.
19
+ #constructorMetadata: WeakMap<Constructor, ConstructorMetadata | null> | undefined;
20
+ #hasPostConstruct: WeakMap<Constructor, boolean> | undefined;
21
+ #needsActiveContainer: WeakMap<Constructor, boolean> | undefined;
22
+ readonly #reader: MetadataReader;
23
+ readonly #container: Container;
24
+
25
+ constructor(reader: MetadataReader, container: Container) {
26
+ this.#reader = reader;
27
+ this.#container = container;
28
+ }
29
+
30
+ constructorMetadata(target: Constructor): ConstructorMetadata | undefined {
31
+ const cached = this.#constructorMetadata?.get(target);
32
+ if (cached !== undefined) {
33
+ return cached === null ? undefined : cached;
34
+ }
35
+ const metadata = this.#reader.getConstructorMetadata(target);
36
+ (this.#constructorMetadata ??= new WeakMap()).set(target, metadata ?? null);
37
+ return metadata;
38
+ }
39
+
40
+ /**
41
+ * Whether the class has a `@postConstruct` hook, or `undefined` until {@link discoverPostConstruct}.
42
+ *
43
+ * @remarks Callers treat unknown as "assume it does", so the first activation settles it.
44
+ */
45
+ knownPostConstruct(target: Constructor): boolean | undefined {
46
+ return this.#hasPostConstruct?.get(target);
47
+ }
48
+
49
+ discoverPostConstruct(target: Constructor): void {
50
+ const lifecycle = this.#reader.getLifecycleMetadata(target);
51
+ (this.#hasPostConstruct ??= new WeakMap()).set(
52
+ target,
53
+ lifecycle !== undefined && lifecycle.postConstruct !== undefined && lifecycle.postConstruct.length > 0,
54
+ );
55
+ }
56
+
57
+ /** True when the class has accessor injection, which reads the container during construction. */
58
+ needsActiveContainer(target: Constructor): boolean {
59
+ let needsActiveContainer = this.#needsActiveContainer?.get(target);
60
+ if (needsActiveContainer === undefined) {
61
+ needsActiveContainer = (this.#reader.getAccessorMetadata?.(target)?.length ?? 0) > 0;
62
+ (this.#needsActiveContainer ??= new WeakMap()).set(target, needsActiveContainer);
63
+ }
64
+ return needsActiveContainer;
65
+ }
66
+
67
+ instantiate(target: Constructor, deps: Array<unknown>): unknown {
68
+ const invokable = target as ConstructorInvocation;
69
+ if (!this.needsActiveContainer(target)) {
70
+ return new invokable(...deps);
71
+ }
72
+ return runWithContainer(this.#container, () => new invokable(...deps));
73
+ }
74
+ }