@codefast/di 0.10.1 → 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 (88) hide show
  1. package/CHANGELOG.md +376 -0
  2. package/README.md +94 -17
  3. package/dist/{ambient/active-container.d.ts → ambient-container.d.ts} +7 -2
  4. package/dist/{ambient/active-container.js → ambient-container.js} +6 -1
  5. package/dist/container/{binding-builders.d.ts → binding-chain.d.ts} +27 -16
  6. package/dist/container/binding-chain.js +254 -0
  7. package/dist/container/container.d.ts +11 -2
  8. package/dist/container/container.js +293 -73
  9. package/dist/core/binding-builders.d.ts +114 -0
  10. package/dist/core/binding-declaration.d.ts +127 -0
  11. package/dist/core/binding-declaration.js +187 -0
  12. package/dist/core/binding.d.ts +62 -138
  13. package/dist/core/binding.js +54 -17
  14. package/dist/core/module.d.ts +8 -5
  15. package/dist/core/module.js +17 -3
  16. package/dist/core/registry.d.ts +17 -28
  17. package/dist/core/registry.js +92 -133
  18. package/dist/core/state-epoch.d.ts +18 -1
  19. package/dist/core/state-epoch.js +17 -0
  20. package/dist/core/tag.js +1 -1
  21. package/dist/decorators/inject.js +6 -5
  22. package/dist/decorators/injectable.js +4 -2
  23. package/dist/decorators/{lifecycle-decorators.js → lifecycle.js} +9 -3
  24. package/dist/decorators/metadata-record.d.ts +9 -0
  25. package/dist/decorators/metadata-record.js +20 -0
  26. package/dist/{errors/errors.d.ts → errors.d.ts} +84 -9
  27. package/dist/{errors/errors.js → errors.js} +101 -17
  28. package/dist/index.d.ts +14 -10
  29. package/dist/index.js +7 -6
  30. package/dist/injection/{resolve-options.js → dependency-slot.js} +6 -4
  31. package/dist/injection/descriptor.d.ts +1 -1
  32. package/dist/injection/descriptor.js +3 -7
  33. package/dist/introspection/dependency-graph.d.ts +8 -3
  34. package/dist/introspection/dependency-graph.js +48 -26
  35. package/dist/introspection/explanation.d.ts +101 -0
  36. package/dist/introspection/explanation.js +151 -0
  37. package/dist/introspection/graph-adapters/reactflow.js +6 -4
  38. package/dist/introspection/inspector.d.ts +9 -2
  39. package/dist/introspection/inspector.js +30 -28
  40. package/dist/lifecycle/{lifecycle-manager.d.ts → hooks.d.ts} +9 -2
  41. package/dist/lifecycle/{lifecycle-manager.js → hooks.js} +59 -30
  42. package/dist/lifecycle/{scope-manager.d.ts → scopes.d.ts} +3 -5
  43. package/dist/lifecycle/{scope-manager.js → scopes.js} +36 -29
  44. package/dist/metadata/{metadata-reader-token.d.ts → reader-token.d.ts} +1 -1
  45. package/dist/metadata/{symbol-metadata-reader.d.ts → symbol-reader.d.ts} +1 -1
  46. package/dist/metadata/{symbol-metadata-reader.js → symbol-reader.js} +1 -1
  47. package/dist/metadata/{metadata-types.d.ts → types.d.ts} +1 -1
  48. package/dist/metadata/types.js +1 -0
  49. package/dist/metadata/verify.d.ts +43 -0
  50. package/dist/metadata/verify.js +133 -0
  51. package/dist/metadata/{verifying-metadata-reader.d.ts → verifying-reader.d.ts} +5 -4
  52. package/dist/metadata/verifying-reader.js +53 -0
  53. package/dist/resolution/async-fan-out.d.ts +12 -0
  54. package/dist/resolution/async-fan-out.js +26 -0
  55. package/dist/resolution/cache/activation-need.d.ts +1 -2
  56. package/dist/resolution/cache/activation-need.js +16 -19
  57. package/dist/resolution/cache/class-introspector.d.ts +38 -36
  58. package/dist/resolution/cache/class-introspector.js +79 -159
  59. package/dist/resolution/cache/{binding-lookup-cache.d.ts → lookup.d.ts} +7 -9
  60. package/dist/resolution/cache/{binding-lookup-cache.js → lookup.js} +70 -25
  61. package/dist/resolution/context.d.ts +16 -24
  62. package/dist/resolution/context.js +47 -56
  63. package/dist/resolution/path.d.ts +128 -0
  64. package/dist/resolution/path.js +167 -0
  65. package/dist/resolution/plan/{plan-codegen.d.ts → codegen.d.ts} +5 -4
  66. package/dist/resolution/plan/{plan-codegen.js → codegen.js} +58 -32
  67. package/dist/resolution/plan/{instantiation-plan.d.ts → compiler.d.ts} +1 -1
  68. package/dist/resolution/plan/{instantiation-plan.js → compiler.js} +64 -24
  69. package/dist/resolution/resolver.d.ts +11 -12
  70. package/dist/resolution/resolver.js +573 -356
  71. package/dist/resolution/select/{binding-select.d.ts → candidates.d.ts} +30 -0
  72. package/dist/resolution/select/{binding-select.js → candidates.js} +51 -10
  73. package/dist/resolution/select/constraints.js +1 -1
  74. package/package.json +87 -67
  75. package/dist/container/binding-builders.js +0 -305
  76. package/dist/core/binding-scope.d.ts +0 -12
  77. package/dist/core/binding-scope.js +0 -12
  78. package/dist/metadata/verifying-metadata-reader.js +0 -31
  79. package/dist/resolution/path/resolution-path.d.ts +0 -93
  80. package/dist/resolution/path/resolution-path.js +0 -116
  81. /package/dist/{metadata/metadata-types.js → core/binding-builders.js} +0 -0
  82. /package/dist/decorators/{lifecycle-decorators.d.ts → lifecycle.d.ts} +0 -0
  83. /package/dist/injection/{resolve-options.d.ts → dependency-slot.d.ts} +0 -0
  84. /package/dist/{errors → introspection}/diagnostics.d.ts +0 -0
  85. /package/dist/{errors → introspection}/diagnostics.js +0 -0
  86. /package/dist/metadata/{metadata-keys.d.ts → keys.d.ts} +0 -0
  87. /package/dist/metadata/{metadata-keys.js → keys.js} +0 -0
  88. /package/dist/metadata/{metadata-reader-token.js → reader-token.js} +0 -0
