@codefast/di 0.9.0 → 0.10.1

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 (73) hide show
  1. package/CHANGELOG.md +165 -0
  2. package/README.md +6 -5
  3. package/dist/ambient/active-container.d.ts +14 -4
  4. package/dist/ambient/active-container.js +22 -1
  5. package/dist/container/binding-builders.d.ts +35 -13
  6. package/dist/container/binding-builders.js +174 -97
  7. package/dist/container/container.d.ts +10 -10
  8. package/dist/container/container.js +53 -36
  9. package/dist/core/binding-scope.d.ts +2 -2
  10. package/dist/core/binding.d.ts +42 -49
  11. package/dist/core/binding.js +12 -38
  12. package/dist/core/constraint-requirement.d.ts +1 -1
  13. package/dist/core/module.d.ts +3 -3
  14. package/dist/core/registry.d.ts +52 -9
  15. package/dist/core/registry.js +376 -162
  16. package/dist/core/state-epoch.d.ts +16 -0
  17. package/dist/core/state-epoch.js +21 -0
  18. package/dist/core/token.d.ts +1 -1
  19. package/dist/core/types.d.ts +12 -9
  20. package/dist/decorators/inject.d.ts +3 -3
  21. package/dist/decorators/inject.js +5 -5
  22. package/dist/decorators/injectable.d.ts +2 -2
  23. package/dist/decorators/injectable.js +2 -2
  24. package/dist/decorators/lifecycle-decorators.js +2 -2
  25. package/dist/errors/diagnostics.d.ts +2 -0
  26. package/dist/errors/errors.d.ts +29 -3
  27. package/dist/errors/errors.js +34 -2
  28. package/dist/index.d.ts +35 -35
  29. package/dist/index.js +19 -19
  30. package/dist/injection/descriptor.d.ts +9 -7
  31. package/dist/injection/descriptor.js +3 -1
  32. package/dist/injection/resolve-options.d.ts +9 -3
  33. package/dist/injection/resolve-options.js +17 -1
  34. package/dist/introspection/dependency-graph.d.ts +3 -3
  35. package/dist/introspection/dependency-graph.js +15 -10
  36. package/dist/introspection/graph-adapters/cytoscape.d.ts +1 -1
  37. package/dist/introspection/graph-adapters/dot.d.ts +1 -1
  38. package/dist/introspection/graph-adapters/mermaid.d.ts +1 -1
  39. package/dist/introspection/graph-adapters/reactflow.d.ts +1 -1
  40. package/dist/introspection/inspector.d.ts +7 -5
  41. package/dist/introspection/inspector.js +13 -27
  42. package/dist/lifecycle/lifecycle-manager.d.ts +4 -4
  43. package/dist/lifecycle/lifecycle-manager.js +13 -11
  44. package/dist/lifecycle/scope-manager.d.ts +2 -2
  45. package/dist/lifecycle/scope-manager.js +4 -4
  46. package/dist/metadata/metadata-reader-token.d.ts +2 -2
  47. package/dist/metadata/metadata-reader-token.js +1 -1
  48. package/dist/metadata/metadata-types.d.ts +3 -3
  49. package/dist/metadata/symbol-metadata-reader.d.ts +3 -3
  50. package/dist/metadata/symbol-metadata-reader.js +1 -1
  51. package/dist/metadata/verifying-metadata-reader.d.ts +1 -1
  52. package/dist/metadata/verifying-metadata-reader.js +2 -2
  53. package/dist/resolution/cache/activation-need.d.ts +6 -4
  54. package/dist/resolution/cache/activation-need.js +13 -6
  55. package/dist/resolution/cache/binding-lookup-cache.d.ts +39 -6
  56. package/dist/resolution/cache/binding-lookup-cache.js +97 -13
  57. package/dist/resolution/cache/class-introspector.d.ts +6 -6
  58. package/dist/resolution/cache/class-introspector.js +82 -69
  59. package/dist/resolution/context.d.ts +11 -11
  60. package/dist/resolution/context.js +1 -1
  61. package/dist/resolution/path/resolution-path.d.ts +1 -1
  62. package/dist/resolution/path/resolution-path.js +1 -1
  63. package/dist/resolution/plan/instantiation-plan.d.ts +16 -4
  64. package/dist/resolution/plan/instantiation-plan.js +160 -69
  65. package/dist/resolution/plan/plan-codegen.d.ts +100 -0
  66. package/dist/resolution/plan/plan-codegen.js +185 -0
  67. package/dist/resolution/resolver.d.ts +26 -16
  68. package/dist/resolution/resolver.js +401 -147
  69. package/dist/resolution/select/binding-select.d.ts +6 -5
  70. package/dist/resolution/select/binding-select.js +17 -11
  71. package/dist/resolution/select/constraints.d.ts +3 -3
  72. package/dist/resolution/select/constraints.js +4 -4
  73. package/package.json +11 -3
