@codefast/di 0.9.0 → 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.
Files changed (44) hide show
  1. package/CHANGELOG.md +137 -0
  2. package/README.md +3 -3
  3. package/dist/ambient/active-container.d.ts +13 -0
  4. package/dist/ambient/active-container.js +24 -0
  5. package/dist/container/binding-builders.d.ts +31 -9
  6. package/dist/container/binding-builders.js +155 -87
  7. package/dist/container/container.d.ts +2 -2
  8. package/dist/container/container.js +33 -16
  9. package/dist/core/binding.d.ts +36 -43
  10. package/dist/core/binding.js +11 -37
  11. package/dist/core/registry.d.ts +47 -4
  12. package/dist/core/registry.js +361 -159
  13. package/dist/core/state-epoch.d.ts +16 -0
  14. package/dist/core/state-epoch.js +21 -0
  15. package/dist/core/types.d.ts +7 -4
  16. package/dist/errors/diagnostics.d.ts +2 -0
  17. package/dist/errors/errors.d.ts +29 -0
  18. package/dist/errors/errors.js +35 -0
  19. package/dist/index.d.ts +1 -1
  20. package/dist/index.js +1 -1
  21. package/dist/injection/descriptor.d.ts +6 -4
  22. package/dist/injection/descriptor.js +3 -1
  23. package/dist/injection/resolve-options.d.ts +6 -0
  24. package/dist/injection/resolve-options.js +16 -0
  25. package/dist/introspection/dependency-graph.js +10 -5
  26. package/dist/introspection/inspector.d.ts +3 -1
  27. package/dist/introspection/inspector.js +10 -24
  28. package/dist/lifecycle/lifecycle-manager.js +10 -8
  29. package/dist/lifecycle/scope-manager.js +1 -1
  30. package/dist/resolution/cache/activation-need.d.ts +2 -0
  31. package/dist/resolution/cache/activation-need.js +13 -6
  32. package/dist/resolution/cache/binding-lookup-cache.d.ts +34 -1
  33. package/dist/resolution/cache/binding-lookup-cache.js +96 -12
  34. package/dist/resolution/cache/class-introspector.d.ts +1 -1
  35. package/dist/resolution/cache/class-introspector.js +28 -14
  36. package/dist/resolution/context.d.ts +8 -8
  37. package/dist/resolution/plan/instantiation-plan.d.ts +12 -0
  38. package/dist/resolution/plan/instantiation-plan.js +156 -65
  39. package/dist/resolution/plan/plan-codegen.d.ts +100 -0
  40. package/dist/resolution/plan/plan-codegen.js +185 -0
  41. package/dist/resolution/resolver.d.ts +14 -4
  42. package/dist/resolution/resolver.js +375 -134
  43. package/dist/resolution/select/binding-select.js +12 -7
  44. package/package.json +9 -1
