@codefast/di 0.11.0 → 0.12.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 (77) hide show
  1. package/CHANGELOG.md +111 -0
  2. package/README.md +26 -12
  3. package/dist/container/{binding-builders.d.ts → binding-chain.d.ts} +21 -28
  4. package/dist/container/{binding-builders.js → binding-chain.js} +53 -163
  5. package/dist/container/container.d.ts +11 -2
  6. package/dist/container/container.js +107 -64
  7. package/dist/core/binding-builders.d.ts +114 -0
  8. package/dist/core/binding-declaration.d.ts +7 -0
  9. package/dist/core/binding-declaration.js +3 -2
  10. package/dist/core/binding.d.ts +21 -149
  11. package/dist/core/binding.js +15 -25
  12. package/dist/core/module.d.ts +1 -1
  13. package/dist/core/registry.d.ts +9 -24
  14. package/dist/core/registry.js +30 -155
  15. package/dist/core/tag.js +1 -1
  16. package/dist/decorators/inject.js +5 -5
  17. package/dist/decorators/injectable.js +2 -2
  18. package/dist/decorators/{lifecycle-decorators.js → lifecycle.js} +3 -3
  19. package/dist/decorators/{decorator-metadata.js → metadata-record.js} +1 -1
  20. package/dist/{errors/errors.d.ts → errors.d.ts} +7 -6
  21. package/dist/{errors/errors.js → errors.js} +8 -7
  22. package/dist/index.d.ts +12 -10
  23. package/dist/index.js +6 -6
  24. package/dist/injection/descriptor.d.ts +1 -1
  25. package/dist/introspection/dependency-graph.d.ts +1 -1
  26. package/dist/introspection/dependency-graph.js +3 -4
  27. package/dist/introspection/explanation.d.ts +101 -0
  28. package/dist/introspection/explanation.js +151 -0
  29. package/dist/introspection/inspector.d.ts +9 -2
  30. package/dist/introspection/inspector.js +25 -20
  31. package/dist/lifecycle/{lifecycle-manager.d.ts → hooks.d.ts} +9 -2
  32. package/dist/lifecycle/{lifecycle-manager.js → hooks.js} +45 -28
  33. package/dist/lifecycle/{scope-manager.d.ts → scopes.d.ts} +3 -5
  34. package/dist/lifecycle/{scope-manager.js → scopes.js} +8 -19
  35. package/dist/metadata/{metadata-reader-token.d.ts → reader-token.d.ts} +1 -1
  36. package/dist/metadata/{symbol-metadata-reader.d.ts → symbol-reader.d.ts} +1 -1
  37. package/dist/metadata/{symbol-metadata-reader.js → symbol-reader.js} +1 -1
  38. package/dist/metadata/{metadata-types.d.ts → types.d.ts} +1 -1
  39. package/dist/metadata/types.js +1 -0
  40. package/dist/metadata/verify.d.ts +43 -0
  41. package/dist/metadata/verify.js +133 -0
  42. package/dist/metadata/{verifying-metadata-reader.d.ts → verifying-reader.d.ts} +1 -1
  43. package/dist/metadata/{verifying-metadata-reader.js → verifying-reader.js} +2 -2
  44. package/dist/resolution/cache/activation-need.d.ts +1 -1
  45. package/dist/resolution/cache/activation-need.js +5 -1
  46. package/dist/resolution/cache/class-introspector.d.ts +27 -36
  47. package/dist/resolution/cache/class-introspector.js +61 -159
  48. package/dist/resolution/cache/{binding-lookup-cache.d.ts → lookup.d.ts} +7 -2
  49. package/dist/resolution/cache/{binding-lookup-cache.js → lookup.js} +40 -8
  50. package/dist/resolution/context.d.ts +1 -1
  51. package/dist/resolution/context.js +1 -1
  52. package/dist/resolution/{path/resolution-path.js → path.js} +1 -1
  53. package/dist/resolution/plan/{plan-codegen.d.ts → codegen.d.ts} +2 -4
  54. package/dist/resolution/plan/{plan-codegen.js → codegen.js} +2 -4
  55. package/dist/resolution/plan/{instantiation-plan.d.ts → compiler.d.ts} +1 -1
  56. package/dist/resolution/plan/{instantiation-plan.js → compiler.js} +4 -4
  57. package/dist/resolution/resolver.d.ts +7 -7
  58. package/dist/resolution/resolver.js +308 -121
  59. package/dist/resolution/select/{binding-select.d.ts → candidates.d.ts} +30 -0
  60. package/dist/resolution/select/{binding-select.js → candidates.js} +51 -10
  61. package/dist/resolution/select/constraints.js +1 -1
  62. package/package.json +79 -71
  63. package/dist/core/binding-scope.d.ts +0 -12
  64. package/dist/core/binding-scope.js +0 -12
  65. /package/dist/{ambient/active-container.d.ts → ambient-container.d.ts} +0 -0
  66. /package/dist/{ambient/active-container.js → ambient-container.js} +0 -0
  67. /package/dist/{metadata/metadata-types.js → core/binding-builders.js} +0 -0
  68. /package/dist/decorators/{lifecycle-decorators.d.ts → lifecycle.d.ts} +0 -0
  69. /package/dist/decorators/{decorator-metadata.d.ts → metadata-record.d.ts} +0 -0
  70. /package/dist/injection/{resolve-options.d.ts → dependency-slot.d.ts} +0 -0
  71. /package/dist/injection/{resolve-options.js → dependency-slot.js} +0 -0
  72. /package/dist/{errors → introspection}/diagnostics.d.ts +0 -0
  73. /package/dist/{errors → introspection}/diagnostics.js +0 -0
  74. /package/dist/metadata/{metadata-keys.d.ts → keys.d.ts} +0 -0
  75. /package/dist/metadata/{metadata-keys.js → keys.js} +0 -0
  76. /package/dist/metadata/{metadata-reader-token.js → reader-token.js} +0 -0
  77. /package/dist/resolution/{path/resolution-path.d.ts → path.d.ts} +0 -0
