@codefast/di 0.3.13 → 0.3.14-canary.1
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 +30 -0
- package/README.md +270 -234
- package/dist/binding-select.d.mts +17 -6
- package/dist/binding-select.mjs +17 -6
- package/dist/binding.d.mts +167 -34
- package/dist/binding.mjs +111 -14
- package/dist/constraints.d.mts +18 -3
- package/dist/constraints.mjs +18 -3
- package/dist/container.d.mts +85 -35
- package/dist/container.mjs +140 -6
- 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 +36 -13
- package/dist/dependency-graph.mjs +42 -8
- package/dist/errors.d.mts +132 -21
- package/dist/errors.mjs +126 -18
- package/dist/graph-adapters/cytoscape.d.mts +10 -0
- package/dist/graph-adapters/cytoscape.mjs +40 -0
- package/dist/graph-adapters/dot.d.mts +9 -0
- package/dist/graph-adapters/dot.mjs +97 -0
- package/dist/graph-adapters/reactflow.d.mts +10 -0
- package/dist/graph-adapters/reactflow.mjs +80 -0
- package/dist/graph-adapters/types.d.mts +91 -0
- package/dist/graph-adapters/types.mjs +1 -0
- package/dist/index.d.mts +2 -3
- package/dist/index.mjs +2 -2
- package/dist/inspector.d.mts +42 -40
- package/dist/inspector.mjs +18 -169
- 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 +42 -18
- 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 +46 -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 +38 -6
- package/dist/scope.mjs +42 -13
- package/dist/token.d.mts +9 -2
- package/dist/token.mjs +7 -1
- package/package.json +18 -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,25 +17,37 @@ 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.
|
|
24
26
|
*/
|
|
25
27
|
type ResolveHint = {
|
|
26
|
-
readonly name?: string;
|
|
28
|
+
/** Matches bindings configured with `.whenNamed(name)`. */readonly name?: string; /** Matches bindings configured with `.whenTagged(tagKey, value)`. */
|
|
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
|
-
readonly registryKey: RegistryKey;
|
|
36
|
-
readonly bindingId: BindingIdentifier;
|
|
37
|
-
readonly bindingKind: ConstraintBindingKind;
|
|
38
|
-
readonly tags: ReadonlyMap<string, unknown>;
|
|
47
|
+
/** Registry key (token/constructor) that selected this binding. */readonly registryKey: RegistryKey; /** Stable identifier of the materialized binding. */
|
|
48
|
+
readonly bindingId: BindingIdentifier; /** Discriminant of the selected binding strategy. */
|
|
49
|
+
readonly bindingKind: ConstraintBindingKind; /** Immutable tag map present on the selected binding. */
|
|
50
|
+
readonly tags: ReadonlyMap<string, unknown>; /** Effective scope of the selected binding. */
|
|
39
51
|
readonly scope: BindingScope;
|
|
40
52
|
};
|
|
41
53
|
/**
|
|
@@ -46,10 +58,10 @@ type MaterializationFrame = ConstraintParentFrame;
|
|
|
46
58
|
* Context for {@link BindingBuilder.when} predicates: path, ancestor metadata, and the current resolve hint.
|
|
47
59
|
*/
|
|
48
60
|
type ConstraintContext = {
|
|
49
|
-
readonly resolutionPath: readonly string[];
|
|
50
|
-
readonly materializationStack: readonly ConstraintParentFrame[];
|
|
51
|
-
readonly parent: ConstraintParentFrame | undefined;
|
|
52
|
-
readonly ancestors: readonly ConstraintParentFrame[];
|
|
61
|
+
/** Resolution labels from root request to current key. */readonly resolutionPath: readonly string[]; /** Full chain of materialized parent frames (oldest → newest). */
|
|
62
|
+
readonly materializationStack: readonly ConstraintParentFrame[]; /** Immediate parent frame, if the current resolution has one. */
|
|
63
|
+
readonly parent: ConstraintParentFrame | undefined; /** Parent chain excluding the immediate parent frame. */
|
|
64
|
+
readonly ancestors: readonly ConstraintParentFrame[]; /** Name/tag hint used for the current lookup, if provided. */
|
|
53
65
|
readonly currentResolveHint: ResolveHint | undefined;
|
|
54
66
|
};
|
|
55
67
|
/**
|
|
@@ -60,11 +72,20 @@ type ConstraintContext = {
|
|
|
60
72
|
* context-sensitive bindings such as `whenParentIs` or `whenAnyAncestorIs`.
|
|
61
73
|
*/
|
|
62
74
|
type ResolutionContext = {
|
|
63
|
-
readonly resolve: <Value>(token: Token<Value> | Constructor<Value>, hint?: ResolveOptions) => Value;
|
|
64
|
-
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; /**
|
|
75
|
+
/** Resolves one binding synchronously using current path/stack context. */readonly resolve: <Value>(token: Token<Value> | Constructor<Value>, hint?: ResolveOptions) => Value; /** Async variant of {@link ResolutionContext.resolve}. */
|
|
76
|
+
readonly resolveAsync: <Value>(token: Token<Value> | Constructor<Value>, hint?: ResolveOptions) => Promise<Value>; /** Returns `undefined` when the requested root key is unbound. */
|
|
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,134 @@ 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;
|
|
216
|
+
/**
|
|
217
|
+
* Latest `onActivation` hook provided by `.onActivation(...)`.
|
|
218
|
+
* Applied to emitted binding snapshots until replaced.
|
|
219
|
+
*/
|
|
144
220
|
private onActivationHandler;
|
|
221
|
+
/**
|
|
222
|
+
* Latest `onDeactivation` hook provided by `.onDeactivation(...)`.
|
|
223
|
+
* Emitted only on builder variants that expose deactivation support.
|
|
224
|
+
*/
|
|
145
225
|
private onDeactivationHandler;
|
|
226
|
+
/**
|
|
227
|
+
* Set when this binding was created inside a {@link Module} / {@link AsyncModule} setup callback.
|
|
228
|
+
*/
|
|
146
229
|
private readonly moduleId;
|
|
230
|
+
/**
|
|
231
|
+
* The most recently emitted {@link Binding} snapshot; `undefined` before the first `to*()` call.
|
|
232
|
+
*/
|
|
147
233
|
private currentBinding;
|
|
234
|
+
/**
|
|
235
|
+
* Container-injected hooks that sync builder mutations into the live registry.
|
|
236
|
+
*/
|
|
148
237
|
private readonly callbacks;
|
|
149
238
|
constructor(bindingKey: Token<Value> | Constructor<Value>, moduleId?: string, callbacks?: RegistryCallbacks<Value>);
|
|
150
|
-
/**
|
|
239
|
+
/**
|
|
240
|
+
* Binds the token to a concrete implementation class; the container constructs it on demand.
|
|
241
|
+
*/
|
|
151
242
|
to<C extends Constructor<Value>>(implementationClass: C): TransientBindingBuilder<Value>;
|
|
152
|
-
/**
|
|
243
|
+
/**
|
|
244
|
+
* Binds the class key to itself — only valid when the key is a constructor.
|
|
245
|
+
*/
|
|
153
246
|
toSelf(): TransientBindingBuilder<Value>;
|
|
154
|
-
/**
|
|
247
|
+
/**
|
|
248
|
+
* Binds the token to a pre-existing value; always resolved as singleton, no construction.
|
|
249
|
+
*/
|
|
155
250
|
toConstantValue<const ConcreteValue extends Value>(value: ConcreteValue): ConstantBindingBuilder<Value>;
|
|
156
|
-
/**
|
|
251
|
+
/**
|
|
252
|
+
* Binds to a synchronous factory; `ctx` provides nested resolution within the same path.
|
|
253
|
+
*/
|
|
157
254
|
toDynamic(factory: (ctx: ResolutionContext) => Value): TransientBindingBuilder<Value>;
|
|
158
|
-
/**
|
|
255
|
+
/**
|
|
256
|
+
* Binds to an async factory; must be resolved via `resolveAsync` / `resolveAllAsync`.
|
|
257
|
+
*/
|
|
159
258
|
toDynamicAsync(factory: (ctx: ResolutionContext) => Promise<Value>): TransientBindingBuilder<Value>;
|
|
160
259
|
/**
|
|
161
260
|
* Binds to a factory whose dependencies are declared explicitly in `deps` and pre-resolved by
|
|
162
261
|
* the container before the factory is called — no `ResolutionContext` needed inside the factory.
|
|
163
262
|
*/
|
|
164
263
|
toResolved<Deps extends readonly (Token<unknown> | Constructor<unknown>)[]>(factory: (...args: { [Index in keyof Deps]: TokenValue<Deps[Index]> }) => Value, deps: Deps): TransientBindingBuilder<Value>;
|
|
165
|
-
/**
|
|
264
|
+
/**
|
|
265
|
+
* Redirects resolution to `targetToken`; the container resolves whatever is bound there.
|
|
266
|
+
*/
|
|
166
267
|
toAlias(targetToken: Token<Value>): TransientBindingBuilder<Value>;
|
|
167
|
-
/**
|
|
268
|
+
/**
|
|
269
|
+
* One instance per container; supports `onDeactivation`.
|
|
270
|
+
*/
|
|
168
271
|
singleton(): SingletonBindingBuilder<Value>;
|
|
169
|
-
/**
|
|
272
|
+
/**
|
|
273
|
+
* New instance on every resolution (default scope).
|
|
274
|
+
*/
|
|
170
275
|
transient(): TransientBindingBuilder<Value>;
|
|
171
|
-
/**
|
|
276
|
+
/**
|
|
277
|
+
* One instance per child container scope.
|
|
278
|
+
*/
|
|
172
279
|
scoped(): ScopedBindingBuilder<Value>;
|
|
173
|
-
/**
|
|
280
|
+
/**
|
|
281
|
+
* Called with the resolved instance after construction; the return value replaces the instance.
|
|
282
|
+
*/
|
|
174
283
|
onActivation(handler: ActivationHandler<Value>): this;
|
|
175
|
-
/**
|
|
284
|
+
/**
|
|
285
|
+
* @internal Keep public for runtime correctness; hidden from transient/scoped builders via type aliases.
|
|
286
|
+
*/
|
|
176
287
|
onDeactivation(handler: DeactivationHandler<Value>): this;
|
|
177
|
-
/**
|
|
288
|
+
/**
|
|
289
|
+
* This binding only resolves when the caller passes `{ name }` as the resolve hint.
|
|
290
|
+
*/
|
|
178
291
|
whenNamed(name: string): this;
|
|
179
|
-
/**
|
|
292
|
+
/**
|
|
293
|
+
* This binding only resolves when the caller passes `{ tag: [tag, tagValue] }` as the resolve hint.
|
|
294
|
+
*/
|
|
180
295
|
whenTagged(tag: string, tagValue: unknown): this;
|
|
181
|
-
/**
|
|
296
|
+
/**
|
|
297
|
+
* Adds a custom predicate; all predicates must pass for the binding to be selected.
|
|
298
|
+
*/
|
|
182
299
|
when(constraint: (ctx: ConstraintContext) => boolean): this;
|
|
183
300
|
/**
|
|
184
301
|
* Returns the binding's stable ID, allocating one if needed.
|
|
@@ -187,9 +304,25 @@ declare class BindingBuilder<Value> {
|
|
|
187
304
|
*/
|
|
188
305
|
id(): BindingIdentifier;
|
|
189
306
|
id(identifier: BindingIdentifier): BindingIdentifier;
|
|
307
|
+
/**
|
|
308
|
+
* Records the chosen strategy and emits the first {@link Binding} snapshot.
|
|
309
|
+
* Throws {@link InternalError} if a strategy was already selected (double `to*()` call).
|
|
310
|
+
*/
|
|
190
311
|
private registerWithStrategy;
|
|
312
|
+
/**
|
|
313
|
+
* Re-creates the {@link Binding} snapshot from the current builder state and pushes
|
|
314
|
+
* the update to the registry. No-op if no strategy has been set yet.
|
|
315
|
+
*/
|
|
191
316
|
private refreshRegisteredBinding;
|
|
317
|
+
/**
|
|
318
|
+
* Guards against calling `.singleton()` / `.transient()` / `.scoped()` on a constant binding,
|
|
319
|
+
* which is locked to `"singleton"` scope by invariant.
|
|
320
|
+
*/
|
|
192
321
|
private assertScopeMutable;
|
|
322
|
+
/**
|
|
323
|
+
* Produces an immutable {@link Binding} snapshot from the current builder fields.
|
|
324
|
+
* Called by both {@link registerWithStrategy} and {@link refreshRegisteredBinding}.
|
|
325
|
+
*/
|
|
193
326
|
private createBinding;
|
|
194
327
|
}
|
|
195
328
|
/**
|
package/dist/binding.mjs
CHANGED
|
@@ -6,25 +6,80 @@ 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 = [];
|
|
53
|
+
/**
|
|
54
|
+
* Latest `onActivation` hook provided by `.onActivation(...)`.
|
|
55
|
+
* Applied to emitted binding snapshots until replaced.
|
|
56
|
+
*/
|
|
17
57
|
onActivationHandler;
|
|
58
|
+
/**
|
|
59
|
+
* Latest `onDeactivation` hook provided by `.onDeactivation(...)`.
|
|
60
|
+
* Emitted only on builder variants that expose deactivation support.
|
|
61
|
+
*/
|
|
18
62
|
onDeactivationHandler;
|
|
63
|
+
/**
|
|
64
|
+
* Set when this binding was created inside a {@link Module} / {@link AsyncModule} setup callback.
|
|
65
|
+
*/
|
|
19
66
|
moduleId;
|
|
67
|
+
/**
|
|
68
|
+
* The most recently emitted {@link Binding} snapshot; `undefined` before the first `to*()` call.
|
|
69
|
+
*/
|
|
20
70
|
currentBinding;
|
|
71
|
+
/**
|
|
72
|
+
* Container-injected hooks that sync builder mutations into the live registry.
|
|
73
|
+
*/
|
|
21
74
|
callbacks;
|
|
22
75
|
constructor(bindingKey, moduleId, callbacks) {
|
|
23
76
|
this.bindingKey = bindingKey;
|
|
24
77
|
this.moduleId = moduleId;
|
|
25
78
|
this.callbacks = callbacks ?? {};
|
|
26
79
|
}
|
|
27
|
-
/**
|
|
80
|
+
/**
|
|
81
|
+
* Binds the token to a concrete implementation class; the container constructs it on demand.
|
|
82
|
+
*/
|
|
28
83
|
to(implementationClass) {
|
|
29
84
|
this.registerWithStrategy({
|
|
30
85
|
type: "class",
|
|
@@ -32,7 +87,9 @@ var BindingBuilder = class {
|
|
|
32
87
|
});
|
|
33
88
|
return this;
|
|
34
89
|
}
|
|
35
|
-
/**
|
|
90
|
+
/**
|
|
91
|
+
* Binds the class key to itself — only valid when the key is a constructor.
|
|
92
|
+
*/
|
|
36
93
|
toSelf() {
|
|
37
94
|
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
95
|
this.registerWithStrategy({
|
|
@@ -41,7 +98,9 @@ var BindingBuilder = class {
|
|
|
41
98
|
});
|
|
42
99
|
return this;
|
|
43
100
|
}
|
|
44
|
-
/**
|
|
101
|
+
/**
|
|
102
|
+
* Binds the token to a pre-existing value; always resolved as singleton, no construction.
|
|
103
|
+
*/
|
|
45
104
|
toConstantValue(value) {
|
|
46
105
|
this.scope = "singleton";
|
|
47
106
|
this.registerWithStrategy({
|
|
@@ -50,7 +109,9 @@ var BindingBuilder = class {
|
|
|
50
109
|
});
|
|
51
110
|
return this;
|
|
52
111
|
}
|
|
53
|
-
/**
|
|
112
|
+
/**
|
|
113
|
+
* Binds to a synchronous factory; `ctx` provides nested resolution within the same path.
|
|
114
|
+
*/
|
|
54
115
|
toDynamic(factory) {
|
|
55
116
|
this.registerWithStrategy({
|
|
56
117
|
type: "dynamic",
|
|
@@ -58,7 +119,9 @@ var BindingBuilder = class {
|
|
|
58
119
|
});
|
|
59
120
|
return this;
|
|
60
121
|
}
|
|
61
|
-
/**
|
|
122
|
+
/**
|
|
123
|
+
* Binds to an async factory; must be resolved via `resolveAsync` / `resolveAllAsync`.
|
|
124
|
+
*/
|
|
62
125
|
toDynamicAsync(factory) {
|
|
63
126
|
this.registerWithStrategy({
|
|
64
127
|
type: "async-dynamic",
|
|
@@ -78,7 +141,9 @@ var BindingBuilder = class {
|
|
|
78
141
|
});
|
|
79
142
|
return this;
|
|
80
143
|
}
|
|
81
|
-
/**
|
|
144
|
+
/**
|
|
145
|
+
* Redirects resolution to `targetToken`; the container resolves whatever is bound there.
|
|
146
|
+
*/
|
|
82
147
|
toAlias(targetToken) {
|
|
83
148
|
this.registerWithStrategy({
|
|
84
149
|
type: "alias",
|
|
@@ -86,7 +151,9 @@ var BindingBuilder = class {
|
|
|
86
151
|
});
|
|
87
152
|
return this;
|
|
88
153
|
}
|
|
89
|
-
/**
|
|
154
|
+
/**
|
|
155
|
+
* One instance per container; supports `onDeactivation`.
|
|
156
|
+
*/
|
|
90
157
|
singleton() {
|
|
91
158
|
this.assertScopeMutable();
|
|
92
159
|
this.scope = "singleton";
|
|
@@ -94,7 +161,9 @@ var BindingBuilder = class {
|
|
|
94
161
|
this.refreshRegisteredBinding();
|
|
95
162
|
return this;
|
|
96
163
|
}
|
|
97
|
-
/**
|
|
164
|
+
/**
|
|
165
|
+
* New instance on every resolution (default scope).
|
|
166
|
+
*/
|
|
98
167
|
transient() {
|
|
99
168
|
this.assertScopeMutable();
|
|
100
169
|
this.scope = "transient";
|
|
@@ -102,7 +171,9 @@ var BindingBuilder = class {
|
|
|
102
171
|
this.refreshRegisteredBinding();
|
|
103
172
|
return this;
|
|
104
173
|
}
|
|
105
|
-
/**
|
|
174
|
+
/**
|
|
175
|
+
* One instance per child container scope.
|
|
176
|
+
*/
|
|
106
177
|
scoped() {
|
|
107
178
|
this.assertScopeMutable();
|
|
108
179
|
this.scope = "scoped";
|
|
@@ -110,31 +181,41 @@ var BindingBuilder = class {
|
|
|
110
181
|
this.refreshRegisteredBinding();
|
|
111
182
|
return this;
|
|
112
183
|
}
|
|
113
|
-
/**
|
|
184
|
+
/**
|
|
185
|
+
* Called with the resolved instance after construction; the return value replaces the instance.
|
|
186
|
+
*/
|
|
114
187
|
onActivation(handler) {
|
|
115
188
|
this.onActivationHandler = handler;
|
|
116
189
|
this.refreshRegisteredBinding();
|
|
117
190
|
return this;
|
|
118
191
|
}
|
|
119
|
-
/**
|
|
192
|
+
/**
|
|
193
|
+
* @internal Keep public for runtime correctness; hidden from transient/scoped builders via type aliases.
|
|
194
|
+
*/
|
|
120
195
|
onDeactivation(handler) {
|
|
121
196
|
this.onDeactivationHandler = handler;
|
|
122
197
|
this.refreshRegisteredBinding();
|
|
123
198
|
return this;
|
|
124
199
|
}
|
|
125
|
-
/**
|
|
200
|
+
/**
|
|
201
|
+
* This binding only resolves when the caller passes `{ name }` as the resolve hint.
|
|
202
|
+
*/
|
|
126
203
|
whenNamed(name) {
|
|
127
204
|
this.bindingName = name;
|
|
128
205
|
this.refreshRegisteredBinding();
|
|
129
206
|
return this;
|
|
130
207
|
}
|
|
131
|
-
/**
|
|
208
|
+
/**
|
|
209
|
+
* This binding only resolves when the caller passes `{ tag: [tag, tagValue] }` as the resolve hint.
|
|
210
|
+
*/
|
|
132
211
|
whenTagged(tag, tagValue) {
|
|
133
212
|
this.tags.set(tag, tagValue);
|
|
134
213
|
this.refreshRegisteredBinding();
|
|
135
214
|
return this;
|
|
136
215
|
}
|
|
137
|
-
/**
|
|
216
|
+
/**
|
|
217
|
+
* Adds a custom predicate; all predicates must pass for the binding to be selected.
|
|
218
|
+
*/
|
|
138
219
|
when(constraint) {
|
|
139
220
|
this.constraintPredicates.push(constraint);
|
|
140
221
|
this.refreshRegisteredBinding();
|
|
@@ -152,6 +233,10 @@ var BindingBuilder = class {
|
|
|
152
233
|
this.explicitId = this.explicitId ?? createBindingIdentifier();
|
|
153
234
|
return this.explicitId;
|
|
154
235
|
}
|
|
236
|
+
/**
|
|
237
|
+
* Records the chosen strategy and emits the first {@link Binding} snapshot.
|
|
238
|
+
* Throws {@link InternalError} if a strategy was already selected (double `to*()` call).
|
|
239
|
+
*/
|
|
155
240
|
registerWithStrategy(next) {
|
|
156
241
|
if (this.strategy.type !== "unset") throw new InternalError("A binding strategy was already selected; only one to*(...) chain is allowed per builder.");
|
|
157
242
|
this.strategy = next;
|
|
@@ -160,15 +245,27 @@ var BindingBuilder = class {
|
|
|
160
245
|
this.currentBinding = binding;
|
|
161
246
|
this.callbacks.register?.(binding);
|
|
162
247
|
}
|
|
248
|
+
/**
|
|
249
|
+
* Re-creates the {@link Binding} snapshot from the current builder state and pushes
|
|
250
|
+
* the update to the registry. No-op if no strategy has been set yet.
|
|
251
|
+
*/
|
|
163
252
|
refreshRegisteredBinding() {
|
|
164
253
|
if (this.currentBinding === void 0 || this.strategy.type === "unset") return;
|
|
165
254
|
const next = this.createBinding(this.currentBinding.id, this.strategy);
|
|
166
255
|
this.currentBinding = next;
|
|
167
256
|
this.callbacks.update?.(next);
|
|
168
257
|
}
|
|
258
|
+
/**
|
|
259
|
+
* Guards against calling `.singleton()` / `.transient()` / `.scoped()` on a constant binding,
|
|
260
|
+
* which is locked to `"singleton"` scope by invariant.
|
|
261
|
+
*/
|
|
169
262
|
assertScopeMutable() {
|
|
170
263
|
if (this.strategy.type === "constant") throw new InternalError("Constant bindings are always singleton and do not support scope changes.");
|
|
171
264
|
}
|
|
265
|
+
/**
|
|
266
|
+
* Produces an immutable {@link Binding} snapshot from the current builder fields.
|
|
267
|
+
* Called by both {@link registerWithStrategy} and {@link refreshRegisteredBinding}.
|
|
268
|
+
*/
|
|
172
269
|
createBinding(id, strategy) {
|
|
173
270
|
const constraint = this.constraintPredicates.length === 0 ? void 0 : (ctx) => this.constraintPredicates.every((predicate) => predicate(ctx));
|
|
174
271
|
const lifecycle = {
|