@@ -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
+ }
@@ -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 string that uniquely identifies one binding.
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 = string & {
27
+ export type BindingIdentifier = number & {
25
28
  readonly [BINDING_ID_BRAND]: true;
26
29
  };
27
30
  /**
@@ -109,8 +112,8 @@ export interface ResolutionContext {
109
112
  resolveAsync<Value>(token: Token<Value> | Constructor<Value>, options?: ResolveOptions): Promise<Value>;
110
113
  resolveOptional<Value>(token: Token<Value> | Constructor<Value>, options?: ResolveOptions): Value | undefined;
111
114
  resolveOptionalAsync<Value>(token: Token<Value> | Constructor<Value>, options?: ResolveOptions): Promise<Value | undefined>;
112
- resolveAll<Value>(token: Token<Value> | Constructor<Value>, options?: ResolveOptions): Array<Value>;
113
- resolveAllAsync<Value>(token: Token<Value> | Constructor<Value>, options?: ResolveOptions): Promise<Array<Value>>;
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>>;
114
117
  readonly graph: ConstraintContext;
115
118
  }
116
119
  /**
@@ -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. */
@@ -232,6 +232,35 @@ export declare class MissingContainerContextError extends DiError {
232
232
  *
233
233
  * @since 0.5.0-canary.8
234
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
+ */
235
264
  export declare class ChainNotRegisteredError extends DiError {
236
265
  readonly code = "CHAIN_NOT_REGISTERED";
237
266
  readonly tokenName: string;
@@ -325,6 +325,41 @@ export class MissingContainerContextError extends DiError {
325
325
  *
326
326
  * @since 0.5.0-canary.8
327
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
+ */
328
363
  export class ChainNotRegisteredError extends DiError {
329
364
  code = "CHAIN_NOT_REGISTERED";
330
365
  tokenName;
package/dist/index.d.ts CHANGED
@@ -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";
@@ -51,7 +51,7 @@ export type InjectableDependency<Value = unknown> = Token<Value> | Constructor<V
51
51
  */
52
52
  export type ResolvedDependencyValue<Dependency> = Dependency extends {
53
53
  readonly multi: true;
54
- } ? Array<DescribedValue<Dependency>> : Dependency extends {
54
+ } ? ReadonlyArray<DescribedValue<Dependency>> : Dependency extends {
55
55
  readonly optional: true;
56
56
  } ? DescribedValue<Dependency> | undefined : DescribedValue<Dependency>;
57
57
  /**
@@ -59,7 +59,7 @@ export type ResolvedDependencyValue<Dependency> = Dependency extends {
59
59
  *
60
60
  * @remarks Split out because a hand-written descriptor states its flags and its value type
61
61
  * separately, and only the flags are load-bearing: `{ token: Plugin, multi: true }` says `Plugin`
62
- * and delivers `Array<Plugin>`. `injectAll()` and `optional()` already fold their effect in, so the
62
+ * and delivers `ReadonlyArray<Plugin>`. `injectAll()` and `optional()` already fold their effect in, so the
63
63
  * flags find an array or an optional there and leave it alone.
64
64
  */
65
65
  type DescribedValue<Dependency> = Dependency extends InjectionDescriptor<infer Value> ? Value : TokenValue<Dependency>;
@@ -88,9 +88,11 @@ export declare function buildInjectionDescriptor<Value, Names extends string = s
88
88
  */
89
89
  export declare function optional<Value, Names extends string = string>(token: Token<Value, Names> | Constructor<Value>, options?: NoInfer<InjectOptions<Names>>): InjectionDescriptor<Value | undefined>;
90
90
  /**
91
- * Creates a descriptor that resolves every matching binding for the token into an array.
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.
92
94
  *
93
95
  * @since 0.3.16-canary.0
94
96
  */
95
- export declare function injectAll<Value, Names extends string = string>(token: Token<Value, Names> | Constructor<Value>, options?: NoInfer<InjectOptions<Names>>): InjectionDescriptor<Array<Value>>;
97
+ export declare function injectAll<Value, Names extends string = string>(token: Token<Value, Names> | Constructor<Value>, options?: NoInfer<InjectOptions<Names>>): InjectionDescriptor<ReadonlyArray<Value>>;
96
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 an array.
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.id,
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.id, param, index, lookup);
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.id, dependency, index, lookup);
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({ from: binding.id, to: target.id, label: "alias", optional: false });
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.id,
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
- has(token: Token<unknown> | Constructor, options?: ResolveOptions, parentHas?: () => boolean): boolean;
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
- has(token, options, parentHas) {
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
- const bindings = this.#registry.getAll(token);
51
- if (bindings.length === 0) {
52
- return false;
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
- return true;
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.id,
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.onActivation !== undefined) {
81
- const activationResult = binding.onActivation(resolutionContext, activatedInstance);
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.onActivation !== undefined) {
104
- const activationResult = binding.onActivation(resolutionContext, activatedInstance);
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.onDeactivation !== undefined) {
138
- const hookResult = binding.onDeactivation(instance);
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.onDeactivation !== undefined) {
166
- const hookResult = binding.onDeactivation(instance);
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.id, instance);
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) {
@@ -16,6 +16,8 @@ import type { ClassIntrospector } from "#/resolution/cache/class-introspector";
16
16
  export declare class ActivationNeedCache {
17
17
  #private;
18
18
  constructor(lifecycle: LifecycleManager, classes: ClassIntrospector, registry: BindingRegistry);
19
+ /** Whether an answer the early returns could not give has had to allocate the memo. */
20
+ get isMemoBuilt(): boolean;
19
21
  needsActivation<Value>(binding: Binding<Value>): boolean;
20
22
  /**
21
23
  * Settles a class binding's answer once its lifecycle metadata has actually been read, which
@@ -4,7 +4,9 @@
4
4
  * @since 0.5.0-canary.8
5
5
  */
6
6
  export class ActivationNeedCache {
7
- #needByBindingId = new Map();
7
+ // Allocated by the first answer the early returns cannot give, so a hook-free container that
8
+ // resolves no class or alias never pays for it.
9
+ #needByBindingId;
8
10
  #version = -1;
9
11
  #lifecycle;
10
12
  #classes;
@@ -14,10 +16,14 @@ export class ActivationNeedCache {
14
16
  this.#classes = classes;
15
17
  this.#registry = registry;
16
18
  }
19
+ /** Whether an answer the early returns could not give has had to allocate the memo. */
20
+ get isMemoBuilt() {
21
+ return this.#needByBindingId !== undefined;
22
+ }
17
23
  needsActivation(binding) {
18
24
  // The chain writes a binding's own hook in place with no version anything here can see, so it
19
25
  // is read fresh on every call; the memo covers only container hooks and lifecycle metadata.
20
- if (binding.kind !== "alias" && binding.onActivation !== undefined) {
26
+ if (binding.kind !== "alias" && binding.activationHook !== undefined) {
21
27
  return true;
22
28
  }
23
29
  const lifecycleVersion = this.#lifecycle.activationVersion;
@@ -29,15 +35,16 @@ export class ActivationNeedCache {
29
35
  // The registry version evicts entries for binding ids a rebind has retired.
30
36
  const version = lifecycleVersion + this.#registry.version;
31
37
  if (this.#version !== version) {
32
- this.#needByBindingId.clear();
38
+ this.#needByBindingId?.clear();
33
39
  this.#version = version;
34
40
  }
35
- const cached = this.#needByBindingId.get(binding.id);
41
+ const memo = (this.#needByBindingId ??= new Map());
42
+ const cached = memo.get(binding.identifier);
36
43
  if (cached !== undefined) {
37
44
  return cached;
38
45
  }
39
46
  const needsActivation = binding.kind === "class" ? this.#classNeedsActivation(binding) : this.#nonClassNeedsActivation(binding);
40
- this.#needByBindingId.set(binding.id, needsActivation);
47
+ memo.set(binding.identifier, needsActivation);
41
48
  return needsActivation;
42
49
  }
43
50
  /**
@@ -52,7 +59,7 @@ export class ActivationNeedCache {
52
59
  return needsActivation;
53
60
  }
54
61
  this.#classes.discoverPostConstruct(binding.target);
55
- this.#needByBindingId.delete(binding.id);
62
+ this.#needByBindingId?.delete(binding.identifier);
56
63
  return this.needsActivation(binding);
57
64
  }
58
65
  // Own hooks are answered before the memo, so both computations cover the memoizable rest only.
@@ -20,6 +20,20 @@ export interface DefaultLookupEntry<Owner> {
20
20
  readonly binding: Binding;
21
21
  readonly owner: Owner;
22
22
  }
23
+ /**
24
+ * A root-level collection read, memoized until any registry in the chain changes.
25
+ *
26
+ * @remarks `values` is kept only while every member is a hook-free constant or a hook-free singleton
27
+ * whose instance is cached, and `activationVersion` is the chain's activation version that promise was
28
+ * made under.
29
+ *
30
+ * @since 0.10.0
31
+ */
32
+ export interface CollectionEntry {
33
+ readonly candidates: ReadonlyArray<Binding>;
34
+ values: ReadonlyArray<unknown> | undefined;
35
+ activationVersion: number;
36
+ }
23
37
  /**
24
38
  * Alias folding gives up past this many hops and defers to the full resolve loop, whose
25
39
  * Set-based traversal detects genuine cycles exactly rather than by an arbitrary cap.
@@ -35,10 +49,29 @@ export declare const ALIAS_HOP_LIMIT = 32;
35
49
  export declare class BindingLookupCache<Owner> {
36
50
  #private;
37
51
  constructor(registry: BindingRegistry, owner: Owner, parent: BindingLookupCache<Owner> | undefined);
38
- /** Summed registry versions of this cache's whole chain — the memo stamp. */
52
+ /** Whether a second distinct token or tag has had to allocate a memo map behind the one-entry fronts. */
53
+ get isMemoBuilt(): boolean;
54
+ /**
55
+ * Summed registry versions of this cache's whole chain — the memo stamp.
56
+ *
57
+ * @remarks Re-summed only when the process-wide state epoch has moved since the last sum: no
58
+ * registry anywhere changed in between, so no registry in this chain did either.
59
+ */
39
60
  chainVersion(): number;
40
61
  /** `null` when the token's shape needs the full selection path. */
41
62
  defaultEntry(token: Token<unknown> | Constructor): DefaultLookupEntry<Owner> | null;
42
63
  /** `null` when the tag's shape needs the full selection path. */
43
64
  taggedEntry(token: Token<unknown> | Constructor, tag: BindingTag): DefaultLookupEntry<Owner> | null;
65
+ /**
66
+ * The memoized entry for a request carrying a name and one tag, or `null` when the answer is not this lane's.
67
+ *
68
+ * @remarks Same contract as the one-criterion memo: a predicate needs a live context and an alias
69
+ * carries options through the full path, so both decline; a registry that holds the token without the
70
+ * exact slot declines too, leaving the parent walk to the full lookup.
71
+ */
72
+ namedTaggedEntry(token: Token<unknown> | Constructor, nameCriterion: BindingTag, tag: BindingTag): DefaultLookupEntry<Owner> | null;
73
+ /** The memoized root-level collection for a token, or `undefined` once the chain changed since it was stored. */
74
+ collection(token: Token<unknown> | Constructor): CollectionEntry | undefined;
75
+ /** Stores a root-level collection under the chain version the last `collection()` read stamped. */
76
+ rememberCollection(token: Token<unknown> | Constructor, entry: CollectionEntry): void;
44
77
  }