@codefast/di 0.8.1 → 0.10.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 +164 -0
- package/LICENSE +1 -1
- package/README.md +29 -20
- package/dist/ambient/active-container.d.ts +13 -0
- package/dist/ambient/active-container.js +24 -0
- package/dist/container/binding-builders.d.ts +42 -20
- package/dist/container/binding-builders.js +155 -87
- package/dist/container/container.d.ts +10 -10
- package/dist/container/container.js +55 -23
- package/dist/core/binding.d.ts +55 -57
- package/dist/core/binding.js +11 -37
- package/dist/core/constraint-requirement.d.ts +19 -4
- package/dist/core/constraint-requirement.js +19 -9
- package/dist/core/registry.d.ts +47 -4
- package/dist/core/registry.js +361 -159
- package/dist/core/state-epoch.d.ts +16 -0
- package/dist/core/state-epoch.js +21 -0
- package/dist/core/tag.d.ts +2 -2
- package/dist/core/tag.js +2 -2
- package/dist/core/token.d.ts +24 -4
- package/dist/core/token.js +1 -1
- package/dist/core/types.d.ts +15 -6
- package/dist/decorators/inject.d.ts +1 -1
- package/dist/decorators/inject.js +6 -4
- package/dist/errors/diagnostics.d.ts +2 -0
- package/dist/errors/errors.d.ts +33 -1
- package/dist/errors/errors.js +44 -3
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/injection/descriptor.d.ts +16 -8
- package/dist/injection/descriptor.js +3 -1
- package/dist/injection/resolve-options.d.ts +6 -0
- package/dist/injection/resolve-options.js +16 -0
- package/dist/introspection/dependency-graph.js +10 -5
- package/dist/introspection/inspector.d.ts +3 -1
- package/dist/introspection/inspector.js +10 -24
- package/dist/lifecycle/lifecycle-manager.js +10 -8
- package/dist/lifecycle/scope-manager.js +1 -1
- package/dist/metadata/metadata-reader-token.js +1 -1
- package/dist/resolution/cache/activation-need.d.ts +2 -0
- package/dist/resolution/cache/activation-need.js +13 -6
- package/dist/resolution/cache/binding-lookup-cache.d.ts +34 -1
- package/dist/resolution/cache/binding-lookup-cache.js +96 -12
- package/dist/resolution/cache/class-introspector.d.ts +1 -1
- package/dist/resolution/cache/class-introspector.js +28 -14
- package/dist/resolution/context.d.ts +8 -8
- package/dist/resolution/plan/instantiation-plan.d.ts +12 -0
- package/dist/resolution/plan/instantiation-plan.js +156 -65
- package/dist/resolution/plan/plan-codegen.d.ts +100 -0
- package/dist/resolution/plan/plan-codegen.js +185 -0
- package/dist/resolution/resolver.d.ts +14 -4
- package/dist/resolution/resolver.js +375 -134
- package/dist/resolution/select/binding-select.js +12 -7
- package/dist/resolution/select/constraints.d.ts +7 -4
- package/dist/resolution/select/constraints.js +34 -13
- package/package.json +14 -40
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The process-wide counter every container mutation advances, so a chain walk can be skipped while nothing moved.
|
|
3
|
+
*/
|
|
4
|
+
let epoch = 0;
|
|
5
|
+
/**
|
|
6
|
+
* The current state epoch: unchanged between two reads exactly when no registry or lifecycle table
|
|
7
|
+
* in the process was mutated in between.
|
|
8
|
+
*
|
|
9
|
+
* @since 0.10.0
|
|
10
|
+
*/
|
|
11
|
+
export function stateEpoch() {
|
|
12
|
+
return epoch;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Advances the epoch; every registry version bump and every activation-hook registration calls it.
|
|
16
|
+
*
|
|
17
|
+
* @since 0.10.0
|
|
18
|
+
*/
|
|
19
|
+
export function advanceStateEpoch() {
|
|
20
|
+
epoch += 1;
|
|
21
|
+
}
|
package/dist/core/tag.d.ts
CHANGED
|
@@ -59,12 +59,12 @@ export interface TagKey<Value = unknown> {
|
|
|
59
59
|
/**
|
|
60
60
|
* Declares a tag key, whose `of()` builds the criteria a `whenTagged` and a resolve both take.
|
|
61
61
|
*
|
|
62
|
-
* @remarks The value type is checked at both ends: a key declared `tag<Region>("region")` refuses a
|
|
62
|
+
* @remarks The value type is checked at both ends: a key declared `tag<Region>("di:region")` refuses a
|
|
63
63
|
* value that is not a `Region`, so a bind site and a resolve site cannot drift apart silently.
|
|
64
64
|
*
|
|
65
65
|
* @example
|
|
66
66
|
* ```ts
|
|
67
|
-
* const Region = tag<"eu" | "us">("region");
|
|
67
|
+
* const Region = tag<"eu" | "us">("di:region");
|
|
68
68
|
* container.bind(Storage).to(S3).whenTagged(Region.of("eu"));
|
|
69
69
|
* container.resolve(Storage, { tag: Region.of("eu") });
|
|
70
70
|
* ```
|
package/dist/core/tag.js
CHANGED
|
@@ -27,12 +27,12 @@ function internKeyFor(value) {
|
|
|
27
27
|
/**
|
|
28
28
|
* Declares a tag key, whose `of()` builds the criteria a `whenTagged` and a resolve both take.
|
|
29
29
|
*
|
|
30
|
-
* @remarks The value type is checked at both ends: a key declared `tag<Region>("region")` refuses a
|
|
30
|
+
* @remarks The value type is checked at both ends: a key declared `tag<Region>("di:region")` refuses a
|
|
31
31
|
* value that is not a `Region`, so a bind site and a resolve site cannot drift apart silently.
|
|
32
32
|
*
|
|
33
33
|
* @example
|
|
34
34
|
* ```ts
|
|
35
|
-
* const Region = tag<"eu" | "us">("region");
|
|
35
|
+
* const Region = tag<"eu" | "us">("di:region");
|
|
36
36
|
* container.bind(Storage).to(S3).whenTagged(Region.of("eu"));
|
|
37
37
|
* container.resolve(Storage, { tag: Region.of("eu") });
|
|
38
38
|
* ```
|
package/dist/core/token.d.ts
CHANGED
|
@@ -1,20 +1,40 @@
|
|
|
1
1
|
import type { Constructor } from "#/core/constructor-type";
|
|
2
2
|
declare const TOKEN_BRAND: unique symbol;
|
|
3
|
+
declare const TOKEN_NAMES_BRAND: unique symbol;
|
|
3
4
|
/**
|
|
4
|
-
* A branded identifier carrying the value type its bindings resolve to.
|
|
5
|
+
* A branded identifier carrying the value type its bindings resolve to, and the slot names they may declare.
|
|
6
|
+
*
|
|
7
|
+
* @remarks `Names` exists at the type level only: `whenNamed`, and `name` in `ResolveOptions` and
|
|
8
|
+
* `InjectOptions`, narrow to it, so a bind site and a request site cannot drift apart silently. It
|
|
9
|
+
* is covariant, so a token declaring names is still a `Token<unknown>` wherever the engine erases
|
|
10
|
+
* the value type; the default `string` leaves a token that declares none unconstrained.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* const Logger = token<Logger, "console" | "file">("di:Logger");
|
|
15
|
+
* container.bind(Logger).to(FileLogger).whenNamed("file");
|
|
16
|
+
* container.resolve(Logger, { name: "file" });
|
|
17
|
+
* ```
|
|
5
18
|
*
|
|
6
19
|
* @since 0.3.16-canary.0
|
|
7
20
|
*/
|
|
8
|
-
export interface Token<out Value> {
|
|
21
|
+
export interface Token<out Value, out Names extends string = string> {
|
|
9
22
|
readonly name: string;
|
|
10
23
|
readonly [TOKEN_BRAND]: Value;
|
|
24
|
+
readonly [TOKEN_NAMES_BRAND]?: Names;
|
|
11
25
|
}
|
|
12
26
|
/**
|
|
13
|
-
*
|
|
27
|
+
* The slot names a dependency key declares — `string` for a class, or a token that declares none.
|
|
28
|
+
*
|
|
29
|
+
* @since 0.9.0
|
|
30
|
+
*/
|
|
31
|
+
export type SlotNamesOf<Key> = Key extends Token<unknown, infer Names extends string> ? Names : string;
|
|
32
|
+
/**
|
|
33
|
+
* Creates a named `Token` for the given value type, optionally declaring the slot names its bindings may use.
|
|
14
34
|
*
|
|
15
35
|
* @since 0.3.16-canary.0
|
|
16
36
|
*/
|
|
17
|
-
export declare function token<Value>(name: string): Token<Value>;
|
|
37
|
+
export declare function token<Value, Names extends string = string>(name: string): Token<Value, Names>;
|
|
18
38
|
/**
|
|
19
39
|
* Returns the display name of a token or class used as a dependency key.
|
|
20
40
|
*
|
package/dist/core/token.js
CHANGED
package/dist/core/types.d.ts
CHANGED
|
@@ -17,11 +17,14 @@ export type DependencyKey = Token<unknown> | Constructor;
|
|
|
17
17
|
export type BindingScope = "singleton" | "transient" | "scoped";
|
|
18
18
|
declare const BINDING_ID_BRAND: unique symbol;
|
|
19
19
|
/**
|
|
20
|
-
* A branded
|
|
20
|
+
* A branded number that uniquely identifies one binding for the life of the process.
|
|
21
|
+
*
|
|
22
|
+
* @remarks Minted from a counter, never parsed or displayed as an identity: the brand is what makes
|
|
23
|
+
* it opaque, and a number costs a plain bind nothing where a string cost it an allocation.
|
|
21
24
|
*
|
|
22
25
|
* @since 0.3.16-canary.0
|
|
23
26
|
*/
|
|
24
|
-
export type BindingIdentifier =
|
|
27
|
+
export type BindingIdentifier = number & {
|
|
25
28
|
readonly [BINDING_ID_BRAND]: true;
|
|
26
29
|
};
|
|
27
30
|
/**
|
|
@@ -47,8 +50,14 @@ export type DeactivationHandler<Value> = (instance: Value) => void | Promise<voi
|
|
|
47
50
|
*
|
|
48
51
|
* @since 0.3.16-canary.0
|
|
49
52
|
*/
|
|
50
|
-
export interface ResolveOptions {
|
|
51
|
-
|
|
53
|
+
export interface ResolveOptions<Names extends string = string> {
|
|
54
|
+
/**
|
|
55
|
+
* The slot name a binding declared with `whenNamed`.
|
|
56
|
+
*
|
|
57
|
+
* @remarks Narrowed to the names the token declares, so a request cannot ask for a name no
|
|
58
|
+
* binding could carry; a token declaring none takes any string.
|
|
59
|
+
*/
|
|
60
|
+
name?: Names | undefined;
|
|
52
61
|
/**
|
|
53
62
|
* Single-tag shorthand, equivalent to listing the one pair in `tags`.
|
|
54
63
|
*
|
|
@@ -103,8 +112,8 @@ export interface ResolutionContext {
|
|
|
103
112
|
resolveAsync<Value>(token: Token<Value> | Constructor<Value>, options?: ResolveOptions): Promise<Value>;
|
|
104
113
|
resolveOptional<Value>(token: Token<Value> | Constructor<Value>, options?: ResolveOptions): Value | undefined;
|
|
105
114
|
resolveOptionalAsync<Value>(token: Token<Value> | Constructor<Value>, options?: ResolveOptions): Promise<Value | undefined>;
|
|
106
|
-
resolveAll<Value>(token: Token<Value> | Constructor<Value>, options?: ResolveOptions):
|
|
107
|
-
resolveAllAsync<Value>(token: Token<Value> | Constructor<Value>, options?: ResolveOptions): Promise<
|
|
115
|
+
resolveAll<Value>(token: Token<Value> | Constructor<Value>, options?: ResolveOptions): ReadonlyArray<Value>;
|
|
116
|
+
resolveAllAsync<Value>(token: Token<Value> | Constructor<Value>, options?: ResolveOptions): Promise<ReadonlyArray<Value>>;
|
|
108
117
|
readonly graph: ConstraintContext;
|
|
109
118
|
}
|
|
110
119
|
/**
|
|
@@ -8,5 +8,5 @@ type ClassAccessorDecorator<This, Value> = (target: ClassAccessorDecoratorTarget
|
|
|
8
8
|
*
|
|
9
9
|
* @since 0.3.16-canary.0
|
|
10
10
|
*/
|
|
11
|
-
export declare function inject<Value>(token: Token<Value> | Constructor<Value>, options?: InjectOptions): InjectionDescriptor<Value> & ClassAccessorDecorator<unknown, Value>;
|
|
11
|
+
export declare function inject<Value, Names extends string = string>(token: Token<Value, Names> | Constructor<Value>, options?: NoInfer<InjectOptions<Names>>): InjectionDescriptor<Value> & ClassAccessorDecorator<unknown, Value>;
|
|
12
12
|
export {};
|
|
@@ -30,6 +30,8 @@ export function inject(token, options) {
|
|
|
30
30
|
// Derived from the descriptor, not from `options`: the descriptor is where the tag shorthand has
|
|
31
31
|
// already been folded. Built once here rather than per constructed instance.
|
|
32
32
|
const resolveOptions = injectionSlotToResolveOptions(descriptor);
|
|
33
|
+
// The names are checked above; the container lane only needs the value type.
|
|
34
|
+
const resolveKey = token;
|
|
33
35
|
const decoratorFn = (_target, context) => {
|
|
34
36
|
if (context.static) {
|
|
35
37
|
throw new StaticMemberDecoratorError("inject", String(context.name));
|
|
@@ -50,8 +52,8 @@ export function inject(token, options) {
|
|
|
50
52
|
const ambient = getAmbientResolution();
|
|
51
53
|
if (ambient !== undefined) {
|
|
52
54
|
const value = descriptor.optional
|
|
53
|
-
? ambient.resolveOptional(
|
|
54
|
-
: ambient.resolve(
|
|
55
|
+
? ambient.resolveOptional(resolveKey, resolveOptions)
|
|
56
|
+
: ambient.resolve(resolveKey, resolveOptions);
|
|
55
57
|
context.access.set(this, value);
|
|
56
58
|
return;
|
|
57
59
|
}
|
|
@@ -60,8 +62,8 @@ export function inject(token, options) {
|
|
|
60
62
|
throw new MissingContainerContextError(classNameOf(this), context.name);
|
|
61
63
|
}
|
|
62
64
|
const value = descriptor.optional
|
|
63
|
-
? container.resolveOptional(
|
|
64
|
-
: container.resolve(
|
|
65
|
+
? container.resolveOptional(resolveKey, resolveOptions)
|
|
66
|
+
: container.resolve(resolveKey, resolveOptions);
|
|
65
67
|
context.access.set(this, value);
|
|
66
68
|
});
|
|
67
69
|
return {};
|
|
@@ -25,6 +25,8 @@ export interface ResolutionDiagnostics {
|
|
|
25
25
|
readonly compiledPlanCount: number;
|
|
26
26
|
/** Bindings with a compiled async instantiation plan. */
|
|
27
27
|
readonly compiledAsyncPlanCount: number;
|
|
28
|
+
/** Plans this container's resolver has generated as functions of their own. */
|
|
29
|
+
readonly generatedPlanCount: number;
|
|
28
30
|
/** Contexts held by the depth-indexed sync pool. */
|
|
29
31
|
readonly syncContextPoolSize: number;
|
|
30
32
|
/** Scoped instances currently cached by this container's scope manager. */
|
package/dist/errors/errors.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ConstraintRequirement } from "#/core/constraint-requirement";
|
|
1
2
|
import type { BindingIdentifier, BindingScope, ResolveOptions } from "#/core/types";
|
|
2
3
|
/**
|
|
3
4
|
* Base class for every error the library throws, each carrying a machine-readable `code`.
|
|
@@ -132,8 +133,10 @@ export declare class UnreachableConstraintError extends DiError {
|
|
|
132
133
|
readonly code = "UNREACHABLE_CONSTRAINT";
|
|
133
134
|
readonly tokenName: string;
|
|
134
135
|
readonly requiredName: string;
|
|
136
|
+
/** The token the name was required on, or `undefined` when the constraint named no token. */
|
|
137
|
+
readonly requiredTokenName: string | undefined;
|
|
135
138
|
readonly helperName: string;
|
|
136
|
-
constructor(tokenName: string,
|
|
139
|
+
constructor(tokenName: string, requirement: ConstraintRequirement);
|
|
137
140
|
}
|
|
138
141
|
/**
|
|
139
142
|
* A container-level lifecycle hook whose token nothing is bound to, so it can never run.
|
|
@@ -229,6 +232,35 @@ export declare class MissingContainerContextError extends DiError {
|
|
|
229
232
|
*
|
|
230
233
|
* @since 0.5.0-canary.8
|
|
231
234
|
*/
|
|
235
|
+
/**
|
|
236
|
+
* A second `to*()` on a chain that already registered its binding.
|
|
237
|
+
*
|
|
238
|
+
* @remarks A chain is its binding, so it registers exactly once; a token bound twice is two `bind()`
|
|
239
|
+
* calls, the second of which displaces the first under slot last-wins.
|
|
240
|
+
*
|
|
241
|
+
* @since 0.10.0
|
|
242
|
+
*/
|
|
243
|
+
export declare class ChainAlreadyRegisteredError extends DiError {
|
|
244
|
+
readonly code = "CHAIN_ALREADY_REGISTERED";
|
|
245
|
+
readonly tokenName: string;
|
|
246
|
+
constructor(tokenName: string);
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* `many()` on a binding with a named or tagged slot, or a slot constraint on a collection member.
|
|
250
|
+
*
|
|
251
|
+
* @remarks A collection member keeps the default slot: its membership replaces slot last-wins, and a
|
|
252
|
+
* tagged member would have no index able to return every member of the tag.
|
|
253
|
+
*
|
|
254
|
+
* @since 0.10.0
|
|
255
|
+
*/
|
|
256
|
+
export declare class ManyBindingSlotError extends DiError {
|
|
257
|
+
readonly code = "MANY_BINDING_SLOT";
|
|
258
|
+
readonly tokenName: string;
|
|
259
|
+
constructor(tokenName: string);
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* @since 0.10.0
|
|
263
|
+
*/
|
|
232
264
|
export declare class ChainNotRegisteredError extends DiError {
|
|
233
265
|
readonly code = "CHAIN_NOT_REGISTERED";
|
|
234
266
|
readonly tokenName: string;
|
package/dist/errors/errors.js
CHANGED
|
@@ -191,11 +191,17 @@ export class UnreachableConstraintError extends DiError {
|
|
|
191
191
|
code = "UNREACHABLE_CONSTRAINT";
|
|
192
192
|
tokenName;
|
|
193
193
|
requiredName;
|
|
194
|
+
/** The token the name was required on, or `undefined` when the constraint named no token. */
|
|
195
|
+
requiredTokenName;
|
|
194
196
|
helperName;
|
|
195
|
-
constructor(tokenName,
|
|
196
|
-
|
|
197
|
+
constructor(tokenName, requirement) {
|
|
198
|
+
const { name, helperName, tokenName: requiredTokenName } = requirement;
|
|
199
|
+
const scope = requiredTokenName === undefined ? "no binding" : `no binding for '${requiredTokenName}'`;
|
|
200
|
+
const target = requiredTokenName === undefined ? "the binding it should match" : `a '${requiredTokenName}' binding`;
|
|
201
|
+
super(`The binding for '${tokenName}' is constrained by ${helperName} waiting on the slot name '${name}', but ${scope} in this container or its ancestors declares it, so the constraint can never hold. Name the slot with .whenNamed('${name}') on ${target}, or correct the name here.`);
|
|
197
202
|
this.tokenName = tokenName;
|
|
198
|
-
this.requiredName =
|
|
203
|
+
this.requiredName = name;
|
|
204
|
+
this.requiredTokenName = requiredTokenName;
|
|
199
205
|
this.helperName = helperName;
|
|
200
206
|
}
|
|
201
207
|
}
|
|
@@ -319,6 +325,41 @@ export class MissingContainerContextError extends DiError {
|
|
|
319
325
|
*
|
|
320
326
|
* @since 0.5.0-canary.8
|
|
321
327
|
*/
|
|
328
|
+
/**
|
|
329
|
+
* A second `to*()` on a chain that already registered its binding.
|
|
330
|
+
*
|
|
331
|
+
* @remarks A chain is its binding, so it registers exactly once; a token bound twice is two `bind()`
|
|
332
|
+
* calls, the second of which displaces the first under slot last-wins.
|
|
333
|
+
*
|
|
334
|
+
* @since 0.10.0
|
|
335
|
+
*/
|
|
336
|
+
export class ChainAlreadyRegisteredError extends DiError {
|
|
337
|
+
code = "CHAIN_ALREADY_REGISTERED";
|
|
338
|
+
tokenName;
|
|
339
|
+
constructor(tokenName) {
|
|
340
|
+
super(`The binding for token '${tokenName}' is already registered on this chain. Call bind() again to register another binding for the token.`);
|
|
341
|
+
this.tokenName = tokenName;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* `many()` on a binding with a named or tagged slot, or a slot constraint on a collection member.
|
|
346
|
+
*
|
|
347
|
+
* @remarks A collection member keeps the default slot: its membership replaces slot last-wins, and a
|
|
348
|
+
* tagged member would have no index able to return every member of the tag.
|
|
349
|
+
*
|
|
350
|
+
* @since 0.10.0
|
|
351
|
+
*/
|
|
352
|
+
export class ManyBindingSlotError extends DiError {
|
|
353
|
+
code = "MANY_BINDING_SLOT";
|
|
354
|
+
tokenName;
|
|
355
|
+
constructor(tokenName) {
|
|
356
|
+
super(`A many() binding for token '${tokenName}' keeps the default slot: it cannot also declare whenNamed() or whenTagged().`);
|
|
357
|
+
this.tokenName = tokenName;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* @since 0.10.0
|
|
362
|
+
*/
|
|
322
363
|
export class ChainNotRegisteredError extends DiError {
|
|
323
364
|
code = "CHAIN_NOT_REGISTERED";
|
|
324
365
|
tokenName;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export type { ActivationHandler, BindingConstraint, BindingIdentifier, BindingKind, BindingScope, BindingTag, ConstraintContext, Constructor, DependencyKey, DeactivationHandler, ResolutionFrame, ResolveOptions, ResolutionContext, TokenValue, } from "#/core/types";
|
|
2
2
|
export { token, tokenName } from "#/core/token";
|
|
3
|
-
export type { Token } from "#/core/token";
|
|
3
|
+
export type { SlotNamesOf, Token } from "#/core/token";
|
|
4
4
|
export { coversTagKeys, NO_TAG_KEYS, slotName, tag, tagKeyMaskOf } from "#/core/tag";
|
|
5
5
|
export type { TagKey, TagKeyMask } from "#/core/tag";
|
|
6
6
|
export type { AliasBindingBuilder, BindToBuilder, BindingBuilder, ConstantBindingBuilder, ScopedBindingBuilder, SingletonBindingBuilder, SingletonLifecycleBuilder, SlotConstrainedBuilder, TransientBindingBuilder, } from "#/core/binding";
|
|
@@ -25,7 +25,7 @@ export { MetadataReaderToken } from "#/metadata/metadata-reader-token";
|
|
|
25
25
|
export type { ConstructorMetadata, LifecycleMetadata, MetadataReader, MutableLifecycleMetadata, ParamMetadata, } from "#/metadata/metadata-types";
|
|
26
26
|
export { defaultMetadataReader, SymbolMetadataReader } from "#/metadata/symbol-metadata-reader";
|
|
27
27
|
export { whenAnyAncestorIs, whenAnyAncestorNamed, whenAnyAncestorTagged, whenAnyAncestorTaggedAll, whenNoAncestorIs, whenNoParentIs, whenParentIs, whenParentNamed, whenParentTagged, whenParentTaggedAll, } from "#/resolution/select/constraints";
|
|
28
|
-
export { AmbiguousBindingError, AsyncActivationError, AsyncDeactivationError, AsyncModuleLoadError, AsyncResolutionError, ChainNotRegisteredError, CircularDependencyError, DiError, DisposedContainerError, InternalError, InvalidMetadataError, MissingContainerContextError, MissingMetadataError, MissingScopeContextError, NoMatchingBindingError, RebindUnboundTokenError, ScopeViolationError, SelfBindingRequiresClassError, StaticMemberDecoratorError, SyncDisposalNotSupportedError, EmptyTagCriteriaError, TokenNotBoundError, UnreachableConstraintError, UnreachableLifecycleHookError, } from "#/errors/errors";
|
|
28
|
+
export { AmbiguousBindingError, AsyncActivationError, AsyncDeactivationError, AsyncModuleLoadError, AsyncResolutionError, ChainAlreadyRegisteredError, ChainNotRegisteredError, ManyBindingSlotError, CircularDependencyError, DiError, DisposedContainerError, InternalError, InvalidMetadataError, MissingContainerContextError, MissingMetadataError, MissingScopeContextError, NoMatchingBindingError, RebindUnboundTokenError, ScopeViolationError, SelfBindingRequiresClassError, StaticMemberDecoratorError, SyncDisposalNotSupportedError, EmptyTagCriteriaError, TokenNotBoundError, UnreachableConstraintError, UnreachableLifecycleHookError, } from "#/errors/errors";
|
|
29
29
|
export type { ScopeViolationDetails } from "#/errors/errors";
|
|
30
30
|
export { toDotGraph } from "#/introspection/graph-adapters/dot";
|
|
31
31
|
export { toCytoscapeGraph } from "#/introspection/graph-adapters/cytoscape";
|
package/dist/index.js
CHANGED
|
@@ -25,7 +25,7 @@ export { defaultMetadataReader, SymbolMetadataReader } from "#/metadata/symbol-m
|
|
|
25
25
|
// Constraints — contextual injection predicates for .when()
|
|
26
26
|
export { whenAnyAncestorIs, whenAnyAncestorNamed, whenAnyAncestorTagged, whenAnyAncestorTaggedAll, whenNoAncestorIs, whenNoParentIs, whenParentIs, whenParentNamed, whenParentTagged, whenParentTaggedAll, } from "#/resolution/select/constraints";
|
|
27
27
|
// Errors
|
|
28
|
-
export { AmbiguousBindingError, AsyncActivationError, AsyncDeactivationError, AsyncModuleLoadError, AsyncResolutionError, ChainNotRegisteredError, CircularDependencyError, DiError, DisposedContainerError, InternalError, InvalidMetadataError, MissingContainerContextError, MissingMetadataError, MissingScopeContextError, NoMatchingBindingError, RebindUnboundTokenError, ScopeViolationError, SelfBindingRequiresClassError, StaticMemberDecoratorError, SyncDisposalNotSupportedError, EmptyTagCriteriaError, TokenNotBoundError, UnreachableConstraintError, UnreachableLifecycleHookError, } from "#/errors/errors";
|
|
28
|
+
export { AmbiguousBindingError, AsyncActivationError, AsyncDeactivationError, AsyncModuleLoadError, AsyncResolutionError, ChainAlreadyRegisteredError, ChainNotRegisteredError, ManyBindingSlotError, CircularDependencyError, DiError, DisposedContainerError, InternalError, InvalidMetadataError, MissingContainerContextError, MissingMetadataError, MissingScopeContextError, NoMatchingBindingError, RebindUnboundTokenError, ScopeViolationError, SelfBindingRequiresClassError, StaticMemberDecoratorError, SyncDisposalNotSupportedError, EmptyTagCriteriaError, TokenNotBoundError, UnreachableConstraintError, UnreachableLifecycleHookError, } from "#/errors/errors";
|
|
29
29
|
// Graph adapters — render `generateDependencyGraph()` output for common viewers
|
|
30
30
|
export { toDotGraph } from "#/introspection/graph-adapters/dot";
|
|
31
31
|
export { toCytoscapeGraph } from "#/introspection/graph-adapters/cytoscape";
|
|
@@ -7,8 +7,14 @@ import type { DependencySlot } from "#/injection/resolve-options";
|
|
|
7
7
|
*
|
|
8
8
|
* @since 0.3.16-canary.0
|
|
9
9
|
*/
|
|
10
|
-
export interface InjectOptions {
|
|
11
|
-
|
|
10
|
+
export interface InjectOptions<Names extends string = string> {
|
|
11
|
+
/**
|
|
12
|
+
* The slot name a binding declared with `whenNamed`.
|
|
13
|
+
*
|
|
14
|
+
* @remarks Narrowed to the names the token declares, so a dependency cannot ask for a name no
|
|
15
|
+
* binding could carry; a token declaring none takes any string.
|
|
16
|
+
*/
|
|
17
|
+
name?: Names | undefined;
|
|
12
18
|
/**
|
|
13
19
|
* Single-tag shorthand, equivalent to listing the one pair in `tags`.
|
|
14
20
|
*
|
|
@@ -45,7 +51,7 @@ export type InjectableDependency<Value = unknown> = Token<Value> | Constructor<V
|
|
|
45
51
|
*/
|
|
46
52
|
export type ResolvedDependencyValue<Dependency> = Dependency extends {
|
|
47
53
|
readonly multi: true;
|
|
48
|
-
} ?
|
|
54
|
+
} ? ReadonlyArray<DescribedValue<Dependency>> : Dependency extends {
|
|
49
55
|
readonly optional: true;
|
|
50
56
|
} ? DescribedValue<Dependency> | undefined : DescribedValue<Dependency>;
|
|
51
57
|
/**
|
|
@@ -53,7 +59,7 @@ export type ResolvedDependencyValue<Dependency> = Dependency extends {
|
|
|
53
59
|
*
|
|
54
60
|
* @remarks Split out because a hand-written descriptor states its flags and its value type
|
|
55
61
|
* separately, and only the flags are load-bearing: `{ token: Plugin, multi: true }` says `Plugin`
|
|
56
|
-
* and delivers `
|
|
62
|
+
* and delivers `ReadonlyArray<Plugin>`. `injectAll()` and `optional()` already fold their effect in, so the
|
|
57
63
|
* flags find an array or an optional there and leave it alone.
|
|
58
64
|
*/
|
|
59
65
|
type DescribedValue<Dependency> = Dependency extends InjectionDescriptor<infer Value> ? Value : TokenValue<Dependency>;
|
|
@@ -74,17 +80,19 @@ export declare function normalizeToDescriptor(dependency: InjectableDependency):
|
|
|
74
80
|
*
|
|
75
81
|
* @since 0.6.0
|
|
76
82
|
*/
|
|
77
|
-
export declare function buildInjectionDescriptor<Value>(token: Token<Value> | Constructor<Value>, options?: InjectOptions): InjectionDescriptor<Value>;
|
|
83
|
+
export declare function buildInjectionDescriptor<Value, Names extends string = string>(token: Token<Value, Names> | Constructor<Value>, options?: NoInfer<InjectOptions<Names>>): InjectionDescriptor<Value>;
|
|
78
84
|
/**
|
|
79
85
|
* Creates a descriptor that resolves to `undefined` instead of throwing when no binding matches.
|
|
80
86
|
*
|
|
81
87
|
* @since 0.3.16-canary.0
|
|
82
88
|
*/
|
|
83
|
-
export declare function optional<Value>(token: Token<Value> | Constructor<Value>, options?: InjectOptions): InjectionDescriptor<Value | undefined>;
|
|
89
|
+
export declare function optional<Value, Names extends string = string>(token: Token<Value, Names> | Constructor<Value>, options?: NoInfer<InjectOptions<Names>>): InjectionDescriptor<Value | undefined>;
|
|
84
90
|
/**
|
|
85
|
-
* Creates a descriptor that resolves every matching binding for the token into
|
|
91
|
+
* Creates a descriptor that resolves every matching binding for the token into a read-only array.
|
|
92
|
+
*
|
|
93
|
+
* @remarks Read-only because a root-level read may hand out the engine's own cached list.
|
|
86
94
|
*
|
|
87
95
|
* @since 0.3.16-canary.0
|
|
88
96
|
*/
|
|
89
|
-
export declare function injectAll<Value>(token: Token<Value> | Constructor<Value>, options?: InjectOptions): InjectionDescriptor<
|
|
97
|
+
export declare function injectAll<Value, Names extends string = string>(token: Token<Value, Names> | Constructor<Value>, options?: NoInfer<InjectOptions<Names>>): InjectionDescriptor<ReadonlyArray<Value>>;
|
|
90
98
|
export {};
|
|
@@ -112,7 +112,9 @@ export function optional(token, options) {
|
|
|
112
112
|
}, options);
|
|
113
113
|
}
|
|
114
114
|
/**
|
|
115
|
-
* Creates a descriptor that resolves every matching binding for the token into
|
|
115
|
+
* Creates a descriptor that resolves every matching binding for the token into a read-only array.
|
|
116
|
+
*
|
|
117
|
+
* @remarks Read-only because a root-level read may hand out the engine's own cached list.
|
|
116
118
|
*
|
|
117
119
|
* @since 0.3.16-canary.0
|
|
118
120
|
*/
|
|
@@ -26,6 +26,12 @@ export interface DependencySlot {
|
|
|
26
26
|
* @since 0.5.0-canary.9
|
|
27
27
|
*/
|
|
28
28
|
export declare function singleCriterionOnlyOf(options: ResolveOptions | undefined): BindingTag | undefined;
|
|
29
|
+
/**
|
|
30
|
+
* The one tag a request carries beside its name, for the name-plus-tag lane; `undefined` for every other shape.
|
|
31
|
+
*
|
|
32
|
+
* @since 0.10.0
|
|
33
|
+
*/
|
|
34
|
+
export declare function loneTagBesideNameOf(options: ResolveOptions): BindingTag | undefined;
|
|
29
35
|
/**
|
|
30
36
|
* Builds a {@link ResolveOptions} safe for `exactOptionalPropertyTypes`:
|
|
31
37
|
* omits keys instead of assigning `undefined`.
|
|
@@ -22,6 +22,22 @@ export function singleCriterionOnlyOf(options) {
|
|
|
22
22
|
}
|
|
23
23
|
return listed !== undefined && listed.length === 1 ? listed[0] : undefined;
|
|
24
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* The one tag a request carries beside its name, for the name-plus-tag lane; `undefined` for every other shape.
|
|
27
|
+
*
|
|
28
|
+
* @since 0.10.0
|
|
29
|
+
*/
|
|
30
|
+
export function loneTagBesideNameOf(options) {
|
|
31
|
+
if (options.name === undefined) {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
const listed = options.tags;
|
|
35
|
+
const shorthand = options.tag;
|
|
36
|
+
if (shorthand !== undefined) {
|
|
37
|
+
return listed === undefined || listed.length === 0 ? shorthand : undefined;
|
|
38
|
+
}
|
|
39
|
+
return listed !== undefined && listed.length === 1 ? listed[0] : undefined;
|
|
40
|
+
}
|
|
25
41
|
/** The name spelling's half of the fold, kept apart so the common body stays small enough to inline. */
|
|
26
42
|
function loneNameCriterionOf(options) {
|
|
27
43
|
// A name next to any tag means the request carries two criteria, which no single index answers.
|
|
@@ -100,7 +100,7 @@ function addDependencyEdges(accumulator, from, ref, index, lookup) {
|
|
|
100
100
|
: label;
|
|
101
101
|
accumulator.edges.push({
|
|
102
102
|
from,
|
|
103
|
-
to: target.
|
|
103
|
+
to: String(target.identifier),
|
|
104
104
|
label: perTargetLabel,
|
|
105
105
|
optional: ref.optional,
|
|
106
106
|
...(edgeSlotName !== undefined ? { slotName: edgeSlotName } : {}),
|
|
@@ -113,21 +113,26 @@ function addBindingEdges(accumulator, binding, metadataReader, lookup) {
|
|
|
113
113
|
const meta = metadataReader.getConstructorMetadata(binding.target);
|
|
114
114
|
if (meta !== undefined) {
|
|
115
115
|
for (const [index, param] of meta.params.entries()) {
|
|
116
|
-
addDependencyEdges(accumulator, binding.
|
|
116
|
+
addDependencyEdges(accumulator, String(binding.identifier), param, index, lookup);
|
|
117
117
|
}
|
|
118
118
|
}
|
|
119
119
|
return;
|
|
120
120
|
}
|
|
121
121
|
if (binding.kind === "resolved" || binding.kind === "resolved-async") {
|
|
122
122
|
for (const [index, dependency] of binding.deps.entries()) {
|
|
123
|
-
addDependencyEdges(accumulator, binding.
|
|
123
|
+
addDependencyEdges(accumulator, String(binding.identifier), dependency, index, lookup);
|
|
124
124
|
}
|
|
125
125
|
return;
|
|
126
126
|
}
|
|
127
127
|
if (binding.kind === "alias") {
|
|
128
128
|
const aliasRef = { token: binding.target, optional: false, multi: false };
|
|
129
129
|
for (const target of matchingTargets(lookup(binding.target), aliasRef)) {
|
|
130
|
-
accumulator.edges.push({
|
|
130
|
+
accumulator.edges.push({
|
|
131
|
+
from: String(binding.identifier),
|
|
132
|
+
to: String(target.identifier),
|
|
133
|
+
label: "alias",
|
|
134
|
+
optional: false,
|
|
135
|
+
});
|
|
131
136
|
}
|
|
132
137
|
}
|
|
133
138
|
}
|
|
@@ -135,7 +140,7 @@ function addRegistryBindings(accumulator, sourceRegistry, metadataReader, fromPa
|
|
|
135
140
|
const lookup = bindingLookup(sourceRegistry, fallbackRegistry);
|
|
136
141
|
for (const binding of sourceRegistry.allBindings()) {
|
|
137
142
|
accumulator.nodes.push({
|
|
138
|
-
id: binding.
|
|
143
|
+
id: String(binding.identifier),
|
|
139
144
|
tokenName: tokenName(binding.token),
|
|
140
145
|
tokenKey: tokenKeyOf(binding.token),
|
|
141
146
|
kind: binding.kind,
|
|
@@ -16,6 +16,8 @@ export interface BindingSnapshot {
|
|
|
16
16
|
readonly tags: ReadonlyArray<BindingTag>;
|
|
17
17
|
};
|
|
18
18
|
readonly id: BindingIdentifier;
|
|
19
|
+
/** Whether the binding is a collection member only, taken by `resolveAll` and never by `resolve`. */
|
|
20
|
+
readonly isMany: boolean;
|
|
19
21
|
}
|
|
20
22
|
/**
|
|
21
23
|
* A read-only view of one container's own bindings and state.
|
|
@@ -38,6 +40,6 @@ export declare class Inspector {
|
|
|
38
40
|
constructor(registry: BindingRegistry, scope: ScopeManager, hasParent: boolean, isDisposed: () => boolean);
|
|
39
41
|
inspect(): ContainerSnapshot;
|
|
40
42
|
lookupBindings<Value>(token: Token<Value> | Constructor<Value>): ReadonlyArray<BindingSnapshot>;
|
|
41
|
-
|
|
43
|
+
/** Whether this container's own registry holds a binding the request could select. */
|
|
42
44
|
hasOwn(token: Token<unknown> | Constructor, options?: ResolveOptions): boolean;
|
|
43
45
|
}
|
|
@@ -30,31 +30,16 @@ export class Inspector {
|
|
|
30
30
|
const bindings = this.#registry.getAll(token);
|
|
31
31
|
return bindings.map((binding) => this.#toSnapshot(binding));
|
|
32
32
|
}
|
|
33
|
-
|
|
34
|
-
const bindings = this.#registry.getAll(token);
|
|
35
|
-
if (bindings.length > 0) {
|
|
36
|
-
// An existence probe answers ambiguity with `true` — several matches still exist; only
|
|
37
|
-
// resolution has to pick one.
|
|
38
|
-
if (options !== undefined) {
|
|
39
|
-
if (selectAllBindings(bindings, options, this.#makeConstraintContext(options)).length > 0) {
|
|
40
|
-
return true;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
else {
|
|
44
|
-
return true;
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
return parentHas?.() ?? false;
|
|
48
|
-
}
|
|
33
|
+
/** Whether this container's own registry holds a binding the request could select. */
|
|
49
34
|
hasOwn(token, options) {
|
|
50
|
-
|
|
51
|
-
if (
|
|
52
|
-
return
|
|
53
|
-
}
|
|
54
|
-
if (options !== undefined) {
|
|
55
|
-
return selectAllBindings(bindings, options, this.#makeConstraintContext(options)).length > 0;
|
|
35
|
+
// Presence alone is a registry probe; only a request carrying criteria has to see the list.
|
|
36
|
+
if (options === undefined) {
|
|
37
|
+
return this.#registry.has(token);
|
|
56
38
|
}
|
|
57
|
-
|
|
39
|
+
const bindings = this.#registry.getAll(token);
|
|
40
|
+
// An existence probe answers ambiguity with `true` — several matches still exist; only
|
|
41
|
+
// resolution has to pick one.
|
|
42
|
+
return bindings.length > 0 && selectAllBindings(bindings, options, this.#makeConstraintContext(options)).length > 0;
|
|
58
43
|
}
|
|
59
44
|
#makeConstraintContext(options) {
|
|
60
45
|
return {
|
|
@@ -76,7 +61,8 @@ export class Inspector {
|
|
|
76
61
|
kind: binding.kind,
|
|
77
62
|
scope: effectiveBindingScope(binding),
|
|
78
63
|
slot,
|
|
79
|
-
id: binding.
|
|
64
|
+
id: binding.identifier,
|
|
65
|
+
isMany: binding.isMany,
|
|
80
66
|
};
|
|
81
67
|
}
|
|
82
68
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getOrInsert } from "#/core/map-upsert";
|
|
2
|
+
import { advanceStateEpoch } from "#/core/state-epoch";
|
|
2
3
|
import { tokenName } from "#/core/token";
|
|
3
4
|
import { AsyncActivationError, AsyncDeactivationError, InvalidMetadataError } from "#/errors/errors";
|
|
4
5
|
/**
|
|
@@ -18,6 +19,7 @@ export class LifecycleManager {
|
|
|
18
19
|
#cachedHooks;
|
|
19
20
|
registerActivation(token, handler) {
|
|
20
21
|
this.#activationVersion += 1;
|
|
22
|
+
advanceStateEpoch();
|
|
21
23
|
this.#cachedToken = undefined;
|
|
22
24
|
this.#cachedHooks = undefined;
|
|
23
25
|
this.#activationHooks ??= new Map();
|
|
@@ -77,8 +79,8 @@ export class LifecycleManager {
|
|
|
77
79
|
}
|
|
78
80
|
}
|
|
79
81
|
// 2. per-binding onActivation
|
|
80
|
-
if (binding.kind !== "alias" && binding.
|
|
81
|
-
const activationResult = binding.
|
|
82
|
+
if (binding.kind !== "alias" && binding.activationHook !== undefined) {
|
|
83
|
+
const activationResult = binding.activationHook(resolutionContext, activatedInstance);
|
|
82
84
|
activatedInstance = activationResult instanceof Promise ? await activationResult : activationResult;
|
|
83
85
|
}
|
|
84
86
|
// 3. container-level onActivation
|
|
@@ -100,8 +102,8 @@ export class LifecycleManager {
|
|
|
100
102
|
}
|
|
101
103
|
}
|
|
102
104
|
// 2. per-binding onActivation (must be sync)
|
|
103
|
-
if (binding.kind !== "alias" && binding.
|
|
104
|
-
const activationResult = binding.
|
|
105
|
+
if (binding.kind !== "alias" && binding.activationHook !== undefined) {
|
|
106
|
+
const activationResult = binding.activationHook(resolutionContext, activatedInstance);
|
|
105
107
|
if (activationResult instanceof Promise) {
|
|
106
108
|
throw new AsyncActivationError(tokenName(binding.token), "onActivation");
|
|
107
109
|
}
|
|
@@ -134,8 +136,8 @@ export class LifecycleManager {
|
|
|
134
136
|
}
|
|
135
137
|
}
|
|
136
138
|
// 2. per-binding onDeactivation
|
|
137
|
-
if (binding.kind !== "alias" && binding.
|
|
138
|
-
const hookResult = binding.
|
|
139
|
+
if (binding.kind !== "alias" && binding.deactivationHook !== undefined) {
|
|
140
|
+
const hookResult = binding.deactivationHook(instance);
|
|
139
141
|
if (hookResult instanceof Promise) {
|
|
140
142
|
await hookResult;
|
|
141
143
|
}
|
|
@@ -162,8 +164,8 @@ export class LifecycleManager {
|
|
|
162
164
|
}
|
|
163
165
|
}
|
|
164
166
|
// 2. per-binding onDeactivation
|
|
165
|
-
if (binding.kind !== "alias" && binding.
|
|
166
|
-
const hookResult = binding.
|
|
167
|
+
if (binding.kind !== "alias" && binding.deactivationHook !== undefined) {
|
|
168
|
+
const hookResult = binding.deactivationHook(instance);
|
|
167
169
|
if (hookResult instanceof Promise) {
|
|
168
170
|
throw new AsyncDeactivationError(tokenDisplayName);
|
|
169
171
|
}
|
|
@@ -99,7 +99,7 @@ export class ScopeManager {
|
|
|
99
99
|
if (!this.isChild) {
|
|
100
100
|
throw new MissingScopeContextError(tokenName(binding.token));
|
|
101
101
|
}
|
|
102
|
-
(this.#scoped ??= new Map()).set(binding.
|
|
102
|
+
(this.#scoped ??= new Map()).set(binding.identifier, instance);
|
|
103
103
|
}
|
|
104
104
|
/** Releases a removed binding's scoped instance. A scoped instance has no deactivation. */
|
|
105
105
|
deleteScoped(id) {
|