@codefast/di 0.10.1 → 0.12.0

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 (88) hide show
  1. package/CHANGELOG.md +376 -0
  2. package/README.md +94 -17
  3. package/dist/{ambient/active-container.d.ts → ambient-container.d.ts} +7 -2
  4. package/dist/{ambient/active-container.js → ambient-container.js} +6 -1
  5. package/dist/container/{binding-builders.d.ts → binding-chain.d.ts} +27 -16
  6. package/dist/container/binding-chain.js +254 -0
  7. package/dist/container/container.d.ts +11 -2
  8. package/dist/container/container.js +293 -73
  9. package/dist/core/binding-builders.d.ts +114 -0
  10. package/dist/core/binding-declaration.d.ts +127 -0
  11. package/dist/core/binding-declaration.js +187 -0
  12. package/dist/core/binding.d.ts +62 -138
  13. package/dist/core/binding.js +54 -17
  14. package/dist/core/module.d.ts +8 -5
  15. package/dist/core/module.js +17 -3
  16. package/dist/core/registry.d.ts +17 -28
  17. package/dist/core/registry.js +92 -133
  18. package/dist/core/state-epoch.d.ts +18 -1
  19. package/dist/core/state-epoch.js +17 -0
  20. package/dist/core/tag.js +1 -1
  21. package/dist/decorators/inject.js +6 -5
  22. package/dist/decorators/injectable.js +4 -2
  23. package/dist/decorators/{lifecycle-decorators.js → lifecycle.js} +9 -3
  24. package/dist/decorators/metadata-record.d.ts +9 -0
  25. package/dist/decorators/metadata-record.js +20 -0
  26. package/dist/{errors/errors.d.ts → errors.d.ts} +84 -9
  27. package/dist/{errors/errors.js → errors.js} +101 -17
  28. package/dist/index.d.ts +14 -10
  29. package/dist/index.js +7 -6
  30. package/dist/injection/{resolve-options.js → dependency-slot.js} +6 -4
  31. package/dist/injection/descriptor.d.ts +1 -1
  32. package/dist/injection/descriptor.js +3 -7
  33. package/dist/introspection/dependency-graph.d.ts +8 -3
  34. package/dist/introspection/dependency-graph.js +48 -26
  35. package/dist/introspection/explanation.d.ts +101 -0
  36. package/dist/introspection/explanation.js +151 -0
  37. package/dist/introspection/graph-adapters/reactflow.js +6 -4
  38. package/dist/introspection/inspector.d.ts +9 -2
  39. package/dist/introspection/inspector.js +30 -28
  40. package/dist/lifecycle/{lifecycle-manager.d.ts → hooks.d.ts} +9 -2
  41. package/dist/lifecycle/{lifecycle-manager.js → hooks.js} +59 -30
  42. package/dist/lifecycle/{scope-manager.d.ts → scopes.d.ts} +3 -5
  43. package/dist/lifecycle/{scope-manager.js → scopes.js} +36 -29
  44. package/dist/metadata/{metadata-reader-token.d.ts → reader-token.d.ts} +1 -1
  45. package/dist/metadata/{symbol-metadata-reader.d.ts → symbol-reader.d.ts} +1 -1
  46. package/dist/metadata/{symbol-metadata-reader.js → symbol-reader.js} +1 -1
  47. package/dist/metadata/{metadata-types.d.ts → types.d.ts} +1 -1
  48. package/dist/metadata/types.js +1 -0
  49. package/dist/metadata/verify.d.ts +43 -0
  50. package/dist/metadata/verify.js +133 -0
  51. package/dist/metadata/{verifying-metadata-reader.d.ts → verifying-reader.d.ts} +5 -4
  52. package/dist/metadata/verifying-reader.js +53 -0
  53. package/dist/resolution/async-fan-out.d.ts +12 -0
  54. package/dist/resolution/async-fan-out.js +26 -0
  55. package/dist/resolution/cache/activation-need.d.ts +1 -2
  56. package/dist/resolution/cache/activation-need.js +16 -19
  57. package/dist/resolution/cache/class-introspector.d.ts +38 -36
  58. package/dist/resolution/cache/class-introspector.js +79 -159
  59. package/dist/resolution/cache/{binding-lookup-cache.d.ts → lookup.d.ts} +7 -9
  60. package/dist/resolution/cache/{binding-lookup-cache.js → lookup.js} +70 -25
  61. package/dist/resolution/context.d.ts +16 -24
  62. package/dist/resolution/context.js +47 -56
  63. package/dist/resolution/path.d.ts +128 -0
  64. package/dist/resolution/path.js +167 -0
  65. package/dist/resolution/plan/{plan-codegen.d.ts → codegen.d.ts} +5 -4
  66. package/dist/resolution/plan/{plan-codegen.js → codegen.js} +58 -32
  67. package/dist/resolution/plan/{instantiation-plan.d.ts → compiler.d.ts} +1 -1
  68. package/dist/resolution/plan/{instantiation-plan.js → compiler.js} +64 -24
  69. package/dist/resolution/resolver.d.ts +11 -12
  70. package/dist/resolution/resolver.js +573 -356
  71. package/dist/resolution/select/{binding-select.d.ts → candidates.d.ts} +30 -0
  72. package/dist/resolution/select/{binding-select.js → candidates.js} +51 -10
  73. package/dist/resolution/select/constraints.js +1 -1
  74. package/package.json +87 -67
  75. package/dist/container/binding-builders.js +0 -305
  76. package/dist/core/binding-scope.d.ts +0 -12
  77. package/dist/core/binding-scope.js +0 -12
  78. package/dist/metadata/verifying-metadata-reader.js +0 -31
  79. package/dist/resolution/path/resolution-path.d.ts +0 -93
  80. package/dist/resolution/path/resolution-path.js +0 -116
  81. /package/dist/{metadata/metadata-types.js → core/binding-builders.js} +0 -0
  82. /package/dist/decorators/{lifecycle-decorators.d.ts → lifecycle.d.ts} +0 -0
  83. /package/dist/injection/{resolve-options.d.ts → dependency-slot.d.ts} +0 -0
  84. /package/dist/{errors → introspection}/diagnostics.d.ts +0 -0
  85. /package/dist/{errors → introspection}/diagnostics.js +0 -0
  86. /package/dist/metadata/{metadata-keys.d.ts → keys.d.ts} +0 -0
  87. /package/dist/metadata/{metadata-keys.js → keys.js} +0 -0
  88. /package/dist/metadata/{metadata-reader-token.js → reader-token.js} +0 -0