@@ -1,7 +1,7 @@
1
1
  import { getOrInsert } from "#core/map-upsert";
2
2
  import { advanceStateEpoch } from "#core/state-epoch";
3
3
  import { tokenName } from "#core/token";
4
- import { AsyncActivationError, AsyncDeactivationError, InvalidMetadataError } from "#errors/errors";
4
+ import { AsyncActivationError, AsyncDeactivationError, InvalidMetadataError } from "#errors";
5
5
  /**
6
6
  * One container's registry of container-level activation and deactivation hooks, keyed by token.
7
7
  *
@@ -17,6 +17,10 @@ export class LifecycleManager {
17
17
  // and registration is the only thing that can change the answer.
18
18
  #cachedToken;
19
19
  #cachedHooks;
20
+ // The class whose hooks were read last, and through which reader: a teardown walks many bindings of one class.
21
+ #lastLifecycleTarget;
22
+ #lastLifecycleReader;
23
+ #lastLifecycle;
20
24
  registerActivation(token, handler) {
21
25
  this.#activationVersion += 1;
22
26
  advanceStateEpoch();
@@ -69,34 +73,39 @@ export class LifecycleManager {
69
73
  }
70
74
  return hooked;
71
75
  }
72
- async runActivation(resolutionContext, binding, instance, metadataReader) {
73
- let activatedInstance = instance;
76
+ /**
77
+ * Runs the activation pipeline on the async lane: the activated instance, or its promise once a
78
+ * hook has returned one.
79
+ *
80
+ * @remarks A level yields only where a hook does, so a synchronous pipeline hands the instance
81
+ * back in the caller's own tick and a singleton it activates is cached before any sibling reads it.
82
+ */
83
+ runActivation(resolutionContext, binding, instance, metadataReader) {
84
+ let activated = instance;
74
85
  // 1. @postConstruct() — after TC39 construction (constructor + accessor addInitializer callbacks)
75
- for (const methodName of lifecycleMethods(binding, metadataReader, "postConstruct")) {
76
- const hookResult = callHook(activatedInstance, methodName);
77
- if (hookResult instanceof Promise) {
78
- await hookResult;
79
- }
86
+ for (const methodName of this.#lifecycleMethods(binding, metadataReader, "postConstruct")) {
87
+ activated = afterActivationStep(activated, (current) => {
88
+ const hookResult = callHook(current, methodName);
89
+ return hookResult instanceof Promise ? hookResult.then(() => current) : current;
90
+ });
80
91
  }
81
92
  // 2. per-binding onActivation
82
93
  if (binding.kind !== "alias" && binding.activationHook !== undefined) {
83
- const activationResult = binding.activationHook(resolutionContext, activatedInstance);
84
- activatedInstance = activationResult instanceof Promise ? await activationResult : activationResult;
94
+ activated = afterActivationStep(activated, (current) => binding.activationHook(resolutionContext, current));
85
95
  }
86
96
  // 3. container-level onActivation
87
97
  const containerHooks = this.#activationHooks?.get(binding.token);
88
98
  if (containerHooks !== undefined) {
89
99
  for (const hook of containerHooks) {
90
- const activationResult = hook(resolutionContext, activatedInstance);
91
- activatedInstance = (activationResult instanceof Promise ? await activationResult : activationResult);
100
+ activated = afterActivationStep(activated, (current) => hook(resolutionContext, current));
92
101
  }
93
102
  }
94
- return activatedInstance;
103
+ return activated;
95
104
  }
96
105
  runActivationSync(resolutionContext, binding, instance, metadataReader) {
97
106
  let activatedInstance = instance;
98
107
  // 1. @postConstruct() — must be sync (instance fully constructed per TC39 order)
99
- for (const methodName of lifecycleMethods(binding, metadataReader, "postConstruct")) {
108
+ for (const methodName of this.#lifecycleMethods(binding, metadataReader, "postConstruct")) {
100
109
  const hookResult = callHook(activatedInstance, methodName);
101
110
  if (hookResult instanceof Promise) {
102
111
  // The hook has already run; adopt its rejection so a failing async hook cannot become an
@@ -115,14 +124,13 @@ export class LifecycleManager {
115
124
  activatedInstance = activationResult;
116
125
  }
117
126
  // 3. container-level onActivation (must be sync)
118
- const tokenDisplayName = tokenName(binding.token);
119
127
  const containerHooks = this.#activationHooks?.get(binding.token);
120
128
  if (containerHooks !== undefined) {
121
129
  for (const hook of containerHooks) {
122
130
  const activationResult = hook(resolutionContext, activatedInstance);
123
131
  if (activationResult instanceof Promise) {
124
132
  void activationResult.catch(() => { });
125
- throw new AsyncActivationError(tokenDisplayName, "onActivation");
133
+ throw new AsyncActivationError(tokenName(binding.token), "onActivation");
126
134
  }
127
135
  activatedInstance = activationResult;
128
136
  }
@@ -149,7 +157,7 @@ export class LifecycleManager {
149
157
  }
150
158
  }
151
159
  // 3. @preDestroy() — all methods in declaration order
152
- for (const methodName of lifecycleMethods(binding, metadataReader, "preDestroy")) {
160
+ for (const methodName of this.#lifecycleMethods(binding, metadataReader, "preDestroy")) {
153
161
  const hookResult = callHook(instance, methodName);
154
162
  if (hookResult instanceof Promise) {
155
163
  await hookResult;
@@ -157,7 +165,6 @@ export class LifecycleManager {
157
165
  }
158
166
  }
159
167
  runDeactivationSync(binding, instance, metadataReader) {
160
- const tokenDisplayName = tokenName(binding.token);
161
168
  const tokenKey = binding.token;
162
169
  // 1. container-level onDeactivation
163
170
  const containerHooks = this.#deactivationHooks?.get(tokenKey);
@@ -168,7 +175,7 @@ export class LifecycleManager {
168
175
  // The hook has already run; adopt its rejection so a failing async hook cannot become an
169
176
  // unhandled rejection that ends the process, then report the sync-lane violation.
170
177
  void hookResult.catch(() => { });
171
- throw new AsyncDeactivationError(tokenDisplayName);
178
+ throw new AsyncDeactivationError(tokenName(binding.token));
172
179
  }
173
180
  }
174
181
  }
@@ -177,30 +184,40 @@ export class LifecycleManager {
177
184
  const hookResult = binding.deactivationHook(instance);
178
185
  if (hookResult instanceof Promise) {
179
186
  void hookResult.catch(() => { });
180
- throw new AsyncDeactivationError(tokenDisplayName);
187
+ throw new AsyncDeactivationError(tokenName(binding.token));
181
188
  }
182
189
  }
183
190
  // 3. @preDestroy()
184
- for (const methodName of lifecycleMethods(binding, metadataReader, "preDestroy")) {
191
+ for (const methodName of this.#lifecycleMethods(binding, metadataReader, "preDestroy")) {
185
192
  const hookResult = callHook(instance, methodName);
186
193
  if (hookResult instanceof Promise) {
187
194
  void hookResult.catch(() => { });
188
- throw new AsyncDeactivationError(tokenDisplayName);
195
+ throw new AsyncDeactivationError(tokenName(binding.token));
189
196
  }
190
197
  }
191
198
  }
199
+ /** The `@postConstruct` / `@preDestroy` methods a binding declares — only a class can declare any. */
200
+ #lifecycleMethods(binding, metadataReader, phase) {
201
+ if (binding.kind !== "class") {
202
+ return NO_METHODS;
203
+ }
204
+ const target = binding.target;
205
+ if (target !== this.#lastLifecycleTarget || metadataReader !== this.#lastLifecycleReader) {
206
+ this.#lastLifecycle = metadataReader.getLifecycleMetadata(target);
207
+ this.#lastLifecycleTarget = target;
208
+ this.#lastLifecycleReader = metadataReader;
209
+ }
210
+ return this.#lastLifecycle?.[phase] ?? NO_METHODS;
211
+ }
192
212
  /** Whether the deferred activation-hook table has had to be built. */
193
213
  get isActivationTableBuilt() {
194
214
  return this.#activationHooks !== undefined;
195
215
  }
196
216
  }
197
217
  const NO_METHODS = [];
198
- /** The `@postConstruct` / `@preDestroy` methods a binding declares — only a class can declare any. */
199
- function lifecycleMethods(binding, metadataReader, phase) {
200
- if (binding.kind !== "class") {
201
- return NO_METHODS;
202
- }
203
- return metadataReader.getLifecycleMetadata(binding.target)?.[phase] ?? NO_METHODS;
218
+ /** Applies one activation step now, or once the step before it has settled. */
219
+ function afterActivationStep(activated, step) {
220
+ return activated instanceof Promise ? activated.then(step) : step(activated);
204
221
  }
205
222
  /**
206
223
  * Invokes a lifecycle hook by name.
@@ -17,8 +17,6 @@ export declare class ScopeManager {
17
17
  /** Every binding in this container holding a cached singleton. */
18
18
  cachedSingletons(): ReadonlyArray<Binding<unknown>>;
19
19
  deleteSingleton<Value>(binding: Binding<Value>): boolean;
20
- /** Swaps a re-slotted binding's tracked entry, so teardown pairs the instance with the live object. */
21
- replaceSingleton(previous: Binding, next: Binding): void;
22
20
  getInflight(id: BindingIdentifier): Promise<unknown> | undefined;
23
21
  setInflight(id: BindingIdentifier, promise: Promise<unknown>): void;
24
22
  clearInflight(id: BindingIdentifier): void;
@@ -28,11 +26,11 @@ export declare class ScopeManager {
28
26
  * @remarks One map read answers both existence and value; a cached `undefined` is the only
29
27
  * shape that pays for a second, and it is the rare one.
30
28
  */
31
- readScoped(id: BindingIdentifier): unknown;
29
+ readScoped(key: BindingIdentifier): unknown;
32
30
  /** Takes the binding rather than its id, so a failure here can name the token — as `setSingleton` does. */
33
31
  setScoped(binding: Binding, instance: unknown): void;
34
- /** Releases a removed binding's scoped instance. A scoped instance has no deactivation. */
35
- deleteScoped(id: BindingIdentifier): void;
32
+ /** Releases a removed binding's scoped instance, by its scoped cache key. A scoped instance has no deactivation. */
33
+ deleteScoped(key: BindingIdentifier): void;
36
34
  /** Scoped instances currently cached — a structural count for diagnostics. */
37
35
  get scopedCount(): number;
38
36
  clearAll(): void;
@@ -1,6 +1,6 @@
1
1
  import { NO_INSTANCE } from "#core/binding";
2
2
  import { tokenName } from "#core/token";
3
- import { MissingScopeContextError } from "#errors/errors";
3
+ import { MissingScopeContextError } from "#errors";
4
4
  /**
5
5
  * One container's instance caches — singletons, in-flight async creations, and the scoped cache.
6
6
  *
@@ -74,17 +74,6 @@ export class ScopeManager {
74
74
  this.#compacted = false;
75
75
  return true;
76
76
  }
77
- /** Swaps a re-slotted binding's tracked entry, so teardown pairs the instance with the live object. */
78
- replaceSingleton(previous, next) {
79
- const tracked = this.#singletonBindings;
80
- if (tracked === undefined) {
81
- return;
82
- }
83
- const index = tracked.indexOf(previous);
84
- if (index !== -1) {
85
- tracked[index] = next;
86
- }
87
- }
88
77
  getInflight(id) {
89
78
  return this.#inflight?.get(id);
90
79
  }
@@ -100,27 +89,27 @@ export class ScopeManager {
100
89
  * @remarks One map read answers both existence and value; a cached `undefined` is the only
101
90
  * shape that pays for a second, and it is the rare one.
102
91
  */
103
- readScoped(id) {
92
+ readScoped(key) {
104
93
  const scoped = this.#scoped;
105
94
  if (scoped === undefined) {
106
95
  return SCOPED_MISS;
107
96
  }
108
- const cached = scoped.get(id);
97
+ const cached = scoped.get(key);
109
98
  if (cached !== undefined) {
110
99
  return cached;
111
100
  }
112
- return scoped.has(id) ? undefined : SCOPED_MISS;
101
+ return scoped.has(key) ? undefined : SCOPED_MISS;
113
102
  }
114
103
  /** Takes the binding rather than its id, so a failure here can name the token — as `setSingleton` does. */
115
104
  setScoped(binding, instance) {
116
105
  if (!this.isChild) {
117
106
  throw new MissingScopeContextError(tokenName(binding.token));
118
107
  }
119
- (this.#scoped ??= new Map()).set(binding.identifier, instance);
108
+ (this.#scoped ??= new Map()).set(binding.scopedCacheKey, instance);
120
109
  }
121
- /** Releases a removed binding's scoped instance. A scoped instance has no deactivation. */
122
- deleteScoped(id) {
123
- this.#scoped?.delete(id);
110
+ /** Releases a removed binding's scoped instance, by its scoped cache key. A scoped instance has no deactivation. */
111
+ deleteScoped(key) {
112
+ this.#scoped?.delete(key);
124
113
  }
125
114
  /** Scoped instances currently cached — a structural count for diagnostics. */
126
115
  get scopedCount() {
@@ -1,5 +1,5 @@
1
1
  import type { Token } from "#core/token";
2
- import type { MetadataReader } from "#metadata/metadata-types";
2
+ import type { MetadataReader } from "#metadata/types";
3
3
  /**
4
4
  * The token a custom `MetadataReader` is bound to for a container to pick up.
5
5
  *
@@ -1,6 +1,6 @@
1
1
  import type { Constructor } from "#core/types";
2
2
  import type { InjectionDescriptor } from "#injection/descriptor";
3
- import type { ConstructorMetadata, LifecycleMetadata, MetadataReader } from "#metadata/metadata-types";
3
+ import type { ConstructorMetadata, LifecycleMetadata, MetadataReader } from "#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/keys";
2
2
  /**
3
3
  * The default `MetadataReader`, reading decorator metadata stored under `Symbol.metadata`.
4
4
  *
@@ -1,6 +1,6 @@
1
1
  import type { Constructor } from "#core/types";
2
+ import type { DependencySlot } from "#injection/dependency-slot";
2
3
  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
  *
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Verification of what a foreign `MetadataReader` claims about a class, done once per reader and class.
3
+ */
4
+ import type { Constructor } from "#core/types";
5
+ import type { InjectionDescriptor } from "#injection/descriptor";
6
+ import type { ConstructorMetadata, LifecycleMetadata, MetadataReader } from "#metadata/types";
7
+ /**
8
+ * A reader's constructor metadata for a class, verified the first time this process asks.
9
+ *
10
+ * @remarks Metadata cannot change once a class is defined, so re-checking per container would charge
11
+ * every fresh container for a fact already established — a per-request child or a cold boot pays
12
+ * that repeatedly.
13
+ *
14
+ * @since 0.6.0
15
+ */
16
+ export declare function verifyConstructorMetadata(reader: MetadataReader, target: Constructor): ConstructorMetadata | undefined;
17
+ /**
18
+ * Verifies what a reader claims about a class, since a `MetadataReader` is a public seam.
19
+ *
20
+ * @remarks Every path that reads constructor metadata comes through here, so a reader that answers
21
+ * wrongly is a named error at the class it described rather than a `TypeError` raised later inside a
22
+ * resolve. Only what a consumer dereferences is checked: `params` and each entry's `token`.
23
+ * `optional`/`multi` degrade to falsy without crashing, and `index` is decorative — dependencies are
24
+ * consumed positionally.
25
+ *
26
+ * @since 0.6.0
27
+ */
28
+ export declare function assertConstructorMetadata(metadata: unknown, target: Constructor): ConstructorMetadata | undefined;
29
+ /**
30
+ * A reader's lifecycle metadata for a class, verified the first time this process asks.
31
+ *
32
+ * @since 0.6.0
33
+ */
34
+ export declare function verifyLifecycleMetadata(reader: MetadataReader, target: Constructor): LifecycleMetadata | undefined;
35
+ /**
36
+ * A reader's accessor metadata for a class, verified the first time this process asks.
37
+ *
38
+ * @since 0.6.0
39
+ */
40
+ export declare function verifyAccessorMetadata(reader: MetadataReader, target: Constructor): ReadonlyArray<{
41
+ readonly key: string | symbol;
42
+ readonly descriptor: InjectionDescriptor;
43
+ }> | undefined;
@@ -0,0 +1,133 @@
1
+ import { InvalidMetadataError } from "#errors";
2
+ // Verified pairs, not verified classes: two readers may disagree about the same class, and a reader
3
+ // that goes out of scope takes its record with it.
4
+ const verifiedTargets = new WeakMap();
5
+ const verifiedLifecycleTargets = new WeakMap();
6
+ const verifiedAccessorTargets = new WeakMap();
7
+ function isVerified(cache, reader, target) {
8
+ return cache.get(reader)?.has(target) ?? false;
9
+ }
10
+ function markVerified(cache, reader, target) {
11
+ let verified = cache.get(reader);
12
+ if (verified === undefined) {
13
+ verified = new WeakSet();
14
+ cache.set(reader, verified);
15
+ }
16
+ verified.add(target);
17
+ }
18
+ /**
19
+ * Runs `validate` against a reader's metadata the first time this process sees the pair, then remembers it.
20
+ *
21
+ * @remarks An absent metadata answer needs no check; a verified pair is returned untouched, so the
22
+ * shape assertion runs once per `(reader, target)` rather than per container.
23
+ */
24
+ function verifyOnce(cache, reader, target, metadata, validate) {
25
+ if (metadata === undefined || isVerified(cache, reader, target)) {
26
+ return metadata;
27
+ }
28
+ validate(metadata);
29
+ markVerified(cache, reader, target);
30
+ return metadata;
31
+ }
32
+ /**
33
+ * A reader's constructor metadata for a class, verified the first time this process asks.
34
+ *
35
+ * @remarks Metadata cannot change once a class is defined, so re-checking per container would charge
36
+ * every fresh container for a fact already established — a per-request child or a cold boot pays
37
+ * that repeatedly.
38
+ *
39
+ * @since 0.6.0
40
+ */
41
+ export function verifyConstructorMetadata(reader, target) {
42
+ return verifyOnce(verifiedTargets, reader, target, reader.getConstructorMetadata(target), (metadata) => {
43
+ assertConstructorMetadata(metadata, target);
44
+ });
45
+ }
46
+ /**
47
+ * Verifies what a reader claims about a class, since a `MetadataReader` is a public seam.
48
+ *
49
+ * @remarks Every path that reads constructor metadata comes through here, so a reader that answers
50
+ * wrongly is a named error at the class it described rather than a `TypeError` raised later inside a
51
+ * resolve. Only what a consumer dereferences is checked: `params` and each entry's `token`.
52
+ * `optional`/`multi` degrade to falsy without crashing, and `index` is decorative — dependencies are
53
+ * consumed positionally.
54
+ *
55
+ * @since 0.6.0
56
+ */
57
+ export function assertConstructorMetadata(metadata, target) {
58
+ if (metadata === undefined) {
59
+ return undefined;
60
+ }
61
+ if (typeof metadata !== "object" || metadata === null) {
62
+ throw new InvalidMetadataError(target.name, `constructor metadata: expected an object, received ${typeof metadata}`);
63
+ }
64
+ const params = Reflect.get(metadata, "params");
65
+ if (!Array.isArray(params)) {
66
+ throw new InvalidMetadataError(target.name, "constructor metadata: params is not an array");
67
+ }
68
+ for (const [position, param] of params.entries()) {
69
+ if (typeof param !== "object" || param === null) {
70
+ throw new InvalidMetadataError(target.name, `constructor metadata: params[${String(position)}] is not an object`);
71
+ }
72
+ const dependency = Reflect.get(param, "token");
73
+ if (typeof dependency !== "object" && typeof dependency !== "function") {
74
+ throw new InvalidMetadataError(target.name, `constructor metadata: params[${String(position)}].token is not a token or a class`);
75
+ }
76
+ }
77
+ return metadata;
78
+ }
79
+ /**
80
+ * A reader's lifecycle metadata for a class, verified the first time this process asks.
81
+ *
82
+ * @since 0.6.0
83
+ */
84
+ export function verifyLifecycleMetadata(reader, target) {
85
+ return verifyOnce(verifiedLifecycleTargets, reader, target, reader.getLifecycleMetadata(target), (metadata) => {
86
+ if (typeof metadata !== "object" || metadata === null) {
87
+ throw new InvalidMetadataError(target.name, `lifecycle metadata: expected an object, received ${typeof metadata}`);
88
+ }
89
+ for (const phase of ["postConstruct", "preDestroy"]) {
90
+ const methods = Reflect.get(metadata, phase);
91
+ if (methods === undefined) {
92
+ continue;
93
+ }
94
+ if (!Array.isArray(methods)) {
95
+ throw new InvalidMetadataError(target.name, `lifecycle metadata: ${phase} is not an array`);
96
+ }
97
+ for (const [position, name] of methods.entries()) {
98
+ if (typeof name !== "string") {
99
+ throw new InvalidMetadataError(target.name, `lifecycle metadata: ${phase}[${String(position)}] is not a string`);
100
+ }
101
+ }
102
+ }
103
+ });
104
+ }
105
+ /**
106
+ * A reader's accessor metadata for a class, verified the first time this process asks.
107
+ *
108
+ * @since 0.6.0
109
+ */
110
+ export function verifyAccessorMetadata(reader, target) {
111
+ return verifyOnce(verifiedAccessorTargets, reader, target, reader.getAccessorMetadata?.(target), (metadata) => {
112
+ if (!Array.isArray(metadata)) {
113
+ throw new InvalidMetadataError(target.name, "accessor metadata: expected an array");
114
+ }
115
+ for (const [position, entry] of metadata.entries()) {
116
+ if (typeof entry !== "object" || entry === null) {
117
+ throw new InvalidMetadataError(target.name, `accessor metadata: [${String(position)}] is not an object`);
118
+ }
119
+ const key = Reflect.get(entry, "key");
120
+ if (typeof key !== "string" && typeof key !== "symbol") {
121
+ throw new InvalidMetadataError(target.name, `accessor metadata: [${String(position)}].key is not a string or symbol`);
122
+ }
123
+ const descriptor = Reflect.get(entry, "descriptor");
124
+ if (typeof descriptor !== "object" || descriptor === null) {
125
+ throw new InvalidMetadataError(target.name, `accessor metadata: [${String(position)}].descriptor is not an object`);
126
+ }
127
+ const dependency = Reflect.get(descriptor, "token");
128
+ if (typeof dependency !== "object" && typeof dependency !== "function") {
129
+ throw new InvalidMetadataError(target.name, `accessor metadata: [${String(position)}].descriptor.token is not a token or a class`);
130
+ }
131
+ }
132
+ });
133
+ }
@@ -2,7 +2,7 @@
2
2
  * Wraps a foreign {@link MetadataReader} so its answers are verified before anything dereferences
3
3
  * them, and asked for once per class.
4
4
  */
5
- import type { MetadataReader } from "#metadata/metadata-types";
5
+ import type { MetadataReader } from "#metadata/types";
6
6
  /**
7
7
  * The reader a container should hand its resolver: verified and memoized 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, and asked for once per class.
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-reader";
6
+ import { verifyAccessorMetadata, verifyConstructorMetadata, verifyLifecycleMetadata } from "#metadata/verify";
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
  // One wrapper per foreign reader, so every container handed that reader shares its answers.
@@ -6,7 +6,7 @@
6
6
  */
7
7
  import type { Binding } from "#core/binding";
8
8
  import type { BindingRegistry } from "#core/registry";
9
- import type { LifecycleManager } from "#lifecycle/lifecycle-manager";
9
+ import type { LifecycleManager } from "#lifecycle/hooks";
10
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.
@@ -48,7 +48,11 @@ export class ActivationNeedCache {
48
48
  * conservative `true`.
49
49
  */
50
50
  refreshAfterFirstInstantiation(binding, needsActivation) {
51
- if (binding.kind !== "class" || this.#classes.knownPostConstruct(binding.target) !== undefined) {
51
+ // A class binding answers `false` only once its lifecycle metadata is known, so only a conservative
52
+ // `true` can still settle, and a `false` needs no second look at the class.
53
+ if (!needsActivation ||
54
+ binding.kind !== "class" ||
55
+ this.#classes.knownPostConstruct(binding.target) !== undefined) {
52
56
  return needsActivation;
53
57
  }
54
58
  this.#classes.discoverPostConstruct(binding.target);
@@ -3,57 +3,44 @@
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";
6
+ import type { AmbientResolution } from "#ambient-container";
7
7
  import type { Container } from "#container/container";
8
8
  import type { Constructor } from "#core/types";
9
- import type { InjectionDescriptor } from "#injection/descriptor";
10
- import type { ConstructorMetadata, LifecycleMetadata, MetadataReader } from "#metadata/metadata-types";
9
+ import type { ConstructorMetadata, MetadataReader, ParamMetadata } from "#metadata/types";
11
10
  /**
12
- * A reader's constructor metadata for a class, verified the first time this process asks.
11
+ * What one reader has answered about one class so far, each field unknown until first asked.
13
12
  *
14
- * @remarks Metadata cannot change once a class is defined, so re-checking per container would charge
15
- * every fresh container for a fact already established — a per-request child or a cold boot pays
16
- * that repeatedly.
13
+ * @remarks One record per class, so a cold resolve looks the class up once and hands the record down
14
+ * rather than asking a map per fact.
17
15
  *
18
- * @since 0.6.0
16
+ * @since 0.12.0
19
17
  */
20
- export declare function verifyConstructorMetadata(reader: MetadataReader, target: Constructor): ConstructorMetadata | undefined;
21
- /**
22
- * Verifies what a reader claims about a class, since a `MetadataReader` is a public seam.
23
- *
24
- * @remarks Every path that reads constructor metadata comes through here, so a reader that answers
25
- * wrongly is a named error at the class it described rather than a `TypeError` raised later inside a
26
- * resolve. Only what a consumer dereferences is checked: `params` and each entry's `token`.
27
- * `optional`/`multi` degrade to falsy without crashing, and `index` is decorative — dependencies are
28
- * consumed positionally.
29
- *
30
- * @since 0.6.0
31
- */
32
- export declare function assertConstructorMetadata(metadata: unknown, target: Constructor): ConstructorMetadata | undefined;
33
- /**
34
- * A reader's lifecycle metadata for a class, verified the first time this process asks.
35
- *
36
- * @since 0.6.0
37
- */
38
- export declare function verifyLifecycleMetadata(reader: MetadataReader, target: Constructor): LifecycleMetadata | undefined;
39
- /**
40
- * A reader's accessor metadata for a class, verified the first time this process asks.
41
- *
42
- * @since 0.6.0
43
- */
44
- export declare function verifyAccessorMetadata(reader: MetadataReader, target: Constructor): ReadonlyArray<{
45
- readonly key: string | symbol;
46
- readonly descriptor: InjectionDescriptor;
47
- }> | undefined;
18
+ export interface ClassFacts {
19
+ /** The reader's constructor metadata, `null` once it answered that it knows nothing about the class. */
20
+ constructorMetadata: ConstructorMetadata | null | undefined;
21
+ /** Unknown until the first instantiation reads lifecycle metadata; callers read unknown as "it does". */
22
+ hasPostConstruct: boolean | undefined;
23
+ needsActiveContainer: boolean | undefined;
24
+ /** The parameters an instantiation injects, settled by the first one that succeeds. */
25
+ params: ReadonlyArray<ParamMetadata> | undefined;
26
+ }
48
27
  /**
49
28
  * A per-class cache of constructor metadata and the activation facts derived from it.
50
29
  *
30
+ * @remarks A child inheriting its parent's reader takes the parent's facts too, and resolves a class
31
+ * the parent already met without reading its metadata again; a root finds them by reader on its first
32
+ * question.
33
+ *
51
34
  * @since 0.5.0-canary.8
52
35
  */
53
36
  export declare class ClassIntrospector {
54
37
  #private;
55
38
  constructor(reader: MetadataReader, container: Container, inherited: ClassIntrospector | undefined);
39
+ /** The record of what this reader has answered about a class, allocated on the class's first question. */
40
+ facts(target: Constructor): ClassFacts;
56
41
  constructorMetadata(target: Constructor): ConstructorMetadata | undefined;
42
+ /** The constructor metadata of a class whose facts the caller already holds. */
43
+ constructorMetadataOf(target: Constructor, facts: ClassFacts): ConstructorMetadata | undefined;
57
44
  /**
58
45
  * The nearest ancestor's own constructor metadata, for a subclass that declares none of its own.
59
46
  *
@@ -74,5 +61,9 @@ export declare class ClassIntrospector {
74
61
  discoverPostConstruct(target: Constructor): void;
75
62
  /** True when the class has accessor injection, which reads the container during construction. */
76
63
  needsActiveContainer(target: Constructor): boolean;
64
+ /** Whether a class whose facts the caller already holds reads the container during construction. */
65
+ needsActiveContainerOf(target: Constructor, facts: ClassFacts): boolean;
77
66
  instantiate(target: Constructor, deps: Array<unknown>, resolution?: AmbientResolution): unknown;
67
+ /** Builds an instance whose accessor need the caller already knows; an empty `deps` is a call with no arguments. */
68
+ construct(target: Constructor, deps: ReadonlyArray<unknown>, needsActiveContainer: boolean, resolution: AmbientResolution | undefined): unknown;
78
69
  }