@codefast/di 0.10.1 → 0.11.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 (55) hide show
  1. package/CHANGELOG.md +265 -0
  2. package/README.md +69 -6
  3. package/dist/ambient/active-container.d.ts +7 -2
  4. package/dist/ambient/active-container.js +6 -1
  5. package/dist/container/binding-builders.d.ts +19 -1
  6. package/dist/container/binding-builders.js +75 -16
  7. package/dist/container/container.js +224 -47
  8. package/dist/core/binding-declaration.d.ts +120 -0
  9. package/dist/core/binding-declaration.js +186 -0
  10. package/dist/core/binding.d.ts +57 -5
  11. package/dist/core/binding.js +48 -1
  12. package/dist/core/module.d.ts +7 -4
  13. package/dist/core/module.js +17 -3
  14. package/dist/core/registry.d.ts +11 -7
  15. package/dist/core/registry.js +122 -38
  16. package/dist/core/state-epoch.d.ts +18 -1
  17. package/dist/core/state-epoch.js +17 -0
  18. package/dist/core/tag.js +1 -1
  19. package/dist/decorators/decorator-metadata.d.ts +9 -0
  20. package/dist/decorators/decorator-metadata.js +20 -0
  21. package/dist/decorators/inject.js +2 -1
  22. package/dist/decorators/injectable.js +3 -1
  23. package/dist/decorators/lifecycle-decorators.js +8 -2
  24. package/dist/errors/errors.d.ts +77 -3
  25. package/dist/errors/errors.js +93 -10
  26. package/dist/index.d.ts +3 -1
  27. package/dist/index.js +2 -1
  28. package/dist/injection/descriptor.js +3 -7
  29. package/dist/injection/resolve-options.js +6 -4
  30. package/dist/introspection/dependency-graph.d.ts +7 -2
  31. package/dist/introspection/dependency-graph.js +46 -23
  32. package/dist/introspection/graph-adapters/reactflow.js +6 -4
  33. package/dist/introspection/inspector.js +6 -9
  34. package/dist/lifecycle/lifecycle-manager.js +14 -2
  35. package/dist/lifecycle/scope-manager.js +28 -10
  36. package/dist/metadata/verifying-metadata-reader.d.ts +4 -3
  37. package/dist/metadata/verifying-metadata-reader.js +28 -6
  38. package/dist/resolution/async-fan-out.d.ts +12 -0
  39. package/dist/resolution/async-fan-out.js +26 -0
  40. package/dist/resolution/cache/activation-need.d.ts +0 -1
  41. package/dist/resolution/cache/activation-need.js +11 -18
  42. package/dist/resolution/cache/binding-lookup-cache.d.ts +0 -7
  43. package/dist/resolution/cache/binding-lookup-cache.js +30 -17
  44. package/dist/resolution/cache/class-introspector.d.ts +11 -0
  45. package/dist/resolution/cache/class-introspector.js +18 -0
  46. package/dist/resolution/context.d.ts +15 -23
  47. package/dist/resolution/context.js +47 -56
  48. package/dist/resolution/path/resolution-path.d.ts +48 -13
  49. package/dist/resolution/path/resolution-path.js +89 -38
  50. package/dist/resolution/plan/instantiation-plan.js +61 -21
  51. package/dist/resolution/plan/plan-codegen.d.ts +7 -4
  52. package/dist/resolution/plan/plan-codegen.js +60 -32
  53. package/dist/resolution/resolver.d.ts +4 -5
  54. package/dist/resolution/resolver.js +288 -258
  55. package/package.json +14 -2
@@ -1,3 +1,4 @@
1
+ import { stringifyTagValue } from "#core/binding";
1
2
  import { effectiveBindingScope } from "#core/binding-scope";
2
3
  import { slotName } from "#core/tag";
3
4
  import { tokenName } from "#core/token";
@@ -41,18 +42,31 @@ function edgeLabel(ref, index) {
41
42
  const criterion = name !== undefined
42
43
  ? `name:${name}`
43
44
  : firstPlainTag !== undefined
44
- ? `tag:${firstPlainTag.key.name}=${String(firstPlainTag.value)}`
45
+ ? `tag:${firstPlainTag.key.name}=${stringifyTagValue(firstPlainTag.value)}`
45
46
  : `[${index}]`;
46
47
  return ref.optional ? `${criterion} optional` : criterion;
47
48
  }