@@ -1,7 +1,7 @@
1
1
  import type { BindingTag, TagKeyMask } from "#core/tag";
2
2
  import type { Token } from "#core/token";
3
- import type { ActivationHandler, BindingIdentifier, BindingScope, Constructor, DeactivationHandler, ResolutionContext, ResolutionFrame, BindingConstraint } from "#core/types";
4
- import type { InjectableDependency, InjectionDescriptor, ResolvedDependencyValue } from "#injection/descriptor";
3
+ import type { BindingConstraint, BindingIdentifier, BindingScope, Constructor, ResolutionContext, ResolutionFrame } from "#core/types";
4
+ import type { InjectionDescriptor } from "#injection/descriptor";
5
5
  /**
6
6
  * The criterion set a binding registers under and a request matches against.
7
7
  *
@@ -32,6 +32,14 @@ export declare function createBindingSlot(tags: ReadonlyArray<BindingTag>): Bind
32
32
  * @since 0.3.16-canary.0
33
33
  */
34
34
  export declare function bindingSlotEquals(left: BindingSlot, right: BindingSlot): boolean;
35
+ /**
36
+ * Returns the slot with one criterion added, replacing any earlier criterion of the same key.
37
+ *
38
+ * @remarks One criterion per key: re-tagging a key replaces it rather than asking for both values.
39
+ *
40
+ * @since 0.11.0
41
+ */
42
+ export declare function withSlotCriterion(slot: BindingSlot, criterion: BindingTag): BindingSlot;
35
43
  /**
36
44
  * Cached singleton absent — distinguishes "not resolved yet" from a cached `undefined`.
37
45
  *
@@ -48,6 +56,15 @@ export declare const NO_INSTANCE: unique symbol;
48
56
  * @since 0.3.16-canary.0
49
57
  */
50
58
  export declare const DEFAULT_BINDING_SLOT: BindingSlot;