@@ -1,14 +1,15 @@
1
1
  /**
2
2
  * Wraps a foreign {@link MetadataReader} so its answers are verified before anything dereferences
3
- * them.
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
- * The reader a container should hand its resolver: verified when it came from outside.
7
+ * The reader a container should hand its resolver: verified and memoized when it came from outside.
8
8
  *
9
9
  * @remarks The decorator reader writes the metadata it later reads, so there is nothing to check and
10
10
  * nothing to pay — a container that supplies no reader of its own is left on the same code path it
11
- * has always taken. A supplied reader is a claim, and only its callers can be charged for checking.
11
+ * has always taken. A supplied reader is a claim, and only its callers can be charged for checking;
12
+ * it is asked about a class once, however many containers read through it.
12
13
  *
13
14
  * @since 0.6.0
14
15
  */
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Wraps a foreign {@link MetadataReader} so its answers are verified before anything dereferences
3
+ * them, and asked for once per class.
4
+ */
5
+ import { defaultMetadataReader } from "#metadata/symbol-reader";
6
+ import { verifyAccessorMetadata, verifyConstructorMetadata, verifyLifecycleMetadata } from "#metadata/verify";
7
+ // Wrapping a wrapper would stack a layer per child container, so each one is remembered.
8
+ const verifyingReaders = new WeakSet();
9
+ // One wrapper per foreign reader, so every container handed that reader shares its answers.
10
+ const wrapperByReader = new WeakMap();
11
+ // `null` records a verified "no metadata", so an absent answer is not asked for again either.
12
+ function memoized(ask) {
13
+ const answers = new WeakMap();
14
+ return (target) => {
15
+ const known = answers.get(target);
16
+ if (known !== undefined) {
17
+ return known === null ? undefined : known;
18
+ }
19
+ // Stored only after `ask` returns, so an answer that fails verification is asked for, and fails, again.
20
+ const answer = ask(target);
21
+ answers.set(target, answer ?? null);
22
+ return answer;
23
+ };
24
+ }
25
+ /**
26
+ * The reader a container should hand its resolver: verified and memoized when it came from outside.
27
+ *
28
+ * @remarks The decorator reader writes the metadata it later reads, so there is nothing to check and
29
+ * nothing to pay — a container that supplies no reader of its own is left on the same code path it
30
+ * has always taken. A supplied reader is a claim, and only its callers can be charged for checking;
31
+ * it is asked about a class once, however many containers read through it.
32
+ *
33
+ * @since 0.6.0
34
+ */
35
+ export function verifyingMetadataReader(reader) {
36
+ if (reader === defaultMetadataReader || verifyingReaders.has(reader)) {
37
+ return reader;
38
+ }
39
+ const existing = wrapperByReader.get(reader);
40
+ if (existing !== undefined) {
41
+ return existing;
42
+ }
43
+ const verifying = {
44
+ getConstructorMetadata: memoized((target) => verifyConstructorMetadata(reader, target)),
45
+ getLifecycleMetadata: memoized((target) => verifyLifecycleMetadata(reader, target)),
46
+ ...(reader.getAccessorMetadata === undefined
47
+ ? {}
48
+ : { getAccessorMetadata: memoized((target) => verifyAccessorMetadata(reader, target)) }),
49
+ };
50
+ verifyingReaders.add(verifying);
51
+ wrapperByReader.set(reader, verifying);
52
+ return verifying;
53
+ }
@@ -0,0 +1,12 @@
1
+ /** Settles the concurrent dependencies of one async level and reports a failure in declaration order. */
2
+ /**
3
+ * Applies `apply` to every dependency's settled value, or rejects with the first failure in declaration order.
4
+ *
5
+ * @remarks Siblings start concurrently, and `Promise.all` alone would report whichever rejection
6
+ * settles first — a fact about microtask depth, not about the graph. The synchronous lanes report
7
+ * the first failing dependency in declaration order, so this does too: the happy path is `Promise.all`,
8
+ * and only a failure waits for every sibling to settle and then picks the earliest one that did not.
9
+ *
10
+ * @since 0.11.0
11
+ */
12
+ export declare function settleInOrder<Result>(pending: ReadonlyArray<unknown>, apply: (values: Array<unknown>) => Result): Promise<Result>;
@@ -0,0 +1,26 @@
1
+ /** Settles the concurrent dependencies of one async level and reports a failure in declaration order. */
2
+ /**
3
+ * Applies `apply` to every dependency's settled value, or rejects with the first failure in declaration order.
4
+ *
5
+ * @remarks Siblings start concurrently, and `Promise.all` alone would report whichever rejection
6
+ * settles first — a fact about microtask depth, not about the graph. The synchronous lanes report
7
+ * the first failing dependency in declaration order, so this does too: the happy path is `Promise.all`,
8
+ * and only a failure waits for every sibling to settle and then picks the earliest one that did not.
9
+ *
10
+ * @since 0.11.0
11
+ */
12
+ export function settleInOrder(pending, apply) {
13
+ return Promise.all(pending).then(apply, () => firstRejectionInOrder(pending));
14
+ }
15
+ function firstRejectionInOrder(pending) {
16
+ return Promise.allSettled(pending).then((results) => {
17
+ for (let index = 0; index < results.length; index += 1) {
18
+ const result = results[index];
19
+ if (result.status === "rejected") {
20
+ throw result.reason;
21
+ }
22
+ }
23
+ // Unreachable: this branch only runs once `Promise.all` has rejected, so a rejection is settled.
24
+ throw new Error("settleInOrder: a rejected fan-out settled with no rejection");
25
+ });
26
+ }
@@ -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.
@@ -17,7 +17,6 @@ export declare class ActivationNeedCache {
17
17
  #private;
18
18
  constructor(lifecycle: LifecycleManager, classes: ClassIntrospector, registry: BindingRegistry);
19
19
  /** Whether an answer the early returns could not give has had to allocate the memo. */
20
- get isMemoBuilt(): boolean;
21
20
  needsActivation<Value>(binding: Binding<Value>): boolean;
22
21
  /**
23
22
  * Settles a class binding's answer once its lifecycle metadata has actually been read, which
@@ -1,13 +1,10 @@
1
+ import { NO_ACTIVATION_STAMP } from "#core/binding";
1
2
  /**
2
3
  * A per-binding cache of whether activation work — hooks or `@postConstruct` — is needed on resolve.
3
4
  *
4
5
  * @since 0.5.0-canary.8
5
6
  */
6
7
  export class ActivationNeedCache {
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;
10
- #version = -1;
11
8
  #lifecycle;
12
9
  #classes;
13
10
  #registry;
@@ -17,9 +14,6 @@ export class ActivationNeedCache {
17
14
  this.#registry = registry;
18
15
  }
19
16
  /** Whether an answer the early returns could not give has had to allocate the memo. */
20
- get isMemoBuilt() {
21
- return this.#needByBindingId !== undefined;
22
- }
23
17
  needsActivation(binding) {
24
18
  // The chain writes a binding's own hook in place with no version anything here can see, so it
25
19
  // is read fresh on every call; the memo covers only container hooks and lifecycle metadata.
@@ -32,19 +26,18 @@ export class ActivationNeedCache {
32
26
  if (lifecycleVersion === 0 && binding.kind !== "class" && binding.kind !== "alias") {
33
27
  return false;
34
28
  }
35
- // The registry version evicts entries for binding ids a rebind has retired.
36
- const version = lifecycleVersion + this.#registry.version;
37
- if (this.#version !== version) {
38
- this.#needByBindingId?.clear();
39
- this.#version = version;
29
+ // The stamp is the version pair the answer was computed under, doubled, plus the answer: a
30
+ // registry or lifecycle mutation moves the version and retires every stamp at once.
31
+ const stampBase = (lifecycleVersion + this.#registry.version) * 2;
32
+ const stamp = binding.activationStamp;
33
+ if (stamp === stampBase) {
34
+ return false;
40
35
  }
41
- const memo = (this.#needByBindingId ??= new Map());
42
- const cached = memo.get(binding.identifier);
43
- if (cached !== undefined) {
44
- return cached;
36
+ if (stamp === stampBase + 1) {
37
+ return true;
45
38
  }
46
39
  const needsActivation = binding.kind === "class" ? this.#classNeedsActivation(binding) : this.#nonClassNeedsActivation(binding);
47
- memo.set(binding.identifier, needsActivation);
40
+ binding.activationStamp = needsActivation ? stampBase + 1 : stampBase;
48
41
  return needsActivation;
49
42
  }
50
43
  /**
@@ -55,11 +48,15 @@ export class ActivationNeedCache {
55
48
  * conservative `true`.
56
49
  */
57
50
  refreshAfterFirstInstantiation(binding, needsActivation) {
58
- 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) {
59
56
  return needsActivation;
60
57
  }
61
58
  this.#classes.discoverPostConstruct(binding.target);
62
- this.#needByBindingId?.delete(binding.identifier);
59
+ binding.activationStamp = NO_ACTIVATION_STAMP;
63
60
  return this.needsActivation(binding);
64
61
  }
65
62
  // Own hooks are answered before the memo, so both computations cover the memoizable rest only.
@@ -3,57 +3,55 @@
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;
44
+ /**
45
+ * The nearest ancestor's own constructor metadata, for a subclass that declares none of its own.
46
+ *
47
+ * @remarks Constructor metadata is never borrowed down the chain, so a subclass with an implicit
48
+ * constructor would be built with zero arguments; this lets the resolver name the base whose
49
+ * declared deps the subclass silently drops.
50
+ */
51
+ inheritedConstructorMetadata(target: Constructor): {
52
+ base: Constructor;
53
+ metadata: ConstructorMetadata;
54
+ } | undefined;
57
55
  /**
58
56
  * Whether the class has a `@postConstruct` hook, or `undefined` until {@link discoverPostConstruct}.
59
57
  *
@@ -63,5 +61,9 @@ export declare class ClassIntrospector {
63
61
  discoverPostConstruct(target: Constructor): void;
64
62
  /** True when the class has accessor injection, which reads the container during construction. */
65
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;
66
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;
67
69
  }
@@ -3,175 +3,87 @@
3
3
  *
4
4
  * @remarks Metadata cannot change once a class is defined, so nothing here needs version stamping.
5
5
  */
6
- import { constructWithAmbientResolution, runWithAmbientResolution } from "#ambient/active-container";
7
- import { InvalidMetadataError } from "#errors/errors";
8
- // Verified pairs, not verified classes: two readers may disagree about the same class, and a reader
9
- // that goes out of scope takes its record with it.
10
- const verifiedTargets = new WeakMap();
11
- const verifiedLifecycleTargets = new WeakMap();
12
- const verifiedAccessorTargets = new WeakMap();
13
- function isVerified(cache, reader, target) {
14
- return cache.get(reader)?.has(target) ?? false;
15
- }
16
- function markVerified(cache, reader, target) {
17
- let verified = cache.get(reader);
18
- if (verified === undefined) {
19
- verified = new WeakSet();
20
- cache.set(reader, verified);
21
- }
22
- verified.add(target);
23
- }
24
- /**
25
- * Runs `validate` against a reader's metadata the first time this process sees the pair, then remembers it.
26
- *
27
- * @remarks An absent metadata answer needs no check; a verified pair is returned untouched, so the
28
- * shape assertion runs once per `(reader, target)` rather than per container.
29
- */
30
- function verifyOnce(cache, reader, target, metadata, validate) {
31
- if (metadata === undefined || isVerified(cache, reader, target)) {
32
- return metadata;
6
+ import { constructWithAmbientResolution, runWithAmbientResolution } from "#ambient-container";
7
+ // A reader answers from the class alone, so its facts are shared by every container reading through it.
8
+ const factsByReader = new WeakMap();
9
+ function factsFor(reader) {
10
+ let byClass = factsByReader.get(reader);
11
+ if (byClass === undefined) {
12
+ byClass = new WeakMap();
13
+ factsByReader.set(reader, byClass);
33
14
  }
34
- validate(metadata);
35
- markVerified(cache, reader, target);
36
- return metadata;
37
- }
38
- /**
39
- * A reader's constructor metadata for a class, verified the first time this process asks.
40
- *
41
- * @remarks Metadata cannot change once a class is defined, so re-checking per container would charge
42
- * every fresh container for a fact already established — a per-request child or a cold boot pays
43
- * that repeatedly.
44
- *
45
- * @since 0.6.0
46
- */
47
- export function verifyConstructorMetadata(reader, target) {
48
- return verifyOnce(verifiedTargets, reader, target, reader.getConstructorMetadata(target), (metadata) => {
49
- assertConstructorMetadata(metadata, target);
50
- });
51
- }
52
- /**
53
- * Verifies what a reader claims about a class, since a `MetadataReader` is a public seam.
54
- *
55
- * @remarks Every path that reads constructor metadata comes through here, so a reader that answers
56
- * wrongly is a named error at the class it described rather than a `TypeError` raised later inside a
57
- * resolve. Only what a consumer dereferences is checked: `params` and each entry's `token`.
58
- * `optional`/`multi` degrade to falsy without crashing, and `index` is decorative — dependencies are
59
- * consumed positionally.
60
- *
61
- * @since 0.6.0
62
- */
63
- export function assertConstructorMetadata(metadata, target) {
64
- if (metadata === undefined) {
65
- return undefined;
66
- }
67
- if (typeof metadata !== "object" || metadata === null) {
68
- throw new InvalidMetadataError(target.name, `constructor metadata: expected an object, received ${typeof metadata}`);
69
- }
70
- const params = Reflect.get(metadata, "params");
71
- if (!Array.isArray(params)) {
72
- throw new InvalidMetadataError(target.name, "constructor metadata: params is not an array");
73
- }
74
- for (const [position, param] of params.entries()) {
75
- if (typeof param !== "object" || param === null) {
76
- throw new InvalidMetadataError(target.name, `constructor metadata: params[${String(position)}] is not an object`);
77
- }
78
- const dependency = Reflect.get(param, "token");
79
- if (typeof dependency !== "object" && typeof dependency !== "function") {
80
- throw new InvalidMetadataError(target.name, `constructor metadata: params[${String(position)}].token is not a token or a class`);
81
- }
82
- }
83
- return metadata;
84
- }
85
- /**
86
- * A reader's lifecycle metadata for a class, verified the first time this process asks.
87
- *
88
- * @since 0.6.0
89
- */
90
- export function verifyLifecycleMetadata(reader, target) {
91
- return verifyOnce(verifiedLifecycleTargets, reader, target, reader.getLifecycleMetadata(target), (metadata) => {
92
- if (typeof metadata !== "object" || metadata === null) {
93
- throw new InvalidMetadataError(target.name, `lifecycle metadata: expected an object, received ${typeof metadata}`);
94
- }
95
- for (const phase of ["postConstruct", "preDestroy"]) {
96
- const methods = Reflect.get(metadata, phase);
97
- if (methods === undefined) {
98
- continue;
99
- }
100
- if (!Array.isArray(methods)) {
101
- throw new InvalidMetadataError(target.name, `lifecycle metadata: ${phase} is not an array`);
102
- }
103
- for (const [position, name] of methods.entries()) {
104
- if (typeof name !== "string") {
105
- throw new InvalidMetadataError(target.name, `lifecycle metadata: ${phase}[${String(position)}] is not a string`);
106
- }
107
- }
108
- }
109
- });
110
- }
111
- /**
112
- * A reader's accessor metadata for a class, verified the first time this process asks.
113
- *
114
- * @since 0.6.0
115
- */
116
- export function verifyAccessorMetadata(reader, target) {
117
- return verifyOnce(verifiedAccessorTargets, reader, target, reader.getAccessorMetadata?.(target), (metadata) => {
118
- if (!Array.isArray(metadata)) {
119
- throw new InvalidMetadataError(target.name, "accessor metadata: expected an array");
120
- }
121
- for (const [position, entry] of metadata.entries()) {
122
- if (typeof entry !== "object" || entry === null) {
123
- throw new InvalidMetadataError(target.name, `accessor metadata: [${String(position)}] is not an object`);
124
- }
125
- const key = Reflect.get(entry, "key");
126
- if (typeof key !== "string" && typeof key !== "symbol") {
127
- throw new InvalidMetadataError(target.name, `accessor metadata: [${String(position)}].key is not a string or symbol`);
128
- }
129
- const descriptor = Reflect.get(entry, "descriptor");
130
- if (typeof descriptor !== "object" || descriptor === null) {
131
- throw new InvalidMetadataError(target.name, `accessor metadata: [${String(position)}].descriptor is not an object`);
132
- }
133
- const dependency = Reflect.get(descriptor, "token");
134
- if (typeof dependency !== "object" && typeof dependency !== "function") {
135
- throw new InvalidMetadataError(target.name, `accessor metadata: [${String(position)}].descriptor.token is not a token or a class`);
136
- }
137
- }
138
- });
139
- }
140
- const cachesByReader = new WeakMap();
141
- function cachesFor(reader) {
142
- let caches = cachesByReader.get(reader);
143
- if (caches === undefined) {
144
- caches = { constructorMetadata: undefined, hasPostConstruct: undefined, needsActiveContainer: undefined };
145
- cachesByReader.set(reader, caches);
146
- }
147
- return caches;
15
+ return byClass;
148
16
  }
149
17
  /**
150
18
  * A per-class cache of constructor metadata and the activation facts derived from it.
151
19
  *
20
+ * @remarks A child inheriting its parent's reader takes the parent's facts too, and resolves a class
21
+ * the parent already met without reading its metadata again; a root finds them by reader on its first
22
+ * question.
23
+ *
152
24
  * @since 0.5.0-canary.8
153
25
  */
154
26
  export class ClassIntrospector {
155
- #caches;
27
+ #byClass;
28
+ // The class asked about last and its record: a cold resolve asks about one class several times in a row.
29
+ #lastTarget;
30
+ #lastFacts;
156
31
  #reader;
157
32
  #container;
158
33
  constructor(reader, container, inherited) {
159
- this.#caches = inherited !== undefined && inherited.#reader === reader ? inherited.#caches : undefined;
34
+ this.#byClass = inherited !== undefined && inherited.#reader === reader ? inherited.#byClass : undefined;
160
35
  this.#reader = reader;
161
36
  this.#container = container;
162
37
  }
163
- #shared() {
164
- return (this.#caches ??= cachesFor(this.#reader));
38
+ /** The record of what this reader has answered about a class, allocated on the class's first question. */
39
+ facts(target) {
40
+ if (target === this.#lastTarget) {
41
+ return this.#lastFacts;
42
+ }
43
+ const byClass = (this.#byClass ??= factsFor(this.#reader));
44
+ let facts = byClass.get(target);
45
+ if (facts === undefined) {
46
+ facts = {
47
+ constructorMetadata: undefined,
48
+ hasPostConstruct: undefined,
49
+ needsActiveContainer: undefined,
50
+ params: undefined,
51
+ };
52
+ byClass.set(target, facts);
53
+ }
54
+ this.#lastTarget = target;
55
+ this.#lastFacts = facts;
56
+ return facts;
165
57
  }
166
58
  constructorMetadata(target) {
167
- const caches = this.#shared();
168
- const cached = caches.constructorMetadata?.get(target);
169
- if (cached !== undefined) {
170
- return cached === null ? undefined : cached;
59
+ return this.constructorMetadataOf(target, this.facts(target));
60
+ }
61
+ /** The constructor metadata of a class whose facts the caller already holds. */
62
+ constructorMetadataOf(target, facts) {
63
+ let metadata = facts.constructorMetadata;
64
+ if (metadata === undefined) {
65
+ metadata = this.#reader.getConstructorMetadata(target) ?? null;
66
+ facts.constructorMetadata = metadata;
67
+ }
68
+ return metadata === null ? undefined : metadata;
69
+ }
70
+ /**
71
+ * The nearest ancestor's own constructor metadata, for a subclass that declares none of its own.
72
+ *
73
+ * @remarks Constructor metadata is never borrowed down the chain, so a subclass with an implicit
74
+ * constructor would be built with zero arguments; this lets the resolver name the base whose
75
+ * declared deps the subclass silently drops.
76
+ */
77
+ inheritedConstructorMetadata(target) {
78
+ let current = Object.getPrototypeOf(target);
79
+ while (typeof current === "function" && current !== Function.prototype) {
80
+ const metadata = this.constructorMetadata(current);
81
+ if (metadata !== undefined) {
82
+ return { base: current, metadata };
83
+ }
84
+ current = Object.getPrototypeOf(current);
171
85
  }
172
- const metadata = this.#reader.getConstructorMetadata(target);
173
- (caches.constructorMetadata ??= new WeakMap()).set(target, metadata ?? null);
174
- return metadata;
86
+ return undefined;
175
87
  }
176
88
  /**
177
89
  * Whether the class has a `@postConstruct` hook, or `undefined` until {@link discoverPostConstruct}.
@@ -179,26 +91,34 @@ export class ClassIntrospector {
179
91
  * @remarks Callers treat unknown as "assume it does", so the first activation settles it.
180
92
  */
181
93
  knownPostConstruct(target) {
182
- return this.#shared().hasPostConstruct?.get(target);
94
+ return this.facts(target).hasPostConstruct;
183
95
  }
184
96
  discoverPostConstruct(target) {
185
97
  const lifecycle = this.#reader.getLifecycleMetadata(target);
186
- (this.#shared().hasPostConstruct ??= new WeakMap()).set(target, lifecycle !== undefined && lifecycle.postConstruct !== undefined && lifecycle.postConstruct.length > 0);
98
+ this.facts(target).hasPostConstruct =
99
+ lifecycle !== undefined && lifecycle.postConstruct !== undefined && lifecycle.postConstruct.length > 0;
187
100
  }
188
101
  /** True when the class has accessor injection, which reads the container during construction. */
189
102
  needsActiveContainer(target) {
190
- const caches = this.#shared();
191
- let needsActiveContainer = caches.needsActiveContainer?.get(target);
103
+ return this.needsActiveContainerOf(target, this.facts(target));
104
+ }
105
+ /** Whether a class whose facts the caller already holds reads the container during construction. */
106
+ needsActiveContainerOf(target, facts) {
107
+ let needsActiveContainer = facts.needsActiveContainer;
192
108
  if (needsActiveContainer === undefined) {
193
109
  needsActiveContainer = (this.#reader.getAccessorMetadata?.(target)?.length ?? 0) > 0;
194
- (caches.needsActiveContainer ??= new WeakMap()).set(target, needsActiveContainer);
110
+ facts.needsActiveContainer = needsActiveContainer;
195
111
  }
196
112
  return needsActiveContainer;
197
113
  }
198
114
  instantiate(target, deps, resolution) {
115
+ return this.construct(target, deps, this.needsActiveContainer(target), resolution);
116
+ }
117
+ /** Builds an instance whose accessor need the caller already knows; an empty `deps` is a call with no arguments. */
118
+ construct(target, deps, needsActiveContainer, resolution) {
199
119
  const invokable = target;
200
- if (!this.needsActiveContainer(target)) {
201
- return new invokable(...deps);
120
+ if (!needsActiveContainer) {
121
+ return deps.length === 0 ? new invokable() : new invokable(...deps);
202
122
  }
203
123
  if (resolution === undefined) {
204
124
  return runWithAmbientResolution(this.#container, undefined, () => new invokable(...deps));
@@ -25,7 +25,8 @@ export interface DefaultLookupEntry<Owner> {
25
25
  *
26
26
  * @remarks `values` is kept only while every member is a hook-free constant or a hook-free singleton
27
27
  * whose instance is cached, and `activationVersion` is the chain's activation version that promise was
28
- * made under.
28
+ * made under. It is settled on the read that repeats the first, which `readBefore` records, so a
29
+ * container read once never builds it.
29
30
  *
30
31
  * @since 0.10.0
31
32
  */
@@ -33,14 +34,8 @@ export interface CollectionEntry {
33
34
  readonly candidates: ReadonlyArray<Binding>;
34
35
  values: ReadonlyArray<unknown> | undefined;
35
36
  activationVersion: number;
37
+ readBefore: boolean;
36
38
  }
37
- /**
38
- * Alias folding gives up past this many hops and defers to the full resolve loop, whose
39
- * Set-based traversal detects genuine cycles exactly rather than by an arbitrary cap.
40
- *
41
- * @since 0.5.0-canary.8
42
- */
43
- export declare const ALIAS_HOP_LIMIT = 32;
44
39
  /**
45
40
  * A version-stamped cache of binding lookups by token and criterion across the container chain.
46
41
  *
@@ -72,6 +67,9 @@ export declare class BindingLookupCache<Owner> {
72
67
  namedTaggedEntry(token: Token<unknown> | Constructor, nameCriterion: BindingTag, tag: BindingTag): DefaultLookupEntry<Owner> | null;
73
68
  /** The memoized root-level collection for a token, or `undefined` once the chain changed since it was stored. */
74
69
  collection(token: Token<unknown> | Constructor): CollectionEntry | undefined;
75
- /** Stores a root-level collection under the chain version the last `collection()` read stamped. */
70
+ /**
71
+ * Stores a root-level collection under the chain version the last `collection()` read stamped, so a
72
+ * predicate that binds during the gather leaves the entry stale rather than current.
73
+ */
76
74
  rememberCollection(token: Token<unknown> | Constructor, entry: CollectionEntry): void;
77
75
  }