@codefast/di 0.12.0 → 0.13.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 +40 -0
- package/README.md +102 -19
- package/dist/container/container.d.ts +0 -1
- package/dist/container/container.js +72 -41
- package/dist/core/binding-builders.d.ts +5 -2
- package/dist/core/binding-declaration.d.ts +2 -2
- package/dist/core/binding-declaration.js +2 -2
- package/dist/decorators/inject.d.ts +3 -1
- package/dist/decorators/lifecycle.d.ts +9 -3
- package/dist/decorators/lifecycle.js +5 -1
- package/dist/errors.d.ts +22 -13
- package/dist/errors.js +35 -18
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/introspection/explanation.js +26 -8
- package/dist/lifecycle/hooks.d.ts +2 -0
- package/dist/lifecycle/hooks.js +6 -0
- package/dist/lifecycle/scopes.d.ts +21 -2
- package/dist/lifecycle/scopes.js +86 -2
- package/dist/metadata/keys.d.ts +1 -1
- package/dist/metadata/keys.js +1 -1
- package/dist/resolution/path.d.ts +12 -2
- package/dist/resolution/path.js +22 -2
- package/dist/resolution/resolver.d.ts +12 -0
- package/dist/resolution/resolver.js +39 -8
- package/package.json +2 -2
package/dist/errors.d.ts
CHANGED
|
@@ -28,7 +28,9 @@ export declare class TokenNotBoundError extends DiError {
|
|
|
28
28
|
readonly name = "TokenNotBoundError";
|
|
29
29
|
readonly code = "TOKEN_NOT_BOUND";
|
|
30
30
|
readonly tokenName: string;
|
|
31
|
-
|
|
31
|
+
/** The token names from the outermost request down to the unbound one, which is last. */
|
|
32
|
+
readonly path: ReadonlyArray<string>;
|
|
33
|
+
constructor(tokenName: string, path?: ReadonlyArray<string>);
|
|
32
34
|
}
|
|
33
35
|
/**
|
|
34
36
|
* A token that has bindings, none of whose slots match the given `ResolveOptions`.
|
|
@@ -41,7 +43,9 @@ export declare class NoMatchingBindingError extends DiError {
|
|
|
41
43
|
readonly tokenName: string;
|
|
42
44
|
readonly options: ResolveOptions;
|
|
43
45
|
readonly availableSlots: Array<string>;
|
|
44
|
-
|
|
46
|
+
/** The token names from the outermost request down to the unmatched one, which is last. */
|
|
47
|
+
readonly path: ReadonlyArray<string>;
|
|
48
|
+
constructor(tokenName: string, options: ResolveOptions, availableSlots: Array<string>, path?: ReadonlyArray<string>);
|
|
45
49
|
}
|
|
46
50
|
/**
|
|
47
51
|
* Two or more candidate bindings that matched without the more-specific rule deciding a winner.
|
|
@@ -162,7 +166,7 @@ export declare class UnreachableLifecycleHookError extends DiError {
|
|
|
162
166
|
readonly code = "UNREACHABLE_LIFECYCLE_HOOK";
|
|
163
167
|
readonly tokenName: string;
|
|
164
168
|
readonly phase: "onActivation" | "onDeactivation";
|
|
165
|
-
/** Nothing binds the token, or — for a deactivation hook — every binding it has is transient
|
|
169
|
+
/** Nothing binds the token, or — for a deactivation hook — every binding it has is transient. */
|
|
166
170
|
readonly reason: "unbound" | "no-deactivatable-binding";
|
|
167
171
|
constructor(tokenName: string, phase: "onActivation" | "onDeactivation", reason?: "unbound" | "no-deactivatable-binding");
|
|
168
172
|
}
|
|
@@ -207,16 +211,6 @@ export declare class AsyncModuleLoadError extends DiError {
|
|
|
207
211
|
readonly moduleName: string;
|
|
208
212
|
constructor(moduleName: string);
|
|
209
213
|
}
|
|
210
|
-
/**
|
|
211
|
-
* A synchronous disposal attempt on a container whose `onDeactivation` handlers may be async.
|
|
212
|
-
*
|
|
213
|
-
* @since 0.3.16-canary.0
|
|
214
|
-
*/
|
|
215
|
-
export declare class SyncDisposalNotSupportedError extends DiError {
|
|
216
|
-
readonly name = "SyncDisposalNotSupportedError";
|
|
217
|
-
readonly code = "SYNC_DISPOSAL_NOT_SUPPORTED";
|
|
218
|
-
constructor();
|
|
219
|
-
}
|
|
220
214
|
/**
|
|
221
215
|
* A `scoped` binding resolved from a container with no child scope context.
|
|
222
216
|
*
|
|
@@ -342,6 +336,21 @@ export declare class SymbolKeyedLifecycleError extends DiError {
|
|
|
342
336
|
readonly memberName: string;
|
|
343
337
|
constructor(decoratorName: string, memberName: string);
|
|
344
338
|
}
|
|
339
|
+
/**
|
|
340
|
+
* A `@postConstruct` or `@preDestroy` on a private method, which the lifecycle reader cannot call by name.
|
|
341
|
+
*
|
|
342
|
+
* @remarks Reported at decoration rather than at resolve, so the error points at the declaration
|
|
343
|
+
* instead of a `MetadataReader` the caller never configured.
|
|
344
|
+
*
|
|
345
|
+
* @since 0.13.0
|
|
346
|
+
*/
|
|
347
|
+
export declare class PrivateLifecycleMethodError extends DiError {
|
|
348
|
+
readonly name = "PrivateLifecycleMethodError";
|
|
349
|
+
readonly code = "PRIVATE_LIFECYCLE_METHOD";
|
|
350
|
+
readonly decoratorName: string;
|
|
351
|
+
readonly memberName: string;
|
|
352
|
+
constructor(decoratorName: string, memberName: string);
|
|
353
|
+
}
|
|
345
354
|
/**
|
|
346
355
|
* A decorator handed no metadata object, because the runtime has no `Symbol.metadata` to key one by.
|
|
347
356
|
*
|
package/dist/errors.js
CHANGED
|
@@ -18,6 +18,10 @@ export class InternalError extends DiError {
|
|
|
18
18
|
super(message);
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
|
+
// A path of one token only repeats the name the message already opens with.
|
|
22
|
+
function pathSuffix(path) {
|
|
23
|
+
return path.length > 1 ? ` Path: ${path.join(" → ")}` : "";
|
|
24
|
+
}
|
|
21
25
|
/**
|
|
22
26
|
* A token with no binding at all, even after walking the parent container chain.
|
|
23
27
|
*
|
|
@@ -27,9 +31,12 @@ export class TokenNotBoundError extends DiError {
|
|
|
27
31
|
name = "TokenNotBoundError";
|
|
28
32
|
code = "TOKEN_NOT_BOUND";
|
|
29
33
|
tokenName;
|
|
30
|
-
|
|
31
|
-
|
|
34
|
+
/** The token names from the outermost request down to the unbound one, which is last. */
|
|
35
|
+
path;
|
|
36
|
+
constructor(tokenName, path = [tokenName]) {
|
|
37
|
+
super(`No binding found for token '${tokenName}'. Did you forget container.bind(${tokenName})?${pathSuffix(path)}`);
|
|
32
38
|
this.tokenName = tokenName;
|
|
39
|
+
this.path = path;
|
|
33
40
|
}
|
|
34
41
|
}
|
|
35
42
|
// Options carry caller values a tag may hold — a bigint or a circular object must not make the
|
|
@@ -78,13 +85,16 @@ export class NoMatchingBindingError extends DiError {
|
|
|
78
85
|
tokenName;
|
|
79
86
|
options;
|
|
80
87
|
availableSlots;
|
|
81
|
-
|
|
88
|
+
/** The token names from the outermost request down to the unmatched one, which is last. */
|
|
89
|
+
path;
|
|
90
|
+
constructor(tokenName, options, availableSlots, path = [tokenName]) {
|
|
82
91
|
const optionsString = describeResolveOptions(options);
|
|
83
92
|
const slotsStr = availableSlots.join(", ");
|
|
84
|
-
super(`No binding for '${tokenName}' matching ${optionsString}. Available slots: [${slotsStr}]
|
|
93
|
+
super(`No binding for '${tokenName}' matching ${optionsString}. Available slots: [${slotsStr}].${pathSuffix(path)}`);
|
|
85
94
|
this.tokenName = tokenName;
|
|
86
95
|
this.options = options;
|
|
87
96
|
this.availableSlots = availableSlots;
|
|
97
|
+
this.path = path;
|
|
88
98
|
}
|
|
89
99
|
}
|
|
90
100
|
/**
|
|
@@ -225,12 +235,12 @@ export class UnreachableLifecycleHookError extends DiError {
|
|
|
225
235
|
code = "UNREACHABLE_LIFECYCLE_HOOK";
|
|
226
236
|
tokenName;
|
|
227
237
|
phase;
|
|
228
|
-
/** Nothing binds the token, or — for a deactivation hook — every binding it has is transient
|
|
238
|
+
/** Nothing binds the token, or — for a deactivation hook — every binding it has is transient. */
|
|
229
239
|
reason;
|
|
230
240
|
constructor(tokenName, phase, reason = "unbound") {
|
|
231
241
|
super(reason === "unbound"
|
|
232
242
|
? `${phase}() is registered for '${tokenName}', which nothing is bound to in this container or its ancestors, so the hook can never run. Bind the token, or — if '${tokenName}' is a class you bound as an implementation via .to(${tokenName}) — register the hook against the token you bound instead.`
|
|
233
|
-
: `${phase}() is registered for '${tokenName}', whose every binding in this container and its ancestors is transient
|
|
243
|
+
: `${phase}() is registered for '${tokenName}', whose every binding in this container and its ancestors is transient, so the hook can never run: a transient instance is never deactivated. Make one of its bindings a singleton or scoped, or drop the hook.`);
|
|
234
244
|
this.tokenName = tokenName;
|
|
235
245
|
this.phase = phase;
|
|
236
246
|
this.reason = reason;
|
|
@@ -286,18 +296,6 @@ export class AsyncModuleLoadError extends DiError {
|
|
|
286
296
|
this.moduleName = moduleName;
|
|
287
297
|
}
|
|
288
298
|
}
|
|
289
|
-
/**
|
|
290
|
-
* A synchronous disposal attempt on a container whose `onDeactivation` handlers may be async.
|
|
291
|
-
*
|
|
292
|
-
* @since 0.3.16-canary.0
|
|
293
|
-
*/
|
|
294
|
-
export class SyncDisposalNotSupportedError extends DiError {
|
|
295
|
-
name = "SyncDisposalNotSupportedError";
|
|
296
|
-
code = "SYNC_DISPOSAL_NOT_SUPPORTED";
|
|
297
|
-
constructor() {
|
|
298
|
-
super("Container cannot be disposed synchronously because onDeactivation handlers may be async. Use `await using` or call container.dispose() explicitly.");
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
299
|
/**
|
|
302
300
|
* A `scoped` binding resolved from a container with no child scope context.
|
|
303
301
|
*
|
|
@@ -456,6 +454,25 @@ export class SymbolKeyedLifecycleError extends DiError {
|
|
|
456
454
|
this.memberName = memberName;
|
|
457
455
|
}
|
|
458
456
|
}
|
|
457
|
+
/**
|
|
458
|
+
* A `@postConstruct` or `@preDestroy` on a private method, which the lifecycle reader cannot call by name.
|
|
459
|
+
*
|
|
460
|
+
* @remarks Reported at decoration rather than at resolve, so the error points at the declaration
|
|
461
|
+
* instead of a `MetadataReader` the caller never configured.
|
|
462
|
+
*
|
|
463
|
+
* @since 0.13.0
|
|
464
|
+
*/
|
|
465
|
+
export class PrivateLifecycleMethodError extends DiError {
|
|
466
|
+
name = "PrivateLifecycleMethodError";
|
|
467
|
+
code = "PRIVATE_LIFECYCLE_METHOD";
|
|
468
|
+
decoratorName;
|
|
469
|
+
memberName;
|
|
470
|
+
constructor(decoratorName, memberName) {
|
|
471
|
+
super(`@${decoratorName}() does not support a private method ('${memberName}'). Make the lifecycle method public, and call the private code from it.`);
|
|
472
|
+
this.decoratorName = decoratorName;
|
|
473
|
+
this.memberName = memberName;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
459
476
|
/**
|
|
460
477
|
* A decorator handed no metadata object, because the runtime has no `Symbol.metadata` to key one by.
|
|
461
478
|
*
|
package/dist/index.d.ts
CHANGED
|
@@ -29,7 +29,7 @@ export { MetadataReaderToken } from "#metadata/reader-token";
|
|
|
29
29
|
export type { ConstructorMetadata, LifecycleMetadata, MetadataReader, MutableLifecycleMetadata, ParamMetadata, } from "#metadata/types";
|
|
30
30
|
export { defaultMetadataReader, SymbolMetadataReader } from "#metadata/symbol-reader";
|
|
31
31
|
export { whenAnyAncestorIs, whenAnyAncestorNamed, whenAnyAncestorTagged, whenAnyAncestorTaggedAll, whenNoAncestorIs, whenNoParentIs, whenParentIs, whenParentNamed, whenParentTagged, whenParentTaggedAll, } from "#resolution/select/constraints";
|
|
32
|
-
export { AmbiguousBindingError, AsyncActivationError, AsyncDeactivationError, AsyncModuleLoadError, AsyncResolutionError, ChainAlreadyRegisteredError, ChainNotRegisteredError, ManyBindingSlotError, CircularDependencyError, DiError, DisposedContainerError, InternalError, InvalidBindingDeclarationError, InvalidMetadataError, MissingContainerContextError, MissingDecoratorMetadataError, MissingMetadataError, MissingScopeContextError, NoMatchingBindingError, RebindUnboundTokenError, ScopeViolationError, SelfBindingRequiresClassError, StaticMemberDecoratorError, SymbolKeyedLifecycleError,
|
|
32
|
+
export { AmbiguousBindingError, AsyncActivationError, AsyncDeactivationError, AsyncModuleLoadError, AsyncResolutionError, ChainAlreadyRegisteredError, ChainNotRegisteredError, ManyBindingSlotError, CircularDependencyError, DiError, DisposedContainerError, InternalError, InvalidBindingDeclarationError, InvalidMetadataError, MissingContainerContextError, MissingDecoratorMetadataError, MissingMetadataError, MissingScopeContextError, NoMatchingBindingError, RebindUnboundTokenError, ScopeViolationError, SelfBindingRequiresClassError, PrivateLifecycleMethodError, StaticMemberDecoratorError, SymbolKeyedLifecycleError, EmptyTagCriteriaError, TokenNotBoundError, UnreachableConstraintError, UnreachableLifecycleHookError, } from "#errors";
|
|
33
33
|
export type { ScopeViolationDetails } from "#errors";
|
|
34
34
|
export { toDotGraph } from "#introspection/graph-adapters/dot";
|
|
35
35
|
export { toCytoscapeGraph } from "#introspection/graph-adapters/cytoscape";
|
package/dist/index.js
CHANGED
|
@@ -26,7 +26,7 @@ export { defaultMetadataReader, SymbolMetadataReader } from "#metadata/symbol-re
|
|
|
26
26
|
// Constraints — contextual injection predicates for .when()
|
|
27
27
|
export { whenAnyAncestorIs, whenAnyAncestorNamed, whenAnyAncestorTagged, whenAnyAncestorTaggedAll, whenNoAncestorIs, whenNoParentIs, whenParentIs, whenParentNamed, whenParentTagged, whenParentTaggedAll, } from "#resolution/select/constraints";
|
|
28
28
|
// Errors
|
|
29
|
-
export { AmbiguousBindingError, AsyncActivationError, AsyncDeactivationError, AsyncModuleLoadError, AsyncResolutionError, ChainAlreadyRegisteredError, ChainNotRegisteredError, ManyBindingSlotError, CircularDependencyError, DiError, DisposedContainerError, InternalError, InvalidBindingDeclarationError, InvalidMetadataError, MissingContainerContextError, MissingDecoratorMetadataError, MissingMetadataError, MissingScopeContextError, NoMatchingBindingError, RebindUnboundTokenError, ScopeViolationError, SelfBindingRequiresClassError, StaticMemberDecoratorError, SymbolKeyedLifecycleError,
|
|
29
|
+
export { AmbiguousBindingError, AsyncActivationError, AsyncDeactivationError, AsyncModuleLoadError, AsyncResolutionError, ChainAlreadyRegisteredError, ChainNotRegisteredError, ManyBindingSlotError, CircularDependencyError, DiError, DisposedContainerError, InternalError, InvalidBindingDeclarationError, InvalidMetadataError, MissingContainerContextError, MissingDecoratorMetadataError, MissingMetadataError, MissingScopeContextError, NoMatchingBindingError, RebindUnboundTokenError, ScopeViolationError, SelfBindingRequiresClassError, PrivateLifecycleMethodError, StaticMemberDecoratorError, SymbolKeyedLifecycleError, EmptyTagCriteriaError, TokenNotBoundError, UnreachableConstraintError, UnreachableLifecycleHookError, } from "#errors";
|
|
30
30
|
// Graph adapters — render `generateDependencyGraph()` output for common viewers
|
|
31
31
|
export { toDotGraph } from "#introspection/graph-adapters/dot";
|
|
32
32
|
export { toCytoscapeGraph } from "#introspection/graph-adapters/cytoscape";
|
|
@@ -3,7 +3,7 @@ import { tokenName } from "#core/token";
|
|
|
3
3
|
import { AmbiguousBindingError, CircularDependencyError, InternalError, NoMatchingBindingError, TokenNotBoundError, } from "#errors";
|
|
4
4
|
import { snapshotOf } from "#introspection/inspector";
|
|
5
5
|
import { DefaultConstraintContext } from "#resolution/context";
|
|
6
|
-
import { buildResolutionFrame } from "#resolution/path";
|
|
6
|
+
import { buildResolutionFrame, missPathOf } from "#resolution/path";
|
|
7
7
|
import { candidateRuleOf, chooseCandidate, matchesSlot } from "#resolution/select/candidates";
|
|
8
8
|
// ── Explanation ──────────────────────────────────────────────────────────────────────────────────────────────────────
|
|
9
9
|
/**
|
|
@@ -21,7 +21,7 @@ export function explainRequest(registries, token, options) {
|
|
|
21
21
|
for (const ancestor of options?.ancestors ?? []) {
|
|
22
22
|
const walk = walkLookup(registries, ancestor, undefined, stack);
|
|
23
23
|
if (walk.binding === undefined) {
|
|
24
|
-
throw lookupError(registries, walk);
|
|
24
|
+
throw lookupError(registries, ancestor, walk, stack);
|
|
25
25
|
}
|
|
26
26
|
stack.push(buildResolutionFrame(walk.binding, tokenName(walk.binding.token)));
|
|
27
27
|
}
|
|
@@ -57,15 +57,32 @@ function walkLookup(registries, token, request, stack) {
|
|
|
57
57
|
lastToken: current,
|
|
58
58
|
ambiguity: found.eligible,
|
|
59
59
|
cycle: undefined,
|
|
60
|
+
aliasHops: visited,
|
|
60
61
|
};
|
|
61
62
|
}
|
|
62
63
|
if (found.kind === "none") {
|
|
63
64
|
const outcome = registries.some((registry) => registry.getAll(current).length > 0) ? "unmatched" : "unbound";
|
|
64
|
-
return {
|
|
65
|
+
return {
|
|
66
|
+
steps,
|
|
67
|
+
binding: undefined,
|
|
68
|
+
outcome,
|
|
69
|
+
lastToken: current,
|
|
70
|
+
ambiguity: undefined,
|
|
71
|
+
cycle: undefined,
|
|
72
|
+
aliasHops: visited,
|
|
73
|
+
};
|
|
65
74
|
}
|
|
66
75
|
const binding = found.binding;
|
|
67
76
|
if (binding.kind !== "alias") {
|
|
68
|
-
return {
|
|
77
|
+
return {
|
|
78
|
+
steps,
|
|
79
|
+
binding,
|
|
80
|
+
outcome: "selected",
|
|
81
|
+
lastToken: current,
|
|
82
|
+
ambiguity: undefined,
|
|
83
|
+
cycle: undefined,
|
|
84
|
+
aliasHops: visited,
|
|
85
|
+
};
|
|
69
86
|
}
|
|
70
87
|
// An alias hop restarts at the container asked, as `resolve` does, and a revisited token is a cycle.
|
|
71
88
|
const target = binding.target;
|
|
@@ -78,6 +95,7 @@ function walkLookup(registries, token, request, stack) {
|
|
|
78
95
|
lastToken: current,
|
|
79
96
|
ambiguity: undefined,
|
|
80
97
|
cycle: [...visited, target],
|
|
98
|
+
aliasHops: visited,
|
|
81
99
|
};
|
|
82
100
|
}
|
|
83
101
|
visited.add(target);
|
|
@@ -133,8 +151,8 @@ function verdictOf(binding, request, ctx) {
|
|
|
133
151
|
const predicate = binding.predicate;
|
|
134
152
|
return predicate === undefined || predicate(ctx) ? "eligible" : "predicate-refused";
|
|
135
153
|
}
|
|
136
|
-
/** The error `resolve` throws for a lookup that ended without a binding. */
|
|
137
|
-
function lookupError(registries, walk) {
|
|
154
|
+
/** The error `resolve` throws for a lookup that ended without a binding, named along the path it was made from. */
|
|
155
|
+
function lookupError(registries, requested, walk, stack) {
|
|
138
156
|
const name = tokenName(walk.lastToken);
|
|
139
157
|
switch (walk.outcome) {
|
|
140
158
|
case "ambiguous":
|
|
@@ -142,9 +160,9 @@ function lookupError(registries, walk) {
|
|
|
142
160
|
case "alias-cycle":
|
|
143
161
|
return new CircularDependencyError((walk.cycle ?? []).map((entry) => tokenName(entry)));
|
|
144
162
|
case "unmatched":
|
|
145
|
-
return new NoMatchingBindingError(name, {}, registries.flatMap((registry) => registry.getAll(walk.lastToken).map((binding) => bindingSlotToString(binding.slot))));
|
|
163
|
+
return new NoMatchingBindingError(name, {}, registries.flatMap((registry) => registry.getAll(walk.lastToken).map((binding) => bindingSlotToString(binding.slot))), missPathOf(stack, requested, walk.aliasHops));
|
|
146
164
|
case "unbound":
|
|
147
|
-
return new TokenNotBoundError(name);
|
|
165
|
+
return new TokenNotBoundError(name, missPathOf(stack, requested, walk.aliasHops));
|
|
148
166
|
case "selected":
|
|
149
167
|
return new InternalError("a lookup that selected a binding has no error");
|
|
150
168
|
}
|
|
@@ -17,6 +17,8 @@ export declare class LifecycleManager {
|
|
|
17
17
|
registerDeactivation<Value>(token: Token<Value> | Constructor<Value>, handler: DeactivationHandler<Value>): void;
|
|
18
18
|
/** Asked only while tearing down, so it reads the map directly rather than caching like activation. */
|
|
19
19
|
hasDeactivationHandlers<Value>(token: Token<Value> | Constructor<Value>): boolean;
|
|
20
|
+
/** Whether deactivating an instance of a binding runs anything: a container hook, the binding's own, or `@preDestroy()`. */
|
|
21
|
+
owesDeactivation(binding: Binding, metadataReader: MetadataReader): boolean;
|
|
20
22
|
/** Every token carrying a container-level hook, paired with the phase that registered it. */
|
|
21
23
|
hookedTokens(): Array<[DependencyKey, "onActivation" | "onDeactivation"]>;
|
|
22
24
|
/**
|
package/dist/lifecycle/hooks.js
CHANGED
|
@@ -62,6 +62,12 @@ export class LifecycleManager {
|
|
|
62
62
|
const list = this.#deactivationHooks?.get(token);
|
|
63
63
|
return list !== undefined && list.length > 0;
|
|
64
64
|
}
|
|
65
|
+
/** Whether deactivating an instance of a binding runs anything: a container hook, the binding's own, or `@preDestroy()`. */
|
|
66
|
+
owesDeactivation(binding, metadataReader) {
|
|
67
|
+
return (this.hasDeactivationHandlers(binding.token) ||
|
|
68
|
+
(binding.kind !== "alias" && binding.deactivationHook !== undefined) ||
|
|
69
|
+
this.#lifecycleMethods(binding, metadataReader, "preDestroy").length > 0);
|
|
70
|
+
}
|
|
65
71
|
/** Every token carrying a container-level hook, paired with the phase that registered it. */
|
|
66
72
|
hookedTokens() {
|
|
67
73
|
const hooked = [];
|
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
import type { Binding } from "#core/binding";
|
|
2
2
|
import type { BindingIdentifier } from "#core/types";
|
|
3
|
+
/**
|
|
4
|
+
* What deactivates a scoped instance: the resolver owning its binding, which activated it.
|
|
5
|
+
*
|
|
6
|
+
* @remarks Answers `undefined` when the binding owes no teardown, so disposing a per-request child awaits nothing.
|
|
7
|
+
*
|
|
8
|
+
* @since 0.13.0
|
|
9
|
+
*/
|
|
10
|
+
export interface ScopedInstanceOwner {
|
|
11
|
+
deactivateScoped(binding: Binding, instance: unknown): Promise<void> | undefined;
|
|
12
|
+
}
|
|
3
13
|
/**
|
|
4
14
|
* One container's instance caches — singletons, in-flight async creations, and the scoped cache.
|
|
5
15
|
*
|
|
@@ -28,9 +38,18 @@ export declare class ScopeManager {
|
|
|
28
38
|
*/
|
|
29
39
|
readScoped(key: BindingIdentifier): unknown;
|
|
30
40
|
/** Takes the binding rather than its id, so a failure here can name the token — as `setSingleton` does. */
|
|
31
|
-
setScoped(binding: Binding, instance: unknown): void;
|
|
32
|
-
/**
|
|
41
|
+
setScoped(binding: Binding, instance: unknown, owner: ScopedInstanceOwner): void;
|
|
42
|
+
/** Drops the instance cached under a scoped cache key, owing it nothing — what a binding leaving `scoped` retires. */
|
|
33
43
|
deleteScoped(key: BindingIdentifier): void;
|
|
44
|
+
/** Removes the instance cached under a scoped cache key and hands it back, or {@link SCOPED_MISS} when none is. */
|
|
45
|
+
takeScoped(key: BindingIdentifier): unknown;
|
|
46
|
+
/**
|
|
47
|
+
* Deactivates every cached scoped instance through its owner, the latest cached first and one at a time, collecting
|
|
48
|
+
* each failure into `errors` — or answers `undefined` when no owner had a teardown to run.
|
|
49
|
+
*
|
|
50
|
+
* @remarks A binding cached again after its instance was taken is listed twice; only its current instance is owed.
|
|
51
|
+
*/
|
|
52
|
+
deactivateScoped(errors: Array<unknown>): Promise<void> | undefined;
|
|
34
53
|
/** Scoped instances currently cached — a structural count for diagnostics. */
|
|
35
54
|
get scopedCount(): number;
|
|
36
55
|
clearAll(): void;
|
package/dist/lifecycle/scopes.js
CHANGED
|
@@ -13,6 +13,12 @@ export class ScopeManager {
|
|
|
13
13
|
#inflight;
|
|
14
14
|
// Scoped cache — only a child container resolving a `scoped` binding ever needs it.
|
|
15
15
|
#scoped;
|
|
16
|
+
// Scoped bindings in the order their instances were cached, each beside its owner, so disposal can deactivate them
|
|
17
|
+
// latest first. The first sits in fields: a per-request child caching one scoped instance allocates nothing for it.
|
|
18
|
+
#firstScopedBinding;
|
|
19
|
+
#firstScopedOwner;
|
|
20
|
+
#laterScopedBindings;
|
|
21
|
+
#laterScopedOwners;
|
|
16
22
|
// Set once by the owning container's dispose — refuses new materializations into torn-down state.
|
|
17
23
|
#closed = false;
|
|
18
24
|
isChild;
|
|
@@ -101,16 +107,90 @@ export class ScopeManager {
|
|
|
101
107
|
return scoped.has(key) ? undefined : SCOPED_MISS;
|
|
102
108
|
}
|
|
103
109
|
/** Takes the binding rather than its id, so a failure here can name the token — as `setSingleton` does. */
|
|
104
|
-
setScoped(binding, instance) {
|
|
110
|
+
setScoped(binding, instance, owner) {
|
|
105
111
|
if (!this.isChild) {
|
|
106
112
|
throw new MissingScopeContextError(tokenName(binding.token));
|
|
107
113
|
}
|
|
108
114
|
(this.#scoped ??= new Map()).set(binding.scopedCacheKey, instance);
|
|
115
|
+
if (this.#firstScopedBinding === undefined) {
|
|
116
|
+
this.#firstScopedBinding = binding;
|
|
117
|
+
this.#firstScopedOwner = owner;
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
(this.#laterScopedBindings ??= []).push(binding);
|
|
121
|
+
(this.#laterScopedOwners ??= []).push(owner);
|
|
122
|
+
}
|
|
109
123
|
}
|
|
110
|
-
/**
|
|
124
|
+
/** Drops the instance cached under a scoped cache key, owing it nothing — what a binding leaving `scoped` retires. */
|
|
111
125
|
deleteScoped(key) {
|
|
112
126
|
this.#scoped?.delete(key);
|
|
113
127
|
}
|
|
128
|
+
/** Removes the instance cached under a scoped cache key and hands it back, or {@link SCOPED_MISS} when none is. */
|
|
129
|
+
takeScoped(key) {
|
|
130
|
+
const scoped = this.#scoped;
|
|
131
|
+
if (scoped === undefined || !scoped.has(key)) {
|
|
132
|
+
return SCOPED_MISS;
|
|
133
|
+
}
|
|
134
|
+
const instance = scoped.get(key);
|
|
135
|
+
scoped.delete(key);
|
|
136
|
+
return instance;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Deactivates every cached scoped instance through its owner, the latest cached first and one at a time, collecting
|
|
140
|
+
* each failure into `errors` — or answers `undefined` when no owner had a teardown to run.
|
|
141
|
+
*
|
|
142
|
+
* @remarks A binding cached again after its instance was taken is listed twice; only its current instance is owed.
|
|
143
|
+
*/
|
|
144
|
+
deactivateScoped(errors) {
|
|
145
|
+
if (this.#firstScopedBinding === undefined) {
|
|
146
|
+
return undefined;
|
|
147
|
+
}
|
|
148
|
+
return this.#deactivateScopedFrom(this.#laterScopedBindings?.length ?? 0, errors);
|
|
149
|
+
}
|
|
150
|
+
// Positions count down from the latest: `position - 1` in the later lists while positive, then the first at 0.
|
|
151
|
+
#deactivateScopedFrom(position, errors) {
|
|
152
|
+
for (let current = position; current >= 0; current -= 1) {
|
|
153
|
+
const pending = this.#deactivateScopedAt(current, errors);
|
|
154
|
+
if (pending !== undefined) {
|
|
155
|
+
return this.#finishDeactivatingScoped(pending, current - 1, errors);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return undefined;
|
|
159
|
+
}
|
|
160
|
+
// The rest of the walk once a teardown is pending: each one settles before the next starts.
|
|
161
|
+
async #finishDeactivatingScoped(pending, position, errors) {
|
|
162
|
+
let current = pending;
|
|
163
|
+
for (let next = position;; next -= 1) {
|
|
164
|
+
if (current !== undefined) {
|
|
165
|
+
try {
|
|
166
|
+
await current;
|
|
167
|
+
}
|
|
168
|
+
catch (error) {
|
|
169
|
+
errors.push(error);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (next < 0) {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
current = this.#deactivateScopedAt(next, errors);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
#deactivateScopedAt(position, errors) {
|
|
179
|
+
const binding = position === 0 ? this.#firstScopedBinding : this.#laterScopedBindings[position - 1];
|
|
180
|
+
const owner = position === 0 ? this.#firstScopedOwner : this.#laterScopedOwners[position - 1];
|
|
181
|
+
// The first is read last, so only a later entry can find a key cached twice; taking it leaves the rest a miss.
|
|
182
|
+
const instance = position === 0 ? this.readScoped(binding.scopedCacheKey) : this.takeScoped(binding.scopedCacheKey);
|
|
183
|
+
if (instance === SCOPED_MISS) {
|
|
184
|
+
return undefined;
|
|
185
|
+
}
|
|
186
|
+
try {
|
|
187
|
+
return owner.deactivateScoped(binding, instance);
|
|
188
|
+
}
|
|
189
|
+
catch (error) {
|
|
190
|
+
errors.push(error);
|
|
191
|
+
return undefined;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
114
194
|
/** Scoped instances currently cached — a structural count for diagnostics. */
|
|
115
195
|
get scopedCount() {
|
|
116
196
|
return this.#scoped?.size ?? 0;
|
|
@@ -126,6 +206,10 @@ export class ScopeManager {
|
|
|
126
206
|
}
|
|
127
207
|
this.#inflight?.clear();
|
|
128
208
|
this.#scoped?.clear();
|
|
209
|
+
this.#firstScopedBinding = undefined;
|
|
210
|
+
this.#firstScopedOwner = undefined;
|
|
211
|
+
this.#laterScopedBindings = undefined;
|
|
212
|
+
this.#laterScopedOwners = undefined;
|
|
129
213
|
}
|
|
130
214
|
/** Whether the deferred scoped-instance cache has had to be built. */
|
|
131
215
|
get isScopedCacheBuilt() {
|
package/dist/metadata/keys.d.ts
CHANGED
|
@@ -17,7 +17,7 @@ export declare const LIFECYCLE_KEY: unique symbol;
|
|
|
17
17
|
*/
|
|
18
18
|
export declare const INJECT_ACCESSOR_KEY: unique symbol;
|
|
19
19
|
/**
|
|
20
|
-
* The symbol
|
|
20
|
+
* The symbol standard decorator transforms store class metadata under.
|
|
21
21
|
*
|
|
22
22
|
* @remarks Falls back to the global-registry symbol, which is what Babel and esbuild emit until
|
|
23
23
|
* a runtime ships `Symbol.metadata` natively.
|
package/dist/metadata/keys.js
CHANGED
|
@@ -17,7 +17,7 @@ export const LIFECYCLE_KEY = Symbol("di:lifecycle");
|
|
|
17
17
|
*/
|
|
18
18
|
export const INJECT_ACCESSOR_KEY = Symbol("di:inject-accessor");
|
|
19
19
|
/**
|
|
20
|
-
* The symbol
|
|
20
|
+
* The symbol standard decorator transforms store class metadata under.
|
|
21
21
|
*
|
|
22
22
|
* @remarks Falls back to the global-registry symbol, which is what Babel and esbuild emit until
|
|
23
23
|
* a runtime ships `Symbol.metadata` natively.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Binding } from "#core/binding";
|
|
2
|
-
import type {
|
|
2
|
+
import type { Token } from "#core/token";
|
|
3
|
+
import type { Constructor, ResolutionFrame } from "#core/types";
|
|
3
4
|
/**
|
|
4
5
|
* Creates the resolution-stack frame for one in-flight resolve of a binding.
|
|
5
6
|
*
|
|
@@ -17,11 +18,20 @@ export declare function buildResolutionFrame(binding: Binding, tokenName: string
|
|
|
17
18
|
*/
|
|
18
19
|
export declare function bindingsOf(frames: ReadonlyArray<ResolutionFrame>): Array<Binding>;
|
|
19
20
|
/**
|
|
20
|
-
* The token names along a stack plus the
|
|
21
|
+
* The token names along a stack plus the one ending the path, a cycle's closing frame or a miss — built only to throw.
|
|
21
22
|
*
|
|
22
23
|
* @since 0.6.0
|
|
23
24
|
*/
|
|
24
25
|
export declare function cycleNamesOf(resolutionStack: ReadonlyArray<ResolutionFrame>, closingName: string): Array<string>;
|
|
26
|
+
/**
|
|
27
|
+
* The token names from the outermost request to a lookup that selected nothing, its alias hops included.
|
|
28
|
+
*
|
|
29
|
+
* @remarks `aliasHops` is the set an alias walk keeps, which starts at `requested` — the name the path already
|
|
30
|
+
* ends on.
|
|
31
|
+
*
|
|
32
|
+
* @since 0.13.0
|
|
33
|
+
*/
|
|
34
|
+
export declare function missPathOf(resolutionStack: ReadonlyArray<ResolutionFrame>, requested: Token<unknown> | Constructor, aliasHops: ReadonlySet<Token<unknown> | Constructor> | undefined): Array<string>;
|
|
25
35
|
/**
|
|
26
36
|
* Marks a level in flight on a synchronous path and pushes its frame, throwing if its binding already is.
|
|
27
37
|
*
|
package/dist/resolution/path.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
import { tokenName } from "#core/token";
|
|
2
|
+
/** The resolution path: cycle bookkeeping carried on the stack itself, and the names an error prints. */
|
|
2
3
|
import { CircularDependencyError } from "#errors";
|
|
3
4
|
/** The key under which a resolver-built frame carries its binding, for marking a seeded path. */
|
|
4
5
|
const FRAME_BINDING = Symbol("di:frame-binding");
|
|
@@ -38,7 +39,7 @@ export function bindingsOf(frames) {
|
|
|
38
39
|
return bindings;
|
|
39
40
|
}
|
|
40
41
|
/**
|
|
41
|
-
* The token names along a stack plus the
|
|
42
|
+
* The token names along a stack plus the one ending the path, a cycle's closing frame or a miss — built only to throw.
|
|
42
43
|
*
|
|
43
44
|
* @since 0.6.0
|
|
44
45
|
*/
|
|
@@ -50,6 +51,25 @@ export function cycleNamesOf(resolutionStack, closingName) {
|
|
|
50
51
|
names[resolutionStack.length] = closingName;
|
|
51
52
|
return names;
|
|
52
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* The token names from the outermost request to a lookup that selected nothing, its alias hops included.
|
|
56
|
+
*
|
|
57
|
+
* @remarks `aliasHops` is the set an alias walk keeps, which starts at `requested` — the name the path already
|
|
58
|
+
* ends on.
|
|
59
|
+
*
|
|
60
|
+
* @since 0.13.0
|
|
61
|
+
*/
|
|
62
|
+
export function missPathOf(resolutionStack, requested, aliasHops) {
|
|
63
|
+
const path = cycleNamesOf(resolutionStack, tokenName(requested));
|
|
64
|
+
if (aliasHops !== undefined) {
|
|
65
|
+
for (const hop of aliasHops) {
|
|
66
|
+
if (hop !== requested) {
|
|
67
|
+
path.push(tokenName(hop));
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return path;
|
|
72
|
+
}
|
|
53
73
|
/**
|
|
54
74
|
* Marks a level in flight on a synchronous path and pushes its frame, throwing if its binding already is.
|
|
55
75
|
*
|
|
@@ -3,6 +3,7 @@ import type { Binding } from "#core/binding";
|
|
|
3
3
|
import type { BindingRegistry } from "#core/registry";
|
|
4
4
|
import type { Token } from "#core/token";
|
|
5
5
|
import type { BindingTag, Constructor, ResolutionFrame, ResolveOptions } from "#core/types";
|
|
6
|
+
import { NoMatchingBindingError, TokenNotBoundError } from "#errors";
|
|
6
7
|
import type { ResolutionDiagnostics } from "#introspection/diagnostics";
|
|
7
8
|
import type { LifecycleManager } from "#lifecycle/hooks";
|
|
8
9
|
import type { ScopeManager } from "#lifecycle/scopes";
|
|
@@ -39,6 +40,17 @@ export declare class DependencyResolver implements ResolverCallbacks {
|
|
|
39
40
|
* Mirrors {@link DependencyResolver.resolveAll} candidate selection only (no instantiation).
|
|
40
41
|
*/
|
|
41
42
|
peekCandidateBindingsForValidate(token: Token<unknown> | Constructor, options: ResolveOptions | undefined): ReadonlyArray<Binding>;
|
|
43
|
+
/**
|
|
44
|
+
* The error `resolve` raises for a request nothing selects, or `undefined` while a `when()` candidate leaves it open.
|
|
45
|
+
*
|
|
46
|
+
* @remarks A predicate reads the resolution path, which a static walk does not have, so a slot-matching candidate
|
|
47
|
+
* that carries one may still be selected when the request is really made.
|
|
48
|
+
*/
|
|
49
|
+
missForValidate(token: Token<unknown> | Constructor, options: ResolveOptions | undefined, path: ReadonlyArray<string>): TokenNotBoundError | NoMatchingBindingError | undefined;
|
|
50
|
+
/** The resolver whose chain a binding this one can see resolves its dependencies from, as a singleton does. */
|
|
51
|
+
ownerForValidate(binding: Binding): DependencyResolver;
|
|
52
|
+
/** Deactivates a scoped instance of a binding this resolver owns, with its hooks and reader, when it owes a teardown. */
|
|
53
|
+
deactivateScoped(binding: Binding, instance: unknown): Promise<void> | undefined;
|
|
42
54
|
resolveFromContext<Value>(token: Token<Value> | Constructor<Value>, resolutionStack: Array<ResolutionFrame>): Value;
|
|
43
55
|
resolve<Value>(token: Token<Value> | Constructor<Value>, options: ResolveOptions | undefined, resolutionStack: Array<ResolutionFrame>, precomputedCriterion?: BindingTag | null): Value;
|
|
44
56
|
resolveOptional<Value>(token: Token<Value> | Constructor<Value>, options: ResolveOptions | undefined, resolutionStack: Array<ResolutionFrame>, precomputedCriterion?: BindingTag | null): Value | undefined;
|