59
+ /**
60
+ * Renders a tag value for a diagnostic, never throwing.
61
+ *
62
+ * @remarks A tag value is caller data — a bigint, a null-prototype object, a throwing `toString` —
63
+ * so stringifying it must not become the error that masks the real one.
64
+ *
65
+ * @since 0.11.0
66
+ */
67
+ export declare function stringifyTagValue(value: unknown): string;
51
68
  /**
52
69
  * Formats a slot for diagnostics — `default`, or its `name:`/`tag:` parts.
53
70
  *
@@ -57,12 +74,13 @@ export declare function bindingSlotToString(slot: BindingSlot): string;
57
74
  interface BindingBase<Value> {
58
75
  readonly identifier: BindingIdentifier;
59
76
  /**
60
- * True while this binding's factory is executing on the current synchronous call stack.
77
+ * True while this binding is being resolved on the current synchronous call stack.
61
78
  *
62
- * @remarks Both cycle guards that can use an `O(1)` flag read this — the sync transient-dynamic
63
- * lane and the async cascade lane — because synchronous code does not interleave, so the flag *is*
64
- * exact path membership. Not optional: the binding builder always initializes it, and a field that
65
- * may be absent is a field that can cost the shared hidden class. Resolver-owned; callers never set it.
79
+ * @remarks Every synchronous cycle guard reads this flag and nothing else: synchronous code does not
80
+ * interleave, so the flag *is* exact path membership at any depth. The async lanes hold it only for a
81
+ * factory's synchronous prefix, or for the seeded path a synchronous call from an async level runs
82
+ * over. Not optional: the binding builder always initializes it, and a field that may be absent is a
83
+ * field that can cost the shared hidden class. Resolver-owned; callers never set it.
66
84
  */
67
85
  inFlight: boolean;
68
86
  /**
@@ -73,6 +91,21 @@ interface BindingBase<Value> {
73
91
  * @remarks Resolver-owned bookkeeping — `registry.add` normalizes it, so callers never set it.
74
92
  */
75
93
  frame: ResolutionFrame | undefined;
94
+ /**
95
+ * The context a transient factory root is handed, built once per binding: the root's path is its own frame
96
+ * alone, so every resolve of the root reads the same one.
97
+ *
98
+ * @remarks Resolver-owned bookkeeping, cleared with the frame it is built over.
99
+ */
100
+ rootContext: ResolutionContext | undefined;
101
+ /**
102
+ * The activation need last computed for this binding, stamped with the versions it was computed
103
+ * under, or {@link NO_ACTIVATION_STAMP}.
104
+ *
105
+ * @remarks Resolver-owned bookkeeping: a field the level reads beats a per-resolver map that a
106
+ * container resolving each binding once would build and never read again.
107
+ */
108
+ activationStamp: number;
76
109
  /**
77
110
  * Cached singleton instance, or {@link NO_INSTANCE}.
78
111
  *
@@ -80,6 +113,13 @@ interface BindingBase<Value> {
80
113
  * a field read replaces a keyed lookup on the hottest resolve shape there is.
81
114
  */
82
115
  instance: unknown;
116
+ /**
117
+ * The key every container's scoped cache files this binding's instance under.
118
+ *
119
+ * @remarks Chain-owned. It starts as the binding's id, and a scope verb that changes the scope mints a new one, so
120
+ * a scoped instance any child cached under an earlier scope is never found again. The id itself stays stable.
121
+ */
122
+ scopedCacheKey: BindingIdentifier;
83
123
  readonly token: Token<Value> | Constructor<Value>;
84
124
  readonly slot: BindingSlot;
85
125
  readonly predicate?: BindingConstraint | undefined;
