@codefast/di 0.10.0 → 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 (81) hide show
  1. package/CHANGELOG.md +293 -0
  2. package/README.md +71 -7
  3. package/dist/ambient/active-container.d.ts +13 -11
  4. package/dist/ambient/active-container.js +8 -6
  5. package/dist/container/binding-builders.d.ts +25 -7
  6. package/dist/container/binding-builders.js +110 -42
  7. package/dist/container/container.d.ts +8 -8
  8. package/dist/container/container.js +244 -67
  9. package/dist/core/binding-declaration.d.ts +120 -0
  10. package/dist/core/binding-declaration.js +186 -0
  11. package/dist/core/binding-scope.d.ts +2 -2
  12. package/dist/core/binding.d.ts +61 -9
  13. package/dist/core/binding.js +49 -2
  14. package/dist/core/constraint-requirement.d.ts +1 -1
  15. package/dist/core/module.d.ts +10 -7
  16. package/dist/core/module.js +17 -3
  17. package/dist/core/registry.d.ts +17 -13
  18. package/dist/core/registry.js +155 -59
  19. package/dist/core/state-epoch.d.ts +18 -1
  20. package/dist/core/state-epoch.js +17 -0
  21. package/dist/core/tag.js +1 -1
  22. package/dist/core/token.d.ts +1 -1
  23. package/dist/core/types.d.ts +5 -5
  24. package/dist/decorators/decorator-metadata.d.ts +9 -0
  25. package/dist/decorators/decorator-metadata.js +20 -0
  26. package/dist/decorators/inject.d.ts +3 -3
  27. package/dist/decorators/inject.js +7 -6
  28. package/dist/decorators/injectable.d.ts +2 -2
  29. package/dist/decorators/injectable.js +5 -3
  30. package/dist/decorators/lifecycle-decorators.js +9 -3
  31. package/dist/errors/errors.d.ts +85 -14
  32. package/dist/errors/errors.js +100 -20
  33. package/dist/index.d.ts +37 -35
  34. package/dist/index.js +20 -19
  35. package/dist/injection/descriptor.d.ts +3 -3
  36. package/dist/injection/descriptor.js +3 -7
  37. package/dist/injection/resolve-options.d.ts +3 -3
  38. package/dist/injection/resolve-options.js +7 -5
  39. package/dist/introspection/dependency-graph.d.ts +10 -5
  40. package/dist/introspection/dependency-graph.js +51 -28
  41. package/dist/introspection/graph-adapters/cytoscape.d.ts +1 -1
  42. package/dist/introspection/graph-adapters/dot.d.ts +1 -1
  43. package/dist/introspection/graph-adapters/mermaid.d.ts +1 -1
  44. package/dist/introspection/graph-adapters/reactflow.d.ts +1 -1
  45. package/dist/introspection/graph-adapters/reactflow.js +6 -4
  46. package/dist/introspection/inspector.d.ts +4 -4
  47. package/dist/introspection/inspector.js +9 -12
  48. package/dist/lifecycle/lifecycle-manager.d.ts +4 -4
  49. package/dist/lifecycle/lifecycle-manager.js +18 -6
  50. package/dist/lifecycle/scope-manager.d.ts +2 -2
  51. package/dist/lifecycle/scope-manager.js +31 -13
  52. package/dist/metadata/metadata-reader-token.d.ts +2 -2
  53. package/dist/metadata/metadata-reader-token.js +1 -1
  54. package/dist/metadata/metadata-types.d.ts +3 -3
  55. package/dist/metadata/symbol-metadata-reader.d.ts +3 -3
  56. package/dist/metadata/symbol-metadata-reader.js +1 -1
  57. package/dist/metadata/verifying-metadata-reader.d.ts +5 -4
  58. package/dist/metadata/verifying-metadata-reader.js +30 -8
  59. package/dist/resolution/async-fan-out.d.ts +12 -0
  60. package/dist/resolution/async-fan-out.js +26 -0
  61. package/dist/resolution/cache/activation-need.d.ts +4 -5
  62. package/dist/resolution/cache/activation-need.js +11 -18
  63. package/dist/resolution/cache/binding-lookup-cache.d.ts +5 -12
  64. package/dist/resolution/cache/binding-lookup-cache.js +32 -19
  65. package/dist/resolution/cache/class-introspector.d.ts +16 -5
  66. package/dist/resolution/cache/class-introspector.js +73 -56
  67. package/dist/resolution/context.d.ts +17 -25
  68. package/dist/resolution/context.js +47 -56
  69. package/dist/resolution/path/resolution-path.d.ts +48 -13
  70. package/dist/resolution/path/resolution-path.js +90 -39
  71. package/dist/resolution/plan/instantiation-plan.d.ts +5 -5
  72. package/dist/resolution/plan/instantiation-plan.js +66 -26
  73. package/dist/resolution/plan/plan-codegen.d.ts +10 -7
  74. package/dist/resolution/plan/plan-codegen.js +62 -34
  75. package/dist/resolution/resolver.d.ts +16 -17
  76. package/dist/resolution/resolver.js +316 -273
  77. package/dist/resolution/select/binding-select.d.ts +6 -5
  78. package/dist/resolution/select/binding-select.js +5 -4
  79. package/dist/resolution/select/constraints.d.ts +3 -3
  80. package/dist/resolution/select/constraints.js +4 -4
  81. package/package.json +14 -2