48
- function bindingLookup(sourceRegistry, fallbackRegistry) {
49
- return (token) => {
50
- const own = sourceRegistry.getAll(token);
51
- if (own.length > 0 || fallbackRegistry === undefined) {
52
- return own;
49
+ /**
50
+ * The bindings a dependency reaches, found the way resolution finds them.
51
+ *
52
+ * @remarks A single dependency stops at the nearest registry holding a binding its slot matches, and
53
+ * a collection gathers every registry's matches — `resolve` and `resolveAll` respectively.
54
+ */
55
+ function dependencyTargets(chain, ref) {
56
+ if (ref.multi) {
57
+ const gathered = [];
58
+ for (const registry of chain) {
59
+ gathered.push(...matchingTargets(registry.getAll(ref.token), ref));
60
+ }
61
+ return gathered;
62
+ }
63
+ for (const registry of chain) {
64
+ const targets = matchingTargets(registry.getAll(ref.token), ref);
65
+ if (targets.length > 0) {
66
+ return targets;
53
67
  }
54
- return fallbackRegistry.getAll(token);
55
- };
68
+ }
69
+ return [];
56
70
  }
57
71
  /** The placeholder node an optional-but-unbound dependency points at, minted once per token. */
58
72
  function unboundNodeIdFor(accumulator, dependency) {
@@ -73,8 +87,8 @@ function unboundNodeIdFor(accumulator, dependency) {
73
87
  });
74
88
  return id;
75
89
  }
76
- function addDependencyEdges(accumulator, from, ref, index, lookup) {
77
- const targets = matchingTargets(lookup(ref.token), ref);
90
+ function addDependencyEdges(accumulator, from, ref, index, chain) {
91
+ const targets = dependencyTargets(chain, ref);
78
92
  const label = edgeLabel(ref, index);
79
93
  if (targets.length === 0) {
80
94
  // A required-but-unbound dependency is validate()'s story, not the graph's.
@@ -108,25 +122,25 @@ function addDependencyEdges(accumulator, from, ref, index, lookup) {
108
122
  }
109
123
  }
110
124
  /** What one binding declares up front — a class's params, a factory's descriptors, an alias's target. */
111
- function addBindingEdges(accumulator, binding, metadataReader, lookup) {
125
+ function addBindingEdges(accumulator, binding, metadataReader, chain) {
112
126
  if (binding.kind === "class") {
113
127
  const meta = metadataReader.getConstructorMetadata(binding.target);
114
128
  if (meta !== undefined) {
115
129
  for (const [index, param] of meta.params.entries()) {
116
- addDependencyEdges(accumulator, String(binding.identifier), param, index, lookup);
130
+ addDependencyEdges(accumulator, String(binding.identifier), param, index, chain);
117
131
  }
118
132
  }
119
133
  return;
120
134
  }
121
135
  if (binding.kind === "resolved" || binding.kind === "resolved-async") {
122
136
  for (const [index, dependency] of binding.deps.entries()) {
123
- addDependencyEdges(accumulator, String(binding.identifier), dependency, index, lookup);
137
+ addDependencyEdges(accumulator, String(binding.identifier), dependency, index, chain);
124
138
  }
125
139
  return;
126
140
  }
127
141
  if (binding.kind === "alias") {
128
142
  const aliasRef = { token: binding.target, optional: false, multi: false };
129
- for (const target of matchingTargets(lookup(binding.target), aliasRef)) {
143
+ for (const target of dependencyTargets(chain, aliasRef)) {
130
144
  accumulator.edges.push({
131
145
  from: String(binding.identifier),
132
146
  to: String(target.identifier),
@@ -136,9 +150,9 @@ function addBindingEdges(accumulator, binding, metadataReader, lookup) {
136
150
  }
137
151
  }
138
152
  }
139
- function addRegistryBindings(accumulator, sourceRegistry, metadataReader, fromParent, fallbackRegistry) {
140
- const lookup = bindingLookup(sourceRegistry, fallbackRegistry);
141
- for (const binding of sourceRegistry.allBindings()) {
153
+ /** Adds the nodes of `chain[0]`, the registry the chain starts at, and the edges each of its bindings declares. */
154
+ function addRegistryBindings(accumulator, chain, metadataReader, fromParent) {
155
+ for (const binding of chain[0].allBindings()) {
142
156
  accumulator.nodes.push({
143
157
  id: String(binding.identifier),
144
158
  tokenName: tokenName(binding.token),
@@ -147,20 +161,29 @@ function addRegistryBindings(accumulator, sourceRegistry, metadataReader, fromPa
147
161
  scope: effectiveBindingScope(binding),
148
162
  fromParent,
149
163
  });
150
- addBindingEdges(accumulator, binding, metadataReader, lookup);
164
+ addBindingEdges(accumulator, binding, metadataReader, chain);
151
165
  }
152
166
  }
153
167
  /**
154
- * Builds the JSON dependency graph of a registry's bindings, optionally including the parent's.
168
+ * Builds the JSON dependency graph of a registry's bindings, optionally including its ancestors'.
169
+ *
170
+ * @param registry - The registry whose bindings the graph is for.
171
+ * @param metadataReader - The reader class dependencies are read through.
172
+ * @param options - Whether the ancestors' bindings join the graph.
173
+ * @param ancestorRegistries - The ancestor containers' registries, nearest first.
155
174
  *
156
175
  * @since 0.3.16-canary.0
157
176
  */
158
- export function buildDependencyGraph(registry, metadataReader, options, parentRegistry) {
177
+ export function buildDependencyGraph(registry, metadataReader, options, ancestorRegistries = []) {
159
178
  const accumulator = { nodes: [], edges: [], unboundNodeIds: new Map() };
160
179
  const includesParent = options?.includeParent === true;
161
- addRegistryBindings(accumulator, registry, metadataReader, false, includesParent ? parentRegistry : undefined);
162
- if (includesParent && parentRegistry !== undefined) {
163
- addRegistryBindings(accumulator, parentRegistry, metadataReader, true);
180
+ if (!includesParent) {
181
+ addRegistryBindings(accumulator, [registry], metadataReader, false);
182
+ return { nodes: accumulator.nodes, edges: accumulator.edges, includesParent };
183
+ }
184
+ const chain = [registry, ...ancestorRegistries];
185
+ for (let depth = 0; depth < chain.length; depth += 1) {
186
+ addRegistryBindings(accumulator, chain.slice(depth), metadataReader, depth > 0);
164
187
  }
165
188
  return { nodes: accumulator.nodes, edges: accumulator.edges, includesParent };
166
189
  }
@@ -1,9 +1,9 @@
1
1
  /**
2
- * Initial grid layout: React Flow expects concrete positions; viewers re-layout anyway.
2
+ * Initial grid cell, in the pixels React Flow positions in: a starting layout viewers re-lay out,
3
+ * fixed by no contract.
3
4
  *
4
5
  * @since 0.3.16-canary.0
5
6
  */
6
- const GRID_COLUMN_COUNT = 5;
7
7
  const GRID_CELL_WIDTH_PX = 200;
8
8
  const GRID_CELL_HEIGHT_PX = 100;
9
9
  /**
@@ -12,6 +12,8 @@ const GRID_CELL_HEIGHT_PX = 100;
12
12
  * @since 0.5.0-canary.7
13
13
  */
14
14
  export function toReactFlowGraph(graph) {
15
+ // A square grid over the graph: as many columns as rows, derived from the node count.
16
+ const columnCount = Math.max(1, Math.ceil(Math.sqrt(graph.nodes.length)));
15
17
  const nodes = graph.nodes.map((node, idx) => ({
16
18
  id: node.id,
17
19
  data: {
@@ -22,8 +24,8 @@ export function toReactFlowGraph(graph) {
22
24
  fromParent: node.fromParent,
23
25
  },
24
26
  position: {
25
- x: (idx % GRID_COLUMN_COUNT) * GRID_CELL_WIDTH_PX,
26
- y: Math.floor(idx / GRID_COLUMN_COUNT) * GRID_CELL_HEIGHT_PX,
27
+ x: (idx % columnCount) * GRID_CELL_WIDTH_PX,
28
+ y: Math.floor(idx / columnCount) * GRID_CELL_HEIGHT_PX,
27
29
  },
28
30
  }));
29
31
  const edges = graph.edges.map((edge, idx) => ({
@@ -1,5 +1,6 @@
1
1
  import { effectiveBindingScope } from "#core/binding-scope";
2
2
  import { tokenName } from "#core/token";
3
+ import { DefaultConstraintContext } from "#resolution/context";
3
4
  import { selectAllBindings } from "#resolution/select/binding-select";
4
5
  // ── Inspector ────────────────────────────────────────────────────────────────────────────────────────────────────────
5
6
  /**
@@ -38,17 +39,13 @@ export class Inspector {
38
39
  }
39
40
  const bindings = this.#registry.getAll(token);
40
41
  // An existence probe answers ambiguity with `true` — several matches still exist; only
41
- // resolution has to pick one.
42
- return bindings.length > 0 && selectAllBindings(bindings, options, this.#makeConstraintContext(options)).length > 0;
42
+ // resolution has to pick one. A default-slot alias answers any criteria by forwarding them.
43
+ return (bindings.length > 0 &&
44
+ (selectAllBindings(bindings, options, this.#makeConstraintContext(options)).length > 0 ||
45
+ this.#registry.getDefaultSlotBinding(token)?.kind === "alias"));
43
46
  }
44
47
  #makeConstraintContext(options) {
45
- return {
46
- resolutionPath: [],
47
- resolutionStack: [],
48
- parent: undefined,
49
- ancestors: [],
50
- currentResolveOptions: options,
51
- };
48
+ return new DefaultConstraintContext([], options);
52
49
  }
53
50
  #toSnapshot(binding) {
54
51
  // Aliased, not copied: slot tags are frozen where they are built, so a caller's write throws
@@ -97,7 +97,11 @@ export class LifecycleManager {
97
97
  let activatedInstance = instance;
98
98
  // 1. @postConstruct() — must be sync (instance fully constructed per TC39 order)
99
99
  for (const methodName of lifecycleMethods(binding, metadataReader, "postConstruct")) {
100
- if (callHook(activatedInstance, methodName) instanceof Promise) {
100
+ const hookResult = callHook(activatedInstance, methodName);
101
+ if (hookResult instanceof Promise) {
102
+ // The hook has already run; adopt its rejection so a failing async hook cannot become an
103
+ // unhandled rejection that ends the process, then report the sync-lane violation.
104
+ void hookResult.catch(() => { });
101
105
  throw new AsyncActivationError(tokenName(binding.token), "postConstruct", methodName);
102
106
  }
103
107
  }
@@ -105,6 +109,7 @@ export class LifecycleManager {
105
109
  if (binding.kind !== "alias" && binding.activationHook !== undefined) {
106
110
  const activationResult = binding.activationHook(resolutionContext, activatedInstance);
107
111
  if (activationResult instanceof Promise) {
112
+ void activationResult.catch(() => { });
108
113
  throw new AsyncActivationError(tokenName(binding.token), "onActivation");
109
114
  }
110
115
  activatedInstance = activationResult;
@@ -116,6 +121,7 @@ export class LifecycleManager {
116
121
  for (const hook of containerHooks) {
117
122
  const activationResult = hook(resolutionContext, activatedInstance);
118
123
  if (activationResult instanceof Promise) {
124
+ void activationResult.catch(() => { });
119
125
  throw new AsyncActivationError(tokenDisplayName, "onActivation");
120
126
  }
121
127
  activatedInstance = activationResult;
@@ -159,6 +165,9 @@ export class LifecycleManager {
159
165
  for (const hook of containerHooks) {
160
166
  const hookResult = hook(instance);
161
167
  if (hookResult instanceof Promise) {
168
+ // The hook has already run; adopt its rejection so a failing async hook cannot become an
169
+ // unhandled rejection that ends the process, then report the sync-lane violation.
170
+ void hookResult.catch(() => { });
162
171
  throw new AsyncDeactivationError(tokenDisplayName);
163
172
  }
164
173
  }
@@ -167,12 +176,15 @@ export class LifecycleManager {
167
176
  if (binding.kind !== "alias" && binding.deactivationHook !== undefined) {
168
177
  const hookResult = binding.deactivationHook(instance);
169
178
  if (hookResult instanceof Promise) {
179
+ void hookResult.catch(() => { });
170
180
  throw new AsyncDeactivationError(tokenDisplayName);
171
181
  }
172
182
  }
173
183
  // 3. @preDestroy()
174
184
  for (const methodName of lifecycleMethods(binding, metadataReader, "preDestroy")) {
175
- if (callHook(instance, methodName) instanceof Promise) {
185
+ const hookResult = callHook(instance, methodName);
186
+ if (hookResult instanceof Promise) {
187
+ void hookResult.catch(() => { });
176
188
  throw new AsyncDeactivationError(tokenDisplayName);
177
189
  }
178
190
  }
@@ -39,22 +39,39 @@ export class ScopeManager {
39
39
  }
40
40
  binding.instance = instance;
41
41
  }
42
+ // A removal only clears the instance; the list keeps its entry until a read compacts it, so a
43
+ // teardown of a hundred singletons is a hundred field writes, not a hundred splices.
44
+ #compacted = true;
42
45
  /** Every binding in this container holding a cached singleton. */
43
46
  cachedSingletons() {
44
- return this.#singletonBindings ?? EMPTY_BINDINGS;
47
+ const tracked = this.#singletonBindings;
48
+ if (tracked === undefined) {
49
+ return EMPTY_BINDINGS;
50
+ }
51
+ if (!this.#compacted) {
52
+ // Latest materialization wins the position: walk from the end keeping each live binding once.
53
+ const seen = new Set();
54
+ const live = [];
55
+ for (let index = tracked.length - 1; index >= 0; index -= 1) {
56
+ const binding = tracked[index];
57
+ if (binding.instance !== NO_INSTANCE && !seen.has(binding)) {
58
+ seen.add(binding);
59
+ live.push(binding);
60
+ }
61
+ }
62
+ live.reverse();
63
+ this.#singletonBindings = live;
64
+ this.#compacted = true;
65
+ return live;
66
+ }
67
+ return tracked;
45
68
  }
46
69
  deleteSingleton(binding) {
47
70
  if (binding.instance === NO_INSTANCE) {
48
71
  return false;
49
72
  }
50
73
  binding.instance = NO_INSTANCE;
51
- const tracked = this.#singletonBindings;
52
- if (tracked !== undefined) {
53
- const index = tracked.indexOf(binding);
54
- if (index !== -1) {
55
- tracked.splice(index, 1);
56
- }
57
- }
74
+ this.#compacted = false;
58
75
  return true;
59
76
  }
60
77
  /** Swaps a re-slotted binding's tracked entry, so teardown pairs the instance with the live object. */
@@ -112,10 +129,11 @@ export class ScopeManager {
112
129
  clearAll() {
113
130
  const tracked = this.#singletonBindings;
114
131
  if (tracked !== undefined) {
115
- for (const binding of tracked) {
116
- binding.instance = NO_INSTANCE;
132
+ for (let index = 0; index < tracked.length; index += 1) {
133
+ tracked[index].instance = NO_INSTANCE;
117
134
  }
118
135
  tracked.length = 0;
136
+ this.#compacted = true;
119
137
  }
120
138
  this.#inflight?.clear();
121
139
  this.#scoped?.clear();
@@ -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
5
  import type { MetadataReader } from "#metadata/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
  */
@@ -1,17 +1,34 @@
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
5
  import { defaultMetadataReader } from "#metadata/symbol-metadata-reader";
6
6
  import { verifyAccessorMetadata, verifyConstructorMetadata, verifyLifecycleMetadata, } from "#resolution/cache/class-introspector";
7
7
  // Wrapping a wrapper would stack a layer per child container, so each one is remembered.
8
8
  const verifyingReaders = new WeakSet();
9
+ // 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
+ }
9
25
  /**
10
- * The reader a container should hand its resolver: verified when it came from outside.
26
+ * The reader a container should hand its resolver: verified and memoized when it came from outside.
11
27
  *
12
28
  * @remarks The decorator reader writes the metadata it later reads, so there is nothing to check and
13
29
  * nothing to pay — a container that supplies no reader of its own is left on the same code path it
14
- * has always taken. A supplied reader is a claim, and only its callers can be charged for checking.
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.
15
32
  *
16
33
  * @since 0.6.0
17
34
  */
@@ -19,13 +36,18 @@ export function verifyingMetadataReader(reader) {
19
36
  if (reader === defaultMetadataReader || verifyingReaders.has(reader)) {
20
37
  return reader;
21
38
  }
39
+ const existing = wrapperByReader.get(reader);
40
+ if (existing !== undefined) {
41
+ return existing;
42
+ }
22
43
  const verifying = {
23
- getConstructorMetadata: (target) => verifyConstructorMetadata(reader, target),
24
- getLifecycleMetadata: (target) => verifyLifecycleMetadata(reader, target),
44
+ getConstructorMetadata: memoized((target) => verifyConstructorMetadata(reader, target)),
45
+ getLifecycleMetadata: memoized((target) => verifyLifecycleMetadata(reader, target)),
25
46
  ...(reader.getAccessorMetadata === undefined
26
47
  ? {}
27
- : { getAccessorMetadata: (target) => verifyAccessorMetadata(reader, target) }),
48
+ : { getAccessorMetadata: memoized((target) => verifyAccessorMetadata(reader, target)) }),
28
49
  };
29
50
  verifyingReaders.add(verifying);
51
+ wrapperByReader.set(reader, verifying);
30
52
  return verifying;
31
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
+ }
@@ -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
  /**
@@ -59,7 +52,7 @@ export class ActivationNeedCache {
59
52
  return needsActivation;
60
53
  }
61
54
  this.#classes.discoverPostConstruct(binding.target);
62
- this.#needByBindingId?.delete(binding.identifier);
55
+ binding.activationStamp = NO_ACTIVATION_STAMP;
63
56
  return this.needsActivation(binding);
64
57
  }
65
58
  // Own hooks are answered before the memo, so both computations cover the memoizable rest only.
@@ -34,13 +34,6 @@ export interface CollectionEntry {
34
34
  values: ReadonlyArray<unknown> | undefined;
35
35
  activationVersion: number;
36
36
  }
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
37
  /**
45
38
  * A version-stamped cache of binding lookups by token and criterion across the container chain.
46
39
  *
@@ -1,12 +1,5 @@
1
1
  import { getOrInsertComputed } from "#core/map-upsert";
2
2
  import { stateEpoch } from "#core/state-epoch";
3
- /**
4
- * Alias folding gives up past this many hops and defers to the full resolve loop, whose
5
- * Set-based traversal detects genuine cycles exactly rather than by an arbitrary cap.
6
- *
7
- * @since 0.5.0-canary.8
8
- */
9
- export const ALIAS_HOP_LIMIT = 32;
10
3
  const newTagToEntryMap = () => new Map();
11
4
  const newNameToTagMap = () => new Map();
12
5
  /**
@@ -76,15 +69,19 @@ export class BindingLookupCache {
76
69
  }
77
70
  /** `null` when the token's shape needs the full selection path. */
78
71
  defaultEntry(token) {
72
+ // The repeat hit is the whole method, small enough for a hot caller to inline; the fill is the miss.
73
+ if (token === this.#lastToken && this.chainVersion() === this.#version) {
74
+ return this.#lastEntry;
75
+ }
76
+ return this.#defaultEntryMiss(token);
77
+ }
78
+ #defaultEntryMiss(token) {
79
79
  const version = this.chainVersion();
80
80
  if (version !== this.#version) {
81
81
  this.#byToken?.clear();
82
82
  this.#version = version;
83
83
  this.#lastToken = undefined;
84
84
  }
85
- else if (token === this.#lastToken) {
86
- return this.#lastEntry;
87
- }
88
85
  let entry;
89
86
  if (this.#lastToken === undefined) {
90
87
  // First token this cache generation sees: answer from the walk and defer the map entirely.
@@ -159,7 +156,12 @@ export class BindingLookupCache {
159
156
  #findPairInChain(token, nameCriterion, tag) {
160
157
  const found = this.#registry.getPairTagged(token, nameCriterion, tag);
161
158
  if (found !== undefined) {
162
- return found.predicate !== undefined || found.kind === "alias" ? null : { binding: found, owner: this.#owner };
159
+ // A predicate candidate at this level can win the more-specific rule's first step, which this
160
+ // lane cannot weigh; an alias carries options through the full path. Either declines to selection.
161
+ if (found.kind === "alias" || this.#registry.hasPredicateCandidate(token)) {
162
+ return null;
163
+ }
164
+ return { binding: found, owner: this.#owner };
163
165
  }
164
166
  if (this.#registry.has(token)) {
165
167
  return null;
@@ -180,19 +182,30 @@ export class BindingLookupCache {
180
182
  rememberCollection(token, entry) {
181
183
  (this.#collections ??= new Map()).set(token, entry);
182
184
  }
185
+ /**
186
+ * Follows alias hops to the terminal entry, or declines with `null` on a cycle, which the full
187
+ * resolve loop reports.
188
+ *
189
+ * @remarks The origin and the current token are compared by hand and the set holds only what lies
190
+ * between them, so the common one-hop alias allocates nothing and any chain is folded exactly.
191
+ */
183
192
  #foldAliases(token) {
184
193
  let current = token;
185
- for (let hop = 0; hop < ALIAS_HOP_LIMIT; hop += 1) {
194
+ let visited;
195
+ for (;;) {
186
196
  const entry = this.#findDefaultInChain(current);
187
- if (entry === null) {
197
+ if (entry === null || entry.binding.kind !== "alias") {
198
+ return entry;
199
+ }
200
+ const next = entry.binding.target;
201
+ if (next === token || next === current || visited?.has(next) === true) {
188
202
  return null;
189
203
  }
190
- if (entry.binding.kind !== "alias") {
191
- return entry;
204
+ if (current !== token) {
205
+ (visited ??= new Set()).add(current);
192
206
  }
193
- current = entry.binding.target;
207
+ current = next;
194
208
  }
195
- return null;
196
209
  }
197
210
  #findDefaultInChain(token) {
198
211
  const fast = this.#registry.getFastDefault(token);
@@ -54,6 +54,17 @@ export declare class ClassIntrospector {
54
54
  #private;
55
55
  constructor(reader: MetadataReader, container: Container, inherited: ClassIntrospector | undefined);
56
56
  constructorMetadata(target: Constructor): ConstructorMetadata | undefined;
57
+ /**
58
+ * The nearest ancestor's own constructor metadata, for a subclass that declares none of its own.
59
+ *
60
+ * @remarks Constructor metadata is never borrowed down the chain, so a subclass with an implicit
61
+ * constructor would be built with zero arguments; this lets the resolver name the base whose
62
+ * declared deps the subclass silently drops.
63
+ */
64
+ inheritedConstructorMetadata(target: Constructor): {
65
+ base: Constructor;
66
+ metadata: ConstructorMetadata;
67
+ } | undefined;
57
68
  /**
58
69
  * Whether the class has a `@postConstruct` hook, or `undefined` until {@link discoverPostConstruct}.
59
70
  *
@@ -173,6 +173,24 @@ export class ClassIntrospector {
173
173
  (caches.constructorMetadata ??= new WeakMap()).set(target, metadata ?? null);
174
174
  return metadata;
175
175
  }
176
+ /**
177
+ * The nearest ancestor's own constructor metadata, for a subclass that declares none of its own.
178
+ *
179
+ * @remarks Constructor metadata is never borrowed down the chain, so a subclass with an implicit
180
+ * constructor would be built with zero arguments; this lets the resolver name the base whose
181
+ * declared deps the subclass silently drops.
182
+ */
183
+ inheritedConstructorMetadata(target) {
184
+ let current = Object.getPrototypeOf(target);
185
+ while (typeof current === "function" && current !== Function.prototype) {
186
+ const metadata = this.constructorMetadata(current);
187
+ if (metadata !== undefined) {
188
+ return { base: current, metadata };
189
+ }
190
+ current = Object.getPrototypeOf(current);
191
+ }
192
+ return undefined;
193
+ }
176
194
  /**
177
195
  * Whether the class has a `@postConstruct` hook, or `undefined` until {@link discoverPostConstruct}.
178
196
  *