@@ -188,150 +228,34 @@ export interface AliasBinding<Value> extends BindingBase<Value> {
188
228
  */
189
229
  export type Binding<Value = unknown> = ClassBinding<Value> | DynamicBinding<Value> | DynamicAsyncBinding<Value> | ResolvedBinding<Value> | ResolvedAsyncBinding<Value> | ConstantBinding<Value> | AliasBinding<Value>;
190
230
  /**
191
- * Returns a process-unique identifier for a new binding.
192
- *
193
- * @since 0.3.16-canary.0
194
- */
195
- export declare function generateBindingId(): BindingIdentifier;
196
- /**
197
- * Writable view of the one selection field a chain may refine without re-registering: nothing
198
- * indexes on the predicate, so the registry rewrites it in place and re-homes the binding itself.
199
- *
200
- * @since 0.10.0
201
- */
202
- export interface PredicateField {
203
- predicate: BindingConstraint | undefined;
204
- }
205
- /**
206
- * Narrows a registered binding to its predicate for the registry to rewrite.
207
- *
208
- * @since 0.10.0
209
- */
210
- export declare function writablePredicate(binding: Binding): PredicateField;
211
- /**
212
- * Writable view of collection membership, which the registry sets because it decides the binding's map.
213
- *
214
- * @since 0.10.0
215
- */
216
- export interface MembershipField {
217
- isMany: boolean;
218
- }
219
- /**
220
- * Narrows a registered binding to its membership flag for the registry to set.
221
- *
222
- * @since 0.10.0
223
- */
224
- export declare function writableMembership(binding: Binding): MembershipField;
225
- /**
226
- * Drops the memoized resolution frame, for a refinement that changes what the frame reports.
227
- *
228
- * @remarks The frame derives from `scope` and `slot`, both of which a chain now writes in place on
229
- * the registered object, so every such refinement clears it.
230
- *
231
- * @since 0.5.0-canary.9
232
- */
233
- export declare function clearBindingFrame<Value>(binding: Binding<Value>): void;
234
- /**
235
- * Common slot-constraint + id methods shared by all concrete binding builders.
236
- *
237
- * @since 0.3.16-canary.0
238
- */
239
- export interface SlotConstrainedBuilder<Names extends string = string> {
240
- /** Narrows the binding to requests the predicate accepts, evaluated on every resolve. */
241
- when(predicate: BindingConstraint): this;
242
- /** Declares the binding's slot name, one of the names the token declares. */
243
- whenNamed(name: Names): this;
244
- /** Declares one criterion of the binding's slot, replacing any earlier criterion of the same key. */
245
- whenTagged(criterion: BindingTag): this;
246
- /** Keeps the binding on the default slot, the one an unconstrained request selects. */
247
- whenDefault(): this;
248
- /**
249
- * Makes the binding a collection member: one of several the token's `resolveAll` returns, never
250
- * what a single `resolve` selects, and outside slot last-wins. It keeps the default slot.
251
- */
252
- many(): this;
253
- /** The identifier this binding is registered under. */
254
- id(): BindingIdentifier;
255
- }
256
- /**
257
- * The `to*` step of the fluent chain, choosing what a token resolves to.
231
+ * The scope a binding resolves under.
258
232
  *
259
- * @since 0.3.16-canary.0
260
- */
261
- export interface BindToBuilder<Value, Names extends string = string> {
262
- to(type: Constructor<Value>): BindingBuilder<Value, Names>;
263
- toSelf(): BindingBuilder<Value, Names>;
264
- toConstantValue(value: Value): ConstantBindingBuilder<Value, Names>;
265
- toDynamic(factory: (ctx: ResolutionContext) => Value): BindingBuilder<Value, Names>;
266
- toDynamicAsync(factory: (ctx: ResolutionContext) => Promise<Value>): BindingBuilder<Value, Names>;
267
- toResolved<const Deps extends ReadonlyArray<InjectableDependency>>(factory: (...args: {
268
- [K in keyof Deps]: ResolvedDependencyValue<NoInfer<Deps>[K]>;
269
- }) => Value, deps: Deps): BindingBuilder<Value, Names>;
270
- toResolvedAsync<const Deps extends ReadonlyArray<InjectableDependency>>(factory: (...args: {
271
- [K in keyof Deps]: ResolvedDependencyValue<NoInfer<Deps>[K]>;
272
- }) => Promise<Value>, deps: Deps): BindingBuilder<Value, Names>;
273
- toAlias(target: Token<Value> | Constructor<Value>): AliasBindingBuilder<Names>;
274
- }
275
- /**
276
- * The scope-selection step of the fluent chain.
233
+ * @remarks Every kind declares one — an alias declares `transient`, since it defers scoping to the
234
+ * binding it points at — so this is a field read, kept as a named function because it is the
235
+ * vocabulary validation and introspection speak.
277
236
  *
278
237
  * @since 0.3.16-canary.0
279
238
  */
280
- export interface BindingBuilder<Value, Names extends string = string> extends SlotConstrainedBuilder<Names> {
281
- singleton(): SingletonBindingBuilder<Value>;
282
- transient(): TransientBindingBuilder<Value>;
283
- scoped(): ScopedBindingBuilder<Value>;
284
- }
239
+ export declare function effectiveBindingScope(binding: Binding): BindingScope;
285
240
  /**
286
- * The fluent chain for a constant — lifecycle hooks only, since the scope is fixed.
287
- *
288
- * @since 0.3.16-canary.0
289
- */
290
- export interface ConstantBindingBuilder<Value, Names extends string = string> extends SlotConstrainedBuilder<Names> {
291
- onActivation(fn: ActivationHandler<Value>): SingletonLifecycleBuilder<Value>;
292
- onDeactivation(fn: DeactivationHandler<Value>): SingletonLifecycleBuilder<Value>;
293
- }
294
- /**
295
- * The fluent chain for an alias — slot constraints only, since scoping belongs to the target.
241
+ * Returns a process-unique identifier for a new binding.
296
242
  *
297
243
  * @since 0.3.16-canary.0
298
244
  */
299
- export interface AliasBindingBuilder<Names extends string = string> extends SlotConstrainedBuilder<Names> {
300
- }
245
+ export declare function generateBindingId(): BindingIdentifier;
301
246
  /**
302
- * The fluent chain after `singleton()`, where both lifecycle hooks stay available.
247
+ * The stamp of a binding whose activation need has not been computed under the current versions.
303
248
  *
304
- * @since 0.3.16-canary.0
249
+ * @since 0.11.0
305
250
  */
306
- export interface SingletonBindingBuilder<Value> {
307
- onActivation(fn: ActivationHandler<Value>): this;
308
- onDeactivation(fn: DeactivationHandler<Value>): this;
309
- id(): BindingIdentifier;
310
- }
251
+ export declare const NO_ACTIVATION_STAMP = -1;
311
252
  /**
312
- * The fluent chain after `transient()`, where activation is the one lifecycle hook offered.
253
+ * Drops the memoized resolution frame, for a scope change that alters what the frame reports.
313
254
  *
314
- * @since 0.3.16-canary.0
315
- */
316
- export interface TransientBindingBuilder<Value> {
317
- onActivation(fn: ActivationHandler<Value>): this;
318
- id(): BindingIdentifier;
319
- }
320
- /**
321
- * The fluent chain after `scoped()`, sharing the `transient()` surface.
255
+ * @remarks The frame derives from `scope` and `slot`; the slot is final once the binding registers, so
256
+ * only a scope written in place on the registered object clears it.
322
257
  *
323
- * @since 0.3.16-canary.0
324
- */
325
- export interface ScopedBindingBuilder<Value> extends TransientBindingBuilder<Value> {
326
- }
327
- /**
328
- * The fluent chain a constant enters once a lifecycle hook is added.
329
- *
330
- * @since 0.3.16-canary.0
258
+ * @since 0.5.0-canary.9
331
259
  */
332
- export interface SingletonLifecycleBuilder<Value> {
333
- onActivation(fn: ActivationHandler<Value>): this;
334
- onDeactivation(fn: DeactivationHandler<Value>): this;
335
- id(): BindingIdentifier;
336
- }
260
+ export declare function clearBindingFrame<Value>(binding: Binding<Value>): void;
337
261
  export {};
@@ -36,6 +36,24 @@ export function bindingSlotEquals(left, right) {
36
36
  }
37
37
  return true;
38
38
  }
