@codefast/di 0.3.13 → 0.3.14-canary.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.
- package/CHANGELOG.md +20 -0
- package/README.md +262 -235
- package/dist/binding-select.d.mts +17 -6
- package/dist/binding-select.mjs +17 -6
- package/dist/binding.d.mts +148 -23
- package/dist/binding.mjs +103 -14
- package/dist/constraints.d.mts +18 -3
- package/dist/constraints.mjs +18 -3
- package/dist/container.d.mts +81 -26
- package/dist/container.mjs +91 -3
- package/dist/decorators/inject.d.mts +40 -9
- package/dist/decorators/inject.mjs +50 -11
- package/dist/decorators/injectable.d.mts +2 -1
- package/dist/decorators/injectable.mjs +14 -2
- package/dist/decorators/lifecycle-decorators.d.mts +16 -4
- package/dist/decorators/lifecycle-decorators.mjs +16 -4
- package/dist/dependency-graph.d.mts +31 -8
- package/dist/dependency-graph.mjs +42 -8
- package/dist/errors.d.mts +124 -13
- package/dist/errors.mjs +126 -18
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +2 -2
- package/dist/inspector.d.mts +38 -14
- package/dist/inspector.mjs +36 -15
- package/dist/lifecycle.d.mts +28 -6
- package/dist/lifecycle.mjs +29 -10
- package/dist/metadata/metadata-keys.d.mts +17 -6
- package/dist/metadata/metadata-keys.mjs +17 -6
- package/dist/metadata/metadata-types.d.mts +29 -5
- package/dist/metadata/param-registry.mjs +6 -0
- package/dist/metadata/symbol-metadata-reader.d.mts +20 -3
- package/dist/metadata/symbol-metadata-reader.mjs +23 -4
- package/dist/module.d.mts +34 -2
- package/dist/module.mjs +19 -0
- package/dist/registry.d.mts +39 -8
- package/dist/registry.mjs +39 -8
- package/dist/resolver.d.mts +107 -12
- package/dist/resolver.mjs +134 -37
- package/dist/scope-validation.d.mts +3 -2
- package/dist/scope-validation.mjs +3 -2
- package/dist/scope.d.mts +34 -6
- package/dist/scope.mjs +38 -13
- package/dist/token.d.mts +9 -2
- package/dist/token.mjs +7 -1
- package/package.json +2 -2
|
@@ -3,19 +3,30 @@ import { RegistryKey } from "./registry.mjs";
|
|
|
3
3
|
import { Binding, ConstraintContext, Constructor, ResolveHint } from "./binding.mjs";
|
|
4
4
|
|
|
5
5
|
//#region src/binding-select.d.ts
|
|
6
|
-
/**
|
|
6
|
+
/**
|
|
7
|
+
* Returns a human-readable label for a token or constructor (used in error messages and graph output).
|
|
8
|
+
*/
|
|
7
9
|
declare function registryKeyLabel(key: Token<unknown> | Constructor<unknown>): string;
|
|
8
10
|
/**
|
|
9
|
-
*
|
|
11
|
+
* Narrows a binding list by applying name/tag hints and `when()` constraint predicates.
|
|
12
|
+
*
|
|
13
|
+
* Filtering order: name filter → tag filter → constraint predicate. Bindings without a
|
|
14
|
+
* constraint predicate always pass the constraint stage. Returns the surviving candidates
|
|
15
|
+
* (may be empty).
|
|
10
16
|
*/
|
|
11
|
-
declare function filterMatchingBindings(bindings: readonly Binding<unknown>[], hint: ResolveHint | undefined, constraintCtx: ConstraintContext | undefined): Binding<unknown>[];
|
|
17
|
+
declare function filterMatchingBindings(bindings: readonly Binding<unknown>[], hint: ResolveHint | undefined, constraintCtx: ConstraintContext | undefined): readonly Binding<unknown>[];
|
|
12
18
|
/**
|
|
13
|
-
*
|
|
14
|
-
*
|
|
19
|
+
* Selects exactly one binding from the provided list, applying hint and constraint filtering.
|
|
20
|
+
*
|
|
21
|
+
* @throws {@link TokenNotBoundError} — `bindings` list is empty, or no candidate survives
|
|
22
|
+
* filtering without a name/tag hint.
|
|
23
|
+
* @throws {@link NoMatchingBindingError} — a name/tag hint was provided but no candidate matched.
|
|
24
|
+
* @throws {@link InternalError} — multiple candidates survive filtering (ambiguous binding).
|
|
15
25
|
*/
|
|
16
26
|
declare function selectBindingForRegistry(bindings: readonly Binding<unknown>[], hint: ResolveHint | undefined, tokenLabel: string, pathLabels: readonly string[], constraintCtx: ConstraintContext | undefined): Binding<unknown>;
|
|
17
27
|
/**
|
|
18
|
-
*
|
|
28
|
+
* Convenience wrapper: looks up bindings for `key` and selects the default (no-hint,
|
|
29
|
+
* no-constraint) binding. Throws {@link TokenNotBoundError} if the key is unregistered.
|
|
19
30
|
*/
|
|
20
31
|
declare function selectDefaultBindingForKey(lookup: (key: RegistryKey) => readonly Binding<unknown>[] | undefined, key: RegistryKey, pathPrefix: readonly string[]): Binding<unknown>;
|
|
21
32
|
//#endregion
|
package/dist/binding-select.mjs
CHANGED
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
import { InternalError, NoMatchingBindingError, TokenNotBoundError } from "./errors.mjs";
|
|
2
2
|
//#region src/binding-select.ts
|
|
3
|
-
/**
|
|
3
|
+
/**
|
|
4
|
+
* Returns a human-readable label for a token or constructor (used in error messages and graph output).
|
|
5
|
+
*/
|
|
4
6
|
function registryKeyLabel(key) {
|
|
5
7
|
if (typeof key === "function") return key.name.length > 0 ? key.name : "(anonymous class)";
|
|
6
8
|
return key.name.trim().length > 0 ? key.name : "(anonymous token)";
|
|
7
9
|
}
|
|
8
10
|
/**
|
|
9
|
-
*
|
|
11
|
+
* Narrows a binding list by applying name/tag hints and `when()` constraint predicates.
|
|
12
|
+
*
|
|
13
|
+
* Filtering order: name filter → tag filter → constraint predicate. Bindings without a
|
|
14
|
+
* constraint predicate always pass the constraint stage. Returns the surviving candidates
|
|
15
|
+
* (may be empty).
|
|
10
16
|
*/
|
|
11
17
|
function filterMatchingBindings(bindings, hint, constraintCtx) {
|
|
12
|
-
let candidates =
|
|
18
|
+
let candidates = bindings;
|
|
13
19
|
if (hint?.name !== void 0) candidates = candidates.filter((binding) => binding.bindingName === hint.name);
|
|
14
20
|
if (hint?.tag !== void 0) {
|
|
15
21
|
const [tagKey, tagValue] = hint.tag;
|
|
@@ -19,8 +25,12 @@ function filterMatchingBindings(bindings, hint, constraintCtx) {
|
|
|
19
25
|
return candidates;
|
|
20
26
|
}
|
|
21
27
|
/**
|
|
22
|
-
*
|
|
23
|
-
*
|
|
28
|
+
* Selects exactly one binding from the provided list, applying hint and constraint filtering.
|
|
29
|
+
*
|
|
30
|
+
* @throws {@link TokenNotBoundError} — `bindings` list is empty, or no candidate survives
|
|
31
|
+
* filtering without a name/tag hint.
|
|
32
|
+
* @throws {@link NoMatchingBindingError} — a name/tag hint was provided but no candidate matched.
|
|
33
|
+
* @throws {@link InternalError} — multiple candidates survive filtering (ambiguous binding).
|
|
24
34
|
*/
|
|
25
35
|
function selectBindingForRegistry(bindings, hint, tokenLabel, pathLabels, constraintCtx) {
|
|
26
36
|
if (bindings.length === 0) throw new TokenNotBoundError(tokenLabel, [...pathLabels]);
|
|
@@ -37,7 +47,8 @@ function selectBindingForRegistry(bindings, hint, tokenLabel, pathLabels, constr
|
|
|
37
47
|
throw new InternalError(`Ambiguous binding for "${tokenLabel}": ${String(candidates.length)} candidates matched after applying ResolveHint (resolution path: ${pathLabels.join(" -> ")})`);
|
|
38
48
|
}
|
|
39
49
|
/**
|
|
40
|
-
*
|
|
50
|
+
* Convenience wrapper: looks up bindings for `key` and selects the default (no-hint,
|
|
51
|
+
* no-constraint) binding. Throws {@link TokenNotBoundError} if the key is unregistered.
|
|
41
52
|
*/
|
|
42
53
|
function selectDefaultBindingForKey(lookup, key, pathPrefix) {
|
|
43
54
|
const label = registryKeyLabel(key);
|
package/dist/binding.d.mts
CHANGED
|
@@ -17,7 +17,9 @@ declare function createBindingIdentifier(): BindingIdentifier;
|
|
|
17
17
|
* Runtime constructor token used as a registry key (no reflection metadata).
|
|
18
18
|
*/
|
|
19
19
|
type Constructor<Value> = abstract new (...args: never[]) => Value;
|
|
20
|
-
/**
|
|
20
|
+
/**
|
|
21
|
+
* Lifetime strategy for a resolved instance.
|
|
22
|
+
*/
|
|
21
23
|
type BindingScope = "singleton" | "transient" | "scoped";
|
|
22
24
|
/**
|
|
23
25
|
* Hint for disambiguating multi-bindings registered against the same token or constructor.
|
|
@@ -26,11 +28,21 @@ type ResolveHint = {
|
|
|
26
28
|
readonly name?: string;
|
|
27
29
|
readonly tag?: readonly [tag: string, value: unknown];
|
|
28
30
|
};
|
|
31
|
+
/**
|
|
32
|
+
* Public alias for {@link ResolveHint} — the `hint` parameter accepted by
|
|
33
|
+
* `Container.resolve`, `Container.resolveAsync`, and related methods.
|
|
34
|
+
* Passes `name` and/or `tag` to select among multi-bindings.
|
|
35
|
+
*/
|
|
29
36
|
type ResolveOptions = ResolveHint;
|
|
30
37
|
/**
|
|
31
38
|
* Snapshot of a binding on the materialization stack (for {@link ConstraintContext}).
|
|
32
39
|
*/
|
|
33
40
|
type ConstraintBindingKind = "constant" | "class" | "dynamic" | "async-dynamic" | "resolved" | "alias";
|
|
41
|
+
/**
|
|
42
|
+
* Snapshot of a single binding on the materialization stack during resolution.
|
|
43
|
+
* Each frame captures enough identity to implement contextual constraints
|
|
44
|
+
* ({@link whenParentIs}, {@link whenAnyAncestorIs}) and captive-dependency detection.
|
|
45
|
+
*/
|
|
34
46
|
type ConstraintParentFrame = {
|
|
35
47
|
readonly registryKey: RegistryKey;
|
|
36
48
|
readonly bindingId: BindingIdentifier;
|
|
@@ -62,9 +74,18 @@ type ConstraintContext = {
|
|
|
62
74
|
type ResolutionContext = {
|
|
63
75
|
readonly resolve: <Value>(token: Token<Value> | Constructor<Value>, hint?: ResolveOptions) => Value;
|
|
64
76
|
readonly resolveAsync: <Value>(token: Token<Value> | Constructor<Value>, hint?: ResolveOptions) => Promise<Value>;
|
|
65
|
-
readonly resolveOptional: <Value>(token: Token<Value> | Constructor<Value>, hint?: ResolveOptions) => Value | undefined; /**
|
|
77
|
+
readonly resolveOptional: <Value>(token: Token<Value> | Constructor<Value>, hint?: ResolveOptions) => Value | undefined; /** Resolves every binding registered for `token` (multi-binding). */
|
|
78
|
+
readonly resolveAll: <Value>(token: Token<Value> | Constructor<Value>, hint?: ResolveOptions) => Value[]; /** Async variant of {@link ResolutionContext.resolveAll}. */
|
|
79
|
+
readonly resolveAllAsync: <Value>(token: Token<Value> | Constructor<Value>, hint?: ResolveOptions) => Promise<Value[]>;
|
|
80
|
+
/**
|
|
81
|
+
* Dependency-graph navigation context — path, materialization stack, parent/ancestor frames.
|
|
82
|
+
*/
|
|
66
83
|
readonly graph: ConstraintContext;
|
|
67
84
|
};
|
|
85
|
+
/**
|
|
86
|
+
* Lifecycle and constraint fields shared by every concrete {@link Binding} variant.
|
|
87
|
+
* Separated from {@link BindingBase} so the builder can accumulate them independently of `id` / `scope`.
|
|
88
|
+
*/
|
|
68
89
|
type BindingLifecycle = {
|
|
69
90
|
readonly bindingName?: string;
|
|
70
91
|
readonly tags: ReadonlyMap<string, unknown>;
|
|
@@ -85,36 +106,51 @@ type ActivationHandler<Value> = (ctx: ResolutionContext, instance: Value) => Val
|
|
|
85
106
|
type DeactivationHandler<Value> = (instance: Value) => void | Promise<void>;
|
|
86
107
|
type BindingBase = BindingLifecycle & {
|
|
87
108
|
readonly id: BindingIdentifier;
|
|
88
|
-
readonly scope: BindingScope;
|
|
109
|
+
readonly scope: BindingScope;
|
|
110
|
+
/**
|
|
111
|
+
* Set when the binding was registered from {@link Module} / {@link AsyncModule} setup.
|
|
112
|
+
*/
|
|
89
113
|
readonly moduleId?: string;
|
|
90
114
|
};
|
|
91
|
-
/**
|
|
115
|
+
/**
|
|
116
|
+
* Binding backed by a pre-existing constant value; always singleton, no construction cost.
|
|
117
|
+
*/
|
|
92
118
|
type ConstantBinding<Value> = BindingBase & {
|
|
93
119
|
readonly kind: "constant";
|
|
94
120
|
readonly value: Value;
|
|
95
121
|
};
|
|
96
|
-
/**
|
|
122
|
+
/**
|
|
123
|
+
* Binding that constructs `implementationClass` via the container's metadata-driven instantiation.
|
|
124
|
+
*/
|
|
97
125
|
type ClassBinding<Value> = BindingBase & {
|
|
98
126
|
readonly kind: "class";
|
|
99
127
|
readonly implementationClass: Constructor<Value>;
|
|
100
128
|
};
|
|
101
|
-
/**
|
|
129
|
+
/**
|
|
130
|
+
* Binding backed by a synchronous factory that receives a {@link ResolutionContext}.
|
|
131
|
+
*/
|
|
102
132
|
type DynamicBinding<Value> = BindingBase & {
|
|
103
133
|
readonly kind: "dynamic";
|
|
104
134
|
readonly factory: (ctx: ResolutionContext) => Value;
|
|
105
135
|
};
|
|
106
|
-
/**
|
|
136
|
+
/**
|
|
137
|
+
* Binding backed by an async factory; must be resolved via `resolveAsync` / `resolveAllAsync`.
|
|
138
|
+
*/
|
|
107
139
|
type AsyncDynamicBinding<Value> = BindingBase & {
|
|
108
140
|
readonly kind: "async-dynamic";
|
|
109
141
|
readonly factory: (ctx: ResolutionContext) => Promise<Value>;
|
|
110
142
|
};
|
|
111
|
-
/**
|
|
143
|
+
/**
|
|
144
|
+
* Binding whose dependencies are declared statically and pre-resolved before the factory is called.
|
|
145
|
+
*/
|
|
112
146
|
type ResolvedBinding<Value> = BindingBase & {
|
|
113
147
|
readonly kind: "resolved";
|
|
114
148
|
readonly dependencyTokens: readonly (Token<unknown> | Constructor<unknown>)[];
|
|
115
149
|
readonly factory: (...args: unknown[]) => Value;
|
|
116
150
|
};
|
|
117
|
-
/**
|
|
151
|
+
/**
|
|
152
|
+
* Binding that forwards resolution to `targetToken`; the container resolves whatever is bound there.
|
|
153
|
+
*/
|
|
118
154
|
type AliasBinding<Value> = BindingBase & {
|
|
119
155
|
readonly kind: "alias";
|
|
120
156
|
readonly targetToken: Token<Value>;
|
|
@@ -132,53 +168,126 @@ type RegistryCallbacks<Value> = {
|
|
|
132
168
|
readonly register?: (binding: Binding<Value>) => void;
|
|
133
169
|
readonly update?: (binding: Binding<Value>) => void;
|
|
134
170
|
};
|
|
171
|
+
/**
|
|
172
|
+
* Fluent builder for registering a single binding against a {@link Token} or {@link Constructor}.
|
|
173
|
+
*
|
|
174
|
+
* A builder has two phases:
|
|
175
|
+
* 1. **Strategy selection** — exactly one `to*()` call (`to`, `toSelf`, `toConstantValue`,
|
|
176
|
+
* `toDynamic`, `toDynamicAsync`, `toResolved`, `toAlias`) that determines how the value is produced.
|
|
177
|
+
* 2. **Refinement chain** — optional calls to `singleton()`, `transient()`, `scoped()`,
|
|
178
|
+
* `onActivation()`, `onDeactivation()`, `whenNamed()`, `whenTagged()`, `when()`, and `id()`.
|
|
179
|
+
*
|
|
180
|
+
* Calling a second `to*()` method throws {@link InternalError}.
|
|
181
|
+
*
|
|
182
|
+
* The builder is created by {@link Container.bind} or by `bind` on {@link ModuleBuilder}.
|
|
183
|
+
* The container injects {@link RegistryCallbacks} so that every strategy selection and
|
|
184
|
+
* refinement is immediately reflected in the live registry.
|
|
185
|
+
*/
|
|
135
186
|
declare class BindingBuilder<Value> {
|
|
136
187
|
protected readonly bindingKey: Token<Value> | Constructor<Value>;
|
|
188
|
+
/**
|
|
189
|
+
* Current resolution strategy; starts as `"unset"` until a `to*()` method is called.
|
|
190
|
+
*/
|
|
137
191
|
private strategy;
|
|
192
|
+
/**
|
|
193
|
+
* Lifetime scope applied to the next binding snapshot; defaults to `"transient"`.
|
|
194
|
+
*/
|
|
138
195
|
private scope;
|
|
196
|
+
/**
|
|
197
|
+
* True after an explicit `.singleton()` / `.transient()` / `.scoped()` call (prevents constant scope change).
|
|
198
|
+
*/
|
|
139
199
|
private isScopeExplicit;
|
|
200
|
+
/**
|
|
201
|
+
* Pre-allocated binding ID set via `id(identifier)` before the first `to*()` call.
|
|
202
|
+
*/
|
|
140
203
|
private explicitId;
|
|
204
|
+
/**
|
|
205
|
+
* Resolve-hint name filter set by `.whenNamed()`.
|
|
206
|
+
*/
|
|
141
207
|
private bindingName;
|
|
208
|
+
/**
|
|
209
|
+
* Tag filters accumulated by successive `.whenTagged()` calls.
|
|
210
|
+
*/
|
|
142
211
|
private readonly tags;
|
|
212
|
+
/**
|
|
213
|
+
* Custom constraint predicates accumulated by `.when()`; all must pass for this binding to be selected.
|
|
214
|
+
*/
|
|
143
215
|
private readonly constraintPredicates;
|
|
144
216
|
private onActivationHandler;
|
|
145
217
|
private onDeactivationHandler;
|
|
218
|
+
/**
|
|
219
|
+
* Set when this binding was created inside a {@link Module} / {@link AsyncModule} setup callback.
|
|
220
|
+
*/
|
|
146
221
|
private readonly moduleId;
|
|
222
|
+
/**
|
|
223
|
+
* The most recently emitted {@link Binding} snapshot; `undefined` before the first `to*()` call.
|
|
224
|
+
*/
|
|
147
225
|
private currentBinding;
|
|
226
|
+
/**
|
|
227
|
+
* Container-injected hooks that sync builder mutations into the live registry.
|
|
228
|
+
*/
|
|
148
229
|
private readonly callbacks;
|
|
149
230
|
constructor(bindingKey: Token<Value> | Constructor<Value>, moduleId?: string, callbacks?: RegistryCallbacks<Value>);
|
|
150
|
-
/**
|
|
231
|
+
/**
|
|
232
|
+
* Binds the token to a concrete implementation class; the container constructs it on demand.
|
|
233
|
+
*/
|
|
151
234
|
to<C extends Constructor<Value>>(implementationClass: C): TransientBindingBuilder<Value>;
|
|
152
|
-
/**
|
|
235
|
+
/**
|
|
236
|
+
* Binds the class key to itself — only valid when the key is a constructor.
|
|
237
|
+
*/
|
|
153
238
|
toSelf(): TransientBindingBuilder<Value>;
|
|
154
|
-
/**
|
|
239
|
+
/**
|
|
240
|
+
* Binds the token to a pre-existing value; always resolved as singleton, no construction.
|
|
241
|
+
*/
|
|
155
242
|
toConstantValue<const ConcreteValue extends Value>(value: ConcreteValue): ConstantBindingBuilder<Value>;
|
|
156
|
-
/**
|
|
243
|
+
/**
|
|
244
|
+
* Binds to a synchronous factory; `ctx` provides nested resolution within the same path.
|
|
245
|
+
*/
|
|
157
246
|
toDynamic(factory: (ctx: ResolutionContext) => Value): TransientBindingBuilder<Value>;
|
|
158
|
-
/**
|
|
247
|
+
/**
|
|
248
|
+
* Binds to an async factory; must be resolved via `resolveAsync` / `resolveAllAsync`.
|
|
249
|
+
*/
|
|
159
250
|
toDynamicAsync(factory: (ctx: ResolutionContext) => Promise<Value>): TransientBindingBuilder<Value>;
|
|
160
251
|
/**
|
|
161
252
|
* Binds to a factory whose dependencies are declared explicitly in `deps` and pre-resolved by
|
|
162
253
|
* the container before the factory is called — no `ResolutionContext` needed inside the factory.
|
|
163
254
|
*/
|
|
164
255
|
toResolved<Deps extends readonly (Token<unknown> | Constructor<unknown>)[]>(factory: (...args: { [Index in keyof Deps]: TokenValue<Deps[Index]> }) => Value, deps: Deps): TransientBindingBuilder<Value>;
|
|
165
|
-
/**
|
|
256
|
+
/**
|
|
257
|
+
* Redirects resolution to `targetToken`; the container resolves whatever is bound there.
|
|
258
|
+
*/
|
|
166
259
|
toAlias(targetToken: Token<Value>): TransientBindingBuilder<Value>;
|
|
167
|
-
/**
|
|
260
|
+
/**
|
|
261
|
+
* One instance per container; supports `onDeactivation`.
|
|
262
|
+
*/
|
|
168
263
|
singleton(): SingletonBindingBuilder<Value>;
|
|
169
|
-
/**
|
|
264
|
+
/**
|
|
265
|
+
* New instance on every resolution (default scope).
|
|
266
|
+
*/
|
|
170
267
|
transient(): TransientBindingBuilder<Value>;
|
|
171
|
-
/**
|
|
268
|
+
/**
|
|
269
|
+
* One instance per child container scope.
|
|
270
|
+
*/
|
|
172
271
|
scoped(): ScopedBindingBuilder<Value>;
|
|
173
|
-
/**
|
|
272
|
+
/**
|
|
273
|
+
* Called with the resolved instance after construction; the return value replaces the instance.
|
|
274
|
+
*/
|
|
174
275
|
onActivation(handler: ActivationHandler<Value>): this;
|
|
175
|
-
/**
|
|
276
|
+
/**
|
|
277
|
+
* @internal Keep public for runtime correctness; hidden from transient/scoped builders via type aliases.
|
|
278
|
+
*/
|
|
176
279
|
onDeactivation(handler: DeactivationHandler<Value>): this;
|
|
177
|
-
/**
|
|
280
|
+
/**
|
|
281
|
+
* This binding only resolves when the caller passes `{ name }` as the resolve hint.
|
|
282
|
+
*/
|
|
178
283
|
whenNamed(name: string): this;
|
|
179
|
-
/**
|
|
284
|
+
/**
|
|
285
|
+
* This binding only resolves when the caller passes `{ tag: [tag, tagValue] }` as the resolve hint.
|
|
286
|
+
*/
|
|
180
287
|
whenTagged(tag: string, tagValue: unknown): this;
|
|
181
|
-
/**
|
|
288
|
+
/**
|
|
289
|
+
* Adds a custom predicate; all predicates must pass for the binding to be selected.
|
|
290
|
+
*/
|
|
182
291
|
when(constraint: (ctx: ConstraintContext) => boolean): this;
|
|
183
292
|
/**
|
|
184
293
|
* Returns the binding's stable ID, allocating one if needed.
|
|
@@ -187,9 +296,25 @@ declare class BindingBuilder<Value> {
|
|
|
187
296
|
*/
|
|
188
297
|
id(): BindingIdentifier;
|
|
189
298
|
id(identifier: BindingIdentifier): BindingIdentifier;
|
|
299
|
+
/**
|
|
300
|
+
* Records the chosen strategy and emits the first {@link Binding} snapshot.
|
|
301
|
+
* Throws {@link InternalError} if a strategy was already selected (double `to*()` call).
|
|
302
|
+
*/
|
|
190
303
|
private registerWithStrategy;
|
|
304
|
+
/**
|
|
305
|
+
* Re-creates the {@link Binding} snapshot from the current builder state and pushes
|
|
306
|
+
* the update to the registry. No-op if no strategy has been set yet.
|
|
307
|
+
*/
|
|
191
308
|
private refreshRegisteredBinding;
|
|
309
|
+
/**
|
|
310
|
+
* Guards against calling `.singleton()` / `.transient()` / `.scoped()` on a constant binding,
|
|
311
|
+
* which is locked to `"singleton"` scope by invariant.
|
|
312
|
+
*/
|
|
192
313
|
private assertScopeMutable;
|
|
314
|
+
/**
|
|
315
|
+
* Produces an immutable {@link Binding} snapshot from the current builder fields.
|
|
316
|
+
* Called by both {@link registerWithStrategy} and {@link refreshRegisteredBinding}.
|
|
317
|
+
*/
|
|
193
318
|
private createBinding;
|
|
194
319
|
}
|
|
195
320
|
/**
|
package/dist/binding.mjs
CHANGED
|
@@ -6,25 +6,72 @@ import { InternalError } from "./errors.mjs";
|
|
|
6
6
|
function createBindingIdentifier() {
|
|
7
7
|
return globalThis.crypto.randomUUID();
|
|
8
8
|
}
|
|
9
|
+
/**
|
|
10
|
+
* Fluent builder for registering a single binding against a {@link Token} or {@link Constructor}.
|
|
11
|
+
*
|
|
12
|
+
* A builder has two phases:
|
|
13
|
+
* 1. **Strategy selection** — exactly one `to*()` call (`to`, `toSelf`, `toConstantValue`,
|
|
14
|
+
* `toDynamic`, `toDynamicAsync`, `toResolved`, `toAlias`) that determines how the value is produced.
|
|
15
|
+
* 2. **Refinement chain** — optional calls to `singleton()`, `transient()`, `scoped()`,
|
|
16
|
+
* `onActivation()`, `onDeactivation()`, `whenNamed()`, `whenTagged()`, `when()`, and `id()`.
|
|
17
|
+
*
|
|
18
|
+
* Calling a second `to*()` method throws {@link InternalError}.
|
|
19
|
+
*
|
|
20
|
+
* The builder is created by {@link Container.bind} or by `bind` on {@link ModuleBuilder}.
|
|
21
|
+
* The container injects {@link RegistryCallbacks} so that every strategy selection and
|
|
22
|
+
* refinement is immediately reflected in the live registry.
|
|
23
|
+
*/
|
|
9
24
|
var BindingBuilder = class {
|
|
25
|
+
/**
|
|
26
|
+
* Current resolution strategy; starts as `"unset"` until a `to*()` method is called.
|
|
27
|
+
*/
|
|
10
28
|
strategy = { type: "unset" };
|
|
29
|
+
/**
|
|
30
|
+
* Lifetime scope applied to the next binding snapshot; defaults to `"transient"`.
|
|
31
|
+
*/
|
|
11
32
|
scope = "transient";
|
|
33
|
+
/**
|
|
34
|
+
* True after an explicit `.singleton()` / `.transient()` / `.scoped()` call (prevents constant scope change).
|
|
35
|
+
*/
|
|
12
36
|
isScopeExplicit = false;
|
|
37
|
+
/**
|
|
38
|
+
* Pre-allocated binding ID set via `id(identifier)` before the first `to*()` call.
|
|
39
|
+
*/
|
|
13
40
|
explicitId;
|
|
41
|
+
/**
|
|
42
|
+
* Resolve-hint name filter set by `.whenNamed()`.
|
|
43
|
+
*/
|
|
14
44
|
bindingName;
|
|
45
|
+
/**
|
|
46
|
+
* Tag filters accumulated by successive `.whenTagged()` calls.
|
|
47
|
+
*/
|
|
15
48
|
tags = /* @__PURE__ */ new Map();
|
|
49
|
+
/**
|
|
50
|
+
* Custom constraint predicates accumulated by `.when()`; all must pass for this binding to be selected.
|
|
51
|
+
*/
|
|
16
52
|
constraintPredicates = [];
|
|
17
53
|
onActivationHandler;
|
|
18
54
|
onDeactivationHandler;
|
|
55
|
+
/**
|
|
56
|
+
* Set when this binding was created inside a {@link Module} / {@link AsyncModule} setup callback.
|
|
57
|
+
*/
|
|
19
58
|
moduleId;
|
|
59
|
+
/**
|
|
60
|
+
* The most recently emitted {@link Binding} snapshot; `undefined` before the first `to*()` call.
|
|
61
|
+
*/
|
|
20
62
|
currentBinding;
|
|
63
|
+
/**
|
|
64
|
+
* Container-injected hooks that sync builder mutations into the live registry.
|
|
65
|
+
*/
|
|
21
66
|
callbacks;
|
|
22
67
|
constructor(bindingKey, moduleId, callbacks) {
|
|
23
68
|
this.bindingKey = bindingKey;
|
|
24
69
|
this.moduleId = moduleId;
|
|
25
70
|
this.callbacks = callbacks ?? {};
|
|
26
71
|
}
|
|
27
|
-
/**
|
|
72
|
+
/**
|
|
73
|
+
* Binds the token to a concrete implementation class; the container constructs it on demand.
|
|
74
|
+
*/
|
|
28
75
|
to(implementationClass) {
|
|
29
76
|
this.registerWithStrategy({
|
|
30
77
|
type: "class",
|
|
@@ -32,7 +79,9 @@ var BindingBuilder = class {
|
|
|
32
79
|
});
|
|
33
80
|
return this;
|
|
34
81
|
}
|
|
35
|
-
/**
|
|
82
|
+
/**
|
|
83
|
+
* Binds the class key to itself — only valid when the key is a constructor.
|
|
84
|
+
*/
|
|
36
85
|
toSelf() {
|
|
37
86
|
if (typeof this.bindingKey !== "function") throw new InternalError("toSelf() requires the binding key to be a constructor; use bind(SomeClass) or call to(Class) instead.");
|
|
38
87
|
this.registerWithStrategy({
|
|
@@ -41,7 +90,9 @@ var BindingBuilder = class {
|
|
|
41
90
|
});
|
|
42
91
|
return this;
|
|
43
92
|
}
|
|
44
|
-
/**
|
|
93
|
+
/**
|
|
94
|
+
* Binds the token to a pre-existing value; always resolved as singleton, no construction.
|
|
95
|
+
*/
|
|
45
96
|
toConstantValue(value) {
|
|
46
97
|
this.scope = "singleton";
|
|
47
98
|
this.registerWithStrategy({
|
|
@@ -50,7 +101,9 @@ var BindingBuilder = class {
|
|
|
50
101
|
});
|
|
51
102
|
return this;
|
|
52
103
|
}
|
|
53
|
-
/**
|
|
104
|
+
/**
|
|
105
|
+
* Binds to a synchronous factory; `ctx` provides nested resolution within the same path.
|
|
106
|
+
*/
|
|
54
107
|
toDynamic(factory) {
|
|
55
108
|
this.registerWithStrategy({
|
|
56
109
|
type: "dynamic",
|
|
@@ -58,7 +111,9 @@ var BindingBuilder = class {
|
|
|
58
111
|
});
|
|
59
112
|
return this;
|
|
60
113
|
}
|
|
61
|
-
/**
|
|
114
|
+
/**
|
|
115
|
+
* Binds to an async factory; must be resolved via `resolveAsync` / `resolveAllAsync`.
|
|
116
|
+
*/
|
|
62
117
|
toDynamicAsync(factory) {
|
|
63
118
|
this.registerWithStrategy({
|
|
64
119
|
type: "async-dynamic",
|
|
@@ -78,7 +133,9 @@ var BindingBuilder = class {
|
|
|
78
133
|
});
|
|
79
134
|
return this;
|
|
80
135
|
}
|
|
81
|
-
/**
|
|
136
|
+
/**
|
|
137
|
+
* Redirects resolution to `targetToken`; the container resolves whatever is bound there.
|
|
138
|
+
*/
|
|
82
139
|
toAlias(targetToken) {
|
|
83
140
|
this.registerWithStrategy({
|
|
84
141
|
type: "alias",
|
|
@@ -86,7 +143,9 @@ var BindingBuilder = class {
|
|
|
86
143
|
});
|
|
87
144
|
return this;
|
|
88
145
|
}
|
|
89
|
-
/**
|
|
146
|
+
/**
|
|
147
|
+
* One instance per container; supports `onDeactivation`.
|
|
148
|
+
*/
|
|
90
149
|
singleton() {
|
|
91
150
|
this.assertScopeMutable();
|
|
92
151
|
this.scope = "singleton";
|
|
@@ -94,7 +153,9 @@ var BindingBuilder = class {
|
|
|
94
153
|
this.refreshRegisteredBinding();
|
|
95
154
|
return this;
|
|
96
155
|
}
|
|
97
|
-
/**
|
|
156
|
+
/**
|
|
157
|
+
* New instance on every resolution (default scope).
|
|
158
|
+
*/
|
|
98
159
|
transient() {
|
|
99
160
|
this.assertScopeMutable();
|
|
100
161
|
this.scope = "transient";
|
|
@@ -102,7 +163,9 @@ var BindingBuilder = class {
|
|
|
102
163
|
this.refreshRegisteredBinding();
|
|
103
164
|
return this;
|
|
104
165
|
}
|
|
105
|
-
/**
|
|
166
|
+
/**
|
|
167
|
+
* One instance per child container scope.
|
|
168
|
+
*/
|
|
106
169
|
scoped() {
|
|
107
170
|
this.assertScopeMutable();
|
|
108
171
|
this.scope = "scoped";
|
|
@@ -110,31 +173,41 @@ var BindingBuilder = class {
|
|
|
110
173
|
this.refreshRegisteredBinding();
|
|
111
174
|
return this;
|
|
112
175
|
}
|
|
113
|
-
/**
|
|
176
|
+
/**
|
|
177
|
+
* Called with the resolved instance after construction; the return value replaces the instance.
|
|
178
|
+
*/
|
|
114
179
|
onActivation(handler) {
|
|
115
180
|
this.onActivationHandler = handler;
|
|
116
181
|
this.refreshRegisteredBinding();
|
|
117
182
|
return this;
|
|
118
183
|
}
|
|
119
|
-
/**
|
|
184
|
+
/**
|
|
185
|
+
* @internal Keep public for runtime correctness; hidden from transient/scoped builders via type aliases.
|
|
186
|
+
*/
|
|
120
187
|
onDeactivation(handler) {
|
|
121
188
|
this.onDeactivationHandler = handler;
|
|
122
189
|
this.refreshRegisteredBinding();
|
|
123
190
|
return this;
|
|
124
191
|
}
|
|
125
|
-
/**
|
|
192
|
+
/**
|
|
193
|
+
* This binding only resolves when the caller passes `{ name }` as the resolve hint.
|
|
194
|
+
*/
|
|
126
195
|
whenNamed(name) {
|
|
127
196
|
this.bindingName = name;
|
|
128
197
|
this.refreshRegisteredBinding();
|
|
129
198
|
return this;
|
|
130
199
|
}
|
|
131
|
-
/**
|
|
200
|
+
/**
|
|
201
|
+
* This binding only resolves when the caller passes `{ tag: [tag, tagValue] }` as the resolve hint.
|
|
202
|
+
*/
|
|
132
203
|
whenTagged(tag, tagValue) {
|
|
133
204
|
this.tags.set(tag, tagValue);
|
|
134
205
|
this.refreshRegisteredBinding();
|
|
135
206
|
return this;
|
|
136
207
|
}
|
|
137
|
-
/**
|
|
208
|
+
/**
|
|
209
|
+
* Adds a custom predicate; all predicates must pass for the binding to be selected.
|
|
210
|
+
*/
|
|
138
211
|
when(constraint) {
|
|
139
212
|
this.constraintPredicates.push(constraint);
|
|
140
213
|
this.refreshRegisteredBinding();
|
|
@@ -152,6 +225,10 @@ var BindingBuilder = class {
|
|
|
152
225
|
this.explicitId = this.explicitId ?? createBindingIdentifier();
|
|
153
226
|
return this.explicitId;
|
|
154
227
|
}
|
|
228
|
+
/**
|
|
229
|
+
* Records the chosen strategy and emits the first {@link Binding} snapshot.
|
|
230
|
+
* Throws {@link InternalError} if a strategy was already selected (double `to*()` call).
|
|
231
|
+
*/
|
|
155
232
|
registerWithStrategy(next) {
|
|
156
233
|
if (this.strategy.type !== "unset") throw new InternalError("A binding strategy was already selected; only one to*(...) chain is allowed per builder.");
|
|
157
234
|
this.strategy = next;
|
|
@@ -160,15 +237,27 @@ var BindingBuilder = class {
|
|
|
160
237
|
this.currentBinding = binding;
|
|
161
238
|
this.callbacks.register?.(binding);
|
|
162
239
|
}
|
|
240
|
+
/**
|
|
241
|
+
* Re-creates the {@link Binding} snapshot from the current builder state and pushes
|
|
242
|
+
* the update to the registry. No-op if no strategy has been set yet.
|
|
243
|
+
*/
|
|
163
244
|
refreshRegisteredBinding() {
|
|
164
245
|
if (this.currentBinding === void 0 || this.strategy.type === "unset") return;
|
|
165
246
|
const next = this.createBinding(this.currentBinding.id, this.strategy);
|
|
166
247
|
this.currentBinding = next;
|
|
167
248
|
this.callbacks.update?.(next);
|
|
168
249
|
}
|
|
250
|
+
/**
|
|
251
|
+
* Guards against calling `.singleton()` / `.transient()` / `.scoped()` on a constant binding,
|
|
252
|
+
* which is locked to `"singleton"` scope by invariant.
|
|
253
|
+
*/
|
|
169
254
|
assertScopeMutable() {
|
|
170
255
|
if (this.strategy.type === "constant") throw new InternalError("Constant bindings are always singleton and do not support scope changes.");
|
|
171
256
|
}
|
|
257
|
+
/**
|
|
258
|
+
* Produces an immutable {@link Binding} snapshot from the current builder fields.
|
|
259
|
+
* Called by both {@link registerWithStrategy} and {@link refreshRegisteredBinding}.
|
|
260
|
+
*/
|
|
172
261
|
createBinding(id, strategy) {
|
|
173
262
|
const constraint = this.constraintPredicates.length === 0 ? void 0 : (ctx) => this.constraintPredicates.every((predicate) => predicate(ctx));
|
|
174
263
|
const lifecycle = {
|
package/dist/constraints.d.mts
CHANGED
|
@@ -3,15 +3,30 @@ import { ConstraintContext, Constructor } from "./binding.mjs";
|
|
|
3
3
|
|
|
4
4
|
//#region src/constraints.d.ts
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
6
|
+
* Constraint predicate factory: matches when the direct parent on the materialization stack
|
|
7
|
+
* was registered under `registryKey`. Pass the result to {@link BindingBuilder.when}.
|
|
8
|
+
*
|
|
9
|
+
* @param registryKey - Token or constructor that the parent binding must be registered against.
|
|
10
|
+
* @returns A predicate compatible with {@link BindingBuilder.when}.
|
|
7
11
|
*/
|
|
8
12
|
declare function whenParentIs(registryKey: Token<unknown> | Constructor<unknown>): (ctx: ConstraintContext) => boolean;
|
|
9
13
|
/**
|
|
10
|
-
*
|
|
14
|
+
* Constraint predicate factory: matches when *any* ancestor on the materialization stack
|
|
15
|
+
* (not just the immediate parent) was registered under `registryKey`.
|
|
16
|
+
* Pass the result to {@link BindingBuilder.when}.
|
|
17
|
+
*
|
|
18
|
+
* @param registryKey - Token or constructor to search for across the full construction chain.
|
|
19
|
+
* @returns A predicate compatible with {@link BindingBuilder.when}.
|
|
11
20
|
*/
|
|
12
21
|
declare function whenAnyAncestorIs(registryKey: Token<unknown> | Constructor<unknown>): (ctx: ConstraintContext) => boolean;
|
|
13
22
|
/**
|
|
14
|
-
*
|
|
23
|
+
* Constraint predicate factory: matches when the immediate parent binding carries a tag
|
|
24
|
+
* whose key is `tag` and whose value is reference-equal to `tagValue` (`Object.is`).
|
|
25
|
+
* Pass the result to {@link BindingBuilder.when}.
|
|
26
|
+
*
|
|
27
|
+
* @param tag - Tag key to check on the parent binding.
|
|
28
|
+
* @param tagValue - Expected value; compared via `Object.is`.
|
|
29
|
+
* @returns A predicate compatible with {@link BindingBuilder.when}.
|
|
15
30
|
*/
|
|
16
31
|
declare function whenTargetTagged(tag: string, tagValue: unknown): (ctx: ConstraintContext) => boolean;
|
|
17
32
|
//#endregion
|