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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (214) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/README.md +73 -496
  3. package/dist/binding.d.ts +211 -0
  4. package/dist/binding.d.ts.map +1 -0
  5. package/dist/binding.js +46 -0
  6. package/dist/binding.js.map +1 -0
  7. package/dist/{constructor-type.d.mts → constructor-type.d.ts} +3 -5
  8. package/dist/constructor-type.d.ts.map +1 -0
  9. package/dist/constructor-type.js +2 -0
  10. package/dist/constructor-type.js.map +1 -0
  11. package/dist/container/binding-builders.d.ts +35 -0
  12. package/dist/container/binding-builders.d.ts.map +1 -0
  13. package/dist/container/binding-builders.js +247 -0
  14. package/dist/container/binding-builders.js.map +1 -0
  15. package/dist/container/container.d.ts +58 -0
  16. package/dist/container/container.d.ts.map +1 -0
  17. package/dist/container/container.js +641 -0
  18. package/dist/container/container.js.map +1 -0
  19. package/dist/decorators/inject.d.ts +57 -0
  20. package/dist/decorators/inject.d.ts.map +1 -0
  21. package/dist/decorators/inject.js +145 -0
  22. package/dist/decorators/inject.js.map +1 -0
  23. package/dist/decorators/injectable.d.ts +29 -0
  24. package/dist/decorators/injectable.d.ts.map +1 -0
  25. package/dist/decorators/injectable.js +53 -0
  26. package/dist/decorators/injectable.js.map +1 -0
  27. package/dist/decorators/lifecycle-decorators.d.ts +9 -0
  28. package/dist/decorators/lifecycle-decorators.d.ts.map +1 -0
  29. package/dist/decorators/lifecycle-decorators.js +40 -0
  30. package/dist/decorators/lifecycle-decorators.js.map +1 -0
  31. package/dist/errors.d.ts +150 -0
  32. package/dist/errors.d.ts.map +1 -0
  33. package/dist/errors.js +197 -0
  34. package/dist/errors.js.map +1 -0
  35. package/dist/index.d.ts +30 -0
  36. package/dist/index.d.ts.map +1 -0
  37. package/dist/index.js +25 -0
  38. package/dist/index.js.map +1 -0
  39. package/dist/introspection/dependency-graph.d.ts +40 -0
  40. package/dist/introspection/dependency-graph.d.ts.map +1 -0
  41. package/dist/introspection/dependency-graph.js +66 -0
  42. package/dist/introspection/dependency-graph.js.map +1 -0
  43. package/dist/introspection/graph-adapters/cytoscape.d.ts +33 -0
  44. package/dist/introspection/graph-adapters/cytoscape.d.ts.map +1 -0
  45. package/dist/introspection/graph-adapters/cytoscape.js +30 -0
  46. package/dist/introspection/graph-adapters/cytoscape.js.map +1 -0
  47. package/dist/introspection/graph-adapters/dot.d.ts +6 -0
  48. package/dist/introspection/graph-adapters/dot.d.ts.map +1 -0
  49. package/dist/introspection/graph-adapters/dot.js +18 -0
  50. package/dist/introspection/graph-adapters/dot.js.map +1 -0
  51. package/dist/introspection/graph-adapters/reactflow.d.ts +38 -0
  52. package/dist/introspection/graph-adapters/reactflow.d.ts.map +1 -0
  53. package/dist/introspection/graph-adapters/reactflow.js +33 -0
  54. package/dist/introspection/graph-adapters/reactflow.js.map +1 -0
  55. package/dist/introspection/inspector.d.ts +39 -0
  56. package/dist/introspection/inspector.d.ts.map +1 -0
  57. package/dist/introspection/inspector.js +82 -0
  58. package/dist/introspection/inspector.js.map +1 -0
  59. package/dist/metadata/{metadata-keys.d.mts → metadata-keys.d.ts} +5 -7
  60. package/dist/metadata/metadata-keys.d.ts.map +1 -0
  61. package/dist/metadata/metadata-keys.js +25 -0
  62. package/dist/metadata/metadata-keys.js.map +1 -0
  63. package/dist/metadata/metadata-reader-token.d.ts +7 -0
  64. package/dist/metadata/metadata-reader-token.d.ts.map +1 -0
  65. package/dist/metadata/metadata-reader-token.js +6 -0
  66. package/dist/metadata/metadata-reader-token.js.map +1 -0
  67. package/dist/metadata/metadata-types.d.ts +48 -0
  68. package/dist/metadata/metadata-types.d.ts.map +1 -0
  69. package/dist/metadata/metadata-types.js +2 -0
  70. package/dist/metadata/metadata-types.js.map +1 -0
  71. package/dist/metadata/symbol-metadata-reader.d.ts +20 -0
  72. package/dist/metadata/symbol-metadata-reader.d.ts.map +1 -0
  73. package/dist/metadata/symbol-metadata-reader.js +34 -0
  74. package/dist/metadata/symbol-metadata-reader.js.map +1 -0
  75. package/dist/module.d.ts +67 -0
  76. package/dist/module.d.ts.map +1 -0
  77. package/dist/module.js +54 -0
  78. package/dist/module.js.map +1 -0
  79. package/dist/registry.d.ts +33 -0
  80. package/dist/registry.d.ts.map +1 -0
  81. package/dist/registry.js +249 -0
  82. package/dist/registry.js.map +1 -0
  83. package/dist/resolution/binding-scope.d.ts +10 -0
  84. package/dist/resolution/binding-scope.d.ts.map +1 -0
  85. package/dist/resolution/binding-scope.js +24 -0
  86. package/dist/resolution/binding-scope.js.map +1 -0
  87. package/dist/resolution/binding-select.d.ts +16 -0
  88. package/dist/resolution/binding-select.d.ts.map +1 -0
  89. package/dist/resolution/binding-select.js +143 -0
  90. package/dist/resolution/binding-select.js.map +1 -0
  91. package/dist/resolution/constraints.d.ts +51 -0
  92. package/dist/resolution/constraints.d.ts.map +1 -0
  93. package/dist/resolution/constraints.js +88 -0
  94. package/dist/resolution/constraints.js.map +1 -0
  95. package/dist/resolution/environment.d.ts +47 -0
  96. package/dist/resolution/environment.d.ts.map +1 -0
  97. package/dist/resolution/environment.js +100 -0
  98. package/dist/resolution/environment.js.map +1 -0
  99. package/dist/resolution/instantiation-plan.d.ts +67 -0
  100. package/dist/resolution/instantiation-plan.d.ts.map +1 -0
  101. package/dist/resolution/instantiation-plan.js +163 -0
  102. package/dist/resolution/instantiation-plan.js.map +1 -0
  103. package/dist/resolution/lifecycle.d.ts +21 -0
  104. package/dist/resolution/lifecycle.d.ts.map +1 -0
  105. package/dist/resolution/lifecycle.js +178 -0
  106. package/dist/resolution/lifecycle.js.map +1 -0
  107. package/dist/resolution/resolution-path.d.ts +31 -0
  108. package/dist/resolution/resolution-path.d.ts.map +1 -0
  109. package/dist/resolution/resolution-path.js +53 -0
  110. package/dist/resolution/resolution-path.js.map +1 -0
  111. package/dist/resolution/resolve-options.d.ts +19 -0
  112. package/dist/resolution/resolve-options.d.ts.map +1 -0
  113. package/dist/resolution/resolve-options.js +32 -0
  114. package/dist/resolution/resolve-options.js.map +1 -0
  115. package/dist/resolution/resolver.d.ts +35 -0
  116. package/dist/resolution/resolver.d.ts.map +1 -0
  117. package/dist/resolution/resolver.js +1320 -0
  118. package/dist/resolution/resolver.js.map +1 -0
  119. package/dist/resolution/scope.d.ts +32 -0
  120. package/dist/resolution/scope.d.ts.map +1 -0
  121. package/dist/resolution/scope.js +76 -0
  122. package/dist/resolution/scope.js.map +1 -0
  123. package/dist/token.d.ts +23 -0
  124. package/dist/token.d.ts.map +1 -0
  125. package/dist/token.js +22 -0
  126. package/dist/token.js.map +1 -0
  127. package/dist/types.d.ts +90 -0
  128. package/dist/types.d.ts.map +1 -0
  129. package/dist/types.js +2 -0
  130. package/dist/types.js.map +1 -0
  131. package/package.json +143 -120
  132. package/src/binding.ts +38 -22
  133. package/src/container/binding-builders.ts +397 -0
  134. package/src/container/container.ts +838 -0
  135. package/src/decorators/inject.ts +16 -3
  136. package/src/decorators/injectable.ts +3 -3
  137. package/src/index.ts +14 -7
  138. package/src/{dependency-graph.ts → introspection/dependency-graph.ts} +1 -1
  139. package/src/{graph-adapters → introspection/graph-adapters}/cytoscape.ts +1 -1
  140. package/src/{graph-adapters → introspection/graph-adapters}/dot.ts +1 -1
  141. package/src/{graph-adapters → introspection/graph-adapters}/reactflow.ts +13 -2
  142. package/src/{inspector.ts → introspection/inspector.ts} +26 -21
  143. package/src/metadata/symbol-metadata-reader.ts +4 -4
  144. package/src/module.ts +14 -6
  145. package/src/registry.ts +115 -61
  146. package/src/{binding-select.ts → resolution/binding-select.ts} +16 -3
  147. package/src/{environment.ts → resolution/environment.ts} +35 -35
  148. package/src/resolution/instantiation-plan.ts +240 -0
  149. package/src/{lifecycle.ts → resolution/lifecycle.ts} +20 -13
  150. package/src/resolution/resolution-path.ts +77 -0
  151. package/src/resolution/resolver.ts +1755 -0
  152. package/src/{scope.ts → resolution/scope.ts} +35 -18
  153. package/src/token.ts +0 -3
  154. package/dist/binding-scope.d.mts +0 -13
  155. package/dist/binding-scope.mjs +0 -21
  156. package/dist/binding-select.d.mts +0 -19
  157. package/dist/binding-select.mjs +0 -72
  158. package/dist/binding.d.mts +0 -191
  159. package/dist/binding.mjs +0 -36
  160. package/dist/constraints.d.mts +0 -55
  161. package/dist/constraints.mjs +0 -87
  162. package/dist/constructor-type.mjs +0 -1
  163. package/dist/container.d.mts +0 -62
  164. package/dist/container.mjs +0 -741
  165. package/dist/decorators/inject.d.mts +0 -49
  166. package/dist/decorators/inject.mjs +0 -136
  167. package/dist/decorators/injectable.d.mts +0 -32
  168. package/dist/decorators/injectable.mjs +0 -57
  169. package/dist/decorators/lifecycle-decorators.d.mts +0 -11
  170. package/dist/decorators/lifecycle-decorators.mjs +0 -38
  171. package/dist/dependency-graph.d.mts +0 -43
  172. package/dist/dependency-graph.mjs +0 -63
  173. package/dist/environment.d.mts +0 -55
  174. package/dist/environment.mjs +0 -98
  175. package/dist/errors.d.mts +0 -153
  176. package/dist/errors.mjs +0 -197
  177. package/dist/graph-adapters/cytoscape.d.mts +0 -36
  178. package/dist/graph-adapters/cytoscape.mjs +0 -26
  179. package/dist/graph-adapters/dot.d.mts +0 -9
  180. package/dist/graph-adapters/dot.mjs +0 -20
  181. package/dist/graph-adapters/reactflow.d.mts +0 -41
  182. package/dist/graph-adapters/reactflow.mjs +0 -29
  183. package/dist/index.d.mts +0 -18
  184. package/dist/index.mjs +0 -12
  185. package/dist/inspector.d.mts +0 -48
  186. package/dist/inspector.mjs +0 -71
  187. package/dist/lifecycle.d.mts +0 -25
  188. package/dist/lifecycle.mjs +0 -118
  189. package/dist/metadata/metadata-keys.mjs +0 -27
  190. package/dist/metadata/metadata-reader-token.d.mts +0 -10
  191. package/dist/metadata/metadata-reader-token.mjs +0 -8
  192. package/dist/metadata/metadata-types.d.mts +0 -51
  193. package/dist/metadata/metadata-types.mjs +0 -1
  194. package/dist/metadata/symbol-metadata-reader.d.mts +0 -23
  195. package/dist/metadata/symbol-metadata-reader.mjs +0 -29
  196. package/dist/module.d.mts +0 -62
  197. package/dist/module.mjs +0 -42
  198. package/dist/registry.d.mts +0 -45
  199. package/dist/registry.mjs +0 -163
  200. package/dist/resolve-options.d.mts +0 -22
  201. package/dist/resolve-options.mjs +0 -28
  202. package/dist/resolver.d.mts +0 -94
  203. package/dist/resolver.mjs +0 -767
  204. package/dist/scope.d.mts +0 -28
  205. package/dist/scope.mjs +0 -58
  206. package/dist/token.d.mts +0 -25
  207. package/dist/token.mjs +0 -22
  208. package/dist/types.d.mts +0 -92
  209. package/dist/types.mjs +0 -1
  210. package/src/container.ts +0 -1141
  211. package/src/resolver.ts +0 -1626
  212. /package/src/{binding-scope.ts → resolution/binding-scope.ts} +0 -0
  213. /package/src/{constraints.ts → resolution/constraints.ts} +0 -0
  214. /package/src/{resolve-options.ts → resolution/resolve-options.ts} +0 -0