39
+ /**
40
+ * Returns the slot with one criterion added, replacing any earlier criterion of the same key.
41
+ *
42
+ * @remarks One criterion per key: re-tagging a key replaces it rather than asking for both values.
43
+ *
44
+ * @since 0.11.0
45
+ */
46
+ export function withSlotCriterion(slot, criterion) {
47
+ const tags = [...slot.tags];
48
+ const existingIndex = tags.findIndex((existing) => existing.key === criterion.key);
49
+ if (existingIndex === -1) {
50
+ tags.push(criterion);
51
+ }
52
+ else {
53
+ tags[existingIndex] = criterion;
54
+ }
55
+ return createBindingSlot(tags);
56
+ }
39
57
  /**
40
58
  * Cached singleton absent — distinguishes "not resolved yet" from a cached `undefined`.
41
59
  *
@@ -52,6 +70,22 @@ export const NO_INSTANCE = Symbol("di:no-instance");
52
70
  * @since 0.3.16-canary.0
53
71
  */
54
72
  export const DEFAULT_BINDING_SLOT = { name: undefined, tags: Object.freeze([]), keyMask: NO_TAG_KEYS };
73
+ /**
74
+ * Renders a tag value for a diagnostic, never throwing.
75
+ *
76
+ * @remarks A tag value is caller data — a bigint, a null-prototype object, a throwing `toString` —
77
+ * so stringifying it must not become the error that masks the real one.
78
+ *
79
+ * @since 0.11.0
80
+ */
81
+ export function stringifyTagValue(value) {
82
+ try {
83
+ return String(value);
84
+ }
85
+ catch {
86
+ return "<unprintable>";
87
+ }
88
+ }
55
89
  /**
56
90
  * Formats a slot for diagnostics — `default`, or its `name:`/`tag:` parts.
57
91
  *
@@ -70,10 +104,22 @@ export function bindingSlotToString(slot) {
70
104
  if (criterion.key === slotName) {
71
105
  continue;
72
106
  }
73
- parts.push(`tag:${criterion.key.name}=${String(criterion.value)}`);
107
+ parts.push(`tag:${criterion.key.name}=${stringifyTagValue(criterion.value)}`);
74
108
  }
75
109
  return parts.join(",");
76
110
  }
111
+ /**
112
+ * The scope a binding resolves under.
113
+ *
114
+ * @remarks Every kind declares one — an alias declares `transient`, since it defers scoping to the
115
+ * binding it points at — so this is a field read, kept as a named function because it is the
116
+ * vocabulary validation and introspection speak.
117
+ *
118
+ * @since 0.3.16-canary.0
119
+ */
120
+ export function effectiveBindingScope(binding) {
121
+ return binding.scope;
122
+ }
77
123
  // ── ID generation ────────────────────────────────────────────────────────────────────────────────────────────────────
