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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/CHANGELOG.md +68 -0
  2. package/README.md +3 -1
  3. package/dist/binding.d.ts +47 -5
  4. package/dist/binding.d.ts.map +1 -1
  5. package/dist/binding.js +44 -0
  6. package/dist/binding.js.map +1 -1
  7. package/dist/constructor-type.d.ts +4 -5
  8. package/dist/constructor-type.d.ts.map +1 -1
  9. package/dist/container/binding-builders.d.ts +32 -11
  10. package/dist/container/binding-builders.d.ts.map +1 -1
  11. package/dist/container/binding-builders.js +140 -191
  12. package/dist/container/binding-builders.js.map +1 -1
  13. package/dist/container/container.d.ts.map +1 -1
  14. package/dist/container/container.js +75 -92
  15. package/dist/container/container.js.map +1 -1
  16. package/dist/decorators/inject.d.ts +2 -4
  17. package/dist/decorators/inject.d.ts.map +1 -1
  18. package/dist/decorators/inject.js.map +1 -1
  19. package/dist/errors.d.ts +14 -0
  20. package/dist/errors.d.ts.map +1 -1
  21. package/dist/errors.js +17 -0
  22. package/dist/errors.js.map +1 -1
  23. package/dist/index.d.ts +1 -1
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +1 -1
  26. package/dist/index.js.map +1 -1
  27. package/dist/introspection/inspector.js +1 -1
  28. package/dist/introspection/inspector.js.map +1 -1
  29. package/dist/metadata/metadata-keys.d.ts +3 -6
  30. package/dist/metadata/metadata-keys.d.ts.map +1 -1
  31. package/dist/metadata/metadata-keys.js +3 -6
  32. package/dist/metadata/metadata-keys.js.map +1 -1
  33. package/dist/registry.d.ts +14 -2
  34. package/dist/registry.d.ts.map +1 -1
  35. package/dist/registry.js +35 -45
  36. package/dist/registry.js.map +1 -1
  37. package/dist/resolution/activation-need.d.ts +25 -0
  38. package/dist/resolution/activation-need.d.ts.map +1 -0
  39. package/dist/resolution/activation-need.js +64 -0
  40. package/dist/resolution/activation-need.js.map +1 -0
  41. package/dist/resolution/binding-lookup-cache.d.ts +41 -0
  42. package/dist/resolution/binding-lookup-cache.d.ts.map +1 -0
  43. package/dist/resolution/binding-lookup-cache.js +102 -0
  44. package/dist/resolution/binding-lookup-cache.js.map +1 -0
  45. package/dist/resolution/binding-select.js +1 -4
  46. package/dist/resolution/binding-select.js.map +1 -1
  47. package/dist/resolution/class-introspector.d.ts +27 -0
  48. package/dist/resolution/class-introspector.d.ts.map +1 -0
  49. package/dist/resolution/class-introspector.js +60 -0
  50. package/dist/resolution/class-introspector.js.map +1 -0
  51. package/dist/resolution/diagnostics.d.ts +41 -0
  52. package/dist/resolution/diagnostics.d.ts.map +1 -0
  53. package/dist/resolution/diagnostics.js +18 -0
  54. package/dist/resolution/diagnostics.js.map +1 -0
  55. package/dist/resolution/environment.d.ts +22 -1
  56. package/dist/resolution/environment.d.ts.map +1 -1
  57. package/dist/resolution/environment.js +27 -1
  58. package/dist/resolution/environment.js.map +1 -1
  59. package/dist/resolution/instantiation-plan.d.ts +15 -15
  60. package/dist/resolution/instantiation-plan.d.ts.map +1 -1
  61. package/dist/resolution/instantiation-plan.js +68 -48
  62. package/dist/resolution/instantiation-plan.js.map +1 -1
  63. package/dist/resolution/lifecycle.d.ts +2 -0
  64. package/dist/resolution/lifecycle.d.ts.map +1 -1
  65. package/dist/resolution/lifecycle.js +16 -11
  66. package/dist/resolution/lifecycle.js.map +1 -1
  67. package/dist/resolution/resolution-path.d.ts +4 -13
  68. package/dist/resolution/resolution-path.d.ts.map +1 -1
  69. package/dist/resolution/resolution-path.js +3 -16
  70. package/dist/resolution/resolution-path.js.map +1 -1
  71. package/dist/resolution/resolver.d.ts +7 -2
  72. package/dist/resolution/resolver.d.ts.map +1 -1
  73. package/dist/resolution/resolver.js +116 -328
  74. package/dist/resolution/resolver.js.map +1 -1
  75. package/dist/resolution/scope.d.ts +7 -15
  76. package/dist/resolution/scope.d.ts.map +1 -1
  77. package/dist/resolution/scope.js +48 -44
  78. package/dist/resolution/scope.js.map +1 -1
  79. package/package.json +7 -97
  80. package/src/binding.ts +104 -5
  81. package/src/constructor-type.ts +4 -5
  82. package/src/container/binding-builders.ts +180 -283
  83. package/src/container/container.ts +90 -103
  84. package/src/decorators/inject.ts +3 -5
  85. package/src/errors.ts +21 -0
  86. package/src/index.ts +1 -0
  87. package/src/introspection/inspector.ts +1 -1
  88. package/src/metadata/metadata-keys.ts +3 -6
  89. package/src/registry.ts +38 -59
  90. package/src/resolution/activation-need.ts +81 -0
  91. package/src/resolution/binding-lookup-cache.ts +132 -0
  92. package/src/resolution/binding-select.ts +1 -4
  93. package/src/resolution/class-introspector.ts +74 -0
  94. package/src/resolution/diagnostics.ts +43 -0
  95. package/src/resolution/environment.ts +31 -1
  96. package/src/resolution/instantiation-plan.ts +113 -61
  97. package/src/resolution/lifecycle.ts +16 -11
  98. package/src/resolution/resolution-path.ts +5 -20
  99. package/src/resolution/resolver.ts +142 -371
  100. package/src/resolution/scope.ts +50 -49
