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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/CHANGELOG.md +205 -0
  2. package/README.md +6 -2
  3. package/dist/binding.d.ts +85 -24
  4. package/dist/binding.d.ts.map +1 -1
  5. package/dist/binding.js +55 -0
  6. package/dist/binding.js.map +1 -1
  7. package/dist/constructor-type.d.ts +4 -5
  8. package/dist/constructor-type.d.ts.map +1 -1
  9. package/dist/container/binding-builders.d.ts +32 -11
  10. package/dist/container/binding-builders.d.ts.map +1 -1
  11. package/dist/container/binding-builders.js +144 -192
  12. package/dist/container/binding-builders.js.map +1 -1
  13. package/dist/container/container.d.ts.map +1 -1
  14. package/dist/container/container.js +141 -201
  15. package/dist/container/container.js.map +1 -1
  16. package/dist/decorators/inject.d.ts +2 -4
  17. package/dist/decorators/inject.d.ts.map +1 -1
  18. package/dist/decorators/inject.js.map +1 -1
  19. package/dist/errors.d.ts +24 -0
  20. package/dist/errors.d.ts.map +1 -1
  21. package/dist/errors.js +30 -0
  22. package/dist/errors.js.map +1 -1
  23. package/dist/index.d.ts +1 -2
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +3 -2
  26. package/dist/index.js.map +1 -1
  27. package/dist/introspection/inspector.d.ts +0 -1
  28. package/dist/introspection/inspector.d.ts.map +1 -1
  29. package/dist/introspection/inspector.js +3 -8
  30. package/dist/introspection/inspector.js.map +1 -1
  31. package/dist/metadata/metadata-keys.d.ts +3 -6
  32. package/dist/metadata/metadata-keys.d.ts.map +1 -1
  33. package/dist/metadata/metadata-keys.js +3 -6
  34. package/dist/metadata/metadata-keys.js.map +1 -1
  35. package/dist/registry.d.ts +14 -2
  36. package/dist/registry.d.ts.map +1 -1
  37. package/dist/registry.js +81 -78
  38. package/dist/registry.js.map +1 -1
  39. package/dist/resolution/activation-need.d.ts +27 -0
  40. package/dist/resolution/activation-need.d.ts.map +1 -0
  41. package/dist/resolution/activation-need.js +68 -0
  42. package/dist/resolution/activation-need.js.map +1 -0
  43. package/dist/resolution/binding-lookup-cache.d.ts +41 -0
  44. package/dist/resolution/binding-lookup-cache.d.ts.map +1 -0
  45. package/dist/resolution/binding-lookup-cache.js +118 -0
  46. package/dist/resolution/binding-lookup-cache.js.map +1 -0
  47. package/dist/resolution/binding-scope.d.ts +5 -2
  48. package/dist/resolution/binding-scope.d.ts.map +1 -1
  49. package/dist/resolution/binding-scope.js +6 -17
  50. package/dist/resolution/binding-scope.js.map +1 -1
  51. package/dist/resolution/binding-select.d.ts +8 -1
  52. package/dist/resolution/binding-select.d.ts.map +1 -1
  53. package/dist/resolution/binding-select.js +14 -36
  54. package/dist/resolution/binding-select.js.map +1 -1
  55. package/dist/resolution/class-introspector.d.ts +27 -0
  56. package/dist/resolution/class-introspector.d.ts.map +1 -0
  57. package/dist/resolution/class-introspector.js +60 -0
  58. package/dist/resolution/class-introspector.js.map +1 -0
  59. package/dist/resolution/diagnostics.d.ts +41 -0
  60. package/dist/resolution/diagnostics.d.ts.map +1 -0
  61. package/dist/resolution/diagnostics.js +18 -0
  62. package/dist/resolution/diagnostics.js.map +1 -0
  63. package/dist/resolution/environment.d.ts +48 -1
  64. package/dist/resolution/environment.d.ts.map +1 -1
  65. package/dist/resolution/environment.js +134 -5
  66. package/dist/resolution/environment.js.map +1 -1
  67. package/dist/resolution/instantiation-plan.d.ts +15 -15
  68. package/dist/resolution/instantiation-plan.d.ts.map +1 -1
  69. package/dist/resolution/instantiation-plan.js +69 -49
  70. package/dist/resolution/instantiation-plan.js.map +1 -1
  71. package/dist/resolution/lifecycle.d.ts +2 -0
  72. package/dist/resolution/lifecycle.d.ts.map +1 -1
  73. package/dist/resolution/lifecycle.js +60 -62
  74. package/dist/resolution/lifecycle.js.map +1 -1
  75. package/dist/resolution/resolution-path.d.ts +84 -17
  76. package/dist/resolution/resolution-path.d.ts.map +1 -1
  77. package/dist/resolution/resolution-path.js +68 -23
  78. package/dist/resolution/resolution-path.js.map +1 -1
  79. package/dist/resolution/resolve-options.d.ts +41 -4
  80. package/dist/resolution/resolve-options.d.ts.map +1 -1
  81. package/dist/resolution/resolve-options.js +25 -1
  82. package/dist/resolution/resolve-options.js.map +1 -1
  83. package/dist/resolution/resolver.d.ts +33 -10
  84. package/dist/resolution/resolver.d.ts.map +1 -1
  85. package/dist/resolution/resolver.js +471 -830
  86. package/dist/resolution/resolver.js.map +1 -1
  87. package/dist/resolution/scope.d.ts +11 -15
  88. package/dist/resolution/scope.d.ts.map +1 -1
  89. package/dist/resolution/scope.js +55 -43
  90. package/dist/resolution/scope.js.map +1 -1
  91. package/package.json +10 -106
  92. package/src/binding.ts +146 -24
  93. package/src/constructor-type.ts +4 -5
  94. package/src/container/binding-builders.ts +184 -284
  95. package/src/container/container.ts +161 -221
  96. package/src/decorators/inject.ts +3 -5
  97. package/src/errors.ts +38 -0
  98. package/src/index.ts +4 -1
  99. package/src/introspection/inspector.ts +3 -9
  100. package/src/metadata/metadata-keys.ts +3 -6
  101. package/src/registry.ts +90 -94
  102. package/src/resolution/activation-need.ts +85 -0
  103. package/src/resolution/binding-lookup-cache.ts +148 -0
  104. package/src/resolution/binding-scope.ts +6 -17
  105. package/src/resolution/binding-select.ts +15 -39
  106. package/src/resolution/class-introspector.ts +74 -0
  107. package/src/resolution/diagnostics.ts +43 -0
  108. package/src/resolution/environment.ts +181 -5
  109. package/src/resolution/instantiation-plan.ts +116 -64
  110. package/src/resolution/lifecycle.ts +69 -62
  111. package/src/resolution/resolution-path.ts +122 -43
  112. package/src/resolution/resolve-options.ts +51 -4
  113. package/src/resolution/resolver.ts +649 -1081
  114. package/src/resolution/scope.ts +58 -47