@@ -1,7 +1,7 @@
1
- import type { BindingRegistry } from "#/core/registry";
2
- import type { Token } from "#/core/token";
3
- import type { BindingIdentifier, BindingKind, BindingScope, BindingTag, Constructor, ResolveOptions } from "#/core/types";
4
- import type { ScopeManager } from "#/lifecycle/scope-manager";
1
+ import type { BindingRegistry } from "#core/registry";
2
+ import type { Token } from "#core/token";
3
+ import type { BindingIdentifier, BindingKind, BindingScope, BindingTag, Constructor, ResolveOptions } from "#core/types";
4
+ import type { ScopeManager } from "#lifecycle/scope-manager";
5
5
  /**
6
6
  * A read-only view of one binding: token, kind, scope, slot, and id.
7
7
  *
@@ -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
  }
@@ -1,6 +1,6 @@
1
- import { effectiveBindingScope } from "#/core/binding-scope";
2
- import { tokenName } from "#/core/token";
3
- import { selectAllBindings } from "#/resolution/select/binding-select";
1
+ import { effectiveBindingScope } from "#core/binding-scope";
2
+ import { tokenName } from "#core/token";
3
+ import { selectAllBindings } from "#resolution/select/binding-select";
4
4
  // ── Inspector ────────────────────────────────────────────────────────────────────────────────────────────────────────
5
5
  /**
6
6
  * The read-only introspection surface behind a container's `inspect()`.
@@ -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,7 +1,7 @@
1
- import type { Binding } from "#/core/binding";
2
- import type { Token } from "#/core/token";
3
- import type { ActivationHandler, Constructor, DeactivationHandler, DependencyKey, ResolutionContext } from "#/core/types";
4
- import type { MetadataReader } from "#/metadata/metadata-types";
1
+ import type { Binding } from "#core/binding";
2
+ import type { Token } from "#core/token";
3
+ import type { ActivationHandler, Constructor, DeactivationHandler, DependencyKey, ResolutionContext } from "#core/types";
4
+ import type { MetadataReader } from "#metadata/metadata-types";
5
5
  /**
6
6
  * One container's registry of container-level activation and deactivation hooks, keyed by token.
7
7
  *
@@ -1,6 +1,7 @@
1
- import { getOrInsert } from "#/core/map-upsert";
2
- import { tokenName } from "#/core/token";
3
- import { AsyncActivationError, AsyncDeactivationError, InvalidMetadataError } from "#/errors/errors";
1
+ import { getOrInsert } from "#core/map-upsert";
2
+ import { advanceStateEpoch } from "#core/state-epoch";
3
+ import { tokenName } from "#core/token";
4
+ import { AsyncActivationError, AsyncDeactivationError, InvalidMetadataError } from "#errors/errors";
4
5
  /**
5
6
  * One container's registry of container-level activation and deactivation hooks, keyed by token.
6
7
  *
@@ -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
  }
@@ -1,5 +1,5 @@
1
- import type { Binding } from "#/core/binding";
2
- import type { BindingIdentifier } from "#/core/types";
1
+ import type { Binding } from "#core/binding";
2
+ import type { BindingIdentifier } from "#core/types";
3
3
  /**
4
4
  * One container's instance caches — singletons, in-flight async creations, and the scoped cache.
5
5
  *
@@ -1,6 +1,6 @@
1
- import { NO_INSTANCE } from "#/core/binding";
2
- import { tokenName } from "#/core/token";
3
- import { MissingScopeContextError } from "#/errors/errors";
1
+ import { NO_INSTANCE } from "#core/binding";
2
+ import { tokenName } from "#core/token";
3
+ import { MissingScopeContextError } from "#errors/errors";
4
4
  /**
5
5
  * One container's instance caches — singletons, in-flight async creations, and the scoped cache.
6
6
  *
@@ -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) {
@@ -1,5 +1,5 @@
1
- import type { Token } from "#/core/token";
2
- import type { MetadataReader } from "#/metadata/metadata-types";
1
+ import type { Token } from "#core/token";
2
+ import type { MetadataReader } from "#metadata/metadata-types";
3
3
  /**
4
4
  * The token a custom `MetadataReader` is bound to for a container to pick up.
5
5
  *
@@ -1,4 +1,4 @@
1
- import { token } from "#/core/token";
1
+ import { token } from "#core/token";
2
2
  /**
3
3
  * The token a custom `MetadataReader` is bound to for a container to pick up.
4
4
  *
@@ -1,6 +1,6 @@
1
- import type { Constructor } from "#/core/types";
2
- import type { InjectionDescriptor } from "#/injection/descriptor";
3
- import type { DependencySlot } from "#/injection/resolve-options";
1
+ import type { Constructor } from "#core/types";
2
+ import type { InjectionDescriptor } from "#injection/descriptor";
3
+ import type { DependencySlot } from "#injection/resolve-options";
4
4
  /**
5
5
  * One constructor parameter's declaration.
6
6
  *
@@ -1,6 +1,6 @@
1
- import type { Constructor } from "#/core/types";
2
- import type { InjectionDescriptor } from "#/injection/descriptor";
3
- import type { ConstructorMetadata, LifecycleMetadata, MetadataReader } from "#/metadata/metadata-types";
1
+ import type { Constructor } from "#core/types";
2
+ import type { InjectionDescriptor } from "#injection/descriptor";
3
+ import type { ConstructorMetadata, LifecycleMetadata, MetadataReader } from "#metadata/metadata-types";
4
4
  type AccessorEntry = {
5
5
  readonly key: string | symbol;
6
6
  readonly descriptor: InjectionDescriptor;
@@ -1,4 +1,4 @@
1
- import { INJECT_ACCESSOR_KEY, INJECTABLE_KEY, LIFECYCLE_KEY, METADATA_SYMBOL } from "#/metadata/metadata-keys";
1
+ import { INJECT_ACCESSOR_KEY, INJECTABLE_KEY, LIFECYCLE_KEY, METADATA_SYMBOL } from "#metadata/metadata-keys";
2
2
  /**
3
3
  * The default `MetadataReader`, reading decorator metadata stored under `Symbol.metadata`.
4
4
  *
@@ -2,7 +2,7 @@
2
2
  * Wraps a foreign {@link MetadataReader} so its answers are verified before anything dereferences
3
3
  * them.
4
4
  */