@@ -1,9 +1,7 @@
1
1
  /**
2
- * Fluent binding-builder chain behind `container.bind()` / `container.rebind()`.
2
+ * The fluent chain `bind()` returns: it registers the binding and refines it in place.
3
3
  *
4
- * Every builder commits eagerly on construction and re-commits (replacing the
5
- * previous binding by id) whenever a later fluent call refines the binding —
6
- * so a chain left half-finished is still a valid registration.
4
+ * @see `ARCHITECTURE.md` — why one object plays every role, and what pins the ordering.
7
5
  */
8
6
  import type {
9
7
  AliasBindingBuilder,
@@ -18,380 +16,279 @@ import type {
18
16
  SingletonLifecycleBuilder,
19
17
  TransientBindingBuilder,
20
18
  } from "#/binding";
21
- import { DEFAULT_BINDING_SLOT, generateBindingId } from "#/binding";
19
+ import { bindingSlotEquals, createBinding, DEFAULT_BINDING_SLOT, refinableFields } from "#/binding";
22
20
  import type { InjectableDependency, ResolvedDependencyValue } from "#/decorators/inject";
23
21
  import { normalizeToDescriptor } from "#/decorators/inject";
22
+ import { ChainNotRegisteredError } from "#/errors";
23
+ import type { BindingRegistry } from "#/registry";
24
24
  import type { Token } from "#/token";
25
+ import { tokenName } from "#/token";
25
26
  import type {
26
27
  ActivationHandler,
27
28
  BindingIdentifier,
29
+ BindingScope,
28
30
  ConstraintContext,
29
31
  Constructor,
30
32
  DeactivationHandler,
31
33
  ResolutionContext,
32
34
  } from "#/types";
33
35
 
34
- // ── Shared builder helpers ────────────────────────────────────────────────────
35
-
36
- /**
37
- * @since 0.5.0-canary.7
38
- */
39
- export type CommitFn = <Value>(binding: Binding<Value>, previousId?: BindingIdentifier) => BindingIdentifier;
40
-
41
- // Builder payloads that still accept a scope — alias/constant bindings never flow through ConstraintBuilder/ScopeBuilder.
42
- type ScopedPartialBinding<Value> = Exclude<PartialBinding<Value>, { kind: "alias" } | { kind: "constant" }>;
43
-
44
36
  function updateSlotTag(slot: BindingSlot, tag: string, value: unknown): BindingSlot {
45
- const existing = [...slot.tags];
46
- const idx = existing.findIndex(([k]) => k === tag);
47
- if (idx !== -1) {
48
- existing[idx] = [tag, value];
37
+ const tags = [...slot.tags];
38
+ const existingIndex = tags.findIndex(([key]) => key === tag);
39
+ if (existingIndex === -1) {
40
+ tags.push([tag, value]);
49
41
  } else {
50
- existing.push([tag, value]);
42
+ tags[existingIndex] = [tag, value];
51
43
  }
52
- return { ...slot, tags: existing };
44
+ return { ...slot, tags };
53
45
  }
54
46
 
55
- // ── SlotBuilder ───────────────────────────────────────────────────────────────
56
-
57
- abstract class SlotBuilder {
58
- protected slot: BindingSlot = DEFAULT_BINDING_SLOT; // ✓ T3-1: no redundant spread
59
- protected predicate: ((ctx: ConstraintContext) => boolean) | undefined;
60
- protected committedId: BindingIdentifier | undefined;
61
-
62
- protected abstract commit(): void;
63
-
64
- when(predicate: (ctx: ConstraintContext) => boolean): this {
65
- this.predicate = predicate;
66
- this.commit();
67
- return this;
68
- }
69
-
70
- whenNamed(name: string): this {
71
- this.slot = { ...this.slot, name };
72
- this.commit();
73
- return this;
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;
74
54
  }
55
+ return bindingSlotEquals(current.slot, restored.slot);
56
+ }
75
57
 
76
- whenTagged(tag: string, value: unknown): this {
77
- this.slot = updateSlotTag(this.slot, tag, value);
78
- this.commit();
79
- return this;
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
+ }
80
69
  }
70
+ ids.push(id);
71
+ }
81
72
 
