@codefast/di 0.10.1 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/CHANGELOG.md +376 -0
  2. package/README.md +94 -17
  3. package/dist/{ambient/active-container.d.ts → ambient-container.d.ts} +7 -2
  4. package/dist/{ambient/active-container.js → ambient-container.js} +6 -1
  5. package/dist/container/{binding-builders.d.ts → binding-chain.d.ts} +27 -16
  6. package/dist/container/binding-chain.js +254 -0
  7. package/dist/container/container.d.ts +11 -2
  8. package/dist/container/container.js +293 -73
  9. package/dist/core/binding-builders.d.ts +114 -0
  10. package/dist/core/binding-declaration.d.ts +127 -0
  11. package/dist/core/binding-declaration.js +187 -0
  12. package/dist/core/binding.d.ts +62 -138
  13. package/dist/core/binding.js +54 -17
  14. package/dist/core/module.d.ts +8 -5
  15. package/dist/core/module.js +17 -3
  16. package/dist/core/registry.d.ts +17 -28
  17. package/dist/core/registry.js +92 -133
  18. package/dist/core/state-epoch.d.ts +18 -1
  19. package/dist/core/state-epoch.js +17 -0
  20. package/dist/core/tag.js +1 -1
  21. package/dist/decorators/inject.js +6 -5
  22. package/dist/decorators/injectable.js +4 -2
  23. package/dist/decorators/{lifecycle-decorators.js → lifecycle.js} +9 -3
  24. package/dist/decorators/metadata-record.d.ts +9 -0
  25. package/dist/decorators/metadata-record.js +20 -0
  26. package/dist/{errors/errors.d.ts → errors.d.ts} +84 -9
  27. package/dist/{errors/errors.js → errors.js} +101 -17
  28. package/dist/index.d.ts +14 -10
  29. package/dist/index.js +7 -6
  30. package/dist/injection/{resolve-options.js → dependency-slot.js} +6 -4
  31. package/dist/injection/descriptor.d.ts +1 -1
  32. package/dist/injection/descriptor.js +3 -7
  33. package/dist/introspection/dependency-graph.d.ts +8 -3
  34. package/dist/introspection/dependency-graph.js +48 -26
  35. package/dist/introspection/explanation.d.ts +101 -0
  36. package/dist/introspection/explanation.js +151 -0
  37. package/dist/introspection/graph-adapters/reactflow.js +6 -4
  38. package/dist/introspection/inspector.d.ts +9 -2
  39. package/dist/introspection/inspector.js +30 -28
  40. package/dist/lifecycle/{lifecycle-manager.d.ts → hooks.d.ts} +9 -2
  41. package/dist/lifecycle/{lifecycle-manager.js → hooks.js} +59 -30
  42. package/dist/lifecycle/{scope-manager.d.ts → scopes.d.ts} +3 -5
  43. package/dist/lifecycle/{scope-manager.js → scopes.js} +36 -29
  44. package/dist/metadata/{metadata-reader-token.d.ts → reader-token.d.ts} +1 -1
  45. package/dist/metadata/{symbol-metadata-reader.d.ts → symbol-reader.d.ts} +1 -1
  46. package/dist/metadata/{symbol-metadata-reader.js → symbol-reader.js} +1 -1
  47. package/dist/metadata/{metadata-types.d.ts → types.d.ts} +1 -1
  48. package/dist/metadata/types.js +1 -0
  49. package/dist/metadata/verify.d.ts +43 -0
  50. package/dist/metadata/verify.js +133 -0
  51. package/dist/metadata/{verifying-metadata-reader.d.ts → verifying-reader.d.ts} +5 -4
  52. package/dist/metadata/verifying-reader.js +53 -0
  53. package/dist/resolution/async-fan-out.d.ts +12 -0
  54. package/dist/resolution/async-fan-out.js +26 -0
  55. package/dist/resolution/cache/activation-need.d.ts +1 -2
  56. package/dist/resolution/cache/activation-need.js +16 -19
  57. package/dist/resolution/cache/class-introspector.d.ts +38 -36
  58. package/dist/resolution/cache/class-introspector.js +79 -159
  59. package/dist/resolution/cache/{binding-lookup-cache.d.ts → lookup.d.ts} +7 -9
  60. package/dist/resolution/cache/{binding-lookup-cache.js → lookup.js} +70 -25
  61. package/dist/resolution/context.d.ts +16 -24
  62. package/dist/resolution/context.js +47 -56
  63. package/dist/resolution/path.d.ts +128 -0
  64. package/dist/resolution/path.js +167 -0
  65. package/dist/resolution/plan/{plan-codegen.d.ts → codegen.d.ts} +5 -4
  66. package/dist/resolution/plan/{plan-codegen.js → codegen.js} +58 -32
  67. package/dist/resolution/plan/{instantiation-plan.d.ts → compiler.d.ts} +1 -1
  68. package/dist/resolution/plan/{instantiation-plan.js → compiler.js} +64 -24
  69. package/dist/resolution/resolver.d.ts +11 -12
  70. package/dist/resolution/resolver.js +573 -356
  71. package/dist/resolution/select/{binding-select.d.ts → candidates.d.ts} +30 -0
  72. package/dist/resolution/select/{binding-select.js → candidates.js} +51 -10
  73. package/dist/resolution/select/constraints.js +1 -1
  74. package/package.json +87 -67
  75. package/dist/container/binding-builders.js +0 -305
  76. package/dist/core/binding-scope.d.ts +0 -12
  77. package/dist/core/binding-scope.js +0 -12
  78. package/dist/metadata/verifying-metadata-reader.js +0 -31
  79. package/dist/resolution/path/resolution-path.d.ts +0 -93
  80. package/dist/resolution/path/resolution-path.js +0 -116
  81. /package/dist/{metadata/metadata-types.js → core/binding-builders.js} +0 -0
  82. /package/dist/decorators/{lifecycle-decorators.d.ts → lifecycle.d.ts} +0 -0
  83. /package/dist/injection/{resolve-options.d.ts → dependency-slot.d.ts} +0 -0
  84. /package/dist/{errors → introspection}/diagnostics.d.ts +0 -0
  85. /package/dist/{errors → introspection}/diagnostics.js +0 -0
  86. /package/dist/metadata/{metadata-keys.d.ts → keys.d.ts} +0 -0
  87. /package/dist/metadata/{metadata-keys.js → keys.js} +0 -0
  88. /package/dist/metadata/{metadata-reader-token.js → reader-token.js} +0 -0