5
- import type { MetadataReader } from "#/metadata/metadata-types";
5
+ import type { MetadataReader } from "#metadata/metadata-types";
6
6
  /**
7
7
  * The reader a container should hand its resolver: verified when it came from outside.
8
8
  *
@@ -2,8 +2,8 @@
2
2
  * Wraps a foreign {@link MetadataReader} so its answers are verified before anything dereferences
3
3
  * them.
4
4
  */
5
- import { defaultMetadataReader } from "#/metadata/symbol-metadata-reader";
6
- import { verifyAccessorMetadata, verifyConstructorMetadata, verifyLifecycleMetadata, } from "#/resolution/cache/class-introspector";
5
+ import { defaultMetadataReader } from "#metadata/symbol-metadata-reader";
6
+ import { verifyAccessorMetadata, verifyConstructorMetadata, verifyLifecycleMetadata, } from "#resolution/cache/class-introspector";
7
7
  // Wrapping a wrapper would stack a layer per child container, so each one is remembered.
8
8
  const verifyingReaders = new WeakSet();
9
9
  /**
@@ -4,10 +4,10 @@
4
4
  * @remarks Versioned on the lifecycle manager plus the own registry, since `onActivation` can be
5
5
  * registered at any time and a rebind mints binding ids the memo must not keep forever.
6
6
  */