78
124
  let bindingIdCounter = 0;
79
125
  /**
@@ -86,29 +132,20 @@ export function generateBindingId() {
86
132
  return bindingIdCounter;
87
133
  }
88
134
  /**
89
- * Narrows a registered binding to its predicate for the registry to rewrite.
90
- *
91
- * @since 0.10.0
92
- */
93
- export function writablePredicate(binding) {
94
- return binding;
95
- }
96
- /**
97
- * Narrows a registered binding to its membership flag for the registry to set.
135
+ * The stamp of a binding whose activation need has not been computed under the current versions.
98
136
  *
99
- * @since 0.10.0
137
+ * @since 0.11.0
100
138
  */
101
- export function writableMembership(binding) {
102
- return binding;
103
- }
139
+ export const NO_ACTIVATION_STAMP = -1;
104
140
  /**
105
- * Drops the memoized resolution frame, for a refinement that changes what the frame reports.
141
+ * Drops the memoized resolution frame, for a scope change that alters what the frame reports.
106
142
  *
107
- * @remarks The frame derives from `scope` and `slot`, both of which a chain now writes in place on
108
- * the registered object, so every such refinement clears it.
143
+ * @remarks The frame derives from `scope` and `slot`; the slot is final once the binding registers, so
144
+ * only a scope written in place on the registered object clears it.
109
145
  *
110
146
  * @since 0.5.0-canary.9
111
147
  */
112
148
  export function clearBindingFrame(binding) {
113
149
  binding.frame = undefined;
150
+ binding.rootContext = undefined;
114
151
  }
@@ -1,11 +1,12 @@
1
- import type { BindToBuilder } from "#core/binding";
1
+ import type { BindToBuilder } from "#core/binding-builders";
2
+ import type { BindingDeclaration, DeclaredBinding } from "#core/binding-declaration";
2
3
  import type { Token } from "#core/token";
3
4
  import type { Constructor } from "#core/types";
4
5
  declare const SYNC_MODULE_BRAND: unique symbol;
5
6
  declare const ASYNC_MODULE_BRAND: unique symbol;
6
7
  /**
7
- * Key for the module's setup callback. A symbol (not exported from the package root)
8
- * keeps the container-only member out of consumer-facing autocomplete entirely.
8
+ * Key for what a module applies when it loads: its setup callback, or a declared module's
9
+ * declarations. A symbol (not exported from the package root) keeps it out of autocomplete.
9
10
  *
10
11
  * @since 0.5.0-canary.7
11
12
  */
@@ -18,7 +19,7 @@ export declare const MODULE_SETUP: unique symbol;
18
19
  export interface SyncModule {
19
20
  readonly name: string;
20
21
  readonly [SYNC_MODULE_BRAND]: true;
21
- readonly [MODULE_SETUP]: (builder: ModuleBuilder) => void;
22
+ readonly [MODULE_SETUP]: ((builder: ModuleBuilder) => void) | ReadonlyArray<DeclaredBinding>;
22
23
  }
23
24
  /**
24
25
  * A named group of bindings whose setup is async, applied via `loadAsync()`.
@@ -55,6 +56,7 @@ export interface AsyncModuleBuilder {
55
56
  */
