@codefast/di 0.7.1 → 0.8.1

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 (218) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README.md +242 -159
  3. package/dist/ambient/active-container.d.ts +1 -2
  4. package/dist/ambient/active-container.js +1 -2
  5. package/dist/container/binding-builders.d.ts +1 -2
  6. package/dist/container/binding-builders.js +5 -7
  7. package/dist/container/container.d.ts +1 -2
  8. package/dist/container/container.js +4 -6
  9. package/dist/core/binding-scope.d.ts +1 -2
  10. package/dist/core/binding-scope.js +1 -2
  11. package/dist/core/binding.d.ts +16 -4
  12. package/dist/core/binding.js +30 -6
  13. package/dist/core/constraint-requirement.d.ts +1 -2
  14. package/dist/core/constraint-requirement.js +1 -2
  15. package/dist/core/constructor-type.d.ts +1 -2
  16. package/dist/core/constructor-type.js +1 -2
  17. package/dist/core/map-upsert.d.ts +1 -2
  18. package/dist/core/map-upsert.js +1 -2
  19. package/dist/core/module.d.ts +1 -2
  20. package/dist/core/module.js +1 -2
  21. package/dist/core/registry.d.ts +1 -5
  22. package/dist/core/registry.js +15 -63
  23. package/dist/core/tag.d.ts +27 -2
  24. package/dist/core/tag.js +55 -13
  25. package/dist/core/token.d.ts +1 -2
  26. package/dist/core/token.js +1 -2
  27. package/dist/core/types.d.ts +1 -2
  28. package/dist/core/types.js +1 -2
  29. package/dist/decorators/inject.d.ts +1 -2
  30. package/dist/decorators/inject.js +1 -2
  31. package/dist/decorators/injectable.d.ts +1 -2
  32. package/dist/decorators/injectable.js +1 -2
  33. package/dist/decorators/lifecycle-decorators.d.ts +1 -2
  34. package/dist/decorators/lifecycle-decorators.js +1 -2
  35. package/dist/errors/diagnostics.d.ts +1 -2
  36. package/dist/errors/diagnostics.js +1 -2
  37. package/dist/errors/errors.d.ts +1 -2
  38. package/dist/errors/errors.js +29 -5
  39. package/dist/index.d.ts +2 -3
  40. package/dist/index.js +2 -3
  41. package/dist/injection/descriptor.d.ts +1 -2
  42. package/dist/injection/descriptor.js +1 -2
  43. package/dist/injection/resolve-options.d.ts +18 -14
  44. package/dist/injection/resolve-options.js +70 -17
  45. package/dist/introspection/dependency-graph.d.ts +1 -2
  46. package/dist/introspection/dependency-graph.js +23 -11
  47. package/dist/introspection/graph-adapters/cytoscape.d.ts +1 -2
  48. package/dist/introspection/graph-adapters/cytoscape.js +1 -2
  49. package/dist/introspection/graph-adapters/dot.d.ts +1 -2
  50. package/dist/introspection/graph-adapters/dot.js +1 -2
  51. package/dist/introspection/graph-adapters/mermaid.d.ts +1 -2
  52. package/dist/introspection/graph-adapters/mermaid.js +1 -2
  53. package/dist/introspection/graph-adapters/reactflow.d.ts +1 -2
  54. package/dist/introspection/graph-adapters/reactflow.js +1 -2
  55. package/dist/introspection/inspector.d.ts +1 -2
  56. package/dist/introspection/inspector.js +1 -2
  57. package/dist/lifecycle/lifecycle-manager.d.ts +1 -2
  58. package/dist/lifecycle/lifecycle-manager.js +1 -2
  59. package/dist/lifecycle/scope-manager.d.ts +1 -2
  60. package/dist/lifecycle/scope-manager.js +1 -2
  61. package/dist/metadata/metadata-keys.d.ts +1 -2
  62. package/dist/metadata/metadata-keys.js +1 -2
  63. package/dist/metadata/metadata-reader-token.d.ts +1 -2
  64. package/dist/metadata/metadata-reader-token.js +1 -2
  65. package/dist/metadata/metadata-types.d.ts +1 -2
  66. package/dist/metadata/metadata-types.js +1 -2
  67. package/dist/metadata/symbol-metadata-reader.d.ts +1 -2
  68. package/dist/metadata/symbol-metadata-reader.js +1 -2
  69. package/dist/metadata/verifying-metadata-reader.d.ts +1 -2
  70. package/dist/metadata/verifying-metadata-reader.js +1 -2
  71. package/dist/resolution/cache/activation-need.d.ts +1 -2
  72. package/dist/resolution/cache/activation-need.js +1 -2
  73. package/dist/resolution/cache/binding-lookup-cache.d.ts +2 -5
  74. package/dist/resolution/cache/binding-lookup-cache.js +2 -40
  75. package/dist/resolution/cache/class-introspector.d.ts +1 -2
  76. package/dist/resolution/cache/class-introspector.js +1 -2
  77. package/dist/resolution/context.d.ts +1 -2
  78. package/dist/resolution/context.js +1 -2
  79. package/dist/resolution/path/resolution-path.d.ts +1 -2
  80. package/dist/resolution/path/resolution-path.js +1 -2
  81. package/dist/resolution/plan/instantiation-plan.d.ts +5 -14
  82. package/dist/resolution/plan/instantiation-plan.js +10 -29
  83. package/dist/resolution/resolver.d.ts +13 -7
  84. package/dist/resolution/resolver.js +107 -143
  85. package/dist/resolution/select/binding-select.d.ts +5 -7
  86. package/dist/resolution/select/binding-select.js +19 -22
  87. package/dist/resolution/select/constraints.d.ts +1 -2
  88. package/dist/resolution/select/constraints.js +1 -2
  89. package/package.json +6 -58
  90. package/dist/ambient/active-container.d.ts.map +0 -1
  91. package/dist/ambient/active-container.js.map +0 -1
  92. package/dist/container/binding-builders.d.ts.map +0 -1
  93. package/dist/container/binding-builders.js.map +0 -1
  94. package/dist/container/container.d.ts.map +0 -1
  95. package/dist/container/container.js.map +0 -1
  96. package/dist/core/binding-scope.d.ts.map +0 -1
  97. package/dist/core/binding-scope.js.map +0 -1
  98. package/dist/core/binding.d.ts.map +0 -1
  99. package/dist/core/binding.js.map +0 -1
  100. package/dist/core/constraint-requirement.d.ts.map +0 -1
  101. package/dist/core/constraint-requirement.js.map +0 -1
  102. package/dist/core/constructor-type.d.ts.map +0 -1
  103. package/dist/core/constructor-type.js.map +0 -1
  104. package/dist/core/map-upsert.d.ts.map +0 -1
  105. package/dist/core/map-upsert.js.map +0 -1
  106. package/dist/core/module.d.ts.map +0 -1
  107. package/dist/core/module.js.map +0 -1
  108. package/dist/core/registry.d.ts.map +0 -1
  109. package/dist/core/registry.js.map +0 -1
  110. package/dist/core/tag.d.ts.map +0 -1
  111. package/dist/core/tag.js.map +0 -1
  112. package/dist/core/token.d.ts.map +0 -1
  113. package/dist/core/token.js.map +0 -1
  114. package/dist/core/types.d.ts.map +0 -1
  115. package/dist/core/types.js.map +0 -1
  116. package/dist/decorators/inject.d.ts.map +0 -1
  117. package/dist/decorators/inject.js.map +0 -1
  118. package/dist/decorators/injectable.d.ts.map +0 -1
  119. package/dist/decorators/injectable.js.map +0 -1
  120. package/dist/decorators/lifecycle-decorators.d.ts.map +0 -1
  121. package/dist/decorators/lifecycle-decorators.js.map +0 -1
  122. package/dist/errors/diagnostics.d.ts.map +0 -1
  123. package/dist/errors/diagnostics.js.map +0 -1
  124. package/dist/errors/errors.d.ts.map +0 -1
  125. package/dist/errors/errors.js.map +0 -1
  126. package/dist/index.d.ts.map +0 -1
  127. package/dist/index.js.map +0 -1
  128. package/dist/injection/descriptor.d.ts.map +0 -1
  129. package/dist/injection/descriptor.js.map +0 -1
  130. package/dist/injection/resolve-options.d.ts.map +0 -1
  131. package/dist/injection/resolve-options.js.map +0 -1
  132. package/dist/introspection/dependency-graph.d.ts.map +0 -1
  133. package/dist/introspection/dependency-graph.js.map +0 -1
  134. package/dist/introspection/graph-adapters/cytoscape.d.ts.map +0 -1
  135. package/dist/introspection/graph-adapters/cytoscape.js.map +0 -1
  136. package/dist/introspection/graph-adapters/dot.d.ts.map +0 -1
  137. package/dist/introspection/graph-adapters/dot.js.map +0 -1
  138. package/dist/introspection/graph-adapters/mermaid.d.ts.map +0 -1
  139. package/dist/introspection/graph-adapters/mermaid.js.map +0 -1
  140. package/dist/introspection/graph-adapters/reactflow.d.ts.map +0 -1
  141. package/dist/introspection/graph-adapters/reactflow.js.map +0 -1
  142. package/dist/introspection/inspector.d.ts.map +0 -1
  143. package/dist/introspection/inspector.js.map +0 -1
  144. package/dist/lifecycle/lifecycle-manager.d.ts.map +0 -1
  145. package/dist/lifecycle/lifecycle-manager.js.map +0 -1
  146. package/dist/lifecycle/scope-manager.d.ts.map +0 -1
  147. package/dist/lifecycle/scope-manager.js.map +0 -1
  148. package/dist/metadata/metadata-keys.d.ts.map +0 -1
  149. package/dist/metadata/metadata-keys.js.map +0 -1
  150. package/dist/metadata/metadata-reader-token.d.ts.map +0 -1
  151. package/dist/metadata/metadata-reader-token.js.map +0 -1
  152. package/dist/metadata/metadata-types.d.ts.map +0 -1
  153. package/dist/metadata/metadata-types.js.map +0 -1
  154. package/dist/metadata/symbol-metadata-reader.d.ts.map +0 -1
  155. package/dist/metadata/symbol-metadata-reader.js.map +0 -1
  156. package/dist/metadata/verifying-metadata-reader.d.ts.map +0 -1
  157. package/dist/metadata/verifying-metadata-reader.js.map +0 -1
  158. package/dist/resolution/cache/activation-need.d.ts.map +0 -1
  159. package/dist/resolution/cache/activation-need.js.map +0 -1
  160. package/dist/resolution/cache/binding-lookup-cache.d.ts.map +0 -1
  161. package/dist/resolution/cache/binding-lookup-cache.js.map +0 -1
  162. package/dist/resolution/cache/class-introspector.d.ts.map +0 -1
  163. package/dist/resolution/cache/class-introspector.js.map +0 -1
  164. package/dist/resolution/context.d.ts.map +0 -1
  165. package/dist/resolution/context.js.map +0 -1
  166. package/dist/resolution/path/resolution-path.d.ts.map +0 -1
  167. package/dist/resolution/path/resolution-path.js.map +0 -1
  168. package/dist/resolution/plan/instantiation-plan.d.ts.map +0 -1
  169. package/dist/resolution/plan/instantiation-plan.js.map +0 -1
  170. package/dist/resolution/resolver.d.ts.map +0 -1
  171. package/dist/resolution/resolver.js.map +0 -1
  172. package/dist/resolution/select/binding-select.d.ts.map +0 -1
  173. package/dist/resolution/select/binding-select.js.map +0 -1
  174. package/dist/resolution/select/constraints.d.ts.map +0 -1
  175. package/dist/resolution/select/constraints.js.map +0 -1
  176. package/src/ambient/active-container.ts +0 -69
  177. package/src/container/binding-builders.ts +0 -332
  178. package/src/container/container.ts +0 -973
  179. package/src/core/binding-scope.ts +0 -15
  180. package/src/core/binding.ts +0 -477
  181. package/src/core/constraint-requirement.ts +0 -94
  182. package/src/core/constructor-type.ts +0 -18
  183. package/src/core/map-upsert.ts +0 -53
  184. package/src/core/module.ts +0 -117
  185. package/src/core/registry.ts +0 -371
  186. package/src/core/tag.ts +0 -158
  187. package/src/core/token.ts +0 -31
  188. package/src/core/types.ts +0 -149
  189. package/src/decorators/inject.ts +0 -101
  190. package/src/decorators/injectable.ts +0 -134
  191. package/src/decorators/lifecycle-decorators.ts +0 -43
  192. package/src/errors/diagnostics.ts +0 -45
  193. package/src/errors/errors.ts +0 -476
  194. package/src/index.ts +0 -139
  195. package/src/injection/descriptor.ts +0 -218
  196. package/src/injection/resolve-options.ts +0 -144
  197. package/src/introspection/dependency-graph.ts +0 -282
  198. package/src/introspection/graph-adapters/cytoscape.ts +0 -78
  199. package/src/introspection/graph-adapters/dot.ts +0 -32
  200. package/src/introspection/graph-adapters/mermaid.ts +0 -61
  201. package/src/introspection/graph-adapters/reactflow.ts +0 -84
  202. package/src/introspection/inspector.ts +0 -134
  203. package/src/lifecycle/lifecycle-manager.ts +0 -272
  204. package/src/lifecycle/scope-manager.ts +0 -158
  205. package/src/metadata/metadata-keys.ts +0 -28
  206. package/src/metadata/metadata-reader-token.ts +0 -10
  207. package/src/metadata/metadata-types.ts +0 -57
  208. package/src/metadata/symbol-metadata-reader.ts +0 -126
  209. package/src/metadata/verifying-metadata-reader.ts +0 -41
  210. package/src/resolution/cache/activation-need.ts +0 -87
  211. package/src/resolution/cache/binding-lookup-cache.ts +0 -209
  212. package/src/resolution/cache/class-introspector.ts +0 -256
  213. package/src/resolution/context.ts +0 -331
  214. package/src/resolution/path/resolution-path.ts +0 -167
  215. package/src/resolution/plan/instantiation-plan.ts +0 -617
  216. package/src/resolution/resolver.ts +0 -1675
  217. package/src/resolution/select/binding-select.ts +0 -210
  218. package/src/resolution/select/constraints.ts +0 -146