7
- import type { Binding } from "#/core/binding";
8
- import type { BindingRegistry } from "#/core/registry";
9
- import type { LifecycleManager } from "#/lifecycle/lifecycle-manager";
10
- import type { ClassIntrospector } from "#/resolution/cache/class-introspector";
7
+ import type { Binding } from "#core/binding";
8
+ import type { BindingRegistry } from "#core/registry";
9
+ import type { LifecycleManager } from "#lifecycle/lifecycle-manager";
10
+ import type { ClassIntrospector } from "#resolution/cache/class-introspector";
11
11
  /**
12
12
  * A per-binding cache of whether activation work — hooks or `@postConstruct` — is needed on resolve.
13
13
  *
@@ -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.
@@ -4,11 +4,11 @@
4
4
  * @remarks Each cache links to its parent's, so a child answers from its own chain without walking
5
5
  * the container hierarchy again.
6
6
  */
7
- import type { Binding } from "#/core/binding";
8
- import type { BindingRegistry } from "#/core/registry";
9
- import type { BindingTag } from "#/core/tag";
10
- import type { Token } from "#/core/token";
11
- import type { Constructor } from "#/core/types";
7
+ import type { Binding } from "#core/binding";
8
+ import type { BindingRegistry } from "#core/registry";
9
+ import type { BindingTag } from "#core/tag";
10
+ import type { Token } from "#core/token";
11
+ import type { Constructor } from "#core/types";
12
12
  /**
13
13
  * A token's terminal binding with alias hops already folded, plus the container that owns it.
14
14
  *
@@ -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
  }
@@ -1,4 +1,5 @@
1
- import { getOrInsertComputed } from "#/core/map-upsert";
1
+ import { getOrInsertComputed } from "#core/map-upsert";
2
+ import { stateEpoch } from "#core/state-epoch";
2
3
  /**
3
4
  * Alias folding gives up past this many hops and defers to the full resolve loop, whose
4
5
  * Set-based traversal detects genuine cycles exactly rather than by an arbitrary cap.
@@ -7,27 +8,37 @@ import { getOrInsertComputed } from "#/core/map-upsert";
7
8
  */
8
9
  export const ALIAS_HOP_LIMIT = 32;
9
10
  const newTagToEntryMap = () => new Map();
11
+ const newNameToTagMap = () => new Map();
10
12
  /**
11
13
  * A version-stamped cache of binding lookups by token and criterion across the container chain.
12
14
  *
13
15
  * @since 0.5.0-canary.9
14
16
  */