56
57
  export declare const SyncModule: {
57
58
  create(name: string, setup: (builder: ModuleBuilder) => void): SyncModule;
59
+ fromBindings(name: string, declarations: ReadonlyArray<BindingDeclaration>): SyncModule;
58
60
  };
59
61
  /**
60
62
  * The companion factory that creates `AsyncModule` values.
@@ -65,12 +67,13 @@ export declare const AsyncModule: {
65
67
  create(name: string, setup: (builder: AsyncModuleBuilder) => Promise<void>): AsyncModule;
66
68
  };
67
69
  /**
68
- * The unified module factory — `create` for sync modules, `createAsync` for async ones.
70
+ * The unified module factory — `create` and `fromBindings` for sync modules, `createAsync` for async ones.
69
71
  *
70
72
  * @since 0.3.16-canary.0
71
73
  */
72
74
  export declare const Module: {
73
75
  create(name: string, setup: (builder: ModuleBuilder) => void): SyncModule;
76
+ fromBindings(name: string, declarations: ReadonlyArray<BindingDeclaration>): SyncModule;
74
77
  createAsync(name: string, setup: (builder: AsyncModuleBuilder) => Promise<void>): AsyncModule;
75
78
  };
76
79
  /**
@@ -1,9 +1,10 @@
1
+ import { asDeclaredBinding } from "#core/binding-declaration";
1
2
  // ── Branded types (runtime symbols for branding) ─────────────────────────────────────────────────────────────────────
2
3
  const SYNC_MODULE_BRAND = Symbol("di:sync-module");
3
4
  const ASYNC_MODULE_BRAND = Symbol("di:async-module");
4
5
  /**
5
- * Key for the module's setup callback. A symbol (not exported from the package root)
6
- * keeps the container-only member out of consumer-facing autocomplete entirely.
6
+ * Key for what a module applies when it loads: its setup callback, or a declared module's
7
+ * declarations. A symbol (not exported from the package root) keeps it out of autocomplete.
7
8
  *
8
9
  * @since 0.5.0-canary.7
9
10
  */
@@ -22,6 +23,16 @@ export const SyncModule = {
22
23
  [MODULE_SETUP]: setup,
23
24
  };
24
25
  },
26
+ fromBindings(name, declarations) {
27
+ // Copied, so a later write to the caller's list cannot change what the module loads. Not frozen:
28
+ // every load walks this list, and V8 reads a frozen array's elements on a slower path.
29
+ const declared = declarations.map((declaration, index) => asDeclaredBinding(declaration, name, index));
30
+ return {
31
+ name,
32
+ [SYNC_MODULE_BRAND]: true,
33
+ [MODULE_SETUP]: declared,
34
+ };
35
+ },
25
36
  };
26
37
  /**
27
38
  * The companion factory that creates `AsyncModule` values.
@@ -39,7 +50,7 @@ export const AsyncModule = {
39
50
  };
40
51
  // ── Module — unified API ─────────────────────────────────────────────────────────────────────────────────────────────
41
52
  /**
42
- * The unified module factory — `create` for sync modules, `createAsync` for async ones.
53
+ * The unified module factory — `create` and `fromBindings` for sync modules, `createAsync` for async ones.
43
54
  *
44
55
  * @since 0.3.16-canary.0
45
56
  */