@@ -1,973 +0,0 @@
1
- import type { BindingRegistration } from "#/container/binding-builders";
2
- import { BindingChain } from "#/container/binding-builders";
3
- import type { Binding, BindingBuilder, BindToBuilder, ConstantBinding } from "#/core/binding";
4
- import { NO_INSTANCE } from "#/core/binding";
5
- import { effectiveBindingScope } from "#/core/binding-scope";
6
- import { constraintRequirementsOf } from "#/core/constraint-requirement";
7
- import { getOrInsert } from "#/core/map-upsert";
8
- import type { AsyncModule, AsyncModuleBuilder, ModuleBuilder, SyncModule } from "#/core/module";
9
- import { isSyncModule, MODULE_SETUP } from "#/core/module";
10
- import { BindingRegistry } from "#/core/registry";
11
- import type { Token } from "#/core/token";
12
- import { tokenName } from "#/core/token";
13
- import type {
14
- ActivationHandler,
15
- BindingIdentifier,
16
- BindingScope,
17
- Constructor,
18
- DeactivationHandler,
19
- ResolveOptions,
20
- } from "#/core/types";
21
- import type { AutoRegisterRegistry } from "#/decorators/injectable";
22
- import type { ResolutionDiagnostics } from "#/errors/diagnostics";
23
- import { RESOLUTION_DIAGNOSTICS } from "#/errors/diagnostics";
24
- import {
25
- AsyncModuleLoadError,
26
- CircularDependencyError,
27
- DisposedContainerError,
28
- InternalError,
29
- RebindUnboundTokenError,
30
- ScopeViolationError,
31
- SyncDisposalNotSupportedError,
32
- UnreachableConstraintError,
33
- UnreachableLifecycleHookError,
34
- } from "#/errors/errors";
35
- import type { DependencySlot } from "#/injection/resolve-options";
36
- import { injectionSlotToResolveOptions, bindingSlotToResolveOptions } from "#/injection/resolve-options";
37
- import type { ContainerGraphJson, GraphOptions } from "#/introspection/dependency-graph";
38
- import { buildDependencyGraph } from "#/introspection/dependency-graph";
39
- import type { BindingSnapshot, ContainerSnapshot } from "#/introspection/inspector";
40
- import { Inspector } from "#/introspection/inspector";
41
- import { LifecycleManager } from "#/lifecycle/lifecycle-manager";
42
- import { ScopeManager } from "#/lifecycle/scope-manager";
43
- import { MetadataReaderToken } from "#/metadata/metadata-reader-token";
44
- import type { MetadataReader } from "#/metadata/metadata-types";
45
- import { defaultMetadataReader } from "#/metadata/symbol-metadata-reader";
46
- import { verifyingMetadataReader } from "#/metadata/verifying-metadata-reader";
47
- import { ROOT_BRANCH } from "#/resolution/path/resolution-path";
48
- import { DependencyResolver } from "#/resolution/resolver";
49
-
50
- // ── Container interface ──────────────────────────────────────────────────────────────────────────────────────────────
51
-
52
- /**
53
- * The public surface: binding, resolving, modules, lifecycle hooks, child scopes, and disposal.
54
- *
55
- * @since 0.3.16-canary.0
56
- */
57
- export interface Container {
58
- readonly isDisposed: boolean;
59
-
60
- bind<Value>(token: Token<Value> | Constructor<Value>): BindToBuilder<Value>;
61
- unbind(tokenOrId: Token<unknown> | Constructor | BindingIdentifier): void;
62
- unbindAsync(tokenOrId: Token<unknown> | Constructor | BindingIdentifier): Promise<void>;
63
- unbindAll(): void;
64
- unbindAllAsync(): Promise<void>;
65
- rebind<Value>(token: Token<Value> | Constructor<Value>): BindToBuilder<Value>;
66
-
67
- load(...modules: Array<SyncModule>): void;
68
- loadAsync(...modules: Array<SyncModule | AsyncModule>): Promise<void>;
69
- unload(...modules: Array<SyncModule>): void;
70
- unloadAsync(...modules: Array<SyncModule | AsyncModule>): Promise<void>;
71
- loadAutoRegistered(registry: AutoRegisterRegistry): number;
72
-
73
- onActivation<Value>(token: Token<Value> | Constructor<Value>, handler: ActivationHandler<Value>): void;
74
- onDeactivation<Value>(token: Token<Value> | Constructor<Value>, handler: DeactivationHandler<Value>): void;
75
-
76
- resolve<Value>(token: Token<Value> | Constructor<Value>, options?: ResolveOptions): Value;
77
- resolveAsync<Value>(token: Token<Value> | Constructor<Value>, options?: ResolveOptions): Promise<Value>;
78
- resolveOptional<Value>(token: Token<Value> | Constructor<Value>, options?: ResolveOptions): Value | undefined;
79
- resolveOptionalAsync<Value>(
80
- token: Token<Value> | Constructor<Value>,
81
- options?: ResolveOptions,
82
- ): Promise<Value | undefined>;
83
- resolveAll<Value>(token: Token<Value> | Constructor<Value>, options?: ResolveOptions): Array<Value>;
84
- resolveAllAsync<Value>(token: Token<Value> | Constructor<Value>, options?: ResolveOptions): Promise<Array<Value>>;
85
-
86
- createChild(): Container;
87
-
88
- dispose(): Promise<void>;
89
- [Symbol.asyncDispose](): Promise<void>;
90
- [Symbol.dispose](): never;
91
-
92
- initializeAsync(): Promise<void>;
93
- validate(): void;
94
-
95
- has(token: Token<unknown> | Constructor, options?: ResolveOptions): boolean;
96
- hasOwn(token: Token<unknown> | Constructor, options?: ResolveOptions): boolean;
97
- lookupBindings<Value>(token: Token<Value> | Constructor<Value>): ReadonlyArray<BindingSnapshot>;
98
- inspect(): ContainerSnapshot;
99
- generateDependencyGraph(options?: GraphOptions): ContainerGraphJson;
100
- }
101
-
102
- /**
103
- * What a container has to be told before it exists, as opposed to what it can be bound later.
104
- *
105
- * @since 0.6.0
106
- */
107
- export interface ContainerOptions {
108
- /**
109
- * Reader the resolver consults for class metadata, replacing the decorator reader.
110
- *
111
- * @remarks Takes precedence over a {@link MetadataReaderToken} binding, and children inherit it.
112
- * A resolver is handed its reader when it is built, so this is the only way to give the container
113
- * you are creating a reader of your own.
114
- */
115
- readonly metadataReader?: MetadataReader | undefined;
116
- }
117
-
118
- /**
119
- * The factory surface the `Container` companion object exposes.
120
- *
121
- * @since 0.3.16-canary.0
122
- */
123
- export interface ContainerStatic {
124
- create(options?: ContainerOptions): Container;
125
- fromModules(...modules: Array<SyncModule>): Container;
126
- fromModulesAsync(...modules: Array<SyncModule | AsyncModule>): Promise<Container>;
127
- }
128
-
129
- // A Record rather than an if-chain, so a new `BindingScope` is a compile error here instead of
130
- // silently landing in whichever branch happened to be last.
131
- const APPLY_BINDING_SCOPE: Record<BindingScope, (builder: BindingBuilder<unknown>) => void> = {
132
- singleton: (builder) => {
133
- builder.singleton();
134
- },
135
- scoped: (builder) => {
136
- builder.scoped();
137
- },
138
- transient: (builder) => {
139
- builder.transient();
140
- },
141
- };
142
-
143
- // ── DefaultContainer ─────────────────────────────────────────────────────────────────────────────────────────────────
144
-
145
- class DefaultContainer implements Container {
146
- #disposed = false;
147
- // The one teardown run — every dispose() call returns it once it exists.
148
- #disposePromise: Promise<void> | undefined;
149
- readonly #registry: BindingRegistry;
150
- readonly #scope: ScopeManager;
151
- readonly #lifecycle: LifecycleManager;
152
- #resolver!: DependencyResolver;
153
- // Built on the first introspecting call — a container that only binds and resolves never needs it.
154
- #inspector: Inspector | undefined;
155
- readonly #parent: DefaultContainer | undefined;
156
-
157
- // Module tracking: module -> ref count. Both tables stay unallocated until a module is loaded.
158
- #moduleRefs: Map<object, number> | undefined;
159
- // Module bindings: module -> array of binding IDs registered by it
160
- #moduleBindingIds: Map<object, Array<BindingIdentifier>> | undefined;
161
- // One shared registration for every chain this container's own `bind()` creates.
162
- #registration: BindingRegistration | undefined;
163
-
164
- constructor(parent?: DefaultContainer, options?: ContainerOptions) {
165
- this.#parent = parent;
166
- this.#registry = new BindingRegistry();
167
- this.#scope = new ScopeManager(parent !== undefined);
168
- this.#lifecycle = new LifecycleManager();
169
- this.#initResolver(options?.metadataReader);
170
- }
171
-
172
- #getInspector(): Inspector {
173
- return (this.#inspector ??= new Inspector(
174
- this.#registry,
175
- this.#scope,
176
- this.#parent !== undefined,
177
- () => this.#disposed,
178
- ));
179
- }
180
-
181
- [RESOLUTION_DIAGNOSTICS](): ResolutionDiagnostics {
182
- const builtSubsystems: Array<string> = [];
183
- if (this.#inspector !== undefined) {
184
- builtSubsystems.push("container.inspector");
185
- }
186
- if (this.#moduleRefs !== undefined || this.#moduleBindingIds !== undefined) {
187
- builtSubsystems.push("container.moduleTables");
188
- }
189
- if (this.#registry.isNamedIndexBuilt) {
190
- builtSubsystems.push("registry.namedIndex");
191
- }
192
- if (this.#registry.isTaggedIndexBuilt) {
193
- builtSubsystems.push("registry.taggedIndex");
194
- }
195
- if (this.#scope.isScopedCacheBuilt) {
196
- builtSubsystems.push("scope.scoped");
197
- }
198
- if (this.#lifecycle.isActivationTableBuilt) {
199
- builtSubsystems.push("lifecycle.activationHooks");
200
- }
201
- return { ...this.#resolver.describeCaches(), scopedInstanceCount: this.#scope.scopedCount, builtSubsystems };
202
- }
203
-
204
- #initResolver(configuredReader: MetadataReader | undefined): void {
205
- const parent = this.#parent;
206
- const metadataReader = verifyingMetadataReader(
207
- configuredReader ?? (parent === undefined ? defaultMetadataReader : parent.#readerForChild()),
208
- );
209
- const parentResolver = parent === undefined ? undefined : parent.#resolver;
210
- this.#resolver = new DependencyResolver(
211
- this.#registry,
212
- this.#scope,
213
- this.#lifecycle,
214
- metadataReader,
215
- this,
216
- parentResolver,
217
- );
218
- }
219
-
220
- /** What a container being constructed under this one inherits: a reader bound here, else this one's. */
221
- #readerForChild(): MetadataReader {
222
- if (this.#registry.getAll(MetadataReaderToken).length > 0) {
223
- try {
224
- return this.#resolver.resolve(MetadataReaderToken, undefined, []);
225
- } catch {
226
- // An unresolvable reader binding is not worth failing a child over.
227
- }
228
- }
229
- return this.#resolver.metadataReader;
230
- }
231
-
232
- /** One reader per container, fixed when its resolver was built — so every path agrees on it. */
233
- #getMetadataReader(): MetadataReader {
234
- return this.#resolver.metadataReader;
235
- }
236
-
237
- get isDisposed(): boolean {
238
- return this.#disposed;
239
- }
240
-
241
- // ── Binding ────────────────────────────────────────────────────────────────────────────────────────────────────────
242
-
243
- bind<Value>(token: Token<Value> | Constructor<Value>): BindToBuilder<Value> {
244
- this.#assertNotDisposed();
245
- return this.#createBindToBuilder(token);
246
- }
247
-
248
- /** The registration every non-module chain shares, so `bind()` allocates only the builder. */
249
- #ownRegistration(): BindingRegistration {
250
- return (this.#registration ??= { registry: this.#registry, scope: this.#scope, moduleBindingIds: undefined });
251
- }
252
-
253
- /** One registration per module load, holding that module's id list directly. */
254
- #moduleRegistration(moduleRef: object): BindingRegistration {
255
- this.#moduleBindingIds ??= new Map();
256
- return {
257
- registry: this.#registry,
258
- scope: this.#scope,
259
- moduleBindingIds: getOrInsert(this.#moduleBindingIds, moduleRef, []),
260
- };
261
- }
262
-
263
- #createBindToBuilder<Value>(
264
- token: Token<Value> | Constructor<Value>,
265
- registration: BindingRegistration = this.#ownRegistration(),
266
- ): BindToBuilder<Value> {
267
- return new BindingChain<Value>(token, registration);
268
- }
269
-
270
- unbind(tokenOrId: Token<unknown> | Constructor | BindingIdentifier): void {
271
- this.#assertNotDisposed();
272
- this.#unbindSync(tokenOrId);
273
- }
274
-
275
- /** Remove bindings from registry + scope and collect [binding, instance] pairs for deactivation. */
276
- #collectDeactivationPairs(tokenOrId: Token<unknown> | Constructor | BindingIdentifier): Array<[Binding, unknown]> {
277
- if (typeof tokenOrId === "string") {
278
- const binding = this.#registry.removeById(tokenOrId);
279
- return binding === undefined ? [] : this.#drainSingletons([binding]);
280
- }
281
- // Dropping the whole token in one pass: removing each binding by id instead would re-scan and
282
- // re-index the token's binding list once per binding.
283
- return this.#drainSingletons(this.#registry.removeByToken(tokenOrId));
284
- }
285
-
286
- /** Drain scope entries for already-removed bindings, and pair each one that still owes a deactivation. */
287
- #drainSingletons(bindings: ReadonlyArray<Binding>): Array<[Binding, unknown]> {
288
- const pairs: Array<[Binding, unknown]> = [];
289
- for (const binding of bindings) {
290
- if (binding.instance !== NO_INSTANCE) {
291
- pairs.push([binding, binding.instance]);
292
- this.#scope.deleteSingleton(binding);
293
- } else if (this.#owesConstantDeactivation(binding)) {
294
- pairs.push([binding, binding.value]);
295
- }
296
- this.#scope.deleteScoped(binding.id);
297
- }
298
- return pairs;
299
- }
300
-
301
- /**
302
- * Whether a constant still owes its deactivation.
303
- *
304
- * @remarks A constant's value is handed in at bind time rather than built on demand, so its hook is
305
- * owed whether or not anything ever resolved it. Callers check `instance` first: a constant that
306
- * carries one was cached through activation and is deactivated with that value instead.
307
- */
308
- #owesConstantDeactivation(binding: Binding): binding is ConstantBinding<unknown> {
309
- return (
310
- binding.kind === "constant" &&
311
- (binding.onDeactivation !== undefined || this.#lifecycle.hasDeactivationHandlers(binding.token))
312
- );
313
- }
314
-
315
- /** Runs every pair's deactivation even when one throws, then reports what threw. */
316
- #deactivatePairsSync(pairs: ReadonlyArray<[Binding, unknown]>): void {
317
- if (pairs.length === 0) {
318
- return;
319
- }
320
- const reader = this.#getMetadataReader();
321
- const errors: Array<unknown> = [];
322
- for (const [binding, instance] of pairs) {
323
- try {
324
- this.#lifecycle.runDeactivationSync(binding, instance, reader);
325
- } catch (error) {
326
- errors.push(error);
327
- }
328
- }
329
- throwCollected(errors, "unbind completed, but deactivation hooks threw");
330
- }
331
-
332
- async #deactivatePairs(pairs: ReadonlyArray<[Binding, unknown]>): Promise<void> {
333
- if (pairs.length === 0) {
334
- return;
335
- }
336
- const reader = this.#getMetadataReader();
337
- const errors: Array<unknown> = [];
338
- for (const [binding, instance] of pairs) {
339
- try {
340
- await this.#lifecycle.runDeactivation(binding, instance, reader);
341
- } catch (error) {
342
- errors.push(error);
343
- }
344
- }
345
- throwCollected(errors, "unbind completed, but deactivation hooks threw");
346
- }
347
-
348
- #unbindSync(tokenOrId: Token<unknown> | Constructor | BindingIdentifier): void {
349
- this.#deactivatePairsSync(this.#collectDeactivationPairs(tokenOrId));
350
- }
351
-
352
- async unbindAsync(tokenOrId: Token<unknown> | Constructor | BindingIdentifier): Promise<void> {
353
- this.#assertNotDisposed();
354
- await this.#deactivatePairs(this.#collectDeactivationPairs(tokenOrId));
355
- }
356
-
357
- unbindAll(): void {
358
- this.#assertNotDisposed();
359
- this.#deactivatePairsSync(this.#drainSingletons(this.#registry.clear()));
360
- }
361
-
362
- async unbindAllAsync(): Promise<void> {
363
- this.#assertNotDisposed();
364
- await this.#deactivatePairs(this.#drainSingletons(this.#registry.clear()));
365
- }
366
-
367
- rebind<Value>(token: Token<Value> | Constructor<Value>): BindToBuilder<Value> {
368
- this.#assertNotDisposed();
369
- if (!this.#registry.has(token)) {
370
- throw new RebindUnboundTokenError(tokenName(token));
371
- }
372
- // Unbind existing (sync — if async deactivation, will throw AsyncDeactivationError)
373
- this.#unbindSync(token);
374
- return this.#createBindToBuilder(token);
375
- }
376
-
377
- // ── Module ─────────────────────────────────────────────────────────────────────────────────────────────────────────
378
-
379
- load(...modules: Array<SyncModule>): void {
380
- this.#assertNotDisposed();
381
- this.#loadSyncModules(modules);
382
- }
383
-
384
- // Imports nested inside a module's setup re-enter here through the builder, so a module listed
385
- // twice in one call is deduped by identity and the rest is ref-counting.
386
- #loadSyncModules(modules: ReadonlyArray<SyncModule | AsyncModule>): void {
387
- for (const module of new Set(modules)) {
388
- if (!isSyncModule(module)) {
389
- throw new AsyncModuleLoadError(module.name);
390
- }
391
- const moduleRef = module as object;
392
- const moduleRefs = (this.#moduleRefs ??= new Map<object, number>());
393
- const existing = moduleRefs.get(moduleRef);
394
- if (existing !== undefined) {
395
- moduleRefs.set(moduleRef, existing + 1);
396
- continue;
397
- }
398
- moduleRefs.set(moduleRef, 1);
399
- const builder = this.#createModuleBuilder(moduleRef);
400
- try {
401
- module[MODULE_SETUP](builder);
402
- } catch (error) {
403
- throw this.#rollbackFailedLoadSync(moduleRef, error);
404
- }
405
- }
406
- }
407
-
408
- async loadAsync(...modules: Array<SyncModule | AsyncModule>): Promise<void> {
409
- this.#assertNotDisposed();
410
- // Deduped by identity like the sync lane, so `loadAsync(m, m)` ref-counts once.
411
- for (const module of new Set(modules)) {
412
- await this.#loadOneModuleAsync(module);
413
- }
414
- }
415
-
416
- /** Undo a failed load — bindings registered before the throw and the ref-count both roll back. */
417
- #rollbackFailedLoadSync(moduleRef: object, cause: unknown): unknown {
418
- const pairs = this.#removeModuleBindings(moduleRef);
419
- try {
420
- this.#deactivatePairsSync(pairs);
421
- } catch (rollbackError) {
422
- return new AggregateError([cause, rollbackError], "module load failed, and rolling it back threw too");
423
- }
424
- return cause;
425
- }
426
-
427
- async #rollbackFailedLoad(moduleRef: object, cause: unknown): Promise<unknown> {
428
- const pairs = this.#removeModuleBindings(moduleRef);
429
- try {
430
- await this.#deactivatePairs(pairs);
431
- } catch (rollbackError) {
432
- return new AggregateError([cause, rollbackError], "module load failed, and rolling it back threw too");
433
- }
434
- return cause;
435
- }
436
-
437
- async #loadOneModuleAsync(module: SyncModule | AsyncModule): Promise<void> {
438
- const moduleRef = module as object;
439
- const moduleRefs = (this.#moduleRefs ??= new Map<object, number>());
440
- const existing = moduleRefs.get(moduleRef);
441
- if (existing !== undefined) {
442
- moduleRefs.set(moduleRef, existing + 1);
443
- return;
444
- }
445
- moduleRefs.set(moduleRef, 1);
446
-
447
- if (isSyncModule(module)) {
448
- const builder = this.#createModuleBuilder(moduleRef);
449
- try {
450
- module[MODULE_SETUP](builder);
451
- } catch (error) {
452
- throw this.#rollbackFailedLoadSync(moduleRef, error);
453
- }
454
- } else {
455
- const importPromises: Array<Promise<void>> = [];
456
- const builder = this.#createAsyncModuleBuilder(moduleRef, importPromises);
457
- try {
458
- await module[MODULE_SETUP](builder);
459
- // Await nested async imports triggered inside the setup callback
460
- if (importPromises.length > 0) {
461
- await Promise.all(importPromises);
462
- }
463
- } catch (error) {
464
- // Nested imports may still be registering — let them settle before rolling back, so no
465
- // registration lands after the rejection and none becomes an unhandled rejection.
466
- await Promise.allSettled(importPromises);
467
- throw await this.#rollbackFailedLoad(moduleRef, error);
468
- }
469
- }
470
- }
471
-
472
- #createModuleBuilder(moduleRef: object): ModuleBuilder {
473
- const registration = this.#moduleRegistration(moduleRef);
474
- return {
475
- bind: <Value>(token: Token<Value> | Constructor<Value>): BindToBuilder<Value> =>
476
- this.#createBindToBuilder(token, registration),
477
- import: (...modules: Array<SyncModule>): void => {
478
- this.#loadSyncModules(modules);
479
- },
480
- };
481
- }
482
-
483
- #createAsyncModuleBuilder(moduleRef: object, importPromises: Array<Promise<void>>): AsyncModuleBuilder {
484
- const registration = this.#moduleRegistration(moduleRef);
485
- return {
486
- bind: <Value>(token: Token<Value> | Constructor<Value>): BindToBuilder<Value> =>
487
- this.#createBindToBuilder(token, registration),
488
- import: (...modules: Array<SyncModule | AsyncModule>): void => {
489
- for (const module of modules) {
490
- importPromises.push(this.#loadOneModuleAsync(module));
491
- }
492
- },
493
- };
494
- }
495
-
496
- unload(...modules: Array<SyncModule>): void {
497
- this.#assertNotDisposed();
498
- for (const module of modules) {
499
- this.#unloadModuleSync(module as object);
500
- }
501
- }
502
-
503
- /** Unregister module bindings and collect [binding, instance] pairs for deactivation. */
504
- #removeModuleBindings(ref: object): Array<[Binding, unknown]> {
505
- this.#moduleRefs?.delete(ref);
506
- const ids = this.#moduleBindingIds?.get(ref) ?? [];
507
- this.#moduleBindingIds?.delete(ref);
508
- const removed: Array<Binding> = [];
509
- for (const id of ids) {
510
- const binding = this.#registry.removeById(id);
511
- if (binding !== undefined) {
512
- removed.push(binding);
513
- }
514
- }
515
- return this.#drainSingletons(removed);
516
- }
517
-
518
- #unloadModuleSync(ref: object): void {
519
- const count = this.#moduleRefs?.get(ref) ?? 0;
520
- if (count <= 1) {
521
- this.#deactivatePairsSync(this.#removeModuleBindings(ref));
522
- } else {
523
- this.#moduleRefs!.set(ref, count - 1);
524
- }
525
- }
526
-
527
- async unloadAsync(...modules: Array<SyncModule | AsyncModule>): Promise<void> {
528
- this.#assertNotDisposed();
529
- for (const module of modules) {
530
- await this.#unloadModuleAsync(module as object);
531
- }
532
- }
533
-
534
- async #unloadModuleAsync(ref: object): Promise<void> {
535
- const count = this.#moduleRefs?.get(ref) ?? 0;
536
- if (count <= 1) {
537
- await this.#deactivatePairs(this.#removeModuleBindings(ref));
538
- } else {
539
- this.#moduleRefs!.set(ref, count - 1);
540
- }
541
- }
542
-
543
- loadAutoRegistered(registry: AutoRegisterRegistry): number {
544
- this.#assertNotDisposed();
545
- const entries = registry.entries();
546
- for (const { target, scope } of entries) {
547
- APPLY_BINDING_SCOPE[scope](this.#createBindToBuilder(target).toSelf());
548
- }
549
- return entries.length;
550
- }
551
-
552
- // ── Lifecycle hooks ────────────────────────────────────────────────────────────────────────────────────────────────
553
-
554
- onActivation<Value>(token: Token<Value> | Constructor<Value>, handler: ActivationHandler<Value>): void {
555
- this.#assertNotDisposed();
556
- this.#lifecycle.registerActivation(token, handler);
557
- }
558
-
559
- onDeactivation<Value>(token: Token<Value> | Constructor<Value>, handler: DeactivationHandler<Value>): void {
560
- this.#assertNotDisposed();
561
- this.#lifecycle.registerDeactivation(token, handler);
562
- }
563
-
564
- // ── Resolution ─────────────────────────────────────────────────────────────────────────────────────────────────────
565
-
566
- resolve<Value>(token: Token<Value> | Constructor<Value>, options?: ResolveOptions): Value {
567
- this.#assertNotDisposed();
568
- const rootStack = this.#resolver.rootStack;
569
- // A resolve already holding the shared stack means this one is nested; it mints its own.
570
- if (rootStack.length !== 0) {
571
- return options === undefined
572
- ? this.#resolver.resolveFromContext(token, [])
573
- : this.#resolver.resolve(token, options, []);
574
- }
575
- if (options === undefined) {
576
- return this.#resolver.resolveFromContext(token, rootStack);
577
- }
578
- return this.#resolver.resolve(token, options, rootStack);
579
- }
580
-
581
- resolveAsync<Value>(token: Token<Value> | Constructor<Value>, options?: ResolveOptions): Promise<Value> {
582
- this.#assertNotDisposed();
583
- if (options === undefined) {
584
- return this.#resolver.resolveAsyncFromRoot(token) as Promise<Value>;
585
- }
586
- return this.#resolver.resolveAsync(token, options, [], ROOT_BRANCH);
587
- }
588
-
589
- resolveOptional<Value>(token: Token<Value> | Constructor<Value>, options?: ResolveOptions): Value | undefined {
590
- this.#assertNotDisposed();
591
- const rootStack = this.#resolver.rootStack;
592
- return rootStack.length === 0
593
- ? this.#resolver.resolveOptional(token, options, rootStack)
594
- : this.#resolver.resolveOptional(token, options, []);
595
- }
596
-
597
- resolveOptionalAsync<Value>(
598
- token: Token<Value> | Constructor<Value>,
599
- options?: ResolveOptions,
600
- ): Promise<Value | undefined> {
601
- this.#assertNotDisposed();
602
- return this.#resolver.resolveOptionalAsync(token, options, [], ROOT_BRANCH);
603
- }
604
-
605
- resolveAll<Value>(token: Token<Value> | Constructor<Value>, options?: ResolveOptions): Array<Value> {
606
- this.#assertNotDisposed();
607
- const rootStack = this.#resolver.rootStack;
608
- return rootStack.length === 0
609
- ? this.#resolver.resolveAll(token, options, rootStack)
610
- : this.#resolver.resolveAll(token, options, []);
611
- }
612
-
613
- resolveAllAsync<Value>(token: Token<Value> | Constructor<Value>, options?: ResolveOptions): Promise<Array<Value>> {
614
- this.#assertNotDisposed();
615
- return this.#resolver.resolveAllAsync(token, options, [], ROOT_BRANCH);
616
- }
617
-
618
- // ── Child ──────────────────────────────────────────────────────────────────────────────────────────────────────────
619
-
620
- createChild(): Container {
621
- this.#assertNotDisposed();
622
- return new DefaultContainer(this);
623
- }
624
-
625
- // ── Dispose ────────────────────────────────────────────────────────────────────────────────────────────────────────
626
-
627
- dispose(): Promise<void> {
628
- // One teardown, shared: a second caller awaits the same run instead of returning early while
629
- // hooks from the first are still executing.
630
- return (this.#disposePromise ??= this.#runDispose());
631
- }
632
-
633
- async #runDispose(): Promise<void> {
634
- this.#disposed = true;
635
- // Refuses new materializations immediately; in-flight ones are drained just below.
636
- this.#scope.markClosed();
637
- await this.#scope.settleInflight();
638
-
639
- const reader = this.#getMetadataReader();
640
- const errors: Array<unknown> = [];
641
- // Iterate a copy: a deactivation handler is user code, and the live list is what
642
- // materializing or dropping a singleton mutates. Reverse materialization order, so a
643
- // dependent tears down before the dependencies it may still reach through.
644
- const cached = this.#scope.cachedSingletons().slice();
645
- for (let index = cached.length - 1; index >= 0; index -= 1) {
646
- const binding = cached[index]!;
647
- try {
648
- await this.#lifecycle.runDeactivation(binding, binding.instance, reader);
649
- } catch (error) {
650
- errors.push(error);
651
- }
652
- }
653
- // A constant never reaches the singleton cache unless activation put it there, so its hook is
654
- // owed from the registry instead — and only a container that has held one pays for looking.
655
- if (this.#registry.hasHeldConstantBinding) {
656
- for (const binding of this.#registry.allBindings()) {
657
- if (binding.instance === NO_INSTANCE && this.#owesConstantDeactivation(binding)) {
658
- try {
659
- await this.#lifecycle.runDeactivation(binding, binding.value, reader);
660
- } catch (error) {
661
- errors.push(error);
662
- }
663
- }
664
- }
665
- }
666
-
667
- this.#scope.clearAll();
668
- throwCollected(errors, "dispose() completed, but deactivation hooks threw");
669
- }
670
-
671
- [Symbol.asyncDispose](): Promise<void> {
672
- return this.dispose();
673
- }
674
-
675
- [Symbol.dispose](): never {
676
- throw new SyncDisposalNotSupportedError();
677
- }
678
-
679
- // ── Initialization ─────────────────────────────────────────────────────────────────────────────────────────────────
680
-
681
- async initializeAsync(): Promise<void> {
682
- this.#assertNotDisposed();
683
- const allBindings = this.#registry.allBindings();
684
- for (const binding of allBindings) {
685
- if (binding.kind === "alias") {
686
- continue;
687
- }
688
- const scope = effectiveBindingScope(binding);
689
- if (scope === "singleton" && binding.instance === NO_INSTANCE) {
690
- if (binding.predicate !== undefined) {
691
- continue;
692
- }
693
- // A constant is already its own instance, so only an activation hook gives the warm-up
694
- // something to run — and a container-level hook counts as one just as a per-binding hook does.
695
- if (
696
- binding.kind === "constant" &&
697
- binding.onActivation === undefined &&
698
- !this.#lifecycle.hasActivationHandlers(binding.token)
699
- ) {
700
- continue;
701
- }
702
- const slotOptions = bindingSlotToResolveOptions(binding.slot);
703
- await this.resolveAsync(binding.token, slotOptions);
704
- }
705
- }
706
- }
707
-
708
- // ── Validate ───────────────────────────────────────────────────────────────────────────────────────────────────────
709
-
710
- validate(): void {
711
- this.#assertNotDisposed();
712
- const reader = this.#getMetadataReader();
713
- const allBindings = this.#registry.allBindings();
714
-
715
- for (const binding of allBindings) {
716
- if (!this.#isSingletonStaticAnalyzableBinding(binding)) {
717
- continue;
718
- }
719
- this.#validateSingletonBindingGraph(binding, reader);
720
- }
721
-
722
- for (const [hookToken, phase] of this.#lifecycle.hookedTokens()) {
723
- if (!this.#isBoundInChain(hookToken)) {
724
- throw new UnreachableLifecycleHookError(tokenName(hookToken), phase);
725
- }
726
- }
727
-
728
- this.#validateConstraintRequirements(allBindings);
729
- }
730
-
731
- /** A constraint waiting on a slot name no binding declares can never hold. */
732
- #validateConstraintRequirements(allBindings: ReadonlyArray<Binding>): void {
733
- let declaredSlotNames: Set<string> | undefined;
734
-
735
- for (const binding of allBindings) {
736
- const { predicate } = binding;
737
- if (predicate === undefined) {
738
- continue;
739
- }
740
- const requirements = constraintRequirementsOf(predicate);
741
- if (requirements.length === 0) {
742
- continue;
743
- }
744
- declaredSlotNames ??= this.#slotNamesInChain();
745
- for (const requirement of requirements) {
746
- if (!declaredSlotNames.has(requirement.name)) {
747
- throw new UnreachableConstraintError(tokenName(binding.token), requirement.name, requirement.helperName);
748
- }
749
- }
750
- }
751
- }
752
-
753
- /** Every slot name declared anywhere a resolve through this container could reach. */
754
- #slotNamesInChain(): Set<string> {
755
- const names = this.#parent === undefined ? new Set<string>() : this.#parent.#slotNamesInChain();
756
- for (const binding of this.#registry.allBindings()) {
757
- if (binding.slot.name !== undefined) {
758
- names.add(binding.slot.name);
759
- }
760
- }
761
- return names;
762
- }
763
-
764
- // Ancestors count: a parent-owned binding is one this container can still resolve through.
765
- #isBoundInChain(token: Token<unknown> | Constructor): boolean {
766
- if (this.#registry.has(token)) {
767
- return true;
768
- }
769
- const parent = this.#parent;
770
- return parent !== undefined && parent.#isBoundInChain(token);
771
- }
772
-
773
- #isSingletonStaticAnalyzableBinding(binding: Binding): boolean {
774
- if (effectiveBindingScope(binding) !== "singleton") {
775
- return false;
776
- }
777
- return binding.kind === "class" || binding.kind === "resolved" || binding.kind === "resolved-async";
778
- }
779
-
780
- /**
781
- * DFS over explicit constructor / `toResolved*` dependency edges. Follows `toAlias` chains to the
782
- * terminal binding for scope checks.
783
- *
784
- * @remarks A `toDynamic*` dependency is scope-checked like any other — its declared scope is what
785
- * makes it captive — but the DFS does not descend into the factory, whose body is opaque.
786
- */
787
- #validateSingletonBindingGraph(root: Binding, reader: MetadataReader): void {
788
- const rootName = tokenName(root.token);
789
-
790
- const dfs = (current: Binding, pathNames: Array<string>, pathBindingIds: Set<BindingIdentifier>): void => {
791
- if (pathBindingIds.has(current.id)) {
792
- return;
793
- }
794
- const extendedPathIds = new Set(pathBindingIds);
795
- extendedPathIds.add(current.id);
796
-
797
- for (const edge of this.#collectStaticDependencyEdges(current, reader)) {
798
- const { terminal, depTokenName } = edge;
799
- const depScope = this.#validationScopeFromTerminal(terminal);
800
- if (depScope !== "singleton") {
801
- throw new ScopeViolationError({
802
- consumerToken: rootName,
803
- consumerScope: "singleton",
804
- dependencyToken: depTokenName,
805
- dependencyScope: depScope,
806
- path: [...pathNames, depTokenName],
807
- });
808
- }
809
- if (terminal.kind === "class" || terminal.kind === "resolved" || terminal.kind === "resolved-async") {
810
- dfs(terminal, [...pathNames, depTokenName], extendedPathIds);
811
- }
812
- }
813
- };
814
-
815
- dfs(root, [rootName], new Set());
816
- }
817
-
818
- /**
819
- * The scope this edge is judged against.
820
- *
821
- * @remarks A factory's *body* is not statically analyzable, but the scope it was bound with is
822
- * declared like any other — so the captive-dependency check applies to it too. The DFS still
823
- * refuses to descend into a factory; only the edge is judged.
824
- */
825
- #validationScopeFromTerminal(terminal: Binding): BindingScope {
826
- if (terminal.kind === "alias") {
827
- throw new InternalError("validate: expected terminal binding after alias resolution");
828
- }
829
- return terminal.scope;
830
- }
831
-
832
- #followAliasChainToTerminal(binding: Binding, options: ResolveOptions | undefined): Binding | undefined {
833
- const cyclePath: Array<string> = [];
834
- const seenAliasIds = new Set<BindingIdentifier>();
835
- let current: Binding | undefined = binding;
836
-
837
- while (current !== undefined && current.kind === "alias") {
838
- if (seenAliasIds.has(current.id)) {
839
- throw new CircularDependencyError(cyclePath);
840
- }
841
- seenAliasIds.add(current.id);
842
- cyclePath.push(tokenName(current.token));
843
- const nextToken = current.target;
844
- const next = this.#resolver.peekBindingForValidate(nextToken, options);
845
- if (next === undefined) {
846
- return undefined;
847
- }
848
- current = next.binding;
849
- }
850
- return current;
851
- }
852
-
853
- /** What one dependency could resolve to: every candidate for `injectAll`, else at most one. */
854
- #peekDependencyCandidates(dep: DependencySlot, options: ResolveOptions | undefined): ReadonlyArray<Binding> {
855
- if (dep.multi) {
856
- return this.#resolver.peekCandidateBindingsForValidate(dep.token, options);
857
- }
858
- const found = this.#resolver.peekBindingForValidate(dep.token, options);
859
- return found === undefined ? [] : [found.binding];
860
- }
861
-
862
- /** What a binding declares up front — a class's params, a factory's descriptors, else nothing. */
863
- #staticDependencies(binding: Binding, reader: MetadataReader): ReadonlyArray<DependencySlot> {
864
- if (binding.kind === "class") {
865
- return reader.getConstructorMetadata(binding.target)?.params ?? [];
866
- }
867
- if (binding.kind === "resolved" || binding.kind === "resolved-async") {
868
- return binding.deps;
869
- }
870
- return [];
871
- }
872
-
873
- #collectStaticDependencyEdges(
874
- binding: Binding,
875
- reader: MetadataReader,
876
- ): Array<{ terminal: Binding; depTokenName: string }> {
877
- const edges: Array<{ terminal: Binding; depTokenName: string }> = [];
878
-
879
- for (const dep of this.#staticDependencies(binding, reader)) {
880
- // An optional dependency imposes no scope constraint: it may legitimately be absent.
881
- if (dep.optional) {
882
- continue;
883
- }
884
- const depOptions = injectionSlotToResolveOptions(dep);
885
- for (const candidate of this.#peekDependencyCandidates(dep, depOptions)) {
886
- const terminal = this.#followAliasChainToTerminal(candidate, depOptions);
887
- if (terminal !== undefined) {
888
- edges.push({ terminal, depTokenName: tokenName(terminal.token) });
889
- }
890
- }
891
- }
892
-
893
- return edges;
894
- }
895
-
896
- // ── Introspection ──────────────────────────────────────────────────────────────────────────────────────────────────
897
-
898
- has(token: Token<unknown> | Constructor, options?: ResolveOptions): boolean {
899
- this.#assertNotDisposed();
900
- return this.#getInspector().has(token, options, () => this.#parent?.has(token, options) ?? false);
901
- }
902
-
903
- hasOwn(token: Token<unknown> | Constructor, options?: ResolveOptions): boolean {
904
- this.#assertNotDisposed();
905
- return this.#getInspector().hasOwn(token, options);
906
- }
907
-
908
- lookupBindings<Value>(token: Token<Value> | Constructor<Value>): ReadonlyArray<BindingSnapshot> {
909
- this.#assertNotDisposed();
910
- return this.#getInspector().lookupBindings(token);
911
- }
912
-
913
- inspect(): ContainerSnapshot {
914
- this.#assertNotDisposed();
915
- return this.#getInspector().inspect();
916
- }
917
-
918
- generateDependencyGraph(options?: GraphOptions): ContainerGraphJson {
919
- this.#assertNotDisposed();
920
- return buildDependencyGraph(
921
- this.#registry,
922
- this.#getMetadataReader(),
923
- options,
924
- this.#parent === undefined ? undefined : this.#parent.#registry,
925
- );
926
- }
927
-
928
- // ── Internal ───────────────────────────────────────────────────────────────────────────────────────────────────────
929
-
930
- #assertNotDisposed(): void {
931
- if (this.#disposed) {
932
- throw new DisposedContainerError();
933
- }
934
- }
935
- }
936
-
937
- /** Reports collected hook failures once teardown finished: the one error as itself, several aggregated. */
938
- function throwCollected(errors: ReadonlyArray<unknown>, message: string): void {
939
- if (errors.length === 0) {
940
- return;
941
- }
942
- if (errors.length === 1) {
943
- throw errors[0];
944
- }
945
- throw new AggregateError(errors, message);
946
- }
947
-
948
- // ── Container static ─────────────────────────────────────────────────────────────────────────────────────────────────
949
-
950
- /**
951
- * The companion factory that creates containers, empty or preloaded from modules.
952
- *
953
- * @since 0.3.16-canary.0
954
- */
955
- export const Container: ContainerStatic = {
956
- create(options?: ContainerOptions): Container {
957
- return new DefaultContainer(undefined, options);
958
- },
959
-
960
- // Variadic modules leave no room for an options argument. A container that needs both is
961
- // `Container.create(options)` followed by `load(...)`, which is what these two do anyway.
962
- fromModules(...modules: Array<SyncModule>): Container {
963
- const container = new DefaultContainer();
964
- container.load(...modules);
965
- return container;
966
- },
967
-
968
- async fromModulesAsync(...modules: Array<SyncModule | AsyncModule>): Promise<Container> {
969
- const container = new DefaultContainer();
970
- await container.loadAsync(...modules);
971
- return container;
972
- },
973
- };