15
17
  export class BindingLookupCache {
16
- #byToken = new Map();
18
+ #byToken;
17
19
  #version = -1;
18
- // One entry in front of the map: the two shapes that reach here — an alias, and a token owned by
19
- // a parent — are both resolved in a loop over the same token. `null` is a real answer, so absence
20
- // is tracked by the token slot rather than by the entry.
20
+ // The last chain sum and the epoch it was taken at.
21
+ #chainVersion = -1;
22
+ #chainEpoch = -1;
23
+ // One entry in front of the map, and the map is not written until a second distinct token appears
24
+ // in one generation: the two shapes that reach here — an alias, and a token owned by a parent —
25
+ // are both resolved in a loop over the same token. `null` is a real answer, so absence is tracked
26
+ // by the token slot rather than by the entry.
21
27
  #lastToken;
22
28
  #lastEntry = null;
23
- #byTokenAndTag = new Map();
29
+ #byTokenAndTag;
24
30
  #taggedVersion = -1;
31
+ #byTokenNameAndTag;
32
+ #pairVersion = -1;
25
33
  // One entry in front of the tag map, and the map is not written until a second distinct request
26
34
  // shape appears: a per-request child usually asks one (token, tag) once, and the inner-map
27
35
  // allocation was that shape's whole regression when this memo landed.
28
36
  #lastTagToken;
29
37
  #lastTag;
30
38
  #lastTaggedEntry = null;
39
+ // Root-level collections by token, stamped with the chain version like the two memos above.
40
+ #collections;
41
+ #collectionsVersion = -1;
31
42
  #registry;
32
43
  #owner;
33
44
  #parent;
@@ -36,29 +47,56 @@ export class BindingLookupCache {
36
47
  this.#owner = owner;
37
48
  this.#parent = parent;
38
49
  }
39
- /** Summed registry versions of this cache's whole chain — the memo stamp. */
50
+ /** Whether a second distinct token or tag has had to allocate a memo map behind the one-entry fronts. */
51
+ get isMemoBuilt() {
52
+ return this.#byToken !== undefined || this.#byTokenAndTag !== undefined || this.#byTokenNameAndTag !== undefined;
53
+ }
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
+ */
40
60
  chainVersion() {
61
+ // A root's sum is its own version: one field read, cheaper than the memo it would stamp.
62
+ if (this.#parent === undefined) {
63
+ return this.#registry.version;
64
+ }
65
+ const epoch = stateEpoch();
66
+ if (epoch === this.#chainEpoch) {
67
+ return this.#chainVersion;
68
+ }
41
69
  let version = this.#registry.version;
42
70
  for (let cache = this.#parent; cache !== undefined; cache = cache.#parent) {
43
71
  version += cache.#registry.version;
44
72
  }
73
+ this.#chainEpoch = epoch;
74
+ this.#chainVersion = version;
45
75
  return version;
46
76
  }
47
77
  /** `null` when the token's shape needs the full selection path. */
48
78
  defaultEntry(token) {
49
79
  const version = this.chainVersion();
50
80
  if (version !== this.#version) {
51
- this.#byToken.clear();
81
+ this.#byToken?.clear();
52
82
  this.#version = version;
53
83
  this.#lastToken = undefined;
54
84
  }
55
85
  else if (token === this.#lastToken) {
56
86
  return this.#lastEntry;
57
87
  }
58
- let entry = this.#byToken.get(token);
59
- if (entry === undefined) {
88
+ let entry;
89
+ if (this.#lastToken === undefined) {
90
+ // First token this cache generation sees: answer from the walk and defer the map entirely.
60
91
  entry = this.#foldAliases(token);
61
- this.#byToken.set(token, entry);
92
+ }
93
+ else {
94
+ const byToken = (this.#byToken ??= new Map());
95
+ entry = byToken.get(token);
96
+ if (entry === undefined) {
97
+ entry = this.#foldAliases(token);
98
+ byToken.set(token, entry);
99
+ }
62
100
  }
63
101
  this.#lastToken = token;
64
102
  this.#lastEntry = entry;
@@ -68,7 +106,7 @@ export class BindingLookupCache {
68
106
  taggedEntry(token, tag) {
69
107
  const version = this.chainVersion();
70
108
  if (version !== this.#taggedVersion) {
71
- this.#byTokenAndTag.clear();
109
+ this.#byTokenAndTag?.clear();
72
110
  this.#taggedVersion = version;
73
111
  this.#lastTagToken = undefined;
74
112
  this.#lastTag = undefined;
@@ -84,7 +122,7 @@ export class BindingLookupCache {
84
122
  else {
85
123
  // Keyed by the criterion object itself: criteria are interned, so identity is the slot
86
124
  // contract's own `Object.is` — the same exactness the registry's tagged index relies on.
87
- const byTag = getOrInsertComputed(this.#byTokenAndTag, token, newTagToEntryMap);
125
+ const byTag = getOrInsertComputed((this.#byTokenAndTag ??= new Map()), token, newTagToEntryMap);
88
126
  entry = byTag.get(tag);
89
127
  if (entry === undefined) {
90
128
  entry = this.#findTaggedInChain(token, tag);
@@ -96,6 +134,52 @@ export class BindingLookupCache {
96
134
  this.#lastTaggedEntry = entry;
97
135
  return entry;
98
136
  }
137
+ /**
138
+ * The memoized entry for a request carrying a name and one tag, or `null` when the answer is not this lane's.
139
+ *
140
+ * @remarks Same contract as the one-criterion memo: a predicate needs a live context and an alias
141
+ * carries options through the full path, so both decline; a registry that holds the token without the
142
+ * exact slot declines too, leaving the parent walk to the full lookup.
143
+ */
144
+ namedTaggedEntry(token, nameCriterion, tag) {
145
+ const version = this.chainVersion();
146
+ if (version !== this.#pairVersion) {
147
+ this.#byTokenNameAndTag?.clear();
148
+ this.#pairVersion = version;
149
+ }
150
+ const byName = getOrInsertComputed((this.#byTokenNameAndTag ??= new Map()), token, newNameToTagMap);
151
+ const byTag = getOrInsertComputed(byName, nameCriterion, newTagToEntryMap);
152
+ let entry = byTag.get(tag);
153
+ if (entry === undefined) {
154
+ entry = this.#findPairInChain(token, nameCriterion, tag);
155
+ byTag.set(tag, entry);
156
+ }
157
+ return entry;
158
+ }
159
+ #findPairInChain(token, nameCriterion, tag) {
160
+ const found = this.#registry.getPairTagged(token, nameCriterion, tag);
161
+ if (found !== undefined) {
162
+ return found.predicate !== undefined || found.kind === "alias" ? null : { binding: found, owner: this.#owner };
163
+ }
164
+ if (this.#registry.has(token)) {
165
+ return null;
166
+ }
167
+ return this.#parent === undefined ? null : this.#parent.#findPairInChain(token, nameCriterion, tag);
168
+ }
169
+ /** The memoized root-level collection for a token, or `undefined` once the chain changed since it was stored. */
170
+ collection(token) {
171
+ const version = this.chainVersion();
172
+ if (version !== this.#collectionsVersion) {
173
+ this.#collections?.clear();
174
+ this.#collectionsVersion = version;
175
+ return undefined;
176
+ }
177
+ return this.#collections?.get(token);
178
+ }
179
+ /** Stores a root-level collection under the chain version the last `collection()` read stamped. */
180
+ rememberCollection(token, entry) {
181
+ (this.#collections ??= new Map()).set(token, entry);
182
+ }
99
183
  #foldAliases(token) {
100
184
  let current = token;
101
185
  for (let hop = 0; hop < ALIAS_HOP_LIMIT; hop += 1) {
@@ -3,11 +3,11 @@
3
3
  *
4
4
  * @remarks Metadata cannot change once a class is defined, so nothing here needs version stamping.
5
5
  */
6
- import type { AmbientResolution } from "#/ambient/active-container";
7
- import type { Container } from "#/container/container";
8
- import type { Constructor } from "#/core/types";
9
- import type { InjectionDescriptor } from "#/injection/descriptor";
10
- import type { ConstructorMetadata, LifecycleMetadata, MetadataReader } from "#/metadata/metadata-types";
6
+ import type { AmbientResolution } from "#ambient/active-container";
7
+ import type { Container } from "#container/container";
8
+ import type { Constructor } from "#core/types";
9
+ import type { InjectionDescriptor } from "#injection/descriptor";
10
+ import type { ConstructorMetadata, LifecycleMetadata, MetadataReader } from "#metadata/metadata-types";
11
11
  /**
12
12
  * A reader's constructor metadata for a class, verified the first time this process asks.
13
13
  *
@@ -52,7 +52,7 @@ export declare function verifyAccessorMetadata(reader: MetadataReader, target: C
52
52
  */
53
53
  export declare class ClassIntrospector {
54
54
  #private;
55
- constructor(reader: MetadataReader, container: Container);
55
+ constructor(reader: MetadataReader, container: Container, inherited: ClassIntrospector | undefined);
56
56
  constructorMetadata(target: Constructor): ConstructorMetadata | undefined;
57
57
  /**
58
58
  * Whether the class has a `@postConstruct` hook, or `undefined` until {@link discoverPostConstruct}.