@@ -1,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
  /**
@@ -36,9 +29,9 @@ export class BindingLookupCache {
36
29
  #lastTagToken;
37
30
  #lastTag;
38
31
  #lastTaggedEntry = null;
39
- // 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.
40
34
  #collections;
41
- #collectionsVersion = -1;
42
35
  #registry;
43
36
  #owner;
44
37
  #parent;
@@ -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;
@@ -169,30 +171,73 @@ export class BindingLookupCache {
169
171
  /** The memoized root-level collection for a token, or `undefined` once the chain changed since it was stored. */
170
172
  collection(token) {
171
173
  const version = this.chainVersion();
172
- if (version !== this.#collectionsVersion) {
173
- this.#collections?.clear();
174
- this.#collectionsVersion = version;
174
+ const memo = this.#collections;
175
+ if (memo === undefined) {
176
+ this.#collections = { version, lastToken: undefined, lastEntry: undefined, byToken: undefined };
175
177
  return undefined;
176
178
  }
177
- 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;
178
195
  }
179
- /** 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
+ */
180
200
  rememberCollection(token, entry) {
181
- (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;
182
216
  }
217
+ /**
218
+ * Follows alias hops to the terminal entry, or declines with `null` on a cycle, which the full
219
+ * resolve loop reports.
220
+ *
221
+ * @remarks The origin and the current token are compared by hand and the set holds only what lies
222
+ * between them, so the common one-hop alias allocates nothing and any chain is folded exactly.
223
+ */
183
224
  #foldAliases(token) {
184
225
  let current = token;
185
- for (let hop = 0; hop < ALIAS_HOP_LIMIT; hop += 1) {
226
+ let visited;
227
+ for (;;) {
186
228
  const entry = this.#findDefaultInChain(current);
187
- if (entry === null) {
229
+ if (entry === null || entry.binding.kind !== "alias") {
230
+ return entry;
231
+ }
232
+ const next = entry.binding.target;
233
+ if (next === token || next === current || visited?.has(next) === true) {
188
234
  return null;
189
235
  }
190
- if (entry.binding.kind !== "alias") {
191
- return entry;
236
+ if (current !== token) {
237
+ (visited ??= new Set()).add(current);
192
238
  }
193
- current = entry.binding.target;
239
+ current = next;
194
240
  }
195
- return null;
196
241
  }
197
242
  #findDefaultInChain(token) {
198
243
  const fast = this.#registry.getFastDefault(token);
@@ -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
- import type { BindingIdentifier, BindingKind, BindingScope, ConstraintContext, Constructor, ResolutionFrame, ResolutionContext, ResolveOptions } from "#core/types";
4
- import type { BranchDepth, OwnedBranchStack } from "#resolution/path/resolution-path";
3
+ import type { ConstraintContext, Constructor, ResolutionFrame, ResolutionContext, ResolveOptions } from "#core/types";
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
  *
@@ -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";
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
  }
@@ -0,0 +1,128 @@
1
+ import type { Binding } from "#core/binding";
2
+ import type { ResolutionFrame } from "#core/types";
3
+ /**
4
+ * Creates the resolution-stack frame for one in-flight resolve of a binding.
5
+ *
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.
9
+ *
10
+ * @since 0.3.16-canary.0
11
+ */
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>;
19
+ /**
20
+ * The token names along a stack plus the frame that closed the cycle — built only to throw.
21
+ *
22
+ * @since 0.6.0
23
+ */
24
+ export declare function cycleNamesOf(resolutionStack: ReadonlyArray<ResolutionFrame>, closingName: string): Array<string>;
25
+ /**
26
+ * Marks a level in flight on a synchronous path and pushes its frame, throwing if its binding already is.
27
+ *
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}.
33
+ *
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.
60
+ *
61
+ * @since 0.11.0
62
+ */
63
+ export declare function leaveSeededPath(bindings: ReadonlyArray<Binding>, alreadyInFlight: Set<Binding> | undefined): void;
64
+ declare const BRANCH_BRAND: unique symbol;
65
+ /**
66
+ * A resolution stack one async branch owns, so appending to it cannot disturb another branch.
67
+ *
68
+ * @remarks Only {@link extendResolutionBranch} mints one. That is what makes "may this lane append
69
+ * to this array" a question the compiler answers instead of a rule in a doc — a sync frame's stack,
70
+ * which that frame will pop, is a plain `Array<ResolutionFrame>` and cannot reach a level that owns
71
+ * its own.
72
+ *
73
+ * @since 0.5.0-canary.9
74
+ */
75
+ export type OwnedBranchStack = Array<ResolutionFrame> & {
76
+ readonly [BRANCH_BRAND]: true;
77
+ };
78
+ declare const BRANCH_DEPTH_BRAND: unique symbol;
79
+ /**
80
+ * How many leading entries of a stack belong to one async branch.
81
+ *
82
+ * @remarks Branded so a bare number cannot be passed: a depth from anywhere but this branch silently
83
+ * re-parents a level.
84
+ *
85
+ * @since 0.5.0-canary.9
86
+ */
87
+ export type OwnedBranchDepth = number & {
88
+ readonly [BRANCH_DEPTH_BRAND]: true;
89
+ };
90
+ /**
91
+ * A stack no async branch owns yet, so its first extension must copy rather than append.
92
+ *
93
+ * @since 0.5.0-canary.9
94
+ */
95
+ export declare const UNOWNED_BRANCH = -1;
96
+ /**
97
+ * How far into a stack one extension may reach: this branch's own depth, or nobody's.
98
+ *
99
+ * @remarks A union rather than a sentinel hidden inside the branded number, so the two cases are
100
+ * visible at every signature that takes one and `=== UNOWNED_BRANCH` narrows to the owned case.
101
+ *
102
+ * @since 0.5.0-canary.9
103
+ */
104
+ export type BranchDepth = OwnedBranchDepth | typeof UNOWNED_BRANCH;
105
+ /**
106
+ * The depth a chain's first level extends from, over an array its caller just minted.
107
+ *
108
+ * @since 0.5.0-canary.9
109
+ */
110
+ export declare const ROOT_BRANCH: OwnedBranchDepth;
111
+ /**
112
+ * A branch's own depth: the length its stack had when this level took it.
113
+ *
114
+ * @since 0.5.0-canary.9
115
+ */
116
+ export declare function branchDepthOf(branch: OwnedBranchStack): OwnedBranchDepth;
117
+ /**
118
+ * Extends one branch of an append-only stack, throwing if the binding is already an ancestor.
119
+ *
120
+ * @remarks The check compares binding ids on the frames; the names an error prints are derived at
121
+ * the throw site. Appends in place while this branch still owns the next slot, and copies its own
122
+ * prefix once a sibling has claimed it. Nothing is ever removed, so no async level has to observe
123
+ * its own settlement to unwind.
124
+ *
125
+ * @since 0.5.0-canary.9
126
+ */
127
+ export declare function extendResolutionBranch(resolutionStack: Array<ResolutionFrame>, branchDepth: BranchDepth, frame: ResolutionFrame): OwnedBranchStack;
128
+ export {};
@@ -0,0 +1,167 @@
1
+ /** Cycle-detection bookkeeping carried on the resolution stack itself. */
2
+ import { CircularDependencyError } from "#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");
5
+ /**
6
+ * Creates the resolution-stack frame for one in-flight resolve of a binding.
7
+ *
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.
11
+ *
12
+ * @since 0.3.16-canary.0
13
+ */
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
+ }
40
+ /**
41
+ * The token names along a stack plus the frame that closed the cycle — built only to throw.
42
+ *
43
+ * @since 0.6.0
44
+ */
45
+ export function cycleNamesOf(resolutionStack, closingName) {
46
+ const names = new Array(resolutionStack.length + 1);
47
+ for (let index = 0; index < resolutionStack.length; index += 1) {
48
+ names[index] = resolutionStack[index].tokenName;
49
+ }
50
+ names[resolutionStack.length] = closingName;
51
+ return names;
52
+ }
53
+ /**
54
+ * Marks a level in flight on a synchronous path and pushes its frame, throwing if its binding already is.
55
+ *
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}.
61
+ *
62
+ * @since 0.11.0
63
+ */
64
+ export function enterSyncPath(resolutionStack, binding, frame) {
65
+ if (binding.inFlight) {
66
+ throw new CircularDependencyError(cycleNamesOf(resolutionStack, frame.tokenName));
67
+ }
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;
101
+ }
102
+ binding.inFlight = true;
103
+ }
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;
116
+ }
117
+ }
118
+ }
119
+ /**
120
+ * A stack no async branch owns yet, so its first extension must copy rather than append.
121
+ *
122
+ * @since 0.5.0-canary.9
123
+ */
124
+ export const UNOWNED_BRANCH = -1;
125
+ /**
126
+ * The depth a chain's first level extends from, over an array its caller just minted.
127
+ *
128
+ * @since 0.5.0-canary.9
129
+ */
130
+ export const ROOT_BRANCH = 0;
131
+ /**
132
+ * A branch's own depth: the length its stack had when this level took it.
133
+ *
134
+ * @since 0.5.0-canary.9
135
+ */
136
+ export function branchDepthOf(branch) {
137
+ return branch.length;
138
+ }
139
+ /**
140
+ * Extends one branch of an append-only stack, throwing if the binding is already an ancestor.
141
+ *
142
+ * @remarks The check compares binding ids on the frames; the names an error prints are derived at
143
+ * the throw site. Appends in place while this branch still owns the next slot, and copies its own
144
+ * prefix once a sibling has claimed it. Nothing is ever removed, so no async level has to observe
145
+ * its own settlement to unwind.
146
+ *
147
+ * @since 0.5.0-canary.9
148
+ */
149
+ export function extendResolutionBranch(resolutionStack, branchDepth, frame) {
150
+ const depth = branchDepth === UNOWNED_BRANCH ? resolutionStack.length : branchDepth;
151
+ for (let index = 0; index < depth; index += 1) {
152
+ if (resolutionStack[index].bindingId === frame.bindingId) {
153
+ throw new CircularDependencyError(cycleNamesOf(resolutionStack.slice(0, depth), frame.tokenName));
154
+ }
155
+ }
156
+ // An unowned array belongs to a sync frame that will pop it, or carries a membership Set this
157
+ // lane cannot keep true; copying is what makes the branch's own appends safe.
158
+ if (branchDepth === resolutionStack.length) {
159
+ // The sole mint: appending in place needs a depth that came from a branch already owned, or
160
+ // ROOT_BRANCH over an array its caller minted for this chain alone.
161
+ resolutionStack.push(frame);
162
+ return resolutionStack;
163
+ }
164
+ const branch = resolutionStack.slice(0, depth);
165
+ branch.push(frame);
166
+ return branch;
167
+ }