package/src/registry.ts CHANGED
@@ -3,83 +3,136 @@ import { bindingSlotEquals, bindingSlotToString } from "#/binding";
3
3
  import type { Token } from "#/token";
4
4
  import type { BindingIdentifier, Constructor, DependencyKey } from "#/types";
5
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
+ }
43
+
6
44
  /**
7
45
  * @since 0.3.16-canary.0
8
46
  */
9
47
  export class BindingRegistry {
48
+ // Monotonic mutation counter — lets resolvers version-stamp lookup caches across a container chain.
49
+ #version = 0;
10
50
  // Map from token key -> array of bindings (order matters for last-wins)
11
- private readonly _bindings = new Map<DependencyKey, Array<Binding>>();
51
+ readonly #bindings = new Map<DependencyKey, Array<Binding>>();
12
52
  // Fast lookup by binding ID
13
- private readonly _byId = new Map<BindingIdentifier, Binding>();
53
+ readonly #byId = new Map<BindingIdentifier, Binding>();
14
54
  // Fast lookup for slot { name, tags: [] }
15
- private readonly _simpleNamed = new Map<DependencyKey, Map<string, Binding>>();
55
+ readonly #simpleNamed = new Map<DependencyKey, Map<string, Binding>>();
16
56
  // Fast path for one default slot binding with no predicate
17
- private readonly _fastDefault = new Map<DependencyKey, Binding>();
57
+ readonly #fastDefault = new Map<DependencyKey, Binding>();
18
58
  // Fast lookup for slot { name: undefined, tags: [[key, value]] } with no predicate
19
- private readonly _simpleTagged = new Map<DependencyKey, Map<string, Map<unknown, Binding>>>();
59
+ readonly #simpleTagged = new Map<DependencyKey, Map<string, Map<unknown, Binding>>>();
60
+
61
+ /** Monotonic version — increments on every mutation. */
62
+ get version(): number {
63
+ return this.#version;
64
+ }
20
65
 
21
- /** Add or replace binding using slot-aware last-wins. */
22
- add(binding: Binding): void {
66
+ /** Add or replace binding using slot-aware last-wins. Returns the displaced binding, if any. */
67
+ add(uncommittedBinding: Binding): Binding | undefined {
68
+ this.#version += 1;
69
+ const binding = normalizeBindingShape(uncommittedBinding);
23
70
  const key = binding.token as DependencyKey;
24
71
  // ✓ TS6.0: Map.getOrInsert (ES2025) replaces the manual get+check+set upsert
25
- const bindingsForToken = this._bindings.getOrInsert(key, []);
72
+ const bindingsForToken = this.#bindings.getOrInsert(key, []);
26
73
 
27
74
  // Only apply last-wins for slot-based bindings (not predicate-only)
28
- if (!this._isPurePredicateBinding(binding)) {
75
+ let displacedBinding: Binding | undefined;
76
+ if (!this.#isPurePredicateBinding(binding)) {
29
77
  const existingIndex = bindingsForToken.findIndex(
30
- (candidate) => !this._isPurePredicateBinding(candidate) && bindingSlotEquals(candidate.slot, binding.slot),
78
+ (candidate) => !this.#isPurePredicateBinding(candidate) && bindingSlotEquals(candidate.slot, binding.slot),
31
79
  );
32
80
  if (existingIndex !== -1) {
33
- const replacedBinding = bindingsForToken[existingIndex]!;
34
- this._byId.delete(replacedBinding.id);
81
+ displacedBinding = bindingsForToken[existingIndex]!;
82
+ this.#byId.delete(displacedBinding.id);
35
83
  bindingsForToken.splice(existingIndex, 1);
84
+ this.#deindexSimpleNamedBinding(key, displacedBinding);
85
+ this.#deindexSimpleTaggedBinding(key, displacedBinding);
36
86
  }
37
87
  }
38
88
 
39
89
  bindingsForToken.push(binding);
40
- this._byId.set(binding.id, binding);
41
- this._indexSimpleNamedBinding(key, binding);
42
- this._indexSimpleTaggedBinding(key, binding);
43
- this._refreshFastDefaultForToken(key);
90
+ this.#byId.set(binding.id, binding);
91
+ this.#indexSimpleNamedBinding(key, binding);
92
+ this.#indexSimpleTaggedBinding(key, binding);
93
+ this.#refreshFastDefaultForToken(key);
94
+ return displacedBinding;
44
95
  }
45
96
 
46
97
  /** Remove all bindings for a token. Returns removed bindings. */
47
98
  removeByToken(token: Token<unknown> | Constructor): Array<Binding> {
99
+ this.#version += 1;
48
100
  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);
101
+ const bindingsForToken = this.#bindings.get(key) ?? [];
102
+ this.#bindings.delete(key);
103
+ this.#simpleNamed.delete(key);
104
+ this.#simpleTagged.delete(key);
105
+ this.#fastDefault.delete(key);
54
106
  for (const binding of bindingsForToken) {
55
- this._byId.delete(binding.id);
107
+ this.#byId.delete(binding.id);
56
108
  }
57
109
  return bindingsForToken;
58
110
  }
59
111
 
60
112
  /** Remove a specific binding by ID. Returns the removed binding or undefined. */
61
113
  removeById(id: BindingIdentifier): Binding | undefined {
62
- const binding = this._byId.get(id);
114
+ const binding = this.#byId.get(id);
63
115
  if (binding === undefined) {
64
116
  return undefined;
65
117
  }
66
- this._byId.delete(id);
118
+ this.#version += 1;
119
+ this.#byId.delete(id);
67
120
  const key = binding.token as DependencyKey;
68
- const bindingsForToken = this._bindings.get(key);
121
+ const bindingsForToken = this.#bindings.get(key);
69
122
  if (bindingsForToken !== undefined) {
70
123
  const bindingIndex = bindingsForToken.findIndex((candidate) => candidate.id === id);
71
124
  if (bindingIndex !== -1) {
72
125
  bindingsForToken.splice(bindingIndex, 1);
73
126
  }
74
- this._deindexSimpleNamedBinding(key, binding);
75
- this._deindexSimpleTaggedBinding(key, binding);
127
+ this.#deindexSimpleNamedBinding(key, binding);
128
+ this.#deindexSimpleTaggedBinding(key, binding);
76
129
  if (bindingsForToken.length === 0) {
77
- this._bindings.delete(key);
78
- this._simpleNamed.delete(key);
79
- this._simpleTagged.delete(key);
80
- this._fastDefault.delete(key);
130
+ this.#bindings.delete(key);
131
+ this.#simpleNamed.delete(key);
132
+ this.#simpleTagged.delete(key);
133
+ this.#fastDefault.delete(key);
81
134
  } else {
82
- this._refreshFastDefaultForToken(key);
135
+ this.#refreshFastDefaultForToken(key);
83
136
  }
84
137
  }
85
138
  return binding;
@@ -87,25 +140,25 @@ export class BindingRegistry {
87
140
 
88
141
  /** Get all bindings for a token. */
89
142
  getAll(token: Token<unknown> | Constructor): ReadonlyArray<Binding> {
90
- return this._bindings.get(token as DependencyKey) ?? [];
143
+ return this.#bindings.get(token as DependencyKey) ?? [];
91
144
  }
92
145
 
93
146
  /** Get binding by ID. */
94
147
  getById(id: BindingIdentifier): Binding | undefined {
95
- return this._byId.get(id);
148
+ return this.#byId.get(id);
96
149
  }
97
150
 
98
151
  /** Check if any binding exists for token. */
99
152
  has(token: Token<unknown> | Constructor): boolean {
100
153
  const key = token as DependencyKey;
101
- const list = this._bindings.get(key);
154
+ const list = this.#bindings.get(key);
102
155
  return list !== undefined && list.length > 0;
103
156
  }
104
157
 
105
158
  /** All bindings in the registry. */
106
159
  allBindings(): ReadonlyArray<Binding> {
107
160
  const allBindings: Array<Binding> = [];
108
- for (const bindingsForToken of this._bindings.values()) {
161
+ for (const bindingsForToken of this.#bindings.values()) {
109
162
  allBindings.push(...bindingsForToken);
110
163
  }
111
164
  return allBindings;
@@ -113,54 +166,55 @@ export class BindingRegistry {
113
166
 
114
167
  /** Remove all bindings. Returns all removed. */
115
168
  clear(): ReadonlyArray<Binding> {
169
+ this.#version += 1;
116
170
  const all = this.allBindings();
117
- this._bindings.clear();
118
- this._byId.clear();
119
- this._simpleNamed.clear();
120
- this._simpleTagged.clear();
121
- this._fastDefault.clear();
171
+ this.#bindings.clear();
172
+ this.#byId.clear();
173
+ this.#simpleNamed.clear();
174
+ this.#simpleTagged.clear();
175
+ this.#fastDefault.clear();
122
176
  return all;
123
177
  }
124
178
 
125
179
  getSimpleNamed(token: Token<unknown> | Constructor, name: string): Binding | undefined {
126
- return this._simpleNamed.get(token as DependencyKey)?.get(name);
180
+ return this.#simpleNamed.get(token as DependencyKey)?.get(name);
127
181
  }
128
182
 
129
183
  getSimpleTagged(token: Token<unknown> | Constructor, tagKey: string, tagValue: unknown): Binding | undefined {
130
- return this._simpleTagged
184
+ return this.#simpleTagged
131
185
  .get(token as DependencyKey)
132
186
  ?.get(tagKey)
133
187
  ?.get(tagValue);
134
188
  }
135
189
 
136
190
  getFastDefault(token: Token<unknown> | Constructor): Binding | undefined {
137
- return this._fastDefault.get(token as DependencyKey);
191
+ return this.#fastDefault.get(token as DependencyKey);
138
192
  }
139
193
 
140
194
  /** Summarize available slot strings for a token (for error messages). */
141
195
  availableSlotStrings(token: Token<unknown> | Constructor): Array<string> {
142
- const bindingsForToken = this._bindings.get(token as DependencyKey) ?? [];
196
+ const bindingsForToken = this.#bindings.get(token as DependencyKey) ?? [];
143
197
  return bindingsForToken.map((binding) => bindingSlotToString(binding.slot));
144
198
  }
145
199
 
146
- private _indexSimpleTaggedBinding(tokenKey: DependencyKey, binding: Binding): void {
200
+ #indexSimpleTaggedBinding(tokenKey: DependencyKey, binding: Binding): void {
147
201
  const slot = binding.slot;
148
202
  if (slot.name !== undefined || slot.tags.length !== 1 || binding.predicate !== undefined) {
149
203
  return;
150
204
  }
151
205
  const [tagKey, tagValue] = slot.tags[0]!;
152
- const byTagKey = this._simpleTagged.getOrInsert(tokenKey, new Map<string, Map<unknown, Binding>>());
206
+ const byTagKey = this.#simpleTagged.getOrInsert(tokenKey, new Map<string, Map<unknown, Binding>>());
153
207
  const byTagValue = byTagKey.getOrInsert(tagKey, new Map<unknown, Binding>());
154
208
  byTagValue.set(tagValue, binding);
155
209
  }
156
210
 
157
- private _deindexSimpleTaggedBinding(tokenKey: DependencyKey, binding: Binding): void {
211
+ #deindexSimpleTaggedBinding(tokenKey: DependencyKey, binding: Binding): void {
158
212
  const slot = binding.slot;
159
213
  if (slot.name !== undefined || slot.tags.length !== 1 || binding.predicate !== undefined) {
160
214
  return;
161
215
  }
162
216
  const [tagKey, tagValue] = slot.tags[0]!;
163
- const byTagKey = this._simpleTagged.get(tokenKey);
217
+ const byTagKey = this.#simpleTagged.get(tokenKey);
164
218
  if (byTagKey === undefined) {
165
219
  return;
166
220
  }
@@ -174,13 +228,13 @@ export class BindingRegistry {
174
228
  if (byTagValue.size === 0) {
175
229
  byTagKey.delete(tagKey);
176
230
  if (byTagKey.size === 0) {
177
- this._simpleTagged.delete(tokenKey);
231
+ this.#simpleTagged.delete(tokenKey);
178
232
  }
179
233
  }
180
234
  }
181
235
  }
182
236
 
183
- private _isPurePredicateBinding(binding: Binding): boolean {
237
+ #isPurePredicateBinding(binding: Binding): boolean {
184
238
  const slot = binding.slot;
185
239
  const hasPredicate = binding.predicate !== undefined;
186
240
  const hasConstraint = slot.name !== undefined || slot.tags.length > 0;
@@ -188,21 +242,21 @@ export class BindingRegistry {
188
242
  return hasPredicate && !hasConstraint;
189
243
  }
190
244
 
191
- private _indexSimpleNamedBinding(tokenKey: DependencyKey, binding: Binding): void {
245
+ #indexSimpleNamedBinding(tokenKey: DependencyKey, binding: Binding): void {
192
246
  const slot = binding.slot;
193
247
  if (slot.name === undefined || slot.tags.length > 0) {
194
248
  return;
195
249
  }
196
- const bindingsByName = this._simpleNamed.getOrInsert(tokenKey, new Map<string, Binding>());
250
+ const bindingsByName = this.#simpleNamed.getOrInsert(tokenKey, new Map<string, Binding>());
197
251
  bindingsByName.set(slot.name, binding);
198
252
  }
199
253
 
200
- private _deindexSimpleNamedBinding(tokenKey: DependencyKey, binding: Binding): void {
254
+ #deindexSimpleNamedBinding(tokenKey: DependencyKey, binding: Binding): void {
201
255
  const slot = binding.slot;
202
256
  if (slot.name === undefined || slot.tags.length > 0) {
203
257
  return;
204
258
  }
205
- const bindingsByName = this._simpleNamed.get(tokenKey);
259
+ const bindingsByName = this.#simpleNamed.get(tokenKey);
206
260
  if (bindingsByName === undefined) {
207
261
  return;
208
262
  }
@@ -210,23 +264,23 @@ export class BindingRegistry {
210
264
  if (currentBinding?.id === binding.id) {
211
265
  bindingsByName.delete(slot.name);
212
266
  if (bindingsByName.size === 0) {
213
- this._simpleNamed.delete(tokenKey);
267
+ this.#simpleNamed.delete(tokenKey);
214
268
  }
215
269
  }
216
270
  }
217
271
 
218
- private _refreshFastDefaultForToken(tokenKey: DependencyKey): void {
219
- const bindingsForToken = this._bindings.get(tokenKey);
272
+ #refreshFastDefaultForToken(tokenKey: DependencyKey): void {
273
+ const bindingsForToken = this.#bindings.get(tokenKey);
220
274
  if (bindingsForToken === undefined || bindingsForToken.length !== 1) {
221
- this._fastDefault.delete(tokenKey);
275
+ this.#fastDefault.delete(tokenKey);
222
276
  return;
223
277
  }
224
278
  const onlyBinding = bindingsForToken[0]!;
225
279
  const isDefaultSlot = onlyBinding.slot.name === undefined && onlyBinding.slot.tags.length === 0;
226
280
  if (!isDefaultSlot || onlyBinding.predicate !== undefined) {
227
- this._fastDefault.delete(tokenKey);
281
+ this.#fastDefault.delete(tokenKey);
228
282
  return;
229
283
  }
230
- this._fastDefault.set(tokenKey, onlyBinding);
284
+ this.#fastDefault.set(tokenKey, onlyBinding);
231
285
  }
232
286
  }
@@ -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),
@@ -1,4 +1,4 @@
1
- import type { Container } from "#/container";
1
+ import type { Container } from "#/container/container";
2
2
  import type { Token } from "#/token";
3
3
  import type {
4
4
  BindingIdentifier,
@@ -13,18 +13,18 @@ import type {
13
13
 
14
14
  // ── Active container ──────────────────────────────────────────────────────────
15
15
 
16
- let _activeContainer: Container | undefined;
16
+ let activeContainer: Container | undefined;
17
17
 
18
18
  /**
19
19
  * @since 0.3.16-canary.0
20
20
  */
21
21
  export function runWithContainer<Result>(container: Container, fn: () => Result): Result {
22
- const prev = _activeContainer;
23
- _activeContainer = container;
22
+ const prev = activeContainer;
23
+ activeContainer = container;
24
24
  try {
25
25
  return fn();
26
26
  } finally {
27
- _activeContainer = prev;
27
+ activeContainer = prev;
28
28
  }
29
29
  }
30
30
 
@@ -32,7 +32,7 @@ export function runWithContainer<Result>(container: Container, fn: () => Result)
32
32
  * @since 0.3.16-canary.0
33
33
  */
34
34
  export function getActiveContainer(): Container | undefined {
35
- return _activeContainer;
35
+ return activeContainer;
36
36
  }
37
37
 
38
38
  // ── ResolutionContext implementation ──────────────────────────────────────────
@@ -93,10 +93,10 @@ export interface ResolverCallbacks {
93
93
  * @since 0.3.16-canary.0
94
94
  */
95
95
  export class DefaultResolutionContext implements ResolutionContext {
96
- private _resolver: ResolverCallbacks;
97
- private _resolutionPath: Array<string>;
98
- private _resolutionStack: Array<ResolutionFrame>;
99
- private _currentOptions: ResolveOptions | undefined;
96
+ #resolver: ResolverCallbacks;
97
+ #resolutionPath: Array<string>;
98
+ #resolutionStack: Array<ResolutionFrame>;
99
+ #currentOptions: ResolveOptions | undefined;
100
100
 
101
101
  constructor(
102
102
  resolver: ResolverCallbacks,
@@ -104,19 +104,19 @@ export class DefaultResolutionContext implements ResolutionContext {
104
104
  resolutionStack: Array<ResolutionFrame>,
105
105
  currentOptions: ResolveOptions | undefined,
106
106
  ) {
107
- this._resolver = resolver;
108
- this._resolutionPath = resolutionPath;
109
- this._resolutionStack = resolutionStack;
110
- this._currentOptions = currentOptions;
107
+ this.#resolver = resolver;
108
+ this.#resolutionPath = resolutionPath;
109
+ this.#resolutionStack = resolutionStack;
110
+ this.#currentOptions = currentOptions;
111
111
  }
112
112
 
113
- private _graph: ConstraintContext | undefined;
113
+ #graph: ConstraintContext | undefined;
114
114
 
115
115
  get graph(): ConstraintContext {
116
- if (this._graph === undefined) {
117
- this._graph = new DefaultConstraintContext(this._resolutionPath, this._resolutionStack, this._currentOptions);
116
+ if (this.#graph === undefined) {
117
+ this.#graph = new DefaultConstraintContext(this.#resolutionPath, this.#resolutionStack, this.#currentOptions);
118
118
  }
119
- return this._graph;
119
+ return this.#graph;
120
120
  }
121
121
 
122
122
  reset(
@@ -125,47 +125,47 @@ export class DefaultResolutionContext implements ResolutionContext {
125
125
  resolutionStack: Array<ResolutionFrame>,
126
126
  currentOptions: ResolveOptions | undefined,
127
127
  ): void {
128
- this._resolver = resolver;
129
- this._resolutionPath = resolutionPath;
130
- this._resolutionStack = resolutionStack;
131
- this._currentOptions = currentOptions;
132
- this._graph = undefined;
128
+ this.#resolver = resolver;
129
+ this.#resolutionPath = resolutionPath;
130
+ this.#resolutionStack = resolutionStack;
131
+ this.#currentOptions = currentOptions;
132
+ this.#graph = undefined;
133
133
  }
134
134
 
135
135
  resolve<const Value>(token: Token<Value> | Constructor<Value>, options?: ResolveOptions): Value {
136
136
  if (options === undefined) {
137
- return this._resolver.resolveFromContext(token, this._resolutionPath, this._resolutionStack);
137
+ return this.#resolver.resolveFromContext(token, this.#resolutionPath, this.#resolutionStack);
138
138
  }
139
- return this._resolver.resolve(token, options, this._resolutionPath, this._resolutionStack);
139
+ return this.#resolver.resolve(token, options, this.#resolutionPath, this.#resolutionStack);
140
140
  }
141
141
 
142
142
  resolveAsync<const Value>(token: Token<Value> | Constructor<Value>, options?: ResolveOptions): Promise<Value> {
143
143
  if (options === undefined) {
144
- return this._resolver.resolveAsyncFromContext(token, this._resolutionPath, this._resolutionStack);
144
+ return this.#resolver.resolveAsyncFromContext(token, this.#resolutionPath, this.#resolutionStack);
145
145
  }
146
- return this._resolver.resolveAsync(token, options, this._resolutionPath, this._resolutionStack);
146
+ return this.#resolver.resolveAsync(token, options, this.#resolutionPath, this.#resolutionStack);
147
147
  }
148
148
 
149
149
  resolveOptional<const Value>(token: Token<Value> | Constructor<Value>, options?: ResolveOptions): Value | undefined {
150
- return this._resolver.resolveOptional(token, options, this._resolutionPath, this._resolutionStack);
150
+ return this.#resolver.resolveOptional(token, options, this.#resolutionPath, this.#resolutionStack);
151
151
  }
152
152
 
153
153
  resolveOptionalAsync<const Value>(
154
154
  token: Token<Value> | Constructor<Value>,
155
155
  options?: ResolveOptions,
156
156
  ): Promise<Value | undefined> {
157
- return this._resolver.resolveOptionalAsync(token, options, this._resolutionPath, this._resolutionStack);
157
+ return this.#resolver.resolveOptionalAsync(token, options, this.#resolutionPath, this.#resolutionStack);
158
158
  }
159
159
 
160
160
  resolveAll<const Value>(token: Token<Value> | Constructor<Value>, options?: ResolveOptions): Array<Value> {
161
- return this._resolver.resolveAll(token, options, this._resolutionPath, this._resolutionStack);
161
+ return this.#resolver.resolveAll(token, options, this.#resolutionPath, this.#resolutionStack);
162
162
  }
163
163
 
164
164
  resolveAllAsync<const Value>(
165
165
  token: Token<Value> | Constructor<Value>,
166
166
  options?: ResolveOptions,
167
167
  ): Promise<Array<Value>> {
168
- return this._resolver.resolveAllAsync(token, options, this._resolutionPath, this._resolutionStack);
168
+ return this.#resolver.resolveAllAsync(token, options, this.#resolutionPath, this.#resolutionStack);
169
169
  }
170
170
  }
171
171
 
@@ -186,13 +186,13 @@ class DefaultConstraintContext implements ConstraintContext {
186
186
  this.currentResolveOptions = currentResolveOptions;
187
187
  }
188
188
 
189
- private _ancestors: ReadonlyArray<ResolutionFrame> | undefined;
189
+ #ancestors: ReadonlyArray<ResolutionFrame> | undefined;
190
190
 
191
191
  get ancestors(): ReadonlyArray<ResolutionFrame> {
192
- if (this._ancestors === undefined) {
193
- this._ancestors = this.resolutionStack.length > 1 ? this.resolutionStack.slice(0, -1) : [];
192
+ if (this.#ancestors === undefined) {
193
+ this.#ancestors = this.resolutionStack.length > 1 ? this.resolutionStack.slice(0, -1) : [];
194
194
  }
195
- return this._ancestors;
195
+ return this.#ancestors;
196
196
  }
197
197
  }
198
198