@@ -47,6 +58,9 @@ export const Module = {
47
58
  create(name, setup) {
48
59
  return SyncModule.create(name, setup);
49
60
  },
61
+ fromBindings(name, declarations) {
62
+ return SyncModule.fromBindings(name, declarations);
63
+ },
50
64
  createAsync(name, setup) {
51
65
  return AsyncModule.create(name, setup);
52
66
  },
@@ -1,11 +1,9 @@
1
1
  import type { Binding } from "#core/binding";
2
2
  import type { BindingTag } from "#core/tag";
3
3
  import type { Token } from "#core/token";
4
- import type { BindingConstraint, BindingIdentifier, Constructor } from "#core/types";
4
+ import type { BindingIdentifier, Constructor } from "#core/types";
5
5
  /**
6
- * One container's binding store, indexed by token, binding id, and slot for fast lookup.
7
- *
8
- * @since 0.3.16-canary.0
6
+ * @since 0.11.0
9
7
  */
10
8
  export declare class BindingRegistry {
11
9
  #private;
@@ -13,6 +11,8 @@ export declare class BindingRegistry {
13
11
  get version(): number;
14
12
  /** Whether a constant has ever been registered here, and so whether teardown has anything to sweep. */
15
13
  get hasHeldConstantBinding(): boolean;
14
+ /** Whether an alias has ever been registered here, and so whether a candidate list can hold one. */
15
+ get hasHeldAlias(): boolean;
16
16
  /** Whether the deferred tagged-slot index has had to be built. */
17
17
  get isTaggedIndexBuilt(): boolean;
18
18
  /** Whether an id-keyed operation has had to build the id index. */
@@ -49,8 +49,6 @@ export declare class BindingRegistry {
49
49
  * to ask, and a lone binding's one-element list is never materialised here.
50
50
  */
51
51
  getRecorded(token: Token<unknown> | Constructor): ReadonlyArray<Binding>;
52
- /** How many bindings a token holds, without materialising a lone binding's list. */
53
- countBindings(token: Token<unknown> | Constructor): number;
54
52
  /** Get binding by ID. */
55
53
  getById(id: BindingIdentifier): Binding | undefined;
56
54
  /** Check if any binding exists for token. */
@@ -59,8 +57,6 @@ export declare class BindingRegistry {
59
57
  allBindings(): ReadonlyArray<Binding>;
60
58
  /** Remove all bindings. Returns all removed. */
61
59
  clear(): ReadonlyArray<Binding>;
62
- /** Whether a slot-based binding currently occupies `binding`'s slot, so adding it would displace. */
63
- hasSlotOccupant(binding: Binding): boolean;
64
60
  /**
65
61
  * The binding indexed under one criterion.
66
62
  *
@@ -77,30 +73,23 @@ export declare class BindingRegistry {
77
73
  * against the request — first-criterion bucketing only guarantees each candidate appears once.
78
74
  */
79
75
  getMultiTagged(token: Token<unknown> | Constructor, criterion: BindingTag): ReadonlyArray<Binding> | undefined;
80
- /** A token's lone default-slot binding — the first read of every synchronous resolve. */
81
- getFastDefault(token: Token<unknown> | Constructor): Binding | undefined;
82
76
  /**
83
- * Takes a live binding out of every index, lets `rewrite` change its slot or predicate, and reports
84
- * whether it was live; the caller registers it again with `add`.
77
+ * Whether any binding for the token carries a `when()` predicate.
85
78
  *
86
- * @remarks The binding keeps its object and its id, so the id index needs no touch and nothing
87
- * that holds the object has to be told. `false` means the binding was unbound or displaced since
88
- * it registered, and a refinement must not resurrect it.
89
- */
90
- reslot(binding: Binding, rewrite: () => void): boolean;
91
- /**
92
- * Marks a live binding as a collection member in place, moving it out of the lone map: a member is
93
- * never the token's lone default answer, and nothing else indexes on membership.
79
+ * @remarks Only a token that keeps a record can hold a predicate — a lone binding is default-slot
80
+ * with none — so an index fast lane consults this to decline to full selection when the
81
+ * more-specific rule's predicate step could apply.
94
82
  */
95
- setMany(binding: Binding): void;
83
+ hasPredicateCandidate(token: Token<unknown> | Constructor): boolean;
84
+ /** The binding holding a token's default slot — lone or recorded — or `undefined` when none does. */
85
+ getDefaultSlotBinding(token: Token<unknown> | Constructor): Binding | undefined;
96
86
  /**
97
- * Adds a predicate to a live binding in place.
87
+ * The token's default-slot alias, which forwards a request's criteria to its target, or `undefined`.
98
88
  *
99
- * @remarks Only ever narrows — `when()` composes with any existing predicate — so the argument is
100
- * never absent. Nothing indexes on the predicate, so the binding object and its id stay; a lone
101
- * binding moves to a record because the lone map holds default-slot bindings with no predicate.
89
+ * @remarks A registry that never held an alias answers without probing, so a request missing every slot of a
90
+ * token pays for forwarding only where forwarding can happen.
102
91
  */
103
- setPredicate(binding: Binding, predicate: BindingConstraint): void;
104
- /** Summarize available slot strings for a token (for error messages). */
105
- availableSlotStrings(token: Token<unknown> | Constructor): Array<string>;
92
+ defaultSlotAlias(token: Token<unknown> | Constructor): Binding | undefined;
93
+ /** A token's lone default-slot binding — the first read of every synchronous resolve. */
94
+ getFastDefault(token: Token<unknown> | Constructor): Binding | undefined;
106
95
  }