@codefast/di 0.11.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/CHANGELOG.md +151 -0
  2. package/README.md +128 -31
  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 -3
  6. package/dist/container/container.js +176 -102
  7. package/dist/core/binding-builders.d.ts +117 -0
  8. package/dist/core/binding-declaration.d.ts +9 -2
  9. package/dist/core/binding-declaration.js +5 -4
  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.d.ts +3 -1
  17. package/dist/decorators/inject.js +5 -5
  18. package/dist/decorators/injectable.js +2 -2
  19. package/dist/decorators/lifecycle.d.ts +20 -0
  20. package/dist/decorators/{lifecycle-decorators.js → lifecycle.js} +7 -3
  21. package/dist/decorators/{decorator-metadata.js → metadata-record.js} +1 -1
  22. package/dist/{errors/errors.d.ts → errors.d.ts} +29 -19
  23. package/dist/{errors/errors.js → errors.js} +43 -25
  24. package/dist/index.d.ts +12 -10
  25. package/dist/index.js +6 -6
  26. package/dist/injection/descriptor.d.ts +1 -1
  27. package/dist/introspection/dependency-graph.d.ts +1 -1
  28. package/dist/introspection/dependency-graph.js +3 -4
  29. package/dist/introspection/explanation.d.ts +101 -0
  30. package/dist/introspection/explanation.js +169 -0
  31. package/dist/introspection/inspector.d.ts +9 -2
  32. package/dist/introspection/inspector.js +25 -20
  33. package/dist/lifecycle/{lifecycle-manager.d.ts → hooks.d.ts} +11 -2
  34. package/dist/lifecycle/{lifecycle-manager.js → hooks.js} +51 -28
  35. package/dist/lifecycle/{scope-manager.d.ts → scopes.d.ts} +23 -6
  36. package/dist/lifecycle/{scope-manager.js → scopes.js} +93 -20
  37. package/dist/metadata/{metadata-keys.d.ts → keys.d.ts} +1 -1
  38. package/dist/metadata/{metadata-keys.js → keys.js} +1 -1
  39. package/dist/metadata/{metadata-reader-token.d.ts → reader-token.d.ts} +1 -1
  40. package/dist/metadata/{symbol-metadata-reader.d.ts → symbol-reader.d.ts} +1 -1
  41. package/dist/metadata/{symbol-metadata-reader.js → symbol-reader.js} +1 -1
  42. package/dist/metadata/{metadata-types.d.ts → types.d.ts} +1 -1
  43. package/dist/metadata/types.js +1 -0
  44. package/dist/metadata/verify.d.ts +43 -0
  45. package/dist/metadata/verify.js +133 -0
  46. package/dist/metadata/{verifying-metadata-reader.d.ts → verifying-reader.d.ts} +1 -1
  47. package/dist/metadata/{verifying-metadata-reader.js → verifying-reader.js} +2 -2
  48. package/dist/resolution/cache/activation-need.d.ts +1 -1
  49. package/dist/resolution/cache/activation-need.js +5 -1
  50. package/dist/resolution/cache/class-introspector.d.ts +27 -36
  51. package/dist/resolution/cache/class-introspector.js +61 -159
  52. package/dist/resolution/cache/{binding-lookup-cache.d.ts → lookup.d.ts} +7 -2
  53. package/dist/resolution/cache/{binding-lookup-cache.js → lookup.js} +40 -8
  54. package/dist/resolution/context.d.ts +1 -1
  55. package/dist/resolution/context.js +1 -1
  56. package/dist/resolution/{path/resolution-path.d.ts → path.d.ts} +12 -2
  57. package/dist/resolution/{path/resolution-path.js → path.js} +23 -3
  58. package/dist/resolution/plan/{plan-codegen.d.ts → codegen.d.ts} +2 -4
  59. package/dist/resolution/plan/{plan-codegen.js → codegen.js} +2 -4
  60. package/dist/resolution/plan/{instantiation-plan.d.ts → compiler.d.ts} +1 -1
  61. package/dist/resolution/plan/{instantiation-plan.js → compiler.js} +4 -4
  62. package/dist/resolution/resolver.d.ts +19 -7
  63. package/dist/resolution/resolver.js +343 -125
  64. package/dist/resolution/select/{binding-select.d.ts → candidates.d.ts} +30 -0
  65. package/dist/resolution/select/{binding-select.js → candidates.js} +51 -10
  66. package/dist/resolution/select/constraints.js +1 -1
  67. package/package.json +80 -72
  68. package/dist/core/binding-scope.d.ts +0 -12
  69. package/dist/core/binding-scope.js +0 -12
  70. package/dist/decorators/lifecycle-decorators.d.ts +0 -14
  71. /package/dist/{ambient/active-container.d.ts → ambient-container.d.ts} +0 -0
  72. /package/dist/{ambient/active-container.js → ambient-container.js} +0 -0
  73. /package/dist/{metadata/metadata-types.js → core/binding-builders.js} +0 -0
  74. /package/dist/decorators/{decorator-metadata.d.ts → metadata-record.d.ts} +0 -0
  75. /package/dist/injection/{resolve-options.d.ts → dependency-slot.d.ts} +0 -0
  76. /package/dist/injection/{resolve-options.js → dependency-slot.js} +0 -0
  77. /package/dist/{errors → introspection}/diagnostics.d.ts +0 -0
  78. /package/dist/{errors → introspection}/diagnostics.js +0 -0
  79. /package/dist/metadata/{metadata-reader-token.js → reader-token.js} +0 -0