package/src/binding.ts CHANGED
@@ -40,6 +40,13 @@ export function bindingSlotEquals(left: BindingSlot, right: BindingSlot): boolea
40
40
  return true;
41
41
  }
42
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
+
43
50
  /**
44
51
  * @since 0.3.16-canary.0
45
52
  */
@@ -67,14 +74,14 @@ export function bindingSlotToString(slot: BindingSlot): string {
67
74
  interface BindingBase<Value> {
68
75
  readonly id: BindingIdentifier;
69
76
  /**
70
- * True while this binding's factory is on the sync resolution stack. The sync resolver marks
71
- * it on enter and clears it on exit, making cycle detection an O(1) field read with no hashing,
72
- * no path scan, and no side table. Sync resolution is single-threaded, so the flag is exactly
73
- * path membership; the async lane keeps its own per-path check because chains can interleave.
77
+ * True while this binding's factory is executing on the current synchronous call stack.
74
78
  *
75
- * @remarks Resolver-owned bookkeeping — `registry.add` normalizes it, so callers never set it.
79
+ * @remarks Both cycle guards that can use an `O(1)` flag read this — the sync transient-dynamic
80
+ * lane and the async cascade lane — because synchronous code does not interleave, so the flag *is*
81
+ * exact path membership. Not optional: `createBinding` always sets it, and a field that may be
82
+ * absent is a field that can cost the shared hidden class. Resolver-owned; callers never set it.
76
83
  */
77
- inFlight?: boolean | undefined;
84
+ inFlight: boolean;
78
85
  /**
79
86
  * Memoized resolution frame for this binding. Its contents derive only from immutable binding
80
87
  * fields, so it is computed once on first resolve and reused instead of a per-resolver Map
@@ -83,6 +90,13 @@ interface BindingBase<Value> {
83
90
  * @remarks Resolver-owned bookkeeping — `registry.add` normalizes it, so callers never set it.
84
91
  */
85
92
  frame?: ResolutionFrame | undefined;
93
+ /**
94
+ * Cached singleton instance, or {@link NO_INSTANCE}.
95
+ *
96
+ * @remarks A binding belongs to exactly one container, so its singleton slot is per-binding —
97
+ * a field read replaces a keyed lookup on the hottest resolve shape there is.
98
+ */
99
+ instance?: unknown;
86
100
  readonly token: Token<Value> | Constructor<Value>;
87
101
  readonly slot: BindingSlot;
88
102
  readonly predicate?: ((ctx: ConstraintContext) => boolean) | undefined;
@@ -90,74 +104,76 @@ interface BindingBase<Value> {
90
104
 
91
105
  type BindingBaseKeys = keyof BindingBase<unknown>;
92
106
 
107
+ /**
108
+ * The lifecycle hooks every kind but `alias` may carry.
109
+ *
110
+ * @remarks Declared as **methods**, not function-typed properties, so their parameters compare
111
+ * bivariantly and `Binding<Value>` stays assignable to `Binding`. The engine erases the value type at
112
+ * every lane boundary regardless; the public `ActivationHandler` / `DeactivationHandler` keep strict
113
+ * checking, which is where a user's handler is actually verified. Not `readonly`: a fluent chain
114
+ * refines both in place — see {@link RefinableBindingFields}.
115
+ */
116
+ interface BindingLifecycleHooks<Value> {
117
+ onActivation?(ctx: ResolutionContext, instance: Value): Value | Promise<Value>;
118
+ onDeactivation?(instance: Value): void | Promise<void>;
119
+ }
120
+
93
121
  // ── Binding kinds ─────────────────────────────────────────────────────────────
94
122
 
95
123
  /**
96
124
  * @since 0.3.16-canary.0
97
125
  */
98
- export interface ClassBinding<Value> extends BindingBase<Value> {
126
+ export interface ClassBinding<Value> extends BindingBase<Value>, BindingLifecycleHooks<Value> {
99
127
  readonly kind: "class";
100
128
  readonly target: Constructor<Value>;
101
129
  readonly scope: BindingScope;
102
- readonly onActivation?: ActivationHandler<Value> | undefined;
103
- readonly onDeactivation?: DeactivationHandler<Value> | undefined;
104
130
  }
105
131
 
106
132
  /**
107
133
  * @since 0.3.16-canary.0
108
134
  */
109
- export interface DynamicBinding<Value> extends BindingBase<Value> {
135
+ export interface DynamicBinding<Value> extends BindingBase<Value>, BindingLifecycleHooks<Value> {
110
136
  readonly kind: "dynamic";
111
137
  readonly factory: (ctx: ResolutionContext) => Value;
112
138
  readonly scope: BindingScope;
113
- readonly onActivation?: ActivationHandler<Value> | undefined;
114
- readonly onDeactivation?: DeactivationHandler<Value> | undefined;
115
139
  }
116
140
 
117
141
  /**
118
142
  * @since 0.3.16-canary.0
119
143
  */
120
- export interface DynamicAsyncBinding<Value> extends BindingBase<Value> {
144
+ export interface DynamicAsyncBinding<Value> extends BindingBase<Value>, BindingLifecycleHooks<Value> {
121
145
  readonly kind: "dynamic-async";
122
146
  readonly factory: (ctx: ResolutionContext) => Promise<Value>;
123
147
  readonly scope: BindingScope;
124
- readonly onActivation?: ActivationHandler<Value> | undefined;
125
- readonly onDeactivation?: DeactivationHandler<Value> | undefined;
126
148
  }
127
149
 
128
150
  /**
129
151
  * @since 0.3.16-canary.0
130
152
  */
131
- export interface ResolvedBinding<Value> extends BindingBase<Value> {
153
+ export interface ResolvedBinding<Value> extends BindingBase<Value>, BindingLifecycleHooks<Value> {
132
154
  readonly kind: "resolved";
133
155
  readonly factory: (...args: Array<unknown>) => Value;
134
156
  readonly deps: ReadonlyArray<InjectionDescriptor>;
135
157
  readonly scope: BindingScope;
136
- readonly onActivation?: ActivationHandler<Value> | undefined;
137
- readonly onDeactivation?: DeactivationHandler<Value> | undefined;
138
158
  }
139
159
 
140
160
  /**
141
161
  * @since 0.3.16-canary.0
142
162
  */
143
- export interface ResolvedAsyncBinding<Value> extends BindingBase<Value> {
163
+ export interface ResolvedAsyncBinding<Value> extends BindingBase<Value>, BindingLifecycleHooks<Value> {
144
164
  readonly kind: "resolved-async";
145
165
  readonly factory: (...args: Array<unknown>) => Promise<Value>;
146
166
  readonly deps: ReadonlyArray<InjectionDescriptor>;
147
167
  readonly scope: BindingScope;
148
- readonly onActivation?: ActivationHandler<Value> | undefined;
149
- readonly onDeactivation?: DeactivationHandler<Value> | undefined;
150
168
  }
151
169
 
152
170
  /**
153
171
  * @since 0.3.16-canary.0
154
172
  */
155
- export interface ConstantBinding<Value> extends BindingBase<Value> {
173
+ export interface ConstantBinding<Value> extends BindingBase<Value>, BindingLifecycleHooks<Value> {
156
174
  readonly kind: "constant";
157
175
  readonly value: Value;
158
176
  readonly scope: "singleton";
159
- readonly onActivation?: ActivationHandler<Value> | undefined;
160
- readonly onDeactivation?: DeactivationHandler<Value> | undefined;
161
177
  }
162
178
 
163
179
  /**
@@ -166,6 +182,13 @@ export interface ConstantBinding<Value> extends BindingBase<Value> {
166
182
  export interface AliasBinding<Value> extends BindingBase<Value> {
167
183
  readonly kind: "alias";
168
184
  readonly target: Token<Value> | Constructor<Value>;
185
+ /**
186
+ * Always `transient` — an alias defers scoping to the binding it points at.
187
+ *
188
+ * @remarks Declared so `scope` is present on every kind, which is what lets the engine read it
189
+ * as a plain field instead of testing for the one kind that lacks it.
190
+ */
191
+ readonly scope: "transient";
169
192
  }
170
193
 
171
194
  /**
@@ -204,6 +227,105 @@ export function generateBindingId(): BindingIdentifier {
204
227
  return String(++bindingIdCounter) as BindingIdentifier;
205
228
  }
206
229
 
230
+ // ── Construction ──────────────────────────────────────────────────────────────
231
+
232
+ // Superset of every kind's fields, so one literal can copy any binding shape.
233
+ /** Union of every key any binding kind declares — `keyof` a union would give the intersection. */
234
+ type BindingFieldName = Binding<unknown> extends infer Kind ? (Kind extends unknown ? keyof Kind : never) : never;
235
+
236
+ /**
237
+ * Completeness guard for {@link createBinding}'s literal.
238
+ *
239
+ * @remarks The literal is `satisfies` this, so a field added to any binding kind that the literal
240
+ * forgets to write is a compile error rather than a binding silently missing it.
241
+ */
242
+ type ConstructedBindingFields = Record<BindingFieldName, unknown>;
243
+
244
+ type BindingFieldSuperset = {
245
+ readonly kind: Binding["kind"];
246
+ readonly instance?: unknown;
247
+ readonly scope: BindingScope;
248
+ readonly target?: unknown;
249
+ readonly factory?: unknown;
250
+ readonly deps?: unknown;
251
+ readonly value?: unknown;
252
+ readonly onActivation?: unknown;
253
+ readonly onDeactivation?: unknown;
254
+ };
255
+
256
+ /**
257
+ * The single construction site for bindings — one literal, one V8 hidden class.
258
+ *
259
+ * @see `ARCHITECTURE.md` — why the field order and the single site are load-bearing.
260
+ *
261
+ * @param source - the kind-specific payload, or an existing binding to re-slot
262
+ * @param id - reuse a caller's id to keep a fluent chain's `id()` stable across refinements
263
+ *
264
+ * @since 0.5.0-canary.8
265
+ */
266
+ export function createBinding<Value>(
267
+ source: PartialBinding<Value> | Binding<Value>,
268
+ token: Token<Value> | Constructor<Value>,
269
+ slot: BindingSlot,
270
+ predicate: ((ctx: ConstraintContext) => boolean) | undefined,
271
+ id: BindingIdentifier = generateBindingId(),
272
+ ): Binding<Value> {
273
+ const fields = source as BindingFieldSuperset;
274
+ return {
275
+ kind: fields.kind,
276
+ id,
277
+ inFlight: false,
278
+ frame: undefined,
279
+ instance: (source as { instance?: unknown }).instance ?? NO_INSTANCE,
280
+ token,
281
+ slot,
282
+ predicate,
283
+ scope: fields.scope,
284
+ target: fields.target,
285
+ factory: fields.factory,
286
+ deps: fields.deps,
287
+ value: fields.value,
288
+ onActivation: fields.onActivation,
289
+ onDeactivation: fields.onDeactivation,
290
+ } satisfies ConstructedBindingFields as Binding<Value>;
291
+ }
292
+
293
+ /**
294
+ * Writable view of the only fields a fluent chain may refine after registration.
295
+ *
296
+ * @remarks No registry index is keyed on these, so a builder that owns the registered object
297
+ * can write them directly instead of re-registering. `token`, `slot`, `predicate` and `id`
298
+ * are excluded on purpose — changing those means re-indexing.
299
+ *
300
+ * @since 0.5.0-canary.8
301
+ */
302
+ export interface RefinableBindingFields<Value> {
303
+ onActivation: ActivationHandler<Value> | undefined;
304
+ onDeactivation: DeactivationHandler<Value> | undefined;
305
+ scope: BindingScope;
306
+ }
307
+
308
+ /**
309
+ * Narrows a registered binding to the fields a fluent chain may still refine.
310
+ *
311
+ * @since 0.5.0-canary.8
312
+ */
313
+ export function refinableFields<Value>(binding: Binding<Value>): RefinableBindingFields<Value> {
314
+ return binding as RefinableBindingFields<Value>;
315
+ }
316
+
317
+ /**
318
+ * Drops the memoized resolution frame, for a refinement that changes what the frame reports.
319
+ *
320
+ * @remarks `scope` is the only field a chain writes in place that the frame derives from — a
321
+ * re-slot builds a fresh binding, whose frame starts empty anyway.
322
+ *
323
+ * @since 0.5.0-canary.9
324
+ */
325
+ export function clearBindingFrame<Value>(binding: Binding<Value>): void {
326
+ (binding as { frame: ResolutionFrame | undefined }).frame = undefined;
327
+ }
328
+
207
329
  // ── Builder interfaces ────────────────────────────────────────────────────────
208
330
 
209
331
  /**
@@ -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
  */