@codefast/di 0.5.0-canary.6 → 0.5.0-canary.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +123 -0
- package/README.md +15 -4
- package/dist/binding.d.ts +78 -19
- package/dist/binding.d.ts.map +1 -1
- package/dist/binding.js +46 -2
- package/dist/binding.js.map +1 -1
- package/dist/constructor-type.d.ts +4 -5
- package/dist/constructor-type.d.ts.map +1 -1
- package/dist/container/binding-builders.d.ts +56 -0
- package/dist/container/binding-builders.d.ts.map +1 -0
- package/dist/container/binding-builders.js +196 -0
- package/dist/container/binding-builders.js.map +1 -0
- package/dist/{container.d.ts → container/container.d.ts} +2 -2
- package/dist/container/container.d.ts.map +1 -0
- package/dist/container/container.js +624 -0
- package/dist/container/container.js.map +1 -0
- package/dist/decorators/inject.d.ts +10 -1
- package/dist/decorators/inject.d.ts.map +1 -1
- package/dist/decorators/inject.js +2 -2
- package/dist/decorators/inject.js.map +1 -1
- package/dist/decorators/injectable.js +3 -3
- package/dist/decorators/injectable.js.map +1 -1
- package/dist/errors.d.ts +14 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +17 -0
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +13 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -5
- package/dist/index.js.map +1 -1
- package/dist/introspection/dependency-graph.d.ts.map +1 -0
- package/dist/{dependency-graph.js → introspection/dependency-graph.js} +1 -1
- package/dist/introspection/dependency-graph.js.map +1 -0
- package/dist/{graph-adapters → introspection/graph-adapters}/cytoscape.d.ts +1 -1
- package/dist/introspection/graph-adapters/cytoscape.d.ts.map +1 -0
- package/dist/introspection/graph-adapters/cytoscape.js.map +1 -0
- package/dist/introspection/graph-adapters/dot.d.ts +6 -0
- package/dist/introspection/graph-adapters/dot.d.ts.map +1 -0
- package/dist/introspection/graph-adapters/dot.js.map +1 -0
- package/dist/{graph-adapters → introspection/graph-adapters}/reactflow.d.ts +2 -2
- package/dist/introspection/graph-adapters/reactflow.d.ts.map +1 -0
- package/dist/{graph-adapters → introspection/graph-adapters}/reactflow.js +11 -1
- package/dist/introspection/graph-adapters/reactflow.js.map +1 -0
- package/dist/{inspector.d.ts → introspection/inspector.d.ts} +3 -8
- package/dist/introspection/inspector.d.ts.map +1 -0
- package/dist/{inspector.js → introspection/inspector.js} +23 -23
- package/dist/introspection/inspector.js.map +1 -0
- package/dist/metadata/metadata-keys.d.ts +3 -6
- package/dist/metadata/metadata-keys.d.ts.map +1 -1
- package/dist/metadata/metadata-keys.js +3 -6
- package/dist/metadata/metadata-keys.js.map +1 -1
- package/dist/metadata/symbol-metadata-reader.d.ts +1 -1
- package/dist/metadata/symbol-metadata-reader.d.ts.map +1 -1
- package/dist/metadata/symbol-metadata-reader.js +4 -4
- package/dist/metadata/symbol-metadata-reader.js.map +1 -1
- package/dist/module.d.ts +9 -2
- package/dist/module.d.ts.map +1 -1
- package/dist/module.js +9 -2
- package/dist/module.js.map +1 -1
- package/dist/registry.d.ts +17 -13
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +94 -63
- package/dist/registry.js.map +1 -1
- package/dist/resolution/activation-need.d.ts +25 -0
- package/dist/resolution/activation-need.d.ts.map +1 -0
- package/dist/resolution/activation-need.js +64 -0
- package/dist/resolution/activation-need.js.map +1 -0
- package/dist/resolution/binding-lookup-cache.d.ts +41 -0
- package/dist/resolution/binding-lookup-cache.d.ts.map +1 -0
- package/dist/resolution/binding-lookup-cache.js +102 -0
- package/dist/resolution/binding-lookup-cache.js.map +1 -0
- package/dist/resolution/binding-scope.d.ts.map +1 -0
- package/dist/resolution/binding-scope.js.map +1 -0
- package/dist/resolution/binding-select.d.ts.map +1 -0
- package/dist/{binding-select.js → resolution/binding-select.js} +17 -7
- package/dist/resolution/binding-select.js.map +1 -0
- package/dist/resolution/class-introspector.d.ts +27 -0
- package/dist/resolution/class-introspector.d.ts.map +1 -0
- package/dist/resolution/class-introspector.js +60 -0
- package/dist/resolution/class-introspector.js.map +1 -0
- package/dist/resolution/constraints.d.ts.map +1 -0
- package/dist/resolution/constraints.js.map +1 -0
- package/dist/resolution/diagnostics.d.ts +41 -0
- package/dist/resolution/diagnostics.d.ts.map +1 -0
- package/dist/resolution/diagnostics.js +18 -0
- package/dist/resolution/diagnostics.js.map +1 -0
- package/dist/{environment.d.ts → resolution/environment.d.ts} +24 -7
- package/dist/resolution/environment.d.ts.map +1 -0
- package/dist/resolution/environment.js +126 -0
- package/dist/resolution/environment.js.map +1 -0
- package/dist/resolution/instantiation-plan.d.ts +67 -0
- package/dist/resolution/instantiation-plan.d.ts.map +1 -0
- package/dist/resolution/instantiation-plan.js +183 -0
- package/dist/resolution/instantiation-plan.js.map +1 -0
- package/dist/{lifecycle.d.ts → resolution/lifecycle.d.ts} +5 -3
- package/dist/resolution/lifecycle.d.ts.map +1 -0
- package/dist/{lifecycle.js → resolution/lifecycle.js} +23 -14
- package/dist/resolution/lifecycle.js.map +1 -0
- package/dist/resolution/resolution-path.d.ts +22 -0
- package/dist/resolution/resolution-path.d.ts.map +1 -0
- package/dist/resolution/resolution-path.js +40 -0
- package/dist/resolution/resolution-path.js.map +1 -0
- package/dist/resolution/resolve-options.d.ts.map +1 -0
- package/dist/resolution/resolve-options.js.map +1 -0
- package/dist/resolution/resolver.d.ts +40 -0
- package/dist/resolution/resolver.d.ts.map +1 -0
- package/dist/resolution/resolver.js +1108 -0
- package/dist/resolution/resolver.js.map +1 -0
- package/dist/{scope.d.ts → resolution/scope.d.ts} +8 -9
- package/dist/resolution/scope.d.ts.map +1 -0
- package/dist/resolution/scope.js +80 -0
- package/dist/resolution/scope.js.map +1 -0
- package/dist/token.d.ts.map +1 -1
- package/dist/token.js +0 -3
- package/dist/token.js.map +1 -1
- package/package.json +19 -94
- package/src/binding.ts +137 -22
- package/src/constructor-type.ts +4 -5
- package/src/container/binding-builders.ts +294 -0
- package/src/container/container.ts +825 -0
- package/src/decorators/inject.ts +15 -4
- package/src/decorators/injectable.ts +3 -3
- package/src/errors.ts +21 -0
- package/src/index.ts +15 -7
- package/src/{dependency-graph.ts → introspection/dependency-graph.ts} +1 -1
- package/src/{graph-adapters → introspection/graph-adapters}/cytoscape.ts +1 -1
- package/src/{graph-adapters → introspection/graph-adapters}/dot.ts +1 -1
- package/src/{graph-adapters → introspection/graph-adapters}/reactflow.ts +13 -2
- package/src/{inspector.ts → introspection/inspector.ts} +26 -21
- package/src/metadata/metadata-keys.ts +3 -6
- package/src/metadata/symbol-metadata-reader.ts +4 -4
- package/src/module.ts +14 -6
- package/src/registry.ts +97 -64
- package/src/resolution/activation-need.ts +81 -0
- package/src/resolution/binding-lookup-cache.ts +132 -0
- package/src/{binding-select.ts → resolution/binding-select.ts} +17 -7
- package/src/resolution/class-introspector.ts +74 -0
- package/src/resolution/diagnostics.ts +43 -0
- package/src/{environment.ts → resolution/environment.ts} +65 -35
- package/src/resolution/instantiation-plan.ts +292 -0
- package/src/{lifecycle.ts → resolution/lifecycle.ts} +26 -14
- package/src/resolution/resolution-path.ts +62 -0
- package/src/resolution/resolver.ts +1526 -0
- package/src/resolution/scope.ts +95 -0
- package/src/token.ts +0 -3
- package/dist/binding-scope.d.ts.map +0 -1
- package/dist/binding-scope.js.map +0 -1
- package/dist/binding-select.d.ts.map +0 -1
- package/dist/binding-select.js.map +0 -1
- package/dist/constraints.d.ts.map +0 -1
- package/dist/constraints.js.map +0 -1
- package/dist/container.d.ts.map +0 -1
- package/dist/container.js +0 -853
- package/dist/container.js.map +0 -1
- package/dist/dependency-graph.d.ts.map +0 -1
- package/dist/dependency-graph.js.map +0 -1
- package/dist/environment.d.ts.map +0 -1
- package/dist/environment.js +0 -100
- package/dist/environment.js.map +0 -1
- package/dist/graph-adapters/cytoscape.d.ts.map +0 -1
- package/dist/graph-adapters/cytoscape.js.map +0 -1
- package/dist/graph-adapters/dot.d.ts +0 -6
- package/dist/graph-adapters/dot.d.ts.map +0 -1
- package/dist/graph-adapters/dot.js.map +0 -1
- package/dist/graph-adapters/reactflow.d.ts.map +0 -1
- package/dist/graph-adapters/reactflow.js.map +0 -1
- package/dist/inspector.d.ts.map +0 -1
- package/dist/inspector.js.map +0 -1
- package/dist/lifecycle.d.ts.map +0 -1
- package/dist/lifecycle.js.map +0 -1
- package/dist/resolve-options.d.ts.map +0 -1
- package/dist/resolve-options.js.map +0 -1
- package/dist/resolver.d.ts +0 -90
- package/dist/resolver.d.ts.map +0 -1
- package/dist/resolver.js +0 -1208
- package/dist/resolver.js.map +0 -1
- package/dist/scope.d.ts.map +0 -1
- package/dist/scope.js +0 -61
- package/dist/scope.js.map +0 -1
- package/src/container.ts +0 -1141
- package/src/resolver.ts +0 -1626
- package/src/scope.ts +0 -77
- /package/dist/{dependency-graph.d.ts → introspection/dependency-graph.d.ts} +0 -0
- /package/dist/{graph-adapters → introspection/graph-adapters}/cytoscape.js +0 -0
- /package/dist/{graph-adapters → introspection/graph-adapters}/dot.js +0 -0
- /package/dist/{binding-scope.d.ts → resolution/binding-scope.d.ts} +0 -0
- /package/dist/{binding-scope.js → resolution/binding-scope.js} +0 -0
- /package/dist/{binding-select.d.ts → resolution/binding-select.d.ts} +0 -0
- /package/dist/{constraints.d.ts → resolution/constraints.d.ts} +0 -0
- /package/dist/{constraints.js → resolution/constraints.js} +0 -0
- /package/dist/{resolve-options.d.ts → resolution/resolve-options.d.ts} +0 -0
- /package/dist/{resolve-options.js → resolution/resolve-options.js} +0 -0
- /package/src/{binding-scope.ts → resolution/binding-scope.ts} +0 -0
- /package/src/{constraints.ts → resolution/constraints.ts} +0 -0
- /package/src/{resolve-options.ts → resolution/resolve-options.ts} +0 -0
package/src/binding.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { InjectionDescriptor } from "#/decorators/inject";
|
|
1
|
+
import type { InjectableDependency, InjectionDescriptor, ResolvedDependencyValue } from "#/decorators/inject";
|
|
2
2
|
import type { Token } from "#/token";
|
|
3
3
|
import type {
|
|
4
4
|
ActivationHandler,
|
|
@@ -7,9 +7,8 @@ import type {
|
|
|
7
7
|
BindingTag,
|
|
8
8
|
Constructor,
|
|
9
9
|
DeactivationHandler,
|
|
10
|
-
DependencyKey,
|
|
11
10
|
ResolutionContext,
|
|
12
|
-
|
|
11
|
+
ResolutionFrame,
|
|
13
12
|
ConstraintContext,
|
|
14
13
|
} from "#/types";
|
|
15
14
|
|
|
@@ -41,6 +40,13 @@ export function bindingSlotEquals(left: BindingSlot, right: BindingSlot): boolea
|
|
|
41
40
|
return true;
|
|
42
41
|
}
|
|
43
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Cached singleton absent — distinguishes "not resolved yet" from a cached `undefined`.
|
|
45
|
+
*
|
|
46
|
+
* @since 0.5.0-canary.8
|
|
47
|
+
*/
|
|
48
|
+
export const NO_INSTANCE: unique symbol = Symbol("di:no-instance");
|
|
49
|
+
|
|
44
50
|
/**
|
|
45
51
|
* @since 0.3.16-canary.0
|
|
46
52
|
*/
|
|
@@ -67,9 +73,31 @@ export function bindingSlotToString(slot: BindingSlot): string {
|
|
|
67
73
|
|
|
68
74
|
interface BindingBase<Value> {
|
|
69
75
|
readonly id: BindingIdentifier;
|
|
76
|
+
/**
|
|
77
|
+
* True while this binding's factory is on the sync resolution stack — the sync lane's cycle check.
|
|
78
|
+
*
|
|
79
|
+
* @remarks Resolver-owned bookkeeping; callers never set it. See `ARCHITECTURE.md` for why the
|
|
80
|
+
* sync and async lanes detect cycles differently.
|
|
81
|
+
*/
|
|
82
|
+
inFlight?: boolean | undefined;
|
|
83
|
+
/**
|
|
84
|
+
* Memoized resolution frame for this binding. Its contents derive only from immutable binding
|
|
85
|
+
* fields, so it is computed once on first resolve and reused instead of a per-resolver Map
|
|
86
|
+
* lookup on every hop.
|
|
87
|
+
*
|
|
88
|
+
* @remarks Resolver-owned bookkeeping — `registry.add` normalizes it, so callers never set it.
|
|
89
|
+
*/
|
|
90
|
+
frame?: ResolutionFrame | undefined;
|
|
91
|
+
/**
|
|
92
|
+
* Cached singleton instance, or {@link NO_INSTANCE}.
|
|
93
|
+
*
|
|
94
|
+
* @remarks A binding belongs to exactly one container, so its singleton slot is per-binding —
|
|
95
|
+
* a field read replaces a keyed lookup on the hottest resolve shape there is.
|
|
96
|
+
*/
|
|
97
|
+
instance?: unknown;
|
|
70
98
|
readonly token: Token<Value> | Constructor<Value>;
|
|
71
99
|
readonly slot: BindingSlot;
|
|
72
|
-
readonly predicate?: (ctx: ConstraintContext) => boolean;
|
|
100
|
+
readonly predicate?: ((ctx: ConstraintContext) => boolean) | undefined;
|
|
73
101
|
}
|
|
74
102
|
|
|
75
103
|
type BindingBaseKeys = keyof BindingBase<unknown>;
|
|
@@ -83,8 +111,8 @@ export interface ClassBinding<Value> extends BindingBase<Value> {
|
|
|
83
111
|
readonly kind: "class";
|
|
84
112
|
readonly target: Constructor<Value>;
|
|
85
113
|
readonly scope: BindingScope;
|
|
86
|
-
readonly onActivation?: ActivationHandler<Value
|
|
87
|
-
readonly onDeactivation?: DeactivationHandler<Value
|
|
114
|
+
readonly onActivation?: ActivationHandler<Value> | undefined;
|
|
115
|
+
readonly onDeactivation?: DeactivationHandler<Value> | undefined;
|
|
88
116
|
}
|
|
89
117
|
|
|
90
118
|
/**
|
|
@@ -94,8 +122,8 @@ export interface DynamicBinding<Value> extends BindingBase<Value> {
|
|
|
94
122
|
readonly kind: "dynamic";
|
|
95
123
|
readonly factory: (ctx: ResolutionContext) => Value;
|
|
96
124
|
readonly scope: BindingScope;
|
|
97
|
-
readonly onActivation?: ActivationHandler<Value
|
|
98
|
-
readonly onDeactivation?: DeactivationHandler<Value
|
|
125
|
+
readonly onActivation?: ActivationHandler<Value> | undefined;
|
|
126
|
+
readonly onDeactivation?: DeactivationHandler<Value> | undefined;
|
|
99
127
|
}
|
|
100
128
|
|
|
101
129
|
/**
|
|
@@ -105,8 +133,8 @@ export interface DynamicAsyncBinding<Value> extends BindingBase<Value> {
|
|
|
105
133
|
readonly kind: "dynamic-async";
|
|
106
134
|
readonly factory: (ctx: ResolutionContext) => Promise<Value>;
|
|
107
135
|
readonly scope: BindingScope;
|
|
108
|
-
readonly onActivation?: ActivationHandler<Value
|
|
109
|
-
readonly onDeactivation?: DeactivationHandler<Value
|
|
136
|
+
readonly onActivation?: ActivationHandler<Value> | undefined;
|
|
137
|
+
readonly onDeactivation?: DeactivationHandler<Value> | undefined;
|
|
110
138
|
}
|
|
111
139
|
|
|
112
140
|
/**
|
|
@@ -117,8 +145,8 @@ export interface ResolvedBinding<Value> extends BindingBase<Value> {
|
|
|
117
145
|
readonly factory: (...args: Array<unknown>) => Value;
|
|
118
146
|
readonly deps: ReadonlyArray<InjectionDescriptor>;
|
|
119
147
|
readonly scope: BindingScope;
|
|
120
|
-
readonly onActivation?: ActivationHandler<Value
|
|
121
|
-
readonly onDeactivation?: DeactivationHandler<Value
|
|
148
|
+
readonly onActivation?: ActivationHandler<Value> | undefined;
|
|
149
|
+
readonly onDeactivation?: DeactivationHandler<Value> | undefined;
|
|
122
150
|
}
|
|
123
151
|
|
|
124
152
|
/**
|
|
@@ -129,8 +157,8 @@ export interface ResolvedAsyncBinding<Value> extends BindingBase<Value> {
|
|
|
129
157
|
readonly factory: (...args: Array<unknown>) => Promise<Value>;
|
|
130
158
|
readonly deps: ReadonlyArray<InjectionDescriptor>;
|
|
131
159
|
readonly scope: BindingScope;
|
|
132
|
-
readonly onActivation?: ActivationHandler<Value
|
|
133
|
-
readonly onDeactivation?: DeactivationHandler<Value
|
|
160
|
+
readonly onActivation?: ActivationHandler<Value> | undefined;
|
|
161
|
+
readonly onDeactivation?: DeactivationHandler<Value> | undefined;
|
|
134
162
|
}
|
|
135
163
|
|
|
136
164
|
/**
|
|
@@ -140,8 +168,8 @@ export interface ConstantBinding<Value> extends BindingBase<Value> {
|
|
|
140
168
|
readonly kind: "constant";
|
|
141
169
|
readonly value: Value;
|
|
142
170
|
readonly scope: "singleton";
|
|
143
|
-
readonly onActivation?: ActivationHandler<Value
|
|
144
|
-
readonly onDeactivation?: DeactivationHandler<Value
|
|
171
|
+
readonly onActivation?: ActivationHandler<Value> | undefined;
|
|
172
|
+
readonly onDeactivation?: DeactivationHandler<Value> | undefined;
|
|
145
173
|
}
|
|
146
174
|
|
|
147
175
|
/**
|
|
@@ -180,12 +208,99 @@ export type PartialBinding<Value> =
|
|
|
180
208
|
|
|
181
209
|
// ── ID generation ─────────────────────────────────────────────────────────────
|
|
182
210
|
|
|
183
|
-
let
|
|
211
|
+
let bindingIdCounter = 0;
|
|
184
212
|
/**
|
|
185
213
|
* @since 0.3.16-canary.0
|
|
186
214
|
*/
|
|
187
215
|
export function generateBindingId(): BindingIdentifier {
|
|
188
|
-
return String(++
|
|
216
|
+
return String(++bindingIdCounter) as BindingIdentifier;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// ── Construction ──────────────────────────────────────────────────────────────
|
|
220
|
+
|
|
221
|
+
// Superset of every kind's fields, so one literal can copy any binding shape.
|
|
222
|
+
/** Union of every key any binding kind declares — `keyof` a union would give the intersection. */
|
|
223
|
+
type BindingFieldName = Binding<unknown> extends infer Kind ? (Kind extends unknown ? keyof Kind : never) : never;
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Completeness guard for {@link createBinding}'s literal.
|
|
227
|
+
*
|
|
228
|
+
* @remarks The literal is `satisfies` this, so a field added to any binding kind that the literal
|
|
229
|
+
* forgets to write is a compile error rather than a binding silently missing it.
|
|
230
|
+
*/
|
|
231
|
+
type ConstructedBindingFields = Record<BindingFieldName, unknown>;
|
|
232
|
+
|
|
233
|
+
type BindingFieldSuperset = {
|
|
234
|
+
readonly kind: Binding["kind"];
|
|
235
|
+
readonly instance?: unknown;
|
|
236
|
+
readonly scope?: unknown;
|
|
237
|
+
readonly target?: unknown;
|
|
238
|
+
readonly factory?: unknown;
|
|
239
|
+
readonly deps?: unknown;
|
|
240
|
+
readonly value?: unknown;
|
|
241
|
+
readonly onActivation?: unknown;
|
|
242
|
+
readonly onDeactivation?: unknown;
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* The single construction site for bindings — one literal, one V8 hidden class.
|
|
247
|
+
*
|
|
248
|
+
* @see `ARCHITECTURE.md` — why the field order and the single site are load-bearing.
|
|
249
|
+
*
|
|
250
|
+
* @param source - the kind-specific payload, or an existing binding to re-slot
|
|
251
|
+
* @param id - reuse a caller's id to keep a fluent chain's `id()` stable across refinements
|
|
252
|
+
*
|
|
253
|
+
* @since 0.5.0-canary.8
|
|
254
|
+
*/
|
|
255
|
+
export function createBinding<Value>(
|
|
256
|
+
source: PartialBinding<Value> | Binding<Value>,
|
|
257
|
+
token: Token<Value> | Constructor<Value>,
|
|
258
|
+
slot: BindingSlot,
|
|
259
|
+
predicate: ((ctx: ConstraintContext) => boolean) | undefined,
|
|
260
|
+
id: BindingIdentifier = generateBindingId(),
|
|
261
|
+
): Binding<Value> {
|
|
262
|
+
const fields = source as BindingFieldSuperset;
|
|
263
|
+
return {
|
|
264
|
+
kind: fields.kind,
|
|
265
|
+
id,
|
|
266
|
+
inFlight: false,
|
|
267
|
+
frame: undefined,
|
|
268
|
+
instance: (source as { instance?: unknown }).instance ?? NO_INSTANCE,
|
|
269
|
+
token,
|
|
270
|
+
slot,
|
|
271
|
+
predicate,
|
|
272
|
+
scope: fields.scope,
|
|
273
|
+
target: fields.target,
|
|
274
|
+
factory: fields.factory,
|
|
275
|
+
deps: fields.deps,
|
|
276
|
+
value: fields.value,
|
|
277
|
+
onActivation: fields.onActivation,
|
|
278
|
+
onDeactivation: fields.onDeactivation,
|
|
279
|
+
} satisfies ConstructedBindingFields as Binding<Value>;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Writable view of the only fields a fluent chain may refine after registration.
|
|
284
|
+
*
|
|
285
|
+
* @remarks No registry index is keyed on these, so a builder that owns the registered object
|
|
286
|
+
* can write them directly instead of re-registering. `token`, `slot`, `predicate` and `id`
|
|
287
|
+
* are excluded on purpose — changing those means re-indexing.
|
|
288
|
+
*
|
|
289
|
+
* @since 0.5.0-canary.8
|
|
290
|
+
*/
|
|
291
|
+
export interface RefinableBindingFields<Value> {
|
|
292
|
+
onActivation: ActivationHandler<Value> | undefined;
|
|
293
|
+
onDeactivation: DeactivationHandler<Value> | undefined;
|
|
294
|
+
scope: BindingScope;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Narrows a registered binding to the fields a fluent chain may still refine.
|
|
299
|
+
*
|
|
300
|
+
* @since 0.5.0-canary.8
|
|
301
|
+
*/
|
|
302
|
+
export function refinableFields<Value>(binding: Binding<Value>): RefinableBindingFields<Value> {
|
|
303
|
+
return binding as RefinableBindingFields<Value>;
|
|
189
304
|
}
|
|
190
305
|
|
|
191
306
|
// ── Builder interfaces ────────────────────────────────────────────────────────
|
|
@@ -212,12 +327,12 @@ export interface BindToBuilder<Value> {
|
|
|
212
327
|
toConstantValue(value: Value): ConstantBindingBuilder<Value>;
|
|
213
328
|
toDynamic(factory: (ctx: ResolutionContext) => Value): BindingBuilder<Value>;
|
|
214
329
|
toDynamicAsync(factory: (ctx: ResolutionContext) => Promise<Value>): BindingBuilder<Value>;
|
|
215
|
-
toResolved<const Deps extends ReadonlyArray<
|
|
216
|
-
factory: (...args: { [K in keyof Deps]:
|
|
330
|
+
toResolved<const Deps extends ReadonlyArray<InjectableDependency>>(
|
|
331
|
+
factory: (...args: { [K in keyof Deps]: ResolvedDependencyValue<NoInfer<Deps>[K]> }) => Value,
|
|
217
332
|
deps: Deps,
|
|
218
333
|
): BindingBuilder<Value>;
|
|
219
|
-
toResolvedAsync<const Deps extends ReadonlyArray<
|
|
220
|
-
factory: (...args: { [K in keyof Deps]:
|
|
334
|
+
toResolvedAsync<const Deps extends ReadonlyArray<InjectableDependency>>(
|
|
335
|
+
factory: (...args: { [K in keyof Deps]: ResolvedDependencyValue<NoInfer<Deps>[K]> }) => Promise<Value>,
|
|
221
336
|
deps: Deps,
|
|
222
337
|
): BindingBuilder<Value>;
|
|
223
338
|
toAlias(target: Token<Value> | Constructor<Value>): AliasBindingBuilder;
|
package/src/constructor-type.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* A class
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* this alias is the DI “class token” surface only.
|
|
2
|
+
* A class token: newable, producing `Value`.
|
|
3
|
+
*
|
|
4
|
+
* @remarks Rest parameters are `never[]` so classes with typed constructors stay assignable under
|
|
5
|
+
* `strictFunctionTypes`. Construction uses the real shape; this alias is the token surface only.
|
|
7
6
|
*
|
|
8
7
|
* @since 0.3.16-canary.0
|
|
9
8
|
*/
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The fluent chain `bind()` returns: it registers the binding and refines it in place.
|
|
3
|
+
*
|
|
4
|
+
* @see `ARCHITECTURE.md` — why one object plays every role, and what pins the ordering.
|
|
5
|
+
*/
|
|
6
|
+
import type {
|
|
7
|
+
AliasBindingBuilder,
|
|
8
|
+
Binding,
|
|
9
|
+
BindingBuilder,
|
|
10
|
+
BindingSlot,
|
|
11
|
+
BindToBuilder,
|
|
12
|
+
ConstantBindingBuilder,
|
|
13
|
+
PartialBinding,
|
|
14
|
+
ScopedBindingBuilder,
|
|
15
|
+
SingletonBindingBuilder,
|
|
16
|
+
SingletonLifecycleBuilder,
|
|
17
|
+
TransientBindingBuilder,
|
|
18
|
+
} from "#/binding";
|
|
19
|
+
import { bindingSlotEquals, createBinding, DEFAULT_BINDING_SLOT, refinableFields } from "#/binding";
|
|
20
|
+
import type { InjectableDependency, ResolvedDependencyValue } from "#/decorators/inject";
|
|
21
|
+
import { normalizeToDescriptor } from "#/decorators/inject";
|
|
22
|
+
import { ChainNotRegisteredError } from "#/errors";
|
|
23
|
+
import type { BindingRegistry } from "#/registry";
|
|
24
|
+
import type { Token } from "#/token";
|
|
25
|
+
import { tokenName } from "#/token";
|
|
26
|
+
import type {
|
|
27
|
+
ActivationHandler,
|
|
28
|
+
BindingIdentifier,
|
|
29
|
+
BindingScope,
|
|
30
|
+
ConstraintContext,
|
|
31
|
+
Constructor,
|
|
32
|
+
DeactivationHandler,
|
|
33
|
+
ResolutionContext,
|
|
34
|
+
} from "#/types";
|
|
35
|
+
|
|
36
|
+
function updateSlotTag(slot: BindingSlot, tag: string, value: unknown): BindingSlot {
|
|
37
|
+
const tags = [...slot.tags];
|
|
38
|
+
const existingIndex = tags.findIndex(([key]) => key === tag);
|
|
39
|
+
if (existingIndex === -1) {
|
|
40
|
+
tags.push([tag, value]);
|
|
41
|
+
} else {
|
|
42
|
+
tags[existingIndex] = [tag, value];
|
|
43
|
+
}
|
|
44
|
+
return { ...slot, tags };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// True when re-adding `restored` would immediately be displaced again by `current`
|
|
48
|
+
// (both slot-based with equal slots) — in that case the replacement was legitimate.
|
|
49
|
+
function displacesRestoredBinding(current: Binding, restored: Binding): boolean {
|
|
50
|
+
const currentIsPurePredicate =
|
|
51
|
+
current.predicate !== undefined && current.slot.name === undefined && current.slot.tags.length === 0;
|
|
52
|
+
if (currentIsPurePredicate) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
return bindingSlotEquals(current.slot, restored.slot);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Record a module's binding id, dropping the id the chain re-slotted away from. */
|
|
59
|
+
function trackBindingForModule(
|
|
60
|
+
ids: Array<BindingIdentifier>,
|
|
61
|
+
id: BindingIdentifier,
|
|
62
|
+
previousId: BindingIdentifier | undefined,
|
|
63
|
+
): void {
|
|
64
|
+
if (previousId !== undefined) {
|
|
65
|
+
const previousIndex = ids.indexOf(previousId);
|
|
66
|
+
if (previousIndex !== -1) {
|
|
67
|
+
ids.splice(previousIndex, 1);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
ids.push(id);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ── Registration target ───────────────────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Where a chain registers, and on whose behalf.
|
|
77
|
+
*
|
|
78
|
+
* @remarks Built once per container and shared by every chain it creates. `moduleBindingIds` is
|
|
79
|
+
* present exactly when the chain belongs to a module load.
|
|
80
|
+
*
|
|
81
|
+
* @since 0.5.0-canary.8
|
|
82
|
+
*/
|
|
83
|
+
export interface BindingRegistration {
|
|
84
|
+
readonly registry: BindingRegistry;
|
|
85
|
+
readonly moduleBindingIds: Array<BindingIdentifier> | undefined;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ── BindingChain ──────────────────────────────────────────────────────────────
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The one builder behind `bind()` and every `to*()` return type. Each interface exposes only the
|
|
92
|
+
* calls that are legal at that point in the chain; the runtime object is shared because every
|
|
93
|
+
* refinement is the same operation — narrow the registered binding, keep its id.
|
|
94
|
+
*
|
|
95
|
+
* @since 0.5.0-canary.8
|
|
96
|
+
*/
|
|
97
|
+
export class BindingChain<Value>
|
|
98
|
+
implements
|
|
99
|
+
AliasBindingBuilder,
|
|
100
|
+
BindingBuilder<Value>,
|
|
101
|
+
BindToBuilder<Value>,
|
|
102
|
+
ConstantBindingBuilder<Value>,
|
|
103
|
+
ScopedBindingBuilder<Value>,
|
|
104
|
+
SingletonBindingBuilder<Value>,
|
|
105
|
+
SingletonLifecycleBuilder<Value>,
|
|
106
|
+
TransientBindingBuilder<Value>
|
|
107
|
+
{
|
|
108
|
+
// Undefined until a `to*()` call registers the binding.
|
|
109
|
+
#binding: Binding<Value> | undefined;
|
|
110
|
+
// Allocated only by a chain that actually displaces something — most never do.
|
|
111
|
+
#displacedByChain: Array<Binding> | undefined;
|
|
112
|
+
readonly #token: Token<Value> | Constructor<Value>;
|
|
113
|
+
readonly #registration: BindingRegistration;
|
|
114
|
+
|
|
115
|
+
constructor(token: Token<Value> | Constructor<Value>, registration: BindingRegistration) {
|
|
116
|
+
this.#token = token;
|
|
117
|
+
this.#registration = registration;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** The registered binding, or a loud failure if no `to*()` has run yet. */
|
|
121
|
+
#registered(): Binding<Value> {
|
|
122
|
+
if (this.#binding === undefined) {
|
|
123
|
+
throw new ChainNotRegisteredError(tokenName(this.#token));
|
|
124
|
+
}
|
|
125
|
+
return this.#binding;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
#register(partial: PartialBinding<Value>): this {
|
|
129
|
+
// Each `to*()` starts its own registration, so anything a previous one displaced is not this
|
|
130
|
+
// registration's to restore.
|
|
131
|
+
this.#displacedByChain = undefined;
|
|
132
|
+
this.#binding = createBinding(partial, this.#token, DEFAULT_BINDING_SLOT, undefined);
|
|
133
|
+
this.#commit(this.#binding, undefined);
|
|
134
|
+
return this;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ── Registration ───────────────────────────────────────────────────────────
|
|
138
|
+
|
|
139
|
+
to(type: Constructor<Value>): BindingBuilder<Value> {
|
|
140
|
+
return this.#register({ kind: "class", target: type, scope: "transient" });
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
toSelf(): BindingBuilder<Value> {
|
|
144
|
+
if (typeof this.#token !== "function") {
|
|
145
|
+
throw new Error("toSelf() requires token to be a Constructor");
|
|
146
|
+
}
|
|
147
|
+
return this.#register({ kind: "class", target: this.#token, scope: "transient" });
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
toConstantValue(value: Value): ConstantBindingBuilder<Value> {
|
|
151
|
+
return this.#register({ kind: "constant", scope: "singleton", value });
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
toDynamic(factory: (ctx: ResolutionContext) => Value): BindingBuilder<Value> {
|
|
155
|
+
return this.#register({ kind: "dynamic", factory, scope: "transient" });
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
toDynamicAsync(factory: (ctx: ResolutionContext) => Promise<Value>): BindingBuilder<Value> {
|
|
159
|
+
return this.#register({ kind: "dynamic-async", factory, scope: "transient" });
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
toResolved<const Deps extends ReadonlyArray<InjectableDependency>>(
|
|
163
|
+
factory: (...args: { [K in keyof Deps]: ResolvedDependencyValue<NoInfer<Deps>[K]> }) => Value,
|
|
164
|
+
deps: Deps,
|
|
165
|
+
): BindingBuilder<Value> {
|
|
166
|
+
return this.#register({
|
|
167
|
+
kind: "resolved",
|
|
168
|
+
deps: deps.map((dependency) => normalizeToDescriptor(dependency)),
|
|
169
|
+
factory: factory as (...args: Array<unknown>) => Value,
|
|
170
|
+
scope: "transient",
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
toResolvedAsync<const Deps extends ReadonlyArray<InjectableDependency>>(
|
|
175
|
+
factory: (...args: { [K in keyof Deps]: ResolvedDependencyValue<NoInfer<Deps>[K]> }) => Promise<Value>,
|
|
176
|
+
deps: Deps,
|
|
177
|
+
): BindingBuilder<Value> {
|
|
178
|
+
return this.#register({
|
|
179
|
+
kind: "resolved-async",
|
|
180
|
+
deps: deps.map((dependency) => normalizeToDescriptor(dependency)),
|
|
181
|
+
factory: factory as (...args: Array<unknown>) => Promise<Value>,
|
|
182
|
+
scope: "transient",
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
toAlias(target: Token<Value> | Constructor<Value>): AliasBindingBuilder {
|
|
187
|
+
return this.#register({ kind: "alias", target });
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ── Refinement ─────────────────────────────────────────────────────────────
|
|
191
|
+
|
|
192
|
+
// Slot and predicate are what the registry indexes on, so a re-slot rebuilds the binding and
|
|
193
|
+
// re-registers it — under the original id, keeping `id()` stable for the whole chain.
|
|
194
|
+
#reslot(slot: BindingSlot, predicate: ((ctx: ConstraintContext) => boolean) | undefined): this {
|
|
195
|
+
const previous = this.#registered();
|
|
196
|
+
this.#binding = createBinding(previous, previous.token, slot, predicate, previous.id);
|
|
197
|
+
this.#commit(this.#binding, previous.id);
|
|
198
|
+
return this;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
#withScope(scope: BindingScope): this {
|
|
202
|
+
refinableFields(this.#registered()).scope = scope;
|
|
203
|
+
this.#registration.registry.touch();
|
|
204
|
+
return this;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
when(predicate: (ctx: ConstraintContext) => boolean): this {
|
|
208
|
+
return this.#reslot(this.#registered().slot, predicate);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
whenNamed(name: string): this {
|
|
212
|
+
return this.#reslot({ ...this.#registered().slot, name }, this.#registered().predicate);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
whenTagged(tag: string, value: unknown): this {
|
|
216
|
+
const binding = this.#registered();
|
|
217
|
+
return this.#reslot(updateSlotTag(binding.slot, tag, value), binding.predicate);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
whenDefault(): this {
|
|
221
|
+
// The default slot is what a fresh registration already has, so there is nothing to re-slot —
|
|
222
|
+
// but an unregistered chain must fail here exactly as it does in every other refinement.
|
|
223
|
+
this.#registered();
|
|
224
|
+
return this;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
singleton(): SingletonBindingBuilder<Value> {
|
|
228
|
+
return this.#withScope("singleton");
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
transient(): TransientBindingBuilder<Value> {
|
|
232
|
+
return this.#withScope("transient");
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
scoped(): ScopedBindingBuilder<Value> {
|
|
236
|
+
return this.#withScope("scoped");
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
onActivation(fn: ActivationHandler<Value>): this {
|
|
240
|
+
refinableFields(this.#registered()).onActivation = fn;
|
|
241
|
+
this.#registration.registry.touch();
|
|
242
|
+
return this;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
onDeactivation(fn: DeactivationHandler<Value>): this {
|
|
246
|
+
refinableFields(this.#registered()).onDeactivation = fn;
|
|
247
|
+
this.#registration.registry.touch();
|
|
248
|
+
return this;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
id(): BindingIdentifier {
|
|
252
|
+
return this.#registered().id;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// ── Registry ───────────────────────────────────────────────────────────────
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Register `binding`, first removing `previousId` when the chain is re-slotting.
|
|
259
|
+
*
|
|
260
|
+
* @remarks A `when*()` that follows `to*()` re-slots an already-live binding and can displace one
|
|
261
|
+
* the final shape would never conflict with. Those stay parked in `#displacedByChain` until the
|
|
262
|
+
* chain settles, then get restored.
|
|
263
|
+
*/
|
|
264
|
+
#commit(binding: Binding<Value>, previousId: BindingIdentifier | undefined): void {
|
|
265
|
+
const { registry, moduleBindingIds } = this.#registration;
|
|
266
|
+
// The registry is heterogeneous by design — one instance holds every value type — so the
|
|
267
|
+
// chain's `Binding<Value>` is erased once, here, rather than at each call site.
|
|
268
|
+
const registered = binding as Binding;
|
|
269
|
+
|
|
270
|
+
if (previousId !== undefined) {
|
|
271
|
+
registry.removeById(previousId);
|
|
272
|
+
}
|
|
273
|
+
const displaced = registry.add(registered);
|
|
274
|
+
if (displaced !== undefined) {
|
|
275
|
+
(this.#displacedByChain ??= []).push(displaced);
|
|
276
|
+
}
|
|
277
|
+
if (previousId !== undefined && this.#displacedByChain !== undefined) {
|
|
278
|
+
this.#restoreNonConflicting(registered, this.#displacedByChain);
|
|
279
|
+
}
|
|
280
|
+
if (moduleBindingIds !== undefined) {
|
|
281
|
+
trackBindingForModule(moduleBindingIds, registered.id, previousId);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
#restoreNonConflicting(binding: Binding, displaced: Array<Binding>): void {
|
|
286
|
+
for (let index = displaced.length - 1; index >= 0; index -= 1) {
|
|
287
|
+
const candidate = displaced[index]!;
|
|
288
|
+
if (!displacesRestoredBinding(binding, candidate)) {
|
|
289
|
+
this.#registration.registry.add(candidate);
|
|
290
|
+
displaced.splice(index, 1);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|