@@ -3,8 +3,8 @@
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";
6
+ import { constructWithAmbientResolution, runWithAmbientResolution } from "#ambient/active-container";
7
+ import { InvalidMetadataError } from "#errors/errors";
8
8
  // Verified pairs, not verified classes: two readers may disagree about the same class, and a reader
9
9
  // that goes out of scope takes its record with it.
10
10
  const verifiedTargets = new WeakMap();
@@ -21,6 +21,20 @@ function markVerified(cache, reader, target) {
21
21
  }
22
22
  verified.add(target);
23
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
+ }
24
38
  /**
25
39
  * A reader's constructor metadata for a class, verified the first time this process asks.
26
40
  *
@@ -31,16 +45,9 @@ function markVerified(cache, reader, target) {
31
45
  * @since 0.6.0
32
46
  */
33
47
  export function verifyConstructorMetadata(reader, target) {
34
- const metadata = reader.getConstructorMetadata(target);
35
- if (metadata === undefined) {
36
- return undefined;
37
- }
38
- if (isVerified(verifiedTargets, reader, target)) {
39
- return metadata;
40
- }
41
- assertConstructorMetadata(metadata, target);
42
- markVerified(verifiedTargets, reader, target);
43
- return metadata;
48
+ return verifyOnce(verifiedTargets, reader, target, reader.getConstructorMetadata(target), (metadata) => {
49
+ assertConstructorMetadata(metadata, target);
50
+ });
44
51
  }
45
52
  /**
46
53
  * Verifies what a reader claims about a class, since a `MetadataReader` is a public seam.
@@ -81,29 +88,25 @@ export function assertConstructorMetadata(metadata, target) {
81
88
  * @since 0.6.0
82
89
  */
