@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/binding.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { InjectionDescriptor } from "#/decorators/inject";
1
+ import type { InjectableDependency, InjectionDescriptor, ResolvedDependencyValue } from "#/decorators/inject";
2
2
  import type { Token } from "#/token";
3
3
  import type {
4
4
  ActivationHandler,
@@ -7,9 +7,8 @@ import type {
7
7
  BindingTag,
8
8
  Constructor,
9
9
  DeactivationHandler,
10
- DependencyKey,
11
10
  ResolutionContext,
12
- TokenValue,
11
+ ResolutionFrame,
13
12
  ConstraintContext,
14
13
  } from "#/types";
15
14
 
@@ -41,6 +40,13 @@ export function bindingSlotEquals(left: BindingSlot, right: BindingSlot): boolea
41
40
  return true;
42
41
  }
43
42
 
43
+ /**
44
+ * Cached singleton absent — distinguishes "not resolved yet" from a cached `undefined`.
45
+ *
46
+ * @since 0.5.0-canary.8
47
+ */
48
+ export const NO_INSTANCE: unique symbol = Symbol("di:no-instance");
49
+
44
50
  /**
45
51
  * @since 0.3.16-canary.0
46
52
  */
@@ -67,9 +73,31 @@ export function bindingSlotToString(slot: BindingSlot): string {
67
73
 
68
74
  interface BindingBase<Value> {
69
75
  readonly id: BindingIdentifier;
76
+ /**
77
+ * True while this binding's factory is on the sync resolution stack — the sync lane's cycle check.
78
+ *
79
+ * @remarks Resolver-owned bookkeeping; callers never set it. See `ARCHITECTURE.md` for why the
80
+ * sync and async lanes detect cycles differently.
81
+ */
82
+ inFlight?: boolean | undefined;
83
+ /**
84
+ * Memoized resolution frame for this binding. Its contents derive only from immutable binding
85
+ * fields, so it is computed once on first resolve and reused instead of a per-resolver Map
86
+ * lookup on every hop.
87
+ *
88
+ * @remarks Resolver-owned bookkeeping — `registry.add` normalizes it, so callers never set it.
89
+ */
90
+ frame?: ResolutionFrame | undefined;
91
+ /**
92
+ * Cached singleton instance, or {@link NO_INSTANCE}.
93
+ *
94
+ * @remarks A binding belongs to exactly one container, so its singleton slot is per-binding —
95
+ * a field read replaces a keyed lookup on the hottest resolve shape there is.
96
+ */
97
+ instance?: unknown;
70
98
  readonly token: Token<Value> | Constructor<Value>;
71
99
  readonly slot: BindingSlot;
72
- readonly predicate?: (ctx: ConstraintContext) => boolean;
100
+ readonly predicate?: ((ctx: ConstraintContext) => boolean) | undefined;
73
101
  }
74
102
 
75
103
  type BindingBaseKeys = keyof BindingBase<unknown>;
@@ -83,8 +111,8 @@ export interface ClassBinding<Value> extends BindingBase<Value> {
83
111
  readonly kind: "class";
84
112
  readonly target: Constructor<Value>;
85
113
  readonly scope: BindingScope;
86
- readonly onActivation?: ActivationHandler<Value>;
87
- readonly onDeactivation?: DeactivationHandler<Value>;
114
+ readonly onActivation?: ActivationHandler<Value> | undefined;
115
+ readonly onDeactivation?: DeactivationHandler<Value> | undefined;
88
116
  }
89
117
 
90
118
  /**
@@ -94,8 +122,8 @@ export interface DynamicBinding<Value> extends BindingBase<Value> {
94
122
  readonly kind: "dynamic";
95
123
  readonly factory: (ctx: ResolutionContext) => Value;
96
124
  readonly scope: BindingScope;
97
- readonly onActivation?: ActivationHandler<Value>;
98
- readonly onDeactivation?: DeactivationHandler<Value>;
125
+ readonly onActivation?: ActivationHandler<Value> | undefined;
126
+ readonly onDeactivation?: DeactivationHandler<Value> | undefined;
99
127
  }
100
128
 
101
129
  /**
@@ -105,8 +133,8 @@ export interface DynamicAsyncBinding<Value> extends BindingBase<Value> {
105
133
  readonly kind: "dynamic-async";
106
134
  readonly factory: (ctx: ResolutionContext) => Promise<Value>;
107
135
  readonly scope: BindingScope;
108
- readonly onActivation?: ActivationHandler<Value>;
109
- readonly onDeactivation?: DeactivationHandler<Value>;
136
+ readonly onActivation?: ActivationHandler<Value> | undefined;
137
+ readonly onDeactivation?: DeactivationHandler<Value> | undefined;
110
138
  }
111
139
 
112
140
  /**
@@ -117,8 +145,8 @@ export interface ResolvedBinding<Value> extends BindingBase<Value> {
117
145
  readonly factory: (...args: Array<unknown>) => Value;
118
146
  readonly deps: ReadonlyArray<InjectionDescriptor>;
119
147
  readonly scope: BindingScope;
120
- readonly onActivation?: ActivationHandler<Value>;
121
- readonly onDeactivation?: DeactivationHandler<Value>;
148
+ readonly onActivation?: ActivationHandler<Value> | undefined;
149
+ readonly onDeactivation?: DeactivationHandler<Value> | undefined;
122
150
  }
123
151
 
124
152
  /**
@@ -129,8 +157,8 @@ export interface ResolvedAsyncBinding<Value> extends BindingBase<Value> {
129
157
  readonly factory: (...args: Array<unknown>) => Promise<Value>;
130
158
  readonly deps: ReadonlyArray<InjectionDescriptor>;
131
159
  readonly scope: BindingScope;
132
- readonly onActivation?: ActivationHandler<Value>;
133
- readonly onDeactivation?: DeactivationHandler<Value>;
160
+ readonly onActivation?: ActivationHandler<Value> | undefined;
161
+ readonly onDeactivation?: DeactivationHandler<Value> | undefined;
134
162
  }
135
163
 
136
164
  /**
@@ -140,8 +168,8 @@ export interface ConstantBinding<Value> extends BindingBase<Value> {
140
168
  readonly kind: "constant";
141
169
  readonly value: Value;
142
170
  readonly scope: "singleton";
143
- readonly onActivation?: ActivationHandler<Value>;
144
- readonly onDeactivation?: DeactivationHandler<Value>;
171
+ readonly onActivation?: ActivationHandler<Value> | undefined;
172
+ readonly onDeactivation?: DeactivationHandler<Value> | undefined;
145
173
  }
146
174
 
147
175
  /**
@@ -180,12 +208,99 @@ export type PartialBinding<Value> =
180
208
 
181
209
  // ── ID generation ─────────────────────────────────────────────────────────────
182
210
 
183
- let _idCounter = 0;
211
+ let bindingIdCounter = 0;
184
212
  /**
185
213
  * @since 0.3.16-canary.0
186
214
  */
187
215
  export function generateBindingId(): BindingIdentifier {
188
- return String(++_idCounter) as BindingIdentifier;
216
+ return String(++bindingIdCounter) as BindingIdentifier;
217
+ }
218
+
219
+ // ── Construction ──────────────────────────────────────────────────────────────
220
+
221
+ // Superset of every kind's fields, so one literal can copy any binding shape.
222
+ /** Union of every key any binding kind declares — `keyof` a union would give the intersection. */
223
+ type BindingFieldName = Binding<unknown> extends infer Kind ? (Kind extends unknown ? keyof Kind : never) : never;
224
+
225
+ /**
226
+ * Completeness guard for {@link createBinding}'s literal.
227
+ *
228
+ * @remarks The literal is `satisfies` this, so a field added to any binding kind that the literal
229
+ * forgets to write is a compile error rather than a binding silently missing it.
230
+ */
231
+ type ConstructedBindingFields = Record<BindingFieldName, unknown>;
232
+
233
+ type BindingFieldSuperset = {
234
+ readonly kind: Binding["kind"];
235
+ readonly instance?: unknown;
236
+ readonly scope?: unknown;
237
+ readonly target?: unknown;
238
+ readonly factory?: unknown;
239
+ readonly deps?: unknown;
240
+ readonly value?: unknown;
241
+ readonly onActivation?: unknown;
242
+ readonly onDeactivation?: unknown;
243
+ };
244
+
245
+ /**
246
+ * The single construction site for bindings — one literal, one V8 hidden class.
247
+ *
248
+ * @see `ARCHITECTURE.md` — why the field order and the single site are load-bearing.
249
+ *
250
+ * @param source - the kind-specific payload, or an existing binding to re-slot
251
+ * @param id - reuse a caller's id to keep a fluent chain's `id()` stable across refinements
252
+ *
253
+ * @since 0.5.0-canary.8
254
+ */
255
+ export function createBinding<Value>(
256
+ source: PartialBinding<Value> | Binding<Value>,
257
+ token: Token<Value> | Constructor<Value>,
258
+ slot: BindingSlot,
259
+ predicate: ((ctx: ConstraintContext) => boolean) | undefined,
260
+ id: BindingIdentifier = generateBindingId(),
261
+ ): Binding<Value> {
262
+ const fields = source as BindingFieldSuperset;
263
+ return {
264
+ kind: fields.kind,
265
+ id,
266
+ inFlight: false,
267
+ frame: undefined,
268
+ instance: (source as { instance?: unknown }).instance ?? NO_INSTANCE,
269
+ token,
270
+ slot,
271
+ predicate,
272
+ scope: fields.scope,
273
+ target: fields.target,
274
+ factory: fields.factory,
275
+ deps: fields.deps,
276
+ value: fields.value,
277
+ onActivation: fields.onActivation,
278
+ onDeactivation: fields.onDeactivation,
279
+ } satisfies ConstructedBindingFields as Binding<Value>;
280
+ }
281
+
282
+ /**
283
+ * Writable view of the only fields a fluent chain may refine after registration.
284
+ *
285
+ * @remarks No registry index is keyed on these, so a builder that owns the registered object
286
+ * can write them directly instead of re-registering. `token`, `slot`, `predicate` and `id`
287
+ * are excluded on purpose — changing those means re-indexing.
288
+ *
289
+ * @since 0.5.0-canary.8
290
+ */
291
+ export interface RefinableBindingFields<Value> {
292
+ onActivation: ActivationHandler<Value> | undefined;
293
+ onDeactivation: DeactivationHandler<Value> | undefined;
294
+ scope: BindingScope;
295
+ }
296
+
297
+ /**
298
+ * Narrows a registered binding to the fields a fluent chain may still refine.
299
+ *
300
+ * @since 0.5.0-canary.8
301
+ */
302
+ export function refinableFields<Value>(binding: Binding<Value>): RefinableBindingFields<Value> {
303
+ return binding as RefinableBindingFields<Value>;
189
304
  }
190
305
 
191
306
  // ── Builder interfaces ────────────────────────────────────────────────────────
@@ -212,12 +327,12 @@ export interface BindToBuilder<Value> {
212
327
  toConstantValue(value: Value): ConstantBindingBuilder<Value>;
213
328
  toDynamic(factory: (ctx: ResolutionContext) => Value): BindingBuilder<Value>;
214
329
  toDynamicAsync(factory: (ctx: ResolutionContext) => Promise<Value>): BindingBuilder<Value>;
215
- toResolved<const Deps extends ReadonlyArray<DependencyKey>>(
216
- factory: (...args: { [K in keyof Deps]: TokenValue<NoInfer<Deps>[K]> }) => Value,
330
+ toResolved<const Deps extends ReadonlyArray<InjectableDependency>>(
331
+ factory: (...args: { [K in keyof Deps]: ResolvedDependencyValue<NoInfer<Deps>[K]> }) => Value,
217
332
  deps: Deps,
218
333
  ): BindingBuilder<Value>;
219
- toResolvedAsync<const Deps extends ReadonlyArray<DependencyKey>>(
220
- factory: (...args: { [K in keyof Deps]: TokenValue<NoInfer<Deps>[K]> }) => Promise<Value>,
334
+ toResolvedAsync<const Deps extends ReadonlyArray<InjectableDependency>>(
335
+ factory: (...args: { [K in keyof Deps]: ResolvedDependencyValue<NoInfer<Deps>[K]> }) => Promise<Value>,
221
336
  deps: Deps,
222
337
  ): BindingBuilder<Value>;
223
338
  toAlias(target: Token<Value> | Constructor<Value>): AliasBindingBuilder;
@@ -1,9 +1,8 @@
1
1
  /**
2
- * A class (newable) that produces `Value`. Rest parameters are `never[]` so
3
- * real classes with typed constructors remain assignable under
4
- * `strictFunctionTypes` (unlike `unknown[]`, which is not assignable from
5
- * narrower parameter types). Runtime construction still uses the real shape;
6
- * this alias is the DI “class token” surface only.
2
+ * A class token: newable, producing `Value`.
3
+ *
4
+ * @remarks Rest parameters are `never[]` so classes with typed constructors stay assignable under
5
+ * `strictFunctionTypes`. Construction uses the real shape; this alias is the token surface only.
7
6
  *
8
7
  * @since 0.3.16-canary.0
9
8
  */
@@ -0,0 +1,294 @@
1
+ /**
2
+ * The fluent chain `bind()` returns: it registers the binding and refines it in place.
3
+ *
4
+ * @see `ARCHITECTURE.md` — why one object plays every role, and what pins the ordering.
5
+ */
6
+ import type {
7
+ AliasBindingBuilder,
8
+ Binding,
9
+ BindingBuilder,
10
+ BindingSlot,
11
+ BindToBuilder,
12
+ ConstantBindingBuilder,
13
+ PartialBinding,
14
+ ScopedBindingBuilder,
15
+ SingletonBindingBuilder,
16
+ SingletonLifecycleBuilder,
17
+ TransientBindingBuilder,
18
+ } from "#/binding";
19
+ import { bindingSlotEquals, createBinding, DEFAULT_BINDING_SLOT, refinableFields } from "#/binding";
20
+ import type { InjectableDependency, ResolvedDependencyValue } from "#/decorators/inject";
21
+ import { normalizeToDescriptor } from "#/decorators/inject";
22
+ import { ChainNotRegisteredError } from "#/errors";
23
+ import type { BindingRegistry } from "#/registry";
24
+ import type { Token } from "#/token";
25
+ import { tokenName } from "#/token";
26
+ import type {
27
+ ActivationHandler,
28
+ BindingIdentifier,
29
+ BindingScope,
30
+ ConstraintContext,
31
+ Constructor,
32
+ DeactivationHandler,
33
+ ResolutionContext,
34
+ } from "#/types";
35
+
36
+ function updateSlotTag(slot: BindingSlot, tag: string, value: unknown): BindingSlot {
37
+ const tags = [...slot.tags];
38
+ const existingIndex = tags.findIndex(([key]) => key === tag);
39
+ if (existingIndex === -1) {
40
+ tags.push([tag, value]);
41
+ } else {
42
+ tags[existingIndex] = [tag, value];
43
+ }
44
+ return { ...slot, tags };
45
+ }
46
+
47
+ // True when re-adding `restored` would immediately be displaced again by `current`
48
+ // (both slot-based with equal slots) — in that case the replacement was legitimate.
49
+ function displacesRestoredBinding(current: Binding, restored: Binding): boolean {
50
+ const currentIsPurePredicate =
51
+ current.predicate !== undefined && current.slot.name === undefined && current.slot.tags.length === 0;
52
+ if (currentIsPurePredicate) {
53
+ return false;
54
+ }
55
+ return bindingSlotEquals(current.slot, restored.slot);
56
+ }
57
+
58
+ /** Record a module's binding id, dropping the id the chain re-slotted away from. */
59
+ function trackBindingForModule(
60
+ ids: Array<BindingIdentifier>,
61
+ id: BindingIdentifier,
62
+ previousId: BindingIdentifier | undefined,
63
+ ): void {
64
+ if (previousId !== undefined) {
65
+ const previousIndex = ids.indexOf(previousId);
66
+ if (previousIndex !== -1) {
67
+ ids.splice(previousIndex, 1);
68
+ }
69
+ }
70
+ ids.push(id);
71
+ }
72
+
73
+ // ── Registration target ───────────────────────────────────────────────────────
74
+
75
+ /**
76
+ * Where a chain registers, and on whose behalf.
77
+ *
78
+ * @remarks Built once per container and shared by every chain it creates. `moduleBindingIds` is
79
+ * present exactly when the chain belongs to a module load.
80
+ *
81
+ * @since 0.5.0-canary.8
82
+ */
83
+ export interface BindingRegistration {
84
+ readonly registry: BindingRegistry;
85
+ readonly moduleBindingIds: Array<BindingIdentifier> | undefined;
86
+ }
87
+
88
+ // ── BindingChain ──────────────────────────────────────────────────────────────
89
+
90
+ /**
91
+ * The one builder behind `bind()` and every `to*()` return type. Each interface exposes only the
92
+ * calls that are legal at that point in the chain; the runtime object is shared because every
93
+ * refinement is the same operation — narrow the registered binding, keep its id.
94
+ *
95
+ * @since 0.5.0-canary.8
96
+ */
97
+ export class BindingChain<Value>
98
+ implements
99
+ AliasBindingBuilder,
100
+ BindingBuilder<Value>,
101
+ BindToBuilder<Value>,
102
+ ConstantBindingBuilder<Value>,
103
+ ScopedBindingBuilder<Value>,
104
+ SingletonBindingBuilder<Value>,
105
+ SingletonLifecycleBuilder<Value>,
106
+ TransientBindingBuilder<Value>
107
+ {
108
+ // Undefined until a `to*()` call registers the binding.
109
+ #binding: Binding<Value> | undefined;
110
+ // Allocated only by a chain that actually displaces something — most never do.
111
+ #displacedByChain: Array<Binding> | undefined;
112
+ readonly #token: Token<Value> | Constructor<Value>;
113
+ readonly #registration: BindingRegistration;
114
+
115
+ constructor(token: Token<Value> | Constructor<Value>, registration: BindingRegistration) {
116
+ this.#token = token;
117
+ this.#registration = registration;
118
+ }
119
+
120
+ /** The registered binding, or a loud failure if no `to*()` has run yet. */
121
+ #registered(): Binding<Value> {
122
+ if (this.#binding === undefined) {
123
+ throw new ChainNotRegisteredError(tokenName(this.#token));
124
+ }
125
+ return this.#binding;
126
+ }
127
+
128
+ #register(partial: PartialBinding<Value>): this {
129
+ // Each `to*()` starts its own registration, so anything a previous one displaced is not this
130
+ // registration's to restore.
131
+ this.#displacedByChain = undefined;
132
+ this.#binding = createBinding(partial, this.#token, DEFAULT_BINDING_SLOT, undefined);
133
+ this.#commit(this.#binding, undefined);
134
+ return this;
135
+ }
136
+
137
+ // ── Registration ───────────────────────────────────────────────────────────
138
+
139
+ to(type: Constructor<Value>): BindingBuilder<Value> {
140
+ return this.#register({ kind: "class", target: type, scope: "transient" });
141
+ }
142
+
143
+ toSelf(): BindingBuilder<Value> {
144
+ if (typeof this.#token !== "function") {
145
+ throw new Error("toSelf() requires token to be a Constructor");
146
+ }
147
+ return this.#register({ kind: "class", target: this.#token, scope: "transient" });
148
+ }
149
+
150
+ toConstantValue(value: Value): ConstantBindingBuilder<Value> {
151
+ return this.#register({ kind: "constant", scope: "singleton", value });
152
+ }
153
+
154
+ toDynamic(factory: (ctx: ResolutionContext) => Value): BindingBuilder<Value> {
155
+ return this.#register({ kind: "dynamic", factory, scope: "transient" });
156
+ }
157
+
158
+ toDynamicAsync(factory: (ctx: ResolutionContext) => Promise<Value>): BindingBuilder<Value> {
159
+ return this.#register({ kind: "dynamic-async", factory, scope: "transient" });
160
+ }
161
+
162
+ toResolved<const Deps extends ReadonlyArray<InjectableDependency>>(
163
+ factory: (...args: { [K in keyof Deps]: ResolvedDependencyValue<NoInfer<Deps>[K]> }) => Value,
164
+ deps: Deps,
165
+ ): BindingBuilder<Value> {
166
+ return this.#register({
167
+ kind: "resolved",
168
+ deps: deps.map((dependency) => normalizeToDescriptor(dependency)),
169
+ factory: factory as (...args: Array<unknown>) => Value,
170
+ scope: "transient",
171
+ });
172
+ }
173
+
174
+ toResolvedAsync<const Deps extends ReadonlyArray<InjectableDependency>>(
175
+ factory: (...args: { [K in keyof Deps]: ResolvedDependencyValue<NoInfer<Deps>[K]> }) => Promise<Value>,
176
+ deps: Deps,
177
+ ): BindingBuilder<Value> {
178
+ return this.#register({
179
+ kind: "resolved-async",
180
+ deps: deps.map((dependency) => normalizeToDescriptor(dependency)),
181
+ factory: factory as (...args: Array<unknown>) => Promise<Value>,
182
+ scope: "transient",
183
+ });
184
+ }
185
+
186
+ toAlias(target: Token<Value> | Constructor<Value>): AliasBindingBuilder {
187
+ return this.#register({ kind: "alias", target });
188
+ }
189
+
190
+ // ── Refinement ─────────────────────────────────────────────────────────────
191
+
192
+ // Slot and predicate are what the registry indexes on, so a re-slot rebuilds the binding and
193
+ // re-registers it — under the original id, keeping `id()` stable for the whole chain.
194
+ #reslot(slot: BindingSlot, predicate: ((ctx: ConstraintContext) => boolean) | undefined): this {
195
+ const previous = this.#registered();
196
+ this.#binding = createBinding(previous, previous.token, slot, predicate, previous.id);
197
+ this.#commit(this.#binding, previous.id);
198
+ return this;
199
+ }
200
+
201
+ #withScope(scope: BindingScope): this {
202
+ refinableFields(this.#registered()).scope = scope;
203
+ this.#registration.registry.touch();
204
+ return this;
205
+ }
206
+
207
+ when(predicate: (ctx: ConstraintContext) => boolean): this {
208
+ return this.#reslot(this.#registered().slot, predicate);
209
+ }
210
+
211
+ whenNamed(name: string): this {
212
+ return this.#reslot({ ...this.#registered().slot, name }, this.#registered().predicate);
213
+ }
214
+
215
+ whenTagged(tag: string, value: unknown): this {
216
+ const binding = this.#registered();
217
+ return this.#reslot(updateSlotTag(binding.slot, tag, value), binding.predicate);
218
+ }
219
+
220
+ whenDefault(): this {
221
+ // The default slot is what a fresh registration already has, so there is nothing to re-slot —
222
+ // but an unregistered chain must fail here exactly as it does in every other refinement.
223
+ this.#registered();
224
+ return this;
225
+ }
226
+
227
+ singleton(): SingletonBindingBuilder<Value> {
228
+ return this.#withScope("singleton");
229
+ }
230
+
231
+ transient(): TransientBindingBuilder<Value> {
232
+ return this.#withScope("transient");
233
+ }
234
+
235
+ scoped(): ScopedBindingBuilder<Value> {
236
+ return this.#withScope("scoped");
237
+ }
238
+
239
+ onActivation(fn: ActivationHandler<Value>): this {
240
+ refinableFields(this.#registered()).onActivation = fn;
241
+ this.#registration.registry.touch();
242
+ return this;
243
+ }
244
+
245
+ onDeactivation(fn: DeactivationHandler<Value>): this {
246
+ refinableFields(this.#registered()).onDeactivation = fn;
247
+ this.#registration.registry.touch();
248
+ return this;
249
+ }
250
+
251
+ id(): BindingIdentifier {
252
+ return this.#registered().id;
253
+ }
254
+
255
+ // ── Registry ───────────────────────────────────────────────────────────────
256
+
257
+ /**
258
+ * Register `binding`, first removing `previousId` when the chain is re-slotting.
259
+ *
260
+ * @remarks A `when*()` that follows `to*()` re-slots an already-live binding and can displace one
261
+ * the final shape would never conflict with. Those stay parked in `#displacedByChain` until the
262
+ * chain settles, then get restored.
263
+ */
264
+ #commit(binding: Binding<Value>, previousId: BindingIdentifier | undefined): void {
265
+ const { registry, moduleBindingIds } = this.#registration;
266
+ // The registry is heterogeneous by design — one instance holds every value type — so the
267
+ // chain's `Binding<Value>` is erased once, here, rather than at each call site.
268
+ const registered = binding as Binding;
269
+
270
+ if (previousId !== undefined) {
271
+ registry.removeById(previousId);
272
+ }
273
+ const displaced = registry.add(registered);
274
+ if (displaced !== undefined) {
275
+ (this.#displacedByChain ??= []).push(displaced);
276
+ }
277
+ if (previousId !== undefined && this.#displacedByChain !== undefined) {
278
+ this.#restoreNonConflicting(registered, this.#displacedByChain);
279
+ }
280
+ if (moduleBindingIds !== undefined) {
281
+ trackBindingForModule(moduleBindingIds, registered.id, previousId);
282
+ }
283
+ }
284
+
285
+ #restoreNonConflicting(binding: Binding, displaced: Array<Binding>): void {
286
+ for (let index = displaced.length - 1; index >= 0; index -= 1) {
287
+ const candidate = displaced[index]!;
288
+ if (!displacesRestoredBinding(binding, candidate)) {
289
+ this.#registration.registry.add(candidate);
290
+ displaced.splice(index, 1);
291
+ }
292
+ }
293
+ }
294
+ }