82
- whenDefault(): this {
83
- return this;
84
- }
73
+ // ── Registration target ───────────────────────────────────────────────────────
85
74
 
86
- id(): BindingIdentifier {
87
- return this.committedId!;
88
- }
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;
89
86
  }
90
87
 
91
- // ── BindingEntry ──────────────────────────────────────────────────────────────
88
+ // ── BindingChain ──────────────────────────────────────────────────────────────
92
89
 
93
90
  /**
94
- * @since 0.5.0-canary.7
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
95
96
  */
96
- export class BindingEntry<Value> implements BindToBuilder<Value> {
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;
97
112
  readonly #token: Token<Value> | Constructor<Value>;
98
- readonly #commitBinding: CommitFn;
113
+ readonly #registration: BindingRegistration;
99
114
 
100
- constructor(token: Token<Value> | Constructor<Value>, commitBinding: CommitFn) {
115
+ constructor(token: Token<Value> | Constructor<Value>, registration: BindingRegistration) {
101
116
  this.#token = token;
102
- this.#commitBinding = commitBinding;
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;
103
126
  }
104
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
+
105
139
  to(type: Constructor<Value>): BindingBuilder<Value> {
106
- return new ConstraintBuilder<Value>(
107
- this.#token,
108
- { kind: "class", target: type, scope: "transient" },
109
- this.#commitBinding,
110
- );
140
+ return this.#register({ kind: "class", target: type, scope: "transient" });
111
141
  }
112
142
 
113
143
  toSelf(): BindingBuilder<Value> {
114
144
  if (typeof this.#token !== "function") {
115
145
  throw new Error("toSelf() requires token to be a Constructor");
116
146
  }
117
- return new ConstraintBuilder<Value>(
118
- this.#token,
119
- { kind: "class", target: this.#token, scope: "transient" },
120
- this.#commitBinding,
121
- );
147
+ return this.#register({ kind: "class", target: this.#token, scope: "transient" });
122
148
  }
123
149
 
124
150
  toConstantValue(value: Value): ConstantBindingBuilder<Value> {
125
- return new ConstantBuilder<Value>(this.#token, value, this.#commitBinding);
151
+ return this.#register({ kind: "constant", scope: "singleton", value });
126
152
  }
127
153
 
128
154
  toDynamic(factory: (ctx: ResolutionContext) => Value): BindingBuilder<Value> {
129
- return new ConstraintBuilder<Value>(
130
- this.#token,
131
- { kind: "dynamic", factory, scope: "transient" },
132
- this.#commitBinding,
133
- );
155
+ return this.#register({ kind: "dynamic", factory, scope: "transient" });
134
156
  }
135
157
 
136
158
  toDynamicAsync(factory: (ctx: ResolutionContext) => Promise<Value>): BindingBuilder<Value> {
137
- return new ConstraintBuilder<Value>(
138
- this.#token,
139
- { kind: "dynamic-async", factory, scope: "transient" },
140
- this.#commitBinding,
141
- );
159
+ return this.#register({ kind: "dynamic-async", factory, scope: "transient" });
142
160
  }
143
161
 
144
162
  toResolved<const Deps extends ReadonlyArray<InjectableDependency>>(
145
163
  factory: (...args: { [K in keyof Deps]: ResolvedDependencyValue<NoInfer<Deps>[K]> }) => Value,
146
164
  deps: Deps,
147
165
  ): BindingBuilder<Value> {
148
- const normalizedDeps = deps.map((dependency) => normalizeToDescriptor(dependency));
149
- return new ConstraintBuilder<Value>(
150
- this.#token,
151
- {
152
- kind: "resolved",
153
- factory: factory as (...args: Array<unknown>) => Value,
154
- deps: normalizedDeps,
155
- scope: "transient",
156
- },
157
- this.#commitBinding,
158
- );
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
+ });
159
172
  }
160
173
 
161
174
  toResolvedAsync<const Deps extends ReadonlyArray<InjectableDependency>>(
162
175
  factory: (...args: { [K in keyof Deps]: ResolvedDependencyValue<NoInfer<Deps>[K]> }) => Promise<Value>,
163
176
  deps: Deps,
164
177
  ): BindingBuilder<Value> {
165
- const normalizedDeps = deps.map((dependency) => normalizeToDescriptor(dependency));
166
- return new ConstraintBuilder<Value>(
167
- this.#token,
168
- {
169
- kind: "resolved-async",
170
- factory: factory as (...args: Array<unknown>) => Promise<Value>,
171
- deps: normalizedDeps,
172
- scope: "transient",
173
- },
174
- this.#commitBinding,
175
- );
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
+ });
176
184
  }