83
90
  export function verifyLifecycleMetadata(reader, target) {
84
- const metadata = reader.getLifecycleMetadata(target);
85
- if (metadata === undefined || isVerified(verifiedLifecycleTargets, reader, target)) {
86
- return metadata;
87
- }
88
- if (typeof metadata !== "object" || metadata === null) {
89
- throw new InvalidMetadataError(target.name, `lifecycle metadata: expected an object, received ${typeof metadata}`);
90
- }
91
- for (const phase of ["postConstruct", "preDestroy"]) {
92
- const methods = Reflect.get(metadata, phase);
93
- if (methods === undefined) {
94
- continue;
95
- }
96
- if (!Array.isArray(methods)) {
97
- throw new InvalidMetadataError(target.name, `lifecycle metadata: ${phase} is not an array`);
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}`);
98
94
  }
99
- for (const [position, name] of methods.entries()) {
100
- if (typeof name !== "string") {
101
- throw new InvalidMetadataError(target.name, `lifecycle metadata: ${phase}[${String(position)}] is not a string`);
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
+ }
102
107
  }
103
108
  }
104
- }
105
- markVerified(verifiedLifecycleTargets, reader, target);
106
- return metadata;
109
+ });
107
110
  }
108
111
  /**
109
112
  * A reader's accessor metadata for a class, verified the first time this process asks.
@@ -111,32 +114,28 @@ export function verifyLifecycleMetadata(reader, target) {
111
114
  * @since 0.6.0
112
115
  */
113
116
  export function verifyAccessorMetadata(reader, target) {
114
- const metadata = reader.getAccessorMetadata?.(target);
115
- if (metadata === undefined || isVerified(verifiedAccessorTargets, reader, target)) {
116
- return metadata;
117
- }
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`);
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");
128
120
  }
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`);
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
+ }
136
137
  }
137
- }
138
- markVerified(verifiedAccessorTargets, reader, target);
139
- return metadata;
138
+ });
140
139
  }
141
140
  const cachesByReader = new WeakMap();
142
141
  function cachesFor(reader) {
@@ -174,6 +173,24 @@ export class ClassIntrospector {
174
173
  (caches.constructorMetadata ??= new WeakMap()).set(target, metadata ?? null);
175
174
  return metadata;
176
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
+ }
177
194
  /**
178
195
  * Whether the class has a `@postConstruct` hook, or `undefined` until {@link discoverPostConstruct}.
179
196
  *
@@ -1,7 +1,7 @@
1
1
  /** The `ResolutionContext` a factory is handed, and the callbacks the resolver answers it with. */
2
- import type { Token } from "#/core/token";
3
- import type { BindingIdentifier, BindingKind, BindingScope, ConstraintContext, Constructor, ResolutionFrame, ResolutionContext, ResolveOptions } from "#/core/types";
4
- import type { BranchDepth, OwnedBranchStack } from "#/resolution/path/resolution-path";
2
+ import type { Token } from "#core/token";
3
+ import type { ConstraintContext, Constructor, ResolutionFrame, ResolutionContext, ResolveOptions } from "#core/types";
4
+ import type { BranchDepth, OwnedBranchStack } from "#resolution/path/resolution-path";
5
5
  /**
6
6
  * The engine surface a resolution context calls back into to resolve further dependencies.
7
7
  *
@@ -11,8 +11,6 @@ export interface ResolverCallbacks {
11
11
  resolveFromContext<Value>(token: Token<Value> | Constructor<Value>, resolutionStack: Array<ResolutionFrame>): Value;
12
12
  resolve<Value>(token: Token<Value> | Constructor<Value>, options: ResolveOptions | undefined, resolutionStack: Array<ResolutionFrame>): Value;
13
13
  resolveAsyncFromContext<Value>(token: Token<Value> | Constructor<Value>, resolutionStack: Array<ResolutionFrame>, branchDepth: BranchDepth): Promise<Value>;
14
- /** Not one of the eight `Value`-naming entry points: its caller is, and casts once. */
15
- resolveAsyncFromCascade(token: Token<unknown> | Constructor): Promise<unknown>;
16
14
  resolveAsync<Value>(token: Token<Value> | Constructor<Value>, options: ResolveOptions | undefined, resolutionStack: Array<ResolutionFrame>): Promise<Value>;
17
15
  resolveOptional<Value>(token: Token<Value> | Constructor<Value>, options: ResolveOptions | undefined, resolutionStack: Array<ResolutionFrame>): Value | undefined;
18
16
  resolveOptionalAsync<Value>(token: Token<Value> | Constructor<Value>, options: ResolveOptions | undefined, resolutionStack: Array<ResolutionFrame>): Promise<Value | undefined>;
@@ -55,6 +53,8 @@ export declare class AsyncLevelContext implements ResolutionContext {
55
53
  */
56
54
  constructor(resolver: ResolverCallbacks, resolutionStack: OwnedBranchStack, currentOptions: ResolveOptions | undefined);
57
55
  get graph(): ConstraintContext;
56
+ /** This level's own path, fixed for its lifetime, for a caller that names it in an error. */
57
+ get ownPath(): ReadonlyArray<ResolutionFrame>;
58
58
  resolve<Value>(token: Token<Value> | Constructor<Value>, options?: ResolveOptions): Value;
59
59
  resolveAsync<Value>(token: Token<Value> | Constructor<Value>, options?: ResolveOptions): Promise<Value>;
60
60
  resolveOptional<Value>(token: Token<Value> | Constructor<Value>, options?: ResolveOptions): Value | undefined;
@@ -63,27 +63,19 @@ export declare class AsyncLevelContext implements ResolutionContext {
63
63
  resolveAllAsync<Value>(token: Token<Value> | Constructor<Value>, options?: ResolveOptions): Promise<ReadonlyArray<Value>>;
64
64
  }
65
65
  /**
66
- * The one context every level of an open synchronous factory cascade shares.
66
+ * The one constraint context every predicate reads, whatever lane built it.
67
67
  *
68
- * @remarks It carries no per-level state at all: while the cascade is open, the resolver's stack
69
- * *is* this level's ancestor chain, so nothing has to be allocated per level.
68
+ * @remarks One shape for the shared root, a selection over a live synchronous stack, an async
69
+ * level's prefix and an inspector's probe, so a predicate's call site stays monomorphic.
70
70
  *
71
- * @since 0.5.0-canary.9
71
+ * @since 0.11.0
72
72
  */
73
- export declare class AsyncCascadeContext implements ResolutionContext {
73
+ export declare class DefaultConstraintContext implements ConstraintContext {
74
74
  #private;
75
- constructor(resolver: ResolverCallbacks, cascadeStack: Array<ResolutionFrame>);
76
- get graph(): ConstraintContext;
77
- resolve<Value>(token: Token<Value> | Constructor<Value>, options?: ResolveOptions): Value;
78
- resolveAsync<Value>(token: Token<Value> | Constructor<Value>, options?: ResolveOptions): Promise<Value>;
79
- resolveOptional<Value>(token: Token<Value> | Constructor<Value>, options?: ResolveOptions): Value | undefined;
80
- resolveOptionalAsync<Value>(token: Token<Value> | Constructor<Value>, options?: ResolveOptions): Promise<Value | undefined>;
81
- resolveAll<Value>(token: Token<Value> | Constructor<Value>, options?: ResolveOptions): ReadonlyArray<Value>;
82
- resolveAllAsync<Value>(token: Token<Value> | Constructor<Value>, options?: ResolveOptions): Promise<ReadonlyArray<Value>>;
83
- }
84
- /**
85
- * Creates the resolution-stack frame for one in-flight resolve.
86
- *
87
- * @since 0.3.16-canary.0
88
- */
89
- export declare function buildResolutionFrame(tokenName: string, scope: BindingScope, bindingId: BindingIdentifier, kind: BindingKind, slot: ResolutionFrame["slot"]): ResolutionFrame;
75
+ readonly resolutionStack: ReadonlyArray<ResolutionFrame>;
76
+ readonly parent: ResolutionFrame | undefined;
77
+ readonly currentResolveOptions: Readonly<ResolveOptions> | undefined;
78
+ constructor(resolutionStack: ReadonlyArray<ResolutionFrame>, currentResolveOptions: ResolveOptions | undefined);
79
+ get resolutionPath(): ReadonlyArray<string>;
80
+ get ancestors(): ReadonlyArray<ResolutionFrame>;
81
+ }
@@ -1,4 +1,4 @@
1
- import { UNOWNED_BRANCH } from "#/resolution/path/resolution-path";
1
+ import { bindingsOf, enterSeededPath, leaveSeededPath, UNOWNED_BRANCH } from "#resolution/path/resolution-path";
2
2
  /**
3
3
  * The `ResolutionContext` handed to factories and hooks, backed by the engine's callbacks.
4
4
  *
@@ -88,6 +88,7 @@ export class AsyncLevelContext {
88
88
  }
89
89
  #graph;
90
90
  #exactStackCache;
91
+ #exactBindingsCache;
91
92
  get graph() {
92
93
  if (this.#graph === undefined) {
93
94
  this.#graph = new DefaultConstraintContext(this.#exactStack(), this.#currentOptions);
@@ -99,11 +100,30 @@ export class AsyncLevelContext {
99
100
  #exactStack() {
100
101
  return (this.#exactStackCache ??= this.#resolutionStack.slice(0, this.#branchDepth));
101
102
  }
103
+ /** This level's own path, fixed for its lifetime, for a caller that names it in an error. */
104
+ get ownPath() {
105
+ return this.#exactStack();
106
+ }
107
+ // The bindings a synchronous call from this level marks in flight, read off the frames once.
108
+ #exactBindings() {
109
+ return (this.#exactBindingsCache ??= bindingsOf(this.#exactStack()));
110
+ }
111
+ // A synchronous call from an async level runs over a path no synchronous frame pushed, so the
112
+ // level's ancestors are marked in flight for its duration — and the level itself, whose factory is
113
+ // the caller: a factory resolving its own token synchronously has closed a cycle.
102
114
  resolve(token, options) {
103
- if (options === undefined) {
104
- return this.#resolver.resolveFromContext(token, this.#exactStack());
115
+ const path = this.#exactStack();
116
+ const marked = this.#exactBindings();
117
+ const alreadyInFlight = enterSeededPath(marked);
118
+ try {
119
+ if (options === undefined) {
120
+ return this.#resolver.resolveFromContext(token, path);
121
+ }
122
+ return this.#resolver.resolve(token, options, path);
123
+ }
124
+ finally {
125
+ leaveSeededPath(marked, alreadyInFlight);
105
126
  }
106
- return this.#resolver.resolve(token, options, this.#exactStack());
107
127
  }
108
128
  resolveAsync(token, options) {
109
129
  if (options === undefined) {
@@ -113,64 +133,43 @@ export class AsyncLevelContext {
113
133
  return this.#resolver.resolveAsync(token, options, this.#exactStack());
114
134
  }
115
135
  resolveOptional(token, options) {
116
- return this.#resolver.resolveOptional(token, options, this.#exactStack());
136
+ const path = this.#exactStack();
137
+ const marked = this.#exactBindings();
138
+ const alreadyInFlight = enterSeededPath(marked);
139
+ try {
140
+ return this.#resolver.resolveOptional(token, options, path);
141
+ }
142
+ finally {
143
+ leaveSeededPath(marked, alreadyInFlight);
144
+ }
117
145
  }
118
146
  resolveOptionalAsync(token, options) {
119
147
  return this.#resolver.resolveOptionalAsync(token, options, this.#exactStack());
120
148
  }
121
149
  resolveAll(token, options) {
122
- return this.#resolver.resolveAll(token, options, this.#exactStack());
150
+ const path = this.#exactStack();
151
+ const marked = this.#exactBindings();
152
+ const alreadyInFlight = enterSeededPath(marked);
153
+ try {
154
+ return this.#resolver.resolveAll(token, options, path);
155
+ }
156
+ finally {
157
+ leaveSeededPath(marked, alreadyInFlight);
158
+ }
123
159
  }
124
160
  resolveAllAsync(token, options) {
125
161
  return this.#resolver.resolveAllAsync(token, options, this.#exactStack());
126
162
  }
127
163
  }
128
164
  /**
129
- * The one context every level of an open synchronous factory cascade shares.
165
+ * The one constraint context every predicate reads, whatever lane built it.
130
166
  *
131
- * @remarks It carries no per-level state at all: while the cascade is open, the resolver's stack
132
- * *is* this level's ancestor chain, so nothing has to be allocated per level.
167
+ * @remarks One shape for the shared root, a selection over a live synchronous stack, an async
168
+ * level's prefix and an inspector's probe, so a predicate's call site stays monomorphic.
133
169
  *
134
- * @since 0.5.0-canary.9
170
+ * @since 0.11.0
135
171
  */
136
- export class AsyncCascadeContext {
137
- #resolver;
138
- #cascadeStack;
139
- constructor(resolver, cascadeStack) {
140
- this.#resolver = resolver;
141
- this.#cascadeStack = cascadeStack;
142
- }
143
- get graph() {
144
- // Not memoized: this context outlives every level, so a cached graph would describe whichever
145
- // level asked first. The cascade stack is only this level's ancestors while it is open.
146
- return new DefaultConstraintContext(this.#cascadeStack, undefined);
147
- }
148
- resolve(token, options) {
149
- if (options === undefined) {
150
- return this.#resolver.resolveFromContext(token, this.#cascadeStack);
151
- }
152
- return this.#resolver.resolve(token, options, this.#cascadeStack);
153
- }
154
- resolveAsync(token, options) {
155
- if (options === undefined) {
156
- return this.#resolver.resolveAsyncFromCascade(token);
157
- }
158
- return this.#resolver.resolveAsync(token, options, [...this.#cascadeStack]);
159
- }
160
- resolveOptional(token, options) {
161
- return this.#resolver.resolveOptional(token, options, this.#cascadeStack);
162
- }
163
- resolveOptionalAsync(token, options) {
164
- return this.#resolver.resolveOptionalAsync(token, options, [...this.#cascadeStack]);
165
- }
166
- resolveAll(token, options) {
167
- return this.#resolver.resolveAll(token, options, this.#cascadeStack);
168
- }
169
- resolveAllAsync(token, options) {
170
- return this.#resolver.resolveAllAsync(token, options, [...this.#cascadeStack]);
171
- }
172
- }
173
- class DefaultConstraintContext {
172
+ export class DefaultConstraintContext {
174
173
  resolutionStack;
175
174
  parent;
176
175
  currentResolveOptions;
@@ -195,12 +194,4 @@ class DefaultConstraintContext {
195
194
  }
196
195
  return this.#ancestors;
197
196
  }
198
- }
199
- /**
200
- * Creates the resolution-stack frame for one in-flight resolve.
201
- *
202
- * @since 0.3.16-canary.0
203
- */
204
- export function buildResolutionFrame(tokenName, scope, bindingId, kind, slot) {
205
- return { tokenName, scope, bindingId, kind, slot };
206
197
  }
@@ -1,12 +1,21 @@
1
- import type { BindingIdentifier, ResolutionFrame } from "#/core/types";
1
+ import type { Binding } from "#core/binding";
2
+ import type { ResolutionFrame } from "#core/types";
2
3
  /**
3
- * Where the cycle check switches from a linear frame scan to an attached Set.
4
+ * Creates the resolution-stack frame for one in-flight resolve of a binding.
4
5
  *
5
- * @remarks Measured rather than guessed: below this depth the linear scan wins, above it the Set does.
6
+ * @remarks A literal, because the engine's hot loads read frames: a class instance costs every level
7
+ * a few nanoseconds, and a weak map beside the frames costs every cold container an ephemeron insert
8
+ * per binding. The binding rides under a symbol no caller enumerates.
6
9
  *
7
- * @since 0.5.0-canary.7
10
+ * @since 0.3.16-canary.0
8
11
  */
9
- export declare const RESOLUTION_SET_THRESHOLD = 32;
12
+ export declare function buildResolutionFrame(binding: Binding, tokenName: string): ResolutionFrame;
13
+ /**
14
+ * The bindings behind the resolver-built frames of a path, in path order.
15
+ *
16
+ * @since 0.11.0
17
+ */
18
+ export declare function bindingsOf(frames: ReadonlyArray<ResolutionFrame>): Array<Binding>;
10
19
  /**
11
20
  * The token names along a stack plus the frame that closed the cycle — built only to throw.
12
21
  *
@@ -14,18 +23,44 @@ export declare const RESOLUTION_SET_THRESHOLD = 32;
14
23
  */
15
24
  export declare function cycleNamesOf(resolutionStack: ReadonlyArray<ResolutionFrame>, closingName: string): Array<string>;
16
25
  /**
17
- * Marks a level as in-flight on this stack, throwing if its binding is already an ancestor.
26
+ * Marks a level in flight on a synchronous path and pushes its frame, throwing if its binding already is.
18
27
  *
19
- * @remarks The check keys on binding identity — two distinct tokens may share a display name — and
20
- * the names an error prints are derived from the frames at the throw site. Unmark by popping the
21
- * stack plus `set?.delete(frame.bindingId)`. Sync only — the async lane never removes an entry, so
22
- * it extends a branch instead; see {@link extendResolutionBranch}.
28
+ * @remarks Synchronous code does not interleave, so the flag on the binding is exact membership in the
29
+ * path one call stack is resolving — at any depth, with no side table and nothing to size. The check
30
+ * keys on the binding, never on a token's display name, and the names an error prints are read off
31
+ * the frames at the throw site. Unmark with {@link leaveSyncPath}. Sync only: the async branch lane
32
+ * never unwinds, so it extends a branch instead; see {@link extendResolutionBranch}.
23
33
  *
24
- * @returns the membership set once the stack is deep enough to carry one, else `undefined`.
34
+ * @since 0.11.0
35
+ */
36
+ export declare function enterSyncPath(resolutionStack: Array<ResolutionFrame>, binding: Binding, frame: ResolutionFrame): void;
37
+ /**
38
+ * Pops the level {@link enterSyncPath} pushed and clears its binding's flag.
39
+ *
40
+ * @since 0.11.0
41
+ */
42
+ export declare function leaveSyncPath(resolutionStack: Array<ResolutionFrame>, binding: Binding): void;
43
+ /**
44
+ * Marks every binding of a seeded path in flight for the synchronous call about to run over it.
45
+ *
46
+ * @remarks A seed is a path no synchronous frame pushed — a plan's static ancestors handed to an
47
+ * escape, or an async level's branch handed to a factory's synchronous call — so its bindings carry no
48
+ * flag, and marking them is what keeps the flag the one check. A binding already in flight was
49
+ * flagged by an enclosing synchronous frame that is still running, which is the same fact stated
50
+ * once already, so it is left as it is and left alone on the way out; a cycle is reported where the
51
+ * path re-enters that binding, which is where the frames to name it are.
52
+ *
53
+ * @returns the bindings this call left flagged as it found them, when there were any, for {@link leaveSeededPath}
54
+ *
55
+ * @since 0.11.0
56
+ */
57
+ export declare function enterSeededPath(bindings: ReadonlyArray<Binding>): Set<Binding> | undefined;
58
+ /**
59
+ * Clears the flags {@link enterSeededPath} set, leaving the ones it found already set.
25
60
  *
26
- * @since 0.5.0-canary.7
61
+ * @since 0.11.0
27
62
  */
28
- export declare function enterResolutionPath(resolutionStack: Array<ResolutionFrame>, frame: ResolutionFrame): Set<BindingIdentifier> | undefined;
63
+ export declare function leaveSeededPath(bindings: ReadonlyArray<Binding>, alreadyInFlight: Set<Binding> | undefined): void;
29
64
  declare const BRANCH_BRAND: unique symbol;
30
65
  /**
31
66
  * A resolution stack one async branch owns, so appending to it cannot disturb another branch.
@@ -1,14 +1,42 @@
1
1
  /** Cycle-detection bookkeeping carried on the resolution stack itself. */
2
- import { CircularDependencyError } from "#/errors/errors";
3
- const RESOLUTION_SET_KEY = Symbol("di:resolution-set");
2
+ import { CircularDependencyError } from "#errors/errors";
3
+ /** The key under which a resolver-built frame carries its binding, for marking a seeded path. */
4
+ const FRAME_BINDING = Symbol("di:frame-binding");
4
5
  /**
5
- * Where the cycle check switches from a linear frame scan to an attached Set.
6
+ * Creates the resolution-stack frame for one in-flight resolve of a binding.
6
7
  *
7
- * @remarks Measured rather than guessed: below this depth the linear scan wins, above it the Set does.
8
+ * @remarks A literal, because the engine's hot loads read frames: a class instance costs every level
9
+ * a few nanoseconds, and a weak map beside the frames costs every cold container an ephemeron insert
10
+ * per binding. The binding rides under a symbol no caller enumerates.
8
11
  *
9
- * @since 0.5.0-canary.7
12
+ * @since 0.3.16-canary.0
10
13
  */
11
- export const RESOLUTION_SET_THRESHOLD = 32;
14
+ export function buildResolutionFrame(binding, tokenName) {
15
+ const frame = {
16
+ tokenName,
17
+ scope: binding.scope,
18
+ bindingId: binding.identifier,
19
+ kind: binding.kind,
20
+ slot: binding.slot,
21
+ [FRAME_BINDING]: binding,
22
+ };
23
+ return frame;
24
+ }
25
+ /**
26
+ * The bindings behind the resolver-built frames of a path, in path order.
27
+ *
28
+ * @since 0.11.0
29
+ */
30
+ export function bindingsOf(frames) {
31
+ const bindings = [];
32
+ for (let index = 0; index < frames.length; index += 1) {
33
+ const binding = frames[index][FRAME_BINDING];
34
+ if (binding !== undefined) {
35
+ bindings.push(binding);
36
+ }
37
+ }
38
+ return bindings;
39
+ }
12
40
  /**
13
41
  * The token names along a stack plus the frame that closed the cycle — built only to throw.
14
42
  *
@@ -23,47 +51,70 @@ export function cycleNamesOf(resolutionStack, closingName) {
23
51
  return names;
24
52
  }
25
53
  /**
26
- * Marks a level as in-flight on this stack, throwing if its binding is already an ancestor.
54
+ * Marks a level in flight on a synchronous path and pushes its frame, throwing if its binding already is.
27
55
  *
28
- * @remarks The check keys on binding identity — two distinct tokens may share a display name — and
29
- * the names an error prints are derived from the frames at the throw site. Unmark by popping the
30
- * stack plus `set?.delete(frame.bindingId)`. Sync only — the async lane never removes an entry, so
31
- * it extends a branch instead; see {@link extendResolutionBranch}.
56
+ * @remarks Synchronous code does not interleave, so the flag on the binding is exact membership in the
57
+ * path one call stack is resolving — at any depth, with no side table and nothing to size. The check
58
+ * keys on the binding, never on a token's display name, and the names an error prints are read off
59
+ * the frames at the throw site. Unmark with {@link leaveSyncPath}. Sync only: the async branch lane
60
+ * never unwinds, so it extends a branch instead; see {@link extendResolutionBranch}.
32
61
  *
33
- * @returns the membership set once the stack is deep enough to carry one, else `undefined`.
34
- *
35
- * @since 0.5.0-canary.7
62
+ * @since 0.11.0
36
63
  */
37
- export function enterResolutionPath(resolutionStack, frame) {
38
- const stackWithSet = resolutionStack;
39
- let resolutionSet = stackWithSet[RESOLUTION_SET_KEY];
40
- // A live set mirrors the stack exactly, so a size that disagrees means it is holding ids of
41
- // frames that unwound: the ones already on the stack when it attached were handed no set to
42
- // delete from. Dropped rather than repaired, because the next deep frame rebuilds it.
43
- if (resolutionSet !== undefined && resolutionSet.size !== resolutionStack.length) {
44
- resolutionSet = undefined;
45
- stackWithSet[RESOLUTION_SET_KEY] = undefined;
64
+ export function enterSyncPath(resolutionStack, binding, frame) {
65
+ if (binding.inFlight) {
66
+ throw new CircularDependencyError(cycleNamesOf(resolutionStack, frame.tokenName));
46
67
  }
47
- if (resolutionSet === undefined && resolutionStack.length >= RESOLUTION_SET_THRESHOLD) {
48
- resolutionSet = new Set();
49
- for (let index = 0; index < resolutionStack.length; index += 1) {
50
- resolutionSet.add(resolutionStack[index].bindingId);
68
+ binding.inFlight = true;
69
+ resolutionStack.push(frame);
70
+ }
71
+ /**
72
+ * Pops the level {@link enterSyncPath} pushed and clears its binding's flag.
73
+ *
74
+ * @since 0.11.0
75
+ */
76
+ export function leaveSyncPath(resolutionStack, binding) {
77
+ resolutionStack.pop();
78
+ binding.inFlight = false;
79
+ }
80
+ /**
81
+ * Marks every binding of a seeded path in flight for the synchronous call about to run over it.
82
+ *
83
+ * @remarks A seed is a path no synchronous frame pushed — a plan's static ancestors handed to an
84
+ * escape, or an async level's branch handed to a factory's synchronous call — so its bindings carry no
85
+ * flag, and marking them is what keeps the flag the one check. A binding already in flight was
86
+ * flagged by an enclosing synchronous frame that is still running, which is the same fact stated
87
+ * once already, so it is left as it is and left alone on the way out; a cycle is reported where the
88
+ * path re-enters that binding, which is where the frames to name it are.
89
+ *
90
+ * @returns the bindings this call left flagged as it found them, when there were any, for {@link leaveSeededPath}
91
+ *
92
+ * @since 0.11.0
93
+ */
94
+ export function enterSeededPath(bindings) {
95
+ let alreadyInFlight;
96
+ for (let index = 0; index < bindings.length; index += 1) {
97
+ const binding = bindings[index];
98
+ if (binding.inFlight) {
99
+ (alreadyInFlight ??= new Set()).add(binding);
100
+ continue;
51
101
  }
52
- stackWithSet[RESOLUTION_SET_KEY] = resolutionSet;
102
+ binding.inFlight = true;
53
103
  }
54
- if (resolutionSet === undefined) {
55
- for (let index = 0; index < resolutionStack.length; index += 1) {
56
- if (resolutionStack[index].bindingId === frame.bindingId) {
57
- throw new CircularDependencyError(cycleNamesOf(resolutionStack, frame.tokenName));
58
- }
104
+ return alreadyInFlight;
105
+ }
106
+ /**
107
+ * Clears the flags {@link enterSeededPath} set, leaving the ones it found already set.
108
+ *
109
+ * @since 0.11.0
110
+ */
111
+ export function leaveSeededPath(bindings, alreadyInFlight) {
112
+ for (let index = 0; index < bindings.length; index += 1) {
113
+ const binding = bindings[index];
114
+ if (alreadyInFlight === undefined || !alreadyInFlight.has(binding)) {
115
+ binding.inFlight = false;
59
116
  }
60
117
  }
61
- else if (resolutionSet.has(frame.bindingId)) {
62
- throw new CircularDependencyError(cycleNamesOf(resolutionStack, frame.tokenName));
63
- }
64
- resolutionStack.push(frame);
65
- resolutionSet?.add(frame.bindingId);
66
- return resolutionSet;
67
118
  }
68
119
  /**
69
120
  * A stack no async branch owns yet, so its first extension must copy rather than append.
@@ -4,11 +4,11 @@
4
4
  * @remarks A dependency the compiler cannot inline escapes to a resolver callback instead; the
5
5
  * closure must stay callable for it, which is what bounds what may be inlined.
6
6
  */
7
- import type { Binding } from "#/core/binding";
8
- import type { ConstructorInvocation } from "#/core/constructor-type";
9
- import type { Token } from "#/core/token";
10
- import type { Constructor, ResolutionFrame, ResolveOptions } from "#/core/types";
11
- import type { ConstructorMetadata } from "#/metadata/metadata-types";
7
+ import type { Binding } from "#core/binding";
8
+ import type { ConstructorInvocation } from "#core/constructor-type";
9
+ import type { Token } from "#core/token";
10
+ import type { Constructor, ResolutionFrame, ResolveOptions } from "#core/types";
11
+ import type { ConstructorMetadata } from "#metadata/metadata-types";
12
12
  /**
13
13
  * Compilation asked to retry later (class lifecycle metadata not discovered yet).
14
14
  *