@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/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
- constructor(tokenName: string);
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
- constructor(tokenName: string, options: ResolveOptions, availableSlots: Array<string>);
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 or scoped. */
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
- constructor(tokenName) {
31
- super(`No binding found for token '${tokenName}'. Did you forget container.bind(${tokenName})?`);
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
- constructor(tokenName, options, availableSlots) {
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 or scoped. */
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 or scoped, so the hook can never run: only a singleton or a constant is deactivated. Make one of its bindings a singleton, or drop the hook.`);
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, SyncDisposalNotSupportedError, EmptyTagCriteriaError, TokenNotBoundError, UnreachableConstraintError, UnreachableLifecycleHookError, } from "#errors";
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, SyncDisposalNotSupportedError, EmptyTagCriteriaError, TokenNotBoundError, UnreachableConstraintError, UnreachableLifecycleHookError, } from "#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, 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 { steps, binding: undefined, outcome, lastToken: current, ambiguity: undefined, cycle: undefined };
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 { steps, binding, outcome: "selected", lastToken: current, ambiguity: undefined, cycle: undefined };
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
  /**
@@ -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
- /** Releases a removed binding's scoped instance, by its scoped cache key. A scoped instance has no deactivation. */
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;
@@ -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
- /** Releases a removed binding's scoped instance, by its scoped cache key. A scoped instance has no deactivation. */
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() {
@@ -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 TC39 Stage 3 decorator transforms store class metadata under.
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.
@@ -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 TC39 Stage 3 decorator transforms store class metadata under.
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 { ResolutionFrame } from "#core/types";
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 frame that closed the cycle — built only to throw.
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
  *
@@ -1,4 +1,5 @@
1
- /** Cycle-detection bookkeeping carried on the resolution stack itself. */
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 frame that closed the cycle — built only to throw.
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;