177
185
 
178
186
  toAlias(target: Token<Value> | Constructor<Value>): AliasBindingBuilder {
179
- return new AliasBuilder<Value>(this.#token, target, this.#commitBinding);
187
+ return this.#register({ kind: "alias", target });
180
188
  }
181
- }
182
189
 
183
- // ── ConstraintBuilder ─────────────────────────────────────────────────────────
184
-
185
- class ConstraintBuilder<Value> extends SlotBuilder implements BindingBuilder<Value> {
186
- readonly #token: Token<Value> | Constructor<Value>;
187
- readonly #partial: ScopedPartialBinding<Value>;
188
- readonly #commitBinding: CommitFn;
189
-
190
- constructor(token: Token<Value> | Constructor<Value>, partial: ScopedPartialBinding<Value>, commitBinding: CommitFn) {
191
- super();
192
- this.#token = token;
193
- this.#partial = partial;
194
- this.#commitBinding = commitBinding;
195
- this.commit();
196
- }
190
+ // ── Refinement ─────────────────────────────────────────────────────────────
197
191
 
198
- protected commit(): void {
199
- const previousId = this.committedId;
200
- const binding: Binding<Value> = {
201
- ...this.#partial,
202
- id: generateBindingId(),
203
- inFlight: false,
204
- token: this.#token,
205
- slot: this.slot,
206
- predicate: this.predicate,
207
- };
208
- this.committedId = this.#commitBinding(binding, previousId);
209
- }
210
-
211
- singleton(): SingletonBindingBuilder<Value> {
212
- const previousId = this.committedId;
213
- this.committedId = undefined;
214
- return new ScopeBuilder<Value>(
215
- this.#token,
216
- { ...this.#partial, scope: "singleton" },
217
- this.slot,
218
- this.predicate,
219
- this.#commitBinding,
220
- previousId,
221
- );
222
- }
223
-
224
- transient(): TransientBindingBuilder<Value> {
225
- const previousId = this.committedId;
226
- this.committedId = undefined;
227
- return new ScopeBuilder<Value>(
228
- this.#token,
229
- { ...this.#partial, scope: "transient" },
230
- this.slot,
231
- this.predicate,
232
- this.#commitBinding,
233
- previousId,
234
- );
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;
235
199
  }
236
200
 
237
- scoped(): ScopedBindingBuilder<Value> {
238
- const previousId = this.committedId;
239
- this.committedId = undefined;
240
- return new ScopeBuilder<Value>(
241
- this.#token,
242
- { ...this.#partial, scope: "scoped" },
243
- this.slot,
244
- this.predicate,
245
- this.#commitBinding,
246
- previousId,
247
- );
201
+ #withScope(scope: BindingScope): this {
202
+ refinableFields(this.#registered()).scope = scope;
203
+ this.#registration.registry.touch();
204
+ return this;
248
205
  }
249
- }
250
-
251
- // ── ScopeBuilder ─────────────────────────────────────────────────────────────
252
206
 
253
- class ScopeBuilder<Value> implements SingletonBindingBuilder<Value>, TransientBindingBuilder<Value> {
254
- #onActivation: ActivationHandler<Value> | undefined;
255
- #onDeactivation: DeactivationHandler<Value> | undefined;
256
- #committedId: BindingIdentifier | undefined;
257
-
258
- readonly #token: Token<Value> | Constructor<Value>;
259
- readonly #partial: ScopedPartialBinding<Value>;
260
- readonly #slot: BindingSlot;
261
- readonly #predicate: ((ctx: ConstraintContext) => boolean) | undefined;
262
- readonly #commitBinding: CommitFn;
263
- readonly #initialPreviousId: BindingIdentifier | undefined;
264
-
265
- constructor(
266
- token: Token<Value> | Constructor<Value>,
267
- partial: ScopedPartialBinding<Value>,
268
- slot: BindingSlot,
269
- predicate: ((ctx: ConstraintContext) => boolean) | undefined,
270
- commitBinding: CommitFn,
271
- initialPreviousId?: BindingIdentifier,
272
- ) {
273
- this.#token = token;
274
- this.#partial = partial;
275
- this.#slot = slot;
276
- this.#predicate = predicate;
277
- this.#commitBinding = commitBinding;
278
- this.#initialPreviousId = initialPreviousId;
279
- this.#commit();
207
+ when(predicate: (ctx: ConstraintContext) => boolean): this {
208
+ return this.#reslot(this.#registered().slot, predicate);
280
209
  }
281
210
 
282
- #commit(): void {
283
- const previousId = this.#committedId ?? this.#initialPreviousId;
284
- const binding: Binding<Value> = {
285
- ...this.#partial,
286
- id: generateBindingId(),
287
- inFlight: false,
288
- token: this.#token,
289
- slot: this.#slot,
290
- predicate: this.#predicate,
291
- onActivation: this.#onActivation,
292
- onDeactivation: this.#onDeactivation,
293
- };
294
- this.#committedId = this.#commitBinding(binding, previousId);
211
+ whenNamed(name: string): this {
212
+ return this.#reslot({ ...this.#registered().slot, name }, this.#registered().predicate);
295
213
  }
296
214
 
297
- onActivation(fn: ActivationHandler<Value>): this {
298
- this.#onActivation = fn;
299
- this.#commit();
300
- return this;
215
+ whenTagged(tag: string, value: unknown): this {
216
+ const binding = this.#registered();
217
+ return this.#reslot(updateSlotTag(binding.slot, tag, value), binding.predicate);
301
218
  }
302
219
 
303
- onDeactivation(fn: DeactivationHandler<Value>): this {
304
- this.#onDeactivation = fn;
305
- this.#commit();
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();
306
224
  return this;
307
225
  }
308
226
 
309
- id(): BindingIdentifier {
310
- return this.#committedId!;
227
+ singleton(): SingletonBindingBuilder<Value> {
228
+ return this.#withScope("singleton");
311
229
  }
312
- }
313
-
314
- // ── ConstantBuilder ───────────────────────────────────────────────────────────
315
-
316
- class ConstantBuilder<Value>
317
- extends SlotBuilder
318
- implements ConstantBindingBuilder<Value>, SingletonLifecycleBuilder<Value>
319
- {
320
- #onActivation: ActivationHandler<Value> | undefined;
321
- #onDeactivation: DeactivationHandler<Value> | undefined;
322
230
 
323
- readonly #token: Token<Value> | Constructor<Value>;
324
- readonly #value: Value;
325
- readonly #commitBinding: CommitFn;
326
-
327
- constructor(token: Token<Value> | Constructor<Value>, value: Value, commitBinding: CommitFn) {
328
- super();
329
- this.#token = token;
330
- this.#value = value;
331
- this.#commitBinding = commitBinding;
332
- this.commit();
231
+ transient(): TransientBindingBuilder<Value> {
232
+ return this.#withScope("transient");
333
233
  }
334
234
 
335
- protected commit(): void {
336
- const previousId = this.committedId;
337
- const binding: Binding<Value> = {
338
- kind: "constant",
339
- id: generateBindingId(),
340
- inFlight: false,
341
- token: this.#token,
342
- slot: this.slot,
343
- predicate: this.predicate,
344
- value: this.#value,
345
- scope: "singleton",
346
- onActivation: this.#onActivation,
347
- onDeactivation: this.#onDeactivation,
348
- };
349
- this.committedId = this.#commitBinding(binding, previousId);
235
+ scoped(): ScopedBindingBuilder<Value> {
236
+ return this.#withScope("scoped");
350
237
  }
351
238
 
352
239
  onActivation(fn: ActivationHandler<Value>): this {
353
- this.#onActivation = fn;
354
- this.commit();
240
+ refinableFields(this.#registered()).onActivation = fn;
241
+ this.#registration.registry.touch();
355
242
  return this;
356
243
  }
357
244
 
358
245
  onDeactivation(fn: DeactivationHandler<Value>): this {
359
- this.#onDeactivation = fn;
360
- this.commit();
246
+ refinableFields(this.#registered()).onDeactivation = fn;
247
+ this.#registration.registry.touch();
361
248
  return this;
362
249
  }
363
- }
364
250
 
365
- // ── AliasBuilder ──────────────────────────────────────────────────────────────
251
+ id(): BindingIdentifier {
252
+ return this.#registered().id;
253
+ }
366
254
 
367
- class AliasBuilder<Value> extends SlotBuilder implements AliasBindingBuilder {
368
- readonly #token: Token<Value> | Constructor<Value>;
369
- readonly #target: Token<Value> | Constructor<Value>;
370
- readonly #commitBinding: CommitFn;
371
-
372
- constructor(
373
- token: Token<Value> | Constructor<Value>,
374
- target: Token<Value> | Constructor<Value>,
375
- commitBinding: CommitFn,
376
- ) {
377
- super();
378
- this.#token = token;
379
- this.#target = target;
380
- this.#commitBinding = commitBinding;
381
- this.commit();
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
+ }
382
283
  }
383
284
 
384
- protected commit(): void {
385
- const previousId = this.committedId;
386
- const binding: Binding<Value> = {
387
- kind: "alias",
388
- id: generateBindingId(),
389
- inFlight: false,
390
- token: this.#token,
391
- slot: this.slot,
392
- predicate: this.predicate,
393
- target: this.#target,
394
- };
395
- this.committedId = this.#commitBinding(binding, previousId);
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
+ }
396
293
  }
397
294
  }