@@ -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
  }
@@ -3,175 +3,69 @@
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;
33
- }
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);
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);
146
14
  }
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;
171
67
  }
172
- const metadata = this.#reader.getConstructorMetadata(target);
173
- (caches.constructorMetadata ??= new WeakMap()).set(target, metadata ?? null);
174
- return metadata;
68
+ return metadata === null ? undefined : metadata;
175
69
  }
176
70
  /**
177
71
  * The nearest ancestor's own constructor metadata, for a subclass that declares none of its own.
@@ -197,26 +91,34 @@ export class ClassIntrospector {
197
91
  * @remarks Callers treat unknown as "assume it does", so the first activation settles it.
198
92
  */
199
93
  knownPostConstruct(target) {
200
- return this.#shared().hasPostConstruct?.get(target);
94
+ return this.facts(target).hasPostConstruct;
201
95
  }
202
96
  discoverPostConstruct(target) {
203
97
  const lifecycle = this.#reader.getLifecycleMetadata(target);
204
- (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;
205
100
  }
206
101
  /** True when the class has accessor injection, which reads the container during construction. */
207
102
  needsActiveContainer(target) {
208
- const caches = this.#shared();
209
- 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;
210
108
  if (needsActiveContainer === undefined) {
211
109
  needsActiveContainer = (this.#reader.getAccessorMetadata?.(target)?.length ?? 0) > 0;
212
- (caches.needsActiveContainer ??= new WeakMap()).set(target, needsActiveContainer);
110
+ facts.needsActiveContainer = needsActiveContainer;
213
111
  }
214
112
  return needsActiveContainer;
215
113
  }
216
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) {
217
119
  const invokable = target;
218
- if (!this.needsActiveContainer(target)) {
219
- return new invokable(...deps);
120
+ if (!needsActiveContainer) {
121
+ return deps.length === 0 ? new invokable() : new invokable(...deps);
220
122
  }
221
123
  if (resolution === undefined) {
222
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,6 +34,7 @@ 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
39
  /**
38
40
  * A version-stamped cache of binding lookups by token and criterion across the container chain.
@@ -65,6 +67,9 @@ export declare class BindingLookupCache<Owner> {
65
67
  namedTaggedEntry(token: Token<unknown> | Constructor, nameCriterion: BindingTag, tag: BindingTag): DefaultLookupEntry<Owner> | null;
66
68
  /** The memoized root-level collection for a token, or `undefined` once the chain changed since it was stored. */
67
69
  collection(token: Token<unknown> | Constructor): CollectionEntry | undefined;
68
- /** 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
+ */
69
74
  rememberCollection(token: Token<unknown> | Constructor, entry: CollectionEntry): void;
70
75
  }
@@ -29,9 +29,9 @@ export class BindingLookupCache {
29
29
  #lastTagToken;
30
30
  #lastTag;
31
31
  #lastTaggedEntry = null;
32
- // Root-level collections by token, stamped with the chain version like the two memos above.
32
+ // Root-level collections, stamped with the chain version like the memos above. Deferred into one object,
33
+ // because every per-request child builds this cache and almost none reads a collection at the root.
33
34
  #collections;
34
- #collectionsVersion = -1;
35
35
  #registry;
36
36
  #owner;
37
37
  #parent;
@@ -171,16 +171,48 @@ export class BindingLookupCache {
171
171
  /** The memoized root-level collection for a token, or `undefined` once the chain changed since it was stored. */
172
172
  collection(token) {
173
173
  const version = this.chainVersion();
174
- if (version !== this.#collectionsVersion) {
175
- this.#collections?.clear();
176
- this.#collectionsVersion = version;
174
+ const memo = this.#collections;
175
+ if (memo === undefined) {
176
+ this.#collections = { version, lastToken: undefined, lastEntry: undefined, byToken: undefined };
177
177
  return undefined;
178
178
  }
179
- return this.#collections?.get(token);
179
+ if (version !== memo.version) {
180
+ memo.version = version;
181
+ memo.byToken?.clear();
182
+ // The rebuild that follows can throw, so the entry in front must not outlive the generation.
183
+ memo.lastToken = undefined;
184
+ return undefined;
185
+ }
186
+ if (token === memo.lastToken) {
187
+ return memo.lastEntry;
188
+ }
189
+ const entry = memo.byToken?.get(token);
190
+ if (entry !== undefined) {
191
+ memo.lastToken = token;
192
+ memo.lastEntry = entry;
193
+ }
194
+ return entry;
180
195
  }
181
- /** Stores a root-level collection under the chain version the last `collection()` read stamped. */
196
+ /**
197
+ * Stores a root-level collection under the chain version the last `collection()` read stamped, so a
198
+ * predicate that binds during the gather leaves the entry stale rather than current.
199
+ */
182
200
  rememberCollection(token, entry) {
183
- (this.#collections ??= new Map()).set(token, entry);
201
+ const memo = this.#collections;
202
+ if (memo === undefined) {
203
+ return;
204
+ }
205
+ if (memo.byToken !== undefined) {
206
+ memo.byToken.set(token, entry);
207
+ }
208
+ else if (memo.lastToken !== undefined && memo.lastEntry !== undefined) {
209
+ memo.byToken = new Map([
210
+ [memo.lastToken, memo.lastEntry],
211
+ [token, entry],
212
+ ]);
213
+ }
214
+ memo.lastToken = token;
215
+ memo.lastEntry = entry;
184
216
  }
185
217
  /**
186
218
  * Follows alias hops to the terminal entry, or declines with `null` on a cycle, which the full
@@ -1,7 +1,7 @@
1
1
  /** The `ResolutionContext` a factory is handed, and the callbacks the resolver answers it with. */
2
2
  import type { Token } from "#core/token";
3
3
  import type { ConstraintContext, Constructor, ResolutionFrame, ResolutionContext, ResolveOptions } from "#core/types";
4
- import type { BranchDepth, OwnedBranchStack } from "#resolution/path/resolution-path";
4
+ import type { BranchDepth, OwnedBranchStack } from "#resolution/path";
5
5
  /**
6
6
  * The engine surface a resolution context calls back into to resolve further dependencies.
7
7
  *
@@ -1,4 +1,4 @@
1
- import { bindingsOf, enterSeededPath, leaveSeededPath, UNOWNED_BRANCH } from "#resolution/path/resolution-path";
1
+ import { bindingsOf, enterSeededPath, leaveSeededPath, UNOWNED_BRANCH } from "#resolution/path";
2
2
  /**
3
3
  * The `ResolutionContext` handed to factories and hooks, backed by the engine's callbacks.
4
4
  *
@@ -1,5 +1,6 @@
1
1
  import type { Binding } from "#core/binding";
2
- import type { ResolutionFrame } from "#core/types";
2
+ import type { Token } from "#core/token";
3
+ import type { Constructor, ResolutionFrame } from "#core/types";
3
4
  /**
4
5
  * Creates the resolution-stack frame for one in-flight resolve of a binding.
5
6
  *
@@ -17,11 +18,20 @@ export declare function buildResolutionFrame(binding: Binding, tokenName: string
17
18
  */
18
19
  export declare function bindingsOf(frames: ReadonlyArray<ResolutionFrame>): Array<Binding>;
19
20
  /**
20
- * The token names along a stack plus the frame that closed the cycle — built only to throw.
21
+ * The token names along a stack plus the one ending the path, a cycle's closing frame or a miss — built only to throw.
21
22
  *
22
23
  * @since 0.6.0
23
24
  */
24
25
  export declare function cycleNamesOf(resolutionStack: ReadonlyArray<ResolutionFrame>, closingName: string): Array<string>;
26
+ /**
27
+ * The token names from the outermost request to a lookup that selected nothing, its alias hops included.
28
+ *
29
+ * @remarks `aliasHops` is the set an alias walk keeps, which starts at `requested` — the name the path already
30
+ * ends on.
31
+ *
32
+ * @since 0.13.0
33
+ */
34
+ export declare function missPathOf(resolutionStack: ReadonlyArray<ResolutionFrame>, requested: Token<unknown> | Constructor, aliasHops: ReadonlySet<Token<unknown> | Constructor> | undefined): Array<string>;
25
35
  /**
26
36
  * Marks a level in flight on a synchronous path and pushes its frame, throwing if its binding already is.
27
37
  *
@@ -1,5 +1,6 @@
1
- /** Cycle-detection bookkeeping carried on the resolution stack itself. */
2
- import { CircularDependencyError } from "#errors/errors";
1
+ import { tokenName } from "#core/token";
2
+ /** The resolution path: cycle bookkeeping carried on the stack itself, and the names an error prints. */
3
+ import { CircularDependencyError } from "#errors";
3
4
  /** The key under which a resolver-built frame carries its binding, for marking a seeded path. */
4
5
  const FRAME_BINDING = Symbol("di:frame-binding");
5
6
  /**
@@ -38,7 +39,7 @@ export function bindingsOf(frames) {
38
39
  return bindings;
39
40
  }
40
41
  /**
41
- * The token names along a stack plus the frame that closed the cycle — built only to throw.
42
+ * The token names along a stack plus the one ending the path, a cycle's closing frame or a miss — built only to throw.
42
43
  *
43
44
  * @since 0.6.0
44
45
  */
@@ -50,6 +51,25 @@ export function cycleNamesOf(resolutionStack, closingName) {
50
51
  names[resolutionStack.length] = closingName;
51
52
  return names;
52
53
  }
54
+ /**
55
+ * The token names from the outermost request to a lookup that selected nothing, its alias hops included.
56
+ *
57
+ * @remarks `aliasHops` is the set an alias walk keeps, which starts at `requested` — the name the path already
58
+ * ends on.
59
+ *
60
+ * @since 0.13.0
61
+ */
62
+ export function missPathOf(resolutionStack, requested, aliasHops) {
63
+ const path = cycleNamesOf(resolutionStack, tokenName(requested));
64
+ if (aliasHops !== undefined) {
65
+ for (const hop of aliasHops) {
66
+ if (hop !== requested) {
67
+ path.push(tokenName(hop));
68
+ }
69
+ }
70
+ }
71
+ return path;
72
+ }
53
73
  /**
54
74
  * Marks a level in flight on a synchronous path and pushes its frame, throwing if its binding already is.
55
75
  *
@@ -6,10 +6,8 @@ import type { ConstructorInvocation } from "#core/constructor-type";
6
6
  /**
7
7
  * The number of runs a plan's closure makes before the plan is generated as its own function.
8
8
  *
9
- * @remarks A measured policy, not a machine width, a contract value or bind-time data: generating
10
- * costs some fifty closure runs and the new function runs cold for thirty more, so it repays only
11
- * over runs in the thousands. Below it a plan stays a closure, which is all a cold container or a
12
- * per-request child ever runs.
9
+ * @remarks A measured policy: the generated function costs a compile and a warm-up, so it repays only over
10
+ * many runs of one plan in one container, and a cold container or a per-request child keeps the closure.
13
11
  *
14
12
  * @since 0.10.0
15
13
  */
@@ -3,10 +3,8 @@ import { settleInOrder } from "#resolution/async-fan-out";
3
3
  /**
4
4
  * The number of runs a plan's closure makes before the plan is generated as its own function.
5
5
  *
6
- * @remarks A measured policy, not a machine width, a contract value or bind-time data: generating
7
- * costs some fifty closure runs and the new function runs cold for thirty more, so it repays only
8
- * over runs in the thousands. Below it a plan stays a closure, which is all a cold container or a
9
- * per-request child ever runs.
6
+ * @remarks A measured policy: the generated function costs a compile and a warm-up, so it repays only over
7
+ * many runs of one plan in one container, and a cold container or a per-request child keeps the closure.
10
8
  *
11
9
  * @since 0.10.0
12
10
  */
@@ -8,7 +8,7 @@ import type { Binding } from "#core/binding";
8
8
  import type { ConstructorInvocation } from "#core/constructor-type";
9
9
  import type { Token } from "#core/token";
10
10
  import type { Constructor, ResolutionFrame, ResolveOptions } from "#core/types";
11
- import type { ConstructorMetadata } from "#metadata/metadata-types";
11
+ import type { ConstructorMetadata } from "#metadata/types";
12
12
  /**
13
13
  * Compilation asked to retry later (class lifecycle metadata not discovered yet).
14
14
  *
@@ -1,10 +1,10 @@
1
1
  import { NO_INSTANCE } from "#core/binding";
2
2
  import { tokenName } from "#core/token";
3
- import { AsyncResolutionError } from "#errors/errors";
4
- import { injectionSlotToResolveOptions } from "#injection/resolve-options";
3
+ import { AsyncResolutionError } from "#errors";
4
+ import { injectionSlotToResolveOptions } from "#injection/dependency-slot";
5
5
  import { settleInOrder } from "#resolution/async-fan-out";
6
- import { enterSeededPath, leaveSeededPath } from "#resolution/path/resolution-path";
7
- import { generateAsyncPlan, generatePlan, isPlanCodegenAvailable, PLAN_CODEGEN_THRESHOLD, } from "#resolution/plan/plan-codegen";
6
+ import { enterSeededPath, leaveSeededPath } from "#resolution/path";
7
+ import { generateAsyncPlan, generatePlan, isPlanCodegenAvailable, PLAN_CODEGEN_THRESHOLD, } from "#resolution/plan/codegen";
8
8
  /**
9
9
  * Compilation asked to retry later (class lifecycle metadata not discovered yet).
10
10
  *